@moretea-labs/forge 1.7.2 → 1.8.1

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 (900) hide show
  1. package/AGENTS.md +121 -62
  2. package/CHANGELOG.md +40 -0
  3. package/CLAUDE.md +57 -62
  4. package/CONTRIBUTING.md +3 -2
  5. package/README.md +2 -2
  6. package/SKILL.md +0 -1
  7. package/THIRD_PARTY_NOTICES.md +5 -2
  8. package/adapters/browser/index.ts +2 -0
  9. package/adapters/chatgpt/browser-delivery-host.ts +77 -0
  10. package/adapters/chatgpt/browser-delivery-runtime.ts +1256 -0
  11. package/adapters/chatgpt/controller-binding-store.ts +134 -0
  12. package/adapters/chatgpt/controller-round-host.ts +68 -0
  13. package/adapters/chatgpt/controller-round-settlement-store.ts +99 -0
  14. package/adapters/chatgpt/provider-delivery.ts +340 -0
  15. package/adapters/chatgpt/work-conversation-binding-store.ts +218 -0
  16. package/adapters/chatgpt/wsl-bridge-delivery-host.ts +63 -0
  17. package/adapters/computer/chatgpt-extension-target.ts +463 -0
  18. package/adapters/computer/chatgpt-target-router.ts +103 -0
  19. package/adapters/computer/chrome-extension/background.js +385 -0
  20. package/adapters/computer/chrome-extension/content.js +378 -0
  21. package/adapters/computer/chrome-extension/core.js +24 -0
  22. package/adapters/computer/chrome-extension/manifest.json +30 -0
  23. package/adapters/computer/desktop-operator-contract.ts +7 -0
  24. package/adapters/computer/desktop-operator-discovery.ts +181 -0
  25. package/adapters/computer/desktop-operator-negotiation.ts +174 -0
  26. package/adapters/computer/desktop-operator-provider.ts +312 -0
  27. package/adapters/computer/index.ts +1 -0
  28. package/adapters/computer/interaction-target-authority.ts +1068 -0
  29. package/adapters/controller-process/binding-store.ts +34 -0
  30. package/adapters/controller-process/controller-host.ts +26 -0
  31. package/adapters/mcp/access-mode.ts +132 -0
  32. package/adapters/mcp/audit.ts +32 -0
  33. package/adapters/mcp/auth.ts +406 -0
  34. package/adapters/mcp/instructions.ts +37 -0
  35. package/adapters/mcp/legacy-context.ts +18 -0
  36. package/adapters/mcp/multi-repository.ts +421 -0
  37. package/adapters/mcp/oauth.ts +464 -0
  38. package/adapters/mcp/paths.ts +143 -0
  39. package/adapters/mcp/policy.ts +232 -0
  40. package/adapters/mcp/proxy-env.ts +62 -0
  41. package/adapters/mcp/readiness-projection.ts +24 -0
  42. package/adapters/mcp/redaction.ts +21 -0
  43. package/adapters/mcp/repo.ts +24 -0
  44. package/adapters/mcp/runtime-gateway/artifact-adapter.ts +52 -0
  45. package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +87 -0
  46. package/adapters/mcp/runtime-gateway/bounded-result-runtime.ts +62 -0
  47. package/adapters/mcp/runtime-gateway/cognition-settlement.ts +201 -0
  48. package/adapters/mcp/runtime-gateway/context-adapter.ts +1167 -0
  49. package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +91 -0
  50. package/adapters/mcp/runtime-gateway/controller-authority-adapter.ts +206 -0
  51. package/adapters/mcp/runtime-gateway/core-capability-adapter.ts +258 -0
  52. package/adapters/mcp/runtime-gateway/engineering-preconditions.ts +130 -0
  53. package/adapters/mcp/runtime-gateway/engineering-tool-contract.ts +15 -0
  54. package/adapters/mcp/runtime-gateway/execution-control-adapter.ts +28 -0
  55. package/adapters/mcp/runtime-gateway/execution-tools.ts +173 -0
  56. package/adapters/mcp/runtime-gateway/filesystem-adapter.ts +38 -0
  57. package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +412 -0
  58. package/adapters/mcp/runtime-gateway/legacy-ios-tool-adapter.ts +105 -0
  59. package/adapters/mcp/runtime-gateway/maintenance-adapter.ts +44 -0
  60. package/adapters/mcp/runtime-gateway/model-adapter.ts +63 -0
  61. package/adapters/mcp/runtime-gateway/persisted-check-process.ts +3 -0
  62. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +380 -0
  63. package/adapters/mcp/runtime-gateway/process-tools.ts +457 -0
  64. package/adapters/mcp/runtime-gateway/protected-computer-adapter.ts +88 -0
  65. package/adapters/mcp/runtime-gateway/recovery-adapter.ts +285 -0
  66. package/adapters/mcp/runtime-gateway/recovery-client-adapter.ts +70 -0
  67. package/adapters/mcp/runtime-gateway/recovery-tool-contract.ts +99 -0
  68. package/adapters/mcp/runtime-gateway/repository-compat-adapter.ts +79 -0
  69. package/adapters/mcp/runtime-gateway/result-adapter.ts +114 -0
  70. package/adapters/mcp/runtime-gateway/router.ts +242 -0
  71. package/adapters/mcp/runtime-gateway/routing-policy.ts +499 -0
  72. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1200 -0
  73. package/adapters/mcp/runtime-gateway/runtime-readiness-observation.ts +218 -0
  74. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +574 -0
  75. package/adapters/mcp/runtime-gateway/runtime-tool-shared.ts +27 -0
  76. package/adapters/mcp/runtime-gateway/runtime-tools.ts +78 -0
  77. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +187 -0
  78. package/adapters/mcp/runtime-gateway/shared-adapter.ts +45 -0
  79. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +962 -0
  80. package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +136 -0
  81. package/adapters/mcp/runtime-gateway/work-adapter.ts +325 -0
  82. package/adapters/mcp/runtime-gateway/work-controller-operations.ts +257 -0
  83. package/adapters/mcp/runtime-gateway/work-detail-projection.ts +60 -0
  84. package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +246 -0
  85. package/adapters/mcp/runtime-gateway/work-learning-operations.ts +145 -0
  86. package/adapters/mcp/runtime-gateway/work-list-projection.ts +38 -0
  87. package/adapters/mcp/runtime-gateway/work-plan-operations.ts +382 -0
  88. package/adapters/mcp/runtime-gateway/work-repair-adapter.ts +308 -0
  89. package/adapters/mcp/runtime-gateway/work-requirement-operations.ts +148 -0
  90. package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +263 -0
  91. package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +1 -0
  92. package/adapters/mcp/runtime-gateway/work-workflow-operations.ts +113 -0
  93. package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +156 -0
  94. package/adapters/mcp/server.ts +1092 -0
  95. package/adapters/mcp/setup.ts +789 -0
  96. package/adapters/mcp/tool-capability-cache.ts +87 -0
  97. package/adapters/mcp/tool-mapping/access-tools.ts +387 -0
  98. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +5139 -0
  99. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +145 -0
  100. package/adapters/mcp/tool-mapping/repository-tools.ts +1682 -0
  101. package/adapters/mcp/tool-mapping/tools.ts +9 -0
  102. package/adapters/mcp/tool-mapping/umbrella-tools.ts +117 -0
  103. package/adapters/mcp/toolset-names.ts +79 -0
  104. package/adapters/mcp/toolset-selection.ts +41 -0
  105. package/adapters/mcp/toolset.ts +337 -0
  106. package/adapters/mcp/transports/http-observation.ts +365 -0
  107. package/adapters/mcp/transports/http.ts +933 -0
  108. package/adapters/mcp/transports/oauth-http.ts +322 -0
  109. package/adapters/mcp/transports/session-registry.ts +519 -0
  110. package/adapters/mcp/transports/stdio.ts +8 -0
  111. package/adapters/mcp/tunnels/openai-secure-tunnel.ts +223 -0
  112. package/adapters/mcp/types.ts +41 -0
  113. package/assets/AGENTS.md +2 -16
  114. package/assets/CLAUDE.md +2 -16
  115. package/assets/hooks/AGENTS.md +3 -14
  116. package/assets/hooks/CLAUDE.md +3 -14
  117. package/assets/hooks/hook-input.sh +9 -2
  118. package/assets/hooks/lib/workflow-state.sh +28 -10
  119. package/assets/hooks/prompt-guard.sh +63 -37
  120. package/assets/hooks/security-sentinel.sh +2 -1
  121. package/assets/hooks/session-start-context.sh +7 -5
  122. package/assets/hooks/stop-orchestrator.sh +13 -3
  123. package/assets/initializer-question-pack.v4.json +11 -7
  124. package/assets/partials/05-workflow.partial.md +18 -73
  125. package/assets/partials/07-footer.partial.md +1 -1
  126. package/assets/partials-agents/02-operating-mode.partial.md +1 -2
  127. package/assets/partials-agents/04-task-protocol.partial.md +14 -59
  128. package/assets/partials-agents/06-quality-safety.partial.md +11 -16
  129. package/assets/partials-agents/08-deep-docs.partial.md +1 -2
  130. package/assets/plan-map.json +11 -11
  131. package/assets/plugin-registry.v1.json +11 -3
  132. package/assets/plugin-trust-policy.v1.json +6 -0
  133. package/assets/recovery/ForgeRecovery.ps1 +100 -0
  134. package/assets/recovery/forge-wsl-rescue +271 -0
  135. package/assets/reference-configs/agentic-development-flow.md +25 -0
  136. package/assets/reference-configs/development-protocol.md +11 -15
  137. package/assets/reference-configs/document-generation.md +1 -1
  138. package/assets/reference-configs/global-working-rules.md +28 -1
  139. package/assets/reference-configs/harness-overview.md +22 -112
  140. package/assets/reference-configs/hook-operations.md +1 -1
  141. package/assets/reference-configs/sprint-contracts.md +2 -2
  142. package/assets/skill-version.json +2 -2
  143. package/assets/templates/helpers/architecture-event.ts +3 -34
  144. package/assets/templates/helpers/architecture-queue.sh +61 -10
  145. package/assets/templates/helpers/archive-workflow.sh +90 -180
  146. package/assets/templates/helpers/capability-config.ts +8 -11
  147. package/assets/templates/helpers/capability-resolver.ts +37 -19
  148. package/assets/templates/helpers/capture-plan.sh +2 -2
  149. package/assets/templates/helpers/check-architecture-sync.sh +1 -9
  150. package/assets/templates/helpers/check-task-sync.sh +82 -11
  151. package/assets/templates/helpers/check-task-workflow.sh +1 -3
  152. package/assets/templates/helpers/context-contract-sync.sh +1 -12
  153. package/assets/templates/helpers/contract-worktree.sh +26 -24
  154. package/assets/templates/helpers/ensure-task-workflow.sh +6 -8
  155. package/assets/templates/helpers/inspect-project-state.ts +1 -1
  156. package/assets/templates/helpers/migrate-project-template.sh +1 -1
  157. package/assets/templates/helpers/migrate-workflow-docs.ts +53 -239
  158. package/assets/templates/helpers/new-plan.sh +2 -2
  159. package/assets/templates/helpers/plan-to-todo.sh +3 -31
  160. package/assets/templates/helpers/sprint-backlog.sh +1 -1
  161. package/assets/templates/helpers/switch-plan.sh +1 -1
  162. package/assets/templates/helpers/workflow-contract.ts +1 -0
  163. package/assets/workflow-contract.v1.json +63 -171
  164. package/assets/workflows/xiaohongshu/xiaohongshu.collect-metrics.v1.draft.json +118 -0
  165. package/assets/workflows/xiaohongshu/xiaohongshu.publish-image-note.v1.draft.json +316 -0
  166. package/assets/workflows/xiaohongshu/xiaohongshu.publish-long-text.v1.draft.json +324 -0
  167. package/docs/architecture/CURRENT.md +233 -28
  168. package/docs/architecture/EVOLUTION.md +13 -0
  169. package/docs/architecture/decisions/20260801-controller-home-sqlite-state.md +2 -1
  170. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +14 -5
  171. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +6 -3
  172. package/docs/architecture/decisions/20260831-independent-windows-wsl-host-rescue.md +59 -0
  173. package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +120 -0
  174. package/docs/architecture/decisions/20260904-repository-runtime-data-boundary.md +42 -0
  175. package/docs/architecture/decisions/20260907-assistant-learning-loop.md +37 -0
  176. package/docs/architecture/decisions/20260907-controller-round-transition-policy.md +311 -0
  177. package/docs/architecture/decisions/20260907-maintenance-diagnosis-bounded-read.md +39 -0
  178. package/docs/architecture/decisions/20260916-workflow-supervisor-authority.md +249 -0
  179. package/docs/architecture/decisions/20260917-cognitive-memory-foundation.md +82 -0
  180. package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +221 -0
  181. package/docs/architecture/decisions/20260919-autonomous-work-liveness.md +62 -0
  182. package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +72 -0
  183. package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +237 -0
  184. package/docs/architecture/decisions/20261005-forge-v3-desktop-client-boundary.md +77 -0
  185. package/docs/architecture/domains/verification.md +8 -0
  186. package/docs/architecture/domains/workflow-engine.md +7 -1
  187. package/docs/architecture/index.md +3 -3
  188. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +19 -8
  189. package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +124 -0
  190. package/docs/forge-chatgpt-mcp-setup.md +5 -3
  191. package/docs/forge-plugin-management.md +20 -4
  192. package/docs/forge-runtime-self-healing-loop.md +2 -0
  193. package/docs/operations/benchmark-cross-version-freeze.md +38 -0
  194. package/docs/operations/controller-browser-plugin.md +3 -1
  195. package/docs/operations/controller-performance-and-502.md +17 -0
  196. package/docs/operations/controller-reliability-runbook.md +2 -2
  197. package/docs/operations/features.md +1 -1
  198. package/docs/operations/features.zh-CN.md +1 -1
  199. package/docs/operations/homebrew.md +2 -2
  200. package/docs/operations/mcp-tool-exposure.md +2 -0
  201. package/docs/operations/platform-support.md +16 -8
  202. package/docs/operations/platform-support.zh-CN.md +2 -2
  203. package/docs/operations/releasing.md +2 -0
  204. package/docs/operations/standalone-disaster-recovery.md +38 -5
  205. package/docs/operations/uu-remote-rescue.md +41 -0
  206. package/docs/operations/windows-wsl-host-rescue.md +133 -0
  207. package/docs/public-usage-guide.md +1 -1
  208. package/docs/public-usage-guide.zh-CN.md +1 -1
  209. package/docs/tutorials/01-install-and-start.md +1 -1
  210. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  211. package/docs/tutorials/02-connect-chatgpt.md +4 -4
  212. package/docs/tutorials/02-connect-chatgpt.zh-CN.md +4 -4
  213. package/docs/wiki/Implementation.md +1 -1
  214. package/package.json +46 -26
  215. package/packages/kernel/cognition/api/index.ts +6 -0
  216. package/packages/kernel/cognition/application/activation-service.ts +451 -0
  217. package/packages/kernel/cognition/application/consolidation-service.ts +185 -0
  218. package/packages/kernel/cognition/application/legacy-experience-adapter.ts +38 -0
  219. package/packages/kernel/cognition/application/memory-service.ts +64 -0
  220. package/packages/kernel/cognition/domain/learning.ts +106 -0
  221. package/packages/kernel/cognition/domain/memory.ts +254 -0
  222. package/packages/kernel/controller/api/index.ts +3 -0
  223. package/packages/kernel/controller/application/continuation-service.ts +281 -0
  224. package/packages/kernel/controller/application/controller-round-service.ts +41 -0
  225. package/packages/kernel/controller/application/controller-service.ts +30 -0
  226. package/packages/kernel/controller/domain/controller-round-transition-policy.ts +529 -0
  227. package/packages/kernel/controller/domain/controller-round.ts +110 -0
  228. package/packages/kernel/controller/domain/execution-quality.ts +135 -0
  229. package/packages/kernel/controller/domain/types.ts +56 -0
  230. package/packages/kernel/controller/index.ts +1 -0
  231. package/packages/kernel/controller/infrastructure/controller-binding-store.ts +84 -0
  232. package/packages/kernel/controller/infrastructure/controller-round-store.ts +1970 -0
  233. package/packages/kernel/controller/infrastructure/controller-session-store.ts +804 -0
  234. package/packages/kernel/controller/ports/controller-host.ts +18 -0
  235. package/packages/kernel/goal/api/index.ts +1 -0
  236. package/packages/kernel/goal/domain/types.ts +40 -0
  237. package/packages/kernel/goal/index.ts +1 -0
  238. package/packages/kernel/identity/api/index.ts +15 -0
  239. package/packages/kernel/identity/application/identity-service.ts +67 -0
  240. package/packages/kernel/identity/domain/activity-projection.ts +43 -0
  241. package/packages/kernel/identity/domain/capability-broker.ts +60 -0
  242. package/packages/kernel/identity/domain/grant.ts +72 -0
  243. package/packages/kernel/identity/domain/owned-resource.ts +116 -0
  244. package/packages/kernel/identity/domain/scope.ts +63 -0
  245. package/packages/kernel/identity/domain/semantic-sync.ts +81 -0
  246. package/packages/kernel/identity/domain/types.ts +44 -0
  247. package/packages/kernel/identity/domain/user-request.ts +93 -0
  248. package/packages/kernel/identity/domain/workspace.ts +56 -0
  249. package/packages/kernel/identity/index.ts +1 -0
  250. package/packages/kernel/identity/infrastructure/activity-projection-store.ts +77 -0
  251. package/packages/kernel/identity/infrastructure/capability-broker.ts +81 -0
  252. package/packages/kernel/identity/infrastructure/grant-store.ts +284 -0
  253. package/packages/kernel/identity/infrastructure/identity-store.ts +75 -0
  254. package/packages/kernel/identity/infrastructure/owned-resource-store.ts +259 -0
  255. package/packages/kernel/identity/infrastructure/user-request-store.ts +182 -0
  256. package/packages/kernel/memory/api/index.ts +5 -0
  257. package/packages/kernel/memory/application/experience-service.ts +118 -0
  258. package/packages/kernel/memory/application/outcome-service.ts +77 -0
  259. package/packages/kernel/memory/domain/experience.ts +103 -0
  260. package/packages/kernel/memory/domain/operational-prior.ts +92 -0
  261. package/packages/kernel/memory/index.ts +1 -0
  262. package/packages/kernel/scheduler/api/index.ts +4 -0
  263. package/packages/kernel/scheduler/application/eligibility.ts +252 -0
  264. package/packages/kernel/scheduler/application/schedule-service.ts +27 -0
  265. package/packages/kernel/scheduler/application/settlement.ts +182 -0
  266. package/packages/kernel/scheduler/domain/schedule.ts +236 -0
  267. package/packages/kernel/scheduler/index.ts +1 -0
  268. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +805 -0
  269. package/packages/kernel/work/api/index.ts +16 -0
  270. package/packages/kernel/work/application/work-service.ts +123 -0
  271. package/packages/kernel/work/domain/check-receipt.ts +43 -0
  272. package/packages/kernel/work/domain/context-closure-receipt.ts +32 -0
  273. package/packages/kernel/work/domain/current-task-lineage.ts +104 -0
  274. package/packages/kernel/work/domain/engineering-contracts.ts +157 -0
  275. package/packages/kernel/work/domain/engineering-design.ts +286 -0
  276. package/packages/kernel/work/domain/engineering-profile.ts +238 -0
  277. package/packages/kernel/work/domain/evaluation-promotion-receipt.ts +186 -0
  278. package/packages/kernel/work/domain/execution-concurrency.ts +247 -0
  279. package/packages/kernel/work/domain/execution-snapshot.ts +2 -0
  280. package/packages/kernel/work/domain/implementation-review.ts +193 -0
  281. package/packages/kernel/work/domain/objective-graph.ts +119 -0
  282. package/packages/kernel/work/domain/project-engineering-contract.ts +224 -0
  283. package/packages/kernel/work/domain/repository-completion-receipt.ts +73 -0
  284. package/{src/runtime/control-plane/facade/work-state-machine.ts → packages/kernel/work/domain/state-machine.ts} +37 -66
  285. package/packages/kernel/work/domain/types.ts +534 -0
  286. package/packages/kernel/work/index.ts +1 -0
  287. package/packages/kernel/work/infrastructure/work-contract-store.ts +1967 -0
  288. package/packages/kernel/work/ports/work-contract-store.ts +21 -0
  289. package/packages/plugin-runtime/browser/index.ts +3 -0
  290. package/packages/plugin-runtime/browser/provider-registry.ts +110 -0
  291. package/packages/plugin-runtime/browser/runtime-contract.ts +150 -0
  292. package/packages/plugin-runtime/browser/session-authority.ts +62 -0
  293. package/packages/plugin-runtime/computer/chatgpt-identity.ts +24 -0
  294. package/packages/plugin-runtime/computer/conversation-target.ts +143 -0
  295. package/packages/plugin-runtime/computer/index.ts +8 -0
  296. package/packages/plugin-runtime/computer/provider-error.ts +26 -0
  297. package/packages/plugin-runtime/computer/provider-registration.ts +52 -0
  298. package/packages/plugin-runtime/computer/provider-registry.ts +63 -0
  299. package/packages/plugin-runtime/computer/provider.ts +19 -0
  300. package/packages/plugin-runtime/computer/target-authority.ts +215 -0
  301. package/packages/plugin-runtime/computer/target-persistence.ts +54 -0
  302. package/packages/plugin-runtime/external/index.ts +1 -0
  303. package/packages/plugin-runtime/external/unix-jsonl-transport.ts +581 -0
  304. package/packages/plugin-runtime/index.ts +3 -0
  305. package/packages/protocols/browser/index.ts +1 -0
  306. package/packages/protocols/browser/session.ts +98 -0
  307. package/packages/protocols/computer/contract.ts +225 -0
  308. package/packages/protocols/computer/index.ts +1 -0
  309. package/packages/protocols/failure/contract.ts +158 -0
  310. package/packages/protocols/failure/index.ts +1 -0
  311. package/packages/protocols/handoff/index.ts +1 -0
  312. package/packages/protocols/handoff/status.ts +22 -0
  313. package/packages/protocols/mcp/execution-context.ts +11 -0
  314. package/packages/protocols/mcp/index.ts +2 -0
  315. package/packages/protocols/mcp/tool-contract.ts +42 -0
  316. package/packages/workflow-runtime/api/index.ts +2 -0
  317. package/packages/workflow-runtime/application/interpreter.ts +342 -0
  318. package/packages/workflow-runtime/domain/workflow-asset.ts +223 -0
  319. package/packages/workflow-runtime/index.ts +1 -0
  320. package/scripts/activate-source-baseline.ts +7 -9
  321. package/scripts/assemble-template.ts +1 -1
  322. package/scripts/bootstrap-runtime-maintenance-recovery.sh +4 -1
  323. package/scripts/canary-chatgpt-provider-delivery.ts +97 -0
  324. package/scripts/canary-controller-semantic-wait.ts +183 -0
  325. package/scripts/canary-global-hook.sh +2 -2
  326. package/scripts/check-mcp-compatibility.ts +85 -7
  327. package/scripts/check-platform-support.mjs +28 -2
  328. package/scripts/check-public-docs.sh +1 -1
  329. package/scripts/check-repository-hygiene.mjs +92 -0
  330. package/scripts/check-runtime-architecture.mjs +1643 -127
  331. package/scripts/check-source-duplication.mjs +9 -1
  332. package/scripts/controller-ngrok-rotation.sh +5 -4
  333. package/scripts/create-project-dirs.sh +10 -74
  334. package/scripts/forge-local-recovery-helper.mjs +282 -0
  335. package/scripts/forge-uu-remote-rescue-helper.d.mts +31 -0
  336. package/scripts/forge-uu-remote-rescue-helper.mjs +409 -0
  337. package/scripts/forge-windows-host-recovery-helper.mjs +136 -0
  338. package/scripts/hook-shim.sh +2 -2
  339. package/scripts/init-project.sh +12 -70
  340. package/scripts/install-desktop-operator-registration.ts +4 -2
  341. package/scripts/install-independent-windows-wsl-recovery.ts +125 -0
  342. package/scripts/install-standalone-recovery.ts +122 -10
  343. package/scripts/install-uu-remote-rescue-registration.ts +74 -0
  344. package/scripts/install-windows-host-recovery-registration.ts +22 -0
  345. package/scripts/lib/controller-home-env.sh +6 -3
  346. package/scripts/lib/project-init-lib.sh +42 -582
  347. package/scripts/migrate-chatgpt-automation-schedules.ts +2 -2
  348. package/scripts/migrate-windows-wsl-controller-home.ts +306 -0
  349. package/scripts/open-source-audit-allowlist.txt +11 -1
  350. package/scripts/portability-bindings.mjs +70 -0
  351. package/scripts/run-bounded-command.mjs +60 -0
  352. package/scripts/run-bun-test-file.ts +22 -6
  353. package/scripts/run-governed-gate.ts +4 -2
  354. package/scripts/run-mobile-monitor-bridge.ts +0 -1
  355. package/scripts/smoke-control-plane-failure-domain.ts +94 -0
  356. package/scripts/smoke-mcp-http-runtime.ts +6 -1
  357. package/scripts/smoke-runtime-recovery.ts +13 -11
  358. package/scripts/smoke-schedule-engine.ts +24 -16
  359. package/scripts/smoke-unattended-transport-recovery.ts +223 -0
  360. package/scripts/smoke-user-request-continuation-restart.ts +232 -0
  361. package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +92 -0
  362. package/scripts/smoke-workflow-supervisor-process-restart.ts +276 -0
  363. package/scripts/smoke-workflow-supervisor-restart-recovery.ts +150 -0
  364. package/scripts/smoke-workflow-supervisor.ts +103 -0
  365. package/scripts/test-governance.ts +51 -2
  366. package/scripts/verify-forge-runtime.sh +2 -1
  367. package/src/cli/agent-jobs/executor-health.ts +11 -34
  368. package/src/cli/agent-jobs/integration.ts +18 -49
  369. package/src/cli/agent-jobs/job-manager.ts +17 -4
  370. package/src/cli/chatgpt-browser/bind-server.ts +2 -1
  371. package/src/cli/chatgpt-browser/binding.ts +20 -10
  372. package/src/cli/chatgpt-browser/bridge-extension.ts +75 -0
  373. package/src/cli/chatgpt-browser/bridge-provider.ts +376 -11
  374. package/src/cli/chatgpt-browser/engine.ts +10 -11
  375. package/src/cli/chatgpt-browser/native-provider.ts +24 -83
  376. package/src/cli/chatgpt-browser/oracle-provider.ts +5 -3
  377. package/src/cli/chatgpt-browser/types.ts +1 -0
  378. package/src/cli/commands/bootstrap-control.ts +334 -0
  379. package/src/cli/commands/brain-assistant.ts +52 -0
  380. package/src/cli/commands/brain.ts +191 -40
  381. package/src/cli/commands/chatgpt.ts +32 -67
  382. package/src/cli/commands/computer.ts +437 -0
  383. package/src/cli/commands/doctor.ts +18 -1
  384. package/src/cli/commands/init-hook.ts +71 -61
  385. package/src/cli/commands/init.ts +3 -1
  386. package/src/cli/commands/mcp.ts +16 -10
  387. package/src/cli/commands/plugin.ts +38 -12
  388. package/src/cli/commands/recovery.ts +507 -80
  389. package/src/cli/commands/runtime.ts +12 -7
  390. package/src/cli/commands/setup-profile.ts +97 -39
  391. package/src/cli/commands/status.ts +2 -1
  392. package/src/cli/controller/check-runner.ts +500 -100
  393. package/src/cli/controller/completion-backlog.ts +3 -3
  394. package/src/cli/controller/completion-orchestrator.ts +4 -43
  395. package/src/cli/controller/completion-target.ts +3 -72
  396. package/src/cli/controller/context/source-materializer.ts +81 -100
  397. package/src/cli/controller/context/types.ts +7 -1
  398. package/src/cli/controller/context-pack.ts +28 -6
  399. package/src/cli/controller/execution-completion.ts +4 -7
  400. package/src/cli/controller/execution-policy.ts +36 -153
  401. package/src/cli/controller/governance.ts +2 -3
  402. package/src/cli/controller/issue-store.ts +4 -99
  403. package/src/cli/controller/launch-agents.ts +59 -18
  404. package/src/cli/controller/operational-plan.ts +24 -51
  405. package/src/cli/controller/runtime-config.ts +5 -1
  406. package/src/cli/controller/stuck-state-migration.ts +2 -3
  407. package/src/cli/controller/systemd-user.ts +148 -0
  408. package/src/cli/controller/types.ts +10 -70
  409. package/src/cli/controller/worklog.ts +67 -13
  410. package/src/cli/editing/edit-operation-contract.ts +92 -0
  411. package/src/cli/editing/edit-session.ts +290 -20
  412. package/src/cli/github/plugin.ts +17 -12
  413. package/src/cli/hook/route-registry.ts +3 -5
  414. package/src/cli/hook/runtime.ts +49 -3
  415. package/src/cli/index.ts +3 -0
  416. package/src/cli/local-bridge/connector-freshness.ts +2 -2
  417. package/src/cli/local-bridge/console-automations.ts +20 -15
  418. package/src/cli/local-bridge/console-view-models.ts +3 -12
  419. package/src/cli/local-bridge/facade-api.ts +105 -205
  420. package/src/cli/local-bridge/job-store.ts +1 -1
  421. package/src/cli/local-bridge/server.ts +48 -174
  422. package/src/cli/local-bridge/types.ts +0 -1
  423. package/src/cli/mcp/access-mode.ts +2 -132
  424. package/src/cli/mcp/access-tools.ts +2 -387
  425. package/src/cli/mcp/audit.ts +2 -32
  426. package/src/cli/mcp/auth.ts +2 -404
  427. package/src/cli/mcp/instructions.ts +2 -34
  428. package/src/cli/mcp/legacy-context.ts +2 -16
  429. package/src/cli/mcp/legacy-tool-service.ts +2 -5308
  430. package/src/cli/mcp/multi-repository.ts +2 -436
  431. package/src/cli/mcp/oauth-client-retirement.ts +74 -0
  432. package/src/cli/mcp/oauth.ts +2 -375
  433. package/src/cli/mcp/openai-secure-tunnel.ts +2 -0
  434. package/src/cli/mcp/paths.ts +2 -140
  435. package/src/cli/mcp/policy.ts +2 -232
  436. package/src/cli/mcp/post-finalize-work-attribution.ts +2 -150
  437. package/src/cli/mcp/proxy-env.ts +2 -62
  438. package/src/cli/mcp/readiness-projection.ts +2 -24
  439. package/src/cli/mcp/redaction.ts +2 -21
  440. package/src/cli/mcp/repo.ts +2 -24
  441. package/src/cli/mcp/repository-tools.ts +2 -1613
  442. package/src/cli/mcp/server.ts +2 -951
  443. package/src/cli/mcp/setup.ts +2 -737
  444. package/src/cli/mcp/tool-capability-cache.ts +2 -87
  445. package/src/cli/mcp/tool-contract.ts +2 -14
  446. package/src/cli/mcp/tools.ts +2 -9
  447. package/src/cli/mcp/toolset-names.ts +2 -70
  448. package/src/cli/mcp/toolset-selection.ts +2 -41
  449. package/src/cli/mcp/toolset.ts +2 -336
  450. package/src/cli/mcp/transports/AGENTS.md +1 -10
  451. package/src/cli/mcp/transports/CLAUDE.md +1 -10
  452. package/src/cli/mcp/transports/http.ts +2 -1414
  453. package/src/cli/mcp/transports/session-registry.ts +2 -490
  454. package/src/cli/mcp/transports/stdio.ts +2 -8
  455. package/src/cli/mcp/types.ts +2 -41
  456. package/src/cli/mcp/umbrella-tools.ts +2 -117
  457. package/src/cli/repositories/command-executor.ts +91 -39
  458. package/src/cli/repositories/command-scope.ts +139 -4
  459. package/src/cli/repositories/controller-home.ts +167 -1
  460. package/src/cli/repositories/ephemeral-workspace.ts +20 -30
  461. package/src/cli/repositories/git-command-executor.ts +2 -2
  462. package/src/cli/repositories/identity.ts +13 -0
  463. package/src/cli/repositories/registry.ts +145 -60
  464. package/src/cli/repositories/repository-snapshot.ts +53 -7
  465. package/src/cli/repositories/runtime-storage.ts +27 -2
  466. package/src/cli/repositories/safe-patch.ts +34 -38
  467. package/src/cli/repositories/selected-path-actions.ts +22 -2
  468. package/src/cli/repositories/structured-git.ts +167 -27
  469. package/src/cli/repository/session-cache.ts +92 -6
  470. package/src/cli/runtime/helper-runner.ts +7 -0
  471. package/src/cli/runtime-invocation.ts +8 -1
  472. package/src/cli/tools/codegraph.ts +44 -25
  473. package/src/core/adoption/manifest-templates.ts +1 -1
  474. package/src/core/adoption/plan.ts +3 -26
  475. package/src/core/adoption/workflow-contract-plan.ts +17 -9
  476. package/src/effects/process-runner.ts +8 -14
  477. package/src/runtime/context/assistant-context.ts +167 -0
  478. package/src/runtime/context/assistant-work-context.ts +209 -0
  479. package/src/runtime/context/automatic-learning.ts +565 -0
  480. package/src/runtime/context/codegraph-cache-boundary.ts +255 -0
  481. package/src/runtime/context/codegraph-read-provider.ts +55 -22
  482. package/src/runtime/context/codegraph-sidecar.cjs +8 -1
  483. package/src/runtime/context/cognitive-attention.ts +173 -0
  484. package/src/runtime/context/cognitive-skill-canary.ts +158 -0
  485. package/src/runtime/context/context-closure.ts +305 -0
  486. package/src/runtime/context/context-pack-process.ts +103 -0
  487. package/src/runtime/context/context-plane.ts +203 -0
  488. package/src/runtime/context/direct-controller-learning.ts +298 -0
  489. package/src/runtime/context/project-engineering-contract.ts +60 -0
  490. package/src/runtime/context/semantic-navigation-contract.ts +9 -0
  491. package/src/runtime/context/semantic-navigation.ts +6 -34
  492. package/src/runtime/context/typescript-navigation-process.ts +145 -0
  493. package/src/runtime/context/typescript-navigation.ts +89 -3
  494. package/src/runtime/control-plane/bootstrap/capability-provider.ts +74 -0
  495. package/src/runtime/control-plane/bootstrap/control-api.ts +57 -0
  496. package/src/runtime/control-plane/bootstrap/index.ts +5 -0
  497. package/src/runtime/control-plane/bootstrap/state-machine.ts +141 -0
  498. package/src/runtime/control-plane/bootstrap/store.ts +21 -0
  499. package/src/runtime/control-plane/bootstrap/types.ts +87 -0
  500. package/src/runtime/control-plane/cleanup-artifact-retention.ts +531 -0
  501. package/src/runtime/control-plane/codegraph-cache-retention.ts +164 -0
  502. package/src/runtime/control-plane/concurrency/work-execution-concurrency.ts +282 -0
  503. package/src/runtime/control-plane/execution/controller-authority-recovery.ts +380 -0
  504. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +113 -16
  505. package/src/runtime/control-plane/execution/execution-identity.ts +23 -3
  506. package/src/runtime/control-plane/execution/implementation-review-content.ts +95 -0
  507. package/src/runtime/control-plane/execution/repository-work-attribution.ts +7 -30
  508. package/src/runtime/control-plane/execution/retained-work-resume.ts +439 -102
  509. package/src/runtime/control-plane/execution/session-store.ts +31 -1
  510. package/src/runtime/control-plane/execution/validation.ts +34 -11
  511. package/src/runtime/control-plane/execution/work-authority-reconciler.ts +87 -0
  512. package/src/runtime/control-plane/execution/work-check-evidence.ts +137 -0
  513. package/src/runtime/control-plane/execution/work-completion-authority.ts +31 -44
  514. package/src/runtime/control-plane/execution/work-delivery-service.ts +497 -0
  515. package/src/runtime/control-plane/execution/work-evidence-policy.ts +46 -53
  516. package/src/runtime/control-plane/execution/work-execution-support.ts +85 -164
  517. package/src/runtime/control-plane/execution/work-handle-authority.ts +349 -73
  518. package/src/runtime/control-plane/execution/work-handle-store.ts +55 -2
  519. package/src/runtime/control-plane/execution/work-head-settlement.ts +108 -0
  520. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +173 -28
  521. package/src/runtime/control-plane/execution/work-operation-service.ts +92 -40
  522. package/src/runtime/control-plane/execution/work-preparation-service.ts +145 -269
  523. package/src/runtime/control-plane/execution/work-remote-delivery.ts +2 -3
  524. package/src/runtime/control-plane/execution/work-revision-diff.ts +39 -0
  525. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +543 -225
  526. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +59 -45
  527. package/src/runtime/control-plane/execution/work-verification-context.ts +1 -1
  528. package/src/runtime/control-plane/execution/work-verification-service.ts +662 -52
  529. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +205 -16
  530. package/src/runtime/control-plane/facade/capability-registry.ts +380 -44
  531. package/src/runtime/control-plane/facade/check-normalization.ts +3 -9
  532. package/src/runtime/control-plane/facade/codex-delegation.ts +10 -11
  533. package/src/runtime/control-plane/facade/controller-connection.ts +135 -0
  534. package/src/runtime/control-plane/facade/controller-session-store.ts +2 -488
  535. package/src/runtime/control-plane/facade/goal-workloop-access.ts +6 -22
  536. package/src/runtime/control-plane/facade/goal-workloop.ts +565 -1202
  537. package/src/runtime/control-plane/facade/handoff-inbox-application.ts +145 -0
  538. package/src/runtime/control-plane/facade/handoff-inbox-store.ts +266 -52
  539. package/src/runtime/control-plane/facade/index.ts +4 -3
  540. package/src/runtime/control-plane/facade/operation-digest.ts +34 -73
  541. package/src/runtime/control-plane/facade/plan-contract-store.ts +671 -245
  542. package/src/runtime/control-plane/facade/policy-gate.ts +0 -23
  543. package/src/runtime/control-plane/facade/repository-work-admission.ts +31 -23
  544. package/src/runtime/control-plane/facade/requirement-authority.ts +121 -0
  545. package/src/runtime/control-plane/facade/requirement-board.ts +9 -4
  546. package/src/runtime/control-plane/facade/rh-work-operation-contract.ts +85 -0
  547. package/src/runtime/control-plane/facade/self-healing-loop.ts +18 -53
  548. package/src/runtime/control-plane/facade/semantic-admission.ts +8 -16
  549. package/src/runtime/control-plane/facade/suggested-actions.ts +15 -15
  550. package/src/runtime/control-plane/facade/types.ts +218 -620
  551. package/src/runtime/control-plane/facade/work-continuation.ts +8 -8
  552. package/src/runtime/control-plane/facade/work-contract-store.ts +2 -1043
  553. package/src/runtime/control-plane/generated-cache-retention.ts +336 -19
  554. package/src/runtime/control-plane/global-scheduler/autonomous-continuation.ts +248 -0
  555. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +13 -2
  556. package/src/runtime/control-plane/global-scheduler/maintenance.ts +274 -84
  557. package/src/runtime/control-plane/global-scheduler/periodic-cleanup-entry.ts +55 -0
  558. package/src/runtime/control-plane/global-scheduler/periodic-cleanup-process.ts +98 -0
  559. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -2
  560. package/src/runtime/control-plane/global-scheduler/scheduler-worker-entry.ts +10 -0
  561. package/src/runtime/control-plane/global-scheduler/scheduler.ts +227 -109
  562. package/src/runtime/control-plane/global-scheduler/wake-signal.ts +42 -2
  563. package/src/runtime/control-plane/global-scheduler/worker-launch.ts +18 -6
  564. package/src/runtime/control-plane/governance/access-policy.ts +19 -2
  565. package/src/runtime/control-plane/governance/authorization.ts +20 -3
  566. package/src/runtime/control-plane/launcher/chatgpt-round-continuation.ts +160 -0
  567. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +2 -177
  568. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +139 -670
  569. package/src/runtime/control-plane/launcher/local-conversation-controller.ts +238 -0
  570. package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +25 -6
  571. package/src/runtime/control-plane/launcher/thin-launcher.ts +96 -194
  572. package/src/runtime/control-plane/lifecycle-retention-metrics.ts +60 -0
  573. package/src/runtime/control-plane/persistence/cognition-store.ts +820 -0
  574. package/src/runtime/control-plane/persistence/experience-store.ts +302 -0
  575. package/src/runtime/control-plane/persistence/operational-prior-store.ts +348 -0
  576. package/src/runtime/control-plane/persistence/requirement-store.ts +230 -80
  577. package/src/runtime/control-plane/persistence/sqlite-store.ts +654 -78
  578. package/src/runtime/control-plane/persistence/workflow-content-store.ts +98 -0
  579. package/src/runtime/control-plane/persistence/workflow-registry-store.ts +186 -0
  580. package/src/runtime/control-plane/persistence/workflow-run-store.ts +123 -0
  581. package/src/runtime/control-plane/release-retention.ts +154 -26
  582. package/src/runtime/control-plane/repository-namespace-retention.ts +208 -0
  583. package/src/runtime/control-plane/runtime-cleanup.ts +844 -38
  584. package/src/runtime/control-plane/runtime-generation.ts +6 -0
  585. package/src/runtime/control-plane/workspace/project-onboarding.ts +80 -0
  586. package/src/runtime/control-plane/workspace/semantic-sync-service.ts +479 -0
  587. package/src/runtime/control-plane/workspace/workspace-store.ts +249 -0
  588. package/src/runtime/diagnostics/incident-repair.ts +573 -0
  589. package/src/runtime/diagnostics/mcp-timing.ts +239 -6
  590. package/src/runtime/diagnostics/performance.ts +6 -14
  591. package/src/runtime/diagnostics/read-only-tool.ts +1 -1
  592. package/src/runtime/evidence/artifact-store.ts +216 -35
  593. package/src/runtime/evidence/evidence-store.ts +112 -8
  594. package/src/runtime/evidence/operational-shadow.ts +38 -0
  595. package/src/runtime/evidence/output-handle-index.ts +225 -0
  596. package/src/runtime/evidence/process-check-receipt.ts +5 -43
  597. package/src/runtime/evidence/result-store.ts +158 -44
  598. package/src/runtime/evidence/sensitive-output.ts +4 -4
  599. package/src/runtime/execution/jobs/receipt-recovery.ts +1 -1
  600. package/src/runtime/execution/jobs/store.ts +124 -6
  601. package/src/runtime/execution/managed-workspace.ts +112 -5
  602. package/src/runtime/execution/process-runtime/canary.ts +19 -5
  603. package/src/runtime/execution/process-runtime/check-classification.ts +48 -0
  604. package/src/runtime/execution/process-runtime/check-facade.ts +24 -10
  605. package/src/runtime/execution/process-runtime/check-receipt.ts +1 -1
  606. package/src/runtime/execution/process-runtime/check-result-retention.ts +132 -0
  607. package/src/runtime/execution/process-runtime/check-result.ts +177 -2
  608. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +58 -9
  609. package/src/runtime/execution/process-runtime/check-storage.ts +187 -0
  610. package/src/runtime/execution/process-runtime/command-facade.ts +117 -114
  611. package/src/runtime/execution/process-runtime/gc.ts +15 -2
  612. package/src/runtime/execution/process-runtime/handle-index.ts +176 -0
  613. package/src/runtime/execution/process-runtime/host-command-lane.ts +139 -0
  614. package/src/runtime/execution/process-runtime/index.ts +4 -0
  615. package/src/runtime/execution/process-runtime/interactive-admission.ts +2 -1
  616. package/src/runtime/execution/process-runtime/lightweight-managed.ts +283 -73
  617. package/src/runtime/execution/process-runtime/persisted-check.ts +130 -23
  618. package/src/runtime/execution/process-runtime/process-runner-entry.ts +47 -5
  619. package/src/runtime/execution/process-runtime/process-scope.ts +160 -0
  620. package/src/runtime/execution/process-runtime/resource-claims.ts +125 -64
  621. package/src/runtime/execution/process-runtime/runtime.ts +167 -17
  622. package/src/runtime/execution/process-runtime/store.ts +30 -2
  623. package/src/runtime/execution/process-runtime/types.ts +27 -0
  624. package/src/runtime/execution/workers/executor.ts +18 -19
  625. package/src/runtime/execution/workers/worker-entry.ts +1 -1
  626. package/src/runtime/gateway/mcp/execution-tools.ts +2 -207
  627. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +2 -61
  628. package/src/runtime/gateway/mcp/persisted-check-process.ts +2 -3
  629. package/src/runtime/gateway/mcp/process-tools.ts +2 -289
  630. package/src/runtime/gateway/mcp/router.ts +2 -1125
  631. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +2 -565
  632. package/src/runtime/gateway/mcp/runtime-tools.ts +2 -6655
  633. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +2 -1
  634. package/src/runtime/maintenance/cleanup.ts +38 -24
  635. package/src/runtime/platform/browser-product-discovery.ts +174 -0
  636. package/src/runtime/platform/computer-platform.ts +14 -0
  637. package/src/runtime/platform/executable-discovery.ts +54 -0
  638. package/src/runtime/platform/fetch-http-server.ts +90 -0
  639. package/src/runtime/platform/service-inventory.ts +130 -0
  640. package/src/runtime/platform/service-manager.ts +146 -0
  641. package/src/runtime/platform/windows-host-discovery.ts +118 -0
  642. package/src/runtime/platform/windows-recovery-deployment.ts +67 -0
  643. package/src/runtime/plugins/action-application.ts +192 -0
  644. package/src/runtime/plugins/app-store-connect-adapter.ts +47 -35
  645. package/src/runtime/plugins/browser-adapter.ts +898 -135
  646. package/src/runtime/plugins/browser-automation-service.ts +33 -34
  647. package/src/runtime/plugins/browser-macos-bridge.ts +386 -46
  648. package/src/runtime/plugins/browser-manifest-surface.ts +75 -17
  649. package/src/runtime/plugins/browser-native-session-service.ts +64 -14
  650. package/src/runtime/plugins/browser-provider-registry.ts +2 -109
  651. package/src/runtime/plugins/browser-registration.ts +32 -0
  652. package/src/runtime/plugins/browser-runtime-contract.ts +2 -154
  653. package/src/runtime/plugins/browser-runtime.ts +55 -10
  654. package/src/runtime/plugins/browser-session-authority.ts +101 -291
  655. package/src/runtime/plugins/browser-session-legacy-migration.ts +203 -0
  656. package/src/runtime/plugins/browser-session-store.ts +243 -80
  657. package/src/runtime/plugins/browser-session-types.ts +2 -98
  658. package/src/runtime/plugins/capability-authorization-grants.ts +272 -59
  659. package/src/runtime/plugins/catalog-trust.ts +82 -0
  660. package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +219 -0
  661. package/src/runtime/plugins/computer-chatgpt-macos-target.ts +891 -0
  662. package/src/runtime/plugins/computer-registration.ts +1050 -0
  663. package/src/runtime/plugins/config-store.ts +119 -0
  664. package/src/runtime/plugins/desktop-operator-external-policy.ts +964 -0
  665. package/src/runtime/plugins/desktop-operator-registration.ts +123 -12
  666. package/src/runtime/plugins/errors.ts +8 -1
  667. package/src/runtime/plugins/external-adapter.ts +82 -323
  668. package/src/runtime/plugins/external-provider-policy.ts +48 -0
  669. package/src/runtime/plugins/external-registration.ts +30 -2
  670. package/src/runtime/plugins/external-unix-socket-probe.cjs +9 -3
  671. package/src/runtime/plugins/external-unix-socket.ts +64 -193
  672. package/src/runtime/plugins/first-party-external-registration.ts +163 -0
  673. package/src/runtime/plugins/first-party-registry.ts +3 -1
  674. package/src/runtime/plugins/github-adapter.ts +1 -2
  675. package/src/runtime/plugins/gmail-adapter.ts +29 -8
  676. package/src/runtime/plugins/google-calendar-adapter.ts +8 -8
  677. package/src/runtime/plugins/google-shared.ts +45 -53
  678. package/src/runtime/plugins/google-tasks-adapter.ts +8 -8
  679. package/src/runtime/plugins/ios-adapter.ts +2 -3
  680. package/src/runtime/plugins/lightweight-action.ts +208 -2
  681. package/src/runtime/plugins/local-system-adapter.ts +192 -2
  682. package/src/runtime/plugins/macos-capability-broker.ts +15 -105
  683. package/src/runtime/plugins/managed-process-adapter.ts +12 -5
  684. package/src/runtime/plugins/plugin-action-sidecar.ts +17 -13
  685. package/src/runtime/plugins/plugin-management-adapter.ts +1 -1
  686. package/src/runtime/plugins/resend-adapter.ts +15 -24
  687. package/src/runtime/plugins/store.ts +1003 -387
  688. package/src/runtime/plugins/types.ts +28 -1
  689. package/src/runtime/plugins/uu-remote-rescue-registration.ts +155 -0
  690. package/src/runtime/plugins/windows-host-recovery-install.ts +68 -0
  691. package/src/runtime/plugins/windows-host-recovery-registration.ts +85 -0
  692. package/src/runtime/plugins/xiaohongshu-publish.ts +102 -430
  693. package/src/runtime/projections/controller-context.ts +12 -6
  694. package/src/runtime/projections/git-status-sampler.ts +73 -22
  695. package/src/runtime/projections/invalidation.ts +7 -3
  696. package/src/runtime/projections/materialized-view.ts +93 -43
  697. package/src/runtime/recovery/actions.ts +3 -3
  698. package/src/runtime/recovery/capability-action-application.ts +229 -0
  699. package/src/runtime/recovery/index.ts +3 -0
  700. package/src/runtime/recovery/lifecycle-inventory.ts +338 -0
  701. package/src/runtime/recovery/local-jobs-repair.ts +14 -14
  702. package/src/runtime/recovery/maintenance-executor.ts +701 -168
  703. package/src/runtime/recovery/quarantine-retention.ts +256 -0
  704. package/src/runtime/recovery/store.ts +29 -11
  705. package/src/runtime/release/identity.ts +6 -0
  706. package/src/runtime/release/release-coordinator.ts +296 -0
  707. package/src/runtime/release/release-session.ts +716 -0
  708. package/src/runtime/release/release-state-migration.ts +25 -0
  709. package/src/runtime/resources/leases/store.ts +56 -25
  710. package/src/runtime/root/active-execution-power-assertion.ts +139 -0
  711. package/src/runtime/root/assistant-learning-loop.ts +45 -0
  712. package/src/runtime/root/browser-session-composition.ts +59 -0
  713. package/src/runtime/root/computer-composition.ts +376 -0
  714. package/src/runtime/root/computer-target-composition.ts +16 -0
  715. package/src/runtime/root/computer-target-persistence.ts +39 -0
  716. package/src/runtime/root/controller-progression-composition.ts +192 -0
  717. package/src/runtime/root/controller-round-composition.ts +126 -0
  718. package/src/runtime/root/controller-services.ts +2 -0
  719. package/src/runtime/root/deployment-topology.ts +106 -0
  720. package/src/runtime/root/entry.ts +8 -6
  721. package/src/runtime/root/gateway-adapter.ts +3 -3
  722. package/src/runtime/root/known-good-recovery.ts +184 -0
  723. package/src/runtime/root/local-bridge.ts +5 -3
  724. package/src/runtime/root/mcp-transport.ts +116 -23
  725. package/src/runtime/root/ownership.ts +261 -9
  726. package/src/runtime/root/package-connector-service.ts +212 -74
  727. package/src/runtime/root/package-runtime-release.ts +199 -33
  728. package/src/runtime/root/package-runtime-service.ts +77 -108
  729. package/src/runtime/root/release-execution-canary.ts +103 -0
  730. package/src/runtime/root/release-loader.ts +150 -0
  731. package/src/runtime/root/release-manifest.ts +185 -7
  732. package/src/runtime/root/release-materialize.ts +474 -31
  733. package/src/runtime/root/release-store.ts +425 -37
  734. package/src/runtime/root/runtime-lane.ts +191 -0
  735. package/src/runtime/root/runtime.ts +379 -32
  736. package/src/runtime/root/scheduled-controller-composition.ts +76 -0
  737. package/src/runtime/root/scheduler.ts +12 -4
  738. package/src/runtime/root/service-runner.ts +3 -3
  739. package/src/runtime/root/service.ts +73 -45
  740. package/src/runtime/root/status.ts +49 -0
  741. package/src/runtime/root/types.ts +55 -3
  742. package/src/runtime/root/workflow-supervisor-composition.ts +493 -0
  743. package/src/runtime/root/workflow-supervisor-runtime.ts +209 -0
  744. package/src/runtime/root/write-fence.ts +50 -2
  745. package/src/runtime/safe-tooling/google-credential-store.ts +76 -21
  746. package/src/runtime/safe-tooling/google-oauth-broker.ts +25 -7
  747. package/src/runtime/safe-tooling/workspace-auth.ts +1 -1
  748. package/src/runtime/shared/bounded-child-supervisor.ts +9 -1
  749. package/src/runtime/shared/local-bridge-surface.ts +1 -1
  750. package/src/runtime/shared/process-environment.ts +36 -10
  751. package/src/runtime/shared/response-budget.ts +1 -3
  752. package/src/runtime/shared/storage-capacity.ts +68 -0
  753. package/src/runtime/standalone-recovery/controller-home-migration.ts +1108 -0
  754. package/src/runtime/standalone-recovery/core.ts +4609 -495
  755. package/src/runtime/standalone-recovery/entry.ts +642 -81
  756. package/src/runtime/standalone-recovery/independent-host-rescue.ts +172 -0
  757. package/src/runtime/standalone-recovery/installer.ts +399 -177
  758. package/src/runtime/standalone-recovery/mcp-server.ts +79 -0
  759. package/src/runtime/standalone-recovery/mutation-identity-contract.ts +21 -0
  760. package/src/runtime/standalone-recovery/operation-lock.ts +160 -0
  761. package/src/runtime/standalone-recovery/performance.ts +173 -0
  762. package/src/runtime/standalone-recovery/release.ts +58 -5
  763. package/src/runtime/standalone-recovery/service-labels.ts +5 -2
  764. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +5 -2
  765. package/src/runtime/watchdog/workflow-watchdog.ts +1 -1
  766. package/src/runtime/workflow/schedules/browser-probe.ts +17 -6
  767. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +11 -4
  768. package/src/runtime/workflow/schedules/engine.ts +298 -484
  769. package/src/runtime/workflow/schedules/pending-activation.ts +1 -1
  770. package/src/runtime/workflow/schedules/settlement.ts +2 -191
  771. package/src/runtime/workflow/schedules/store.ts +2 -607
  772. package/src/runtime/workflow/schedules/types.ts +2 -127
  773. package/src/runtime/workflow/schedules/work-continuation.ts +220 -16
  774. package/src/runtime/workflows/first-party/xiaohongshu.ts +122 -0
  775. package/src/runtime/workflows/runtime.ts +482 -0
  776. package/src/testing/test-governance.ts +95 -30
  777. package/supervisor/chatgpt-conversation.ts +23 -0
  778. package/supervisor/client.ts +201 -0
  779. package/supervisor/control-plane.ts +868 -0
  780. package/supervisor/entry.ts +15 -0
  781. package/supervisor/forge-validators.ts +80 -0
  782. package/supervisor/index.ts +5 -0
  783. package/supervisor/native-browser-adapter.ts +786 -0
  784. package/supervisor/native-messaging/host.ts +39 -0
  785. package/supervisor/native-messaging/manifest.ts +26 -0
  786. package/supervisor/native-messaging/protocol.ts +27 -0
  787. package/supervisor/paths.ts +18 -0
  788. package/supervisor/protocol.ts +202 -0
  789. package/supervisor/server.ts +404 -0
  790. package/supervisor/store.ts +1451 -0
  791. package/supervisor/tsconfig.json +8 -0
  792. package/supervisor/types.ts +255 -0
  793. package/assets/templates/helpers/archive-architecture-request.sh +0 -244
  794. package/docs/architecture/history/README.md +0 -7
  795. package/docs/architecture/history/chatgpt-handoff-facade.md +0 -397
  796. package/docs/architecture/history/global-hook-runtime.md +0 -166
  797. package/docs/architecture/history/ios-semantic-automation-provider-v2.md +0 -170
  798. package/docs/architecture/requests/archive/2026/20260528-154740-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
  799. package/docs/architecture/requests/archive/2026/20260528-154816-assets-hooks-assets-hooks-hook-input-sh.md +0 -59
  800. package/docs/architecture/requests/archive/2026/20260528-161851-ai-harness-workflow-contract-json-ai-harness-workflow-contract-json.md +0 -59
  801. package/docs/architecture/requests/archive/2026/20260528-161853-scripts-check-task-workflow-sh-scripts-check-task-workflow-sh.md +0 -59
  802. package/docs/architecture/requests/archive/2026/20260528-161911-assets-workflow-contract-v1-json-assets-workflow-contract-v1-json.md +0 -59
  803. package/docs/architecture/requests/archive/2026/20260528-222416-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
  804. package/docs/architecture/requests/archive/2026/20260528-222444-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
  805. package/docs/architecture/requests/archive/2026/20260528-222457-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
  806. package/docs/architecture/requests/archive/2026/20260528-222648-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
  807. package/docs/architecture/requests/archive/2026/20260528-222705-ai-hooks-ai-hooks-worktree-guard-sh.md +0 -59
  808. package/docs/architecture/requests/archive/2026/20260528-222726-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
  809. package/docs/architecture/requests/archive/2026/20260528-222727-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
  810. package/docs/architecture/requests/archive/2026/20260528-224736-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
  811. package/docs/architecture/requests/archive/2026/20260528-233259-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
  812. package/docs/architecture/requests/archive/2026/20260528-233335-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
  813. package/docs/architecture/requests/archive/2026/20260528-233456-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
  814. package/docs/architecture/requests/archive/2026/20260528-233457-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
  815. package/docs/architecture/requests/archive/2026/20260528-233637-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
  816. package/docs/architecture/requests/archive/2026/20260528-233638-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
  817. package/docs/architecture/requests/archive/2026/20260529-000322-root-package-json.md +0 -59
  818. package/docs/architecture/requests/archive/2026/20260529-004238-ai-hooks-ai-hooks-lib-workflow-state-sh.md +0 -59
  819. package/docs/architecture/requests/archive/2026/20260529-020654-ai-harness-policy-json-ai-harness-policy-json.md +0 -63
  820. package/docs/architecture/requests/archive/2026/20260529-094236-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -60
  821. package/docs/architecture/requests/archive/2026/20260529-094323-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -60
  822. package/docs/architecture/requests/archive/2026/20260529-094331-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -60
  823. package/docs/architecture/requests/archive/2026/20260529-094339-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -60
  824. package/docs/architecture/requests/archive/2026/20260529-094430-assets-hooks-assets-hooks-session-start-context-sh.md +0 -60
  825. package/docs/architecture/requests/archive/2026/20260529-094446-ai-hooks-ai-hooks-session-start-context-sh.md +0 -60
  826. package/docs/architecture/requests/archive/2026/20260613-002040-runtime-harness-hook-adapters.md +0 -68
  827. package/docs/architecture/requests/archive/2026/root.md +0 -68
  828. package/docs/architecture/requests/archive/2026/runtime-harness-hook-adapters.md +0 -76
  829. package/docs/architecture/requests/archive/2026/verification-evals-checks.md +0 -68
  830. package/docs/architecture/requests/archive/2026/workflow-engine-contract-assets.md +0 -69
  831. package/scripts/archive-architecture-request.sh +0 -4
  832. package/scripts/benchmark-background-check-overlap.ts +0 -228
  833. package/scripts/benchmark-controller-hotpath.ts +0 -212
  834. package/scripts/benchmark-controller-round2.ts +0 -139
  835. package/scripts/benchmark-ios-agent-device.ts +0 -514
  836. package/scripts/benchmark-mcp-host.sh +0 -117
  837. package/scripts/benchmark-mcp-transport.ts +0 -170
  838. package/scripts/benchmark-quality-harness.ts +0 -146
  839. package/scripts/benchmark-route-session-concurrency.ts +0 -915
  840. package/scripts/benchmark-thin-harness-gateway-ab.ts +0 -632
  841. package/scripts/benchmark-thin-harness-mcp-e2e.ts +0 -498
  842. package/scripts/benchmark-thin-harness.ts +0 -251
  843. package/scripts/benchmark-typescript-navigation.ts +0 -186
  844. package/scripts/benchmark-work-efficiency.ts +0 -334
  845. package/scripts/build-controller-ui.ts +0 -28
  846. package/scripts/check-npm-release.sh +0 -32
  847. package/scripts/check-public-release-surface.sh +0 -19
  848. package/scripts/check-release-published.sh +0 -57
  849. package/scripts/check-release-readiness.sh +0 -37
  850. package/scripts/check-release-version.mjs +0 -74
  851. package/scripts/check-tarball-install-smoke.sh +0 -67
  852. package/scripts/package-source-archive.sh +0 -147
  853. package/scripts/public-release-files.txt +0 -39
  854. package/scripts/public-release.ts +0 -174
  855. package/scripts/publish-release-tarball.sh +0 -27
  856. package/scripts/route-nl-vs-ts-eval.ts +0 -679
  857. package/scripts/run-skill-evals.ts +0 -1022
  858. package/scripts/stage-runtime-release.ts +0 -39
  859. package/src/cli/controller/progress.ts +0 -656
  860. package/src/cli/controller/work-mode.ts +0 -181
  861. package/src/cli/local-bridge/console-assets.ts +0 -17
  862. package/src/cli/local-bridge/dashboard.ts +0 -17
  863. package/src/cli/local-bridge/frontend/src/App.tsx +0 -10
  864. package/src/cli/local-bridge/frontend/src/components/Button.tsx +0 -1
  865. package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +0 -3
  866. package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +0 -1
  867. package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +0 -1
  868. package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +0 -1
  869. package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +0 -3
  870. package/src/cli/local-bridge/frontend/src/design/icons.tsx +0 -11
  871. package/src/cli/local-bridge/frontend/src/design/tokens.css +0 -8
  872. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +0 -153
  873. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +0 -37
  874. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +0 -183
  875. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +0 -2
  876. package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +0 -2
  877. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +0 -4
  878. package/src/cli/local-bridge/frontend/src/lib/api.ts +0 -30
  879. package/src/cli/local-bridge/frontend/src/lib/format.ts +0 -4
  880. package/src/cli/local-bridge/frontend/src/lib/routes.ts +0 -7
  881. package/src/cli/local-bridge/frontend/src/main.tsx +0 -1
  882. package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +0 -1
  883. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +0 -2
  884. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +0 -4
  885. package/src/cli/local-bridge/frontend/src/styles.css +0 -28
  886. package/src/cli/local-bridge/frontend/src/types.ts +0 -15
  887. package/src/cli/local-bridge/frontend/src/vite-env.d.ts +0 -1
  888. package/src/cli/local-bridge/frontend/vite.config.ts +0 -27
  889. package/src/cli/local-bridge/ui-dist/app.css +0 -1
  890. package/src/cli/local-bridge/ui-dist/app.js +0 -11228
  891. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +0 -385
  892. package/src/runtime/control-plane/execution/work-finalization-service.ts +0 -1913
  893. package/src/runtime/control-plane/execution/work-path-scope.ts +0 -37
  894. package/src/runtime/control-plane/execution/work-task-receipt.ts +0 -449
  895. package/src/runtime/control-plane/facade/controller-round-relay.ts +0 -1118
  896. package/src/runtime/control-plane/facade/work-admission-policy.ts +0 -93
  897. package/src/runtime/control-plane/facade/work-task-projection.ts +0 -66
  898. package/src/runtime/control-plane/routing/index.ts +0 -1
  899. package/src/runtime/control-plane/routing/route-policy.ts +0 -382
  900. package/src/runtime/control-plane/routing/workspace-admission.ts +0 -70
