Actenon-Permit
uv run permit demo
Requires uv (
curl -LsSf https://astral.sh/uv/install.sh | sh). No uv?pip install -e '.[dev]' && permit demo, orpython -m actenon_permit.cli demo. No API keys, no network, no real money — works from a fresh clone.
Why
Most agent security incidents aren't clever exploits — they're over-permissioned agents doing permitted-but-catastrophic things (a refund bot that can also charge, an email helper with no spend cap, a coding agent whose shell tool has no rate limit). Actenon-Permit is the enforcement layer that sits between an agent and the real world: it issues bounded, expiring, revocable capability "grants", enforces hard runtime limits the agent physically cannot exceed, gates high-impact actions behind human approval, and keeps a tamper-evident audit trail.
The demo
uv run permit demo --auto-approve produces this exact 7-step arc (v0
in-process mode):
======================================================================
Actenon-Permit demo — refund-bot (scripted agent, no LLM, no network)
======================================================================
issued grant: id=grant_xxxxxxxxxxxxxxxx
agent: refund-bot
budget: USD 50 (remaining 50)
scopes.allow: ['payment.refund', 'email.send']
scopes.deny: ['payment.charge', 'shell.*']
approval: ['email.send']
step 1: refund($20) -> [ALLOW] allowed (budget 50 -> 30 ...)
step 2: refund($25) -> [ALLOW] allowed (budget 30 -> 5 ...)
step 3: refund($20) -> [DENY] would exceed USD 50 budget (only $5 left)
step 4: send_email(...) -> [ALLOW] approved by human (...)
step 5: charge($100) -> [DENY] scope denied: payment.charge (simulated injection)
>>> kill switch: `permit revoke refund-bot` — grant REVOKED
step 7: refund($1) -> [DENY] grant status is revoked (grant REVOKED)
ledger chain intact: True
proof the agent never held the real key:
the call signature the agent used was `refund(amount=20)` — no `secret` arg.
the broker resolved MOCK_STRIPE_KEY=sk_mock_*** internally and passed it
only to the mock provider. the agent only saw the allow/deny result.
======================================================================
demo complete.
======================================================================
v1 gateway demo (out-of-process PEP)
uv run permit demo --mode gateway --auto-approve runs the same 7-step arc,
but every action goes through the v1 out-of-process gateway over HTTP. The
agent process never imports the mock providers, never holds the secret, and
only talks to the gateway via POST /proxy/<tool> with a bearer token. This
is the real airlock: even an agent with arbitrary code-exec cannot import the
provider SDK to bypass the wrapper, because the secret is not in its memory.
How it works
- Grant — a signed, scoped, expiring capability issued to an agent out-of-band.
- Decide — every action runs through a deterministic, fail-closed PDP (status → expiry → deny-scope → allow-scope → rate → budget → approval).
- Broker — on ALLOW, the broker swaps the grant for the real credential for that one call; the secret never enters the agent's context.
- Ledger — every decision is appended to a hash-chained, tamper-evident log. The agent only ever sees the tool signature and the allow/deny result.
v0 vs v1 — both shipped
v0 = in-process cooperative enforcement. The PEP is a Python decorator
(@guard) inside the agent's process. This defeats the dominant threat — an
injected or confused agent choosing a dangerous tool or argument, runaway
loops, overspend — because the agent picks tool + args from a fixed registry
and the raw key only exists inside the broker. This is ~90% of real agent
incidents (Step Finance, Grok, Replit were all "agent did a permitted-but-
catastrophic thing").
v1 = out-of-process proxy / MCP-gateway PEP (now built). The gateway runs in a separate process, holds the real credentials, and enforces every decision server-side. The agent only ever holds a signed bearer token. Two transports are supported:
- HTTP proxy —
POST /proxy/{tool}withX-Actenon-Grantheader. Returns the tool's result on ALLOW, 403 with a JSON body on DENY. - MCP stdio — JSON-RPC 2.0 over stdin/stdout. Compatible with Claude
Desktop, Cursor, and any MCP-aware agent host. The grant token is passed
in
params._meta.actenon_grant.
Switch from v0 to v1 by changing one import in agent code:
# v0 (in-process)
from actenon_permit import guard, GuardRegistry
@guard("payment.refund", cost_from="amount",
credential_name="STRIPE_KEY", registry=reg)
def refund(secret, amount, reason=""): ... # agent holds the secret
# v1 (out-of-process — agent never holds the secret)
from actenon_permit import remote_guard, RemoteGuardRegistry
reg = RemoteGuardRegistry("http://127.0.0.1:7780")
reg.set_grant_token(token)
@remote_guard("payment.refund", cost_from="amount", registry=reg)
def refund(amount, reason=""): ... # no `secret` param — gateway holds it
v1 CLI
# ONE-TIME: persist a stable dev signing key so grants validate across
# process restarts (essential for multi-terminal workflows).
permit init-key
# Start the control plane + gateway on one port
permit serve --with-gateway --port 7780
# In another terminal: issue a grant + mint a token
permit issue examples/refund-bot-policy.yaml --quiet
permit mint-token <grant_id> --quiet
# Or run just the MCP stdio server (for Claude Desktop / Cursor)
permit mcp-serve
# Derive a weaker sub-grant (UCAN-style multi-agent delegation)
permit attenuate <grant_id> --budget-limit 10 --scopes-allow payment.refund
Without
permit init-key, eachpermitinvocation generates an ephemeral key, so a token minted bypermit mint-tokenin one process won't validate inpermit servein another.permit init-keywrites a stable key to~/.actenon-permit/signing-key(mode 0600) — run it once and forget.
TS SDK (ts-sdk/)
A typed TypeScript SDK for non-Python agents:
import { ControlPlaneClient, GatewayClient } from "@actenon/permit-sdk";
const cp = new ControlPlaneClient({ baseUrl: "http://127.0.0.1:7780" });
const grant = await cp.issueGrant({
agent: "refund-bot", ttl: "1h",
budget: { currency: "USD", limit: 50 },
scopes: { allow: ["payment.refund"], deny: ["payment.charge"] },
});
const { token } = await cp.mintToken(grant.id);
const gw = new GatewayClient({ baseUrl: "http://127.0.0.1:7780", grantToken: token });
const result = await gw.callTool("refund", { amount: 20, reason: "customer" });
Repo layout
actenon-permit/
├── README.md # this file — first code block is the working command
├── LICENSE # Apache-2.0
├── SPEC.md # the Grant / capability-token format (standards seed)
├── pyproject.toml # uv/pip installable; console entrypoint `permit`
├── .env.example # NAMES of secrets only — never real values
├── .gitignore # .env, *.db, __pycache__, .venv
├── .github/workflows/ci.yml # ruff + pytest + demo smoke on every push
├── src/actenon_permit/
│ ├── __init__.py # public API (v0 + v1)
│ ├── __main__.py # python -m actenon_permit
│ ├── model.py # Grant, Action, Decision, Scope, Budget, Rate
│ ├── policy.py # compile YAML/dict policy -> signed Grant
│ ├── pdp.py # decide(): deterministic, fail-closed
│ ├── state.py # StateStore iface + SQLiteStore (atomic reserve/commit)
│ ├── ledger.py # append-only hash-chained log
│ ├── broker.py # name->secret resolution; guarded execution
│ ├── enforce.py # @guard decorator + wrap (v0 in-process PEP)
│ ├── control.py # FastAPI control plane: grants/approvals/ledger/attenuate
│ ├── token.py # v1: grant bearer token wire format (v1.<b64url>)
│ ├── gateway.py # v1: out-of-process PEP gateway (ToolRegistry + Gateway)
│ ├── _proxy_routes.py # v1: HTTP proxy FastAPI routes (no future-annotations)
│ ├── pep_client.py # v1: remote_guard + RemoteGuardRegistry (agent side)
│ ├── _demo.py # v0 demo (in-process)
│ ├── _demo_gateway.py # v1 demo (out-of-process HTTP gateway)
│ ├── _mock_providers.py # fake payments + email — NO real network, NO real money
│ └── cli.py # permit issue|revoke|watch|ledger|demo|serve|mcp-serve|attenuate|mint-token
├── ts-sdk/ # v1: TypeScript SDK
│ ├── src/
│ │ ├── types.ts # Grant, Action, Decision, Policy, AttenuateRequest, ...
│ │ ├── token.ts # grant_to_token / verifyGrantToken (Web Crypto HMAC)
│ │ ├── client.ts # ControlPlaneClient (issue/revoke/attenuate/ledger)
│ │ ├── gateway-client.ts # GatewayClient (callTool via HTTP proxy)
│ │ └── index.ts # barrel export
│ ├── tests/sdk.test.ts # 6 tests incl. end-to-end against a real Python gateway
│ ├── package.json # @actenon/permit-sdk
│ └── tsconfig.json
├── examples/
│ ├── mock_providers.py # thin re-export wrapper (canonical impl in package)
│ └── demo.py # thin re-export wrapper
└── tests/
├── test_pdp.py # v0: allow/deny/budget/rate/expiry/scope/fail-closed
├── test_state.py # v0: concurrency — two parallel charges can't both clear $50
├── test_ledger.py # v0: tamper-evidence — editing an entry breaks the chain
├── test_demo.py # v0: full 7-step decision sequence end-to-end
├── test_token.py # v1: grant token roundtrip, tamper detection, wrong-key
├── test_gateway.py # v1: gateway direct + HTTP proxy + MCP stdio
├── test_remote_pep.py # v1: remote_guard end-to-end against a live HTTP server
└── test_attenuation_wire.py # v1: /grants/:id/attenuate + child token usable at gateway
Non-goals for v0/v1
No SaaS/multi-tenant, no distributed state, no real payment integration, no enterprise directory/SSO, no cloud, no Turing-complete policy DSL (declarative YAML only; OPA/Rego is a later escape hatch). Local, single-user, one unforgettable demo.
Roadmap (beyond v1)
- Distributed state — Raft/CRDT-backed state store for multi-node gateways. Currently the gateway is single-process; the state store is a single SQLite file.
- OPA/Rego policy escape hatch — a
policy_engine: "opa"option that delegatesdecide()to an embedded OPA server for Turing-complete rules. - Real provider SDKs — first-class adapters for Stripe, SendGrid, AWS,
etc. Currently the broker is provider-agnostic (just
real_call(secret, **args)). - MCP gateway-as-a-sidecar — a Docker image that runs
permit serve --with-gatewayso non-Python agents (Claude Desktop, Cursor, a Node.js agent) can mount it as a sidecar with zero local setup.
License
Apache-2.0. See LICENSE.
Metadata
Release files for actenon-permit 1.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| actenon_permit-1.4.0.tar.gz | 226.4 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| actenon_permit-1.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 369.5 kB
Release files / actenon_permit-1.4.0.tar.gz
| Download URL | actenon_permit-1.4.0.tar.gz |
|---|---|
| Size | 226.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2a48f8be1ed8dd3f747addfa0b8585c05eb8a8f12a6e97748cde45b5fd8dac61
|
|
BLAKE2b-256 checksum How to use checksums |
bc3e91aad55cb2bceb389b1c1c4371e5158cfda0b1fa7c88fa2636e9f9d52db8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Release files / actenon_permit-1.4.0-py3-none-any.whl
| Download URL | actenon_permit-1.4.0-py3-none-any.whl |
|---|---|
| Size | 143.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0c8610d586da74258f96855eabf382c6bd2eee1c5c94686cfefc14e10f1740e2
|
|
BLAKE2b-256 checksum How to use checksums |
15a04fd4a615d8abda13d0242d70f16fd8cc0928dba8f79124fd3777a2590a73
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Security for actenon-permit 1.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.