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.
- package/README.md +64 -2
- package/package.json +1 -1
- package/prompts/auth-compile-agent.md +167 -22
- package/prompts/compile-agent.md +45 -8
- package/prompts/component-recovery.md +40 -0
- package/prompts/master-teach-decision.md +28 -3
- package/prompts/master-teach-focused-planner.md +18 -5
- package/src/cli.ts +26 -18
- package/src/imprint/agent-tool.ts +15 -0
- package/src/imprint/agent.ts +2 -11
- package/src/imprint/audit.ts +35 -13
- package/src/imprint/auth-browser-discovery.ts +276 -0
- package/src/imprint/auth-compile-agent.ts +513 -224
- package/src/imprint/auth-compile-tools.ts +213 -25
- package/src/imprint/auth-discovery-evidence.ts +81 -0
- package/src/imprint/auth-plan.ts +28 -0
- package/src/imprint/auth-verifier.ts +234 -20
- package/src/imprint/authentication-interaction.ts +137 -0
- package/src/imprint/backend-ladder.ts +218 -34
- package/src/imprint/build-plan.ts +2 -22
- package/src/imprint/cdp-browser-fetch.ts +192 -53
- package/src/imprint/chromium.ts +93 -29
- package/src/imprint/claude-cli-compile.ts +2 -0
- package/src/imprint/codex-cli-compile.ts +99 -46
- package/src/imprint/compile-agent-types.ts +9 -5
- package/src/imprint/compile-agent.ts +33 -0
- package/src/imprint/compile-tool-executor.ts +783 -0
- package/src/imprint/compile-tools.ts +42 -9
- package/src/imprint/compile-verification.ts +2 -0
- package/src/imprint/compile.ts +3 -0
- package/src/imprint/compiler-log.ts +16 -0
- package/src/imprint/component-recovery.ts +120 -0
- package/src/imprint/cookie-jar.ts +117 -7
- package/src/imprint/credential-extract.ts +9 -2
- package/src/imprint/credential-lock.ts +146 -0
- package/src/imprint/credential-observation.ts +261 -0
- package/src/imprint/credential-store.ts +247 -44
- package/src/imprint/live-component-recovery.ts +73 -0
- package/src/imprint/llm.ts +4 -3
- package/src/imprint/log.ts +16 -1
- package/src/imprint/login.ts +9 -0
- package/src/imprint/master-teach-agent-contracts.ts +40 -3
- package/src/imprint/master-teach-agents.ts +229 -14
- package/src/imprint/master-teach-checks.ts +8 -1
- package/src/imprint/master-teach-controller.ts +681 -138
- package/src/imprint/master-teach-prompt-projections.ts +2 -0
- package/src/imprint/mcp-compile-server.ts +62 -850
- package/src/imprint/native-teach-agents.ts +232 -108
- package/src/imprint/runtime.ts +357 -36
- package/src/imprint/stealth-chromium.ts +67 -7
- package/src/imprint/teach-browser-resources.ts +88 -0
- package/src/imprint/teach-stage-progress.ts +38 -0
- package/src/imprint/terminal-user-prompt.ts +42 -0
- package/src/imprint/types.ts +46 -18
- 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
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
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 =
|
|
1782
|
-
|
|
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;
|
package/src/imprint/chromium.ts
CHANGED
|
@@ -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
|
-
|
|
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-${
|
|
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
|
-
|
|
618
|
-
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
|
633
|
-
|
|
634
|
-
let
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
sleep(2000)
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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',
|