@@ -1,38 +1,50 @@
1
1
  import { createHash, randomUUID } from 'crypto';
2
+ import { spawn, spawnSync } from 'child_process';
2
3
  import { createRequire } from 'module';
3
- import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from 'fs';
4
+ import { createServer as createNetServer } from 'net';
5
+ import { existsSync, mkdirSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'fs';
6
+ import { homedir } from 'os';
4
7
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'path';
5
8
  import type {
6
9
  AssistantPluginActionExecutionInput,
7
10
  AssistantPluginAuthorizationContext,
11
+ AssistantPluginBuildContext,
8
12
  AssistantPluginHealth,
9
13
  AssistantPluginManifest,
10
14
  } from './types';
11
15
  import { AssistantPluginError, toAssistantPluginError } from './errors';
16
+ import { listCanonicalGrants } from '../../../packages/kernel/identity/api/index';
17
+ import { resolveRepoPreferredControllerHome } from '../../cli/repositories/controller-home';
18
+ import { readRepositoryPluginConfig, writeRepositoryPluginConfig, type RepositoryPluginConfigContext } from './config-store';
12
19
  import {
13
20
  browserActions,
14
21
  browserCapabilities,
15
22
  browserPermissions,
16
23
  } from './browser-manifest-surface';
17
24
  import { executeBrowserActionThroughNode, shouldUseBrowserNodeBridge } from './browser-node-bridge';
18
- import { BrowserProviderUnavailableBeforeActionError } from './browser-provider-registry';
19
- import { MAX_BROWSER_CDP_ENDPOINT_CANDIDATES } from './browser-runtime-contract';
25
+ import { BrowserProviderUnavailableBeforeActionError } from '../../../packages/plugin-runtime/browser/provider-registry';
26
+ import { MAX_BROWSER_CDP_ENDPOINT_CANDIDATES } from '../../../packages/plugin-runtime/browser/runtime-contract';
20
27
  import {
21
28
  ALL_BROWSER_PROVIDER_CAPABILITIES,
22
29
  browserActionCanReplayAfterDispatch as canReplayBrowserActionAfterDispatch,
30
+ browserExplicitPostActionWaitMs,
31
+ browserNativeForegroundVerificationWaitMs,
23
32
  executeBrowserRuntimeAction,
24
33
  invalidateBrowserRuntime,
25
34
  } from './browser-runtime';
26
- import { getExternalPluginAdapter } from './external-adapter';
35
+ import { activateRuntimeComputerBrowserApplication } from '../root/computer-composition';
27
36
  import {
28
- currentBrowserSessionAuthorityContext,
29
37
  DEFAULT_BROWSER_SESSION_LIST_LIMIT,
30
38
  MAX_BROWSER_SESSION_LIST_LIMIT,
31
- withBrowserSessionAuthorityContext,
32
- } from './browser-session-authority';
39
+ } from '../../../packages/plugin-runtime/browser/session-authority';
40
+ import {
41
+ currentRuntimeBrowserSessionAuthorityContext as currentBrowserSessionAuthorityContext,
42
+ withRuntimeBrowserSessionAuthorityContext as withBrowserSessionAuthorityContext,
43
+ } from '../root/browser-session-composition';
33
44
  import {
34
45
  closeMacOsBrowserOwnedTab,
35
46
  createMacOsBrowserOwnedPage,
47
+ createMacOsBrowserOwnedPageForProduct,
36
48
  reattachMacOsBrowserOwnedPage,
37
49
  discoverMacOsBrowserAttachment,
38
50
  invalidateMacOsBrowserPageHandle,
@@ -87,13 +99,11 @@ import type {
87
99
  BrowserTabMatchReason,
88
100
  BrowserTabResumeState,
89
101
  CdpAttachAttempt,
90
- } from './browser-session-types';
102
+ } from '../../../packages/protocols/browser/index';
91
103
 
