Skip to content

fix(authzen,mcp): surface parity — one engine, every surface (freeze, CPO, token horizons, MCP loop)#156

Merged
ericlovold merged 1 commit into
mainfrom
claude/authzen-mcp-parity
Jul 9, 2026
Merged

fix(authzen,mcp): surface parity — one engine, every surface (freeze, CPO, token horizons, MCP loop)#156
ericlovold merged 1 commit into
mainfrom
claude/authzen-mcp-parity

Conversation

@ericlovold

@ericlovold ericlovold commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Closes the cross-surface drift a code review surfaced: the AuthZEN PDP and the MCP tools were built before freeze/CPO/token-horizons landed on the native path, and had quietly fallen behind — so the same request could get a different decision depending on which surface asked. That breaks the determinism guarantee (same request + same policy + same state ⇒ same decision) the product sells, and one gap was a real security hole. This lands the fix that was verified on an earlier branch, rebased clean onto current main.

What was broken on the live surfaces

  • Freeze bypass (security). A frozen wallet still got decision:true from POST /access/v1/evaluation, and the AARP grant-redeem path would consume a grant and ledger approved spend on a frozen wallet — while every native route hard-denies WALLET_FROZEN first. The one-control kill switch didn't hold on the PDP.
  • CPO ceiling not enforced on the PDP. A wallet over its cost-per-outcome ceiling was throttled by /authorize but decision:true from the PDP — an enforcement point could keep spending through a throttled channel.
  • Token caps under-enforced. POST /tokens (and MCP sanction_log_tokens) enforced only the seat daily budget — not the seat monthly or the pooled subtree-daily cap — so that path could outspend a cap it still drains for gateway siblings.
  • MCP escalations were dead ends. sanction_authorize/_provision told the agent to "retry with the grant_id" but never surfaced request_id and offered no way to poll for the grant; and config/freeze failures rendered as a generic "DENIED."

The fix

  • Freeze gate in evaluateAuthZen — fetches the ancestor chain once, denies WALLET_FROZEN before the redeem-write and before the ladder, exactly like the native routes.
  • CPO paritydecidePolicy/decideProvisionPolicy thread the cpo context; SPEND_LADDER/PROVISION_LADDER include costPerOutcomeRule (no-op without a ceiling), so the PDP throttles identically to /authorize. Shared readSpendState now fetches cpoContext.
  • /tokens three-horizon enforcement — seat daily, seat monthly, pooled subtree-daily; the 402 names which line hit.
  • MCP loop — new sanction_check_authorization tool polls an escalation for its grant; one shared renderAuthResult surfaces request_id and the real error/WALLET_FROZEN code instead of a generic denial.
  • AARP — Idempotency-Key now required on access-request (interim replay guard).
  • Cross-surface parity test suite — asserts the PDP and /authorize agree under freeze and over-CPO, so the surfaces can't silently drift again. Version → v0.5.0 + changelog entry.

Verification

Deferred to a follow-up (already in docs/BACKLOG.md): batch grant-atomicity refactor, binding-token single-use (jti), PDP rate-limiting, AARP timeout-approve grant, empty-batch guard — the deeper review findings that need real design.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UEw1RdHk5bmphKxr6N5mJY


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Added a new way to check the status of escalated authorization requests and retrieve approval details when available.
    • Token spending now respects daily, monthly, and shared pool limits with clearer error feedback.
  • Bug Fixes

    • Requests to submit access denials now require a retry-safe header, preventing duplicate submissions.
    • Authorization results now show more accurate denial and escalation messages, including frozen-wallet handling and cost-per-outcome limits.
  • Chores

    • Updated release version to 0.5.0.

…; MCP loop + v0.5.0

Closes the review's cross-surface drift: the AuthZEN PDP and MCP tools now
answer identically to /authorize (determinism invariant).

- Freeze (KILL-1): evaluateAuthZen fetches the ancestor chain once, denies
  WALLET_FROZEN before the redeem-write and before the ladder — no spend or
  grant redemption on a frozen wallet via the PDP.
- CPO-1: decidePolicy/decideProvisionPolicy thread cpo; SPEND_LADDER +
  PROVISION_LADDER include costPerOutcomeRule (no-op without cpo), so the PDP
  throttles a breached ceiling like the native route. Shared readSpendState
  now fetches cpoContext.
