Quickstart
The critical path runs end to end from a fresh clone: an agent speaks MCP to the gateway, the policy decides, the log is sealed, and the auditor verifies offline.
Build and run the demo
Section titled “Build and run the demo”git clone https://github.com/obsign/obsigncd obsigncargo build --workspace
cargo run -p obsign-policy --example mkbundle -- /tmp/democargo run -p obsign-proxy --example mint_demo_token -- /tmp/demo 1800 user
printf '%s\n' \ '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \ '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"delete_production_db","arguments":{"database":"customers"}}}' \ '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"ticket_update","arguments":{"ticket":"T-8821"}}}' \ '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"send_message","arguments":{"channel":"#support","text":"T-8821 updated"}}}' \ '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"send_message","arguments":{"channel":"#all-hands","text":"T-8821 updated"}}}' \ | ./target/debug/obsign-proxy \ --policy /tmp/demo/policy-bundle.json \ --trusted-keys /tmp/demo/trusted-keys.json \ --identity-bundle /tmp/demo/identity-bundle.json \ --token-file /tmp/demo/token.jwt \ --wal /tmp/demo/wal --chain-id demo --env prod \ -- ./target/debug/mock-mcp-serverWhat happens:
[obsign] identity PROVEN — u:marie.dupont via https://sso.acme.fr/realms/corp — expires in 1756 s[obsign] REFUSED delete_production_db: forbidden by an explicit rule[obsign] tools/list: 2 hidden — delete_production_db, exfiltrate_secrets[server] EXECUTING ticket_update[server] EXECUTING send_message[obsign] REFUSED send_message: forbidden by an explicit ruleThree things worth noticing:
- The MCP server never saw the destructive call. Refusal happens at the gateway, before forwarding.
exfiltrate_secretsis refused too. The server advertises it, but the signed catalogue does not describe it, and the catalogue is authoritative.send_messageexecuted exactly once. Same tool, two verdicts. The difference is an argument, and the rule readscontext.args.channel:
@id("support_channel_only")forbid (principal, action == Action::"tool_call", resource == Tool::"send_message")when { context.args.channel != "#support" };Seal the log
Section titled “Seal the log”The WAL under /tmp/demo/wal is the gateway’s only output; it holds no
signing key. Sealing is the ledger’s job, with a key the gateway never sees:
openssl rand -hex 32 > /tmp/demo/seal-seed.hex
./target/debug/obsign-ledger seal \ --wal /tmp/demo/wal --chain-id demo \ --store /tmp/demo/ledger \ --key /tmp/demo/seal-seed.hex --key-id seal-prod
./target/debug/obsign-ledger export \ --wal /tmp/demo/wal --chain-id demo \ --store /tmp/demo/ledger --out /tmp/demo/evidence.json(The hex key file is development-grade by construction; production sealing goes through an HSM. See Sealing & the ledger.)
Verify, then tamper
Section titled “Verify, then tamper”./target/debug/obsign verify /tmp/demo/evidence.json \ --trusted-keys /tmp/demo/ledger/keys.json# exit 0 — proven
sed -i '' 's/"outcome": "deny"/"outcome": "allow"/' /tmp/demo/evidence.json./target/debug/obsign verify /tmp/demo/evidence.json \ --trusted-keys /tmp/demo/ledger/keys.json# exit 1 — tamperedThe exit codes and what they mean: Verifying evidence.
Or run it in Docker
Section titled “Or run it in Docker”docker compose up -d gateway consoledocker compose run --rm demo # the demo above, sealed and verified: exit 0Next steps
Section titled “Next steps”- The HTTP gateway — one shared network service instead of one process per agent.
- Identity & delegation — swap the demo token for your real IdP.
- Deploying with Docker — the production images and their non-negotiables.