imprint-mcp 0.6.0 → 0.6.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.
Files changed (43) hide show
  1. package/README.md +2 -0
  2. package/package.json +4 -2
  3. package/prompts/audit-agent.md +4 -2
  4. package/prompts/auth-compile-agent.md +149 -158
  5. package/prompts/build-planning.md +14 -9
  6. package/prompts/compile-agent.md +46 -23
  7. package/prompts/live-verifier-agent.md +101 -0
  8. package/prompts/playbook-compilation.md +8 -1
  9. package/prompts/tool-candidate-detection.md +9 -29
  10. package/src/cli.ts +83 -1
  11. package/src/imprint/audit.ts +219 -20
  12. package/src/imprint/auth-compile-agent.ts +215 -155
  13. package/src/imprint/auth-compile-tools.ts +300 -118
  14. package/src/imprint/auth-verifier.ts +167 -180
  15. package/src/imprint/backend-cache.ts +18 -11
  16. package/src/imprint/backend-ladder.ts +225 -177
  17. package/src/imprint/build-plan.ts +3 -8
  18. package/src/imprint/cdp-browser-fetch.ts +930 -83
  19. package/src/imprint/claude-cli-compile.ts +46 -20
  20. package/src/imprint/codex-cli-compile.ts +436 -139
  21. package/src/imprint/compile-agent-types.ts +15 -10
  22. package/src/imprint/compile-agent.ts +88 -28
  23. package/src/imprint/compile-tools.ts +228 -39
  24. package/src/imprint/compile-verification.ts +187 -0
  25. package/src/imprint/compile.ts +71 -2
  26. package/src/imprint/credential-store.ts +57 -44
  27. package/src/imprint/emit.ts +3 -1
  28. package/src/imprint/freeform-redact.ts +27 -6
  29. package/src/imprint/live-verifier.ts +1665 -0
  30. package/src/imprint/llm.ts +22 -3
  31. package/src/imprint/login.ts +107 -25
  32. package/src/imprint/mcp-compile-server.ts +447 -114
  33. package/src/imprint/mcp-live-verifier-server.ts +359 -0
  34. package/src/imprint/mcp-server.ts +116 -35
  35. package/src/imprint/probe-backends.ts +88 -7
  36. package/src/imprint/recording-request.ts +36 -0
  37. package/src/imprint/request-capture.ts +40 -17
  38. package/src/imprint/runtime.ts +490 -337
  39. package/src/imprint/teach-state.ts +36 -2
  40. package/src/imprint/teach.ts +302 -146
  41. package/src/imprint/tool-candidates.ts +8 -10
  42. package/src/imprint/tool-loader.ts +12 -4
  43. package/src/imprint/types.ts +137 -68
@@ -1,12 +1,10 @@
1
1
  /**
2
2
  * Agentic auth tool compilation — the agent examines the session, writes
3
- * workflow.json, tests it against the live site, iterates until login works.
3
+ * workflow.json, tests its declared actions live, and iterates until login works.
4
4
  *
5
5
  * Mirrors compile-agent.ts for data tools but with the auth-specific live-test
6
6
  * tool (run_verification) and lighter verification. Authentication runs on a
7
- * single rung — headed cdp-replay (a real visible browser that replays the
8
- * recorded requests in-page); see AUTH_RUNGS in auth-verifier.ts. There is no
9
- * bespoke login backend and no playbook rung in the auth path.
7
+ * persistent headed cdp-replay session so browser state survives checkpoints.
10
8
  *
11
9
  * Provider paths mirror compile-agent.ts exactly:
12
10
  * - claude-cli / codex-cli: shell out with the auth toolset registered as a
@@ -15,7 +13,7 @@
15
13
  * - anthropic-api (or any ToolUseProvider): drive in-process via runAgentLoop.
16
14
  */
17
15
 
18
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
16
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
19
17
  import { join as pathJoin } from 'node:path';
