@moretea-labs/forge 1.6.0 → 1.7.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 +4 -0
- package/CHANGELOG.md +39 -0
- package/CLAUDE.md +4 -0
- package/README.md +2 -2
- package/README.zh-CN.md +1 -1
- package/SKILL.md +2 -2
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/agents/openai.yaml +1 -1
- package/assets/hooks/hook-input.sh +11 -8
- package/assets/hooks/lib/session-state.sh +10 -7
- package/assets/hooks/lib/workflow-state.sh +13 -177
- package/assets/hooks/post-tool-observer.sh +8 -2
- package/assets/hooks/prompt-guard.sh +5 -81
- package/assets/hooks/session-start-context.sh +56 -1
- package/assets/partials/05-workflow.partial.md +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +2 -0
- package/assets/partials-agents/03-orchestration.partial.md +1 -1
- package/assets/plugin-registry.v1.json +3 -3
- package/assets/reference-configs/document-generation.md +1 -1
- package/assets/reference-configs/harness-overview.md +1 -1
- package/assets/reference-configs/sprint-contracts.md +3 -3
- package/assets/skill-version.json +2 -2
- package/assets/templates/contract.template.md +2 -7
- package/assets/templates/helpers/capture-plan.sh +2 -2
- package/assets/templates/helpers/check-architecture-sync.sh +17 -0
- package/assets/templates/helpers/check-task-workflow.sh +4 -4
- package/assets/templates/helpers/contract-worktree.sh +1 -21
- package/assets/templates/helpers/ensure-task-workflow.sh +11 -36
- package/assets/templates/helpers/inspect-project-state.ts +8 -6
- package/assets/templates/helpers/migrate-project-template.sh +22 -45
- package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
- package/assets/templates/helpers/new-plan.sh +1 -1
- package/assets/templates/helpers/plan-to-todo.sh +12 -37
- package/assets/templates/helpers/ship-worktrees.sh +2 -29
- package/assets/templates/helpers/verify-contract.sh +1 -111
- package/assets/templates/helpers/verify-sprint.sh +3 -180
- package/assets/templates/helpers/workflow-contract.ts +5 -1
- package/assets/templates/plan.template.md +1 -1
- package/assets/templates/review.template.md +9 -34
- package/assets/workflow-contract.v1.json +10 -19
- package/docs/README.md +10 -10
- package/docs/architecture/CURRENT.md +178 -0
- package/docs/architecture/EVOLUTION.md +42 -0
- package/docs/architecture/current/README.md +4 -156
- package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +2 -2
- package/docs/architecture/decisions/20260823-immediate-controller-relay.md +53 -0
- package/docs/architecture/decisions/20260826-browser-runtime-v2.md +73 -0
- package/docs/architecture/history/README.md +7 -0
- package/docs/architecture/{chatgpt-handoff-facade.md → history/chatgpt-handoff-facade.md} +4 -0
- package/docs/architecture/{global-hook-runtime.md → history/global-hook-runtime.md} +7 -3
- package/docs/architecture/{ios-semantic-automation-provider-v2.md → history/ios-semantic-automation-provider-v2.md} +6 -2
- package/docs/architecture/history.md +3 -34
- package/docs/architecture/index.md +15 -98
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
- package/docs/architecture/reviews/2026-08-22-semantic-harness-convergence-acceptance.md +39 -0
- package/docs/architecture/versions/1.6.md +38 -0
- package/docs/architecture/versions/1.7.md +41 -0
- package/docs/forge-chatgpt-browser-engine.md +2 -2
- package/docs/forge-chatgpt-mcp-setup.md +4 -28
- package/docs/forge-chatgpt-supervised-automation.md +3 -3
- package/docs/forge-github-issue-launcher.md +1 -1
- package/docs/forge-plugin-management.md +1 -1
- package/docs/forge-runtime-self-healing-loop.md +1 -1
- package/docs/operations/controller-browser-plugin.md +26 -6
- package/docs/operations/controller-performance-and-502.md +143 -0
- package/docs/operations/controller-reliability-runbook.md +1 -1
- package/docs/operations/features.md +2 -2
- package/docs/operations/github-repository.md +1 -1
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/mcp-tool-exposure.md +5 -3
- package/docs/wiki/Architecture.md +6 -6
- package/docs/wiki/Implementation.md +2 -2
- package/docs/wiki/Integrations.md +2 -2
- package/docs/wiki/Work-Lifecycle.md +1 -1
- package/package.json +33 -11
- package/scripts/activate-source-baseline.ts +17 -13
- package/scripts/architecture-event.ts +2 -820
- package/scripts/architecture-queue.sh +2 -645
- package/scripts/archive-architecture-request.sh +2 -242
- package/scripts/archive-workflow.sh +1 -291
- package/scripts/benchmark-background-check-overlap.ts +228 -0
- package/scripts/benchmark-mcp-host.sh +117 -0
- package/scripts/benchmark-mcp-transport.ts +170 -0
- package/scripts/benchmark-quality-harness.ts +146 -0
- package/scripts/benchmark-route-session-concurrency.ts +207 -6
- package/scripts/benchmark-thin-harness-mcp-e2e.ts +7 -1
- package/scripts/benchmark-typescript-navigation.ts +186 -0
- package/scripts/benchmark-work-efficiency.ts +334 -0
- package/scripts/capability-config.ts +2 -392
- package/scripts/capability-resolver.ts +2 -503
- package/scripts/capture-plan.sh +1 -332
- package/scripts/check-agent-tooling.sh +2 -1558
- package/scripts/check-architecture-sync.sh +2 -371
- package/scripts/check-brain-manifest.sh +2 -253
- package/scripts/check-context-files.sh +2 -47
- package/scripts/check-deploy-sql-order.sh +2 -115
- package/scripts/check-mcp-compatibility.ts +14 -0
- package/scripts/check-runtime-architecture.mjs +349 -79
- package/scripts/check-skill-version.ts +3 -191
- package/scripts/check-source-duplication.mjs +61 -0
- package/scripts/check-stable-baseline.ts +92 -0
- package/scripts/check-task-sync.sh +2 -63
- package/scripts/check-task-workflow.sh +2 -1000
- package/scripts/codex-handoff-resume.sh +2 -267
- package/scripts/context-contract-sync.sh +2 -514
- package/scripts/contract-run.ts +2 -463
- package/scripts/contract-worktree.sh +1 -722
- package/scripts/ensure-task-workflow.sh +1 -1225
- package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
- package/scripts/inspect-project-state.ts +4 -279
- package/scripts/install-standalone-recovery.ts +24 -0
- package/scripts/lib/project-init-lib.sh +22 -46
- package/scripts/lib/run-forge-helper.sh +19 -0
- package/scripts/lib/run-forge-helper.ts +26 -0
- package/scripts/migrate-project-template.sh +1 -1189
- package/scripts/migrate-workflow-docs.ts +4 -412
- package/scripts/new-plan.sh +1 -180
- package/scripts/new-spec.sh +1 -47
- package/scripts/new-sprint.sh +1 -46
- package/scripts/plan-to-todo.sh +1 -796
- package/scripts/prepare-codex-handoff.sh +2 -201
- package/scripts/prepare-handoff.sh +1 -78
- package/scripts/refresh-current-status.sh +2 -467
- package/scripts/run-governed-gate.ts +5 -1
- package/scripts/select-agent-context-blocks.sh +2 -64
- package/scripts/ship-worktrees.sh +1 -558
- package/scripts/smoke-ios-physical-device.ts +4 -2
- package/scripts/smoke-runtime-recovery.ts +1 -35
- package/scripts/smoke-schedule-engine.ts +1 -20
- package/scripts/sprint-backlog.sh +1 -834
- package/scripts/stage-runtime-release.ts +2 -1
- package/scripts/summarize-failures.sh +2 -123
- package/scripts/switch-plan.sh +1 -150
- package/scripts/sync-brain-docs.sh +2 -379
- package/scripts/sync-generated-authority.mjs +175 -0
- package/scripts/verify-contract.sh +2 -703
- package/scripts/verify-forge-runtime.sh +0 -1
- package/scripts/verify-sprint.sh +1 -677
- package/scripts/workflow-contract.ts +4 -205
- package/scripts/workstream-sync.sh +2 -261
- package/src/cli/agent-jobs/executor-health.ts +1 -1
- package/src/cli/commands/init-hook.ts +3 -3
- package/src/cli/commands/mcp.ts +9 -5
- package/src/cli/commands/plugin.ts +12 -3
- package/src/cli/commands/recovery.ts +25 -3
- package/src/cli/commands/runtime.ts +55 -3
- package/src/cli/commands/security.ts +12 -1
- package/src/cli/commands/setup-profile.ts +31 -21
- package/src/cli/controller/check-runner.ts +113 -5
- package/src/cli/controller/composite-result.ts +1 -1
- package/src/cli/controller/context/focus.ts +51 -0
- package/src/cli/controller/context/instruction-context.ts +114 -0
- package/src/cli/controller/context/known-paths.ts +76 -0
- package/src/cli/controller/context/query-planning.ts +110 -0
- package/src/cli/controller/context/rule-resolution.ts +532 -0
- package/src/cli/controller/context/source-materializer.ts +278 -0
- package/src/cli/controller/context/structural.ts +16 -0
- package/src/cli/controller/context/types.ts +286 -0
- package/src/cli/controller/context-pack.ts +828 -487
- package/src/cli/controller/launch-agents.ts +77 -2
- package/src/cli/controller/operational-plan.ts +2 -2
- package/src/cli/controller/work-mode.ts +6 -4
- package/src/cli/editing/edit-session.ts +60 -7
- package/src/cli/github/contracts.ts +16 -0
- package/src/cli/github/github.ts +135 -16
- package/src/cli/github/session.ts +2 -1
- package/src/cli/installer/managed-entries.ts +15 -1
- package/src/cli/local-bridge/console-automations.ts +54 -78
- package/src/cli/local-bridge/console-view-models.ts +1 -35
- package/src/cli/local-bridge/facade-api.ts +37 -264
- package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
- package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +23 -13
- package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
- package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +6 -4
- package/src/cli/local-bridge/frontend/src/lib/api.ts +1 -6
- package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
- package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -2
- package/src/cli/local-bridge/frontend/src/styles.css +1 -1
- package/src/cli/local-bridge/frontend/src/types.ts +5 -8
- package/src/cli/local-bridge/job-store.ts +1 -490
- package/src/cli/local-bridge/server.ts +2 -515
- package/src/cli/local-bridge/ui-dist/app.css +1 -1
- package/src/cli/local-bridge/ui-dist/app.js +3703 -3757
- package/src/cli/mcp/auth.ts +5 -5
- package/src/cli/mcp/instructions.ts +3 -2
- package/src/cli/mcp/legacy-tool-service.ts +10 -222
- package/src/cli/mcp/multi-repository.ts +16 -5
- package/src/cli/mcp/oauth.ts +175 -26
- package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
- package/src/cli/mcp/repository-tools.ts +282 -146
- package/src/cli/mcp/server.ts +586 -116
- package/src/cli/mcp/tool-capability-cache.ts +1 -1
- package/src/cli/mcp/tool-contract.ts +14 -0
- package/src/cli/mcp/toolset-selection.ts +1 -1
- package/src/cli/mcp/toolset.ts +44 -7
- package/src/cli/mcp/transports/http.ts +124 -14
- package/src/cli/mcp/transports/session-registry.ts +43 -5
- package/src/cli/mcp/types.ts +3 -3
- package/src/cli/repositories/command-classifier.ts +103 -21
- package/src/cli/repositories/command-executor.ts +78 -526
- package/src/cli/repositories/command-process.ts +108 -0
- package/src/cli/repositories/command-scope.ts +99 -41
- package/src/cli/repositories/controller-home.ts +7 -8
- package/src/cli/repositories/git-command-executor.ts +2 -0
- package/src/cli/repositories/locks.ts +53 -16
- package/src/cli/repositories/repository-snapshot.ts +124 -0
- package/src/cli/repositories/selected-path-actions.ts +17 -9
- package/src/cli/repositories/structured-git.ts +118 -6
- package/src/cli/repository/inspector.ts +563 -65
- package/src/cli/repository/session-cache.ts +58 -17
- package/src/cli/v81-entry.ts +1 -4
- package/src/effects/fs-transaction.ts +85 -17
- package/src/runtime/context/codegraph-read-provider.ts +233 -2
- package/src/runtime/context/codegraph-sidecar.cjs +74 -40
- package/src/runtime/context/generic-lsp-provider.ts +355 -0
- package/src/runtime/context/lsp-client.ts +283 -0
- package/src/runtime/context/semantic-navigation-contract.ts +61 -0
- package/src/runtime/context/semantic-navigation.ts +223 -0
- package/src/runtime/context/swift-navigation.ts +565 -0
- package/src/runtime/context/typescript-navigation.ts +226 -0
- package/src/runtime/control-plane/dispatch-priority.ts +28 -0
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +48 -16
- package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
- package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
- package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
- package/src/runtime/control-plane/execution/session-store.ts +18 -0
- package/src/runtime/control-plane/execution/verification-evidence.ts +58 -3
- package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
- package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
- package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
- package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
- package/src/runtime/control-plane/execution/work-handle-authority.ts +96 -0
- package/src/runtime/control-plane/execution/work-handle-store.ts +27 -2
- package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
- package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
- package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
- package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
- package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
- package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
- package/src/runtime/control-plane/execution/work-task-receipt.ts +22 -18
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +156 -11
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +22 -14
- package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
- package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +90 -9
- package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
- package/src/runtime/control-plane/facade/controller-round-relay.ts +1118 -0
- package/src/runtime/control-plane/facade/controller-session-store.ts +302 -12
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +17 -1
- package/src/runtime/control-plane/facade/goal-workloop.ts +928 -190
- package/src/runtime/control-plane/facade/index.ts +3 -0
- package/src/runtime/control-plane/facade/plan-contract-store.ts +275 -42
- package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
- package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
- package/src/runtime/control-plane/facade/semantic-admission.ts +205 -0
- package/src/runtime/control-plane/facade/suggested-actions.ts +5 -1
- package/src/runtime/control-plane/facade/types.ts +90 -10
- package/src/runtime/control-plane/facade/work-contract-store.ts +157 -261
- package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
- package/src/runtime/control-plane/generated-cache-retention.ts +271 -0
- package/src/runtime/control-plane/global-scheduler/config.ts +44 -0
- package/src/runtime/control-plane/global-scheduler/dispatch-capacity.ts +80 -0
- package/src/runtime/control-plane/global-scheduler/durable-admission.ts +46 -0
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +152 -0
- package/src/runtime/control-plane/global-scheduler/projection-refresh.ts +84 -0
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -3
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +278 -543
- package/src/runtime/control-plane/global-scheduler/source-scan.ts +59 -0
- package/src/runtime/control-plane/global-scheduler/state.ts +88 -0
- package/src/runtime/control-plane/global-scheduler/worker-attachment.ts +57 -0
- package/src/runtime/control-plane/global-scheduler/worker-exit-decision.ts +38 -0
- package/src/runtime/control-plane/global-scheduler/worker-exit-reconciler.ts +175 -0
- package/src/runtime/control-plane/global-scheduler/worker-launch.ts +111 -0
- package/src/runtime/control-plane/global-scheduler/worker-lifecycle-store.ts +64 -0
- package/src/runtime/control-plane/global-scheduler/worker-lifecycle.ts +150 -0
- package/src/runtime/control-plane/global-scheduler/worker-process.ts +119 -0
- package/src/runtime/control-plane/global-scheduler/worker-stderr.ts +60 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +63 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +365 -117
- package/src/runtime/control-plane/launcher/launch-reservation-store.ts +89 -0
- package/src/runtime/control-plane/launcher/thin-launcher.ts +244 -11
- package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
- package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
- package/src/runtime/control-plane/persistence/sqlite-store.ts +33 -2
- package/src/runtime/control-plane/release-retention.ts +62 -2
- package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
- package/src/runtime/control-plane/routing/route-policy.ts +24 -21
- package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
- package/src/runtime/control-plane/runtime-status-client.ts +0 -2
- package/src/runtime/diagnostics/mcp-timing.ts +13 -0
- package/src/runtime/diagnostics/mcp-transport-benchmark.ts +129 -0
- package/src/runtime/diagnostics/performance.ts +24 -1
- package/src/runtime/evidence/event-ledger.ts +1 -1
- package/src/runtime/evidence/process-check-execution.ts +18 -0
- package/src/runtime/execution/jobs/child-reference.ts +6 -2
- package/src/runtime/execution/jobs/store.ts +58 -4
- package/src/runtime/execution/jobs/types.ts +1 -1
- package/src/runtime/execution/managed-workspace.ts +203 -1
- package/src/runtime/execution/process-runtime/check-facade.ts +51 -4
- package/src/runtime/execution/process-runtime/check-result.ts +88 -0
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
- package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
- package/src/runtime/execution/process-runtime/command-facade.ts +367 -40
- package/src/runtime/execution/process-runtime/gc.ts +38 -6
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +1201 -0
- package/src/runtime/execution/process-runtime/persisted-check.ts +51 -5
- package/src/runtime/execution/process-runtime/resource-claims.ts +10 -6
- package/src/runtime/execution/process-runtime/runtime.ts +53 -3
- package/src/runtime/execution/process-runtime/store.ts +1 -0
- package/src/runtime/execution/process-runtime/types.ts +4 -12
- package/src/runtime/execution/thin-harness/async-process.ts +12 -2
- package/src/runtime/execution/thin-harness/search-worker.ts +54 -5
- package/src/runtime/execution/work-activity.ts +21 -0
- package/src/runtime/execution/workers/executor.ts +18 -61
- package/src/runtime/gateway/mcp/execution-tools.ts +35 -1852
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
- package/src/runtime/gateway/mcp/process-tools.ts +83 -37
- package/src/runtime/gateway/mcp/router.ts +186 -84
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +565 -0
- package/src/runtime/gateway/mcp/runtime-tools.ts +2378 -1947
- package/src/runtime/plugins/app-store-connect-adapter.ts +216 -47
- package/src/runtime/plugins/browser-adapter.ts +2662 -829
- package/src/runtime/plugins/browser-automation-service.ts +10 -0
- package/src/runtime/plugins/browser-handoff-host.ts +2 -25
- package/src/runtime/plugins/browser-handoff.ts +110 -5
- package/src/runtime/plugins/browser-macos-bridge.ts +701 -63
- package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
- package/src/runtime/plugins/browser-native-session-service.ts +222 -0
- package/src/runtime/plugins/browser-node-bridge.ts +6 -34
- package/src/runtime/plugins/browser-provider-registry.ts +109 -0
- package/src/runtime/plugins/browser-runtime-contract.ts +154 -0
- package/src/runtime/plugins/browser-runtime.ts +193 -0
- package/src/runtime/plugins/browser-session-authority.ts +332 -0
- package/src/runtime/plugins/browser-session-store.ts +121 -0
- package/src/runtime/plugins/browser-session-types.ts +98 -0
- package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +10 -4
- package/src/runtime/plugins/external-adapter.ts +358 -3
- package/src/runtime/plugins/external-unix-socket.ts +7 -1
- package/src/runtime/plugins/first-party-registry.ts +2 -0
- package/src/runtime/plugins/github-adapter.ts +125 -0
- package/src/runtime/plugins/ios/agent-device-provider.ts +1 -1
- package/src/runtime/plugins/ios/agent-device-typed-provider.ts +52 -9
- package/src/runtime/plugins/ios/remote-xpc-hid.ts +1171 -0
- package/src/runtime/plugins/ios-adapter.ts +106 -16
- package/src/runtime/plugins/ios-agent-device.ts +234 -64
- package/src/runtime/plugins/ios-physical-device.ts +1143 -392
- package/src/runtime/plugins/lightweight-action.ts +153 -0
- package/src/runtime/plugins/local-system-adapter.ts +123 -6
- package/src/runtime/plugins/macos-capability-broker.ts +47 -9
- package/src/runtime/plugins/plugin-action-sidecar.ts +59 -0
- package/src/runtime/plugins/plugin-management-adapter.ts +49 -8
- package/src/runtime/plugins/resend-adapter.ts +1 -1
- package/src/runtime/plugins/store.ts +330 -33
- package/src/runtime/plugins/types.ts +38 -3
- package/src/runtime/plugins/xiaohongshu-publish.ts +515 -0
- package/src/runtime/projections/git-status-sampler.ts +16 -16
- package/src/runtime/projections/materialized-view.ts +28 -10
- package/src/runtime/recovery/maintenance-executor.ts +426 -25
- package/src/runtime/root/mcp-transport.ts +180 -10
- package/src/runtime/root/package-connector-service.ts +230 -10
- package/src/runtime/root/package-runtime-release.ts +143 -17
- package/src/runtime/root/package-runtime-service.ts +461 -22
- package/src/runtime/root/release-manifest.ts +144 -18
- package/src/runtime/root/release-materialize.ts +125 -32
- package/src/runtime/root/release-store.ts +48 -11
- package/src/runtime/root/runtime.ts +23 -5
- package/src/runtime/root/service.ts +100 -29
- package/src/runtime/root/types.ts +12 -0
- package/src/runtime/safe-tooling/ios-development.ts +321 -63
- package/src/runtime/shared/trusted-executable.ts +62 -0
- package/src/runtime/shared/trusted-node-executable.ts +48 -0
- package/src/runtime/standalone-recovery/core.ts +1079 -240
- package/src/runtime/standalone-recovery/entry.ts +100 -24
- package/src/runtime/standalone-recovery/installer.ts +20 -4
- package/src/runtime/standalone-recovery/service-labels.ts +2 -0
- package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +160 -0
- package/src/runtime/workflow/schedules/browser-probe.ts +81 -3
- package/src/runtime/workflow/schedules/engine.ts +364 -202
- package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
- package/src/runtime/workflow/schedules/settlement.ts +64 -17
- package/src/runtime/workflow/schedules/store.ts +209 -19
- package/src/runtime/workflow/schedules/types.ts +2 -4
- package/src/runtime/workflow/schedules/work-continuation.ts +26 -13
- package/src/testing/test-governance.ts +8 -40
- package/assets/reference-configs/heartbeat-triage.md +0 -57
- package/assets/templates/helpers/harness-trace-grade.sh +0 -150
- package/assets/templates/helpers/heartbeat-triage.sh +0 -361
- package/assets/templates/helpers/maintenance-triage.sh +0 -167
- package/docs/architecture/current/approved-target-architecture.zh-CN.md +0 -1501
- package/docs/architecture/current/architecture-invariants.md +0 -331
- package/docs/architecture/current/automation-and-schedule-engine.md +0 -459
- package/docs/architecture/current/codegraph-context-provider.md +0 -238
- package/docs/architecture/current/control-plane-authority-inventory.md +0 -94
- package/docs/architecture/current/controller-repository-decoupling.md +0 -148
- package/docs/architecture/current/dispatch-and-agent-strategy.md +0 -373
- package/docs/architecture/current/entity-model.md +0 -612
- package/docs/architecture/current/external-plugin-governance.md +0 -396
- package/docs/architecture/current/failure-recovery.md +0 -447
- package/docs/architecture/current/governance.md +0 -213
- package/docs/architecture/current/human-interaction-plane.md +0 -87
- package/docs/architecture/current/implementation-status.md +0 -128
- package/docs/architecture/current/job-and-run-lifecycle.md +0 -413
- package/docs/architecture/current/legacy-governance.md +0 -58
- package/docs/architecture/current/local-workspace-target-grants.md +0 -70
- package/docs/architecture/current/migration-roadmap.md +0 -105
- package/docs/architecture/current/multi-repository-execution.md +0 -371
- package/docs/architecture/current/operations-runbook.md +0 -45
- package/docs/architecture/current/peer-supercontroller-runtime.md +0 -32
- package/docs/architecture/current/personal-assistant-plugin-baseline.md +0 -206
- package/docs/architecture/current/repository-access-modes.md +0 -102
- package/docs/architecture/current/route-policy.md +0 -15
- package/docs/architecture/current/route-session-concurrency.md +0 -85
- package/docs/architecture/current/runtime-architecture-simplification.md +0 -214
- package/docs/architecture/current/runtime-directory-map.md +0 -49
- package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +0 -85
- package/docs/architecture/current/scheduler-and-resource-claims.md +0 -422
- package/docs/architecture/current/session-aware-execution-and-authorization.md +0 -77
- package/docs/architecture/current/system-overview.md +0 -169
- package/docs/architecture/current/target-requirements-traceability.md +0 -202
- package/docs/architecture/current/thin-harness-governance-policy.md +0 -32
- package/docs/architecture/current/thin-harness-v1.md +0 -321
- package/docs/architecture/current/verification-and-release-gates.md +0 -391
- package/docs/forge-autonomous-goal-loop.md +0 -9
- package/docs/forge-provider-configuration.md +0 -7
- package/docs/operations/chatgpt-personal-assistant-runtime.md +0 -93
- package/docs/operations/gmail-assistant-routines.md +0 -186
- package/docs/operations/personal-assistant-operator-guide.md +0 -61
- package/docs/operations/provider-configuration.md +0 -31
- package/scripts/harness-trace-grade.sh +0 -150
- package/scripts/heartbeat-triage.sh +0 -361
- package/scripts/maintenance-triage.sh +0 -167
- package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +0 -2
- package/src/cli/repositories/goal-registry.ts +0 -239
- package/src/runtime/assistant/action-proposals.ts +0 -279
- package/src/runtime/assistant/intent.ts +0 -395
- package/src/runtime/assistant/model-provider.ts +0 -460
- package/src/runtime/assistant/openapi.ts +0 -223
- package/src/runtime/assistant/policy.ts +0 -89
- package/src/runtime/assistant/readiness.ts +0 -258
- package/src/runtime/assistant/routine-runtime.ts +0 -477
- package/src/runtime/assistant/schedule-binding.ts +0 -171
- package/src/runtime/assistant/standing-grants.ts +0 -375
- package/src/runtime/assistant/store.ts +0 -243
- package/src/runtime/assistant/types.ts +0 -135
- package/src/runtime/control-plane/goal-loop/config-facade.ts +0 -700
- package/src/runtime/control-plane/goal-loop/config-store.ts +0 -570
- package/src/runtime/control-plane/goal-loop/config-types.ts +0 -213
- package/src/runtime/control-plane/goal-loop/executor-router.ts +0 -109
- package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +0 -149
- package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +0 -304
- package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +0 -1001
- package/src/runtime/control-plane/goal-loop/handoff-packet.ts +0 -160
- package/src/runtime/control-plane/goal-loop/index.ts +0 -13
- package/src/runtime/control-plane/goal-loop/local-tools.ts +0 -290
- package/src/runtime/control-plane/goal-loop/policy-gates.ts +0 -203
- package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +0 -93
- package/src/runtime/control-plane/goal-loop/provider-registry.ts +0 -545
- package/src/runtime/control-plane/goal-loop/types.ts +0 -378
- package/src/runtime/control-plane/startup-recovery.ts +0 -198
- package/src/runtime/gateway/mcp/resource-policy.ts +0 -186
- package/src/runtime/personal-assistant/gmail-triage-manager.ts +0 -231
- package/src/runtime/personal-assistant/reporting-runtime.ts +0 -344
- package/src/runtime/personal-assistant/triage-runtime.ts +0 -748
- package/src/runtime/workflow/findings/store.ts +0 -204
- package/src/runtime/workflow/findings/types.ts +0 -58
- package/src/runtime/workflow/portfolio/engine.ts +0 -20
- package/src/runtime/workflow/portfolio/store.ts +0 -185
- package/src/runtime/workflow/portfolio/types.ts +0 -29
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { execFile } from 'child_process';
|
|
2
|
-
import { existsSync, writeFileSync } from 'fs';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
+
import { basename, extname } from 'path';
|
|
4
5
|
import {
|
|
5
6
|
callBrowserAutomationBroker,
|
|
6
7
|
captureBrowserAutomationRegion,
|
|
7
8
|
type BrowserAutomationBrokerAction,
|
|
9
|
+
type BrowserAutomationTrustedInput,
|
|
8
10
|
} from './browser-automation-service';
|
|
9
11
|
import { AssistantPluginError } from './errors';
|
|
12
|
+
import type { BrowserNativeAttachAttempt, BrowserNativeProduct } from './browser-session-types';
|
|
10
13
|
|
|
11
14
|
const RECORD_SEPARATOR = String.fromCharCode(30);
|
|
12
15
|
const DEFAULT_NATIVE_TIMEOUT_MS = 5_000;
|
|
13
16
|
const MAX_NATIVE_OUTPUT_BYTES = 1_048_576;
|
|
14
17
|
|
|
15
|
-
export type MacOsBrowserProduct =
|
|
18
|
+
export type MacOsBrowserProduct = BrowserNativeProduct;
|
|
16
19
|
|
|
17
20
|
interface MacOsBrowserDefinition {
|
|
18
21
|
product: MacOsBrowserProduct;
|
|
@@ -27,6 +30,20 @@ export interface MacOsBrowserTabRef {
|
|
|
27
30
|
tabId: string;
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
export interface MacOsBrowserCreateTabNavigationProvenance {
|
|
34
|
+
provenanceVersion: 1;
|
|
35
|
+
requestedUrl: string;
|
|
36
|
+
assignmentAccepted: true;
|
|
37
|
+
acceptedBy?: string;
|
|
38
|
+
observedUrlAfterAssignment?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface MacOsBrowserCreateTabEvidence {
|
|
42
|
+
ref: MacOsBrowserTabRef;
|
|
43
|
+
refSource: 'structured' | 'legacy_value';
|
|
44
|
+
navigation?: MacOsBrowserCreateTabNavigationProvenance;
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
export interface MacOsBrowserMetadata {
|
|
31
48
|
product: MacOsBrowserProduct;
|
|
32
49
|
appName: string;
|
|
@@ -41,15 +58,22 @@ export interface MacOsBrowserMetadata {
|
|
|
41
58
|
loading?: boolean;
|
|
42
59
|
}
|
|
43
60
|
|
|
44
|
-
export interface
|
|
61
|
+
export interface MacOsBrowserTabInventoryEntry {
|
|
62
|
+
windowId: string;
|
|
63
|
+
tabId: string;
|
|
64
|
+
url: string;
|
|
65
|
+
title: string;
|
|
66
|
+
active: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface MacOsBrowserTabInventory {
|
|
45
70
|
product: MacOsBrowserProduct;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
status: 'selected' | 'available' | 'not_installed' | 'not_running' | 'unavailable';
|
|
49
|
-
frontmost?: boolean;
|
|
50
|
-
error?: string;
|
|
71
|
+
tabs: MacOsBrowserTabInventoryEntry[];
|
|
72
|
+
truncated: boolean;
|
|
51
73
|
}
|
|
52
74
|
|
|
75
|
+
export type MacOsBrowserAttachAttempt = BrowserNativeAttachAttempt;
|
|
76
|
+
|
|
53
77
|
export interface MacOsBrowserAttachment {
|
|
54
78
|
metadata: MacOsBrowserMetadata;
|
|
55
79
|
attempts: MacOsBrowserAttachAttempt[];
|
|
@@ -74,7 +98,9 @@ interface MacOsBrowserRuntimeHooks {
|
|
|
74
98
|
// Test-only escape hatches. Production intentionally leaves these undefined so
|
|
75
99
|
// Apple Events and screen capture are attributed to the stable helper process.
|
|
76
100
|
runAppleScript?: (script: string, args: string[], timeoutMs: number) => Promise<string>;
|
|
101
|
+
tabInventory?: (product: MacOsBrowserProduct, timeoutMs: number) => Promise<MacOsBrowserTabInventory>;
|
|
77
102
|
captureRegion?: (region: { x: number; y: number; width: number; height: number }, path: string, timeoutMs: number) => Promise<Buffer>;
|
|
103
|
+
trustedInput?: (request: Extract<BrowserAutomationBrokerAction, { action: 'trusted_input' }>, timeoutMs: number) => Promise<void>;
|
|
78
104
|
}
|
|
79
105
|
|
|
80
106
|
function execFileText(file: string, args: string[], timeoutMs: number): Promise<string> {
|
|
@@ -114,6 +140,87 @@ const defaultRuntimeHooks: MacOsBrowserRuntimeHooks = {
|
|
|
114
140
|
},
|
|
115
141
|
};
|
|
116
142
|
|
|
143
|
+
export function parseMacOsBrowserCreateTabBrokerResult(result: Record<string, unknown>): MacOsBrowserCreateTabEvidence {
|
|
144
|
+
const structuredRef = result.ref;
|
|
145
|
+
let ref: MacOsBrowserTabRef;
|
|
146
|
+
let refSource: MacOsBrowserCreateTabEvidence['refSource'];
|
|
147
|
+
if (structuredRef !== undefined) {
|
|
148
|
+
if (!structuredRef || typeof structuredRef !== 'object' || Array.isArray(structuredRef)) {
|
|
149
|
+
throw new AssistantPluginError('PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR', 'Stable Forge macOS capability broker returned an invalid create-tab ref.', { retryable: true });
|
|
150
|
+
}
|
|
151
|
+
const record = structuredRef as Record<string, unknown>;
|
|
152
|
+
if (typeof record.windowId !== 'string' || !record.windowId.trim() || typeof record.tabId !== 'string' || !record.tabId.trim()) {
|
|
153
|
+
throw new AssistantPluginError('PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR', 'Stable Forge macOS capability broker returned an incomplete create-tab ref.', { retryable: true });
|
|
154
|
+
}
|
|
155
|
+
ref = { windowId: record.windowId, tabId: record.tabId };
|
|
156
|
+
refSource = 'structured';
|
|
157
|
+
if (typeof result.value === 'string') {
|
|
158
|
+
const legacyParts = result.value.split(RECORD_SEPARATOR);
|
|
159
|
+
if (legacyParts.length >= 2) {
|
|
160
|
+
const legacyRef = {
|
|
161
|
+
windowId: parseBrowserId(legacyParts[0] ?? '', 'window id'),
|
|
162
|
+
tabId: parseBrowserId(legacyParts[1] ?? '', 'tab id'),
|
|
163
|
+
};
|
|
164
|
+
if (legacyRef.windowId !== ref.windowId || legacyRef.tabId !== ref.tabId) {
|
|
165
|
+
throw new AssistantPluginError('PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR', 'Stable Forge macOS capability broker returned conflicting structured and legacy create-tab refs.', { retryable: true });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
if (typeof result.value !== 'string') {
|
|
171
|
+
throw new AssistantPluginError('PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR', 'Stable Forge macOS capability broker returned no create-tab ref.', { retryable: true });
|
|
172
|
+
}
|
|
173
|
+
const parts = result.value.split(RECORD_SEPARATOR);
|
|
174
|
+
if (parts.length < 2) throw new AssistantPluginError('PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR', 'Stable Forge macOS capability broker returned incomplete legacy create-tab metadata.', { retryable: true });
|
|
175
|
+
ref = { windowId: parseBrowserId(parts[0] ?? '', 'window id'), tabId: parseBrowserId(parts[1] ?? '', 'tab id') };
|
|
176
|
+
refSource = 'legacy_value';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const navigationValue = result.navigation;
|
|
180
|
+
if (navigationValue === undefined) return { ref, refSource };
|
|
181
|
+
if (!navigationValue || typeof navigationValue !== 'object' || Array.isArray(navigationValue)) {
|
|
182
|
+
throw new AssistantPluginError('PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR', 'Stable Forge macOS capability broker returned invalid create-tab navigation provenance.', { retryable: true });
|
|
183
|
+
}
|
|
184
|
+
const navigation = navigationValue as Record<string, unknown>;
|
|
185
|
+
if (navigation.provenanceVersion !== 1
|
|
186
|
+
|| typeof navigation.requestedUrl !== 'string'
|
|
187
|
+
|| navigation.assignmentAccepted !== true) {
|
|
188
|
+
throw new AssistantPluginError('PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR', 'Stable Forge macOS capability broker returned incomplete create-tab navigation provenance.', { retryable: true });
|
|
189
|
+
}
|
|
190
|
+
if (navigation.acceptedBy !== undefined && typeof navigation.acceptedBy !== 'string') {
|
|
191
|
+
throw new AssistantPluginError('PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR', 'Stable Forge macOS capability broker returned invalid create-tab acceptedBy provenance.', { retryable: true });
|
|
192
|
+
}
|
|
193
|
+
if (navigation.observedUrlAfterAssignment !== undefined && typeof navigation.observedUrlAfterAssignment !== 'string') {
|
|
194
|
+
throw new AssistantPluginError('PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR', 'Stable Forge macOS capability broker returned invalid create-tab observed URL provenance.', { retryable: true });
|
|
195
|
+
}
|
|
196
|
+
const acceptedBy = typeof navigation.acceptedBy === 'string' ? navigation.acceptedBy : undefined;
|
|
197
|
+
const observedUrlAfterAssignment = typeof navigation.observedUrlAfterAssignment === 'string' ? navigation.observedUrlAfterAssignment : undefined;
|
|
198
|
+
return {
|
|
199
|
+
ref,
|
|
200
|
+
refSource,
|
|
201
|
+
navigation: {
|
|
202
|
+
provenanceVersion: 1,
|
|
203
|
+
requestedUrl: navigation.requestedUrl,
|
|
204
|
+
assignmentAccepted: true,
|
|
205
|
+
...(acceptedBy ? { acceptedBy } : {}),
|
|
206
|
+
...(observedUrlAfterAssignment ? { observedUrlAfterAssignment } : {}),
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function runCreateTabAutomation(
|
|
212
|
+
request: Extract<BrowserAutomationBrokerAction, { action: 'create_tab' }>,
|
|
213
|
+
testScript: string,
|
|
214
|
+
testArgs: string[],
|
|
215
|
+
timeoutMs: number,
|
|
216
|
+
): Promise<MacOsBrowserCreateTabEvidence> {
|
|
217
|
+
if (runtimeHooks.runAppleScript) {
|
|
218
|
+
const value = await runtimeHooks.runAppleScript(testScript, testArgs, timeoutMs);
|
|
219
|
+
return parseMacOsBrowserCreateTabBrokerResult({ value });
|
|
220
|
+
}
|
|
221
|
+
return parseMacOsBrowserCreateTabBrokerResult(await callBrowserAutomationBroker(request, timeoutMs));
|
|
222
|
+
}
|
|
223
|
+
|
|
117
224
|
async function runBrowserAutomationText(
|
|
118
225
|
request: BrowserAutomationBrokerAction,
|
|
119
226
|
testScript: string,
|
|
@@ -143,8 +250,72 @@ async function captureBrowserAutomation(
|
|
|
143
250
|
return bytes;
|
|
144
251
|
}
|
|
145
252
|
|
|
253
|
+
async function sendBrowserTrustedInput(
|
|
254
|
+
request: Extract<BrowserAutomationBrokerAction, { action: 'trusted_input' }>,
|
|
255
|
+
timeoutMs: number,
|
|
256
|
+
): Promise<void> {
|
|
257
|
+
if (runtimeHooks.trustedInput) {
|
|
258
|
+
await runtimeHooks.trustedInput(request, timeoutMs);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
let result: Record<string, unknown>;
|
|
262
|
+
try {
|
|
263
|
+
result = await callBrowserAutomationBroker(request, timeoutMs);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
const message = error instanceof Error ? error.message : String(error ?? '');
|
|
266
|
+
if ((error instanceof AssistantPluginError && error.code === 'BROWSER_AUTOMATION_ACTION_UNSUPPORTED')
|
|
267
|
+
|| /\bBROWSER_AUTOMATION_ACTION_UNSUPPORTED\b/.test(message)) {
|
|
268
|
+
throw new AssistantPluginError(
|
|
269
|
+
'PLUGIN_BROWSER_TRUSTED_INPUT_UNAVAILABLE',
|
|
270
|
+
'Installed Forge Desktop Operator does not support native browser trusted input. Keep the exact tab explicitly foregrounded and use a separately grounded Desktop Operator interaction, or upgrade the stable Desktop Operator provider.',
|
|
271
|
+
{ retryable: false, details: { browserProduct: request.product, requiredProviderAction: 'trusted_input' } },
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
if (result.performed !== true) {
|
|
277
|
+
throw new AssistantPluginError(
|
|
278
|
+
'PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR',
|
|
279
|
+
'Stable Forge macOS capability broker did not confirm trusted browser input.',
|
|
280
|
+
{ retryable: true },
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
146
285
|
let runtimeHooks: MacOsBrowserRuntimeHooks = { ...defaultRuntimeHooks };
|
|
147
286
|
let lastAttachObservation: MacOsBrowserAttachObservation | undefined;
|
|
287
|
+
const MAX_WARM_NATIVE_PAGE_HANDLES = 128;
|
|
288
|
+
const warmNativePageHandles = new Map<string, MacOsAppleEventsPage>();
|
|
289
|
+
|
|
290
|
+
function nativePageHandleKey(product: MacOsBrowserProduct, ref: MacOsBrowserTabRef): string {
|
|
291
|
+
// tabId is the stable browser entity. The windowId is only a mutable placement hint and
|
|
292
|
+
// is re-resolved from live inventory immediately before exact-target Apple Events operations.
|
|
293
|
+
return `${product}:${ref.tabId}`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function rememberMacOsBrowserPageHandle(product: MacOsBrowserProduct, ref: MacOsBrowserTabRef, page: MacOsAppleEventsPage): void {
|
|
297
|
+
const key = nativePageHandleKey(product, ref);
|
|
298
|
+
warmNativePageHandles.delete(key);
|
|
299
|
+
warmNativePageHandles.set(key, page);
|
|
300
|
+
while (warmNativePageHandles.size > MAX_WARM_NATIVE_PAGE_HANDLES) {
|
|
301
|
+
const oldest = warmNativePageHandles.keys().next().value;
|
|
302
|
+
if (typeof oldest !== 'string') break;
|
|
303
|
+
warmNativePageHandles.delete(oldest);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function invalidateMacOsBrowserPageHandle(product: MacOsBrowserProduct, ref: MacOsBrowserTabRef): void {
|
|
308
|
+
warmNativePageHandles.delete(nativePageHandleKey(product, ref));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function invalidateMacOsBrowserPageHandles(): void {
|
|
312
|
+
warmNativePageHandles.clear();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function macOsBrowserPageHandleStale(error: unknown): boolean {
|
|
316
|
+
const message = error instanceof Error ? error.message : String(error ?? '');
|
|
317
|
+
return /FORGE_BROWSER_TAB_NOT_FOUND|BROWSER_AUTOMATION_TAB_NOT_FOUND|target tab.*not found|can[’']t get tab .*invalid index.*-1719/i.test(message);
|
|
318
|
+
}
|
|
148
319
|
|
|
149
320
|
export function setMacOsBrowserRuntimeHooksForTest(hooks: Partial<MacOsBrowserRuntimeHooks>): void {
|
|
150
321
|
runtimeHooks = { ...defaultRuntimeHooks, ...hooks };
|
|
@@ -153,6 +324,7 @@ export function setMacOsBrowserRuntimeHooksForTest(hooks: Partial<MacOsBrowserRu
|
|
|
153
324
|
export function resetMacOsBrowserRuntimeHooksForTest(): void {
|
|
154
325
|
runtimeHooks = { ...defaultRuntimeHooks };
|
|
155
326
|
lastAttachObservation = undefined;
|
|
327
|
+
warmNativePageHandles.clear();
|
|
156
328
|
}
|
|
157
329
|
|
|
158
330
|
export function getMacOsBrowserAttachObservation(): MacOsBrowserAttachObservation | undefined {
|
|
@@ -197,12 +369,120 @@ set targetWindow to front window
|
|
|
197
369
|
set targetTab to active tab of targetWindow
|
|
198
370
|
set windowBounds to bounds of targetWindow
|
|
199
371
|
set separator to ASCII character 30
|
|
200
|
-
return (frontmost as text) & separator & (URL of targetTab as text) & separator & (title of targetTab as text) & separator & ((item 1 of windowBounds) as text) & separator & ((item 2 of windowBounds) as text) & separator & ((item 3 of windowBounds) as text) & separator & ((item 4 of windowBounds) as text)
|
|
372
|
+
return (frontmost as text) & separator & (URL of targetTab as text) & separator & (title of targetTab as text) & separator & ((item 1 of windowBounds) as text) & separator & ((item 2 of windowBounds) as text) & separator & ((item 3 of windowBounds) as text) & separator & ((item 4 of windowBounds) as text) & separator & ((id of targetWindow) as text) & separator & ((id of targetTab) as text) & separator & "true" & separator & (loading of targetTab as text)
|
|
201
373
|
`);
|
|
202
374
|
}
|
|
203
375
|
|
|
376
|
+
function listTabsScript(browser: MacOsBrowserDefinition): string {
|
|
377
|
+
return `on replaceText(sourceText, needle, replacement)
|
|
378
|
+
set previousDelimiters to AppleScript's text item delimiters
|
|
379
|
+
set AppleScript's text item delimiters to needle
|
|
380
|
+
set sourceItems to every text item of sourceText
|
|
381
|
+
set AppleScript's text item delimiters to replacement
|
|
382
|
+
set resultText to sourceItems as text
|
|
383
|
+
set AppleScript's text item delimiters to previousDelimiters
|
|
384
|
+
return resultText
|
|
385
|
+
end replaceText
|
|
386
|
+
|
|
387
|
+
on cleanField(sourceText, recordSeparator, fieldSeparator)
|
|
388
|
+
set cleaned to my replaceText(sourceText as text, recordSeparator, " ")
|
|
389
|
+
return my replaceText(cleaned, fieldSeparator, " ")
|
|
390
|
+
end cleanField
|
|
391
|
+
|
|
392
|
+
${browserTellScript(browser, `
|
|
393
|
+
set recordSeparator to ASCII character 30
|
|
394
|
+
set fieldSeparator to ASCII character 31
|
|
395
|
+
set maxTabs to 256
|
|
396
|
+
set returnedCount to 0
|
|
397
|
+
set truncatedInventory to false
|
|
398
|
+
set outputText to "false"
|
|
399
|
+
repeat with candidateWindow in windows
|
|
400
|
+
set activeTabId to ""
|
|
401
|
+
try
|
|
402
|
+
set activeTabId to ((id of active tab of candidateWindow) as text)
|
|
403
|
+
end try
|
|
404
|
+
repeat with candidateTab in tabs of candidateWindow
|
|
405
|
+
if returnedCount is greater than or equal to maxTabs then
|
|
406
|
+
set truncatedInventory to true
|
|
407
|
+
exit repeat
|
|
408
|
+
end if
|
|
409
|
+
set candidateWindowId to ((id of candidateWindow) as text)
|
|
410
|
+
set candidateTabId to ((id of candidateTab) as text)
|
|
411
|
+
set candidateURL to my cleanField((URL of candidateTab as text), recordSeparator, fieldSeparator)
|
|
412
|
+
set candidateTitle to my cleanField((title of candidateTab as text), recordSeparator, fieldSeparator)
|
|
413
|
+
set candidateActive to (candidateTabId is activeTabId)
|
|
414
|
+
set outputText to outputText & recordSeparator & candidateWindowId & fieldSeparator & candidateTabId & fieldSeparator & (candidateActive as text) & fieldSeparator & candidateURL & fieldSeparator & candidateTitle
|
|
415
|
+
set returnedCount to returnedCount + 1
|
|
416
|
+
end repeat
|
|
417
|
+
if truncatedInventory then exit repeat
|
|
418
|
+
end repeat
|
|
419
|
+
if truncatedInventory then
|
|
420
|
+
set outputText to "true" & text 6 thru -1 of outputText
|
|
421
|
+
end if
|
|
422
|
+
return outputText
|
|
423
|
+
`)}
|
|
424
|
+
`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function parseTabInventory(product: MacOsBrowserProduct, raw: string): MacOsBrowserTabInventory {
|
|
428
|
+
const records = raw.split(RECORD_SEPARATOR);
|
|
429
|
+
const marker = (records.shift() ?? '').trim().toLowerCase();
|
|
430
|
+
if (marker !== 'true' && marker !== 'false') {
|
|
431
|
+
throw new AssistantPluginError(
|
|
432
|
+
'PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR',
|
|
433
|
+
'Stable Forge macOS capability broker returned an invalid browser tab inventory marker.',
|
|
434
|
+
{ retryable: true },
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
const tabs: MacOsBrowserTabInventoryEntry[] = [];
|
|
438
|
+
const fieldSeparator = String.fromCharCode(31);
|
|
439
|
+
for (const record of records) {
|
|
440
|
+
if (!record) continue;
|
|
441
|
+
const fields = record.split(fieldSeparator);
|
|
442
|
+
if (fields.length < 5) {
|
|
443
|
+
throw new AssistantPluginError(
|
|
444
|
+
'PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR',
|
|
445
|
+
'Stable Forge macOS capability broker returned incomplete browser tab inventory metadata.',
|
|
446
|
+
{ retryable: true },
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
const windowId = (fields[0] ?? '').trim();
|
|
450
|
+
const tabId = (fields[1] ?? '').trim();
|
|
451
|
+
const activeText = (fields[2] ?? '').trim().toLowerCase();
|
|
452
|
+
if (!windowId || !tabId || (activeText !== 'true' && activeText !== 'false')) {
|
|
453
|
+
throw new AssistantPluginError(
|
|
454
|
+
'PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR',
|
|
455
|
+
'Stable Forge macOS capability broker returned invalid browser tab inventory metadata.',
|
|
456
|
+
{ retryable: true },
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
tabs.push({ windowId, tabId, active: activeText === 'true', url: fields[3] ?? '', title: fields.slice(4).join(fieldSeparator) });
|
|
460
|
+
if (tabs.length > 256) {
|
|
461
|
+
throw new AssistantPluginError(
|
|
462
|
+
'PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR',
|
|
463
|
+
'Stable Forge macOS capability broker exceeded the bounded browser tab inventory size.',
|
|
464
|
+
{ retryable: true },
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return { product, tabs, truncated: marker === 'true' };
|
|
469
|
+
}
|
|
470
|
+
|
|
204
471
|
function targetTabPreamble(ref: MacOsBrowserTabRef): string {
|
|
205
|
-
return `set
|
|
472
|
+
return `set targetTabId to ${quotedAppleScript(ref.tabId)}
|
|
473
|
+
set targetWindow to missing value
|
|
474
|
+
set targetTab to missing value
|
|
475
|
+
repeat with candidateWindow in windows
|
|
476
|
+
repeat with candidateTab in tabs of candidateWindow
|
|
477
|
+
if ((id of candidateTab) as text) is targetTabId then
|
|
478
|
+
set targetWindow to candidateWindow
|
|
479
|
+
set targetTab to candidateTab
|
|
480
|
+
exit repeat
|
|
481
|
+
end if
|
|
482
|
+
end repeat
|
|
483
|
+
if targetTab is not missing value then exit repeat
|
|
484
|
+
end repeat
|
|
485
|
+
if targetTab is missing value then error "FORGE_BROWSER_TAB_NOT_FOUND:" & targetTabId`;
|
|
206
486
|
}
|
|
207
487
|
|
|
208
488
|
function targetMetadataScript(browser: MacOsBrowserDefinition, ref: MacOsBrowserTabRef): string {
|
|
@@ -211,7 +491,7 @@ ${targetTabPreamble(ref)}
|
|
|
211
491
|
set windowBounds to bounds of targetWindow
|
|
212
492
|
set separator to ASCII character 30
|
|
213
493
|
set targetIsActive to ((id of active tab of targetWindow) is (id of targetTab))
|
|
214
|
-
return (frontmost as text) & separator & (URL of targetTab as text) & separator & "" & separator & ((item 1 of windowBounds) as text) & separator & ((item 2 of windowBounds) as text) & separator & ((item 3 of windowBounds) as text) & separator & ((item 4 of windowBounds) as text) & separator &
|
|
494
|
+
return (frontmost as text) & separator & (URL of targetTab as text) & separator & "" & separator & ((item 1 of windowBounds) as text) & separator & ((item 2 of windowBounds) as text) & separator & ((item 3 of windowBounds) as text) & separator & ((item 4 of windowBounds) as text) & separator & ((id of targetWindow) as text) & separator & ((id of targetTab) as text) & separator & (targetIsActive as text) & separator & (loading of targetTab as text)
|
|
215
495
|
`);
|
|
216
496
|
}
|
|
217
497
|
|
|
@@ -416,7 +696,8 @@ function serializeEvaluation(expression: string | ((...args: unknown[]) => unkno
|
|
|
416
696
|
return `(() => {
|
|
417
697
|
try {
|
|
418
698
|
const value = (${source});
|
|
419
|
-
|
|
699
|
+
const page = { url: String(document.location.href || ''), title: String(document.title || '') };
|
|
700
|
+
return JSON.stringify({ ok: true, value: value === undefined ? { __forgeUndefined: true } : value, page });
|
|
420
701
|
} catch (error) {
|
|
421
702
|
return JSON.stringify({ ok: false, error: String(error && (error.stack || error.message) || error) });
|
|
422
703
|
}
|
|
@@ -464,27 +745,53 @@ function keyEventSource(selector: string | undefined, keySpec: string): string {
|
|
|
464
745
|
})()`;
|
|
465
746
|
}
|
|
466
747
|
|
|
748
|
+
export function nativeDomLoadStateSatisfied(
|
|
749
|
+
state: string,
|
|
750
|
+
readyState: string,
|
|
751
|
+
currentUrl: string,
|
|
752
|
+
requireHttpUrl: boolean,
|
|
753
|
+
): boolean {
|
|
754
|
+
if (requireHttpUrl && !/^https?:\/\//i.test(currentUrl ?? '')) return false;
|
|
755
|
+
return state === 'load' || state === 'networkidle'
|
|
756
|
+
? readyState === 'complete'
|
|
757
|
+
: readyState === 'interactive' || readyState === 'complete';
|
|
758
|
+
}
|
|
759
|
+
|
|
467
760
|
export class MacOsAppleEventsPage {
|
|
468
761
|
private metadata: MacOsBrowserMetadata;
|
|
469
762
|
private readonly browser: MacOsBrowserDefinition;
|
|
470
|
-
private
|
|
471
|
-
private
|
|
763
|
+
private timeoutMs: number;
|
|
764
|
+
private targetRef?: MacOsBrowserTabRef;
|
|
765
|
+
private readonly createTabEvidence?: MacOsBrowserCreateTabEvidence;
|
|
472
766
|
|
|
473
|
-
constructor(attachment: MacOsBrowserAttachment, timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS, targetRef?: MacOsBrowserTabRef) {
|
|
767
|
+
constructor(attachment: MacOsBrowserAttachment, timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS, targetRef?: MacOsBrowserTabRef, createTabEvidence?: MacOsBrowserCreateTabEvidence) {
|
|
474
768
|
this.metadata = { ...attachment.metadata, ...(targetRef ? { windowId: targetRef.windowId, tabId: targetRef.tabId } : {}) };
|
|
475
769
|
this.browser = browserDefinition(attachment.metadata.product);
|
|
476
770
|
this.timeoutMs = timeoutMs;
|
|
477
771
|
this.targetRef = targetRef;
|
|
772
|
+
this.createTabEvidence = createTabEvidence ? {
|
|
773
|
+
...createTabEvidence,
|
|
774
|
+
ref: { ...createTabEvidence.ref },
|
|
775
|
+
...(createTabEvidence.navigation ? { navigation: { ...createTabEvidence.navigation } } : {}),
|
|
776
|
+
} : undefined;
|
|
478
777
|
}
|
|
479
778
|
|
|
480
779
|
private async runAutomation(
|
|
481
780
|
request: BrowserAutomationBrokerAction,
|
|
482
|
-
script: string,
|
|
781
|
+
script: string | ((resolvedRef?: MacOsBrowserTabRef) => string),
|
|
483
782
|
args: string[] = [],
|
|
484
783
|
timeoutMs = this.timeoutMs,
|
|
485
784
|
): Promise<string> {
|
|
486
785
|
try {
|
|
487
|
-
|
|
786
|
+
let effectiveRequest = request;
|
|
787
|
+
let resolvedRef: MacOsBrowserTabRef | undefined;
|
|
788
|
+
if ('ref' in request && request.ref) {
|
|
789
|
+
resolvedRef = await resolveCurrentMacOsBrowserTabRef(this.browser.product, request.ref, timeoutMs);
|
|
790
|
+
this.targetRef = resolvedRef;
|
|
791
|
+
effectiveRequest = { ...request, ref: resolvedRef } as BrowserAutomationBrokerAction;
|
|
792
|
+
}
|
|
793
|
+
const effectiveScript = typeof script === 'function' ? script(resolvedRef ?? this.targetRef) : script;
|
|
794
|
+
return await runBrowserAutomationText(effectiveRequest, effectiveScript, args, timeoutMs);
|
|
488
795
|
} catch (error) {
|
|
489
796
|
const message = error instanceof Error ? error.message : String(error);
|
|
490
797
|
throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_OPERATION_FAILED', `${this.browser.appName} Apple Events operation failed: ${message}`, {
|
|
@@ -495,11 +802,13 @@ export class MacOsAppleEventsPage {
|
|
|
495
802
|
}
|
|
496
803
|
|
|
497
804
|
private async refreshMetadata(): Promise<MacOsBrowserMetadata> {
|
|
498
|
-
const script = this.targetRef ? targetMetadataScript(this.browser, this.targetRef) : metadataScript(this.browser);
|
|
499
805
|
this.metadata = parseMetadata(this.browser.product, await this.runAutomation(
|
|
500
806
|
{ action: 'metadata', product: this.browser.product, ...(this.targetRef ? { ref: this.targetRef } : {}) },
|
|
501
|
-
|
|
807
|
+
(resolvedRef) => resolvedRef ? targetMetadataScript(this.browser, resolvedRef) : metadataScript(this.browser),
|
|
502
808
|
));
|
|
809
|
+
if (this.targetRef && this.metadata.windowId && this.metadata.tabId) {
|
|
810
|
+
this.targetRef = { windowId: this.metadata.windowId, tabId: this.metadata.tabId };
|
|
811
|
+
}
|
|
503
812
|
return this.metadata;
|
|
504
813
|
}
|
|
505
814
|
|
|
@@ -507,7 +816,7 @@ export class MacOsAppleEventsPage {
|
|
|
507
816
|
try {
|
|
508
817
|
return await this.runAutomation(
|
|
509
818
|
{ action: 'execute_javascript', product: this.browser.product, ...(this.targetRef ? { ref: this.targetRef } : {}), source },
|
|
510
|
-
executeJavaScriptScript(this.browser,
|
|
819
|
+
(resolvedRef) => executeJavaScriptScript(this.browser, resolvedRef),
|
|
511
820
|
[source],
|
|
512
821
|
timeoutMs,
|
|
513
822
|
);
|
|
@@ -527,18 +836,36 @@ export class MacOsAppleEventsPage {
|
|
|
527
836
|
return this.targetRef ? { ...this.targetRef } : undefined;
|
|
528
837
|
}
|
|
529
838
|
|
|
839
|
+
creationEvidence(): MacOsBrowserCreateTabEvidence | undefined {
|
|
840
|
+
return this.createTabEvidence ? {
|
|
841
|
+
...this.createTabEvidence,
|
|
842
|
+
ref: { ...this.createTabEvidence.ref },
|
|
843
|
+
...(this.createTabEvidence.navigation ? { navigation: { ...this.createTabEvidence.navigation } } : {}),
|
|
844
|
+
} : undefined;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
updateTimeout(timeoutMs: number): void {
|
|
848
|
+
if (Number.isFinite(timeoutMs) && timeoutMs > 0) this.timeoutMs = Math.trunc(timeoutMs);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
attachmentSnapshot(): MacOsBrowserAttachment {
|
|
852
|
+
return {
|
|
853
|
+
metadata: { ...this.metadata, bounds: { ...this.metadata.bounds } },
|
|
854
|
+
attempts: [{
|
|
855
|
+
product: this.browser.product,
|
|
856
|
+
appName: this.browser.appName,
|
|
857
|
+
bundleId: this.browser.bundleId,
|
|
858
|
+
status: 'selected',
|
|
859
|
+
frontmost: this.metadata.frontmost,
|
|
860
|
+
}],
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
530
864
|
async goto(url: string, options: Record<string, unknown> = {}): Promise<unknown> {
|
|
531
865
|
const timeout = typeof options.timeout === 'number' ? Math.trunc(options.timeout) : this.timeoutMs;
|
|
532
|
-
if (this.targetRef) {
|
|
533
|
-
throw new AssistantPluginError(
|
|
534
|
-
'PLUGIN_BROWSER_BACKGROUND_NAVIGATION_REQUIRES_REPLACEMENT',
|
|
535
|
-
'Chrome/Vivaldi background tabs cannot be navigated reliably in place through Apple Events; create a replacement plugin-owned tab instead.',
|
|
536
|
-
{ retryable: true, details: { browserProduct: this.browser.product, windowId: this.targetRef.windowId, tabId: this.targetRef.tabId } },
|
|
537
|
-
);
|
|
538
|
-
}
|
|
539
866
|
await this.runAutomation(
|
|
540
|
-
{ action: 'navigate', product: this.browser.product, url },
|
|
541
|
-
navigateScript(this.browser),
|
|
867
|
+
{ action: 'navigate', product: this.browser.product, ...(this.targetRef ? { ref: this.targetRef } : {}), url },
|
|
868
|
+
(resolvedRef) => navigateScript(this.browser, resolvedRef),
|
|
542
869
|
[url],
|
|
543
870
|
timeout,
|
|
544
871
|
);
|
|
@@ -551,7 +878,7 @@ export class MacOsAppleEventsPage {
|
|
|
551
878
|
const timeout = typeof options.timeout === 'number' ? Math.trunc(options.timeout) : this.timeoutMs;
|
|
552
879
|
await this.runAutomation(
|
|
553
880
|
{ action: 'reload', product: this.browser.product, ...(this.targetRef ? { ref: this.targetRef } : {}) },
|
|
554
|
-
reloadScript(this.browser,
|
|
881
|
+
(resolvedRef) => reloadScript(this.browser, resolvedRef),
|
|
555
882
|
[],
|
|
556
883
|
timeout,
|
|
557
884
|
);
|
|
@@ -609,21 +936,183 @@ export class MacOsAppleEventsPage {
|
|
|
609
936
|
|
|
610
937
|
async evaluate<T>(expression: string | ((...args: unknown[]) => unknown), arg?: unknown): Promise<T> {
|
|
611
938
|
const raw = await this.executeJavaScriptRaw(serializeEvaluation(expression, arg));
|
|
612
|
-
let parsed: { ok?: boolean; value?: unknown; error?: string };
|
|
939
|
+
let parsed: { ok?: boolean; value?: unknown; error?: string; page?: { url?: unknown; title?: unknown } };
|
|
613
940
|
try {
|
|
614
|
-
parsed = JSON.parse(raw) as { ok?: boolean; value?: unknown; error?: string };
|
|
941
|
+
parsed = JSON.parse(raw) as { ok?: boolean; value?: unknown; error?: string; page?: { url?: unknown; title?: unknown } };
|
|
615
942
|
} catch {
|
|
616
943
|
throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_PROTOCOL_ERROR', `${this.browser.appName} returned an invalid JavaScript result.`, {
|
|
617
944
|
retryable: true,
|
|
618
945
|
});
|
|
619
946
|
}
|
|
620
947
|
if (!parsed.ok) throw new Error(parsed.error || 'Browser JavaScript evaluation failed.');
|
|
948
|
+
if (parsed.page && typeof parsed.page.url === 'string' && parsed.page.url) {
|
|
949
|
+
this.metadata = {
|
|
950
|
+
...this.metadata,
|
|
951
|
+
url: parsed.page.url,
|
|
952
|
+
title: typeof parsed.page.title === 'string' ? parsed.page.title : this.metadata.title,
|
|
953
|
+
};
|
|
954
|
+
}
|
|
621
955
|
if (parsed.value && typeof parsed.value === 'object' && (parsed.value as { __forgeUndefined?: unknown }).__forgeUndefined === true) {
|
|
622
956
|
return undefined as T;
|
|
623
957
|
}
|
|
624
958
|
return parsed.value as T;
|
|
625
959
|
}
|
|
626
960
|
|
|
961
|
+
async setInputFiles(selector: string, files: string | string[]): Promise<void> {
|
|
962
|
+
const filePaths = Array.isArray(files) ? files : [files];
|
|
963
|
+
if (filePaths.length === 0 || filePaths.length > 32) {
|
|
964
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Native browser file input accepts 1-32 local files.', { retryable: false });
|
|
965
|
+
}
|
|
966
|
+
for (const filePath of filePaths) {
|
|
967
|
+
if (!filePath || !existsSync(filePath)) {
|
|
968
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Local file does not exist.', { retryable: false });
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
if (filePaths.length === 1) {
|
|
973
|
+
const filePath = filePaths[0];
|
|
974
|
+
const bytes = readFileSync(filePath);
|
|
975
|
+
const fileName = basename(filePath);
|
|
976
|
+
const extension = extname(fileName).toLowerCase();
|
|
977
|
+
const mimeType = extension === '.pdf' ? 'application/pdf'
|
|
978
|
+
: extension === '.zip' ? 'application/zip'
|
|
979
|
+
: extension === '.png' ? 'image/png'
|
|
980
|
+
: extension === '.jpg' || extension === '.jpeg' ? 'image/jpeg'
|
|
981
|
+
: 'application/octet-stream';
|
|
982
|
+
const bufferKey = `__forgeLocalFile_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
983
|
+
const encoded = bytes.toString('base64');
|
|
984
|
+
const chunkChars = 192 * 1024;
|
|
985
|
+
try {
|
|
986
|
+
await this.evaluate(`(() => { globalThis[${JSON.stringify(bufferKey)}] = ''; return true; })()`);
|
|
987
|
+
for (let offset = 0; offset < encoded.length; offset += chunkChars) {
|
|
988
|
+
const chunk = encoded.slice(offset, offset + chunkChars);
|
|
989
|
+
await this.evaluate(`(() => { globalThis[${JSON.stringify(bufferKey)}] += ${JSON.stringify(chunk)}; return true; })()`);
|
|
990
|
+
}
|
|
991
|
+
const attached = await this.evaluate<{ name?: string; size?: number }>(`(() => {
|
|
992
|
+
const element = document.querySelector(${JSON.stringify(selector)});
|
|
993
|
+
if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
|
|
994
|
+
const expectedName = ${JSON.stringify(fileName)};
|
|
995
|
+
const expectedSize = ${bytes.length};
|
|
996
|
+
const encoded = String(globalThis[${JSON.stringify(bufferKey)}] || '');
|
|
997
|
+
const binary = atob(encoded);
|
|
998
|
+
const data = new Uint8Array(binary.length);
|
|
999
|
+
for (let i = 0; i < binary.length; i += 1) data[i] = binary.charCodeAt(i);
|
|
1000
|
+
const file = new File([data], expectedName, { type: ${JSON.stringify(mimeType)} });
|
|
1001
|
+
const transfer = new DataTransfer();
|
|
1002
|
+
transfer.items.add(file);
|
|
1003
|
+
element.files = transfer.files;
|
|
1004
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1005
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1006
|
+
const selected = element.files && element.files[0];
|
|
1007
|
+
return selected ? { name: selected.name, size: selected.size, expectedName, expectedSize } : null;
|
|
1008
|
+
})()`);
|
|
1009
|
+
if (attached?.name !== fileName || attached?.size !== bytes.length) {
|
|
1010
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_FILE_ATTACH_FAILED', 'Native browser did not retain the requested local file on the target input.', {
|
|
1011
|
+
retryable: true,
|
|
1012
|
+
details: { expectedName: fileName, expectedSize: bytes.length, actualName: attached?.name, actualSize: attached?.size },
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
} finally {
|
|
1016
|
+
await this.evaluate(`(() => { try { delete globalThis[${JSON.stringify(bufferKey)}]; } catch {} return true; })()`).catch(() => undefined);
|
|
1017
|
+
}
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const attachments = filePaths.map((filePath) => {
|
|
1022
|
+
const bytes = readFileSync(filePath);
|
|
1023
|
+
const fileName = basename(filePath);
|
|
1024
|
+
const extension = extname(fileName).toLowerCase();
|
|
1025
|
+
const mimeType = extension === '.pdf' ? 'application/pdf'
|
|
1026
|
+
: extension === '.zip' ? 'application/zip'
|
|
1027
|
+
: extension === '.png' ? 'image/png'
|
|
1028
|
+
: extension === '.jpg' || extension === '.jpeg' ? 'image/jpeg'
|
|
1029
|
+
: 'application/octet-stream';
|
|
1030
|
+
return {
|
|
1031
|
+
bytes,
|
|
1032
|
+
fileName,
|
|
1033
|
+
mimeType,
|
|
1034
|
+
bufferKey: `__forgeLocalFile_${Date.now()}_${Math.random().toString(16).slice(2)}`,
|
|
1035
|
+
};
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
if (attachments.length > 1) {
|
|
1039
|
+
await this.evaluate(`(() => {
|
|
1040
|
+
const element = document.querySelector(${JSON.stringify(selector)});
|
|
1041
|
+
if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
|
|
1042
|
+
if (!element.multiple) throw new Error('Target input does not allow multiple files.');
|
|
1043
|
+
const transfer = new DataTransfer();
|
|
1044
|
+
element.files = transfer.files;
|
|
1045
|
+
return true;
|
|
1046
|
+
})()`);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const chunkChars = 192 * 1024;
|
|
1050
|
+
try {
|
|
1051
|
+
for (let index = 0; index < attachments.length; index += 1) {
|
|
1052
|
+
const attachment = attachments[index];
|
|
1053
|
+
const encoded = attachment.bytes.toString('base64');
|
|
1054
|
+
await this.evaluate(`(() => { globalThis[${JSON.stringify(attachment.bufferKey)}] = ''; return true; })()`);
|
|
1055
|
+
for (let offset = 0; offset < encoded.length; offset += chunkChars) {
|
|
1056
|
+
const chunk = encoded.slice(offset, offset + chunkChars);
|
|
1057
|
+
await this.evaluate(`(() => { globalThis[${JSON.stringify(attachment.bufferKey)}] += ${JSON.stringify(chunk)}; return true; })()`);
|
|
1058
|
+
}
|
|
1059
|
+
const append = attachments.length > 1 && index > 0;
|
|
1060
|
+
await this.evaluate(`(() => {
|
|
1061
|
+
const element = document.querySelector(${JSON.stringify(selector)});
|
|
1062
|
+
if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
|
|
1063
|
+
const expectedName = ${JSON.stringify(attachment.fileName)};
|
|
1064
|
+
const expectedSize = ${attachment.bytes.length};
|
|
1065
|
+
const encoded = String(globalThis[${JSON.stringify(attachment.bufferKey)}] || '');
|
|
1066
|
+
const binary = atob(encoded);
|
|
1067
|
+
const data = new Uint8Array(binary.length);
|
|
1068
|
+
for (let i = 0; i < binary.length; i += 1) data[i] = binary.charCodeAt(i);
|
|
1069
|
+
const file = new File([data], expectedName, { type: ${JSON.stringify(attachment.mimeType)} });
|
|
1070
|
+
const transfer = new DataTransfer();
|
|
1071
|
+
if (${append ? 'true' : 'false'}) {
|
|
1072
|
+
for (const existing of Array.from(element.files || [])) transfer.items.add(existing);
|
|
1073
|
+
}
|
|
1074
|
+
transfer.items.add(file);
|
|
1075
|
+
element.files = transfer.files;
|
|
1076
|
+
return Array.from(element.files || []).map((selected) => ({ name: selected.name, size: selected.size }));
|
|
1077
|
+
})()`);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const selected = await this.evaluate<Array<{ name?: string; size?: number }>>(`(() => {
|
|
1081
|
+
const element = document.querySelector(${JSON.stringify(selector)});
|
|
1082
|
+
if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
|
|
1083
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1084
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1085
|
+
return Array.from(element.files || []).map((file) => ({ name: file.name, size: file.size }));
|
|
1086
|
+
})()`);
|
|
1087
|
+
const expected = attachments.map((attachment) => ({ name: attachment.fileName, size: attachment.bytes.length }));
|
|
1088
|
+
const matches = selected.length === expected.length && expected.every((item, index) => selected[index]?.name === item.name && selected[index]?.size === item.size);
|
|
1089
|
+
if (!matches) {
|
|
1090
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_FILE_ATTACH_FAILED', 'Native browser did not retain the requested local files on the target input.', {
|
|
1091
|
+
retryable: true,
|
|
1092
|
+
details: { expected, actual: selected },
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
} finally {
|
|
1096
|
+
for (const attachment of attachments) {
|
|
1097
|
+
await this.evaluate(`(() => { try { delete globalThis[${JSON.stringify(attachment.bufferKey)}]; } catch {} return true; })()`).catch(() => undefined);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
async foregroundState(): Promise<{ frontmost: boolean; active: boolean }> {
|
|
1103
|
+
const metadata = await this.refreshMetadata();
|
|
1104
|
+
return { frontmost: metadata.frontmost, active: metadata.active === true };
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
async trustedInput(input: BrowserAutomationTrustedInput): Promise<void> {
|
|
1108
|
+
if (!this.targetRef) {
|
|
1109
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_TRUSTED_INPUT_UNAVAILABLE', 'Native trusted input requires an exact saved browser tab.', { retryable: false });
|
|
1110
|
+
}
|
|
1111
|
+
this.targetRef = await resolveCurrentMacOsBrowserTabRef(this.browser.product, this.targetRef, this.timeoutMs);
|
|
1112
|
+
await sendBrowserTrustedInput({ action: 'trusted_input', product: this.browser.product, ref: this.targetRef, input }, this.timeoutMs);
|
|
1113
|
+
await this.refreshMetadata();
|
|
1114
|
+
}
|
|
1115
|
+
|
|
627
1116
|
async screenshot(options: Record<string, unknown>): Promise<Buffer> {
|
|
628
1117
|
const path = typeof options.path === 'string' ? options.path : undefined;
|
|
629
1118
|
if (!path) throw new Error('Native browser screenshot requires an output path.');
|
|
@@ -638,7 +1127,40 @@ export class MacOsAppleEventsPage {
|
|
|
638
1127
|
}
|
|
639
1128
|
|
|
640
1129
|
async click(selector: string): Promise<void> {
|
|
641
|
-
await this.evaluate(selectorSource(selector,
|
|
1130
|
+
await this.evaluate(selectorSource(selector, `
|
|
1131
|
+
if (element instanceof HTMLOptionElement) {
|
|
1132
|
+
const selectElement = element.closest('select');
|
|
1133
|
+
if (!(selectElement instanceof HTMLSelectElement)) throw new Error('Option is not attached to a select.');
|
|
1134
|
+
if (element.disabled || selectElement.disabled) throw new Error('Option is disabled.');
|
|
1135
|
+
if (!selectElement.multiple) {
|
|
1136
|
+
const valueSetter = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value')?.set;
|
|
1137
|
+
if (valueSetter) valueSetter.call(selectElement, element.value);
|
|
1138
|
+
else selectElement.value = element.value;
|
|
1139
|
+
} else {
|
|
1140
|
+
element.selected = true;
|
|
1141
|
+
}
|
|
1142
|
+
selectElement.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1143
|
+
selectElement.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1144
|
+
if (!Array.from(selectElement.selectedOptions).includes(element)) throw new Error('Option selection did not persist.');
|
|
1145
|
+
return true;
|
|
1146
|
+
}
|
|
1147
|
+
if (typeof element.click !== "function") throw new Error("Element is not clickable.");
|
|
1148
|
+
const rect = typeof element.getBoundingClientRect === 'function' ? element.getBoundingClientRect() : { left: 0, top: 0, width: 0, height: 0 };
|
|
1149
|
+
const clientX = Number(rect.left || 0) + Number(rect.width || 0) / 2;
|
|
1150
|
+
const clientY = Number(rect.top || 0) + Number(rect.height || 0) / 2;
|
|
1151
|
+
const mouseInit = { bubbles: true, cancelable: true, composed: true, view: window, button: 0, buttons: 1, clientX, clientY };
|
|
1152
|
+
if (typeof PointerEvent === 'function') {
|
|
1153
|
+
element.dispatchEvent(new PointerEvent('pointerdown', { ...mouseInit, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
|
|
1154
|
+
}
|
|
1155
|
+
element.dispatchEvent(new MouseEvent('mousedown', mouseInit));
|
|
1156
|
+
if (typeof element.focus === 'function') element.focus();
|
|
1157
|
+
if (typeof PointerEvent === 'function') {
|
|
1158
|
+
element.dispatchEvent(new PointerEvent('pointerup', { ...mouseInit, buttons: 0, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
|
|
1159
|
+
}
|
|
1160
|
+
element.dispatchEvent(new MouseEvent('mouseup', { ...mouseInit, buttons: 0 }));
|
|
1161
|
+
element.click();
|
|
1162
|
+
return true;
|
|
1163
|
+
`));
|
|
642
1164
|
}
|
|
643
1165
|
|
|
644
1166
|
async dblclick(selector: string): Promise<void> {
|
|
@@ -681,10 +1203,20 @@ export class MacOsAppleEventsPage {
|
|
|
681
1203
|
return await this.evaluate(selectorSource(selector, `
|
|
682
1204
|
if (!(element instanceof HTMLSelectElement)) throw new Error('Element is not a select.');
|
|
683
1205
|
const selected = new Set(${JSON.stringify(normalized)});
|
|
684
|
-
|
|
1206
|
+
let matched = 0;
|
|
1207
|
+
for (const option of element.options) {
|
|
1208
|
+
const shouldSelect = selected.has(option.value) || selected.has(option.label);
|
|
1209
|
+
option.selected = shouldSelect;
|
|
1210
|
+
if (shouldSelect) matched += 1;
|
|
1211
|
+
}
|
|
1212
|
+
if (matched === 0) throw new Error('Requested select option was not found.');
|
|
685
1213
|
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
686
1214
|
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
687
|
-
|
|
1215
|
+
const actual = Array.from(element.selectedOptions);
|
|
1216
|
+
if (!actual.some((option) => selected.has(option.value) || selected.has(option.label))) {
|
|
1217
|
+
throw new Error('Requested select option did not persist.');
|
|
1218
|
+
}
|
|
1219
|
+
return actual.map((option) => option.value);
|
|
688
1220
|
`));
|
|
689
1221
|
}
|
|
690
1222
|
|
|
@@ -730,15 +1262,19 @@ export class MacOsAppleEventsPage {
|
|
|
730
1262
|
|
|
731
1263
|
async waitForLoadState(state = 'domcontentloaded', options: Record<string, unknown> = {}): Promise<void> {
|
|
732
1264
|
const timeout = typeof options.timeout === 'number' ? Math.trunc(options.timeout) : this.timeoutMs;
|
|
1265
|
+
const requireHttpUrl = options.requireHttpUrl === true;
|
|
733
1266
|
const deadline = Date.now() + timeout;
|
|
734
1267
|
while (Date.now() <= deadline) {
|
|
735
1268
|
if (this.targetRef) {
|
|
736
1269
|
try {
|
|
737
1270
|
const metadata = await this.refreshMetadata();
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
1271
|
+
// Browser actions only admit HTTP(S) targets. Native Chrome/Vivaldi can
|
|
1272
|
+
// expose several internal URLs (new-tab, error/interstitial, extension
|
|
1273
|
+
// bootstrap pages) while navigation is still settling. Treat every
|
|
1274
|
+
// non-HTTP(S) metadata URL as transitional here; the adapter still
|
|
1275
|
+
// applies the strict final HTTP(S) URL validator before persisting a
|
|
1276
|
+
// session, so this does not widen the accepted navigation surface.
|
|
1277
|
+
const transitionalUrl = !/^https?:\/\//i.test(metadata.url ?? '');
|
|
742
1278
|
if (transitionalUrl) {
|
|
743
1279
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
744
1280
|
continue;
|
|
@@ -749,20 +1285,24 @@ export class MacOsAppleEventsPage {
|
|
|
749
1285
|
}
|
|
750
1286
|
}
|
|
751
1287
|
try {
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
1288
|
+
const domState = await this.evaluate<{ readyState: string; url: string }>(
|
|
1289
|
+
'({ readyState: String(document.readyState || ""), url: String(document.location.href || "") })',
|
|
1290
|
+
);
|
|
1291
|
+
if (nativeDomLoadStateSatisfied(state, domState.readyState, domState.url, requireHttpUrl)) return;
|
|
756
1292
|
} catch (error) {
|
|
757
1293
|
if (error instanceof AssistantPluginError && error.code === 'PLUGIN_BROWSER_JAVASCRIPT_PERMISSION_REQUIRED') {
|
|
758
1294
|
const metadata = await this.refreshMetadata();
|
|
759
|
-
if (
|
|
1295
|
+
if (/^https?:\/\//i.test(metadata.url ?? '')) return;
|
|
760
1296
|
}
|
|
761
1297
|
// Navigation can transiently invalidate the current JavaScript execution context.
|
|
762
1298
|
}
|
|
763
1299
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
764
1300
|
}
|
|
765
|
-
throw new
|
|
1301
|
+
throw new AssistantPluginError(
|
|
1302
|
+
'PLUGIN_BROWSER_LOAD_STATE_TIMEOUT',
|
|
1303
|
+
`Timed out waiting for native browser load state ${state}.`,
|
|
1304
|
+
{ retryable: true, details: { state, timeoutMs: timeout, requireHttpUrl } },
|
|
1305
|
+
);
|
|
766
1306
|
}
|
|
767
1307
|
|
|
768
1308
|
locator(_selector: string): { screenshot: (options?: Record<string, unknown>) => Promise<Buffer> } {
|
|
@@ -776,6 +1316,9 @@ export class MacOsAppleEventsPage {
|
|
|
776
1316
|
async bringToFront(): Promise<void> {
|
|
777
1317
|
if (this.targetRef) {
|
|
778
1318
|
this.metadata = await activateMacOsBrowserOwnedTab(this.browser.product, this.targetRef, this.timeoutMs);
|
|
1319
|
+
if (this.metadata.windowId && this.metadata.tabId) {
|
|
1320
|
+
this.targetRef = { windowId: this.metadata.windowId, tabId: this.metadata.tabId };
|
|
1321
|
+
}
|
|
779
1322
|
return;
|
|
780
1323
|
}
|
|
781
1324
|
await this.runAutomation(
|
|
@@ -791,15 +1334,71 @@ export class MacOsAppleEventsPage {
|
|
|
791
1334
|
};
|
|
792
1335
|
}
|
|
793
1336
|
|
|
1337
|
+
async function resolveCurrentMacOsBrowserTabRef(
|
|
1338
|
+
product: MacOsBrowserProduct,
|
|
1339
|
+
ref: MacOsBrowserTabRef,
|
|
1340
|
+
timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
|
|
1341
|
+
): Promise<MacOsBrowserTabRef> {
|
|
1342
|
+
// Existing unit-level AppleScript hooks historically model one exact script at a time.
|
|
1343
|
+
// Preserve that narrow test seam unless a live inventory hook is explicitly supplied;
|
|
1344
|
+
// production always resolves through the stable Desktop Operator broker inventory.
|
|
1345
|
+
const inventory = runtimeHooks.tabInventory
|
|
1346
|
+
? await runtimeHooks.tabInventory(product, timeoutMs)
|
|
1347
|
+
: runtimeHooks.runAppleScript
|
|
1348
|
+
? undefined
|
|
1349
|
+
: await listMacOsBrowserTabs(product, timeoutMs);
|
|
1350
|
+
if (!inventory) return ref;
|
|
1351
|
+
|
|
1352
|
+
const matches = inventory.tabs.filter((candidate) => candidate.tabId === ref.tabId);
|
|
1353
|
+
if (inventory.truncated || matches.length !== 1) {
|
|
1354
|
+
throw new AssistantPluginError(
|
|
1355
|
+
'PLUGIN_BROWSER_NATIVE_TAB_IDENTITY_UNPROVEN',
|
|
1356
|
+
inventory.truncated
|
|
1357
|
+
? `Cannot prove the current ${product} tab identity because live inventory was truncated.`
|
|
1358
|
+
: matches.length === 0
|
|
1359
|
+
? `Saved ${product} tab ${ref.tabId} no longer exists in live inventory.`
|
|
1360
|
+
: `Saved ${product} tab ${ref.tabId} is ambiguous in live inventory.`,
|
|
1361
|
+
{
|
|
1362
|
+
retryable: true,
|
|
1363
|
+
details: {
|
|
1364
|
+
browserProduct: product,
|
|
1365
|
+
tabId: ref.tabId,
|
|
1366
|
+
savedWindowId: ref.windowId,
|
|
1367
|
+
candidateCount: matches.length,
|
|
1368
|
+
inventoryTruncated: inventory.truncated,
|
|
1369
|
+
},
|
|
1370
|
+
},
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
const match = matches[0]!;
|
|
1375
|
+
return { windowId: match.windowId, tabId: match.tabId };
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
export async function listMacOsBrowserTabs(
|
|
1379
|
+
product: MacOsBrowserProduct,
|
|
1380
|
+
timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
|
|
1381
|
+
): Promise<MacOsBrowserTabInventory> {
|
|
1382
|
+
const browser = browserDefinition(product);
|
|
1383
|
+
const raw = await runBrowserAutomationText(
|
|
1384
|
+
{ action: 'list_tabs', product },
|
|
1385
|
+
listTabsScript(browser),
|
|
1386
|
+
[],
|
|
1387
|
+
timeoutMs,
|
|
1388
|
+
);
|
|
1389
|
+
return parseTabInventory(product, raw);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
794
1392
|
export async function readMacOsBrowserOwnedTabMetadata(
|
|
795
1393
|
product: MacOsBrowserProduct,
|
|
796
1394
|
ref: MacOsBrowserTabRef,
|
|
797
1395
|
timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
|
|
798
1396
|
): Promise<MacOsBrowserMetadata> {
|
|
799
1397
|
const browser = browserDefinition(product);
|
|
1398
|
+
const resolvedRef = await resolveCurrentMacOsBrowserTabRef(product, ref, timeoutMs);
|
|
800
1399
|
return parseMetadata(product, await runBrowserAutomationText(
|
|
801
|
-
{ action: 'metadata', product, ref },
|
|
802
|
-
targetMetadataScript(browser,
|
|
1400
|
+
{ action: 'metadata', product, ref: resolvedRef },
|
|
1401
|
+
targetMetadataScript(browser, resolvedRef),
|
|
803
1402
|
[],
|
|
804
1403
|
timeoutMs,
|
|
805
1404
|
));
|
|
@@ -811,13 +1410,33 @@ export async function activateMacOsBrowserOwnedTab(
|
|
|
811
1410
|
timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
|
|
812
1411
|
): Promise<MacOsBrowserMetadata> {
|
|
813
1412
|
const browser = browserDefinition(product);
|
|
1413
|
+
const resolvedRef = await resolveCurrentMacOsBrowserTabRef(product, ref, timeoutMs);
|
|
814
1414
|
await runBrowserAutomationText(
|
|
815
|
-
{ action: 'activate', product, ref },
|
|
816
|
-
activateTargetTabScript(browser,
|
|
1415
|
+
{ action: 'activate', product, ref: resolvedRef },
|
|
1416
|
+
activateTargetTabScript(browser, resolvedRef),
|
|
817
1417
|
[],
|
|
818
1418
|
timeoutMs,
|
|
819
1419
|
);
|
|
820
|
-
|
|
1420
|
+
const metadata = await readMacOsBrowserOwnedTabMetadata(product, resolvedRef, timeoutMs);
|
|
1421
|
+
if (metadata.tabId !== resolvedRef.tabId || metadata.active !== true) {
|
|
1422
|
+
throw new AssistantPluginError(
|
|
1423
|
+
'PLUGIN_BROWSER_NATIVE_ACTIVATION_POSTCONDITION_FAILED',
|
|
1424
|
+
`${browser.appName} did not prove that the exact requested tab became active.`,
|
|
1425
|
+
{
|
|
1426
|
+
retryable: true,
|
|
1427
|
+
details: {
|
|
1428
|
+
browserProduct: product,
|
|
1429
|
+
requestedTabId: resolvedRef.tabId,
|
|
1430
|
+
resolvedWindowId: resolvedRef.windowId,
|
|
1431
|
+
observedWindowId: metadata.windowId,
|
|
1432
|
+
observedTabId: metadata.tabId,
|
|
1433
|
+
observedActive: metadata.active,
|
|
1434
|
+
observedFrontmost: metadata.frontmost,
|
|
1435
|
+
},
|
|
1436
|
+
},
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
return metadata;
|
|
821
1440
|
}
|
|
822
1441
|
|
|
823
1442
|
export function createMacOsBrowserPage(attachment: MacOsBrowserAttachment, timeoutMs?: number): MacOsAppleEventsPage {
|
|
@@ -830,19 +1449,19 @@ export async function createMacOsBrowserOwnedPage(
|
|
|
830
1449
|
timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
|
|
831
1450
|
): Promise<MacOsAppleEventsPage> {
|
|
832
1451
|
const browser = browserDefinition(attachment.metadata.product);
|
|
833
|
-
const
|
|
1452
|
+
const creationEvidence = await runCreateTabAutomation(
|
|
834
1453
|
{ action: 'create_tab', product: attachment.metadata.product, url },
|
|
835
1454
|
createOwnedTabScript(browser),
|
|
836
1455
|
[url],
|
|
837
1456
|
timeoutMs,
|
|
838
1457
|
);
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
const ref = { windowId: parseBrowserId(parts[0] ?? '', 'window id'), tabId: parseBrowserId(parts[1] ?? '', 'tab id') };
|
|
842
|
-
return new MacOsAppleEventsPage({
|
|
1458
|
+
const ref = creationEvidence.ref;
|
|
1459
|
+
const page = new MacOsAppleEventsPage({
|
|
843
1460
|
...attachment,
|
|
844
1461
|
metadata: { ...attachment.metadata, url, title: '', windowId: ref.windowId, tabId: ref.tabId, active: false },
|
|
845
|
-
}, timeoutMs, ref);
|
|
1462
|
+
}, timeoutMs, ref, creationEvidence);
|
|
1463
|
+
rememberMacOsBrowserPageHandle(attachment.metadata.product, ref, page);
|
|
1464
|
+
return page;
|
|
846
1465
|
}
|
|
847
1466
|
|
|
848
1467
|
export async function reattachMacOsBrowserOwnedPage(
|
|
@@ -850,12 +1469,26 @@ export async function reattachMacOsBrowserOwnedPage(
|
|
|
850
1469
|
ref: MacOsBrowserTabRef,
|
|
851
1470
|
timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
|
|
852
1471
|
): Promise<{ page: MacOsAppleEventsPage; attachment: MacOsBrowserAttachment }> {
|
|
1472
|
+
const cacheKey = nativePageHandleKey(product, ref);
|
|
1473
|
+
const cached = warmNativePageHandles.get(cacheKey);
|
|
1474
|
+
if (cached) {
|
|
1475
|
+
cached.updateTimeout(timeoutMs);
|
|
1476
|
+
// Refresh insertion order so bounded eviction behaves as a small LRU.
|
|
1477
|
+
warmNativePageHandles.delete(cacheKey);
|
|
1478
|
+
warmNativePageHandles.set(cacheKey, cached);
|
|
1479
|
+
return { page: cached, attachment: cached.attachmentSnapshot() };
|
|
1480
|
+
}
|
|
853
1481
|
const metadata = await readMacOsBrowserOwnedTabMetadata(product, ref, timeoutMs);
|
|
854
1482
|
const attachment: MacOsBrowserAttachment = {
|
|
855
1483
|
metadata,
|
|
856
1484
|
attempts: [{ product, appName: metadata.appName, bundleId: metadata.bundleId, status: 'selected', frontmost: metadata.frontmost }],
|
|
857
1485
|
};
|
|
858
|
-
|
|
1486
|
+
const resolvedRef = metadata.windowId && metadata.tabId
|
|
1487
|
+
? { windowId: metadata.windowId, tabId: metadata.tabId }
|
|
1488
|
+
: ref;
|
|
1489
|
+
const page = new MacOsAppleEventsPage(attachment, timeoutMs, resolvedRef);
|
|
1490
|
+
rememberMacOsBrowserPageHandle(product, resolvedRef, page);
|
|
1491
|
+
return { page, attachment };
|
|
859
1492
|
}
|
|
860
1493
|
|
|
861
1494
|
export async function closeMacOsBrowserOwnedTab(
|
|
@@ -864,10 +1497,15 @@ export async function closeMacOsBrowserOwnedTab(
|
|
|
864
1497
|
timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
|
|
865
1498
|
): Promise<void> {
|
|
866
1499
|
const browser = browserDefinition(product);
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
1500
|
+
try {
|
|
1501
|
+
const resolvedRef = await resolveCurrentMacOsBrowserTabRef(product, ref, timeoutMs);
|
|
1502
|
+
await runBrowserAutomationText(
|
|
1503
|
+
{ action: 'close_tab', product, ref: resolvedRef },
|
|
1504
|
+
closeOwnedTabScript(browser, resolvedRef),
|
|
1505
|
+
[],
|
|
1506
|
+
timeoutMs,
|
|
1507
|
+
);
|
|
1508
|
+
} finally {
|
|
1509
|
+
invalidateMacOsBrowserPageHandle(product, ref);
|
|
1510
|
+
}
|
|
873
1511
|
}
|