@moretea-labs/forge 1.8.1 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. package/AGENTS.md +5 -2
  2. package/CHANGELOG.md +20 -0
  3. package/README.en.md +24 -3
  4. package/README.md +10 -6
  5. package/README.zh-CN.md +5 -3
  6. package/THIRD_PARTY_NOTICES.md +1 -2
  7. package/adapters/chatgpt/browser-delivery-runtime.ts +5 -3
  8. package/adapters/chatgpt/provider-delivery.ts +1 -1
  9. package/adapters/chatgpt/work-conversation-binding-store.ts +20 -1
  10. package/adapters/computer/chatgpt-extension-target.ts +295 -50
  11. package/adapters/computer/chrome-extension/background.js +227 -71
  12. package/adapters/computer/chrome-extension/content.js +181 -157
  13. package/adapters/computer/chrome-extension/manifest.json +3 -1
  14. package/adapters/computer/desktop-operator-provider.ts +18 -6
  15. package/adapters/computer/interaction-target-authority.ts +39 -9
  16. package/adapters/mcp/access-mode.ts +18 -39
  17. package/adapters/mcp/auth.ts +2 -6
  18. package/adapters/mcp/instructions.ts +2 -2
  19. package/adapters/mcp/multi-repository.ts +7 -28
  20. package/adapters/mcp/runtime-gateway/context-adapter.ts +79 -40
  21. package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +7 -15
  22. package/adapters/mcp/runtime-gateway/execution-tools.ts +7 -13
  23. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +16 -3
  24. package/adapters/mcp/runtime-gateway/process-tools.ts +3 -21
  25. package/adapters/mcp/runtime-gateway/router.ts +3 -5
  26. package/adapters/mcp/runtime-gateway/routing-policy.ts +17 -10
  27. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1 -1
  28. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +14 -9
  29. package/adapters/mcp/runtime-gateway/runtime-tools.ts +1 -5
  30. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +36 -1
  31. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +58 -4
  32. package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +7 -3
  33. package/adapters/mcp/runtime-gateway/work-adapter.ts +16 -16
  34. package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +0 -17
  35. package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +69 -12
  36. package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +26 -2
  37. package/adapters/mcp/server.ts +24 -3
  38. package/adapters/mcp/setup.ts +4 -10
  39. package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
  40. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +672 -2935
  41. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +1 -1
  42. package/adapters/mcp/tool-mapping/repository-tools.ts +52 -86
  43. package/adapters/mcp/toolset-names.ts +11 -42
  44. package/adapters/mcp/toolset.ts +16 -90
  45. package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
  46. package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
  47. package/adapters/mcp/transports/http-observation.ts +18 -6
  48. package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
  49. package/adapters/mcp/transports/http.ts +54 -14
  50. package/adapters/mcp/types.ts +0 -6
  51. package/assets/hooks/lib/session-state.sh +0 -38
  52. package/assets/hooks/post-bash.sh +1 -1
  53. package/assets/hooks/post-tool-observer.sh +0 -5
  54. package/assets/hooks/prompt-guard.sh +0 -55
  55. package/assets/partials/04-project-structure.partial.md +1 -1
  56. package/assets/partials/07-footer.partial.md +1 -1
  57. package/assets/partials-agents/02-operating-mode.partial.md +1 -1
  58. package/assets/partials-agents/08-deep-docs.partial.md +1 -1
  59. package/assets/reference-configs/agentic-development-flow.md +1 -1
  60. package/assets/reference-configs/external-tooling.md +0 -76
  61. package/assets/reference-configs/release-deploy.md +1 -1
  62. package/assets/templates/helpers/architecture-queue.sh +1 -1
  63. package/assets/templates/helpers/check-agent-tooling.sh +1 -327
  64. package/assets/templates/helpers/check-task-workflow.sh +1 -1
  65. package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
  66. package/assets/templates/helpers/migrate-project-template.sh +2 -2
  67. package/assets/workflow-contract.v1.json +0 -2
  68. package/docs/README.md +5 -2
  69. package/docs/architecture/CURRENT.md +114 -30
  70. package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +42 -0
  71. package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +15 -2
  72. package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +4 -2
  73. package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +109 -4
  74. package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
  75. package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
  76. package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
  77. package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
  78. package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
  79. package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
  80. package/docs/architecture/domains/public-surface.md +3 -5
  81. package/docs/architecture/domains/verification.md +5 -5
  82. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +2 -2
  83. package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
  84. package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
  85. package/docs/forge-plugin-management.md +14 -2
  86. package/docs/operations/controller-browser-plugin.md +23 -0
  87. package/docs/operations/features.md +6 -7
  88. package/docs/operations/features.zh-CN.md +1 -1
  89. package/docs/operations/github-repository.md +4 -4
  90. package/docs/operations/github-semantic-release-projection.md +20 -0
  91. package/docs/operations/homebrew.md +2 -2
  92. package/docs/operations/mcp-tool-exposure.md +13 -54
  93. package/docs/operations/runtime-performance-diagnostics.md +8 -0
  94. package/docs/operations/standalone-disaster-recovery.md +1 -1
  95. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  96. package/docs/tutorials/03-first-repository-task.md +2 -0
  97. package/docs/wiki/Computer-and-Plugins.md +78 -0
  98. package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
  99. package/docs/wiki/Home.md +16 -2
  100. package/docs/wiki/Implementation.md +24 -12
  101. package/docs/wiki/Installation.md +28 -32
  102. package/docs/wiki/Integrations.md +4 -0
  103. package/docs/wiki/Quick-Start.md +35 -22
  104. package/docs/wiki/Releases-and-Upgrades.md +15 -25
  105. package/docs/wiki/_Sidebar.md +5 -1
  106. package/package.json +1 -12
  107. package/packages/kernel/controller/infrastructure/controller-session-store.ts +63 -38
  108. package/packages/kernel/identity/infrastructure/activity-projection-store.ts +10 -0
  109. package/packages/kernel/scheduler/application/eligibility.ts +4 -1
  110. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +13 -10
  111. package/packages/kernel/work/application/work-service.ts +1 -0
  112. package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
  113. package/packages/kernel/work/domain/state-machine.ts +9 -2
  114. package/packages/kernel/work/infrastructure/work-contract-store.ts +28 -1
  115. package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
  116. package/packages/plugin-runtime/computer/conversation-target.ts +17 -7
  117. package/scripts/assemble-template.ts +11 -72
  118. package/scripts/check-mcp-compatibility.ts +55 -74
  119. package/scripts/check-package-identity.mjs +0 -5
  120. package/scripts/check-runtime-architecture.mjs +305 -28
  121. package/scripts/init-project.sh +0 -46
  122. package/scripts/install-ios-hid-toolchain.sh +60 -0
  123. package/scripts/install-standalone-recovery.ts +1 -1
  124. package/scripts/lib/project-init-lib.sh +3 -3
  125. package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
  126. package/scripts/open-source-audit-allowlist.txt +0 -1
  127. package/scripts/run-governed-gate.ts +2 -0
  128. package/scripts/smoke-ios-physical-device.ts +38 -10
  129. package/scripts/smoke-mcp-http-runtime.ts +7 -8
  130. package/scripts/smoke-mcp-tool-surface.ts +27 -41
  131. package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +6 -3
  132. package/scripts/sync-generated-authority.mjs +0 -1
  133. package/src/cli/agent-jobs/executor-health.ts +1 -1
  134. package/src/cli/agent-jobs/integration.ts +2 -2
  135. package/src/cli/chatgpt-browser/file-policy.ts +2 -2
  136. package/src/cli/commands/doctor.ts +1 -143
  137. package/src/cli/commands/global-runtime.ts +3 -196
  138. package/src/cli/commands/init-hook.ts +6 -23
  139. package/src/cli/commands/init.ts +3 -252
  140. package/src/cli/commands/mcp.ts +0 -5
  141. package/src/cli/commands/prompt-guard-decision.ts +0 -2
  142. package/src/cli/commands/recovery.ts +2 -14
  143. package/src/cli/commands/repository.ts +17 -1
  144. package/src/cli/commands/setup-execution.ts +41 -0
  145. package/src/cli/controller/completion-orchestrator.ts +1 -1
  146. package/src/cli/controller/composite-operations.ts +1 -1
  147. package/src/cli/controller/context/instruction-context.ts +1 -1
  148. package/src/cli/controller/context/known-paths.ts +2 -2
  149. package/src/cli/controller/context/query-planning.ts +11 -4
  150. package/src/cli/controller/context/rule-resolution.ts +2 -2
  151. package/src/cli/controller/context/source-materializer.ts +2 -2
  152. package/src/cli/controller/context/structural-provider.ts +84 -0
  153. package/src/cli/controller/context/structural.ts +6 -6
  154. package/src/cli/controller/context/types.ts +8 -9
  155. package/src/cli/controller/context-pack.ts +97 -76
  156. package/src/cli/controller/operational-plan.ts +1 -1
  157. package/src/cli/controller/systemd-user.ts +4 -10
  158. package/src/cli/editing/edit-session.ts +17 -5
  159. package/src/cli/github/github.ts +4 -4
  160. package/src/cli/github/semantic-projection.ts +174 -0
  161. package/src/cli/hook/prompt-intents.ts +0 -9
  162. package/src/cli/index.ts +1 -43
  163. package/src/cli/local-bridge/connector-freshness.ts +1 -12
  164. package/src/cli/local-bridge/console-automations.ts +1 -1
  165. package/src/cli/local-bridge/console-view-models.ts +1 -1
  166. package/src/cli/local-bridge/facade-api.ts +0 -2
  167. package/src/cli/repositories/command-classifier.ts +0 -9
  168. package/src/cli/repositories/command-scope.ts +2 -2
  169. package/src/cli/repositories/locks.ts +20 -0
  170. package/src/cli/repositories/registry.ts +2 -1
  171. package/src/cli/repositories/safe-patch.ts +2 -12
  172. package/src/cli/repositories/structured-git.ts +39 -3
  173. package/src/cli/repository/inspector.ts +185 -343
  174. package/src/effects/process-runner.ts +17 -1
  175. package/src/runtime/context/generic-lsp-provider.ts +1 -1
  176. package/src/runtime/context/semantic-navigation-contract.ts +7 -0
  177. package/src/runtime/context/semantic-navigation.ts +1 -1
  178. package/src/runtime/control-plane/cleanup-artifact-retention.ts +4 -4
  179. package/src/runtime/control-plane/execution/work-delivery-service.ts +150 -31
  180. package/src/runtime/control-plane/execution/work-handle-authority.ts +12 -4
  181. package/src/runtime/control-plane/execution/work-handle-store.ts +53 -1
  182. package/src/runtime/control-plane/execution/work-operation-service.ts +20 -10
  183. package/src/runtime/control-plane/execution/work-preparation-service.ts +108 -4
  184. package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
  185. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +28 -13
  186. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +5 -1
  187. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +1 -1
  188. package/src/runtime/control-plane/facade/capability-registry.ts +2 -0
  189. package/src/runtime/control-plane/facade/work-continuation.ts +1 -1
  190. package/src/runtime/control-plane/generated-cache-retention.ts +114 -63
  191. package/src/runtime/control-plane/global-scheduler/maintenance.ts +20 -8
  192. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -11
  193. package/src/runtime/control-plane/governance/external-effects.ts +1 -1
  194. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +1 -0
  195. package/src/runtime/control-plane/runtime-cleanup.ts +12 -93
  196. package/src/runtime/evidence/sensitive-output.ts +22 -2
  197. package/src/runtime/execution/jobs/child-reference.ts +11 -0
  198. package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
  199. package/src/runtime/execution/process-runtime/command-facade.ts +3 -3
  200. package/src/runtime/execution/process-runtime/runtime.ts +20 -16
  201. package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
  202. package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
  203. package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
  204. package/src/runtime/execution/workers/executor.ts +1 -2
  205. package/src/runtime/maintenance/cleanup.ts +41 -5
  206. package/src/runtime/plugins/action-application.ts +12 -1
  207. package/src/runtime/plugins/app-store-connect-adapter.ts +252 -4
  208. package/src/runtime/plugins/browser-adapter.ts +381 -313
  209. package/src/runtime/plugins/browser-macos-bridge.ts +114 -112
  210. package/src/runtime/plugins/browser-manifest-surface.ts +2 -1
  211. package/src/runtime/plugins/browser-registration.ts +2 -0
  212. package/src/runtime/plugins/browser-runtime.ts +26 -35
  213. package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +11 -2
  214. package/src/runtime/plugins/computer-registration.ts +76 -9
  215. package/src/runtime/plugins/github-adapter.ts +30 -3
  216. package/src/runtime/plugins/gmail-adapter.ts +166 -1
  217. package/src/runtime/plugins/google-shared.ts +10 -5
  218. package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
  219. package/src/runtime/plugins/ios-adapter.ts +45 -40
  220. package/src/runtime/plugins/ios-agent-device.ts +54 -4
  221. package/src/runtime/plugins/ios-device-ownership.ts +103 -0
  222. package/src/runtime/plugins/ios-physical-device.ts +497 -79
  223. package/src/runtime/plugins/local-system-adapter.ts +1 -99
  224. package/src/runtime/plugins/store.ts +80 -7
  225. package/src/runtime/plugins/types.ts +4 -0
  226. package/src/runtime/projections/git-status-sampler.ts +2 -19
  227. package/src/runtime/recovery/actions.ts +1 -1
  228. package/src/runtime/recovery/lifecycle-inventory.ts +1 -11
  229. package/src/runtime/recovery/probe.ts +2 -2
  230. package/src/runtime/release/release-coordinator.ts +20 -4
  231. package/src/runtime/root/controller-progression-composition.ts +67 -5
  232. package/src/runtime/root/package-connector-service.ts +123 -11
  233. package/src/runtime/root/package-runtime-release.ts +6 -18
  234. package/src/runtime/root/package-runtime-service.ts +7 -13
  235. package/src/runtime/root/release-execution-canary.ts +18 -5
  236. package/src/runtime/root/release-manifest.ts +0 -34
  237. package/src/runtime/root/release-materialize.ts +0 -78
  238. package/src/runtime/root/runtime.ts +1 -2
  239. package/src/runtime/root/scheduled-controller-composition.ts +2 -1
  240. package/src/runtime/root/service.ts +90 -11
  241. package/src/runtime/root/types.ts +0 -6
  242. package/src/runtime/root/workflow-supervisor-composition.ts +207 -19
  243. package/src/runtime/root/workflow-supervisor-runtime.ts +365 -10
  244. package/src/runtime/safe-tooling/google-credential-store.ts +1 -11
  245. package/src/runtime/safe-tooling/ios-development.ts +22 -8
  246. package/src/runtime/safe-tooling/job-summary.ts +1 -1
  247. package/src/runtime/safe-tooling/workspace-auth.ts +4 -1
  248. package/src/runtime/shared/atomic-text-file.ts +21 -0
  249. package/src/runtime/shared/json-files.ts +8 -3
  250. package/src/runtime/standalone-recovery/core.ts +189 -174
  251. package/src/runtime/standalone-recovery/entry.ts +62 -30
  252. package/src/runtime/standalone-recovery/operation-lock.ts +11 -1
  253. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -0
  254. package/src/runtime/workflow/schedules/engine.ts +36 -14
  255. package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
  256. package/src/runtime/workflow/schedules/work-continuation.ts +51 -6
  257. package/supervisor/client.ts +31 -14
  258. package/supervisor/control-plane.ts +210 -115
  259. package/supervisor/forge-validators.ts +88 -21
  260. package/supervisor/native-browser-adapter.ts +393 -28
  261. package/supervisor/native-messaging/host.ts +21 -2
  262. package/supervisor/protocol.ts +74 -26
  263. package/supervisor/server.ts +16 -8
  264. package/supervisor/store.ts +476 -57
  265. package/supervisor/types.ts +19 -13
  266. package/SKILL.md +0 -427
  267. package/adapters/computer/chatgpt-target-router.ts +0 -103
  268. package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +0 -87
  269. package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
  270. package/adapters/mcp/toolset-selection.ts +0 -41
  271. package/agents/openai.yaml +0 -4
  272. package/assets/skill-hooks.json +0 -41
  273. package/assets/skill-version.json +0 -189
  274. package/assets/skills/claude-review/SKILL.md +0 -231
  275. package/assets/skills/codex-review/SKILL.md +0 -103
  276. package/assets/templates/helpers/check-skill-version.ts +0 -193
  277. package/scripts/check-skill-version.ts +0 -5
  278. package/scripts/ensure-codegraph.sh +0 -16
  279. package/scripts/run-skill-hook.ts +0 -244
  280. package/scripts/sync-codex-installed-copies.sh +0 -182
  281. package/src/cli/commands/tools.ts +0 -167
  282. package/src/cli/mcp/access-mode.ts +0 -2
  283. package/src/cli/mcp/access-tools.ts +0 -2
  284. package/src/cli/mcp/audit.ts +0 -2
  285. package/src/cli/mcp/auth.ts +0 -2
  286. package/src/cli/mcp/instructions.ts +0 -2
  287. package/src/cli/mcp/legacy-context.ts +0 -2
  288. package/src/cli/mcp/legacy-tool-service.ts +0 -2
  289. package/src/cli/mcp/multi-repository.ts +0 -2
  290. package/src/cli/mcp/oauth.ts +0 -2
  291. package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
  292. package/src/cli/mcp/paths.ts +0 -2
  293. package/src/cli/mcp/policy.ts +0 -2
  294. package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
  295. package/src/cli/mcp/proxy-env.ts +0 -2
  296. package/src/cli/mcp/readiness-projection.ts +0 -2
  297. package/src/cli/mcp/redaction.ts +0 -2
  298. package/src/cli/mcp/repo.ts +0 -2
  299. package/src/cli/mcp/repository-tools.ts +0 -2
  300. package/src/cli/mcp/server.ts +0 -2
  301. package/src/cli/mcp/setup.ts +0 -2
  302. package/src/cli/mcp/tool-capability-cache.ts +0 -2
  303. package/src/cli/mcp/tool-contract.ts +0 -2
  304. package/src/cli/mcp/tools.ts +0 -2
  305. package/src/cli/mcp/toolset-names.ts +0 -2
  306. package/src/cli/mcp/toolset-selection.ts +0 -2
  307. package/src/cli/mcp/toolset.ts +0 -2
  308. package/src/cli/mcp/transports/http.ts +0 -2
  309. package/src/cli/mcp/transports/session-registry.ts +0 -2
  310. package/src/cli/mcp/transports/stdio.ts +0 -2
  311. package/src/cli/mcp/types.ts +0 -2
  312. package/src/cli/mcp/umbrella-tools.ts +0 -2
  313. package/src/cli/tools/codegraph.ts +0 -695
  314. package/src/core/adoption/helper-wrapper-plan.ts +0 -159
  315. package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
  316. package/src/runtime/context/codegraph-read-provider.ts +0 -611
  317. package/src/runtime/context/codegraph-sidecar.cjs +0 -248
  318. package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
  319. package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
  320. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
  321. package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
  322. package/src/runtime/gateway/mcp/process-tools.ts +0 -2
  323. package/src/runtime/gateway/mcp/router.ts +0 -2
  324. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
  325. package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
  326. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
  327. package/src/runtime/plugins/browser-provider-registry.ts +0 -2
  328. package/src/runtime/plugins/computer-chatgpt-macos-target.ts +0 -891
