imprint-mcp 0.7.0 → 0.7.1

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 (55) hide show
  1. package/README.md +64 -2
  2. package/package.json +1 -1
  3. package/prompts/auth-compile-agent.md +167 -22
  4. package/prompts/compile-agent.md +45 -8
  5. package/prompts/component-recovery.md +40 -0
  6. package/prompts/master-teach-decision.md +28 -3
  7. package/prompts/master-teach-focused-planner.md +18 -5
  8. package/src/cli.ts +26 -18
  9. package/src/imprint/agent-tool.ts +15 -0
  10. package/src/imprint/agent.ts +2 -11
  11. package/src/imprint/audit.ts +35 -13
  12. package/src/imprint/auth-browser-discovery.ts +276 -0
  13. package/src/imprint/auth-compile-agent.ts +513 -224
  14. package/src/imprint/auth-compile-tools.ts +213 -25
  15. package/src/imprint/auth-discovery-evidence.ts +81 -0
  16. package/src/imprint/auth-plan.ts +28 -0
  17. package/src/imprint/auth-verifier.ts +234 -20
  18. package/src/imprint/authentication-interaction.ts +137 -0
  19. package/src/imprint/backend-ladder.ts +218 -34
  20. package/src/imprint/build-plan.ts +2 -22
  21. package/src/imprint/cdp-browser-fetch.ts +192 -53
  22. package/src/imprint/chromium.ts +93 -29
  23. package/src/imprint/claude-cli-compile.ts +2 -0
  24. package/src/imprint/codex-cli-compile.ts +99 -46
  25. package/src/imprint/compile-agent-types.ts +9 -5
  26. package/src/imprint/compile-agent.ts +33 -0
  27. package/src/imprint/compile-tool-executor.ts +783 -0
  28. package/src/imprint/compile-tools.ts +42 -9
  29. package/src/imprint/compile-verification.ts +2 -0
  30. package/src/imprint/compile.ts +3 -0
  31. package/src/imprint/compiler-log.ts +16 -0
  32. package/src/imprint/component-recovery.ts +120 -0
  33. package/src/imprint/cookie-jar.ts +117 -7
  34. package/src/imprint/credential-extract.ts +9 -2
  35. package/src/imprint/credential-lock.ts +146 -0
  36. package/src/imprint/credential-observation.ts +261 -0
  37. package/src/imprint/credential-store.ts +247 -44
  38. package/src/imprint/live-component-recovery.ts +73 -0
  39. package/src/imprint/llm.ts +4 -3
  40. package/src/imprint/log.ts +16 -1
  41. package/src/imprint/login.ts +9 -0
  42. package/src/imprint/master-teach-agent-contracts.ts +40 -3
  43. package/src/imprint/master-teach-agents.ts +229 -14
  44. package/src/imprint/master-teach-checks.ts +8 -1
  45. package/src/imprint/master-teach-controller.ts +681 -138
  46. package/src/imprint/master-teach-prompt-projections.ts +2 -0
  47. package/src/imprint/mcp-compile-server.ts +62 -850
  48. package/src/imprint/native-teach-agents.ts +232 -108
  49. package/src/imprint/runtime.ts +357 -36
  50. package/src/imprint/stealth-chromium.ts +67 -7
  51. package/src/imprint/teach-browser-resources.ts +88 -0
  52. package/src/imprint/teach-stage-progress.ts +38 -0
  53. package/src/imprint/terminal-user-prompt.ts +42 -0
  54. package/src/imprint/types.ts +46 -18
  55. package/src/imprint/user-interaction.ts +836 -0
@@ -28,8 +28,14 @@
28
28
  * trusted browser session.
29
29
  */
30
30
 
31
+ import { randomUUID } from 'node:crypto';
31
32
  import CDP from 'chrome-remote-interface';
32
33
  import { getDomain } from 'tldts';
34
+ import {
35
+ type AuthBrowserAction,
36
+ AuthBrowserDiscovery,
37
+ type AuthBrowserObservation,
38
+ } from './auth-browser-discovery.ts';
33
39
  import { AKAMAI_SENSOR_COOKIE, abckFlag, isAbckValidated } from './bot-defense.ts';
