Skip to content

Architecture

BULWARK is a twelve-agent fleet for the Fortified Enterprise Fleet track: continuous third-party assurance built on an event-driven Agent Runtime, a shared control-evidence graph, per-agent zero-trust identity, Model Armor guardrails at every untrusted boundary, and a live kill switch. Seven agents implement the spec's Onboard/Watch/Attest loops; five more -- Contract Intelligence, Concentration Analyzer, Framework Crosswalk, the Offboarding Agent, and Executive Risk Digest -- go past the spec to attack real enterprise pain no per-vendor checklist review can reach: manual contract/DPA review, the hidden portfolio risk of vendors that look diversified but secretly share a subprocessor, the redundant evidence collection enterprises repeat across every overlapping compliance framework they carry, the DPA-mandated data-deletion deadline every vendor termination creates that otherwise lives in a spreadsheet if it's tracked at all, and the 30+ API endpoints of fleet state nobody has time to click through every week. Drift Sentinel additionally gained a fifth signal -- offboarding_overdue -- and a fourth, predictive one -- risk_trend_rising -- that flags a control's residual risk climbing across consecutive reassessments before any single one crosses a hard gap threshold (see "Beyond the spec" below).

System diagram

flowchart TB
    subgraph EXT["Untrusted Perimeter"]
        V1["Vendor artifacts\nSOC2 / ISO / DPA / MSA"]
        V2["Buyer questionnaires\nXLSX / CAIQ / SIG"]
    end

    subgraph GW["Agent Gateway (FastAPI ingress)"]
        AUTH["API key + rate limit"]
    end

    subgraph RT["Agent Runtime (ADK Runners)"]
        SUP[["Supervisor\nno tools"]]
        A1["Intake"]
        A2["Evidence Collector\ndeterministic, no LLM"]
        A3["Risk Assessor\nGemini Pro"]
        A4["Questionnaire Responder"]
        A5["Drift Sentinel"]
        A6["Remediation Router"]
        A7["Contract Intelligence"]
        A8["Concentration Analyzer\ndeterministic, no LLM"]
        A9["Framework Crosswalk\ndeterministic, no LLM"]
        A10["Offboarding Agent\ndeterministic, no LLM"]
        A11["Executive Risk Digest"]
    end

    subgraph BUS["Event Bus (Pub/Sub-shaped)"]
        T1["vendor.artifact.received"]
        T2["assertion.extracted"]
        T3["assessment.requested"]
        T4["finding.created"]
        T5["drift.detected"]
        T6["questionnaire.received"]
        T7["evidence.collected"]
        T8["answer.drafted"]
        T9["human.decision"]
        T10["contract.terms_extracted"]
        T11["subprocessors.extracted"]
        DLQ[("dead-letter")]
    end

    subgraph STATE["State"]
        FS[("Firestore-shaped graph\nvendors / assertions / controls\nevidence / findings")]
        MB[("Memory Bank\nper-vendor exceptions")]
        AL[("Audit Log\nreasoning-chain trace")]
    end

    subgraph GUARD["Model Armor"]
        MA["before/after_model_callback\nbefore_tool_callback"]
    end

    HITL["Human Review\n(decision packets, kill switch)"]

    V1 & V2 --> AUTH --> A1 & A7 & SUP
    A1 -- "hardcoded gate:\nuntrusted never reaches Supervisor" --> A1
    SUP --> A3 & A4

    A1 -->|publish| T2 --> T3 --> SUP
    A3 -->|publish| T4 --> A6
    A5 -->|publish| T5 --> T3
    A2 -->|publish| T7 --> A5
    A4 -->|publish| T8 --> HITL
    HITL -->|publish| T9 --> A6
    A7 -->|publish| T10
    A7 -->|publish| T11 --> A8
    BUS -.failures.-> DLQ

    A1 <--> MA
    A3 <--> MA
    A4 <--> MA
    A7 <--> MA
    A11 <--> MA
    MA --> GEMINI[["Gemini"]]

    A1 --> FS
    A2 --> FS
    A3 --> FS
    A4 --> FS
    A5 <--> MB
    A5 --> FS
    A6 --> FS
    A7 --> FS
    A8 --> FS
    A9 -.reads Findings on demand.-> FS
    A10 --> FS
    A5 -.reads OffboardingRecords\nfor offboarding_overdue.-> FS
    A11 -.reads findings/vendors/\nconcentration/offboarding.-> FS

    RT --> AL --> HITL
    A6 --> HITL
    HITL -->|"POST /fleet-config"| KILL["Autonomy ladder + kill switch"]
    KILL -.enforces.-> A1 & A2 & A3 & A4 & A5 & A6 & A7 & A8 & A9 & A10 & A11

