petter2025 commited on
Commit
926fcdb
·
verified ·
1 Parent(s): 450072f

Upload folder using huggingface_hub

Browse files
README.md CHANGED
@@ -98,7 +98,7 @@ curl -X POST "http://localhost:8000/api/v1/v1/incidents/evaluate" -H "Content-
98
  "justification": "Causal: If we apply restart_container instead of no_action, latency would change from 600.00 to 510.00 (Δ = -90.00). Based on heuristic causal model.",
99
  "confidence": 0.85,
100
  "risk_score": 0.54,
101
- "status": "success"
102
  },
103
  "causal_explanation": {
104
  "factual_outcome": 600,
 
98
  "justification": "Causal: If we apply restart_container instead of no_action, latency would change from 600.00 to 510.00 (Δ = -90.00). Based on heuristic causal model.",
99
  "confidence": 0.85,
100
  "risk_score": 0.54,
101
+ "status": "oss_advisory_only"
102
  },
103
  "causal_explanation": {
104
  "factual_outcome": 600,
app/api/routes_admin.py CHANGED
@@ -2,7 +2,7 @@
2
  Admin API endpoints for API key management and audit logs.
3
  These endpoints should be protected (e.g., by an admin API key) in production.
4
  """
5
- from fastapi import APIRouter, Depends, HTTPException, Query, Path, Body, Request
6
  from pydantic import BaseModel
7
  from typing import Optional
8
  from datetime import datetime
@@ -21,10 +21,10 @@ router = APIRouter(prefix="/admin", tags=["admin"])
21
  ADMIN_API_KEY = os.getenv("ARF_ADMIN_API_KEY")
22
 
23
 
24
- def verify_admin(admin_key: str = Query(..., alias="admin_key")):
25
  if not ADMIN_API_KEY:
26
  raise HTTPException(status_code=403, detail="Admin API is not configured")
27
- if not secrets.compare_digest(admin_key, ADMIN_API_KEY):
28
  raise HTTPException(status_code=403, detail="Invalid admin key")
29
  return True
30
 
@@ -73,7 +73,7 @@ async def create_api_key(req: CreateKeyRequest, db: Session = Depends(get_db)):
73
 
74
 
75
  @router.get("/keys", dependencies=[Depends(verify_admin)])
76
- async def list_api_keys(limit: int = 100, offset: int = 0):
77
  """Lists keys by a non-secret `key_id` (the key's pepper-HMAC lookup
78
  hash), never the plaintext key -- there is no plaintext key to show
79
  since the H-2 fix (api_keys are hashed at rest). Use `key_id` in the
@@ -184,7 +184,7 @@ async def get_audit_logs(
184
  api_key: str = Path(..., description="The API key to audit"),
185
  start_date: Optional[str] = Query(None),
186
  end_date: Optional[str] = Query(None),
187
- limit: int = 100,
188
  ):
189
  start = datetime.fromisoformat(start_date) if start_date else None
190
  end = datetime.fromisoformat(end_date) if end_date else None
 
2
  Admin API endpoints for API key management and audit logs.
3
  These endpoints should be protected (e.g., by an admin API key) in production.
4
  """
5
+ from fastapi import APIRouter, Depends, HTTPException, Query, Path, Body, Request, Header
6
  from pydantic import BaseModel
7
  from typing import Optional
8
  from datetime import datetime
 
21
  ADMIN_API_KEY = os.getenv("ARF_ADMIN_API_KEY")
22
 
23
 
24
+ def verify_admin(x_admin_key: str = Header(..., alias="X-Admin-Key")):
25
  if not ADMIN_API_KEY:
26
  raise HTTPException(status_code=403, detail="Admin API is not configured")
27
+ if not secrets.compare_digest(x_admin_key, ADMIN_API_KEY):
28
  raise HTTPException(status_code=403, detail="Invalid admin key")
29
  return True
30
 
 
73
 
74
 
75
  @router.get("/keys", dependencies=[Depends(verify_admin)])
76
+ async def list_api_keys(limit: int = Query(100, ge=0, le=1000), offset: int = Query(0, ge=0)):
77
  """Lists keys by a non-secret `key_id` (the key's pepper-HMAC lookup
78
  hash), never the plaintext key -- there is no plaintext key to show
79
  since the H-2 fix (api_keys are hashed at rest). Use `key_id` in the
 
184
  api_key: str = Path(..., description="The API key to audit"),
185
  start_date: Optional[str] = Query(None),
186
  end_date: Optional[str] = Query(None),
187
+ limit: int = Query(100, ge=0, le=1000),
188
  ):
