imprint-mcp 0.7.0 → 0.7.2
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 +66 -2
- package/package.json +1 -1
- package/prompts/auth-compile-agent.md +173 -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 +271 -43
- package/src/imprint/build-plan.ts +2 -22
- package/src/imprint/cdp-browser-fetch.ts +242 -60
- 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 +147 -0
- package/src/imprint/credential-observation.ts +261 -0
- package/src/imprint/credential-store.ts +284 -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-cdp-sessions.ts +7 -2
- package/src/imprint/mcp-compile-server.ts +62 -850
- package/src/imprint/mcp-server.ts +667 -91
- package/src/imprint/mcp-session-recovery.ts +167 -0
- package/src/imprint/native-teach-agents.ts +232 -108
- package/src/imprint/runtime.ts +416 -47
- 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 +51 -18
- package/src/imprint/user-interaction.ts +836 -0
|
@@ -18,6 +18,7 @@ import { createHash } from 'node:crypto';
|
|
|
18
18
|
import { existsSync, readFileSync } from 'node:fs';
|
|
19
19
|
import { basename, dirname, resolve as pathResolve } from 'node:path';
|
|
20
20
|
import type { Page } from 'playwright';
|
|
21
|
+
import { AUTH_DISCOVERY_POOL_KEY } from './auth-browser-discovery.ts';
|
|
21
22
|
import { loadBackendsCacheStatus } from './backend-cache.ts';
|
|
22
23
|
import {
|
|
23
24
|
type CdpBrowserFetch,
|
|
@@ -34,7 +35,9 @@ import {
|
|
|
34
35
|
} from './cdp-jar-cache.ts';
|
|
35
36
|
import { proxyUrl } from './chromium.ts';
|
|
36
37
|
import { abortSignalError, abortableDelay, withAbortSignal } from './concurrency.ts';
|
|
37
|
-
import { RuntimeCookieJar } from './cookie-jar.ts';
|
|
38
|
+
import { RuntimeCookieJar, cookieScopeKey } from './cookie-jar.ts';
|
|
39
|
+
import { recordBrowserCookieState } from './credential-observation.ts';
|
|
40
|
+
import { compareAndSwapSiteCookies } from './credential-store.ts';
|
|
38
41
|
import { redactFreeformText } from './freeform-redact.ts';
|
|
39
42
|
import { createLog } from './log.ts';
|
|
40
43
|
import { runPlaybook } from './playbook-runner.ts';
|
|
@@ -57,6 +60,7 @@ import {
|
|
|
57
60
|
createStealthFetch,
|
|
58
61
|
} from './stealth-fetch.ts';
|
|
59
62
|
import { clearCachedToken, loadCachedToken, saveCachedToken } from './stealth-token-cache.ts';
|
|
63
|
+
import { prepareTeachBrowserContext } from './teach-browser-resources.ts';
|
|
60
64
|
import type { ResolvedTool } from './tool-loader.ts';
|
|
61
65
|
import { WorkflowSchema } from './types.ts';
|
|
62
66
|
import type {
|
|
@@ -346,6 +350,8 @@ export async function runWithLadder(
|
|
|
346
350
|
/** Per-site CDP browser pool so cdp-replay reuses a live Chrome across
|
|
347
351
|
* calls (~2-5s) instead of launching a fresh one each time (~33s). */
|
|
348
352
|
cdpPool?: Map<string, CdpBrowserFetch>;
|
|
353
|
+
/** A managed continuation must use the browser that produced it. */
|
|
354
|
+
authBrowser?: CdpBrowserFetch;
|
|
349
355
|
/** Per-session memo of the backend that last served each tool. Once set, the
|
|
350
356
|
* next call starts at that backend instead of re-walking the doomed early
|
|
351
357
|
* rungs — the runtime analog of the compile path's `compileWinningBackend`.
|
|
@@ -475,6 +481,7 @@ export async function runWithLadder(
|
|
|
475
481
|
options?.onResponse ? onResponse : undefined,
|
|
476
482
|
options?.onPreparedRequest ? onPreparedRequest : undefined,
|
|
477
483
|
options?.signal,
|
|
484
|
+
options?.cdpPool,
|
|
478
485
|
);
|
|
479
486
|
break;
|
|
480
487
|
case 'cdp-replay':
|
|
@@ -487,11 +494,17 @@ export async function runWithLadder(
|
|
|
487
494
|
options?.signal,
|
|
488
495
|
options?.onResponse ? onResponse : undefined,
|
|
489
496
|
options?.onPreparedRequest ? onPreparedRequest : undefined,
|
|
497
|
+
options?.authBrowser,
|
|
490
498
|
);
|
|
491
499
|
break;
|
|
492
500
|
case 'stealth-fetch': {
|
|
493
501
|
const paramsWithDefaults = withWorkflowDefaults(tool.workflow, params);
|
|
494
|
-
const sf = await ensureStealthFetch(
|
|
502
|
+
const sf = await ensureStealthFetch(
|
|
503
|
+
tool,
|
|
504
|
+
stealthCache,
|
|
505
|
+
paramsWithDefaults,
|
|
506
|
+
options?.signal,
|
|
507
|
+
);
|
|
495
508
|
// When the workflow declares a bootstrap block, mint its declared
|
|
496
509
|
// session-token state (CSRF cookies etc.) from the SAME stealth
|
|
497
510
|
// session that provides the transport cookies. Without this, a
|
|
@@ -810,8 +823,14 @@ async function getOrMintCdpJar(
|
|
|
810
823
|
cacheDir: string,
|
|
811
824
|
recordingDir: string,
|
|
812
825
|
forceFresh: boolean,
|
|
826
|
+
cdpPool?: Map<string, CdpBrowserFetch>,
|
|
827
|
+
signal?: AbortSignal,
|
|
813
828
|
): Promise<MintedJar | null> {
|
|
814
|
-
if (cdpJarMinterForTest)
|
|
829
|
+
if (cdpJarMinterForTest) {
|
|
830
|
+
await prepareTeachBrowserContext(cdpPool);
|
|
831
|
+
if (signal?.aborted) throw abortSignalError(signal);
|
|
832
|
+
return cdpJarMinterForTest(baseUrl, bootstrapUrl);
|
|
833
|
+
}
|
|
815
834
|
if (!forceFresh) {
|
|
816
835
|
let cached = loadJar(cacheDir);
|
|
817
836
|
// A recording NEWER than the cached jar supersedes it — e.g. the user
|
|
@@ -843,6 +862,8 @@ async function getOrMintCdpJar(
|
|
|
843
862
|
}
|
|
844
863
|
let cf: CdpBrowserFetch | undefined;
|
|
845
864
|
try {
|
|
865
|
+
await prepareTeachBrowserContext(cdpPool);
|
|
866
|
+
if (signal?.aborted) throw abortSignalError(signal);
|
|
846
867
|
cf = (cdpBrowserFetchFactoryForTest ?? createCdpBrowserFetch)({ baseUrl, bootstrapUrl });
|
|
847
868
|
const jar = await cf.mintJar();
|
|
848
869
|
if (jar.abckFlag !== '0') {
|
|
@@ -905,7 +926,7 @@ function jarLikelyStale(result: ToolResult): boolean {
|
|
|
905
926
|
|
|
906
927
|
function credentialSeedCookies(credentials: CredentialStore): MintedJar['cookies'] {
|
|
907
928
|
return credentials.cookies
|
|
908
|
-
.filter((c) => c.name && c.value && c.domain)
|
|
929
|
+
.filter((c) => c.name && c.value && c.domain && !c.httpResponse?.deleted)
|
|
909
930
|
.map((c) => ({
|
|
910
931
|
name: c.name,
|
|
911
932
|
value: c.value,
|
|
@@ -915,6 +936,7 @@ function credentialSeedCookies(credentials: CredentialStore): MintedJar['cookies
|
|
|
915
936
|
httpOnly: c.httpOnly,
|
|
916
937
|
secure: c.secure,
|
|
917
938
|
sameSite: c.sameSite,
|
|
939
|
+
hostOnly: c.hostOnly,
|
|
918
940
|
}));
|
|
919
941
|
}
|
|
920
942
|
|
|
@@ -922,11 +944,38 @@ function uniqueSeedCookies(cookies: MintedJar['cookies']): MintedJar['cookies']
|
|
|
922
944
|
if (cookies.length === 0) return undefined;
|
|
923
945
|
const byScope = new Map<string, MintedJar['cookies'][number]>();
|
|
924
946
|
for (const c of cookies) {
|
|
925
|
-
byScope.set(
|
|
947
|
+
byScope.set(cookieScopeKey({ ...c, path: c.path ?? '/' }), c);
|
|
926
948
|
}
|
|
927
949
|
return [...byScope.values()];
|
|
928
950
|
}
|
|
929
951
|
|
|
952
|
+
/** Content identity only: no cookie names or values imply session semantics. */
|
|
953
|
+
function credentialStateHash(credentials: CredentialStore): string {
|
|
954
|
+
const canonical = (value: unknown): unknown => {
|
|
955
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
956
|
+
if (value && typeof value === 'object')
|
|
957
|
+
return Object.fromEntries(
|
|
958
|
+
Object.entries(value)
|
|
959
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
960
|
+
.map(([key, entry]) => [key, canonical(entry)]),
|
|
961
|
+
);
|
|
962
|
+
return value;
|
|
963
|
+
};
|
|
964
|
+
return createHash('sha256')
|
|
965
|
+
.update(
|
|
966
|
+
JSON.stringify(
|
|
967
|
+
canonical({
|
|
968
|
+
cookies: credentials.cookies,
|
|
969
|
+
values: credentials.values,
|
|
970
|
+
storage: credentials.storage ?? [],
|
|
971
|
+
}),
|
|
972
|
+
),
|
|
973
|
+
)
|
|
974
|
+
.digest('hex');
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const cdpCredentialStates = new WeakMap<CdpBrowserFetch, string>();
|
|
978
|
+
|
|
930
979
|
/**
|
|
931
980
|
* fetch-bootstrap rung — the API anti-bot path. Mint a validated session jar via
|
|
932
981
|
* cdp-browser (real Chrome, used ONLY to bootstrap), CLOSE the browser, then
|
|
@@ -946,9 +995,10 @@ async function runFetchBootstrap(
|
|
|
946
995
|
onResponse?: (observation: ResponseObservation) => void,
|
|
947
996
|
onPreparedRequest?: (observation: PreparedRequestObservation) => void,
|
|
948
997
|
signal?: AbortSignal,
|
|
998
|
+
cdpPool?: Map<string, CdpBrowserFetch>,
|
|
949
999
|
): Promise<ToolResult> {
|
|
950
1000
|
const credentials = credentialOverride ??
|
|
951
|
-
(await loadCredentialStore(tool.site)) ?? {
|
|
1001
|
+
(await loadCredentialStore(tool.site, signal)) ?? {
|
|
952
1002
|
site: tool.site,
|
|
953
1003
|
cookies: [],
|
|
954
1004
|
values: {},
|
|
@@ -978,7 +1028,15 @@ async function runFetchBootstrap(
|
|
|
978
1028
|
);
|
|
979
1029
|
|
|
980
1030
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
981
|
-
const jar = await getOrMintCdpJar(
|
|
1031
|
+
const jar = await getOrMintCdpJar(
|
|
1032
|
+
baseUrl,
|
|
1033
|
+
bootstrapUrl,
|
|
1034
|
+
cacheDir,
|
|
1035
|
+
recordingDir,
|
|
1036
|
+
attempt > 0,
|
|
1037
|
+
cdpPool,
|
|
1038
|
+
signal,
|
|
1039
|
+
);
|
|
982
1040
|
if (signal?.aborted) throw abortSignalError(signal);
|
|
983
1041
|
if (!jar) {
|
|
984
1042
|
// Couldn't even launch the bootstrap browser → let the ladder escalate.
|
|
@@ -1076,9 +1134,10 @@ async function runCdpReplay(
|
|
|
1076
1134
|
signal?: AbortSignal,
|
|
1077
1135
|
onResponse?: (observation: ResponseObservation) => void,
|
|
1078
1136
|
onPreparedRequest?: (observation: PreparedRequestObservation) => void,
|
|
1137
|
+
authBrowser?: CdpBrowserFetch,
|
|
1079
1138
|
): Promise<ToolResult> {
|
|
1080
1139
|
const credentials = credentialOverride ??
|
|
1081
|
-
(await loadCredentialStore(tool.site)) ?? {
|
|
1140
|
+
(await loadCredentialStore(tool.site, signal)) ?? {
|
|
1082
1141
|
site: tool.site,
|
|
1083
1142
|
cookies: [],
|
|
1084
1143
|
values: {},
|
|
@@ -1101,9 +1160,60 @@ async function runCdpReplay(
|
|
|
1101
1160
|
: undefined;
|
|
1102
1161
|
|
|
1103
1162
|
const recordingDir = pathResolve(tool.dir, '..');
|
|
1104
|
-
const cacheDir =
|
|
1105
|
-
|
|
1106
|
-
|
|
1163
|
+
const cacheDir = pathResolve(
|
|
1164
|
+
compileBackendStateDir(tool.dir, 'cdp-replay'),
|
|
1165
|
+
createHash('sha256')
|
|
1166
|
+
.update(bootstrapUrl ?? baseUrl)
|
|
1167
|
+
.digest('hex'),
|
|
1168
|
+
);
|
|
1169
|
+
const pinned =
|
|
1170
|
+
authBrowser && cdpPool
|
|
1171
|
+
? [...cdpPool].find(([, browser]) => browser === authBrowser)?.[0]
|
|
1172
|
+
: undefined;
|
|
1173
|
+
if (authBrowser && pinned === undefined) {
|
|
1174
|
+
return {
|
|
1175
|
+
ok: false,
|
|
1176
|
+
error: 'AUTH_EXPIRED',
|
|
1177
|
+
message: 'The browser for this sign-in has closed.',
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
const requestedKey = cdpReplayPoolKey(tool.site, tool.workflow.toolName, bootstrapUrl ?? baseUrl);
|
|
1181
|
+
if (
|
|
1182
|
+
pinned &&
|
|
1183
|
+
pinned !== requestedKey &&
|
|
1184
|
+
tool.workflow.bootstrap?.captures?.some((capture) => capture.source !== 'cookie')
|
|
1185
|
+
) {
|
|
1186
|
+
return {
|
|
1187
|
+
ok: false,
|
|
1188
|
+
error: 'STATE_MISSING',
|
|
1189
|
+
message:
|
|
1190
|
+
'This sign-in action needs captures from a different bootstrap page. Use standalone sign-in.',
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
const poolKey = pinned ?? requestedKey;
|
|
1194
|
+
if (tool.workflow.toolKind !== 'authenticate') await prepareTeachBrowserContext(cdpPool, poolKey);
|
|
1195
|
+
if (signal?.aborted) throw abortSignalError(signal);
|
|
1196
|
+
const credentialHash = credentialStateHash(credentials);
|
|
1197
|
+
let pooled = cdpPool?.get(poolKey);
|
|
1198
|
+
// A compiler may discover the live login before it has written a workflow.
|
|
1199
|
+
// Adopt that same run-owned browser for its first compiled action.
|
|
1200
|
+
if (!pooled && tool.workflow.toolKind === 'authenticate') {
|
|
1201
|
+
pooled = cdpPool?.get(AUTH_DISCOVERY_POOL_KEY);
|
|
1202
|
+
if (pooled) {
|
|
1203
|
+
cdpPool?.delete(AUTH_DISCOVERY_POOL_KEY);
|
|
1204
|
+
cdpPool?.set(poolKey, pooled);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
if (
|
|
1208
|
+
pooled &&
|
|
1209
|
+
tool.workflow.toolKind !== 'authenticate' &&
|
|
1210
|
+
cdpCredentialStates.has(pooled) &&
|
|
1211
|
+
cdpCredentialStates.get(pooled) !== credentialHash
|
|
1212
|
+
) {
|
|
1213
|
+
cdpPool?.delete(poolKey);
|
|
1214
|
+
await pooled.close();
|
|
1215
|
+
pooled = undefined;
|
|
1216
|
+
}
|
|
1107
1217
|
const ownsSession = !pooled;
|
|
1108
1218
|
|
|
1109
1219
|
let cf: CdpBrowserFetch;
|
|
@@ -1111,7 +1221,7 @@ async function runCdpReplay(
|
|
|
1111
1221
|
log('cdp-replay: reusing pooled Chrome session');
|
|
1112
1222
|
cf = pooled;
|
|
1113
1223
|
} else {
|
|
1114
|
-
|
|
1224
|
+
let seeds = credentialSeedCookies(credentials);
|
|
1115
1225
|
// Cached replay jars are recording-derived and may be stale, so authenticate
|
|
1116
1226
|
// workflows do not inherit them implicitly. Current credential-store cookies
|
|
1117
1227
|
// are different: they represent durable device/session state and remain
|
|
@@ -1123,13 +1233,26 @@ async function runCdpReplay(
|
|
|
1123
1233
|
if (cached && rec && rec.mtimeMs > cached.bootstrapEpoch) cached = null;
|
|
1124
1234
|
if (!cached && seedJarFromRecording(cacheDir, rec, bootstrapUrl))
|
|
1125
1235
|
cached = loadJar(cacheDir);
|
|
1126
|
-
|
|
1236
|
+
const emptyCredentials =
|
|
1237
|
+
!credentials.cookies.length &&
|
|
1238
|
+
!Object.keys(credentials.values).length &&
|
|
1239
|
+
!credentials.storage?.length;
|
|
1240
|
+
if (
|
|
1241
|
+
cached &&
|
|
1242
|
+
(cached.credentialStateHash === credentialHash ||
|
|
1243
|
+
(cached.credentialStateHash === undefined && emptyCredentials))
|
|
1244
|
+
) {
|
|
1245
|
+
// A retained full snapshot is authoritative, including its removals.
|
|
1246
|
+
// A credential refresh invalidates it rather than mixing sessions.
|
|
1247
|
+
seeds = cached.cookies;
|
|
1248
|
+
}
|
|
1127
1249
|
} catch {
|
|
1128
1250
|
// best-effort
|
|
1129
1251
|
}
|
|
1130
1252
|
}
|
|
1131
|
-
seeds.push(...credentialSeedCookies(credentials));
|
|
1132
1253
|
const seedCookies = uniqueSeedCookies(seeds);
|
|
1254
|
+
recordBrowserCookieState({ ...credentials, cookies: seedCookies ?? [] }, 'cdp-before-seed');
|
|
1255
|
+
if (signal?.aborted) throw abortSignalError(signal);
|
|
1133
1256
|
cf = (cdpBrowserFetchFactoryForTest ?? createCdpBrowserFetch)({
|
|
1134
1257
|
baseUrl,
|
|
1135
1258
|
bootstrapUrl,
|
|
@@ -1146,30 +1269,39 @@ async function runCdpReplay(
|
|
|
1146
1269
|
// declares it — never a blanket default. See AuthConfig.crossOriginCookieReinjection.
|
|
1147
1270
|
reinjectCrossOriginCookies: tool.workflow.authConfig?.crossOriginCookieReinjection ?? false,
|
|
1148
1271
|
});
|
|
1272
|
+
cdpCredentialStates.set(cf, credentialHash);
|
|
1149
1273
|
// Register immediately so caller cancellation can close a browser that is
|
|
1150
1274
|
// still launching or minting its first jar.
|
|
1151
1275
|
if (cdpPool) cdpPool.set(poolKey, cf);
|
|
1152
1276
|
}
|
|
1153
1277
|
|
|
1278
|
+
// Existing callers keep their normal timing. Managed auth owns this browser
|
|
1279
|
+
// and closes it when the recovery deadline or client cancellation fires.
|
|
1280
|
+
const authSignal = credentials.authCommitRevision === undefined ? undefined : signal;
|
|
1154
1281
|
try {
|
|
1155
|
-
const jar = await cf.mintJar();
|
|
1282
|
+
const jar = await withAbortSignal(() => cf.mintJar(), authSignal);
|
|
1156
1283
|
if (signal?.aborted) throw abortSignalError(signal);
|
|
1284
|
+
const observedCookies = jar.cookies.map((c) => ({
|
|
1285
|
+
name: c.name,
|
|
1286
|
+
value: c.value,
|
|
1287
|
+
domain: c.domain,
|
|
1288
|
+
path: c.path,
|
|
1289
|
+
expires: c.expires,
|
|
1290
|
+
httpOnly: c.httpOnly,
|
|
1291
|
+
secure: c.secure,
|
|
1292
|
+
sameSite: c.sameSite,
|
|
1293
|
+
hostOnly: c.hostOnly ?? !c.domain.startsWith('.'),
|
|
1294
|
+
}));
|
|
1295
|
+
recordBrowserCookieState(
|
|
1296
|
+
{ ...credentials, cookies: observedCookies },
|
|
1297
|
+
'cdp-after-bootstrap',
|
|
1298
|
+
jar.seedCookieDiagnostics,
|
|
1299
|
+
);
|
|
1300
|
+
const replayCookies = new RuntimeCookieJar(credentials.cookies);
|
|
1301
|
+
replayCookies.restoreBrowserSnapshot(observedCookies);
|
|
1157
1302
|
const bootstrappedCredentials: CredentialStore = {
|
|
1158
1303
|
...credentials,
|
|
1159
|
-
cookies:
|
|
1160
|
-
...credentials.cookies,
|
|
1161
|
-
...jar.cookies.map((c) => ({
|
|
1162
|
-
name: c.name,
|
|
1163
|
-
value: c.value,
|
|
1164
|
-
domain: c.domain,
|
|
1165
|
-
path: c.path,
|
|
1166
|
-
expires: c.expires,
|
|
1167
|
-
httpOnly: c.httpOnly,
|
|
1168
|
-
secure: c.secure,
|
|
1169
|
-
sameSite: c.sameSite,
|
|
1170
|
-
hostOnly: !c.domain.startsWith('.'),
|
|
1171
|
-
})),
|
|
1172
|
-
],
|
|
1304
|
+
cookies: replayCookies.toJSON(),
|
|
1173
1305
|
};
|
|
1174
1306
|
const captureResult = jarBootstrapCaptureState(
|
|
1175
1307
|
tool.workflow.bootstrap,
|
|
@@ -1182,30 +1314,117 @@ async function runCdpReplay(
|
|
|
1182
1314
|
return captureResult.result;
|
|
1183
1315
|
}
|
|
1184
1316
|
|
|
1317
|
+
const navigate = cf.navigate?.bind(cf);
|
|
1318
|
+
const snapshotCookies = cf.snapshotCookies?.bind(cf);
|
|
1319
|
+
const fetchImpl: typeof fetch = authSignal
|
|
1320
|
+
? Object.assign(
|
|
1321
|
+
(input: Parameters<typeof fetch>[0], init?: RequestInit) =>
|
|
1322
|
+
withAbortSignal(() => cf.fetchImpl(input, init), authSignal),
|
|
1323
|
+
{ preconnect: cf.fetchImpl.preconnect },
|
|
1324
|
+
)
|
|
1325
|
+
: cf.fetchImpl;
|
|
1326
|
+
let runtimeCookies: CredentialStore['cookies'] | undefined;
|
|
1185
1327
|
let result = await tool.toolFn(paramsWithDefaults, {
|
|
1186
1328
|
credentials: bootstrappedCredentials,
|
|
1187
1329
|
initialState: { ...callerState, ...captureResult.state },
|
|
1188
|
-
fetchImpl
|
|
1330
|
+
fetchImpl,
|
|
1189
1331
|
browser:
|
|
1190
|
-
|
|
1332
|
+
navigate && snapshotCookies
|
|
1191
1333
|
? {
|
|
1192
|
-
navigate:
|
|
1193
|
-
|
|
1334
|
+
navigate: (...args: Parameters<NonNullable<CdpBrowserFetch['navigate']>>) =>
|
|
1335
|
+
withAbortSignal(() => navigate(...args), authSignal),
|
|
1336
|
+
snapshotCookies: () => withAbortSignal(snapshotCookies, authSignal),
|
|
1337
|
+
initialCookies: observedCookies,
|
|
1194
1338
|
}
|
|
1195
1339
|
: undefined,
|
|
1196
1340
|
signal,
|
|
1341
|
+
onCookieState: (cookies: CredentialStore['cookies']) => {
|
|
1342
|
+
runtimeCookies = cookies;
|
|
1343
|
+
},
|
|
1197
1344
|
...(onResponse ? { onResponse } : {}),
|
|
1198
1345
|
...(onPreparedRequest ? { onPreparedRequest } : {}),
|
|
1199
1346
|
});
|
|
1200
1347
|
|
|
1201
1348
|
if (signal?.aborted) throw abortSignalError(signal);
|
|
1349
|
+
if (!result.ok && cf.snapshotExistingCookies) {
|
|
1350
|
+
try {
|
|
1351
|
+
const snapshotExistingCookies = cf.snapshotExistingCookies.bind(cf);
|
|
1352
|
+
const snapshotSignal = AbortSignal.timeout(3_000);
|
|
1353
|
+
const cookies = await withAbortSignal(
|
|
1354
|
+
snapshotExistingCookies,
|
|
1355
|
+
signal ? AbortSignal.any([signal, snapshotSignal]) : snapshotSignal,
|
|
1356
|
+
);
|
|
1357
|
+
if (cookies) {
|
|
1358
|
+
recordBrowserCookieState(
|
|
1359
|
+
{ ...credentials, cookies },
|
|
1360
|
+
'cdp-after-request',
|
|
1361
|
+
jar.seedCookieDiagnostics,
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1364
|
+
} catch {
|
|
1365
|
+
if (signal?.aborted) throw abortSignalError(signal);
|
|
1366
|
+
// A failed diagnostic must preserve the original consumer failure.
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
if (jarLikelyStale(result)) clearJar(cacheDir);
|
|
1202
1370
|
if (result.ok) {
|
|
1371
|
+
if (authSignal) return result;
|
|
1203
1372
|
if (cdpPool && ownsSession) cdpPool.set(poolKey, cf);
|
|
1204
1373
|
try {
|
|
1205
|
-
const postJar = await cf.mintJar();
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1374
|
+
const postJar = await withAbortSignal(() => cf.mintJar(), authSignal);
|
|
1375
|
+
recordBrowserCookieState(
|
|
1376
|
+
{ ...credentials, cookies: postJar.cookies },
|
|
1377
|
+
'cdp-after-request',
|
|
1378
|
+
postJar.seedCookieDiagnostics,
|
|
1379
|
+
);
|
|
1380
|
+
let postCredentialHash = credentialHash;
|
|
1381
|
+
// Only a host-loaded durable snapshot opts into writeback. Synthetic
|
|
1382
|
+
// overrides and auth actions retain their existing persistence contract.
|
|
1383
|
+
// A partial seed must never turn restoration failures into deletions.
|
|
1384
|
+
if (
|
|
1385
|
+
tool.workflow.toolKind !== 'authenticate' &&
|
|
1386
|
+
credentials.durableRevision &&
|
|
1387
|
+
!postJar.seedCookieDiagnostics?.rejected &&
|
|
1388
|
+
!postJar.seedCookieDiagnostics?.failed
|
|
1389
|
+
) {
|
|
1390
|
+
const finalCookies = new RuntimeCookieJar(runtimeCookies ?? credentials.cookies);
|
|
1391
|
+
finalCookies.restoreBrowserSnapshot(
|
|
1392
|
+
postJar.cookies.map((cookie) => ({
|
|
1393
|
+
...cookie,
|
|
1394
|
+
hostOnly: cookie.hostOnly ?? !cookie.domain.startsWith('.'),
|
|
1395
|
+
})),
|
|
1396
|
+
);
|
|
1397
|
+
// Insertion counters belong to this runtime jar. Do not rotate the
|
|
1398
|
+
// durable version merely because another process reconstructed it.
|
|
1399
|
+
const previousScopes = new Map(
|
|
1400
|
+
credentials.cookies.map((cookie) => [cookieScopeKey(cookie), cookie]),
|
|
1401
|
+
);
|
|
1402
|
+
const cookies = finalCookies.toJSON().map((entry) => {
|
|
1403
|
+
const { creationIndex: _creationIndex, ...cookie } = entry;
|
|
1404
|
+
const previous = previousScopes.get(cookieScopeKey(cookie));
|
|
1405
|
+
return previous?.creationIndex === undefined
|
|
1406
|
+
? cookie
|
|
1407
|
+
: { ...cookie, creationIndex: previous.creationIndex };
|
|
1408
|
+
});
|
|
1409
|
+
const update = await compareAndSwapSiteCookies(
|
|
1410
|
+
tool.site,
|
|
1411
|
+
credentials.durableRevision,
|
|
1412
|
+
cookies,
|
|
1413
|
+
{ signal },
|
|
1414
|
+
);
|
|
1415
|
+
if (update.status !== 'stale') {
|
|
1416
|
+
postCredentialHash = credentialStateHash({ ...credentials, cookies });
|
|
1417
|
+
cdpCredentialStates.set(cf, postCredentialHash);
|
|
1418
|
+
}
|
|
1419
|
+
log(`cdp-replay: durable browser cookies ${update.status}`);
|
|
1420
|
+
}
|
|
1421
|
+
saveJar(cacheDir, { ...postJar, credentialStateHash: postCredentialHash });
|
|
1422
|
+
} catch (err) {
|
|
1423
|
+
if (signal?.aborted) throw abortSignalError(signal);
|
|
1424
|
+
clearJar(cacheDir);
|
|
1425
|
+
log(
|
|
1426
|
+
`cdp-replay post-run cookie snapshot unavailable: ${err instanceof Error ? err.message : String(err)}`,
|
|
1427
|
+
);
|
|
1209
1428
|
} finally {
|
|
1210
1429
|
if (!cdpPool && ownsSession) await cf.close();
|
|
1211
1430
|
}
|
|
@@ -1213,11 +1432,16 @@ async function runCdpReplay(
|
|
|
1213
1432
|
// A paused auth action is healthy progress. Retain the browser so the next
|
|
1214
1433
|
// declared action sees the same cookies, page, and browser-owned state.
|
|
1215
1434
|
if (ownsSession) cdpPool.set(poolKey, cf);
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1435
|
+
if (credentials.authCommitRevision === undefined) {
|
|
1436
|
+
try {
|
|
1437
|
+
const postJar = await cf.mintJar();
|
|
1438
|
+
saveJar(cacheDir, { ...postJar, credentialStateHash: credentialHash });
|
|
1439
|
+
} catch (err) {
|
|
1440
|
+
clearJar(cacheDir);
|
|
1441
|
+
log(
|
|
1442
|
+
`cdp-replay paused cookie snapshot unavailable: ${err instanceof Error ? err.message : String(err)}`,
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1221
1445
|
}
|
|
1222
1446
|
// Deliberately do NOT close cf — the pool retains it for the completion phase.
|
|
1223
1447
|
} else if (cdpPool) {
|
|
@@ -1275,6 +1499,7 @@ async function runCdpReplay(
|
|
|
1275
1499
|
}
|
|
1276
1500
|
}
|
|
1277
1501
|
|
|
1502
|
+
if (signal?.aborted) throw abortSignalError(signal);
|
|
1278
1503
|
return result;
|
|
1279
1504
|
} catch (err) {
|
|
1280
1505
|
// Session is dead — evict from pool so the next call creates a fresh one.
|
|
@@ -1549,8 +1774,9 @@ async function ensureStealthFetch(
|
|
|
1549
1774
|
tool: ResolvedTool,
|
|
1550
1775
|
cache: Map<string, StealthFetch>,
|
|
1551
1776
|
params: Record<string, string | number | boolean>,
|
|
1777
|
+
signal?: AbortSignal,
|
|
1552
1778
|
): Promise<StealthFetch> {
|
|
1553
|
-
const credentials = (await loadCredentialStore(tool.site)) ?? {
|
|
1779
|
+
const credentials = (await loadCredentialStore(tool.site, signal)) ?? {
|
|
1554
1780
|
site: tool.site,
|
|
1555
1781
|
cookies: [],
|
|
1556
1782
|
values: {},
|
|
@@ -1910,6 +2136,7 @@ export function resolveWorkflowTool(
|
|
|
1910
2136
|
initialState?: Record<string, unknown>;
|
|
1911
2137
|
credentials?: CredentialStore;
|
|
1912
2138
|
signal?: AbortSignal;
|
|
2139
|
+
onCookieState?: (cookies: CredentialStore['cookies']) => void;
|
|
1913
2140
|
onResponse?: (observation: ResponseObservation) => void;
|
|
1914
2141
|
onPreparedRequest?: (observation: PreparedRequestObservation) => void;
|
|
1915
2142
|
onResponseEvidence?: (evidence: ResponseEvidence) => void;
|
|
@@ -1924,6 +2151,7 @@ export function resolveWorkflowTool(
|
|
|
1924
2151
|
browser: o?.browser,
|
|
1925
2152
|
initialState: o?.initialState,
|
|
1926
2153
|
signal: o?.signal,
|
|
2154
|
+
onCookieState: o?.onCookieState,
|
|
1927
2155
|
onResponse: o?.onResponse,
|
|
1928
2156
|
onPreparedRequest: o?.onPreparedRequest,
|
|
1929
2157
|
onResponseEvidence: o?.onResponseEvidence,
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
import { existsSync, readFileSync } from 'node:fs';
|
|
14
14
|
import { join as pathJoin } from 'node:path';
|
|
15
15
|
import { z } from 'zod';
|
|
16
|
+
import { AuthCaptureSchema, AuthToolPlanSchema } from './auth-plan.ts';
|
|
17
|
+
export { AuthToolPlanSchema, type AuthToolPlan } from './auth-plan.ts';
|
|
16
18
|
import { compactUrlForLlm } from './llm-url.ts';
|
|
17
19
|
import { localSiteDir } from './paths.ts';
|
|
18
20
|
import { detectPageMintedHeaders } from './redact.ts';
|
|
@@ -49,16 +51,6 @@ export const SharedModuleSpecSchema = z.object({
|
|
|
49
51
|
});
|
|
50
52
|
export type SharedModuleSpec = z.infer<typeof SharedModuleSpecSchema>;
|
|
51
53
|
|
|
52
|
-
const AuthCaptureSchema = z.object({
|
|
53
|
-
name: z.string().min(1),
|
|
54
|
-
/** Capture source: json | response_header | cookie | text_regex. */
|
|
55
|
-
source: z.string().min(1),
|
|
56
|
-
/** Path / header name / cookie name / regex that locates the value. */
|
|
57
|
-
locator: z.string().min(1),
|
|
58
|
-
/** Where the captured value is injected downstream, e.g. "header:Authorization". */
|
|
59
|
-
usedAs: z.string().default(''),
|
|
60
|
-
});
|
|
61
|
-
|
|
62
54
|
const AuthRecipeSchema = z
|
|
63
55
|
.object({
|
|
64
56
|
required: z.boolean().default(false),
|
|
@@ -194,18 +186,6 @@ const PerToolPlanSchema = z.object({
|
|
|
194
186
|
});
|
|
195
187
|
type PerToolPlan = z.infer<typeof PerToolPlanSchema>;
|
|
196
188
|
|
|
197
|
-
export const AuthToolPlanSchema = z
|
|
198
|
-
.object({
|
|
199
|
-
toolName: z.string(),
|
|
200
|
-
credentialRequestSeqs: z.array(z.number().int().nonnegative()).default([]),
|
|
201
|
-
authRequestSeqs: z.array(z.number().int().nonnegative()).default([]),
|
|
202
|
-
credentialNames: z.array(z.string()).default([]),
|
|
203
|
-
captures: z.array(AuthCaptureSchema).default([]),
|
|
204
|
-
notes: z.string().default(''),
|
|
205
|
-
})
|
|
206
|
-
.optional();
|
|
207
|
-
export type AuthToolPlan = z.infer<typeof AuthToolPlanSchema>;
|
|
208
|
-
|
|
209
189
|
export const BuildPlanSchema = z
|
|
210
190
|
.object({
|
|
211
191
|
sharedModules: z.array(SharedModuleSpecSchema).default([]),
|