mecord-connect 1.0.0
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/DISCLOSURE +47 -0
- package/LICENSE +64 -0
- package/README.md +44 -0
- package/bin/operator.mjs +10 -0
- package/package.json +58 -0
- package/runtime/app/apps/local-agent/src/approval-store.js +342 -0
- package/runtime/app/apps/local-agent/src/bootstrap-config.js +255 -0
- package/runtime/app/apps/local-agent/src/cli.js +237 -0
- package/runtime/app/apps/local-agent/src/device-reset.js +260 -0
- package/runtime/app/apps/local-agent/src/emergency-stop.js +141 -0
- package/runtime/app/apps/local-agent/src/main.js +311 -0
- package/runtime/app/apps/local-agent/src/privacy-data.js +129 -0
- package/runtime/app/apps/local-agent/src/relay-agent.js +333 -0
- package/runtime/app/apps/local-agent/src/relay-enrollment.js +156 -0
- package/runtime/app/apps/local-agent/src/relay-session-credentials.js +436 -0
- package/runtime/app/apps/local-agent/src/remote.js +130 -0
- package/runtime/app/apps/local-agent/src/runtime-factory.js +66 -0
- package/runtime/app/apps/local-agent/src/server.js +578 -0
- package/runtime/app/package.json +4 -0
- package/runtime/app/src/capabilities/browser-cdp-connection.js +199 -0
- package/runtime/app/src/capabilities/browser-cdp-frames.js +288 -0
- package/runtime/app/src/capabilities/browser-cdp-page.js +430 -0
- package/runtime/app/src/capabilities/browser-cdp.js +384 -0
- package/runtime/app/src/capabilities/browser-managed.js +345 -0
- package/runtime/app/src/capabilities/docker.js +448 -0
- package/runtime/app/src/capabilities/filesystem.js +264 -0
- package/runtime/app/src/capabilities/git-checkpoint.js +415 -0
- package/runtime/app/src/capabilities/git-write.js +321 -0
- package/runtime/app/src/capabilities/git.js +252 -0
- package/runtime/app/src/capabilities/index.js +16 -0
- package/runtime/app/src/capabilities/path-scope.js +86 -0
- package/runtime/app/src/capabilities/postgres.js +566 -0
- package/runtime/app/src/capabilities/process.js +258 -0
- package/runtime/app/src/capabilities/project-command.js +599 -0
- package/runtime/app/src/capabilities/project-transaction.js +215 -0
- package/runtime/app/src/capabilities/project.js +67 -0
- package/runtime/app/src/capabilities/system.js +40 -0
- package/runtime/app/src/capabilities/vscode.js +312 -0
- package/runtime/app/src/capabilities/windows-uia.js +331 -0
- package/runtime/app/src/core/account-device-registry.js +604 -0
- package/runtime/app/src/core/action-identity.js +44 -0
- package/runtime/app/src/core/audit.js +295 -0
- package/runtime/app/src/core/capability-policy.js +58 -0
- package/runtime/app/src/core/child-environment.js +29 -0
- package/runtime/app/src/core/device-enrollment.js +417 -0
- package/runtime/app/src/core/device-identity.js +436 -0
- package/runtime/app/src/core/device-registry.js +477 -0
- package/runtime/app/src/core/device-reset.js +178 -0
- package/runtime/app/src/core/device-routing.js +305 -0
- package/runtime/app/src/core/durable-state.js +306 -0
- package/runtime/app/src/core/errors.js +20 -0
- package/runtime/app/src/core/evidence.js +9 -0
- package/runtime/app/src/core/index.js +17 -0
- package/runtime/app/src/core/machine-state.js +160 -0
- package/runtime/app/src/core/network-authority.js +20 -0
- package/runtime/app/src/core/policy.js +67 -0
- package/runtime/app/src/core/product-identity.js +7 -0
- package/runtime/app/src/core/provenance.js +18 -0
- package/runtime/app/src/core/provider-learning.js +274 -0
- package/runtime/app/src/core/public-plugin-surface.js +25 -0
- package/runtime/app/src/core/public-restricted-data.js +105 -0
- package/runtime/app/src/core/rate-limit.js +118 -0
- package/runtime/app/src/core/relay-client.js +568 -0
- package/runtime/app/src/core/relay-delivery-store.js +512 -0
- package/runtime/app/src/core/relay-result-store.js +321 -0
- package/runtime/app/src/core/release-update.js +253 -0
- package/runtime/app/src/core/router.js +96 -0
- package/runtime/app/src/core/runtime.js +136 -0
- package/runtime/app/src/core/scoped-path-syntax.js +16 -0
- package/runtime/app/src/core/session-token.js +498 -0
- package/runtime/app/src/core/task-orchestrator.js +1148 -0
- package/runtime/app/src/core/task-store.js +517 -0
- package/runtime/app/src/core/task.js +142 -0
- package/runtime/app/src/core/trusted-executable.js +124 -0
- package/runtime/app/src/core/types.js +83 -0
- package/runtime/app/src/core/windows-path-lease.js +138 -0
- package/runtime/native/operator-windows-dpapi.exe +0 -0
- package/runtime/native/operator-windows-path-lease.exe +0 -0
- package/runtime/native/operator-windows-uia.exe +0 -0
- package/runtime/runtime-manifest.json +380 -0
- package/src/cli.mjs +345 -0
- package/src/publish-check.mjs +22 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { spawn, } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { evidence } from '../core/evidence.js';
|
|
7
|
+
import { OperatorError } from '../core/errors.js';
|
|
8
|
+
import { safeChildEnvironment } from '../core/child-environment.js';
|
|
9
|
+
import { BrowserCdpProvider } from './browser-cdp.js';
|
|
10
|
+
import { assertLoopbackEndpoint } from './browser-cdp-page.js';
|
|
11
|
+
|
|
12
|
+
const SCORE = {
|
|
13
|
+
reliability: 0.95,
|
|
14
|
+
latency: 0.94,
|
|
15
|
+
determinism: 0.97,
|
|
16
|
+
security: 0.96,
|
|
17
|
+
reversibility: 0.95,
|
|
18
|
+
informationQuality: 0.96,
|
|
19
|
+
interactionCost: 0.015
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const RECOVERABLE_CDP_CODES = new Set([
|
|
23
|
+
'CDP_UNAVAILABLE',
|
|
24
|
+
'CDP_HTTP_ERROR',
|
|
25
|
+
'CDP_CONNECT_FAILED',
|
|
26
|
+
'CDP_CONNECT_TIMEOUT',
|
|
27
|
+
'CDP_CONNECTION_CLOSED',
|
|
28
|
+
'CDP_BROWSER_TARGET_UNAVAILABLE',
|
|
29
|
+
'CDP_TARGET_UNAVAILABLE'
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
export function candidateBrowserPaths(platform , env ) {
|
|
48
|
+
const values = [];
|
|
49
|
+
const add = (value ) => { if (value && !values.includes(value)) values.push(value); };
|
|
50
|
+
|
|
51
|
+
if (platform === 'win32') {
|
|
52
|
+
for (const root of [env.PROGRAMFILES, env['PROGRAMFILES(X86)'], env.LOCALAPPDATA]) {
|
|
53
|
+
if (!root) continue;
|
|
54
|
+
add(path.win32.join(root, 'Google', 'Chrome', 'Application', 'chrome.exe'));
|
|
55
|
+
add(path.win32.join(root, 'Google', 'Chrome for Testing', 'Application', 'chrome.exe'));
|
|
56
|
+
add(path.win32.join(root, 'Microsoft', 'Edge', 'Application', 'msedge.exe'));
|
|
57
|
+
}
|
|
58
|
+
} else if (platform === 'darwin') {
|
|
59
|
+
add('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome');
|
|
60
|
+
add('/Applications/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing');
|
|
61
|
+
add('/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge');
|
|
62
|
+
} else {
|
|
63
|
+
add('/usr/bin/google-chrome');
|
|
64
|
+
add('/usr/bin/google-chrome-stable');
|
|
65
|
+
add('/usr/bin/chromium');
|
|
66
|
+
add('/usr/bin/chromium-browser');
|
|
67
|
+
add('/usr/bin/microsoft-edge');
|
|
68
|
+
add('/usr/bin/microsoft-edge-stable');
|
|
69
|
+
}
|
|
70
|
+
return values;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function candidateBrowserDataDirs(platform , env , homeDir ) {
|
|
74
|
+
const values = [];
|
|
75
|
+
const pathApi = platform === 'win32' ? path.win32 : path.posix;
|
|
76
|
+
const add = (value ) => {
|
|
77
|
+
if (!value || !pathApi.isAbsolute(value) || value === pathApi.parse(value).root || values.includes(value)) return;
|
|
78
|
+
values.push(value);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (platform === 'win32') {
|
|
82
|
+
const local = env.LOCALAPPDATA;
|
|
83
|
+
if (local) {
|
|
84
|
+
add(pathApi.join(local, 'Google', 'Chrome', 'User Data'));
|
|
85
|
+
add(pathApi.join(local, 'Google', 'Chrome Beta', 'User Data'));
|
|
86
|
+
add(pathApi.join(local, 'Google', 'Chrome SxS', 'User Data'));
|
|
87
|
+
add(pathApi.join(local, 'Microsoft', 'Edge', 'User Data'));
|
|
88
|
+
add(pathApi.join(local, 'Microsoft', 'Edge Beta', 'User Data'));
|
|
89
|
+
add(pathApi.join(local, 'Microsoft', 'Edge Dev', 'User Data'));
|
|
90
|
+
}
|
|
91
|
+
} else if (platform === 'darwin') {
|
|
92
|
+
add(pathApi.join(homeDir, 'Library', 'Application Support', 'Google', 'Chrome'));
|
|
93
|
+
add(pathApi.join(homeDir, 'Library', 'Application Support', 'Google', 'Chrome Beta'));
|
|
94
|
+
add(pathApi.join(homeDir, 'Library', 'Application Support', 'Google', 'Chrome Canary'));
|
|
95
|
+
add(pathApi.join(homeDir, 'Library', 'Application Support', 'Microsoft Edge'));
|
|
96
|
+
add(pathApi.join(homeDir, 'Library', 'Application Support', 'Microsoft Edge Beta'));
|
|
97
|
+
add(pathApi.join(homeDir, 'Library', 'Application Support', 'Microsoft Edge Dev'));
|
|
98
|
+
} else {
|
|
99
|
+
add(pathApi.join(homeDir, '.config', 'google-chrome'));
|
|
100
|
+
add(pathApi.join(homeDir, '.config', 'google-chrome-beta'));
|
|
101
|
+
add(pathApi.join(homeDir, '.config', 'google-chrome-unstable'));
|
|
102
|
+
add(pathApi.join(homeDir, '.config', 'chromium'));
|
|
103
|
+
add(pathApi.join(homeDir, '.config', 'microsoft-edge'));
|
|
104
|
+
add(pathApi.join(homeDir, '.config', 'microsoft-edge-beta'));
|
|
105
|
+
add(pathApi.join(homeDir, '.config', 'microsoft-edge-dev'));
|
|
106
|
+
}
|
|
107
|
+
return values;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function buildManagedBrowserArgs(dataDir ) {
|
|
111
|
+
const resolved = path.resolve(dataDir);
|
|
112
|
+
if (!path.isAbsolute(dataDir) || resolved === path.parse(resolved).root) {
|
|
113
|
+
throw new OperatorError('UNSAFE_BROWSER_DATA_DIR', 'Managed browser data directory must be a non-root absolute path.');
|
|
114
|
+
}
|
|
115
|
+
return [
|
|
116
|
+
'--remote-debugging-port=0',
|
|
117
|
+
`--user-data-dir=${resolved}`,
|
|
118
|
+
'--no-first-run',
|
|
119
|
+
'--no-default-browser-check',
|
|
120
|
+
'about:blank'
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function parseDevToolsActivePort(raw ) {
|
|
125
|
+
const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
126
|
+
const port = Number(lines[0]);
|
|
127
|
+
const browserPath = lines[1] ?? '';
|
|
128
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
129
|
+
throw new OperatorError('INVALID_DEVTOOLS_ACTIVE_PORT', 'DevToolsActivePort contains an invalid TCP port.');
|
|
130
|
+
}
|
|
131
|
+
if (!/^\/devtools\/browser\/[A-Za-z0-9._:-]+$/.test(browserPath)) {
|
|
132
|
+
throw new OperatorError('INVALID_DEVTOOLS_ACTIVE_PORT', 'DevToolsActivePort contains an invalid browser WebSocket path.');
|
|
133
|
+
}
|
|
134
|
+
return { port, browserPath, endpoint: `http://127.0.0.1:${port}` };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isLoopbackHost(hostname ) {
|
|
138
|
+
const host = hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
|
139
|
+
return host === '127.0.0.1' || host === 'localhost' || host === '::1';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function endpointHealthy(endpoint ) {
|
|
143
|
+
let url ;
|
|
144
|
+
try { url = new URL(endpoint); } catch { return false; }
|
|
145
|
+
try { assertLoopbackEndpoint(url); } catch { return false; }
|
|
146
|
+
try {
|
|
147
|
+
const response = await fetch(new URL('/json/version', url), { redirect: 'error', signal: AbortSignal.timeout(1_500) });
|
|
148
|
+
if (!response.ok) return false;
|
|
149
|
+
const payload = await response.json() ;
|
|
150
|
+
if (typeof payload.Browser !== 'string' || !/(Chrome|Chromium|Edg)/i.test(payload.Browser)) return false;
|
|
151
|
+
if (typeof payload.webSocketDebuggerUrl !== 'string') return false;
|
|
152
|
+
const websocket = new URL(payload.webSocketDebuggerUrl);
|
|
153
|
+
if (websocket.protocol !== 'ws:' || !isLoopbackHost(websocket.hostname)) return false;
|
|
154
|
+
if (!/^\/devtools\/browser\/[A-Za-z0-9._:-]+$/.test(websocket.pathname)) return false;
|
|
155
|
+
if (websocket.port && url.port && websocket.port !== url.port) return false;
|
|
156
|
+
return true;
|
|
157
|
+
} catch { return false; }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function discoverDevToolsEndpoint(dataDirs ) {
|
|
161
|
+
const seen = new Set ();
|
|
162
|
+
for (const candidate of dataDirs) {
|
|
163
|
+
if (!path.isAbsolute(candidate)) continue;
|
|
164
|
+
const dataDir = path.resolve(candidate);
|
|
165
|
+
if (dataDir === path.parse(dataDir).root || seen.has(dataDir)) continue;
|
|
166
|
+
seen.add(dataDir);
|
|
167
|
+
try {
|
|
168
|
+
const raw = await fs.readFile(path.join(dataDir, 'DevToolsActivePort'), 'utf8');
|
|
169
|
+
const parsed = parseDevToolsActivePort(raw);
|
|
170
|
+
if (await endpointHealthy(parsed.endpoint)) return { endpoint: parsed.endpoint, dataDir };
|
|
171
|
+
} catch { /* absent, stale, or malformed candidate; try the next bounded root */ }
|
|
172
|
+
}
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export class ManagedChromiumLauncher {
|
|
177
|
+
#options ;
|
|
178
|
+
#child ;
|
|
179
|
+
#launchedEndpoint ;
|
|
180
|
+
|
|
181
|
+
constructor(options = {}) {
|
|
182
|
+
this.#options = options;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async ensureEndpoint() {
|
|
186
|
+
const configured = this.#options.endpoint ?? 'http://127.0.0.1:9222';
|
|
187
|
+
if (await endpointHealthy(configured)) return configured;
|
|
188
|
+
if (this.#launchedEndpoint && await endpointHealthy(this.#launchedEndpoint)) return this.#launchedEndpoint;
|
|
189
|
+
|
|
190
|
+
const discovered = await discoverDevToolsEndpoint(this.#discoveryDataDirs());
|
|
191
|
+
if (discovered) {
|
|
192
|
+
this.#launchedEndpoint = discovered.endpoint;
|
|
193
|
+
return discovered.endpoint;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (this.#options.autoLaunch === false) {
|
|
197
|
+
throw new OperatorError('BROWSER_ENDPOINT_UNAVAILABLE', 'No healthy configured or discoverable local Chromium CDP endpoint is available and managed auto-launch is disabled.', { retryable: true });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const executable = await this.#resolveExecutable();
|
|
201
|
+
const dataDir = this.#managedDataDir();
|
|
202
|
+
const args = buildManagedBrowserArgs(dataDir);
|
|
203
|
+
await fs.mkdir(dataDir, { recursive: true, mode: 0o700 });
|
|
204
|
+
const portFile = path.join(dataDir, 'DevToolsActivePort');
|
|
205
|
+
await fs.rm(portFile, { force: true });
|
|
206
|
+
|
|
207
|
+
const child = spawn(executable, args, {
|
|
208
|
+
shell: false,
|
|
209
|
+
stdio: 'ignore',
|
|
210
|
+
windowsHide: true,
|
|
211
|
+
env: safeChildEnvironment('desktop')
|
|
212
|
+
});
|
|
213
|
+
this.#child = child;
|
|
214
|
+
|
|
215
|
+
const timeoutMs = Math.min(Math.max(this.#options.launchTimeoutMs ?? 12_000, 2_000), 60_000);
|
|
216
|
+
const deadline = Date.now() + timeoutMs;
|
|
217
|
+
let lastError ;
|
|
218
|
+
while (Date.now() < deadline) {
|
|
219
|
+
if (child.exitCode !== null) {
|
|
220
|
+
throw new OperatorError('BROWSER_LAUNCH_FAILED', `Managed browser exited before exposing CDP (exit ${child.exitCode}).`, { retryable: true });
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const parsed = parseDevToolsActivePort(await fs.readFile(portFile, 'utf8'));
|
|
224
|
+
if (await endpointHealthy(parsed.endpoint)) {
|
|
225
|
+
this.#launchedEndpoint = parsed.endpoint;
|
|
226
|
+
return parsed.endpoint;
|
|
227
|
+
}
|
|
228
|
+
} catch (error) {
|
|
229
|
+
lastError = error;
|
|
230
|
+
}
|
|
231
|
+
await new Promise((resolve) => setTimeout(resolve, 75));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
this.#terminateChild();
|
|
235
|
+
throw new OperatorError('BROWSER_LAUNCH_TIMEOUT', 'Managed browser did not expose a healthy CDP endpoint before timeout.', {
|
|
236
|
+
retryable: true,
|
|
237
|
+
details: { cause: lastError instanceof Error ? lastError.message : undefined }
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
close() {
|
|
242
|
+
this.#terminateChild();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
#managedDataDir() {
|
|
246
|
+
return path.resolve(this.#options.dataDir ?? path.join(os.homedir(), '.operator', 'browser-profile'));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
#discoveryDataDirs() {
|
|
250
|
+
const values = [this.#managedDataDir()];
|
|
251
|
+
for (const candidate of this.#options.discoveryDataDirs ?? []) {
|
|
252
|
+
if (!path.isAbsolute(candidate)) {
|
|
253
|
+
throw new OperatorError('UNSAFE_BROWSER_DISCOVERY_DIR', 'Browser discovery data directories must be absolute paths.');
|
|
254
|
+
}
|
|
255
|
+
values.push(path.resolve(candidate));
|
|
256
|
+
}
|
|
257
|
+
// Do not auto-attach to normal Chrome/Edge user profiles. A configured CDP endpoint
|
|
258
|
+
// or an explicit discoveryDataDirs entry is required for non-managed profiles.
|
|
259
|
+
return values;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async #resolveExecutable() {
|
|
263
|
+
const explicit = this.#options.executablePath;
|
|
264
|
+
if (explicit) {
|
|
265
|
+
if (!path.isAbsolute(explicit)) throw new OperatorError('UNSAFE_BROWSER_EXECUTABLE', 'Configured browser executable path must be absolute.');
|
|
266
|
+
try { await fs.access(explicit); return explicit; } catch {
|
|
267
|
+
throw new OperatorError('BROWSER_EXECUTABLE_NOT_FOUND', 'Configured browser executable does not exist.', { details: { path: explicit } });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
for (const candidate of candidateBrowserPaths(process.platform, process.env)) {
|
|
272
|
+
try { await fs.access(candidate); return candidate; } catch { /* next candidate */ }
|
|
273
|
+
}
|
|
274
|
+
throw new OperatorError('BROWSER_EXECUTABLE_NOT_FOUND', 'Chrome, Chrome for Testing, Chromium, or Edge was not found. Configure OPERATOR_BROWSER_PATH.', { retryable: false });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
#terminateChild() {
|
|
278
|
+
const child = this.#child;
|
|
279
|
+
this.#child = undefined;
|
|
280
|
+
this.#launchedEndpoint = undefined;
|
|
281
|
+
if (!child || child.exitCode !== null || child.killed) return;
|
|
282
|
+
try { child.kill('SIGTERM'); } catch { /* noop */ }
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export class ManagedBrowserProvider {
|
|
287
|
+
name = 'browser.managed';
|
|
288
|
+
#delegate ;
|
|
289
|
+
#launcher ;
|
|
290
|
+
#endpoint ;
|
|
291
|
+
|
|
292
|
+
constructor(options = {}) {
|
|
293
|
+
this.#endpoint = options.endpoint ?? 'http://127.0.0.1:9222';
|
|
294
|
+
this.#delegate = new BrowserCdpProvider(this.#endpoint);
|
|
295
|
+
this.#launcher = options.launcher ?? new ManagedChromiumLauncher(options);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
supports(action ) {
|
|
299
|
+
return this.#delegate.supports(action);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
score() { return SCORE; }
|
|
303
|
+
|
|
304
|
+
async execute(action ) {
|
|
305
|
+
const started = performance.now();
|
|
306
|
+
const first = await this.#delegate.execute(action);
|
|
307
|
+
if (first.ok || !first.error?.code || !RECOVERABLE_CDP_CODES.has(first.error.code)) {
|
|
308
|
+
return { ...first, provider: this.name };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const endpoint = await this.#launcher.ensureEndpoint();
|
|
313
|
+
if (endpoint !== this.#endpoint) {
|
|
314
|
+
this.#delegate.close();
|
|
315
|
+
this.#endpoint = endpoint;
|
|
316
|
+
this.#delegate = new BrowserCdpProvider(endpoint);
|
|
317
|
+
}
|
|
318
|
+
const retried = await this.#delegate.execute(action);
|
|
319
|
+
return {
|
|
320
|
+
...retried,
|
|
321
|
+
provider: this.name,
|
|
322
|
+
evidence: [
|
|
323
|
+
evidence('browser_lifecycle', retried.ok ? 'pass' : 'fail', retried.ok ? 'A local Chromium endpoint was recovered or discovered and the browser action was retried.' : 'A local Chromium endpoint was recovered or discovered but the browser action still failed.', { endpoint }),
|
|
324
|
+
...retried.evidence
|
|
325
|
+
],
|
|
326
|
+
durationMs: Math.round(performance.now() - started)
|
|
327
|
+
};
|
|
328
|
+
} catch (error) {
|
|
329
|
+
const op = error instanceof OperatorError ? error : new OperatorError('BROWSER_LAUNCH_FAILED', error instanceof Error ? error.message : String(error), { retryable: true });
|
|
330
|
+
return {
|
|
331
|
+
ok: false,
|
|
332
|
+
capability: action.capability,
|
|
333
|
+
provider: this.name,
|
|
334
|
+
evidence: [...first.evidence, evidence('browser_lifecycle', 'fail', op.message, { code: op.code })],
|
|
335
|
+
error: { code: op.code, message: op.message, retryable: op.retryable },
|
|
336
|
+
durationMs: Math.round(performance.now() - started)
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
close() {
|
|
342
|
+
this.#delegate.close();
|
|
343
|
+
this.#launcher.close();
|
|
344
|
+
}
|
|
345
|
+
}
|