From an OpenAPI specification to a governed agent surface
Every screen and command below comes from the shipped binary running against a ticketing API. Same binary in every edition, same output in your environment.
1Define the policy, compile the surface
The policy is the contract your security function reviews: which operations an agent may call, which fields come back, and which writes need a human. Nothing outside it is emitted.
allow beats nothing, deny beats allow, writes must be named
allow:
- GET /tickets
- GET /tickets/{id}
- POST /tickets/{id}/comment
deny:
- DELETE
- /admin/**
- /internal/**
fields:
Ticket:
include: [id, status, priority, summary, blocker_flag, assignee.name]
exclude: [customer_email, ssn, raw_description]
params:
deny: [include_deleted, internal]
limits:
response_bytes: 8000
max_tools: 12
tools:
POST /tickets/{id}/comment:
name: add_comment
approve: true
conditions:
- arg: id
from_claim: ticket_scope$ absorb diff --spec tickets.yaml --policy agent.yaml 15 operations to 3 tools, 12 withheld contract 7.1 KB to 1.1 KB (-83.8%) Exposed (3) + search_tickets GET /tickets + get_ticket GET /tickets/{id} + add_comment POST /tickets/{id}/comment Withheld (12) - DELETE /tickets/{id} denied by DELETE - GET /admin/audit denied by /admin/** ... 10 more
2Give security a document, not a specification
The same compilation produces a self-contained report: every permitted tool with its arguments, returned fields and governing rule, then every withheld operation grouped by reason. It opens offline and prints fully expanded.

$ absorb lint --spec tickets.yaml --policy agent.yaml
warn get_ticket_discount: the spec has no summary or description, so the agent
only sees a name; add a description under tools in the policy
info search_tickets: required arguments with no description, enum or example: q
3 tools, 1 warnings, 1 info$ absorb audit --file audit.jsonl TIME AGENT EVENT TOOL DECISION STATUS BYTES STRIPPED DRIFT MS 2026-09-22 20:55:46 claude-desktop session_start - - - 0 0 0 0 2026-09-22 20:55:46 claude-desktop tool_call get_ticket strip 200 162 9 0 4 2026-09-22 20:55:47 claude-desktop tool_call search_tickets strip 200 529 28 0 12 2026-09-22 20:56:02 renewals-bot tool_call add_comment held - 0 0 0 0 2026-09-22 20:56:02 renewals-bot approval add_comment requested - 0 0 0 0 2026-09-22 20:58:11 renewals-bot approval add_comment approved - 0 0 0 0 2026-09-22 20:58:11 renewals-bot tool_call add_comment allow 201 86 0 0 41 2026-09-22 21:02:44 reporting-bot tool_call delete_ticket deny - 0 0 0 0
3Publish to the agents your teams already use
One configuration file defines the upstream, its credentials, the agents allowed to connect and where audit records go. Secrets are referenced from the environment or a mounted file, never held in the policy.
upstream:
url: https://tickets.internal.example.com/api
credentials:
- type: bearer
token: ${env:TICKETS_API_TOKEN}
agents:
- id: renewals-bot
token_sha256: 5c3a1f0b...
tools: ["get_*", "search_*", "add_comment"]
admins:
- id: alice
token_sha256: 9f1c22de...
approvals:
slack:
webhook_url: ${env:SLACK_WEBHOOK_URL}
signing_secret: ${env:SLACK_SIGNING_SECRET}
audit:
path: /var/log/absorb/audit.jsonl
export:
url: https://collector.internal.example.com/absorb
secret: ${env:AUDIT_EXPORT_SECRET}Cursor, Windsurf, VS Code and any MCP client that takes a URL { "mcpServers": { "tickets": { "url": "https://absorb.internal:8080/mcp" } } } Claude Desktop, running the binary locally { "mcpServers": { "tickets": { "command": "/usr/local/bin/absorb", "args": ["serve", "--stdio", "--config", "/etc/absorb/absorb.yaml", "--spec", "/etc/absorb/tickets.yaml", "--policy", "/etc/absorb/agent.yaml"] } } }
4Route sensitive operations to a human decision
Mark an operation with approve: true, or set a threshold such as approve_above: 1000 on an amount. The call is held before it reaches your API. On approval it runs once and the result returns to the agent.
- Agent callsadd_comment
- Heldbefore your API
- RoutedSlack, ServiceNow, webhook or admin queue
- Human decidesapprover, channel and arguments recorded
- Runs onceresult returned to the agent

agent calls a tool the policy holds for approval { "status": "pending_approval", "approval_id": "apr_58c74db86fff9c8830", "expires": "2026-09-22T21:08:30Z", "message": "A human must approve this call before it runs. Tell the user it is waiting for approval, then call absorb_check_approval with this approval_id." } after a human approves, the same agent asks again { "id": "c_42", "body": "noted", "created_at": "2026-09-22T21:00:12Z" } [absorb] approved by alice via admin
5Detect specification drift before your agents do
The spec is not the runtime. absorb probe calls every exposed read tool once against the real API and compares the response with the spec. It exits non-zero, so it can gate a deployment in your pipeline.
- Wrong typesinteger in the spec, string in production
- Missing fieldsdeclared but never returned
- Error bodies as 200failures disguised as success
- Emptied responsesthe policy would strip everything
$ absorb probe --spec tickets.yaml --policy agent.yaml --config absorb.yaml absorb probe https://tickets.internal.example.com/api OK search_tickets 200 1.5 KB 61ms undeclared fields (dropped by projection): debug OK get_ticket 200 489 B 44ms DRIFT list_comments 200 2.1 KB 58ms ~ [].author.id: expected integer, got string (x12) ERROR BODY get_billing 200 144 B 39ms HTTP 200 but the body matches ResponseNOTOK: message="NOTOK" SKIPPED add_comment writes are not probed 2 ok, 1 drift, 0 empty, 1 error bodies, 0 failed, 1 skipped
In production the same mismatches are recorded in the audit log and in the absorb_drift_values_total metric, so they surface on a dashboard rather than in a support ticket.
Begin with one specification
A 45 minute session: we compile your API, walk your security team through the review document, and hand over the policy and deployment manifest.