feat: thread app_bundle_id through open_app tool#7434
Conversation
The target-app-hints system resolves app names to bundleId pairs, and the Swift openApp() already accepts bundleId, but the value was lost between the daemon and Swift because it was never included in the tool schema or IPC payload. Changes: - Add app_bundle_id as optional param to computer_use_open_app tool definition (definitions.ts + TOOLS.json) - Inject session's targetAppBundleId as default when LLM omits it (computer-use-session.ts) - Add appBundleId field to Swift AgentAction struct (ActionTypes.swift) - Extract app_bundle_id from IPC input dict (Session.swift) - Pass action.appBundleId to openApp() call (ActionExecutor.swift) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 389ee232a2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!input.app_bundle_id && this.targetAppBundleId) { | ||
| input = { ...input, app_bundle_id: this.targetAppBundleId }; |
There was a problem hiding this comment.
Skip target bundle injection for allowed cross-app steps
This defaulting logic runs whenever app_bundle_id is missing, even when taskExplicitlyRequestsCrossApp() has already allowed switching to a different app_name. In cross-app tasks, that means a step like “open Slack” can be rewritten with the target app’s bundle ID, and because openApp resolves bundle IDs before names, the executor activates the wrong app and the workflow gets stuck. Restrict this injection to cases where the requested app is still the target app.
Useful? React with 👍 / 👎.
| case .openApp: | ||
| guard let appName = action.appName else { throw ExecutorError.appNotFound("(no name)") } | ||
| try await openApp(name: appName) | ||
| try await openApp(name: appName, bundleId: action.appBundleId) |
There was a problem hiding this comment.
Enforce target-app guard on bundle ID activations
Passing action.appBundleId directly into openApp introduces a scope bypass: the daemon’s fail-closed check only validates app_name, but bundle ID is the first resolver in openApp. A model step can therefore use a target-matching app_name plus a different app_bundle_id to activate another app in sessions that are supposed to be single-app constrained.
Useful? React with 👍 / 👎.
| // Inject targetAppBundleId when the LLM didn't provide one | ||
| if (!input.app_bundle_id && this.targetAppBundleId) { | ||
| input = { ...input, app_bundle_id: this.targetAppBundleId }; | ||
| } |
There was a problem hiding this comment.
🔴 Target app bundle ID injected for non-target app in cross-app workflows
When a user's task explicitly requests a cross-app workflow (e.g. "Copy from Chrome and paste into Safari"), the guard at lines 670-686 is bypassed because taskExplicitlyRequestsCrossApp() returns true. However, the bundle ID injection at lines 689-690 still unconditionally injects this.targetAppBundleId into the input when the LLM didn't provide one. This causes the target app's bundle ID to be attached to a request to open a different app.
Root Cause and Impact
Consider this scenario:
- Target app is Safari (
com.apple.Safari) - User task: "Copy text from Chrome and paste into Safari"
- LLM calls
computer_use_open_appwithapp_name: "Google Chrome"(noapp_bundle_id) - The guard is bypassed because
taskExplicitlyRequestsCrossApp()istrue - Line 689:
!input.app_bundle_idis true,this.targetAppBundleIdis"com.apple.Safari" - Line 690: injects
app_bundle_id: "com.apple.Safari"into the input - On the Swift side at
ActionExecutor.swift:350-371,openApp(name: "Google Chrome", bundleId: "com.apple.Safari")is called — bundle ID resolution takes priority, so Safari is activated instead of Chrome.
The fix should only inject the bundle ID when the requested app actually matches the target app (i.e., this.isTargetAppMatch(requestedApp) is true).
| // Inject targetAppBundleId when the LLM didn't provide one | |
| if (!input.app_bundle_id && this.targetAppBundleId) { | |
| input = { ...input, app_bundle_id: this.targetAppBundleId }; | |
| } | |
| // Inject targetAppBundleId only when the requested app matches the target app | |
| if (!input.app_bundle_id && this.targetAppBundleId && (!requestedApp || this.isTargetAppMatch(requestedApp))) { | |
| input = { ...input, app_bundle_id: this.targetAppBundleId }; | |
| } | |
Was this helpful? React with 👍 or 👎 to provide feedback.
In cross-app workflows (e.g., "copy from Chrome and paste into Safari"), the targetAppBundleId was unconditionally injected when the LLM didn't provide one. This caused the target app's bundle ID to be attached to requests for a different app, leading to wrong app activation since bundle ID takes priority in openApp resolution. Now only injects targetAppBundleId when the requested app matches the target app (or no app name is specified). Addresses review feedback from #7434. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
In cross-app workflows (e.g., "copy from Chrome and paste into Safari"), the targetAppBundleId was unconditionally injected when the LLM didn't provide one. This caused the target app's bundle ID to be attached to requests for a different app, leading to wrong app activation since bundle ID takes priority in openApp resolution. Now only injects targetAppBundleId when the requested app matches the target app (or no app name is specified). Addresses review feedback from #7434. Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
|
Addressed in #7461 |
Summary
app_bundle_idas an optional string parameter to thecomputer_use_open_apptool definition (bothdefinitions.tsandTOOLS.json)computer-use-session.ts, inject the session'stargetAppBundleIdas the defaultapp_bundle_idwhen the LLM doesn't provide oneappBundleId: String?field to the SwiftAgentActionstruct and wire it through the initializerapp_bundle_id/appBundleIdfrom the IPC input dictionary inSession.swift'smapToAgentAction()action.appBundleIdto the existingopenApp(name:bundleId:)method instead ofnilThis threads the bundleId from target-app-hints through the tool definition, IPC, and Swift action types so that
openAppcan use precise bundle identifier resolution instead of just fuzzy name matching.Test plan
app_bundle_idin the IPC action messagebundleIdtoopenApp(), which activates via bundle ID when availableapp_bundle_idis provided, behavior is identical to before (name-based resolution)🤖 Generated with Claude Code