@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.
Files changed (235) hide show
  1. package/README.md +29 -86
  2. package/dist/cli-help.d.ts +63 -0
  3. package/dist/cli-help.js +102 -0
  4. package/dist/cli-help.js.map +1 -0
  5. package/dist/cli.js +79 -56
  6. package/dist/cli.js.map +1 -1
  7. package/dist/config/config-command.js +136 -2
  8. package/dist/config/config-command.js.map +1 -1
  9. package/dist/config/constitution-readonly.md +21 -0
  10. package/dist/config/generated/tool-annotations.json +487 -338
  11. package/dist/config/generated-readonly/compiled-policy.json +468 -0
  12. package/dist/config/index.d.ts +21 -1
  13. package/dist/config/index.js +12 -4
  14. package/dist/config/index.js.map +1 -1
  15. package/dist/config/mcp-servers.json +2 -8
  16. package/dist/config/paths.d.ts +47 -0
  17. package/dist/config/paths.js +81 -0
  18. package/dist/config/paths.js.map +1 -1
  19. package/dist/config/tool-description-hints.json +15 -0
  20. package/dist/config/types.d.ts +7 -0
  21. package/dist/config/user-config.d.ts +21 -0
  22. package/dist/config/user-config.js +8 -0
  23. package/dist/config/user-config.js.map +1 -1
  24. package/dist/cron/compile-task-policy.d.ts +17 -0
  25. package/dist/cron/compile-task-policy.js +39 -0
  26. package/dist/cron/compile-task-policy.js.map +1 -0
  27. package/dist/cron/constitution-generator.d.ts +60 -0
  28. package/dist/cron/constitution-generator.js +217 -0
  29. package/dist/cron/constitution-generator.js.map +1 -0
  30. package/dist/cron/cron-scheduler.d.ts +65 -0
  31. package/dist/cron/cron-scheduler.js +184 -0
  32. package/dist/cron/cron-scheduler.js.map +1 -0
  33. package/dist/cron/file-lock.d.ts +41 -0
  34. package/dist/cron/file-lock.js +156 -0
  35. package/dist/cron/file-lock.js.map +1 -0
  36. package/dist/cron/format-utils.d.ts +18 -0
  37. package/dist/cron/format-utils.js +145 -0
  38. package/dist/cron/format-utils.js.map +1 -0
  39. package/dist/cron/git-sync.d.ts +22 -0
  40. package/dist/cron/git-sync.js +121 -0
  41. package/dist/cron/git-sync.js.map +1 -0
  42. package/dist/cron/headless-transport.d.ts +27 -0
  43. package/dist/cron/headless-transport.js +43 -0
  44. package/dist/cron/headless-transport.js.map +1 -0
  45. package/dist/cron/job-commands.d.ts +39 -0
  46. package/dist/cron/job-commands.js +585 -0
  47. package/dist/cron/job-commands.js.map +1 -0
  48. package/dist/cron/job-customizer.d.ts +22 -0
  49. package/dist/cron/job-customizer.js +47 -0
  50. package/dist/cron/job-customizer.js.map +1 -0
  51. package/dist/cron/job-store.d.ts +35 -0
  52. package/dist/cron/job-store.js +111 -0
  53. package/dist/cron/job-store.js.map +1 -0
  54. package/dist/cron/types.d.ts +135 -0
  55. package/dist/cron/types.js +22 -0
  56. package/dist/cron/types.js.map +1 -0
  57. package/dist/daemon/control-socket.d.ts +127 -0
  58. package/dist/daemon/control-socket.js +261 -0
  59. package/dist/daemon/control-socket.js.map +1 -0
  60. package/dist/daemon/daemon-command.d.ts +13 -0
  61. package/dist/daemon/daemon-command.js +230 -0
  62. package/dist/daemon/daemon-command.js.map +1 -0
  63. package/dist/daemon/ironcurtain-daemon.d.ts +90 -0
  64. package/dist/daemon/ironcurtain-daemon.js +550 -0
  65. package/dist/daemon/ironcurtain-daemon.js.map +1 -0
  66. package/dist/docker/adapters/claude-code.js +7 -47
  67. package/dist/docker/adapters/claude-code.js.map +1 -1
  68. package/dist/docker/adapters/goose.d.ts +58 -0
  69. package/dist/docker/adapters/goose.js +250 -0
  70. package/dist/docker/adapters/goose.js.map +1 -0
  71. package/dist/docker/adapters/shared-scripts.d.ts +29 -0
  72. package/dist/docker/adapters/shared-scripts.js +80 -0
  73. package/dist/docker/adapters/shared-scripts.js.map +1 -0
  74. package/dist/docker/agent-adapter.d.ts +12 -0
  75. package/dist/docker/agent-adapter.js.map +1 -1
  76. package/dist/docker/agent-registry.d.ts +6 -1
  77. package/dist/docker/agent-registry.js +10 -1
  78. package/dist/docker/agent-registry.js.map +1 -1
  79. package/dist/docker/docker-agent-session.d.ts +2 -25
  80. package/dist/docker/docker-agent-session.js +7 -106
  81. package/dist/docker/docker-agent-session.js.map +1 -1
  82. package/dist/docker/docker-infrastructure.d.ts +8 -0
  83. package/dist/docker/docker-infrastructure.js +12 -11
  84. package/dist/docker/docker-infrastructure.js.map +1 -1
  85. package/dist/docker/mitm-proxy.js +10 -5
  86. package/dist/docker/mitm-proxy.js.map +1 -1
  87. package/dist/docker/oauth-credentials.js +2 -2
  88. package/dist/docker/oauth-credentials.js.map +1 -1
  89. package/dist/docker/pty-session.js +146 -39
  90. package/dist/docker/pty-session.js.map +1 -1
  91. package/dist/escalation/escalation-watcher.d.ts +2 -0
  92. package/dist/escalation/escalation-watcher.js +3 -0
  93. package/dist/escalation/escalation-watcher.js.map +1 -1
  94. package/dist/escalation/session-registry.js +1 -14
  95. package/dist/escalation/session-registry.js.map +1 -1
  96. package/dist/index.js +43 -5
  97. package/dist/index.js.map +1 -1
  98. package/dist/mux/mux-app.js +11 -6
  99. package/dist/mux/mux-app.js.map +1 -1
  100. package/dist/mux/mux-command.js +49 -10
  101. package/dist/mux/mux-command.js.map +1 -1
  102. package/dist/mux/mux-input-handler.d.ts +5 -0
  103. package/dist/mux/mux-input-handler.js +64 -9
  104. package/dist/mux/mux-input-handler.js.map +1 -1
  105. package/dist/mux/mux-renderer.d.ts +20 -0
  106. package/dist/mux/mux-renderer.js +127 -35
  107. package/dist/mux/mux-renderer.js.map +1 -1
  108. package/dist/mux/mux-splash.js +2 -1
  109. package/dist/mux/mux-splash.js.map +1 -1
  110. package/dist/mux/paste-interceptor.d.ts +22 -0
  111. package/dist/mux/paste-interceptor.js +143 -0
  112. package/dist/mux/paste-interceptor.js.map +1 -0
  113. package/dist/mux/pty-bridge.js +1 -1
  114. package/dist/mux/pty-bridge.js.map +1 -1
  115. package/dist/mux/types.d.ts +8 -1
  116. package/dist/mux/types.js +17 -5
  117. package/dist/mux/types.js.map +1 -1
  118. package/dist/persona/compile-persona-policy.d.ts +21 -0
  119. package/dist/persona/compile-persona-policy.js +50 -0
  120. package/dist/persona/compile-persona-policy.js.map +1 -0
  121. package/dist/persona/persona-command.d.ts +9 -0
  122. package/dist/persona/persona-command.js +479 -0
  123. package/dist/persona/persona-command.js.map +1 -0
  124. package/dist/persona/persona-customizer.d.ts +23 -0
  125. package/dist/persona/persona-customizer.js +52 -0
  126. package/dist/persona/persona-customizer.js.map +1 -0
  127. package/dist/persona/persona-prompt.d.ts +19 -0
  128. package/dist/persona/persona-prompt.js +42 -0
  129. package/dist/persona/persona-prompt.js.map +1 -0
  130. package/dist/persona/resolve.d.ts +46 -0
  131. package/dist/persona/resolve.js +108 -0
  132. package/dist/persona/resolve.js.map +1 -0
  133. package/dist/persona/types.d.ts +40 -0
  134. package/dist/persona/types.js +20 -0
  135. package/dist/persona/types.js.map +1 -0
  136. package/dist/pipeline/annotate.js +10 -4
  137. package/dist/pipeline/annotate.js.map +1 -1
  138. package/dist/pipeline/compile.d.ts +14 -31
  139. package/dist/pipeline/compile.js +50 -563
  140. package/dist/pipeline/compile.js.map +1 -1
  141. package/dist/pipeline/constitution-compiler.d.ts +6 -0
  142. package/dist/pipeline/constitution-compiler.js +42 -6
  143. package/dist/pipeline/constitution-compiler.js.map +1 -1
  144. package/dist/pipeline/constitution-customizer.d.ts +3 -0
  145. package/dist/pipeline/constitution-customizer.js +19 -5
  146. package/dist/pipeline/constitution-customizer.js.map +1 -1
  147. package/dist/pipeline/customizer-loop.d.ts +34 -0
  148. package/dist/pipeline/customizer-loop.js +167 -0
  149. package/dist/pipeline/customizer-loop.js.map +1 -0
  150. package/dist/pipeline/mcp-connections.d.ts +16 -0
  151. package/dist/pipeline/mcp-connections.js +61 -0
  152. package/dist/pipeline/mcp-connections.js.map +1 -0
  153. package/dist/pipeline/pipeline-runner.d.ts +86 -0
  154. package/dist/pipeline/pipeline-runner.js +547 -0
  155. package/dist/pipeline/pipeline-runner.js.map +1 -0
  156. package/dist/pipeline/pipeline-shared.d.ts +37 -3
  157. package/dist/pipeline/pipeline-shared.js +77 -16
  158. package/dist/pipeline/pipeline-shared.js.map +1 -1
  159. package/dist/pipeline/refresh-lists.js +38 -12
  160. package/dist/pipeline/refresh-lists.js.map +1 -1
  161. package/dist/pipeline/scenario-generator.js +2 -1
  162. package/dist/pipeline/scenario-generator.js.map +1 -1
  163. package/dist/pipeline/tool-annotator.d.ts +3 -3
  164. package/dist/pipeline/tool-annotator.js +114 -4
  165. package/dist/pipeline/tool-annotator.js.map +1 -1
  166. package/dist/pipeline/types.d.ts +52 -0
  167. package/dist/sandbox/index.js +36 -1
  168. package/dist/sandbox/index.js.map +1 -1
  169. package/dist/session/agent-session.d.ts +3 -0
  170. package/dist/session/agent-session.js +10 -1
  171. package/dist/session/agent-session.js.map +1 -1
  172. package/dist/session/index.d.ts +10 -0
  173. package/dist/session/index.js +134 -12
  174. package/dist/session/index.js.map +1 -1
  175. package/dist/session/preflight.js +26 -6
  176. package/dist/session/preflight.js.map +1 -1
  177. package/dist/session/prompts.d.ts +14 -0
  178. package/dist/session/prompts.js +39 -2
  179. package/dist/session/prompts.js.map +1 -1
  180. package/dist/session/session-manager.d.ts +105 -0
  181. package/dist/session/session-manager.js +134 -0
  182. package/dist/session/session-manager.js.map +1 -0
  183. package/dist/session/session-metadata.d.ts +18 -0
  184. package/dist/session/session-metadata.js +34 -0
  185. package/dist/session/session-metadata.js.map +1 -0
  186. package/dist/session/types.d.ts +47 -0
  187. package/dist/session/workspace-validation.d.ts +2 -0
  188. package/dist/session/workspace-validation.js +6 -5
  189. package/dist/session/workspace-validation.js.map +1 -1
  190. package/dist/signal/bot-command.d.ts +2 -0
  191. package/dist/signal/bot-command.js +8 -0
  192. package/dist/signal/bot-command.js.map +1 -1
  193. package/dist/signal/format.d.ts +27 -0
  194. package/dist/signal/format.js +59 -0
  195. package/dist/signal/format.js.map +1 -1
  196. package/dist/signal/signal-bot-daemon.d.ts +27 -25
  197. package/dist/signal/signal-bot-daemon.js +153 -135
  198. package/dist/signal/signal-bot-daemon.js.map +1 -1
  199. package/dist/signal/signal-container.js +63 -19
  200. package/dist/signal/signal-container.js.map +1 -1
  201. package/dist/signal/signal-transport.js +3 -8
  202. package/dist/signal/signal-transport.js.map +1 -1
  203. package/dist/trusted-process/domain-utils.d.ts +37 -4
  204. package/dist/trusted-process/domain-utils.js +130 -11
  205. package/dist/trusted-process/domain-utils.js.map +1 -1
  206. package/dist/trusted-process/index.js +6 -2
  207. package/dist/trusted-process/index.js.map +1 -1
  208. package/dist/trusted-process/mcp-client-manager.js +11 -5
  209. package/dist/trusted-process/mcp-client-manager.js.map +1 -1
  210. package/dist/trusted-process/mcp-proxy-server.d.ts +21 -0
  211. package/dist/trusted-process/mcp-proxy-server.js +91 -28
  212. package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
  213. package/dist/trusted-process/permissive-output-validator.d.ts +23 -0
  214. package/dist/trusted-process/permissive-output-validator.js +10 -0
  215. package/dist/trusted-process/permissive-output-validator.js.map +1 -0
  216. package/dist/trusted-process/policy-engine.d.ts +29 -7
  217. package/dist/trusted-process/policy-engine.js +149 -92
  218. package/dist/trusted-process/policy-engine.js.map +1 -1
  219. package/dist/trusted-process/tool-description-hints.d.ts +19 -0
  220. package/dist/trusted-process/tool-description-hints.js +53 -0
  221. package/dist/trusted-process/tool-description-hints.js.map +1 -0
  222. package/dist/types/argument-roles.d.ts +33 -1
  223. package/dist/types/argument-roles.js +129 -1
  224. package/dist/types/argument-roles.js.map +1 -1
  225. package/dist/types/slug.d.ts +7 -0
  226. package/dist/types/slug.js +14 -0
  227. package/dist/types/slug.js.map +1 -0
  228. package/dist/utils/editor.d.ts +24 -0
  229. package/dist/utils/editor.js +66 -0
  230. package/dist/utils/editor.js.map +1 -0
  231. package/docker/Dockerfile.goose +34 -0
  232. package/docker/entrypoint-goose.sh +30 -0
  233. package/package.json +1 -1
  234. package/src/config/generated/tool-annotations.json +487 -338
  235. package/src/config/mcp-servers.json +2 -8
