@provos/ironcurtain 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/README.md +21 -2
  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 +73 -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/generated/tool-annotations.json +487 -338
  10. package/dist/config/index.d.ts +21 -1
  11. package/dist/config/index.js +12 -4
  12. package/dist/config/index.js.map +1 -1
  13. package/dist/config/mcp-servers.json +2 -8
  14. package/dist/config/paths.d.ts +30 -0
  15. package/dist/config/paths.js +58 -0
  16. package/dist/config/paths.js.map +1 -1
  17. package/dist/config/tool-description-hints.json +15 -0
  18. package/dist/config/types.d.ts +7 -0
  19. package/dist/config/user-config.d.ts +21 -0
  20. package/dist/config/user-config.js +8 -0
  21. package/dist/config/user-config.js.map +1 -1
  22. package/dist/cron/compile-task-policy.d.ts +17 -0
  23. package/dist/cron/compile-task-policy.js +39 -0
  24. package/dist/cron/compile-task-policy.js.map +1 -0
  25. package/dist/cron/cron-scheduler.d.ts +65 -0
  26. package/dist/cron/cron-scheduler.js +184 -0
  27. package/dist/cron/cron-scheduler.js.map +1 -0
  28. package/dist/cron/file-lock.d.ts +41 -0
  29. package/dist/cron/file-lock.js +156 -0
  30. package/dist/cron/file-lock.js.map +1 -0
  31. package/dist/cron/format-utils.d.ts +18 -0
  32. package/dist/cron/format-utils.js +145 -0
  33. package/dist/cron/format-utils.js.map +1 -0
  34. package/dist/cron/git-sync.d.ts +22 -0
  35. package/dist/cron/git-sync.js +121 -0
  36. package/dist/cron/git-sync.js.map +1 -0
  37. package/dist/cron/headless-transport.d.ts +27 -0
  38. package/dist/cron/headless-transport.js +43 -0
  39. package/dist/cron/headless-transport.js.map +1 -0
  40. package/dist/cron/job-commands.d.ts +39 -0
  41. package/dist/cron/job-commands.js +522 -0
  42. package/dist/cron/job-commands.js.map +1 -0
  43. package/dist/cron/job-store.d.ts +35 -0
  44. package/dist/cron/job-store.js +111 -0
  45. package/dist/cron/job-store.js.map +1 -0
  46. package/dist/cron/types.d.ts +127 -0
  47. package/dist/cron/types.js +25 -0
  48. package/dist/cron/types.js.map +1 -0
  49. package/dist/daemon/control-socket.d.ts +127 -0
  50. package/dist/daemon/control-socket.js +261 -0
  51. package/dist/daemon/control-socket.js.map +1 -0
  52. package/dist/daemon/daemon-command.d.ts +13 -0
  53. package/dist/daemon/daemon-command.js +230 -0
  54. package/dist/daemon/daemon-command.js.map +1 -0
  55. package/dist/daemon/ironcurtain-daemon.d.ts +90 -0
  56. package/dist/daemon/ironcurtain-daemon.js +548 -0
  57. package/dist/daemon/ironcurtain-daemon.js.map +1 -0
  58. package/dist/docker/adapters/claude-code.js +7 -47
  59. package/dist/docker/adapters/claude-code.js.map +1 -1
  60. package/dist/docker/adapters/goose.d.ts +58 -0
  61. package/dist/docker/adapters/goose.js +250 -0
  62. package/dist/docker/adapters/goose.js.map +1 -0
  63. package/dist/docker/adapters/shared-scripts.d.ts +29 -0
  64. package/dist/docker/adapters/shared-scripts.js +80 -0
  65. package/dist/docker/adapters/shared-scripts.js.map +1 -0
  66. package/dist/docker/agent-adapter.d.ts +12 -0
  67. package/dist/docker/agent-adapter.js.map +1 -1
  68. package/dist/docker/agent-registry.d.ts +6 -1
  69. package/dist/docker/agent-registry.js +10 -1
  70. package/dist/docker/agent-registry.js.map +1 -1
  71. package/dist/docker/docker-agent-session.d.ts +2 -25
  72. package/dist/docker/docker-agent-session.js +7 -106
  73. package/dist/docker/docker-agent-session.js.map +1 -1
  74. package/dist/docker/docker-infrastructure.d.ts +8 -0
  75. package/dist/docker/docker-infrastructure.js +12 -11
  76. package/dist/docker/docker-infrastructure.js.map +1 -1
  77. package/dist/docker/pty-session.js +7 -4
  78. package/dist/docker/pty-session.js.map +1 -1
  79. package/dist/escalation/escalation-watcher.d.ts +2 -0
  80. package/dist/escalation/escalation-watcher.js +3 -0
  81. package/dist/escalation/escalation-watcher.js.map +1 -1
  82. package/dist/escalation/session-registry.js +1 -14
  83. package/dist/escalation/session-registry.js.map +1 -1
  84. package/dist/index.js +30 -1
  85. package/dist/index.js.map +1 -1
  86. package/dist/mux/mux-app.js +11 -6
  87. package/dist/mux/mux-app.js.map +1 -1
  88. package/dist/mux/mux-command.js +20 -9
  89. package/dist/mux/mux-command.js.map +1 -1
  90. package/dist/mux/mux-input-handler.d.ts +5 -0
  91. package/dist/mux/mux-input-handler.js +64 -9
  92. package/dist/mux/mux-input-handler.js.map +1 -1
  93. package/dist/mux/mux-renderer.d.ts +20 -0
  94. package/dist/mux/mux-renderer.js +127 -35
  95. package/dist/mux/mux-renderer.js.map +1 -1
  96. package/dist/mux/mux-splash.js +2 -1
  97. package/dist/mux/mux-splash.js.map +1 -1
  98. package/dist/mux/paste-interceptor.d.ts +22 -0
  99. package/dist/mux/paste-interceptor.js +143 -0
  100. package/dist/mux/paste-interceptor.js.map +1 -0
  101. package/dist/mux/pty-bridge.js +1 -1
  102. package/dist/mux/pty-bridge.js.map +1 -1
  103. package/dist/mux/types.d.ts +8 -1
  104. package/dist/mux/types.js +17 -5
  105. package/dist/mux/types.js.map +1 -1
  106. package/dist/pipeline/annotate.js +2 -1
  107. package/dist/pipeline/annotate.js.map +1 -1
  108. package/dist/pipeline/compile.d.ts +4 -34
  109. package/dist/pipeline/compile.js +29 -568
  110. package/dist/pipeline/compile.js.map +1 -1
  111. package/dist/pipeline/constitution-compiler.js +1 -1
  112. package/dist/pipeline/mcp-connections.d.ts +16 -0
  113. package/dist/pipeline/mcp-connections.js +61 -0
  114. package/dist/pipeline/mcp-connections.js.map +1 -0
  115. package/dist/pipeline/pipeline-runner.d.ts +86 -0
  116. package/dist/pipeline/pipeline-runner.js +554 -0
  117. package/dist/pipeline/pipeline-runner.js.map +1 -0
  118. package/dist/pipeline/pipeline-shared.d.ts +29 -2
  119. package/dist/pipeline/pipeline-shared.js +68 -12
  120. package/dist/pipeline/pipeline-shared.js.map +1 -1
  121. package/dist/pipeline/refresh-lists.js +38 -12
  122. package/dist/pipeline/refresh-lists.js.map +1 -1
  123. package/dist/pipeline/scenario-generator.js +2 -1
  124. package/dist/pipeline/scenario-generator.js.map +1 -1
  125. package/dist/pipeline/tool-annotator.d.ts +3 -3
  126. package/dist/pipeline/tool-annotator.js +96 -4
  127. package/dist/pipeline/tool-annotator.js.map +1 -1
  128. package/dist/pipeline/types.d.ts +52 -0
  129. package/dist/sandbox/index.js +36 -1
  130. package/dist/sandbox/index.js.map +1 -1
  131. package/dist/session/agent-session.d.ts +3 -0
  132. package/dist/session/agent-session.js +10 -1
  133. package/dist/session/agent-session.js.map +1 -1
  134. package/dist/session/index.js +54 -5
  135. package/dist/session/index.js.map +1 -1
  136. package/dist/session/preflight.js +26 -6
  137. package/dist/session/preflight.js.map +1 -1
  138. package/dist/session/prompts.d.ts +14 -0
  139. package/dist/session/prompts.js +39 -2
  140. package/dist/session/prompts.js.map +1 -1
  141. package/dist/session/session-manager.d.ts +105 -0
  142. package/dist/session/session-manager.js +134 -0
  143. package/dist/session/session-manager.js.map +1 -0
  144. package/dist/session/types.d.ts +27 -0
  145. package/dist/session/workspace-validation.d.ts +2 -0
  146. package/dist/session/workspace-validation.js +6 -5
  147. package/dist/session/workspace-validation.js.map +1 -1
  148. package/dist/signal/bot-command.d.ts +2 -0
  149. package/dist/signal/bot-command.js +8 -0
  150. package/dist/signal/bot-command.js.map +1 -1
  151. package/dist/signal/format.d.ts +27 -0
  152. package/dist/signal/format.js +59 -0
  153. package/dist/signal/format.js.map +1 -1
  154. package/dist/signal/signal-bot-daemon.d.ts +27 -25
  155. package/dist/signal/signal-bot-daemon.js +145 -130
  156. package/dist/signal/signal-bot-daemon.js.map +1 -1
  157. package/dist/signal/signal-container.js +63 -19
  158. package/dist/signal/signal-container.js.map +1 -1
  159. package/dist/signal/signal-transport.js +3 -8
  160. package/dist/signal/signal-transport.js.map +1 -1
  161. package/dist/trusted-process/domain-utils.d.ts +38 -4
  162. package/dist/trusted-process/domain-utils.js +118 -11
  163. package/dist/trusted-process/domain-utils.js.map +1 -1
  164. package/dist/trusted-process/index.js +6 -2
  165. package/dist/trusted-process/index.js.map +1 -1
  166. package/dist/trusted-process/mcp-client-manager.js +11 -5
  167. package/dist/trusted-process/mcp-client-manager.js.map +1 -1
  168. package/dist/trusted-process/mcp-proxy-server.d.ts +2 -0
  169. package/dist/trusted-process/mcp-proxy-server.js +59 -16
  170. package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
  171. package/dist/trusted-process/permissive-output-validator.d.ts +23 -0
  172. package/dist/trusted-process/permissive-output-validator.js +10 -0
  173. package/dist/trusted-process/permissive-output-validator.js.map +1 -0
  174. package/dist/trusted-process/policy-engine.d.ts +27 -4
  175. package/dist/trusted-process/policy-engine.js +152 -57
  176. package/dist/trusted-process/policy-engine.js.map +1 -1
  177. package/dist/trusted-process/tool-description-hints.d.ts +19 -0
  178. package/dist/trusted-process/tool-description-hints.js +53 -0
  179. package/dist/trusted-process/tool-description-hints.js.map +1 -0
  180. package/dist/types/argument-roles.d.ts +33 -1
  181. package/dist/types/argument-roles.js +129 -1
  182. package/dist/types/argument-roles.js.map +1 -1
  183. package/docker/Dockerfile.goose +34 -0
  184. package/docker/entrypoint-goose.sh +30 -0
  185. package/package.json +1 -1
  186. package/src/config/generated/tool-annotations.json +487 -338
  187. package/src/config/mcp-servers.json +2 -8
