@mercuryo-ai/agentbrowse 0.2.60 → 0.2.63
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/CHANGELOG.md +33 -1
- package/README.md +132 -14
- package/dist/browser-session-state.d.ts +40 -10
- package/dist/browser-session-state.d.ts.map +1 -1
- package/dist/browser-session-state.js +63 -5
- package/dist/commands/act.d.ts.map +1 -1
- package/dist/commands/act.js +548 -535
- package/dist/commands/attach.d.ts +1 -3
- package/dist/commands/attach.d.ts.map +1 -1
- package/dist/commands/attach.js +5 -12
- package/dist/commands/browser-connection-failure.d.ts +9 -0
- package/dist/commands/browser-connection-failure.d.ts.map +1 -0
- package/dist/commands/browser-connection-failure.js +15 -0
- package/dist/commands/browser-status.d.ts +0 -2
- package/dist/commands/browser-status.d.ts.map +1 -1
- package/dist/commands/browser-status.js +27 -37
- package/dist/commands/close.d.ts.map +1 -1
- package/dist/commands/close.js +5 -0
- package/dist/commands/extract.d.ts.map +1 -1
- package/dist/commands/extract.js +147 -144
- package/dist/commands/interaction-kernel.d.ts +1 -1
- package/dist/commands/interaction-kernel.d.ts.map +1 -1
- package/dist/commands/interaction-kernel.js +1 -1
- package/dist/commands/launch.d.ts +0 -1
- package/dist/commands/launch.d.ts.map +1 -1
- package/dist/commands/launch.js +11 -12
- package/dist/commands/navigate.d.ts.map +1 -1
- package/dist/commands/navigate.js +79 -73
- package/dist/commands/observe-accessibility.d.ts.map +1 -1
- package/dist/commands/observe-accessibility.js +36 -2
- package/dist/commands/observe-inventory.d.ts +50 -7
- package/dist/commands/observe-inventory.d.ts.map +1 -1
- package/dist/commands/observe-inventory.js +822 -99
- package/dist/commands/observe-persistence.d.ts.map +1 -1
- package/dist/commands/observe-persistence.js +49 -6
- package/dist/commands/observe-projection.d.ts +6 -2
- package/dist/commands/observe-projection.d.ts.map +1 -1
- package/dist/commands/observe-projection.js +251 -27
- package/dist/commands/observe-semantics.d.ts +1 -0
- package/dist/commands/observe-semantics.d.ts.map +1 -1
- package/dist/commands/observe-semantics.js +541 -135
- package/dist/commands/observe-signals.d.ts +4 -4
- package/dist/commands/observe-signals.d.ts.map +1 -1
- package/dist/commands/observe-signals.js +2 -2
- package/dist/commands/observe-surfaces.d.ts +2 -1
- package/dist/commands/observe-surfaces.d.ts.map +1 -1
- package/dist/commands/observe-surfaces.js +143 -45
- package/dist/commands/observe.d.ts +5 -1
- package/dist/commands/observe.d.ts.map +1 -1
- package/dist/commands/observe.js +266 -274
- package/dist/commands/screenshot.d.ts.map +1 -1
- package/dist/commands/screenshot.js +50 -64
- package/dist/commands/semantic-observe.d.ts.map +1 -1
- package/dist/commands/semantic-observe.js +43 -0
- package/dist/library.d.ts +3 -1
- package/dist/library.d.ts.map +1 -1
- package/dist/library.js +3 -1
- package/dist/match-resolve-fill.d.ts +196 -0
- package/dist/match-resolve-fill.d.ts.map +1 -0
- package/dist/match-resolve-fill.js +700 -0
- package/dist/match-resolve-fill.test-support.d.ts +34 -0
- package/dist/match-resolve-fill.test-support.d.ts.map +1 -0
- package/dist/match-resolve-fill.test-support.js +81 -0
- package/dist/protected-fill.d.ts.map +1 -1
- package/dist/protected-fill.js +46 -7
- package/dist/runtime-protected-state.d.ts.map +1 -1
- package/dist/runtime-protected-state.js +12 -0
- package/dist/runtime-state.d.ts +6 -0
- package/dist/runtime-state.d.ts.map +1 -1
- package/dist/runtime-state.js +6 -0
- package/dist/secrets/form-matcher.d.ts.map +1 -1
- package/dist/secrets/form-matcher.js +76 -27
- package/dist/secrets/protected-exact-value-redaction.d.ts.map +1 -1
- package/dist/secrets/protected-exact-value-redaction.js +6 -0
- package/dist/secrets/protected-fill.js +3 -3
- package/dist/session.d.ts +3 -3
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +2 -2
- package/dist/solver/browser-launcher.d.ts.map +1 -1
- package/dist/solver/browser-launcher.js +2 -1
- package/dist/sticky-owner-host-entry.d.ts +2 -0
- package/dist/sticky-owner-host-entry.d.ts.map +1 -0
- package/dist/sticky-owner-host-entry.js +97 -0
- package/dist/sticky-owner.d.ts +15 -0
- package/dist/sticky-owner.d.ts.map +1 -0
- package/dist/sticky-owner.js +431 -0
- package/dist/testing.d.ts +1 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +1 -0
- package/docs/README.md +28 -11
- package/docs/api-reference.md +311 -19
- package/docs/assistive-runtime.md +41 -16
- package/docs/configuration.md +36 -4
- package/docs/getting-started.md +73 -5
- package/docs/integration-checklist.md +32 -3
- package/docs/match-resolve-fill.md +699 -0
- package/docs/protected-fill.md +373 -91
- package/docs/testing.md +147 -15
- package/docs/troubleshooting.md +47 -6
- package/examples/README.md +7 -0
- package/examples/match-resolve-fill.ts +107 -0
- package/package.json +4 -2
- package/dist/protected-fill-browser.d.ts +0 -22
- package/dist/protected-fill-browser.d.ts.map +0 -1
- package/dist/protected-fill-browser.js +0 -52
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { existsSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { connectPlaywright, disconnectPlaywright } from './playwright-runtime.js';
|
|
3
|
+
function readArgValue(argv, name) {
|
|
4
|
+
const index = argv.indexOf(name);
|
|
5
|
+
const value = index >= 0 ? argv[index + 1] : undefined;
|
|
6
|
+
if (!value || value.startsWith('--')) {
|
|
7
|
+
throw new Error(`Missing ${name} argument.`);
|
|
8
|
+
}
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
function parseArgs(argv = process.argv) {
|
|
12
|
+
const ttlRaw = readArgValue(argv, '--ttl-ms');
|
|
13
|
+
const ttlMs = Number(ttlRaw);
|
|
14
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
15
|
+
throw new Error('Invalid --ttl-ms argument.');
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
manifestPath: readArgValue(argv, '--manifest'),
|
|
19
|
+
cdpUrl: readArgValue(argv, '--cdp-url'),
|
|
20
|
+
hostId: readArgValue(argv, '--host-id'),
|
|
21
|
+
browserSessionId: readArgValue(argv, '--browser-session-id'),
|
|
22
|
+
touchPath: readArgValue(argv, '--touch-path'),
|
|
23
|
+
ttlMs,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function touchLeaseFile(path) {
|
|
27
|
+
writeFileSync(path, new Date().toISOString());
|
|
28
|
+
}
|
|
29
|
+
function readLeaseAgeMs(path) {
|
|
30
|
+
if (!existsSync(path)) {
|
|
31
|
+
return Number.POSITIVE_INFINITY;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return Math.max(0, Date.now() - statSync(path).mtimeMs);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return Number.POSITIVE_INFINITY;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function main() {
|
|
41
|
+
const args = parseArgs();
|
|
42
|
+
const browser = await connectPlaywright(args.cdpUrl);
|
|
43
|
+
const binding = await browser.bind(args.hostId, {
|
|
44
|
+
metadata: {
|
|
45
|
+
hostId: args.hostId,
|
|
46
|
+
browserSessionId: args.browserSessionId,
|
|
47
|
+
pid: process.pid,
|
|
48
|
+
startedAt: new Date().toISOString(),
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
touchLeaseFile(args.touchPath);
|
|
52
|
+
writeFileSync(args.manifestPath, JSON.stringify({
|
|
53
|
+
endpoint: binding.endpoint,
|
|
54
|
+
pid: process.pid,
|
|
55
|
+
startedAt: new Date().toISOString(),
|
|
56
|
+
}, null, 2));
|
|
57
|
+
let shuttingDown = false;
|
|
58
|
+
let ttlInterval = null;
|
|
59
|
+
const shutdown = async () => {
|
|
60
|
+
if (shuttingDown) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
shuttingDown = true;
|
|
64
|
+
if (ttlInterval) {
|
|
65
|
+
clearInterval(ttlInterval);
|
|
66
|
+
ttlInterval = null;
|
|
67
|
+
}
|
|
68
|
+
await browser.unbind().catch(() => undefined);
|
|
69
|
+
await disconnectPlaywright(browser).catch(() => undefined);
|
|
70
|
+
rmSync(args.touchPath, { force: true });
|
|
71
|
+
process.exit(0);
|
|
72
|
+
};
|
|
73
|
+
const reapExpiredOwner = () => {
|
|
74
|
+
if (readLeaseAgeMs(args.touchPath) >= args.ttlMs) {
|
|
75
|
+
void shutdown();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
browser.on('disconnected', () => {
|
|
79
|
+
void shutdown();
|
|
80
|
+
});
|
|
81
|
+
process.on('SIGINT', () => {
|
|
82
|
+
void shutdown();
|
|
83
|
+
});
|
|
84
|
+
process.on('SIGTERM', () => {
|
|
85
|
+
void shutdown();
|
|
86
|
+
});
|
|
87
|
+
const ttlCheckMs = Math.max(5_000, Math.min(Math.floor(args.ttlMs / 6), 60_000));
|
|
88
|
+
ttlInterval = setInterval(reapExpiredOwner, ttlCheckMs);
|
|
89
|
+
ttlInterval.unref?.();
|
|
90
|
+
reapExpiredOwner();
|
|
91
|
+
await new Promise(() => { });
|
|
92
|
+
}
|
|
93
|
+
void main().catch((error) => {
|
|
94
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
95
|
+
console.error(message);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Browser } from 'playwright-core';
|
|
2
|
+
import { type BrowserSessionState } from './browser-session-state.js';
|
|
3
|
+
export type RestoreBrowserSessionOwnerResult = {
|
|
4
|
+
success: true;
|
|
5
|
+
session: BrowserSessionState;
|
|
6
|
+
restored: boolean;
|
|
7
|
+
} | {
|
|
8
|
+
success: false;
|
|
9
|
+
reason: 'sticky_owner_unrecoverable';
|
|
10
|
+
};
|
|
11
|
+
export declare function initializeBrowserSessionOwner(session: BrowserSessionState): Promise<BrowserSessionState>;
|
|
12
|
+
export declare function terminateBrowserSessionOwner(session: BrowserSessionState | null): Promise<void>;
|
|
13
|
+
export declare function restoreBrowserSessionOwner(session: BrowserSessionState): Promise<RestoreBrowserSessionOwnerResult>;
|
|
14
|
+
export declare function withStickyOwnerBrowser<TResult>(session: BrowserSessionState, operation: (browser: Browser) => Promise<TResult>): Promise<TResult>;
|
|
15
|
+
//# sourceMappingURL=sticky-owner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sticky-owner.d.ts","sourceRoot":"","sources":["../src/sticky-owner.ts"],"names":[],"mappings":"AAMA,OAAO,EAAY,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAIL,KAAK,mBAAmB,EAGzB,MAAM,4BAA4B,CAAC;AAWpC,MAAM,MAAM,gCAAgC,GACxC;IACE,OAAO,EAAE,IAAI,CAAC;IACd,OAAO,EAAE,mBAAmB,CAAC;IAC7B,QAAQ,EAAE,OAAO,CAAC;CACnB,GACD;IACE,OAAO,EAAE,KAAK,CAAC;IACf,MAAM,EAAE,4BAA4B,CAAC;CACtC,CAAC;AAyVN,wBAAsB,6BAA6B,CACjD,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,mBAAmB,CAAC,CAU9B;AAED,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,mBAAmB,GAAG,IAAI,GAClC,OAAO,CAAC,IAAI,CAAC,CA2Bf;AAED,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,gCAAgC,CAAC,CAiG3C;AAED,wBAAsB,sBAAsB,CAAC,OAAO,EAClD,OAAO,EAAE,mBAAmB,EAC5B,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,GAChD,OAAO,CAAC,OAAO,CAAC,CA0BlB"}
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { existsSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { chromium } from 'playwright-core';
|
|
8
|
+
import { buildStickyOwnerMetadata, isSessionAlive, resolveBrowserSessionId, } from './browser-session-state.js';
|
|
9
|
+
import { connectPlaywright, disconnectPlaywright } from './playwright-runtime.js';
|
|
10
|
+
const HOST_READY_TIMEOUT_MS = 5_000;
|
|
11
|
+
const HOST_READY_POLL_MS = 50;
|
|
12
|
+
const HOST_CONNECT_TIMEOUT_MS = 3_000;
|
|
13
|
+
const HOST_STOP_TIMEOUT_MS = 1_000;
|
|
14
|
+
const DEFAULT_STICKY_OWNER_TTL_MS = 30 * 60 * 1_000;
|
|
15
|
+
const STICKY_OWNER_HEARTBEAT_FLOOR_MS = 5_000;
|
|
16
|
+
const STICKY_OWNER_HEARTBEAT_CEILING_MS = 60_000;
|
|
17
|
+
const inProcessOwners = new WeakMap();
|
|
18
|
+
function resolveStickyOwnerBootstrapMode() {
|
|
19
|
+
const explicit = process.env.AGENTBROWSE_STICKY_OWNER_MODE?.trim().toLowerCase();
|
|
20
|
+
if (explicit === 'detached_process') {
|
|
21
|
+
return 'detached_process';
|
|
22
|
+
}
|
|
23
|
+
if (explicit === 'in_process') {
|
|
24
|
+
return 'in_process';
|
|
25
|
+
}
|
|
26
|
+
if (process.env.VITEST) {
|
|
27
|
+
return 'in_process';
|
|
28
|
+
}
|
|
29
|
+
return 'detached_process';
|
|
30
|
+
}
|
|
31
|
+
function createHostId() {
|
|
32
|
+
return `owner_${randomUUID().replace(/-/g, '')}`;
|
|
33
|
+
}
|
|
34
|
+
function resolveStickyOwnerTtlMs() {
|
|
35
|
+
const raw = process.env.AGENTBROWSE_STICKY_OWNER_TTL_MS?.trim();
|
|
36
|
+
if (!raw) {
|
|
37
|
+
return DEFAULT_STICKY_OWNER_TTL_MS;
|
|
38
|
+
}
|
|
39
|
+
const ttlMs = Number(raw);
|
|
40
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
41
|
+
return DEFAULT_STICKY_OWNER_TTL_MS;
|
|
42
|
+
}
|
|
43
|
+
return ttlMs;
|
|
44
|
+
}
|
|
45
|
+
function getStickyOwnerTouchPath(hostId) {
|
|
46
|
+
return join(tmpdir(), `agentbrowse-sticky-owner-${hostId}.touch`);
|
|
47
|
+
}
|
|
48
|
+
function buildInProcessStickyOwner(session, options = {}) {
|
|
49
|
+
return buildStickyOwnerMetadata({
|
|
50
|
+
hostId: options.hostId ?? session.stickyOwner?.hostId ?? createHostId(),
|
|
51
|
+
state: options.state ?? 'active',
|
|
52
|
+
startedAt: options.startedAt ?? session.stickyOwner?.startedAt ?? new Date().toISOString(),
|
|
53
|
+
browserSessionId: resolveBrowserSessionId(session),
|
|
54
|
+
transport: {
|
|
55
|
+
type: 'in_process',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function readStickyOwnerLastUsedMs(stickyOwner) {
|
|
60
|
+
if (stickyOwner.touchPath) {
|
|
61
|
+
try {
|
|
62
|
+
if (existsSync(stickyOwner.touchPath)) {
|
|
63
|
+
return statSync(stickyOwner.touchPath).mtimeMs;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Fall through to metadata timestamps.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const candidate = stickyOwner.lastUsedAt ?? stickyOwner.startedAt;
|
|
71
|
+
const parsed = Date.parse(candidate);
|
|
72
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
73
|
+
}
|
|
74
|
+
function isStickyOwnerExpired(stickyOwner) {
|
|
75
|
+
if (stickyOwner.transport.type !== 'playwright_bind') {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
const ttlMs = stickyOwner.ttlMs ?? DEFAULT_STICKY_OWNER_TTL_MS;
|
|
79
|
+
return Date.now() - readStickyOwnerLastUsedMs(stickyOwner) >= ttlMs;
|
|
80
|
+
}
|
|
81
|
+
function removeStickyOwnerTouchFile(stickyOwner) {
|
|
82
|
+
if (!stickyOwner?.touchPath) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
rmSync(stickyOwner.touchPath, { force: true });
|
|
86
|
+
}
|
|
87
|
+
function touchStickyOwnerLease(session) {
|
|
88
|
+
if (!session.stickyOwner || session.stickyOwner.transport.type !== 'playwright_bind') {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const touchedAt = new Date().toISOString();
|
|
92
|
+
if (session.stickyOwner.touchPath) {
|
|
93
|
+
writeFileSync(session.stickyOwner.touchPath, touchedAt);
|
|
94
|
+
}
|
|
95
|
+
session.stickyOwner = {
|
|
96
|
+
...session.stickyOwner,
|
|
97
|
+
lastUsedAt: touchedAt,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function startStickyOwnerLeaseHeartbeat(session) {
|
|
101
|
+
if (!session.stickyOwner || session.stickyOwner.transport.type !== 'playwright_bind') {
|
|
102
|
+
return () => undefined;
|
|
103
|
+
}
|
|
104
|
+
const ttlMs = session.stickyOwner.ttlMs ?? DEFAULT_STICKY_OWNER_TTL_MS;
|
|
105
|
+
const heartbeatMs = Math.max(STICKY_OWNER_HEARTBEAT_FLOOR_MS, Math.min(Math.floor(ttlMs / 3), STICKY_OWNER_HEARTBEAT_CEILING_MS));
|
|
106
|
+
const timer = setInterval(() => {
|
|
107
|
+
try {
|
|
108
|
+
touchStickyOwnerLease(session);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Best-effort heartbeat; restore path will handle stale or dead owners later.
|
|
112
|
+
}
|
|
113
|
+
}, heartbeatMs);
|
|
114
|
+
timer.unref?.();
|
|
115
|
+
return () => clearInterval(timer);
|
|
116
|
+
}
|
|
117
|
+
function markStickyOwnerState(session, state) {
|
|
118
|
+
if (!session.stickyOwner) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
session.stickyOwner = {
|
|
122
|
+
...session.stickyOwner,
|
|
123
|
+
state,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function isProcessAlive(pid) {
|
|
127
|
+
if (typeof pid !== 'number' || !Number.isFinite(pid) || pid <= 0) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
process.kill(pid, 0);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function sleep(ms) {
|
|
139
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
140
|
+
}
|
|
141
|
+
async function waitForProcessExit(pid, timeoutMs) {
|
|
142
|
+
const deadline = Date.now() + timeoutMs;
|
|
143
|
+
while (Date.now() < deadline) {
|
|
144
|
+
if (!isProcessAlive(pid)) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
await sleep(50);
|
|
148
|
+
}
|
|
149
|
+
return !isProcessAlive(pid);
|
|
150
|
+
}
|
|
151
|
+
async function canConnectToStickyOwnerEndpoint(endpoint) {
|
|
152
|
+
let browser = null;
|
|
153
|
+
try {
|
|
154
|
+
browser = await chromium.connect(endpoint, {
|
|
155
|
+
timeout: HOST_CONNECT_TIMEOUT_MS,
|
|
156
|
+
});
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
if (browser) {
|
|
164
|
+
await disconnectPlaywright(browser);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function getStickyOwnerHostEntryPath() {
|
|
169
|
+
const currentPath = fileURLToPath(import.meta.url);
|
|
170
|
+
const suffix = currentPath.endsWith('.ts') ? '.ts' : '.js';
|
|
171
|
+
return fileURLToPath(new URL(`./sticky-owner-host-entry${suffix}`, import.meta.url));
|
|
172
|
+
}
|
|
173
|
+
function getStickyOwnerHostSpawnArgs(entryPath, manifestPath, cdpUrl, hostId, browserSessionId, touchPath, ttlMs) {
|
|
174
|
+
const args = [
|
|
175
|
+
entryPath,
|
|
176
|
+
'--manifest',
|
|
177
|
+
manifestPath,
|
|
178
|
+
'--cdp-url',
|
|
179
|
+
cdpUrl,
|
|
180
|
+
'--host-id',
|
|
181
|
+
hostId,
|
|
182
|
+
'--browser-session-id',
|
|
183
|
+
browserSessionId,
|
|
184
|
+
'--touch-path',
|
|
185
|
+
touchPath,
|
|
186
|
+
'--ttl-ms',
|
|
187
|
+
String(ttlMs),
|
|
188
|
+
];
|
|
189
|
+
if (entryPath.endsWith('.ts')) {
|
|
190
|
+
return ['--import', 'tsx', ...args];
|
|
191
|
+
}
|
|
192
|
+
return args;
|
|
193
|
+
}
|
|
194
|
+
async function waitForStickyOwnerManifest(manifestPath) {
|
|
195
|
+
const deadline = Date.now() + HOST_READY_TIMEOUT_MS;
|
|
196
|
+
while (Date.now() < deadline) {
|
|
197
|
+
if (existsSync(manifestPath)) {
|
|
198
|
+
const payload = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
199
|
+
if (typeof payload.endpoint === 'string' &&
|
|
200
|
+
payload.endpoint.trim().length > 0 &&
|
|
201
|
+
typeof payload.pid === 'number' &&
|
|
202
|
+
Number.isFinite(payload.pid) &&
|
|
203
|
+
payload.pid > 0 &&
|
|
204
|
+
typeof payload.startedAt === 'string' &&
|
|
205
|
+
payload.startedAt.trim().length > 0) {
|
|
206
|
+
rmSync(manifestPath, { force: true });
|
|
207
|
+
return {
|
|
208
|
+
endpoint: payload.endpoint,
|
|
209
|
+
pid: payload.pid,
|
|
210
|
+
startedAt: payload.startedAt,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
await sleep(HOST_READY_POLL_MS);
|
|
215
|
+
}
|
|
216
|
+
throw new Error('sticky_owner_bootstrap_timeout');
|
|
217
|
+
}
|
|
218
|
+
async function bootstrapDetachedStickyOwner(session) {
|
|
219
|
+
const hostId = session.stickyOwner?.hostId ?? createHostId();
|
|
220
|
+
const browserSessionId = resolveBrowserSessionId(session);
|
|
221
|
+
const ttlMs = session.stickyOwner?.ttlMs ?? resolveStickyOwnerTtlMs();
|
|
222
|
+
const manifestPath = join(tmpdir(), `agentbrowse-sticky-owner-${hostId}.json`);
|
|
223
|
+
const touchPath = session.stickyOwner?.touchPath ?? getStickyOwnerTouchPath(hostId);
|
|
224
|
+
const touchedAt = new Date().toISOString();
|
|
225
|
+
writeFileSync(touchPath, touchedAt);
|
|
226
|
+
const entryPath = getStickyOwnerHostEntryPath();
|
|
227
|
+
const child = spawn(process.execPath, getStickyOwnerHostSpawnArgs(entryPath, manifestPath, session.cdpUrl, hostId, browserSessionId, touchPath, ttlMs), {
|
|
228
|
+
detached: true,
|
|
229
|
+
stdio: 'ignore',
|
|
230
|
+
});
|
|
231
|
+
child.unref();
|
|
232
|
+
try {
|
|
233
|
+
const ready = await waitForStickyOwnerManifest(manifestPath);
|
|
234
|
+
return buildStickyOwnerMetadata({
|
|
235
|
+
hostId,
|
|
236
|
+
state: 'active',
|
|
237
|
+
startedAt: ready.startedAt,
|
|
238
|
+
lastUsedAt: touchedAt,
|
|
239
|
+
ttlMs,
|
|
240
|
+
touchPath,
|
|
241
|
+
browserSessionId,
|
|
242
|
+
pid: ready.pid,
|
|
243
|
+
transport: {
|
|
244
|
+
type: 'playwright_bind',
|
|
245
|
+
endpoint: ready.endpoint,
|
|
246
|
+
mode: 'pipe',
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
if (isProcessAlive(child.pid)) {
|
|
252
|
+
process.kill(child.pid, 'SIGTERM');
|
|
253
|
+
await waitForProcessExit(child.pid, HOST_STOP_TIMEOUT_MS).catch(() => undefined);
|
|
254
|
+
}
|
|
255
|
+
rmSync(manifestPath, { force: true });
|
|
256
|
+
rmSync(touchPath, { force: true });
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function ensureInProcessOwner(session) {
|
|
261
|
+
const existing = inProcessOwners.get(session);
|
|
262
|
+
if (existing) {
|
|
263
|
+
return existing;
|
|
264
|
+
}
|
|
265
|
+
const browser = await connectPlaywright(session.cdpUrl);
|
|
266
|
+
const owner = {
|
|
267
|
+
browser,
|
|
268
|
+
hostId: session.stickyOwner?.hostId ?? createHostId(),
|
|
269
|
+
startedAt: session.stickyOwner?.startedAt ?? new Date().toISOString(),
|
|
270
|
+
};
|
|
271
|
+
inProcessOwners.set(session, owner);
|
|
272
|
+
session.stickyOwner = buildInProcessStickyOwner(session, {
|
|
273
|
+
hostId: owner.hostId,
|
|
274
|
+
startedAt: owner.startedAt,
|
|
275
|
+
});
|
|
276
|
+
return owner;
|
|
277
|
+
}
|
|
278
|
+
export async function initializeBrowserSessionOwner(session) {
|
|
279
|
+
if (resolveStickyOwnerBootstrapMode() === 'in_process') {
|
|
280
|
+
session.stickyOwner = buildInProcessStickyOwner(session, {
|
|
281
|
+
state: 'active',
|
|
282
|
+
});
|
|
283
|
+
return session;
|
|
284
|
+
}
|
|
285
|
+
session.stickyOwner = await bootstrapDetachedStickyOwner(session);
|
|
286
|
+
return session;
|
|
287
|
+
}
|
|
288
|
+
export async function terminateBrowserSessionOwner(session) {
|
|
289
|
+
if (!session?.stickyOwner) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (session.stickyOwner.transport.type === 'in_process') {
|
|
293
|
+
const owner = inProcessOwners.get(session);
|
|
294
|
+
inProcessOwners.delete(session);
|
|
295
|
+
if (owner) {
|
|
296
|
+
await disconnectPlaywright(owner.browser);
|
|
297
|
+
}
|
|
298
|
+
markStickyOwnerState(session, 'dead');
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const ownerPid = session.stickyOwner.pid;
|
|
302
|
+
if (isProcessAlive(ownerPid)) {
|
|
303
|
+
process.kill(ownerPid, 'SIGTERM');
|
|
304
|
+
const exited = await waitForProcessExit(ownerPid, HOST_STOP_TIMEOUT_MS);
|
|
305
|
+
if (!exited && isProcessAlive(ownerPid)) {
|
|
306
|
+
process.kill(ownerPid, 'SIGKILL');
|
|
307
|
+
await waitForProcessExit(ownerPid, HOST_STOP_TIMEOUT_MS).catch(() => undefined);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
removeStickyOwnerTouchFile(session.stickyOwner);
|
|
311
|
+
markStickyOwnerState(session, 'dead');
|
|
312
|
+
}
|
|
313
|
+
export async function restoreBrowserSessionOwner(session) {
|
|
314
|
+
const stickyOwner = session.stickyOwner;
|
|
315
|
+
if (!stickyOwner) {
|
|
316
|
+
if (resolveStickyOwnerBootstrapMode() === 'in_process') {
|
|
317
|
+
try {
|
|
318
|
+
await ensureInProcessOwner(session);
|
|
319
|
+
return {
|
|
320
|
+
success: true,
|
|
321
|
+
session,
|
|
322
|
+
restored: true,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return {
|
|
327
|
+
success: false,
|
|
328
|
+
reason: 'sticky_owner_unrecoverable',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (!(await isSessionAlive(session))) {
|
|
333
|
+
return {
|
|
334
|
+
success: false,
|
|
335
|
+
reason: 'sticky_owner_unrecoverable',
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
session.stickyOwner = await bootstrapDetachedStickyOwner(session);
|
|
340
|
+
return {
|
|
341
|
+
success: true,
|
|
342
|
+
session,
|
|
343
|
+
restored: true,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return {
|
|
348
|
+
success: false,
|
|
349
|
+
reason: 'sticky_owner_unrecoverable',
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (stickyOwner.transport.type === 'in_process') {
|
|
354
|
+
try {
|
|
355
|
+
await ensureInProcessOwner(session);
|
|
356
|
+
return {
|
|
357
|
+
success: true,
|
|
358
|
+
session,
|
|
359
|
+
restored: false,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return {
|
|
364
|
+
success: false,
|
|
365
|
+
reason: 'sticky_owner_unrecoverable',
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (stickyOwner.state === 'active' &&
|
|
370
|
+
!isStickyOwnerExpired(stickyOwner) &&
|
|
371
|
+
isProcessAlive(stickyOwner.pid) &&
|
|
372
|
+
(await canConnectToStickyOwnerEndpoint(stickyOwner.transport.endpoint))) {
|
|
373
|
+
return {
|
|
374
|
+
success: true,
|
|
375
|
+
session,
|
|
376
|
+
restored: false,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
await terminateBrowserSessionOwner(session);
|
|
380
|
+
if (!(await isSessionAlive(session))) {
|
|
381
|
+
return {
|
|
382
|
+
success: false,
|
|
383
|
+
reason: 'sticky_owner_unrecoverable',
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
markStickyOwnerState(session, 'recovering');
|
|
388
|
+
session.stickyOwner = await bootstrapDetachedStickyOwner(session);
|
|
389
|
+
return {
|
|
390
|
+
success: true,
|
|
391
|
+
session,
|
|
392
|
+
restored: true,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
session.stickyOwner = {
|
|
397
|
+
...stickyOwner,
|
|
398
|
+
state: 'dead',
|
|
399
|
+
};
|
|
400
|
+
return {
|
|
401
|
+
success: false,
|
|
402
|
+
reason: 'sticky_owner_unrecoverable',
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
export async function withStickyOwnerBrowser(session, operation) {
|
|
407
|
+
const stickyOwner = session.stickyOwner;
|
|
408
|
+
if (!stickyOwner || stickyOwner.transport.type === 'playwright_bind') {
|
|
409
|
+
const restored = await restoreBrowserSessionOwner(session);
|
|
410
|
+
if (!restored.success || !session.stickyOwner) {
|
|
411
|
+
throw new Error('sticky_owner_unrecoverable');
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (session.stickyOwner?.transport.type === 'playwright_bind') {
|
|
415
|
+
touchStickyOwnerLease(session);
|
|
416
|
+
const browser = await chromium.connect(session.stickyOwner.transport.endpoint, {
|
|
417
|
+
timeout: HOST_CONNECT_TIMEOUT_MS,
|
|
418
|
+
});
|
|
419
|
+
const stopHeartbeat = startStickyOwnerLeaseHeartbeat(session);
|
|
420
|
+
try {
|
|
421
|
+
return await operation(browser);
|
|
422
|
+
}
|
|
423
|
+
finally {
|
|
424
|
+
stopHeartbeat();
|
|
425
|
+
touchStickyOwnerLease(session);
|
|
426
|
+
await disconnectPlaywright(browser);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const owner = await ensureInProcessOwner(session);
|
|
430
|
+
return operation(owner.browser);
|
|
431
|
+
}
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stable testing helpers for packages that wrap AgentBrowse.
|
|
3
3
|
*/
|
|
4
|
+
export { createFixtureGroupStore, createFixtureMatchStore, createFixtureResolver, fixtureResolvedArtifact, fixtureResolvedValue, } from './match-resolve-fill.test-support.js';
|
|
4
5
|
export { installFetchBackedTestAssistiveRuntime, uninstallTestAssistiveRuntime, } from './assistive-runtime.test-support.js';
|
|
5
6
|
//# sourceMappingURL=testing.d.ts.map
|
package/dist/testing.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,sCAAsC,EACtC,6BAA6B,GAC9B,MAAM,qCAAqC,CAAC"}
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,qBAAqB,EACrB,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EACL,sCAAsC,EACtC,6BAA6B,GAC9B,MAAM,qCAAqC,CAAC"}
|
package/dist/testing.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stable testing helpers for packages that wrap AgentBrowse.
|
|
3
3
|
*/
|
|
4
|
+
export { createFixtureGroupStore, createFixtureMatchStore, createFixtureResolver, fixtureResolvedArtifact, fixtureResolvedValue, } from './match-resolve-fill.test-support.js';
|
|
4
5
|
export { installFetchBackedTestAssistiveRuntime, uninstallTestAssistiveRuntime, } from './assistive-runtime.test-support.js';
|
package/docs/README.md
CHANGED
|
@@ -9,30 +9,47 @@ Public guides for `@mercuryo-ai/agentbrowse`.
|
|
|
9
9
|
turns the Quick Start into a working model for every main API.
|
|
10
10
|
3. [api-reference.md](./api-reference.md) — lookup for types, result
|
|
11
11
|
shapes, and error codes (keep open while coding).
|
|
12
|
-
4.
|
|
12
|
+
4. [match-resolve-fill.md](./match-resolve-fill.md) when your runtime
|
|
13
|
+
needs to decide which caller-supplied value belongs in which
|
|
14
|
+
observed field.
|
|
15
|
+
5. Only-when-needed: [configuration.md](./configuration.md),
|
|
13
16
|
[assistive-runtime.md](./assistive-runtime.md),
|
|
14
17
|
[protected-fill.md](./protected-fill.md).
|
|
15
|
-
|
|
18
|
+
6. Before shipping: [integration-checklist.md](./integration-checklist.md)
|
|
16
19
|
and [troubleshooting.md](./troubleshooting.md).
|
|
17
20
|
|
|
18
21
|
Full list:
|
|
19
22
|
|
|
20
23
|
- [getting-started.md](./getting-started.md)
|
|
21
24
|
The first guide to read after the package README. It explains the normal
|
|
22
|
-
session flow and what each main API is for
|
|
25
|
+
session flow and what each main API is for, including the
|
|
26
|
+
`match` / `resolve` / `fill` primitives.
|
|
23
27
|
- [api-reference.md](./api-reference.md)
|
|
24
|
-
Compact reference for the public API, result shapes, observe payloads,
|
|
25
|
-
extraction schema input.
|
|
28
|
+
Compact reference for the public API, result shapes, observe payloads,
|
|
29
|
+
match/resolve/fill types, and extraction schema input.
|
|
30
|
+
|
|
31
|
+
## Match / resolve / fill
|
|
32
|
+
|
|
33
|
+
- [match-resolve-fill.md](./match-resolve-fill.md)
|
|
34
|
+
How the three primitives decide which value fits which observed field,
|
|
35
|
+
how to wire a resolver adapter, and the design rules (no raw values in
|
|
36
|
+
public results, stable resolved refs, adapter boundary).
|
|
37
|
+
- [protected-fill.md](./protected-fill.md)
|
|
38
|
+
How the low-level `fillProtectedForm(...)` path works and when to
|
|
39
|
+
prefer it over the `fill(session, form, plan, { resolver })` route.
|
|
40
|
+
- [testing.md](./testing.md)
|
|
41
|
+
Stable fixture builders for match/resolve/fill unit tests, plus the
|
|
42
|
+
fetch-backed assistive runtime helper for packages that wrap
|
|
43
|
+
AgentBrowse.
|
|
44
|
+
|
|
45
|
+
## Runtime and integration
|
|
46
|
+
|
|
26
47
|
- [configuration.md](./configuration.md)
|
|
27
48
|
Persistence, proxy, diagnostics, and client configuration.
|
|
28
49
|
- [assistive-runtime.md](./assistive-runtime.md)
|
|
29
|
-
How to connect an OpenAI-compatible LLM backend for extraction and
|
|
30
|
-
goal-based page understanding.
|
|
31
|
-
- [protected-fill.md](./protected-fill.md)
|
|
32
|
-
How protected fill works and when to use it instead of a normal fill action.
|
|
50
|
+
How to connect an OpenAI-compatible LLM backend for extraction and
|
|
51
|
+
better goal-based page understanding.
|
|
33
52
|
- [integration-checklist.md](./integration-checklist.md)
|
|
34
53
|
Checklist for packages and services that integrate AgentBrowse.
|
|
35
|
-
- [testing.md](./testing.md)
|
|
36
|
-
Stable testing helpers for packages that wrap AgentBrowse.
|
|
37
54
|
- [troubleshooting.md](./troubleshooting.md)
|
|
38
55
|
Common launch, observe, extract, and anti-bot issues.
|