@provos/ironcurtain 0.5.1 → 0.7.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 +85 -66
- package/dist/cli-help.d.ts +63 -0
- package/dist/cli-help.js +102 -0
- package/dist/cli-help.js.map +1 -0
- package/dist/cli.js +78 -52
- package/dist/cli.js.map +1 -1
- package/dist/config/config-command.js +136 -2
- package/dist/config/config-command.js.map +1 -1
- package/dist/config/generated/tool-annotations.json +487 -338
- package/dist/config/index.d.ts +21 -1
- package/dist/config/index.js +12 -4
- package/dist/config/index.js.map +1 -1
- package/dist/config/mcp-servers.json +2 -8
- package/dist/config/paths.d.ts +30 -0
- package/dist/config/paths.js +58 -0
- package/dist/config/paths.js.map +1 -1
- package/dist/config/tool-description-hints.json +15 -0
- package/dist/config/types.d.ts +13 -0
- package/dist/config/user-config.d.ts +21 -0
- package/dist/config/user-config.js +8 -0
- package/dist/config/user-config.js.map +1 -1
- package/dist/cron/compile-task-policy.d.ts +17 -0
- package/dist/cron/compile-task-policy.js +39 -0
- package/dist/cron/compile-task-policy.js.map +1 -0
- package/dist/cron/cron-scheduler.d.ts +65 -0
- package/dist/cron/cron-scheduler.js +184 -0
- package/dist/cron/cron-scheduler.js.map +1 -0
- package/dist/cron/file-lock.d.ts +41 -0
- package/dist/cron/file-lock.js +156 -0
- package/dist/cron/file-lock.js.map +1 -0
- package/dist/cron/format-utils.d.ts +18 -0
- package/dist/cron/format-utils.js +145 -0
- package/dist/cron/format-utils.js.map +1 -0
- package/dist/cron/git-sync.d.ts +22 -0
- package/dist/cron/git-sync.js +121 -0
- package/dist/cron/git-sync.js.map +1 -0
- package/dist/cron/headless-transport.d.ts +27 -0
- package/dist/cron/headless-transport.js +43 -0
- package/dist/cron/headless-transport.js.map +1 -0
- package/dist/cron/job-commands.d.ts +39 -0
- package/dist/cron/job-commands.js +522 -0
- package/dist/cron/job-commands.js.map +1 -0
- package/dist/cron/job-store.d.ts +35 -0
- package/dist/cron/job-store.js +111 -0
- package/dist/cron/job-store.js.map +1 -0
- package/dist/cron/types.d.ts +127 -0
- package/dist/cron/types.js +25 -0
- package/dist/cron/types.js.map +1 -0
- package/dist/daemon/control-socket.d.ts +127 -0
- package/dist/daemon/control-socket.js +261 -0
- package/dist/daemon/control-socket.js.map +1 -0
- package/dist/daemon/daemon-command.d.ts +13 -0
- package/dist/daemon/daemon-command.js +230 -0
- package/dist/daemon/daemon-command.js.map +1 -0
- package/dist/daemon/ironcurtain-daemon.d.ts +90 -0
- package/dist/daemon/ironcurtain-daemon.js +548 -0
- package/dist/daemon/ironcurtain-daemon.js.map +1 -0
- package/dist/docker/adapters/claude-code.js +8 -48
- package/dist/docker/adapters/claude-code.js.map +1 -1
- package/dist/docker/adapters/goose.d.ts +58 -0
- package/dist/docker/adapters/goose.js +250 -0
- package/dist/docker/adapters/goose.js.map +1 -0
- package/dist/docker/adapters/shared-scripts.d.ts +29 -0
- package/dist/docker/adapters/shared-scripts.js +80 -0
- package/dist/docker/adapters/shared-scripts.js.map +1 -0
- package/dist/docker/agent-adapter.d.ts +12 -0
- package/dist/docker/agent-adapter.js.map +1 -1
- package/dist/docker/agent-registry.d.ts +6 -1
- package/dist/docker/agent-registry.js +10 -1
- package/dist/docker/agent-registry.js.map +1 -1
- package/dist/docker/docker-agent-session.d.ts +2 -25
- package/dist/docker/docker-agent-session.js +7 -106
- package/dist/docker/docker-agent-session.js.map +1 -1
- package/dist/docker/docker-infrastructure.d.ts +8 -0
- package/dist/docker/docker-infrastructure.js +22 -14
- package/dist/docker/docker-infrastructure.js.map +1 -1
- package/dist/docker/mitm-proxy.d.ts +5 -2
- package/dist/docker/mitm-proxy.js +169 -46
- package/dist/docker/mitm-proxy.js.map +1 -1
- package/dist/docker/oauth-credentials.d.ts +15 -2
- package/dist/docker/oauth-credentials.js +92 -8
- package/dist/docker/oauth-credentials.js.map +1 -1
- package/dist/docker/oauth-token-manager.d.ts +80 -0
- package/dist/docker/oauth-token-manager.js +133 -0
- package/dist/docker/oauth-token-manager.js.map +1 -0
- package/dist/docker/pty-session.d.ts +2 -0
- package/dist/docker/pty-session.js +17 -8
- package/dist/docker/pty-session.js.map +1 -1
- package/dist/escalation/escalation-watcher.d.ts +2 -0
- package/dist/escalation/escalation-watcher.js +5 -0
- package/dist/escalation/escalation-watcher.js.map +1 -1
- package/dist/escalation/listener-command.js +9 -58
- package/dist/escalation/listener-command.js.map +1 -1
- package/dist/escalation/listener-lock.d.ts +20 -0
- package/dist/escalation/listener-lock.js +69 -0
- package/dist/escalation/listener-lock.js.map +1 -0
- package/dist/escalation/session-registry.js +1 -14
- package/dist/escalation/session-registry.js.map +1 -1
- package/dist/index.js +51 -2
- package/dist/index.js.map +1 -1
- package/dist/mux/mux-app.d.ts +24 -0
- package/dist/mux/mux-app.js +450 -0
- package/dist/mux/mux-app.js.map +1 -0
- package/dist/mux/mux-command.d.ts +7 -0
- package/dist/mux/mux-command.js +104 -0
- package/dist/mux/mux-command.js.map +1 -0
- package/dist/mux/mux-escalation-manager.d.ts +48 -0
- package/dist/mux/mux-escalation-manager.js +162 -0
- package/dist/mux/mux-escalation-manager.js.map +1 -0
- package/dist/mux/mux-input-handler.d.ts +54 -0
- package/dist/mux/mux-input-handler.js +512 -0
- package/dist/mux/mux-input-handler.js.map +1 -0
- package/dist/mux/mux-renderer.d.ts +106 -0
- package/dist/mux/mux-renderer.js +850 -0
- package/dist/mux/mux-renderer.js.map +1 -0
- package/dist/mux/mux-splash.d.ts +17 -0
- package/dist/mux/mux-splash.js +323 -0
- package/dist/mux/mux-splash.js.map +1 -0
- package/dist/mux/paste-interceptor.d.ts +22 -0
- package/dist/mux/paste-interceptor.js +143 -0
- package/dist/mux/paste-interceptor.js.map +1 -0
- package/dist/mux/pty-bridge.d.ts +79 -0
- package/dist/mux/pty-bridge.js +151 -0
- package/dist/mux/pty-bridge.js.map +1 -0
- package/dist/mux/trusted-input.d.ts +27 -0
- package/dist/mux/trusted-input.js +27 -0
- package/dist/mux/trusted-input.js.map +1 -0
- package/dist/mux/types.d.ts +99 -0
- package/dist/mux/types.js +57 -0
- package/dist/mux/types.js.map +1 -0
- package/dist/pipeline/annotate.js +2 -1
- package/dist/pipeline/annotate.js.map +1 -1
- package/dist/pipeline/compile.d.ts +4 -34
- package/dist/pipeline/compile.js +29 -568
- package/dist/pipeline/compile.js.map +1 -1
- package/dist/pipeline/constitution-compiler.js +1 -1
- package/dist/pipeline/mcp-connections.d.ts +16 -0
- package/dist/pipeline/mcp-connections.js +61 -0
- package/dist/pipeline/mcp-connections.js.map +1 -0
- package/dist/pipeline/pipeline-runner.d.ts +86 -0
- package/dist/pipeline/pipeline-runner.js +554 -0
- package/dist/pipeline/pipeline-runner.js.map +1 -0
- package/dist/pipeline/pipeline-shared.d.ts +29 -2
- package/dist/pipeline/pipeline-shared.js +68 -12
- package/dist/pipeline/pipeline-shared.js.map +1 -1
- package/dist/pipeline/refresh-lists.js +38 -12
- package/dist/pipeline/refresh-lists.js.map +1 -1
- package/dist/pipeline/scenario-generator.js +2 -1
- package/dist/pipeline/scenario-generator.js.map +1 -1
- package/dist/pipeline/tool-annotator.d.ts +3 -3
- package/dist/pipeline/tool-annotator.js +96 -4
- package/dist/pipeline/tool-annotator.js.map +1 -1
- package/dist/pipeline/types.d.ts +52 -0
- package/dist/sandbox/index.js +40 -1
- package/dist/sandbox/index.js.map +1 -1
- package/dist/session/agent-session.d.ts +3 -0
- package/dist/session/agent-session.js +13 -1
- package/dist/session/agent-session.js.map +1 -1
- package/dist/session/index.js +65 -9
- package/dist/session/index.js.map +1 -1
- package/dist/session/preflight.js +26 -6
- package/dist/session/preflight.js.map +1 -1
- package/dist/session/prompts.d.ts +14 -0
- package/dist/session/prompts.js +39 -2
- package/dist/session/prompts.js.map +1 -1
- package/dist/session/session-manager.d.ts +105 -0
- package/dist/session/session-manager.js +134 -0
- package/dist/session/session-manager.js.map +1 -0
- package/dist/session/types.d.ts +33 -0
- package/dist/session/workspace-validation.d.ts +19 -0
- package/dist/session/workspace-validation.js +66 -0
- package/dist/session/workspace-validation.js.map +1 -0
- package/dist/signal/bot-command.d.ts +2 -0
- package/dist/signal/bot-command.js +8 -0
- package/dist/signal/bot-command.js.map +1 -1
- package/dist/signal/format.d.ts +27 -0
- package/dist/signal/format.js +59 -0
- package/dist/signal/format.js.map +1 -1
- package/dist/signal/signal-bot-daemon.d.ts +27 -25
- package/dist/signal/signal-bot-daemon.js +145 -130
- package/dist/signal/signal-bot-daemon.js.map +1 -1
- package/dist/signal/signal-container.js +63 -19
- package/dist/signal/signal-container.js.map +1 -1
- package/dist/signal/signal-transport.js +3 -8
- package/dist/signal/signal-transport.js.map +1 -1
- package/dist/trusted-process/auto-approver.d.ts +13 -4
- package/dist/trusted-process/auto-approver.js +12 -5
- package/dist/trusted-process/auto-approver.js.map +1 -1
- package/dist/trusted-process/domain-utils.d.ts +38 -4
- package/dist/trusted-process/domain-utils.js +118 -11
- package/dist/trusted-process/domain-utils.js.map +1 -1
- package/dist/trusted-process/escalation.js +5 -0
- package/dist/trusted-process/escalation.js.map +1 -1
- package/dist/trusted-process/index.js +6 -2
- package/dist/trusted-process/index.js.map +1 -1
- package/dist/trusted-process/mcp-client-manager.js +11 -5
- package/dist/trusted-process/mcp-client-manager.js.map +1 -1
- package/dist/trusted-process/mcp-proxy-server.d.ts +2 -0
- package/dist/trusted-process/mcp-proxy-server.js +98 -32
- package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
- package/dist/trusted-process/permissive-output-validator.d.ts +23 -0
- package/dist/trusted-process/permissive-output-validator.js +10 -0
- package/dist/trusted-process/permissive-output-validator.js.map +1 -0
- package/dist/trusted-process/policy-engine.d.ts +27 -4
- package/dist/trusted-process/policy-engine.js +152 -57
- package/dist/trusted-process/policy-engine.js.map +1 -1
- package/dist/trusted-process/tool-description-hints.d.ts +19 -0
- package/dist/trusted-process/tool-description-hints.js +53 -0
- package/dist/trusted-process/tool-description-hints.js.map +1 -0
- package/dist/types/argument-roles.d.ts +33 -1
- package/dist/types/argument-roles.js +129 -1
- package/dist/types/argument-roles.js.map +1 -1
- package/docker/Dockerfile.goose +34 -0
- package/docker/entrypoint-goose.sh +30 -0
- package/package.json +6 -1
- package/src/config/generated/tool-annotations.json +487 -338
- package/src/config/mcp-servers.json +2 -8
package/dist/pipeline/compile.js
CHANGED
|
@@ -1,321 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI entry point for `ironcurtain compile-policy`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 2. Generate test scenarios via LLM
|
|
7
|
-
* 3. Verify compiled policy against real engine + LLM judge
|
|
4
|
+
* Thin wrapper over PipelineRunner that handles CLI-specific concerns:
|
|
5
|
+
* loading config, printing the progress banner, and exit codes.
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
* `npm run annotate-tools` first to generate it.
|
|
7
|
+
* The core compile-verify-repair loop lives in PipelineRunner.
|
|
11
8
|
*/
|
|
12
9
|
import { resolve } from 'node:path';
|
|
13
10
|
import { fileURLToPath } from 'node:url';
|
|
14
|
-
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
15
|
-
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
16
11
|
import chalk from 'chalk';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
import { getHandwrittenScenarios } from './handwritten-scenarios.js';
|
|
22
|
-
import { resolveAllLists } from './list-resolver.js';
|
|
23
|
-
import { computeHash, createPipelineLlm, loadExistingArtifact, loadPipelineConfig, showCached, writeArtifact, withSpinner, } from './pipeline-shared.js';
|
|
24
|
-
import { applyScenarioCorrections, buildJudgeSystemPrompt, extractScenarioCorrections, filterStructuralConflicts, PolicyVerifierSession, verifyPolicy, } from './policy-verifier.js';
|
|
25
|
-
import { buildGeneratorSystemPrompt, ScenarioGeneratorSession } from './scenario-generator.js';
|
|
26
|
-
import { VERSION } from '../version.js';
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
// Content-Hash Caching (compile-specific)
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
function computePolicyHash(systemPrompt, annotations) {
|
|
31
|
-
return computeHash(systemPrompt, JSON.stringify(annotations));
|
|
32
|
-
}
|
|
33
|
-
function computeScenariosHash(systemPrompt, handwrittenScenarios) {
|
|
34
|
-
return computeHash(systemPrompt, JSON.stringify(handwrittenScenarios));
|
|
35
|
-
}
|
|
36
|
-
function extractPermittedDirectories(rules) {
|
|
37
|
-
const dirs = new Set();
|
|
38
|
-
for (const rule of rules) {
|
|
39
|
-
if (rule.if.paths?.within) {
|
|
40
|
-
dirs.add(rule.if.paths.within);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return [...dirs].sort();
|
|
44
|
-
}
|
|
45
|
-
function collectProbeScenarios(result) {
|
|
46
|
-
return result.rounds.flatMap((round) => round.newScenarios);
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Filters scenarios against structural invariants and logs any discarded ones.
|
|
50
|
-
* Returns the valid scenarios and the full list of discarded scenarios.
|
|
51
|
-
*/
|
|
52
|
-
function filterAndLogStructuralConflicts(engine, scenarios, label = 'Discarded scenario (structural conflict)') {
|
|
53
|
-
const { valid, discarded } = filterStructuralConflicts(engine, scenarios);
|
|
54
|
-
for (const d of discarded) {
|
|
55
|
-
const prefix = d.scenario.source === 'handwritten'
|
|
56
|
-
? chalk.yellow('Warning: handwritten scenario conflicts with structural invariant:')
|
|
57
|
-
: chalk.dim(`${label}:`);
|
|
58
|
-
console.error(` ${prefix} "${d.scenario.description}" — ${d.rule} always returns ${d.actual}`);
|
|
59
|
-
}
|
|
60
|
-
return { valid, discarded };
|
|
61
|
-
}
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
// Scenario Merge (multi-turn regeneration)
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
/**
|
|
66
|
-
* Merges replacement scenarios from the regeneration session into the
|
|
67
|
-
* scenario list. Removes corrected and discarded scenarios, then adds
|
|
68
|
-
* unique replacements that don't duplicate any remaining scenario.
|
|
69
|
-
*/
|
|
70
|
-
export function mergeReplacements(scenarios, replacements, corrections, discardedScenarios) {
|
|
71
|
-
const removedDescriptions = new Set([
|
|
72
|
-
...corrections.map((c) => c.scenarioDescription),
|
|
73
|
-
...discardedScenarios.filter((d) => d.scenario.source !== 'handwritten').map((d) => d.scenario.description),
|
|
74
|
-
]);
|
|
75
|
-
const kept = scenarios.filter((s) => !removedDescriptions.has(s.description));
|
|
76
|
-
const keptDescriptions = new Set(kept.map((s) => s.description));
|
|
77
|
-
const uniqueReplacements = replacements.filter((r) => !keptDescriptions.has(r.description));
|
|
78
|
-
return [...kept, ...uniqueReplacements];
|
|
79
|
-
}
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
// Path Resolution (post-LLM normalization)
|
|
82
|
-
// ---------------------------------------------------------------------------
|
|
83
|
-
/**
|
|
84
|
-
* Resolves all `paths.within` values in compiled rules to their real
|
|
85
|
-
* filesystem paths, following symlinks. This ensures that symlinked
|
|
86
|
-
* directories (e.g., ~/Downloads -> /mnt/c/.../Downloads on WSL) are
|
|
87
|
-
* resolved to their canonical form so that runtime path comparisons
|
|
88
|
-
* match correctly.
|
|
89
|
-
*
|
|
90
|
-
* Falls back to path.resolve() if the path does not exist on disk.
|
|
91
|
-
*/
|
|
92
|
-
export function resolveRulePaths(rules) {
|
|
93
|
-
return rules.map((rule) => {
|
|
94
|
-
if (!rule.if.paths?.within)
|
|
95
|
-
return rule;
|
|
96
|
-
const resolved = resolveRealPath(rule.if.paths.within);
|
|
97
|
-
if (resolved === rule.if.paths.within)
|
|
98
|
-
return rule;
|
|
99
|
-
return {
|
|
100
|
-
...rule,
|
|
101
|
-
if: {
|
|
102
|
-
...rule.if,
|
|
103
|
-
paths: { ...rule.if.paths, within: resolved },
|
|
104
|
-
},
|
|
105
|
-
};
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
async function compilePolicyRules(constitutionText, annotations, protectedPaths, inputHash, existingPolicy, llm, stepLabel = '[1/3]', system) {
|
|
109
|
-
const stepText = `${stepLabel} Compiling constitution`;
|
|
110
|
-
// Check cache: skip LLM call if inputs haven't changed.
|
|
111
|
-
// Still resolve paths in case symlink targets changed since last run.
|
|
112
|
-
if (existingPolicy && existingPolicy.inputHash === inputHash) {
|
|
113
|
-
showCached(stepText);
|
|
114
|
-
return {
|
|
115
|
-
rules: resolveRulePaths(existingPolicy.rules),
|
|
116
|
-
listDefinitions: existingPolicy.listDefinitions ?? [],
|
|
117
|
-
inputHash,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
const session = new ConstitutionCompilerSession({
|
|
121
|
-
system: system ?? buildCompilerSystemPrompt(constitutionText, annotations, { protectedPaths }),
|
|
122
|
-
model: llm,
|
|
123
|
-
annotations,
|
|
124
|
-
});
|
|
125
|
-
const { result: compilationOutput } = await withSpinner(stepText, async (spinner) => {
|
|
126
|
-
const output = await session.compile((msg) => {
|
|
127
|
-
spinner.text = `${stepText} — ${msg}`;
|
|
128
|
-
});
|
|
129
|
-
const rules = resolveRulePaths(output.rules);
|
|
130
|
-
validateRulesOrThrow(rules, output.listDefinitions);
|
|
131
|
-
return { rules, listDefinitions: output.listDefinitions };
|
|
132
|
-
}, (output, elapsed) => `${stepText}: ${output.rules.length} rules compiled (${elapsed.toFixed(1)}s)`);
|
|
133
|
-
return {
|
|
134
|
-
rules: compilationOutput.rules,
|
|
135
|
-
listDefinitions: compilationOutput.listDefinitions,
|
|
136
|
-
inputHash,
|
|
137
|
-
session,
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
class RuleValidationError extends Error {
|
|
141
|
-
errors;
|
|
142
|
-
constructor(errors) {
|
|
143
|
-
super('Compiled rule validation failed');
|
|
144
|
-
this.errors = errors;
|
|
145
|
-
this.name = 'RuleValidationError';
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
function validateRulesOrThrow(rules, listDefinitions = []) {
|
|
149
|
-
const ruleValidation = validateCompiledRules(rules, listDefinitions);
|
|
150
|
-
if (ruleValidation.warnings.length > 0) {
|
|
151
|
-
for (const w of ruleValidation.warnings) {
|
|
152
|
-
console.error(` ${chalk.yellow('Warning:')} ${w}`);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
if (!ruleValidation.valid) {
|
|
156
|
-
throw new RuleValidationError(ruleValidation.errors);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
async function compilePolicyRulesWithRepair(constitutionText, annotations, protectedPaths, baseInputHash, repairContext, llm, onProgress, system, session) {
|
|
160
|
-
let output;
|
|
161
|
-
if (session) {
|
|
162
|
-
output = await session.recompile(repairContext, onProgress);
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
output = await compileConstitution(constitutionText, annotations, { protectedPaths }, llm, repairContext, onProgress, system);
|
|
166
|
-
}
|
|
167
|
-
const compiledRules = resolveRulePaths(output.rules);
|
|
168
|
-
validateRulesOrThrow(compiledRules, output.listDefinitions);
|
|
169
|
-
return {
|
|
170
|
-
rules: compiledRules,
|
|
171
|
-
listDefinitions: output.listDefinitions,
|
|
172
|
-
inputHash: `${baseInputHash}-repair`,
|
|
173
|
-
session,
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
// ---------------------------------------------------------------------------
|
|
177
|
-
// Artifact Construction (pure data transformation)
|
|
178
|
-
// ---------------------------------------------------------------------------
|
|
179
|
-
function buildPolicyArtifact(constitutionHash, compilationResult) {
|
|
180
|
-
const artifact = {
|
|
181
|
-
generatedAt: new Date().toISOString(),
|
|
182
|
-
constitutionHash,
|
|
183
|
-
inputHash: compilationResult.inputHash,
|
|
184
|
-
rules: compilationResult.rules,
|
|
185
|
-
};
|
|
186
|
-
if (compilationResult.listDefinitions.length > 0) {
|
|
187
|
-
artifact.listDefinitions = compilationResult.listDefinitions;
|
|
188
|
-
}
|
|
189
|
-
return artifact;
|
|
190
|
-
}
|
|
191
|
-
async function generateTestScenarios(constitutionText, annotations, allowedDirectory, permittedDirectories, inputHash, existingScenarios, llm, stepLabel = '[2/3]', system) {
|
|
192
|
-
const handwrittenScenarios = getHandwrittenScenarios(allowedDirectory);
|
|
193
|
-
const stepText = `${stepLabel} Generating test scenarios`;
|
|
194
|
-
// Check cache: skip LLM call if inputs haven't changed
|
|
195
|
-
if (existingScenarios && existingScenarios.inputHash === inputHash) {
|
|
196
|
-
showCached(stepText);
|
|
197
|
-
return { scenarios: existingScenarios.scenarios, inputHash };
|
|
198
|
-
}
|
|
199
|
-
// Create a multi-turn session so the repair loop can feed corrections back
|
|
200
|
-
const session = new ScenarioGeneratorSession({
|
|
201
|
-
system: system ?? buildGeneratorSystemPrompt(constitutionText, annotations, allowedDirectory, permittedDirectories),
|
|
202
|
-
model: llm,
|
|
203
|
-
annotations,
|
|
204
|
-
handwrittenScenarios,
|
|
205
|
-
});
|
|
206
|
-
const { result: scenarios } = await withSpinner(stepText, async (spinner) => session.generate((msg) => {
|
|
207
|
-
spinner.text = `${stepText} — ${msg}`;
|
|
208
|
-
}), (scenarios, elapsed) => {
|
|
209
|
-
const generatedCount = scenarios.length - handwrittenScenarios.length;
|
|
210
|
-
return `${stepText}: ${scenarios.length} scenarios (${handwrittenScenarios.length} handwritten + ${generatedCount} generated) (${elapsed.toFixed(1)}s)`;
|
|
211
|
-
});
|
|
212
|
-
return { scenarios, inputHash, session };
|
|
213
|
-
}
|
|
214
|
-
// ---------------------------------------------------------------------------
|
|
215
|
-
// Policy Verification (LLM step)
|
|
216
|
-
// ---------------------------------------------------------------------------
|
|
217
|
-
async function verifyCompiledPolicy(constitutionText, compiledPolicyFile, toolAnnotationsFile, protectedPaths, scenarios, llm, allowedDirectory, maxRounds = 3, verbose = true, onProgress, serverDomainAllowlists, dynamicLists, system, session) {
|
|
218
|
-
const result = await verifyPolicy(constitutionText, compiledPolicyFile, toolAnnotationsFile, protectedPaths, scenarios, llm, maxRounds, allowedDirectory, onProgress, serverDomainAllowlists, dynamicLists, system, session);
|
|
219
|
-
if (!result.pass) {
|
|
220
|
-
if (verbose) {
|
|
221
|
-
console.error('');
|
|
222
|
-
console.error(chalk.red('Verification FAILED:'));
|
|
223
|
-
console.error(result.summary);
|
|
224
|
-
console.error('');
|
|
225
|
-
for (const f of result.failedScenarios) {
|
|
226
|
-
console.error(` ${chalk.red('FAIL:')} ${f.scenario.description}`);
|
|
227
|
-
console.error(` Expected: ${f.scenario.expectedDecision}, Got: ${f.actualDecision} (rule: ${f.matchingRule})`);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
else {
|
|
231
|
-
console.error(` ${result.failedScenarios.length} scenario(s) failed.`);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
return result;
|
|
235
|
-
}
|
|
236
|
-
// ---------------------------------------------------------------------------
|
|
237
|
-
// Artifact Output
|
|
238
|
-
// ---------------------------------------------------------------------------
|
|
239
|
-
function writeScenariosArtifact(generatedDir, constitutionHash, scenarioResult) {
|
|
240
|
-
const scenariosFile = {
|
|
241
|
-
generatedAt: new Date().toISOString(),
|
|
242
|
-
constitutionHash,
|
|
243
|
-
inputHash: scenarioResult.inputHash,
|
|
244
|
-
scenarios: scenarioResult.scenarios,
|
|
245
|
-
};
|
|
246
|
-
writeArtifact(generatedDir, 'test-scenarios.json', scenariosFile);
|
|
247
|
-
}
|
|
248
|
-
function writePolicyArtifact(generatedDir, compiledPolicyFile) {
|
|
249
|
-
writeArtifact(generatedDir, 'compiled-policy.json', compiledPolicyFile);
|
|
250
|
-
}
|
|
251
|
-
// ---------------------------------------------------------------------------
|
|
252
|
-
// MCP Server Connection for Data-Backed Lists
|
|
253
|
-
// ---------------------------------------------------------------------------
|
|
254
|
-
/**
|
|
255
|
-
* Connects to MCP servers needed for data-backed list resolution.
|
|
256
|
-
* Only connects to servers hinted by list definitions with requiresMcp: true.
|
|
257
|
-
* Returns a map keyed by server name for the resolver.
|
|
258
|
-
*/
|
|
259
|
-
export async function connectMcpServersForLists(definitions, mcpServers) {
|
|
260
|
-
const mcpDefs = definitions.filter((d) => d.requiresMcp);
|
|
261
|
-
const hasUnhintedLists = mcpDefs.some((d) => !d.mcpServerHint);
|
|
262
|
-
// Connect all configured servers if any list lacks a hint,
|
|
263
|
-
// otherwise connect only the hinted servers.
|
|
264
|
-
const neededServers = hasUnhintedLists
|
|
265
|
-
? new Set(Object.keys(mcpServers))
|
|
266
|
-
: new Set(mcpDefs
|
|
267
|
-
.filter((d) => d.mcpServerHint != null)
|
|
268
|
-
.map((d) => d.mcpServerHint));
|
|
269
|
-
const connections = new Map();
|
|
270
|
-
for (const serverName of neededServers) {
|
|
271
|
-
const serverConfig = mcpServers[serverName];
|
|
272
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- serverName may not exist in mcpServers at runtime (from mcpServerHint)
|
|
273
|
-
if (!serverConfig) {
|
|
274
|
-
console.error(` ${chalk.yellow('Warning:')} MCP server "${serverName}" not configured — skipping`);
|
|
275
|
-
continue;
|
|
276
|
-
}
|
|
277
|
-
const transport = new StdioClientTransport({
|
|
278
|
-
command: serverConfig.command,
|
|
279
|
-
args: serverConfig.args,
|
|
280
|
-
env: serverConfig.env ? { ...process.env, ...serverConfig.env } : undefined,
|
|
281
|
-
stderr: 'pipe',
|
|
282
|
-
});
|
|
283
|
-
// Drain piped stderr to prevent backpressure
|
|
284
|
-
if (transport.stderr) {
|
|
285
|
-
transport.stderr.on('data', () => { });
|
|
286
|
-
}
|
|
287
|
-
const client = new Client({ name: 'ironcurtain-list-resolver', version: VERSION });
|
|
288
|
-
await client.connect(transport);
|
|
289
|
-
const toolsResult = await client.listTools();
|
|
290
|
-
connections.set(serverName, { client, tools: toolsResult.tools });
|
|
291
|
-
}
|
|
292
|
-
return connections;
|
|
293
|
-
}
|
|
294
|
-
export async function disconnectMcpServers(connections) {
|
|
295
|
-
for (const conn of connections.values()) {
|
|
296
|
-
try {
|
|
297
|
-
await conn.client.close();
|
|
298
|
-
}
|
|
299
|
-
catch {
|
|
300
|
-
// Ignore cleanup errors
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
12
|
+
import { loadPipelineConfig, loadToolAnnotationsFile } from './pipeline-shared.js';
|
|
13
|
+
import { PipelineRunner, createPipelineModels } from './pipeline-runner.js';
|
|
14
|
+
// Re-export utilities that test files import from this file.
|
|
15
|
+
export { resolveRulePaths, mergeReplacements } from './pipeline-shared.js';
|
|
304
16
|
// ---------------------------------------------------------------------------
|
|
305
17
|
// Main Pipeline
|
|
306
18
|
// ---------------------------------------------------------------------------
|
|
307
19
|
export async function main() {
|
|
308
20
|
const config = loadPipelineConfig();
|
|
309
|
-
// Load tool annotations
|
|
310
|
-
const toolAnnotationsFile =
|
|
21
|
+
// Load tool annotations early to validate they exist before printing the banner
|
|
22
|
+
const toolAnnotationsFile = loadToolAnnotationsFile(config.generatedDir, config.packageGeneratedDir);
|
|
311
23
|
if (!toolAnnotationsFile) {
|
|
312
24
|
console.error(chalk.red.bold("Error: tool-annotations.json not found. Run 'npm run annotate-tools' first to generate tool annotations."));
|
|
313
25
|
process.exit(1);
|
|
314
26
|
}
|
|
315
|
-
// Flatten annotations from all servers
|
|
316
27
|
const allAnnotations = Object.values(toolAnnotationsFile.servers).flatMap((server) => server.tools);
|
|
317
|
-
// Compute domain allowlists for policy verification
|
|
318
|
-
const serverDomainAllowlists = extractServerDomainAllowlists(config.mcpServers);
|
|
319
28
|
console.error(chalk.bold('Policy Compilation Pipeline'));
|
|
320
29
|
console.error(chalk.bold('==========================='));
|
|
321
30
|
console.error(`Constitution: ${chalk.dim(config.constitutionPath)}`);
|
|
@@ -323,278 +32,30 @@ export async function main() {
|
|
|
323
32
|
console.error(`Output: ${chalk.dim(config.generatedDir + '/')}`);
|
|
324
33
|
console.error(`Annotations: ${chalk.dim(`${allAnnotations.length} tools from ${Object.keys(toolAnnotationsFile.servers).length} server(s)`)}`);
|
|
325
34
|
console.error('');
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
// inspection even if verification fails, and cached for next run.
|
|
342
|
-
let compiledPolicyFile = buildPolicyArtifact(config.constitutionHash, compilationResult);
|
|
343
|
-
writePolicyArtifact(config.generatedDir, compiledPolicyFile);
|
|
344
|
-
// Resolve dynamic lists if the compiler emitted list definitions
|
|
345
|
-
const hasLists = compilationResult.listDefinitions.length > 0;
|
|
346
|
-
const totalSteps = hasLists ? 4 : 3;
|
|
347
|
-
let dynamicLists;
|
|
348
|
-
if (hasLists) {
|
|
349
|
-
logContext.stepName = 'resolve-lists';
|
|
350
|
-
const existingLists = loadExistingArtifact(config.generatedDir, 'dynamic-lists.json', config.packageGeneratedDir);
|
|
351
|
-
// Connect to MCP servers if any list definitions require it
|
|
352
|
-
const needsMcp = compilationResult.listDefinitions.some((d) => d.requiresMcp);
|
|
353
|
-
let mcpConnections;
|
|
354
|
-
if (needsMcp) {
|
|
355
|
-
mcpConnections = await connectMcpServersForLists(compilationResult.listDefinitions, config.mcpServers);
|
|
356
|
-
}
|
|
357
|
-
const listStepText = `[2/${totalSteps}] Resolving dynamic lists`;
|
|
358
|
-
try {
|
|
359
|
-
const { result: resolvedLists } = await withSpinner(listStepText, async (spinner) => resolveAllLists(compilationResult.listDefinitions, { model: llm, mcpConnections }, existingLists, (msg) => {
|
|
360
|
-
spinner.text = `${listStepText} — ${msg}`;
|
|
361
|
-
}), (result, elapsed) => {
|
|
362
|
-
const count = Object.keys(result.lists).length;
|
|
363
|
-
return `${listStepText}: ${count} list(s) resolved (${elapsed.toFixed(1)}s)`;
|
|
364
|
-
});
|
|
365
|
-
dynamicLists = resolvedLists;
|
|
366
|
-
writeArtifact(config.generatedDir, 'dynamic-lists.json', dynamicLists);
|
|
367
|
-
}
|
|
368
|
-
finally {
|
|
369
|
-
if (mcpConnections) {
|
|
370
|
-
await disconnectMcpServers(mcpConnections);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
35
|
+
const models = await createPipelineModels(config.generatedDir);
|
|
36
|
+
const runner = new PipelineRunner(models);
|
|
37
|
+
try {
|
|
38
|
+
await runner.run({
|
|
39
|
+
constitutionInput: config.constitutionText,
|
|
40
|
+
constitutionKind: 'constitution',
|
|
41
|
+
outputDir: config.generatedDir,
|
|
42
|
+
toolAnnotationsDir: config.generatedDir,
|
|
43
|
+
toolAnnotationsFallbackDir: config.packageGeneratedDir,
|
|
44
|
+
allowedDirectory: config.allowedDirectory,
|
|
45
|
+
protectedPaths: config.protectedPaths,
|
|
46
|
+
mcpServers: config.mcpServers,
|
|
47
|
+
llmLogPath: models.logPath,
|
|
48
|
+
preloadedToolAnnotations: toolAnnotationsFile,
|
|
49
|
+
});
|
|
373
50
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
// Generate test scenarios (LLM-cacheable)
|
|
377
|
-
const scenarioStepLabel = `[${hasLists ? 3 : 2}/${totalSteps}]`;
|
|
378
|
-
logContext.stepName = 'generate-scenarios';
|
|
379
|
-
const scenarioPrompt = buildGeneratorSystemPrompt(config.constitutionText, allAnnotations, config.allowedDirectory, permittedDirectories, dynamicLists);
|
|
380
|
-
const scenarioHash = computeScenariosHash(scenarioPrompt, getHandwrittenScenarios(config.allowedDirectory));
|
|
381
|
-
const scenarioSystem = cacheStrategy.wrapSystemPrompt(scenarioPrompt);
|
|
382
|
-
const scenarioResult = await generateTestScenarios(config.constitutionText, allAnnotations, config.allowedDirectory, permittedDirectories, scenarioHash, existingScenarios, llm, scenarioStepLabel, scenarioSystem);
|
|
383
|
-
// Write scenarios to disk immediately so they're available for
|
|
384
|
-
// inspection even if verification fails, and cached for next run.
|
|
385
|
-
writeScenariosArtifact(config.generatedDir, config.constitutionHash, scenarioResult);
|
|
386
|
-
// Filter out scenarios that conflict with structural invariants
|
|
387
|
-
const filterEngine = new PolicyEngine(compiledPolicyFile, toolAnnotationsFile, config.protectedPaths, config.allowedDirectory, undefined, dynamicLists);
|
|
388
|
-
const { valid: initialValid, discarded: discardedScenarios } = filterAndLogStructuralConflicts(filterEngine, scenarioResult.scenarios, 'Discarded scenario (structural conflict)');
|
|
389
|
-
let filteredScenarios = initialValid;
|
|
390
|
-
// Verify compiled policy against scenarios (full depth)
|
|
391
|
-
const verifyStepLabel = `[${totalSteps}/${totalSteps}]`;
|
|
392
|
-
logContext.stepName = 'verify-policy';
|
|
393
|
-
const allAvailableTools = allAnnotations.map((a) => ({ serverName: a.serverName, toolName: a.toolName }));
|
|
394
|
-
const serverNamesList = [...new Set(allAnnotations.map((a) => a.serverName))];
|
|
395
|
-
const toolNamesList = [...new Set(allAnnotations.map((a) => a.toolName))];
|
|
396
|
-
let verifierSystem = cacheStrategy.wrapSystemPrompt(buildJudgeSystemPrompt(config.constitutionText, compiledPolicyFile, config.protectedPaths, allAvailableTools, dynamicLists, config.allowedDirectory));
|
|
397
|
-
let verifierSession = new PolicyVerifierSession({
|
|
398
|
-
system: verifierSystem,
|
|
399
|
-
model: llm,
|
|
400
|
-
serverNames: serverNamesList,
|
|
401
|
-
toolNames: toolNamesList,
|
|
402
|
-
});
|
|
403
|
-
const { result: verificationResultInitial } = await withSpinner(`${verifyStepLabel} Verifying policy`, async (spinner) => verifyCompiledPolicy(config.constitutionText, compiledPolicyFile, toolAnnotationsFile, config.protectedPaths, filteredScenarios, llm, config.allowedDirectory, 3, true, (msg) => {
|
|
404
|
-
spinner.text = `${verifyStepLabel} Verifying policy — ${msg}`;
|
|
405
|
-
}, serverDomainAllowlists, dynamicLists, verifierSystem, verifierSession), (r, elapsed) => r.pass
|
|
406
|
-
? `${verifyStepLabel} Verified policy: ${r.rounds.length} round(s) (${elapsed.toFixed(1)}s)`
|
|
407
|
-
: `${verifyStepLabel} Verification completed with failures (${elapsed.toFixed(1)}s)`);
|
|
408
|
-
let verificationResult = verificationResultInitial;
|
|
409
|
-
// Collect probe scenarios from verifier across all attempts, filtering
|
|
410
|
-
// out any that conflict with structural invariants
|
|
411
|
-
const { valid: filteredInitialProbes } = filterAndLogStructuralConflicts(filterEngine, collectProbeScenarios(verificationResult), 'Discarded probe (structural conflict)');
|
|
412
|
-
const accumulatedProbes = filteredInitialProbes;
|
|
413
|
-
// Dual-channel compile-verify-repair loop (up to 2 repair attempts)
|
|
414
|
-
// The judge attributes each failure to 'rule', 'scenario', or 'both':
|
|
415
|
-
// - scenario-blamed: patch the test expectation
|
|
416
|
-
// - rule-blamed: recompile rules with failure feedback
|
|
417
|
-
// - both: patch scenario AND recompile
|
|
418
|
-
const MAX_REPAIRS = 2;
|
|
419
|
-
let repairAttempts = 0;
|
|
420
|
-
let scenarioCorrectionsApplied = 0;
|
|
421
|
-
if (!verificationResult.pass) {
|
|
422
|
-
const baseInputHash = compilationResult.inputHash;
|
|
423
|
-
for (let attempt = 1; attempt <= MAX_REPAIRS; attempt++) {
|
|
51
|
+
catch (err) {
|
|
52
|
+
if (err instanceof Error && err.message.includes('Verification FAILED')) {
|
|
424
53
|
console.error('');
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const judgeAnalysis = lastRound?.llmAnalysis ?? verificationResult.summary;
|
|
428
|
-
const attributedFailures = lastRound?.attributedFailures ?? [];
|
|
429
|
-
// Split failures into scenario corrections and rule-blamed failures
|
|
430
|
-
const allScenarios = [...scenarioResult.scenarios, ...accumulatedProbes];
|
|
431
|
-
const { corrections, handwrittenWarnings } = extractScenarioCorrections(attributedFailures, allScenarios);
|
|
432
|
-
for (const warning of handwrittenWarnings) {
|
|
433
|
-
console.error(` ${chalk.yellow('Warning:')} ${warning}`);
|
|
434
|
-
}
|
|
435
|
-
// Apply scenario corrections (patch expectations on generated scenarios)
|
|
436
|
-
if (corrections.length > 0) {
|
|
437
|
-
scenarioResult.scenarios = applyScenarioCorrections(scenarioResult.scenarios, corrections);
|
|
438
|
-
const correctedProbes = applyScenarioCorrections(accumulatedProbes, corrections);
|
|
439
|
-
accumulatedProbes.splice(0, accumulatedProbes.length, ...correctedProbes);
|
|
440
|
-
scenarioCorrectionsApplied += corrections.length;
|
|
441
|
-
console.error(` ${chalk.dim(`Corrected ${corrections.length} scenario expectation(s)`)}`);
|
|
442
|
-
}
|
|
443
|
-
// Feed corrections back to the scenario generator session so it can
|
|
444
|
-
// produce better replacement scenarios that avoid the same mistakes.
|
|
445
|
-
const { session } = scenarioResult;
|
|
446
|
-
if (session && (corrections.length > 0 || discardedScenarios.length > 0 || accumulatedProbes.length > 0)) {
|
|
447
|
-
const feedback = {
|
|
448
|
-
corrections,
|
|
449
|
-
discardedScenarios,
|
|
450
|
-
probeScenarios: accumulatedProbes,
|
|
451
|
-
};
|
|
452
|
-
logContext.stepName = `repair-regenerate-${attempt}`;
|
|
453
|
-
const regenText = `Repair ${attempt}/${MAX_REPAIRS}: Regenerating scenarios`;
|
|
454
|
-
const { result: replacements } = await withSpinner(regenText, async (spinner) => session.regenerate(feedback, (msg) => {
|
|
455
|
-
spinner.text = `${regenText} — ${msg}`;
|
|
456
|
-
}), (r, elapsed) => `${regenText}: ${r.length} replacement(s) (${elapsed.toFixed(1)}s)`);
|
|
457
|
-
// Merge: remove corrected/discarded scenarios and add unique replacements
|
|
458
|
-
scenarioResult.scenarios = mergeReplacements(scenarioResult.scenarios, replacements, corrections, discardedScenarios);
|
|
459
|
-
}
|
|
460
|
-
// Re-filter after corrections/regeneration (a corrected expected decision
|
|
461
|
-
// might now match a structural invariant result, making it valid)
|
|
462
|
-
({ valid: filteredScenarios } = filterAndLogStructuralConflicts(filterEngine, scenarioResult.scenarios));
|
|
463
|
-
// Determine which failures need rule recompilation:
|
|
464
|
-
// - no attribution or blamed on 'rule'/'both' (conservative default)
|
|
465
|
-
// - handwritten scenarios blamed on 'scenario' (reclassified as rule issues)
|
|
466
|
-
const allRuleBlamedFailures = verificationResult.failedScenarios.filter((f) => {
|
|
467
|
-
const attr = attributedFailures.find((a) => a.scenarioDescription === f.scenario.description);
|
|
468
|
-
if (!attr || attr.blame.kind === 'rule' || attr.blame.kind === 'both')
|
|
469
|
-
return true;
|
|
470
|
-
return handwrittenWarnings.some((w) => w.includes(f.scenario.description));
|
|
471
|
-
});
|
|
472
|
-
if (allRuleBlamedFailures.length > 0) {
|
|
473
|
-
// Recompile with failure feedback (always calls LLM, no cache)
|
|
474
|
-
const repairContext = {
|
|
475
|
-
failedScenarios: allRuleBlamedFailures,
|
|
476
|
-
judgeAnalysis,
|
|
477
|
-
attemptNumber: attempt,
|
|
478
|
-
existingListDefinitions: compilationResult.listDefinitions.length > 0 ? compilationResult.listDefinitions : undefined,
|
|
479
|
-
handwrittenScenarios,
|
|
480
|
-
};
|
|
481
|
-
logContext.stepName = `repair-compile-${attempt}`;
|
|
482
|
-
const repairCompileText = `Repair ${attempt}/${MAX_REPAIRS}: Recompiling`;
|
|
483
|
-
const { result: repairCompileResult } = await withSpinner(repairCompileText, async (spinner) => compilePolicyRulesWithRepair(config.constitutionText, allAnnotations, config.protectedPaths, baseInputHash, repairContext, llm, (msg) => {
|
|
484
|
-
spinner.text = `${repairCompileText} — ${msg}`;
|
|
485
|
-
}, compilerSystem, compilationResult.session), (r, elapsed) => `Repair ${attempt}/${MAX_REPAIRS}: Recompiled ${r.rules.length} rules (${elapsed.toFixed(1)}s)`);
|
|
486
|
-
compilationResult = repairCompileResult;
|
|
487
|
-
// Re-resolve dynamic lists if repair introduced new list definitions
|
|
488
|
-
if (dynamicLists && compilationResult.listDefinitions.length > 0) {
|
|
489
|
-
const currentLists = dynamicLists;
|
|
490
|
-
const newListDefs = compilationResult.listDefinitions.filter((def) => !(def.name in currentLists.lists));
|
|
491
|
-
if (newListDefs.length > 0) {
|
|
492
|
-
const mcpRequired = newListDefs.filter((d) => d.requiresMcp);
|
|
493
|
-
if (mcpRequired.length > 0) {
|
|
494
|
-
console.error(` ${chalk.yellow('Warning:')} Repair introduced ${mcpRequired.length} new MCP-requiring list(s) — skipping resolution`);
|
|
495
|
-
}
|
|
496
|
-
const knowledgeDefs = newListDefs.filter((d) => !d.requiresMcp);
|
|
497
|
-
if (knowledgeDefs.length > 0) {
|
|
498
|
-
console.error(` ${chalk.dim(`Resolving ${knowledgeDefs.length} new list(s) from repair...`)}`);
|
|
499
|
-
const resolved = await resolveAllLists(knowledgeDefs, { model: llm }, currentLists);
|
|
500
|
-
dynamicLists = {
|
|
501
|
-
...resolved,
|
|
502
|
-
lists: { ...currentLists.lists, ...resolved.lists },
|
|
503
|
-
};
|
|
504
|
-
writeArtifact(config.generatedDir, 'dynamic-lists.json', dynamicLists);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
// Write updated policy artifact
|
|
509
|
-
compiledPolicyFile = buildPolicyArtifact(config.constitutionHash, compilationResult);
|
|
510
|
-
writePolicyArtifact(config.generatedDir, compiledPolicyFile);
|
|
511
|
-
// Rebuild verifier system prompt and session with updated rules
|
|
512
|
-
verifierSystem = cacheStrategy.wrapSystemPrompt(buildJudgeSystemPrompt(config.constitutionText, compiledPolicyFile, config.protectedPaths, allAvailableTools, dynamicLists, config.allowedDirectory));
|
|
513
|
-
verifierSession = new PolicyVerifierSession({
|
|
514
|
-
system: verifierSystem,
|
|
515
|
-
model: llm,
|
|
516
|
-
serverNames: serverNamesList,
|
|
517
|
-
toolNames: toolNamesList,
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
else {
|
|
521
|
-
console.error(` ${chalk.dim('No rule-blamed failures — skipping recompilation')}`);
|
|
522
|
-
}
|
|
523
|
-
// Verify with reduced depth, using base scenarios + accumulated probes
|
|
524
|
-
logContext.stepName = `repair-verify-${attempt}`;
|
|
525
|
-
const repairScenarios = [...filteredScenarios, ...accumulatedProbes];
|
|
526
|
-
const repairVerifyText = `Repair ${attempt}/${MAX_REPAIRS}: Verifying`;
|
|
527
|
-
const { result: repairVerifyResult } = await withSpinner(repairVerifyText, async (spinner) => verifyCompiledPolicy(config.constitutionText, compiledPolicyFile, toolAnnotationsFile, config.protectedPaths, repairScenarios, llm, config.allowedDirectory, 1, true, (msg) => {
|
|
528
|
-
spinner.text = `${repairVerifyText} — ${msg}`;
|
|
529
|
-
}, serverDomainAllowlists, dynamicLists, verifierSystem, verifierSession), (r, elapsed) => r.pass
|
|
530
|
-
? `Repair ${attempt}/${MAX_REPAIRS}: Verified (${elapsed.toFixed(1)}s)`
|
|
531
|
-
: `Repair ${attempt}/${MAX_REPAIRS}: ${r.failedScenarios.length} failure(s) (${elapsed.toFixed(1)}s)`);
|
|
532
|
-
verificationResult = repairVerifyResult;
|
|
533
|
-
// Accumulate any new probe scenarios, filtering structural conflicts
|
|
534
|
-
const { valid: validRepairProbes } = filterAndLogStructuralConflicts(filterEngine, collectProbeScenarios(verificationResult), 'Discarded probe (structural conflict)');
|
|
535
|
-
accumulatedProbes.push(...validRepairProbes);
|
|
536
|
-
repairAttempts = attempt;
|
|
537
|
-
if (verificationResult.pass) {
|
|
538
|
-
// Run final full verification with all accumulated scenarios
|
|
539
|
-
logContext.stepName = 'final-verify';
|
|
540
|
-
const finalScenarios = [...filteredScenarios, ...accumulatedProbes];
|
|
541
|
-
// New session for final verification (fresh history for clean evaluation)
|
|
542
|
-
const finalSession = new PolicyVerifierSession({
|
|
543
|
-
system: verifierSystem,
|
|
544
|
-
model: llm,
|
|
545
|
-
serverNames: serverNamesList,
|
|
546
|
-
toolNames: toolNamesList,
|
|
547
|
-
});
|
|
548
|
-
const { result: finalVerifyResult } = await withSpinner('Final full verification', async (spinner) => verifyCompiledPolicy(config.constitutionText, compiledPolicyFile, toolAnnotationsFile, config.protectedPaths, finalScenarios, llm, config.allowedDirectory, 3, true, (msg) => {
|
|
549
|
-
spinner.text = `Final full verification — ${msg}`;
|
|
550
|
-
}, serverDomainAllowlists, dynamicLists, verifierSystem, finalSession), (r, elapsed) => r.pass
|
|
551
|
-
? `Final full verification: passed (${elapsed.toFixed(1)}s)`
|
|
552
|
-
: `Final full verification: ${r.failedScenarios.length} failure(s) (${elapsed.toFixed(1)}s)`);
|
|
553
|
-
verificationResult = finalVerifyResult;
|
|
554
|
-
// Accumulate any new probes from final verification, filtering structural conflicts
|
|
555
|
-
const { valid: validFinalProbes } = filterAndLogStructuralConflicts(filterEngine, collectProbeScenarios(verificationResult), 'Discarded probe (structural conflict)');
|
|
556
|
-
accumulatedProbes.push(...validFinalProbes);
|
|
557
|
-
break;
|
|
558
|
-
}
|
|
54
|
+
console.error(chalk.red.bold('Verification FAILED — artifacts written but policy may need review.'));
|
|
55
|
+
process.exit(1);
|
|
559
56
|
}
|
|
57
|
+
throw err;
|
|
560
58
|
}
|
|
561
|
-
// Re-write scenarios artifact if the repair loop modified them
|
|
562
|
-
if (repairAttempts > 0) {
|
|
563
|
-
writeScenariosArtifact(config.generatedDir, config.constitutionHash, scenarioResult);
|
|
564
|
-
}
|
|
565
|
-
// Deduplicate accumulated probes by description
|
|
566
|
-
const seenDescriptions = new Set(scenarioResult.scenarios.map((s) => s.description));
|
|
567
|
-
const uniqueProbes = accumulatedProbes.filter((s) => {
|
|
568
|
-
if (seenDescriptions.has(s.description))
|
|
569
|
-
return false;
|
|
570
|
-
seenDescriptions.add(s.description);
|
|
571
|
-
return true;
|
|
572
|
-
});
|
|
573
|
-
const totalScenariosTested = filteredScenarios.length + uniqueProbes.length;
|
|
574
|
-
console.error('');
|
|
575
|
-
console.error(` Rules: ${compilationResult.rules.length}`);
|
|
576
|
-
console.error(` Scenarios tested: ${totalScenariosTested}`);
|
|
577
|
-
if (discardedScenarios.length > 0) {
|
|
578
|
-
console.error(` Scenarios discarded (structural conflicts): ${discardedScenarios.length}`);
|
|
579
|
-
}
|
|
580
|
-
if (uniqueProbes.length > 0) {
|
|
581
|
-
console.error(` Probe scenarios accumulated: ${uniqueProbes.length}`);
|
|
582
|
-
}
|
|
583
|
-
if (repairAttempts > 0) {
|
|
584
|
-
console.error(` Repair attempts: ${repairAttempts}`);
|
|
585
|
-
}
|
|
586
|
-
if (scenarioCorrectionsApplied > 0) {
|
|
587
|
-
console.error(` Scenario corrections: ${scenarioCorrectionsApplied}`);
|
|
588
|
-
}
|
|
589
|
-
console.error(` Artifacts written to: ${chalk.dim(config.generatedDir + '/')}`);
|
|
590
|
-
console.error(` LLM interaction log: ${chalk.dim(logPath)}`);
|
|
591
|
-
if (!verificationResult.pass) {
|
|
592
|
-
console.error('');
|
|
593
|
-
console.error(chalk.red.bold('Verification FAILED — artifacts written but policy may need review.'));
|
|
594
|
-
process.exit(1);
|
|
595
|
-
}
|
|
596
|
-
console.error('');
|
|
597
|
-
console.error(chalk.green.bold('Policy compilation successful!'));
|
|
598
59
|
}
|
|
599
60
|
// Only run when executed directly (not when imported by cli.ts or tests)
|
|
600
61
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|