A greenfield vertical slice of a governed payments platform: three domains — account (KYC), wallet, and transfer (P2P) — where an agent orchestrates through governed tools while deterministic services own their data and enforce the money-safety invariants transactionally. The account and wallet agents run on LangChain; the transfer agent runs on CrewAI — proving the governance path (kernel + policy + audit) is framework-agnostic.
Every model/tool action passes through an in-process governance kernel that
returns one of four verdicts — allow | deny | escalate | transform — and
records the decision to an append-only audit log.
browser → web-ui BFF → governed tool (kernel: policy + audit) → service API → PostgreSQL
- Services own their data (Database-per-Service).
account-service,wallet-service, andtransfer-serviceeach own a private PostgreSQL and are the only components that touch it. Others — including agents — reach data only through the service API. - The agent holds no database credentials. Its only capability is calling a service as a governed tool. The model never sees a connection string and cannot bypass a service invariant.
- Safety-critical logic stays deterministic.
balance >= 0, uniqueidempotency_key, one-wallet-per-user, and the KYC state machine are enforced in the service, in code — never left to the model. Policy is an advisory early-deny + audit layer on top.
| Component | Path | Role |
|---|---|---|
agent_lifecycle_platform |
agent_lifecycle_platform/ | Governance kernel, policy engine, audit + model wiring, governed-agent factory |
| account-service | services/account/service/ | Users + KYC system of record (FastAPI + Postgres) |
| wallet-service | services/wallet/service/ | Wallets + top-ups system of record (FastAPI + Postgres) |
| transfer-service | services/transfer/service/ | P2P transfers system of record (FastAPI + Postgres) |
| account-agent | services/account/agent/ | LangChain onboarding assistant (governed tools) |
| wallet-agent | services/wallet/agent/ | LangChain balance/top-up assistant (governed tools) |
| transfer-agent | services/transfer/agent/ | CrewAI P2P payment agent (governed tools) |
| audit-service | audit_service/ | Append-only JSONL audit sink + SSE stream |
| web-ui BFF | web_ui/ | Scenario console, approvals, live audit feed |
| model-service | model_service/ | Local open-weight model via Ollama (no API keys) |
Policies are declarative YAML (account,
wallet,
transfer). Each rule matches a
condition (field / operator / value, or a compound all / any)
against the tool-call context and prescribes a verdict. Highest-priority match
wins; otherwise the defaults.action (fail-closed deny) applies.
The kernel (agent_lifecycle_platform/kernel.py):
- wraps every tool so it is decided → audited → enforced on each call;
- fails closed — an escalation with no approver resolves to
deny; - refuses to start on an empty policy, so a mis-wired policy fails loudly instead of silently governing nothing;
- populates governance context explicitly (e.g. the wallet KYC gate reads the
user's KYC status into the decision context before
create_wallet).
Design note: this slice ships a self-contained kernel that speaks the plan's full four-verdict policy language. The upstream declarative engine only supports
allow/deny, and the upstreamGovernancePolicycannot express argument-conditional rules (caps, allow-lists), so a purpose-built kernel is the faithful realization of the plan's intent. Swapping in the upstream kernel later only touches agent_lifecycle_platform.
Requires Docker + Docker Compose.
# Full stack (services + Postgres x2 + audit + BFF + model + agents)
docker compose up -d
# Deterministic subset only (no LLM / model pull) — enough for the demo:
docker compose up -d account-db wallet-db account-service wallet-service audit-service web-ui
# Open the console
open http://localhost:8080
# Seed a few verified users
BASE=http://localhost:8080 scripts/seed.sh
# Run the golden scenarios headless (asserts every verdict)
BASE=http://localhost:8080 scripts/demo.shThe LLM-driven agents (account-agent, wallet-agent) and model-service are
included for the "agent narrative" path; the deterministic demo drives the BFF,
which routes every action through the same governance kernel.
By default (AGENT_ROUTE=on) every console action is first sent to the local
open-weight model (model-service, Ollama) so the LLM selects the governed
tool to call — the request genuinely goes "through the agent". Governance then
enforces the outcome, and the executed action is always the user's requested one,
so a flaky small model can never corrupt the money flow; the model's choice is
reported back (route: "agent", llm.chosen_tool, llm.matched) and shown in
the console. If the model is unreachable the BFF falls back to the deterministic
path automatically. Set AGENT_ROUTE=off (on the web-ui service) for the pure
deterministic path used by the test suite. model-service pulls llama3.2:3b
(~2 GB) on first start.
The console at http://localhost:8080 is a three-step flow — 1 · Onboard → 2 · Log in → 3 · Wallet — with a live governance panel and audit feed on the right. Full walkthrough: docs/ui-console.md.
- Onboard — create a user and submit KYC (must be verified to hold a wallet).
- Log in — act as a verified user (no password auth in this slice; login selects the acting user).
- Wallet — create a wallet, top up, and watch verdicts: $5,000 escalates (Approve/Reject), $12,000 and unlisted sources are denied, a repeated identical top-up is deduped by the service.
docker compose down -v # also removes Postgres + audit + model volumes| # | Action | Verdict | Enforced by |
|---|---|---|---|
| 1 | Onboard new user | allow | account policy |
| 2 | Submit KYC → verified | allow | account service state machine |
| 3 | Create wallet before KYC | deny | wallet policy (KYC gate) + service |
| 4 | Create wallet after KYC | allow | wallet policy |
| 5 | Top-up $50 from bank_ach |
allow | wallet policy + service |
| 6 | Top-up from crypto_unknown |
deny | wallet policy (source allow-list) |
| 7 | Top-up $5,000 | escalate | wallet policy + UI approval |
| 8 | Top-up $12,000 | deny | wallet policy (hard cap) |
| 9 | Duplicate top-up (same key) | service dedupe (409) | wallet service (unique constraint) |
| 10 | Read profile without purpose | transform (masked) | account policy |
| 11 | Out-of-scope tool (debit) |
deny | wallet policy (least privilege) |
| 12 | Send $25 to a friend | allow | transfer policy + service |
| 13 | Send $600 | escalate | transfer policy + UI approval |
| 14 | Send $6,000 | deny | transfer policy (per-transfer hard cap) |
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest -q- tests/test_policy_kernel.py — policy engine + kernel (fail-closed, escalate, transform, redaction).
- tests/test_account_service.py, tests/test_wallet_service.py, tests/test_transfer_service.py — service invariants (idempotency, balance, caps, KYC gate, flagged-recipient block, one wallet per user).
- tests/test_golden_scenarios.py — scenarios 1-11 end-to-end (governed tools → real services), model-agnostic.
- tests/test_bff.py — BFF decision paths.
- Fail-closed default deny; kernel refuses an empty policy (no silent fail-open).
- Non-root containers; agents hold no DB credentials.
- PII masked by default; full profile requires a declared data purpose.
- Sensitive argument keys are redacted before audit.
- Governance and service authorization are independent gates — either one denying stops the action.