@provos/ironcurtain 0.7.2 → 0.9.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 (259) hide show
  1. package/README.md +29 -100
  2. package/dist/auth/auth-command.d.ts +15 -0
  3. package/dist/auth/auth-command.js +424 -0
  4. package/dist/auth/auth-command.js.map +1 -0
  5. package/dist/auth/oauth-flow.d.ts +46 -0
  6. package/dist/auth/oauth-flow.js +279 -0
  7. package/dist/auth/oauth-flow.js.map +1 -0
  8. package/dist/auth/oauth-provider.d.ts +55 -0
  9. package/dist/auth/oauth-provider.js +73 -0
  10. package/dist/auth/oauth-provider.js.map +1 -0
  11. package/dist/auth/oauth-registry.d.ts +34 -0
  12. package/dist/auth/oauth-registry.js +69 -0
  13. package/dist/auth/oauth-registry.js.map +1 -0
  14. package/dist/auth/oauth-token-provider.d.ts +52 -0
  15. package/dist/auth/oauth-token-provider.js +159 -0
  16. package/dist/auth/oauth-token-provider.js.map +1 -0
  17. package/dist/auth/oauth-token-store.d.ts +30 -0
  18. package/dist/auth/oauth-token-store.js +93 -0
  19. package/dist/auth/oauth-token-store.js.map +1 -0
  20. package/dist/auth/providers/google-scopes.d.ts +35 -0
  21. package/dist/auth/providers/google-scopes.js +183 -0
  22. package/dist/auth/providers/google-scopes.js.map +1 -0
  23. package/dist/auth/providers/google.d.ts +2 -0
  24. package/dist/auth/providers/google.js +28 -0
  25. package/dist/auth/providers/google.js.map +1 -0
  26. package/dist/cli.js +6 -0
  27. package/dist/cli.js.map +1 -1
  28. package/dist/config/config-command.js +39 -1
  29. package/dist/config/config-command.js.map +1 -1
  30. package/dist/config/constitution-readonly.md +14 -11
  31. package/dist/config/constitution-user-base.md +15 -0
  32. package/dist/config/first-start.js +4 -1
  33. package/dist/config/first-start.js.map +1 -1
  34. package/dist/config/generated/test-scenarios.json +1 -1
  35. package/dist/config/generated/tool-annotations.json +10617 -296
  36. package/dist/config/generated-readonly/compiled-policy.json +787 -244
  37. package/dist/config/index.d.ts +2 -2
  38. package/dist/config/index.js +8 -0
  39. package/dist/config/index.js.map +1 -1
  40. package/dist/config/mcp-servers.json +31 -2
  41. package/dist/config/paths.d.ts +20 -0
  42. package/dist/config/paths.js +43 -0
  43. package/dist/config/paths.js.map +1 -1
  44. package/dist/config/types.d.ts +8 -0
  45. package/dist/config/user-config.d.ts +29 -0
  46. package/dist/config/user-config.js +30 -0
  47. package/dist/config/user-config.js.map +1 -1
  48. package/dist/cron/constitution-generator.d.ts +2 -1
  49. package/dist/cron/constitution-generator.js +35 -36
  50. package/dist/cron/constitution-generator.js.map +1 -1
  51. package/dist/cron/headless-transport.d.ts +4 -0
  52. package/dist/cron/headless-transport.js +1 -1
  53. package/dist/cron/headless-transport.js.map +1 -1
  54. package/dist/daemon/ironcurtain-daemon.js +7 -1
  55. package/dist/daemon/ironcurtain-daemon.js.map +1 -1
  56. package/dist/docker/adapters/claude-code.js +32 -5
  57. package/dist/docker/adapters/claude-code.js.map +1 -1
  58. package/dist/docker/agent-adapter.d.ts +40 -0
  59. package/dist/docker/claude-md-seed.d.ts +27 -0
  60. package/dist/docker/claude-md-seed.js +42 -0
  61. package/dist/docker/claude-md-seed.js.map +1 -0
  62. package/dist/docker/docker-agent-session.d.ts +3 -1
  63. package/dist/docker/docker-agent-session.js +51 -28
  64. package/dist/docker/docker-agent-session.js.map +1 -1
  65. package/dist/docker/docker-infrastructure.d.ts +15 -1
  66. package/dist/docker/docker-infrastructure.js +105 -9
  67. package/dist/docker/docker-infrastructure.js.map +1 -1
  68. package/dist/docker/docker-manager.d.ts +2 -0
  69. package/dist/docker/docker-manager.js +9 -1
  70. package/dist/docker/docker-manager.js.map +1 -1
  71. package/dist/docker/mitm-proxy.d.ts +65 -0
  72. package/dist/docker/mitm-proxy.js +288 -31
  73. package/dist/docker/mitm-proxy.js.map +1 -1
  74. package/dist/docker/package-types.d.ts +127 -0
  75. package/dist/docker/package-types.js +8 -0
  76. package/dist/docker/package-types.js.map +1 -0
  77. package/dist/docker/package-validator.d.ts +41 -0
  78. package/dist/docker/package-validator.js +90 -0
  79. package/dist/docker/package-validator.js.map +1 -0
  80. package/dist/docker/platform.d.ts +6 -6
  81. package/dist/docker/platform.js +8 -6
  82. package/dist/docker/platform.js.map +1 -1
  83. package/dist/docker/proxy-tools.d.ts +80 -0
  84. package/dist/docker/proxy-tools.js +248 -0
  85. package/dist/docker/proxy-tools.js.map +1 -0
  86. package/dist/docker/pty-session.d.ts +15 -0
  87. package/dist/docker/pty-session.js +253 -73
  88. package/dist/docker/pty-session.js.map +1 -1
  89. package/dist/docker/pty-types.d.ts +28 -0
  90. package/dist/docker/pty-types.js +2 -0
  91. package/dist/docker/pty-types.js.map +1 -1
  92. package/dist/docker/registry-proxy.d.ts +142 -0
  93. package/dist/docker/registry-proxy.js +856 -0
  94. package/dist/docker/registry-proxy.js.map +1 -0
  95. package/dist/escalation/escalation-watcher.d.ts +3 -1
  96. package/dist/escalation/escalation-watcher.js +6 -2
  97. package/dist/escalation/escalation-watcher.js.map +1 -1
  98. package/dist/escalation/listener-command.js +24 -10
  99. package/dist/escalation/listener-command.js.map +1 -1
  100. package/dist/escalation/listener-lock.d.ts +5 -0
  101. package/dist/escalation/listener-lock.js +14 -0
  102. package/dist/escalation/listener-lock.js.map +1 -1
  103. package/dist/index.js +16 -3
  104. package/dist/index.js.map +1 -1
  105. package/dist/memory/auto-save.d.ts +25 -0
  106. package/dist/memory/auto-save.js +89 -0
  107. package/dist/memory/auto-save.js.map +1 -0
  108. package/dist/memory/memory-annotations.d.ts +47 -0
  109. package/dist/memory/memory-annotations.js +87 -0
  110. package/dist/memory/memory-annotations.js.map +1 -0
  111. package/dist/memory/memory-prompt.d.ts +12 -0
  112. package/dist/memory/memory-prompt.js +20 -0
  113. package/dist/memory/memory-prompt.js.map +1 -0
  114. package/dist/memory/resolve-memory-path.d.ts +20 -0
  115. package/dist/memory/resolve-memory-path.js +30 -0
  116. package/dist/memory/resolve-memory-path.js.map +1 -0
  117. package/dist/mux/mux-app.d.ts +4 -0
  118. package/dist/mux/mux-app.js +246 -28
  119. package/dist/mux/mux-app.js.map +1 -1
  120. package/dist/mux/mux-command.d.ts +2 -2
  121. package/dist/mux/mux-command.js +14 -22
  122. package/dist/mux/mux-command.js.map +1 -1
  123. package/dist/mux/mux-escalation-manager.d.ts +9 -3
  124. package/dist/mux/mux-escalation-manager.js +69 -19
  125. package/dist/mux/mux-escalation-manager.js.map +1 -1
  126. package/dist/mux/mux-input-handler.d.ts +59 -3
  127. package/dist/mux/mux-input-handler.js +270 -32
  128. package/dist/mux/mux-input-handler.js.map +1 -1
  129. package/dist/mux/mux-renderer.d.ts +19 -1
  130. package/dist/mux/mux-renderer.js +466 -11
  131. package/dist/mux/mux-renderer.js.map +1 -1
  132. package/dist/mux/persona-scanner.d.ts +19 -0
  133. package/dist/mux/persona-scanner.js +53 -0
  134. package/dist/mux/persona-scanner.js.map +1 -0
  135. package/dist/mux/pty-bridge.d.ts +8 -0
  136. package/dist/mux/pty-bridge.js +19 -1
  137. package/dist/mux/pty-bridge.js.map +1 -1
  138. package/dist/mux/session-scanner.d.ts +18 -0
  139. package/dist/mux/session-scanner.js +65 -0
  140. package/dist/mux/session-scanner.js.map +1 -0
  141. package/dist/mux/types.d.ts +36 -1
  142. package/dist/mux/types.js +14 -1
  143. package/dist/mux/types.js.map +1 -1
  144. package/dist/persona/persona-command.js +130 -93
  145. package/dist/persona/persona-command.js.map +1 -1
  146. package/dist/persona/persona-prompt.d.ts +7 -7
  147. package/dist/persona/persona-prompt.js +14 -29
  148. package/dist/persona/persona-prompt.js.map +1 -1
  149. package/dist/persona/resolve.js +5 -2
  150. package/dist/persona/resolve.js.map +1 -1
  151. package/dist/pipeline/annotate.js +1 -7
  152. package/dist/pipeline/annotate.js.map +1 -1
  153. package/dist/pipeline/compile.d.ts +5 -2
  154. package/dist/pipeline/compile.js +23 -11
  155. package/dist/pipeline/compile.js.map +1 -1
  156. package/dist/pipeline/constitution-compiler.d.ts +302 -3
  157. package/dist/pipeline/constitution-compiler.js +285 -23
  158. package/dist/pipeline/constitution-compiler.js.map +1 -1
  159. package/dist/pipeline/generate-with-repair.d.ts +2 -2
  160. package/dist/pipeline/generate-with-repair.js +2 -2
  161. package/dist/pipeline/handwritten-scenarios.d.ts +20 -7
  162. package/dist/pipeline/handwritten-scenarios.js +20 -41
  163. package/dist/pipeline/handwritten-scenarios.js.map +1 -1
  164. package/dist/pipeline/list-resolver.d.ts +18 -20
  165. package/dist/pipeline/list-resolver.js +28 -36
  166. package/dist/pipeline/list-resolver.js.map +1 -1
  167. package/dist/pipeline/pipeline-runner.d.ts +102 -13
  168. package/dist/pipeline/pipeline-runner.js +497 -237
  169. package/dist/pipeline/pipeline-runner.js.map +1 -1
  170. package/dist/pipeline/pipeline-shared.d.ts +7 -9
  171. package/dist/pipeline/pipeline-shared.js +8 -18
  172. package/dist/pipeline/pipeline-shared.js.map +1 -1
  173. package/dist/pipeline/policy-verifier.d.ts +32 -5
  174. package/dist/pipeline/policy-verifier.js +137 -8
  175. package/dist/pipeline/policy-verifier.js.map +1 -1
  176. package/dist/pipeline/proxy-mcp-connections.d.ts +25 -0
  177. package/dist/pipeline/proxy-mcp-connections.js +121 -0
  178. package/dist/pipeline/proxy-mcp-connections.js.map +1 -0
  179. package/dist/pipeline/refresh-lists.js +21 -13
  180. package/dist/pipeline/refresh-lists.js.map +1 -1
  181. package/dist/pipeline/scenario-generator.d.ts +29 -49
  182. package/dist/pipeline/scenario-generator.js +180 -143
  183. package/dist/pipeline/scenario-generator.js.map +1 -1
  184. package/dist/pipeline/scenario-schema-validator.d.ts +51 -0
  185. package/dist/pipeline/scenario-schema-validator.js +132 -0
  186. package/dist/pipeline/scenario-schema-validator.js.map +1 -0
  187. package/dist/pipeline/tool-annotator.d.ts +9 -8
  188. package/dist/pipeline/tool-annotator.js +48 -95
  189. package/dist/pipeline/tool-annotator.js.map +1 -1
  190. package/dist/pipeline/types.d.ts +31 -24
  191. package/dist/sandbox/index.d.ts +26 -10
  192. package/dist/sandbox/index.js +151 -40
  193. package/dist/sandbox/index.js.map +1 -1
  194. package/dist/servers/fetch-server.js +10 -5
  195. package/dist/servers/fetch-server.js.map +1 -1
  196. package/dist/session/agent-session.d.ts +8 -20
  197. package/dist/session/agent-session.js +22 -70
  198. package/dist/session/agent-session.js.map +1 -1
  199. package/dist/session/base-transport.d.ts +17 -1
  200. package/dist/session/base-transport.js +25 -1
  201. package/dist/session/base-transport.js.map +1 -1
  202. package/dist/session/cli-transport.d.ts +5 -0
  203. package/dist/session/cli-transport.js +48 -25
  204. package/dist/session/cli-transport.js.map +1 -1
  205. package/dist/session/index.d.ts +25 -1
  206. package/dist/session/index.js +57 -8
  207. package/dist/session/index.js.map +1 -1
  208. package/dist/session/prompts.js +8 -9
  209. package/dist/session/prompts.js.map +1 -1
  210. package/dist/session/session-manager.d.ts +6 -0
  211. package/dist/session/session-manager.js +12 -0
  212. package/dist/session/session-manager.js.map +1 -1
  213. package/dist/session/types.d.ts +11 -1
  214. package/dist/session/types.js.map +1 -1
  215. package/dist/session/workspace-validation.js +37 -6
  216. package/dist/session/workspace-validation.js.map +1 -1
  217. package/dist/signal/signal-bot-daemon.js +24 -8
  218. package/dist/signal/signal-bot-daemon.js.map +1 -1
  219. package/dist/signal/signal-transport.d.ts +5 -2
  220. package/dist/signal/signal-transport.js +3 -3
  221. package/dist/signal/signal-transport.js.map +1 -1
  222. package/dist/trusted-process/approval-whitelist.d.ts +117 -0
  223. package/dist/trusted-process/approval-whitelist.js +237 -0
  224. package/dist/trusted-process/approval-whitelist.js.map +1 -0
  225. package/dist/trusted-process/gworkspace-credentials.d.ts +40 -0
  226. package/dist/trusted-process/gworkspace-credentials.js +45 -0
  227. package/dist/trusted-process/gworkspace-credentials.js.map +1 -0
  228. package/dist/trusted-process/index.d.ts +11 -1
  229. package/dist/trusted-process/index.js +83 -53
  230. package/dist/trusted-process/index.js.map +1 -1
  231. package/dist/trusted-process/mcp-proxy-server.d.ts +23 -0
  232. package/dist/trusted-process/mcp-proxy-server.js +409 -94
  233. package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
  234. package/dist/trusted-process/policy-engine.d.ts +12 -3
  235. package/dist/trusted-process/policy-engine.js +33 -23
  236. package/dist/trusted-process/policy-engine.js.map +1 -1
  237. package/dist/trusted-process/policy-types.d.ts +17 -0
  238. package/dist/trusted-process/sandbox-integration.d.ts +23 -0
  239. package/dist/trusted-process/sandbox-integration.js +115 -3
  240. package/dist/trusted-process/sandbox-integration.js.map +1 -1
  241. package/dist/trusted-process/token-file-refresher.d.ts +69 -0
  242. package/dist/trusted-process/token-file-refresher.js +120 -0
  243. package/dist/trusted-process/token-file-refresher.js.map +1 -0
  244. package/dist/types/argument-roles.d.ts +7 -1
  245. package/dist/types/argument-roles.js +50 -0
  246. package/dist/types/argument-roles.js.map +1 -1
  247. package/dist/types/audit.d.ts +11 -0
  248. package/docker/Dockerfile.base +22 -0
  249. package/docker/Dockerfile.base.arm64 +44 -4
  250. package/docker/Dockerfile.claude-code +2 -2
  251. package/docker/entrypoint-claude-code.sh +24 -6
  252. package/package.json +8 -2
  253. package/src/config/constitution-user-base.md +15 -0
  254. package/src/config/generated/test-scenarios.json +1 -1
  255. package/src/config/generated/tool-annotations.json +10617 -296
  256. package/src/config/mcp-servers.json +31 -2
  257. package/dist/pipeline/mcp-connections.d.ts +0 -16
  258. package/dist/pipeline/mcp-connections.js +0 -61
  259. package/dist/pipeline/mcp-connections.js.map +0 -1
