@yeaft/webchat-agent 1.0.413 → 1.0.415

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.
Files changed (58) hide show
  1. package/browser-runtime/browser-install.js +497 -0
  2. package/browser-runtime/cli.js +88 -0
  3. package/browser-runtime/config.js +116 -0
  4. package/browser-runtime/errors.js +8 -0
  5. package/browser-runtime/extension/manifest.json +18 -0
  6. package/browser-runtime/extension/offscreen.html +5 -0
  7. package/browser-runtime/extension/offscreen.js +101 -0
  8. package/browser-runtime/extension/popup.html +5 -0
  9. package/browser-runtime/extension/popup.js +1 -0
  10. package/browser-runtime/extension/service-worker.js +48 -0
  11. package/browser-runtime/extension.js +45 -0
  12. package/browser-runtime/index.js +5 -0
  13. package/browser-runtime/probe.js +427 -0
  14. package/browser-runtime/protocol.js +71 -0
  15. package/browser-runtime/service.js +132 -0
  16. package/browser-runtime/windows-version-job.ps1 +233 -0
  17. package/browser-runtime/windows-version-worker.js +75 -0
  18. package/browser-runtime/windows-version.js +85 -0
  19. package/cli.js +24 -7
  20. package/connection/index.js +12 -0
  21. package/context.js +1 -0
  22. package/index.js +19 -2
  23. package/llm-config-cli.js +24 -21
  24. package/local-runtime/server/client-protocol.js +14 -0
  25. package/local-runtime/server/context.js +3 -2
  26. package/local-runtime/server/handlers/agent-file-terminal.js +185 -115
  27. package/local-runtime/server/handlers/agent-output.js +3 -0
  28. package/local-runtime/server/handlers/client-misc.js +21 -4
  29. package/local-runtime/server/handlers/client-workbench.js +222 -41
  30. package/local-runtime/server/workbench-correlation.js +184 -0
  31. package/local-runtime/server/workbench-route.js +180 -0
  32. package/local-runtime/server/ws-agent.js +4 -0
  33. package/local-runtime/server/ws-client.js +25 -3
  34. package/local-runtime/version.json +1 -1
  35. package/local-runtime/web/app.bundle.js +191 -135
  36. package/local-runtime/web/app.bundle.js.gz +0 -0
  37. package/local-runtime/web/index.html +2 -2
  38. package/local-runtime/web/style.bundle.css +1 -1
  39. package/local-runtime/web/style.bundle.css.gz +0 -0
  40. package/package.json +5 -1
  41. package/service/config.js +23 -2
  42. package/service/index.js +1 -0
  43. package/service/linux.js +3 -2
  44. package/terminal.js +167 -30
  45. package/workbench/file-ops.js +21 -20
  46. package/workbench/file-search.js +4 -3
  47. package/workbench/git-ops.js +23 -22
  48. package/workbench/request-routing.js +16 -0
  49. package/yeaft/cli.js +57 -1
  50. package/yeaft/config-api.js +138 -192
  51. package/yeaft/config-store.js +192 -0
  52. package/yeaft/config.js +3 -0
  53. package/yeaft/init.js +20 -7
  54. package/yeaft/sessions/feature-flag.js +15 -33
  55. package/yeaft/sessions/session-manifest.js +114 -10
  56. package/yeaft/stdio-protocol.js +57 -0
  57. package/yeaft/storage/atomic.js +43 -17
  58. package/yeaft/tools/process-runner.js +86 -13
