Skip to content

BULWARK — Architecture & Design Diagrams

Every diagram below is Mermaid (renders natively on GitHub) and is drawn directly from the code in src/bulwark/ — function names, event topic names, status literals, and file paths are exact, not illustrative. For prose explanation of why each mechanism is built the way it is, see architecture.md; this document is the visual companion, organized the way a design doc typically is: HLD, LLD, DFD, ERD, sequence diagrams, state diagrams, and a deployment diagram.

Contents

  1. High-Level Design (HLD)
  2. 1.1 System Context Diagram
  3. 1.2 Container / Component Diagram
  4. Low-Level Design (LLD)
  5. 2.1 Platform Layer — Class Diagram
  6. 2.2 Agent Layer — Tool Function Signatures
  7. 2.3 API Layer — Route Groupings
  8. Data Flow Diagrams (DFD)
  9. 3.1 Level 0 — Context
  10. 3.2 Level 1 — Major Processes
  11. 3.3 Level 2 — Onboard / Assure Detail
  12. 3.4 Level 2 — Watch Detail
  13. Entity-Relationship Diagram (ERD)
  14. Sequence Diagrams
  15. 5.1 Onboard Loop — clean artifact
  16. 5.2 Onboard Loop — blocked by Model Armor
  17. 5.3 Assure Loop — contract review → concentration-risk cascade
  18. 5.4 Watch Loop — scheduled sweep incl. predictive risk-trend signal
  19. 5.5 Attest Loop — questionnaire answering
  20. 5.6 Human review & explainability
  21. 5.7 Kill switch (manual, live)
  22. 5.8 Circuit breaker (automatic)
  23. 5.9 Rollback
  24. 5.10 Framework crosswalk query
  25. State Diagrams
  26. 6.1 Vendor lifecycle
  27. 6.2 Finding status
  28. 6.3 Fleet autonomy ladder
  29. 6.4 Answer status
  30. Deployment Diagram
  31. Zero-Trust Identity Map

1. High-Level Design (HLD)

1.1 System Context Diagram

Who and what BULWARK talks to, with no internal detail.

flowchart TB
    Vendor["Vendor\n(untrusted third party)"]
    Buyer["Buyer\n(runs security due diligence on you)"]
    Reviewer["Security / Compliance\nReviewer (human)"]
    Gemini[["Gemini\n(Flash + Pro, via Google ADK)"]]
    GCP[("Google Cloud\nCloud Run · Firestore · Pub/Sub · Cloud Scheduler")]

    subgraph SYS["BULWARK — Continuous Third-Party Assurance Fleet"]
        CORE(("12-agent\nevent-driven fleet"))
    end

    Vendor -- "SOC2 / ISO / pen-test /\nMSA / DPA text" --> SYS
    Buyer -- "security questionnaire" --> SYS
    SYS -- "cited answers" --> Buyer
    SYS -- "findings, tickets,\nconcentration risks,\nrisk-trend alerts" --> Reviewer
    Reviewer -- "decisions, kill switch,\nautonomy overrides" --> SYS
    SYS <-- "prompts / structured tool calls" --> Gemini
    SYS <-- "state, events, deploy" --> GCP

1.2 Container / Component Diagram

The four architectural layers and how they connect — this is the "container diagram" level, one box per deployable/runnable concern.

flowchart TB
    subgraph EXT["External"]
        Caller["Caller\n(vendor / buyer / reviewer / Cloud Scheduler)"]
        Gemini[["Gemini API / Vertex AI"]]
    end

    subgraph GW["Agent Gateway  (api/routes.py + platform/auth.py)"]
        AUTH["API-key auth + rate limit\n(_authorize)"]
    end

    subgraph RT["Agent Runtime  (agents/orchestrator.py, ADK Runners)"]
        UNTRUSTED["Untrusted-zone agents\nIntake · Contract Intelligence"]
        INTERNAL["Internal agents\nSupervisor · Risk Assessor (Pro) ·\nQuestionnaire Responder · Drift Sentinel ·\nRemediation Router · Executive Risk Digest"]
        DETERMINISTIC["Deterministic agents (no LLM)\nEvidence Collector · Concentration Analyzer ·\nFramework Crosswalk · Offboarding Agent"]
    end

    subgraph GUARD["Model Armor  (platform/guardrails.py)"]
        MA["before/after_model_callback\nbefore_tool_callback — ADK plugin"]
    end

    subgraph BUS["Event Bus  (platform/event_bus.py, Pub/Sub-shaped)"]
        TOPICS["11 named topics + DLQ +\nidempotency dedup"]
    end

    subgraph STATE["State"]
        FS[("Firestore-shaped graph\nplatform/models.py\nin-memory fallback, same code path")]
        MB[("Memory Bank\nplatform/memory_bank.py")]
        AL[("Audit Log\nplatform/observability.py")]
        SP[("Spend Ledger\nplatform/spend.py")]
        RB[("Rollback Ledger\nplatform/rollback.py")]
    end

    subgraph GOV["Governance"]
        REG["Agent Registry\nplatform/registry.py"]
        ID["Agent Identity\nplatform/identity.py"]
        POL["Autonomy ladder + kill switch\nplatform/policy.py"]
    end

    Caller --> AUTH --> RT
    UNTRUSTED <--> MA
    INTERNAL <--> MA
    MA <--> Gemini

    RT -->|publish| BUS -->|subscribe| RT
    BUS -.failed handler.-> DLQ[("dead-letter queue")]

    RT --> FS
    RT --> MB
    RT --> AL
    RT --> SP
    RT --> RB
    SP -.breach: set_global_autonomy 0.-> POL

    RT -. "require_grant()\nevery tool call" .-> ID
    RT -. "enforce_autonomy()\nevery tool call" .-> POL
    RT -. self-registers at startup .-> REG

    AUTH -->|"POST /fleet-config"| POL
    POL -.enforces on next call.-> RT

2. Low-Level Design (LLD)

2.1 Platform Layer — Class Diagram

The actual classes/functions each agent and route calls into. This is the layer every one of the 12 agents shares — none of it is agent- specific.

