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