BULWARK¶
Continuous Third-Party Assurance Fleet¶
A twelve-agent fleet for the Fortified Enterprise Fleet track that
collapses the blind window -- the time between a risk-relevant signal
existing in the world and a human at your company knowing about it --
from roughly half a review cycle down to the length of one scheduled
sweep, for both directions of third-party risk: assessing your vendors,
and answering buyers' security questionnaires from the same evidence
graph. Five of the twelve agents, plus a predictive upgrade to Drift
Sentinel, go past the spec to solve pain no per-vendor checklist review
can reach: Contract Intelligence automates the manual
MSA/DPA-vs-playbook review that eats hundreds of paralegal hours a year,
Concentration Analyzer catches the portfolio-level blind spot where
"diversified" vendors secretly share a subprocessor -- the failure
pattern behind incidents like the 2024 CrowdStrike outage --
Framework Crosswalk cuts the redundant evidence collection
enterprises repeat across every overlapping compliance framework they
carry, the Offboarding Agent operationalizes the DPA-mandated
data-deletion deadline every vendor termination creates -- an obligation
that otherwise lives in a spreadsheet, if it's tracked at all --
Executive Risk Digest synthesizes the fleet's 44 API endpoints of
state into a short prioritized narrative a busy executive can actually
read, and Drift Sentinel's new risk_trend_rising signal catches a
control's risk climbing across reassessments before it becomes a hard
gap.
A React dashboard
sits on top of the same API surface -- fleet health and the kill switch,
per-vendor findings with the full reasoning chain behind each one, the
assessment-history risk-trend chart, concentration risks, the executive
digest, and trace timelines -- so the fleet is drivable without
curl/jq, though every one of those curl examples below still works
identically.
See Architecture for the full system diagram, the
per-agent identity/autonomy table, and the exact event-bus wiring for
all three loops; Diagrams for the full
HLD/LLD/DFD/ERD/sequence/state/deployment diagram set;
Setup Guide for detailed install/configuration/deployment
instructions beyond the quick-start below; User Guide
for every workflow and API endpoint with worked examples;
frontend/README.md
for the dashboard's own setup and design notes; and
DEMO_VIDEO_SCRIPT.md
for the ~4-minute demo script, and
SUBMISSION.md
for the Devpost submission text.
Hackathon requirements checklist¶
Track: Fortified Enterprise Fleet.
Technology requirements (all tracks):
| Requirement | This build |
|---|---|
| Gemini 3.5+ via Gemini API or Vertex AI | GEMINI_FLASH_MODEL / GEMINI_PRO_MODEL default to gemini-3.5-flash / gemini-3.1-pro-preview (src/bulwark/config.py), callable via either the Gemini API (GOOGLE_API_KEY) or Vertex AI (GOOGLE_GENAI_USE_VERTEXAI=true), see "Configure Gemini" below. gemini-3.5-flash is GA; there's no GA Gemini 3.5+ Pro model yet, so Risk Assessor's Pro tier runs the newest available Pro generation, gemini-3.1-pro-preview (preview -- override GEMINI_PRO_MODEL if your project lacks allowlist access to it) |
| A Google Agent Framework | Google ADK (google-adk) -- every agent is an ADK LlmAgent (or, for the three deliberately non-LLM agents, plain Python called the same way); agents/orchestrator.py runs them through ADK Runners |
| A Google Cloud infrastructure service | Cloud Run (deploy/deploy_cloud_run.sh, scale-to-zero), Firestore (platform/store.py, live when GOOGLE_CLOUD_PROJECT is set), Pub/Sub (platform/event_bus.py, mirrored live when USE_PUBSUB=true) -- all three, not just one |
Fortified Enterprise Fleet pillars (from the track brief) -- every one is live code, not a diagram:
| Pillar | This build |
|---|---|
| Agent Registry (publish/version/discover) | platform/registry.py, live at GET /registry -- 12 agents, each with a departments tag for cross-department reuse (security/procurement/legal/sales-engineering) |
| Agent Runtime (long-running async execution) | ADK Runners in agents/orchestrator.py; Drift Sentinel's sweeps checkpoint progress via platform/models.py's RunRepo |
| Memory Bank (persistent cross-session context) | platform/memory_bank.py -- per-vendor negotiated exceptions and accepted-risk decisions that outlive any single sweep, with expires_at windows spanning weeks, not just the current session |
| Agent Identity (zero-trust access control) | platform/identity.py -- a per-agent allow/deny grant table (tests/test_identity.py exercises the denials directly, not just the allows) |
| Agent Gateway (unified routing + policy) | api/routes.py + platform/auth.py -- API-key auth, rate limiting, and the autonomy-ladder kill switch enforced on every route |
| Model Armor (guardrails) | platform/guardrails.py -- blocks prompt injection before any LLM call, scans DLP-style patterns before an answer is marked exportable, applied as an ADK plugin so it can't be bypassed by a single agent's prompt |
| Agent Observability (OTel audit + reasoning traces) | platform/observability.py -- OpenTelemetry spans plus a persisted reasoning-chain record per decision, replayed at GET /traces/{id} and GET /findings/{id}/explain |
Track-specific demonstrations the brief calls out explicitly:
- Cross-department reuse --
GET /registryshows the same 12 agents tagged for reuse across Security, Procurement, Legal, and Sales Engineering (platform/registry.py'sdepartmentsfield on everyAgentRecord). - Asynchronous context persistence over weeks --
platform/memory_bank.py's negotiated exceptions and accepted-risk decisions carry anexpires_atand are read by every subsequent Drift Sentinel sweep, however many weeks later that sweep runs;has_active_exceptionis what stops the fleet from re-litigating an already-accepted risk every six hours. - Compliance/data-sovereignty-aware production data interaction --
platform/models.py::TenantReporecords aregion_pinechoed on every eventEnvelope; findings on a critical-tier vendor or above the residual-risk threshold forcerequires_human = trueregardless of autonomy level (section 6.4's mandatory HITL gates); see "Mechanisms worth reading in the code directly" in Architecture.
Judging-criteria alignment:
| Criterion | Weight | Where to look |
|---|---|---|
| Innovation & Operational Utility | 40% | The fleet acts autonomously end-to-end (screens → extracts → cross-references → decides → tickets, with no human in the loop unless a mandatory gate fires); Contract Intelligence, Concentration Analyzer, Framework Crosswalk, the Offboarding Agent, Executive Risk Digest, and the risk-trend early-warning signal solve real, underserved enterprise pain (manual contract review, hidden portfolio concentration risk, redundant cross-framework evidence collection, untracked data-deletion obligations, 30+ endpoints nobody has time to click through, reactive-only risk detection) rather than adding checklist coverage -- see "Beyond the spec" in Architecture |
| Architectural Discipline & Tech Stack | 30% | Event-driven decoupling (tests/test_orchestrator_wiring.py asserts no agent module imports another), zero-trust identity, a real circuit breaker sharing code with the kill switch, compensating-action rollback, citation-validated anti-hallucination gates -- see "Mechanisms worth reading in the code directly" in Architecture |
| Demo & Production Readiness | 30% | 195 passing tests requiring zero live credentials (PYTHONPATH=src pytest -q), a one-command local spin-up, deploy/setup_gcp.sh + deploy/deploy_cloud_run.sh for real Cloud Run deployment, and DEMO_VIDEO_SCRIPT.md for the required demo video |
What it does¶
Forward a vendor's SOC 2 report (or ISO cert, DPA, pen-test summary) and BULWARK runs the whole onboarding cycle on its own: screen it for prompt injection, extract structured claims, cross-reference them against your control framework and independently-observed evidence, decide whether anything is actually a gap, and -- only if so -- open a ticket and draft an appeal citing the exact codes. Weeks later, a scheduled sweep notices a certificate about to expire or your own controls drifting, and reopens the affected assessment without anyone asking. The same evidence graph answers a buyer's security questionnaire, with a citation on every answer and an honest abstention wherever the evidence doesn't support one.
Features¶
- Contract Intelligence: automated MSA/DPA review against a legal
playbook.
extract_contract_termscompares every clause it finds againstagents/contract_playbook.pyand flags deviations (an unlimited-liability clause, a breach-notification window longer than the playbook allows, silence on subprocessor flow-down) as structured, actionable data instead of a paralegal's hand-written notes. - Concentration Analyzer: catches the risk no single-vendor review
can see. A dozen vendors that look independently diversified but all
secretly depend on the same subprocessor turn one incident into a
correlated failure across every one of them at once. This agent
clusters every subprocessor
Contract Intelligencehas ever extracted by name across the whole portfolio and flags the overlaps, weighted by how many affected vendors are critical-tier. - Framework Crosswalk: audit fatigue reduction across overlapping
compliance frameworks. SOC 2 for one buyer, ISO 27001 for another,
NIST CSF for a third -- standard tooling re-collects evidence for each
independently.
compute_framework_coveragereports which target- framework controls a vendor already indirectly satisfies via its existing SOC 2 findings, each covered entry citing thefinding_idthat justifies it, so a compliance team sees exactly what's left instead of starting from zero. - Predictive risk-trend early-warning.
Findingoverwrites in place on every reassessment, so on its own it can never show a trajectory.AssessmentSnapshotis append-only, and Drift Sentinel's fourth signal (risk_trend_rising) flags a control's residual risk climbing across three consecutive reassessments before any single one crosses a hard gap threshold -- reactive detection becomes predictive. - Offboarding & termination assistance tracking.
VendorStatushas had an"offboarding"value since the very first version of this schema, with no code path that ever set or cleared it. This agent operationalizes it:initiate_offboardingstarts the data-deletion clock every DPA'stermination_assistanceclause creates (using the vendor's own contracted deadline if Contract Intelligence found one, else the playbook default),confirm_data_deletioncloses it out, and Drift Sentinel's fifth signal (offboarding_overdue) flags a vendor still holding your data past the date they were contractually required to delete it -- a real, auditable data-retention exposure that otherwise lives in a spreadsheet, if it's tracked at all. - An executive digest for a fleet with 44 endpoints nobody has time to
click through.
gather_digest_inputsdeterministically pulls the week's most urgent state (critical-tier gaps, top findings by residual risk, concentration risks, overdue offboardings); the Executive Risk Digest Agent turns that into a 3-5 paragraph prioritized narrative a busy executive can read in under a minute, with every claim grounded in the inputs it was given, never invented. - Twelve agents, one event bus, zero direct agent-to-agent calls.
Every handoff is
bus.publish/bus.subscribeon a named topic (platform/event_bus.py);tests/test_orchestrator_wiring.pyasserts this mechanically. - A hardcoded security gate, not a prompted one. An untrusted vendor artifact is screened by a deterministic Model Armor scan before any LLM call, and if clean, is only ever read by the Intake Agent -- the Supervisor's LLM never sees untrusted content at all, so there's no instruction for an injection to override in the first place.
- A citation-enforced anti-hallucination gate. Risk Assessor's
create_findingrefuses to persist a finding with zero citations, or a citation to an id that doesn't exist -- checked in code, not asked of the model. - A live kill switch, and a circuit breaker that pulls the same
lever.
POST /fleet-config {"autonomy_level": 0}and every L1+ action across all twelve agents starts raisingAutonomyBlockedon its next tool call.pause_agent_iddoes the same for one agent. A daily token-spend cap breach calls the exact same function automatically (platform/spend.py). No redeploy either way. - Per-agent zero-trust identity. The two agents that read
attacker-adjacent input (Intake, and Contract Intelligence for vendor
contracts) have no grant to read evidence or write findings; the agents
that can write findings never touch a vendor document.
platform/identity.pyis the enforced table, not a diagram. - A deliberately non-LLM agent. Evidence Collector costs zero tokens and has zero prompt-injection surface, because comparing an observed value to a policy is a lookup, not reasoning -- see "Findings and learnings" below.
- Abstention as the feature, not a fallback. Questionnaire Responder answers what it can cite and confidently flags the rest for a human, rather than guessing.
- Mandatory human review that a higher autonomy level can't override.
A finding on a critical-tier vendor, a residual risk above threshold,
or evidence too stale to trust all force
requires_human = truein code (agents/risk_assessor.py) -- and outbound vendor email has nosendfunction anywhere in this codebase, only a draft gated on an already-recorded human decision. - Every finding explains itself.
create_findingpersists the alternatives it weighed (with scores and why each was or wasn't chosen);GET /findings/{id}/explainreplays that reasoning chain alongside the finding, not just its final verdict. - Reversible by construction -- except where reversing would be
dishonest. The autonomous (L3) actions that change state and could
legitimately be undone -- reopening an assessment, opening a ticket,
initiating offboarding -- each write a compensating-action record;
POST /runs/{trace_id}/rollbackreplays them in reverse, idempotently.confirm_data_deletiondeliberately does not: it certifies a real-world fact (the vendor's data is gone), and reversing a database field wouldn't un-delete it.
Technologies used¶
- Gemini (Flash for seven agents, Pro reserved for Risk
Assessor's final cross-referencing judgment; Evidence Collector,
Concentration Analyzer, Framework Crosswalk, and the Offboarding Agent
make zero LLM calls at all), via the
Google ADK (
google-adk) - Google Cloud Run -- scale-to-zero deployment
- Pub/Sub -- the event bus, mirrored live when
USE_PUBSUB=true - Firestore -- the control-evidence graph, audit log, Memory Bank (in-memory fallback for local dev/tests)
- Cloud Scheduler -- the two sweep cadences
- FastAPI -- the Agent Gateway's HTTP surface
- OpenTelemetry -- Agent Observability spans
What's live vs. what's documented¶
Everything above the line in this table is real code, exercised by the
test suite, that runs unmodified whether GOOGLE_CLOUD_PROJECT is unset
(local dev) or set (Cloud Run). Below the line is represented by
equivalent-purpose logic and documented for the real integration, because
this repo has no GCP project to verify a live binding against -- the
table says so directly rather than implying broader GCP bindings than a
hackathon build actually has.
| Live | Represented / documented |
|---|---|
| Agent Registry, Agent Identity, Model Armor, Agent Observability, the event bus + DLQ + idempotency, the kill switch, Firestore-shaped storage, Memory Bank, Cloud Run deploy | BigQuery audit warehouse (the audit log is real and queryable via GET /traces/{id}; a live BigQuery export isn't wired) · Cloud DLP (represented by guardrails.scan_for_dlp_violation, same pattern/patterns a DLP inspection job would flag) · Vertex AI Vector Search (represented by a substring match over control titles in questionnaire_responder.search_evidence) · Cloud Asset Inventory / Security Command Center / IAM / Logging / GitHub / Jira (represented by agents/internal_sources.py, a small labeled mock dataset, illustrative rather than a live integration) |
deploy/setup_gcp.sh provisions the real Pub/Sub topics, per-agent
service accounts with least-privilege IAM, and Firestore; swapping the
mocked sources/DLP/Vector Search for live calls is a matter of replacing
those functions' bodies behind the same return shape, not a redesign.
Repository layout¶
src/bulwark/
agents/ 12 ADK agent definitions + orchestrator.py (event-bus wiring) + mock internal sources
+ contract_playbook.py, framework_crosswalk_reference.py (illustrative reference data)
platform/ store, event_bus, registry, identity, guardrails, observability, policy (kill switch),
spend (circuit breaker), rollback (compensating actions), models (data graph + reasoning
records + assessment snapshots + tenants + offboarding records + digests), memory_bank, auth
api/ FastAPI routes + schemas (44 routes -- see architecture.md's API surface table)
main.py ASGI entrypoint (Cloud Run / uvicorn)
tests/ 195 tests, no live Gemini credentials required
deploy/ setup_gcp.sh, deploy_cloud_run.sh
docs/ architecture.md (diagram + agent table + event table + API surface + mechanisms)
+ DIAGRAMS.md (HLD/LLD/DFD/ERD/sequence/state/deployment diagrams, all Mermaid)
+ SETUP_GUIDE.md (detailed install/config/deployment) + USER_GUIDE.md (every
workflow + API endpoint, worked examples)
scripts/ seed_demo_data.py, demo_cli.py
frontend/ React + Vite + TypeScript dashboard over the API surface above -- see frontend/README.md
SUBMISSION.md Devpost submission text, drafted from this README
DEMO_VIDEO_SCRIPT.md The required ~4-minute demo video, scripted
Spin-up instructions¶
1. Install dependencies¶
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pip install pytest pytest-asyncio httpx # dev/test only
2. Run the test suite (no cloud setup or API keys required)¶
Every deterministic piece -- guardrail detection/redaction, the citation-validation gate, the autonomy ladder and kill switch, the event bus's DLQ and idempotency dedup, Drift Sentinel's expiry/drift detection, Questionnaire Responder's confidence/DLP gating, and the full API request flow -- is tested directly, without needing a live Gemini call.
3. Run the service locally¶
export PYTHONPATH=src
export BULWARK_SEED_DEMO_DATA=true # seeds vendors, findings, a poisoned artifact, a questionnaire on startup
uvicorn bulwark.main:app --reload --port 8080
No GOOGLE_CLOUD_PROJECT needed for this -- everything runs in-memory.
BULWARK_SEED_DEMO_DATA=true runs scripts/seed_demo_data.py's scenario
inside this same process on startup -- important, because the store is
plain in-memory by default: running that script as a separate python
process afterward would seed a store the running server never sees.
Explore immediately, no credentials required:
curl -s -H 'X-API-Key: demo-key' http://localhost:8080/registry | jq
curl -s -H 'X-API-Key: demo-key' http://localhost:8080/vendors | jq
curl -s -H 'X-API-Key: demo-key' http://localhost:8080/evidence-collector/tick -X POST | jq # deterministic, works with zero credentials
4. Run the dashboard¶
Open Connection in the sidebar to confirm the Base URL (http://localhost:8080
by default) and API key (demo-key by default) match your running
server -- both live in localStorage, per browser. See
frontend/README.md
for building/deploying it.
5. Configure Gemini and run a full agent cycle¶
# Path A: Gemini API / AI Studio
export GOOGLE_API_KEY=your-key
# Path B: Vertex AI (uses Application Default Credentials)
export GOOGLE_CLOUD_PROJECT=your-project
export GOOGLE_GENAI_USE_VERTEXAI=true
gcloud auth application-default login
Then, with the server running:
This drives the real multi-agent cycle end to end: blocks a poisoned artifact, onboards a clean vendor, prints its full reasoning-chain trace, answers a questionnaire, triggers both sweeps, and demonstrates the kill switch live.
6. Deploy to Cloud Run¶
export PROJECT_ID=your-project
export REGION=us-central1
./deploy/setup_gcp.sh # APIs, Firestore, Pub/Sub topics + DLQs, 12 per-agent service accounts
./deploy/deploy_cloud_run.sh # build + deploy (scale-to-zero, max-instances=3) + wires Cloud Scheduler to the live URL
Environment variables¶
| Variable | Purpose | Default |
|---|---|---|
GOOGLE_API_KEY |
Gemini API key (Path A) | unset |
GOOGLE_CLOUD_PROJECT |
GCP project (Path B / Firestore / Pub/Sub) | unset |
GOOGLE_GENAI_USE_VERTEXAI |
Use Vertex AI instead of the Gemini API | false |
GEMINI_FLASH_MODEL / GEMINI_PRO_MODEL |
Model ids | gemini-3.5-flash / gemini-3.1-pro-preview |
USE_FIRESTORE |
Use Firestore instead of in-memory stores | true if GOOGLE_CLOUD_PROJECT is set |
USE_PUBSUB |
Also mirror events onto real Pub/Sub topics | false |
BULWARK_API_KEYS |
Comma-separated Agent Gateway allowlist | demo-key |
BULWARK_CORS_ALLOW_ORIGINS |
Comma-separated origins allowed to call the API cross-origin (the dashboard's origin) | http://localhost:5173,http://127.0.0.1:5173 |
RATE_LIMIT_REQUESTS / RATE_LIMIT_WINDOW_SECONDS |
Gateway rate limit | 30 / 60 |
ANSWER_CONFIDENCE_THRESHOLD |
Questionnaire abstention threshold | 0.75 |
EVIDENCE_FRESHNESS_DAYS |
Evidence staleness window | 30 |
EVIDENCE_SWEEP_HOURS / DRIFT_SWEEP_HOURS |
Scheduler cadence | 6 / 24 |
RESIDUAL_RISK_HUMAN_THRESHOLD |
Mandatory-human-review risk threshold (of 1-25) | 15 |
fleet_config.max_daily_token_spend (the circuit breaker's cap) is a
runtime value, not an env var -- set it via POST /fleet-config or
FleetConfigRepo, since it's meant to be adjustable without a redeploy,
same as the kill switch it shares a code path with.
See .env.example for the full list.
Findings and learnings¶
- Not every agent in a fleet needs to be an LLM call. Evidence
Collector compares an observed value to a policy value -- that's a
lookup, not reasoning. Making it deterministic Python instead of an ADK
LlmAgentcosts zero tokens on a step that runs every six hours across every control, and removes an entire class of risk: there's no prompt for anything to inject into, because there's no prompt. Good agent design sometimes means recognizing where an agent isn't needed at all. - The security-critical decision belongs in code, not in a prompt. Two gates in this build are deliberately not "the agent is instructed to..." -- the untrusted-provenance routing gate (an untrusted event literally cannot reach the Supervisor's context window) and the citation-validation gate on findings (rejected by a post-processor before persistence). An instruction can be argued with by a good enough injection; a code path that was never wired can't be.
- Autonomy ceilings need two independent checks, not one. An agent's
own registered ceiling (what it declared it's allowed to do) and the
global kill switch (what's currently allowed fleet-wide) are separate
gates in
platform/policy.py. Collapsing them into one dial would mean bumping the global level for one agent's legitimate need accidentally raises every other agent's ceiling too. - A circuit breaker that doesn't share code with the kill switch isn't
really a circuit breaker. It would be easy to have spend-cap breach
set some separate
emergency_stopflag that every check site would also need to know about. Insteadcheck_circuit_breaker()just callsset_global_autonomy(0)-- the exact function the manual kill switch calls -- so there's only ever one "is the fleet allowed to act" answer to get right, not two that could disagree. - The correlation id you actually have beats the one the spec named.
Section 8 says
POST /runs/{id}/rollback, but this build'sRunrecords only exist for Drift Sentinel's own sweep checkpointing --trace_idis what's actually threaded through every hop of a causal chain (every Envelope, every audit entry, every reasoning record). Rather than force-fitting arun_idinto functions that don't naturally have one, rollback groups bytrace_idand says so in the docstring, rather than pretending the path parameter's name is the whole story. - Mock data belongs to a table with a straight face about which parts
are mocked.
agents/internal_sources.pyis illustrative, not a live Cloud Asset Inventory call; the README says so directly rather than implying otherwise in a demo -- a genuinely better story for judges than pretending a hackathon build has live bindings to six different GCP security products.
Cost controls¶
Cloud Run deployed with --min-instances=0 (scales to zero) and
--max-instances=3; Flash is the default model for seven of twelve
agents, with Pro reserved for the one step that needs it; Evidence
Collector, Concentration Analyzer, Framework Crosswalk, and the
Offboarding Agent make zero LLM calls at all; Firestore instead of an
always-on database; the API is protected by
the app's own X-API-Key check. Turn the Cloud Run service and Cloud
Scheduler jobs off after recording your demo.