- /tokens: enforces the same three horizons as the gateway wall — seat daily,
  seat monthly, pooled subtree-daily — so MCP sanction_log_tokens can't
  outspend a cap it drains for gateway siblings. 402 names the horizon.
- MCP: new sanction_check_authorization tool polls an escalation for its
  grant (spend/provision escalations were dead ends); one shared
  renderAuthResult surfaces request_id + real error/WALLET_FROZEN codes
  instead of a generic denial; log_tokens description corrected.
- AARP: Idempotency-Key now required on access-request (replay guard until
  binding-token jti single-use lands).
- Tests: cross-surface parity suite (freeze + CPO on the PDP) and /tokens
  horizon route tests pin the invariant so the surfaces can't drift again.
- Docs: package.json 0.2.0 → 0.5.0; changelog v0.5.0 entry.

Deferred to sprint 2 (queued): batch grant-atomicity refactor (move redeem to
the route shell), binding-token jti single-use, AARP timeout-approve grant,
PDP rate-limiting, empty-batch fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UEw1RdHk5bmphKxr6N5mJY
@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@vercel

vercel Bot commented Jul 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sanction Ready Ready Preview, Comment Jul 9, 2026 5:01pm

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds Idempotency-Key enforcement with replay to the access-request route, multi-horizon (daily/monthly/subtree-daily) token budget enforcement to the tokens route, cost-per-outcome throttling and early wallet-freeze denial to AuthZEN policy evaluation, a shared renderAuthResult helper plus a new sanction_check_authorization polling tool in both MCP servers, and a version/changelog bump.

Changes

Idempotency-Key Enforcement

Layer / File(s) Summary
Idempotency requirement and replay
app/api/access/v1/access-request/route.ts, tests/authzen-aarp.route.test.ts, tests/richDenials.route.test.ts
Route requires Idempotency-Key, returns 400 if missing, and replays the existing decision when a matching agentId+key request exists; tests cover the header requirement and helper wiring.

Multi-Horizon Token Budget Enforcement

Layer / File(s) Summary
Horizon budget computation and responses
app/api/v1/tokens/route.ts
Computes daily, monthly, and pooled subtree-daily caps via ancestor chain/subtree helpers inside an advisory-lock transaction and returns horizon-specific 402 fields.
Horizon test coverage
tests/tokens-horizons.route.test.ts
New suite asserts 402 responses per horizon with correct fields and a 200 success path with token log creation.

AuthZEN CPO Throttling and Wallet Freeze Denial

Layer / File(s) Summary
CPO field plumbing
lib/decisions.ts, lib/rules/provision.ts, lib/rules/spend.ts
DecideInput gains an optional cpo field forwarded into ladder evaluation; costPerOutcomeRule inserted before ladderRule in both ladders.
Freeze detection and CPO context in evaluation
lib/authzen.ts
PolicyShape gains CPO config fields; readSpendState returns cpo context instead of ancestorChain; evaluateAuthZen computes ancestorChain once and returns WALLET_FROZEN early.
Parity tests
tests/authzen-parity.test.ts
New tests confirm matching WALLET_FROZEN and COST_PER_OUTCOME_CEILING decisions across PDP evaluation and authorize routes.

MCP Authorization Rendering and Escalation Polling

Layer / File(s) Summary
renderAuthResult and check-authorization (TS)
mcp-server.ts
Adds shared renderAuthResult helper for consistent success/denial/escalation text, updates existing tool handlers, and adds sanction_check_authorization polling GET /authorize/{request_id}.
Mirrored changes (JS)
packages/sanction-mcp/mcp-server.js
Applies the same renderAuthResult helper and sanction_check_authorization tool to the built server file.

Changelog and Version Bump

Layer / File(s) Summary
Version 0.5.0 release notes
lib/changelog.ts, package.json
Adds a new changelog entry for 0.5.0 and bumps the package version.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AccessRequestRoute
  participant AuthorizationRequestDB

  Client->>AccessRequestRoute: POST /access-request (Idempotency-Key?)
  alt missing key
    AccessRequestRoute-->>Client: 400 Idempotency-Key required
  else key present
    AccessRequestRoute->>AuthorizationRequestDB: lookup by agentId+idempotencyKey
    alt existing request found
      AuthorizationRequestDB-->>AccessRequestRoute: existing request
      AccessRequestRoute-->>Client: 200 existing task response
    else no existing request
      AccessRequestRoute->>AuthorizationRequestDB: create new request
      AccessRequestRoute-->>Client: task response
    end
  end