classDiagram
    class DocumentStore {
        -collection_name: str
        -is_firestore: bool
        +get(doc_id) dict
        +set(doc_id, data)
        +update(doc_id, patch) dict
        +delete(doc_id)
        +list(predicate) list~dict~
        +append_to_list_field(doc_id, field, item) dict
    }

    class EventBus {
        -_subscribers: dict~str, list~
        -_seen_idempotency_keys: set
        +subscribe(topic, handler)
        +publish(topic, envelope) async
        +dlq_entries() list~dict~
    }

    class Envelope {
        +event_id: str
        +trace_id: str
        +idempotency_key: str
        +provenance: str
        +tenant: str
        +region_pin: str
        +attempt: int
        +payload: dict
        +published_at: str
    }

    class AgentRegistry {
        +register(record: AgentRecord)
        +get(agent_id) AgentRecord
        +list() list~AgentRecord~
    }

    class AgentRecord {
        +agent_id: str
        +name: str
        +version: str
        +model: str
        +trust_zone: str
        +autonomy_ceiling: int
        +departments: list~str~
        +tools: list~str~
    }

    class AgentGrant {
        +service_account: str
        +allowed: frozenset~str~
        +denied: frozenset~str~
    }

    class PolicyModule {
        +enforce_autonomy(agent_id, level)
        +set_global_autonomy(level)
        +pause_agent(agent_id)
        +resume_agent(agent_id)
        +requires_mandatory_human_review(vendor_tier, residual_risk) tuple
    }

    class SpendLedger {
        +record(tokens_in, tokens_out) DailySpend
        +today() DailySpend
        +reset_today()
    }

    class RollbackLedger {
        +record(trace_id, action_type, subject_type, subject_id, field_name, before_value, after_value)
    }

    class MemoryBank {
        +get(vendor_id) VendorMemory
        +record_assessment(vendor_id)
        +add_negotiated_exception(vendor_id, control_ref, reason, expires_at)
        +add_accepted_risk(vendor_id, control_ref, actor, expires_at)
        +has_active_exception(vendor_id, control_ref) bool
        +note_reviewer_posture(vendor_id, control_ref, note)
    }

    class AuditLog {
        +record(agent_name, event, detail, trace_id)
        +trace(trace_id) list~dict~
        +count_events(event) int
    }

    class RateLimiter {
        +check(api_key)
        +reset()
    }

    EventBus "1" *-- "many" Envelope : dispatches
    AgentRegistry "1" *-- "many" AgentRecord : catalogs
    PolicyModule ..> AgentRegistry : reads own ceiling
    PolicyModule ..> DocumentStore : fleet_config doc
    SpendLedger ..> PolicyModule : trips breaker on cap breach
    EventBus --> DocumentStore : DLQ persistence
    MemoryBank --> DocumentStore
    AuditLog --> DocumentStore
    RollbackLedger --> DocumentStore
    AgentRegistry --> DocumentStore
    SpendLedger --> DocumentStore

2.2 Agent Layer — Tool Function Signatures

Every agent modeled as a "class" whose methods are its exact ADK FunctionTool signatures — the real, callable contract each agent exposes to its LlmAgent (or, for the four deterministic agents, that a route calls directly).

classDiagram
    class SupervisorAgent {
        +trust_zone : internal, no tools
    }

    class IntakeAgent {
        +trust_zone : untrusted, L1
        +prescan_artifact(vendor_name, doc_type, raw_text, gcs_uri, sha256) dict
        +emit_assertion(vendor_id, artifact_id, control_ref, claim, source_page, confidence) dict
    }

    class ContractIntelligenceAgent {
        +trust_zone : untrusted, L1
        +extract_contract_terms(vendor_id, artifact_id, terms) dict
        +extract_subprocessors(vendor_id, artifact_id, subprocessors) dict
    }

    class EvidenceCollectorAgent {
        +trust_zone : deterministic, L3, no LLM
        +collect_evidence() list~Evidence~
    }

    class RiskAssessorAgent {
        +trust_zone : internal, L1, Gemini Pro
        +get_assessment_context(vendor_id) dict
        +create_finding(vendor_id, control_ref, status, gap_description, residual_risk, evidence_ids, assertion_ids, trace_id, considered) dict
    }

    class QuestionnaireResponderAgent {
        +trust_zone : egress-controlled, L2
        +search_evidence(query) list
        +draft_answer(questionnaire_id, question, answer, confidence, citations) dict
    }

    class DriftSentinelAgent {
        +trust_zone : internal, L3
        +run_drift_sweep(trace_id) dict
        +reopen_assessment(vendor_id, reason, severity, trace_id) dict
    }

    class RemediationRouterAgent {
        +trust_zone : internal-write, L2
        +open_ticket(finding_id, queue, note) dict
        +draft_vendor_email(finding_id, subject, body) dict
        +build_decision_packet(finding_id) dict
    }

    class ConcentrationAnalyzerAgent {
        +trust_zone : deterministic, L3, no LLM
        +analyze_concentration_risk(trace_id) list~ConcentrationRisk~
    }

    class FrameworkCrosswalkAgent {
        +trust_zone : deterministic, L3, no LLM
        +compute_framework_coverage(vendor_id, target_framework) dict
    }

    class OffboardingAgent {
        +trust_zone : internal-write, deterministic, no LLM
        +initiate_offboarding(vendor_id, reason, trace_id) dict
        +confirm_data_deletion(vendor_id, evidence_note, trace_id) dict
        +check_offboarding_overdue(trace_id) list~dict~
    }

    class ExecutiveDigestAgent {
        +trust_zone : internal, L3 gather / L1 publish
        +gather_digest_inputs() dict
        +publish_digest(narrative, highlights, trace_id) dict
    }

    SupervisorAgent --> RiskAssessorAgent : ADK transfer_to_agent
    SupervisorAgent --> QuestionnaireResponderAgent : ADK transfer_to_agent
    IntakeAgent ..> RiskAssessorAgent : assertion.extracted then assessment.requested
    ContractIntelligenceAgent ..> ConcentrationAnalyzerAgent : subprocessors.extracted
    EvidenceCollectorAgent ..> DriftSentinelAgent : evidence.collected
    RiskAssessorAgent ..> RemediationRouterAgent : finding.created
    DriftSentinelAgent ..> RiskAssessorAgent : drift.detected then assessment.requested
    FrameworkCrosswalkAgent ..> RiskAssessorAgent : reads Findings, query-only, no event
    OffboardingAgent ..> DriftSentinelAgent : offboarding_overdue read on sweep, no event
    ContractIntelligenceAgent ..> OffboardingAgent : termination_assistance deadline override, query-only
    ExecutiveDigestAgent ..> RiskAssessorAgent : reads Findings, query-only, no event
    ExecutiveDigestAgent ..> ConcentrationAnalyzerAgent : reads ConcentrationRisks, query-only, no event
    ExecutiveDigestAgent ..> OffboardingAgent : reads OffboardingRecords, query-only, no event