34
40
  import {
35
41
  __registerChromePid,
@@ -37,6 +43,7 @@ import {
37
43
  launchChromium,
38
44
  proxyUrl,
39
45
  } from './chromium.ts';
46
+ import { recordBrowserCredentialObservation } from './credential-observation.ts';
40
47
  import { createLog } from './log.ts';
41
48
  import type { NavigationNetworkResponseMatcher } from './types.ts';
42
49
 
@@ -53,7 +60,10 @@ export interface MintedJar {
53
60
  httpOnly?: boolean;
54
61
  secure?: boolean;
55
62
  sameSite?: string;
63
+ hostOnly?: boolean;
56
64
  }>;
65
+ /** Factual outcome of the initial cookie planting; never contains values. */
66
+ seedCookieDiagnostics?: CookieSeedDiagnostics;
57
67
  /** The exact UA the bootstrap browser presented (HeadlessChrome stripped) —
58
68
  * replay fetches MUST send this verbatim or Akamai drops the jar. */
59
69
  ua: string;
@@ -83,6 +93,9 @@ export interface MintedJar {
83
93
  * seeded from the user's recorded session. Used only for accurate diagnostics
84
94
  * (both now carry `html`, so emptiness no longer distinguishes them). */
85
95
  source?: 'mint' | 'recording';
96
+ /** Durable credential input for a retained replay snapshot. A new credential
97
+ * state supersedes the entire snapshot, including cookies it had deleted. */
98
+ credentialStateHash?: string;
86
99
  }
87
100
 
88
101
  /** A session is replay-safe when `_abck` is validated (`~0~`) OR the Akamai
@@ -188,9 +201,14 @@ export interface CdpBrowserFetch {
188
201
  /** Snapshot every cookie in this dedicated browser, including cookies minted
189
202
  * on origins reached by redirects rather than the bootstrap origin. */
190
203
  snapshotCookies?(): Promise<MintedJar['cookies']>;
204
+ /** Read diagnostic state only when a client already exists. Unlike ordinary
205
+ * snapshotCookies, this never launches or bootstraps a replacement browser. */
206
+ snapshotExistingCookies?(): Promise<MintedJar['cookies'] | undefined>;
191
207
  /** Inspect the currently rendered page without navigating or mutating browser
192
208
  * state. Cookie values are intentionally excluded from this diagnostic view. */
193
209
  inspectPage?(): Promise<CdpPageSnapshot>;
210
+ /** Agent-selected discovery on this same browser, with ongoing network capture. */
211
+ interactBrowser?(action: AuthBrowserAction): Promise<AuthBrowserObservation>;
194
212
  /** Force the bootstrap navigation + `_abck` validation now; returns the
195
213
  * session cookies so callers can read session tokens (CSRF) for `${state.X}`. */
196
214
  ensureBootstrapped(): Promise<Array<{ name: string; value: string }>>;
@@ -215,6 +233,7 @@ export interface CdpPageSnapshot {
215
233
  httpOnly?: boolean;
216
234
  secure?: boolean;
217
235
  sameSite?: string;
236
+ hostOnly?: boolean;
218
237
  }>;
219
238
  }
220
239
 
@@ -489,6 +508,7 @@ export interface CdpBrowserFetchOptions {
489
508
  httpOnly?: boolean;
490
509
  sameSite?: string;
491
510
  expires?: number;
511
+ hostOnly?: boolean;
492
512
  }>;
493
513
  /** Browser storage to restore before page scripts run. Entries are scoped to
494
514
  * their recorded origin and applied on every matching document navigation. */
@@ -509,6 +529,101 @@ export interface CdpBrowserFetchOptions {
509
529
  }
510
530
 
511
531
  type CdpClient = Awaited<ReturnType<typeof CDP>>;
