Base URL (dev): http://localhost:8000 · WebSocket: ws://localhost:8000/ws
CORS is wide open — a Stitch HTML export opened from a folder can call this directly.
Start the server: uvicorn orca.api:app --reload --port 8000
(deps: pip install fastapi 'uvicorn[standard]' pydantic websockets)
Every REST list and every WebSocket message is this JSON object (fields may be
null when not applicable):
{
"event_id": "8d3f…",
"trace_id": "T-4a1b2c3d",
"parent_id": "77aa…",
"agent_id": "procurement-01",
"framework": "orca",
"team": "supply-chain",
"timestamp": "2026-07-09T12:00:01.123456+00:00",
"event_type": "tool_call",
"status": "blocked",
"task": "Export vendor list to external address",
"action": {"tool": "vendor_db.export", "risk_level": "critical", "reversible": true},
"data_provenance": {"sources": ["doc:pid_042.pdf"], "tainted": true, "taint_source": "doc:pid_042.pdf"},
"cost": {"input_tokens": 900, "output_tokens": 60, "usd": 0.010},
"policy": {"verdict": "block", "rule_id": "tainted-input-dangerous-action", "tags": ["OWASP-LLM01"]},
"review": null,
"prev_hash": "…64 hex chars…",
"hash": "…64 hex chars…"
}event_type:agent_start | tool_call | handoff | decision | output | error | escalation | agent_endstatus:ok | blocked | escalated | flagged | failedoutputevents may carrycontent(the produced text) andreview({confidence, judge_verdict}from the Quality Gate) —status: "flagged"means the judge disagreed and a human should lookdecisionevents carryapproval({decision, reviewed_by}) — the audit answer to "who approved this?"- Judge config (server env):
JUDGE_PROVIDER=mock(default) orllmwithLLM_ENDPOINT,LLM_API_KEY,LLM_DEPLOYMENT policy.verdict:allow | block | escalate;policy.tagscarries threat labels likeOWASP-LLM01- Lineage: follow
parent_id→event_id(works across agents and sub-agents)
| Endpoint | Returns | Powers |
|---|---|---|
GET /agents |
registered agents + per-task tool allowlists | agent cards |
GET /events?trace_id=&agent_id=&limit= |
Event list (chronological) | feed, replay, per-agent filter |
GET /traces |
one summary row per workflow run (events, usd, blocked, escalated, agents) |
run picker, replay entry point |
GET /lineage?trace_id= |
{from, to, trace_id, task, timestamp} agent-level edges |
the DAG (react-flow) |
GET /costs |
{per_agent, per_team, per_task} USD roll-ups |
cost attribution charts |
GET /flagged |
Events with status blocked/escalated/flagged/failed | alerts panel |
GET /trace/{trace_id} |
{events, layers, agents} — everything for one run; layers = event_ids grouped by call-tree depth |
replay view + "one layer at a time" filter |
GET /traceback/{event_id} |
the event's ancestry, root first | "trace to root cause" click |
GET /blast/{event_id} |
all events triggered downstream | blast-radius highlight on the DAG |
GET /verify |
{chain_ok: bool, first_bad_event: id|null} |
"audit chain verified ✓" badge |
GET /approvals |
frozen Events awaiting a human — escalated tool calls AND judge-flagged outputs (action is null for outputs) |
approval modal list |
GET /report?trace_id= |
compliance report as JSON (omit trace_id for all runs) | compliance view |
GET /report.md?trace_id= |
same report as downloadable markdown | "download compliance report" button |
POST /approvals/{event_id} body {"decision": "approve"|"give_up"|"try_another", "reviewed_by": "name"} |
{ok: bool} — releases the frozen agent; reviewer lands in the audit chain |
approval modal buttons |
POST /demo/rogue |
launches a runaway + silent agent; tripwires fire live | demo trigger button |
WS /ws |
every new Event as it happens | everything live |
Tripwire alerts arrive as escalation events with status: "flagged" and
policy.rule_id of tripwire-token-budget (cumulative per trace),
tripwire-loop (same tool N× consecutively), or tripwire-silent-failure
(agent started, then silence). Cross-framework: events from guarded
LangGraph tools carry framework: "langgraph" and are enforced (blocked
calls never execute) — see orca/adapters.py.
- On page load, snapshot via REST (
/agents,/events,/costs,/flagged). - Open the WebSocket; every message is one Event — append to the feed,
add
cost.usdto that agent's total, and ifstatusisblocked/escalated, raise the alert UI. - Send any text on the socket every ~15s as a keep-alive.
The demo scenario re-runs every 45s, so there's always live data. A blocked
event with policy.tags containing OWASP-LLM01 is the demo moment — give
it the loudest visual treatment.