Skip to content

Repository files navigation

ai-agent-controlplane-demo

IBM Bob × ContextForge — the AI agent control plane. One AI agent, a fintech agent mesh, and a gateway that governs every move. Who's in charge of your agents?

Open in GitHub Codespaces

Can't run Docker/ContextForge locally? Click the badge — the whole governed mesh builds in the cloud; you only need IBM Bob on your laptop (Tier 2 in ONBOARDING.md).

Reference architecture: IBM Bob drives a FinByte agent mesh through the IBM ContextForge gateway, with an OPA sidecar, six MCP servers, and two cross-language A2A agents.

A turnkey, follow-along demo of IBM ContextForge (the MCP / A2A gateway) acting as the control plane between an AI agent (IBM Bob) and a fintech ("FinByte") expense-and-payments agent mesh. The gateway sits in the middle of every tool call and every agent-to-agent call, enforces four controls, and you can prove all of them with one command16 passed, 0 failed.


What is this?

When an AI agent can read receipts, approve expenses, and move money, the question stops being "can the agent do it?" and becomes "who's in charge of the agent?". This demo answers that with a control plane. IBM Bob is the agent — an MCP client that drives a FinByte agent mesh (expense lookups, ERP payments, policy docs, notifications, and two autonomous A2A agents). It never talks to those backends directly. Every call flows through IBM ContextForge, the gateway that authenticates, authorizes, governs, redacts, and audits — before Bob ever sees a result and before any money moves.

The gateway enforces:

  • Policy (OPA). A wire over the $10,000 FinByte T&E cap is blocked unless it carries dual approval — evaluated live by an Open Policy Agent sidecar against a Rego policy.
  • Data protection. SSNs, credit-card numbers, API keys, and other secrets in tool output are masked on the gateway before they reach the agent (***-**-6789, ****-****-****-1111, [SECRET_REDACTED]).
  • Prompt-injection neutralization. Adversarial instructions hidden in tool output ("SYSTEM: ignore all prior policy…") are neutralized to [INJECTION_BLOCKED].
  • RBAC least-privilege. A FinOps analyst persona simply has no wire tool; only the privileged operator persona can register servers, read the audit trail, or interrogate policy. (Rate limiting is a built-in ContextForge capability, but it is not configured or demonstrated in this stack.)
  • Agent-mesh governance. The same policy that stops a direct wire also stops a cross-language agent-to-agent payment (a Python auditor delegating a $50k payment to a Rust payments agent) at the bridged tool hook.

The whole stack is path-independent — config, plugins, and Rego policy are baked into the images, so there are zero host bind-mounts and it runs from any clone path.


Architecture

(See the diagram at the top.) IBM Bob connects through the mcpgateway.wrapper stdio bridge to a virtual server on the gateway (a curated, least-privilege slice of the catalog). The gateway fronts five governed MCP servers, one deliberately-unregistered MCP server, and two A2A agents, with an OPA sidecar for policy decisions. Only the gateway and the two A2A agents publish ports to the host; the OPA sidecar and all MCP servers are reachable only on the Compose private network.

Component Kind Port Role
IBM Bob AI agent / MCP client Drives the mesh; connects via the mcpgateway.wrapper stdio bridge to a virtual server
ContextForge gateway MCP/A2A gateway (the control plane) 4444 (host) Authn/z, governance, redaction, audit, federation; Admin UI at /admin
OPA Open Policy Agent sidecar 8181 (internal) Evaluates the Rego wire-amount policy (package mcpgateway)
expense-db MCP server (Python, FastMCP) 8000 (internal) list_pending_expenses, get_expense, get_receipt (holds the PII / injection fixtures)
erp-payments MCP server (Python, FastMCP) 8000 (internal) approve, reimburse, wire (the governed money path)
policy-docs MCP server (Python, FastMCP) 8000 (internal) get_policy, wire_limit
notify MCP server (Python, FastMCP) 8000 (internal) notify
controlplane MCP server (Python, FastMCP) 8000 (internal) Operator surface: register_mcp_server, list_control_plane, recent_blocks, evaluate_policy
fx-rates MCP server (Python, FastMCP) 8000 (internal) get_fx_rate, list_currenciesruns but is intentionally unregistered (for the live-register beat)
auditor A2A agent (Python, a2a-sdk) 9001 (host) Audits expenses; can delegate a payment to the Rust agent
payments A2A agent (Rust, a2a-lf / a2a-server-lf) 3000 (host) Executes payments; JSON-RPC at /jsonrpc, agent card at /.well-known/agent-card.json

