Skip to content

feat(agent): tell the model the app asks for device tool approval itself (#20938, PR 3) - #20998

Open
sauranshbhardwaj wants to merge 9 commits into
BasedHardware:mainfrom
sauranshbhardwaj:feat/device-tool-descriptions
Open

sauranshbhardwaj wants to merge 9 commits into
BasedHardware:mainfrom
sauranshbhardwaj:feat/device-tool-descriptions

Conversation

@sauranshbhardwaj

@sauranshbhardwaj sauranshbhardwaj commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

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 (fe0cbf0); 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 703fd95 (on feat/desktop-approval-card fe0cbf0): 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

@Git-on-my-level

Copy link
Copy Markdown
Collaborator

Thanks @sauranshbhardwaj — this is a careful piece of work on a sensitive surface. I reviewed the kernel, relay, and card paths statically; summary below. No concrete defect found, but this needs maintainer sign-off before merge.

What the design gets right (verified against the diff):

  • run-tool-capability.ts — authorizeRelayInvocationOrRequestApproval parks an invocation behind a dispatch bound by invocation id + input hash, and the new invalid_tool_input / input_too_large_to_approve reject codes fail closed on unknown keys and >4 KiB inputs before anything is prepared.
  • kernel-coordinator.ts — the 24 h cap on dispatch-minted grants, and createDesktopDispatch stripping the invocation payload key so a model-created dispatch can't impersonate a parked tool call, are both exactly the right guards.
  • sqlite-store.ts — startup reconciliation expiring invocation-bound approval dispatches (regardless of TTL) means a parked call never survives a daemon restart. Good.
  • DesktopToolApprovalStore.swift — answers go only through signed direct control; allow_once mints no grant; allow_session is scoped to the exact capability/operation/resource with a 1 h expiry; deny runs nothing. The harness actions are correctly gated on AppBuild.isNonProduction.
  • The waitingApproval state pausing the 90 s no-progress abort (StallDetector.swift, ChatProvider.swift) is keyed by the turn's own session — nice detail.

Why I'm not approving and why human review is needed:

  1. This is the authorization path for send_message / run_applescript / Messages and Mail reads — security-sensitive by definition, and it's stacked on unmerged feat(desktop): approval card for parked device tool calls (#20938, PR 2) #20997 with the explicit instruction to merge in the same release window. That sequencing and the product decision need a maintainer, not automation.
  2. No build or test CI ran on this head, so the new suites (ApprovalCardTests, relay-tool-approvals.test.ts, the control-tools.test.ts / run-tool-capability.test.ts additions) have no CI-executed evidence on this branch. Please confirm they've been run locally, or point at a run.
  3. This PR changes model-facing guidance: omi-tool-manifest.ts and GeneratedToolCapabilities.swift move from "always requires an approval dispatch" to "call it directly — Omi asks the person," and the read tools move to timeoutClass: "long". Agents working in this repo will follow that wording, and on an older daemon without the desktop_tool_approval_cards handshake the model is told to call directly while the relay still rejects with approval_required. That degraded-but-coherent fallback looks intentional — please confirm it's the intended UX. The .github/agent-docs/agent-grants-approval-ux.mdx flip from "design-only" to implementation scope is accurate against this diff but deserves a maintainer's eye for the same reason.

Leaving for human maintainer review: the approval-flow security design and the stacked-merge sequencing both need a maintainer decision. Labeling security-review, feature-fit-review, and macOS.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@Git-on-my-level Git-on-my-level added security-review Touches auth, provider routing, secrets, or security-sensitive surfaces feature-fit-review Needs review of product/feature direction with Omi mission/vision macOS labels Oct 8, 2026
@sauranshbhardwaj

Copy link
Copy Markdown
Contributor Author

Thanks. On the three points:

  1. Sequencing: agreed it's a maintainer call. The intended order is feat(desktop): park sensitive tool calls in waiting_approval and emit approval.requested (#20938, PR 1a) #20973 → feat(desktop): relay approval_requested to Swift and resume parked tool calls (#20938, PR 1b) #20975 → feat(desktop): approval card for parked device tool calls (#20938, PR 2) #20997 → this, in one release window, so no beta ships cards the model rarely reaches.
  2. Local runs on this head (8be563830f): desktop agent npx vitest run → 945 passed, including relay-tool-approvals, control-tools, run-tool-capability and tool-surfaces-exhaustiveness; Swift → ApprovalCardTests 22, AgentRuntimeProcessTests 74, StallDetectorTests 24, ChatStallWatchdogTests 10, ToolCallStatusTests 31, all passing; scripts/pr-preflight against main → 39/39. And a live run on a packaged build of this branch: a plain "send an iMessage to …" (no tool instructions) raised the card, Allow once ran it once, Deny ran nothing, and a card left alone expired at 180 s with no cancellation (kernel: approval.resolved decision: expired 180.005 s after approval.requested). If a maintainer can approve the workflows, I'll watch CI too.
  3. Older pairing: yes, intended. The app and the agent daemon ship in the same bundle, so in production the wording and the parking always match. On a mismatched dev pairing, the model calls the tool, gets an immediate approval_required, and tells the person, the same outcome as today, without a dispatch that never surfaces. (The timeoutClass: "long" change is from feat(desktop): relay approval_requested to Swift and resume parked tool calls (#20938, PR 1b) #20975 and shows here only through the stack.)

@Git-on-my-level

Copy link
Copy Markdown
Collaborator

Thanks @sauranshbhardwaj — this is careful work, and the PR-3-only delta (the model-facing tool guidance) is exactly the right scope for the last piece of the stack.

What I verified against the diff:

  • desktop/macos/agent/src/runtime/omi-tool-manifest.ts and agent/tests/fixtures/tool-manifest.json — the guidance for capture_screen, send_message, and run_applescript moves from "Always requires an approval dispatch showing…" to "Call it directly: Omi asks the person to approve it in the app…", which matches the approval-parking implementation carried from PR 2. The capture_screen timeoutClass change normal → long is a good catch, since a wait on a person can exceed a normal tool timeout.
  • desktop/macos/agent/tests/omi-tool-manifest.test.ts — expectations updated consistently, and confining the realtime screenshot executor to the realtime_voice surface (off the pi-mono/stdio chat adapters) is a real tightening, not just churn.
  • The carried machinery holds up on inspection: kernel-coordinator.ts strips the invocation binding key from model-created dispatches so a forged payload can't impersonate a parked call, grants are capped at 24 h with the card minting 1 h session grants scoped to the exact capability/operation/resource, and sqlite-store.ts expires invocation-bound approval dispatches at daemon startup regardless of TTL so a parked call is never replayed.
  • On the Swift side, the new waitingApproval state pausing StallDetector clocks (StallDetector.swift setWaitingOnUser, keyed by the turn's own session in ChatProvider) is well tested in StallDetectorTests/ChatStallWatchdogTests.
  • .github/agent-docs/agent-grants-approval-ux.mdx — noting for maintainers that this flips the doc from "design-only, do not treat as implementation scope" to describing live behavior. The wording is accurate to this stack, but it changes what AI agents reading the repo will treat as real behavior, and the model-facing tool guidance itself now instructs direct calls instead of pre-filed dispatches — both deserve a human read before merge.

One caveat: GitHub only shows the AI reviewer check on this PR, so the added Swift/vitest suites aren't independently green here — please make sure the stacked branch's local runs (and the e2e flow + UI evidence) stay reproducible when PR 2 merges underneath this.

Leaving for human maintainer review: this is a security-sensitive device-tool authorization flow (approval parking, user-minted scoped grants through signed direct control), and the product call on the approval UX — Allow Once vs the 1-hour session grant, and which tools are covered — needs a maintainer sign-off before merge.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@Git-on-my-level Git-on-my-level added the positive-signal Automation verified a genuine fix/quality contribution label Oct 10, 2026
@sauranshbhardwaj

Copy link
Copy Markdown
Contributor Author

Thanks. On keeping it reproducible: after each PR beneath this one merges, I'll rebase this branch and rerun the agent suite (npm run build && npx vitest run) and the generator check. If the merge changes anything the card relies on, I'll also rerun the e2e approval flow and the UI evidence capture, and update the counts here.

@Git-on-my-level

Copy link
Copy Markdown
Collaborator

Reviewed the PR-3 delta on top of the stacked sequence — this is a careful piece of work, and I want to name the parts that stood out before the maintainers' pass.

What I verified against the diff:

  • run-tool-capability.ts — the authorization outcome now either admits an invocation or parks it behind a single approval dispatch, and gated tools are held to their manifest schema at the kernel boundary (invalid_tool_input, input_too_large_to_approve), so the model can't name its own resource_ref to steer a future session grant.
  • kernel-coordinator.ts — model-created dispatches get the invocation payload key stripped, so a forged dispatch can't impersonate a parked tool invocation, and dispatch-minted grants are capped at 24 h.
  • sqlite-store.ts — startup reconciliation expires invocation-bound approval dispatches regardless of TTL, so a parked call can never be replayed after a daemon restart. Covered in sqlite-store.test.ts.
  • DesktopToolApprovalStore.swift — "Allow for This Chat" mints a grant scoped to the exact capability/operation/resource with a 1 h expiry, well under the kernel cap; Deny mints nothing.
  • StallDetector/ChatProvider — a pending card is the person's turn: no stall promotion, no 90 s abort, clocks restart on both edges, keyed by the turn's own session (well tested in StallDetectorTests.swift and ChatStallWatchdogTests.swift, including the pill-vs-main-chat keying case).
  • omi-tool-manifest.ts — the realtime screenshot tool now projects only to the realtime_voice surface, closing what would have been an ungated capture path for typed chat. Good that the test pins this.

The PR-3-specific change itself — rewriting the tool descriptions from "always requires an approval dispatch" to "call it directly; Omi asks the person in the app" (GeneratedToolCapabilities.swift, omi-tool-manifest.ts, fixtures/tool-manifest.json, with digests regenerated) — reads accurate to me because enforcement is unchanged and kernel-side; the model just stops pre-filing dispatches the app now supersedes. One note for maintainers: this rewrites the standing instructions every desktop model receives about sensitive tools, and the per-screenshot-always-once rule plus the interim desktop_tool_approval_cards client-capability gate are product decisions worth an explicit sign-off. The agent-grants-approval-ux.mdx update from "design-only" to describing this implementation matches the code as of this head.

Checks that ran are green (Desktop Swift Build & Tests, Agent Runtime, Mobile Release Eligibility). No blocking issues found; leaving the approval-flow product direction and the model-wording change for human maintainer review.

AI-generated maintainer feedback on behalf of the Omi maintainers.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@undivisible

Copy link
Copy Markdown
Collaborator

@sauranshbhardwaj this conflicts with main now. Please rebase and push; it still looks valid.

…ool policy

A dispatch_required decision for a sensitive on-device tool now has a policy-owned shape for what the user is asked: the capability, operation and resource the grant would be matched against, a bounded display preview of the input (attachment paths reduced to a file name, text and scripts capped at the 4 KiB card bound with a truncation flag the broker fails closed on), and the answers. Deny is the default; allow_once is always offered; allow_session is offered only when there is an exact resource for a grant to cover and carries plain words for what it would cover, so a script grant is shown as covering only that exact script. The 180s expiry matches the mobile device-tool transport and stays under the pi-mono long tool wait minus the Swift executor timeout, so a late answer cannot run an effect the model already reported as failed. Only this module decides the question and the options; adapters and the model never choose allow or deny.
… approval.requested

send_message, run_applescript, read_message_history, list_message_chats and list_mail_messages used to fail with approval_required whenever no scoped grant existed, and nothing in the app could create one. capture_screen did not fail at all: it took and shared a full-screen screenshot with no approval, although its own description promised one, so it now parks the same way under the stable resource "screen". A screenshot holds whatever is on screen, including windows the person keeps out of capture elsewhere, so each one is allowed once: its card offers no session grant, no grant row covers a capture, and the kernel refuses to mint a grant the card did not offer. The realtime voice screenshot tool is no longer advertised to pi-mono, where it sat ungated in every typed chat, pill and workstream run; realtime voice runs still get it through their surface projection. The capability broker now prepares the ledger row and inserts one approval dispatch bound to that exact invocation (id, input hash, daemon boot epoch) in the same transaction, the kernel moves the run and attempt to waiting_approval and appends run.waiting_approval plus approval.requested, and the broker refuses to dispatch the invocation until the dispatch is resolved. resolve_desktop_dispatch gains the two user answers: decision allow without a grant admits exactly this invocation once and mints nothing; allow with a grant whose runId is null mints a session-scoped grant through the existing insertGrant path, capped at 24 hours. Deny, cancellation, expiry, run termination and owner change fail the prepared invocation closed as failed, never outcome_unknown, each with its own approval.resolved, and the run resumes so the model receives approval_denied. A new kernel operation, endChatApprovalScope, ends what a chat granted when the person clears it: the main chat keeps one kernel session across clears, so it closes every approval still parked in that session and revokes every grant the person minted for it, with one grants.revoked audit event. Startup reconciliation expires any pending bound approval and records approval.resolved, so a restart never replays a parked call.

Every relay-callable tool is now classified on purpose (named in a policy set or declared unbundled, never by the read-only default), and a test holds the approval set to every relay-callable tool in a sensitive bundle, with each deliberate exception named and justified, so a new tool cannot ship unclassified or ungated by omission. Grants for desktop.context.screenshot_image must name their exact resource. The gated tools are held to their manifest schema at the kernel boundary and the resource a grant covers is derived only from the fields the tool acts on, so a model-supplied resource_ref can no longer become the grant pattern. A dispatch created through create_desktop_dispatch has its invocation payload key stripped and can never pose as a parked call. Authority is revalidated before a resolution is admitted and that check runs before the resolving transaction, so a revocation it triggers commits on its own; a rolled-back decision restores the broker's memory. The model still cannot resolve its own dispatch: the relay path keeps returning policy_denied without trustedUserControl.

Parking sits behind a kernel switch, desktopToolApprovalsEnabled, that defaults to off and is not reachable from the daemon in this change, so runtime behavior is unchanged until a client that renders approval cards turns it on. The Swift card PR flips the default and deletes the switch.
…ol calls

The stdio relay no longer answers a parked device tool call immediately. A new relay-tool-approvals module, taking the kernel and the frame and result writers, sends Swift an approval_requested frame carrying the dispatch id, the bound invocation, the policy preview, the options and the expiry, keeps the relay call pending, and settles it from the kernel's approval.resolved event: an allow decision hands the invocation to Swift through the ordinary authorized_tool_execution path exactly once, every other outcome returns approval_denied to the model. Every resolution is mirrored to Swift as approval_resolved so a card can close whatever ended it, including owner changes. Expiry fires from the runtime, a disconnected relay client cancels its own dispatch, and terminal run events fail parked calls like any other pending tool call. The Swift hand-off itself moves into the same module so the whole relay side is covered by hermetic tests with injected timers.

Clearing a chat through journal_clear_turns now ends what that chat granted before the journal is cleared: the kernel closes the session's parked approvals and revokes the grants the person minted for it, because the main chat keeps one kernel session across clears and an "Allow for This Chat" grant would otherwise cover the new conversation. No tool reaches this; only the app's own clear message does.

Parking is on only after a client declares desktop_tool_approval_cards through the new client_capabilities message; until the Swift card ships no client does, and the relay keeps today's immediate approval_required. The runtime advertises desktop_tool_approval_requests so that client can require it first. Realtime voice keeps today's behavior because its Swift tool wait is 180s and would race the approval TTL.
The pi-mono extension waits 30s for a normal Swift tool result and gives up with a timeout error. A human answering an approval card needs longer than that, and a late approval would then run a send the model had already reported as failed. The six device tools that park behind an approval, capture_screen among them, now declare the long timeout class, which the extension maps to its 10 minute wait, and a test holds every gated tool to it; the approval TTL of 180s plus the 120s Swift executor timeout stays well inside it. Swift does not consume timeoutClass outside the generated surfaces, so the regenerated files only carry the new manifest digests.
The approval UX design doc keeps its design status and points at the device tool surface doc for the runtime half that now exists; the Swift card, the Claude Code lane and the voice surface stay deferred and section 24 still applies. The device tool surface doc gains the ask-and-answer table, the schema and resource rules that stop the model naming what a grant covers, the session-grant options and their 24 hour cap, the restart and expiry behavior, and the interim client-capability gate. The changelog fragment is kind none because nothing is user-visible until the Swift card lands.
Render the kernel's approval_requested frame as a card in the main chat, workstream and agent pill threads, matched to the asking session, with the exact target, the content preview, the expiry and the answers the runtime offered; answer through signed direct control (allow once, allow for this chat with a 1 h session grant, deny) and close the card on approval_resolved whoever ended it. Declare desktop_tool_approval_cards after the handshake so the relay parks a sensitive call instead of failing it, require desktop_tool_approval_requests from the runtime, and add tool_approval_snapshot and answer_tool_approval bridge actions so omi-ctl can drive the card without Accessibility permission.
… stall

In the live run the chat's 90 s tool no-progress guard revoked the turn and interrupted the bridge while a send_message card was waiting for the person, so the kernel cancelled the run and the card read Cancelled, not run. Teach StallDetector a waitingOnUser state the turn's tick loop sets from the approval store: while a card for the session is pending nothing promotes to slow or stalled, no tool is ever overdue, the tool row reads Waiting for your approval, and when the card closes every clock restarts from that moment; the stop button still cancels. Give the hermes and openclaw ACP adapters the same rule, treating a run the kernel holds in waiting_approval as progress for their 150 s no-progress cancel.
Six in-process captures from the packaged named bundle: a parked send_message card, the same card after Deny, parked list_message_chats and list_mail_messages cards, and the Allowed once state after an answer, with evidence.json recording agent capture through the automation bridge.
The five sensitive device tools described themselves as always requiring an approval dispatch, so the in-app model answered a send request by calling create_desktop_dispatch instead of send_message; that dispatch has no card, nothing renders it, and it never expires. Now that the kernel parks a direct call behind the approval card, say so in the descriptions and capability guidance and tell the model to call the tool directly, including capture_screen, which is now gated the same way, and regenerate the Swift tool surfaces for the changed manifest digest.
@sauranshbhardwaj
sauranshbhardwaj force-pushed the feat/device-tool-descriptions branch from 703fd95 to a1575f0 Compare October 15, 2026 17:18
@Git-on-my-level

Copy link
Copy Markdown
Collaborator

Reviewed the device tool approval flow end to end - this is careful work, and the security shape holds up on inspection.

What I verified against the diff:

  • desktop/macos/agent/src/runtime/run-tool-capability.ts + kernel-coordinator.ts: a sensitive tool call parks behind an invocation-bound approval dispatch (prepared ledger row + dispatch + run status + approval.requested in one transaction); resolve_desktop_dispatch is the only admit path, and the binding payload key ("invocation") is stripped from model-created dispatches, so a forged dispatch can never resolve as a tool approval.
  • Grants: "Allow for This Chat" mints a user-source grant scoped to the exact capability/operation/resource with a 1h expiry (DesktopToolApprovalStore.swift), kernel-capped at 24h (MAX_DESKTOP_DISPATCH_GRANT_TTL_MS). capture_screen is never grantable, and the realtime screenshot executor is now offered only to realtime_voice runs (omi-tool-manifest.test.ts) - typed chat cannot reach an ungated capture.
  • sqlite-store.ts startup reconciliation expires invocation-bound approval dispatches regardless of TTL, so a parked call never replays after a daemon restart (covered in sqlite-store.test.ts).
  • Swift side: DesktopToolApprovalCard.swift renders the full input or nothing (input_too_large_to_approve above 4 KiB), answers go through signed direct control, and StallDetector/ChatProvider treat a pending card as the person's turn (no stall banner, no 90s abort) - keyed by the turn's own session so the pill and workstreams each see their own card.
  • Docs: .github/agent-docs/agent-grants-approval-ux.mdx is updated from design-only to describing the live resolve_desktop_dispatch path; I checked the wording against the implementation and it reads accurate. device-tool-surface.md documents the ask/answer table and the stable resource refs (messages:chats, mail:inbox).

One CI note: the Hygiene failure on this head is a runner network flake - desktop-agent-runtime-contract-conformance died on an npm ECONNRESET during dependency install, not a manifest mismatch (Agent Runtime passed on the same head). Please re-run that job; no code change needed for it.

Since this rewrites the model-facing instructions for the messaging/mail/screenshot/AppleScript tools to "call directly, the app asks for approval", and it defines the grant semantics for those tools, it needs a human maintainer's sign-off on the approval UX and grant scope before merge (also given the stack on #20973/#20975/#20997). Leaving for maintainer review of the permission semantics - not urgent.


This review was generated by AI on behalf of the maintainer team.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature-fit-review Needs review of product/feature direction with Omi mission/vision macOS positive-signal Automation verified a genuine fix/quality contribution security-review Touches auth, provider routing, secrets, or security-sensitive surfaces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants