v0.9.0 — the engine stops asking permission to enforce
v0.8.0 made the wallet a thing you could paste, hand over, and verify. v0.9.0 is the enforcement release: on governed paths, the engine no longer depends on the agent's cooperation.
Interception. The MCP broker fronts any upstream MCP server: register it, point the host at /mcp/broker/<upstream>, and every tools/call is authorized before a byte reaches the upstream — the upstream credential stays vaulted server-side, so the agent never holds it. And when a fronted upstream demands money with an x402 402, the broker prices the challenge and runs the ladder first; on anything but approval the challenge is withheld — an agent that never receives payment requirements cannot sign them. POST /v1/authorize/quote offers the same gate to any client. USD-pegged assets with known decimals only, worst case wins, no FX at decision time.
The tree and the clock. Tool and capability rules now inherit down the wallet tree — a child may tighten, never loosen; block it at the org root and no team below can allow it back. Rules can also read context: outside_hours_utc escalates deploys after hours, and after_model_calls_today is the runaway breaker. Signals are captured once into evidence, so replay reads the snapshot, never the live clock.
The meter. Every wallet counts decisions per month — approve, escalate, and deny each once; replays and simulations never. It surfaces on the spend console and /v1/wallets/stats. No fee attaches; the unit is measured honestly long before anything is priced in it.
Approvals where you are.Sanction for Slack is a live install surface: Add to Slack binds a workspace and channel to your wallet over OAuth, the bot token lands in the wallet's encrypted vault, and interactive Approve / Deny cards resolve through the same grant-minting path as the dashboard, actor recorded.
Under the hood: all 16 open dependency advisories cleared (npm audit: 0) — including Next.js SSRF and cache-confusion fixes via 16.3.3 — with weekly automated dependency PRs now standing guard; getsanction.com now speaks only for the authorization product; and a new guide covers composing Sanction with Vercel Connect: authorize the capability, then mint the token.
slackapprovalsdistributiongovernance
Slack is now a live approval surface
Sanction for Slack is live at /slack. When an agent crosses a policy boundary, the person who can decide no longer has to find the dashboard: they connect a workspace and channel with Add to Slack, then receive an interactive card where the work is already happening. Approve mints the same one-use, expiring grant as the dashboard; Deny stops the action. Both outcomes retain the actor and the complete decision record.
The install is wallet-bound from the first redirect. Slack OAuth state names the admin's wallet, the workspace bot token is stored under that wallet's SEC-1 envelope, and a click is accepted only after Slack signature verification and a match to the installed workspace and channel. A channel archive URL and platform bot token remain a fallback for existing webhook routes, but the normal path is now a real install — no pasted URL, no shared platform token.
This is distribution, not a parallel approval system: MCP, the SDK, REST, and Slack all resolve through the same policy ladder and resolveApproval path. The agent still retries with its grant; the audit trail still tells one story.
engineapimcpbroker
The x402 spend gate: authorized before the wallet signs
STABLE-0 taught the ledger the rail. STABLE-1 puts the engine in front of it. An x402 402 is a demand for money, and stablecoin settlement is irreversible — there is no chargeback, so the moment between receiving a payment challenge and signing it is the only control point there is. Sanction now takes that moment. Post the challenge to POST /v1/authorize/quote and it is priced and run through the same ladder as any other purchase — budgets, escalation bands, one-use grants, cascade caps, evidence, the decision meter — with STABLE-0 settlement metadata derived from the quote itself, so the audit row already says x402 / usdc / base. Through the MCP broker this is interception, not cooperation: when a fronted upstream answers 402, the broker prices and authorizes it, and on anything but an approval the challenge is withheld — an agent that never receives the payment requirements cannot sign them. Two rules keep it honest. No oracle, no FX: a quote is priced only when its asset is a USD-pegged stablecoin with known decimals (native USDC on Base today); anything else is denied as QUOTE_NOT_PRICEABLE rather than guessed, because a decision must never depend on a rate we looked up at decision time. Worst case wins: when a challenge offers several ways to pay, the most expensive is what gets authorized, and one unpriceable option poisons the whole challenge — the client could have picked exactly that one. Still non-custodial and veto-only: Sanction holds no keys and signs nothing. It decides whether the demand is allowed to reach your wallet.
engineapiconsole
The ledger learns the rail; the meter learns the decision
Two small slices that point at the same future. STABLE-0: POST /v1/authorize now accepts optional settlement metadata — {rail, asset, network}, a closed vocabulary (x402/usdc/base is the triple the agent-payments world is converging on) — inert to the decision itself, recorded on the row, and surfaced in the audit feed and CSV. Stablecoin settlement is irreversible, which makes pre-transaction authorization the only control point; the ledger is now rail-aware before any rail-specific enforcement exists. Sanction authorizes the spend; any rail settles it. MONO-0: a per-wallet, per-month decision counter across all four authorize surfaces — approve, escalate, and deny each count once; idempotent replays, grant redemptions, and simulate runs never do, and metering runs after the response so it can never fail or slow a decision. It shows as Decisions (month) on the spend console and month.decisions on /v1/wallets/stats. No fee attaches to it — it exists so the unit is measured honestly long before anything is ever priced in it. Found and fixed while wiring: the provision route still caught a subtree-cap breach inside its transaction, committing the ancestor counters the reservation loop had already incremented — phantom pool spend on a denied provision. It now rolls back and persists the denial outside, evidence attached, exactly like the spend route.
mcpbrokerenginecredentials
The broker: on fronted MCP servers, a hijacked agent cannot act
Until today, MCP governance was cooperative — the host had to ask. The broker removes the asking. Register an upstream MCP server on the wallet (POST /v1/broker/upstreams) and point the host at /mcp/broker/<upstream> instead of the upstream: every tools/call is authorized through the same shell as POST /v1/authorize/tool — inheritance, conditional rules, observe mode, evidence, escalation — before a single byte reaches the upstream. A refused call comes back as a proper MCP result with the machine code and remediation; an escalation names its request_id, and the approval is redeemed by retrying the same call with _meta["sanction/grant_id"]. The credential story is the quiet half: the upstream's API key is SEC-1-vaulted on the wallet and injected server-side on the forward — the agent holds only its Sanction key, and the broker never copies inbound headers upstream, so the Sanction key cannot leak either. Honest scope, stated on the Wallet Card: interception holds for traffic through the broker (and the LLM gateway, as before); the plain wallet URL stays cooperative, and traffic that goes straight to an upstream is not governed. One shell, not a fork: the broker calls the same enforcement path the REST route uses, so the two surfaces cannot drift. Found and fixed along the way: the gateway's vaulted provider-key read ran outside the tenant context, and under the vault's forced row-level security that read silently returned nothing — a connected provider still answered PROVIDER_NOT_CONNECTED. Both reads now run inside the tenant transaction.
enginegovernanceconditions
Rules that read the clock and the meter
A tool rule can now carry a when. Two predicates ship, a closed vocabulary with exactly one per rule: outside_hours_utc — production deploys escalate to a human outside business hours, wrapping midnight if your hours do — and after_model_calls_today — the runaway breaker: once an agent's model calls pass N today, block everything until a human looks. Effects are restrictive only; an allow is never conditional, so allow-list semantics never depend on a clock. Determinism holds the way ADR-0009 demands: the enforcement shell captures the signals once per request (the UTC hour, the persisted model-call count — fetched only when a rule actually conditions on it) and stores them in the decision's evidence, so replay reads the recorded snapshot, never the live clock. Conditions inherit down the wallet tree like every other rule — an org-level breaker binds every team. One honest correction from the design doc: the counting signal is model calls, not "decisions" — allowed tool calls are decision-only and never persisted, so a decisions counter would be blind to exactly the loops this exists to stop. Not a DSL, not an expression language: composition is more rules, which stays simulable and explainable.
enginegovernanceinheritancetree
Block it once at the org — no team below can allow it back
Budgets always cascaded down the wallet tree; rules now do too. Tool and capability decisions consult every ancestor policy — each layer runs through the same pure ladder independently, and the verdicts fold deny > escalate > allow with the root-most objector named in the decision. The semantics are one sentence: a child may tighten, never loosen. Block payments.charge at the org root and no team's allow-list can bring it back; put deploy.production on the org's escalate list and every deploy below waits for a human; and a permissive parent (allow *) can never silently widen a team's strict allow-list — which is exactly why this is per-layer evaluation, not a merge of rule lists. Nothing copies down: every wallet keeps its own policy and revision chain, and each persisted decision records which layer decided plus every revision consulted, so evidence replay reproduces the verdict and the trail shows the whole chain was heard. Same fold on the REST routes and the AuthZEN PDP — one engine, every surface. The policy console shows what a wallet inherits, read-only, named by parent. Spend thresholds stay per-wallet by design: money ceilings are the piece owners tune per team, and the tree-wide money control remains the subtree cap.
sdkdistributionrelease
npm install sanction-sdk
The TypeScript SDK is on npm: npm install sanction-sdk — 0.8.0, FSL-1.1-MIT, zero runtime dependencies, ESM + types. SanctionClient for the agent plane (authorize spend and tools, log tokens, the full escalate→poll→grant_id retry loop), SanctionAdminClient for the management plane, and the framework adapters — SanctionMiddleware for any tool runtime and sanctionTool for the Vercel AI SDK — so the tool executes behind the decision. Unscoped like sanction-mcp, published by the same automation path. The Python side (LiteLLM callback, LangChain/LangGraph + CrewAI bindings) is Next.
0.8.0releasemcpwalletconsoleapprovalsteam
v0.8.0 — a wallet you can paste, hand over, and verify
Where v0.7.0 made governance adoptable, v0.8.0 makes the wallet a thing you can paste, hand over, and verify.
Paste it. The wallet is a URL: https://getsanction.com/mcp speaks Streamable HTTP and authenticates with the agent key, so Cursor and Claude connectors take a URL and a header instead of a JSON blob — the same ten tools as npx sanction-mcp. A browser GET now returns a paste page rather than a raw 401. sanction-mcp is conformant with the 2026-07-28 revision and forwards W3C trace context, so a governed decision correlates with the agent run that asked for it. Still cooperative: the host must ask before acting. Broker interception of tools/call is Next, and we don't claim it today.
Verify it.GET /.well-known/wallet-card.json is how a counterparty discovers what this wallet carries, and POST /v1/mandate/verify lets them check a presented execution mandate with no Sanction API key — frozen, revoked, expired, or garbage, each named, and invalid answers HTTP 200 so agents fail closed on the body.
Hand it over. Wallets now have people: team membership with roles, a wallet switcher that reaches every membership, and a viewer role that genuinely cannot mutate. An org owner decides escalations anywhere in their subtree. And approvals reach you where you already are — Add to Slack installs per workspace over OAuth and posts interactive Approve / Deny cards that run the same resolveApproval path as the dashboard, with the actor recorded.
See it. The console leads with a roster — the wallet tree as groups, agents as cards, a mandate stamp on each — and the rail is three items instead of thirteen. Month runway draws cumulative burn against the monthly cap with the projected exhaust date, and seat health flags the seats whose denial rate is hot or climbing, each with the decision code it keeps hitting. Providers are connected once: keys land in the vault and inject at the gateway.
One money fix worth naming: gateway metering no longer fails open to $0 for a model missing from the price table — unknown models bill at the table-max fallback rate with a warning. Under-metering is a budget hole, so it now errs expensive.
Two enforcement-path fixes found by the pre-release review, both fail-open or state-corrupting. A denied subtree-cap decision used to commit the ancestor counters the reservation had already incremented before it hit the cap — phantom spend against a parent pool that the reconcile could never heal downward, eventually denying legitimate spend on money nobody spent. The breach now rolls the whole transaction back, the way the grant path and the AuthZEN PDP already did, and the denial it writes carries the revision and context it was missing. Separately, OpenAI-compatible streams only report usage when the caller asks for it — so an agent could stream with stream_options omitted and burn the wallet's provider key unmetered. The gateway now sets include_usage on the way out: metering is not the governed party's choice, and a stream that still reports nothing is logged instead of passing silently.
Eleven policy packs ship installable, each previewable against your last 30 days before it applies. The TypeScript SDK ships as sanction-sdk — renamed from the planned @sanction/sdk scope so it publishes unscoped like sanction-mcp, no npm org required (older entries reference the scoped name; this one supersedes them). It builds, typechecks, and passes its 55 tests in-repo.
mcpwalletdistribution
A URL to paste: hosted Sanction MCP
The wallet is now a URL. https://getsanction.com/mcp is Streamable HTTP, authenticated with the agent key (x-api-key or Authorization: Bearer pxy_...). Same ten tools as npx sanction-mcp. Still cooperative — the host must ask before acting; broker interception of tools/call is Next. Cursor / Claude connectors paste the URL and a header, not a JSON blob. Discovery: /.well-known/wallet-card.json lists carry.mcp_remote. The card already names carry, present, verify, and the signed evidence export — a per-decision receipt both parties keep is Next.
consoleux
The dashboard is a roster
Overview is no longer a monthly report of zeros. Roster is the home: the wallet tree as group cards, agents as cards inside them, a mandate stamp (live / paused / blocked) on each. Add an agent on the group you are looking at — name it, copy the key once. The rail is three items: Roster, Approvals, Vault. Old URLs stay; Vault is the drawer for credentials, people, policy, spend, and the signed record.
approvalsslackoauth
Add to Slack — approve without pasting a channel URL
Interactive Approve/Deny no longer needs a platform bot token and a pasted archive URL. Add to Slack on the Approvals page runs Slack OAuth v2 (chat:write + incoming-webhook), stores the bot token under the wallet's SEC-1 envelope, and posts the card to the channel picked at install. Incoming webhooks still deep-link only. The env bot token remains a fallback for archive-URL routes. Notifications.
approvalsslacknotifications
Approve or deny in Slack — without opening the dashboard
Incoming Slack webhooks still deep-link; they cannot receive button clicks. Interactive Approve / Deny is a Slack app: paste a channel archive URL (https://slack.com/archives/C…), set SANCTION_SLACK_SIGNING_SECRET and SANCTION_SLACK_BOT_TOKEN, and the bot posts the card via chat.postMessage. The click is HMAC-verified over the raw body and runs the same resolveApproval path as the dashboard — grant, audit, actor recorded as slack:<username>. The interactive endpoint fails closed if the signing secret is unset. Notifications.
sdkpythonlitellm
One Python import: LiteLLM posts usage to Sanction
Python agents get a real adapter, not a copy-paste recipe. SanctionLiteLLMLogger duck-types LiteLLM's CustomLogger and posts each successful completion to POST /tokens. Wire it with litellm.callbacks = [SanctionLiteLLMLogger(api_key=...)]. The package lives at packages/sanction-python (unpublished; runtime dep is httpx only — it never imports litellm). This is meter/report after the call. Fail-closed spend is still /api/gateway/<provider>. LangChain and CrewAI stay Next. Adapter guide.
mcpwalleta2amandate
The agent wallet is a public object
The engine was already a wallet; MCP was a client. This ships the public object. GET /.well-known/wallet-card.json is how other agents discover carry, present, and verify — and that stdio MCP is still cooperative (only the LLM gateway intercepts today). POST /v1/mandate/verify lets a counterparty check a presented execution JWT with no API key: frozen, revoked, expired, or garbage, each named; invalid is HTTP 200 so agents fail closed on the body. /exec mints that mandate. Hosted remote MCP (the broker that intercepts tools/call) stays Next. Guide.
mcpconformanceobservabilityaudit
MCP checks the badge. Sanction decides what the badge opens.
MCP shipped its largest revision since launch today: the protocol core goes stateless, the initialize handshake is gone, Roots/Sampling/Logging enter a formal deprecation lifecycle, and authorization gets hardened. If you read "MCP hardens authorization" and reached for the alarm — don't. That work is authentication: RFC 9207 issuer validation, credentials bound to their issuing server, metadata documents replacing dynamic registration. It proves who is calling. It says nothing about whether this agent may spend $4,000 with an unfamiliar merchant at 2am, under whose budget, or who signs when it crosses the line. The spec is not moving into the policy layer.
sanction-mcp is conformant, and the parts that were ours are done. The stateless core is a tailwind — our authorize call was already a self-contained POST, and the escalation loop already threads state the way the revision prescribes, as an explicit request_id then a one-use grant_id the caller passes back. Zero dependencies on the three deprecated features. Tool schema bodies verified clean under a strict JSON Schema 2020-12 validator. And the deprecation of protocol Logging is now a feature: Sanction reads the revision's reserved W3C trace context keys off every tool call and forwards them to the API, so a governed decision correlates with the agent run that requested it — one story across your traces and our audit log. Tracing is diagnostic and never load-bearing; every value is validated against its W3C grammar before it becomes an outbound header, because host input that turns into headers is how you get header injection. All of it is a CI gate, not a claim: tests/mcpConformance.test.ts fails the build if the surface drifts. Next, and deliberately not this week: Tasks maps almost exactly onto escalation — a request that pauses for a human and mints a grant on approval is a resumable task — and MCP Apps can render the approve/deny card where the human already is. Both need the v2 SDK, which is beta. This package governs real money in other people's stacks; it doesn't take a beta dependency on release week. Conformance details.
providersgatewayvault
Connect a provider once
Provider API keys stop living in every agent's environment. Connect a provider on the Providers page and the key is stored under the wallet's SEC-1 envelope as provider:<id>, then injected into that provider's native auth header at the gateway — server-side, never returned to the caller. Agents that bring their own provider auth are untouched. An agent with neither its own auth nor a vaulted key fails closed before any upstream call, with a pointer rather than a silent pass-through.
teamrolesconsole
A wallet holds people, and a viewer cannot touch anything
A wallet is no longer one human with one key. Team membership gives each person their own identity at their own role — owner, admin, viewer — invited by email and accepted through Better Auth, never through the shared management key. The role floor is structural, not cosmetic: every dashboard mutation action calls requireSessionRole("admin"), so a viewer can read the whole console and change none of it. The two read-only policy actions (pack preview, draft simulation) stay open deliberately — simulating a hypothetical policy is visibility, not a mutation. A wallet switcher reaches every membership you hold, so owning your own wallet no longer hides the ones you were invited to.
mcpdx
The MCP wallet knows which wallet it is
sanction_wallet_status used to require SANCTION_WALLET_ID in the host config. It now derives the wallet from the agent key that is already authenticating the call, so an MCP host needs one secret instead of two. SANCTION_WALLET_ID still works when set.
approvalsgovernanceconsole
The org owner can decide anywhere in their subtree
An escalation used to be resolvable only by the owner of the exact wallet it belonged to — so a parent-org owner watching a child pool's escalation could see it and not act on it. Resolution now authorizes against the whole subtree (subtreeWalletIds), behind the same admin role floor, and runs the identical resolveApproval path with the actor recorded. The pool's own owner still resolves in their own inbox. Honest boundary: the subtree authorization is asserted by wiring rather than proven by a test that builds a real two-level org — it is listed in docs/TRACEABILITY.md Gaps until that test exists.
0.7.0releaseobservelocalsdkadoption
v0.7.0 — adopt first, enforce when the numbers say so
Where v0.6.0 made governance drop-in and provable, v0.7.0 makes it adoptable — you don't have to flip enforcement on to start. Observe mode runs the real engine and records exactly what it would have done, blocking nothing, so you can point Sanction at a live fleet, watch a week of would-be denials and the dollars behind them, and flip each pool to enforce when the ledger convinces you — the revision chain marking the moment it went live. Around that on-ramp: Sanction Local's install package (the no-egress pack + the assessor's self-verifying evidence download) turns the air-gapped runtime into something a regulated shop can actually stand up; the @sanction/sdk is publish-ready with the escalate→grant loop closed; escalation notices now land the human on the exact decision, not a generic inbox; and the first pay-per-crawl slice governs what your agents pay the metered web. Adopt quietly, enforce deliberately, govern where the agents already are.
observeadoptiongovernanceconsole
See what enforcement would do — before it does anything
The hardest part of adopting a governance layer is the first deny — nobody wants to be the reason a production agent stalled on day one. Observe mode removes that risk. Set a pool to observe and the identical decision engine runs on every request and persists the truthful would-be outcome — but blocks nothing: responses read approved with the real decision in would_be, no approvals page a human, and no counters move (an observed pool can never trip a shared cap for an enforcing sibling). Freeze and no-policy still enforce; everything else just watches. The new Observe console page reads those rows back as the flip-to-enforce case — per pool over 7 days, what enforcement would have denied or escalated with the dollars behind it and the exact decision codes that would fire, the real ledger with enforcement stood down, never a simulation. Flip a pool observe→enforce in one confirm-gated click; the policy revision chain records precisely when enforcement went live. Watch first, enforce when you're convinced — and org roots now read the whole subtree, so a parent wallet shows every pool's activity, not an empty page.
approvalsnotificationsux
The approval email lands you on the decision, not the inbox
When an agent's charge escalates, the notification that finds you — email, Slack, or a signed webhook — now deep-links to that pending request, not the generic approvals list. The email's "Review & decide" button drops you straight onto the decision waiting on you, so approving is one click from wherever the alert reached you. Small change, real difference when a fleet is escalating in parallel and you're deciding from your phone.
adaptersx402pay-per-crawlspend
The web now charges your agents. Sanction decides what they pay.
Cloudflare flipped the switch: any site, dataset, API, or MCP tool behind them can now charge agents per request — a 402 with a crawler-price quote, settled over x402, with agent crawlers blocked by default on ad pages from September 15. The protocol's own answer to "what will my fleet pay?" is a static max-price header. A cap is not governance. The SDK's new sanctionedFetch wraps your crawler's fetch (your Web Bot Auth signing stays yours): a paid page's quote runs through /authorize as a real spend decision — merchant = the site, category = content-access — so auto-approve bands, per-domain audit, daily and pooled department budgets, human escalation on unusual prices, and one-call freeze all apply before a cent of payment intent leaves your stack. Approved quotes retry echoing the site's own price verbatim; anything else is a typed planning outcome carrying the request id. Every payment lands in the audit feed tagged pay-per-crawl with the URL, so finance can answer "what did we pay the web last month, by department?" — the question this era just invented. Guide.
The TypeScript SDK is ready for npm as @sanction/sdk@0.6.0 (FSL-1.1-MIT, same terms as the server source). Package metadata, LICENSE, build/prepublishOnly, and a manual publish-sdk workflow mirror the MCP publish path. The escalate→grant loop is complete in the client: getAuthorization(requestId) polls an escalated decision for its one-use grantId, and sanctionTool accepts grantId on retry. npm install @sanction/sdk once the workflow has run against the sanction npm org. Python adapters stay Next.
The Local roadmap item was "runtime to install." The install half lands here. A new no-egress policy pack (channel: local) allows only exact on-box tools — local.ollama, local.chroma, local.embeddings, local.memory — blocks named cloud egress tools, and escalates or blocks new capability. Every deny still persists as an audit row (the no-egress evidence shape). On the console, the Audit page gains Signed evidence (JSON): the same tamper-evident AUDIT-1 document as GET /v1/audit/export, cookie-authed so an operator downloads it for the assessor without curling an sk_ key. Preview the pack against your last 30 days before applying; verify any export with POST /v1/audit/verify. Sanction decides and evidences — the runtime must still refuse egress.
0.6.0releasesdkauditauthzenconsole
v0.6.0 — drop-in governance, provable history
One day after v0.5.0 closed the surface-parity gap, v0.6.0 ships the two things a governance layer must do to be adopted and to be believed: drop in without a rewrite, and prove its own record. The SDK's framework adapters put the decision in front of execution — authorizeTool, SanctionMiddleware, and native Vercel AI SDK tool binding — so the model plans freely but the tool runs behind an approve. The decision history now exports as a signed, hash-chained document any auditor verifies self-contained, and the what-if simulator replays your real week in order, an early denial freeing budget downstream. Around the core: the AuthZEN PDP hardened (single-use denial tokens, no grant burn in batches, no approval dead loops, per-agent rate limits), capability governance reached the MCP runtime (sanction_authorize_capability, sanction-mcp 0.6.0), the org owner's console now sees the whole wallet tree, and the storefront finally names the internal-governance buyer it was built for. Full detail in the entries below.
mcpcapabilityrelease
Capabilities ask first from any MCP host — sanction-mcp 0.6.0
Capability governance (CAP-1) reached the runtime where agents actually live. The new sanction_authorize_capability MCP tool asks before an agent installs a skill, adds a plugin, or reaches a new API — the same ordered rule ladder, approval inbox, and one-use grants as the REST surface, now from any MCP host. npx sanction-mcp; ten tools total.
consolegovernancepools
The org owner sees the whole org
Budgets already cascaded through the wallet tree; now visibility does too. The Audit page reads your entire subtree — every department pool's decisions, token burn, and secret access in one trail, each row carrying its pool name, the per-agent rollup spanning the org. The Approvals inbox gains Waiting in your pools: escalations pending in the wallets below yours, visible the moment they stall — read-only by design, because each pool's owner decides in their own inbox; the org owner's job is knowing they're waiting, not deciding over their head. A leaf wallet sees exactly what it saw before. This is the internal-governance shape made whole: the CFO who set the department budgets can now watch the same tree enforce them.
authzensecurityhardening
The PDP grows armor — AuthZEN hardening sprint 2
Five deeper findings from the AuthZEN code review, closed. Binding tokens are now single-use: every requestable denial carries a jti that's consumed atomically with the escalation it opens — replay the same denial under a fresh idempotency key and you get a problem+json refusal, not a second approval (honest retries still replay safely; they're checked first). Batch requests pre-validate every item before anything evaluates, so a malformed sibling can no longer burn a grant redemption into a 400 it didn't cause. Timeout-approvals mint a real grant (issuedBy: policy_timeout) — an AARP poller now receives something redeemable instead of an approved-with-nothing dead loop. All four AuthZEN endpoints rate-limit per agent with Retry-After (the 50-item batch amplifies, so it gets the tighter window). And an explicit empty batch returns empty, not a surprise default-tuple evaluation. Fourteen new tests pin it all.
simulationpolicyevidence
What-if, replayed in order
The retro-simulation could already tell you which of last week's decisions a candidate policy would flip — but it held budget counters constant, so it couldn't model the knock-on: a policy that denies an early charge frees that budget for the request that came after. Now it can. POST /v1/policy/simulate takes mode: "sequential" — it replays the window in chronological order, threading each agent's approved spend forward (daily and monthly counters reset at the UTC boundaries), so a would-denial no longer counts against the requests that follow it. The default stays as_recorded (each decision replays with the budget it actually saw); sequential shows the week as it would have lived under the new policy. Same read-only honesty envelope — nothing persisted, debited, or escalated — and the response says which mode ran and that subtree/pool caps aren't threaded yet.
sdkadaptersintegrations
The tool runs behind the decision — framework adapters land in the SDK
The TypeScript SDK (in-repo; publishing is next) grew the adapter layer that puts Sanction's tool gate in front of execution instead of beside it in a log. authorizeTool wraps any "run this tool" thunk: approved → run; escalated → wait for the one-use grant; denied → SanctionToolBlocked with the machine code and request id, so a denial is a normal planning outcome, not a crash. SanctionMiddleware does the same as a wrap-everything layer, and sanctionTool binds it natively to Vercel AI SDK tools — the model can plan whatever it likes, but the tool only executes behind an approve. The adapters are framework-agnostic on purpose (they take a client and a thunk); LangChain, CrewAI, and LiteLLM bindings ride the same core next, recipes already in the adapter guide.
0.5.0authzenmcpgovernancerelease
v0.5.0 — one engine, every surface: AuthZEN + MCP reach parity
The guarantee Sanction sells — same request, same policy, same state, same decision — now holds across surfaces, not just on /authorize. The AuthZEN PDP and the MCP tools were built earlier and had quietly fallen behind the native path as new controls landed; this release closes the gap. A frozen wallet now denies on the AuthZEN evaluation and on grant redemption, exactly as every native route does — no spending through the one-control stop. Cost-per-outcome ceilings are enforced on the PDP too, so a spec-conformant enforcement point can't keep buying through a throttled channel. Token budgets — the seat's monthly line and the pooled per-department daily cap, not just the daily — are enforced when usage is logged via /tokens or MCP, closing a path that drained caps it didn't check. On the MCP side, escalations are no longer a dead end: a new sanction_check_authorization tool polls a pending approval for its one-use grant, and config or freeze failures surface their real cause instead of a generic denial. Access-request submissions now require an idempotency key so one approval can't be replayed into many. A cross-surface parity test suite pins it all so the surfaces can't drift again.
0.4.0mcpoutcomesrelease
sanction-mcp 0.4.0 — outcomes from any MCP host
The MCP server learned to report results, not just ask permission. sanction_log_outcome records a confirmed business outcome — an enrollment, a booking, a signed engagement — from whatever MCP host your agent runs in, with dedupe_key idempotency so a retried tool call never double-counts. That closes the loop for cost-per-outcome governance without a single custom integration: the same agent that asks sanction_authorize before spending now attests what the spend produced. npx sanction-mcp as always; eight tools total.
outcomesgovernancefreezebudgets
Spend answers to outcomes — ceilings, freeze, reallocation
A budget says what an agent may spend. An outcome says what the spend was for. Now Sanction governs on the ratio. Your systems attest results (POST /v1/outcomes — an enrollment, a booking; idempotent via dedupe_key, Sanction never invents them), and a wallet with a cost_per_outcome ceiling escalates every further charge once its windowed cost-per-outcome would cross the line — never denies, the channel just stops spending silently and starts asking a human. A cold-start guard keeps small samples ungoverned until there's enough signal. Around it, the two controls a CFO actually reaches for: wallet freeze — one call pauses a wallet and its entire subtree across every surface (spend, tools, provisioning, credentials, the gateway), deleting nothing, and unfreeze resumes where the fleet stopped — and budget reallocation, which moves cap between sibling pools atomically, refuses to overdraw, and leaves a revision trail plus an audit row. The dashboard's new Outcomes page reads it all back: cost per outcome per pool against its ceiling, with the engine's own status semantics — frozen, cold start, throttled, or earning its cost.
localairgapaudit
Sanction Local's runtime went air-gapped
The reference client (AIIA, the agent runtime we dogfood Sanction with) now has a real air-gap mode: one flag (AIIA_AIRGAP=1) and inference, embeddings, retrieval, and memory all stay on the box. Every cloud egress point — LLM providers, transcription, notifications, TTS, web fetch — is denied fail-closed, and each denied attempt is reported to Sanction, where it persists as an audit row. That's the Sanction Local claim made mechanical: data never leaves the building isn't a promise, it's a deny-list plus an export your assessor can read. The runbook ships with it — probe script, health readout of every governed egress point, and the network-watch commands to verify silence on the wire yourself.
auditauthorizationevidence
Denials are now evidence
A denied tool authorization used to be decision-only — correct, deterministic, and gone. Now every deny on /v1/authorize/tool persists as an audit row carrying the policy revision it ran under and the exact context the engine evaluated, so it replays like every other decision. Allows stay decision-only (tools fire at high frequency; deterministic decisions replay for free) — but a deny is an anomaly, and on a no-egress policy it's the whole point: the row is the proof the attempt was stopped. Denied rows surface in the audit feed and the CSV export with everything else.
gatewayprovidersmetering
Perplexity behind the gateway
The LLM gateway now meters Perplexity. Point any OpenAI-compatible client at /api/gateway/perplexity with your x-sanction-key and every sonar call — search, reasoning, deep research — lands in the same token budget, spend caps, and audit trail as your Anthropic, OpenAI, and Gemini traffic. Streaming metered in-flight, fail-closed like the rest: if usage can't be metered, the response is withheld, not given away free.
consoleonboardingauth
The console catches up to the API
Everything the API could do that the console couldn't: closed. The full policy editor is on the dashboard — every field, packs with a one-click picker, and simulation preview inline so you see which of last month's calls would flip before you save. The audit & reporting page brings the decision feed and CSV export to the browser. Onboarding got a guidance layer with one-click MCP installs for Claude Code and Cursor. And Sign in with Apple joins Google and GitHub. You click what you used to curl.
policypackssimulation
Policy packs — start from a policy that fits, with proof
A blank policy form asks you to invent numbers. Packs start you from a position: Metering first (watch, don't block), Startup defaults (a ceiling, not a committee), Team workspace (departmental budgets, escalation where it matters), Compliance baseline (fail closed — everything asks). GET /v1/policy/packs lists them; one call applies one — through the same revision-writing path as every policy edit, so the evidentiary chain doesn't care whether you typed the numbers or picked a pack. And because simulation shipped first, every pack comes with proof instead of promises: POST /v1/policy/packs/{id}/preview replays your last 30 days under the pack and shows exactly which calls would have flipped — before you commit.
docsarchitecture
The architecture, taught
The docs taught integration before they taught the mental model. Fixed, in three surfaces. How Sanction works: the whole system in one diagram — identity stays upstream, every action passes through one atomic evaluation, three outcomes, execution wherever the agent runs. Why Sanction: the six claims underneath everything — identity isn't authorization, prompts aren't policy, observability isn't enforcement, approval is not execution, evidence requires replay, governance should travel with the agent. And a Concepts library — Authorization, Evidence & replay, Capability governance — so the model comes before the endpoints. Read those once and every API makes sense.
policyevidencesimulation
What if? — test a policy against last week before you set it
Editing a budget used to be a guess: set $500, wait a week, see what breaks. Now you ask first. POST /v1/policy/simulate takes a candidate policy — any subset of the normal policy fields, in dollars — and replays your real decision history under it: under a $500 daily budget, 14 of last week's 212 approvals would have been denied, $1,840 of spend would not have cleared, and these are the exact fourteen. Every stored decision already carries the exact context the engine evaluated (that's the evidence layer), and the rules are pure, so the simulation is the same code path that made the original calls — not an estimate. The response is honest about its envelope: counters are held as recorded (an early simulated denial doesn't free up budget for a later request — yet), and anything it can't simulate is counted and named, never guessed.
reportingnotifications
The Monday-morning digest
The week now reports itself. Subscribe a notification route to report.weekly_digest (one checkbox on the dashboard, or the events array on POST /v1/webhooks) and every Monday your Slack gets last week in one card: spend and token cost with week-over-week deltas, approved / denied / escalated counts, secret accesses, and the busiest agent. Machine endpoints get the same rollup as signed JSON. It's opt-in — nothing changes for existing routes unless they subscribed to everything — and a quiet week still reports: all zeros is information too.
reportingprojectionsbudgets
Reporting that looks both ways
Reporting used to answer one day at a time, looking backward. Now it spans periods and looks ahead. GET /v1/reporting/summary takes any range up to 92 days and returns totals, day-by-day buckets, and per-agent breakdowns — this week vs last week is two calls. GET /v1/wallets/stats now projects: today and this month each carry their budget, the linear pace projection, and an exhaustion forecast ("at this pace, the month's budget runs out on the 23rd"). The early-morning and early-month guards keep one 6 a.m. purchase from projecting nonsense. And GET /v1/audit-events?format=csv hands finance the whole feed, spreadsheet-ready. Scheduled digests — the Monday-morning summary in your Slack — are next.
authorizationcapabilitiesskills
Capability governance — new powers ask first
An agent that installs a skill, adds a plugin, or reaches a new API has changed what it can do. That change is now a governed action like spending money. One ordered rule list on the wallet policy, with namespaced patterns: block skill:install:crypto-*, escalate skill:install:*, allow api:github.com/*. POST /v1/authorize/capability answers allow, deny, or escalate; escalations land in the same approval inbox, approval mints the same one-use grant, and every escalation carries replayable evidence. The AuthZEN wire speaks it too: resource.type: "capability", with the AARP appeal offer on escalations. Skills are how agents grow; now growing is governed.
authorizationdeveloper-experienceevidence
Denials that answer back
A 403 is a dead end. A Sanction denial now answers the four questions an agent actually has:
What happened? The machine code, as always: DAILY_BUDGET_EXCEEDED.
Why? A limit block with the fired rule's live values: limit $200, used $184.20, remaining $15.80, you asked for $42.
What changes the answer?resets_at tells you when the clock helps. And hard budget denials now carry the same signed access_request offer as escalations — on the native API and the AuthZEN wire both — so the agent can appeal to a human instead of waiting for midnight.
Where is the evidence?links.record and links.evidence point at the decision and its replay.
The numbers come from the decision's own stored evidence, so an idempotent replay answers identically without re-reading budget state. An appealed denial lands in the same inbox as every escalation; approving mints the same one-use grant.
evidencepolicyaudit
Decisions you can replay
Audit tells you what happened. Evidence proves the engine decided correctly under the policy that existed then. Those are different problems, and today Sanction solves the second one:
Every policy edit is now an immutable revision. Change a budget, a threshold, a tool list — the previous state is snapshotted forever, and the policy's revision number ticks up. There is no code path that mutates a policy without writing the record.
Every decision records what it saw. The revision in force, and the exact context the engine evaluated — the amount, the budget state read under the lock, the lists, the thresholds.
GET /v1/authorize/{id}/evidence returns all of it, plus a live replay: the same pure rules re-run over the stored context, right now, with a matches flag proving the record still reproduces the decision.
This is the determinism principle doing real work: same request, same policy revision, same state snapshot, same decision — so replay is one call to a pure function, not a reconstruction project. Existing policies were backfilled as revision 1; hash-chained exports stay on the roadmap as the next rung.
authorizationstandardsauthzenapprovals
The approval loop speaks AuthZEN too — AARP, same day
This morning Sanction learned to answer the AuthZEN evaluation wire. Tonight the part that makes Sanction Sanction — deny is easy, escalate is the product — rides the standard too, via the draft Access Request and Approval Profile:
Requestable denials. A would-escalate evaluation now returns context.access_request: where to open the approval, and a signed binding_token proving the denial happened here.
POST /access/v1/access-request turns that denial into a real escalation — the owner's inbox, email, Slack, webhooks, all of it. You get an AARP task handle back.
Poll the task, and approval carries the profile's artifact: approval.id is the one-use grant, approved_until its expiry.
Redeem by re-evaluating with context.approval — the grant is consumed atomically, one use, exactly the native semantics. A replay denies with aarp_reason: "approval_expired" and next_action: "request" so the agent replans instead of retrying forever.
GET /.well-known/authzen-configuration advertises the endpoints and the access-request capability, so a PEP given only the hostname finds everything.
The profile is draft 1; we implemented the loop and skipped the periphery (callbacks — use notification routes; catalogs and form schemas) while it stabilizes. The loop itself has been in production here since before the profile named it.
authorizationstandardsauthzen
Sanction speaks AuthZEN — a standards-native PDP
Sanction now answers the OpenID AuthZEN Authorization API 1.0 — the approved standard for how an enforcement point asks a decision point "may this happen?". Any AuthZEN-capable gateway or framework can point at Sanction as its PDP with zero Sanction-specific code:
POST /access/v1/evaluation — the standard subject/action/resource tuple in, { "decision": true } out. resource.type selects the ladder: tool runs the block/allow/escalate lists, spend and provision run the dollar ladders against live budget state (daily, monthly, per-transaction, cascading subtree caps).
POST /access/v1/evaluations — the batch form, with all three spec semantics: execute_all, deny_on_first_deny, permit_on_first_permit.
Denials explain themselves. A deny is a spec-correct decision: false, and context carries Sanction's stable machine code and remediation — TOOL_BLOCKED, ESCALATION_REQUIRED, DAILY_BUDGET_EXCEEDED — so agents replan instead of guessing.
Decision-only, fail-closed. Evaluation never debits a budget or opens an approval (the ?simulate=true contract); a would-escalate answer tells the PEP exactly which Sanction endpoint opens the real approval and mints the one-use grant. The subject binds to the authenticated agent's key — asking about anyone else fails closed.
The AuthZEN access-request-and-approval profile (AARP) — the standards path for Sanction's escalate → approve → grant loop — is next.
approvalsnotificationsslack
Approvals that find you — Slack, natively
Paste a Slack incoming-webhook URL into notification routes and escalations arrive as readable messages, not raw JSON:
Approvals land as "tenet needs approval for $60.00 — Vendor" with a Review in Sanction button.
Budget warnings read "tenet has used 84% of its daily spend" — same 80% line as the dashboard meters and emails.
Resolutions and exhaustions report themselves too. Machine consumers are untouched: non-Slack endpoints keep the HMAC-signed raw payload.
No Slack app to install, no OAuth — the incoming-webhook URL is the whole setup, and Sanction detects it automatically.
seatsagentsmanagement
Seat wallets, slice 1 — seats you can hand around
An agent is now a seat: a keyed identity with budgets you can hand to whoever holds it.
Contractor auto-shutoff — set expires_at on any agent and its key fails closed past that instant, on the data plane and the LLM gateway both. No deactivation step to forget.
Holders — every seat can carry the name of the human holding it, for the audit trail and the dashboard. Rotating a key accepts a new holder in the same motion: the person changes, the seat's history, budgets, and clearance stay.
Batch seats — POST /v1/agents/batch stamps one template across up to 50 seats in one call: "five engineering seats, $20/day each, clearance 2, expiring end of quarter." Each key is shown once; only hashes are stored.
Slice 2 (seat-history semantics on reassignment, per-provider budgets within a seat) is being shaped with our design partner. Roadmap: seat wallets stays in Now until the full story ships.
authorizationtoolsgrants
Tool escalations reach the inbox — govern any action closes the loop
Tool governance was decision-only: an escalated tool call answered escalated and then evaporated. Now it completes the same loop as spend and provision:
Escalations persist — an escalated POST /v1/authorize/tool writes an audit row and lands in the owner's approval inbox alongside spend and provision requests, with webhook + email notification.
Approval mints a one-use tool grant — redeem it by retrying the same tool (and server) with grant_id, or poll /v1/authorize/{id}. Consumption is atomic and one-use; a mismatched tool gets GRANT_MISMATCH, a replayed grant gets GRANT_ALREADY_USED.
Idempotency-Key on tool calls — replaying the key returns the escalation's current state, including the terminal decision once the owner resolves it, so a re-POST doubles as a status check.
sanction-mcpsanction_authorize_tool gains grant_id, and /api/openapi.json now documents the endpoint.
Proven end-to-end against real Postgres: escalate → inbox approval → grant minted → redeem on retry → second redemption refused. The full trace is in docs/TRACEABILITY.md.
auditbudgetssdkdashboard
The audit plane arrives — unified feed, daily summary, monthly caps, and a TypeScript SDK
Yesterday the authorization plane grew muscles; today it grew a memory and a voice:
Unified audit feed — GET /v1/audit-events merges every spend decision, token log, and credential injection into one time-sorted feed with type filters and cursor pagination. The "what did my agents do?" endpoint.
Daily summary — GET /v1/reporting/daily-summary: one call returns the day's spend, decision counts, token cost, secret accesses, and the five most expensive tasks. The screen you check before coffee.
Monthly spend caps — policies can now set an opt-in monthly_spend_budget_usd alongside the daily cap, enforced atomically on both spend and provision (a provision is spend — no side doors). New MONTHLY_BUDGET_EXCEEDED decision code with a remediation hint.
@sanction/sdk — a zero-dependency TypeScript SDK for both planes: SanctionClient (authorize, log tokens, scoped credential injection) and SanctionAdminClient (wallets, agents, policy). A denied decision is returned, not thrown — agents branch and replan.
Runnable starter kit — examples/nightly-coding-agent demonstrates the whole governed-autonomy loop offline against a deterministic mock, plus copyable policy blueprints for coding and research agents.
Console shell — the dashboard is now one console: a persistent sidebar with a live pending-approvals badge across Overview, Agents, Pools, Spend, Approvals, and Grants.
Red-team CI gate — five adversarial guardrail probes (over-limit spend, blocked category, over-clearance credential, out-of-scope injection, exec-budget breach) now run on every push. The suite passing is the "5/5 guardrails held" scorecard.
Next on the roadmap: Sanction Local, and a local-first SDK fallback for when the network isn't there.
The biggest shipping day since going public — the authorization plane grew three new muscles:
Provision authorization — govern resource provisioning as one native call: POST /v1/authorize/provision takes resource + line item + quantity + dollars, runs the same policy ladder and daily budget as spend, and adds resource allow/block/escalate lists. Escalations mint one-use grants; retry with the grant to proceed.
No-surprises budget alerts — a new budget.threshold webhook + heads-up email fires exactly once when a charge crosses 80% of any daily budget or pool cap — before anything is denied. Dashboards now project the burn: "on pace for $86 today · cap hit ~4:12 PM."
Budget pools + allocation — delegated pools across your account tree, with allocation strategies to rebalance a parent cap across child pools from the dashboard.
Postgres row-level security — the credential vault and clearance tables are now tenant-isolated at the database level, fail-closed, on top of the KMS envelope encryption already at rest. A SQL bug can't read another tenant's rows; a DB dump can't be decrypted.
sanction-mcp 0.3.0 (npm) — the MCP server catches up to all of it: a new sanction_authorize_provision tool, grant_id retries on authorize, and hardened transport (network failures now degrade to a clear deny, never an ambiguous error).
The roadmap moved with it: approval→grants, provision, pools, and threshold alerts are all Now; next up — Sanction Local and the agent-platform starter kit.
approvalswallets
Human approvals now issue grants
Sanction's approval loop now has a durable authorization record:
Generic approvals — spend escalations now land as PendingApproval rows that can also support tool and credential workflows.
Grants ledger — owner approval issues a short-lived Grant with provenance: who approved it, what agent received it, what resource it covers, and why.
Opt-in subtree caps — parent wallets can set subtree_daily_cap_usd to enforce a tree-wide daily spend cap across descendants. Leaving it null keeps subtree enforcement disabled while spend rollups remain visible.
v0.2.0release
v0.2.0 — framework guides, public roadmap, clearer positioning
Account tree — govern many agents under one master account, with per-tenant budgets and subtree spend rollup.
Clearer positioning — Sanction is the authorization layer for AI agents, with an honest integrations story.
product
Building Sanction in the open
We're now shipping our roadmap and release notes in public. Every
product update lands here, and you can tell us what to build next —
submit ideas, vote, and watch them move from under consideration to shipped.
Subscribe below and we'll send updates as they ship. No spam.
walletsplatform
Account tree — govern a fleet under one master account
Wallets can now nest into an org → tenant → sub-tenant hierarchy.
Provision one agent per tenant, set per-tenant budgets, and roll spend up the
tree for chargeback — all from a single account.
gatewaypolicy
Gateway metering across providers + escalation timeouts
Route Anthropic, OpenAI, and Gemini through one gateway and one key —
every token metered and capped in one place. Plus: an escalated request that no
human resolves now settles to a fail-closed fallback, so a polling agent never
deadlocks.
Security gate · PR #1security
Authenticated management plane + atomic spend
Closed a credential-disclosure path: the management plane now requires
an owner key on every call. Spend authorization is atomic and idempotent — a
retry can't double-spend, and two concurrent requests can't both slip past the
daily cap.