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.
Files changed (58) hide show
  1. package/README.md +66 -2
  2. package/package.json +1 -1
  3. package/prompts/auth-compile-agent.md +173 -22
  4. package/prompts/compile-agent.md +45 -8
  5. package/prompts/component-recovery.md +40 -0
  6. package/prompts/master-teach-decision.md +28 -3
  7. package/prompts/master-teach-focused-planner.md +18 -5
  8. package/src/cli.ts +26 -18
  9. package/src/imprint/agent-tool.ts +15 -0
  10. package/src/imprint/agent.ts +2 -11
  11. package/src/imprint/audit.ts +35 -13
  12. package/src/imprint/auth-browser-discovery.ts +276 -0
  13. package/src/imprint/auth-compile-agent.ts +513 -224
  14. package/src/imprint/auth-compile-tools.ts +213 -25
  15. package/src/imprint/auth-discovery-evidence.ts +81 -0
  16. package/src/imprint/auth-plan.ts +28 -0
  17. package/src/imprint/auth-verifier.ts +234 -20
  18. package/src/imprint/authentication-interaction.ts +137 -0
  19. package/src/imprint/backend-ladder.ts +271 -43
  20. package/src/imprint/build-plan.ts +2 -22
  21. package/src/imprint/cdp-browser-fetch.ts +242 -60
  22. package/src/imprint/chromium.ts +93 -29
  23. package/src/imprint/claude-cli-compile.ts +2 -0
  24. package/src/imprint/codex-cli-compile.ts +99 -46
  25. package/src/imprint/compile-agent-types.ts +9 -5
  26. package/src/imprint/compile-agent.ts +33 -0
  27. package/src/imprint/compile-tool-executor.ts +783 -0
  28. package/src/imprint/compile-tools.ts +42 -9
  29. package/src/imprint/compile-verification.ts +2 -0
  30. package/src/imprint/compile.ts +3 -0
  31. package/src/imprint/compiler-log.ts +16 -0
  32. package/src/imprint/component-recovery.ts +120 -0
  33. package/src/imprint/cookie-jar.ts +117 -7
  34. package/src/imprint/credential-extract.ts +9 -2
  35. package/src/imprint/credential-lock.ts +147 -0
  36. package/src/imprint/credential-observation.ts +261 -0
  37. package/src/imprint/credential-store.ts +284 -44
  38. package/src/imprint/live-component-recovery.ts +73 -0
  39. package/src/imprint/llm.ts +4 -3
  40. package/src/imprint/log.ts +16 -1
  41. package/src/imprint/login.ts +9 -0
  42. package/src/imprint/master-teach-agent-contracts.ts +40 -3
  43. package/src/imprint/master-teach-agents.ts +229 -14
  44. package/src/imprint/master-teach-checks.ts +8 -1
  45. package/src/imprint/master-teach-controller.ts +681 -138
  46. package/src/imprint/master-teach-prompt-projections.ts +2 -0
  47. package/src/imprint/mcp-cdp-sessions.ts +7 -2
  48. package/src/imprint/mcp-compile-server.ts +62 -850
  49. package/src/imprint/mcp-server.ts +667 -91
  50. package/src/imprint/mcp-session-recovery.ts +167 -0
  51. package/src/imprint/native-teach-agents.ts +232 -108
  52. package/src/imprint/runtime.ts +416 -47
  53. package/src/imprint/stealth-chromium.ts +67 -7
  54. package/src/imprint/teach-browser-resources.ts +88 -0
  55. package/src/imprint/teach-stage-progress.ts +38 -0
  56. package/src/imprint/terminal-user-prompt.ts +42 -0
  57. package/src/imprint/types.ts +51 -18
  58. package/src/imprint/user-interaction.ts +836 -0
@@ -13,17 +13,21 @@
13
13
  * - anthropic-api (or any ToolUseProvider): drive in-process via runAgentLoop.
14
14
  */
15
15
 
16
+ import { randomUUID } from 'node:crypto';
16
17
  import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
17
18
  import { join as pathJoin } from 'node:path';
18
19
  import {
19
20
  type AgentProgress,
20
21
  type AgentResult,
21
22
  type AgentTool,
23
+ type ConversationLogEntry,
22
24
  doneTool,
23
25
  giveUpTool,
24
26
  runAgentLoop,
25
27
  } from './agent.ts';