532
+
533
+ /** Read the target's complete cookie partition, rather than current-page URLs. */
534
+ export async function readCdpCookieSnapshot(
535
+ network: Pick<CdpClient['Network'], 'getAllCookies'>,
536
+ timeoutMs: number,
537
+ ): Promise<MintedJar['cookies']> {
538
+ // Network.getCookies({}) filters by the page and subframes. Keeping this
539
+ // command on the existing target also avoids reading another browser context.
540
+ const { cookies } = await withTimeout(
541
+ network.getAllCookies(),
542
+ 'CDP Network.getAllCookies',
543
+ timeoutMs,
544
+ );
545
+ return cookies.map((cookie) => ({
546
+ name: cookie.name,
547
+ value: cookie.value,
548
+ domain: cookie.domain,
549
+ path: cookie.path,
550
+ expires: cookie.expires > 0 ? cookie.expires : undefined,
551
+ httpOnly: cookie.httpOnly,
552
+ secure: cookie.secure,
553
+ sameSite: cookie.sameSite,
554
+ hostOnly: !cookie.domain.startsWith('.'),
555
+ }));
556
+ }
557
+
558
+ export interface CookieSeedDiagnostics {
559
+ attempted: number;
560
+ accepted: number;
561
+ rejected: number;
562
+ failed: number;
563
+ /** Positions identify input scopes without exposing names, paths or values. */
564
+ issues: Array<{ index: number; outcome: 'rejected' | 'error' }>;
565
+ issuesTruncated: boolean;
566
+ }
567
+
568
+ /** Preserve browser cookie scope and distinguish a rejected CDP command from
569
+ * a fulfilled one. A stale optional cookie does not make navigation fail. */
570
+ export async function seedCdpCookies(
571
+ network: Pick<CdpClient['Network'], 'setCookie'>,
572
+ cookies: NonNullable<CdpBrowserFetchOptions['seedCookies']>,
573
+ timeoutMs: number,
574
+ ): Promise<CookieSeedDiagnostics> {
575
+ const diagnostics: CookieSeedDiagnostics = {
576
+ attempted: cookies.length,
577
+ accepted: 0,
578
+ rejected: 0,
579
+ failed: 0,
580
+ issues: [],
581
+ issuesTruncated: false,
582
+ };
583
+ const issue = (index: number, outcome: 'rejected' | 'error'): void => {
584
+ if (diagnostics.issues.length < 50) diagnostics.issues.push({ index, outcome });
585
+ else diagnostics.issuesTruncated = true;
586
+ };
587
+ for (const [index, cookie] of cookies.entries()) {
588
+ try {
589
+ const hostOnly = cookie.hostOnly ?? !cookie.domain.startsWith('.');
590
+ const url = new URL(
591
+ `${cookie.secure ? 'https' : 'http'}://${cookie.domain.replace(/^\./, '')}/`,
592
+ );
593
+ url.pathname = cookie.path ?? '/';
594
+ const result = await withTimeout(
595
+ network.setCookie({
596
+ name: cookie.name,
597
+ value: cookie.value,
598
+ url: url.href,
599
+ ...(hostOnly ? {} : { domain: `.${cookie.domain.replace(/^\./, '')}` }),
600
+ path: cookie.path ?? '/',
601
+ secure: cookie.secure ?? false,
602
+ httpOnly: cookie.httpOnly ?? false,
603
+ ...(cookie.sameSite ? { sameSite: normalizeSameSite(cookie.sameSite) } : {}),
604
+ ...(typeof cookie.expires === 'number' && cookie.expires > 0
605
+ ? { expires: cookie.expires }
606
+ : {}),
607
+ }),
608
+ `CDP Network.setCookie(${index})`,
609
+ timeoutMs,
610
+ );
611
+ if (result.success === true) diagnostics.accepted++;
612
+ else if (result.success === false) {
613
+ diagnostics.rejected++;
614
+ issue(index, 'rejected');
615
+ } else {
616
+ diagnostics.failed++;
617
+ issue(index, 'error');
618
+ }
619
+ } catch {
620
+ diagnostics.failed++;
621
+ issue(index, 'error');
622
+ }
623
+ }
624
+ return diagnostics;
625
+ }
626
+
512
627
  type LaunchedChromium = Awaited<ReturnType<typeof launchChromium>>;
513
628
  type ChromiumLauncher = (opts: Parameters<typeof launchChromium>[0]) => Promise<LaunchedChromium>;
514
629
  type CdpConnector = (port: number) => Promise<CdpClient>;