2.3 API Layer — Route Groupings

All 44 routes in api/routes.py, grouped by resource, and what each group ultimately calls.

flowchart LR
    subgraph Fleet["Fleet meta"]
        R1["GET /status"]
        R2["GET /registry"]
        R3["GET /fleet-config\nPOST /fleet-config"]
        R4["GET /fleet/health"]
        R5["GET /metrics"]
    end
    subgraph Vendors["Vendor onboarding + Assure"]
        R6["POST /vendors"]
        R7["POST /vendors/artifacts\nPOST /vendors/artifacts/upload"]
        R8["GET /vendors, /vendors/{id}"]
        R9["GET .../findings\nGET .../contract-terms\nGET .../subprocessors\nGET .../assessment-history\nGET .../crosswalk"]
        R9b["POST .../offboard\nPOST .../offboard/confirm\nGET .../offboarding"]
    end
    subgraph Assess["Assessments + Findings"]
        R10["POST /assessments\nGET /assessments/{trace_id}"]
        R11["GET /findings\nGET /findings/{id}\nGET /findings/{id}/explain"]
        R12["POST /findings/{id}/decision\nPOST /decisions"]
    end
    subgraph Quest["Questionnaires"]
        R13["POST /questionnaires\nGET /questionnaires/{id}"]
        R14["POST /questionnaires/{id}/export"]
    end
    subgraph Sweeps["Sweeps + Rollback"]
        R15["POST /evidence-collector/tick"]
        R16["POST /drift-sentinel/tick"]
        R17["POST /concentration-analyzer/tick\nGET /concentration-risks"]
        R18["POST /runs/{trace_id}/rollback"]
    end
    subgraph Obs["Observability"]
        R19["GET /traces/{id}"]
        R20["GET /dlq"]
    end
    subgraph Digest["Executive digest"]
        R21["POST /digest/generate\nGET /digest/latest\nGET /digest/{id}"]
    end

    R2 --> REG[("platform.registry")]
    R3 --> POL[("platform.policy")]
    R4 --> POL
    R5 --> MODELS[("platform.models repos")]
    R6 & R8 & R9 --> MODELS
    R7 --> ORCH["orchestrator.process_vendor_artifact"]
    R9b --> OFF["agents.offboarding"]
    R10 --> BUS[("platform.event_bus")]
    R11 & R12 --> MODELS
    R13 --> ORCH2["orchestrator.submit_questionnaire"]
    R14 --> MODELS
    R15 --> EC["agents.evidence_collector"]
    R16 --> ORCH3["orchestrator.run_drift_sweep"]
    R17 --> CA["agents.concentration_analyzer"]
    R18 --> RB[("platform.rollback")]
    R19 --> AL[("platform.observability")]
    R20 --> BUS
    R21 --> ORCH4["orchestrator.generate_digest"]

3. Data Flow Diagrams (DFD)

Standard DFD notation: rectangles are external entities, rounded shapes are processes, cylinders are data stores.

3.1 Level 0 — Context

flowchart LR
    V["Vendor"]
    B["Buyer"]
    R["Reviewer"]
    G[["Gemini"]]
    P(("0.0\nBULWARK Fleet"))
    DS[("Control-Evidence\nGraph")]

    V -->|"artifact text"| P
    B -->|"questions"| P
    P -->|"cited answers"| B
    P -->|"findings, tickets,\nrisk alerts"| R
    R -->|"decisions,\nkill switch"| P
    P <-->|"prompts / tool calls"| G
    P <--> DS

3.2 Level 1 — Major Processes

flowchart TB
    V["Vendor"]
    B["Buyer"]
    R["Reviewer"]
    Sched["Cloud Scheduler"]

    P1(("1.0\nScreen + Intake"))
    P2(("2.0\nContract Review"))
    P3(("3.0\nRisk Assessment"))
    P4(("4.0\nConcentration\nAnalysis"))
    P5(("5.0\nFramework\nCrosswalk"))
    P6(("6.0\nEvidence + Drift\nDetection"))
    P7(("7.0\nQuestionnaire\nResponse"))
    P8(("8.0\nRemediation"))

    D1[("D1 Vendors /\nArtifacts / Assertions")]
    D2[("D2 Controls /\nEvidence")]
    D3[("D3 Findings /\nSnapshots / Reasoning")]
    D4[("D4 Contract Terms /\nSubprocessors / Concentration Risks")]
    D5[("D5 Memory Bank")]
    D6[("D6 Audit Log")]

    V -->|"SOC2/ISO/pentest text"| P1
    V -->|"MSA/DPA text"| P2
    P1 --> D1
    P1 -->|"assertion.extracted"| P3
    P2 --> D1
    P2 --> D4
    P2 -->|"subprocessors.extracted"| P4
    P3 --> D1
    P3 --> D2
    P3 --> D3
    P3 -->|"finding.created"| P8
    P4 --> D4
    P5 --> D3
    P5 --> D4
    P6 --> D2
    P6 --> D5
    P6 -->|"drift.detected"| P3
    Sched -->|"scheduled tick"| P6
    B --> P7
    P7 --> D2
    P7 -->|"answers"| B
    P8 --> D3
    P8 -->|"tickets, decision packets"| R
    R -->|"human.decision"| P8
    R -->|"target_framework query"| P5
    P1 & P2 & P3 & P4 & P5 & P6 & P7 & P8 --> D6

