@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.
- package/AGENTS.md +70 -7
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +17 -2
- package/CONTRIBUTING.md +3 -2
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/adapters/chatgpt/browser-delivery-host.ts +7 -1
- package/adapters/chatgpt/browser-delivery-runtime.ts +529 -56
- package/adapters/chatgpt/controller-binding-store.ts +101 -22
- package/adapters/chatgpt/controller-round-host.ts +15 -25
- package/adapters/chatgpt/controller-round-settlement-store.ts +58 -4
- package/adapters/chatgpt/provider-delivery.ts +228 -1
- package/adapters/chatgpt/work-conversation-binding-store.ts +79 -53
- package/adapters/computer/chatgpt-extension-target.ts +463 -0
- package/adapters/computer/chatgpt-target-router.ts +103 -0
- package/adapters/computer/chrome-extension/background.js +385 -0
- package/adapters/computer/chrome-extension/content.js +378 -0
- package/adapters/computer/chrome-extension/core.js +24 -0
- package/adapters/computer/chrome-extension/manifest.json +30 -0
- package/adapters/computer/desktop-operator-discovery.ts +11 -31
- package/adapters/computer/interaction-target-authority.ts +142 -18
- package/adapters/mcp/auth.ts +0 -1
- package/adapters/mcp/instructions.ts +5 -4
- package/adapters/mcp/multi-repository.ts +3 -19
- package/adapters/mcp/oauth.ts +73 -0
- package/adapters/mcp/paths.ts +4 -1
- package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +87 -0
- package/adapters/mcp/runtime-gateway/bounded-result-runtime.ts +62 -0
- package/adapters/mcp/runtime-gateway/cognition-settlement.ts +201 -0
- package/adapters/mcp/runtime-gateway/context-adapter.ts +411 -73
- package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +91 -0
- package/adapters/mcp/runtime-gateway/controller-authority-adapter.ts +20 -5
- package/adapters/mcp/runtime-gateway/core-capability-adapter.ts +258 -0
- package/adapters/mcp/runtime-gateway/engineering-preconditions.ts +10 -27
- package/adapters/mcp/runtime-gateway/engineering-tool-contract.ts +15 -0
- package/adapters/mcp/runtime-gateway/execution-tools.ts +8 -42
- package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +2 -1
- package/adapters/mcp/runtime-gateway/plugin-adapter.ts +117 -45
- package/adapters/mcp/runtime-gateway/process-tools.ts +190 -22
- package/adapters/mcp/runtime-gateway/protected-computer-adapter.ts +16 -4
- package/adapters/mcp/runtime-gateway/recovery-adapter.ts +69 -52
- package/adapters/mcp/runtime-gateway/repository-compat-adapter.ts +4 -35
- package/adapters/mcp/runtime-gateway/result-adapter.ts +10 -4
- package/adapters/mcp/runtime-gateway/router.ts +18 -504
- package/adapters/mcp/runtime-gateway/routing-policy.ts +499 -0
- package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +14 -65
- package/adapters/mcp/runtime-gateway/runtime-readiness-observation.ts +218 -0
- package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +113 -173
- package/adapters/mcp/runtime-gateway/runtime-tools.ts +15 -5
- package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +1 -0
- package/adapters/mcp/runtime-gateway/shared-adapter.ts +20 -0
- package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +358 -266
- package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +136 -0
- package/adapters/mcp/runtime-gateway/work-adapter.ts +261 -3122
- package/adapters/mcp/runtime-gateway/work-controller-operations.ts +257 -0
- package/adapters/mcp/runtime-gateway/work-detail-projection.ts +60 -0
- package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +246 -0
- package/adapters/mcp/runtime-gateway/work-learning-operations.ts +145 -0
- package/adapters/mcp/runtime-gateway/work-list-projection.ts +38 -0
- package/adapters/mcp/runtime-gateway/work-plan-operations.ts +382 -0
- package/adapters/mcp/runtime-gateway/work-repair-adapter.ts +308 -0
- package/adapters/mcp/runtime-gateway/work-requirement-operations.ts +148 -0
- package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +263 -0
- package/adapters/mcp/runtime-gateway/work-workflow-operations.ts +113 -0
- package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +156 -0
- package/adapters/mcp/server.ts +67 -23
- package/adapters/mcp/setup.ts +1 -3
- package/adapters/mcp/tool-mapping/legacy-tool-service.ts +44 -170
- package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +4 -9
- package/adapters/mcp/tool-mapping/repository-tools.ts +133 -142
- package/adapters/mcp/toolset-names.ts +5 -0
- package/adapters/mcp/toolset.ts +6 -5
- package/adapters/mcp/transports/http-observation.ts +365 -0
- package/adapters/mcp/transports/http.ts +154 -677
- package/adapters/mcp/transports/oauth-http.ts +322 -0
- package/adapters/mcp/transports/session-registry.ts +6 -0
- package/adapters/mcp/tunnels/openai-secure-tunnel.ts +89 -5
- package/assets/plugin-registry.v1.json +6 -4
- package/assets/reference-configs/agentic-development-flow.md +25 -0
- package/assets/reference-configs/global-working-rules.md +19 -0
- package/assets/skill-version.json +2 -2
- package/docs/architecture/CURRENT.md +111 -41
- package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +14 -5
- package/docs/architecture/decisions/20260823-immediate-controller-relay.md +6 -3
- package/docs/architecture/decisions/20260907-assistant-learning-loop.md +2 -0
- package/docs/architecture/decisions/20260907-controller-round-transition-policy.md +19 -4
- package/docs/architecture/decisions/20260916-workflow-supervisor-authority.md +249 -0
- package/docs/architecture/decisions/20260917-cognitive-memory-foundation.md +82 -0
- package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +221 -0
- package/docs/architecture/decisions/20260919-autonomous-work-liveness.md +62 -0
- package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +72 -0
- package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +237 -0
- package/docs/architecture/decisions/20261005-forge-v3-desktop-client-boundary.md +77 -0
- package/docs/architecture/domains/verification.md +1 -0
- package/docs/architecture/domains/workflow-engine.md +6 -0
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
- package/docs/forge-plugin-management.md +2 -2
- package/docs/operations/controller-performance-and-502.md +17 -0
- package/docs/operations/controller-reliability-runbook.md +2 -2
- package/docs/operations/features.md +1 -1
- package/docs/operations/features.zh-CN.md +1 -1
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/mcp-tool-exposure.md +2 -0
- package/docs/operations/releasing.md +2 -0
- package/docs/operations/standalone-disaster-recovery.md +14 -6
- package/docs/wiki/Implementation.md +1 -1
- package/package.json +25 -14
- package/packages/kernel/cognition/api/index.ts +6 -0
- package/packages/kernel/cognition/application/activation-service.ts +451 -0
- package/packages/kernel/cognition/application/consolidation-service.ts +185 -0
- package/packages/kernel/cognition/application/legacy-experience-adapter.ts +38 -0
- package/packages/kernel/cognition/application/memory-service.ts +64 -0
- package/packages/kernel/cognition/domain/learning.ts +106 -0
- package/packages/kernel/cognition/domain/memory.ts +254 -0
- package/packages/kernel/controller/api/index.ts +1 -0
- package/packages/kernel/controller/application/continuation-service.ts +281 -0
- package/packages/kernel/controller/application/controller-round-service.ts +12 -0
- package/packages/kernel/controller/domain/controller-round-transition-policy.ts +242 -21
- package/packages/kernel/controller/domain/controller-round.ts +4 -1
- package/packages/kernel/controller/domain/execution-quality.ts +3 -1
- package/packages/kernel/controller/infrastructure/controller-round-store.ts +689 -103
- package/packages/kernel/controller/infrastructure/controller-session-store.ts +297 -71
- package/packages/kernel/controller/ports/controller-host.ts +2 -0
- package/packages/kernel/identity/api/index.ts +10 -0
- package/packages/kernel/identity/domain/activity-projection.ts +43 -0
- package/packages/kernel/identity/domain/capability-broker.ts +60 -0
- package/packages/kernel/identity/domain/grant.ts +72 -0
- package/packages/kernel/identity/domain/owned-resource.ts +116 -0
- package/packages/kernel/identity/domain/user-request.ts +93 -0
- package/packages/kernel/identity/infrastructure/activity-projection-store.ts +77 -0
- package/packages/kernel/identity/infrastructure/capability-broker.ts +81 -0
- package/packages/kernel/identity/infrastructure/grant-store.ts +284 -0
- package/packages/kernel/identity/infrastructure/owned-resource-store.ts +259 -0
- package/packages/kernel/identity/infrastructure/user-request-store.ts +182 -0
- package/packages/kernel/scheduler/api/index.ts +0 -2
- package/packages/kernel/scheduler/application/schedule-service.ts +3 -0
- package/packages/kernel/scheduler/application/settlement.ts +0 -15
- package/packages/kernel/scheduler/domain/schedule.ts +87 -0
- package/packages/kernel/scheduler/infrastructure/schedule-store.ts +153 -133
- package/packages/kernel/work/api/index.ts +3 -2
- package/packages/kernel/work/application/work-service.ts +17 -42
- package/packages/kernel/work/domain/current-task-lineage.ts +104 -0
- package/packages/kernel/work/domain/engineering-design.ts +10 -5
- package/packages/kernel/work/domain/engineering-profile.ts +8 -12
- package/packages/kernel/work/domain/evaluation-promotion-receipt.ts +186 -0
- package/packages/kernel/work/domain/execution-concurrency.ts +19 -26
- package/packages/kernel/work/domain/execution-snapshot.ts +0 -31
- package/packages/kernel/work/domain/implementation-review.ts +14 -331
- package/packages/kernel/work/domain/objective-graph.ts +119 -0
- package/packages/kernel/work/domain/project-engineering-contract.ts +23 -1
- package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
- package/packages/kernel/work/domain/state-machine.ts +25 -63
- package/packages/kernel/work/domain/types.ts +57 -51
- package/packages/kernel/work/infrastructure/work-contract-store.ts +819 -728
- package/packages/kernel/work/ports/work-contract-store.ts +5 -2
- package/packages/plugin-runtime/browser/session-authority.ts +2 -1
- package/packages/plugin-runtime/computer/chatgpt-identity.ts +24 -0
- package/packages/plugin-runtime/computer/conversation-target.ts +143 -0
- package/packages/plugin-runtime/computer/index.ts +2 -0
- package/packages/plugin-runtime/computer/target-authority.ts +26 -1
- package/packages/plugin-runtime/external/unix-jsonl-transport.ts +142 -18
- package/packages/protocols/computer/contract.ts +27 -1
- package/scripts/activate-source-baseline.ts +7 -9
- package/scripts/canary-controller-semantic-wait.ts +12 -24
- package/scripts/check-mcp-compatibility.ts +48 -136
- package/scripts/check-runtime-architecture.mjs +622 -217
- package/scripts/forge-local-recovery-helper.mjs +93 -15
- package/scripts/lib/project-init-lib.sh +3 -0
- package/scripts/open-source-audit-allowlist.txt +1 -2
- package/scripts/run-bounded-command.mjs +5 -0
- package/scripts/run-governed-gate.ts +0 -1
- package/scripts/run-mobile-monitor-bridge.ts +0 -1
- package/scripts/smoke-control-plane-failure-domain.ts +94 -0
- package/scripts/smoke-mcp-http-runtime.ts +5 -0
- package/scripts/smoke-runtime-recovery.ts +12 -43
- package/scripts/smoke-schedule-engine.ts +23 -15
- package/scripts/smoke-unattended-transport-recovery.ts +223 -0
- package/scripts/smoke-user-request-continuation-restart.ts +232 -0
- package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +92 -0
- package/scripts/smoke-workflow-supervisor-process-restart.ts +276 -0
- package/scripts/smoke-workflow-supervisor-restart-recovery.ts +150 -0
- package/scripts/smoke-workflow-supervisor.ts +103 -0
- package/scripts/verify-forge-runtime.sh +2 -1
- package/src/cli/agent-jobs/executor-health.ts +11 -34
- package/src/cli/agent-jobs/integration.ts +18 -49
- package/src/cli/agent-jobs/job-manager.ts +7 -3
- package/src/cli/commands/brain-assistant.ts +27 -13
- package/src/cli/commands/brain.ts +13 -15
- package/src/cli/commands/chatgpt.ts +1 -163
- package/src/cli/commands/recovery.ts +135 -121
- package/src/cli/commands/runtime.ts +3 -3
- package/src/cli/commands/setup-profile.ts +8 -0
- package/src/cli/controller/check-runner.ts +13 -2
- package/src/cli/controller/completion-backlog.ts +3 -3
- package/src/cli/controller/completion-orchestrator.ts +4 -43
- package/src/cli/controller/completion-target.ts +3 -72
- package/src/cli/controller/context/source-materializer.ts +81 -100
- package/src/cli/controller/context-pack.ts +10 -8
- package/src/cli/controller/execution-completion.ts +4 -7
- package/src/cli/controller/execution-policy.ts +36 -153
- package/src/cli/controller/governance.ts +2 -3
- package/src/cli/controller/issue-store.ts +4 -99
- package/src/cli/controller/launch-agents.ts +59 -18
- package/src/cli/controller/operational-plan.ts +24 -51
- package/src/cli/controller/runtime-config.ts +5 -1
- package/src/cli/controller/stuck-state-migration.ts +2 -3
- package/src/cli/controller/systemd-user.ts +39 -1
- package/src/cli/editing/edit-session.ts +1 -7
- package/src/cli/local-bridge/console-automations.ts +18 -13
- package/src/cli/local-bridge/console-view-models.ts +3 -12
- package/src/cli/local-bridge/facade-api.ts +64 -191
- package/src/cli/local-bridge/server.ts +24 -147
- package/src/cli/local-bridge/types.ts +0 -1
- package/src/cli/mcp/oauth-client-retirement.ts +74 -0
- package/src/cli/repositories/command-executor.ts +29 -23
- package/src/cli/repositories/command-scope.ts +35 -16
- package/src/cli/repositories/controller-home.ts +48 -0
- package/src/cli/repositories/ephemeral-workspace.ts +20 -30
- package/src/cli/repositories/git-command-executor.ts +2 -2
- package/src/cli/repositories/registry.ts +77 -11
- package/src/cli/repositories/structured-git.ts +62 -2
- package/src/cli/repository/session-cache.ts +92 -6
- package/src/runtime/context/assistant-context.ts +20 -7
- package/src/runtime/context/assistant-work-context.ts +89 -27
- package/src/runtime/context/automatic-learning.ts +565 -0
- package/src/runtime/context/cognitive-attention.ts +173 -0
- package/src/runtime/context/cognitive-skill-canary.ts +158 -0
- package/src/runtime/context/context-closure.ts +46 -9
- package/src/runtime/context/context-pack-process.ts +103 -0
- package/src/runtime/context/context-plane.ts +1 -9
- package/src/runtime/context/direct-controller-learning.ts +298 -0
- package/src/runtime/context/semantic-navigation-contract.ts +9 -0
- package/src/runtime/context/semantic-navigation.ts +6 -34
- package/src/runtime/context/typescript-navigation-process.ts +145 -0
- package/src/runtime/context/typescript-navigation.ts +89 -3
- package/src/runtime/control-plane/cleanup-artifact-retention.ts +212 -9
- package/src/runtime/control-plane/concurrency/work-execution-concurrency.ts +9 -47
- package/src/runtime/control-plane/execution/controller-authority-recovery.ts +34 -4
- package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +29 -20
- package/src/runtime/control-plane/execution/execution-identity.ts +23 -3
- package/src/runtime/control-plane/execution/repository-work-attribution.ts +7 -30
- package/src/runtime/control-plane/execution/retained-work-resume.ts +206 -199
- package/src/runtime/control-plane/execution/session-store.ts +31 -1
- package/src/runtime/control-plane/execution/validation.ts +6 -8
- package/src/runtime/control-plane/execution/work-authority-reconciler.ts +13 -42
- package/src/runtime/control-plane/execution/work-check-evidence.ts +137 -0
- package/src/runtime/control-plane/execution/work-completion-authority.ts +30 -85
- package/src/runtime/control-plane/execution/work-delivery-service.ts +497 -0
- package/src/runtime/control-plane/execution/work-evidence-policy.ts +12 -50
- package/src/runtime/control-plane/execution/work-execution-support.ts +50 -149
- package/src/runtime/control-plane/execution/work-handle-authority.ts +188 -109
- package/src/runtime/control-plane/execution/work-handle-store.ts +19 -43
- package/src/runtime/control-plane/execution/work-head-settlement.ts +53 -20
- package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +52 -29
- package/src/runtime/control-plane/execution/work-operation-service.ts +43 -27
- package/src/runtime/control-plane/execution/work-preparation-service.ts +126 -266
- package/src/runtime/control-plane/execution/work-remote-delivery.ts +2 -3
- package/src/runtime/control-plane/execution/work-revision-diff.ts +39 -0
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +173 -186
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +28 -84
- package/src/runtime/control-plane/execution/work-verification-service.ts +406 -50
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
- package/src/runtime/control-plane/facade/capability-registry.ts +362 -37
- package/src/runtime/control-plane/facade/check-normalization.ts +3 -9
- package/src/runtime/control-plane/facade/codex-delegation.ts +10 -11
- package/src/runtime/control-plane/facade/controller-connection.ts +135 -0
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +1 -20
- package/src/runtime/control-plane/facade/goal-workloop.ts +281 -1753
- package/src/runtime/control-plane/facade/handoff-inbox-application.ts +12 -25
- package/src/runtime/control-plane/facade/handoff-inbox-store.ts +256 -52
- package/src/runtime/control-plane/facade/index.ts +0 -1
- package/src/runtime/control-plane/facade/operation-digest.ts +12 -15
- package/src/runtime/control-plane/facade/plan-contract-store.ts +451 -991
- package/src/runtime/control-plane/facade/policy-gate.ts +0 -23
- package/src/runtime/control-plane/facade/repository-work-admission.ts +12 -22
- package/src/runtime/control-plane/facade/requirement-authority.ts +102 -199
- package/src/runtime/control-plane/facade/requirement-board.ts +3 -3
- package/src/runtime/control-plane/facade/rh-work-operation-contract.ts +51 -14
- package/src/runtime/control-plane/facade/self-healing-loop.ts +18 -53
- package/src/runtime/control-plane/facade/suggested-actions.ts +15 -18
- package/src/runtime/control-plane/facade/types.ts +49 -108
- package/src/runtime/control-plane/facade/work-continuation.ts +8 -8
- package/src/runtime/control-plane/generated-cache-retention.ts +97 -1
- package/src/runtime/control-plane/global-scheduler/autonomous-continuation.ts +248 -0
- package/src/runtime/control-plane/global-scheduler/durable-admission.ts +1 -1
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +239 -112
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +2 -1
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +66 -40
- package/src/runtime/control-plane/global-scheduler/wake-signal.ts +42 -2
- package/src/runtime/control-plane/governance/access-policy.ts +19 -2
- package/src/runtime/control-plane/governance/authorization.ts +20 -3
- package/src/runtime/control-plane/launcher/chatgpt-round-continuation.ts +63 -300
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +28 -300
- package/src/runtime/control-plane/launcher/local-conversation-controller.ts +238 -0
- package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +5 -2
- package/src/runtime/control-plane/launcher/thin-launcher.ts +96 -240
- package/src/runtime/control-plane/persistence/cognition-store.ts +820 -0
- package/src/runtime/control-plane/persistence/experience-store.ts +162 -29
- package/src/runtime/control-plane/persistence/requirement-store.ts +216 -74
- package/src/runtime/control-plane/persistence/sqlite-store.ts +113 -6
- package/src/runtime/control-plane/release-retention.ts +135 -47
- package/src/runtime/control-plane/repository-namespace-retention.ts +2 -2
- package/src/runtime/control-plane/runtime-cleanup.ts +290 -21
- package/src/runtime/control-plane/workspace/project-onboarding.ts +80 -0
- package/src/runtime/control-plane/workspace/semantic-sync-service.ts +19 -5
- package/src/runtime/control-plane/workspace/workspace-store.ts +169 -0
- package/src/runtime/diagnostics/incident-repair.ts +313 -146
- package/src/runtime/diagnostics/mcp-timing.ts +114 -0
- package/src/runtime/evidence/artifact-store.ts +158 -40
- package/src/runtime/evidence/evidence-store.ts +112 -8
- package/src/runtime/evidence/output-handle-index.ts +225 -0
- package/src/runtime/evidence/result-store.ts +158 -44
- package/src/runtime/execution/jobs/store.ts +3 -4
- package/src/runtime/execution/managed-workspace.ts +57 -0
- package/src/runtime/execution/process-runtime/check-facade.ts +1 -1
- package/src/runtime/execution/process-runtime/check-result-retention.ts +2 -2
- package/src/runtime/execution/process-runtime/check-result.ts +105 -1
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +18 -8
- package/src/runtime/execution/process-runtime/command-facade.ts +79 -27
- package/src/runtime/execution/process-runtime/gc.ts +15 -2
- package/src/runtime/execution/process-runtime/handle-index.ts +176 -0
- package/src/runtime/execution/process-runtime/host-command-lane.ts +139 -0
- package/src/runtime/execution/process-runtime/index.ts +3 -0
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +251 -72
- package/src/runtime/execution/process-runtime/persisted-check.ts +3 -3
- package/src/runtime/execution/process-runtime/process-runner-entry.ts +47 -5
- package/src/runtime/execution/process-runtime/process-scope.ts +160 -0
- package/src/runtime/execution/process-runtime/resource-claims.ts +65 -61
- package/src/runtime/execution/process-runtime/runtime.ts +35 -2
- package/src/runtime/execution/process-runtime/store.ts +30 -2
- package/src/runtime/execution/process-runtime/types.ts +6 -1
- package/src/runtime/execution/workers/executor.ts +2 -6
- package/src/runtime/platform/service-inventory.ts +130 -0
- package/src/runtime/platform/service-manager.ts +8 -0
- package/src/runtime/plugins/action-application.ts +85 -28
- package/src/runtime/plugins/app-store-connect-adapter.ts +38 -14
- package/src/runtime/plugins/browser-adapter.ts +757 -34
- package/src/runtime/plugins/browser-automation-service.ts +23 -3
- package/src/runtime/plugins/browser-macos-bridge.ts +321 -29
- package/src/runtime/plugins/browser-manifest-surface.ts +45 -11
- package/src/runtime/plugins/browser-native-session-service.ts +63 -13
- package/src/runtime/plugins/browser-registration.ts +16 -0
- package/src/runtime/plugins/browser-runtime.ts +14 -1
- package/src/runtime/plugins/browser-session-authority.ts +2 -2
- package/src/runtime/plugins/browser-session-store.ts +46 -76
- package/src/runtime/plugins/capability-authorization-grants.ts +258 -62
- package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +219 -0
- package/src/runtime/plugins/computer-chatgpt-macos-target.ts +891 -0
- package/src/runtime/plugins/computer-registration.ts +222 -11
- package/src/runtime/plugins/desktop-operator-external-policy.ts +36 -4
- package/src/runtime/plugins/desktop-operator-registration.ts +12 -4
- package/src/runtime/plugins/external-adapter.ts +42 -6
- package/src/runtime/plugins/external-registration.ts +21 -1
- package/src/runtime/plugins/gmail-adapter.ts +23 -2
- package/src/runtime/plugins/google-calendar-adapter.ts +2 -2
- package/src/runtime/plugins/google-shared.ts +14 -7
- package/src/runtime/plugins/google-tasks-adapter.ts +2 -2
- package/src/runtime/plugins/ios-adapter.ts +1 -1
- package/src/runtime/plugins/lightweight-action.ts +208 -2
- package/src/runtime/plugins/local-system-adapter.ts +12 -2
- package/src/runtime/plugins/managed-process-adapter.ts +12 -5
- package/src/runtime/plugins/plugin-action-sidecar.ts +17 -13
- package/src/runtime/plugins/plugin-management-adapter.ts +1 -1
- package/src/runtime/plugins/store.ts +602 -428
- package/src/runtime/plugins/types.ts +20 -0
- package/src/runtime/plugins/xiaohongshu-publish.ts +16 -0
- package/src/runtime/projections/invalidation.ts +7 -3
- package/src/runtime/projections/materialized-view.ts +80 -36
- package/src/runtime/recovery/actions.ts +3 -3
- package/src/runtime/recovery/capability-action-application.ts +46 -45
- package/src/runtime/recovery/lifecycle-inventory.ts +29 -29
- package/src/runtime/recovery/maintenance-executor.ts +374 -123
- package/src/runtime/recovery/store.ts +29 -11
- package/src/runtime/release/release-coordinator.ts +296 -0
- package/src/runtime/release/release-session.ts +716 -0
- package/src/runtime/release/release-state-migration.ts +25 -0
- package/src/runtime/resources/leases/store.ts +56 -25
- package/src/runtime/root/computer-composition.ts +191 -6
- package/src/runtime/root/controller-progression-composition.ts +192 -0
- package/src/runtime/root/controller-round-composition.ts +20 -1
- package/src/runtime/root/deployment-topology.ts +106 -0
- package/src/runtime/root/entry.ts +4 -3
- package/src/runtime/root/known-good-recovery.ts +184 -0
- package/src/runtime/root/local-bridge.ts +0 -1
- package/src/runtime/root/mcp-transport.ts +111 -18
- package/src/runtime/root/ownership.ts +261 -9
- package/src/runtime/root/package-connector-service.ts +105 -19
- package/src/runtime/root/package-runtime-release.ts +117 -18
- package/src/runtime/root/package-runtime-service.ts +17 -11
- package/src/runtime/root/release-execution-canary.ts +44 -12
- package/src/runtime/root/release-loader.ts +90 -8
- package/src/runtime/root/release-manifest.ts +80 -6
- package/src/runtime/root/release-materialize.ts +372 -28
- package/src/runtime/root/release-store.ts +395 -36
- package/src/runtime/root/runtime-lane.ts +191 -0
- package/src/runtime/root/runtime.ts +159 -27
- package/src/runtime/root/scheduled-controller-composition.ts +28 -3
- package/src/runtime/root/service-runner.ts +2 -2
- package/src/runtime/root/service.ts +36 -33
- package/src/runtime/root/status.ts +48 -0
- package/src/runtime/root/types.ts +32 -3
- package/src/runtime/root/workflow-supervisor-composition.ts +493 -0
- package/src/runtime/root/workflow-supervisor-runtime.ts +209 -0
- package/src/runtime/root/write-fence.ts +50 -2
- package/src/runtime/safe-tooling/google-credential-store.ts +76 -21
- package/src/runtime/safe-tooling/google-oauth-broker.ts +25 -7
- package/src/runtime/safe-tooling/workspace-auth.ts +1 -1
- package/src/runtime/shared/bounded-child-supervisor.ts +9 -1
- package/src/runtime/shared/process-environment.ts +20 -9
- package/src/runtime/shared/response-budget.ts +1 -3
- package/src/runtime/standalone-recovery/controller-home-migration.ts +16 -24
- package/src/runtime/standalone-recovery/core.ts +3702 -342
- package/src/runtime/standalone-recovery/entry.ts +469 -39
- package/src/runtime/standalone-recovery/installer.ts +262 -148
- package/src/runtime/standalone-recovery/mcp-server.ts +29 -193
- package/src/runtime/standalone-recovery/operation-lock.ts +1 -1
- package/src/runtime/standalone-recovery/performance.ts +54 -16
- package/src/runtime/standalone-recovery/release.ts +53 -5
- package/src/runtime/standalone-recovery/service-labels.ts +5 -2
- package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +5 -2
- package/src/runtime/workflow/schedules/browser-probe.ts +17 -6
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +10 -3
- package/src/runtime/workflow/schedules/engine.ts +138 -172
- package/src/runtime/workflow/schedules/work-continuation.ts +44 -5
- package/src/runtime/workflows/runtime.ts +15 -19
- package/src/testing/test-governance.ts +5 -1
- package/supervisor/chatgpt-conversation.ts +23 -0
- package/supervisor/client.ts +201 -0
- package/supervisor/control-plane.ts +868 -0
- package/supervisor/entry.ts +15 -0
- package/supervisor/forge-validators.ts +80 -0
- package/supervisor/index.ts +5 -0
- package/supervisor/native-browser-adapter.ts +786 -0
- package/supervisor/native-messaging/host.ts +39 -0
- package/supervisor/native-messaging/manifest.ts +26 -0
- package/supervisor/native-messaging/protocol.ts +27 -0
- package/supervisor/paths.ts +18 -0
- package/supervisor/protocol.ts +202 -0
- package/supervisor/server.ts +404 -0
- package/supervisor/store.ts +1451 -0
- package/supervisor/tsconfig.json +8 -0
- package/supervisor/types.ts +255 -0
- package/adapters/chatgpt/controller-host.ts +0 -119
- package/adapters/mcp/controller-round-compatibility.ts +0 -184
- package/adapters/mcp/frozen-client-semantic-compatibility.ts +0 -292
- package/adapters/mcp/runtime-gateway/work-compat-adapter.ts +0 -502
- package/packages/kernel/progression/api/index.ts +0 -2
- package/packages/kernel/progression/application/projector.ts +0 -211
- package/packages/kernel/progression/domain/types.ts +0 -116
- package/packages/kernel/progression/index.ts +0 -1
- package/packages/kernel/scheduler/application/continuation-service.ts +0 -212
- package/packages/kernel/scheduler/domain/continuation.ts +0 -38
- package/packages/kernel/scheduler/infrastructure/continuation-dispatch-store.ts +0 -38
- package/packages/kernel/work/domain/admission-policy.ts +0 -34
- package/scripts/benchmark-background-check-overlap.ts +0 -228
- package/scripts/benchmark-computer-hotpath.ts +0 -255
- package/scripts/benchmark-controller-hotpath.ts +0 -212
- package/scripts/benchmark-controller-round2.ts +0 -191
- package/scripts/benchmark-ios-agent-device.ts +0 -511
- package/scripts/benchmark-mcp-host.sh +0 -117
- package/scripts/benchmark-mcp-transport.ts +0 -169
- package/scripts/benchmark-operational-memory-activation.ts +0 -235
- package/scripts/benchmark-operational-shadow.ts +0 -28
- package/scripts/benchmark-quality-harness.ts +0 -146
- package/scripts/benchmark-route-session-concurrency.ts +0 -915
- package/scripts/benchmark-thin-harness-gateway-ab.ts +0 -632
- package/scripts/benchmark-thin-harness-mcp-e2e.ts +0 -498
- package/scripts/benchmark-thin-harness.ts +0 -251
- package/scripts/benchmark-typescript-navigation.ts +0 -186
- package/scripts/benchmark-work-efficiency.ts +0 -334
- package/scripts/build-controller-ui.ts +0 -28
- package/scripts/check-npm-release.sh +0 -32
- package/scripts/check-public-release-surface.sh +0 -19
- package/scripts/check-release-published.sh +0 -57
- package/scripts/check-release-readiness.sh +0 -37
- package/scripts/check-release-version.mjs +0 -74
- package/scripts/check-tarball-install-smoke.sh +0 -150
- package/scripts/package-source-archive.sh +0 -147
- package/scripts/public-release-files.txt +0 -39
- package/scripts/public-release.ts +0 -174
- package/scripts/publish-release-tarball.sh +0 -27
- package/scripts/route-nl-vs-ts-eval.ts +0 -679
- package/scripts/stage-runtime-release.ts +0 -44
- package/src/cli/controller/progress.ts +0 -656
- package/src/cli/controller/work-mode.ts +0 -181
- package/src/cli/local-bridge/console-assets.ts +0 -17
- package/src/cli/local-bridge/dashboard.ts +0 -17
- package/src/cli/local-bridge/frontend/src/App.tsx +0 -10
- package/src/cli/local-bridge/frontend/src/components/Button.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +0 -3
- package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +0 -3
- package/src/cli/local-bridge/frontend/src/design/icons.tsx +0 -11
- package/src/cli/local-bridge/frontend/src/design/tokens.css +0 -8
- package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +0 -153
- package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +0 -37
- package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +0 -183
- package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +0 -2
- package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +0 -2
- package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +0 -4
- package/src/cli/local-bridge/frontend/src/lib/api.ts +0 -30
- package/src/cli/local-bridge/frontend/src/lib/format.ts +0 -4
- package/src/cli/local-bridge/frontend/src/lib/routes.ts +0 -7
- package/src/cli/local-bridge/frontend/src/main.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +0 -2
- package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +0 -4
- package/src/cli/local-bridge/frontend/src/styles.css +0 -28
- package/src/cli/local-bridge/frontend/src/types.ts +0 -15
- package/src/cli/local-bridge/frontend/src/vite-env.d.ts +0 -1
- package/src/cli/local-bridge/frontend/vite.config.ts +0 -27
- package/src/cli/local-bridge/ui-dist/app.css +0 -1
- package/src/cli/local-bridge/ui-dist/app.js +0 -11228
- package/src/runtime/control-plane/execution/content-equivalent-commit-authority.ts +0 -169
- package/src/runtime/control-plane/execution/direct-canonical-work-reconciliation.ts +0 -85
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +0 -655
- package/src/runtime/control-plane/execution/work-finalization-service.ts +0 -2850
- package/src/runtime/control-plane/execution/work-path-scope.ts +0 -37
- package/src/runtime/control-plane/execution/work-task-receipt.ts +0 -468
- package/src/runtime/control-plane/facade/controller-round-relay.ts +0 -3
- package/src/runtime/control-plane/facade/work-admission-policy.ts +0 -142
- package/src/runtime/control-plane/facade/work-implementation-review.ts +0 -2
- package/src/runtime/control-plane/facade/work-state-machine.ts +0 -2
- package/src/runtime/control-plane/facade/work-task-projection.ts +0 -66
- package/src/runtime/control-plane/routing/index.ts +0 -1
- package/src/runtime/control-plane/routing/route-policy.ts +0 -401
- 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 {
|
|
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 =
|
|
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:
|
|
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) ?? '
|
|
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:
|
|
438
|
-
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) ?? '
|
|
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
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
|
|
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')
|
|
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
|
-
...(
|
|
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 (
|
|
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.
|
|
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(
|
|
4249
|
-
|
|
4250
|
-
|
|
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':
|