@provos/ironcurtain 0.1.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/LICENSE +202 -0
- package/README.md +311 -0
- package/dist/agent/index.d.ts +10 -0
- package/dist/agent/index.js +71 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/prompts.d.ts +5 -0
- package/dist/agent/prompts.js +26 -0
- package/dist/agent/prompts.js.map +1 -0
- package/dist/agent/tools.d.ts +13 -0
- package/dist/agent/tools.js +51 -0
- package/dist/agent/tools.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +78 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/constitution.md +16 -0
- package/dist/config/generated/compiled-policy.json +236 -0
- package/dist/config/generated/test-scenarios.json +765 -0
- package/dist/config/generated/tool-annotations.json +955 -0
- package/dist/config/index.d.ts +25 -0
- package/dist/config/index.js +151 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/mcp-servers.json +22 -0
- package/dist/config/model-provider.d.ts +49 -0
- package/dist/config/model-provider.js +78 -0
- package/dist/config/model-provider.js.map +1 -0
- package/dist/config/paths.d.ts +59 -0
- package/dist/config/paths.js +96 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/types.d.ts +89 -0
- package/dist/config/types.js +2 -0
- package/dist/config/types.js.map +1 -0
- package/dist/config/user-config.d.ts +93 -0
- package/dist/config/user-config.js +309 -0
- package/dist/config/user-config.js.map +1 -0
- package/dist/hash.d.ts +17 -0
- package/dist/hash.js +34 -0
- package/dist/hash.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +11 -0
- package/dist/logger.js +93 -0
- package/dist/logger.js.map +1 -0
- package/dist/pipeline/annotate.d.ts +9 -0
- package/dist/pipeline/annotate.js +136 -0
- package/dist/pipeline/annotate.js.map +1 -0
- package/dist/pipeline/compile.d.ts +23 -0
- package/dist/pipeline/compile.js +386 -0
- package/dist/pipeline/compile.js.map +1 -0
- package/dist/pipeline/constitution-compiler.d.ts +22 -0
- package/dist/pipeline/constitution-compiler.js +197 -0
- package/dist/pipeline/constitution-compiler.js.map +1 -0
- package/dist/pipeline/generate-with-repair.d.ts +22 -0
- package/dist/pipeline/generate-with-repair.js +64 -0
- package/dist/pipeline/generate-with-repair.js.map +1 -0
- package/dist/pipeline/handwritten-scenarios.d.ts +9 -0
- package/dist/pipeline/handwritten-scenarios.js +321 -0
- package/dist/pipeline/handwritten-scenarios.js.map +1 -0
- package/dist/pipeline/llm-logger.d.ts +42 -0
- package/dist/pipeline/llm-logger.js +78 -0
- package/dist/pipeline/llm-logger.js.map +1 -0
- package/dist/pipeline/pipeline-shared.d.ts +47 -0
- package/dist/pipeline/pipeline-shared.js +145 -0
- package/dist/pipeline/pipeline-shared.js.map +1 -0
- package/dist/pipeline/policy-verifier.d.ts +46 -0
- package/dist/pipeline/policy-verifier.js +277 -0
- package/dist/pipeline/policy-verifier.js.map +1 -0
- package/dist/pipeline/scenario-generator.d.ts +11 -0
- package/dist/pipeline/scenario-generator.js +128 -0
- package/dist/pipeline/scenario-generator.js.map +1 -0
- package/dist/pipeline/tool-annotator.d.ts +24 -0
- package/dist/pipeline/tool-annotator.js +201 -0
- package/dist/pipeline/tool-annotator.js.map +1 -0
- package/dist/pipeline/types.d.ts +122 -0
- package/dist/pipeline/types.js +10 -0
- package/dist/pipeline/types.js.map +1 -0
- package/dist/sandbox/index.d.ts +39 -0
- package/dist/sandbox/index.js +178 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/session/agent-session.d.ts +83 -0
- package/dist/session/agent-session.js +382 -0
- package/dist/session/agent-session.js.map +1 -0
- package/dist/session/cli-transport.d.ts +61 -0
- package/dist/session/cli-transport.js +320 -0
- package/dist/session/cli-transport.js.map +1 -0
- package/dist/session/errors.d.ts +19 -0
- package/dist/session/errors.js +33 -0
- package/dist/session/errors.js.map +1 -0
- package/dist/session/index.d.ts +29 -0
- package/dist/session/index.js +104 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/message-compactor.d.ts +32 -0
- package/dist/session/message-compactor.js +81 -0
- package/dist/session/message-compactor.js.map +1 -0
- package/dist/session/prompts.d.ts +5 -0
- package/dist/session/prompts.js +62 -0
- package/dist/session/prompts.js.map +1 -0
- package/dist/session/resource-budget-tracker.d.ts +124 -0
- package/dist/session/resource-budget-tracker.js +327 -0
- package/dist/session/resource-budget-tracker.js.map +1 -0
- package/dist/session/step-loop-detector.d.ts +63 -0
- package/dist/session/step-loop-detector.js +136 -0
- package/dist/session/step-loop-detector.js.map +1 -0
- package/dist/session/transport.d.ts +24 -0
- package/dist/session/transport.js +2 -0
- package/dist/session/transport.js.map +1 -0
- package/dist/session/truncate-result.d.ts +35 -0
- package/dist/session/truncate-result.js +71 -0
- package/dist/session/truncate-result.js.map +1 -0
- package/dist/session/types.d.ts +220 -0
- package/dist/session/types.js +6 -0
- package/dist/session/types.js.map +1 -0
- package/dist/trusted-process/audit-log.d.ts +7 -0
- package/dist/trusted-process/audit-log.js +21 -0
- package/dist/trusted-process/audit-log.js.map +1 -0
- package/dist/trusted-process/call-circuit-breaker.d.ts +33 -0
- package/dist/trusted-process/call-circuit-breaker.js +61 -0
- package/dist/trusted-process/call-circuit-breaker.js.map +1 -0
- package/dist/trusted-process/escalation.d.ts +7 -0
- package/dist/trusted-process/escalation.js +38 -0
- package/dist/trusted-process/escalation.js.map +1 -0
- package/dist/trusted-process/index.d.ts +32 -0
- package/dist/trusted-process/index.js +151 -0
- package/dist/trusted-process/index.js.map +1 -0
- package/dist/trusted-process/mcp-client-manager.d.ts +25 -0
- package/dist/trusted-process/mcp-client-manager.js +90 -0
- package/dist/trusted-process/mcp-client-manager.js.map +1 -0
- package/dist/trusted-process/mcp-proxy-server.d.ts +24 -0
- package/dist/trusted-process/mcp-proxy-server.js +451 -0
- package/dist/trusted-process/mcp-proxy-server.js.map +1 -0
- package/dist/trusted-process/path-utils.d.ts +50 -0
- package/dist/trusted-process/path-utils.js +158 -0
- package/dist/trusted-process/path-utils.js.map +1 -0
- package/dist/trusted-process/policy-engine.d.ts +88 -0
- package/dist/trusted-process/policy-engine.js +523 -0
- package/dist/trusted-process/policy-engine.js.map +1 -0
- package/dist/trusted-process/policy-roots.d.ts +50 -0
- package/dist/trusted-process/policy-roots.js +67 -0
- package/dist/trusted-process/policy-roots.js.map +1 -0
- package/dist/trusted-process/policy-types.d.ts +6 -0
- package/dist/trusted-process/policy-types.js +2 -0
- package/dist/trusted-process/policy-types.js.map +1 -0
- package/dist/trusted-process/sandbox-integration.d.ts +92 -0
- package/dist/trusted-process/sandbox-integration.js +184 -0
- package/dist/trusted-process/sandbox-integration.js.map +1 -0
- package/dist/types/argument-roles.d.ts +112 -0
- package/dist/types/argument-roles.js +344 -0
- package/dist/types/argument-roles.js.map +1 -0
- package/dist/types/audit.d.ts +18 -0
- package/dist/types/audit.js +2 -0
- package/dist/types/audit.js.map +1 -0
- package/dist/types/mcp.d.ts +20 -0
- package/dist/types/mcp.js +2 -0
- package/dist/types/mcp.js.map +1 -0
- package/package.json +83 -0
- package/src/config/constitution.md +16 -0
- package/src/config/generated/compiled-policy.json +236 -0
- package/src/config/generated/test-scenarios.json +765 -0
- package/src/config/generated/tool-annotations.json +955 -0
- package/src/config/mcp-servers.json +22 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Interaction Logger -- AI SDK middleware that captures all LLM
|
|
3
|
+
* prompts and responses during the policy compilation pipeline.
|
|
4
|
+
*
|
|
5
|
+
* Uses `wrapLanguageModel()` with a custom middleware that intercepts
|
|
6
|
+
* every `doGenerate` call. The caller sets `context.stepName` before
|
|
7
|
+
* each pipeline phase so logs are labeled without changing module APIs.
|
|
8
|
+
*
|
|
9
|
+
* Writes a single JSONL file with one entry per LLM call.
|
|
10
|
+
*/
|
|
11
|
+
import type { LanguageModelMiddleware } from 'ai';
|
|
12
|
+
export interface LlmLogEntry {
|
|
13
|
+
timestamp: string;
|
|
14
|
+
stepName: string;
|
|
15
|
+
modelId: string;
|
|
16
|
+
/**
|
|
17
|
+
* For the first entry (promptOffset === 0), this is the full prompt array.
|
|
18
|
+
* For subsequent entries, this contains only new messages since the last
|
|
19
|
+
* logged entry. Reconstruct the full prompt by concatenating all entries'
|
|
20
|
+
* prompt arrays in order.
|
|
21
|
+
*/
|
|
22
|
+
prompt: unknown;
|
|
23
|
+
/** Index into the full prompt array where this entry's messages start. */
|
|
24
|
+
promptOffset: number;
|
|
25
|
+
responseText: string;
|
|
26
|
+
usage: {
|
|
27
|
+
inputTokens: number | undefined;
|
|
28
|
+
outputTokens: number | undefined;
|
|
29
|
+
};
|
|
30
|
+
durationMs: number;
|
|
31
|
+
}
|
|
32
|
+
export interface LlmLogContext {
|
|
33
|
+
stepName: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Creates an AI SDK middleware that logs every generate call to a JSONL file.
|
|
37
|
+
*
|
|
38
|
+
* @param logPath - Absolute path to the output JSONL file.
|
|
39
|
+
* @param context - Mutable context object. Set `context.stepName` before each
|
|
40
|
+
* pipeline phase to label the log entries.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createLlmLoggingMiddleware(logPath: string, context: LlmLogContext): LanguageModelMiddleware;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Interaction Logger -- AI SDK middleware that captures all LLM
|
|
3
|
+
* prompts and responses during the policy compilation pipeline.
|
|
4
|
+
*
|
|
5
|
+
* Uses `wrapLanguageModel()` with a custom middleware that intercepts
|
|
6
|
+
* every `doGenerate` call. The caller sets `context.stepName` before
|
|
7
|
+
* each pipeline phase so logs are labeled without changing module APIs.
|
|
8
|
+
*
|
|
9
|
+
* Writes a single JSONL file with one entry per LLM call.
|
|
10
|
+
*/
|
|
11
|
+
import { writeFileSync, appendFileSync, mkdirSync } from 'node:fs';
|
|
12
|
+
import { dirname } from 'node:path';
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Middleware factory
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
/**
|
|
17
|
+
* Creates an AI SDK middleware that logs every generate call to a JSONL file.
|
|
18
|
+
*
|
|
19
|
+
* @param logPath - Absolute path to the output JSONL file.
|
|
20
|
+
* @param context - Mutable context object. Set `context.stepName` before each
|
|
21
|
+
* pipeline phase to label the log entries.
|
|
22
|
+
*/
|
|
23
|
+
export function createLlmLoggingMiddleware(logPath, context) {
|
|
24
|
+
initLogFile(logPath);
|
|
25
|
+
// Track how many prompt items were logged so far to enable delta logging.
|
|
26
|
+
let previousPromptLength = 0;
|
|
27
|
+
return {
|
|
28
|
+
specificationVersion: 'v3',
|
|
29
|
+
wrapGenerate: async ({ doGenerate, params, model }) => {
|
|
30
|
+
const start = Date.now();
|
|
31
|
+
const result = await doGenerate();
|
|
32
|
+
const durationMs = Date.now() - start;
|
|
33
|
+
const responseText = extractTextFromContent(result.content);
|
|
34
|
+
// Only log new messages since the last entry (delta logging).
|
|
35
|
+
const fullPrompt = params.prompt;
|
|
36
|
+
const promptArray = Array.isArray(fullPrompt) ? fullPrompt : [fullPrompt];
|
|
37
|
+
const promptOffset = previousPromptLength;
|
|
38
|
+
const newMessages = promptArray.slice(promptOffset);
|
|
39
|
+
previousPromptLength = promptArray.length;
|
|
40
|
+
const entry = {
|
|
41
|
+
timestamp: new Date().toISOString(),
|
|
42
|
+
stepName: context.stepName,
|
|
43
|
+
modelId: model.modelId,
|
|
44
|
+
prompt: newMessages,
|
|
45
|
+
promptOffset,
|
|
46
|
+
responseText,
|
|
47
|
+
usage: {
|
|
48
|
+
inputTokens: result.usage?.inputTokens?.total,
|
|
49
|
+
outputTokens: result.usage?.outputTokens?.total,
|
|
50
|
+
},
|
|
51
|
+
durationMs,
|
|
52
|
+
};
|
|
53
|
+
appendLogEntry(logPath, entry);
|
|
54
|
+
return result;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// File I/O helpers
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
function initLogFile(logPath) {
|
|
62
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
63
|
+
// Truncate any existing file from a previous run
|
|
64
|
+
writeFileSync(logPath, '');
|
|
65
|
+
}
|
|
66
|
+
function appendLogEntry(logPath, entry) {
|
|
67
|
+
appendFileSync(logPath, JSON.stringify(entry) + '\n');
|
|
68
|
+
}
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Content extraction
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
function extractTextFromContent(content) {
|
|
73
|
+
return content
|
|
74
|
+
.filter((part) => part.type === 'text' && typeof part.text === 'string')
|
|
75
|
+
.map(part => part.text)
|
|
76
|
+
.join('');
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=llm-logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llm-logger.js","sourceRoot":"","sources":["../../src/pipeline/llm-logger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACnE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgCpC,8EAA8E;AAC9E,qBAAqB;AACrB,8EAA8E;AAE9E;;;;;;GAMG;AACH,MAAM,UAAU,0BAA0B,CACxC,OAAe,EACf,OAAsB;IAEtB,WAAW,CAAC,OAAO,CAAC,CAAC;IAErB,0EAA0E;IAC1E,IAAI,oBAAoB,GAAG,CAAC,CAAC;IAE7B,OAAO;QACL,oBAAoB,EAAE,IAAI;QAC1B,YAAY,EAAE,KAAK,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE;YACpD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACzB,MAAM,MAAM,GAAG,MAAM,UAAU,EAAE,CAAC;YAClC,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;YAEtC,MAAM,YAAY,GAAG,sBAAsB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAE5D,8DAA8D;YAC9D,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC;YACjC,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;YAC1E,MAAM,YAAY,GAAG,oBAAoB,CAAC;YAC1C,MAAM,WAAW,GAAG,WAAW,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;YACpD,oBAAoB,GAAG,WAAW,CAAC,MAAM,CAAC;YAE1C,MAAM,KAAK,GAAgB;gBACzB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;gBACnC,QAAQ,EAAE,OAAO,CAAC,QAAQ;gBAC1B,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,MAAM,EAAE,WAAW;gBACnB,YAAY;gBACZ,YAAY;gBACZ,KAAK,EAAE;oBACL,WAAW,EAAE,MAAM,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK;oBAC7C,YAAY,EAAE,MAAM,CAAC,KAAK,EAAE,YAAY,EAAE,KAAK;iBAChD;gBACD,UAAU;aACX,CAAC;YAEF,cAAc,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YAC/B,OAAO,MAAM,CAAC;QAChB,CAAC;KACF,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,mBAAmB;AACnB,8EAA8E;AAE9E,SAAS,WAAW,CAAC,OAAe;IAClC,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACjD,iDAAiD;IACjD,aAAa,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AAC7B,CAAC;AAED,SAAS,cAAc,CAAC,OAAe,EAAE,KAAkB;IACzD,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;AACxD,CAAC;AAED,8EAA8E;AAC9E,qBAAqB;AACrB,8EAA8E;AAE9E,SAAS,sBAAsB,CAAC,OAA+C;IAC7E,OAAO,OAAO;SACX,MAAM,CAAC,CAAC,IAAI,EAA0C,EAAE,CACvD,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CACtD;SACA,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;SACtB,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for the policy compilation pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Used by both `annotate.ts` (tool annotation) and `compile.ts`
|
|
5
|
+
* (policy compilation) to avoid duplication of config loading,
|
|
6
|
+
* content-hash caching, and spinner helpers.
|
|
7
|
+
*/
|
|
8
|
+
import type { LanguageModel } from 'ai';
|
|
9
|
+
import { type Ora } from 'ora';
|
|
10
|
+
import type { MCPServerConfig } from '../config/types.js';
|
|
11
|
+
import { type LlmLogContext } from './llm-logger.js';
|
|
12
|
+
export interface PipelineConfig {
|
|
13
|
+
constitutionPath: string;
|
|
14
|
+
constitutionText: string;
|
|
15
|
+
constitutionHash: string;
|
|
16
|
+
mcpServers: Record<string, MCPServerConfig>;
|
|
17
|
+
generatedDir: string;
|
|
18
|
+
packageGeneratedDir: string;
|
|
19
|
+
allowedDirectory: string;
|
|
20
|
+
protectedPaths: string[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Loads the combined constitution text (base + optional user constitution).
|
|
24
|
+
* The user constitution file is at ~/.ironcurtain/constitution-user.md.
|
|
25
|
+
* When present, it is appended to the base constitution text.
|
|
26
|
+
*/
|
|
27
|
+
export declare function loadConstitutionText(basePath: string): string;
|
|
28
|
+
export declare function loadPipelineConfig(): PipelineConfig;
|
|
29
|
+
export declare function computeHash(...inputs: string[]): string;
|
|
30
|
+
export declare function loadExistingArtifact<T>(generatedDir: string, filename: string, fallbackDir?: string): T | undefined;
|
|
31
|
+
export declare function showCached(text: string): void;
|
|
32
|
+
export declare function withSpinner<T>(text: string, fn: (spinner: Ora) => Promise<T>, successFn?: (result: T, elapsed: number) => string): Promise<{
|
|
33
|
+
result: T;
|
|
34
|
+
elapsed: number;
|
|
35
|
+
}>;
|
|
36
|
+
/** Writes a JSON artifact to the generated directory with consistent formatting. */
|
|
37
|
+
export declare function writeArtifact(generatedDir: string, filename: string, data: unknown): void;
|
|
38
|
+
export interface PipelineLlm {
|
|
39
|
+
model: LanguageModel;
|
|
40
|
+
logContext: LlmLogContext;
|
|
41
|
+
logPath: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Creates a language model wrapped with pipeline logging middleware.
|
|
45
|
+
* Both annotate.ts and compile.ts use this to set up their LLM instance.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createPipelineLlm(generatedDir: string, initialStepName: string): Promise<PipelineLlm>;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for the policy compilation pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Used by both `annotate.ts` (tool annotation) and `compile.ts`
|
|
5
|
+
* (policy compilation) to avoid duplication of config loading,
|
|
6
|
+
* content-hash caching, and spinner helpers.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { resolve, dirname } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { wrapLanguageModel } from 'ai';
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import ora from 'ora';
|
|
15
|
+
import { computeProtectedPaths } from '../config/index.js';
|
|
16
|
+
import { createLanguageModel } from '../config/model-provider.js';
|
|
17
|
+
import { getIronCurtainHome, getUserConstitutionPath, getUserGeneratedDir } from '../config/paths.js';
|
|
18
|
+
import { loadUserConfig } from '../config/user-config.js';
|
|
19
|
+
import { createLlmLoggingMiddleware } from './llm-logger.js';
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
/**
|
|
22
|
+
* Loads the combined constitution text (base + optional user constitution).
|
|
23
|
+
* The user constitution file is at ~/.ironcurtain/constitution-user.md.
|
|
24
|
+
* When present, it is appended to the base constitution text.
|
|
25
|
+
*/
|
|
26
|
+
export function loadConstitutionText(basePath) {
|
|
27
|
+
const base = readFileSync(basePath, 'utf-8');
|
|
28
|
+
const userPath = getUserConstitutionPath();
|
|
29
|
+
if (existsSync(userPath)) {
|
|
30
|
+
const user = readFileSync(userPath, 'utf-8');
|
|
31
|
+
return `${base}\n\n${user}`;
|
|
32
|
+
}
|
|
33
|
+
return base;
|
|
34
|
+
}
|
|
35
|
+
export function loadPipelineConfig() {
|
|
36
|
+
const configDir = resolve(__dirname, '..', 'config');
|
|
37
|
+
const constitutionPath = resolve(configDir, 'constitution.md');
|
|
38
|
+
const constitutionText = loadConstitutionText(constitutionPath);
|
|
39
|
+
const constitutionHash = createHash('sha256').update(constitutionText).digest('hex');
|
|
40
|
+
const mcpServersPath = resolve(configDir, 'mcp-servers.json');
|
|
41
|
+
const mcpServers = JSON.parse(readFileSync(mcpServersPath, 'utf-8'));
|
|
42
|
+
const generatedDir = getUserGeneratedDir();
|
|
43
|
+
const packageGeneratedDir = resolve(configDir, 'generated');
|
|
44
|
+
const defaultAllowedDir = resolve(getIronCurtainHome(), 'sandbox');
|
|
45
|
+
const allowedDirectory = process.env.ALLOWED_DIRECTORY ?? defaultAllowedDir;
|
|
46
|
+
const auditLogPath = process.env.AUDIT_LOG_PATH ?? './audit.jsonl';
|
|
47
|
+
const protectedPaths = computeProtectedPaths({
|
|
48
|
+
constitutionPath,
|
|
49
|
+
generatedDir,
|
|
50
|
+
packageGeneratedDir,
|
|
51
|
+
mcpServersPath,
|
|
52
|
+
auditLogPath,
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
constitutionPath,
|
|
56
|
+
constitutionText,
|
|
57
|
+
constitutionHash,
|
|
58
|
+
mcpServers,
|
|
59
|
+
generatedDir,
|
|
60
|
+
packageGeneratedDir,
|
|
61
|
+
allowedDirectory,
|
|
62
|
+
protectedPaths,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Content-Hash Caching
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
export function computeHash(...inputs) {
|
|
69
|
+
const hash = createHash('sha256');
|
|
70
|
+
for (const input of inputs) {
|
|
71
|
+
hash.update(input);
|
|
72
|
+
}
|
|
73
|
+
return hash.digest('hex');
|
|
74
|
+
}
|
|
75
|
+
export function loadExistingArtifact(generatedDir, filename, fallbackDir) {
|
|
76
|
+
const candidates = [resolve(generatedDir, filename)];
|
|
77
|
+
if (fallbackDir) {
|
|
78
|
+
candidates.push(resolve(fallbackDir, filename));
|
|
79
|
+
}
|
|
80
|
+
for (const path of candidates) {
|
|
81
|
+
if (existsSync(path)) {
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Corrupt file -- try next candidate
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Spinner Helpers
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
export function showCached(text) {
|
|
96
|
+
const spinner = ora({ text, stream: process.stderr, discardStdin: false }).start();
|
|
97
|
+
spinner.succeed(`${text} ${chalk.dim('(cached)')}`);
|
|
98
|
+
}
|
|
99
|
+
export async function withSpinner(text, fn, successFn) {
|
|
100
|
+
const spinner = ora({ text, stream: process.stderr, discardStdin: false }).start();
|
|
101
|
+
const start = Date.now();
|
|
102
|
+
const timer = setInterval(() => {
|
|
103
|
+
const secs = ((Date.now() - start) / 1000).toFixed(0);
|
|
104
|
+
spinner.text = `${text} (${secs}s)`;
|
|
105
|
+
}, 1000);
|
|
106
|
+
try {
|
|
107
|
+
const result = await fn(spinner);
|
|
108
|
+
clearInterval(timer);
|
|
109
|
+
const elapsed = (Date.now() - start) / 1000;
|
|
110
|
+
const successText = successFn
|
|
111
|
+
? successFn(result, elapsed)
|
|
112
|
+
: `${text} (${elapsed.toFixed(1)}s)`;
|
|
113
|
+
spinner.succeed(successText);
|
|
114
|
+
return { result, elapsed };
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
clearInterval(timer);
|
|
118
|
+
spinner.fail(chalk.red(text));
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Artifact I/O
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
/** Writes a JSON artifact to the generated directory with consistent formatting. */
|
|
126
|
+
export function writeArtifact(generatedDir, filename, data) {
|
|
127
|
+
mkdirSync(generatedDir, { recursive: true });
|
|
128
|
+
writeFileSync(resolve(generatedDir, filename), JSON.stringify(data, null, 2) + '\n');
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Creates a language model wrapped with pipeline logging middleware.
|
|
132
|
+
* Both annotate.ts and compile.ts use this to set up their LLM instance.
|
|
133
|
+
*/
|
|
134
|
+
export async function createPipelineLlm(generatedDir, initialStepName) {
|
|
135
|
+
const userConfig = loadUserConfig();
|
|
136
|
+
const baseLlm = await createLanguageModel(userConfig.policyModelId, userConfig);
|
|
137
|
+
const logContext = { stepName: initialStepName };
|
|
138
|
+
const logPath = resolve(generatedDir, 'llm-interactions.jsonl');
|
|
139
|
+
const model = wrapLanguageModel({
|
|
140
|
+
model: baseLlm,
|
|
141
|
+
middleware: createLlmLoggingMiddleware(logPath, logContext),
|
|
142
|
+
});
|
|
143
|
+
return { model, logContext, logPath };
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=pipeline-shared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline-shared.js","sourceRoot":"","sources":["../../src/pipeline/pipeline-shared.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy Verifier -- Multi-round verification with real engine execution
|
|
3
|
+
* and LLM judge.
|
|
4
|
+
*
|
|
5
|
+
* The verifier instantiates a real PolicyEngine, runs test scenarios through
|
|
6
|
+
* it, and uses an LLM judge to analyze results, identify gaps, and generate
|
|
7
|
+
* additional probe scenarios. Up to maxRounds iterations.
|
|
8
|
+
*/
|
|
9
|
+
import type { LanguageModel } from 'ai';
|
|
10
|
+
import { PolicyEngine } from '../trusted-process/policy-engine.js';
|
|
11
|
+
import type { CompiledPolicyFile, ToolAnnotationsFile, TestScenario, ExecutionResult, VerificationResult, AttributedFailure, ScenarioCorrection } from './types.js';
|
|
12
|
+
export declare function executeScenarios(engine: PolicyEngine, scenarios: TestScenario[]): ExecutionResult[];
|
|
13
|
+
export declare function formatExecutionResults(results: ExecutionResult[]): string;
|
|
14
|
+
export declare function verifyPolicy(constitutionText: string, compiledPolicy: CompiledPolicyFile, toolAnnotations: ToolAnnotationsFile, protectedPaths: string[], scenarios: TestScenario[], llm: LanguageModel, maxRounds?: number, allowedDirectory?: string, onProgress?: (message: string) => void): Promise<VerificationResult>;
|
|
15
|
+
export interface DiscardedScenario {
|
|
16
|
+
scenario: TestScenario;
|
|
17
|
+
actual: ExecutionResult['actualDecision'];
|
|
18
|
+
rule: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Filters out scenarios whose expected decision conflicts with a structural
|
|
22
|
+
* invariant result. Structural invariants are ground truth (hardcoded in the
|
|
23
|
+
* engine), so a disagreeing scenario expectation is wrong by definition.
|
|
24
|
+
*
|
|
25
|
+
* Returns the valid scenarios and the discarded ones (with metadata for
|
|
26
|
+
* logging / feedback).
|
|
27
|
+
*/
|
|
28
|
+
export declare function filterStructuralConflicts(engine: PolicyEngine, scenarios: TestScenario[]): {
|
|
29
|
+
valid: TestScenario[];
|
|
30
|
+
discarded: DiscardedScenario[];
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Extracts scenario corrections from attributed failures.
|
|
34
|
+
* Only failures blamed on 'scenario' or 'both' produce corrections.
|
|
35
|
+
* Handwritten scenarios are never auto-corrected — they are returned
|
|
36
|
+
* separately as warnings so the caller can reclassify them as rule issues.
|
|
37
|
+
*/
|
|
38
|
+
export declare function extractScenarioCorrections(attributedFailures: AttributedFailure[], scenarios: TestScenario[]): {
|
|
39
|
+
corrections: ScenarioCorrection[];
|
|
40
|
+
handwrittenWarnings: string[];
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Applies corrections to a scenario list, returning a new array with
|
|
44
|
+
* updated expectedDecision and reasoning for corrected scenarios.
|
|
45
|
+
*/
|
|
46
|
+
export declare function applyScenarioCorrections(scenarios: TestScenario[], corrections: ScenarioCorrection[]): TestScenario[];
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy Verifier -- Multi-round verification with real engine execution
|
|
3
|
+
* and LLM judge.
|
|
4
|
+
*
|
|
5
|
+
* The verifier instantiates a real PolicyEngine, runs test scenarios through
|
|
6
|
+
* it, and uses an LLM judge to analyze results, identify gaps, and generate
|
|
7
|
+
* additional probe scenarios. Up to maxRounds iterations.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { generateObjectWithRepair } from './generate-with-repair.js';
|
|
11
|
+
import { PolicyEngine } from '../trusted-process/policy-engine.js';
|
|
12
|
+
const DEFAULT_MAX_ROUNDS = 3;
|
|
13
|
+
export function executeScenarios(engine, scenarios) {
|
|
14
|
+
return scenarios.map(scenario => {
|
|
15
|
+
const request = {
|
|
16
|
+
requestId: `verify-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
17
|
+
serverName: scenario.request.serverName,
|
|
18
|
+
toolName: scenario.request.toolName,
|
|
19
|
+
arguments: scenario.request.arguments,
|
|
20
|
+
timestamp: new Date().toISOString(),
|
|
21
|
+
};
|
|
22
|
+
const result = engine.evaluate(request);
|
|
23
|
+
return {
|
|
24
|
+
scenario,
|
|
25
|
+
actualDecision: result.decision,
|
|
26
|
+
matchingRule: result.rule,
|
|
27
|
+
pass: result.decision === scenario.expectedDecision,
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export function formatExecutionResults(results) {
|
|
32
|
+
return results.map((r, i) => {
|
|
33
|
+
const status = r.pass ? 'PASS' : 'FAIL';
|
|
34
|
+
return [
|
|
35
|
+
`[${i + 1}] ${status}: ${r.scenario.description}`,
|
|
36
|
+
` Tool: ${r.scenario.request.serverName}/${r.scenario.request.toolName}`,
|
|
37
|
+
` Args: ${JSON.stringify(r.scenario.request.arguments)}`,
|
|
38
|
+
` Expected: ${r.scenario.expectedDecision}`,
|
|
39
|
+
` Actual: ${r.actualDecision} (rule: ${r.matchingRule})`,
|
|
40
|
+
` Source: ${r.scenario.source}`,
|
|
41
|
+
].join('\n');
|
|
42
|
+
}).join('\n\n');
|
|
43
|
+
}
|
|
44
|
+
function buildJudgePrompt(constitutionText, compiledPolicy, toolAnnotations, protectedPaths, executionResults, roundNumber, previousAnalysis, availableTools) {
|
|
45
|
+
const rulesText = compiledPolicy.rules
|
|
46
|
+
.map((r, i) => ` ${i + 1}. [${r.name}] if: ${JSON.stringify(r.if)} then: ${r.then} -- ${r.reason}`)
|
|
47
|
+
.join('\n');
|
|
48
|
+
const resultsText = formatExecutionResults(executionResults);
|
|
49
|
+
const previousContext = previousAnalysis
|
|
50
|
+
? `\n## Previous Round Analysis\n\n${previousAnalysis}\n`
|
|
51
|
+
: '';
|
|
52
|
+
return `You are a security policy verifier. Analyze whether the compiled policy correctly implements the constitution.
|
|
53
|
+
|
|
54
|
+
## Constitution
|
|
55
|
+
|
|
56
|
+
${constitutionText}
|
|
57
|
+
|
|
58
|
+
## Compiled Policy Rules (evaluated in order, first match wins)
|
|
59
|
+
|
|
60
|
+
${rulesText}
|
|
61
|
+
|
|
62
|
+
## Structural Invariants (hardcoded, evaluated before compiled rules)
|
|
63
|
+
|
|
64
|
+
- Unknown tools (no annotation): denied
|
|
65
|
+
- Protected paths (any write/delete targeting these is automatically denied):
|
|
66
|
+
${protectedPaths.map(p => ` - ${p}`).join('\n')}
|
|
67
|
+
|
|
68
|
+
A \`deny\` result for write/delete operations on these paths is correct structural behavior, not a policy gap. Files with similar names inside the sandbox are NOT protected.
|
|
69
|
+
|
|
70
|
+
## Execution Results (Round ${roundNumber})
|
|
71
|
+
${previousContext}
|
|
72
|
+
${resultsText}
|
|
73
|
+
|
|
74
|
+
## Decision Types
|
|
75
|
+
|
|
76
|
+
The three possible policy decisions are:
|
|
77
|
+
- **allow** — the operation is explicitly permitted by the constitution
|
|
78
|
+
- **deny** — the operation is categorically forbidden by the constitution (absolute prohibition)
|
|
79
|
+
- **escalate** — the operation is not explicitly permitted but also not forbidden; it requires human approval
|
|
80
|
+
|
|
81
|
+
When analyzing FAIL results, pay attention to whether the constitution implies "deny" vs "escalate". If the constitution does not explicitly forbid an operation, the correct decision is typically "escalate" (not "deny") so a human can make the judgment call.
|
|
82
|
+
|
|
83
|
+
## Instructions
|
|
84
|
+
|
|
85
|
+
1. Analyze any FAIL results. For each failure, determine the blame:
|
|
86
|
+
- **"rule"**: The compiled rule is wrong and needs to be fixed. The scenario expectation correctly reflects the constitution.
|
|
87
|
+
- **"scenario"**: The scenario expectation is wrong. The compiled rule is correct per the constitution. Provide the corrected expectedDecision and reasoning.
|
|
88
|
+
- **"both"**: The rule needs adjustment AND the scenario expectation is wrong. Provide the corrected expectedDecision and reasoning.
|
|
89
|
+
2. Identify suspicious patterns (e.g., a broad allow rule shadowing a narrow deny, or "deny" used where "escalate" would be more appropriate).
|
|
90
|
+
3. Identify missing coverage -- scenarios the constitution implies that were not tested.
|
|
91
|
+
4. If you suspect gaps, generate additional test scenarios to probe them.
|
|
92
|
+
5. Set "pass" to true ONLY if all results are correct and coverage is adequate.
|
|
93
|
+
6. Return a "failureAttributions" entry for EVERY FAIL result. The scenarioDescription must exactly match the FAIL scenario's description.
|
|
94
|
+
|
|
95
|
+
For additional scenarios, use concrete paths matching the directories in the compiled rules. Note: sandbox containment is handled by a structural invariant before compiled rules run — any tool call where all paths are within the sandbox directory is automatically allowed.
|
|
96
|
+
|
|
97
|
+
## Available Tools
|
|
98
|
+
|
|
99
|
+
IMPORTANT: Only use these exact server/tool combinations in additional scenarios. Do NOT invent tool names.
|
|
100
|
+
|
|
101
|
+
${(availableTools ?? []).map(t => `- ${t.serverName}/${t.toolName}`).join('\n')}
|
|
102
|
+
|
|
103
|
+
## Response Format
|
|
104
|
+
|
|
105
|
+
Be concise. Keep the analysis to 2-3 sentences per issue found. Only generate additional scenarios that test genuinely untested gaps -- do not duplicate existing coverage. Limit additional scenarios to at most 5.`;
|
|
106
|
+
}
|
|
107
|
+
export async function verifyPolicy(constitutionText, compiledPolicy, toolAnnotations, protectedPaths, scenarios, llm, maxRounds = DEFAULT_MAX_ROUNDS, allowedDirectory, onProgress) {
|
|
108
|
+
const engine = new PolicyEngine(compiledPolicy, toolAnnotations, protectedPaths, allowedDirectory);
|
|
109
|
+
const allAnnotations = Object.values(toolAnnotations.servers).flatMap(s => s.tools);
|
|
110
|
+
const serverNamesList = [...new Set(allAnnotations.map(a => a.serverName))];
|
|
111
|
+
const toolNamesList = [...new Set(allAnnotations.map(a => a.toolName))];
|
|
112
|
+
const availableTools = allAnnotations.map(a => ({ serverName: a.serverName, toolName: a.toolName }));
|
|
113
|
+
const blameSchema = z.discriminatedUnion('kind', [
|
|
114
|
+
z.object({
|
|
115
|
+
kind: z.literal('rule'),
|
|
116
|
+
reasoning: z.string(),
|
|
117
|
+
}),
|
|
118
|
+
z.object({
|
|
119
|
+
kind: z.literal('scenario'),
|
|
120
|
+
reasoning: z.string(),
|
|
121
|
+
correctedDecision: z.enum(['allow', 'deny', 'escalate']),
|
|
122
|
+
correctedReasoning: z.string(),
|
|
123
|
+
}),
|
|
124
|
+
z.object({
|
|
125
|
+
kind: z.literal('both'),
|
|
126
|
+
reasoning: z.string(),
|
|
127
|
+
correctedDecision: z.enum(['allow', 'deny', 'escalate']),
|
|
128
|
+
correctedReasoning: z.string(),
|
|
129
|
+
}),
|
|
130
|
+
]);
|
|
131
|
+
const responseSchema = z.object({
|
|
132
|
+
analysis: z.string(),
|
|
133
|
+
pass: z.boolean(),
|
|
134
|
+
failureAttributions: z.array(z.object({
|
|
135
|
+
scenarioDescription: z.string(),
|
|
136
|
+
blame: blameSchema,
|
|
137
|
+
})),
|
|
138
|
+
additionalScenarios: z.array(z.object({
|
|
139
|
+
description: z.string(),
|
|
140
|
+
request: z.object({
|
|
141
|
+
serverName: z.enum(serverNamesList),
|
|
142
|
+
toolName: z.enum(toolNamesList),
|
|
143
|
+
arguments: z.record(z.string(), z.unknown()),
|
|
144
|
+
}),
|
|
145
|
+
expectedDecision: z.enum(['allow', 'deny', 'escalate']),
|
|
146
|
+
reasoning: z.string(),
|
|
147
|
+
})),
|
|
148
|
+
});
|
|
149
|
+
const rounds = [];
|
|
150
|
+
const allFailedScenarios = [];
|
|
151
|
+
let currentScenarios = scenarios;
|
|
152
|
+
let previousAnalysis;
|
|
153
|
+
for (let round = 1; round <= maxRounds; round++) {
|
|
154
|
+
// Execute scenarios against the real engine
|
|
155
|
+
const executionResults = executeScenarios(engine, currentScenarios);
|
|
156
|
+
const failures = executionResults.filter(r => !r.pass);
|
|
157
|
+
allFailedScenarios.push(...failures);
|
|
158
|
+
// Send results to LLM judge
|
|
159
|
+
const prompt = buildJudgePrompt(constitutionText, compiledPolicy, toolAnnotations, protectedPaths, executionResults, round, previousAnalysis, availableTools);
|
|
160
|
+
const { output: judgment } = await generateObjectWithRepair({
|
|
161
|
+
model: llm,
|
|
162
|
+
schema: responseSchema,
|
|
163
|
+
prompt,
|
|
164
|
+
onProgress,
|
|
165
|
+
});
|
|
166
|
+
const newScenarios = judgment.additionalScenarios.map(s => ({
|
|
167
|
+
...s,
|
|
168
|
+
source: 'generated',
|
|
169
|
+
}));
|
|
170
|
+
rounds.push({
|
|
171
|
+
round,
|
|
172
|
+
executionResults,
|
|
173
|
+
llmAnalysis: judgment.analysis,
|
|
174
|
+
newScenarios,
|
|
175
|
+
attributedFailures: judgment.failureAttributions,
|
|
176
|
+
});
|
|
177
|
+
previousAnalysis = judgment.analysis;
|
|
178
|
+
// If judge says pass and no new scenarios, we are done
|
|
179
|
+
if (judgment.pass && newScenarios.length === 0) {
|
|
180
|
+
return {
|
|
181
|
+
pass: true,
|
|
182
|
+
rounds,
|
|
183
|
+
summary: judgment.analysis,
|
|
184
|
+
failedScenarios: allFailedScenarios,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
// If no new scenarios to probe, exit the loop
|
|
188
|
+
if (newScenarios.length === 0)
|
|
189
|
+
break;
|
|
190
|
+
// Run additional scenarios in next round
|
|
191
|
+
currentScenarios = newScenarios;
|
|
192
|
+
}
|
|
193
|
+
// Determine final result based on failed scenarios
|
|
194
|
+
const lastRound = rounds[rounds.length - 1];
|
|
195
|
+
const hasFailures = allFailedScenarios.length > 0;
|
|
196
|
+
return {
|
|
197
|
+
pass: !hasFailures && (lastRound?.llmAnalysis.toLowerCase().includes('pass') ?? false),
|
|
198
|
+
rounds,
|
|
199
|
+
summary: lastRound?.llmAnalysis ?? 'Verification did not complete',
|
|
200
|
+
failedScenarios: allFailedScenarios,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Filters out scenarios whose expected decision conflicts with a structural
|
|
205
|
+
* invariant result. Structural invariants are ground truth (hardcoded in the
|
|
206
|
+
* engine), so a disagreeing scenario expectation is wrong by definition.
|
|
207
|
+
*
|
|
208
|
+
* Returns the valid scenarios and the discarded ones (with metadata for
|
|
209
|
+
* logging / feedback).
|
|
210
|
+
*/
|
|
211
|
+
export function filterStructuralConflicts(engine, scenarios) {
|
|
212
|
+
const results = executeScenarios(engine, scenarios);
|
|
213
|
+
const valid = [];
|
|
214
|
+
const discarded = [];
|
|
215
|
+
for (const result of results) {
|
|
216
|
+
const isStructural = result.matchingRule.startsWith('structural-');
|
|
217
|
+
if (isStructural && !result.pass) {
|
|
218
|
+
discarded.push({
|
|
219
|
+
scenario: result.scenario,
|
|
220
|
+
actual: result.actualDecision,
|
|
221
|
+
rule: result.matchingRule,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
valid.push(result.scenario);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return { valid, discarded };
|
|
229
|
+
}
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Scenario Corrections
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
/**
|
|
234
|
+
* Extracts scenario corrections from attributed failures.
|
|
235
|
+
* Only failures blamed on 'scenario' or 'both' produce corrections.
|
|
236
|
+
* Handwritten scenarios are never auto-corrected — they are returned
|
|
237
|
+
* separately as warnings so the caller can reclassify them as rule issues.
|
|
238
|
+
*/
|
|
239
|
+
export function extractScenarioCorrections(attributedFailures, scenarios) {
|
|
240
|
+
const corrections = [];
|
|
241
|
+
const handwrittenWarnings = [];
|
|
242
|
+
for (const af of attributedFailures) {
|
|
243
|
+
if (af.blame.kind === 'rule')
|
|
244
|
+
continue;
|
|
245
|
+
const scenario = scenarios.find(s => s.description === af.scenarioDescription);
|
|
246
|
+
if (!scenario)
|
|
247
|
+
continue;
|
|
248
|
+
if (scenario.source === 'handwritten') {
|
|
249
|
+
handwrittenWarnings.push(`Judge blamed scenario "${af.scenarioDescription}" but it is handwritten — treating as rule issue`);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
corrections.push({
|
|
253
|
+
scenarioDescription: af.scenarioDescription,
|
|
254
|
+
correctedDecision: af.blame.correctedDecision,
|
|
255
|
+
correctedReasoning: af.blame.correctedReasoning,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return { corrections, handwrittenWarnings };
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Applies corrections to a scenario list, returning a new array with
|
|
262
|
+
* updated expectedDecision and reasoning for corrected scenarios.
|
|
263
|
+
*/
|
|
264
|
+
export function applyScenarioCorrections(scenarios, corrections) {
|
|
265
|
+
const correctionMap = new Map(corrections.map(c => [c.scenarioDescription, c]));
|
|
266
|
+
return scenarios.map(s => {
|
|
267
|
+
const correction = correctionMap.get(s.description);
|
|
268
|
+
if (!correction)
|
|
269
|
+
return s;
|
|
270
|
+
return {
|
|
271
|
+
...s,
|
|
272
|
+
expectedDecision: correction.correctedDecision,
|
|
273
|
+
reasoning: correction.correctedReasoning,
|
|
274
|
+
};
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
//# sourceMappingURL=policy-verifier.js.map
|