28
+ import { AuthBrowserActionSchema } from './auth-browser-discovery.ts';
26
29
  import {
30
+ AUTH_CHECKPOINT_TOOLS,
27
31
  AUTH_COMPILE_TOOL_NAMES,
28
32
  AUTH_VERIFICATION_ATTEMPT_SENTINEL,
29
33
  authExternalVerification,
@@ -31,18 +35,22 @@ import {
31
35
  authWorkflowHash,
32
36
  authWorkflowPreflightFailures,
33
37
  buildAuthCompileTools,
38
+ parseAuthCheckpoint,
34
39
  } from './auth-compile-tools.ts';
40
+ import { AuthDiscoveryEvidence } from './auth-discovery-evidence.ts';
35
41
  import { type AuthActionResult, AuthVerifier } from './auth-verifier.ts';
36
42
  import type { AuthToolPlan } from './build-plan.ts';
37
43
  import { compileAuthViaClaudeCli } from './claude-cli-compile.ts';
38
44
  import { compileViaCodexCli } from './codex-cli-compile.ts';
39
45
  import type {
40
46
  AuthCheckpoint,
47
+ AuthCheckpointResult,
41
48
  CompileAgentProgress,
42
49
  CompileAgentResult,
43
50
  } from './compile-agent-types.ts';
44
51
  import { runCompileWithProviderRecovery } from './compile-provider-recovery.ts';
45
52
  import { abortSignalError, abortableDelay, withAbortSignal } from './concurrency.ts';
53
+ import { isCredentialReference } from './credential-extract.ts';
46
54
  import {
47
55
  type LLMOptions,
48
56
  type ToolUseProvider,
@@ -58,15 +66,278 @@ import {
58
66
  providerControlError,
59
67
  resolvedRunDeadline,
60
68
  } from './provider-retry.ts';
61
- import { loadCredentialStore } from './runtime.ts';
69
+ import { type CredentialStore, loadCredentialStore } from './runtime.ts';
62
70
  import { type Session, WorkflowSchema } from './types.ts';
71
+ import { requestAgentInteraction, resolveUserInput } from './user-interaction.ts';
63
72
 
64
73
  const log = createLog('auth-compile-agent');
65
74
 
66
75
  const REPO_ROOT = pathJoin(import.meta.dir, '..', '..');
67
76
  const PROMPTS_DIR = pathJoin(REPO_ROOT, 'prompts');
68
77
 
78
+ async function loadAuthCredentials(
79
+ site: string,
80
+ recordedValues: Record<string, string>,
81
+ ): Promise<CredentialStore> {
82
+ const stored = await loadCredentialStore(site);
83
+ // Already-redacted recordings can reach the compiler through other callers.
84
+ // Never let their references shadow real values from the credential backend.
85
+ const literalValues = Object.fromEntries(
86
+ Object.entries(recordedValues).filter(([, value]) => !isCredentialReference(value)),
87
+ );
88
+ return {
89
+ site,
90
+ cookies: stored?.cookies ?? [],
91
+ storage: stored?.storage ?? [],
92
+ values: { ...stored?.values, ...literalValues },
93
+ };
94
+ }
95
+
96
+ async function promptAuthUser(
97
+ message: string,
98
+ options?: string[],
99
+ signal?: AbortSignal,
100
+ ): Promise<string> {
101
+ const result = await requestAgentInteraction(
102
+ {
103
+ action: 'prompt_user',
104
+ prompt: { message, options, sensitive: options === undefined },
105
+ },
106
+ signal,
107
+ );
108
+ if (result.status !== 'answered' || !('value' in result)) {
109
+ throw new Error('reason' in result ? result.reason : 'User input is unavailable');
110
+ }
111
+ return await resolveUserInput(result.value);
112
+ }
113
+
114
+ interface AuthCheckpointExecutorOptions {
115
+ context: AuthCompileContext;
116
+ session: Session;
117
+ sessionPath: string;
118
+ toolDir: string;
119
+ authToolPlan: NonNullable<AuthToolPlan>;
120
+ verifier: AuthVerifier;
121
+ signal?: AbortSignal;
122
+ deadline?: () => number | undefined;
123
+ onPrompt?: CompileAuthAgentOptions['onPrompt'];
124
+ onCooldown?: CompileAuthAgentOptions['onCooldown'];
125
+ onVerification?: (action: string, result: AuthActionResult) => void;
126
+ }
127
+
128
+ /** One operation owns its current human references. All provider adapters execute
129
+ * the same host actions; native/API calls await them without ending an assignment. */
130
+ function createAuthCheckpointExecutor(options: AuthCheckpointExecutorOptions) {
131
+ const promptSecrets = new Map<string, string>();
132
+ const lifetime = new AbortController();
133
+ return {
134
+ dispose: () => {
135
+ promptSecrets.clear();
136
+ lifetime.abort(new Error('Authentication checkpoint operation ended'));
137
+ },
138
+ async execute(cp: AuthCheckpoint, requestSignal?: AbortSignal): Promise<AuthCheckpointResult> {
139
+ const signal = AbortSignal.any(
140
+ [lifetime.signal, options.signal, requestSignal].filter(
141
+ (value): value is AbortSignal => value !== undefined,
142
+ ),
143
+ );
144
+ try {
145
+ if (signal?.aborted) throw abortSignalError(signal);
146
+ if (cp.kind === 'prompt_user') {
147
+ const answer = await withAbortSignal(
148
+ () =>
149
+ options.onPrompt
150
+ ? options.onPrompt(cp.message, cp.options)
151
+ : promptAuthUser(cp.message, cp.options, signal),
152
+ signal,
153
+ );
154
+ if (cp.options?.includes(answer))
155
+ return {
156
+ result: JSON.stringify({ status: 'answered', kind: 'choice', value: answer }),
157
+ };
158
+ const reference = `\${prompt.${randomUUID()}}`;
159
+ promptSecrets.set(reference, answer);
160
+ return {
161
+ result: answer
162
+ ? `The user response is stored locally as ${reference}. Pass that exact reference as the appropriate run_verification parameter value; the host resolves it without exposing the answer to the model.`
163
+ : 'The user provided no input.',
164
+ };
165
+ }
166
+ if (cp.kind === 'inspect_verification_page') {
167
+ const inspected = await options.verifier.inspectPage({
168
+ maxChars: cp.maxChars,
169
+ includeCookies: cp.includeCookies,
170
+ signal,
171
+ });
172
+ return {
173
+ result: inspected.ok
174
+ ? `Current verification page snapshot:\n${JSON.stringify(inspected, null, 2)}`
175
+ : `Verification page inspection was unavailable: ${inspected.message ?? 'unknown reason'}`,
176
+ };
177
+ }
178
+ if (cp.kind === 'interact_verification_browser') {
179
+ const action = AuthBrowserActionSchema.parse(cp);
180
+ if ('value' in action)
181
+ action.value = await resolveUserInput(
182
+ promptSecrets.get(action.value) ?? action.value,
183
+ signal,
184
+ );
185
+ return {
186
+ result: options.context
187
+ .discoveryEvidence(options.session, options.toolDir)
188
+ .append(await options.verifier.interactBrowser(action, signal)),
189
+ };
190
+ }
191
+ if (cp.kind === 'wait_for_cooldown') {
192
+ await withAbortSignal(
193
+ () =>
194
+ options.onCooldown
195
+ ? options.onCooldown(cp.minutes, cp.reason, signal)
196
+ : abortableDelay(cp.minutes * 60_000, signal),
197
+ signal,
198
+ );
199
+ return {
200
+ result: `Requested delay of ~${cp.minutes} min completed; no live action ran during the wait.`,
201
+ };
202
+ }
203
+ const failures = await authLivePreflightFailures(
204
+ options.toolDir,
205
+ options.session,
206
+ options.authToolPlan.credentialNames,
207
+ [...options.authToolPlan.credentialRequestSeqs, ...options.authToolPlan.authRequestSeqs],
208
+ options.sessionPath,
209
+ );
210
+ if (signal?.aborted) throw abortSignalError(signal);
211
+ const deadline = options.deadline?.();
212
+ if (deadline !== undefined && Date.now() >= deadline)
213
+ throw new ProviderDeadlineError(deadline);
214
+ if (failures.length)
215
+ return {
216
+ result: `run_verification was blocked before any live login was fired because workflow.json failed auth preflight:\n${failures.map((failure) => `- ${failure}`).join('\n')}`,
217
+ isError: true,
218
+ };
219
+ const workflowHash = authWorkflowHash(
220
+ WorkflowSchema.parse(
221
+ JSON.parse(readFileSync(pathJoin(options.toolDir, 'workflow.json'), 'utf8')),
222
+ ),
223
+ );
224
+ if (cp.freshSession || cp.cleanSession) promptSecrets.clear();
225
+ const parameters = await resolveUserInput(
226
+ Object.fromEntries(
227
+ Object.entries(cp.parameters ?? {}).map(([name, value]) => [
228
+ name,
229
+ typeof value === 'string' && promptSecrets.has(value)
230
+ ? (promptSecrets.get(value) ?? value)
231
+ : value,
232
+ ]),
233
+ ),
234
+ signal,
235
+ );
236
+ const result = await options.verifier.runAction(cp.action, parameters, {
237
+ freshSession: cp.freshSession,
238
+ cleanSession: cp.cleanSession,
239
+ signal,
240
+ });
241
+ writeFileSync(
242
+ pathJoin(options.toolDir, AUTH_VERIFICATION_ATTEMPT_SENTINEL),
243
+ JSON.stringify({
244
+ action: cp.action,
245
+ ok: result.ok,
246
+ status: result.status,
247
+ error: result.error,
248
+ backend: result.usedBackend,
249
+ workflowHash,
250
+ timestamp: Date.now(),
251
+ }),
252
+ { mode: 0o600 },
253
+ );
254
+ options.onVerification?.(cp.action, result);
255
+ return { result: formatVerifyResult(cp.action, result) };
256
+ } catch (error) {
257
+ if (signal?.aborted) throw abortSignalError(signal, 'Auth checkpoint cancelled');
258
+ const control = providerControlError(error);
259
+ if (control) throw control;
260
+ return {
261
+ result: `The host could not perform ${cp.kind}: ${error instanceof Error ? error.message : String(error)}`,
262
+ isError: true,
263
+ };
264
+ }
265
+ },
266
+ };
267
+ }
268
+
269
+ function liveAuthTools(executor: ReturnType<typeof createAuthCheckpointExecutor>): AgentTool[] {
270
+ return AUTH_CHECKPOINT_TOOLS.map((definition) => ({
271
+ ...definition,
272
+ description: `${definition.description} Await the host result and continue in this assignment.`,
273
+ handler: async (input, request) =>
274
+ executor.execute(parseAuthCheckpoint(definition.name, input), request?.signal),
275
+ }));
276
+ }
277
+
278
+ /** Run-local ownership, never persisted or shared across teach runs. */
279
+ export class AuthCompileContext {
280
+ readonly #abort = new AbortController();
281
+ #verifier?: AuthVerifier;
282
+ #workflowPath?: string;
283
+ #evidence?: AuthDiscoveryEvidence;
284
+ #dispose?: Promise<void>;
285
+ cliSession?: { driver: 'claude-cli' | 'codex-cli'; sessionId: string };
286
+ history: ConversationLogEntry[] = [];
287
+ previousResult?: Pick<CompileAgentResult, 'outcome' | 'message'>;
288
+
289
+ get signal(): AbortSignal {
290
+ return this.#abort.signal;
291
+ }
292
+
293
+ async verifier(
294
+ workflowPath: string,
295
+ credentials: () => Promise<CredentialStore>,
296
+ discoveryUrl?: string,
297
+ ): Promise<AuthVerifier> {
298
+ if (this.signal.aborted) throw abortSignalError(this.signal);
299
+ if (this.#workflowPath && this.#workflowPath !== workflowPath)
300
+ throw new Error('An authentication context belongs to one workflow workspace');
301
+ this.#workflowPath = workflowPath;
302
+ const values = await credentials();
303
+ if (this.signal.aborted) throw abortSignalError(this.signal);
304
+ if (this.#verifier) this.#verifier.updateCredentials(values);
305
+ else this.#verifier = new AuthVerifier(workflowPath, values, discoveryUrl);
306
+ return this.#verifier;
307
+ }
308
+
309
+ discoveryEvidence(session: Session, toolDir: string): AuthDiscoveryEvidence {
310
+ this.#evidence ??= new AuthDiscoveryEvidence(session, toolDir);
311
+ return this.#evidence;
312
+ }
313
+
314
+ recoveryMessage(plan: NonNullable<AuthToolPlan>): string {
315
+ return `Continue the existing authentication task in this same workspace and browser.
316
+ Existing artifacts and earlier observations remain available; do not repeat the initial recording read by default.
317
+ Current request: ${JSON.stringify(plan)}
318
+ Previous compiler outcome: ${this.previousResult?.outcome ?? 'interrupted'}
319
+ Previous report: ${this.previousResult?.message ?? '(no terminal report)'}
320
+ Current verification metadata: ${JSON.stringify(this.#verifier?.state ?? {})}
321
+ Earlier human-response references have expired. Ask for fresh input when an action requires it.
322
+ First run a proven noninteractive session check or context-restoration action in the retained browser. If it passes, return the result so the caller can retry its own protected operation. Before starting another login or delivering a factor, inspect current evidence that the existing access is unusable and the new login/challenge is required. Revise the procedure with response-based branches when needed. A prior auth success does not prove the caller's data operation works.`;
323
+ }
324
+
325
+ dispose(): Promise<void> {
326
+ if (this.#dispose) return this.#dispose;
327
+ this.#abort.abort(new DOMException('Authentication context closed', 'AbortError'));
328
+ this.cliSession = undefined;
329
+ this.history = [];
330
+ this.previousResult = undefined;
331
+ this.#dispose = this.#verifier?.drain() ?? Promise.resolve();
332
+ return this.#dispose;
333
+ }
334
+ }
335
+
69
336
  interface CompileAuthAgentOptions {
337
+ /** Keep a teach's internal authentication build in its run staging directory. */
338
+ outDir?: string;
339
+ /** Keep browser and provider conversation for later recovery in this teach. */
340
+ context?: AuthCompileContext;
70
341
  site: string;
71
342
  session: Session;
72
343
  sessionPath: string;
@@ -131,11 +402,38 @@ MANDATORY FIRST ACTION: call read_session_summary now. Do not write prose, do no
131
402
  }
132
403
 
133
404
  export async function compileAuthAgent(opts: CompileAuthAgentOptions): Promise<CompileAgentResult> {
405
+ const context = opts.context ?? new AuthCompileContext();
406
+ try {
407
+ const result = await compileAuthWithContext({
408
+ ...opts,
409
+ context,
410
+ signal: opts.signal ? AbortSignal.any([opts.signal, context.signal]) : context.signal,
411
+ });
412
+ context.previousResult = { outcome: result.outcome, message: result.message };
413
+ return result;
414
+ } catch (error) {
415
+ context.previousResult = {
416
+ outcome: 'error',
417
+ message:
418
+ 'The previous authentication request was interrupted; its retained state is available for inspection.',
419
+ };
420
+ throw error;
421
+ } finally {
422
+ if (!opts.context) await context.dispose();
423
+ }
424
+ }
425
+
426
+ async function compileAuthWithContext(
427
+ input: CompileAuthAgentOptions & { context: AuthCompileContext },
428
+ ): Promise<CompileAgentResult> {
134
429
  const startTime = Date.now();
135
- const { site, session, authToolPlan } = opts;
430
+ const { site, authToolPlan } = input;
136
431
  const toolName = authToolPlan.toolName;
137
- const toolDir = localToolDir(site, toolName);
432
+ const toolDir = input.outDir ?? localToolDir(site, toolName);
138
433
  mkdirSync(toolDir, { recursive: true });
434
+ const evidence = input.context.discoveryEvidence(input.session, toolDir);
435
+ const opts = { ...input, session: evidence.session, sessionPath: evidence.sessionPath };
436
+ const session = evidence.session;
139
437
 
140
438
  const systemPromptPath = pathJoin(PROMPTS_DIR, 'auth-compile-agent.md');
141
439
  if (!existsSync(systemPromptPath)) {
@@ -146,7 +444,10 @@ export async function compileAuthAgent(opts: CompileAuthAgentOptions): Promise<C
146
444
  opts.runDeadline,
147
445
  opts.deadlineMs ?? Date.now() + (opts.maxDurationMs ?? 10 * 60 * 1000),
148
446
  );
149
- const initialUserMessage = buildAuthInitialMessage({ site, toolName, toolDir, authToolPlan });
447
+ const recovering = opts.context.cliSession !== undefined || opts.context.history.length > 0;
448
+ const initialUserMessage = recovering
449
+ ? opts.context.recoveryMessage(authToolPlan)
450
+ : buildAuthInitialMessage({ site, toolName, toolDir, authToolPlan });
150
451
 
151
452
  // Provider dispatch mirrors compile-agent.ts. CLI providers don't implement
152
453
  // messageWithTools — shell out with the auth toolset as a stdio MCP server.
@@ -158,6 +459,7 @@ export async function compileAuthAgent(opts: CompileAuthAgentOptions): Promise<C
158
459
  if (resolved.name === 'claude-cli') {
159
460
  return await runAuthSegmentLoop({
160
461
  driver: 'claude-cli',
462
+ context: opts.context,
161
463
  site,
162
464
  session,
163
465
  sessionPath: opts.sessionPath,
@@ -180,6 +482,7 @@ export async function compileAuthAgent(opts: CompileAuthAgentOptions): Promise<C
180
482
  if (resolved.name === 'codex-cli') {
181
483
  return await runAuthSegmentLoop({
182
484
  driver: 'codex-cli',
485
+ context: opts.context,
183
486
  site,
184
487
  session,
185
488
  sessionPath: opts.sessionPath,
@@ -211,108 +514,147 @@ export async function compileAuthAgent(opts: CompileAuthAgentOptions): Promise<C
211
514
  }
212
515
 
213
516
  // ─── In-process runAgentLoop path (anthropic-api / injected provider) ───────
214
- const systemPrompt = `${readFileSync(systemPromptPath, 'utf8')}\n\nToday's date is ${new Date().toISOString().slice(0, 10)}.`;
215
- const tools: AgentTool[] = [
216
- ...buildAuthCompileTools(session, toolDir, opts.sessionPath, opts.teachCredentials),
217
- doneTool(),
218
- giveUpTool(),
219
- ];
517
+ const active = combinedDeadlineSignal(
518
+ runDeadline,
519
+ undefined,
520
+ opts.signal,
521
+ Date.now,
522
+ undefined,
523
+ opts.onDeadlineReached,
524
+ );
525
+ const attemptPath = pathJoin(toolDir, AUTH_VERIFICATION_ATTEMPT_SENTINEL);
526
+ if (existsSync(attemptPath)) unlinkSync(attemptPath);
527
+ const verifier = await opts.context.verifier(
528
+ pathJoin(toolDir, 'workflow.json'),
529
+ () => loadAuthCredentials(site, opts.teachCredentials.values),
530
+ session.url,
531
+ );
532
+ const checkpointExecutor = createAuthCheckpointExecutor({
533
+ ...opts,
534
+ toolDir,
535
+ verifier,
536
+ signal: active.signal,
537
+ deadline: () => runDeadline?.deadlineMs,
538
+ });
539
+ try {
540
+ const systemPrompt = `${readFileSync(systemPromptPath, 'utf8')}\n\nToday's date is ${new Date().toISOString().slice(0, 10)}.`;
541
+ const tools: AgentTool[] = [
542
+ ...buildAuthCompileTools(session, toolDir, opts.sessionPath, opts.teachCredentials),
543
+ ...liveAuthTools(checkpointExecutor),
544
+ doneTool(),
545
+ giveUpTool(),
546
+ ];
547
+
548
+ const conversationLogPath = pathJoin(toolDir, '.compile-log.json');
549
+
550
+ let totalTurns = 0;
551
+ let totalInputTokens = 0;
552
+ let totalOutputTokens = 0;
553
+ let outcome: AgentResult['outcome'] = 'error';
554
+ let message = '';
555
+ let conversationLog: AgentResult['conversationLog'] = [...opts.context.history];
556
+
557
+ let verificationCycle = 0;
558
+ let currentInitialMessage = initialUserMessage;
220
559
 
221
- const conversationLogPath = pathJoin(toolDir, '.compile-log.json');
560
+ while (true) {
561
+ verificationCycle++;
562
+ conversationLog = completeAuthHistory(conversationLog);
222
563
 
223
- let totalTurns = 0;
224
- let totalInputTokens = 0;
225
- let totalOutputTokens = 0;
226
- let outcome: AgentResult['outcome'] = 'error';
227
- let message = '';
228
- let conversationLog: AgentResult['conversationLog'] = [];
229
-
230
- let verificationCycle = 0;
231
- let currentInitialMessage = initialUserMessage;
232
-
233
- while (true) {
234
- verificationCycle++;
235
-
236
- const userOnProgress = opts.onProgress;
237
- const wrappedOnProgress = userOnProgress
238
- ? (p: AgentProgress) =>
239
- userOnProgress({
240
- ...p,
241
- verificationCycle,
242
- })
243
- : undefined;
564
+ const userOnProgress = opts.onProgress;
565
+ const wrappedOnProgress = userOnProgress
566
+ ? (p: AgentProgress) =>
567
+ userOnProgress({
568
+ ...p,
569
+ verificationCycle,
570
+ })
571
+ : undefined;
244
572
 
245
- const result = await runAgentLoop({
246
- systemPrompt,
247
- initialUserMessage: currentInitialMessage,
248
- tools,
249
- deadlineMs: runDeadline?.deadlineMs ?? Date.now(),
250
- runDeadline,
251
- softTurnCap: 30,
252
- llm: provider,
253
- onProgress: wrappedOnProgress,
254
- onConversationUpdate: (currentCycleLog) => {
255
- const fullLog = [...conversationLog, ...currentCycleLog];
256
- writeFileSync(conversationLogPath, JSON.stringify(fullLog, null, 2), 'utf8');
257
- },
258
- onDeadlineReached: opts.onDeadlineReached,
259
- signal: opts.signal,
260
- });
573
+ const result = await runAgentLoop({
574
+ systemPrompt,
575
+ initialUserMessage: currentInitialMessage,
576
+ tools,
577
+ deadlineMs: runDeadline?.deadlineMs ?? Date.now(),
578
+ runDeadline,
579
+ softTurnCap: 30,
580
+ llm: {
581
+ ...provider,
582
+ messageWithTools: (input) =>
583
+ provider.messageWithTools({
584
+ ...input,
585
+ messages: [...authHistoryMessages(conversationLog), ...input.messages],
586
+ }),
587
+ },
588
+ onProgress: wrappedOnProgress,
589
+ onConversationUpdate: (currentCycleLog) => {
590
+ const fullLog = [...conversationLog, ...currentCycleLog];
591
+ opts.context.history = fullLog;
592
+ writeFileSync(conversationLogPath, JSON.stringify(fullLog, null, 2), 'utf8');
593
+ },
594
+ onDeadlineReached: opts.onDeadlineReached,
595
+ signal: active.signal,
596
+ });
597
+
598
+ totalTurns += result.turns;
599
+ totalInputTokens += result.inputTokens;
600
+ totalOutputTokens += result.outputTokens;
601
+ conversationLog = [...conversationLog, ...result.conversationLog];
602
+ opts.context.history = conversationLog;
261
603
 
262
- totalTurns += result.turns;
263
- totalInputTokens += result.inputTokens;
264
- totalOutputTokens += result.outputTokens;
265
- conversationLog = [...conversationLog, ...result.conversationLog];
604
+ outcome = result.outcome;
266
605
 
267
- outcome = result.outcome;
606
+ if (result.outcome !== 'done') {
607
+ message = buildMessageFromOutcome(result);
608
+ break;
609
+ }
268
610
 
269
- if (result.outcome !== 'done') {
270
- message = buildMessageFromOutcome(result);
271
- break;
272
- }
611
+ const failures = authExternalVerification(
612
+ toolDir,
613
+ (authToolPlan.captures ?? []).map((c) => ({ name: c.name, usedAs: c.usedAs })),
614
+ {
615
+ requireLiveAttempt: true,
616
+ requiredCredentialNames: authToolPlan.credentialNames,
617
+ },
618
+ );
273
619
 
274
- const failures = authExternalVerification(
275
- toolDir,
276
- (authToolPlan.captures ?? []).map((c) => ({ name: c.name, usedAs: c.usedAs })),
277
- {
278
- requireLiveAttempt: true,
279
- requiredCredentialNames: authToolPlan.credentialNames,
280
- },
281
- );
282
-
283
- if (failures.length === 0) {
284
- message = result.doneSummary ?? 'Auth tool compiled';
285
- break;
286
- }
620
+ if (failures.length === 0) {
621
+ message = result.doneSummary ?? 'Auth tool compiled';
622
+ break;
623
+ }
287
624
 
288
- log(`auth verification failed (cycle ${verificationCycle}), resuming agent loop...`);
289
- currentInitialMessage = `You called done but verification failed:
625
+ log(`auth verification failed (cycle ${verificationCycle}), resuming agent loop...`);
626
+ currentInitialMessage = `You called done but verification failed:
290
627
 
291
628
  ${failures.map((f) => `- ${f}`).join('\n')}
292
629
 
293
630
  Fix the issues in workflow.json, re-test with run_verification, and call done again.`;
294
- }
631
+ }
295
632
 
296
- writeFileSync(conversationLogPath, JSON.stringify(conversationLog, null, 2), 'utf8');
633
+ writeFileSync(conversationLogPath, JSON.stringify(conversationLog, null, 2), 'utf8');
297
634
 
298
- const workflowPath = pathJoin(toolDir, 'workflow.json');
635
+ const workflowPath = pathJoin(toolDir, 'workflow.json');
299
636
 
300
- return {
301
- success: outcome === 'done',
302
- outcome,
303
- workflowPath: existsSync(workflowPath) ? workflowPath : undefined,
304
- message,
305
- conversationLogPath,
306
- turns: totalTurns,
307
- durationMs: Date.now() - startTime,
308
- inputTokens: totalInputTokens,
309
- outputTokens: totalOutputTokens,
310
- cacheReadInputTokens: 0,
311
- cacheCreationInputTokens: 0,
312
- };
637
+ return {
638
+ success: outcome === 'done',
639
+ outcome,
640
+ workflowPath: existsSync(workflowPath) ? workflowPath : undefined,
641
+ message,
642
+ conversationLogPath,
643
+ turns: totalTurns,
644
+ durationMs: Date.now() - startTime,
645
+ inputTokens: totalInputTokens,
646
+ outputTokens: totalOutputTokens,
647
+ cacheReadInputTokens: 0,
648
+ cacheCreationInputTokens: 0,
649
+ };
650
+ } finally {
651
+ checkpointExecutor.dispose();
652
+ active.dispose();
653
+ }
313
654
  }
314
655
 
315
656
  interface AuthSegmentLoopOptions {
657
+ context: AuthCompileContext;
316
658
  driver: 'claude-cli' | 'codex-cli';
317
659
  site: string;
318
660
  session: Session;
@@ -341,6 +683,10 @@ function verifyFacts(r: AuthActionResult): string {
341
683
  if (typeof r.status === 'number') parts.push(`httpStatus=${r.status}`);
342
684
  if (r.error) parts.push(`error=${r.error}`);
343
685
  let s = `[${parts.join(' | ')}]`;
686
+ if (r.requestStageFacts) s += `\nRequest stages: ${JSON.stringify(r.requestStageFacts)}`;
687
+ if (r.authExecution) s += `\nAction execution: ${JSON.stringify(r.authExecution)}`;
688
+ if (r.responseObservations)
689
+ s += `\nResponse observations: ${JSON.stringify(r.responseObservations)}`;
344
690
  if (r.responseBodyPreview) s += `\nResponse body (truncated): ${r.responseBodyPreview}`;
345
691
  return s;
346
692
  }
@@ -367,7 +713,7 @@ function formatVerifyResult(action: string, r: AuthActionResult): string {
367
713
  * page inspection on the persistent AuthVerifier session, prompt_user via the
368
714
  * TUI bridge, or a cool-off wait — then feeds the result into the next segment.
369
715
  * The ONE stateful thing (the live browser) lives in the AuthVerifier and is
370
- * drained at the end. CLI conversation state is carried by provider-native resume
716
+ * retained until its owning context closes. CLI conversation state uses provider-native resume
371
717
  * (`claude --resume`, `codex exec resume`), not retained here.
372
718
  */
373
719
  async function runAuthSegmentLoop(opts: AuthSegmentLoopOptions): Promise<CompileAgentResult> {
@@ -383,28 +729,15 @@ async function runAuthSegmentLoop(opts: AuthSegmentLoopOptions): Promise<Compile
383
729
  const workflowPath = pathJoin(opts.toolDir, 'workflow.json');
384
730
  const verificationAttemptPath = pathJoin(opts.toolDir, AUTH_VERIFICATION_ATTEMPT_SENTINEL);
385
731
  if (existsSync(verificationAttemptPath)) unlinkSync(verificationAttemptPath);
386
- const storedCredentials = await loadCredentialStore(opts.site);
387
- const credsForRun = {
388
- site: opts.site,
389
- cookies: storedCredentials?.cookies ?? [],
390
- values: { ...(storedCredentials?.values ?? {}), ...opts.teachCredentials.values },
391
- storage: storedCredentials?.storage ?? [],
392
- };
393
- const verifier = new AuthVerifier(workflowPath, credsForRun);
394
- const authMode = {
395
- site: opts.site,
396
- authPlanJson: JSON.stringify(opts.authToolPlan),
397
- allowedTools: AUTH_COMPILE_TOOL_NAMES,
398
- initialPrompt: opts.initialPrompt,
399
- };
400
-
401
- const onPrompt = opts.onPrompt ?? (async () => '');
402
- const onCooldown =
403
- opts.onCooldown ??
404
- (async (minutes: number, _reason?: string, signal?: AbortSignal) =>
405
- abortableDelay(minutes * 60_000, signal));
406
-
407
- let resume: { sessionId: string; message: string } | undefined;
732
+ const verifier = await opts.context.verifier(
733
+ workflowPath,
734
+ () => loadAuthCredentials(opts.site, opts.teachCredentials.values),
735
+ opts.session.url,
736
+ );
737
+ let resume: { sessionId: string; message: string } | undefined =
738
+ opts.context.cliSession?.driver === opts.driver
739
+ ? { sessionId: opts.context.cliSession.sessionId, message: opts.initialPrompt }
740
+ : undefined;
408
741
  let last: CompileAgentResult | undefined;
409
742
  let totalTurns = 0;
410
743
  let totalInputTokens = 0;
@@ -415,8 +748,42 @@ async function runAuthSegmentLoop(opts: AuthSegmentLoopOptions): Promise<Compile
415
748
  // line so a failure (e.g. a 403) is visible the instant it happens.
416
749
  let lastVerification: CompileAgentProgress['lastVerification'];
417
750
  let segment = 0;
418
- const promptSecrets = new Map<string, string>();
419
- let promptSecretSequence = 0;
751
+ const checkpointExecutor = createAuthCheckpointExecutor({
752
+ ...opts,
753
+ verifier,
754
+ signal: active.signal,
755
+ deadline: () => runDeadline?.deadlineMs ?? opts.deadlineMs,
756
+ onVerification: (action, result) => {
757
+ lastVerification = {
758
+ action,
759
+ ok: result.ok,
760
+ status: result.status,
761
+ error: result.error,
762
+ backend: result.usedBackend,
763
+ durationMs: result.durationMs,
764
+ checkpoint: 'run_verification',
765
+ };
766
+ opts.onProgress?.({
767
+ turn: totalTurns,
768
+ phase: 'tool',
769
+ elapsedMs: Date.now() - opts.startTime,
770
+ budgetMs: Math.max(0, (runDeadline?.deadlineMs ?? opts.deadlineMs) - Date.now()),
771
+ inputTokens: totalInputTokens,
772
+ outputTokens: totalOutputTokens,
773
+ verificationCycle: 1,
774
+ segment,
775
+ lastVerification,
776
+ });
777
+ },
778
+ });
779
+ const authMode = {
780
+ site: opts.site,
781
+ contextRef: opts.authToolPlan.contextRef,
782
+ authPlanJson: JSON.stringify(opts.authToolPlan),
783
+ allowedTools: AUTH_COMPILE_TOOL_NAMES,
784
+ initialPrompt: opts.initialPrompt,
785
+ executeCheckpoint: checkpointExecutor.execute,
786
+ };
420
787
 
421
788
  try {
422
789
  while (true) {
@@ -467,6 +834,8 @@ async function runAuthSegmentLoop(opts: AuthSegmentLoopOptions): Promise<Compile
467
834
  : compileViaCodexCli(common);
468
835
  },
469
836
  });
837
+ if (result.sessionId)
838
+ opts.context.cliSession = { driver: opts.driver, sessionId: result.sessionId };
470
839
  last = result;
471
840
  totalTurns += result.turns;
472
841
  totalInputTokens += result.inputTokens;
@@ -507,116 +876,8 @@ async function runAuthSegmentLoop(opts: AuthSegmentLoopOptions): Promise<Compile
507
876
  break;
508
877
  }
509
878
 
510
- const cp: AuthCheckpoint = result.checkpoint;
511
- let resultMsg: string;
512
- try {
513
- if (cp.kind === 'run_verification') {
514
- const preflightFailures = await authLivePreflightFailures(
515
- opts.toolDir,
516
- opts.session,
517
- opts.authToolPlan.credentialNames,
518
- [...opts.authToolPlan.credentialRequestSeqs, ...opts.authToolPlan.authRequestSeqs],
519
- opts.sessionPath,
520
- );
521
- if (Date.now() >= (runDeadline?.deadlineMs ?? opts.deadlineMs)) {
522
- throw new ProviderDeadlineError(runDeadline?.deadlineMs ?? opts.deadlineMs);
523
- }
524
- if (preflightFailures.length > 0) {
525
- resultMsg = `run_verification was blocked before any live login was fired because workflow.json failed auth preflight:\n${preflightFailures
526
- .map((failure) => `- ${failure}`)
527
- .join('\n')}`;
528
- } else {
529
- const verifiedWorkflowHash = authWorkflowHash(
530
- WorkflowSchema.parse(
531
- JSON.parse(readFileSync(pathJoin(opts.toolDir, 'workflow.json'), 'utf8')),
532
- ),
533
- );
534
- const resolvedParameters = Object.fromEntries(
535
- Object.entries(cp.parameters ?? {}).map(([name, value]) => [
536
- name,
537
- typeof value === 'string' && promptSecrets.has(value)
538
- ? (promptSecrets.get(value) ?? '')
539
- : value,
540
- ]),
541
- );
542
- const r = await verifier.runAction(cp.action, resolvedParameters, {
543
- freshSession: cp.freshSession,
544
- cleanSession: cp.cleanSession,
545
- signal: active.signal,
546
- });
547
- writeFileSync(
548
- verificationAttemptPath,
549
- JSON.stringify(
550
- {
551
- action: cp.action,
552
- ok: r.ok,
553
- status: r.status,
554
- error: r.error,
555
- backend: r.usedBackend,
556
- workflowHash: verifiedWorkflowHash,
557
- timestamp: Date.now(),
558
- },
559
- null,
560
- 2,
561
- ),
562
- 'utf8',
563
- );
564
- // Record + immediately surface the result so the spinner reflects a
565
- // failure the moment it happens — not only on the next agent turn.
566
- lastVerification = {
567
- action: cp.action,
568
- ok: r.ok,
569
- status: r.status,
570
- error: r.error,
571
- backend: r.usedBackend,
572
- durationMs: r.durationMs,
573
- checkpoint: 'run_verification',
574
- };
575
- opts.onProgress?.({
576
- turn: totalTurns,
577
- phase: 'tool',
578
- elapsedMs: Date.now() - opts.startTime,
579
- budgetMs: Math.max(0, (runDeadline?.deadlineMs ?? opts.deadlineMs) - Date.now()),
580
- inputTokens: totalInputTokens,
581
- outputTokens: totalOutputTokens,
582
- verificationCycle: 1,
583
- segment,
584
- lastVerification,
585
- });
586
- resultMsg = formatVerifyResult(cp.action, r);
587
- }
588
- } else if (cp.kind === 'inspect_verification_page') {
589
- const inspected = await verifier.inspectPage({
590
- maxChars: cp.maxChars,
591
- includeCookies: cp.includeCookies,
592
- signal: active.signal,
593
- });
594
- resultMsg = inspected.ok
595
- ? `Current verification page snapshot:\n${JSON.stringify(inspected, null, 2)}`
596
- : `Verification page inspection was unavailable: ${inspected.message ?? 'unknown reason'}`;
597
- } else if (cp.kind === 'prompt_user') {
598
- const answer = await withAbortSignal(
599
- () => onPrompt(cp.message, cp.options),
600
- active.signal,
601
- );
602
- const reference = `\${prompt.${++promptSecretSequence}}`;
603
- promptSecrets.set(reference, answer);
604
- resultMsg = answer
605
- ? `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.`
606
- : 'The user provided no input.';
607
- } else {
608
- await withAbortSignal(
609
- () => onCooldown(cp.minutes, cp.reason, active.signal),
610
- active.signal,
611
- );
612
- resultMsg = `Requested delay of ~${cp.minutes} min completed; no live action ran during the wait.`;
613
- }
614
- } catch (err) {
615
- if (opts.signal?.aborted) throw abortSignalError(opts.signal, 'Auth compile cancelled');
616
- const control = providerControlError(active.signal?.aborted ? active.signal.reason : err);
617
- if (control) throw control;
618
- resultMsg = `The orchestrator could not perform ${cp.kind}: ${err instanceof Error ? err.message : String(err)}`;
619
- }
879
+ const cp = result.checkpoint;
880
+ const { result: resultMsg } = await checkpointExecutor.execute(cp);
620
881
 
621
882
  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.`;
622
883
  resume = {
@@ -626,7 +887,7 @@ async function runAuthSegmentLoop(opts: AuthSegmentLoopOptions): Promise<Compile
626
887
  }
627
888
  } finally {
628
889
  active.dispose();
629
- await verifier.drain();
890
+ checkpointExecutor.dispose();
630
891
  }
631
892
 
632
893
  if (!last) {
@@ -656,6 +917,34 @@ async function runAuthSegmentLoop(opts: AuthSegmentLoopOptions): Promise<Compile
656
917
  };
657
918
  }
658
919
 
920
+ /** Complete terminal tool calls before a later API recovery turn. */
921
+ function completeAuthHistory(history: ConversationLogEntry[]): ConversationLogEntry[] {
922
+ const last = history.at(-1);
923
+ if (last?.role !== 'assistant' || !Array.isArray(last.content)) return history;
924
+ const calls = last.content.filter((block) => block.type === 'tool_use');
925
+ if (!calls.length) return history;
926
+ return [
927
+ ...history,
928
+ {
929
+ turn: last.turn,
930
+ role: 'user',
931
+ content: calls.map((block) => ({
932
+ type: 'tool_result',
933
+ tool_use_id: block.id,
934
+ content: 'Previous authentication request ended. Continue from current evidence.',
935
+ })),
936
+ },
937
+ ];
938
+ }
939
+
940
+ function authHistoryMessages(
941
+ history: ConversationLogEntry[],
942
+ ): Parameters<ToolUseProvider['messageWithTools']>[0]['messages'] {
943
+ return completeAuthHistory(history).map(({ role, content }) => ({ role, content })) as Parameters<
944
+ ToolUseProvider['messageWithTools']
945
+ >[0]['messages'];
946
+ }
947
+
659
948
  function buildMessageFromOutcome(result: AgentResult): string {
660
949
  switch (result.outcome) {
661
950
  case 'give_up':