@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.
- package/README.md +29 -100
- package/dist/auth/auth-command.d.ts +15 -0
- package/dist/auth/auth-command.js +424 -0
- package/dist/auth/auth-command.js.map +1 -0
- package/dist/auth/oauth-flow.d.ts +46 -0
- package/dist/auth/oauth-flow.js +279 -0
- package/dist/auth/oauth-flow.js.map +1 -0
- package/dist/auth/oauth-provider.d.ts +55 -0
- package/dist/auth/oauth-provider.js +73 -0
- package/dist/auth/oauth-provider.js.map +1 -0
- package/dist/auth/oauth-registry.d.ts +34 -0
- package/dist/auth/oauth-registry.js +69 -0
- package/dist/auth/oauth-registry.js.map +1 -0
- package/dist/auth/oauth-token-provider.d.ts +52 -0
- package/dist/auth/oauth-token-provider.js +159 -0
- package/dist/auth/oauth-token-provider.js.map +1 -0
- package/dist/auth/oauth-token-store.d.ts +30 -0
- package/dist/auth/oauth-token-store.js +93 -0
- package/dist/auth/oauth-token-store.js.map +1 -0
- package/dist/auth/providers/google-scopes.d.ts +35 -0
- package/dist/auth/providers/google-scopes.js +183 -0
- package/dist/auth/providers/google-scopes.js.map +1 -0
- package/dist/auth/providers/google.d.ts +2 -0
- package/dist/auth/providers/google.js +28 -0
- package/dist/auth/providers/google.js.map +1 -0
- package/dist/cli.js +6 -0
- package/dist/cli.js.map +1 -1
- package/dist/config/config-command.js +39 -1
- package/dist/config/config-command.js.map +1 -1
- package/dist/config/constitution-readonly.md +14 -11
- package/dist/config/constitution-user-base.md +15 -0
- package/dist/config/first-start.js +4 -1
- package/dist/config/first-start.js.map +1 -1
- package/dist/config/generated/test-scenarios.json +1 -1
- package/dist/config/generated/tool-annotations.json +10617 -296
- package/dist/config/generated-readonly/compiled-policy.json +787 -244
- package/dist/config/index.d.ts +2 -2
- package/dist/config/index.js +8 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/mcp-servers.json +31 -2
- package/dist/config/paths.d.ts +20 -0
- package/dist/config/paths.js +43 -0
- package/dist/config/paths.js.map +1 -1
- package/dist/config/types.d.ts +8 -0
- package/dist/config/user-config.d.ts +29 -0
- package/dist/config/user-config.js +30 -0
- package/dist/config/user-config.js.map +1 -1
- package/dist/cron/constitution-generator.d.ts +2 -1
- package/dist/cron/constitution-generator.js +35 -36
- package/dist/cron/constitution-generator.js.map +1 -1
- package/dist/cron/headless-transport.d.ts +4 -0
- package/dist/cron/headless-transport.js +1 -1
- package/dist/cron/headless-transport.js.map +1 -1
- package/dist/daemon/ironcurtain-daemon.js +7 -1
- package/dist/daemon/ironcurtain-daemon.js.map +1 -1
- package/dist/docker/adapters/claude-code.js +32 -5
- package/dist/docker/adapters/claude-code.js.map +1 -1
- package/dist/docker/agent-adapter.d.ts +40 -0
- package/dist/docker/claude-md-seed.d.ts +27 -0
- package/dist/docker/claude-md-seed.js +42 -0
- package/dist/docker/claude-md-seed.js.map +1 -0
- package/dist/docker/docker-agent-session.d.ts +3 -1
- package/dist/docker/docker-agent-session.js +51 -28
- package/dist/docker/docker-agent-session.js.map +1 -1
- package/dist/docker/docker-infrastructure.d.ts +15 -1
- package/dist/docker/docker-infrastructure.js +105 -9
- package/dist/docker/docker-infrastructure.js.map +1 -1
- package/dist/docker/docker-manager.d.ts +2 -0
- package/dist/docker/docker-manager.js +9 -1
- package/dist/docker/docker-manager.js.map +1 -1
- package/dist/docker/mitm-proxy.d.ts +65 -0
- package/dist/docker/mitm-proxy.js +288 -31
- package/dist/docker/mitm-proxy.js.map +1 -1
- package/dist/docker/package-types.d.ts +127 -0
- package/dist/docker/package-types.js +8 -0
- package/dist/docker/package-types.js.map +1 -0
- package/dist/docker/package-validator.d.ts +41 -0
- package/dist/docker/package-validator.js +90 -0
- package/dist/docker/package-validator.js.map +1 -0
- package/dist/docker/platform.d.ts +6 -6
- package/dist/docker/platform.js +8 -6
- package/dist/docker/platform.js.map +1 -1
- package/dist/docker/proxy-tools.d.ts +80 -0
- package/dist/docker/proxy-tools.js +248 -0
- package/dist/docker/proxy-tools.js.map +1 -0
- package/dist/docker/pty-session.d.ts +15 -0
- package/dist/docker/pty-session.js +253 -73
- package/dist/docker/pty-session.js.map +1 -1
- package/dist/docker/pty-types.d.ts +28 -0
- package/dist/docker/pty-types.js +2 -0
- package/dist/docker/pty-types.js.map +1 -1
- package/dist/docker/registry-proxy.d.ts +142 -0
- package/dist/docker/registry-proxy.js +856 -0
- package/dist/docker/registry-proxy.js.map +1 -0
- package/dist/escalation/escalation-watcher.d.ts +3 -1
- package/dist/escalation/escalation-watcher.js +6 -2
- package/dist/escalation/escalation-watcher.js.map +1 -1
- package/dist/escalation/listener-command.js +24 -10
- package/dist/escalation/listener-command.js.map +1 -1
- package/dist/escalation/listener-lock.d.ts +5 -0
- package/dist/escalation/listener-lock.js +14 -0
- package/dist/escalation/listener-lock.js.map +1 -1
- package/dist/index.js +16 -3
- package/dist/index.js.map +1 -1
- package/dist/memory/auto-save.d.ts +25 -0
- package/dist/memory/auto-save.js +89 -0
- package/dist/memory/auto-save.js.map +1 -0
- package/dist/memory/memory-annotations.d.ts +47 -0
- package/dist/memory/memory-annotations.js +87 -0
- package/dist/memory/memory-annotations.js.map +1 -0
- package/dist/memory/memory-prompt.d.ts +12 -0
- package/dist/memory/memory-prompt.js +20 -0
- package/dist/memory/memory-prompt.js.map +1 -0
- package/dist/memory/resolve-memory-path.d.ts +20 -0
- package/dist/memory/resolve-memory-path.js +30 -0
- package/dist/memory/resolve-memory-path.js.map +1 -0
- package/dist/mux/mux-app.d.ts +4 -0
- package/dist/mux/mux-app.js +246 -28
- package/dist/mux/mux-app.js.map +1 -1
- package/dist/mux/mux-command.d.ts +2 -2
- package/dist/mux/mux-command.js +14 -22
- package/dist/mux/mux-command.js.map +1 -1
- package/dist/mux/mux-escalation-manager.d.ts +9 -3
- package/dist/mux/mux-escalation-manager.js +69 -19
- package/dist/mux/mux-escalation-manager.js.map +1 -1
- package/dist/mux/mux-input-handler.d.ts +59 -3
- package/dist/mux/mux-input-handler.js +270 -32
- package/dist/mux/mux-input-handler.js.map +1 -1
- package/dist/mux/mux-renderer.d.ts +19 -1
- package/dist/mux/mux-renderer.js +466 -11
- package/dist/mux/mux-renderer.js.map +1 -1
- package/dist/mux/persona-scanner.d.ts +19 -0
- package/dist/mux/persona-scanner.js +53 -0
- package/dist/mux/persona-scanner.js.map +1 -0
- package/dist/mux/pty-bridge.d.ts +8 -0
- package/dist/mux/pty-bridge.js +19 -1
- package/dist/mux/pty-bridge.js.map +1 -1
- package/dist/mux/session-scanner.d.ts +18 -0
- package/dist/mux/session-scanner.js +65 -0
- package/dist/mux/session-scanner.js.map +1 -0
- package/dist/mux/types.d.ts +36 -1
- package/dist/mux/types.js +14 -1
- package/dist/mux/types.js.map +1 -1
- package/dist/persona/persona-command.js +130 -93
- package/dist/persona/persona-command.js.map +1 -1
- package/dist/persona/persona-prompt.d.ts +7 -7
- package/dist/persona/persona-prompt.js +14 -29
- package/dist/persona/persona-prompt.js.map +1 -1
- package/dist/persona/resolve.js +5 -2
- package/dist/persona/resolve.js.map +1 -1
- package/dist/pipeline/annotate.js +1 -7
- package/dist/pipeline/annotate.js.map +1 -1
- package/dist/pipeline/compile.d.ts +5 -2
- package/dist/pipeline/compile.js +23 -11
- package/dist/pipeline/compile.js.map +1 -1
- package/dist/pipeline/constitution-compiler.d.ts +302 -3
- package/dist/pipeline/constitution-compiler.js +285 -23
- package/dist/pipeline/constitution-compiler.js.map +1 -1
- package/dist/pipeline/generate-with-repair.d.ts +2 -2
- package/dist/pipeline/generate-with-repair.js +2 -2
- package/dist/pipeline/handwritten-scenarios.d.ts +20 -7
- package/dist/pipeline/handwritten-scenarios.js +20 -41
- package/dist/pipeline/handwritten-scenarios.js.map +1 -1
- package/dist/pipeline/list-resolver.d.ts +18 -20
- package/dist/pipeline/list-resolver.js +28 -36
- package/dist/pipeline/list-resolver.js.map +1 -1
- package/dist/pipeline/pipeline-runner.d.ts +102 -13
- package/dist/pipeline/pipeline-runner.js +497 -237
- package/dist/pipeline/pipeline-runner.js.map +1 -1
- package/dist/pipeline/pipeline-shared.d.ts +7 -9
- package/dist/pipeline/pipeline-shared.js +8 -18
- package/dist/pipeline/pipeline-shared.js.map +1 -1
- package/dist/pipeline/policy-verifier.d.ts +32 -5
- package/dist/pipeline/policy-verifier.js +137 -8
- package/dist/pipeline/policy-verifier.js.map +1 -1
- package/dist/pipeline/proxy-mcp-connections.d.ts +25 -0
- package/dist/pipeline/proxy-mcp-connections.js +121 -0
- package/dist/pipeline/proxy-mcp-connections.js.map +1 -0
- package/dist/pipeline/refresh-lists.js +21 -13
- package/dist/pipeline/refresh-lists.js.map +1 -1
- package/dist/pipeline/scenario-generator.d.ts +29 -49
- package/dist/pipeline/scenario-generator.js +180 -143
- package/dist/pipeline/scenario-generator.js.map +1 -1
- package/dist/pipeline/scenario-schema-validator.d.ts +51 -0
- package/dist/pipeline/scenario-schema-validator.js +132 -0
- package/dist/pipeline/scenario-schema-validator.js.map +1 -0
- package/dist/pipeline/tool-annotator.d.ts +9 -8
- package/dist/pipeline/tool-annotator.js +48 -95
- package/dist/pipeline/tool-annotator.js.map +1 -1
- package/dist/pipeline/types.d.ts +31 -24
- package/dist/sandbox/index.d.ts +26 -10
- package/dist/sandbox/index.js +151 -40
- package/dist/sandbox/index.js.map +1 -1
- package/dist/servers/fetch-server.js +10 -5
- package/dist/servers/fetch-server.js.map +1 -1
- package/dist/session/agent-session.d.ts +8 -20
- package/dist/session/agent-session.js +22 -70
- package/dist/session/agent-session.js.map +1 -1
- package/dist/session/base-transport.d.ts +17 -1
- package/dist/session/base-transport.js +25 -1
- package/dist/session/base-transport.js.map +1 -1
- package/dist/session/cli-transport.d.ts +5 -0
- package/dist/session/cli-transport.js +48 -25
- package/dist/session/cli-transport.js.map +1 -1
- package/dist/session/index.d.ts +25 -1
- package/dist/session/index.js +57 -8
- package/dist/session/index.js.map +1 -1
- package/dist/session/prompts.js +8 -9
- package/dist/session/prompts.js.map +1 -1
- package/dist/session/session-manager.d.ts +6 -0
- package/dist/session/session-manager.js +12 -0
- package/dist/session/session-manager.js.map +1 -1
- package/dist/session/types.d.ts +11 -1
- package/dist/session/types.js.map +1 -1
- package/dist/session/workspace-validation.js +37 -6
- package/dist/session/workspace-validation.js.map +1 -1
- package/dist/signal/signal-bot-daemon.js +24 -8
- package/dist/signal/signal-bot-daemon.js.map +1 -1
- package/dist/signal/signal-transport.d.ts +5 -2
- package/dist/signal/signal-transport.js +3 -3
- package/dist/signal/signal-transport.js.map +1 -1
- package/dist/trusted-process/approval-whitelist.d.ts +117 -0
- package/dist/trusted-process/approval-whitelist.js +237 -0
- package/dist/trusted-process/approval-whitelist.js.map +1 -0
- package/dist/trusted-process/gworkspace-credentials.d.ts +40 -0
- package/dist/trusted-process/gworkspace-credentials.js +45 -0
- package/dist/trusted-process/gworkspace-credentials.js.map +1 -0
- package/dist/trusted-process/index.d.ts +11 -1
- package/dist/trusted-process/index.js +83 -53
- package/dist/trusted-process/index.js.map +1 -1
- package/dist/trusted-process/mcp-proxy-server.d.ts +23 -0
- package/dist/trusted-process/mcp-proxy-server.js +409 -94
- package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
- package/dist/trusted-process/policy-engine.d.ts +12 -3
- package/dist/trusted-process/policy-engine.js +33 -23
- package/dist/trusted-process/policy-engine.js.map +1 -1
- package/dist/trusted-process/policy-types.d.ts +17 -0
- package/dist/trusted-process/sandbox-integration.d.ts +23 -0
- package/dist/trusted-process/sandbox-integration.js +115 -3
- package/dist/trusted-process/sandbox-integration.js.map +1 -1
- package/dist/trusted-process/token-file-refresher.d.ts +69 -0
- package/dist/trusted-process/token-file-refresher.js +120 -0
- package/dist/trusted-process/token-file-refresher.js.map +1 -0
- package/dist/types/argument-roles.d.ts +7 -1
- package/dist/types/argument-roles.js +50 -0
- package/dist/types/argument-roles.js.map +1 -1
- package/dist/types/audit.d.ts +11 -0
- package/docker/Dockerfile.base +22 -0
- package/docker/Dockerfile.base.arm64 +44 -4
- package/docker/Dockerfile.claude-code +2 -2
- package/docker/entrypoint-claude-code.sh +24 -6
- package/package.json +8 -2
- package/src/config/constitution-user-base.md +15 -0
- package/src/config/generated/test-scenarios.json +1 -1
- package/src/config/generated/tool-annotations.json +10617 -296
- package/src/config/mcp-servers.json +31 -2
- package/dist/pipeline/mcp-connections.d.ts +0 -16
- package/dist/pipeline/mcp-connections.js +0 -61
- 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
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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,
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
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
|
|
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":
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
195
|
-
const
|
|
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
|
-
//
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
//
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
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
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
|
255
|
-
|
|
502
|
+
inputHash,
|
|
503
|
+
rules,
|
|
504
|
+
listDefinitions: listDefinitions.length > 0 ? listDefinitions : undefined,
|
|
256
505
|
});
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
this
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
272
|
-
toolNames:
|
|
549
|
+
serverNames,
|
|
550
|
+
toolNames: serverToolNames,
|
|
551
|
+
storedAnnotations: unit.storedAnnotations,
|
|
273
552
|
});
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
601
|
+
handwrittenScenarios: unit.handwrittenScenarios.length > 0 ? unit.handwrittenScenarios : undefined,
|
|
339
602
|
};
|
|
340
|
-
this.logContext.stepName = `repair-compile-${attempt}`;
|
|
341
|
-
const
|
|
342
|
-
const { result: repairResult } = await withSpinner(
|
|
343
|
-
spinner.text = `${
|
|
344
|
-
}), (r, elapsed) =>
|
|
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-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
|
377
|
-
toolNames:
|
|
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
|
|
385
|
-
const repairVerifyText = `
|
|
386
|
-
const { result: repairVerifyResult } = await withSpinner(repairVerifyText, async (spinner) => verifyPolicy(
|
|
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
|
-
?
|
|
390
|
-
:
|
|
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
|
-
//
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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(
|
|
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
|
-
|
|
462
|
-
return
|
|
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
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
794
|
+
return { scenarios, inputHash };
|
|
535
795
|
}
|
|
536
796
|
logVerboseFailures(result) {
|
|
537
797
|
console.error('');
|