feat(agent): tell the model the app asks for device tool approval itself (#20938, PR 3)
Depends on #20973, #20975 and #20997 (PR 2, the approval card). Stacked on feat/desktop-approval-card; the PR 3-only change is the last commit (6 files). Merge right after PR 2, in the same release window, so no beta ships cards the model rarely uses.
Invariants carried from PR 2 (its changes show in this diff until it merges; PR 3 itself touches only the tool manifest, its fixture, the regenerated surfaces and one manifest test): INV-AUTH-1 (product/invariants/auth-session.md) and INV-CHAT-2 (product/invariants/chat-scroll-placement.md), named by PR 2 for the post-handshake card clearing and the card list in ChatMessagesView.swift; see PR 2's body.
Line-count exceptions carried from PR 2 (this branch is stacked on it, so its file growth shows in this diff until PR 2 merges; PR 3 itself grows no ratcheted file):
Line-Count-Exception: desktop/macos/Desktop/Sources/Chat/AgentRuntimeProcess.swift | 4373 -> 4384 | two kinds in the closed message switch, the post-handshake clear and conditional capability send, a two-line routing case, the owner-revoke case that clears cards, and one clear on daemon exit; the statics, wire message, routing body and clearing live in AgentRuntimeProcess+ToolApprovals.swift, and messageOwnerIsCurrentlyAuthorized drops private so that extension can call it
Line-Count-Exception: desktop/macos/Desktop/Sources/DesktopAutomationBridge.swift | 5172 -> 5173 | one registerToolApprovalActions() call in the builtin registration list; the actions live in Automation/DesktopAutomationToolApprovalActions.swift
Line-Count-Exception: desktop/macos/Desktop/Sources/FloatingControlBar/FloatingControlBarView.swift | 3018 -> 3020 | the approvalSurface argument at the one AIResponseView call site
Line-Count-Exception: desktop/macos/Desktop/Sources/MainWindow/Components/ChatBubble.swift | 2162 -> 2172 | the "Waiting for your approval" caption under the tool row and its accessibility value; the status itself and the icon live in ChatProvider's enum and ToolActivityTimelineLayout
Line-Count-Exception: desktop/macos/Desktop/Sources/Providers/ChatProvider.swift | 6824 -> 6834 | the waitingApproval case of ToolCallStatus with its doc line, its detector mapping, the turn's session id captured for the stall clocks, and the tick-loop lines that ask the approval store about that session; the lookup lives in ChatProvider+ToolApprovalWait.swift
What changed and why
Follow-up to #20938 PR 2, stacked on feat/desktop-approval-card (fe0cbf069a); merge right after it. The five sensitive device tools (send_message, run_applescript, read_message_history, list_message_chats, list_mail_messages) described themselves to the model as "always requires an approval dispatch", and their capability guidance repeated it. In PR 2's live run the in-app pi-mono model therefore answered "send an iMessage" by calling create_desktop_dispatch instead of send_message: that model-authored dispatch has no invocation binding, no card renders it, and it never expires, so the person saw nothing and the message was never sent. Now that the kernel parks a direct call behind the approval card (PR 1a, 1b, 2), the two descriptions and four guidance lines in omi-tool-manifest.ts say that Omi asks the person in the app before the tool runs and tell the model to call the tool directly and not create a dispatch first. capture_screen, which PR 1a added to the approval set, gets the same wording in its description and its two guidance lines, replacing "requires explicit approval before image bytes are shared", plus a note that each screenshot is approved on its own, so the model should capture once and reuse the image. The read tools' descriptions were already plain and are unchanged. The manifest digest changes, so GeneratedToolExecutors.swift, GeneratedToolCapabilities.swift and the tool-manifest.json fixture are regenerated with scripts/generate-tool-surfaces.mjs; no Swift logic changes.
Product invariants affected
INV-AGENT-* (product/invariants/agent-control-plane.md) is named because desktop/macos/agent/src/runtime/** matches its glob. Unaffected: only model-facing text and the digests derived from it change; policy, parking, grants and the ledger are untouched. INV-CHAT-1 (product/invariants/chat-continuity.md) is named for the same glob and is unaffected for the same reason.
How it was verified
From desktop/macos at 703fd95620 (on feat/desktop-approval-card fe0cbf069a): node --experimental-strip-types scripts/generate-tool-surfaces.mjs wrote the two generated files and the fixture (manifest digest e2c2289938e8), and --check then reports all outputs match; npm run build clean; npx vitest run -> 73 files, 959 tests passed, including tool-surfaces-exhaustiveness.test.ts against the regenerated fixture; xcrun swift build of the app with the regenerated surfaces clean and ToolCallStatusTests 31/31, AuthorizedToolExecutionTests 15/15, ApprovalCardTests 24/24; pi-mono-extension npm test 143/143; desktop-changelog.py validate OK; scripts/pr-preflight --base feat/desktop-approval-card --pr-body-file <this body> -> "PR preflight passed: 31 checks" (base feat/desktop-approval-card, Python 3.11).
Verified live with this branch on top of PR 2 (rebuilt bundle): a person asked, with no tool instructions, "Send an iMessage to +1 555 010 0002 saying: testing Omi approvals", and the in-app pi-mono model called send_message directly, raising the card (Allow Once ran it once; a second ask was denied); "List my recent Mail messages" likewise called list_mail_messages directly. Before this change the same plain ask produced a create_desktop_dispatch row and the reply "I've requested approval" (PR 2's live run). One run each, so the model's compliance is observed, not guaranteed. The capture_screen wording was added after that run and was not exercised live.
Tests
No new test: omi-tool-manifest.test.ts now expects the capture_screen guidance to say Omi asks the person in the app (it pinned the old "requires explicit approval" wording), the exhaustiveness test already pins the generated surfaces to the manifest, and the wording has no code path of its own.
UI evidence
UI-Evidence: none -- runtime-only change
Failure class (fixes)
Failure-Class: none