imprint-mcp 0.6.1 → 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.
- package/README.md +2 -0
- package/package.json +3 -2
- package/prompts/audit-agent.md +4 -2
- package/prompts/compile-agent.md +46 -23
- package/prompts/live-verifier-agent.md +101 -0
- package/prompts/playbook-compilation.md +8 -1
- package/src/cli.ts +83 -1
- package/src/imprint/audit.ts +219 -20
- package/src/imprint/backend-cache.ts +18 -11
- package/src/imprint/backend-ladder.ts +48 -34
- package/src/imprint/cdp-browser-fetch.ts +135 -42
- package/src/imprint/claude-cli-compile.ts +38 -16
- package/src/imprint/codex-cli-compile.ts +120 -21
- package/src/imprint/compile-agent.ts +71 -26
- package/src/imprint/compile-tools.ts +169 -38
- package/src/imprint/compile-verification.ts +187 -0
- package/src/imprint/compile.ts +71 -2
- package/src/imprint/freeform-redact.ts +27 -6
- package/src/imprint/live-verifier.ts +1665 -0
- package/src/imprint/llm.ts +16 -0
- package/src/imprint/mcp-compile-server.ts +380 -81
- package/src/imprint/mcp-live-verifier-server.ts +359 -0
- package/src/imprint/mcp-server.ts +12 -2
- package/src/imprint/probe-backends.ts +88 -7
- package/src/imprint/runtime.ts +61 -11
- package/src/imprint/teach-state.ts +18 -1
- package/src/imprint/teach.ts +77 -19
- package/src/imprint/types.ts +39 -2
|
@@ -28,6 +28,11 @@ import {
|
|
|
28
28
|
externalVerification,
|
|
29
29
|
} from './compile-tools.ts';
|
|
30
30
|
import { type Replacement, extractCredentials } from './credential-extract.ts';
|
|
31
|
+
import {
|
|
32
|
+
mergeSemanticParamVerification,
|
|
33
|
+
runLiveSemanticVerification,
|
|
34
|
+
semanticVerificationFailures,
|
|
35
|
+
} from './live-verifier.ts';
|
|
31
36
|
import {
|
|
32
37
|
type LLMOptions,
|
|
33
38
|
type ProviderName,
|
|
@@ -39,6 +44,7 @@ import {
|
|
|
39
44
|
import { loadJsonFile } from './load-json.ts';
|
|
40
45
|
import { createLog } from './log.ts';
|
|
41
46
|
import { localSiteDir } from './paths.ts';
|
|
47
|
+
import { rebindExistingBackendsCacheToWorkflow } from './probe-backends.ts';
|
|
42
48
|
import { detectPageMintedHeaders, redactSession } from './redact.ts';
|
|
43
49
|
import type { ClassifiedValue } from './session-diff.ts';
|
|
44
50
|
import type { SharedCompileContext, ToolCandidate } from './tool-candidates.ts';
|
|
@@ -114,6 +120,19 @@ interface CompileAgentOptions {
|
|
|
114
120
|
* response parsing, shared-module imports). Injected into the agent's initial
|
|
115
121
|
* message so the compile follows it. Generic — not tied to any site. */
|
|
116
122
|
toolPlan?: string;
|
|
123
|
+
/** Revise an existing generated artifact, using durable verification feedback
|
|
124
|
+
* as the starting point instead of rebuilding it from raw capture. */
|
|
125
|
+
revisionMode?: boolean;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function formatRevisionMode(enabled: boolean | undefined): string {
|
|
129
|
+
return enabled
|
|
130
|
+
? [
|
|
131
|
+
'REVISION MODE: this is a bounded resume of an existing generated tool, not a from-scratch compile.',
|
|
132
|
+
'Use read_session_summary.revisionContext, then read the listed current artifacts and durable verification feedback before inspecting raw response bodies.',
|
|
133
|
+
'Preserve proven behavior and make the smallest evidence-backed repair or contract reduction.',
|
|
134
|
+
].join(' ')
|
|
135
|
+
: '';
|
|
117
136
|
}
|
|
118
137
|
|
|
119
138
|
export async function compileAgent(opts: CompileAgentOptions): Promise<CompileAgentResult> {
|
|
@@ -216,6 +235,7 @@ export async function compileAgent(opts: CompileAgentOptions): Promise<CompileAg
|
|
|
216
235
|
teachCredentials: opts.teachCredentials,
|
|
217
236
|
buildPlanPath: opts.buildPlanPath,
|
|
218
237
|
sharedModules: opts.sharedModules,
|
|
238
|
+
revisionMode: opts.revisionMode,
|
|
219
239
|
}),
|
|
220
240
|
doneTool(),
|
|
221
241
|
giveUpTool(),
|
|
@@ -229,11 +249,12 @@ Tool directory: ${absoluteToolDir}
|
|
|
229
249
|
You will write artifacts into the tool directory.
|
|
230
250
|
${formatCandidateContext(opts.candidate, opts.sharedContext, assignedSharedModules)}
|
|
231
251
|
${formatToolPlan(opts.toolPlan)}
|
|
252
|
+
${formatRevisionMode(opts.revisionMode)}
|
|
232
253
|
|
|
233
254
|
Begin by calling read_session_summary to orient yourself, then proceed per the system prompt.`;
|
|
234
255
|
|
|
235
256
|
// 7. Compute deadline
|
|
236
|
-
|
|
257
|
+
let deadlineMs = Date.now() + (opts.maxDurationMs ?? 20 * 60 * 1000);
|
|
237
258
|
|
|
238
259
|
// 8. Instantiate provider (or use injected one for testing).
|
|
239
260
|
// CLI providers take a different path: they don't implement Anthropic
|
|
@@ -260,6 +281,7 @@ Begin by calling read_session_summary to orient yourself, then proceed per the s
|
|
|
260
281
|
buildPlanPath: opts.buildPlanPath,
|
|
261
282
|
sharedModules: opts.sharedModules,
|
|
262
283
|
toolPlan: opts.toolPlan,
|
|
284
|
+
revisionMode: opts.revisionMode,
|
|
263
285
|
model: opts.llmConfig?.model,
|
|
264
286
|
});
|
|
265
287
|
}
|
|
@@ -278,6 +300,7 @@ Begin by calling read_session_summary to orient yourself, then proceed per the s
|
|
|
278
300
|
buildPlanPath: opts.buildPlanPath,
|
|
279
301
|
sharedModules: opts.sharedModules,
|
|
280
302
|
toolPlan: opts.toolPlan,
|
|
303
|
+
revisionMode: opts.revisionMode,
|
|
281
304
|
model: opts.llmConfig?.model,
|
|
282
305
|
});
|
|
283
306
|
}
|
|
@@ -351,11 +374,9 @@ Begin by calling read_session_summary to orient yourself, then proceed per the s
|
|
|
351
374
|
}
|
|
352
375
|
|
|
353
376
|
// Perform external verification
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
session,
|
|
357
|
-
sessionPathAbs,
|
|
358
|
-
{
|
|
377
|
+
const deterministicVerificationStartedAt = Date.now();
|
|
378
|
+
const { failures, warnings, paramVerification, integrationEvidence } =
|
|
379
|
+
await externalVerification(absoluteToolDir, session, sessionPathAbs, {
|
|
359
380
|
expectedToolName: opts.candidate?.toolName,
|
|
360
381
|
likelyParams: opts.candidate?.likelyParams,
|
|
361
382
|
candidateRequestSeqs: opts.candidate?.requestSeqs,
|
|
@@ -370,35 +391,58 @@ Begin by calling read_session_summary to orient yourself, then proceed per the s
|
|
|
370
391
|
requiredInputs,
|
|
371
392
|
credentialValues: opts.teachCredentials?.values,
|
|
372
393
|
credentialNames: opts.sharedContext?.credentialNames,
|
|
373
|
-
|
|
374
|
-
|
|
394
|
+
deferLiveIntegrationToSemanticAgent: true,
|
|
395
|
+
});
|
|
396
|
+
deadlineMs += Date.now() - deterministicVerificationStartedAt;
|
|
375
397
|
|
|
376
398
|
if (warnings.length > 0) {
|
|
377
399
|
log(`verification warnings (non-blocking):\n${warnings.join('\n')}`);
|
|
378
400
|
}
|
|
379
401
|
|
|
380
402
|
if (failures.length === 0) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
403
|
+
const semanticVerificationStartedAt = Date.now();
|
|
404
|
+
const semantic = await runLiveSemanticVerification({
|
|
405
|
+
provider: provider.name,
|
|
406
|
+
toolDir: absoluteToolDir,
|
|
407
|
+
evidence: integrationEvidence,
|
|
408
|
+
// Live semantic verification is a distinct phase. Do not inherit a
|
|
409
|
+
// nearly exhausted compile-agent deadline.
|
|
410
|
+
});
|
|
411
|
+
deadlineMs += Date.now() - semanticVerificationStartedAt;
|
|
412
|
+
failures.push(...semanticVerificationFailures(semantic.report));
|
|
413
|
+
if (semantic.report.status === 'approved_with_gaps') {
|
|
414
|
+
warnings.push(
|
|
415
|
+
`independent semantic verification approved with gaps: ${semantic.report.gaps.join('; ')}`,
|
|
416
|
+
);
|
|
394
417
|
}
|
|
395
|
-
if (
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
418
|
+
if (failures.length === 0) {
|
|
419
|
+
// Success (possibly with explicit semantic gaps). Persist per-parameter
|
|
420
|
+
// flags and mint liveVerified=true only after independent approval.
|
|
421
|
+
applyLiveVerification(absoluteToolDir, undefined);
|
|
422
|
+
const paramWarnings = applyParamVerification(
|
|
423
|
+
absoluteToolDir,
|
|
424
|
+
mergeSemanticParamVerification(paramVerification, semantic.report),
|
|
425
|
+
);
|
|
426
|
+
// liveVerified/parameter annotations change the hash-strict cache key,
|
|
427
|
+
// but not transport capabilities. Rebind the backend already exercised
|
|
428
|
+
// by the verifier instead of triggering another probe.
|
|
429
|
+
rebindExistingBackendsCacheToWorkflow(absoluteToolDir);
|
|
430
|
+
const allWarnings = [...warnings, ...paramWarnings];
|
|
431
|
+
if (paramWarnings.length > 0) {
|
|
432
|
+
log(`parameter verification:\n${paramWarnings.join('\n')}`);
|
|
433
|
+
}
|
|
434
|
+
message = result.doneSummary ?? 'Task completed';
|
|
435
|
+
if (allWarnings.length > 0) {
|
|
436
|
+
message += `\n\nWarnings:\n${allWarnings.join('\n')}`;
|
|
399
437
|
}
|
|
438
|
+
if (!opts.keepTest) {
|
|
439
|
+
for (const f of ['parser.test.ts', 'integration.test.ts']) {
|
|
440
|
+
const testPath = pathJoin(absoluteToolDir, f);
|
|
441
|
+
if (existsSync(testPath)) unlinkSync(testPath);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
break;
|
|
400
445
|
}
|
|
401
|
-
break;
|
|
402
446
|
}
|
|
403
447
|
|
|
404
448
|
// Verification failed — re-enter the loop with a continuation message
|
|
@@ -414,6 +458,7 @@ Begin by calling read_session_summary to orient yourself, then proceed per the s
|
|
|
414
458
|
${failures.map((f) => `- ${f}`).join('\n')}
|
|
415
459
|
|
|
416
460
|
Resume your work. Read the files you wrote (workflow.json, parser.ts, parser.test.ts), fix the issues, re-run tests, and call done again when fixed.`;
|
|
461
|
+
currentInitialMessage += `\n\nUse the verifier's semantic evidence, not a requirement for 100% of the original candidate inputs. If the core intent works but a secondary parameter cannot be supported from the recording and live evidence without guessing, remove that parameter and add workflow.limitations with omittedParameters and a specific reason. Do not use a limitation to hide a broken core tool, and do not keep a known broken or ignored public input.`;
|
|
417
462
|
}
|
|
418
463
|
|
|
419
464
|
// 10. Final flush of the complete conversation log
|
|
@@ -11,7 +11,9 @@ import {
|
|
|
11
11
|
existsSync,
|
|
12
12
|
mkdirSync,
|
|
13
13
|
readFileSync,
|
|
14
|
+
readdirSync,
|
|
14
15
|
realpathSync,
|
|
16
|
+
statSync,
|
|
15
17
|
unlinkSync,
|
|
16
18
|
writeFileSync,
|
|
17
19
|
} from 'node:fs';
|
|
@@ -27,6 +29,11 @@ import {
|
|
|
27
29
|
readBuildPlanFile,
|
|
28
30
|
resolveAssignedModules,
|
|
29
31
|
} from './build-plan.ts';
|
|
32
|
+
import {
|
|
33
|
+
LIVE_EVIDENCE_PATH_ENV,
|
|
34
|
+
type LiveIntegrationEvidence,
|
|
35
|
+
readLiveIntegrationEvidence,
|
|
36
|
+
} from './compile-verification.ts';
|
|
30
37
|
import { splitSetCookieHeader } from './cookie-jar.ts';
|
|
31
38
|
import { isSameRegistrableDomain, registrableDomain } from './etld.ts';
|
|
32
39
|
import {
|
|
@@ -80,7 +87,7 @@ export function buildCompileTools(
|
|
|
80
87
|
? { IMPRINT_TEACH_CREDENTIALS: JSON.stringify(context.teachCredentials) }
|
|
81
88
|
: undefined;
|
|
82
89
|
const tools = [
|
|
83
|
-
buildReadSessionSummaryTool(session, context),
|
|
90
|
+
buildReadSessionSummaryTool(session, context, toolDir),
|
|
84
91
|
buildReadRequestTool(session),
|
|
85
92
|
buildSearchRequestsTool(session),
|
|
86
93
|
buildRevealRequestTool(sessionPath),
|
|
@@ -115,6 +122,10 @@ export interface CompileToolContext {
|
|
|
115
122
|
buildPlanPath?: string;
|
|
116
123
|
/** Shared-module build manifest (verified flags) for this site. */
|
|
117
124
|
sharedModules?: SharedModuleManifestEntry[];
|
|
125
|
+
/** A bounded teach resume is revising an already-generated tool. Surface the
|
|
126
|
+
* existing artifact and durable verifier/audit feedback to the agent so it
|
|
127
|
+
* preserves proven behavior instead of re-deriving the tool from raw capture. */
|
|
128
|
+
revisionMode?: boolean;
|
|
118
129
|
}
|
|
119
130
|
|
|
120
131
|
// ─── Tool: read_build_plan ───────────────────────────────────────────────────
|
|
@@ -164,7 +175,7 @@ function buildReadBuildPlanTool(
|
|
|
164
175
|
? ` The producer declares shape: ${producerShape}. If that shape contains requiredKeys=[...], request-transform.ts must decode/unpack the fresh value and reference each required key in the outgoing request.`
|
|
165
176
|
: '';
|
|
166
177
|
tokenNotes.push(
|
|
167
|
-
`CONSUMER CONTRACT: param \`${tp.param}\` is an opaque token minted by the \`${tp.sourceTool}\` tool's \`${tp.sourceField}\` output.${shapeNote} Write a CHAINED \`param:${tp.param}\` integration test that calls \`
|
|
178
|
+
`CONSUMER CONTRACT: param \`${tp.param}\` is an opaque token minted by the \`${tp.sourceTool}\` tool's \`${tp.sourceField}\` output.${shapeNote} Write a CHAINED \`param:${tp.param}\` integration test that calls \`runCapturedIntegrationCase\` on \`../${tp.sourceTool}/workflow.json\` directly inside the \`param:${tp.param}\` test block, reads \`${tp.sourceField}\` from the producer's ACTUAL output shape (top-level field, or an item in any returned collection such as flights/items/results), and passes THAT fresh value (not the recorded constant) into this tool — then asserts the response is non-empty. When several params come from the same producer, mint once and choose one producer item containing all sibling fields so the values stay from the same result. On producer bot/infra error, rethrow so the suite waives; if the producer ran but the field is absent, fix the producer parser/output contract instead of waiving. Do not fabricate a fallback value or mutate producer.result.data to make the test pass.`,
|
|
168
179
|
);
|
|
169
180
|
}
|
|
170
181
|
// Per-source wiring guidance for the general dependency contract. These are
|
|
@@ -232,6 +243,7 @@ function buildReadBuildPlanTool(
|
|
|
232
243
|
export function buildReadSessionSummaryTool(
|
|
233
244
|
session: Session,
|
|
234
245
|
context: CompileToolContext,
|
|
246
|
+
toolDir?: string,
|
|
235
247
|
): AgentTool {
|
|
236
248
|
return {
|
|
237
249
|
name: 'read_session_summary',
|
|
@@ -343,6 +355,10 @@ export function buildReadSessionSummaryTool(
|
|
|
343
355
|
captureHints: captureHints.length > 0 ? captureHints : undefined,
|
|
344
356
|
paramGroundingHints: paramGroundingHints.length > 0 ? paramGroundingHints : undefined,
|
|
345
357
|
inputProvenanceHints: inputProvenanceHints.length > 0 ? inputProvenanceHints : undefined,
|
|
358
|
+
revisionContext:
|
|
359
|
+
context.revisionMode && toolDir
|
|
360
|
+
? buildExistingArtifactRevisionContext(toolDir)
|
|
361
|
+
: undefined,
|
|
346
362
|
loadBearingRequests,
|
|
347
363
|
};
|
|
348
364
|
|
|
@@ -361,6 +377,65 @@ export function buildReadSessionSummaryTool(
|
|
|
361
377
|
};
|
|
362
378
|
}
|
|
363
379
|
|
|
380
|
+
const REVISION_ARTIFACT_NAMES = [
|
|
381
|
+
'workflow.json',
|
|
382
|
+
'parser.ts',
|
|
383
|
+
'request-transform.ts',
|
|
384
|
+
'integration.test.ts',
|
|
385
|
+
'playbook.yaml',
|
|
386
|
+
] as const;
|
|
387
|
+
|
|
388
|
+
const REVISION_DIAGNOSTIC_NAMES = [
|
|
389
|
+
'.live-verification.json',
|
|
390
|
+
'.live-verification-evidence.json',
|
|
391
|
+
'.live-verifier-log.jsonl',
|
|
392
|
+
] as const;
|
|
393
|
+
|
|
394
|
+
/** Describe, but do not interpret or gate on, prior compile evidence. The
|
|
395
|
+
* compile agent owns the judgment: it can read any listed file through the
|
|
396
|
+
* existing read_file tool and decide what to preserve, repair, narrow, or
|
|
397
|
+
* document. This is intentionally agent context rather than another runtime
|
|
398
|
+
* validator. */
|
|
399
|
+
function buildExistingArtifactRevisionContext(toolDir: string): Record<string, unknown> {
|
|
400
|
+
const describe = (name: string): { path: string; bytes: number } | null => {
|
|
401
|
+
const absolute = pathJoin(toolDir, name);
|
|
402
|
+
if (!existsSync(absolute)) return null;
|
|
403
|
+
try {
|
|
404
|
+
return { path: name, bytes: statSync(absolute).size };
|
|
405
|
+
} catch {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const artifacts = REVISION_ARTIFACT_NAMES.map(describe).filter(
|
|
411
|
+
(entry): entry is { path: string; bytes: number } => entry !== null,
|
|
412
|
+
);
|
|
413
|
+
const diagnostics = REVISION_DIAGNOSTIC_NAMES.map(describe).filter(
|
|
414
|
+
(entry): entry is { path: string; bytes: number } => entry !== null,
|
|
415
|
+
);
|
|
416
|
+
const notesDir = pathJoin(toolDir, 'notes');
|
|
417
|
+
const feedbackNotes = existsSync(notesDir)
|
|
418
|
+
? readdirSync(notesDir)
|
|
419
|
+
.filter((name) => name.endsWith('.md'))
|
|
420
|
+
.sort()
|
|
421
|
+
.map((name) => describe(pathJoin('notes', name)))
|
|
422
|
+
.filter((entry): entry is { path: string; bytes: number } => entry !== null)
|
|
423
|
+
: [];
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
mode: 'revise_existing_artifact',
|
|
427
|
+
instruction: [
|
|
428
|
+
'This bounded resume is a revision, not a from-scratch compile.',
|
|
429
|
+
'Read the existing workflow, parser, request transform, integration test, durable live-verification diagnostics, and feedback notes before re-reading raw recording bodies.',
|
|
430
|
+
'Preserve behavior that live evidence proves works. Repair or narrow only the behavior contradicted by evidence.',
|
|
431
|
+
'If a secondary branch cannot be grounded, remove its dependent parameters and claims together and record the reasoning in workflow limitations; do not guess.',
|
|
432
|
+
].join(' '),
|
|
433
|
+
existingArtifacts: artifacts,
|
|
434
|
+
durableDiagnostics: diagnostics,
|
|
435
|
+
feedbackNotes,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
364
439
|
// ─── Inline request/response data for candidate-scoped requests ─────────────
|
|
365
440
|
|
|
366
441
|
// claude-cli truncates tool results > ~40K chars. Keep the total summary
|
|
@@ -1345,6 +1420,7 @@ export async function runCommand(
|
|
|
1345
1420
|
cwd: string,
|
|
1346
1421
|
timeoutMs: number,
|
|
1347
1422
|
extraEnv?: Record<string, string>,
|
|
1423
|
+
onOutput?: (stream: 'stdout' | 'stderr', chunk: string) => void,
|
|
1348
1424
|
): Promise<{ result: string; isError?: boolean }> {
|
|
1349
1425
|
return new Promise((resolve) => {
|
|
1350
1426
|
// `detached: true` makes the child its own process-group leader so a timeout
|
|
@@ -1362,11 +1438,15 @@ export async function runCommand(
|
|
|
1362
1438
|
const TRUNCATE_LIMIT = 16 * 1024; // 16KB
|
|
1363
1439
|
|
|
1364
1440
|
proc.stdout.on('data', (chunk) => {
|
|
1365
|
-
|
|
1441
|
+
const text = chunk.toString();
|
|
1442
|
+
stdout += text;
|
|
1443
|
+
onOutput?.('stdout', text);
|
|
1366
1444
|
});
|
|
1367
1445
|
|
|
1368
1446
|
proc.stderr.on('data', (chunk) => {
|
|
1369
|
-
|
|
1447
|
+
const text = chunk.toString();
|
|
1448
|
+
stderr += text;
|
|
1449
|
+
onOutput?.('stderr', text);
|
|
1370
1450
|
});
|
|
1371
1451
|
|
|
1372
1452
|
const timeout = setTimeout(() => {
|
|
@@ -1716,7 +1796,7 @@ interface BunTestRun {
|
|
|
1716
1796
|
|
|
1717
1797
|
/** Per-exposed-parameter verification outcome. `verified` is true only when a
|
|
1718
1798
|
* `param:<name>` integration test actually ran green against live data. */
|
|
1719
|
-
interface ParamVerification {
|
|
1799
|
+
export interface ParamVerification {
|
|
1720
1800
|
name: string;
|
|
1721
1801
|
verified: boolean;
|
|
1722
1802
|
/** Why an exposed param is unverified. Undefined when `verified` is true.
|
|
@@ -1727,7 +1807,7 @@ interface ParamVerification {
|
|
|
1727
1807
|
* - `waived-chain`: the param is a producer-sourced token but the producer
|
|
1728
1808
|
* tool could not be run at compile time (anti-bot / not compiled), so the
|
|
1729
1809
|
* chain could not be verified. */
|
|
1730
|
-
reason?: 'waived-bot' | 'waived-infra' | 'annotated' | 'waived-chain';
|
|
1810
|
+
reason?: 'waived-bot' | 'waived-infra' | 'annotated' | 'waived-chain' | 'semantic-gap';
|
|
1731
1811
|
/** For a producer-sourced token param, the sibling tool + output field its
|
|
1732
1812
|
* value comes from. Stamped into workflow.json (`param.sourcedFrom`) so the
|
|
1733
1813
|
* MCP description tells the orchestrating LLM where to mint it and the audit
|
|
@@ -1751,11 +1831,15 @@ interface TokenSource {
|
|
|
1751
1831
|
* bot-defense / infra detection and error surfacing) and the per-test pass/fail
|
|
1752
1832
|
* names via a JUnit report written to a transient file in the tool dir.
|
|
1753
1833
|
*/
|
|
1754
|
-
async function runBunTestWithResults(
|
|
1834
|
+
export async function runBunTestWithResults(
|
|
1755
1835
|
testPath: string,
|
|
1756
1836
|
toolDir: string,
|
|
1757
1837
|
timeoutMs: number,
|
|
1758
1838
|
env: Record<string, string> = {},
|
|
1839
|
+
opts: {
|
|
1840
|
+
bail?: boolean;
|
|
1841
|
+
onOutput?: (stream: 'stdout' | 'stderr', chunk: string) => void;
|
|
1842
|
+
} = {},
|
|
1759
1843
|
): Promise<BunTestRun> {
|
|
1760
1844
|
const junitPath = pathJoin(toolDir, `.imprint-junit-${basename(testPath)}.xml`);
|
|
1761
1845
|
try {
|
|
@@ -1764,10 +1848,11 @@ async function runBunTestWithResults(
|
|
|
1764
1848
|
// best-effort
|
|
1765
1849
|
}
|
|
1766
1850
|
const result = await runCommand(
|
|
1767
|
-
`bun test ${testPath} --reporter=junit --reporter-outfile=${junitPath}`,
|
|
1851
|
+
`bun test ${testPath} --reporter=junit --reporter-outfile=${junitPath}${opts.bail ? ' --bail=1' : ''}`,
|
|
1768
1852
|
toolDir,
|
|
1769
1853
|
timeoutMs,
|
|
1770
1854
|
env,
|
|
1855
|
+
opts.onOutput,
|
|
1771
1856
|
);
|
|
1772
1857
|
const output = JSON.parse(result.result) as {
|
|
1773
1858
|
stdout: string;
|
|
@@ -1802,6 +1887,10 @@ interface TestBlock {
|
|
|
1802
1887
|
body: string;
|
|
1803
1888
|
}
|
|
1804
1889
|
|
|
1890
|
+
function hasCapturedLiveWorkflowCall(source: string): boolean {
|
|
1891
|
+
return /\b(?:runWorkflowWithLadder|runCapturedIntegrationCase)\s*\(/.test(source);
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1805
1894
|
/** Split a test file into `test(...)` / `it(...)` blocks (title + source from
|
|
1806
1895
|
* that test's start to the next test's start). Good enough to check whether a
|
|
1807
1896
|
* named per-parameter test's body actually calls the workflow. */
|
|
@@ -2241,7 +2330,7 @@ export function classifyParamCoverage(opts: {
|
|
|
2241
2330
|
if (passedLive) {
|
|
2242
2331
|
const chained =
|
|
2243
2332
|
!!block &&
|
|
2244
|
-
|
|
2333
|
+
hasCapturedLiveWorkflowCall(block.body) &&
|
|
2245
2334
|
SIBLING_WORKFLOW_RE.test(block.body);
|
|
2246
2335
|
if (chained) {
|
|
2247
2336
|
paramVerification.push({ name: lp.name, verified: true, sourcedFrom });
|
|
@@ -2269,7 +2358,7 @@ export function classifyParamCoverage(opts: {
|
|
|
2269
2358
|
if (passedLive) {
|
|
2270
2359
|
// Anti-tautology: a passing per-param test must actually exercise the live
|
|
2271
2360
|
// workflow, not assert a constant.
|
|
2272
|
-
if (block &&
|
|
2361
|
+
if (block && !hasCapturedLiveWorkflowCall(block.body)) {
|
|
2273
2362
|
tautological.push(lp.name);
|
|
2274
2363
|
} else {
|
|
2275
2364
|
paramVerification.push({ name: lp.name, verified: true });
|
|
@@ -2302,7 +2391,7 @@ export function hasLiveBaselineWorkflowTest(integrationSrc: string): boolean {
|
|
|
2302
2391
|
return extractTestBlocks(integrationSrc).some(
|
|
2303
2392
|
(block) =>
|
|
2304
2393
|
!block.title.includes('param:') &&
|
|
2305
|
-
|
|
2394
|
+
hasCapturedLiveWorkflowCall(block.body) &&
|
|
2306
2395
|
!returnsOnFailedResult.test(block.body) &&
|
|
2307
2396
|
(requiresOk.test(block.body) || throwsOnFailure.test(block.body)),
|
|
2308
2397
|
);
|
|
@@ -3427,6 +3516,10 @@ export async function externalVerification(
|
|
|
3427
3516
|
* verifier blocks invented ${credential.X} placeholders that are not in this
|
|
3428
3517
|
* set or in requiredInputs. */
|
|
3429
3518
|
credentialNames?: string[];
|
|
3519
|
+
/** Agentic compile path only: validate the integration suite's source here,
|
|
3520
|
+
* but let the independent semantic verifier invoke the final live suite as
|
|
3521
|
+
* a tool. Parser/type/schema checks remain deterministic. */
|
|
3522
|
+
deferLiveIntegrationToSemanticAgent?: boolean;
|
|
3430
3523
|
} = {},
|
|
3431
3524
|
): Promise<{
|
|
3432
3525
|
failures: string[];
|
|
@@ -3440,9 +3533,13 @@ export async function externalVerification(
|
|
|
3440
3533
|
firstError: string;
|
|
3441
3534
|
exhaustedBackends: string[];
|
|
3442
3535
|
};
|
|
3536
|
+
/** Actual tool-level inputs and parsed results captured by the single live
|
|
3537
|
+
* integration run. The caller hands these to the independent semantic agent. */
|
|
3538
|
+
integrationEvidence: LiveIntegrationEvidence[];
|
|
3443
3539
|
}> {
|
|
3444
3540
|
const failures: string[] = [];
|
|
3445
3541
|
const warnings: string[] = [];
|
|
3542
|
+
let integrationEvidence: LiveIntegrationEvidence[] = [];
|
|
3446
3543
|
const paramVerification: ParamVerification[] = [];
|
|
3447
3544
|
let liveVerification:
|
|
3448
3545
|
| { kind: 'waived-bot' | 'waived-infra'; firstError: string; exhaustedBackends: string[] }
|
|
@@ -3647,6 +3744,7 @@ export async function externalVerification(
|
|
|
3647
3744
|
|
|
3648
3745
|
const notTemplated: string[] = [];
|
|
3649
3746
|
const inventedOnly: string[] = [];
|
|
3747
|
+
const intentionallyOmitted: Array<{ name: string; feature: string; reason: string }> = [];
|
|
3650
3748
|
|
|
3651
3749
|
for (const lp of opts.likelyParams) {
|
|
3652
3750
|
const placeholder = `\${param.${lp.name}}`;
|
|
@@ -3684,7 +3782,19 @@ export async function externalVerification(
|
|
|
3684
3782
|
}
|
|
3685
3783
|
|
|
3686
3784
|
if (!inBody && !inHeader && !inOriginalQuery && !inInventedQuery) {
|
|
3687
|
-
|
|
3785
|
+
const limitation = workflow.limitations?.find((entry) =>
|
|
3786
|
+
entry.omittedParameters?.includes(lp.name),
|
|
3787
|
+
);
|
|
3788
|
+
const stillDeclared = workflow.parameters.some((param) => param.name === lp.name);
|
|
3789
|
+
if (limitation && !stillDeclared) {
|
|
3790
|
+
intentionallyOmitted.push({
|
|
3791
|
+
name: lp.name,
|
|
3792
|
+
feature: limitation.feature,
|
|
3793
|
+
reason: limitation.reason,
|
|
3794
|
+
});
|
|
3795
|
+
} else {
|
|
3796
|
+
notTemplated.push(lp.name);
|
|
3797
|
+
}
|
|
3688
3798
|
} else if (!inBody && !inHeader && !inOriginalQuery && inInventedQuery) {
|
|
3689
3799
|
inventedOnly.push(lp.name);
|
|
3690
3800
|
}
|
|
@@ -3692,7 +3802,7 @@ export async function externalVerification(
|
|
|
3692
3802
|
|
|
3693
3803
|
if (notTemplated.length > 0) {
|
|
3694
3804
|
failures.push(
|
|
3695
|
-
`${notTemplated.length} likelyParam(s)
|
|
3805
|
+
`${notTemplated.length} likelyParam(s) have no valid disposition: ${notTemplated.join(', ')}. Each must either appear as \${param.NAME} in a request URL, body, or header, or be removed from the public parameters list and named in workflow.limitations[].omittedParameters with an evidence-based reason. For parameters recorded as null or [] (filters the user toggled but didn\'t apply), find the correct position in the request body and replace the placeholder value with \${param.NAME}.`,
|
|
3696
3806
|
);
|
|
3697
3807
|
}
|
|
3698
3808
|
if (inventedOnly.length > 0) {
|
|
@@ -3700,6 +3810,13 @@ export async function externalVerification(
|
|
|
3700
3810
|
`${inventedOnly.length} likelyParam(s) are templated only in URL query params that do not exist in any recorded request URL: ${inventedOnly.join(', ')}. The API server likely ignores these invented params — wire them into the request body or an existing query param instead. For complex body formats, use a requestTransformModule to construct the body programmatically.`,
|
|
3701
3811
|
);
|
|
3702
3812
|
}
|
|
3813
|
+
if (intentionallyOmitted.length > 0) {
|
|
3814
|
+
warnings.push(
|
|
3815
|
+
`${intentionallyOmitted.length} detector-suggested parameter(s) were intentionally omitted by the compile agent: ${intentionallyOmitted
|
|
3816
|
+
.map(({ name, feature, reason }) => `${name} (${feature}: ${reason})`)
|
|
3817
|
+
.join('; ')}. The limitations are retained in workflow.json and surfaced to callers.`,
|
|
3818
|
+
);
|
|
3819
|
+
}
|
|
3703
3820
|
}
|
|
3704
3821
|
} catch (err) {
|
|
3705
3822
|
failures.push(
|
|
@@ -3816,8 +3933,13 @@ export async function externalVerification(
|
|
|
3816
3933
|
if (!hasLiveBaselineWorkflowTest(integrationSrc)) {
|
|
3817
3934
|
integrationOutcome = 'failed';
|
|
3818
3935
|
failures.push(
|
|
3819
|
-
'integration.test.ts has no strict baseline live workflow test: add a non-`param:` test that calls `
|
|
3936
|
+
'integration.test.ts has no strict baseline live workflow test: add a non-`param:` test that calls `runCapturedIntegrationCase(...)` on this tool workflow, fails when `result.ok` is false, and asserts the successful response contains real data. Static workflow-shape assertions or green returns on bot/infra errors are not enough to verify a generated tool.',
|
|
3820
3937
|
);
|
|
3938
|
+
} else if (opts.deferLiveIntegrationToSemanticAgent) {
|
|
3939
|
+
// The independent verifier owns the final live execution. Keeping the
|
|
3940
|
+
// suite out of this deterministic gate avoids two disconnected verdicts
|
|
3941
|
+
// and lets the reviewer inspect the exact outputs it requested.
|
|
3942
|
+
integrationOutcome = 'absent';
|
|
3821
3943
|
} else {
|
|
3822
3944
|
// Scale the verifier's live-test timeout to the suite size: the baseline plus
|
|
3823
3945
|
// one live `runWorkflowWithLadder` per param, each gated by the ~25s compile
|
|
@@ -3827,26 +3949,30 @@ export async function externalVerification(
|
|
|
3827
3949
|
const paramCount = opts.likelyParams?.length ?? 0;
|
|
3828
3950
|
const pacingMs = Number(process.env.IMPRINT_COMPILE_ACT_SPACING_MS ?? 25_000) || 0;
|
|
3829
3951
|
const verifierTimeoutMs = Math.min(120_000 + paramCount * (pacingMs + 20_000), 10 * 60_000);
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3952
|
+
const evidencePath = pathJoin(
|
|
3953
|
+
toolDir,
|
|
3954
|
+
`.imprint-live-evidence-${process.pid}-${Date.now()}.jsonl`,
|
|
3955
|
+
);
|
|
3956
|
+
try {
|
|
3957
|
+
if (existsSync(evidencePath)) unlinkSync(evidencePath);
|
|
3958
|
+
} catch {
|
|
3959
|
+
// best-effort cleanup before the run
|
|
3960
|
+
}
|
|
3961
|
+
const run = await runBunTestWithResults(integrationTestPath, toolDir, verifierTimeoutMs, {
|
|
3962
|
+
[LIVE_EVIDENCE_PATH_ENV]: evidencePath,
|
|
3963
|
+
});
|
|
3964
|
+
try {
|
|
3965
|
+
integrationEvidence = readLiveIntegrationEvidence(evidencePath);
|
|
3966
|
+
} catch (error) {
|
|
3967
|
+
failures.push(
|
|
3968
|
+
`could not read captured live integration evidence: ${error instanceof Error ? error.message : String(error)}`,
|
|
3969
|
+
);
|
|
3970
|
+
} finally {
|
|
3971
|
+
try {
|
|
3972
|
+
if (existsSync(evidencePath)) unlinkSync(evidencePath);
|
|
3973
|
+
} catch {
|
|
3974
|
+
// Evidence is ephemeral and intentionally never kept with artifacts.
|
|
3975
|
+
}
|
|
3850
3976
|
}
|
|
3851
3977
|
integrationPassedTests = run.passed;
|
|
3852
3978
|
|
|
@@ -3862,7 +3988,11 @@ export async function externalVerification(
|
|
|
3862
3988
|
integrationOutcome = verdict.outcome;
|
|
3863
3989
|
|
|
3864
3990
|
if (verdict.outcome === 'passed') {
|
|
3865
|
-
|
|
3991
|
+
if (integrationEvidence.length === 0) {
|
|
3992
|
+
failures.push(
|
|
3993
|
+
'integration.test.ts passed but captured no live evidence. Import `runCapturedIntegrationCase` from `imprint/compile-verification` and use it for every live workflow call so the independent verifier can inspect the actual tool inputs and parsed outputs.',
|
|
3994
|
+
);
|
|
3995
|
+
}
|
|
3866
3996
|
} else if (verdict.captureFailName !== null) {
|
|
3867
3997
|
const capName = verdict.captureFailName;
|
|
3868
3998
|
// If the failing capture is a `response_header` on a REPLAYED workflow
|
|
@@ -3913,7 +4043,7 @@ export async function externalVerification(
|
|
|
3913
4043
|
);
|
|
3914
4044
|
} else {
|
|
3915
4045
|
failures.push(
|
|
3916
|
-
`bun test integration.test.ts exited ${run.exitCode} — the workflow failed to produce live data
|
|
4046
|
+
`bun test integration.test.ts exited ${run.exitCode} — the workflow failed to produce live data. The baseline suite runs exactly once per verification cycle.\nstdout:\n${run.stdout}\nstderr:\n${run.stderr}`,
|
|
3917
4047
|
);
|
|
3918
4048
|
}
|
|
3919
4049
|
}
|
|
@@ -3930,7 +4060,8 @@ export async function externalVerification(
|
|
|
3930
4060
|
!referencedStateBroken &&
|
|
3931
4061
|
existsSync(integrationTestPath) &&
|
|
3932
4062
|
opts.likelyParams &&
|
|
3933
|
-
opts.likelyParams.length > 0
|
|
4063
|
+
opts.likelyParams.length > 0 &&
|
|
4064
|
+
!opts.deferLiveIntegrationToSemanticAgent
|
|
3934
4065
|
) {
|
|
3935
4066
|
const integrationSrc = readFileSync(integrationTestPath, 'utf8');
|
|
3936
4067
|
|
|
@@ -4146,5 +4277,5 @@ export async function externalVerification(
|
|
|
4146
4277
|
}
|
|
4147
4278
|
}
|
|
4148
4279
|
|
|
4149
|
-
return { failures, warnings, paramVerification, liveVerification };
|
|
4280
|
+
return { failures, warnings, paramVerification, liveVerification, integrationEvidence };
|
|
4150
4281
|
}
|