imprint-mcp 0.6.2 → 0.6.4
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 +8 -1
- 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 +41 -6
- package/src/imprint/agent.ts +7 -2
- package/src/imprint/audit.ts +255 -42
- package/src/imprint/auth-compile-tools.ts +64 -13
- package/src/imprint/claude-cli-compile.ts +43 -7
- package/src/imprint/codex-cli-compile.ts +205 -129
- 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/inject-listener.ts +31 -3
- package/src/imprint/live-verifier.ts +167 -6
- package/src/imprint/llm.ts +7 -3
- 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/react-flight-limits.ts +31 -0
- package/src/imprint/record.ts +657 -46
- package/src/imprint/redact.ts +240 -30
- package/src/imprint/response-body-intent.ts +73 -0
- package/src/imprint/response-body-lifecycle.ts +32 -0
- package/src/imprint/response-body-stream.ts +704 -0
- package/src/imprint/teach-state.ts +52 -2
- package/src/imprint/teach.ts +94 -69
- package/src/imprint/tracing.ts +60 -238
|
@@ -35,6 +35,10 @@ import {
|
|
|
35
35
|
type SharedModuleManifestEntry,
|
|
36
36
|
resolvePlanSliceFromFile,
|
|
37
37
|
} from './build-plan.ts';
|
|
38
|
+
import {
|
|
39
|
+
advanceIncompleteSemanticVerificationRuns,
|
|
40
|
+
advanceSemanticVerificationCycle,
|
|
41
|
+
} from './compile-agent-types.ts';
|
|
38
42
|
import {
|
|
39
43
|
applyLiveVerification,
|
|
40
44
|
applyParamVerification,
|
|
@@ -42,10 +46,12 @@ import {
|
|
|
42
46
|
externalVerification,
|
|
43
47
|
} from './compile-tools.ts';
|
|
44
48
|
import {
|
|
49
|
+
isInfrastructureOnlyInconclusiveReport,
|
|
45
50
|
mergeSemanticParamVerification,
|
|
46
51
|
runLiveSemanticVerification,
|
|
47
52
|
semanticVerificationFailures,
|
|
48
53
|
} from './live-verifier.ts';
|
|
54
|
+
import { DEFAULT_VERIFICATION_PROVIDER } from './llm.ts';
|
|
49
55
|
import type { ProviderName } from './llm.ts';
|
|
50
56
|
import { loadJsonFile } from './load-json.ts';
|
|
51
57
|
import { createLog } from './log.ts';
|
|
@@ -210,22 +216,45 @@ export function compileArtifactFingerprint(toolDir: string): string {
|
|
|
210
216
|
}
|
|
211
217
|
|
|
212
218
|
export function canAcceptInconclusiveDecision(opts: {
|
|
219
|
+
semanticVerificationCycles: number;
|
|
220
|
+
maxVerificationCycles: number;
|
|
221
|
+
completedReview: boolean;
|
|
222
|
+
infrastructureOnly: boolean;
|
|
213
223
|
pendingFingerprint?: string;
|
|
214
224
|
currentFingerprint: string;
|
|
215
225
|
acceptInconclusive?: boolean;
|
|
216
226
|
inconclusiveReason?: string;
|
|
217
227
|
}): boolean {
|
|
218
228
|
return (
|
|
229
|
+
opts.completedReview &&
|
|
230
|
+
opts.infrastructureOnly &&
|
|
231
|
+
opts.semanticVerificationCycles >= opts.maxVerificationCycles &&
|
|
219
232
|
opts.acceptInconclusive === true &&
|
|
220
233
|
Boolean(opts.inconclusiveReason?.trim()) &&
|
|
221
234
|
opts.pendingFingerprint === opts.currentFingerprint
|
|
222
235
|
);
|
|
223
236
|
}
|
|
224
237
|
|
|
238
|
+
export function inconclusiveDecisionAtSemanticCap(opts: {
|
|
239
|
+
semanticVerificationCycles: number;
|
|
240
|
+
maxVerificationCycles: number;
|
|
241
|
+
pendingFingerprint?: string;
|
|
242
|
+
currentFingerprint: string;
|
|
243
|
+
}): 'continue-verification' | 'await-compiler-decision' | 'fail-artifact-changed' {
|
|
244
|
+
if (!opts.pendingFingerprint || opts.semanticVerificationCycles < opts.maxVerificationCycles) {
|
|
245
|
+
return 'continue-verification';
|
|
246
|
+
}
|
|
247
|
+
return opts.pendingFingerprint === opts.currentFingerprint
|
|
248
|
+
? 'await-compiler-decision'
|
|
249
|
+
: 'fail-artifact-changed';
|
|
250
|
+
}
|
|
251
|
+
|
|
225
252
|
export async function runCompileMcpServer(opts: RunCompileMcpServerOptions): Promise<void> {
|
|
226
253
|
const isAuthMode = !!opts.authToolPlan;
|
|
227
254
|
const maxVerificationCycles = opts.maxVerificationCycles ?? 5;
|
|
255
|
+
const maxIncompleteVerifierRuns = 2;
|
|
228
256
|
let pendingInconclusive: PendingInconclusiveDecision | undefined;
|
|
257
|
+
let incompleteVerifierRuns = 0;
|
|
229
258
|
|
|
230
259
|
// Load + auto-redact the session, exactly as compile-agent.ts does.
|
|
231
260
|
let session: Session = loadJsonFile(
|
|
@@ -412,7 +441,7 @@ export async function runCompileMcpServer(opts: RunCompileMcpServerOptions): Pro
|
|
|
412
441
|
]
|
|
413
442
|
: [];
|
|
414
443
|
|
|
415
|
-
let
|
|
444
|
+
let semanticVerificationCycles = 0;
|
|
416
445
|
/** One pending checkpoint per segment — refuse a second so the orchestrator
|
|
417
446
|
* acts on exactly one request. Cleared implicitly: each segment is a fresh
|
|
418
447
|
* process with the sentinel removed before spawn. */
|
|
@@ -487,15 +516,15 @@ export async function runCompileMcpServer(opts: RunCompileMcpServerOptions): Pro
|
|
|
487
516
|
};
|
|
488
517
|
}
|
|
489
518
|
|
|
490
|
-
|
|
491
|
-
log(`auth verification failed (cycle ${
|
|
519
|
+
semanticVerificationCycles++;
|
|
520
|
+
log(`auth verification failed (cycle ${semanticVerificationCycles})`);
|
|
492
521
|
|
|
493
522
|
return {
|
|
494
523
|
isError: true,
|
|
495
524
|
content: [
|
|
496
525
|
{
|
|
497
526
|
type: 'text',
|
|
498
|
-
text: `You called done but verification failed (cycle ${
|
|
527
|
+
text: `You called done but verification failed (cycle ${semanticVerificationCycles}):
|
|
499
528
|
|
|
500
529
|
${failures.map((f) => `- ${f}`).join('\n')}
|
|
501
530
|
|
|
@@ -507,6 +536,8 @@ Fix the issues in workflow.json, re-test with run_verification, and call done ag
|
|
|
507
536
|
|
|
508
537
|
const finishVerification = beginCompileVerification(opts.toolDir);
|
|
509
538
|
try {
|
|
539
|
+
let semanticReviewCompleted = false;
|
|
540
|
+
let semanticReviewAttempted = false;
|
|
510
541
|
const { failures, warnings, paramVerification, integrationEvidence } =
|
|
511
542
|
await externalVerification(opts.toolDir, session, opts.sessionPath, {
|
|
512
543
|
expectedToolName: opts.candidate?.toolName,
|
|
@@ -536,6 +567,12 @@ Fix the issues in workflow.json, re-test with run_verification, and call done ag
|
|
|
536
567
|
if (
|
|
537
568
|
pendingInconclusive &&
|
|
538
569
|
canAcceptInconclusiveDecision({
|
|
570
|
+
semanticVerificationCycles,
|
|
571
|
+
maxVerificationCycles,
|
|
572
|
+
completedReview: pendingInconclusive.report.completedReview,
|
|
573
|
+
infrastructureOnly: isInfrastructureOnlyInconclusiveReport(
|
|
574
|
+
pendingInconclusive.report.report,
|
|
575
|
+
),
|
|
539
576
|
pendingFingerprint: pendingInconclusive.artifactFingerprint,
|
|
540
577
|
currentFingerprint,
|
|
541
578
|
acceptInconclusive: doneArgs.accept_inconclusive,
|
|
@@ -601,89 +638,154 @@ Fix the issues in workflow.json, re-test with run_verification, and call done ag
|
|
|
601
638
|
};
|
|
602
639
|
}
|
|
603
640
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
'compile artifacts changed after inconclusive review; running verification again',
|
|
610
|
-
);
|
|
611
|
-
pendingInconclusive = undefined;
|
|
612
|
-
}
|
|
613
|
-
const semantic = await runLiveSemanticVerification({
|
|
614
|
-
provider: opts.provider,
|
|
615
|
-
toolDir: opts.toolDir,
|
|
616
|
-
evidence: integrationEvidence,
|
|
641
|
+
const capDecision = inconclusiveDecisionAtSemanticCap({
|
|
642
|
+
semanticVerificationCycles,
|
|
643
|
+
maxVerificationCycles,
|
|
644
|
+
pendingFingerprint: pendingInconclusive?.artifactFingerprint,
|
|
645
|
+
currentFingerprint,
|
|
617
646
|
});
|
|
618
|
-
if (
|
|
619
|
-
pendingInconclusive = {
|
|
620
|
-
artifactFingerprint: compileArtifactFingerprint(opts.toolDir),
|
|
621
|
-
report: semantic,
|
|
622
|
-
};
|
|
623
|
-
} else {
|
|
647
|
+
if (capDecision === 'fail-artifact-changed') {
|
|
624
648
|
pendingInconclusive = undefined;
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
failures.push(...semanticFailures);
|
|
628
|
-
if (semantic.report.status === 'approved_with_gaps') {
|
|
629
|
-
warnings.push(
|
|
630
|
-
`independent semantic verification approved with gaps: ${semantic.report.gaps.join('; ')}`,
|
|
649
|
+
failures.push(
|
|
650
|
+
'compile artifacts changed after the final allowed inconclusive semantic review; refusing to run an additional review beyond the semantic cycle cap',
|
|
631
651
|
);
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
paramVerification,
|
|
636
|
-
semantic.report,
|
|
652
|
+
} else if (capDecision === 'await-compiler-decision') {
|
|
653
|
+
failures.push(
|
|
654
|
+
'semantic review limit reached with an unchanged infrastructure-only inconclusive result; explicitly accept the unverified artifact with reasoning or revise it',
|
|
637
655
|
);
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
656
|
+
} else {
|
|
657
|
+
if (
|
|
658
|
+
pendingInconclusive &&
|
|
659
|
+
pendingInconclusive.artifactFingerprint !== currentFingerprint
|
|
660
|
+
) {
|
|
661
|
+
log(
|
|
662
|
+
'compile artifacts changed after inconclusive review; running verification again',
|
|
663
|
+
);
|
|
664
|
+
pendingInconclusive = undefined;
|
|
647
665
|
}
|
|
648
|
-
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
666
|
+
semanticReviewAttempted = true;
|
|
667
|
+
const semantic = await runLiveSemanticVerification({
|
|
668
|
+
provider: DEFAULT_VERIFICATION_PROVIDER,
|
|
669
|
+
toolDir: opts.toolDir,
|
|
670
|
+
evidence: integrationEvidence,
|
|
671
|
+
});
|
|
672
|
+
semanticReviewCompleted = semantic.completedReview;
|
|
673
|
+
if (semantic.report.status === 'inconclusive') {
|
|
674
|
+
pendingInconclusive = {
|
|
675
|
+
artifactFingerprint: compileArtifactFingerprint(opts.toolDir),
|
|
676
|
+
report: semantic,
|
|
677
|
+
};
|
|
678
|
+
} else {
|
|
679
|
+
pendingInconclusive = undefined;
|
|
680
|
+
}
|
|
681
|
+
const semanticFailures = semanticVerificationFailures(semantic.report);
|
|
682
|
+
failures.push(...semanticFailures);
|
|
683
|
+
if (semantic.report.status === 'approved_with_gaps') {
|
|
684
|
+
warnings.push(
|
|
685
|
+
`independent semantic verification approved with gaps: ${semantic.report.gaps.join('; ')}`,
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
if (semanticFailures.length === 0) {
|
|
689
|
+
const semanticParams = mergeSemanticParamVerification(
|
|
690
|
+
paramVerification,
|
|
691
|
+
semantic.report,
|
|
692
|
+
);
|
|
693
|
+
// Only the independent semantic agent can mint liveVerified=true.
|
|
694
|
+
applyLiveVerification(opts.toolDir, undefined);
|
|
695
|
+
const paramWarnings = applyParamVerification(opts.toolDir, semanticParams);
|
|
696
|
+
// liveVerified/parameter annotations are metadata-only, but they
|
|
697
|
+
// change the hash-strict workflow cache key. Keep the verifier's
|
|
698
|
+
// already-proven backend preference current without probing again.
|
|
699
|
+
rebindExistingBackendsCacheToWorkflow(opts.toolDir);
|
|
700
|
+
if (paramWarnings.length > 0) {
|
|
701
|
+
log(`parameter verification:\n${paramWarnings.join('\n')}`);
|
|
702
|
+
}
|
|
703
|
+
const allWarnings = [...warnings, ...paramWarnings];
|
|
704
|
+
const sentinel = pathJoin(opts.toolDir, DONE_SENTINEL);
|
|
705
|
+
writeFileSync(
|
|
706
|
+
sentinel,
|
|
707
|
+
JSON.stringify(
|
|
708
|
+
{
|
|
709
|
+
summary,
|
|
710
|
+
verification: 'passed',
|
|
711
|
+
semanticVerification: {
|
|
712
|
+
status: semantic.report.status,
|
|
713
|
+
provider: semantic.provider,
|
|
714
|
+
model: semantic.model,
|
|
715
|
+
attempts: semantic.attempts,
|
|
716
|
+
evidenceArtifact: semantic.report.evidenceArtifact,
|
|
717
|
+
logArtifact: semantic.report.logArtifact,
|
|
718
|
+
},
|
|
719
|
+
warnings: allWarnings,
|
|
720
|
+
timestamp: Date.now(),
|
|
663
721
|
},
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
)
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
],
|
|
680
|
-
};
|
|
722
|
+
null,
|
|
723
|
+
2,
|
|
724
|
+
),
|
|
725
|
+
'utf8',
|
|
726
|
+
);
|
|
727
|
+
log(`verification passed; wrote ${sentinel}`);
|
|
728
|
+
return {
|
|
729
|
+
content: [
|
|
730
|
+
{
|
|
731
|
+
type: 'text',
|
|
732
|
+
text: `DONE_VERIFIED — deterministic checks and independent live semantic verification passed (${semantic.report.status}). Evidence: ${semantic.report.evidenceArtifact ?? '.live-verification-evidence.json'}. Verifier log: ${semantic.report.logArtifact ?? '.live-verifier-log.jsonl'}. The orchestrator will exit shortly. Do not call any more tools.`,
|
|
733
|
+
},
|
|
734
|
+
],
|
|
735
|
+
};
|
|
736
|
+
}
|
|
681
737
|
}
|
|
682
738
|
}
|
|
683
739
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
740
|
+
semanticVerificationCycles = advanceSemanticVerificationCycle(
|
|
741
|
+
semanticVerificationCycles,
|
|
742
|
+
semanticReviewCompleted,
|
|
743
|
+
);
|
|
744
|
+
incompleteVerifierRuns = advanceIncompleteSemanticVerificationRuns(
|
|
745
|
+
incompleteVerifierRuns,
|
|
746
|
+
semanticReviewAttempted,
|
|
747
|
+
semanticReviewCompleted,
|
|
748
|
+
);
|
|
749
|
+
if (incompleteVerifierRuns >= maxIncompleteVerifierRuns) {
|
|
750
|
+
const sentinel = pathJoin(opts.toolDir, DONE_SENTINEL);
|
|
751
|
+
writeFileSync(
|
|
752
|
+
sentinel,
|
|
753
|
+
JSON.stringify(
|
|
754
|
+
{
|
|
755
|
+
summary,
|
|
756
|
+
verification: 'failed',
|
|
757
|
+
incompleteVerifierRuns,
|
|
758
|
+
failures,
|
|
759
|
+
warnings,
|
|
760
|
+
timestamp: Date.now(),
|
|
761
|
+
},
|
|
762
|
+
null,
|
|
763
|
+
2,
|
|
764
|
+
),
|
|
765
|
+
'utf8',
|
|
766
|
+
);
|
|
767
|
+
return {
|
|
768
|
+
isError: true,
|
|
769
|
+
content: [
|
|
770
|
+
{
|
|
771
|
+
type: 'text',
|
|
772
|
+
text: `Independent semantic verifier failed to complete after ${maxIncompleteVerifierRuns} bounded runs. Final failures:\n${failures.map((failure) => `- ${failure}`).join('\n')}`,
|
|
773
|
+
},
|
|
774
|
+
],
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
const failurePhase = semanticReviewCompleted
|
|
778
|
+
? `semantic verification failed (cycle ${semanticVerificationCycles}/${maxVerificationCycles})`
|
|
779
|
+
: semanticReviewAttempted
|
|
780
|
+
? 'semantic verifier did not complete a review (semantic cycle unchanged)'
|
|
781
|
+
: 'deterministic verification failed (semantic cycle unchanged)';
|
|
782
|
+
log(failurePhase);
|
|
783
|
+
const awaitingFinalInconclusiveDecision =
|
|
784
|
+
semanticVerificationCycles >= maxVerificationCycles && pendingInconclusive !== undefined;
|
|
785
|
+
if (
|
|
786
|
+
semanticVerificationCycles >= maxVerificationCycles &&
|
|
787
|
+
!awaitingFinalInconclusiveDecision
|
|
788
|
+
) {
|
|
687
789
|
const sentinel = pathJoin(opts.toolDir, DONE_SENTINEL);
|
|
688
790
|
writeFileSync(
|
|
689
791
|
sentinel,
|
|
@@ -691,7 +793,7 @@ Fix the issues in workflow.json, re-test with run_verification, and call done ag
|
|
|
691
793
|
{
|
|
692
794
|
summary,
|
|
693
795
|
verification: 'failed',
|
|
694
|
-
cycles:
|
|
796
|
+
cycles: semanticVerificationCycles,
|
|
695
797
|
failures,
|
|
696
798
|
warnings,
|
|
697
799
|
timestamp: Date.now(),
|
|
@@ -706,20 +808,23 @@ Fix the issues in workflow.json, re-test with run_verification, and call done ag
|
|
|
706
808
|
content: [
|
|
707
809
|
{
|
|
708
810
|
type: 'text',
|
|
709
|
-
text: `
|
|
811
|
+
text: `Semantic verification failed after ${maxVerificationCycles} cycles. Giving up. Final failures:\n${failures.map((f) => `- ${f}`).join('\n')}`,
|
|
710
812
|
},
|
|
711
813
|
],
|
|
712
814
|
};
|
|
713
815
|
}
|
|
714
816
|
|
|
715
|
-
const continuationMessage = `You called done but
|
|
817
|
+
const continuationMessage = `You called done but ${failurePhase}:
|
|
716
818
|
|
|
717
819
|
${failures.map((f) => `- ${f}`).join('\n')}
|
|
718
820
|
|
|
719
821
|
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.`;
|
|
720
|
-
const compilerDecisionGuidance = `\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.
|
|
822
|
+
const compilerDecisionGuidance = `\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. Treat emitsTokens as downstream compatibility targets, not automatic definitions of producer success: when useful core records lack an optional consumer token, preserve the records and supported input classes, document the missing output and affected consumers, and leave those consumers limited/unverified. Narrow an input class only when its core results are unusable or misleading. Remove claims that directly depend on an omitted feature, but do not erase valid producer results solely to make a downstream chain total. Do not use a limitation to hide a broken core tool, and do not keep a known broken or ignored public input.`;
|
|
721
823
|
const inconclusiveDecisionGuidance = pendingInconclusive
|
|
722
|
-
?
|
|
824
|
+
? semanticVerificationCycles >= maxVerificationCycles &&
|
|
825
|
+
pendingInconclusive.report.completedReview
|
|
826
|
+
? '\n\nThe final allowed live semantic review was infrastructure-only inconclusive. Review its evidence and log. If they expose a tool defect, do not attempt to change the artifact after the review cap. If they expose no tool-level defect and the unchanged deterministic artifact should ship explicitly unverified, call done again without changing compile artifacts, set accept_inconclusive=true, and provide your concrete reasoning in inconclusive_reason. This compiler decision is persisted and does not mint liveVerified.'
|
|
827
|
+
: '\n\nThe live verifier was infrastructure-only inconclusive. Review its evidence and log. Fix any exposed tool defect, or call done normally to request another independent review. Explicit unverified acceptance is available only after the semantic review limit is reached.'
|
|
723
828
|
: '';
|
|
724
829
|
return {
|
|
725
830
|
isError: true,
|
|
@@ -49,6 +49,9 @@ interface RunMcpServerOptions {
|
|
|
49
49
|
name?: string;
|
|
50
50
|
/** Server version. */
|
|
51
51
|
version?: string;
|
|
52
|
+
/** Optional exact tool-name allowlist. Used by audit sessions to keep
|
|
53
|
+
* human-interactive authentication workflows out of headless MCP exposure. */
|
|
54
|
+
includeToolNames?: readonly string[];
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
interface ResolvedTool extends DiscoveredTool {
|
|
@@ -58,6 +61,17 @@ interface ResolvedTool extends DiscoveredTool {
|
|
|
58
61
|
preferredOrder?: ConcreteBackend[];
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
/** Exact allowlist selection used by bounded MCP sessions such as audit. An
|
|
65
|
+
* omitted allowlist preserves normal production behavior and exposes all tools. */
|
|
66
|
+
export function selectMcpTools<T extends { workflow: { toolName: string } }>(
|
|
67
|
+
tools: readonly T[],
|
|
68
|
+
includeToolNames?: readonly string[],
|
|
69
|
+
): T[] {
|
|
70
|
+
if (!includeToolNames) return [...tools];
|
|
71
|
+
const includedNames = new Set(includeToolNames);
|
|
72
|
+
return tools.filter((tool) => includedNames.has(tool.workflow.toolName));
|
|
73
|
+
}
|
|
74
|
+
|
|
61
75
|
/** Tool description shown to MCP clients. Includes the operator's
|
|
62
76
|
* recorded narration when present — surprisingly load-bearing for the
|
|
63
77
|
* LLM picking the right tool. */
|
|
@@ -437,7 +451,8 @@ function formatToolError(
|
|
|
437
451
|
|
|
438
452
|
export async function runMcpServer(opts: RunMcpServerOptions): Promise<void> {
|
|
439
453
|
const assetRoot = opts.assetRoot ?? imprintHomeDir();
|
|
440
|
-
const
|
|
454
|
+
const discoveredAll = await discoverTools(assetRoot, opts.site, '[imprint mcp]');
|
|
455
|
+
const discovered = selectMcpTools(discoveredAll, opts.includeToolNames);
|
|
441
456
|
const tools: ResolvedTool[] = discovered.map((t) => {
|
|
442
457
|
const playbookPath = pathResolve(t.dir, 'playbook.yaml');
|
|
443
458
|
const cacheStatus = loadBackendsCacheStatus(t.site, assetRoot, t.dir, {
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
BackendsCacheSchema,
|
|
27
27
|
type ConcreteBackend,
|
|
28
28
|
CronConfigSchema,
|
|
29
|
+
type ToolResult,
|
|
29
30
|
WorkflowSchema,
|
|
30
31
|
} from './types.ts';
|
|
31
32
|
import { VERSION } from './version.ts';
|
|
@@ -120,20 +121,32 @@ export async function probeResolvedTool(
|
|
|
120
121
|
log(`probing backends for ${tool.workflow.toolName}…`);
|
|
121
122
|
log(` params: ${JSON.stringify(params)}`);
|
|
122
123
|
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
// falls through fetch-bootstrap (~30-60s) before reaching the spliced-in
|
|
127
|
-
// cdp-replay rung, wasting time on every call.
|
|
124
|
+
// Probe the workflow's normal candidates first. Plain workflows keep the
|
|
125
|
+
// cheap fetch/stealth/playbook path; browser-backed API transports are only
|
|
126
|
+
// added if none of those candidates works.
|
|
128
127
|
const stealthCache = new Map<string, StealthFetch>();
|
|
129
128
|
const cdpPool = new Map<string, CdpBrowserFetch>();
|
|
130
129
|
const allBackends = probeCandidateBackendsForWorkflow(tool.workflow);
|
|
131
130
|
const results: BackendsCache['results'] = {};
|
|
132
131
|
const working: BackendProbeCandidate[] = [];
|
|
133
132
|
const preferredMaxMs = preferredBackendMaxMs();
|
|
133
|
+
const persistCurrentCache = (): BackendsCache => {
|
|
134
|
+
const cache: BackendsCache = {
|
|
135
|
+
probedAt: new Date().toISOString(),
|
|
136
|
+
imprintVersion: VERSION,
|
|
137
|
+
schemaVersion: 2,
|
|
138
|
+
workflowHash: workflowHash(tool.workflow),
|
|
139
|
+
capabilityHash: workflowCapabilityHash(tool.workflow),
|
|
140
|
+
preferredOrder: rankSuccessfulBackends(working),
|
|
141
|
+
results,
|
|
142
|
+
};
|
|
143
|
+
BackendsCacheSchema.parse(cache);
|
|
144
|
+
writeFileSync(outPath, `${JSON.stringify(cache, null, 2)}\n`);
|
|
145
|
+
return cache;
|
|
146
|
+
};
|
|
134
147
|
|
|
135
|
-
|
|
136
|
-
for (const backend of
|
|
148
|
+
const probeCandidates = async (backends: ConcreteBackend[]): Promise<void> => {
|
|
149
|
+
for (const backend of backends) {
|
|
137
150
|
log(`probing ${backend}…`);
|
|
138
151
|
const t0 = Date.now();
|
|
139
152
|
const { result, attempts } = await runWithLadder(
|
|
@@ -147,6 +160,14 @@ export async function probeResolvedTool(
|
|
|
147
160
|
const durationMs = Date.now() - t0;
|
|
148
161
|
const attempt = attempts[0];
|
|
149
162
|
|
|
163
|
+
const invariantFailure = backendInvariantProbeFailure(result);
|
|
164
|
+
if (invariantFailure) {
|
|
165
|
+
log(` ${backend}: workflow rejected the request before transport — stopping probe`);
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Backend-independent workflow failure for ${tool.workflow.toolName}: ${invariantFailure}`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
150
171
|
if (!attempt) {
|
|
151
172
|
results[backend] = { outcome: 'skipped', detail: 'no attempt recorded' };
|
|
152
173
|
continue;
|
|
@@ -189,6 +210,11 @@ export async function probeResolvedTool(
|
|
|
189
210
|
...(warm?.ok ? { warmDurationMs: warm.durationMs, rankingDurationMs } : {}),
|
|
190
211
|
tooSlow,
|
|
191
212
|
});
|
|
213
|
+
// Keep a valid preferred backend durable as soon as one succeeds. A
|
|
214
|
+
// later optional candidate may tarpit until the aggregate preparation
|
|
215
|
+
// deadline; losing this earlier success would force an unnecessary
|
|
216
|
+
// reprobe and leave the suite with no prepared backend.
|
|
217
|
+
persistCurrentCache();
|
|
192
218
|
log(
|
|
193
219
|
` ${backend}: OK in ${durationMs}ms${warm?.ok ? ` (warm ${warm.durationMs}ms)` : ''}${tooSlow ? ' (cold slow)' : ''}`,
|
|
194
220
|
);
|
|
@@ -212,6 +238,14 @@ export async function probeResolvedTool(
|
|
|
212
238
|
log(` ${backend}: ${result.error} — ${result.message.slice(0, 100)}`);
|
|
213
239
|
}
|
|
214
240
|
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
await probeCandidates(allBackends);
|
|
245
|
+
if (working.length === 0 && !allBackends.includes('cdp-replay')) {
|
|
246
|
+
log('default backends failed; probing browser-backed API fallbacks…');
|
|
247
|
+
await probeCandidates(['cdp-replay', 'fetch-bootstrap']);
|
|
248
|
+
}
|
|
215
249
|
} finally {
|
|
216
250
|
await closeProbeCdpPool(cdpPool);
|
|
217
251
|
}
|
|
@@ -224,24 +258,24 @@ export async function probeResolvedTool(
|
|
|
224
258
|
);
|
|
225
259
|
}
|
|
226
260
|
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
probedAt: new Date().toISOString(),
|
|
230
|
-
imprintVersion: VERSION,
|
|
231
|
-
schemaVersion: 2,
|
|
232
|
-
workflowHash: workflowHash(tool.workflow),
|
|
233
|
-
capabilityHash: workflowCapabilityHash(tool.workflow),
|
|
234
|
-
preferredOrder,
|
|
235
|
-
results,
|
|
236
|
-
};
|
|
237
|
-
BackendsCacheSchema.parse(cache); // catch schema drift early
|
|
238
|
-
|
|
239
|
-
writeFileSync(outPath, `${JSON.stringify(cache, null, 2)}\n`);
|
|
240
|
-
log(`wrote ${outPath} — preferred: ${preferredOrder.join(' → ')}`);
|
|
261
|
+
const cache = persistCurrentCache();
|
|
262
|
+
log(`wrote ${outPath} — preferred: ${cache.preferredOrder.join(' → ')}`);
|
|
241
263
|
|
|
242
264
|
return { cache, outPath };
|
|
243
265
|
}
|
|
244
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Some generated-workflow failures happen before any HTTP request is sent.
|
|
269
|
+
* Retrying those through browser-backed transports is both expensive and
|
|
270
|
+
* misleading: a different transport cannot repair a deterministic transform.
|
|
271
|
+
* Keep remote HTTP BAD_RESPONSE failures probeable because bot defenses can
|
|
272
|
+
* return backend-specific 4xx responses.
|
|
273
|
+
*/
|
|
274
|
+
export function backendInvariantProbeFailure(result: ToolResult): string | null {
|
|
275
|
+
if (result.ok || result.error !== 'BAD_RESPONSE') return null;
|
|
276
|
+
return /^request transform failed for request \d+:/i.test(result.message) ? result.message : null;
|
|
277
|
+
}
|
|
278
|
+
|
|
245
279
|
export function rankSuccessfulBackends(candidates: BackendProbeCandidate[]): ConcreteBackend[] {
|
|
246
280
|
return [...candidates]
|
|
247
281
|
.sort((a, b) => {
|
|
@@ -302,9 +336,10 @@ function preferredBackendMaxMs(): number {
|
|
|
302
336
|
|
|
303
337
|
function workflowNeedsBootstrap(workflow: ResolvedTool['workflow']): boolean {
|
|
304
338
|
if (workflow.bootstrap) return true;
|
|
305
|
-
return workflow.requests.some((
|
|
306
|
-
(
|
|
307
|
-
(
|
|
339
|
+
return workflow.requests.some((request) =>
|
|
340
|
+
(request.captures ?? []).some(
|
|
341
|
+
(capture) =>
|
|
342
|
+
capture.capability === 'browser_bootstrap' || capture.capability === 'stealth_bootstrap',
|
|
308
343
|
),
|
|
309
344
|
);
|
|
310
345
|
}
|
|
@@ -313,7 +348,7 @@ export function probeCandidateBackendsForWorkflow(
|
|
|
313
348
|
workflow: ResolvedTool['workflow'],
|
|
314
349
|
): ConcreteBackend[] {
|
|
315
350
|
return workflowNeedsBootstrap(workflow) || prefersCdpReplayFirst(workflow)
|
|
316
|
-
? ['fetch', '
|
|
351
|
+
? ['fetch', 'cdp-replay', 'fetch-bootstrap', 'stealth-fetch', 'playbook']
|
|
317
352
|
: ['fetch', 'stealth-fetch', 'playbook'];
|
|
318
353
|
}
|
|
319
354
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Bound per-row metadata for untrusted React Flight bodies below the byte cap. */
|
|
2
|
+
export const MAX_REACT_FLIGHT_ROWS = 16_384;
|
|
3
|
+
|
|
4
|
+
/** Bound aggregate parsed JSON width across an untrusted React Flight body. */
|
|
5
|
+
export const MAX_REACT_FLIGHT_JSON_NODES = 32_768;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Estimate JSON node count before JSON.parse allocates the object graph.
|
|
9
|
+
* For valid JSON, one root plus each container opener and comma is a
|
|
10
|
+
* conservative upper approximation of containers and values. Delimiters in
|
|
11
|
+
* strings are ignored, including escaped quotes.
|
|
12
|
+
*/
|
|
13
|
+
export function boundedJsonNodeCount(json: string, limit: number): number | null {
|
|
14
|
+
if (limit < 1) return null;
|
|
15
|
+
let count = 1;
|
|
16
|
+
let inString = false;
|
|
17
|
+
for (let index = 0; index < json.length; index++) {
|
|
18
|
+
const char = json.charCodeAt(index);
|
|
19
|
+
if (inString) {
|
|
20
|
+
if (char === 0x5c) index++;
|
|
21
|
+
else if (char === 0x22) inString = false;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (char === 0x22) {
|
|
25
|
+
inString = true;
|
|
26
|
+
} else if (char === 0x7b || char === 0x5b || char === 0x2c) {
|
|
27
|
+
if (++count > limit) return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return count;
|
|
31
|
+
}
|