fix(mothership): refuse desktop claims for unapproved or stopped calls

Merged
WA
1mo ago
+30765-178

Summary

  • /api/desktop/tool/authorize no longer hands a call to the desktop app when it should not run:
    • a call held for the user's approval is refused (403) until they allow it. A declined call stays refused. Whether a call is gated depends on the turn (flag, arguments, allow lists), so pre-persist records it on the row (permission_requested_at, nullable, expand-only migration 0395).
    • once the run's tool admission has closed (Stop, a newer turn, or the run's end), every desktop call is refused (410), including non-claiming local reads.
  • One claim primitive. claimSimToolExecution becomes claimToolExecution, which takes either a Sim owner token (the existing leased claim, unchanged) or a desktop claim owner. Both lock the run row and check admission the same way, so a desktop claim serializes with Stop. There is no second copy of that logic.
  • Stop settles the stopped runs' open desktop calls in the transaction that closes admission. An unclaimed call is cancelled as never started (notStarted); a claimed one as outcome unknown (outcomeUnknown, doNotRetry).
    • Candidate rows are picked with the shared TS classifier (lib/mothership/tools/desktop-tools.ts, now the single "is this a desktop tool" module), not a SQL copy of it.
    • Each result is sealed exactly as /api/copilot/confirm seals a client result (the shared sealClientToolSettlement), so a waiter still listening restores what Stop did.
  • /api/copilot/confirm acknowledges a result for an already-settled call with the stored outcome (200), instead of 404 (native) or 500 (other client tools). That covers a retried delivery and a late result after Stop or a server-side settlement. Nothing is written or published again, and the renderer stops retrying.
  • No desktop release is needed: the desktop app only checks response.ok on authorize, and an import refusal maps as before.

A terminal command already running when Stop lands keeps running on the desktop. There is no cancel channel for it in the current IPC, so that is left to the desktop executor work.

Type of Change

  • Bug fix

Testing

  • desktop-tool-authorization.integration.ts (real Postgres + Redis, production pre-persist, authorize, tool-permission, Stop and confirm paths): 11 tests. On staging they fail with 200 instead of 403/410, claims succeed after Stop, the waiter never wakes, a late result revives a stopped call, and duplicate results get 404/500. The Stop wake-up test runs with sealed provenance: with an unsealed Stop result it restores only a generic "Tool cancelled".
  • Reverted each guard on its own and watched its test go red: the claim permission predicate, the claim admission check, the route admission check, the Stop settlement (incl. local-folder reads), Stop sealing, the settled-call acknowledgement, the lost-write re-read, and the pre-persist gate flag.
  • bun run lint, bun run type-check, bun run check:audits, bun run check:migrations, bun run test, bun run test:integration (1438 passed), bun run docs-manifest:check, block-registry check: all pass. drizzle-kit generate produces no new migration.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing (new tests pass the test-audit authoring gate)
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)