Loading
sequenceDiagram
  participant evaluateAuthZen
  participant WalletAncestorChain
  participant readSpendState
  participant decidePolicy

  evaluateAuthZen->>WalletAncestorChain: fetch ancestorChain
  evaluateAuthZen->>evaluateAuthZen: derive freeze state
  alt wallet/ancestor frozen
    evaluateAuthZen-->>evaluateAuthZen: deny WALLET_FROZEN
  else not frozen
    evaluateAuthZen->>readSpendState: read cpo context + spend totals
    evaluateAuthZen->>decidePolicy: decide with ancestorChain + cpo
    decidePolicy-->>evaluateAuthZen: PolicyDecision
  end
Loading
sequenceDiagram
  participant Agent
  participant MCPServer
  participant AuthorizeAPI

  Agent->>MCPServer: sanction_authorize_tool(...)
  MCPServer-->>Agent: escalated (request_id)
  Agent->>MCPServer: sanction_check_authorization(request_id)
  MCPServer->>AuthorizeAPI: GET /authorize/{request_id}
  AuthorizeAPI-->>MCPServer: status approved/pending/denied
  alt approved
    MCPServer-->>Agent: grant_id (retry)
  else pending/escalated
    MCPServer-->>Agent: still awaiting
  else denied
    MCPServer-->>Agent: error with reason
  end
Loading

Possibly related PRs

  • ericlovold/sanction#134: Both PRs order the cost-per-outcome rule before spend/provision ladders and enforce wallet-freeze terminal denial with matching WALLET_FROZEN/COST_PER_OUTCOME_CEILING codes.
  • ericlovold/sanction#149: Both PRs extend multi-horizon token budget enforcement (daily/monthly/subtree-daily) with horizon-specific 402 responses in the tokens route.
  • ericlovold/sanction#94: Both PRs modify the access-request route's Idempotency-Key lookup/replay behavior against existing authorizationRequest rows.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main cross-surface parity work across AuthZEN, MCP, and token horizon enforcement.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/authzen-mcp-parity

Comment @coderabbitai help to get the list of available commands.

