@moretea-labs/forge 1.8.0 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +73 -7
- package/CHANGELOG.md +34 -0
- package/CLAUDE.md +17 -2
- package/CONTRIBUTING.md +3 -2
- package/README.en.md +24 -3
- package/README.md +10 -6
- package/README.zh-CN.md +5 -3
- package/THIRD_PARTY_NOTICES.md +1 -2
- package/adapters/chatgpt/browser-delivery-host.ts +7 -1
- package/adapters/chatgpt/browser-delivery-runtime.ts +531 -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 +229 -2
- package/adapters/chatgpt/work-conversation-binding-store.ts +99 -54
- package/adapters/computer/chatgpt-extension-target.ts +708 -0
- package/adapters/computer/chrome-extension/background.js +541 -0
- package/adapters/computer/chrome-extension/content.js +402 -0
- package/adapters/computer/chrome-extension/core.js +24 -0
- package/adapters/computer/chrome-extension/manifest.json +32 -0
- package/adapters/computer/desktop-operator-discovery.ts +11 -31
- package/adapters/computer/desktop-operator-provider.ts +18 -6
- package/adapters/computer/interaction-target-authority.ts +178 -24
- package/adapters/mcp/access-mode.ts +18 -39
- package/adapters/mcp/auth.ts +2 -7
- package/adapters/mcp/instructions.ts +6 -5
- package/adapters/mcp/multi-repository.ts +10 -47
- package/adapters/mcp/oauth.ts +73 -0
- package/adapters/mcp/paths.ts +4 -1
- 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 +490 -113
- package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +83 -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 +14 -54
- package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +2 -1
- package/adapters/mcp/runtime-gateway/plugin-adapter.ts +132 -47
- package/adapters/mcp/runtime-gateway/process-tools.ts +173 -23
- 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 +19 -507
- package/adapters/mcp/runtime-gateway/routing-policy.ts +506 -0
- package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +15 -66
- package/adapters/mcp/runtime-gateway/runtime-readiness-observation.ts +218 -0
- package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +123 -178
- package/adapters/mcp/runtime-gateway/runtime-tools.ts +11 -5
- package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +37 -1
- package/adapters/mcp/runtime-gateway/shared-adapter.ts +20 -0
- package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +416 -270
- package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +140 -0
- package/adapters/mcp/runtime-gateway/work-adapter.ts +262 -3123
- 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 +229 -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 +320 -0
- package/adapters/mcp/runtime-gateway/work-workflow-operations.ts +113 -0
- package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +180 -0
- package/adapters/mcp/server.ts +91 -26
- package/adapters/mcp/setup.ts +5 -13
- package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
- package/adapters/mcp/tool-mapping/legacy-tool-service.ts +703 -3092
- package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +5 -10
- package/adapters/mcp/tool-mapping/repository-tools.ts +170 -213
- package/adapters/mcp/toolset-names.ts +11 -37
- package/adapters/mcp/toolset.ts +19 -92
- package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
- package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
- package/adapters/mcp/transports/http-observation.ts +377 -0
- package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
- package/adapters/mcp/transports/http.ts +200 -683
- 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/adapters/mcp/types.ts +0 -6
- package/assets/hooks/lib/session-state.sh +0 -38
- package/assets/hooks/post-bash.sh +1 -1
- package/assets/hooks/post-tool-observer.sh +0 -5
- package/assets/hooks/prompt-guard.sh +0 -55
- package/assets/partials/04-project-structure.partial.md +1 -1
- package/assets/partials/07-footer.partial.md +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +1 -1
- package/assets/partials-agents/08-deep-docs.partial.md +1 -1
- package/assets/plugin-registry.v1.json +6 -4
- package/assets/reference-configs/agentic-development-flow.md +26 -1
- package/assets/reference-configs/external-tooling.md +0 -76
- package/assets/reference-configs/global-working-rules.md +19 -0
- package/assets/reference-configs/release-deploy.md +1 -1
- package/assets/templates/helpers/architecture-queue.sh +1 -1
- package/assets/templates/helpers/check-agent-tooling.sh +1 -327
- package/assets/templates/helpers/check-task-workflow.sh +1 -1
- package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
- package/assets/templates/helpers/migrate-project-template.sh +2 -2
- package/assets/workflow-contract.v1.json +0 -2
- package/docs/README.md +5 -2
- package/docs/architecture/CURRENT.md +205 -51
- 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/20260904-ios-device-migration-baseline.md +42 -0
- 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 +234 -0
- package/docs/architecture/decisions/20260919-autonomous-work-liveness.md +62 -0
- package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +74 -0
- package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +342 -0
- package/docs/architecture/decisions/20261005-forge-v3-desktop-client-boundary.md +77 -0
- package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
- package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
- package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
- package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
- package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
- package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
- package/docs/architecture/domains/public-surface.md +3 -5
- package/docs/architecture/domains/verification.md +6 -5
- package/docs/architecture/domains/workflow-engine.md +6 -0
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +3 -3
- package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
- package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
- package/docs/forge-plugin-management.md +16 -4
- package/docs/operations/controller-browser-plugin.md +23 -0
- package/docs/operations/controller-performance-and-502.md +17 -0
- package/docs/operations/controller-reliability-runbook.md +2 -2
- package/docs/operations/features.md +6 -7
- package/docs/operations/features.zh-CN.md +1 -1
- package/docs/operations/github-repository.md +4 -4
- package/docs/operations/github-semantic-release-projection.md +20 -0
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/mcp-tool-exposure.md +13 -52
- package/docs/operations/releasing.md +2 -0
- package/docs/operations/runtime-performance-diagnostics.md +8 -0
- package/docs/operations/standalone-disaster-recovery.md +15 -7
- package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
- package/docs/tutorials/03-first-repository-task.md +2 -0
- package/docs/wiki/Computer-and-Plugins.md +78 -0
- package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
- package/docs/wiki/Home.md +16 -2
- package/docs/wiki/Implementation.md +25 -13
- package/docs/wiki/Installation.md +28 -32
- package/docs/wiki/Integrations.md +4 -0
- package/docs/wiki/Quick-Start.md +35 -22
- package/docs/wiki/Releases-and-Upgrades.md +15 -25
- package/docs/wiki/_Sidebar.md +5 -1
- package/package.json +25 -25
- 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 +348 -97
- 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 +87 -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/eligibility.ts +4 -1
- 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 +166 -143
- package/packages/kernel/work/api/index.ts +3 -2
- package/packages/kernel/work/application/work-service.ts +18 -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 +34 -65
- package/packages/kernel/work/domain/types.ts +57 -51
- package/packages/kernel/work/infrastructure/work-contract-store.ts +847 -729
- package/packages/kernel/work/ports/work-contract-store.ts +5 -2
- package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
- 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 +153 -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/assemble-template.ts +11 -72
- package/scripts/canary-controller-semantic-wait.ts +12 -24
- package/scripts/check-mcp-compatibility.ts +94 -201
- package/scripts/check-package-identity.mjs +0 -5
- package/scripts/check-runtime-architecture.mjs +921 -239
- package/scripts/forge-local-recovery-helper.mjs +93 -15
- package/scripts/init-project.sh +0 -46
- package/scripts/install-ios-hid-toolchain.sh +60 -0
- package/scripts/install-standalone-recovery.ts +1 -1
- package/scripts/lib/project-init-lib.sh +6 -3
- package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
- package/scripts/open-source-audit-allowlist.txt +1 -3
- package/scripts/run-bounded-command.mjs +5 -0
- package/scripts/run-governed-gate.ts +2 -1
- package/scripts/run-mobile-monitor-bridge.ts +0 -1
- package/scripts/smoke-control-plane-failure-domain.ts +94 -0
- package/scripts/smoke-ios-physical-device.ts +38 -10
- package/scripts/smoke-mcp-http-runtime.ts +12 -8
- package/scripts/smoke-mcp-tool-surface.ts +27 -41
- 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 +95 -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/sync-generated-authority.mjs +0 -1
- package/scripts/verify-forge-runtime.sh +2 -1
- package/src/cli/agent-jobs/executor-health.ts +12 -35
- package/src/cli/agent-jobs/integration.ts +20 -51
- package/src/cli/agent-jobs/job-manager.ts +7 -3
- package/src/cli/chatgpt-browser/file-policy.ts +2 -2
- 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/doctor.ts +1 -143
- package/src/cli/commands/global-runtime.ts +3 -196
- package/src/cli/commands/init-hook.ts +6 -23
- package/src/cli/commands/init.ts +3 -252
- package/src/cli/commands/mcp.ts +0 -5
- package/src/cli/commands/prompt-guard-decision.ts +0 -2
- package/src/cli/commands/recovery.ts +137 -135
- package/src/cli/commands/repository.ts +17 -1
- package/src/cli/commands/runtime.ts +3 -3
- package/src/cli/commands/setup-execution.ts +41 -0
- 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 +5 -44
- package/src/cli/controller/completion-target.ts +3 -72
- package/src/cli/controller/composite-operations.ts +1 -1
- package/src/cli/controller/context/instruction-context.ts +1 -1
- package/src/cli/controller/context/known-paths.ts +2 -2
- package/src/cli/controller/context/query-planning.ts +11 -4
- package/src/cli/controller/context/rule-resolution.ts +2 -2
- package/src/cli/controller/context/source-materializer.ts +83 -102
- package/src/cli/controller/context/structural-provider.ts +84 -0
- package/src/cli/controller/context/structural.ts +6 -6
- package/src/cli/controller/context/types.ts +8 -9
- package/src/cli/controller/context-pack.ts +107 -84
- 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 +25 -52
- 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 +42 -10
- package/src/cli/editing/edit-session.ts +18 -12
- package/src/cli/github/github.ts +4 -4
- package/src/cli/github/semantic-projection.ts +174 -0
- package/src/cli/hook/prompt-intents.ts +0 -9
- package/src/cli/index.ts +1 -43
- package/src/cli/local-bridge/connector-freshness.ts +1 -12
- package/src/cli/local-bridge/console-automations.ts +19 -14
- package/src/cli/local-bridge/console-view-models.ts +4 -13
- package/src/cli/local-bridge/facade-api.ts +64 -193
- 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-classifier.ts +0 -9
- package/src/cli/repositories/command-executor.ts +29 -23
- package/src/cli/repositories/command-scope.ts +37 -18
- 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/locks.ts +20 -0
- package/src/cli/repositories/registry.ts +79 -12
- package/src/cli/repositories/safe-patch.ts +2 -12
- package/src/cli/repositories/structured-git.ts +101 -5
- package/src/cli/repository/inspector.ts +185 -343
- package/src/cli/repository/session-cache.ts +92 -6
- package/src/effects/process-runner.ts +17 -1
- 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/generic-lsp-provider.ts +1 -1
- package/src/runtime/context/semantic-navigation-contract.ts +16 -0
- package/src/runtime/context/semantic-navigation.ts +7 -35
- 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 +215 -12
- 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 +616 -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 +197 -110
- package/src/runtime/control-plane/execution/work-handle-store.ts +72 -44
- 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 +60 -34
- package/src/runtime/control-plane/execution/work-preparation-service.ts +231 -267
- package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
- 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 +201 -199
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +33 -85
- package/src/runtime/control-plane/execution/work-verification-service.ts +406 -50
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +6 -2
- package/src/runtime/control-plane/facade/capability-registry.ts +364 -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 +9 -9
- package/src/runtime/control-plane/generated-cache-retention.ts +206 -59
- 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 +256 -117
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -12
- 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/governance/external-effects.ts +1 -1
- package/src/runtime/control-plane/launcher/chatgpt-round-continuation.ts +63 -300
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +29 -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 +295 -107
- 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/evidence/sensitive-output.ts +22 -2
- package/src/runtime/execution/jobs/child-reference.ts +11 -0
- package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
- 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 +82 -30
- 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 +55 -18
- package/src/runtime/execution/process-runtime/store.ts +30 -2
- package/src/runtime/execution/process-runtime/types.ts +6 -1
- package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
- package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
- package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
- package/src/runtime/execution/workers/executor.ts +3 -8
- package/src/runtime/maintenance/cleanup.ts +41 -5
- 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 +97 -29
- package/src/runtime/plugins/app-store-connect-adapter.ts +290 -18
- package/src/runtime/plugins/browser-adapter.ts +1124 -333
- package/src/runtime/plugins/browser-automation-service.ts +23 -3
- package/src/runtime/plugins/browser-macos-bridge.ts +431 -137
- package/src/runtime/plugins/browser-manifest-surface.ts +47 -12
- package/src/runtime/plugins/browser-native-session-service.ts +63 -13
- package/src/runtime/plugins/browser-registration.ts +18 -0
- package/src/runtime/plugins/browser-runtime.ts +40 -36
- 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 +228 -0
- package/src/runtime/plugins/computer-registration.ts +298 -20
- 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/github-adapter.ts +30 -3
- package/src/runtime/plugins/gmail-adapter.ts +189 -3
- package/src/runtime/plugins/google-calendar-adapter.ts +2 -2
- package/src/runtime/plugins/google-shared.ts +24 -12
- package/src/runtime/plugins/google-tasks-adapter.ts +2 -2
- package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
- package/src/runtime/plugins/ios-adapter.ts +46 -41
- package/src/runtime/plugins/ios-agent-device.ts +54 -4
- package/src/runtime/plugins/ios-device-ownership.ts +103 -0
- package/src/runtime/plugins/ios-physical-device.ts +497 -79
- package/src/runtime/plugins/lightweight-action.ts +208 -2
- package/src/runtime/plugins/local-system-adapter.ts +12 -100
- 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 +682 -435
- package/src/runtime/plugins/types.ts +24 -0
- package/src/runtime/plugins/xiaohongshu-publish.ts +16 -0
- package/src/runtime/projections/git-status-sampler.ts +2 -19
- package/src/runtime/projections/invalidation.ts +7 -3
- package/src/runtime/projections/materialized-view.ts +80 -36
- package/src/runtime/recovery/actions.ts +4 -4
- package/src/runtime/recovery/capability-action-application.ts +46 -45
- package/src/runtime/recovery/lifecycle-inventory.ts +30 -40
- package/src/runtime/recovery/maintenance-executor.ts +374 -123
- package/src/runtime/recovery/probe.ts +2 -2
- package/src/runtime/recovery/store.ts +29 -11
- package/src/runtime/release/release-coordinator.ts +312 -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 +254 -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 +227 -29
- package/src/runtime/root/package-runtime-release.ts +117 -30
- package/src/runtime/root/package-runtime-service.ts +24 -24
- package/src/runtime/root/release-execution-canary.ts +57 -12
- package/src/runtime/root/release-loader.ts +90 -8
- package/src/runtime/root/release-manifest.ts +80 -40
- package/src/runtime/root/release-materialize.ts +372 -106
- package/src/runtime/root/release-store.ts +395 -36
- package/src/runtime/root/runtime-lane.ts +191 -0
- package/src/runtime/root/runtime.ts +160 -29
- package/src/runtime/root/scheduled-controller-composition.ts +29 -3
- package/src/runtime/root/service-runner.ts +2 -2
- package/src/runtime/root/service.ts +126 -44
- package/src/runtime/root/status.ts +48 -0
- package/src/runtime/root/types.ts +32 -9
- package/src/runtime/root/workflow-supervisor-composition.ts +681 -0
- package/src/runtime/root/workflow-supervisor-runtime.ts +564 -0
- package/src/runtime/root/write-fence.ts +50 -2
- package/src/runtime/safe-tooling/google-credential-store.ts +67 -22
- package/src/runtime/safe-tooling/google-oauth-broker.ts +25 -7
- package/src/runtime/safe-tooling/ios-development.ts +22 -8
- package/src/runtime/safe-tooling/job-summary.ts +1 -1
- package/src/runtime/safe-tooling/workspace-auth.ts +5 -2
- package/src/runtime/shared/atomic-text-file.ts +21 -0
- package/src/runtime/shared/bounded-child-supervisor.ts +9 -1
- package/src/runtime/shared/json-files.ts +8 -3
- 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 +3792 -417
- package/src/runtime/standalone-recovery/entry.ts +507 -45
- 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 +12 -2
- 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 +11 -3
- package/src/runtime/workflow/schedules/engine.ts +166 -178
- package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
- package/src/runtime/workflow/schedules/work-continuation.ts +95 -11
- 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 +218 -0
- package/supervisor/control-plane.ts +963 -0
- package/supervisor/entry.ts +15 -0
- package/supervisor/forge-validators.ts +147 -0
- package/supervisor/index.ts +5 -0
- package/supervisor/native-browser-adapter.ts +1151 -0
- package/supervisor/native-messaging/host.ts +58 -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 +250 -0
- package/supervisor/server.ts +412 -0
- package/supervisor/store.ts +1870 -0
- package/supervisor/tsconfig.json +8 -0
- package/supervisor/types.ts +261 -0
- package/SKILL.md +0 -427
- 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/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
- package/adapters/mcp/toolset-selection.ts +0 -41
- package/agents/openai.yaml +0 -4
- package/assets/skill-hooks.json +0 -41
- package/assets/skill-version.json +0 -189
- package/assets/skills/claude-review/SKILL.md +0 -231
- package/assets/skills/codex-review/SKILL.md +0 -103
- package/assets/templates/helpers/check-skill-version.ts +0 -193
- 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-skill-version.ts +0 -5
- package/scripts/check-tarball-install-smoke.sh +0 -150
- package/scripts/ensure-codegraph.sh +0 -16
- 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/run-skill-hook.ts +0 -244
- package/scripts/stage-runtime-release.ts +0 -44
- package/scripts/sync-codex-installed-copies.sh +0 -182
- package/src/cli/commands/tools.ts +0 -167
- 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/cli/mcp/access-mode.ts +0 -2
- package/src/cli/mcp/access-tools.ts +0 -2
- package/src/cli/mcp/audit.ts +0 -2
- package/src/cli/mcp/auth.ts +0 -2
- package/src/cli/mcp/instructions.ts +0 -2
- package/src/cli/mcp/legacy-context.ts +0 -2
- package/src/cli/mcp/legacy-tool-service.ts +0 -2
- package/src/cli/mcp/multi-repository.ts +0 -2
- package/src/cli/mcp/oauth.ts +0 -2
- package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
- package/src/cli/mcp/paths.ts +0 -2
- package/src/cli/mcp/policy.ts +0 -2
- package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
- package/src/cli/mcp/proxy-env.ts +0 -2
- package/src/cli/mcp/readiness-projection.ts +0 -2
- package/src/cli/mcp/redaction.ts +0 -2
- package/src/cli/mcp/repo.ts +0 -2
- package/src/cli/mcp/repository-tools.ts +0 -2
- package/src/cli/mcp/server.ts +0 -2
- package/src/cli/mcp/setup.ts +0 -2
- package/src/cli/mcp/tool-capability-cache.ts +0 -2
- package/src/cli/mcp/tool-contract.ts +0 -2
- package/src/cli/mcp/tools.ts +0 -2
- package/src/cli/mcp/toolset-names.ts +0 -2
- package/src/cli/mcp/toolset-selection.ts +0 -2
- package/src/cli/mcp/toolset.ts +0 -2
- package/src/cli/mcp/transports/http.ts +0 -2
- package/src/cli/mcp/transports/session-registry.ts +0 -2
- package/src/cli/mcp/transports/stdio.ts +0 -2
- package/src/cli/mcp/types.ts +0 -2
- package/src/cli/mcp/umbrella-tools.ts +0 -2
- package/src/cli/tools/codegraph.ts +0 -695
- package/src/core/adoption/helper-wrapper-plan.ts +0 -159
- package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
- package/src/runtime/context/codegraph-read-provider.ts +0 -611
- package/src/runtime/context/codegraph-sidecar.cjs +0 -248
- package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
- 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
- package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
- package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
- package/src/runtime/gateway/mcp/process-tools.ts +0 -2
- package/src/runtime/gateway/mcp/router.ts +0 -2
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
- package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
- package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
- package/src/runtime/plugins/browser-provider-registry.ts +0 -2
|
@@ -1,7 +1,11 @@
|
|
|
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';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
5
9
|
import type {
|
|
6
10
|
AssistantPluginActionExecutionInput,
|
|
7
11
|
AssistantPluginAuthorizationContext,
|
|
@@ -10,6 +14,8 @@ import type {
|
|
|
10
14
|
AssistantPluginManifest,
|
|
11
15
|
} from './types';
|
|
12
16
|
import { AssistantPluginError, toAssistantPluginError } from './errors';
|
|
17
|
+
import { listCanonicalGrants } from '../../../packages/kernel/identity/api/index';
|
|
18
|
+
import { resolveRepoPreferredControllerHome } from '../../cli/repositories/controller-home';
|
|
13
19
|
import { readRepositoryPluginConfig, writeRepositoryPluginConfig, type RepositoryPluginConfigContext } from './config-store';
|
|
14
20
|
import {
|
|
15
21
|
browserActions,
|
|
@@ -17,7 +23,6 @@ import {
|
|
|
17
23
|
browserPermissions,
|
|
18
24
|
} from './browser-manifest-surface';
|
|
19
25
|
import { executeBrowserActionThroughNode, shouldUseBrowserNodeBridge } from './browser-node-bridge';
|
|
20
|
-
import { BrowserProviderUnavailableBeforeActionError } from '../../../packages/plugin-runtime/browser/provider-registry';
|
|
21
26
|
import { MAX_BROWSER_CDP_ENDPOINT_CANDIDATES } from '../../../packages/plugin-runtime/browser/runtime-contract';
|
|
22
27
|
import {
|
|
23
28
|
ALL_BROWSER_PROVIDER_CAPABILITIES,
|
|
@@ -28,6 +33,8 @@ import {
|
|
|
28
33
|
invalidateBrowserRuntime,
|
|
29
34
|
} from './browser-runtime';
|
|
30
35
|
import { activateRuntimeComputerBrowserApplication } from '../root/computer-composition';
|
|
36
|
+
import { resolveWorkspaceTargetPath, WorkspaceTargetGrantError } from '../workspace-targets';
|
|
37
|
+
import { pluginCapabilityAuthorizationOwnerScope } from './capability-authorization-grants';
|
|
31
38
|
import {
|
|
32
39
|
DEFAULT_BROWSER_SESSION_LIST_LIMIT,
|
|
33
40
|
MAX_BROWSER_SESSION_LIST_LIMIT,
|
|
@@ -50,6 +57,7 @@ import {
|
|
|
50
57
|
readMacOsBrowserOwnedTabMetadata,
|
|
51
58
|
macOsActiveBrowserAttachSupported,
|
|
52
59
|
macOsBrowserJavaScriptAutomationDisabled,
|
|
60
|
+
assertMacOsBrowserApplicationRunning,
|
|
53
61
|
type MacOsBrowserAttachAttempt,
|
|
54
62
|
type MacOsBrowserCreateTabEvidence,
|
|
55
63
|
type MacOsBrowserProduct,
|
|
@@ -107,12 +115,12 @@ type WaitForSelectorState = 'attached' | 'detached' | 'visible' | 'hidden';
|
|
|
107
115
|
type BrowserChannel = 'chromium' | 'chrome' | 'chrome-beta' | 'chrome-dev' | 'chrome-canary';
|
|
108
116
|
type BrowserNativeAttachMode = 'auto' | 'disabled';
|
|
109
117
|
|
|
110
|
-
const CURRENT_BROWSER_CONFIG_SCHEMA_VERSION =
|
|
118
|
+
const CURRENT_BROWSER_CONFIG_SCHEMA_VERSION = 3 as const;
|
|
111
119
|
const DEFAULT_USER_BROWSER_CHANNEL: BrowserChannel = 'chrome';
|
|
112
|
-
const DEFAULT_USER_NATIVE_BROWSER_CANDIDATES: MacOsBrowserProduct[] = ['chrome'];
|
|
120
|
+
const DEFAULT_USER_NATIVE_BROWSER_CANDIDATES: MacOsBrowserProduct[] = ['chrome', 'vivaldi'];
|
|
113
121
|
|
|
114
122
|
interface BrowserPluginConfig {
|
|
115
|
-
schemaVersion:
|
|
123
|
+
schemaVersion: 3;
|
|
116
124
|
enabled: boolean;
|
|
117
125
|
provider: 'playwright';
|
|
118
126
|
browserMode?: BrowserMode;
|
|
@@ -131,7 +139,7 @@ interface BrowserPluginConfig {
|
|
|
131
139
|
}
|
|
132
140
|
|
|
133
141
|
type PersistedBrowserPluginConfig = Partial<Omit<BrowserPluginConfig, 'schemaVersion'>> & {
|
|
134
|
-
schemaVersion?: 1 | 2;
|
|
142
|
+
schemaVersion?: 1 | 2 | 3;
|
|
135
143
|
};
|
|
136
144
|
|
|
137
145
|
type BrowserSessionInventory = {
|
|
@@ -175,15 +183,109 @@ interface BrowserProfileSelection {
|
|
|
175
183
|
selectedProfilePath: string;
|
|
176
184
|
}
|
|
177
185
|
|
|
186
|
+
type BrowserExtensionTargetLike = { url(): string };
|
|
187
|
+
type BrowserCdpSessionLike = {
|
|
188
|
+
send(method: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
189
|
+
detach?(): Promise<void>;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
function connectNativeBrowserCdp(endpoint: string, timeoutMs: number): Promise<BrowserCdpSessionLike> {
|
|
193
|
+
const boundedTimeoutMs = Math.max(250, Math.min(timeoutMs, 30_000));
|
|
194
|
+
return new Promise<BrowserCdpSessionLike>((resolveSession, rejectSession) => {
|
|
195
|
+
const socket = new WebSocket(endpoint);
|
|
196
|
+
const pending = new Map<number, {
|
|
197
|
+
resolve(value: unknown): void;
|
|
198
|
+
reject(error: Error): void;
|
|
199
|
+
timer: ReturnType<typeof setTimeout>;
|
|
200
|
+
}>();
|
|
201
|
+
let nextId = 1;
|
|
202
|
+
let connectSettled = false;
|
|
203
|
+
|
|
204
|
+
const failPending = (error: Error) => {
|
|
205
|
+
for (const request of pending.values()) {
|
|
206
|
+
clearTimeout(request.timer);
|
|
207
|
+
request.reject(error);
|
|
208
|
+
}
|
|
209
|
+
pending.clear();
|
|
210
|
+
};
|
|
211
|
+
const failConnect = (error: Error) => {
|
|
212
|
+
if (connectSettled) return;
|
|
213
|
+
connectSettled = true;
|
|
214
|
+
clearTimeout(connectTimer);
|
|
215
|
+
rejectSession(error);
|
|
216
|
+
};
|
|
217
|
+
const connectTimer = setTimeout(() => {
|
|
218
|
+
failConnect(new Error(`Browser CDP WebSocket did not open within ${boundedTimeoutMs}ms.`));
|
|
219
|
+
try { socket.close(); } catch { /* best-effort bounded cleanup */ }
|
|
220
|
+
}, boundedTimeoutMs);
|
|
221
|
+
|
|
222
|
+
socket.onopen = () => {
|
|
223
|
+
if (connectSettled) return;
|
|
224
|
+
connectSettled = true;
|
|
225
|
+
clearTimeout(connectTimer);
|
|
226
|
+
resolveSession({
|
|
227
|
+
send: (method, params) => new Promise<unknown>((resolveResult, rejectResult) => {
|
|
228
|
+
const id = nextId++;
|
|
229
|
+
const timer = setTimeout(() => {
|
|
230
|
+
pending.delete(id);
|
|
231
|
+
rejectResult(new Error(`Browser CDP method ${method} timed out after ${boundedTimeoutMs}ms.`));
|
|
232
|
+
}, boundedTimeoutMs);
|
|
233
|
+
pending.set(id, { resolve: resolveResult, reject: rejectResult, timer });
|
|
234
|
+
try {
|
|
235
|
+
socket.send(JSON.stringify({ id, method, ...(params ? { params } : {}) }));
|
|
236
|
+
} catch (error) {
|
|
237
|
+
clearTimeout(timer);
|
|
238
|
+
pending.delete(id);
|
|
239
|
+
rejectResult(error instanceof Error ? error : new Error(String(error)));
|
|
240
|
+
}
|
|
241
|
+
}),
|
|
242
|
+
detach: async () => {
|
|
243
|
+
failPending(new Error('Browser CDP session detached.'));
|
|
244
|
+
try { socket.close(); } catch { /* best-effort bounded cleanup */ }
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
socket.onmessage = (event) => {
|
|
249
|
+
if (typeof event.data !== 'string') return;
|
|
250
|
+
let message: { id?: unknown; result?: unknown; error?: { message?: unknown } };
|
|
251
|
+
try { message = JSON.parse(event.data) as typeof message; }
|
|
252
|
+
catch { return; }
|
|
253
|
+
if (!Number.isInteger(message.id)) return;
|
|
254
|
+
const request = pending.get(Number(message.id));
|
|
255
|
+
if (!request) return;
|
|
256
|
+
pending.delete(Number(message.id));
|
|
257
|
+
clearTimeout(request.timer);
|
|
258
|
+
if (message.error) {
|
|
259
|
+
request.reject(new Error(typeof message.error.message === 'string' ? message.error.message : 'Browser CDP command failed.'));
|
|
260
|
+
} else {
|
|
261
|
+
request.resolve(message.result);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
socket.onerror = () => {
|
|
265
|
+
const error = new Error('Browser CDP WebSocket transport failed.');
|
|
266
|
+
failConnect(error);
|
|
267
|
+
failPending(error);
|
|
268
|
+
};
|
|
269
|
+
socket.onclose = () => {
|
|
270
|
+
const error = new Error('Browser CDP WebSocket transport closed.');
|
|
271
|
+
failConnect(error);
|
|
272
|
+
failPending(error);
|
|
273
|
+
};
|
|
274
|
+
});
|
|
275
|
+
}
|
|
178
276
|
type BrowserContextLike = {
|
|
179
277
|
pages(): PageLike[];
|
|
180
278
|
newPage(): Promise<PageLike>;
|
|
181
279
|
close(): Promise<void>;
|
|
280
|
+
browser?(): BrowserLike | null;
|
|
281
|
+
serviceWorkers?(): BrowserExtensionTargetLike[];
|
|
282
|
+
backgroundPages?(): BrowserExtensionTargetLike[];
|
|
182
283
|
};
|
|
183
284
|
|
|
184
285
|
type BrowserLike = {
|
|
185
286
|
contexts(): BrowserContextLike[];
|
|
186
287
|
newContext?(): Promise<BrowserContextLike>;
|
|
288
|
+
newBrowserCDPSession?(): Promise<BrowserCdpSessionLike>;
|
|
187
289
|
close?(): Promise<void>;
|
|
188
290
|
disconnect?(): Promise<void> | void;
|
|
189
291
|
};
|
|
@@ -251,20 +353,73 @@ type PlaywrightRuntime = {
|
|
|
251
353
|
};
|
|
252
354
|
};
|
|
253
355
|
|
|
356
|
+
export function browserDependencyRequireAnchors(repoRoot?: string, moduleUrl = import.meta.url): string[] {
|
|
357
|
+
const releasePackageManifest = join(dirname(fileURLToPath(moduleUrl)), 'package', 'package.json');
|
|
358
|
+
return [
|
|
359
|
+
existsSync(releasePackageManifest) ? releasePackageManifest : undefined,
|
|
360
|
+
repoRoot ? join(repoRoot, 'package.json') : undefined,
|
|
361
|
+
moduleUrl,
|
|
362
|
+
].filter((anchor): anchor is string => typeof anchor === 'string');
|
|
363
|
+
}
|
|
364
|
+
|
|
254
365
|
interface BrowserPluginRuntimeHooks {
|
|
255
366
|
now(): string;
|
|
256
367
|
moduleAvailable(name: string, repoRoot?: string): boolean;
|
|
257
368
|
loadPlaywright(repoRoot?: string): PlaywrightRuntime;
|
|
258
369
|
fetchJson(url: string, timeoutMs: number): Promise<unknown>;
|
|
259
370
|
activateNativeBrowserApplication(input: AssistantPluginActionExecutionInput, product: MacOsBrowserProduct): Promise<void>;
|
|
371
|
+
/** Bounded process inventory used only to reclaim Forge-owned browser profiles. */
|
|
372
|
+
listProcesses(): Array<{ pid: number; command: string }>;
|
|
373
|
+
/** Signal one process; returns false when it already exited. */
|
|
374
|
+
signalProcess(pid: number, signal: NodeJS.Signals): boolean;
|
|
375
|
+
/** Launch one browser process without claiming its lifecycle through Playwright. */
|
|
376
|
+
launchProcess(executable: string, args: string[]): { pid?: number };
|
|
377
|
+
/** Allocate one currently-free loopback TCP port for an immediately following browser launch. */
|
|
378
|
+
allocateLoopbackPort(): Promise<number>;
|
|
379
|
+
/** Open one bounded browser-level CDP session without a Playwright browser/context adapter. */
|
|
380
|
+
connectBrowserCdp(endpoint: string, timeoutMs: number): Promise<BrowserCdpSessionLike>;
|
|
260
381
|
}
|
|
261
382
|
|
|
262
383
|
const defaultRuntimeHooks: BrowserPluginRuntimeHooks = {
|
|
263
384
|
now: () => new Date().toISOString(),
|
|
385
|
+
listProcesses: () => {
|
|
386
|
+
const result = spawnSync('ps', ['-eo', 'pid=,command='], { encoding: 'utf8', timeout: 10_000, maxBuffer: 8 * 1024 * 1024 });
|
|
387
|
+
if (result.status !== 0 || typeof result.stdout !== 'string') return [];
|
|
388
|
+
const entries: Array<{ pid: number; command: string }> = [];
|
|
389
|
+
for (const line of result.stdout.split('\n')) {
|
|
390
|
+
const match = /^\s*(\d+)\s+(.*)$/.exec(line);
|
|
391
|
+
if (!match?.[1] || !match[2]) continue;
|
|
392
|
+
entries.push({ pid: Number(match[1]), command: match[2] });
|
|
393
|
+
}
|
|
394
|
+
return entries;
|
|
395
|
+
},
|
|
396
|
+
signalProcess: (pid, signal) => {
|
|
397
|
+
try { process.kill(pid, signal); return true; }
|
|
398
|
+
catch (error) { return (error as NodeJS.ErrnoException).code !== 'ESRCH'; }
|
|
399
|
+
},
|
|
400
|
+
launchProcess: (executable, args) => {
|
|
401
|
+
const child = spawn(executable, args, { stdio: 'ignore', detached: true });
|
|
402
|
+
child.once('error', () => undefined);
|
|
403
|
+
child.unref();
|
|
404
|
+
return { pid: child.pid };
|
|
405
|
+
},
|
|
406
|
+
allocateLoopbackPort: () => new Promise<number>((resolvePort, rejectPort) => {
|
|
407
|
+
const server = createNetServer();
|
|
408
|
+
server.unref();
|
|
409
|
+
server.once('error', rejectPort);
|
|
410
|
+
server.listen({ host: '127.0.0.1', port: 0, exclusive: true }, () => {
|
|
411
|
+
const address = server.address();
|
|
412
|
+
const port = typeof address === 'object' && address ? address.port : 0;
|
|
413
|
+
server.close((error) => {
|
|
414
|
+
if (error) return rejectPort(error);
|
|
415
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65_535) return rejectPort(new Error('BROWSER_LOOPBACK_PORT_ALLOCATION_FAILED'));
|
|
416
|
+
resolvePort(port);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
}),
|
|
420
|
+
connectBrowserCdp: connectNativeBrowserCdp,
|
|
264
421
|
moduleAvailable: (name: string, repoRoot?: string) => {
|
|
265
|
-
const
|
|
266
|
-
.filter((value): value is string => Boolean(value));
|
|
267
|
-
for (const anchor of anchors) {
|
|
422
|
+
for (const anchor of browserDependencyRequireAnchors(repoRoot)) {
|
|
268
423
|
try {
|
|
269
424
|
createRequire(anchor).resolve(name);
|
|
270
425
|
return true;
|
|
@@ -275,16 +430,14 @@ const defaultRuntimeHooks: BrowserPluginRuntimeHooks = {
|
|
|
275
430
|
return false;
|
|
276
431
|
},
|
|
277
432
|
loadPlaywright: (repoRoot?: string) => {
|
|
278
|
-
const
|
|
279
|
-
.filter((value): value is string => Boolean(value));
|
|
280
|
-
for (const anchor of anchors) {
|
|
433
|
+
for (const anchor of browserDependencyRequireAnchors(repoRoot)) {
|
|
281
434
|
try {
|
|
282
435
|
return createRequire(anchor)('playwright') as PlaywrightRuntime;
|
|
283
436
|
} catch {
|
|
284
437
|
// Try the next trusted dependency anchor.
|
|
285
438
|
}
|
|
286
439
|
}
|
|
287
|
-
throw new AssistantPluginError('PLUGIN_DEPENDENCY_MISSING', 'Browser plugin
|
|
440
|
+
throw new AssistantPluginError('PLUGIN_DEPENDENCY_MISSING', 'Browser plugin cannot resolve packaged Playwright or an authorized repository-local installation.', {
|
|
288
441
|
retryable: false,
|
|
289
442
|
});
|
|
290
443
|
},
|
|
@@ -313,6 +466,7 @@ interface ManagedBrowserContextState {
|
|
|
313
466
|
}
|
|
314
467
|
|
|
315
468
|
const managedBrowserContexts = new Map<string, Promise<ManagedBrowserContextState>>();
|
|
469
|
+
const managedExtensionPaths = new Map<string, Set<string>>();
|
|
316
470
|
|
|
317
471
|
export function setBrowserPluginRuntimeHooksForTest(hooks: Partial<BrowserPluginRuntimeHooks>): void {
|
|
318
472
|
runtimeHooks = { ...defaultRuntimeHooks, ...hooks };
|
|
@@ -323,6 +477,7 @@ export function resetBrowserPluginRuntimeHooksForTest(): void {
|
|
|
323
477
|
runtimeHooks = { ...defaultRuntimeHooks };
|
|
324
478
|
runtimeHooksCustomized = false;
|
|
325
479
|
managedBrowserContexts.clear();
|
|
480
|
+
managedExtensionPaths.clear();
|
|
326
481
|
}
|
|
327
482
|
|
|
328
483
|
function now(): string {
|
|
@@ -426,6 +581,8 @@ function resolveProfileSelection(repoRoot: string, profileDir: string, profileDi
|
|
|
426
581
|
|
|
427
582
|
function normalizeConfig(raw: PersistedBrowserPluginConfig): BrowserPluginConfig {
|
|
428
583
|
const normalizedProfileDir = stringValue(raw.profileDir);
|
|
584
|
+
const normalizedExecutablePath = stringValue(raw.executablePath);
|
|
585
|
+
const normalizedBrowserChannel = browserChannel(raw.browserChannel);
|
|
429
586
|
return {
|
|
430
587
|
schemaVersion: CURRENT_BROWSER_CONFIG_SCHEMA_VERSION,
|
|
431
588
|
enabled: raw.enabled === true,
|
|
@@ -434,8 +591,8 @@ function normalizeConfig(raw: PersistedBrowserPluginConfig): BrowserPluginConfig
|
|
|
434
591
|
profileMode: browserProfileMode(raw.profileMode) ?? (normalizedProfileDir ? 'custom' : 'repo_local'),
|
|
435
592
|
profileDir: normalizedProfileDir,
|
|
436
593
|
profileDirectory: stringValue(raw.profileDirectory),
|
|
437
|
-
browserChannel:
|
|
438
|
-
executablePath:
|
|
594
|
+
browserChannel: normalizedBrowserChannel ?? (normalizedExecutablePath ? undefined : DEFAULT_USER_BROWSER_CHANNEL),
|
|
595
|
+
executablePath: normalizedExecutablePath,
|
|
439
596
|
cdpEndpoint: stringValue(raw.cdpEndpoint),
|
|
440
597
|
cdpEndpointCandidates: stringList(raw.cdpEndpointCandidates)?.slice(0, MAX_BROWSER_CDP_ENDPOINT_CANDIDATES),
|
|
441
598
|
cdpDiscoveryTimeoutMs: typeof raw.cdpDiscoveryTimeoutMs === 'number'
|
|
@@ -449,22 +606,38 @@ function normalizeConfig(raw: PersistedBrowserPluginConfig): BrowserPluginConfig
|
|
|
449
606
|
}
|
|
450
607
|
|
|
451
608
|
function migrateLegacyRepositoryConfig(raw: PersistedBrowserPluginConfig): PersistedBrowserPluginConfig {
|
|
452
|
-
if (raw.schemaVersion
|
|
609
|
+
if (raw.schemaVersion === CURRENT_BROWSER_CONFIG_SCHEMA_VERSION) return raw;
|
|
610
|
+
// v1 legacy managed profiles were an implicit route to a fresh browser.
|
|
611
|
+
// Migrate that default to the existing signed-in native browser; preserve
|
|
612
|
+
// explicit v2 managed configurations as separate scripting capabilities.
|
|
613
|
+
const legacyV1 = raw.schemaVersion === 1;
|
|
453
614
|
return {
|
|
615
|
+
...raw,
|
|
454
616
|
schemaVersion: CURRENT_BROWSER_CONFIG_SCHEMA_VERSION,
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
nativeBrowserCandidates: [...DEFAULT_USER_NATIVE_BROWSER_CANDIDATES],
|
|
463
|
-
defaultTimeoutMs: raw.defaultTimeoutMs,
|
|
464
|
-
cdpDiscoveryTimeoutMs: raw.cdpDiscoveryTimeoutMs,
|
|
617
|
+
browserMode: legacyV1 ? 'attach_preferred' : (browserMode(raw.browserMode) ?? 'attach_preferred'),
|
|
618
|
+
cdpAttachFallback: legacyV1 ? 'fail_closed' : (raw.cdpAttachFallback ?? 'fail_closed'),
|
|
619
|
+
nativeAttachMode: legacyV1 ? 'auto' : (raw.nativeAttachMode ?? 'auto'),
|
|
620
|
+
...(legacyV1 ? {
|
|
621
|
+
profileMode: 'repo_local' as const, profileDir: undefined, profileDirectory: undefined,
|
|
622
|
+
browserChannel: DEFAULT_USER_BROWSER_CHANNEL, nativeBrowserCandidates: [...DEFAULT_USER_NATIVE_BROWSER_CANDIDATES],
|
|
623
|
+
} : {}),
|
|
465
624
|
};
|
|
466
625
|
}
|
|
467
626
|
|
|
627
|
+
export function normalizeLegacyBrowserConfig(raw: unknown): BrowserPluginConfig {
|
|
628
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
629
|
+
throw new Error('PLUGIN_CONFIG_MIGRATION_INVALID: browser config must be an object');
|
|
630
|
+
}
|
|
631
|
+
const parsed = raw as PersistedBrowserPluginConfig;
|
|
632
|
+
if (![1, 2, 3].includes(parsed.schemaVersion as number)) {
|
|
633
|
+
throw new Error('PLUGIN_CONFIG_MIGRATION_INVALID: unsupported browser schemaVersion');
|
|
634
|
+
}
|
|
635
|
+
const migrated = normalizeConfig(migrateLegacyRepositoryConfig(parsed));
|
|
636
|
+
const errors = validateConfig(migrated);
|
|
637
|
+
if (errors.length) throw new Error(`PLUGIN_CONFIG_MIGRATION_INVALID: ${errors[0]}`);
|
|
638
|
+
return migrated;
|
|
639
|
+
}
|
|
640
|
+
|
|
468
641
|
function readJson<T>(path: string): T | undefined {
|
|
469
642
|
try {
|
|
470
643
|
return JSON.parse(readFileSync(path, 'utf-8')) as T;
|
|
@@ -478,28 +651,63 @@ function writeJson(path: string, value: unknown): void {
|
|
|
478
651
|
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, 'utf-8');
|
|
479
652
|
}
|
|
480
653
|
|
|
481
|
-
function loadConfig(
|
|
654
|
+
function loadConfig(
|
|
655
|
+
repoRoot: string,
|
|
656
|
+
context?: Pick<RepositoryPluginConfigContext, 'controllerHome' | 'repoId'>,
|
|
657
|
+
computerNativeOnly = false,
|
|
658
|
+
): BrowserPluginConfig {
|
|
659
|
+
// Computer Browser is a built-in Forge capability, not an optional
|
|
660
|
+
// repository Browser-script plugin. Its native defaults ship with the
|
|
661
|
+
// first-party Computer adapter; never inherit an independent Playwright
|
|
662
|
+
// profile, enabled flag, CDP endpoint, or fallback from a repository.
|
|
663
|
+
if (computerNativeOnly) return normalizeConfig({
|
|
664
|
+
enabled: true, browserMode: 'attach_preferred',
|
|
665
|
+
nativeAttachMode: 'auto', cdpAttachFallback: 'fail_closed',
|
|
666
|
+
nativeBrowserCandidates: [...DEFAULT_USER_NATIVE_BROWSER_CANDIDATES],
|
|
667
|
+
});
|
|
482
668
|
const persisted = context
|
|
483
669
|
? readRepositoryPluginConfig<PersistedBrowserPluginConfig>({ ...context, repoRoot }, BROWSER_PLUGIN_ID)
|
|
484
670
|
: undefined;
|
|
485
|
-
|
|
486
|
-
return normalizeConfig(migrateLegacyRepositoryConfig(persisted));
|
|
671
|
+
return persisted ? normalizeConfig(migrateLegacyRepositoryConfig(persisted)) : normalizeConfig({});
|
|
487
672
|
}
|
|
488
673
|
|
|
489
674
|
async function resolveBrowserPluginAuthorizationContextInternal(
|
|
490
675
|
input: AssistantPluginActionExecutionInput,
|
|
491
676
|
): Promise<AssistantPluginAuthorizationContext | undefined> {
|
|
492
677
|
const action = browserActions().find((entry) => entry.actionId === input.actionId);
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
678
|
+
// Browser session/profile mutations also need an exact reusable target. In
|
|
679
|
+
// particular, create_session is the bootstrap action for a scheduled
|
|
680
|
+
// Controller round and intentionally carries browser.read + browser.profile
|
|
681
|
+
// rather than browser.interact. Restricting authorization resolution to
|
|
682
|
+
// browser.interact made the first interactive session usable only for that
|
|
683
|
+
// call and left the Scheduler without a durable browser-session grant.
|
|
684
|
+
if (!action || action.confirmation !== 'authorization' || !action.scopes.includes('browser.profile')) return undefined;
|
|
685
|
+
|
|
686
|
+
const persistedConfig = loadConfig(input.repoRoot, input, input.pluginId === 'computer');
|
|
496
687
|
const sessionId = stringValue(input.args.session_id);
|
|
497
688
|
const authorizationBrowserMode = parseBrowserModeInput(input.args.browser_mode) ?? persistedConfig.browserMode;
|
|
498
689
|
if (!sessionId || authorizationBrowserMode === 'isolated') return undefined;
|
|
499
690
|
const session = findBrowserSession(input.repoRoot, sessionId);
|
|
500
691
|
const config = effectiveBrowserActionConfig(persistedConfig, input.args, session);
|
|
501
692
|
const connection = session?.browser;
|
|
502
|
-
if (!session || !connection || connection.activeMode === 'isolated')
|
|
693
|
+
if (!session || !connection || connection.activeMode === 'isolated') {
|
|
694
|
+
// create_session precedes its durable session record. Recover only the
|
|
695
|
+
// exact browser-origin target already named by this Work's grant refs.
|
|
696
|
+
if (input.actionId !== 'create_session') return undefined;
|
|
697
|
+
let origin: string;
|
|
698
|
+
try { origin = new URL(stringValue(input.args.url) ?? '').origin.toLowerCase(); } catch { return undefined; }
|
|
699
|
+
const refs = new Set((input.authorizationGrantRefs ?? []).map((ref) => ref.trim()).filter(Boolean));
|
|
700
|
+
const target = listCanonicalGrants(input.controllerHome).find((grant) => (
|
|
701
|
+
refs.has(grant.grantId)
|
|
702
|
+
&& grant.target?.kind === 'browser-origin'
|
|
703
|
+
&& grant.target.id.endsWith(`@${origin}`)
|
|
704
|
+
&& grant.scopes?.includes('browser.profile')
|
|
705
|
+
))?.target;
|
|
706
|
+
return target ? {
|
|
707
|
+
target: { kind: target.kind, id: target.id, identityFingerprint: target.identityFingerprint },
|
|
708
|
+
expiresInMinutes: 30 * 24 * 60,
|
|
709
|
+
} : undefined;
|
|
710
|
+
}
|
|
503
711
|
|
|
504
712
|
let origin: string;
|
|
505
713
|
try {
|
|
@@ -549,6 +757,10 @@ export async function resolveBrowserPluginAuthorizationContext(
|
|
|
549
757
|
|
|
550
758
|
function saveConfig(input: Pick<AssistantPluginActionExecutionInput, 'controllerHome' | 'repoId' | 'repoRoot'>, patch: Partial<BrowserPluginConfig>): BrowserPluginConfig {
|
|
551
759
|
const next = normalizeConfig({ ...loadConfig(input.repoRoot, input), ...patch });
|
|
760
|
+
const errors = validateConfig(next);
|
|
761
|
+
if (errors.length > 0) {
|
|
762
|
+
throw new AssistantPluginError('PLUGIN_CONFIGURATION_INVALID', errors[0], { retryable: false });
|
|
763
|
+
}
|
|
552
764
|
return writeRepositoryPluginConfig(input, BROWSER_PLUGIN_ID, next);
|
|
553
765
|
}
|
|
554
766
|
|
|
@@ -740,11 +952,11 @@ function validateConfig(config: BrowserPluginConfig): string[] {
|
|
|
740
952
|
const error = cdpEndpointValidationError(endpoint);
|
|
741
953
|
if (error) errors.push(error);
|
|
742
954
|
}
|
|
743
|
-
if (config.browserMode === 'attach_preferred'
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
955
|
+
if (config.browserMode === 'attach_preferred') {
|
|
956
|
+
if (endpoints.length > 1) errors.push('One Browser attach capability may bind only one CDP endpoint.');
|
|
957
|
+
if (config.cdpAttachFallback === 'managed_persistent') errors.push('Managed Browser is an explicit browser_mode, never an attach fallback.');
|
|
958
|
+
// The configured list is discovery scope; execution must bind exactly one
|
|
959
|
+
// provider from the actual tab/product evidence, never select by fallback.
|
|
748
960
|
}
|
|
749
961
|
return errors;
|
|
750
962
|
}
|
|
@@ -757,14 +969,8 @@ function configWarnings(config: BrowserPluginConfig): string[] {
|
|
|
757
969
|
warnings.push('Custom profile mode is more reliable with an explicit Chrome channel or executable path that matches the selected profile format.');
|
|
758
970
|
}
|
|
759
971
|
}
|
|
760
|
-
if (config.browserMode === 'attach_preferred' && cdpEndpoints(config).length === 0) {
|
|
761
|
-
|
|
762
|
-
warnings.push(config.cdpAttachFallback === 'fail_closed'
|
|
763
|
-
? 'browserMode=attach_preferred has no configured CDP endpoint; configured native-browser attach will be attempted and managed fallback is disabled.'
|
|
764
|
-
: 'browserMode=attach_preferred has no configured CDP endpoint; configured native-browser attach will be attempted before explicit managed fallback.');
|
|
765
|
-
} else {
|
|
766
|
-
warnings.push('browserMode=attach_preferred has no configured CDP endpoint; actions will use the configured fallback policy.');
|
|
767
|
-
}
|
|
972
|
+
if (config.browserMode === 'attach_preferred' && cdpEndpoints(config).length === 0 && config.nativeAttachMode === 'auto') {
|
|
973
|
+
warnings.push('Native browser attach is the single selected provider; CDP and managed browsers are not automatic alternatives.');
|
|
768
974
|
}
|
|
769
975
|
if (config.browserMode === 'isolated') {
|
|
770
976
|
warnings.push('browserMode=isolated uses a per-session repo-local profile and will not share the default browser profile.');
|
|
@@ -1056,7 +1262,7 @@ interface BrowserOpenHandle {
|
|
|
1056
1262
|
|
|
1057
1263
|
interface PageDiagnostics {
|
|
1058
1264
|
consoleErrors: Array<{ type: string; text: string }>;
|
|
1059
|
-
failedRequests: Array<{ url: string; status?: number; failure?: string }>;
|
|
1265
|
+
failedRequests: Array<{ url: string; method?: string; status?: number; failure?: string }>;
|
|
1060
1266
|
navigation?: { url: string; status?: number };
|
|
1061
1267
|
}
|
|
1062
1268
|
|
|
@@ -1074,18 +1280,21 @@ function attachDiagnostics(page: PageLike): PageDiagnostics {
|
|
|
1074
1280
|
}
|
|
1075
1281
|
});
|
|
1076
1282
|
page.on('requestfailed', (request) => {
|
|
1077
|
-
const entry = request as { url?: () => string; failure?: () => { errorText?: string } | null };
|
|
1283
|
+
const entry = request as { url?: () => string; method?: () => string; failure?: () => { errorText?: string } | null };
|
|
1078
1284
|
diagnostics.failedRequests.push({
|
|
1079
1285
|
url: typeof entry.url === 'function' ? entry.url() : '',
|
|
1286
|
+
method: typeof entry.method === 'function' ? entry.method() : undefined,
|
|
1080
1287
|
failure: typeof entry.failure === 'function' ? entry.failure()?.errorText : undefined,
|
|
1081
1288
|
});
|
|
1082
1289
|
});
|
|
1083
1290
|
page.on('response', (response) => {
|
|
1084
|
-
const entry = response as { url?: () => string; status?: () => number; ok?: () => boolean };
|
|
1291
|
+
const entry = response as { url?: () => string; status?: () => number; ok?: () => boolean; request?: () => { method?: () => string } };
|
|
1085
1292
|
const status = typeof entry.status === 'function' ? entry.status() : undefined;
|
|
1086
1293
|
if (typeof status === 'number' && status >= 400) {
|
|
1294
|
+
const request = typeof entry.request === 'function' ? entry.request() : undefined;
|
|
1087
1295
|
diagnostics.failedRequests.push({
|
|
1088
1296
|
url: typeof entry.url === 'function' ? entry.url() : '',
|
|
1297
|
+
method: typeof request?.method === 'function' ? request.method() : undefined,
|
|
1089
1298
|
status,
|
|
1090
1299
|
});
|
|
1091
1300
|
}
|
|
@@ -1214,54 +1423,7 @@ function sameOrigin(left: string, right: string): boolean {
|
|
|
1214
1423
|
}
|
|
1215
1424
|
}
|
|
1216
1425
|
|
|
1217
|
-
function
|
|
1218
|
-
try {
|
|
1219
|
-
const url = new URL(value);
|
|
1220
|
-
if (url.protocol !== 'https:' && url.protocol !== 'http:') return undefined;
|
|
1221
|
-
const segments = url.pathname.split('/').filter(Boolean);
|
|
1222
|
-
for (let index = 0; index + 1 < segments.length; index += 1) {
|
|
1223
|
-
if ((segments[index] ?? '').toLowerCase() !== 'apps') continue;
|
|
1224
|
-
const id = segments[index + 1]?.trim();
|
|
1225
|
-
if (id) return `${url.origin}/apps/${id}`;
|
|
1226
|
-
}
|
|
1227
|
-
return undefined;
|
|
1228
|
-
} catch {
|
|
1229
|
-
return undefined;
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
function selectReusableNativeTab(
|
|
1234
|
-
referenceUrls: string[],
|
|
1235
|
-
inventory: Awaited<ReturnType<typeof listMacOsBrowserTabs>>,
|
|
1236
|
-
preferredWindowId?: string,
|
|
1237
|
-
): {
|
|
1238
|
-
candidate?: (typeof inventory.tabs)[number];
|
|
1239
|
-
match: 'exact_url' | 'stable_entity' | 'none' | 'ambiguous';
|
|
1240
|
-
candidateCount: number;
|
|
1241
|
-
} {
|
|
1242
|
-
const normalizedReferences = [...new Set(referenceUrls.filter(Boolean).map(comparableUrl))];
|
|
1243
|
-
const entityKeys = new Set(normalizedReferences.map(stableBrowserEntityKey).filter((value): value is string => Boolean(value)));
|
|
1244
|
-
const scored = inventory.tabs.map((candidate) => {
|
|
1245
|
-
const normalizedCandidate = comparableUrl(candidate.url);
|
|
1246
|
-
const entityKey = stableBrowserEntityKey(candidate.url);
|
|
1247
|
-
const matchScore = normalizedReferences.includes(normalizedCandidate)
|
|
1248
|
-
? 100
|
|
1249
|
-
: entityKey && entityKeys.has(entityKey)
|
|
1250
|
-
? 50
|
|
1251
|
-
: 0;
|
|
1252
|
-
const score = matchScore === 0
|
|
1253
|
-
? 0
|
|
1254
|
-
: matchScore + (preferredWindowId && candidate.windowId === preferredWindowId ? 10 : 0) + (candidate.active ? 1 : 0);
|
|
1255
|
-
return { candidate, score, matchScore };
|
|
1256
|
-
});
|
|
1257
|
-
const maxScore = scored.reduce((max, entry) => Math.max(max, entry.score), 0);
|
|
1258
|
-
if (maxScore === 0) return { match: 'none', candidateCount: 0 };
|
|
1259
|
-
const matches = scored.filter((entry) => entry.score === maxScore);
|
|
1260
|
-
if (matches.length !== 1) return { match: 'ambiguous', candidateCount: matches.length };
|
|
1261
|
-
return { candidate: matches[0]!.candidate, match: matches[0]!.matchScore === 100 ? 'exact_url' : 'stable_entity', candidateCount: 1 };
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
function selectLiveForgeOwnedNativeTab(
|
|
1426
|
+
export function selectLiveForgeOwnedNativeTab(
|
|
1265
1427
|
repoRoot: string,
|
|
1266
1428
|
targetUrl: string,
|
|
1267
1429
|
product: MacOsBrowserProduct,
|
|
@@ -1287,11 +1449,11 @@ function selectLiveForgeOwnedNativeTab(
|
|
|
1287
1449
|
}
|
|
1288
1450
|
|
|
1289
1451
|
const values = [...candidates.values()];
|
|
1452
|
+
if (inventory.truncated) return { match: 'ambiguous', candidateCount: values.length };
|
|
1290
1453
|
const exact = values.filter(({ candidate }) => comparableUrl(candidate.url) === comparableUrl(targetUrl));
|
|
1291
1454
|
const choose = (matches: typeof values, match: 'exact_url' | 'same_origin') => {
|
|
1292
1455
|
if (matches.length === 1) return { ...matches[0], match, candidateCount: 1 } as const;
|
|
1293
|
-
|
|
1294
|
-
if (active.length === 1) return { ...active[0], match, candidateCount: matches.length } as const;
|
|
1456
|
+
// Multiple matching tabs remain ambiguous even when one is active.
|
|
1295
1457
|
return { match: 'ambiguous' as const, candidateCount: matches.length };
|
|
1296
1458
|
};
|
|
1297
1459
|
if (exact.length > 0) return choose(exact, 'exact_url');
|
|
@@ -1301,15 +1463,15 @@ function selectLiveForgeOwnedNativeTab(
|
|
|
1301
1463
|
return { match: 'none', candidateCount: 0 };
|
|
1302
1464
|
}
|
|
1303
1465
|
|
|
1304
|
-
function selectExactNativeTab(
|
|
1466
|
+
export function selectExactNativeTab(
|
|
1305
1467
|
targetUrl: string,
|
|
1306
1468
|
inventory: Awaited<ReturnType<typeof listMacOsBrowserTabs>>,
|
|
1307
1469
|
): { candidate?: (typeof inventory.tabs)[number]; match: 'exact_url' | 'none' | 'ambiguous'; candidateCount: number } {
|
|
1470
|
+
if (inventory.truncated) return { match: 'ambiguous', candidateCount: inventory.tabs.length };
|
|
1308
1471
|
const matches = inventory.tabs.filter((candidate) => comparableUrl(candidate.url) === comparableUrl(targetUrl));
|
|
1309
1472
|
if (matches.length === 0) return { match: 'none', candidateCount: 0 };
|
|
1310
1473
|
if (matches.length === 1) return { candidate: matches[0], match: 'exact_url', candidateCount: 1 };
|
|
1311
|
-
|
|
1312
|
-
if (active.length === 1) return { candidate: active[0], match: 'exact_url', candidateCount: matches.length };
|
|
1474
|
+
// A frontmost tab does not disambiguate two tabs with the same URL.
|
|
1313
1475
|
return { match: 'ambiguous', candidateCount: matches.length };
|
|
1314
1476
|
}
|
|
1315
1477
|
|
|
@@ -1342,7 +1504,7 @@ async function inventoryTabs(context: BrowserContextLike): Promise<BrowserTabInv
|
|
|
1342
1504
|
return entries;
|
|
1343
1505
|
}
|
|
1344
1506
|
|
|
1345
|
-
function chooseTab(
|
|
1507
|
+
export function chooseTab(
|
|
1346
1508
|
inventory: BrowserTabInventoryEntry[],
|
|
1347
1509
|
target: BrowserActionTarget,
|
|
1348
1510
|
options: { allowBlank?: boolean; requireOwnedToken?: boolean } = {},
|
|
@@ -1350,6 +1512,20 @@ function chooseTab(
|
|
|
1350
1512
|
const desiredUrl = comparableUrl(target.url);
|
|
1351
1513
|
const savedTab = target.existingSession?.browser?.tab;
|
|
1352
1514
|
const expectedOwnerToken = savedTab?.ownership === 'plugin_owned' ? savedTab.ownerToken : undefined;
|
|
1515
|
+
const unique = (matches: BrowserTabInventoryEntry[], basis: string): BrowserTabInventoryEntry | undefined => {
|
|
1516
|
+
if (matches.length > 1) {
|
|
1517
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_REUSABLE_TAB_AMBIGUOUS',
|
|
1518
|
+
'Multiple browser tabs satisfy the same lookup; exact tab identity is not proven.',
|
|
1519
|
+
{ retryable: false, details: { sessionId: target.sessionId, basis, candidateCount: matches.length } });
|
|
1520
|
+
}
|
|
1521
|
+
return matches[0];
|
|
1522
|
+
};
|
|
1523
|
+
if (options.requireOwnedToken && expectedOwnerToken
|
|
1524
|
+
&& inventory.filter((entry) => entry.ownerToken === expectedOwnerToken).length > 1) {
|
|
1525
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_REUSABLE_TAB_AMBIGUOUS',
|
|
1526
|
+
'Multiple browser tabs report the same owned marker.',
|
|
1527
|
+
{ retryable: false, details: { sessionId: target.sessionId, basis: 'owned_token' } });
|
|
1528
|
+
}
|
|
1353
1529
|
if (options.requireOwnedToken) {
|
|
1354
1530
|
const owned = expectedOwnerToken ? inventory.find((entry) => entry.ownerToken === expectedOwnerToken) : undefined;
|
|
1355
1531
|
if (owned) {
|
|
@@ -1367,25 +1543,8 @@ function chooseTab(
|
|
|
1367
1543
|
};
|
|
1368
1544
|
}
|
|
1369
1545
|
const savedUrl = savedTab?.url ? comparableUrl(savedTab.url) : undefined;
|
|
1370
|
-
const savedTitle = savedTab?.title;
|
|
1371
|
-
const savedUrlTitle = savedUrl && savedTitle
|
|
1372
|
-
? inventory.find((entry) => comparableUrl(entry.url) === savedUrl && entry.title === savedTitle)
|
|
1373
|
-
: undefined;
|
|
1374
|
-
if (savedUrlTitle) {
|
|
1375
|
-
return {
|
|
1376
|
-
entry: savedUrlTitle,
|
|
1377
|
-
matchedBy: 'saved_url_title',
|
|
1378
|
-
sessionResume: {
|
|
1379
|
-
sessionId: target.sessionId,
|
|
1380
|
-
status: 'matched',
|
|
1381
|
-
reason: 'Saved tab URL and title matched an attached browser tab.',
|
|
1382
|
-
savedTab,
|
|
1383
|
-
},
|
|
1384
|
-
};
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
1546
|
const savedUrlOnly = savedUrl
|
|
1388
|
-
? inventory.
|
|
1547
|
+
? unique(inventory.filter((entry) => comparableUrl(entry.url) === savedUrl), 'saved_url')
|
|
1389
1548
|
: undefined;
|
|
1390
1549
|
if (savedUrlOnly) {
|
|
1391
1550
|
return {
|
|
@@ -1394,13 +1553,13 @@ function chooseTab(
|
|
|
1394
1553
|
sessionResume: {
|
|
1395
1554
|
sessionId: target.sessionId,
|
|
1396
1555
|
status: savedTab ? 'matched' : 'no_saved_tab',
|
|
1397
|
-
reason: savedTab ? 'Saved tab URL
|
|
1556
|
+
reason: savedTab ? 'Saved tab URL was unique in this browser context; page title was not used as identity.' : 'No saved tab metadata was available.',
|
|
1398
1557
|
savedTab,
|
|
1399
1558
|
},
|
|
1400
1559
|
};
|
|
1401
1560
|
}
|
|
1402
1561
|
|
|
1403
|
-
const exactUrl = inventory.
|
|
1562
|
+
const exactUrl = unique(inventory.filter((entry) => comparableUrl(entry.url) === desiredUrl), 'exact_url');
|
|
1404
1563
|
if (exactUrl) {
|
|
1405
1564
|
return {
|
|
1406
1565
|
entry: exactUrl,
|
|
@@ -1420,7 +1579,7 @@ function chooseTab(
|
|
|
1420
1579
|
};
|
|
1421
1580
|
}
|
|
1422
1581
|
|
|
1423
|
-
const blank = options.allowBlank === false ? undefined : inventory.
|
|
1582
|
+
const blank = options.allowBlank === false ? undefined : unique(inventory.filter((entry) => isBlankPage(entry.url)), 'blank');
|
|
1424
1583
|
if (blank) {
|
|
1425
1584
|
return {
|
|
1426
1585
|
entry: blank,
|
|
@@ -1544,14 +1703,71 @@ async function discoverCdpEndpoint(endpoint: string, timeoutMs: number): Promise
|
|
|
1544
1703
|
};
|
|
1545
1704
|
}
|
|
1546
1705
|
|
|
1706
|
+
async function waitForCdpEndpoint(endpoint: string, timeoutMs: number): Promise<CdpAttachAttempt> {
|
|
1707
|
+
const deadline = Date.now() + Math.max(250, timeoutMs);
|
|
1708
|
+
let lastError: unknown;
|
|
1709
|
+
while (Date.now() < deadline) {
|
|
1710
|
+
try {
|
|
1711
|
+
return await discoverCdpEndpoint(endpoint, Math.max(100, Math.min(500, deadline - Date.now())));
|
|
1712
|
+
} catch (error) {
|
|
1713
|
+
lastError = error;
|
|
1714
|
+
await delay(50);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_EXTENSION_CONTROL_UNAVAILABLE', 'Vivaldi did not expose the allocated loopback CDP endpoint after launch.', {
|
|
1718
|
+
retryable: true,
|
|
1719
|
+
details: { endpoint, error: lastError instanceof Error ? lastError.message : String(lastError ?? 'unknown') },
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
async function browserCdpExtensionTarget(session: BrowserCdpSessionLike, extensionId: string): Promise<string | undefined> {
|
|
1724
|
+
const response = await session.send('Target.getTargets') as { targetInfos?: Array<{ url?: unknown }> };
|
|
1725
|
+
const prefix = `chrome-extension://${extensionId}/`;
|
|
1726
|
+
return (response.targetInfos ?? [])
|
|
1727
|
+
.map((target) => typeof target.url === 'string' ? target.url : '')
|
|
1728
|
+
.find((url) => url.startsWith(prefix));
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
async function waitForBrowserCdpExtensionTarget(session: BrowserCdpSessionLike, extensionId: string, timeoutMs: number): Promise<string> {
|
|
1732
|
+
const deadline = Date.now() + Math.min(Math.max(timeoutMs, 250), 10_000);
|
|
1733
|
+
while (Date.now() <= deadline) {
|
|
1734
|
+
const target = await browserCdpExtensionTarget(session, extensionId);
|
|
1735
|
+
if (target) return target;
|
|
1736
|
+
await delay(100);
|
|
1737
|
+
}
|
|
1738
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_EXTENSION_POSTCONDITION_FAILED', 'Browser-level CDP did not expose an exact runtime target for the requested extension id.', {
|
|
1739
|
+
retryable: true,
|
|
1740
|
+
details: { extensionId },
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1547
1744
|
function launchOptionsForRepo(repoRoot: string, config: BrowserPluginConfig, profile: BrowserProfileSelection): Record<string, unknown> {
|
|
1745
|
+
const extensionPaths = [...(managedExtensionPaths.get(managedContextKey(profile)) ?? [])].sort();
|
|
1746
|
+
const args = [
|
|
1747
|
+
...(profile.profileDirectory ? [`--profile-directory=${profile.profileDirectory}`] : []),
|
|
1748
|
+
...(extensionPaths.length > 0 ? ['--enable-unsafe-extension-debugging'] : []),
|
|
1749
|
+
];
|
|
1750
|
+
const customProfileDefaultArgsToPreserve = [
|
|
1751
|
+
'--disable-extensions',
|
|
1752
|
+
'--disable-component-extensions-with-background-pages',
|
|
1753
|
+
'--disable-default-apps',
|
|
1754
|
+
'--disable-sync',
|
|
1755
|
+
'--use-mock-keychain',
|
|
1756
|
+
'--password-store=basic',
|
|
1757
|
+
];
|
|
1758
|
+
const ignoreDefaultArgs = config.profileMode === 'custom'
|
|
1759
|
+
? customProfileDefaultArgsToPreserve
|
|
1760
|
+
: extensionPaths.length > 0
|
|
1761
|
+
? ['--disable-extensions']
|
|
1762
|
+
: [];
|
|
1548
1763
|
return {
|
|
1549
1764
|
headless: false,
|
|
1550
1765
|
acceptDownloads: true,
|
|
1551
1766
|
viewport: { width: 1280, height: 900 },
|
|
1552
1767
|
...(config.executablePath ? { executablePath: resolveConfiguredPath(repoRoot, config.executablePath) } : {}),
|
|
1553
|
-
...(!config.executablePath && config.browserChannel && config.browserChannel !== 'chromium' ? { channel: config.browserChannel } : {}),
|
|
1554
|
-
...(
|
|
1768
|
+
...(!config.executablePath && extensionPaths.length === 0 && config.browserChannel && config.browserChannel !== 'chromium' ? { channel: config.browserChannel } : {}),
|
|
1769
|
+
...(ignoreDefaultArgs.length > 0 ? { ignoreDefaultArgs } : {}),
|
|
1770
|
+
...(args.length > 0 ? { args } : {}),
|
|
1555
1771
|
};
|
|
1556
1772
|
}
|
|
1557
1773
|
|
|
@@ -1571,8 +1787,87 @@ async function evictManagedContext(key: string): Promise<void> {
|
|
|
1571
1787
|
}
|
|
1572
1788
|
}
|
|
1573
1789
|
|
|
1790
|
+
/**
|
|
1791
|
+
* Forge-owned isolated/handoff browser profiles are per-session and cannot be
|
|
1792
|
+
* shared, so a browser still running on one of those profiles while no managed
|
|
1793
|
+
* context in this Runtime incarnation owns it is an orphan: the session that
|
|
1794
|
+
* launched it is gone. Left alone it stays visible as a logged-out browser the
|
|
1795
|
+
* user never opened and keeps a stale profile locked.
|
|
1796
|
+
*
|
|
1797
|
+
* Reclamation is mechanical and bounded: only processes whose own command line
|
|
1798
|
+
* names a Forge-owned profile path are considered, browser child processes
|
|
1799
|
+
* (`--type=`) are ignored so the main process is signalled once, and a profile
|
|
1800
|
+
* with a live managed context is never touched.
|
|
1801
|
+
*/
|
|
1802
|
+
export function reclaimOrphanedIsolatedBrowserProfiles(input: { profileRoots: readonly string[]; graceMs?: number }): { terminated: number; details: string[] } {
|
|
1803
|
+
const roots = input.profileRoots.map((root) => resolve(root)).filter(Boolean);
|
|
1804
|
+
if (roots.length === 0) return { terminated: 0, details: [] };
|
|
1805
|
+
const owned = (profileDir: string): boolean => {
|
|
1806
|
+
const normalized = resolve(profileDir);
|
|
1807
|
+
const isForgeProfile = /\/browser\/profiles\/(?:isolated|handoff)\//.test(normalized);
|
|
1808
|
+
if (!isForgeProfile) return false;
|
|
1809
|
+
return roots.some((root) => normalized === root || normalized.startsWith(`${root}${sep}`));
|
|
1810
|
+
};
|
|
1811
|
+
const liveOwners = new Set([...managedBrowserContexts.keys()].map((key) => resolve(key)));
|
|
1812
|
+
const graceMs = Math.max(0, Math.min(30_000, input.graceMs ?? 2_000));
|
|
1813
|
+
const targets: Array<{ pid: number; profileDir: string }> = [];
|
|
1814
|
+
for (const entry of runtimeHooks.listProcesses()) {
|
|
1815
|
+
if (entry.command.includes('--type=')) continue;
|
|
1816
|
+
const match = /--user-data-dir=(?:"([^"]+)"|'([^']+)'|(\S+))/.exec(entry.command);
|
|
1817
|
+
const profileDir = match?.[1] ?? match?.[2] ?? match?.[3];
|
|
1818
|
+
if (!profileDir || !owned(profileDir)) continue;
|
|
1819
|
+
if (liveOwners.has(resolve(profileDir))) continue;
|
|
1820
|
+
targets.push({ pid: entry.pid, profileDir: resolve(profileDir) });
|
|
1821
|
+
}
|
|
1822
|
+
const details: string[] = [];
|
|
1823
|
+
let terminated = 0;
|
|
1824
|
+
for (const target of targets) {
|
|
1825
|
+
runtimeHooks.signalProcess(target.pid, 'SIGTERM');
|
|
1826
|
+
details.push(`SIGTERM ${target.pid} ${target.profileDir}`);
|
|
1827
|
+
terminated += 1;
|
|
1828
|
+
}
|
|
1829
|
+
if (graceMs > 0 && targets.length > 0) {
|
|
1830
|
+
const deadline = Date.now() + graceMs;
|
|
1831
|
+
while (Date.now() < deadline) {
|
|
1832
|
+
const remaining = new Set(targets
|
|
1833
|
+
.filter((target) => runtimeHooks.listProcesses().some((entry) => entry.pid === target.pid && !entry.command.includes('--type=')))
|
|
1834
|
+
.map((target) => target.pid));
|
|
1835
|
+
if (remaining.size === 0) break;
|
|
1836
|
+
sleepSyncMs(50);
|
|
1837
|
+
}
|
|
1838
|
+
for (const target of targets) {
|
|
1839
|
+
const alive = runtimeHooks.listProcesses().some((entry) => entry.pid === target.pid && !entry.command.includes('--type='));
|
|
1840
|
+
if (!alive) continue;
|
|
1841
|
+
runtimeHooks.signalProcess(target.pid, 'SIGKILL');
|
|
1842
|
+
details.push(`SIGKILL ${target.pid} ${target.profileDir}`);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
return { terminated, details };
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
function sleepSyncMs(ms: number): void {
|
|
1849
|
+
try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.max(0, Math.min(1_000, ms))); }
|
|
1850
|
+
catch { /* best-effort bounded wait */ }
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
export function browserProfileRootsForRuntime(input: { controllerHome: string; repoRoots?: readonly string[] }): string[] {
|
|
1854
|
+
const roots = new Set<string>();
|
|
1855
|
+
const home = input.controllerHome.trim();
|
|
1856
|
+
if (home) roots.add(join(resolve(home), 'repositories'));
|
|
1857
|
+
for (const repoRoot of input.repoRoots ?? []) {
|
|
1858
|
+
const trimmed = repoRoot.trim();
|
|
1859
|
+
if (trimmed) roots.add(join(resolve(trimmed), '.forge'));
|
|
1860
|
+
}
|
|
1861
|
+
return [...roots];
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1574
1864
|
async function closeManagedContextsForRepo(repoRoot: string, options: { strict?: boolean } = {}): Promise<void> {
|
|
1575
1865
|
const canonicalRoot = resolve(repoRoot);
|
|
1866
|
+
// Explicit session cleanup must also retire a browser whose session record is
|
|
1867
|
+
// gone; otherwise the profile stays locked by a process nothing owns.
|
|
1868
|
+
reclaimOrphanedIsolatedBrowserProfiles({
|
|
1869
|
+
profileRoots: browserProfileRootsForRuntime({ controllerHome: resolveRepoPreferredControllerHome(canonicalRoot), repoRoots: [canonicalRoot] }),
|
|
1870
|
+
});
|
|
1576
1871
|
for (const [key, pending] of [...managedBrowserContexts.entries()]) {
|
|
1577
1872
|
let state: ManagedBrowserContextState;
|
|
1578
1873
|
try {
|
|
@@ -1884,6 +2179,342 @@ async function openManagedContext(
|
|
|
1884
2179
|
};
|
|
1885
2180
|
}
|
|
1886
2181
|
|
|
2182
|
+
const FORGE_NATIVE_MESSAGING_DECLARATION = 'forge-native-messaging-host.json';
|
|
2183
|
+
interface ManagedNativeMessagingHost {
|
|
2184
|
+
name: string;
|
|
2185
|
+
manifest: string;
|
|
2186
|
+
}
|
|
2187
|
+
interface BrowserExtensionInfo {
|
|
2188
|
+
id: string;
|
|
2189
|
+
name?: string;
|
|
2190
|
+
version?: string;
|
|
2191
|
+
path: string;
|
|
2192
|
+
enabled: boolean;
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
function pathWithin(root: string, candidate: string): boolean {
|
|
2196
|
+
const rel = relative(root, candidate);
|
|
2197
|
+
return rel === '' || (!rel.startsWith(`..${sep}`) && rel !== '..' && !isAbsolute(rel));
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
function unpackedExtensionPath(input: AssistantPluginActionExecutionInput): { path: string; expectedId?: string; nativeHost?: ManagedNativeMessagingHost } {
|
|
2201
|
+
const requested = requiredString(input.args.extension_path, 'extension_path');
|
|
2202
|
+
if (!isAbsolute(requested)) throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'extension_path must be absolute.', { retryable: false });
|
|
2203
|
+
if (!existsSync(requested) || !statSync(requested).isDirectory()) {
|
|
2204
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'extension_path must reference an existing directory.', { retryable: false });
|
|
2205
|
+
}
|
|
2206
|
+
const realPath = realpathSync(requested);
|
|
2207
|
+
const trustedRoots = [realpathSync(input.repoRoot)];
|
|
2208
|
+
const controllerBrowserRoot = join(resolve(input.controllerHome), 'supervisor', 'browser-adapter');
|
|
2209
|
+
if (existsSync(controllerBrowserRoot)) trustedRoots.push(realpathSync(controllerBrowserRoot));
|
|
2210
|
+
if (!trustedRoots.some((root) => pathWithin(root, realPath))) {
|
|
2211
|
+
throw new AssistantPluginError('PLUGIN_POLICY_BLOCKED', 'Unpacked extensions must resolve inside the repository or Controller-owned browser-adapter root.', { retryable: false });
|
|
2212
|
+
}
|
|
2213
|
+
const manifestPath = join(realPath, 'manifest.json');
|
|
2214
|
+
if (!existsSync(manifestPath) || !statSync(manifestPath).isFile() || !pathWithin(realPath, realpathSync(manifestPath))) {
|
|
2215
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Unpacked extension requires a regular manifest.json inside the extension directory.', { retryable: false });
|
|
2216
|
+
}
|
|
2217
|
+
let manifest: { key?: unknown };
|
|
2218
|
+
try { manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as { key?: unknown }; }
|
|
2219
|
+
catch { throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Unpacked extension manifest.json must contain valid JSON.', { retryable: false }); }
|
|
2220
|
+
let expectedId: string | undefined;
|
|
2221
|
+
if (typeof manifest.key === 'string' && /^[A-Za-z0-9+/]+={0,2}$/.test(manifest.key.trim())) {
|
|
2222
|
+
const bytes = Buffer.from(manifest.key.trim(), 'base64');
|
|
2223
|
+
if (bytes.length > 0) expectedId = createHash('sha256').update(bytes).digest('hex').slice(0, 32)
|
|
2224
|
+
.replace(/[0-9a-f]/g, (nibble) => String.fromCharCode(97 + Number.parseInt(nibble, 16)));
|
|
2225
|
+
}
|
|
2226
|
+
const declarationPath = join(realPath, FORGE_NATIVE_MESSAGING_DECLARATION);
|
|
2227
|
+
let nativeHost: ManagedNativeMessagingHost | undefined;
|
|
2228
|
+
if (existsSync(declarationPath)) {
|
|
2229
|
+
if (!expectedId || !statSync(declarationPath).isFile() || !pathWithin(realPath, realpathSync(declarationPath))) {
|
|
2230
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Managed native messaging declaration requires a regular file and stable extension manifest key.', { retryable: false });
|
|
2231
|
+
}
|
|
2232
|
+
let declaration: { name?: unknown; description?: unknown; path?: unknown; type?: unknown; allowed_origins?: unknown };
|
|
2233
|
+
try { declaration = JSON.parse(readFileSync(declarationPath, 'utf8')) as typeof declaration; }
|
|
2234
|
+
catch { throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'forge-native-messaging-host.json must contain valid JSON.', { retryable: false }); }
|
|
2235
|
+
const name = typeof declaration.name === 'string' ? declaration.name.trim() : '';
|
|
2236
|
+
const hostPath = typeof declaration.path === 'string' ? declaration.path.trim() : '';
|
|
2237
|
+
const expectedOrigin = `chrome-extension://${expectedId}/`;
|
|
2238
|
+
if (!/^[a-z0-9_]+(?:\.[a-z0-9_]+)*$/.test(name)
|
|
2239
|
+
|| declaration.type !== 'stdio'
|
|
2240
|
+
|| !isAbsolute(hostPath)
|
|
2241
|
+
|| !Array.isArray(declaration.allowed_origins)
|
|
2242
|
+
|| declaration.allowed_origins.length !== 1
|
|
2243
|
+
|| declaration.allowed_origins[0] !== expectedOrigin) {
|
|
2244
|
+
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 });
|
|
2245
|
+
}
|
|
2246
|
+
if (!existsSync(hostPath) || !statSync(hostPath).isFile()) {
|
|
2247
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Managed native messaging host executable must exist as a regular file.', { retryable: false });
|
|
2248
|
+
}
|
|
2249
|
+
const realHostPath = realpathSync(hostPath);
|
|
2250
|
+
if (!trustedRoots.some((root) => pathWithin(root, realHostPath))) {
|
|
2251
|
+
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 });
|
|
2252
|
+
}
|
|
2253
|
+
if (process.platform !== 'win32' && (statSync(realHostPath).mode & 0o111) === 0) {
|
|
2254
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Managed native messaging host must be executable.', { retryable: false });
|
|
2255
|
+
}
|
|
2256
|
+
nativeHost = {
|
|
2257
|
+
name,
|
|
2258
|
+
manifest: `${JSON.stringify({
|
|
2259
|
+
name,
|
|
2260
|
+
description: typeof declaration.description === 'string' ? declaration.description.slice(0, 512) : '',
|
|
2261
|
+
path: realHostPath,
|
|
2262
|
+
type: 'stdio',
|
|
2263
|
+
allowed_origins: [expectedOrigin],
|
|
2264
|
+
}, null, 2)}\n`,
|
|
2265
|
+
};
|
|
2266
|
+
}
|
|
2267
|
+
return { path: realPath, expectedId, nativeHost };
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
function projectManagedNativeMessagingHost(profileDir: string, host: ManagedNativeMessagingHost | undefined): boolean {
|
|
2271
|
+
if (!host) return false;
|
|
2272
|
+
const root = join(profileDir, 'NativeMessagingHosts');
|
|
2273
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
2274
|
+
const target = join(root, `${host.name}.json`);
|
|
2275
|
+
if (existsSync(target) && readFileSync(target, 'utf8') === host.manifest) return false;
|
|
2276
|
+
const temporary = `${target}.${randomUUID().slice(0, 12)}.tmp`;
|
|
2277
|
+
writeFileSync(temporary, host.manifest, { mode: 0o600 });
|
|
2278
|
+
renameSync(temporary, target);
|
|
2279
|
+
return true;
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
function extensionTargets(context: BrowserContextLike): string[] {
|
|
2283
|
+
return [...(context.serviceWorkers?.() ?? []), ...(context.backgroundPages?.() ?? [])].map((target) => target.url());
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
async function waitForManagedExtension(context: BrowserContextLike, extensionId: string, timeoutMs: number): Promise<string> {
|
|
2287
|
+
const prefix = `chrome-extension://${extensionId}/`;
|
|
2288
|
+
const deadline = Date.now() + Math.min(Math.max(timeoutMs, 250), 10_000);
|
|
2289
|
+
while (Date.now() <= deadline) {
|
|
2290
|
+
const url = extensionTargets(context).find((candidate) => candidate.startsWith(prefix));
|
|
2291
|
+
if (url) return url;
|
|
2292
|
+
await delay(100);
|
|
2293
|
+
}
|
|
2294
|
+
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 } });
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
async function browserLevelCdp(
|
|
2298
|
+
repoRoot: string,
|
|
2299
|
+
config: BrowserPluginConfig,
|
|
2300
|
+
): Promise<{ browser: BrowserLike; session: BrowserCdpSessionLike; endpoint: string } | undefined> {
|
|
2301
|
+
if (cdpEndpoints(config).length === 0 || !runtimeHooks.moduleAvailable('playwright', repoRoot)) return undefined;
|
|
2302
|
+
const runtime = runtimeHooks.loadPlaywright(repoRoot);
|
|
2303
|
+
const connectOverCDP = runtime.chromium.connectOverCDP;
|
|
2304
|
+
if (typeof connectOverCDP !== 'function') return undefined;
|
|
2305
|
+
for (const endpoint of cdpEndpoints(config)) {
|
|
2306
|
+
try {
|
|
2307
|
+
const discovered = await discoverCdpEndpoint(endpoint, cdpDiscoveryTimeout(config));
|
|
2308
|
+
const browser = await connectOverCDP.call(runtime.chromium, discovered.discoveredEndpoint ?? endpoint);
|
|
2309
|
+
if (typeof browser.newBrowserCDPSession !== 'function') {
|
|
2310
|
+
if (browser.disconnect) await Promise.resolve(browser.disconnect()).catch(() => undefined);
|
|
2311
|
+
else if (browser.close) await browser.close().catch(() => undefined);
|
|
2312
|
+
continue;
|
|
2313
|
+
}
|
|
2314
|
+
const session = await browser.newBrowserCDPSession();
|
|
2315
|
+
return { browser, session, endpoint: discovered.discoveredEndpoint ?? endpoint };
|
|
2316
|
+
} catch { /* Pre-dispatch endpoint discovery/attach failure; try the next configured endpoint. */ }
|
|
2317
|
+
}
|
|
2318
|
+
return undefined;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
async function closeBrowserLevelCdp(handle: { browser: BrowserLike; session: BrowserCdpSessionLike }): Promise<void> {
|
|
2322
|
+
await handle.session.detach?.().catch(() => undefined);
|
|
2323
|
+
if (handle.browser.disconnect) await Promise.resolve(handle.browser.disconnect()).catch(() => undefined);
|
|
2324
|
+
else if (handle.browser.close) await handle.browser.close().catch(() => undefined);
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
async function cdpExtensionList(repoRoot: string, config: BrowserPluginConfig): Promise<{ provider: string; endpoint: string; extensions: BrowserExtensionInfo[] } | undefined> {
|
|
2328
|
+
const handle = await browserLevelCdp(repoRoot, config);
|
|
2329
|
+
if (!handle) return undefined;
|
|
2330
|
+
try {
|
|
2331
|
+
const result = await handle.session.send('Extensions.getExtensions') as { extensions?: BrowserExtensionInfo[] };
|
|
2332
|
+
return { provider: 'playwright-cdp', endpoint: handle.endpoint, extensions: Array.isArray(result.extensions) ? result.extensions : [] };
|
|
2333
|
+
} finally { await closeBrowserLevelCdp(handle); }
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
async function loadAndVerifyUnpackedExtension(
|
|
2337
|
+
session: BrowserCdpSessionLike,
|
|
2338
|
+
extensionPath: string,
|
|
2339
|
+
expectedId: string | undefined,
|
|
2340
|
+
enableInIncognito: boolean,
|
|
2341
|
+
): Promise<BrowserExtensionInfo> {
|
|
2342
|
+
const loaded = await session.send('Extensions.loadUnpacked', {
|
|
2343
|
+
path: extensionPath,
|
|
2344
|
+
...(enableInIncognito ? { enableInIncognito: true } : {}),
|
|
2345
|
+
}) as { id?: string };
|
|
2346
|
+
const id = typeof loaded.id === 'string' && loaded.id.trim() ? loaded.id.trim() : undefined;
|
|
2347
|
+
if (!id) throw new AssistantPluginError('PLUGIN_BROWSER_EXTENSION_POSTCONDITION_FAILED', 'Browser did not return an extension id after loadUnpacked.', { retryable: true });
|
|
2348
|
+
if (expectedId && id !== expectedId) {
|
|
2349
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_EXTENSION_POSTCONDITION_FAILED', 'Browser returned an unpacked extension id that does not match the manifest-derived stable id.', {
|
|
2350
|
+
retryable: false,
|
|
2351
|
+
details: { extensionId: id, expectedExtensionId: expectedId },
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
2354
|
+
const listed = await session.send('Extensions.getExtensions') as { extensions?: BrowserExtensionInfo[] };
|
|
2355
|
+
const exact = (listed.extensions ?? []).find((entry) => {
|
|
2356
|
+
if (entry.id !== id || entry.enabled !== true) return false;
|
|
2357
|
+
try { return realpathSync(entry.path) === extensionPath; } catch { return false; }
|
|
2358
|
+
});
|
|
2359
|
+
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 } });
|
|
2360
|
+
return exact;
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
function customVivaldiExtensionInstall(config: BrowserPluginConfig, repoRoot: string): { executable: string } | undefined {
|
|
2364
|
+
if (config.profileMode !== 'custom' || !config.executablePath) return undefined;
|
|
2365
|
+
const executable = resolveConfiguredPath(repoRoot, config.executablePath);
|
|
2366
|
+
return basename(executable).toLowerCase() === 'vivaldi' ? { executable } : undefined;
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
function processUsesUserDataDir(command: string, profileDir: string): boolean {
|
|
2370
|
+
const expected = resolve(profileDir);
|
|
2371
|
+
const forms = [
|
|
2372
|
+
`--user-data-dir=${expected}`,
|
|
2373
|
+
`--user-data-dir="${expected}"`,
|
|
2374
|
+
`--user-data-dir='${expected}'`,
|
|
2375
|
+
];
|
|
2376
|
+
return forms.some((form) => {
|
|
2377
|
+
const index = command.indexOf(form);
|
|
2378
|
+
if (index < 0) return false;
|
|
2379
|
+
const next = command[index + form.length];
|
|
2380
|
+
return next === undefined || /\s/.test(next);
|
|
2381
|
+
});
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
function exactCustomVivaldiProfileOwner(
|
|
2385
|
+
executable: string,
|
|
2386
|
+
profile: BrowserProfileSelection,
|
|
2387
|
+
): { pid: number; command: string } | undefined {
|
|
2388
|
+
let lockTarget: string;
|
|
2389
|
+
try { lockTarget = readlinkSync(join(profile.profileDir, 'SingletonLock')); }
|
|
2390
|
+
catch { return undefined; }
|
|
2391
|
+
const pidText = /(?:^|-)(\d+)$/.exec(lockTarget.trim())?.[1];
|
|
2392
|
+
const pid = pidText ? Number(pidText) : 0;
|
|
2393
|
+
if (!Number.isInteger(pid) || pid <= 0) return undefined;
|
|
2394
|
+
const process = runtimeHooks.listProcesses().find((entry) => entry.pid === pid && !entry.command.includes('--type='));
|
|
2395
|
+
if (!process || !process.command.includes(executable)) return undefined;
|
|
2396
|
+
const hasExplicitProfileDir = process.command.includes('--user-data-dir=');
|
|
2397
|
+
if (hasExplicitProfileDir) {
|
|
2398
|
+
if (!processUsesUserDataDir(process.command, profile.profileDir)) return undefined;
|
|
2399
|
+
} else {
|
|
2400
|
+
const defaultVivaldiProfileDir = resolve(join(homedir(), 'Library', 'Application Support', 'Vivaldi'));
|
|
2401
|
+
if (resolve(profile.profileDir) !== defaultVivaldiProfileDir) return undefined;
|
|
2402
|
+
}
|
|
2403
|
+
return process;
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
async function gracefullyRestartExactCustomVivaldiOwner(
|
|
2407
|
+
executable: string,
|
|
2408
|
+
profile: BrowserProfileSelection,
|
|
2409
|
+
timeoutMs: number,
|
|
2410
|
+
): Promise<{ restarted: boolean; pid?: number }> {
|
|
2411
|
+
const owner = exactCustomVivaldiProfileOwner(executable, profile);
|
|
2412
|
+
if (!owner) return { restarted: false };
|
|
2413
|
+
runtimeHooks.signalProcess(owner.pid, 'SIGTERM');
|
|
2414
|
+
const deadline = Date.now() + Math.max(500, Math.min(timeoutMs, 10_000));
|
|
2415
|
+
while (Date.now() < deadline) {
|
|
2416
|
+
const alive = runtimeHooks.listProcesses().some((entry) => entry.pid === owner.pid && !entry.command.includes('--type='));
|
|
2417
|
+
if (!alive) return { restarted: true, pid: owner.pid };
|
|
2418
|
+
await delay(50);
|
|
2419
|
+
}
|
|
2420
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_PROFILE_IN_USE', 'The exact Vivaldi custom-profile owner did not exit after a bounded graceful restart request.', {
|
|
2421
|
+
retryable: true,
|
|
2422
|
+
details: { pid: owner.pid, profileDir: profile.profileDir },
|
|
2423
|
+
});
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
async function installExtensionThroughCustomVivaldiCdpPort(
|
|
2427
|
+
input: AssistantPluginActionExecutionInput,
|
|
2428
|
+
config: BrowserPluginConfig,
|
|
2429
|
+
profile: BrowserProfileSelection,
|
|
2430
|
+
extensionPath: string,
|
|
2431
|
+
expectedId: string,
|
|
2432
|
+
): Promise<Record<string, unknown>> {
|
|
2433
|
+
const vivaldi = customVivaldiExtensionInstall(config, input.repoRoot);
|
|
2434
|
+
if (!vivaldi) {
|
|
2435
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_EXTENSION_CONTROL_UNAVAILABLE', 'Custom Vivaldi unpacked-extension install requires the configured Vivaldi executable.', { retryable: true });
|
|
2436
|
+
}
|
|
2437
|
+
const timeoutMs = positiveNumber(input.args.timeout_ms, config.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
2438
|
+
await gracefullyRestartExactCustomVivaldiOwner(vivaldi.executable, profile, timeoutMs);
|
|
2439
|
+
const port = await runtimeHooks.allocateLoopbackPort();
|
|
2440
|
+
const endpoint = `http://127.0.0.1:${port}`;
|
|
2441
|
+
const args = [
|
|
2442
|
+
`--user-data-dir=${profile.profileDir}`,
|
|
2443
|
+
...(profile.profileDirectory ? [`--profile-directory=${profile.profileDirectory}`] : []),
|
|
2444
|
+
'--remote-debugging-address=127.0.0.1',
|
|
2445
|
+
`--remote-debugging-port=${port}`,
|
|
2446
|
+
'--enable-unsafe-extension-debugging',
|
|
2447
|
+
'--no-first-run',
|
|
2448
|
+
'--no-default-browser-check',
|
|
2449
|
+
];
|
|
2450
|
+
const launched = runtimeHooks.launchProcess(vivaldi.executable, args);
|
|
2451
|
+
const pid = launched.pid;
|
|
2452
|
+
if (!pid || !Number.isInteger(pid) || pid <= 0) {
|
|
2453
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_EXTENSION_CONTROL_UNAVAILABLE', 'Vivaldi launcher did not return a process identity for bounded cleanup.', { retryable: true });
|
|
2454
|
+
}
|
|
2455
|
+
let session: BrowserCdpSessionLike | undefined;
|
|
2456
|
+
let keepBrowser = false;
|
|
2457
|
+
try {
|
|
2458
|
+
const discovered = await waitForCdpEndpoint(endpoint, timeoutMs);
|
|
2459
|
+
const browserEndpoint = discovered.discoveredEndpoint ?? endpoint;
|
|
2460
|
+
session = await runtimeHooks.connectBrowserCdp(browserEndpoint, Math.max(1_000, Math.min(timeoutMs, 30_000)));
|
|
2461
|
+
const listed = await session.send('Extensions.getExtensions') as { extensions?: BrowserExtensionInfo[] };
|
|
2462
|
+
let verifiedExtension = (listed.extensions ?? []).find((entry) => {
|
|
2463
|
+
if (entry.id !== expectedId || entry.enabled !== true) return false;
|
|
2464
|
+
try { return realpathSync(entry.path) === extensionPath; } catch { return false; }
|
|
2465
|
+
});
|
|
2466
|
+
let runtimeTarget = await browserCdpExtensionTarget(session, expectedId);
|
|
2467
|
+
if (!verifiedExtension || !runtimeTarget) {
|
|
2468
|
+
verifiedExtension = await loadAndVerifyUnpackedExtension(session, extensionPath, expectedId, input.args.enable_in_incognito === true);
|
|
2469
|
+
if (!runtimeTarget) runtimeTarget = await waitForBrowserCdpExtensionTarget(session, expectedId, timeoutMs);
|
|
2470
|
+
}
|
|
2471
|
+
keepBrowser = true;
|
|
2472
|
+
return {
|
|
2473
|
+
provider: 'native-cdp-vivaldi-custom',
|
|
2474
|
+
endpoint,
|
|
2475
|
+
extension: { ...verifiedExtension, runtimeTarget },
|
|
2476
|
+
verified: true,
|
|
2477
|
+
};
|
|
2478
|
+
} finally {
|
|
2479
|
+
await session?.detach?.().catch(() => undefined);
|
|
2480
|
+
if (!keepBrowser) runtimeHooks.signalProcess(pid, 'SIGTERM');
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
async function managedCdpInstallExtension(
|
|
2485
|
+
input: AssistantPluginActionExecutionInput,
|
|
2486
|
+
context: BrowserContextLike,
|
|
2487
|
+
extensionPath: string,
|
|
2488
|
+
expectedId: string,
|
|
2489
|
+
runtimeTargetPresent: boolean,
|
|
2490
|
+
): Promise<BrowserExtensionInfo> {
|
|
2491
|
+
const browser = context.browser?.();
|
|
2492
|
+
if (!browser || typeof browser.newBrowserCDPSession !== 'function') {
|
|
2493
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_EXTENSION_CONTROL_UNAVAILABLE', 'Managed unpacked-extension install requires browser-level CDP on the launched Chromium context.', { retryable: true });
|
|
2494
|
+
}
|
|
2495
|
+
const session = await browser.newBrowserCDPSession();
|
|
2496
|
+
try {
|
|
2497
|
+
const listed = await session.send('Extensions.getExtensions') as { extensions?: BrowserExtensionInfo[] };
|
|
2498
|
+
const exact = (listed.extensions ?? []).find((entry) => {
|
|
2499
|
+
if (entry.id !== expectedId || entry.enabled !== true) return false;
|
|
2500
|
+
try { return realpathSync(entry.path) === extensionPath; } catch { return false; }
|
|
2501
|
+
});
|
|
2502
|
+
if (exact && runtimeTargetPresent) return exact;
|
|
2503
|
+
return await loadAndVerifyUnpackedExtension(session, extensionPath, expectedId, input.args.enable_in_incognito === true);
|
|
2504
|
+
} finally {
|
|
2505
|
+
await session.detach?.().catch(() => undefined);
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
async function cdpInstallExtension(input: AssistantPluginActionExecutionInput, config: BrowserPluginConfig, extensionPath: string, expectedId?: string): Promise<Record<string, unknown> | undefined> {
|
|
2510
|
+
const handle = await browserLevelCdp(input.repoRoot, config);
|
|
2511
|
+
if (!handle) return undefined;
|
|
2512
|
+
try {
|
|
2513
|
+
const exact = await loadAndVerifyUnpackedExtension(handle.session, extensionPath, expectedId, input.args.enable_in_incognito === true);
|
|
2514
|
+
return { provider: 'playwright-cdp', endpoint: handle.endpoint, extension: exact, verified: true };
|
|
2515
|
+
} finally { await closeBrowserLevelCdp(handle); }
|
|
2516
|
+
}
|
|
2517
|
+
|
|
1887
2518
|
async function openAttachedContext(
|
|
1888
2519
|
runtime: PlaywrightRuntime,
|
|
1889
2520
|
repoRoot: string,
|
|
@@ -1978,15 +2609,13 @@ async function openNativeAttachedContext(
|
|
|
1978
2609
|
args: Record<string, unknown>,
|
|
1979
2610
|
): Promise<{ handle?: BrowserOpenHandle; attempts: MacOsBrowserAttachAttempt[] }> {
|
|
1980
2611
|
if (config.nativeAttachMode === 'disabled') return { attempts: [] };
|
|
1981
|
-
if (!target.existingSession && !stringValue(args.session_id)) {
|
|
2612
|
+
if (!target.existingSession && !stringValue(args.session_id) && args.__forge_computer_native_only !== true) {
|
|
1982
2613
|
await inspectNativeOwnedSessions({ repoRoot, savedSessions: listSavedSessions(repoRoot), timeoutMs: config.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS, pruneDead: true });
|
|
1983
2614
|
}
|
|
1984
2615
|
const timeout = positiveNumber(args.timeout_ms, config.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
1985
2616
|
const savedTab = target.existingSession?.browser?.tab;
|
|
1986
2617
|
const savedOwnership = savedTab?.ownership;
|
|
1987
2618
|
let effectiveOwnership = savedOwnership;
|
|
1988
|
-
let recoveredTabTitle = '';
|
|
1989
|
-
let recoveredTabUrl: string | undefined;
|
|
1990
2619
|
const savedProduct = target.existingSession?.browser?.provider === 'macos-apple-events'
|
|
1991
2620
|
&& savedTab
|
|
1992
2621
|
&& (savedOwnership === 'plugin_owned' || savedOwnership === 'user_owned')
|
|
@@ -2036,63 +2665,18 @@ async function openNativeAttachedContext(
|
|
|
2036
2665
|
savedTab,
|
|
2037
2666
|
};
|
|
2038
2667
|
} catch (error) {
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2668
|
+
// URL/title or a formerly active window cannot identify a lost native tab.
|
|
2669
|
+
// Never silently adopt another user's tab or create a replacement for a saved session.
|
|
2670
|
+
if (savedOwnership === 'user_owned' && error instanceof AssistantPluginError && error.code === 'PLUGIN_BROWSER_NATIVE_TAB_IDENTITY_UNPROVEN') {
|
|
2042
2671
|
throw error;
|
|
2043
2672
|
}
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
},
|
|
2052
|
-
);
|
|
2053
|
-
}
|
|
2054
|
-
let inventory: Awaited<ReturnType<typeof listMacOsBrowserTabs>> | undefined;
|
|
2055
|
-
let selection: ReturnType<typeof selectReusableNativeTab> | undefined;
|
|
2056
|
-
let recoveryFailure: string | undefined;
|
|
2057
|
-
try {
|
|
2058
|
-
inventory = await listMacOsBrowserTabs(savedProduct, timeout);
|
|
2059
|
-
selection = selectReusableNativeTab(
|
|
2060
|
-
[savedTab.url, target.existingSession?.url ?? '', target.url],
|
|
2061
|
-
inventory,
|
|
2062
|
-
savedTab.windowId,
|
|
2063
|
-
);
|
|
2064
|
-
if (selection.candidate) {
|
|
2065
|
-
const recovered = await reattachMacOsBrowserOwnedPage(savedProduct, { windowId: selection.candidate.windowId, tabId: selection.candidate.tabId }, timeout);
|
|
2066
|
-
page = recovered.page as unknown as PageLike;
|
|
2067
|
-
discovered = { attachment: recovered.attachment, attempts: recovered.attachment.attempts };
|
|
2068
|
-
matchedBy = 'recovered_tab';
|
|
2069
|
-
effectiveOwnership = 'user_owned';
|
|
2070
|
-
recoveredTabTitle = selection.candidate.title;
|
|
2071
|
-
recoveredTabUrl = selection.candidate.url;
|
|
2072
|
-
sessionResume = {
|
|
2073
|
-
sessionId: target.sessionId,
|
|
2074
|
-
status: 'matched',
|
|
2075
|
-
reason: selection.match === 'exact_url'
|
|
2076
|
-
? 'Saved plugin-owned tab disappeared; rebound safely to the unique currently open tab with the same URL and preserved it as user-owned.'
|
|
2077
|
-
: 'Saved plugin-owned tab disappeared; rebound safely to the unique currently open tab for the same stable app entity and preserved its current page as user-owned.',
|
|
2078
|
-
savedTab,
|
|
2079
|
-
};
|
|
2080
|
-
}
|
|
2081
|
-
} catch (recoveryError) {
|
|
2082
|
-
recoveryFailure = recoveryError instanceof Error ? recoveryError.message : String(recoveryError);
|
|
2083
|
-
}
|
|
2084
|
-
if (!page) {
|
|
2085
|
-
sessionResume = {
|
|
2086
|
-
sessionId: target.sessionId,
|
|
2087
|
-
status: 'stale_tab',
|
|
2088
|
-
reason: recoveryFailure
|
|
2089
|
-
? `Saved plugin-owned macOS browser tab disappeared and current-tab recovery failed (${recoveryFailure}); creating one replacement plugin-owned tab.`
|
|
2090
|
-
: selection?.match === 'ambiguous'
|
|
2091
|
-
? 'Saved plugin-owned macOS browser tab disappeared and multiple reusable current tabs matched; refusing to guess between user tabs and creating one replacement plugin-owned tab.'
|
|
2092
|
-
: 'Saved plugin-owned macOS browser tab disappeared and no uniquely reusable current tab matched; creating one replacement plugin-owned tab.',
|
|
2093
|
-
savedTab,
|
|
2094
|
-
};
|
|
2095
|
-
}
|
|
2673
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_SESSION_STATE_LOST',
|
|
2674
|
+
'Saved native browser tab could not be reattached by its exact identity; refusing URL-based recovery or replacement.',
|
|
2675
|
+
{ retryable: false, details: {
|
|
2676
|
+
sessionId: target.sessionId, browserProduct: savedProduct, ownership: savedOwnership,
|
|
2677
|
+
windowId: savedTab.windowId, tabId: savedTab.tabId,
|
|
2678
|
+
causeCode: error instanceof AssistantPluginError ? error.code : 'NATIVE_REATTACH_FAILED',
|
|
2679
|
+
} });
|
|
2096
2680
|
}
|
|
2097
2681
|
} else {
|
|
2098
2682
|
sessionResume = savedTab
|
|
@@ -2100,11 +2684,105 @@ async function openNativeAttachedContext(
|
|
|
2100
2684
|
: { sessionId: target.sessionId, status: 'no_saved_tab', reason: 'Created one plugin-owned tab while preserving the user active tab.' };
|
|
2101
2685
|
}
|
|
2102
2686
|
|
|
2687
|
+
// Session-bound reads and mutations must never turn a dead tab into a new
|
|
2688
|
+
// tab or silently select another profile. The explicit open-page route is
|
|
2689
|
+
// separate and retains its documented create/recover semantics.
|
|
2690
|
+
if (args.__forge_require_existing_resource === true && target.existingSession && !page) {
|
|
2691
|
+
throw new AssistantPluginError(
|
|
2692
|
+
'PLUGIN_BROWSER_SESSION_STATE_LOST',
|
|
2693
|
+
'The saved native browser tab is unavailable and no exact existing tab can be verified; the session was not moved to another profile or recreated.',
|
|
2694
|
+
{ retryable: false, details: {
|
|
2695
|
+
sessionId: target.sessionId, browserProduct: savedProduct,
|
|
2696
|
+
savedWindowId: savedTab?.windowId, savedTabId: savedTab?.tabId,
|
|
2697
|
+
} },
|
|
2698
|
+
);
|
|
2699
|
+
}
|
|
2103
2700
|
let createdThisCallRef: MacOsBrowserTabRef | undefined;
|
|
2701
|
+
let preferredNativeWindowId: string | undefined;
|
|
2104
2702
|
if (!discovered?.attachment) {
|
|
2105
2703
|
const candidates = savedProduct ? [savedProduct] : (config.nativeBrowserCandidates ?? [...DEFAULT_USER_NATIVE_BROWSER_CANDIDATES]);
|
|
2106
|
-
|
|
2704
|
+
let preferredCandidates = candidates;
|
|
2705
|
+
if (candidates.length > 1 && !target.existingSession && (args.__forge_computer_native_only === true || !stringValue(args.session_id))) {
|
|
2706
|
+
// Choosing the frontmost browser can select a different, signed-out
|
|
2707
|
+
// profile even while the user's matching Chrome tab is in the background.
|
|
2708
|
+
// Prefer exact URL evidence across *all* configured native products.
|
|
2709
|
+
const inventories = await Promise.all(candidates.map(async (product) => {
|
|
2710
|
+
try { await assertMacOsBrowserApplicationRunning(product, timeout); }
|
|
2711
|
+
catch (error) {
|
|
2712
|
+
if (error instanceof AssistantPluginError && [
|
|
2713
|
+
'PLUGIN_BROWSER_NATIVE_APP_NOT_RUNNING', 'PLUGIN_BROWSER_NATIVE_APP_NOT_INSTALLED',
|
|
2714
|
+
'PLUGIN_BROWSER_NATIVE_APP_UNSUPPORTED_PLATFORM',
|
|
2715
|
+
].includes(error.code)) return undefined;
|
|
2716
|
+
throw error;
|
|
2717
|
+
}
|
|
2718
|
+
try {
|
|
2719
|
+
const inventory = await listMacOsBrowserTabs(product, timeout, { correctFrontmost: false });
|
|
2720
|
+
if (inventory.truncated) throw new AssistantPluginError('PLUGIN_COMPUTER_BROWSER_INVENTORY_INCOMPLETE',
|
|
2721
|
+
'Native browser inventory truncated; cannot prove the existing target tab is absent.',
|
|
2722
|
+
{ retryable: true, details: { browserProduct: product } });
|
|
2723
|
+
return { product, inventory };
|
|
2724
|
+
} catch (error) {
|
|
2725
|
+
// Unreadable background tabs are not absent tabs: no replacement
|
|
2726
|
+
// browser or new tab may be created from an incomplete inventory.
|
|
2727
|
+
if (args.__forge_computer_native_only === true) {
|
|
2728
|
+
throw new AssistantPluginError('PLUGIN_COMPUTER_BROWSER_INVENTORY_UNAVAILABLE',
|
|
2729
|
+
'Cannot enumerate existing native browser tabs; refusing to create a duplicate.',
|
|
2730
|
+
{ retryable: true, details: { browserProduct: product, cause: error instanceof Error ? error.message : String(error) } });
|
|
2731
|
+
}
|
|
2732
|
+
return undefined;
|
|
2733
|
+
}
|
|
2734
|
+
}));
|
|
2735
|
+
const observed = inventories.filter((item): item is NonNullable<typeof item> => Boolean(item));
|
|
2736
|
+
const exact = observed.flatMap(({ product, inventory }) => inventory.tabs
|
|
2737
|
+
.filter((entry) => comparableUrl(entry.url) === comparableUrl(target.url))
|
|
2738
|
+
.map((entry) => ({ product, entry })));
|
|
2739
|
+
if (exact.length > 1) {
|
|
2740
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_REUSABLE_TAB_AMBIGUOUS',
|
|
2741
|
+
'Multiple browser products have existing tabs for the exact URL; refusing to guess and create another tab.',
|
|
2742
|
+
{ retryable: false, details: { targetUrl: target.url, candidateCount: exact.length } });
|
|
2743
|
+
}
|
|
2744
|
+
if (exact.length === 1) {
|
|
2745
|
+
preferredCandidates = [exact[0]!.product];
|
|
2746
|
+
} else {
|
|
2747
|
+
// Same-origin tabs only identify a likely browser *profile*, not an
|
|
2748
|
+
// exact resource. A new tab may be created there, but never overwrite
|
|
2749
|
+
// an unrelated user tab or invent proof of authentication.
|
|
2750
|
+
const origin = (() => { try { return new URL(target.url).origin; } catch { return ''; } })();
|
|
2751
|
+
const originProducts = observed.filter(({ inventory }) => origin && inventory.tabs.some((entry) => {
|
|
2752
|
+
try { return new URL(entry.url).origin === origin; } catch { return false; }
|
|
2753
|
+
})).map(({ product }) => product);
|
|
2754
|
+
if (originProducts.length === 1) {
|
|
2755
|
+
preferredCandidates = [originProducts[0]!];
|
|
2756
|
+
const matchingWindows = new Set(observed
|
|
2757
|
+
.filter(({ product }) => product === originProducts[0])
|
|
2758
|
+
.flatMap(({ inventory }) => inventory.tabs.filter((entry) => {
|
|
2759
|
+
try { return new URL(entry.url).origin === origin; } catch { return false; }
|
|
2760
|
+
}).map((entry) => entry.windowId)));
|
|
2761
|
+
if (matchingWindows.size === 1) preferredNativeWindowId = [...matchingWindows][0];
|
|
2762
|
+
} else if (observed.length === 1) {
|
|
2763
|
+
// Exactly one available browser product is observed. Product discovery
|
|
2764
|
+
// must not silently fall through multiple provider backends.
|
|
2765
|
+
preferredCandidates = [observed[0]!.product];
|
|
2766
|
+
} else if (observed.length === 0) {
|
|
2767
|
+
throw new AssistantPluginError(args.__forge_computer_native_only === true
|
|
2768
|
+
? 'PLUGIN_COMPUTER_NATIVE_BROWSER_UNAVAILABLE' : 'PLUGIN_BROWSER_ATTACH_UNAVAILABLE',
|
|
2769
|
+
'No usable existing native browser was observed; refusing to launch or guess a provider.',
|
|
2770
|
+
{ retryable: false });
|
|
2771
|
+
} else {
|
|
2772
|
+
if (args.__forge_require_existing_resource === true) {
|
|
2773
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_EXISTING_TAB_NOT_FOUND',
|
|
2774
|
+
'No uniquely reusable tab matches the requested existing resource; refusing new tab creation.',
|
|
2775
|
+
{ retryable: false });
|
|
2776
|
+
}
|
|
2777
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_PROVIDER_CARDINALITY_INVALID',
|
|
2778
|
+
'No uniquely identified existing browser product; refusing implicit cross-provider selection.',
|
|
2779
|
+
{ retryable: false, details: { candidateCount: observed.length } });
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
discovered = await discoverMacOsBrowserAttachment(preferredCandidates, Math.min(timeout, MAX_CDP_DISCOVERY_TIMEOUT_MS));
|
|
2107
2784
|
const zeroWindowAttempt = args.__forge_require_existing_resource !== true
|
|
2785
|
+
&& args.__forge_computer_native_only !== true
|
|
2108
2786
|
? discovered.attempts.find((attempt) => attempt.status === 'unavailable' && attempt.error?.includes('FORGE_NO_BROWSER_WINDOW'))
|
|
2109
2787
|
: undefined;
|
|
2110
2788
|
if (!discovered.attachment && zeroWindowAttempt) {
|
|
@@ -2112,6 +2790,7 @@ async function openNativeAttachedContext(
|
|
|
2112
2790
|
zeroWindowAttempt.product,
|
|
2113
2791
|
target.url,
|
|
2114
2792
|
discovered.attempts,
|
|
2793
|
+
undefined,
|
|
2115
2794
|
timeout,
|
|
2116
2795
|
);
|
|
2117
2796
|
page = created.page as unknown as PageLike;
|
|
@@ -2220,12 +2899,17 @@ async function openNativeAttachedContext(
|
|
|
2220
2899
|
}
|
|
2221
2900
|
};
|
|
2222
2901
|
try {
|
|
2223
|
-
if (!page && !savedTab && !stringValue(args.session_id)) {
|
|
2902
|
+
if (!page && !savedTab && !target.existingSession && (args.__forge_computer_native_only === true || !stringValue(args.session_id))) {
|
|
2224
2903
|
let inventory: Awaited<ReturnType<typeof listMacOsBrowserTabs>> | undefined;
|
|
2225
2904
|
try {
|
|
2226
2905
|
inventory = await listMacOsBrowserTabs(activeAttachment.metadata.product, timeout);
|
|
2227
2906
|
} catch (error) {
|
|
2228
2907
|
if (!nativeTabInventoryUnsupported(error)) throw error;
|
|
2908
|
+
if (args.__forge_computer_native_only === true) {
|
|
2909
|
+
throw new AssistantPluginError('PLUGIN_COMPUTER_BROWSER_INVENTORY_UNAVAILABLE',
|
|
2910
|
+
'Native tab inventory unavailable; Computer cannot prove an existing tab is absent.',
|
|
2911
|
+
{ retryable: true });
|
|
2912
|
+
}
|
|
2229
2913
|
}
|
|
2230
2914
|
if (inventory) {
|
|
2231
2915
|
const ownedSelection = selectLiveForgeOwnedNativeTab(repoRoot, target.url, activeAttachment.metadata.product, inventory);
|
|
@@ -2279,8 +2963,6 @@ async function openNativeAttachedContext(
|
|
|
2279
2963
|
activeAttachment = rebound.attachment;
|
|
2280
2964
|
matchedBy = 'exact_url';
|
|
2281
2965
|
effectiveOwnership = 'user_owned';
|
|
2282
|
-
recoveredTabTitle = exactSelection.candidate.title;
|
|
2283
|
-
recoveredTabUrl = exactSelection.candidate.url;
|
|
2284
2966
|
sessionResume = {
|
|
2285
2967
|
sessionId: target.sessionId,
|
|
2286
2968
|
status: 'matched',
|
|
@@ -2291,7 +2973,23 @@ async function openNativeAttachedContext(
|
|
|
2291
2973
|
}
|
|
2292
2974
|
}
|
|
2293
2975
|
if (!page) {
|
|
2294
|
-
|
|
2976
|
+
// Explicit existing-resource requests (including read-only live
|
|
2977
|
+
// acceptance) must never create a new tab if no exact tab was found.
|
|
2978
|
+
if (!target.existingSession && args.__forge_require_existing_resource === true) {
|
|
2979
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_EXISTING_TAB_NOT_FOUND',
|
|
2980
|
+
'No uniquely reusable native tab matches the requested URL; refusing to create a new tab.',
|
|
2981
|
+
{ retryable: false });
|
|
2982
|
+
}
|
|
2983
|
+
if (preferredNativeWindowId) {
|
|
2984
|
+
const created = await createMacOsBrowserOwnedPageForProduct(
|
|
2985
|
+
activeAttachment.metadata.product, target.url, activeAttachment.attempts, preferredNativeWindowId, timeout,
|
|
2986
|
+
);
|
|
2987
|
+
page = created.page as unknown as PageLike;
|
|
2988
|
+
activeAttachment = created.attachment;
|
|
2989
|
+
discovered = { attachment: created.attachment, attempts: created.attachment.attempts };
|
|
2990
|
+
} else {
|
|
2991
|
+
page = await createMacOsBrowserOwnedPage(activeAttachment, target.url, timeout) as unknown as PageLike;
|
|
2992
|
+
}
|
|
2295
2993
|
createdThisCallRef = (page as unknown as { tabRef?: () => MacOsBrowserTabRef | undefined }).tabRef?.();
|
|
2296
2994
|
const liveIdentity = await settleNativeCreatedPageIdentity(
|
|
2297
2995
|
page as unknown as {
|
|
@@ -2311,21 +3009,15 @@ async function openNativeAttachedContext(
|
|
|
2311
3009
|
if (comparableUrl(page.url()) !== comparableUrl(target.url) && !explicitUserOwnedUrlMatch) {
|
|
2312
3010
|
const currentRef = (page as unknown as { tabRef?: () => MacOsBrowserTabRef | undefined }).tabRef?.();
|
|
2313
3011
|
if (effectiveOwnership === 'user_owned') {
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
'Explicitly adopted user-owned tab no longer matches the saved session URL; refusing to navigate, replace, or close the user tab.',
|
|
2320
|
-
{ retryable: true, details: { sessionId: target.sessionId, expectedUrl: target.url, actualUrl: page.url(), windowId: savedTab?.windowId, tabId: savedTab?.tabId } },
|
|
2321
|
-
);
|
|
2322
|
-
}
|
|
3012
|
+
throw new AssistantPluginError(
|
|
3013
|
+
'PLUGIN_BROWSER_ADOPTED_TAB_URL_MISMATCH',
|
|
3014
|
+
'Explicitly adopted user-owned tab no longer matches the saved session URL; refusing to navigate, replace, or close the user tab.',
|
|
3015
|
+
{ retryable: true, details: { sessionId: target.sessionId, expectedUrl: target.url, actualUrl: page.url(), windowId: savedTab?.windowId, tabId: savedTab?.tabId } },
|
|
3016
|
+
);
|
|
2323
3017
|
}
|
|
2324
3018
|
const sessionTargetUnchanged = Boolean(target.existingSession?.url) && comparableUrl(target.url) === comparableUrl(target.existingSession?.url ?? '');
|
|
2325
3019
|
const preserveOwnedSameOriginDrift = matchedBy === 'owned_token' && sessionTargetUnchanged && sameOrigin(page.url(), target.url);
|
|
2326
|
-
if (
|
|
2327
|
-
// Safe recovery adopts an already-open tab. Its current page is authoritative and must not be navigated.
|
|
2328
|
-
} else if (preserveOwnedSameOriginDrift) {
|
|
3020
|
+
if (preserveOwnedSameOriginDrift) {
|
|
2329
3021
|
sessionResume = { sessionId: target.sessionId, status: 'matched', reason: 'Plugin-owned tab navigated within the same origin; preserved the live tab and refreshed its session URL.', savedTab };
|
|
2330
3022
|
} else {
|
|
2331
3023
|
page = await navigateNativePageWithReplacement(page);
|
|
@@ -2343,13 +3035,13 @@ async function openNativeAttachedContext(
|
|
|
2343
3035
|
if (matchedBy !== 'owned_token' && page.waitForLoadState && !explicitInternalUserSession) {
|
|
2344
3036
|
await page.waitForLoadState(waitUntil(args.wait_until), { timeout });
|
|
2345
3037
|
}
|
|
2346
|
-
const observedPageUrl =
|
|
3038
|
+
const observedPageUrl = page.url();
|
|
2347
3039
|
const pageUrl = explicitInternalUserSession
|
|
2348
3040
|
? normalizedExplicitNativeAdoptionUrl(observedPageUrl)
|
|
2349
3041
|
: normalizedUrl(observedPageUrl);
|
|
2350
3042
|
const nativeRef = (page as unknown as { tabRef?: () => MacOsBrowserTabRef | undefined }).tabRef?.();
|
|
2351
3043
|
if (!nativeRef) throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_OWNERSHIP_MISSING', 'Native browser did not return a stable plugin-owned tab reference.', { retryable: true });
|
|
2352
|
-
const title = matchedBy === 'owned_token' ? (target.existingSession?.title ?? '') :
|
|
3044
|
+
const title = matchedBy === 'owned_token' ? (target.existingSession?.title ?? '') : '';
|
|
2353
3045
|
const inventory: BrowserTabInventoryEntry[] = [{ index: 0, key: tabKey(pageUrl, title), url: pageUrl, title }];
|
|
2354
3046
|
const metadata = activeAttachment.metadata;
|
|
2355
3047
|
// The live exact-tab URL is authoritative after a safe native rebind. Keep the
|
|
@@ -2392,80 +3084,66 @@ async function openBrowser(
|
|
|
2392
3084
|
target: BrowserActionTarget,
|
|
2393
3085
|
args: Record<string, unknown>,
|
|
2394
3086
|
): Promise<BrowserOpenHandle> {
|
|
2395
|
-
const
|
|
3087
|
+
const computerNativeOnly = args.__forge_computer_native_only === true;
|
|
3088
|
+
const requestedMode = computerNativeOnly ? 'attach_preferred' : (config.browserMode ?? 'attach_preferred');
|
|
3089
|
+
if (computerNativeOnly) {
|
|
3090
|
+
// Computer operates existing desktop/browser resources. Playwright CDP and
|
|
3091
|
+
// managed profiles remain exclusive to explicitly selected Browser scripts.
|
|
3092
|
+
if (target.existingSession?.browser?.provider
|
|
3093
|
+
&& target.existingSession.browser.provider !== 'macos-apple-events') {
|
|
3094
|
+
throw new AssistantPluginError('PLUGIN_COMPUTER_BROWSER_PROVIDER_MISMATCH',
|
|
3095
|
+
'This saved session belongs to an independent scripted Browser provider; Computer cannot silently adopt or replace it.',
|
|
3096
|
+
{ retryable: false, details: { sessionId: target.sessionId, provider: target.existingSession.browser.provider } });
|
|
3097
|
+
}
|
|
3098
|
+
const native = await openNativeAttachedContext(repoRoot, {
|
|
3099
|
+
...config, browserMode: 'attach_preferred', nativeAttachMode: 'auto', cdpAttachFallback: 'fail_closed',
|
|
3100
|
+
}, target, args);
|
|
3101
|
+
if (native.handle) return native.handle;
|
|
3102
|
+
throw new AssistantPluginError('PLUGIN_COMPUTER_NATIVE_BROWSER_UNAVAILABLE',
|
|
3103
|
+
'No attachable existing Chrome or Vivaldi browser is available. Computer will not start a Playwright profile.',
|
|
3104
|
+
{ retryable: true, details: { nativeAttempts: native.attempts } });
|
|
3105
|
+
}
|
|
2396
3106
|
if (requestedMode === 'attach_preferred') {
|
|
2397
|
-
const
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
if (attached.handle) return attached.handle;
|
|
2403
|
-
if (requireExistingResource && existingProvider === 'playwright-cdp') {
|
|
2404
|
-
throw new AssistantPluginError(
|
|
2405
|
-
'PLUGIN_BROWSER_SESSION_STATE_LOST',
|
|
2406
|
-
'Saved CDP browser page is no longer attachable; refusing to fall back to another browser or create a replacement page.',
|
|
2407
|
-
{ retryable: false, details: { sessionId: target.sessionId, provider: existingProvider } },
|
|
2408
|
-
);
|
|
3107
|
+
const savedProvider = target.existingSession?.browser?.provider;
|
|
3108
|
+
if (savedProvider === 'playwright-persistent-context') {
|
|
3109
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_SESSION_STATE_LOST',
|
|
3110
|
+
'A managed session cannot be silently adopted by an attach provider. Select managed_persistent explicitly.',
|
|
3111
|
+
{ retryable: false, details: { sessionId: target.sessionId, provider: savedProvider } });
|
|
2409
3112
|
}
|
|
2410
|
-
|
|
3113
|
+
const endpoints = cdpEndpoints(config);
|
|
3114
|
+
const route = savedProvider === 'macos-apple-events' ? 'native'
|
|
3115
|
+
: savedProvider === 'playwright-cdp' || endpoints.length > 0 ? 'cdp' : 'native';
|
|
3116
|
+
if (route === 'cdp') {
|
|
3117
|
+
if (endpoints.length !== 1) {
|
|
3118
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_PROVIDER_CARDINALITY_INVALID',
|
|
3119
|
+
'CDP attach requires exactly one configured endpoint.', { retryable: false });
|
|
3120
|
+
}
|
|
2411
3121
|
if (!runtimeHooks.moduleAvailable('playwright', repoRoot)) {
|
|
2412
|
-
throw new AssistantPluginError('PLUGIN_BROWSER_DEPENDENCY_UNAVAILABLE',
|
|
3122
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_DEPENDENCY_UNAVAILABLE',
|
|
3123
|
+
'Selected CDP provider requires Playwright; no other provider will be attempted.', { retryable: false });
|
|
2413
3124
|
}
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
3125
|
+
const attached = await openAttachedContext(runtimeHooks.loadPlaywright(repoRoot), repoRoot, config, target, args);
|
|
3126
|
+
if (attached.handle) return attached.handle;
|
|
3127
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_ATTACH_UNAVAILABLE',
|
|
3128
|
+
'Configured CDP endpoint unavailable. Native and managed providers will not be attempted.',
|
|
3129
|
+
{ retryable: true, details: { provider: 'playwright-cdp', attempts: attached.attempts } });
|
|
3130
|
+
}
|
|
3131
|
+
if (config.nativeAttachMode !== 'auto') {
|
|
3132
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_ATTACH_UNAVAILABLE',
|
|
3133
|
+
'Native attach is not configured. Choose an explicit Browser mode or one CDP endpoint.',
|
|
3134
|
+
{ retryable: false });
|
|
2421
3135
|
}
|
|
2422
|
-
|
|
2423
3136
|
const native = await openNativeAttachedContext(repoRoot, config, target, args);
|
|
2424
3137
|
if (native.handle) return native.handle;
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
'Saved browser resource is no longer attachable; refusing to create a replacement tab/page for this existing-session action.',
|
|
2429
|
-
{ retryable: false, details: { sessionId: target.sessionId, provider: existingProvider } },
|
|
2430
|
-
);
|
|
2431
|
-
}
|
|
2432
|
-
|
|
2433
|
-
const fallbackPolicy = config.cdpAttachFallback ?? 'fail_closed';
|
|
2434
|
-
const cdpReason = attached.attempts.map((attempt) => `${attempt.endpoint}: ${attempt.error ?? 'unavailable'}`).join('; ')
|
|
2435
|
-
|| 'No CDP endpoint candidates were configured.';
|
|
2436
|
-
const nativeReason = native.attempts.map((attempt) => `${attempt.appName}: ${attempt.error ?? attempt.status}`).join('; ')
|
|
2437
|
-
|| (config.nativeAttachMode === 'disabled' ? 'Native browser attach is disabled.' : 'No scriptable macOS browser was available.');
|
|
2438
|
-
const reason = `CDP: ${cdpReason} Native: ${nativeReason}`;
|
|
2439
|
-
if (fallbackPolicy === 'fail_closed') {
|
|
2440
|
-
throw new AssistantPluginError('PLUGIN_BROWSER_ATTACH_UNAVAILABLE', `No configured browser attach provider was available. Fallback policy fail_closed prevented managed launch. ${reason}`, {
|
|
2441
|
-
retryable: true,
|
|
2442
|
-
details: {
|
|
2443
|
-
browserMode: requestedMode,
|
|
2444
|
-
fallbackPolicy,
|
|
2445
|
-
attempts: attached.attempts,
|
|
2446
|
-
nativeAttempts: native.attempts,
|
|
2447
|
-
},
|
|
2448
|
-
});
|
|
2449
|
-
}
|
|
2450
|
-
if (!runtimeHooks.moduleAvailable('playwright', repoRoot)) {
|
|
2451
|
-
throw new AssistantPluginError('PLUGIN_BROWSER_DEPENDENCY_UNAVAILABLE', `Managed browser fallback requires Playwright, but native/CDP attach was unavailable. ${reason}`, { retryable: false });
|
|
2452
|
-
}
|
|
2453
|
-
return openManagedContext(runtimeHooks.loadPlaywright(repoRoot), repoRoot, config, target, args, 'managed_persistent', requestedMode, {
|
|
2454
|
-
policy: fallbackPolicy,
|
|
2455
|
-
from: 'attach_preferred',
|
|
2456
|
-
to: 'managed_persistent',
|
|
2457
|
-
reason,
|
|
2458
|
-
attempts: attached.attempts,
|
|
2459
|
-
nativeAttempts: native.attempts,
|
|
2460
|
-
});
|
|
3138
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_ATTACH_UNAVAILABLE',
|
|
3139
|
+
'Configured native browser target unavailable. No alternate provider will be attempted.',
|
|
3140
|
+
{ retryable: true, details: { provider: 'macos-apple-events', attempts: native.attempts } });
|
|
2461
3141
|
}
|
|
2462
|
-
|
|
2463
3142
|
if (!runtimeHooks.moduleAvailable('playwright', repoRoot)) {
|
|
2464
3143
|
throw new AssistantPluginError('PLUGIN_BROWSER_DEPENDENCY_UNAVAILABLE', 'Managed browser mode requires Playwright.', { retryable: false });
|
|
2465
3144
|
}
|
|
2466
3145
|
return openManagedContext(runtimeHooks.loadPlaywright(repoRoot), repoRoot, config, target, args, requestedMode, requestedMode);
|
|
2467
3146
|
}
|
|
2468
|
-
|
|
2469
3147
|
async function withPage<T>(
|
|
2470
3148
|
actionId: string,
|
|
2471
3149
|
repoRoot: string,
|
|
@@ -2816,11 +3494,15 @@ const EXTRACTION_SCRIPTS = {
|
|
|
2816
3494
|
})()`,
|
|
2817
3495
|
};
|
|
2818
3496
|
|
|
3497
|
+
function selectorUnavailableInTime(errorMessage: string): boolean {
|
|
3498
|
+
return /timeout|timed out|waiting for selector|selector.*not found|not found.*selector/i.test(errorMessage);
|
|
3499
|
+
}
|
|
3500
|
+
|
|
2819
3501
|
function selectorRepairHint(selector: string, errorMessage: string): string {
|
|
2820
3502
|
if (/strict mode violation|resolved to \d+ elements/i.test(errorMessage)) {
|
|
2821
3503
|
return `Selector "${selector}" matched multiple elements. Prefer a unique #id, [data-testid], or more specific path.`;
|
|
2822
3504
|
}
|
|
2823
|
-
if (
|
|
3505
|
+
if (selectorUnavailableInTime(errorMessage)) {
|
|
2824
3506
|
return `Selector "${selector}" was not found in time. Use snapshot_interactive or query_all to discover stable selectors, then retry.`;
|
|
2825
3507
|
}
|
|
2826
3508
|
return `Check selector "${selector}" against the current page structure.`;
|
|
@@ -3221,9 +3903,12 @@ function health(config: BrowserPluginConfig, repoRoot?: string): AssistantPlugin
|
|
|
3221
3903
|
};
|
|
3222
3904
|
}
|
|
3223
3905
|
|
|
3224
|
-
export function buildBrowserPluginManifest(
|
|
3906
|
+
export function buildBrowserPluginManifest(
|
|
3907
|
+
previousRevision = 0, previousUpdatedAt?: string, repoRoot?: string,
|
|
3908
|
+
context?: AssistantPluginBuildContext, computerNativeOnly = false,
|
|
3909
|
+
): AssistantPluginManifest {
|
|
3225
3910
|
const root = repoRoot ?? process.cwd();
|
|
3226
|
-
const config = loadConfig(root, context);
|
|
3911
|
+
const config = loadConfig(root, context, computerNativeOnly);
|
|
3227
3912
|
const state = health(config, root);
|
|
3228
3913
|
return {
|
|
3229
3914
|
schemaVersion: 1,
|
|
@@ -3232,7 +3917,7 @@ export function buildBrowserPluginManifest(previousRevision = 0, previousUpdated
|
|
|
3232
3917
|
pluginId: BROWSER_PLUGIN_ID,
|
|
3233
3918
|
provider: 'local-browser',
|
|
3234
3919
|
displayName: 'Controller Browser Plugin',
|
|
3235
|
-
pluginVersion: '1.
|
|
3920
|
+
pluginVersion: '1.2.0',
|
|
3236
3921
|
authority: {
|
|
3237
3922
|
strategy: 'derived',
|
|
3238
3923
|
duplicateStateAllowed: false,
|
|
@@ -3245,9 +3930,7 @@ export function buildBrowserPluginManifest(previousRevision = 0, previousUpdated
|
|
|
3245
3930
|
? 'Browser plugin is disabled.'
|
|
3246
3931
|
: state.ready
|
|
3247
3932
|
? `Browser plugin is ready via ${config.browserMode === 'attach_preferred'
|
|
3248
|
-
?
|
|
3249
|
-
? 'CDP, macOS native attach, and explicit managed fallback.'
|
|
3250
|
-
: 'CDP or macOS native attach; managed fallback is disabled.'
|
|
3933
|
+
? 'the one configured CDP or native browser target (no provider fallback).'
|
|
3251
3934
|
: 'Playwright persistent context.'}`
|
|
3252
3935
|
: state.state === 'degraded'
|
|
3253
3936
|
? state.warnings[0] ?? 'Browser plugin requires a successful live native attach probe.'
|
|
@@ -3261,13 +3944,6 @@ export function buildBrowserPluginManifest(previousRevision = 0, previousUpdated
|
|
|
3261
3944
|
};
|
|
3262
3945
|
}
|
|
3263
3946
|
|
|
3264
|
-
const NODE_BRIDGE_PRE_ACTION_FAILURE_CODES = new Set([
|
|
3265
|
-
'PLUGIN_BROWSER_NODE_UNAVAILABLE',
|
|
3266
|
-
'PLUGIN_BROWSER_NODE_HOST_UNAVAILABLE',
|
|
3267
|
-
'PLUGIN_BROWSER_NODE_REQUEST_TOO_LARGE',
|
|
3268
|
-
'PLUGIN_BROWSER_NODE_START_FAILED',
|
|
3269
|
-
]);
|
|
3270
|
-
|
|
3271
3947
|
function browserRuntimeKey(input: AssistantPluginActionExecutionInput): string {
|
|
3272
3948
|
return `${input.controllerHome}:${input.repoId}`;
|
|
3273
3949
|
}
|
|
@@ -3276,9 +3952,15 @@ async function executeBrowserPluginActionInternal(
|
|
|
3276
3952
|
input: AssistantPluginActionExecutionInput,
|
|
3277
3953
|
runtimeAlreadyRouted = false,
|
|
3278
3954
|
): Promise<Record<string, unknown>> {
|
|
3279
|
-
const persisted = loadConfig(input.repoRoot, input);
|
|
3955
|
+
const persisted = loadConfig(input.repoRoot, input, input.pluginId === 'computer');
|
|
3280
3956
|
const actionSessionId = input.actionId === 'configure' ? undefined : stringValue(input.args.session_id);
|
|
3281
3957
|
const actionSession = actionSessionId ? findBrowserSession(input.repoRoot, actionSessionId) : undefined;
|
|
3958
|
+
if (input.pluginId === 'computer' && actionSession?.browser?.provider
|
|
3959
|
+
&& actionSession.browser.provider !== 'macos-apple-events') {
|
|
3960
|
+
throw new AssistantPluginError('PLUGIN_COMPUTER_BROWSER_PROVIDER_MISMATCH',
|
|
3961
|
+
'Computer cannot manage an independent scripted Browser session; use the Browser plugin for that session.',
|
|
3962
|
+
{ retryable: false, details: { sessionId: actionSessionId, provider: actionSession.browser.provider } });
|
|
3963
|
+
}
|
|
3282
3964
|
const current = input.actionId === 'configure' ? persisted : effectiveBrowserActionConfig(persisted, input.args, actionSession);
|
|
3283
3965
|
if (!current.enabled && input.actionId !== 'configure') {
|
|
3284
3966
|
throw new AssistantPluginError('PLUGIN_DISABLED', 'Browser plugin is disabled.', { retryable: false });
|
|
@@ -3291,7 +3973,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
3291
3973
|
}
|
|
3292
3974
|
if (!runtimeAlreadyRouted) {
|
|
3293
3975
|
const runtimeKey = browserRuntimeKey(input);
|
|
3294
|
-
const nodeBridgeEligible = shouldUseBrowserNodeBridge(
|
|
3976
|
+
const nodeBridgeEligible = input.pluginId !== 'computer' && shouldUseBrowserNodeBridge(
|
|
3295
3977
|
input.actionId,
|
|
3296
3978
|
current.browserMode,
|
|
3297
3979
|
runtimeHooksCustomized,
|
|
@@ -3300,33 +3982,21 @@ async function executeBrowserPluginActionInternal(
|
|
|
3300
3982
|
const result = await executeBrowserRuntimeAction({
|
|
3301
3983
|
runtimeKey,
|
|
3302
3984
|
input,
|
|
3303
|
-
providers: [
|
|
3304
|
-
...(nodeBridgeEligible ? [{
|
|
3985
|
+
providers: nodeBridgeEligible ? [{
|
|
3305
3986
|
providerId: 'browser.node_bridge',
|
|
3306
3987
|
capabilities: ALL_BROWSER_PROVIDER_CAPABILITIES.filter((capability) => capability !== 'browser.persistent_handle'),
|
|
3307
3988
|
foreground: 'none' as const,
|
|
3308
3989
|
verifiesPostconditions: true,
|
|
3309
3990
|
persistentHandle: false,
|
|
3310
|
-
priority: 10,
|
|
3311
3991
|
supportsInput: () => true,
|
|
3312
|
-
execute: async (routedInput: AssistantPluginActionExecutionInput) =>
|
|
3313
|
-
|
|
3314
|
-
return await executeBrowserActionThroughNode(routedInput);
|
|
3315
|
-
} catch (error) {
|
|
3316
|
-
if (error instanceof AssistantPluginError && NODE_BRIDGE_PRE_ACTION_FAILURE_CODES.has(error.code)) {
|
|
3317
|
-
throw new BrowserProviderUnavailableBeforeActionError('browser.node_bridge', error.code);
|
|
3318
|
-
}
|
|
3319
|
-
throw error;
|
|
3320
|
-
}
|
|
3321
|
-
},
|
|
3322
|
-
}] : []),
|
|
3992
|
+
execute: async (routedInput: AssistantPluginActionExecutionInput) => await executeBrowserActionThroughNode(routedInput),
|
|
3993
|
+
}] : [
|
|
3323
3994
|
{
|
|
3324
3995
|
providerId: 'browser.local',
|
|
3325
3996
|
capabilities: ALL_BROWSER_PROVIDER_CAPABILITIES,
|
|
3326
3997
|
foreground: 'none' as const,
|
|
3327
3998
|
verifiesPostconditions: true,
|
|
3328
3999
|
persistentHandle: true,
|
|
3329
|
-
priority: 20,
|
|
3330
4000
|
supportsInput: () => true,
|
|
3331
4001
|
execute: async (routedInput: AssistantPluginActionExecutionInput) => executeBrowserPluginActionInternal(routedInput, true),
|
|
3332
4002
|
},
|
|
@@ -3364,7 +4034,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
3364
4034
|
profileMode: nextProfileMode,
|
|
3365
4035
|
profileDir: nextProfileMode === 'repo_local' ? undefined : nextProfileDir,
|
|
3366
4036
|
profileDirectory: nextProfileMode === 'repo_local' ? undefined : nextProfileDirectory,
|
|
3367
|
-
browserChannel: nextBrowserChannel ?? 'chromium',
|
|
4037
|
+
browserChannel: nextBrowserChannel ?? (nextExecutablePath ? undefined : 'chromium'),
|
|
3368
4038
|
executablePath: nextExecutablePath,
|
|
3369
4039
|
cdpEndpoint: nextCdpEndpoint,
|
|
3370
4040
|
cdpEndpointCandidates: nextCdpEndpointCandidates,
|
|
@@ -3378,13 +4048,74 @@ async function executeBrowserPluginActionInternal(
|
|
|
3378
4048
|
? positiveNumber(args.default_timeout_ms, DEFAULT_TIMEOUT_MS)
|
|
3379
4049
|
: current.defaultTimeoutMs,
|
|
3380
4050
|
});
|
|
3381
|
-
const configErrors = validateConfig(config);
|
|
3382
|
-
if (configErrors.length > 0) {
|
|
3383
|
-
throw new AssistantPluginError('PLUGIN_CONFIGURATION_INVALID', configErrors[0], { retryable: false });
|
|
3384
|
-
}
|
|
3385
4051
|
await closeManagedContextsForRepo(input.repoRoot);
|
|
3386
4052
|
return { config, health: health(config, input.repoRoot) };
|
|
3387
4053
|
}
|
|
4054
|
+
case 'list_unpacked_extensions': {
|
|
4055
|
+
if (current.browserMode === 'attach_preferred') {
|
|
4056
|
+
const cdp = await cdpExtensionList(input.repoRoot, current);
|
|
4057
|
+
if (cdp) return { ...cdp, verified: true };
|
|
4058
|
+
if (current.cdpAttachFallback !== 'managed_persistent') {
|
|
4059
|
+
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 });
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
if (current.browserMode !== 'managed_persistent' && current.cdpAttachFallback !== 'managed_persistent') {
|
|
4063
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_EXTENSION_CONTROL_UNAVAILABLE', 'Selected Browser mode does not provide unpacked-extension control.', { retryable: false });
|
|
4064
|
+
}
|
|
4065
|
+
const profile = selectedProfile(current, input.repoRoot, 'managed_persistent');
|
|
4066
|
+
const pending = managedBrowserContexts.get(managedContextKey(profile));
|
|
4067
|
+
if (!pending) return { provider: 'playwright-persistent-context', extensions: [], verified: true };
|
|
4068
|
+
const state = await pending;
|
|
4069
|
+
const urls = extensionTargets(state.context);
|
|
4070
|
+
const extensions = [...(managedExtensionPaths.get(managedContextKey(profile)) ?? [])].map((path) => {
|
|
4071
|
+
const { expectedId } = unpackedExtensionPath({ ...input, args: { ...input.args, extension_path: path } });
|
|
4072
|
+
const enabled = Boolean(expectedId && urls.some((url) => url.startsWith(`chrome-extension://${expectedId}/`)));
|
|
4073
|
+
return { id: expectedId, path, enabled };
|
|
4074
|
+
});
|
|
4075
|
+
return { provider: 'playwright-persistent-context', extensions, verified: true };
|
|
4076
|
+
}
|
|
4077
|
+
case 'install_unpacked_extension': {
|
|
4078
|
+
const extension = unpackedExtensionPath(input);
|
|
4079
|
+
if (current.browserMode === 'attach_preferred') {
|
|
4080
|
+
const cdp = await cdpInstallExtension(input, current, extension.path, extension.expectedId);
|
|
4081
|
+
if (cdp) return cdp;
|
|
4082
|
+
if (current.cdpAttachFallback !== 'managed_persistent') {
|
|
4083
|
+
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 });
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
if ((current.browserMode !== 'managed_persistent' && current.cdpAttachFallback !== 'managed_persistent') || !extension.expectedId) {
|
|
4087
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_EXTENSION_CONTROL_UNAVAILABLE', extension.expectedId
|
|
4088
|
+
? 'Selected Browser mode does not provide managed unpacked-extension control.'
|
|
4089
|
+
: 'Managed unpacked-extension verification requires a stable manifest key.', { retryable: false });
|
|
4090
|
+
}
|
|
4091
|
+
const profile = selectedProfile(current, input.repoRoot, 'managed_persistent');
|
|
4092
|
+
const nativeHostChanged = projectManagedNativeMessagingHost(profile.profileDir, extension.nativeHost);
|
|
4093
|
+
if (customVivaldiExtensionInstall(current, input.repoRoot)) {
|
|
4094
|
+
return await installExtensionThroughCustomVivaldiCdpPort(input, current, profile, extension.path, extension.expectedId);
|
|
4095
|
+
}
|
|
4096
|
+
if (!runtimeHooks.moduleAvailable('playwright', input.repoRoot)) {
|
|
4097
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_DEPENDENCY_UNAVAILABLE', 'Managed unpacked-extension install requires Playwright.', { retryable: false });
|
|
4098
|
+
}
|
|
4099
|
+
const runtime = runtimeHooks.loadPlaywright(input.repoRoot);
|
|
4100
|
+
const key = managedContextKey(profile);
|
|
4101
|
+
const paths = managedExtensionPaths.get(key) ?? new Set<string>();
|
|
4102
|
+
const changed = !paths.has(extension.path);
|
|
4103
|
+
paths.add(extension.path);
|
|
4104
|
+
managedExtensionPaths.set(key, paths);
|
|
4105
|
+
if (changed || nativeHostChanged) await evictManagedContext(key);
|
|
4106
|
+
const state = await managedContextState(runtime, input.repoRoot, current, profile);
|
|
4107
|
+
const prefix = `chrome-extension://${extension.expectedId}/`;
|
|
4108
|
+
let runtimeTarget = extensionTargets(state.context).find((candidate) => candidate.startsWith(prefix));
|
|
4109
|
+
const verifiedExtension = await managedCdpInstallExtension(input, state.context, extension.path, extension.expectedId, Boolean(runtimeTarget));
|
|
4110
|
+
if (!runtimeTarget) {
|
|
4111
|
+
runtimeTarget = await waitForManagedExtension(state.context, extension.expectedId, positiveNumber(input.args.timeout_ms, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS));
|
|
4112
|
+
}
|
|
4113
|
+
return {
|
|
4114
|
+
provider: 'playwright-persistent-context',
|
|
4115
|
+
extension: { ...verifiedExtension, runtimeTarget },
|
|
4116
|
+
verified: true,
|
|
4117
|
+
};
|
|
4118
|
+
}
|
|
3388
4119
|
case 'list_sessions': {
|
|
3389
4120
|
const inventory = await inspectSavedSessions(input.repoRoot, current);
|
|
3390
4121
|
const requestedLimit = Math.max(1, Math.min(Math.trunc(positiveNumber(input.args.limit, DEFAULT_BROWSER_SESSION_LIST_LIMIT)), MAX_BROWSER_SESSION_LIST_LIMIT));
|
|
@@ -3399,7 +4130,10 @@ async function executeBrowserPluginActionInternal(
|
|
|
3399
4130
|
throw new AssistantPluginError('PLUGIN_BROWSER_SESSION_CURSOR_INVALID', 'Browser session cursor is invalid or expired.', { retryable: false });
|
|
3400
4131
|
}
|
|
3401
4132
|
}
|
|
3402
|
-
const
|
|
4133
|
+
const visibleSessions = input.pluginId === 'computer'
|
|
4134
|
+
? inventory.sessions.filter(({ session }) => session.browser?.provider === 'macos-apple-events')
|
|
4135
|
+
: inventory.sessions;
|
|
4136
|
+
const page = visibleSessions.slice(offset, offset + requestedLimit);
|
|
3403
4137
|
const nextOffset = offset + page.length;
|
|
3404
4138
|
return {
|
|
3405
4139
|
provider: 'playwright',
|
|
@@ -3412,8 +4146,8 @@ async function executeBrowserPluginActionInternal(
|
|
|
3412
4146
|
deadManagedSessionCount: inventory.deadManagedSessionCount,
|
|
3413
4147
|
deadNativeSessionCount: inventory.deadNativeSessionCount,
|
|
3414
4148
|
limit: requestedLimit,
|
|
3415
|
-
totalCount:
|
|
3416
|
-
...(nextOffset <
|
|
4149
|
+
totalCount: visibleSessions.length,
|
|
4150
|
+
...(nextOffset < visibleSessions.length ? { nextCursor: Buffer.from(JSON.stringify({ offset: nextOffset }), 'utf8').toString('base64url') } : {}),
|
|
3417
4151
|
sessions: page.map(({ session, liveness, evidence, cleanupError }) => ({
|
|
3418
4152
|
sessionId: session.sessionId,
|
|
3419
4153
|
url: session.url,
|
|
@@ -3498,7 +4232,13 @@ async function executeBrowserPluginActionInternal(
|
|
|
3498
4232
|
case 'request_human_handoff': {
|
|
3499
4233
|
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
3500
4234
|
const managedSession = target.existingSession;
|
|
3501
|
-
|
|
4235
|
+
if (input.pluginId === 'computer' && managedSession?.browser?.provider
|
|
4236
|
+
&& managedSession.browser.provider !== 'macos-apple-events') {
|
|
4237
|
+
throw new AssistantPluginError('PLUGIN_COMPUTER_BROWSER_PROVIDER_MISMATCH',
|
|
4238
|
+
'Computer handoff requires a native browser tab, not a scripted Browser session.', { retryable: false });
|
|
4239
|
+
}
|
|
4240
|
+
const managedTarget = input.pluginId === 'computer'
|
|
4241
|
+
? undefined : managedSession ? await runtimeManagedPageForSession(input.repoRoot, current, managedSession) : undefined;
|
|
3502
4242
|
if (managedSession && managedTarget) {
|
|
3503
4243
|
const result = { url: managedTarget.page.url(), title: await managedTarget.page.title().catch(() => managedSession.title) };
|
|
3504
4244
|
const handoff = startRuntimeManagedBrowserHandoff({
|
|
@@ -3535,7 +4275,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
3535
4275
|
const nativeTab = nativeSession?.browser?.tab;
|
|
3536
4276
|
const nativeBrowser = nativeSession?.browser?.provider === 'macos-apple-events'
|
|
3537
4277
|
&& nativeSession.browser.browserProduct
|
|
3538
|
-
&& nativeTab?.ownership === 'plugin_owned'
|
|
4278
|
+
&& (nativeTab?.ownership === 'plugin_owned' || nativeTab?.ownership === 'user_owned')
|
|
3539
4279
|
&& typeof nativeTab.windowId === 'string'
|
|
3540
4280
|
&& typeof nativeTab.tabId === 'string'
|
|
3541
4281
|
? {
|
|
@@ -3543,6 +4283,11 @@ async function executeBrowserPluginActionInternal(
|
|
|
3543
4283
|
ref: { windowId: nativeTab.windowId, tabId: nativeTab.tabId },
|
|
3544
4284
|
}
|
|
3545
4285
|
: undefined;
|
|
4286
|
+
if (input.pluginId === 'computer' && !nativeBrowser) {
|
|
4287
|
+
throw new AssistantPluginError('PLUGIN_COMPUTER_NATIVE_BROWSER_HANDOFF_UNAVAILABLE',
|
|
4288
|
+
'Computer handoff requires an existing bound native tab; it will not open a Playwright profile.',
|
|
4289
|
+
{ retryable: false });
|
|
4290
|
+
}
|
|
3546
4291
|
const handoffMode: BrowserMode = current.browserMode === 'isolated' ? 'isolated' : 'managed_persistent';
|
|
3547
4292
|
const profile = selectedProfile(current, input.repoRoot, handoffMode, target.sessionId);
|
|
3548
4293
|
if (!nativeBrowser) mkdirSync(profile.profileDir, { recursive: true });
|
|
@@ -3577,6 +4322,11 @@ async function executeBrowserPluginActionInternal(
|
|
|
3577
4322
|
case 'get_handoff_status': {
|
|
3578
4323
|
const interactionId = requiredString(input.args.interaction_id, 'interaction_id');
|
|
3579
4324
|
const handoff = getBrowserHandoff(input.repoRoot, interactionId);
|
|
4325
|
+
if (input.pluginId === 'computer' && !handoff.targetId.startsWith('macos-apple-events:')) {
|
|
4326
|
+
throw new AssistantPluginError('PLUGIN_COMPUTER_BROWSER_PROVIDER_MISMATCH',
|
|
4327
|
+
'Computer can only inspect native browser handoffs; standalone scripted handoffs are independent.',
|
|
4328
|
+
{ retryable: false });
|
|
4329
|
+
}
|
|
3580
4330
|
if (handoff.status === 'completed' && handoff.result?.url) {
|
|
3581
4331
|
const session = findBrowserSession(input.repoRoot, handoff.sessionId);
|
|
3582
4332
|
if (session) {
|
|
@@ -3604,6 +4354,11 @@ async function executeBrowserPluginActionInternal(
|
|
|
3604
4354
|
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'resolution must be resume or cancel', { retryable: false });
|
|
3605
4355
|
}
|
|
3606
4356
|
const currentHandoff = getBrowserHandoff(input.repoRoot, interactionId);
|
|
4357
|
+
if (input.pluginId === 'computer' && !currentHandoff.targetId.startsWith('macos-apple-events:')) {
|
|
4358
|
+
throw new AssistantPluginError('PLUGIN_COMPUTER_BROWSER_PROVIDER_MISMATCH',
|
|
4359
|
+
'Computer can resolve only native browser handoffs; scripted Browser handoffs remain independent.',
|
|
4360
|
+
{ retryable: false });
|
|
4361
|
+
}
|
|
3607
4362
|
if (isRuntimeManagedBrowserHandoff(currentHandoff)) {
|
|
3608
4363
|
const session = findBrowserSession(input.repoRoot, currentHandoff.sessionId);
|
|
3609
4364
|
const managedTarget = session ? await runtimeManagedPageForSession(input.repoRoot, current, session) : undefined;
|
|
@@ -4245,10 +5000,14 @@ async function executeBrowserPluginActionInternal(
|
|
|
4245
5000
|
});
|
|
4246
5001
|
} catch (error) {
|
|
4247
5002
|
const message = error instanceof Error ? error.message : String(error);
|
|
4248
|
-
throw new AssistantPluginError(
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
5003
|
+
throw new AssistantPluginError(
|
|
5004
|
+
selectorUnavailableInTime(message) ? 'PLUGIN_BROWSER_SELECTOR_UNAVAILABLE' : 'PLUGIN_ACTION_FAILED',
|
|
5005
|
+
message,
|
|
5006
|
+
{
|
|
5007
|
+
retryable: true,
|
|
5008
|
+
details: { selector, repairHint: selectorRepairHint(selector, message) },
|
|
5009
|
+
},
|
|
5010
|
+
);
|
|
4252
5011
|
}
|
|
4253
5012
|
}
|
|
4254
5013
|
case 'type':
|
|
@@ -4500,30 +5259,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
4500
5259
|
if (requested.length === 0 || requested.length > 32) {
|
|
4501
5260
|
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'attach_local_file requires file_path or 1-32 file_paths.', { retryable: false });
|
|
4502
5261
|
}
|
|
4503
|
-
const
|
|
4504
|
-
const repositoryRoot = realpathSync(input.repoRoot);
|
|
4505
|
-
const resolved = requested.map((filePath) => {
|
|
4506
|
-
const lexicalPath = resolve(input.repoRoot, filePath);
|
|
4507
|
-
const lexicalRelative = relative(repositoryLexicalRoot, lexicalPath);
|
|
4508
|
-
if (lexicalRelative === '..' || lexicalRelative.startsWith(`..${sep}`) || isAbsolute(lexicalRelative)) {
|
|
4509
|
-
throw new AssistantPluginError('PLUGIN_POLICY_BLOCKED', 'Local file attachments must stay inside the repository root.', { retryable: false, details: { fileName: basename(filePath) } });
|
|
4510
|
-
}
|
|
4511
|
-
if (!existsSync(lexicalPath)) {
|
|
4512
|
-
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', `Local attachment does not exist: ${basename(lexicalPath)}`, { retryable: false });
|
|
4513
|
-
}
|
|
4514
|
-
const realPath = realpathSync(lexicalPath);
|
|
4515
|
-
const repositoryRelative = relative(repositoryRoot, realPath);
|
|
4516
|
-
if (repositoryRelative === '..' || repositoryRelative.startsWith(`..${sep}`) || isAbsolute(repositoryRelative)) {
|
|
4517
|
-
throw new AssistantPluginError('PLUGIN_POLICY_BLOCKED', 'Local file attachments must resolve inside the repository root.', { retryable: false, details: { fileName: basename(lexicalPath) } });
|
|
4518
|
-
}
|
|
4519
|
-
if (!statSync(realPath).isFile()) {
|
|
4520
|
-
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', `Local attachment is not a regular file: ${basename(lexicalPath)}`, { retryable: false });
|
|
4521
|
-
}
|
|
4522
|
-
if (/\.(exe|dmg|pkg|sh|bat|cmd|app)$/i.test(realPath)) {
|
|
4523
|
-
throw new AssistantPluginError('PLUGIN_POLICY_BLOCKED', 'Executable file attachments are not allowed.', { retryable: false });
|
|
4524
|
-
}
|
|
4525
|
-
return lexicalPath;
|
|
4526
|
-
});
|
|
5262
|
+
const resolved = resolveBrowserAttachmentPaths(input, requested, stringValue(input.args.target_key));
|
|
4527
5263
|
return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
4528
5264
|
await page.evaluate((payload) => {
|
|
4529
5265
|
const args = payload as { selector: string; count: number };
|
|
@@ -4629,6 +5365,61 @@ async function executeBrowserPluginActionInternal(
|
|
|
4629
5365
|
}
|
|
4630
5366
|
}
|
|
4631
5367
|
|
|
5368
|
+
/** Resolve one upload batch under the repository or the existing local-system target grant.
|
|
5369
|
+
* A target_key changes the path base; it never grants arbitrary absolute filesystem access.
|
|
5370
|
+
*/
|
|
5371
|
+
export function resolveBrowserAttachmentPaths(
|
|
5372
|
+
input: AssistantPluginActionExecutionInput,
|
|
5373
|
+
requested: string[],
|
|
5374
|
+
targetKey?: string,
|
|
5375
|
+
): string[] {
|
|
5376
|
+
const repositoryLexicalRoot = resolve(input.repoRoot);
|
|
5377
|
+
const repositoryRoot = realpathSync(input.repoRoot);
|
|
5378
|
+
return requested.map((filePath) => {
|
|
5379
|
+
let allowedPath: string;
|
|
5380
|
+
if (targetKey) {
|
|
5381
|
+
try {
|
|
5382
|
+
allowedPath = resolveWorkspaceTargetPath(input.controllerHome, targetKey, filePath, {
|
|
5383
|
+
mustExist: true,
|
|
5384
|
+
kind: 'file',
|
|
5385
|
+
operation: 'read',
|
|
5386
|
+
ownerScope: pluginCapabilityAuthorizationOwnerScope(input.origin),
|
|
5387
|
+
}).path;
|
|
5388
|
+
} catch (error) {
|
|
5389
|
+
if (!(error instanceof WorkspaceTargetGrantError)) throw error;
|
|
5390
|
+
throw new AssistantPluginError('PLUGIN_POLICY_BLOCKED',
|
|
5391
|
+
'Upload file is not within an active, caller-owned local-system target grant.',
|
|
5392
|
+
{ retryable: false, details: { fileName: basename(filePath), targetKey, causeCode: error.code } });
|
|
5393
|
+
}
|
|
5394
|
+
} else {
|
|
5395
|
+
const lexicalPath = resolve(input.repoRoot, filePath);
|
|
5396
|
+
const lexicalRelative = relative(repositoryLexicalRoot, lexicalPath);
|
|
5397
|
+
if (lexicalRelative === '..' || lexicalRelative.startsWith(`..${sep}`) || isAbsolute(lexicalRelative)) {
|
|
5398
|
+
throw new AssistantPluginError('PLUGIN_POLICY_BLOCKED', 'Local file attachments must stay inside the repository root.', {
|
|
5399
|
+
retryable: false, details: { fileName: basename(filePath) },
|
|
5400
|
+
});
|
|
5401
|
+
}
|
|
5402
|
+
if (!existsSync(lexicalPath)) {
|
|
5403
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', `Local attachment does not exist: ${basename(lexicalPath)}`, { retryable: false });
|
|
5404
|
+
}
|
|
5405
|
+
allowedPath = realpathSync(lexicalPath);
|
|
5406
|
+
const repositoryRelative = relative(repositoryRoot, allowedPath);
|
|
5407
|
+
if (repositoryRelative === '..' || repositoryRelative.startsWith(`..${sep}`) || isAbsolute(repositoryRelative)) {
|
|
5408
|
+
throw new AssistantPluginError('PLUGIN_POLICY_BLOCKED', 'Local file attachments must resolve inside the repository root.', {
|
|
5409
|
+
retryable: false, details: { fileName: basename(lexicalPath) },
|
|
5410
|
+
});
|
|
5411
|
+
}
|
|
5412
|
+
}
|
|
5413
|
+
if (!statSync(allowedPath).isFile()) {
|
|
5414
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', `Local attachment is not a regular file: ${basename(filePath)}`, { retryable: false });
|
|
5415
|
+
}
|
|
5416
|
+
if (/\.(exe|dmg|pkg|sh|bat|cmd|app)$/i.test(allowedPath)) {
|
|
5417
|
+
throw new AssistantPluginError('PLUGIN_POLICY_BLOCKED', 'Executable file attachments are not allowed.', { retryable: false });
|
|
5418
|
+
}
|
|
5419
|
+
return allowedPath;
|
|
5420
|
+
});
|
|
5421
|
+
}
|
|
5422
|
+
|
|
4632
5423
|
export async function executeBrowserPluginAction(input: AssistantPluginActionExecutionInput): Promise<Record<string, unknown>> {
|
|
4633
5424
|
return await withBrowserSessionAuthorityContext(
|
|
4634
5425
|
{ controllerHome: input.controllerHome, repoId: input.repoId },
|