Skip to content

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.

Terminal window
git clone https://github.com/obsign/obsign
cd obsign
cargo build --workspace
cargo run -p obsign-policy --example mkbundle -- /tmp/demo
cargo 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-server

What 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 rule

Three things worth noticing:

  • The MCP server never saw the destructive call. Refusal happens at the gateway, before forwarding.
  • exfiltrate_secrets is refused too. The server advertises it, but the signed catalogue does not describe it, and the catalogue is authoritative.
  • send_message executed exactly once. Same tool, two verdicts. The difference is an argument, and the rule reads context.args.channel:
@id("support_channel_only")
forbid (principal, action == Action::"tool_call", resource == Tool::"send_message")
when { context.args.channel != "#support" };

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:

Terminal window
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.)

Terminal window
./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 — tampered

The exit codes and what they mean: Verifying evidence.

Terminal window
docker compose up -d gateway console
docker compose run --rm demo # the demo above, sealed and verified: exit 0