@provos/ironcurtain 0.9.0 → 0.10.0
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 +20 -12
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/config/config-command.js +12 -3
- package/dist/config/config-command.js.map +1 -1
- package/dist/config/model-provider.d.ts +8 -2
- package/dist/config/model-provider.js +52 -9
- package/dist/config/model-provider.js.map +1 -1
- package/dist/config/paths.d.ts +6 -0
- package/dist/config/paths.js +18 -9
- package/dist/config/paths.js.map +1 -1
- package/dist/config/user-config.d.ts +9 -0
- package/dist/config/user-config.js +26 -5
- package/dist/config/user-config.js.map +1 -1
- package/dist/cron/compile-task-policy.js +1 -0
- package/dist/cron/compile-task-policy.js.map +1 -1
- package/dist/docker/adapters/claude-code.d.ts +2 -1
- package/dist/docker/adapters/claude-code.js +135 -120
- package/dist/docker/adapters/claude-code.js.map +1 -1
- package/dist/docker/agent-registry.js +3 -2
- package/dist/docker/agent-registry.js.map +1 -1
- package/dist/docker/container-lifecycle.d.ts +15 -0
- package/dist/docker/container-lifecycle.js +32 -0
- package/dist/docker/container-lifecycle.js.map +1 -0
- package/dist/docker/docker-agent-session.js +16 -23
- package/dist/docker/docker-agent-session.js.map +1 -1
- package/dist/docker/docker-infrastructure.d.ts +10 -0
- package/dist/docker/docker-infrastructure.js +71 -4
- package/dist/docker/docker-infrastructure.js.map +1 -1
- package/dist/docker/docker-manager.js +37 -0
- package/dist/docker/docker-manager.js.map +1 -1
- package/dist/docker/mitm-proxy.d.ts +6 -0
- package/dist/docker/mitm-proxy.js +343 -25
- package/dist/docker/mitm-proxy.js.map +1 -1
- package/dist/docker/oauth-credentials.d.ts +45 -6
- package/dist/docker/oauth-credentials.js +151 -24
- package/dist/docker/oauth-credentials.js.map +1 -1
- package/dist/docker/oauth-token-manager.d.ts +15 -16
- package/dist/docker/oauth-token-manager.js +23 -17
- package/dist/docker/oauth-token-manager.js.map +1 -1
- package/dist/docker/provider-config.d.ts +32 -0
- package/dist/docker/provider-config.js +20 -0
- package/dist/docker/provider-config.js.map +1 -1
- package/dist/docker/pty-session.js +14 -22
- package/dist/docker/pty-session.js.map +1 -1
- package/dist/docker/types.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/mux/mux-app.d.ts +2 -0
- package/dist/mux/mux-app.js +48 -37
- package/dist/mux/mux-app.js.map +1 -1
- package/dist/mux/mux-command.js +7 -1
- package/dist/mux/mux-command.js.map +1 -1
- package/dist/mux/mux-input-handler.d.ts +2 -0
- package/dist/mux/mux-input-handler.js +6 -4
- package/dist/mux/mux-input-handler.js.map +1 -1
- package/dist/mux/mux-renderer.js +20 -5
- package/dist/mux/mux-renderer.js.map +1 -1
- package/dist/mux/pty-bridge.d.ts +2 -0
- package/dist/mux/pty-bridge.js +3 -0
- package/dist/mux/pty-bridge.js.map +1 -1
- package/dist/mux/session-scanner.d.ts +7 -0
- package/dist/mux/session-scanner.js +21 -1
- package/dist/mux/session-scanner.js.map +1 -1
- package/dist/mux/types.d.ts +0 -4
- package/dist/mux/types.js.map +1 -1
- package/dist/persona/compile-persona-policy.js +1 -0
- package/dist/persona/compile-persona-policy.js.map +1 -1
- package/dist/pipeline/annotate.d.ts +7 -1
- package/dist/pipeline/annotate.js +92 -2
- package/dist/pipeline/annotate.js.map +1 -1
- package/dist/pipeline/compile.js +2 -0
- package/dist/pipeline/compile.js.map +1 -1
- package/dist/pipeline/llm-logger.d.ts +1 -0
- package/dist/pipeline/llm-logger.js +8 -1
- package/dist/pipeline/llm-logger.js.map +1 -1
- package/dist/pipeline/parallel-progress.d.ts +59 -0
- package/dist/pipeline/parallel-progress.js +263 -0
- package/dist/pipeline/parallel-progress.js.map +1 -0
- package/dist/pipeline/pipeline-runner.d.ts +49 -8
- package/dist/pipeline/pipeline-runner.js +375 -226
- package/dist/pipeline/pipeline-runner.js.map +1 -1
- package/dist/pipeline/pipeline-shared.d.ts +58 -0
- package/dist/pipeline/pipeline-shared.js +124 -1
- package/dist/pipeline/pipeline-shared.js.map +1 -1
- package/dist/pipeline/server-prefilter.d.ts +31 -0
- package/dist/pipeline/server-prefilter.js +115 -0
- package/dist/pipeline/server-prefilter.js.map +1 -0
- package/dist/pipeline/types.d.ts +5 -0
- package/dist/session/preflight.js +15 -16
- package/dist/session/preflight.js.map +1 -1
- package/dist/trusted-process/sandbox-integration.js +8 -0
- package/dist/trusted-process/sandbox-integration.js.map +1 -1
- package/docker/Dockerfile.base +3 -1
- package/docker/Dockerfile.base.arm64 +8 -1
- package/package.json +4 -1
|
@@ -12,26 +12,33 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { resolve } from 'node:path';
|
|
14
14
|
import chalk from 'chalk';
|
|
15
|
+
import pLimit from 'p-limit';
|
|
15
16
|
import { extractServerDomainAllowlists } from '../config/index.js';
|
|
17
|
+
import { createLanguageModel } from '../config/model-provider.js';
|
|
18
|
+
import { loadUserConfig } from '../config/user-config.js';
|
|
16
19
|
import { PolicyEngine } from '../trusted-process/policy-engine.js';
|
|
17
20
|
import { buildCompilerSystemPrompt, ConstitutionCompilerSession, formatAnnotationsSummary, validateCompiledRules, } from './constitution-compiler.js';
|
|
18
21
|
import { getHandwrittenScenarios } from './handwritten-scenarios.js';
|
|
19
22
|
import { resolveAllLists } from './list-resolver.js';
|
|
20
|
-
import { computeHash, loadExistingArtifact, loadStoredToolAnnotationsFile, resolveRulePaths, writeArtifact,
|
|
23
|
+
import { computeHash, loadExistingArtifact, loadStoredToolAnnotationsFile, resolveRulePaths, writeArtifact, createPipelineLlm, createPerServerModel, createThrottledModel, SpinnerProgressReporter, } from './pipeline-shared.js';
|
|
21
24
|
import { resolveStoredAnnotationsFile } from '../types/argument-roles.js';
|
|
22
25
|
import { applyScenarioCorrections, buildJudgeSystemPrompt, detectAllDefaultRoleFallbacks, extractScenarioCorrections, filterStructuralConflicts, PolicyVerifierSession, verifyPolicy, } from './policy-verifier.js';
|
|
23
26
|
import { filterInvalidSchemaScenarios } from './scenario-schema-validator.js';
|
|
24
27
|
import { buildGeneratorSystemPrompt, generateScenarios, repairScenarios } from './scenario-generator.js';
|
|
28
|
+
import { prefilterServers } from './server-prefilter.js';
|
|
25
29
|
import { connectViaProxy } from './proxy-mcp-connections.js';
|
|
26
30
|
/** Creates PipelineModels from user config. Delegates to shared createPipelineLlm. */
|
|
27
31
|
export async function createPipelineModels(logDir) {
|
|
28
32
|
const effectiveLogDir = logDir ?? resolve(process.cwd(), 'generated');
|
|
29
33
|
const llm = await createPipelineLlm(effectiveLogDir, 'unknown');
|
|
34
|
+
const userConfig = loadUserConfig();
|
|
35
|
+
const haikuBaseLlm = await createLanguageModel(userConfig.prefilterModelId, userConfig);
|
|
36
|
+
const { model: prefilterModel } = createPerServerModel(haikuBaseLlm, llm.logPath, 'prefilter');
|
|
30
37
|
return {
|
|
31
|
-
|
|
38
|
+
baseLlm: llm.baseLlm,
|
|
32
39
|
cacheStrategy: llm.cacheStrategy,
|
|
33
|
-
logContext: llm.logContext,
|
|
34
40
|
logPath: llm.logPath,
|
|
41
|
+
prefilterModel,
|
|
35
42
|
};
|
|
36
43
|
}
|
|
37
44
|
// ---------------------------------------------------------------------------
|
|
@@ -52,7 +59,8 @@ function extractPermittedDirectories(rules) {
|
|
|
52
59
|
function collectProbeScenarios(result) {
|
|
53
60
|
return result.rounds.flatMap((round) => round.newScenarios);
|
|
54
61
|
}
|
|
55
|
-
function filterAndLogStructuralConflicts(engine, scenarios, label = 'Discarded scenario (structural conflict)', storedAnnotations) {
|
|
62
|
+
function filterAndLogStructuralConflicts(engine, scenarios, label = 'Discarded scenario (structural conflict)', storedAnnotations, reporter) {
|
|
63
|
+
const warn = (msg) => (reporter ? reporter.warn(msg) : console.error(msg));
|
|
56
64
|
const filterResult = filterStructuralConflicts(engine, scenarios);
|
|
57
65
|
let valid = filterResult.valid;
|
|
58
66
|
const discarded = filterResult.discarded;
|
|
@@ -60,7 +68,7 @@ function filterAndLogStructuralConflicts(engine, scenarios, label = 'Discarded s
|
|
|
60
68
|
const prefix = d.scenario.source === 'handwritten'
|
|
61
69
|
? chalk.yellow('Warning: handwritten scenario conflicts with structural invariant:')
|
|
62
70
|
: chalk.dim(`${label}:`);
|
|
63
|
-
|
|
71
|
+
warn(` ${prefix} "${d.scenario.description}" — ${d.rule} always returns ${d.actual}`);
|
|
64
72
|
}
|
|
65
73
|
// Discard scenarios whose arguments don't match any conditional role spec —
|
|
66
74
|
// they fall back to all default roles and test the wrong thing.
|
|
@@ -74,7 +82,7 @@ function filterAndLogStructuralConflicts(engine, scenarios, label = 'Discarded s
|
|
|
74
82
|
const w = fallbackWarnings.find((fw) => fw.scenario.description === s.description);
|
|
75
83
|
const details = w?.details.join('; ') ?? 'unknown conditional mismatch';
|
|
76
84
|
const discardLabel = 'Discarded (default role fallback)';
|
|
77
|
-
|
|
85
|
+
warn(` ${chalk.dim(`${discardLabel}:`)} "${s.description}" — ${details}`);
|
|
78
86
|
discarded.push({ scenario: s, rule: 'default-role-fallback', actual: 'deny' });
|
|
79
87
|
}
|
|
80
88
|
else {
|
|
@@ -88,7 +96,7 @@ function filterAndLogStructuralConflicts(engine, scenarios, label = 'Discarded s
|
|
|
88
96
|
const schemaResult = filterInvalidSchemaScenarios(valid, storedAnnotations);
|
|
89
97
|
if (schemaResult.discarded.length > 0) {
|
|
90
98
|
for (const d of schemaResult.discarded) {
|
|
91
|
-
|
|
99
|
+
warn(` ${chalk.dim('Discarded (schema mismatch):')} "${d.scenario.description}" — ${d.rule}`);
|
|
92
100
|
}
|
|
93
101
|
discarded.push(...schemaResult.discarded);
|
|
94
102
|
valid = schemaResult.valid;
|
|
@@ -96,6 +104,10 @@ function filterAndLogStructuralConflicts(engine, scenarios, label = 'Discarded s
|
|
|
96
104
|
}
|
|
97
105
|
return { valid, discarded };
|
|
98
106
|
}
|
|
107
|
+
/** Coerce an unknown thrown value into an Error. */
|
|
108
|
+
function toError(err) {
|
|
109
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
110
|
+
}
|
|
99
111
|
class RuleValidationError extends Error {
|
|
100
112
|
errors;
|
|
101
113
|
constructor(errors) {
|
|
@@ -104,11 +116,12 @@ class RuleValidationError extends Error {
|
|
|
104
116
|
this.name = 'RuleValidationError';
|
|
105
117
|
}
|
|
106
118
|
}
|
|
107
|
-
function validateRulesOrThrow(rules, listDefinitions = []) {
|
|
119
|
+
function validateRulesOrThrow(rules, listDefinitions = [], reporter) {
|
|
120
|
+
const warn = (msg) => (reporter ? reporter.warn(msg) : console.error(msg));
|
|
108
121
|
const ruleValidation = validateCompiledRules(rules, listDefinitions);
|
|
109
122
|
if (ruleValidation.warnings.length > 0) {
|
|
110
123
|
for (const w of ruleValidation.warnings) {
|
|
111
|
-
|
|
124
|
+
warn(` ${chalk.yellow('Warning:')} ${w}`);
|
|
112
125
|
}
|
|
113
126
|
}
|
|
114
127
|
if (!ruleValidation.valid) {
|
|
@@ -262,7 +275,7 @@ export function deduplicateListDefinitions(defs) {
|
|
|
262
275
|
* Rules are concatenated in alphabetical server order for determinism.
|
|
263
276
|
* List definitions are deduplicated by name.
|
|
264
277
|
*/
|
|
265
|
-
export function mergeServerResults(results, constitutionHash) {
|
|
278
|
+
export function mergeServerResults(results, constitutionHash, skippedServers) {
|
|
266
279
|
const sortedResults = [...results].sort((a, b) => a.serverName.localeCompare(b.serverName));
|
|
267
280
|
const allRules = sortedResults.flatMap((r) => r.rules);
|
|
268
281
|
const allListDefs = sortedResults.flatMap((r) => r.listDefinitions);
|
|
@@ -277,6 +290,12 @@ export function mergeServerResults(results, constitutionHash) {
|
|
|
277
290
|
if (uniqueListDefs.length > 0) {
|
|
278
291
|
artifact.listDefinitions = uniqueListDefs;
|
|
279
292
|
}
|
|
293
|
+
// skippedServers is informational metadata — not read by PolicyEngine.
|
|
294
|
+
// It's intentionally excluded from inputHash; the constitutionHash already
|
|
295
|
+
// changes when the constitution changes, which drives different skip decisions.
|
|
296
|
+
if (skippedServers && skippedServers.length > 0) {
|
|
297
|
+
artifact.skippedServers = skippedServers;
|
|
298
|
+
}
|
|
280
299
|
return artifact;
|
|
281
300
|
}
|
|
282
301
|
// ---------------------------------------------------------------------------
|
|
@@ -291,13 +310,15 @@ export function mergeServerResults(results, constitutionHash) {
|
|
|
291
310
|
* (broad principles) and 'task-policy' (strict whitelist) modes.
|
|
292
311
|
*/
|
|
293
312
|
export class PipelineRunner {
|
|
294
|
-
|
|
313
|
+
baseLlm;
|
|
295
314
|
cacheStrategy;
|
|
296
|
-
|
|
315
|
+
logPath;
|
|
316
|
+
prefilterModel;
|
|
297
317
|
constructor(models) {
|
|
298
|
-
this.
|
|
318
|
+
this.baseLlm = models.baseLlm;
|
|
299
319
|
this.cacheStrategy = models.cacheStrategy;
|
|
300
|
-
this.
|
|
320
|
+
this.prefilterModel = models.prefilterModel;
|
|
321
|
+
this.logPath = models.logPath;
|
|
301
322
|
}
|
|
302
323
|
/**
|
|
303
324
|
* Runs the full pipeline. Returns the compiled policy on success.
|
|
@@ -330,10 +351,10 @@ export class PipelineRunner {
|
|
|
330
351
|
// Resolve conditional role specs to flat annotations for compiler prompts
|
|
331
352
|
const toolAnnotationsFile = resolveStoredAnnotationsFile(storedAnnotationsFile);
|
|
332
353
|
const constitutionHash = computeHash(config.constitutionInput);
|
|
333
|
-
// Phase 1: Per-server compilation
|
|
334
|
-
const serverResults = await this.compileAllServers(config, toolAnnotationsFile, constitutionHash, storedAnnotationsFile);
|
|
354
|
+
// Phase 1: Per-server compilation (with optional Haiku pre-filter)
|
|
355
|
+
const { results: serverResults, skippedServers } = await this.compileAllServers(config, toolAnnotationsFile, constitutionHash, storedAnnotationsFile);
|
|
335
356
|
// Phase 2: Merge
|
|
336
|
-
const mergedPolicy = mergeServerResults(serverResults, constitutionHash);
|
|
357
|
+
const mergedPolicy = mergeServerResults(serverResults, constitutionHash, skippedServers);
|
|
337
358
|
writeArtifact(config.outputDir, 'compiled-policy.json', mergedPolicy);
|
|
338
359
|
// Merge scenarios for the global artifact
|
|
339
360
|
const allScenarios = serverResults.flatMap((r) => r.scenarios);
|
|
@@ -391,11 +412,11 @@ export class PipelineRunner {
|
|
|
391
412
|
return mergedPolicy;
|
|
392
413
|
}
|
|
393
414
|
/**
|
|
394
|
-
* Compiles rules for all servers
|
|
395
|
-
*
|
|
415
|
+
* Compiles rules for all servers, in parallel when multiple servers are present.
|
|
416
|
+
* Single-server compilation uses sequential spinner-based output.
|
|
417
|
+
* Multi-server compilation uses parallel execution with a multi-line progress display.
|
|
396
418
|
*/
|
|
397
419
|
async compileAllServers(config, toolAnnotationsFile, constitutionHash, storedAnnotationsFile) {
|
|
398
|
-
const results = [];
|
|
399
420
|
const serverEntries = Object.entries(toolAnnotationsFile.servers);
|
|
400
421
|
// Apply server filter if provided
|
|
401
422
|
const { serverFilter } = config;
|
|
@@ -406,35 +427,126 @@ export class PipelineRunner {
|
|
|
406
427
|
throw new Error(`No matching servers found for filter: ${config.serverFilter.join(', ')}. ` +
|
|
407
428
|
`Available: ${serverEntries.map(([n]) => n).join(', ')}`);
|
|
408
429
|
}
|
|
430
|
+
// Pre-filter with Haiku: skip servers irrelevant to the input text
|
|
431
|
+
let entriesToCompile = filteredEntries;
|
|
432
|
+
const skippedServers = [];
|
|
433
|
+
if (config.prefilterText !== undefined && !config.serverFilter) {
|
|
434
|
+
const prefilterText = config.prefilterText;
|
|
435
|
+
if (prefilterText.trim() === '') {
|
|
436
|
+
console.error(chalk.yellow('Pre-filter input is empty. All servers skipped (default-deny applies).'));
|
|
437
|
+
const allSkipped = filteredEntries.map(([name]) => ({
|
|
438
|
+
serverName: name,
|
|
439
|
+
reason: 'No input text provided — all servers skipped by default',
|
|
440
|
+
}));
|
|
441
|
+
return { results: [], skippedServers: allSkipped };
|
|
442
|
+
}
|
|
443
|
+
const serverToolPairs = filteredEntries.map(([name]) => [name, toolAnnotationsFile.servers[name].tools]);
|
|
444
|
+
const decisions = await prefilterServers(prefilterText, serverToolPairs, this.prefilterModel, config.constitutionKind);
|
|
445
|
+
const decisionMap = new Map(decisions.map((d) => [d.serverName, d]));
|
|
446
|
+
entriesToCompile = filteredEntries.filter(([name]) => {
|
|
447
|
+
const decision = decisionMap.get(name);
|
|
448
|
+
if (decision?.skip) {
|
|
449
|
+
console.error(` ${chalk.dim(name)}: ${chalk.yellow('skipped')} — ${decision.reason}`);
|
|
450
|
+
skippedServers.push({ serverName: name, reason: decision.reason });
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
return true;
|
|
454
|
+
});
|
|
455
|
+
const skippedCount = filteredEntries.length - entriesToCompile.length;
|
|
456
|
+
if (skippedCount > 0) {
|
|
457
|
+
console.error(` Pre-filter: ${skippedCount} server(s) skipped, ${entriesToCompile.length} proceeding`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (entriesToCompile.length === 0) {
|
|
461
|
+
console.error(chalk.yellow('All servers skipped by pre-filter. Policy will use default-deny only.'));
|
|
462
|
+
return { results: [], skippedServers };
|
|
463
|
+
}
|
|
409
464
|
// Load handwritten scenarios once, then filter per server (avoids N redundant loads)
|
|
410
465
|
const includeHandwritten = config.includeHandwrittenScenarios ?? config.constitutionKind === 'constitution';
|
|
411
466
|
const allHandwrittenScenarios = includeHandwritten ? getHandwrittenScenarios(config.allowedDirectory) : [];
|
|
467
|
+
// Create concurrency controls
|
|
468
|
+
const llmSemaphore = pLimit(8);
|
|
469
|
+
const serverLimit = pLimit(10);
|
|
470
|
+
const buildUnit = (serverName) => ({
|
|
471
|
+
serverName,
|
|
472
|
+
annotations: toolAnnotationsFile.servers[serverName].tools,
|
|
473
|
+
storedAnnotations: storedAnnotationsFile.servers[serverName].tools,
|
|
474
|
+
constitutionText: config.constitutionInput,
|
|
475
|
+
constitutionKind: config.constitutionKind,
|
|
476
|
+
allowedDirectory: config.allowedDirectory,
|
|
477
|
+
protectedPaths: config.protectedPaths,
|
|
478
|
+
mcpServerConfig: config.mcpServers?.[serverName],
|
|
479
|
+
handwrittenScenarios: allHandwrittenScenarios.filter((s) => s.request.serverName === serverName),
|
|
480
|
+
});
|
|
481
|
+
const useParallel = entriesToCompile.length > 1;
|
|
482
|
+
let results;
|
|
483
|
+
if (useParallel) {
|
|
484
|
+
results = await this.compileServersParallel(entriesToCompile, config, constitutionHash, buildUnit, llmSemaphore, serverLimit);
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
results = await this.compileServersSequential(entriesToCompile, config, constitutionHash, buildUnit, llmSemaphore);
|
|
488
|
+
}
|
|
489
|
+
return { results, skippedServers };
|
|
490
|
+
}
|
|
491
|
+
/** Creates a per-server model wrapped with LLM call throttling. */
|
|
492
|
+
createServerModel(serverName, llmSemaphore) {
|
|
493
|
+
const { model, logContext } = createPerServerModel(this.baseLlm, this.logPath, serverName);
|
|
494
|
+
return { model: createThrottledModel(model, llmSemaphore), logContext };
|
|
495
|
+
}
|
|
496
|
+
async compileServersSequential(filteredEntries, config, constitutionHash, buildUnit, llmSemaphore) {
|
|
497
|
+
const results = [];
|
|
412
498
|
const totalServers = filteredEntries.length;
|
|
413
499
|
const failedServers = [];
|
|
414
500
|
for (let i = 0; i < filteredEntries.length; i++) {
|
|
415
|
-
const [serverName
|
|
501
|
+
const [serverName] = filteredEntries[i];
|
|
416
502
|
console.error('');
|
|
417
503
|
console.error(chalk.bold(`[${i + 1}/${totalServers}] Compiling server: ${serverName}`));
|
|
504
|
+
const { model, logContext } = this.createServerModel(serverName, llmSemaphore);
|
|
505
|
+
const reporter = new SpinnerProgressReporter(serverName);
|
|
418
506
|
try {
|
|
419
|
-
const result = await this.compileServer(
|
|
420
|
-
serverName,
|
|
421
|
-
annotations: serverData.tools,
|
|
422
|
-
storedAnnotations: storedAnnotationsFile.servers[serverName].tools,
|
|
423
|
-
constitutionText: config.constitutionInput,
|
|
424
|
-
constitutionKind: config.constitutionKind,
|
|
425
|
-
allowedDirectory: config.allowedDirectory,
|
|
426
|
-
protectedPaths: config.protectedPaths,
|
|
427
|
-
mcpServerConfig: config.mcpServers?.[serverName],
|
|
428
|
-
handwrittenScenarios: allHandwrittenScenarios.filter((s) => s.request.serverName === serverName),
|
|
429
|
-
}, config, constitutionHash);
|
|
507
|
+
const result = await this.compileServer(buildUnit(serverName), config, constitutionHash, model, logContext, reporter);
|
|
430
508
|
results.push(result);
|
|
431
509
|
}
|
|
432
510
|
catch (err) {
|
|
433
|
-
const
|
|
434
|
-
|
|
511
|
+
const coerced = toError(err);
|
|
512
|
+
reporter.fail('compiling', coerced);
|
|
513
|
+
console.error(` ${chalk.red(`Server "${serverName}" failed:`)} ${coerced.message}`);
|
|
435
514
|
failedServers.push(serverName);
|
|
436
515
|
}
|
|
437
516
|
}
|
|
517
|
+
return this.handleCompilationResults(results, failedServers);
|
|
518
|
+
}
|
|
519
|
+
async compileServersParallel(filteredEntries, config, constitutionHash, buildUnit, llmSemaphore, serverLimit) {
|
|
520
|
+
// Lazy import to avoid loading parallel-progress.ts when not needed
|
|
521
|
+
const { ParallelProgressDisplay, ParallelProgressReporter } = await import('./parallel-progress.js');
|
|
522
|
+
const serverNames = filteredEntries.map(([name]) => name);
|
|
523
|
+
const display = new ParallelProgressDisplay(serverNames);
|
|
524
|
+
const settled = await Promise.allSettled(filteredEntries.map(([serverName]) => serverLimit(async () => {
|
|
525
|
+
const { model, logContext } = this.createServerModel(serverName, llmSemaphore);
|
|
526
|
+
const reporter = new ParallelProgressReporter(display, serverName);
|
|
527
|
+
try {
|
|
528
|
+
return await this.compileServer(buildUnit(serverName), config, constitutionHash, model, logContext, reporter);
|
|
529
|
+
}
|
|
530
|
+
catch (err) {
|
|
531
|
+
reporter.fail('compiling', toError(err));
|
|
532
|
+
throw err;
|
|
533
|
+
}
|
|
534
|
+
})));
|
|
535
|
+
display.finish();
|
|
536
|
+
const results = [];
|
|
537
|
+
const failedServers = [];
|
|
538
|
+
for (const [i, outcome] of settled.entries()) {
|
|
539
|
+
if (outcome.status === 'fulfilled') {
|
|
540
|
+
results.push(outcome.value);
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
const serverName = filteredEntries[i][0];
|
|
544
|
+
failedServers.push(serverName);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return this.handleCompilationResults(results, failedServers);
|
|
548
|
+
}
|
|
549
|
+
handleCompilationResults(results, failedServers) {
|
|
438
550
|
if (failedServers.length > 0) {
|
|
439
551
|
console.error(`\n${chalk.yellow(`Warning: ${failedServers.length} server(s) failed verification: ${failedServers.join(', ')}`)}`);
|
|
440
552
|
console.error(chalk.yellow('Continuing with successfully compiled servers.'));
|
|
@@ -448,8 +560,12 @@ export class PipelineRunner {
|
|
|
448
560
|
/**
|
|
449
561
|
* Compiles a single server: compile -> generate scenarios -> verify -> repair.
|
|
450
562
|
* Self-contained compile-verify-repair loop per server.
|
|
563
|
+
*
|
|
564
|
+
* Each server receives its own model (with per-server logging middleware),
|
|
565
|
+
* logContext (for step labeling), and reporter (for progress/warnings).
|
|
566
|
+
* This makes the method safe for concurrent execution.
|
|
451
567
|
*/
|
|
452
|
-
async compileServer(unit, config, constitutionHash) {
|
|
568
|
+
async compileServer(unit, config, constitutionHash, model, logContext, reporter) {
|
|
453
569
|
const serverOutputDir = resolve(config.outputDir, 'servers', unit.serverName);
|
|
454
570
|
// Build per-server system prompt (only this server's annotations)
|
|
455
571
|
const compilerPrompt = unit.constitutionKind === 'task-policy'
|
|
@@ -460,13 +576,12 @@ export class PipelineRunner {
|
|
|
460
576
|
const existingServerPolicy = loadExistingArtifact(serverOutputDir, 'compiled-policy.json');
|
|
461
577
|
const existingServerScenarios = loadExistingArtifact(serverOutputDir, 'test-scenarios.json');
|
|
462
578
|
if (existingServerPolicy && existingServerPolicy.inputHash === inputHash && existingServerScenarios) {
|
|
463
|
-
// Verify scenario hash to detect stale scenarios (e.g., changed templates or handwritten scenarios)
|
|
464
579
|
const cachedPermittedDirs = extractPermittedDirectories(resolveRulePaths(existingServerPolicy.rules));
|
|
465
580
|
const cachedDynamicLists = loadExistingArtifact(serverOutputDir, 'dynamic-lists.json');
|
|
466
581
|
const cachedScenarioPrompt = buildGeneratorSystemPrompt(unit.constitutionText, unit.annotations, unit.allowedDirectory, cachedPermittedDirs, cachedDynamicLists, unit.storedAnnotations);
|
|
467
582
|
const cachedScenarioHash = computeScenariosHash(cachedScenarioPrompt, unit.handwrittenScenarios);
|
|
468
583
|
if (existingServerScenarios.inputHash === cachedScenarioHash) {
|
|
469
|
-
|
|
584
|
+
reporter.complete('cached', ` ${unit.serverName}: compilation ${chalk.dim('(cached)')}`, 0);
|
|
470
585
|
return {
|
|
471
586
|
serverName: unit.serverName,
|
|
472
587
|
rules: resolveRulePaths(existingServerPolicy.rules),
|
|
@@ -480,20 +595,19 @@ export class PipelineRunner {
|
|
|
480
595
|
}
|
|
481
596
|
const compilerSystem = this.cacheStrategy.wrapSystemPrompt(compilerPrompt);
|
|
482
597
|
// Step 1: Compile rules for this server
|
|
483
|
-
|
|
484
|
-
const compileResult = await this.compileServerPolicyRules(unit, compilerSystem, inputHash);
|
|
598
|
+
logContext.stepName = `compile-${unit.serverName}`;
|
|
599
|
+
const compileResult = await this.compileServerPolicyRules(unit, compilerSystem, inputHash, model, reporter);
|
|
485
600
|
let { rules } = compileResult;
|
|
486
601
|
let { listDefinitions } = compileResult;
|
|
487
|
-
let compilerSession = compileResult.session;
|
|
488
602
|
// Validate server scoping (debug assertion)
|
|
489
603
|
validateServerScoping(unit.serverName, rules);
|
|
490
604
|
// Resolve dynamic lists for this server (before scenario generation so lists are available)
|
|
491
605
|
let dynamicLists;
|
|
492
606
|
if (listDefinitions.length > 0) {
|
|
493
|
-
dynamicLists = await this.resolveServerLists(listDefinitions, serverOutputDir, config, ` ${unit.serverName}
|
|
607
|
+
dynamicLists = await this.resolveServerLists(listDefinitions, serverOutputDir, config, ` ${unit.serverName}`, model, logContext, reporter);
|
|
494
608
|
}
|
|
495
609
|
// Build per-server policy artifact for engine construction
|
|
496
|
-
|
|
610
|
+
const serverPolicyFile = buildPolicyArtifact(constitutionHash, rules, listDefinitions, inputHash);
|
|
497
611
|
// Persist early so intermediate results survive scenario/verification failures
|
|
498
612
|
const writeServerPolicy = () => writeArtifact(serverOutputDir, 'compiled-policy.json', {
|
|
499
613
|
generatedAt: new Date().toISOString(),
|
|
@@ -514,143 +628,18 @@ export class PipelineRunner {
|
|
|
514
628
|
? extractServerDomainAllowlists({ [unit.serverName]: unit.mcpServerConfig })
|
|
515
629
|
: undefined;
|
|
516
630
|
// Step 2: Generate test scenarios for this server
|
|
517
|
-
|
|
631
|
+
logContext.stepName = `scenarios-${unit.serverName}`;
|
|
518
632
|
const permittedDirectories = extractPermittedDirectories(rules);
|
|
519
633
|
const scenarioPrompt = buildGeneratorSystemPrompt(unit.constitutionText, unit.annotations, unit.allowedDirectory, permittedDirectories, dynamicLists, unit.storedAnnotations);
|
|
520
634
|
const scenarioHash = computeScenariosHash(scenarioPrompt, unit.handwrittenScenarios);
|
|
521
|
-
const scenarioResult = await this.generateTestScenarios(unit.constitutionText, unit.annotations, unit.allowedDirectory, unit.handwrittenScenarios, scenarioHash, existingServerScenarios, ` ${unit.serverName}`, permittedDirectories, dynamicLists, unit.storedAnnotations);
|
|
522
|
-
// Step 3: Verify
|
|
523
|
-
this.
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
if (discardedScenarios.length > 0) {
|
|
528
|
-
const discardedForRepair = discardedScenarios.map((d) => ({
|
|
529
|
-
scenario: d.scenario,
|
|
530
|
-
feedback: `${d.rule} always returns ${d.actual}`,
|
|
531
|
-
}));
|
|
532
|
-
this.logContext.stepName = `repair-scenarios-${unit.serverName}`;
|
|
533
|
-
const replacementScenarios = await repairScenarios(discardedForRepair, unit.constitutionText, unit.annotations, unit.allowedDirectory, this.model, permittedDirectories, dynamicLists, (msg) => console.error(` ${chalk.dim(msg)}`), unit.storedAnnotations);
|
|
534
|
-
if (replacementScenarios.length > 0) {
|
|
535
|
-
// Filter replacements through structural invariants too
|
|
536
|
-
const { valid: validReplacements } = filterAndLogStructuralConflicts(filterEngine, replacementScenarios, 'Discarded replacement (structural conflict)', unit.storedAnnotations);
|
|
537
|
-
scenarioResult.scenarios.push(...validReplacements);
|
|
538
|
-
filteredScenarios.push(...validReplacements);
|
|
539
|
-
console.error(` ${chalk.dim(`Repaired ${discardedScenarios.length} discarded scenario(s) → ${validReplacements.length} replacement(s)`)}`);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
const serverToolNames = [...new Set(unit.annotations.map((a) => a.toolName))];
|
|
543
|
-
const serverNames = [unit.serverName];
|
|
544
|
-
const serverTools = unit.annotations.map((a) => ({ serverName: a.serverName, toolName: a.toolName }));
|
|
545
|
-
let verifierSystem = this.cacheStrategy.wrapSystemPrompt(buildJudgeSystemPrompt(unit.constitutionText, serverPolicyFile, unit.protectedPaths, serverTools, dynamicLists, unit.allowedDirectory, unit.storedAnnotations));
|
|
546
|
-
let verifierSession = new PolicyVerifierSession({
|
|
547
|
-
system: verifierSystem,
|
|
548
|
-
model: this.model,
|
|
549
|
-
serverNames,
|
|
550
|
-
toolNames: serverToolNames,
|
|
551
|
-
storedAnnotations: unit.storedAnnotations,
|
|
552
|
-
});
|
|
553
|
-
const verifyLabel = ` ${unit.serverName}: Verifying`;
|
|
554
|
-
const { result: verificationResultInitial } = await withSpinner(verifyLabel, async (spinner) => verifyPolicy(unit.constitutionText, serverPolicyFile, serverAnnotationsFile, unit.protectedPaths, filteredScenarios, this.model, 3, unit.allowedDirectory, (msg) => {
|
|
555
|
-
spinner.text = `${verifyLabel} — ${msg}`;
|
|
556
|
-
}, serverDomainAllowlists, dynamicLists, verifierSystem, verifierSession, unit.storedAnnotations), (r, elapsed) => r.pass
|
|
557
|
-
? `${verifyLabel}: ${r.rounds.length} round(s) (${elapsed.toFixed(1)}s)`
|
|
558
|
-
: `${verifyLabel}: completed with failures (${elapsed.toFixed(1)}s)`);
|
|
559
|
-
let verificationResult = verificationResultInitial;
|
|
560
|
-
if (!verificationResult.pass) {
|
|
561
|
-
this.logVerboseFailures(verificationResult);
|
|
562
|
-
}
|
|
563
|
-
// Collect probe scenarios
|
|
564
|
-
const { valid: filteredInitialProbes } = filterAndLogStructuralConflicts(filterEngine, collectProbeScenarios(verificationResult), 'Discarded probe (structural conflict)', unit.storedAnnotations);
|
|
565
|
-
const accumulatedProbes = filteredInitialProbes;
|
|
566
|
-
// Compile-verify-repair loop (up to 2 repair attempts)
|
|
567
|
-
const MAX_REPAIRS = 2;
|
|
568
|
-
let repairAttempts = 0;
|
|
569
|
-
let currentFilteredScenarios;
|
|
570
|
-
if (!verificationResult.pass) {
|
|
571
|
-
const baseInputHash = inputHash;
|
|
572
|
-
for (let attempt = 1; attempt <= MAX_REPAIRS; attempt++) {
|
|
573
|
-
console.error('');
|
|
574
|
-
const lastRound = verificationResult.rounds[verificationResult.rounds.length - 1];
|
|
575
|
-
const judgeAnalysis = lastRound?.llmAnalysis ?? verificationResult.summary;
|
|
576
|
-
const attributedFailures = lastRound?.attributedFailures ?? [];
|
|
577
|
-
const allScenarios = [...scenarioResult.scenarios, ...accumulatedProbes];
|
|
578
|
-
const { corrections, handwrittenWarnings } = extractScenarioCorrections(attributedFailures, allScenarios);
|
|
579
|
-
for (const warning of handwrittenWarnings) {
|
|
580
|
-
console.error(` ${chalk.yellow('Warning:')} ${warning}`);
|
|
581
|
-
}
|
|
582
|
-
if (corrections.length > 0) {
|
|
583
|
-
scenarioResult.scenarios = applyScenarioCorrections(scenarioResult.scenarios, corrections);
|
|
584
|
-
const correctedProbes = applyScenarioCorrections(accumulatedProbes, corrections);
|
|
585
|
-
accumulatedProbes.splice(0, accumulatedProbes.length, ...correctedProbes);
|
|
586
|
-
console.error(` ${chalk.dim(`Corrected ${corrections.length} scenario expectation(s)`)}`);
|
|
587
|
-
}
|
|
588
|
-
({ valid: currentFilteredScenarios } = filterAndLogStructuralConflicts(filterEngine, scenarioResult.scenarios, undefined, unit.storedAnnotations));
|
|
589
|
-
const allRuleBlamedFailures = verificationResult.failedScenarios.filter((f) => {
|
|
590
|
-
const attr = attributedFailures.find((a) => a.scenarioDescription === f.scenario.description);
|
|
591
|
-
if (!attr || attr.blame.kind === 'rule' || attr.blame.kind === 'both')
|
|
592
|
-
return true;
|
|
593
|
-
return handwrittenWarnings.some((w) => w.includes(f.scenario.description));
|
|
594
|
-
});
|
|
595
|
-
if (allRuleBlamedFailures.length > 0) {
|
|
596
|
-
const repairContext = {
|
|
597
|
-
failedScenarios: allRuleBlamedFailures,
|
|
598
|
-
judgeAnalysis,
|
|
599
|
-
attemptNumber: attempt,
|
|
600
|
-
existingListDefinitions: listDefinitions.length > 0 ? listDefinitions : undefined,
|
|
601
|
-
handwrittenScenarios: unit.handwrittenScenarios.length > 0 ? unit.handwrittenScenarios : undefined,
|
|
602
|
-
};
|
|
603
|
-
this.logContext.stepName = `repair-compile-${unit.serverName}-${attempt}`;
|
|
604
|
-
const repairText = ` ${unit.serverName} repair ${attempt}/${MAX_REPAIRS}: Recompiling`;
|
|
605
|
-
const { result: repairResult } = await withSpinner(repairText, async (spinner) => this.compilePolicyRulesWithPointFix(rules, unit.annotations, unit.protectedPaths, baseInputHash, repairContext, compilerSystem, compilerSession, listDefinitions, (msg) => {
|
|
606
|
-
spinner.text = `${repairText} — ${msg}`;
|
|
607
|
-
}), (r, elapsed) => `${repairText}: ${r.rules.length} rules (${elapsed.toFixed(1)}s)`);
|
|
608
|
-
rules = repairResult.rules;
|
|
609
|
-
listDefinitions = repairResult.listDefinitions;
|
|
610
|
-
compilerSession = repairResult.session;
|
|
611
|
-
// Re-validate server scoping after repair
|
|
612
|
-
validateServerScoping(unit.serverName, rules);
|
|
613
|
-
// Re-resolve dynamic lists if repair changed list definitions
|
|
614
|
-
if (listDefinitions.length > 0) {
|
|
615
|
-
dynamicLists = await this.resolveServerLists(listDefinitions, serverOutputDir, config, ` ${unit.serverName}`);
|
|
616
|
-
}
|
|
617
|
-
serverPolicyFile = buildPolicyArtifact(constitutionHash, rules, listDefinitions, inputHash);
|
|
618
|
-
// Persist repaired rules immediately
|
|
619
|
-
writeServerPolicy();
|
|
620
|
-
verifierSystem = this.cacheStrategy.wrapSystemPrompt(buildJudgeSystemPrompt(unit.constitutionText, serverPolicyFile, unit.protectedPaths, serverTools, dynamicLists, unit.allowedDirectory, unit.storedAnnotations));
|
|
621
|
-
verifierSession = new PolicyVerifierSession({
|
|
622
|
-
system: verifierSystem,
|
|
623
|
-
model: this.model,
|
|
624
|
-
serverNames,
|
|
625
|
-
toolNames: serverToolNames,
|
|
626
|
-
storedAnnotations: unit.storedAnnotations,
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
else {
|
|
630
|
-
console.error(` ${chalk.dim('No rule-blamed failures — skipping recompilation')}`);
|
|
631
|
-
}
|
|
632
|
-
this.logContext.stepName = `repair-verify-${unit.serverName}-${attempt}`;
|
|
633
|
-
const scenariosForRepairVerify = [...currentFilteredScenarios, ...accumulatedProbes];
|
|
634
|
-
const repairVerifyText = ` ${unit.serverName} repair ${attempt}/${MAX_REPAIRS}: Verifying`;
|
|
635
|
-
const { result: repairVerifyResult } = await withSpinner(repairVerifyText, async (spinner) => verifyPolicy(unit.constitutionText, serverPolicyFile, serverAnnotationsFile, unit.protectedPaths, scenariosForRepairVerify, this.model, 1, unit.allowedDirectory, (msg) => {
|
|
636
|
-
spinner.text = `${repairVerifyText} — ${msg}`;
|
|
637
|
-
}, serverDomainAllowlists, dynamicLists, verifierSystem, verifierSession, unit.storedAnnotations), (r, elapsed) => r.pass
|
|
638
|
-
? `${repairVerifyText}: passed (${elapsed.toFixed(1)}s)`
|
|
639
|
-
: `${repairVerifyText}: ${r.failedScenarios.length} failure(s) (${elapsed.toFixed(1)}s)`);
|
|
640
|
-
verificationResult = repairVerifyResult;
|
|
641
|
-
if (!verificationResult.pass) {
|
|
642
|
-
this.logVerboseFailures(verificationResult);
|
|
643
|
-
}
|
|
644
|
-
const { valid: validRepairProbes } = filterAndLogStructuralConflicts(filterEngine, collectProbeScenarios(verificationResult), 'Discarded probe (structural conflict)', unit.storedAnnotations);
|
|
645
|
-
accumulatedProbes.push(...validRepairProbes);
|
|
646
|
-
repairAttempts = attempt;
|
|
647
|
-
if (verificationResult.pass) {
|
|
648
|
-
break;
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
}
|
|
635
|
+
const scenarioResult = await this.generateTestScenarios(unit.constitutionText, unit.annotations, unit.allowedDirectory, unit.handwrittenScenarios, scenarioHash, existingServerScenarios, ` ${unit.serverName}`, permittedDirectories, dynamicLists, unit.storedAnnotations, model, reporter);
|
|
636
|
+
// Step 3: Verify and repair
|
|
637
|
+
const state = await this.verifyAndRepairServer(unit, config, constitutionHash, inputHash, serverPolicyFile, serverAnnotationsFile, serverDomainAllowlists, dynamicLists, rules, listDefinitions, compileResult.session, scenarioResult, permittedDirectories, compilerSystem, model, logContext, reporter, serverOutputDir, writeServerPolicy);
|
|
638
|
+
// Sync repaired state back so the writeServerPolicy closure sees updated values
|
|
639
|
+
rules = state.rules;
|
|
640
|
+
listDefinitions = state.listDefinitions;
|
|
652
641
|
// Write per-server artifacts (before verification check so they can be inspected on failure)
|
|
653
|
-
const finalScenarios = [...scenarioResult.scenarios, ...accumulatedProbes];
|
|
642
|
+
const finalScenarios = [...scenarioResult.scenarios, ...state.accumulatedProbes];
|
|
654
643
|
writeServerPolicy();
|
|
655
644
|
writeArtifact(serverOutputDir, 'test-scenarios.json', {
|
|
656
645
|
generatedAt: new Date().toISOString(),
|
|
@@ -658,44 +647,205 @@ export class PipelineRunner {
|
|
|
658
647
|
inputHash: scenarioResult.inputHash,
|
|
659
648
|
scenarios: finalScenarios,
|
|
660
649
|
});
|
|
661
|
-
if (!verificationResult.pass) {
|
|
650
|
+
if (!state.verificationResult.pass) {
|
|
662
651
|
throw new Error(`Verification FAILED for server "${unit.serverName}" — artifacts written for inspection but policy may need review.`);
|
|
663
652
|
}
|
|
664
|
-
|
|
665
|
-
(repairAttempts > 0 ? `, ${repairAttempts} repair(s)` : '')
|
|
653
|
+
const summary = `${state.rules.length} rules, ${finalScenarios.length} scenarios` +
|
|
654
|
+
(state.repairAttempts > 0 ? `, ${state.repairAttempts} repair(s)` : '');
|
|
655
|
+
reporter.done(summary);
|
|
666
656
|
return {
|
|
667
657
|
serverName: unit.serverName,
|
|
668
|
-
rules,
|
|
669
|
-
listDefinitions,
|
|
658
|
+
rules: state.rules,
|
|
659
|
+
listDefinitions: state.listDefinitions,
|
|
670
660
|
scenarios: finalScenarios,
|
|
671
661
|
inputHash,
|
|
672
662
|
constitutionHash,
|
|
673
|
-
resolvedLists: dynamicLists,
|
|
663
|
+
resolvedLists: state.dynamicLists,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Runs initial verification and the compile-verify-repair loop for a server.
|
|
668
|
+
* Returns the mutable compilation state after all repair attempts.
|
|
669
|
+
*/
|
|
670
|
+
async verifyAndRepairServer(unit, config, constitutionHash, inputHash, serverPolicyFile, serverAnnotationsFile, serverDomainAllowlists, dynamicLists, rules, listDefinitions, compilerSession, scenarioResult, permittedDirectories, compilerSystem, model, logContext, reporter, serverOutputDir, writeServerPolicy) {
|
|
671
|
+
logContext.stepName = `verify-${unit.serverName}`;
|
|
672
|
+
const filterEngine = new PolicyEngine(serverPolicyFile, serverAnnotationsFile, unit.protectedPaths, unit.allowedDirectory, undefined, dynamicLists);
|
|
673
|
+
const { filteredScenarios } = await this.filterAndRepairDiscardedScenarios(filterEngine, scenarioResult, unit, permittedDirectories, dynamicLists, model, logContext, reporter);
|
|
674
|
+
const serverToolNames = [...new Set(unit.annotations.map((a) => a.toolName))];
|
|
675
|
+
const serverNames = [unit.serverName];
|
|
676
|
+
const serverTools = unit.annotations.map((a) => ({ serverName: a.serverName, toolName: a.toolName }));
|
|
677
|
+
const verifierSystem = this.cacheStrategy.wrapSystemPrompt(buildJudgeSystemPrompt(unit.constitutionText, serverPolicyFile, unit.protectedPaths, serverTools, dynamicLists, unit.allowedDirectory, unit.storedAnnotations));
|
|
678
|
+
const verifierSession = new PolicyVerifierSession({
|
|
679
|
+
system: verifierSystem,
|
|
680
|
+
model,
|
|
681
|
+
serverNames,
|
|
682
|
+
toolNames: serverToolNames,
|
|
683
|
+
storedAnnotations: unit.storedAnnotations,
|
|
684
|
+
});
|
|
685
|
+
reporter.update('verifying');
|
|
686
|
+
const verifyStart = Date.now();
|
|
687
|
+
const verificationResultInitial = await verifyPolicy(unit.constitutionText, serverPolicyFile, serverAnnotationsFile, unit.protectedPaths, filteredScenarios, model, 3, unit.allowedDirectory, (msg) => reporter.update('verifying', msg), serverDomainAllowlists, dynamicLists, verifierSystem, verifierSession, unit.storedAnnotations);
|
|
688
|
+
const verifyElapsed = (Date.now() - verifyStart) / 1000;
|
|
689
|
+
const verifyLabel = ` ${unit.serverName}: Verifying`;
|
|
690
|
+
reporter.complete('verifying', verificationResultInitial.pass
|
|
691
|
+
? `${verifyLabel}: ${verificationResultInitial.rounds.length} round(s) (${verifyElapsed.toFixed(1)}s)`
|
|
692
|
+
: `${verifyLabel}: completed with failures (${verifyElapsed.toFixed(1)}s)`, verifyElapsed);
|
|
693
|
+
if (!verificationResultInitial.pass) {
|
|
694
|
+
this.logVerboseFailures(verificationResultInitial, reporter);
|
|
695
|
+
}
|
|
696
|
+
// Collect probe scenarios
|
|
697
|
+
const { valid: filteredInitialProbes } = filterAndLogStructuralConflicts(filterEngine, collectProbeScenarios(verificationResultInitial), 'Discarded probe (structural conflict)', unit.storedAnnotations, reporter);
|
|
698
|
+
const state = {
|
|
699
|
+
rules,
|
|
700
|
+
listDefinitions,
|
|
701
|
+
compilerSession,
|
|
702
|
+
dynamicLists,
|
|
703
|
+
serverPolicyFile,
|
|
704
|
+
verificationResult: verificationResultInitial,
|
|
705
|
+
accumulatedProbes: filteredInitialProbes,
|
|
706
|
+
repairAttempts: 0,
|
|
674
707
|
};
|
|
708
|
+
if (!state.verificationResult.pass) {
|
|
709
|
+
await this.runRepairLoop(state, unit, config, constitutionHash, inputHash, serverAnnotationsFile, serverDomainAllowlists, scenarioResult, filterEngine, serverToolNames, serverNames, serverTools, compilerSystem, model, logContext, reporter, serverOutputDir, writeServerPolicy);
|
|
710
|
+
}
|
|
711
|
+
return state;
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Filters structurally conflicting scenarios and generates replacements
|
|
715
|
+
* for discarded ones via LLM repair (pre-loop, one-time).
|
|
716
|
+
*/
|
|
717
|
+
async filterAndRepairDiscardedScenarios(filterEngine, scenarioResult, unit, permittedDirectories, dynamicLists, model, logContext, reporter) {
|
|
718
|
+
const { valid: filteredScenarios, discarded: discardedScenarios } = filterAndLogStructuralConflicts(filterEngine, scenarioResult.scenarios, undefined, unit.storedAnnotations, reporter);
|
|
719
|
+
if (discardedScenarios.length > 0) {
|
|
720
|
+
const discardedForRepair = discardedScenarios.map((d) => ({
|
|
721
|
+
scenario: d.scenario,
|
|
722
|
+
feedback: `${d.rule} always returns ${d.actual}`,
|
|
723
|
+
}));
|
|
724
|
+
logContext.stepName = `repair-scenarios-${unit.serverName}`;
|
|
725
|
+
reporter.update('repair-scenarios');
|
|
726
|
+
const repairStart = Date.now();
|
|
727
|
+
const replacementScenarios = await repairScenarios(discardedForRepair, unit.constitutionText, unit.annotations, unit.allowedDirectory, model, permittedDirectories, dynamicLists, (msg) => reporter.warn(` ${chalk.dim(msg)}`), unit.storedAnnotations);
|
|
728
|
+
if (replacementScenarios.length > 0) {
|
|
729
|
+
const { valid: validReplacements } = filterAndLogStructuralConflicts(filterEngine, replacementScenarios, 'Discarded replacement (structural conflict)', unit.storedAnnotations, reporter);
|
|
730
|
+
scenarioResult.scenarios.push(...validReplacements);
|
|
731
|
+
filteredScenarios.push(...validReplacements);
|
|
732
|
+
reporter.warn(` ${chalk.dim(`Repaired ${discardedScenarios.length} discarded scenario(s) → ${validReplacements.length} replacement(s)`)}`);
|
|
733
|
+
}
|
|
734
|
+
const repairElapsed = (Date.now() - repairStart) / 1000;
|
|
735
|
+
reporter.complete('repair-scenarios', ` ${unit.serverName}: Repaired scenarios (${repairElapsed.toFixed(1)}s)`, repairElapsed);
|
|
736
|
+
}
|
|
737
|
+
return { filteredScenarios };
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Compile-verify-repair loop (up to 2 repair attempts).
|
|
741
|
+
* Mutates `state` in place with updated rules, lists, and verification results.
|
|
742
|
+
*/
|
|
743
|
+
async runRepairLoop(state, unit, config, constitutionHash, inputHash, serverAnnotationsFile, serverDomainAllowlists, scenarioResult, filterEngine, serverToolNames, serverNames, serverTools, compilerSystem, model, logContext, reporter, serverOutputDir, writeServerPolicy) {
|
|
744
|
+
const MAX_REPAIRS = 2;
|
|
745
|
+
for (let attempt = 1; attempt <= MAX_REPAIRS; attempt++) {
|
|
746
|
+
const lastRound = state.verificationResult.rounds[state.verificationResult.rounds.length - 1];
|
|
747
|
+
const judgeAnalysis = lastRound?.llmAnalysis ?? state.verificationResult.summary;
|
|
748
|
+
const attributedFailures = lastRound?.attributedFailures ?? [];
|
|
749
|
+
const allScenarios = [...scenarioResult.scenarios, ...state.accumulatedProbes];
|
|
750
|
+
const { corrections, handwrittenWarnings } = extractScenarioCorrections(attributedFailures, allScenarios);
|
|
751
|
+
for (const warning of handwrittenWarnings) {
|
|
752
|
+
reporter.warn(` ${chalk.yellow('Warning:')} ${warning}`);
|
|
753
|
+
}
|
|
754
|
+
if (corrections.length > 0) {
|
|
755
|
+
scenarioResult.scenarios = applyScenarioCorrections(scenarioResult.scenarios, corrections);
|
|
756
|
+
const correctedProbes = applyScenarioCorrections(state.accumulatedProbes, corrections);
|
|
757
|
+
state.accumulatedProbes.splice(0, state.accumulatedProbes.length, ...correctedProbes);
|
|
758
|
+
reporter.warn(` ${chalk.dim(`Corrected ${corrections.length} scenario expectation(s)`)}`);
|
|
759
|
+
}
|
|
760
|
+
const { valid: currentFilteredScenarios } = filterAndLogStructuralConflicts(filterEngine, scenarioResult.scenarios, undefined, unit.storedAnnotations, reporter);
|
|
761
|
+
const allRuleBlamedFailures = state.verificationResult.failedScenarios.filter((f) => {
|
|
762
|
+
const attr = attributedFailures.find((a) => a.scenarioDescription === f.scenario.description);
|
|
763
|
+
if (!attr || attr.blame.kind === 'rule' || attr.blame.kind === 'both')
|
|
764
|
+
return true;
|
|
765
|
+
return handwrittenWarnings.some((w) => w.includes(f.scenario.description));
|
|
766
|
+
});
|
|
767
|
+
if (allRuleBlamedFailures.length > 0) {
|
|
768
|
+
const repairContext = {
|
|
769
|
+
failedScenarios: allRuleBlamedFailures,
|
|
770
|
+
judgeAnalysis,
|
|
771
|
+
attemptNumber: attempt,
|
|
772
|
+
existingListDefinitions: state.listDefinitions.length > 0 ? state.listDefinitions : undefined,
|
|
773
|
+
handwrittenScenarios: unit.handwrittenScenarios.length > 0 ? unit.handwrittenScenarios : undefined,
|
|
774
|
+
};
|
|
775
|
+
logContext.stepName = `repair-compile-${unit.serverName}-${attempt}`;
|
|
776
|
+
reporter.update('repair-compile', `attempt ${attempt}/${MAX_REPAIRS}`);
|
|
777
|
+
const repairCompileStart = Date.now();
|
|
778
|
+
const repairResult = await this.compilePolicyRulesWithPointFix(state.rules, unit.annotations, unit.protectedPaths, inputHash, repairContext, compilerSystem, state.compilerSession, state.listDefinitions, model, (msg) => reporter.update('repair-compile', msg), reporter);
|
|
779
|
+
const repairCompileElapsed = (Date.now() - repairCompileStart) / 1000;
|
|
780
|
+
reporter.complete('repair-compile', ` ${unit.serverName} repair ${attempt}/${MAX_REPAIRS}: ${repairResult.rules.length} rules (${repairCompileElapsed.toFixed(1)}s)`, repairCompileElapsed);
|
|
781
|
+
state.rules = repairResult.rules;
|
|
782
|
+
state.listDefinitions = repairResult.listDefinitions;
|
|
783
|
+
state.compilerSession = repairResult.session;
|
|
784
|
+
// Re-validate server scoping after repair
|
|
785
|
+
validateServerScoping(unit.serverName, state.rules);
|
|
786
|
+
// Re-resolve dynamic lists if repair changed list definitions
|
|
787
|
+
if (state.listDefinitions.length > 0) {
|
|
788
|
+
state.dynamicLists = await this.resolveServerLists(state.listDefinitions, serverOutputDir, config, ` ${unit.serverName}`, model, logContext, reporter);
|
|
789
|
+
}
|
|
790
|
+
state.serverPolicyFile = buildPolicyArtifact(constitutionHash, state.rules, state.listDefinitions, inputHash);
|
|
791
|
+
// Persist repaired rules immediately
|
|
792
|
+
writeServerPolicy();
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
reporter.warn(` ${chalk.dim('No rule-blamed failures — skipping recompilation')}`);
|
|
796
|
+
}
|
|
797
|
+
// Build verifier from current state (may have been updated by repair above)
|
|
798
|
+
const verifierSystem = this.cacheStrategy.wrapSystemPrompt(buildJudgeSystemPrompt(unit.constitutionText, state.serverPolicyFile, unit.protectedPaths, serverTools, state.dynamicLists, unit.allowedDirectory, unit.storedAnnotations));
|
|
799
|
+
const verifierSession = new PolicyVerifierSession({
|
|
800
|
+
system: verifierSystem,
|
|
801
|
+
model,
|
|
802
|
+
serverNames,
|
|
803
|
+
toolNames: serverToolNames,
|
|
804
|
+
storedAnnotations: unit.storedAnnotations,
|
|
805
|
+
});
|
|
806
|
+
logContext.stepName = `repair-verify-${unit.serverName}-${attempt}`;
|
|
807
|
+
const scenariosForRepairVerify = [...currentFilteredScenarios, ...state.accumulatedProbes];
|
|
808
|
+
reporter.update('repair-verify', `attempt ${attempt}/${MAX_REPAIRS}`);
|
|
809
|
+
const repairVerifyStart = Date.now();
|
|
810
|
+
const repairVerifyResult = await verifyPolicy(unit.constitutionText, state.serverPolicyFile, serverAnnotationsFile, unit.protectedPaths, scenariosForRepairVerify, model, 1, unit.allowedDirectory, (msg) => reporter.update('repair-verify', msg), serverDomainAllowlists, state.dynamicLists, verifierSystem, verifierSession, unit.storedAnnotations);
|
|
811
|
+
const repairVerifyElapsed = (Date.now() - repairVerifyStart) / 1000;
|
|
812
|
+
const repairVerifyText = ` ${unit.serverName} repair ${attempt}/${MAX_REPAIRS}: Verifying`;
|
|
813
|
+
reporter.complete('repair-verify', repairVerifyResult.pass
|
|
814
|
+
? `${repairVerifyText}: passed (${repairVerifyElapsed.toFixed(1)}s)`
|
|
815
|
+
: `${repairVerifyText}: ${repairVerifyResult.failedScenarios.length} failure(s) (${repairVerifyElapsed.toFixed(1)}s)`, repairVerifyElapsed);
|
|
816
|
+
state.verificationResult = repairVerifyResult;
|
|
817
|
+
if (!state.verificationResult.pass) {
|
|
818
|
+
this.logVerboseFailures(state.verificationResult, reporter);
|
|
819
|
+
}
|
|
820
|
+
const { valid: validRepairProbes } = filterAndLogStructuralConflicts(filterEngine, collectProbeScenarios(state.verificationResult), 'Discarded probe (structural conflict)', unit.storedAnnotations, reporter);
|
|
821
|
+
state.accumulatedProbes.push(...validRepairProbes);
|
|
822
|
+
state.repairAttempts = attempt;
|
|
823
|
+
if (state.verificationResult.pass) {
|
|
824
|
+
break;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
675
827
|
}
|
|
676
828
|
/**
|
|
677
829
|
* Compiles policy rules for a single server (initial compilation).
|
|
678
830
|
* Uses per-server schema with requireServer: true.
|
|
679
831
|
*/
|
|
680
|
-
async compileServerPolicyRules(unit, system, inputHash) {
|
|
681
|
-
|
|
832
|
+
async compileServerPolicyRules(unit, system, inputHash, model, reporter) {
|
|
833
|
+
reporter.update('compiling');
|
|
834
|
+
const start = Date.now();
|
|
682
835
|
const session = new ConstitutionCompilerSession({
|
|
683
836
|
system,
|
|
684
|
-
model
|
|
837
|
+
model,
|
|
685
838
|
annotations: unit.annotations,
|
|
686
839
|
schemaOptions: { requireServer: true },
|
|
687
840
|
});
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
validateRulesOrThrow(compiledRules, output.listDefinitions);
|
|
694
|
-
return { rules: compiledRules, listDefinitions: output.listDefinitions };
|
|
695
|
-
}, (output, elapsed) => `${stepText}: ${output.rules.length} rules (${elapsed.toFixed(1)}s)`);
|
|
841
|
+
const output = await session.compile((msg) => reporter.update('compiling', msg));
|
|
842
|
+
const compiledRules = resolveRulePaths(output.rules);
|
|
843
|
+
validateRulesOrThrow(compiledRules, output.listDefinitions, reporter);
|
|
844
|
+
const elapsed = (Date.now() - start) / 1000;
|
|
845
|
+
reporter.complete('compiling', ` ${unit.serverName}: Compiling rules: ${compiledRules.length} rules (${elapsed.toFixed(1)}s)`, elapsed);
|
|
696
846
|
return {
|
|
697
|
-
rules:
|
|
698
|
-
listDefinitions:
|
|
847
|
+
rules: compiledRules,
|
|
848
|
+
listDefinitions: output.listDefinitions,
|
|
699
849
|
inputHash,
|
|
700
850
|
session,
|
|
701
851
|
};
|
|
@@ -703,7 +853,7 @@ export class PipelineRunner {
|
|
|
703
853
|
// -----------------------------------------------------------------------
|
|
704
854
|
// Private compilation methods
|
|
705
855
|
// -----------------------------------------------------------------------
|
|
706
|
-
async compilePolicyRulesWithRepair(annotations, protectedPaths, baseInputHash, repairContext, system, session, onProgress) {
|
|
856
|
+
async compilePolicyRulesWithRepair(annotations, protectedPaths, baseInputHash, repairContext, system, session, model, onProgress, reporter) {
|
|
707
857
|
let output;
|
|
708
858
|
if (session) {
|
|
709
859
|
output = await session.recompile(repairContext, onProgress);
|
|
@@ -711,10 +861,10 @@ export class PipelineRunner {
|
|
|
711
861
|
else {
|
|
712
862
|
const { compileConstitution } = await import('./constitution-compiler.js');
|
|
713
863
|
output = await compileConstitution('', // Not needed when system prompt is provided
|
|
714
|
-
annotations, { protectedPaths },
|
|
864
|
+
annotations, { protectedPaths }, model, repairContext, onProgress, system);
|
|
715
865
|
}
|
|
716
866
|
const compiledRules = resolveRulePaths(output.rules);
|
|
717
|
-
validateRulesOrThrow(compiledRules, output.listDefinitions);
|
|
867
|
+
validateRulesOrThrow(compiledRules, output.listDefinitions, reporter);
|
|
718
868
|
return {
|
|
719
869
|
rules: compiledRules,
|
|
720
870
|
listDefinitions: output.listDefinitions,
|
|
@@ -728,17 +878,17 @@ export class PipelineRunner {
|
|
|
728
878
|
* patch instead of regenerating the entire rule set. This avoids oscillation
|
|
729
879
|
* where fixing one failure breaks previously-passing rules.
|
|
730
880
|
*/
|
|
731
|
-
async compilePolicyRulesWithPointFix(existingRules, annotations, protectedPaths, baseInputHash, repairContext, system, session, existingListDefinitions, onProgress) {
|
|
881
|
+
async compilePolicyRulesWithPointFix(existingRules, annotations, protectedPaths, baseInputHash, repairContext, system, session, existingListDefinitions, model, onProgress, reporter) {
|
|
732
882
|
let output;
|
|
733
883
|
if (session) {
|
|
734
884
|
output = await session.repairPointFix(existingRules, repairContext, existingListDefinitions, onProgress);
|
|
735
885
|
}
|
|
736
886
|
else {
|
|
737
887
|
// No session available -- fall back to full recompile
|
|
738
|
-
return this.compilePolicyRulesWithRepair(annotations, protectedPaths, baseInputHash, repairContext, system, session, onProgress);
|
|
888
|
+
return this.compilePolicyRulesWithRepair(annotations, protectedPaths, baseInputHash, repairContext, system, session, model, onProgress, reporter);
|
|
739
889
|
}
|
|
740
890
|
const compiledRules = resolveRulePaths(output.rules);
|
|
741
|
-
validateRulesOrThrow(compiledRules, output.listDefinitions);
|
|
891
|
+
validateRulesOrThrow(compiledRules, output.listDefinitions, reporter);
|
|
742
892
|
return {
|
|
743
893
|
rules: compiledRules,
|
|
744
894
|
listDefinitions: output.listDefinitions,
|
|
@@ -751,25 +901,24 @@ export class PipelineRunner {
|
|
|
751
901
|
* Loads existing per-server dynamic-lists.json for cache comparison,
|
|
752
902
|
* connects MCP servers if needed, and writes results to serverOutputDir.
|
|
753
903
|
*/
|
|
754
|
-
async resolveServerLists(listDefinitions, serverOutputDir, config, labelPrefix) {
|
|
755
|
-
|
|
904
|
+
async resolveServerLists(listDefinitions, serverOutputDir, config, labelPrefix, model, logContext, reporter) {
|
|
905
|
+
logContext.stepName = `resolve-lists-${labelPrefix.trim()}`;
|
|
756
906
|
const existingLists = loadExistingArtifact(serverOutputDir, 'dynamic-lists.json');
|
|
757
907
|
const needsMcp = listDefinitions.some((d) => d.requiresMcp);
|
|
758
908
|
let proxy;
|
|
759
909
|
if (needsMcp && config.mcpServers) {
|
|
760
910
|
proxy = await connectViaProxy(listDefinitions, config.mcpServers, config.toolAnnotationsDir);
|
|
761
911
|
}
|
|
762
|
-
|
|
912
|
+
reporter.update('lists');
|
|
913
|
+
const start = Date.now();
|
|
763
914
|
try {
|
|
764
|
-
const
|
|
765
|
-
model
|
|
915
|
+
const resolvedLists = await resolveAllLists(listDefinitions, {
|
|
916
|
+
model,
|
|
766
917
|
proxyConnection: proxy ? { client: proxy.client, tools: proxy.tools } : undefined,
|
|
767
|
-
}, existingLists, (msg) =>
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
return `${listStepText}: ${count} list(s) resolved (${elapsed.toFixed(1)}s)`;
|
|
772
|
-
});
|
|
918
|
+
}, existingLists, (msg) => reporter.update('lists', msg));
|
|
919
|
+
const elapsed = (Date.now() - start) / 1000;
|
|
920
|
+
const count = Object.keys(resolvedLists.lists).length;
|
|
921
|
+
reporter.complete('lists', `${labelPrefix}: Resolving dynamic lists: ${count} list(s) resolved (${elapsed.toFixed(1)}s)`, elapsed);
|
|
773
922
|
writeArtifact(serverOutputDir, 'dynamic-lists.json', resolvedLists);
|
|
774
923
|
return resolvedLists;
|
|
775
924
|
}
|
|
@@ -779,28 +928,28 @@ export class PipelineRunner {
|
|
|
779
928
|
}
|
|
780
929
|
}
|
|
781
930
|
}
|
|
782
|
-
async generateTestScenarios(constitutionText, annotations, allowedDirectory, handwrittenScenarios, inputHash, existingScenarios, stepLabel, permittedDirectories, dynamicLists, storedAnnotations) {
|
|
931
|
+
async generateTestScenarios(constitutionText, annotations, allowedDirectory, handwrittenScenarios, inputHash, existingScenarios, stepLabel, permittedDirectories, dynamicLists, storedAnnotations, model, reporter) {
|
|
783
932
|
const stepText = `${stepLabel} Generating test scenarios`;
|
|
784
933
|
if (existingScenarios && existingScenarios.inputHash === inputHash) {
|
|
785
|
-
|
|
934
|
+
reporter.complete('scenarios', `${stepText} ${chalk.dim('(cached)')}`, 0);
|
|
786
935
|
return { scenarios: existingScenarios.scenarios, inputHash };
|
|
787
936
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
});
|
|
937
|
+
reporter.update('scenarios');
|
|
938
|
+
const start = Date.now();
|
|
939
|
+
const scenarios = await generateScenarios(constitutionText, annotations, handwrittenScenarios, allowedDirectory, model, permittedDirectories, (msg) => reporter.update('scenarios', msg), dynamicLists, (prompt) => this.cacheStrategy.wrapSystemPrompt(prompt), storedAnnotations);
|
|
940
|
+
const elapsed = (Date.now() - start) / 1000;
|
|
941
|
+
const generatedCount = scenarios.length - handwrittenScenarios.length;
|
|
942
|
+
reporter.complete('scenarios', `${stepText}: ${scenarios.length} scenarios (${handwrittenScenarios.length} handwritten + ${generatedCount} generated) (${elapsed.toFixed(1)}s)`, elapsed);
|
|
794
943
|
return { scenarios, inputHash };
|
|
795
944
|
}
|
|
796
|
-
logVerboseFailures(result) {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
945
|
+
logVerboseFailures(result, reporter) {
|
|
946
|
+
reporter.warn('');
|
|
947
|
+
reporter.warn(chalk.red('Verification FAILED:'));
|
|
948
|
+
reporter.warn(result.summary);
|
|
949
|
+
reporter.warn('');
|
|
801
950
|
for (const f of result.failedScenarios) {
|
|
802
|
-
|
|
803
|
-
|
|
951
|
+
reporter.warn(` ${chalk.red('FAIL:')} ${f.scenario.description}`);
|
|
952
|
+
reporter.warn(` Expected: ${f.scenario.expectedDecision}, Got: ${f.actualDecision} (rule: ${f.matchingRule})`);
|
|
804
953
|
}
|
|
805
954
|
}
|
|
806
955
|
}
|