@@ -1,321 +1,56 @@
1
1
  /**
2
2
  * CLI entry point for `ironcurtain compile-policy`.
3
3
  *
4
- * Orchestrates the policy compilation pipeline (stages 1-3):
5
- * 1. Compile constitution into declarative rules via LLM
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
- * Requires tool-annotations.json to exist on disk. Run
10
- * `npm run annotate-tools` first to generate it.
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 { Client } from '@modelcontextprotocol/sdk/client/index.js';
15
- import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
15
+ import { parseArgs } from 'node:util';
16
16
  import chalk from 'chalk';
17
- import { extractServerDomainAllowlists } from '../config/index.js';
18
- import { PolicyEngine } from '../trusted-process/policy-engine.js';
19
- import { resolveRealPath } from '../types/argument-roles.js';
20
- import { buildCompilerSystemPrompt, compileConstitution, ConstitutionCompilerSession, validateCompiledRules, } from './constitution-compiler.js';
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
- * 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.
22
+ * Parses --constitution and --output-dir from process.argv.
23
+ * Returns resolved absolute paths when provided.
69
24
  */
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
- };
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
- 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)`);
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
- 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,
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 config = loadPipelineConfig();
309
- // Load tool annotations from disk (produced by `npm run annotate-tools`)
310
- const toolAnnotationsFile = loadExistingArtifact(config.generatedDir, 'tool-annotations.json', config.packageGeneratedDir);
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 { model: llm, logContext, logPath, cacheStrategy } = await createPipelineLlm(config.generatedDir, 'unknown');
327
- // Build raw system prompt and derive hash + cache-wrapped version
328
- const handwrittenScenarios = getHandwrittenScenarios(config.allowedDirectory);
329
- const compilerPrompt = buildCompilerSystemPrompt(config.constitutionText, allAnnotations, { protectedPaths: config.protectedPaths }, handwrittenScenarios);
330
- const compilerHash = computePolicyHash(compilerPrompt, allAnnotations);
331
- const compilerSystem = cacheStrategy.wrapSystemPrompt(compilerPrompt);
332
- // Load existing artifacts for cache comparison
333
- const existingPolicy = loadExistingArtifact(config.generatedDir, 'compiled-policy.json', config.packageGeneratedDir);
334
- const existingScenarios = loadExistingArtifact(config.generatedDir, 'test-scenarios.json', config.packageGeneratedDir);
335
- // Compile constitution into policy rules (LLM-cacheable)
336
- // Step numbering depends on whether list definitions are emitted,
337
- // so compilation always uses [1/N] and the total is determined after.
338
- logContext.stepName = 'compile-constitution';
339
- let compilationResult = await compilePolicyRules(config.constitutionText, allAnnotations, config.protectedPaths, compilerHash, existingPolicy, llm, '[1/3]', compilerSystem);
340
- // Build and write policy artifact immediately so it's available for
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
- // Extract permitted directories from compiled rules for scenario generation
375
- const permittedDirectories = extractPermittedDirectories(compilationResult.rules);
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
- // Gather judge analysis and attributions from the most recent verification
426
- const lastRound = verificationResult.rounds[verificationResult.rounds.length - 1];
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)) {