Install
ABOM is an open-source CLI on PyPI. Requires Python 3.10+.
pip install -U abom-cli ... Successfully installed abom-cli-0.2.0
Confirm it's working — every command opens with the ABOM banner:
abom version ╭───────╮ ▄▀█ █▄▄ █▀█ █▀▄▀█ │ ✔ │ █▀█ █▄█ █▄█ █░▀░█ ╰──╮ ╭──╯ Agent Bill of Materials v0.2.0 ╰─╯ a signed record of what your AI agent is, what it can do, and what it did abom 0.2.0 · ABOM spec v0.1 (manifest) + v0.2 (gateway, draft)
In a hurry? Scan an agent and run the /authorize API in two commands — your agent asks before it acts and gets back a verdict plus a signed receipt:
abom scan . -o abom.json # signed manifest abom serve abom.json # POST /authorize on :8088 curl localhost:8088/authorize -d '{"tool":"wire_transfer"}' { "verdict": "deny", "rule": "tool_not_in_manifest", "receipt": { "inclusion_proof": {…}, "head_signature": {…} } } # run it metered (Bearer keys + per-key /usage): abom keys issue --label acme abom serve abom.json --keys .abom/keys.json
Scan a local agent what it's made of
Point abom scan at any agent project. It detects the models, tools, prompts, data sources, frameworks and MCP servers it's built from — each with the exact DETECTED FROM reference — and emits a signed manifest.
Here's a minimal example agent — a few dependencies, some source, and a system prompt:
tree abom-demo
abom-demo
├── agent.py
├── prompts
│ └── system.txt
└── requirements.txt
2 directories, 3 fileslangchain==0.2.16 langgraph>=0.2.0 openai==1.51.0 anthropic chromadb~=0.4.22 guardrails-ai
"""A tiny customer-support agent.""" from langchain.chat_models import init_chat_model SMART_MODEL = "gpt-4o" FAST_MODEL = "claude-3-5-sonnet-20241022" def lookup_order(order_id: str) -> dict: """Look up an order by id.""" return {"id": order_id, "status": "shipped"} def issue_refund(order_id: str, amount: float) -> dict: """Refund an order.""" return {"id": order_id, "refunded": amount}
You are a helpful customer-support agent. Only answer order questions.
Scan it:
abom scan . ╭───────╮ ▄▀█ █▄▄ █▀█ █▀▄▀█ │ ✔ │ █▀█ █▄█ █▄█ █░▀░█ ╰──╮ ╭──╯ Agent Bill of Materials v0.2.0 ╰─╯ a signed record of what your AI agent is, what it can do, and what it did ABOM · abom-demo @ 0.0.0 — 9 components ┌───┬─────────────┬────────────────────────────┬──────────┬──────────────────────────┐ │ # │ TYPE │ NAME │ VERSION │ DETECTED FROM │ ├───┼─────────────┼────────────────────────────┼──────────┼──────────────────────────┤ │ 1 │ model │ Anthropic (SDK) │ — │ dependency:anthropic │ │ 2 │ model │ OpenAI (SDK) │ 1.51.0 │ dependency:openai │ │ 3 │ model │ claude-3-5-sonnet-20241022 │ — │ file:agent.py:5 │ │ 4 │ model │ gpt-4o │ — │ file:agent.py:4 │ │ 5 │ framework │ LangChain │ 0.2.16 │ dependency:langchain │ │ 6 │ framework │ LangGraph │ >=0.2.0 │ dependency:langgraph │ │ 7 │ data source │ Chroma │ ~=0.4.22 │ dependency:chromadb │ │ 8 │ policy │ Guardrails AI │ — │ dependency:guardrails-ai │ │ 9 │ prompt │ prompts/system.txt │ — │ file:prompts/system.txt │ └───┴─────────────┴────────────────────────────┴──────────┴──────────────────────────┘ signed: ed25519 · key 7f7aa8d6efabb18f composition_sha256: c780ddca2eb619e0… → wrote abom.json
Verify the signature any time — it's self-contained, so anyone you hand the file to can check it:
abom verify abom.json ╭───────╮ ▄▀█ █▄▄ █▀█ █▀▄▀█ │ ✔ │ █▀█ █▄█ █▄█ █░▀░█ ╰──╮ ╭──╯ Agent Bill of Materials v0.2.0 ╰─╯ a signed record of what your AI agent is, what it can do, and what it did ✓ VALID — signature OK, 9 components
Block an unauthorized action what it's allowed to do
The gate enforces the signed manifest at the tool boundary, deny-by-default. A tool that isn't declared is blocked before it runs — not logged after.
Start from a manifest that declares which tools the agent may use:
{
"abom": "0.1",
"type": "CompositionManifest",
"agent": { "name": "support-agent", "version": "1.0.0" },
"components": [
{ "type": "tool", "name": "lookup_order" },
{ "type": "tool", "name": "http_fetch", "scope": "egress",
"allowed_endpoints": ["api.internal.shop"] }
]
}A declared tool is allowed (exit 0):
abom gate gate.json --tool lookup_order ╭───────╮ ▄▀█ █▄▄ █▀█ █▀▄▀█ │ ✔ │ █▀█ █▄█ █▄█ █░▀░█ ╰──╮ ╭──╯ Agent Bill of Materials v0.2.0 ╰─╯ a signed record of what your AI agent is, what it can do, and what it did ALLOW lookup_order action is within the signed envelope notarized · log root d4806211478cc5fcb081fe9c… · signed key 7f7aa8d6efabb18f
An undeclared tool — say a prompt injection trying to call wire_transfer — is denied (exit 1, so it fails a CI build):
abom gate gate.json --tool wire_transfer ╭───────╮ ▄▀█ █▄▄ █▀█ █▀▄▀█ │ ✔ │ █▀█ █▄█ █▄█ █░▀░█ ╰──╮ ╭──╯ Agent Bill of Materials v0.2.0 ╰─╯ a signed record of what your AI agent is, what it can do, and what it did DENY wire_transfer (tool_not_in_manifest) tool 'wire_transfer' is not in the signed Composition Manifest (declared: ['http_fetch', 'lookup_order']) notarized · log root d749e5cc0ad8cff29b9917ae… · signed key 7f7aa8d6efabb18f
Or wrap a tool in your agent code — a prompt injection that calls an undeclared tool raises ActionDenied before the body executes:
from abom import Gate, ActionDenied gate = Gate(signed_manifest) @gate.gated() def wire_transfer(amount, to): ... # never runs if undeclared try: wire_transfer(1_000_000, to="attacker") except ActionDenied as e: print(e.decision.rule) # → "tool_not_in_manifest" # the money never moved — and the denial is notarized
Or move the gate out of the agent's process entirely with the MCP broker. The agent connects to the broker instead of its MCP server; every tools/call is checked against the signed manifest before it's forwarded, so a call the agent was never granted never reaches the server:
abom broker abom.json --upstream https://mcp.internal/rpc \ --trusted-key 9a72f7ef… --log gate.mlog ABOM broker → https://mcp.internal/rpc mediating tools/call against the signed manifest (deny-by-default) tools/call lookup_customer # → forwarded tools/call wire_transfer # → DENIED (-32001) # upstream never saw it
Authorize against the /authorize API zero agent code
The same broker can authorize every tool call against a hosted /authorize endpoint instead of deciding locally — so a brokered MCP agent gets a signed receipt per call and the endpoint meters usage. The agent changes nothing: it just points its MCP client at the broker.
First, run the API yourself (self-hosted, free). Issue a key to run it metered:
abom serve abom.json # POST /authorize on :8088 abom keys issue --label acme # a Bearer key (shown once) abom serve abom.json --keys .abom/keys.json # metered curl localhost:8088/authorize -d '{"tool":"wire_transfer"}' { "verdict": "deny", "rule": "tool_not_in_manifest", "receipt": { "inclusion_proof": {…}, "head_signature": {…} } }
Then point the broker at that endpoint. Now every brokered tools/call is authorized by the API and comes back with a signed receipt — the agent never touches the API:
abom broker abom.json --upstream https://mcp.internal/rpc \ --authorize-url https://api.abom.ai/authorize \ --api-key abom_… tools/call lookup_customer # → authorized, forwarded tools/call wire_transfer # → DENIED # signed receipt, upstream never saw it
Run the MCP gateway signed evidence per tool call · v0.2
The gateway sits between your agent and its MCP server and turns every tools/call into a signed, hash-chained Action Provenance Record — written and fsync'd to disk before the call is forwarded, so a forwarded-but-unrecorded call cannot exist. The default is inspect-only: policy violations are recorded as would_deny and forwarded — the evidence is the product, blocking is one flag away.
Point it at your MCP server with a project policy and an org-wide floor no template can weaken:
abom gateway run gateway-baseline.json --floor org-floor.json \ --upstream https://mcp.internal/rpc --jurisdiction EU ABOM gateway → https://mcp.internal/rpc (policy 'gateway-baseline' · 26618a1394…) inspect mode (default) — violations recorded as would_deny and forwarded; pass --enforce to block signed provenance per call → gateway.apr.jsonl # … the agent works normally; the log fills with evidence … session ended · 4 forwarded · 0 denied · 1 would-deny · 0 degraded · 5 record(s)
Every record is ed25519-signed and hash-chained; the file opens with a signed epoch (its creation is itself attested), and arguments enter the log as salted-HMAC commitments plus a de-identified shape — never content. Verify the whole chain, or a set of rotated files, in one command:
abom gateway verify gateway.apr.jsonl ✓ VALID — 5 record(s), chain intact, every record signed # tamper with any record — the chain breaks at the exact seq: abom gateway verify gateway.apr.jsonl ✗ INVALID at seq 3 — chain: hash mismatch
Because arguments are commitments, you can later prove which call was made without payloads ever having entered the log — the per-file salt stays in your boundary, its hash sealed in the epoch:
from abom.gateway import verify_argument_claim verify_argument_claim("gateway.apr.jsonl", seq=3, salt_hex=salt, args={"amount": 1_000_000, "to": "attacker-iban"}) {"valid": True} # it WAS the $1,000,000 call… verify_argument_claim(…, args={"amount": 20, "to": "attacker-iban"}) {"valid": False} # …and provably not the $20 one
Already running Google Model Armor? Keep it — and seal its verdicts into the same signed records. And when the evidence has earned trust, turn on blocking:
# they decide, you prove: sanitize-API verdicts land in detectors[] abom gateway run gateway-baseline.json -u https://mcp.internal/rpc \ --model-armor-template projects/acme/locations/eu/templates/strict # opt into blocking — would_deny becomes a denial the upstream never sees: abom gateway run gateway-baseline.json --floor org-floor.json \ -u https://mcp.internal/rpc --enforce tools/call wire_transfer # → DENIED (-32001) # floor policy: *_transfer
Prove the record can't be rewritten verify without trusting the operator
Every gate decision lands in an append-only Merkle transparency log — the same RFC 6962 construction behind Certificate Transparency and Sigstore. Two independent steps make that record something a third party can rely on, even against the operator who runs it.
First, run the gate or broker with --log so decisions land in a durable transparency log:
abom gate abom.json --tool lookup_customer --log gate.mlog abom broker abom.json --upstream https://mcp.internal/rpc --log gate.mlog
A witness cosigns the log with its own key — separate from the operator's. It refuses to cosign unless the log only grew since it last saw it, so a rewrite or truncation is rejected:
abom witness gate.mlog ✓ cosigned size=2 root 1ffb0c515b588b7d… witness key 9987a9c4c7d5f2c8 · → gate.mlog.cosig.json # operator rewrites the log and tries again: abom witness gate.mlog ✗ WITNESS REFUSED — a log may only grow (append-only)
Anchor the root to an RFC 3161 timestamp authority — an independent third party the operator doesn't control. Under eIDAS Art. 41 a qualified timestamp carries a legal presumption of time and integrity, proving the root existed, unaltered, by time T:
abom anchor gate.mlog --tsa https://freetsa.org/tsr ✓ anchored root 1ffb0c515b588b7d… RFC 3161 token · → gate.mlog.anchor.json # re-verifiable offline, later # the auditor's check — no trust in the operator: abom witness-verify gate.mlog.cosig.json ✓ VALID — witness attests size=2, root 1ffb0c51…
Scan a remote agent three ways
You don't need the code locally. ABOM scans a GitHub repo by URL, or a live deployed agent over the Model Context Protocol.
Local code
A directory or repo on your machine.
A GitHub repo
Clones, scans, and cleans up. Never runs the code.
A live endpoint
Enumerates a running agent's declared capabilities.
Scan a public GitHub repo directly — here, the real GPT Researcher agent. ABOM clones it, scans it, and shows the exact file:line each model was found at:
abom scan --url https://github.com/assafelovic/gpt-researcher ABOM · gpt-researcher @ 0.14.7 — 25 components ┌────┬────────────┬────────────────────────┬──────────┬─────────────────────────────────────────────────────────────────┐ │ # │ TYPE │ NAME │ VERSION │ DETECTED FROM │ ├────┼────────────┼────────────────────────┼──────────┼─────────────────────────────────────────────────────────────────┤ │ 1 │ model │ ChatGPT │ — │ file:evals/hallucination_eval/results/aggregate_results.json:17 │ │ 3 │ model │ Hugging Face (SDK) │ >=0.32.0 │ dependency:huggingface-hub │ │ 6 │ model │ Ollama (SDK) │ >=0.4.8 │ dependency:ollama │ │ 7 │ model │ OpenAI (SDK) │ >=1.3.3 │ dependency:openai │ │ 13 │ model │ gpt-4o │ — │ file:multi_agents_ag2/task.json:12 │ │ 14 │ model │ gpt-4o-mini │ — │ file:gpt_researcher/config/config.py:220 │ │ 19 │ framework │ Google GenAI │ >=1.0.0 │ dependency:google-genai │ │ 20 │ framework │ LangChain │ >=1.0.0 │ dependency:langchain │ │ 21 │ framework │ LangGraph │ >=0.2.76 │ dependency:langgraph │ │ 22 │ framework │ Model Context Protocol │ >=1.9.1 │ dependency:mcp │ │ 23 │ tool │ custom_tool │ — │ file:gpt_researcher/utils/tools.py │ │ 24 │ tool │ search_tool │ — │ file:gpt_researcher/utils/tools.py │ │ 25 │ MCP server │ gpt-researcher │ — │ file:.mcp.json │ └────┴────────────┴────────────────────────┴──────────┴─────────────────────────────────────────────────────────────────┘ # 25 components total — trimmed to 13 rows here composition_sha256: 255bc40431675931… → wrote ./gptr.json
Or scan a live deployed agent over MCP — ABOM connects, runs the handshake, and reads the capabilities the server itself declares (it never invokes a tool):
abom scan --mcp https://mcp.deepwiki.com/mcp
ABOM · DeepWiki @ 2.14.3 — 4 components
┌───┬────────────┬─────────────────────┬─────────┬──────────────────────────────────┐
│ # │ TYPE │ NAME │ VERSION │ DETECTED FROM │
├───┼────────────┼─────────────────────┼─────────┼──────────────────────────────────┤
│ 1 │ tool │ ask_question │ — │ mcp:https://mcp.deepwiki.com/mcp │
│ 2 │ tool │ read_wiki_contents │ — │ mcp:https://mcp.deepwiki.com/mcp │
│ 3 │ tool │ read_wiki_structure │ — │ mcp:https://mcp.deepwiki.com/mcp │
│ 4 │ MCP server │ DeepWiki │ 2.14.3 │ mcp:https://mcp.deepwiki.com/mcp │
└───┴────────────┴─────────────────────┴─────────┴──────────────────────────────────┘
signed: ed25519 · key 7f7aa8d6efabb18f
composition_sha256: bb85b78a15d2eb91…Auth-gated servers work too. Point it at GitHub's production MCP server with a token (--token <PAT>, or the ABOM_MCP_TOKEN env var) and you get a signed inventory of its entire capability surface — including the consequential ones a security team cares about, like delete_file and merge_pull_request:
export ABOM_MCP_TOKEN=github_pat_… abom scan --mcp https://api.githubcopilot.com/mcp/ ABOM · github-mcp-server @ github-mcp-server/remote-3249da0b62a1a670d81b660ad408f1d4810eacdc — 51 components ┌────┬─────────────┬───────────────────────────────────┬───────────────────────────────────────────────────────────────────┬────────────────────────────────────────┐ │ # │ TYPE │ NAME │ VERSION │ DETECTED FROM │ ├────┼─────────────┼───────────────────────────────────┼───────────────────────────────────────────────────────────────────┼────────────────────────────────────────┤ │ 1 │ tool │ add_comment_to_pending_review │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 2 │ tool │ add_issue_comment │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 3 │ tool │ add_reply_to_pull_request_comment │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 4 │ tool │ create_branch │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 5 │ tool │ create_or_update_file │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 6 │ tool │ create_pull_request │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 7 │ tool │ create_repository │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 8 │ tool │ delete_file │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 9 │ tool │ fork_repository │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 10 │ tool │ get_commit │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 11 │ tool │ get_file_contents │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 12 │ tool │ get_label │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 13 │ tool │ get_latest_release │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 14 │ tool │ get_me │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 15 │ tool │ get_release_by_tag │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 16 │ tool │ get_tag │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 17 │ tool │ get_team_members │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 18 │ tool │ get_teams │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 19 │ tool │ issue_read │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 20 │ tool │ issue_write │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 21 │ tool │ list_branches │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 22 │ tool │ list_commits │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 23 │ tool │ list_issue_fields │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 24 │ tool │ list_issue_types │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 25 │ tool │ list_issues │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 26 │ tool │ list_pull_requests │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 27 │ tool │ list_releases │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 28 │ tool │ list_repository_collaborators │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 29 │ tool │ list_tags │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 30 │ tool │ merge_pull_request │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 31 │ tool │ pull_request_read │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 32 │ tool │ pull_request_review_write │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 33 │ tool │ push_files │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 34 │ tool │ request_copilot_review │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 35 │ tool │ run_secret_scanning │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 36 │ tool │ search_code │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 37 │ tool │ search_commits │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 38 │ tool │ search_issues │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 39 │ tool │ search_pull_requests │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 40 │ tool │ search_repositories │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 41 │ tool │ search_users │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 42 │ tool │ sub_issue_write │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 43 │ tool │ update_pull_request │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 44 │ tool │ update_pull_request_branch │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 45 │ MCP server │ github-mcp-server │ github-mcp-server/remote-3249da0b62a1a670d81b660ad408f1d4810eacdc │ mcp:https://api.githubcopilot.com/mcp/ │ │ 46 │ data source │ get_me_ui │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 47 │ data source │ issue_write_ui │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 48 │ data source │ pr_edit_ui │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 49 │ data source │ pr_write_ui │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 50 │ prompt │ AssignCodingAgent │ — │ mcp:https://api.githubcopilot.com/mcp/ │ │ 51 │ prompt │ issue_to_fix_workflow │ — │ mcp:https://api.githubcopilot.com/mcp/ │ └────┴─────────────┴───────────────────────────────────┴───────────────────────────────────────────────────────────────────┴────────────────────────────────────────┘ signed: ed25519 · key 7f7aa8d6efabb18f composition_sha256: 770a91fc2bc1cd11… → wrote github-mcp.json
Make it shareable
Add --card to any scan or gate to also write a self-contained SVG "report card" you can drop into a PR, a doc, or a slide.
abom scan . --card abom-card.svg abom gate abom.json --tool wire_transfer --card decision.svg
And it drops into CI — abom gate exits non-zero on DENY, so an unauthorized action fails the build:
- uses: josephassiga/abom-dev/.github/actions/abom-scan@main with: path: .
Troubleshooting
A couple of real-world Python gotchas, and how to fix them.
| Symptom | Fix |
|---|---|
| The abom command says No module named httpx even though you installed it | Your abom binary (often a pipx install) uses a different environment than your pip. Either pipx inject abom-cli httpx, or run it via the right interpreter: python -m abom scan --mcp … |
| import abom reports an old version | A stale copy is shadowing the install. Reinstall cleanly: pip install -U abom-cli (or pip install -e . from a source checkout for the latest). |
| --no-banner "no such option" | It's a top-level flag — put it before the command: abom --no-banner scan … |
| Piping scan -o - into jq | Works cleanly — the banner and summary go to stderr, so stdout is pure JSON. |
Next steps
How it works
The three questions, the gate, and the Merkle Notary explained.
Build status
What's real vs. roadmap — honest maturity.
Source & spec
The spec · Architecture · PyPI (the sdist ships spec + ADRs)