189
  start = datetime.fromisoformat(start_date) if start_date else None
190
  end = datetime.fromisoformat(end_date) if end_date else None
app/api/routes_incidents.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- Incident evaluation endpoints — backward‑compatible Bayesian reroute.
3
 
4
  This module provides two incident‑related routes:
5
 
@@ -7,10 +7,12 @@ This module provides two incident‑related routes:
7
  Stores a ``ReliabilityEvent`` in an in‑memory history for auditing
8
  and debugging.
9
  * ``POST /api/v1/v1/incidents/evaluate`` **(deprecated)**
10
- Former heuristic endpoint now **rerouted to the full Bayesian risk
11
- engine**. All callers should migrate to
12
- ``POST /api/v1/intents/evaluate``, which returns richer metadata
13
- including CUDL uncertainty decomposition and decision traces.
 
 
14
 
15
  The local model duplicates (``ReliabilityEvent``, ``HealingAction``)
16
  have been removed; all types are imported from the canonical ARF core
@@ -84,7 +86,8 @@ async def evaluate_incident(
84
  quota: dict = Depends(enforce_quota),
85
  ) -> dict:
86
  """
87
- Evaluate an incident using the **Bayesian risk engine**.
 
88
 
89
  .. deprecated:: 0.6.0
90
  Use ``POST /api/v1/intents/evaluate`` instead. This endpoint
@@ -93,13 +96,12 @@ async def evaluate_incident(
93
 
94
  The following steps are performed:
95
 
96
- 1. Convert the ``ReliabilityEvent`` into a minimal
97
- ``DeployConfigurationIntent`` via ``intent_adapter``.
98
- 2. Call ``risk_service.evaluate_intent()`` to obtain a Bayesian
99
- risk score.
100
- 3. Generate a heuristic healing action based on the risk score.
101
- 4. Run the causal explainer for counterfactual text.
102
- 5. Build a backward‑compatible response envelope.
103
 
104
  Parameters
105
  ----------
@@ -135,45 +137,22 @@ async def evaluate_incident(
135
 
136
  try:
137
  # ------------------------------------------------------------------
138
- # Step 1 Convert the event into an infrastructure intent
139
- # ------------------------------------------------------------------
140
- from app.services.intent_adapter import to_oss_intent
141
- from app.services.risk_service import evaluate_intent
142
-
143
- raw_intent = {
144
- "intent_type": "deploy_config",
145
- "environment": "prod",
146
- "service_name": event.component,
147
- "requester": "auto",
148
- "change_scope": "global",
149
- "deployment_target": "prod",
150
- "configuration": {},
151
- "provenance": {"source": "incident_evaluate"},
152
- }
153
- oss_intent = to_oss_intent(raw_intent)
154
-
155
- # ------------------------------------------------------------------
156
- # Step 2 – Bayesian risk evaluation
157
  # ------------------------------------------------------------------
158
  risk_engine = request.app.state.risk_engine
159
- result = evaluate_intent(
160
- engine=risk_engine,
161
- intent=oss_intent,
162
- cost_estimate=None,
163
- policy_violations=[],
164
- )
165
 
166
  # ------------------------------------------------------------------
167
- # Step 3 Heuristic action selection based on risk threshold
168
  # ------------------------------------------------------------------
169
  optimal_action = (
170
  HealingAction.RESTART_CONTAINER
171
- if result["risk_score"] > 0.5
172
  else HealingAction.NO_ACTION
173
  )
174
 
175
  # ------------------------------------------------------------------
176
- # Step 4 Causal explainer
177
  # ------------------------------------------------------------------
178
  causal_explainer = CausalExplainer()
179
  current_state = {
@@ -187,19 +166,19 @@ async def evaluate_incident(
187
  )
188
 
189
  # ------------------------------------------------------------------
190
- # Step 5 Build response envelope
191
  # ------------------------------------------------------------------
192
  healing_intent = {
193
  "action": optimal_action.value,
194
  "component": event.component,
195
  "parameters": {},
196
  "justification": (
197
- f"Bayesian risk score: {result['risk_score']:.3f}. "
198
  f"Causal: {causal_exp.explanation_text}"
199
  ),
200
- "confidence": 1.0 - result.get("uncertainty", 0.0),
201
- "risk_score": result["risk_score"],
202
- "status": "success",
203
  }
204
 
205
  response_data = {
@@ -220,7 +199,7 @@ async def evaluate_incident(
220
  "best_action": optimal_action.value,
221
  "expected_utility": 0.5,
222
  "explanation": (
223
- "Decision based on Bayesian risk threshold > 0.5"
224
  ),
225
  },
226
  }
 
1
  """
