Prove Context ownership parity and revocation across every action
Summary
Proves, per Context action, that ownership is enforced identically through REST, MCP (API key and OAuth) and background workers, and that a denial never leaks private detail. A generic zod fixture builder iterates contextOperationSchema.options, so every current action (27 today) is covered without a hand-written map, and actions that sibling PRs add are covered on merge as long as the candidate pool can satisfy their required fields.
ownershipParity.test.tsruns three scenarios per action through the realcontextOperationHandler, the real registeredcontextMCP callback and the realprocessContextOperation:- Authorized: every private sink call (RPC, Supabase wrapper or workflow dispatch) receives the authorized organization as owner and the authenticated account as actor. HTTP and MCP make identical sink calls and return the same result. With an empty database answer, 19 of 27 actions succeed on both transports and the other 8 fail identically on both.
- Denied in the domain layer, with a private canary in the error: both transports return an opaque non-success (HTTP 4xx), the canary and organization ID are absent, and no sink runs.
- Denied organization membership: HTTP stops at the transport with 403; MCP is denied in the domain layer; no sink runs.
oauthContextParity.test.tsregisters the realcontexttool throughregisterFullOAuthToolsand checks one public tool per action, action re-injection, verified-identity binding,readOnlyHintparity withcontextToolOperations, and an opaque result on domain denial.workerRevocationParity.test.tsrevokes membership after work was queued.runReleaseVerificationandrunRecordedReleaseTrackIsrcsrun through the realrunRecordedContextModules, scheduler and execution wrappers against one mocked RPC boundary. Revoked before execution authorization, the run rejects and nothing is recorded. Revoked at node authorization, the run resolves and saves exactly one genericfailed/authorizeoutcome, with no claim, dispatch or credential fetch. The suite also coversrunContextRequest,runContextEnrichment(revoked while queued and during the paid call), an adoptedrunGuestContextjob and thecontextWorkflowreview step. In every case, no persisted RPC parameter carries the revocation detail.lib/context/OWNERSHIP.mdstates the enforced contract, current denial responses per transport, acceptance coverage and explicit non-claims.
Acceptance coverage: items 1 (REST, MCP and worker parity) and 2 (revocation while queued or running) are fixture-tested. Item 3 (another customer's canary through briefs, caches, snippets or exports) is covered only indirectly: denied callers reach no sink and every authorized read is bound to the caller's owner, but no cross-customer brief, cache, snippet or export fixture exists yet. Item 4 (corrections scoping) is documented from the migration, not exercised. Item 5 belongs to the open storage PRs.
No production code changes: processContextOperation.ts, README.md, the dispatchers and the MCP registrations are untouched.
Refs recoupable/app#2120 (child of epic recoupable/app#2116).
Merge sequencing
api only (no docs, database or app change). Compatible with open siblings in either merge order; checked by applying each sibling's gh pr diff onto this branch in a disposable worktree:
- api#1005 (typed Context error contract): the suites pin only the security property (4xx / non-success, no canary, no organization ID, no sink), not the envelope or exact status. With #1005 applied: 172 of 172 of these tests pass, plus its own 67.
- api#1019 (
ingest_video): a syntactic YouTube locator is in the fixture candidate pool, so the new action is covered. With #1019 applied: 178 passed (28 actions). With #1005 and #1019 both applied: 178 passed. - api#981 / #983 / #985 do not apply cleanly to current main and were not tested. If a sibling adds an action whose required fields the candidate pool cannot satisfy,
buildContextOperationFixturethrows with the field name; extend the candidate list in that PR.
Verification
- Ran:
pnpm exec vitest run lib/context/__tests__/ownershipParity.test.ts lib/context/__tests__/oauthContextParity.test.ts lib/context/__tests__/workerRevocationParity.test.ts→ 3 files, 172 passed, 0 failed, 0 skipped (81 = 27 actions × 3 scenarios; 82 = 1 catalog check + 27 × 3; 9 worker cases). - Ran:
pnpm exec vitest run lib/context lib/mcp/oauth/__tests__/registerFullOAuthTools.test.ts→ 71 files passed, 8 skipped; 508 tests passed, 8 skipped, 0 failed (baseline on origin/main: 68 files, 336 passed, 8 skipped; the skips are the existing opt-in live/local-database suites). - Ran:
pnpm exec eslintandpnpm exec prettier --checkon the four.tsfiles andOWNERSHIP.md→ clean. - Ran:
pnpm exec tsc --noEmit→ 0 errors in any file touched by this PR and 0 underlib/context/**. The repo-wide run reports 199 pre-existing errors, all in files identical to origin/main, unrelated to this change. - Red → green: the behavior under test already exists, so red was shown by temporarily bypassing it and reverting with
git checkout --:- Persisting the authorize error message into the saved node outcome (
runPlannedContextModules.ts):workerRevocationParity2 failed / 7 passed (both node-authorization cases). - Passing the account instead of the authorized owner as
p_ownertoread_context_brief(processContextOperation.ts):ownershipParity1 failed / 80 passed (read_briefauthorized case). - On the first commit: leaking
error.messagefrom thecatchblocks ofcontextOperationHandler.tsandregisterContextTool.tsfailed every "denied in the domain layer" case (27 inownershipParity, 27 inoauthContextParity) on the canary assertion. Removing the pre-commit recheck inrunContextRequest.tsand the execution/node rechecks inrunReleaseVerification.tsfailed the corresponding worker cases.
- Persisting the authorize error message into the saved node outcome (
- Not verified: hosted deployment, live providers (no Spotify or other provider call is made; all provider and database access is mocked), production or hosted database, network MCP sessions (parity is exercised by invoking the registered callbacks in-process), and Vercel Workflow execution.
Out of scope / known gaps
- No in-transaction actor recheck inside worker commit RPCs (
commit_spotify_context,save_context_execution_outcome,complete_context_release_track_isrcs,complete_context_enrichment); the application rechecks immediately before calling them. Shared commit RPCs with an in-transaction lock belong to CE05/CE06 (recoupable/app#2121, #2122). - The
contextWorkflowreview step authorizes only at its start; if access is revoked between planning and saving it still records the blocked plan's server-owned reasons (it never claims a node or calls a provider). - Private deletable storage, original-file receipts and the revoked-membership-during-download lifecycle (acceptance item 5) are in open api#986–#991 and database#94/#95; evidence-attachment actor locks are in open database#90 / api#983.
- No cross-customer brief, cache, snippet or export canary fixture (acceptance item 3) and no corrections-scoping test (item 4) yet.
- Workflow runtime logs may still carry a thrown error's message server-side; only customer-readable records and transport responses are asserted opaque.
OWNERSHIP.mdis a new file so it does not conflict with the README edits in the open PRs; a one-line README pointer can follow once those merge.
🤖 Generated with Claude Code
Summary by cubic
Proves that Context ownership enforcement is identical across REST, MCP, and background workers for all 27 actions, and that denials never leak private detail. Adds three test suites driven by a generic zod fixture builder that iterates contextOperationSchema, so future actions are covered automatically, and documents the enforced contract in lib/context/OWNERSHIP.md. No production code changes.
Test coverage
ownershipParity.test.tsruns authorized, domain-denied, and organization-denied scenarios through the real HTTP handler, MCP callback, andprocessContextOperation, asserting opaque responses, identical sink calls bound to the authorized workspace and actor, and no RPC or dispatch after denial.oauthContextParity.test.tsregisters the realcontexttool throughregisterFullOAuthToolsand checks one public tool per action, action re-injection, verified-identity binding, and permission annotations.workerRevocationParity.test.tsrevokes membership after queueing through the real recorder, scheduler, and execution wrappers, coveringrunContextRequest,runReleaseVerification,runRecordedReleaseTrackIsrcs,runContextEnrichment, an adoptedrunGuestContextjob, and thecontextWorkflowreview step. None commits, calls a provider, or fetches credentials; node-authorization denials save a single genericfailed/authorizeoutcome, and no persisted record carries private detail.
OWNERSHIP.md states the enforced contract and explicit non-claims (in-transaction RPC rechecks, deletable storage, evidence attachment locks); the latter are owned by open sibling PRs.