Spaces:
Build error
Build error
Upload folder using huggingface_hub
Browse files- README.md +1 -1
- app/api/routes_admin.py +5 -5
- app/api/routes_incidents.py +26 -47
- docs/authentication.md +1 -1
- docs/docs_endpoints.md +2 -2
- tests/test_incidents.py +6 -1
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": "
|
| 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(
|
| 25 |
if not ADMIN_API_KEY:
|
| 26 |
raise HTTPException(status_code=403, detail="Admin API is not configured")
|
| 27 |
-
if not secrets.compare_digest(
|
| 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
|
| 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 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
| 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
|
|
|
|
| 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.
|
| 97 |
-
``
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
3.
|
| 101 |
-
4.
|
| 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
|
| 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 |
-
|
| 160 |
-
engine=risk_engine,
|
| 161 |
-
intent=oss_intent,
|
| 162 |
-
cost_estimate=None,
|
| 163 |
-
policy_violations=[],
|
| 164 |
-
)
|
| 165 |
|
| 166 |
# ------------------------------------------------------------------
|
| 167 |
-
# Step
|
| 168 |
# ------------------------------------------------------------------
|
| 169 |
optimal_action = (
|
| 170 |
HealingAction.RESTART_CONTAINER
|
| 171 |
-
if
|
| 172 |
else HealingAction.NO_ACTION
|
| 173 |
)
|
| 174 |
|
| 175 |
# ------------------------------------------------------------------
|
| 176 |
-
# Step
|
| 177 |
# ------------------------------------------------------------------
|
| 178 |
causal_explainer = CausalExplainer()
|
| 179 |
current_state = {
|
|
@@ -187,19 +166,19 @@ async def evaluate_incident(
|
|
| 187 |
)
|
| 188 |
|
| 189 |
# ------------------------------------------------------------------
|
| 190 |
-
# Step
|
| 191 |
# ------------------------------------------------------------------
|
| 192 |
healing_intent = {
|
| 193 |
"action": optimal_action.value,
|
| 194 |
"component": event.component,
|
| 195 |
"parameters": {},
|
| 196 |
"justification": (
|
| 197 |
-
f"
|
| 198 |
f"Causal: {causal_exp.explanation_text}"
|
| 199 |
),
|
| 200 |
-
"confidence":
|
| 201 |
-
"risk_score":
|
| 202 |
-
"status": "
|
| 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 |
-
"
|
| 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 backward‑compatible 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 `
|
| 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
|
| 149 |
else NO_ACTION
|
| 150 |
```
|
| 151 |
|
| 152 |
In the implementation, this is encoded as:
|
| 153 |
|
| 154 |
-
- `restart_container` when
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"
|