3.3 Level 2 — Onboard / Assure Detail

flowchart TB
    V["Vendor artifact\n(doc_type)"]
    P1_1(("1.1\nModel Armor\nprescan (deterministic)"))
    P1_2(("1.2\nIntake:\nextract Assertions"))
    P2_1(("2.1\nContract Intelligence:\nextract Contract Terms"))
    P2_2(("2.2\nContract Intelligence:\nextract Subprocessors"))
    P3_1(("3.1\nRisk Assessor:\ncross-reference"))
    P3_2(("3.2\nRisk Assessor:\ncitation-validation gate"))
    P4_1(("4.1\nConcentration Analyzer:\ncluster subprocessors"))

    D1a[("Artifacts")]
    D1b[("Assertions")]
    D2a[("Controls")]
    D2b[("Evidence")]
    D3a[("Findings")]
    D3b[("Assessment Snapshots")]
    D4a[("Contract Terms")]
    D4b[("Subprocessors")]
    D4c[("Concentration Risks")]
    DLQ[("dead-letter queue")]

    V --> P1_1
    P1_1 -->|"blocked → stop, no LLM ever called"| DLQ
    P1_1 -->|"clean + doc_type not contract"| P1_2
    P1_1 -->|"clean + doc_type is contract"| P2_1
    P1_1 --> D1a

    P1_2 --> D1b
    P1_2 -->|"assertion.extracted"| P3_1

    P2_1 --> D4a
    P2_1 --> P2_2
    P2_2 --> D4b
    P2_2 -->|"subprocessors.extracted"| P4_1
    P4_1 --> D4c

    P3_1 --> D2a
    P3_1 --> D2b
    P3_1 --> P3_2
    P3_2 -->|"rejected: no/unknown citations"| P3_1
    P3_2 -->|"accepted"| D3a
    P3_2 --> D3b

3.4 Level 2 — Watch Detail

flowchart TB
    Sched["Cloud Scheduler\n(EVIDENCE_SWEEP_HOURS)"]
    P6_1(("6.1\nEvidence Collector\n(deterministic)"))
    P6_2(("6.2\nDrift Sentinel:\ndetect signals\n(deterministic)"))
    P6_3(("6.3\nDrift Sentinel:\njudge severity\n(Gemini Flash)"))
    P6_4(("6.4\nreopen_assessment"))

    D2b[("Evidence")]
    D3b[("Assessment Snapshots")]
    D1a[("Artifacts — expiry")]
    D5[("Memory Bank —\nactive exceptions")]
    MOCK[("Mock breach feed")]

    Sched --> P6_1
    P6_1 --> D2b
    P6_1 -->|"evidence.collected"| P6_2

    P6_2 -->|"reads"| D1a
    P6_2 -->|"reads"| D2b
    P6_2 -->|"reads"| D3b
    P6_2 -->|"reads"| MOCK
    P6_2 -.suppressed by.-> D5
    P6_2 -->|"signals: expiry_approaching,\ncontrol_drift, breach_disclosure,\nrisk_trend_rising"| P6_3

    P6_3 -->|"per affected vendor"| P6_4
    P6_4 -->|"drift.detected"| RE(("re-enters\nRisk Assessment\n(3.1)"))
    P6_4 --> D5

4. Entity-Relationship Diagram (ERD)

The core control-evidence graph (platform/models.py). Run, FleetConfig, and CompensatingAction are cross-cutting / trace-scoped rather than vendor-relational, so they're called out below the diagram instead of forced into it.