@@ -1106,6 +1221,7 @@ function buildUaOverride(rawUa: string): {
1106
1221
 
1107
1222
  /** Create a CDP-browser-backed fetch. Lazily launches Chrome on first use. */
1108
1223
  export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserFetch {
1224
+ const diagnosticBrowserId = randomUUID();
1109
1225
  const baseOrigin = new URL(opts.baseUrl).origin;
1110
1226
  // Navigate the bootstrap page when declared; otherwise the base URL — but
1111
1227
  // never an obvious API/.act endpoint (opening one cold yields an error page
@@ -1123,6 +1239,7 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1123
1239
  let bootstrapped = false;
1124
1240
  let appliedUa: string | undefined;
1125
1241
  let bootstrapResponseHeaders: Record<string, string> = {};
1242
+ let seedCookieDiagnostics: CookieSeedDiagnostics | undefined;
1126
1243
  let currentPageUrl = navUrl;
1127
1244
  let mainFrameId: string | undefined;
1128
1245
  let lastDocumentResponse: {
@@ -1141,8 +1258,12 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1141
1258
  const activeNetworkBodyReads = new Set<Promise<void>>();
1142
1259
  let finishActiveNetworkResponse: ((requestId: string) => void) | undefined;
1143
1260
  let navigationInProgress = false;
1261
+ let discoveryRequested = false;
1262
+ let discovery: AuthBrowserDiscovery | undefined;
1144
1263
 
1145
1264
  async function close(): Promise<void> {
1265
+ discovery?.close();
1266
+ discovery = undefined;
1146
1267
  const c = client;
1147
1268
  const ch = chrome;
1148
1269
  const chromePid = chrome?.process.pid;
@@ -1151,6 +1272,7 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1151
1272
  bootstrapped = false;
1152
1273
  appliedUa = undefined;
1153
1274
  bootstrapResponseHeaders = {};
1275
+ seedCookieDiagnostics = undefined;
1154
1276
  currentPageUrl = navUrl;
1155
1277
  mainFrameId = undefined;
1156
1278
  lastDocumentResponse = null;
@@ -1208,6 +1330,8 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1208
1330
  const { Runtime, Network, Input, Page } = client;
1209
1331
  await withTimeout(Runtime.enable(), 'CDP Runtime.enable', cdpCommandTimeoutMs);
1210
1332
  await withTimeout(Network.enable(), 'CDP Network.enable', cdpCommandTimeoutMs);
1333
+ if (discoveryRequested && !discovery)
1334
+ discovery = new AuthBrowserDiscovery(client, cdpCommandTimeoutMs);
1211
1335
  await withTimeout(Page.enable(), 'CDP Page.enable', cdpCommandTimeoutMs);
1212
1336
  if (typeof Page.getFrameTree === 'function') {
1213
1337
  const tree = await withTimeout(
@@ -1248,6 +1372,12 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1248
1372
  request?: { method?: string; url?: string };
1249
1373
  }) => {
1250
1374
  if (!event.requestId) return;
1375
+ recordBrowserCredentialObservation({
1376
+ event: 'browser-request',
1377
+ browserId: diagnosticBrowserId,
1378
+ requestId: event.requestId,
1379
+ request: event.request,
1380
+ });
1251
1381
  requestSequence++;
1252
1382
  requestFacts.set(event.requestId, {
1253
1383
  method: event.request?.method ?? '',
@@ -1268,6 +1398,22 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1268
1398
  }
1269
1399
  },
1270
1400
  );
1401
+ if (
1402
+ process.env.IMPRINT_CREDENTIAL_DIAGNOSTICS_PATH &&
1403
+ typeof Network.requestWillBeSentExtraInfo === 'function'
1404
+ ) {
1405
+ Network.requestWillBeSentExtraInfo(
1406
+ (event: { requestId?: string; headers?: Record<string, unknown> }) => {
1407
+ if (!event.requestId) return;
1408
+ recordBrowserCredentialObservation({
1409
+ event: 'browser-wire',
1410
+ browserId: diagnosticBrowserId,
1411
+ requestId: event.requestId,
1412
+ headers: event.headers,
1413
+ });
1414
+ },
1415
+ );
1416
+ }
1271
1417
  const onResponseReceived = (event: {
1272
1418
  requestId?: string;
1273
1419
  loaderId?: string;
@@ -1283,6 +1429,12 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1283
1429
  if (!url) return;
1284
1430
  const headers = normalizeCdpResponseHeaders(event.response?.headers ?? {});
1285
1431
  if (event.requestId) {
1432
+ recordBrowserCredentialObservation({
1433
+ event: 'browser-response',
1434
+ browserId: diagnosticBrowserId,
1435
+ requestId: event.requestId,
1436
+ status: event.response?.status,
1437
+ });
1286
1438
  const facts = requestFacts.get(event.requestId);
1287
1439
  const observed = {
1288
1440
  requestId: event.requestId,
@@ -1362,29 +1514,10 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1362
1514
  // the in-page protected POSTs succeed (the live bmak sensor then keeps it
1363
1515
  // re-validated between calls).
1364
1516
  if (opts.seedCookies && opts.seedCookies.length > 0) {
1365
- let planted = 0;
1366
- for (const c of opts.seedCookies) {
1367
- try {
1368
- await withTimeout(
1369
- Network.setCookie({
1370
- name: c.name,
1371
- value: c.value,
1372
- domain: c.domain,
1373
- path: c.path ?? '/',
1374
- secure: c.secure ?? false,
1375
- httpOnly: c.httpOnly ?? false,
1376
- ...(c.sameSite ? { sameSite: normalizeSameSite(c.sameSite) } : {}),
1377
- ...(typeof c.expires === 'number' && c.expires > 0 ? { expires: c.expires } : {}),
1378
- }),
1379
- `CDP Network.setCookie(${c.name})`,
1380
- shortCdpTimeoutMs,
1381
- );
1382
- planted++;
1383
- } catch {
1384
- // best-effort — a cookie Akamai re-issues on navigate isn't fatal
1385
- }
1386
- }
1387
- log(`seeded ${planted}/${opts.seedCookies.length} high-trust cookies before navigate`);
1517
+ seedCookieDiagnostics = await seedCdpCookies(Network, opts.seedCookies, shortCdpTimeoutMs);
1518
+ log(
1519
+ `seeded ${seedCookieDiagnostics.accepted}/${seedCookieDiagnostics.attempted} cookies before navigate; rejected=${seedCookieDiagnostics.rejected} failed=${seedCookieDiagnostics.failed}`,
1520
+ );
1388
1521
  }
1389
1522
  // Strip the `HeadlessChrome` UA token (Akamai's only headless edge-tell) and
1390
1523
  // send matching client hints — BEFORE navigating to the protected origin.
@@ -1677,6 +1810,21 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1677
1810
  // Route the cross-origin plain fetch through the SAME proxy as the browser
1678
1811
  // (Bun fetch `proxy` opt) so its egress IP matches the in-page traffic.
1679
1812
  const proxy = proxyUrl();
1813
+ const diagnosticRequestId = randomUUID();
1814
+ recordBrowserCredentialObservation({
1815
+ event: 'browser-request',
1816
+ browserId: diagnosticBrowserId,
1817
+ requestId: diagnosticRequestId,
1818
+ transport: 'browser-http',
1819
+ request: { method, url: fullUrl },
1820
+ });
1821
+ recordBrowserCredentialObservation({
1822
+ event: 'browser-wire',
1823
+ browserId: diagnosticBrowserId,
1824
+ requestId: diagnosticRequestId,
1825
+ transport: 'browser-http',
1826
+ headers: outHeaders,
1827
+ });
1680
1828
  const xResp = await globalThis.fetch(fullUrl, {
1681
1829
  method,
1682
1830
  headers: outHeaders,
@@ -1684,6 +1832,13 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1684
1832
  signal: init?.signal ?? undefined,
1685
1833
  ...(proxy ? { proxy } : {}),
1686
1834
  } as RequestInit);
1835
+ recordBrowserCredentialObservation({
1836
+ event: 'browser-response',
1837
+ browserId: diagnosticBrowserId,
1838
+ requestId: diagnosticRequestId,
1839
+ transport: 'browser-http',
1840
+ status: xResp.status,
1841
+ });
1687
1842
  // Re-inject the cross-origin response's Set-Cookie back into the browser's
1688
1843
  // cookie jar — ONLY when the auth compile agent opted in (the recorded login
1689
1844
  // carries its session through a cross-origin Set-Cookie). The plain fetch
@@ -1778,35 +1933,8 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
1778
1933
  });
1779
1934
  }) as typeof fetch;
1780
1935
 
1781
- const snapshotAllCookies = async (c: CdpClient): Promise<MintedJar['cookies']> => {
1782
- const cookies: MintedJar['cookies'] = [];
1783
- try {
1784
- // With no URL filter CDP returns the dedicated browser's complete jar.
1785
- // This includes host-only cookies minted on relying-party origins reached
1786
- // through redirects, which a bootstrap-origin-only query cannot see.
1787
- const res = await withTimeout(
1788
- c.Network.getCookies({}),
1789
- 'CDP Network.getCookies(all origins)',
1790
- cdpCommandTimeoutMs,
1791
- );
1792
- for (const ck of res.cookies as unknown as Array<Record<string, unknown>>) {
1793
- cookies.push({
1794
- name: ck.name as string,
1795
- value: ck.value as string,
1796
- domain: ck.domain as string,
1797
- path: (ck.path as string) ?? '/',
1798
- expires:
1799
- typeof ck.expires === 'number' && ck.expires > 0 ? (ck.expires as number) : undefined,
1800
- httpOnly: ck.httpOnly as boolean | undefined,
1801
- secure: ck.secure as boolean | undefined,
1802
- sameSite: ck.sameSite as string | undefined,
1803
- });
1804
- }
1805
- } catch {
1806
- // best-effort
1807
- }
1808
- return cookies;
1809
- };
1936
+ const snapshotAllCookies = (c: CdpClient): Promise<MintedJar['cookies']> =>
1937
+ readCdpCookieSnapshot(c.Network, cdpCommandTimeoutMs);
1810
1938
 
1811
1939
  const readCurrentPageUrl = async (c: CdpClient): Promise<string> => {
1812
1940
  try {
@@ -2385,6 +2513,12 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
2385
2513
  return {
2386
2514
  fetchImpl,
2387
2515
  navigate,
2516
+ async interactBrowser(action) {
2517
+ discoveryRequested = true;
2518
+ const c = await ensure();
2519
+ discovery ??= new AuthBrowserDiscovery(c, cdpCommandTimeoutMs);
2520
+ return discovery.interact(action, buildNavigationClickTargetExpression);
2521
+ },
2388
2522
  async inspectPage(): Promise<CdpPageSnapshot> {
2389
2523
  const c = await ensure();
2390
2524
  const evaluation = await withTimeout(
@@ -2409,12 +2543,16 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
2409
2543
  httpOnly: cookie.httpOnly,
2410
2544
  secure: cookie.secure,
2411
2545
  sameSite: cookie.sameSite,
2546
+ hostOnly: cookie.hostOnly,
2412
2547
  })),
2413
2548
  };
2414
2549
  },
2415
2550
  async snapshotCookies() {
2416
2551
  return snapshotAllCookies(await ensure());
2417
2552
  },
2553
+ async snapshotExistingCookies() {
2554
+ return client ? snapshotAllCookies(client) : undefined;
2555
+ },
2418
2556
  async ensureBootstrapped() {
2419
2557
  const c = await ensure();
2420
2558
  try {
@@ -2458,6 +2596,7 @@ export function createCdpBrowserFetch(opts: CdpBrowserFetchOptions): CdpBrowserF
2458
2596
  abckFlag: abckFlag(abck),
2459
2597
  validated: jarCookiesValidated(cookies),
2460
2598
  source: 'mint',
2599
+ ...(seedCookieDiagnostics ? { seedCookieDiagnostics } : {}),
2461
2600
  };
2462
2601
  },
2463
2602
  close,
@@ -2561,7 +2700,7 @@ export function parseSetCookieForCdp(setCookie: string, requestUrl: string): Cdp
2561
2700
  const i = seg.indexOf('=');
2562
2701
  const k = (i < 0 ? seg : seg.slice(0, i)).trim().toLowerCase();
2563
2702
  const v = i < 0 ? '' : seg.slice(i + 1).trim();
2564
- if (k === 'domain' && v) ck.domain = v;
2703
+ if (k === 'domain' && v) ck.domain = `.${v.replace(/^\./, '')}`;
2565
2704
  else if (k === 'path' && v) ck.path = v;
2566
2705
  else if (k === 'secure') ck.secure = true;
2567
2706
  else if (k === 'httponly') ck.httpOnly = true;
@@ -6,12 +6,15 @@ import { type ChildProcess, spawnSync as nodeSpawnSync, spawn } from 'node:child
6
6
  import {
7
7
  closeSync,
8
8
  existsSync,
9
+ mkdtempSync,
9
10
  openSync,
10
11
  readFileSync,
11
12
  readdirSync,
13
+ rmSync,
12
14
  statSync,
13
15
  unlinkSync,
14
16
  } from 'node:fs';
17
+ import { rm } from 'node:fs/promises';
15
18
  import { createRequire } from 'node:module';
16
19
  import { createServer } from 'node:net';
17
20
  import { homedir, tmpdir } from 'node:os';
@@ -26,6 +29,7 @@ import { isDebug } from './log.ts';
26
29
  * Chrome-launch module, a dependency leaf) rather than in backend-ladder so the
27
30
  * ladder→cdp-browser-fetch import does not form a cycle. */
28
31
  const chromeProcessPids = new Set<number>();
32
+ const chromeTemporaryProfiles = new Map<number, string>();
29
33
 
30
34
  /** Register a Chrome PID for exit-time cleanup. Called on launch. */
31
35
  export function __registerChromePid(pid: number): void {
@@ -47,8 +51,16 @@ process.on('exit', () => {
47
51
  for (const pid of chromeProcessPids) {
48
52
  try {
49
53
  process.kill(pid, 'SIGKILL');
50
- } catch {
51
- // Already dead or not owned by this process — ignore.
54
+ } catch (error) {
55
+ if ((error as NodeJS.ErrnoException).code !== 'ESRCH') continue;
56
+ }
57
+ const profile = chromeTemporaryProfiles.get(pid);
58
+ if (profile) {
59
+ try {
60
+ rmSync(profile, { recursive: true, force: true });
61
+ } catch {
62
+ // A forced host exit cannot await cleanup; normal close reports errors.
63
+ }
52
64
  }
53
65
  }
54
66
  });
@@ -556,17 +568,18 @@ async function pickFreePort(): Promise<number> {
556
568
  });
557
569
  }
558
570
 
559
- async function waitForCdp(port: number, timeoutMs = 10_000): Promise<void> {
571
+ async function waitForCdp(port: number, timeoutMs = 10_000, signal?: AbortSignal): Promise<void> {
560
572
  const deadline = Date.now() + timeoutMs;
561
573
  let lastError: unknown;
562
574
  while (Date.now() < deadline) {
575
+ signal?.throwIfAborted();
563
576
  try {
564
- const res = await fetch(`http://127.0.0.1:${port}/json/version`);
577
+ const res = await fetch(`http://127.0.0.1:${port}/json/version`, { signal });
565
578
  if (res.ok) return;
566
579
  } catch (err) {
567
580
  lastError = err;
568
581
  }
569
- await sleep(100);
582
+ await sleep(100, undefined, { signal });
570
583
  }
571
584
  throw new Error(
572
585
  `Chromium did not open CDP on port ${port} within ${timeoutMs}ms (${String(lastError)})`,
@@ -578,8 +591,9 @@ export async function launchChromium(opts: LaunchOptions = {}): Promise<Launched
578
591
  log: (message) => process.stderr.write(`[imprint] ${message}\n`),
579
592
  }).path;
580
593
  const port = opts.port ?? (await pickFreePort());
594
+ const ownsProfile = opts.userDataDir === undefined;
581
595
  const userDataDir =
582
- opts.userDataDir ?? pathJoin(tmpdir(), `imprint-chrome-${Date.now()}-${process.pid}`);
596
+ opts.userDataDir ?? mkdtempSync(pathJoin(tmpdir(), `imprint-chrome-${process.pid}-`));
583
597
 
584
598
  const args = [
585
599
  `--remote-debugging-port=${port}`,
@@ -614,38 +628,88 @@ export async function launchChromium(opts: LaunchOptions = {}): Promise<Launched
614
628
  let xvfb: XvfbHandle | undefined;
615
629
  let display = opts.display ?? process.env.DISPLAY;
616
630
  if (process.platform === 'linux' && !opts.headless && !display) {
617
- xvfb = await startXvfb();
618
- display = xvfb.display;
631
+ try {
632
+ xvfb = await startXvfb();
633
+ display = xvfb.display;
634
+ } catch (error) {
635
+ if (ownsProfile) await rm(userDataDir, { recursive: true, force: true });
636
+ throw error;
637
+ }
619
638
  }
620
639
 
621
- const child = spawn(exe, args, {
622
- stdio: ['ignore', 'ignore', 'pipe'],
623
- detached: false,
624
- env: display ? { ...process.env, DISPLAY: display } : process.env,
625
- });
640
+ let child: ChildProcess;
641
+ try {
642
+ child = spawn(exe, args, {
643
+ stdio: ['ignore', 'ignore', 'pipe'],
644
+ detached: false,
645
+ env: display ? { ...process.env, DISPLAY: display } : process.env,
646
+ });
647
+ } catch (error) {
648
+ await xvfb?.close().catch(() => {});
649
+ if (ownsProfile) await rm(userDataDir, { recursive: true, force: true });
650
+ throw error;
651
+ }
652
+ if (child.pid !== undefined) {
653
+ __registerChromePid(child.pid);
654
+ if (ownsProfile) chromeTemporaryProfiles.set(child.pid, userDataDir);
655
+ }
626
656
 
627
657
  // Chromium is noisy — only surface stderr under IMPRINT_DEBUG.
628
658
  if (isDebug()) {
629
659
  child.stderr?.on('data', (chunk) => process.stderr.write(chunk));
630
660
  }
631
661
 
632
- const ready = waitForCdp(port);
633
-
634
- let closed = false;
635
- const close = async (): Promise<void> => {
636
- if (closed) return;
637
- closed = true;
638
- if (child.exitCode === null && child.signalCode === null) {
639
- child.kill('SIGTERM');
640
- await Promise.race([
641
- new Promise<void>((resolve) => child.once('exit', () => resolve())),
642
- sleep(2000),
643
- ]);
644
- if (child.exitCode === null) child.kill('SIGKILL');
645
- }
646
- // Tear down the virtual display we started for this launch (if any).
647
- await xvfb?.close().catch(() => {});
662
+ const startup = new AbortController();
663
+ let stopped = false;
664
+ let spawnError: Error | undefined;
665
+ const exit = Promise.withResolvers<void>();
666
+ let closing: Promise<void> | undefined;
667
+ const close = (): Promise<void> => {
668
+ closing ??= (async () => {
669
+ startup.abort(new Error('Chromium closed before CDP became ready'));
670
+ if (!stopped) {
671
+ child.kill('SIGTERM');
672
+ await Promise.race([exit.promise, sleep(2000)]);
673
+ if (!stopped) {
674
+ child.kill('SIGKILL');
675
+ await Promise.race([exit.promise, sleep(2000)]);
676
+ }
677
+ }
678
+ await xvfb?.close().catch(() => {});
679
+ if (!stopped) throw new Error('Chromium did not exit; preserving its profile');
680
+ if (ownsProfile) await rm(userDataDir, { recursive: true, force: true });
681
+ if (child.pid !== undefined) {
682
+ chromeTemporaryProfiles.delete(child.pid);
683
+ __unregisterChromePid(child.pid);
684
+ }
685
+ })();
686
+ return closing;
648
687
  };
688
+ child.once('error', (error) => {
689
+ spawnError = error;
690
+ if (child.pid === undefined) {
691
+ stopped = true;
692
+ exit.resolve();
693
+ }
694
+ startup.abort(error);
695
+ });
696
+ child.once('exit', () => {
697
+ stopped = true;
698
+ if (child.pid !== undefined) __unregisterChromePid(child.pid);
699
+ exit.resolve();
700
+ void close().catch((error) => {
701
+ process.stderr.write(`[imprint] browser cleanup: ${String(error)}\n`);
702
+ });
703
+ });
704
+ const ready = Promise.race([
705
+ waitForCdp(port, 10_000, startup.signal),
706
+ exit.promise.then(() => {
707
+ throw spawnError ?? new Error('Chromium exited before CDP became ready');
708
+ }),
709
+ ]).catch(async (error) => {
710
+ await close();
711
+ throw error;
712
+ });
649
713
 
650
714
  return { process: child, port, userDataDir, ready, close };
651
715
  }
@@ -85,6 +85,8 @@ const MAX_VERIFICATION_CYCLES = 5;
85
85
  /** Data-compile tools exposed to claude-cli. Keep this list in one exported
86
86
  * place so tests catch prompt/tool drift before a real teach run does. */
87
87
  export const DATA_COMPILE_TOOL_NAMES = [
88
+ 'prompt_user',
89
+ 'authenticate',
88
90
  'read_session_summary',
89
91
  'search_requests',
90
92
  'read_request',