imprint-mcp 0.6.2 → 0.6.3
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/package.json +1 -1
- package/prompts/audit-agent.md +6 -5
- package/prompts/build-planning.md +2 -2
- package/prompts/compile-agent.md +6 -4
- package/prompts/live-verifier-agent.md +24 -11
- package/src/cli.ts +38 -3
- package/src/imprint/agent.ts +7 -2
- package/src/imprint/audit.ts +212 -20
- package/src/imprint/auth-compile-tools.ts +64 -13
- package/src/imprint/compile-agent-types.ts +25 -2
- package/src/imprint/compile-agent.ts +197 -40
- package/src/imprint/compile-tools.ts +164 -22
- package/src/imprint/live-verifier.ts +167 -6
- package/src/imprint/llm.ts +4 -0
- package/src/imprint/mcp-compile-server.ts +188 -83
- package/src/imprint/mcp-server.ts +16 -1
- package/src/imprint/probe-backends.ts +60 -25
- package/src/imprint/teach-state.ts +52 -2
- package/src/imprint/teach.ts +94 -69
package/src/imprint/audit.ts
CHANGED
|
@@ -17,10 +17,15 @@ import { type ChildProcess, spawn } from 'node:child_process';
|
|
|
17
17
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
18
18
|
import { dirname, join as pathJoin } from 'node:path';
|
|
19
19
|
import { z } from 'zod';
|
|
20
|
+
import {
|
|
21
|
+
type AuthVerificationReceiptStatus,
|
|
22
|
+
readAuthVerificationReceiptStatus,
|
|
23
|
+
} from './auth-compile-tools.ts';
|
|
20
24
|
import { type ProviderName, preferredAgentModel } from './llm.ts';
|
|
21
25
|
import { createLog } from './log.ts';
|
|
22
26
|
import { imprintHomeDir } from './paths.ts';
|
|
23
|
-
import {
|
|
27
|
+
import { type TeachState, loadTeachState } from './teach-state.ts';
|
|
28
|
+
import { type ResolvedTool, discoverTools } from './tool-loader.ts';
|
|
24
29
|
import { llmSpanAttributes, setSpanAttributes, totalPromptTokens, traced } from './tracing.ts';
|
|
25
30
|
|
|
26
31
|
const log = createLog('audit');
|
|
@@ -110,6 +115,123 @@ interface AuditEvaluation {
|
|
|
110
115
|
missingParameters: MissingAuditedParameter[];
|
|
111
116
|
}
|
|
112
117
|
|
|
118
|
+
interface SkippedInteractiveAuth {
|
|
119
|
+
name: string;
|
|
120
|
+
reason: string;
|
|
121
|
+
verificationStatus: AuthVerificationReceiptStatus['status'];
|
|
122
|
+
verificationReason: string;
|
|
123
|
+
verificationAction?: string;
|
|
124
|
+
verificationBackend?: string;
|
|
125
|
+
verificationTimestamp?: number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface AuditToolPartition {
|
|
129
|
+
eligible: ResolvedTool[];
|
|
130
|
+
nonInteractiveAuthNames: string[];
|
|
131
|
+
skippedInteractiveAuth: SkippedInteractiveAuth[];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface DetectedButUnemittedTool {
|
|
135
|
+
name: string;
|
|
136
|
+
reason?: string;
|
|
137
|
+
lastCompletedStep?: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Report candidates from the latest retained recording that teach detected but
|
|
141
|
+
* did not emit. They are diagnostic metadata, not part of the audit score: the
|
|
142
|
+
* audit agent can only call emitted MCP tools. */
|
|
143
|
+
export function detectedButUnemittedTools(
|
|
144
|
+
state: TeachState,
|
|
145
|
+
emittedToolNames: readonly string[],
|
|
146
|
+
): DetectedButUnemittedTool[] {
|
|
147
|
+
const candidates = Object.values(state.workflows).filter((workflow) => workflow.candidate);
|
|
148
|
+
if (candidates.length === 0) return [];
|
|
149
|
+
|
|
150
|
+
const latest = candidates.reduce((current, workflow) =>
|
|
151
|
+
Date.parse(workflow.updatedAt) > Date.parse(current.updatedAt) ? workflow : current,
|
|
152
|
+
);
|
|
153
|
+
const emitted = new Set(emittedToolNames);
|
|
154
|
+
const byName = new Map<string, DetectedButUnemittedTool>();
|
|
155
|
+
for (const workflow of candidates) {
|
|
156
|
+
if (workflow.sessionPath !== latest.sessionPath || !workflow.candidate) continue;
|
|
157
|
+
const name = workflow.candidate.toolName;
|
|
158
|
+
// A same-named artifact may belong to an older successful teach. Only
|
|
159
|
+
// suppress the diagnostic when the latest recording itself reached emit
|
|
160
|
+
// successfully and the emitted artifact still exists.
|
|
161
|
+
if (emitted.has(name) && workflow.completedSteps.includes('emit') && !workflow.error) continue;
|
|
162
|
+
byName.set(name, {
|
|
163
|
+
name,
|
|
164
|
+
...(workflow.error ? { reason: workflow.error.split('\n')[0] } : {}),
|
|
165
|
+
...(workflow.completedSteps.at(-1)
|
|
166
|
+
? { lastCompletedStep: workflow.completedSteps.at(-1) }
|
|
167
|
+
: {}),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function countDetectedToolNames(
|
|
174
|
+
emittedToolNames: readonly string[],
|
|
175
|
+
detectedButUnemitted: readonly DetectedButUnemittedTool[],
|
|
176
|
+
): number {
|
|
177
|
+
return new Set([...emittedToolNames, ...detectedButUnemitted.map((candidate) => candidate.name)])
|
|
178
|
+
.size;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Partition generated tools for a headless audit. Authentication is eligible
|
|
182
|
+
* only when every declared action completes directly: any pause means a human
|
|
183
|
+
* continuation (OTP, CAPTCHA, device approval, email link, etc.) may be needed. */
|
|
184
|
+
export function partitionAuditTools(tools: readonly ResolvedTool[]): AuditToolPartition {
|
|
185
|
+
const eligible: ResolvedTool[] = [];
|
|
186
|
+
const nonInteractiveAuthNames: string[] = [];
|
|
187
|
+
const skippedInteractiveAuth: SkippedInteractiveAuth[] = [];
|
|
188
|
+
|
|
189
|
+
for (const tool of tools) {
|
|
190
|
+
// authConfig is the authoritative capability marker for legacy workflows
|
|
191
|
+
// where the newer optional toolKind field may be absent.
|
|
192
|
+
const auth = tool.workflow.authConfig;
|
|
193
|
+
if (tool.workflow.toolKind === 'authenticate' && !auth) {
|
|
194
|
+
const receipt = readAuthVerificationReceiptStatus(tool.dir, tool.workflow);
|
|
195
|
+
skippedInteractiveAuth.push({
|
|
196
|
+
name: tool.workflow.toolName,
|
|
197
|
+
reason: 'authentication configuration is missing, so unattended completion is not proven',
|
|
198
|
+
verificationStatus: receipt.status,
|
|
199
|
+
verificationReason: receipt.reason,
|
|
200
|
+
...(receipt.action ? { verificationAction: receipt.action } : {}),
|
|
201
|
+
...(receipt.backend ? { verificationBackend: receipt.backend } : {}),
|
|
202
|
+
...(receipt.timestamp ? { verificationTimestamp: receipt.timestamp } : {}),
|
|
203
|
+
});
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const pauses = auth
|
|
207
|
+
? Object.entries(auth.actions).filter(([, action]) => action.outcome.type === 'pause')
|
|
208
|
+
: [];
|
|
209
|
+
if (pauses.length === 0) {
|
|
210
|
+
eligible.push(tool);
|
|
211
|
+
if (auth) nonInteractiveAuthNames.push(tool.workflow.toolName);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const receipt = readAuthVerificationReceiptStatus(tool.dir, tool.workflow);
|
|
216
|
+
const pauseSummary = pauses
|
|
217
|
+
.map(([name, action]) =>
|
|
218
|
+
action.outcome.type === 'pause' ? `${name}: ${action.outcome.message}` : name,
|
|
219
|
+
)
|
|
220
|
+
.join('; ');
|
|
221
|
+
skippedInteractiveAuth.push({
|
|
222
|
+
name: tool.workflow.toolName,
|
|
223
|
+
reason: `requires human continuation (${pauseSummary})`,
|
|
224
|
+
verificationStatus: receipt.status,
|
|
225
|
+
verificationReason: receipt.reason,
|
|
226
|
+
...(receipt.action ? { verificationAction: receipt.action } : {}),
|
|
227
|
+
...(receipt.backend ? { verificationBackend: receipt.backend } : {}),
|
|
228
|
+
...(receipt.timestamp ? { verificationTimestamp: receipt.timestamp } : {}),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { eligible, nonInteractiveAuthNames, skippedInteractiveAuth };
|
|
233
|
+
}
|
|
234
|
+
|
|
113
235
|
/**
|
|
114
236
|
* Restrict an agent-authored report to the connected workflows before it is
|
|
115
237
|
* scored. Agent output is not a trusted inventory: it can repeat a tool or a
|
|
@@ -405,6 +527,10 @@ export function buildAuditMcpEnvironment(assetRoot = imprintHomeDir()): Record<s
|
|
|
405
527
|
};
|
|
406
528
|
}
|
|
407
529
|
|
|
530
|
+
export function buildAuditMcpCommandArgs(site: string, toolNames: readonly string[]): string[] {
|
|
531
|
+
return ['run', CLI_PATH, 'mcp-server', site, '--include-tools', toolNames.join(',')];
|
|
532
|
+
}
|
|
533
|
+
|
|
408
534
|
export async function runAudit(opts: RunAuditOptions): Promise<AuditScore> {
|
|
409
535
|
return await traced(
|
|
410
536
|
'audit.session',
|
|
@@ -415,13 +541,27 @@ export async function runAudit(opts: RunAuditOptions): Promise<AuditScore> {
|
|
|
415
541
|
},
|
|
416
542
|
async (span) => {
|
|
417
543
|
const assetRoot = imprintHomeDir();
|
|
418
|
-
const
|
|
419
|
-
const toolCount =
|
|
544
|
+
const discoveredTools = await discoverTools(assetRoot, opts.site, '[imprint audit]');
|
|
545
|
+
const toolCount = discoveredTools.length;
|
|
420
546
|
if (toolCount === 0) {
|
|
421
547
|
throw new Error(
|
|
422
548
|
`No generated tool found for site "${opts.site}" — run \`imprint teach ${opts.site}\` first, then audit it.`,
|
|
423
549
|
);
|
|
424
550
|
}
|
|
551
|
+
const {
|
|
552
|
+
eligible: tools,
|
|
553
|
+
nonInteractiveAuthNames,
|
|
554
|
+
skippedInteractiveAuth,
|
|
555
|
+
} = partitionAuditTools(discoveredTools);
|
|
556
|
+
const auditedToolCount = tools.length;
|
|
557
|
+
const detectedButUnemitted = detectedButUnemittedTools(
|
|
558
|
+
loadTeachState(opts.site),
|
|
559
|
+
discoveredTools.map((tool) => tool.workflow.toolName),
|
|
560
|
+
);
|
|
561
|
+
const detectedToolCount = countDetectedToolNames(
|
|
562
|
+
discoveredTools.map((tool) => tool.workflow.toolName),
|
|
563
|
+
detectedButUnemitted,
|
|
564
|
+
);
|
|
425
565
|
|
|
426
566
|
const provider = opts.provider ?? 'claude-cli';
|
|
427
567
|
const model = opts.model ?? preferredAgentModel(provider);
|
|
@@ -433,18 +573,29 @@ export async function runAudit(opts: RunAuditOptions): Promise<AuditScore> {
|
|
|
433
573
|
);
|
|
434
574
|
}
|
|
435
575
|
|
|
576
|
+
const nonInteractiveAuthSet = new Set(nonInteractiveAuthNames);
|
|
436
577
|
const expectedTools = tools.map((tool) => ({
|
|
437
578
|
name: tool.workflow.toolName,
|
|
438
|
-
parameters: tool.workflow.
|
|
579
|
+
parameters: nonInteractiveAuthSet.has(tool.workflow.toolName)
|
|
580
|
+
? []
|
|
581
|
+
: tool.workflow.parameters.map((parameter) => parameter.name),
|
|
439
582
|
}));
|
|
440
583
|
const toolNames = expectedTools.map((tool) => tool.name);
|
|
441
|
-
log(
|
|
584
|
+
log(
|
|
585
|
+
`auditing ${auditedToolCount}/${toolCount} eligible tool(s) for site "${opts.site}": ${toolNames.join(', ') || '(none)'}`,
|
|
586
|
+
);
|
|
587
|
+
for (const skipped of skippedInteractiveAuth) {
|
|
588
|
+
log(
|
|
589
|
+
`skipping interactive auth ${skipped.name}: ${skipped.reason}; receipt=${skipped.verificationStatus}`,
|
|
590
|
+
);
|
|
591
|
+
}
|
|
442
592
|
|
|
443
593
|
// Parameters that shipped live-unverified at compile time (Fix D). Tell the
|
|
444
594
|
// auditor to probe them especially — these are the most likely to be broken
|
|
445
595
|
// (the compile-time differential could not confirm their effect).
|
|
446
596
|
const unverifiedParams: Array<{ tool: string; params: string[] }> = [];
|
|
447
597
|
for (const t of tools) {
|
|
598
|
+
if (nonInteractiveAuthSet.has(t.workflow.toolName)) continue;
|
|
448
599
|
const params = (t.workflow.parameters ?? [])
|
|
449
600
|
.filter((p) => p.verified === false)
|
|
450
601
|
.map((p) => p.name);
|
|
@@ -468,16 +619,23 @@ export async function runAudit(opts: RunAuditOptions): Promise<AuditScore> {
|
|
|
468
619
|
}
|
|
469
620
|
}
|
|
470
621
|
|
|
471
|
-
const drive =
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
622
|
+
const drive =
|
|
623
|
+
auditedToolCount === 0
|
|
624
|
+
? emptyDriveAuditResult()
|
|
625
|
+
: await driveAudit({
|
|
626
|
+
site: opts.site,
|
|
627
|
+
provider,
|
|
628
|
+
model,
|
|
629
|
+
timeoutMs,
|
|
630
|
+
systemPromptPath,
|
|
631
|
+
toolNames,
|
|
632
|
+
nonInteractiveAuthNames,
|
|
633
|
+
unverifiedParams,
|
|
634
|
+
tokenDeps,
|
|
635
|
+
});
|
|
636
|
+
if (auditedToolCount === 0) {
|
|
637
|
+
log('all discovered tools require human-interactive authentication; auditor not spawned');
|
|
638
|
+
}
|
|
481
639
|
|
|
482
640
|
// Cross-reference compile-time live verification with the audit grade.
|
|
483
641
|
// The downgrade rule's purpose is to surface "flying blind" runs —
|
|
@@ -558,6 +716,10 @@ export async function runAudit(opts: RunAuditOptions): Promise<AuditScore> {
|
|
|
558
716
|
'imprint.audit.params_broken': score.paramsBroken,
|
|
559
717
|
'imprint.audit.params_untestable': score.paramsUntestable,
|
|
560
718
|
'imprint.audit.tool_count': toolCount,
|
|
719
|
+
'imprint.audit.audited_tool_count': auditedToolCount,
|
|
720
|
+
'imprint.audit.detected_tool_count': detectedToolCount,
|
|
721
|
+
'imprint.audit.detected_but_unemitted_count': detectedButUnemitted.length,
|
|
722
|
+
'imprint.audit.skipped_interactive_auth_count': skippedInteractiveAuth.length,
|
|
561
723
|
'imprint.audit.verdict': score.verdict,
|
|
562
724
|
'imprint.audit.unverified_and_ungradeable_count': unverifiedAndUngradeable.length,
|
|
563
725
|
'imprint.audit.missing_tool_count': missingToolNames.length,
|
|
@@ -583,6 +745,10 @@ export async function runAudit(opts: RunAuditOptions): Promise<AuditScore> {
|
|
|
583
745
|
report: drive.report,
|
|
584
746
|
site: opts.site,
|
|
585
747
|
toolCount,
|
|
748
|
+
auditedToolCount,
|
|
749
|
+
detectedToolCount,
|
|
750
|
+
detectedButUnemittedTools: detectedButUnemitted,
|
|
751
|
+
skippedInteractiveAuth,
|
|
586
752
|
ungradeableTools: ungradeableNames,
|
|
587
753
|
/** Connected tools without an invocation record. A partial audit
|
|
588
754
|
* cannot pass merely because the auditor named an uncalled tool. */
|
|
@@ -623,6 +789,10 @@ export async function runAudit(opts: RunAuditOptions): Promise<AuditScore> {
|
|
|
623
789
|
missingTools: missingToolNames,
|
|
624
790
|
missingParameters,
|
|
625
791
|
report: drive.report,
|
|
792
|
+
auditedToolCount,
|
|
793
|
+
skippedInteractiveAuth,
|
|
794
|
+
detectedToolCount,
|
|
795
|
+
detectedButUnemittedTools: detectedButUnemitted,
|
|
626
796
|
});
|
|
627
797
|
}
|
|
628
798
|
|
|
@@ -661,6 +831,9 @@ interface DriveAuditOptions {
|
|
|
661
831
|
timeoutMs: number;
|
|
662
832
|
systemPromptPath: string;
|
|
663
833
|
toolNames: string[];
|
|
834
|
+
/** Eligible password-only/direct-success auth tools. Interactive auth tools
|
|
835
|
+
* are filtered before the MCP session is created and never appear here. */
|
|
836
|
+
nonInteractiveAuthNames?: string[];
|
|
664
837
|
/** Per-tool params that shipped live-unverified at compile time. */
|
|
665
838
|
unverifiedParams: Array<{ tool: string; params: string[] }>;
|
|
666
839
|
/** Producer→consumer token contracts (param.sourcedFrom) so the auditor chains. */
|
|
@@ -723,17 +896,18 @@ async function driveAuditWithClaude(opts: DriveAuditOptions): Promise<DriveAudit
|
|
|
723
896
|
// with zero tools. The `imprint-audit-` prefix keeps the inline server unique.
|
|
724
897
|
const serverName = `imprint-audit-${opts.site}`;
|
|
725
898
|
const bunPath = process.execPath;
|
|
899
|
+
const mcpEnv = buildAuditMcpEnvironment();
|
|
726
900
|
const mcpConfig = {
|
|
727
901
|
mcpServers: {
|
|
728
902
|
[serverName]: {
|
|
729
903
|
command: bunPath,
|
|
730
|
-
args:
|
|
904
|
+
args: buildAuditMcpCommandArgs(opts.site, opts.toolNames),
|
|
731
905
|
// Pace every audit tool call: the auditor now differentially probes
|
|
732
906
|
// bot-defended idempotent reads (search/calendar) instead of bailing
|
|
733
907
|
// after one call, so a deliberate inter-call delay keeps the probing
|
|
734
908
|
// steady enough that the per-IP anti-bot defense isn't tripped. Only
|
|
735
909
|
// the audit sets this; production mcp-server runs unpaced.
|
|
736
|
-
env:
|
|
910
|
+
env: mcpEnv,
|
|
737
911
|
},
|
|
738
912
|
},
|
|
739
913
|
};
|
|
@@ -829,7 +1003,7 @@ async function driveAuditWithClaude(opts: DriveAuditOptions): Promise<DriveAudit
|
|
|
829
1003
|
async function driveAuditWithCodex(opts: DriveAuditOptions): Promise<DriveAuditResult> {
|
|
830
1004
|
const serverName = `imprint-audit-${opts.site}`;
|
|
831
1005
|
const bunPath = process.execPath;
|
|
832
|
-
const mcpArgs =
|
|
1006
|
+
const mcpArgs = buildAuditMcpCommandArgs(opts.site, opts.toolNames);
|
|
833
1007
|
const mcpEnv = buildAuditMcpEnvironment();
|
|
834
1008
|
|
|
835
1009
|
const unverifiedNote = buildUnverifiedParamNote(opts.unverifiedParams);
|
|
@@ -937,6 +1111,10 @@ export function buildUnverifiedParamNote(
|
|
|
937
1111
|
}
|
|
938
1112
|
|
|
939
1113
|
export function buildAuditInitialPrompt(opts: DriveAuditOptions, unverifiedNote: string): string {
|
|
1114
|
+
const nonInteractiveAuthNames = opts.nonInteractiveAuthNames ?? [];
|
|
1115
|
+
const authNote = nonInteractiveAuthNames.length
|
|
1116
|
+
? `\n\nThe following authentication tool(s) are eligible because every action completes without a human pause: ${nonInteractiveAuthNames.join(', ')}. Call each exactly once with one safe baseline. Never differentially probe or retry authentication actions. If any DATA tool returns AUTH_EXPIRED, classify that invocation as infra/inconclusive evidence; do not call an authentication tool in response.`
|
|
1117
|
+
: '\n\nNo authentication tool is eligible in this audit. If a DATA tool returns AUTH_EXPIRED, classify that invocation as infra/inconclusive evidence; do not attempt to authenticate.';
|
|
940
1118
|
return `Audit every MCP tool connected to you for the site "${opts.site}".
|
|
941
1119
|
|
|
942
1120
|
There are ${opts.toolNames.length} connected tool(s). For each one: read its description and input schema, invoke it with a realistic parameter set, judge the result, and classify each invocation as correct | tool_broken | infra | bad_params per your system prompt.
|
|
@@ -945,7 +1123,7 @@ Parameter coverage is mandatory for idempotent read tools. A cold cdp-replay/bro
|
|
|
945
1123
|
|
|
946
1124
|
Promo/coupon/voucher/discount-code parameters require a known valid code to prove a visible effect. If neither the schema nor another tool output gives you a valid code, classify that parameter as untestable; do not invent a random code and call an unchanged response no_op.
|
|
947
1125
|
|
|
948
|
-
IMPORTANT: Call tools strictly sequentially — issue exactly one tool call, wait for its result, then issue the next. Never issue tool calls in parallel or batch them in one turn. Many target sites share an anti-bot defense across endpoints, so a parallel burst trips a site-wide rate-limit (HTTP 429) that then poisons every later call. If a call returns a 429 / rate-limit / anti-bot result, classify it \`infra\` and pause before the next call.${unverifiedNote}${buildTokenDepNote(opts.tokenDeps)}
|
|
1126
|
+
IMPORTANT: Call tools strictly sequentially — issue exactly one tool call, wait for its result, then issue the next. Never issue tool calls in parallel or batch them in one turn. Many target sites share an anti-bot defense across endpoints, so a parallel burst trips a site-wide rate-limit (HTTP 429) that then poisons every later call. If a call returns a 429 / rate-limit / anti-bot result, classify it \`infra\` and pause before the next call.${authNote}${unverifiedNote}${buildTokenDepNote(opts.tokenDeps)}
|
|
949
1127
|
|
|
950
1128
|
When you are done, end your final message with exactly one fenced \`\`\`json block containing the full report and nothing after it.`;
|
|
951
1129
|
}
|
|
@@ -1391,6 +1569,10 @@ function printSummary(
|
|
|
1391
1569
|
missingTools: string[];
|
|
1392
1570
|
missingParameters: MissingAuditedParameter[];
|
|
1393
1571
|
report: AuditReport;
|
|
1572
|
+
auditedToolCount: number;
|
|
1573
|
+
skippedInteractiveAuth: SkippedInteractiveAuth[];
|
|
1574
|
+
detectedToolCount: number;
|
|
1575
|
+
detectedButUnemittedTools: DetectedButUnemittedTool[];
|
|
1394
1576
|
},
|
|
1395
1577
|
): void {
|
|
1396
1578
|
const pct = score.graded === 0 ? 'n/a' : `${score.score.toFixed(1)}%`;
|
|
@@ -1404,8 +1586,18 @@ function printSummary(
|
|
|
1404
1586
|
const invGraded = score.graded - paramsTested;
|
|
1405
1587
|
const invTotal = invGraded + score.infra + score.badParams;
|
|
1406
1588
|
console.log(
|
|
1407
|
-
`[imprint] graded ${score.graded} unit(s) = ${invGraded}/${invTotal} invocation(s) + ${paramsTested} parameter(s) across ${toolCount} tool(s) — excluded: ${score.infra} infra, ${score.badParams} bad_params, ${score.paramsUntestable} untestable param(s)`,
|
|
1589
|
+
`[imprint] graded ${score.graded} unit(s) = ${invGraded}/${invTotal} invocation(s) + ${paramsTested} parameter(s) across ${extra.auditedToolCount}/${toolCount} eligible tool(s) — excluded: ${score.infra} infra, ${score.badParams} bad_params, ${score.paramsUntestable} untestable param(s)`,
|
|
1408
1590
|
);
|
|
1591
|
+
for (const skipped of extra.skippedInteractiveAuth) {
|
|
1592
|
+
console.log(
|
|
1593
|
+
`[imprint] skipped interactive auth ${skipped.name} — ${skipped.reason}; verification receipt: ${skipped.verificationStatus}`,
|
|
1594
|
+
);
|
|
1595
|
+
}
|
|
1596
|
+
if (extra.detectedButUnemittedTools.length > 0) {
|
|
1597
|
+
console.log(
|
|
1598
|
+
`[imprint] ${extra.detectedButUnemittedTools.length}/${extra.detectedToolCount} detected tool(s) were not emitted and are excluded from scoring: ${extra.detectedButUnemittedTools.map((tool) => tool.name).join(', ')}`,
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1409
1601
|
if (paramsTested + score.paramsUntestable > 0) {
|
|
1410
1602
|
console.log(
|
|
1411
1603
|
`[imprint] parameters: ${score.paramsWorking}/${paramsTested} working — ${score.paramsNoOp} no-op, ${score.paramsBroken} broken, ${score.paramsUntestable} untestable`,
|
|
@@ -362,30 +362,81 @@ export function authWorkflowHash(workflow: Workflow): string {
|
|
|
362
362
|
return createHash('sha256').update(JSON.stringify(workflow)).digest('hex');
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
-
|
|
365
|
+
export interface AuthVerificationReceiptStatus {
|
|
366
|
+
status: 'verified' | 'stale' | 'failed' | 'missing';
|
|
367
|
+
reason: string;
|
|
368
|
+
action?: string;
|
|
369
|
+
backend?: string;
|
|
370
|
+
timestamp?: number;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Read the latest auth verification receipt without initiating authentication.
|
|
374
|
+
* The workflow hash is authoritative: a successful receipt for older generated
|
|
375
|
+
* code is diagnostic history, not current verification evidence. */
|
|
376
|
+
export function readAuthVerificationReceiptStatus(
|
|
377
|
+
toolDir: string,
|
|
378
|
+
workflow: Workflow,
|
|
379
|
+
): AuthVerificationReceiptStatus {
|
|
366
380
|
const path = pathJoin(toolDir, AUTH_VERIFICATION_ATTEMPT_SENTINEL);
|
|
367
381
|
if (!existsSync(path)) {
|
|
368
|
-
return
|
|
382
|
+
return {
|
|
383
|
+
status: 'missing',
|
|
384
|
+
reason: 'no live auth verification attempt was recorded',
|
|
385
|
+
};
|
|
369
386
|
}
|
|
370
387
|
|
|
371
|
-
let attempt: {
|
|
388
|
+
let attempt: {
|
|
389
|
+
action?: unknown;
|
|
390
|
+
ok?: unknown;
|
|
391
|
+
workflowHash?: unknown;
|
|
392
|
+
backend?: unknown;
|
|
393
|
+
timestamp?: unknown;
|
|
394
|
+
};
|
|
372
395
|
try {
|
|
373
396
|
attempt = JSON.parse(readFileSync(path, 'utf8')) as typeof attempt;
|
|
374
397
|
} catch (err) {
|
|
375
|
-
return
|
|
376
|
-
|
|
377
|
-
|
|
398
|
+
return {
|
|
399
|
+
status: 'failed',
|
|
400
|
+
reason: `live auth verification record is invalid: ${err instanceof Error ? err.message : String(err)}`,
|
|
401
|
+
};
|
|
378
402
|
}
|
|
403
|
+
const details = {
|
|
404
|
+
...(typeof attempt.action === 'string' ? { action: attempt.action } : {}),
|
|
405
|
+
...(typeof attempt.backend === 'string' ? { backend: attempt.backend } : {}),
|
|
406
|
+
...(typeof attempt.timestamp === 'number' ? { timestamp: attempt.timestamp } : {}),
|
|
407
|
+
};
|
|
379
408
|
if (attempt.ok !== true || typeof attempt.action !== 'string') {
|
|
380
|
-
return
|
|
409
|
+
return {
|
|
410
|
+
status: 'failed',
|
|
411
|
+
reason: 'the most recent live auth verification did not succeed',
|
|
412
|
+
...details,
|
|
413
|
+
};
|
|
381
414
|
}
|
|
382
415
|
if (attempt.workflowHash !== authWorkflowHash(workflow)) {
|
|
383
|
-
return
|
|
416
|
+
return {
|
|
417
|
+
status: 'stale',
|
|
418
|
+
reason: 'workflow.json changed after the most recent successful live auth verification',
|
|
419
|
+
...details,
|
|
420
|
+
};
|
|
384
421
|
}
|
|
385
422
|
const action = workflow.authConfig?.actions[attempt.action];
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
423
|
+
if (action?.outcome.type !== 'success') {
|
|
424
|
+
return {
|
|
425
|
+
status: 'failed',
|
|
426
|
+
reason: `the most recent successful live action ${JSON.stringify(attempt.action)} does not declare a success outcome`,
|
|
427
|
+
...details,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
status: 'verified',
|
|
432
|
+
reason: `live auth verification succeeded with action ${JSON.stringify(attempt.action)}`,
|
|
433
|
+
...details,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function liveAttemptFailures(toolDir: string, workflow: Workflow): string[] {
|
|
438
|
+
const receipt = readAuthVerificationReceiptStatus(toolDir, workflow);
|
|
439
|
+
if (receipt.status === 'verified') return [];
|
|
440
|
+
const suffix = receipt.status === 'missing' ? '; call run_verification before done' : '';
|
|
441
|
+
return [`${receipt.reason}${suffix}`];
|
|
391
442
|
}
|
|
@@ -10,6 +10,29 @@ import type { AgentProgress } from './agent.ts';
|
|
|
10
10
|
import { type AssignedSharedModule, describeAssignedModules } from './build-plan.ts';
|
|
11
11
|
import type { SharedCompileContext, ToolCandidate } from './tool-candidates.ts';
|
|
12
12
|
|
|
13
|
+
/** Only an independent semantic-verifier result consumes the bounded review
|
|
14
|
+
* budget. Deterministic preflight failures remain deadline-bounded but do not
|
|
15
|
+
* spend a compiler↔reviewer iteration. */
|
|
16
|
+
export function advanceSemanticVerificationCycle(
|
|
17
|
+
current: number,
|
|
18
|
+
semanticReviewCompleted: boolean,
|
|
19
|
+
): number {
|
|
20
|
+
return semanticReviewCompleted ? current + 1 : current;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Bound verifier infrastructure failures separately from semantic review
|
|
24
|
+
* cycles. One verifier invocation already includes its own provider retry; a
|
|
25
|
+
* second failed invocation gives the compiler one chance to react without
|
|
26
|
+
* allowing a missing provider to extend the compile deadline forever. */
|
|
27
|
+
export function advanceIncompleteSemanticVerificationRuns(
|
|
28
|
+
current: number,
|
|
29
|
+
semanticReviewAttempted: boolean,
|
|
30
|
+
semanticReviewCompleted: boolean,
|
|
31
|
+
): number {
|
|
32
|
+
if (semanticReviewCompleted) return 0;
|
|
33
|
+
return semanticReviewAttempted ? current + 1 : current;
|
|
34
|
+
}
|
|
35
|
+
|
|
13
36
|
/** Render a per-tool implementation plan (param→field mapping, request
|
|
14
37
|
* construction, response parsing, shared-module imports, edge cases) into an
|
|
15
38
|
* initial-message section the compile agent must follow. Shared verbatim by the
|
|
@@ -47,8 +70,8 @@ Compile only the selected candidate. Do not create tools for other actions in th
|
|
|
47
70
|
}
|
|
48
71
|
|
|
49
72
|
export interface CompileAgentProgress extends AgentProgress {
|
|
50
|
-
/** 1-based
|
|
51
|
-
*
|
|
73
|
+
/** 1-based semantic-review cycle. Deterministic preflight retries leave this
|
|
74
|
+
* value unchanged. */
|
|
52
75
|
verificationCycle: number;
|
|
53
76
|
/** Verification cap for bounded data-tool compiles; auth is deadline-bounded. */
|
|
54
77
|
maxVerificationCycles?: number;
|