@ericlovold
ericlovold merged commit 5a350ce into main Jul 9, 2026
7 of 8 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/api/v1/tokens/route.ts`:
- Around line 82-93: The subtree cap check in the token creation flow still
races because only the agent is serialized, so sibling agents under the same
pooled cap can both read the same total and overspend. Update the enforcement
path in the token route to also serialize by the cap wallet, using the same
lock/transaction flow around the subtree aggregate and create, or switch this
check to the shared atomic counter path before inserting the token log. Keep the
fix anchored in the existing token-cap logic and the subtree-cap branch that
returns the exceeded result.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b44e20e2-9670-441d-81f9-fac3a5b39578

📥 Commits

Reviewing files that changed from the base of the PR and between 5e4452e and 4cd38d1.

📒 Files selected for processing (14)
  • app/api/access/v1/access-request/route.ts
  • app/api/v1/tokens/route.ts
  • lib/authzen.ts
  • lib/changelog.ts
  • lib/decisions.ts
  • lib/rules/provision.ts
  • lib/rules/spend.ts
  • mcp-server.ts
  • package.json
  • packages/sanction-mcp/mcp-server.js
  • tests/authzen-aarp.route.test.ts
  • tests/authzen-parity.test.ts
  • tests/richDenials.route.test.ts
  • tests/tokens-horizons.route.test.ts

Comment on lines +82 to +93
for (const node of tokenCapAncestors) {
const cap = node.policy!.subtreeDailyTokenCapUsd! / 100
const subtree = await walletSubtreeIds(tx, node.id)
const agg = await tx.tokenLog.aggregate({
where: { agent: { walletId: { in: subtree } }, createdAt: { gte: dayStart } },
_sum: { costUsd: true },
})
const subtreeSpent = agg._sum.costUsd ?? 0
if (subtreeSpent + cost_usd > cap) {
return { exceeded: "subtree-daily" as const, spent: subtreeSpent, limit: cap, capWalletId: node.id }
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for any pool/wallet-level locking used alongside subtree cap enforcement.
rg -nP 'pg_advisory(_xact)?_lock' -C3
echo '--- subtree cap enforcement sites ---'
rg -nP 'subtreeDailyTokenCapUsd|walletSubtreeIds' -C3

Repository: ericlovold/sanction

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- file list near app/api/v1/tokens ---\n'
git ls-files 'app/api/v1/tokens/*' 'app/api/v1/*' | sed -n '1,120p'

printf '\n--- route.ts around the relevant lines ---\n'
cat -n app/api/v1/tokens/route.ts | sed -n '1,180p'

printf '\n--- advisory lock / token cap searches ---\n'
rg -n --hidden -S 'pg_advisory(_xact)?_lock|subtreeDailyTokenCapUsd|walletSubtreeIds|tokenCapAncestors|agent\.id' app api lib src . 2>/dev/null || true

Repository: ericlovold/sanction

Length of output: 248


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- route.ts around the relevant lines ---\n'
cat -n app/api/v1/tokens/route.ts | sed -n '1,200p'

printf '\n--- advisory lock / token cap searches ---\n'
rg -n --hidden -S 'pg_advisory(_xact)?_lock|subtreeDailyTokenCapUsd|walletSubtreeIds|tokenCapAncestors|agent\.id' . 2>/dev/null || true

Repository: ericlovold/sanction

Length of output: 248


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- file list near app/api/v1/tokens ---'
git ls-files 'app/api/v1/tokens/*' 'app/api/v1/*' | sed -n '1,120p'

echo
echo '--- route.ts around the relevant lines ---'
cat -n app/api/v1/tokens/route.ts | sed -n '1,220p'

echo
echo '--- advisory lock / token cap searches ---'
rg -n --hidden -S 'pg_advisory(_xact)?_lock|subtreeDailyTokenCapUsd|walletSubtreeIds|tokenCapAncestors|agent\.id' . 2>/dev/null || true

Repository: ericlovold/sanction

Length of output: 22087


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- docs/TRACEABILITY.md relevant section ---'
sed -n '1,120p' docs/TRACEABILITY.md

echo
echo '--- concurrency/race tests mentioning advisory lock or budgets ---'
rg -n --hidden -S 'advisory|race|overshoot|subtreeDailyTokenCapUsd|token budget|cap wallet|Pool daily token cap' tests app lib docs . 2>/dev/null | sed -n '1,220p'

echo
echo '--- lib/gateway.ts relevant section ---'
cat -n lib/gateway.ts | sed -n '130,220p'

Repository: ericlovold/sanction

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- lib/gateway.ts around subtree cap logic ---'
cat -n lib/gateway.ts | sed -n '140,210p'

echo
echo '--- lib/cascadeBudget.ts around atomic enforcement ---'
cat -n lib/cascadeBudget.ts | sed -n '1,220p'

echo
echo '--- concurrency.db.test.ts relevant subtree-cap test names ---'
rg -n --hidden -S 'subtree|cascade|race|cap' tests/concurrency.db.test.ts tests/* 2>/dev/null | sed -n '1,220p'

Repository: ericlovold/sanction

Length of output: 36319


Serialize subtree-cap enforcement by cap wallet

In app/api/v1/tokens/route.ts, the advisory lock still only serializes a single agent, so sibling agents under the same pooled cap can both read the same subtree total and both insert, pushing the pool past its limit. Serialize this horizon on the cap wallet too, or route it through the shared atomic counter path before the aggregate/create.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/v1/tokens/route.ts` around lines 82 - 93, The subtree cap check in
the token creation flow still races because only the agent is serialized, so
sibling agents under the same pooled cap can both read the same total and
overspend. Update the enforcement path in the token route to also serialize by
the cap wallet, using the same lock/transaction flow around the subtree
aggregate and create, or switch this check to the shared atomic counter path
before inserting the token log. Keep the fix anchored in the existing token-cap
logic and the subtree-cap branch that returns the exceeded result.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants