@provos/ironcurtain 0.6.0 → 0.7.1
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 +29 -86
- 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 +79 -56
- 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/constitution-readonly.md +21 -0
- package/dist/config/generated/tool-annotations.json +487 -338
- package/dist/config/generated-readonly/compiled-policy.json +468 -0
- 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 +47 -0
- package/dist/config/paths.js +81 -0
- package/dist/config/paths.js.map +1 -1
- package/dist/config/tool-description-hints.json +15 -0
- package/dist/config/types.d.ts +7 -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/constitution-generator.d.ts +60 -0
- package/dist/cron/constitution-generator.js +217 -0
- package/dist/cron/constitution-generator.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 +585 -0
- package/dist/cron/job-commands.js.map +1 -0
- package/dist/cron/job-customizer.d.ts +22 -0
- package/dist/cron/job-customizer.js +47 -0
- package/dist/cron/job-customizer.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 +135 -0
- package/dist/cron/types.js +22 -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 +550 -0
- package/dist/daemon/ironcurtain-daemon.js.map +1 -0
- package/dist/docker/adapters/claude-code.js +7 -47
- 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 +12 -11
- package/dist/docker/docker-infrastructure.js.map +1 -1
- package/dist/docker/mitm-proxy.js +10 -5
- package/dist/docker/mitm-proxy.js.map +1 -1
- package/dist/docker/oauth-credentials.js +2 -2
- package/dist/docker/oauth-credentials.js.map +1 -1
- package/dist/docker/pty-session.js +146 -39
- package/dist/docker/pty-session.js.map +1 -1
- package/dist/escalation/escalation-watcher.d.ts +2 -0
- package/dist/escalation/escalation-watcher.js +3 -0
- package/dist/escalation/escalation-watcher.js.map +1 -1
- package/dist/escalation/session-registry.js +1 -14
- package/dist/escalation/session-registry.js.map +1 -1
- package/dist/index.js +43 -5
- package/dist/index.js.map +1 -1
- package/dist/mux/mux-app.js +11 -6
- package/dist/mux/mux-app.js.map +1 -1
- package/dist/mux/mux-command.js +49 -10
- package/dist/mux/mux-command.js.map +1 -1
- package/dist/mux/mux-input-handler.d.ts +5 -0
- package/dist/mux/mux-input-handler.js +64 -9
- package/dist/mux/mux-input-handler.js.map +1 -1
- package/dist/mux/mux-renderer.d.ts +20 -0
- package/dist/mux/mux-renderer.js +127 -35
- package/dist/mux/mux-renderer.js.map +1 -1
- package/dist/mux/mux-splash.js +2 -1
- package/dist/mux/mux-splash.js.map +1 -1
- 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.js +1 -1
- package/dist/mux/pty-bridge.js.map +1 -1
- package/dist/mux/types.d.ts +8 -1
- package/dist/mux/types.js +17 -5
- package/dist/mux/types.js.map +1 -1
- package/dist/persona/compile-persona-policy.d.ts +21 -0
- package/dist/persona/compile-persona-policy.js +50 -0
- package/dist/persona/compile-persona-policy.js.map +1 -0
- package/dist/persona/persona-command.d.ts +9 -0
- package/dist/persona/persona-command.js +479 -0
- package/dist/persona/persona-command.js.map +1 -0
- package/dist/persona/persona-customizer.d.ts +23 -0
- package/dist/persona/persona-customizer.js +52 -0
- package/dist/persona/persona-customizer.js.map +1 -0
- package/dist/persona/persona-prompt.d.ts +19 -0
- package/dist/persona/persona-prompt.js +42 -0
- package/dist/persona/persona-prompt.js.map +1 -0
- package/dist/persona/resolve.d.ts +46 -0
- package/dist/persona/resolve.js +108 -0
- package/dist/persona/resolve.js.map +1 -0
- package/dist/persona/types.d.ts +40 -0
- package/dist/persona/types.js +20 -0
- package/dist/persona/types.js.map +1 -0
- package/dist/pipeline/annotate.js +10 -4
- package/dist/pipeline/annotate.js.map +1 -1
- package/dist/pipeline/compile.d.ts +14 -31
- package/dist/pipeline/compile.js +50 -563
- package/dist/pipeline/compile.js.map +1 -1
- package/dist/pipeline/constitution-compiler.d.ts +6 -0
- package/dist/pipeline/constitution-compiler.js +42 -6
- package/dist/pipeline/constitution-compiler.js.map +1 -1
- package/dist/pipeline/constitution-customizer.d.ts +3 -0
- package/dist/pipeline/constitution-customizer.js +19 -5
- package/dist/pipeline/constitution-customizer.js.map +1 -1
- package/dist/pipeline/customizer-loop.d.ts +34 -0
- package/dist/pipeline/customizer-loop.js +167 -0
- package/dist/pipeline/customizer-loop.js.map +1 -0
- 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 +547 -0
- package/dist/pipeline/pipeline-runner.js.map +1 -0
- package/dist/pipeline/pipeline-shared.d.ts +37 -3
- package/dist/pipeline/pipeline-shared.js +77 -16
- 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 +114 -4
- package/dist/pipeline/tool-annotator.js.map +1 -1
- package/dist/pipeline/types.d.ts +52 -0
- package/dist/sandbox/index.js +36 -1
- package/dist/sandbox/index.js.map +1 -1
- package/dist/session/agent-session.d.ts +3 -0
- package/dist/session/agent-session.js +10 -1
- package/dist/session/agent-session.js.map +1 -1
- package/dist/session/index.d.ts +10 -0
- package/dist/session/index.js +134 -12
- 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/session-metadata.d.ts +18 -0
- package/dist/session/session-metadata.js +34 -0
- package/dist/session/session-metadata.js.map +1 -0
- package/dist/session/types.d.ts +47 -0
- package/dist/session/workspace-validation.d.ts +2 -0
- package/dist/session/workspace-validation.js +6 -5
- package/dist/session/workspace-validation.js.map +1 -1
- 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 +153 -135
- 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/domain-utils.d.ts +37 -4
- package/dist/trusted-process/domain-utils.js +130 -11
- package/dist/trusted-process/domain-utils.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 +21 -0
- package/dist/trusted-process/mcp-proxy-server.js +91 -28
- 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 +29 -7
- package/dist/trusted-process/policy-engine.js +149 -92
- 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/dist/types/slug.d.ts +7 -0
- package/dist/types/slug.js +14 -0
- package/dist/types/slug.js.map +1 -0
- package/dist/utils/editor.d.ts +24 -0
- package/dist/utils/editor.js +66 -0
- package/dist/utils/editor.js.map +1 -0
- package/docker/Dockerfile.goose +34 -0
- package/docker/entrypoint-goose.sh +30 -0
- package/package.json +1 -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,56 @@
|
|
|
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
|
-
*
|
|
7
|
+
* The core compile-verify-repair loop lives in PipelineRunner.
|
|
8
|
+
*
|
|
9
|
+
* CLI flags:
|
|
10
|
+
* --constitution <path> Use an alternative constitution file
|
|
11
|
+
* --output-dir <path> Write compiled artifacts to this directory
|
|
11
12
|
*/
|
|
12
13
|
import { resolve } from 'node:path';
|
|
13
14
|
import { fileURLToPath } from 'node:url';
|
|
14
|
-
import {
|
|
15
|
-
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
15
|
+
import { parseArgs } from 'node:util';
|
|
16
16
|
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
|
-
// ---------------------------------------------------------------------------
|
|
17
|
+
import { loadPipelineConfig, loadToolAnnotationsFile } from './pipeline-shared.js';
|
|
18
|
+
import { PipelineRunner, createPipelineModels } from './pipeline-runner.js';
|
|
19
|
+
// Re-export utilities that test files import from this file.
|
|
20
|
+
export { resolveRulePaths, mergeReplacements } from './pipeline-shared.js';
|
|
65
21
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* unique replacements that don't duplicate any remaining scenario.
|
|
22
|
+
* Parses --constitution and --output-dir from process.argv.
|
|
23
|
+
* Returns resolved absolute paths when provided.
|
|
69
24
|
*/
|
|
70
|
-
export function
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
};
|
|
25
|
+
export function parseCompilePolicyArgs(argv = process.argv.slice(2)) {
|
|
26
|
+
const { values } = parseArgs({
|
|
27
|
+
args: argv,
|
|
28
|
+
options: {
|
|
29
|
+
constitution: { type: 'string' },
|
|
30
|
+
'output-dir': { type: 'string' },
|
|
31
|
+
},
|
|
32
|
+
strict: false,
|
|
106
33
|
});
|
|
107
|
-
|
|
108
|
-
|
|
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)`);
|
|
34
|
+
const constitution = typeof values.constitution === 'string' ? values.constitution : undefined;
|
|
35
|
+
const outputDir = typeof values['output-dir'] === 'string' ? values['output-dir'] : undefined;
|
|
133
36
|
return {
|
|
134
|
-
|
|
135
|
-
|
|
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,
|
|
37
|
+
constitution: constitution ? resolve(constitution) : undefined,
|
|
38
|
+
outputDir: outputDir ? resolve(outputDir) : undefined,
|
|
245
39
|
};
|
|
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
40
|
}
|
|
304
41
|
// ---------------------------------------------------------------------------
|
|
305
42
|
// Main Pipeline
|
|
306
43
|
// ---------------------------------------------------------------------------
|
|
307
44
|
export async function main() {
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
45
|
+
const cliArgs = parseCompilePolicyArgs();
|
|
46
|
+
const config = loadPipelineConfig(cliArgs);
|
|
47
|
+
// Load tool annotations early to validate they exist before printing the banner
|
|
48
|
+
const toolAnnotationsFile = loadToolAnnotationsFile(config.generatedDir, config.packageGeneratedDir);
|
|
311
49
|
if (!toolAnnotationsFile) {
|
|
312
50
|
console.error(chalk.red.bold("Error: tool-annotations.json not found. Run 'npm run annotate-tools' first to generate tool annotations."));
|
|
313
51
|
process.exit(1);
|
|
314
52
|
}
|
|
315
|
-
// Flatten annotations from all servers
|
|
316
53
|
const allAnnotations = Object.values(toolAnnotationsFile.servers).flatMap((server) => server.tools);
|
|
317
|
-
// Compute domain allowlists for policy verification
|
|
318
|
-
const serverDomainAllowlists = extractServerDomainAllowlists(config.mcpServers);
|
|
319
54
|
console.error(chalk.bold('Policy Compilation Pipeline'));
|
|
320
55
|
console.error(chalk.bold('==========================='));
|
|
321
56
|
console.error(`Constitution: ${chalk.dim(config.constitutionPath)}`);
|
|
@@ -323,278 +58,30 @@ export async function main() {
|
|
|
323
58
|
console.error(`Output: ${chalk.dim(config.generatedDir + '/')}`);
|
|
324
59
|
console.error(`Annotations: ${chalk.dim(`${allAnnotations.length} tools from ${Object.keys(toolAnnotationsFile.servers).length} server(s)`)}`);
|
|
325
60
|
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
|
-
}
|
|
61
|
+
const models = await createPipelineModels(config.generatedDir);
|
|
62
|
+
const runner = new PipelineRunner(models);
|
|
63
|
+
try {
|
|
64
|
+
await runner.run({
|
|
65
|
+
constitutionInput: config.constitutionText,
|
|
66
|
+
constitutionKind: 'constitution',
|
|
67
|
+
outputDir: config.generatedDir,
|
|
68
|
+
toolAnnotationsDir: config.generatedDir,
|
|
69
|
+
toolAnnotationsFallbackDir: config.packageGeneratedDir,
|
|
70
|
+
allowedDirectory: config.allowedDirectory,
|
|
71
|
+
protectedPaths: config.protectedPaths,
|
|
72
|
+
mcpServers: config.mcpServers,
|
|
73
|
+
llmLogPath: models.logPath,
|
|
74
|
+
preloadedToolAnnotations: toolAnnotationsFile,
|
|
75
|
+
});
|
|
373
76
|
}
|
|
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++) {
|
|
77
|
+
catch (err) {
|
|
78
|
+
if (err instanceof Error && err.message.includes('Verification FAILED')) {
|
|
424
79
|
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
|
-
}
|
|
80
|
+
console.error(chalk.red.bold('Verification FAILED — artifacts written but policy may need review.'));
|
|
81
|
+
process.exit(1);
|
|
559
82
|
}
|
|
83
|
+
throw err;
|
|
560
84
|
}
|
|
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
85
|
}
|
|
599
86
|
// Only run when executed directly (not when imported by cli.ts or tests)
|
|
600
87
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|