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
@@ -94,6 +94,74 @@ interface AuditScore {
94
94
  verdict: 'pass' | 'fail' | 'inconclusive' | 'timeout';
95
95
  }
96
96
 
97
+ interface ExpectedAuditTool {
98
+ name: string;
99
+ parameters: readonly string[];
100
+ }
101
+
102
+ interface MissingAuditedParameter {
103
+ tool: string;
104
+ name: string;
105
+ }
106
+
107
+ interface AuditEvaluation {
108
+ score: AuditScore;
109
+ missingTools: string[];
110
+ missingParameters: MissingAuditedParameter[];
111
+ }
112
+
113
+ /**
114
+ * Restrict an agent-authored report to the connected workflows before it is
115
+ * scored. Agent output is not a trusted inventory: it can repeat a tool or a
116
+ * parameter, or mention a tool that was never connected. Taking the first
117
+ * report for each expected name keeps the result deterministic and, more
118
+ * importantly, prevents repeated/unknown positive verdicts from inflating the
119
+ * acceptance score. Later duplicates remain available in the persisted raw
120
+ * report for diagnosis; they simply carry no grading weight.
121
+ */
122
+ function normalizeAuditReport(
123
+ report: AuditReport,
124
+ expectedTools: readonly ExpectedAuditTool[],
125
+ ): AuditReport {
126
+ const firstToolByName = new Map<string, AuditReport['tools'][number]>();
127
+ for (const tool of report.tools) {
128
+ if (!firstToolByName.has(tool.name)) firstToolByName.set(tool.name, tool);
129
+ }
130
+
131
+ const normalizedTools: AuditReport['tools'] = [];
132
+ const seenExpectedTools = new Set<string>();
133
+ for (const expected of expectedTools) {
134
+ if (seenExpectedTools.has(expected.name)) continue;
135
+ seenExpectedTools.add(expected.name);
136
+
137
+ const reported = firstToolByName.get(expected.name);
138
+ if (!reported) continue;
139
+
140
+ const firstParameterByName = new Map<
141
+ string,
142
+ AuditReport['tools'][number]['parameters'][number]
143
+ >();
144
+ for (const parameter of reported.parameters) {
145
+ if (!firstParameterByName.has(parameter.name)) {
146
+ firstParameterByName.set(parameter.name, parameter);
147
+ }
148
+ }
149
+
150
+ const parameters: AuditReport['tools'][number]['parameters'] = [];
151
+ const seenExpectedParameters = new Set<string>();
152
+ for (const name of expected.parameters) {
153
+ if (seenExpectedParameters.has(name)) continue;
154
+ seenExpectedParameters.add(name);
155
+ const parameter = firstParameterByName.get(name);
156
+ if (parameter) parameters.push(parameter);
157
+ }
158
+
159
+ normalizedTools.push({ ...reported, parameters });
160
+ }
161
+
162
+ return { ...report, tools: normalizedTools };
163
+ }
164
+
97
165
  /**
98
166
  * Pure, deterministic scoring over the model's verdicts.
99
167
  *
@@ -107,7 +175,7 @@ interface AuditScore {
107
175
  * - `score = 100 * correct / graded` (0 when nothing was gradeable).
108
176
  * - Verdict: no gradeable invocations → `inconclusive` (re-run / site blocked
109
177
  * us, not a code fail). Otherwise `pass` requires both `score >= minScore`
110
- * AND at least `max(2, gradeableTools)` gradeable invocations, where
178
+ * AND at least one gradeable unit per gradeable tool, where
111
179
  * `gradeableTools` is the number of tools that produced ≥1 gradeable
112
180
  * invocation. Scaling the signal floor to *gradeable* tools (not all tools)
113
181
  * means a tool the auditor can never exercise — e.g. one that needs an opaque
@@ -175,7 +243,7 @@ export function computeAuditScore(report: AuditReport, minScore: number): AuditS
175
243
  }
176
244
  const graded = correct + broken;
177
245
  const score = graded === 0 ? 0 : (100 * correct) / graded;
178
- const minGraded = Math.max(2, gradeableTools);
246
+ const minGraded = Math.max(1, gradeableTools);
179
247
  let verdict: AuditScore['verdict'];
180
248
  if (graded === 0) {
181
249
  verdict = 'inconclusive';
@@ -223,6 +291,82 @@ export function ungradeableToolNames(report: AuditReport): string[] {
223
291
  return report.tools.filter((t) => !toolHasGradeableAuditSignal(t)).map((t) => t.name);
224
292
  }
225
293
 
294
+ /** Connected tools for which the auditor reported no invocation attempt.
295
+ * Merely naming a tool (or reporting parameter verdicts) is not evidence that
296
+ * its core operation was called. This is distinct from an ungradeable tool: an
297
+ * explicit infra/bad_params invocation is useful attempt evidence, while zero
298
+ * invocation records gives us no evidence that the auditor called it at all. */
299
+ export function missingAuditedToolNames(
300
+ report: AuditReport,
301
+ expectedToolNames: readonly string[],
302
+ ): string[] {
303
+ const attemptedNames = new Set(
304
+ report.tools.filter((tool) => tool.invocations.length > 0).map((tool) => tool.name),
305
+ );
306
+ return expectedToolNames.filter((name) => !attemptedNames.has(name));
307
+ }
308
+
309
+ /** Advertised workflow parameters absent from the auditor's final report.
310
+ * Every explicit parameter verdict counts as coverage, including `untestable`:
311
+ * the latter is an honest result, while omission provides no evidence that the
312
+ * auditor considered the parameter at all. */
313
+ export function missingAuditedParameters(
314
+ report: AuditReport,
315
+ expectedTools: readonly ExpectedAuditTool[],
316
+ ): MissingAuditedParameter[] {
317
+ const reportedParameters = new Map<string, Set<string>>();
318
+ for (const tool of report.tools) {
319
+ const names = reportedParameters.get(tool.name) ?? new Set<string>();
320
+ for (const parameter of tool.parameters) names.add(parameter.name);
321
+ reportedParameters.set(tool.name, names);
322
+ }
323
+
324
+ const missing: MissingAuditedParameter[] = [];
325
+ for (const tool of expectedTools) {
326
+ const covered = reportedParameters.get(tool.name) ?? new Set<string>();
327
+ for (const name of tool.parameters) {
328
+ if (!covered.has(name)) missing.push({ tool: tool.name, name });
329
+ }
330
+ }
331
+ return missing;
332
+ }
333
+
334
+ /** Complete deterministic audit evaluation, including report coverage. A high
335
+ * score cannot pass when the auditor omitted a connected tool or advertised
336
+ * parameter; timeouts remain the highest-priority terminal verdict. */
337
+ export function evaluateAuditReport(
338
+ report: AuditReport,
339
+ minScore: number,
340
+ expectedTools: readonly ExpectedAuditTool[],
341
+ timedOut = false,
342
+ ): AuditEvaluation {
343
+ const normalizedReport = normalizeAuditReport(report, expectedTools);
344
+ const rawScore = computeAuditScore(normalizedReport, minScore);
345
+ const missingTools = missingAuditedToolNames(
346
+ normalizedReport,
347
+ expectedTools.map((tool) => tool.name),
348
+ );
349
+ const missingParameters = missingAuditedParameters(normalizedReport, expectedTools);
350
+
351
+ let verdict = rawScore.verdict;
352
+ if (timedOut) {
353
+ verdict = 'timeout';
354
+ } else if (
355
+ rawScore.verdict === 'pass' &&
356
+ (!auditHasCorrectSignal(normalizedReport) ||
357
+ missingTools.length > 0 ||
358
+ missingParameters.length > 0)
359
+ ) {
360
+ verdict = 'inconclusive';
361
+ }
362
+
363
+ return {
364
+ score: { ...rawScore, verdict },
365
+ missingTools,
366
+ missingParameters,
367
+ };
368
+ }
369
+
226
370
  /** Advertised parameters the auditor could not differentially test (opaque enum
227
371
  * with no constructible value, or a state-changing/bot-defended tool). Surfaced
228
372
  * so an unverifiable parameter is visible rather than silently passing. */
@@ -250,6 +394,17 @@ interface RunAuditOptions {
250
394
  json?: boolean;
251
395
  }
252
396
 
397
+ export function buildAuditMcpEnvironment(assetRoot = imprintHomeDir()): Record<string, string> {
398
+ return {
399
+ // Codex MCP server `env` entries are an explicit environment, so relying on
400
+ // the audit driver's inherited IMPRINT_HOME can make an isolated audit
401
+ // silently fall back to ~/.imprint and expose stale tools.
402
+ IMPRINT_HOME: assetRoot,
403
+ IMPRINT_AUDIT_PACING_MS: '5000',
404
+ IMPRINT_AUDIT_TOOL_DEADLINE_MS: '120000',
405
+ };
406
+ }
407
+
253
408
  export async function runAudit(opts: RunAuditOptions): Promise<AuditScore> {
254
409
  return await traced(
255
410
  'audit.session',
@@ -278,7 +433,11 @@ export async function runAudit(opts: RunAuditOptions): Promise<AuditScore> {
278
433
  );
279
434
  }
280
435
 
281
- const toolNames = tools.map((t) => t.workflow.toolName);
436
+ const expectedTools = tools.map((tool) => ({
437
+ name: tool.workflow.toolName,
438
+ parameters: tool.workflow.parameters.map((parameter) => parameter.name),
439
+ }));
440
+ const toolNames = expectedTools.map((tool) => tool.name);
282
441
  log(`auditing ${toolCount} tool(s) for site "${opts.site}": ${toolNames.join(', ')}`);
283
442
 
284
443
  // Parameters that shipped live-unverified at compile time (Fix D). Tell the
@@ -320,8 +479,6 @@ export async function runAudit(opts: RunAuditOptions): Promise<AuditScore> {
320
479
  tokenDeps,
321
480
  });
