{
  "markdown": "# Clay Seal\n\n<!-- mcp-name: io.github.clayseal/clayseal -->\n\n<img src=\"https://raw.githubusercontent.com/clayseal/clayseal-capabilities/main/docs/assets/clay-seal-logo.png\" alt=\"Clay Seal logo\" width=\"420\">\n\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n[![Python](https://img.shields.io/badge/python-3.10%20%E2%80%93%203.14-blue.svg)](pyproject.toml)\n[![Tests](https://img.shields.io/badge/tests-3300%2B%20passing-brightgreen.svg)](.github/workflows/ci.yml)\n[![PyPI](https://img.shields.io/badge/pip-clayseal-orange.svg)](https://pypi.org/project/clayseal/)\n\n**Incorporating business-process-logic constraints into AI systems.**\n\nYour agent has a $1,000 refund ceiling. It issues eleven refunds of $900. Every\ncall is inside the per-refund limit, so every per-call check passes, and $9,900\ngoes out the door. Nothing that looks at one call at a time can see this.\n\nClay Seal sits in front of your tools and judges each call against the whole\nsession: the grant, the running totals, where the arguments came from, and what\nthe agent has already done.\n\n## Contents\n\n| | |\n| --- | --- |\n| [Start here](#start-here) | install and watch it stop two attacks |\n| [Use it in two lines](#use-it-in-two-lines) | wrap tools you already have |\n| [In front of an MCP server](#or-put-it-in-front-of-an-mcp-server) | no code change |\n| [Write the policy](#write-the-policy) | the grant an operator seals |\n| [How it works](#how-it-works) | the six layers, in the order they run |\n| [What it measures](#what-it-measures) | the numbers, and what they are not |\n| [Where the boundary is](#where-the-boundary-is) | what this does not do |\n| [Documentation](#documentation) | [docs/START.md](https://github.com/clayseal/clayseal-capabilities/blob/main/docs/START.md) first, then [docs/](https://github.com/clayseal/clayseal-capabilities/blob/main/docs/README.md) |\n| [Build from source](#build-from-source) | and run the tests |\n\n**Working on the code?** [docs/INTEGRATION.md](https://github.com/clayseal/clayseal-capabilities/blob/main/docs/INTEGRATION.md) names which\nmodules the shipped gateway actually executes, which is the fastest way to tell\nthe live path from the parts that are not wired in. The decision itself is\n`SessionBroker._authorize_locked` in\n[clayseal/capabilities/broker.py](https://github.com/clayseal/clayseal-capabilities/blob/main/clayseal/capabilities/broker.py); most refusals\ncome from `_floor` in the same file.\n\n## Start here\n\nIf you are a coding agent, [skip to the deploy runbook](#if-you-are-an-agent).\n\n```bash\npip install clayseal\nclayseal try\n```\n\n`clayseal try` takes about a minute. It runs two attacks in front of you and\nshows the gateway stopping them. There is nothing to configure, no key to get\nand nothing leaves your machine.\n\n<img src=\"https://raw.githubusercontent.com/clayseal/clayseal-capabilities/main/docs/assets/clayseal-try.svg\" alt=\"clayseal try: eleven $900 refunds against a $1,000 ceiling, the first allowed and the rest refused, then an injected email recipient held for a person\" width=\"820\">\n\nEvery verdict there is decided live by the same gateway you would deploy. The\npicture is generated from a real run by `python scripts/render_try_svg.py`, and\na test fails if it drifts from what the command prints, so it cannot become a\nscreenshot of something that used to work.\n\nPython 3.10 to 3.14. Two dependencies, `cryptography` and `pyyaml`.\n\n> **On names.** The directory is `agentauth-capabilities`, the package is\n> `clayseal`, and the import is `clayseal.capabilities`. The project was renamed\n> at 0.6; `agentauth` still imports as a deprecation shim. See\n> [Naming](#naming) at the end for the detail.\n\n## If you are an agent\n\nDo this in the project you are editing, in order:\n\n```bash\nclayseal try --fast\nclayseal howto\nclayseal policy new > policy.yaml\n# rename your_* tools; put mailboxes in egress.recipients; then:\nclayseal policy lint policy.yaml\nclayseal skill --write\n```\n\nPython functions: `Guardrail.from_policy_file(\"policy.yaml\").wrap_all({...})`\nand bind the wrappers. Keys must match `tools.allow`. Construct one\n`Guardrail` per session (a new one resets ceilings). Catch `Refused`\n(budget / not granted) and `StepUpRequired` (held — typical for off-list\nemail). Non-file tools go in `paths.pathless`.\n\nAlready an MCP server (Claude Desktop / Cursor): `clayseal proxy`, not\n`clayseal serve`. Do not point `proxy` at a plain `.py` file. `clayseal howto`\nis the runbook.\n\n## Use it in two lines\n\nWrap the tools you already have. Nothing else about your agent changes.\n\n```python\nfrom clayseal.capabilities import Guardrail, Refused, StepUpRequired\n\ndef list_open_refunds():\n    return [{\"invoice\": \"INV-001\", \"amount\": 900.0},\n            {\"invoice\": \"INV-002\", \"amount\": 900.0}]\n\ndef issue_refund(invoice, amount):\n    return f\"refunded {invoice} ${amount:.2f}\"\n\nguard = Guardrail.from_dict({\n    \"version\": 1,\n    \"goal\": {\"id\": \"refund-run\",\n             \"summary\": \"Refund the invoices the customer disputed.\"},\n    \"expires_at\": \"2030-01-01T00:00:00Z\",\n    \"tools\": {\"allow\": [\"list_open_refunds\", \"issue_refund\"],\n              \"harmless\": [\"list_open_refunds\"],\n              \"effects\": {\"list_open_refunds\": \"read\", \"issue_refund\": \"transfer\"}},\n    \"paths\": {\"pathless\": [\"list_open_refunds\", \"issue_refund\"]},\n    \"budgets\": {\"value\": {\"ceilings\": {\"refunds\": \"1000.00\"},\n                          \"tracked\": {\"issue_refund\": {\"arg\": \"amount\",\n                                                       \"budget\": \"refunds\"}}}},\n})\ntools = guard.wrap_all({\"list_open_refunds\": list_open_refunds,\n                        \"issue_refund\": issue_refund})\n\n# Call them exactly as before. The gateway decides before the tool runs.\nfor row in tools[\"list_open_refunds\"]():\n    try:\n        print(tools[\"issue_refund\"](invoice=row[\"invoice\"], amount=row[\"amount\"]))\n    except Refused as exc:\n        print(\"refused:\", exc.reasons)      # hand the reason back to the agent\n    except StepUpRequired as exc:\n        print(\"needs a human:\", exc.reasons)\n```\n\nThe first refund goes through. The second is refused, because the $1,000\nsession ceiling is already spent. Neither call reached your function.\n\nThe policy is inline here so you can paste the whole thing into a file and run\nit. In a real deployment it lives in its own YAML, where a security team can\nreview it and a pull request can gate it. `clayseal policy new > policy.yaml`\nwrites a commented one to start from, and `Guardrail.from_policy_file` loads it.\n\nThe wrappers keep the name, docstring and signature of your originals, so any\nframework that introspects them sees the tool it saw before. That covers\nLangGraph, the OpenAI Agents SDK, CrewAI and hand-written loops. Call the\nwrappers the same way you called the originals, positionally or by name.\n\n## Or put it in front of an MCP server\n\nNo code change at all. The gateway speaks MCP, so it sits between your agent and\nthe server:\n\n```bash\nclayseal proxy --policy policy.yaml -- npx @your-org/mcp-server\n```\n\nOr paste this into Claude Desktop's MCP config, Cursor's MCP settings, or\n`.cursor/mcp.json`. The agent talks to Clay Seal; Clay Seal talks to the server:\n\n```json\n{\n  \"mcpServers\": {\n    \"billing\": {\n      \"command\": \"clayseal\",\n      \"args\": [\"proxy\", \"--policy\", \"policy.yaml\",\n               \"--\", \"npx\", \"@your-org/mcp-server\"]\n    }\n  }\n}\n```\n\nTools the policy does not grant are removed from the catalogue, so the agent is\nnever told they exist.\n\n## Write the policy\n\nThis is a whole policy. It lints clean.\n\n```yaml\nversion: 1\n\ngoal:\n  id: refund-run\n  summary: Refund the invoices the customer disputed.\n\nexpires_at: 2027-12-31T00:00:00Z\n\ntools:\n  allow:    [list_open_refunds, issue_refund]\n  harmless: [list_open_refunds]                 # a read spends nothing\n  effects:  {list_open_refunds: read, issue_refund: transfer}\n\npaths:\n  pathless: [list_open_refunds, issue_refund]   # these act on invoices, not files\n\negress:\n  domains: [acme.example]\n  recipients: [ops@acme.example]               # a domain alone is every mailbox\n  bind_recipients: true\n\nbudgets:\n  value:\n    ceilings: {refunds: \"1000.00\"}              # dollars, for the whole session\n    tracked:\n      issue_refund: {arg: amount, budget: refunds}\n```\n\nRun `clayseal policy lint policy.yaml` before you ship. It catches the mistake\nthat matters most: a tool that can spend money but debits no budget. On the file\nabove it reports no errors and one warning (the ceiling is per session unless\nyou bind a principal), which names a real decision you have not made yet.\n\nFull reference: [docs/POLICY.md](https://github.com/clayseal/clayseal-capabilities/blob/main/docs/POLICY.md).\n\n## See it stop the attack\n\nFrom a checkout. [examples/](https://github.com/clayseal/clayseal-capabilities/blob/main/examples/) has seven, each runnable with no key and\nno network:\n\n```bash\npython examples/02_the_proxy.py\n```\n\n```\ntools advertised to the agent: list_open_refunds, issue_refund\n  (the server offers wire_funds; the policy does not grant it, so the agent is never told it exists)\n\n  ok   issue_refund       INV-001   paid INV-001 $900.00\n  DENY issue_refund       INV-002   refused by policy: value_budget_exceeded\n  ...\n  DENY issue_refund       INV-011   refused by policy: value_budget_exceeded\n  DENY wire_funds         ops-float refused by policy: tool 'wire_funds' is not in this session's policy\n\nthe server executed 2 call(s):\n  list_open_refunds {}\n  issue_refund {\"amount\": 900.0, \"invoice\": \"INV-001\"}\n\nclayseal proxy: 2 allowed, 11 denied, 0 held for approval; withheld from the catalog: wire_funds\n```\n\nRead the last block. A JSON-RPC error proves the agent was told\nno; the server's own ledger proves the refund did not happen, and those are\ndifferent claims. The same run on the command line, which is the deployment\nshape a deployment actually uses:\n\n```bash\nclayseal proxy --policy examples/refund.yaml -- python examples/refund_server.py\n```\n\n## How it works\n\nYou give the gateway a **policy file** and a **goal** for the session. It seals\nthe goal at the start, so nothing the agent reads later can widen what was\napproved. Every tool call then goes through one decision point before it runs.\n\n```mermaid\nflowchart LR\n    P[\"policy.yaml<br/>reviewed and diffed<br/>like any other file\"]\n    A[\"your agent\"]\n    G{{\"Clay Seal\"}}\n    T[\"your tools<br/>or MCP server\"]\n    H[\"a person\"]\n    X[\"never runs\"]\n    S[(\"session state:<br/>running totals,<br/>where arguments<br/>came from\")]\n\n    A -- \"tool call\" --> G\n    P -. \"sealed at session start\" .-> G\n    G -- \"allow\" --> T\n    G -- \"step up\" --> H\n    G -- \"deny\" --> X\n    H -- \"yes, once, for<br/>these arguments\" --> T\n    T -- \"result\" --> S\n    S -. \"the next call is judged<br/>against all of this\" .-> G\n```\n\nThat loop at the bottom is the whole idea. The gateway does not just check a\ncall, it checks a call against everything the session has already done.\n\nA call gets one of three answers:\n\n- **allow** and the call goes to the tool\n- **step up** and the call waits for a person to approve it\n- **deny** and the call never runs\n\nStep-up exists because a wrong refusal is expensive. An autonomous attacker is\nstopped just as hard by a call that waits for approval as by one that is\nrefused, and a legitimate agent is not stopped permanently. Denial is reserved\nfor cases with positive evidence of a problem.\n\nThe checks that produce those answers run in a fixed order, cheapest and\nstrictest first, so a call refused early never reaches the expensive layers.\n\n1. **Floor.** The flat rules: has the grant expired, is this tool allowed, is\n   this path in scope, is this destination on the egress list, is there budget\n   left. Fastest and most of the denials.\n2. **Declaration.** If the agent states a plan up front, the plan is checked\n   against the sealed goal before any of it runs.\n3. **Content.** Checks that a declared write matches the goal, and inspects the\n   payload of a write that has an effect. Produces a step-up, never a denial.\n4. **Session state.** Running totals, which values came from untrusted text,\n   and any rules written against them.\n5. **Provenance.** Where a destination came from. A payee named in the sealed\n   goal is trusted; one that appeared in text the agent read afterwards is not.\n6. **Behavioural.** Watches the shape of the session against the goal. Advisory\n   by default: it escalates, it does not block. Its blocking tiers are also\n   arithmetically incapable of firing until enough benign sessions have been\n   observed to calibrate them, because a conformal p-value cannot go below\n   1/(n+1) and the alpha they are gated at sits under that floor. The detector\n   names the inert tiers, so one that cannot fire does not look like one that\n   looked and found nothing.\n\nThe word **budget** below means a running total the gateway keeps for the whole\nsession: money, calls, or anything else countable. It is the only check that can\nsee a sequence of individually legal calls adding up to something illegal, and\nthe measurements below show it is what decides whether this helps you.\n\n## What it measures\n\nThe scenarios and the sweep live in the repository rather than the wheel, so\nthis needs a clone. It takes about ten seconds and reaches no network:\n\n```bash\ngit clone https://github.com/clayseal/clayseal-capabilities.git\ncd clayseal-capabilities\npip install -e .\npython -m benchmarks.bpl_sweep --suite full\n```\n\n132 business-process scenarios, each with a scripted attack and its benign twin.\nTwo columns, because either is trivially winnable alone: refuse everything and\nyou win containment, allow everything and you win completion. The column that\nmatters is the conjunction.\n\n**The attack was contained AND its benign twin completed, full suite, n=132:**\n\n| condition | contained and completed |\n| --- | ---: |\n| undefended | 0/132, 97.5% upper bound 2.8% |\n| refuse everything | 0/132, 97.5% upper bound 2.8% |\n| per-call authorization, given the policy | 0.8% (1/132) |\n| dataflow taint | 11.4% (15/132) |\n| **Clay Seal** | **39.4% (52/132)** |\n\nAgainst dataflow taint that is **28.0 points [18.2, 37.9], exact McNemar\np=1.2e-07**. Of the 132 benign twins this gate refuses 2, and neither loses\nwork; taint refuses 49 and loses work on 43.\n\n### The one number to read before the headline\n\n**39.4% is an average over two different cases, not a rate.** What decides which\ncase you are in is the rule, and you can tell by reading your own policy before\nrunning anything:\n\n| Does the rule state a countable limit? | scenarios | Clay Seal | dataflow taint |\n| --- | ---: | --- | --- |\n| **yes** | 42 | **83.3% [69.4%, 91.7%]** | 2.4% [0.4%, 12.3%] |\n| no | 90 | 18.9% [12.1%, 28.2%] | 15.6% [9.5%, 24.4%] |\n\nFisher exact p = 6.5e-11.\n\n**So write your rules as ceilings on something you can count.** \"No more than\n$1,000 in refunds per session\" is enforced. \"Do not do anything inappropriate\"\nis not: there is no running total for the gateway to keep. `clayseal policy\nlint` already flags a tool that can spend but debits no budget, and this\nmeasurement is what that warning is worth.\n\n### The rest of it\n\nThe suite above is one we wrote, which is the first thing to distrust.\n[docs/EVIDENCE.md](https://github.com/clayseal/clayseal-capabilities/blob/main/docs/EVIDENCE.md) carries the evidence that is not ours,\nwhat the gate costs, and the limits, in full:\n\n| | |\n| --- | --- |\n| **An attacker that knows the defense** | 24 corpus and objective pairs, 173,699 candidates. In 23 of them, full visibility of the verdicts buys the attacker nothing. The 24th says what adaptivity is worth when it works |\n| **Eleven external corpora** nobody here authored | 98 to 100% where harm is defined by something the gateway can bind to, 31 to 55% where it is defined by the content of an authorized action, and the friction column that says what an incomplete policy costs |\n| **A live model**, AgentDojo prompt injection | 1 attack success in 216 runs, 0.5% [0.1, 2.6], against Progent's published 11.1 to 16.7% |\n| **What that costs**, paired per task over four models | −25 points on the weakest, −3 on the strongest, where CaMeL's published cost is 7 |\n| **34 µs per decision**, flat in session length | and the three numbers that are more interesting than the median |\n| **The limit that outranks all of it** | on open-ended work where the next step cannot be known in advance, 21.67 interruptions per task |\n\nEvery headline result and the command that reproduces it:\n[benchmarks/results/README.md](https://github.com/clayseal/clayseal-capabilities/blob/main/benchmarks/results/README.md).\n\n## Where the boundary is\n\nThree ways to place the enforcement point. They differ in what an agent has to do\nto get past them, and the difference is worth stating plainly instead of\ncollapsing into one number.\n\n| tier | mediates | got past by | status |\n| --- | --- | --- | --- |\n| in-process | tool calls your harness routes through `authorize()` | a harness that does not call it, or a tool nobody wrapped | shipped |\n| **proxy** | **every tool call on the transport, by construction** | a process that can reach the tool another way | **shipped** |\n| syscall | everything the guest can attempt, unforgeably | nothing at this layer | reference backend is macOS only |\n\nThe proxy also runs three checks the in-process path cannot. It screens every\npath a call names, not just the one the action carries. It refuses a message\nwhose JSON has duplicate keys. And it holds an effectful call until earlier\nresults arrive, so it is never asked where a destination came from before it has\nbeen told. The table in\n[docs/POLICY.md](https://github.com/clayseal/clayseal-capabilities/blob/main/docs/POLICY.md) lists the differences, and what an in-process\nintegration has to do itself.\n\nMost deployments want the proxy. The agent connects to `clayseal\nproxy`, and `clayseal proxy` runs the real MCP server, so a denied call is\nanswered with a JSON-RPC error and the server subprocess never receives the\nframe. Tools outside the policy are also removed from the advertised catalog, so\nthe agent does not plan around a tool it will then be refused.\n\nThe syscall tier compiles an envelope's egress and path scope into a sandbox\npolicy and takes back an unforgeable verdict stream. The reference backend\ndrives iVisor.\n\n**It catches one class nothing above it can.** A DNS tunnel encodes its payload\ninto subdomains, and the data leaves through the resolver. No tool is\ninvolved. There is no tool call, so `authorize()` is never asked, and every\nlayer above denies nothing. On the captured trace iVisor denies all four\nqueries and the tool-call layers deny none\n([syscall_tier.md](https://github.com/clayseal/clayseal-capabilities/blob/main/benchmarks/results/syscall_tier.md)).\n\nIt runs the other way too. Beaconing over an allow-listed channel gives iVisor\nnothing to refuse, because each syscall really is permitted; what is anomalous\nis the running total, and only the session budget holds that. Neither layer\nsubsumes the other, which is the argument for running both.\n\niVisor is macOS only by construction: Hypervisor.framework allows one VM per\nprocess and applies an irreversible Seatbelt profile to its caller. The\n`agentauth.sandbox_backends` entry point makes the substrate swappable, and a\nLinux seccomp or Landlock backend is open work, not something we ship.\n\n### The gap this does not close\n\nIn-scope staging. An agent reads a secret-classed object inside its grant and\nwrites the content to a publication-classed object inside the same grant. Both\nactions are authorized and the path scope cannot help, because the agent was\nlegitimately given the workspace that holds both. Measured against an oracle\nattacker, the escaped-task set is byte-identical to `allow-all`\n([in_scope_exfiltration.md](https://github.com/clayseal/clayseal-capabilities/blob/main/benchmarks/results/in_scope_exfiltration.md)). The\nconfidentiality flow tracker covers part of it as a step-up layer; wide fragment\nsplits and unkeyed encodings remain open. Any containment claim for a coding\nagent has to carry this one.\n\n## The policy document\n\nThe authority is a file, so a security team can read it, diff it in a pull\nrequest, and gate a merge on it.\n\n```yaml\nversion: 1\ngoal:\n  id: billing-triage-2026-08\n  summary: Triage the open billing tickets and email a summary to the ops archive.\nprofile: supervised          # autonomous | supervised | benchmark\nexpires_at: 2026-12-31T00:00:00Z\n\ntools:\n  allow:    [list_tickets, read_ticket, write_summary, send_email]\n  harmless: [list_tickets, read_ticket, write_summary]\n  effects:  {write_summary: write, send_email: send}\n\npaths:\n  allow: [\"out/**\", \"tickets/**\"]\n  deny:  [\".env\", \".git/**\"]\n  arg_names: {write_summary: path}     # which argument carries the path\n  pathless:  [send_email]              # asserted to act on no path\n\negress:\n  domains: [acme-internal.com]\n  bind_recipients: true\n\nbudgets:\n  calls:\n    ceilings: {emails: 3}\n    tracked:  {send_email: emails}\n```\n\n```bash\nclayseal policy show policy.yaml   # what it authorizes\nclayseal policy lint policy.yaml   # what a reviewer should ask about\n```\n\n`lint` exits non-zero on an error finding, so it works as a pre-merge gate. It\nreports what the gateway would refuse to start on, so the author does not find\nout from a traceback:\n\n```\nERROR   untracked-effectful-tool issue_refund: reachable and in the 'value'\n        family but debits no budget, so no ceiling applies to it at all\nWARNING session-scoped-ceiling   emails: counted per session, so a second\n        session gets a second ceiling\n```\n\nThe compiled policy carries a digest over the document, and the gateway attaches\nit to every decision, so an audit trail says which authority produced a decision\nand not only what the decision was.\n\n### Pointing it at your own tools\n\nTwo declarations do the work, and both exist because the defaults are tuned on\nbenchmark catalogs, not on real ones.\n\n`tools.effects` says what each tool does. The verb decides which floor rules\napply, and guessing it from the name works on names like `send_email` and fails\non names like `terraform_destroy`: of 24 tool names taken from widely used MCP\nservers, 17 are unrecognised by the classifier.\n\n`paths.arg_names` says which argument carries the path. The gateway looks for\n`file_path`, `path`, `filename` and `file`; a tool that calls it `target_dir` or\n`key` yields no path, and a path scope that cannot find a path does not apply. An\neffectful call whose path cannot be resolved is refused, not allowed\nunchecked, so the failure is loud instead of silent.\n\n`clayseal policy lint` names every tool that is missing either one. Read\n[docs/POLICY.md](https://github.com/clayseal/clayseal-capabilities/blob/main/docs/POLICY.md) before you write the first policy for a catalog\nyou did not design.\n\nIf the person who signs off works in risk and not engineering,\n[docs/CONTROLS.md](https://github.com/clayseal/clayseal-capabilities/blob/main/docs/CONTROLS.md) says which obligations this produces\nevidence for, quoting the framework text where the mapping is exact and staying\nat the function level where it is not. It opens by saying what it is not: a\nlibrary is not a control regime and cannot make anyone compliant with\nanything.\n\n### Writing the first one from what you already have\n\nTwo commands exist so the first policy is not written from a blank file. Both\nproduce a **draft a person finishes**, never a grant.\n\n```bash\n# What the tools are called, from the server you already run.\nclayseal policy init -- npx @your-org/mcp-server\n\n# What the organisation permits, from the document that already says so.\nclayseal policy draft delegation_of_authority.md\n\n# Both halves in one file. This is the one worth running.\nclayseal policy init --rules delegation_of_authority.md \\\n    -- npx @your-org/mcp-server > draft.yaml\nclayseal policy lint draft.yaml\n```\n\n`init` asks the server for `tools/list` and reads the tool names, the effect of\neach one, and the argument each carries its path in out of the schemas the\nserver already publishes. `draft` reads the ceilings, windows, once-only rules,\ndirectories and allowed domains out of prose, from sentences like \"a single\nvendor payment must not exceed $10,000\".\n\nNeither one is authority, for two different reasons. The **document** is an\nartefact edited by people who are not thinking about an agent, so whoever\ncontrols it would otherwise control the grant. The **catalog** is written by the\nserver this policy constrains, so a server describing `wire_funds` as \"reads the\nbalance\" would be writing its own limits. Three properties follow:\n\n- Every rule cites the line of the document it came from, so a reviewer checks\n  the YAML against the sentence without re-reading the source.\n- A sentence that reads like a rule and did not translate becomes a `TODO`\n  comment. A draft that looks complete is worse than one that admits what it\n  dropped, because a rule that vanished in translation is one nobody notices.\n- The catalog may **raise** a tool's effect and never lower one, and a server\n  calling its own effectful tool read-only is reported, not believed. A\n  tool that publishes no schema is not recorded as taking no path: \"the server\n  did not say\" and \"the server said no\" are different facts.\n\nWhat is deliberately left blank is `budgets.tracked`, where a ceiling meets a\ntool. The document knows the ceiling and the catalog knows which argument\ncarries the amount; which tool debits which ceiling is in neither, and naming\nthe wrong one splits a shared limit in two. Both are printed as commented\nsuggestions for a person to connect.\n\nWorked end to end on a 30-line delegation-of-authority document and a seven-tool\nAP server. It extracted 9 rules and left 3 sentences as TODOs. After review the\ngateway denies four things: a payment taking the rolling 24-hour total past\n$50,000, a second payment of an invoice already paid, a write to a `.ledger`\nfile, and a write outside `/finance/ap/`. One of those TODOs is\n\"the person who prepares a payment may not approve it\", which is a real rule\nthis layer does not express, and it appears as a comment in the output, not a\nsilence.\n\n## Adding a rule for your own workload\n\nEvery knob above tunes behaviour someone else chose. This is where \"in our shop\nX is also forbidden\" goes, without forking:\n\n```python\nfrom clayseal.capabilities import GoalSpec\nfrom clayseal.capabilities.deployable_stack import DeployableStack\nfrom clayseal.capabilities.session_rules import SessionRuleHit\n\ndef no_competitor_domains(action, session, *, goal_summary, egress_verbs):\n    if \"competitor.test\" in str(action.args or {}):\n        return SessionRuleHit(\"house-rules\", \"destination is a competitor domain\")\n    return None      # None means \"this rule has nothing to say\"\n\ngoal = GoalSpec(query_id=\"refund-run\", summary=\"Refund the disputed invoices.\")\nstack = DeployableStack.from_goal(goal, house_rules=(no_competitor_domains,))\n```\n\nA hit becomes a **step-up, never a denial**. A rule written against your\nworkload has not been measured against the traffic it will refuse, and a\nstep-up halts an autonomous attacker just as hard while leaving a person able\nto say yes. Your rules run after the shipped ones, so a house rule cannot mask\none that ships. A rule that raises is skipped and counted in\n`session_rules.RULE_FAILURES`, and the decision still goes through. A gateway\nthat stops authorizing because a regex threw is worse than one that misses a\nrule.\n\n## The lower-level API\n\n`Guardrail` above is the wrapper most integrations want. If you are building\nyour own loop and would rather call the gateway directly, the decision API is\none method:\n\n```python\nfrom clayseal.capabilities.monitor.action import Action\nfrom clayseal.capabilities.policy import compile_policy\nfrom clayseal.capabilities.tool_verbs import classify_verb\n\ngateway = compile_policy({\n    \"version\": 1,\n    \"goal\": {\"id\": \"billing\",\n             \"summary\": \"Triage tickets and email ops@acme-internal.com\"},\n    \"expires_at\": \"2030-01-01T00:00:00Z\",\n    \"tools\": {\"allow\": [\"read_ticket\", \"send_email\"],\n              \"harmless\": [\"read_ticket\"],\n              \"effects\": {\"read_ticket\": \"read\", \"send_email\": \"send\"}},\n    \"paths\": {\"pathless\": [\"read_ticket\", \"send_email\"]},\n    \"egress\": {\"domains\": [\"acme-internal.com\"], \"bind_recipients\": True},\n}).build()\n\nagent_calls = [\n    (\"read_ticket\", {\"id\": \"T-1042\"}),\n    (\"send_email\", {\"to\": \"ops@acme-internal.com\", \"body\": \"3 open, 1 escalated\"}),\n    (\"send_email\", {\"to\": \"collector-metrics.example\", \"body\": \"3 open, 1 escalated\"}),\n]\n\nfor step, (tool, args) in enumerate(agent_calls):\n    decision = gateway.authorize(Action(\n        step=step, tool=tool, resource=f\"mcp:tool:{tool}\",\n        verb=classify_verb(tool), args=args,\n    ))\n    print(tool, decision.outcome, decision.reasons)\n    if not decision.allowed:\n        continue                    # hand the reasons back to the agent\n    # run(tool, args)\n```\n\n`examples/01_gateway.py` (from a checkout) runs this end to end against a prompt injection planted\nin a ticket the agent was allowed to read. The fourth call is allowed and the\nfifth is held for a person.\n\n**What stops it there is the egress allow-list, which is a per-call rule.** The\ninjected address is off-domain, so the cheapest floor catches it and the\nprovenance layer is never consulted. That is the gateway working in the right\norder, and it is not a demonstration of anything this document claims is\ndistinctive, so the example prints which layer fired and\n[examples/README.md](https://github.com/clayseal/clayseal-capabilities/blob/main/examples/README.md) runs the same session with the domain\nlist removed, where provenance is what answers.\n\nIf your policy names a domain and an attacker names an address inside it, the\nfloor has nothing to say and `egress.recipients` is what binds the mailbox. It\nis not on by default and `clayseal policy lint` does not require it, so a\npolicy that grants a domain grants every mailbox on it.\n\n## Security posture\n\nThe guards fail closed unless the environment names itself development.\n\n```bash\nCLAYSEAL_ENV=development    # relaxes them, and says so once per process\n```\n\nUnset, or set to production, means an unpinned commit-token minting key is\nrefused, a missing replay store is refused, an unsigned step-up approval is\nrefused, and an intent envelope from an unpinned signer is refused. The polarity\nused to be the other way around, which meant every deployment that had not read\nthis section accepted all four.\n\nSee [docs/THREAT_MODEL.md](https://github.com/clayseal/clayseal-capabilities/blob/main/docs/THREAT_MODEL.md) for what is signed, who signs\nit, and what is out of scope.\n\n## Build from source\n\n```bash\ngit clone https://github.com/clayseal/clayseal-capabilities.git\ncd clayseal-capabilities\npython -m venv .venv && source .venv/bin/activate\npip install -e \".[dev]\"\npytest python/tests -q\npython examples/01_gateway.py\n```\n\nOptional extras:\n\n```bash\npip install \"clayseal[oidc]\"     # live OIDC/JWKS verification\npip install \"clayseal[spiffe]\"   # SPIFFE Workload API\npip install \"clayseal[redis]\"    # shared replay and ledger stores\npip install \"clayseal[monitor]\"  # the learned trajectory scorer\n```\n\n## Identity\n\nThe gateway does not need an identity layer. Bring verified claims from your own\nIdP, build an `IdentitySession`, and issue commit tokens from there. Adapters\nship for SPIFFE JWT-SVID, OIDC, Auth0, AWS STS, Entra Agent ID, Azure AD, GCP,\nand A2A signed agent cards.\n\n```python\nfrom clayseal.capabilities.identity_adapters import get_identity_provider\n\nsession = get_identity_provider(\"oidc\").build_session(\n    verified_claims,          # your IdP already checked signature, aud, exp\n    evidence_verified=True,\n)\n```\n\n## Documentation\n\n[docs/README.md](https://github.com/clayseal/clayseal-capabilities/blob/main/docs/README.md) is the index. The ones you are most likely to\nwant:\n\n- [Your first ten minutes](https://github.com/clayseal/clayseal-capabilities/blob/main/docs/START.md) to go from the demo to your own agent\n- [Agent notes](https://github.com/clayseal/clayseal-capabilities/blob/main/AGENTS.md) if you are a coding agent deploying this\n- [llms.txt](https://github.com/clayseal/clayseal-capabilities/blob/main/llms.txt) for a machine-readable index\n- [Evidence](https://github.com/clayseal/clayseal-capabilities/blob/main/docs/EVIDENCE.md) for every measured number and the limit it does not cross\n- [API reference](https://github.com/clayseal/clayseal-capabilities/blob/main/docs/API.md) for the exported names, tiered by what\n  most integrations actually use\n- [Developer guide](https://github.com/clayseal/clayseal-capabilities/blob/main/docs/DEV_GUIDE.md) to install it and wire it in\n- [Policy reference](https://github.com/clayseal/clayseal-capabilities/blob/main/docs/POLICY.md) for what a policy file can say\n- [Threat model](https://github.com/clayseal/clayseal-capabilities/blob/main/docs/THREAT_MODEL.md) for what it defends against and what it does not\n- [Privacy and data handling](https://github.com/clayseal/clayseal-capabilities/blob/main/docs/PRIVACY.md) for what it stores and what leaves the process\n\nFor reporting a vulnerability see [SECURITY.md](https://github.com/clayseal/clayseal-capabilities/blob/main/SECURITY.md); to contribute see\n[CONTRIBUTING.md](https://github.com/clayseal/clayseal-capabilities/blob/main/CONTRIBUTING.md) and the\n[code of conduct](https://github.com/clayseal/clayseal-capabilities/blob/main/CODE_OF_CONDUCT.md). Upgrading from `agentauth-capabilities`:\n[docs/MIGRATION.md](https://github.com/clayseal/clayseal-capabilities/blob/main/docs/MIGRATION.md). Corpora and their licences:\n[THIRD_PARTY.md](https://github.com/clayseal/clayseal-capabilities/blob/main/THIRD_PARTY.md). Cutting a release:\n[docs/RELEASING.md](https://github.com/clayseal/clayseal-capabilities/blob/main/docs/RELEASING.md).\n\n\n## Naming\n\nThe product, the distribution, the import root and the CLI are all `clayseal`.\n\nBefore 0.6 this shipped to design partners on a private feed as\n`agentauth-capabilities`, importing from `agentauth.capabilities`. Those import\npaths still resolve and emit a `DeprecationWarning`; they are removed in 0.7.\nThe aliased module is the same object as the real one, so a plugin registered\nthrough the old path is visible through the new one. Migration is a search and\nreplace: [docs/MIGRATION.md](https://github.com/clayseal/clayseal-capabilities/blob/main/docs/MIGRATION.md).\n\nStorage keys and wire identifiers were deliberately **not** renamed. The replay\nstore still keys commit tokens under `agentauth:commit:`, because a gateway that\nsilently stopped recognising the tokens it had already spent would reopen the\nreplay window it exists to close.\n\n`clayseal.core`, the shared contracts and crypto helpers, used to be a separate\nprivate distribution and now lives in this repository. The identity and receipts\nlayers remain separate distributions and neither is required here.\n\n## License\n\nMIT. See [LICENSE](https://github.com/clayseal/clayseal-capabilities/blob/main/LICENSE).\n",
  "bytes": 35111,
  "sha": "edd1ebe92cf08dbb7f1d18bcaa723649cd2f8a287455629c3212918b55939909",
  "repo_slug": "clayseal/clayseal-capabilities",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_clayseal_clayseal_95b6063b/readme"
}