feat(brand): favicon + app icons from the new wordmark; re-land the stranded hero fix - #257
Conversation
Context-conditional tool rules, the second approved engine arc. A tool rule can carry a `when` — a CLOSED vocabulary, exactly one predicate per rule, restrictive effects only (an allow is never conditional, so allow-list semantics never depend on a clock or a counter). No DSL, no expressions: composition is more rules, which stays simulable and explainable. Two predicates ship: - outside_hours_utc: [start, end) UTC, wrapping midnight — "deploys escalate outside business hours". - after_model_calls_today: N — the runaway breaker: once the agent's persisted model calls pass N today, block (or escalate) matching tools. One honest correction from the design doc, recorded there: 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 runaway loops this exists to stop. Determinism (ADR-0009): signals are captured ONCE per request by the enforcement shell — the UTC hour, and the token-log count fetched ONLY when some layer actually conditions on it — passed into the pure ladder, and persisted in the decision's evidence. Replay reads the stored snapshot, never the live clock. A rule whose signal is absent from a context is deterministically inactive, so pre-COND rows replay unchanged. Precedence: block → conditional block → allow-list → escalate → conditional escalate. Conditions ride the INHERIT-1 layers, so an org-level breaker binds every team below; parity on the AuthZEN PDP with the same signal contract. New Policy.toolConditions Json column (migration included), validated in lib/policy.ts (one predicate, hour bounds, start != end), writable through the existing PATCH /wallets/policy and console actions via policyInputSchema. OpenAPI documents the shape. Console: tool-posture card counts conditionals; the inherited card names them. Simulate boundary, stated not hidden: the tool ladder predates SIM-1's simulatable set; candidate tool_conditions echo back in ignored_fields. Verified: 13 new unit tests (window boundaries + midnight wrap, threshold edges with counts in the reason, absent-signal inactivity, precedence, prefix-glob), 4 route tests (threshold deny with signals persisted in evidence; the counter queried only when conditioned on; a parent's conditional rule binding the child), and both predicates live-fired in both directions against the seeded org — including catching and redoing a false positive where an unconditional escalate masked the conditional one. Policy page render-checked. Truth surfaces: TRACEABILITY COND-1 row + suite map, DOMAIN, roadmap Now, changelog, design doc stamped with the correction, backlog checked off. Gate: tsc clean, eslint 0 errors, 1130 passing. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Eb6zTfLyE42dYS8tgWfvn
/mcp/broker/<upstream> fronts registered MCP servers: every tools/call is authorized through the tool ladder (inheritance + conditions included) before it forwards; refusals come back as MCP isError results the model can read and replan on; escalations name the request_id and the _meta["sanction/grant_id"] retry path, and a redeemed grant is stripped before forwarding. Batched requests containing tools/call are refused — fail closed, never around. For brokered traffic, "a hijacked agent cannot act" is now enforcement, not cooperation. - lib/broker.ts: upstream registry (SEC-1-vaulted under mcp:<name>), request classification, forwarding with fresh outbound headers — the agent's Sanction key never reaches an upstream, and vaulted upstream credentials never reach the agent. - app/mcp/broker/[upstream]/route.ts: authorization runs as an in-process call to the /authorize/tool route handler — one enforcement shell by construction, no drift between broker and REST decisions. - app/api/v1/broker/upstreams: owner-plane register/list/revoke. - Found and fixed shipped bug: the gateway's provider-key vault read ran outside withTenant, so FORCE RLS silently returned null and connected providers 401'd PROVIDER_NOT_CONNECTED. Now tenant-scoped (and the gateway test mock now exercises withTenant). - Honesty flip: Wallet Card now says enforcement cooperative+broker, interception gateway+mcp-broker, with the scope stated plainly — traffic that bypasses the broker is not governed. - Version literals: walletCard/mcp.json/server.json now 0.8.0 (walletCard reads MCP_SERVER_VERSION instead of a hardcoded string). - Truth surfaces: README, roadmap, changelog, DOMAIN, AGENT-WALLET, TRACEABILITY, OpenAPI, backlog annotation. Tests: 17 new (broker classification/refusal + route: blocked never forwards, escalation carries the grant path, allowed forwards with upstream auth only, grant redeemed+stripped, batch refused). Full suite 1147 passing. Co-Authored-By: Claude <noreply@anthropic.com>
Eric's direction: agents' authorized spend settles in regulated stablecoins (USDC most likely), not card-rail USD — build the path. Position: stablecoin settlement is irreversible, so pre-transaction authorization becomes the only control point; the rail shift makes the engine more necessary. Slices STABLE-0..3 (settlement metadata → x402 spend gate → wallet-provider co-signer → onchain mirror), custody explicitly rejected. Market state verified 2026-08-22 (x402 under the Linux Foundation w/ Stripe/Visa/Mastercard, AP2 v0.2 mandates, GENIUS Act). Co-Authored-By: Claude <noreply@anthropic.com>
…itecture Market-structure bill (House-passed, Senate cloture 2026-09-15): its Section 604 codifies that non-custodial, non-controlling developers are not money transmitters. Adopt the design constraint now: Sanction's co-signing (STABLE-2/3) is veto-only — it can refuse a transfer the wallet initiates, never initiate or effectuate one. Co-Authored-By: Claude <noreply@anthropic.com>
Companion to stablecoin-rails.md, from a UX / financial / growth / brand / competitive research fan-out. Position: if Sanction meters, the unit is the decision (bps on spend rejected — wrong economics, wrong optics); Zapier-style bundles; self-serve tier payable via x402/USDC itself. Trust surfaces (fee meter, operator-set fee cap, per-decision receipts, grace mode) precede any invoice. Distribution ranked: MCP registries, /govern pages, Slack App Directory, x402/AP2 ecosystem listing (no policy layer listed there today), n8n before Zapier. First slices MONO-0 / DIST-0. Co-Authored-By: Claude <noreply@anthropic.com>
STABLE-0 (docs/plans/stablecoin-rails.md): POST /v1/authorize accepts
optional settlement {rail, asset, network} — a closed vocabulary
(lib/settlement.ts; a chain network is refused on a non-x402 rail so
evidence cannot lie). Inert to the decision like attribution tags, rides
detailsJson, surfaces in the audit feed and CSV, documented in OpenAPI.
Stablecoin settlement is irreversible, so pre-transaction authorization
is the only control point — the ledger is rail-aware before any
rail-specific enforcement exists.
MONO-0 (docs/plans/monetization-and-distribution.md): per-wallet,
per-UTC-month WalletDecisionCounter incremented once per FRESH engine
decision across all four authorize shells (approve/escalate/deny alike,
observe included). Idempotent replays, grant redemptions, simulate runs,
and frozen short-circuits never count; increments run via after() and
swallow errors — metering can never fail or slow a decision. Exposed as
month.decisions on /v1/wallets/stats and a Decisions (month) card on the
spend console. Instrumentation only: no fee attaches — the unit is
measured before it is ever priced.
Found and fixed while wiring: the provision route caught a subtree-cap
breach INSIDE its transaction, committing the ancestor counter increments
the reservation loop had already written — phantom pool spend on a denied
provision (the same defect the spend route's D1 fix removed). It now lets
the transaction roll back and persists the denial outside, evidence
attached, mirroring the spend route.
Tests: 14 new (closed-vocab settlement accept/reject, persistence, meter
counts fresh approve+deny once, replay/simulate never count, meter
failure swallowed, month key UTC, D1-parity provision regression).
Full suite 1161 passing. Live-fired: x402/usdc/base persisted on an
approved row, off-vocab rail 400s, counter=2 after one spend + one
allowed tool call, console card render-checked.
Co-Authored-By: Claude <noreply@anthropic.com>
Eric's call 2026-08-22: the model gains a metered middle — Pro $20/mo, metered in decisions, early access only until real billing ships (no checkout; trust surfaces precede any invoice). Free and enterprise framing unchanged. GTM launch arc captured in the backlog. Co-Authored-By: Claude <noreply@anthropic.com>
…the unit New /blog (index + slug pages, changelog conventions: typed array in lib/blog.ts, shared Markdown rendering, static params + per-post metadata). Post #1 announces the August arc honestly: broker interception, the settlement-aware ledger, the decision meter, the GENIUS/CLARITY ground and the veto-only architecture, with the x402 spend gate and Pro tier explicitly marked next-not-shipped. Co-Authored-By: Claude <noreply@anthropic.com>
New #rails section under the canonical line ("Sanction authorizes the
spend; any rail settles it"): the irreversibility argument, the
settlement-aware ledger marked live, the x402 spend gate marked next
(never overclaimed), and the non-custodial/veto-only regulatory posture
(GENIUS as instrument law, CLARITY as pending). Pricing becomes three
cards: Free (unchanged) / Pro $20/mo early access (metered in decisions,
lead-capture CTA, no checkout) / Enterprise Agreement. Disambiguation
line: sanction (v.) — to give official authorization.
Co-Authored-By: Claude <noreply@anthropic.com>
Two self-contained HTML documents under docs/gtm/: the 14-day launch playbook (positioning, sequence, the CLARITY-vote play, content engine, Pro funnel, metrics) and "Where Sanction sits" — seven integration architectures with pre-rendered sequence diagrams (static SVG, no runtime dependencies; open in any browser). Co-Authored-By: Claude <noreply@anthropic.com>
Strategy/GTM material does not belong in the public repo (same call as the 2026-07-06 move) — the launch playbook and use-case catalog now live in ericlovold/sanction-strategy under docs/gtm/. Co-Authored-By: Claude <noreply@anthropic.com>
…allet signs An x402 402 is a demand for money, and stablecoin settlement is irreversible: the moment between receiving a payment challenge and signing it is the only control point there is. STABLE-1 takes that moment. Two surfaces, one shell (both call POST /v1/authorize in-process, so a quote can never drift from ordinary spend): - Cooperative: POST /v1/authorize/quote — post the 402 body you just received; Sanction prices it and runs the same ladder (budgets, escalation, grants, cascade caps, evidence, decision meter), with STABLE-0 settlement metadata derived from the quote itself. - Intercepting: the MCP broker gates upstream 402s and, on anything but an approval, WITHHOLDS the challenge. The refusal type structurally cannot carry payTo/maxAmountRequired — an agent cannot sign payment requirements it never received. Two rules keep pricing honest and deterministic: - No oracle, no FX. A quote is priced only when its asset is a USD-pegged stablecoin with known decimals (native USDC on Base; address + 6 decimals verified against Circle). Anything else denies as QUOTE_NOT_PRICEABLE rather than being guessed — a decision must never depend on a rate looked up at decision time (ADR-0009). - Worst case wins. A challenge offering several ways to pay is authorized at its most expensive option, and one unpriceable option poisons the whole challenge: the client picks, and we cannot know which. Still non-custodial and veto-only — no keys, no signing, refusal only, which is the shape CLARITY §604's non-controlling test describes. Tests: 28 new (pricing, worst-case selection, poisoned challenges, the gate's withholding contract, quote route, broker interception). Full suite 1189 passing. Live-fired against Postgres: $0.05 and $15 approved, $900 denied PER_TXN_LIMIT, a multi-option challenge authorized at its worst case, every row carrying settlement metadata. Truth surfaces: /platform now says the gate is live (was "next"), changelog, roadmap, TRACEABILITY, OpenAPI, README, backlog, design doc. Co-Authored-By: Claude <noreply@anthropic.com>
… hero fix Icons were still the old shield-and-checkmark mark. The whole icon set is now generated from the $ glyph in Paul's wordmark (the three green paths of sanction-wordmark-green.svg, bounds measured with getBBox rather than eyeballed), on the brand's own pine ground with the wordmark's signal green: app/icon.svg 512, rounded square (browser tab) app/favicon.ico 16 + 32, PNG-in-ICO app/apple-icon.png 180 public/icons/icon-192|512.png PWA "any" public/icons/icon-maskable-512.png glyph inside the 80% safe zone manifest theme/background move to pine so the PWA splash frames the icon in the same dark it uses. Also re-lands the hero fix from #256, which was stranded: it merged into the marketing branch after that branch had already been squashed into main, so main still read "AVAILABLE $2,500.00". Sanction is non-custodial and veto-only, so the card states the limit — "DAILY CAP / ENFORCED". Verified: icon legible at 16px on light and dark tab strips; /, /icon.svg, /favicon.ico and /apple-icon.png all 200 with the right content types. Caught in render-check and fixed: Next decodes favicon.ico and requires RGBA, and an RGB one 500s every page — noted in AGENTS.md along with the production-deploy-path and stacked-PR lessons. Co-Authored-By: Claude <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 7a2886e. Configure here.
| background_color: "#0a0a0a", | ||
| theme_color: "#0a0a0a", | ||
| background_color: "#0c332a", | ||
| theme_color: "#0c332a", |
There was a problem hiding this comment.
Stale PWA icons after update
Medium Severity
The regenerated PWA icons still live at fixed /icons/* URLs that public/sw.js already treats as cache-first immutables under VERSION v2. This commit replaces those bytes but does not bump the service-worker cache version, so returning dashboard/PWA clients keep receiving the old shield icons from sanction-static-v2.
Reviewed by Cursor Bugbot for commit 7a2886e. Configure here.
…udit Cleanup sprint before handing the repo to a Codex pass. New skill `/fresh-eyes` — the inverse of `/INPUT`. Where INPUT ingests outside material INTO the project, fresh-eyes exports verified state OUT to a reader with no context, then stares at the repo the way that reader will. Two jobs, and the second is the one people skip: the export, and naming what our own fluency hides. Every claim must be verified live in the run that writes it — a handoff is read by someone who cannot check your work, so a stale claim is not caught, it is acted on. Contract is a blind-spot ledger: VERIFIED / LANDMINE / ASSUMED / UNOWNED / DECIDE / DRIFT. First run of it produced docs/handoff/CODEX-SPRINT-2026-08-23.md, and turned up one thing that matters more than the sprint: PRODUCTION IS A MERGE BEHIND MAIN. PR #259 merged at 19:23 UTC and produced no Vercel build at all. Last production deploy is #258, from the previous day. Live site confirms it — /opengraph-image.png 404s and /moral-intention still 200s instead of redirecting. Main auto-deployed three times the day before, so this is an anomaly, not configuration. Needs a human redeploy of main@5dc4c9c. Also verified and recorded: - Slack OAuth is complete end to end (SLACK-1 slice 2) — install path, interactive approve/deny, SlackInstall under RLS, fail-closed 503 when the signing secret is unset, five test files. Documented in the export. - Seven remote branches are fully absorbed into main. Each verified by locating its PR's squash commit, and for fix/hero-noncustodial (whose #256 was stranded and re-landed via #257) by content comparison. The delete command and every branch tip are recorded so it is reversible — the sandbox classifier denies the deletion, so it is queued for Eric. - Five remaining branches carry real unmerged work and are listed with what each holds, so nobody prunes them by mistake. AGENTS.md gains a dated note: a merge to main is not a deploy. Close an arc by checking production, not the merge. Gate: tsc 0, eslint 0, 1189 tests passing.
…udit (#260) * feat(engine): COND-1 — rules that read the clock and the meter Context-conditional tool rules, the second approved engine arc. A tool rule can carry a `when` — a CLOSED vocabulary, exactly one predicate per rule, restrictive effects only (an allow is never conditional, so allow-list semantics never depend on a clock or a counter). No DSL, no expressions: composition is more rules, which stays simulable and explainable. Two predicates ship: - outside_hours_utc: [start, end) UTC, wrapping midnight — "deploys escalate outside business hours". - after_model_calls_today: N — the runaway breaker: once the agent's persisted model calls pass N today, block (or escalate) matching tools. One honest correction from the design doc, recorded there: 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 runaway loops this exists to stop. Determinism (ADR-0009): signals are captured ONCE per request by the enforcement shell — the UTC hour, and the token-log count fetched ONLY when some layer actually conditions on it — passed into the pure ladder, and persisted in the decision's evidence. Replay reads the stored snapshot, never the live clock. A rule whose signal is absent from a context is deterministically inactive, so pre-COND rows replay unchanged. Precedence: block → conditional block → allow-list → escalate → conditional escalate. Conditions ride the INHERIT-1 layers, so an org-level breaker binds every team below; parity on the AuthZEN PDP with the same signal contract. New Policy.toolConditions Json column (migration included), validated in lib/policy.ts (one predicate, hour bounds, start != end), writable through the existing PATCH /wallets/policy and console actions via policyInputSchema. OpenAPI documents the shape. Console: tool-posture card counts conditionals; the inherited card names them. Simulate boundary, stated not hidden: the tool ladder predates SIM-1's simulatable set; candidate tool_conditions echo back in ignored_fields. Verified: 13 new unit tests (window boundaries + midnight wrap, threshold edges with counts in the reason, absent-signal inactivity, precedence, prefix-glob), 4 route tests (threshold deny with signals persisted in evidence; the counter queried only when conditioned on; a parent's conditional rule binding the child), and both predicates live-fired in both directions against the seeded org — including catching and redoing a false positive where an unconditional escalate masked the conditional one. Policy page render-checked. Truth surfaces: TRACEABILITY COND-1 row + suite map, DOMAIN, roadmap Now, changelog, design doc stamped with the correction, backlog checked off. Gate: tsc clean, eslint 0 errors, 1130 passing. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Eb6zTfLyE42dYS8tgWfvn * feat(broker): BROKER-1 — the MCP broker intercepts tools/call /mcp/broker/<upstream> fronts registered MCP servers: every tools/call is authorized through the tool ladder (inheritance + conditions included) before it forwards; refusals come back as MCP isError results the model can read and replan on; escalations name the request_id and the _meta["sanction/grant_id"] retry path, and a redeemed grant is stripped before forwarding. Batched requests containing tools/call are refused — fail closed, never around. For brokered traffic, "a hijacked agent cannot act" is now enforcement, not cooperation. - lib/broker.ts: upstream registry (SEC-1-vaulted under mcp:<name>), request classification, forwarding with fresh outbound headers — the agent's Sanction key never reaches an upstream, and vaulted upstream credentials never reach the agent. - app/mcp/broker/[upstream]/route.ts: authorization runs as an in-process call to the /authorize/tool route handler — one enforcement shell by construction, no drift between broker and REST decisions. - app/api/v1/broker/upstreams: owner-plane register/list/revoke. - Found and fixed shipped bug: the gateway's provider-key vault read ran outside withTenant, so FORCE RLS silently returned null and connected providers 401'd PROVIDER_NOT_CONNECTED. Now tenant-scoped (and the gateway test mock now exercises withTenant). - Honesty flip: Wallet Card now says enforcement cooperative+broker, interception gateway+mcp-broker, with the scope stated plainly — traffic that bypasses the broker is not governed. - Version literals: walletCard/mcp.json/server.json now 0.8.0 (walletCard reads MCP_SERVER_VERSION instead of a hardcoded string). - Truth surfaces: README, roadmap, changelog, DOMAIN, AGENT-WALLET, TRACEABILITY, OpenAPI, backlog annotation. Tests: 17 new (broker classification/refusal + route: blocked never forwards, escalation carries the grant path, allowed forwards with upstream auth only, grant redeemed+stripped, batch refused). Full suite 1147 passing. Co-Authored-By: Claude <noreply@anthropic.com> * docs(plans): stablecoin rails — design position + backlog capture Eric's direction: agents' authorized spend settles in regulated stablecoins (USDC most likely), not card-rail USD — build the path. Position: stablecoin settlement is irreversible, so pre-transaction authorization becomes the only control point; the rail shift makes the engine more necessary. Slices STABLE-0..3 (settlement metadata → x402 spend gate → wallet-provider co-signer → onchain mirror), custody explicitly rejected. Market state verified 2026-08-22 (x402 under the Linux Foundation w/ Stripe/Visa/Mastercard, AP2 v0.2 mandates, GENIUS Act). Co-Authored-By: Claude <noreply@anthropic.com> * docs(plans): CLARITY Act — Section 604 makes veto-only the legal architecture Market-structure bill (House-passed, Senate cloture 2026-09-15): its Section 604 codifies that non-custodial, non-controlling developers are not money transmitters. Adopt the design constraint now: Sanction's co-signing (STABLE-2/3) is veto-only — it can refuse a transfer the wallet initiates, never initiate or effectuate one. Co-Authored-By: Claude <noreply@anthropic.com> * docs(plans): monetization + distribution — five-discipline synthesis Companion to stablecoin-rails.md, from a UX / financial / growth / brand / competitive research fan-out. Position: if Sanction meters, the unit is the decision (bps on spend rejected — wrong economics, wrong optics); Zapier-style bundles; self-serve tier payable via x402/USDC itself. Trust surfaces (fee meter, operator-set fee cap, per-decision receipts, grace mode) precede any invoice. Distribution ranked: MCP registries, /govern pages, Slack App Directory, x402/AP2 ecosystem listing (no policy layer listed there today), n8n before Zapier. First slices MONO-0 / DIST-0. Co-Authored-By: Claude <noreply@anthropic.com> * feat(engine): STABLE-0 rail-aware ledger + MONO-0 decision meter STABLE-0 (docs/plans/stablecoin-rails.md): POST /v1/authorize accepts optional settlement {rail, asset, network} — a closed vocabulary (lib/settlement.ts; a chain network is refused on a non-x402 rail so evidence cannot lie). Inert to the decision like attribution tags, rides detailsJson, surfaces in the audit feed and CSV, documented in OpenAPI. Stablecoin settlement is irreversible, so pre-transaction authorization is the only control point — the ledger is rail-aware before any rail-specific enforcement exists. MONO-0 (docs/plans/monetization-and-distribution.md): per-wallet, per-UTC-month WalletDecisionCounter incremented once per FRESH engine decision across all four authorize shells (approve/escalate/deny alike, observe included). Idempotent replays, grant redemptions, simulate runs, and frozen short-circuits never count; increments run via after() and swallow errors — metering can never fail or slow a decision. Exposed as month.decisions on /v1/wallets/stats and a Decisions (month) card on the spend console. Instrumentation only: no fee attaches — the unit is measured before it is ever priced. Found and fixed while wiring: the provision route caught a subtree-cap breach INSIDE its transaction, committing the ancestor counter increments the reservation loop had already written — phantom pool spend on a denied provision (the same defect the spend route's D1 fix removed). It now lets the transaction roll back and persists the denial outside, evidence attached, mirroring the spend route. Tests: 14 new (closed-vocab settlement accept/reject, persistence, meter counts fresh approve+deny once, replay/simulate never count, meter failure swallowed, month key UTC, D1-parity provision regression). Full suite 1161 passing. Live-fired: x402/usdc/base persisted on an approved row, off-vocab rail 400s, counter=2 after one spend + one allowed tool call, console card render-checked. Co-Authored-By: Claude <noreply@anthropic.com> * docs: dated model note (Pro $20 metered middle) + GTM arc backlog entry Eric's call 2026-08-22: the model gains a metered middle — Pro $20/mo, metered in decisions, early access only until real billing ships (no checkout; trust surfaces precede any invoice). Free and enterprise framing unchanged. GTM launch arc captured in the backlog. Co-Authored-By: Claude <noreply@anthropic.com> * feat(site): blog — first post: the rail is changing, the decision is the unit New /blog (index + slug pages, changelog conventions: typed array in lib/blog.ts, shared Markdown rendering, static params + per-post metadata). Post #1 announces the August arc honestly: broker interception, the settlement-aware ledger, the decision meter, the GENIUS/CLARITY ground and the veto-only architecture, with the x402 spend gate and Pro tier explicitly marked next-not-shipped. Co-Authored-By: Claude <noreply@anthropic.com> * feat(site): platform goes stablecoin-era — rails section + Pro pricing New #rails section under the canonical line ("Sanction authorizes the spend; any rail settles it"): the irreversibility argument, the settlement-aware ledger marked live, the x402 spend gate marked next (never overclaimed), and the non-custodial/veto-only regulatory posture (GENIUS as instrument law, CLARITY as pending). Pricing becomes three cards: Free (unchanged) / Pro $20/mo early access (metered in decisions, lead-capture CTA, no checkout) / Enterprise Agreement. Disambiguation line: sanction (v.) — to give official authorization. Co-Authored-By: Claude <noreply@anthropic.com> * docs(gtm): launch playbook + integration use-case catalog as HTML Two self-contained HTML documents under docs/gtm/: the 14-day launch playbook (positioning, sequence, the CLARITY-vote play, content engine, Pro funnel, metrics) and "Where Sanction sits" — seven integration architectures with pre-rendered sequence diagrams (static SVG, no runtime dependencies; open in any browser). Co-Authored-By: Claude <noreply@anthropic.com> * docs: relocate GTM documents to the private strategy repo Strategy/GTM material does not belong in the public repo (same call as the 2026-07-06 move) — the launch playbook and use-case catalog now live in ericlovold/sanction-strategy under docs/gtm/. Co-Authored-By: Claude <noreply@anthropic.com> * feat(engine): STABLE-1 — the x402 spend gate, authorized before the wallet signs An x402 402 is a demand for money, and stablecoin settlement is irreversible: the moment between receiving a payment challenge and signing it is the only control point there is. STABLE-1 takes that moment. Two surfaces, one shell (both call POST /v1/authorize in-process, so a quote can never drift from ordinary spend): - Cooperative: POST /v1/authorize/quote — post the 402 body you just received; Sanction prices it and runs the same ladder (budgets, escalation, grants, cascade caps, evidence, decision meter), with STABLE-0 settlement metadata derived from the quote itself. - Intercepting: the MCP broker gates upstream 402s and, on anything but an approval, WITHHOLDS the challenge. The refusal type structurally cannot carry payTo/maxAmountRequired — an agent cannot sign payment requirements it never received. Two rules keep pricing honest and deterministic: - No oracle, no FX. A quote is priced only when its asset is a USD-pegged stablecoin with known decimals (native USDC on Base; address + 6 decimals verified against Circle). Anything else denies as QUOTE_NOT_PRICEABLE rather than being guessed — a decision must never depend on a rate looked up at decision time (ADR-0009). - Worst case wins. A challenge offering several ways to pay is authorized at its most expensive option, and one unpriceable option poisons the whole challenge: the client picks, and we cannot know which. Still non-custodial and veto-only — no keys, no signing, refusal only, which is the shape CLARITY §604's non-controlling test describes. Tests: 28 new (pricing, worst-case selection, poisoned challenges, the gate's withholding contract, quote route, broker interception). Full suite 1189 passing. Live-fired against Postgres: $0.05 and $15 approved, $900 denied PER_TXN_LIMIT, a multi-option challenge authorized at its worst case, every row carrying settlement metadata. Truth surfaces: /platform now says the gate is live (was "next"), changelog, roadmap, TRACEABILITY, OpenAPI, README, backlog, design doc. Co-Authored-By: Claude <noreply@anthropic.com> * feat(brand): favicon and app icons from the new wordmark; re-land the hero fix Icons were still the old shield-and-checkmark mark. The whole icon set is now generated from the $ glyph in Paul's wordmark (the three green paths of sanction-wordmark-green.svg, bounds measured with getBBox rather than eyeballed), on the brand's own pine ground with the wordmark's signal green: app/icon.svg 512, rounded square (browser tab) app/favicon.ico 16 + 32, PNG-in-ICO app/apple-icon.png 180 public/icons/icon-192|512.png PWA "any" public/icons/icon-maskable-512.png glyph inside the 80% safe zone manifest theme/background move to pine so the PWA splash frames the icon in the same dark it uses. Also re-lands the hero fix from #256, which was stranded: it merged into the marketing branch after that branch had already been squashed into main, so main still read "AVAILABLE $2,500.00". Sanction is non-custodial and veto-only, so the card states the limit — "DAILY CAP / ENFORCED". Verified: icon legible at 16px on light and dark tab strips; /, /icon.svg, /favicon.ico and /apple-icon.png all 200 with the right content types. Caught in render-check and fixed: Next decodes favicon.ico and requires RGBA, and an RGB one 500s every page — noted in AGENTS.md along with the production-deploy-path and stacked-PR lessons. Co-Authored-By: Claude <noreply@anthropic.com> * feat(marketing): drop the certification strip from the homepage The "ANTHROPIC CERTIFIED" trust strip (and the "Also certified: Clio Legal AI Fundamentals" line under it) belonged to the services-firm framing. On the reframed page it reads as personal credentials on what is now a product homepage, and it sat between the hero and the argument it leads into. Removes the section and deletes components/anthropic-certs.tsx — its own header said it served the homepage and the consulting page, and the consulting page has since split out, so nothing else referenced it. Git history keeps it if it wants a home on the services site. Verified: rendered homepage contains no ANTHROPIC CERTIFIED / Clio / cert-pill markers; hero, wallet card, and the "Why authorization comes first" section all intact. Co-Authored-By: Claude <noreply@anthropic.com> * feat(marketing): getsanction.com is the authorization product only The Moral Intention Analyst has its own space now, so it comes off this site entirely — one product, one story. - deletes app/moral-intention/ (page + reveal component) - homepage: drops the /moral-intention footer link - /about: removes the "Moral Intention" nav and footer links, the MIA sentence from the thesis paragraph, MIA from the metadata description and from Eric's bio, and Dr. A.C. Ping's team card together with the ac-ping photo placeholder — the ethics authorship belongs with MIA wherever MIA now lives, not on the authorization product's site No references remain anywhere in app/, components/, lib/ or public/. /moral-intention now 404s; if the new home has a public URL a permanent redirect there would be better than a dead end for anything already linking to it. Verified: /, /about, /platform, /blog all 200 and carry no MIA, A.C. Ping, acping, ethics-framework or brain-trust residue. tsc 0, eslint 0, 1189 tests passing. Co-Authored-By: Claude <noreply@anthropic.com> * feat(marketing): redirect /moral-intention to its own site moralintention.com is live, so the old page becomes a permanent cross-origin redirect rather than a 404 — anything already linking to it lands on the real thing, and the link equity follows. Sub-paths are preserved too (/moral-intention/:path* -> the same path on moralintention.com), so any deeper link that ever existed still resolves. Verified locally: /moral-intention -> 308 https://moralintention.com/, /moral-intention/analyzer -> 308 https://moralintention.com/analyzer, and /consulting's existing redirect plus /, /about, /platform, /blog are all unaffected. Note the destination itself could not be fetched from this sandbox — its proxy 403s that host — but DNS resolves and the domain is confirmed live by its owner. Co-Authored-By: Claude <noreply@anthropic.com> * feat(brand): lockup carries the category, the hero carries the moment The wordmark alone says nothing about what Sanction does — and to finance readers the name reads as OFAC first. The nav lockup now carries a descriptor: "Sanction | AGENT AUTHORIZATION", inline right with a hairline divider, on the homepage, /about and /platform. That created a second problem worth fixing in the same pass: the hero eyebrow said "Spend governance for autonomous systems" thirty pixels below it, so the page opened with two competing descriptions of the same company. The mark now owns the category and the eyebrow owns the moment — "Runaway agent spend, stopped" — which also sets up the headline instead of restating it. Mechanics: .sn-lockup-tag drops out below 1100px rather than wrapping, where the nav needs its width for links. The tagline is aria-hidden because the wordmark's alt already announces the brand — a screen reader should hear "Sanction", not "Sanction Agent authorization". Render-checked at 1440 / 1180 / 1040 / 640: visible above the breakpoint, gone below it, no wrap at any width. eslint 0 errors, 1189 tests passing. Co-Authored-By: Claude <noreply@anthropic.com> * brand: outlined lockup SVGs + social cards with the descriptor baked in The root site had no Open Graph image at all — layout.tsx declared twitter card "summary_large_image" with nothing to fill it, so a link to getsanction.com rendered a large-image card with an empty image well. Adds: - public/brand/sanction-lockup-{green,white}.svg — the wordmark plus the "Agent authorization" descriptor as VECTOR OUTLINES, not <text>. Off-site consumers (social cards, npm/registry READMEs, decks) do not load our webfonts, so a <text> tagline would fall back to a system font and the lockup would drift. Verified: zero <text> elements, renders identically with no webfont loaded, legible down to 16px on light and dark. - app/opengraph-image.png + app/twitter-image.png (1200x630) using that lockup, with matching .alt.txt files. Verified against a production build: og:image / twitter:image / og:image:alt all emit, images serve 200 at the right dimensions. * chore(sprint): /fresh-eyes skill + Codex cold-start export + branch audit Cleanup sprint before handing the repo to a Codex pass. New skill `/fresh-eyes` — the inverse of `/INPUT`. Where INPUT ingests outside material INTO the project, fresh-eyes exports verified state OUT to a reader with no context, then stares at the repo the way that reader will. Two jobs, and the second is the one people skip: the export, and naming what our own fluency hides. Every claim must be verified live in the run that writes it — a handoff is read by someone who cannot check your work, so a stale claim is not caught, it is acted on. Contract is a blind-spot ledger: VERIFIED / LANDMINE / ASSUMED / UNOWNED / DECIDE / DRIFT. First run of it produced docs/handoff/CODEX-SPRINT-2026-08-23.md, and turned up one thing that matters more than the sprint: PRODUCTION IS A MERGE BEHIND MAIN. PR #259 merged at 19:23 UTC and produced no Vercel build at all. Last production deploy is #258, from the previous day. Live site confirms it — /opengraph-image.png 404s and /moral-intention still 200s instead of redirecting. Main auto-deployed three times the day before, so this is an anomaly, not configuration. Needs a human redeploy of main@5dc4c9c. Also verified and recorded: - Slack OAuth is complete end to end (SLACK-1 slice 2) — install path, interactive approve/deny, SlackInstall under RLS, fail-closed 503 when the signing secret is unset, five test files. Documented in the export. - Seven remote branches are fully absorbed into main. Each verified by locating its PR's squash commit, and for fix/hero-noncustodial (whose #256 was stranded and re-landed via #257) by content comparison. The delete command and every branch tip are recorded so it is reversible — the sandbox classifier denies the deletion, so it is queued for Eric. - Five remaining branches carry real unmerged work and are listed with what each holds, so nobody prunes them by mistake. AGENTS.md gains a dated note: a merge to main is not a deploy. Close an arc by checking production, not the merge. Gate: tsc 0, eslint 0, 1189 tests passing. --------- Co-authored-by: Claude <noreply@anthropic.com>
* feat(engine): COND-1 — rules that read the clock and the meter Context-conditional tool rules, the second approved engine arc. A tool rule can carry a `when` — a CLOSED vocabulary, exactly one predicate per rule, restrictive effects only (an allow is never conditional, so allow-list semantics never depend on a clock or a counter). No DSL, no expressions: composition is more rules, which stays simulable and explainable. Two predicates ship: - outside_hours_utc: [start, end) UTC, wrapping midnight — "deploys escalate outside business hours". - after_model_calls_today: N — the runaway breaker: once the agent's persisted model calls pass N today, block (or escalate) matching tools. One honest correction from the design doc, recorded there: 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 runaway loops this exists to stop. Determinism (ADR-0009): signals are captured ONCE per request by the enforcement shell — the UTC hour, and the token-log count fetched ONLY when some layer actually conditions on it — passed into the pure ladder, and persisted in the decision's evidence. Replay reads the stored snapshot, never the live clock. A rule whose signal is absent from a context is deterministically inactive, so pre-COND rows replay unchanged. Precedence: block → conditional block → allow-list → escalate → conditional escalate. Conditions ride the INHERIT-1 layers, so an org-level breaker binds every team below; parity on the AuthZEN PDP with the same signal contract. New Policy.toolConditions Json column (migration included), validated in lib/policy.ts (one predicate, hour bounds, start != end), writable through the existing PATCH /wallets/policy and console actions via policyInputSchema. OpenAPI documents the shape. Console: tool-posture card counts conditionals; the inherited card names them. Simulate boundary, stated not hidden: the tool ladder predates SIM-1's simulatable set; candidate tool_conditions echo back in ignored_fields. Verified: 13 new unit tests (window boundaries + midnight wrap, threshold edges with counts in the reason, absent-signal inactivity, precedence, prefix-glob), 4 route tests (threshold deny with signals persisted in evidence; the counter queried only when conditioned on; a parent's conditional rule binding the child), and both predicates live-fired in both directions against the seeded org — including catching and redoing a false positive where an unconditional escalate masked the conditional one. Policy page render-checked. Truth surfaces: TRACEABILITY COND-1 row + suite map, DOMAIN, roadmap Now, changelog, design doc stamped with the correction, backlog checked off. Gate: tsc clean, eslint 0 errors, 1130 passing. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Eb6zTfLyE42dYS8tgWfvn * feat(broker): BROKER-1 — the MCP broker intercepts tools/call /mcp/broker/<upstream> fronts registered MCP servers: every tools/call is authorized through the tool ladder (inheritance + conditions included) before it forwards; refusals come back as MCP isError results the model can read and replan on; escalations name the request_id and the _meta["sanction/grant_id"] retry path, and a redeemed grant is stripped before forwarding. Batched requests containing tools/call are refused — fail closed, never around. For brokered traffic, "a hijacked agent cannot act" is now enforcement, not cooperation. - lib/broker.ts: upstream registry (SEC-1-vaulted under mcp:<name>), request classification, forwarding with fresh outbound headers — the agent's Sanction key never reaches an upstream, and vaulted upstream credentials never reach the agent. - app/mcp/broker/[upstream]/route.ts: authorization runs as an in-process call to the /authorize/tool route handler — one enforcement shell by construction, no drift between broker and REST decisions. - app/api/v1/broker/upstreams: owner-plane register/list/revoke. - Found and fixed shipped bug: the gateway's provider-key vault read ran outside withTenant, so FORCE RLS silently returned null and connected providers 401'd PROVIDER_NOT_CONNECTED. Now tenant-scoped (and the gateway test mock now exercises withTenant). - Honesty flip: Wallet Card now says enforcement cooperative+broker, interception gateway+mcp-broker, with the scope stated plainly — traffic that bypasses the broker is not governed. - Version literals: walletCard/mcp.json/server.json now 0.8.0 (walletCard reads MCP_SERVER_VERSION instead of a hardcoded string). - Truth surfaces: README, roadmap, changelog, DOMAIN, AGENT-WALLET, TRACEABILITY, OpenAPI, backlog annotation. Tests: 17 new (broker classification/refusal + route: blocked never forwards, escalation carries the grant path, allowed forwards with upstream auth only, grant redeemed+stripped, batch refused). Full suite 1147 passing. Co-Authored-By: Claude <noreply@anthropic.com> * docs(plans): stablecoin rails — design position + backlog capture Eric's direction: agents' authorized spend settles in regulated stablecoins (USDC most likely), not card-rail USD — build the path. Position: stablecoin settlement is irreversible, so pre-transaction authorization becomes the only control point; the rail shift makes the engine more necessary. Slices STABLE-0..3 (settlement metadata → x402 spend gate → wallet-provider co-signer → onchain mirror), custody explicitly rejected. Market state verified 2026-08-22 (x402 under the Linux Foundation w/ Stripe/Visa/Mastercard, AP2 v0.2 mandates, GENIUS Act). Co-Authored-By: Claude <noreply@anthropic.com> * docs(plans): CLARITY Act — Section 604 makes veto-only the legal architecture Market-structure bill (House-passed, Senate cloture 2026-09-15): its Section 604 codifies that non-custodial, non-controlling developers are not money transmitters. Adopt the design constraint now: Sanction's co-signing (STABLE-2/3) is veto-only — it can refuse a transfer the wallet initiates, never initiate or effectuate one. Co-Authored-By: Claude <noreply@anthropic.com> * docs(plans): monetization + distribution — five-discipline synthesis Companion to stablecoin-rails.md, from a UX / financial / growth / brand / competitive research fan-out. Position: if Sanction meters, the unit is the decision (bps on spend rejected — wrong economics, wrong optics); Zapier-style bundles; self-serve tier payable via x402/USDC itself. Trust surfaces (fee meter, operator-set fee cap, per-decision receipts, grace mode) precede any invoice. Distribution ranked: MCP registries, /govern pages, Slack App Directory, x402/AP2 ecosystem listing (no policy layer listed there today), n8n before Zapier. First slices MONO-0 / DIST-0. Co-Authored-By: Claude <noreply@anthropic.com> * feat(engine): STABLE-0 rail-aware ledger + MONO-0 decision meter STABLE-0 (docs/plans/stablecoin-rails.md): POST /v1/authorize accepts optional settlement {rail, asset, network} — a closed vocabulary (lib/settlement.ts; a chain network is refused on a non-x402 rail so evidence cannot lie). Inert to the decision like attribution tags, rides detailsJson, surfaces in the audit feed and CSV, documented in OpenAPI. Stablecoin settlement is irreversible, so pre-transaction authorization is the only control point — the ledger is rail-aware before any rail-specific enforcement exists. MONO-0 (docs/plans/monetization-and-distribution.md): per-wallet, per-UTC-month WalletDecisionCounter incremented once per FRESH engine decision across all four authorize shells (approve/escalate/deny alike, observe included). Idempotent replays, grant redemptions, simulate runs, and frozen short-circuits never count; increments run via after() and swallow errors — metering can never fail or slow a decision. Exposed as month.decisions on /v1/wallets/stats and a Decisions (month) card on the spend console. Instrumentation only: no fee attaches — the unit is measured before it is ever priced. Found and fixed while wiring: the provision route caught a subtree-cap breach INSIDE its transaction, committing the ancestor counter increments the reservation loop had already written — phantom pool spend on a denied provision (the same defect the spend route's D1 fix removed). It now lets the transaction roll back and persists the denial outside, evidence attached, mirroring the spend route. Tests: 14 new (closed-vocab settlement accept/reject, persistence, meter counts fresh approve+deny once, replay/simulate never count, meter failure swallowed, month key UTC, D1-parity provision regression). Full suite 1161 passing. Live-fired: x402/usdc/base persisted on an approved row, off-vocab rail 400s, counter=2 after one spend + one allowed tool call, console card render-checked. Co-Authored-By: Claude <noreply@anthropic.com> * docs: dated model note (Pro $20 metered middle) + GTM arc backlog entry Eric's call 2026-08-22: the model gains a metered middle — Pro $20/mo, metered in decisions, early access only until real billing ships (no checkout; trust surfaces precede any invoice). Free and enterprise framing unchanged. GTM launch arc captured in the backlog. Co-Authored-By: Claude <noreply@anthropic.com> * feat(site): blog — first post: the rail is changing, the decision is the unit New /blog (index + slug pages, changelog conventions: typed array in lib/blog.ts, shared Markdown rendering, static params + per-post metadata). Post #1 announces the August arc honestly: broker interception, the settlement-aware ledger, the decision meter, the GENIUS/CLARITY ground and the veto-only architecture, with the x402 spend gate and Pro tier explicitly marked next-not-shipped. Co-Authored-By: Claude <noreply@anthropic.com> * feat(site): platform goes stablecoin-era — rails section + Pro pricing New #rails section under the canonical line ("Sanction authorizes the spend; any rail settles it"): the irreversibility argument, the settlement-aware ledger marked live, the x402 spend gate marked next (never overclaimed), and the non-custodial/veto-only regulatory posture (GENIUS as instrument law, CLARITY as pending). Pricing becomes three cards: Free (unchanged) / Pro $20/mo early access (metered in decisions, lead-capture CTA, no checkout) / Enterprise Agreement. Disambiguation line: sanction (v.) — to give official authorization. Co-Authored-By: Claude <noreply@anthropic.com> * docs(gtm): launch playbook + integration use-case catalog as HTML Two self-contained HTML documents under docs/gtm/: the 14-day launch playbook (positioning, sequence, the CLARITY-vote play, content engine, Pro funnel, metrics) and "Where Sanction sits" — seven integration architectures with pre-rendered sequence diagrams (static SVG, no runtime dependencies; open in any browser). Co-Authored-By: Claude <noreply@anthropic.com> * docs: relocate GTM documents to the private strategy repo Strategy/GTM material does not belong in the public repo (same call as the 2026-07-06 move) — the launch playbook and use-case catalog now live in ericlovold/sanction-strategy under docs/gtm/. Co-Authored-By: Claude <noreply@anthropic.com> * feat(engine): STABLE-1 — the x402 spend gate, authorized before the wallet signs An x402 402 is a demand for money, and stablecoin settlement is irreversible: the moment between receiving a payment challenge and signing it is the only control point there is. STABLE-1 takes that moment. Two surfaces, one shell (both call POST /v1/authorize in-process, so a quote can never drift from ordinary spend): - Cooperative: POST /v1/authorize/quote — post the 402 body you just received; Sanction prices it and runs the same ladder (budgets, escalation, grants, cascade caps, evidence, decision meter), with STABLE-0 settlement metadata derived from the quote itself. - Intercepting: the MCP broker gates upstream 402s and, on anything but an approval, WITHHOLDS the challenge. The refusal type structurally cannot carry payTo/maxAmountRequired — an agent cannot sign payment requirements it never received. Two rules keep pricing honest and deterministic: - No oracle, no FX. A quote is priced only when its asset is a USD-pegged stablecoin with known decimals (native USDC on Base; address + 6 decimals verified against Circle). Anything else denies as QUOTE_NOT_PRICEABLE rather than being guessed — a decision must never depend on a rate looked up at decision time (ADR-0009). - Worst case wins. A challenge offering several ways to pay is authorized at its most expensive option, and one unpriceable option poisons the whole challenge: the client picks, and we cannot know which. Still non-custodial and veto-only — no keys, no signing, refusal only, which is the shape CLARITY §604's non-controlling test describes. Tests: 28 new (pricing, worst-case selection, poisoned challenges, the gate's withholding contract, quote route, broker interception). Full suite 1189 passing. Live-fired against Postgres: $0.05 and $15 approved, $900 denied PER_TXN_LIMIT, a multi-option challenge authorized at its worst case, every row carrying settlement metadata. Truth surfaces: /platform now says the gate is live (was "next"), changelog, roadmap, TRACEABILITY, OpenAPI, README, backlog, design doc. Co-Authored-By: Claude <noreply@anthropic.com> * feat(brand): favicon and app icons from the new wordmark; re-land the hero fix Icons were still the old shield-and-checkmark mark. The whole icon set is now generated from the $ glyph in Paul's wordmark (the three green paths of sanction-wordmark-green.svg, bounds measured with getBBox rather than eyeballed), on the brand's own pine ground with the wordmark's signal green: app/icon.svg 512, rounded square (browser tab) app/favicon.ico 16 + 32, PNG-in-ICO app/apple-icon.png 180 public/icons/icon-192|512.png PWA "any" public/icons/icon-maskable-512.png glyph inside the 80% safe zone manifest theme/background move to pine so the PWA splash frames the icon in the same dark it uses. Also re-lands the hero fix from #256, which was stranded: it merged into the marketing branch after that branch had already been squashed into main, so main still read "AVAILABLE $2,500.00". Sanction is non-custodial and veto-only, so the card states the limit — "DAILY CAP / ENFORCED". Verified: icon legible at 16px on light and dark tab strips; /, /icon.svg, /favicon.ico and /apple-icon.png all 200 with the right content types. Caught in render-check and fixed: Next decodes favicon.ico and requires RGBA, and an RGB one 500s every page — noted in AGENTS.md along with the production-deploy-path and stacked-PR lessons. Co-Authored-By: Claude <noreply@anthropic.com> * feat(marketing): drop the certification strip from the homepage The "ANTHROPIC CERTIFIED" trust strip (and the "Also certified: Clio Legal AI Fundamentals" line under it) belonged to the services-firm framing. On the reframed page it reads as personal credentials on what is now a product homepage, and it sat between the hero and the argument it leads into. Removes the section and deletes components/anthropic-certs.tsx — its own header said it served the homepage and the consulting page, and the consulting page has since split out, so nothing else referenced it. Git history keeps it if it wants a home on the services site. Verified: rendered homepage contains no ANTHROPIC CERTIFIED / Clio / cert-pill markers; hero, wallet card, and the "Why authorization comes first" section all intact. Co-Authored-By: Claude <noreply@anthropic.com> * feat(marketing): getsanction.com is the authorization product only The Moral Intention Analyst has its own space now, so it comes off this site entirely — one product, one story. - deletes app/moral-intention/ (page + reveal component) - homepage: drops the /moral-intention footer link - /about: removes the "Moral Intention" nav and footer links, the MIA sentence from the thesis paragraph, MIA from the metadata description and from Eric's bio, and Dr. A.C. Ping's team card together with the ac-ping photo placeholder — the ethics authorship belongs with MIA wherever MIA now lives, not on the authorization product's site No references remain anywhere in app/, components/, lib/ or public/. /moral-intention now 404s; if the new home has a public URL a permanent redirect there would be better than a dead end for anything already linking to it. Verified: /, /about, /platform, /blog all 200 and carry no MIA, A.C. Ping, acping, ethics-framework or brain-trust residue. tsc 0, eslint 0, 1189 tests passing. Co-Authored-By: Claude <noreply@anthropic.com> * feat(marketing): redirect /moral-intention to its own site moralintention.com is live, so the old page becomes a permanent cross-origin redirect rather than a 404 — anything already linking to it lands on the real thing, and the link equity follows. Sub-paths are preserved too (/moral-intention/:path* -> the same path on moralintention.com), so any deeper link that ever existed still resolves. Verified locally: /moral-intention -> 308 https://moralintention.com/, /moral-intention/analyzer -> 308 https://moralintention.com/analyzer, and /consulting's existing redirect plus /, /about, /platform, /blog are all unaffected. Note the destination itself could not be fetched from this sandbox — its proxy 403s that host — but DNS resolves and the domain is confirmed live by its owner. Co-Authored-By: Claude <noreply@anthropic.com> * feat(brand): lockup carries the category, the hero carries the moment The wordmark alone says nothing about what Sanction does — and to finance readers the name reads as OFAC first. The nav lockup now carries a descriptor: "Sanction | AGENT AUTHORIZATION", inline right with a hairline divider, on the homepage, /about and /platform. That created a second problem worth fixing in the same pass: the hero eyebrow said "Spend governance for autonomous systems" thirty pixels below it, so the page opened with two competing descriptions of the same company. The mark now owns the category and the eyebrow owns the moment — "Runaway agent spend, stopped" — which also sets up the headline instead of restating it. Mechanics: .sn-lockup-tag drops out below 1100px rather than wrapping, where the nav needs its width for links. The tagline is aria-hidden because the wordmark's alt already announces the brand — a screen reader should hear "Sanction", not "Sanction Agent authorization". Render-checked at 1440 / 1180 / 1040 / 640: visible above the breakpoint, gone below it, no wrap at any width. eslint 0 errors, 1189 tests passing. Co-Authored-By: Claude <noreply@anthropic.com> * brand: outlined lockup SVGs + social cards with the descriptor baked in The root site had no Open Graph image at all — layout.tsx declared twitter card "summary_large_image" with nothing to fill it, so a link to getsanction.com rendered a large-image card with an empty image well. Adds: - public/brand/sanction-lockup-{green,white}.svg — the wordmark plus the "Agent authorization" descriptor as VECTOR OUTLINES, not <text>. Off-site consumers (social cards, npm/registry READMEs, decks) do not load our webfonts, so a <text> tagline would fall back to a system font and the lockup would drift. Verified: zero <text> elements, renders identically with no webfont loaded, legible down to 16px on light and dark. - app/opengraph-image.png + app/twitter-image.png (1200x630) using that lockup, with matching .alt.txt files. Verified against a production build: og:image / twitter:image / og:image:alt all emit, images serve 200 at the right dimensions. * chore(sprint): /fresh-eyes skill + Codex cold-start export + branch audit Cleanup sprint before handing the repo to a Codex pass. New skill `/fresh-eyes` — the inverse of `/INPUT`. Where INPUT ingests outside material INTO the project, fresh-eyes exports verified state OUT to a reader with no context, then stares at the repo the way that reader will. Two jobs, and the second is the one people skip: the export, and naming what our own fluency hides. Every claim must be verified live in the run that writes it — a handoff is read by someone who cannot check your work, so a stale claim is not caught, it is acted on. Contract is a blind-spot ledger: VERIFIED / LANDMINE / ASSUMED / UNOWNED / DECIDE / DRIFT. First run of it produced docs/handoff/CODEX-SPRINT-2026-08-23.md, and turned up one thing that matters more than the sprint: PRODUCTION IS A MERGE BEHIND MAIN. PR #259 merged at 19:23 UTC and produced no Vercel build at all. Last production deploy is #258, from the previous day. Live site confirms it — /opengraph-image.png 404s and /moral-intention still 200s instead of redirecting. Main auto-deployed three times the day before, so this is an anomaly, not configuration. Needs a human redeploy of main@5dc4c9c. Also verified and recorded: - Slack OAuth is complete end to end (SLACK-1 slice 2) — install path, interactive approve/deny, SlackInstall under RLS, fail-closed 503 when the signing secret is unset, five test files. Documented in the export. - Seven remote branches are fully absorbed into main. Each verified by locating its PR's squash commit, and for fix/hero-noncustodial (whose #256 was stranded and re-landed via #257) by content comparison. The delete command and every branch tip are recorded so it is reversible — the sandbox classifier denies the deletion, so it is queued for Eric. - Five remaining branches carry real unmerged work and are listed with what each holds, so nobody prunes them by mistake. AGENTS.md gains a dated note: a merge to main is not a deploy. Close an arc by checking production, not the merge. Gate: tsc 0, eslint 0, 1189 tests passing. * fix(ci): rebuild the MCP bundle so main is green again CI has been red on main for 20 consecutive runs across six days — since the v0.8.0 release (#244, 2026-08-19). Every PR since, mine included, merged straight through it. The cause is one line. `lib/mcpServer.ts` exports MCP_SERVER_VERSION, esbuild INLINES it into packages/sanction-mcp/mcp-server.js, and #244 bumped the source without rebuilding the bundle. CI regenerates the bundle and runs `git diff --exit-code packages/` as its LAST step, so every run went green through 1,191 tests and 90% coverage and then failed on: -var MCP_SERVER_VERSION = "0.7.0"; +var MCP_SERVER_VERSION = "0.8.0"; which reads like a flake at the end of a green run, and got treated as one. The published npm artifact is fine — the workflow builds fresh. What was wrong was the artifact checked into the repo. The second cost is the one worth fixing properly: a permanently-red main means nobody can distinguish a NEW failure from the standing one. The merge gate was decorative for six days. So this does three things, not one: 1. Rebuilds the bundle. `npm run build:mcp && git diff --exit-code packages/` now passes locally, which is CI's exact check. 2. /cut-release gains a step: regenerate every artifact that EMBEDS the version, not just the ones that declare it, with the reason and the verify command. The ritual is why this recurred; the ritual is where it gets fixed. 3. AGENTS.md gains a dated note: a red main is an incident, not a backlog item. Never merge into a red main assuming the red is "the known one" without reading the log. Also corrects the version-literal list in the 2026-08-23 handoff export, which omitted the generated bundle — the exact omission that caused this — and wrongly listed lib/walletCard.ts, which derives the constant rather than restating it. Gate: tsc 0, eslint 0, 1191 tests passing. --------- Co-authored-by: Claude <noreply@anthropic.com>
* feat(engine): COND-1 — rules that read the clock and the meter Context-conditional tool rules, the second approved engine arc. A tool rule can carry a `when` — a CLOSED vocabulary, exactly one predicate per rule, restrictive effects only (an allow is never conditional, so allow-list semantics never depend on a clock or a counter). No DSL, no expressions: composition is more rules, which stays simulable and explainable. Two predicates ship: - outside_hours_utc: [start, end) UTC, wrapping midnight — "deploys escalate outside business hours". - after_model_calls_today: N — the runaway breaker: once the agent's persisted model calls pass N today, block (or escalate) matching tools. One honest correction from the design doc, recorded there: 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 runaway loops this exists to stop. Determinism (ADR-0009): signals are captured ONCE per request by the enforcement shell — the UTC hour, and the token-log count fetched ONLY when some layer actually conditions on it — passed into the pure ladder, and persisted in the decision's evidence. Replay reads the stored snapshot, never the live clock. A rule whose signal is absent from a context is deterministically inactive, so pre-COND rows replay unchanged. Precedence: block → conditional block → allow-list → escalate → conditional escalate. Conditions ride the INHERIT-1 layers, so an org-level breaker binds every team below; parity on the AuthZEN PDP with the same signal contract. New Policy.toolConditions Json column (migration included), validated in lib/policy.ts (one predicate, hour bounds, start != end), writable through the existing PATCH /wallets/policy and console actions via policyInputSchema. OpenAPI documents the shape. Console: tool-posture card counts conditionals; the inherited card names them. Simulate boundary, stated not hidden: the tool ladder predates SIM-1's simulatable set; candidate tool_conditions echo back in ignored_fields. Verified: 13 new unit tests (window boundaries + midnight wrap, threshold edges with counts in the reason, absent-signal inactivity, precedence, prefix-glob), 4 route tests (threshold deny with signals persisted in evidence; the counter queried only when conditioned on; a parent's conditional rule binding the child), and both predicates live-fired in both directions against the seeded org — including catching and redoing a false positive where an unconditional escalate masked the conditional one. Policy page render-checked. Truth surfaces: TRACEABILITY COND-1 row + suite map, DOMAIN, roadmap Now, changelog, design doc stamped with the correction, backlog checked off. Gate: tsc clean, eslint 0 errors, 1130 passing. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Eb6zTfLyE42dYS8tgWfvn * feat(broker): BROKER-1 — the MCP broker intercepts tools/call /mcp/broker/<upstream> fronts registered MCP servers: every tools/call is authorized through the tool ladder (inheritance + conditions included) before it forwards; refusals come back as MCP isError results the model can read and replan on; escalations name the request_id and the _meta["sanction/grant_id"] retry path, and a redeemed grant is stripped before forwarding. Batched requests containing tools/call are refused — fail closed, never around. For brokered traffic, "a hijacked agent cannot act" is now enforcement, not cooperation. - lib/broker.ts: upstream registry (SEC-1-vaulted under mcp:<name>), request classification, forwarding with fresh outbound headers — the agent's Sanction key never reaches an upstream, and vaulted upstream credentials never reach the agent. - app/mcp/broker/[upstream]/route.ts: authorization runs as an in-process call to the /authorize/tool route handler — one enforcement shell by construction, no drift between broker and REST decisions. - app/api/v1/broker/upstreams: owner-plane register/list/revoke. - Found and fixed shipped bug: the gateway's provider-key vault read ran outside withTenant, so FORCE RLS silently returned null and connected providers 401'd PROVIDER_NOT_CONNECTED. Now tenant-scoped (and the gateway test mock now exercises withTenant). - Honesty flip: Wallet Card now says enforcement cooperative+broker, interception gateway+mcp-broker, with the scope stated plainly — traffic that bypasses the broker is not governed. - Version literals: walletCard/mcp.json/server.json now 0.8.0 (walletCard reads MCP_SERVER_VERSION instead of a hardcoded string). - Truth surfaces: README, roadmap, changelog, DOMAIN, AGENT-WALLET, TRACEABILITY, OpenAPI, backlog annotation. Tests: 17 new (broker classification/refusal + route: blocked never forwards, escalation carries the grant path, allowed forwards with upstream auth only, grant redeemed+stripped, batch refused). Full suite 1147 passing. Co-Authored-By: Claude <noreply@anthropic.com> * docs(plans): stablecoin rails — design position + backlog capture Eric's direction: agents' authorized spend settles in regulated stablecoins (USDC most likely), not card-rail USD — build the path. Position: stablecoin settlement is irreversible, so pre-transaction authorization becomes the only control point; the rail shift makes the engine more necessary. Slices STABLE-0..3 (settlement metadata → x402 spend gate → wallet-provider co-signer → onchain mirror), custody explicitly rejected. Market state verified 2026-08-22 (x402 under the Linux Foundation w/ Stripe/Visa/Mastercard, AP2 v0.2 mandates, GENIUS Act). Co-Authored-By: Claude <noreply@anthropic.com> * docs(plans): CLARITY Act — Section 604 makes veto-only the legal architecture Market-structure bill (House-passed, Senate cloture 2026-09-15): its Section 604 codifies that non-custodial, non-controlling developers are not money transmitters. Adopt the design constraint now: Sanction's co-signing (STABLE-2/3) is veto-only — it can refuse a transfer the wallet initiates, never initiate or effectuate one. Co-Authored-By: Claude <noreply@anthropic.com> * docs(plans): monetization + distribution — five-discipline synthesis Companion to stablecoin-rails.md, from a UX / financial / growth / brand / competitive research fan-out. Position: if Sanction meters, the unit is the decision (bps on spend rejected — wrong economics, wrong optics); Zapier-style bundles; self-serve tier payable via x402/USDC itself. Trust surfaces (fee meter, operator-set fee cap, per-decision receipts, grace mode) precede any invoice. Distribution ranked: MCP registries, /govern pages, Slack App Directory, x402/AP2 ecosystem listing (no policy layer listed there today), n8n before Zapier. First slices MONO-0 / DIST-0. Co-Authored-By: Claude <noreply@anthropic.com> * feat(engine): STABLE-0 rail-aware ledger + MONO-0 decision meter STABLE-0 (docs/plans/stablecoin-rails.md): POST /v1/authorize accepts optional settlement {rail, asset, network} — a closed vocabulary (lib/settlement.ts; a chain network is refused on a non-x402 rail so evidence cannot lie). Inert to the decision like attribution tags, rides detailsJson, surfaces in the audit feed and CSV, documented in OpenAPI. Stablecoin settlement is irreversible, so pre-transaction authorization is the only control point — the ledger is rail-aware before any rail-specific enforcement exists. MONO-0 (docs/plans/monetization-and-distribution.md): per-wallet, per-UTC-month WalletDecisionCounter incremented once per FRESH engine decision across all four authorize shells (approve/escalate/deny alike, observe included). Idempotent replays, grant redemptions, simulate runs, and frozen short-circuits never count; increments run via after() and swallow errors — metering can never fail or slow a decision. Exposed as month.decisions on /v1/wallets/stats and a Decisions (month) card on the spend console. Instrumentation only: no fee attaches — the unit is measured before it is ever priced. Found and fixed while wiring: the provision route caught a subtree-cap breach INSIDE its transaction, committing the ancestor counter increments the reservation loop had already written — phantom pool spend on a denied provision (the same defect the spend route's D1 fix removed). It now lets the transaction roll back and persists the denial outside, evidence attached, mirroring the spend route. Tests: 14 new (closed-vocab settlement accept/reject, persistence, meter counts fresh approve+deny once, replay/simulate never count, meter failure swallowed, month key UTC, D1-parity provision regression). Full suite 1161 passing. Live-fired: x402/usdc/base persisted on an approved row, off-vocab rail 400s, counter=2 after one spend + one allowed tool call, console card render-checked. Co-Authored-By: Claude <noreply@anthropic.com> * docs: dated model note (Pro $20 metered middle) + GTM arc backlog entry Eric's call 2026-08-22: the model gains a metered middle — Pro $20/mo, metered in decisions, early access only until real billing ships (no checkout; trust surfaces precede any invoice). Free and enterprise framing unchanged. GTM launch arc captured in the backlog. Co-Authored-By: Claude <noreply@anthropic.com> * feat(site): blog — first post: the rail is changing, the decision is the unit New /blog (index + slug pages, changelog conventions: typed array in lib/blog.ts, shared Markdown rendering, static params + per-post metadata). Post #1 announces the August arc honestly: broker interception, the settlement-aware ledger, the decision meter, the GENIUS/CLARITY ground and the veto-only architecture, with the x402 spend gate and Pro tier explicitly marked next-not-shipped. Co-Authored-By: Claude <noreply@anthropic.com> * feat(site): platform goes stablecoin-era — rails section + Pro pricing New #rails section under the canonical line ("Sanction authorizes the spend; any rail settles it"): the irreversibility argument, the settlement-aware ledger marked live, the x402 spend gate marked next (never overclaimed), and the non-custodial/veto-only regulatory posture (GENIUS as instrument law, CLARITY as pending). Pricing becomes three cards: Free (unchanged) / Pro $20/mo early access (metered in decisions, lead-capture CTA, no checkout) / Enterprise Agreement. Disambiguation line: sanction (v.) — to give official authorization. Co-Authored-By: Claude <noreply@anthropic.com> * docs(gtm): launch playbook + integration use-case catalog as HTML Two self-contained HTML documents under docs/gtm/: the 14-day launch playbook (positioning, sequence, the CLARITY-vote play, content engine, Pro funnel, metrics) and "Where Sanction sits" — seven integration architectures with pre-rendered sequence diagrams (static SVG, no runtime dependencies; open in any browser). Co-Authored-By: Claude <noreply@anthropic.com> * docs: relocate GTM documents to the private strategy repo Strategy/GTM material does not belong in the public repo (same call as the 2026-07-06 move) — the launch playbook and use-case catalog now live in ericlovold/sanction-strategy under docs/gtm/. Co-Authored-By: Claude <noreply@anthropic.com> * feat(engine): STABLE-1 — the x402 spend gate, authorized before the wallet signs An x402 402 is a demand for money, and stablecoin settlement is irreversible: the moment between receiving a payment challenge and signing it is the only control point there is. STABLE-1 takes that moment. Two surfaces, one shell (both call POST /v1/authorize in-process, so a quote can never drift from ordinary spend): - Cooperative: POST /v1/authorize/quote — post the 402 body you just received; Sanction prices it and runs the same ladder (budgets, escalation, grants, cascade caps, evidence, decision meter), with STABLE-0 settlement metadata derived from the quote itself. - Intercepting: the MCP broker gates upstream 402s and, on anything but an approval, WITHHOLDS the challenge. The refusal type structurally cannot carry payTo/maxAmountRequired — an agent cannot sign payment requirements it never received. Two rules keep pricing honest and deterministic: - No oracle, no FX. A quote is priced only when its asset is a USD-pegged stablecoin with known decimals (native USDC on Base; address + 6 decimals verified against Circle). Anything else denies as QUOTE_NOT_PRICEABLE rather than being guessed — a decision must never depend on a rate looked up at decision time (ADR-0009). - Worst case wins. A challenge offering several ways to pay is authorized at its most expensive option, and one unpriceable option poisons the whole challenge: the client picks, and we cannot know which. Still non-custodial and veto-only — no keys, no signing, refusal only, which is the shape CLARITY §604's non-controlling test describes. Tests: 28 new (pricing, worst-case selection, poisoned challenges, the gate's withholding contract, quote route, broker interception). Full suite 1189 passing. Live-fired against Postgres: $0.05 and $15 approved, $900 denied PER_TXN_LIMIT, a multi-option challenge authorized at its worst case, every row carrying settlement metadata. Truth surfaces: /platform now says the gate is live (was "next"), changelog, roadmap, TRACEABILITY, OpenAPI, README, backlog, design doc. Co-Authored-By: Claude <noreply@anthropic.com> * feat(brand): favicon and app icons from the new wordmark; re-land the hero fix Icons were still the old shield-and-checkmark mark. The whole icon set is now generated from the $ glyph in Paul's wordmark (the three green paths of sanction-wordmark-green.svg, bounds measured with getBBox rather than eyeballed), on the brand's own pine ground with the wordmark's signal green: app/icon.svg 512, rounded square (browser tab) app/favicon.ico 16 + 32, PNG-in-ICO app/apple-icon.png 180 public/icons/icon-192|512.png PWA "any" public/icons/icon-maskable-512.png glyph inside the 80% safe zone manifest theme/background move to pine so the PWA splash frames the icon in the same dark it uses. Also re-lands the hero fix from #256, which was stranded: it merged into the marketing branch after that branch had already been squashed into main, so main still read "AVAILABLE $2,500.00". Sanction is non-custodial and veto-only, so the card states the limit — "DAILY CAP / ENFORCED". Verified: icon legible at 16px on light and dark tab strips; /, /icon.svg, /favicon.ico and /apple-icon.png all 200 with the right content types. Caught in render-check and fixed: Next decodes favicon.ico and requires RGBA, and an RGB one 500s every page — noted in AGENTS.md along with the production-deploy-path and stacked-PR lessons. Co-Authored-By: Claude <noreply@anthropic.com> * feat(marketing): drop the certification strip from the homepage The "ANTHROPIC CERTIFIED" trust strip (and the "Also certified: Clio Legal AI Fundamentals" line under it) belonged to the services-firm framing. On the reframed page it reads as personal credentials on what is now a product homepage, and it sat between the hero and the argument it leads into. Removes the section and deletes components/anthropic-certs.tsx — its own header said it served the homepage and the consulting page, and the consulting page has since split out, so nothing else referenced it. Git history keeps it if it wants a home on the services site. Verified: rendered homepage contains no ANTHROPIC CERTIFIED / Clio / cert-pill markers; hero, wallet card, and the "Why authorization comes first" section all intact. Co-Authored-By: Claude <noreply@anthropic.com> * feat(marketing): getsanction.com is the authorization product only The Moral Intention Analyst has its own space now, so it comes off this site entirely — one product, one story. - deletes app/moral-intention/ (page + reveal component) - homepage: drops the /moral-intention footer link - /about: removes the "Moral Intention" nav and footer links, the MIA sentence from the thesis paragraph, MIA from the metadata description and from Eric's bio, and Dr. A.C. Ping's team card together with the ac-ping photo placeholder — the ethics authorship belongs with MIA wherever MIA now lives, not on the authorization product's site No references remain anywhere in app/, components/, lib/ or public/. /moral-intention now 404s; if the new home has a public URL a permanent redirect there would be better than a dead end for anything already linking to it. Verified: /, /about, /platform, /blog all 200 and carry no MIA, A.C. Ping, acping, ethics-framework or brain-trust residue. tsc 0, eslint 0, 1189 tests passing. Co-Authored-By: Claude <noreply@anthropic.com> * feat(marketing): redirect /moral-intention to its own site moralintention.com is live, so the old page becomes a permanent cross-origin redirect rather than a 404 — anything already linking to it lands on the real thing, and the link equity follows. Sub-paths are preserved too (/moral-intention/:path* -> the same path on moralintention.com), so any deeper link that ever existed still resolves. Verified locally: /moral-intention -> 308 https://moralintention.com/, /moral-intention/analyzer -> 308 https://moralintention.com/analyzer, and /consulting's existing redirect plus /, /about, /platform, /blog are all unaffected. Note the destination itself could not be fetched from this sandbox — its proxy 403s that host — but DNS resolves and the domain is confirmed live by its owner. Co-Authored-By: Claude <noreply@anthropic.com> * feat(brand): lockup carries the category, the hero carries the moment The wordmark alone says nothing about what Sanction does — and to finance readers the name reads as OFAC first. The nav lockup now carries a descriptor: "Sanction | AGENT AUTHORIZATION", inline right with a hairline divider, on the homepage, /about and /platform. That created a second problem worth fixing in the same pass: the hero eyebrow said "Spend governance for autonomous systems" thirty pixels below it, so the page opened with two competing descriptions of the same company. The mark now owns the category and the eyebrow owns the moment — "Runaway agent spend, stopped" — which also sets up the headline instead of restating it. Mechanics: .sn-lockup-tag drops out below 1100px rather than wrapping, where the nav needs its width for links. The tagline is aria-hidden because the wordmark's alt already announces the brand — a screen reader should hear "Sanction", not "Sanction Agent authorization". Render-checked at 1440 / 1180 / 1040 / 640: visible above the breakpoint, gone below it, no wrap at any width. eslint 0 errors, 1189 tests passing. Co-Authored-By: Claude <noreply@anthropic.com> * brand: outlined lockup SVGs + social cards with the descriptor baked in The root site had no Open Graph image at all — layout.tsx declared twitter card "summary_large_image" with nothing to fill it, so a link to getsanction.com rendered a large-image card with an empty image well. Adds: - public/brand/sanction-lockup-{green,white}.svg — the wordmark plus the "Agent authorization" descriptor as VECTOR OUTLINES, not <text>. Off-site consumers (social cards, npm/registry READMEs, decks) do not load our webfonts, so a <text> tagline would fall back to a system font and the lockup would drift. Verified: zero <text> elements, renders identically with no webfont loaded, legible down to 16px on light and dark. - app/opengraph-image.png + app/twitter-image.png (1200x630) using that lockup, with matching .alt.txt files. Verified against a production build: og:image / twitter:image / og:image:alt all emit, images serve 200 at the right dimensions. * chore(sprint): /fresh-eyes skill + Codex cold-start export + branch audit Cleanup sprint before handing the repo to a Codex pass. New skill `/fresh-eyes` — the inverse of `/INPUT`. Where INPUT ingests outside material INTO the project, fresh-eyes exports verified state OUT to a reader with no context, then stares at the repo the way that reader will. Two jobs, and the second is the one people skip: the export, and naming what our own fluency hides. Every claim must be verified live in the run that writes it — a handoff is read by someone who cannot check your work, so a stale claim is not caught, it is acted on. Contract is a blind-spot ledger: VERIFIED / LANDMINE / ASSUMED / UNOWNED / DECIDE / DRIFT. First run of it produced docs/handoff/CODEX-SPRINT-2026-08-23.md, and turned up one thing that matters more than the sprint: PRODUCTION IS A MERGE BEHIND MAIN. PR #259 merged at 19:23 UTC and produced no Vercel build at all. Last production deploy is #258, from the previous day. Live site confirms it — /opengraph-image.png 404s and /moral-intention still 200s instead of redirecting. Main auto-deployed three times the day before, so this is an anomaly, not configuration. Needs a human redeploy of main@5dc4c9c. Also verified and recorded: - Slack OAuth is complete end to end (SLACK-1 slice 2) — install path, interactive approve/deny, SlackInstall under RLS, fail-closed 503 when the signing secret is unset, five test files. Documented in the export. - Seven remote branches are fully absorbed into main. Each verified by locating its PR's squash commit, and for fix/hero-noncustodial (whose #256 was stranded and re-landed via #257) by content comparison. The delete command and every branch tip are recorded so it is reversible — the sandbox classifier denies the deletion, so it is queued for Eric. - Five remaining branches carry real unmerged work and are listed with what each holds, so nobody prunes them by mistake. AGENTS.md gains a dated note: a merge to main is not a deploy. Close an arc by checking production, not the merge. Gate: tsc 0, eslint 0, 1189 tests passing. * fix(ci): rebuild the MCP bundle so main is green again CI has been red on main for 20 consecutive runs across six days — since the v0.8.0 release (#244, 2026-08-19). Every PR since, mine included, merged straight through it. The cause is one line. `lib/mcpServer.ts` exports MCP_SERVER_VERSION, esbuild INLINES it into packages/sanction-mcp/mcp-server.js, and #244 bumped the source without rebuilding the bundle. CI regenerates the bundle and runs `git diff --exit-code packages/` as its LAST step, so every run went green through 1,191 tests and 90% coverage and then failed on: -var MCP_SERVER_VERSION = "0.7.0"; +var MCP_SERVER_VERSION = "0.8.0"; which reads like a flake at the end of a green run, and got treated as one. The published npm artifact is fine — the workflow builds fresh. What was wrong was the artifact checked into the repo. The second cost is the one worth fixing properly: a permanently-red main means nobody can distinguish a NEW failure from the standing one. The merge gate was decorative for six days. So this does three things, not one: 1. Rebuilds the bundle. `npm run build:mcp && git diff --exit-code packages/` now passes locally, which is CI's exact check. 2. /cut-release gains a step: regenerate every artifact that EMBEDS the version, not just the ones that declare it, with the reason and the verify command. The ritual is why this recurred; the ritual is where it gets fixed. 3. AGENTS.md gains a dated note: a red main is an incident, not a backlog item. Never merge into a red main assuming the red is "the known one" without reading the log. Also corrects the version-literal list in the 2026-08-23 handoff export, which omitted the generated bundle — the exact omission that caused this — and wrongly listed lib/walletCard.ts, which derives the constant rather than restating it. Gate: tsc 0, eslint 0, 1191 tests passing. * fix(deps): clear all 16 advisories without downgrading Prisma The `Dependency audit (high+)` CI step is `continue-on-error: true`, so a green check on it means "did not block", not "clean". 16 advisories (12 high, 4 moderate) accumulated behind that green check, several of them request-path: Next.js SSRF in rewrites, cache confusion on bodied requests, DoS in Server Actions; undici response desync; postcss source-map path traversal. Cleared in three separable steps so any regression is attributable: 1. `npm audit fix` — 13 semver-compatible: brace-expansion, fast-uri, ip-address, js-yaml, nanoid, undici, hono, @hono/node-server, valibot, prisma, @prisma/dev and friends. 16 -> 6. 2. next 16.2.9 -> 16.3.3, which also clears postcss and sharp. This is a MINOR bump inside Next 16, not the major upgrade the audit output implies — npm says "outside the stated dependency range" only because package.json pins the version exactly, with no caret. 6 -> 3. 3. `overrides: { "deepmerge-ts": "^8.0.2" }` for the last three. Step 3 needs explaining, because npm's own suggestion here is a trap. For the deepmerge-ts stack-exhaustion advisory npm proposes `prisma@6.12.0` — a MAJOR DOWNGRADE from the installed 7.10.0, which would tear out the Prisma 7 driver adapter the entire data layer runs on. The real constraint is that @prisma/config@7.10.0 hard-pins deepmerge-ts to exactly 7.1.5, while the patched line is 8.0.2. An override fixes it and keeps Prisma 7. Result: `npm audit` reports 0 vulnerabilities, Prisma stays at 7.10.0. Verified, not assumed: - gate: tsc 0, eslint 0, 1191 tests passing - DB tests: 20/20 under CI's exact env, RLS isolation and e2e data plane included (they fail locally only because the dev role cannot CREATE ROLE) - prisma generate / validate / migrate status all load prisma.config.ts through the overridden @prisma/config — 38 migrations, schema up to date - next build clean; next start + Chromium on /, /platform, /slack: all render correctly, zero page errors (only console noise is the Vercel Analytics script, which 404s off-Vercel by design) - the regenerated MCP bundle still reports 0.8.0 and answers `initialize` over stdio; bundle-freshness check passes The bundle diff is large because esbuild inlines the upgraded transitive deps; it is committed here, per the lesson from the last PR. Adds .github/dependabot.yml — there was none, which is why this rotted unseen. Weekly, grouped, majors excluded from groups so each gets its own gate. The audit step stays non-blocking on purpose: making it blocking would let a new upstream advisory redden main with no commit behind it, which is the exact failure that made the merge gate decorative for six days. --------- Co-authored-by: Claude <noreply@anthropic.com>
* feat(engine): COND-1 — rules that read the clock and the meter Context-conditional tool rules, the second approved engine arc. A tool rule can carry a `when` — a CLOSED vocabulary, exactly one predicate per rule, restrictive effects only (an allow is never conditional, so allow-list semantics never depend on a clock or a counter). No DSL, no expressions: composition is more rules, which stays simulable and explainable. Two predicates ship: - outside_hours_utc: [start, end) UTC, wrapping midnight — "deploys escalate outside business hours". - after_model_calls_today: N — the runaway breaker: once the agent's persisted model calls pass N today, block (or escalate) matching tools. One honest correction from the design doc, recorded there: 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 runaway loops this exists to stop. Determinism (ADR-0009): signals are captured ONCE per request by the enforcement shell — the UTC hour, and the token-log count fetched ONLY when some layer actually conditions on it — passed into the pure ladder, and persisted in the decision's evidence. Replay reads the stored snapshot, never the live clock. A rule whose signal is absent from a context is deterministically inactive, so pre-COND rows replay unchanged. Precedence: block → conditional block → allow-list → escalate → conditional escalate. Conditions ride the INHERIT-1 layers, so an org-level breaker binds every team below; parity on the AuthZEN PDP with the same signal contract. New Policy.toolConditions Json column (migration included), validated in lib/policy.ts (one predicate, hour bounds, start != end), writable through the existing PATCH /wallets/policy and console actions via policyInputSchema. OpenAPI documents the shape. Console: tool-posture card counts conditionals; the inherited card names them. Simulate boundary, stated not hidden: the tool ladder predates SIM-1's simulatable set; candidate tool_conditions echo back in ignored_fields. Verified: 13 new unit tests (window boundaries + midnight wrap, threshold edges with counts in the reason, absent-signal inactivity, precedence, prefix-glob), 4 route tests (threshold deny with signals persisted in evidence; the counter queried only when conditioned on; a parent's conditional rule binding the child), and both predicates live-fired in both directions against the seeded org — including catching and redoing a false positive where an unconditional escalate masked the conditional one. Policy page render-checked. Truth surfaces: TRACEABILITY COND-1 row + suite map, DOMAIN, roadmap Now, changelog, design doc stamped with the correction, backlog checked off. Gate: tsc clean, eslint 0 errors, 1130 passing. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Eb6zTfLyE42dYS8tgWfvn * feat(broker): BROKER-1 — the MCP broker intercepts tools/call /mcp/broker/<upstream> fronts registered MCP servers: every tools/call is authorized through the tool ladder (inheritance + conditions included) before it forwards; refusals come back as MCP isError results the model can read and replan on; escalations name the request_id and the _meta["sanction/grant_id"] retry path, and a redeemed grant is stripped before forwarding. Batched requests containing tools/call are refused — fail closed, never around. For brokered traffic, "a hijacked agent cannot act" is now enforcement, not cooperation. - lib/broker.ts: upstream registry (SEC-1-vaulted under mcp:<name>), request classification, forwarding with fresh outbound headers — the agent's Sanction key never reaches an upstream, and vaulted upstream credentials never reach the agent. - app/mcp/broker/[upstream]/route.ts: authorization runs as an in-process call to the /authorize/tool route handler — one enforcement shell by construction, no drift between broker and REST decisions. - app/api/v1/broker/upstreams: owner-plane register/list/revoke. - Found and fixed shipped bug: the gateway's provider-key vault read ran outside withTenant, so FORCE RLS silently returned null and connected providers 401'd PROVIDER_NOT_CONNECTED. Now tenant-scoped (and the gateway test mock now exercises withTenant). - Honesty flip: Wallet Card now says enforcement cooperative+broker, interception gateway+mcp-broker, with the scope stated plainly — traffic that bypasses the broker is not governed. - Version literals: walletCard/mcp.json/server.json now 0.8.0 (walletCard reads MCP_SERVER_VERSION instead of a hardcoded string). - Truth surfaces: README, roadmap, changelog, DOMAIN, AGENT-WALLET, TRACEABILITY, OpenAPI, backlog annotation. Tests: 17 new (broker classification/refusal + route: blocked never forwards, escalation carries the grant path, allowed forwards with upstream auth only, grant redeemed+stripped, batch refused). Full suite 1147 passing. Co-Authored-By: Claude <noreply@anthropic.com> * docs(plans): stablecoin rails — design position + backlog capture Eric's direction: agents' authorized spend settles in regulated stablecoins (USDC most likely), not card-rail USD — build the path. Position: stablecoin settlement is irreversible, so pre-transaction authorization becomes the only control point; the rail shift makes the engine more necessary. Slices STABLE-0..3 (settlement metadata → x402 spend gate → wallet-provider co-signer → onchain mirror), custody explicitly rejected. Market state verified 2026-08-22 (x402 under the Linux Foundation w/ Stripe/Visa/Mastercard, AP2 v0.2 mandates, GENIUS Act). Co-Authored-By: Claude <noreply@anthropic.com> * docs(plans): CLARITY Act — Section 604 makes veto-only the legal architecture Market-structure bill (House-passed, Senate cloture 2026-09-15): its Section 604 codifies that non-custodial, non-controlling developers are not money transmitters. Adopt the design constraint now: Sanction's co-signing (STABLE-2/3) is veto-only — it can refuse a transfer the wallet initiates, never initiate or effectuate one. Co-Authored-By: Claude <noreply@anthropic.com> * docs(plans): monetization + distribution — five-discipline synthesis Companion to stablecoin-rails.md, from a UX / financial / growth / brand / competitive research fan-out. Position: if Sanction meters, the unit is the decision (bps on spend rejected — wrong economics, wrong optics); Zapier-style bundles; self-serve tier payable via x402/USDC itself. Trust surfaces (fee meter, operator-set fee cap, per-decision receipts, grace mode) precede any invoice. Distribution ranked: MCP registries, /govern pages, Slack App Directory, x402/AP2 ecosystem listing (no policy layer listed there today), n8n before Zapier. First slices MONO-0 / DIST-0. Co-Authored-By: Claude <noreply@anthropic.com> * feat(engine): STABLE-0 rail-aware ledger + MONO-0 decision meter STABLE-0 (docs/plans/stablecoin-rails.md): POST /v1/authorize accepts optional settlement {rail, asset, network} — a closed vocabulary (lib/settlement.ts; a chain network is refused on a non-x402 rail so evidence cannot lie). Inert to the decision like attribution tags, rides detailsJson, surfaces in the audit feed and CSV, documented in OpenAPI. Stablecoin settlement is irreversible, so pre-transaction authorization is the only control point — the ledger is rail-aware before any rail-specific enforcement exists. MONO-0 (docs/plans/monetization-and-distribution.md): per-wallet, per-UTC-month WalletDecisionCounter incremented once per FRESH engine decision across all four authorize shells (approve/escalate/deny alike, observe included). Idempotent replays, grant redemptions, simulate runs, and frozen short-circuits never count; increments run via after() and swallow errors — metering can never fail or slow a decision. Exposed as month.decisions on /v1/wallets/stats and a Decisions (month) card on the spend console. Instrumentation only: no fee attaches — the unit is measured before it is ever priced. Found and fixed while wiring: the provision route caught a subtree-cap breach INSIDE its transaction, committing the ancestor counter increments the reservation loop had already written — phantom pool spend on a denied provision (the same defect the spend route's D1 fix removed). It now lets the transaction roll back and persists the denial outside, evidence attached, mirroring the spend route. Tests: 14 new (closed-vocab settlement accept/reject, persistence, meter counts fresh approve+deny once, replay/simulate never count, meter failure swallowed, month key UTC, D1-parity provision regression). Full suite 1161 passing. Live-fired: x402/usdc/base persisted on an approved row, off-vocab rail 400s, counter=2 after one spend + one allowed tool call, console card render-checked. Co-Authored-By: Claude <noreply@anthropic.com> * docs: dated model note (Pro $20 metered middle) + GTM arc backlog entry Eric's call 2026-08-22: the model gains a metered middle — Pro $20/mo, metered in decisions, early access only until real billing ships (no checkout; trust surfaces precede any invoice). Free and enterprise framing unchanged. GTM launch arc captured in the backlog. Co-Authored-By: Claude <noreply@anthropic.com> * feat(site): blog — first post: the rail is changing, the decision is the unit New /blog (index + slug pages, changelog conventions: typed array in lib/blog.ts, shared Markdown rendering, static params + per-post metadata). Post #1 announces the August arc honestly: broker interception, the settlement-aware ledger, the decision meter, the GENIUS/CLARITY ground and the veto-only architecture, with the x402 spend gate and Pro tier explicitly marked next-not-shipped. Co-Authored-By: Claude <noreply@anthropic.com> * feat(site): platform goes stablecoin-era — rails section + Pro pricing New #rails section under the canonical line ("Sanction authorizes the spend; any rail settles it"): the irreversibility argument, the settlement-aware ledger marked live, the x402 spend gate marked next (never overclaimed), and the non-custodial/veto-only regulatory posture (GENIUS as instrument law, CLARITY as pending). Pricing becomes three cards: Free (unchanged) / Pro $20/mo early access (metered in decisions, lead-capture CTA, no checkout) / Enterprise Agreement. Disambiguation line: sanction (v.) — to give official authorization. Co-Authored-By: Claude <noreply@anthropic.com> * docs(gtm): launch playbook + integration use-case catalog as HTML Two self-contained HTML documents under docs/gtm/: the 14-day launch playbook (positioning, sequence, the CLARITY-vote play, content engine, Pro funnel, metrics) and "Where Sanction sits" — seven integration architectures with pre-rendered sequence diagrams (static SVG, no runtime dependencies; open in any browser). Co-Authored-By: Claude <noreply@anthropic.com> * docs: relocate GTM documents to the private strategy repo Strategy/GTM material does not belong in the public repo (same call as the 2026-07-06 move) — the launch playbook and use-case catalog now live in ericlovold/sanction-strategy under docs/gtm/. Co-Authored-By: Claude <noreply@anthropic.com> * feat(engine): STABLE-1 — the x402 spend gate, authorized before the wallet signs An x402 402 is a demand for money, and stablecoin settlement is irreversible: the moment between receiving a payment challenge and signing it is the only control point there is. STABLE-1 takes that moment. Two surfaces, one shell (both call POST /v1/authorize in-process, so a quote can never drift from ordinary spend): - Cooperative: POST /v1/authorize/quote — post the 402 body you just received; Sanction prices it and runs the same ladder (budgets, escalation, grants, cascade caps, evidence, decision meter), with STABLE-0 settlement metadata derived from the quote itself. - Intercepting: the MCP broker gates upstream 402s and, on anything but an approval, WITHHOLDS the challenge. The refusal type structurally cannot carry payTo/maxAmountRequired — an agent cannot sign payment requirements it never received. Two rules keep pricing honest and deterministic: - No oracle, no FX. A quote is priced only when its asset is a USD-pegged stablecoin with known decimals (native USDC on Base; address + 6 decimals verified against Circle). Anything else denies as QUOTE_NOT_PRICEABLE rather than being guessed — a decision must never depend on a rate looked up at decision time (ADR-0009). - Worst case wins. A challenge offering several ways to pay is authorized at its most expensive option, and one unpriceable option poisons the whole challenge: the client picks, and we cannot know which. Still non-custodial and veto-only — no keys, no signing, refusal only, which is the shape CLARITY §604's non-controlling test describes. Tests: 28 new (pricing, worst-case selection, poisoned challenges, the gate's withholding contract, quote route, broker interception). Full suite 1189 passing. Live-fired against Postgres: $0.05 and $15 approved, $900 denied PER_TXN_LIMIT, a multi-option challenge authorized at its worst case, every row carrying settlement metadata. Truth surfaces: /platform now says the gate is live (was "next"), changelog, roadmap, TRACEABILITY, OpenAPI, README, backlog, design doc. Co-Authored-By: Claude <noreply@anthropic.com> * feat(brand): favicon and app icons from the new wordmark; re-land the hero fix Icons were still the old shield-and-checkmark mark. The whole icon set is now generated from the $ glyph in Paul's wordmark (the three green paths of sanction-wordmark-green.svg, bounds measured with getBBox rather than eyeballed), on the brand's own pine ground with the wordmark's signal green: app/icon.svg 512, rounded square (browser tab) app/favicon.ico 16 + 32, PNG-in-ICO app/apple-icon.png 180 public/icons/icon-192|512.png PWA "any" public/icons/icon-maskable-512.png glyph inside the 80% safe zone manifest theme/background move to pine so the PWA splash frames the icon in the same dark it uses. Also re-lands the hero fix from #256, which was stranded: it merged into the marketing branch after that branch had already been squashed into main, so main still read "AVAILABLE $2,500.00". Sanction is non-custodial and veto-only, so the card states the limit — "DAILY CAP / ENFORCED". Verified: icon legible at 16px on light and dark tab strips; /, /icon.svg, /favicon.ico and /apple-icon.png all 200 with the right content types. Caught in render-check and fixed: Next decodes favicon.ico and requires RGBA, and an RGB one 500s every page — noted in AGENTS.md along with the production-deploy-path and stacked-PR lessons. Co-Authored-By: Claude <noreply@anthropic.com> * feat(marketing): drop the certification strip from the homepage The "ANTHROPIC CERTIFIED" trust strip (and the "Also certified: Clio Legal AI Fundamentals" line under it) belonged to the services-firm framing. On the reframed page it reads as personal credentials on what is now a product homepage, and it sat between the hero and the argument it leads into. Removes the section and deletes components/anthropic-certs.tsx — its own header said it served the homepage and the consulting page, and the consulting page has since split out, so nothing else referenced it. Git history keeps it if it wants a home on the services site. Verified: rendered homepage contains no ANTHROPIC CERTIFIED / Clio / cert-pill markers; hero, wallet card, and the "Why authorization comes first" section all intact. Co-Authored-By: Claude <noreply@anthropic.com> * feat(marketing): getsanction.com is the authorization product only The Moral Intention Analyst has its own space now, so it comes off this site entirely — one product, one story. - deletes app/moral-intention/ (page + reveal component) - homepage: drops the /moral-intention footer link - /about: removes the "Moral Intention" nav and footer links, the MIA sentence from the thesis paragraph, MIA from the metadata description and from Eric's bio, and Dr. A.C. Ping's team card together with the ac-ping photo placeholder — the ethics authorship belongs with MIA wherever MIA now lives, not on the authorization product's site No references remain anywhere in app/, components/, lib/ or public/. /moral-intention now 404s; if the new home has a public URL a permanent redirect there would be better than a dead end for anything already linking to it. Verified: /, /about, /platform, /blog all 200 and carry no MIA, A.C. Ping, acping, ethics-framework or brain-trust residue. tsc 0, eslint 0, 1189 tests passing. Co-Authored-By: Claude <noreply@anthropic.com> * feat(marketing): redirect /moral-intention to its own site moralintention.com is live, so the old page becomes a permanent cross-origin redirect rather than a 404 — anything already linking to it lands on the real thing, and the link equity follows. Sub-paths are preserved too (/moral-intention/:path* -> the same path on moralintention.com), so any deeper link that ever existed still resolves. Verified locally: /moral-intention -> 308 https://moralintention.com/, /moral-intention/analyzer -> 308 https://moralintention.com/analyzer, and /consulting's existing redirect plus /, /about, /platform, /blog are all unaffected. Note the destination itself could not be fetched from this sandbox — its proxy 403s that host — but DNS resolves and the domain is confirmed live by its owner. Co-Authored-By: Claude <noreply@anthropic.com> * feat(brand): lockup carries the category, the hero carries the moment The wordmark alone says nothing about what Sanction does — and to finance readers the name reads as OFAC first. The nav lockup now carries a descriptor: "Sanction | AGENT AUTHORIZATION", inline right with a hairline divider, on the homepage, /about and /platform. That created a second problem worth fixing in the same pass: the hero eyebrow said "Spend governance for autonomous systems" thirty pixels below it, so the page opened with two competing descriptions of the same company. The mark now owns the category and the eyebrow owns the moment — "Runaway agent spend, stopped" — which also sets up the headline instead of restating it. Mechanics: .sn-lockup-tag drops out below 1100px rather than wrapping, where the nav needs its width for links. The tagline is aria-hidden because the wordmark's alt already announces the brand — a screen reader should hear "Sanction", not "Sanction Agent authorization". Render-checked at 1440 / 1180 / 1040 / 640: visible above the breakpoint, gone below it, no wrap at any width. eslint 0 errors, 1189 tests passing. Co-Authored-By: Claude <noreply@anthropic.com> * brand: outlined lockup SVGs + social cards with the descriptor baked in The root site had no Open Graph image at all — layout.tsx declared twitter card "summary_large_image" with nothing to fill it, so a link to getsanction.com rendered a large-image card with an empty image well. Adds: - public/brand/sanction-lockup-{green,white}.svg — the wordmark plus the "Agent authorization" descriptor as VECTOR OUTLINES, not <text>. Off-site consumers (social cards, npm/registry READMEs, decks) do not load our webfonts, so a <text> tagline would fall back to a system font and the lockup would drift. Verified: zero <text> elements, renders identically with no webfont loaded, legible down to 16px on light and dark. - app/opengraph-image.png + app/twitter-image.png (1200x630) using that lockup, with matching .alt.txt files. Verified against a production build: og:image / twitter:image / og:image:alt all emit, images serve 200 at the right dimensions. * chore(sprint): /fresh-eyes skill + Codex cold-start export + branch audit Cleanup sprint before handing the repo to a Codex pass. New skill `/fresh-eyes` — the inverse of `/INPUT`. Where INPUT ingests outside material INTO the project, fresh-eyes exports verified state OUT to a reader with no context, then stares at the repo the way that reader will. Two jobs, and the second is the one people skip: the export, and naming what our own fluency hides. Every claim must be verified live in the run that writes it — a handoff is read by someone who cannot check your work, so a stale claim is not caught, it is acted on. Contract is a blind-spot ledger: VERIFIED / LANDMINE / ASSUMED / UNOWNED / DECIDE / DRIFT. First run of it produced docs/handoff/CODEX-SPRINT-2026-08-23.md, and turned up one thing that matters more than the sprint: PRODUCTION IS A MERGE BEHIND MAIN. PR #259 merged at 19:23 UTC and produced no Vercel build at all. Last production deploy is #258, from the previous day. Live site confirms it — /opengraph-image.png 404s and /moral-intention still 200s instead of redirecting. Main auto-deployed three times the day before, so this is an anomaly, not configuration. Needs a human redeploy of main@5dc4c9c. Also verified and recorded: - Slack OAuth is complete end to end (SLACK-1 slice 2) — install path, interactive approve/deny, SlackInstall under RLS, fail-closed 503 when the signing secret is unset, five test files. Documented in the export. - Seven remote branches are fully absorbed into main. Each verified by locating its PR's squash commit, and for fix/hero-noncustodial (whose #256 was stranded and re-landed via #257) by content comparison. The delete command and every branch tip are recorded so it is reversible — the sandbox classifier denies the deletion, so it is queued for Eric. - Five remaining branches carry real unmerged work and are listed with what each holds, so nobody prunes them by mistake. AGENTS.md gains a dated note: a merge to main is not a deploy. Close an arc by checking production, not the merge. Gate: tsc 0, eslint 0, 1189 tests passing. * fix(ci): rebuild the MCP bundle so main is green again CI has been red on main for 20 consecutive runs across six days — since the v0.8.0 release (#244, 2026-08-19). Every PR since, mine included, merged straight through it. The cause is one line. `lib/mcpServer.ts` exports MCP_SERVER_VERSION, esbuild INLINES it into packages/sanction-mcp/mcp-server.js, and #244 bumped the source without rebuilding the bundle. CI regenerates the bundle and runs `git diff --exit-code packages/` as its LAST step, so every run went green through 1,191 tests and 90% coverage and then failed on: -var MCP_SERVER_VERSION = "0.7.0"; +var MCP_SERVER_VERSION = "0.8.0"; which reads like a flake at the end of a green run, and got treated as one. The published npm artifact is fine — the workflow builds fresh. What was wrong was the artifact checked into the repo. The second cost is the one worth fixing properly: a permanently-red main means nobody can distinguish a NEW failure from the standing one. The merge gate was decorative for six days. So this does three things, not one: 1. Rebuilds the bundle. `npm run build:mcp && git diff --exit-code packages/` now passes locally, which is CI's exact check. 2. /cut-release gains a step: regenerate every artifact that EMBEDS the version, not just the ones that declare it, with the reason and the verify command. The ritual is why this recurred; the ritual is where it gets fixed. 3. AGENTS.md gains a dated note: a red main is an incident, not a backlog item. Never merge into a red main assuming the red is "the known one" without reading the log. Also corrects the version-literal list in the 2026-08-23 handoff export, which omitted the generated bundle — the exact omission that caused this — and wrongly listed lib/walletCard.ts, which derives the constant rather than restating it. Gate: tsc 0, eslint 0, 1191 tests passing. * fix(deps): clear all 16 advisories without downgrading Prisma The `Dependency audit (high+)` CI step is `continue-on-error: true`, so a green check on it means "did not block", not "clean". 16 advisories (12 high, 4 moderate) accumulated behind that green check, several of them request-path: Next.js SSRF in rewrites, cache confusion on bodied requests, DoS in Server Actions; undici response desync; postcss source-map path traversal. Cleared in three separable steps so any regression is attributable: 1. `npm audit fix` — 13 semver-compatible: brace-expansion, fast-uri, ip-address, js-yaml, nanoid, undici, hono, @hono/node-server, valibot, prisma, @prisma/dev and friends. 16 -> 6. 2. next 16.2.9 -> 16.3.3, which also clears postcss and sharp. This is a MINOR bump inside Next 16, not the major upgrade the audit output implies — npm says "outside the stated dependency range" only because package.json pins the version exactly, with no caret. 6 -> 3. 3. `overrides: { "deepmerge-ts": "^8.0.2" }` for the last three. Step 3 needs explaining, because npm's own suggestion here is a trap. For the deepmerge-ts stack-exhaustion advisory npm proposes `prisma@6.12.0` — a MAJOR DOWNGRADE from the installed 7.10.0, which would tear out the Prisma 7 driver adapter the entire data layer runs on. The real constraint is that @prisma/config@7.10.0 hard-pins deepmerge-ts to exactly 7.1.5, while the patched line is 8.0.2. An override fixes it and keeps Prisma 7. Result: `npm audit` reports 0 vulnerabilities, Prisma stays at 7.10.0. Verified, not assumed: - gate: tsc 0, eslint 0, 1191 tests passing - DB tests: 20/20 under CI's exact env, RLS isolation and e2e data plane included (they fail locally only because the dev role cannot CREATE ROLE) - prisma generate / validate / migrate status all load prisma.config.ts through the overridden @prisma/config — 38 migrations, schema up to date - next build clean; next start + Chromium on /, /platform, /slack: all render correctly, zero page errors (only console noise is the Vercel Analytics script, which 404s off-Vercel by design) - the regenerated MCP bundle still reports 0.8.0 and answers `initialize` over stdio; bundle-freshness check passes The bundle diff is large because esbuild inlines the upgraded transitive deps; it is committed here, per the lesson from the last PR. Adds .github/dependabot.yml — there was none, which is why this rotted unseen. Weekly, grouped, majors excluded from groups so each gets its own gate. The audit step stays non-blocking on purpose: making it blocking would let a new upstream advisory redden main with no commit behind it, which is the exact failure that made the merge gate decorative for six days. * docs(connect): decide before the token is minted Vercel Connect reached GA 2026-08-25 — runtime-minted short-lived scoped credentials, 100+ connectors, replacing stored provider secrets. Verified against vercel.com/docs/connect, not the launch post. It closes a problem Sanction never claimed to solve, and leaves one open. Read the token request wire format: the requested `scopes` come from the CALLER. Connect authenticates the requester via deployment OIDC and issues a correctly-scoped, correctly-expiring credential. It is an issuer, not a decision point. An agent in a loop asking for a legitimate scope 4,000 times gets 4,000 correctly-scoped, correctly-expiring tokens. The composition needs no engine change: a Connect token request IS a capability acquisition, and POST /v1/authorize/capability already governs those against ordered namespaced rules — the same ladder as skill:install:* and api:host/path. Namespace it `connector:<connector>:<scope>`, authorize, then call getToken. Denied or escalated, nothing is minted; on approval the single-use grant replays as grant_id. Capability rules inherit down the wallet tree, so a department can tighten what the org allows. Ships with the boundary stated plainly, including two things the pattern does NOT do: it is cooperative, not enforcement at the issuer (an agent that skips the call still gets a token), and it cannot narrow a scope — Sanction answers yes or no on what the caller asked for. Also records where the vault still applies: agents that do not run on Vercel, and broker-held upstreams. Where Connect applies, prefer it — a credential that never exists at rest beats one encrypted at rest. Hold the mandate, not the rail: Connect owns issuance, Sanction owns the decision and the record. The follow-on arc is captured in the backlog — enforcement at the issuer via the existing AuthZEN PDP, which is the same veto-only posture as STABLE-2, and worth a conversation with Vercel rather than speculative code. Gate: tsc 0, eslint 0, 1191 tests passing. --------- Co-authored-by: Claude <noreply@anthropic.com>
* feat(engine): COND-1 — rules that read the clock and the meter Context-conditional tool rules, the second approved engine arc. A tool rule can carry a `when` — a CLOSED vocabulary, exactly one predicate per rule, restrictive effects only (an allow is never conditional, so allow-list semantics never depend on a clock or a counter). No DSL, no expressions: composition is more rules, which stays simulable and explainable. Two predicates ship: - outside_hours_utc: [start, end) UTC, wrapping midnight — "deploys escalate outside business hours". - after_model_calls_today: N — the runaway breaker: once the agent's persisted model calls pass N today, block (or escalate) matching tools. One honest correction from the design doc, recorded there: 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 runaway loops this exists to stop. Determinism (ADR-0009): signals are captured ONCE per request by the enforcement shell — the UTC hour, and the token-log count fetched ONLY when some layer actually conditions on it — passed into the pure ladder, and persisted in the decision's evidence. Replay reads the stored snapshot, never the live clock. A rule whose signal is absent from a context is deterministically inactive, so pre-COND rows replay unchanged. Precedence: block → conditional block → allow-list → escalate → conditional escalate. Conditions ride the INHERIT-1 layers, so an org-level breaker binds every team below; parity on the AuthZEN PDP with the same signal contract. New Policy.toolConditions Json column (migration included), validated in lib/policy.ts (one predicate, hour bounds, start != end), writable through the existing PATCH /wallets/policy and console actions via policyInputSchema. OpenAPI documents the shape. Console: tool-posture card counts conditionals; the inherited card names them. Simulate boundary, stated not hidden: the tool ladder predates SIM-1's simulatable set; candidate tool_conditions echo back in ignored_fields. Verified: 13 new unit tests (window boundaries + midnight wrap, threshold edges with counts in the reason, absent-signal inactivity, precedence, prefix-glob), 4 route tests (threshold deny with signals persisted in evidence; the counter queried only when conditioned on; a parent's conditional rule binding the child), and both predicates live-fired in both directions against the seeded org — including catching and redoing a false positive where an unconditional escalate masked the conditional one. Policy page render-checked. Truth surfaces: TRACEABILITY COND-1 row + suite map, DOMAIN, roadmap Now, changelog, design doc stamped with the correction, backlog checked off. Gate: tsc clean, eslint 0 errors, 1130 passing. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Eb6zTfLyE42dYS8tgWfvn * feat(broker): BROKER-1 — the MCP broker intercepts tools/call /mcp/broker/<upstream> fronts registered MCP servers: every tools/call is authorized through the tool ladder (inheritance + conditions included) before it forwards; refusals come back as MCP isError results the model can read and replan on; escalations name the request_id and the _meta["sanction/grant_id"] retry path, and a redeemed grant is stripped before forwarding. Batched requests containing tools/call are refused — fail closed, never around. For brokered traffic, "a hijacked agent cannot act" is now enforcement, not cooperation. - lib/broker.ts: upstream registry (SEC-1-vaulted under mcp:<name>), request classification, forwarding with fresh outbound headers — the agent's Sanction key never reaches an upstream, and vaulted upstream credentials never reach the agent. - app/mcp/broker/[upstream]/route.ts: authorization runs as an in-process call to the /authorize/tool route handler — one enforcement shell by construction, no drift between broker and REST decisions. - app/api/v1/broker/upstreams: owner-plane register/list/revoke. - Found and fixed shipped bug: the gateway's provider-key vault read ran outside withTenant, so FORCE RLS silently returned null and connected providers 401'd PROVIDER_NOT_CONNECTED. Now tenant-scoped (and the gateway test mock now exercises withTenant). - Honesty flip: Wallet Card now says enforcement cooperative+broker, interception gateway+mcp-broker, with the scope stated plainly — traffic that bypasses the broker is not governed. - Version literals: walletCard/mcp.json/server.json now 0.8.0 (walletCard reads MCP_SERVER_VERSION instead of a hardcoded string). - Truth surfaces: README, roadmap, changelog, DOMAIN, AGENT-WALLET, TRACEABILITY, OpenAPI, backlog annotation. Tests: 17 new (broker classification/refusal + route: blocked never forwards, escalation carries the grant path, allowed forwards with upstream auth only, grant redeemed+stripped, batch refused). Full suite 1147 passing. Co-Authored-By: Claude <noreply@anthropic.com> * docs(plans): stablecoin rails — design position + backlog capture Eric's direction: agents' authorized spend settles in regulated stablecoins (USDC most likely), not card-rail USD — build the path. Position: stablecoin settlement is irreversible, so pre-transaction authorization becomes the only control point; the rail shift makes the engine more necessary. Slices STABLE-0..3 (settlement metadata → x402 spend gate → wallet-provider co-signer → onchain mirror), custody explicitly rejected. Market state verified 2026-08-22 (x402 under the Linux Foundation w/ Stripe/Visa/Mastercard, AP2 v0.2 mandates, GENIUS Act). Co-Authored-By: Claude <noreply@anthropic.com> * docs(plans): CLARITY Act — Section 604 makes veto-only the legal architecture Market-structure bill (House-passed, Senate cloture 2026-09-15): its Section 604 codifies that non-custodial, non-controlling developers are not money transmitters. Adopt the design constraint now: Sanction's co-signing (STABLE-2/3) is veto-only — it can refuse a transfer the wallet initiates, never initiate or effectuate one. Co-Authored-By: Claude <noreply@anthropic.com> * docs(plans): monetization + distribution — five-discipline synthesis Companion to stablecoin-rails.md, from a UX / financial / growth / brand / competitive research fan-out. Position: if Sanction meters, the unit is the decision (bps on spend rejected — wrong economics, wrong optics); Zapier-style bundles; self-serve tier payable via x402/USDC itself. Trust surfaces (fee meter, operator-set fee cap, per-decision receipts, grace mode) precede any invoice. Distribution ranked: MCP registries, /govern pages, Slack App Directory, x402/AP2 ecosystem listing (no policy layer listed there today), n8n before Zapier. First slices MONO-0 / DIST-0. Co-Authored-By: Claude <noreply@anthropic.com> * feat(engine): STABLE-0 rail-aware ledger + MONO-0 decision meter STABLE-0 (docs/plans/stablecoin-rails.md): POST /v1/authorize accepts optional settlement {rail, asset, network} — a closed vocabulary (lib/settlement.ts; a chain network is refused on a non-x402 rail so evidence cannot lie). Inert to the decision like attribution tags, rides detailsJson, surfaces in the audit feed and CSV, documented in OpenAPI. Stablecoin settlement is irreversible, so pre-transaction authorization is the only control point — the ledger is rail-aware before any rail-specific enforcement exists. MONO-0 (docs/plans/monetization-and-distribution.md): per-wallet, per-UTC-month WalletDecisionCounter incremented once per FRESH engine decision across all four authorize shells (approve/escalate/deny alike, observe included). Idempotent replays, grant redemptions, simulate runs, and frozen short-circuits never count; increments run via after() and swallow errors — metering can never fail or slow a decision. Exposed as month.decisions on /v1/wallets/stats and a Decisions (month) card on the spend console. Instrumentation only: no fee attaches — the unit is measured before it is ever priced. Found and fixed while wiring: the provision route caught a subtree-cap breach INSIDE its transaction, committing the ancestor counter increments the reservation loop had already written — phantom pool spend on a denied provision (the same defect the spend route's D1 fix removed). It now lets the transaction roll back and persists the denial outside, evidence attached, mirroring the spend route. Tests: 14 new (closed-vocab settlement accept/reject, persistence, meter counts fresh approve+deny once, replay/simulate never count, meter failure swallowed, month key UTC, D1-parity provision regression). Full suite 1161 passing. Live-fired: x402/usdc/base persisted on an approved row, off-vocab rail 400s, counter=2 after one spend + one allowed tool call, console card render-checked. Co-Authored-By: Claude <noreply@anthropic.com> * docs: dated model note (Pro $20 metered middle) + GTM arc backlog entry Eric's call 2026-08-22: the model gains a metered middle — Pro $20/mo, metered in decisions, early access only until real billing ships (no checkout; trust surfaces precede any invoice). Free and enterprise framing unchanged. GTM launch arc captured in the backlog. Co-Authored-By: Claude <noreply@anthropic.com> * feat(site): blog — first post: the rail is changing, the decision is the unit New /blog (index + slug pages, changelog conventions: typed array in lib/blog.ts, shared Markdown rendering, static params + per-post metadata). Post #1 announces the August arc honestly: broker interception, the settlement-aware ledger, the decision meter, the GENIUS/CLARITY ground and the veto-only architecture, with the x402 spend gate and Pro tier explicitly marked next-not-shipped. Co-Authored-By: Claude <noreply@anthropic.com> * feat(site): platform goes stablecoin-era — rails section + Pro pricing New #rails section under the canonical line ("Sanction authorizes the spend; any rail settles it"): the irreversibility argument, the settlement-aware ledger marked live, the x402 spend gate marked next (never overclaimed), and the non-custodial/veto-only regulatory posture (GENIUS as instrument law, CLARITY as pending). Pricing becomes three cards: Free (unchanged) / Pro $20/mo early access (metered in decisions, lead-capture CTA, no checkout) / Enterprise Agreement. Disambiguation line: sanction (v.) — to give official authorization. Co-Authored-By: Claude <noreply@anthropic.com> * docs(gtm): launch playbook + integration use-case catalog as HTML Two self-contained HTML documents under docs/gtm/: the 14-day launch playbook (positioning, sequence, the CLARITY-vote play, content engine, Pro funnel, metrics) and "Where Sanction sits" — seven integration architectures with pre-rendered sequence diagrams (static SVG, no runtime dependencies; open in any browser). Co-Authored-By: Claude <noreply@anthropic.com> * docs: relocate GTM documents to the private strategy repo Strategy/GTM material does not belong in the public repo (same call as the 2026-07-06 move) — the launch playbook and use-case catalog now live in ericlovold/sanction-strategy under docs/gtm/. Co-Authored-By: Claude <noreply@anthropic.com> * feat(engine): STABLE-1 — the x402 spend gate, authorized before the wallet signs An x402 402 is a demand for money, and stablecoin settlement is irreversible: the moment between receiving a payment challenge and signing it is the only control point there is. STABLE-1 takes that moment. Two surfaces, one shell (both call POST /v1/authorize in-process, so a quote can never drift from ordinary spend): - Cooperative: POST /v1/authorize/quote — post the 402 body you just received; Sanction prices it and runs the same ladder (budgets, escalation, grants, cascade caps, evidence, decision meter), with STABLE-0 settlement metadata derived from the quote itself. - Intercepting: the MCP broker gates upstream 402s and, on anything but an approval, WITHHOLDS the challenge. The refusal type structurally cannot carry payTo/maxAmountRequired — an agent cannot sign payment requirements it never received. Two rules keep pricing honest and deterministic: - No oracle, no FX. A quote is priced only when its asset is a USD-pegged stablecoin with known decimals (native USDC on Base; address + 6 decimals verified against Circle). Anything else denies as QUOTE_NOT_PRICEABLE rather than being guessed — a decision must never depend on a rate looked up at decision time (ADR-0009). - Worst case wins. A challenge offering several ways to pay is authorized at its most expensive option, and one unpriceable option poisons the whole challenge: the client picks, and we cannot know which. Still non-custodial and veto-only — no keys, no signing, refusal only, which is the shape CLARITY §604's non-controlling test describes. Tests: 28 new (pricing, worst-case selection, poisoned challenges, the gate's withholding contract, quote route, broker interception). Full suite 1189 passing. Live-fired against Postgres: $0.05 and $15 approved, $900 denied PER_TXN_LIMIT, a multi-option challenge authorized at its worst case, every row carrying settlement metadata. Truth surfaces: /platform now says the gate is live (was "next"), changelog, roadmap, TRACEABILITY, OpenAPI, README, backlog, design doc. Co-Authored-By: Claude <noreply@anthropic.com> * feat(brand): favicon and app icons from the new wordmark; re-land the hero fix Icons were still the old shield-and-checkmark mark. The whole icon set is now generated from the $ glyph in Paul's wordmark (the three green paths of sanction-wordmark-green.svg, bounds measured with getBBox rather than eyeballed), on the brand's own pine ground with the wordmark's signal green: app/icon.svg 512, rounded square (browser tab) app/favicon.ico 16 + 32, PNG-in-ICO app/apple-icon.png 180 public/icons/icon-192|512.png PWA "any" public/icons/icon-maskable-512.png glyph inside the 80% safe zone manifest theme/background move to pine so the PWA splash frames the icon in the same dark it uses. Also re-lands the hero fix from #256, which was stranded: it merged into the marketing branch after that branch had already been squashed into main, so main still read "AVAILABLE $2,500.00". Sanction is non-custodial and veto-only, so the card states the limit — "DAILY CAP / ENFORCED". Verified: icon legible at 16px on light and dark tab strips; /, /icon.svg, /favicon.ico and /apple-icon.png all 200 with the right content types. Caught in render-check and fixed: Next decodes favicon.ico and requires RGBA, and an RGB one 500s every page — noted in AGENTS.md along with the production-deploy-path and stacked-PR lessons. Co-Authored-By: Claude <noreply@anthropic.com> * feat(marketing): drop the certification strip from the homepage The "ANTHROPIC CERTIFIED" trust strip (and the "Also certified: Clio Legal AI Fundamentals" line under it) belonged to the services-firm framing. On the reframed page it reads as personal credentials on what is now a product homepage, and it sat between the hero and the argument it leads into. Removes the section and deletes components/anthropic-certs.tsx — its own header said it served the homepage and the consulting page, and the consulting page has since split out, so nothing else referenced it. Git history keeps it if it wants a home on the services site. Verified: rendered homepage contains no ANTHROPIC CERTIFIED / Clio / cert-pill markers; hero, wallet card, and the "Why authorization comes first" section all intact. Co-Authored-By: Claude <noreply@anthropic.com> * feat(marketing): getsanction.com is the authorization product only The Moral Intention Analyst has its own space now, so it comes off this site entirely — one product, one story. - deletes app/moral-intention/ (page + reveal component) - homepage: drops the /moral-intention footer link - /about: removes the "Moral Intention" nav and footer links, the MIA sentence from the thesis paragraph, MIA from the metadata description and from Eric's bio, and Dr. A.C. Ping's team card together with the ac-ping photo placeholder — the ethics authorship belongs with MIA wherever MIA now lives, not on the authorization product's site No references remain anywhere in app/, components/, lib/ or public/. /moral-intention now 404s; if the new home has a public URL a permanent redirect there would be better than a dead end for anything already linking to it. Verified: /, /about, /platform, /blog all 200 and carry no MIA, A.C. Ping, acping, ethics-framework or brain-trust residue. tsc 0, eslint 0, 1189 tests passing. Co-Authored-By: Claude <noreply@anthropic.com> * feat(marketing): redirect /moral-intention to its own site moralintention.com is live, so the old page becomes a permanent cross-origin redirect rather than a 404 — anything already linking to it lands on the real thing, and the link equity follows. Sub-paths are preserved too (/moral-intention/:path* -> the same path on moralintention.com), so any deeper link that ever existed still resolves. Verified locally: /moral-intention -> 308 https://moralintention.com/, /moral-intention/analyzer -> 308 https://moralintention.com/analyzer, and /consulting's existing redirect plus /, /about, /platform, /blog are all unaffected. Note the destination itself could not be fetched from this sandbox — its proxy 403s that host — but DNS resolves and the domain is confirmed live by its owner. Co-Authored-By: Claude <noreply@anthropic.com> * feat(brand): lockup carries the category, the hero carries the moment The wordmark alone says nothing about what Sanction does — and to finance readers the name reads as OFAC first. The nav lockup now carries a descriptor: "Sanction | AGENT AUTHORIZATION", inline right with a hairline divider, on the homepage, /about and /platform. That created a second problem worth fixing in the same pass: the hero eyebrow said "Spend governance for autonomous systems" thirty pixels below it, so the page opened with two competing descriptions of the same company. The mark now owns the category and the eyebrow owns the moment — "Runaway agent spend, stopped" — which also sets up the headline instead of restating it. Mechanics: .sn-lockup-tag drops out below 1100px rather than wrapping, where the nav needs its width for links. The tagline is aria-hidden because the wordmark's alt already announces the brand — a screen reader should hear "Sanction", not "Sanction Agent authorization". Render-checked at 1440 / 1180 / 1040 / 640: visible above the breakpoint, gone below it, no wrap at any width. eslint 0 errors, 1189 tests passing. Co-Authored-By: Claude <noreply@anthropic.com> * brand: outlined lockup SVGs + social cards with the descriptor baked in The root site had no Open Graph image at all — layout.tsx declared twitter card "summary_large_image" with nothing to fill it, so a link to getsanction.com rendered a large-image card with an empty image well. Adds: - public/brand/sanction-lockup-{green,white}.svg — the wordmark plus the "Agent authorization" descriptor as VECTOR OUTLINES, not <text>. Off-site consumers (social cards, npm/registry READMEs, decks) do not load our webfonts, so a <text> tagline would fall back to a system font and the lockup would drift. Verified: zero <text> elements, renders identically with no webfont loaded, legible down to 16px on light and dark. - app/opengraph-image.png + app/twitter-image.png (1200x630) using that lockup, with matching .alt.txt files. Verified against a production build: og:image / twitter:image / og:image:alt all emit, images serve 200 at the right dimensions. * chore(sprint): /fresh-eyes skill + Codex cold-start export + branch audit Cleanup sprint before handing the repo to a Codex pass. New skill `/fresh-eyes` — the inverse of `/INPUT`. Where INPUT ingests outside material INTO the project, fresh-eyes exports verified state OUT to a reader with no context, then stares at the repo the way that reader will. Two jobs, and the second is the one people skip: the export, and naming what our own fluency hides. Every claim must be verified live in the run that writes it — a handoff is read by someone who cannot check your work, so a stale claim is not caught, it is acted on. Contract is a blind-spot ledger: VERIFIED / LANDMINE / ASSUMED / UNOWNED / DECIDE / DRIFT. First run of it produced docs/handoff/CODEX-SPRINT-2026-08-23.md, and turned up one thing that matters more than the sprint: PRODUCTION IS A MERGE BEHIND MAIN. PR #259 merged at 19:23 UTC and produced no Vercel build at all. Last production deploy is #258, from the previous day. Live site confirms it — /opengraph-image.png 404s and /moral-intention still 200s instead of redirecting. Main auto-deployed three times the day before, so this is an anomaly, not configuration. Needs a human redeploy of main@5dc4c9c. Also verified and recorded: - Slack OAuth is complete end to end (SLACK-1 slice 2) — install path, interactive approve/deny, SlackInstall under RLS, fail-closed 503 when the signing secret is unset, five test files. Documented in the export. - Seven remote branches are fully absorbed into main. Each verified by locating its PR's squash commit, and for fix/hero-noncustodial (whose #256 was stranded and re-landed via #257) by content comparison. The delete command and every branch tip are recorded so it is reversible — the sandbox classifier denies the deletion, so it is queued for Eric. - Five remaining branches carry real unmerged work and are listed with what each holds, so nobody prunes them by mistake. AGENTS.md gains a dated note: a merge to main is not a deploy. Close an arc by checking production, not the merge. Gate: tsc 0, eslint 0, 1189 tests passing. * fix(ci): rebuild the MCP bundle so main is green again CI has been red on main for 20 consecutive runs across six days — since the v0.8.0 release (#244, 2026-08-19). Every PR since, mine included, merged straight through it. The cause is one line. `lib/mcpServer.ts` exports MCP_SERVER_VERSION, esbuild INLINES it into packages/sanction-mcp/mcp-server.js, and #244 bumped the source without rebuilding the bundle. CI regenerates the bundle and runs `git diff --exit-code packages/` as its LAST step, so every run went green through 1,191 tests and 90% coverage and then failed on: -var MCP_SERVER_VERSION = "0.7.0"; +var MCP_SERVER_VERSION = "0.8.0"; which reads like a flake at the end of a green run, and got treated as one. The published npm artifact is fine — the workflow builds fresh. What was wrong was the artifact checked into the repo. The second cost is the one worth fixing properly: a permanently-red main means nobody can distinguish a NEW failure from the standing one. The merge gate was decorative for six days. So this does three things, not one: 1. Rebuilds the bundle. `npm run build:mcp && git diff --exit-code packages/` now passes locally, which is CI's exact check. 2. /cut-release gains a step: regenerate every artifact that EMBEDS the version, not just the ones that declare it, with the reason and the verify command. The ritual is why this recurred; the ritual is where it gets fixed. 3. AGENTS.md gains a dated note: a red main is an incident, not a backlog item. Never merge into a red main assuming the red is "the known one" without reading the log. Also corrects the version-literal list in the 2026-08-23 handoff export, which omitted the generated bundle — the exact omission that caused this — and wrongly listed lib/walletCard.ts, which derives the constant rather than restating it. Gate: tsc 0, eslint 0, 1191 tests passing. * fix(deps): clear all 16 advisories without downgrading Prisma The `Dependency audit (high+)` CI step is `continue-on-error: true`, so a green check on it means "did not block", not "clean". 16 advisories (12 high, 4 moderate) accumulated behind that green check, several of them request-path: Next.js SSRF in rewrites, cache confusion on bodied requests, DoS in Server Actions; undici response desync; postcss source-map path traversal. Cleared in three separable steps so any regression is attributable: 1. `npm audit fix` — 13 semver-compatible: brace-expansion, fast-uri, ip-address, js-yaml, nanoid, undici, hono, @hono/node-server, valibot, prisma, @prisma/dev and friends. 16 -> 6. 2. next 16.2.9 -> 16.3.3, which also clears postcss and sharp. This is a MINOR bump inside Next 16, not the major upgrade the audit output implies — npm says "outside the stated dependency range" only because package.json pins the version exactly, with no caret. 6 -> 3. 3. `overrides: { "deepmerge-ts": "^8.0.2" }` for the last three. Step 3 needs explaining, because npm's own suggestion here is a trap. For the deepmerge-ts stack-exhaustion advisory npm proposes `prisma@6.12.0` — a MAJOR DOWNGRADE from the installed 7.10.0, which would tear out the Prisma 7 driver adapter the entire data layer runs on. The real constraint is that @prisma/config@7.10.0 hard-pins deepmerge-ts to exactly 7.1.5, while the patched line is 8.0.2. An override fixes it and keeps Prisma 7. Result: `npm audit` reports 0 vulnerabilities, Prisma stays at 7.10.0. Verified, not assumed: - gate: tsc 0, eslint 0, 1191 tests passing - DB tests: 20/20 under CI's exact env, RLS isolation and e2e data plane included (they fail locally only because the dev role cannot CREATE ROLE) - prisma generate / validate / migrate status all load prisma.config.ts through the overridden @prisma/config — 38 migrations, schema up to date - next build clean; next start + Chromium on /, /platform, /slack: all render correctly, zero page errors (only console noise is the Vercel Analytics script, which 404s off-Vercel by design) - the regenerated MCP bundle still reports 0.8.0 and answers `initialize` over stdio; bundle-freshness check passes The bundle diff is large because esbuild inlines the upgraded transitive deps; it is committed here, per the lesson from the last PR. Adds .github/dependabot.yml — there was none, which is why this rotted unseen. Weekly, grouped, majors excluded from groups so each gets its own gate. The audit step stays non-blocking on purpose: making it blocking would let a new upstream advisory redden main with no commit behind it, which is the exact failure that made the merge gate decorative for six days. * docs(connect): decide before the token is minted Vercel Connect reached GA 2026-08-25 — runtime-minted short-lived scoped credentials, 100+ connectors, replacing stored provider secrets. Verified against vercel.com/docs/connect, not the launch post. It closes a problem Sanction never claimed to solve, and leaves one open. Read the token request wire format: the requested `scopes` come from the CALLER. Connect authenticates the requester via deployment OIDC and issues a correctly-scoped, correctly-expiring credential. It is an issuer, not a decision point. An agent in a loop asking for a legitimate scope 4,000 times gets 4,000 correctly-scoped, correctly-expiring tokens. The composition needs no engine change: a Connect token request IS a capability acquisition, and POST /v1/authorize/capability already governs those against ordered namespaced rules — the same ladder as skill:install:* and api:host/path. Namespace it `connector:<connector>:<scope>`, authorize, then call getToken. Denied or escalated, nothing is minted; on approval the single-use grant replays as grant_id. Capability rules inherit down the wallet tree, so a department can tighten what the org allows. Ships with the boundary stated plainly, including two things the pattern does NOT do: it is cooperative, not enforcement at the issuer (an agent that skips the call still gets a token), and it cannot narrow a scope — Sanction answers yes or no on what the caller asked for. Also records where the vault still applies: agents that do not run on Vercel, and broker-held upstreams. Where Connect applies, prefer it — a credential that never exists at rest beats one encrypted at rest. Hold the mandate, not the rail: Connect owns issuance, Sanction owns the decision and the record. The follow-on arc is captured in the backlog — enforcement at the issuer via the existing AuthZEN PDP, which is the same veto-only posture as STABLE-2, and worth a conversation with Vercel rather than speculative code. Gate: tsc 0, eslint 0, 1191 tests passing. * release: v0.9.0 — the engine stops asking permission to enforce The cut: version bump + changelog stamp + regenerated artifacts, nothing else. Every version literal moves together this time: package.json, package-lock.json, sdk/package.json, packages/sanction-mcp/package.json, packages/sanction-mcp/server.json (both fields), public/.well-known/mcp.json, lib/mcpServer.ts, tests/wallet-card.route.test.ts — and the esbuild bundle packages/sanction-mcp/mcp-server.js REGENERATED so the inlined MCP_SERVER_VERSION says 0.9.0. That last step missing is what kept CI red for six days after v0.8.0; /cut-release now carries it as a step and this commit is its first exercise. The pack since v0.8.0, verified against code not memory: - BROKER-1: /mcp/broker/<upstream> authorizes every tools/call before a byte reaches the upstream; upstream credentials vaulted server-side. - STABLE-1: POST /v1/authorize/quote prices x402 challenges through the ladder; the broker withholds refused challenges. USD-pegged only, worst case wins. - STABLE-0: settlement {rail, asset, network} on the ledger. - MONO-0: per-wallet monthly decision counter, replay-safe. - INHERIT-1: tool + capability rules inherit down the wallet tree — tighten, never loosen. - COND-1: outside_hours_utc and after_model_calls_today predicates, signals snapshotted into evidence for replay. - Slack: /slack install surface; OAuth-bound workspace + channel, bot token under the wallet's SEC-1 envelope, interactive Approve/Deny through resolveApproval. - sanction-sdk on npm; Python LiteLLM logger in-repo. - Security: all 16 advisories cleared (npm audit: 0), Next 16.3.3, Dependabot standing guard. Gates run for this cut: npm run check (1191 passing), sdk typecheck + build + 55 tests, npm pack --dry-run for both packages (sanction-sdk 0.9.0, sanction-mcp 0.9.0), build:mcp + git diff --exit-code packages/. --------- Co-authored-by: Claude <noreply@anthropic.com>
* feat(engine): COND-1 — rules that read the clock and the meter Context-conditional tool rules, the second approved engine arc. A tool rule can carry a `when` — a CLOSED vocabulary, exactly one predicate per rule, restrictive effects only (an allow is never conditional, so allow-list semantics never depend on a clock or a counter). No DSL, no expressions: composition is more rules, which stays simulable and explainable. Two predicates ship: - outside_hours_utc: [start, end) UTC, wrapping midnight — "deploys escalate outside business hours". - after_model_calls_today: N — the runaway breaker: once the agent's persisted model calls pass N today, block (or escalate) matching tools. One honest correction from the design doc, recorded there: 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 runaway loops this exists to stop. Determinism (ADR-0009): signals are captured ONCE per request by the enforcement shell — the UTC hour, and the token-log count fetched ONLY when some layer actually conditions on it — passed into the pure ladder, and persisted in the decision's evidence. Replay reads the stored snapshot, never the live clock. A rule whose signal is absent from a context is deterministically inactive, so pre-COND rows replay unchanged. Precedence: block → conditional block → allow-list → escalate → conditional escalate. Conditions ride the INHERIT-1 layers, so an org-level breaker binds every team below; parity on the AuthZEN PDP with the same signal contract. New Policy.toolConditions Json column (migration included), validated in lib/policy.ts (one predicate, hour bounds, start != end), writable through the existing PATCH /wallets/policy and console actions via policyInputSchema. OpenAPI documents the shape. Console: tool-posture card counts conditionals; the inherited card names them. Simulate boundary, stated not hidden: the tool ladder predates SIM-1's simulatable set; candidate tool_conditions echo back in ignored_fields. Verified: 13 new unit tests (window boundaries + midnight wrap, threshold edges with counts in the reason, absent-signal inactivity, precedence, prefix-glob), 4 route tests (threshold deny with signals persisted in evidence; the counter queried only when conditioned on; a parent's conditional rule binding the child), and both predicates live-fired in both directions against the seeded org — including catching and redoing a false positive where an unconditional escalate masked the conditional one. Policy page render-checked. Truth surfaces: TRACEABILITY COND-1 row + suite map, DOMAIN, roadmap Now, changelog, design doc stamped with the correction, backlog checked off. Gate: tsc clean, eslint 0 errors, 1130 passing. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Eb6zTfLyE42dYS8tgWfvn * feat(broker): BROKER-1 — the MCP broker intercepts tools/call /mcp/broker/<upstream> fronts registered MCP servers: every tools/call is authorized through the tool ladder (inheritance + conditions included) before it forwards; refusals come back as MCP isError results the model can read and replan on; escalations name the request_id and the _meta["sanction/grant_id"] retry path, and a redeemed grant is stripped before forwarding. Batched requests containing tools/call are refused — fail closed, never around. For brokered traffic, "a hijacked agent cannot act" is now enforcement, not cooperation. - lib/broker.ts: upstream registry (SEC-1-vaulted under mcp:<name>), request classification, forwarding with fresh outbound headers — the agent's Sanction key never reaches an upstream, and vaulted upstream credentials never reach the agent. - app/mcp/broker/[upstream]/route.ts: authorization runs as an in-process call to the /authorize/tool route handler — one enforcement shell by construction, no drift between broker and REST decisions. - app/api/v1/broker/upstreams: owner-plane register/list/revoke. - Found and fixed shipped bug: the gateway's provider-key vault read ran outside withTenant, so FORCE RLS silently returned null and connected providers 401'd PROVIDER_NOT_CONNECTED. Now tenant-scoped (and the gateway test mock now exercises withTenant). - Honesty flip: Wallet Card now says enforcement cooperative+broker, interception gateway+mcp-broker, with the scope stated plainly — traffic that bypasses the broker is not governed. - Version literals: walletCard/mcp.json/server.json now 0.8.0 (walletCard reads MCP_SERVER_VERSION instead of a hardcoded string). - Truth surfaces: README, roadmap, changelog, DOMAIN, AGENT-WALLET, TRACEABILITY, OpenAPI, backlog annotation. Tests: 17 new (broker classification/refusal + route: blocked never forwards, escalation carries the grant path, allowed forwards with upstream auth only, grant redeemed+stripped, batch refused). Full suite 1147 passing. Co-Authored-By: Claude <noreply@anthropic.com> * docs(plans): stablecoin rails — design position + backlog capture Eric's direction: agents' authorized spend settles in regulated stablecoins (USDC most likely), not card-rail USD — build the path. Position: stablecoin settlement is irreversible, so pre-transaction authorization becomes the only control point; the rail shift makes the engine more necessary. Slices STABLE-0..3 (settlement metadata → x402 spend gate → wallet-provider co-signer → onchain mirror), custody explicitly rejected. Market state verified 2026-08-22 (x402 under the Linux Foundation w/ Stripe/Visa/Mastercard, AP2 v0.2 mandates, GENIUS Act). Co-Authored-By: Claude <noreply@anthropic.com> * docs(plans): CLARITY Act — Section 604 makes veto-only the legal architecture Market-structure bill (House-passed, Senate cloture 2026-09-15): its Section 604 codifies that non-custodial, non-controlling developers are not money transmitters. Adopt the design constraint now: Sanction's co-signing (STABLE-2/3) is veto-only — it can refuse a transfer the wallet initiates, never initiate or effectuate one. Co-Authored-By: Claude <noreply@anthropic.com> * docs(plans): monetization + distribution — five-discipline synthesis Companion to stablecoin-rails.md, from a UX / financial / growth / brand / competitive research fan-out. Position: if Sanction meters, the unit is the decision (bps on spend rejected — wrong economics, wrong optics); Zapier-style bundles; self-serve tier payable via x402/USDC itself. Trust surfaces (fee meter, operator-set fee cap, per-decision receipts, grace mode) precede any invoice. Distribution ranked: MCP registries, /govern pages, Slack App Directory, x402/AP2 ecosystem listing (no policy layer listed there today), n8n before Zapier. First slices MONO-0 / DIST-0. Co-Authored-By: Claude <noreply@anthropic.com> * feat(engine): STABLE-0 rail-aware ledger + MONO-0 decision meter STABLE-0 (docs/plans/stablecoin-rails.md): POST /v1/authorize accepts optional settlement {rail, asset, network} — a closed vocabulary (lib/settlement.ts; a chain network is refused on a non-x402 rail so evidence cannot lie). Inert to the decision like attribution tags, rides detailsJson, surfaces in the audit feed and CSV, documented in OpenAPI. Stablecoin settlement is irreversible, so pre-transaction authorization is the only control point — the ledger is rail-aware before any rail-specific enforcement exists. MONO-0 (docs/plans/monetization-and-distribution.md): per-wallet, per-UTC-month WalletDecisionCounter incremented once per FRESH engine decision across all four authorize shells (approve/escalate/deny alike, observe included). Idempotent replays, grant redemptions, simulate runs, and frozen short-circuits never count; increments run via after() and swallow errors — metering can never fail or slow a decision. Exposed as month.decisions on /v1/wallets/stats and a Decisions (month) card on the spend console. Instrumentation only: no fee attaches — the unit is measured before it is ever priced. Found and fixed while wiring: the provision route caught a subtree-cap breach INSIDE its transaction, committing the ancestor counter increments the reservation loop had already written — phantom pool spend on a denied provision (the same defect the spend route's D1 fix removed). It now lets the transaction roll back and persists the denial outside, evidence attached, mirroring the spend route. Tests: 14 new (closed-vocab settlement accept/reject, persistence, meter counts fresh approve+deny once, replay/simulate never count, meter failure swallowed, month key UTC, D1-parity provision regression). Full suite 1161 passing. Live-fired: x402/usdc/base persisted on an approved row, off-vocab rail 400s, counter=2 after one spend + one allowed tool call, console card render-checked. Co-Authored-By: Claude <noreply@anthropic.com> * docs: dated model note (Pro $20 metered middle) + GTM arc backlog entry Eric's call 2026-08-22: the model gains a metered middle — Pro $20/mo, metered in decisions, early access only until real billing ships (no checkout; trust surfaces precede any invoice). Free and enterprise framing unchanged. GTM launch arc captured in the backlog. Co-Authored-By: Claude <noreply@anthropic.com> * feat(site): blog — first post: the rail is changing, the decision is the unit New /blog (index + slug pages, changelog conventions: typed array in lib/blog.ts, shared Markdown rendering, static params + per-post metadata). Post #1 announces the August arc honestly: broker interception, the settlement-aware ledger, the decision meter, the GENIUS/CLARITY ground and the veto-only architecture, with the x402 spend gate and Pro tier explicitly marked next-not-shipped. Co-Authored-By: Claude <noreply@anthropic.com> * feat(site): platform goes stablecoin-era — rails section + Pro pricing New #rails section under the canonical line ("Sanction authorizes the spend; any rail settles it"): the irreversibility argument, the settlement-aware ledger marked live, the x402 spend gate marked next (never overclaimed), and the non-custodial/veto-only regulatory posture (GENIUS as instrument law, CLARITY as pending). Pricing becomes three cards: Free (unchanged) / Pro $20/mo early access (metered in decisions, lead-capture CTA, no checkout) / Enterprise Agreement. Disambiguation line: sanction (v.) — to give official authorization. Co-Authored-By: Claude <noreply@anthropic.com> * docs(gtm): launch playbook + integration use-case catalog as HTML Two self-contained HTML documents under docs/gtm/: the 14-day launch playbook (positioning, sequence, the CLARITY-vote play, content engine, Pro funnel, metrics) and "Where Sanction sits" — seven integration architectures with pre-rendered sequence diagrams (static SVG, no runtime dependencies; open in any browser). Co-Authored-By: Claude <noreply@anthropic.com> * docs: relocate GTM documents to the private strategy repo Strategy/GTM material does not belong in the public repo (same call as the 2026-07-06 move) — the launch playbook and use-case catalog now live in ericlovold/sanction-strategy under docs/gtm/. Co-Authored-By: Claude <noreply@anthropic.com> * feat(engine): STABLE-1 — the x402 spend gate, authorized before the wallet signs An x402 402 is a demand for money, and stablecoin settlement is irreversible: the moment between receiving a payment challenge and signing it is the only control point there is. STABLE-1 takes that moment. Two surfaces, one shell (both call POST /v1/authorize in-process, so a quote can never drift from ordinary spend): - Cooperative: POST /v1/authorize/quote — post the 402 body you just received; Sanction prices it and runs the same ladder (budgets, escalation, grants, cascade caps, evidence, decision meter), with STABLE-0 settlement metadata derived from the quote itself. - Intercepting: the MCP broker gates upstream 402s and, on anything but an approval, WITHHOLDS the challenge. The refusal type structurally cannot carry payTo/maxAmountRequired — an agent cannot sign payment requirements it never received. Two rules keep pricing honest and deterministic: - No oracle, no FX. A quote is priced only when its asset is a USD-pegged stablecoin with known decimals (native USDC on Base; address + 6 decimals verified against Circle). Anything else denies as QUOTE_NOT_PRICEABLE rather than being guessed — a decision must never depend on a rate looked up at decision time (ADR-0009). - Worst case wins. A challenge offering several ways to pay is authorized at its most expensive option, and one unpriceable option poisons the whole challenge: the client picks, and we cannot know which. Still non-custodial and veto-only — no keys, no signing, refusal only, which is the shape CLARITY §604's non-controlling test describes. Tests: 28 new (pricing, worst-case selection, poisoned challenges, the gate's withholding contract, quote route, broker interception). Full suite 1189 passing. Live-fired against Postgres: $0.05 and $15 approved, $900 denied PER_TXN_LIMIT, a multi-option challenge authorized at its worst case, every row carrying settlement metadata. Truth surfaces: /platform now says the gate is live (was "next"), changelog, roadmap, TRACEABILITY, OpenAPI, README, backlog, design doc. Co-Authored-By: Claude <noreply@anthropic.com> * feat(brand): favicon and app icons from the new wordmark; re-land the hero fix Icons were still the old shield-and-checkmark mark. The whole icon set is now generated from the $ glyph in Paul's wordmark (the three green paths of sanction-wordmark-green.svg, bounds measured with getBBox rather than eyeballed), on the brand's own pine ground with the wordmark's signal green: app/icon.svg 512, rounded square (browser tab) app/favicon.ico 16 + 32, PNG-in-ICO app/apple-icon.png 180 public/icons/icon-192|512.png PWA "any" public/icons/icon-maskable-512.png glyph inside the 80% safe zone manifest theme/background move to pine so the PWA splash frames the icon in the same dark it uses. Also re-lands the hero fix from #256, which was stranded: it merged into the marketing branch after that branch had already been squashed into main, so main still read "AVAILABLE $2,500.00". Sanction is non-custodial and veto-only, so the card states the limit — "DAILY CAP / ENFORCED". Verified: icon legible at 16px on light and dark tab strips; /, /icon.svg, /favicon.ico and /apple-icon.png all 200 with the right content types. Caught in render-check and fixed: Next decodes favicon.ico and requires RGBA, and an RGB one 500s every page — noted in AGENTS.md along with the production-deploy-path and stacked-PR lessons. Co-Authored-By: Claude <noreply@anthropic.com> * feat(marketing): drop the certification strip from the homepage The "ANTHROPIC CERTIFIED" trust strip (and the "Also certified: Clio Legal AI Fundamentals" line under it) belonged to the services-firm framing. On the reframed page it reads as personal credentials on what is now a product homepage, and it sat between the hero and the argument it leads into. Removes the section and deletes components/anthropic-certs.tsx — its own header said it served the homepage and the consulting page, and the consulting page has since split out, so nothing else referenced it. Git history keeps it if it wants a home on the services site. Verified: rendered homepage contains no ANTHROPIC CERTIFIED / Clio / cert-pill markers; hero, wallet card, and the "Why authorization comes first" section all intact. Co-Authored-By: Claude <noreply@anthropic.com> * feat(marketing): getsanction.com is the authorization product only The Moral Intention Analyst has its own space now, so it comes off this site entirely — one product, one story. - deletes app/moral-intention/ (page + reveal component) - homepage: drops the /moral-intention footer link - /about: removes the "Moral Intention" nav and footer links, the MIA sentence from the thesis paragraph, MIA from the metadata description and from Eric's bio, and Dr. A.C. Ping's team card together with the ac-ping photo placeholder — the ethics authorship belongs with MIA wherever MIA now lives, not on the authorization product's site No references remain anywhere in app/, components/, lib/ or public/. /moral-intention now 404s; if the new home has a public URL a permanent redirect there would be better than a dead end for anything already linking to it. Verified: /, /about, /platform, /blog all 200 and carry no MIA, A.C. Ping, acping, ethics-framework or brain-trust residue. tsc 0, eslint 0, 1189 tests passing. Co-Authored-By: Claude <noreply@anthropic.com> * feat(marketing): redirect /moral-intention to its own site moralintention.com is live, so the old page becomes a permanent cross-origin redirect rather than a 404 — anything already linking to it lands on the real thing, and the link equity follows. Sub-paths are preserved too (/moral-intention/:path* -> the same path on moralintention.com), so any deeper link that ever existed still resolves. Verified locally: /moral-intention -> 308 https://moralintention.com/, /moral-intention/analyzer -> 308 https://moralintention.com/analyzer, and /consulting's existing redirect plus /, /about, /platform, /blog are all unaffected. Note the destination itself could not be fetched from this sandbox — its proxy 403s that host — but DNS resolves and the domain is confirmed live by its owner. Co-Authored-By: Claude <noreply@anthropic.com> * feat(brand): lockup carries the category, the hero carries the moment The wordmark alone says nothing about what Sanction does — and to finance readers the name reads as OFAC first. The nav lockup now carries a descriptor: "Sanction | AGENT AUTHORIZATION", inline right with a hairline divider, on the homepage, /about and /platform. That created a second problem worth fixing in the same pass: the hero eyebrow said "Spend governance for autonomous systems" thirty pixels below it, so the page opened with two competing descriptions of the same company. The mark now owns the category and the eyebrow owns the moment — "Runaway agent spend, stopped" — which also sets up the headline instead of restating it. Mechanics: .sn-lockup-tag drops out below 1100px rather than wrapping, where the nav needs its width for links. The tagline is aria-hidden because the wordmark's alt already announces the brand — a screen reader should hear "Sanction", not "Sanction Agent authorization". Render-checked at 1440 / 1180 / 1040 / 640: visible above the breakpoint, gone below it, no wrap at any width. eslint 0 errors, 1189 tests passing. Co-Authored-By: Claude <noreply@anthropic.com> * brand: outlined lockup SVGs + social cards with the descriptor baked in The root site had no Open Graph image at all — layout.tsx declared twitter card "summary_large_image" with nothing to fill it, so a link to getsanction.com rendered a large-image card with an empty image well. Adds: - public/brand/sanction-lockup-{green,white}.svg — the wordmark plus the "Agent authorization" descriptor as VECTOR OUTLINES, not <text>. Off-site consumers (social cards, npm/registry READMEs, decks) do not load our webfonts, so a <text> tagline would fall back to a system font and the lockup would drift. Verified: zero <text> elements, renders identically with no webfont loaded, legible down to 16px on light and dark. - app/opengraph-image.png + app/twitter-image.png (1200x630) using that lockup, with matching .alt.txt files. Verified against a production build: og:image / twitter:image / og:image:alt all emit, images serve 200 at the right dimensions. * chore(sprint): /fresh-eyes skill + Codex cold-start export + branch audit Cleanup sprint before handing the repo to a Codex pass. New skill `/fresh-eyes` — the inverse of `/INPUT`. Where INPUT ingests outside material INTO the project, fresh-eyes exports verified state OUT to a reader with no context, then stares at the repo the way that reader will. Two jobs, and the second is the one people skip: the export, and naming what our own fluency hides. Every claim must be verified live in the run that writes it — a handoff is read by someone who cannot check your work, so a stale claim is not caught, it is acted on. Contract is a blind-spot ledger: VERIFIED / LANDMINE / ASSUMED / UNOWNED / DECIDE / DRIFT. First run of it produced docs/handoff/CODEX-SPRINT-2026-08-23.md, and turned up one thing that matters more than the sprint: PRODUCTION IS A MERGE BEHIND MAIN. PR #259 merged at 19:23 UTC and produced no Vercel build at all. Last production deploy is #258, from the previous day. Live site confirms it — /opengraph-image.png 404s and /moral-intention still 200s instead of redirecting. Main auto-deployed three times the day before, so this is an anomaly, not configuration. Needs a human redeploy of main@5dc4c9c. Also verified and recorded: - Slack OAuth is complete end to end (SLACK-1 slice 2) — install path, interactive approve/deny, SlackInstall under RLS, fail-closed 503 when the signing secret is unset, five test files. Documented in the export. - Seven remote branches are fully absorbed into main. Each verified by locating its PR's squash commit, and for fix/hero-noncustodial (whose #256 was stranded and re-landed via #257) by content comparison. The delete command and every branch tip are recorded so it is reversible — the sandbox classifier denies the deletion, so it is queued for Eric. - Five remaining branches carry real unmerged work and are listed with what each holds, so nobody prunes them by mistake. AGENTS.md gains a dated note: a merge to main is not a deploy. Close an arc by checking production, not the merge. Gate: tsc 0, eslint 0, 1189 tests passing. * fix(ci): rebuild the MCP bundle so main is green again CI has been red on main for 20 consecutive runs across six days — since the v0.8.0 release (#244, 2026-08-19). Every PR since, mine included, merged straight through it. The cause is one line. `lib/mcpServer.ts` exports MCP_SERVER_VERSION, esbuild INLINES it into packages/sanction-mcp/mcp-server.js, and #244 bumped the source without rebuilding the bundle. CI regenerates the bundle and runs `git diff --exit-code packages/` as its LAST step, so every run went green through 1,191 tests and 90% coverage and then failed on: -var MCP_SERVER_VERSION = "0.7.0"; +var MCP_SERVER_VERSION = "0.8.0"; which reads like a flake at the end of a green run, and got treated as one. The published npm artifact is fine — the workflow builds fresh. What was wrong was the artifact checked into the repo. The second cost is the one worth fixing properly: a permanently-red main means nobody can distinguish a NEW failure from the standing one. The merge gate was decorative for six days. So this does three things, not one: 1. Rebuilds the bundle. `npm run build:mcp && git diff --exit-code packages/` now passes locally, which is CI's exact check. 2. /cut-release gains a step: regenerate every artifact that EMBEDS the version, not just the ones that declare it, with the reason and the verify command. The ritual is why this recurred; the ritual is where it gets fixed. 3. AGENTS.md gains a dated note: a red main is an incident, not a backlog item. Never merge into a red main assuming the red is "the known one" without reading the log. Also corrects the version-literal list in the 2026-08-23 handoff export, which omitted the generated bundle — the exact omission that caused this — and wrongly listed lib/walletCard.ts, which derives the constant rather than restating it. Gate: tsc 0, eslint 0, 1191 tests passing. * fix(deps): clear all 16 advisories without downgrading Prisma The `Dependency audit (high+)` CI step is `continue-on-error: true`, so a green check on it means "did not block", not "clean". 16 advisories (12 high, 4 moderate) accumulated behind that green check, several of them request-path: Next.js SSRF in rewrites, cache confusion on bodied requests, DoS in Server Actions; undici response desync; postcss source-map path traversal. Cleared in three separable steps so any regression is attributable: 1. `npm audit fix` — 13 semver-compatible: brace-expansion, fast-uri, ip-address, js-yaml, nanoid, undici, hono, @hono/node-server, valibot, prisma, @prisma/dev and friends. 16 -> 6. 2. next 16.2.9 -> 16.3.3, which also clears postcss and sharp. This is a MINOR bump inside Next 16, not the major upgrade the audit output implies — npm says "outside the stated dependency range" only because package.json pins the version exactly, with no caret. 6 -> 3. 3. `overrides: { "deepmerge-ts": "^8.0.2" }` for the last three. Step 3 needs explaining, because npm's own suggestion here is a trap. For the deepmerge-ts stack-exhaustion advisory npm proposes `prisma@6.12.0` — a MAJOR DOWNGRADE from the installed 7.10.0, which would tear out the Prisma 7 driver adapter the entire data layer runs on. The real constraint is that @prisma/config@7.10.0 hard-pins deepmerge-ts to exactly 7.1.5, while the patched line is 8.0.2. An override fixes it and keeps Prisma 7. Result: `npm audit` reports 0 vulnerabilities, Prisma stays at 7.10.0. Verified, not assumed: - gate: tsc 0, eslint 0, 1191 tests passing - DB tests: 20/20 under CI's exact env, RLS isolation and e2e data plane included (they fail locally only because the dev role cannot CREATE ROLE) - prisma generate / validate / migrate status all load prisma.config.ts through the overridden @prisma/config — 38 migrations, schema up to date - next build clean; next start + Chromium on /, /platform, /slack: all render correctly, zero page errors (only console noise is the Vercel Analytics script, which 404s off-Vercel by design) - the regenerated MCP bundle still reports 0.8.0 and answers `initialize` over stdio; bundle-freshness check passes The bundle diff is large because esbuild inlines the upgraded transitive deps; it is committed here, per the lesson from the last PR. Adds .github/dependabot.yml — there was none, which is why this rotted unseen. Weekly, grouped, majors excluded from groups so each gets its own gate. The audit step stays non-blocking on purpose: making it blocking would let a new upstream advisory redden main with no commit behind it, which is the exact failure that made the merge gate decorative for six days. * docs(connect): decide before the token is minted Vercel Connect reached GA 2026-08-25 — runtime-minted short-lived scoped credentials, 100+ connectors, replacing stored provider secrets. Verified against vercel.com/docs/connect, not the launch post. It closes a problem Sanction never claimed to solve, and leaves one open. Read the token request wire format: the requested `scopes` come from the CALLER. Connect authenticates the requester via deployment OIDC and issues a correctly-scoped, correctly-expiring credential. It is an issuer, not a decision point. An agent in a loop asking for a legitimate scope 4,000 times gets 4,000 correctly-scoped, correctly-expiring tokens. The composition needs no engine change: a Connect token request IS a capability acquisition, and POST /v1/authorize/capability already governs those against ordered namespaced rules — the same ladder as skill:install:* and api:host/path. Namespace it `connector:<connector>:<scope>`, authorize, then call getToken. Denied or escalated, nothing is minted; on approval the single-use grant replays as grant_id. Capability rules inherit down the wallet tree, so a department can tighten what the org allows. Ships with the boundary stated plainly, including two things the pattern does NOT do: it is cooperative, not enforcement at the issuer (an agent that skips the call still gets a token), and it cannot narrow a scope — Sanction answers yes or no on what the caller asked for. Also records where the vault still applies: agents that do not run on Vercel, and broker-held upstreams. Where Connect applies, prefer it — a credential that never exists at rest beats one encrypted at rest. Hold the mandate, not the rail: Connect owns issuance, Sanction owns the decision and the record. The follow-on arc is captured in the backlog — enforcement at the issuer via the existing AuthZEN PDP, which is the same veto-only posture as STABLE-2, and worth a conversation with Vercel rather than speculative code. Gate: tsc 0, eslint 0, 1191 tests passing. * release: v0.9.0 — the engine stops asking permission to enforce The cut: version bump + changelog stamp + regenerated artifacts, nothing else. Every version literal moves together this time: package.json, package-lock.json, sdk/package.json, packages/sanction-mcp/package.json, packages/sanction-mcp/server.json (both fields), public/.well-known/mcp.json, lib/mcpServer.ts, tests/wallet-card.route.test.ts — and the esbuild bundle packages/sanction-mcp/mcp-server.js REGENERATED so the inlined MCP_SERVER_VERSION says 0.9.0. That last step missing is what kept CI red for six days after v0.8.0; /cut-release now carries it as a step and this commit is its first exercise. The pack since v0.8.0, verified against code not memory: - BROKER-1: /mcp/broker/<upstream> authorizes every tools/call before a byte reaches the upstream; upstream credentials vaulted server-side. - STABLE-1: POST /v1/authorize/quote prices x402 challenges through the ladder; the broker withholds refused challenges. USD-pegged only, worst case wins. - STABLE-0: settlement {rail, asset, network} on the ledger. - MONO-0: per-wallet monthly decision counter, replay-safe. - INHERIT-1: tool + capability rules inherit down the wallet tree — tighten, never loosen. - COND-1: outside_hours_utc and after_model_calls_today predicates, signals snapshotted into evidence for replay. - Slack: /slack install surface; OAuth-bound workspace + channel, bot token under the wallet's SEC-1 envelope, interactive Approve/Deny through resolveApproval. - sanction-sdk on npm; Python LiteLLM logger in-repo. - Security: all 16 advisories cleared (npm audit: 0), Next 16.3.3, Dependabot standing guard. Gates run for this cut: npm run check (1191 passing), sdk typecheck + build + 55 tests, npm pack --dry-run for both packages (sanction-sdk 0.9.0, sanction-mcp 0.9.0), build:mcp + git diff --exit-code packages/. * fix(mcp): registry rejects descriptions over 100 chars — trim ours to 96 The v0.9.0 "Publish MCP" dispatch failed at the registry step: 422 validation failed — "expected length <= 100" at body.description The official MCP registry now caps server.json descriptions at 100 characters; ours was 117. This is also why the v0.8.0 registry publish failed on 2026-08-19 — the registry listing has been stale at 0.7.0 for six weeks while npm moved on, because the workflow publishes npm first and the registry step failed silently after it. npm itself is fine and already live: sanction-sdk@0.9.0 and sanction-mcp@0.9.0 are both latest, and npx sanction-mcp@0.9.0 from a clean directory answers initialize reporting 0.9.0. Trimmed to 96 chars, same voice, nothing else touched. After merge, re-running Publish MCP is safe end to end: the npm step skips versions that already exist, so only the registry publish executes. * fix(slack): one card per escalation, and buttons on every card Review sprint before the first real Slack install. Two defects found by tracing install -> escalation -> delivery -> click, both of which would have surfaced in the first minute of dogfooding: 1. DOUBLE CARDS. Every escalation site fires the approval.created + escalation.created PAIR (and approval.resolved + escalation.resolved on resolve), and the SlackInstall model's default event list subscribes to all four. So a fresh install received TWO interactive cards per escalation and two notices per resolve — four messages per lifecycle, half of them duplicates, each created card carrying its own working Approve/Deny buttons for the same approval. Delivery now collapses the alias pairs per install: the escalation.* member yields to its approval.* twin whenever the install would receive that twin anyway (explicitly or via "*"). An install subscribed ONLY to escalation.* still gets its card. Legacy webhook routes are unchanged — they subscribe to explicit lists. 2. BUTTONLESS CARDS. The interactive Approve/Deny buttons require an action token, which requires approval_id in the event data. The spend and provision shells pass it; the TOOL and CAPABILITY shells never did — their PendingApproval was created and discarded, so their Slack cards fell back to a deep link with no buttons. Both routes now return the approval id from their escalation transaction and carry it on both events. (The approval helpers already returned the created row; the routes were dropping it.) Tests: two new delivery tests pin the dedupe (default-events install gets exactly one post per pair, wildcard included; an escalation-only subscriber still receives its card). The broker route test's createToolPendingApproval mock now returns the created row, as Prisma does — the route rightly assumes create() returns what it created. Gate: tsc 0, eslint 0, 1193 passing. --------- Co-authored-by: Claude <noreply@anthropic.com>


Two things, plus the ops notes that stop both of them recurring.
1. The icon set is now Paul's mark
Every icon was still the old shield-and-checkmark — #253 swapped the wordmark in the nav and sidebar but nothing regenerated the favicon. All of these are now derived from the
$glyph insanction-wordmark-green.svg(its three green paths, with bounds measured viagetBBoxin Chromium rather than eyeballed), on the brand's own pine ground--pine-9 #0c332awith the wordmark's--signal #10b981:app/icon.svgapp/favicon.icoapp/apple-icon.pngpublic/icons/icon-192.png,icon-512.pnganypublic/icons/icon-maskable-512.pngmanifest.tstheme/background move from#0a0a0ato pine, so the PWA splash frames the icon in the same dark rather than a slightly different one.Verified legible at 16px on both light and dark tab strips, and
/,/icon.svg,/favicon.ico,/apple-icon.pngall return 200 with the right content types.2. Re-lands the hero fix that got stranded
#256 was merged — into the marketing branch, after that branch had already been squashed into
main. So it looked merged while being absent from the product:mainstill readAVAILABLE $2,500.00, and so did production. Confirmed withgit merge-base --is-ancestor 8ce2f67 origin/main→ not an ancestor.Same change as #256, now based on
main: the card readsDAILY CAP / ENFORCED. Sanction is non-custodial and veto-only — a held-balance figure contradicts the position the page argues, in front of the audience most likely to ask the custody question.A real breakage caught by render-check
The first favicon build 500'd every page. Next.js decodes
app/favicon.icoto build icon metadata and throwsThe PNG is not in RGBA format— and that failure isn't scoped to the icon, it takes down the whole app. Chromium screenshots are RGB unless captured withomitBackground: true. Rebuilt as RGBA;file app/favicon.iconow reports RGBA, and/returns 200.Ops notes added to AGENTS.md
Three dated entries, because each of these cost real time today:
main. The marketing branch was promoted straight to production, so getsanction.com ran code that wasn't inmain— any routine merge would have silently reverted the live homepage and the "x402 spend gate is live" banner.merge-base --is-ancestorcheck to catch it.app/favicon.icomust contain RGBA PNGs, with the verification command.Gate
tsc --noEmit0 ·eslint0 errors · 1189 tests passing · homepage and all icon routes render-checked locally.🤖 Generated with Claude Code
https://claude.ai/code/session_013Eb6zTfLyE42dYS8tgWfvn
Generated by Claude Code
Note
Low Risk
Brand assets, marketing copy, and agent ops notes only. The favicon RGBA requirement is a rendering-correctness fix, not a security or data-path change.
Overview
Replaces the leftover shield-and-checkmark icon set with the
$wordmark on pine (#0c332a), and re-lands the homepage wallet readout that never made it ontomain.Tab, Apple, and PWA icons (
app/icon.svg, favicon, apple-icon,public/icons/*) now use the green glyph on brand pine. PWAtheme_color/background_colormatch. Favicon PNGs are RGBA so Next.js icon metadata no longer 500s the whole app.The hero wallet card no longer shows AVAILABLE $2,500.00. It reads DAILY CAP / ENFORCED, so the visual does not look custodial.
AGENTS.mdadds dated ops notes: deploy production frommain, do not stack PRs onto a branch about to squash-merge, and keep favicon PNGs in RGBA.Reviewed by Cursor Bugbot for commit 7a2886e. Bugbot is set up for automated code reviews on this repo. Configure here.