Prove Context ownership parity and revocation across every action

SI
10d ago
+865-0

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.ts runs three scenarios per action through the real contextOperationHandler, the real registered context MCP callback and the real processContextOperation:
    • 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.ts registers the real context tool through registerFullOAuthTools and checks one public tool per action, action re-injection, verified-identity binding, readOnlyHint parity with contextToolOperations, and an opaque result on domain denial.
  • workerRevocationParity.test.ts revokes membership after work was queued. runReleaseVerification and runRecordedReleaseTrackIsrcs run through the real runRecordedContextModules, 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 generic failed/authorize outcome, with no claim, dispatch or credential fetch. The suite also covers runContextRequest, runContextEnrichment (revoked while queued and during the paid call), an adopted runGuestContext job and the contextWorkflow review step. In every case, no persisted RPC parameter carries the revocation detail.
  • lib/context/OWNERSHIP.md states 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, buildContextOperationFixture throws 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 eslint and pnpm exec prettier --check on the four .ts files and OWNERSHIP.md → clean.
  • Ran: pnpm exec tsc --noEmit → 0 errors in any file touched by this PR and 0 under lib/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): workerRevocationParity 2 failed / 7 passed (both node-authorization cases).
    • Passing the account instead of the authorized owner as p_owner to read_context_brief (processContextOperation.ts): ownershipParity 1 failed / 80 passed (read_brief authorized case).
    • On the first commit: leaking error.message from the catch blocks of contextOperationHandler.ts and registerContextTool.ts failed every "denied in the domain layer" case (27 in ownershipParity, 27 in oauthContextParity) on the canary assertion. Removing the pre-commit recheck in runContextRequest.ts and the execution/node rechecks in runReleaseVerification.ts failed the corresponding worker cases.
  • 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 contextWorkflow review 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.md is 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.ts runs authorized, domain-denied, and organization-denied scenarios through the real HTTP handler, MCP callback, and processContextOperation, asserting opaque responses, identical sink calls bound to the authorized workspace and actor, and no RPC or dispatch after denial.
  • oauthContextParity.test.ts registers the real context tool through registerFullOAuthTools and checks one public tool per action, action re-injection, verified-identity binding, and permission annotations.
  • workerRevocationParity.test.ts revokes membership after queueing through the real recorder, scheduler, and execution wrappers, covering runContextRequest, runReleaseVerification, runRecordedReleaseTrackIsrcs, runContextEnrichment, an adopted runGuestContext job, and the contextWorkflow review step. None commits, calls a provider, or fetches credentials; node-authorization denials save a single generic failed/authorize outcome, 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.