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
@@ -18,7 +18,6 @@ import { createLog } from './log.ts';
18
18
  import { compactRequestContexts, requestContextDigest } from './request-context.ts';
19
19
  import { isTelemetryRequest } from './telemetry.ts';
20
20
  import { setSpanAttributes, traced } from './tracing.ts';
21
- import { TwoFactorTypeSchema } from './types.ts';
22
21
  import type { CapturedRequest, Session } from './types.ts';
23
22
 
24
23
  const PROMPTS_DIR = pathJoin(import.meta.dir, '..', '..', 'prompts');
@@ -97,14 +96,8 @@ export const SharedCompileContextSchema = z.object({
97
96
  credentialNames: z.array(z.string()).default([]),
98
97
  tokenExtractionNotes: z.string().default(''),
99
98
  sharedHelperNotes: z.string().default(''),
100
- twoFactorDetected: z.boolean().default(false),
101
- twoFactorType: TwoFactorTypeSchema,
102
- twoFactorRequestSeqs: z.array(z.number().int().nonnegative()).default([]),
103
- authCompletionSeqs: z.array(z.number().int().nonnegative()).default([]),
104
- /** OTP only: names of initiate-response fields the completion request reads
105
- * (chained as ${state.X}); listed structurally from the recording. */
106
- twoFactorContext: z.array(z.string()).default([]),
107
- twoFactorNotes: z.string().default(''),
99
+ authRequestSeqs: z.array(z.number().int().nonnegative()).default([]),
100
+ authNotes: z.string().default(''),
108
101
  });
109
102
  export type SharedCompileContext = z.infer<typeof SharedCompileContextSchema>;
110
103
 
@@ -115,7 +108,11 @@ export type SharedCompileContext = z.infer<typeof SharedCompileContextSchema>;
115
108
  * replaying the login inline (which hammers the site at compile time). */
116
109
  export function sharedContextHasAuth(ctx: SharedCompileContext | undefined): boolean {
117
110
  if (!ctx) return false;
118
- return ctx.twoFactorDetected || ctx.loginRequestSeqs.length > 0 || ctx.credentialNames.length > 0;
111
+ return (
112
+ ctx.authRequestSeqs.length > 0 ||
113
+ ctx.loginRequestSeqs.length > 0 ||
114
+ ctx.credentialNames.length > 0
115
+ );
119
116
  }
120
117
 