92
104
  const BROWSER_PLUGIN_ID = 'browser';
93
- const CONFIG_ROOT = '.forge/plugins';
94
105
  const DEFAULT_TIMEOUT_MS = 30_000;
95
106
  const DEFAULT_MAX_TEXT_CHARS = 20_000;
96
- const DEFAULT_POST_ACTION_WAIT_MS = 750;
97
107
  const DEFAULT_CDP_DISCOVERY_TIMEOUT_MS = 1_500;
98
108
  const MAX_CDP_DISCOVERY_TIMEOUT_MS = 5_000;
99
109
 
@@ -102,12 +112,12 @@ type WaitForSelectorState = 'attached' | 'detached' | 'visible' | 'hidden';
102
112
  type BrowserChannel = 'chromium' | 'chrome' | 'chrome-beta' | 'chrome-dev' | 'chrome-canary';
103
113
  type BrowserNativeAttachMode = 'auto' | 'disabled';
104
114
 
105
- const CURRENT_BROWSER_CONFIG_SCHEMA_VERSION = 2 as const;
115
+ const CURRENT_BROWSER_CONFIG_SCHEMA_VERSION = 3 as const;
106
116
  const DEFAULT_USER_BROWSER_CHANNEL: BrowserChannel = 'chrome';
107
- const DEFAULT_USER_NATIVE_BROWSER_CANDIDATES: MacOsBrowserProduct[] = ['chrome'];
117
+ const DEFAULT_USER_NATIVE_BROWSER_CANDIDATES: MacOsBrowserProduct[] = ['chrome', 'vivaldi'];
108
118
 