322
481
 
323
- const rawScore = computeAuditScore(drive.report, opts.minScore);
324
-
325
482
  // Cross-reference compile-time live verification with the audit grade.
326
483
  // The downgrade rule's purpose is to surface "flying blind" runs —
327
484
  // ones where the gate has no positive evidence the framework works
@@ -331,7 +488,7 @@ export async function runAudit(opts: RunAuditOptions): Promise<AuditScore> {
331
488
  // chained tool was unreachable from auditor's connected set).
332
489
  // v2: downgrade only if a flying-blind tool had infra invocations
333
490
  // → still over-attributed transient page-state to defects.
334
- // v3 (current): downgrade only when the audit produced ZERO
491
+ // v3: downgrade only when the audit produced ZERO
335
492
  // `correct` invocations across ALL tools. If even one
336
493
  // invocation graded correctly, that's positive evidence the
337
494
  // framework + runtime work for at least that tool — the
@@ -339,21 +496,32 @@ export async function runAudit(opts: RunAuditOptions): Promise<AuditScore> {
339
496
  // signal. Tools that couldn't be exercised still surface via
340
497
  // `ungradeableTools` / `unverifiedAndUngradeable` for visibility
341
498
  // without spoiling a verdict the score honestly earned.
499
+ // v4: retain v3's treatment of tools explicitly reported as
500
+ // blocked, but require every connected tool to appear in the final
501
+ // report. An omitted tool has no audit evidence at all, so a report
502
+ // covering only a successful subset is inconclusive rather than a
503
+ // pass.
504
+ // v5: likewise require an explicit verdict for every
505
+ // advertised workflow parameter. `untestable` is valid coverage;
506
+ // silently omitting a parameter is not.
507
+ // v6 (current): a connected tool must have at least one invocation
508
+ // record. Merely naming it in the report or discussing its
509
+ // parameters is not evidence that its core operation was attempted;
510
+ // infra and bad_params invocations still count as honest attempts.
342
511
  const ungradeableNames = ungradeableToolNames(drive.report);
512
+ const evaluation = evaluateAuditReport(
513
+ drive.report,
514
+ opts.minScore,
515
+ expectedTools,
516
+ drive.timedOut,
517
+ );
518
+ const { missingTools: missingToolNames, missingParameters } = evaluation;
343
519
  const untestableParamList = untestableParams(drive.report);
344
520
  const unverifiedAndUngradeable = tools
345
521
  .filter((t) => t.workflow.liveVerified === false)
346
522
  .map((t) => t.workflow.toolName)
347
523
  .filter((name) => ungradeableNames.includes(name));
348
- const anyCorrectAcrossAudit = auditHasCorrectSignal(drive.report);
349
- let verdict = rawScore.verdict;
350
- // Timeout takes precedence over inconclusive downgrade.
351
- if (drive.timedOut) {
352
- verdict = 'timeout';
353
- } else if (rawScore.verdict === 'pass' && !anyCorrectAcrossAudit) {
354
- verdict = 'inconclusive';
355
- }
356
- const score: AuditScore = { ...rawScore, verdict };
524
+ const score = evaluation.score;
357
525
 
358
526
  // Persist the auditor transcript next to the report so a stuck/killed run
359
527
  // can be inspected after the fact.
@@ -392,6 +560,8 @@ export async function runAudit(opts: RunAuditOptions): Promise<AuditScore> {
392
560
  'imprint.audit.tool_count': toolCount,
393
561
  'imprint.audit.verdict': score.verdict,
394
562
  'imprint.audit.unverified_and_ungradeable_count': unverifiedAndUngradeable.length,
563
+ 'imprint.audit.missing_tool_count': missingToolNames.length,
564
+ 'imprint.audit.missing_parameter_count': missingParameters.length,
395
565
  'imprint.audit.timed_out': drive.timedOut,
396
566
  'imprint.audit.turns': drive.turns,
397
567
  ...(drive.totalCostUsd != null ? { 'imprint.audit.cost_usd': drive.totalCostUsd } : {}),
@@ -414,6 +584,11 @@ export async function runAudit(opts: RunAuditOptions): Promise<AuditScore> {
414
584
  site: opts.site,
415
585
  toolCount,
416
586
  ungradeableTools: ungradeableNames,
587
+ /** Connected tools without an invocation record. A partial audit
588
+ * cannot pass merely because the auditor named an uncalled tool. */
589
+ missingTools: missingToolNames,
590
+ /** Advertised workflow parameters absent from the auditor's report. */
591
+ missingParameters,
417
592
  /** Advertised parameters the auditor could not differentially test. */
418
593
  untestableParams: untestableParamList,
419
594
  /** Tools that shipped without live verification at compile time AND
@@ -445,6 +620,8 @@ export async function runAudit(opts: RunAuditOptions): Promise<AuditScore> {
445
620
  transcriptPath,
446
621
  costUsd: drive.totalCostUsd,
447
622
  unverifiedAndUngradeable,
623
+ missingTools: missingToolNames,
624
+ missingParameters,
448
625
  report: drive.report,
449
626
  });
450
627
  }
@@ -653,6 +830,7 @@ async function driveAuditWithCodex(opts: DriveAuditOptions): Promise<DriveAuditR
653
830
  const serverName = `imprint-audit-${opts.site}`;
654
831
  const bunPath = process.execPath;
655
832
  const mcpArgs = ['run', CLI_PATH, 'mcp-server', opts.site];
833
+ const mcpEnv = buildAuditMcpEnvironment();
656
834
 
657
835
  const unverifiedNote = buildUnverifiedParamNote(opts.unverifiedParams);
658
836
  const initialPrompt = `<system_instructions>
@@ -685,9 +863,11 @@ ${buildAuditInitialPrompt(opts, unverifiedNote)}`;
685
863
  '-c',
686
864
  `mcp_servers.${serverName}.tool_timeout_sec=300`,
687
865
  '-c',
688
- `mcp_servers.${serverName}.env.IMPRINT_AUDIT_PACING_MS=${JSON.stringify('5000')}`,
866
+ `mcp_servers.${serverName}.env.IMPRINT_HOME=${JSON.stringify(mcpEnv.IMPRINT_HOME)}`,
689
867
  '-c',
690
- `mcp_servers.${serverName}.env.IMPRINT_AUDIT_TOOL_DEADLINE_MS=${JSON.stringify('120000')}`,
868
+ `mcp_servers.${serverName}.env.IMPRINT_AUDIT_PACING_MS=${JSON.stringify(mcpEnv.IMPRINT_AUDIT_PACING_MS)}`,
869
+ '-c',
870
+ `mcp_servers.${serverName}.env.IMPRINT_AUDIT_TOOL_DEADLINE_MS=${JSON.stringify(mcpEnv.IMPRINT_AUDIT_TOOL_DEADLINE_MS)}`,
691
871
  '-c',
692
872
  'shell_environment_policy.inherit=all',
693
873
  '-',
@@ -701,8 +881,7 @@ ${buildAuditInitialPrompt(opts, unverifiedNote)}`;
701
881
  cwd: REPO_ROOT,
702
882
  env: {
703
883
  ...process.env,
704
- IMPRINT_AUDIT_PACING_MS: '5000',
705
- IMPRINT_AUDIT_TOOL_DEADLINE_MS: '120000',
884
+ ...mcpEnv,
706
885
  },
707
886
  stdio: ['pipe', 'pipe', 'pipe'],
708
887
  });
@@ -1209,6 +1388,8 @@ function printSummary(
1209
1388
  transcriptPath?: string;
1210
1389
  costUsd?: number | null;
1211
1390
  unverifiedAndUngradeable: string[];
1391
+ missingTools: string[];
1392
+ missingParameters: MissingAuditedParameter[];
1212
1393
  report: AuditReport;
1213
1394
  },
1214
1395
  ): void {
@@ -1255,12 +1436,30 @@ function printSummary(
1255
1436
  `[imprint] ${extra.unverifiedAndUngradeable.length} tool(s) flying blind (no live verification at compile, no graded calls at audit): ${extra.unverifiedAndUngradeable.join(', ')}`,
1256
1437
  );
1257
1438
  }
1439
+ if (extra.missingTools.length > 0) {
1440
+ console.log(
1441
+ `[imprint] ${extra.missingTools.length} connected tool(s) have no invocation attempt in the auditor report: ${extra.missingTools.join(', ')}`,
1442
+ );
1443
+ }
1444
+ if (extra.missingParameters.length > 0) {
1445
+ console.log(
1446
+ `[imprint] ${extra.missingParameters.length} advertised parameter(s) missing from the auditor report: ${extra.missingParameters.map((parameter) => `${parameter.tool}.${parameter.name}`).join(', ')}`,
1447
+ );
1448
+ }
1258
1449
  if (score.verdict === 'timeout') {
1259
1450
  console.log(
1260
1451
  `[imprint] audit was killed at the ${formatDeadline(extra.timeoutMs)} deadline before finishing — partial results only. Re-run with a longer --timeout, or inspect the transcript to see where it stalled.`,
1261
1452
  );
1262
1453
  } else if (score.verdict === 'inconclusive') {
1263
- if (extra.unverifiedAndUngradeable.length > 0) {
1454
+ if (extra.missingTools.length > 0) {
1455
+ console.log(
1456
+ '[imprint] verdict downgraded to inconclusive because the auditor did not record an invocation attempt for every connected tool.',
1457
+ );
1458
+ } else if (extra.missingParameters.length > 0) {
1459
+ console.log(
1460
+ '[imprint] verdict downgraded to inconclusive because the auditor did not report every advertised parameter.',
1461
+ );
1462
+ } else if (extra.unverifiedAndUngradeable.length > 0) {
1264
1463
  console.log(
1265
1464
  '[imprint] verdict downgraded to inconclusive because at least one tool has zero live signal anywhere.',
1266
1465
  );