@@ -2,13 +2,13 @@
2
2
  * PipelineRunner -- Encapsulates the full policy compilation pipeline.
3
3
  *
4
4
  * Provides a reusable abstraction over the compile-verify-repair loop.
5
- * Used by both:
6
- * - compile.ts CLI (constitutionKind: 'constitution')
7
- * - compileTaskPolicy() (constitutionKind: 'task-policy')
5
+ * Used by:
6
+ * - compile.ts CLI (constitutionKind: 'constitution') -- per-server compilation
7
+ * - compileTaskPolicy() (constitutionKind: 'task-policy') -- per-server whitelist
8
8
  *
9
- * The two constitutionKind variants differ only in the LLM prompt:
10
- * - 'constitution': broad-principle compilation from a constitution doc
11
- * - 'task-policy': whitelist-generation from an English task description
9
+ * Each MCP server is compiled independently with its own compile-verify-repair
10
+ * cycle, then results are merged. This enables incremental recompilation and
11
+ * better per-server rule quality.
12
12
  */
13
13
  import { resolve } from 'node:path';
14
14
  import chalk from 'chalk';
@@ -17,10 +17,12 @@ import { PolicyEngine } from '../trusted-process/policy-engine.js';
17
17
  import { buildCompilerSystemPrompt, ConstitutionCompilerSession, formatAnnotationsSummary, validateCompiledRules, } from './constitution-compiler.js';
18
18
  import { getHandwrittenScenarios } from './handwritten-scenarios.js';
19
19
  import { resolveAllLists } from './list-resolver.js';
20
- import { computeHash, loadExistingArtifact, loadToolAnnotationsFile, mergeReplacements, resolveRulePaths, writeArtifact, withSpinner, showCached, createPipelineLlm, } from './pipeline-shared.js';
21
- import { applyScenarioCorrections, buildJudgeSystemPrompt, extractScenarioCorrections, filterStructuralConflicts, PolicyVerifierSession, verifyPolicy, } from './policy-verifier.js';
22
- import { buildGeneratorSystemPrompt, ScenarioGeneratorSession } from './scenario-generator.js';
23
- import { connectMcpServersForLists, disconnectMcpServers } from './mcp-connections.js';
20
+ import { computeHash, loadExistingArtifact, loadStoredToolAnnotationsFile, resolveRulePaths, writeArtifact, withSpinner, showCached, createPipelineLlm, } from './pipeline-shared.js';
21
+ import { resolveStoredAnnotationsFile } from '../types/argument-roles.js';
22
+ import { applyScenarioCorrections, buildJudgeSystemPrompt, detectAllDefaultRoleFallbacks, extractScenarioCorrections, filterStructuralConflicts, PolicyVerifierSession, verifyPolicy, } from './policy-verifier.js';
23
+ import { filterInvalidSchemaScenarios } from './scenario-schema-validator.js';
24
+ import { buildGeneratorSystemPrompt, generateScenarios, repairScenarios } from './scenario-generator.js';
25
+ import { connectViaProxy } from './proxy-mcp-connections.js';
24
26
  /** Creates PipelineModels from user config. Delegates to shared createPipelineLlm. */