121
118
  export const ToolCandidateSchema = z.object({
@@ -338,6 +335,7 @@ export function buildSharedCompileContext(
338
335
  return {
339
336
  ...detection.sharedContext,
340
337
  loginRequestSeqs: [...new Set(detection.sharedContext.loginRequestSeqs)].sort((a, b) => a - b),
338
+ authRequestSeqs: [...new Set(detection.sharedContext.authRequestSeqs)].sort((a, b) => a - b),
341
339
  };
342
340
  }
343
341
 
@@ -161,9 +161,12 @@ export function toCamelCase(snake: string): string {
161
161
  .join('');
162
162
  }
163
163
 
164
- /** Zod validator from workflow parameters — enforces the same contract
165
- * for MCP args (from the LLM) and cron.json params. */
166
- export function buildZodValidator(parameters: WorkflowParameter[]): z.ZodObject<z.ZodRawShape> {
164
+ /** Zod validator from workflow parameters. Auth actions select their own
165
+ * required subset at runtime, so the MCP transport accepts omitted fields. */
166
+ export function buildZodValidator(
167
+ parameters: WorkflowParameter[],
168
+ options: { allOptional?: boolean } = {},
169
+ ): z.ZodObject<z.ZodRawShape> {
167
170
  const shape: z.ZodRawShape = {};
168
171
  for (const p of parameters) {
169
172
  let field: z.ZodType;
@@ -178,8 +181,13 @@ export function buildZodValidator(parameters: WorkflowParameter[]): z.ZodObject<
178
181
  field = z.boolean();
179
182
  break;
180
183
  }
184
+ if (p.choices?.length) {
185
+ field = field.refine((value) => p.choices?.includes(value), {
186
+ message: `Expected one of: ${p.choices.join(', ')}`,
187
+ });
188
+ }
181
189
  field = field.describe(p.description);
182
- if (p.default !== undefined) field = field.optional();
190
+ if (p.default !== undefined || options.allOptional) field = field.optional();
183
191
  shape[p.name] = field;
184
192
  }
185
193
  return z.object(shape);
@@ -111,6 +111,8 @@ const WorkflowParameterSchema = z.object({
111
111
  name: z.string(),
112
112
  type: z.enum(['string', 'number', 'boolean']),
113
113
  description: z.string(),
114
+ /** Optional finite choices to surface as JSON Schema enum values. */
115
+ choices: z.array(z.union([z.string(), z.number(), z.boolean()])).optional(),
114
116
  /** Optional with this default if set. */
115
117
  default: z.union([z.string(), z.number(), z.boolean()]).optional(),
116
118
  /** Whether a `param:<name>` integration test verified this parameter's effect
@@ -152,6 +154,13 @@ const CaptureCommonSchema = z.object({
152
154
  capability: StateCapabilitySchema.optional().default('ordinary_http'),
153
155
  });
154
156
 
157
+ const CaptureEqualsSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
158
+ const RequestCaptureCommonSchema = CaptureCommonSchema.extend({
159
+ /** Optional exact scalar match. This also makes an explicitly matched empty
160
+ * string or null a valid capture value. */
161
+ equals: CaptureEqualsSchema.optional(),
162
+ });
163
+
155
164
  const CookieCaptureSchema = CaptureCommonSchema.extend({
156
165
  source: z.literal('cookie'),
157
166
  cookie: z.string(),
@@ -162,22 +171,22 @@ const CookieCaptureSchema = CaptureCommonSchema.extend({
162
171
  allowHttpOnlyProjection: z.boolean().optional().default(false),
163
172
  });
164
173
 
165
- const RequestCaptureSchema = z.discriminatedUnion('source', [
166
- CaptureCommonSchema.extend({
174
+ export const RequestCaptureSchema = z.discriminatedUnion('source', [
175
+ RequestCaptureCommonSchema.extend({
167
176
  source: z.literal('json'),
168
177
  path: z.string(),
169
178
  }),
170
- CaptureCommonSchema.extend({
179
+ RequestCaptureCommonSchema.extend({
171
180
  source: z.literal('response_header'),
172
181
  header: z.string(),
173
182
  mode: z.enum(['first', 'last', 'all']).optional().default('last'),
174
183
  }),
175
- CaptureCommonSchema.extend({
184
+ RequestCaptureCommonSchema.extend({
176
185
  source: z.literal('text_regex'),
177
186
  pattern: z.string(),
178
187
  group: z.number().int().nonnegative().optional().default(1),
179
188
  }),
180
- CookieCaptureSchema,
189
+ CookieCaptureSchema.extend({ equals: CaptureEqualsSchema.optional() }),
181
190
  ]);
182
191
  export type RequestCapture = z.infer<typeof RequestCaptureSchema>;
183
192
 
@@ -226,11 +235,59 @@ export type BootstrapCapture = z.infer<typeof BootstrapCaptureSchema>;
226
235
 
227
236
  const WorkflowRequestSchema = z.object({
228
237
  method: z.string(),
238
+ /** Sequence number of the captured request that grounds this workflow request.
239
+ * Auth login uses this to resolve persisted captures only against their
240
+ * producing recorded response. */
241
+ recordingRequestSeq: z.number().int().nonnegative().optional(),
229
242
  /** Template; ${param.X} substitutes a parameter, ${response[N].path} an
230
243
  * earlier extracted value. */
231
244
  url: z.string(),
232
245
  headers: z.record(z.string()),
233
246
  body: z.string().optional(),
247
+ /** Execute this GET or URL-encoded POST as a top-level browser navigation instead of fetch().
248
+ * This lets the recorded page run its own JavaScript and mint coupled browser
249
+ * state, and lets form POSTs retain document-navigation semantics, without
250
+ * teaching Imprint site- or framework-specific behavior. */
251
+ mode: z.enum(['fetch', 'navigate']).optional(),
252
+ /** Bounded completion criteria for mode="navigate". Predicates are ANDed.
253
+ * With no predicate, navigation completes at the selected lifecycle event. */
254
+ navigation: z
255
+ .object({
256
+ waitUntil: z.enum(['domcontentloaded', 'load']).optional(),
257
+ timeoutMs: z.number().int().positive().optional(),
258
+ pollIntervalMs: z.number().int().positive().optional(),
259
+ urlIncludes: z.string().min(1).optional(),
260
+ /** Wait until the rendered document contains this CSS selector before
261
+ * snapshotting outerHTML. Useful for pages whose meaningful results
262
+ * hydrate after the load event. */
263
+ selector: z.string().min(1).optional(),
264
+ /** Optional browser interactions performed after the initial navigation
265
+ * predicate is satisfied. This is a generic escape hatch for workflows
266
+ * whose next request is assembled from private in-page state rather than
267
+ * values serialized into HTML. */
268
+ actions: z
269
+ .array(
270
+ z
271
+ .object({
272
+ action: z.literal('click'),
273
+ selector: z.string().min(1),
274
+ })
275
+ .strict(),
276
+ )
277
+ .min(1)
278
+ .optional(),
279
+ /** After navigation actions run, wait for this rendered selector before
280
+ * snapshotting the final document. */
281
+ resultSelector: z.string().min(1).optional(),
282
+ cookie: z
283
+ .object({
284
+ name: z.string().min(1),
285
+ domain: z.string().min(1).optional(),
286
+ path: z.string().min(1).optional(),
287
+ })
288
+ .optional(),
289
+ })
290
+ .optional(),
234
291
  /** Names → jsonpath expressions; later requests reference via ${response[N].name}. */
235
292
  extract: z.record(z.string()).optional(),
236
293
  captures: z.array(RequestCaptureSchema).optional(),
@@ -244,44 +301,56 @@ const WorkflowRequestSchema = z.object({
244
301
  });
245
302
  export type WorkflowRequest = z.infer<typeof WorkflowRequestSchema>;
246
303
 
247
- /** The two *structural* 2FA cases the runtime actually branches on, plus `none`.
248
- * A code that arrives out-of-band (sms / email / authenticator app) and is typed
249
- * back into a second request is one case (`otp`); an approval the user taps
250
- * elsewhere while we poll is the other (`push`). The delivery *channel* never
251
- * changes how we replay it, so we don't model it. */
252
- export const TwoFactorTypeSchema = z.enum(['otp', 'push', 'none']).default('none');
253
-
254
304
  const AuthConfigSchema = z
255
305
  .object({
256
- twoFactorType: TwoFactorTypeSchema,
257
- /** How many requests to execute for the 'initiate' phase (login + 2FA trigger).
258
- * The remaining requests run during the 'complete'/'submit_otp' phase. */
259
- initiateRequestCount: z.number().int().nonnegative().default(0),
260
- pollEndpoint: z.string().optional(),
261
- /** Push only: HTTP method for the poll request (default POST). */
262
- pollMethod: z.string().optional(),
263
- /** Push only: the request body to send on each poll attempt, templated like
264
- * any other request body (`${param.X}` / `${state.X}` / `${credential.X}`).
265
- * Some poll/status endpoints reject an empty body (e.g. require a JSON
266
- * `{mfaId,...}` payload), so the compile agent must declare the recorded
267
- * poll body here — otherwise the poll silently sends nothing and the
268
- * approved push is never recognized. Omitted → body-less poll (legacy). */
269
- pollBody: z.string().optional(),
270
- /** Push only: Content-Type for the poll body (default application/json when
271
- * pollBody is set). Grounded in the recorded poll request's header. */
272
- pollContentType: z.string().optional(),
273
- pollIntervalMs: z.number().int().positive().default(3000),
274
- maxPollAttempts: z.number().int().positive().default(60),
275
- /** Push only: a recording-grounded capture that resolves on the *approved*
276
- * poll response (and not on the pending ones). When it yields a non-empty
277
- * value the poll is done. Omitted → fall back to "a session Set-Cookie
278
- * appeared". Replaces hardcoded body-substring matching. */
279
- pollTerminal: RequestCaptureSchema.optional(),
280
- /** OTP only: the names of `${state.X}` values captured from the initiate
281
- * response that the completion (submit_otp) requests need (e.g. a reauth
282
- * `mfaId`). Because each MCP call is stateless, these are echoed back to
283
- * the caller in the AWAITING_2FA result and passed in again on submit_otp. */
284
- twoFactorContext: z.array(z.string()).default([]),
306
+ /** Recording-grounded auth program. Action names, boundaries, evidence,
307
+ * retry behavior, and completion are chosen by the compile agent. */
308
+ entry: z.string().min(1),
309
+ actions: z.record(
310
+ z.string().min(1),
311
+ z
312
+ .object({
313
+ parameters: z.array(z.string().min(1)).optional().default([]),
314
+ steps: z
315
+ .array(
316
+ z
317
+ .object({
318
+ request: z.number().int().nonnegative(),
319
+ onError: z.enum(['fail', 'continue', 'retry']).optional().default('fail'),
320
+ repeat: z
321
+ .object({
322
+ until: RequestCaptureSchema,
323
+ intervalMs: z.number().int().nonnegative(),
324
+ maxAttempts: z.number().int().positive(),
325
+ })
326
+ .strict()
327
+ .optional(),
328
+ })
329
+ .strict(),
330
+ )
331
+ .min(1),
332
+ outcome: z.discriminatedUnion('type', [
333
+ z
334
+ .object({
335
+ type: z.literal('pause'),
336
+ next: z.string().min(1),
337
+ evidence: z.array(z.string().min(1)).optional().default([]),
338
+ carry: z.array(z.string().min(1)).optional().default([]),
339
+ message: z.string().min(1),
340
+ })
341
+ .strict(),
342
+ z
343
+ .object({
344
+ type: z.literal('success'),
345
+ evidence: z.array(z.string().min(1)).optional().default([]),
346
+ })
347
+ .strict(),
348
+ ]),
349
+ })
350
+ .strict(),
351
+ ),
352
+ /** Captured state names to store as durable credentials after success. */
353
+ persist: z.array(z.string().min(1)).optional().default([]),
285
354
  /** Opt-in: when the recorded login carries its session through a CROSS-ORIGIN
286
355
  * `Set-Cookie` (e.g. a `functions.*`/`global.*` host sets a cookie that a
287
356
  * later leg depends on), set this so cdp-replay writes those cross-origin
@@ -289,15 +358,8 @@ const AuthConfigSchema = z
289
358
  * when the recording actually shows cross-origin cookie chaining; otherwise
290
359
  * the browser's normal same-origin jar is left untouched. */
291
360
  crossOriginCookieReinjection: z.boolean().default(false),
292
- /** Durable session tokens to persist after a SUCCESSFUL login completion so
293
- * DATA tools can reuse them without re-running auth (they re-auth only on
294
- * expiry / AUTH_EXPIRED). Cookies persist automatically; declare here only
295
- * the NON-cookie material a data request needs — a bearer / access / CSRF
296
- * token the completion response returns in its body or a header. Each
297
- * capture's resolved value is stored as a durable credential secret
298
- * (`${credential.NAME}`). Grounded in the recording; channel/site-agnostic. */
299
- sessionCapture: z.array(RequestCaptureSchema).default([]),
300
361
  })
362
+ .strict()
301
363
  .optional();
302
364
  export type AuthConfig = z.infer<typeof AuthConfigSchema>;
303
365
 
@@ -331,14 +393,30 @@ export const WorkflowSchema = z.object({
331
393
  *
332
394
  * Return value:
333
395
  * - `string` — the transformed URL (backward-compatible).
334
- * - `{ url: string; body?: string; headers?: Record<string, string> }` —
396
+ * - `{ url?: string; body?: string; headers?: Record<string, string>; navigation?: object; skip?: boolean }` —
335
397
  * URL plus optional body and header overrides for complex body formats
336
398
  * (JSPB, nested JSON-in-form) where placeholder substitution alone
337
- * cannot handle the encoding.
399
+ * cannot handle the encoding. `navigation` may supply dynamic browser
400
+ * navigation actions derived from resolved parameters. `skip: true` skips this request, for
401
+ * conditional follow-up requests such as pagination/detail calls.
338
402
  *
339
403
  * The optional 4th arg `params` carries the resolved workflow parameters
340
404
  * so the transform can construct request bodies programmatically. */
341
405
  requestTransformModule: z.string().optional(),
406
+ /** Agent-authored capability limitations retained with the generated tool.
407
+ * A compiler may deliberately omit a secondary feature or parameter when the
408
+ * recording and live evidence cannot support it without guessing. Runtime
409
+ * execution does not interpret these notes; MCP descriptions and teach
410
+ * summaries surface them to callers and operators. */
411
+ limitations: z
412
+ .array(
413
+ z.object({
414
+ feature: z.string().min(1),
415
+ reason: z.string().min(1),
416
+ omittedParameters: z.array(z.string().min(1)).optional(),
417
+ }),
418
+ )
419
+ .optional(),
342
420
  /** Did this tool's integration test produce live data at compile time?
343
421
  *
344
422
  * - `liveVerified: true` (default when present) — the integration test
@@ -383,12 +461,12 @@ export interface StateMissingItem {
383
461
 
384
462
  /** Discriminated union returned by every generated tool. */
385
463
  export type ToolResult<T = unknown> =
386
- | { ok: true; data: T; loginResponsePreview?: string }
464
+ | { ok: true; data: T }
387
465
  | {
388
466
  ok: false;
389
467
  error:
390
468
  | 'AUTH_EXPIRED' // 401 — run `imprint login`
391
- | 'AWAITING_2FA' // 2FA required — user must approve push / enter OTP
469
+ | 'ACTION_REQUIRED' // declared auth action completed; another action/user input is needed
392
470
  | 'FORBIDDEN' // 403 — bot detection, geo, ToS, capability mismatch
393
471
  | 'NETWORK' // fetch threw / timed out
394
472
  | 'RATE_LIMITED' // 429
@@ -402,17 +480,12 @@ export type ToolResult<T = unknown> =
402
480
  * (absent for transport/STATE_MISSING failures). Surfaced so the auth
403
481
  * compile agent sees the concrete code, not just a prose message. */
404
482
  status?: number;
405
- /** Truncated response body of the failing request (first ~500 chars).
406
- * Lets the compile agent inspect the server's actual error payload
407
- * without re-running. Distinct from `loginResponsePreview`, which is the
408
- * *initiate* response preview on the AWAITING_2FA path. */
483
+ /** Truncated response body of the failing request (first ~500 chars). */
409
484
  responseBodyPreview?: string;
410
- twoFactorType?: string;
411
- /** OTP only: the `${state.X}` values captured from the initiate response
412
- * that the caller must echo back on the submit_otp call (stateless
413
- * state-chain bridge). Names are declared in authConfig.twoFactorContext. */
414
- twoFactorContext?: Record<string, unknown>;
415
- loginResponsePreview?: string;
485
+ /** Recording-derived state and generic action progress. Echo this object
486
+ * unchanged on the next auth call. */
487
+ continuation?: Record<string, unknown>;
488
+ nextAction?: string;
416
489
  };
417
490
 
418
491
  // ─── Cron config (input to `imprint cron`) ───────────────────────────────────
@@ -610,11 +683,7 @@ const PlaybookResultSchema = z.discriminatedUnion('source', [
610
683
  ]);
611
684
  export type PlaybookResult = z.infer<typeof PlaybookResultSchema>;
612
685
 
613
- /** A named value extracted from a captured XHR response, in addition to the
614
- * success marker. Used by a login playbook to carry a 2FA-chain token (e.g. a
615
- * single-use `SecurityCode` minted in the browser during the OTP-send step)
616
- * out of the playbook run so a later stateless `submit_otp` can include it.
617
- * The `name` should match an `authConfig.twoFactorContext` entry. */
686
+ /** A named value extracted from a playbook's captured XHR response. */
618
687
  const PlaybookCaptureSchema = z.object({
619
688
  name: z.string(),
620
689
  /** Regex matched against the captured response URL. */