The twelve agents

# Agent Model Trust zone Ceiling Why
1 Supervisor Flash internal L1 Classifies an internal-provenance event and delegates via ADK's native sub-agent transfer. Holds no tools -- nothing to compromise.
2 Intake Flash untrusted L1 The only agent that reads vendor-supplied documents. Extraction is claim-only: it can never write a finding, only an Assertion.
3 Evidence Collector none internal, read-only L3 Deliberately deterministic -- comparing an observed value to a policy is a lookup, not reasoning, so it costs zero tokens and has no prompt-injection surface at all.
4 Risk Assessor Pro internal L1 The one step that's genuine complex judgment (per the hackathon's own cost guidance: reserve Pro for that, not for rubber-stamping). Also the write path for AssessmentSnapshot's append-only history -- see "Beyond the spec" below.
5 Questionnaire Responder Flash egress-controlled L2 Answers with citations; abstains below a confidence threshold; every answer passes a DLP-style scan before it's marked exportable.
6 Drift Sentinel Flash internal L3 Long-running, checkpointed sweeps; reopening an assessment is fully reversible, which is what justifies L3 (autonomous). Its fourth signal, risk_trend_rising, is predictive rather than reactive, and its fifth, offboarding_overdue, is a hardcoded guard against clobbering an offboarding vendor's status -- see "Beyond the spec" below.
7 Remediation Router Flash internal-write L2 Opens tickets and builds decision packets freely; drafting vendor email requires a recorded human decision already existing on the finding -- checked in code, not by policy level alone -- and there is no send_email function anywhere in this codebase.
8 Contract Intelligence Flash untrusted L1 The second (and only other) agent that reads vendor-supplied documents -- MSAs/DPAs carry the same injection threat model as a SOC 2 report, so it gets the identical trust zone and Model Armor gate as Intake. Extracts clauses against a legal playbook and flags deviations (liability caps, breach-notification windows, audit rights, subprocessor flow-down, ...) instead of a paralegal re-deriving them by hand.
9 Concentration Analyzer none internal, read-only L3 Deliberately deterministic, same reasoning as Evidence Collector -- clustering subprocessor names shared across vendors is a graph lookup, not judgment. Catches the portfolio-level blind spot no single-vendor review can: multiple "independently diversified" vendors secretly depending on the same subprocessor, the pattern behind incidents like the 2024 CrowdStrike outage.
10 Framework Crosswalk none internal, read-only L3 Deliberately deterministic -- mapping a control to its equivalent in another framework is a table lookup, not judgment. Reports how much of a target framework (ISO 27001, NIST CSF) a vendor already satisfies via its existing SOC 2 findings, cutting redundant evidence collection across overlapping frameworks.
11 Offboarding Agent none internal-write L3 (init/detect) / L2 (confirm) Deliberately deterministic -- computing a deadline and comparing it to today is arithmetic, not judgment. Tracks the data-deletion clock every DPA's termination_assistance clause creates; confirm_data_deletion is gated one rung lower (L2) since it closes out a compliance obligation, not something that should be silently reversible.
12 Executive Risk Digest Flash internal L3 (gather) / L1 (publish) gather_digest_inputs is a deterministic read (L3, same shape as Framework Crosswalk's own analysis); the narrative synthesis itself is genuine LLM judgment, so publish_digest stays at L1 (Draft) -- a human-readable summary, not an autonomous action on anything.

Full per-agent resource grants (the zero-trust table) are in platform/identity.py; the registry entries with versions and department tags are in platform/registry.py.

Event topics (section 4.2)

Topic Producer Consumer This build
vendor.artifact.received Agent Gateway Intake agents/orchestrator.py::process_vendor_artifact
assertion.extracted Intake (routes to assessment.requested) orchestrator._on_assertion_extracted
assessment.requested Supervisor / Sentinel / API / Gateway Risk Assessor orchestrator._on_assessment_requested
finding.created Risk Assessor Remediation Router orchestrator._on_finding_created
drift.detected Drift Sentinel Supervisor (via assessment.requested) orchestrator._on_drift_detected
questionnaire.received Agent Gateway Questionnaire Responder orchestrator.submit_questionnaire
evidence.collected Evidence Collector Drift Sentinel orchestrator._on_evidence_collected -- runs the same run_drift_sweep the scheduled tick uses
answer.drafted Questionnaire Responder Human review queue published with {questionnaire_id, auto_pct, abstain_count}; no further automated reaction
human.decision HITL (POST /findings/{id}/decision, POST /decisions) Remediation Router orchestrator._on_human_decision -- this is what lets draft_vendor_email's human-decision gate actually pass
contract.terms_extracted Contract Intelligence (documented, no automated consumer in this build) published with {vendor_id, artifact_id}; a natural extension routes high/critical-risk deviations into Remediation Router the same way finding.created does, left out to keep this addition well-scoped
subprocessors.extracted Contract Intelligence Concentration Analyzer orchestrator._on_subprocessors_extracted -- re-runs the full-portfolio cluster check on every new disclosure
*.dlq (all) GET /dlq handled generically by platform/event_bus.py, not as separate named topics -- see that module's docstring

Every publish above uses platform/event_bus.py's Envelope (event_id, idempotency_key, trace_id, provenance, tenant, region_pin, attempt, payload) exactly as section 4.2 specifies.

The three loops, as actually wired

Onboard (agents/orchestrator.py): POST /vendors/artifacts → prescan_artifact (deterministic Model Armor scan) → if blocked, stop here, before any LLM call → if clean, Intake extracts Assertions → publish assertion.extracted → republish assessment.requested → Supervisor transfers to Risk Assessor → create_finding (citation validation gate) → publish finding.created → Remediation Router opens a ticket + decision packet.

Watch (agents/drift_sentinel.py): POST /drift-sentinel/tick (Cloud Scheduler in production) → run_drift_sweep checks artifact expiry, evidence drift, a mock breach feed, and a control's residual risk rising for three consecutive reassessments (all skipping controls covered by an active Memory Bank exception), checkpointing progress via the Run ledger → Drift Sentinel judges severity and calls reopen_assessment → publishes drift.detected → orchestrator republishes assessment.requested for every affected vendor, with no human or API call in between.

Attest (agents/questionnaire_responder.py): POST /questionnaires → Supervisor transfers to Questionnaire Responder → search_evidence (stands in for Vertex AI Vector Search) grounds each answer → draft_answer gates on confidence and a DLP-style scan before marking an answer auto.

Assure (agents/contract_intelligence.py, agents/concentration_analyzer.py): POST /vendors/artifacts with doc_type one of msa/dpa/contract/ sla/order form → same prescan_artifact Model Armor gate as Onboard → routes to Contract Intelligence instead of Intake → extract_contract_terms flags playbook deviations → extract_subprocessors publishes subprocessors.extracted → Concentration Analyzer re-clusters every subprocessor in the tenant's portfolio by normalized name and supersedes its prior ConcentrationRisk records with current state, with no human or API call in between.

Offboard (agents/offboarding.py): POST /vendors/{id}/offboard → initiate_offboarding sets vendor.status = "offboarding" and computes a data-deletion deadline from the vendor's own extracted termination_assistance clause (or the playbook default) → Drift Sentinel's sweep calls check_offboarding_overdue deterministically on every tick, same as its other three detection steps → a miss surfaces as an offboarding_overdue signal through the same LLM-judgment path every other Drift Sentinel signal uses, guarded so it never reopens an already-offboarding vendor into under_review → POST /vendors/{id}/offboard/confirm closes it out, terminal, not reversible.

Digest (agents/executive_digest.py): POST /digest/generate (or a weekly Cloud Scheduler cadence) → gather_digest_inputs deterministically pulls the week's most urgent state across every other agent's output → the LLM writes a 3-5 paragraph prioritized narrative grounded only in those inputs → publish_digest persists it → GET /digest/latest serves it without re-running anything.

No agent module imports another agent module anywhere in this codebase. Every handoff above is bus.publish/bus.subscribe on a named topic; tests/test_orchestrator_wiring.py asserts this mechanically, not just in the diagram.

Mechanisms worth reading in the code directly

The citation-validation gate (agents/risk_assessor.py, create_finding): a finding with zero evidence_ids/assertion_ids, or a citation to an id that doesn't actually exist in the graph, is rejected by a post-processor before it's ever persisted -- deterministic code, not a prompt telling the model to behave. tests/test_agents_tools.py exercises both rejection paths directly.

The kill switch (platform/policy.py): POST /fleet-config with {"autonomy_level": 0} and every L1+ tool call across all twelve agents starts raising AutonomyBlocked on its very next invocation -- no redeploy, no restart, checked against platform/models.py's fleet_config singleton on every single tool call. POST /fleet-config {"pause_agent_id": "drift-sentinel"} does the same for one agent without touching the rest of the fleet. This is meant to be demonstrated live, not just described.

The circuit breaker (platform/spend.py): every ADK Runner call in orchestrator._run extracts real usage_metadata (prompt/candidate token counts) off the event stream, accumulates them into a daily SpendLedger, and calls check_circuit_breaker() -- which, on breach of fleet_config.max_daily_token_spend, calls the exact same set_global_autonomy(0) the manual kill switch uses. "Breach -> all agents drop to L0" (section 8) is one function call, not a separate code path that could drift from the kill switch's actual behavior. The per-1K-token dollar rates are illustrative placeholders (labeled as such in the module docstring) -- what's real is the mechanism, not the number.

Rollback (platform/rollback.py): the genuinely reversible L3 actions -- reopen_assessment (Drift Sentinel), open_ticket (Remediation Router), and initiate_offboarding (Offboarding Agent) -- each write a CompensatingAction record (before value, after value) before returning. confirm_data_deletion deliberately does not: it certifies a real-world fact (the vendor's data is actually gone), and a rollback that flipped a database field back wouldn't undo that, so pretending it's reversible would be dishonest about what "rollback" means in this system. POST /runs/{trace_id}/rollback replays a trace's compensating actions most-recent-first, restores each field, and marks them rolled back (idempotent -- replaying twice is a no-op the second time). The grouping key is trace_id, not a Drift Sentinel Run's own run_id; see the module docstring for why.

Mandatory human-in-the-loop gates (section 6.4): three of the four are enforced in agents/risk_assessor.py::create_finding, all setting Finding.requires_human = True regardless of the current autonomy level -- (1) evidence cited as "satisfied" that's actually stale gets the status itself downgraded to "unknown" (section 10's exact fail-closed example), (2) any finding on a tier="critical" vendor (platform/policy.py::requires_mandatory_human_review), (3) any finding with residual_risk >= RESIDUAL_RISK_HUMAN_THRESHOLD (default 15/25). The fourth -- any outbound external communication -- is agents/remediation_router.py::draft_vendor_email's refusal to run without a Finding.human_decision already recorded, which is also the only place in this codebase capable of producing vendor-facing text; a send_email function does not exist, so "never sends autonomously" isn't a policy check a future change could loosen.

The reasoning-chain record and /explain (section 7): create_finding also persists a ReasoningRecord -- every status it weighed, a 0.0-1.0 score per option, why_not on each one it didn't pick, chosen: true on the one it did, plus an inputs_hash and (once orchestrator._run finishes and stamps real telemetry onto it) model/tokens_in/tokens_out/latency_ms. GET /findings/{id}/explain replays it alongside the finding itself -- "why did the agent decide this" gets a straight, structured answer, not a re-run.

Data sovereignty (platform/models.py::TenantRepo, section 6.3): a tenant's region_pin is recorded and echoed on every event Envelope, but per the spec's own explicit guidance the actual enforcement is an Organization Policy (gcp.resourceLocations) binding set outside application code -- region_pin_matches is an advisory app-level check an operator could alert on, not the control itself. Deliberately not over-built: an auditor should be able to verify this independently of anything this codebase does.

Beyond the spec

The seven agents in the first table implement the spec as written. The capabilities below solve real enterprise pain the spec doesn't ask for but that standard per-vendor third-party review structurally can't catch:

Contract Intelligence (agents/contract_intelligence.py): manual MSA/DPA review against a legal playbook is one of the most expensive, most manual bottlenecks in enterprise procurement -- a paralegal or general counsel reading every contract by hand for a liability cap, a breach-notification window, subprocessor flow-down language, audit rights. extract_contract_terms does that comparison automatically against agents/contract_playbook.py (explicitly labeled illustrative reference data, same pattern as this project's other mock datasets) and flags the gap as a structured ContractTerm instead of a manual re-derivation. GET /vendors/{id}/contract-terms surfaces the result; filter client-side on a non-empty deviation for just the flagged ones.

Concentration Analyzer (agents/concentration_analyzer.py): standard review is per-vendor -- each SOC 2 or contract comes back looking fine in isolation. What that misses is portfolio risk: if a dozen vendors that look independently diversified all secretly run on the same subprocessor (the same cloud region, the same auth provider), a single incident there becomes a correlated failure across all of them at once -- structurally invisible to any tool that only ever looks at one vendor at a time. This is the exact blind spot behind real incidents like the 2024 CrowdStrike outage cascading through supposedly-independent downstream vendors. analyze_concentration_risk clusters every subprocessor agents/contract_intelligence.py has ever extracted by normalized name, flags every cluster touching 2+ vendors, and weights severity by how many of them are critical-tier -- re-running always supersedes stale clusters rather than accumulating duplicates. POST /concentration-analyzer/tick triggers it manually; it also runs automatically off every subprocessors.extracted event. GET /concentration-risks lists current clusters, ranked by how many critical-tier vendors are exposed.

Risk trend early-warning (agents/drift_sentinel.py's fourth signal, risk_trend_rising; platform/models.py::AssessmentSnapshot): Finding is keyed by vendor+control and overwritten in place on every reassessment -- deliberately, since the finding itself should always show current state, not a growing pile of stale ones. But that means, on its own, the fleet has no way to see a trajectory: a control whose residual risk climbs 4 -> 9 -> 16 across three sweeps looks, at every individual point, like just another gap finding, with nothing connecting the three into a pattern a human should notice before it gets worse. create_finding now also appends an immutable AssessmentSnapshot on every call (never overwritten, unlike Finding), and run_drift_sweep's new step scans for three consecutive strictly-rising snapshots on the same vendor+control -- suppressed the same way every other signal is by an active Memory Bank exception -- and raises risk_trend_rising through the exact same LLM-judgment -> reopen_assessment path every other signal already uses. This is what turns the fleet from purely reactive (you have a gap) to predictive (you're trending toward one), using the same weeks-of-persistence machinery the Fortified Enterprise Fleet track already requires Memory Bank to provide. GET /vendors/{id}/assessment-history exposes the raw trail for inspection or charting.

Framework Crosswalk (agents/framework_crosswalk.py, agents/framework_crosswalk_reference.py): enterprises rarely carry just one compliance framework -- SOC 2 for one buyer, ISO 27001 for a European one, NIST CSF for a federal one -- and standard tooling re-collects evidence for each framework independently, multiplying audit fatigue linearly with framework count. compute_framework_coverage doesn't re-collect anything: it looks at Findings Risk Assessor has already produced for a vendor's SOC 2 controls and reports, per an illustrative control-equivalence crosswalk (explicitly labeled as such, same pattern as contract_playbook.py), which controls in a target framework are already indirectly satisfied versus which ones genuinely still need fresh evidence -- each covered entry citing the source finding_id, so the coverage claim is traceable, not just a percentage. GET /vendors/{id}/crosswalk?target_framework=ISO27001 exposes it.

Offboarding & termination assistance (agents/offboarding.py): VendorStatus has carried an "offboarding" value since the very first version of this schema, but until now no code path ever set or cleared it -- a documented gap. Every DPA this fleet reviews contains a termination_assistance clause obligating the vendor to export data and certify its deletion within a fixed window; in practice that obligation is tracked in a spreadsheet, if at all. initiate_offboarding starts the clock -- using the vendor's own contracted deadline if Contract Intelligence flagged one as a deviation from the playbook default, so the two agents' work compounds rather than duplicates -- confirm_data_deletion stops it, and Drift Sentinel's fifth signal (offboarding_overdue) is what actually surfaces a miss: a vendor still holding your data past the date they were contractually required to delete it, a real, auditable data-retention and GDPR/CCPA exposure. GET /vendors/{id}/offboarding exposes the current record.

Executive Risk Digest (agents/executive_digest.py): this build's API surface grew to 44 routes across five feature areas: nobody clicks through all of them weekly, so an urgent item (a critical-tier gap, a newly-detected concentration risk, an overdue offboarding) can sit unnoticed for a full review cycle. gather_digest_inputs deterministically pulls the week's most urgent state across every other agent's output in one call; the LLM then writes a 3-5 paragraph narrative that prioritizes by urgency and is instructed never to invent a figure not present in those inputs, and publish_digest persists it alongside the exact inputs it was grounded in, so a claim in the digest can always be checked against what the model actually saw.

All six of the above follow the same design rules as the rest of the fleet: Contract Intelligence gets Intake's identical untrusted-zone grant shape (it reads the same class of vendor-supplied, attacker-adjacent documents); Concentration Analyzer, Framework Crosswalk, and the Offboarding Agent get Evidence Collector's identical deterministic, no-LLM, L3 shape (clustering names, looking up a control's equivalent, or comparing a deadline to today is a lookup, not judgment, so all three cost zero tokens and carry zero prompt-injection surface); the risk-trend and offboarding-overdue signals reuse Drift Sentinel's existing identity, registry entry, and LLM-judgment step rather than standing up a redundant agent for what is, architecturally, just a fourth and fifth kind of drift; Executive Risk Digest splits its own two tools the same deterministic-gather/LLM-judgment way Risk Assessor's finding pipeline does, just compressed into one agent.

API surface (section 9)

Every route below lives in api/routes.py, behind the Agent Gateway's API-key + rate-limit check (_authorize).

Route Purpose
POST /vendors Register a vendor and set its tier up front
POST /vendors/artifacts Upload (JSON raw_text) → quarantine-scan → Model Armor → extract
POST /vendors/artifacts/upload Same pipeline, but a real file (PDF/DOCX/TXT, multipart) -- text extraction happens at the HTTP edge (api/document_extraction.py) before Model Armor ever sees it
GET /vendors / GET /vendors/{id} List / fetch, with blind_window_days computed live
GET /vendors/{id}/findings Findings for one vendor
GET /vendors/{id}/contract-terms Extracted clauses for one vendor's contract(s), each pre-evaluated against the playbook
GET /vendors/{id}/subprocessors Subprocessors disclosed in one vendor's contract(s)
GET /vendors/{id}/assessment-history Append-only trend of every reassessment ever recorded, not just current state
GET /vendors/{id}/crosswalk?target_framework= Coverage of a target compliance framework via the vendor's existing SOC 2 findings
POST /vendors/{id}/offboard Start the offboarding clock -- sets vendor.status = "offboarding", computes a data-deletion deadline
POST /vendors/{id}/offboard/confirm Close out an offboarding -- sets vendor.status = "offboarded", terminal
GET /vendors/{id}/offboarding Current OffboardingRecord for one vendor
POST /assessments Manually (re-)trigger an assessment
GET /assessments/{trace_id} Status + checkpoint position for a triggered assessment
GET /findings ?status=gap Global, filterable findings listing
GET /findings/{id} Fetch one finding
GET /findings/{id}/explain Replay its reasoning-chain record(s)
POST /findings/{id}/decision / POST /decisions Record a human decision (path- or body-keyed)
POST /questionnaires Upload/submit a buyer questionnaire
GET /questionnaires/{id} Answers + confidence + citations
POST /questionnaires/{id}/export DLP-gated export (only auto-status answers)
POST /runs/{trace_id}/rollback Compensating-action rollback
POST /evidence-collector/tick Deterministic sweep -- no credentials needed
POST /drift-sentinel/tick Scheduled/manual drift sweep
POST /concentration-analyzer/tick Deterministic full-portfolio subprocessor-cluster sweep -- no credentials needed
GET /concentration-risks Current concentration-risk clusters, ranked by critical-tier vendor exposure
POST /digest/generate Run the Executive Risk Digest Agent now, rather than waiting for the next scheduled run
GET /digest/latest The most recently generated digest
GET /digest/{id} Fetch one digest by id
GET /traces Browse recent trace summaries -- optionally ?vendor_id= scoped -- instead of needing a trace_id already in hand
GET /traces/{id} Full reasoning-chain audit trail for a trace
GET /dlq Dead-letter queue contents
GET /fleet-config / POST /fleet-config Kill switch: read/set autonomy level, pause/resume an agent
GET /fleet/health Per-agent effective ceiling, DLQ depth, today's spend
GET /metrics Section 13's dashboard numbers, computed live
GET /registry The Agent Registry listing

GCP + GEAP service mapping

Need Service This build
Agent hosting Cloud Run deploy/deploy_cloud_run.sh, min-instances=0
Discovery/versioning Agent Registry platform/registry.py, live -- GET /registry
Long-running async execution Agent Runtime platform/models.py RunRepo (checkpointing), live
Cross-session memory Memory Bank platform/memory_bank.py, live
Zero-trust access Agent Identity platform/identity.py, live
Routing + policy Agent Gateway api/routes.py + platform/auth.py, live
Guardrails Model Armor platform/guardrails.py, live
Audit + reasoning traces Agent Observability platform/observability.py (OTel spans + GET /traces/{id}), live
Event bus Pub/Sub platform/event_bus.py: in-process dispatch always live; mirrors to real Pub/Sub topics when USE_PUBSUB=true against topics deploy/setup_gcp.sh provisions
Graph state Firestore platform/store.py: in-memory by default, Firestore-backed when GOOGLE_CLOUD_PROJECT is set
Evidence retrieval Vertex AI Vector Search represented by questionnaire_responder.search_evidence's substring match -- documented as a stand-in, see README
Audit warehouse BigQuery documented target for the audit log; not wired to a live BigQuery table in this build (no GCP project to verify against)
Egress inspection Cloud DLP represented by guardrails.scan_for_dlp_violation -- documented as a stand-in, see README
Cadence Cloud Scheduler deploy/deploy_cloud_run.sh provisions both sweep jobs against the live Cloud Run URL
Secrets Secret Manager remediation-router's identity grant (platform/identity.py) declares secrets:read; no ticket-system credential actually needs fetching in this build (tickets are mocked), so there is no live Secret Manager call to point at -- the grant exists so the shape is right when a real Jira/GitHub integration is added
Data sovereignty Org Policy (gcp.resourceLocations) platform/models.py::TenantRepo records region_pin; actual enforcement is the Org Policy binding, per section 6.3's own guidance, not application code

Composite indexes (production Firestore)

findings(vendor_id, status, residual_risk desc) · evidence(control_ref, collected_at desc) · artifacts(valid_until asc) · answers(status, confidence asc)

The in-memory backend used for local dev and tests doesn't need indexes (it's a linear scan over a dict); these apply once USE_FIRESTORE=true.