@moretea-labs/forge 1.8.0 → 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 (528) hide show
  1. package/AGENTS.md +70 -7
  2. package/CHANGELOG.md +14 -0
  3. package/CLAUDE.md +17 -2
  4. package/CONTRIBUTING.md +3 -2
  5. package/THIRD_PARTY_NOTICES.md +1 -1
  6. package/adapters/chatgpt/browser-delivery-host.ts +7 -1
  7. package/adapters/chatgpt/browser-delivery-runtime.ts +529 -56
  8. package/adapters/chatgpt/controller-binding-store.ts +101 -22
  9. package/adapters/chatgpt/controller-round-host.ts +15 -25
  10. package/adapters/chatgpt/controller-round-settlement-store.ts +58 -4
  11. package/adapters/chatgpt/provider-delivery.ts +228 -1
  12. package/adapters/chatgpt/work-conversation-binding-store.ts +79 -53
  13. package/adapters/computer/chatgpt-extension-target.ts +463 -0
  14. package/adapters/computer/chatgpt-target-router.ts +103 -0
  15. package/adapters/computer/chrome-extension/background.js +385 -0
  16. package/adapters/computer/chrome-extension/content.js +378 -0
  17. package/adapters/computer/chrome-extension/core.js +24 -0
  18. package/adapters/computer/chrome-extension/manifest.json +30 -0
  19. package/adapters/computer/desktop-operator-discovery.ts +11 -31
  20. package/adapters/computer/interaction-target-authority.ts +142 -18
  21. package/adapters/mcp/auth.ts +0 -1
  22. package/adapters/mcp/instructions.ts +5 -4
  23. package/adapters/mcp/multi-repository.ts +3 -19
  24. package/adapters/mcp/oauth.ts +73 -0
  25. package/adapters/mcp/paths.ts +4 -1
  26. package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +87 -0
  27. package/adapters/mcp/runtime-gateway/bounded-result-runtime.ts +62 -0
  28. package/adapters/mcp/runtime-gateway/cognition-settlement.ts +201 -0
  29. package/adapters/mcp/runtime-gateway/context-adapter.ts +411 -73
  30. package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +91 -0
  31. package/adapters/mcp/runtime-gateway/controller-authority-adapter.ts +20 -5
  32. package/adapters/mcp/runtime-gateway/core-capability-adapter.ts +258 -0
  33. package/adapters/mcp/runtime-gateway/engineering-preconditions.ts +10 -27
  34. package/adapters/mcp/runtime-gateway/engineering-tool-contract.ts +15 -0
  35. package/adapters/mcp/runtime-gateway/execution-tools.ts +8 -42
  36. package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +2 -1
  37. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +117 -45
  38. package/adapters/mcp/runtime-gateway/process-tools.ts +190 -22
  39. package/adapters/mcp/runtime-gateway/protected-computer-adapter.ts +16 -4
  40. package/adapters/mcp/runtime-gateway/recovery-adapter.ts +69 -52
  41. package/adapters/mcp/runtime-gateway/repository-compat-adapter.ts +4 -35
  42. package/adapters/mcp/runtime-gateway/result-adapter.ts +10 -4
  43. package/adapters/mcp/runtime-gateway/router.ts +18 -504
  44. package/adapters/mcp/runtime-gateway/routing-policy.ts +499 -0
  45. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +14 -65
  46. package/adapters/mcp/runtime-gateway/runtime-readiness-observation.ts +218 -0
  47. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +113 -173
  48. package/adapters/mcp/runtime-gateway/runtime-tools.ts +15 -5
  49. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +1 -0
  50. package/adapters/mcp/runtime-gateway/shared-adapter.ts +20 -0
  51. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +358 -266
  52. package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +136 -0
  53. package/adapters/mcp/runtime-gateway/work-adapter.ts +261 -3122
  54. package/adapters/mcp/runtime-gateway/work-controller-operations.ts +257 -0
  55. package/adapters/mcp/runtime-gateway/work-detail-projection.ts +60 -0
  56. package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +246 -0
  57. package/adapters/mcp/runtime-gateway/work-learning-operations.ts +145 -0
  58. package/adapters/mcp/runtime-gateway/work-list-projection.ts +38 -0
  59. package/adapters/mcp/runtime-gateway/work-plan-operations.ts +382 -0
  60. package/adapters/mcp/runtime-gateway/work-repair-adapter.ts +308 -0
  61. package/adapters/mcp/runtime-gateway/work-requirement-operations.ts +148 -0
  62. package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +263 -0
  63. package/adapters/mcp/runtime-gateway/work-workflow-operations.ts +113 -0
  64. package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +156 -0
  65. package/adapters/mcp/server.ts +67 -23
  66. package/adapters/mcp/setup.ts +1 -3
  67. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +44 -170
  68. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +4 -9
  69. package/adapters/mcp/tool-mapping/repository-tools.ts +133 -142
  70. package/adapters/mcp/toolset-names.ts +5 -0
  71. package/adapters/mcp/toolset.ts +6 -5
  72. package/adapters/mcp/transports/http-observation.ts +365 -0
  73. package/adapters/mcp/transports/http.ts +154 -677
  74. package/adapters/mcp/transports/oauth-http.ts +322 -0
  75. package/adapters/mcp/transports/session-registry.ts +6 -0
  76. package/adapters/mcp/tunnels/openai-secure-tunnel.ts +89 -5
  77. package/assets/plugin-registry.v1.json +6 -4
  78. package/assets/reference-configs/agentic-development-flow.md +25 -0
  79. package/assets/reference-configs/global-working-rules.md +19 -0
  80. package/assets/skill-version.json +2 -2
  81. package/docs/architecture/CURRENT.md +111 -41
  82. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +14 -5
  83. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +6 -3
  84. package/docs/architecture/decisions/20260907-assistant-learning-loop.md +2 -0
  85. package/docs/architecture/decisions/20260907-controller-round-transition-policy.md +19 -4
  86. package/docs/architecture/decisions/20260916-workflow-supervisor-authority.md +249 -0
  87. package/docs/architecture/decisions/20260917-cognitive-memory-foundation.md +82 -0
  88. package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +221 -0
  89. package/docs/architecture/decisions/20260919-autonomous-work-liveness.md +62 -0
  90. package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +72 -0
  91. package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +237 -0
  92. package/docs/architecture/decisions/20261005-forge-v3-desktop-client-boundary.md +77 -0
  93. package/docs/architecture/domains/verification.md +1 -0
  94. package/docs/architecture/domains/workflow-engine.md +6 -0
  95. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
  96. package/docs/forge-plugin-management.md +2 -2
  97. package/docs/operations/controller-performance-and-502.md +17 -0
  98. package/docs/operations/controller-reliability-runbook.md +2 -2
  99. package/docs/operations/features.md +1 -1
  100. package/docs/operations/features.zh-CN.md +1 -1
  101. package/docs/operations/homebrew.md +2 -2
  102. package/docs/operations/mcp-tool-exposure.md +2 -0
  103. package/docs/operations/releasing.md +2 -0
  104. package/docs/operations/standalone-disaster-recovery.md +14 -6
  105. package/docs/wiki/Implementation.md +1 -1
  106. package/package.json +25 -14
  107. package/packages/kernel/cognition/api/index.ts +6 -0
  108. package/packages/kernel/cognition/application/activation-service.ts +451 -0
  109. package/packages/kernel/cognition/application/consolidation-service.ts +185 -0
  110. package/packages/kernel/cognition/application/legacy-experience-adapter.ts +38 -0
  111. package/packages/kernel/cognition/application/memory-service.ts +64 -0
  112. package/packages/kernel/cognition/domain/learning.ts +106 -0
  113. package/packages/kernel/cognition/domain/memory.ts +254 -0
  114. package/packages/kernel/controller/api/index.ts +1 -0
  115. package/packages/kernel/controller/application/continuation-service.ts +281 -0
  116. package/packages/kernel/controller/application/controller-round-service.ts +12 -0
  117. package/packages/kernel/controller/domain/controller-round-transition-policy.ts +242 -21
  118. package/packages/kernel/controller/domain/controller-round.ts +4 -1
  119. package/packages/kernel/controller/domain/execution-quality.ts +3 -1
  120. package/packages/kernel/controller/infrastructure/controller-round-store.ts +689 -103
  121. package/packages/kernel/controller/infrastructure/controller-session-store.ts +297 -71
  122. package/packages/kernel/controller/ports/controller-host.ts +2 -0
  123. package/packages/kernel/identity/api/index.ts +10 -0
  124. package/packages/kernel/identity/domain/activity-projection.ts +43 -0
  125. package/packages/kernel/identity/domain/capability-broker.ts +60 -0
  126. package/packages/kernel/identity/domain/grant.ts +72 -0
  127. package/packages/kernel/identity/domain/owned-resource.ts +116 -0
  128. package/packages/kernel/identity/domain/user-request.ts +93 -0
  129. package/packages/kernel/identity/infrastructure/activity-projection-store.ts +77 -0
  130. package/packages/kernel/identity/infrastructure/capability-broker.ts +81 -0
  131. package/packages/kernel/identity/infrastructure/grant-store.ts +284 -0
  132. package/packages/kernel/identity/infrastructure/owned-resource-store.ts +259 -0
  133. package/packages/kernel/identity/infrastructure/user-request-store.ts +182 -0
  134. package/packages/kernel/scheduler/api/index.ts +0 -2
  135. package/packages/kernel/scheduler/application/schedule-service.ts +3 -0
  136. package/packages/kernel/scheduler/application/settlement.ts +0 -15
  137. package/packages/kernel/scheduler/domain/schedule.ts +87 -0
  138. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +153 -133
  139. package/packages/kernel/work/api/index.ts +3 -2
  140. package/packages/kernel/work/application/work-service.ts +17 -42
  141. package/packages/kernel/work/domain/current-task-lineage.ts +104 -0
  142. package/packages/kernel/work/domain/engineering-design.ts +10 -5
  143. package/packages/kernel/work/domain/engineering-profile.ts +8 -12
  144. package/packages/kernel/work/domain/evaluation-promotion-receipt.ts +186 -0
  145. package/packages/kernel/work/domain/execution-concurrency.ts +19 -26
  146. package/packages/kernel/work/domain/execution-snapshot.ts +0 -31
  147. package/packages/kernel/work/domain/implementation-review.ts +14 -331
  148. package/packages/kernel/work/domain/objective-graph.ts +119 -0
  149. package/packages/kernel/work/domain/project-engineering-contract.ts +23 -1
  150. package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
  151. package/packages/kernel/work/domain/state-machine.ts +25 -63
  152. package/packages/kernel/work/domain/types.ts +57 -51
  153. package/packages/kernel/work/infrastructure/work-contract-store.ts +819 -728
  154. package/packages/kernel/work/ports/work-contract-store.ts +5 -2
  155. package/packages/plugin-runtime/browser/session-authority.ts +2 -1
  156. package/packages/plugin-runtime/computer/chatgpt-identity.ts +24 -0
  157. package/packages/plugin-runtime/computer/conversation-target.ts +143 -0
  158. package/packages/plugin-runtime/computer/index.ts +2 -0
  159. package/packages/plugin-runtime/computer/target-authority.ts +26 -1
  160. package/packages/plugin-runtime/external/unix-jsonl-transport.ts +142 -18
  161. package/packages/protocols/computer/contract.ts +27 -1
  162. package/scripts/activate-source-baseline.ts +7 -9
  163. package/scripts/canary-controller-semantic-wait.ts +12 -24
  164. package/scripts/check-mcp-compatibility.ts +48 -136
  165. package/scripts/check-runtime-architecture.mjs +622 -217
  166. package/scripts/forge-local-recovery-helper.mjs +93 -15
  167. package/scripts/lib/project-init-lib.sh +3 -0
  168. package/scripts/open-source-audit-allowlist.txt +1 -2
  169. package/scripts/run-bounded-command.mjs +5 -0
  170. package/scripts/run-governed-gate.ts +0 -1
  171. package/scripts/run-mobile-monitor-bridge.ts +0 -1
  172. package/scripts/smoke-control-plane-failure-domain.ts +94 -0
  173. package/scripts/smoke-mcp-http-runtime.ts +5 -0
  174. package/scripts/smoke-runtime-recovery.ts +12 -43
  175. package/scripts/smoke-schedule-engine.ts +23 -15
  176. package/scripts/smoke-unattended-transport-recovery.ts +223 -0
  177. package/scripts/smoke-user-request-continuation-restart.ts +232 -0
  178. package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +92 -0
  179. package/scripts/smoke-workflow-supervisor-process-restart.ts +276 -0
  180. package/scripts/smoke-workflow-supervisor-restart-recovery.ts +150 -0
  181. package/scripts/smoke-workflow-supervisor.ts +103 -0
  182. package/scripts/verify-forge-runtime.sh +2 -1
  183. package/src/cli/agent-jobs/executor-health.ts +11 -34
  184. package/src/cli/agent-jobs/integration.ts +18 -49
  185. package/src/cli/agent-jobs/job-manager.ts +7 -3
  186. package/src/cli/commands/brain-assistant.ts +27 -13
  187. package/src/cli/commands/brain.ts +13 -15
  188. package/src/cli/commands/chatgpt.ts +1 -163
  189. package/src/cli/commands/recovery.ts +135 -121
  190. package/src/cli/commands/runtime.ts +3 -3
  191. package/src/cli/commands/setup-profile.ts +8 -0
  192. package/src/cli/controller/check-runner.ts +13 -2
  193. package/src/cli/controller/completion-backlog.ts +3 -3
  194. package/src/cli/controller/completion-orchestrator.ts +4 -43
  195. package/src/cli/controller/completion-target.ts +3 -72
  196. package/src/cli/controller/context/source-materializer.ts +81 -100
  197. package/src/cli/controller/context-pack.ts +10 -8
  198. package/src/cli/controller/execution-completion.ts +4 -7
  199. package/src/cli/controller/execution-policy.ts +36 -153
  200. package/src/cli/controller/governance.ts +2 -3
  201. package/src/cli/controller/issue-store.ts +4 -99
  202. package/src/cli/controller/launch-agents.ts +59 -18
  203. package/src/cli/controller/operational-plan.ts +24 -51
  204. package/src/cli/controller/runtime-config.ts +5 -1
  205. package/src/cli/controller/stuck-state-migration.ts +2 -3
  206. package/src/cli/controller/systemd-user.ts +39 -1
  207. package/src/cli/editing/edit-session.ts +1 -7
  208. package/src/cli/local-bridge/console-automations.ts +18 -13
  209. package/src/cli/local-bridge/console-view-models.ts +3 -12
  210. package/src/cli/local-bridge/facade-api.ts +64 -191
  211. package/src/cli/local-bridge/server.ts +24 -147
  212. package/src/cli/local-bridge/types.ts +0 -1
  213. package/src/cli/mcp/oauth-client-retirement.ts +74 -0
  214. package/src/cli/repositories/command-executor.ts +29 -23
  215. package/src/cli/repositories/command-scope.ts +35 -16
  216. package/src/cli/repositories/controller-home.ts +48 -0
  217. package/src/cli/repositories/ephemeral-workspace.ts +20 -30
  218. package/src/cli/repositories/git-command-executor.ts +2 -2
  219. package/src/cli/repositories/registry.ts +77 -11
  220. package/src/cli/repositories/structured-git.ts +62 -2
  221. package/src/cli/repository/session-cache.ts +92 -6
  222. package/src/runtime/context/assistant-context.ts +20 -7
  223. package/src/runtime/context/assistant-work-context.ts +89 -27
  224. package/src/runtime/context/automatic-learning.ts +565 -0
  225. package/src/runtime/context/cognitive-attention.ts +173 -0
  226. package/src/runtime/context/cognitive-skill-canary.ts +158 -0
  227. package/src/runtime/context/context-closure.ts +46 -9
  228. package/src/runtime/context/context-pack-process.ts +103 -0
  229. package/src/runtime/context/context-plane.ts +1 -9
  230. package/src/runtime/context/direct-controller-learning.ts +298 -0
  231. package/src/runtime/context/semantic-navigation-contract.ts +9 -0
  232. package/src/runtime/context/semantic-navigation.ts +6 -34
  233. package/src/runtime/context/typescript-navigation-process.ts +145 -0
  234. package/src/runtime/context/typescript-navigation.ts +89 -3
  235. package/src/runtime/control-plane/cleanup-artifact-retention.ts +212 -9
  236. package/src/runtime/control-plane/concurrency/work-execution-concurrency.ts +9 -47
  237. package/src/runtime/control-plane/execution/controller-authority-recovery.ts +34 -4
  238. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +29 -20
  239. package/src/runtime/control-plane/execution/execution-identity.ts +23 -3
  240. package/src/runtime/control-plane/execution/repository-work-attribution.ts +7 -30
  241. package/src/runtime/control-plane/execution/retained-work-resume.ts +206 -199
  242. package/src/runtime/control-plane/execution/session-store.ts +31 -1
  243. package/src/runtime/control-plane/execution/validation.ts +6 -8
  244. package/src/runtime/control-plane/execution/work-authority-reconciler.ts +13 -42
  245. package/src/runtime/control-plane/execution/work-check-evidence.ts +137 -0
  246. package/src/runtime/control-plane/execution/work-completion-authority.ts +30 -85
  247. package/src/runtime/control-plane/execution/work-delivery-service.ts +497 -0
  248. package/src/runtime/control-plane/execution/work-evidence-policy.ts +12 -50
  249. package/src/runtime/control-plane/execution/work-execution-support.ts +50 -149
  250. package/src/runtime/control-plane/execution/work-handle-authority.ts +188 -109
  251. package/src/runtime/control-plane/execution/work-handle-store.ts +19 -43
  252. package/src/runtime/control-plane/execution/work-head-settlement.ts +53 -20
  253. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +52 -29
  254. package/src/runtime/control-plane/execution/work-operation-service.ts +43 -27
  255. package/src/runtime/control-plane/execution/work-preparation-service.ts +126 -266
  256. package/src/runtime/control-plane/execution/work-remote-delivery.ts +2 -3
  257. package/src/runtime/control-plane/execution/work-revision-diff.ts +39 -0
  258. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +173 -186
  259. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +28 -84
  260. package/src/runtime/control-plane/execution/work-verification-service.ts +406 -50
  261. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
  262. package/src/runtime/control-plane/facade/capability-registry.ts +362 -37
  263. package/src/runtime/control-plane/facade/check-normalization.ts +3 -9
  264. package/src/runtime/control-plane/facade/codex-delegation.ts +10 -11
  265. package/src/runtime/control-plane/facade/controller-connection.ts +135 -0
  266. package/src/runtime/control-plane/facade/goal-workloop-access.ts +1 -20
  267. package/src/runtime/control-plane/facade/goal-workloop.ts +281 -1753
  268. package/src/runtime/control-plane/facade/handoff-inbox-application.ts +12 -25
  269. package/src/runtime/control-plane/facade/handoff-inbox-store.ts +256 -52
  270. package/src/runtime/control-plane/facade/index.ts +0 -1
  271. package/src/runtime/control-plane/facade/operation-digest.ts +12 -15
  272. package/src/runtime/control-plane/facade/plan-contract-store.ts +451 -991
  273. package/src/runtime/control-plane/facade/policy-gate.ts +0 -23
  274. package/src/runtime/control-plane/facade/repository-work-admission.ts +12 -22
  275. package/src/runtime/control-plane/facade/requirement-authority.ts +102 -199
  276. package/src/runtime/control-plane/facade/requirement-board.ts +3 -3
  277. package/src/runtime/control-plane/facade/rh-work-operation-contract.ts +51 -14
  278. package/src/runtime/control-plane/facade/self-healing-loop.ts +18 -53
  279. package/src/runtime/control-plane/facade/suggested-actions.ts +15 -18
  280. package/src/runtime/control-plane/facade/types.ts +49 -108
  281. package/src/runtime/control-plane/facade/work-continuation.ts +8 -8
  282. package/src/runtime/control-plane/generated-cache-retention.ts +97 -1
  283. package/src/runtime/control-plane/global-scheduler/autonomous-continuation.ts +248 -0
  284. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +1 -1
  285. package/src/runtime/control-plane/global-scheduler/maintenance.ts +239 -112
  286. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +2 -1
  287. package/src/runtime/control-plane/global-scheduler/scheduler.ts +66 -40
  288. package/src/runtime/control-plane/global-scheduler/wake-signal.ts +42 -2
  289. package/src/runtime/control-plane/governance/access-policy.ts +19 -2
  290. package/src/runtime/control-plane/governance/authorization.ts +20 -3
  291. package/src/runtime/control-plane/launcher/chatgpt-round-continuation.ts +63 -300
  292. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +28 -300
  293. package/src/runtime/control-plane/launcher/local-conversation-controller.ts +238 -0
  294. package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +5 -2
  295. package/src/runtime/control-plane/launcher/thin-launcher.ts +96 -240
  296. package/src/runtime/control-plane/persistence/cognition-store.ts +820 -0
  297. package/src/runtime/control-plane/persistence/experience-store.ts +162 -29
  298. package/src/runtime/control-plane/persistence/requirement-store.ts +216 -74
  299. package/src/runtime/control-plane/persistence/sqlite-store.ts +113 -6
  300. package/src/runtime/control-plane/release-retention.ts +135 -47
  301. package/src/runtime/control-plane/repository-namespace-retention.ts +2 -2
  302. package/src/runtime/control-plane/runtime-cleanup.ts +290 -21
  303. package/src/runtime/control-plane/workspace/project-onboarding.ts +80 -0
  304. package/src/runtime/control-plane/workspace/semantic-sync-service.ts +19 -5
  305. package/src/runtime/control-plane/workspace/workspace-store.ts +169 -0
  306. package/src/runtime/diagnostics/incident-repair.ts +313 -146
  307. package/src/runtime/diagnostics/mcp-timing.ts +114 -0
  308. package/src/runtime/evidence/artifact-store.ts +158 -40
  309. package/src/runtime/evidence/evidence-store.ts +112 -8
  310. package/src/runtime/evidence/output-handle-index.ts +225 -0
  311. package/src/runtime/evidence/result-store.ts +158 -44
  312. package/src/runtime/execution/jobs/store.ts +3 -4
  313. package/src/runtime/execution/managed-workspace.ts +57 -0
  314. package/src/runtime/execution/process-runtime/check-facade.ts +1 -1
  315. package/src/runtime/execution/process-runtime/check-result-retention.ts +2 -2
  316. package/src/runtime/execution/process-runtime/check-result.ts +105 -1
  317. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +18 -8
  318. package/src/runtime/execution/process-runtime/command-facade.ts +79 -27
  319. package/src/runtime/execution/process-runtime/gc.ts +15 -2
  320. package/src/runtime/execution/process-runtime/handle-index.ts +176 -0
  321. package/src/runtime/execution/process-runtime/host-command-lane.ts +139 -0
  322. package/src/runtime/execution/process-runtime/index.ts +3 -0
  323. package/src/runtime/execution/process-runtime/lightweight-managed.ts +251 -72
  324. package/src/runtime/execution/process-runtime/persisted-check.ts +3 -3
  325. package/src/runtime/execution/process-runtime/process-runner-entry.ts +47 -5
  326. package/src/runtime/execution/process-runtime/process-scope.ts +160 -0
  327. package/src/runtime/execution/process-runtime/resource-claims.ts +65 -61
  328. package/src/runtime/execution/process-runtime/runtime.ts +35 -2
  329. package/src/runtime/execution/process-runtime/store.ts +30 -2
  330. package/src/runtime/execution/process-runtime/types.ts +6 -1
  331. package/src/runtime/execution/workers/executor.ts +2 -6
  332. package/src/runtime/platform/service-inventory.ts +130 -0
  333. package/src/runtime/platform/service-manager.ts +8 -0
  334. package/src/runtime/plugins/action-application.ts +85 -28
  335. package/src/runtime/plugins/app-store-connect-adapter.ts +38 -14
  336. package/src/runtime/plugins/browser-adapter.ts +757 -34
  337. package/src/runtime/plugins/browser-automation-service.ts +23 -3
  338. package/src/runtime/plugins/browser-macos-bridge.ts +321 -29
  339. package/src/runtime/plugins/browser-manifest-surface.ts +45 -11
  340. package/src/runtime/plugins/browser-native-session-service.ts +63 -13
  341. package/src/runtime/plugins/browser-registration.ts +16 -0
  342. package/src/runtime/plugins/browser-runtime.ts +14 -1
  343. package/src/runtime/plugins/browser-session-authority.ts +2 -2
  344. package/src/runtime/plugins/browser-session-store.ts +46 -76
  345. package/src/runtime/plugins/capability-authorization-grants.ts +258 -62
  346. package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +219 -0
  347. package/src/runtime/plugins/computer-chatgpt-macos-target.ts +891 -0
  348. package/src/runtime/plugins/computer-registration.ts +222 -11
  349. package/src/runtime/plugins/desktop-operator-external-policy.ts +36 -4
  350. package/src/runtime/plugins/desktop-operator-registration.ts +12 -4
  351. package/src/runtime/plugins/external-adapter.ts +42 -6
  352. package/src/runtime/plugins/external-registration.ts +21 -1
  353. package/src/runtime/plugins/gmail-adapter.ts +23 -2
  354. package/src/runtime/plugins/google-calendar-adapter.ts +2 -2
  355. package/src/runtime/plugins/google-shared.ts +14 -7
  356. package/src/runtime/plugins/google-tasks-adapter.ts +2 -2
  357. package/src/runtime/plugins/ios-adapter.ts +1 -1
  358. package/src/runtime/plugins/lightweight-action.ts +208 -2
  359. package/src/runtime/plugins/local-system-adapter.ts +12 -2
  360. package/src/runtime/plugins/managed-process-adapter.ts +12 -5
  361. package/src/runtime/plugins/plugin-action-sidecar.ts +17 -13
  362. package/src/runtime/plugins/plugin-management-adapter.ts +1 -1
  363. package/src/runtime/plugins/store.ts +602 -428
  364. package/src/runtime/plugins/types.ts +20 -0
  365. package/src/runtime/plugins/xiaohongshu-publish.ts +16 -0
  366. package/src/runtime/projections/invalidation.ts +7 -3
  367. package/src/runtime/projections/materialized-view.ts +80 -36
  368. package/src/runtime/recovery/actions.ts +3 -3
  369. package/src/runtime/recovery/capability-action-application.ts +46 -45
  370. package/src/runtime/recovery/lifecycle-inventory.ts +29 -29
  371. package/src/runtime/recovery/maintenance-executor.ts +374 -123
  372. package/src/runtime/recovery/store.ts +29 -11
  373. package/src/runtime/release/release-coordinator.ts +296 -0
  374. package/src/runtime/release/release-session.ts +716 -0
  375. package/src/runtime/release/release-state-migration.ts +25 -0
  376. package/src/runtime/resources/leases/store.ts +56 -25
  377. package/src/runtime/root/computer-composition.ts +191 -6
  378. package/src/runtime/root/controller-progression-composition.ts +192 -0
  379. package/src/runtime/root/controller-round-composition.ts +20 -1
  380. package/src/runtime/root/deployment-topology.ts +106 -0
  381. package/src/runtime/root/entry.ts +4 -3
  382. package/src/runtime/root/known-good-recovery.ts +184 -0
  383. package/src/runtime/root/local-bridge.ts +0 -1
  384. package/src/runtime/root/mcp-transport.ts +111 -18
  385. package/src/runtime/root/ownership.ts +261 -9
  386. package/src/runtime/root/package-connector-service.ts +105 -19
  387. package/src/runtime/root/package-runtime-release.ts +117 -18
  388. package/src/runtime/root/package-runtime-service.ts +17 -11
  389. package/src/runtime/root/release-execution-canary.ts +44 -12
  390. package/src/runtime/root/release-loader.ts +90 -8
  391. package/src/runtime/root/release-manifest.ts +80 -6
  392. package/src/runtime/root/release-materialize.ts +372 -28
  393. package/src/runtime/root/release-store.ts +395 -36
  394. package/src/runtime/root/runtime-lane.ts +191 -0
  395. package/src/runtime/root/runtime.ts +159 -27
  396. package/src/runtime/root/scheduled-controller-composition.ts +28 -3
  397. package/src/runtime/root/service-runner.ts +2 -2
  398. package/src/runtime/root/service.ts +36 -33
  399. package/src/runtime/root/status.ts +48 -0
  400. package/src/runtime/root/types.ts +32 -3
  401. package/src/runtime/root/workflow-supervisor-composition.ts +493 -0
  402. package/src/runtime/root/workflow-supervisor-runtime.ts +209 -0
  403. package/src/runtime/root/write-fence.ts +50 -2
  404. package/src/runtime/safe-tooling/google-credential-store.ts +76 -21
  405. package/src/runtime/safe-tooling/google-oauth-broker.ts +25 -7
  406. package/src/runtime/safe-tooling/workspace-auth.ts +1 -1
  407. package/src/runtime/shared/bounded-child-supervisor.ts +9 -1
  408. package/src/runtime/shared/process-environment.ts +20 -9
  409. package/src/runtime/shared/response-budget.ts +1 -3
  410. package/src/runtime/standalone-recovery/controller-home-migration.ts +16 -24
  411. package/src/runtime/standalone-recovery/core.ts +3702 -342
  412. package/src/runtime/standalone-recovery/entry.ts +469 -39
  413. package/src/runtime/standalone-recovery/installer.ts +262 -148
  414. package/src/runtime/standalone-recovery/mcp-server.ts +29 -193
  415. package/src/runtime/standalone-recovery/operation-lock.ts +1 -1
  416. package/src/runtime/standalone-recovery/performance.ts +54 -16
  417. package/src/runtime/standalone-recovery/release.ts +53 -5
  418. package/src/runtime/standalone-recovery/service-labels.ts +5 -2
  419. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +5 -2
  420. package/src/runtime/workflow/schedules/browser-probe.ts +17 -6
  421. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +10 -3
  422. package/src/runtime/workflow/schedules/engine.ts +138 -172
  423. package/src/runtime/workflow/schedules/work-continuation.ts +44 -5
  424. package/src/runtime/workflows/runtime.ts +15 -19
  425. package/src/testing/test-governance.ts +5 -1
  426. package/supervisor/chatgpt-conversation.ts +23 -0
  427. package/supervisor/client.ts +201 -0
  428. package/supervisor/control-plane.ts +868 -0
  429. package/supervisor/entry.ts +15 -0
  430. package/supervisor/forge-validators.ts +80 -0
  431. package/supervisor/index.ts +5 -0
  432. package/supervisor/native-browser-adapter.ts +786 -0
  433. package/supervisor/native-messaging/host.ts +39 -0
  434. package/supervisor/native-messaging/manifest.ts +26 -0
  435. package/supervisor/native-messaging/protocol.ts +27 -0
  436. package/supervisor/paths.ts +18 -0
  437. package/supervisor/protocol.ts +202 -0
  438. package/supervisor/server.ts +404 -0
  439. package/supervisor/store.ts +1451 -0
  440. package/supervisor/tsconfig.json +8 -0
  441. package/supervisor/types.ts +255 -0
  442. package/adapters/chatgpt/controller-host.ts +0 -119
  443. package/adapters/mcp/controller-round-compatibility.ts +0 -184
  444. package/adapters/mcp/frozen-client-semantic-compatibility.ts +0 -292
  445. package/adapters/mcp/runtime-gateway/work-compat-adapter.ts +0 -502
  446. package/packages/kernel/progression/api/index.ts +0 -2
  447. package/packages/kernel/progression/application/projector.ts +0 -211
  448. package/packages/kernel/progression/domain/types.ts +0 -116
  449. package/packages/kernel/progression/index.ts +0 -1
  450. package/packages/kernel/scheduler/application/continuation-service.ts +0 -212
  451. package/packages/kernel/scheduler/domain/continuation.ts +0 -38
  452. package/packages/kernel/scheduler/infrastructure/continuation-dispatch-store.ts +0 -38
  453. package/packages/kernel/work/domain/admission-policy.ts +0 -34
  454. package/scripts/benchmark-background-check-overlap.ts +0 -228
  455. package/scripts/benchmark-computer-hotpath.ts +0 -255
  456. package/scripts/benchmark-controller-hotpath.ts +0 -212
  457. package/scripts/benchmark-controller-round2.ts +0 -191
  458. package/scripts/benchmark-ios-agent-device.ts +0 -511
  459. package/scripts/benchmark-mcp-host.sh +0 -117
  460. package/scripts/benchmark-mcp-transport.ts +0 -169
  461. package/scripts/benchmark-operational-memory-activation.ts +0 -235
  462. package/scripts/benchmark-operational-shadow.ts +0 -28
  463. package/scripts/benchmark-quality-harness.ts +0 -146
  464. package/scripts/benchmark-route-session-concurrency.ts +0 -915
  465. package/scripts/benchmark-thin-harness-gateway-ab.ts +0 -632
  466. package/scripts/benchmark-thin-harness-mcp-e2e.ts +0 -498
  467. package/scripts/benchmark-thin-harness.ts +0 -251
  468. package/scripts/benchmark-typescript-navigation.ts +0 -186
  469. package/scripts/benchmark-work-efficiency.ts +0 -334
  470. package/scripts/build-controller-ui.ts +0 -28
  471. package/scripts/check-npm-release.sh +0 -32
  472. package/scripts/check-public-release-surface.sh +0 -19
  473. package/scripts/check-release-published.sh +0 -57
  474. package/scripts/check-release-readiness.sh +0 -37
  475. package/scripts/check-release-version.mjs +0 -74
  476. package/scripts/check-tarball-install-smoke.sh +0 -150
  477. package/scripts/package-source-archive.sh +0 -147
  478. package/scripts/public-release-files.txt +0 -39
  479. package/scripts/public-release.ts +0 -174
  480. package/scripts/publish-release-tarball.sh +0 -27
  481. package/scripts/route-nl-vs-ts-eval.ts +0 -679
  482. package/scripts/stage-runtime-release.ts +0 -44
  483. package/src/cli/controller/progress.ts +0 -656
  484. package/src/cli/controller/work-mode.ts +0 -181
  485. package/src/cli/local-bridge/console-assets.ts +0 -17
  486. package/src/cli/local-bridge/dashboard.ts +0 -17
  487. package/src/cli/local-bridge/frontend/src/App.tsx +0 -10
  488. package/src/cli/local-bridge/frontend/src/components/Button.tsx +0 -1
  489. package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +0 -3
  490. package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +0 -1
  491. package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +0 -1
  492. package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +0 -1
  493. package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +0 -3
  494. package/src/cli/local-bridge/frontend/src/design/icons.tsx +0 -11
  495. package/src/cli/local-bridge/frontend/src/design/tokens.css +0 -8
  496. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +0 -153
  497. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +0 -37
  498. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +0 -183
  499. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +0 -2
  500. package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +0 -2
  501. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +0 -4
  502. package/src/cli/local-bridge/frontend/src/lib/api.ts +0 -30
  503. package/src/cli/local-bridge/frontend/src/lib/format.ts +0 -4
  504. package/src/cli/local-bridge/frontend/src/lib/routes.ts +0 -7
  505. package/src/cli/local-bridge/frontend/src/main.tsx +0 -1
  506. package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +0 -1
  507. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +0 -2
  508. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +0 -4
  509. package/src/cli/local-bridge/frontend/src/styles.css +0 -28
  510. package/src/cli/local-bridge/frontend/src/types.ts +0 -15
  511. package/src/cli/local-bridge/frontend/src/vite-env.d.ts +0 -1
  512. package/src/cli/local-bridge/frontend/vite.config.ts +0 -27
  513. package/src/cli/local-bridge/ui-dist/app.css +0 -1
  514. package/src/cli/local-bridge/ui-dist/app.js +0 -11228
  515. package/src/runtime/control-plane/execution/content-equivalent-commit-authority.ts +0 -169
  516. package/src/runtime/control-plane/execution/direct-canonical-work-reconciliation.ts +0 -85
  517. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +0 -655
  518. package/src/runtime/control-plane/execution/work-finalization-service.ts +0 -2850
  519. package/src/runtime/control-plane/execution/work-path-scope.ts +0 -37
  520. package/src/runtime/control-plane/execution/work-task-receipt.ts +0 -468
  521. package/src/runtime/control-plane/facade/controller-round-relay.ts +0 -3
  522. package/src/runtime/control-plane/facade/work-admission-policy.ts +0 -142
  523. package/src/runtime/control-plane/facade/work-implementation-review.ts +0 -2
  524. package/src/runtime/control-plane/facade/work-state-machine.ts +0 -2
  525. package/src/runtime/control-plane/facade/work-task-projection.ts +0 -66
  526. package/src/runtime/control-plane/routing/index.ts +0 -1
  527. package/src/runtime/control-plane/routing/route-policy.ts +0 -401
  528. package/src/runtime/control-plane/routing/workspace-admission.ts +0 -70
