Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@ live mode reports actual Ready pod counts from the authorized kubectl context. E
failure includes its evidence and an exact remediation. The platform demo commands will
not declare success unless this panel reports **Ready to present**.

Select **How it works** in Sentinel's header for the unified 30-second explanation. The
full-screen flow introduces Argus observation, the SOG shared model, Sentinel/OpenAI
decision support, human governance, and Phoenix recovery plus verification. It includes
a ready-to-speak judge narration, explicit autonomous/high-risk boundaries, and all four
evidence-provenance labels. The overlay is responsive and closes with its button, the
backdrop, or `Escape`.

### Sentinel-only development

Run the Sentinel Operations Graph service first, then:
Expand Down
36 changes: 35 additions & 1 deletion backend/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,23 +166,57 @@ def _correlated_incidents(timeline: list[dict]) -> list[dict]:
proof_record = next((item for item in reversed(ordered)
if isinstance(item.get("payload", {}).get("lifecycle"), list)), None)
proof = None
proof_payload = {}
if proof_record:
payload = proof_record.get("payload", {})
proof_payload = payload
proof = {
"lifecycle": payload.get("lifecycle", []),
"metrics": payload.get("metrics", {}),
"evidence_source": payload.get("evidence_source"),
"experiment_id": payload.get("scenario_id"),
}
entity = next((item.get("entity_name") for item in ordered if item.get("entity_name")), None)
entity_id = next((item.get("entity_id") for item in ordered if item.get("entity_id")), None)
argus_record = next((item for item in ordered if _source(item) == "argus"), {})
phoenix_record = next((item for item in reversed(ordered) if _source(item) == "phoenix"), {})
argus_payload = argus_record.get("payload", {})
phoenix_payload = phoenix_record.get("payload", {})
lifecycle = proof_payload.get("lifecycle", [])

def stage_evidence(stage: str, fallback: str) -> str:
record = next((item for item in lifecycle if item.get("stage") == stage), None)
return str(record.get("evidence")) if record and record.get("evidence") else fallback

detection = str(argus_record.get("summary") or _summary(argus_record) or "Detection detail not supplied")
recovery = str(phoenix_record.get("summary") or _summary(phoenix_record) or "Recovery detail not supplied")
resource = str(entity or entity_id or "Unmapped resource")
root_cause = (argus_payload.get("root_cause") or argus_payload.get("causal_chain")
or phoenix_payload.get("root_cause") or "Not established by the supplied evidence")
report = {
"executive_summary": f"{detection.rstrip('.')}. Phoenix reported: {recovery.rstrip('.')}.",
"detection": detection,
"affected_resource": resource,
"impact": str(argus_payload.get("impact") or
f"{severity.capitalize()} evidence was attached to {resource}; no wider impact is claimed without supporting evidence."),
"root_cause": str(root_cause),
"decision": stage_evidence("decision", "No explicit decision record was supplied"),
"governance": stage_evidence("human_approval", "Approval evidence was not supplied"),
"recovery": stage_evidence("recovery", recovery),
"verification": stage_evidence("verification", "Verification evidence was not supplied"),
"operator_next_step": str(phoenix_payload.get("operator_next_step") or
"Review the supporting evidence and keep the resource under observation."),
"evidence_source": str(proof_payload.get("evidence_source") or
" + ".join(sorted({str(item.get("provenance") or "observed") for item in ordered}))),
}
incidents.append({
"incident_id": f"corr:{correlation_id}", "correlation_id": correlation_id,
"title": f"Argus → Phoenix lifecycle{f' for {entity}' if entity else ''}",
"status": "resolved" if terminal else "open", "severity": severity,
"started_at": ordered[0].get("timestamp"), "updated_at": ordered[-1].get("timestamp"),
"sources": sorted(sources), "evidence_count": len(ordered), "timeline": ordered,
"provenance": sorted({str(item.get("provenance") or "observed") for item in ordered}),
"proof": proof,
"proof": proof, "report": report,
})
return sorted(incidents, key=lambda item: str(item.get("updated_at") or ""), reverse=True)

Expand Down
6 changes: 6 additions & 0 deletions backend/tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ def test_incident_requires_explicit_cross_agent_correlation():
assert incidents[0]["status"] == "resolved"
assert incidents[0]["sources"] == ["argus", "phoenix"]
assert [item["id"] for item in incidents[0]["timeline"]] == ["a", "p"]
assert incidents[0]["report"]["detection"] == "detected"
assert incidents[0]["report"]["recovery"] == "recovered"
assert incidents[0]["report"]["root_cause"] == "Not established by the supplied evidence"
assert "no wider impact is claimed" in incidents[0]["report"]["impact"]


def test_incident_exposes_only_supplied_proof_stages_and_metrics():
Expand All @@ -45,6 +49,8 @@ def test_incident_exposes_only_supplied_proof_stages_and_metrics():
assert [item["stage"] for item in incident["proof"]["lifecycle"]] == ["healthy", "verification"]
assert incident["proof"]["metrics"]["availability_percent"] == 99.8
assert incident["proof"]["experiment_id"] == "chaos-1"
assert incident["report"]["verification"] == "Verification evidence was not supplied"
assert incident["report"]["evidence_source"] == "Falco + HTTP probe"


@pytest.mark.asyncio
Expand Down
Loading