@@ -1,321 +1,30 @@
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.
11
8
  */
12
9
  import { resolve } from 'node:path';
13
10
  import { fileURLToPath } from 'node:url';
14
- import { Client } from '@modelcontextprotocol/sdk/client/index.js';
15
- import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
16
11
  import chalk from 'chalk';
17
- import { 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
- // ---------------------------------------------------------------------------
65
- /**
66
- * Merges replacement scenarios from the regeneration session into the
67
- * scenario list. Removes corrected and discarded scenarios, then adds
68
- * unique replacements that don't duplicate any remaining scenario.
69
- */
70
- export function mergeReplacements(scenarios, replacements, corrections, discardedScenarios) {
71
- const removedDescriptions = new Set([
72
- ...corrections.map((c) => c.scenarioDescription),
73
- ...discardedScenarios.filter((d) => d.scenario.source !== 'handwritten').map((d) => d.scenario.description),
74
- ]);
75
- const kept = scenarios.filter((s) => !removedDescriptions.has(s.description));
76
- const keptDescriptions = new Set(kept.map((s) => s.description));
77
- const uniqueReplacements = replacements.filter((r) => !keptDescriptions.has(r.description));
78
- return [...kept, ...uniqueReplacements];
79
- }
80
- // ---------------------------------------------------------------------------
81
- // Path Resolution (post-LLM normalization)
82
- // ---------------------------------------------------------------------------
83
- /**
84
- * Resolves all `paths.within` values in compiled rules to their real
85
- * filesystem paths, following symlinks. This ensures that symlinked
86
- * directories (e.g., ~/Downloads -> /mnt/c/.../Downloads on WSL) are
87
- * resolved to their canonical form so that runtime path comparisons
88
- * match correctly.
89
- *
90
- * Falls back to path.resolve() if the path does not exist on disk.
91
- */
92
- export function resolveRulePaths(rules) {
93
- return rules.map((rule) => {
94
- if (!rule.if.paths?.within)
95
- return rule;
96
- const resolved = resolveRealPath(rule.if.paths.within);
97
- if (resolved === rule.if.paths.within)
98
- return rule;
99
- return {
100
- ...rule,
101
- if: {
102
- ...rule.if,
103
- paths: { ...rule.if.paths, within: resolved },
104
- },
105
- };
106
- });
107
- }
108
- async function compilePolicyRules(constitutionText, annotations, protectedPaths, inputHash, existingPolicy, llm, stepLabel = '[1/3]', system) {
109
- const stepText = `${stepLabel} Compiling constitution`;
110
- // Check cache: skip LLM call if inputs haven't changed.
111
- // Still resolve paths in case symlink targets changed since last run.
112
- if (existingPolicy && existingPolicy.inputHash === inputHash) {
113
- showCached(stepText);
114
- return {
115
- rules: resolveRulePaths(existingPolicy.rules),
116
- listDefinitions: existingPolicy.listDefinitions ?? [],
117
- inputHash,
118
- };
119
- }
120
- const session = new ConstitutionCompilerSession({
121
- system: system ?? buildCompilerSystemPrompt(constitutionText, annotations, { protectedPaths }),
122
- model: llm,
123
- annotations,
124
- });
125
- const { result: compilationOutput } = await withSpinner(stepText, async (spinner) => {
126
- const output = await session.compile((msg) => {
127
- spinner.text = `${stepText} — ${msg}`;
128
- });
129
- const rules = resolveRulePaths(output.rules);
130
- validateRulesOrThrow(rules, output.listDefinitions);
131
- return { rules, listDefinitions: output.listDefinitions };
132
- }, (output, elapsed) => `${stepText}: ${output.rules.length} rules compiled (${elapsed.toFixed(1)}s)`);
133
- return {
134
- rules: compilationOutput.rules,
135
- listDefinitions: compilationOutput.listDefinitions,
136
- inputHash,
137
- session,
138
- };
139
- }
140
- class RuleValidationError extends Error {
141
- errors;
142
- constructor(errors) {
143
- super('Compiled rule validation failed');
144
- this.errors = errors;
145
- this.name = 'RuleValidationError';
146
- }
147
- }
148
- function validateRulesOrThrow(rules, listDefinitions = []) {
149
- const ruleValidation = validateCompiledRules(rules, listDefinitions);
150
- if (ruleValidation.warnings.length > 0) {
151
- for (const w of ruleValidation.warnings) {
152
- console.error(` ${chalk.yellow('Warning:')} ${w}`);
153
- }
154
- }
155
- if (!ruleValidation.valid) {
156
- throw new RuleValidationError(ruleValidation.errors);
157
- }
158
- }
159
- async function compilePolicyRulesWithRepair(constitutionText, annotations, protectedPaths, baseInputHash, repairContext, llm, onProgress, system, session) {
160
- let output;
161
- if (session) {
162
- output = await session.recompile(repairContext, onProgress);
163
- }
164
- else {
165
- output = await compileConstitution(constitutionText, annotations, { protectedPaths }, llm, repairContext, onProgress, system);
166
- }
167
- const compiledRules = resolveRulePaths(output.rules);
168
- validateRulesOrThrow(compiledRules, output.listDefinitions);
169
- return {
170
- rules: compiledRules,
171
- listDefinitions: output.listDefinitions,
172
- inputHash: `${baseInputHash}-repair`,
173
- session,
174
- };
175
- }
176
- // ---------------------------------------------------------------------------
177
- // Artifact Construction (pure data transformation)
178
- // ---------------------------------------------------------------------------
179
- function buildPolicyArtifact(constitutionHash, compilationResult) {
180
- const artifact = {
181
- generatedAt: new Date().toISOString(),
182
- constitutionHash,
183
- inputHash: compilationResult.inputHash,
184
- rules: compilationResult.rules,
185
- };
186
- if (compilationResult.listDefinitions.length > 0) {
187
- artifact.listDefinitions = compilationResult.listDefinitions;
188
- }
189
- return artifact;
190
- }
191
- async function generateTestScenarios(constitutionText, annotations, allowedDirectory, permittedDirectories, inputHash, existingScenarios, llm, stepLabel = '[2/3]', system) {
192
- const handwrittenScenarios = getHandwrittenScenarios(allowedDirectory);
193
- const stepText = `${stepLabel} Generating test scenarios`;
194
- // Check cache: skip LLM call if inputs haven't changed
195
- if (existingScenarios && existingScenarios.inputHash === inputHash) {
196
- showCached(stepText);
197
- return { scenarios: existingScenarios.scenarios, inputHash };
198
- }
199
- // Create a multi-turn session so the repair loop can feed corrections back
200
- const session = new ScenarioGeneratorSession({
201
- system: system ?? buildGeneratorSystemPrompt(constitutionText, annotations, allowedDirectory, permittedDirectories),
202
- model: llm,
203
- annotations,
204
- handwrittenScenarios,
205
- });
206
- const { result: scenarios } = await withSpinner(stepText, async (spinner) => session.generate((msg) => {
207
- spinner.text = `${stepText} — ${msg}`;
208
- }), (scenarios, elapsed) => {
209
- const generatedCount = scenarios.length - handwrittenScenarios.length;
210
- return `${stepText}: ${scenarios.length} scenarios (${handwrittenScenarios.length} handwritten + ${generatedCount} generated) (${elapsed.toFixed(1)}s)`;
211
- });
212
- return { scenarios, inputHash, session };
213
- }
214
- // ---------------------------------------------------------------------------
215
- // Policy Verification (LLM step)
216
- // ---------------------------------------------------------------------------
217
- async function verifyCompiledPolicy(constitutionText, compiledPolicyFile, toolAnnotationsFile, protectedPaths, scenarios, llm, allowedDirectory, maxRounds = 3, verbose = true, onProgress, serverDomainAllowlists, dynamicLists, system, session) {
218
- const result = await verifyPolicy(constitutionText, compiledPolicyFile, toolAnnotationsFile, protectedPaths, scenarios, llm, maxRounds, allowedDirectory, onProgress, serverDomainAllowlists, dynamicLists, system, session);
219
- if (!result.pass) {
220
- if (verbose) {
221
- console.error('');
222
- console.error(chalk.red('Verification FAILED:'));
223
- console.error(result.summary);
224
- console.error('');
225
- for (const f of result.failedScenarios) {
226
- console.error(` ${chalk.red('FAIL:')} ${f.scenario.description}`);
227
- console.error(` Expected: ${f.scenario.expectedDecision}, Got: ${f.actualDecision} (rule: ${f.matchingRule})`);
228
- }
229
- }
230
- else {
231
- console.error(` ${result.failedScenarios.length} scenario(s) failed.`);
232
- }
233
- }
234
- return result;
235
- }
236
- // ---------------------------------------------------------------------------
237
- // Artifact Output
238
- // ---------------------------------------------------------------------------
239
- function writeScenariosArtifact(generatedDir, constitutionHash, scenarioResult) {
240
- const scenariosFile = {
241
- generatedAt: new Date().toISOString(),
242
- constitutionHash,
243
- inputHash: scenarioResult.inputHash,
244
- scenarios: scenarioResult.scenarios,
245
- };
246
- writeArtifact(generatedDir, 'test-scenarios.json', scenariosFile);
247
- }
248
- function writePolicyArtifact(generatedDir, compiledPolicyFile) {
249
- writeArtifact(generatedDir, 'compiled-policy.json', compiledPolicyFile);
250
- }
251
- // ---------------------------------------------------------------------------
252
- // MCP Server Connection for Data-Backed Lists
253
- // ---------------------------------------------------------------------------
254
- /**
255
- * Connects to MCP servers needed for data-backed list resolution.
256
- * Only connects to servers hinted by list definitions with requiresMcp: true.
257
- * Returns a map keyed by server name for the resolver.
258
- */
259
- export async function connectMcpServersForLists(definitions, mcpServers) {
260
- const mcpDefs = definitions.filter((d) => d.requiresMcp);
261
- const hasUnhintedLists = mcpDefs.some((d) => !d.mcpServerHint);
262
- // Connect all configured servers if any list lacks a hint,
263
- // otherwise connect only the hinted servers.
264
- const neededServers = hasUnhintedLists
265
- ? new Set(Object.keys(mcpServers))
266
- : new Set(mcpDefs
267
- .filter((d) => d.mcpServerHint != null)
268
- .map((d) => d.mcpServerHint));
269
- const connections = new Map();
270
- for (const serverName of neededServers) {
271
- const serverConfig = mcpServers[serverName];
272
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- serverName may not exist in mcpServers at runtime (from mcpServerHint)
273
- if (!serverConfig) {
274
- console.error(` ${chalk.yellow('Warning:')} MCP server "${serverName}" not configured — skipping`);
275
- continue;
276
- }
277
- const transport = new StdioClientTransport({
278
- command: serverConfig.command,
279
- args: serverConfig.args,
280
- env: serverConfig.env ? { ...process.env, ...serverConfig.env } : undefined,
281
- stderr: 'pipe',
282
- });
283
- // Drain piped stderr to prevent backpressure
284
- if (transport.stderr) {
285
- transport.stderr.on('data', () => { });
286
- }
287
- const client = new Client({ name: 'ironcurtain-list-resolver', version: VERSION });
288
- await client.connect(transport);
289
- const toolsResult = await client.listTools();
290
- connections.set(serverName, { client, tools: toolsResult.tools });
291
- }
292
- return connections;
293
- }
294
- export async function disconnectMcpServers(connections) {
295
- for (const conn of connections.values()) {
296
- try {
297
- await conn.client.close();
298
- }
299
- catch {
300
- // Ignore cleanup errors
301
- }
302
- }
303
- }
12
+ import { loadPipelineConfig, loadToolAnnotationsFile } from './pipeline-shared.js';
13
+ import { PipelineRunner, createPipelineModels } from './pipeline-runner.js';
14
+ // Re-export utilities that test files import from this file.
15
+ export { resolveRulePaths, mergeReplacements } from './pipeline-shared.js';
304
16
  // ---------------------------------------------------------------------------
305
17
  // Main Pipeline
306
18
  // ---------------------------------------------------------------------------
307
19
  export async function main() {
308
20
  const config = loadPipelineConfig();
309
- // Load tool annotations from disk (produced by `npm run annotate-tools`)
310
- const toolAnnotationsFile = loadExistingArtifact(config.generatedDir, 'tool-annotations.json', config.packageGeneratedDir);
21
+ // Load tool annotations early to validate they exist before printing the banner
22
+ const toolAnnotationsFile = loadToolAnnotationsFile(config.generatedDir, config.packageGeneratedDir);
311
23
  if (!toolAnnotationsFile) {
312
24
  console.error(chalk.red.bold("Error: tool-annotations.json not found. Run 'npm run annotate-tools' first to generate tool annotations."));
313
25
  process.exit(1);
314
26
  }
315
- // Flatten annotations from all servers
316
27
  const allAnnotations = Object.values(toolAnnotationsFile.servers).flatMap((server) => server.tools);
317
- // Compute domain allowlists for policy verification
318
- const serverDomainAllowlists = extractServerDomainAllowlists(config.mcpServers);
319
28
  console.error(chalk.bold('Policy Compilation Pipeline'));
320
29
  console.error(chalk.bold('==========================='));
321
30
  console.error(`Constitution: ${chalk.dim(config.constitutionPath)}`);
@@ -323,278 +32,30 @@ export async function main() {
323
32
  console.error(`Output: ${chalk.dim(config.generatedDir + '/')}`);
324
33
  console.error(`Annotations: ${chalk.dim(`${allAnnotations.length} tools from ${Object.keys(toolAnnotationsFile.servers).length} server(s)`)}`);
325
34
  console.error('');
326
- const { 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
- }
35
+ const models = await createPipelineModels(config.generatedDir);
36
+ const runner = new PipelineRunner(models);
37
+ try {
38
+ await runner.run({
39
+ constitutionInput: config.constitutionText,
40
+ constitutionKind: 'constitution',
41
+ outputDir: config.generatedDir,
42
+ toolAnnotationsDir: config.generatedDir,
43
+ toolAnnotationsFallbackDir: config.packageGeneratedDir,
44
+ allowedDirectory: config.allowedDirectory,
45
+ protectedPaths: config.protectedPaths,
46
+ mcpServers: config.mcpServers,
47
+ llmLogPath: models.logPath,
48
+ preloadedToolAnnotations: toolAnnotationsFile,
49
+ });
373
50
  }
374
- // 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++) {
51
+ catch (err) {
52
+ if (err instanceof Error && err.message.includes('Verification FAILED')) {
424
53
  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
- }
54
+ console.error(chalk.red.bold('Verification FAILED artifacts written but policy may need review.'));
55
+ process.exit(1);
559
56
  }
57
+ throw err;
560
58
  }
561
- // Re-write scenarios artifact if the repair loop modified them
562
- if (repairAttempts > 0) {
563
- writeScenariosArtifact(config.generatedDir, config.constitutionHash, scenarioResult);
564
- }
565
- // Deduplicate accumulated probes by description
566
- const seenDescriptions = new Set(scenarioResult.scenarios.map((s) => s.description));
567
- const uniqueProbes = accumulatedProbes.filter((s) => {
568
- if (seenDescriptions.has(s.description))
569
- return false;
570
- seenDescriptions.add(s.description);
571
- return true;
572
- });
573
- const totalScenariosTested = filteredScenarios.length + uniqueProbes.length;
574
- console.error('');
575
- console.error(` Rules: ${compilationResult.rules.length}`);
576
- console.error(` Scenarios tested: ${totalScenariosTested}`);
577
- if (discardedScenarios.length > 0) {
578
- console.error(` Scenarios discarded (structural conflicts): ${discardedScenarios.length}`);
579
- }
580
- if (uniqueProbes.length > 0) {
581
- console.error(` Probe scenarios accumulated: ${uniqueProbes.length}`);
582
- }
583
- if (repairAttempts > 0) {
584
- console.error(` Repair attempts: ${repairAttempts}`);
585
- }
586
- if (scenarioCorrectionsApplied > 0) {
587
- console.error(` Scenario corrections: ${scenarioCorrectionsApplied}`);
588
- }
589
- console.error(` Artifacts written to: ${chalk.dim(config.generatedDir + '/')}`);
590
- console.error(` LLM interaction log: ${chalk.dim(logPath)}`);
591
- if (!verificationResult.pass) {
592
- console.error('');
593
- console.error(chalk.red.bold('Verification FAILED — artifacts written but policy may need review.'));
594
- process.exit(1);
595
- }
596
- console.error('');
597
- console.error(chalk.green.bold('Policy compilation successful!'));
598
59
  }
599
60
  // Only run when executed directly (not when imported by cli.ts or tests)
600
61
  if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {