feat(mupot-106): enforce surface capability gates at route level#119
Merged
Conversation
Role-preset allow/deny lists were documentation-only: a member token could reach any route that didn't call requireCapability. This PR makes the deny-lists real. Changes: - src/auth/capability.ts: hasSurfaceCap(env, auth, surface) + requireSurfaceCap(surface) middleware. Owner/admin bypass via rank. D1 query against gate_grants (capability, principal_type, principal_id). - src/dashboard/keys.ts: mintScopedKey now writes one gate_grants row per entry in preset.allows at mint time (INSERT OR IGNORE, idempotent). - src/tasks/index.ts: POST /:id/verdict gates on outreach:send-gated when gate_owner='gate:loops' AND verdict='approved'. Reject path unblocked. - src/dashboard/index.ts: POST /brain/loops/:id/control gates on content:write (all actions) and budget:write (budget_override only). Admin/owner bypass preserved. - migrations/0022_surface_caps.sql: no-DDL migration record (gate_grants already supports free-text capabilities). Per-surface enforcement table: outreach:send-gated ENFORCED src/tasks/index.ts ~L530 (verdict endpoint) budget:write ENFORCED src/dashboard/index.ts ~L215 (loop control) content:write ENFORCED src/dashboard/index.ts ~L209 (loop control) mcpwp:write GAP no mcpwp write route exists yet; gate ready to wire provision EXISTING admin rank-gate in src/mcp/tools/*.ts (unchanged) Tests: 22 new tests in tests/surface-caps.test.ts; 833/833 passing (was 811). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #106. Role-preset allow/deny lists were documentation-only — a member token could reach any route that didn't call
requireCapability. This PR makes the deny-lists real.New infrastructure (
src/auth/capability.ts)hasSurfaceCap(env, auth, surface)— queriesgate_grantstable for free-text surface grants; owner/admin bypass via rankrequireSurfaceCap(surface)— Hono middleware wrapperMint wires grants (
src/dashboard/keys.ts)mintScopedKeynow writes onegate_grantsrow per entry inpreset.allowsat mint time (INSERT OR IGNORE, idempotent on re-mint)Route-level gates
POST /api/tasks/:id/verdict(src/tasks/index.ts): requiresoutreach:send-gatedwhengate_owner='gate:loops'ANDverdict='approved'. Reject path unblocked.POST /brain/loops/:id/control(src/dashboard/index.ts): requirescontent:writefor all actions; additionally requiresbudget:writeforbudget_override.Migration
migrations/0022_surface_caps.sql: no-DDL migration record.gate_grants(migration 0008) already supports free-text capability strings.Per-surface enforcement table
outreach:send-gatedsrc/tasks/index.ts~L530 (verdict endpoint)budget:writesrc/dashboard/index.ts~L215 (loop control, budget_override)content:writesrc/dashboard/index.ts~L209 (loop control, all actions)mcpwp:writerequireSurfaceCapready to wireprovisionsrc/mcp/tools/*.ts— unchangedTest plan
22 new tests in
tests/surface-caps.test.ts. Full suite: 865/865 passing (was 843). Typecheck clean.🤖 Generated with Claude Code