@@ -1,5 +1,5 @@
1
1
  import { execFile } from 'child_process';
2
- import { existsSync, readFileSync, writeFileSync } from 'fs';
2
+ import { existsSync, readFileSync, statSync, writeFileSync } from 'fs';
3
3
  import { homedir } from 'os';
4
4
  import { basename, extname } from 'path';
5
5
  import {
@@ -16,6 +16,7 @@ import type { BrowserNativeAttachAttempt, BrowserNativeProduct } from '../../../
16
16
  const RECORD_SEPARATOR = String.fromCharCode(30);
17
17
  const DEFAULT_NATIVE_TIMEOUT_MS = 5_000;
18
18
  const MAX_NATIVE_OUTPUT_BYTES = 1_048_576;
19
+ const MAX_NATIVE_BROWSER_FILE_ATTACH_BYTES = 16 * 1024 * 1024;
19
20
 
20
21
  export type MacOsBrowserProduct = BrowserNativeProduct;
21
22
 
@@ -136,6 +137,8 @@ const defaultRuntimeHooks: MacOsBrowserRuntimeHooks = {
136
137
  appExists: (path) => existsSync(path),
137
138
  processRunning: async (processName, timeoutMs) => {
138
139
  try {
140
+ // Process existence only: the canonical Computer provider owns exact
141
+ // GUI PID selection and rejects ambiguous or unverified targets.
139
142
  await execFileText('/usr/bin/pgrep', ['-x', processName], timeoutMs);
140
143
  return true;
141
144
  } catch (error) {
@@ -222,6 +225,7 @@ async function runCreateTabAutomation(
222
225
  testArgs: string[],
223
226
  timeoutMs: number,
224
227
  ): Promise<MacOsBrowserCreateTabEvidence> {
228
+ await assertMacOsBrowserApplicationRunning(request.product, timeoutMs);
225
229
  if (runtimeHooks.runAppleScript) {
226
230
  const value = await runtimeHooks.runAppleScript(testScript, testArgs, timeoutMs);
227
231
  return parseMacOsBrowserCreateTabBrokerResult({ value });
@@ -338,6 +342,7 @@ async function sendBrowserTrustedInput(
338
342
  request: BrowserAutomationTrustedInputRequest,
339
343
  timeoutMs: number,
340
344
  ): Promise<void> {
345
+ await assertMacOsBrowserApplicationRunning(request.product, timeoutMs);
341
346
  if (runtimeHooks.trustedInput) {
342
347
  await runtimeHooks.trustedInput(request, timeoutMs);
343
348
  return;
@@ -647,13 +652,15 @@ if preferredWindowId is not "" then
647
652
  end if
648
653
  end repeat
649
654
  end if
655
+ if preferredWindowId is not "" and targetWindow is missing value then error "FORGE_BROWSER_PREFERRED_WINDOW_MISSING:" & preferredWindowId
650
656
  if targetWindow is missing value and (count of windows) is greater than 0 then set targetWindow to front window
651
657
  if targetWindow is missing value then
652
658
  set targetWindow to make new window
653
659
  set targetTab to active tab of targetWindow
654
660
  set URL of targetTab to targetUrl
655
661
  else
656
- set targetWindow to front window
662
+ -- Preserve the selected background window; front-window reassignment here
663
+ -- previously discarded the caller's stable target and opened the wrong tab.
657
664
  set originalActiveTabId to ((id of active tab of targetWindow) as text)
658
665
  set targetTab to make new tab at end of tabs of targetWindow with properties {URL:targetUrl}
659
666
  -- Restore the previously active tab by identity, never by a stale position:
@@ -946,12 +953,25 @@ export class MacOsAppleEventsPage {
946
953
  return await runBrowserAutomationText(effectiveRequest, effectiveScript, args, timeoutMs);
947
954
  } catch (error) {
948
955
  const message = error instanceof Error ? error.message : String(error);
949
- // Keep the broker's bounded timeout/serialization identity visible to
950
- // the sole Supervisor retry policy. Flattening both to the generic
951
- // operation error makes a blocked tab hot-loop against the global lane.
952
- const brokerFailure = /\b(BROWSER_AUTOMATION_TIMEOUT|BROWSER_AUTOMATION_SERIALIZATION_BUSY)\b/.exec(message)?.[1];
956
+ // Preserve the provider's *typed* process-identity refusal. In particular,
957
+ // do not turn ambiguous Chrome app roots into a retryable generic error:
958
+ // retries must never guess another user's running browser instance.
959
+ const nativeCode = error instanceof AssistantPluginError ? error.code : undefined;
960
+ const nativeFailure = nativeCode === 'BROWSER_AUTOMATION_PROCESS_TARGET_AMBIGUOUS'
961
+ || nativeCode === 'BROWSER_AUTOMATION_PROCESS_IDENTITY_LOST'
962
+ || nativeCode === 'BROWSER_AUTOMATION_PROCESS_PROBE_UNAVAILABLE'
963
+ || nativeCode === 'BROWSER_AUTOMATION_NATIVE_EVENT_FAILED'
964
+ || nativeCode === 'BROWSER_AUTOMATION_MUTATION_OUTCOME_UNKNOWN'
965
+ ? nativeCode : undefined;
966
+ const nonRetryable = nativeFailure && nativeFailure !== 'BROWSER_AUTOMATION_PROCESS_PROBE_UNAVAILABLE';
967
+ // An Apple Event can mutate the page even if its callback fails.
968
+ // Preserve uncertainty and never retry such a mutation blindly.
969
+ const outcomeUnknown = nativeFailure === 'BROWSER_AUTOMATION_MUTATION_OUTCOME_UNKNOWN'
970
+ || (error instanceof AssistantPluginError && error.effectOutcome === 'outcome_unknown');
971
+ const brokerFailure = nativeFailure ?? /\b(BROWSER_AUTOMATION_TIMEOUT|BROWSER_AUTOMATION_SERIALIZATION_BUSY)\b/.exec(message)?.[1];
953
972
  throw new AssistantPluginError(brokerFailure ?? 'PLUGIN_BROWSER_NATIVE_OPERATION_FAILED', `${this.browser.appName} Apple Events operation failed: ${message}`, {
954
- retryable: true,
973
+ retryable: !nonRetryable && !outcomeUnknown,
974
+ effectOutcome: outcomeUnknown ? 'outcome_unknown' : 'failed',
955
975
  details: { browserProduct: this.browser.product, bundleId: this.browser.bundleId },
956
976
  });
957
977
  }
@@ -980,8 +1000,12 @@ export class MacOsAppleEventsPage {
980
1000
  if (macOsBrowserJavaScriptAutomationDisabled(error)) {
981
1001
  throw new AssistantPluginError(
982
1002
  'PLUGIN_BROWSER_JAVASCRIPT_PERMISSION_REQUIRED',
983
- `${this.browser.appName} allows tab/window automation, but DOM actions require Settings > Privacy > Apple Events > Allow JavaScript from Apple Events.`,
984
- { retryable: false, details: { browserProduct: this.browser.product, bundleId: this.browser.bundleId } },
1003
+ `${this.browser.appName} rejected JavaScript from Apple Events for the browser instance addressed by macOS automation. This does not prove that the user's visible Chrome instance has this setting disabled. Verify the exact Chrome process/profile and View > Developer > Allow JavaScript from Apple Events before changing settings.`,
1004
+ { retryable: false, details: {
1005
+ browserProduct: this.browser.product, bundleId: this.browser.bundleId,
1006
+ processIdentity: 'unverified',
1007
+ ...(this.targetRef ? { windowId: this.targetRef.windowId, tabId: this.targetRef.tabId } : {}),
1008
+ } },
985
1009
  );
986
1010
  }
987
1011
  throw error;
@@ -1119,15 +1143,16 @@ export class MacOsAppleEventsPage {
1119
1143
  if (filePaths.length === 0 || filePaths.length > 32) {
1120
1144
  throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Native browser file input accepts 1-32 local files.', { retryable: false });
1121
1145
  }
1122
- for (const filePath of filePaths) {
1146
+
1147
+ // Validate the entire batch before sending bytes or changing input.files.
1148
+ const attachments = filePaths.map((filePath) => {
1123
1149
  if (!filePath || !existsSync(filePath)) {
1124
1150
  throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Local file does not exist.', { retryable: false });
1125
1151
  }
1126
- }
1127
-
1128
- if (filePaths.length === 1) {
1129
- const filePath = filePaths[0];
1130
- const bytes = readFileSync(filePath);
1152
+ const metadata = statSync(filePath);
1153
+ if (!metadata.isFile()) {
1154
+ throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Native browser can attach only regular files.', { retryable: false });
1155
+ }
1131
1156
  const fileName = basename(filePath);
1132
1157
  const extension = extname(fileName).toLowerCase();
1133
1158
  const mimeType = extension === '.pdf' ? 'application/pdf'
@@ -1135,123 +1160,98 @@ export class MacOsAppleEventsPage {
1135
1160
  : extension === '.png' ? 'image/png'
1136
1161
  : extension === '.jpg' || extension === '.jpeg' ? 'image/jpeg'
1137
1162
  : 'application/octet-stream';
1138
- const bufferKey = `__forgeLocalFile_${Date.now()}_${Math.random().toString(16).slice(2)}`;
1139
- const encoded = bytes.toString('base64');
1140
- const chunkChars = 192 * 1024;
1141
- try {
1163
+ return { filePath, fileName, size: metadata.size, mimeType };
1164
+ });
1165
+ const totalBytes = attachments.reduce((sum, attachment) => sum + attachment.size, 0);
1166
+ if (totalBytes > MAX_NATIVE_BROWSER_FILE_ATTACH_BYTES) {
1167
+ throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_FILE_TOO_LARGE',
1168
+ 'Native browser upload is bounded to 16 MiB per batch; select a smaller batch instead of queuing unbounded Apple Events.',
1169
+ { retryable: false, details: { totalBytes, limitBytes: MAX_NATIVE_BROWSER_FILE_ATTACH_BYTES } });
1170
+ }
1171
+
1172
+ // A read-only DOM preflight must pass before staging starts. In particular,
1173
+ // multi-file uploads cannot reset a previous file selection on preflight failure.
1174
+ await this.evaluate(`(() => { // __forgeFileInputPreflight
1175
+ const matches = document.querySelectorAll(${JSON.stringify(selector)});
1176
+ if (matches.length !== 1) throw new Error('File input selector must resolve to exactly one element.');
1177
+ const element = matches[0];
1178
+ if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
1179
+ if (element.disabled) throw new Error('Target file input is disabled.');
1180
+ if (${attachments.length > 1} && !element.multiple) throw new Error('Target input does not allow multiple files.');
1181
+ return true;
1182
+ })()`);
1183
+
1184
+ const batchKey = `__forgeStagedFiles_${Date.now()}_${Math.random().toString(16).slice(2)}`;
1185
+ const bufferKey = `__forgeEncodedFile_${Date.now()}_${Math.random().toString(16).slice(2)}`;
1186
+ const expected = attachments.map(({ fileName, size }) => ({ name: fileName, size }));
1187
+ const chunkChars = 192 * 1024;
1188
+ try {
1189
+ for (const attachment of attachments) {
1190
+ // Stage one file at a time. No DOM selection or change event occurs
1191
+ // until the complete batch has arrived and its sizes have been checked.
1192
+ const encoded = readFileSync(attachment.filePath).toString('base64');
1142
1193
  await this.evaluate(`(() => { globalThis[${JSON.stringify(bufferKey)}] = ''; return true; })()`);
1143
1194
  for (let offset = 0; offset < encoded.length; offset += chunkChars) {
1144
1195
  const chunk = encoded.slice(offset, offset + chunkChars);
1145
1196
  await this.evaluate(`(() => { globalThis[${JSON.stringify(bufferKey)}] += ${JSON.stringify(chunk)}; return true; })()`);
1146
1197
  }
1147
- const attached = await this.evaluate<{ name?: string; size?: number }>(`(() => {
1148
- const element = document.querySelector(${JSON.stringify(selector)});
1149
- if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
1150
- const expectedName = ${JSON.stringify(fileName)};
1151
- const expectedSize = ${bytes.length};
1198
+ const staged = await this.evaluate<{ name?: string; size?: number }>(`(() => { // __forgeFileInputStaged
1152
1199
  const encoded = String(globalThis[${JSON.stringify(bufferKey)}] || '');
1153
1200
  const binary = atob(encoded);
1154
1201
  const data = new Uint8Array(binary.length);
1155
1202
  for (let i = 0; i < binary.length; i += 1) data[i] = binary.charCodeAt(i);
1156
- const file = new File([data], expectedName, { type: ${JSON.stringify(mimeType)} });
1157
- const transfer = new DataTransfer();
1158
- transfer.items.add(file);
1159
- element.files = transfer.files;
1160
- element.dispatchEvent(new Event('input', { bubbles: true }));
1161
- element.dispatchEvent(new Event('change', { bubbles: true }));
1162
- const selected = element.files && element.files[0];
1163
- return selected ? { name: selected.name, size: selected.size, expectedName, expectedSize } : null;
1203
+ const file = new File([data], ${JSON.stringify(attachment.fileName)}, { type: ${JSON.stringify(attachment.mimeType)} });
1204
+ const staged = globalThis[${JSON.stringify(batchKey)}] || [];
1205
+ staged.push(file);
1206
+ globalThis[${JSON.stringify(batchKey)}] = staged;
1207
+ delete globalThis[${JSON.stringify(bufferKey)}];
1208
+ return { name: file.name, size: file.size };
1164
1209
  })()`);
1165
- if (attached?.name !== fileName || attached?.size !== bytes.length) {
1166
- throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_FILE_ATTACH_FAILED', 'Native browser did not retain the requested local file on the target input.', {
1167
- retryable: true,
1168
- details: { expectedName: fileName, expectedSize: bytes.length, actualName: attached?.name, actualSize: attached?.size },
1169
- });
1210
+ if (staged?.name !== attachment.fileName || staged?.size !== attachment.size) {
1211
+ throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_FILE_ATTACH_FAILED',
1212
+ 'Browser did not stage the expected local file.', { retryable: false });
1170
1213
  }
1171
- } finally {
1172
- await this.evaluate(`(() => { try { delete globalThis[${JSON.stringify(bufferKey)}]; } catch {} return true; })()`).catch(() => undefined);
1173
1214
  }
1174
- return;
1175
- }
1176
1215
 
1177
- const attachments = filePaths.map((filePath) => {
1178
- const bytes = readFileSync(filePath);
1179
- const fileName = basename(filePath);
1180
- const extension = extname(fileName).toLowerCase();
1181
- const mimeType = extension === '.pdf' ? 'application/pdf'
1182
- : extension === '.zip' ? 'application/zip'
1183
- : extension === '.png' ? 'image/png'
1184
- : extension === '.jpg' || extension === '.jpeg' ? 'image/jpeg'
1185
- : 'application/octet-stream';
1186
- return {
1187
- bytes,
1188
- fileName,
1189
- mimeType,
1190
- bufferKey: `__forgeLocalFile_${Date.now()}_${Math.random().toString(16).slice(2)}`,
1191
- };
1192
- });
1193
-
1194
- if (attachments.length > 1) {
1195
- await this.evaluate(`(() => {
1196
- const element = document.querySelector(${JSON.stringify(selector)});
1197
- if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
1198
- if (!element.multiple) throw new Error('Target input does not allow multiple files.');
1216
+ // One commit to input.files and one event pair, after all File objects
1217
+ // are staged and verified. Failed transfer never partially selects files.
1218
+ const selected = await this.evaluate<Array<{ name?: string; size?: number }>>(`(() => { // __forgeFileInputCommit
1219
+ const matches = document.querySelectorAll(${JSON.stringify(selector)});
1220
+ if (matches.length !== 1) throw new Error('File input selector changed during transfer.');
1221
+ const element = matches[0];
1222
+ if (!(element instanceof HTMLInputElement) || element.type !== 'file' || element.disabled) {
1223
+ throw new Error('File input became unavailable during transfer.');
1224
+ }
1225
+ if (${attachments.length > 1} && !element.multiple) throw new Error('File input no longer supports multiple files.');
1226
+ const staged = globalThis[${JSON.stringify(batchKey)}];
1227
+ const expected = ${JSON.stringify(expected)};
1228
+ if (!Array.isArray(staged) || staged.length !== expected.length ||
1229
+ !staged.every((file, i) => file instanceof File && file.name === expected[i].name && file.size === expected[i].size)) {
1230
+ throw new Error('Staged file batch does not match the requested selection.');
1231
+ }
1199
1232
  const transfer = new DataTransfer();
1233
+ staged.forEach((file) => transfer.items.add(file));
1200
1234
  element.files = transfer.files;
1201
- return true;
1202
- })()`);
1203
- }
1204
-
1205
- const chunkChars = 192 * 1024;
1206
- try {
1207
- for (let index = 0; index < attachments.length; index += 1) {
1208
- const attachment = attachments[index];
1209
- const encoded = attachment.bytes.toString('base64');
1210
- await this.evaluate(`(() => { globalThis[${JSON.stringify(attachment.bufferKey)}] = ''; return true; })()`);
1211
- for (let offset = 0; offset < encoded.length; offset += chunkChars) {
1212
- const chunk = encoded.slice(offset, offset + chunkChars);
1213
- await this.evaluate(`(() => { globalThis[${JSON.stringify(attachment.bufferKey)}] += ${JSON.stringify(chunk)}; return true; })()`);
1235
+ const selected = Array.from(element.files || []).map((file) => ({ name: file.name, size: file.size }));
1236
+ if (selected.length !== expected.length ||
1237
+ !selected.every((file, i) => file.name === expected[i].name && file.size === expected[i].size)) {
1238
+ throw new Error('Browser did not retain the requested file selection.');
1214
1239
  }
1215
- const append = attachments.length > 1 && index > 0;
1216
- await this.evaluate(`(() => {
1217
- const element = document.querySelector(${JSON.stringify(selector)});
1218
- if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
1219
- const expectedName = ${JSON.stringify(attachment.fileName)};
1220
- const expectedSize = ${attachment.bytes.length};
1221
- const encoded = String(globalThis[${JSON.stringify(attachment.bufferKey)}] || '');
1222
- const binary = atob(encoded);
1223
- const data = new Uint8Array(binary.length);
1224
- for (let i = 0; i < binary.length; i += 1) data[i] = binary.charCodeAt(i);
1225
- const file = new File([data], expectedName, { type: ${JSON.stringify(attachment.mimeType)} });
1226
- const transfer = new DataTransfer();
1227
- if (${append ? 'true' : 'false'}) {
1228
- for (const existing of Array.from(element.files || [])) transfer.items.add(existing);
1229
- }
1230
- transfer.items.add(file);
1231
- element.files = transfer.files;
1232
- return Array.from(element.files || []).map((selected) => ({ name: selected.name, size: selected.size }));
1233
- })()`);
1234
- }
1235
-
1236
- const selected = await this.evaluate<Array<{ name?: string; size?: number }>>(`(() => {
1237
- const element = document.querySelector(${JSON.stringify(selector)});
1238
- if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
1239
1240
  element.dispatchEvent(new Event('input', { bubbles: true }));
1240
1241
  element.dispatchEvent(new Event('change', { bubbles: true }));
1241
- return Array.from(element.files || []).map((file) => ({ name: file.name, size: file.size }));
1242
+ return selected;
1242
1243
  })()`);
1243
- const expected = attachments.map((attachment) => ({ name: attachment.fileName, size: attachment.bytes.length }));
1244
- const matches = selected.length === expected.length && expected.every((item, index) => selected[index]?.name === item.name && selected[index]?.size === item.size);
1245
- if (!matches) {
1246
- throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_FILE_ATTACH_FAILED', 'Native browser did not retain the requested local files on the target input.', {
1247
- retryable: true,
1248
- details: { expected, actual: selected },
1249
- });
1244
+ if (selected.length !== expected.length ||
1245
+ !expected.every((item, index) => selected[index]?.name === item.name && selected[index]?.size === item.size)) {
1246
+ throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_FILE_ATTACH_FAILED',
1247
+ 'Native browser did not retain the requested local files on the target input.', { retryable: false });
1250
1248
  }
1251
1249
  } finally {
1252
- for (const attachment of attachments) {
1253
- await this.evaluate(`(() => { try { delete globalThis[${JSON.stringify(attachment.bufferKey)}]; } catch {} return true; })()`).catch(() => undefined);
1254
- }
1250
+ // Unknown effect outcomes must be reconciled by observation, never by replay.
1251
+ await this.evaluate(`(() => {
1252
+ try { delete globalThis[${JSON.stringify(bufferKey)}]; delete globalThis[${JSON.stringify(batchKey)}]; } catch {}
1253
+ return true;
1254
+ })()`).catch(() => undefined);
1255
1255
  }
1256
1256
  }
1257
1257
 
@@ -1581,7 +1581,9 @@ async function resolveCurrentMacOsBrowserTabRef(
1581
1581
  ? await runtimeHooks.tabInventory(product, timeoutMs)
1582
1582
  : runtimeHooks.runAppleScript
1583
1583
  ? undefined
1584
- : await listMacOsBrowserTabs(product, timeoutMs);
1584
+ // Stable identity needs the complete inventory, not UI-current flags.
1585
+ // Input/activation checks read their exact target's foreground state.
1586
+ : await listMacOsBrowserTabs(product, timeoutMs, { correctFrontmost: false });
1585
1587
  if (!inventory) return ref;
1586
1588
 
1587
1589
  const matches = inventory.tabs.filter((candidate) => candidate.tabId === ref.tabId);
@@ -616,13 +616,14 @@ export function browserActions(): AssistantPluginActionDescriptor[] {
616
616
  {
617
617
  actionId: 'attach_local_file',
618
618
  title: 'Attach local file(s)',
619
- description: 'Set one or more allowed local files on input[type=file] after authorization. file_path remains backward-compatible; file_paths supports multiple selection. Never auto-opens executables.',
619
+ description: 'Attach allowed local files to input[type=file]. Paths are repository-relative by default; optional target_key makes them relative to an existing caller-owned local-system read grant (e.g. Downloads). Does not drive a macOS Open Panel or submit the upload form.',
620
620
  readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
621
621
  scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
622
622
  argumentsSchema: interactSchema({
623
623
  selector: { type: 'string' },
624
624
  file_path: { type: 'string' },
625
625
  file_paths: { type: 'array', minItems: 1, maxItems: 32, items: { type: 'string' } },
626
+ target_key: { type: 'string' },
626
627
  }, ['selector']),
627
628
  },
628
629
  {
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  buildBrowserPluginManifest,
3
+ normalizeLegacyBrowserConfig,
3
4
  executeBrowserPluginAction,
4
5
  resolveBrowserPluginAuthorizationContext,
5
6
  } from './browser-adapter';
@@ -24,6 +25,7 @@ export const browserPluginAdapter: AssistantPluginAdapter = {
24
25
  scope: 'controller_with_repository_overlay',
25
26
  exposure: 'internal',
26
27
  buildManifest: buildBrowserPluginManifest,
28
+ normalizeLegacyConfig: normalizeLegacyBrowserConfig,
27
29
  executeAction: executeBrowserPluginAction,
28
30
  resolveAuthorizationContext: resolveBrowserPluginAuthorizationContext,
29
31
  shouldRefreshManifestAfterAction(actionId) {
@@ -11,7 +11,6 @@ import {
11
11
  import {
12
12
  BrowserProviderRegistry,
13
13
  BrowserProviderSelectionError,
14
- BrowserProviderUnavailableBeforeActionError,
15
14
  type BrowserRuntimeProvider,
16
15
  } from '../../../packages/plugin-runtime/browser/provider-registry';
17
16
 
@@ -197,42 +196,34 @@ export async function executeBrowserRuntimeAction(options: {
197
196
  input: AssistantPluginActionExecutionInput;
198
197
  providers: readonly BrowserRuntimeProvider<AssistantPluginActionExecutionInput, Record<string, unknown>>[];
199
198
  }): Promise<Record<string, unknown>> {
199
+ if (options.providers.length !== 1) {
200
+ throw new AssistantPluginError('PLUGIN_BROWSER_PROVIDER_CARDINALITY_INVALID',
201
+ 'One Browser action must bind exactly one provider before dispatch.', { retryable: false });
202
+ }
200
203
  const registry = registryFor(options.runtimeKey);
201
- for (const provider of options.providers) registry.register(provider);
202
-
204
+ registry.register(options.providers[0]!);
203
205
  const policy = browserRuntimeActionPolicy(options.input.actionId);
204
- const excluded = new Set<string>();
205
- while (true) {
206
- let selection;
207
- try {
208
- selection = await registry.select({
209
- input: options.input,
210
- requiredCapabilities: policy.requiredCapabilities,
211
- foreground: policy.foreground,
212
- excludedProviderIds: excluded,
213
- });
214
- } catch (error) {
215
- if (error instanceof BrowserProviderSelectionError) {
216
- throw new AssistantPluginError(error.code, error.message, { retryable: error.retryable });
217
- }
218
- throw error;
219
- }
220
- const transaction = transactionFor(options.input, selection.provider.providerId, policy);
221
- const validationErrors = validateBrowserTransaction(transaction);
222
- if (validationErrors.length > 0) {
223
- throw new AssistantPluginError('PLUGIN_BROWSER_TRANSACTION_INVALID', validationErrors[0]!, {
224
- retryable: false,
225
- details: { transactionId: transaction.transactionId, errors: validationErrors },
226
- });
227
- }
228
-
229
- try {
230
- return await selection.provider.execute(options.input);
231
- } catch (error) {
232
- if (!(error instanceof BrowserProviderUnavailableBeforeActionError)) throw error;
233
- excluded.add(selection.provider.providerId);
234
- // This is provider reselection, not action replay: the typed error proves the rejected provider
235
- // never attempted the action. Unknown outcomes from non-idempotent actions always propagate.
206
+ let selection;
207
+ try {
208
+ selection = await registry.select({
209
+ input: options.input,
210
+ requiredCapabilities: policy.requiredCapabilities,
211
+ foreground: policy.foreground,
212
+ });
213
+ } catch (error) {
214
+ if (error instanceof BrowserProviderSelectionError) {
215
+ throw new AssistantPluginError(error.code, error.message, { retryable: error.retryable });
236
216
  }
217
+ throw error;
218
+ }
219
+ const transaction = transactionFor(options.input, selection.provider.providerId, policy);
220
+ const validationErrors = validateBrowserTransaction(transaction);
221
+ if (validationErrors.length > 0) {
222
+ throw new AssistantPluginError('PLUGIN_BROWSER_TRANSACTION_INVALID', validationErrors[0]!, {
223
+ retryable: false,
224
+ details: { transactionId: transaction.transactionId, errors: validationErrors },
225
+ });
237
226
  }
227
+ // Provider failures are returned to the caller. Never silently switch target/provider.
228
+ return await selection.provider.execute(options.input);
238
229
  }
@@ -112,8 +112,17 @@ function projectExtension(source: string, destination: string): void {
112
112
  assertRegularFile(sourcePath, 'WORKFLOW_SUPERVISOR_EXTENSION_SOURCE_INVALID');
113
113
  writeFileSync(join(staging, file), readFileSync(sourcePath), { mode: 0o600 });
114
114
  }
115
- rmSync(destination, { recursive: true, force: true });
116
- renameSync(staging, destination);
115
+ // Preserve the previous projection until the staged replacement is ready.
116
+ const backup = destination + '.backup-' + randomUUID().slice(0, 12);
117
+ const hadDestination = existsSync(destination);
118
+ if (hadDestination) renameSync(destination, backup);
119
+ try {
120
+ renameSync(staging, destination);
121
+ } catch (error) {
122
+ if (hadDestination) renameSync(backup, destination);
123
+ throw error;
124
+ }
125
+ if (hadDestination) rmSync(backup, { recursive: true, force: true });
117
126
  } catch (error) {
118
127
  rmSync(staging, { recursive: true, force: true });
119
128
  throw error;
@@ -31,6 +31,8 @@ import {
31
31
  import { desktopOperatorActions } from './desktop-operator-registration';
32
32
  import { getExternalPluginAdapter } from './external-adapter';
33
33
  import { AssistantPluginError } from './errors';
34
+ import { executeIosPhysicalDeviceAction, iosPhysicalDeviceActions, iosPhysicalDeviceCapabilities, iosPhysicalDeviceStatus, isIosPhysicalDeviceAction, resolveIosPhysicalDeviceAuthorizationContext } from './ios-physical-device';
35
+ import { remoteXpcHidCachedStatus } from './ios/remote-xpc-hid';
34
36
  import type {
35
37
  AssistantPluginActionDescriptor,
36
38
  AssistantPluginActionExecutionInput,
@@ -445,14 +447,37 @@ function buildComputerManifest(
445
447
  repoRoot?: string,
446
448
  context?: AssistantPluginBuildContext,
447
449
  ): AssistantPluginManifest {
448
- const browser = buildBrowserPluginManifest(previousRevision, previousUpdatedAt, repoRoot, context);
450
+ const browser = buildBrowserPluginManifest(previousRevision, previousUpdatedAt, repoRoot, context, true);
451
+ const scriptedBrowserActions = new Set([
452
+ 'configure', 'install_unpacked_extension', 'list_unpacked_extensions', 'clear_session', 'reconcile_sessions',
453
+ ]);
454
+ const browserActions = browser.actions.filter((action) => !scriptedBrowserActions.has(action.actionId));
455
+ const browserCapabilities = browser.capabilities.map((capability) => ({
456
+ ...capability,
457
+ actions: capability.actions.filter((action) => !scriptedBrowserActions.has(action)),
458
+ ...(capability.capabilityId === 'browser-interaction'
459
+ ? { description: 'Interact with existing Chrome or Vivaldi through native Computer; scripted Playwright is a separate explicit capability.' }
460
+ : {}),
461
+ ...(capability.capabilityId === 'browser-session'
462
+ ? { description: 'Discover, bind and reuse tabs of the user’s existing browser.' }
463
+ : {}),
464
+ ...(capability.capabilityId === 'browser-human-handoff'
465
+ ? { description: 'Present the exact existing native browser tab for manual interaction.' }
466
+ : {}),
467
+ })).filter((capability) => capability.actions.length > 0);
449
468
  const desktop = desktopProviderManifest(context);
450
469
  const platform = currentComputerPlatform();
451
470
  const desktopSupported = platform === 'darwin';
452
471
  const browserReady = browser.enabled && browser.health.ready;
453
472
  const desktopReady = desktopSupported && desktop?.enabled === true && desktop.health.ready;
473
+ const physicalRepository = desktopSupported && context?.controllerScoped === false;
474
+ const physicalReady = physicalRepository && iosPhysicalDeviceStatus().coreDeviceReady;
475
+ // Passive manifest reads must never launch Python: input readiness is
476
+ // published only after an explicit HID action verified the exact toolchain.
477
+ const hidStatus = physicalReady && context ? remoteXpcHidCachedStatus(context.controllerHome) : undefined;
478
+ const physicalInputReady = physicalReady && hidStatus?.available === true;
454
479
  const ready = browserReady && desktopReady;
455
- const partial = browserReady || desktopReady;
480
+ const partial = browserReady || desktopReady || physicalReady;
456
481
  const checkedAt = new Date().toISOString();
457
482
  return {
458
483
  ...browser,
@@ -464,11 +489,14 @@ function buildComputerManifest(
464
489
  strategy: 'derived',
465
490
  duplicateStateAllowed: false,
466
491
  sourceOfTruth: [
467
- ...browser.authority.sourceOfTruth,
492
+ // Computer browser is built in and never reads a project's independent
493
+ // Browser-script configuration to decide activation or routing.
468
494
  'controllerHome:sqlite/computer_interaction_target',
495
+ 'controller-home:repository browser artifacts',
496
+ 'controller-home:repository ios-device interactions and Controller-global UDID lock',
469
497
  ],
470
498
  },
471
- enabled: browser.enabled || desktop?.enabled === true,
499
+ enabled: browser.enabled || desktop?.enabled === true || physicalReady,
472
500
  lifecycle: {
473
501
  state: ready ? 'enabled' : partial ? 'degraded' : 'error',
474
502
  reason: ready
@@ -488,6 +516,7 @@ function buildComputerManifest(
488
516
  ],
489
517
  warnings: [
490
518
  ...browser.health.warnings,
519
+ ...(physicalReady && !physicalInputReady ? [`Physical iPhone observation is ready, but RemoteXPC HID input requires Controller-owned pymobiledevice3 ${String(hidStatus?.toolchainVersion ?? 'toolchain')}.`] : []),
491
520
  ...(desktopSupported
492
521
  ? (desktopReady ? [] : ['Native Computer capability diagnostics are available through the internal platform provider.'])
493
522
  : [`Native Desktop Computer capabilities are unsupported on ${platform}.`]),
@@ -496,11 +525,24 @@ function buildComputerManifest(
496
525
  partial: !ready && partial,
497
526
  browser: { ready: browserReady, state: browser.health.state },
498
527
  desktop: { supported: desktopSupported, ready: desktopReady, state: desktop?.health.state ?? (desktopSupported ? 'not_installed' : 'unsupported') },
528
+ iphone: {
529
+ supported: desktopSupported, ready: physicalInputReady,
530
+ observationReady: physicalReady, inputReady: physicalInputReady,
531
+ toolchainVersion: hidStatus?.toolchainVersion,
532
+ provider: 'coredevice',
533
+ },
499
534
  },
500
535
  },
501
- permissions: [...browser.permissions, ...desktopProductPermissions(desktopReady)],
502
- capabilities: [...browser.capabilities, ...desktopProductCapabilities()],
503
- actions: [...browser.actions, ...desktopProductActions()],
536
+ permissions: [...browser.permissions.filter((permission) => permission.scope !== 'browser.extensions').map((permission) =>
537
+ permission.scope === 'browser.profile'
538
+ ? { ...permission, description: 'Bind and persist exact existing native browser tab identities.' }
539
+ : permission), ...desktopProductPermissions(desktopReady),
540
+ ...(physicalRepository ? [
541
+ { scope: 'ios.discover' as const, mode: 'read' as const, description: 'Discover paired physical iPhones through CoreDevice.', granted: physicalReady, required: false },
542
+ { scope: 'ios.device' as const, mode: 'write' as const, description: 'Use one exact iPhone through the existing CoreDevice/RemoteXPC owner.', granted: physicalReady, required: false },
543
+ ] : [])],
544
+ capabilities: [...browserCapabilities, ...desktopProductCapabilities(), ...(physicalRepository ? iosPhysicalDeviceCapabilities() : [])],
545
+ actions: [...browserActions, ...desktopProductActions(), ...(physicalRepository ? iosPhysicalDeviceActions() : [])],
504
546
  updatedAt: previousUpdatedAt ?? checkedAt,
505
547
  };
506
548
  }
@@ -1003,19 +1045,44 @@ async function resolveDesktopAuthorizationContext(input: AssistantPluginActionEx
1003
1045
  return provider.resolveAuthorizationContext?.(providerInput(input, input.actionId, input.args));
1004
1046
  }
1005
1047
 
1048
+ // Computer's browser subdomain always targets the user's real browser.
1049
+ // The independent Browser plugin retains explicit Playwright scripting modes,
1050
+ // but Computer must neither select nor silently fall back to those providers.
1051
+ function computerNativeBrowserInput(input: AssistantPluginActionExecutionInput): AssistantPluginActionExecutionInput {
1052
+ if (['configure', 'install_unpacked_extension', 'list_unpacked_extensions', 'clear_session', 'reconcile_sessions'].includes(input.actionId)) {
1053
+ throw new AssistantPluginError('PLUGIN_COMPUTER_BROWSER_SCRIPT_ACTION_SEPARATE',
1054
+ 'Browser profile configuration and script-managed extensions belong to the independent Browser plugin.', { retryable: false });
1055
+ }
1056
+ if ((input.args.browser_mode !== undefined && input.args.browser_mode !== 'attach_preferred')
1057
+ || (input.args.cdp_attach_fallback !== undefined && input.args.cdp_attach_fallback !== 'fail_closed')
1058
+ || (input.args.native_attach_mode !== undefined && input.args.native_attach_mode !== 'auto')) {
1059
+ throw new AssistantPluginError('PLUGIN_COMPUTER_BROWSER_SCRIPT_MODE_SEPARATE',
1060
+ 'Computer only operates the native browser; use the independent Browser plugin for scripted or isolated profiles.',
1061
+ { retryable: false });
1062
+ }
1063
+ return { ...input, args: { ...input.args,
1064
+ browser_mode: 'attach_preferred',
1065
+ cdp_attach_fallback: 'fail_closed',
1066
+ native_attach_mode: 'auto',
1067
+ __forge_computer_native_only: true,
1068
+ } };
1069
+ }
1070
+
1006
1071
  export const computerPluginAdapter: AssistantPluginAdapter = {
1007
1072
  pluginId: 'computer',
1008
1073
  scope: 'controller_with_repository_overlay',
1009
1074
  buildManifest: buildComputerManifest,
1010
1075
  async resolveAuthorizationContext(input) {
1076
+ if (isIosPhysicalDeviceAction(input.actionId)) return resolveIosPhysicalDeviceAuthorizationContext(input);
1011
1077
  if (!isDesktopProductAction(input.actionId)) {
1012
- return resolveBrowserPluginAuthorizationContext({ ...input, pluginId: 'browser' });
1078
+ return resolveBrowserPluginAuthorizationContext(computerNativeBrowserInput(input));
1013
1079
  }
1014
1080
  return resolveDesktopAuthorizationContext(input);
1015
1081
  },
1016
1082
  async executeAction(input) {
1083
+ if (isIosPhysicalDeviceAction(input.actionId)) return executeIosPhysicalDeviceAction(input);
1017
1084
  if (!isDesktopProductAction(input.actionId)) {
1018
- return executeBrowserPluginAction({ ...input, pluginId: 'browser' });
1085
+ return executeBrowserPluginAction(computerNativeBrowserInput(input));
1019
1086
  }
1020
1087
  if (input.actionId === COMPUTER_CONSOLE_UNLOCK_PREPARE_ACTION) {
1021
1088
  return executeProtectedConsoleUnlockPreparation({ confirmAuthorization: input.confirmAuthorization === true, timeoutMs: input.timeoutMs }, input.controllerHome);