@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.
- package/bundle/manifest.json +15 -15
- package/bundle/parall-browser-pod.js +29726 -375
- package/bundle/parall-channel-exec.js +2 -0
- package/bundle/parall-claude-agent.js +26091 -273
- package/bundle/parall-codex-agent.js +26650 -331
- package/bundle/parall-daemon.js +31384 -1998
- package/bundle/parall-openclaw-agent.js +1 -0
- package/dist/browser-pod.d.ts +23 -1
- package/dist/browser-pod.d.ts.map +1 -1
- package/dist/browser-pod.js +104 -34
- package/dist/browser-profile-reconcile.d.ts +21 -0
- package/dist/browser-profile-reconcile.d.ts.map +1 -0
- package/dist/browser-profile-reconcile.js +188 -0
- package/dist/clip-runtime/browser-cdp.d.ts +40 -0
- package/dist/clip-runtime/browser-cdp.d.ts.map +1 -0
- package/dist/clip-runtime/browser-cdp.js +218 -0
- package/dist/clip-runtime/browser-profile-manager.d.ts +97 -24
- package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -1
- package/dist/clip-runtime/browser-profile-manager.js +316 -182
- package/dist/clip-runtime/browser-profile-pool.d.ts +3 -0
- package/dist/clip-runtime/browser-profile-pool.d.ts.map +1 -1
- package/dist/clip-runtime/browser-profile-pool.js +18 -1
- package/dist/clip-runtime/browser-proxy-reconcile.d.ts +77 -0
- package/dist/clip-runtime/browser-proxy-reconcile.d.ts.map +1 -0
- package/dist/clip-runtime/browser-proxy-reconcile.js +139 -0
- package/dist/clip-runtime/browser-proxy-state.d.ts +55 -0
- package/dist/clip-runtime/browser-proxy-state.d.ts.map +1 -0
- package/dist/clip-runtime/browser-proxy-state.js +149 -0
- package/dist/clip-runtime/browser-quiescence.d.ts +71 -0
- package/dist/clip-runtime/browser-quiescence.d.ts.map +1 -0
- package/dist/clip-runtime/browser-quiescence.js +136 -0
- package/dist/clip-runtime/browser-readiness.d.ts +64 -0
- package/dist/clip-runtime/browser-readiness.d.ts.map +1 -0
- package/dist/clip-runtime/browser-readiness.js +161 -0
- package/dist/clip-runtime/browser-state-store.d.ts +13 -2
- package/dist/clip-runtime/browser-state-store.d.ts.map +1 -1
- package/dist/clip-runtime/browser-state-store.js +15 -6
- package/dist/clip-runtime/browser-target-registry.d.ts +143 -0
- package/dist/clip-runtime/browser-target-registry.d.ts.map +1 -0
- package/dist/clip-runtime/browser-target-registry.js +297 -0
- package/dist/clip-runtime/browser-viewer-streamer.d.ts +13 -14
- package/dist/clip-runtime/browser-viewer-streamer.d.ts.map +1 -1
- package/dist/clip-runtime/browser-viewer-streamer.js +11 -63
- package/dist/daemon-main.d.ts.map +1 -1
- package/dist/daemon-main.js +3 -1
- package/dist/supervisor.d.ts +6 -0
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +48 -187
- package/package.json +8 -6
package/dist/browser-pod.d.ts
CHANGED
|
@@ -68,6 +68,13 @@ export interface BrowserPodConfig {
|
|
|
68
68
|
/** Per-profile outbound proxy (controller-injected via PRLL_BROWSER_PROXY_* env on a
|
|
69
69
|
* cold pod, or the /assign body on a warm one). null = direct egress. */
|
|
70
70
|
proxy: BrowserProxyConfig | null;
|
|
71
|
+
/** Proxy readiness probe endpoint (PRLL_BROWSER_PROXY_PROBE_URL; the controller
|
|
72
|
+
* injects `<public api base>/generate_204` via ProxyProbeURLFromAPIBase).
|
|
73
|
+
* MUST be a PUBLIC, proxy-resolvable first-party 204 — never a cluster-internal
|
|
74
|
+
* /health host, which an external residential/datacenter proxy cannot resolve
|
|
75
|
+
* (the probe's whole point is "can we reach the public internet through this
|
|
76
|
+
* proxy"). Zero-byte 204 keeps per-GB proxy cost negligible. */
|
|
77
|
+
proxyProbeUrl: string;
|
|
71
78
|
}
|
|
72
79
|
/**
|
|
73
80
|
* Resolve and validate the pod's runtime config from the environment. Throws on
|
|
@@ -76,6 +83,14 @@ export interface BrowserPodConfig {
|
|
|
76
83
|
* doomed reconnect loop).
|
|
77
84
|
*/
|
|
78
85
|
export declare function resolveBrowserPodConfig(env?: NodeJS.ProcessEnv): BrowserPodConfig;
|
|
86
|
+
/** Probe endpoint for proxy readiness (browser-readiness.ts). The probe navigates
|
|
87
|
+
* a sacrificial tab THROUGH the profile's proxy, so the target must be a PUBLIC,
|
|
88
|
+
* proxy-resolvable host — the pod's cluster-internal clip-service (`clipRpcUrl`)
|
|
89
|
+
* could NEVER be resolved by an external residential/datacenter proxy, so it is
|
|
90
|
+
* deliberately not used here. Order: explicit `PRLL_BROWSER_PROXY_PROBE_URL`
|
|
91
|
+
* (controller may inject a per-env Parall-owned public 204), else a well-known
|
|
92
|
+
* public 204 fallback. `clipRpcUrl` is unused now; kept for signature stability. */
|
|
93
|
+
export declare function resolveProxyProbeUrl(env: NodeJS.ProcessEnv, _clipRpcUrl: string): string;
|
|
79
94
|
/** Resolved warm-pool config — the identity-less env a warm pod boots with. */
|
|
80
95
|
export interface PoolPodConfig {
|
|
81
96
|
/** Controller-minted pba_ pool bootstrap token; the bearer required on POST /assign. */
|
|
@@ -112,6 +127,9 @@ export interface AssignRequest {
|
|
|
112
127
|
orgId: string;
|
|
113
128
|
leaseId: string;
|
|
114
129
|
generation: number;
|
|
130
|
+
/** State epoch (reset_generation at assignment) — scopes the S3 state key.
|
|
131
|
+
* Absent from an older controller → 0 (legacy un-suffixed key). */
|
|
132
|
+
stateEpoch: number;
|
|
115
133
|
state?: AssignStateCreds;
|
|
116
134
|
/** Per-profile outbound proxy (warm-pod counterpart of the cold pod's
|
|
117
135
|
* PRLL_BROWSER_PROXY_* env). Absent = direct egress. */
|
|
@@ -205,10 +223,14 @@ export declare class BrowserPodRuntime {
|
|
|
205
223
|
private readonly log;
|
|
206
224
|
private readonly checkpointer?;
|
|
207
225
|
private readonly checkpointIntervalMs;
|
|
226
|
+
private readonly quiesce?;
|
|
208
227
|
private draining;
|
|
209
228
|
private drainPromise;
|
|
210
229
|
private checkpointTimer;
|
|
211
|
-
constructor(provider: PodProvider, browser: PodBrowser, log: Pick<GatewayLogger, 'info' | 'warn' | 'error'>, checkpointer?: PodStateCheckpointer | undefined, checkpointIntervalMs?: number)
|
|
230
|
+
constructor(provider: PodProvider, browser: PodBrowser, log: Pick<GatewayLogger, 'info' | 'warn' | 'error'>, checkpointer?: PodStateCheckpointer | undefined, checkpointIntervalMs?: number, quiesce?: (() => Promise<{
|
|
231
|
+
quiesced: boolean;
|
|
232
|
+
reason?: string;
|
|
233
|
+
}>) | undefined);
|
|
212
234
|
/**
|
|
213
235
|
* Route-readiness: registered to the hub AND not draining. `draining` is checked
|
|
214
236
|
* first so the readiness gate flips false the instant a drain begins — before
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-pod.d.ts","sourceRoot":"","sources":["../src/browser-pod.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAIH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAIlC,OAAO,EAAgB,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,2CAA2C,CAAC;
|
|
1
|
+
{"version":3,"file":"browser-pod.d.ts","sourceRoot":"","sources":["../src/browser-pod.ts"],"names":[],"mappings":"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"}
|
package/dist/browser-pod.js
CHANGED
|
@@ -52,6 +52,9 @@ import * as path from 'node:path';
|
|
|
52
52
|
import { pathToFileURL } from 'node:url';
|
|
53
53
|
import { createLogger } from '@parall/agent-core';
|
|
54
54
|
import { BrowserProfileManager, } from './clip-runtime/browser-profile-manager.js';
|
|
55
|
+
import { normalizeProxyConfig } from './clip-runtime/browser-proxy-state.js';
|
|
56
|
+
import { awaitBrowserQuiescence } from './clip-runtime/browser-quiescence.js';
|
|
57
|
+
import { PUBLIC_PROXY_PROBE_FALLBACK_URL } from './clip-runtime/browser-readiness.js';
|
|
55
58
|
import { BrowserStateStore } from './clip-runtime/browser-state-store.js';
|
|
56
59
|
import { ClipProvider } from './clip-runtime/clip-provider.js';
|
|
57
60
|
import { ClipProcessManager } from './clip-runtime/process-manager.js';
|
|
@@ -115,11 +118,30 @@ export function resolveBrowserPodConfig(env = process.env) {
|
|
|
115
118
|
stateDir,
|
|
116
119
|
homeDir,
|
|
117
120
|
proxy: parseProxyEnv(env),
|
|
121
|
+
proxyProbeUrl: resolveProxyProbeUrl(env, clipRpcUrl),
|
|
118
122
|
};
|
|
119
123
|
}
|
|
124
|
+
/** Probe endpoint for proxy readiness (browser-readiness.ts). The probe navigates
|
|
125
|
+
* a sacrificial tab THROUGH the profile's proxy, so the target must be a PUBLIC,
|
|
126
|
+
* proxy-resolvable host — the pod's cluster-internal clip-service (`clipRpcUrl`)
|
|
127
|
+
* could NEVER be resolved by an external residential/datacenter proxy, so it is
|
|
128
|
+
* deliberately not used here. Order: explicit `PRLL_BROWSER_PROXY_PROBE_URL`
|
|
129
|
+
* (controller may inject a per-env Parall-owned public 204), else a well-known
|
|
130
|
+
* public 204 fallback. `clipRpcUrl` is unused now; kept for signature stability. */
|
|
131
|
+
export function resolveProxyProbeUrl(env, _clipRpcUrl) {
|
|
132
|
+
const override = env.PRLL_BROWSER_PROXY_PROBE_URL?.trim();
|
|
133
|
+
if (override)
|
|
134
|
+
return override;
|
|
135
|
+
return PUBLIC_PROXY_PROBE_FALLBACK_URL;
|
|
136
|
+
}
|
|
120
137
|
/** Parse the controller-injected PRLL_BROWSER_PROXY_* env into a proxy config, or
|
|
121
138
|
* null when no proxy is set (PRLL_BROWSER_PROXY_SERVER absent → direct egress).
|
|
122
|
-
* The cold pod gets these as env; the warm pod's /assign writes the same vars.
|
|
139
|
+
* The cold pod gets these as env; the warm pod's /assign writes the same vars.
|
|
140
|
+
* Validation is the SHARED rule set (browser-proxy-state.ts, mirroring the Go
|
|
141
|
+
* server): orphan/half credentials, credential-less proxies (silently dropped by
|
|
142
|
+
* bb-browser → real-IP egress), SOCKS5+auth, and unknown schemes all FAIL CLOSED
|
|
143
|
+
* here — a misconfigured pod spec or a legacy DB row must crash the pod at boot,
|
|
144
|
+
* never leak traffic past the intended proxy. */
|
|
123
145
|
function parseProxyEnv(env) {
|
|
124
146
|
const server = env.PRLL_BROWSER_PROXY_SERVER?.trim();
|
|
125
147
|
// username + password are credential-like passthrough — kept VERBATIM (never
|
|
@@ -127,23 +149,12 @@ function parseProxyEnv(env) {
|
|
|
127
149
|
// a provider could legitimately use surrounding whitespace. Empty string → undefined.
|
|
128
150
|
const username = env.PRLL_BROWSER_PROXY_USERNAME || undefined;
|
|
129
151
|
const password = env.PRLL_BROWSER_PROXY_PASSWORD || undefined;
|
|
130
|
-
if (!server) {
|
|
131
|
-
|
|
132
|
-
// to direct egress: a misconfigured pod spec that drops the server var while
|
|
133
|
-
// keeping creds should crash here, not leak traffic past the intended proxy.
|
|
134
|
-
if (username !== undefined || password !== undefined) {
|
|
135
|
-
throw new Error('browser-pod: PRLL_BROWSER_PROXY_USERNAME/PASSWORD set without PRLL_BROWSER_PROXY_SERVER');
|
|
136
|
-
}
|
|
137
|
-
return null; // no proxy at all → direct egress
|
|
152
|
+
if (!server && (username !== undefined || password !== undefined)) {
|
|
153
|
+
throw new Error('browser-pod: PRLL_BROWSER_PROXY_USERNAME/PASSWORD set without PRLL_BROWSER_PROXY_SERVER');
|
|
138
154
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
// runtime auth failure.
|
|
143
|
-
if ((username === undefined) !== (password === undefined)) {
|
|
144
|
-
throw new Error('browser-pod: PRLL_BROWSER_PROXY_USERNAME and PRLL_BROWSER_PROXY_PASSWORD must be set together');
|
|
145
|
-
}
|
|
146
|
-
return { server, username, password };
|
|
155
|
+
if (!server)
|
|
156
|
+
return null; // no proxy at all → direct egress
|
|
157
|
+
return normalizeProxyConfig({ server, username, password });
|
|
147
158
|
}
|
|
148
159
|
/**
|
|
149
160
|
* Resolve a warm pod's identity-less config. Unlike resolveBrowserPodConfig this
|
|
@@ -192,6 +203,9 @@ export function parseAssignRequest(body) {
|
|
|
192
203
|
const orgId = str(b.org_id);
|
|
193
204
|
const leaseId = str(b.lease_id);
|
|
194
205
|
const generation = typeof b.generation === 'number' && Number.isFinite(b.generation) ? b.generation : 0;
|
|
206
|
+
const stateEpoch = typeof b.state_epoch === 'number' && Number.isSafeInteger(b.state_epoch) && b.state_epoch > 0
|
|
207
|
+
? b.state_epoch
|
|
208
|
+
: 0;
|
|
195
209
|
if (!assignmentToken)
|
|
196
210
|
return { ok: false, error: 'assignment_token required' };
|
|
197
211
|
if (!assignmentToken.startsWith(ASSIGNMENT_TOKEN_PREFIX)) {
|
|
@@ -237,16 +251,19 @@ export function parseAssignRequest(body) {
|
|
|
237
251
|
// empty string → undefined. (server is a structured host:port, so it is trimmed.)
|
|
238
252
|
const username = (typeof p.username === 'string' ? p.username : '') || undefined;
|
|
239
253
|
const password = (typeof p.password === 'string' ? p.password : '') || undefined;
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
254
|
+
// Shared rule set (browser-proxy-state.ts, mirrors the Go server): half or
|
|
255
|
+
// missing credentials, SOCKS5+auth, and unknown schemes are rejected before
|
|
256
|
+
// they can reach account_create as a silent drop / confusing runtime failure.
|
|
257
|
+
try {
|
|
258
|
+
proxy = normalizeProxyConfig({ server, username, password }) ?? undefined;
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
244
262
|
}
|
|
245
|
-
proxy = { server, username, password };
|
|
246
263
|
}
|
|
247
264
|
return {
|
|
248
265
|
ok: true,
|
|
249
|
-
value: { assignmentToken, profileId, orgId, leaseId, generation, state, proxy },
|
|
266
|
+
value: { assignmentToken, profileId, orgId, leaseId, generation, stateEpoch, state, proxy },
|
|
250
267
|
};
|
|
251
268
|
}
|
|
252
269
|
/**
|
|
@@ -268,6 +285,7 @@ export function applyAssignmentEnv(env, req) {
|
|
|
268
285
|
env.AWS_REGION = req.state.region;
|
|
269
286
|
env.PRLL_BROWSER_STATE_BUCKET = req.state.bucket;
|
|
270
287
|
env.PRLL_STATE_GENERATION = String(req.generation);
|
|
288
|
+
env.PRLL_STATE_EPOCH = String(req.stateEpoch);
|
|
271
289
|
if (req.state.endpoint)
|
|
272
290
|
env.PRLL_BROWSER_STATE_ENDPOINT = req.state.endpoint;
|
|
273
291
|
}
|
|
@@ -362,18 +380,27 @@ export class BrowserPodRuntime {
|
|
|
362
380
|
log;
|
|
363
381
|
checkpointer;
|
|
364
382
|
checkpointIntervalMs;
|
|
383
|
+
quiesce;
|
|
365
384
|
draining = false;
|
|
366
385
|
drainPromise = null;
|
|
367
386
|
checkpointTimer = null;
|
|
368
387
|
constructor(provider, browser, log,
|
|
369
388
|
// Optional S3 state checkpointer (PR4). Absent => stateless pod (no
|
|
370
389
|
// hydrate/checkpoint); the drain order then matches the pre-PR4 contract.
|
|
371
|
-
checkpointer, checkpointIntervalMs = DEFAULT_CHECKPOINT_INTERVAL_MS
|
|
390
|
+
checkpointer, checkpointIntervalMs = DEFAULT_CHECKPOINT_INTERVAL_MS,
|
|
391
|
+
// Chrome-exit barrier before the FINAL checkpoint (browser-quiescence.ts):
|
|
392
|
+
// bb-browser exits without awaiting Chrome, so the drain must independently
|
|
393
|
+
// confirm the user-data-dir is quiet before snapshotting it. Absent (older
|
|
394
|
+
// wiring / stateless pod) => checkpoint immediately after browser.stop() as
|
|
395
|
+
// before. quiesced=false => the final checkpoint is SKIPPED — a possibly-torn
|
|
396
|
+
// snapshot must never be published (bounded loss, structured error).
|
|
397
|
+
quiesce) {
|
|
372
398
|
this.provider = provider;
|
|
373
399
|
this.browser = browser;
|
|
374
400
|
this.log = log;
|
|
375
401
|
this.checkpointer = checkpointer;
|
|
376
402
|
this.checkpointIntervalMs = checkpointIntervalMs;
|
|
403
|
+
this.quiesce = quiesce;
|
|
377
404
|
}
|
|
378
405
|
/**
|
|
379
406
|
* Route-readiness: registered to the hub AND not draining. `draining` is checked
|
|
@@ -436,17 +463,35 @@ export class BrowserPodRuntime {
|
|
|
436
463
|
catch (err) {
|
|
437
464
|
this.log.warn(`browser stop during drain failed: ${String(err)}`);
|
|
438
465
|
}
|
|
439
|
-
// Final checkpoint AFTER bb-browser has torn down
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
//
|
|
443
|
-
//
|
|
466
|
+
// Final checkpoint AFTER bb-browser has torn down AND Chrome's exit +
|
|
467
|
+
// write-quiescence are CONFIRMED (bb-browser SIGTERMs Chrome and exits
|
|
468
|
+
// without awaiting it, so browser.stop() returning does not mean the
|
|
469
|
+
// user-data-dir is quiet). An unconfirmed exit SKIPS the upload — a
|
|
470
|
+
// possibly-torn snapshot must never be published as good state; the last
|
|
471
|
+
// periodic checkpoint stands (bounded loss, design §3.2 "simplified
|
|
472
|
+
// durability", no controller-side verify/repair).
|
|
444
473
|
if (this.checkpointer) {
|
|
445
|
-
|
|
446
|
-
|
|
474
|
+
let safe = true;
|
|
475
|
+
if (this.quiesce) {
|
|
476
|
+
try {
|
|
477
|
+
const q = await this.quiesce();
|
|
478
|
+
if (!q.quiesced) {
|
|
479
|
+
safe = false;
|
|
480
|
+
this.log.error(`BROWSER_CHECKPOINT_UNSAFE: ${q.reason ?? 'browser did not quiesce'} — skipping the final checkpoint (the last periodic snapshot stands)`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
catch (err) {
|
|
484
|
+
safe = false;
|
|
485
|
+
this.log.error(`BROWSER_CHECKPOINT_UNSAFE: quiescence check failed (${String(err)}) — skipping the final checkpoint`);
|
|
486
|
+
}
|
|
447
487
|
}
|
|
448
|
-
|
|
449
|
-
|
|
488
|
+
if (safe) {
|
|
489
|
+
try {
|
|
490
|
+
await this.checkpointer.checkpointFinal();
|
|
491
|
+
}
|
|
492
|
+
catch (err) {
|
|
493
|
+
this.log.error(`final checkpoint during drain failed: ${String(err)}`);
|
|
494
|
+
}
|
|
450
495
|
}
|
|
451
496
|
}
|
|
452
497
|
})();
|
|
@@ -570,6 +615,18 @@ export function buildStateStore(config, env, log) {
|
|
|
570
615
|
if (!Number.isSafeInteger(gen) || gen < 0) {
|
|
571
616
|
throw new Error(`browser state enabled (bucket=${bucket}) but PRLL_STATE_GENERATION="${genRaw}" is not a non-negative safe integer — refusing to run a non-persisted profile (fail closed)`);
|
|
572
617
|
}
|
|
618
|
+
// Epoch is deliberately fail-OPEN to 0 (unlike the generation): a missing value
|
|
619
|
+
// means an older controller that predates epoch-scoped keys, and 0 IS the
|
|
620
|
+
// legacy key — the correct read for every profile that has never been reset
|
|
621
|
+
// under the new scheme. A garbled value still refuses (misconfiguration).
|
|
622
|
+
const epochRaw = env.PRLL_STATE_EPOCH?.trim();
|
|
623
|
+
let epoch = 0;
|
|
624
|
+
if (epochRaw) {
|
|
625
|
+
epoch = Number(epochRaw);
|
|
626
|
+
if (!Number.isSafeInteger(epoch) || epoch < 0) {
|
|
627
|
+
throw new Error(`browser state enabled (bucket=${bucket}) but PRLL_STATE_EPOCH="${epochRaw}" is not a non-negative safe integer — refusing to run a non-persisted profile (fail closed)`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
573
630
|
return new BrowserStateStore({
|
|
574
631
|
bucket,
|
|
575
632
|
region: env.AWS_REGION?.trim() || env.PRLL_BROWSER_STATE_REGION?.trim() || '',
|
|
@@ -577,6 +634,7 @@ export function buildStateStore(config, env, log) {
|
|
|
577
634
|
orgId: config.orgId,
|
|
578
635
|
profileId: config.profileId,
|
|
579
636
|
generation: gen,
|
|
637
|
+
epoch,
|
|
580
638
|
homeDir: config.homeDir,
|
|
581
639
|
log,
|
|
582
640
|
});
|
|
@@ -632,6 +690,7 @@ export async function preparePodRuntime(config, env, log) {
|
|
|
632
690
|
homeDir: config.homeDir,
|
|
633
691
|
log,
|
|
634
692
|
resolveProxy: () => config.proxy,
|
|
693
|
+
proxyProbeUrl: config.proxyProbeUrl,
|
|
635
694
|
});
|
|
636
695
|
// The process manager hosts no installable clips in pod mode — its only job is
|
|
637
696
|
// to advertise the synthetic "browser" capability (getProviderClips appends it
|
|
@@ -691,7 +750,18 @@ export async function preparePodRuntime(config, env, log) {
|
|
|
691
750
|
else {
|
|
692
751
|
log.info('browser state externalization disabled (no PRLL_BROWSER_STATE_BUCKET) — running stateless');
|
|
693
752
|
}
|
|
694
|
-
|
|
753
|
+
// Chrome-exit barrier before the final checkpoint — only meaningful when a
|
|
754
|
+
// checkpointer exists (its whole purpose is snapshot consistency). The
|
|
755
|
+
// manager's spawned process group is the ownership handle: quiescence waits
|
|
756
|
+
// for (and if needed, signals) exactly that group — never a scanned pid.
|
|
757
|
+
const quiesce = checkpointer
|
|
758
|
+
? () => awaitBrowserQuiescence({
|
|
759
|
+
pgid: browser.lastKnownProcessGroup(),
|
|
760
|
+
cdpEndpoint: browser.lastKnownCdpEndpoint(),
|
|
761
|
+
log,
|
|
762
|
+
})
|
|
763
|
+
: undefined;
|
|
764
|
+
return new BrowserPodRuntime(provider, browser, log, checkpointer, resolveCheckpointIntervalMs(env), quiesce);
|
|
695
765
|
}
|
|
696
766
|
/**
|
|
697
767
|
* Wire the real clip-runtime stack for a hosted browser pod and run until the
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { GatewayLogger } from '@parall/agent-core';
|
|
2
|
+
import type { ParallClient } from '@parall/sdk';
|
|
3
|
+
import type { BrowserProfilePool } from './clip-runtime/index.js';
|
|
4
|
+
export interface BrowserProfileReconcileContext {
|
|
5
|
+
/** The pool captured by the caller BEFORE the server fetch. */
|
|
6
|
+
pool: BrowserProfilePool;
|
|
7
|
+
/** The supervisor's CURRENT pool — a concurrent stop() may have nulled/replaced it. */
|
|
8
|
+
currentPool: () => BrowserProfilePool | null;
|
|
9
|
+
client: Pick<ParallClient, 'listMachineBrowserProfiles'>;
|
|
10
|
+
log: GatewayLogger;
|
|
11
|
+
/** Serialized per-profile op (stop/reset/release/wipe) — the supervisor's queue. */
|
|
12
|
+
enqueueOp: (profileId: string, op: () => Promise<void>) => Promise<void>;
|
|
13
|
+
/** Serialized per-profile REVIVE (open/ensure), tracked for reconnect fencing. */
|
|
14
|
+
enqueueRevive: (profileId: string, op: () => Promise<void>) => Promise<void>;
|
|
15
|
+
/** Wipe-then-report-error-on-failure, then re-throw so the caller skips the open. */
|
|
16
|
+
wipeBeforeRevive: (pool: BrowserProfilePool, profileId: string, resetGen: number, generation: number | undefined) => Promise<void>;
|
|
17
|
+
/** Snapshot of profiles with a queued-but-not-yet-live revive (pre-fetch). */
|
|
18
|
+
pendingReviveIds: () => Set<string>;
|
|
19
|
+
}
|
|
20
|
+
export declare function reconcileBrowserProfiles(ctx: BrowserProfileReconcileContext): Promise<void>;
|
|
21
|
+
//# sourceMappingURL=browser-profile-reconcile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-profile-reconcile.d.ts","sourceRoot":"","sources":["../src/browser-profile-reconcile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAyB,YAAY,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAYlE,MAAM,WAAW,8BAA8B;IAC7C,+DAA+D;IAC/D,IAAI,EAAE,kBAAkB,CAAC;IACzB,uFAAuF;IACvF,WAAW,EAAE,MAAM,kBAAkB,GAAG,IAAI,CAAC;IAC7C,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,4BAA4B,CAAC,CAAC;IACzD,GAAG,EAAE,aAAa,CAAC;IACnB,oFAAoF;IACpF,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzE,kFAAkF;IAClF,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,qFAAqF;IACrF,gBAAgB,EAAE,CAChB,IAAI,EAAE,kBAAkB,EACxB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GAAG,SAAS,KAC3B,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,8EAA8E;IAC9E,gBAAgB,EAAE,MAAM,GAAG,CAAC,MAAM,CAAC,CAAC;CACrC;AAED,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,8BAA8B,GAAG,OAAO,CAAC,IAAI,CAAC,CA8LjG"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
export async function reconcileBrowserProfiles(ctx) {
|
|
2
|
+
const pool = ctx.pool;
|
|
3
|
+
// Snapshot the locally-live set BEFORE the server fetch. A profile opened via a
|
|
4
|
+
// WS lifecycle event DURING the fetch is NOT in this snapshot, so the negative
|
|
5
|
+
// convergence below (which is stale w.r.t. that open) can never release it.
|
|
6
|
+
// There is no periodic browser reconcile to re-open a wrongly-released profile —
|
|
7
|
+
// it only runs on startup / WS-hello — so a stale release would strand it until
|
|
8
|
+
// the next reconnect.
|
|
9
|
+
const activeBeforeList = new Set(pool.activeProfileIds());
|
|
10
|
+
// Also snapshot revives (open/ensure) queued but not yet a live manager: such a
|
|
11
|
+
// stale open is invisible to activeProfileIds() and would otherwise drain after a
|
|
12
|
+
// missed offline stop/delete and start a runtime the server no longer wants.
|
|
13
|
+
const pendingReviveBeforeList = ctx.pendingReviveIds();
|
|
14
|
+
// And ALL local profile state — per-profile homes AND legacy flat account files
|
|
15
|
+
// (the pool owns that storage layout). State with NO server row is a
|
|
16
|
+
// deleted/reassigned profile whose cookies/storage must be wiped from this host;
|
|
17
|
+
// a pre-upgrade profile never reopened after migration exists only as a legacy flat
|
|
18
|
+
// file, so home-only enumeration would miss it. Pre-fetch so state created during
|
|
19
|
+
// the fetch is left alone.
|
|
20
|
+
const localStateBeforeList = new Set(pool.localStateProfileIds());
|
|
21
|
+
let profiles;
|
|
22
|
+
try {
|
|
23
|
+
profiles = await ctx.client.listMachineBrowserProfiles();
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
ctx.log.warn(`browser profile reconcile: list failed: ${String(err)}`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
// The pool must still be the same instance after the await — stop() may have
|
|
30
|
+
// nulled/replaced it during a concurrent shutdown; bail rather than act on a
|
|
31
|
+
// torn-down pool (also avoids NPE-ing through Promise.allSettled into warnings).
|
|
32
|
+
if (ctx.currentPool() !== pool)
|
|
33
|
+
return;
|
|
34
|
+
// Converge local runtimes to the server's desired-live set (the server is the
|
|
35
|
+
// SSOT). The supervisor is the desired-state coordinator; the pool is the
|
|
36
|
+
// per-profile runtime registry. Every op here goes through
|
|
37
|
+
// enqueueBrowserProfileOp so all supervisor-originated profile work shares one
|
|
38
|
+
// ordering layer (the pool's own queue is the final per-profile mutex).
|
|
39
|
+
const desiredLive = new Set();
|
|
40
|
+
const serverIds = new Set(); // every byoc profile the server still lists (any status)
|
|
41
|
+
const serverResetGen = new Map();
|
|
42
|
+
const ops = [];
|
|
43
|
+
// reset_generation (server) is the SSOT for reset intent; the daemon's per-profile
|
|
44
|
+
// sidecar (read via pool.appliedResetGeneration) is only a local ack of "already
|
|
45
|
+
// wiped up to this generation". A missed reset (server newer than the local ack) must
|
|
46
|
+
// wipe local state BEFORE any open/ensure, so a reopen starts clean and never
|
|
47
|
+
// resurrects the cookies the reset cleared. Returns the server reset_generation to
|
|
48
|
+
// apply, or null if nothing is missed. Pure (no side effects).
|
|
49
|
+
const missedResetGen = (profileId) => {
|
|
50
|
+
const gen = serverResetGen.get(profileId) ?? 0;
|
|
51
|
+
return gen > pool.appliedResetGeneration(profileId) ? gen : null;
|
|
52
|
+
};
|
|
53
|
+
// Negative-path helper (a stopped profile with a missed reset): fence synchronously
|
|
54
|
+
// then enqueue a STANDALONE wipe. No revive follows, so a wipe failure simply leaves
|
|
55
|
+
// the profile fenced and the sidecar unwritten → the next reconcile retries (the
|
|
56
|
+
// SSOT reset_generation is still unapplied locally). Fail-closed by construction.
|
|
57
|
+
const enqueueResetWipeIfMissed = (profileId) => {
|
|
58
|
+
const gen = missedResetGen(profileId);
|
|
59
|
+
if (gen === null)
|
|
60
|
+
return false;
|
|
61
|
+
// Fence SYNCHRONOUSLY before the async wipe — same contract as the WS stop/reset
|
|
62
|
+
// path. On a machine-WS reconnect whose clip-provider stream stays live, a hub
|
|
63
|
+
// invoke could otherwise run on the stale runtime in the gap before the wipe op
|
|
64
|
+
// starts. (In the negative loop this re-asserts an already-set fence within the same
|
|
65
|
+
// synchronous iteration — atomic, no revive can observe an intermediate generation.)
|
|
66
|
+
pool.fence(profileId);
|
|
67
|
+
ops.push(ctx.enqueueOp(profileId, () => pool.applyResetWipe(profileId, gen)));
|
|
68
|
+
return true;
|
|
69
|
+
};
|
|
70
|
+
for (const profile of profiles) {
|
|
71
|
+
// The daemon supervises only machine-bound (byoc) profiles; hosted profiles
|
|
72
|
+
// (null machine_id) run on the platform pool and are never owned by a daemon.
|
|
73
|
+
if (!profile.machine_id)
|
|
74
|
+
continue;
|
|
75
|
+
serverIds.add(profile.id);
|
|
76
|
+
serverResetGen.set(profile.id, profile.reset_generation ?? 0);
|
|
77
|
+
if (profile.status !== 'running' && profile.status !== 'pending')
|
|
78
|
+
continue;
|
|
79
|
+
desiredLive.add(profile.id);
|
|
80
|
+
// A missed reset must wipe SUCCESSFULLY before this profile is revived. Fence
|
|
81
|
+
// synchronously now (reject hub invokes in the gap before the queued wipe runs).
|
|
82
|
+
const wipeGen = missedResetGen(profile.id);
|
|
83
|
+
if (wipeGen !== null)
|
|
84
|
+
pool.fence(profile.id);
|
|
85
|
+
// Capture the fence generation AFTER any fence bump so a stop arriving while the op
|
|
86
|
+
// waits makes it skip as stale; carry the lifecycle generation so the daemon's
|
|
87
|
+
// `running` report is fenced server-side. Drive desired state through the CAPTURED
|
|
88
|
+
// pool (not enqueueBrowserProfileLifecycle, which re-reads this.browserProfilePool
|
|
89
|
+
// and would negative-ACK `error` for a pending profile if stop() nulled the field).
|
|
90
|
+
const sinceSeq = pool.stopSeqOf(profile.id);
|
|
91
|
+
const generation = profile.lifecycle_generation;
|
|
92
|
+
// forceStatusReport ONLY for a `pending` row: the daemon's last report may already be
|
|
93
|
+
// `running` (Chromium survived the WS drop), so a deduped repeat would leave the
|
|
94
|
+
// server stuck at `pending`. A `running` row keeps the normal dedup.
|
|
95
|
+
const forceStatusReport = profile.status === 'pending';
|
|
96
|
+
ops.push(ctx.enqueueRevive(profile.id, async () => {
|
|
97
|
+
// Reset-before-open, FAIL-CLOSED: a missed reset must wipe successfully BEFORE
|
|
98
|
+
// the revive un-fences and reopens. If applyResetWipe throws, the await
|
|
99
|
+
// propagates and ensureRuntime is SKIPPED — the profile stays fenced (the wipe
|
|
100
|
+
// fenced it and the applied-reset sidecar was NOT written), so the next reconcile
|
|
101
|
+
// retries because the SSOT reset_generation is still unapplied locally. This
|
|
102
|
+
// ordering is expressed locally; the enqueueBrowserProfileOp queue contract (a
|
|
103
|
+
// rejected op does not block later ops) is intentionally unchanged. A failed
|
|
104
|
+
// wipe reports `error` (visible) and re-throws here, so ensureRuntime is skipped.
|
|
105
|
+
if (wipeGen !== null)
|
|
106
|
+
await ctx.wipeBeforeRevive(pool, profile.id, wipeGen, generation);
|
|
107
|
+
// Reconnect recovery is liveness-only: ensure the runtime is live (account +
|
|
108
|
+
// tab) for both pending and running. The original open's `start_url` is NOT
|
|
109
|
+
// replayed (it rides only the live lifecycle event, not persisted); a `pending`
|
|
110
|
+
// profile whose open event the daemon missed comes up at the default page rather
|
|
111
|
+
// than a targetless `about:blank`. Persisting start_url is a separate follow-up.
|
|
112
|
+
await pool.ensureRuntime(profile.id, sinceSeq, generation, { forceStatusReport });
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
// Negative convergence over everything locally known but NOT desired — active
|
|
116
|
+
// runtimes, queued revives (open/ensure not yet a manager), and local state (homes +
|
|
117
|
+
// legacy flat files) — all from the PRE-FETCH snapshots, so a profile that appeared
|
|
118
|
+
// DURING the fetch is left alone (no periodic reconcile would undo a wrong teardown).
|
|
119
|
+
const candidates = new Set([
|
|
120
|
+
...activeBeforeList,
|
|
121
|
+
...pendingReviveBeforeList,
|
|
122
|
+
...localStateBeforeList,
|
|
123
|
+
]);
|
|
124
|
+
for (const profileId of candidates) {
|
|
125
|
+
if (desiredLive.has(profileId))
|
|
126
|
+
continue;
|
|
127
|
+
const isActive = activeBeforeList.has(profileId);
|
|
128
|
+
const hasLocalState = localStateBeforeList.has(profileId);
|
|
129
|
+
// Pending-only revive (queued open/ensure, no runtime, no on-disk state at the
|
|
130
|
+
// pre-fetch snapshot): the synchronous fence alone neutralizes the stale open (it
|
|
131
|
+
// bumps stopSeq, so the queued revive skips as stale). Do NOT AWAIT a teardown — it
|
|
132
|
+
// would block behind the possibly-stuck queue and stall the whole reconcile (the
|
|
133
|
+
// queued-stale-open deadlock).
|
|
134
|
+
if (!isActive && !hasLocalState) {
|
|
135
|
+
pool.fence(profileId);
|
|
136
|
+
// Race: a pending-only revive can COMPLETE during the list fetch (create a manager
|
|
137
|
+
// + on-disk state) BEFORE this fence — the snapshot is then stale, so the fence
|
|
138
|
+
// alone leaves an orphaned runtime/disk the negative loop never converged. Converge
|
|
139
|
+
// it FIRE-AND-FORGET (NOT in `ops`: a still-stuck revive queue must not hang the
|
|
140
|
+
// reconcile; it runs once the queue drains — after the fenced open skips or
|
|
141
|
+
// completes — and is a no-op if nothing was created):
|
|
142
|
+
// - deleted/reassigned → wipeAbsent: the old host must keep no session data.
|
|
143
|
+
// - present + missed reset → applyResetWipe: the completed open may have applied
|
|
144
|
+
// only its event's (older) reset_generation, not the current one.
|
|
145
|
+
// - present + stopped → releaseRuntime: free the orphaned Chromium; a plain
|
|
146
|
+
// stop keeps its cookies on disk.
|
|
147
|
+
let cleanup;
|
|
148
|
+
if (!serverIds.has(profileId)) {
|
|
149
|
+
cleanup = pool.wipeAbsent(profileId);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const wipeGen = missedResetGen(profileId);
|
|
153
|
+
cleanup =
|
|
154
|
+
wipeGen !== null
|
|
155
|
+
? pool.applyResetWipe(profileId, wipeGen)
|
|
156
|
+
: pool.releaseRuntime(profileId);
|
|
157
|
+
}
|
|
158
|
+
void cleanup.catch((err) => ctx.log.warn(`browser profile pending-revive cleanup failed for ${profileId}: ${String(err)}`));
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (!serverIds.has(profileId)) {
|
|
162
|
+
// Absent from the server list → deleted/reassigned → fence + wipe local state (the
|
|
163
|
+
// old host must not keep cookies/storage). No status report — server is the SSOT.
|
|
164
|
+
pool.fence(profileId);
|
|
165
|
+
ops.push(ctx.enqueueOp(profileId, () => pool.wipeAbsent(profileId)));
|
|
166
|
+
}
|
|
167
|
+
else if (enqueueResetWipeIfMissed(profileId)) {
|
|
168
|
+
// Present (stopped) with a missed reset → fenced synchronously + wipe enqueued.
|
|
169
|
+
}
|
|
170
|
+
else if (isActive) {
|
|
171
|
+
// Present (stopped) but the runtime is still live → fence + release the runtime
|
|
172
|
+
// only; a plain stop keeps its cookies on disk.
|
|
173
|
+
pool.fence(profileId);
|
|
174
|
+
ops.push(ctx.enqueueOp(profileId, () => pool.releaseRuntime(profileId)));
|
|
175
|
+
}
|
|
176
|
+
// else: present + stopped + on-disk state + no runtime + no missed reset → LEAVE
|
|
177
|
+
// ALONE (do NOT fence). Its stopped-fence is already held — the start-sequence
|
|
178
|
+
// pre-fence on a fresh daemon, or the persisted pool state on a WS reconnect — so a
|
|
179
|
+
// hub invoke is still rejected. Re-fencing here would bump stopSeq and drop an `open`
|
|
180
|
+
// that arrived during the list fetch (it captured an older sinceSeq, and the stale
|
|
181
|
+
// `stopped` snapshot doesn't reflect it yet), stranding the server at `pending`.
|
|
182
|
+
}
|
|
183
|
+
for (const result of await Promise.allSettled(ops)) {
|
|
184
|
+
if (result.status === 'rejected') {
|
|
185
|
+
ctx.log.warn(`browser profile reconcile op failed: ${String(result.reason)}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export declare class BrowserCdpClient {
|
|
2
|
+
private readonly socket;
|
|
3
|
+
private nextId;
|
|
4
|
+
private readonly pending;
|
|
5
|
+
private closed;
|
|
6
|
+
private constructor();
|
|
7
|
+
/**
|
|
8
|
+
* Connect to the browser-level CDP endpoint behind host:port (`/json/version`).
|
|
9
|
+
* `timeoutMs` bounds BOTH the /json/version fetch and the websocket handshake —
|
|
10
|
+
* injectable so tests can force a fast timeout against a hung server, and so a
|
|
11
|
+
* caller with a wall-clock budget can hand down its remaining budget.
|
|
12
|
+
*/
|
|
13
|
+
static connect(host: string, port: number, timeoutMs?: number): Promise<BrowserCdpClient>;
|
|
14
|
+
/** Send a browser-level CDP command and await its result. */
|
|
15
|
+
command<T>(method: string, params?: Record<string, unknown>): Promise<T>;
|
|
16
|
+
close(): void;
|
|
17
|
+
private onMessage;
|
|
18
|
+
private failAll;
|
|
19
|
+
}
|
|
20
|
+
/** The browserContextId owning a page target (empty for the default context). */
|
|
21
|
+
export declare function targetBrowserContextId(cdp: BrowserCdpClient, targetId: string): Promise<string>;
|
|
22
|
+
/**
|
|
23
|
+
* The full cookie jar of a BrowserContext (HttpOnly included) as CDP
|
|
24
|
+
* `Network.Cookie` objects — the capture side of cookie-preserving proxy
|
|
25
|
+
* reconciliation.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getContextCookies(cdp: BrowserCdpClient, browserContextId: string): Promise<Array<Record<string, unknown>>>;
|
|
28
|
+
/** Restore a captured jar into a (fresh) BrowserContext. */
|
|
29
|
+
export declare function setContextCookies(cdp: BrowserCdpClient, browserContextId: string, cookies: Array<Record<string, unknown>>): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Map a CDP `Network.Cookie` (as returned by Storage.getCookies) to a
|
|
32
|
+
* `Network.CookieParam` accepted by Storage.setCookies. Allowlisted fields only:
|
|
33
|
+
* read-side extras (`size`, `session`, `sameParty`, …) are not valid params. A
|
|
34
|
+
* session cookie (session=true / expires=-1) is restored WITHOUT `expires`,
|
|
35
|
+
* which is exactly what makes it a session cookie again. This is
|
|
36
|
+
* higher-fidelity than bb-browser's own account-JSON persistence (which drops
|
|
37
|
+
* `sameSite=None` and partition keys).
|
|
38
|
+
*/
|
|
39
|
+
export declare function toCookieParam(cookie: Record<string, unknown>): Record<string, unknown>;
|
|
40
|
+
//# sourceMappingURL=browser-cdp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-cdp.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-cdp.ts"],"names":[],"mappings":"AAyCA,qBAAa,gBAAgB;IAKP,OAAO,CAAC,QAAQ,CAAC,MAAM;IAJ3C,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqC;IAC7D,OAAO,CAAC,MAAM,CAAS;IAEvB,OAAO;IAOP;;;;;OAKG;WACU,OAAO,CAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,SAAqB,GAC7B,OAAO,CAAC,gBAAgB,CAAC;IAgD5B,6DAA6D;IAC7D,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IA0B5E,KAAK,IAAI,IAAI;IAUb,OAAO,CAAC,SAAS;IAmBjB,OAAO,CAAC,OAAO;CAOhB;AAED,iFAAiF;AACjF,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,gBAAgB,EACrB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CAKjB;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,gBAAgB,EACrB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAMzC;AAED,4DAA4D;AAC5D,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,gBAAgB,EACrB,gBAAgB,EAAE,MAAM,EACxB,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACtC,OAAO,CAAC,IAAI,CAAC,CAMf;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAkBtF"}
|