@moretea-labs/forge 1.7.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 +2 -0
- package/CHANGELOG.md +13 -0
- package/CLAUDE.md +2 -0
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/agents/openai.yaml +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +2 -0
- package/assets/plugin-registry.v1.json +3 -3
- package/assets/skill-version.json +2 -2
- package/assets/templates/helpers/check-architecture-sync.sh +17 -0
- package/assets/templates/helpers/inspect-project-state.ts +8 -6
- package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
- package/assets/templates/helpers/workflow-contract.ts +5 -1
- package/docs/architecture/CURRENT.md +12 -4
- package/docs/forge-plugin-management.md +1 -1
- package/docs/operations/controller-performance-and-502.md +19 -0
- package/docs/operations/homebrew.md +2 -2
- package/package.json +5 -2
- 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-work-efficiency.ts +0 -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 -1531
- package/scripts/check-architecture-sync.sh +2 -302
- 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-runtime-architecture.mjs +243 -16
- package/scripts/check-skill-version.ts +3 -191
- package/scripts/check-source-duplication.mjs +61 -0
- package/scripts/check-stable-baseline.ts +0 -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 -702
- package/scripts/ensure-task-workflow.sh +1 -1192
- package/scripts/inspect-project-state.ts +4 -279
- package/scripts/lib/project-init-lib.sh +7 -2
- 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 -33
- 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 -771
- 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 +3 -0
- package/scripts/select-agent-context-blocks.sh +2 -64
- package/scripts/ship-worktrees.sh +1 -531
- 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 -593
- package/scripts/verify-sprint.sh +1 -500
- package/scripts/workflow-contract.ts +4 -201
- package/scripts/workstream-sync.sh +2 -261
- package/src/cli/agent-jobs/executor-health.ts +1 -1
- package/src/cli/commands/plugin.ts +12 -3
- package/src/cli/controller/check-runner.ts +7 -2
- package/src/cli/controller/context-pack.ts +46 -21
- package/src/cli/github/contracts.ts +16 -0
- package/src/cli/github/github.ts +2 -16
- package/src/cli/github/session.ts +2 -1
- package/src/cli/local-bridge/facade-api.ts +37 -47
- package/src/cli/local-bridge/server.ts +2 -2
- package/src/cli/mcp/legacy-tool-service.ts +10 -33
- package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
- package/src/cli/mcp/repository-tools.ts +85 -89
- package/src/cli/mcp/server.ts +57 -13
- package/src/cli/mcp/tool-contract.ts +14 -0
- package/src/cli/mcp/toolset.ts +2 -2
- package/src/cli/mcp/transports/http.ts +23 -0
- package/src/cli/repositories/command-classifier.ts +2 -2
- package/src/cli/repositories/command-executor.ts +24 -6
- package/src/cli/repositories/repository-snapshot.ts +5 -2
- package/src/cli/repositories/structured-git.ts +57 -3
- package/src/cli/repository/inspector.ts +11 -3
- package/src/runtime/context/generic-lsp-provider.ts +1 -1
- package/src/runtime/context/semantic-navigation-contract.ts +61 -0
- package/src/runtime/context/semantic-navigation.ts +9 -61
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +18 -25
- 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/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 +1 -1
- 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-task-receipt.ts +4 -43
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +13 -3
- 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 +5 -1
- package/src/runtime/control-plane/facade/controller-round-relay.ts +164 -37
- package/src/runtime/control-plane/facade/controller-session-store.ts +183 -24
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +15 -1
- package/src/runtime/control-plane/facade/goal-workloop.ts +329 -148
- package/src/runtime/control-plane/facade/index.ts +2 -0
- package/src/runtime/control-plane/facade/plan-contract-store.ts +90 -22
- 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/types.ts +68 -5
- package/src/runtime/control-plane/facade/work-contract-store.ts +3 -209
- package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +14 -7
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +5 -1
- package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +13 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +15 -6
- package/src/runtime/control-plane/launcher/launch-reservation-store.ts +54 -5
- package/src/runtime/control-plane/launcher/thin-launcher.ts +56 -7
- package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
- package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
- package/src/runtime/control-plane/routing/route-policy.ts +18 -6
- package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
- package/src/runtime/evidence/process-check-execution.ts +18 -0
- package/src/runtime/execution/jobs/child-reference.ts +6 -2
- package/src/runtime/execution/process-runtime/check-result.ts +53 -18
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
- package/src/runtime/execution/process-runtime/command-facade.ts +39 -17
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +20 -3
- package/src/runtime/execution/process-runtime/persisted-check.ts +4 -1
- package/src/runtime/execution/process-runtime/runtime.ts +10 -0
- package/src/runtime/execution/process-runtime/types.ts +2 -12
- package/src/runtime/execution/work-activity.ts +21 -0
- package/src/runtime/execution/workers/executor.ts +18 -13
- package/src/runtime/gateway/mcp/execution-tools.ts +35 -2930
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
- package/src/runtime/gateway/mcp/router.ts +2 -2
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +21 -2
- package/src/runtime/gateway/mcp/runtime-tools.ts +805 -1009
- package/src/runtime/plugins/app-store-connect-adapter.ts +93 -28
- package/src/runtime/plugins/browser-adapter.ts +256 -1083
- package/src/runtime/plugins/browser-handoff-host.ts +2 -25
- package/src/runtime/plugins/browser-handoff.ts +18 -3
- package/src/runtime/plugins/browser-macos-bridge.ts +121 -16
- 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-runtime-contract.ts +2 -0
- package/src/runtime/plugins/browser-runtime.ts +10 -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/desktop-operator-registration.ts +4 -4
- package/src/runtime/plugins/external-adapter.ts +92 -58
- package/src/runtime/plugins/macos-capability-broker.ts +0 -8
- package/src/runtime/plugins/store.ts +66 -9
- package/src/runtime/plugins/types.ts +3 -1
- package/src/runtime/recovery/maintenance-executor.ts +61 -25
- package/src/runtime/root/package-connector-service.ts +46 -4
- package/src/runtime/root/package-runtime-release.ts +1 -1
- package/src/runtime/root/package-runtime-service.ts +33 -2
- package/src/runtime/root/release-manifest.ts +137 -16
- package/src/runtime/root/release-materialize.ts +109 -35
- package/src/runtime/root/types.ts +10 -0
- package/src/runtime/standalone-recovery/core.ts +425 -194
- package/src/runtime/standalone-recovery/entry.ts +1 -1
- package/src/runtime/standalone-recovery/installer.ts +1 -1
- package/src/runtime/workflow/schedules/engine.ts +113 -87
- package/src/runtime/workflow/schedules/settlement.ts +26 -23
- package/src/runtime/workflow/schedules/store.ts +79 -10
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'crypto';
|
|
2
2
|
import { createRequire } from 'module';
|
|
3
|
-
import { existsSync, mkdirSync,
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from 'fs';
|
|
4
4
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'path';
|
|
5
|
-
import { readBrowserBinding } from '../../cli/chatgpt-browser/binding';
|
|
6
5
|
import type {
|
|
7
|
-
AssistantPluginActionDescriptor,
|
|
8
6
|
AssistantPluginActionExecutionInput,
|
|
9
7
|
AssistantPluginAuthorizationContext,
|
|
10
|
-
AssistantPluginCapability,
|
|
11
8
|
AssistantPluginHealth,
|
|
12
9
|
AssistantPluginManifest,
|
|
13
|
-
AssistantPluginPermissionScope,
|
|
14
10
|
} from './types';
|
|
15
11
|
import { AssistantPluginError, toAssistantPluginError } from './errors';
|
|
12
|
+
import {
|
|
13
|
+
browserActions,
|
|
14
|
+
browserCapabilities,
|
|
15
|
+
browserPermissions,
|
|
16
|
+
} from './browser-manifest-surface';
|
|
16
17
|
import { executeBrowserActionThroughNode, shouldUseBrowserNodeBridge } from './browser-node-bridge';
|
|
17
18
|
import { BrowserProviderUnavailableBeforeActionError } from './browser-provider-registry';
|
|
19
|
+
import { MAX_BROWSER_CDP_ENDPOINT_CANDIDATES } from './browser-runtime-contract';
|
|
18
20
|
import {
|
|
19
21
|
ALL_BROWSER_PROVIDER_CAPABILITIES,
|
|
22
|
+
browserActionCanReplayAfterDispatch as canReplayBrowserActionAfterDispatch,
|
|
20
23
|
executeBrowserRuntimeAction,
|
|
21
24
|
invalidateBrowserRuntime,
|
|
22
25
|
} from './browser-runtime';
|
|
@@ -24,11 +27,7 @@ import { getExternalPluginAdapter } from './external-adapter';
|
|
|
24
27
|
import {
|
|
25
28
|
currentBrowserSessionAuthorityContext,
|
|
26
29
|
DEFAULT_BROWSER_SESSION_LIST_LIMIT,
|
|
27
|
-
findBrowserSession as findBrowserSessionAuthority,
|
|
28
|
-
listAllBrowserSessionsForRepository,
|
|
29
30
|
MAX_BROWSER_SESSION_LIST_LIMIT,
|
|
30
|
-
saveBrowserSession as saveBrowserSessionAuthority,
|
|
31
|
-
tombstoneBrowserSession,
|
|
32
31
|
withBrowserSessionAuthorityContext,
|
|
33
32
|
} from './browser-session-authority';
|
|
34
33
|
import {
|
|
@@ -45,6 +44,7 @@ import {
|
|
|
45
44
|
macOsActiveBrowserAttachSupported,
|
|
46
45
|
macOsBrowserJavaScriptAutomationDisabled,
|
|
47
46
|
type MacOsBrowserAttachAttempt,
|
|
47
|
+
type MacOsBrowserCreateTabEvidence,
|
|
48
48
|
type MacOsBrowserProduct,
|
|
49
49
|
type MacOsBrowserTabRef,
|
|
50
50
|
} from './browser-macos-bridge';
|
|
@@ -56,31 +56,58 @@ import {
|
|
|
56
56
|
isRuntimeManagedBrowserHandoff,
|
|
57
57
|
listBrowserHandoffs,
|
|
58
58
|
resolveRuntimeManagedBrowserHandoff,
|
|
59
|
-
|
|
59
|
+
resumeBrowserHandoffAndWait,
|
|
60
60
|
startBrowserHandoff,
|
|
61
61
|
startRuntimeManagedBrowserHandoff,
|
|
62
62
|
} from './browser-handoff';
|
|
63
|
+
import {
|
|
64
|
+
browserStateDir,
|
|
65
|
+
findBrowserSession,
|
|
66
|
+
listSavedBrowserSessions,
|
|
67
|
+
removeBrowserSession,
|
|
68
|
+
requireBrowserSession,
|
|
69
|
+
saveBrowserSession,
|
|
70
|
+
} from './browser-session-store';
|
|
71
|
+
import {
|
|
72
|
+
closeTrackedNativeOwnedSession,
|
|
73
|
+
inspectNativeOwnedSessions,
|
|
74
|
+
nativeTabInventoryUnsupported,
|
|
75
|
+
nativeOwnedAliasSessionIds,
|
|
76
|
+
} from './browser-native-session-service';
|
|
77
|
+
export { browserActionCanReplayAfterDispatch } from './browser-runtime';
|
|
78
|
+
import type {
|
|
79
|
+
BrowserCdpAttachFallback,
|
|
80
|
+
BrowserConnectionFallback,
|
|
81
|
+
BrowserMode,
|
|
82
|
+
BrowserProfileMode,
|
|
83
|
+
BrowserSessionResumeDiagnostic,
|
|
84
|
+
BrowserSessionState,
|
|
85
|
+
BrowserSessionConnectionState,
|
|
86
|
+
BrowserSessionInventoryItem,
|
|
87
|
+
BrowserTabMatchReason,
|
|
88
|
+
BrowserTabResumeState,
|
|
89
|
+
CdpAttachAttempt,
|
|
90
|
+
} from './browser-session-types';
|
|
63
91
|
|
|
64
92
|
const BROWSER_PLUGIN_ID = 'browser';
|
|
65
93
|
const CONFIG_ROOT = '.forge/plugins';
|
|
66
|
-
const STATE_ROOT = '.forge/browser';
|
|
67
94
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
68
95
|
const DEFAULT_MAX_TEXT_CHARS = 20_000;
|
|
69
96
|
const DEFAULT_POST_ACTION_WAIT_MS = 750;
|
|
70
97
|
const DEFAULT_CDP_DISCOVERY_TIMEOUT_MS = 1_500;
|
|
71
98
|
const MAX_CDP_DISCOVERY_TIMEOUT_MS = 5_000;
|
|
72
|
-
const MAX_CDP_ENDPOINT_CANDIDATES = 5;
|
|
73
99
|
|
|
74
100
|
type WaitUntil = 'load' | 'domcontentloaded' | 'networkidle';
|
|
75
101
|
type WaitForSelectorState = 'attached' | 'detached' | 'visible' | 'hidden';
|
|
76
|
-
type BrowserMode = 'attach_preferred' | 'managed_persistent' | 'isolated';
|
|
77
|
-
type BrowserProfileMode = 'repo_local' | 'custom';
|
|
78
102
|
type BrowserChannel = 'chromium' | 'chrome' | 'chrome-beta' | 'chrome-dev' | 'chrome-canary';
|
|
79
|
-
type BrowserCdpAttachFallback = 'managed_persistent' | 'fail_closed';
|
|
80
103
|
type BrowserNativeAttachMode = 'auto' | 'disabled';
|
|
81
104
|
|
|
105
|
+
const CURRENT_BROWSER_CONFIG_SCHEMA_VERSION = 2 as const;
|
|
106
|
+
const DEFAULT_USER_BROWSER_CHANNEL: BrowserChannel = 'chrome';
|
|
107
|
+
const DEFAULT_USER_NATIVE_BROWSER_CANDIDATES: MacOsBrowserProduct[] = ['chrome'];
|
|
108
|
+
|
|
82
109
|
interface BrowserPluginConfig {
|
|
83
|
-
schemaVersion:
|
|
110
|
+
schemaVersion: 2;
|
|
84
111
|
enabled: boolean;
|
|
85
112
|
provider: 'playwright';
|
|
86
113
|
browserMode?: BrowserMode;
|
|
@@ -98,51 +125,8 @@ interface BrowserPluginConfig {
|
|
|
98
125
|
defaultTimeoutMs?: number;
|
|
99
126
|
}
|
|
100
127
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
index: number;
|
|
104
|
-
url: string;
|
|
105
|
-
title?: string;
|
|
106
|
-
matchedBy: BrowserTabMatchReason;
|
|
107
|
-
inventoryCount: number;
|
|
108
|
-
capturedAt: string;
|
|
109
|
-
ownership?: 'plugin_owned' | 'user_owned';
|
|
110
|
-
ownerToken?: string;
|
|
111
|
-
windowId?: string;
|
|
112
|
-
tabId?: string;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
interface BrowserSessionConnectionState {
|
|
116
|
-
mode: BrowserMode;
|
|
117
|
-
activeMode: BrowserMode;
|
|
118
|
-
provider: 'playwright-cdp' | 'playwright-persistent-context' | 'macos-apple-events';
|
|
119
|
-
endpoint?: string;
|
|
120
|
-
browserVersion?: string;
|
|
121
|
-
browserProduct?: MacOsBrowserProduct;
|
|
122
|
-
nativeBrowserCandidates?: MacOsBrowserProduct[];
|
|
123
|
-
fallback?: BrowserConnectionFallback;
|
|
124
|
-
tab?: BrowserTabResumeState;
|
|
125
|
-
sessionResume?: BrowserSessionResumeDiagnostic;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
interface BrowserSessionState {
|
|
129
|
-
schemaVersion: 1;
|
|
130
|
-
sessionId: string;
|
|
131
|
-
url: string;
|
|
132
|
-
title?: string;
|
|
133
|
-
createdAt: string;
|
|
134
|
-
updatedAt: string;
|
|
135
|
-
browser?: BrowserSessionConnectionState;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
type BrowserSessionLiveness = 'live' | 'unverified' | 'dead';
|
|
139
|
-
|
|
140
|
-
type BrowserSessionInventoryItem = {
|
|
141
|
-
session: BrowserSessionState;
|
|
142
|
-
liveness: BrowserSessionLiveness;
|
|
143
|
-
evidence: 'runtime_session_binding' | 'runtime_owner_token' | 'runtime_bound_page_missing' | 'runtime_context_unavailable' | 'runtime_owner_token_not_observed' | 'provider_unverified' | 'native_tab_live' | 'native_tab_missing' | 'native_tab_invalid' | 'native_tab_invalid_closed' | 'native_tab_invalid_close_failed' | 'native_inventory_unavailable';
|
|
144
|
-
pruned?: boolean;
|
|
145
|
-
cleanupError?: string;
|
|
128
|
+
type PersistedBrowserPluginConfig = Partial<Omit<BrowserPluginConfig, 'schemaVersion'>> & {
|
|
129
|
+
schemaVersion?: 1 | 2;
|
|
146
130
|
};
|
|
147
131
|
|
|
148
132
|
type BrowserSessionInventory = {
|
|
@@ -426,16 +410,12 @@ function configPath(repoRoot: string): string {
|
|
|
426
410
|
return join(repoRoot, CONFIG_ROOT, 'browser.json');
|
|
427
411
|
}
|
|
428
412
|
|
|
429
|
-
function stateDir(repoRoot: string, name: 'sessions' | 'screenshots' | 'profiles' | 'downloads' | 'diagnostics'): string {
|
|
430
|
-
return join(repoRoot, STATE_ROOT, name);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
413
|
function defaultProfileDir(repoRoot: string): string {
|
|
434
|
-
return join(
|
|
414
|
+
return join(browserStateDir(repoRoot, 'profiles'), 'default');
|
|
435
415
|
}
|
|
436
416
|
|
|
437
417
|
function isolatedProfileDir(repoRoot: string, sessionId: string): string {
|
|
438
|
-
return join(
|
|
418
|
+
return join(browserStateDir(repoRoot, 'profiles'), 'isolated', sessionId.replace(/[^a-zA-Z0-9_-]/g, '_'));
|
|
439
419
|
}
|
|
440
420
|
|
|
441
421
|
function resolveConfiguredPath(repoRoot: string, value: string): string {
|
|
@@ -467,30 +447,47 @@ function resolveProfileSelection(repoRoot: string, profileDir: string, profileDi
|
|
|
467
447
|
};
|
|
468
448
|
}
|
|
469
449
|
|
|
470
|
-
function normalizeConfig(raw:
|
|
450
|
+
function normalizeConfig(raw: PersistedBrowserPluginConfig): BrowserPluginConfig {
|
|
471
451
|
const normalizedProfileDir = stringValue(raw.profileDir);
|
|
472
452
|
return {
|
|
473
|
-
schemaVersion:
|
|
453
|
+
schemaVersion: CURRENT_BROWSER_CONFIG_SCHEMA_VERSION,
|
|
474
454
|
enabled: raw.enabled === true,
|
|
475
455
|
provider: 'playwright',
|
|
476
456
|
browserMode: browserMode(raw.browserMode) ?? 'attach_preferred',
|
|
477
457
|
profileMode: browserProfileMode(raw.profileMode) ?? (normalizedProfileDir ? 'custom' : 'repo_local'),
|
|
478
458
|
profileDir: normalizedProfileDir,
|
|
479
459
|
profileDirectory: stringValue(raw.profileDirectory),
|
|
480
|
-
browserChannel: browserChannel(raw.browserChannel) ??
|
|
460
|
+
browserChannel: browserChannel(raw.browserChannel) ?? DEFAULT_USER_BROWSER_CHANNEL,
|
|
481
461
|
executablePath: stringValue(raw.executablePath),
|
|
482
462
|
cdpEndpoint: stringValue(raw.cdpEndpoint),
|
|
483
|
-
cdpEndpointCandidates: stringList(raw.cdpEndpointCandidates)?.slice(0,
|
|
463
|
+
cdpEndpointCandidates: stringList(raw.cdpEndpointCandidates)?.slice(0, MAX_BROWSER_CDP_ENDPOINT_CANDIDATES),
|
|
484
464
|
cdpDiscoveryTimeoutMs: typeof raw.cdpDiscoveryTimeoutMs === 'number'
|
|
485
465
|
? Math.min(positiveNumber(raw.cdpDiscoveryTimeoutMs, DEFAULT_CDP_DISCOVERY_TIMEOUT_MS), MAX_CDP_DISCOVERY_TIMEOUT_MS)
|
|
486
466
|
: undefined,
|
|
487
467
|
cdpAttachFallback: browserCdpAttachFallback(raw.cdpAttachFallback) ?? 'fail_closed',
|
|
488
468
|
nativeAttachMode: browserNativeAttachMode(raw.nativeAttachMode) ?? 'auto',
|
|
489
|
-
nativeBrowserCandidates: browserProductList(raw.nativeBrowserCandidates) ?? [
|
|
469
|
+
nativeBrowserCandidates: browserProductList(raw.nativeBrowserCandidates) ?? [...DEFAULT_USER_NATIVE_BROWSER_CANDIDATES],
|
|
490
470
|
defaultTimeoutMs: typeof raw.defaultTimeoutMs === 'number' ? positiveNumber(raw.defaultTimeoutMs, DEFAULT_TIMEOUT_MS) : undefined,
|
|
491
471
|
};
|
|
492
472
|
}
|
|
493
473
|
|
|
474
|
+
function migrateLegacyRepositoryConfig(raw: PersistedBrowserPluginConfig): PersistedBrowserPluginConfig {
|
|
475
|
+
if (raw.schemaVersion !== 1) return raw;
|
|
476
|
+
return {
|
|
477
|
+
schemaVersion: CURRENT_BROWSER_CONFIG_SCHEMA_VERSION,
|
|
478
|
+
enabled: raw.enabled,
|
|
479
|
+
provider: 'playwright',
|
|
480
|
+
browserMode: 'attach_preferred',
|
|
481
|
+
profileMode: 'repo_local',
|
|
482
|
+
browserChannel: DEFAULT_USER_BROWSER_CHANNEL,
|
|
483
|
+
cdpAttachFallback: 'fail_closed',
|
|
484
|
+
nativeAttachMode: 'auto',
|
|
485
|
+
nativeBrowserCandidates: [...DEFAULT_USER_NATIVE_BROWSER_CANDIDATES],
|
|
486
|
+
defaultTimeoutMs: raw.defaultTimeoutMs,
|
|
487
|
+
cdpDiscoveryTimeoutMs: raw.cdpDiscoveryTimeoutMs,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
494
491
|
function readJson<T>(path: string): T | undefined {
|
|
495
492
|
try {
|
|
496
493
|
return JSON.parse(readFileSync(path, 'utf-8')) as T;
|
|
@@ -504,71 +501,23 @@ function writeJson(path: string, value: unknown): void {
|
|
|
504
501
|
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, 'utf-8');
|
|
505
502
|
}
|
|
506
503
|
|
|
507
|
-
function writeAtomicJson(path: string, value: unknown): void {
|
|
508
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
509
|
-
const tempPath = join(dirname(path), `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
|
|
510
|
-
try {
|
|
511
|
-
writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, 'utf-8');
|
|
512
|
-
renameSync(tempPath, path);
|
|
513
|
-
} finally {
|
|
514
|
-
rmSync(tempPath, { force: true });
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
function readBrowserSessionJson(path: string): BrowserSessionState | undefined {
|
|
519
|
-
let raw: string;
|
|
520
|
-
try {
|
|
521
|
-
raw = readFileSync(path, 'utf-8');
|
|
522
|
-
} catch (error) {
|
|
523
|
-
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') return undefined;
|
|
524
|
-
throw new AssistantPluginError('PLUGIN_BROWSER_SESSION_STATE_READ_FAILED', 'Saved browser session metadata could not be read.', {
|
|
525
|
-
retryable: true,
|
|
526
|
-
details: { fileName: basename(path), cause: error instanceof Error ? error.message : String(error) },
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
try {
|
|
530
|
-
return JSON.parse(raw) as BrowserSessionState;
|
|
531
|
-
} catch (error) {
|
|
532
|
-
throw new AssistantPluginError('PLUGIN_BROWSER_SESSION_STATE_CORRUPT', 'Saved browser session metadata is malformed; refusing to treat corrupt state as a missing session.', {
|
|
533
|
-
retryable: false,
|
|
534
|
-
details: { fileName: basename(path), cause: error instanceof Error ? error.message : String(error) },
|
|
535
|
-
});
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
function legacyChatgptBindingConfig(repoRoot: string): Partial<BrowserPluginConfig> {
|
|
540
|
-
const binding = readBrowserBinding(repoRoot).binding;
|
|
541
|
-
if (!binding) return {};
|
|
542
|
-
return {
|
|
543
|
-
profileMode: 'custom',
|
|
544
|
-
profileDir: binding.profileDir,
|
|
545
|
-
profileDirectory: binding.profileDirectory,
|
|
546
|
-
browserChannel: browserChannel(binding.browserChannel) ?? 'chromium',
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
|
|
550
504
|
function loadConfig(repoRoot: string): BrowserPluginConfig {
|
|
551
|
-
const persisted = readJson<
|
|
552
|
-
if (!persisted) return normalizeConfig(
|
|
553
|
-
|
|
554
|
-
if (browserProfileMode(persisted.profileMode) === 'repo_local') {
|
|
555
|
-
merged.profileDir = undefined;
|
|
556
|
-
merged.profileDirectory = undefined;
|
|
557
|
-
}
|
|
558
|
-
return normalizeConfig(merged);
|
|
505
|
+
const persisted = readJson<PersistedBrowserPluginConfig>(configPath(repoRoot));
|
|
506
|
+
if (!persisted) return normalizeConfig({});
|
|
507
|
+
return normalizeConfig(migrateLegacyRepositoryConfig(persisted));
|
|
559
508
|
}
|
|
560
509
|
|
|
561
510
|
async function resolveBrowserPluginAuthorizationContextInternal(
|
|
562
511
|
input: AssistantPluginActionExecutionInput,
|
|
563
512
|
): Promise<AssistantPluginAuthorizationContext | undefined> {
|
|
564
|
-
const action =
|
|
513
|
+
const action = browserActions().find((entry) => entry.actionId === input.actionId);
|
|
565
514
|
if (!action || action.confirmation !== 'authorization' || !action.scopes.includes('browser.interact')) return undefined;
|
|
566
515
|
|
|
567
516
|
const persistedConfig = loadConfig(input.repoRoot);
|
|
568
517
|
const sessionId = stringValue(input.args.session_id);
|
|
569
518
|
const authorizationBrowserMode = parseBrowserModeInput(input.args.browser_mode) ?? persistedConfig.browserMode;
|
|
570
519
|
if (!sessionId || authorizationBrowserMode === 'isolated') return undefined;
|
|
571
|
-
const session =
|
|
520
|
+
const session = findBrowserSession(input.repoRoot, sessionId);
|
|
572
521
|
const config = effectiveBrowserActionConfig(persistedConfig, input.args, session);
|
|
573
522
|
const connection = session?.browser;
|
|
574
523
|
if (!session || !connection || connection.activeMode === 'isolated') return undefined;
|
|
@@ -630,73 +579,6 @@ function saveConfig(repoRoot: string, patch: Partial<BrowserPluginConfig>): Brow
|
|
|
630
579
|
return next;
|
|
631
580
|
}
|
|
632
581
|
|
|
633
|
-
function sessionPath(repoRoot: string, sessionId: string): string {
|
|
634
|
-
return join(stateDir(repoRoot, 'sessions'), `${sessionId.replace(/[^a-zA-Z0-9_-]/g, '_')}.json`);
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
function saveSession(repoRoot: string, session: BrowserSessionState): BrowserSessionState {
|
|
638
|
-
const authority = currentBrowserSessionAuthorityContext();
|
|
639
|
-
if (!authority) {
|
|
640
|
-
writeAtomicJson(sessionPath(repoRoot, session.sessionId), session);
|
|
641
|
-
return session;
|
|
642
|
-
}
|
|
643
|
-
const saved = saveBrowserSessionAuthority<BrowserSessionState>(authority.controllerHome, authority.repoId, repoRoot, session);
|
|
644
|
-
if (saved.sessionId === session.sessionId || !saved.browser?.sessionResume) return saved;
|
|
645
|
-
return {
|
|
646
|
-
...saved,
|
|
647
|
-
browser: {
|
|
648
|
-
...saved.browser,
|
|
649
|
-
sessionResume: { ...saved.browser.sessionResume, sessionId: saved.sessionId },
|
|
650
|
-
},
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
function findSession(repoRoot: string, sessionId: string): BrowserSessionState | undefined {
|
|
655
|
-
const authority = currentBrowserSessionAuthorityContext();
|
|
656
|
-
return authority
|
|
657
|
-
? findBrowserSessionAuthority<BrowserSessionState>(authority.controllerHome, authority.repoId, repoRoot, sessionId)
|
|
658
|
-
: readBrowserSessionJson(sessionPath(repoRoot, sessionId));
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
function listSavedBrowserSessions(repoRoot: string): BrowserSessionState[] {
|
|
662
|
-
const authority = currentBrowserSessionAuthorityContext();
|
|
663
|
-
if (authority) {
|
|
664
|
-
return listAllBrowserSessionsForRepository<BrowserSessionState>(authority.controllerHome, authority.repoId, repoRoot);
|
|
665
|
-
}
|
|
666
|
-
const root = stateDir(repoRoot, 'sessions');
|
|
667
|
-
let names: string[];
|
|
668
|
-
try {
|
|
669
|
-
names = readdirSync(root);
|
|
670
|
-
} catch (error) {
|
|
671
|
-
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') return [];
|
|
672
|
-
throw new AssistantPluginError('PLUGIN_BROWSER_SESSION_STATE_READ_FAILED', 'Saved browser session directory could not be read.', {
|
|
673
|
-
retryable: true,
|
|
674
|
-
details: { cause: error instanceof Error ? error.message : String(error) },
|
|
675
|
-
});
|
|
676
|
-
}
|
|
677
|
-
return names
|
|
678
|
-
.filter((name) => name.endsWith('.json'))
|
|
679
|
-
.map((name) => readBrowserSessionJson(join(root, name)))
|
|
680
|
-
.filter((session): session is BrowserSessionState => Boolean(session));
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
function removeSession(repoRoot: string, sessionId: string): void {
|
|
684
|
-
const authority = currentBrowserSessionAuthorityContext();
|
|
685
|
-
if (authority) {
|
|
686
|
-
tombstoneBrowserSession(authority.controllerHome, authority.repoId, repoRoot, sessionId);
|
|
687
|
-
return;
|
|
688
|
-
}
|
|
689
|
-
rmSync(sessionPath(repoRoot, sessionId), { force: true });
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
function loadSession(repoRoot: string, sessionId: string): BrowserSessionState {
|
|
693
|
-
const state = findSession(repoRoot, sessionId);
|
|
694
|
-
if (!state) {
|
|
695
|
-
throw new AssistantPluginError('PLUGIN_SESSION_NOT_FOUND', `Browser session not found: ${sessionId}`, { retryable: false });
|
|
696
|
-
}
|
|
697
|
-
return state;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
582
|
function parsedAbsoluteUrl(value: unknown): { raw: string; parsed: URL } {
|
|
701
583
|
const raw = stringValue(value);
|
|
702
584
|
if (!raw) throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'url is required.', { retryable: false });
|
|
@@ -840,7 +722,7 @@ function cdpEndpoints(config: BrowserPluginConfig): string[] {
|
|
|
840
722
|
return Array.from(new Set([
|
|
841
723
|
...(config.cdpEndpoint ? [config.cdpEndpoint] : []),
|
|
842
724
|
...(config.cdpEndpointCandidates ?? []),
|
|
843
|
-
].map((entry) => entry.trim()).filter(Boolean))).slice(0,
|
|
725
|
+
].map((entry) => entry.trim()).filter(Boolean))).slice(0, MAX_BROWSER_CDP_ENDPOINT_CANDIDATES);
|
|
844
726
|
}
|
|
845
727
|
|
|
846
728
|
function isLoopbackHostname(hostname: string): boolean {
|
|
@@ -878,8 +760,8 @@ function validateConfig(config: BrowserPluginConfig): string[] {
|
|
|
878
760
|
if (config.browserChannel && config.browserChannel !== 'chromium' && config.executablePath) {
|
|
879
761
|
errors.push('browserChannel and executablePath cannot both be set.');
|
|
880
762
|
}
|
|
881
|
-
if (endpoints.length >
|
|
882
|
-
errors.push(`At most ${
|
|
763
|
+
if (endpoints.length > MAX_BROWSER_CDP_ENDPOINT_CANDIDATES) {
|
|
764
|
+
errors.push(`At most ${MAX_BROWSER_CDP_ENDPOINT_CANDIDATES} CDP endpoint candidates are supported.`);
|
|
883
765
|
}
|
|
884
766
|
for (const endpoint of endpoints) {
|
|
885
767
|
const error = cdpEndpointValidationError(endpoint);
|
|
@@ -904,7 +786,9 @@ function configWarnings(config: BrowserPluginConfig): string[] {
|
|
|
904
786
|
}
|
|
905
787
|
if (config.browserMode === 'attach_preferred' && cdpEndpoints(config).length === 0) {
|
|
906
788
|
if (config.nativeAttachMode === 'auto' && macOsActiveBrowserAttachSupported()) {
|
|
907
|
-
warnings.push(
|
|
789
|
+
warnings.push(config.cdpAttachFallback === 'fail_closed'
|
|
790
|
+
? 'browserMode=attach_preferred has no configured CDP endpoint; configured native-browser attach will be attempted and managed fallback is disabled.'
|
|
791
|
+
: 'browserMode=attach_preferred has no configured CDP endpoint; configured native-browser attach will be attempted before explicit managed fallback.');
|
|
908
792
|
} else {
|
|
909
793
|
warnings.push('browserMode=attach_preferred has no configured CDP endpoint; actions will use the configured fallback policy.');
|
|
910
794
|
}
|
|
@@ -977,7 +861,7 @@ function resolveActionTarget(repoRoot: string, args: Record<string, unknown>): B
|
|
|
977
861
|
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Provide either url or session_id.', { retryable: false });
|
|
978
862
|
}
|
|
979
863
|
if (providedSessionId) {
|
|
980
|
-
const existingSession =
|
|
864
|
+
const existingSession = requireBrowserSession(repoRoot, providedSessionId);
|
|
981
865
|
const nativeUserOwned = isExplicitNativeUserOwnedSession(existingSession);
|
|
982
866
|
const sessionUrl = normalizedSavedSessionUrl(existingSession);
|
|
983
867
|
if (directUrl) {
|
|
@@ -1006,7 +890,7 @@ function sessionFromPage(target: BrowserActionTarget, pageUrl: string, title: st
|
|
|
1006
890
|
}
|
|
1007
891
|
|
|
1008
892
|
function screenshotFilePath(repoRoot: string, actionId: string, sessionId: string, url: string): string {
|
|
1009
|
-
const screenshotDir =
|
|
893
|
+
const screenshotDir = browserStateDir(repoRoot, 'screenshots');
|
|
1010
894
|
mkdirSync(screenshotDir, { recursive: true });
|
|
1011
895
|
const digest = createHash('sha256').update(url).digest('hex').slice(0, 10);
|
|
1012
896
|
const actionLabel = actionId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
@@ -1100,8 +984,6 @@ function selectedProfile(config: BrowserPluginConfig, repoRoot: string, activeMo
|
|
|
1100
984
|
};
|
|
1101
985
|
}
|
|
1102
986
|
|
|
1103
|
-
type BrowserTabMatchReason = 'owned_token' | 'recovered_tab' | 'saved_url_title' | 'saved_url' | 'exact_url' | 'blank' | 'new_page';
|
|
1104
|
-
|
|
1105
987
|
const BROWSER_OWNER_PREFIX = 'forge-browser-owned:';
|
|
1106
988
|
|
|
1107
989
|
interface BrowserTabInventoryEntry {
|
|
@@ -1116,30 +998,6 @@ function ownerTokenForSession(sessionId: string): string {
|
|
|
1116
998
|
return `${BROWSER_OWNER_PREFIX}${createHash('sha256').update(sessionId).digest('hex').slice(0, 24)}`;
|
|
1117
999
|
}
|
|
1118
1000
|
|
|
1119
|
-
interface BrowserSessionResumeDiagnostic {
|
|
1120
|
-
sessionId: string;
|
|
1121
|
-
status: 'matched' | 'stale_tab' | 'no_saved_tab';
|
|
1122
|
-
reason: string;
|
|
1123
|
-
savedTab?: Pick<BrowserTabResumeState, 'key' | 'url' | 'title' | 'index'>;
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
interface BrowserConnectionFallback {
|
|
1127
|
-
policy: BrowserCdpAttachFallback;
|
|
1128
|
-
from: 'attach_preferred';
|
|
1129
|
-
to: 'managed_persistent';
|
|
1130
|
-
reason: string;
|
|
1131
|
-
attempts: CdpAttachAttempt[];
|
|
1132
|
-
nativeAttempts?: MacOsBrowserAttachAttempt[];
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
interface CdpAttachAttempt {
|
|
1136
|
-
endpoint: string;
|
|
1137
|
-
discoveredEndpoint?: string;
|
|
1138
|
-
probeUrl?: string;
|
|
1139
|
-
browserVersion?: string;
|
|
1140
|
-
error?: string;
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
1001
|
interface BrowserConnectionSummary {
|
|
1144
1002
|
requestedMode: BrowserMode;
|
|
1145
1003
|
mode: BrowserMode;
|
|
@@ -1290,6 +1148,27 @@ export function nativeReplacementUrlMatchesTarget(requestedUrl: string, actualUr
|
|
|
1290
1148
|
}
|
|
1291
1149
|
}
|
|
1292
1150
|
|
|
1151
|
+
export function nativeReplacementAssignmentProvenanceProvesTarget(
|
|
1152
|
+
evidence: MacOsBrowserCreateTabEvidence | undefined,
|
|
1153
|
+
requestedUrl: string,
|
|
1154
|
+
replacementRef: MacOsBrowserTabRef,
|
|
1155
|
+
): boolean {
|
|
1156
|
+
if (!evidence || evidence.refSource !== 'structured' || !evidence.navigation) return false;
|
|
1157
|
+
if (evidence.ref.windowId !== replacementRef.windowId || evidence.ref.tabId !== replacementRef.tabId) return false;
|
|
1158
|
+
return evidence.navigation.provenanceVersion === 1
|
|
1159
|
+
&& evidence.navigation.assignmentAccepted === true
|
|
1160
|
+
&& evidence.navigation.requestedUrl === requestedUrl;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
export function nativeReplacementPostAssignmentUrlMatchesTarget(
|
|
1164
|
+
requestedUrl: string,
|
|
1165
|
+
actualUrl: string,
|
|
1166
|
+
assignmentProvenanceProven: boolean,
|
|
1167
|
+
): boolean {
|
|
1168
|
+
if (nativeReplacementUrlMatchesTarget(requestedUrl, actualUrl)) return true;
|
|
1169
|
+
return assignmentProvenanceProven && sameOrigin(actualUrl, requestedUrl);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1293
1172
|
export async function settleNativeCreatedPageIdentity(
|
|
1294
1173
|
page: {
|
|
1295
1174
|
waitForLoadState: (state?: string, options?: Record<string, unknown>) => Promise<void>;
|
|
@@ -1297,9 +1176,29 @@ export async function settleNativeCreatedPageIdentity(
|
|
|
1297
1176
|
},
|
|
1298
1177
|
state: string,
|
|
1299
1178
|
timeoutMs: number,
|
|
1179
|
+
target?: { requestedUrl: string; previousUrl?: string },
|
|
1300
1180
|
): Promise<{ url: string; title: string }> {
|
|
1181
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
1301
1182
|
await page.waitForLoadState(state, { timeout: timeoutMs, requireHttpUrl: true });
|
|
1302
|
-
|
|
1183
|
+
let identity = await page.identity();
|
|
1184
|
+
if (!target || nativeReplacementUrlMatchesTarget(target.requestedUrl, identity.url)) return identity;
|
|
1185
|
+
|
|
1186
|
+
const previousUrl = target.previousUrl?.trim();
|
|
1187
|
+
if (!previousUrl || comparableUrl(identity.url) !== comparableUrl(previousUrl)) return identity;
|
|
1188
|
+
|
|
1189
|
+
// Native Chrome/Vivaldi can briefly expose the obsolete tab URL on the newly
|
|
1190
|
+
// created stable replacement ref before the create-tab navigation commits.
|
|
1191
|
+
// Only that exact previous URL is treated as transitional. A different HTTP(S)
|
|
1192
|
+
// URL remains an immediate postcondition failure, and authority never moves to
|
|
1193
|
+
// another tab while this bounded settlement loop runs.
|
|
1194
|
+
while (Date.now() < deadline) {
|
|
1195
|
+
const remainingMs = deadline - Date.now();
|
|
1196
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(100, remainingMs)));
|
|
1197
|
+
identity = await page.identity();
|
|
1198
|
+
if (nativeReplacementUrlMatchesTarget(target.requestedUrl, identity.url)) return identity;
|
|
1199
|
+
if (comparableUrl(identity.url) !== comparableUrl(previousUrl)) return identity;
|
|
1200
|
+
}
|
|
1201
|
+
return identity;
|
|
1303
1202
|
}
|
|
1304
1203
|
|
|
1305
1204
|
export function nativeReplacementMismatchDiagnostic(input: {
|
|
@@ -2107,7 +2006,7 @@ async function openNativeAttachedContext(
|
|
|
2107
2006
|
): Promise<{ handle?: BrowserOpenHandle; attempts: MacOsBrowserAttachAttempt[] }> {
|
|
2108
2007
|
if (config.nativeAttachMode === 'disabled') return { attempts: [] };
|
|
2109
2008
|
if (!target.existingSession && !stringValue(args.session_id)) {
|
|
2110
|
-
await inspectNativeOwnedSessions(repoRoot,
|
|
2009
|
+
await inspectNativeOwnedSessions({ repoRoot, savedSessions: listSavedSessions(repoRoot), timeoutMs: config.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS, pruneDead: true });
|
|
2111
2010
|
}
|
|
2112
2011
|
const timeout = positiveNumber(args.timeout_ms, config.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
2113
2012
|
const savedTab = target.existingSession?.browser?.tab;
|
|
@@ -2164,7 +2063,9 @@ async function openNativeAttachedContext(
|
|
|
2164
2063
|
savedTab,
|
|
2165
2064
|
};
|
|
2166
2065
|
} catch (error) {
|
|
2167
|
-
if (error instanceof AssistantPluginError
|
|
2066
|
+
if (error instanceof AssistantPluginError
|
|
2067
|
+
&& error.code === 'PLUGIN_BROWSER_NATIVE_TAB_IDENTITY_UNPROVEN'
|
|
2068
|
+
&& savedOwnership === 'user_owned') {
|
|
2168
2069
|
throw error;
|
|
2169
2070
|
}
|
|
2170
2071
|
if (savedOwnership === 'user_owned') {
|
|
@@ -2227,7 +2128,7 @@ async function openNativeAttachedContext(
|
|
|
2227
2128
|
}
|
|
2228
2129
|
|
|
2229
2130
|
if (!discovered?.attachment) {
|
|
2230
|
-
const candidates = savedProduct ? [savedProduct] : (config.nativeBrowserCandidates ?? [
|
|
2131
|
+
const candidates = savedProduct ? [savedProduct] : (config.nativeBrowserCandidates ?? [...DEFAULT_USER_NATIVE_BROWSER_CANDIDATES]);
|
|
2231
2132
|
discovered = await discoverMacOsBrowserAttachment(candidates, Math.min(timeout, MAX_CDP_DISCOVERY_TIMEOUT_MS));
|
|
2232
2133
|
}
|
|
2233
2134
|
if (!discovered.attachment) return { attempts: discovered.attempts };
|
|
@@ -2243,11 +2144,37 @@ async function openNativeAttachedContext(
|
|
|
2243
2144
|
} catch (error) {
|
|
2244
2145
|
if (!nativeBackgroundNavigationRequiresReplacement(error) || effectiveOwnership === 'user_owned' || !obsoleteRef) throw error;
|
|
2245
2146
|
const replacement = await createMacOsBrowserOwnedPage(activeAttachment, target.url, timeout) as unknown as PageLike;
|
|
2246
|
-
const
|
|
2147
|
+
const replacementPage = replacement as unknown as {
|
|
2148
|
+
tabRef?: () => MacOsBrowserTabRef | undefined;
|
|
2149
|
+
creationEvidence?: () => MacOsBrowserCreateTabEvidence | undefined;
|
|
2150
|
+
};
|
|
2151
|
+
const replacementRef = replacementPage.tabRef?.();
|
|
2247
2152
|
if (!replacementRef) {
|
|
2248
2153
|
throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_OWNERSHIP_MISSING', 'Native replacement tab did not return a stable plugin-owned tab reference.', { retryable: true });
|
|
2249
2154
|
}
|
|
2155
|
+
const creationEvidence = replacementPage.creationEvidence?.();
|
|
2156
|
+
const assignmentProvenanceProven = nativeReplacementAssignmentProvenanceProvesTarget(creationEvidence, target.url, replacementRef);
|
|
2157
|
+
if (!assignmentProvenanceProven) {
|
|
2158
|
+
await closeMacOsBrowserOwnedTab(activeAttachment.metadata.product, replacementRef, timeout).catch(() => undefined);
|
|
2159
|
+
throw new AssistantPluginError(
|
|
2160
|
+
'PLUGIN_BROWSER_NATIVE_REPLACEMENT_PROVENANCE_UNPROVEN',
|
|
2161
|
+
'Native replacement tab did not prove exact requested-URL assignment through the stable create-tab provenance contract; preserving the original plugin-owned tab.',
|
|
2162
|
+
{
|
|
2163
|
+
retryable: true,
|
|
2164
|
+
details: {
|
|
2165
|
+
browserProduct: activeAttachment.metadata.product,
|
|
2166
|
+
stableRef: creationEvidence?.refSource === 'structured',
|
|
2167
|
+
provenanceVersion: creationEvidence?.navigation?.provenanceVersion ?? null,
|
|
2168
|
+
assignmentAccepted: creationEvidence?.navigation?.assignmentAccepted === true,
|
|
2169
|
+
requestedUrlMatches: creationEvidence?.navigation
|
|
2170
|
+
? creationEvidence.navigation.requestedUrl === target.url
|
|
2171
|
+
: false,
|
|
2172
|
+
},
|
|
2173
|
+
},
|
|
2174
|
+
);
|
|
2175
|
+
}
|
|
2250
2176
|
try {
|
|
2177
|
+
const previousUrl = target.existingSession?.url ?? candidate.url();
|
|
2251
2178
|
const identity = await settleNativeCreatedPageIdentity(
|
|
2252
2179
|
replacement as unknown as {
|
|
2253
2180
|
waitForLoadState: (state?: string, options?: Record<string, unknown>) => Promise<void>;
|
|
@@ -2255,12 +2182,13 @@ async function openNativeAttachedContext(
|
|
|
2255
2182
|
},
|
|
2256
2183
|
waitUntil(args.wait_until),
|
|
2257
2184
|
timeout,
|
|
2185
|
+
{ requestedUrl: target.url, previousUrl },
|
|
2258
2186
|
);
|
|
2259
|
-
if (!
|
|
2187
|
+
if (!nativeReplacementPostAssignmentUrlMatchesTarget(target.url, identity.url, assignmentProvenanceProven)) {
|
|
2260
2188
|
const diagnostic = nativeReplacementMismatchDiagnostic({
|
|
2261
2189
|
requestedUrl: target.url,
|
|
2262
2190
|
actualUrl: identity.url,
|
|
2263
|
-
previousUrl
|
|
2191
|
+
previousUrl,
|
|
2264
2192
|
obsoleteRef,
|
|
2265
2193
|
replacementRef,
|
|
2266
2194
|
});
|
|
@@ -2544,6 +2472,7 @@ async function openBrowser(
|
|
|
2544
2472
|
}
|
|
2545
2473
|
|
|
2546
2474
|
async function withPage<T>(
|
|
2475
|
+
actionId: string,
|
|
2547
2476
|
repoRoot: string,
|
|
2548
2477
|
config: BrowserPluginConfig,
|
|
2549
2478
|
target: BrowserActionTarget,
|
|
@@ -2552,20 +2481,27 @@ async function withPage<T>(
|
|
|
2552
2481
|
options: { persistSession?: boolean; requireExistingResource?: boolean; pruneStaleSessionMetadata?: boolean } = {},
|
|
2553
2482
|
): Promise<T> {
|
|
2554
2483
|
const retries = Math.min(Math.max(positiveNumber(args.retries, 1), 1), 3);
|
|
2484
|
+
const requireExistingResource = options.requireExistingResource ?? Boolean(target.existingSession);
|
|
2485
|
+
const postDispatchReplaySafe = canReplayBrowserActionAfterDispatch(actionId, requireExistingResource);
|
|
2555
2486
|
let lastError: unknown;
|
|
2556
2487
|
for (let attempt = 1; attempt <= retries; attempt += 1) {
|
|
2557
2488
|
let handle: BrowserOpenHandle | undefined;
|
|
2489
|
+
// Opening a browser resource may itself create or navigate a tab before a
|
|
2490
|
+
// handle is returned. Replay is safe only for an observation action that is
|
|
2491
|
+
// fenced to an already-existing resource. Anything else is potentially
|
|
2492
|
+
// externally mutating from the outset.
|
|
2493
|
+
let actionDispatched = !postDispatchReplaySafe;
|
|
2558
2494
|
try {
|
|
2559
2495
|
if (target.existingSession) assertBrowserSessionAvailable(repoRoot, target.sessionId);
|
|
2560
|
-
const requireExistingResource = options.requireExistingResource ?? Boolean(target.existingSession);
|
|
2561
2496
|
const browserArgs = requireExistingResource
|
|
2562
2497
|
? { ...args, __forge_require_existing_resource: true }
|
|
2563
2498
|
: args;
|
|
2564
2499
|
handle = await openBrowser(repoRoot, config, target, browserArgs);
|
|
2500
|
+
actionDispatched = true;
|
|
2565
2501
|
const result = await run(handle.page, handle.diagnostics, handle.connection);
|
|
2566
2502
|
if (options.persistSession) {
|
|
2567
2503
|
const identity = await readPageIdentity(handle.page, handle.connection, target.url);
|
|
2568
|
-
|
|
2504
|
+
saveBrowserSession(repoRoot, sessionFromPage(target, identity.url, identity.title, handle.connection));
|
|
2569
2505
|
if (result && typeof result === 'object' && !Array.isArray(result) && Object.prototype.hasOwnProperty.call(result, 'url')) {
|
|
2570
2506
|
(result as Record<string, unknown>).url = identity.url;
|
|
2571
2507
|
}
|
|
@@ -2590,11 +2526,21 @@ async function withPage<T>(
|
|
|
2590
2526
|
&& target.existingSession
|
|
2591
2527
|
&& error instanceof AssistantPluginError
|
|
2592
2528
|
&& error.code === 'PLUGIN_BROWSER_SESSION_STATE_LOST') {
|
|
2593
|
-
|
|
2529
|
+
removeBrowserSession(repoRoot, target.sessionId);
|
|
2594
2530
|
}
|
|
2595
2531
|
const message = error instanceof Error ? error.message : String(error);
|
|
2596
|
-
const transient =
|
|
2597
|
-
|
|
2532
|
+
const transient = error instanceof AssistantPluginError
|
|
2533
|
+
? error.retryable
|
|
2534
|
+
: /timeout|timed out|net::|ERR_|Navigation failed|Target closed|Protocol error/i.test(message);
|
|
2535
|
+
if (!transient) throw error;
|
|
2536
|
+
if (actionDispatched && !postDispatchReplaySafe) {
|
|
2537
|
+
throw new AssistantPluginError(
|
|
2538
|
+
'PLUGIN_BROWSER_MUTATION_OUTCOME_UNKNOWN',
|
|
2539
|
+
`Browser action ${actionId} reported a transient failure after dispatch. Forge refused automatic replay because the external mutation outcome is unknown; verify current browser state before retrying.`,
|
|
2540
|
+
{ retryable: false, details: { actionId, requestedRetries: retries } },
|
|
2541
|
+
);
|
|
2542
|
+
}
|
|
2543
|
+
if (attempt >= retries) throw error;
|
|
2598
2544
|
} finally {
|
|
2599
2545
|
if (handle) await handle.close();
|
|
2600
2546
|
}
|
|
@@ -2608,213 +2554,6 @@ function listSavedSessions(repoRoot: string): BrowserSessionState[] {
|
|
|
2608
2554
|
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
2609
2555
|
}
|
|
2610
2556
|
|
|
2611
|
-
function nativeTabInventoryUnsupported(error: unknown): boolean {
|
|
2612
|
-
if (error instanceof AssistantPluginError && error.code === 'BROWSER_AUTOMATION_ACTION_UNSUPPORTED') return true;
|
|
2613
|
-
const message = error instanceof Error ? error.message : String(error ?? '');
|
|
2614
|
-
return /\bBROWSER_AUTOMATION_ACTION_UNSUPPORTED\b/.test(message);
|
|
2615
|
-
}
|
|
2616
|
-
|
|
2617
|
-
function isDiscardableNativeOwnedTabUrl(url: string): boolean {
|
|
2618
|
-
const normalized = url.trim().toLowerCase();
|
|
2619
|
-
return isBlankPage(normalized)
|
|
2620
|
-
|| normalized === 'chrome://new-tab-page/'
|
|
2621
|
-
|| normalized === 'vivaldi://newtab/'
|
|
2622
|
-
|| normalized === 'vivaldi://startpage/';
|
|
2623
|
-
}
|
|
2624
|
-
|
|
2625
|
-
type NativeOwnedSessionInspection = {
|
|
2626
|
-
items: Map<string, BrowserSessionInventoryItem>;
|
|
2627
|
-
liveCount: number;
|
|
2628
|
-
deadCount: number;
|
|
2629
|
-
unverifiedCount: number;
|
|
2630
|
-
prunedCount: number;
|
|
2631
|
-
closedInvalidCount: number;
|
|
2632
|
-
failedCleanupCount: number;
|
|
2633
|
-
};
|
|
2634
|
-
|
|
2635
|
-
async function inspectNativeOwnedSessions(
|
|
2636
|
-
repoRoot: string,
|
|
2637
|
-
config: BrowserPluginConfig,
|
|
2638
|
-
saved: BrowserSessionState[],
|
|
2639
|
-
options: { pruneDead?: boolean } = {},
|
|
2640
|
-
): Promise<NativeOwnedSessionInspection> {
|
|
2641
|
-
const items = new Map<string, BrowserSessionInventoryItem>();
|
|
2642
|
-
const inventories = new Map<MacOsBrowserProduct, Awaited<ReturnType<typeof listMacOsBrowserTabs>> | Error>();
|
|
2643
|
-
const groups = new Map<string, BrowserSessionState[]>();
|
|
2644
|
-
let liveCount = 0;
|
|
2645
|
-
let deadCount = 0;
|
|
2646
|
-
let unverifiedCount = 0;
|
|
2647
|
-
let prunedCount = 0;
|
|
2648
|
-
let closedInvalidCount = 0;
|
|
2649
|
-
let failedCleanupCount = 0;
|
|
2650
|
-
const timeout = config.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
2651
|
-
|
|
2652
|
-
for (const session of saved) {
|
|
2653
|
-
const browser = session.browser;
|
|
2654
|
-
const tab = browser?.tab;
|
|
2655
|
-
const product = browser?.browserProduct;
|
|
2656
|
-
if (browser?.provider !== 'macos-apple-events' || tab?.ownership !== 'plugin_owned' || !product || !tab.windowId || !tab.tabId) continue;
|
|
2657
|
-
const key = `${product}:${tab.windowId}:${tab.tabId}`;
|
|
2658
|
-
const group = groups.get(key) ?? [];
|
|
2659
|
-
group.push(session);
|
|
2660
|
-
groups.set(key, group);
|
|
2661
|
-
}
|
|
2662
|
-
|
|
2663
|
-
for (const group of groups.values()) {
|
|
2664
|
-
const canonical = group.reduce((latest, session) => session.updatedAt > latest.updatedAt ? session : latest);
|
|
2665
|
-
const browser = canonical.browser!;
|
|
2666
|
-
const tab = browser.tab!;
|
|
2667
|
-
const product = browser.browserProduct!;
|
|
2668
|
-
let inventory = inventories.get(product);
|
|
2669
|
-
if (!inventory) {
|
|
2670
|
-
try {
|
|
2671
|
-
inventory = await listMacOsBrowserTabs(product, timeout);
|
|
2672
|
-
} catch (error) {
|
|
2673
|
-
inventory = error instanceof Error ? error : new Error(String(error));
|
|
2674
|
-
}
|
|
2675
|
-
inventories.set(product, inventory);
|
|
2676
|
-
}
|
|
2677
|
-
if (inventory instanceof Error) {
|
|
2678
|
-
if (!nativeTabInventoryUnsupported(inventory)) {
|
|
2679
|
-
for (const session of group) items.set(session.sessionId, { session, liveness: 'unverified', evidence: 'native_inventory_unavailable', cleanupError: inventory.message });
|
|
2680
|
-
unverifiedCount += group.length;
|
|
2681
|
-
continue;
|
|
2682
|
-
}
|
|
2683
|
-
|
|
2684
|
-
const ref = { windowId: tab.windowId!, tabId: tab.tabId! };
|
|
2685
|
-
let metadata: Awaited<ReturnType<typeof readMacOsBrowserOwnedTabMetadata>>;
|
|
2686
|
-
try {
|
|
2687
|
-
metadata = await readMacOsBrowserOwnedTabMetadata(product, ref, timeout);
|
|
2688
|
-
} catch (metadataError) {
|
|
2689
|
-
if (macOsBrowserPageHandleStale(metadataError)) {
|
|
2690
|
-
const pruned = options.pruneDead === true;
|
|
2691
|
-
if (pruned) {
|
|
2692
|
-
for (const session of group) removeSession(repoRoot, session.sessionId);
|
|
2693
|
-
prunedCount += group.length;
|
|
2694
|
-
}
|
|
2695
|
-
for (const session of group) items.set(session.sessionId, { session, liveness: 'dead', evidence: 'native_tab_missing', pruned });
|
|
2696
|
-
deadCount += group.length;
|
|
2697
|
-
} else {
|
|
2698
|
-
const cleanupError = metadataError instanceof Error ? metadataError.message : String(metadataError);
|
|
2699
|
-
for (const session of group) items.set(session.sessionId, { session, liveness: 'unverified', evidence: 'native_inventory_unavailable', cleanupError });
|
|
2700
|
-
unverifiedCount += group.length;
|
|
2701
|
-
}
|
|
2702
|
-
continue;
|
|
2703
|
-
}
|
|
2704
|
-
|
|
2705
|
-
if (isDiscardableNativeOwnedTabUrl(metadata.url)) {
|
|
2706
|
-
deadCount += group.length;
|
|
2707
|
-
if (options.pruneDead === true) {
|
|
2708
|
-
try {
|
|
2709
|
-
await closeMacOsBrowserOwnedTab(product, ref, timeout);
|
|
2710
|
-
for (const session of group) removeSession(repoRoot, session.sessionId);
|
|
2711
|
-
prunedCount += group.length;
|
|
2712
|
-
closedInvalidCount += 1;
|
|
2713
|
-
for (const session of group) items.set(session.sessionId, { session, liveness: 'dead', evidence: 'native_tab_invalid_closed', pruned: true });
|
|
2714
|
-
} catch (error) {
|
|
2715
|
-
failedCleanupCount += 1;
|
|
2716
|
-
const cleanupError = error instanceof Error ? error.message : String(error);
|
|
2717
|
-
for (const session of group) items.set(session.sessionId, { session, liveness: 'dead', evidence: 'native_tab_invalid_close_failed', cleanupError });
|
|
2718
|
-
}
|
|
2719
|
-
} else {
|
|
2720
|
-
for (const session of group) items.set(session.sessionId, { session, liveness: 'dead', evidence: 'native_tab_invalid' });
|
|
2721
|
-
}
|
|
2722
|
-
} else {
|
|
2723
|
-
for (const session of group) items.set(session.sessionId, { session, liveness: 'live', evidence: 'native_tab_live' });
|
|
2724
|
-
liveCount += group.length;
|
|
2725
|
-
}
|
|
2726
|
-
continue;
|
|
2727
|
-
}
|
|
2728
|
-
|
|
2729
|
-
const live = inventory.tabs.find((entry) => entry.windowId === tab.windowId && entry.tabId === tab.tabId);
|
|
2730
|
-
if (!live) {
|
|
2731
|
-
const pruned = options.pruneDead === true;
|
|
2732
|
-
if (pruned) {
|
|
2733
|
-
for (const session of group) removeSession(repoRoot, session.sessionId);
|
|
2734
|
-
prunedCount += group.length;
|
|
2735
|
-
}
|
|
2736
|
-
for (const session of group) items.set(session.sessionId, { session, liveness: 'dead', evidence: 'native_tab_missing', pruned });
|
|
2737
|
-
deadCount += group.length;
|
|
2738
|
-
continue;
|
|
2739
|
-
}
|
|
2740
|
-
|
|
2741
|
-
if (isDiscardableNativeOwnedTabUrl(live.url)) {
|
|
2742
|
-
deadCount += group.length;
|
|
2743
|
-
if (options.pruneDead === true) {
|
|
2744
|
-
try {
|
|
2745
|
-
await closeMacOsBrowserOwnedTab(product, { windowId: tab.windowId!, tabId: tab.tabId! }, timeout);
|
|
2746
|
-
for (const session of group) removeSession(repoRoot, session.sessionId);
|
|
2747
|
-
prunedCount += group.length;
|
|
2748
|
-
closedInvalidCount += 1;
|
|
2749
|
-
for (const session of group) items.set(session.sessionId, { session, liveness: 'dead', evidence: 'native_tab_invalid_closed', pruned: true });
|
|
2750
|
-
} catch (error) {
|
|
2751
|
-
failedCleanupCount += 1;
|
|
2752
|
-
const cleanupError = error instanceof Error ? error.message : String(error);
|
|
2753
|
-
for (const session of group) items.set(session.sessionId, { session, liveness: 'dead', evidence: 'native_tab_invalid_close_failed', cleanupError });
|
|
2754
|
-
}
|
|
2755
|
-
} else {
|
|
2756
|
-
for (const session of group) items.set(session.sessionId, { session, liveness: 'dead', evidence: 'native_tab_invalid' });
|
|
2757
|
-
}
|
|
2758
|
-
continue;
|
|
2759
|
-
}
|
|
2760
|
-
|
|
2761
|
-
for (const session of group) items.set(session.sessionId, { session, liveness: 'live', evidence: 'native_tab_live' });
|
|
2762
|
-
liveCount += group.length;
|
|
2763
|
-
}
|
|
2764
|
-
|
|
2765
|
-
return { items, liveCount, deadCount, unverifiedCount, prunedCount, closedInvalidCount, failedCleanupCount };
|
|
2766
|
-
}
|
|
2767
|
-
|
|
2768
|
-
async function closeTrackedNativeOwnedSession(
|
|
2769
|
-
session: BrowserSessionState,
|
|
2770
|
-
config: BrowserPluginConfig,
|
|
2771
|
-
): Promise<{ resourceClosed: boolean; resourceAlreadyMissing: boolean }> {
|
|
2772
|
-
const browser = session.browser;
|
|
2773
|
-
const tab = browser?.tab;
|
|
2774
|
-
if (browser?.provider !== 'macos-apple-events' || tab?.ownership !== 'plugin_owned') {
|
|
2775
|
-
return { resourceClosed: false, resourceAlreadyMissing: false };
|
|
2776
|
-
}
|
|
2777
|
-
if (!browser.browserProduct || !tab.windowId || !tab.tabId) {
|
|
2778
|
-
throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_OWNERSHIP_MISSING', 'Tracked plugin-owned native tab is missing stable browser identity; retaining session metadata instead of orphaning the tab.', {
|
|
2779
|
-
retryable: false, details: { sessionId: session.sessionId, browserProduct: browser.browserProduct, windowId: tab.windowId, tabId: tab.tabId },
|
|
2780
|
-
});
|
|
2781
|
-
}
|
|
2782
|
-
const timeout = config.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
2783
|
-
const ref = { windowId: tab.windowId, tabId: tab.tabId };
|
|
2784
|
-
let inventory: Awaited<ReturnType<typeof listMacOsBrowserTabs>> | undefined;
|
|
2785
|
-
try {
|
|
2786
|
-
inventory = await listMacOsBrowserTabs(browser.browserProduct, timeout);
|
|
2787
|
-
} catch (error) {
|
|
2788
|
-
if (!nativeTabInventoryUnsupported(error)) throw error;
|
|
2789
|
-
try {
|
|
2790
|
-
await readMacOsBrowserOwnedTabMetadata(browser.browserProduct, ref, timeout);
|
|
2791
|
-
} catch (metadataError) {
|
|
2792
|
-
if (macOsBrowserPageHandleStale(metadataError)) return { resourceClosed: false, resourceAlreadyMissing: true };
|
|
2793
|
-
throw metadataError;
|
|
2794
|
-
}
|
|
2795
|
-
}
|
|
2796
|
-
if (inventory && !inventory.tabs.some((entry) => entry.windowId === tab.windowId && entry.tabId === tab.tabId)) {
|
|
2797
|
-
return { resourceClosed: false, resourceAlreadyMissing: true };
|
|
2798
|
-
}
|
|
2799
|
-
await closeMacOsBrowserOwnedTab(browser.browserProduct, ref, timeout);
|
|
2800
|
-
return { resourceClosed: true, resourceAlreadyMissing: false };
|
|
2801
|
-
}
|
|
2802
|
-
|
|
2803
|
-
function nativeOwnedAliasSessionIds(repoRoot: string, session: BrowserSessionState): string[] {
|
|
2804
|
-
const browser = session.browser;
|
|
2805
|
-
const tab = browser?.tab;
|
|
2806
|
-
if (browser?.provider !== 'macos-apple-events' || tab?.ownership !== 'plugin_owned' || !browser.browserProduct || !tab.windowId || !tab.tabId) {
|
|
2807
|
-
return [session.sessionId];
|
|
2808
|
-
}
|
|
2809
|
-
return listSavedSessions(repoRoot)
|
|
2810
|
-
.filter((candidate) => candidate.browser?.provider === 'macos-apple-events'
|
|
2811
|
-
&& candidate.browser.browserProduct === browser.browserProduct
|
|
2812
|
-
&& candidate.browser.tab?.ownership === 'plugin_owned'
|
|
2813
|
-
&& candidate.browser.tab.windowId === tab.windowId
|
|
2814
|
-
&& candidate.browser.tab.tabId === tab.tabId)
|
|
2815
|
-
.map((candidate) => candidate.sessionId);
|
|
2816
|
-
}
|
|
2817
|
-
|
|
2818
2557
|
async function inspectSavedSessions(
|
|
2819
2558
|
repoRoot: string,
|
|
2820
2559
|
config: BrowserPluginConfig,
|
|
@@ -2826,7 +2565,7 @@ async function inspectSavedSessions(
|
|
|
2826
2565
|
let unverifiedSessionCount = 0;
|
|
2827
2566
|
let deadManagedSessionCount = 0;
|
|
2828
2567
|
let prunedSessionCount = 0;
|
|
2829
|
-
const native = await inspectNativeOwnedSessions(repoRoot,
|
|
2568
|
+
const native = await inspectNativeOwnedSessions({ repoRoot, savedSessions: saved, timeoutMs: config.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS, pruneDead: options.pruneDead });
|
|
2830
2569
|
let liveNativeSessionCount = native.liveCount;
|
|
2831
2570
|
let deadNativeSessionCount = native.deadCount;
|
|
2832
2571
|
let closedInvalidNativeSessionCount = native.closedInvalidCount;
|
|
@@ -2875,7 +2614,7 @@ async function inspectSavedSessions(
|
|
|
2875
2614
|
const pruned = options.pruneDead === true;
|
|
2876
2615
|
if (pruned) {
|
|
2877
2616
|
state.pagesBySession.delete(session.sessionId);
|
|
2878
|
-
|
|
2617
|
+
removeBrowserSession(repoRoot, session.sessionId);
|
|
2879
2618
|
prunedSessionCount += 1;
|
|
2880
2619
|
}
|
|
2881
2620
|
sessions.push({ session, liveness: 'dead', evidence: 'runtime_bound_page_missing', pruned });
|
|
@@ -3328,7 +3067,7 @@ async function finalizeInteractiveAction(
|
|
|
3328
3067
|
const identity = await readPageIdentity(page, connection, target.url);
|
|
3329
3068
|
const title = identity.title;
|
|
3330
3069
|
const pageUrl = identity.url;
|
|
3331
|
-
const session =
|
|
3070
|
+
const session = saveBrowserSession(repoRoot, sessionFromPage(target, pageUrl, title, connection));
|
|
3332
3071
|
let screenshot: BrowserActionScreenshot | undefined;
|
|
3333
3072
|
const silentNativeEvidence = connection.provider === 'macos-apple-events';
|
|
3334
3073
|
if (!silentNativeEvidence) {
|
|
@@ -3350,598 +3089,6 @@ async function finalizeInteractiveAction(
|
|
|
3350
3089
|
};
|
|
3351
3090
|
}
|
|
3352
3091
|
|
|
3353
|
-
function permissions(ready: boolean): AssistantPluginPermissionScope[] {
|
|
3354
|
-
return [
|
|
3355
|
-
{
|
|
3356
|
-
scope: 'browser.read',
|
|
3357
|
-
mode: 'read',
|
|
3358
|
-
description: 'Open pages, extract text, and capture screenshots.',
|
|
3359
|
-
granted: ready,
|
|
3360
|
-
required: true,
|
|
3361
|
-
},
|
|
3362
|
-
{
|
|
3363
|
-
scope: 'browser.interact',
|
|
3364
|
-
mode: 'write',
|
|
3365
|
-
description: 'Click, type, press keys, and wait on allowed browser pages after explicit authorization.',
|
|
3366
|
-
granted: ready,
|
|
3367
|
-
required: true,
|
|
3368
|
-
},
|
|
3369
|
-
{
|
|
3370
|
-
scope: 'browser.profile',
|
|
3371
|
-
mode: 'write',
|
|
3372
|
-
description: 'Persist the dedicated local browser profile, session metadata, and screenshots.',
|
|
3373
|
-
granted: ready,
|
|
3374
|
-
required: true,
|
|
3375
|
-
},
|
|
3376
|
-
];
|
|
3377
|
-
}
|
|
3378
|
-
|
|
3379
|
-
function capabilities(): AssistantPluginCapability[] {
|
|
3380
|
-
return [
|
|
3381
|
-
{
|
|
3382
|
-
capabilityId: 'browser-session',
|
|
3383
|
-
title: 'Browser Sessions',
|
|
3384
|
-
description: 'Create, list, reuse, and close persistent browser sessions.',
|
|
3385
|
-
scopes: ['browser.read', 'browser.profile'],
|
|
3386
|
-
actions: ['create_session', 'list_sessions', 'reconcile_sessions', 'close_session', 'close_page', 'clear_session'],
|
|
3387
|
-
},
|
|
3388
|
-
{
|
|
3389
|
-
capabilityId: 'browser-human-handoff',
|
|
3390
|
-
title: 'Browser Human Handoff',
|
|
3391
|
-
description: 'Present the persistent browser in the foreground, keep it open for manual verification, and resume safely.',
|
|
3392
|
-
scopes: ['browser.read', 'browser.profile'],
|
|
3393
|
-
actions: ['request_human_handoff', 'get_handoff_status', 'resolve_handoff'],
|
|
3394
|
-
},
|
|
3395
|
-
{
|
|
3396
|
-
capabilityId: 'browser-readonly',
|
|
3397
|
-
title: 'Read-only Browser',
|
|
3398
|
-
description: 'Navigate allowed pages, extract DOM/text, capture screenshots, and collect diagnostics.',
|
|
3399
|
-
scopes: ['browser.read', 'browser.profile'],
|
|
3400
|
-
actions: [
|
|
3401
|
-
'open_page', 'navigate', 'reload', 'go_back', 'wait_for_load_state',
|
|
3402
|
-
'get_text', 'get_html', 'query_selector', 'query_all', 'get_attribute', 'list_frames', 'verify_state',
|
|
3403
|
-
'screenshot', 'extract_links', 'extract_tables', 'extract_forms', 'snapshot_interactive',
|
|
3404
|
-
'get_console_errors', 'get_failed_requests',
|
|
3405
|
-
],
|
|
3406
|
-
},
|
|
3407
|
-
{
|
|
3408
|
-
capabilityId: 'browser-interaction',
|
|
3409
|
-
title: 'Browser Interaction',
|
|
3410
|
-
description: 'Perform explicit form and pointer interactions on HTTP(S) pages through the persistent Playwright profile.',
|
|
3411
|
-
scopes: ['browser.interact', 'browser.profile'],
|
|
3412
|
-
actions: [
|
|
3413
|
-
'activate_page', 'click', 'click_text', 'double_click', 'hover', 'focus', 'type', 'fill', 'select_option',
|
|
3414
|
-
'check', 'uncheck', 'press', 'keyboard_shortcut', 'trusted_input', 'dispatch_event', 'wait_for_selector', 'attach_local_file', 'await_file_transfer',
|
|
3415
|
-
],
|
|
3416
|
-
},
|
|
3417
|
-
];
|
|
3418
|
-
}
|
|
3419
|
-
|
|
3420
|
-
function sessionTargetSchema(extra: Record<string, unknown> = {}, required: string[] = []): Record<string, unknown> {
|
|
3421
|
-
return {
|
|
3422
|
-
type: 'object',
|
|
3423
|
-
properties: {
|
|
3424
|
-
session_id: { type: 'string' },
|
|
3425
|
-
url: { type: 'string' },
|
|
3426
|
-
wait_until: { type: 'string', enum: ['load', 'domcontentloaded', 'networkidle'] },
|
|
3427
|
-
timeout_ms: { type: 'number' },
|
|
3428
|
-
retries: { type: 'number' },
|
|
3429
|
-
browser_mode: { type: 'string', enum: ['attach_preferred', 'managed_persistent', 'isolated'] },
|
|
3430
|
-
cdp_attach_fallback: { type: 'string', enum: ['managed_persistent', 'fail_closed'] },
|
|
3431
|
-
native_attach_mode: { type: 'string', enum: ['auto', 'disabled'] },
|
|
3432
|
-
native_browser_candidates: { type: 'array', items: { type: 'string', enum: ['vivaldi', 'chrome'] }, maxItems: 2 },
|
|
3433
|
-
...extra,
|
|
3434
|
-
},
|
|
3435
|
-
...(required.length > 0 ? { required } : {}),
|
|
3436
|
-
additionalProperties: false,
|
|
3437
|
-
};
|
|
3438
|
-
}
|
|
3439
|
-
|
|
3440
|
-
const frameScopeProperties = {
|
|
3441
|
-
frame_url: { type: 'string' },
|
|
3442
|
-
frame_name: { type: 'string' },
|
|
3443
|
-
};
|
|
3444
|
-
|
|
3445
|
-
function interactSchema(extra: Record<string, unknown>, required: string[]): Record<string, unknown> {
|
|
3446
|
-
return sessionTargetSchema({
|
|
3447
|
-
post_action_wait_ms: { type: 'number' },
|
|
3448
|
-
...extra,
|
|
3449
|
-
}, required);
|
|
3450
|
-
}
|
|
3451
|
-
|
|
3452
|
-
function actions(): AssistantPluginActionDescriptor[] {
|
|
3453
|
-
const readRemote = [
|
|
3454
|
-
{ resource: 'remote' as const, mode: 'read' as const },
|
|
3455
|
-
{ resource: 'repo-state' as const, mode: 'write' as const },
|
|
3456
|
-
];
|
|
3457
|
-
const writeRemote = [
|
|
3458
|
-
{ resource: 'remote' as const, mode: 'exclusive' as const },
|
|
3459
|
-
{ resource: 'repo-state' as const, mode: 'write' as const },
|
|
3460
|
-
];
|
|
3461
|
-
const descriptors: AssistantPluginActionDescriptor[] = [
|
|
3462
|
-
{
|
|
3463
|
-
actionId: 'configure',
|
|
3464
|
-
title: 'Configure browser plugin',
|
|
3465
|
-
description: 'Enable or update the local browser plugin configuration.',
|
|
3466
|
-
readOnly: false,
|
|
3467
|
-
risk: 'workspace_write',
|
|
3468
|
-
confirmation: 'authorization',
|
|
3469
|
-
defaultTimeoutMs: 30_000,
|
|
3470
|
-
cancellable: true,
|
|
3471
|
-
idempotent: true,
|
|
3472
|
-
scopes: ['browser.profile'],
|
|
3473
|
-
resourceClaims: [{ resource: 'repo-state', mode: 'write' }],
|
|
3474
|
-
argumentsSchema: {
|
|
3475
|
-
type: 'object',
|
|
3476
|
-
properties: {
|
|
3477
|
-
enabled: { type: 'boolean' },
|
|
3478
|
-
browser_mode: { type: 'string', enum: ['attach_preferred', 'managed_persistent', 'isolated'] },
|
|
3479
|
-
profile_mode: { type: 'string', enum: ['repo_local', 'custom'] },
|
|
3480
|
-
profile_dir: { type: 'string' },
|
|
3481
|
-
profile_directory: { type: 'string' },
|
|
3482
|
-
clear_profile_dir: { type: 'boolean' },
|
|
3483
|
-
clear_profile_directory: { type: 'boolean' },
|
|
3484
|
-
browser_channel: { type: 'string', enum: ['chromium', 'chrome', 'chrome-beta', 'chrome-dev', 'chrome-canary'] },
|
|
3485
|
-
clear_browser_channel: { type: 'boolean' },
|
|
3486
|
-
browser_executable_path: { type: 'string' },
|
|
3487
|
-
clear_browser_executable_path: { type: 'boolean' },
|
|
3488
|
-
cdp_endpoint: { type: 'string' },
|
|
3489
|
-
clear_cdp_endpoint: { type: 'boolean' },
|
|
3490
|
-
cdp_endpoint_candidates: { type: 'array', items: { type: 'string' }, maxItems: MAX_CDP_ENDPOINT_CANDIDATES },
|
|
3491
|
-
clear_cdp_endpoint_candidates: { type: 'boolean' },
|
|
3492
|
-
cdp_discovery_timeout_ms: { type: 'number' },
|
|
3493
|
-
cdp_attach_fallback: { type: 'string', enum: ['managed_persistent', 'fail_closed'] },
|
|
3494
|
-
native_attach_mode: { type: 'string', enum: ['auto', 'disabled'] },
|
|
3495
|
-
native_browser_candidates: { type: 'array', items: { type: 'string', enum: ['vivaldi', 'chrome'] }, maxItems: 2 },
|
|
3496
|
-
default_timeout_ms: { type: 'number' },
|
|
3497
|
-
},
|
|
3498
|
-
additionalProperties: false,
|
|
3499
|
-
},
|
|
3500
|
-
},
|
|
3501
|
-
{
|
|
3502
|
-
actionId: 'create_session',
|
|
3503
|
-
title: 'Create browser session',
|
|
3504
|
-
description: 'Open an HTTP(S) URL or explicitly adopt the matching frontmost native tab, then persist a reusable session id.',
|
|
3505
|
-
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
3506
|
-
scopes: ['browser.read', 'browser.profile'], resourceClaims: [{ resource: 'repo-state', mode: 'write' }, ...readRemote],
|
|
3507
|
-
argumentsSchema: sessionTargetSchema({
|
|
3508
|
-
extract_text: { type: 'boolean' },
|
|
3509
|
-
max_chars: { type: 'number' },
|
|
3510
|
-
native_browser_product: { type: 'string', enum: ['chrome', 'vivaldi'] },
|
|
3511
|
-
native_window_id: { type: 'string' },
|
|
3512
|
-
native_tab_id: { type: 'string' },
|
|
3513
|
-
native_active_tab: { type: 'boolean' },
|
|
3514
|
-
}, ['url']),
|
|
3515
|
-
},
|
|
3516
|
-
{
|
|
3517
|
-
actionId: 'list_sessions',
|
|
3518
|
-
title: 'List browser sessions',
|
|
3519
|
-
description: 'List saved browser session metadata without secrets or cookies.',
|
|
3520
|
-
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 10_000, cancellable: true, idempotent: true,
|
|
3521
|
-
scopes: ['browser.read', 'browser.profile'], resourceClaims: [{ resource: 'repo-state', mode: 'read' }],
|
|
3522
|
-
argumentsSchema: {
|
|
3523
|
-
type: 'object',
|
|
3524
|
-
properties: {
|
|
3525
|
-
limit: { type: 'number', minimum: 1, maximum: MAX_BROWSER_SESSION_LIST_LIMIT, default: DEFAULT_BROWSER_SESSION_LIST_LIMIT },
|
|
3526
|
-
cursor: { type: 'string' },
|
|
3527
|
-
},
|
|
3528
|
-
additionalProperties: false,
|
|
3529
|
-
},
|
|
3530
|
-
},
|
|
3531
|
-
{
|
|
3532
|
-
actionId: 'reconcile_sessions',
|
|
3533
|
-
title: 'Reconcile browser sessions',
|
|
3534
|
-
description: 'Prune only saved managed-session metadata whose exact Runtime-bound page is positively proven gone. Never removes profiles, cookies, or unverified native sessions.',
|
|
3535
|
-
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 15_000, cancellable: true, idempotent: true,
|
|
3536
|
-
scopes: ['browser.read', 'browser.profile'], resourceClaims: [{ resource: 'repo-state', mode: 'write' }],
|
|
3537
|
-
argumentsSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
3538
|
-
},
|
|
3539
|
-
{
|
|
3540
|
-
actionId: 'close_session',
|
|
3541
|
-
title: 'Close browser session',
|
|
3542
|
-
description: 'Remove one saved session metadata record.',
|
|
3543
|
-
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 15_000, cancellable: true, idempotent: true,
|
|
3544
|
-
scopes: ['browser.read', 'browser.profile'], resourceClaims: [{ resource: 'repo-state', mode: 'write' }],
|
|
3545
|
-
argumentsSchema: { type: 'object', properties: { session_id: { type: 'string' } }, required: ['session_id'], additionalProperties: false },
|
|
3546
|
-
},
|
|
3547
|
-
{
|
|
3548
|
-
actionId: 'clear_session',
|
|
3549
|
-
title: 'Clear all browser sessions',
|
|
3550
|
-
description: 'Remove all saved session metadata while keeping the profile.',
|
|
3551
|
-
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 15_000, cancellable: true, idempotent: true,
|
|
3552
|
-
scopes: ['browser.profile'], resourceClaims: [{ resource: 'repo-state', mode: 'write' }],
|
|
3553
|
-
argumentsSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
3554
|
-
},
|
|
3555
|
-
{
|
|
3556
|
-
actionId: 'request_human_handoff',
|
|
3557
|
-
title: 'Request human browser handoff',
|
|
3558
|
-
description: 'Keep a saved browser session open for CAPTCHA, login, two-factor authentication, or another manual step.',
|
|
3559
|
-
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 30_000, cancellable: true, idempotent: false,
|
|
3560
|
-
scopes: ['browser.read', 'browser.profile'], resourceClaims: readRemote,
|
|
3561
|
-
argumentsSchema: sessionTargetSchema({
|
|
3562
|
-
reason: { type: 'string', enum: ['captcha', 'login', 'two_factor', 'manual_review', 'sensitive_confirmation'] },
|
|
3563
|
-
instructions: { type: 'string' },
|
|
3564
|
-
handoff_timeout_ms: { type: 'number' },
|
|
3565
|
-
}, ['session_id']),
|
|
3566
|
-
},
|
|
3567
|
-
{
|
|
3568
|
-
actionId: 'get_handoff_status',
|
|
3569
|
-
title: 'Get browser handoff status',
|
|
3570
|
-
description: 'Read durable browser handoff status and reconcile a stale or crashed host.',
|
|
3571
|
-
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 10_000, cancellable: true, idempotent: true,
|
|
3572
|
-
scopes: ['browser.read', 'browser.profile'], resourceClaims: [{ resource: 'repo-state', mode: 'read' }],
|
|
3573
|
-
argumentsSchema: { type: 'object', properties: { interaction_id: { type: 'string' } }, required: ['interaction_id'], additionalProperties: false },
|
|
3574
|
-
},
|
|
3575
|
-
{
|
|
3576
|
-
actionId: 'resolve_handoff',
|
|
3577
|
-
title: 'Resolve browser handoff',
|
|
3578
|
-
description: 'Resume or cancel a foreground browser handoff.',
|
|
3579
|
-
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 15_000, cancellable: true, idempotent: true,
|
|
3580
|
-
scopes: ['browser.read', 'browser.profile'], resourceClaims: [{ resource: 'repo-state', mode: 'write' }],
|
|
3581
|
-
argumentsSchema: {
|
|
3582
|
-
type: 'object',
|
|
3583
|
-
properties: {
|
|
3584
|
-
interaction_id: { type: 'string' },
|
|
3585
|
-
resolution: { type: 'string', enum: ['resume', 'cancel'] },
|
|
3586
|
-
},
|
|
3587
|
-
required: ['interaction_id', 'resolution'],
|
|
3588
|
-
additionalProperties: false,
|
|
3589
|
-
},
|
|
3590
|
-
},
|
|
3591
|
-
{
|
|
3592
|
-
actionId: 'open_page',
|
|
3593
|
-
title: 'Open page',
|
|
3594
|
-
description: 'Open an HTTP(S) URL with the persistent profile and save a lightweight session.',
|
|
3595
|
-
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
3596
|
-
scopes: ['browser.read'], resourceClaims: readRemote,
|
|
3597
|
-
argumentsSchema: sessionTargetSchema({ extract_text: { type: 'boolean' }, max_chars: { type: 'number' } }, ['url']),
|
|
3598
|
-
},
|
|
3599
|
-
{
|
|
3600
|
-
actionId: 'navigate',
|
|
3601
|
-
title: 'Navigate',
|
|
3602
|
-
description: 'Navigate an existing session or open a new page to an HTTP(S) URL.',
|
|
3603
|
-
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
3604
|
-
scopes: ['browser.read', 'browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
3605
|
-
argumentsSchema: sessionTargetSchema({}, ['url']),
|
|
3606
|
-
},
|
|
3607
|
-
{
|
|
3608
|
-
actionId: 'reload',
|
|
3609
|
-
title: 'Reload page',
|
|
3610
|
-
description: 'Reload the current page for a session.',
|
|
3611
|
-
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
3612
|
-
scopes: ['browser.read', 'browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
3613
|
-
argumentsSchema: sessionTargetSchema({}, ['session_id']),
|
|
3614
|
-
},
|
|
3615
|
-
{
|
|
3616
|
-
actionId: 'go_back',
|
|
3617
|
-
title: 'Go back',
|
|
3618
|
-
description: 'Navigate back in history for a session.',
|
|
3619
|
-
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
3620
|
-
scopes: ['browser.read', 'browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
3621
|
-
argumentsSchema: sessionTargetSchema({}, ['session_id']),
|
|
3622
|
-
},
|
|
3623
|
-
{
|
|
3624
|
-
actionId: 'wait_for_load_state',
|
|
3625
|
-
title: 'Wait for load state',
|
|
3626
|
-
description: 'Wait for load/domcontentloaded/networkidle on a session page.',
|
|
3627
|
-
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
3628
|
-
scopes: ['browser.read'], resourceClaims: readRemote,
|
|
3629
|
-
argumentsSchema: sessionTargetSchema({ state: { type: 'string', enum: ['load', 'domcontentloaded', 'networkidle'] } }),
|
|
3630
|
-
},
|
|
3631
|
-
{
|
|
3632
|
-
actionId: 'get_text',
|
|
3633
|
-
title: 'Get text',
|
|
3634
|
-
description: 'Extract text from a URL or saved browser session.',
|
|
3635
|
-
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
3636
|
-
scopes: ['browser.read'], resourceClaims: [{ resource: 'remote', mode: 'read' }],
|
|
3637
|
-
argumentsSchema: sessionTargetSchema({ selector: { type: 'string' }, max_chars: { type: 'number' }, ...frameScopeProperties }),
|
|
3638
|
-
},
|
|
3639
|
-
{
|
|
3640
|
-
actionId: 'get_html',
|
|
3641
|
-
title: 'Get HTML',
|
|
3642
|
-
description: 'Extract HTML for the page or a selector (bounded).',
|
|
3643
|
-
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
3644
|
-
scopes: ['browser.read'], resourceClaims: [{ resource: 'remote', mode: 'read' }],
|
|
3645
|
-
argumentsSchema: sessionTargetSchema({ selector: { type: 'string' }, max_chars: { type: 'number' }, ...frameScopeProperties }),
|
|
3646
|
-
},
|
|
3647
|
-
{
|
|
3648
|
-
actionId: 'query_selector',
|
|
3649
|
-
title: 'Query selector',
|
|
3650
|
-
description: 'Return the first matching element summary with a stable selector hint.',
|
|
3651
|
-
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
3652
|
-
scopes: ['browser.read'], resourceClaims: [{ resource: 'remote', mode: 'read' }],
|
|
3653
|
-
argumentsSchema: sessionTargetSchema({ selector: { type: 'string' }, ...frameScopeProperties }, ['selector']),
|
|
3654
|
-
},
|
|
3655
|
-
{
|
|
3656
|
-
actionId: 'query_all',
|
|
3657
|
-
title: 'Query all selectors',
|
|
3658
|
-
description: 'Return matching element summaries (bounded).',
|
|
3659
|
-
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
3660
|
-
scopes: ['browser.read'], resourceClaims: [{ resource: 'remote', mode: 'read' }],
|
|
3661
|
-
argumentsSchema: sessionTargetSchema({ selector: { type: 'string' }, limit: { type: 'number' }, ...frameScopeProperties }, ['selector']),
|
|
3662
|
-
},
|
|
3663
|
-
{
|
|
3664
|
-
actionId: 'get_attribute',
|
|
3665
|
-
title: 'Get attribute',
|
|
3666
|
-
description: 'Read one attribute from a selector.',
|
|
3667
|
-
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
3668
|
-
scopes: ['browser.read'], resourceClaims: [{ resource: 'remote', mode: 'read' }],
|
|
3669
|
-
argumentsSchema: sessionTargetSchema({ selector: { type: 'string' }, attribute: { type: 'string' }, ...frameScopeProperties }, ['selector', 'attribute']),
|
|
3670
|
-
},
|
|
3671
|
-
{
|
|
3672
|
-
actionId: 'list_frames',
|
|
3673
|
-
title: 'List page frames',
|
|
3674
|
-
description: 'List bounded Playwright/CDP frame identities for explicit frame-scoped DOM observation.',
|
|
3675
|
-
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
3676
|
-
scopes: ['browser.read'], resourceClaims: [{ resource: 'remote', mode: 'read' }],
|
|
3677
|
-
argumentsSchema: sessionTargetSchema({ limit: { type: 'number', minimum: 1, maximum: 100 } }),
|
|
3678
|
-
},
|
|
3679
|
-
{
|
|
3680
|
-
actionId: 'verify_state',
|
|
3681
|
-
title: 'Verify browser state',
|
|
3682
|
-
description: 'Observe URL, selector existence/visibility, and bounded text criteria in one read-only call for post-action verification.',
|
|
3683
|
-
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
3684
|
-
scopes: ['browser.read'], resourceClaims: [{ resource: 'remote', mode: 'read' }],
|
|
3685
|
-
argumentsSchema: sessionTargetSchema({
|
|
3686
|
-
expected_url: { type: 'string' },
|
|
3687
|
-
url_contains: { type: 'string' },
|
|
3688
|
-
selector: { type: 'string' },
|
|
3689
|
-
require_visible: { type: 'boolean' },
|
|
3690
|
-
text_contains: { type: 'string', maxLength: 10000 },
|
|
3691
|
-
max_chars: { type: 'number', minimum: 1, maximum: 100000 },
|
|
3692
|
-
...frameScopeProperties,
|
|
3693
|
-
}, ['session_id']),
|
|
3694
|
-
},
|
|
3695
|
-
{
|
|
3696
|
-
actionId: 'screenshot',
|
|
3697
|
-
title: 'Screenshot',
|
|
3698
|
-
description: 'Capture a page, full-page, or element screenshot under browser artifact storage.',
|
|
3699
|
-
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
3700
|
-
scopes: ['browser.read'], resourceClaims: readRemote,
|
|
3701
|
-
argumentsSchema: sessionTargetSchema({ full_page: { type: 'boolean' }, selector: { type: 'string' } }),
|
|
3702
|
-
},
|
|
3703
|
-
{
|
|
3704
|
-
actionId: 'extract_links',
|
|
3705
|
-
title: 'Extract links',
|
|
3706
|
-
description: 'Extract anchors with href/text from the page.',
|
|
3707
|
-
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
3708
|
-
scopes: ['browser.read'], resourceClaims: [{ resource: 'remote', mode: 'read' }],
|
|
3709
|
-
argumentsSchema: sessionTargetSchema({ limit: { type: 'number' } }),
|
|
3710
|
-
},
|
|
3711
|
-
{
|
|
3712
|
-
actionId: 'extract_tables',
|
|
3713
|
-
title: 'Extract tables',
|
|
3714
|
-
description: 'Extract simple HTML tables as row arrays.',
|
|
3715
|
-
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
3716
|
-
scopes: ['browser.read'], resourceClaims: [{ resource: 'remote', mode: 'read' }],
|
|
3717
|
-
argumentsSchema: sessionTargetSchema({ limit: { type: 'number' } }),
|
|
3718
|
-
},
|
|
3719
|
-
{
|
|
3720
|
-
actionId: 'extract_forms',
|
|
3721
|
-
title: 'Extract forms',
|
|
3722
|
-
description: 'Extract form field summaries without values that look like secrets.',
|
|
3723
|
-
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
3724
|
-
scopes: ['browser.read'], resourceClaims: [{ resource: 'remote', mode: 'read' }],
|
|
3725
|
-
argumentsSchema: sessionTargetSchema({ limit: { type: 'number' } }),
|
|
3726
|
-
},
|
|
3727
|
-
{
|
|
3728
|
-
actionId: 'snapshot_interactive',
|
|
3729
|
-
title: 'Snapshot interactive elements',
|
|
3730
|
-
description: 'Snapshot interactive elements with stable selector hints plus CSS-pixel viewport geometry for screenshot-to-input grounding.',
|
|
3731
|
-
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
3732
|
-
scopes: ['browser.read'], resourceClaims: [{ resource: 'remote', mode: 'read' }],
|
|
3733
|
-
argumentsSchema: sessionTargetSchema({ limit: { type: 'number' }, ...frameScopeProperties }),
|
|
3734
|
-
},
|
|
3735
|
-
{
|
|
3736
|
-
actionId: 'get_console_errors',
|
|
3737
|
-
title: 'Get console errors',
|
|
3738
|
-
description: 'Return captured console error messages for a page open cycle.',
|
|
3739
|
-
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
3740
|
-
scopes: ['browser.read'], resourceClaims: [{ resource: 'remote', mode: 'read' }],
|
|
3741
|
-
argumentsSchema: sessionTargetSchema({}),
|
|
3742
|
-
},
|
|
3743
|
-
{
|
|
3744
|
-
actionId: 'get_failed_requests',
|
|
3745
|
-
title: 'Get failed requests',
|
|
3746
|
-
description: 'Return failed network requests captured during a page open cycle.',
|
|
3747
|
-
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
3748
|
-
scopes: ['browser.read'], resourceClaims: [{ resource: 'remote', mode: 'read' }],
|
|
3749
|
-
argumentsSchema: sessionTargetSchema({}),
|
|
3750
|
-
},
|
|
3751
|
-
{
|
|
3752
|
-
actionId: 'activate_page',
|
|
3753
|
-
title: 'Activate browser page',
|
|
3754
|
-
description: 'Bring the exact saved browser page/tab to the foreground without opening or replacing a tab.',
|
|
3755
|
-
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
3756
|
-
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
3757
|
-
argumentsSchema: interactSchema({}, []),
|
|
3758
|
-
},
|
|
3759
|
-
{
|
|
3760
|
-
actionId: 'click',
|
|
3761
|
-
title: 'Click element',
|
|
3762
|
-
description: 'Click a selector on an allowed page after authorization.',
|
|
3763
|
-
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
3764
|
-
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
3765
|
-
argumentsSchema: interactSchema({ selector: { type: 'string' } }, ['selector']),
|
|
3766
|
-
},
|
|
3767
|
-
{
|
|
3768
|
-
actionId: 'click_text',
|
|
3769
|
-
title: 'Click exact visible text',
|
|
3770
|
-
description: 'Click the smallest visible standard DOM element whose normalized text exactly matches the requested text. Closed shadow roots are intentionally not traversed.',
|
|
3771
|
-
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
3772
|
-
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
3773
|
-
argumentsSchema: interactSchema({ text: { type: 'string', minLength: 1, maxLength: 200 } }, ['text']),
|
|
3774
|
-
},
|
|
3775
|
-
{
|
|
3776
|
-
actionId: 'double_click',
|
|
3777
|
-
title: 'Double-click element',
|
|
3778
|
-
description: 'Double-click a selector after authorization.',
|
|
3779
|
-
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
3780
|
-
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
3781
|
-
argumentsSchema: interactSchema({ selector: { type: 'string' } }, ['selector']),
|
|
3782
|
-
},
|
|
3783
|
-
{
|
|
3784
|
-
actionId: 'hover',
|
|
3785
|
-
title: 'Hover element',
|
|
3786
|
-
description: 'Hover a selector after authorization.',
|
|
3787
|
-
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
3788
|
-
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
3789
|
-
argumentsSchema: interactSchema({ selector: { type: 'string' } }, ['selector']),
|
|
3790
|
-
},
|
|
3791
|
-
{
|
|
3792
|
-
actionId: 'focus',
|
|
3793
|
-
title: 'Focus element',
|
|
3794
|
-
description: 'Focus a selector after authorization.',
|
|
3795
|
-
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
3796
|
-
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
3797
|
-
argumentsSchema: interactSchema({ selector: { type: 'string' } }, ['selector']),
|
|
3798
|
-
},
|
|
3799
|
-
{
|
|
3800
|
-
actionId: 'type',
|
|
3801
|
-
title: 'Type into element',
|
|
3802
|
-
description: 'Type text into a selector (append-style) after authorization.',
|
|
3803
|
-
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
3804
|
-
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
3805
|
-
argumentsSchema: interactSchema({ selector: { type: 'string' }, text: { type: 'string' } }, ['selector', 'text']),
|
|
3806
|
-
},
|
|
3807
|
-
{
|
|
3808
|
-
actionId: 'fill',
|
|
3809
|
-
title: 'Fill element',
|
|
3810
|
-
description: 'Replace the value of a selector after authorization.',
|
|
3811
|
-
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
3812
|
-
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
3813
|
-
argumentsSchema: interactSchema({ selector: { type: 'string' }, text: { type: 'string' } }, ['selector', 'text']),
|
|
3814
|
-
},
|
|
3815
|
-
{
|
|
3816
|
-
actionId: 'select_option',
|
|
3817
|
-
title: 'Select option',
|
|
3818
|
-
description: 'Select one or more option values on a <select> after authorization.',
|
|
3819
|
-
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
3820
|
-
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
3821
|
-
argumentsSchema: interactSchema({ selector: { type: 'string' }, values: { type: 'array', items: { type: 'string' } } }, ['selector', 'values']),
|
|
3822
|
-
},
|
|
3823
|
-
{
|
|
3824
|
-
actionId: 'check',
|
|
3825
|
-
title: 'Check checkbox',
|
|
3826
|
-
description: 'Check a checkbox/radio after authorization.',
|
|
3827
|
-
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
3828
|
-
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
3829
|
-
argumentsSchema: interactSchema({ selector: { type: 'string' } }, ['selector']),
|
|
3830
|
-
},
|
|
3831
|
-
{
|
|
3832
|
-
actionId: 'uncheck',
|
|
3833
|
-
title: 'Uncheck checkbox',
|
|
3834
|
-
description: 'Uncheck a checkbox after authorization.',
|
|
3835
|
-
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
3836
|
-
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
3837
|
-
argumentsSchema: interactSchema({ selector: { type: 'string' } }, ['selector']),
|
|
3838
|
-
},
|
|
3839
|
-
{
|
|
3840
|
-
actionId: 'press',
|
|
3841
|
-
title: 'Press key',
|
|
3842
|
-
description: 'Press a key on a selector on an allowed page after authorization.',
|
|
3843
|
-
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
3844
|
-
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
3845
|
-
argumentsSchema: interactSchema({ selector: { type: 'string' }, key: { type: 'string' } }, ['selector', 'key']),
|
|
3846
|
-
},
|
|
3847
|
-
{
|
|
3848
|
-
actionId: 'keyboard_shortcut',
|
|
3849
|
-
title: 'Keyboard shortcut',
|
|
3850
|
-
description: 'Press a keyboard shortcut (e.g. Meta+A) after authorization.',
|
|
3851
|
-
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
3852
|
-
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
3853
|
-
argumentsSchema: interactSchema({ key: { type: 'string' } }, ['key']),
|
|
3854
|
-
},
|
|
3855
|
-
{
|
|
3856
|
-
actionId: 'trusted_input',
|
|
3857
|
-
title: 'Trusted browser input',
|
|
3858
|
-
description: 'Send bounded mouse, wheel, drag, key, or text input after authorization. Playwright/CDP uses page input primitives; native macOS sessions require the exact saved tab to already be foreground/active and use the stable Desktop Operator input broker without activating another tab.',
|
|
3859
|
-
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
3860
|
-
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
3861
|
-
argumentsSchema: interactSchema({
|
|
3862
|
-
kind: { type: 'string', enum: ['click', 'move', 'wheel', 'drag', 'key', 'text'] },
|
|
3863
|
-
x: { type: 'number', minimum: 0, maximum: 100000 },
|
|
3864
|
-
y: { type: 'number', minimum: 0, maximum: 100000 },
|
|
3865
|
-
from_x: { type: 'number', minimum: 0, maximum: 100000 },
|
|
3866
|
-
from_y: { type: 'number', minimum: 0, maximum: 100000 },
|
|
3867
|
-
to_x: { type: 'number', minimum: 0, maximum: 100000 },
|
|
3868
|
-
to_y: { type: 'number', minimum: 0, maximum: 100000 },
|
|
3869
|
-
delta_x: { type: 'number', minimum: -100000, maximum: 100000 },
|
|
3870
|
-
delta_y: { type: 'number', minimum: -100000, maximum: 100000 },
|
|
3871
|
-
button: { type: 'string', enum: ['left', 'middle', 'right'] },
|
|
3872
|
-
click_count: { type: 'number', minimum: 1, maximum: 3 },
|
|
3873
|
-
steps: { type: 'number', minimum: 1, maximum: 100 },
|
|
3874
|
-
key: { type: 'string', minLength: 1, maxLength: 100 },
|
|
3875
|
-
text: { type: 'string', maxLength: 10000 },
|
|
3876
|
-
guard_selector: { type: 'string' },
|
|
3877
|
-
...frameScopeProperties,
|
|
3878
|
-
}, ['kind']),
|
|
3879
|
-
},
|
|
3880
|
-
{
|
|
3881
|
-
actionId: 'dispatch_event',
|
|
3882
|
-
title: 'Dispatch DOM event',
|
|
3883
|
-
description: 'Dispatch one named bubbling/composed DOM CustomEvent on an explicit selector after authorization. No arbitrary JavaScript payload is accepted.',
|
|
3884
|
-
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
3885
|
-
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
3886
|
-
argumentsSchema: interactSchema({ selector: { type: 'string' }, event: { type: 'string', minLength: 1, maxLength: 64 } }, ['selector', 'event']),
|
|
3887
|
-
},
|
|
3888
|
-
{
|
|
3889
|
-
actionId: 'wait_for_selector',
|
|
3890
|
-
title: 'Wait for selector',
|
|
3891
|
-
description: 'Wait for a selector state on an allowed page after authorization.',
|
|
3892
|
-
readOnly: true, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
3893
|
-
scopes: ['browser.interact', 'browser.profile'], resourceClaims: readRemote,
|
|
3894
|
-
argumentsSchema: sessionTargetSchema({
|
|
3895
|
-
selector: { type: 'string' },
|
|
3896
|
-
state: { type: 'string', enum: ['attached', 'detached', 'visible', 'hidden'] },
|
|
3897
|
-
}, ['selector']),
|
|
3898
|
-
},
|
|
3899
|
-
{
|
|
3900
|
-
actionId: 'attach_local_file',
|
|
3901
|
-
title: 'Attach local file(s)',
|
|
3902
|
-
description: 'Set one or more allowed local files on input[type=file] after authorization. file_path remains backward-compatible; file_paths supports multiple selection. Never auto-opens executables.',
|
|
3903
|
-
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
3904
|
-
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
3905
|
-
argumentsSchema: interactSchema({
|
|
3906
|
-
selector: { type: 'string' },
|
|
3907
|
-
file_path: { type: 'string' },
|
|
3908
|
-
file_paths: { type: 'array', minItems: 1, maxItems: 32, items: { type: 'string' } },
|
|
3909
|
-
}, ['selector']),
|
|
3910
|
-
},
|
|
3911
|
-
{
|
|
3912
|
-
actionId: 'await_file_transfer',
|
|
3913
|
-
title: 'Await file transfer',
|
|
3914
|
-
description: 'Capture a browser download into bounded artifact storage after authorization. Never auto-opens downloaded files.',
|
|
3915
|
-
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 120_000, cancellable: true, idempotent: false,
|
|
3916
|
-
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
3917
|
-
argumentsSchema: interactSchema({ selector: { type: 'string' }, suggested_name: { type: 'string' } }, ['selector']),
|
|
3918
|
-
},
|
|
3919
|
-
{
|
|
3920
|
-
actionId: 'close_page',
|
|
3921
|
-
title: 'Close session',
|
|
3922
|
-
description: 'Remove saved session metadata while keeping the persistent profile.',
|
|
3923
|
-
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 15_000, cancellable: true, idempotent: true,
|
|
3924
|
-
scopes: ['browser.read', 'browser.profile'], resourceClaims: [{ resource: 'repo-state', mode: 'write' }],
|
|
3925
|
-
argumentsSchema: { type: 'object', properties: { session_id: { type: 'string' } }, required: ['session_id'], additionalProperties: false },
|
|
3926
|
-
},
|
|
3927
|
-
];
|
|
3928
|
-
const foregroundRequired = new Set(['activate_page', 'request_human_handoff']);
|
|
3929
|
-
const foregroundPossible = new Set([
|
|
3930
|
-
'create_session', 'open_page', 'navigate', 'reload', 'go_back', 'resolve_handoff',
|
|
3931
|
-
'click', 'click_text', 'double_click', 'hover', 'focus', 'type', 'fill', 'select_option',
|
|
3932
|
-
'check', 'uncheck', 'press', 'keyboard_shortcut', 'trusted_input', 'dispatch_event',
|
|
3933
|
-
'wait_for_selector', 'attach_local_file', 'await_file_transfer',
|
|
3934
|
-
]);
|
|
3935
|
-
return descriptors.map((descriptor) => ({
|
|
3936
|
-
...descriptor,
|
|
3937
|
-
foregroundEffect: foregroundRequired.has(descriptor.actionId)
|
|
3938
|
-
? 'required'
|
|
3939
|
-
: foregroundPossible.has(descriptor.actionId)
|
|
3940
|
-
? 'possible'
|
|
3941
|
-
: 'none',
|
|
3942
|
-
}));
|
|
3943
|
-
}
|
|
3944
|
-
|
|
3945
3092
|
function browserUserFacingStatus(config: BrowserPluginConfig, ready: boolean, sessionCount = 0): string {
|
|
3946
3093
|
if (!config.enabled) return 'disabled';
|
|
3947
3094
|
if (!ready) return 'not ready';
|
|
@@ -4050,7 +3197,9 @@ function health(config: BrowserPluginConfig, repoRoot?: string): AssistantPlugin
|
|
|
4050
3197
|
details: {
|
|
4051
3198
|
...baseDetails,
|
|
4052
3199
|
provider: config.browserMode === 'attach_preferred'
|
|
4053
|
-
? '
|
|
3200
|
+
? config.cdpAttachFallback === 'managed_persistent'
|
|
3201
|
+
? 'cdp-or-macos-active-browser-or-persistent-context'
|
|
3202
|
+
: 'cdp-or-macos-active-browser'
|
|
4054
3203
|
: 'playwright-persistent-context',
|
|
4055
3204
|
userFacingStatus: 'not ready',
|
|
4056
3205
|
},
|
|
@@ -4097,15 +3246,19 @@ export function buildBrowserPluginManifest(previousRevision = 0, previousUpdated
|
|
|
4097
3246
|
reason: !config.enabled
|
|
4098
3247
|
? 'Browser plugin is disabled.'
|
|
4099
3248
|
: state.ready
|
|
4100
|
-
? `Browser plugin is ready via ${config.browserMode === 'attach_preferred'
|
|
3249
|
+
? `Browser plugin is ready via ${config.browserMode === 'attach_preferred'
|
|
3250
|
+
? config.cdpAttachFallback === 'managed_persistent'
|
|
3251
|
+
? 'CDP, macOS native attach, and explicit managed fallback.'
|
|
3252
|
+
: 'CDP or macOS native attach; managed fallback is disabled.'
|
|
3253
|
+
: 'Playwright persistent context.'}`
|
|
4101
3254
|
: state.state === 'degraded'
|
|
4102
3255
|
? state.warnings[0] ?? 'Browser plugin requires a successful live native attach probe.'
|
|
4103
3256
|
: state.errors[0],
|
|
4104
3257
|
},
|
|
4105
3258
|
health: state,
|
|
4106
|
-
permissions:
|
|
4107
|
-
capabilities:
|
|
4108
|
-
actions:
|
|
3259
|
+
permissions: browserPermissions(state.ready),
|
|
3260
|
+
capabilities: browserCapabilities(),
|
|
3261
|
+
actions: browserActions(),
|
|
4109
3262
|
updatedAt: previousUpdatedAt ?? now(),
|
|
4110
3263
|
};
|
|
4111
3264
|
}
|
|
@@ -4127,7 +3280,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
4127
3280
|
): Promise<Record<string, unknown>> {
|
|
4128
3281
|
const persisted = loadConfig(input.repoRoot);
|
|
4129
3282
|
const actionSessionId = input.actionId === 'configure' ? undefined : stringValue(input.args.session_id);
|
|
4130
|
-
const actionSession = actionSessionId ?
|
|
3283
|
+
const actionSession = actionSessionId ? findBrowserSession(input.repoRoot, actionSessionId) : undefined;
|
|
4131
3284
|
const current = input.actionId === 'configure' ? persisted : effectiveBrowserActionConfig(persisted, input.args, actionSession);
|
|
4132
3285
|
if (!current.enabled && input.actionId !== 'configure') {
|
|
4133
3286
|
throw new AssistantPluginError('PLUGIN_DISABLED', 'Browser plugin is disabled.', { retryable: false });
|
|
@@ -4299,12 +3452,12 @@ async function executeBrowserPluginActionInternal(
|
|
|
4299
3452
|
case 'close_page': {
|
|
4300
3453
|
const sessionId = requiredString(input.args.session_id, 'session_id');
|
|
4301
3454
|
assertBrowserSessionAvailable(input.repoRoot, sessionId);
|
|
4302
|
-
const session =
|
|
3455
|
+
const session = findBrowserSession(input.repoRoot, sessionId);
|
|
4303
3456
|
const tab = session?.browser?.tab;
|
|
4304
3457
|
const managedClosed = session ? await closeManagedSessionPage(input.repoRoot, current, session) : false;
|
|
4305
|
-
const nativeClose = session ? await closeTrackedNativeOwnedSession(session, current) : { resourceClosed: false, resourceAlreadyMissing: false };
|
|
3458
|
+
const nativeClose = session ? await closeTrackedNativeOwnedSession(session, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS) : { resourceClosed: false, resourceAlreadyMissing: false };
|
|
4306
3459
|
const closedSessionIds = session ? nativeOwnedAliasSessionIds(input.repoRoot, session) : [sessionId];
|
|
4307
|
-
for (const closedSessionId of closedSessionIds)
|
|
3460
|
+
for (const closedSessionId of closedSessionIds) removeBrowserSession(input.repoRoot, closedSessionId);
|
|
4308
3461
|
return {
|
|
4309
3462
|
closed: true, sessionId,
|
|
4310
3463
|
resourceClosed: managedClosed || nativeClose.resourceClosed,
|
|
@@ -4324,10 +3477,10 @@ async function executeBrowserPluginActionInternal(
|
|
|
4324
3477
|
let resourceAlreadyMissingCount = 0;
|
|
4325
3478
|
for (const session of sessions) {
|
|
4326
3479
|
try {
|
|
4327
|
-
const nativeClose = await closeTrackedNativeOwnedSession(session, current);
|
|
3480
|
+
const nativeClose = await closeTrackedNativeOwnedSession(session, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
4328
3481
|
if (nativeClose.resourceClosed) resourceClosedCount += 1;
|
|
4329
3482
|
if (nativeClose.resourceAlreadyMissing) resourceAlreadyMissingCount += 1;
|
|
4330
|
-
|
|
3483
|
+
removeBrowserSession(input.repoRoot, session.sessionId);
|
|
4331
3484
|
removedCount += 1;
|
|
4332
3485
|
} catch (error) {
|
|
4333
3486
|
failedSessionIds.push(session.sessionId);
|
|
@@ -4356,7 +3509,6 @@ async function executeBrowserPluginActionInternal(
|
|
|
4356
3509
|
requestId: input.requestId,
|
|
4357
3510
|
jobId: input.jobId,
|
|
4358
3511
|
sessionId: target.sessionId,
|
|
4359
|
-
sessionPath: sessionPath(input.repoRoot, target.sessionId),
|
|
4360
3512
|
url: target.url,
|
|
4361
3513
|
profileDir: managedTarget.profile.profileDir,
|
|
4362
3514
|
selectedProfilePath: managedTarget.profile.selectedProfilePath,
|
|
@@ -4405,7 +3557,6 @@ async function executeBrowserPluginActionInternal(
|
|
|
4405
3557
|
requestId: input.requestId,
|
|
4406
3558
|
jobId: input.jobId,
|
|
4407
3559
|
sessionId: target.sessionId,
|
|
4408
|
-
sessionPath: sessionPath(input.repoRoot, target.sessionId),
|
|
4409
3560
|
url: target.url,
|
|
4410
3561
|
profileDir: profile.profileDir,
|
|
4411
3562
|
selectedProfilePath,
|
|
@@ -4428,6 +3579,17 @@ async function executeBrowserPluginActionInternal(
|
|
|
4428
3579
|
case 'get_handoff_status': {
|
|
4429
3580
|
const interactionId = requiredString(input.args.interaction_id, 'interaction_id');
|
|
4430
3581
|
const handoff = getBrowserHandoff(input.repoRoot, interactionId);
|
|
3582
|
+
if (handoff.status === 'completed' && handoff.result?.url) {
|
|
3583
|
+
const session = findBrowserSession(input.repoRoot, handoff.sessionId);
|
|
3584
|
+
if (session) {
|
|
3585
|
+
saveBrowserSession(input.repoRoot, {
|
|
3586
|
+
...session,
|
|
3587
|
+
url: handoff.result.url,
|
|
3588
|
+
title: handoff.result.title,
|
|
3589
|
+
updatedAt: new Date().toISOString(),
|
|
3590
|
+
});
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
4431
3593
|
return {
|
|
4432
3594
|
provider: isRuntimeManagedBrowserHandoff(handoff)
|
|
4433
3595
|
? 'playwright-runtime-managed-handoff'
|
|
@@ -4445,13 +3607,13 @@ async function executeBrowserPluginActionInternal(
|
|
|
4445
3607
|
}
|
|
4446
3608
|
const currentHandoff = getBrowserHandoff(input.repoRoot, interactionId);
|
|
4447
3609
|
if (isRuntimeManagedBrowserHandoff(currentHandoff)) {
|
|
4448
|
-
const session =
|
|
3610
|
+
const session = findBrowserSession(input.repoRoot, currentHandoff.sessionId);
|
|
4449
3611
|
const managedTarget = session ? await runtimeManagedPageForSession(input.repoRoot, current, session) : undefined;
|
|
4450
3612
|
const result = managedTarget
|
|
4451
3613
|
? { url: managedTarget.page.url(), title: await managedTarget.page.title().catch(() => session?.title) }
|
|
4452
3614
|
: currentHandoff.result;
|
|
4453
3615
|
if (resolution === 'resume' && session && result?.url) {
|
|
4454
|
-
|
|
3616
|
+
saveBrowserSession(input.repoRoot, {
|
|
4455
3617
|
...session,
|
|
4456
3618
|
url: result.url,
|
|
4457
3619
|
title: result.title,
|
|
@@ -4466,8 +3628,19 @@ async function executeBrowserPluginActionInternal(
|
|
|
4466
3628
|
};
|
|
4467
3629
|
}
|
|
4468
3630
|
const handoff = resolution === 'resume'
|
|
4469
|
-
?
|
|
3631
|
+
? await resumeBrowserHandoffAndWait(input.repoRoot, interactionId, input.requestId)
|
|
4470
3632
|
: cancelBrowserHandoff(input.repoRoot, interactionId, input.requestId);
|
|
3633
|
+
if (resolution === 'resume' && handoff.status === 'completed' && handoff.result?.url) {
|
|
3634
|
+
const session = findBrowserSession(input.repoRoot, handoff.sessionId);
|
|
3635
|
+
if (session) {
|
|
3636
|
+
saveBrowserSession(input.repoRoot, {
|
|
3637
|
+
...session,
|
|
3638
|
+
url: handoff.result.url,
|
|
3639
|
+
title: handoff.result.title,
|
|
3640
|
+
updatedAt: new Date().toISOString(),
|
|
3641
|
+
});
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
4471
3644
|
return {
|
|
4472
3645
|
provider: handoff.targetId.startsWith('macos-apple-events:') ? 'macos-apple-events-handoff-host' : 'playwright-handoff-host',
|
|
4473
3646
|
resolutionRequested: resolution,
|
|
@@ -4488,7 +3661,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
4488
3661
|
: normalizedUrl(input.args.url);
|
|
4489
3662
|
const existingSessionId = stringValue(input.args.session_id);
|
|
4490
3663
|
const target: BrowserActionTarget = existingSessionId
|
|
4491
|
-
? { sessionId: existingSessionId, url, existingSession:
|
|
3664
|
+
? { sessionId: existingSessionId, url, existingSession: findBrowserSession(input.repoRoot, existingSessionId) }
|
|
4492
3665
|
: { sessionId: sessionIdFor(url), url };
|
|
4493
3666
|
if (hasNativeAdoption) {
|
|
4494
3667
|
if (input.actionId !== 'create_session' || (nativeProduct && !normalizedNativeProduct)) {
|
|
@@ -4510,7 +3683,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
4510
3683
|
{ retryable: false },
|
|
4511
3684
|
);
|
|
4512
3685
|
}
|
|
4513
|
-
const configuredCandidates = (current.nativeBrowserCandidates ?? [
|
|
3686
|
+
const configuredCandidates = (current.nativeBrowserCandidates ?? [...DEFAULT_USER_NATIVE_BROWSER_CANDIDATES])
|
|
4514
3687
|
.filter((candidate): candidate is MacOsBrowserProduct => candidate === 'chrome' || candidate === 'vivaldi');
|
|
4515
3688
|
const candidates: MacOsBrowserProduct[] = normalizedNativeProduct ? [normalizedNativeProduct] : configuredCandidates;
|
|
4516
3689
|
const discovered = await discoverMacOsBrowserAttachment(candidates, Math.min(timeout, MAX_CDP_DISCOVERY_TIMEOUT_MS));
|
|
@@ -4607,7 +3780,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
4607
3780
|
: 'Explicitly adopted an existing user-owned native browser tab.',
|
|
4608
3781
|
},
|
|
4609
3782
|
}, requestedUrl, title, 'saved_url', { ownership: 'user_owned', windowId: adoptedRef.windowId, tabId: adoptedRef.tabId });
|
|
4610
|
-
const session =
|
|
3783
|
+
const session = saveBrowserSession(input.repoRoot, sessionFromPage(target, requestedUrl, title, connection));
|
|
4611
3784
|
return {
|
|
4612
3785
|
provider: 'macos-apple-events',
|
|
4613
3786
|
session,
|
|
@@ -4618,9 +3791,9 @@ async function executeBrowserPluginActionInternal(
|
|
|
4618
3791
|
: {}),
|
|
4619
3792
|
};
|
|
4620
3793
|
}
|
|
4621
|
-
return await withPage(input.repoRoot, current, target, input.args, async (page, diagnostics, connection) => {
|
|
3794
|
+
return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, diagnostics, connection) => {
|
|
4622
3795
|
const identity = await readPageIdentity(page, connection, target.url);
|
|
4623
|
-
const session =
|
|
3796
|
+
const session = saveBrowserSession(input.repoRoot, sessionFromPage(target, identity.url, identity.title, connection));
|
|
4624
3797
|
return {
|
|
4625
3798
|
provider: browserResultProvider(connection.provider),
|
|
4626
3799
|
session,
|
|
@@ -4640,7 +3813,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
4640
3813
|
case 'go_back':
|
|
4641
3814
|
case 'wait_for_load_state': {
|
|
4642
3815
|
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
4643
|
-
return await withPage(input.repoRoot, current, target, input.args, async (page, diagnostics, connection) => {
|
|
3816
|
+
return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, diagnostics, connection) => {
|
|
4644
3817
|
if (input.actionId === 'reload') {
|
|
4645
3818
|
if (page.reload) await page.reload({ waitUntil: waitUntil(input.args.wait_until), timeout: positiveNumber(input.args.timeout_ms, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS) });
|
|
4646
3819
|
else await page.goto(target.url, { waitUntil: waitUntil(input.args.wait_until), timeout: positiveNumber(input.args.timeout_ms, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS) });
|
|
@@ -4652,7 +3825,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
4652
3825
|
});
|
|
4653
3826
|
}
|
|
4654
3827
|
const identity = await readPageIdentity(page, connection);
|
|
4655
|
-
const session =
|
|
3828
|
+
const session = saveBrowserSession(input.repoRoot, sessionFromPage(target, identity.url, identity.title, connection));
|
|
4656
3829
|
return {
|
|
4657
3830
|
provider: browserResultProvider(connection.provider),
|
|
4658
3831
|
session,
|
|
@@ -4670,7 +3843,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
4670
3843
|
}
|
|
4671
3844
|
case 'get_text': {
|
|
4672
3845
|
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
4673
|
-
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
3846
|
+
return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
4674
3847
|
const selection = resolveBrowserEvaluationScope(page, input.args, connection);
|
|
4675
3848
|
return {
|
|
4676
3849
|
provider: browserResultProvider(connection.provider),
|
|
@@ -4684,7 +3857,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
4684
3857
|
}
|
|
4685
3858
|
case 'get_html': {
|
|
4686
3859
|
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
4687
|
-
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
3860
|
+
return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
4688
3861
|
const selection = resolveBrowserEvaluationScope(page, input.args, connection);
|
|
4689
3862
|
const raw = await selection.scope.evaluate<string>(EXTRACTION_SCRIPTS.html(stringValue(input.args.selector)));
|
|
4690
3863
|
return {
|
|
@@ -4702,7 +3875,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
4702
3875
|
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
4703
3876
|
const selector = requiredString(input.args.selector, 'selector');
|
|
4704
3877
|
const limit = Math.min(positiveNumber(input.args.limit, 25), 100);
|
|
4705
|
-
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
3878
|
+
return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
4706
3879
|
const selection = resolveBrowserEvaluationScope(page, input.args, connection);
|
|
4707
3880
|
const matches = await selection.scope.evaluate<Array<Record<string, unknown>>>(EXTRACTION_SCRIPTS.query(selector, input.actionId === 'query_selector' ? 1 : limit));
|
|
4708
3881
|
return {
|
|
@@ -4722,7 +3895,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
4722
3895
|
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
4723
3896
|
const selector = requiredString(input.args.selector, 'selector');
|
|
4724
3897
|
const attribute = requiredString(input.args.attribute, 'attribute');
|
|
4725
|
-
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
3898
|
+
return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
4726
3899
|
const selection = resolveBrowserEvaluationScope(page, input.args, connection);
|
|
4727
3900
|
return {
|
|
4728
3901
|
provider: browserResultProvider(connection.provider),
|
|
@@ -4739,7 +3912,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
4739
3912
|
case 'list_frames': {
|
|
4740
3913
|
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
4741
3914
|
const limit = Math.min(positiveNumber(input.args.limit, 50), 100);
|
|
4742
|
-
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
3915
|
+
return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
4743
3916
|
if (connection.provider === 'macos-apple-events' || !page.frames) {
|
|
4744
3917
|
throw new AssistantPluginError('PLUGIN_BROWSER_FRAME_SCOPE_UNAVAILABLE', 'Frame discovery requires a Playwright/CDP-controlled page; native Apple Events sessions fail closed.', {
|
|
4745
3918
|
retryable: false,
|
|
@@ -4774,7 +3947,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
4774
3947
|
if (requireVisible && !selector) {
|
|
4775
3948
|
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'require_visible requires selector.', { retryable: false });
|
|
4776
3949
|
}
|
|
4777
|
-
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
3950
|
+
return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
4778
3951
|
const selection = resolveBrowserEvaluationScope(page, input.args, connection);
|
|
4779
3952
|
const observedUrl = selection.frame?.url ?? page.url();
|
|
4780
3953
|
const checks: Array<Record<string, unknown>> = [];
|
|
@@ -4816,7 +3989,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
4816
3989
|
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
4817
3990
|
const file = screenshotFilePath(input.repoRoot, 'screenshot', target.sessionId, target.url);
|
|
4818
3991
|
const selector = stringValue(input.args.selector);
|
|
4819
|
-
const screenshot = await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
3992
|
+
const screenshot = await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
4820
3993
|
let bytes: number;
|
|
4821
3994
|
if (selector && page.locator) {
|
|
4822
3995
|
bytes = (await page.locator(selector).screenshot({ path: file })).length;
|
|
@@ -4842,7 +4015,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
4842
4015
|
case 'snapshot_interactive': {
|
|
4843
4016
|
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
4844
4017
|
const limit = Math.min(positiveNumber(input.args.limit, 50), 200);
|
|
4845
|
-
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
4018
|
+
return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
4846
4019
|
const script = input.actionId === 'extract_links'
|
|
4847
4020
|
? EXTRACTION_SCRIPTS.links(limit)
|
|
4848
4021
|
: input.actionId === 'extract_tables'
|
|
@@ -4890,7 +4063,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
4890
4063
|
case 'get_console_errors':
|
|
4891
4064
|
case 'get_failed_requests': {
|
|
4892
4065
|
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
4893
|
-
return await withPage(input.repoRoot, current, target, input.args, async (_page, diagnostics, connection) => {
|
|
4066
|
+
return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (_page, diagnostics, connection) => {
|
|
4894
4067
|
if (connection.provider === 'macos-apple-events') {
|
|
4895
4068
|
throw new AssistantPluginError(
|
|
4896
4069
|
'PLUGIN_BROWSER_DIAGNOSTICS_UNAVAILABLE',
|
|
@@ -4912,7 +4085,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
4912
4085
|
}
|
|
4913
4086
|
case 'activate_page': {
|
|
4914
4087
|
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
4915
|
-
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
4088
|
+
return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
4916
4089
|
const waitMs = positiveNumber(input.args.post_action_wait_ms, DEFAULT_POST_ACTION_WAIT_MS);
|
|
4917
4090
|
if (connection.provider === 'macos-apple-events') {
|
|
4918
4091
|
await establishAuthoritativeNativeForeground(input, page, connection, target, waitMs);
|
|
@@ -4932,7 +4105,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
4932
4105
|
if (text.length > 200) {
|
|
4933
4106
|
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'text must be 200 characters or fewer.', { retryable: false });
|
|
4934
4107
|
}
|
|
4935
|
-
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
4108
|
+
return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
4936
4109
|
const clicked = await page.evaluate((payload) => {
|
|
4937
4110
|
const args = payload as { text: string };
|
|
4938
4111
|
const normalize = (value: string) => value.replace(/\s+/g, ' ').trim();
|
|
@@ -4978,7 +4151,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
4978
4151
|
const selector = requiredString(input.args.selector, 'selector');
|
|
4979
4152
|
const timeoutMs = positiveNumber(input.args.timeout_ms, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
4980
4153
|
try {
|
|
4981
|
-
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
4154
|
+
return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
4982
4155
|
if (input.actionId === 'click' && openShadowSelectorParts(selector).length > 1) {
|
|
4983
4156
|
const parts = openShadowSelectorParts(selector);
|
|
4984
4157
|
await page.evaluate((payload) => {
|
|
@@ -5037,7 +4210,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
5037
4210
|
const selector = requiredString(input.args.selector, 'selector');
|
|
5038
4211
|
const text = requiredString(input.args.text, 'text');
|
|
5039
4212
|
const timeoutMs = positiveNumber(input.args.timeout_ms, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
5040
|
-
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
4213
|
+
return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
5041
4214
|
if (input.actionId === 'fill' || !page.type) await page.fill(selector, text, { timeout: timeoutMs });
|
|
5042
4215
|
else await page.type(selector, text, { timeout: timeoutMs });
|
|
5043
4216
|
await delay(positiveNumber(input.args.post_action_wait_ms, DEFAULT_POST_ACTION_WAIT_MS));
|
|
@@ -5052,7 +4225,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
5052
4225
|
const selector = requiredString(input.args.selector, 'selector');
|
|
5053
4226
|
const values = Array.isArray(input.args.values) ? input.args.values.map(String) : [];
|
|
5054
4227
|
if (values.length === 0) throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'values is required.', { retryable: false });
|
|
5055
|
-
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
4228
|
+
return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
5056
4229
|
if (!page.selectOption) {
|
|
5057
4230
|
throw new AssistantPluginError('PLUGIN_BROWSER_SELECT_OPTION_UNAVAILABLE', 'The active browser provider cannot select options safely.', { retryable: false });
|
|
5058
4231
|
}
|
|
@@ -5068,7 +4241,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
5068
4241
|
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
5069
4242
|
const selector = requiredString(input.args.selector, 'selector');
|
|
5070
4243
|
const key = requiredString(input.args.key, 'key');
|
|
5071
|
-
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
4244
|
+
return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
5072
4245
|
await page.press(selector, key, { timeout: positiveNumber(input.args.timeout_ms, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS) });
|
|
5073
4246
|
await delay(positiveNumber(input.args.post_action_wait_ms, DEFAULT_POST_ACTION_WAIT_MS));
|
|
5074
4247
|
return finalizeInteractiveAction(input.repoRoot, current, page, target, connection, 'press', `Pressed ${key} on ${selector}.`, {
|
|
@@ -5080,7 +4253,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
5080
4253
|
case 'keyboard_shortcut': {
|
|
5081
4254
|
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
5082
4255
|
const key = requiredString(input.args.key, 'key');
|
|
5083
|
-
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
4256
|
+
return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
5084
4257
|
if (page.keyboard?.press) await page.keyboard.press(key);
|
|
5085
4258
|
else await page.press('body', key, { timeout: positiveNumber(input.args.timeout_ms, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS) });
|
|
5086
4259
|
await delay(positiveNumber(input.args.post_action_wait_ms, DEFAULT_POST_ACTION_WAIT_MS));
|
|
@@ -5101,7 +4274,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
5101
4274
|
if (guardSelector && kind !== 'click' && kind !== 'drag') {
|
|
5102
4275
|
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'guard_selector is supported only for trusted_input click or drag.', { retryable: false });
|
|
5103
4276
|
}
|
|
5104
|
-
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
4277
|
+
return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
5105
4278
|
const nativeTrustedInput = connection.provider === 'macos-apple-events';
|
|
5106
4279
|
if (nativeTrustedInput && !page.trustedInput) {
|
|
5107
4280
|
throw new AssistantPluginError(
|
|
@@ -5236,7 +4409,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
5236
4409
|
if (!/^[A-Za-z][A-Za-z0-9:_-]{0,63}$/.test(event)) {
|
|
5237
4410
|
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'event must be a simple DOM event name (letters, digits, colon, underscore, or hyphen; max 64 chars).', { retryable: false });
|
|
5238
4411
|
}
|
|
5239
|
-
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
4412
|
+
return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
5240
4413
|
const dispatched = await page.evaluate((payload) => {
|
|
5241
4414
|
const args = payload as { selector: string; event: string };
|
|
5242
4415
|
const element = document.querySelector(args.selector);
|
|
@@ -5257,7 +4430,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
5257
4430
|
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
5258
4431
|
const selector = requiredString(input.args.selector, 'selector');
|
|
5259
4432
|
const state = waitForSelectorState(input.args.state);
|
|
5260
|
-
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
4433
|
+
return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
5261
4434
|
await page.waitForSelector(selector, {
|
|
5262
4435
|
state,
|
|
5263
4436
|
timeout: positiveNumber(input.args.timeout_ms, current.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
@@ -5304,7 +4477,7 @@ async function executeBrowserPluginActionInternal(
|
|
|
5304
4477
|
}
|
|
5305
4478
|
return lexicalPath;
|
|
5306
4479
|
});
|
|
5307
|
-
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
4480
|
+
return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
5308
4481
|
await page.evaluate((payload) => {
|
|
5309
4482
|
const args = payload as { selector: string; count: number };
|
|
5310
4483
|
const el = document.querySelector(args.selector) as HTMLInputElement | null;
|
|
@@ -5329,9 +4502,9 @@ async function executeBrowserPluginActionInternal(
|
|
|
5329
4502
|
const target = resolveActionTarget(input.repoRoot, input.args);
|
|
5330
4503
|
const selector = requiredString(input.args.selector, 'selector');
|
|
5331
4504
|
const requestedName = stringValue(input.args.suggested_name);
|
|
5332
|
-
const downloadDir =
|
|
4505
|
+
const downloadDir = browserStateDir(input.repoRoot, 'downloads');
|
|
5333
4506
|
mkdirSync(downloadDir, { recursive: true });
|
|
5334
|
-
return await withPage(input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
4507
|
+
return await withPage(input.actionId, input.repoRoot, current, target, input.args, async (page, _diagnostics, connection) => {
|
|
5335
4508
|
if (connection.provider === 'macos-apple-events') {
|
|
5336
4509
|
throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_ACTION_UNSUPPORTED', 'Native Apple Events attachment does not support download capture. Use CDP or a managed browser context.', { retryable: false });
|
|
5337
4510
|
}
|