109
119
  interface BrowserPluginConfig {
110
- schemaVersion: 2;
120
+ schemaVersion: 3;
111
121
  enabled: boolean;
112
122
  provider: 'playwright';
113
123
  browserMode?: BrowserMode;
@@ -126,7 +136,7 @@ interface BrowserPluginConfig {
126
136
  }
127
137
 
128
138
  type PersistedBrowserPluginConfig = Partial<Omit<BrowserPluginConfig, 'schemaVersion'>> & {
129
- schemaVersion?: 1 | 2;
139
+ schemaVersion?: 1 | 2 | 3;
130
140
  };
131
141
 
132
142
  type BrowserSessionInventory = {
@@ -149,6 +159,15 @@ interface BrowserActionTarget {
149
159
  existingSession?: BrowserSessionState;
150
160
  }
151
161
 
162
+ export function browserExplicitSessionRequiresExistingResource(
163
+ actionId: string,
164
+ explicitSessionId: string | undefined,
165
+ existingSessionPresent: boolean,
166
+ ): boolean {
167
+ if (!explicitSessionId) return false;
168
+ return actionId === 'navigate' || existingSessionPresent;
169
+ }
170
+
152
171
  interface BrowserActionScreenshot {
153
172
  path: string;
154
173
  relativePath: string;
@@ -161,15 +180,109 @@ interface BrowserProfileSelection {
161
180
  selectedProfilePath: string;
162
181
  }
163
182
 
183
+ type BrowserExtensionTargetLike = { url(): string };
184
+ type BrowserCdpSessionLike = {
185
+ send(method: string, params?: Record<string, unknown>): Promise<unknown>;
186
+ detach?(): Promise<void>;
187
+ };
188
+
189
+ function connectNativeBrowserCdp(endpoint: string, timeoutMs: number): Promise<BrowserCdpSessionLike> {
190
+ const boundedTimeoutMs = Math.max(250, Math.min(timeoutMs, 30_000));
191
+ return new Promise<BrowserCdpSessionLike>((resolveSession, rejectSession) => {
192
+ const socket = new WebSocket(endpoint);
193
+ const pending = new Map<number, {
194
+ resolve(value: unknown): void;
195
+ reject(error: Error): void;
196
+ timer: ReturnType<typeof setTimeout>;
197
+ }>();
198
+ let nextId = 1;
199
+ let connectSettled = false;
200
+
201
+ const failPending = (error: Error) => {
202
+ for (const request of pending.values()) {
203
+ clearTimeout(request.timer);
204
+ request.reject(error);
205
+ }
206
+ pending.clear();
207
+ };
208
+ const failConnect = (error: Error) => {
209
+ if (connectSettled) return;
210
+ connectSettled = true;
211
+ clearTimeout(connectTimer);
212
+ rejectSession(error);
213
+ };
214
+ const connectTimer = setTimeout(() => {
215
+ failConnect(new Error(`Browser CDP WebSocket did not open within ${boundedTimeoutMs}ms.`));
216
+ try { socket.close(); } catch { /* best-effort bounded cleanup */ }
217
+ }, boundedTimeoutMs);
218
+
219
+ socket.onopen = () => {
220
+ if (connectSettled) return;
221
+ connectSettled = true;
222
+ clearTimeout(connectTimer);
223
+ resolveSession({
224
+ send: (method, params) => new Promise<unknown>((resolveResult, rejectResult) => {
225
+ const id = nextId++;
226
+ const timer = setTimeout(() => {
227
+ pending.delete(id);
228
+ rejectResult(new Error(`Browser CDP method ${method} timed out after ${boundedTimeoutMs}ms.`));
229
+ }, boundedTimeoutMs);
230
+ pending.set(id, { resolve: resolveResult, reject: rejectResult, timer });
231
+ try {
232
+ socket.send(JSON.stringify({ id, method, ...(params ? { params } : {}) }));
233
+ } catch (error) {
234
+ clearTimeout(timer);
235
+ pending.delete(id);
236
+ rejectResult(error instanceof Error ? error : new Error(String(error)));
237
+ }
238
+ }),
239
+ detach: async () => {
240
+ failPending(new Error('Browser CDP session detached.'));
241
+ try { socket.close(); } catch { /* best-effort bounded cleanup */ }
242
+ },
243
+ });
244
+ };
245
+ socket.onmessage = (event) => {
246
+ if (typeof event.data !== 'string') return;
247
+ let message: { id?: unknown; result?: unknown; error?: { message?: unknown } };
248
+ try { message = JSON.parse(event.data) as typeof message; }
249
+ catch { return; }
250
+ if (!Number.isInteger(message.id)) return;
251
+ const request = pending.get(Number(message.id));
252
+ if (!request) return;
253
+ pending.delete(Number(message.id));
254
+ clearTimeout(request.timer);
255
+ if (message.error) {
256
+ request.reject(new Error(typeof message.error.message === 'string' ? message.error.message : 'Browser CDP command failed.'));
257
+ } else {
258
+ request.resolve(message.result);
259
+ }
260
+ };
261
+ socket.onerror = () => {
262
+ const error = new Error('Browser CDP WebSocket transport failed.');
263
+ failConnect(error);
264
+ failPending(error);
265
+ };
266
+ socket.onclose = () => {
267
+ const error = new Error('Browser CDP WebSocket transport closed.');
268
+ failConnect(error);
269
+ failPending(error);
270
+ };
271
+ });
272
+ }
164
273
  type BrowserContextLike = {
165
274
  pages(): PageLike[];
166
275
  newPage(): Promise<PageLike>;
167
276
  close(): Promise<void>;
277
+ browser?(): BrowserLike | null;
278
+ serviceWorkers?(): BrowserExtensionTargetLike[];
279
+ backgroundPages?(): BrowserExtensionTargetLike[];
168
280
  };
169
281
 
170
282
  type BrowserLike = {
171
283
  contexts(): BrowserContextLike[];
172
284
  newContext?(): Promise<BrowserContextLike>;
285
+ newBrowserCDPSession?(): Promise<BrowserCdpSessionLike>;
173
286
  close?(): Promise<void>;
174
287
  disconnect?(): Promise<void> | void;
175
288
  };
@@ -243,43 +356,56 @@ interface BrowserPluginRuntimeHooks {
243
356
  loadPlaywright(repoRoot?: string): PlaywrightRuntime;
244
357
  fetchJson(url: string, timeoutMs: number): Promise<unknown>;
245
358
  activateNativeBrowserApplication(input: AssistantPluginActionExecutionInput, product: MacOsBrowserProduct): Promise<void>;
246
- }
247
-
248
- const NATIVE_BROWSER_BUNDLE_IDS: Record<MacOsBrowserProduct, string> = {
249
- chrome: 'com.google.Chrome',
250
- vivaldi: 'com.vivaldi.Vivaldi',
251
- };
252
-
253
- async function activateNativeBrowserApplicationViaDesktopOperator(
254
- input: AssistantPluginActionExecutionInput,
255
- product: MacOsBrowserProduct,
256
- ): Promise<void> {
257
- const desktopOperator = getExternalPluginAdapter(input.controllerHome, 'desktop_operator');
258
- if (!desktopOperator) {
259
- throw new AssistantPluginError(
260
- 'PLUGIN_BROWSER_NATIVE_FOREGROUND_ACTIVATOR_UNAVAILABLE',
261
- 'Native browser foreground activation requires the registered Forge Desktop Operator.',
262
- { retryable: true, details: { browserProduct: product } },
263
- );
264
- }
265
- await desktopOperator.executeAction({
266
- controllerHome: input.controllerHome,
267
- repoId: input.repoId,
268
- repoRoot: input.repoRoot,
269
- pluginId: 'desktop_operator',
270
- actionId: 'desktop_session_open',
271
- requestId: `${input.requestId}:native-browser-foreground:${product}`,
272
- args: { bundle_id: NATIVE_BROWSER_BUNDLE_IDS[product], launch: false, activate: true },
273
- origin: input.origin,
274
- jobId: input.jobId,
275
- timeoutMs: input.timeoutMs,
276
- signal: input.signal,
277
- deadlineAtMs: input.deadlineAtMs,
278
- });
359
+ /** Bounded process inventory used only to reclaim Forge-owned browser profiles. */
360
+ listProcesses(): Array<{ pid: number; command: string }>;
361
+ /** Signal one process; returns false when it already exited. */
362
+ signalProcess(pid: number, signal: NodeJS.Signals): boolean;
363
+ /** Launch one browser process without claiming its lifecycle through Playwright. */
364
+ launchProcess(executable: string, args: string[]): { pid?: number };
365
+ /** Allocate one currently-free loopback TCP port for an immediately following browser launch. */
366
+ allocateLoopbackPort(): Promise<number>;
367
+ /** Open one bounded browser-level CDP session without a Playwright browser/context adapter. */
368
+ connectBrowserCdp(endpoint: string, timeoutMs: number): Promise<BrowserCdpSessionLike>;
279
369
  }
280
370
 
281
371
  const defaultRuntimeHooks: BrowserPluginRuntimeHooks = {
282
372
  now: () => new Date().toISOString(),
373
+ listProcesses: () => {
374
+ const result = spawnSync('ps', ['-eo', 'pid=,command='], { encoding: 'utf8', timeout: 10_000, maxBuffer: 8 * 1024 * 1024 });
375
+ if (result.status !== 0 || typeof result.stdout !== 'string') return [];
376
+ const entries: Array<{ pid: number; command: string }> = [];
377
+ for (const line of result.stdout.split('\n')) {
378
+ const match = /^\s*(\d+)\s+(.*)$/.exec(line);
379
+ if (!match?.[1] || !match[2]) continue;
380
+ entries.push({ pid: Number(match[1]), command: match[2] });
381
+ }
382
+ return entries;
383
+ },
384
+ signalProcess: (pid, signal) => {
385
+ try { process.kill(pid, signal); return true; }
386
+ catch (error) { return (error as NodeJS.ErrnoException).code !== 'ESRCH'; }
387
+ },
388
+ launchProcess: (executable, args) => {
389
+ const child = spawn(executable, args, { stdio: 'ignore', detached: true });
390
+ child.once('error', () => undefined);
391
+ child.unref();
392
+ return { pid: child.pid };
393
+ },
394
+ allocateLoopbackPort: () => new Promise<number>((resolvePort, rejectPort) => {
395
+ const server = createNetServer();
396
+ server.unref();
397
+ server.once('error', rejectPort);
398
+ server.listen({ host: '127.0.0.1', port: 0, exclusive: true }, () => {
399
+ const address = server.address();
400
+ const port = typeof address === 'object' && address ? address.port : 0;
401
+ server.close((error) => {
402
+ if (error) return rejectPort(error);
403
+ if (!Number.isInteger(port) || port <= 0 || port > 65_535) return rejectPort(new Error('BROWSER_LOOPBACK_PORT_ALLOCATION_FAILED'));
404
+ resolvePort(port);
405
+ });
406
+ });
407
+ }),
408
+ connectBrowserCdp: connectNativeBrowserCdp,
283
409
  moduleAvailable: (name: string, repoRoot?: string) => {
284
410
  const anchors = [repoRoot ? join(repoRoot, 'package.json') : undefined, import.meta.url]
285
411
  .filter((value): value is string => Boolean(value));
@@ -318,7 +444,7 @@ const defaultRuntimeHooks: BrowserPluginRuntimeHooks = {
318
444
  clearTimeout(timer);
319
445
  }
320
446
  },
321
- activateNativeBrowserApplication: activateNativeBrowserApplicationViaDesktopOperator,
447
+ activateNativeBrowserApplication: activateRuntimeComputerBrowserApplication,
322
448
  };
323
449
 
324
450
  let runtimeHooks: BrowserPluginRuntimeHooks = { ...defaultRuntimeHooks };
@@ -332,6 +458,7 @@ interface ManagedBrowserContextState {
332
458
  }
333
459
 
334
460
  const managedBrowserContexts = new Map<string, Promise<ManagedBrowserContextState>>();
461
+ const managedExtensionPaths = new Map<string, Set<string>>();
335
462
 
336
463
  export function setBrowserPluginRuntimeHooksForTest(hooks: Partial<BrowserPluginRuntimeHooks>): void {
337
464
  runtimeHooks = { ...defaultRuntimeHooks, ...hooks };
@@ -342,6 +469,7 @@ export function resetBrowserPluginRuntimeHooksForTest(): void {
342
469
  runtimeHooks = { ...defaultRuntimeHooks };
343
470
  runtimeHooksCustomized = false;
344
471
  managedBrowserContexts.clear();
472
+ managedExtensionPaths.clear();
345
473
  }
346
474
 
347
475
  function now(): string {
@@ -406,10 +534,6 @@ function stringList(value: unknown): string[] | undefined {
406
534
  return normalized.length > 0 ? Array.from(new Set(normalized)) : undefined;
407
535
  }
408
536
 
409
- function configPath(repoRoot: string): string {
410
- return join(repoRoot, CONFIG_ROOT, 'browser.json');
411
- }
412
-
413
537
  function defaultProfileDir(repoRoot: string): string {
414
538
  return join(browserStateDir(repoRoot, 'profiles'), 'default');
415
539
  }
@@ -449,42 +573,46 @@ function resolveProfileSelection(repoRoot: string, profileDir: string, profileDi
449
573
 
450
574
  function normalizeConfig(raw: PersistedBrowserPluginConfig): BrowserPluginConfig {
451
575
  const normalizedProfileDir = stringValue(raw.profileDir);
576
+ const normalizedExecutablePath = stringValue(raw.executablePath);
577
+ const normalizedBrowserChannel = browserChannel(raw.browserChannel);
452
578
  return {
453
579
  schemaVersion: CURRENT_BROWSER_CONFIG_SCHEMA_VERSION,
454
580
  enabled: raw.enabled === true,
455
581
  provider: 'playwright',
456
- browserMode: browserMode(raw.browserMode) ?? 'attach_preferred',
582
+ browserMode: browserMode(raw.browserMode) ?? 'managed_persistent',
457
583
  profileMode: browserProfileMode(raw.profileMode) ?? (normalizedProfileDir ? 'custom' : 'repo_local'),
458
584
  profileDir: normalizedProfileDir,
459
585
  profileDirectory: stringValue(raw.profileDirectory),
460
- browserChannel: browserChannel(raw.browserChannel) ?? DEFAULT_USER_BROWSER_CHANNEL,
461
- executablePath: stringValue(raw.executablePath),
586
+ browserChannel: normalizedBrowserChannel ?? (normalizedExecutablePath ? undefined : DEFAULT_USER_BROWSER_CHANNEL),
587
+ executablePath: normalizedExecutablePath,
462
588
  cdpEndpoint: stringValue(raw.cdpEndpoint),
463
589
  cdpEndpointCandidates: stringList(raw.cdpEndpointCandidates)?.slice(0, MAX_BROWSER_CDP_ENDPOINT_CANDIDATES),
464
590
  cdpDiscoveryTimeoutMs: typeof raw.cdpDiscoveryTimeoutMs === 'number'
465
591
  ? Math.min(positiveNumber(raw.cdpDiscoveryTimeoutMs, DEFAULT_CDP_DISCOVERY_TIMEOUT_MS), MAX_CDP_DISCOVERY_TIMEOUT_MS)
466
592
  : undefined,
467
593
  cdpAttachFallback: browserCdpAttachFallback(raw.cdpAttachFallback) ?? 'fail_closed',
468
- nativeAttachMode: browserNativeAttachMode(raw.nativeAttachMode) ?? 'auto',
594
+ nativeAttachMode: browserNativeAttachMode(raw.nativeAttachMode) ?? 'disabled',
469
595
  nativeBrowserCandidates: browserProductList(raw.nativeBrowserCandidates) ?? [...DEFAULT_USER_NATIVE_BROWSER_CANDIDATES],
470
596
  defaultTimeoutMs: typeof raw.defaultTimeoutMs === 'number' ? positiveNumber(raw.defaultTimeoutMs, DEFAULT_TIMEOUT_MS) : undefined,
471
597
  };
472
598
  }
473
599
 
474
600
  function migrateLegacyRepositoryConfig(raw: PersistedBrowserPluginConfig): PersistedBrowserPluginConfig {
475
- if (raw.schemaVersion !== 1) return raw;
601
+ if (raw.schemaVersion === CURRENT_BROWSER_CONFIG_SCHEMA_VERSION) return raw;
602
+ const configuredCdp = Boolean(
603
+ stringValue(raw.cdpEndpoint)
604
+ || (stringList(raw.cdpEndpointCandidates)?.length ?? 0) > 0,
605
+ );
606
+ const legacyMode = browserMode(raw.browserMode) ?? 'attach_preferred';
476
607
  return {
608
+ ...raw,
477
609
  schemaVersion: CURRENT_BROWSER_CONFIG_SCHEMA_VERSION,
478
- enabled: raw.enabled,
479
- provider: 'playwright',
480
- browserMode: 'attach_preferred',
481
- profileMode: 'repo_local',
482
- browserChannel: DEFAULT_USER_BROWSER_CHANNEL,
483
- cdpAttachFallback: 'fail_closed',
484
- nativeAttachMode: 'auto',
485
- nativeBrowserCandidates: [...DEFAULT_USER_NATIVE_BROWSER_CANDIDATES],
486
- defaultTimeoutMs: raw.defaultTimeoutMs,
487
- cdpDiscoveryTimeoutMs: raw.cdpDiscoveryTimeoutMs,
610
+ // v1/v2 defaulted to attach_preferred + native auto and silently made
611
+ // Apple Events JavaScript a global Browser dependency whenever no CDP endpoint
612
+ // was configured. Preserve explicit CDP attachment, but retire native attach
613
+ // from migrated defaults; it remains available only when explicitly selected.
614
+ browserMode: legacyMode === 'attach_preferred' && !configuredCdp ? 'managed_persistent' : legacyMode,
615
+ nativeAttachMode: 'disabled',
488
616
  };
489
617
  }
490
618
 
@@ -501,8 +629,10 @@ function writeJson(path: string, value: unknown): void {
501
629
  writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, 'utf-8');
502
630
  }
503
631
 
504
- function loadConfig(repoRoot: string): BrowserPluginConfig {
505
- const persisted = readJson<PersistedBrowserPluginConfig>(configPath(repoRoot));
632
+ function loadConfig(repoRoot: string, context?: Pick<RepositoryPluginConfigContext, 'controllerHome' | 'repoId'>): BrowserPluginConfig {
633
+ const persisted = context
634
+ ? readRepositoryPluginConfig<PersistedBrowserPluginConfig>({ ...context, repoRoot }, BROWSER_PLUGIN_ID)
635
+ : undefined;
506
636
  if (!persisted) return normalizeConfig({});
507
637
  return normalizeConfig(migrateLegacyRepositoryConfig(persisted));
508
638
  }
@@ -511,16 +641,39 @@ async function resolveBrowserPluginAuthorizationContextInternal(
511
641
  input: AssistantPluginActionExecutionInput,
512
642
  ): Promise<AssistantPluginAuthorizationContext | undefined> {
513
643
  const action = browserActions().find((entry) => entry.actionId === input.actionId);
514
- if (!action || action.confirmation !== 'authorization' || !action.scopes.includes('browser.interact')) return undefined;
515
-
516
- const persistedConfig = loadConfig(input.repoRoot);
644
+ // Browser session/profile mutations also need an exact reusable target. In
645
+ // particular, create_session is the bootstrap action for a scheduled
646
+ // Controller round and intentionally carries browser.read + browser.profile
647
+ // rather than browser.interact. Restricting authorization resolution to
648
+ // browser.interact made the first interactive session usable only for that
649
+ // call and left the Scheduler without a durable browser-session grant.
650
+ if (!action || action.confirmation !== 'authorization' || !action.scopes.includes('browser.profile')) return undefined;
651
+
652
+ const persistedConfig = loadConfig(input.repoRoot, input);
517
653
  const sessionId = stringValue(input.args.session_id);
518
654
  const authorizationBrowserMode = parseBrowserModeInput(input.args.browser_mode) ?? persistedConfig.browserMode;
519
655
  if (!sessionId || authorizationBrowserMode === 'isolated') return undefined;
520
656
  const session = findBrowserSession(input.repoRoot, sessionId);
521
657
  const config = effectiveBrowserActionConfig(persistedConfig, input.args, session);
522
658
  const connection = session?.browser;
523
- if (!session || !connection || connection.activeMode === 'isolated') return undefined;
659
+ if (!session || !connection || connection.activeMode === 'isolated') {
660
+ // create_session precedes its durable session record. Recover only the
661
+ // exact browser-origin target already named by this Work's grant refs.
662
+ if (input.actionId !== 'create_session') return undefined;
663
+ let origin: string;
664
+ try { origin = new URL(stringValue(input.args.url) ?? '').origin.toLowerCase(); } catch { return undefined; }
665
+ const refs = new Set((input.authorizationGrantRefs ?? []).map((ref) => ref.trim()).filter(Boolean));
666
+ const target = listCanonicalGrants(input.controllerHome).find((grant) => (
667
+ refs.has(grant.grantId)
668
+ && grant.target?.kind === 'browser-origin'
669
+ && grant.target.id.endsWith(`@${origin}`)
670
+ && grant.scopes?.includes('browser.profile')
671
+ ))?.target;
672
+ return target ? {
673
+ target: { kind: target.kind, id: target.id, identityFingerprint: target.identityFingerprint },
674
+ expiresInMinutes: 30 * 24 * 60,
675
+ } : undefined;
676
+ }
524
677
 
525
678
  let origin: string;
526
679
  try {
@@ -568,15 +721,9 @@ export async function resolveBrowserPluginAuthorizationContext(
568
721
  );
569
722
  }
570
723
 
571
- export function readBrowserPluginConfiguration(repoRoot: string): { enabled: boolean } {
572
- const config = loadConfig(repoRoot);
573
- return { enabled: config.enabled };
574
- }
575
-
576
- function saveConfig(repoRoot: string, patch: Partial<BrowserPluginConfig>): BrowserPluginConfig {
577
- const next = normalizeConfig({ ...loadConfig(repoRoot), ...patch });
578
- writeJson(configPath(repoRoot), next);
579
- return next;
724
+ function saveConfig(input: Pick<AssistantPluginActionExecutionInput, 'controllerHome' | 'repoId' | 'repoRoot'>, patch: Partial<BrowserPluginConfig>): BrowserPluginConfig {
725
+ const next = normalizeConfig({ ...loadConfig(input.repoRoot, input), ...patch });
726
+ return writeRepositoryPluginConfig(input, BROWSER_PLUGIN_ID, next);
580
727
  }
581
728
 
582
729
  function parsedAbsoluteUrl(value: unknown): { raw: string; parsed: URL } {
@@ -1054,7 +1201,7 @@ async function establishAuthoritativeNativeForeground(
1054
1201
  const cause = error instanceof Error ? error.message : String(error);
1055
1202
  throw new AssistantPluginError(
1056
1203
  'PLUGIN_BROWSER_ACTIVATION_FAILED',
1057
- 'Native browser exact-tab activation could not establish macOS system foreground authority through Forge Desktop Operator.',
1204
+ 'Native browser exact-tab activation could not establish system foreground authority through the configured Computer provider.',
1058
1205
  { retryable: true, details: { sessionId: target.sessionId, browserProduct: product, ...state, ...(causeCode ? { causeCode } : {}), cause: cause.slice(0, 500) } },
1059
1206
  );
1060
1207
  }
@@ -1083,7 +1230,7 @@ interface BrowserOpenHandle {
1083
1230
 
1084
1231
  interface PageDiagnostics {
1085
1232
  consoleErrors: Array<{ type: string; text: string }>;
1086
- failedRequests: Array<{ url: string; status?: number; failure?: string }>;
1233
+ failedRequests: Array<{ url: string; method?: string; status?: number; failure?: string }>;
1087
1234
  navigation?: { url: string; status?: number };
1088
1235
  }
1089
1236
 
@@ -1101,18 +1248,21 @@ function attachDiagnostics(page: PageLike): PageDiagnostics {
1101
1248
  }
1102
1249
  });
1103
1250
  page.on('requestfailed', (request) => {
1104
- const entry = request as { url?: () => string; failure?: () => { errorText?: string } | null };
1251
+ const entry = request as { url?: () => string; method?: () => string; failure?: () => { errorText?: string } | null };
1105
1252
  diagnostics.failedRequests.push({
1106
1253
  url: typeof entry.url === 'function' ? entry.url() : '',
1254
+ method: typeof entry.method === 'function' ? entry.method() : undefined,
1107
1255
  failure: typeof entry.failure === 'function' ? entry.failure()?.errorText : undefined,
1108
1256
  });
1109
1257
  });
1110
1258
  page.on('response', (response) => {
1111
- const entry = response as { url?: () => string; status?: () => number; ok?: () => boolean };
1259
+ const entry = response as { url?: () => string; status?: () => number; ok?: () => boolean; request?: () => { method?: () => string } };
1112
1260
  const status = typeof entry.status === 'function' ? entry.status() : undefined;
1113
1261
  if (typeof status === 'number' && status >= 400) {
1262
+ const request = typeof entry.request === 'function' ? entry.request() : undefined;
1114
1263
  diagnostics.failedRequests.push({
1115
1264
  url: typeof entry.url === 'function' ? entry.url() : '',
1265
+ method: typeof request?.method === 'function' ? request.method() : undefined,
1116
1266
  status,
1117
1267
  });
1118
1268
  }
@@ -1571,14 +1721,71 @@ async function discoverCdpEndpoint(endpoint: string, timeoutMs: number): Promise
1571
1721
  };
1572
1722
  }
1573
1723
 
1724
+ async function waitForCdpEndpoint(endpoint: string, timeoutMs: number): Promise<CdpAttachAttempt> {
1725
+ const deadline = Date.now() + Math.max(250, timeoutMs);
1726
+ let lastError: unknown;
1727
+ while (Date.now() < deadline) {
1728
+ try {
1729
+ return await discoverCdpEndpoint(endpoint, Math.max(100, Math.min(500, deadline - Date.now())));
1730
+ } catch (error) {
1731
+ lastError = error;
1732
+ await delay(50);
1733
+ }
1734
+ }
1735
+ throw new AssistantPluginError('PLUGIN_BROWSER_EXTENSION_CONTROL_UNAVAILABLE', 'Vivaldi did not expose the allocated loopback CDP endpoint after launch.', {
1736
+ retryable: true,
1737
+ details: { endpoint, error: lastError instanceof Error ? lastError.message : String(lastError ?? 'unknown') },
1738
+ });
1739
+ }
1740
+
1741
+ async function browserCdpExtensionTarget(session: BrowserCdpSessionLike, extensionId: string): Promise<string | undefined> {
1742
+ const response = await session.send('Target.getTargets') as { targetInfos?: Array<{ url?: unknown }> };
1743
+ const prefix = `chrome-extension://${extensionId}/`;
1744
+ return (response.targetInfos ?? [])
1745
+ .map((target) => typeof target.url === 'string' ? target.url : '')
1746
+ .find((url) => url.startsWith(prefix));
1747
+ }
1748
+
1749
+ async function waitForBrowserCdpExtensionTarget(session: BrowserCdpSessionLike, extensionId: string, timeoutMs: number): Promise<string> {
1750
+ const deadline = Date.now() + Math.min(Math.max(timeoutMs, 250), 10_000);
1751
+ while (Date.now() <= deadline) {
1752
+ const target = await browserCdpExtensionTarget(session, extensionId);
1753
+ if (target) return target;
1754
+ await delay(100);
1755
+ }
1756
+ throw new AssistantPluginError('PLUGIN_BROWSER_EXTENSION_POSTCONDITION_FAILED', 'Browser-level CDP did not expose an exact runtime target for the requested extension id.', {
1757
+ retryable: true,
1758
+ details: { extensionId },
1759
+ });
1760
+ }
1761
+
1574
1762
  function launchOptionsForRepo(repoRoot: string, config: BrowserPluginConfig, profile: BrowserProfileSelection): Record<string, unknown> {
1763
+ const extensionPaths = [...(managedExtensionPaths.get(managedContextKey(profile)) ?? [])].sort();
1764
+ const args = [
1765
+ ...(profile.profileDirectory ? [`--profile-directory=${profile.profileDirectory}`] : []),
1766
+ ...(extensionPaths.length > 0 ? ['--enable-unsafe-extension-debugging'] : []),
1767
+ ];
1768
+ const customProfileDefaultArgsToPreserve = [
1769
+ '--disable-extensions',
1770
+ '--disable-component-extensions-with-background-pages',
1771
+ '--disable-default-apps',
1772
+ '--disable-sync',
1773
+ '--use-mock-keychain',
1774
+ '--password-store=basic',
1775
+ ];
1776
+ const ignoreDefaultArgs = config.profileMode === 'custom'
1777
+ ? customProfileDefaultArgsToPreserve
1778
+ : extensionPaths.length > 0
1779
+ ? ['--disable-extensions']
1780
+ : [];
1575
1781
  return {
1576
1782
  headless: false,
1577
1783
  acceptDownloads: true,
1578
1784
  viewport: { width: 1280, height: 900 },
1579
1785
  ...(config.executablePath ? { executablePath: resolveConfiguredPath(repoRoot, config.executablePath) } : {}),
1580
- ...(!config.executablePath && config.browserChannel && config.browserChannel !== 'chromium' ? { channel: config.browserChannel } : {}),
1581
- ...(profile.profileDirectory ? { args: [`--profile-directory=${profile.profileDirectory}`] } : {}),
1786
+ ...(!config.executablePath && extensionPaths.length === 0 && config.browserChannel && config.browserChannel !== 'chromium' ? { channel: config.browserChannel } : {}),
1787
+ ...(ignoreDefaultArgs.length > 0 ? { ignoreDefaultArgs } : {}),
1788
+ ...(args.length > 0 ? { args } : {}),
1582
1789
  };
1583
1790
  }
1584
1791
 
@@ -1598,8 +1805,87 @@ async function evictManagedContext(key: string): Promise<void> {
1598
1805
  }
1599
1806
  }
1600
1807
 
1808
+ /**
1809
+ * Forge-owned isolated/handoff browser profiles are per-session and cannot be
1810
+ * shared, so a browser still running on one of those profiles while no managed
1811
+ * context in this Runtime incarnation owns it is an orphan: the session that
1812
+ * launched it is gone. Left alone it stays visible as a logged-out browser the
1813
+ * user never opened and keeps a stale profile locked.
1814
+ *
1815
+ * Reclamation is mechanical and bounded: only processes whose own command line
1816
+ * names a Forge-owned profile path are considered, browser child processes
1817
+ * (`--type=`) are ignored so the main process is signalled once, and a profile
1818
+ * with a live managed context is never touched.
1819
+ */
1820
+ export function reclaimOrphanedIsolatedBrowserProfiles(input: { profileRoots: readonly string[]; graceMs?: number }): { terminated: number; details: string[] } {
1821
+ const roots = input.profileRoots.map((root) => resolve(root)).filter(Boolean);
1822
+ if (roots.length === 0) return { terminated: 0, details: [] };
1823
+ const owned = (profileDir: string): boolean => {
1824
+ const normalized = resolve(profileDir);
1825
+ const isForgeProfile = /\/browser\/profiles\/(?:isolated|handoff)\//.test(normalized);
1826
+ if (!isForgeProfile) return false;
1827
+ return roots.some((root) => normalized === root || normalized.startsWith(`${root}${sep}`));
1828
+ };
1829
+ const liveOwners = new Set([...managedBrowserContexts.keys()].map((key) => resolve(key)));
1830
+ const graceMs = Math.max(0, Math.min(30_000, input.graceMs ?? 2_000));
1831
+ const targets: Array<{ pid: number; profileDir: string }> = [];
1832
+ for (const entry of runtimeHooks.listProcesses()) {
1833
+ if (entry.command.includes('--type=')) continue;
1834
+ const match = /--user-data-dir=(?:"([^"]+)"|'([^']+)'|(\S+))/.exec(entry.command);
1835
+ const profileDir = match?.[1] ?? match?.[2] ?? match?.[3];
1836
+ if (!profileDir || !owned(profileDir)) continue;
1837
+ if (liveOwners.has(resolve(profileDir))) continue;
1838
+ targets.push({ pid: entry.pid, profileDir: resolve(profileDir) });
1839
+ }
1840
+ const details: string[] = [];
1841
+ let terminated = 0;
1842
+ for (const target of targets) {
1843
+ runtimeHooks.signalProcess(target.pid, 'SIGTERM');
1844
+ details.push(`SIGTERM ${target.pid} ${target.profileDir}`);
1845
+ terminated += 1;
1846
+ }
1847
+ if (graceMs > 0 && targets.length > 0) {
1848
+ const deadline = Date.now() + graceMs;
1849
+ while (Date.now() < deadline) {
1850
+ const remaining = new Set(targets
1851
+ .filter((target) => runtimeHooks.listProcesses().some((entry) => entry.pid === target.pid && !entry.command.includes('--type=')))
1852
+ .map((target) => target.pid));
1853
+ if (remaining.size === 0) break;
1854
+ sleepSyncMs(50);
1855
+ }
1856
+ for (const target of targets) {
1857
+ const alive = runtimeHooks.listProcesses().some((entry) => entry.pid === target.pid && !entry.command.includes('--type='));
1858
+ if (!alive) continue;
1859
+ runtimeHooks.signalProcess(target.pid, 'SIGKILL');
1860
+ details.push(`SIGKILL ${target.pid} ${target.profileDir}`);
1861
+ }
1862
+ }
1863
+ return { terminated, details };
1864
+ }
1865
+
1866
+ function sleepSyncMs(ms: number): void {
1867
+ try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.max(0, Math.min(1_000, ms))); }
1868
+ catch { /* best-effort bounded wait */ }
1869
+ }
1870
+
1871
+ export function browserProfileRootsForRuntime(input: { controllerHome: string; repoRoots?: readonly string[] }): string[] {
1872
+ const roots = new Set<string>();
1873
+ const home = input.controllerHome.trim();
1874
+ if (home) roots.add(join(resolve(home), 'repositories'));
1875
+ for (const repoRoot of input.repoRoots ?? []) {
1876
+ const trimmed = repoRoot.trim();
1877
+ if (trimmed) roots.add(join(resolve(trimmed), '.forge'));
1878
+ }
1879
+ return [...roots];
1880
+ }
1881
+
1601
1882
  async function closeManagedContextsForRepo(repoRoot: string, options: { strict?: boolean } = {}): Promise<void> {
1602
1883
  const canonicalRoot = resolve(repoRoot);
1884
+ // Explicit session cleanup must also retire a browser whose session record is
1885
+ // gone; otherwise the profile stays locked by a process nothing owns.
1886
+ reclaimOrphanedIsolatedBrowserProfiles({
1887
+ profileRoots: browserProfileRootsForRuntime({ controllerHome: resolveRepoPreferredControllerHome(canonicalRoot), repoRoots: [canonicalRoot] }),
1888
+ });
1603
1889
  for (const [key, pending] of [...managedBrowserContexts.entries()]) {
1604
1890
  let state: ManagedBrowserContextState;
1605
1891
  try {
@@ -1911,6 +2197,342 @@ async function openManagedContext(
1911
2197
  };
1912
2198
  }
1913
2199
 
2200
+ const FORGE_NATIVE_MESSAGING_DECLARATION = 'forge-native-messaging-host.json';
2201
+ interface ManagedNativeMessagingHost {
2202
+ name: string;
2203
+ manifest: string;
2204
+ }
2205
+ interface BrowserExtensionInfo {
2206
+ id: string;
2207
+ name?: string;
2208
+ version?: string;
2209
+ path: string;
2210
+ enabled: boolean;
2211
+ }
2212
+
2213
+ function pathWithin(root: string, candidate: string): boolean {
2214
+ const rel = relative(root, candidate);
2215
+ return rel === '' || (!rel.startsWith(`..${sep}`) && rel !== '..' && !isAbsolute(rel));
2216
+ }
2217
+
2218
+ function unpackedExtensionPath(input: AssistantPluginActionExecutionInput): { path: string; expectedId?: string; nativeHost?: ManagedNativeMessagingHost } {
2219
+ const requested = requiredString(input.args.extension_path, 'extension_path');
2220
+ if (!isAbsolute(requested)) throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'extension_path must be absolute.', { retryable: false });
2221
+ if (!existsSync(requested) || !statSync(requested).isDirectory()) {
2222
+ throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'extension_path must reference an existing directory.', { retryable: false });
2223
+ }
2224
+ const realPath = realpathSync(requested);
2225
+ const trustedRoots = [realpathSync(input.repoRoot)];
2226
+ const controllerBrowserRoot = join(resolve(input.controllerHome), 'supervisor', 'browser-adapter');
2227
+ if (existsSync(controllerBrowserRoot)) trustedRoots.push(realpathSync(controllerBrowserRoot));
2228
+ if (!trustedRoots.some((root) => pathWithin(root, realPath))) {
2229
+ throw new AssistantPluginError('PLUGIN_POLICY_BLOCKED', 'Unpacked extensions must resolve inside the repository or Controller-owned browser-adapter root.', { retryable: false });
2230
+ }
2231
+ const manifestPath = join(realPath, 'manifest.json');
2232
+ if (!existsSync(manifestPath) || !statSync(manifestPath).isFile() || !pathWithin(realPath, realpathSync(manifestPath))) {
2233
+ throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Unpacked extension requires a regular manifest.json inside the extension directory.', { retryable: false });
2234
+ }
2235
+ let manifest: { key?: unknown };
2236
+ try { manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as { key?: unknown }; }
2237
+ catch { throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Unpacked extension manifest.json must contain valid JSON.', { retryable: false }); }
2238
+ let expectedId: string | undefined;
2239
+ if (typeof manifest.key === 'string' && /^[A-Za-z0-9+/]+={0,2}$/.test(manifest.key.trim())) {
2240
+ const bytes = Buffer.from(manifest.key.trim(), 'base64');
2241
+ if (bytes.length > 0) expectedId = createHash('sha256').update(bytes).digest('hex').slice(0, 32)
2242
+ .replace(/[0-9a-f]/g, (nibble) => String.fromCharCode(97 + Number.parseInt(nibble, 16)));
2243
+ }
2244
+ const declarationPath = join(realPath, FORGE_NATIVE_MESSAGING_DECLARATION);
2245
+ let nativeHost: ManagedNativeMessagingHost | undefined;
2246
+ if (existsSync(declarationPath)) {
2247
+ if (!expectedId || !statSync(declarationPath).isFile() || !pathWithin(realPath, realpathSync(declarationPath))) {
2248
+ throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Managed native messaging declaration requires a regular file and stable extension manifest key.', { retryable: false });
2249
+ }
2250
+ let declaration: { name?: unknown; description?: unknown; path?: unknown; type?: unknown; allowed_origins?: unknown };
2251
+ try { declaration = JSON.parse(readFileSync(declarationPath, 'utf8')) as typeof declaration; }
2252
+ catch { throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'forge-native-messaging-host.json must contain valid JSON.', { retryable: false }); }
2253
+ const name = typeof declaration.name === 'string' ? declaration.name.trim() : '';
2254
+ const hostPath = typeof declaration.path === 'string' ? declaration.path.trim() : '';
2255
+ const expectedOrigin = `chrome-extension://${expectedId}/`;
2256
+ if (!/^[a-z0-9_]+(?:\.[a-z0-9_]+)*$/.test(name)
2257
+ || declaration.type !== 'stdio'
2258
+ || !isAbsolute(hostPath)
2259
+ || !Array.isArray(declaration.allowed_origins)
2260
+ || declaration.allowed_origins.length !== 1
2261
+ || declaration.allowed_origins[0] !== expectedOrigin) {
2262
+ throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Managed native messaging declaration must bind one valid host name, stdio transport, absolute executable, and the exact stable extension origin.', { retryable: false });
2263
+ }
2264
+ if (!existsSync(hostPath) || !statSync(hostPath).isFile()) {
2265
+ throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Managed native messaging host executable must exist as a regular file.', { retryable: false });
2266
+ }
2267
+ const realHostPath = realpathSync(hostPath);
2268
+ if (!trustedRoots.some((root) => pathWithin(root, realHostPath))) {
2269
+ throw new AssistantPluginError('PLUGIN_POLICY_BLOCKED', 'Managed native messaging host executable must resolve inside an already trusted extension, repository, or Controller browser-adapter root.', { retryable: false });
2270
+ }
2271
+ if (process.platform !== 'win32' && (statSync(realHostPath).mode & 0o111) === 0) {
2272
+ throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Managed native messaging host must be executable.', { retryable: false });
2273
+ }
2274
+ nativeHost = {
2275
+ name,
2276
+ manifest: `${JSON.stringify({
2277
+ name,
2278
+ description: typeof declaration.description === 'string' ? declaration.description.slice(0, 512) : '',
2279
+ path: realHostPath,
2280
+ type: 'stdio',
2281
+ allowed_origins: [expectedOrigin],
2282
+ }, null, 2)}\n`,
2283
+ };
2284
+ }
2285
+ return { path: realPath, expectedId, nativeHost };
2286
+ }
2287
+
2288
+ function projectManagedNativeMessagingHost(profileDir: string, host: ManagedNativeMessagingHost | undefined): boolean {
2289
+ if (!host) return false;
2290
+ const root = join(profileDir, 'NativeMessagingHosts');
2291
+ mkdirSync(root, { recursive: true, mode: 0o700 });
2292
+ const target = join(root, `${host.name}.json`);
2293
+ if (existsSync(target) && readFileSync(target, 'utf8') === host.manifest) return false;
2294
+ const temporary = `${target}.${randomUUID().slice(0, 12)}.tmp`;
2295
+ writeFileSync(temporary, host.manifest, { mode: 0o600 });
2296
+ renameSync(temporary, target);
2297
+ return true;
2298
+ }
2299
+
2300
+ function extensionTargets(context: BrowserContextLike): string[] {
2301
+ return [...(context.serviceWorkers?.() ?? []), ...(context.backgroundPages?.() ?? [])].map((target) => target.url());
2302
+ }
2303
+
2304
+ async function waitForManagedExtension(context: BrowserContextLike, extensionId: string, timeoutMs: number): Promise<string> {
2305
+ const prefix = `chrome-extension://${extensionId}/`;
2306
+ const deadline = Date.now() + Math.min(Math.max(timeoutMs, 250), 10_000);
2307
+ while (Date.now() <= deadline) {
2308
+ const url = extensionTargets(context).find((candidate) => candidate.startsWith(prefix));
2309
+ if (url) return url;
2310
+ await delay(100);
2311
+ }
2312
+ throw new AssistantPluginError('PLUGIN_BROWSER_EXTENSION_POSTCONDITION_FAILED', 'Managed browser did not expose an exact runtime target for the requested extension id.', { retryable: true, details: { extensionId } });
2313
+ }
2314
+
2315
+ async function browserLevelCdp(
2316
+ repoRoot: string,
2317
+ config: BrowserPluginConfig,
2318
+ ): Promise<{ browser: BrowserLike; session: BrowserCdpSessionLike; endpoint: string } | undefined> {
2319
+ if (cdpEndpoints(config).length === 0 || !runtimeHooks.moduleAvailable('playwright', repoRoot)) return undefined;
2320
+ const runtime = runtimeHooks.loadPlaywright(repoRoot);
2321
+ const connectOverCDP = runtime.chromium.connectOverCDP;
2322
+ if (typeof connectOverCDP !== 'function') return undefined;
2323
+ for (const endpoint of cdpEndpoints(config)) {
2324
+ try {
2325
+ const discovered = await discoverCdpEndpoint(endpoint, cdpDiscoveryTimeout(config));
2326
+ const browser = await connectOverCDP.call(runtime.chromium, discovered.discoveredEndpoint ?? endpoint);
2327
+ if (typeof browser.newBrowserCDPSession !== 'function') {
2328
+ if (browser.disconnect) await Promise.resolve(browser.disconnect()).catch(() => undefined);
2329
+ else if (browser.close) await browser.close().catch(() => undefined);
2330
+ continue;
2331
+ }
2332
+ const session = await browser.newBrowserCDPSession();
2333
+ return { browser, session, endpoint: discovered.discoveredEndpoint ?? endpoint };
2334
+ } catch { /* Pre-dispatch endpoint discovery/attach failure; try the next configured endpoint. */ }
2335
+ }
2336
+ return undefined;
2337
+ }
2338
+
2339
+ async function closeBrowserLevelCdp(handle: { browser: BrowserLike; session: BrowserCdpSessionLike }): Promise<void> {
2340
+ await handle.session.detach?.().catch(() => undefined);
2341
+ if (handle.browser.disconnect) await Promise.resolve(handle.browser.disconnect()).catch(() => undefined);
2342
+ else if (handle.browser.close) await handle.browser.close().catch(() => undefined);
2343
+ }
2344
+
2345
+ async function cdpExtensionList(repoRoot: string, config: BrowserPluginConfig): Promise<{ provider: string; endpoint: string; extensions: BrowserExtensionInfo[] } | undefined> {
2346
+ const handle = await browserLevelCdp(repoRoot, config);
2347
+ if (!handle) return undefined;
2348
+ try {
2349
+ const result = await handle.session.send('Extensions.getExtensions') as { extensions?: BrowserExtensionInfo[] };
2350
+ return { provider: 'playwright-cdp', endpoint: handle.endpoint, extensions: Array.isArray(result.extensions) ? result.extensions : [] };
2351
+ } finally { await closeBrowserLevelCdp(handle); }
2352
+ }
2353
+
2354
+ async function loadAndVerifyUnpackedExtension(
2355
+ session: BrowserCdpSessionLike,
2356
+ extensionPath: string,
2357
+ expectedId: string | undefined,
2358
+ enableInIncognito: boolean,
2359
+ ): Promise<BrowserExtensionInfo> {
2360
+ const loaded = await session.send('Extensions.loadUnpacked', {
2361
+ path: extensionPath,
2362
+ ...(enableInIncognito ? { enableInIncognito: true } : {}),
2363
+ }) as { id?: string };
2364
+ const id = typeof loaded.id === 'string' && loaded.id.trim() ? loaded.id.trim() : undefined;
2365
+ if (!id) throw new AssistantPluginError('PLUGIN_BROWSER_EXTENSION_POSTCONDITION_FAILED', 'Browser did not return an extension id after loadUnpacked.', { retryable: true });
2366
+ if (expectedId && id !== expectedId) {
2367
+ throw new AssistantPluginError('PLUGIN_BROWSER_EXTENSION_POSTCONDITION_FAILED', 'Browser returned an unpacked extension id that does not match the manifest-derived stable id.', {
2368
+ retryable: false,
2369
+ details: { extensionId: id, expectedExtensionId: expectedId },
2370
+ });
2371
+ }
2372
+ const listed = await session.send('Extensions.getExtensions') as { extensions?: BrowserExtensionInfo[] };
2373
+ const exact = (listed.extensions ?? []).find((entry) => {
2374
+ if (entry.id !== id || entry.enabled !== true) return false;
2375
+ try { return realpathSync(entry.path) === extensionPath; } catch { return false; }
2376
+ });
2377
+ if (!exact) throw new AssistantPluginError('PLUGIN_BROWSER_EXTENSION_POSTCONDITION_FAILED', 'Browser did not verify the exact enabled unpacked extension id/path after loadUnpacked.', { retryable: true, details: { extensionId: id } });
2378
+ return exact;
2379
+ }
2380
+
2381
+ function customVivaldiExtensionInstall(config: BrowserPluginConfig, repoRoot: string): { executable: string } | undefined {
2382
+ if (config.profileMode !== 'custom' || !config.executablePath) return undefined;
2383
+ const executable = resolveConfiguredPath(repoRoot, config.executablePath);
2384
+ return basename(executable).toLowerCase() === 'vivaldi' ? { executable } : undefined;
2385
+ }
2386
+
2387
+ function processUsesUserDataDir(command: string, profileDir: string): boolean {
2388
+ const expected = resolve(profileDir);
2389
+ const forms = [
2390
+ `--user-data-dir=${expected}`,
2391
+ `--user-data-dir="${expected}"`,
2392
+ `--user-data-dir='${expected}'`,
2393
+ ];
2394
+ return forms.some((form) => {
2395
+ const index = command.indexOf(form);
2396
+ if (index < 0) return false;
2397
+ const next = command[index + form.length];
2398
+ return next === undefined || /\s/.test(next);
2399
+ });
2400
+ }
2401
+
2402
+ function exactCustomVivaldiProfileOwner(
2403
+ executable: string,
2404
+ profile: BrowserProfileSelection,
2405
+ ): { pid: number; command: string } | undefined {
2406
+ let lockTarget: string;
2407
+ try { lockTarget = readlinkSync(join(profile.profileDir, 'SingletonLock')); }
2408
+ catch { return undefined; }
2409
+ const pidText = /(?:^|-)(\d+)$/.exec(lockTarget.trim())?.[1];
2410
+ const pid = pidText ? Number(pidText) : 0;
2411
+ if (!Number.isInteger(pid) || pid <= 0) return undefined;
2412
+ const process = runtimeHooks.listProcesses().find((entry) => entry.pid === pid && !entry.command.includes('--type='));
2413
+ if (!process || !process.command.includes(executable)) return undefined;
2414
+ const hasExplicitProfileDir = process.command.includes('--user-data-dir=');
2415
+ if (hasExplicitProfileDir) {
2416
+ if (!processUsesUserDataDir(process.command, profile.profileDir)) return undefined;
2417
+ } else {
2418
+ const defaultVivaldiProfileDir = resolve(join(homedir(), 'Library', 'Application Support', 'Vivaldi'));
2419
+ if (resolve(profile.profileDir) !== defaultVivaldiProfileDir) return undefined;
2420
+ }
2421
+ return process;
2422
+ }
2423
+
2424
+ async function gracefullyRestartExactCustomVivaldiOwner(
2425
+ executable: string,
2426
+ profile: BrowserProfileSelection,
2427
+ timeoutMs: number,
2428
+ ): Promise<{ restarted: boolean; pid?: number }> {
2429
+ const owner = exactCustomVivaldiProfileOwner(executable, profile);
2430
+ if (!owner) return { restarted: false };
2431
+ runtimeHooks.signalProcess(owner.pid, 'SIGTERM');
2432
+ const deadline = Date.now() + Math.max(500, Math.min(timeoutMs, 10_000));
2433
+ while (Date.now() < deadline) {
2434
+ const alive = runtimeHooks.listProcesses().some((entry) => entry.pid === owner.pid && !entry.command.includes('--type='));
2435
+ if (!alive) return { restarted: true, pid: owner.pid };
2436
+ await delay(50);
2437
+ }
2438
+ throw new AssistantPluginError('PLUGIN_BROWSER_PROFILE_IN_USE', 'The exact Vivaldi custom-profile owner did not exit after a bounded graceful restart request.', {
2439
+ retryable: true,
2440
+ details: { pid: owner.pid, profileDir: profile.profileDir },
2441
+ });
2442
+ }
2443
+
2444
+ async function installExtensionThroughCustomVivaldiCdpPort(
2445
+ input: AssistantPluginActionExecutionInput,
2446
+ config: BrowserPluginConfig,
2447
+ profile: BrowserProfileSelection,
2448
+ extensionPath: string,
2449
+ expectedId: string,
2450
+ ): Promise<Record<string, unknown>> {
2451
+ const vivaldi = customVivaldiExtensionInstall(config, input.repoRoot);
2452
+ if (!vivaldi) {
2453
+ throw new AssistantPluginError('PLUGIN_BROWSER_EXTENSION_CONTROL_UNAVAILABLE', 'Custom Vivaldi unpacked-extension install requires the configured Vivaldi executable.', { retryable: true });
2454
+ }
2455
+ const timeoutMs = positiveNumber(input.args.timeout_ms, config.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS);
2456
+ await gracefullyRestartExactCustomVivaldiOwner(vivaldi.executable, profile, timeoutMs);
2457
+ const port = await runtimeHooks.allocateLoopbackPort();
2458
+ const endpoint = `http://127.0.0.1:${port}`;
2459
+ const args = [
2460
+ `--user-data-dir=${profile.profileDir}`,
2461
+ ...(profile.profileDirectory ? [`--profile-directory=${profile.profileDirectory}`] : []),
2462
+ '--remote-debugging-address=127.0.0.1',
2463
+ `--remote-debugging-port=${port}`,
2464
+ '--enable-unsafe-extension-debugging',
2465
+ '--no-first-run',
2466
+ '--no-default-browser-check',
2467
+ ];
2468
+ const launched = runtimeHooks.launchProcess(vivaldi.executable, args);
2469
+ const pid = launched.pid;
2470
+ if (!pid || !Number.isInteger(pid) || pid <= 0) {
2471
+ throw new AssistantPluginError('PLUGIN_BROWSER_EXTENSION_CONTROL_UNAVAILABLE', 'Vivaldi launcher did not return a process identity for bounded cleanup.', { retryable: true });
2472
+ }
2473
+ let session: BrowserCdpSessionLike | undefined;
2474
+ let keepBrowser = false;
2475
+ try {
2476
+ const discovered = await waitForCdpEndpoint(endpoint, timeoutMs);
2477
+ const browserEndpoint = discovered.discoveredEndpoint ?? endpoint;
2478
+ session = await runtimeHooks.connectBrowserCdp(browserEndpoint, Math.max(1_000, Math.min(timeoutMs, 30_000)));
2479
+ const listed = await session.send('Extensions.getExtensions') as { extensions?: BrowserExtensionInfo[] };
2480
+ let verifiedExtension = (listed.extensions ?? []).find((entry) => {
2481
+ if (entry.id !== expectedId || entry.enabled !== true) return false;
2482
+ try { return realpathSync(entry.path) === extensionPath; } catch { return false; }
2483
+ });
2484
+ let runtimeTarget = await browserCdpExtensionTarget(session, expectedId);
2485
+ if (!verifiedExtension || !runtimeTarget) {
2486
+ verifiedExtension = await loadAndVerifyUnpackedExtension(session, extensionPath, expectedId, input.args.enable_in_incognito === true);
2487
+ if (!runtimeTarget) runtimeTarget = await waitForBrowserCdpExtensionTarget(session, expectedId, timeoutMs);
2488
+ }
2489
+ keepBrowser = true;
2490
+ return {
2491
+ provider: 'native-cdp-vivaldi-custom',
2492
+ endpoint,
2493
+ extension: { ...verifiedExtension, runtimeTarget },
2494
+ verified: true,
2495
+ };
2496
+ } finally {
2497
+ await session?.detach?.().catch(() => undefined);
2498
+ if (!keepBrowser) runtimeHooks.signalProcess(pid, 'SIGTERM');
2499
+ }
2500
+ }
2501
+
2502
+ async function managedCdpInstallExtension(
2503
+ input: AssistantPluginActionExecutionInput,
2504
+ context: BrowserContextLike,
2505
+ extensionPath: string,
2506
+ expectedId: string,
2507
+ runtimeTargetPresent: boolean,
2508
+ ): Promise<BrowserExtensionInfo> {
2509
+ const browser = context.browser?.();
2510
+ if (!browser || typeof browser.newBrowserCDPSession !== 'function') {
2511
+ throw new AssistantPluginError('PLUGIN_BROWSER_EXTENSION_CONTROL_UNAVAILABLE', 'Managed unpacked-extension install requires browser-level CDP on the launched Chromium context.', { retryable: true });
2512
+ }
2513
+ const session = await browser.newBrowserCDPSession();
2514
+ try {
2515
+ const listed = await session.send('Extensions.getExtensions') as { extensions?: BrowserExtensionInfo[] };
2516
+ const exact = (listed.extensions ?? []).find((entry) => {
2517
+ if (entry.id !== expectedId || entry.enabled !== true) return false;
2518
+ try { return realpathSync(entry.path) === extensionPath; } catch { return false; }
2519
+ });
2520
+ if (exact && runtimeTargetPresent) return exact;
2521
+ return await loadAndVerifyUnpackedExtension(session, extensionPath, expectedId, input.args.enable_in_incognito === true);
2522
+ } finally {
2523
+ await session.detach?.().catch(() => undefined);
2524
+ }
2525
+ }
2526
+
2527
+ async function cdpInstallExtension(input: AssistantPluginActionExecutionInput, config: BrowserPluginConfig, extensionPath: string, expectedId?: string): Promise<Record<string, unknown> | undefined> {
2528
+ const handle = await browserLevelCdp(input.repoRoot, config);
2529
+ if (!handle) return undefined;
2530
+ try {
2531
+ const exact = await loadAndVerifyUnpackedExtension(handle.session, extensionPath, expectedId, input.args.enable_in_incognito === true);
2532
+ return { provider: 'playwright-cdp', endpoint: handle.endpoint, extension: exact, verified: true };
2533
+ } finally { await closeBrowserLevelCdp(handle); }
2534
+ }
2535
+
1914
2536
  async function openAttachedContext(
1915
2537
  runtime: PlaywrightRuntime,
1916
2538
  repoRoot: string,
@@ -2127,14 +2749,37 @@ async function openNativeAttachedContext(
2127
2749
  : { sessionId: target.sessionId, status: 'no_saved_tab', reason: 'Created one plugin-owned tab while preserving the user active tab.' };
2128
2750
  }
2129
2751
 
2752
+ let createdThisCallRef: MacOsBrowserTabRef | undefined;
2130
2753
  if (!discovered?.attachment) {
2131
2754
  const candidates = savedProduct ? [savedProduct] : (config.nativeBrowserCandidates ?? [...DEFAULT_USER_NATIVE_BROWSER_CANDIDATES]);
2132
2755
  discovered = await discoverMacOsBrowserAttachment(candidates, Math.min(timeout, MAX_CDP_DISCOVERY_TIMEOUT_MS));
2756
+ const zeroWindowAttempt = args.__forge_require_existing_resource !== true
2757
+ ? discovered.attempts.find((attempt) => attempt.status === 'unavailable' && attempt.error?.includes('FORGE_NO_BROWSER_WINDOW'))
2758
+ : undefined;
2759
+ if (!discovered.attachment && zeroWindowAttempt) {
2760
+ const created = await createMacOsBrowserOwnedPageForProduct(
2761
+ zeroWindowAttempt.product,
2762
+ target.url,
2763
+ discovered.attempts,
2764
+ undefined,
2765
+ timeout,
2766
+ );
2767
+ page = created.page as unknown as PageLike;
2768
+ discovered = { attachment: created.attachment, attempts: created.attachment.attempts };
2769
+ createdThisCallRef = created.page.tabRef();
2770
+ matchedBy = 'owned_token';
2771
+ effectiveOwnership = 'plugin_owned';
2772
+ sessionResume = {
2773
+ sessionId: target.sessionId,
2774
+ status: 'stale_tab',
2775
+ reason: 'Browser process had no window; created the first Forge-owned tab through the existing create_tab Computer capability.',
2776
+ savedTab,
2777
+ };
2778
+ }
2133
2779
  }
2134
2780
  if (!discovered.attachment) return { attempts: discovered.attempts };
2135
2781
  let activeAttachment = discovered.attachment;
2136
2782
 
2137
- let createdThisCallRef: MacOsBrowserTabRef | undefined;
2138
2783
  let navigationUsedReplacement = false;
2139
2784
  const navigateNativePageWithReplacement = async (candidate: PageLike): Promise<PageLike> => {
2140
2785
  const obsoleteRef = (candidate as unknown as { tabRef?: () => MacOsBrowserTabRef | undefined }).tabRef?.();
@@ -2486,11 +3131,13 @@ async function withPage<T>(
2486
3131
  let lastError: unknown;
2487
3132
  for (let attempt = 1; attempt <= retries; attempt += 1) {
2488
3133
  let handle: BrowserOpenHandle | undefined;
2489
- // Opening a browser resource may itself create or navigate a tab before a
2490
- // handle is returned. Replay is safe only for an observation action that is
2491
- // fenced to an already-existing resource. Anything else is potentially
2492
- // externally mutating from the outset.
2493
- let actionDispatched = !postDispatchReplaySafe;
3134
+ // A provider/readiness failure before openBrowser returns a handle has not
3135
+ // crossed the action dispatch boundary. Native open-page creation is itself
3136
+ // transactional: its bridge either returns an exact created-tab receipt or
3137
+ // cleans up the created tab before propagating failure. Mark dispatch only
3138
+ // after a usable handle exists; subsequent goto/click/fill failures retain
3139
+ // outcome-unknown fencing for non-replay-safe actions.
3140
+ let actionDispatched = false;
2494
3141
  try {
2495
3142
  if (target.existingSession) assertBrowserSessionAvailable(repoRoot, target.sessionId);
2496
3143
  const browserArgs = requireExistingResource
@@ -2537,7 +3184,7 @@ async function withPage<T>(
2537
3184
  throw new AssistantPluginError(
2538
3185
  'PLUGIN_BROWSER_MUTATION_OUTCOME_UNKNOWN',
2539
3186
  `Browser action ${actionId} reported a transient failure after dispatch. Forge refused automatic replay because the external mutation outcome is unknown; verify current browser state before retrying.`,
2540
- { retryable: false, details: { actionId, requestedRetries: retries } },
3187
+ { retryable: false, effectOutcome: 'outcome_unknown', details: { actionId, requestedRetries: retries } },
2541
3188
  );
2542
3189
  }
2543
3190
  if (attempt >= retries) throw error;
@@ -2733,9 +3380,10 @@ const STABLE_SELECTOR_HELPERS = `
2733
3380
  `;
2734
3381
 
2735
3382
  const EXTRACTION_SCRIPTS = {
2736
- query: (selector: string, limit: number) => `(() => {
3383
+ query: (selector: string, limit: number, fromEnd = false) => `(() => {
2737
3384
  ${STABLE_SELECTOR_HELPERS}
2738
- const nodes = ${openShadowSelectorExpression(selector, true)}.slice(0, ${limit});
3385
+ const allNodes = ${openShadowSelectorExpression(selector, true)};
3386
+ const nodes = ${fromEnd ? `allNodes.slice(-${limit})` : `allNodes.slice(0, ${limit})`};
2739
3387
  return nodes.map((el) => {
2740
3388
  const role = el.getAttribute('role');
2741
3389
  const text = (el.innerText || el.textContent || '').trim().slice(0, 120);
@@ -2818,11 +3466,15 @@ const EXTRACTION_SCRIPTS = {
2818
3466
  })()`,
2819
3467
  };
2820
3468
 
3469
+ function selectorUnavailableInTime(errorMessage: string): boolean {
3470
+ return /timeout|timed out|waiting for selector|selector.*not found|not found.*selector/i.test(errorMessage);
3471
+ }
3472
+
2821
3473
  function selectorRepairHint(selector: string, errorMessage: string): string {
2822
3474
  if (/strict mode violation|resolved to \d+ elements/i.test(errorMessage)) {
2823
3475
  return `Selector "${selector}" matched multiple elements. Prefer a unique #id, [data-testid], or more specific path.`;
2824
3476
  }
2825
- if (/Timeout|waiting for selector|not found/i.test(errorMessage)) {
3477
+ if (selectorUnavailableInTime(errorMessage)) {
2826
3478
  return `Selector "${selector}" was not found in time. Use snapshot_interactive or query_all to discover stable selectors, then retry.`;
2827
3479
  }
2828
3480
  return `Check selector "${selector}" against the current page structure.`;
@@ -3223,9 +3875,9 @@ function health(config: BrowserPluginConfig, repoRoot?: string): AssistantPlugin
3223
3875
  };
3224
3876
  }
3225
3877
 
3226
- export function buildBrowserPluginManifest(previousRevision = 0, previousUpdatedAt?: string, repoRoot?: string): AssistantPluginManifest {
3878
+ export function buildBrowserPluginManifest(previousRevision = 0, previousUpdatedAt?: string, repoRoot?: string, context?: AssistantPluginBuildContext): AssistantPluginManifest {
3227
3879
  const root = repoRoot ?? process.cwd();
3228
- const config = loadConfig(root);
3880
+ const config = loadConfig(root, context);
3229
3881
  const state = health(config, root);
3230
3882
  return {
3231
3883
  schemaVersion: 1,
@@ -3234,11 +3886,11 @@ export function buildBrowserPluginManifest(previousRevision = 0, previousUpdated
3234
3886
  pluginId: BROWSER_PLUGIN_ID,
3235
3887
  provider: 'local-browser',
3236
3888
  displayName: 'Controller Browser Plugin',
3237
- pluginVersion: '1.1.0',
3889
+ pluginVersion: '1.2.0',
3238
3890
  authority: {
3239
3891
  strategy: 'derived',
3240
3892
  duplicateStateAllowed: false,
3241
- sourceOfTruth: ['repo-local:.forge/plugins/browser.json', 'controller-home:sqlite/browser_session', 'repo-local:.forge/browser/{screenshots,downloads,diagnostics}'],
3893
+ sourceOfTruth: ['controller-home:repositories/<repoId>/plugins/config/browser.json', 'controller-home:sqlite/computer_interaction_target', 'controller-home:repository browser artifacts'],
3242
3894
  },
3243
3895
  enabled: config.enabled,
3244
3896
  lifecycle: {
@@ -3278,7 +3930,7 @@ async function executeBrowserPluginActionInternal(
3278
3930
  input: AssistantPluginActionExecutionInput,
3279
3931
  runtimeAlreadyRouted = false,
3280
3932
  ): Promise<Record<string, unknown>> {
3281
- const persisted = loadConfig(input.repoRoot);
3933
+ const persisted = loadConfig(input.repoRoot, input);
3282
3934
  const actionSessionId = input.actionId === 'configure' ? undefined : stringValue(input.args.session_id);
3283
3935
  const actionSession = actionSessionId ? findBrowserSession(input.repoRoot, actionSessionId) : undefined;
3284
3936
  const current = input.actionId === 'configure' ? persisted : effectiveBrowserActionConfig(persisted, input.args, actionSession);
@@ -3360,13 +4012,13 @@ async function executeBrowserPluginActionInternal(
3360
4012
  const nextCdpAttachFallback = parseCdpAttachFallbackInput(args.cdp_attach_fallback) ?? current.cdpAttachFallback;
3361
4013
  const nextNativeAttachMode = parseNativeAttachModeInput(args.native_attach_mode) ?? current.nativeAttachMode;
3362
4014
  const nextNativeBrowserCandidates = parseNativeBrowserCandidatesInput(args.native_browser_candidates) ?? current.nativeBrowserCandidates;
3363
- const config = saveConfig(input.repoRoot, {
4015
+ const config = saveConfig(input, {
3364
4016
  enabled: typeof args.enabled === 'boolean' ? args.enabled : current.enabled,
3365
4017
  browserMode: nextBrowserMode,
3366
4018
  profileMode: nextProfileMode,
3367
4019
  profileDir: nextProfileMode === 'repo_local' ? undefined : nextProfileDir,
3368
4020
  profileDirectory: nextProfileMode === 'repo_local' ? undefined : nextProfileDirectory,
3369
- browserChannel: nextBrowserChannel ?? 'chromium',
4021
+ browserChannel: nextBrowserChannel ?? (nextExecutablePath ? undefined : 'chromium'),
3370
4022
  executablePath: nextExecutablePath,
3371
4023
  cdpEndpoint: nextCdpEndpoint,
3372
4024
  cdpEndpointCandidates: nextCdpEndpointCandidates,
@@ -3387,6 +4039,71 @@ async function executeBrowserPluginActionInternal(
3387
4039
  await closeManagedContextsForRepo(input.repoRoot);
3388
4040
  return { config, health: health(config, input.repoRoot) };
3389
4041
  }
4042
+ case 'list_unpacked_extensions': {
4043
+ if (current.browserMode === 'attach_preferred') {
4044
+ const cdp = await cdpExtensionList(input.repoRoot, current);
4045
+ if (cdp) return { ...cdp, verified: true };
4046
+ if (current.cdpAttachFallback !== 'managed_persistent') {
4047
+ throw new AssistantPluginError('PLUGIN_BROWSER_EXTENSION_CONTROL_UNAVAILABLE', 'Unpacked-extension control requires a configured browser-level CDP endpoint; refusing to restart or mutate the user-owned native browser.', { retryable: true });
4048
+ }
4049
+ }
4050
+ if (current.browserMode !== 'managed_persistent' && current.cdpAttachFallback !== 'managed_persistent') {
4051
+ throw new AssistantPluginError('PLUGIN_BROWSER_EXTENSION_CONTROL_UNAVAILABLE', 'Selected Browser mode does not provide unpacked-extension control.', { retryable: false });
4052
+ }
4053
+ const profile = selectedProfile(current, input.repoRoot, 'managed_persistent');
4054
+ const pending = managedBrowserContexts.get(managedContextKey(profile));
4055
+ if (!pending) return { provider: 'playwright-persistent-context', extensions: [], verified: true };
4056
+ const state = await pending;
4057
+ const urls = extensionTargets(state.context);
4058
+ const extensions = [...(managedExtensionPaths.get(managedContextKey(profile)) ?? [])].map((path) => {
4059
+ const { expectedId } = unpackedExtensionPath({ ...input, args: { ...input.args, extension_path: path } });
4060
+ const enabled = Boolean(expectedId && urls.some((url) => url.startsWith(`chrome-extension://${expectedId}/`)));
4061
+ return { id: expectedId, path, enabled };
4062
+ });
4063
+ return { provider: 'playwright-persistent-context', extensions, verified: true };
4064
+ }
4065
+ case 'install_unpacked_extension': {
4066
+ const extension = unpackedExtensionPath(input);
4067
+ if (current.browserMode === 'attach_preferred') {
4068
+ const cdp = await cdpInstallExtension(input, current, extension.path, extension.expectedId);
4069
+ if (cdp) return cdp;
4070
+ if (current.cdpAttachFallback !== 'managed_persistent') {
4071
+ throw new AssistantPluginError('PLUGIN_BROWSER_EXTENSION_CONTROL_UNAVAILABLE', 'Unpacked-extension install requires a configured browser-level CDP endpoint; refusing to restart or mutate the user-owned native browser.', { retryable: true });
4072
+ }
4073
+ }
4074
+ if ((current.browserMode !== 'managed_persistent' && current.cdpAttachFallback !== 'managed_persistent') || !extension.expectedId) {
4075
+ throw new AssistantPluginError('PLUGIN_BROWSER_EXTENSION_CONTROL_UNAVAILABLE', extension.expectedId
4076
+ ? 'Selected Browser mode does not provide managed unpacked-extension control.'
4077
+ : 'Managed unpacked-extension verification requires a stable manifest key.', { retryable: false });
4078
+ }
4079
+ const profile = selectedProfile(current, input.repoRoot, 'managed_persistent');
4080
+ const nativeHostChanged = projectManagedNativeMessagingHost(profile.profileDir, extension.nativeHost);
4081
+ if (customVivaldiExtensionInstall(current, input.repoRoot)) {
4082
+ return await installExtensionThroughCustomVivaldiCdpPort(input, current, profile, extension.path, extension.expectedId);
4083
+ }
4084
+ if (!runtimeHooks.moduleAvailable('playwright', input.repoRoot)) {
4085
+ throw new AssistantPluginError('PLUGIN_BROWSER_DEPENDENCY_UNAVAILABLE', 'Managed unpacked-extension install requires Playwright.', { retryable: false });
4086
+ }
4087
+ const runtime = runtimeHooks.loadPlaywright(input.repoRoot);
4088
+ const key = managedContextKey(profile);
4089
+ const paths = managedExtensionPaths.get(key) ?? new Set<string>();
4090
+ const changed = !paths.has(extension.path);
4091
+ paths.add(extension.path);
4092
+ managedExtensionPaths.set(key, paths);
4093
+ if (changed || nativeHostChanged) await evictManagedContext(key);
4094
+ const state = await managedContextState(runtime, input.repoRoot, current, profile);
4095
+ const prefix = `chrome-extension://${extension.expectedId}/`;
4096
+ let runtimeTarget = extensionTargets(state.context).find((candidate) => candidate.startsWith(prefix));
4097
+ const verifiedExtension = await managedCdpInstallExtension(input, state.context, extension.path, extension.expectedId, Boolean(runtimeTarget));
4098
+ if (!runtimeTarget) {
4099
+ runtimeTarget = await waitForManagedExtension(state.context, extension.expectedId, positiveNumber(input.args.timeout_ms, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS));
4100
+ }
4101
+ return {
4102
+ provider: 'playwright-persistent-context',
4103
+ extension: { ...verifiedExtension, runtimeTarget },
4104
+ verified: true,
4105
+ };
4106
+ }
3390
4107
  case 'list_sessions': {
3391
4108
  const inventory = await inspectSavedSessions(input.repoRoot, current);
3392
4109
  const requestedLimit = Math.max(1, Math.min(Math.trunc(positiveNumber(input.args.limit, DEFAULT_BROWSER_SESSION_LIST_LIMIT)), MAX_BROWSER_SESSION_LIST_LIMIT));
@@ -3807,7 +4524,13 @@ async function executeBrowserPluginActionInternal(
3807
4524
  ? { text: await extractText(page, undefined, positiveNumber(input.args.max_chars, DEFAULT_MAX_TEXT_CHARS)) }
3808
4525
  : {}),
3809
4526
  };
3810
- }, { requireExistingResource: Boolean(existingSessionId) });
4527
+ }, {
4528
+ requireExistingResource: browserExplicitSessionRequiresExistingResource(
4529
+ input.actionId,
4530
+ existingSessionId,
4531
+ Boolean(target.existingSession),
4532
+ ),
4533
+ });
3811
4534
  }
3812
4535
  case 'reload':
3813
4536
  case 'go_back':
@@ -3877,7 +4600,11 @@ async function executeBrowserPluginActionInternal(
3877
4600
  const limit = Math.min(positiveNumber(input.args.limit, 25), 100);
3878
4601
  return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
3879
4602
  const selection = resolveBrowserEvaluationScope(page, input.args, connection);
3880
- const matches = await selection.scope.evaluate<Array<Record<string, unknown>>>(EXTRACTION_SCRIPTS.query(selector, input.actionId === 'query_selector' ? 1 : limit));
4603
+ const matches = await selection.scope.evaluate<Array<Record<string, unknown>>>(EXTRACTION_SCRIPTS.query(
4604
+ selector,
4605
+ input.actionId === 'query_selector' ? 1 : limit,
4606
+ input.actionId === 'query_all' && input.args.from_end === true,
4607
+ ));
3881
4608
  return {
3882
4609
  provider: browserResultProvider(connection.provider),
3883
4610
  sessionId: target.sessionId,
@@ -3932,17 +4659,50 @@ async function executeBrowserPluginActionInternal(
3932
4659
  };
3933
4660
  }, { persistSession: true });
3934
4661
  }
4662
+ case 'reconcile_effect': {
4663
+ const effectRequestId = requiredString(input.args.effect_request_id, 'effect_request_id');
4664
+ requiredString(input.args.session_id, 'session_id');
4665
+ const observationArgs = (prefix: 'applied' | 'not_applied'): Record<string, unknown> => ({
4666
+ session_id: input.args.session_id,
4667
+ ...(input.args.url !== undefined ? { url: input.args.url } : {}),
4668
+ ...(input.args.max_chars !== undefined ? { max_chars: input.args.max_chars } : {}),
4669
+ ...(input.args.frame_url !== undefined ? { frame_url: input.args.frame_url } : {}),
4670
+ ...(input.args.frame_name !== undefined ? { frame_name: input.args.frame_name } : {}),
4671
+ ...(input.args.frame_index !== undefined ? { frame_index: input.args.frame_index } : {}),
4672
+ ...(input.args[`${prefix}_url_contains_any`] !== undefined ? { url_contains_any: input.args[`${prefix}_url_contains_any`] } : {}),
4673
+ ...(input.args[`${prefix}_url_contains_none`] !== undefined ? { url_contains_none: input.args[`${prefix}_url_contains_none`] } : {}),
4674
+ ...(input.args[`${prefix}_text_contains_any`] !== undefined ? { text_contains_any: input.args[`${prefix}_text_contains_any`] } : {}),
4675
+ ...(input.args[`${prefix}_text_contains_none`] !== undefined ? { text_contains_none: input.args[`${prefix}_text_contains_none`] } : {}),
4676
+ ...(input.args[`${prefix}_selector`] !== undefined ? { selector: input.args[`${prefix}_selector`] } : {}),
4677
+ });
4678
+ const hasCriteria = (args: Record<string, unknown>) => Object.keys(args).some((key) => !['session_id', 'url', 'max_chars', 'frame_url', 'frame_name', 'frame_index'].includes(key));
4679
+ const appliedArgs = observationArgs('applied');
4680
+ if (!hasCriteria(appliedArgs)) throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'reconcile_effect requires at least one applied_* observation criterion.', { retryable: false });
4681
+ const applied = await executeBrowserPluginAction({ ...input, actionId: 'verify_state', args: appliedArgs });
4682
+ if (applied.matched === true) return { effectRequestId, outcome: 'applied', output: applied, observation: applied };
4683
+ const notAppliedArgs = observationArgs('not_applied');
4684
+ if (hasCriteria(notAppliedArgs)) {
4685
+ const notApplied = await executeBrowserPluginAction({ ...input, actionId: 'verify_state', args: notAppliedArgs });
4686
+ if (notApplied.matched === true) return { effectRequestId, outcome: 'not_applied', observation: notApplied };
4687
+ return { effectRequestId, outcome: 'unknown', observation: { applied, notApplied } };
4688
+ }
4689
+ return { effectRequestId, outcome: 'unknown', observation: { applied } };
4690
+ }
3935
4691
  case 'verify_state': {
3936
4692
  requiredString(input.args.session_id, 'session_id');
3937
4693
  const target = resolveActionTarget(input.repoRoot, input.args);
3938
4694
  const expectedUrlRaw = stringValue(input.args.expected_url);
3939
4695
  const expectedUrl = expectedUrlRaw ? normalizedUrl(expectedUrlRaw) : undefined;
3940
4696
  const urlContains = stringValue(input.args.url_contains);
4697
+ const urlContainsAny = stringList(input.args.url_contains_any);
4698
+ const urlContainsNone = stringList(input.args.url_contains_none);
3941
4699
  const selector = stringValue(input.args.selector);
3942
4700
  const requireVisible = input.args.require_visible === true;
3943
4701
  const textContains = stringValue(input.args.text_contains);
3944
- if (!expectedUrl && !urlContains && !selector && !textContains) {
3945
- throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'verify_state requires at least one of expected_url, url_contains, selector, or text_contains.', { retryable: false });
4702
+ const textContainsAny = stringList(input.args.text_contains_any);
4703
+ const textContainsNone = stringList(input.args.text_contains_none);
4704
+ if (!expectedUrl && !urlContains && !urlContainsAny && !urlContainsNone && !selector && !textContains && !textContainsAny && !textContainsNone) {
4705
+ throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'verify_state requires at least one URL, selector, or text criterion.', { retryable: false });
3946
4706
  }
3947
4707
  if (requireVisible && !selector) {
3948
4708
  throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'require_visible requires selector.', { retryable: false });
@@ -3953,9 +4713,11 @@ async function executeBrowserPluginActionInternal(
3953
4713
  const checks: Array<Record<string, unknown>> = [];
3954
4714
  if (expectedUrl) checks.push({ criterion: 'url_exact', expected: expectedUrl, observed: observedUrl, matched: observedUrl === expectedUrl });
3955
4715
  if (urlContains) checks.push({ criterion: 'url_contains', expected: urlContains, observed: observedUrl, matched: observedUrl.includes(urlContains) });
4716
+ if (urlContainsAny) checks.push({ criterion: 'url_contains_any', expected: urlContainsAny, observed: observedUrl, matched: urlContainsAny.some((value) => observedUrl.includes(value)) });
4717
+ if (urlContainsNone) checks.push({ criterion: 'url_contains_none', expected: urlContainsNone, observed: observedUrl, matched: urlContainsNone.every((value) => !observedUrl.includes(value)) });
3956
4718
  let observation: { verificationVersion?: unknown; selectorExists?: unknown; visible?: unknown; textContains?: unknown; textSample?: unknown; textLength?: unknown; truncated?: unknown } | undefined;
3957
- if (selector || textContains) {
3958
- observation = await selection.scope.evaluate(verifyStateObservationScript(selector, textContains, Math.min(positiveNumber(input.args.max_chars, 2_000), 100_000)));
4719
+ if (selector || textContains || textContainsAny || textContainsNone) {
4720
+ observation = await selection.scope.evaluate(verifyStateObservationScript(selector, textContains, Math.min(positiveNumber(input.args.max_chars, 12_000), 100_000)));
3959
4721
  if (observation?.verificationVersion !== 1) {
3960
4722
  throw new AssistantPluginError('PLUGIN_BROWSER_VERIFY_STATE_UNAVAILABLE', 'Browser state observation returned an unsupported result.', { retryable: true });
3961
4723
  }
@@ -3973,6 +4735,9 @@ async function executeBrowserPluginActionInternal(
3973
4735
  ...(selector ? { selector } : {}),
3974
4736
  });
3975
4737
  }
4738
+ const textSample = typeof observation?.textSample === 'string' ? observation.textSample : '';
4739
+ if (textContainsAny) checks.push({ criterion: 'text_contains_any', expected: textContainsAny, observed: textSample, matched: textContainsAny.some((value) => textSample.includes(value)), textLength: observation?.textLength, truncated: observation?.truncated });
4740
+ if (textContainsNone) checks.push({ criterion: 'text_contains_none', expected: textContainsNone, observed: textSample, matched: textContainsNone.every((value) => !textSample.includes(value)), textLength: observation?.textLength, truncated: observation?.truncated });
3976
4741
  return {
3977
4742
  provider: browserResultProvider(connection.provider),
3978
4743
  sessionId: target.sessionId,
@@ -4086,15 +4851,16 @@ async function executeBrowserPluginActionInternal(
4086
4851
  case 'activate_page': {
4087
4852
  const target = resolveActionTarget(input.repoRoot, input.args);
4088
4853
  return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
4089
- const waitMs = positiveNumber(input.args.post_action_wait_ms, DEFAULT_POST_ACTION_WAIT_MS);
4090
4854
  if (connection.provider === 'macos-apple-events') {
4091
- await establishAuthoritativeNativeForeground(input, page, connection, target, waitMs);
4855
+ await establishAuthoritativeNativeForeground(
4856
+ input, page, connection, target, browserNativeForegroundVerificationWaitMs(input.args.post_action_wait_ms),
4857
+ );
4092
4858
  } else {
4093
4859
  if (!page.bringToFront) {
4094
4860
  throw new AssistantPluginError('PLUGIN_BROWSER_ACTIVATION_UNSUPPORTED', 'The selected browser provider cannot bring the saved page to the foreground.', { retryable: false });
4095
4861
  }
4096
4862
  await page.bringToFront();
4097
- await delay(waitMs);
4863
+ await delay(browserExplicitPostActionWaitMs(input.args.post_action_wait_ms));
4098
4864
  }
4099
4865
  return finalizeInteractiveAction(input.repoRoot, current, page, target, connection, 'activate_page', 'Activated the exact saved browser page in authoritative foreground state.', {});
4100
4866
  });
@@ -4137,7 +4903,7 @@ async function executeBrowserPluginActionInternal(
4137
4903
  element.click();
4138
4904
  return { tag: element.tagName.toLowerCase(), className: element.className || '', text: normalize(element.innerText || element.textContent || '') };
4139
4905
  }, { text });
4140
- await delay(positiveNumber(input.args.post_action_wait_ms, DEFAULT_POST_ACTION_WAIT_MS));
4906
+ await delay(browserExplicitPostActionWaitMs(input.args.post_action_wait_ms));
4141
4907
  return finalizeInteractiveAction(input.repoRoot, current, page, target, connection, 'click_text', `Clicked exact visible text ${text}.`, { text, clicked });
4142
4908
  });
4143
4909
  }
@@ -4182,7 +4948,7 @@ async function executeBrowserPluginActionInternal(
4182
4948
  if (page.uncheck) await page.uncheck(selector, { timeout: timeoutMs });
4183
4949
  else await page.click(selector, { timeout: timeoutMs });
4184
4950
  }
4185
- await delay(positiveNumber(input.args.post_action_wait_ms, DEFAULT_POST_ACTION_WAIT_MS));
4951
+ await delay(browserExplicitPostActionWaitMs(input.args.post_action_wait_ms));
4186
4952
  const summary = input.actionId === 'click'
4187
4953
  ? `Clicked ${selector}.`
4188
4954
  : input.actionId === 'double_click'
@@ -4198,10 +4964,14 @@ async function executeBrowserPluginActionInternal(
4198
4964
  });
4199
4965
  } catch (error) {
4200
4966
  const message = error instanceof Error ? error.message : String(error);
4201
- throw new AssistantPluginError('PLUGIN_ACTION_FAILED', message, {
4202
- retryable: true,
4203
- details: { selector, repairHint: selectorRepairHint(selector, message) },
4204
- });
4967
+ throw new AssistantPluginError(
4968
+ selectorUnavailableInTime(message) ? 'PLUGIN_BROWSER_SELECTOR_UNAVAILABLE' : 'PLUGIN_ACTION_FAILED',
4969
+ message,
4970
+ {
4971
+ retryable: true,
4972
+ details: { selector, repairHint: selectorRepairHint(selector, message) },
4973
+ },
4974
+ );
4205
4975
  }
4206
4976
  }
4207
4977
  case 'type':
@@ -4213,7 +4983,7 @@ async function executeBrowserPluginActionInternal(
4213
4983
  return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
4214
4984
  if (input.actionId === 'fill' || !page.type) await page.fill(selector, text, { timeout: timeoutMs });
4215
4985
  else await page.type(selector, text, { timeout: timeoutMs });
4216
- await delay(positiveNumber(input.args.post_action_wait_ms, DEFAULT_POST_ACTION_WAIT_MS));
4986
+ await delay(browserExplicitPostActionWaitMs(input.args.post_action_wait_ms));
4217
4987
  return finalizeInteractiveAction(input.repoRoot, current, page, target, connection, input.actionId, `${input.actionId} ${selector} with ${text.length} characters.`, {
4218
4988
  selector,
4219
4989
  textLength: text.length,
@@ -4233,7 +5003,7 @@ async function executeBrowserPluginActionInternal(
4233
5003
  if (!Array.isArray(selectedValues) || selectedValues.length === 0) {
4234
5004
  throw new AssistantPluginError('PLUGIN_BROWSER_SELECT_OPTION_FAILED', 'The browser provider did not retain any requested option selection.', { retryable: true, details: { selector, requestedCount: values.length } });
4235
5005
  }
4236
- await delay(positiveNumber(input.args.post_action_wait_ms, DEFAULT_POST_ACTION_WAIT_MS));
5006
+ await delay(browserExplicitPostActionWaitMs(input.args.post_action_wait_ms));
4237
5007
  return finalizeInteractiveAction(input.repoRoot, current, page, target, connection, 'select_option', `Selected ${values.length} option(s) on ${selector}.`, { selector, values, selectedValues });
4238
5008
  });
4239
5009
  }
@@ -4243,7 +5013,7 @@ async function executeBrowserPluginActionInternal(
4243
5013
  const key = requiredString(input.args.key, 'key');
4244
5014
  return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
4245
5015
  await page.press(selector, key, { timeout: positiveNumber(input.args.timeout_ms, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS) });
4246
- await delay(positiveNumber(input.args.post_action_wait_ms, DEFAULT_POST_ACTION_WAIT_MS));
5016
+ await delay(browserExplicitPostActionWaitMs(input.args.post_action_wait_ms));
4247
5017
  return finalizeInteractiveAction(input.repoRoot, current, page, target, connection, 'press', `Pressed ${key} on ${selector}.`, {
4248
5018
  selector,
4249
5019
  key,
@@ -4256,7 +5026,7 @@ async function executeBrowserPluginActionInternal(
4256
5026
  return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
4257
5027
  if (page.keyboard?.press) await page.keyboard.press(key);
4258
5028
  else await page.press('body', key, { timeout: positiveNumber(input.args.timeout_ms, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS) });
4259
- await delay(positiveNumber(input.args.post_action_wait_ms, DEFAULT_POST_ACTION_WAIT_MS));
5029
+ await delay(browserExplicitPostActionWaitMs(input.args.post_action_wait_ms));
4260
5030
  return finalizeInteractiveAction(input.repoRoot, current, page, target, connection, 'keyboard_shortcut', `Pressed shortcut ${key}.`, { key });
4261
5031
  });
4262
5032
  }
@@ -4398,7 +5168,7 @@ async function executeBrowserPluginActionInternal(
4398
5168
  }
4399
5169
  }
4400
5170
  if (nativeTrustedInput) await page.trustedInput!(trustedRequest);
4401
- await delay(positiveNumber(input.args.post_action_wait_ms, DEFAULT_POST_ACTION_WAIT_MS));
5171
+ await delay(browserExplicitPostActionWaitMs(input.args.post_action_wait_ms));
4402
5172
  return finalizeInteractiveAction(input.repoRoot, current, page, target, connection, 'trusted_input', `Sent trusted browser input (${kind}).`, { kind, ...(guard ? { guard } : {}) });
4403
5173
  });
4404
5174
  }
@@ -4418,7 +5188,7 @@ async function executeBrowserPluginActionInternal(
4418
5188
  const accepted = element.dispatchEvent(domEvent);
4419
5189
  return { accepted, defaultPrevented: domEvent.defaultPrevented };
4420
5190
  }, { selector, event });
4421
- await delay(positiveNumber(input.args.post_action_wait_ms, DEFAULT_POST_ACTION_WAIT_MS));
5191
+ await delay(browserExplicitPostActionWaitMs(input.args.post_action_wait_ms));
4422
5192
  return finalizeInteractiveAction(input.repoRoot, current, page, target, connection, 'dispatch_event', `Dispatched ${event} on ${selector}.`, {
4423
5193
  selector,
4424
5194
  event,
@@ -4488,7 +5258,7 @@ async function executeBrowserPluginActionInternal(
4488
5258
  throw new AssistantPluginError('PLUGIN_BROWSER_FILE_ATTACH_UNSUPPORTED', 'The selected browser provider does not support local file attachment.', { retryable: false });
4489
5259
  }
4490
5260
  await page.setInputFiles(selector, resolved.length === 1 ? resolved[0] : resolved);
4491
- await delay(positiveNumber(input.args.post_action_wait_ms, DEFAULT_POST_ACTION_WAIT_MS));
5261
+ await delay(browserExplicitPostActionWaitMs(input.args.post_action_wait_ms));
4492
5262
  const fileNames = resolved.map((path) => basename(path));
4493
5263
  return finalizeInteractiveAction(input.repoRoot, current, page, target, connection, 'attach_local_file', `Attached ${resolved.length} local file(s) to ${selector}.`, {
4494
5264
  selector,
@@ -4549,7 +5319,7 @@ async function executeBrowserPluginActionInternal(
4549
5319
  if (!existsSync(dest)) {
4550
5320
  throw new AssistantPluginError('PLUGIN_BROWSER_DOWNLOAD_FAILED', 'Browser download save completed without producing an artifact; refusing false success.', { retryable: true, details: { selector } });
4551
5321
  }
4552
- await delay(positiveNumber(input.args.post_action_wait_ms, DEFAULT_POST_ACTION_WAIT_MS));
5322
+ await delay(browserExplicitPostActionWaitMs(input.args.post_action_wait_ms));
4553
5323
  return finalizeInteractiveAction(input.repoRoot, current, page, target, connection, 'await_file_transfer', `Captured download artifact for ${selector}.`, {
4554
5324
  selector,
4555
5325
  download: {
@@ -4588,10 +5358,3 @@ export async function executeBrowserPluginAction(input: AssistantPluginActionExe
4588
5358
  () => executeBrowserPluginActionInternal(input),
4589
5359
  );
4590
5360
  }
4591
-
4592
- export const browserPluginAdapter = {
4593
- pluginId: BROWSER_PLUGIN_ID,
4594
- buildManifest: buildBrowserPluginManifest,
4595
- executeAction: executeBrowserPluginAction,
4596
- resolveAuthorizationContext: resolveBrowserPluginAuthorizationContext,
4597
- };