@moretea-labs/forge 1.5.0-rc.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 +120 -0
- package/CHANGELOG.md +36 -0
- package/CLAUDE.md +120 -0
- package/CODE_OF_CONDUCT.md +23 -0
- package/CONTRIBUTING.md +77 -0
- package/LICENSE +22 -0
- package/NOTICE +9 -0
- package/README.en.md +5 -0
- package/README.es.md +12 -0
- package/README.fr.md +12 -0
- package/README.ja.md +12 -0
- package/README.md +132 -0
- package/README.zh-CN.md +84 -0
- package/SECURITY.md +50 -0
- package/SKILL.md +431 -0
- package/SUPPORT.md +31 -0
- package/THIRD_PARTY_NOTICES.md +18 -0
- package/agents/openai.yaml +4 -0
- package/assets/AGENTS.md +68 -0
- package/assets/CLAUDE.md +68 -0
- package/assets/branding/README.md +10 -0
- package/assets/branding/forge-controller-flow.mmd +8 -0
- package/assets/hooks/AGENTS.md +75 -0
- package/assets/hooks/CLAUDE.md +75 -0
- package/assets/hooks/anti-simplification.sh +12 -0
- package/assets/hooks/changelog-guard.sh +80 -0
- package/assets/hooks/codex.hooks.template.json +60 -0
- package/assets/hooks/first-principles-guard.sh +72 -0
- package/assets/hooks/hook-input.sh +618 -0
- package/assets/hooks/lib/session-state.sh +106 -0
- package/assets/hooks/lib/workflow-state.sh +1730 -0
- package/assets/hooks/post-bash.sh +215 -0
- package/assets/hooks/post-edit-guard.sh +256 -0
- package/assets/hooks/post-tool-observer.sh +99 -0
- package/assets/hooks/pre-edit-guard.sh +240 -0
- package/assets/hooks/prompt-guard.sh +1077 -0
- package/assets/hooks/run-hook.sh +67 -0
- package/assets/hooks/security-sentinel.sh +115 -0
- package/assets/hooks/session-start-context.sh +593 -0
- package/assets/hooks/settings.template.json +60 -0
- package/assets/hooks/stop-orchestrator.sh +171 -0
- package/assets/hooks/subagent-return-channel-guard.sh +107 -0
- package/assets/hooks/worktree-guard.sh +39 -0
- package/assets/initializer-question-pack.v1.json +132 -0
- package/assets/initializer-question-pack.v1.schema.json +46 -0
- package/assets/initializer-question-pack.v2.json +174 -0
- package/assets/initializer-question-pack.v3.json +206 -0
- package/assets/initializer-question-pack.v3.schema.json +92 -0
- package/assets/initializer-question-pack.v4.json +557 -0
- package/assets/initializer-question-pack.v4.schema.json +169 -0
- package/assets/partials/01-header.partial.md +10 -0
- package/assets/partials/02-iron-rules.partial.md +30 -0
- package/assets/partials/03-philosophy.partial.md +32 -0
- package/assets/partials/04-project-structure.partial.md +56 -0
- package/assets/partials/05-workflow.partial.md +79 -0
- package/assets/partials/06-cloudflare.partial.md +20 -0
- package/assets/partials/07-footer.partial.md +38 -0
- package/assets/partials/08-orchestration.partial.md +49 -0
- package/assets/partials/09-compact-instructions.partial.md +11 -0
- package/assets/partials/_assembly-order.md +40 -0
- package/assets/partials-agents/01-header.partial.md +8 -0
- package/assets/partials-agents/02-operating-mode.partial.md +44 -0
- package/assets/partials-agents/03-orchestration.partial.md +45 -0
- package/assets/partials-agents/04-task-protocol.partial.md +69 -0
- package/assets/partials-agents/05-coding-constraints.partial.md +20 -0
- package/assets/partials-agents/06-quality-safety.partial.md +23 -0
- package/assets/partials-agents/07-cloudflare.partial.md +9 -0
- package/assets/partials-agents/08-deep-docs.partial.md +32 -0
- package/assets/plan-map.json +301 -0
- package/assets/plugin-registry.v1.json +8 -0
- package/assets/project-structures/ai-native-collaborative-editor.txt +25 -0
- package/assets/project-structures/ai-native-product-copilot.txt +21 -0
- package/assets/project-structures/ai-native-runtime-console.txt +25 -0
- package/assets/project-structures/ai-native-sidecar-kernel.txt +19 -0
- package/assets/project-structures/astro-ssr.txt +25 -0
- package/assets/project-structures/expo-nativewind.txt +52 -0
- package/assets/project-structures/monorepo.txt +63 -0
- package/assets/project-structures/remix.txt +50 -0
- package/assets/project-structures/tanstack-start-workers.txt +51 -0
- package/assets/project-structures/vite-tanstack.txt +58 -0
- package/assets/reference-configs/AGENTS.md +13 -0
- package/assets/reference-configs/CLAUDE.md +13 -0
- package/assets/reference-configs/agentic-development-flow.md +94 -0
- package/assets/reference-configs/ai-workflows.md +14 -0
- package/assets/reference-configs/changelog-versioning.md +13 -0
- package/assets/reference-configs/coding-standards.md +14 -0
- package/assets/reference-configs/development-protocol.md +21 -0
- package/assets/reference-configs/document-generation.md +38 -0
- package/assets/reference-configs/evaluator-rubric.md +19 -0
- package/assets/reference-configs/external-tooling.md +374 -0
- package/assets/reference-configs/git-strategy.md +14 -0
- package/assets/reference-configs/global-working-rules.md +64 -0
- package/assets/reference-configs/handoff-protocol.md +38 -0
- package/assets/reference-configs/harness-overview.md +124 -0
- package/assets/reference-configs/heartbeat-triage.md +57 -0
- package/assets/reference-configs/hook-operations.md +96 -0
- package/assets/reference-configs/release-deploy.md +40 -0
- package/assets/reference-configs/spa-day-protocol.md +14 -0
- package/assets/reference-configs/sprint-contracts.md +89 -0
- package/assets/reference-configs/workflow-orchestration.md +14 -0
- package/assets/skill-hooks.json +41 -0
- package/assets/skill-version.json +189 -0
- package/assets/skills/claude-review/SKILL.md +231 -0
- package/assets/skills/codex-review/SKILL.md +103 -0
- package/assets/templates/AGENTS.md +13 -0
- package/assets/templates/CLAUDE.md +13 -0
- package/assets/templates/architecture.template.md +80 -0
- package/assets/templates/brief.template.md +38 -0
- package/assets/templates/contract.template.md +104 -0
- package/assets/templates/decisions.template.md +85 -0
- package/assets/templates/factor-factory/factor-backtest-report.template.md +36 -0
- package/assets/templates/factor-factory/factor-hypothesis.template.md +30 -0
- package/assets/templates/factor-factory/factor-registry.template.json +5 -0
- package/assets/templates/gitignore.template +53 -0
- package/assets/templates/guides/metro-esm-gotchas.template.md +46 -0
- package/assets/templates/helpers/architecture-event.ts +822 -0
- package/assets/templates/helpers/architecture-queue.sh +647 -0
- package/assets/templates/helpers/archive-architecture-request.sh +244 -0
- package/assets/templates/helpers/archive-workflow.sh +295 -0
- package/assets/templates/helpers/capability-config.ts +394 -0
- package/assets/templates/helpers/capability-resolver.ts +505 -0
- package/assets/templates/helpers/capture-plan.sh +335 -0
- package/assets/templates/helpers/check-agent-tooling.sh +1533 -0
- package/assets/templates/helpers/check-architecture-sync.sh +287 -0
- package/assets/templates/helpers/check-brain-manifest.sh +255 -0
- package/assets/templates/helpers/check-context-files.sh +49 -0
- package/assets/templates/helpers/check-deploy-sql-order.sh +117 -0
- package/assets/templates/helpers/check-skill-version.ts +193 -0
- package/assets/templates/helpers/check-task-sync.sh +65 -0
- package/assets/templates/helpers/check-task-workflow.sh +1002 -0
- package/assets/templates/helpers/codex-handoff-resume.sh +269 -0
- package/assets/templates/helpers/context-contract-sync.sh +516 -0
- package/assets/templates/helpers/contract-run.ts +465 -0
- package/assets/templates/helpers/contract-worktree.sh +725 -0
- package/assets/templates/helpers/ensure-task-workflow.sh +1218 -0
- package/assets/templates/helpers/harness-trace-grade.sh +150 -0
- package/assets/templates/helpers/heartbeat-triage.sh +361 -0
- package/assets/templates/helpers/inspect-project-state.ts +280 -0
- package/assets/templates/helpers/maintenance-triage.sh +167 -0
- package/assets/templates/helpers/migrate-project-template.sh +54 -0
- package/assets/templates/helpers/migrate-workflow-docs.ts +413 -0
- package/assets/templates/helpers/new-plan.sh +183 -0
- package/assets/templates/helpers/new-spec.sh +50 -0
- package/assets/templates/helpers/new-sprint.sh +49 -0
- package/assets/templates/helpers/plan-to-todo.sh +799 -0
- package/assets/templates/helpers/prepare-codex-handoff.sh +203 -0
- package/assets/templates/helpers/prepare-handoff.sh +81 -0
- package/assets/templates/helpers/refresh-current-status.sh +469 -0
- package/assets/templates/helpers/select-agent-context-blocks.sh +66 -0
- package/assets/templates/helpers/ship-worktrees.sh +561 -0
- package/assets/templates/helpers/sprint-backlog.sh +837 -0
- package/assets/templates/helpers/summarize-failures.sh +125 -0
- package/assets/templates/helpers/switch-plan.sh +153 -0
- package/assets/templates/helpers/sync-brain-docs.sh +381 -0
- package/assets/templates/helpers/verify-contract.sh +705 -0
- package/assets/templates/helpers/verify-sprint.sh +680 -0
- package/assets/templates/helpers/workflow-contract.ts +202 -0
- package/assets/templates/helpers/workstream-sync.sh +263 -0
- package/assets/templates/implementation-notes.template.md +37 -0
- package/assets/templates/packages.template.md +45 -0
- package/assets/templates/plan.template.md +77 -0
- package/assets/templates/prd.template.md +165 -0
- package/assets/templates/research.template.md +25 -0
- package/assets/templates/review.template.md +78 -0
- package/assets/templates/spec.template.md +31 -0
- package/assets/templates/sprint.template.md +69 -0
- package/assets/templates/tech-stack.template.md +57 -0
- package/assets/versions.json +77 -0
- package/assets/workflow-contract.v1.json +671 -0
- package/bin/forge-desktop-helper.mjs +204 -0
- package/bin/forge-hook.mjs +32 -0
- package/bin/forge-runtime-service.mjs +34 -0
- package/bin/forge-runtime.mjs +30 -0
- package/bin/forge.mjs +32 -0
- package/docs/CHANGELOG.md +978 -0
- package/docs/README.md +72 -0
- package/docs/README.zh-CN.md +58 -0
- package/docs/architecture/chatgpt-handoff-facade.md +393 -0
- package/docs/architecture/current/README.md +158 -0
- package/docs/architecture/current/approved-target-architecture.zh-CN.md +1501 -0
- package/docs/architecture/current/architecture-invariants.md +331 -0
- package/docs/architecture/current/automation-and-schedule-engine.md +453 -0
- package/docs/architecture/current/codegraph-context-provider.md +226 -0
- package/docs/architecture/current/control-plane-authority-inventory.md +82 -0
- package/docs/architecture/current/controller-repository-decoupling.md +148 -0
- package/docs/architecture/current/dispatch-and-agent-strategy.md +354 -0
- package/docs/architecture/current/entity-model.md +619 -0
- package/docs/architecture/current/external-plugin-governance.md +396 -0
- package/docs/architecture/current/failure-recovery.md +471 -0
- package/docs/architecture/current/governance.md +213 -0
- package/docs/architecture/current/human-interaction-plane.md +87 -0
- package/docs/architecture/current/implementation-status.md +128 -0
- package/docs/architecture/current/job-and-run-lifecycle.md +413 -0
- package/docs/architecture/current/local-workspace-target-grants.md +70 -0
- package/docs/architecture/current/migration-roadmap.md +105 -0
- package/docs/architecture/current/multi-repository-execution.md +371 -0
- package/docs/architecture/current/operations-runbook.md +45 -0
- package/docs/architecture/current/peer-supercontroller-runtime.md +32 -0
- package/docs/architecture/current/personal-assistant-plugin-baseline.md +207 -0
- package/docs/architecture/current/repository-access-modes.md +102 -0
- package/docs/architecture/current/route-policy.md +15 -0
- package/docs/architecture/current/route-session-concurrency.md +85 -0
- package/docs/architecture/current/runtime-architecture-simplification.md +214 -0
- package/docs/architecture/current/runtime-directory-map.md +49 -0
- package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +85 -0
- package/docs/architecture/current/scheduler-and-resource-claims.md +432 -0
- package/docs/architecture/current/session-aware-execution-and-authorization.md +77 -0
- package/docs/architecture/current/system-overview.md +144 -0
- package/docs/architecture/current/target-requirements-traceability.md +202 -0
- package/docs/architecture/current/thin-harness-governance-policy.md +32 -0
- package/docs/architecture/current/thin-harness-v1.md +313 -0
- package/docs/architecture/current/verification-and-release-gates.md +391 -0
- package/docs/architecture/decisions/20260801-controller-home-sqlite-state.md +100 -0
- package/docs/architecture/decisions/20260803-canonical-skill-surface.md +40 -0
- package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +53 -0
- package/docs/architecture/diagrams/.gitkeep +1 -0
- package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +186 -0
- package/docs/architecture/domains/.gitkeep +0 -0
- package/docs/architecture/domains/public-surface.md +30 -0
- package/docs/architecture/domains/runtime-harness.md +33 -0
- package/docs/architecture/domains/verification.md +33 -0
- package/docs/architecture/domains/workflow-engine.md +30 -0
- package/docs/architecture/global-hook-runtime.md +162 -0
- package/docs/architecture/history.md +38 -0
- package/docs/architecture/index.md +114 -0
- package/docs/architecture/ios-semantic-automation-provider-v2.md +166 -0
- package/docs/architecture/modules/.gitkeep +0 -0
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +24 -0
- package/docs/architecture/requests/.gitkeep +1 -0
- package/docs/architecture/requests/archive/2026/20260528-154740-ai-hooks-ai-hooks-hook-input-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-154816-assets-hooks-assets-hooks-hook-input-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-161851-ai-harness-workflow-contract-json-ai-harness-workflow-contract-json.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-161853-scripts-check-task-workflow-sh-scripts-check-task-workflow-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-161911-assets-workflow-contract-v1-json-assets-workflow-contract-v1-json.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-222416-ai-hooks-ai-hooks-hook-input-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-222444-ai-hooks-ai-hooks-pre-edit-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-222457-ai-hooks-ai-hooks-pre-edit-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-222648-ai-hooks-ai-hooks-pre-edit-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-222705-ai-hooks-ai-hooks-worktree-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-222726-ai-hooks-ai-hooks-prompt-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-222727-ai-hooks-ai-hooks-prompt-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-224736-ai-hooks-ai-hooks-hook-input-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-233259-ai-hooks-ai-hooks-prompt-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-233335-assets-hooks-assets-hooks-prompt-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-233456-ai-hooks-ai-hooks-prompt-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-233457-assets-hooks-assets-hooks-prompt-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-233637-ai-hooks-ai-hooks-prompt-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-233638-assets-hooks-assets-hooks-prompt-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260529-000322-root-package-json.md +59 -0
- package/docs/architecture/requests/archive/2026/20260529-004238-ai-hooks-ai-hooks-lib-workflow-state-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260529-020654-ai-harness-policy-json-ai-harness-policy-json.md +63 -0
- package/docs/architecture/requests/archive/2026/20260529-094236-ai-hooks-ai-hooks-prompt-guard-sh.md +60 -0
- package/docs/architecture/requests/archive/2026/20260529-094323-assets-hooks-assets-hooks-prompt-guard-sh.md +60 -0
- package/docs/architecture/requests/archive/2026/20260529-094331-ai-hooks-ai-hooks-prompt-guard-sh.md +60 -0
- package/docs/architecture/requests/archive/2026/20260529-094339-assets-hooks-assets-hooks-prompt-guard-sh.md +60 -0
- package/docs/architecture/requests/archive/2026/20260529-094430-assets-hooks-assets-hooks-session-start-context-sh.md +60 -0
- package/docs/architecture/requests/archive/2026/20260529-094446-ai-hooks-ai-hooks-session-start-context-sh.md +60 -0
- package/docs/architecture/requests/archive/2026/20260613-002040-runtime-harness-hook-adapters.md +68 -0
- package/docs/architecture/requests/archive/2026/root.md +68 -0
- package/docs/architecture/requests/archive/2026/runtime-harness-hook-adapters.md +76 -0
- package/docs/architecture/requests/archive/2026/verification-evals-checks.md +68 -0
- package/docs/architecture/requests/archive/2026/workflow-engine-contract-assets.md +69 -0
- package/docs/architecture/reviews/2026-07-11-access-surface-review.md +99 -0
- package/docs/architecture/snapshots/.gitkeep +1 -0
- package/docs/forge-autonomous-goal-loop.md +7 -0
- package/docs/forge-chatgpt-browser-engine.md +11 -0
- package/docs/forge-chatgpt-mcp-setup.md +239 -0
- package/docs/forge-chatgpt-supervised-automation.md +11 -0
- package/docs/forge-github-issue-launcher.md +7 -0
- package/docs/forge-plugin-management.md +95 -0
- package/docs/forge-provider-configuration.md +7 -0
- package/docs/forge-runtime-self-healing-loop.md +7 -0
- package/docs/images/forge-banner-cn.svg +1 -0
- package/docs/images/forge-banner.svg +1 -0
- package/docs/images/forge-controller-flow.svg +1 -0
- package/docs/images/image.png +0 -0
- package/docs/images/repo-harness-gptpro-cn.png +0 -0
- package/docs/images/repo-harness-gptpro.png +0 -0
- package/docs/images/repo-harness-hook-carrot.png +0 -0
- package/docs/operations/chatgpt-personal-assistant-runtime.md +83 -0
- package/docs/operations/checkout-route-integrity.md +22 -0
- package/docs/operations/controller-browser-plugin.md +299 -0
- package/docs/operations/controller-desktop-plugin.md +112 -0
- package/docs/operations/controller-performance-and-502.md +126 -0
- package/docs/operations/controller-reliability-runbook.md +134 -0
- package/docs/operations/environment-hardening.md +50 -0
- package/docs/operations/features.md +45 -0
- package/docs/operations/features.zh-CN.md +45 -0
- package/docs/operations/github-repository.md +60 -0
- package/docs/operations/gmail-assistant-routines.md +186 -0
- package/docs/operations/homebrew.md +26 -0
- package/docs/operations/local-system-assistant.md +46 -0
- package/docs/operations/local-system-assistant.zh-CN.md +46 -0
- package/docs/operations/mcp-tool-exposure.md +58 -0
- package/docs/operations/mobile-intents-shortcuts.md +117 -0
- package/docs/operations/open-source-release-hygiene.md +31 -0
- package/docs/operations/personal-assistant-operator-guide.md +61 -0
- package/docs/operations/platform-support.md +62 -0
- package/docs/operations/platform-support.zh-CN.md +61 -0
- package/docs/operations/provider-configuration.md +31 -0
- package/docs/operations/releasing.md +86 -0
- package/docs/operations/releasing.zh-CN.md +86 -0
- package/docs/operations/route-session-concurrency-baseline.json +893 -0
- package/docs/operations/route-session-concurrency-baseline.md +94 -0
- package/docs/operations/runtime-performance-diagnostics.md +14 -0
- package/docs/operations/sqlite-control-plane-recovery.md +65 -0
- package/docs/operations/standalone-disaster-recovery.md +136 -0
- package/docs/operations/troubleshooting.md +72 -0
- package/docs/operations/troubleshooting.zh-CN.md +70 -0
- package/docs/public-usage-guide.md +33 -0
- package/docs/public-usage-guide.zh-CN.md +33 -0
- package/docs/tutorials/01-install-and-start.md +93 -0
- package/docs/tutorials/01-install-and-start.zh-CN.md +93 -0
- package/docs/tutorials/02-connect-chatgpt.md +82 -0
- package/docs/tutorials/02-connect-chatgpt.zh-CN.md +80 -0
- package/docs/tutorials/03-first-repository-task.md +57 -0
- package/docs/tutorials/03-first-repository-task.zh-CN.md +57 -0
- package/docs/tutorials/README.md +13 -0
- package/docs/tutorials/README.zh-CN.md +13 -0
- package/docs/wiki/Architecture.md +85 -0
- package/docs/wiki/Core-Concepts.md +27 -0
- package/docs/wiki/Home.md +36 -0
- package/docs/wiki/Implementation.md +172 -0
- package/docs/wiki/Installation.md +50 -0
- package/docs/wiki/Integrations.md +29 -0
- package/docs/wiki/Operations.md +26 -0
- package/docs/wiki/Quick-Start.md +46 -0
- package/docs/wiki/Releases-and-Upgrades.md +34 -0
- package/docs/wiki/Runtime-Architecture.md +50 -0
- package/docs/wiki/Security-Model.md +31 -0
- package/docs/wiki/Troubleshooting.md +38 -0
- package/docs/wiki/Work-Lifecycle.md +31 -0
- package/docs/wiki/_Sidebar.md +21 -0
- package/install.ps1 +48 -0
- package/install.sh +66 -0
- package/package.json +156 -0
- package/scripts/activate-source-baseline.ts +335 -0
- package/scripts/architecture-event.ts +822 -0
- package/scripts/architecture-queue.sh +647 -0
- package/scripts/archive-architecture-request.sh +244 -0
- package/scripts/archive-workflow.sh +295 -0
- package/scripts/assemble-template.ts +1172 -0
- package/scripts/benchmark-controller-hotpath.ts +212 -0
- package/scripts/benchmark-controller-round2.ts +139 -0
- package/scripts/benchmark-ios-agent-device.ts +514 -0
- package/scripts/benchmark-route-session-concurrency.ts +714 -0
- package/scripts/benchmark-thin-harness-gateway-ab.ts +632 -0
- package/scripts/benchmark-thin-harness-mcp-e2e.ts +479 -0
- package/scripts/benchmark-thin-harness.ts +251 -0
- package/scripts/bootstrap-runtime-maintenance-recovery.sh +183 -0
- package/scripts/canary-global-hook.sh +263 -0
- package/scripts/capability-config.ts +394 -0
- package/scripts/capability-resolver.ts +505 -0
- package/scripts/capture-plan.sh +335 -0
- package/scripts/check-agent-tooling.sh +1549 -0
- package/scripts/check-architecture-sync.sh +373 -0
- package/scripts/check-brain-manifest.sh +255 -0
- package/scripts/check-ci.sh +16 -0
- package/scripts/check-context-files.sh +49 -0
- package/scripts/check-deploy-sql-order.sh +117 -0
- package/scripts/check-mcp-compatibility.ts +93 -0
- package/scripts/check-npm-release.sh +32 -0
- package/scripts/check-open-source-tracked-surface.sh +146 -0
- package/scripts/check-package-identity.mjs +117 -0
- package/scripts/check-platform-support.mjs +79 -0
- package/scripts/check-public-docs.sh +105 -0
- package/scripts/check-public-release-surface.sh +19 -0
- package/scripts/check-release-published.sh +57 -0
- package/scripts/check-release-readiness.sh +43 -0
- package/scripts/check-release-version.mjs +74 -0
- package/scripts/check-runtime-architecture.mjs +593 -0
- package/scripts/check-runtime-env.mjs +89 -0
- package/scripts/check-skill-version.ts +193 -0
- package/scripts/check-tarball-install-smoke.sh +66 -0
- package/scripts/check-task-sync.sh +65 -0
- package/scripts/check-task-workflow.sh +1002 -0
- package/scripts/check-third-party-notices.mjs +36 -0
- package/scripts/codex-handoff-resume.sh +269 -0
- package/scripts/complete-control-plane-cutover.ts +126 -0
- package/scripts/context-contract-sync.sh +516 -0
- package/scripts/contract-run.ts +465 -0
- package/scripts/contract-worktree.sh +725 -0
- package/scripts/controller-ngrok-rotation.sh +678 -0
- package/scripts/create-project-dirs.sh +282 -0
- package/scripts/ensure-codegraph.sh +16 -0
- package/scripts/ensure-task-workflow.sh +1218 -0
- package/scripts/export-requirement-portfolio.ts +28 -0
- package/scripts/factor-lab-check.sh +73 -0
- package/scripts/factor-lab-new.sh +96 -0
- package/scripts/factor-lab-promote.sh +86 -0
- package/scripts/factor-lab-reject.sh +68 -0
- package/scripts/harness-trace-grade.sh +150 -0
- package/scripts/heartbeat-triage.sh +361 -0
- package/scripts/hook-dispatch-diet-report.ts +234 -0
- package/scripts/hook-shim.sh +93 -0
- package/scripts/init-project.sh +505 -0
- package/scripts/initializer-question-pack.ts +200 -0
- package/scripts/inspect-project-state.ts +280 -0
- package/scripts/install-desktop-operator-registration.ts +45 -0
- package/scripts/install-standalone-recovery.ts +74 -0
- package/scripts/lib/AGENTS.md +13 -0
- package/scripts/lib/CLAUDE.md +13 -0
- package/scripts/lib/controller-home-env.sh +69 -0
- package/scripts/lib/project-init-lib.sh +2759 -0
- package/scripts/load-standalone-recovery.sh +17 -0
- package/scripts/loop-engine-cutover-gate.ts +324 -0
- package/scripts/maintenance-triage.sh +167 -0
- package/scripts/migrate-project-template.sh +1189 -0
- package/scripts/migrate-requirement-portfolio.ts +86 -0
- package/scripts/migrate-workflow-docs.ts +413 -0
- package/scripts/new-plan.sh +183 -0
- package/scripts/new-spec.sh +50 -0
- package/scripts/new-sprint.sh +49 -0
- package/scripts/open-source-audit-allowlist.txt +46 -0
- package/scripts/package-source-archive.sh +147 -0
- package/scripts/plan-to-todo.sh +799 -0
- package/scripts/prepare-codex-handoff.sh +203 -0
- package/scripts/prepare-handoff.sh +81 -0
- package/scripts/public-release-files.txt +39 -0
- package/scripts/public-release.ts +174 -0
- package/scripts/publish-release-tarball.sh +27 -0
- package/scripts/refresh-current-status.sh +469 -0
- package/scripts/refresh-ios-physical-runtime.ts +196 -0
- package/scripts/repair-executable-modes.ts +8 -0
- package/scripts/rollout-all-registered-repos.sh +7 -0
- package/scripts/route-nl-vs-ts-eval.ts +679 -0
- package/scripts/run-bun-test-file.ts +182 -0
- package/scripts/run-governed-gate.ts +140 -0
- package/scripts/run-mobile-monitor-bridge.ts +71 -0
- package/scripts/run-skill-evals.ts +1022 -0
- package/scripts/run-skill-hook.ts +244 -0
- package/scripts/run-supervised-command.ts +24 -0
- package/scripts/run-tests-portable.sh +20 -0
- package/scripts/select-agent-context-blocks.sh +66 -0
- package/scripts/setup-plugins.sh +43 -0
- package/scripts/ship-worktrees.sh +561 -0
- package/scripts/smoke-browser-cdp-bridge.ts +93 -0
- package/scripts/smoke-chatgpt-supervised-automation.ts +208 -0
- package/scripts/smoke-ios-physical-device.ts +114 -0
- package/scripts/smoke-mcp-http-runtime.ts +216 -0
- package/scripts/smoke-mcp-tool-surface.ts +117 -0
- package/scripts/smoke-runtime-recovery.ts +220 -0
- package/scripts/smoke-schedule-engine.ts +283 -0
- package/scripts/sprint-backlog.sh +837 -0
- package/scripts/stage-runtime-release.ts +38 -0
- package/scripts/summarize-failures.sh +125 -0
- package/scripts/switch-plan.sh +153 -0
- package/scripts/sync-brain-docs.sh +381 -0
- package/scripts/sync-codex-installed-copies.sh +183 -0
- package/scripts/test-governance.ts +75 -0
- package/scripts/verify-contract.sh +705 -0
- package/scripts/verify-forge-runtime.sh +31 -0
- package/scripts/verify-sprint.sh +680 -0
- package/scripts/windows-paths.d.mts +7 -0
- package/scripts/windows-paths.mjs +28 -0
- package/scripts/workflow-contract.ts +206 -0
- package/scripts/workstream-sync.sh +263 -0
- package/src/cli/agent-jobs/executable-resolver.ts +289 -0
- package/src/cli/agent-jobs/executor-health.ts +252 -0
- package/src/cli/agent-jobs/integration.ts +963 -0
- package/src/cli/agent-jobs/job-manager.ts +2030 -0
- package/src/cli/agent-jobs/types.ts +219 -0
- package/src/cli/agent-jobs/worker-lifecycle.ts +288 -0
- package/src/cli/chatgpt-browser/bind-server.ts +504 -0
- package/src/cli/chatgpt-browser/binding.ts +140 -0
- package/src/cli/chatgpt-browser/bridge-extension.ts +221 -0
- package/src/cli/chatgpt-browser/bridge-provider.ts +268 -0
- package/src/cli/chatgpt-browser/engine.ts +569 -0
- package/src/cli/chatgpt-browser/file-policy.ts +158 -0
- package/src/cli/chatgpt-browser/native-provider.ts +553 -0
- package/src/cli/chatgpt-browser/oracle-provider.ts +437 -0
- package/src/cli/chatgpt-browser/prompt-assembler.ts +29 -0
- package/src/cli/chatgpt-browser/session-store.ts +301 -0
- package/src/cli/chatgpt-browser/types.ts +172 -0
- package/src/cli/commands/adopt-plan.ts +186 -0
- package/src/cli/commands/brain-root.ts +132 -0
- package/src/cli/commands/brain.ts +609 -0
- package/src/cli/commands/capability-context.ts +613 -0
- package/src/cli/commands/chatgpt.ts +424 -0
- package/src/cli/commands/controller.ts +680 -0
- package/src/cli/commands/docs.ts +103 -0
- package/src/cli/commands/doctor.ts +476 -0
- package/src/cli/commands/global-runtime.ts +323 -0
- package/src/cli/commands/hook.ts +34 -0
- package/src/cli/commands/init-hook.ts +870 -0
- package/src/cli/commands/init.ts +829 -0
- package/src/cli/commands/install.ts +86 -0
- package/src/cli/commands/mcp.ts +315 -0
- package/src/cli/commands/migrate.ts +154 -0
- package/src/cli/commands/plugin.ts +74 -0
- package/src/cli/commands/prompt-guard-decision.ts +238 -0
- package/src/cli/commands/recovery.ts +667 -0
- package/src/cli/commands/repository.ts +224 -0
- package/src/cli/commands/run.ts +23 -0
- package/src/cli/commands/runtime.ts +191 -0
- package/src/cli/commands/security.ts +440 -0
- package/src/cli/commands/status.ts +137 -0
- package/src/cli/commands/tools.ts +167 -0
- package/src/cli/controller/check-runner.ts +1022 -0
- package/src/cli/controller/codex-continuation.ts +153 -0
- package/src/cli/controller/completion-backlog.ts +380 -0
- package/src/cli/controller/completion-orchestrator.ts +1807 -0
- package/src/cli/controller/completion-target.ts +96 -0
- package/src/cli/controller/composite-operations.ts +196 -0
- package/src/cli/controller/composite-result.ts +130 -0
- package/src/cli/controller/context-pack.ts +652 -0
- package/src/cli/controller/execution-completion.ts +175 -0
- package/src/cli/controller/execution-policy.ts +392 -0
- package/src/cli/controller/governance.ts +594 -0
- package/src/cli/controller/issue-store.ts +1421 -0
- package/src/cli/controller/launch-agents.ts +410 -0
- package/src/cli/controller/legacy-issue-cutover.ts +415 -0
- package/src/cli/controller/lifecycle-authority.ts +16 -0
- package/src/cli/controller/migrated-issue-decision.ts +237 -0
- package/src/cli/controller/operational-plan.ts +226 -0
- package/src/cli/controller/postcondition.ts +134 -0
- package/src/cli/controller/progress.ts +656 -0
- package/src/cli/controller/project-state.ts +147 -0
- package/src/cli/controller/requirement-portfolio-export.ts +146 -0
- package/src/cli/controller/requirement-portfolio-migration.ts +613 -0
- package/src/cli/controller/run-evidence.ts +43 -0
- package/src/cli/controller/runtime-config.ts +92 -0
- package/src/cli/controller/stuck-state-migration.ts +396 -0
- package/src/cli/controller/task-ledger.ts +632 -0
- package/src/cli/controller/task-status-resolver.ts +403 -0
- package/src/cli/controller/types.ts +273 -0
- package/src/cli/controller/work-mode.ts +186 -0
- package/src/cli/controller/worklog.ts +201 -0
- package/src/cli/diagnostic-entry.ts +39 -0
- package/src/cli/editing/edit-session.ts +1606 -0
- package/src/cli/editing/executable-modes.ts +70 -0
- package/src/cli/github/github.ts +274 -0
- package/src/cli/github/plugin.ts +345 -0
- package/src/cli/github/session.ts +104 -0
- package/src/cli/hook/prompt-guard-decision.ts +238 -0
- package/src/cli/hook/prompt-intents.ts +534 -0
- package/src/cli/hook/route-registry.ts +107 -0
- package/src/cli/hook/runtime.ts +303 -0
- package/src/cli/hook/state-snapshot.ts +388 -0
- package/src/cli/hook-entry.ts +51 -0
- package/src/cli/index.ts +574 -0
- package/src/cli/installer/managed-entries.ts +78 -0
- package/src/cli/installer/shared.ts +65 -0
- package/src/cli/installer/targets/claude.ts +136 -0
- package/src/cli/installer/targets/codex.ts +174 -0
- package/src/cli/installer/targets/registry.ts +28 -0
- package/src/cli/installer/types.ts +72 -0
- package/src/cli/local-bridge/connector-freshness.ts +529 -0
- package/src/cli/local-bridge/console-view-models.ts +324 -0
- package/src/cli/local-bridge/dashboard.ts +1312 -0
- package/src/cli/local-bridge/facade-api.ts +1877 -0
- package/src/cli/local-bridge/job-store.ts +1451 -0
- package/src/cli/local-bridge/mobile-intents.ts +344 -0
- package/src/cli/local-bridge/mobile-monitor.ts +209 -0
- package/src/cli/local-bridge/server.ts +3183 -0
- package/src/cli/local-bridge/types.ts +176 -0
- package/src/cli/mcp/access-mode.ts +132 -0
- package/src/cli/mcp/access-tools.ts +387 -0
- package/src/cli/mcp/audit.ts +32 -0
- package/src/cli/mcp/auth.ts +402 -0
- package/src/cli/mcp/instructions.ts +33 -0
- package/src/cli/mcp/legacy-context.ts +16 -0
- package/src/cli/mcp/legacy-tool-service.ts +5516 -0
- package/src/cli/mcp/multi-repository.ts +423 -0
- package/src/cli/mcp/oauth.ts +226 -0
- package/src/cli/mcp/paths.ts +134 -0
- package/src/cli/mcp/policy.ts +230 -0
- package/src/cli/mcp/proxy-env.ts +62 -0
- package/src/cli/mcp/readiness-projection.ts +24 -0
- package/src/cli/mcp/redaction.ts +21 -0
- package/src/cli/mcp/repo.ts +24 -0
- package/src/cli/mcp/repository-tools.ts +1295 -0
- package/src/cli/mcp/server.ts +341 -0
- package/src/cli/mcp/setup.ts +793 -0
- package/src/cli/mcp/tool-capability-cache.ts +87 -0
- package/src/cli/mcp/tools.ts +9 -0
- package/src/cli/mcp/toolset-names.ts +70 -0
- package/src/cli/mcp/toolset-selection.ts +41 -0
- package/src/cli/mcp/toolset.ts +300 -0
- package/src/cli/mcp/transports/AGENTS.md +39 -0
- package/src/cli/mcp/transports/CLAUDE.md +39 -0
- package/src/cli/mcp/transports/http.ts +1234 -0
- package/src/cli/mcp/transports/session-registry.ts +428 -0
- package/src/cli/mcp/transports/stdio.ts +8 -0
- package/src/cli/mcp/types.ts +41 -0
- package/src/cli/mcp/umbrella-tools.ts +117 -0
- package/src/cli/repo-adoption/reclaim-runtime.ts +681 -0
- package/src/cli/repositories/branch-name-policy.ts +57 -0
- package/src/cli/repositories/command-classifier.ts +600 -0
- package/src/cli/repositories/command-executor.ts +1066 -0
- package/src/cli/repositories/command-normalization.ts +53 -0
- package/src/cli/repositories/command-scope.ts +264 -0
- package/src/cli/repositories/controller-home.ts +88 -0
- package/src/cli/repositories/entity-migration.ts +199 -0
- package/src/cli/repositories/ephemeral-workspace.ts +62 -0
- package/src/cli/repositories/git-command-executor.ts +301 -0
- package/src/cli/repositories/goal-registry.ts +239 -0
- package/src/cli/repositories/identity.ts +64 -0
- package/src/cli/repositories/local-project-onboarding.ts +539 -0
- package/src/cli/repositories/locks.ts +262 -0
- package/src/cli/repositories/registry.ts +1057 -0
- package/src/cli/repositories/runtime-storage.ts +411 -0
- package/src/cli/repositories/safe-patch.ts +421 -0
- package/src/cli/repositories/selected-path-actions.ts +341 -0
- package/src/cli/repositories/structured-git.ts +257 -0
- package/src/cli/repositories/types.ts +155 -0
- package/src/cli/repositories/umbrella.ts +134 -0
- package/src/cli/repositories/workbench.ts +140 -0
- package/src/cli/repositories/worktree-storage.ts +137 -0
- package/src/cli/repository/inspector.ts +637 -0
- package/src/cli/repository/session-cache.ts +453 -0
- package/src/cli/runtime/helper-runner.ts +172 -0
- package/src/cli/runtime-invocation.ts +318 -0
- package/src/cli/tools/codegraph.ts +676 -0
- package/src/cli/v81-entry.ts +29 -0
- package/src/core/adoption/gitignore-plan.ts +127 -0
- package/src/core/adoption/helper-wrapper-plan.ts +159 -0
- package/src/core/adoption/manifest-templates.ts +60 -0
- package/src/core/adoption/modes.ts +7 -0
- package/src/core/adoption/operations.ts +126 -0
- package/src/core/adoption/plan.ts +149 -0
- package/src/core/adoption/render.ts +70 -0
- package/src/core/adoption/rollback.ts +62 -0
- package/src/core/adoption/summary.ts +43 -0
- package/src/core/adoption/workflow-contract-asset.ts +14 -0
- package/src/core/adoption/workflow-contract-plan.ts +31 -0
- package/src/effects/fs-transaction.ts +540 -0
- package/src/effects/git-executable.ts +59 -0
- package/src/effects/managed-block.ts +86 -0
- package/src/effects/path-safety.ts +47 -0
- package/src/effects/process-runner.ts +192 -0
- package/src/runtime/assistant/action-proposals.ts +279 -0
- package/src/runtime/assistant/intent.ts +395 -0
- package/src/runtime/assistant/model-provider.ts +460 -0
- package/src/runtime/assistant/openapi.ts +223 -0
- package/src/runtime/assistant/policy.ts +89 -0
- package/src/runtime/assistant/readiness.ts +258 -0
- package/src/runtime/assistant/routine-runtime.ts +477 -0
- package/src/runtime/assistant/schedule-binding.ts +171 -0
- package/src/runtime/assistant/standing-grants.ts +375 -0
- package/src/runtime/assistant/store.ts +243 -0
- package/src/runtime/assistant/types.ts +135 -0
- package/src/runtime/context/codegraph-read-provider.ts +347 -0
- package/src/runtime/context/codegraph-sidecar.cjs +207 -0
- package/src/runtime/control-plane/dispatch-priority.ts +67 -0
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +151 -0
- package/src/runtime/control-plane/execution/execution-identity.ts +456 -0
- package/src/runtime/control-plane/execution/session-store.ts +175 -0
- package/src/runtime/control-plane/execution/validation.ts +117 -0
- package/src/runtime/control-plane/execution/verification-evidence.ts +191 -0
- package/src/runtime/control-plane/execution/work-handle-store.ts +225 -0
- package/src/runtime/control-plane/execution/work-prepare-request-store.ts +159 -0
- package/src/runtime/control-plane/execution/work-task-receipt.ts +445 -0
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +821 -0
- package/src/runtime/control-plane/facade/capability-registry.ts +279 -0
- package/src/runtime/control-plane/facade/check-normalization.ts +233 -0
- package/src/runtime/control-plane/facade/codex-delegation.ts +196 -0
- package/src/runtime/control-plane/facade/controller-session-store.ts +199 -0
- package/src/runtime/control-plane/facade/facade-result.ts +41 -0
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +177 -0
- package/src/runtime/control-plane/facade/goal-workloop.ts +1216 -0
- package/src/runtime/control-plane/facade/handoff-inbox-store.ts +221 -0
- package/src/runtime/control-plane/facade/index.ts +16 -0
- package/src/runtime/control-plane/facade/operation-digest.ts +528 -0
- package/src/runtime/control-plane/facade/plan-contract-store.ts +403 -0
- package/src/runtime/control-plane/facade/policy-gate.ts +149 -0
- package/src/runtime/control-plane/facade/requirement-board.ts +280 -0
- package/src/runtime/control-plane/facade/self-healing-loop.ts +555 -0
- package/src/runtime/control-plane/facade/suggested-actions.ts +108 -0
- package/src/runtime/control-plane/facade/types.ts +729 -0
- package/src/runtime/control-plane/facade/work-admission-policy.ts +93 -0
- package/src/runtime/control-plane/facade/work-continuation.ts +104 -0
- package/src/runtime/control-plane/facade/work-contract-store.ts +1118 -0
- package/src/runtime/control-plane/facade/work-task-projection.ts +66 -0
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +365 -0
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +920 -0
- package/src/runtime/control-plane/global-scheduler/wake-signal.ts +89 -0
- package/src/runtime/control-plane/goal-loop/config-facade.ts +700 -0
- package/src/runtime/control-plane/goal-loop/config-store.ts +570 -0
- package/src/runtime/control-plane/goal-loop/config-types.ts +213 -0
- package/src/runtime/control-plane/goal-loop/executor-router.ts +109 -0
- package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +149 -0
- package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +304 -0
- package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +1001 -0
- package/src/runtime/control-plane/goal-loop/handoff-packet.ts +160 -0
- package/src/runtime/control-plane/goal-loop/index.ts +13 -0
- package/src/runtime/control-plane/goal-loop/local-tools.ts +290 -0
- package/src/runtime/control-plane/goal-loop/policy-gates.ts +203 -0
- package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +93 -0
- package/src/runtime/control-plane/goal-loop/provider-registry.ts +545 -0
- package/src/runtime/control-plane/goal-loop/types.ts +378 -0
- package/src/runtime/control-plane/governance/access-policy.ts +147 -0
- package/src/runtime/control-plane/governance/authorization.ts +195 -0
- package/src/runtime/control-plane/governance/external-effects.ts +43 -0
- package/src/runtime/control-plane/launcher/thin-launcher.ts +97 -0
- package/src/runtime/control-plane/persistence/metadata-payload-policy.ts +53 -0
- package/src/runtime/control-plane/persistence/requirement-store.ts +241 -0
- package/src/runtime/control-plane/persistence/sqlite-store.ts +539 -0
- package/src/runtime/control-plane/release-retention.ts +447 -0
- package/src/runtime/control-plane/repo-actor/actor.ts +181 -0
- package/src/runtime/control-plane/repo-actor/registry.ts +20 -0
- package/src/runtime/control-plane/routing/index.ts +1 -0
- package/src/runtime/control-plane/routing/route-policy.ts +379 -0
- package/src/runtime/control-plane/runtime-cleanup.ts +727 -0
- package/src/runtime/control-plane/runtime-generation.ts +509 -0
- package/src/runtime/control-plane/runtime-pid-ownership.ts +11 -0
- package/src/runtime/control-plane/runtime-status-client.ts +90 -0
- package/src/runtime/control-plane/startup-recovery.ts +198 -0
- package/src/runtime/diagnostics/connectivity-observability.ts +475 -0
- package/src/runtime/diagnostics/mcp-timing.ts +64 -0
- package/src/runtime/diagnostics/performance.ts +379 -0
- package/src/runtime/diagnostics/process-facade.ts +350 -0
- package/src/runtime/diagnostics/read-only-tool.ts +226 -0
- package/src/runtime/evidence/artifact-store.ts +154 -0
- package/src/runtime/evidence/event-ledger.ts +80 -0
- package/src/runtime/evidence/evidence-store.ts +68 -0
- package/src/runtime/evidence/process-check-receipt.ts +43 -0
- package/src/runtime/evidence/result-store.ts +209 -0
- package/src/runtime/evidence/sensitive-output.ts +353 -0
- package/src/runtime/execution/jobs/child-reference.ts +124 -0
- package/src/runtime/execution/jobs/legacy-adapter.ts +22 -0
- package/src/runtime/execution/jobs/receipt-recovery.ts +119 -0
- package/src/runtime/execution/jobs/receipt-store.ts +243 -0
- package/src/runtime/execution/jobs/store.ts +737 -0
- package/src/runtime/execution/jobs/timeouts.ts +109 -0
- package/src/runtime/execution/jobs/types.ts +230 -0
- package/src/runtime/execution/jobs/wait.ts +45 -0
- package/src/runtime/execution/process-runtime/canary.ts +26 -0
- package/src/runtime/execution/process-runtime/check-facade.ts +272 -0
- package/src/runtime/execution/process-runtime/check-receipt.ts +185 -0
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +60 -0
- package/src/runtime/execution/process-runtime/command-facade.ts +321 -0
- package/src/runtime/execution/process-runtime/gc.ts +171 -0
- package/src/runtime/execution/process-runtime/index.ts +9 -0
- package/src/runtime/execution/process-runtime/interactive-admission.ts +23 -0
- package/src/runtime/execution/process-runtime/process-runner-entry.ts +466 -0
- package/src/runtime/execution/process-runtime/resource-claims.ts +440 -0
- package/src/runtime/execution/process-runtime/runtime.ts +1816 -0
- package/src/runtime/execution/process-runtime/store.ts +687 -0
- package/src/runtime/execution/process-runtime/types.ts +352 -0
- package/src/runtime/execution/thin-harness/async-process.ts +181 -0
- package/src/runtime/execution/thin-harness/batch-executor.ts +671 -0
- package/src/runtime/execution/thin-harness/execution-router.ts +700 -0
- package/src/runtime/execution/thin-harness/fast-executor.ts +1263 -0
- package/src/runtime/execution/thin-harness/fast-receipt.ts +235 -0
- package/src/runtime/execution/thin-harness/fingerprint-worker.ts +191 -0
- package/src/runtime/execution/thin-harness/index.ts +13 -0
- package/src/runtime/execution/thin-harness/latency-trace.ts +107 -0
- package/src/runtime/execution/thin-harness/lightweight-lanes.ts +745 -0
- package/src/runtime/execution/thin-harness/mutation-gate.ts +578 -0
- package/src/runtime/execution/thin-harness/proposal-store.ts +409 -0
- package/src/runtime/execution/thin-harness/request-ledger.ts +595 -0
- package/src/runtime/execution/thin-harness/search-worker.ts +114 -0
- package/src/runtime/execution/thin-harness/types.ts +313 -0
- package/src/runtime/execution/thin-harness/workspace-savepoint.ts +333 -0
- package/src/runtime/execution/workers/executor.ts +687 -0
- package/src/runtime/execution/workers/ownership.ts +94 -0
- package/src/runtime/execution/workers/worker-entry.ts +277 -0
- package/src/runtime/gateway/mcp/execution-tools.ts +2009 -0
- package/src/runtime/gateway/mcp/persisted-check-process.ts +255 -0
- package/src/runtime/gateway/mcp/process-tools.ts +230 -0
- package/src/runtime/gateway/mcp/resource-policy.ts +195 -0
- package/src/runtime/gateway/mcp/router.ts +1088 -0
- package/src/runtime/gateway/mcp/runtime-tools.ts +5994 -0
- package/src/runtime/gateway/mcp/work-validation-reconciler.ts +219 -0
- package/src/runtime/health/evaluator.ts +474 -0
- package/src/runtime/health/index.ts +2 -0
- package/src/runtime/health/operational-view.ts +198 -0
- package/src/runtime/maintenance/cleanup.ts +419 -0
- package/src/runtime/model-clients/deepseek-adapter.ts +312 -0
- package/src/runtime/model-clients/index.ts +2 -0
- package/src/runtime/model-clients/types.ts +101 -0
- package/src/runtime/observability/gateway-contention-metrics.ts +129 -0
- package/src/runtime/observability/route-session-benchmark.ts +110 -0
- package/src/runtime/personal-assistant/gmail-triage-manager.ts +231 -0
- package/src/runtime/personal-assistant/reporting-runtime.ts +344 -0
- package/src/runtime/personal-assistant/triage-runtime.ts +748 -0
- package/src/runtime/plugins/app-store-connect-adapter.ts +868 -0
- package/src/runtime/plugins/browser-adapter.ts +2812 -0
- package/src/runtime/plugins/browser-automation-service.ts +46 -0
- package/src/runtime/plugins/browser-handoff-host.ts +304 -0
- package/src/runtime/plugins/browser-handoff.ts +371 -0
- package/src/runtime/plugins/browser-macos-bridge.ts +865 -0
- package/src/runtime/plugins/browser-node-bridge-host.ts +64 -0
- package/src/runtime/plugins/browser-node-bridge.ts +249 -0
- package/src/runtime/plugins/desktop-adapter.ts +374 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +418 -0
- package/src/runtime/plugins/errors.ts +39 -0
- package/src/runtime/plugins/execution-origin.ts +26 -0
- package/src/runtime/plugins/external-adapter.ts +322 -0
- package/src/runtime/plugins/external-registration.ts +308 -0
- package/src/runtime/plugins/external-unix-socket-probe.cjs +79 -0
- package/src/runtime/plugins/external-unix-socket.ts +238 -0
- package/src/runtime/plugins/first-party-registry.ts +37 -0
- package/src/runtime/plugins/github-adapter.ts +266 -0
- package/src/runtime/plugins/gmail-adapter.ts +729 -0
- package/src/runtime/plugins/google-calendar-adapter.ts +468 -0
- package/src/runtime/plugins/google-shared.ts +578 -0
- package/src/runtime/plugins/google-tasks-adapter.ts +535 -0
- package/src/runtime/plugins/interaction-session.ts +252 -0
- package/src/runtime/plugins/ios/agent-device-capabilities.ts +258 -0
- package/src/runtime/plugins/ios/agent-device-failures.ts +95 -0
- package/src/runtime/plugins/ios/agent-device-provider.ts +63 -0
- package/src/runtime/plugins/ios/agent-device-typed-provider.ts +268 -0
- package/src/runtime/plugins/ios/app-adapters.ts +122 -0
- package/src/runtime/plugins/ios-adapter.ts +509 -0
- package/src/runtime/plugins/ios-agent-device.ts +2311 -0
- package/src/runtime/plugins/ios-physical-device.ts +955 -0
- package/src/runtime/plugins/local-system-adapter.ts +1016 -0
- package/src/runtime/plugins/macos-capability-broker.ts +73 -0
- package/src/runtime/plugins/managed-process-adapter.ts +366 -0
- package/src/runtime/plugins/resend-adapter.ts +506 -0
- package/src/runtime/plugins/store.ts +1025 -0
- package/src/runtime/plugins/types.ts +138 -0
- package/src/runtime/projections/controller-context.ts +596 -0
- package/src/runtime/projections/git-status-sampler.ts +84 -0
- package/src/runtime/projections/invalidation.ts +250 -0
- package/src/runtime/projections/materialized-view.ts +694 -0
- package/src/runtime/recovery/actions.ts +158 -0
- package/src/runtime/recovery/audit.ts +38 -0
- package/src/runtime/recovery/classifier.ts +163 -0
- package/src/runtime/recovery/index.ts +9 -0
- package/src/runtime/recovery/local-jobs-repair.ts +393 -0
- package/src/runtime/recovery/maintenance-executor.ts +822 -0
- package/src/runtime/recovery/patch-handoff.ts +50 -0
- package/src/runtime/recovery/probe.ts +308 -0
- package/src/runtime/recovery/store.ts +35 -0
- package/src/runtime/recovery/types.ts +175 -0
- package/src/runtime/release/identity.ts +101 -0
- package/src/runtime/release/release-gate.ts +137 -0
- package/src/runtime/resources/claims/conflicts.ts +205 -0
- package/src/runtime/resources/index.ts +1 -0
- package/src/runtime/resources/leases/store.ts +539 -0
- package/src/runtime/resources/leases/types.ts +50 -0
- package/src/runtime/resources/ownership.ts +72 -0
- package/src/runtime/root/controller-services.ts +42 -0
- package/src/runtime/root/entry.ts +70 -0
- package/src/runtime/root/gateway-adapter.ts +46 -0
- package/src/runtime/root/local-bridge.ts +72 -0
- package/src/runtime/root/mcp-transport.ts +198 -0
- package/src/runtime/root/ownership.ts +94 -0
- package/src/runtime/root/readiness.ts +69 -0
- package/src/runtime/root/release-manifest.ts +147 -0
- package/src/runtime/root/release-materialize.ts +509 -0
- package/src/runtime/root/release-store.ts +236 -0
- package/src/runtime/root/runtime.ts +318 -0
- package/src/runtime/root/scheduler.ts +58 -0
- package/src/runtime/root/service-runner-entry.ts +20 -0
- package/src/runtime/root/service-runner.ts +69 -0
- package/src/runtime/root/service.ts +291 -0
- package/src/runtime/root/status.ts +127 -0
- package/src/runtime/root/types.ts +112 -0
- package/src/runtime/root/write-fence.ts +309 -0
- package/src/runtime/safe-tooling/browser-safe.ts +133 -0
- package/src/runtime/safe-tooling/external-filesystem.ts +304 -0
- package/src/runtime/safe-tooling/google-credential-store.ts +95 -0
- package/src/runtime/safe-tooling/google-oauth-broker.ts +286 -0
- package/src/runtime/safe-tooling/index.ts +11 -0
- package/src/runtime/safe-tooling/ios-development.ts +1039 -0
- package/src/runtime/safe-tooling/job-summary.ts +361 -0
- package/src/runtime/safe-tooling/plugin-summary.ts +53 -0
- package/src/runtime/safe-tooling/resend-oauth.ts +387 -0
- package/src/runtime/safe-tooling/review-artifacts.ts +133 -0
- package/src/runtime/safe-tooling/types.ts +103 -0
- package/src/runtime/safe-tooling/workspace-auth.ts +116 -0
- package/src/runtime/shared/bounded-child-supervisor.ts +235 -0
- package/src/runtime/shared/idempotent-retry.ts +128 -0
- package/src/runtime/shared/json-files.ts +43 -0
- package/src/runtime/shared/local-bridge-surface.ts +231 -0
- package/src/runtime/shared/managed-env.ts +154 -0
- package/src/runtime/shared/node-ts-loader.mjs +50 -0
- package/src/runtime/shared/process-environment.ts +97 -0
- package/src/runtime/shared/process-identity.ts +72 -0
- package/src/runtime/shared/process-tree.ts +227 -0
- package/src/runtime/shared/response-budget.ts +246 -0
- package/src/runtime/standalone-recovery/core.ts +2275 -0
- package/src/runtime/standalone-recovery/entry.ts +686 -0
- package/src/runtime/standalone-recovery/http-transport.ts +317 -0
- package/src/runtime/standalone-recovery/installer.ts +747 -0
- package/src/runtime/standalone-recovery/release.ts +241 -0
- package/src/runtime/watchdog/workflow-watchdog.ts +118 -0
- package/src/runtime/workflow/campaigns/cleanup.ts +263 -0
- package/src/runtime/workflow/campaigns/engine.ts +112 -0
- package/src/runtime/workflow/campaigns/normalize.ts +100 -0
- package/src/runtime/workflow/campaigns/review.ts +315 -0
- package/src/runtime/workflow/campaigns/store.ts +526 -0
- package/src/runtime/workflow/campaigns/supervisor.ts +112 -0
- package/src/runtime/workflow/campaigns/types.ts +290 -0
- package/src/runtime/workflow/campaigns/workspace-agent.ts +155 -0
- package/src/runtime/workflow/campaigns/workspace.ts +200 -0
- package/src/runtime/workflow/findings/store.ts +165 -0
- package/src/runtime/workflow/findings/types.ts +36 -0
- package/src/runtime/workflow/portfolio/engine.ts +20 -0
- package/src/runtime/workflow/portfolio/store.ts +185 -0
- package/src/runtime/workflow/portfolio/types.ts +29 -0
- package/src/runtime/workflow/schedules/engine.ts +620 -0
- package/src/runtime/workflow/schedules/settlement.ts +144 -0
- package/src/runtime/workflow/schedules/store.ts +417 -0
- package/src/runtime/workflow/schedules/types.ts +124 -0
- package/src/runtime/workspace-targets/index.ts +24 -0
- package/src/runtime/workspace-targets/target-grant.ts +721 -0
- package/src/testing/test-governance.ts +626 -0
- package/src/version.ts +21 -0
|
@@ -0,0 +1,2812 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'crypto';
|
|
2
|
+
import { createRequire } from 'module';
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
4
|
+
import { basename, dirname, join, relative, resolve } from 'path';
|
|
5
|
+
import type {
|
|
6
|
+
AssistantPluginActionDescriptor,
|
|
7
|
+
AssistantPluginActionExecutionInput,
|
|
8
|
+
AssistantPluginCapability,
|
|
9
|
+
AssistantPluginHealth,
|
|
10
|
+
AssistantPluginManifest,
|
|
11
|
+
AssistantPluginPermissionScope,
|
|
12
|
+
} from './types';
|
|
13
|
+
import { AssistantPluginError, toAssistantPluginError } from './errors';
|
|
14
|
+
import { executeBrowserActionThroughNode, shouldUseBrowserNodeBridge } from './browser-node-bridge';
|
|
15
|
+
import {
|
|
16
|
+
closeMacOsBrowserOwnedTab,
|
|
17
|
+
createMacOsBrowserOwnedPage,
|
|
18
|
+
reattachMacOsBrowserOwnedPage,
|
|
19
|
+
discoverMacOsBrowserAttachment,
|
|
20
|
+
getMacOsBrowserAttachObservation,
|
|
21
|
+
macOsActiveBrowserAttachSupported,
|
|
22
|
+
macOsBrowserJavaScriptAutomationDisabled,
|
|
23
|
+
type MacOsBrowserAttachAttempt,
|
|
24
|
+
type MacOsBrowserProduct,
|
|
25
|
+
type MacOsBrowserTabRef,
|
|
26
|
+
} from './browser-macos-bridge';
|
|
27
|
+
import {
|
|
28
|
+
assertBrowserProfileAvailable,
|
|
29
|
+
assertBrowserSessionAvailable,
|
|
30
|
+
cancelBrowserHandoff,
|
|
31
|
+
getBrowserHandoff,
|
|
32
|
+
listBrowserHandoffs,
|
|
33
|
+
resumeBrowserHandoff,
|
|
34
|
+
startBrowserHandoff,
|
|
35
|
+
} from './browser-handoff';
|
|
36
|
+
|
|
37
|
+
const BROWSER_PLUGIN_ID = 'browser';
|
|
38
|
+
const CONFIG_ROOT = '.forge/plugins';
|
|
39
|
+
const STATE_ROOT = '.forge/browser';
|
|
40
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
41
|
+
const DEFAULT_MAX_TEXT_CHARS = 20_000;
|
|
42
|
+
const DEFAULT_POST_ACTION_WAIT_MS = 750;
|
|
43
|
+
const DEFAULT_CDP_DISCOVERY_TIMEOUT_MS = 1_500;
|
|
44
|
+
const MAX_CDP_DISCOVERY_TIMEOUT_MS = 5_000;
|
|
45
|
+
const MAX_CDP_ENDPOINT_CANDIDATES = 5;
|
|
46
|
+
|
|
47
|
+
type WaitUntil = 'load' | 'domcontentloaded' | 'networkidle';
|
|
48
|
+
type WaitForSelectorState = 'attached' | 'detached' | 'visible' | 'hidden';
|
|
49
|
+
type BrowserMode = 'attach_preferred' | 'managed_persistent' | 'isolated';
|
|
50
|
+
type BrowserProfileMode = 'repo_local' | 'custom';
|
|
51
|
+
type BrowserChannel = 'chromium' | 'chrome' | 'chrome-beta' | 'chrome-dev' | 'chrome-canary';
|
|
52
|
+
type BrowserCdpAttachFallback = 'managed_persistent' | 'fail_closed';
|
|
53
|
+
type BrowserNativeAttachMode = 'auto' | 'disabled';
|
|
54
|
+
|
|
55
|
+
interface BrowserPluginConfig {
|
|
56
|
+
schemaVersion: 1;
|
|
57
|
+
enabled: boolean;
|
|
58
|
+
provider: 'playwright';
|
|
59
|
+
browserMode?: BrowserMode;
|
|
60
|
+
profileMode?: BrowserProfileMode;
|
|
61
|
+
profileDir?: string;
|
|
62
|
+
profileDirectory?: string;
|
|
63
|
+
browserChannel?: BrowserChannel;
|
|
64
|
+
executablePath?: string;
|
|
65
|
+
cdpEndpoint?: string;
|
|
66
|
+
cdpEndpointCandidates?: string[];
|
|
67
|
+
cdpDiscoveryTimeoutMs?: number;
|
|
68
|
+
cdpAttachFallback?: BrowserCdpAttachFallback;
|
|
69
|
+
nativeAttachMode?: BrowserNativeAttachMode;
|
|
70
|
+
nativeBrowserCandidates?: MacOsBrowserProduct[];
|
|
71
|
+
defaultTimeoutMs?: number;
|
|
72
|
+
allowedDomains?: string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface BrowserTabResumeState {
|
|
76
|
+
key: string;
|
|
77
|
+
index: number;
|
|
78
|
+
url: string;
|
|
79
|
+
title?: string;
|
|
80
|
+
matchedBy: BrowserTabMatchReason;
|
|
81
|
+
inventoryCount: number;
|
|
82
|
+
capturedAt: string;
|
|
83
|
+
ownership?: 'plugin_owned' | 'user_owned';
|
|
84
|
+
ownerToken?: string;
|
|
85
|
+
windowId?: string;
|
|
86
|
+
tabId?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface BrowserSessionConnectionState {
|
|
90
|
+
mode: BrowserMode;
|
|
91
|
+
activeMode: BrowserMode;
|
|
92
|
+
provider: 'playwright-cdp' | 'playwright-persistent-context' | 'macos-apple-events';
|
|
93
|
+
endpoint?: string;
|
|
94
|
+
browserVersion?: string;
|
|
95
|
+
browserProduct?: MacOsBrowserProduct;
|
|
96
|
+
fallback?: BrowserConnectionFallback;
|
|
97
|
+
tab?: BrowserTabResumeState;
|
|
98
|
+
sessionResume?: BrowserSessionResumeDiagnostic;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface BrowserSessionState {
|
|
102
|
+
schemaVersion: 1;
|
|
103
|
+
sessionId: string;
|
|
104
|
+
url: string;
|
|
105
|
+
title?: string;
|
|
106
|
+
createdAt: string;
|
|
107
|
+
updatedAt: string;
|
|
108
|
+
browser?: BrowserSessionConnectionState;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface BrowserActionTarget {
|
|
112
|
+
sessionId: string;
|
|
113
|
+
url: string;
|
|
114
|
+
existingSession?: BrowserSessionState;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface BrowserActionScreenshot {
|
|
118
|
+
path: string;
|
|
119
|
+
relativePath: string;
|
|
120
|
+
bytes: number;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface BrowserProfileSelection {
|
|
124
|
+
profileDir: string;
|
|
125
|
+
profileDirectory?: string;
|
|
126
|
+
selectedProfilePath: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
type BrowserContextLike = {
|
|
130
|
+
pages(): PageLike[];
|
|
131
|
+
newPage(): Promise<PageLike>;
|
|
132
|
+
close(): Promise<void>;
|
|
133
|
+
route(pattern: string, handler: (route: RouteLike) => Promise<void> | void): Promise<void>;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
type BrowserLike = {
|
|
137
|
+
contexts(): BrowserContextLike[];
|
|
138
|
+
newContext?(): Promise<BrowserContextLike>;
|
|
139
|
+
close?(): Promise<void>;
|
|
140
|
+
disconnect?(): Promise<void> | void;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
type PageLike = {
|
|
144
|
+
goto(url: string, options?: Record<string, unknown>): Promise<unknown>;
|
|
145
|
+
reload?(options?: Record<string, unknown>): Promise<unknown>;
|
|
146
|
+
goBack?(options?: Record<string, unknown>): Promise<unknown>;
|
|
147
|
+
title(): Promise<string>;
|
|
148
|
+
url(): string;
|
|
149
|
+
identity?(): Promise<{ url: string; title: string }>;
|
|
150
|
+
content?(): Promise<string>;
|
|
151
|
+
evaluate<T>(expression: string | ((...args: unknown[]) => unknown), arg?: unknown): Promise<T>;
|
|
152
|
+
screenshot(options: Record<string, unknown>): Promise<Buffer>;
|
|
153
|
+
click(selector: string, options?: Record<string, unknown>): Promise<void>;
|
|
154
|
+
dblclick?(selector: string, options?: Record<string, unknown>): Promise<void>;
|
|
155
|
+
hover?(selector: string, options?: Record<string, unknown>): Promise<void>;
|
|
156
|
+
focus?(selector: string, options?: Record<string, unknown>): Promise<void>;
|
|
157
|
+
fill(selector: string, value: string, options?: Record<string, unknown>): Promise<void>;
|
|
158
|
+
type?(selector: string, text: string, options?: Record<string, unknown>): Promise<void>;
|
|
159
|
+
press(selector: string, key: string, options?: Record<string, unknown>): Promise<void>;
|
|
160
|
+
selectOption?(selector: string, values: string | string[], options?: Record<string, unknown>): Promise<unknown>;
|
|
161
|
+
check?(selector: string, options?: Record<string, unknown>): Promise<void>;
|
|
162
|
+
uncheck?(selector: string, options?: Record<string, unknown>): Promise<void>;
|
|
163
|
+
waitForSelector(selector: string, options?: Record<string, unknown>): Promise<unknown>;
|
|
164
|
+
waitForLoadState?(state?: string, options?: Record<string, unknown>): Promise<void>;
|
|
165
|
+
locator?(selector: string): { screenshot(options?: Record<string, unknown>): Promise<Buffer> };
|
|
166
|
+
on?(event: string, handler: (...args: unknown[]) => void): void;
|
|
167
|
+
bringToFront?(): Promise<void>;
|
|
168
|
+
keyboard?: { press(key: string): Promise<void> };
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
type RequestLike = { url(): string };
|
|
172
|
+
type RouteLike = {
|
|
173
|
+
request(): RequestLike;
|
|
174
|
+
continue(): Promise<void>;
|
|
175
|
+
abort(errorCode?: string): Promise<void>;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
type PlaywrightRuntime = {
|
|
179
|
+
chromium: {
|
|
180
|
+
launchPersistentContext(userDataDir: string, options: Record<string, unknown>): Promise<BrowserContextLike>;
|
|
181
|
+
connectOverCDP?(endpointURL: string, options?: Record<string, unknown>): Promise<BrowserLike>;
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
interface BrowserPluginRuntimeHooks {
|
|
186
|
+
now(): string;
|
|
187
|
+
moduleAvailable(name: string): boolean;
|
|
188
|
+
loadPlaywright(): PlaywrightRuntime;
|
|
189
|
+
fetchJson(url: string, timeoutMs: number): Promise<unknown>;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const defaultRuntimeHooks: BrowserPluginRuntimeHooks = {
|
|
193
|
+
now: () => new Date().toISOString(),
|
|
194
|
+
moduleAvailable: (name: string) => {
|
|
195
|
+
try {
|
|
196
|
+
createRequire(import.meta.url).resolve(name);
|
|
197
|
+
return true;
|
|
198
|
+
} catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
loadPlaywright: () => {
|
|
203
|
+
try {
|
|
204
|
+
return createRequire(import.meta.url)('playwright') as PlaywrightRuntime;
|
|
205
|
+
} catch {
|
|
206
|
+
throw new AssistantPluginError('PLUGIN_DEPENDENCY_MISSING', 'Browser plugin requires playwright. Run bun install before using browser actions.', {
|
|
207
|
+
retryable: false,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
fetchJson: async (url: string, timeoutMs: number) => {
|
|
212
|
+
const controller = new AbortController();
|
|
213
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
214
|
+
try {
|
|
215
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
216
|
+
if (!response.ok) throw new Error(`HTTP ${response.status} from ${url}`);
|
|
217
|
+
return await response.json();
|
|
218
|
+
} finally {
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
let runtimeHooks: BrowserPluginRuntimeHooks = { ...defaultRuntimeHooks };
|
|
225
|
+
let runtimeHooksCustomized = false;
|
|
226
|
+
|
|
227
|
+
export function setBrowserPluginRuntimeHooksForTest(hooks: Partial<BrowserPluginRuntimeHooks>): void {
|
|
228
|
+
runtimeHooks = { ...defaultRuntimeHooks, ...hooks };
|
|
229
|
+
runtimeHooksCustomized = true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function resetBrowserPluginRuntimeHooksForTest(): void {
|
|
233
|
+
runtimeHooks = { ...defaultRuntimeHooks };
|
|
234
|
+
runtimeHooksCustomized = false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function now(): string {
|
|
238
|
+
return runtimeHooks.now();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function stringValue(value: unknown): string | undefined {
|
|
242
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function positiveNumber(value: unknown, fallback: number): number {
|
|
246
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.trunc(value) : fallback;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function browserProfileMode(value: unknown): BrowserProfileMode | undefined {
|
|
250
|
+
return value === 'repo_local' || value === 'custom' ? value : undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function browserMode(value: unknown): BrowserMode | undefined {
|
|
254
|
+
return value === 'attach_preferred' || value === 'managed_persistent' || value === 'isolated' ? value : undefined;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function browserChannel(value: unknown): BrowserChannel | undefined {
|
|
258
|
+
return value === 'chromium'
|
|
259
|
+
|| value === 'chrome'
|
|
260
|
+
|| value === 'chrome-beta'
|
|
261
|
+
|| value === 'chrome-dev'
|
|
262
|
+
|| value === 'chrome-canary'
|
|
263
|
+
? value
|
|
264
|
+
: undefined;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function browserCdpAttachFallback(value: unknown): BrowserCdpAttachFallback | undefined {
|
|
268
|
+
return value === 'managed_persistent' || value === 'fail_closed' ? value : undefined;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function browserNativeAttachMode(value: unknown): BrowserNativeAttachMode | undefined {
|
|
272
|
+
return value === 'auto' || value === 'disabled' ? value : undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function browserProductList(value: unknown): MacOsBrowserProduct[] | undefined {
|
|
276
|
+
if (!Array.isArray(value)) return undefined;
|
|
277
|
+
const normalized = value
|
|
278
|
+
.map((entry) => String(entry).trim().toLowerCase())
|
|
279
|
+
.filter((entry): entry is MacOsBrowserProduct => entry === 'chrome' || entry === 'vivaldi');
|
|
280
|
+
return normalized.length > 0 ? Array.from(new Set(normalized)) : undefined;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function stringArray(value: unknown): string[] | undefined {
|
|
284
|
+
if (!Array.isArray(value)) return undefined;
|
|
285
|
+
const normalized = value
|
|
286
|
+
.map((entry) => String(entry).trim().toLowerCase())
|
|
287
|
+
.filter(Boolean);
|
|
288
|
+
return normalized.length > 0 ? Array.from(new Set(normalized)) : undefined;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function stringList(value: unknown): string[] | undefined {
|
|
292
|
+
if (!Array.isArray(value)) return undefined;
|
|
293
|
+
const normalized = value
|
|
294
|
+
.map((entry) => String(entry).trim())
|
|
295
|
+
.filter(Boolean);
|
|
296
|
+
return normalized.length > 0 ? Array.from(new Set(normalized)) : undefined;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function configPath(repoRoot: string): string {
|
|
300
|
+
return join(repoRoot, CONFIG_ROOT, 'browser.json');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function stateDir(repoRoot: string, name: 'sessions' | 'screenshots' | 'profiles' | 'downloads' | 'diagnostics'): string {
|
|
304
|
+
return join(repoRoot, STATE_ROOT, name);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function defaultProfileDir(repoRoot: string): string {
|
|
308
|
+
return join(stateDir(repoRoot, 'profiles'), 'default');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function isolatedProfileDir(repoRoot: string, sessionId: string): string {
|
|
312
|
+
return join(stateDir(repoRoot, 'profiles'), 'isolated', sessionId.replace(/[^a-zA-Z0-9_-]/g, '_'));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function resolveConfiguredPath(repoRoot: string, value: string): string {
|
|
316
|
+
return resolve(repoRoot, value);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function resolveProfileSelection(repoRoot: string, profileDir: string, profileDirectory?: string): BrowserProfileSelection {
|
|
320
|
+
const selectedProfilePath = resolveConfiguredPath(repoRoot, profileDir);
|
|
321
|
+
if (profileDirectory) {
|
|
322
|
+
return {
|
|
323
|
+
profileDir: selectedProfilePath,
|
|
324
|
+
profileDirectory,
|
|
325
|
+
selectedProfilePath: join(selectedProfilePath, profileDirectory),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const parent = dirname(selectedProfilePath);
|
|
330
|
+
if (existsSync(join(selectedProfilePath, 'Preferences')) && existsSync(join(parent, 'Local State'))) {
|
|
331
|
+
return {
|
|
332
|
+
profileDir: parent,
|
|
333
|
+
profileDirectory: basename(selectedProfilePath),
|
|
334
|
+
selectedProfilePath,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
profileDir: selectedProfilePath,
|
|
340
|
+
selectedProfilePath,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function normalizeConfig(raw: Partial<BrowserPluginConfig>): BrowserPluginConfig {
|
|
345
|
+
const normalizedProfileDir = stringValue(raw.profileDir);
|
|
346
|
+
return {
|
|
347
|
+
schemaVersion: 1,
|
|
348
|
+
enabled: raw.enabled === true,
|
|
349
|
+
provider: 'playwright',
|
|
350
|
+
browserMode: browserMode(raw.browserMode) ?? 'managed_persistent',
|
|
351
|
+
profileMode: browserProfileMode(raw.profileMode) ?? (normalizedProfileDir ? 'custom' : 'repo_local'),
|
|
352
|
+
profileDir: normalizedProfileDir,
|
|
353
|
+
profileDirectory: stringValue(raw.profileDirectory),
|
|
354
|
+
browserChannel: browserChannel(raw.browserChannel) ?? 'chromium',
|
|
355
|
+
executablePath: stringValue(raw.executablePath),
|
|
356
|
+
cdpEndpoint: stringValue(raw.cdpEndpoint),
|
|
357
|
+
cdpEndpointCandidates: stringList(raw.cdpEndpointCandidates)?.slice(0, MAX_CDP_ENDPOINT_CANDIDATES),
|
|
358
|
+
cdpDiscoveryTimeoutMs: typeof raw.cdpDiscoveryTimeoutMs === 'number'
|
|
359
|
+
? Math.min(positiveNumber(raw.cdpDiscoveryTimeoutMs, DEFAULT_CDP_DISCOVERY_TIMEOUT_MS), MAX_CDP_DISCOVERY_TIMEOUT_MS)
|
|
360
|
+
: undefined,
|
|
361
|
+
cdpAttachFallback: browserCdpAttachFallback(raw.cdpAttachFallback) ?? 'fail_closed',
|
|
362
|
+
nativeAttachMode: browserNativeAttachMode(raw.nativeAttachMode) ?? 'auto',
|
|
363
|
+
nativeBrowserCandidates: browserProductList(raw.nativeBrowserCandidates) ?? ['vivaldi', 'chrome'],
|
|
364
|
+
defaultTimeoutMs: typeof raw.defaultTimeoutMs === 'number' ? positiveNumber(raw.defaultTimeoutMs, DEFAULT_TIMEOUT_MS) : undefined,
|
|
365
|
+
allowedDomains: stringArray(raw.allowedDomains),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function readJson<T>(path: string): T | undefined {
|
|
370
|
+
try {
|
|
371
|
+
return JSON.parse(readFileSync(path, 'utf-8')) as T;
|
|
372
|
+
} catch {
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function writeJson(path: string, value: unknown): void {
|
|
378
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
379
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, 'utf-8');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function loadConfig(repoRoot: string): BrowserPluginConfig {
|
|
383
|
+
return normalizeConfig(readJson<Partial<BrowserPluginConfig>>(configPath(repoRoot)) ?? {});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function saveConfig(repoRoot: string, patch: Partial<BrowserPluginConfig>): BrowserPluginConfig {
|
|
387
|
+
const next = normalizeConfig({ ...loadConfig(repoRoot), ...patch });
|
|
388
|
+
writeJson(configPath(repoRoot), next);
|
|
389
|
+
return next;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function sessionPath(repoRoot: string, sessionId: string): string {
|
|
393
|
+
return join(stateDir(repoRoot, 'sessions'), `${sessionId.replace(/[^a-zA-Z0-9_-]/g, '_')}.json`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function saveSession(repoRoot: string, session: BrowserSessionState): void {
|
|
397
|
+
writeJson(sessionPath(repoRoot, session.sessionId), session);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function findSession(repoRoot: string, sessionId: string): BrowserSessionState | undefined {
|
|
401
|
+
return readJson<BrowserSessionState>(sessionPath(repoRoot, sessionId));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function loadSession(repoRoot: string, sessionId: string): BrowserSessionState {
|
|
405
|
+
const state = findSession(repoRoot, sessionId);
|
|
406
|
+
if (!state) {
|
|
407
|
+
throw new AssistantPluginError('PLUGIN_SESSION_NOT_FOUND', `Browser session not found: ${sessionId}`, { retryable: false });
|
|
408
|
+
}
|
|
409
|
+
return state;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function normalizedUrl(value: unknown): string {
|
|
413
|
+
const raw = stringValue(value);
|
|
414
|
+
if (!raw) throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'url is required.', { retryable: false });
|
|
415
|
+
let parsed: URL;
|
|
416
|
+
try {
|
|
417
|
+
parsed = new URL(raw);
|
|
418
|
+
} catch {
|
|
419
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'url must be absolute.', { retryable: false });
|
|
420
|
+
}
|
|
421
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
422
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Only http and https URLs are supported.', { retryable: false });
|
|
423
|
+
}
|
|
424
|
+
return parsed.toString();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function isRemoteHttpUrl(url: string): boolean {
|
|
428
|
+
try {
|
|
429
|
+
const parsed = new URL(url);
|
|
430
|
+
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
431
|
+
} catch {
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function isUrlAllowed(url: string, config: BrowserPluginConfig): boolean {
|
|
437
|
+
if (!config.allowedDomains || config.allowedDomains.length === 0) return true;
|
|
438
|
+
if (!isRemoteHttpUrl(url)) return true;
|
|
439
|
+
const hostname = new URL(url).hostname.toLowerCase();
|
|
440
|
+
return config.allowedDomains.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function assertUrlAllowed(url: string, config: BrowserPluginConfig): void {
|
|
444
|
+
if (isUrlAllowed(url, config)) return;
|
|
445
|
+
const hostname = new URL(url).hostname.toLowerCase();
|
|
446
|
+
throw new AssistantPluginError('PLUGIN_POLICY_BLOCKED', `Domain is not allowed: ${hostname}`, { retryable: false });
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function waitUntil(value: unknown): WaitUntil {
|
|
450
|
+
return value === 'load' || value === 'networkidle' || value === 'domcontentloaded' ? value : 'domcontentloaded';
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function waitForSelectorState(value: unknown): WaitForSelectorState {
|
|
454
|
+
return value === 'attached' || value === 'detached' || value === 'hidden' || value === 'visible' ? value : 'visible';
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function parseProfileModeInput(value: unknown): BrowserProfileMode | undefined {
|
|
458
|
+
if (value === undefined) return undefined;
|
|
459
|
+
const parsed = browserProfileMode(value);
|
|
460
|
+
if (parsed) return parsed;
|
|
461
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'profile_mode must be repo_local or custom.', { retryable: false });
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function parseBrowserModeInput(value: unknown): BrowserMode | undefined {
|
|
465
|
+
if (value === undefined) return undefined;
|
|
466
|
+
const parsed = browserMode(value);
|
|
467
|
+
if (parsed) return parsed;
|
|
468
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'browser_mode must be attach_preferred, managed_persistent, or isolated.', { retryable: false });
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function parseBrowserChannelInput(value: unknown): BrowserChannel | undefined {
|
|
472
|
+
if (value === undefined) return undefined;
|
|
473
|
+
const parsed = browserChannel(value);
|
|
474
|
+
if (parsed) return parsed;
|
|
475
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'browser_channel must be chromium, chrome, chrome-beta, chrome-dev, or chrome-canary.', { retryable: false });
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function parseCdpAttachFallbackInput(value: unknown): BrowserCdpAttachFallback | undefined {
|
|
479
|
+
if (value === undefined) return undefined;
|
|
480
|
+
const parsed = browserCdpAttachFallback(value);
|
|
481
|
+
if (parsed) return parsed;
|
|
482
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'cdp_attach_fallback must be managed_persistent or fail_closed.', { retryable: false });
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function parseNativeAttachModeInput(value: unknown): BrowserNativeAttachMode | undefined {
|
|
486
|
+
if (value === undefined) return undefined;
|
|
487
|
+
const parsed = browserNativeAttachMode(value);
|
|
488
|
+
if (parsed) return parsed;
|
|
489
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'native_attach_mode must be auto or disabled.', { retryable: false });
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function parseNativeBrowserCandidatesInput(value: unknown): MacOsBrowserProduct[] | undefined {
|
|
493
|
+
if (value === undefined) return undefined;
|
|
494
|
+
const parsed = browserProductList(value);
|
|
495
|
+
if (parsed) return parsed;
|
|
496
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'native_browser_candidates must contain chrome and/or vivaldi.', { retryable: false });
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function cdpEndpoints(config: BrowserPluginConfig): string[] {
|
|
500
|
+
return Array.from(new Set([
|
|
501
|
+
...(config.cdpEndpoint ? [config.cdpEndpoint] : []),
|
|
502
|
+
...(config.cdpEndpointCandidates ?? []),
|
|
503
|
+
].map((entry) => entry.trim()).filter(Boolean))).slice(0, MAX_CDP_ENDPOINT_CANDIDATES);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function isLoopbackHostname(hostname: string): boolean {
|
|
507
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname === '[::1]';
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function cdpEndpointValidationError(endpoint: string): string | undefined {
|
|
511
|
+
let parsed: URL;
|
|
512
|
+
try {
|
|
513
|
+
parsed = new URL(endpoint);
|
|
514
|
+
} catch {
|
|
515
|
+
return `Invalid CDP endpoint URL: ${endpoint}`;
|
|
516
|
+
}
|
|
517
|
+
if (!['http:', 'https:', 'ws:', 'wss:'].includes(parsed.protocol)) {
|
|
518
|
+
return `CDP endpoint must use http, https, ws, or wss: ${endpoint}`;
|
|
519
|
+
}
|
|
520
|
+
if (parsed.username || parsed.password) {
|
|
521
|
+
return `CDP endpoint must not contain credentials: ${endpoint}`;
|
|
522
|
+
}
|
|
523
|
+
if (!isLoopbackHostname(parsed.hostname)) {
|
|
524
|
+
return `CDP endpoint must be loopback-only: ${endpoint}`;
|
|
525
|
+
}
|
|
526
|
+
return undefined;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function validateConfig(config: BrowserPluginConfig): string[] {
|
|
530
|
+
const errors: string[] = [];
|
|
531
|
+
const endpoints = cdpEndpoints(config);
|
|
532
|
+
if (config.profileMode === 'custom' && !config.profileDir) {
|
|
533
|
+
errors.push('profileDir is required when profileMode is custom.');
|
|
534
|
+
}
|
|
535
|
+
if (config.profileMode !== 'custom' && config.profileDirectory) {
|
|
536
|
+
errors.push('profileDirectory requires profileMode=custom.');
|
|
537
|
+
}
|
|
538
|
+
if (config.browserChannel && config.browserChannel !== 'chromium' && config.executablePath) {
|
|
539
|
+
errors.push('browserChannel and executablePath cannot both be set.');
|
|
540
|
+
}
|
|
541
|
+
if (endpoints.length > MAX_CDP_ENDPOINT_CANDIDATES) {
|
|
542
|
+
errors.push(`At most ${MAX_CDP_ENDPOINT_CANDIDATES} CDP endpoint candidates are supported.`);
|
|
543
|
+
}
|
|
544
|
+
for (const endpoint of endpoints) {
|
|
545
|
+
const error = cdpEndpointValidationError(endpoint);
|
|
546
|
+
if (error) errors.push(error);
|
|
547
|
+
}
|
|
548
|
+
if (config.browserMode === 'attach_preferred'
|
|
549
|
+
&& endpoints.length === 0
|
|
550
|
+
&& config.cdpAttachFallback === 'fail_closed'
|
|
551
|
+
&& config.nativeAttachMode === 'disabled') {
|
|
552
|
+
errors.push('A CDP endpoint or nativeAttachMode=auto is required when browserMode=attach_preferred and cdpAttachFallback=fail_closed.');
|
|
553
|
+
}
|
|
554
|
+
return errors;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function configWarnings(config: BrowserPluginConfig): string[] {
|
|
558
|
+
const warnings: string[] = [];
|
|
559
|
+
if (!config.allowedDomains || config.allowedDomains.length === 0) {
|
|
560
|
+
warnings.push('allowedDomains is empty; browser actions can target any domain.');
|
|
561
|
+
}
|
|
562
|
+
if (config.profileMode === 'custom') {
|
|
563
|
+
warnings.push('Custom profile mode uses the configured browser profile directly. If the browser reports the profile is in use, fully close the matching browser instance first.');
|
|
564
|
+
if (!config.executablePath && (config.browserChannel ?? 'chromium') === 'chromium') {
|
|
565
|
+
warnings.push('Custom profile mode is more reliable with an explicit Chrome channel or executable path that matches the selected profile format.');
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (config.browserMode === 'attach_preferred' && cdpEndpoints(config).length === 0) {
|
|
569
|
+
if (config.nativeAttachMode === 'auto' && macOsActiveBrowserAttachSupported()) {
|
|
570
|
+
warnings.push('browserMode=attach_preferred has no configured CDP endpoint; macOS active-browser attach will be attempted before managed fallback.');
|
|
571
|
+
} else {
|
|
572
|
+
warnings.push('browserMode=attach_preferred has no configured CDP endpoint; actions will use the configured fallback policy.');
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (config.browserMode === 'isolated') {
|
|
576
|
+
warnings.push('browserMode=isolated uses a per-session repo-local profile and will not share the default browser profile.');
|
|
577
|
+
}
|
|
578
|
+
return warnings;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function truncateText(value: string, maxChars: number): Record<string, unknown> {
|
|
582
|
+
const clean = value.replace(/[\t\r ]+/g, ' ').replace(/\n{3,}/g, '\n\n').trim();
|
|
583
|
+
return {
|
|
584
|
+
text: clean.slice(0, maxChars),
|
|
585
|
+
truncated: clean.length > maxChars,
|
|
586
|
+
charCount: clean.length,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function sessionIdFor(url: string): string {
|
|
591
|
+
const digest = createHash('sha256')
|
|
592
|
+
.update(`${Date.now()}:${randomUUID()}:${url}`)
|
|
593
|
+
.digest('hex')
|
|
594
|
+
.slice(0, 16);
|
|
595
|
+
return `browser_${digest}`;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function requiredString(value: unknown, field: string): string {
|
|
599
|
+
const normalized = stringValue(value);
|
|
600
|
+
if (!normalized) {
|
|
601
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', `${field} is required.`, { retryable: false });
|
|
602
|
+
}
|
|
603
|
+
return normalized;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function scriptText(selector?: string): string {
|
|
607
|
+
if (!selector) {
|
|
608
|
+
return 'document.body ? document.body.innerText : (document.documentElement ? document.documentElement.textContent : "")';
|
|
609
|
+
}
|
|
610
|
+
return `(() => { const el = document.querySelector(${JSON.stringify(selector)}); return el ? (el.innerText || el.textContent || '') : ''; })()`;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function resolveActionTarget(repoRoot: string, args: Record<string, unknown>): BrowserActionTarget {
|
|
614
|
+
const directUrl = stringValue(args.url);
|
|
615
|
+
const providedSessionId = stringValue(args.session_id);
|
|
616
|
+
if (!directUrl && !providedSessionId) {
|
|
617
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Provide either url or session_id.', { retryable: false });
|
|
618
|
+
}
|
|
619
|
+
if (providedSessionId) {
|
|
620
|
+
const existingSession = loadSession(repoRoot, providedSessionId);
|
|
621
|
+
const sessionUrl = normalizedUrl(existingSession.url);
|
|
622
|
+
if (directUrl) {
|
|
623
|
+
const explicitUrl = normalizedUrl(directUrl);
|
|
624
|
+
if (explicitUrl !== sessionUrl) {
|
|
625
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'url does not match the saved session.', { retryable: false });
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return { sessionId: providedSessionId, url: sessionUrl, existingSession };
|
|
629
|
+
}
|
|
630
|
+
const url = normalizedUrl(directUrl);
|
|
631
|
+
return { sessionId: sessionIdFor(url), url };
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function sessionFromPage(target: BrowserActionTarget, pageUrl: string, title: string, connection?: BrowserConnectionSummary): BrowserSessionState {
|
|
635
|
+
const refreshedConnection = connection ? refreshConnectionTab(connection, pageUrl, title) : undefined;
|
|
636
|
+
return {
|
|
637
|
+
schemaVersion: 1,
|
|
638
|
+
sessionId: target.sessionId,
|
|
639
|
+
url: pageUrl,
|
|
640
|
+
title,
|
|
641
|
+
createdAt: target.existingSession?.createdAt ?? now(),
|
|
642
|
+
updatedAt: now(),
|
|
643
|
+
browser: refreshedConnection ? sessionConnectionFromSummary(refreshedConnection) : target.existingSession?.browser,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function screenshotFilePath(repoRoot: string, actionId: string, sessionId: string, url: string): string {
|
|
648
|
+
const screenshotDir = stateDir(repoRoot, 'screenshots');
|
|
649
|
+
mkdirSync(screenshotDir, { recursive: true });
|
|
650
|
+
const digest = createHash('sha256').update(url).digest('hex').slice(0, 10);
|
|
651
|
+
const actionLabel = actionId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
652
|
+
const sessionLabel = sessionId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
653
|
+
return join(screenshotDir, `${Date.now()}-${actionLabel}-${sessionLabel}-${digest}.png`);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
async function captureActionScreenshot(page: PageLike, repoRoot: string, actionId: string, sessionId: string, url: string): Promise<BrowserActionScreenshot | undefined> {
|
|
657
|
+
const path = screenshotFilePath(repoRoot, actionId, sessionId, url);
|
|
658
|
+
const bytes = (await page.screenshot({ path, fullPage: true })).length;
|
|
659
|
+
return { path, relativePath: relative(repoRoot, path), bytes };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function responseWithWarnings(base: Record<string, unknown>, warnings: string[]): Record<string, unknown> {
|
|
663
|
+
return warnings.length > 0 ? { ...base, warnings } : base;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function interactionResult(
|
|
667
|
+
actionId: string,
|
|
668
|
+
session: BrowserSessionState,
|
|
669
|
+
summary: string,
|
|
670
|
+
screenshot: BrowserActionScreenshot | undefined,
|
|
671
|
+
warnings: string[],
|
|
672
|
+
extra: Record<string, unknown> = {},
|
|
673
|
+
): Record<string, unknown> {
|
|
674
|
+
return responseWithWarnings({
|
|
675
|
+
provider: 'playwright',
|
|
676
|
+
session,
|
|
677
|
+
url: session.url,
|
|
678
|
+
title: session.title,
|
|
679
|
+
action: {
|
|
680
|
+
actionId,
|
|
681
|
+
summary,
|
|
682
|
+
...extra,
|
|
683
|
+
},
|
|
684
|
+
...(screenshot ? { screenshot } : {}),
|
|
685
|
+
}, warnings);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
async function delay(ms: number): Promise<void> {
|
|
689
|
+
if (ms <= 0) return;
|
|
690
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
async function applyDomainGuard(context: BrowserContextLike, config: BrowserPluginConfig): Promise<void> {
|
|
694
|
+
if (!config.allowedDomains || config.allowedDomains.length === 0) return;
|
|
695
|
+
await context.route('**/*', async (route) => {
|
|
696
|
+
const url = route.request().url();
|
|
697
|
+
if (isUrlAllowed(url, config)) {
|
|
698
|
+
await route.continue();
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
await route.abort('blockedbyclient');
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function selectedProfile(config: BrowserPluginConfig, repoRoot: string, activeMode: BrowserMode = 'managed_persistent', sessionId?: string): BrowserProfileSelection {
|
|
706
|
+
if (activeMode === 'isolated') {
|
|
707
|
+
const profileDir = isolatedProfileDir(repoRoot, sessionId ?? 'anonymous');
|
|
708
|
+
return {
|
|
709
|
+
profileDir,
|
|
710
|
+
selectedProfilePath: profileDir,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (config.profileMode === 'custom') {
|
|
715
|
+
if (!config.profileDir) {
|
|
716
|
+
throw new AssistantPluginError('PLUGIN_CONFIGURATION_INVALID', 'Custom browser profile mode requires profileDir.', { retryable: false });
|
|
717
|
+
}
|
|
718
|
+
return resolveProfileSelection(repoRoot, config.profileDir, config.profileDirectory);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const repoLocal = resolve(defaultProfileDir(repoRoot));
|
|
722
|
+
return {
|
|
723
|
+
profileDir: repoLocal,
|
|
724
|
+
selectedProfilePath: repoLocal,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
type BrowserTabMatchReason = 'owned_token' | 'saved_url_title' | 'saved_url' | 'exact_url' | 'blank' | 'new_page';
|
|
729
|
+
|
|
730
|
+
const BROWSER_OWNER_PREFIX = 'forge-browser-owned:';
|
|
731
|
+
|
|
732
|
+
interface BrowserTabInventoryEntry {
|
|
733
|
+
index: number;
|
|
734
|
+
key: string;
|
|
735
|
+
url: string;
|
|
736
|
+
title?: string;
|
|
737
|
+
ownerToken?: string;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function ownerTokenForSession(sessionId: string): string {
|
|
741
|
+
return `${BROWSER_OWNER_PREFIX}${createHash('sha256').update(sessionId).digest('hex').slice(0, 24)}`;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
interface BrowserSessionResumeDiagnostic {
|
|
745
|
+
sessionId: string;
|
|
746
|
+
status: 'matched' | 'stale_tab' | 'no_saved_tab';
|
|
747
|
+
reason: string;
|
|
748
|
+
savedTab?: Pick<BrowserTabResumeState, 'key' | 'url' | 'title' | 'index'>;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
interface BrowserConnectionFallback {
|
|
752
|
+
policy: BrowserCdpAttachFallback;
|
|
753
|
+
from: 'attach_preferred';
|
|
754
|
+
to: 'managed_persistent';
|
|
755
|
+
reason: string;
|
|
756
|
+
attempts: CdpAttachAttempt[];
|
|
757
|
+
nativeAttempts?: MacOsBrowserAttachAttempt[];
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
interface CdpAttachAttempt {
|
|
761
|
+
endpoint: string;
|
|
762
|
+
discoveredEndpoint?: string;
|
|
763
|
+
probeUrl?: string;
|
|
764
|
+
browserVersion?: string;
|
|
765
|
+
error?: string;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
interface BrowserConnectionSummary {
|
|
769
|
+
requestedMode: BrowserMode;
|
|
770
|
+
mode: BrowserMode;
|
|
771
|
+
provider: 'playwright-cdp' | 'playwright-persistent-context' | 'macos-apple-events';
|
|
772
|
+
attached: boolean;
|
|
773
|
+
endpoint?: string;
|
|
774
|
+
browserVersion?: string;
|
|
775
|
+
browserProduct?: MacOsBrowserProduct;
|
|
776
|
+
fallback?: BrowserConnectionFallback;
|
|
777
|
+
profile: {
|
|
778
|
+
profileMode?: BrowserProfileMode;
|
|
779
|
+
profileDirectory?: string;
|
|
780
|
+
selectedProfilePath: string;
|
|
781
|
+
};
|
|
782
|
+
tab?: BrowserTabResumeState;
|
|
783
|
+
tabInventory?: BrowserTabInventoryEntry[];
|
|
784
|
+
sessionResume?: BrowserSessionResumeDiagnostic;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
interface BrowserOpenHandle {
|
|
788
|
+
page: PageLike;
|
|
789
|
+
diagnostics: PageDiagnostics;
|
|
790
|
+
connection: BrowserConnectionSummary;
|
|
791
|
+
close(): Promise<void>;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
interface PageDiagnostics {
|
|
795
|
+
consoleErrors: Array<{ type: string; text: string }>;
|
|
796
|
+
failedRequests: Array<{ url: string; status?: number; failure?: string }>;
|
|
797
|
+
navigation?: { url: string; status?: number };
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function attachDiagnostics(page: PageLike): PageDiagnostics {
|
|
801
|
+
const diagnostics: PageDiagnostics = { consoleErrors: [], failedRequests: [] };
|
|
802
|
+
if (typeof page.on === 'function') {
|
|
803
|
+
page.on('console', (message) => {
|
|
804
|
+
const entry = message as { type?: () => string; text?: () => string };
|
|
805
|
+
const type = typeof entry.type === 'function' ? entry.type() : 'log';
|
|
806
|
+
if (type === 'error' || type === 'warning') {
|
|
807
|
+
diagnostics.consoleErrors.push({
|
|
808
|
+
type,
|
|
809
|
+
text: typeof entry.text === 'function' ? String(entry.text()).slice(0, 500) : '',
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
page.on('requestfailed', (request) => {
|
|
814
|
+
const entry = request as { url?: () => string; failure?: () => { errorText?: string } | null };
|
|
815
|
+
diagnostics.failedRequests.push({
|
|
816
|
+
url: typeof entry.url === 'function' ? entry.url() : '',
|
|
817
|
+
failure: typeof entry.failure === 'function' ? entry.failure()?.errorText : undefined,
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
page.on('response', (response) => {
|
|
821
|
+
const entry = response as { url?: () => string; status?: () => number; ok?: () => boolean };
|
|
822
|
+
const status = typeof entry.status === 'function' ? entry.status() : undefined;
|
|
823
|
+
if (typeof status === 'number' && status >= 400) {
|
|
824
|
+
diagnostics.failedRequests.push({
|
|
825
|
+
url: typeof entry.url === 'function' ? entry.url() : '',
|
|
826
|
+
status,
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
return diagnostics;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function tabKey(url: string, title?: string): string {
|
|
835
|
+
return createHash('sha256').update(`${url}\n${title ?? ''}`).digest('hex').slice(0, 16);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function comparableUrl(value: string): string {
|
|
839
|
+
try {
|
|
840
|
+
return new URL(value).toString();
|
|
841
|
+
} catch {
|
|
842
|
+
return value;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function isBlankPage(url: string): boolean {
|
|
847
|
+
return !url || url === 'about:blank' || url === 'chrome://newtab/';
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async function inventoryTabs(context: BrowserContextLike): Promise<BrowserTabInventoryEntry[]> {
|
|
851
|
+
const entries: BrowserTabInventoryEntry[] = [];
|
|
852
|
+
const pages = context.pages();
|
|
853
|
+
for (let index = 0; index < pages.length; index += 1) {
|
|
854
|
+
const page = pages[index];
|
|
855
|
+
if (!page) continue;
|
|
856
|
+
const url = page.url();
|
|
857
|
+
let title: string | undefined;
|
|
858
|
+
let ownerToken: string | undefined;
|
|
859
|
+
try {
|
|
860
|
+
title = await page.title();
|
|
861
|
+
} catch {
|
|
862
|
+
title = undefined;
|
|
863
|
+
}
|
|
864
|
+
try {
|
|
865
|
+
const candidate = await page.evaluate<string>('typeof window !== "undefined" ? String(window.name || "") : ""');
|
|
866
|
+
ownerToken = typeof candidate === 'string' && candidate.startsWith(BROWSER_OWNER_PREFIX) ? candidate : undefined;
|
|
867
|
+
} catch {
|
|
868
|
+
ownerToken = undefined;
|
|
869
|
+
}
|
|
870
|
+
entries.push({ index, key: tabKey(url, title), url, title, ownerToken });
|
|
871
|
+
}
|
|
872
|
+
return entries;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function chooseTab(
|
|
876
|
+
inventory: BrowserTabInventoryEntry[],
|
|
877
|
+
target: BrowserActionTarget,
|
|
878
|
+
options: { allowBlank?: boolean; requireOwnedToken?: boolean } = {},
|
|
879
|
+
): { entry?: BrowserTabInventoryEntry; matchedBy: BrowserTabMatchReason; sessionResume?: BrowserSessionResumeDiagnostic } {
|
|
880
|
+
const desiredUrl = comparableUrl(target.url);
|
|
881
|
+
const savedTab = target.existingSession?.browser?.tab;
|
|
882
|
+
const expectedOwnerToken = savedTab?.ownership === 'plugin_owned' ? savedTab.ownerToken : undefined;
|
|
883
|
+
if (options.requireOwnedToken) {
|
|
884
|
+
const owned = expectedOwnerToken ? inventory.find((entry) => entry.ownerToken === expectedOwnerToken) : undefined;
|
|
885
|
+
if (owned) {
|
|
886
|
+
return {
|
|
887
|
+
entry: owned,
|
|
888
|
+
matchedBy: 'owned_token',
|
|
889
|
+
sessionResume: { sessionId: target.sessionId, status: 'matched', reason: 'Saved plugin-owned tab marker matched an attached browser tab.', savedTab },
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
return {
|
|
893
|
+
matchedBy: 'new_page',
|
|
894
|
+
sessionResume: savedTab
|
|
895
|
+
? { sessionId: target.sessionId, status: 'stale_tab', reason: 'Saved plugin-owned tab marker was not found; refusing to use a user-owned tab.', savedTab }
|
|
896
|
+
: { sessionId: target.sessionId, status: 'no_saved_tab', reason: 'No plugin-owned tab exists in the attached browser.' },
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
const savedUrl = savedTab?.url ? comparableUrl(savedTab.url) : undefined;
|
|
900
|
+
const savedTitle = savedTab?.title;
|
|
901
|
+
const savedUrlTitle = savedUrl && savedTitle
|
|
902
|
+
? inventory.find((entry) => comparableUrl(entry.url) === savedUrl && entry.title === savedTitle)
|
|
903
|
+
: undefined;
|
|
904
|
+
if (savedUrlTitle) {
|
|
905
|
+
return {
|
|
906
|
+
entry: savedUrlTitle,
|
|
907
|
+
matchedBy: 'saved_url_title',
|
|
908
|
+
sessionResume: {
|
|
909
|
+
sessionId: target.sessionId,
|
|
910
|
+
status: 'matched',
|
|
911
|
+
reason: 'Saved tab URL and title matched an attached browser tab.',
|
|
912
|
+
savedTab,
|
|
913
|
+
},
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const savedUrlOnly = savedUrl
|
|
918
|
+
? inventory.find((entry) => comparableUrl(entry.url) === savedUrl)
|
|
919
|
+
: undefined;
|
|
920
|
+
if (savedUrlOnly) {
|
|
921
|
+
return {
|
|
922
|
+
entry: savedUrlOnly,
|
|
923
|
+
matchedBy: 'saved_url',
|
|
924
|
+
sessionResume: {
|
|
925
|
+
sessionId: target.sessionId,
|
|
926
|
+
status: savedTab ? 'matched' : 'no_saved_tab',
|
|
927
|
+
reason: savedTab ? 'Saved tab URL matched but the previous title was not present.' : 'No saved tab metadata was available.',
|
|
928
|
+
savedTab,
|
|
929
|
+
},
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const exactUrl = inventory.find((entry) => comparableUrl(entry.url) === desiredUrl);
|
|
934
|
+
if (exactUrl) {
|
|
935
|
+
return {
|
|
936
|
+
entry: exactUrl,
|
|
937
|
+
matchedBy: 'exact_url',
|
|
938
|
+
sessionResume: savedTab
|
|
939
|
+
? {
|
|
940
|
+
sessionId: target.sessionId,
|
|
941
|
+
status: 'stale_tab',
|
|
942
|
+
reason: 'Saved tab was not found; reused another tab with the target URL.',
|
|
943
|
+
savedTab,
|
|
944
|
+
}
|
|
945
|
+
: {
|
|
946
|
+
sessionId: target.sessionId,
|
|
947
|
+
status: 'no_saved_tab',
|
|
948
|
+
reason: 'No saved tab metadata was available; reused an existing tab with the target URL.',
|
|
949
|
+
},
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const blank = options.allowBlank === false ? undefined : inventory.find((entry) => isBlankPage(entry.url));
|
|
954
|
+
if (blank) {
|
|
955
|
+
return {
|
|
956
|
+
entry: blank,
|
|
957
|
+
matchedBy: 'blank',
|
|
958
|
+
sessionResume: savedTab
|
|
959
|
+
? {
|
|
960
|
+
sessionId: target.sessionId,
|
|
961
|
+
status: 'stale_tab',
|
|
962
|
+
reason: 'Saved tab was not found; reused a blank tab for navigation.',
|
|
963
|
+
savedTab,
|
|
964
|
+
}
|
|
965
|
+
: {
|
|
966
|
+
sessionId: target.sessionId,
|
|
967
|
+
status: 'no_saved_tab',
|
|
968
|
+
reason: 'No saved tab metadata was available; reused a blank tab for navigation.',
|
|
969
|
+
},
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return {
|
|
974
|
+
matchedBy: 'new_page',
|
|
975
|
+
sessionResume: savedTab
|
|
976
|
+
? {
|
|
977
|
+
sessionId: target.sessionId,
|
|
978
|
+
status: 'stale_tab',
|
|
979
|
+
reason: 'Saved tab was not found; opened a new tab for navigation.',
|
|
980
|
+
savedTab,
|
|
981
|
+
}
|
|
982
|
+
: {
|
|
983
|
+
sessionId: target.sessionId,
|
|
984
|
+
status: 'no_saved_tab',
|
|
985
|
+
reason: 'No saved tab metadata was available; opened a new tab for navigation.',
|
|
986
|
+
},
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
async function selectPage(
|
|
991
|
+
context: BrowserContextLike,
|
|
992
|
+
target: BrowserActionTarget,
|
|
993
|
+
options: { allowBlank?: boolean; allowNewPage?: boolean; bringToFront?: boolean; requireOwnedToken?: boolean } = {},
|
|
994
|
+
): Promise<{ page: PageLike; matchedBy: BrowserTabMatchReason; inventory: BrowserTabInventoryEntry[]; selectedEntry?: BrowserTabInventoryEntry; sessionResume?: BrowserSessionResumeDiagnostic }> {
|
|
995
|
+
const inventory = await inventoryTabs(context);
|
|
996
|
+
const selection = chooseTab(inventory, target, { allowBlank: options.allowBlank, requireOwnedToken: options.requireOwnedToken });
|
|
997
|
+
if (selection.entry) {
|
|
998
|
+
const page = context.pages()[selection.entry.index];
|
|
999
|
+
if (!page) throw new Error(`Browser tab inventory selected missing page index ${selection.entry.index}`);
|
|
1000
|
+
if (options.bringToFront === true && page.bringToFront) await page.bringToFront().catch(() => undefined);
|
|
1001
|
+
return { page, matchedBy: selection.matchedBy, inventory, selectedEntry: selection.entry, sessionResume: selection.sessionResume };
|
|
1002
|
+
}
|
|
1003
|
+
if (options.allowNewPage === false) {
|
|
1004
|
+
throw new AssistantPluginError(
|
|
1005
|
+
'PLUGIN_BROWSER_ATTACHED_TAB_NOT_FOUND',
|
|
1006
|
+
'Attached browser had no saved or exact target tab; refusing to reuse a blank tab or open a new tab in the user browser.',
|
|
1007
|
+
{ retryable: true, details: { sessionId: target.sessionId, targetUrl: target.url, tabCount: inventory.length } },
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
const page = await context.newPage();
|
|
1011
|
+
return { page, matchedBy: 'new_page', inventory, sessionResume: selection.sessionResume };
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function sessionConnectionFromSummary(connection: BrowserConnectionSummary): BrowserSessionConnectionState {
|
|
1015
|
+
return {
|
|
1016
|
+
mode: connection.requestedMode,
|
|
1017
|
+
activeMode: connection.mode,
|
|
1018
|
+
provider: connection.provider,
|
|
1019
|
+
endpoint: connection.endpoint,
|
|
1020
|
+
browserVersion: connection.browserVersion,
|
|
1021
|
+
browserProduct: connection.browserProduct,
|
|
1022
|
+
fallback: connection.fallback,
|
|
1023
|
+
tab: connection.tab,
|
|
1024
|
+
sessionResume: connection.sessionResume,
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function refreshConnectionTab(
|
|
1029
|
+
connection: BrowserConnectionSummary,
|
|
1030
|
+
pageUrl: string,
|
|
1031
|
+
title: string,
|
|
1032
|
+
matchedBy: BrowserTabMatchReason = connection.tab?.matchedBy ?? 'exact_url',
|
|
1033
|
+
ownership?: Pick<BrowserTabResumeState, 'ownership' | 'ownerToken' | 'windowId' | 'tabId'>,
|
|
1034
|
+
): BrowserConnectionSummary {
|
|
1035
|
+
const index = connection.tab?.index ?? connection.tabInventory?.find((entry) => comparableUrl(entry.url) === comparableUrl(pageUrl) && entry.title === title)?.index ?? 0;
|
|
1036
|
+
return {
|
|
1037
|
+
...connection,
|
|
1038
|
+
tab: {
|
|
1039
|
+
key: tabKey(pageUrl, title),
|
|
1040
|
+
index,
|
|
1041
|
+
url: pageUrl,
|
|
1042
|
+
title,
|
|
1043
|
+
matchedBy,
|
|
1044
|
+
inventoryCount: connection.tabInventory?.length ?? 1,
|
|
1045
|
+
capturedAt: now(),
|
|
1046
|
+
ownership: ownership?.ownership ?? connection.tab?.ownership,
|
|
1047
|
+
ownerToken: ownership?.ownerToken ?? connection.tab?.ownerToken,
|
|
1048
|
+
windowId: ownership?.windowId ?? connection.tab?.windowId,
|
|
1049
|
+
tabId: ownership?.tabId ?? connection.tab?.tabId,
|
|
1050
|
+
},
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function cdpDiscoveryTimeout(config: BrowserPluginConfig): number {
|
|
1055
|
+
return Math.min(config.cdpDiscoveryTimeoutMs ?? DEFAULT_CDP_DISCOVERY_TIMEOUT_MS, MAX_CDP_DISCOVERY_TIMEOUT_MS);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
async function discoverCdpEndpoint(endpoint: string, timeoutMs: number): Promise<CdpAttachAttempt> {
|
|
1059
|
+
const parsed = new URL(endpoint);
|
|
1060
|
+
if (parsed.protocol === 'ws:' || parsed.protocol === 'wss:') return { endpoint, discoveredEndpoint: endpoint };
|
|
1061
|
+
const probeUrl = new URL('/json/version', parsed.origin).toString();
|
|
1062
|
+
const body = await runtimeHooks.fetchJson(probeUrl, timeoutMs) as { webSocketDebuggerUrl?: unknown; Browser?: unknown };
|
|
1063
|
+
const discoveredEndpoint = typeof body.webSocketDebuggerUrl === 'string' && body.webSocketDebuggerUrl.trim()
|
|
1064
|
+
? body.webSocketDebuggerUrl.trim()
|
|
1065
|
+
: endpoint;
|
|
1066
|
+
const discoveredError = cdpEndpointValidationError(discoveredEndpoint);
|
|
1067
|
+
if (discoveredError) throw new Error(discoveredError);
|
|
1068
|
+
return {
|
|
1069
|
+
endpoint,
|
|
1070
|
+
discoveredEndpoint,
|
|
1071
|
+
probeUrl,
|
|
1072
|
+
browserVersion: typeof body.Browser === 'string' ? body.Browser : undefined,
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function launchOptionsForRepo(repoRoot: string, config: BrowserPluginConfig, profile: BrowserProfileSelection): Record<string, unknown> {
|
|
1077
|
+
return {
|
|
1078
|
+
headless: false,
|
|
1079
|
+
acceptDownloads: true,
|
|
1080
|
+
viewport: { width: 1280, height: 900 },
|
|
1081
|
+
...(config.executablePath ? { executablePath: resolveConfiguredPath(repoRoot, config.executablePath) } : {}),
|
|
1082
|
+
...(!config.executablePath && config.browserChannel && config.browserChannel !== 'chromium' ? { channel: config.browserChannel } : {}),
|
|
1083
|
+
...(profile.profileDirectory ? { args: [`--profile-directory=${profile.profileDirectory}`] } : {}),
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
async function openManagedContext(
|
|
1088
|
+
runtime: PlaywrightRuntime,
|
|
1089
|
+
repoRoot: string,
|
|
1090
|
+
config: BrowserPluginConfig,
|
|
1091
|
+
target: BrowserActionTarget,
|
|
1092
|
+
args: Record<string, unknown>,
|
|
1093
|
+
activeMode: BrowserMode,
|
|
1094
|
+
requestedMode: BrowserMode,
|
|
1095
|
+
fallback?: BrowserConnectionFallback,
|
|
1096
|
+
): Promise<BrowserOpenHandle> {
|
|
1097
|
+
const profile = selectedProfile(config, repoRoot, activeMode, target.sessionId);
|
|
1098
|
+
assertBrowserProfileAvailable(repoRoot, profile.selectedProfilePath);
|
|
1099
|
+
mkdirSync(profile.profileDir, { recursive: true });
|
|
1100
|
+
const context = await runtime.chromium.launchPersistentContext(profile.profileDir, launchOptionsForRepo(repoRoot, config, profile));
|
|
1101
|
+
await applyDomainGuard(context, config);
|
|
1102
|
+
const selected = await selectPage(context, target);
|
|
1103
|
+
const timeout = positiveNumber(args.timeout_ms, config.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
1104
|
+
let response: { status?: () => number } | null | undefined;
|
|
1105
|
+
if (selected.matchedBy === 'new_page' || selected.matchedBy === 'blank' || comparableUrl(selected.page.url()) !== comparableUrl(target.url)) {
|
|
1106
|
+
response = await selected.page.goto(target.url, { waitUntil: waitUntil(args.wait_until), timeout }) as { status?: () => number } | null | undefined;
|
|
1107
|
+
}
|
|
1108
|
+
const diagnostics = attachDiagnostics(selected.page);
|
|
1109
|
+
diagnostics.navigation = {
|
|
1110
|
+
url: target.url,
|
|
1111
|
+
status: response && typeof response.status === 'function' ? response.status() : undefined,
|
|
1112
|
+
};
|
|
1113
|
+
const title = await selected.page.title();
|
|
1114
|
+
const connection = refreshConnectionTab({
|
|
1115
|
+
requestedMode,
|
|
1116
|
+
mode: activeMode,
|
|
1117
|
+
provider: 'playwright-persistent-context',
|
|
1118
|
+
attached: false,
|
|
1119
|
+
fallback,
|
|
1120
|
+
profile: {
|
|
1121
|
+
profileMode: activeMode === 'isolated' ? 'repo_local' : config.profileMode,
|
|
1122
|
+
profileDirectory: profile.profileDirectory,
|
|
1123
|
+
selectedProfilePath: profile.selectedProfilePath,
|
|
1124
|
+
},
|
|
1125
|
+
tabInventory: selected.inventory,
|
|
1126
|
+
sessionResume: selected.sessionResume,
|
|
1127
|
+
}, normalizedUrl(selected.page.url()), title, selected.matchedBy);
|
|
1128
|
+
return {
|
|
1129
|
+
page: selected.page,
|
|
1130
|
+
diagnostics,
|
|
1131
|
+
connection,
|
|
1132
|
+
close: async () => {
|
|
1133
|
+
await context.close().catch(() => undefined);
|
|
1134
|
+
},
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
async function openAttachedContext(
|
|
1139
|
+
runtime: PlaywrightRuntime,
|
|
1140
|
+
repoRoot: string,
|
|
1141
|
+
config: BrowserPluginConfig,
|
|
1142
|
+
target: BrowserActionTarget,
|
|
1143
|
+
args: Record<string, unknown>,
|
|
1144
|
+
): Promise<{ handle?: BrowserOpenHandle; attempts: CdpAttachAttempt[] }> {
|
|
1145
|
+
const attempts: CdpAttachAttempt[] = [];
|
|
1146
|
+
const profile = selectedProfile(config, repoRoot, 'managed_persistent', target.sessionId);
|
|
1147
|
+
assertBrowserProfileAvailable(repoRoot, profile.selectedProfilePath);
|
|
1148
|
+
const connectOverCDP = runtime.chromium.connectOverCDP;
|
|
1149
|
+
if (typeof connectOverCDP !== 'function') {
|
|
1150
|
+
return {
|
|
1151
|
+
attempts: [{
|
|
1152
|
+
endpoint: cdpEndpoints(config)[0] ?? '(none)',
|
|
1153
|
+
error: 'Playwright chromium.connectOverCDP is unavailable in this runtime.',
|
|
1154
|
+
}],
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
for (const endpoint of cdpEndpoints(config)) {
|
|
1158
|
+
const attempt: CdpAttachAttempt = { endpoint };
|
|
1159
|
+
try {
|
|
1160
|
+
const discovered = await discoverCdpEndpoint(endpoint, cdpDiscoveryTimeout(config));
|
|
1161
|
+
Object.assign(attempt, discovered);
|
|
1162
|
+
const browser = await connectOverCDP.call(runtime.chromium, discovered.discoveredEndpoint ?? endpoint);
|
|
1163
|
+
try {
|
|
1164
|
+
const context = browser.contexts()[0] ?? (browser.newContext ? await browser.newContext() : undefined);
|
|
1165
|
+
if (!context) throw new Error('CDP connection did not expose a browser context.');
|
|
1166
|
+
await applyDomainGuard(context, config);
|
|
1167
|
+
const selected = await selectPage(context, target, { allowBlank: false, allowNewPage: false, requireOwnedToken: true });
|
|
1168
|
+
const timeout = positiveNumber(args.timeout_ms, config.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
1169
|
+
let response: { status?: () => number } | null | undefined;
|
|
1170
|
+
if (selected.matchedBy === 'new_page' || selected.matchedBy === 'blank' || comparableUrl(selected.page.url()) !== comparableUrl(target.url)) {
|
|
1171
|
+
response = await selected.page.goto(target.url, { waitUntil: waitUntil(args.wait_until), timeout }) as { status?: () => number } | null | undefined;
|
|
1172
|
+
}
|
|
1173
|
+
const diagnostics = attachDiagnostics(selected.page);
|
|
1174
|
+
diagnostics.navigation = {
|
|
1175
|
+
url: target.url,
|
|
1176
|
+
status: response && typeof response.status === 'function' ? response.status() : undefined,
|
|
1177
|
+
};
|
|
1178
|
+
const title = await selected.page.title();
|
|
1179
|
+
const connection = refreshConnectionTab({
|
|
1180
|
+
requestedMode: 'attach_preferred',
|
|
1181
|
+
mode: 'attach_preferred',
|
|
1182
|
+
provider: 'playwright-cdp',
|
|
1183
|
+
attached: true,
|
|
1184
|
+
endpoint: discovered.discoveredEndpoint ?? endpoint,
|
|
1185
|
+
browserVersion: discovered.browserVersion,
|
|
1186
|
+
profile: {
|
|
1187
|
+
profileMode: config.profileMode,
|
|
1188
|
+
profileDirectory: profile.profileDirectory,
|
|
1189
|
+
selectedProfilePath: profile.selectedProfilePath,
|
|
1190
|
+
},
|
|
1191
|
+
tabInventory: selected.inventory,
|
|
1192
|
+
sessionResume: selected.sessionResume,
|
|
1193
|
+
}, normalizedUrl(selected.page.url()), title, selected.matchedBy, selected.selectedEntry?.ownerToken
|
|
1194
|
+
? { ownership: 'plugin_owned', ownerToken: selected.selectedEntry.ownerToken }
|
|
1195
|
+
: undefined);
|
|
1196
|
+
return {
|
|
1197
|
+
attempts,
|
|
1198
|
+
handle: {
|
|
1199
|
+
page: selected.page,
|
|
1200
|
+
diagnostics,
|
|
1201
|
+
connection,
|
|
1202
|
+
close: async () => {
|
|
1203
|
+
if (browser.disconnect) await Promise.resolve(browser.disconnect()).catch(() => undefined);
|
|
1204
|
+
else if (browser.close) await browser.close().catch(() => undefined);
|
|
1205
|
+
},
|
|
1206
|
+
},
|
|
1207
|
+
};
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
if (browser.disconnect) await Promise.resolve(browser.disconnect()).catch(() => undefined);
|
|
1210
|
+
else if (browser.close) await browser.close().catch(() => undefined);
|
|
1211
|
+
throw error;
|
|
1212
|
+
}
|
|
1213
|
+
} catch (error) {
|
|
1214
|
+
attempt.error = error instanceof Error ? error.message : String(error);
|
|
1215
|
+
attempts.push(attempt);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
return { attempts };
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
async function openNativeAttachedContext(
|
|
1222
|
+
repoRoot: string,
|
|
1223
|
+
config: BrowserPluginConfig,
|
|
1224
|
+
target: BrowserActionTarget,
|
|
1225
|
+
args: Record<string, unknown>,
|
|
1226
|
+
): Promise<{ handle?: BrowserOpenHandle; attempts: MacOsBrowserAttachAttempt[] }> {
|
|
1227
|
+
if (config.nativeAttachMode === 'disabled') return { attempts: [] };
|
|
1228
|
+
const timeout = positiveNumber(args.timeout_ms, config.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
1229
|
+
const savedTab = target.existingSession?.browser?.tab;
|
|
1230
|
+
const savedProduct = target.existingSession?.browser?.provider === 'macos-apple-events'
|
|
1231
|
+
&& savedTab?.ownership === 'plugin_owned'
|
|
1232
|
+
&& typeof savedTab.windowId === 'string'
|
|
1233
|
+
&& typeof savedTab.tabId === 'string'
|
|
1234
|
+
? target.existingSession.browser.browserProduct
|
|
1235
|
+
: undefined;
|
|
1236
|
+
let page: PageLike | undefined;
|
|
1237
|
+
let discovered: Awaited<ReturnType<typeof discoverMacOsBrowserAttachment>> | undefined;
|
|
1238
|
+
let sessionResume: BrowserSessionResumeDiagnostic;
|
|
1239
|
+
let matchedBy: BrowserTabMatchReason = 'new_page';
|
|
1240
|
+
|
|
1241
|
+
if (savedProduct && savedTab && typeof savedTab.windowId === 'string' && typeof savedTab.tabId === 'string') {
|
|
1242
|
+
const ref: MacOsBrowserTabRef = { windowId: savedTab.windowId, tabId: savedTab.tabId };
|
|
1243
|
+
try {
|
|
1244
|
+
const reattached = await reattachMacOsBrowserOwnedPage(savedProduct, ref, timeout);
|
|
1245
|
+
page = reattached.page as unknown as PageLike;
|
|
1246
|
+
discovered = { attachment: reattached.attachment, attempts: reattached.attachment.attempts };
|
|
1247
|
+
matchedBy = 'owned_token';
|
|
1248
|
+
sessionResume = { sessionId: target.sessionId, status: 'matched', reason: 'Reattached directly to the saved plugin-owned macOS browser tab.', savedTab };
|
|
1249
|
+
} catch {
|
|
1250
|
+
sessionResume = { sessionId: target.sessionId, status: 'stale_tab', reason: 'Saved plugin-owned macOS tab no longer exists; one replacement tab was created.', savedTab };
|
|
1251
|
+
}
|
|
1252
|
+
} else {
|
|
1253
|
+
sessionResume = savedTab
|
|
1254
|
+
? { sessionId: target.sessionId, status: 'stale_tab', reason: 'Legacy or user-owned tab metadata is never reused for plugin actions; one plugin-owned tab was created.', savedTab }
|
|
1255
|
+
: { sessionId: target.sessionId, status: 'no_saved_tab', reason: 'Created one plugin-owned tab while preserving the user active tab.' };
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
if (!discovered?.attachment) {
|
|
1259
|
+
const candidates = savedProduct ? [savedProduct, ...(config.nativeBrowserCandidates ?? ['vivaldi', 'chrome']).filter((product) => product !== savedProduct)] : (config.nativeBrowserCandidates ?? ['vivaldi', 'chrome']);
|
|
1260
|
+
discovered = await discoverMacOsBrowserAttachment(candidates, Math.min(timeout, MAX_CDP_DISCOVERY_TIMEOUT_MS));
|
|
1261
|
+
}
|
|
1262
|
+
if (!discovered.attachment) return { attempts: discovered.attempts };
|
|
1263
|
+
|
|
1264
|
+
let createdThisCallRef: MacOsBrowserTabRef | undefined;
|
|
1265
|
+
let replacedRef: MacOsBrowserTabRef | undefined;
|
|
1266
|
+
try {
|
|
1267
|
+
if (!page) {
|
|
1268
|
+
page = await createMacOsBrowserOwnedPage(discovered.attachment, target.url, timeout) as unknown as PageLike;
|
|
1269
|
+
createdThisCallRef = (page as unknown as { tabRef?: () => MacOsBrowserTabRef | undefined }).tabRef?.();
|
|
1270
|
+
}
|
|
1271
|
+
if (comparableUrl(page.url()) !== comparableUrl(target.url)) {
|
|
1272
|
+
const currentRef = (page as unknown as { tabRef?: () => MacOsBrowserTabRef | undefined }).tabRef?.();
|
|
1273
|
+
if (!currentRef) {
|
|
1274
|
+
await page.goto(target.url, { waitUntil: waitUntil(args.wait_until), timeout });
|
|
1275
|
+
} else {
|
|
1276
|
+
const replacement = await createMacOsBrowserOwnedPage(discovered.attachment, target.url, timeout) as unknown as PageLike;
|
|
1277
|
+
const createdRef = (replacement as unknown as { tabRef?: () => MacOsBrowserTabRef | undefined }).tabRef?.();
|
|
1278
|
+
if (!createdRef) throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_OWNERSHIP_MISSING', 'Replacement native browser tab did not return a stable plugin-owned tab reference.', { retryable: true });
|
|
1279
|
+
replacedRef = currentRef;
|
|
1280
|
+
createdThisCallRef = createdRef;
|
|
1281
|
+
page = replacement;
|
|
1282
|
+
matchedBy = 'new_page';
|
|
1283
|
+
sessionResume = { sessionId: target.sessionId, status: 'matched', reason: 'Reattached to the saved session by replacing its previous plugin-owned background tab.', savedTab };
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
if (matchedBy !== 'owned_token' && page.waitForLoadState) {
|
|
1287
|
+
await page.waitForLoadState(waitUntil(args.wait_until), { timeout });
|
|
1288
|
+
}
|
|
1289
|
+
const pageUrl = normalizedUrl(page.url());
|
|
1290
|
+
assertUrlAllowed(pageUrl, config);
|
|
1291
|
+
const nativeRef = (page as unknown as { tabRef?: () => MacOsBrowserTabRef | undefined }).tabRef?.();
|
|
1292
|
+
if (!nativeRef) throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_OWNERSHIP_MISSING', 'Native browser did not return a stable plugin-owned tab reference.', { retryable: true });
|
|
1293
|
+
const title = matchedBy === 'owned_token' ? (target.existingSession?.title ?? '') : '';
|
|
1294
|
+
const inventory: BrowserTabInventoryEntry[] = [{ index: 0, key: tabKey(pageUrl, title), url: pageUrl, title }];
|
|
1295
|
+
const metadata = discovered.attachment.metadata;
|
|
1296
|
+
const connection = refreshConnectionTab({
|
|
1297
|
+
requestedMode: 'attach_preferred',
|
|
1298
|
+
mode: 'attach_preferred',
|
|
1299
|
+
provider: 'macos-apple-events',
|
|
1300
|
+
attached: true,
|
|
1301
|
+
browserProduct: metadata.product,
|
|
1302
|
+
profile: {
|
|
1303
|
+
profileMode: config.profileMode,
|
|
1304
|
+
profileDirectory: config.profileDirectory,
|
|
1305
|
+
selectedProfilePath: `macos:${metadata.product}:owned-tab`,
|
|
1306
|
+
},
|
|
1307
|
+
tabInventory: inventory,
|
|
1308
|
+
sessionResume,
|
|
1309
|
+
}, pageUrl, title, matchedBy, {
|
|
1310
|
+
ownership: 'plugin_owned', windowId: nativeRef.windowId, tabId: nativeRef.tabId,
|
|
1311
|
+
});
|
|
1312
|
+
const diagnostics: PageDiagnostics = { consoleErrors: [], failedRequests: [], navigation: { url: pageUrl } };
|
|
1313
|
+
if (replacedRef) await closeMacOsBrowserOwnedTab(metadata.product, replacedRef, timeout);
|
|
1314
|
+
createdThisCallRef = undefined;
|
|
1315
|
+
return { attempts: discovered.attempts, handle: { page, diagnostics, connection, close: async () => undefined } };
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
if (createdThisCallRef) {
|
|
1318
|
+
await closeMacOsBrowserOwnedTab(discovered.attachment.metadata.product, createdThisCallRef, timeout).catch(() => undefined);
|
|
1319
|
+
}
|
|
1320
|
+
throw error;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
async function openBrowser(
|
|
1325
|
+
repoRoot: string,
|
|
1326
|
+
config: BrowserPluginConfig,
|
|
1327
|
+
target: BrowserActionTarget,
|
|
1328
|
+
args: Record<string, unknown>,
|
|
1329
|
+
): Promise<BrowserOpenHandle> {
|
|
1330
|
+
const requestedMode = config.browserMode ?? 'managed_persistent';
|
|
1331
|
+
if (requestedMode === 'attach_preferred') {
|
|
1332
|
+
const attached = cdpEndpoints(config).length > 0 && runtimeHooks.moduleAvailable('playwright')
|
|
1333
|
+
? await openAttachedContext(runtimeHooks.loadPlaywright(), repoRoot, config, target, args)
|
|
1334
|
+
: { attempts: [] as CdpAttachAttempt[] };
|
|
1335
|
+
if (attached.handle) return attached.handle;
|
|
1336
|
+
|
|
1337
|
+
const native = await openNativeAttachedContext(repoRoot, config, target, args);
|
|
1338
|
+
if (native.handle) return native.handle;
|
|
1339
|
+
|
|
1340
|
+
const fallbackPolicy = config.cdpAttachFallback ?? 'managed_persistent';
|
|
1341
|
+
const cdpReason = attached.attempts.map((attempt) => `${attempt.endpoint}: ${attempt.error ?? 'unavailable'}`).join('; ')
|
|
1342
|
+
|| 'No CDP endpoint candidates were configured.';
|
|
1343
|
+
const nativeReason = native.attempts.map((attempt) => `${attempt.appName}: ${attempt.error ?? attempt.status}`).join('; ')
|
|
1344
|
+
|| (config.nativeAttachMode === 'disabled' ? 'Native browser attach is disabled.' : 'No scriptable macOS browser was available.');
|
|
1345
|
+
const reason = `CDP: ${cdpReason} Native: ${nativeReason}`;
|
|
1346
|
+
if (fallbackPolicy === 'fail_closed') {
|
|
1347
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_ATTACH_UNAVAILABLE', `No configured browser attach provider was available. Fallback policy fail_closed prevented managed launch. ${reason}`, {
|
|
1348
|
+
retryable: true,
|
|
1349
|
+
details: {
|
|
1350
|
+
browserMode: requestedMode,
|
|
1351
|
+
fallbackPolicy,
|
|
1352
|
+
attempts: attached.attempts,
|
|
1353
|
+
nativeAttempts: native.attempts,
|
|
1354
|
+
},
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
if (!runtimeHooks.moduleAvailable('playwright')) {
|
|
1358
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_DEPENDENCY_UNAVAILABLE', `Managed browser fallback requires Playwright, but native/CDP attach was unavailable. ${reason}`, { retryable: false });
|
|
1359
|
+
}
|
|
1360
|
+
return openManagedContext(runtimeHooks.loadPlaywright(), repoRoot, config, target, args, 'managed_persistent', requestedMode, {
|
|
1361
|
+
policy: fallbackPolicy,
|
|
1362
|
+
from: 'attach_preferred',
|
|
1363
|
+
to: 'managed_persistent',
|
|
1364
|
+
reason,
|
|
1365
|
+
attempts: attached.attempts,
|
|
1366
|
+
nativeAttempts: native.attempts,
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
if (!runtimeHooks.moduleAvailable('playwright')) {
|
|
1371
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_DEPENDENCY_UNAVAILABLE', 'Managed browser mode requires Playwright.', { retryable: false });
|
|
1372
|
+
}
|
|
1373
|
+
return openManagedContext(runtimeHooks.loadPlaywright(), repoRoot, config, target, args, requestedMode, requestedMode);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
async function withPage<T>(
|
|
1377
|
+
repoRoot: string,
|
|
1378
|
+
config: BrowserPluginConfig,
|
|
1379
|
+
target: BrowserActionTarget,
|
|
1380
|
+
args: Record<string, unknown>,
|
|
1381
|
+
run: (page: PageLike, diagnostics: PageDiagnostics, connection: BrowserConnectionSummary) => Promise<T>,
|
|
1382
|
+
options: { persistSession?: boolean } = {},
|
|
1383
|
+
): Promise<T> {
|
|
1384
|
+
assertUrlAllowed(target.url, config);
|
|
1385
|
+
const retries = Math.min(Math.max(positiveNumber(args.retries, 1), 1), 3);
|
|
1386
|
+
let lastError: unknown;
|
|
1387
|
+
for (let attempt = 1; attempt <= retries; attempt += 1) {
|
|
1388
|
+
let handle: BrowserOpenHandle | undefined;
|
|
1389
|
+
try {
|
|
1390
|
+
handle = await openBrowser(repoRoot, config, target, args);
|
|
1391
|
+
const result = await run(handle.page, handle.diagnostics, handle.connection);
|
|
1392
|
+
if (options.persistSession) {
|
|
1393
|
+
const identity = await readPageIdentity(handle.page, handle.connection);
|
|
1394
|
+
assertUrlAllowed(identity.url, config);
|
|
1395
|
+
saveSession(repoRoot, sessionFromPage(target, identity.url, identity.title, handle.connection));
|
|
1396
|
+
}
|
|
1397
|
+
return result;
|
|
1398
|
+
} catch (error) {
|
|
1399
|
+
lastError = error;
|
|
1400
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1401
|
+
const transient = /timeout|net::|ERR_|Navigation failed|Target closed|Protocol error/i.test(message);
|
|
1402
|
+
if (!transient || attempt >= retries) throw error;
|
|
1403
|
+
} finally {
|
|
1404
|
+
if (handle) await handle.close();
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
function listSavedSessions(repoRoot: string): BrowserSessionState[] {
|
|
1411
|
+
const dir = stateDir(repoRoot, 'sessions');
|
|
1412
|
+
if (!existsSync(dir)) return [];
|
|
1413
|
+
try {
|
|
1414
|
+
return readdirSync(dir)
|
|
1415
|
+
.filter((name) => name.endsWith('.json'))
|
|
1416
|
+
.map((name) => readJson<BrowserSessionState>(join(dir, name)))
|
|
1417
|
+
.filter((entry): entry is BrowserSessionState => Boolean(entry?.sessionId && entry.url))
|
|
1418
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
1419
|
+
} catch {
|
|
1420
|
+
return [];
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
const STABLE_SELECTOR_HELPERS = `
|
|
1425
|
+
const forgeUnique = (selector) => {
|
|
1426
|
+
try { return document.querySelectorAll(selector).length === 1; } catch { return false; }
|
|
1427
|
+
};
|
|
1428
|
+
const forgeAttributeSelector = (el, attribute) => {
|
|
1429
|
+
const value = el.getAttribute(attribute);
|
|
1430
|
+
if (!value) return '';
|
|
1431
|
+
const tag = el.tagName.toLowerCase();
|
|
1432
|
+
const attr = '[' + attribute + '=' + JSON.stringify(value) + ']';
|
|
1433
|
+
if (forgeUnique(attr)) return attr;
|
|
1434
|
+
const tagged = tag + attr;
|
|
1435
|
+
return forgeUnique(tagged) ? tagged : '';
|
|
1436
|
+
};
|
|
1437
|
+
const forgeDirectSelector = (el) => {
|
|
1438
|
+
for (const attribute of ['data-testid', 'data-test', 'data-qa', 'data-thread-id', 'data-legacy-thread-id', 'data-legacy-message-id']) {
|
|
1439
|
+
const candidate = forgeAttributeSelector(el, attribute);
|
|
1440
|
+
if (candidate) return candidate;
|
|
1441
|
+
}
|
|
1442
|
+
const tag = el.tagName.toLowerCase();
|
|
1443
|
+
const role = el.getAttribute('role');
|
|
1444
|
+
const ariaLabel = el.getAttribute('aria-label');
|
|
1445
|
+
if (role && ariaLabel) {
|
|
1446
|
+
const candidate = tag + '[role=' + JSON.stringify(role) + '][aria-label=' + JSON.stringify(ariaLabel) + ']';
|
|
1447
|
+
if (forgeUnique(candidate)) return candidate;
|
|
1448
|
+
}
|
|
1449
|
+
const name = forgeAttributeSelector(el, 'name');
|
|
1450
|
+
if (name) return name;
|
|
1451
|
+
if (el.id && !/^:/.test(el.id) && !/^elptr_/.test(el.id)) {
|
|
1452
|
+
const candidate = '#' + CSS.escape(el.id);
|
|
1453
|
+
if (forgeUnique(candidate)) return candidate;
|
|
1454
|
+
}
|
|
1455
|
+
if (ariaLabel) {
|
|
1456
|
+
const candidate = tag + '[aria-label=' + JSON.stringify(ariaLabel) + ']';
|
|
1457
|
+
if (forgeUnique(candidate)) return candidate;
|
|
1458
|
+
}
|
|
1459
|
+
return '';
|
|
1460
|
+
};
|
|
1461
|
+
const forgeDescendantSelector = (el) => {
|
|
1462
|
+
const descendants = Array.from(el.querySelectorAll('[data-testid],[data-test],[data-qa],[data-thread-id],[data-legacy-thread-id],[data-legacy-message-id]')).slice(0, 12);
|
|
1463
|
+
for (const descendant of descendants) {
|
|
1464
|
+
const inner = forgeDirectSelector(descendant);
|
|
1465
|
+
if (!inner) continue;
|
|
1466
|
+
const role = el.getAttribute('role');
|
|
1467
|
+
const base = el.tagName.toLowerCase() + (role ? '[role=' + JSON.stringify(role) + ']' : '');
|
|
1468
|
+
const candidate = base + ':has(' + inner + ')';
|
|
1469
|
+
if (forgeUnique(candidate)) return candidate;
|
|
1470
|
+
}
|
|
1471
|
+
return '';
|
|
1472
|
+
};
|
|
1473
|
+
const forgeStructuralSelector = (el) => {
|
|
1474
|
+
let current = el;
|
|
1475
|
+
const tail = [];
|
|
1476
|
+
for (let depth = 0; current && current.nodeType === 1 && depth < 8; depth += 1) {
|
|
1477
|
+
const direct = forgeDirectSelector(current);
|
|
1478
|
+
if (direct) {
|
|
1479
|
+
const candidate = tail.length ? direct + ' > ' + tail.join(' > ') : direct;
|
|
1480
|
+
if (forgeUnique(candidate)) return candidate;
|
|
1481
|
+
}
|
|
1482
|
+
const tag = current.tagName.toLowerCase();
|
|
1483
|
+
let siblingIndex = 1;
|
|
1484
|
+
let sibling = current.previousElementSibling;
|
|
1485
|
+
while (sibling) {
|
|
1486
|
+
if (sibling.tagName === current.tagName) siblingIndex += 1;
|
|
1487
|
+
sibling = sibling.previousElementSibling;
|
|
1488
|
+
}
|
|
1489
|
+
tail.unshift(tag + ':nth-of-type(' + siblingIndex + ')');
|
|
1490
|
+
const candidate = tail.join(' > ');
|
|
1491
|
+
if (forgeUnique(candidate)) return candidate;
|
|
1492
|
+
current = current.parentElement;
|
|
1493
|
+
}
|
|
1494
|
+
return tail.join(' > ');
|
|
1495
|
+
};
|
|
1496
|
+
const forgeSelectorHint = (el) => forgeDirectSelector(el) || forgeDescendantSelector(el) || forgeStructuralSelector(el);
|
|
1497
|
+
`;
|
|
1498
|
+
|
|
1499
|
+
const EXTRACTION_SCRIPTS = {
|
|
1500
|
+
query: (selector: string, limit: number) => `(() => {
|
|
1501
|
+
${STABLE_SELECTOR_HELPERS}
|
|
1502
|
+
const nodes = Array.from(document.querySelectorAll(${JSON.stringify(selector)})).slice(0, ${limit});
|
|
1503
|
+
return nodes.map((el) => {
|
|
1504
|
+
const role = el.getAttribute('role');
|
|
1505
|
+
const text = (el.innerText || el.textContent || '').trim().slice(0, 120);
|
|
1506
|
+
return { tag: el.tagName.toLowerCase(), text, href: el.getAttribute('href'), name: el.getAttribute('name'), id: el.id || undefined, role, selectorHint: forgeSelectorHint(el) };
|
|
1507
|
+
});
|
|
1508
|
+
})()`,
|
|
1509
|
+
attribute: (selector: string, attribute: string) => `(() => {
|
|
1510
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
1511
|
+
return el ? el.getAttribute(${JSON.stringify(attribute)}) : null;
|
|
1512
|
+
})()`,
|
|
1513
|
+
html: (selector?: string) => selector
|
|
1514
|
+
? `(() => { const el = document.querySelector(${JSON.stringify(selector)}); return el ? el.outerHTML : ''; })()`
|
|
1515
|
+
: 'document.documentElement ? document.documentElement.outerHTML : ""',
|
|
1516
|
+
links: (limit: number) => `(() => {
|
|
1517
|
+
${STABLE_SELECTOR_HELPERS}
|
|
1518
|
+
return Array.from(document.querySelectorAll('a[href]')).slice(0, ${limit}).map((a) => ({
|
|
1519
|
+
href: a.href, text: (a.innerText || a.textContent || '').trim().slice(0, 120), selectorHint: forgeSelectorHint(a)
|
|
1520
|
+
}));
|
|
1521
|
+
})()`,
|
|
1522
|
+
tables: (limit: number) => `(() => Array.from(document.querySelectorAll('table')).slice(0, ${limit}).map((table, tableIndex) => ({
|
|
1523
|
+
index: tableIndex,
|
|
1524
|
+
headers: Array.from(table.querySelectorAll('th')).map((th) => (th.innerText || '').trim().slice(0, 80)),
|
|
1525
|
+
rows: Array.from(table.querySelectorAll('tr')).slice(0, 50).map((tr) => Array.from(tr.querySelectorAll('td,th')).map((cell) => (cell.innerText || '').trim().slice(0, 80)))
|
|
1526
|
+
})))()`,
|
|
1527
|
+
forms: (limit: number) => `(() => {
|
|
1528
|
+
${STABLE_SELECTOR_HELPERS}
|
|
1529
|
+
return Array.from(document.querySelectorAll('form')).slice(0, ${limit}).map((form, index) => ({
|
|
1530
|
+
index,
|
|
1531
|
+
action: form.getAttribute('action') || '',
|
|
1532
|
+
method: (form.getAttribute('method') || 'get').toLowerCase(),
|
|
1533
|
+
fields: Array.from(form.querySelectorAll('input,select,textarea,button')).slice(0, 40).map((el) => ({
|
|
1534
|
+
tag: el.tagName.toLowerCase(),
|
|
1535
|
+
type: el.getAttribute('type') || undefined,
|
|
1536
|
+
name: el.getAttribute('name') || undefined,
|
|
1537
|
+
id: el.id || undefined,
|
|
1538
|
+
selectorHint: forgeSelectorHint(el)
|
|
1539
|
+
}))
|
|
1540
|
+
}));
|
|
1541
|
+
})()`,
|
|
1542
|
+
interactive: (limit: number) => `(() => {
|
|
1543
|
+
${STABLE_SELECTOR_HELPERS}
|
|
1544
|
+
return Array.from(document.querySelectorAll('a,button,input,select,textarea,[role="button"],[role="link"]')).slice(0, ${limit}).map((el) => {
|
|
1545
|
+
const text = (el.innerText || el.getAttribute('aria-label') || el.getAttribute('value') || el.textContent || '').trim().slice(0, 100);
|
|
1546
|
+
return { tag: el.tagName.toLowerCase(), text, type: el.getAttribute('type') || undefined, href: el.getAttribute('href') || undefined, selectorHint: forgeSelectorHint(el) };
|
|
1547
|
+
});
|
|
1548
|
+
})()`,
|
|
1549
|
+
};
|
|
1550
|
+
|
|
1551
|
+
function selectorRepairHint(selector: string, errorMessage: string): string {
|
|
1552
|
+
if (/strict mode violation|resolved to \d+ elements/i.test(errorMessage)) {
|
|
1553
|
+
return `Selector "${selector}" matched multiple elements. Prefer a unique #id, [data-testid], or more specific path.`;
|
|
1554
|
+
}
|
|
1555
|
+
if (/Timeout|waiting for selector|not found/i.test(errorMessage)) {
|
|
1556
|
+
return `Selector "${selector}" was not found in time. Use snapshot_interactive or query_all to discover stable selectors, then retry.`;
|
|
1557
|
+
}
|
|
1558
|
+
return `Check selector "${selector}" against the current page structure.`;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
async function extractText(page: PageLike, selector: string | undefined, maxChars: number): Promise<Record<string, unknown>> {
|
|
1562
|
+
const raw = await page.evaluate<string>(scriptText(selector));
|
|
1563
|
+
return truncateText(raw, maxChars);
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
async function readPageIdentity(page: PageLike, connection?: BrowserConnectionSummary): Promise<{ url: string; title: string }> {
|
|
1567
|
+
if (connection?.provider === 'macos-apple-events' && typeof page.identity === 'function') {
|
|
1568
|
+
const identity = await page.identity();
|
|
1569
|
+
return { url: normalizedUrl(identity.url), title: identity.title };
|
|
1570
|
+
}
|
|
1571
|
+
return { url: normalizedUrl(page.url()), title: await page.title() };
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
async function finalizeInteractiveAction(
|
|
1575
|
+
repoRoot: string,
|
|
1576
|
+
config: BrowserPluginConfig,
|
|
1577
|
+
page: PageLike,
|
|
1578
|
+
target: BrowserActionTarget,
|
|
1579
|
+
connection: BrowserConnectionSummary,
|
|
1580
|
+
actionId: string,
|
|
1581
|
+
summary: string,
|
|
1582
|
+
extra: Record<string, unknown> = {},
|
|
1583
|
+
): Promise<Record<string, unknown>> {
|
|
1584
|
+
const warnings: string[] = [];
|
|
1585
|
+
const identity = await readPageIdentity(page, connection);
|
|
1586
|
+
const title = identity.title;
|
|
1587
|
+
const pageUrl = identity.url;
|
|
1588
|
+
assertUrlAllowed(pageUrl, config);
|
|
1589
|
+
const session = sessionFromPage(target, pageUrl, title, connection);
|
|
1590
|
+
saveSession(repoRoot, session);
|
|
1591
|
+
let screenshot: BrowserActionScreenshot | undefined;
|
|
1592
|
+
const silentNativeEvidence = connection.provider === 'macos-apple-events' && connection.tab?.ownership === 'plugin_owned';
|
|
1593
|
+
if (!silentNativeEvidence) {
|
|
1594
|
+
try {
|
|
1595
|
+
screenshot = await captureActionScreenshot(page, repoRoot, actionId, session.sessionId, session.url);
|
|
1596
|
+
} catch (error) {
|
|
1597
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1598
|
+
warnings.push(`Screenshot capture failed: ${message}`);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
return {
|
|
1602
|
+
...interactionResult(actionId, session, summary, screenshot, warnings, extra),
|
|
1603
|
+
evidenceMode: silentNativeEvidence ? 'dom' : screenshot ? 'screenshot' : 'summary',
|
|
1604
|
+
browserConnection: {
|
|
1605
|
+
...connection,
|
|
1606
|
+
tab: session.browser?.tab,
|
|
1607
|
+
sessionResume: session.browser?.sessionResume,
|
|
1608
|
+
},
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
function permissions(ready: boolean): AssistantPluginPermissionScope[] {
|
|
1613
|
+
return [
|
|
1614
|
+
{
|
|
1615
|
+
scope: 'browser.read',
|
|
1616
|
+
mode: 'read',
|
|
1617
|
+
description: 'Open pages, extract text, and capture screenshots.',
|
|
1618
|
+
granted: ready,
|
|
1619
|
+
required: true,
|
|
1620
|
+
},
|
|
1621
|
+
{
|
|
1622
|
+
scope: 'browser.interact',
|
|
1623
|
+
mode: 'write',
|
|
1624
|
+
description: 'Click, type, press keys, and wait on allowed browser pages after explicit authorization.',
|
|
1625
|
+
granted: ready,
|
|
1626
|
+
required: true,
|
|
1627
|
+
},
|
|
1628
|
+
{
|
|
1629
|
+
scope: 'browser.profile',
|
|
1630
|
+
mode: 'write',
|
|
1631
|
+
description: 'Persist the dedicated local browser profile, session metadata, and screenshots.',
|
|
1632
|
+
granted: ready,
|
|
1633
|
+
required: true,
|
|
1634
|
+
},
|
|
1635
|
+
];
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
function capabilities(): AssistantPluginCapability[] {
|
|
1639
|
+
return [
|
|
1640
|
+
{
|
|
1641
|
+
capabilityId: 'browser-session',
|
|
1642
|
+
title: 'Browser Sessions',
|
|
1643
|
+
description: 'Create, list, reuse, and close persistent browser sessions.',
|
|
1644
|
+
scopes: ['browser.read', 'browser.profile'],
|
|
1645
|
+
actions: ['create_session', 'list_sessions', 'close_session', 'close_page', 'clear_session'],
|
|
1646
|
+
},
|
|
1647
|
+
{
|
|
1648
|
+
capabilityId: 'browser-human-handoff',
|
|
1649
|
+
title: 'Browser Human Handoff',
|
|
1650
|
+
description: 'Present the persistent browser in the foreground, keep it open for manual verification, and resume safely.',
|
|
1651
|
+
scopes: ['browser.read', 'browser.profile'],
|
|
1652
|
+
actions: ['request_human_handoff', 'get_handoff_status', 'resolve_handoff'],
|
|
1653
|
+
},
|
|
1654
|
+
{
|
|
1655
|
+
capabilityId: 'browser-readonly',
|
|
1656
|
+
title: 'Read-only Browser',
|
|
1657
|
+
description: 'Navigate allowed pages, extract DOM/text, capture screenshots, and collect diagnostics.',
|
|
1658
|
+
scopes: ['browser.read', 'browser.profile'],
|
|
1659
|
+
actions: [
|
|
1660
|
+
'open_page', 'navigate', 'reload', 'go_back', 'wait_for_load_state',
|
|
1661
|
+
'get_text', 'get_html', 'query_selector', 'query_all', 'get_attribute',
|
|
1662
|
+
'screenshot', 'extract_links', 'extract_tables', 'extract_forms', 'snapshot_interactive',
|
|
1663
|
+
'get_console_errors', 'get_failed_requests',
|
|
1664
|
+
],
|
|
1665
|
+
},
|
|
1666
|
+
{
|
|
1667
|
+
capabilityId: 'browser-interaction',
|
|
1668
|
+
title: 'Browser Interaction',
|
|
1669
|
+
description: 'Perform explicit form and pointer interactions on allowed domains through the persistent Playwright profile.',
|
|
1670
|
+
scopes: ['browser.interact', 'browser.profile'],
|
|
1671
|
+
actions: [
|
|
1672
|
+
'click', 'double_click', 'hover', 'focus', 'type', 'fill', 'select_option',
|
|
1673
|
+
'check', 'uncheck', 'press', 'keyboard_shortcut', 'wait_for_selector', 'attach_local_file', 'await_file_transfer',
|
|
1674
|
+
],
|
|
1675
|
+
},
|
|
1676
|
+
];
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
function sessionTargetSchema(extra: Record<string, unknown> = {}, required: string[] = []): Record<string, unknown> {
|
|
1680
|
+
return {
|
|
1681
|
+
type: 'object',
|
|
1682
|
+
properties: {
|
|
1683
|
+
session_id: { type: 'string' },
|
|
1684
|
+
url: { type: 'string' },
|
|
1685
|
+
wait_until: { type: 'string', enum: ['load', 'domcontentloaded', 'networkidle'] },
|
|
1686
|
+
timeout_ms: { type: 'number' },
|
|
1687
|
+
retries: { type: 'number' },
|
|
1688
|
+
...extra,
|
|
1689
|
+
},
|
|
1690
|
+
...(required.length > 0 ? { required } : {}),
|
|
1691
|
+
additionalProperties: false,
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
function interactSchema(extra: Record<string, unknown>, required: string[]): Record<string, unknown> {
|
|
1696
|
+
return sessionTargetSchema({
|
|
1697
|
+
post_action_wait_ms: { type: 'number' },
|
|
1698
|
+
...extra,
|
|
1699
|
+
}, required);
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
function actions(): AssistantPluginActionDescriptor[] {
|
|
1703
|
+
const readRemote = [
|
|
1704
|
+
{ resource: 'remote' as const, mode: 'read' as const },
|
|
1705
|
+
{ resource: 'repo-state' as const, mode: 'write' as const },
|
|
1706
|
+
];
|
|
1707
|
+
const writeRemote = [
|
|
1708
|
+
{ resource: 'remote' as const, mode: 'exclusive' as const },
|
|
1709
|
+
{ resource: 'repo-state' as const, mode: 'write' as const },
|
|
1710
|
+
];
|
|
1711
|
+
return [
|
|
1712
|
+
{
|
|
1713
|
+
actionId: 'configure',
|
|
1714
|
+
title: 'Configure browser plugin',
|
|
1715
|
+
description: 'Enable or update the local browser plugin configuration.',
|
|
1716
|
+
readOnly: false,
|
|
1717
|
+
risk: 'workspace_write',
|
|
1718
|
+
confirmation: 'authorization',
|
|
1719
|
+
defaultTimeoutMs: 30_000,
|
|
1720
|
+
cancellable: true,
|
|
1721
|
+
idempotent: true,
|
|
1722
|
+
scopes: ['browser.profile'],
|
|
1723
|
+
resourceClaims: [{ resource: 'repo-state', mode: 'write' }],
|
|
1724
|
+
argumentsSchema: {
|
|
1725
|
+
type: 'object',
|
|
1726
|
+
properties: {
|
|
1727
|
+
enabled: { type: 'boolean' },
|
|
1728
|
+
browser_mode: { type: 'string', enum: ['attach_preferred', 'managed_persistent', 'isolated'] },
|
|
1729
|
+
profile_mode: { type: 'string', enum: ['repo_local', 'custom'] },
|
|
1730
|
+
profile_dir: { type: 'string' },
|
|
1731
|
+
profile_directory: { type: 'string' },
|
|
1732
|
+
clear_profile_dir: { type: 'boolean' },
|
|
1733
|
+
clear_profile_directory: { type: 'boolean' },
|
|
1734
|
+
browser_channel: { type: 'string', enum: ['chromium', 'chrome', 'chrome-beta', 'chrome-dev', 'chrome-canary'] },
|
|
1735
|
+
clear_browser_channel: { type: 'boolean' },
|
|
1736
|
+
browser_executable_path: { type: 'string' },
|
|
1737
|
+
clear_browser_executable_path: { type: 'boolean' },
|
|
1738
|
+
cdp_endpoint: { type: 'string' },
|
|
1739
|
+
clear_cdp_endpoint: { type: 'boolean' },
|
|
1740
|
+
cdp_endpoint_candidates: { type: 'array', items: { type: 'string' }, maxItems: MAX_CDP_ENDPOINT_CANDIDATES },
|
|
1741
|
+
clear_cdp_endpoint_candidates: { type: 'boolean' },
|
|
1742
|
+
cdp_discovery_timeout_ms: { type: 'number' },
|
|
1743
|
+
cdp_attach_fallback: { type: 'string', enum: ['managed_persistent', 'fail_closed'] },
|
|
1744
|
+
native_attach_mode: { type: 'string', enum: ['auto', 'disabled'] },
|
|
1745
|
+
native_browser_candidates: { type: 'array', items: { type: 'string', enum: ['vivaldi', 'chrome'] }, maxItems: 2 },
|
|
1746
|
+
default_timeout_ms: { type: 'number' },
|
|
1747
|
+
allowed_domains: { type: 'array', items: { type: 'string' } },
|
|
1748
|
+
clear_allowed_domains: { type: 'boolean' },
|
|
1749
|
+
},
|
|
1750
|
+
additionalProperties: false,
|
|
1751
|
+
},
|
|
1752
|
+
},
|
|
1753
|
+
{
|
|
1754
|
+
actionId: 'create_session',
|
|
1755
|
+
title: 'Create browser session',
|
|
1756
|
+
description: 'Open an allowed URL and persist a reusable session id.',
|
|
1757
|
+
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
1758
|
+
scopes: ['browser.read', 'browser.profile'], resourceClaims: [{ resource: 'repo-state', mode: 'write' }, ...readRemote],
|
|
1759
|
+
argumentsSchema: sessionTargetSchema({ extract_text: { type: 'boolean' }, max_chars: { type: 'number' } }, ['url']),
|
|
1760
|
+
},
|
|
1761
|
+
{
|
|
1762
|
+
actionId: 'list_sessions',
|
|
1763
|
+
title: 'List browser sessions',
|
|
1764
|
+
description: 'List saved browser session metadata without secrets or cookies.',
|
|
1765
|
+
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 10_000, cancellable: true, idempotent: true,
|
|
1766
|
+
scopes: ['browser.read', 'browser.profile'], resourceClaims: [{ resource: 'repo-state', mode: 'read' }],
|
|
1767
|
+
argumentsSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
1768
|
+
},
|
|
1769
|
+
{
|
|
1770
|
+
actionId: 'close_session',
|
|
1771
|
+
title: 'Close browser session',
|
|
1772
|
+
description: 'Remove one saved session metadata record.',
|
|
1773
|
+
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 15_000, cancellable: true, idempotent: true,
|
|
1774
|
+
scopes: ['browser.read', 'browser.profile'], resourceClaims: [{ resource: 'repo-state', mode: 'write' }],
|
|
1775
|
+
argumentsSchema: { type: 'object', properties: { session_id: { type: 'string' } }, required: ['session_id'], additionalProperties: false },
|
|
1776
|
+
},
|
|
1777
|
+
{
|
|
1778
|
+
actionId: 'clear_session',
|
|
1779
|
+
title: 'Clear all browser sessions',
|
|
1780
|
+
description: 'Remove all saved session metadata while keeping the profile.',
|
|
1781
|
+
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 15_000, cancellable: true, idempotent: true,
|
|
1782
|
+
scopes: ['browser.profile'], resourceClaims: [{ resource: 'repo-state', mode: 'write' }],
|
|
1783
|
+
argumentsSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
1784
|
+
},
|
|
1785
|
+
{
|
|
1786
|
+
actionId: 'request_human_handoff',
|
|
1787
|
+
title: 'Request human browser handoff',
|
|
1788
|
+
description: 'Keep a saved browser session open for CAPTCHA, login, two-factor authentication, or another manual step.',
|
|
1789
|
+
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 30_000, cancellable: true, idempotent: false,
|
|
1790
|
+
scopes: ['browser.read', 'browser.profile'], resourceClaims: readRemote,
|
|
1791
|
+
argumentsSchema: sessionTargetSchema({
|
|
1792
|
+
reason: { type: 'string', enum: ['captcha', 'login', 'two_factor', 'manual_review', 'sensitive_confirmation'] },
|
|
1793
|
+
instructions: { type: 'string' },
|
|
1794
|
+
handoff_timeout_ms: { type: 'number' },
|
|
1795
|
+
}, ['session_id']),
|
|
1796
|
+
},
|
|
1797
|
+
{
|
|
1798
|
+
actionId: 'get_handoff_status',
|
|
1799
|
+
title: 'Get browser handoff status',
|
|
1800
|
+
description: 'Read durable browser handoff status and reconcile a stale or crashed host.',
|
|
1801
|
+
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 10_000, cancellable: true, idempotent: true,
|
|
1802
|
+
scopes: ['browser.read', 'browser.profile'], resourceClaims: [{ resource: 'repo-state', mode: 'read' }],
|
|
1803
|
+
argumentsSchema: { type: 'object', properties: { interaction_id: { type: 'string' } }, required: ['interaction_id'], additionalProperties: false },
|
|
1804
|
+
},
|
|
1805
|
+
{
|
|
1806
|
+
actionId: 'resolve_handoff',
|
|
1807
|
+
title: 'Resolve browser handoff',
|
|
1808
|
+
description: 'Resume or cancel a foreground browser handoff.',
|
|
1809
|
+
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 15_000, cancellable: true, idempotent: true,
|
|
1810
|
+
scopes: ['browser.read', 'browser.profile'], resourceClaims: [{ resource: 'repo-state', mode: 'write' }],
|
|
1811
|
+
argumentsSchema: {
|
|
1812
|
+
type: 'object',
|
|
1813
|
+
properties: {
|
|
1814
|
+
interaction_id: { type: 'string' },
|
|
1815
|
+
resolution: { type: 'string', enum: ['resume', 'cancel'] },
|
|
1816
|
+
},
|
|
1817
|
+
required: ['interaction_id', 'resolution'],
|
|
1818
|
+
additionalProperties: false,
|
|
1819
|
+
},
|
|
1820
|
+
},
|
|
1821
|
+
{
|
|
1822
|
+
actionId: 'open_page',
|
|
1823
|
+
title: 'Open page',
|
|
1824
|
+
description: 'Open an allowed URL with the persistent profile and save a lightweight session.',
|
|
1825
|
+
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
1826
|
+
scopes: ['browser.read'], resourceClaims: readRemote,
|
|
1827
|
+
argumentsSchema: sessionTargetSchema({ extract_text: { type: 'boolean' }, max_chars: { type: 'number' } }, ['url']),
|
|
1828
|
+
},
|
|
1829
|
+
{
|
|
1830
|
+
actionId: 'navigate',
|
|
1831
|
+
title: 'Navigate',
|
|
1832
|
+
description: 'Navigate an existing session or open a new page to an allowed URL.',
|
|
1833
|
+
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
1834
|
+
scopes: ['browser.read'], resourceClaims: readRemote,
|
|
1835
|
+
argumentsSchema: sessionTargetSchema({}, ['url']),
|
|
1836
|
+
},
|
|
1837
|
+
{
|
|
1838
|
+
actionId: 'reload',
|
|
1839
|
+
title: 'Reload page',
|
|
1840
|
+
description: 'Reload the current page for a session.',
|
|
1841
|
+
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
1842
|
+
scopes: ['browser.read'], resourceClaims: readRemote,
|
|
1843
|
+
argumentsSchema: sessionTargetSchema({}, ['session_id']),
|
|
1844
|
+
},
|
|
1845
|
+
{
|
|
1846
|
+
actionId: 'go_back',
|
|
1847
|
+
title: 'Go back',
|
|
1848
|
+
description: 'Navigate back in history for a session.',
|
|
1849
|
+
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
1850
|
+
scopes: ['browser.read'], resourceClaims: readRemote,
|
|
1851
|
+
argumentsSchema: sessionTargetSchema({}, ['session_id']),
|
|
1852
|
+
},
|
|
1853
|
+
{
|
|
1854
|
+
actionId: 'wait_for_load_state',
|
|
1855
|
+
title: 'Wait for load state',
|
|
1856
|
+
description: 'Wait for load/domcontentloaded/networkidle on a session page.',
|
|
1857
|
+
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
1858
|
+
scopes: ['browser.read'], resourceClaims: readRemote,
|
|
1859
|
+
argumentsSchema: sessionTargetSchema({ state: { type: 'string', enum: ['load', 'domcontentloaded', 'networkidle'] } }),
|
|
1860
|
+
},
|
|
1861
|
+
{
|
|
1862
|
+
actionId: 'get_text',
|
|
1863
|
+
title: 'Get text',
|
|
1864
|
+
description: 'Extract text from a URL or saved browser session.',
|
|
1865
|
+
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
1866
|
+
scopes: ['browser.read'], resourceClaims: [{ resource: 'remote', mode: 'read' }],
|
|
1867
|
+
argumentsSchema: sessionTargetSchema({ selector: { type: 'string' }, max_chars: { type: 'number' } }),
|
|
1868
|
+
},
|
|
1869
|
+
{
|
|
1870
|
+
actionId: 'get_html',
|
|
1871
|
+
title: 'Get HTML',
|
|
1872
|
+
description: 'Extract HTML for the page or a selector (bounded).',
|
|
1873
|
+
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
1874
|
+
scopes: ['browser.read'], resourceClaims: [{ resource: 'remote', mode: 'read' }],
|
|
1875
|
+
argumentsSchema: sessionTargetSchema({ selector: { type: 'string' }, max_chars: { type: 'number' } }),
|
|
1876
|
+
},
|
|
1877
|
+
{
|
|
1878
|
+
actionId: 'query_selector',
|
|
1879
|
+
title: 'Query selector',
|
|
1880
|
+
description: 'Return the first matching element summary with a stable selector hint.',
|
|
1881
|
+
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
1882
|
+
scopes: ['browser.read'], resourceClaims: [{ resource: 'remote', mode: 'read' }],
|
|
1883
|
+
argumentsSchema: sessionTargetSchema({ selector: { type: 'string' } }, ['selector']),
|
|
1884
|
+
},
|
|
1885
|
+
{
|
|
1886
|
+
actionId: 'query_all',
|
|
1887
|
+
title: 'Query all selectors',
|
|
1888
|
+
description: 'Return matching element summaries (bounded).',
|
|
1889
|
+
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
1890
|
+
scopes: ['browser.read'], resourceClaims: [{ resource: 'remote', mode: 'read' }],
|
|
1891
|
+
argumentsSchema: sessionTargetSchema({ selector: { type: 'string' }, limit: { type: 'number' } }, ['selector']),
|
|
1892
|
+
},
|
|
1893
|
+
{
|
|
1894
|
+
actionId: 'get_attribute',
|
|
1895
|
+
title: 'Get attribute',
|
|
1896
|
+
description: 'Read one attribute from a selector.',
|
|
1897
|
+
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
1898
|
+
scopes: ['browser.read'], resourceClaims: [{ resource: 'remote', mode: 'read' }],
|
|
1899
|
+
argumentsSchema: sessionTargetSchema({ selector: { type: 'string' }, attribute: { type: 'string' } }, ['selector', 'attribute']),
|
|
1900
|
+
},
|
|
1901
|
+
{
|
|
1902
|
+
actionId: 'screenshot',
|
|
1903
|
+
title: 'Screenshot',
|
|
1904
|
+
description: 'Capture a page, full-page, or element screenshot under browser artifact storage.',
|
|
1905
|
+
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
1906
|
+
scopes: ['browser.read'], resourceClaims: readRemote,
|
|
1907
|
+
argumentsSchema: sessionTargetSchema({ full_page: { type: 'boolean' }, selector: { type: 'string' } }),
|
|
1908
|
+
},
|
|
1909
|
+
{
|
|
1910
|
+
actionId: 'extract_links',
|
|
1911
|
+
title: 'Extract links',
|
|
1912
|
+
description: 'Extract anchors with href/text from the page.',
|
|
1913
|
+
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
1914
|
+
scopes: ['browser.read'], resourceClaims: [{ resource: 'remote', mode: 'read' }],
|
|
1915
|
+
argumentsSchema: sessionTargetSchema({ limit: { type: 'number' } }),
|
|
1916
|
+
},
|
|
1917
|
+
{
|
|
1918
|
+
actionId: 'extract_tables',
|
|
1919
|
+
title: 'Extract tables',
|
|
1920
|
+
description: 'Extract simple HTML tables as row arrays.',
|
|
1921
|
+
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
1922
|
+
scopes: ['browser.read'], resourceClaims: [{ resource: 'remote', mode: 'read' }],
|
|
1923
|
+
argumentsSchema: sessionTargetSchema({ limit: { type: 'number' } }),
|
|
1924
|
+
},
|
|
1925
|
+
{
|
|
1926
|
+
actionId: 'extract_forms',
|
|
1927
|
+
title: 'Extract forms',
|
|
1928
|
+
description: 'Extract form field summaries without values that look like secrets.',
|
|
1929
|
+
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
1930
|
+
scopes: ['browser.read'], resourceClaims: [{ resource: 'remote', mode: 'read' }],
|
|
1931
|
+
argumentsSchema: sessionTargetSchema({ limit: { type: 'number' } }),
|
|
1932
|
+
},
|
|
1933
|
+
{
|
|
1934
|
+
actionId: 'snapshot_interactive',
|
|
1935
|
+
title: 'Snapshot interactive elements',
|
|
1936
|
+
description: 'Snapshot buttons/inputs/links with stable selector hints.',
|
|
1937
|
+
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
1938
|
+
scopes: ['browser.read'], resourceClaims: [{ resource: 'remote', mode: 'read' }],
|
|
1939
|
+
argumentsSchema: sessionTargetSchema({ limit: { type: 'number' } }),
|
|
1940
|
+
},
|
|
1941
|
+
{
|
|
1942
|
+
actionId: 'get_console_errors',
|
|
1943
|
+
title: 'Get console errors',
|
|
1944
|
+
description: 'Return captured console error messages for a page open cycle.',
|
|
1945
|
+
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
1946
|
+
scopes: ['browser.read'], resourceClaims: [{ resource: 'remote', mode: 'read' }],
|
|
1947
|
+
argumentsSchema: sessionTargetSchema({}),
|
|
1948
|
+
},
|
|
1949
|
+
{
|
|
1950
|
+
actionId: 'get_failed_requests',
|
|
1951
|
+
title: 'Get failed requests',
|
|
1952
|
+
description: 'Return failed network requests captured during a page open cycle.',
|
|
1953
|
+
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
1954
|
+
scopes: ['browser.read'], resourceClaims: [{ resource: 'remote', mode: 'read' }],
|
|
1955
|
+
argumentsSchema: sessionTargetSchema({}),
|
|
1956
|
+
},
|
|
1957
|
+
{
|
|
1958
|
+
actionId: 'click',
|
|
1959
|
+
title: 'Click element',
|
|
1960
|
+
description: 'Click a selector on an allowed page after authorization.',
|
|
1961
|
+
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
1962
|
+
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
1963
|
+
argumentsSchema: interactSchema({ selector: { type: 'string' } }, ['selector']),
|
|
1964
|
+
},
|
|
1965
|
+
{
|
|
1966
|
+
actionId: 'double_click',
|
|
1967
|
+
title: 'Double-click element',
|
|
1968
|
+
description: 'Double-click a selector after authorization.',
|
|
1969
|
+
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
1970
|
+
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
1971
|
+
argumentsSchema: interactSchema({ selector: { type: 'string' } }, ['selector']),
|
|
1972
|
+
},
|
|
1973
|
+
{
|
|
1974
|
+
actionId: 'hover',
|
|
1975
|
+
title: 'Hover element',
|
|
1976
|
+
description: 'Hover a selector after authorization.',
|
|
1977
|
+
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
1978
|
+
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
1979
|
+
argumentsSchema: interactSchema({ selector: { type: 'string' } }, ['selector']),
|
|
1980
|
+
},
|
|
1981
|
+
{
|
|
1982
|
+
actionId: 'focus',
|
|
1983
|
+
title: 'Focus element',
|
|
1984
|
+
description: 'Focus a selector after authorization.',
|
|
1985
|
+
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
1986
|
+
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
1987
|
+
argumentsSchema: interactSchema({ selector: { type: 'string' } }, ['selector']),
|
|
1988
|
+
},
|
|
1989
|
+
{
|
|
1990
|
+
actionId: 'type',
|
|
1991
|
+
title: 'Type into element',
|
|
1992
|
+
description: 'Type text into a selector (append-style) after authorization.',
|
|
1993
|
+
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
1994
|
+
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
1995
|
+
argumentsSchema: interactSchema({ selector: { type: 'string' }, text: { type: 'string' } }, ['selector', 'text']),
|
|
1996
|
+
},
|
|
1997
|
+
{
|
|
1998
|
+
actionId: 'fill',
|
|
1999
|
+
title: 'Fill element',
|
|
2000
|
+
description: 'Replace the value of a selector after authorization.',
|
|
2001
|
+
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
2002
|
+
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
2003
|
+
argumentsSchema: interactSchema({ selector: { type: 'string' }, text: { type: 'string' } }, ['selector', 'text']),
|
|
2004
|
+
},
|
|
2005
|
+
{
|
|
2006
|
+
actionId: 'select_option',
|
|
2007
|
+
title: 'Select option',
|
|
2008
|
+
description: 'Select one or more option values on a <select> after authorization.',
|
|
2009
|
+
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
2010
|
+
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
2011
|
+
argumentsSchema: interactSchema({ selector: { type: 'string' }, values: { type: 'array', items: { type: 'string' } } }, ['selector', 'values']),
|
|
2012
|
+
},
|
|
2013
|
+
{
|
|
2014
|
+
actionId: 'check',
|
|
2015
|
+
title: 'Check checkbox',
|
|
2016
|
+
description: 'Check a checkbox/radio after authorization.',
|
|
2017
|
+
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
2018
|
+
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
2019
|
+
argumentsSchema: interactSchema({ selector: { type: 'string' } }, ['selector']),
|
|
2020
|
+
},
|
|
2021
|
+
{
|
|
2022
|
+
actionId: 'uncheck',
|
|
2023
|
+
title: 'Uncheck checkbox',
|
|
2024
|
+
description: 'Uncheck a checkbox after authorization.',
|
|
2025
|
+
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
2026
|
+
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
2027
|
+
argumentsSchema: interactSchema({ selector: { type: 'string' } }, ['selector']),
|
|
2028
|
+
},
|
|
2029
|
+
{
|
|
2030
|
+
actionId: 'press',
|
|
2031
|
+
title: 'Press key',
|
|
2032
|
+
description: 'Press a key on a selector on an allowed page after authorization.',
|
|
2033
|
+
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
2034
|
+
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
2035
|
+
argumentsSchema: interactSchema({ selector: { type: 'string' }, key: { type: 'string' } }, ['selector', 'key']),
|
|
2036
|
+
},
|
|
2037
|
+
{
|
|
2038
|
+
actionId: 'keyboard_shortcut',
|
|
2039
|
+
title: 'Keyboard shortcut',
|
|
2040
|
+
description: 'Press a keyboard shortcut (e.g. Meta+A) after authorization.',
|
|
2041
|
+
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
2042
|
+
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
2043
|
+
argumentsSchema: interactSchema({ key: { type: 'string' } }, ['key']),
|
|
2044
|
+
},
|
|
2045
|
+
{
|
|
2046
|
+
actionId: 'wait_for_selector',
|
|
2047
|
+
title: 'Wait for selector',
|
|
2048
|
+
description: 'Wait for a selector state on an allowed page after authorization.',
|
|
2049
|
+
readOnly: true, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
2050
|
+
scopes: ['browser.interact', 'browser.profile'], resourceClaims: readRemote,
|
|
2051
|
+
argumentsSchema: sessionTargetSchema({
|
|
2052
|
+
selector: { type: 'string' },
|
|
2053
|
+
state: { type: 'string', enum: ['attached', 'detached', 'visible', 'hidden'] },
|
|
2054
|
+
}, ['selector']),
|
|
2055
|
+
},
|
|
2056
|
+
{
|
|
2057
|
+
actionId: 'attach_local_file',
|
|
2058
|
+
title: 'Attach local file',
|
|
2059
|
+
description: 'Set an input[type=file] path for an allowed local file after authorization. Never auto-opens executables.',
|
|
2060
|
+
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
2061
|
+
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
2062
|
+
argumentsSchema: interactSchema({ selector: { type: 'string' }, file_path: { type: 'string' } }, ['selector', 'file_path']),
|
|
2063
|
+
},
|
|
2064
|
+
{
|
|
2065
|
+
actionId: 'await_file_transfer',
|
|
2066
|
+
title: 'Await file transfer',
|
|
2067
|
+
description: 'Capture a browser download into bounded artifact storage after authorization. Never auto-opens downloaded files.',
|
|
2068
|
+
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 120_000, cancellable: true, idempotent: false,
|
|
2069
|
+
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
2070
|
+
argumentsSchema: interactSchema({ selector: { type: 'string' }, suggested_name: { type: 'string' } }, ['selector']),
|
|
2071
|
+
},
|
|
2072
|
+
{
|
|
2073
|
+
actionId: 'close_page',
|
|
2074
|
+
title: 'Close session',
|
|
2075
|
+
description: 'Remove saved session metadata while keeping the persistent profile.',
|
|
2076
|
+
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 15_000, cancellable: true, idempotent: true,
|
|
2077
|
+
scopes: ['browser.read', 'browser.profile'], resourceClaims: [{ resource: 'repo-state', mode: 'write' }],
|
|
2078
|
+
argumentsSchema: { type: 'object', properties: { session_id: { type: 'string' } }, required: ['session_id'], additionalProperties: false },
|
|
2079
|
+
},
|
|
2080
|
+
];
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
function browserUserFacingStatus(config: BrowserPluginConfig, ready: boolean, sessionCount = 0): string {
|
|
2084
|
+
if (!config.enabled) return 'disabled';
|
|
2085
|
+
if (!ready) return 'not ready';
|
|
2086
|
+
if (sessionCount > 0) return 'session active';
|
|
2087
|
+
if (config.allowedDomains && config.allowedDomains.length > 0) return 'domain restricted';
|
|
2088
|
+
return 'ready';
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
function health(config: BrowserPluginConfig, repoRoot?: string): AssistantPluginHealth {
|
|
2092
|
+
const dependencyReady = runtimeHooks.moduleAvailable('playwright');
|
|
2093
|
+
const configErrors = validateConfig(config);
|
|
2094
|
+
const warnings = configWarnings(config);
|
|
2095
|
+
const sessionCount = repoRoot ? listSavedSessions(repoRoot).length : 0;
|
|
2096
|
+
const activeHandoffCount = repoRoot
|
|
2097
|
+
? listBrowserHandoffs(repoRoot).filter((entry) => ['starting', 'waiting_for_user', 'closing'].includes(entry.status)).length
|
|
2098
|
+
: 0;
|
|
2099
|
+
const baseDetails = {
|
|
2100
|
+
dependencyReady,
|
|
2101
|
+
browserMode: config.browserMode,
|
|
2102
|
+
profileMode: config.profileMode,
|
|
2103
|
+
profileDir: config.profileDir,
|
|
2104
|
+
profileDirectory: config.profileDirectory,
|
|
2105
|
+
browserChannel: config.browserChannel,
|
|
2106
|
+
executablePath: config.executablePath,
|
|
2107
|
+
cdpEndpointCount: cdpEndpoints(config).length,
|
|
2108
|
+
cdpAttachFallback: config.cdpAttachFallback,
|
|
2109
|
+
cdpDiscoveryTimeoutMs: cdpDiscoveryTimeout(config),
|
|
2110
|
+
nativeAttachMode: config.nativeAttachMode,
|
|
2111
|
+
nativeBrowserCandidates: config.nativeBrowserCandidates,
|
|
2112
|
+
nativeAttachSupported: macOsActiveBrowserAttachSupported(),
|
|
2113
|
+
windowMode: 'visible' as const,
|
|
2114
|
+
sessionCount,
|
|
2115
|
+
activeHandoffCount,
|
|
2116
|
+
humanHandoffSupported: true,
|
|
2117
|
+
artifactsAvailable: true,
|
|
2118
|
+
artifactRoots: {
|
|
2119
|
+
screenshots: '.forge/browser/screenshots',
|
|
2120
|
+
downloads: '.forge/browser/downloads',
|
|
2121
|
+
diagnostics: '.forge/browser/diagnostics',
|
|
2122
|
+
},
|
|
2123
|
+
};
|
|
2124
|
+
if (!config.enabled) {
|
|
2125
|
+
return {
|
|
2126
|
+
state: 'disabled',
|
|
2127
|
+
checkedAt: now(),
|
|
2128
|
+
ready: false,
|
|
2129
|
+
probed: false,
|
|
2130
|
+
errors: [],
|
|
2131
|
+
warnings: ['Browser plugin is disabled.'],
|
|
2132
|
+
details: { ...baseDetails, userFacingStatus: 'disabled' },
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
const nativeAttachConfigured = config.browserMode === 'attach_preferred'
|
|
2136
|
+
&& config.nativeAttachMode !== 'disabled'
|
|
2137
|
+
&& macOsActiveBrowserAttachSupported();
|
|
2138
|
+
const nativeObservation = nativeAttachConfigured ? getMacOsBrowserAttachObservation() : undefined;
|
|
2139
|
+
const cdpRouteReady = config.browserMode === 'attach_preferred'
|
|
2140
|
+
&& cdpEndpoints(config).length > 0
|
|
2141
|
+
&& dependencyReady;
|
|
2142
|
+
const managedRouteReady = dependencyReady
|
|
2143
|
+
&& (config.browserMode !== 'attach_preferred' || config.cdpAttachFallback === 'managed_persistent');
|
|
2144
|
+
const nativeRouteReady = nativeAttachConfigured && nativeObservation?.ready === true;
|
|
2145
|
+
const nativeRouteUnverified = nativeAttachConfigured
|
|
2146
|
+
&& cdpEndpoints(config).length === 0
|
|
2147
|
+
&& config.cdpAttachFallback === 'fail_closed'
|
|
2148
|
+
&& nativeObservation === undefined;
|
|
2149
|
+
const nativeRouteFailed = nativeAttachConfigured
|
|
2150
|
+
&& cdpEndpoints(config).length === 0
|
|
2151
|
+
&& config.cdpAttachFallback === 'fail_closed'
|
|
2152
|
+
&& nativeObservation?.ready === false;
|
|
2153
|
+
if (nativeRouteUnverified || nativeRouteFailed) {
|
|
2154
|
+
const nativeWarnings = nativeRouteFailed
|
|
2155
|
+
? nativeObservation?.attempts.map((attempt) => `${attempt.appName}: ${attempt.error ?? attempt.status}`) ?? []
|
|
2156
|
+
: ['Native active-browser attach has not completed a live probe in this Runtime instance.'];
|
|
2157
|
+
return {
|
|
2158
|
+
state: 'degraded',
|
|
2159
|
+
checkedAt: now(),
|
|
2160
|
+
ready: false,
|
|
2161
|
+
probed: nativeRouteFailed,
|
|
2162
|
+
errors: [],
|
|
2163
|
+
warnings: [...warnings, ...nativeWarnings],
|
|
2164
|
+
details: {
|
|
2165
|
+
...baseDetails,
|
|
2166
|
+
nativeAttachObservation: nativeObservation,
|
|
2167
|
+
provider: 'macos-active-browser-fail-closed',
|
|
2168
|
+
userFacingStatus: 'not ready',
|
|
2169
|
+
},
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
if (!cdpRouteReady && !managedRouteReady && !nativeRouteReady) {
|
|
2173
|
+
return {
|
|
2174
|
+
state: 'error',
|
|
2175
|
+
checkedAt: now(),
|
|
2176
|
+
ready: false,
|
|
2177
|
+
probed: true,
|
|
2178
|
+
errors: ['Browser plugin requires Playwright for the configured browser mode. Run bun install or configure attach_preferred native attach.'],
|
|
2179
|
+
warnings,
|
|
2180
|
+
details: { ...baseDetails, install: 'bun install', userFacingStatus: 'not ready' },
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
if (configErrors.length > 0) {
|
|
2184
|
+
return {
|
|
2185
|
+
state: 'error',
|
|
2186
|
+
checkedAt: now(),
|
|
2187
|
+
ready: false,
|
|
2188
|
+
probed: true,
|
|
2189
|
+
errors: configErrors,
|
|
2190
|
+
warnings,
|
|
2191
|
+
details: {
|
|
2192
|
+
...baseDetails,
|
|
2193
|
+
provider: config.browserMode === 'attach_preferred'
|
|
2194
|
+
? 'cdp-or-macos-active-browser-or-persistent-context'
|
|
2195
|
+
: 'playwright-persistent-context',
|
|
2196
|
+
userFacingStatus: 'not ready',
|
|
2197
|
+
},
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
return {
|
|
2201
|
+
state: 'ready',
|
|
2202
|
+
checkedAt: now(),
|
|
2203
|
+
ready: true,
|
|
2204
|
+
probed: true,
|
|
2205
|
+
errors: [],
|
|
2206
|
+
warnings,
|
|
2207
|
+
details: {
|
|
2208
|
+
...baseDetails,
|
|
2209
|
+
nativeAttachObservation: nativeObservation,
|
|
2210
|
+
allowedDomains: config.allowedDomains,
|
|
2211
|
+
provider: config.browserMode === 'attach_preferred'
|
|
2212
|
+
? 'cdp-or-macos-active-browser-or-persistent-context'
|
|
2213
|
+
: 'playwright-persistent-context',
|
|
2214
|
+
userFacingStatus: browserUserFacingStatus(config, true, sessionCount),
|
|
2215
|
+
},
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
export function buildBrowserPluginManifest(previousRevision = 0, previousUpdatedAt?: string, repoRoot?: string): AssistantPluginManifest {
|
|
2220
|
+
const root = repoRoot ?? process.cwd();
|
|
2221
|
+
const config = loadConfig(root);
|
|
2222
|
+
const state = health(config, root);
|
|
2223
|
+
return {
|
|
2224
|
+
schemaVersion: 1,
|
|
2225
|
+
manifestVersion: 1,
|
|
2226
|
+
revision: Math.max(1, previousRevision || 1),
|
|
2227
|
+
pluginId: BROWSER_PLUGIN_ID,
|
|
2228
|
+
provider: 'local-browser',
|
|
2229
|
+
displayName: 'Controller Browser Plugin',
|
|
2230
|
+
pluginVersion: '1.1.0',
|
|
2231
|
+
authority: {
|
|
2232
|
+
strategy: 'derived',
|
|
2233
|
+
duplicateStateAllowed: false,
|
|
2234
|
+
sourceOfTruth: ['repo-local:.forge/plugins/browser.json', 'repo-local:.forge/browser/'],
|
|
2235
|
+
},
|
|
2236
|
+
enabled: config.enabled,
|
|
2237
|
+
lifecycle: {
|
|
2238
|
+
state: !config.enabled ? 'disabled' : state.state === 'degraded' ? 'degraded' : state.ready ? 'enabled' : 'error',
|
|
2239
|
+
reason: !config.enabled
|
|
2240
|
+
? 'Browser plugin is disabled.'
|
|
2241
|
+
: state.ready
|
|
2242
|
+
? `Browser plugin is ready via ${config.browserMode === 'attach_preferred' ? 'CDP, macOS active-browser attach, and explicit managed fallback.' : 'Playwright persistent context.'}`
|
|
2243
|
+
: state.state === 'degraded'
|
|
2244
|
+
? state.warnings[0] ?? 'Browser plugin requires a successful live native attach probe.'
|
|
2245
|
+
: state.errors[0],
|
|
2246
|
+
},
|
|
2247
|
+
health: state,
|
|
2248
|
+
permissions: permissions(state.ready),
|
|
2249
|
+
capabilities: capabilities(),
|
|
2250
|
+
actions: actions(),
|
|
2251
|
+
updatedAt: previousUpdatedAt ?? now(),
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
export async function executeBrowserPluginAction(input: AssistantPluginActionExecutionInput): Promise<Record<string, unknown>> {
|
|
2256
|
+
const current = loadConfig(input.repoRoot);
|
|
2257
|
+
if (!current.enabled && input.actionId !== 'configure') {
|
|
2258
|
+
throw new AssistantPluginError('PLUGIN_DISABLED', 'Browser plugin is disabled.', { retryable: false });
|
|
2259
|
+
}
|
|
2260
|
+
if (input.actionId !== 'configure') {
|
|
2261
|
+
const configErrors = validateConfig(current);
|
|
2262
|
+
if (configErrors.length > 0) {
|
|
2263
|
+
throw new AssistantPluginError('PLUGIN_CONFIGURATION_INVALID', configErrors[0], { retryable: false });
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
if (shouldUseBrowserNodeBridge(input.actionId, current.browserMode, runtimeHooksCustomized, cdpEndpoints(current).length > 0)) {
|
|
2267
|
+
return await executeBrowserActionThroughNode(input);
|
|
2268
|
+
}
|
|
2269
|
+
try {
|
|
2270
|
+
switch (input.actionId) {
|
|
2271
|
+
case 'configure': {
|
|
2272
|
+
const args = input.args;
|
|
2273
|
+
const nextBrowserMode = parseBrowserModeInput(args.browser_mode) ?? current.browserMode;
|
|
2274
|
+
const nextProfileMode = parseProfileModeInput(args.profile_mode) ?? current.profileMode;
|
|
2275
|
+
if (stringValue(args.profile_dir) && args.profile_mode === undefined && current.profileMode !== 'custom') {
|
|
2276
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'profile_mode must be set to custom before profile_dir can be used.', { retryable: false });
|
|
2277
|
+
}
|
|
2278
|
+
const nextProfileDir = args.clear_profile_dir === true ? undefined : stringValue(args.profile_dir) ?? current.profileDir;
|
|
2279
|
+
const nextProfileDirectory = args.clear_profile_directory === true ? undefined : stringValue(args.profile_directory) ?? current.profileDirectory;
|
|
2280
|
+
const nextBrowserChannel = args.clear_browser_channel === true ? undefined : parseBrowserChannelInput(args.browser_channel) ?? current.browserChannel;
|
|
2281
|
+
const nextExecutablePath = args.clear_browser_executable_path === true ? undefined : stringValue(args.browser_executable_path) ?? current.executablePath;
|
|
2282
|
+
const nextCdpEndpoint = args.clear_cdp_endpoint === true ? undefined : stringValue(args.cdp_endpoint) ?? current.cdpEndpoint;
|
|
2283
|
+
const nextCdpEndpointCandidates = args.clear_cdp_endpoint_candidates === true
|
|
2284
|
+
? undefined
|
|
2285
|
+
: stringList(args.cdp_endpoint_candidates) ?? current.cdpEndpointCandidates;
|
|
2286
|
+
const nextCdpAttachFallback = parseCdpAttachFallbackInput(args.cdp_attach_fallback) ?? current.cdpAttachFallback;
|
|
2287
|
+
const nextNativeAttachMode = parseNativeAttachModeInput(args.native_attach_mode) ?? current.nativeAttachMode;
|
|
2288
|
+
const nextNativeBrowserCandidates = parseNativeBrowserCandidatesInput(args.native_browser_candidates) ?? current.nativeBrowserCandidates;
|
|
2289
|
+
const config = saveConfig(input.repoRoot, {
|
|
2290
|
+
enabled: typeof args.enabled === 'boolean' ? args.enabled : current.enabled,
|
|
2291
|
+
browserMode: nextBrowserMode,
|
|
2292
|
+
profileMode: nextProfileMode,
|
|
2293
|
+
profileDir: nextProfileMode === 'repo_local' ? undefined : nextProfileDir,
|
|
2294
|
+
profileDirectory: nextProfileMode === 'repo_local' ? undefined : nextProfileDirectory,
|
|
2295
|
+
browserChannel: nextBrowserChannel ?? 'chromium',
|
|
2296
|
+
executablePath: nextExecutablePath,
|
|
2297
|
+
cdpEndpoint: nextCdpEndpoint,
|
|
2298
|
+
cdpEndpointCandidates: nextCdpEndpointCandidates,
|
|
2299
|
+
cdpDiscoveryTimeoutMs: typeof args.cdp_discovery_timeout_ms === 'number'
|
|
2300
|
+
? Math.min(positiveNumber(args.cdp_discovery_timeout_ms, DEFAULT_CDP_DISCOVERY_TIMEOUT_MS), MAX_CDP_DISCOVERY_TIMEOUT_MS)
|
|
2301
|
+
: current.cdpDiscoveryTimeoutMs,
|
|
2302
|
+
cdpAttachFallback: nextCdpAttachFallback,
|
|
2303
|
+
nativeAttachMode: nextNativeAttachMode,
|
|
2304
|
+
nativeBrowserCandidates: nextNativeBrowserCandidates,
|
|
2305
|
+
defaultTimeoutMs: typeof args.default_timeout_ms === 'number'
|
|
2306
|
+
? positiveNumber(args.default_timeout_ms, DEFAULT_TIMEOUT_MS)
|
|
2307
|
+
: current.defaultTimeoutMs,
|
|
2308
|
+
allowedDomains: args.clear_allowed_domains === true ? undefined : stringArray(args.allowed_domains) ?? current.allowedDomains,
|
|
2309
|
+
});
|
|
2310
|
+
const configErrors = validateConfig(config);
|
|
2311
|
+
if (configErrors.length > 0) {
|
|
2312
|
+
throw new AssistantPluginError('PLUGIN_CONFIGURATION_INVALID', configErrors[0], { retryable: false });
|
|
2313
|
+
}
|
|
2314
|
+
return { config, health: health(config, input.repoRoot) };
|
|
2315
|
+
}
|
|
2316
|
+
case 'list_sessions':
|
|
2317
|
+
return {
|
|
2318
|
+
provider: 'playwright',
|
|
2319
|
+
sessions: listSavedSessions(input.repoRoot).map((session) => ({
|
|
2320
|
+
sessionId: session.sessionId,
|
|
2321
|
+
url: session.url,
|
|
2322
|
+
title: session.title,
|
|
2323
|
+
createdAt: session.createdAt,
|
|
2324
|
+
updatedAt: session.updatedAt,
|
|
2325
|
+
browser: session.browser,
|
|
2326
|
+
})),
|
|
2327
|
+
};
|
|
2328
|
+
case 'close_session':
|
|
2329
|
+
case 'close_page': {
|
|
2330
|
+
const sessionId = requiredString(input.args.session_id, 'session_id');
|
|
2331
|
+
assertBrowserSessionAvailable(input.repoRoot, sessionId);
|
|
2332
|
+
const session = findSession(input.repoRoot, sessionId);
|
|
2333
|
+
const tab = session?.browser?.tab;
|
|
2334
|
+
if (session?.browser?.provider === 'macos-apple-events'
|
|
2335
|
+
&& session.browser.browserProduct
|
|
2336
|
+
&& tab?.ownership === 'plugin_owned'
|
|
2337
|
+
&& typeof tab.windowId === 'string'
|
|
2338
|
+
&& typeof tab.tabId === 'string') {
|
|
2339
|
+
await closeMacOsBrowserOwnedTab(session.browser.browserProduct, { windowId: tab.windowId, tabId: tab.tabId }, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
2340
|
+
.catch(() => undefined);
|
|
2341
|
+
}
|
|
2342
|
+
rmSync(sessionPath(input.repoRoot, sessionId), { force: true });
|
|
2343
|
+
return { closed: true, sessionId, resourceClosed: tab?.ownership === 'plugin_owned' };
|
|
2344
|
+
}
|
|
2345
|
+
case 'clear_session': {
|
|
2346
|
+
assertBrowserSessionAvailable(input.repoRoot);
|
|
2347
|
+
const sessions = listSavedSessions(input.repoRoot);
|
|
2348
|
+
for (const session of sessions) {
|
|
2349
|
+
const tab = session.browser?.tab;
|
|
2350
|
+
if (session.browser?.provider === 'macos-apple-events'
|
|
2351
|
+
&& session.browser.browserProduct
|
|
2352
|
+
&& tab?.ownership === 'plugin_owned'
|
|
2353
|
+
&& typeof tab.windowId === 'string'
|
|
2354
|
+
&& typeof tab.tabId === 'string') {
|
|
2355
|
+
await closeMacOsBrowserOwnedTab(session.browser.browserProduct, { windowId: tab.windowId, tabId: tab.tabId }, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
2356
|
+
.catch(() => undefined);
|
|
2357
|
+
}
|
|
2358
|
+
rmSync(sessionPath(input.repoRoot, session.sessionId), { force: true });
|
|
2359
|
+
}
|
|
2360
|
+
return { cleared: true, count: sessions.length };
|
|
2361
|
+
}
|
|
2362
|
+
case 'request_human_handoff': {
|
|
2363
|
+
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
2364
|
+
assertUrlAllowed(target.url, current);
|
|
2365
|
+
const nativeSession = target.existingSession;
|
|
2366
|
+
const nativeTab = nativeSession?.browser?.tab;
|
|
2367
|
+
const nativeBrowser = nativeSession?.browser?.provider === 'macos-apple-events'
|
|
2368
|
+
&& nativeSession.browser.browserProduct
|
|
2369
|
+
&& nativeTab?.ownership === 'plugin_owned'
|
|
2370
|
+
&& typeof nativeTab.windowId === 'string'
|
|
2371
|
+
&& typeof nativeTab.tabId === 'string'
|
|
2372
|
+
? {
|
|
2373
|
+
product: nativeSession.browser.browserProduct,
|
|
2374
|
+
ref: { windowId: nativeTab.windowId, tabId: nativeTab.tabId },
|
|
2375
|
+
}
|
|
2376
|
+
: undefined;
|
|
2377
|
+
const handoffMode: BrowserMode = current.browserMode === 'isolated' ? 'isolated' : 'managed_persistent';
|
|
2378
|
+
const profile = selectedProfile(current, input.repoRoot, handoffMode, target.sessionId);
|
|
2379
|
+
if (!nativeBrowser) mkdirSync(profile.profileDir, { recursive: true });
|
|
2380
|
+
const selectedProfilePath = nativeBrowser
|
|
2381
|
+
? `macos-apple-events:${nativeBrowser.product}:${nativeBrowser.ref.windowId}:${nativeBrowser.ref.tabId}`
|
|
2382
|
+
: profile.selectedProfilePath;
|
|
2383
|
+
const handoff = await startBrowserHandoff({
|
|
2384
|
+
repoRoot: input.repoRoot,
|
|
2385
|
+
repoId: input.repoId,
|
|
2386
|
+
requestId: input.requestId,
|
|
2387
|
+
jobId: input.jobId,
|
|
2388
|
+
sessionId: target.sessionId,
|
|
2389
|
+
sessionPath: sessionPath(input.repoRoot, target.sessionId),
|
|
2390
|
+
url: target.url,
|
|
2391
|
+
profileDir: profile.profileDir,
|
|
2392
|
+
selectedProfilePath,
|
|
2393
|
+
profileDirectory: profile.profileDirectory,
|
|
2394
|
+
browserChannel: current.browserChannel,
|
|
2395
|
+
executablePath: current.executablePath ? resolveConfiguredPath(input.repoRoot, current.executablePath) : undefined,
|
|
2396
|
+
allowedDomains: current.allowedDomains,
|
|
2397
|
+
defaultTimeoutMs: current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
2398
|
+
nativeBrowser,
|
|
2399
|
+
reason: stringValue(input.args.reason) ?? 'manual_review',
|
|
2400
|
+
instructions: stringValue(input.args.instructions),
|
|
2401
|
+
timeoutMs: typeof input.args.handoff_timeout_ms === 'number' ? input.args.handoff_timeout_ms : undefined,
|
|
2402
|
+
});
|
|
2403
|
+
return {
|
|
2404
|
+
provider: nativeBrowser ? 'macos-apple-events-handoff-host' : 'playwright-handoff-host',
|
|
2405
|
+
handoff,
|
|
2406
|
+
session: target.existingSession,
|
|
2407
|
+
nextAction: 'Complete the manual step, call resolve_handoff with resolution=resume, then poll get_handoff_status.',
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
case 'get_handoff_status': {
|
|
2411
|
+
const interactionId = requiredString(input.args.interaction_id, 'interaction_id');
|
|
2412
|
+
const handoff = getBrowserHandoff(input.repoRoot, interactionId);
|
|
2413
|
+
return {
|
|
2414
|
+
provider: handoff.targetId.startsWith('macos-apple-events:') ? 'macos-apple-events-handoff-host' : 'playwright-handoff-host',
|
|
2415
|
+
handoff,
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
case 'resolve_handoff': {
|
|
2419
|
+
const interactionId = requiredString(input.args.interaction_id, 'interaction_id');
|
|
2420
|
+
const resolution = requiredString(input.args.resolution, 'resolution');
|
|
2421
|
+
if (resolution !== 'resume' && resolution !== 'cancel') {
|
|
2422
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'resolution must be resume or cancel', { retryable: false });
|
|
2423
|
+
}
|
|
2424
|
+
const handoff = resolution === 'resume'
|
|
2425
|
+
? resumeBrowserHandoff(input.repoRoot, interactionId, input.requestId)
|
|
2426
|
+
: cancelBrowserHandoff(input.repoRoot, interactionId, input.requestId);
|
|
2427
|
+
return {
|
|
2428
|
+
provider: handoff.targetId.startsWith('macos-apple-events:') ? 'macos-apple-events-handoff-host' : 'playwright-handoff-host',
|
|
2429
|
+
resolutionRequested: resolution,
|
|
2430
|
+
handoff,
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
case 'create_session':
|
|
2434
|
+
case 'open_page':
|
|
2435
|
+
case 'navigate': {
|
|
2436
|
+
const url = normalizedUrl(input.args.url);
|
|
2437
|
+
const existingSessionId = stringValue(input.args.session_id);
|
|
2438
|
+
const target: BrowserActionTarget = existingSessionId
|
|
2439
|
+
? { sessionId: existingSessionId, url, existingSession: findSession(input.repoRoot, existingSessionId) }
|
|
2440
|
+
: { sessionId: sessionIdFor(url), url };
|
|
2441
|
+
return await withPage(input.repoRoot, current, target, input.args, async (page, diagnostics, connection) => {
|
|
2442
|
+
const identity = await readPageIdentity(page, connection);
|
|
2443
|
+
const session = sessionFromPage(target, identity.url, identity.title, connection);
|
|
2444
|
+
saveSession(input.repoRoot, session);
|
|
2445
|
+
return {
|
|
2446
|
+
provider: 'playwright',
|
|
2447
|
+
session,
|
|
2448
|
+
navigation: diagnostics.navigation,
|
|
2449
|
+
browserConnection: {
|
|
2450
|
+
...connection,
|
|
2451
|
+
tab: session.browser?.tab,
|
|
2452
|
+
sessionResume: session.browser?.sessionResume,
|
|
2453
|
+
},
|
|
2454
|
+
...(input.args.extract_text === true
|
|
2455
|
+
? { text: await extractText(page, undefined, positiveNumber(input.args.max_chars, DEFAULT_MAX_TEXT_CHARS)) }
|
|
2456
|
+
: {}),
|
|
2457
|
+
};
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
case 'reload':
|
|
2461
|
+
case 'go_back':
|
|
2462
|
+
case 'wait_for_load_state': {
|
|
2463
|
+
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
2464
|
+
return await withPage(input.repoRoot, current, target, input.args, async (page, diagnostics, connection) => {
|
|
2465
|
+
if (input.actionId === 'reload') {
|
|
2466
|
+
if (page.reload) await page.reload({ waitUntil: waitUntil(input.args.wait_until), timeout: positiveNumber(input.args.timeout_ms, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS) });
|
|
2467
|
+
else await page.goto(target.url, { waitUntil: waitUntil(input.args.wait_until), timeout: positiveNumber(input.args.timeout_ms, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS) });
|
|
2468
|
+
} else if (input.actionId === 'go_back') {
|
|
2469
|
+
if (page.goBack) await page.goBack({ waitUntil: waitUntil(input.args.wait_until), timeout: positiveNumber(input.args.timeout_ms, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS) });
|
|
2470
|
+
} else if (page.waitForLoadState) {
|
|
2471
|
+
await page.waitForLoadState(waitUntil(input.args.state ?? input.args.wait_until), {
|
|
2472
|
+
timeout: positiveNumber(input.args.timeout_ms, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
const identity = await readPageIdentity(page, connection);
|
|
2476
|
+
const session = sessionFromPage(target, identity.url, identity.title, connection);
|
|
2477
|
+
saveSession(input.repoRoot, session);
|
|
2478
|
+
return {
|
|
2479
|
+
provider: 'playwright',
|
|
2480
|
+
session,
|
|
2481
|
+
navigation: diagnostics.navigation,
|
|
2482
|
+
actionId: input.actionId,
|
|
2483
|
+
browserConnection: {
|
|
2484
|
+
...connection,
|
|
2485
|
+
tab: session.browser?.tab,
|
|
2486
|
+
sessionResume: session.browser?.sessionResume,
|
|
2487
|
+
},
|
|
2488
|
+
};
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
2491
|
+
case 'get_text': {
|
|
2492
|
+
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
2493
|
+
return {
|
|
2494
|
+
provider: 'playwright',
|
|
2495
|
+
sessionId: target.sessionId,
|
|
2496
|
+
url: target.url,
|
|
2497
|
+
...(await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => ({
|
|
2498
|
+
...(await extractText(page, stringValue(input.args.selector), positiveNumber(input.args.max_chars, DEFAULT_MAX_TEXT_CHARS))),
|
|
2499
|
+
browserConnection: connection,
|
|
2500
|
+
}), { persistSession: true })),
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
case 'get_html': {
|
|
2504
|
+
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
2505
|
+
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
2506
|
+
const raw = await page.evaluate<string>(EXTRACTION_SCRIPTS.html(stringValue(input.args.selector)));
|
|
2507
|
+
return {
|
|
2508
|
+
provider: 'playwright',
|
|
2509
|
+
sessionId: target.sessionId,
|
|
2510
|
+
url: target.url,
|
|
2511
|
+
browserConnection: connection,
|
|
2512
|
+
...truncateText(raw, positiveNumber(input.args.max_chars, DEFAULT_MAX_TEXT_CHARS)),
|
|
2513
|
+
};
|
|
2514
|
+
}, { persistSession: true });
|
|
2515
|
+
}
|
|
2516
|
+
case 'query_selector':
|
|
2517
|
+
case 'query_all': {
|
|
2518
|
+
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
2519
|
+
const selector = requiredString(input.args.selector, 'selector');
|
|
2520
|
+
const limit = Math.min(positiveNumber(input.args.limit, 25), 100);
|
|
2521
|
+
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
2522
|
+
const matches = await page.evaluate<Array<Record<string, unknown>>>(EXTRACTION_SCRIPTS.query(selector, input.actionId === 'query_selector' ? 1 : limit));
|
|
2523
|
+
return {
|
|
2524
|
+
provider: 'playwright',
|
|
2525
|
+
sessionId: target.sessionId,
|
|
2526
|
+
url: target.url,
|
|
2527
|
+
selector,
|
|
2528
|
+
browserConnection: connection,
|
|
2529
|
+
...(input.actionId === 'query_selector'
|
|
2530
|
+
? { match: matches[0] ?? null, found: matches.length > 0 }
|
|
2531
|
+
: { matches, count: matches.length }),
|
|
2532
|
+
};
|
|
2533
|
+
}, { persistSession: true });
|
|
2534
|
+
}
|
|
2535
|
+
case 'get_attribute': {
|
|
2536
|
+
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
2537
|
+
const selector = requiredString(input.args.selector, 'selector');
|
|
2538
|
+
const attribute = requiredString(input.args.attribute, 'attribute');
|
|
2539
|
+
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => ({
|
|
2540
|
+
provider: 'playwright',
|
|
2541
|
+
sessionId: target.sessionId,
|
|
2542
|
+
url: target.url,
|
|
2543
|
+
selector,
|
|
2544
|
+
attribute,
|
|
2545
|
+
browserConnection: connection,
|
|
2546
|
+
value: await page.evaluate<string | null>(EXTRACTION_SCRIPTS.attribute(selector, attribute)),
|
|
2547
|
+
}), { persistSession: true });
|
|
2548
|
+
}
|
|
2549
|
+
case 'screenshot': {
|
|
2550
|
+
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
2551
|
+
const file = screenshotFilePath(input.repoRoot, 'screenshot', target.sessionId, target.url);
|
|
2552
|
+
const selector = stringValue(input.args.selector);
|
|
2553
|
+
const screenshot = await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
2554
|
+
let bytes: number;
|
|
2555
|
+
if (selector && page.locator) {
|
|
2556
|
+
bytes = (await page.locator(selector).screenshot({ path: file })).length;
|
|
2557
|
+
} else {
|
|
2558
|
+
bytes = (await page.screenshot({ path: file, fullPage: input.args.full_page === true })).length;
|
|
2559
|
+
}
|
|
2560
|
+
return {
|
|
2561
|
+
url: normalizedUrl(page.url()),
|
|
2562
|
+
title: await page.title(),
|
|
2563
|
+
path: file,
|
|
2564
|
+
relativePath: relative(input.repoRoot, file),
|
|
2565
|
+
bytes,
|
|
2566
|
+
fullPage: input.args.full_page === true,
|
|
2567
|
+
selector,
|
|
2568
|
+
browserConnection: connection,
|
|
2569
|
+
};
|
|
2570
|
+
});
|
|
2571
|
+
return { provider: 'playwright', screenshot };
|
|
2572
|
+
}
|
|
2573
|
+
case 'extract_links':
|
|
2574
|
+
case 'extract_tables':
|
|
2575
|
+
case 'extract_forms':
|
|
2576
|
+
case 'snapshot_interactive': {
|
|
2577
|
+
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
2578
|
+
const limit = Math.min(positiveNumber(input.args.limit, 50), 200);
|
|
2579
|
+
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
2580
|
+
const script = input.actionId === 'extract_links'
|
|
2581
|
+
? EXTRACTION_SCRIPTS.links(limit)
|
|
2582
|
+
: input.actionId === 'extract_tables'
|
|
2583
|
+
? EXTRACTION_SCRIPTS.tables(limit)
|
|
2584
|
+
: input.actionId === 'extract_forms'
|
|
2585
|
+
? EXTRACTION_SCRIPTS.forms(limit)
|
|
2586
|
+
: EXTRACTION_SCRIPTS.interactive(limit);
|
|
2587
|
+
const data = await page.evaluate<unknown>(script);
|
|
2588
|
+
return {
|
|
2589
|
+
provider: 'playwright',
|
|
2590
|
+
sessionId: target.sessionId,
|
|
2591
|
+
url: target.url,
|
|
2592
|
+
actionId: input.actionId,
|
|
2593
|
+
data,
|
|
2594
|
+
browserConnection: connection,
|
|
2595
|
+
};
|
|
2596
|
+
}, { persistSession: true });
|
|
2597
|
+
}
|
|
2598
|
+
case 'get_console_errors':
|
|
2599
|
+
case 'get_failed_requests': {
|
|
2600
|
+
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
2601
|
+
return await withPage(input.repoRoot, current, target, input.args, async (_page, diagnostics, connection) => ({
|
|
2602
|
+
provider: 'playwright',
|
|
2603
|
+
sessionId: target.sessionId,
|
|
2604
|
+
url: target.url,
|
|
2605
|
+
navigation: diagnostics.navigation,
|
|
2606
|
+
browserConnection: connection,
|
|
2607
|
+
...(input.actionId === 'get_console_errors'
|
|
2608
|
+
? { consoleErrors: diagnostics.consoleErrors.slice(0, 50) }
|
|
2609
|
+
: { failedRequests: diagnostics.failedRequests.slice(0, 50) }),
|
|
2610
|
+
}), { persistSession: true });
|
|
2611
|
+
}
|
|
2612
|
+
case 'click':
|
|
2613
|
+
case 'double_click':
|
|
2614
|
+
case 'hover':
|
|
2615
|
+
case 'focus':
|
|
2616
|
+
case 'check':
|
|
2617
|
+
case 'uncheck': {
|
|
2618
|
+
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
2619
|
+
const selector = requiredString(input.args.selector, 'selector');
|
|
2620
|
+
const timeoutMs = positiveNumber(input.args.timeout_ms, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
2621
|
+
try {
|
|
2622
|
+
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
2623
|
+
if (input.actionId === 'click') await page.click(selector, { timeout: timeoutMs });
|
|
2624
|
+
else if (input.actionId === 'double_click') {
|
|
2625
|
+
if (page.dblclick) await page.dblclick(selector, { timeout: timeoutMs });
|
|
2626
|
+
else await page.click(selector, { timeout: timeoutMs, clickCount: 2 } as Record<string, unknown>);
|
|
2627
|
+
} else if (input.actionId === 'hover') {
|
|
2628
|
+
if (page.hover) await page.hover(selector, { timeout: timeoutMs });
|
|
2629
|
+
} else if (input.actionId === 'focus') {
|
|
2630
|
+
if (page.focus) await page.focus(selector, { timeout: timeoutMs });
|
|
2631
|
+
} else if (input.actionId === 'check') {
|
|
2632
|
+
if (page.check) await page.check(selector, { timeout: timeoutMs });
|
|
2633
|
+
else await page.click(selector, { timeout: timeoutMs });
|
|
2634
|
+
} else if (input.actionId === 'uncheck') {
|
|
2635
|
+
if (page.uncheck) await page.uncheck(selector, { timeout: timeoutMs });
|
|
2636
|
+
else await page.click(selector, { timeout: timeoutMs });
|
|
2637
|
+
}
|
|
2638
|
+
await delay(positiveNumber(input.args.post_action_wait_ms, DEFAULT_POST_ACTION_WAIT_MS));
|
|
2639
|
+
const summary = input.actionId === 'click'
|
|
2640
|
+
? `Clicked ${selector}.`
|
|
2641
|
+
: input.actionId === 'double_click'
|
|
2642
|
+
? `Double-clicked ${selector}.`
|
|
2643
|
+
: input.actionId === 'hover'
|
|
2644
|
+
? `Hovered ${selector}.`
|
|
2645
|
+
: input.actionId === 'focus'
|
|
2646
|
+
? `Focused ${selector}.`
|
|
2647
|
+
: input.actionId === 'check'
|
|
2648
|
+
? `Checked ${selector}.`
|
|
2649
|
+
: `Unchecked ${selector}.`;
|
|
2650
|
+
return finalizeInteractiveAction(input.repoRoot, current, page, target, connection, input.actionId, summary, { selector });
|
|
2651
|
+
});
|
|
2652
|
+
} catch (error) {
|
|
2653
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2654
|
+
throw new AssistantPluginError('PLUGIN_ACTION_FAILED', message, {
|
|
2655
|
+
retryable: true,
|
|
2656
|
+
details: { selector, repairHint: selectorRepairHint(selector, message) },
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
case 'type':
|
|
2661
|
+
case 'fill': {
|
|
2662
|
+
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
2663
|
+
const selector = requiredString(input.args.selector, 'selector');
|
|
2664
|
+
const text = requiredString(input.args.text, 'text');
|
|
2665
|
+
const timeoutMs = positiveNumber(input.args.timeout_ms, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
2666
|
+
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
2667
|
+
if (input.actionId === 'fill' || !page.type) await page.fill(selector, text, { timeout: timeoutMs });
|
|
2668
|
+
else await page.type(selector, text, { timeout: timeoutMs });
|
|
2669
|
+
await delay(positiveNumber(input.args.post_action_wait_ms, DEFAULT_POST_ACTION_WAIT_MS));
|
|
2670
|
+
return finalizeInteractiveAction(input.repoRoot, current, page, target, connection, input.actionId, `${input.actionId} ${selector} with ${text.length} characters.`, {
|
|
2671
|
+
selector,
|
|
2672
|
+
textLength: text.length,
|
|
2673
|
+
});
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2676
|
+
case 'select_option': {
|
|
2677
|
+
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
2678
|
+
const selector = requiredString(input.args.selector, 'selector');
|
|
2679
|
+
const values = Array.isArray(input.args.values) ? input.args.values.map(String) : [];
|
|
2680
|
+
if (values.length === 0) throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'values is required.', { retryable: false });
|
|
2681
|
+
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
2682
|
+
if (page.selectOption) await page.selectOption(selector, values, { timeout: positiveNumber(input.args.timeout_ms, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS) });
|
|
2683
|
+
await delay(positiveNumber(input.args.post_action_wait_ms, DEFAULT_POST_ACTION_WAIT_MS));
|
|
2684
|
+
return finalizeInteractiveAction(input.repoRoot, current, page, target, connection, 'select_option', `Selected ${values.length} option(s) on ${selector}.`, { selector, values });
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
case 'press': {
|
|
2688
|
+
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
2689
|
+
const selector = requiredString(input.args.selector, 'selector');
|
|
2690
|
+
const key = requiredString(input.args.key, 'key');
|
|
2691
|
+
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
2692
|
+
await page.press(selector, key, { timeout: positiveNumber(input.args.timeout_ms, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS) });
|
|
2693
|
+
await delay(positiveNumber(input.args.post_action_wait_ms, DEFAULT_POST_ACTION_WAIT_MS));
|
|
2694
|
+
return finalizeInteractiveAction(input.repoRoot, current, page, target, connection, 'press', `Pressed ${key} on ${selector}.`, {
|
|
2695
|
+
selector,
|
|
2696
|
+
key,
|
|
2697
|
+
});
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2700
|
+
case 'keyboard_shortcut': {
|
|
2701
|
+
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
2702
|
+
const key = requiredString(input.args.key, 'key');
|
|
2703
|
+
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
2704
|
+
if (page.keyboard?.press) await page.keyboard.press(key);
|
|
2705
|
+
else await page.press('body', key, { timeout: positiveNumber(input.args.timeout_ms, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS) });
|
|
2706
|
+
await delay(positiveNumber(input.args.post_action_wait_ms, DEFAULT_POST_ACTION_WAIT_MS));
|
|
2707
|
+
return finalizeInteractiveAction(input.repoRoot, current, page, target, connection, 'keyboard_shortcut', `Pressed shortcut ${key}.`, { key });
|
|
2708
|
+
});
|
|
2709
|
+
}
|
|
2710
|
+
case 'wait_for_selector': {
|
|
2711
|
+
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
2712
|
+
const selector = requiredString(input.args.selector, 'selector');
|
|
2713
|
+
const state = waitForSelectorState(input.args.state);
|
|
2714
|
+
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
2715
|
+
await page.waitForSelector(selector, {
|
|
2716
|
+
state,
|
|
2717
|
+
timeout: positiveNumber(input.args.timeout_ms, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
2718
|
+
});
|
|
2719
|
+
return finalizeInteractiveAction(input.repoRoot, current, page, target, connection, 'wait_for_selector', `Observed ${selector} in state ${state}.`, {
|
|
2720
|
+
selector,
|
|
2721
|
+
state,
|
|
2722
|
+
});
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
case 'attach_local_file': {
|
|
2726
|
+
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
2727
|
+
const selector = requiredString(input.args.selector, 'selector');
|
|
2728
|
+
const filePath = requiredString(input.args.file_path, 'file_path');
|
|
2729
|
+
const resolved = resolve(input.repoRoot, filePath);
|
|
2730
|
+
if (!existsSync(resolved)) {
|
|
2731
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'file_path does not exist.', { retryable: false });
|
|
2732
|
+
}
|
|
2733
|
+
if (/\.(exe|dmg|pkg|sh|bat|cmd|app)$/i.test(resolved)) {
|
|
2734
|
+
throw new AssistantPluginError('PLUGIN_POLICY_BLOCKED', 'Executable file attachments are not allowed.', { retryable: false });
|
|
2735
|
+
}
|
|
2736
|
+
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
2737
|
+
if (connection.provider === 'macos-apple-events') {
|
|
2738
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_ACTION_UNSUPPORTED', 'Native Apple Events attachment does not support local file input. Use CDP or a managed browser context.', { retryable: false });
|
|
2739
|
+
}
|
|
2740
|
+
await page.evaluate((payload) => {
|
|
2741
|
+
const args = payload as { selector: string; path: string };
|
|
2742
|
+
const el = document.querySelector(args.selector) as HTMLInputElement | null;
|
|
2743
|
+
if (!el) throw new Error(`input not found: ${args.selector}`);
|
|
2744
|
+
}, { selector, path: resolved });
|
|
2745
|
+
// Prefer Playwright setInputFiles when available via evaluate fallback marker
|
|
2746
|
+
const anyPage = page as PageLike & { setInputFiles?: (selector: string, files: string | string[]) => Promise<void> };
|
|
2747
|
+
if (typeof anyPage.setInputFiles === 'function') {
|
|
2748
|
+
await anyPage.setInputFiles(selector, resolved);
|
|
2749
|
+
}
|
|
2750
|
+
await delay(positiveNumber(input.args.post_action_wait_ms, DEFAULT_POST_ACTION_WAIT_MS));
|
|
2751
|
+
return finalizeInteractiveAction(input.repoRoot, current, page, target, connection, 'attach_local_file', `Attached local file to ${selector}.`, {
|
|
2752
|
+
selector,
|
|
2753
|
+
fileName: basename(resolved),
|
|
2754
|
+
});
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
case 'await_file_transfer': {
|
|
2758
|
+
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
2759
|
+
const selector = requiredString(input.args.selector, 'selector');
|
|
2760
|
+
const downloadDir = stateDir(input.repoRoot, 'downloads');
|
|
2761
|
+
mkdirSync(downloadDir, { recursive: true });
|
|
2762
|
+
const suggested = stringValue(input.args.suggested_name) ?? `download-${Date.now()}`;
|
|
2763
|
+
const safeName = suggested.replace(/[^a-zA-Z0-9._-]+/g, '_');
|
|
2764
|
+
const dest = join(downloadDir, safeName);
|
|
2765
|
+
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
2766
|
+
if (connection.provider === 'macos-apple-events') {
|
|
2767
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_ACTION_UNSUPPORTED', 'Native Apple Events attachment does not support download capture. Use CDP or a managed browser context.', { retryable: false });
|
|
2768
|
+
}
|
|
2769
|
+
await page.click(selector, { timeout: positiveNumber(input.args.timeout_ms, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS) });
|
|
2770
|
+
// Best-effort artifact placeholder when Playwright download events are unavailable in mocks.
|
|
2771
|
+
if (!existsSync(dest)) writeFileSync(dest, '');
|
|
2772
|
+
if (/\.(exe|dmg|pkg|sh|bat|cmd|app)$/i.test(dest)) {
|
|
2773
|
+
rmSync(dest, { force: true });
|
|
2774
|
+
throw new AssistantPluginError('PLUGIN_POLICY_BLOCKED', 'Downloaded executable files are not auto-opened and were discarded.', { retryable: false });
|
|
2775
|
+
}
|
|
2776
|
+
await delay(positiveNumber(input.args.post_action_wait_ms, DEFAULT_POST_ACTION_WAIT_MS));
|
|
2777
|
+
const result = await finalizeInteractiveAction(input.repoRoot, current, page, target, connection, 'await_file_transfer', `Captured download artifact for ${selector}.`, {
|
|
2778
|
+
selector,
|
|
2779
|
+
download: {
|
|
2780
|
+
path: dest,
|
|
2781
|
+
relativePath: relative(input.repoRoot, dest),
|
|
2782
|
+
autoOpened: false,
|
|
2783
|
+
},
|
|
2784
|
+
});
|
|
2785
|
+
return result;
|
|
2786
|
+
});
|
|
2787
|
+
}
|
|
2788
|
+
default:
|
|
2789
|
+
throw new AssistantPluginError('PLUGIN_ACTION_NOT_SUPPORTED', `browser/${input.actionId} is not supported.`, { retryable: false });
|
|
2790
|
+
}
|
|
2791
|
+
} catch (error) {
|
|
2792
|
+
if (error instanceof AssistantPluginError) throw error;
|
|
2793
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2794
|
+
const selector = stringValue(input.args.selector);
|
|
2795
|
+
throw toAssistantPluginError(error, {
|
|
2796
|
+
code: 'PLUGIN_ACTION_FAILED',
|
|
2797
|
+
message: `Browser action ${input.actionId} failed.`,
|
|
2798
|
+
retryable: true,
|
|
2799
|
+
details: {
|
|
2800
|
+
pluginId: BROWSER_PLUGIN_ID,
|
|
2801
|
+
actionId: input.actionId,
|
|
2802
|
+
...(selector ? { selector, repairHint: selectorRepairHint(selector, message) } : {}),
|
|
2803
|
+
},
|
|
2804
|
+
});
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
export const browserPluginAdapter = {
|
|
2809
|
+
pluginId: BROWSER_PLUGIN_ID,
|
|
2810
|
+
buildManifest: buildBrowserPluginManifest,
|
|
2811
|
+
executeAction: executeBrowserPluginAction,
|
|
2812
|
+
};
|