fix(mothership): refuse desktop claims for unapproved or stopped calls
Merged
WA
1mo ago+30765+30765+01234567890123456789012345678901234567890123456789-178-178-012345678901234567890123456789
Summary
/api/desktop/tool/authorizeno 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.
- 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 (
- One claim primitive.
claimSimToolExecutionbecomesclaimToolExecution, 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/confirmseals a client result (the sharedsealClientToolSettlement), so a waiter still listening restores what Stop did.
- Candidate rows are picked with the shared TS classifier (
/api/copilot/confirmacknowledges 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.okon 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. Onstagingthey 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 generateproduces no new migration.
Checklist
- Code follows project style guidelines
- Self-reviewed my changes
- Tests added/updated and passing (new tests pass the
test-auditauthoring gate) - No new warnings introduced
- I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)