@@ -1,6 +1,9 @@
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,
@@ -10,6 +13,8 @@ import type {
10
13
  AssistantPluginManifest,
11
14
  } from './types';
12
15
  import { AssistantPluginError, toAssistantPluginError } from './errors';
16
+ import { listCanonicalGrants } from '../../../packages/kernel/identity/api/index';
17
+ import { resolveRepoPreferredControllerHome } from '../../cli/repositories/controller-home';
13
18
  import { readRepositoryPluginConfig, writeRepositoryPluginConfig, type RepositoryPluginConfigContext } from './config-store';
14
19
  import {
15
20
  browserActions,
@@ -107,12 +112,12 @@ type WaitForSelectorState = 'attached' | 'detached' | 'visible' | 'hidden';
107
112
  type BrowserChannel = 'chromium' | 'chrome' | 'chrome-beta' | 'chrome-dev' | 'chrome-canary';
108
113
  type BrowserNativeAttachMode = 'auto' | 'disabled';
109
114
 
110
- const CURRENT_BROWSER_CONFIG_SCHEMA_VERSION = 2 as const;
115
+ const CURRENT_BROWSER_CONFIG_SCHEMA_VERSION = 3 as const;
111
116
  const DEFAULT_USER_BROWSER_CHANNEL: BrowserChannel = 'chrome';
112
- const DEFAULT_USER_NATIVE_BROWSER_CANDIDATES: MacOsBrowserProduct[] = ['chrome'];
117
+ const DEFAULT_USER_NATIVE_BROWSER_CANDIDATES: MacOsBrowserProduct[] = ['chrome', 'vivaldi'];
113
118
 
114
119
  interface BrowserPluginConfig {
115
- schemaVersion: 2;
120
+ schemaVersion: 3;
116
121
  enabled: boolean;
117
122
  provider: 'playwright';
118
123
  browserMode?: BrowserMode;
@@ -131,7 +136,7 @@ interface BrowserPluginConfig {
131
136
  }
132
137
 
133
138
  type PersistedBrowserPluginConfig = Partial<Omit<BrowserPluginConfig, 'schemaVersion'>> & {
134
- schemaVersion?: 1 | 2;
139
+ schemaVersion?: 1 | 2 | 3;
135
140
  };
136
141
 
137
142
  type BrowserSessionInventory = {
@@ -175,15 +180,109 @@ interface BrowserProfileSelection {
175
180
  selectedProfilePath: string;
176
181
  }
177
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
+ }
178
273
  type BrowserContextLike = {
179
274
  pages(): PageLike[];
180
275
  newPage(): Promise<PageLike>;
181
276
  close(): Promise<void>;
277
+ browser?(): BrowserLike | null;
278
+ serviceWorkers?(): BrowserExtensionTargetLike[];
279
+ backgroundPages?(): BrowserExtensionTargetLike[];
182
280
  };
183
281
 
184
282
  type BrowserLike = {
185
283
  contexts(): BrowserContextLike[];
186
284
  newContext?(): Promise<BrowserContextLike>;
285
+ newBrowserCDPSession?(): Promise<BrowserCdpSessionLike>;
187
286
  close?(): Promise<void>;
188
287
  disconnect?(): Promise<void> | void;
189
288
  };
@@ -257,10 +356,56 @@ interface BrowserPluginRuntimeHooks {
257
356
  loadPlaywright(repoRoot?: string): PlaywrightRuntime;
258
357
  fetchJson(url: string, timeoutMs: number): Promise<unknown>;
259
358
  activateNativeBrowserApplication(input: AssistantPluginActionExecutionInput, product: MacOsBrowserProduct): Promise<void>;
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>;
260
369
  }
261
370
 
262
371
  const defaultRuntimeHooks: BrowserPluginRuntimeHooks = {
263
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,
264
409
  moduleAvailable: (name: string, repoRoot?: string) => {
265
410
  const anchors = [repoRoot ? join(repoRoot, 'package.json') : undefined, import.meta.url]
266
411
  .filter((value): value is string => Boolean(value));
@@ -313,6 +458,7 @@ interface ManagedBrowserContextState {
313
458
  }
314
459
 
315
460
  const managedBrowserContexts = new Map<string, Promise<ManagedBrowserContextState>>();
461
+ const managedExtensionPaths = new Map<string, Set<string>>();
316
462
 
317
463
  export function setBrowserPluginRuntimeHooksForTest(hooks: Partial<BrowserPluginRuntimeHooks>): void {
318
464
  runtimeHooks = { ...defaultRuntimeHooks, ...hooks };
@@ -323,6 +469,7 @@ export function resetBrowserPluginRuntimeHooksForTest(): void {
323
469
  runtimeHooks = { ...defaultRuntimeHooks };
324
470
  runtimeHooksCustomized = false;
325
471
  managedBrowserContexts.clear();
472
+ managedExtensionPaths.clear();
326
473
  }
327
474
 
328
475
  function now(): string {
@@ -426,42 +573,46 @@ function resolveProfileSelection(repoRoot: string, profileDir: string, profileDi
426
573
 
427
574
  function normalizeConfig(raw: PersistedBrowserPluginConfig): BrowserPluginConfig {
428
575
  const normalizedProfileDir = stringValue(raw.profileDir);
576
+ const normalizedExecutablePath = stringValue(raw.executablePath);
577
+ const normalizedBrowserChannel = browserChannel(raw.browserChannel);
429
578
  return {
430
579
  schemaVersion: CURRENT_BROWSER_CONFIG_SCHEMA_VERSION,
431
580
  enabled: raw.enabled === true,
432
581
  provider: 'playwright',
433
- browserMode: browserMode(raw.browserMode) ?? 'attach_preferred',
582
+ browserMode: browserMode(raw.browserMode) ?? 'managed_persistent',
434
583
  profileMode: browserProfileMode(raw.profileMode) ?? (normalizedProfileDir ? 'custom' : 'repo_local'),
435
584
  profileDir: normalizedProfileDir,
436
585
  profileDirectory: stringValue(raw.profileDirectory),
437
- browserChannel: browserChannel(raw.browserChannel) ?? DEFAULT_USER_BROWSER_CHANNEL,
438
- executablePath: stringValue(raw.executablePath),
586
+ browserChannel: normalizedBrowserChannel ?? (normalizedExecutablePath ? undefined : DEFAULT_USER_BROWSER_CHANNEL),
587
+ executablePath: normalizedExecutablePath,
439
588
  cdpEndpoint: stringValue(raw.cdpEndpoint),
440
589
  cdpEndpointCandidates: stringList(raw.cdpEndpointCandidates)?.slice(0, MAX_BROWSER_CDP_ENDPOINT_CANDIDATES),
441
590
  cdpDiscoveryTimeoutMs: typeof raw.cdpDiscoveryTimeoutMs === 'number'
442
591
  ? Math.min(positiveNumber(raw.cdpDiscoveryTimeoutMs, DEFAULT_CDP_DISCOVERY_TIMEOUT_MS), MAX_CDP_DISCOVERY_TIMEOUT_MS)
443
592
  : undefined,
444
593
  cdpAttachFallback: browserCdpAttachFallback(raw.cdpAttachFallback) ?? 'fail_closed',
445
- nativeAttachMode: browserNativeAttachMode(raw.nativeAttachMode) ?? 'auto',
594
+ nativeAttachMode: browserNativeAttachMode(raw.nativeAttachMode) ?? 'disabled',
446
595
  nativeBrowserCandidates: browserProductList(raw.nativeBrowserCandidates) ?? [...DEFAULT_USER_NATIVE_BROWSER_CANDIDATES],
447
596
  defaultTimeoutMs: typeof raw.defaultTimeoutMs === 'number' ? positiveNumber(raw.defaultTimeoutMs, DEFAULT_TIMEOUT_MS) : undefined,
448
597
  };
449
598
  }
450
599
 
451
600
  function migrateLegacyRepositoryConfig(raw: PersistedBrowserPluginConfig): PersistedBrowserPluginConfig {
452
- 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';
453
607
  return {
608
+ ...raw,
454
609
  schemaVersion: CURRENT_BROWSER_CONFIG_SCHEMA_VERSION,
455
- enabled: raw.enabled,
456
- provider: 'playwright',
457
- browserMode: 'attach_preferred',
458
- profileMode: 'repo_local',
459
- browserChannel: DEFAULT_USER_BROWSER_CHANNEL,
460
- cdpAttachFallback: 'fail_closed',
461
- nativeAttachMode: 'auto',
462
- nativeBrowserCandidates: [...DEFAULT_USER_NATIVE_BROWSER_CANDIDATES],
463
- defaultTimeoutMs: raw.defaultTimeoutMs,
464
- 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',
465
616
  };
466
617
  }
467
618
 
@@ -490,7 +641,13 @@ async function resolveBrowserPluginAuthorizationContextInternal(
490
641
  input: AssistantPluginActionExecutionInput,
491
642
  ): Promise<AssistantPluginAuthorizationContext | undefined> {
492
643
  const action = browserActions().find((entry) => entry.actionId === input.actionId);
493
- if (!action || action.confirmation !== 'authorization' || !action.scopes.includes('browser.interact')) return undefined;
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;
494
651
 
495
652
  const persistedConfig = loadConfig(input.repoRoot, input);
496
653
  const sessionId = stringValue(input.args.session_id);
@@ -499,7 +656,24 @@ async function resolveBrowserPluginAuthorizationContextInternal(
499
656
  const session = findBrowserSession(input.repoRoot, sessionId);
500
657
  const config = effectiveBrowserActionConfig(persistedConfig, input.args, session);
501
658
  const connection = session?.browser;
502
- 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
+ }
503
677
 
504
678
  let origin: string;
505
679
  try {
@@ -1056,7 +1230,7 @@ interface BrowserOpenHandle {
1056
1230
 
1057
1231
  interface PageDiagnostics {
1058
1232
  consoleErrors: Array<{ type: string; text: string }>;
1059
- failedRequests: Array<{ url: string; status?: number; failure?: string }>;
1233
+ failedRequests: Array<{ url: string; method?: string; status?: number; failure?: string }>;
1060
1234
  navigation?: { url: string; status?: number };
1061
1235
  }
1062
1236
 
@@ -1074,18 +1248,21 @@ function attachDiagnostics(page: PageLike): PageDiagnostics {
1074
1248
  }
1075
1249
  });
1076
1250
  page.on('requestfailed', (request) => {
1077
- const entry = request as { url?: () => string; failure?: () => { errorText?: string } | null };
1251
+ const entry = request as { url?: () => string; method?: () => string; failure?: () => { errorText?: string } | null };
1078
1252
  diagnostics.failedRequests.push({
1079
1253
  url: typeof entry.url === 'function' ? entry.url() : '',
1254
+ method: typeof entry.method === 'function' ? entry.method() : undefined,
1080
1255
  failure: typeof entry.failure === 'function' ? entry.failure()?.errorText : undefined,
1081
1256
  });
1082
1257
  });
1083
1258
  page.on('response', (response) => {
1084
- const entry = response as { url?: () => string; status?: () => number; ok?: () => boolean };
1259
+ const entry = response as { url?: () => string; status?: () => number; ok?: () => boolean; request?: () => { method?: () => string } };
1085
1260
  const status = typeof entry.status === 'function' ? entry.status() : undefined;
1086
1261
  if (typeof status === 'number' && status >= 400) {
1262
+ const request = typeof entry.request === 'function' ? entry.request() : undefined;
1087
1263
  diagnostics.failedRequests.push({
1088
1264
  url: typeof entry.url === 'function' ? entry.url() : '',
1265
+ method: typeof request?.method === 'function' ? request.method() : undefined,
1089
1266
  status,
1090
1267
  });
1091
1268
  }
@@ -1544,14 +1721,71 @@ async function discoverCdpEndpoint(endpoint: string, timeoutMs: number): Promise
1544
1721
  };
1545
1722
  }
1546
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
+
1547
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
+ : [];
1548
1781
  return {
1549
1782
  headless: false,
1550
1783
  acceptDownloads: true,
1551
1784
  viewport: { width: 1280, height: 900 },
1552
1785
  ...(config.executablePath ? { executablePath: resolveConfiguredPath(repoRoot, config.executablePath) } : {}),
1553
- ...(!config.executablePath && config.browserChannel && config.browserChannel !== 'chromium' ? { channel: config.browserChannel } : {}),
1554
- ...(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 } : {}),
1555
1789
  };
1556
1790
  }
1557
1791
 
@@ -1571,8 +1805,87 @@ async function evictManagedContext(key: string): Promise<void> {
1571
1805
  }
1572
1806
  }
1573
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
+
1574
1882
  async function closeManagedContextsForRepo(repoRoot: string, options: { strict?: boolean } = {}): Promise<void> {
1575
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
+ });
1576
1889
  for (const [key, pending] of [...managedBrowserContexts.entries()]) {
1577
1890
  let state: ManagedBrowserContextState;
1578
1891
  try {
@@ -1884,6 +2197,342 @@ async function openManagedContext(
1884
2197
  };
1885
2198
  }
1886
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
+
1887
2536
  async function openAttachedContext(
1888
2537
  runtime: PlaywrightRuntime,
1889
2538
  repoRoot: string,
@@ -2112,6 +2761,7 @@ async function openNativeAttachedContext(
2112
2761
  zeroWindowAttempt.product,
2113
2762
  target.url,
2114
2763
  discovered.attempts,
2764
+ undefined,
2115
2765
  timeout,
2116
2766
  );
2117
2767
  page = created.page as unknown as PageLike;
@@ -2816,11 +3466,15 @@ const EXTRACTION_SCRIPTS = {
2816
3466
  })()`,
2817
3467
  };
2818
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
+
2819
3473
  function selectorRepairHint(selector: string, errorMessage: string): string {
2820
3474
  if (/strict mode violation|resolved to \d+ elements/i.test(errorMessage)) {
2821
3475
  return `Selector "${selector}" matched multiple elements. Prefer a unique #id, [data-testid], or more specific path.`;
2822
3476
  }
2823
- if (/Timeout|waiting for selector|not found/i.test(errorMessage)) {
3477
+ if (selectorUnavailableInTime(errorMessage)) {
2824
3478
  return `Selector "${selector}" was not found in time. Use snapshot_interactive or query_all to discover stable selectors, then retry.`;
2825
3479
  }
2826
3480
  return `Check selector "${selector}" against the current page structure.`;
@@ -3232,7 +3886,7 @@ export function buildBrowserPluginManifest(previousRevision = 0, previousUpdated
3232
3886
  pluginId: BROWSER_PLUGIN_ID,
3233
3887
  provider: 'local-browser',
3234
3888
  displayName: 'Controller Browser Plugin',
3235
- pluginVersion: '1.1.0',
3889
+ pluginVersion: '1.2.0',
3236
3890
  authority: {
3237
3891
  strategy: 'derived',
3238
3892
  duplicateStateAllowed: false,
@@ -3364,7 +4018,7 @@ async function executeBrowserPluginActionInternal(
3364
4018
  profileMode: nextProfileMode,
3365
4019
  profileDir: nextProfileMode === 'repo_local' ? undefined : nextProfileDir,
3366
4020
  profileDirectory: nextProfileMode === 'repo_local' ? undefined : nextProfileDirectory,
3367
- browserChannel: nextBrowserChannel ?? 'chromium',
4021
+ browserChannel: nextBrowserChannel ?? (nextExecutablePath ? undefined : 'chromium'),
3368
4022
  executablePath: nextExecutablePath,
3369
4023
  cdpEndpoint: nextCdpEndpoint,
3370
4024
  cdpEndpointCandidates: nextCdpEndpointCandidates,
@@ -3385,6 +4039,71 @@ async function executeBrowserPluginActionInternal(
3385
4039
  await closeManagedContextsForRepo(input.repoRoot);
3386
4040
  return { config, health: health(config, input.repoRoot) };
3387
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
+ }
3388
4107
  case 'list_sessions': {
3389
4108
  const inventory = await inspectSavedSessions(input.repoRoot, current);
3390
4109
  const requestedLimit = Math.max(1, Math.min(Math.trunc(positiveNumber(input.args.limit, DEFAULT_BROWSER_SESSION_LIST_LIMIT)), MAX_BROWSER_SESSION_LIST_LIMIT));
@@ -4245,10 +4964,14 @@ async function executeBrowserPluginActionInternal(
4245
4964
  });
4246
4965
  } catch (error) {
4247
4966
  const message = error instanceof Error ? error.message : String(error);
4248
- throw new AssistantPluginError('PLUGIN_ACTION_FAILED', message, {
4249
- retryable: true,
4250
- details: { selector, repairHint: selectorRepairHint(selector, message) },
4251
- });
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
+ );
4252
4975
  }
4253
4976
  }
4254
4977
  case 'type':