Expose scoped Context source withdrawal over HTTP and MCP

SI
5d ago
+423-5

Summary

Exposes scoped source withdrawal as the shared Context operation withdraw_source over HTTP (POST /api/context) and MCP (withdraw_music_context_source). It is the first lifecycle mutation reachable through both transports.

  • Input. request_id plus exactly one of source_id or source_version_id. Version IDs are what callers can already see on main, as sourceVersionIds in brief and saved-brief input manifests. After the existing actor/workspace authorization, the operation calls the new withdraw_context_request_source RPC with p_actor, p_owner, p_request, p_source, p_source_version. The database rechecks membership, resolves a version to its source, requires that source to be a recorded input of the request, and reuses the existing withdrawal primitive.
  • Receipt (context-source-withdrawal-v1). Returns the resolved source_id, withdrawn_at, already_withdrawn, and affected lineage (results, documents, request_ids). The counts are identical on replay.
  • Effect. Future authorized reads and briefs exclude the withdrawn evidence. Dependent saved briefs read back unavailable. The withdrawal is sticky for that workspace, source kind and URL: the database migration fences late and in-flight writers, so an older job cannot republish it. Results are relabelled withdrawn (rows kept), and original files are not deleted.
  • MCP annotations. There is no restore, so Context operations can now carry destructive/notice through delegated OAuth registration. withdraw_music_context_source now advertises destructiveHint: true with a "Cannot be undone" notice. All other Context operations keep destructiveHint: false, and the catalog test asserts both.

New SRP files hold the schema, receipt type and operation. Shared files receive single entries: the schema spread, one delegation line, one RPC allowlist entry, one MCP tool entry, one description sentence, and a 3-line policy pass-through in registerFullOAuthTools.ts. The README subsection is appended at the end.

Refs recoupable/app#2124 (child of epic recoupable/app#2116).

Merge sequencing

docs → database → api → app. Sibling PRs on branch claude/ctx-2124-source-withdrawal:

  • recoupable/docs#364 (OpenAPI variant);
  • recoupable/database#102 (migration 20261010212400_context_source_withdrawal.sql). It now takes five parameters; this PR fails at runtime until that migration is applied.

This PR does not depend on any open PR. Callers find version IDs in brief manifests on main, and api#985 + database#92 would later add a read-only per-request source listing.

Known rebase interactions with open sibling PRs:

  • Ticket #2125 PRs (docs#361, api#1005) add "source withdrawal … not exposed yet". Whichever merges second must drop that clause.
  • api#1011 (#2120 ownership parity across every action) will need a withdraw_source case.
  • fullOAuthCatalog.test.ts moves 84 → 85 tools, so any sibling that adds a tool needs the same one-line rebase.

Verification

  • pnpm exec vitest run lib/context: 69 files passed, 8 skipped; 336 tests passed, 8 skipped, 0 failed (the skipped tests are live tests).
  • Targeted: sourceWithdrawalOperation.test.ts, sourceWithdrawalTransports.test.ts, new lib/supabase/context_requests/__tests__/sourceWithdrawalRpc.test.ts, fullOAuthCatalog.test.ts, contextToolOperations.test.ts: 5 files, 14 tests passed.
  • pnpm exec vitest run lib/supabase/context_requests lib/mcp/oauth: 22 files and 89 tests passed. fullOAuthRouting.test.ts fails to load because the gitignored build artifact lib/mcp/extension/generated-ui.ts is missing (pre-existing; same on main). I added a temporary stub of that file and deleted it afterwards. With the stub, vitest run lib/context lib/mcp lib/supabase/context_requests gave 533 passed, 8 skipped, 1 failed; fullOAuthRouting.test.ts passes. The one failure is registerRecoupExtension.test.ts, which asserts real generated UI copy against the stub.
  • pnpm exec tsc --noEmit: 199 errors, none in the 12 changed .ts files. All are pre-existing in untouched files (same count as the review baseline).
  • pnpm exec eslint <12 changed .ts files>: 0 problems. pnpm exec prettier --check <same> lib/context/README.md: clean.
  • Red → green:
    • Reverting the callContextRpc allowlist entry fails the new adapter test. Before this change, no test covered it.
    • Reverting the destructive pass-through to destructive: false fails fullOAuthCatalog.test.ts.
    • The new version-ID and exactly-one cases fail on the previous schema.
    • The original 8 tests failed before the operation existed.
  • Not verified: hosted deployment, live providers, production database, a real MCP client against the hosted server, pnpm build (not run by rule).

Out of scope / known gaps

  • No restore/un-withdraw operation (hence the destructive hint).
  • Scoped corrections, refresh and recompute (correct_context_spotify_release still has no API caller).
  • Duplicate-article corroboration.
  • Customer deletion/retention of original bytes (database#94/#95 + api#986–#991).
  • The shared HTTP 409 message still mentions retrying ingestion with the same idempotency key. The docs now say withdraw_source can be retried unchanged.
  • Workflows, paid provider calls, credits, and any chat UI for discovering or withdrawing sources.

🤖 Generated with Claude Code