@parall/daemon 1.45.0 → 1.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/bundle/manifest.json +15 -15
  2. package/bundle/parall-browser-pod.js +29726 -375
  3. package/bundle/parall-channel-exec.js +2 -0
  4. package/bundle/parall-claude-agent.js +26091 -273
  5. package/bundle/parall-codex-agent.js +26650 -331
  6. package/bundle/parall-daemon.js +31384 -1998
  7. package/bundle/parall-openclaw-agent.js +1 -0
  8. package/dist/browser-pod.d.ts +23 -1
  9. package/dist/browser-pod.d.ts.map +1 -1
  10. package/dist/browser-pod.js +104 -34
  11. package/dist/browser-profile-reconcile.d.ts +21 -0
  12. package/dist/browser-profile-reconcile.d.ts.map +1 -0
  13. package/dist/browser-profile-reconcile.js +188 -0
  14. package/dist/clip-runtime/browser-cdp.d.ts +40 -0
  15. package/dist/clip-runtime/browser-cdp.d.ts.map +1 -0
  16. package/dist/clip-runtime/browser-cdp.js +218 -0
  17. package/dist/clip-runtime/browser-profile-manager.d.ts +97 -24
  18. package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -1
  19. package/dist/clip-runtime/browser-profile-manager.js +316 -182
  20. package/dist/clip-runtime/browser-profile-pool.d.ts +3 -0
  21. package/dist/clip-runtime/browser-profile-pool.d.ts.map +1 -1
  22. package/dist/clip-runtime/browser-profile-pool.js +18 -1
  23. package/dist/clip-runtime/browser-proxy-reconcile.d.ts +77 -0
  24. package/dist/clip-runtime/browser-proxy-reconcile.d.ts.map +1 -0
  25. package/dist/clip-runtime/browser-proxy-reconcile.js +139 -0
  26. package/dist/clip-runtime/browser-proxy-state.d.ts +55 -0
  27. package/dist/clip-runtime/browser-proxy-state.d.ts.map +1 -0
  28. package/dist/clip-runtime/browser-proxy-state.js +149 -0
  29. package/dist/clip-runtime/browser-quiescence.d.ts +71 -0
  30. package/dist/clip-runtime/browser-quiescence.d.ts.map +1 -0
  31. package/dist/clip-runtime/browser-quiescence.js +136 -0
  32. package/dist/clip-runtime/browser-readiness.d.ts +64 -0
  33. package/dist/clip-runtime/browser-readiness.d.ts.map +1 -0
  34. package/dist/clip-runtime/browser-readiness.js +161 -0
  35. package/dist/clip-runtime/browser-state-store.d.ts +13 -2
  36. package/dist/clip-runtime/browser-state-store.d.ts.map +1 -1
  37. package/dist/clip-runtime/browser-state-store.js +15 -6
  38. package/dist/clip-runtime/browser-target-registry.d.ts +143 -0
  39. package/dist/clip-runtime/browser-target-registry.d.ts.map +1 -0
  40. package/dist/clip-runtime/browser-target-registry.js +297 -0
  41. package/dist/clip-runtime/browser-viewer-streamer.d.ts +13 -14
  42. package/dist/clip-runtime/browser-viewer-streamer.d.ts.map +1 -1
  43. package/dist/clip-runtime/browser-viewer-streamer.js +11 -63
  44. package/dist/daemon-main.d.ts.map +1 -1
  45. package/dist/daemon-main.js +3 -1
  46. package/dist/supervisor.d.ts +6 -0
  47. package/dist/supervisor.d.ts.map +1 -1
  48. package/dist/supervisor.js +48 -187
  49. package/package.json +8 -6
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Browser quiescence — the Chrome-exit barrier before a hosted pod's FINAL
3
+ * checkpoint.
4
+ *
5
+ * Why it exists: bb-browser's SIGTERM handler sends Chrome SIGTERM and
6
+ * `process.exit(0)`s WITHOUT awaiting it, so "the bb-browser child exited"
7
+ * does not mean the user-data-dir is quiet — an orphaned Chromium may still be
8
+ * flushing cookies/IndexedDB, and a snapshot taken then is torn (SQLite
9
+ * mid-write). The final checkpoint must run only after every browser process
10
+ * is provably gone.
11
+ *
12
+ * Ownership model: the daemon spawns bb-browser with `detached: true`, making
13
+ * it the LEADER of a fresh process group; bb-browser spawns Chrome with
14
+ * `detached: false` (verified against its bundled source), so Chrome and every
15
+ * renderer inherit that group. The group id (= the bb-browser child's pid) is
16
+ * therefore a complete, daemon-owned handle on the whole browser tree:
17
+ *
18
+ * - liveness = `kill(-pgid, 0)` (ESRCH ⇒ no member left);
19
+ * - shutdown = one signal to `-pgid` reaches every member;
20
+ * - safety = POSIX reserves a pid while it names a live process group, so
21
+ * the pgid cannot be recycled out from under us while any member lives.
22
+ * EPERM on the probe means "members exist but none are ours" — our whole
23
+ * tree (same uid, always signalable) must already be dead and the id was
24
+ * recycled — so it reads as exited, and signalling stops immediately.
25
+ *
26
+ * No pid scanning, no /proc string matching, no directory-mtime polling: a
27
+ * profile's group contains exactly the processes its manager spawned, so
28
+ * quiescing profile A can never signal profile B.
29
+ *
30
+ * One CDP refusal probe remains as a CONFIRMATION (the DevTools listener lives
31
+ * inside the Chrome process, so a refused connect independently corroborates
32
+ * process exit) — it is not a second ownership authority.
33
+ *
34
+ * Everything is bounded by ONE absolute deadline. If exit cannot be confirmed
35
+ * by then, the answer is `quiesced: false` and the caller must skip the
36
+ * checkpoint — never guess safety.
37
+ */
38
+ const DEFAULT_EXIT_DEADLINE_MS = 8_000;
39
+ const DEFAULT_ESCALATE_GRACE_MS = 2_000;
40
+ const DEFAULT_TOTAL_DEADLINE_MS = 20_000;
41
+ const DEFAULT_POLL_MS = 100;
42
+ const sleep = (ms) => new Promise((r) => setTimeout(r, Math.max(0, ms)));
43
+ /** Probe the group with signal 0. ESRCH → gone; EPERM → gone (see the header:
44
+ * our members are always signalable, so EPERM proves the id was recycled to a
45
+ * foreign group AFTER our tree fully exited). Anything else → assume alive
46
+ * (fail closed: an unreadable probe must not green-light a checkpoint). */
47
+ function groupState(pgid, kill) {
48
+ try {
49
+ kill(-pgid, 0);
50
+ return 'alive';
51
+ }
52
+ catch (err) {
53
+ const code = err.code;
54
+ return code === 'ESRCH' || code === 'EPERM' ? 'gone' : 'alive';
55
+ }
56
+ }
57
+ /** Signal the whole group; swallow ESRCH (already gone) and EPERM (recycled to
58
+ * a foreign group — never signal what we do not own). */
59
+ function signalGroup(pgid, signal, kill) {
60
+ try {
61
+ kill(-pgid, signal);
62
+ }
63
+ catch {
64
+ // ESRCH/EPERM: nothing of ours left to signal.
65
+ }
66
+ }
67
+ /**
68
+ * cdpAccepting: does the browser's DevTools endpoint still accept connections?
69
+ * The listener lives inside the Chrome process, so acceptance ⇒ a browser
70
+ * process survives. Bare fetch with an abort budget; any failure ⇒ refused.
71
+ */
72
+ async function cdpAccepting(endpoint, budgetMs) {
73
+ if (budgetMs <= 0)
74
+ return false;
75
+ try {
76
+ await fetch(`http://${endpoint.host}:${endpoint.port}/json/version`, {
77
+ signal: AbortSignal.timeout(Math.min(1_000, budgetMs)),
78
+ });
79
+ return true;
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ }
85
+ export async function awaitBrowserQuiescence(opts) {
86
+ const kill = opts.kill ?? process.kill.bind(process);
87
+ const poll = opts.pollMs ?? DEFAULT_POLL_MS;
88
+ const totalDeadline = Date.now() + (opts.totalDeadlineMs ?? DEFAULT_TOTAL_DEADLINE_MS);
89
+ const capped = (d) => Math.min(d, totalDeadline);
90
+ const remaining = () => totalDeadline - Date.now();
91
+ const log = opts.log;
92
+ if (opts.pgid == null) {
93
+ // Nothing was ever spawned — there is no writer to wait for.
94
+ return { quiesced: true, escalated: false };
95
+ }
96
+ const pgid = opts.pgid;
97
+ // Phase 1 — self-exit: bb-browser's shutdown already SIGTERM'd Chrome; give
98
+ // the group its own deadline to finish flushing and exit.
99
+ const exitDeadline = capped(Date.now() + (opts.exitDeadlineMs ?? DEFAULT_EXIT_DEADLINE_MS));
100
+ let escalated = false;
101
+ while (groupState(pgid, kill) === 'alive') {
102
+ if (Date.now() >= exitDeadline)
103
+ break;
104
+ await sleep(Math.min(poll, remaining()));
105
+ }
106
+ // Phase 2 — escalation: SIGTERM the group, then SIGKILL, each with a capped
107
+ // grace. A group that survives SIGKILL to the deadline is unconfirmable.
108
+ for (const signal of ['SIGTERM', 'SIGKILL']) {
109
+ if (groupState(pgid, kill) === 'gone')
110
+ break;
111
+ escalated = true;
112
+ log?.warn(`browser-quiescence: process group ${pgid} still alive — sending ${signal}`);
113
+ signalGroup(pgid, signal, kill);
114
+ const grace = capped(Date.now() + (opts.escalateGraceMs ?? DEFAULT_ESCALATE_GRACE_MS));
115
+ while (groupState(pgid, kill) === 'alive' && Date.now() < grace) {
116
+ await sleep(Math.min(poll, remaining()));
117
+ }
118
+ }
119
+ if (groupState(pgid, kill) === 'alive') {
120
+ return {
121
+ quiesced: false,
122
+ escalated,
123
+ reason: `browser process group ${pgid} survived SIGKILL to the deadline`,
124
+ };
125
+ }
126
+ // Phase 3 — CDP refusal confirmation (corroboration, not authority).
127
+ if (opts.cdpEndpoint && (await cdpAccepting(opts.cdpEndpoint, remaining()))) {
128
+ return {
129
+ quiesced: false,
130
+ escalated,
131
+ reason: 'CDP endpoint still accepting connections after the process group exited',
132
+ };
133
+ }
134
+ log?.info(`browser-quiescence: process group ${pgid} exited${escalated ? ' (escalated)' : ''}`);
135
+ return { quiesced: true, escalated };
136
+ }
@@ -0,0 +1,64 @@
1
+ import type { BrowserProxyConfig } from './browser-profile-manager.js';
2
+ import type { BrowserTargetRegistry } from './browser-target-registry.js';
3
+ /**
4
+ * Open readiness — an `open` may only report `running` after the runtime is
5
+ * PROVEN usable, not merely after the processes exist:
6
+ *
7
+ * 1. bb-browser answers and the profile's OWNED page target exists (a
8
+ * tab-scoped command round-trips against exactly that target);
9
+ * 2. when a proxy is configured, a real navigation through the proxy
10
+ * completes without a transport error — `net::ERR_*` (proxy connect, DNS,
11
+ * auth, tunnel, timeout) fails the open with a structured code, while any
12
+ * HTTP status FROM THE TARGET (4xx/5xx included) proves the tunnel works. A
13
+ * 407 is the exception: it is the PROXY demanding auth, not the target, so it
14
+ * fails closed as an auth failure — a wrong-credential proxy never passes.
15
+ *
16
+ * The probe navigates the owned tab to a ~zero-byte Parall-owned endpoint
17
+ * (PRLL_BROWSER_PROXY_PROBE_URL override for tests/ops). A 204/2xx response
18
+ * doesn't commit a navigation, so the page the user opened stays put; the
19
+ * verdict is read from bb-browser's per-tab network log (`failed` +
20
+ * `failureReason` carry Chromium's net error). Bounded polling — observation,
21
+ * never a fixed sleep.
22
+ */
23
+ export type BrowserReadinessCode = 'BROWSER_PROXY_UNREACHABLE' | 'BROWSER_PROXY_AUTH_FAILED' | 'BROWSER_OPEN_TIMEOUT' | 'BROWSER_RUNTIME_NOT_READY';
24
+ export declare class BrowserReadinessError extends Error {
25
+ readonly code: BrowserReadinessCode;
26
+ constructor(code: BrowserReadinessCode, message: string);
27
+ }
28
+ export interface ReadinessHost {
29
+ log: {
30
+ info(msg: string): void;
31
+ warn(msg: string): void;
32
+ };
33
+ targets: BrowserTargetRegistry;
34
+ sendCommand(request: Record<string, unknown> & {
35
+ method: string;
36
+ account?: string;
37
+ }): Promise<Record<string, unknown>>;
38
+ }
39
+ export interface OpenReadinessOptions {
40
+ proxy: BrowserProxyConfig | null;
41
+ /** Probe endpoint; tests point this at a local observable proxy target. */
42
+ probeUrl?: string;
43
+ deadlineMs?: number;
44
+ pollIntervalMs?: number;
45
+ }
46
+ /**
47
+ * Last-resort proxy readiness probe target when no Parall-owned public endpoint
48
+ * is derivable (e.g. a hosted pod without an injected probe URL). A well-known,
49
+ * globally-reachable, zero-body 204 (Google's connectivity check) — the probe
50
+ * only needs SOME public host the proxy can reach to prove egress; the "any HTTP
51
+ * status = reachable" rule means it never depends on this specific host's
52
+ * content. Overridable via PRLL_BROWSER_PROXY_PROBE_URL; tests always override
53
+ * it to a local observable proxy target, never the public internet.
54
+ */
55
+ export declare const PUBLIC_PROXY_PROBE_FALLBACK_URL = "https://www.gstatic.com/generate_204";
56
+ /** Throws BrowserReadinessError when the profile runtime is not actually usable. */
57
+ export declare function assertOpenReady(host: ReadinessHost, account: string, opts: OpenReadinessOptions): Promise<void>;
58
+ /**
59
+ * Map Chromium's net error on the probe request to a structured readiness
60
+ * failure. HTTP-level failures never reach here (any status = pass), so every
61
+ * failureReason on a proxied probe indicates the proxy path itself broke.
62
+ */
63
+ export declare function classifyProxyFailure(proxy: BrowserProxyConfig, failureReason: string): BrowserReadinessError;
64
+ //# sourceMappingURL=browser-readiness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-readiness.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-readiness.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAEvE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAG1E;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,MAAM,oBAAoB,GAC5B,2BAA2B,GAC3B,2BAA2B,GAC3B,sBAAsB,GACtB,2BAA2B,CAAC;AAEhC,qBAAa,qBAAsB,SAAQ,KAAK;IAE5C,QAAQ,CAAC,IAAI,EAAE,oBAAoB;gBAA1B,IAAI,EAAE,oBAAoB,EACnC,OAAO,EAAE,MAAM;CAKlB;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE;QAAE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAC1D,OAAO,EAAE,qBAAqB,CAAC;IAC/B,WAAW,CACT,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GACtE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACjC,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAMD;;;;;;;;GAQG;AACH,eAAO,MAAM,+BAA+B,yCAAyC,CAAC;AAEtF,oFAAoF;AACpF,wBAAsB,eAAe,CACnC,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,oBAAoB,GACzB,OAAO,CAAC,IAAI,CAAC,CA6Bf;AA+GD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,kBAAkB,EACzB,aAAa,EAAE,MAAM,GACpB,qBAAqB,CAqCvB"}
@@ -0,0 +1,161 @@
1
+ import { redactedProxyLabel } from './browser-proxy-reconcile.js';
2
+ import { sleep } from './subprocess.js';
3
+ export class BrowserReadinessError extends Error {
4
+ code;
5
+ constructor(code, message) {
6
+ super(`${code}: ${message}`);
7
+ this.code = code;
8
+ this.name = 'BrowserReadinessError';
9
+ }
10
+ }
11
+ const DEFAULT_PROBE_DEADLINE_MS = 20_000;
12
+ const DEFAULT_POLL_INTERVAL_MS = 250;
13
+ let probeSeq = 0;
14
+ /**
15
+ * Last-resort proxy readiness probe target when no Parall-owned public endpoint
16
+ * is derivable (e.g. a hosted pod without an injected probe URL). A well-known,
17
+ * globally-reachable, zero-body 204 (Google's connectivity check) — the probe
18
+ * only needs SOME public host the proxy can reach to prove egress; the "any HTTP
19
+ * status = reachable" rule means it never depends on this specific host's
20
+ * content. Overridable via PRLL_BROWSER_PROXY_PROBE_URL; tests always override
21
+ * it to a local observable proxy target, never the public internet.
22
+ */
23
+ export const PUBLIC_PROXY_PROBE_FALLBACK_URL = 'https://www.gstatic.com/generate_204';
24
+ /** Throws BrowserReadinessError when the profile runtime is not actually usable. */
25
+ export async function assertOpenReady(host, account, opts) {
26
+ // 1. Owned target exists + a command round-trips against exactly that target.
27
+ const targetId = await host.targets.resolveCommandTarget(account);
28
+ try {
29
+ await host.sendCommand({
30
+ method: 'network',
31
+ action: 'requests',
32
+ account,
33
+ tabId: targetId,
34
+ limit: 1,
35
+ });
36
+ }
37
+ catch (err) {
38
+ throw new BrowserReadinessError('BROWSER_RUNTIME_NOT_READY', `profile page did not answer a command: ${err instanceof Error ? err.message : String(err)}`);
39
+ }
40
+ // 2. Proxy reachability — only when a proxy is configured.
41
+ if (!opts.proxy)
42
+ return;
43
+ if (!opts.probeUrl) {
44
+ // No probe endpoint available (misconfigured host) — surface loudly rather
45
+ // than silently skipping the check the caller believes happened.
46
+ throw new BrowserReadinessError('BROWSER_RUNTIME_NOT_READY', 'no proxy probe endpoint configured (PRLL_BROWSER_PROXY_PROBE_URL)');
47
+ }
48
+ await probeProxy(host, account, opts);
49
+ }
50
+ /**
51
+ * Probe the proxy on a SACRIFICIAL profile-owned tab, never the user's page
52
+ * (fix #5). A fresh `tab_new {account}` lands in the profile's own
53
+ * BrowserContext (so it egresses through the same proxy), gets navigated to the
54
+ * ~zero-byte probe URL, and is CLOSED in `finally` — the user's primary tab
55
+ * keeps whatever URL it was on. The probe URL must be a public, proxy-resolvable
56
+ * endpoint (see the daemon/pod probe-URL resolution); a cluster-internal host
57
+ * would never resolve through an external proxy.
58
+ */
59
+ async function probeProxy(host, account, opts) {
60
+ const proxy = opts.proxy;
61
+ const tag = `prllprobe${(probeSeq++).toString(36)}${process.pid.toString(36)}`;
62
+ const probeUrl = withProbeTag(opts.probeUrl, tag);
63
+ const deadline = Date.now() + (opts.deadlineMs ?? DEFAULT_PROBE_DEADLINE_MS);
64
+ const interval = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
65
+ const created = await host.sendCommand({ method: 'tab_new', account, url: 'about:blank' });
66
+ const probeTabId = typeof created.tabId === 'string' ? created.tabId : undefined;
67
+ if (!probeTabId) {
68
+ throw new BrowserReadinessError('BROWSER_RUNTIME_NOT_READY', 'could not open a probe tab in the profile context');
69
+ }
70
+ try {
71
+ await host.sendCommand({ method: 'open', account, tabId: probeTabId, url: probeUrl });
72
+ while (Date.now() < deadline) {
73
+ const entry = await findProbeEntry(host, account, probeTabId, tag);
74
+ if (entry) {
75
+ // A 407 is the PROXY itself demanding auth — it proves the proxy is
76
+ // reachable but NOT that we authenticated, so it must fail closed as an
77
+ // auth failure, never count as a working tunnel (bad-credential case).
78
+ if (entry.status === 407) {
79
+ throw new BrowserReadinessError('BROWSER_PROXY_AUTH_FAILED', `proxy ${redactedProxyLabel(proxy)} returned 407 (authentication required/rejected)`);
80
+ }
81
+ // Any OTHER HTTP status — 4xx/5xx included — means the request traversed
82
+ // the proxy and got an answer from the TARGET: network path OK. (A 204
83
+ // probe also emits a benign ERR_ABORTED after the response; status wins.)
84
+ if (typeof entry.status === 'number') {
85
+ host.log.info(`[bb-browser] proxy probe ok for ${account} via ${redactedProxyLabel(proxy)} (HTTP ${entry.status})`);
86
+ return;
87
+ }
88
+ if (entry.failed === true && typeof entry.failureReason === 'string') {
89
+ throw classifyProxyFailure(proxy, entry.failureReason);
90
+ }
91
+ }
92
+ await sleep(interval);
93
+ }
94
+ throw new BrowserReadinessError('BROWSER_OPEN_TIMEOUT', `proxy probe through ${redactedProxyLabel(proxy)} did not complete within ${opts.deadlineMs ?? DEFAULT_PROBE_DEADLINE_MS}ms`);
95
+ }
96
+ finally {
97
+ // Always tear down the sacrificial tab — a leaked probe tab would accumulate
98
+ // and could be mistaken for the profile's primary. Best-effort: a close
99
+ // failure must not mask the probe verdict.
100
+ try {
101
+ await host.sendCommand({ method: 'close', account, tabId: probeTabId });
102
+ }
103
+ catch (err) {
104
+ host.log.warn(`[bb-browser] failed to close proxy probe tab for ${account}: ${String(err)}`);
105
+ }
106
+ }
107
+ }
108
+ async function findProbeEntry(host, account, targetId, tag) {
109
+ const result = await host.sendCommand({
110
+ method: 'network',
111
+ action: 'requests',
112
+ account,
113
+ tabId: targetId,
114
+ filter: tag,
115
+ });
116
+ const requests = Array.isArray(result.networkRequests)
117
+ ? result.networkRequests
118
+ : [];
119
+ // Newest matching entry wins (retries reuse the tab).
120
+ for (let i = requests.length - 1; i >= 0; i--) {
121
+ const r = requests[i];
122
+ if (typeof r.url === 'string' && r.url.includes(tag)) {
123
+ return {
124
+ status: typeof r.status === 'number' ? r.status : undefined,
125
+ failed: r.failed === true,
126
+ failureReason: typeof r.failureReason === 'string' ? r.failureReason : undefined,
127
+ };
128
+ }
129
+ }
130
+ return null;
131
+ }
132
+ /**
133
+ * Map Chromium's net error on the probe request to a structured readiness
134
+ * failure. HTTP-level failures never reach here (any status = pass), so every
135
+ * failureReason on a proxied probe indicates the proxy path itself broke.
136
+ */
137
+ export function classifyProxyFailure(proxy, failureReason) {
138
+ const reason = failureReason.replace(/^net::/, '');
139
+ const label = redactedProxyLabel(proxy);
140
+ // Chromium surfaces proxy-auth failure under several codes: ERR_PROXY_AUTH_*
141
+ // (challenge/requested), and ERR_{INVALID,MISSING}_AUTH_CREDENTIALS when the
142
+ // proxy REJECTS the supplied credentials — the wrong-password case, which does
143
+ // NOT contain "PROXY_AUTH". Fail closed as an auth failure (not merely
144
+ // "unreachable") so a bad credential never rides through as a transport blip.
145
+ if (/PROXY_AUTH|INVALID_AUTH_CREDENTIALS|MISSING_AUTH_CREDENTIALS|ERR_TOO_MANY_RETRIES/i.test(reason)) {
146
+ return new BrowserReadinessError('BROWSER_PROXY_AUTH_FAILED', `proxy ${label} rejected the credentials (${reason})`);
147
+ }
148
+ if (/TIMED_OUT/i.test(reason)) {
149
+ return new BrowserReadinessError('BROWSER_PROXY_UNREACHABLE', `proxy ${label} timed out (${reason})`);
150
+ }
151
+ if (/NAME_NOT_RESOLVED|NAME_RESOLUTION_FAILED/i.test(reason)) {
152
+ return new BrowserReadinessError('BROWSER_PROXY_UNREACHABLE', `proxy host in ${label} did not resolve (${reason})`);
153
+ }
154
+ // ERR_PROXY_CONNECTION_FAILED / ERR_TUNNEL_CONNECTION_FAILED /
155
+ // ERR_SOCKS_CONNECTION_FAILED / ERR_CONNECTION_REFUSED / ... — with a proxy
156
+ // configured, a transport error on the probe is a proxy-path failure.
157
+ return new BrowserReadinessError('BROWSER_PROXY_UNREACHABLE', `could not reach the network through proxy ${label} (${reason})`);
158
+ }
159
+ function withProbeTag(probeUrl, tag) {
160
+ return probeUrl.includes('?') ? `${probeUrl}&${tag}=1` : `${probeUrl}?${tag}=1`;
161
+ }
@@ -35,8 +35,16 @@ export interface BrowserStateLogger {
35
35
  warn(msg: string): void;
36
36
  error(msg: string): void;
37
37
  }
38
- /** buildStateKey is the SSOT key layout, mirrored by the Go controller. */
39
- export declare function buildStateKey(orgId: string, profileId: string): string;
38
+ /**
39
+ * buildStateKey is the SSOT key layout, mirrored by the Go controller
40
+ * (state_gc.go stateObjectKeyForEpoch). The state EPOCH (the profile's
41
+ * reset_generation at assignment time) scopes the key: epoch 0 is the legacy
42
+ * un-suffixed key so pre-epoch profiles need no data migration; each Reset bumps
43
+ * the epoch, so a new pod hydrates a fresh (empty) key while every pre-reset pod
44
+ * keeps reading/writing only its own superseded object — the old state is
45
+ * structurally unreachable, no tombstone or write-ordering needed.
46
+ */
47
+ export declare function buildStateKey(orgId: string, profileId: string, epoch?: number): string;
40
48
  /** Thrown by a StateBackend.put when a conditional write precondition fails (S3
41
49
  * 412). The store treats it as "lost the CAS race" and aborts the checkpoint. */
42
50
  export declare class PreconditionFailedError extends Error {
@@ -69,6 +77,9 @@ export interface BrowserStateStoreConfig {
69
77
  profileId: string;
70
78
  /** Lease activation generation — the monotonic fencing token (design §3.2). */
71
79
  generation: number;
80
+ /** State epoch (reset_generation at assignment) — scopes the S3 key, see
81
+ * buildStateKey. Missing/0 → the legacy un-suffixed key. */
82
+ epoch?: number;
72
83
  /** bb-browser user-data dir (BB_BROWSER_HOME) to snapshot/restore. */
73
84
  homeDir: string;
74
85
  /** Optional S3 endpoint (MinIO/dev); empty → AWS S3. */
@@ -1 +1 @@
1
- {"version":3,"file":"browser-state-store.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-state-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"browser-state-store.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-state-store.ts"],"names":[],"mappings":"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"}
@@ -35,8 +35,8 @@ import { createHash } from 'node:crypto';
35
35
  import { mkdirSync } from 'node:fs';
36
36
  import { gunzipSync, gzipSync } from 'node:zlib';
37
37
  import { GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client, } from '@aws-sdk/client-s3';
38
- /** S3 object user-metadata keys. Lowercase; the Go controller reads them
39
- * case-insensitively (state_store.go metaValue). SSOT — keep in lockstep. */
38
+ /** S3 object user-metadata keys. Lowercase; the Go side writes them in lockstep
39
+ * (state_gc.go). SSOT. */
40
40
  const META_GENERATION = 'generation';
41
41
  const META_SHA256 = 'sha256';
42
42
  const META_FINAL = 'final';
@@ -62,9 +62,18 @@ const ARCHIVE_EXCLUDES = [
62
62
  '*/component_crx_cache/*',
63
63
  '*/Crashpad/*',
64
64
  ];
65
- /** buildStateKey is the SSOT key layout, mirrored by the Go controller. */
66
- export function buildStateKey(orgId, profileId) {
67
- return `${STATE_KEY_PREFIX}/${orgId}/${profileId}/${STATE_OBJECT}`;
65
+ /**
66
+ * buildStateKey is the SSOT key layout, mirrored by the Go controller
67
+ * (state_gc.go stateObjectKeyForEpoch). The state EPOCH (the profile's
68
+ * reset_generation at assignment time) scopes the key: epoch 0 is the legacy
69
+ * un-suffixed key so pre-epoch profiles need no data migration; each Reset bumps
70
+ * the epoch, so a new pod hydrates a fresh (empty) key while every pre-reset pod
71
+ * keeps reading/writing only its own superseded object — the old state is
72
+ * structurally unreachable, no tombstone or write-ordering needed.
73
+ */
74
+ export function buildStateKey(orgId, profileId, epoch = 0) {
75
+ const base = `${STATE_KEY_PREFIX}/${orgId}/${profileId}`;
76
+ return epoch > 0 ? `${base}/e${epoch}/${STATE_OBJECT}` : `${base}/${STATE_OBJECT}`;
68
77
  }
69
78
  /** Thrown by a StateBackend.put when a conditional write precondition fails (S3
70
79
  * 412). The store treats it as "lost the CAS race" and aborts the checkpoint. */
@@ -87,7 +96,7 @@ export class BrowserStateStore {
87
96
  lastUploadedSha = null;
88
97
  constructor(cfg, backend) {
89
98
  this.cfg = cfg;
90
- this.key = buildStateKey(cfg.orgId, cfg.profileId);
99
+ this.key = buildStateKey(cfg.orgId, cfg.profileId, cfg.epoch ?? 0);
91
100
  // Injected backend wins (tests). Otherwise build a live S3 backend iff a
92
101
  // bucket is configured; no bucket → disabled (stateless pod).
93
102
  this.backend = backend ?? (cfg.bucket ? new S3Backend(buildS3Client(cfg), cfg.bucket) : null);
@@ -0,0 +1,143 @@
1
+ /**
2
+ * BrowserTargetRegistry — binds every browser command to a page target OWNED by
3
+ * the profile's bb-browser account, so no command can fall through to
4
+ * bb-browser's account-blind global routing.
5
+ *
6
+ * WHY. bb-browser-pro 0.15.0 resolves a tab-less command via
7
+ * `ensurePageTarget(undefined)` → global `currentTargetId` → `targets[0]`
8
+ * (verified in its daemon source; `request.account` is never consulted). Chrome
9
+ * is launched with a literal `about:blank` positional tab in the DEFAULT
10
+ * browser context — no account cookies, no account proxy — and nothing ever
11
+ * closes it, so it IS `targets[0]` on a fresh Chromium. Worse, the daemon's own
12
+ * tab-less `tab_list` warm-up is dispatched through `ensurePageTarget` too and
13
+ * pins the global `currentTargetId` to that blank page. Any agent command sent
14
+ * without an explicit `tabId` (navigate/get/click/screenshot/eval/...) then
15
+ * executes against a cookie-less, proxy-less page — including real-IP egress
16
+ * for proxy-configured profiles. The registry closes this by resolving an
17
+ * OWNED target (per `tab_list`'s per-tab `account` attribution) and stamping
18
+ * its full CDP targetId on every tab-addressed command.
19
+ *
20
+ * Ownership ground truth is bb-browser's own `tab_list` attribution (targetId →
21
+ * account, derived from its BrowserContext bookkeeping), so a recreated tab —
22
+ * `tab_new {account}` routes through `createTabInContext` → `ensureContext` —
23
+ * always lands in the profile's BrowserContext with its cookies re-injected,
24
+ * never the default context.
25
+ */
26
+ /**
27
+ * Thrown when a command's target cannot be bound to a profile-owned page: a
28
+ * caller-supplied ref that is not the profile's own tab (foreign tab, numeric
29
+ * global index, stale id), or an owned page that cannot be (re)created. The
30
+ * command is NEVER retargeted at another page — fail closed.
31
+ */
32
+ export declare class BrowserTargetError extends Error {
33
+ readonly code: 'BROWSER_TARGET_FOREIGN' | 'BROWSER_TARGET_UNAVAILABLE';
34
+ constructor(message: string, code: 'BROWSER_TARGET_FOREIGN' | 'BROWSER_TARGET_UNAVAILABLE');
35
+ }
36
+ /** The slice of BrowserProfileManager the registry drives (account-scoped /command). */
37
+ export interface TargetRegistryHost {
38
+ sendCommand(request: Record<string, unknown> & {
39
+ method: string;
40
+ account?: string;
41
+ }): Promise<Record<string, unknown>>;
42
+ }
43
+ /** A profile-owned page tab as reported by `tab_list` (account-attributed). */
44
+ export interface OwnedTab {
45
+ /** Full CDP targetId (`tabId` in bb-browser responses). */
46
+ targetId: string;
47
+ /** bb-browser short tab id (`tab` in responses). */
48
+ shortId?: string;
49
+ url?: string;
50
+ active?: boolean;
51
+ }
52
+ /** Tunable timings for domain-tab navigation commit (injectable so tests don't
53
+ * wait the full production budget). */
54
+ export interface TargetRegistryOptions {
55
+ /** How long to wait for a created domain tab to commit navigation (default 10s). */
56
+ commitTimeoutMs?: number;
57
+ /** Post-commit settle before the tab is used (default 750ms). */
58
+ commitSettleMs?: number;
59
+ /** Poll interval while waiting for commit (default 300ms). */
60
+ commitPollMs?: number;
61
+ }
62
+ export declare class BrowserTargetRegistry {
63
+ private readonly host;
64
+ private readonly opts;
65
+ constructor(host: TargetRegistryHost, opts?: TargetRegistryOptions);
66
+ /** `tab_list` rows owned by the account (per-tab `account` attribution). */
67
+ ownedTabs(account: string): Promise<OwnedTab[]>;
68
+ /**
69
+ * Resolve the full CDP targetId a command must run against.
70
+ *
71
+ * With `ref` (caller-supplied `tabId`/`tab`): it must be one of the account's
72
+ * OWN tabs (full targetId or short id) — anything else throws
73
+ * BrowserTargetError. Numeric refs are rejected outright: bb-browser resolves
74
+ * bare numbers as indices into the GLOBAL page-target array, which can address
75
+ * another context's page.
76
+ *
77
+ * Without `ref`: the owned tab Chrome marks active, else the first owned
78
+ * tab, else a fresh `about:blank` created INSIDE the account's
79
+ * BrowserContext (never the default context). Stateless — derived from the
80
+ * live tab_list every time.
81
+ */
82
+ resolveCommandTarget(account: string, ref?: unknown): Promise<string>;
83
+ /**
84
+ * Validate + canonicalize a caller-supplied tab ref against owned tabs only.
85
+ * The ref is matched as a string against owned full/short ids and NEVER
86
+ * forwarded raw — so bb-browser's numeric-index fallback (which indexes into
87
+ * the GLOBAL page array, any context) is unreachable.
88
+ */
89
+ private resolveExplicitRef;
90
+ /**
91
+ * Ensure the account owns at least one page tab, returning its targetId. A
92
+ * fresh account (or one whose pages were all closed) gets an `about:blank`
93
+ * tab created in ITS BrowserContext via `tab_new {account}` — bb-browser's
94
+ * `createTabInContext` re-activates the context (cookies re-injected) when it
95
+ * was disposed, so recovery never lands in the default context.
96
+ */
97
+ ensureOwnedTab(account: string): Promise<string>;
98
+ private createOwnedTab;
99
+ /** Owned tab currently on `host` (www-tolerant), optionally only a specific ref. */
100
+ findOwnedTabOnHost(account: string, host: string, onlyTargetId?: string): Promise<string | undefined>;
101
+ /** Whether any owned tab is already on `url` (same resource, http→https tolerant). */
102
+ findOwnedTabMatchingUrl(account: string, url: string): Promise<string | undefined>;
103
+ /**
104
+ * Account-scoped tab-by-domain resolution for `eval {domain}` (bb-browser's
105
+ * own `resolveTabByDomain` is account-blind — the reason this exists). Two
106
+ * properties the bare `tab_new` + eval approach lacked, both caught on the
107
+ * staging closed-loop E2E (2026-06-04):
108
+ *
109
+ * 1. Reuse: an existing account tab already on the domain is reused instead
110
+ * of opening a new tab per eval (upstream reuses matching tabs too).
111
+ * 2. Load wait: after creating a tab, wait for the navigation to commit
112
+ * before eval — upstream waits (~10s poll + settle); without it the clip
113
+ * script races `about:blank` and relative fetches fail
114
+ * ("Failed to parse URL from /hot.json").
115
+ */
116
+ resolveDomainTab(account: string, domain: string): Promise<string | undefined>;
117
+ /** Close a tab we created but could not commit, so it does not linger as a
118
+ * residual owned tab. Best-effort — a close failure (already gone / rejected)
119
+ * is swallowed. bb-browser's close command is `close` (→ Target.closeTarget). */
120
+ private closeTabBestEffort;
121
+ }
122
+ /**
123
+ * Commands that take no page target at all — everything else is tab-addressed
124
+ * and MUST carry an owned tabId. `tab_new`, `site_info`, and the `site_*` listing
125
+ * commands resolve/create their page inside the account's context upstream
126
+ * (createTabInContext / account-scoped adapter execution), so they pass `account`
127
+ * instead of a pinned tab. NOTE `tab_list` IS dispatched through bb-browser's
128
+ * `ensurePageTarget` (its global-current-tab side effect is harmless once every
129
+ * real command pins its own target). `site_run` is deliberately NOT tab-less:
130
+ * the manager resolves the adapter's domain via site_info and pins an owned tab
131
+ * (or fails closed for a domain-less adapter), closing the old global-current-tab
132
+ * fallthrough — see BrowserProfileManager.buildCommandRequest.
133
+ */
134
+ export declare const TABLESS_COMMANDS: Set<string>;
135
+ /** Host comparison tolerant of a `www.` prefix on either side. */
136
+ export declare function hostsMatch(a: string, b: string): boolean;
137
+ export declare function normalizeDomainUrl(domain: string): string;
138
+ export declare function comparableUrl(url: string): {
139
+ protocol: string;
140
+ host: string;
141
+ target: string;
142
+ } | null;
143
+ //# sourceMappingURL=browser-target-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-target-registry.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-target-registry.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;;;;GAKG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAGzC,QAAQ,CAAC,IAAI,EAAE,wBAAwB,GAAG,4BAA4B;gBADtE,OAAO,EAAE,MAAM,EACN,IAAI,EAAE,wBAAwB,GAAG,4BAA4B;CAKzE;AAED,wFAAwF;AACxF,MAAM,WAAW,kBAAkB;IACjC,WAAW,CACT,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GACtE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACrC;AAED,+EAA+E;AAC/E,MAAM,WAAW,QAAQ;IACvB,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;wCACwC;AACxC,MAAM,WAAW,qBAAqB;IACpC,oFAAoF;IACpF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iEAAiE;IACjE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,qBAAqB;IAM9B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBADJ,IAAI,EAAE,kBAAkB,EACxB,IAAI,GAAE,qBAA0B;IAGnD,4EAA4E;IACtE,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAoBrD;;;;;;;;;;;;;OAaG;IACG,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAU3E;;;;;OAKG;YACW,kBAAkB;IAgBhC;;;;;;OAMG;IACG,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAOxC,cAAc;IAqB5B,oFAAoF;IAC9E,kBAAkB,CACtB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAc9B,sFAAsF;IAChF,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAiBxF;;;;;;;;;;;;OAYG;IACG,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IA4CpF;;sFAEkF;YACpE,kBAAkB;CAOjC;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB,aAU3B,CAAC;AAEH,kEAAkE;AAClE,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAGxD;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAIzD;AAED,wBAAgB,aAAa,CAC3B,GAAG,EAAE,MAAM,GACV;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAW3D"}