25
27
  export async function createPipelineModels(logDir) {
26
28
  const effectiveLogDir = logDir ?? resolve(process.cwd(), 'generated');
@@ -35,9 +37,6 @@ export async function createPipelineModels(logDir) {
35
37
  // ---------------------------------------------------------------------------
36
38
  // Internal helpers
37
39
  // ---------------------------------------------------------------------------
38
- function computePolicyHash(systemPrompt, annotations) {
39
- return computeHash(systemPrompt, JSON.stringify(annotations));
40
- }
41
40
  function computeScenariosHash(systemPrompt, handwrittenScenarios) {
42
41
  return computeHash(systemPrompt, JSON.stringify(handwrittenScenarios));
43
42
  }
@@ -53,14 +52,48 @@ function extractPermittedDirectories(rules) {
53
52
  function collectProbeScenarios(result) {
54
53
  return result.rounds.flatMap((round) => round.newScenarios);
55
54
  }
56
- function filterAndLogStructuralConflicts(engine, scenarios, label = 'Discarded scenario (structural conflict)') {
57
- const { valid, discarded } = filterStructuralConflicts(engine, scenarios);
55
+ function filterAndLogStructuralConflicts(engine, scenarios, label = 'Discarded scenario (structural conflict)', storedAnnotations) {
56
+ const filterResult = filterStructuralConflicts(engine, scenarios);
57
+ let valid = filterResult.valid;
58
+ const discarded = filterResult.discarded;
58
59
  for (const d of discarded) {
59
60
  const prefix = d.scenario.source === 'handwritten'
60
61
  ? chalk.yellow('Warning: handwritten scenario conflicts with structural invariant:')
61
62
  : chalk.dim(`${label}:`);
62
63
  console.error(` ${prefix} "${d.scenario.description}" — ${d.rule} always returns ${d.actual}`);
63
64
  }
65
+ // Discard scenarios whose arguments don't match any conditional role spec —
66
+ // they fall back to all default roles and test the wrong thing.
67
+ if (storedAnnotations.length > 0) {
68
+ const fallbackWarnings = detectAllDefaultRoleFallbacks(valid, storedAnnotations);
69
+ const fallbackDescriptions = new Set(fallbackWarnings.map((w) => w.scenario.description));
70
+ if (fallbackDescriptions.size > 0) {
71
+ const kept = [];
72
+ for (const s of valid) {
73
+ if (fallbackDescriptions.has(s.description)) {
74
+ const w = fallbackWarnings.find((fw) => fw.scenario.description === s.description);
75
+ const details = w?.details.join('; ') ?? 'unknown conditional mismatch';
76
+ const discardLabel = 'Discarded (default role fallback)';
77
+ console.error(` ${chalk.dim(`${discardLabel}:`)} "${s.description}" — ${details}`);
78
+ discarded.push({ scenario: s, rule: 'default-role-fallback', actual: 'deny' });
79
+ }
80
+ else {
81
+ kept.push(s);
82
+ }
83
+ }
84
+ valid = kept;
85
+ }
86
+ // Discard scenarios whose arguments violate the tool's input schema
87
+ // (unknown arg names, missing required fields, invalid enum values).
88
+ const schemaResult = filterInvalidSchemaScenarios(valid, storedAnnotations);
89
+ if (schemaResult.discarded.length > 0) {
90
+ for (const d of schemaResult.discarded) {
91
+ console.error(` ${chalk.dim('Discarded (schema mismatch):')} "${d.scenario.description}" — ${d.rule}`);
92
+ }
93
+ discarded.push(...schemaResult.discarded);
94
+ valid = schemaResult.valid;
95
+ }
96
+ }
64
97
  return { valid, discarded };
65
98
  }
66
99
  class RuleValidationError extends Error {
@@ -98,10 +131,14 @@ function buildPolicyArtifact(constitutionHash, rules, listDefinitions, inputHash
98
131
  * Builds the task-policy system prompt for per-job compilation.
99
132
  * Instructs the LLM to generate a strict whitelist from the task.
100
133
  */
101
- function buildTaskCompilerSystemPrompt(taskDescription, annotations, protectedPaths, allowedDirectory) {
134
+ function buildTaskCompilerSystemPrompt(taskDescription, annotations, protectedPaths, allowedDirectory, serverScope) {
102
135
  const annotationsSummary = formatAnnotationsSummary(annotations);
103
136
  return `You are compiling a task-scoped security policy for an automated scheduled job. The job runs unattended on a schedule. Your goal is to generate the MINIMUM set of policy rules required for this specific task -- nothing more.
104
137
 
138
+ ## Server Scope
139
+
140
+ You are generating rules ONLY for the "${serverScope}" server. Every rule you emit MUST include "server": ["${serverScope}"] in its "if" condition.
141
+
105
142
  ## Task Description
106
143
 
107
144
  ${taskDescription}
@@ -150,9 +187,8 @@ Produce an ORDERED list of policy rules (first match wins). Each rule has:
150
187
  - "principle": which task requirement this implements
151
188
  - "if": conditions that must ALL be true for the rule to fire:
152
189
  - "roles": array of argument roles to match. Omit = any tool.
153
- - "server": array of server names (omit = any server)
190
+ - "server": MUST be ["${serverScope}"]
154
191
  - "tool": array of specific tool names (omit = any matching tool)
155
- - "sideEffects": match on the tool's sideEffects annotation (omit = don't filter)
156
192
  - "paths": path condition with "roles" and "within" (concrete absolute directory)
157
193
  - "domains": domain condition with "roles" and "allowed" (list of allowed domain patterns). For git-remote-url roles, use hostname/owner/repo patterns for specific repos (e.g., "github.com/provos/ironcurtain") or hostname-only for any repo on that host (e.g., "github.com"). Git tools with both path and URL roles need "domains" conditions for git-remote-url (path roles get sandbox-resolved separately).
158
194
  - "lists": array of list conditions with "roles", "allowed", and "matchType"
@@ -164,7 +200,84 @@ CRITICAL RULES:
164
200
  2. Use CONCRETE ABSOLUTE paths when needed.
165
201
  3. Order matters: more specific rules before more general ones.
166
202
  4. Only output "allow" and "escalate" rules.
167
- 5. Be concise in descriptions and reasons -- one sentence each.`;
203
+ 5. Be concise in descriptions and reasons -- one sentence each.
204
+ 6. EVERY rule MUST have "server": ["${serverScope}"] in its condition.`;
205
+ }
206
+ // ---------------------------------------------------------------------------
207
+ // Per-server merge helpers (exported for testing)
208
+ // ---------------------------------------------------------------------------
209
+ /**
210
+ * Computes a content hash for a single server's policy compilation inputs.
211
+ * The hash includes the server name, constitution, annotations, and prompt
212
+ * template so that any change invalidates the cache for this server.
213
+ */
214
+ export function computeServerPolicyHash(serverName, constitutionText, annotations, compilerPromptTemplate) {
215
+ return computeHash(serverName, constitutionText, JSON.stringify(annotations), compilerPromptTemplate);
216
+ }
217
+ /**
218
+ * Filters handwritten scenarios to only those belonging to a specific server.
219
+ * Safe because handwritten scenarios are tagged with serverName in their request.
220
+ */
221
+ export function getHandwrittenScenariosForServer(serverName, sandboxDir) {
222
+ return getHandwrittenScenarios(sandboxDir).filter((s) => s.request.serverName === serverName);
223
+ }
224
+ /**
225
+ * Debug assertion: validates that all rules from a server's compilation
226
+ * are properly scoped to exactly that server. This should never fail if
227
+ * the Zod schema with requireServer is correct.
228
+ */
229
+ export function validateServerScoping(serverName, rules) {
230
+ for (const rule of rules) {
231
+ if (!rule.if.server || !rule.if.server.includes(serverName)) {
232
+ throw new Error(`Rule "${rule.name}" from server "${serverName}" is missing ` +
233
+ `server: ["${serverName}"] in its condition. This is a compiler bug.`);
234
+ }
235
+ if (rule.if.server.length !== 1 || rule.if.server[0] !== serverName) {
236
+ throw new Error(`Rule "${rule.name}" from server "${serverName}" has unexpected ` +
237
+ `server scope: ${JSON.stringify(rule.if.server)}. Expected exactly ["${serverName}"].`);
238
+ }
239
+ }
240
+ }
241
+ /**
242
+ * Deduplicates list definitions by name. When two servers produce different
243
+ * generationPrompt values for the same named list, uses first-wins
244
+ * (alphabetical server order) and logs a warning.
245
+ */
246
+ export function deduplicateListDefinitions(defs) {
247
+ const seen = new Map();
248
+ for (const def of defs) {
249
+ const existing = seen.get(def.name);
250
+ if (!existing) {
251
+ seen.set(def.name, def);
252
+ }
253
+ else if (existing.generationPrompt !== def.generationPrompt) {
254
+ console.error(` Warning: list "${def.name}" has divergent generation prompts ` +
255
+ `across servers. Using first definition (alphabetical server order).`);
256
+ }
257
+ }
258
+ return [...seen.values()];
259
+ }
260
+ /**
261
+ * Merges per-server compilation results into a single CompiledPolicyFile.
262
+ * Rules are concatenated in alphabetical server order for determinism.
263
+ * List definitions are deduplicated by name.
264
+ */
265
+ export function mergeServerResults(results, constitutionHash) {
266
+ const sortedResults = [...results].sort((a, b) => a.serverName.localeCompare(b.serverName));
267
+ const allRules = sortedResults.flatMap((r) => r.rules);
268
+ const allListDefs = sortedResults.flatMap((r) => r.listDefinitions);
269
+ const uniqueListDefs = deduplicateListDefinitions(allListDefs);
270
+ const mergedInputHash = computeHash(...sortedResults.map((r) => r.inputHash));
271
+ const artifact = {
272
+ generatedAt: new Date().toISOString(),
273
+ constitutionHash,
274
+ inputHash: mergedInputHash,
275
+ rules: allRules,
276
+ };
277
+ if (uniqueListDefs.length > 0) {
278
+ artifact.listDefinitions = uniqueListDefs;
279
+ }
280
+ return artifact;
168
281
  }
169
282
  // ---------------------------------------------------------------------------
170
283
  // PipelineRunner
@@ -172,6 +285,10 @@ CRITICAL RULES:
172
285
  /**
173
286
  * Encapsulates the full policy compilation pipeline:
174
287
  * compile rules -> generate scenarios -> verify -> repair loop.
288
+ *
289
+ * Uses per-server compilation with independent compile-verify-repair
290
+ * cycles per server, then merges results. Works for both 'constitution'
291
+ * (broad principles) and 'task-policy' (strict whitelist) modes.
175
292
  */
176
293
  export class PipelineRunner {
177
294
  model;
@@ -184,109 +301,272 @@ export class PipelineRunner {
184
301
  }
185
302
  /**
186
303
  * Runs the full pipeline. Returns the compiled policy on success.
304
+ * All compilation modes use per-server compilation with independent
305
+ * compile-verify-repair cycles per server, then merge results.
187
306
  */
188
307
  async run(config) {
189
- const toolAnnotationsFile = config.preloadedToolAnnotations ??
190
- loadToolAnnotationsFile(config.toolAnnotationsDir, config.toolAnnotationsFallbackDir);
191
- if (!toolAnnotationsFile) {
308
+ return this.runPerServer(config);
309
+ }
310
+ // -----------------------------------------------------------------------
311
+ // Per-server compilation path
312
+ // -----------------------------------------------------------------------
313
+ /**
314
+ * Orchestrates per-server compilation + merge.
315
+ *
316
+ * Phase 1: Compile each server independently (compile-verify-repair per server)
317
+ * Phase 2: Merge all per-server results into final compiled-policy.json
318
+ * Phase 3: Resolve dynamic lists (global, post-merge)
319
+ *
320
+ * Works for both 'constitution' and 'task-policy' modes -- the
321
+ * constitutionKind is forwarded to each server's compilation unit
322
+ * to select the appropriate compiler prompt.
323
+ */
324
+ async runPerServer(config) {
325
+ const storedAnnotationsFile = config.preloadedStoredAnnotations ??
326
+ loadStoredToolAnnotationsFile(config.toolAnnotationsDir, config.toolAnnotationsFallbackDir);
327
+ if (!storedAnnotationsFile) {
192
328
  throw new Error("tool-annotations.json not found. Run 'ironcurtain annotate-tools' first.");
193
329
  }
194
- const allAnnotations = Object.values(toolAnnotationsFile.servers).flatMap((server) => server.tools);
195
- const serverDomainAllowlists = config.mcpServers ? extractServerDomainAllowlists(config.mcpServers) : undefined;
196
- const includeHandwritten = config.includeHandwrittenScenarios ?? config.constitutionKind === 'constitution';
330
+ // Resolve conditional role specs to flat annotations for compiler prompts
331
+ const toolAnnotationsFile = resolveStoredAnnotationsFile(storedAnnotationsFile);
197
332
  const constitutionHash = computeHash(config.constitutionInput);
198
- // Build the system prompt based on constitutionKind
199
- const handwrittenScenarios = includeHandwritten ? getHandwrittenScenarios(config.allowedDirectory) : [];
200
- const compilerPrompt = config.constitutionKind === 'task-policy'
201
- ? buildTaskCompilerSystemPrompt(config.constitutionInput, allAnnotations, config.protectedPaths, config.allowedDirectory)
202
- : buildCompilerSystemPrompt(config.constitutionInput, allAnnotations, { protectedPaths: config.protectedPaths }, handwrittenScenarios.length > 0 ? handwrittenScenarios : undefined);
203
- const compilerHash = computePolicyHash(compilerPrompt, allAnnotations);
204
- const compilerSystem = this.cacheStrategy.wrapSystemPrompt(compilerPrompt);
205
- // Load existing artifacts for cache comparison
206
- const existingPolicy = loadExistingArtifact(config.outputDir, 'compiled-policy.json');
207
- const existingScenarios = loadExistingArtifact(config.outputDir, 'test-scenarios.json');
208
- // Step 1: Compile constitution/task into policy rules
209
- this.logContext.stepName = 'compile-constitution';
210
- let { rules, listDefinitions, inputHash, session: compilerSession, } = await this.compilePolicyRules(allAnnotations, compilerHash, existingPolicy, compilerSystem);
211
- let compiledPolicyFile = buildPolicyArtifact(constitutionHash, rules, listDefinitions, inputHash);
212
- writeArtifact(config.outputDir, 'compiled-policy.json', compiledPolicyFile);
213
- // Resolve dynamic lists if the compiler emitted list definitions
214
- const hasLists = listDefinitions.length > 0;
215
- const totalSteps = hasLists ? 4 : 3;
216
- let dynamicLists;
217
- if (hasLists) {
218
- this.logContext.stepName = 'resolve-lists';
219
- const existingLists = loadExistingArtifact(config.outputDir, 'dynamic-lists.json');
220
- const needsMcp = listDefinitions.some((d) => d.requiresMcp);
221
- let mcpConnections;
222
- if (needsMcp && config.mcpServers) {
223
- mcpConnections = await connectMcpServersForLists(listDefinitions, config.mcpServers);
333
+ // Phase 1: Per-server compilation
334
+ const serverResults = await this.compileAllServers(config, toolAnnotationsFile, constitutionHash, storedAnnotationsFile);
335
+ // Phase 2: Merge
336
+ const mergedPolicy = mergeServerResults(serverResults, constitutionHash);
337
+ writeArtifact(config.outputDir, 'compiled-policy.json', mergedPolicy);
338
+ // Merge scenarios for the global artifact
339
+ const allScenarios = serverResults.flatMap((r) => r.scenarios);
340
+ const mergedScenariosFile = {
341
+ generatedAt: new Date().toISOString(),
342
+ constitutionHash,
343
+ inputHash: mergedPolicy.inputHash,
344
+ scenarios: allScenarios,
345
+ };
346
+ writeArtifact(config.outputDir, 'test-scenarios.json', mergedScenariosFile);
347
+ // Phase 3: Merge per-server resolved lists into global dynamic-lists.json
348
+ // Each server resolved its own lists during compileServer(); merge them here.
349
+ const listDefinitions = mergedPolicy.listDefinitions ?? [];
350
+ if (listDefinitions.length > 0) {
351
+ const keptListNames = new Set(listDefinitions.map((d) => d.name));
352
+ const mergedListEntries = {};
353
+ // serverResults are already in alphabetical order -- use first-wins
354
+ // to match deduplicateListDefinitions() merge semantics.
355
+ for (const result of serverResults) {
356
+ if (result.resolvedLists) {
357
+ for (const [name, list] of Object.entries(result.resolvedLists.lists)) {
358
+ if (keptListNames.has(name) && !(name in mergedListEntries)) {
359
+ mergedListEntries[name] = list;
360
+ }
361
+ }
362
+ }
224
363
  }
225
- const listStepText = `[2/${totalSteps}] Resolving dynamic lists`;
364
+ const mergedDynamicLists = {
365
+ generatedAt: new Date().toISOString(),
366
+ lists: mergedListEntries,
367
+ };
368
+ writeArtifact(config.outputDir, 'dynamic-lists.json', mergedDynamicLists);
369
+ }
370
+ // Cross-server verification is intentionally omitted. Per-server rules compose
371
+ // correctly by construction: every rule has server: [serverName] (enforced by
372
+ // Zod schema), the engine checks cond.server.includes(request.serverName) so
373
+ // rules from server A never match server B's calls, rules are only allow/escalate
374
+ // with default-deny fallback, and structural invariants are evaluated before
375
+ // compiled rules. If each server's rules verify in isolation, the merged set
376
+ // behaves identically.
377
+ // Summary
378
+ const totalRules = mergedPolicy.rules.length;
379
+ const totalScenarios = allScenarios.length;
380
+ const serverCount = serverResults.length;
381
+ console.error('');
382
+ console.error(` Servers compiled: ${serverCount}`);
383
+ console.error(` Rules: ${totalRules}`);
384
+ console.error(` Scenarios tested: ${totalScenarios}`);
385
+ console.error(` Artifacts written to: ${chalk.dim(config.outputDir + '/')}`);
386
+ if (config.llmLogPath) {
387
+ console.error(` LLM interaction log: ${chalk.dim(config.llmLogPath)}`);
388
+ }
389
+ console.error('');
390
+ console.error(chalk.green.bold('Policy compilation successful!'));
391
+ return mergedPolicy;
392
+ }
393
+ /**
394
+ * Compiles rules for all servers sequentially.
395
+ * Respects serverFilter if provided (for debugging single servers).
396
+ */
397
+ async compileAllServers(config, toolAnnotationsFile, constitutionHash, storedAnnotationsFile) {
398
+ const results = [];
399
+ const serverEntries = Object.entries(toolAnnotationsFile.servers);
400
+ // Apply server filter if provided
401
+ const { serverFilter } = config;
402
+ const filteredEntries = serverFilter
403
+ ? serverEntries.filter(([name]) => serverFilter.includes(name))
404
+ : serverEntries;
405
+ if (config.serverFilter && filteredEntries.length === 0) {
406
+ throw new Error(`No matching servers found for filter: ${config.serverFilter.join(', ')}. ` +
407
+ `Available: ${serverEntries.map(([n]) => n).join(', ')}`);
408
+ }
409
+ // Load handwritten scenarios once, then filter per server (avoids N redundant loads)
410
+ const includeHandwritten = config.includeHandwrittenScenarios ?? config.constitutionKind === 'constitution';
411
+ const allHandwrittenScenarios = includeHandwritten ? getHandwrittenScenarios(config.allowedDirectory) : [];
412
+ const totalServers = filteredEntries.length;
413
+ const failedServers = [];
414
+ for (let i = 0; i < filteredEntries.length; i++) {
415
+ const [serverName, serverData] = filteredEntries[i];
416
+ console.error('');
417
+ console.error(chalk.bold(`[${i + 1}/${totalServers}] Compiling server: ${serverName}`));
226
418
  try {
227
- const { result: resolvedLists } = await withSpinner(listStepText, async (spinner) => resolveAllLists(listDefinitions, { model: this.model, mcpConnections }, existingLists, (msg) => {
228
- spinner.text = `${listStepText} — ${msg}`;
229
- }), (result, elapsed) => {
230
- const count = Object.keys(result.lists).length;
231
- return `${listStepText}: ${count} list(s) resolved (${elapsed.toFixed(1)}s)`;
232
- });
233
- dynamicLists = resolvedLists;
234
- writeArtifact(config.outputDir, 'dynamic-lists.json', dynamicLists);
419
+ const result = await this.compileServer({
420
+ serverName,
421
+ annotations: serverData.tools,
422
+ storedAnnotations: storedAnnotationsFile.servers[serverName].tools,
423
+ constitutionText: config.constitutionInput,
424
+ constitutionKind: config.constitutionKind,
425
+ allowedDirectory: config.allowedDirectory,
426
+ protectedPaths: config.protectedPaths,
427
+ mcpServerConfig: config.mcpServers?.[serverName],
428
+ handwrittenScenarios: allHandwrittenScenarios.filter((s) => s.request.serverName === serverName),
429
+ }, config, constitutionHash);
430
+ results.push(result);
235
431
  }
236
- finally {
237
- if (mcpConnections) {
238
- await disconnectMcpServers(mcpConnections);
239
- }
432
+ catch (err) {
433
+ const msg = err instanceof Error ? err.message : String(err);
434
+ console.error(` ${chalk.red(`Server "${serverName}" failed:`)} ${msg}`);
435
+ failedServers.push(serverName);
240
436
  }
241
437
  }
242
- // Extract permitted directories from compiled rules
243
- const permittedDirectories = extractPermittedDirectories(rules);
244
- // Step 2: Generate test scenarios
245
- const scenarioStepLabel = `[${hasLists ? 3 : 2}/${totalSteps}]`;
246
- this.logContext.stepName = 'generate-scenarios';
247
- const scenarioPrompt = buildGeneratorSystemPrompt(config.constitutionInput, allAnnotations, config.allowedDirectory, permittedDirectories, dynamicLists);
248
- const scenarioHash = computeScenariosHash(scenarioPrompt, handwrittenScenarios);
249
- const scenarioSystem = this.cacheStrategy.wrapSystemPrompt(scenarioPrompt);
250
- const scenarioResult = await this.generateTestScenarios(allAnnotations, config.allowedDirectory, handwrittenScenarios, scenarioHash, existingScenarios, scenarioStepLabel, scenarioSystem);
251
- writeArtifact(config.outputDir, 'test-scenarios.json', {
438
+ if (failedServers.length > 0) {
439
+ console.error(`\n${chalk.yellow(`Warning: ${failedServers.length} server(s) failed verification: ${failedServers.join(', ')}`)}`);
440
+ console.error(chalk.yellow('Continuing with successfully compiled servers.'));
441
+ }
442
+ if (results.length === 0) {
443
+ throw new Error(`All ${failedServers.length} server(s) failed compilation: ${failedServers.join(', ')}. ` +
444
+ 'No policy rules were produced.');
445
+ }
446
+ return results;
447
+ }
448
+ /**
449
+ * Compiles a single server: compile -> generate scenarios -> verify -> repair.
450
+ * Self-contained compile-verify-repair loop per server.
451
+ */
452
+ async compileServer(unit, config, constitutionHash) {
453
+ const serverOutputDir = resolve(config.outputDir, 'servers', unit.serverName);
454
+ // Build per-server system prompt (only this server's annotations)
455
+ const compilerPrompt = unit.constitutionKind === 'task-policy'
456
+ ? buildTaskCompilerSystemPrompt(unit.constitutionText, unit.annotations, unit.protectedPaths, unit.allowedDirectory, unit.serverName)
457
+ : buildCompilerSystemPrompt(unit.constitutionText, unit.annotations, { protectedPaths: unit.protectedPaths, allowedDirectory: unit.allowedDirectory }, unit.handwrittenScenarios.length > 0 ? unit.handwrittenScenarios : undefined, { serverScope: unit.serverName });
458
+ // Check per-server cache
459
+ const inputHash = computeServerPolicyHash(unit.serverName, unit.constitutionText, unit.annotations, compilerPrompt);
460
+ const existingServerPolicy = loadExistingArtifact(serverOutputDir, 'compiled-policy.json');
461
+ const existingServerScenarios = loadExistingArtifact(serverOutputDir, 'test-scenarios.json');
462
+ if (existingServerPolicy && existingServerPolicy.inputHash === inputHash && existingServerScenarios) {
463
+ // Verify scenario hash to detect stale scenarios (e.g., changed templates or handwritten scenarios)
464
+ const cachedPermittedDirs = extractPermittedDirectories(resolveRulePaths(existingServerPolicy.rules));
465
+ const cachedDynamicLists = loadExistingArtifact(serverOutputDir, 'dynamic-lists.json');
466
+ const cachedScenarioPrompt = buildGeneratorSystemPrompt(unit.constitutionText, unit.annotations, unit.allowedDirectory, cachedPermittedDirs, cachedDynamicLists, unit.storedAnnotations);
467
+ const cachedScenarioHash = computeScenariosHash(cachedScenarioPrompt, unit.handwrittenScenarios);
468
+ if (existingServerScenarios.inputHash === cachedScenarioHash) {
469
+ showCached(` ${unit.serverName}: compilation`);
470
+ return {
471
+ serverName: unit.serverName,
472
+ rules: resolveRulePaths(existingServerPolicy.rules),
473
+ listDefinitions: existingServerPolicy.listDefinitions ?? [],
474
+ scenarios: existingServerScenarios.scenarios,
475
+ inputHash,
476
+ constitutionHash,
477
+ resolvedLists: cachedDynamicLists,
478
+ };
479
+ }
480
+ }
481
+ const compilerSystem = this.cacheStrategy.wrapSystemPrompt(compilerPrompt);
482
+ // Step 1: Compile rules for this server
483
+ this.logContext.stepName = `compile-${unit.serverName}`;
484
+ const compileResult = await this.compileServerPolicyRules(unit, compilerSystem, inputHash);
485
+ let { rules } = compileResult;
486
+ let { listDefinitions } = compileResult;
487
+ let compilerSession = compileResult.session;
488
+ // Validate server scoping (debug assertion)
489
+ validateServerScoping(unit.serverName, rules);
490
+ // Resolve dynamic lists for this server (before scenario generation so lists are available)
491
+ let dynamicLists;
492
+ if (listDefinitions.length > 0) {
493
+ dynamicLists = await this.resolveServerLists(listDefinitions, serverOutputDir, config, ` ${unit.serverName}`);
494
+ }
495
+ // Build per-server policy artifact for engine construction
496
+ let serverPolicyFile = buildPolicyArtifact(constitutionHash, rules, listDefinitions, inputHash);
497
+ // Persist early so intermediate results survive scenario/verification failures
498
+ const writeServerPolicy = () => writeArtifact(serverOutputDir, 'compiled-policy.json', {
252
499
  generatedAt: new Date().toISOString(),
500
+ serverName: unit.serverName,
253
501
  constitutionHash,
254
- inputHash: scenarioResult.inputHash,
255
- scenarios: scenarioResult.scenarios,
502
+ inputHash,
503
+ rules,
504
+ listDefinitions: listDefinitions.length > 0 ? listDefinitions : undefined,
256
505
  });
257
- // Filter scenarios against structural invariants
258
- const filterEngine = new PolicyEngine(compiledPolicyFile, toolAnnotationsFile, config.protectedPaths, config.allowedDirectory, undefined, dynamicLists);
259
- const { valid: initialValid, discarded: discardedScenarios } = filterAndLogStructuralConflicts(filterEngine, scenarioResult.scenarios);
260
- let filteredScenarios = initialValid;
261
- // Step 3: Verify compiled policy against scenarios
262
- const verifyStepLabel = `[${totalSteps}/${totalSteps}]`;
263
- this.logContext.stepName = 'verify-policy';
264
- const allAvailableTools = allAnnotations.map((a) => ({ serverName: a.serverName, toolName: a.toolName }));
265
- const serverNamesList = [...new Set(allAnnotations.map((a) => a.serverName))];
266
- const toolNamesList = [...new Set(allAnnotations.map((a) => a.toolName))];
267
- let verifierSystem = this.cacheStrategy.wrapSystemPrompt(buildJudgeSystemPrompt(config.constitutionInput, compiledPolicyFile, config.protectedPaths, allAvailableTools, dynamicLists, config.allowedDirectory));
506
+ writeServerPolicy();
507
+ // Build per-server tool annotations file with conditional role specs preserved
508
+ const serverAnnotationsFile = {
509
+ generatedAt: new Date().toISOString(),
510
+ servers: { [unit.serverName]: { inputHash, tools: unit.storedAnnotations } },
511
+ };
512
+ // Extract domain allowlists for this server
513
+ const serverDomainAllowlists = unit.mcpServerConfig
514
+ ? extractServerDomainAllowlists({ [unit.serverName]: unit.mcpServerConfig })
515
+ : undefined;
516
+ // Step 2: Generate test scenarios for this server
517
+ this.logContext.stepName = `scenarios-${unit.serverName}`;
518
+ const permittedDirectories = extractPermittedDirectories(rules);
519
+ const scenarioPrompt = buildGeneratorSystemPrompt(unit.constitutionText, unit.annotations, unit.allowedDirectory, permittedDirectories, dynamicLists, unit.storedAnnotations);
520
+ const scenarioHash = computeScenariosHash(scenarioPrompt, unit.handwrittenScenarios);
521
+ const scenarioResult = await this.generateTestScenarios(unit.constitutionText, unit.annotations, unit.allowedDirectory, unit.handwrittenScenarios, scenarioHash, existingServerScenarios, ` ${unit.serverName}`, permittedDirectories, dynamicLists, unit.storedAnnotations);
522
+ // Step 3: Verify compiled rules against scenarios
523
+ this.logContext.stepName = `verify-${unit.serverName}`;
524
+ const filterEngine = new PolicyEngine(serverPolicyFile, serverAnnotationsFile, unit.protectedPaths, unit.allowedDirectory, undefined, dynamicLists);
525
+ const { valid: filteredScenarios, discarded: discardedScenarios } = filterAndLogStructuralConflicts(filterEngine, scenarioResult.scenarios, undefined, unit.storedAnnotations);
526
+ // Generate replacement scenarios for structurally discarded ones (pre-loop, one-time)
527
+ if (discardedScenarios.length > 0) {
528
+ const discardedForRepair = discardedScenarios.map((d) => ({
529
+ scenario: d.scenario,
530
+ feedback: `${d.rule} always returns ${d.actual}`,
531
+ }));
532
+ this.logContext.stepName = `repair-scenarios-${unit.serverName}`;
533
+ const replacementScenarios = await repairScenarios(discardedForRepair, unit.constitutionText, unit.annotations, unit.allowedDirectory, this.model, permittedDirectories, dynamicLists, (msg) => console.error(` ${chalk.dim(msg)}`), unit.storedAnnotations);
534
+ if (replacementScenarios.length > 0) {
535
+ // Filter replacements through structural invariants too
536
+ const { valid: validReplacements } = filterAndLogStructuralConflicts(filterEngine, replacementScenarios, 'Discarded replacement (structural conflict)', unit.storedAnnotations);
537
+ scenarioResult.scenarios.push(...validReplacements);
538
+ filteredScenarios.push(...validReplacements);
539
+ console.error(` ${chalk.dim(`Repaired ${discardedScenarios.length} discarded scenario(s) → ${validReplacements.length} replacement(s)`)}`);
540
+ }
541
+ }
542
+ const serverToolNames = [...new Set(unit.annotations.map((a) => a.toolName))];
543
+ const serverNames = [unit.serverName];
544
+ const serverTools = unit.annotations.map((a) => ({ serverName: a.serverName, toolName: a.toolName }));
545
+ let verifierSystem = this.cacheStrategy.wrapSystemPrompt(buildJudgeSystemPrompt(unit.constitutionText, serverPolicyFile, unit.protectedPaths, serverTools, dynamicLists, unit.allowedDirectory, unit.storedAnnotations));
268
546
  let verifierSession = new PolicyVerifierSession({
269
547
  system: verifierSystem,
270
548
  model: this.model,
271
- serverNames: serverNamesList,
272
- toolNames: toolNamesList,
549
+ serverNames,
550
+ toolNames: serverToolNames,
551
+ storedAnnotations: unit.storedAnnotations,
273
552
  });
274
- const { result: verificationResultInitial } = await withSpinner(`${verifyStepLabel} Verifying policy`, async (spinner) => verifyPolicy(config.constitutionInput, compiledPolicyFile, toolAnnotationsFile, config.protectedPaths, filteredScenarios, this.model, 3, config.allowedDirectory, (msg) => {
275
- spinner.text = `${verifyStepLabel} Verifying policy ${msg}`;
276
- }, serverDomainAllowlists, dynamicLists, verifierSystem, verifierSession), (r, elapsed) => r.pass
277
- ? `${verifyStepLabel} Verified policy: ${r.rounds.length} round(s) (${elapsed.toFixed(1)}s)`
278
- : `${verifyStepLabel} Verification completed with failures (${elapsed.toFixed(1)}s)`);
553
+ const verifyLabel = ` ${unit.serverName}: Verifying`;
554
+ const { result: verificationResultInitial } = await withSpinner(verifyLabel, async (spinner) => verifyPolicy(unit.constitutionText, serverPolicyFile, serverAnnotationsFile, unit.protectedPaths, filteredScenarios, this.model, 3, unit.allowedDirectory, (msg) => {
555
+ spinner.text = `${verifyLabel} ${msg}`;
556
+ }, serverDomainAllowlists, dynamicLists, verifierSystem, verifierSession, unit.storedAnnotations), (r, elapsed) => r.pass
557
+ ? `${verifyLabel}: ${r.rounds.length} round(s) (${elapsed.toFixed(1)}s)`
558
+ : `${verifyLabel}: completed with failures (${elapsed.toFixed(1)}s)`);
279
559
  let verificationResult = verificationResultInitial;
280
560
  if (!verificationResult.pass) {
281
561
  this.logVerboseFailures(verificationResult);
282
562
  }
283
- // Collect probe scenarios from verifier
284
- const { valid: filteredInitialProbes } = filterAndLogStructuralConflicts(filterEngine, collectProbeScenarios(verificationResult), 'Discarded probe (structural conflict)');
563
+ // Collect probe scenarios
564
+ const { valid: filteredInitialProbes } = filterAndLogStructuralConflicts(filterEngine, collectProbeScenarios(verificationResult), 'Discarded probe (structural conflict)', unit.storedAnnotations);
285
565
  const accumulatedProbes = filteredInitialProbes;
286
566
  // Compile-verify-repair loop (up to 2 repair attempts)
287
567
  const MAX_REPAIRS = 2;
288
568
  let repairAttempts = 0;
289
- let scenarioCorrectionsApplied = 0;
569
+ let currentFilteredScenarios;
290
570
  if (!verificationResult.pass) {
291
571
  const baseInputHash = inputHash;
292
572
  for (let attempt = 1; attempt <= MAX_REPAIRS; attempt++) {
@@ -303,26 +583,9 @@ export class PipelineRunner {
303
583
  scenarioResult.scenarios = applyScenarioCorrections(scenarioResult.scenarios, corrections);
304
584
  const correctedProbes = applyScenarioCorrections(accumulatedProbes, corrections);
305
585
  accumulatedProbes.splice(0, accumulatedProbes.length, ...correctedProbes);
306
- scenarioCorrectionsApplied += corrections.length;
307
586
  console.error(` ${chalk.dim(`Corrected ${corrections.length} scenario expectation(s)`)}`);
308
587
  }
309
- // Feed corrections back to the scenario generator session
310
- const { session: scenarioSession } = scenarioResult;
311
- if (scenarioSession &&
312
- (corrections.length > 0 || discardedScenarios.length > 0 || accumulatedProbes.length > 0)) {
313
- const feedback = {
314
- corrections,
315
- discardedScenarios,
316
- probeScenarios: accumulatedProbes,
317
- };
318
- this.logContext.stepName = `repair-regenerate-${attempt}`;
319
- const regenText = `Repair ${attempt}/${MAX_REPAIRS}: Regenerating scenarios`;
320
- const { result: replacements } = await withSpinner(regenText, async (spinner) => scenarioSession.regenerate(feedback, (msg) => {
321
- spinner.text = `${regenText} — ${msg}`;
322
- }), (r, elapsed) => `${regenText}: ${r.length} replacement(s) (${elapsed.toFixed(1)}s)`);
323
- scenarioResult.scenarios = mergeReplacements(scenarioResult.scenarios, replacements, corrections, discardedScenarios);
324
- }
325
- ({ valid: filteredScenarios } = filterAndLogStructuralConflicts(filterEngine, scenarioResult.scenarios));
588
+ ({ valid: currentFilteredScenarios } = filterAndLogStructuralConflicts(filterEngine, scenarioResult.scenarios, undefined, unit.storedAnnotations));
326
589
  const allRuleBlamedFailures = verificationResult.failedScenarios.filter((f) => {
327
590
  const attr = attributedFailures.find((a) => a.scenarioDescription === f.scenario.description);
328
591
  if (!attr || attr.blame.kind === 'rule' || attr.blame.kind === 'both')
@@ -335,149 +598,92 @@ export class PipelineRunner {
335
598
  judgeAnalysis,
336
599
  attemptNumber: attempt,
337
600
  existingListDefinitions: listDefinitions.length > 0 ? listDefinitions : undefined,
338
- handwrittenScenarios: includeHandwritten ? handwrittenScenarios : undefined,
601
+ handwrittenScenarios: unit.handwrittenScenarios.length > 0 ? unit.handwrittenScenarios : undefined,
339
602
  };
340
- this.logContext.stepName = `repair-compile-${attempt}`;
341
- const repairCompileText = `Repair ${attempt}/${MAX_REPAIRS}: Recompiling`;
342
- const { result: repairResult } = await withSpinner(repairCompileText, async (spinner) => this.compilePolicyRulesWithRepair(allAnnotations, config.protectedPaths, baseInputHash, repairContext, compilerSystem, compilerSession, (msg) => {
343
- spinner.text = `${repairCompileText} — ${msg}`;
344
- }), (r, elapsed) => `Repair ${attempt}/${MAX_REPAIRS}: Recompiled ${r.rules.length} rules (${elapsed.toFixed(1)}s)`);
603
+ this.logContext.stepName = `repair-compile-${unit.serverName}-${attempt}`;
604
+ const repairText = ` ${unit.serverName} repair ${attempt}/${MAX_REPAIRS}: Recompiling`;
605
+ const { result: repairResult } = await withSpinner(repairText, async (spinner) => this.compilePolicyRulesWithPointFix(rules, unit.annotations, unit.protectedPaths, baseInputHash, repairContext, compilerSystem, compilerSession, listDefinitions, (msg) => {
606
+ spinner.text = `${repairText} — ${msg}`;
607
+ }), (r, elapsed) => `${repairText}: ${r.rules.length} rules (${elapsed.toFixed(1)}s)`);
345
608
  rules = repairResult.rules;
346
609
  listDefinitions = repairResult.listDefinitions;
347
- inputHash = repairResult.inputHash;
348
610
  compilerSession = repairResult.session;
349
- // Re-resolve dynamic lists if repair introduced new ones
350
- if (dynamicLists && listDefinitions.length > 0) {
351
- const currentLists = dynamicLists;
352
- const newListDefs = listDefinitions.filter((def) => !(def.name in currentLists.lists));
353
- if (newListDefs.length > 0) {
354
- const mcpRequired = newListDefs.filter((d) => d.requiresMcp);
355
- if (mcpRequired.length > 0) {
356
- console.error(` ${chalk.yellow('Warning:')} Repair introduced ${mcpRequired.length} new MCP-requiring list(s) — skipping resolution`);
357
- }
358
- const knowledgeDefs = newListDefs.filter((d) => !d.requiresMcp);
359
- if (knowledgeDefs.length > 0) {
360
- console.error(` ${chalk.dim(`Resolving ${knowledgeDefs.length} new list(s) from repair...`)}`);
361
- const resolved = await resolveAllLists(knowledgeDefs, { model: this.model }, currentLists);
362
- dynamicLists = {
363
- ...resolved,
364
- lists: { ...currentLists.lists, ...resolved.lists },
365
- };
366
- writeArtifact(config.outputDir, 'dynamic-lists.json', dynamicLists);
367
- }
368
- }
611
+ // Re-validate server scoping after repair
612
+ validateServerScoping(unit.serverName, rules);
613
+ // Re-resolve dynamic lists if repair changed list definitions
614
+ if (listDefinitions.length > 0) {
615
+ dynamicLists = await this.resolveServerLists(listDefinitions, serverOutputDir, config, ` ${unit.serverName}`);
369
616
  }
370
- compiledPolicyFile = buildPolicyArtifact(constitutionHash, rules, listDefinitions, inputHash);
371
- writeArtifact(config.outputDir, 'compiled-policy.json', compiledPolicyFile);
372
- verifierSystem = this.cacheStrategy.wrapSystemPrompt(buildJudgeSystemPrompt(config.constitutionInput, compiledPolicyFile, config.protectedPaths, allAvailableTools, dynamicLists, config.allowedDirectory));
617
+ serverPolicyFile = buildPolicyArtifact(constitutionHash, rules, listDefinitions, inputHash);
618
+ // Persist repaired rules immediately
619
+ writeServerPolicy();
620
+ verifierSystem = this.cacheStrategy.wrapSystemPrompt(buildJudgeSystemPrompt(unit.constitutionText, serverPolicyFile, unit.protectedPaths, serverTools, dynamicLists, unit.allowedDirectory, unit.storedAnnotations));
373
621
  verifierSession = new PolicyVerifierSession({
374
622
  system: verifierSystem,
375
623
  model: this.model,
376
- serverNames: serverNamesList,
377
- toolNames: toolNamesList,
624
+ serverNames,
625
+ toolNames: serverToolNames,
626
+ storedAnnotations: unit.storedAnnotations,
378
627
  });
379
628
  }
380
629
  else {
381
630
  console.error(` ${chalk.dim('No rule-blamed failures — skipping recompilation')}`);
382
631
  }
383
- this.logContext.stepName = `repair-verify-${attempt}`;
384
- const repairScenarios = [...filteredScenarios, ...accumulatedProbes];
385
- const repairVerifyText = `Repair ${attempt}/${MAX_REPAIRS}: Verifying`;
386
- const { result: repairVerifyResult } = await withSpinner(repairVerifyText, async (spinner) => verifyPolicy(config.constitutionInput, compiledPolicyFile, toolAnnotationsFile, config.protectedPaths, repairScenarios, this.model, 1, config.allowedDirectory, (msg) => {
632
+ this.logContext.stepName = `repair-verify-${unit.serverName}-${attempt}`;
633
+ const scenariosForRepairVerify = [...currentFilteredScenarios, ...accumulatedProbes];
634
+ const repairVerifyText = ` ${unit.serverName} repair ${attempt}/${MAX_REPAIRS}: Verifying`;
635
+ const { result: repairVerifyResult } = await withSpinner(repairVerifyText, async (spinner) => verifyPolicy(unit.constitutionText, serverPolicyFile, serverAnnotationsFile, unit.protectedPaths, scenariosForRepairVerify, this.model, 1, unit.allowedDirectory, (msg) => {
387
636
  spinner.text = `${repairVerifyText} — ${msg}`;
388
- }, serverDomainAllowlists, dynamicLists, verifierSystem, verifierSession), (r, elapsed) => r.pass
389
- ? `Repair ${attempt}/${MAX_REPAIRS}: Verified (${elapsed.toFixed(1)}s)`
390
- : `Repair ${attempt}/${MAX_REPAIRS}: ${r.failedScenarios.length} failure(s) (${elapsed.toFixed(1)}s)`);
637
+ }, serverDomainAllowlists, dynamicLists, verifierSystem, verifierSession, unit.storedAnnotations), (r, elapsed) => r.pass
638
+ ? `${repairVerifyText}: passed (${elapsed.toFixed(1)}s)`
639
+ : `${repairVerifyText}: ${r.failedScenarios.length} failure(s) (${elapsed.toFixed(1)}s)`);
391
640
  verificationResult = repairVerifyResult;
392
641
  if (!verificationResult.pass) {
393
642
  this.logVerboseFailures(verificationResult);
394
643
  }
395
- const { valid: validRepairProbes } = filterAndLogStructuralConflicts(filterEngine, collectProbeScenarios(verificationResult), 'Discarded probe (structural conflict)');
644
+ const { valid: validRepairProbes } = filterAndLogStructuralConflicts(filterEngine, collectProbeScenarios(verificationResult), 'Discarded probe (structural conflict)', unit.storedAnnotations);
396
645
  accumulatedProbes.push(...validRepairProbes);
397
646
  repairAttempts = attempt;
398
647
  if (verificationResult.pass) {
399
- // Final full verification
400
- this.logContext.stepName = 'final-verify';
401
- const finalScenarios = [...filteredScenarios, ...accumulatedProbes];
402
- const finalSession = new PolicyVerifierSession({
403
- system: verifierSystem,
404
- model: this.model,
405
- serverNames: serverNamesList,
406
- toolNames: toolNamesList,
407
- });
408
- const { result: finalVerifyResult } = await withSpinner('Final full verification', async (spinner) => verifyPolicy(config.constitutionInput, compiledPolicyFile, toolAnnotationsFile, config.protectedPaths, finalScenarios, this.model, 3, config.allowedDirectory, (msg) => {
409
- spinner.text = `Final full verification — ${msg}`;
410
- }, serverDomainAllowlists, dynamicLists, verifierSystem, finalSession), (r, elapsed) => r.pass
411
- ? `Final full verification: passed (${elapsed.toFixed(1)}s)`
412
- : `Final full verification: ${r.failedScenarios.length} failure(s) (${elapsed.toFixed(1)}s)`);
413
- verificationResult = finalVerifyResult;
414
- const { valid: validFinalProbes } = filterAndLogStructuralConflicts(filterEngine, collectProbeScenarios(verificationResult), 'Discarded probe (structural conflict)');
415
- accumulatedProbes.push(...validFinalProbes);
416
648
  break;
417
649
  }
418
650
  }
419
651
  }
420
- // Re-write scenarios if the repair loop modified them
421
- if (repairAttempts > 0) {
422
- writeArtifact(config.outputDir, 'test-scenarios.json', {
423
- generatedAt: new Date().toISOString(),
424
- constitutionHash,
425
- inputHash: scenarioResult.inputHash,
426
- scenarios: scenarioResult.scenarios,
427
- });
428
- }
429
- // Summary
430
- const seenDescriptions = new Set(scenarioResult.scenarios.map((s) => s.description));
431
- const uniqueProbes = accumulatedProbes.filter((s) => {
432
- if (seenDescriptions.has(s.description))
433
- return false;
434
- seenDescriptions.add(s.description);
435
- return true;
652
+ // Write per-server artifacts (before verification check so they can be inspected on failure)
653
+ const finalScenarios = [...scenarioResult.scenarios, ...accumulatedProbes];
654
+ writeServerPolicy();
655
+ writeArtifact(serverOutputDir, 'test-scenarios.json', {
656
+ generatedAt: new Date().toISOString(),
657
+ constitutionHash,
658
+ inputHash: scenarioResult.inputHash,
659
+ scenarios: finalScenarios,
436
660
  });
437
- const totalScenariosTested = filteredScenarios.length + uniqueProbes.length;
438
- console.error('');
439
- console.error(` Rules: ${rules.length}`);
440
- console.error(` Scenarios tested: ${totalScenariosTested}`);
441
- if (discardedScenarios.length > 0) {
442
- console.error(` Scenarios discarded (structural conflicts): ${discardedScenarios.length}`);
443
- }
444
- if (uniqueProbes.length > 0) {
445
- console.error(` Probe scenarios accumulated: ${uniqueProbes.length}`);
446
- }
447
- if (repairAttempts > 0) {
448
- console.error(` Repair attempts: ${repairAttempts}`);
449
- }
450
- if (scenarioCorrectionsApplied > 0) {
451
- console.error(` Scenario corrections: ${scenarioCorrectionsApplied}`);
452
- }
453
- console.error(` Artifacts written to: ${chalk.dim(config.outputDir + '/')}`);
454
- if (config.llmLogPath) {
455
- console.error(` LLM interaction log: ${chalk.dim(config.llmLogPath)}`);
456
- }
457
661
  if (!verificationResult.pass) {
458
- throw new Error('Verification FAILED — artifacts written but policy may need review.');
662
+ throw new Error(`Verification FAILED for server "${unit.serverName}" — artifacts written for inspection but policy may need review.`);
459
663
  }
460
- console.error('');
461
- console.error(chalk.green.bold('Policy compilation successful!'));
462
- return compiledPolicyFile;
664
+ console.error(` ${chalk.green(unit.serverName)}: ${rules.length} rules, ${finalScenarios.length} scenarios` +
665
+ (repairAttempts > 0 ? `, ${repairAttempts} repair(s)` : ''));
666
+ return {
667
+ serverName: unit.serverName,
668
+ rules,
669
+ listDefinitions,
670
+ scenarios: finalScenarios,
671
+ inputHash,
672
+ constitutionHash,
673
+ resolvedLists: dynamicLists,
674
+ };
463
675
  }
464
- // -----------------------------------------------------------------------
465
- // Private compilation methods
466
- // -----------------------------------------------------------------------
467
- async compilePolicyRules(annotations, inputHash, existingPolicy, system) {
468
- const stepText = '[1/3] Compiling constitution';
469
- if (existingPolicy && existingPolicy.inputHash === inputHash) {
470
- showCached(stepText);
471
- return {
472
- rules: resolveRulePaths(existingPolicy.rules),
473
- listDefinitions: existingPolicy.listDefinitions ?? [],
474
- inputHash,
475
- };
476
- }
676
+ /**
677
+ * Compiles policy rules for a single server (initial compilation).
678
+ * Uses per-server schema with requireServer: true.
679
+ */
680
+ async compileServerPolicyRules(unit, system, inputHash) {
681
+ const stepText = ` ${unit.serverName}: Compiling rules`;
477
682
  const session = new ConstitutionCompilerSession({
478
683
  system,
479
684
  model: this.model,
480
- annotations,
685
+ annotations: unit.annotations,
686
+ schemaOptions: { requireServer: true },
481
687
  });
482
688
  const { result: compilationOutput } = await withSpinner(stepText, async (spinner) => {
483
689
  const output = await session.compile((msg) => {
@@ -486,7 +692,7 @@ export class PipelineRunner {
486
692
  const compiledRules = resolveRulePaths(output.rules);
487
693
  validateRulesOrThrow(compiledRules, output.listDefinitions);
488
694
  return { rules: compiledRules, listDefinitions: output.listDefinitions };
489
- }, (output, elapsed) => `${stepText}: ${output.rules.length} rules compiled (${elapsed.toFixed(1)}s)`);
695
+ }, (output, elapsed) => `${stepText}: ${output.rules.length} rules (${elapsed.toFixed(1)}s)`);
490
696
  return {
491
697
  rules: compilationOutput.rules,
492
698
  listDefinitions: compilationOutput.listDefinitions,
@@ -494,6 +700,9 @@ export class PipelineRunner {
494
700
  session,
495
701
  };
496
702
  }
703
+ // -----------------------------------------------------------------------
704
+ // Private compilation methods
705
+ // -----------------------------------------------------------------------
497
706
  async compilePolicyRulesWithRepair(annotations, protectedPaths, baseInputHash, repairContext, system, session, onProgress) {
498
707
  let output;
499
708
  if (session) {
@@ -513,25 +722,76 @@ export class PipelineRunner {
513
722
  session,
514
723
  };
515
724
  }
516
- async generateTestScenarios(annotations, allowedDirectory, handwrittenScenarios, inputHash, existingScenarios, stepLabel, system) {
725
+ /**
726
+ * Attempts point-fix repair via the session, falling back to full recompile.
727
+ * When a compiler session exists, uses repairPointFix() to emit a minimal
728
+ * patch instead of regenerating the entire rule set. This avoids oscillation
729
+ * where fixing one failure breaks previously-passing rules.
730
+ */
731
+ async compilePolicyRulesWithPointFix(existingRules, annotations, protectedPaths, baseInputHash, repairContext, system, session, existingListDefinitions, onProgress) {
732
+ let output;
733
+ if (session) {
734
+ output = await session.repairPointFix(existingRules, repairContext, existingListDefinitions, onProgress);
735
+ }
736
+ else {
737
+ // No session available -- fall back to full recompile
738
+ return this.compilePolicyRulesWithRepair(annotations, protectedPaths, baseInputHash, repairContext, system, session, onProgress);
739
+ }
740
+ const compiledRules = resolveRulePaths(output.rules);
741
+ validateRulesOrThrow(compiledRules, output.listDefinitions);
742
+ return {
743
+ rules: compiledRules,
744
+ listDefinitions: output.listDefinitions,
745
+ inputHash: `${baseInputHash}-repair`,
746
+ session,
747
+ };
748
+ }
749
+ /**
750
+ * Resolves dynamic lists for a single server's list definitions.
751
+ * Loads existing per-server dynamic-lists.json for cache comparison,
752
+ * connects MCP servers if needed, and writes results to serverOutputDir.
753
+ */
754
+ async resolveServerLists(listDefinitions, serverOutputDir, config, labelPrefix) {
755
+ this.logContext.stepName = `resolve-lists-${labelPrefix.trim()}`;
756
+ const existingLists = loadExistingArtifact(serverOutputDir, 'dynamic-lists.json');
757
+ const needsMcp = listDefinitions.some((d) => d.requiresMcp);
758
+ let proxy;
759
+ if (needsMcp && config.mcpServers) {
760
+ proxy = await connectViaProxy(listDefinitions, config.mcpServers, config.toolAnnotationsDir);
761
+ }
762
+ const listStepText = `${labelPrefix}: Resolving dynamic lists`;
763
+ try {
764
+ const { result: resolvedLists } = await withSpinner(listStepText, async (spinner) => resolveAllLists(listDefinitions, {
765
+ model: this.model,
766
+ proxyConnection: proxy ? { client: proxy.client, tools: proxy.tools } : undefined,
767
+ }, existingLists, (msg) => {
768
+ spinner.text = `${listStepText} — ${msg}`;
769
+ }), (result, elapsed) => {
770
+ const count = Object.keys(result.lists).length;
771
+ return `${listStepText}: ${count} list(s) resolved (${elapsed.toFixed(1)}s)`;
772
+ });
773
+ writeArtifact(serverOutputDir, 'dynamic-lists.json', resolvedLists);
774
+ return resolvedLists;
775
+ }
776
+ finally {
777
+ if (proxy) {
778
+ await proxy.shutdown();
779
+ }
780
+ }
781
+ }
782
+ async generateTestScenarios(constitutionText, annotations, allowedDirectory, handwrittenScenarios, inputHash, existingScenarios, stepLabel, permittedDirectories, dynamicLists, storedAnnotations) {
517
783
  const stepText = `${stepLabel} Generating test scenarios`;
518
784
  if (existingScenarios && existingScenarios.inputHash === inputHash) {
519
785
  showCached(stepText);
520
786
  return { scenarios: existingScenarios.scenarios, inputHash };
521
787
  }
522
- const session = new ScenarioGeneratorSession({
523
- system,
524
- model: this.model,
525
- annotations,
526
- handwrittenScenarios,
527
- });
528
- const { result: scenarios } = await withSpinner(stepText, async (spinner) => session.generate((msg) => {
788
+ const { result: scenarios } = await withSpinner(stepText, async (spinner) => generateScenarios(constitutionText, annotations, handwrittenScenarios, allowedDirectory, this.model, permittedDirectories, (msg) => {
529
789
  spinner.text = `${stepText} — ${msg}`;
530
- }), (scenarios, elapsed) => {
790
+ }, dynamicLists, (prompt) => this.cacheStrategy.wrapSystemPrompt(prompt), storedAnnotations), (scenarios, elapsed) => {
531
791
  const generatedCount = scenarios.length - handwrittenScenarios.length;
532
792
  return `${stepText}: ${scenarios.length} scenarios (${handwrittenScenarios.length} handwritten + ${generatedCount} generated) (${elapsed.toFixed(1)}s)`;
533
793
  });
534
- return { scenarios, inputHash, session };
794
+ return { scenarios, inputHash };
535
795
  }
536
796
  logVerboseFailures(result) {
537
797
  console.error('');