erDiagram
    TENANT ||--o{ VENDOR : owns
    VENDOR ||--o{ ARTIFACT : submits
    VENDOR ||--o| VENDOR_MEMORY : "remembered as"
    VENDOR ||--o{ OFFBOARDING_RECORD : "tracked by, once ending"
    ARTIFACT ||--o{ ASSERTION : yields
    ARTIFACT ||--o{ CONTRACT_TERM : "yields (contract doc types)"
    ARTIFACT ||--o{ SUBPROCESSOR : discloses
    VENDOR ||--o{ FINDING : "assessed via"
    CONTROL_REQUIREMENT ||--o{ FINDING : "evaluated by"
    CONTROL_REQUIREMENT ||--o{ EVIDENCE : "observed as"
    FINDING ||--o{ ASSESSMENT_SNAPSHOT : "recorded as (append-only)"
    FINDING ||--o{ REASONING_RECORD : "explained by"
    SUBPROCESSOR }o--o{ CONCENTRATION_RISK : "clustered into (shared name, 2+ vendors)"
    QUESTIONNAIRE ||--o{ ANSWER : contains

    TENANT {
        string tenant PK
        string region_pin
        string framework
    }
    VENDOR {
        string vendor_id PK
        string tenant FK
        string name
        string tier "critical|high|moderate|low"
        string status "onboarding|active|under_review|offboarding|offboarded"
        list data_classes
    }
    ARTIFACT {
        string artifact_id PK
        string vendor_id FK
        string doc_type
        string armor_verdict "clean|blocked"
        string valid_until
    }
    ASSERTION {
        string assertion_id PK
        string vendor_id FK
        string control_ref
        string claim
        float confidence
    }
    CONTROL_REQUIREMENT {
        string control_ref PK
        string tenant FK
        string framework
        string criticality
    }
    EVIDENCE {
        string evidence_id PK
        string control_ref FK
        string observed_value
        string expected_value
        string freshness "fresh|stale"
    }
    FINDING {
        string finding_id PK
        string vendor_id FK
        string control_ref FK
        string status "satisfied|gap|exception|unknown"
        int residual_risk "1-25"
        bool requires_human
    }
    ASSESSMENT_SNAPSHOT {
        string snapshot_id PK
        string finding_id FK
        string control_ref
        int residual_risk
        string created_at
    }
    REASONING_RECORD {
        string decision_id PK
        string subject_id FK
        list considered
        string model
        int tokens_in
        int tokens_out
    }
    CONTRACT_TERM {
        string term_id PK
        string vendor_id FK
        string artifact_id FK
        string clause_type
        string deviation
    }
    SUBPROCESSOR {
        string subprocessor_id PK
        string vendor_id FK
        string artifact_id FK
        string name
        string location
    }
    CONCENTRATION_RISK {
        string risk_id PK
        string subprocessor_name
        list vendor_ids
        int critical_vendor_count
        string severity
    }
    QUESTIONNAIRE {
        string questionnaire_id PK
        string buyer
        int total_questions
        int auto_answered
    }
    ANSWER {
        string answer_id PK
        string questionnaire_id FK
        string status "auto|needs_human|approved|blocked_dlp"
        float confidence
        list citations
    }
    VENDOR_MEMORY {
        string vendor_id PK
        string last_assessment_at
        list negotiated_exceptions
        list accepted_risk_decisions
    }
    OFFBOARDING_RECORD {
        string record_id PK
        string vendor_id FK
        string reason
        string deadline
        string status "pending|confirmed"
        string evidence_note
    }

Not shown above (trace-scoped, not vendor-relational): Run (checkpoint state for one sweep, keyed by run_id), FleetConfig (one singleton — autonomy_level, max_daily_token_spend, paused_agents), CompensatingAction (keyed by trace_id + polymorphic subject_type/subject_id, replayed by POST /runs/{trace_id}/rollback), Digest (tenant-scoped snapshot — digest_id, narrative, highlights, and the exact inputs dict it was grounded in — not tied to one vendor, so it's a tenant-wide summary of the graph above at a point in time, not a node in it).


5. Sequence Diagrams

5.1 Onboard Loop — clean artifact

sequenceDiagram
    autonumber
    actor Caller
    participant GW as Agent Gateway
    participant ORCH as orchestrator.py
    participant MA as Model Armor<br/>(prescan_artifact)
    participant Bus as Event Bus
    participant Intake as Intake Agent
    participant Sup as Supervisor
    participant RA as Risk Assessor<br/>(Gemini Pro)
    participant Gem as Gemini
    participant Store as Firestore-shaped Store
    participant Rem as Remediation Router

    Caller->>GW: POST /vendors/artifacts
    GW->>ORCH: process_vendor_artifact(...)
    ORCH->>MA: prescan_artifact(raw_text)
    MA-->>ORCH: armor_verdict = "clean"
    ORCH->>Bus: publish(vendor.artifact.received)
    ORCH->>Intake: _run(intake_runner, prompt)
    Intake->>Gem: extract claims
    Gem-->>Intake: structured claims
    Intake->>Store: emit_assertion(...) → Assertion
    ORCH->>Bus: publish(assertion.extracted)
    Bus->>ORCH: _on_assertion_extracted
    ORCH->>Bus: publish(assessment.requested)
    Bus->>ORCH: _on_assessment_requested
    ORCH->>Sup: _run(supervisor_runner, prompt)
    Sup->>RA: ADK transfer_to_agent
    RA->>Store: get_assessment_context(vendor_id)
    Store-->>RA: assertions + controls + evidence
    RA->>Gem: cross-reference, decide status
    Gem-->>RA: status, residual_risk, considered[]
    RA->>Store: create_finding(...)
    Note over RA,Store: citation-validation gate:<br/>reject if 0 citations or unknown id
    Store-->>RA: Finding + AssessmentSnapshot + ReasoningRecord
    ORCH->>Store: vendor.status = "active"
    alt status == "gap"
        ORCH->>Bus: publish(finding.created)
        Bus->>ORCH: _on_finding_created
        ORCH->>Rem: _run(remediation_runner, prompt)
        Rem->>Store: open_ticket(finding_id)
        Rem->>Store: build_decision_packet(finding_id)
    end
    ORCH-->>GW: {trace_id, status: "extracted", ...}
    GW-->>Caller: 200 OK

5.2 Onboard Loop — blocked by Model Armor

The 30-second demo moment: the injection never reaches an LLM at all.

sequenceDiagram
    autonumber
    actor Caller
    participant GW as Agent Gateway
    participant ORCH as orchestrator.py
    participant MA as Model Armor<br/>(prescan_artifact, deterministic)
    participant Bus as Event Bus
    participant Store as Store

    Caller->>GW: POST /vendors/artifacts<br/>("Ignore previous instructions...")
    GW->>ORCH: process_vendor_artifact(...)
    ORCH->>MA: prescan_artifact(raw_text)
    Note over MA: regex/pattern match for<br/>prompt-injection signatures —<br/>plain code, zero LLM calls
    MA-->>ORCH: armor_verdict = "blocked",<br/>armor_findings = [{type: prompt_injection, ...}]
    ORCH->>Store: Artifact.armor_verdict = "blocked"
    ORCH->>Bus: publish(vendor.artifact.received)
    ORCH->>Store: audit_log.record("artifact_blocked_by_model_armor")
    Note over ORCH: return immediately —<br/>Supervisor, Intake, and Gemini<br/>are never invoked
    ORCH-->>GW: {status: "blocked_by_model_armor"}
    GW-->>Caller: 200 OK (blocked)

5.3 Assure Loop — contract review → concentration-risk cascade

sequenceDiagram
    autonumber
    actor Caller
    participant GW as Agent Gateway
    participant ORCH as orchestrator.py
    participant MA as Model Armor
    participant CI as Contract Intelligence
    participant Gem as Gemini Flash
    participant Store as Store
    participant Bus as Event Bus
    participant CA as Concentration Analyzer<br/>(deterministic)

    Caller->>GW: POST /vendors/artifacts<br/>(doc_type: "DPA")
    GW->>ORCH: process_vendor_artifact(...)
    ORCH->>MA: prescan_artifact(raw_text)
    MA-->>ORCH: clean
    Note over ORCH: doc_type in {msa,dpa,contract,sla,order form}<br/>→ route to Contract Intelligence, not Intake
    ORCH->>CI: _run(contract_runner, prompt)
    CI->>Gem: identify clauses vs. playbook
    Gem-->>CI: clauses + deviations
    CI->>Store: extract_contract_terms(...) → ContractTerm[]
    CI->>Gem: identify disclosed subprocessors
    Gem-->>CI: subprocessor list
    CI->>Store: extract_subprocessors(...) → Subprocessor[]
    CI->>Bus: publish(subprocessors.extracted)
    Bus->>ORCH: _on_subprocessors_extracted (sync, no LLM)
    ORCH->>CA: analyze_concentration_risk(trace_id)
    CA->>Store: read every Subprocessor (all vendors)
    Note over CA: cluster by normalized name,<br/>flag clusters touching 2+ vendors,<br/>weight severity by critical-tier count
    CA->>Store: clear_all() then create() → ConcentrationRisk[]
    ORCH->>Bus: publish(contract.terms_extracted)
    Note over Bus: documented, no automated<br/>consumer in this build
    ORCH-->>GW: {status: "contract_reviewed", ...}
    GW-->>Caller: 200 OK

5.4 Watch Loop — scheduled sweep incl. predictive risk-trend signal

sequenceDiagram
    autonumber
    participant Sched as Cloud Scheduler
    participant GW as Agent Gateway
    participant ORCH as orchestrator.py
    participant DS as Drift Sentinel
    participant Store as Store
    participant Gem as Gemini Flash
    participant Bus as Event Bus
    participant RA as Risk Assessor

    Sched->>GW: POST /drift-sentinel/tick
    GW->>ORCH: run_drift_sweep()
    ORCH->>DS: _run(drift_sentinel_runner)
    DS->>Store: run_drift_sweep() [deterministic tool]
    Store-->>DS: expiry signals
    Store-->>DS: evidence-drift signals<br/>(skips active Memory Bank exceptions)
    Store-->>DS: breach-feed signals
    Store-->>DS: risk_trend_rising signals<br/>(3 consecutive rising AssessmentSnapshots)
    DS->>Gem: judge severity per vendor,<br/>write one-paragraph reason
    Gem-->>DS: severity + reason per vendor
    loop each affected vendor
        DS->>Store: reopen_assessment(vendor_id, reason, severity)
        Store->>Store: vendor.status = "under_review"
        Store->>Store: CompensatingAction recorded (rollback-able)
        DS->>Bus: publish(drift.detected)
        Bus->>ORCH: _on_drift_detected
        ORCH->>Bus: publish(assessment.requested)
        Note over Bus,RA: re-enters the Onboard loop's<br/>assessment sequence (5.1, steps 10+)
        Bus->>RA: _on_assessment_requested
    end
    ORCH-->>GW: {trace_id, summary}
    GW-->>Sched: 200 OK

5.5 Attest Loop — questionnaire answering

sequenceDiagram
    autonumber
    actor Buyer
    participant GW as Agent Gateway
    participant ORCH as orchestrator.py
    participant Sup as Supervisor
    participant QR as Questionnaire Responder
    participant Gem as Gemini Flash
    participant Store as Store

    Buyer->>GW: POST /questionnaires {buyer, questions[]}
    GW->>ORCH: submit_questionnaire(...)
    ORCH->>Store: Questionnaire.create()
    ORCH->>Sup: _run(supervisor_runner, prompt)
    Sup->>QR: ADK transfer_to_agent
    loop each question
        QR->>Store: search_evidence(query)
        Store-->>QR: matching control titles/evidence
        QR->>Gem: draft an answer, cite control_refs
        Gem-->>QR: answer text + confidence
        QR->>Store: draft_answer(...)
        alt confidence < ANSWER_CONFIDENCE_THRESHOLD
            Note over QR,Store: status = "needs_human" (abstain)
        else DLP-style scan finds a leak
            Note over QR,Store: status = "blocked_dlp"
        else
            Note over QR,Store: status = "auto"
        end
    end
    ORCH->>Store: publish(answer.drafted)
    ORCH-->>GW: {questionnaire_id, summary}
    GW-->>Buyer: 200 OK
    Buyer->>GW: POST /questionnaires/{id}/export
    GW->>Store: export only status == "auto" answers
    Store-->>GW: {exported[], excluded_count, excluded_reasons}
    GW-->>Buyer: 200 OK

5.6 Human review & explainability

sequenceDiagram
    autonumber
    actor Reviewer
    participant GW as Agent Gateway
    participant Store as Store
    participant Bus as Event Bus
    participant ORCH as orchestrator.py
    participant Rem as Remediation Router

    Reviewer->>GW: GET /findings?status=gap
    GW-->>Reviewer: filtered Finding[]
    Reviewer->>GW: GET /findings/{id}/explain
    GW->>Store: reasoning_record_repo.list_for_subject(finding_id)
    Store-->>GW: ReasoningRecord[] (options considered, scores, why_not)
    GW-->>Reviewer: {finding, reasoning[]}
    Reviewer->>GW: POST /findings/{id}/decision<br/>{actor, decision, rationale}
    GW->>Store: record_human_decision(...)
    GW->>Bus: publish(human.decision)
    Bus->>ORCH: _on_human_decision
    ORCH->>Rem: _run(remediation_runner, prompt)
    Note over Rem: draft_vendor_email's gate<br/>(finding.human_decision != null)<br/>is now satisfied
    Rem->>Store: draft_vendor_email(finding_id, ...)
    GW-->>Reviewer: 200 OK

5.7 Kill switch (manual, live)

sequenceDiagram
    autonumber
    actor Reviewer
    participant GW as Agent Gateway
    participant Pol as policy.py
    participant Store as fleet_config (Store)
    participant AnyAgent as Any agent's next tool call

    Reviewer->>GW: POST /fleet-config {autonomy_level: 0}
    GW->>Pol: set_global_autonomy(0)
    Pol->>Store: fleet_config.autonomy_level = 0
    Note over Store: no redeploy, no restart —<br/>this is the live state every<br/>tool call reads next
    AnyAgent->>Pol: enforce_autonomy(agent_id, requested_level)
    Pol->>Store: read fleet_config.autonomy_level
    Store-->>Pol: 0
    Pol-->>AnyAgent: raise AutonomyBlocked
    Note over AnyAgent: action refused —<br/>caught, logged, surfaced as an error
    Reviewer->>GW: POST /fleet-config {autonomy_level: 3}
    GW->>Pol: set_global_autonomy(3)
    Pol->>Store: fleet_config.autonomy_level = 3
    Note over Store: fleet resumes on the<br/>very next tool call

5.8 Circuit breaker (automatic)

sequenceDiagram
    autonumber
    participant ORCH as orchestrator._run
    participant Runner as ADK Runner
    participant Gem as Gemini
    participant SL as SpendLedger
    participant CB as check_circuit_breaker()
    participant Pol as policy.set_global_autonomy

    ORCH->>Runner: run_async(prompt)
    Runner->>Gem: model call
    Gem-->>Runner: response + usage_metadata
    Runner-->>ORCH: event stream (tokens_in, tokens_out)
    ORCH->>SL: spend_ledger.record(tokens_in, tokens_out)
    SL-->>ORCH: DailySpend(cost_usd, ...)
    ORCH->>CB: check_circuit_breaker()
    CB->>SL: today()
    SL-->>CB: cost_usd
    alt cost_usd >= fleet_config.max_daily_token_spend
        CB->>Pol: set_global_autonomy(0)
        Note over Pol: exact same function the manual<br/>kill switch calls — one code path,<br/>no drift between the two triggers
    else under cap
        Note over CB: no-op
    end

5.9 Rollback

sequenceDiagram
    autonumber
    actor Reviewer
    participant GW as Agent Gateway
    participant RB as platform.rollback
    participant Store as Store

    Reviewer->>GW: POST /runs/{trace_id}/rollback
    GW->>RB: rollback_trace(trace_id)
    RB->>Store: CompensatingAction[] for trace_id,<br/>most-recent-first
    loop each un-rolled-back action
        RB->>Store: restore subject.field = before_value
        RB->>Store: action.rolled_back = true
    end
    Note over RB: idempotent — replaying a<br/>trace already rolled back is a no-op
    RB-->>GW: {trace_id, reverted[]}
    GW-->>Reviewer: 200 OK

5.10 Framework crosswalk query

sequenceDiagram
    autonumber
    actor Reviewer
    participant GW as Agent Gateway
    participant FC as Framework Crosswalk<br/>(deterministic)
    participant Ref as framework_crosswalk_reference.py
    participant Store as Findings (Store)

    Reviewer->>GW: GET /vendors/{id}/crosswalk?target_framework=ISO27001
    GW->>FC: compute_framework_coverage(vendor_id, "ISO27001")
    FC->>Store: finding_repo.list_for_vendor(vendor_id)
    Store-->>FC: Finding[] (only status == "satisfied" kept)
    FC->>Ref: CROSSWALK[soc2_control][target_framework]
    Ref-->>FC: equivalent target control, or none
    loop each crosswalk pair for this framework
        alt SOC2 control has a satisfied Finding
            FC->>FC: covered_controls.append({target_control, via_soc2_control, source_finding_id})
        else
            FC->>FC: gap_controls.append({target_control, via_soc2_control, reason})
        end
    end
    FC-->>GW: {covered_controls, gap_controls, coverage_pct}
    GW-->>Reviewer: 200 OK

6. State Diagrams

6.1 Vendor lifecycle

VendorStatus = "onboarding" | "active" | "under_review" | "offboarding" | "offboarded" (platform/models.py). offboarding/offboarded were defined in the type since this schema's first version but had no code path that ever set or cleared them, until agents/offboarding.py (initiate_offboarding / confirm_data_deletion) operationalized both.

stateDiagram-v2
    [*] --> onboarding: vendor_repo.get_or_create()
    onboarding --> active: assessment.requested handled<br/>(orchestrator._on_assessment_requested)
    active --> under_review: Drift Sentinel<br/>reopen_assessment()
    under_review --> active: assessment.requested handled again
    active --> offboarding: initiate_offboarding()<br/>(reversible, compensating action recorded)
    under_review --> offboarding: initiate_offboarding()
    offboarding --> offboarded: confirm_data_deletion()<br/>(terminal, not reversible —<br/>certifies a real-world fact)
    offboarding --> offboarding: offboarding_overdue signal<br/>(Drift Sentinel guards against<br/>reopen_assessment clobbering this state)
    offboarded --> [*]

6.2 Finding status

FindingStatus = "satisfied" | "gap" | "exception" | "unknown". The stale-evidence fail-closed rule (section 10) is the one transition that happens inside create_finding itself, not from the agent's own judgment.

stateDiagram-v2
    [*] --> satisfied: Risk Assessor judges<br/>evidence/assertion supports it
    [*] --> gap: nothing supports it,<br/>or evidence contradicts it
    [*] --> exception: negotiated exception<br/>on record (Memory Bank)
    [*] --> unknown: no data either way
    satisfied --> unknown: cited evidence is stale<br/>(fail-closed, forced downgrade)
    gap --> [*]: finding.created published →<br/>Remediation Router opens a ticket
    note right of unknown
        requires_human = true whenever:
        - downgraded from satisfied (stale evidence)
        - vendor.tier == "critical"
        - residual_risk >= RESIDUAL_RISK_HUMAN_THRESHOLD
    end note

6.3 Fleet autonomy ladder

Global dial (fleet_config.autonomy_level) — every agent's effective ceiling is min(own registered ceiling, this global level), and 0 whenever the agent is individually paused.

stateDiagram-v2
    [*] --> L3
    L3: L3 - Act autonomously<br/>(reversible, low-blast-radius only)
    L2: L2 - Act with approval<br/>(execute only after a recorded human decision)
    L1: L1 - Draft<br/>(produce artifacts; nothing leaves the system)
    L0: L0 - Observe<br/>(read and log only)

    L3 --> L0: POST /fleet-config autonomy_level=0<br/>(manual kill switch)
    L3 --> L0: circuit breaker trips<br/>(daily spend cap breached)
    L0 --> L3: POST /fleet-config autonomy_level=3
    L3 --> L2
    L2 --> L1
    L1 --> L0
    L0 --> L1
    L1 --> L2
    L2 --> L3
    note right of L0
        Every L1+ tool call across
        every agent raises AutonomyBlocked
        on its very next invocation,
        checked live, not cached.
    end note

6.4 Answer status

AnswerStatus = "auto" | "needs_human" | "approved" | "blocked_dlp" (draft_answer in agents/questionnaire_responder.py).

stateDiagram-v2
    [*] --> auto: confidence >= threshold<br/>AND no DLP-pattern hit
    [*] --> needs_human: confidence < ANSWER_CONFIDENCE_THRESHOLD<br/>(honest abstention)
    [*] --> blocked_dlp: DLP-style scan<br/>finds a leak pattern
    auto --> approved: POST /questionnaires/id/export<br/>(only auto answers are exportable)
    needs_human --> [*]: excluded from export,<br/>reason surfaced in excluded_reasons
    blocked_dlp --> [*]: excluded from export,<br/>never leaves the system

7. Deployment Diagram

flowchart TB
    subgraph Client["Client side"]
        Browser["curl / Devpost judge /\nbuyer / vendor portal"]
    end

    subgraph GCP["Google Cloud Project"]
        subgraph CloudRun["Cloud Run — service: bulwark"]
            App["FastAPI app\n(uvicorn, --min-instances=0\n--max-instances=3)"]
        end

        Scheduler["Cloud Scheduler\n2 jobs: evidence-sweep, drift-sweep"]

        subgraph PubSub["Pub/Sub"]
            Topics["11 topics + 11 .dlq topics\n(mirrors platform/event_bus.py\nwhen USE_PUBSUB=true)"]
        end

        Firestore[("Firestore\n(Native mode)\nvendors · artifacts · findings ·\ncontract_terms · concentration_risks ·\nassessment_snapshots · offboarding_records ·\ndigests · ...")]

        subgraph IAM["IAM — 12 per-agent service accounts"]
            SA1["sa-supervisor"]
            SA2["sa-intake"]
            SA3["sa-evidence"]
            SA4["sa-assessor\n+ aiplatform.user"]
            SA5["sa-questionnaire\n+ aiplatform.user"]
            SA6["sa-sentinel"]
            SA7["sa-remediation\n+ secretmanager"]
            SA8["sa-contract\n+ storage.objectViewer"]
            SA9["sa-concentration"]
            SA10["sa-crosswalk"]
            SA11["sa-offboarding"]
            SA12["sa-digest\n+ aiplatform.user"]
        end

        Bucket[("Cloud Storage\nquarantine bucket\n(untrusted uploads)")]
    end

    subgraph AI["Vertex AI / Gemini API"]
        Gemini[["Gemini Flash + Pro"]]
    end

    Browser -->|HTTPS + X-API-Key| App
    Scheduler -->|POST /evidence-collector/tick\nPOST /drift-sentinel/tick| App
    App <--> Firestore
    App -.mirrors events.-> Topics
    App --> Gemini
    App -.reads quarantined docs.-> Bucket
    IAM -.identity for.-> App
    Bucket -.->|"storage.objectViewer"| SA2
    Bucket -.->|"storage.objectViewer"| SA8

8. Zero-Trust Identity Map

Every agent's exact allow/deny grant set (platform/identity.py) — this is what a compromised agent could and could not reach, in full.

flowchart LR
    subgraph Untrusted["Untrusted zone — reads attacker-adjacent input"]
        Intake["Intake\nsa-intake"]
        Contract["Contract Intelligence\nsa-contract"]
    end
    subgraph Internal["Internal"]
        Sup["Supervisor\nsa-supervisor"]
        RA["Risk Assessor\nsa-assessor"]
        QR["Questionnaire Responder\nsa-questionnaire"]
        DS["Drift Sentinel\nsa-sentinel"]
        Rem["Remediation Router\nsa-remediation"]
    end
    subgraph Deterministic["Internal, read-only, deterministic"]
        EC["Evidence Collector\nsa-evidence"]
        CA["Concentration Analyzer\nsa-concentration"]
        FC["Framework Crosswalk\nsa-crosswalk"]
    end
    subgraph Offboard["Internal, write, deterministic"]
        OB["Offboarding Agent\nsa-offboarding"]
    end
    subgraph Digest["Internal, broad read, narrow write"]
        ED["Executive Risk Digest\nsa-digest"]
    end

    Intake -->|allowed| GCS["gcs:quarantine:read"]
    Intake -->|allowed| AW["assertions:write"]
    Intake -.denied.-> EVR["evidence:read"]
    Intake -.denied.-> FW["findings:write"]

    Contract -->|allowed| GCS
    Contract -->|allowed| CTW["contract_terms:write\nsubprocessors:write\npubsub:publish"]
    Contract -.denied.-> EVR
    Contract -.denied.-> FW

    RA -->|allowed| ASR["assertions:read\nevidence:read\ncontrols:read"]
    RA -->|allowed| FWW["findings:write\nassessment_snapshots:write"]
    RA -.denied.-> GCS

    QR -->|allowed| EVR2["evidence:read"]
    QR -->|allowed| AnW["answers:write\nexport:dlp_gated"]
    QR -.denied.-> GCS

    DS -->|allowed| ASW["assessments:read/write\nassessment_snapshots:read\nmemory_bank:read/write\npubsub:publish"]
    DS -.denied.-> FW

    Rem -->|allowed| TicW["tickets:write\ndecision_packets:write\nsecrets:read"]
    Rem -.denied.-> SEND["send_email — does not exist\nas a function anywhere"]

    EC -->|allowed| CAI["cloud_asset_inventory:read\nsecurity_command_center:read\nevidence:write"]
    EC -.denied.-> FW

    CA -->|allowed| SPR["subprocessors:read\nvendors:read\nconcentration_risks:write"]
    CA -.denied.-> FW

    FC -->|allowed| FRD["findings:read (only)"]
    FC -.denied.-> FW

    OB -->|allowed| OBW["vendors:write\noffboarding_records:read/write\ncontract_terms:read"]
    OB -.denied.-> FW
    OB -.denied.-> GCS

    ED -->|allowed| EDR["findings:read\nvendors:read\nconcentration_risks:read\noffboarding_records:read\ndigests:write"]
    ED -.denied.-> FW
    ED -.denied.-> VW["vendors:write"]