@@ -0,0 +1,8 @@
1
+ export class BrowserRuntimeError extends Error {
2
+ constructor(code, message = code, details = null) {
3
+ super(message);
4
+ this.name = 'BrowserRuntimeError';
5
+ this.code = code;
6
+ this.details = details;
7
+ }
8
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "Yeaft Browser Runtime",
4
+ "version": "1.0.0",
5
+ "minimum_chrome_version": "151",
6
+ "background": {
7
+ "service_worker": "service-worker.js"
8
+ },
9
+ "action": {
10
+ "default_title": "Yeaft Browser Runtime",
11
+ "default_popup": "popup.html"
12
+ },
13
+ "permissions": [
14
+ "offscreen",
15
+ "storage",
16
+ "tabCapture"
17
+ ]
18
+ }
@@ -0,0 +1,5 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head><meta charset="utf-8"><title>Yeaft Browser Runtime Peer</title></head>
4
+ <body><video id="probe-video" muted autoplay playsinline></video><script src="offscreen.js"></script></body>
5
+ </html>
@@ -0,0 +1,101 @@
1
+ const PROBE_TIMEOUT_MS = 5_000;
2
+
3
+ function sleep(ms) {
4
+ return new Promise(resolve => setTimeout(resolve, ms));
5
+ }
6
+
7
+ async function inboundVideoStats(peer) {
8
+ const stats = await peer.getStats();
9
+ for (const report of stats.values()) {
10
+ if (report.type === 'inbound-rtp' && (report.kind === 'video' || report.mediaType === 'video')) {
11
+ const codec = stats.get(report.codecId);
12
+ return {
13
+ bytesReceived: Number(report.bytesReceived) || 0,
14
+ framesDecoded: Number(report.framesDecoded) || 0,
15
+ framesReceived: Number(report.framesReceived) || 0,
16
+ frameWidth: Number(report.frameWidth) || 0,
17
+ frameHeight: Number(report.frameHeight) || 0,
18
+ codec: typeof codec?.mimeType === 'string' ? codec.mimeType : null,
19
+ };
20
+ }
21
+ }
22
+ return null;
23
+ }
24
+
25
+ async function runMediaProbe(streamId) {
26
+ let stream = null;
27
+ let offerer = null;
28
+ let answerer = null;
29
+ try {
30
+ stream = await navigator.mediaDevices.getUserMedia({
31
+ audio: false,
32
+ video: {
33
+ mandatory: {
34
+ chromeMediaSource: 'tab',
35
+ chromeMediaSourceId: streamId,
36
+ },
37
+ },
38
+ });
39
+ const track = stream.getVideoTracks()[0];
40
+ if (!track) throw new Error('tab capture returned no video track');
41
+
42
+ offerer = new RTCPeerConnection();
43
+ answerer = new RTCPeerConnection();
44
+ offerer.onicecandidate = event => event.candidate && answerer.addIceCandidate(event.candidate);
45
+ answerer.onicecandidate = event => event.candidate && offerer.addIceCandidate(event.candidate);
46
+
47
+ const video = document.querySelector('#probe-video');
48
+ answerer.ontrack = event => {
49
+ video.srcObject = event.streams[0] || new MediaStream([event.track]);
50
+ };
51
+ const sender = offerer.addTrack(track, stream);
52
+ const vp8 = RTCRtpSender.getCapabilities('video')?.codecs
53
+ ?.filter(codec => codec.mimeType?.toLowerCase() === 'video/vp8') || [];
54
+ if (vp8.length === 0) throw new Error('VP8 encoder capability unavailable');
55
+ const transceiver = offerer.getTransceivers().find(item => item.sender === sender);
56
+ transceiver?.setCodecPreferences(vp8);
57
+ await offerer.setLocalDescription(await offerer.createOffer());
58
+ await answerer.setRemoteDescription(offerer.localDescription);
59
+ await answerer.setLocalDescription(await answerer.createAnswer());
60
+ await offerer.setRemoteDescription(answerer.localDescription);
61
+ await video.play().catch(() => {});
62
+
63
+ const deadline = Date.now() + PROBE_TIMEOUT_MS;
64
+ let media = null;
65
+ while (Date.now() < deadline) {
66
+ media = await inboundVideoStats(answerer);
67
+ if (media?.framesDecoded > 0) break;
68
+ await sleep(100);
69
+ }
70
+ if (!media || media.framesDecoded < 1) throw new Error('WebRTC decoded no tab-capture frame');
71
+ if (media.codec?.toLowerCase() !== 'video/vp8') throw new Error(`unexpected negotiated codec: ${media.codec || 'unknown'}`);
72
+ const settings = track.getSettings?.() || {};
73
+ return {
74
+ ok: true,
75
+ captureMode: 'tab',
76
+ codecBaseline: media.codec,
77
+ width: Number(settings.width) || media.frameWidth || 0,
78
+ height: Number(settings.height) || media.frameHeight || 0,
79
+ frameRate: Number(settings.frameRate) || 0,
80
+ framesDecoded: media.framesDecoded,
81
+ bytesReceived: media.bytesReceived,
82
+ };
83
+ } finally {
84
+ stream?.getTracks().forEach(track => track.stop());
85
+ offerer?.close();
86
+ answerer?.close();
87
+ const video = document.querySelector('#probe-video');
88
+ if (video) video.srcObject = null;
89
+ }
90
+ }
91
+
92
+ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
93
+ if (message?.target !== 'browser_runtime_offscreen'
94
+ || message?.type !== 'browser_runtime_probe_media') return undefined;
95
+ runMediaProbe(message.streamId).then(sendResponse, error => sendResponse({
96
+ ok: false,
97
+ code: 'media_probe_failed',
98
+ safeError: String(error?.message || error).slice(0, 500),
99
+ }));
100
+ return true;
101
+ });
@@ -0,0 +1,5 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head><meta charset="utf-8"><title>Yeaft Browser Runtime</title></head>
4
+ <body><script src="popup.js"></script></body>
5
+ </html>
@@ -0,0 +1 @@
1
+ chrome.runtime.sendMessage({ type: 'browser_runtime_probe_start' }).catch(() => {});
@@ -0,0 +1,48 @@
1
+ const OFFSCREEN_PATH = 'offscreen.html';
2
+ let offscreenCreation = null;
3
+
4
+ async function ensureOffscreenDocument() {
5
+ const documentUrl = chrome.runtime.getURL(OFFSCREEN_PATH);
6
+ const contexts = await chrome.runtime.getContexts({
7
+ contextTypes: ['OFFSCREEN_DOCUMENT'],
8
+ documentUrls: [documentUrl],
9
+ });
10
+ if (contexts.length > 0) return;
11
+ if (!offscreenCreation) {
12
+ offscreenCreation = chrome.offscreen.createDocument({
13
+ url: OFFSCREEN_PATH,
14
+ reasons: ['USER_MEDIA', 'WEB_RTC'],
15
+ justification: 'Capture the controlled tab and validate the Browser Runtime WebRTC media path',
16
+ }).finally(() => { offscreenCreation = null; });
17
+ }
18
+ await offscreenCreation;
19
+ }
20
+
21
+ async function runStartupProbe() {
22
+ const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
23
+ if (!tab?.id) throw new Error('active tab unavailable');
24
+ const streamId = await chrome.tabCapture.getMediaStreamId({ targetTabId: tab.id });
25
+ await ensureOffscreenDocument();
26
+ return chrome.runtime.sendMessage({
27
+ target: 'browser_runtime_offscreen',
28
+ type: 'browser_runtime_probe_media',
29
+ streamId,
30
+ });
31
+ }
32
+
33
+ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
34
+ if (message?.type !== 'browser_runtime_probe_start') return undefined;
35
+ runStartupProbe().then(async result => {
36
+ await chrome.storage.session.set({ browserRuntimeProbe: result });
37
+ sendResponse(result);
38
+ }, async error => {
39
+ const result = {
40
+ ok: false,
41
+ code: 'capture_probe_failed',
42
+ safeError: String(error?.message || error).slice(0, 500),
43
+ };
44
+ await chrome.storage.session.set({ browserRuntimeProbe: result });
45
+ sendResponse(result);
46
+ });
47
+ return true;
48
+ });
@@ -0,0 +1,45 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readdir, readFile, stat } from 'node:fs/promises';
3
+ import { dirname, join, relative } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
7
+ export const BROWSER_EXTENSION_DIR = join(moduleDir, 'extension');
8
+ export const BROWSER_EXTENSION_NAME = 'Yeaft Browser Runtime';
9
+ export const BROWSER_EXTENSION_SHA256 = '51cc6519ec9f72f86af7a0a98c3511c32e0e8fde6e8e3406886e7048f0c5972e';
10
+
11
+ async function walkFiles(root, current = root) {
12
+ const entries = await readdir(current, { withFileTypes: true });
13
+ const files = [];
14
+ for (const entry of entries) {
15
+ const path = join(current, entry.name);
16
+ if (entry.isDirectory()) files.push(...await walkFiles(root, path));
17
+ else if (entry.isFile()) files.push(path);
18
+ }
19
+ return files;
20
+ }
21
+
22
+ /**
23
+ * Hash the exact extension package before loading it. The digest is diagnostic
24
+ * evidence and a tamper fence, not a substitute for package signing.
25
+ */
26
+ export async function hashBrowserExtension(root = BROWSER_EXTENSION_DIR, { expectedDigest = null } = {}) {
27
+ const directory = await stat(root);
28
+ if (!directory.isDirectory()) throw new Error('Browser Runtime extension path is not a directory');
29
+ const hash = createHash('sha256');
30
+ const files = (await walkFiles(root)).sort();
31
+ if (files.length === 0) throw new Error('Browser Runtime extension is empty');
32
+ for (const path of files) {
33
+ const name = relative(root, path).replaceAll('\\', '/');
34
+ const data = await readFile(path);
35
+ hash.update(`${name}\0${data.byteLength}\0`);
36
+ hash.update(data);
37
+ }
38
+ const digest = hash.digest('hex');
39
+ if (expectedDigest && digest !== expectedDigest) {
40
+ const error = new Error('Browser Runtime extension digest mismatch');
41
+ error.code = 'extension_digest_mismatch';
42
+ throw error;
43
+ }
44
+ return { digest, fileCount: files.length, root };
45
+ }
@@ -0,0 +1,5 @@
1
+ export { BrowserRuntimeService, bootBrowserRuntime, getBrowserRuntime, shutdownBrowserRuntime } from './service.js';
2
+ export { probeBrowserRuntime } from './probe.js';
3
+ export { installManagedBrowser, findManagedBrowser, defaultBrowserCacheDir, BROWSER_RUNTIME_CHROME_BUILD } from './browser-install.js';
4
+ export { normaliseBrowserRuntimeSection, validateBrowserRuntimeUpdate } from './config.js';
5
+ export { ProducerSequenceState } from './protocol.js';
@@ -0,0 +1,427 @@
1
+ import { mkdir, mkdtemp, readdir, rm, stat } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import {
5
+ BROWSER_RUNTIME_CHROME_BUILD,
6
+ readBrowserExecutableVersion,
7
+ resolveBrowserExecutable,
8
+ } from './browser-install.js';
9
+ import {
10
+ hashBrowserExtension,
11
+ BROWSER_EXTENSION_DIR,
12
+ BROWSER_EXTENSION_NAME,
13
+ BROWSER_EXTENSION_SHA256,
14
+ } from './extension.js';
15
+ import { BrowserRuntimeError } from './errors.js';
16
+
17
+ const PROBE_PAGE = 'data:text/html,<style>body{margin:0;background:%2309637d;color:white;font:48px sans-serif}</style><main>Yeaft Browser Runtime</main>';
18
+ const PROFILE_STALE_MS = 30 * 60_000;
19
+ const CLEANUP_GRACE_MS = 250;
20
+ const VERSION_PROCESS_FORCE_MS = 500;
21
+
22
+ function abortError(signal) {
23
+ const reason = signal?.reason;
24
+ if (reason instanceof BrowserRuntimeError) return reason;
25
+ return new BrowserRuntimeError(
26
+ 'browser_probe_aborted',
27
+ reason instanceof Error ? reason.message : 'Browser Runtime probe aborted',
28
+ );
29
+ }
30
+
31
+ function deadlineError() {
32
+ return new BrowserRuntimeError('browser_probe_timeout', 'Browser Runtime probe exceeded its total deadline');
33
+ }
34
+
35
+ function remaining(deadline) {
36
+ const value = deadline - Date.now();
37
+ if (value <= 0) throw deadlineError();
38
+ return value;
39
+ }
40
+
41
+ function raceBoundary(promise, { deadline, signal, code = 'browser_probe_timeout' }) {
42
+ if (signal?.aborted) return Promise.reject(abortError(signal));
43
+ const timeoutMs = remaining(deadline);
44
+ let timer;
45
+ let abortHandler;
46
+ const boundary = new Promise((_, reject) => {
47
+ timer = setTimeout(() => reject(new BrowserRuntimeError(
48
+ code,
49
+ `Browser Runtime probe exceeded its total deadline after ${timeoutMs}ms`,
50
+ )), timeoutMs);
51
+ abortHandler = () => reject(abortError(signal));
52
+ signal?.addEventListener('abort', abortHandler, { once: true });
53
+ });
54
+ return Promise.race([Promise.resolve(promise), boundary]).finally(() => {
55
+ clearTimeout(timer);
56
+ if (abortHandler) signal?.removeEventListener('abort', abortHandler);
57
+ });
58
+ }
59
+
60
+ function delay(ms, boundary) {
61
+ return raceBoundary(new Promise(resolve => setTimeout(resolve, ms)), boundary);
62
+ }
63
+
64
+ function actualChromeBuild(version) {
65
+ const match = String(version || '').match(/(?:Chrome(?:\s+for\s+Testing)?|Chromium)(?:\/|\s+)(\d+(?:\.\d+){0,3})/i);
66
+ return match?.[1] || null;
67
+ }
68
+
69
+ async function readProbeResult(worker, boundary) {
70
+ for (;;) {
71
+ const result = await raceBoundary(worker.evaluate(() => {
72
+ const storage = globalThis.chrome?.storage?.session;
73
+ return storage ? storage.get('browserRuntimeProbe') : null;
74
+ }), { ...boundary, code: 'probe_result_timeout' });
75
+ if (result?.browserRuntimeProbe) return result.browserRuntimeProbe;
76
+ await delay(100, boundary);
77
+ }
78
+ }
79
+
80
+ function browserProcess(browser) {
81
+ try { return browser?.process?.() || null; } catch { return null; }
82
+ }
83
+
84
+ function forceKillBrowser(browser) {
85
+ const child = browserProcess(browser);
86
+ if (!child || child.exitCode !== null) return child;
87
+ try {
88
+ if (process.platform !== 'win32' && Number.isInteger(child.pid)) process.kill(-child.pid, 'SIGKILL');
89
+ else child.kill('SIGKILL');
90
+ } catch {
91
+ try { child.kill('SIGKILL'); } catch {}
92
+ }
93
+ return child;
94
+ }
95
+
96
+ async function settleWithin(promise, { deadline, signal, maxWaitMs = CLEANUP_GRACE_MS }) {
97
+ if (signal?.aborted) return false;
98
+ const budget = Math.min(maxWaitMs, Math.max(0, deadline - Date.now()));
99
+ if (budget <= 0) return false;
100
+ let timer;
101
+ let abortHandler;
102
+ try {
103
+ return await Promise.race([
104
+ Promise.resolve(promise).catch(() => false),
105
+ new Promise(resolve => { timer = setTimeout(() => resolve(false), budget); }),
106
+ new Promise(resolve => {
107
+ abortHandler = () => resolve(false);
108
+ signal?.addEventListener('abort', abortHandler, { once: true });
109
+ }),
110
+ ]);
111
+ } finally {
112
+ clearTimeout(timer);
113
+ if (abortHandler) signal?.removeEventListener('abort', abortHandler);
114
+ }
115
+ }
116
+
117
+ async function waitForProcessExit(child, boundary) {
118
+ if (!child || child.exitCode !== null) return true;
119
+ if (typeof child.once !== 'function') return false;
120
+ const exited = new Promise(resolve => {
121
+ const onExit = () => {
122
+ child.off?.('exit', onExit);
123
+ child.off?.('close', onExit);
124
+ resolve(true);
125
+ };
126
+ child.once('exit', onExit);
127
+ child.once('close', onExit);
128
+ if (child.exitCode !== null) onExit();
129
+ });
130
+ return settleWithin(exited, boundary);
131
+ }
132
+
133
+ async function cleanupBrowser(browser, profileDir, boundary, removeProfile = rm) {
134
+ if (browser) {
135
+ let closeCompletion;
136
+ try {
137
+ closeCompletion = Promise.resolve(browser.close()).then(() => true, () => false);
138
+ } catch {
139
+ closeCompletion = Promise.resolve(false);
140
+ }
141
+ const closed = await settleWithin(closeCompletion, boundary);
142
+ if (!closed) {
143
+ const child = forceKillBrowser(browser);
144
+ if (Math.max(0, boundary.deadline - Date.now()) > 0) {
145
+ await waitForProcessExit(child, boundary);
146
+ }
147
+ }
148
+ }
149
+ if (profileDir) {
150
+ let removal;
151
+ try {
152
+ removal = Promise.resolve(removeProfile(profileDir, { recursive: true, force: true }))
153
+ .then(() => true, () => false);
154
+ } catch {
155
+ removal = Promise.resolve(false);
156
+ }
157
+ if (!await settleWithin(removal, boundary)) removal.catch(() => {});
158
+ }
159
+ }
160
+
161
+ function baseResult(startedAt, extension, actualBuildId = null) {
162
+ return {
163
+ expectedBuildId: BROWSER_RUNTIME_CHROME_BUILD,
164
+ actualBuildId,
165
+ ...(extension ? {
166
+ extensionDigest: extension.digest,
167
+ extensionFileCount: extension.fileCount,
168
+ } : {}),
169
+ durationMs: Date.now() - startedAt,
170
+ };
171
+ }
172
+
173
+ /**
174
+ * Launch an isolated Chrome for Testing process and prove the complete primary
175
+ * media path under one total deadline. The profile and process are owned until
176
+ * cleanup finishes, including late launch completion after timeout/abort.
177
+ */
178
+ export async function probeBrowserRuntime({
179
+ executablePath = null,
180
+ cacheDir,
181
+ headless = true,
182
+ timeoutMs = 20_000,
183
+ extensionDir = BROWSER_EXTENSION_DIR,
184
+ expectedExtensionDigest = extensionDir === BROWSER_EXTENSION_DIR ? BROWSER_EXTENSION_SHA256 : null,
185
+ launch = null,
186
+ signal = null,
187
+ resolveExecutable = resolveBrowserExecutable,
188
+ versionCheck = readBrowserExecutableVersion,
189
+ hashExtension = hashBrowserExtension,
190
+ createProfile = mkdtemp,
191
+ removeProfile = rm,
192
+ profileParent = tmpdir(),
193
+ } = {}) {
194
+ const startedAt = Date.now();
195
+ const deadline = startedAt + Math.max(1, Number(timeoutMs) || 1);
196
+ const deadlineController = new AbortController();
197
+ const deadlineTimer = setTimeout(() => deadlineController.abort(deadlineError()), Math.max(1, deadline - Date.now()));
198
+ const probeController = new AbortController();
199
+ const abortProbe = reason => {
200
+ if (!probeController.signal.aborted) probeController.abort(reason);
201
+ };
202
+ const abortFromCaller = () => abortProbe(signal?.reason || abortError(signal));
203
+ const abortFromDeadline = () => abortProbe(deadlineController.signal.reason || deadlineError());
204
+ signal?.addEventListener('abort', abortFromCaller, { once: true });
205
+ deadlineController.signal.addEventListener('abort', abortFromDeadline, { once: true });
206
+ if (signal?.aborted) abortFromCaller();
207
+ const boundarySignal = probeController.signal;
208
+ let extension = null;
209
+ let actualBuildId = null;
210
+ let profileDir = null;
211
+ let browser = null;
212
+ let launchPromise = null;
213
+ let launchBoundaryFailed = false;
214
+
215
+ try {
216
+ boundarySignal.throwIfAborted();
217
+ const resolvedExecutable = await raceBoundary(
218
+ resolveExecutable({ executablePath, cacheDir }),
219
+ { deadline, signal: boundarySignal, code: 'browser_executable_timeout' },
220
+ );
221
+ if (!resolvedExecutable) {
222
+ return {
223
+ ok: false,
224
+ code: 'browser_executable_missing',
225
+ ...baseResult(startedAt, null),
226
+ };
227
+ }
228
+
229
+ let version;
230
+ const versionTerminationDeadline = deadline + VERSION_PROCESS_FORCE_MS;
231
+ const versionPromise = Promise.resolve().then(() => versionCheck(resolvedExecutable, {
232
+ signal: boundarySignal,
233
+ gracefulTerminationDeadline: deadline,
234
+ terminationDeadline: versionTerminationDeadline,
235
+ }));
236
+ try {
237
+ version = await raceBoundary(
238
+ versionPromise,
239
+ { deadline, signal: boundarySignal, code: 'browser_version_timeout' },
240
+ );
241
+ } catch (error) {
242
+ if (!boundarySignal.aborted) abortProbe(error);
243
+ await Promise.race([
244
+ versionPromise.catch(() => {}),
245
+ new Promise(resolve => setTimeout(
246
+ resolve,
247
+ Math.max(0, versionTerminationDeadline - Date.now()),
248
+ )),
249
+ ]);
250
+ throw error;
251
+ } finally {
252
+ if (Date.now() >= deadline && !boundarySignal.aborted) abortProbe(deadlineError());
253
+ }
254
+ actualBuildId = actualChromeBuild(version);
255
+ if (actualBuildId !== BROWSER_RUNTIME_CHROME_BUILD) {
256
+ throw new BrowserRuntimeError(
257
+ 'browser_version_mismatch',
258
+ `Browser Runtime requires Chrome ${BROWSER_RUNTIME_CHROME_BUILD}, got ${version || 'unknown'}`,
259
+ );
260
+ }
261
+
262
+ extension = await raceBoundary(
263
+ hashExtension(extensionDir, { expectedDigest: expectedExtensionDigest }),
264
+ { deadline, signal: boundarySignal, code: 'extension_probe_timeout' },
265
+ );
266
+ await raceBoundary(mkdir(profileParent, { recursive: true, mode: 0o700 }), {
267
+ deadline,
268
+ signal: boundarySignal,
269
+ code: 'probe_profile_timeout',
270
+ });
271
+ if (timeoutMs >= 1_000) {
272
+ const profiles = await raceBoundary(readdir(profileParent, { withFileTypes: true }), {
273
+ deadline,
274
+ signal: boundarySignal,
275
+ code: 'probe_profile_timeout',
276
+ });
277
+ await raceBoundary(Promise.all(profiles
278
+ .filter(entry => entry.isDirectory() && entry.name.startsWith('probe-'))
279
+ .map(async entry => {
280
+ const profilePath = join(profileParent, entry.name);
281
+ const details = await stat(profilePath);
282
+ if (Date.now() - details.mtimeMs > PROFILE_STALE_MS) {
283
+ await rm(profilePath, { recursive: true, force: true });
284
+ }
285
+ })), { deadline, signal: boundarySignal, code: 'probe_profile_timeout' });
286
+ }
287
+ profileDir = await raceBoundary(
288
+ createProfile(join(profileParent, 'probe-')),
289
+ { deadline, signal: boundarySignal, code: 'probe_profile_timeout' },
290
+ );
291
+
292
+ const launchBrowser = launch || (await raceBoundary(
293
+ import('puppeteer-core').then(module => module.default.launch),
294
+ { deadline, signal: boundarySignal, code: 'browser_launch_import_timeout' },
295
+ ));
296
+ const launchTimeout = remaining(deadline);
297
+ launchPromise = Promise.resolve(launchBrowser({
298
+ executablePath: resolvedExecutable,
299
+ headless,
300
+ userDataDir: profileDir,
301
+ acceptInsecureCerts: false,
302
+ timeout: launchTimeout,
303
+ protocolTimeout: launchTimeout,
304
+ signal: boundarySignal,
305
+ args: [
306
+ `--disable-extensions-except=${extensionDir}`,
307
+ `--load-extension=${extensionDir}`,
308
+ '--disable-background-networking',
309
+ '--disable-component-update',
310
+ '--disable-default-apps',
311
+ '--disable-features=MediaRouter,Translate',
312
+ '--disable-sync',
313
+ '--no-first-run',
314
+ ],
315
+ }));
316
+ try {
317
+ browser = await raceBoundary(launchPromise, { deadline, signal: boundarySignal, code: 'browser_launch_timeout' });
318
+ } catch (error) {
319
+ launchBoundaryFailed = true;
320
+ throw error;
321
+ }
322
+
323
+ const launchedVersion = await raceBoundary(browser.version(), {
324
+ deadline,
325
+ signal: boundarySignal,
326
+ code: 'browser_version_timeout',
327
+ });
328
+ const launchedBuildId = actualChromeBuild(launchedVersion);
329
+ if (launchedBuildId !== actualBuildId) {
330
+ throw new BrowserRuntimeError(
331
+ 'browser_version_mismatch',
332
+ `Browser Runtime executable changed after preflight: expected ${actualBuildId}, got ${launchedVersion || 'unknown'}`,
333
+ );
334
+ }
335
+
336
+ const page = await raceBoundary(browser.newPage(), {
337
+ deadline,
338
+ signal: boundarySignal,
339
+ code: 'probe_page_create_timeout',
340
+ });
341
+ await raceBoundary(page.goto(PROBE_PAGE, {
342
+ waitUntil: 'domcontentloaded',
343
+ timeout: remaining(deadline),
344
+ }), { deadline, signal: boundarySignal, code: 'probe_page_timeout' });
345
+
346
+ let installed = null;
347
+ while (!installed) {
348
+ const extensions = await raceBoundary(browser.extensions(), {
349
+ deadline,
350
+ signal: boundarySignal,
351
+ code: 'extension_list_timeout',
352
+ });
353
+ installed = [...extensions.values()].find(candidate => candidate.name === BROWSER_EXTENSION_NAME) || null;
354
+ if (!installed) await delay(100, { deadline, signal: boundarySignal });
355
+ }
356
+
357
+ await raceBoundary(page.triggerExtensionAction(installed), {
358
+ deadline,
359
+ signal: boundarySignal,
360
+ code: 'extension_action_timeout',
361
+ });
362
+ const workerTarget = await raceBoundary(browser.waitForTarget(target => (
363
+ target.type() === 'service_worker' && target.url().startsWith(`chrome-extension://${installed.id}/`)
364
+ ), { timeout: remaining(deadline), signal: boundarySignal }), {
365
+ deadline,
366
+ signal: boundarySignal,
367
+ code: 'extension_worker_timeout',
368
+ });
369
+ const worker = await raceBoundary(workerTarget.worker(), {
370
+ deadline,
371
+ signal: boundarySignal,
372
+ code: 'extension_worker_timeout',
373
+ });
374
+ if (!worker) throw new BrowserRuntimeError('extension_worker_missing', 'Browser Runtime extension worker unavailable');
375
+ const media = await readProbeResult(worker, { deadline, signal: boundarySignal });
376
+ if (media.ok !== true || media.framesDecoded < 1) {
377
+ throw new BrowserRuntimeError(media.code || 'media_probe_failed', media.safeError || 'Browser Runtime media probe failed');
378
+ }
379
+
380
+ return {
381
+ ok: true,
382
+ executablePath: resolvedExecutable,
383
+ captureMode: media.captureMode,
384
+ codecBaseline: media.codecBaseline,
385
+ width: media.width,
386
+ height: media.height,
387
+ frameRate: media.frameRate,
388
+ framesDecoded: media.framesDecoded,
389
+ ...baseResult(startedAt, extension, actualBuildId),
390
+ };
391
+ } catch (error) {
392
+ return {
393
+ ok: false,
394
+ code: error?.code || (signal?.aborted ? 'browser_probe_aborted' : 'browser_probe_failed'),
395
+ safeError: String(error?.message || error).slice(0, 500),
396
+ ...baseResult(startedAt, extension, actualBuildId),
397
+ };
398
+ } finally {
399
+ clearTimeout(deadlineTimer);
400
+ signal?.removeEventListener('abort', abortFromCaller);
401
+ deadlineController.signal.removeEventListener('abort', abortFromDeadline);
402
+ if (!browser && launchBoundaryFailed && launchPromise) {
403
+ const lateBudget = Math.min(CLEANUP_GRACE_MS, Math.max(0, deadline - Date.now()));
404
+ if (lateBudget > 0) {
405
+ try {
406
+ browser = await Promise.race([
407
+ launchPromise,
408
+ new Promise(resolve => setTimeout(() => resolve(null), lateBudget)),
409
+ ]);
410
+ } catch {}
411
+ }
412
+ }
413
+ await cleanupBrowser(browser, profileDir, {
414
+ deadline,
415
+ signal: (signal?.aborted || Date.now() >= deadline) ? boundarySignal : null,
416
+ }, removeProfile);
417
+ if (!browser && launchBoundaryFailed && launchPromise) {
418
+ launchPromise.then(lateBrowser => {
419
+ forceKillBrowser(lateBrowser);
420
+ void cleanupBrowser(lateBrowser, profileDir, {
421
+ deadline: Date.now(),
422
+ signal: boundarySignal,
423
+ }, removeProfile);
424
+ }, () => {});
425
+ }
426
+ }
427
+ }