20
18
  import {
21
19
  type AgentProgress,
@@ -25,15 +23,18 @@ import {
25
23
  giveUpTool,
26
24
  runAgentLoop,
27
25
  } from './agent.ts';
28
- import { ensureAuthBootstrap } from './auth-bootstrap.ts';
29
26
  import {
30
27
  AUTH_COMPILE_TOOL_NAMES,
28
+ AUTH_VERIFICATION_ATTEMPT_SENTINEL,
31
29
  authExternalVerification,
30
+ authWorkflowHash,
31
+ authWorkflowPreflightFailures,
32
32
  buildAuthCompileTools,
33
33
  } from './auth-compile-tools.ts';
34
- import { type AuthPhaseResult, AuthVerifier } from './auth-verifier.ts';
34
+ import { type AuthActionResult, AuthVerifier } from './auth-verifier.ts';
35
35
  import type { AuthToolPlan } from './build-plan.ts';
36
36
  import { compileAuthViaClaudeCli } from './claude-cli-compile.ts';
37
+ import { compileViaCodexCli } from './codex-cli-compile.ts';
37
38
  import type {
38
39
  AuthCheckpoint,
39
40
  CompileAgentProgress,
@@ -47,7 +48,8 @@ import {
47
48
  } from './llm.ts';
48
49
  import { createLog } from './log.ts';
49
50
  import { localToolDir } from './paths.ts';
50
- import type { Session } from './types.ts';
51
+ import { loadCredentialStore } from './runtime.ts';
52
+ import { type Session, WorkflowSchema } from './types.ts';
51
53
 
52
54
  const log = createLog('auth-compile-agent');
53
55
 
@@ -66,20 +68,14 @@ interface CompileAuthAgentOptions {
66
68
  onProgress?: (p: CompileAgentProgress) => void;
67
69
  /** Interactive bridge for the agent's `prompt_user` checkpoint: shows the
68
70
  * agent-generated message (+ optional choices) in the teach TUI and returns
69
- * the user's input (the live OTP, a confirmation, etc.). When omitted
70
- * (--no-interactive), a placeholder is supplied so the run still ATTEMPTS the
71
- * completion unattended. */
71
+ * the user's input. When omitted, the agent receives an empty response and
72
+ * decides how to proceed. */
72
73
  onPrompt?: (message: string, options?: string[]) => Promise<string>;
73
74
  /** Cool-off bridge for the agent's `wait_for_cooldown` checkpoint: wait the
74
75
  * given minutes (informing the user, firing NO login). Default sleeps. */
75
76
  onCooldown?: (minutes: number, reason?: string) => Promise<void>;
76
77
  }
77
78
 
78
- /** Unattended OTP placeholder when no interactive prompt bridge is supplied. */
79
- const ATTEMPT_OTP_PLACEHOLDER = '000000';
80
- /** Runaway guard on the number of agent segments per auth run. */
81
- const MAX_AUTH_SEGMENTS = 16;
82
-
83
79
  /** Build the initial user message handed to the agent on its first turn.
84
80
  * Shared verbatim by every provider path so the agent's framing is identical. */
85
81
  function buildAuthInitialMessage(opts: {
@@ -94,15 +90,15 @@ function buildAuthInitialMessage(opts: {
94
90
  // Cookies persist automatically — only surface NON-cookie header contracts.
95
91
  return u.startsWith('header:') && u !== 'header:cookie' && u !== 'header:set-cookie';
96
92
  });
97
- const sessionCaptureNote =
93
+ const durableCaptureNote =
98
94
  headerCaptures.length > 0
99
- ? `\n- sessionCapture contracts (data tools consume these as \${credential.<name>}): ${headerCaptures
95
+ ? `\n- durable credential contracts (data tools consume these as \${credential.<name>}): ${headerCaptures
100
96
  .map(
101
97
  (c) => `${c.name} (used as ${c.usedAs}; seed source ${c.source}, locator ${c.locator})`,
102
98
  )
103
99
  .join(
104
100
  '; ',
105
- )}\n → For each, add an authConfig.sessionCapture that reads this token from the login COMPLETION response (verify the real source/locator against the recorded response; the seed is a hint). Verification fails without it.`
101
+ )}\n → Capture each value on the recording-grounded producing request and include its name in authConfig.persist. The seed source/locator is only a hint.`
106
102
  : '';
107
103
  return `A new auth compile task is starting.
108
104
 
@@ -111,13 +107,12 @@ Tool name: ${toolName}
111
107
  Tool directory: ${toolDir}
112
108
 
113
109
  Auth tool plan:
114
- - loginRequestSeqs: ${JSON.stringify(authToolPlan.loginRequestSeqs)}
115
- - twoFactorRequestSeqs: ${JSON.stringify(authToolPlan.twoFactorRequestSeqs)}
116
- - twoFactorType: ${authToolPlan.twoFactorType}
117
- - credentialNames: ${JSON.stringify(authToolPlan.credentialNames)}${sessionCaptureNote}
110
+ - credential-request hints: ${JSON.stringify(authToolPlan.credentialRequestSeqs)}
111
+ - related-auth-request hints: ${JSON.stringify(authToolPlan.authRequestSeqs)}
112
+ - credentialNames: ${JSON.stringify(authToolPlan.credentialNames)}${durableCaptureNote}
118
113
  - notes: ${authToolPlan.notes || '(none)'}
119
114
 
120
- Begin by calling read_session_summary to orient yourself, then examine the login requests and write workflow.json per the system prompt.`;
115
+ MANDATORY FIRST ACTION: call read_session_summary now. Do not write prose, do not inspect repository files, and do not plan silently before that tool call. After read_session_summary returns, examine the login requests and write workflow.json per the system prompt.`;
121
116
  }
122
117
 
123
118
  export async function compileAuthAgent(opts: CompileAuthAgentOptions): Promise<CompileAgentResult> {
@@ -144,6 +139,7 @@ export async function compileAuthAgent(opts: CompileAuthAgentOptions): Promise<C
144
139
  const resolved = resolveProvider(opts.llmConfig);
145
140
  if (resolved.name === 'claude-cli') {
146
141
  return await runAuthSegmentLoop({
142
+ driver: 'claude-cli',
147
143
  site,
148
144
  session,
149
145
  sessionPath: opts.sessionPath,
@@ -153,6 +149,7 @@ export async function compileAuthAgent(opts: CompileAuthAgentOptions): Promise<C
153
149
  toolDir,
154
150
  authToolPlan,
155
151
  teachCredentials: opts.teachCredentials,
152
+ model: opts.llmConfig?.model,
156
153
  initialPrompt: initialUserMessage,
157
154
  onProgress: opts.onProgress,
158
155
  onPrompt: opts.onPrompt,
@@ -160,20 +157,23 @@ export async function compileAuthAgent(opts: CompileAuthAgentOptions): Promise<C
160
157
  });
161
158
  }
162
159
  if (resolved.name === 'codex-cli') {
163
- // Auth verification is checkpoint-based: the agent calls run_verification and
164
- // STOPS, and the orchestrator must resume the SAME session past that
165
- // checkpoint with the live result. codex-cli runs ephemerally (no resume) and
166
- // its driver only recognizes the done/give_up sentinels — not the checkpoint
167
- // sentinel — so a run_verification checkpoint exits 0, is misread as "stopped
168
- // early", and every codex auth compile fails with a misleading message.
169
- // Reject it up front rather than fail late. (Data-tool compiles, which never
170
- // checkpoint, still run on codex-cli.)
171
- throw new Error(
172
- [
173
- 'provider "codex-cli" cannot compile an authenticate tool: live 2FA verification is checkpoint-based and codex-cli runs ephemerally — it cannot resume past a run_verification checkpoint.',
174
- '→ use one of: claude-cli, anthropic-api (set ANTHROPIC_API_KEY)',
175
- ].join('\n'),
176
- );
160
+ return await runAuthSegmentLoop({
161
+ driver: 'codex-cli',
162
+ site,
163
+ session,
164
+ sessionPath: opts.sessionPath,
165
+ systemPromptPath,
166
+ deadlineMs,
167
+ startTime,
168
+ toolDir,
169
+ authToolPlan,
170
+ teachCredentials: opts.teachCredentials,
171
+ model: opts.llmConfig?.model,
172
+ initialPrompt: initialUserMessage,
173
+ onProgress: opts.onProgress,
174
+ onPrompt: opts.onPrompt,
175
+ onCooldown: opts.onCooldown,
176
+ });
177
177
  }
178
178
  if (!isToolUseProvider(resolved)) {
179
179
  throw new Error(
@@ -203,11 +203,10 @@ export async function compileAuthAgent(opts: CompileAuthAgentOptions): Promise<C
203
203
  let message = '';
204
204
  let conversationLog: AgentResult['conversationLog'] = [];
205
205
 
206
- const MAX_VERIFICATION_CYCLES = 3;
207
206
  let verificationCycle = 0;
208
207
  let currentInitialMessage = initialUserMessage;
209
208
 
210
- while (verificationCycle < MAX_VERIFICATION_CYCLES) {
209
+ while (true) {
211
210
  verificationCycle++;
212
211
 
213
212
  const userOnProgress = opts.onProgress;
@@ -216,7 +215,6 @@ export async function compileAuthAgent(opts: CompileAuthAgentOptions): Promise<C
216
215
  userOnProgress({
217
216
  ...p,
218
217
  verificationCycle,
219
- maxVerificationCycles: MAX_VERIFICATION_CYCLES,
220
218
  })
221
219
  : undefined;
222
220
 
@@ -249,6 +247,10 @@ export async function compileAuthAgent(opts: CompileAuthAgentOptions): Promise<C
249
247
  const failures = authExternalVerification(
250
248
  toolDir,
251
249
  (authToolPlan.captures ?? []).map((c) => ({ name: c.name, usedAs: c.usedAs })),
250
+ {
251
+ requireLiveAttempt: true,
252
+ requiredCredentialNames: authToolPlan.credentialNames,
253
+ },
252
254
  );
253
255
 
254
256
  if (failures.length === 0) {
@@ -256,12 +258,6 @@ export async function compileAuthAgent(opts: CompileAuthAgentOptions): Promise<C
256
258
  break;
257
259
  }
258
260
 
259
- if (verificationCycle >= MAX_VERIFICATION_CYCLES) {
260
- outcome = 'error';
261
- message = `Auth verification failed after ${MAX_VERIFICATION_CYCLES} cycles. Failures:\n${failures.join('\n')}`;
262
- break;
263
- }
264
-
265
261
  log(`auth verification failed (cycle ${verificationCycle}), resuming agent loop...`);
266
262
  currentInitialMessage = `You called done but verification failed:
267
263
 
@@ -272,9 +268,6 @@ Fix the issues in workflow.json, re-test with run_verification, and call done ag
272
268
 
273
269
  writeFileSync(conversationLogPath, JSON.stringify(conversationLog, null, 2), 'utf8');
274
270
 
275
- // Same credential-entry bootstrap safety net as the segmented path.
276
- injectAuthBootstrapArtifact(toolDir, session, authToolPlan);
277
-
278
271
  const workflowPath = pathJoin(toolDir, 'workflow.json');
279
272
 
280
273
  return {
@@ -293,6 +286,7 @@ Fix the issues in workflow.json, re-test with run_verification, and call done ag
293
286
  }
294
287
 
295
288
  interface AuthSegmentLoopOptions {
289
+ driver: 'claude-cli' | 'codex-cli';
296
290
  site: string;
297
291
  session: Session;
298
292
  sessionPath: string;
@@ -302,6 +296,7 @@ interface AuthSegmentLoopOptions {
302
296
  toolDir: string;
303
297
  authToolPlan: NonNullable<AuthToolPlan>;
304
298
  teachCredentials: { site: string; values: Record<string, string> };
299
+ model?: string;
305
300
  initialPrompt: string;
306
301
  onProgress?: (p: CompileAgentProgress) => void;
307
302
  onPrompt?: (message: string, options?: string[]) => Promise<string>;
@@ -315,7 +310,7 @@ function sleep(ms: number): Promise<void> {
315
310
  /** The full set of live-execution facts every verification result carries, so the
316
311
  * agent never has to re-run (or reverse-engineer the runtime) to see what
317
312
  * happened: rung, timing, HTTP status, error code, and the response body. */
318
- function verifyFacts(r: AuthPhaseResult): string {
313
+ function verifyFacts(r: AuthActionResult): string {
319
314
  const parts = [`backend=${r.usedBackend}`, `duration=${r.durationMs}ms`];
320
315
  if (typeof r.status === 'number') parts.push(`httpStatus=${r.status}`);
321
316
  if (r.error) parts.push(`error=${r.error}`);
@@ -324,40 +319,42 @@ function verifyFacts(r: AuthPhaseResult): string {
324
319
  return s;
325
320
  }
326
321
 
327
- /** Render an AuthVerifier phase result into the message the agent receives on
322
+ /** Render an AuthVerifier action result into the message the agent receives on
328
323
  * resume — channel-agnostic, grounded only in the result. Always includes the
329
324
  * full execution facts (status, timing, backend, body) so the agent can decide
330
325
  * its next move without inspecting the runtime. */
331
- function formatVerifyResult(phase: string, r: AuthPhaseResult): string {
326
+ function formatVerifyResult(action: string, r: AuthActionResult): string {
332
327
  const facts = verifyFacts(r);
333
328
  if (r.ok) {
334
- return `Verification phase "${phase}" SUCCEEDED. ${facts}\nThe login completed and the session token is now stored for data tools. Call done with a one-line summary.`;
335
- }
336
- if (r.error === 'AWAITING_2FA') {
337
- const ctxKeys = r.twoFactorContext ? Object.keys(r.twoFactorContext) : [];
338
- return `Verification phase "${phase}" reached the 2FA challenge (AWAITING_2FA, type=${r.twoFactorType ?? 'unknown'}). ${facts}\nThe OTP/push has been delivered to the user${ctxKeys.length ? ` (carried token keys: ${ctxKeys.join(', ')})` : ''}. Now call prompt_user to ask them for the live second factor, then run_verification for the completion phase.`;
339
- }
340
- if (r.error === 'BUDGET_EXHAUSTED') {
341
- return `Verification refused: ${r.message} ${facts}\nDo not request another initiate — give_up if you cannot complete.`;
329
+ return `Verification action "${action}" SUCCEEDED. ${facts}\nThe action's declared success outcome was reached. Call done with a one-line summary.`;
342
330
  }
343
- if (r.error === 'ATTEMPT_BUDGET_EXHAUSTED') {
344
- return `Verification refused: ${r.message} ${facts}\nEvery initiate failed before delivering a 2FA challenge, so cool-off will not help. Stop requesting initiates and give_up — leave the corrected artifacts for a fresh run.`;
331
+ if (r.error === 'ACTION_REQUIRED') {
332
+ return `Verification action "${action}" reached its declared pause outcome. ${facts}\nnextAction=${r.nextAction ?? '(missing)'}\n${r.message ?? ''}\nUse the recording and this result to decide whether to prompt the user or run the next action directly.`;
345
333
  }
346
- return `Verification phase "${phase}" FAILED. ${facts}\n${r.message ?? ''}\nThis attempt did NOT consume your 2FA-challenge budget (no challenge was delivered). If it looks like the site rate-flagging repeated logins, call wait_for_cooldown. If it's a defect in workflow.json (e.g. a bot-block 403 because the login page sensor never ran — make sure a top-level bootstrap points at the credential-entry page), fix it and run_verification again.`;
334
+ return `Verification action "${action}" FAILED. ${facts}\n${r.message ?? ''}\nUse the recording and observed response to revise the action program or choose the next checkpoint.`;
347
335
  }
348
336
 
349
337
  /**
350
- * Drive the claude-cli auth compile as a sequence of resumable SEGMENTS. The
338
+ * Drive a CLI auth compile as a sequence of checkpointed SEGMENTS. The
351
339
  * agent shapes from the recording, then pauses at checkpoint tools; this loop
352
- * (the durable orchestrator) executes each checkpoint — run_verification on the
353
- * persistent AuthVerifier session, prompt_user via the TUI bridge, or a
354
- * cool-off wait — and resumes the same claude session with the result. The ONE
355
- * stateful thing (the live browser) lives in the AuthVerifier and is drained at
356
- * the end. Conversation state is carried by `--resume`, not retained here.
340
+ * (the durable orchestrator) executes each checkpoint — run_verification or
341
+ * page inspection on the persistent AuthVerifier session, prompt_user via the
342
+ * TUI bridge, or a cool-off wait — then feeds the result into the next segment.
343
+ * The ONE stateful thing (the live browser) lives in the AuthVerifier and is
344
+ * drained at the end. CLI conversation state is carried by provider-native resume
345
+ * (`claude --resume`, `codex exec resume`), not retained here.
357
346
  */
358
347
  async function runAuthSegmentLoop(opts: AuthSegmentLoopOptions): Promise<CompileAgentResult> {
359
348
  const workflowPath = pathJoin(opts.toolDir, 'workflow.json');
360
- const credsForRun = { site: opts.site, cookies: [], values: opts.teachCredentials.values };
349
+ const verificationAttemptPath = pathJoin(opts.toolDir, AUTH_VERIFICATION_ATTEMPT_SENTINEL);
350
+ if (existsSync(verificationAttemptPath)) unlinkSync(verificationAttemptPath);
351
+ const storedCredentials = await loadCredentialStore(opts.site);
352
+ const credsForRun = {
353
+ site: opts.site,
354
+ cookies: storedCredentials?.cookies ?? [],
355
+ values: { ...(storedCredentials?.values ?? {}), ...opts.teachCredentials.values },
356
+ storage: storedCredentials?.storage ?? [],
357
+ };
361
358
  const verifier = new AuthVerifier(workflowPath, credsForRun);
362
359
  const authMode = {
363
360
  site: opts.site,
@@ -366,20 +363,24 @@ async function runAuthSegmentLoop(opts: AuthSegmentLoopOptions): Promise<Compile
366
363
  initialPrompt: opts.initialPrompt,
367
364
  };
368
365
 
369
- const onPrompt = opts.onPrompt ?? (async () => ATTEMPT_OTP_PLACEHOLDER); // unattended: placeholder
370
- const onCooldown =
371
- opts.onCooldown ?? (async (minutes: number) => sleep(Math.min(minutes, 10) * 60_000));
366
+ const onPrompt = opts.onPrompt ?? (async () => '');
367
+ const onCooldown = opts.onCooldown ?? (async (minutes: number) => sleep(minutes * 60_000));
372
368
 
373
369
  let resume: { sessionId: string; message: string } | undefined;
374
370
  let last: CompileAgentResult | undefined;
375
371
  let totalTurns = 0;
372
+ let activeDeadlineMs = opts.deadlineMs;
376
373
  // The most recent live verification, surfaced on the orchestrator's progress
377
374
  // line so a failure (e.g. a 403) is visible the instant it happens.
378
375
  let lastVerification: CompileAgentProgress['lastVerification'];
376
+ let segment = 0;
377
+ const promptSecrets = new Map<string, string>();
378
+ let promptSecretSequence = 0;
379
379
 
380
380
  try {
381
- for (let seg = 0; seg < MAX_AUTH_SEGMENTS; seg++) {
382
- // Each resumed segment restarts claude-cli's per-segment `turn` at 0; add
381
+ while (true) {
382
+ segment++;
383
+ // Each resumed/restarted segment has its own per-segment `turn` at 0; add
383
384
  // the prior segments' turns so the displayed count is monotonic (no reset).
384
385
  const offset = totalTurns; // turns from prior segments only (read BEFORE the += below)
385
386
  const wrappedOnProgress = opts.onProgress
@@ -387,85 +388,176 @@ async function runAuthSegmentLoop(opts: AuthSegmentLoopOptions): Promise<Compile
387
388
  opts.onProgress?.({
388
389
  ...p,
389
390
  turn: offset + p.turn,
390
- segment: seg + 1,
391
- maxSegments: MAX_AUTH_SEGMENTS,
392
- attempt: verifier.attemptsUsed,
393
- maxAttempts: verifier.maxInitiateAttempts,
391
+ segment,
394
392
  lastVerification,
395
393
  })
396
394
  : undefined;
397
395
 
398
- const result = await compileAuthViaClaudeCli({
399
- session: opts.session,
400
- absoluteToolDir: opts.toolDir,
401
- sessionPath: opts.sessionPath,
402
- systemPromptPath: opts.systemPromptPath,
403
- deadlineMs: opts.deadlineMs,
404
- startTime: opts.startTime,
405
- onProgress: wrappedOnProgress,
406
- authMode,
407
- resume,
408
- });
396
+ const result =
397
+ opts.driver === 'claude-cli'
398
+ ? await compileAuthViaClaudeCli({
399
+ session: opts.session,
400
+ absoluteToolDir: opts.toolDir,
401
+ sessionPath: opts.sessionPath,
402
+ systemPromptPath: opts.systemPromptPath,
403
+ deadlineMs: activeDeadlineMs,
404
+ startTime: opts.startTime,
405
+ onProgress: wrappedOnProgress,
406
+ authMode,
407
+ resume,
408
+ model: opts.model,
409
+ })
410
+ : await compileViaCodexCli({
411
+ session: opts.session,
412
+ absoluteToolDir: opts.toolDir,
413
+ sessionPath: opts.sessionPath,
414
+ systemPromptPath: opts.systemPromptPath,
415
+ deadlineMs: activeDeadlineMs,
416
+ startTime: opts.startTime,
417
+ onProgress: wrappedOnProgress,
418
+ authMode,
419
+ resume,
420
+ model: opts.model,
421
+ });
409
422
  last = result;
410
423
  totalTurns += result.turns;
411
424
 
412
- if (result.outcome !== 'checkpoint' || !result.checkpoint) break; // terminal
425
+ if (result.outcome !== 'checkpoint' || !result.checkpoint) {
426
+ if (result.outcome === 'soft_cap' && result.sessionId && Date.now() < activeDeadlineMs) {
427
+ const failures = authWorkflowPreflightFailures(
428
+ opts.toolDir,
429
+ opts.session,
430
+ opts.authToolPlan.credentialNames,
431
+ );
432
+ resume = {
433
+ sessionId: result.sessionId,
434
+ message: `You stopped without calling done, give_up, or recording a checkpoint. Continue the auth compile now.${
435
+ failures.length > 0
436
+ ? ` Current workflow preflight failures:\n${failures.map((failure) => `- ${failure}`).join('\n')}`
437
+ : ''
438
+ }`,
439
+ };
440
+ continue;
441
+ }
442
+ break;
443
+ }
413
444
  if (!result.sessionId) {
414
445
  last = {
415
446
  ...result,
416
447
  outcome: 'error',
417
448
  success: false,
418
- message: 'checkpoint reached but no session id was captured — cannot resume the agent.',
449
+ message: `checkpoint reached but no ${opts.driver} session id was captured — cannot resume the agent.`,
419
450
  };
420
451
  break;
421
452
  }
422
453
 
423
454
  const cp: AuthCheckpoint = result.checkpoint;
424
455
  let resultMsg: string;
456
+ const checkpointStartedAt = Date.now();
425
457
  try {
426
458
  if (cp.kind === 'run_verification') {
427
- const r = await verifier.runPhase(cp.phase, { otp_code: cp.otp_code });
428
- // Record + immediately surface the result so the spinner reflects a
429
- // failure the moment it happens — not only on the next agent turn.
430
- lastVerification = {
431
- phase: cp.phase,
432
- ok: r.ok,
433
- status: r.status,
434
- error: r.error,
435
- backend: r.usedBackend,
436
- durationMs: r.durationMs,
437
- checkpoint: 'run_verification',
438
- };
439
- opts.onProgress?.({
440
- turn: totalTurns,
441
- phase: 'tool',
442
- elapsedMs: Date.now() - opts.startTime,
443
- budgetMs: Math.max(0, opts.deadlineMs - Date.now()),
444
- inputTokens: 0,
445
- outputTokens: 0,
446
- verificationCycle: 1,
447
- maxVerificationCycles: 1,
448
- segment: seg + 1,
449
- maxSegments: MAX_AUTH_SEGMENTS,
450
- attempt: verifier.attemptsUsed,
451
- maxAttempts: verifier.maxInitiateAttempts,
452
- lastVerification,
459
+ const preflightFailures = authWorkflowPreflightFailures(
460
+ opts.toolDir,
461
+ opts.session,
462
+ opts.authToolPlan.credentialNames,
463
+ );
464
+ if (Date.now() >= activeDeadlineMs) {
465
+ resultMsg =
466
+ 'run_verification was blocked because the auth compile deadline expired; no live login was fired.';
467
+ } else if (preflightFailures.length > 0) {
468
+ resultMsg = `run_verification was blocked before any live login was fired because workflow.json failed auth preflight:\n${preflightFailures
469
+ .map((failure) => `- ${failure}`)
470
+ .join('\n')}`;
471
+ } else {
472
+ const verifiedWorkflowHash = authWorkflowHash(
473
+ WorkflowSchema.parse(
474
+ JSON.parse(readFileSync(pathJoin(opts.toolDir, 'workflow.json'), 'utf8')),
475
+ ),
476
+ );
477
+ const resolvedParameters = Object.fromEntries(
478
+ Object.entries(cp.parameters ?? {}).map(([name, value]) => [
479
+ name,
480
+ typeof value === 'string' && promptSecrets.has(value)
481
+ ? (promptSecrets.get(value) ?? '')
482
+ : value,
483
+ ]),
484
+ );
485
+ const r = await verifier.runAction(cp.action, resolvedParameters, {
486
+ freshSession: cp.freshSession,
487
+ cleanSession: cp.cleanSession,
488
+ });
489
+ writeFileSync(
490
+ verificationAttemptPath,
491
+ JSON.stringify(
492
+ {
493
+ action: cp.action,
494
+ ok: r.ok,
495
+ status: r.status,
496
+ error: r.error,
497
+ backend: r.usedBackend,
498
+ workflowHash: verifiedWorkflowHash,
499
+ timestamp: Date.now(),
500
+ },
501
+ null,
502
+ 2,
503
+ ),
504
+ 'utf8',
505
+ );
506
+ // Record + immediately surface the result so the spinner reflects a
507
+ // failure the moment it happens — not only on the next agent turn.
508
+ lastVerification = {
509
+ action: cp.action,
510
+ ok: r.ok,
511
+ status: r.status,
512
+ error: r.error,
513
+ backend: r.usedBackend,
514
+ durationMs: r.durationMs,
515
+ checkpoint: 'run_verification',
516
+ };
517
+ opts.onProgress?.({
518
+ turn: totalTurns,
519
+ phase: 'tool',
520
+ elapsedMs: Date.now() - opts.startTime,
521
+ budgetMs: Math.max(0, activeDeadlineMs - Date.now()),
522
+ inputTokens: 0,
523
+ outputTokens: 0,
524
+ verificationCycle: 1,
525
+ segment,
526
+ lastVerification,
527
+ });
528
+ resultMsg = formatVerifyResult(cp.action, r);
529
+ }
530
+ } else if (cp.kind === 'inspect_verification_page') {
531
+ const inspected = await verifier.inspectPage({
532
+ maxChars: cp.maxChars,
533
+ includeCookies: cp.includeCookies,
453
534
  });
454
- resultMsg = formatVerifyResult(cp.phase, r);
535
+ resultMsg = inspected.ok
536
+ ? `Current verification page snapshot:\n${JSON.stringify(inspected, null, 2)}`
537
+ : `Verification page inspection was unavailable: ${inspected.message ?? 'unknown reason'}`;
455
538
  } else if (cp.kind === 'prompt_user') {
456
539
  const answer = await onPrompt(cp.message, cp.options);
457
- resultMsg = `The user responded: ${answer || '(no input provided)'}`;
540
+ const reference = `\${prompt.${++promptSecretSequence}}`;
541
+ promptSecrets.set(reference, answer);
542
+ resultMsg = answer
543
+ ? `The user response is stored locally as ${reference}. Pass that exact reference as the appropriate run_verification parameter value; the orchestrator resolves it without exposing the answer to the model.`
544
+ : 'The user provided no input.';
458
545
  } else {
459
546
  await onCooldown(cp.minutes, cp.reason);
460
- resultMsg = `Cool-off of ~${cp.minutes} min complete (no login was fired during the wait). You may run_verification once more.`;
547
+ resultMsg = `Requested delay of ~${cp.minutes} min completed; no live action ran during the wait.`;
461
548
  }
462
549
  } catch (err) {
463
550
  resultMsg = `The orchestrator could not perform ${cp.kind}: ${err instanceof Error ? err.message : String(err)}`;
551
+ } finally {
552
+ if (cp.kind === 'prompt_user' || cp.kind === 'wait_for_cooldown') {
553
+ activeDeadlineMs += Date.now() - checkpointStartedAt;
554
+ }
464
555
  }
465
556
 
557
+ const resumeMessage = `[orchestrator result for your ${cp.kind} request]\n${resultMsg}\n\nContinue from the recording and observed result. You control the next action or checkpoint.`;
466
558
  resume = {
467
559
  sessionId: result.sessionId,
468
- message: `[orchestrator result for your ${cp.kind} request]\n${resultMsg}\n\nProceed: shape any next phase from the recording if needed, then call the appropriate next tool (run_verification / prompt_user / wait_for_cooldown / done / give_up).`,
560
+ message: resumeMessage,
469
561
  };
470
562
  }
471
563
  } finally {
@@ -486,42 +578,10 @@ async function runAuthSegmentLoop(opts: AuthSegmentLoopOptions): Promise<Compile
486
578
  cacheCreationInputTokens: 0,
487
579
  };
488
580
  }
489
- // Safety net: make sure the auth workflow navigates the credential-entry page
490
- // before the login POST (so cdp-replay validates the login page's anti-bot
491
- // token). The agent is told to set this; fill it in deterministically if it
492
- // didn't, so a forgetful LLM never costs a wasted live-login attempt.
493
- injectAuthBootstrapArtifact(opts.toolDir, opts.session, opts.authToolPlan);
494
-
495
581
  // Surface the cumulative turn count across segments.
496
582
  return { ...last, turns: totalTurns };
497
583
  }
498
584
 
499
- /** Read the compiled auth workflow.json and inject a derived credential-entry
500
- * `bootstrap` if it lacks one. Best-effort: never fails the compile. */
501
- function injectAuthBootstrapArtifact(
502
- toolDir: string,
503
- session: Session,
504
- plan: NonNullable<AuthToolPlan>,
505
- ): void {
506
- const workflowPath = pathJoin(toolDir, 'workflow.json');
507
- try {
508
- if (!existsSync(workflowPath)) return;
509
- const wf = JSON.parse(readFileSync(workflowPath, 'utf8'));
510
- const { changed, url } = ensureAuthBootstrap(
511
- wf,
512
- session,
513
- plan.loginRequestSeqs,
514
- plan.credentialNames,
515
- );
516
- if (changed) {
517
- writeFileSync(workflowPath, JSON.stringify(wf, null, 2), 'utf8');
518
- log(`injected credential-entry bootstrap into workflow.json: ${url}`);
519
- }
520
- } catch {
521
- // best-effort — a bootstrap convenience must never break the compile
522
- }
523
- }
524
-
525
585
  function buildMessageFromOutcome(result: AgentResult): string {
526
586
  switch (result.outcome) {
527
587
  case 'give_up':