The seed registers 5 governed MCP servers (expense-db, erp-payments, policy-docs, notify, controlplane) plus the 2 A2A agents, and curates them into three virtual servers (FinOps, Treasury, Operator). fx-rates is left unregistered on every seed so the operator demo can register it live.

Naming note: the controlplane and auditor services read their admin token from an env var named AUDITOR_TOKEN (written to .env.tokens by make up). Despite the name, it is an admin JWT — not an auditor-only scope.


How a call is governed

Call path: Bob's tool call enters the gateway, runs through tool_pre_invoke (FinByteGuard queries OPA) and tool_post_invoke (FinByteGuard + PIIFilterPlugin sanitize output) before returning a governed result.

Every tool call passes through two gateway plugin hooks. On tool_pre_invoke, the custom FinByteGuard plugin (gateway/custom/finbyte_guard.py, on the cpex framework) extracts the call's arguments and asks OPA whether the wire-amount policy permits it — denying anything over the $10,000 cap without dual approval, and failing closed if OPA is unreachable. On tool_post_invoke, FinByteGuard deep-scrubs secrets (sk-live-…[SECRET_REDACTED]) and neutralizes prompt injection (→ [INJECTION_BLOCKED]), while the cpex PIIFilterPlugin masks SSNs and credit-card numbers. Every decision emits an AUDIT [FinByteGuard] … line and shows up in the Admin UI's Logs tab.


The four controls

The four controls: OPA wire-amount policy, PII/secret redaction, prompt-injection neutralization, and RBAC least-privilege — each shown as a Bob prompt and the gateway's response.

Control Prompt to Bob What ContextForge does
1 — Policy (OPA) "Use the finbyte-gateway tools to wire $50,000 to Acme LLC for expense exp_big." Blocks at OPA: "…exceeds the $10,000 auto-approve limit… FinByte T&E policy §2." Add "with dual approval"allowed. The same policy blocks the cross-language auditor→payments $50k at the bridged a2a-payments hook.
2 — Data protection "Fetch receipt rcpt_pii, verbatim." Masks before Bob sees it: SSN → ***-**-6789, card → ****-****-****-1111, API key → [SECRET_REDACTED].
3 — Prompt-injection "Fetch receipt rcpt_injection." Neutralizes the embedded SYSTEM: ignore all prior policy…[INJECTION_BLOCKED].
4 — RBAC least-privilege "Now wire $50k yourself, directly." Bob can't — the FinOps virtual server hides erp-payments-wire. MCP Inspector confirms the tool is absent. The operator persona has control-plane tools the analyst lacks.