2
+ Incident evaluation endpoints.
3
 
4
  This module provides two incident‑related routes:
5
 
 
7
  Stores a ``ReliabilityEvent`` in an in‑memory history for auditing
8
  and debugging.
9
  * ``POST /api/v1/v1/incidents/evaluate`` **(deprecated)**
10
+ Heuristic risk score computed directly from the reported event via
11
+ ``RiskEngine.compute_risk_from_event`` -- not the Bayesian
12
+ conjugate/HMC path used by intent evaluation. All callers should
13
+ migrate to ``POST /api/v1/intents/evaluate``, which returns richer
14
+ metadata including CUDL uncertainty decomposition and decision
15
+ traces.
16
 
17
  The local model duplicates (``ReliabilityEvent``, ``HealingAction``)
18
  have been removed; all types are imported from the canonical ARF core
 
86
  quota: dict = Depends(enforce_quota),
87
  ) -> dict:
88
  """
89
+ Evaluate an incident using a heuristic risk score computed directly
90
+ from the reported event's latency/error rate (not the Bayesian engine).
91
 
92
  .. deprecated:: 0.6.0
93
  Use ``POST /api/v1/intents/evaluate`` instead. This endpoint
 
96
 
97
  The following steps are performed:
98
 
99
+ 1. Compute a heuristic risk score directly from the reported event's
100
+ ``latency_p99``/``error_rate`` via
101
+ ``RiskEngine.compute_risk_from_event``.
102
+ 2. Generate a heuristic healing action based on that risk threshold.
103
+ 3. Run the causal explainer for counter‑factual text.
104
+ 4. Build a backwardcompatible response envelope.
 
105
 
106
  Parameters
107
  ----------
 
137
 
138
  try:
139
  # ------------------------------------------------------------------
140
+ # Step 1 - Heuristic risk score computed directly from the event
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  # ------------------------------------------------------------------
142
  risk_engine = request.app.state.risk_engine
143
+ risk_score = risk_engine.compute_risk_from_event(event)
 
 
 
 
 
144
 
145
  # ------------------------------------------------------------------
146
+ # Step 2 - Heuristic action selection based on risk threshold
147
  # ------------------------------------------------------------------
148
  optimal_action = (
149
  HealingAction.RESTART_CONTAINER
150
+ if risk_score > 0.5
151
  else HealingAction.NO_ACTION
152
  )
153
 
154
  # ------------------------------------------------------------------
155
+ # Step 3 - Causal explainer
156
  # ------------------------------------------------------------------
157
  causal_explainer = CausalExplainer()
158
  current_state = {
 
166
  )
167
 
168
  # ------------------------------------------------------------------
169
+ # Step 4 - Build response envelope
170
  # ------------------------------------------------------------------
171
  healing_intent = {
172
  "action": optimal_action.value,
173
  "component": event.component,
174
  "parameters": {},
175
  "justification": (
176
+ f"Risk score: {risk_score:.3f}. "
177
  f"Causal: {causal_exp.explanation_text}"
178
  ),
179
+ "confidence": 0.85,
180
+ "risk_score": risk_score,
181
+ "status": "oss_advisory_only",
182
  }
183
 
184
  response_data = {
 
199
  "best_action": optimal_action.value,
200
  "expected_utility": 0.5,
201
  "explanation": (
202
+ "Heuristic decision based on latency/error thresholds"
203
  ),
204
  },
205
  }
docs/authentication.md CHANGED
@@ -21,7 +21,7 @@ Current status
21
  `POST /report_incident` and `GET /history` were reading/writing two _different_ Python lists
22
  with the same name, so `GET /history` had in fact always returned empty regardless of what was
23
  reported; both routers now share the one list in `app.core.storage`.
24
- - `routes_admin.py`: individual `/admin/*` endpoints require an `admin_key` query parameter,
25
  verified against `ARF_ADMIN_API_KEY` (`app/api/deps.py`, or the local `verify_admin`
26
  dependency in that router). Also fails closed if unset. Includes
27
  `POST /admin/keys/{key_id}/rotate` — deactivates a key and issues a new one on the same
 
21
  `POST /report_incident` and `GET /history` were reading/writing two _different_ Python lists
22
  with the same name, so `GET /history` had in fact always returned empty regardless of what was
23
  reported; both routers now share the one list in `app.core.storage`.
24
+ - `routes_admin.py`: individual `/admin/*` endpoints require an `X-Admin-Key` header,
25
  verified against `ARF_ADMIN_API_KEY` (`app/api/deps.py`, or the local `verify_admin`
26
  dependency in that router). Also fails closed if unset. Includes
27
  `POST /admin/keys/{key_id}/rotate` — deactivates a key and issues a new one on the same
docs/docs_endpoints.md CHANGED
@@ -145,13 +145,13 @@ The endpoint uses the following rule to choose the action:
145
 
146
  ```text
147
  optimal_action = RESTART_CONTAINER
148
- if latency_p99 > 500 OR error_rate > 0.15
149
  else NO_ACTION
150
  ```
151
 
152
  In the implementation, this is encoded as:
153
 
154
- - `restart_container` when `latency_p99 > 500` or `error_rate > 0.15`
155
  - `no_action` otherwise
156
 
157
  No probabilistic policy or learned policy is involved.
 
145
 
146
  ```text
147
  optimal_action = RESTART_CONTAINER
148
+ if risk_score > 0.5
149
  else NO_ACTION
150
  ```
151
 
152
  In the implementation, this is encoded as:
153
 
154
+ - `restart_container` when the risk score (see below) is greater than `0.5`
155
  - `no_action` otherwise
156
 
157
  No probabilistic policy or learned policy is involved.
tests/test_incidents.py CHANGED
@@ -35,4 +35,9 @@ def test_evaluate_incident_deprecated():
35
  body = resp.json()
36
  assert "deprecation_notice" in body
37
  assert "healing_intent" in body
38
- assert body["healing_intent"]["risk_score"] is not None
 
 
 
 
 
 
35
  body = resp.json()
36
  assert "deprecation_notice" in body
37
  assert "healing_intent" in body
38
+ healing_intent = body["healing_intent"]
39
+ # latency_norm = min(1.0, 450/1000) = 0.45; risk = 0.45*0.7 + 0.12*0.3 = 0.351
40
+ assert healing_intent["risk_score"] == pytest.approx(0.351)
41
+ assert healing_intent["action"] == "no_action"
42
+ assert healing_intent["confidence"] == 0.85
43
+ assert healing_intent["status"] == "oss_advisory_only"