@moretea-labs/forge 1.8.1 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -2
- package/CHANGELOG.md +20 -0
- package/README.en.md +24 -3
- package/README.md +10 -6
- package/README.zh-CN.md +5 -3
- package/THIRD_PARTY_NOTICES.md +1 -2
- package/adapters/chatgpt/browser-delivery-runtime.ts +5 -3
- package/adapters/chatgpt/provider-delivery.ts +1 -1
- package/adapters/chatgpt/work-conversation-binding-store.ts +20 -1
- package/adapters/computer/chatgpt-extension-target.ts +295 -50
- package/adapters/computer/chrome-extension/background.js +227 -71
- package/adapters/computer/chrome-extension/content.js +181 -157
- package/adapters/computer/chrome-extension/manifest.json +3 -1
- package/adapters/computer/desktop-operator-provider.ts +18 -6
- package/adapters/computer/interaction-target-authority.ts +39 -9
- package/adapters/mcp/access-mode.ts +18 -39
- package/adapters/mcp/auth.ts +2 -6
- package/adapters/mcp/instructions.ts +2 -2
- package/adapters/mcp/multi-repository.ts +7 -28
- package/adapters/mcp/runtime-gateway/context-adapter.ts +79 -40
- package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +7 -15
- package/adapters/mcp/runtime-gateway/execution-tools.ts +7 -13
- package/adapters/mcp/runtime-gateway/plugin-adapter.ts +16 -3
- package/adapters/mcp/runtime-gateway/process-tools.ts +3 -21
- package/adapters/mcp/runtime-gateway/router.ts +3 -5
- package/adapters/mcp/runtime-gateway/routing-policy.ts +17 -10
- package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1 -1
- package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +14 -9
- package/adapters/mcp/runtime-gateway/runtime-tools.ts +1 -5
- package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +36 -1
- package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +58 -4
- package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +7 -3
- package/adapters/mcp/runtime-gateway/work-adapter.ts +16 -16
- package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +0 -17
- package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +69 -12
- package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +26 -2
- package/adapters/mcp/server.ts +24 -3
- package/adapters/mcp/setup.ts +4 -10
- package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
- package/adapters/mcp/tool-mapping/legacy-tool-service.ts +672 -2935
- package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +1 -1
- package/adapters/mcp/tool-mapping/repository-tools.ts +52 -86
- package/adapters/mcp/toolset-names.ts +11 -42
- package/adapters/mcp/toolset.ts +16 -90
- package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
- package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
- package/adapters/mcp/transports/http-observation.ts +18 -6
- package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
- package/adapters/mcp/transports/http.ts +54 -14
- package/adapters/mcp/types.ts +0 -6
- package/assets/hooks/lib/session-state.sh +0 -38
- package/assets/hooks/post-bash.sh +1 -1
- package/assets/hooks/post-tool-observer.sh +0 -5
- package/assets/hooks/prompt-guard.sh +0 -55
- package/assets/partials/04-project-structure.partial.md +1 -1
- package/assets/partials/07-footer.partial.md +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +1 -1
- package/assets/partials-agents/08-deep-docs.partial.md +1 -1
- package/assets/reference-configs/agentic-development-flow.md +1 -1
- package/assets/reference-configs/external-tooling.md +0 -76
- package/assets/reference-configs/release-deploy.md +1 -1
- package/assets/templates/helpers/architecture-queue.sh +1 -1
- package/assets/templates/helpers/check-agent-tooling.sh +1 -327
- package/assets/templates/helpers/check-task-workflow.sh +1 -1
- package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
- package/assets/templates/helpers/migrate-project-template.sh +2 -2
- package/assets/workflow-contract.v1.json +0 -2
- package/docs/README.md +5 -2
- package/docs/architecture/CURRENT.md +114 -30
- package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +42 -0
- package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +15 -2
- package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +4 -2
- package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +109 -4
- package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
- package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
- package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
- package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
- package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
- package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
- package/docs/architecture/domains/public-surface.md +3 -5
- package/docs/architecture/domains/verification.md +5 -5
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +2 -2
- package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
- package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
- package/docs/forge-plugin-management.md +14 -2
- package/docs/operations/controller-browser-plugin.md +23 -0
- package/docs/operations/features.md +6 -7
- package/docs/operations/features.zh-CN.md +1 -1
- package/docs/operations/github-repository.md +4 -4
- package/docs/operations/github-semantic-release-projection.md +20 -0
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/mcp-tool-exposure.md +13 -54
- package/docs/operations/runtime-performance-diagnostics.md +8 -0
- package/docs/operations/standalone-disaster-recovery.md +1 -1
- package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
- package/docs/tutorials/03-first-repository-task.md +2 -0
- package/docs/wiki/Computer-and-Plugins.md +78 -0
- package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
- package/docs/wiki/Home.md +16 -2
- package/docs/wiki/Implementation.md +24 -12
- package/docs/wiki/Installation.md +28 -32
- package/docs/wiki/Integrations.md +4 -0
- package/docs/wiki/Quick-Start.md +35 -22
- package/docs/wiki/Releases-and-Upgrades.md +15 -25
- package/docs/wiki/_Sidebar.md +5 -1
- package/package.json +1 -12
- package/packages/kernel/controller/infrastructure/controller-session-store.ts +63 -38
- package/packages/kernel/identity/infrastructure/activity-projection-store.ts +10 -0
- package/packages/kernel/scheduler/application/eligibility.ts +4 -1
- package/packages/kernel/scheduler/infrastructure/schedule-store.ts +13 -10
- package/packages/kernel/work/application/work-service.ts +1 -0
- package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
- package/packages/kernel/work/domain/state-machine.ts +9 -2
- package/packages/kernel/work/infrastructure/work-contract-store.ts +28 -1
- package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
- package/packages/plugin-runtime/computer/conversation-target.ts +17 -7
- package/scripts/assemble-template.ts +11 -72
- package/scripts/check-mcp-compatibility.ts +55 -74
- package/scripts/check-package-identity.mjs +0 -5
- package/scripts/check-runtime-architecture.mjs +305 -28
- package/scripts/init-project.sh +0 -46
- package/scripts/install-ios-hid-toolchain.sh +60 -0
- package/scripts/install-standalone-recovery.ts +1 -1
- package/scripts/lib/project-init-lib.sh +3 -3
- package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
- package/scripts/open-source-audit-allowlist.txt +0 -1
- package/scripts/run-governed-gate.ts +2 -0
- package/scripts/smoke-ios-physical-device.ts +38 -10
- package/scripts/smoke-mcp-http-runtime.ts +7 -8
- package/scripts/smoke-mcp-tool-surface.ts +27 -41
- package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +6 -3
- package/scripts/sync-generated-authority.mjs +0 -1
- package/src/cli/agent-jobs/executor-health.ts +1 -1
- package/src/cli/agent-jobs/integration.ts +2 -2
- package/src/cli/chatgpt-browser/file-policy.ts +2 -2
- package/src/cli/commands/doctor.ts +1 -143
- package/src/cli/commands/global-runtime.ts +3 -196
- package/src/cli/commands/init-hook.ts +6 -23
- package/src/cli/commands/init.ts +3 -252
- package/src/cli/commands/mcp.ts +0 -5
- package/src/cli/commands/prompt-guard-decision.ts +0 -2
- package/src/cli/commands/recovery.ts +2 -14
- package/src/cli/commands/repository.ts +17 -1
- package/src/cli/commands/setup-execution.ts +41 -0
- package/src/cli/controller/completion-orchestrator.ts +1 -1
- package/src/cli/controller/composite-operations.ts +1 -1
- package/src/cli/controller/context/instruction-context.ts +1 -1
- package/src/cli/controller/context/known-paths.ts +2 -2
- package/src/cli/controller/context/query-planning.ts +11 -4
- package/src/cli/controller/context/rule-resolution.ts +2 -2
- package/src/cli/controller/context/source-materializer.ts +2 -2
- package/src/cli/controller/context/structural-provider.ts +84 -0
- package/src/cli/controller/context/structural.ts +6 -6
- package/src/cli/controller/context/types.ts +8 -9
- package/src/cli/controller/context-pack.ts +97 -76
- package/src/cli/controller/operational-plan.ts +1 -1
- package/src/cli/controller/systemd-user.ts +4 -10
- package/src/cli/editing/edit-session.ts +17 -5
- package/src/cli/github/github.ts +4 -4
- package/src/cli/github/semantic-projection.ts +174 -0
- package/src/cli/hook/prompt-intents.ts +0 -9
- package/src/cli/index.ts +1 -43
- package/src/cli/local-bridge/connector-freshness.ts +1 -12
- package/src/cli/local-bridge/console-automations.ts +1 -1
- package/src/cli/local-bridge/console-view-models.ts +1 -1
- package/src/cli/local-bridge/facade-api.ts +0 -2
- package/src/cli/repositories/command-classifier.ts +0 -9
- package/src/cli/repositories/command-scope.ts +2 -2
- package/src/cli/repositories/locks.ts +20 -0
- package/src/cli/repositories/registry.ts +2 -1
- package/src/cli/repositories/safe-patch.ts +2 -12
- package/src/cli/repositories/structured-git.ts +39 -3
- package/src/cli/repository/inspector.ts +185 -343
- package/src/effects/process-runner.ts +17 -1
- package/src/runtime/context/generic-lsp-provider.ts +1 -1
- package/src/runtime/context/semantic-navigation-contract.ts +7 -0
- package/src/runtime/context/semantic-navigation.ts +1 -1
- package/src/runtime/control-plane/cleanup-artifact-retention.ts +4 -4
- package/src/runtime/control-plane/execution/work-delivery-service.ts +150 -31
- package/src/runtime/control-plane/execution/work-handle-authority.ts +12 -4
- package/src/runtime/control-plane/execution/work-handle-store.ts +53 -1
- package/src/runtime/control-plane/execution/work-operation-service.ts +20 -10
- package/src/runtime/control-plane/execution/work-preparation-service.ts +108 -4
- package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +28 -13
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +5 -1
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +1 -1
- package/src/runtime/control-plane/facade/capability-registry.ts +2 -0
- package/src/runtime/control-plane/facade/work-continuation.ts +1 -1
- package/src/runtime/control-plane/generated-cache-retention.ts +114 -63
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +20 -8
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -11
- package/src/runtime/control-plane/governance/external-effects.ts +1 -1
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +1 -0
- package/src/runtime/control-plane/runtime-cleanup.ts +12 -93
- package/src/runtime/evidence/sensitive-output.ts +22 -2
- package/src/runtime/execution/jobs/child-reference.ts +11 -0
- package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
- package/src/runtime/execution/process-runtime/command-facade.ts +3 -3
- package/src/runtime/execution/process-runtime/runtime.ts +20 -16
- package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
- package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
- package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
- package/src/runtime/execution/workers/executor.ts +1 -2
- package/src/runtime/maintenance/cleanup.ts +41 -5
- package/src/runtime/plugins/action-application.ts +12 -1
- package/src/runtime/plugins/app-store-connect-adapter.ts +252 -4
- package/src/runtime/plugins/browser-adapter.ts +381 -313
- package/src/runtime/plugins/browser-macos-bridge.ts +114 -112
- package/src/runtime/plugins/browser-manifest-surface.ts +2 -1
- package/src/runtime/plugins/browser-registration.ts +2 -0
- package/src/runtime/plugins/browser-runtime.ts +26 -35
- package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +11 -2
- package/src/runtime/plugins/computer-registration.ts +76 -9
- package/src/runtime/plugins/github-adapter.ts +30 -3
- package/src/runtime/plugins/gmail-adapter.ts +166 -1
- package/src/runtime/plugins/google-shared.ts +10 -5
- package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
- package/src/runtime/plugins/ios-adapter.ts +45 -40
- package/src/runtime/plugins/ios-agent-device.ts +54 -4
- package/src/runtime/plugins/ios-device-ownership.ts +103 -0
- package/src/runtime/plugins/ios-physical-device.ts +497 -79
- package/src/runtime/plugins/local-system-adapter.ts +1 -99
- package/src/runtime/plugins/store.ts +80 -7
- package/src/runtime/plugins/types.ts +4 -0
- package/src/runtime/projections/git-status-sampler.ts +2 -19
- package/src/runtime/recovery/actions.ts +1 -1
- package/src/runtime/recovery/lifecycle-inventory.ts +1 -11
- package/src/runtime/recovery/probe.ts +2 -2
- package/src/runtime/release/release-coordinator.ts +20 -4
- package/src/runtime/root/controller-progression-composition.ts +67 -5
- package/src/runtime/root/package-connector-service.ts +123 -11
- package/src/runtime/root/package-runtime-release.ts +6 -18
- package/src/runtime/root/package-runtime-service.ts +7 -13
- package/src/runtime/root/release-execution-canary.ts +18 -5
- package/src/runtime/root/release-manifest.ts +0 -34
- package/src/runtime/root/release-materialize.ts +0 -78
- package/src/runtime/root/runtime.ts +1 -2
- package/src/runtime/root/scheduled-controller-composition.ts +2 -1
- package/src/runtime/root/service.ts +90 -11
- package/src/runtime/root/types.ts +0 -6
- package/src/runtime/root/workflow-supervisor-composition.ts +207 -19
- package/src/runtime/root/workflow-supervisor-runtime.ts +365 -10
- package/src/runtime/safe-tooling/google-credential-store.ts +1 -11
- package/src/runtime/safe-tooling/ios-development.ts +22 -8
- package/src/runtime/safe-tooling/job-summary.ts +1 -1
- package/src/runtime/safe-tooling/workspace-auth.ts +4 -1
- package/src/runtime/shared/atomic-text-file.ts +21 -0
- package/src/runtime/shared/json-files.ts +8 -3
- package/src/runtime/standalone-recovery/core.ts +189 -174
- package/src/runtime/standalone-recovery/entry.ts +62 -30
- package/src/runtime/standalone-recovery/operation-lock.ts +11 -1
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -0
- package/src/runtime/workflow/schedules/engine.ts +36 -14
- package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
- package/src/runtime/workflow/schedules/work-continuation.ts +51 -6
- package/supervisor/client.ts +31 -14
- package/supervisor/control-plane.ts +210 -115
- package/supervisor/forge-validators.ts +88 -21
- package/supervisor/native-browser-adapter.ts +393 -28
- package/supervisor/native-messaging/host.ts +21 -2
- package/supervisor/protocol.ts +74 -26
- package/supervisor/server.ts +16 -8
- package/supervisor/store.ts +476 -57
- package/supervisor/types.ts +19 -13
- package/SKILL.md +0 -427
- package/adapters/computer/chatgpt-target-router.ts +0 -103
- package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +0 -87
- package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
- package/adapters/mcp/toolset-selection.ts +0 -41
- package/agents/openai.yaml +0 -4
- package/assets/skill-hooks.json +0 -41
- package/assets/skill-version.json +0 -189
- package/assets/skills/claude-review/SKILL.md +0 -231
- package/assets/skills/codex-review/SKILL.md +0 -103
- package/assets/templates/helpers/check-skill-version.ts +0 -193
- package/scripts/check-skill-version.ts +0 -5
- package/scripts/ensure-codegraph.sh +0 -16
- package/scripts/run-skill-hook.ts +0 -244
- package/scripts/sync-codex-installed-copies.sh +0 -182
- package/src/cli/commands/tools.ts +0 -167
- package/src/cli/mcp/access-mode.ts +0 -2
- package/src/cli/mcp/access-tools.ts +0 -2
- package/src/cli/mcp/audit.ts +0 -2
- package/src/cli/mcp/auth.ts +0 -2
- package/src/cli/mcp/instructions.ts +0 -2
- package/src/cli/mcp/legacy-context.ts +0 -2
- package/src/cli/mcp/legacy-tool-service.ts +0 -2
- package/src/cli/mcp/multi-repository.ts +0 -2
- package/src/cli/mcp/oauth.ts +0 -2
- package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
- package/src/cli/mcp/paths.ts +0 -2
- package/src/cli/mcp/policy.ts +0 -2
- package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
- package/src/cli/mcp/proxy-env.ts +0 -2
- package/src/cli/mcp/readiness-projection.ts +0 -2
- package/src/cli/mcp/redaction.ts +0 -2
- package/src/cli/mcp/repo.ts +0 -2
- package/src/cli/mcp/repository-tools.ts +0 -2
- package/src/cli/mcp/server.ts +0 -2
- package/src/cli/mcp/setup.ts +0 -2
- package/src/cli/mcp/tool-capability-cache.ts +0 -2
- package/src/cli/mcp/tool-contract.ts +0 -2
- package/src/cli/mcp/tools.ts +0 -2
- package/src/cli/mcp/toolset-names.ts +0 -2
- package/src/cli/mcp/toolset-selection.ts +0 -2
- package/src/cli/mcp/toolset.ts +0 -2
- package/src/cli/mcp/transports/http.ts +0 -2
- package/src/cli/mcp/transports/session-registry.ts +0 -2
- package/src/cli/mcp/transports/stdio.ts +0 -2
- package/src/cli/mcp/types.ts +0 -2
- package/src/cli/mcp/umbrella-tools.ts +0 -2
- package/src/cli/tools/codegraph.ts +0 -695
- package/src/core/adoption/helper-wrapper-plan.ts +0 -159
- package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
- package/src/runtime/context/codegraph-read-provider.ts +0 -611
- package/src/runtime/context/codegraph-sidecar.cjs +0 -248
- package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
- package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
- package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
- package/src/runtime/gateway/mcp/process-tools.ts +0 -2
- package/src/runtime/gateway/mcp/router.ts +0 -2
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
- package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
- package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
- package/src/runtime/plugins/browser-provider-registry.ts +0 -2
- package/src/runtime/plugins/computer-chatgpt-macos-target.ts +0 -891
|
@@ -5,6 +5,7 @@ import { createServer as createNetServer } from 'net';
|
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'fs';
|
|
6
6
|
import { homedir } from 'os';
|
|
7
7
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
8
9
|
import type {
|
|
9
10
|
AssistantPluginActionExecutionInput,
|
|
10
11
|
AssistantPluginAuthorizationContext,
|
|
@@ -22,7 +23,6 @@ import {
|
|
|
22
23
|
browserPermissions,
|
|
23
24
|
} from './browser-manifest-surface';
|
|
24
25
|
import { executeBrowserActionThroughNode, shouldUseBrowserNodeBridge } from './browser-node-bridge';
|
|
25
|
-
import { BrowserProviderUnavailableBeforeActionError } from '../../../packages/plugin-runtime/browser/provider-registry';
|
|
26
26
|
import { MAX_BROWSER_CDP_ENDPOINT_CANDIDATES } from '../../../packages/plugin-runtime/browser/runtime-contract';
|
|
27
27
|
import {
|
|
28
28
|
ALL_BROWSER_PROVIDER_CAPABILITIES,
|
|
@@ -33,6 +33,8 @@ import {
|
|
|
33
33
|
invalidateBrowserRuntime,
|
|
34
34
|
} from './browser-runtime';
|
|
35
35
|
import { activateRuntimeComputerBrowserApplication } from '../root/computer-composition';
|
|
36
|
+
import { resolveWorkspaceTargetPath, WorkspaceTargetGrantError } from '../workspace-targets';
|
|
37
|
+
import { pluginCapabilityAuthorizationOwnerScope } from './capability-authorization-grants';
|
|
36
38
|
import {
|
|
37
39
|
DEFAULT_BROWSER_SESSION_LIST_LIMIT,
|
|
38
40
|
MAX_BROWSER_SESSION_LIST_LIMIT,
|
|
@@ -55,6 +57,7 @@ import {
|
|
|
55
57
|
readMacOsBrowserOwnedTabMetadata,
|
|
56
58
|
macOsActiveBrowserAttachSupported,
|
|
57
59
|
macOsBrowserJavaScriptAutomationDisabled,
|
|
60
|
+
assertMacOsBrowserApplicationRunning,
|
|
58
61
|
type MacOsBrowserAttachAttempt,
|
|
59
62
|
type MacOsBrowserCreateTabEvidence,
|
|
60
63
|
type MacOsBrowserProduct,
|
|
@@ -350,6 +353,15 @@ type PlaywrightRuntime = {
|
|
|
350
353
|
};
|
|
351
354
|
};
|
|
352
355
|
|
|
356
|
+
export function browserDependencyRequireAnchors(repoRoot?: string, moduleUrl = import.meta.url): string[] {
|
|
357
|
+
const releasePackageManifest = join(dirname(fileURLToPath(moduleUrl)), 'package', 'package.json');
|
|
358
|
+
return [
|
|
359
|
+
existsSync(releasePackageManifest) ? releasePackageManifest : undefined,
|
|
360
|
+
repoRoot ? join(repoRoot, 'package.json') : undefined,
|
|
361
|
+
moduleUrl,
|
|
362
|
+
].filter((anchor): anchor is string => typeof anchor === 'string');
|
|
363
|
+
}
|
|
364
|
+
|
|
353
365
|
interface BrowserPluginRuntimeHooks {
|
|
354
366
|
now(): string;
|
|
355
367
|
moduleAvailable(name: string, repoRoot?: string): boolean;
|
|
@@ -407,9 +419,7 @@ const defaultRuntimeHooks: BrowserPluginRuntimeHooks = {
|
|
|
407
419
|
}),
|
|
408
420
|
connectBrowserCdp: connectNativeBrowserCdp,
|
|
409
421
|
moduleAvailable: (name: string, repoRoot?: string) => {
|
|
410
|
-
const
|
|
411
|
-
.filter((value): value is string => Boolean(value));
|
|
412
|
-
for (const anchor of anchors) {
|
|
422
|
+
for (const anchor of browserDependencyRequireAnchors(repoRoot)) {
|
|
413
423
|
try {
|
|
414
424
|
createRequire(anchor).resolve(name);
|
|
415
425
|
return true;
|
|
@@ -420,16 +430,14 @@ const defaultRuntimeHooks: BrowserPluginRuntimeHooks = {
|
|
|
420
430
|
return false;
|
|
421
431
|
},
|
|
422
432
|
loadPlaywright: (repoRoot?: string) => {
|
|
423
|
-
const
|
|
424
|
-
.filter((value): value is string => Boolean(value));
|
|
425
|
-
for (const anchor of anchors) {
|
|
433
|
+
for (const anchor of browserDependencyRequireAnchors(repoRoot)) {
|
|
426
434
|
try {
|
|
427
435
|
return createRequire(anchor)('playwright') as PlaywrightRuntime;
|
|
428
436
|
} catch {
|
|
429
437
|
// Try the next trusted dependency anchor.
|
|
430
438
|
}
|
|
431
439
|
}
|
|
432
|
-
throw new AssistantPluginError('PLUGIN_DEPENDENCY_MISSING', 'Browser plugin
|
|
440
|
+
throw new AssistantPluginError('PLUGIN_DEPENDENCY_MISSING', 'Browser plugin cannot resolve packaged Playwright or an authorized repository-local installation.', {
|
|
433
441
|
retryable: false,
|
|
434
442
|
});
|
|
435
443
|
},
|
|
@@ -579,7 +587,7 @@ function normalizeConfig(raw: PersistedBrowserPluginConfig): BrowserPluginConfig
|
|
|
579
587
|
schemaVersion: CURRENT_BROWSER_CONFIG_SCHEMA_VERSION,
|
|
580
588
|
enabled: raw.enabled === true,
|
|
581
589
|
provider: 'playwright',
|
|
582
|
-
browserMode: browserMode(raw.browserMode) ?? '
|
|
590
|
+
browserMode: browserMode(raw.browserMode) ?? 'attach_preferred',
|
|
583
591
|
profileMode: browserProfileMode(raw.profileMode) ?? (normalizedProfileDir ? 'custom' : 'repo_local'),
|
|
584
592
|
profileDir: normalizedProfileDir,
|
|
585
593
|
profileDirectory: stringValue(raw.profileDirectory),
|
|
@@ -591,7 +599,7 @@ function normalizeConfig(raw: PersistedBrowserPluginConfig): BrowserPluginConfig
|
|
|
591
599
|
? Math.min(positiveNumber(raw.cdpDiscoveryTimeoutMs, DEFAULT_CDP_DISCOVERY_TIMEOUT_MS), MAX_CDP_DISCOVERY_TIMEOUT_MS)
|
|
592
600
|
: undefined,
|
|
593
601
|
cdpAttachFallback: browserCdpAttachFallback(raw.cdpAttachFallback) ?? 'fail_closed',
|
|
594
|
-
nativeAttachMode: browserNativeAttachMode(raw.nativeAttachMode) ?? '
|
|
602
|
+
nativeAttachMode: browserNativeAttachMode(raw.nativeAttachMode) ?? 'auto',
|
|
595
603
|
nativeBrowserCandidates: browserProductList(raw.nativeBrowserCandidates) ?? [...DEFAULT_USER_NATIVE_BROWSER_CANDIDATES],
|
|
596
604
|
defaultTimeoutMs: typeof raw.defaultTimeoutMs === 'number' ? positiveNumber(raw.defaultTimeoutMs, DEFAULT_TIMEOUT_MS) : undefined,
|
|
597
605
|
};
|
|
@@ -599,23 +607,37 @@ function normalizeConfig(raw: PersistedBrowserPluginConfig): BrowserPluginConfig
|
|
|
599
607
|
|
|
600
608
|
function migrateLegacyRepositoryConfig(raw: PersistedBrowserPluginConfig): PersistedBrowserPluginConfig {
|
|
601
609
|
if (raw.schemaVersion === CURRENT_BROWSER_CONFIG_SCHEMA_VERSION) return raw;
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
const legacyMode = browserMode(raw.browserMode) ?? 'attach_preferred';
|
|
610
|
+
// v1 legacy managed profiles were an implicit route to a fresh browser.
|
|
611
|
+
// Migrate that default to the existing signed-in native browser; preserve
|
|
612
|
+
// explicit v2 managed configurations as separate scripting capabilities.
|
|
613
|
+
const legacyV1 = raw.schemaVersion === 1;
|
|
607
614
|
return {
|
|
608
615
|
...raw,
|
|
609
616
|
schemaVersion: CURRENT_BROWSER_CONFIG_SCHEMA_VERSION,
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
617
|
+
browserMode: legacyV1 ? 'attach_preferred' : (browserMode(raw.browserMode) ?? 'attach_preferred'),
|
|
618
|
+
cdpAttachFallback: legacyV1 ? 'fail_closed' : (raw.cdpAttachFallback ?? 'fail_closed'),
|
|
619
|
+
nativeAttachMode: legacyV1 ? 'auto' : (raw.nativeAttachMode ?? 'auto'),
|
|
620
|
+
...(legacyV1 ? {
|
|
621
|
+
profileMode: 'repo_local' as const, profileDir: undefined, profileDirectory: undefined,
|
|
622
|
+
browserChannel: DEFAULT_USER_BROWSER_CHANNEL, nativeBrowserCandidates: [...DEFAULT_USER_NATIVE_BROWSER_CANDIDATES],
|
|
623
|
+
} : {}),
|
|
616
624
|
};
|
|
617
625
|
}
|
|
618
626
|
|
|
627
|
+
export function normalizeLegacyBrowserConfig(raw: unknown): BrowserPluginConfig {
|
|
628
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
629
|
+
throw new Error('PLUGIN_CONFIG_MIGRATION_INVALID: browser config must be an object');
|
|
630
|
+
}
|
|
631
|
+
const parsed = raw as PersistedBrowserPluginConfig;
|
|
632
|
+
if (![1, 2, 3].includes(parsed.schemaVersion as number)) {
|
|
633
|
+
throw new Error('PLUGIN_CONFIG_MIGRATION_INVALID: unsupported browser schemaVersion');
|
|
634
|
+
}
|
|
635
|
+
const migrated = normalizeConfig(migrateLegacyRepositoryConfig(parsed));
|
|
636
|
+
const errors = validateConfig(migrated);
|
|
637
|
+
if (errors.length) throw new Error(`PLUGIN_CONFIG_MIGRATION_INVALID: ${errors[0]}`);
|
|
638
|
+
return migrated;
|
|
639
|
+
}
|
|
640
|
+
|
|
619
641
|
function readJson<T>(path: string): T | undefined {
|
|
620
642
|
try {
|
|
621
643
|
return JSON.parse(readFileSync(path, 'utf-8')) as T;
|
|
@@ -629,12 +651,24 @@ function writeJson(path: string, value: unknown): void {
|
|
|
629
651
|
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, 'utf-8');
|
|
630
652
|
}
|
|
631
653
|
|
|
632
|
-
function loadConfig(
|
|
654
|
+
function loadConfig(
|
|
655
|
+
repoRoot: string,
|
|
656
|
+
context?: Pick<RepositoryPluginConfigContext, 'controllerHome' | 'repoId'>,
|
|
657
|
+
computerNativeOnly = false,
|
|
658
|
+
): BrowserPluginConfig {
|
|
659
|
+
// Computer Browser is a built-in Forge capability, not an optional
|
|
660
|
+
// repository Browser-script plugin. Its native defaults ship with the
|
|
661
|
+
// first-party Computer adapter; never inherit an independent Playwright
|
|
662
|
+
// profile, enabled flag, CDP endpoint, or fallback from a repository.
|
|
663
|
+
if (computerNativeOnly) return normalizeConfig({
|
|
664
|
+
enabled: true, browserMode: 'attach_preferred',
|
|
665
|
+
nativeAttachMode: 'auto', cdpAttachFallback: 'fail_closed',
|
|
666
|
+
nativeBrowserCandidates: [...DEFAULT_USER_NATIVE_BROWSER_CANDIDATES],
|
|
667
|
+
});
|
|
633
668
|
const persisted = context
|
|
634
669
|
? readRepositoryPluginConfig<PersistedBrowserPluginConfig>({ ...context, repoRoot }, BROWSER_PLUGIN_ID)
|
|
635
670
|
: undefined;
|
|
636
|
-
|
|
637
|
-
return normalizeConfig(migrateLegacyRepositoryConfig(persisted));
|
|
671
|
+
return persisted ? normalizeConfig(migrateLegacyRepositoryConfig(persisted)) : normalizeConfig({});
|
|
638
672
|
}
|
|
639
673
|
|
|
640
674
|
async function resolveBrowserPluginAuthorizationContextInternal(
|
|
@@ -649,7 +683,7 @@ async function resolveBrowserPluginAuthorizationContextInternal(
|
|
|
649
683
|
// call and left the Scheduler without a durable browser-session grant.
|
|
650
684
|
if (!action || action.confirmation !== 'authorization' || !action.scopes.includes('browser.profile')) return undefined;
|
|
651
685
|
|
|
652
|
-
const persistedConfig = loadConfig(input.repoRoot, input);
|
|
686
|
+
const persistedConfig = loadConfig(input.repoRoot, input, input.pluginId === 'computer');
|
|
653
687
|
const sessionId = stringValue(input.args.session_id);
|
|
654
688
|
const authorizationBrowserMode = parseBrowserModeInput(input.args.browser_mode) ?? persistedConfig.browserMode;
|
|
655
689
|
if (!sessionId || authorizationBrowserMode === 'isolated') return undefined;
|
|
@@ -723,6 +757,10 @@ export async function resolveBrowserPluginAuthorizationContext(
|
|
|
723
757
|
|
|
724
758
|
function saveConfig(input: Pick<AssistantPluginActionExecutionInput, 'controllerHome' | 'repoId' | 'repoRoot'>, patch: Partial<BrowserPluginConfig>): BrowserPluginConfig {
|
|
725
759
|
const next = normalizeConfig({ ...loadConfig(input.repoRoot, input), ...patch });
|
|
760
|
+
const errors = validateConfig(next);
|
|
761
|
+
if (errors.length > 0) {
|
|
762
|
+
throw new AssistantPluginError('PLUGIN_CONFIGURATION_INVALID', errors[0], { retryable: false });
|
|
763
|
+
}
|
|
726
764
|
return writeRepositoryPluginConfig(input, BROWSER_PLUGIN_ID, next);
|
|
727
765
|
}
|
|
728
766
|
|
|
@@ -914,11 +952,11 @@ function validateConfig(config: BrowserPluginConfig): string[] {
|
|
|
914
952
|
const error = cdpEndpointValidationError(endpoint);
|
|
915
953
|
if (error) errors.push(error);
|
|
916
954
|
}
|
|
917
|
-
if (config.browserMode === 'attach_preferred'
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
955
|
+
if (config.browserMode === 'attach_preferred') {
|
|
956
|
+
if (endpoints.length > 1) errors.push('One Browser attach capability may bind only one CDP endpoint.');
|
|
957
|
+
if (config.cdpAttachFallback === 'managed_persistent') errors.push('Managed Browser is an explicit browser_mode, never an attach fallback.');
|
|
958
|
+
// The configured list is discovery scope; execution must bind exactly one
|
|
959
|
+
// provider from the actual tab/product evidence, never select by fallback.
|
|
922
960
|
}
|
|
923
961
|
return errors;
|
|
924
962
|
}
|
|
@@ -931,14 +969,8 @@ function configWarnings(config: BrowserPluginConfig): string[] {
|
|
|
931
969
|
warnings.push('Custom profile mode is more reliable with an explicit Chrome channel or executable path that matches the selected profile format.');
|
|
932
970
|
}
|
|
933
971
|
}
|
|
934
|
-
if (config.browserMode === 'attach_preferred' && cdpEndpoints(config).length === 0) {
|
|
935
|
-
|
|
936
|
-
warnings.push(config.cdpAttachFallback === 'fail_closed'
|
|
937
|
-
? 'browserMode=attach_preferred has no configured CDP endpoint; configured native-browser attach will be attempted and managed fallback is disabled.'
|
|
938
|
-
: 'browserMode=attach_preferred has no configured CDP endpoint; configured native-browser attach will be attempted before explicit managed fallback.');
|
|
939
|
-
} else {
|
|
940
|
-
warnings.push('browserMode=attach_preferred has no configured CDP endpoint; actions will use the configured fallback policy.');
|
|
941
|
-
}
|
|
972
|
+
if (config.browserMode === 'attach_preferred' && cdpEndpoints(config).length === 0 && config.nativeAttachMode === 'auto') {
|
|
973
|
+
warnings.push('Native browser attach is the single selected provider; CDP and managed browsers are not automatic alternatives.');
|
|
942
974
|
}
|
|
943
975
|
if (config.browserMode === 'isolated') {
|
|
944
976
|
warnings.push('browserMode=isolated uses a per-session repo-local profile and will not share the default browser profile.');
|
|
@@ -1391,54 +1423,7 @@ function sameOrigin(left: string, right: string): boolean {
|
|
|
1391
1423
|
}
|
|
1392
1424
|
}
|
|
1393
1425
|
|
|
1394
|
-
function
|
|
1395
|
-
try {
|
|
1396
|
-
const url = new URL(value);
|
|
1397
|
-
if (url.protocol !== 'https:' && url.protocol !== 'http:') return undefined;
|
|
1398
|
-
const segments = url.pathname.split('/').filter(Boolean);
|
|
1399
|
-
for (let index = 0; index + 1 < segments.length; index += 1) {
|
|
1400
|
-
if ((segments[index] ?? '').toLowerCase() !== 'apps') continue;
|
|
1401
|
-
const id = segments[index + 1]?.trim();
|
|
1402
|
-
if (id) return `${url.origin}/apps/${id}`;
|
|
1403
|
-
}
|
|
1404
|
-
return undefined;
|
|
1405
|
-
} catch {
|
|
1406
|
-
return undefined;
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
function selectReusableNativeTab(
|
|
1411
|
-
referenceUrls: string[],
|
|
1412
|
-
inventory: Awaited<ReturnType<typeof listMacOsBrowserTabs>>,
|
|
1413
|
-
preferredWindowId?: string,
|
|
1414
|
-
): {
|
|
1415
|
-
candidate?: (typeof inventory.tabs)[number];
|
|
1416
|
-
match: 'exact_url' | 'stable_entity' | 'none' | 'ambiguous';
|
|
1417
|
-
candidateCount: number;
|
|
1418
|
-
} {
|
|
1419
|
-
const normalizedReferences = [...new Set(referenceUrls.filter(Boolean).map(comparableUrl))];
|
|
1420
|
-
const entityKeys = new Set(normalizedReferences.map(stableBrowserEntityKey).filter((value): value is string => Boolean(value)));
|
|
1421
|
-
const scored = inventory.tabs.map((candidate) => {
|
|
1422
|
-
const normalizedCandidate = comparableUrl(candidate.url);
|
|
1423
|
-
const entityKey = stableBrowserEntityKey(candidate.url);
|
|
1424
|
-
const matchScore = normalizedReferences.includes(normalizedCandidate)
|
|
1425
|
-
? 100
|
|
1426
|
-
: entityKey && entityKeys.has(entityKey)
|
|
1427
|
-
? 50
|
|
1428
|
-
: 0;
|
|
1429
|
-
const score = matchScore === 0
|
|
1430
|
-
? 0
|
|
1431
|
-
: matchScore + (preferredWindowId && candidate.windowId === preferredWindowId ? 10 : 0) + (candidate.active ? 1 : 0);
|
|
1432
|
-
return { candidate, score, matchScore };
|
|
1433
|
-
});
|
|
1434
|
-
const maxScore = scored.reduce((max, entry) => Math.max(max, entry.score), 0);
|
|
1435
|
-
if (maxScore === 0) return { match: 'none', candidateCount: 0 };
|
|
1436
|
-
const matches = scored.filter((entry) => entry.score === maxScore);
|
|
1437
|
-
if (matches.length !== 1) return { match: 'ambiguous', candidateCount: matches.length };
|
|
1438
|
-
return { candidate: matches[0]!.candidate, match: matches[0]!.matchScore === 100 ? 'exact_url' : 'stable_entity', candidateCount: 1 };
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
function selectLiveForgeOwnedNativeTab(
|
|
1426
|
+
export function selectLiveForgeOwnedNativeTab(
|
|
1442
1427
|
repoRoot: string,
|
|
1443
1428
|
targetUrl: string,
|
|
1444
1429
|
product: MacOsBrowserProduct,
|
|
@@ -1464,11 +1449,11 @@ function selectLiveForgeOwnedNativeTab(
|
|
|
1464
1449
|
}
|
|
1465
1450
|
|
|
1466
1451
|
const values = [...candidates.values()];
|
|
1452
|
+
if (inventory.truncated) return { match: 'ambiguous', candidateCount: values.length };
|
|
1467
1453
|
const exact = values.filter(({ candidate }) => comparableUrl(candidate.url) === comparableUrl(targetUrl));
|
|
1468
1454
|
const choose = (matches: typeof values, match: 'exact_url' | 'same_origin') => {
|
|
1469
1455
|
if (matches.length === 1) return { ...matches[0], match, candidateCount: 1 } as const;
|
|
1470
|
-
|
|
1471
|
-
if (active.length === 1) return { ...active[0], match, candidateCount: matches.length } as const;
|
|
1456
|
+
// Multiple matching tabs remain ambiguous even when one is active.
|
|
1472
1457
|
return { match: 'ambiguous' as const, candidateCount: matches.length };
|
|
1473
1458
|
};
|
|
1474
1459
|
if (exact.length > 0) return choose(exact, 'exact_url');
|
|
@@ -1478,15 +1463,15 @@ function selectLiveForgeOwnedNativeTab(
|
|
|
1478
1463
|
return { match: 'none', candidateCount: 0 };
|
|
1479
1464
|
}
|
|
1480
1465
|
|
|
1481
|
-
function selectExactNativeTab(
|
|
1466
|
+
export function selectExactNativeTab(
|
|
1482
1467
|
targetUrl: string,
|
|
1483
1468
|
inventory: Awaited<ReturnType<typeof listMacOsBrowserTabs>>,
|
|
1484
1469
|
): { candidate?: (typeof inventory.tabs)[number]; match: 'exact_url' | 'none' | 'ambiguous'; candidateCount: number } {
|
|
1470
|
+
if (inventory.truncated) return { match: 'ambiguous', candidateCount: inventory.tabs.length };
|
|
1485
1471
|
const matches = inventory.tabs.filter((candidate) => comparableUrl(candidate.url) === comparableUrl(targetUrl));
|
|
1486
1472
|
if (matches.length === 0) return { match: 'none', candidateCount: 0 };
|
|
1487
1473
|
if (matches.length === 1) return { candidate: matches[0], match: 'exact_url', candidateCount: 1 };
|
|
1488
|
-
|
|
1489
|
-
if (active.length === 1) return { candidate: active[0], match: 'exact_url', candidateCount: matches.length };
|
|
1474
|
+
// A frontmost tab does not disambiguate two tabs with the same URL.
|
|
1490
1475
|
return { match: 'ambiguous', candidateCount: matches.length };
|
|
1491
1476
|
}
|
|
1492
1477
|
|
|
@@ -1519,7 +1504,7 @@ async function inventoryTabs(context: BrowserContextLike): Promise<BrowserTabInv
|
|
|
1519
1504
|
return entries;
|
|
1520
1505
|
}
|
|
1521
1506
|
|
|
1522
|
-
function chooseTab(
|
|
1507
|
+
export function chooseTab(
|
|
1523
1508
|
inventory: BrowserTabInventoryEntry[],
|
|
1524
1509
|
target: BrowserActionTarget,
|
|
1525
1510
|
options: { allowBlank?: boolean; requireOwnedToken?: boolean } = {},
|
|
@@ -1527,6 +1512,20 @@ function chooseTab(
|
|
|
1527
1512
|
const desiredUrl = comparableUrl(target.url);
|
|
1528
1513
|
const savedTab = target.existingSession?.browser?.tab;
|
|
1529
1514
|
const expectedOwnerToken = savedTab?.ownership === 'plugin_owned' ? savedTab.ownerToken : undefined;
|
|
1515
|
+
const unique = (matches: BrowserTabInventoryEntry[], basis: string): BrowserTabInventoryEntry | undefined => {
|
|
1516
|
+
if (matches.length > 1) {
|
|
1517
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_REUSABLE_TAB_AMBIGUOUS',
|
|
1518
|
+
'Multiple browser tabs satisfy the same lookup; exact tab identity is not proven.',
|
|
1519
|
+
{ retryable: false, details: { sessionId: target.sessionId, basis, candidateCount: matches.length } });
|
|
1520
|
+
}
|
|
1521
|
+
return matches[0];
|
|
1522
|
+
};
|
|
1523
|
+
if (options.requireOwnedToken && expectedOwnerToken
|
|
1524
|
+
&& inventory.filter((entry) => entry.ownerToken === expectedOwnerToken).length > 1) {
|
|
1525
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_REUSABLE_TAB_AMBIGUOUS',
|
|
1526
|
+
'Multiple browser tabs report the same owned marker.',
|
|
1527
|
+
{ retryable: false, details: { sessionId: target.sessionId, basis: 'owned_token' } });
|
|
1528
|
+
}
|
|
1530
1529
|
if (options.requireOwnedToken) {
|
|
1531
1530
|
const owned = expectedOwnerToken ? inventory.find((entry) => entry.ownerToken === expectedOwnerToken) : undefined;
|
|
1532
1531
|
if (owned) {
|
|
@@ -1544,25 +1543,8 @@ function chooseTab(
|
|
|
1544
1543
|
};
|
|
1545
1544
|
}
|
|
1546
1545
|
const savedUrl = savedTab?.url ? comparableUrl(savedTab.url) : undefined;
|
|
1547
|
-
const savedTitle = savedTab?.title;
|
|
1548
|
-
const savedUrlTitle = savedUrl && savedTitle
|
|
1549
|
-
? inventory.find((entry) => comparableUrl(entry.url) === savedUrl && entry.title === savedTitle)
|
|
1550
|
-
: undefined;
|
|
1551
|
-
if (savedUrlTitle) {
|
|
1552
|
-
return {
|
|
1553
|
-
entry: savedUrlTitle,
|
|
1554
|
-
matchedBy: 'saved_url_title',
|
|
1555
|
-
sessionResume: {
|
|
1556
|
-
sessionId: target.sessionId,
|
|
1557
|
-
status: 'matched',
|
|
1558
|
-
reason: 'Saved tab URL and title matched an attached browser tab.',
|
|
1559
|
-
savedTab,
|
|
1560
|
-
},
|
|
1561
|
-
};
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
1546
|
const savedUrlOnly = savedUrl
|
|
1565
|
-
? inventory.
|
|
1547
|
+
? unique(inventory.filter((entry) => comparableUrl(entry.url) === savedUrl), 'saved_url')
|
|
1566
1548
|
: undefined;
|
|
1567
1549
|
if (savedUrlOnly) {
|
|
1568
1550
|
return {
|
|
@@ -1571,13 +1553,13 @@ function chooseTab(
|
|
|
1571
1553
|
sessionResume: {
|
|
1572
1554
|
sessionId: target.sessionId,
|
|
1573
1555
|
status: savedTab ? 'matched' : 'no_saved_tab',
|
|
1574
|
-
reason: savedTab ? 'Saved tab URL
|
|
1556
|
+
reason: savedTab ? 'Saved tab URL was unique in this browser context; page title was not used as identity.' : 'No saved tab metadata was available.',
|
|
1575
1557
|
savedTab,
|
|
1576
1558
|
},
|
|
1577
1559
|
};
|
|
1578
1560
|
}
|
|
1579
1561
|
|
|
1580
|
-
const exactUrl = inventory.
|
|
1562
|
+
const exactUrl = unique(inventory.filter((entry) => comparableUrl(entry.url) === desiredUrl), 'exact_url');
|
|
1581
1563
|
if (exactUrl) {
|
|
1582
1564
|
return {
|
|
1583
1565
|
entry: exactUrl,
|
|
@@ -1597,7 +1579,7 @@ function chooseTab(
|
|
|
1597
1579
|
};
|
|
1598
1580
|
}
|
|
1599
1581
|
|
|
1600
|
-
const blank = options.allowBlank === false ? undefined : inventory.
|
|
1582
|
+
const blank = options.allowBlank === false ? undefined : unique(inventory.filter((entry) => isBlankPage(entry.url)), 'blank');
|
|
1601
1583
|
if (blank) {
|
|
1602
1584
|
return {
|
|
1603
1585
|
entry: blank,
|
|
@@ -2627,15 +2609,13 @@ async function openNativeAttachedContext(
|
|
|
2627
2609
|
args: Record<string, unknown>,
|
|
2628
2610
|
): Promise<{ handle?: BrowserOpenHandle; attempts: MacOsBrowserAttachAttempt[] }> {
|
|
2629
2611
|
if (config.nativeAttachMode === 'disabled') return { attempts: [] };
|
|
2630
|
-
if (!target.existingSession && !stringValue(args.session_id)) {
|
|
2612
|
+
if (!target.existingSession && !stringValue(args.session_id) && args.__forge_computer_native_only !== true) {
|
|
2631
2613
|
await inspectNativeOwnedSessions({ repoRoot, savedSessions: listSavedSessions(repoRoot), timeoutMs: config.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS, pruneDead: true });
|
|
2632
2614
|
}
|
|
2633
2615
|
const timeout = positiveNumber(args.timeout_ms, config.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
2634
2616
|
const savedTab = target.existingSession?.browser?.tab;
|
|
2635
2617
|
const savedOwnership = savedTab?.ownership;
|
|
2636
2618
|
let effectiveOwnership = savedOwnership;
|
|
2637
|
-
let recoveredTabTitle = '';
|
|
2638
|
-
let recoveredTabUrl: string | undefined;
|
|
2639
2619
|
const savedProduct = target.existingSession?.browser?.provider === 'macos-apple-events'
|
|
2640
2620
|
&& savedTab
|
|
2641
2621
|
&& (savedOwnership === 'plugin_owned' || savedOwnership === 'user_owned')
|
|
@@ -2685,63 +2665,18 @@ async function openNativeAttachedContext(
|
|
|
2685
2665
|
savedTab,
|
|
2686
2666
|
};
|
|
2687
2667
|
} catch (error) {
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2668
|
+
// URL/title or a formerly active window cannot identify a lost native tab.
|
|
2669
|
+
// Never silently adopt another user's tab or create a replacement for a saved session.
|
|
2670
|
+
if (savedOwnership === 'user_owned' && error instanceof AssistantPluginError && error.code === 'PLUGIN_BROWSER_NATIVE_TAB_IDENTITY_UNPROVEN') {
|
|
2691
2671
|
throw error;
|
|
2692
2672
|
}
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
},
|
|
2701
|
-
);
|
|
2702
|
-
}
|
|
2703
|
-
let inventory: Awaited<ReturnType<typeof listMacOsBrowserTabs>> | undefined;
|
|
2704
|
-
let selection: ReturnType<typeof selectReusableNativeTab> | undefined;
|
|
2705
|
-
let recoveryFailure: string | undefined;
|
|
2706
|
-
try {
|
|
2707
|
-
inventory = await listMacOsBrowserTabs(savedProduct, timeout);
|
|
2708
|
-
selection = selectReusableNativeTab(
|
|
2709
|
-
[savedTab.url, target.existingSession?.url ?? '', target.url],
|
|
2710
|
-
inventory,
|
|
2711
|
-
savedTab.windowId,
|
|
2712
|
-
);
|
|
2713
|
-
if (selection.candidate) {
|
|
2714
|
-
const recovered = await reattachMacOsBrowserOwnedPage(savedProduct, { windowId: selection.candidate.windowId, tabId: selection.candidate.tabId }, timeout);
|
|
2715
|
-
page = recovered.page as unknown as PageLike;
|
|
2716
|
-
discovered = { attachment: recovered.attachment, attempts: recovered.attachment.attempts };
|
|
2717
|
-
matchedBy = 'recovered_tab';
|
|
2718
|
-
effectiveOwnership = 'user_owned';
|
|
2719
|
-
recoveredTabTitle = selection.candidate.title;
|
|
2720
|
-
recoveredTabUrl = selection.candidate.url;
|
|
2721
|
-
sessionResume = {
|
|
2722
|
-
sessionId: target.sessionId,
|
|
2723
|
-
status: 'matched',
|
|
2724
|
-
reason: selection.match === 'exact_url'
|
|
2725
|
-
? 'Saved plugin-owned tab disappeared; rebound safely to the unique currently open tab with the same URL and preserved it as user-owned.'
|
|
2726
|
-
: 'Saved plugin-owned tab disappeared; rebound safely to the unique currently open tab for the same stable app entity and preserved its current page as user-owned.',
|
|
2727
|
-
savedTab,
|
|
2728
|
-
};
|
|
2729
|
-
}
|
|
2730
|
-
} catch (recoveryError) {
|
|
2731
|
-
recoveryFailure = recoveryError instanceof Error ? recoveryError.message : String(recoveryError);
|
|
2732
|
-
}
|
|
2733
|
-
if (!page) {
|
|
2734
|
-
sessionResume = {
|
|
2735
|
-
sessionId: target.sessionId,
|
|
2736
|
-
status: 'stale_tab',
|
|
2737
|
-
reason: recoveryFailure
|
|
2738
|
-
? `Saved plugin-owned macOS browser tab disappeared and current-tab recovery failed (${recoveryFailure}); creating one replacement plugin-owned tab.`
|
|
2739
|
-
: selection?.match === 'ambiguous'
|
|
2740
|
-
? 'Saved plugin-owned macOS browser tab disappeared and multiple reusable current tabs matched; refusing to guess between user tabs and creating one replacement plugin-owned tab.'
|
|
2741
|
-
: 'Saved plugin-owned macOS browser tab disappeared and no uniquely reusable current tab matched; creating one replacement plugin-owned tab.',
|
|
2742
|
-
savedTab,
|
|
2743
|
-
};
|
|
2744
|
-
}
|
|
2673
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_SESSION_STATE_LOST',
|
|
2674
|
+
'Saved native browser tab could not be reattached by its exact identity; refusing URL-based recovery or replacement.',
|
|
2675
|
+
{ retryable: false, details: {
|
|
2676
|
+
sessionId: target.sessionId, browserProduct: savedProduct, ownership: savedOwnership,
|
|
2677
|
+
windowId: savedTab.windowId, tabId: savedTab.tabId,
|
|
2678
|
+
causeCode: error instanceof AssistantPluginError ? error.code : 'NATIVE_REATTACH_FAILED',
|
|
2679
|
+
} });
|
|
2745
2680
|
}
|
|
2746
2681
|
} else {
|
|
2747
2682
|
sessionResume = savedTab
|
|
@@ -2749,11 +2684,105 @@ async function openNativeAttachedContext(
|
|
|
2749
2684
|
: { sessionId: target.sessionId, status: 'no_saved_tab', reason: 'Created one plugin-owned tab while preserving the user active tab.' };
|
|
2750
2685
|
}
|
|
2751
2686
|
|
|
2687
|
+
// Session-bound reads and mutations must never turn a dead tab into a new
|
|
2688
|
+
// tab or silently select another profile. The explicit open-page route is
|
|
2689
|
+
// separate and retains its documented create/recover semantics.
|
|
2690
|
+
if (args.__forge_require_existing_resource === true && target.existingSession && !page) {
|
|
2691
|
+
throw new AssistantPluginError(
|
|
2692
|
+
'PLUGIN_BROWSER_SESSION_STATE_LOST',
|
|
2693
|
+
'The saved native browser tab is unavailable and no exact existing tab can be verified; the session was not moved to another profile or recreated.',
|
|
2694
|
+
{ retryable: false, details: {
|
|
2695
|
+
sessionId: target.sessionId, browserProduct: savedProduct,
|
|
2696
|
+
savedWindowId: savedTab?.windowId, savedTabId: savedTab?.tabId,
|
|
2697
|
+
} },
|
|
2698
|
+
);
|
|
2699
|
+
}
|
|
2752
2700
|
let createdThisCallRef: MacOsBrowserTabRef | undefined;
|
|
2701
|
+
let preferredNativeWindowId: string | undefined;
|
|
2753
2702
|
if (!discovered?.attachment) {
|
|
2754
2703
|
const candidates = savedProduct ? [savedProduct] : (config.nativeBrowserCandidates ?? [...DEFAULT_USER_NATIVE_BROWSER_CANDIDATES]);
|
|
2755
|
-
|
|
2704
|
+
let preferredCandidates = candidates;
|
|
2705
|
+
if (candidates.length > 1 && !target.existingSession && (args.__forge_computer_native_only === true || !stringValue(args.session_id))) {
|
|
2706
|
+
// Choosing the frontmost browser can select a different, signed-out
|
|
2707
|
+
// profile even while the user's matching Chrome tab is in the background.
|
|
2708
|
+
// Prefer exact URL evidence across *all* configured native products.
|
|
2709
|
+
const inventories = await Promise.all(candidates.map(async (product) => {
|
|
2710
|
+
try { await assertMacOsBrowserApplicationRunning(product, timeout); }
|
|
2711
|
+
catch (error) {
|
|
2712
|
+
if (error instanceof AssistantPluginError && [
|
|
2713
|
+
'PLUGIN_BROWSER_NATIVE_APP_NOT_RUNNING', 'PLUGIN_BROWSER_NATIVE_APP_NOT_INSTALLED',
|
|
2714
|
+
'PLUGIN_BROWSER_NATIVE_APP_UNSUPPORTED_PLATFORM',
|
|
2715
|
+
].includes(error.code)) return undefined;
|
|
2716
|
+
throw error;
|
|
2717
|
+
}
|
|
2718
|
+
try {
|
|
2719
|
+
const inventory = await listMacOsBrowserTabs(product, timeout, { correctFrontmost: false });
|
|
2720
|
+
if (inventory.truncated) throw new AssistantPluginError('PLUGIN_COMPUTER_BROWSER_INVENTORY_INCOMPLETE',
|
|
2721
|
+
'Native browser inventory truncated; cannot prove the existing target tab is absent.',
|
|
2722
|
+
{ retryable: true, details: { browserProduct: product } });
|
|
2723
|
+
return { product, inventory };
|
|
2724
|
+
} catch (error) {
|
|
2725
|
+
// Unreadable background tabs are not absent tabs: no replacement
|
|
2726
|
+
// browser or new tab may be created from an incomplete inventory.
|
|
2727
|
+
if (args.__forge_computer_native_only === true) {
|
|
2728
|
+
throw new AssistantPluginError('PLUGIN_COMPUTER_BROWSER_INVENTORY_UNAVAILABLE',
|
|
2729
|
+
'Cannot enumerate existing native browser tabs; refusing to create a duplicate.',
|
|
2730
|
+
{ retryable: true, details: { browserProduct: product, cause: error instanceof Error ? error.message : String(error) } });
|
|
2731
|
+
}
|
|
2732
|
+
return undefined;
|
|
2733
|
+
}
|
|
2734
|
+
}));
|
|
2735
|
+
const observed = inventories.filter((item): item is NonNullable<typeof item> => Boolean(item));
|
|
2736
|
+
const exact = observed.flatMap(({ product, inventory }) => inventory.tabs
|
|
2737
|
+
.filter((entry) => comparableUrl(entry.url) === comparableUrl(target.url))
|
|
2738
|
+
.map((entry) => ({ product, entry })));
|
|
2739
|
+
if (exact.length > 1) {
|
|
2740
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_REUSABLE_TAB_AMBIGUOUS',
|
|
2741
|
+
'Multiple browser products have existing tabs for the exact URL; refusing to guess and create another tab.',
|
|
2742
|
+
{ retryable: false, details: { targetUrl: target.url, candidateCount: exact.length } });
|
|
2743
|
+
}
|
|
2744
|
+
if (exact.length === 1) {
|
|
2745
|
+
preferredCandidates = [exact[0]!.product];
|
|
2746
|
+
} else {
|
|
2747
|
+
// Same-origin tabs only identify a likely browser *profile*, not an
|
|
2748
|
+
// exact resource. A new tab may be created there, but never overwrite
|
|
2749
|
+
// an unrelated user tab or invent proof of authentication.
|
|
2750
|
+
const origin = (() => { try { return new URL(target.url).origin; } catch { return ''; } })();
|
|
2751
|
+
const originProducts = observed.filter(({ inventory }) => origin && inventory.tabs.some((entry) => {
|
|
2752
|
+
try { return new URL(entry.url).origin === origin; } catch { return false; }
|
|
2753
|
+
})).map(({ product }) => product);
|
|
2754
|
+
if (originProducts.length === 1) {
|
|
2755
|
+
preferredCandidates = [originProducts[0]!];
|
|
2756
|
+
const matchingWindows = new Set(observed
|
|
2757
|
+
.filter(({ product }) => product === originProducts[0])
|
|
2758
|
+
.flatMap(({ inventory }) => inventory.tabs.filter((entry) => {
|
|
2759
|
+
try { return new URL(entry.url).origin === origin; } catch { return false; }
|
|
2760
|
+
}).map((entry) => entry.windowId)));
|
|
2761
|
+
if (matchingWindows.size === 1) preferredNativeWindowId = [...matchingWindows][0];
|
|
2762
|
+
} else if (observed.length === 1) {
|
|
2763
|
+
// Exactly one available browser product is observed. Product discovery
|
|
2764
|
+
// must not silently fall through multiple provider backends.
|
|
2765
|
+
preferredCandidates = [observed[0]!.product];
|
|
2766
|
+
} else if (observed.length === 0) {
|
|
2767
|
+
throw new AssistantPluginError(args.__forge_computer_native_only === true
|
|
2768
|
+
? 'PLUGIN_COMPUTER_NATIVE_BROWSER_UNAVAILABLE' : 'PLUGIN_BROWSER_ATTACH_UNAVAILABLE',
|
|
2769
|
+
'No usable existing native browser was observed; refusing to launch or guess a provider.',
|
|
2770
|
+
{ retryable: false });
|
|
2771
|
+
} else {
|
|
2772
|
+
if (args.__forge_require_existing_resource === true) {
|
|
2773
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_EXISTING_TAB_NOT_FOUND',
|
|
2774
|
+
'No uniquely reusable tab matches the requested existing resource; refusing new tab creation.',
|
|
2775
|
+
{ retryable: false });
|
|
2776
|
+
}
|
|
2777
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_PROVIDER_CARDINALITY_INVALID',
|
|
2778
|
+
'No uniquely identified existing browser product; refusing implicit cross-provider selection.',
|
|
2779
|
+
{ retryable: false, details: { candidateCount: observed.length } });
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
discovered = await discoverMacOsBrowserAttachment(preferredCandidates, Math.min(timeout, MAX_CDP_DISCOVERY_TIMEOUT_MS));
|
|
2756
2784
|
const zeroWindowAttempt = args.__forge_require_existing_resource !== true
|
|
2785
|
+
&& args.__forge_computer_native_only !== true
|
|
2757
2786
|
? discovered.attempts.find((attempt) => attempt.status === 'unavailable' && attempt.error?.includes('FORGE_NO_BROWSER_WINDOW'))
|
|
2758
2787
|
: undefined;
|
|
2759
2788
|
if (!discovered.attachment && zeroWindowAttempt) {
|
|
@@ -2870,12 +2899,17 @@ async function openNativeAttachedContext(
|
|
|
2870
2899
|
}
|
|
2871
2900
|
};
|
|
2872
2901
|
try {
|
|
2873
|
-
if (!page && !savedTab && !stringValue(args.session_id)) {
|
|
2902
|
+
if (!page && !savedTab && !target.existingSession && (args.__forge_computer_native_only === true || !stringValue(args.session_id))) {
|
|
2874
2903
|
let inventory: Awaited<ReturnType<typeof listMacOsBrowserTabs>> | undefined;
|
|
2875
2904
|
try {
|
|
2876
2905
|
inventory = await listMacOsBrowserTabs(activeAttachment.metadata.product, timeout);
|
|
2877
2906
|
} catch (error) {
|
|
2878
2907
|
if (!nativeTabInventoryUnsupported(error)) throw error;
|
|
2908
|
+
if (args.__forge_computer_native_only === true) {
|
|
2909
|
+
throw new AssistantPluginError('PLUGIN_COMPUTER_BROWSER_INVENTORY_UNAVAILABLE',
|
|
2910
|
+
'Native tab inventory unavailable; Computer cannot prove an existing tab is absent.',
|
|
2911
|
+
{ retryable: true });
|
|
2912
|
+
}
|
|
2879
2913
|
}
|
|
2880
2914
|
if (inventory) {
|
|
2881
2915
|
const ownedSelection = selectLiveForgeOwnedNativeTab(repoRoot, target.url, activeAttachment.metadata.product, inventory);
|
|
@@ -2929,8 +2963,6 @@ async function openNativeAttachedContext(
|
|
|
2929
2963
|
activeAttachment = rebound.attachment;
|
|
2930
2964
|
matchedBy = 'exact_url';
|
|
2931
2965
|
effectiveOwnership = 'user_owned';
|
|
2932
|
-
recoveredTabTitle = exactSelection.candidate.title;
|
|
2933
|
-
recoveredTabUrl = exactSelection.candidate.url;
|
|
2934
2966
|
sessionResume = {
|
|
2935
2967
|
sessionId: target.sessionId,
|
|
2936
2968
|
status: 'matched',
|
|
@@ -2941,7 +2973,23 @@ async function openNativeAttachedContext(
|
|
|
2941
2973
|
}
|
|
2942
2974
|
}
|
|
2943
2975
|
if (!page) {
|
|
2944
|
-
|
|
2976
|
+
// Explicit existing-resource requests (including read-only live
|
|
2977
|
+
// acceptance) must never create a new tab if no exact tab was found.
|
|
2978
|
+
if (!target.existingSession && args.__forge_require_existing_resource === true) {
|
|
2979
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_EXISTING_TAB_NOT_FOUND',
|
|
2980
|
+
'No uniquely reusable native tab matches the requested URL; refusing to create a new tab.',
|
|
2981
|
+
{ retryable: false });
|
|
2982
|
+
}
|
|
2983
|
+
if (preferredNativeWindowId) {
|
|
2984
|
+
const created = await createMacOsBrowserOwnedPageForProduct(
|
|
2985
|
+
activeAttachment.metadata.product, target.url, activeAttachment.attempts, preferredNativeWindowId, timeout,
|
|
2986
|
+
);
|
|
2987
|
+
page = created.page as unknown as PageLike;
|
|
2988
|
+
activeAttachment = created.attachment;
|
|
2989
|
+
discovered = { attachment: created.attachment, attempts: created.attachment.attempts };
|
|
2990
|
+
} else {
|
|
2991
|
+
page = await createMacOsBrowserOwnedPage(activeAttachment, target.url, timeout) as unknown as PageLike;
|
|
2992
|
+
}
|
|
2945
2993
|
createdThisCallRef = (page as unknown as { tabRef?: () => MacOsBrowserTabRef | undefined }).tabRef?.();
|
|
2946
2994
|
const liveIdentity = await settleNativeCreatedPageIdentity(
|
|
2947
2995
|
page as unknown as {
|
|
@@ -2961,21 +3009,15 @@ async function openNativeAttachedContext(
|
|
|
2961
3009
|
if (comparableUrl(page.url()) !== comparableUrl(target.url) && !explicitUserOwnedUrlMatch) {
|
|
2962
3010
|
const currentRef = (page as unknown as { tabRef?: () => MacOsBrowserTabRef | undefined }).tabRef?.();
|
|
2963
3011
|
if (effectiveOwnership === 'user_owned') {
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
'Explicitly adopted user-owned tab no longer matches the saved session URL; refusing to navigate, replace, or close the user tab.',
|
|
2970
|
-
{ retryable: true, details: { sessionId: target.sessionId, expectedUrl: target.url, actualUrl: page.url(), windowId: savedTab?.windowId, tabId: savedTab?.tabId } },
|
|
2971
|
-
);
|
|
2972
|
-
}
|
|
3012
|
+
throw new AssistantPluginError(
|
|
3013
|
+
'PLUGIN_BROWSER_ADOPTED_TAB_URL_MISMATCH',
|
|
3014
|
+
'Explicitly adopted user-owned tab no longer matches the saved session URL; refusing to navigate, replace, or close the user tab.',
|
|
3015
|
+
{ retryable: true, details: { sessionId: target.sessionId, expectedUrl: target.url, actualUrl: page.url(), windowId: savedTab?.windowId, tabId: savedTab?.tabId } },
|
|
3016
|
+
);
|
|
2973
3017
|
}
|
|
2974
3018
|
const sessionTargetUnchanged = Boolean(target.existingSession?.url) && comparableUrl(target.url) === comparableUrl(target.existingSession?.url ?? '');
|
|
2975
3019
|
const preserveOwnedSameOriginDrift = matchedBy === 'owned_token' && sessionTargetUnchanged && sameOrigin(page.url(), target.url);
|
|
2976
|
-
if (
|
|
2977
|
-
// Safe recovery adopts an already-open tab. Its current page is authoritative and must not be navigated.
|
|
2978
|
-
} else if (preserveOwnedSameOriginDrift) {
|
|
3020
|
+
if (preserveOwnedSameOriginDrift) {
|
|
2979
3021
|
sessionResume = { sessionId: target.sessionId, status: 'matched', reason: 'Plugin-owned tab navigated within the same origin; preserved the live tab and refreshed its session URL.', savedTab };
|
|
2980
3022
|
} else {
|
|
2981
3023
|
page = await navigateNativePageWithReplacement(page);
|
|
@@ -2993,13 +3035,13 @@ async function openNativeAttachedContext(
|
|
|
2993
3035
|
if (matchedBy !== 'owned_token' && page.waitForLoadState && !explicitInternalUserSession) {
|
|
2994
3036
|
await page.waitForLoadState(waitUntil(args.wait_until), { timeout });
|
|
2995
3037
|
}
|
|
2996
|
-
const observedPageUrl =
|
|
3038
|
+
const observedPageUrl = page.url();
|
|
2997
3039
|
const pageUrl = explicitInternalUserSession
|
|
2998
3040
|
? normalizedExplicitNativeAdoptionUrl(observedPageUrl)
|
|
2999
3041
|
: normalizedUrl(observedPageUrl);
|
|
3000
3042
|
const nativeRef = (page as unknown as { tabRef?: () => MacOsBrowserTabRef | undefined }).tabRef?.();
|
|
3001
3043
|
if (!nativeRef) throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_OWNERSHIP_MISSING', 'Native browser did not return a stable plugin-owned tab reference.', { retryable: true });
|
|
3002
|
-
const title = matchedBy === 'owned_token' ? (target.existingSession?.title ?? '') :
|
|
3044
|
+
const title = matchedBy === 'owned_token' ? (target.existingSession?.title ?? '') : '';
|
|
3003
3045
|
const inventory: BrowserTabInventoryEntry[] = [{ index: 0, key: tabKey(pageUrl, title), url: pageUrl, title }];
|
|
3004
3046
|
const metadata = activeAttachment.metadata;
|
|
3005
3047
|
// The live exact-tab URL is authoritative after a safe native rebind. Keep the
|
|
@@ -3042,80 +3084,66 @@ async function openBrowser(
|
|
|
3042
3084
|
target: BrowserActionTarget,
|
|
3043
3085
|
args: Record<string, unknown>,
|
|
3044
3086
|
): Promise<BrowserOpenHandle> {
|
|
3045
|
-
const
|
|
3087
|
+
const computerNativeOnly = args.__forge_computer_native_only === true;
|
|
3088
|
+
const requestedMode = computerNativeOnly ? 'attach_preferred' : (config.browserMode ?? 'attach_preferred');
|
|
3089
|
+
if (computerNativeOnly) {
|
|
3090
|
+
// Computer operates existing desktop/browser resources. Playwright CDP and
|
|
3091
|
+
// managed profiles remain exclusive to explicitly selected Browser scripts.
|
|
3092
|
+
if (target.existingSession?.browser?.provider
|
|
3093
|
+
&& target.existingSession.browser.provider !== 'macos-apple-events') {
|
|
3094
|
+
throw new AssistantPluginError('PLUGIN_COMPUTER_BROWSER_PROVIDER_MISMATCH',
|
|
3095
|
+
'This saved session belongs to an independent scripted Browser provider; Computer cannot silently adopt or replace it.',
|
|
3096
|
+
{ retryable: false, details: { sessionId: target.sessionId, provider: target.existingSession.browser.provider } });
|
|
3097
|
+
}
|
|
3098
|
+
const native = await openNativeAttachedContext(repoRoot, {
|
|
3099
|
+
...config, browserMode: 'attach_preferred', nativeAttachMode: 'auto', cdpAttachFallback: 'fail_closed',
|
|
3100
|
+
}, target, args);
|
|
3101
|
+
if (native.handle) return native.handle;
|
|
3102
|
+
throw new AssistantPluginError('PLUGIN_COMPUTER_NATIVE_BROWSER_UNAVAILABLE',
|
|
3103
|
+
'No attachable existing Chrome or Vivaldi browser is available. Computer will not start a Playwright profile.',
|
|
3104
|
+
{ retryable: true, details: { nativeAttempts: native.attempts } });
|
|
3105
|
+
}
|
|
3046
3106
|
if (requestedMode === 'attach_preferred') {
|
|
3047
|
-
const
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
if (attached.handle) return attached.handle;
|
|
3053
|
-
if (requireExistingResource && existingProvider === 'playwright-cdp') {
|
|
3054
|
-
throw new AssistantPluginError(
|
|
3055
|
-
'PLUGIN_BROWSER_SESSION_STATE_LOST',
|
|
3056
|
-
'Saved CDP browser page is no longer attachable; refusing to fall back to another browser or create a replacement page.',
|
|
3057
|
-
{ retryable: false, details: { sessionId: target.sessionId, provider: existingProvider } },
|
|
3058
|
-
);
|
|
3107
|
+
const savedProvider = target.existingSession?.browser?.provider;
|
|
3108
|
+
if (savedProvider === 'playwright-persistent-context') {
|
|
3109
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_SESSION_STATE_LOST',
|
|
3110
|
+
'A managed session cannot be silently adopted by an attach provider. Select managed_persistent explicitly.',
|
|
3111
|
+
{ retryable: false, details: { sessionId: target.sessionId, provider: savedProvider } });
|
|
3059
3112
|
}
|
|
3060
|
-
|
|
3113
|
+
const endpoints = cdpEndpoints(config);
|
|
3114
|
+
const route = savedProvider === 'macos-apple-events' ? 'native'
|
|
3115
|
+
: savedProvider === 'playwright-cdp' || endpoints.length > 0 ? 'cdp' : 'native';
|
|
3116
|
+
if (route === 'cdp') {
|
|
3117
|
+
if (endpoints.length !== 1) {
|
|
3118
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_PROVIDER_CARDINALITY_INVALID',
|
|
3119
|
+
'CDP attach requires exactly one configured endpoint.', { retryable: false });
|
|
3120
|
+
}
|
|
3061
3121
|
if (!runtimeHooks.moduleAvailable('playwright', repoRoot)) {
|
|
3062
|
-
throw new AssistantPluginError('PLUGIN_BROWSER_DEPENDENCY_UNAVAILABLE',
|
|
3122
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_DEPENDENCY_UNAVAILABLE',
|
|
3123
|
+
'Selected CDP provider requires Playwright; no other provider will be attempted.', { retryable: false });
|
|
3063
3124
|
}
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3125
|
+
const attached = await openAttachedContext(runtimeHooks.loadPlaywright(repoRoot), repoRoot, config, target, args);
|
|
3126
|
+
if (attached.handle) return attached.handle;
|
|
3127
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_ATTACH_UNAVAILABLE',
|
|
3128
|
+
'Configured CDP endpoint unavailable. Native and managed providers will not be attempted.',
|
|
3129
|
+
{ retryable: true, details: { provider: 'playwright-cdp', attempts: attached.attempts } });
|
|
3130
|
+
}
|
|
3131
|
+
if (config.nativeAttachMode !== 'auto') {
|
|
3132
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_ATTACH_UNAVAILABLE',
|
|
3133
|
+
'Native attach is not configured. Choose an explicit Browser mode or one CDP endpoint.',
|
|
3134
|
+
{ retryable: false });
|
|
3071
3135
|
}
|
|
3072
|
-
|
|
3073
3136
|
const native = await openNativeAttachedContext(repoRoot, config, target, args);
|
|
3074
3137
|
if (native.handle) return native.handle;
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
'Saved browser resource is no longer attachable; refusing to create a replacement tab/page for this existing-session action.',
|
|
3079
|
-
{ retryable: false, details: { sessionId: target.sessionId, provider: existingProvider } },
|
|
3080
|
-
);
|
|
3081
|
-
}
|
|
3082
|
-
|
|
3083
|
-
const fallbackPolicy = config.cdpAttachFallback ?? 'fail_closed';
|
|
3084
|
-
const cdpReason = attached.attempts.map((attempt) => `${attempt.endpoint}: ${attempt.error ?? 'unavailable'}`).join('; ')
|
|
3085
|
-
|| 'No CDP endpoint candidates were configured.';
|
|
3086
|
-
const nativeReason = native.attempts.map((attempt) => `${attempt.appName}: ${attempt.error ?? attempt.status}`).join('; ')
|
|
3087
|
-
|| (config.nativeAttachMode === 'disabled' ? 'Native browser attach is disabled.' : 'No scriptable macOS browser was available.');
|
|
3088
|
-
const reason = `CDP: ${cdpReason} Native: ${nativeReason}`;
|
|
3089
|
-
if (fallbackPolicy === 'fail_closed') {
|
|
3090
|
-
throw new AssistantPluginError('PLUGIN_BROWSER_ATTACH_UNAVAILABLE', `No configured browser attach provider was available. Fallback policy fail_closed prevented managed launch. ${reason}`, {
|
|
3091
|
-
retryable: true,
|
|
3092
|
-
details: {
|
|
3093
|
-
browserMode: requestedMode,
|
|
3094
|
-
fallbackPolicy,
|
|
3095
|
-
attempts: attached.attempts,
|
|
3096
|
-
nativeAttempts: native.attempts,
|
|
3097
|
-
},
|
|
3098
|
-
});
|
|
3099
|
-
}
|
|
3100
|
-
if (!runtimeHooks.moduleAvailable('playwright', repoRoot)) {
|
|
3101
|
-
throw new AssistantPluginError('PLUGIN_BROWSER_DEPENDENCY_UNAVAILABLE', `Managed browser fallback requires Playwright, but native/CDP attach was unavailable. ${reason}`, { retryable: false });
|
|
3102
|
-
}
|
|
3103
|
-
return openManagedContext(runtimeHooks.loadPlaywright(repoRoot), repoRoot, config, target, args, 'managed_persistent', requestedMode, {
|
|
3104
|
-
policy: fallbackPolicy,
|
|
3105
|
-
from: 'attach_preferred',
|
|
3106
|
-
to: 'managed_persistent',
|
|
3107
|
-
reason,
|
|
3108
|
-
attempts: attached.attempts,
|
|
3109
|
-
nativeAttempts: native.attempts,
|
|
3110
|
-
});
|
|
3138
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_ATTACH_UNAVAILABLE',
|
|
3139
|
+
'Configured native browser target unavailable. No alternate provider will be attempted.',
|
|
3140
|
+
{ retryable: true, details: { provider: 'macos-apple-events', attempts: native.attempts } });
|
|
3111
3141
|
}
|
|
3112
|
-
|
|
3113
3142
|
if (!runtimeHooks.moduleAvailable('playwright', repoRoot)) {
|
|
3114
3143
|
throw new AssistantPluginError('PLUGIN_BROWSER_DEPENDENCY_UNAVAILABLE', 'Managed browser mode requires Playwright.', { retryable: false });
|
|
3115
3144
|
}
|
|
3116
3145
|
return openManagedContext(runtimeHooks.loadPlaywright(repoRoot), repoRoot, config, target, args, requestedMode, requestedMode);
|
|
3117
3146
|
}
|
|
3118
|
-
|
|
3119
3147
|
async function withPage<T>(
|
|
3120
3148
|
actionId: string,
|
|
3121
3149
|
repoRoot: string,
|
|
@@ -3875,9 +3903,12 @@ function health(config: BrowserPluginConfig, repoRoot?: string): AssistantPlugin
|
|
|
3875
3903
|
};
|
|
3876
3904
|
}
|
|
3877
3905
|
|
|
3878
|
-
export function buildBrowserPluginManifest(
|
|
3906
|
+
export function buildBrowserPluginManifest(
|
|
3907
|
+
previousRevision = 0, previousUpdatedAt?: string, repoRoot?: string,
|
|
3908
|
+
context?: AssistantPluginBuildContext, computerNativeOnly = false,
|
|
3909
|
+
): AssistantPluginManifest {
|
|
3879
3910
|
const root = repoRoot ?? process.cwd();
|
|
3880
|
-
const config = loadConfig(root, context);
|
|
3911
|
+
const config = loadConfig(root, context, computerNativeOnly);
|
|
3881
3912
|
const state = health(config, root);
|
|
3882
3913
|
return {
|
|
3883
3914
|
schemaVersion: 1,
|
|
@@ -3899,9 +3930,7 @@ export function buildBrowserPluginManifest(previousRevision = 0, previousUpdated
|
|
|
3899
3930
|
? 'Browser plugin is disabled.'
|
|
3900
3931
|
: state.ready
|
|
3901
3932
|
? `Browser plugin is ready via ${config.browserMode === 'attach_preferred'
|
|
3902
|
-
?
|
|
3903
|
-
? 'CDP, macOS native attach, and explicit managed fallback.'
|
|
3904
|
-
: 'CDP or macOS native attach; managed fallback is disabled.'
|
|
3933
|
+
? 'the one configured CDP or native browser target (no provider fallback).'
|
|
3905
3934
|
: 'Playwright persistent context.'}`
|
|
3906
3935
|
: state.state === 'degraded'
|
|
3907
3936
|
? state.warnings[0] ?? 'Browser plugin requires a successful live native attach probe.'
|
|
@@ -3915,13 +3944,6 @@ export function buildBrowserPluginManifest(previousRevision = 0, previousUpdated
|
|
|
3915
3944
|
};
|
|
3916
3945
|
}
|
|
3917
3946
|
|
|
3918
|
-
const NODE_BRIDGE_PRE_ACTION_FAILURE_CODES = new Set([
|
|
3919
|
-
'PLUGIN_BROWSER_NODE_UNAVAILABLE',
|
|
3920
|
-
'PLUGIN_BROWSER_NODE_HOST_UNAVAILABLE',
|
|
3921
|
-
'PLUGIN_BROWSER_NODE_REQUEST_TOO_LARGE',
|
|
3922
|
-
'PLUGIN_BROWSER_NODE_START_FAILED',
|
|
3923
|
-
]);
|
|
3924
|
-
|
|
3925
3947
|
function browserRuntimeKey(input: AssistantPluginActionExecutionInput): string {
|
|
3926
3948
|
return `${input.controllerHome}:${input.repoId}`;
|
|
3927
3949
|
}
|
|
@@ -3930,9 +3952,15 @@ async function executeBrowserPluginActionInternal(
|
|
|
3930
3952
|
input: AssistantPluginActionExecutionInput,
|
|
3931
3953
|
runtimeAlreadyRouted = false,
|
|
3932
3954
|
): Promise<Record<string, unknown>> {
|
|
3933
|
-
const persisted = loadConfig(input.repoRoot, input);
|
|
3955
|
+
const persisted = loadConfig(input.repoRoot, input, input.pluginId === 'computer');
|
|
3934
3956
|
const actionSessionId = input.actionId === 'configure' ? undefined : stringValue(input.args.session_id);
|
|
3935
3957
|
const actionSession = actionSessionId ? findBrowserSession(input.repoRoot, actionSessionId) : undefined;
|
|
3958
|
+
if (input.pluginId === 'computer' && actionSession?.browser?.provider
|
|
3959
|
+
&& actionSession.browser.provider !== 'macos-apple-events') {
|
|
3960
|
+
throw new AssistantPluginError('PLUGIN_COMPUTER_BROWSER_PROVIDER_MISMATCH',
|
|
3961
|
+
'Computer cannot manage an independent scripted Browser session; use the Browser plugin for that session.',
|
|
3962
|
+
{ retryable: false, details: { sessionId: actionSessionId, provider: actionSession.browser.provider } });
|
|
3963
|
+
}
|
|
3936
3964
|
const current = input.actionId === 'configure' ? persisted : effectiveBrowserActionConfig(persisted, input.args, actionSession);
|
|
3937
3965
|
if (!current.enabled && input.actionId !== 'configure') {
|
|
3938
3966
|
throw new AssistantPluginError('PLUGIN_DISABLED', 'Browser plugin is disabled.', { retryable: false });
|
|
@@ -3945,7 +3973,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
3945
3973
|
}
|
|
3946
3974
|
if (!runtimeAlreadyRouted) {
|
|
3947
3975
|
const runtimeKey = browserRuntimeKey(input);
|
|
3948
|
-
const nodeBridgeEligible = shouldUseBrowserNodeBridge(
|
|
3976
|
+
const nodeBridgeEligible = input.pluginId !== 'computer' && shouldUseBrowserNodeBridge(
|
|
3949
3977
|
input.actionId,
|
|
3950
3978
|
current.browserMode,
|
|
3951
3979
|
runtimeHooksCustomized,
|
|
@@ -3954,33 +3982,21 @@ async function executeBrowserPluginActionInternal(
|
|
|
3954
3982
|
const result = await executeBrowserRuntimeAction({
|
|
3955
3983
|
runtimeKey,
|
|
3956
3984
|
input,
|
|
3957
|
-
providers: [
|
|
3958
|
-
...(nodeBridgeEligible ? [{
|
|
3985
|
+
providers: nodeBridgeEligible ? [{
|
|
3959
3986
|
providerId: 'browser.node_bridge',
|
|
3960
3987
|
capabilities: ALL_BROWSER_PROVIDER_CAPABILITIES.filter((capability) => capability !== 'browser.persistent_handle'),
|
|
3961
3988
|
foreground: 'none' as const,
|
|
3962
3989
|
verifiesPostconditions: true,
|
|
3963
3990
|
persistentHandle: false,
|
|
3964
|
-
priority: 10,
|
|
3965
3991
|
supportsInput: () => true,
|
|
3966
|
-
execute: async (routedInput: AssistantPluginActionExecutionInput) =>
|
|
3967
|
-
|
|
3968
|
-
return await executeBrowserActionThroughNode(routedInput);
|
|
3969
|
-
} catch (error) {
|
|
3970
|
-
if (error instanceof AssistantPluginError && NODE_BRIDGE_PRE_ACTION_FAILURE_CODES.has(error.code)) {
|
|
3971
|
-
throw new BrowserProviderUnavailableBeforeActionError('browser.node_bridge', error.code);
|
|
3972
|
-
}
|
|
3973
|
-
throw error;
|
|
3974
|
-
}
|
|
3975
|
-
},
|
|
3976
|
-
}] : []),
|
|
3992
|
+
execute: async (routedInput: AssistantPluginActionExecutionInput) => await executeBrowserActionThroughNode(routedInput),
|
|
3993
|
+
}] : [
|
|
3977
3994
|
{
|
|
3978
3995
|
providerId: 'browser.local',
|
|
3979
3996
|
capabilities: ALL_BROWSER_PROVIDER_CAPABILITIES,
|
|
3980
3997
|
foreground: 'none' as const,
|
|
3981
3998
|
verifiesPostconditions: true,
|
|
3982
3999
|
persistentHandle: true,
|
|
3983
|
-
priority: 20,
|
|
3984
4000
|
supportsInput: () => true,
|
|
3985
4001
|
execute: async (routedInput: AssistantPluginActionExecutionInput) => executeBrowserPluginActionInternal(routedInput, true),
|
|
3986
4002
|
},
|
|
@@ -4032,10 +4048,6 @@ async function executeBrowserPluginActionInternal(
|
|
|
4032
4048
|
? positiveNumber(args.default_timeout_ms, DEFAULT_TIMEOUT_MS)
|
|
4033
4049
|
: current.defaultTimeoutMs,
|
|
4034
4050
|
});
|
|
4035
|
-
const configErrors = validateConfig(config);
|
|
4036
|
-
if (configErrors.length > 0) {
|
|
4037
|
-
throw new AssistantPluginError('PLUGIN_CONFIGURATION_INVALID', configErrors[0], { retryable: false });
|
|
4038
|
-
}
|
|
4039
4051
|
await closeManagedContextsForRepo(input.repoRoot);
|
|
4040
4052
|
return { config, health: health(config, input.repoRoot) };
|
|
4041
4053
|
}
|
|
@@ -4118,7 +4130,10 @@ async function executeBrowserPluginActionInternal(
|
|
|
4118
4130
|
throw new AssistantPluginError('PLUGIN_BROWSER_SESSION_CURSOR_INVALID', 'Browser session cursor is invalid or expired.', { retryable: false });
|
|
4119
4131
|
}
|
|
4120
4132
|
}
|
|
4121
|
-
const
|
|
4133
|
+
const visibleSessions = input.pluginId === 'computer'
|
|
4134
|
+
? inventory.sessions.filter(({ session }) => session.browser?.provider === 'macos-apple-events')
|
|
4135
|
+
: inventory.sessions;
|
|
4136
|
+
const page = visibleSessions.slice(offset, offset + requestedLimit);
|
|
4122
4137
|
const nextOffset = offset + page.length;
|
|
4123
4138
|
return {
|
|
4124
4139
|
provider: 'playwright',
|
|
@@ -4131,8 +4146,8 @@ async function executeBrowserPluginActionInternal(
|
|
|
4131
4146
|
deadManagedSessionCount: inventory.deadManagedSessionCount,
|
|
4132
4147
|
deadNativeSessionCount: inventory.deadNativeSessionCount,
|
|
4133
4148
|
limit: requestedLimit,
|
|
4134
|
-
totalCount:
|
|
4135
|
-
...(nextOffset <
|
|
4149
|
+
totalCount: visibleSessions.length,
|
|
4150
|
+
...(nextOffset < visibleSessions.length ? { nextCursor: Buffer.from(JSON.stringify({ offset: nextOffset }), 'utf8').toString('base64url') } : {}),
|
|
4136
4151
|
sessions: page.map(({ session, liveness, evidence, cleanupError }) => ({
|
|
4137
4152
|
sessionId: session.sessionId,
|
|
4138
4153
|
url: session.url,
|
|
@@ -4217,7 +4232,13 @@ async function executeBrowserPluginActionInternal(
|
|
|
4217
4232
|
case 'request_human_handoff': {
|
|
4218
4233
|
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
4219
4234
|
const managedSession = target.existingSession;
|
|
4220
|
-
|
|
4235
|
+
if (input.pluginId === 'computer' && managedSession?.browser?.provider
|
|
4236
|
+
&& managedSession.browser.provider !== 'macos-apple-events') {
|
|
4237
|
+
throw new AssistantPluginError('PLUGIN_COMPUTER_BROWSER_PROVIDER_MISMATCH',
|
|
4238
|
+
'Computer handoff requires a native browser tab, not a scripted Browser session.', { retryable: false });
|
|
4239
|
+
}
|
|
4240
|
+
const managedTarget = input.pluginId === 'computer'
|
|
4241
|
+
? undefined : managedSession ? await runtimeManagedPageForSession(input.repoRoot, current, managedSession) : undefined;
|
|
4221
4242
|
if (managedSession && managedTarget) {
|
|
4222
4243
|
const result = { url: managedTarget.page.url(), title: await managedTarget.page.title().catch(() => managedSession.title) };
|
|
4223
4244
|
const handoff = startRuntimeManagedBrowserHandoff({
|
|
@@ -4254,7 +4275,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
4254
4275
|
const nativeTab = nativeSession?.browser?.tab;
|
|
4255
4276
|
const nativeBrowser = nativeSession?.browser?.provider === 'macos-apple-events'
|
|
4256
4277
|
&& nativeSession.browser.browserProduct
|
|
4257
|
-
&& nativeTab?.ownership === 'plugin_owned'
|
|
4278
|
+
&& (nativeTab?.ownership === 'plugin_owned' || nativeTab?.ownership === 'user_owned')
|
|
4258
4279
|
&& typeof nativeTab.windowId === 'string'
|
|
4259
4280
|
&& typeof nativeTab.tabId === 'string'
|
|
4260
4281
|
? {
|
|
@@ -4262,6 +4283,11 @@ async function executeBrowserPluginActionInternal(
|
|
|
4262
4283
|
ref: { windowId: nativeTab.windowId, tabId: nativeTab.tabId },
|
|
4263
4284
|
}
|
|
4264
4285
|
: undefined;
|
|
4286
|
+
if (input.pluginId === 'computer' && !nativeBrowser) {
|
|
4287
|
+
throw new AssistantPluginError('PLUGIN_COMPUTER_NATIVE_BROWSER_HANDOFF_UNAVAILABLE',
|
|
4288
|
+
'Computer handoff requires an existing bound native tab; it will not open a Playwright profile.',
|
|
4289
|
+
{ retryable: false });
|
|
4290
|
+
}
|
|
4265
4291
|
const handoffMode: BrowserMode = current.browserMode === 'isolated' ? 'isolated' : 'managed_persistent';
|
|
4266
4292
|
const profile = selectedProfile(current, input.repoRoot, handoffMode, target.sessionId);
|
|
4267
4293
|
if (!nativeBrowser) mkdirSync(profile.profileDir, { recursive: true });
|
|
@@ -4296,6 +4322,11 @@ async function executeBrowserPluginActionInternal(
|
|
|
4296
4322
|
case 'get_handoff_status': {
|
|
4297
4323
|
const interactionId = requiredString(input.args.interaction_id, 'interaction_id');
|
|
4298
4324
|
const handoff = getBrowserHandoff(input.repoRoot, interactionId);
|
|
4325
|
+
if (input.pluginId === 'computer' && !handoff.targetId.startsWith('macos-apple-events:')) {
|
|
4326
|
+
throw new AssistantPluginError('PLUGIN_COMPUTER_BROWSER_PROVIDER_MISMATCH',
|
|
4327
|
+
'Computer can only inspect native browser handoffs; standalone scripted handoffs are independent.',
|
|
4328
|
+
{ retryable: false });
|
|
4329
|
+
}
|
|
4299
4330
|
if (handoff.status === 'completed' && handoff.result?.url) {
|
|
4300
4331
|
const session = findBrowserSession(input.repoRoot, handoff.sessionId);
|
|
4301
4332
|
if (session) {
|
|
@@ -4323,6 +4354,11 @@ async function executeBrowserPluginActionInternal(
|
|
|
4323
4354
|
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'resolution must be resume or cancel', { retryable: false });
|
|
4324
4355
|
}
|
|
4325
4356
|
const currentHandoff = getBrowserHandoff(input.repoRoot, interactionId);
|
|
4357
|
+
if (input.pluginId === 'computer' && !currentHandoff.targetId.startsWith('macos-apple-events:')) {
|
|
4358
|
+
throw new AssistantPluginError('PLUGIN_COMPUTER_BROWSER_PROVIDER_MISMATCH',
|
|
4359
|
+
'Computer can resolve only native browser handoffs; scripted Browser handoffs remain independent.',
|
|
4360
|
+
{ retryable: false });
|
|
4361
|
+
}
|
|
4326
4362
|
if (isRuntimeManagedBrowserHandoff(currentHandoff)) {
|
|
4327
4363
|
const session = findBrowserSession(input.repoRoot, currentHandoff.sessionId);
|
|
4328
4364
|
const managedTarget = session ? await runtimeManagedPageForSession(input.repoRoot, current, session) : undefined;
|
|
@@ -5223,30 +5259,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
5223
5259
|
if (requested.length === 0 || requested.length > 32) {
|
|
5224
5260
|
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'attach_local_file requires file_path or 1-32 file_paths.', { retryable: false });
|
|
5225
5261
|
}
|
|
5226
|
-
const
|
|
5227
|
-
const repositoryRoot = realpathSync(input.repoRoot);
|
|
5228
|
-
const resolved = requested.map((filePath) => {
|
|
5229
|
-
const lexicalPath = resolve(input.repoRoot, filePath);
|
|
5230
|
-
const lexicalRelative = relative(repositoryLexicalRoot, lexicalPath);
|
|
5231
|
-
if (lexicalRelative === '..' || lexicalRelative.startsWith(`..${sep}`) || isAbsolute(lexicalRelative)) {
|
|
5232
|
-
throw new AssistantPluginError('PLUGIN_POLICY_BLOCKED', 'Local file attachments must stay inside the repository root.', { retryable: false, details: { fileName: basename(filePath) } });
|
|
5233
|
-
}
|
|
5234
|
-
if (!existsSync(lexicalPath)) {
|
|
5235
|
-
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', `Local attachment does not exist: ${basename(lexicalPath)}`, { retryable: false });
|
|
5236
|
-
}
|
|
5237
|
-
const realPath = realpathSync(lexicalPath);
|
|
5238
|
-
const repositoryRelative = relative(repositoryRoot, realPath);
|
|
5239
|
-
if (repositoryRelative === '..' || repositoryRelative.startsWith(`..${sep}`) || isAbsolute(repositoryRelative)) {
|
|
5240
|
-
throw new AssistantPluginError('PLUGIN_POLICY_BLOCKED', 'Local file attachments must resolve inside the repository root.', { retryable: false, details: { fileName: basename(lexicalPath) } });
|
|
5241
|
-
}
|
|
5242
|
-
if (!statSync(realPath).isFile()) {
|
|
5243
|
-
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', `Local attachment is not a regular file: ${basename(lexicalPath)}`, { retryable: false });
|
|
5244
|
-
}
|
|
5245
|
-
if (/\.(exe|dmg|pkg|sh|bat|cmd|app)$/i.test(realPath)) {
|
|
5246
|
-
throw new AssistantPluginError('PLUGIN_POLICY_BLOCKED', 'Executable file attachments are not allowed.', { retryable: false });
|
|
5247
|
-
}
|
|
5248
|
-
return lexicalPath;
|
|
5249
|
-
});
|
|
5262
|
+
const resolved = resolveBrowserAttachmentPaths(input, requested, stringValue(input.args.target_key));
|
|
5250
5263
|
return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
5251
5264
|
await page.evaluate((payload) => {
|
|
5252
5265
|
const args = payload as { selector: string; count: number };
|
|
@@ -5352,6 +5365,61 @@ async function executeBrowserPluginActionInternal(
|
|
|
5352
5365
|
}
|
|
5353
5366
|
}
|
|
5354
5367
|
|
|
5368
|
+
/** Resolve one upload batch under the repository or the existing local-system target grant.
|
|
5369
|
+
* A target_key changes the path base; it never grants arbitrary absolute filesystem access.
|
|
5370
|
+
*/
|
|
5371
|
+
export function resolveBrowserAttachmentPaths(
|
|
5372
|
+
input: AssistantPluginActionExecutionInput,
|
|
5373
|
+
requested: string[],
|
|
5374
|
+
targetKey?: string,
|
|
5375
|
+
): string[] {
|
|
5376
|
+
const repositoryLexicalRoot = resolve(input.repoRoot);
|
|
5377
|
+
const repositoryRoot = realpathSync(input.repoRoot);
|
|
5378
|
+
return requested.map((filePath) => {
|
|
5379
|
+
let allowedPath: string;
|
|
5380
|
+
if (targetKey) {
|
|
5381
|
+
try {
|
|
5382
|
+
allowedPath = resolveWorkspaceTargetPath(input.controllerHome, targetKey, filePath, {
|
|
5383
|
+
mustExist: true,
|
|
5384
|
+
kind: 'file',
|
|
5385
|
+
operation: 'read',
|
|
5386
|
+
ownerScope: pluginCapabilityAuthorizationOwnerScope(input.origin),
|
|
5387
|
+
}).path;
|
|
5388
|
+
} catch (error) {
|
|
5389
|
+
if (!(error instanceof WorkspaceTargetGrantError)) throw error;
|
|
5390
|
+
throw new AssistantPluginError('PLUGIN_POLICY_BLOCKED',
|
|
5391
|
+
'Upload file is not within an active, caller-owned local-system target grant.',
|
|
5392
|
+
{ retryable: false, details: { fileName: basename(filePath), targetKey, causeCode: error.code } });
|
|
5393
|
+
}
|
|
5394
|
+
} else {
|
|
5395
|
+
const lexicalPath = resolve(input.repoRoot, filePath);
|
|
5396
|
+
const lexicalRelative = relative(repositoryLexicalRoot, lexicalPath);
|
|
5397
|
+
if (lexicalRelative === '..' || lexicalRelative.startsWith(`..${sep}`) || isAbsolute(lexicalRelative)) {
|
|
5398
|
+
throw new AssistantPluginError('PLUGIN_POLICY_BLOCKED', 'Local file attachments must stay inside the repository root.', {
|
|
5399
|
+
retryable: false, details: { fileName: basename(filePath) },
|
|
5400
|
+
});
|
|
5401
|
+
}
|
|
5402
|
+
if (!existsSync(lexicalPath)) {
|
|
5403
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', `Local attachment does not exist: ${basename(lexicalPath)}`, { retryable: false });
|
|
5404
|
+
}
|
|
5405
|
+
allowedPath = realpathSync(lexicalPath);
|
|
5406
|
+
const repositoryRelative = relative(repositoryRoot, allowedPath);
|
|
5407
|
+
if (repositoryRelative === '..' || repositoryRelative.startsWith(`..${sep}`) || isAbsolute(repositoryRelative)) {
|
|
5408
|
+
throw new AssistantPluginError('PLUGIN_POLICY_BLOCKED', 'Local file attachments must resolve inside the repository root.', {
|
|
5409
|
+
retryable: false, details: { fileName: basename(lexicalPath) },
|
|
5410
|
+
});
|
|
5411
|
+
}
|
|
5412
|
+
}
|
|
5413
|
+
if (!statSync(allowedPath).isFile()) {
|
|
5414
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', `Local attachment is not a regular file: ${basename(filePath)}`, { retryable: false });
|
|
5415
|
+
}
|
|
5416
|
+
if (/\.(exe|dmg|pkg|sh|bat|cmd|app)$/i.test(allowedPath)) {
|
|
5417
|
+
throw new AssistantPluginError('PLUGIN_POLICY_BLOCKED', 'Executable file attachments are not allowed.', { retryable: false });
|
|
5418
|
+
}
|
|
5419
|
+
return allowedPath;
|
|
5420
|
+
});
|
|
5421
|
+
}
|
|
5422
|
+
|
|
5355
5423
|
export async function executeBrowserPluginAction(input: AssistantPluginActionExecutionInput): Promise<Record<string, unknown>> {
|
|
5356
5424
|
return await withBrowserSessionAuthorityContext(
|
|
5357
5425
|
{ controllerHome: input.controllerHome, repoId: input.repoId },
|