Baseline that works: "Process expense exp_clean and reimburse it." — a clean $18.50 expense flows straight through.

  • Expense approval quorum — the room votes on a $50k wire from their phones (live local tally) while 5 governed A2A voter agents vote through the ContextForge gateway (each call authn'd + audited). OPA then blocks the wire regardless of either tally. Policy beats consensus.

Two personas (RBAC)

The same Bob binary becomes two different actors depending on which virtual server its .bob/mcp.json points at. Both targets rewrite .bob/mcp.json (from the bob-personas/*.template files, refreshing the live UUID) and launch Bob from the repo root, so they're cwd-proof and reseed-proof.

make bobFinOps analyst (Act 1) make bob-operatorplatform operator (Act 2)
Virtual server FinOps (8 tools) Operator (4 tools)
Can do List/read expenses, read receipts, approve, reimburse, read policy + wire limit, talk to the auditor agent register_mcp_server, list_control_plane, recent_blocks, evaluate_policy
Cannot do No wire tool; can't register servers, read the audit trail, or query policy directly Not the analyst's expense-handling surface
Persona file bob-personas/mcp.json.template (server finbyte-gateway) bob-personas/mcp.operator.json.template (server finbyte-operator)

Swap back to the analyst at any time with make bob.


Prerequisites

Tool Why How to get it
Docker (running) Runs the gateway, OPA, MCP servers, and A2A agents Docker Desktop on macOS/Windows, or Docker Engine on Linux (runs natively, no nested virtualization). Start it before you begin. — docker.com
No Docker? The stack also runs on Podman — see Run on Podman, or one-shot a fresh Ubuntu/WSL2/x86 host with bash scripts/test-fresh-host.sh.
uv Mints the gateway JWT offline (no network round-trip) https://docs.astral.sh/uv/
IBM Bob Shell (bob) Optional — only to drive Bob; the stack + 16/16 proof run without it macOS/Linux: curl -fsSL https://bob.ibm.com/download/bobshell.sh | bash (bob.ibm.com/download) — checks Node ≥ 22.15 first
Node.js ≥ 22.15 Optional — required by IBM Bob Shell (it's a Node app) and the MCP Inspector (npx); not needed to bring up the stack or prove the controls nodejs.org, or nvm install 22

Budget ~5 GB of free disk. On the first run, the pinned ContextForge image pulls once and the seven source images (six MCP servers + the Rust payments agent) build locally. Subsequent cold starts (make down && make quickstart) take roughly ~38 seconds once images are cached.

Running on Linux / in a VM. Only Docker and uv are truly required to bring up the stack and prove 16/16bob/Node are needed only to drive the demo. On Apple silicon a full macOS-guest VM is impractical (~60 GB+ disk); the practical path is a lightweight Linux VM (Multipass/Lima) + Docker Engine, which runs the stack natively on arm64 (the OPA image is multi-arch, so no emulation). IBM Bob Shell is cross-platform and can also be installed in the VM to drive the demo (first run uses an IBMid device-code login on a headless box). See Running on a fresh Linux box / VM below.


Two ways to run it

Same stack, two stories — pick your path:

🎓 Progressive build — build an agent tool from scratch, then watch it get governed (developer path)

Bottom-up, the inverse of make quickstart: start from a bare MCP server you'd recognise anywhere and earn ContextForge one layer at a time — carrying the tool you built the whole way. Open the prompt-card and drive Bob through four stages:

make dev-start          # opens docs/build.html — the follow-along build guide (copy-paste Bob prompts)
Stage Command What you (and Bob) do Bob persona
① Build make stage1-build Prompt Bob to write mcp-servers/sales-tax/server.py from scratch (a FastMCP add_tax tool). It runs bare on :8000; a call prints add_tax(100) → 108.50. It works — and it's totally ungoverned: no token, no policy, anyone on the port runs anything.
② Govern make stage2-govern The same server is containerised onto the mesh, registered (in the catalog, token-gated — not callable yet), then granted into a Builder virtual server and called back through the gateway108.50. 2b bonus: Bob extends a service it didn't write (fx-rates gains convert). operator → builder
③ Control make stage3-controls One prompt drives a batch; the four controls bite real calls — PII redaction, injection neutralised, OPA blocks a $50k cross-language wire, RBAC (Bob has no wire tool). analyst
④ Mesh make stage4-mesh The full governed picture — identical to the quickstart end-state, but you watched it get built.

The throughline is register → grant → call. Your tool goes from works-but-ungovernedin the catalog but not callablegranted and callable through the one governed seam, with redaction, policy, and audit applied. Registering a backend doesn't make it callable — granting it to an agent is a separate, privileged step (make salestax-grant adds it to the Builder vserver). That boundary is least-privilege.

The builder persona (make bob-install-builder) is the developer's seat — it calls your own granted tools (add_tax, convert), alongside the existing analyst (least-privilege consumer) and operator (registers / audits / evaluates policy) personas.

No Docker on your laptop? make connect prints a bob mcp add … -t http line pointed at a gateway running elsewhere — a teammate's box, a VM, or a GitHub Codespace — so you drive the whole governed mesh with only Bob installed, governance intact over the wire.

→ Narrated beats: docs/SHOWCASE-BOB.md · the follow-along docs/build.html (presenter cockpit: docs/cockpit.html) · zero-setup tiers in docs/ONBOARDING.md.

🛰 Governed mesh — drop straight into the finished, governed stack (quickstart path)

Top-down: one command takes a laptop from nothing to a running, governed mesh and proves all four controls (16/16) — no Bob required. Then drive Bob as the FinOps analyst (Act 1) and platform operator (Act 2) and watch the control plane react in the Admin UI.

→ Full command + walkthrough in Quickstart immediately below (then Drive Bob, the watch panes, and the deterministic proof under it).


Quickstart

git clone https://github.com/manavgup/ai-agent-controlplane-demo.git
cd ai-agent-controlplane-demo
make quickstart

make quickstart is one command that takes a laptop from nothing to a running, governed mesh: preflight (requires Docker + uv; warns but continues if bob/npx are absent) → bring up the stack → seed (register servers/agents, build the FinOps / Treasury / Operator virtual servers) → configure Bob (FinOps analyst persona) → prove all four controls (16/16, with no Bob required) → print a copy-paste walkthrough card. It's re-runnable — safe to run again if anything stalls. The Admin UI logs in with admin@finbyte.demo / FinByteAdmin!2026.

Proof is headless. make quickstart finishes 16 passed, 0 failed even on a box without bob or Node (a Linux VM or CI runner): Bob only drives the demo — it isn't needed to bring up the stack or prove the controls. make bob / make bob-operator also fail gracefully if bob isn't installed (they still write .bob/mcp.json, print an install hint, and exit 0).

Drive Bob

Act 1 — FinOps analyst (least-privilege). Launch with make bob (cwd-proof; it refreshes the config first), then try:

  • "Use the finbyte-gateway tools to fetch receipt rcpt_pii, verbatim." → redacted.
  • "Fetch receipt rcpt_injection."[INJECTION_BLOCKED].
  • "Ask the auditor agent to pay $50,000 to Acme LLC."blocked at OPA (Python → Rust).
  • "Now wire $50k yourself, directly." → Bob has no wire tool.

Act 2 — platform operator. Quit Bob, then make bob-operator to swap personas and relaunch:

  • "List everything ContextForge is governing."list_control_plane.
  • "Would a $50,000 wire be allowed? With dual approval?"evaluate_policy (deny + reason, then allow).
  • "Register the fx-rates service at http://fx-rates:8000/mcp."register_mcp_server (a new server joins the catalog live).
  • "Show me what got blocked today."recent_blocks.

The three watch panes

Arrange your screen so you can watch the control plane while you prompt Bob:

make monitor        # ContextForge Admin UI (/admin): catalog + Overview/Metrics/Logs
make inspect-mcp    # MCP Inspector → the 8 governed tools (erp-payments-wire is ABSENT)
make inspect-a2a    # A2A Inspector → validates the Python + Rust agent cards

Advanced: watch everything with make cockpit

Prefer one command over arranging tabs by hand? make cockpit spawns a single tmux window that tiles Bob (big left pane, ~62%) alongside four live watch panes — logs, logs-opa, inspect-mcp, inspect-a2a. It also opens a HOW-TO guide (docs/cockpit.html) in your browser and starts the Companion dashboard on :7070. Each pane just runs the existing make target, so it reuses all the token/UUID logic. Requires tmux (brew install tmux / apt-get install tmux); without it you get the manual fallback list above.

make cockpit                         # FinOps analyst persona (default)
COCKPIT_PERSONA=operator make cockpit  # Act 2: platform operator persona in the Bob pane
make cockpit-down                    # tear it all down (also removes the a2a-inspector container)

Already inside tmux? Run make cockpit from your Bob pane and it adds the four watch panes around it (no second Bob); make cockpit-down then removes only those panes.

Over SSH: the cockpit (tmux) runs fine on the remote host, but the browser UIs (HOW-TO page, Companion, Admin UI, MCP/A2A Inspectors) live there too — make cockpit prints the URLs plus a port-forward hint instead of opening a tab. Forward them from your laptop:

ssh -L 4444:localhost:4444 -L 7070:localhost:7070 -L 6274:localhost:6274 -L 6277:localhost:6277 -L 8090:localhost:8090 <host>

Running on a fresh Linux box / VM

From nothing to 16/16 on a clean Linux box (or a lightweight Multipass/Lima VM on Apple silicon — the practical path, since a macOS-guest VM needs ~60 GB+ disk):

# 1. Install the runtime + build deps (Docker Engine runs natively, no nested virtualization):
#    - Docker Engine          https://docs.docker.com/engine/install/
#    - Node.js ≥ 22.15        nvm install 22          (only needed to DRIVE Bob)
#    - uv                     https://docs.astral.sh/uv/
#    - git + make             your distro's package manager

# 2. Clone + bring it all up → 16 passed, 0 failed (no Bob required):
git clone https://github.com/manavgup/ai-agent-controlplane-demo.git
cd ai-agent-controlplane-demo
make quickstart

# 3. To DRIVE the demo, install IBM Bob Shell (cross-platform; checks Node ≥ 22.15):
curl -fsSL https://bob.ibm.com/download/bobshell.sh | bash
make bob            # first run prompts an IBMid device-code login on a headless box

Build-time DNS gotcha (fresh Docker Engine). If make quickstart fails during an image build with Could not resolve host: index.crates.io (cargo/pip inside a RUN step) even though image pulls succeed, the build container is using the host's dead systemd-resolved 127.0.0.53 stub. Fix it once and re-run (quickstart is idempotent):

echo '{"dns":["8.8.8.8","1.1.1.1"]}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
make quickstart

Prove it / watch the controls

The headless proof is one command:

make verify-controls       # → RESULT: 16 passed, 0 failed

It runs scripts/money-shots/run-all.sh, which asserts all four controls plus the cross-language agent-mesh block and the Rust agent successfully executing a within-policy payment — 16 assertions in total. This is the safety net: run it any time to confirm the stack is honest.

To watch the controls fire live as you prompt Bob, see docs/LOG-CHEATSHEET.md — the exact prompt → log-line map (including which beat is supposed to log nothing) — and tail:

make logs        # raw gateway logs (blocked calls surface as ERROR 'invocation failed')
make logs-opa    # live, readable OPA decisions: ALLOW/DENY + args + reason

Project layout

ai-agent-controlplane-demo/
├── a2a-agents/        # the 2 A2A agents: auditor/ (Python, a2a-sdk) + payments/ (Rust, a2a-lf)
├── bob-personas/      # mcp.json templates for the analyst + operator + builder personas (live-UUID rewrite)
├── companion/         # optional browser dashboard (Flask, :7070) to watch the control plane
├── gateway/           # ContextForge image + OPA image: custom/ plugin, plugins/ config, policies/ Rego, seed/
├── mcp-servers/       # the 6 MCP servers: expense-db, erp-payments, policy-docs, notify, controlplane, fx-rates
│                      #   + sales-tax/ — the Dev Day "Bob builds it from scratch" server (generated; Stage 1→2)
├── scripts/           # quickstart.sh, demo.sh, watch-decisions.sh, money-shots/ proof suite
├── slides/            # the conference talk deck (.pptx) + outline
└── docs/              # QUICKSTART/RUNBOOK/LOG-CHEATSHEET/SCENARIOS/SHOWCASE-BOB + diagrams/

How it works

  • Virtual servers. The gateway exposes curated, least-privilege slices of the full tool catalog: FinOps (8 tools, no wire — the analyst), Treasury (the wire path: wire, reimburse, a2a_payments), and Operator (4 control-plane tools). RBAC is enforced by which virtual server a persona points at, not by the token identity.
  • The stdio bridge. Bob connects through uvx --from mcp-contextforge-gateway python -m mcpgateway.wrapper, with MCP_SERVER_URL=http://localhost:4444/servers/<UUID>/sse and MCP_AUTH=Bearer <admin JWT>. Bob reads .bob/mcp.json from the repo root — which is why you always launch via make bob / make bob-operator.
  • Path-independence. The gateway config, FinByteGuard plugin, and Rego policy are baked into the images (gateway/Dockerfile, gateway/Dockerfile.opa). Zero host bind-mounts means the stack runs from any clone path — even ones Docker can't share, like /tmp on macOS.

Troubleshooting & reset

  • Attendee walkthrough: QUICKSTART.md — the 3-pane, follow-along guide.
  • Presenter run order + recovery: docs/RUNBOOK.md.
  • Anything drifts / 16/16 fails: make demo-reset (recreate + reseed the gateway), then make verify-controls.
  • Footgun — "No MCP servers configured": you launched bob from the wrong directory (often the bob-personas/ subfolder). Bob looks for .bob/mcp.json relative to its cwd. Quit it and run make bob, which launches from the repo root and refreshes the live UUID after a reseed.
  • Fresh Linux VM — Could not resolve host … during a make quickstart image build (e.g. index.crates.io, while image pulls succeed): the Docker build container is using the host's dead systemd-resolved 127.0.0.53 stub. Fix → write /etc/docker/daemon.json with {"dns":["8.8.8.8","1.1.1.1"]}, sudo systemctl restart docker, then re-run make quickstart (idempotent). See Running on a fresh Linux box / VM.

Security / disclaimer

This is a demo, not production. The JWTs are signed with a public demo secret (demo-only-change-me-…), CSRF is disabled for the local HTTP demo, and SSRF guards are loosened for the Compose private network. Do not reuse any of this configuration outside the demo. For production, mint your own secrets, enable HTTPS + CSRF, and tighten SSRF — see the upstream IBM ContextForge project.


License

Released under the MIT License — see LICENSE. The demo-only caveats above still apply: the bundled secrets and passwords are intentionally public and must not be reused in production.

About

IBM Bob × ContextForge — a turnkey demo of the AI agent control plane: a governed MCP/A2A fintech mesh (OPA policy, PII/secret redaction, prompt-injection defense, RBAC least-privilege), provable with one command.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages