@rynx-ai/daemon 0.1.11-beta.45 → 0.1.11-beta.49
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/bundled-plugins/plugins/lark/dist/runtime.js +32 -26
- package/bundled-plugins/plugins/lark/dist/settings.js +17 -11
- package/bundled-plugins/plugins/lark/package.json +1 -1
- package/dist/app-browser-host-supervisor.js +6 -1
- package/dist/daemon-server.js +23 -7
- package/dist/headless-browser-host.js +114 -42
- package/dist/log-files.d.ts +9 -0
- package/dist/log-files.js +96 -0
- package/dist/page-agent-browser-connection.d.ts +6 -0
- package/dist/page-agent-browser-connection.js +71 -0
- package/dist/page-agent-browser-driver.d.ts +14 -0
- package/dist/page-agent-browser-driver.js +216 -0
- package/dist/page-agent-browser-policy.d.ts +3 -0
- package/dist/page-agent-browser-policy.js +37 -0
- package/dist/page-automation-manager.d.ts +29 -0
- package/dist/page-automation-manager.js +144 -0
- package/dist/page-cdp-gateway.d.ts +5 -0
- package/dist/page-cdp-gateway.js +205 -0
- package/dist/plugin-host-rpc.js +44 -0
- package/package.json +12 -11
|
@@ -3,13 +3,14 @@ import { execFile, spawn } from "node:child_process";
|
|
|
3
3
|
import { accessSync, chmodSync, closeSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync, } from "node:fs";
|
|
4
4
|
import { constants as fsConstants } from "node:fs";
|
|
5
5
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
|
-
import { rynxHome } from "@rynx-ai/core";
|
|
6
|
+
import { rynxHome, diagnosticEvents, sessionDiagnosticDir, teeDiagnosticStream } from "@rynx-ai/core";
|
|
7
7
|
import { RUNTIME_BROWSER_MAX_PAGES, RUNTIME_BROWSER_MAX_TITLE_CHARS, RUNTIME_BROWSER_MAX_URL_CHARS, } from "@rynx-ai/protocol/runtime-browser";
|
|
8
8
|
import { RUNTIME_BROWSER_SURFACE_MAX_ENCODED_FRAME_BYTES, RUNTIME_BROWSER_SURFACE_MAX_FRAME_RATE, RUNTIME_BROWSER_SURFACE_MAX_VIEWPORT_HEIGHT, RUNTIME_BROWSER_SURFACE_MAX_VIEWPORT_PIXELS, RUNTIME_BROWSER_SURFACE_MAX_VIEWPORT_WIDTH, } from "@rynx-ai/protocol/runtime-browser-surface";
|
|
9
9
|
import { SessionBrowserHostError, } from "@rynx-ai/server";
|
|
10
10
|
import WebSocket from "ws";
|
|
11
11
|
import { createChromeForTestingArtifactStore, } from "./chrome-for-testing-store.js";
|
|
12
12
|
import { cdpKeyboardEventParams } from "./cdp-keyboard-input.js";
|
|
13
|
+
import { createPageCdpGateway } from "./page-cdp-gateway.js";
|
|
13
14
|
const PROFILE_OWNER_FILE = ".rynx-headless-browser-profile.json";
|
|
14
15
|
const PROCESS_LEASE_FILE = ".rynx-headless-browser-process.json";
|
|
15
16
|
const DEVTOOLS_ACTIVE_PORT_FILE = "DevToolsActivePort";
|
|
@@ -124,6 +125,13 @@ class HeadlessBrowserHost {
|
|
|
124
125
|
throw unavailable("Managed Chrome for Testing could not be launched", error);
|
|
125
126
|
}
|
|
126
127
|
const observed = observeProcess(child);
|
|
128
|
+
const logRoot = sessionDiagnosticDir("browser", input.sessionId);
|
|
129
|
+
const instance = String(input.browserGeneration);
|
|
130
|
+
const event = diagnosticEvents(join(logRoot, "browser-host.log"), { sessionId: input.sessionId, instance });
|
|
131
|
+
teeDiagnosticStream(child.stderr, join(logRoot, "instances", instance, "chrome.stderr.log"));
|
|
132
|
+
event("chrome.spawn", { pid: child.pid });
|
|
133
|
+
child.on("exit", (code, signal) => event("chrome.exit", { code, signal }));
|
|
134
|
+
child.on("error", (error) => event("chrome.error", { message: error.message }));
|
|
127
135
|
const diagnostics = boundedStderr(child);
|
|
128
136
|
let connection;
|
|
129
137
|
let handle;
|
|
@@ -217,6 +225,7 @@ class HeadlessBrowserHandle {
|
|
|
217
225
|
networkTargetTypeBySession = new Map();
|
|
218
226
|
surfaceSources = new Map();
|
|
219
227
|
loadingTargets = new Set();
|
|
228
|
+
automationGateways = new Map();
|
|
220
229
|
activeTargetId = null;
|
|
221
230
|
unavailableReason;
|
|
222
231
|
closing = false;
|
|
@@ -224,8 +233,12 @@ class HeadlessBrowserHandle {
|
|
|
224
233
|
operations = Promise.resolve();
|
|
225
234
|
networkOperations = Promise.resolve();
|
|
226
235
|
requestHeaders;
|
|
236
|
+
diagnostic;
|
|
227
237
|
constructor(host) {
|
|
228
238
|
this.host = host;
|
|
239
|
+
this.diagnostic = diagnosticEvents(join(sessionDiagnosticDir("browser", host.input.sessionId), "page-events.jsonl"), {
|
|
240
|
+
sessionId: host.input.sessionId, browserGeneration: host.input.browserGeneration,
|
|
241
|
+
});
|
|
229
242
|
this.requestHeaders = cloneRequestHeadersConfig(host.input.requestHeaders ?? { enabled: false, headers: [] });
|
|
230
243
|
this.events = this.eventQueue;
|
|
231
244
|
this.host.connection.setEventHandler((event) => this.onCdpEvent(event));
|
|
@@ -253,7 +266,8 @@ class HeadlessBrowserHandle {
|
|
|
253
266
|
throw new Error("Headless Browser startup Page is unavailable");
|
|
254
267
|
const pageSessionId = await this.pageSession(targetId);
|
|
255
268
|
await this.host.connection.command("Page.navigate", { url: requestedUrl }, pageSessionId);
|
|
256
|
-
|
|
269
|
+
// Do not reset navigation history while child frames are committing.
|
|
270
|
+
// Chrome can discard a pending OOPIF navigation and leave it blank.
|
|
257
271
|
}
|
|
258
272
|
}
|
|
259
273
|
async getCdpEndpoint() {
|
|
@@ -276,6 +290,53 @@ class HeadlessBrowserHandle {
|
|
|
276
290
|
engineVersion: this.host.engineVersion,
|
|
277
291
|
};
|
|
278
292
|
}
|
|
293
|
+
async getPageAutomationEndpoint(hostPageId) {
|
|
294
|
+
this.ensureAvailable();
|
|
295
|
+
const target = this.requireTarget(hostPageId);
|
|
296
|
+
let gateway = this.automationGateways.get(hostPageId);
|
|
297
|
+
if (!gateway) {
|
|
298
|
+
gateway = createPageCdpGateway(this.host.endpoint, target.targetId, () => !this.closing && !this.unavailableReason && this.targets.has(hostPageId));
|
|
299
|
+
this.automationGateways.set(hostPageId, gateway);
|
|
300
|
+
void gateway.catch(() => { if (this.automationGateways.get(hostPageId) === gateway)
|
|
301
|
+
this.automationGateways.delete(hostPageId); });
|
|
302
|
+
}
|
|
303
|
+
return { endpoint: (await gateway).endpoint, engineVersion: this.host.engineVersion, pageTargetId: target.targetId };
|
|
304
|
+
}
|
|
305
|
+
acquireAutomationVisibility(hostPageId, canActivate = () => true) {
|
|
306
|
+
return this.exclusive(async () => {
|
|
307
|
+
this.ensureAvailable();
|
|
308
|
+
this.requireTarget(hostPageId);
|
|
309
|
+
if (!canActivate())
|
|
310
|
+
return { release: async () => undefined };
|
|
311
|
+
const previousTargetId = this.activeTargetId;
|
|
312
|
+
try {
|
|
313
|
+
await this.host.connection.command("Target.activateTarget", { targetId: hostPageId });
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
throw this.mapOperationError(error);
|
|
317
|
+
}
|
|
318
|
+
let released = false;
|
|
319
|
+
return {
|
|
320
|
+
release: async (restoreFocus = true) => {
|
|
321
|
+
if (released)
|
|
322
|
+
return;
|
|
323
|
+
released = true;
|
|
324
|
+
await this.exclusive(async () => {
|
|
325
|
+
// A user selection or closed generation supersedes the temporary activation.
|
|
326
|
+
if (!restoreFocus || !canActivate() || this.closing || this.unavailableReason || !previousTargetId ||
|
|
327
|
+
this.activeTargetId !== previousTargetId || !this.targets.has(previousTargetId))
|
|
328
|
+
return;
|
|
329
|
+
try {
|
|
330
|
+
await this.host.connection.command("Target.activateTarget", { targetId: previousTargetId });
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
throw this.mapOperationError(error);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
});
|
|
339
|
+
}
|
|
279
340
|
openSurface(input) {
|
|
280
341
|
return this.exclusive(async () => {
|
|
281
342
|
this.ensureAvailable();
|
|
@@ -361,7 +422,8 @@ class HeadlessBrowserHandle {
|
|
|
361
422
|
if (url !== "about:blank") {
|
|
362
423
|
const pageSessionId = await this.pageSession(targetId);
|
|
363
424
|
await this.host.connection.command("Page.navigate", { url }, pageSessionId);
|
|
364
|
-
|
|
425
|
+
// Leave Chrome's native history intact. Resetting it here can
|
|
426
|
+
// discard a pending cross-origin child navigation.
|
|
365
427
|
}
|
|
366
428
|
this.eventQueue.changed(this.browserGeneration);
|
|
367
429
|
}
|
|
@@ -435,6 +497,8 @@ class HeadlessBrowserHandle {
|
|
|
435
497
|
return this.closePromise;
|
|
436
498
|
this.closing = true;
|
|
437
499
|
this.closePromise = (async () => {
|
|
500
|
+
await Promise.allSettled([...this.automationGateways.values()].map(async (gateway) => (await gateway).close()));
|
|
501
|
+
this.automationGateways.clear();
|
|
438
502
|
await Promise.allSettled([...this.surfaceSources.values()].map((source) => source.close()));
|
|
439
503
|
let terminated = false;
|
|
440
504
|
try {
|
|
@@ -483,24 +547,6 @@ class HeadlessBrowserHandle {
|
|
|
483
547
|
}
|
|
484
548
|
});
|
|
485
549
|
}
|
|
486
|
-
async resetPageNavigationHistory(targetId) {
|
|
487
|
-
let lastError;
|
|
488
|
-
for (let attempt = 0; attempt < PAGE_SNAPSHOT_TRANSITION_ATTEMPTS; attempt += 1) {
|
|
489
|
-
const sessionId = await this.pageSession(targetId);
|
|
490
|
-
try {
|
|
491
|
-
await this.host.connection.command("Page.resetNavigationHistory", {}, sessionId);
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
catch (error) {
|
|
495
|
-
if (!isMissingTargetError(error) && !isTransientPageTransitionError(error))
|
|
496
|
-
throw error;
|
|
497
|
-
lastError = error;
|
|
498
|
-
this.dropAttachedSession(sessionId);
|
|
499
|
-
await delay(PAGE_SNAPSHOT_TRANSITION_POLL_MS);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
throw lastError ?? new Error("Headless Browser could not reset Page navigation history");
|
|
503
|
-
}
|
|
504
550
|
pageCommand(hostPageId, method, params) {
|
|
505
551
|
return this.exclusive(async () => {
|
|
506
552
|
this.ensureAvailable();
|
|
@@ -624,9 +670,11 @@ class HeadlessBrowserHandle {
|
|
|
624
670
|
for (let attempt = 0; attempt < PAGE_SNAPSHOT_TRANSITION_ATTEMPTS; attempt += 1) {
|
|
625
671
|
let history;
|
|
626
672
|
try {
|
|
627
|
-
history = parseNavigationHistory(await this.host.connection.command("Page.getNavigationHistory", {}, sessionId));
|
|
673
|
+
history = parseNavigationHistory(await this.host.connection.command("Page.getNavigationHistory", {}, sessionId, 500));
|
|
628
674
|
}
|
|
629
675
|
catch (error) {
|
|
676
|
+
if (error instanceof CdpCommandTimeoutError)
|
|
677
|
+
break;
|
|
630
678
|
if (!isTransientPageTransitionError(error))
|
|
631
679
|
throw error;
|
|
632
680
|
if (attempt < PAGE_SNAPSHOT_TRANSITION_ATTEMPTS - 1) {
|
|
@@ -634,21 +682,14 @@ class HeadlessBrowserHandle {
|
|
|
634
682
|
}
|
|
635
683
|
continue;
|
|
636
684
|
}
|
|
637
|
-
let ready = "loading";
|
|
638
|
-
try {
|
|
639
|
-
ready = parseReadyState(await this.host.connection.command("Runtime.evaluate", { expression: "document.readyState", returnByValue: true }, sessionId));
|
|
640
|
-
}
|
|
641
|
-
catch (error) {
|
|
642
|
-
if (!isTransientPageTransitionError(error))
|
|
643
|
-
throw error;
|
|
644
|
-
}
|
|
645
685
|
const current = history.entries[history.currentIndex];
|
|
646
686
|
const currentTarget = this.targets.get(target.targetId) ?? target;
|
|
647
687
|
return {
|
|
648
688
|
hostPageId: target.targetId,
|
|
649
689
|
url: displayUrl(current?.url ?? currentTarget.url),
|
|
650
690
|
title: displayTitle(currentTarget.title),
|
|
651
|
-
|
|
691
|
+
// Renderer JS can be paused by DevTools or beforeunload while Chrome is healthy.
|
|
692
|
+
loading: this.loadingTargets.has(target.targetId),
|
|
652
693
|
canGoBack: history.currentIndex > 0,
|
|
653
694
|
canGoForward: history.currentIndex >= 0 && history.currentIndex < history.entries.length - 1,
|
|
654
695
|
};
|
|
@@ -662,6 +703,7 @@ class HeadlessBrowserHandle {
|
|
|
662
703
|
url: displayUrl((this.targets.get(target.targetId) ?? target).url),
|
|
663
704
|
title: displayTitle((this.targets.get(target.targetId) ?? target).title),
|
|
664
705
|
loading: true,
|
|
706
|
+
observationStale: true,
|
|
665
707
|
canGoBack: false,
|
|
666
708
|
canGoForward: false,
|
|
667
709
|
};
|
|
@@ -704,6 +746,23 @@ class HeadlessBrowserHandle {
|
|
|
704
746
|
onCdpEvent(event) {
|
|
705
747
|
if (this.closing || this.unavailableReason)
|
|
706
748
|
return;
|
|
749
|
+
const params = event.params;
|
|
750
|
+
const identity = { cdpSessionId: event.sessionId, targetId: event.sessionId ? this.targetBySession.get(event.sessionId) : params.targetId };
|
|
751
|
+
if (["Runtime.exceptionThrown", "Network.loadingFailed", "Target.targetCrashed", "Inspector.detached"].includes(event.method) ||
|
|
752
|
+
event.method === "Runtime.consoleAPICalled" && ["error", "warning", "assert"].includes(String(params.type))) {
|
|
753
|
+
this.diagnostic(event.method, { ...identity, details: params });
|
|
754
|
+
}
|
|
755
|
+
else if (event.method === "Network.requestWillBeSent") {
|
|
756
|
+
const request = params.request;
|
|
757
|
+
this.diagnostic(event.method, { ...identity, requestId: params.requestId, type: params.type, url: request?.url, method: request?.method });
|
|
758
|
+
}
|
|
759
|
+
else if (event.method === "Network.responseReceived") {
|
|
760
|
+
const response = params.response;
|
|
761
|
+
if (typeof response?.status === "number" && response.status >= 400)
|
|
762
|
+
this.diagnostic(event.method, {
|
|
763
|
+
...identity, requestId: params.requestId, url: response.url, status: response.status, statusText: response.statusText,
|
|
764
|
+
});
|
|
765
|
+
}
|
|
707
766
|
if (event.method === "Fetch.requestPaused" && event.sessionId) {
|
|
708
767
|
void this.continuePausedRequest(event.sessionId, event.params).catch((error) => {
|
|
709
768
|
if (isMissingFetchRequestError(error))
|
|
@@ -855,6 +914,10 @@ class HeadlessBrowserHandle {
|
|
|
855
914
|
return target;
|
|
856
915
|
}
|
|
857
916
|
dropTarget(targetId) {
|
|
917
|
+
const gateway = this.automationGateways.get(targetId);
|
|
918
|
+
this.automationGateways.delete(targetId);
|
|
919
|
+
if (gateway)
|
|
920
|
+
void gateway.then((value) => value.close()).catch(() => undefined);
|
|
858
921
|
const surface = this.surfaceSources.get(targetId);
|
|
859
922
|
if (surface) {
|
|
860
923
|
// Stop routing renderer events immediately; close remains best-effort
|
|
@@ -923,6 +986,11 @@ class HeadlessBrowserHandle {
|
|
|
923
986
|
await this.host.connection.command("Runtime.runIfWaitingForDebugger", {}, sessionId);
|
|
924
987
|
}
|
|
925
988
|
await this.host.connection.command("Network.enable", {}, sessionId);
|
|
989
|
+
// Enable exception/console observation after paused targets are resumed.
|
|
990
|
+
// Diagnostics failure must not reject an otherwise usable Page.
|
|
991
|
+
await this.host.connection.command("Runtime.enable", {}, sessionId).catch((error) => {
|
|
992
|
+
this.diagnostic("observer.enable_failed", { cdpSessionId: sessionId, error: String(error) });
|
|
993
|
+
});
|
|
926
994
|
}
|
|
927
995
|
catch (error) {
|
|
928
996
|
if (isMissingTargetError(error) || isTransientPageTransitionError(error)) {
|
|
@@ -1031,6 +1099,9 @@ class HeadlessBrowserHandle {
|
|
|
1031
1099
|
if (this.closing || this.unavailableReason)
|
|
1032
1100
|
return;
|
|
1033
1101
|
this.unavailableReason = reason;
|
|
1102
|
+
for (const gateway of this.automationGateways.values())
|
|
1103
|
+
void gateway.then((value) => value.close()).catch(() => undefined);
|
|
1104
|
+
this.automationGateways.clear();
|
|
1034
1105
|
for (const source of this.surfaceSources.values())
|
|
1035
1106
|
source.closeAfterSourceLoss();
|
|
1036
1107
|
this.host.connection.close();
|
|
@@ -1045,7 +1116,10 @@ class HeadlessBrowserHandle {
|
|
|
1045
1116
|
this.markUnavailable("The native CDP observer disconnected");
|
|
1046
1117
|
return unavailable("Headless Browser is unavailable", error);
|
|
1047
1118
|
}
|
|
1048
|
-
|
|
1119
|
+
if (error instanceof CdpCommandTimeoutError) {
|
|
1120
|
+
return new SessionBrowserHostError("command_timeout", "Browser command timed out; an accepted action may still complete", { cause: error });
|
|
1121
|
+
}
|
|
1122
|
+
return new SessionBrowserHostError("operation_failed", "Headless Browser rejected the CDP operation", { cause: error });
|
|
1049
1123
|
}
|
|
1050
1124
|
exclusive(operation) {
|
|
1051
1125
|
const result = this.operations.then(operation, operation);
|
|
@@ -1165,7 +1239,10 @@ class HeadlessBrowserSurfaceSource {
|
|
|
1165
1239
|
this.ensureCurrent();
|
|
1166
1240
|
const receivedAt = Date.now();
|
|
1167
1241
|
this.lastScreencastFrameAt = receivedAt;
|
|
1168
|
-
|
|
1242
|
+
// Throttle immediate deliveries, not replacements behind a slow viewer.
|
|
1243
|
+
// Coalesced CDP messages must still leave the latest paint in the bounded
|
|
1244
|
+
// queue, otherwise its final frame can be permanently dropped.
|
|
1245
|
+
if (receivedAt - this.lastAcceptedFrameAt < this.minimumFrameIntervalMs && !this.frameQueue.hasCurrentFrame()) {
|
|
1169
1246
|
void acknowledge().catch(() => undefined);
|
|
1170
1247
|
return;
|
|
1171
1248
|
}
|
|
@@ -1444,6 +1521,7 @@ class LatestSurfaceFrameQueue {
|
|
|
1444
1521
|
currentDelivered = false;
|
|
1445
1522
|
waiter;
|
|
1446
1523
|
closed = false;
|
|
1524
|
+
hasCurrentFrame() { return this.current !== undefined; }
|
|
1447
1525
|
push(frame) {
|
|
1448
1526
|
const entry = this.wrap(frame);
|
|
1449
1527
|
if (this.closed) {
|
|
@@ -1795,7 +1873,7 @@ class CdpConnection {
|
|
|
1795
1873
|
setDisconnectHandler(handler) {
|
|
1796
1874
|
this.disconnectHandler = handler;
|
|
1797
1875
|
}
|
|
1798
|
-
command(method, params = {}, sessionId) {
|
|
1876
|
+
command(method, params = {}, sessionId, timeoutMs = this.commandTimeoutMs) {
|
|
1799
1877
|
if (!this.connected)
|
|
1800
1878
|
return Promise.reject(new CdpDisconnectedError("Native CDP is disconnected"));
|
|
1801
1879
|
const id = this.nextId++;
|
|
@@ -1803,7 +1881,7 @@ class CdpConnection {
|
|
|
1803
1881
|
const timer = setTimeout(() => {
|
|
1804
1882
|
this.pending.delete(id);
|
|
1805
1883
|
rejectCommand(new CdpCommandTimeoutError(`Native CDP command ${method} timed out`));
|
|
1806
|
-
}, this.commandTimeoutMs);
|
|
1884
|
+
}, Math.min(timeoutMs, this.commandTimeoutMs));
|
|
1807
1885
|
this.pending.set(id, { method, resolve: resolveCommand, reject: rejectCommand, timer });
|
|
1808
1886
|
try {
|
|
1809
1887
|
this.socket.send(JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) }));
|
|
@@ -2347,7 +2425,7 @@ function observeProcess(child) {
|
|
|
2347
2425
|
}
|
|
2348
2426
|
function boundedStderr(child) {
|
|
2349
2427
|
let value = "";
|
|
2350
|
-
|
|
2428
|
+
// Keep the underlying stream binary for the raw diagnostic tee.
|
|
2351
2429
|
child.stderr?.on("data", (chunk) => {
|
|
2352
2430
|
value = `${value}${chunk}`.slice(-16_000);
|
|
2353
2431
|
});
|
|
@@ -2419,12 +2497,6 @@ function parseNavigationHistory(value) {
|
|
|
2419
2497
|
}
|
|
2420
2498
|
return { currentIndex, entries };
|
|
2421
2499
|
}
|
|
2422
|
-
function parseReadyState(value) {
|
|
2423
|
-
if (!isRecord(value) || !isRecord(value.result) || typeof value.result.value !== "string") {
|
|
2424
|
-
return "loading";
|
|
2425
|
-
}
|
|
2426
|
-
return value.result.value;
|
|
2427
|
-
}
|
|
2428
2500
|
function displayUrl(value) {
|
|
2429
2501
|
if (value === "about:blank")
|
|
2430
2502
|
return value;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type AgentSessionRecord, type MachineSessionRecord } from "@rynx-ai/core";
|
|
2
|
+
import type { PluginLogFileRoot } from "@rynx-ai/plugin-sdk";
|
|
3
|
+
/** Node-wide files are intentionally separate from Session ownership. Plugins
|
|
4
|
+
* are installed local code; their remote-facing adapter must authorize node
|
|
5
|
+
* diagnostics, not merely task visibility, before calling this method. */
|
|
6
|
+
export declare function nodeLogFiles(): Promise<PluginLogFileRoot[]>;
|
|
7
|
+
/** Roots only: the off-process collector walks files. One Session per RPC
|
|
8
|
+
* bounds control messages independently of file count and file sizes. */
|
|
9
|
+
export declare function sessionLogFiles(session: MachineSessionRecord, binding: AgentSessionRecord | null): Promise<PluginLogFileRoot[]>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { access, readdir, readFile } from "node:fs/promises";
|
|
6
|
+
import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
7
|
+
import { rynxHome, sessionDiagnosticDir } from "@rynx-ai/core";
|
|
8
|
+
import { dbPath } from "./db.js";
|
|
9
|
+
const family = (file) => [file, ...["-wal", "-shm", "-journal"].map((suffix) => file + suffix)];
|
|
10
|
+
const root = (source, file, scope, optional = false) => {
|
|
11
|
+
const rel = relative(rynxHome(), file).split(sep).join("/");
|
|
12
|
+
return { source, path: file, scope, optional, archivePath: !rel.startsWith("../") && !isAbsolute(rel)
|
|
13
|
+
? `files/rynx/${rel}` : `files/external-logs/${createHash("sha256").update(dirname(file)).digest("hex")}/${basename(file)}` };
|
|
14
|
+
};
|
|
15
|
+
/** Node-wide files are intentionally separate from Session ownership. Plugins
|
|
16
|
+
* are installed local code; their remote-facing adapter must authorize node
|
|
17
|
+
* diagnostics, not merely task visibility, before calling this method. */
|
|
18
|
+
export async function nodeLogFiles() {
|
|
19
|
+
const logs = join(rynxHome(), "logs");
|
|
20
|
+
const files = (await readdir(logs).catch(() => []))
|
|
21
|
+
.filter((name) => /^(?:(?:out|err)|daemon\.(?:out|err))\.log(?:[.-].+)?$/.test(name));
|
|
22
|
+
return [
|
|
23
|
+
...files.map((name) => root("daemon", join(logs, name), "host")),
|
|
24
|
+
root("browser", join(rynxHome(), "logs", "browser", "host"), "host", true),
|
|
25
|
+
...family(dbPath()).map((file, i) => root("rynx-db", file, "host", i > 0)),
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
/** Roots only: the off-process collector walks files. One Session per RPC
|
|
29
|
+
* bounds control messages independently of file count and file sizes. */
|
|
30
|
+
export async function sessionLogFiles(session, binding) {
|
|
31
|
+
const extended = binding;
|
|
32
|
+
const state = (id) => join(rynxHome(), "runtime", "sessions", createHash("sha256").update(id).digest("hex").slice(0, 32));
|
|
33
|
+
const owner = extended?.runtimeHomeOwnerSessionId || session.id;
|
|
34
|
+
let home = state(owner);
|
|
35
|
+
const provider = session.execution.provider;
|
|
36
|
+
const legacy = join(tmpdir(), `rynx-${process.getuid?.() ?? "nouid"}`);
|
|
37
|
+
const exists = (file) => access(file).then(() => true, () => false);
|
|
38
|
+
const nativeName = provider === "codex" ? "codex-home" : "trae-home";
|
|
39
|
+
if (!(await exists(join(home, nativeName)))) {
|
|
40
|
+
const old = join(legacy, provider === "codex" ? "codex-native" : "traex-native", createHash("sha256").update(owner).digest("hex").slice(0, 32));
|
|
41
|
+
if (await exists(join(old, nativeName)))
|
|
42
|
+
home = old;
|
|
43
|
+
}
|
|
44
|
+
const result = [
|
|
45
|
+
root("runtime", sessionDiagnosticDir("runtime", session.id), "session", true),
|
|
46
|
+
root("browser", sessionDiagnosticDir("browser", session.id), "session", true),
|
|
47
|
+
root("browser", join(rynxHome(), "logs", "browser", "host"), "host", true),
|
|
48
|
+
];
|
|
49
|
+
if (owner !== session.id)
|
|
50
|
+
result.push(root("runtime", sessionDiagnosticDir("runtime", owner), "runtime_home", true));
|
|
51
|
+
if (provider === "codex" || provider === "traex") {
|
|
52
|
+
const native = join(home, provider === "codex" ? "codex-home" : "trae-home");
|
|
53
|
+
const cli = provider === "codex" ? native : join(native, "cli");
|
|
54
|
+
result.push(root("runtime", join(native, "sessions"), "runtime_home"));
|
|
55
|
+
result.push(root("runtime", join(cli, "log"), "runtime_home", true));
|
|
56
|
+
for (const file of family(join(cli, "logs_2.sqlite")))
|
|
57
|
+
result.push(root("runtime-db", file, "runtime_home", true));
|
|
58
|
+
}
|
|
59
|
+
else if (provider === "claude") {
|
|
60
|
+
const bridgeOwner = extended?.bridgeOwnerSessionId || owner;
|
|
61
|
+
let bridge = join(state(bridgeOwner), "claude-bridge");
|
|
62
|
+
if (!(await exists(bridge))) {
|
|
63
|
+
const old = join(legacy, "claude-native", createHash("sha256").update(bridgeOwner).digest("hex").slice(0, 32));
|
|
64
|
+
if (await exists(old))
|
|
65
|
+
bridge = old;
|
|
66
|
+
}
|
|
67
|
+
for (const name of ["hooks.jsonl", "state.json", "message_deltas.jsonl", "interactions.jsonl", "interaction-acks.jsonl", "interaction-results", "history"]) {
|
|
68
|
+
result.push(root("runtime", join(bridge, name), "session", true));
|
|
69
|
+
}
|
|
70
|
+
const transcripts = new Set();
|
|
71
|
+
const note = (value) => { if (typeof value === "string" && isAbsolute(value))
|
|
72
|
+
transcripts.add(value); };
|
|
73
|
+
const bridges = [bridge, ...(await readdir(join(bridge, "history")).catch(() => [])).map((id) => join(bridge, "history", id))];
|
|
74
|
+
for (const directory of bridges) {
|
|
75
|
+
try {
|
|
76
|
+
note(JSON.parse(await readFile(join(directory, "state.json"), "utf8")).transcriptPath);
|
|
77
|
+
}
|
|
78
|
+
catch { /* older/never-started */ }
|
|
79
|
+
try {
|
|
80
|
+
const lines = createInterface({ input: createReadStream(join(directory, "hooks.jsonl")), crlfDelay: Infinity });
|
|
81
|
+
for await (const line of lines) {
|
|
82
|
+
try {
|
|
83
|
+
note(JSON.parse(line).payload?.transcript_path);
|
|
84
|
+
}
|
|
85
|
+
catch { /* partial live line */ }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch { /* original files and collection warnings still report unreadable sources */ }
|
|
89
|
+
}
|
|
90
|
+
for (const transcript of transcripts) {
|
|
91
|
+
result.push(root("runtime", transcript, "session"));
|
|
92
|
+
result.push(root("runtime", join(dirname(transcript), basename(transcript, ".jsonl"), "subagents"), "session", true));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** A helper epoch owns one transport connection. An upstream CLI must not
|
|
2
|
+
* silently restart and execute a write against this epoch's borrowed Page. */
|
|
3
|
+
export declare function createPageAgentBrowserConnection(endpoint: string, isCurrent: () => boolean): Promise<{
|
|
4
|
+
endpoint: string;
|
|
5
|
+
close(): Promise<void>;
|
|
6
|
+
}>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import WebSocket, { WebSocketServer } from "ws";
|
|
4
|
+
/** A helper epoch owns one transport connection. An upstream CLI must not
|
|
5
|
+
* silently restart and execute a write against this epoch's borrowed Page. */
|
|
6
|
+
export async function createPageAgentBrowserConnection(endpoint, isCurrent) {
|
|
7
|
+
const noncePath = "/devtools/browser/" + randomBytes(18).toString("hex");
|
|
8
|
+
const limit = 8 * 1024 * 1024;
|
|
9
|
+
let used = false, closed = false, authority = "";
|
|
10
|
+
let client, upstream;
|
|
11
|
+
const server = createServer((_request, response) => { response.writeHead(404).end(); });
|
|
12
|
+
const sockets = new WebSocketServer({ noServer: true, maxPayload: limit });
|
|
13
|
+
const disconnect = () => { client?.terminate(); upstream?.terminate(); };
|
|
14
|
+
server.on("upgrade", (request, socket, head) => {
|
|
15
|
+
if (closed || used || !isCurrent() || request.url !== noncePath || request.headers.origin !== undefined || request.headers.host !== authority) {
|
|
16
|
+
socket.destroy();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
used = true;
|
|
20
|
+
sockets.handleUpgrade(request, socket, head, (downstream) => {
|
|
21
|
+
client = downstream;
|
|
22
|
+
const native = new WebSocket(endpoint, { maxPayload: limit, handshakeTimeout: 5_000 });
|
|
23
|
+
upstream = native;
|
|
24
|
+
const early = [];
|
|
25
|
+
let buffered = 0;
|
|
26
|
+
downstream.on("close", disconnect);
|
|
27
|
+
downstream.on("error", disconnect);
|
|
28
|
+
native.on("close", disconnect);
|
|
29
|
+
native.on("error", disconnect);
|
|
30
|
+
downstream.on("message", (bytes, binary) => {
|
|
31
|
+
if (binary || !isCurrent() || native.bufferedAmount > limit) {
|
|
32
|
+
disconnect();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const data = Buffer.from(bytes.toString());
|
|
36
|
+
if (native.readyState === WebSocket.OPEN)
|
|
37
|
+
native.send(data, { binary: false });
|
|
38
|
+
else if (native.readyState === WebSocket.CONNECTING && early.length < 64 && buffered + data.length <= limit) {
|
|
39
|
+
early.push(data);
|
|
40
|
+
buffered += data.length;
|
|
41
|
+
}
|
|
42
|
+
else
|
|
43
|
+
disconnect();
|
|
44
|
+
});
|
|
45
|
+
native.on("open", () => { for (const bytes of early)
|
|
46
|
+
native.send(bytes, { binary: false }); early.length = 0; buffered = 0; });
|
|
47
|
+
native.on("message", (bytes, binary) => {
|
|
48
|
+
if (binary || !isCurrent() || downstream.readyState !== WebSocket.OPEN || downstream.bufferedAmount > limit) {
|
|
49
|
+
disconnect();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
downstream.send(bytes, { binary: false });
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
await new Promise((resolve, reject) => {
|
|
57
|
+
server.once("error", reject);
|
|
58
|
+
server.listen(0, "127.0.0.1", () => { server.off("error", reject); resolve(); });
|
|
59
|
+
});
|
|
60
|
+
const address = server.address();
|
|
61
|
+
if (!address || typeof address === "string")
|
|
62
|
+
throw new Error("Browser helper capability did not bind");
|
|
63
|
+
authority = "127.0.0.1:" + address.port;
|
|
64
|
+
return { endpoint: "ws://" + authority + noncePath, async close() {
|
|
65
|
+
if (closed)
|
|
66
|
+
return;
|
|
67
|
+
closed = true;
|
|
68
|
+
disconnect();
|
|
69
|
+
await Promise.all([new Promise((r) => sockets.close(() => r())), new Promise((r) => server.close(() => r()))]);
|
|
70
|
+
} };
|
|
71
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SessionBrowserAutomationBinding } from "@rynx-ai/server";
|
|
2
|
+
export declare const PAGE_AGENT_BROWSER_VERSION = "0.36.0";
|
|
3
|
+
export declare class PageAutomationError extends Error {
|
|
4
|
+
readonly code: "invalid_request" | "human_control_active" | "driver_restarted" | "outcome_unknown" | "command_failed" | "capacity" | "not_found";
|
|
5
|
+
constructor(code: "invalid_request" | "human_control_active" | "driver_restarted" | "outcome_unknown" | "command_failed" | "capacity" | "not_found", message: string, options?: ErrorOptions);
|
|
6
|
+
}
|
|
7
|
+
export interface PageAutomationDriver {
|
|
8
|
+
readonly version?: string;
|
|
9
|
+
execute(args: string[], beforeInvoke?: () => void): Promise<Record<string, unknown>>;
|
|
10
|
+
close(): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
/** Resolve only the pinned dependency. Never use a PATH/global helper or run its Node wrapper. */
|
|
13
|
+
export declare function pageAgentBrowserBinary(): string;
|
|
14
|
+
export declare function createPageAgentBrowserDriver(root: string, binding: SessionBrowserAutomationBinding): Promise<PageAutomationDriver>;
|