codictator 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/guardrail.js +6 -2
- package/dist/cli/plan.d.ts +3 -1
- package/dist/cli/plan.js +48 -9
- package/dist/features/adapters/harness-files.d.ts +15 -0
- package/dist/features/adapters/harness-files.js +85 -0
- package/dist/features/adapters/index.d.ts +2 -1
- package/dist/features/adapters/index.js +2 -1
- package/dist/features/engine/evaluate.js +42 -54
- package/dist/features/engine/run-enforcer.d.ts +7 -0
- package/dist/features/engine/run-enforcer.js +35 -0
- package/dist/features/engine/waivers.d.ts +5 -1
- package/dist/features/engine/waivers.js +32 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
package/dist/cli/guardrail.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// install-hooks writes the hooks and points core.hooksPath at them;
|
|
4
4
|
// uninstall removes generated files with the CI warning.
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
|
-
import { renderCustomRegenFragment } from "../features/adapters/index.js";
|
|
6
|
+
import { renderCustomRegenFragment, withCodictatorOwner, } from "../features/adapters/index.js";
|
|
7
7
|
import { ENGINE_VERSION } from "../features/config/index.js";
|
|
8
8
|
import { EXPECTED_HOOKS_PATH, HOOK_NAMES, hookInstallPlan, renderHookScript, } from "../features/integrity/index.js";
|
|
9
9
|
import { err, ok, resolveRepoRoot } from "../kernel/index.js";
|
|
@@ -68,8 +68,12 @@ export async function handleRegen(req) {
|
|
|
68
68
|
tunables: configured.tunables,
|
|
69
69
|
architecture: configured.architecture,
|
|
70
70
|
tools: configured.tools,
|
|
71
|
+
}, req.engineVersion ?? ENGINE_VERSION, root.value);
|
|
72
|
+
// SPEC §4.14.1: custom fragments keep the historical codictator owner.
|
|
73
|
+
const customOwned = withCodictatorOwner(customFiles.value);
|
|
74
|
+
const wrote = await writePlan(root.value, {
|
|
75
|
+
files: [...plan.files, ...customOwned],
|
|
71
76
|
}, req.engineVersion ?? ENGINE_VERSION);
|
|
72
|
-
const wrote = await writePlan(root.value, { files: [...plan.files, ...customFiles.value] }, req.engineVersion ?? ENGINE_VERSION);
|
|
73
77
|
if (!wrote.ok) {
|
|
74
78
|
return fail("CDX-E903", wrote.error.message, wrote.error.remediation, req.json);
|
|
75
79
|
}
|
package/dist/cli/plan.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface GuardrailPlan {
|
|
|
10
10
|
readonly files: ReadonlyArray<{
|
|
11
11
|
path: string;
|
|
12
12
|
content: string;
|
|
13
|
+
owner: string;
|
|
14
|
+
mode: "file";
|
|
13
15
|
}>;
|
|
14
16
|
}
|
|
15
17
|
export interface PlanExtras {
|
|
@@ -18,7 +20,7 @@ export interface PlanExtras {
|
|
|
18
20
|
readonly architecture?: unknown;
|
|
19
21
|
readonly tools?: unknown;
|
|
20
22
|
}
|
|
21
|
-
export declare function guardrailPlan(harnesses: ReadonlyArray<string>, profile: string, repoName: string, existingAgents: string | undefined, extras?: PlanExtras, engineVersion?: string): GuardrailPlan;
|
|
23
|
+
export declare function guardrailPlan(harnesses: ReadonlyArray<string>, profile: string, repoName: string, existingAgents: string | undefined, extras?: PlanExtras, engineVersion?: string, root?: string): GuardrailPlan;
|
|
22
24
|
export declare function writePlan(root: string, plan: GuardrailPlan, engineVersion?: string): Promise<Result<Array<{
|
|
23
25
|
path: string;
|
|
24
26
|
hash: string;
|
package/dist/cli/plan.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPEC §4.14.1 + §4.14.5: guardrail plan builders. The file list (config,
|
|
2
2
|
// steering block, CI workflow, hooks, launcher) and the manifest write are
|
|
3
3
|
// shared by init and regen, so regeneration stays byte-identical.
|
|
4
|
+
import { renderHarnessFiles } from "../features/adapters/index.js";
|
|
4
5
|
import { renderWorkflow } from "../features/ci/index.js";
|
|
5
6
|
import { ENGINE_VERSION } from "../features/config/index.js";
|
|
6
7
|
import { buildManifest, EXPECTED_HOOKS_PATH, HOOK_NAMES, hookInstallPlan, renderHookScript, } from "../features/integrity/index.js";
|
|
@@ -65,30 +66,65 @@ function withManagedBlock(existing) {
|
|
|
65
66
|
const after = end + MANAGED_END.length;
|
|
66
67
|
return `${existing.slice(0, start)}${block}${existing.slice(after)}`;
|
|
67
68
|
}
|
|
68
|
-
export function guardrailPlan(harnesses, profile, repoName, existingAgents, extras = {}, engineVersion = ENGINE_VERSION) {
|
|
69
|
+
export function guardrailPlan(harnesses, profile, repoName, existingAgents, extras = {}, engineVersion = ENGINE_VERSION, root = ".") {
|
|
69
70
|
const files = [];
|
|
70
71
|
files.push({
|
|
71
72
|
path: ".codictator/config.json",
|
|
72
73
|
content: configText(harnesses, profile, extras, engineVersion),
|
|
74
|
+
owner: "codictator",
|
|
75
|
+
mode: "file",
|
|
76
|
+
});
|
|
77
|
+
files.push({
|
|
78
|
+
path: "AGENTS.md",
|
|
79
|
+
content: withManagedBlock(existingAgents),
|
|
80
|
+
owner: "codictator",
|
|
81
|
+
mode: "file",
|
|
73
82
|
});
|
|
74
|
-
files.push({ path: "AGENTS.md", content: withManagedBlock(existingAgents) });
|
|
75
83
|
const workflow = renderWorkflow(repoName);
|
|
76
84
|
files.push({
|
|
77
85
|
path: ".github/workflows/codictator.yml",
|
|
78
86
|
content: workflow.text ?? "",
|
|
87
|
+
owner: "codictator",
|
|
88
|
+
mode: "file",
|
|
79
89
|
});
|
|
80
90
|
for (const hook of HOOK_NAMES) {
|
|
81
91
|
files.push({
|
|
82
92
|
path: `${EXPECTED_HOOKS_PATH}/${hook}`,
|
|
83
93
|
content: renderHookScript(hook),
|
|
94
|
+
owner: "codictator",
|
|
95
|
+
mode: "file",
|
|
84
96
|
});
|
|
85
97
|
}
|
|
86
98
|
const plan = hookInstallPlan({});
|
|
87
99
|
files.push({
|
|
88
100
|
path: `${EXPECTED_HOOKS_PATH}/.gitattributes`,
|
|
89
101
|
content: plan.gitattributes,
|
|
102
|
+
owner: "codictator",
|
|
103
|
+
mode: "file",
|
|
104
|
+
});
|
|
105
|
+
files.push({
|
|
106
|
+
path: ".codictator/bin/run.mjs",
|
|
107
|
+
content: LAUNCHER_TEXT,
|
|
108
|
+
owner: "codictator",
|
|
109
|
+
mode: "file",
|
|
90
110
|
});
|
|
91
|
-
|
|
111
|
+
// SPEC §4.13.1 step 2 + §4.5: every configured built-in harness owns
|
|
112
|
+
// its rendered files; universal renders nothing, custom:* stays with
|
|
113
|
+
// renderCustomHarnessFiles. Renders ignore the input, so the "." default
|
|
114
|
+
// keeps the init path working unchanged. SPEC §4.6.2 + core/guardrail-manifest:
|
|
115
|
+
// one row per path, first occurrence wins: base files go first so the
|
|
116
|
+
// plan's managed AGENTS.md beats any harness-rendered AGENTS.md, then
|
|
117
|
+
// harness files follow in sorted harness-id order, skipping paths
|
|
118
|
+
// already emitted (cursor/gemini-cli AGENTS.md, codex-cli/muse-code
|
|
119
|
+
// .agents/skills overlap), so writePlan writes each path once.
|
|
120
|
+
const seen = new Set(files.map((entry) => entry.path));
|
|
121
|
+
const sortedHarnesses = [...new Set(harnesses)].sort();
|
|
122
|
+
for (const artifact of renderHarnessFiles(sortedHarnesses, root)) {
|
|
123
|
+
if (!seen.has(artifact.path)) {
|
|
124
|
+
seen.add(artifact.path);
|
|
125
|
+
files.push({ ...artifact, mode: "file" });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
92
128
|
return { files };
|
|
93
129
|
}
|
|
94
130
|
export async function writePlan(root, plan, engineVersion = ENGINE_VERSION) {
|
|
@@ -107,12 +143,15 @@ export async function writePlan(root, plan, engineVersion = ENGINE_VERSION) {
|
|
|
107
143
|
hash: sha256Text(normalizeText(file.content)),
|
|
108
144
|
});
|
|
109
145
|
}
|
|
110
|
-
const manifest = buildManifest(engineVersion, written.map((entry) =>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
146
|
+
const manifest = buildManifest(engineVersion, written.map((entry, index) => {
|
|
147
|
+
const planned = plan.files[index];
|
|
148
|
+
return {
|
|
149
|
+
path: entry.path,
|
|
150
|
+
owner: planned === undefined ? "codictator" : planned.owner,
|
|
151
|
+
mode: planned === undefined ? "file" : planned.mode,
|
|
152
|
+
hash: entry.hash,
|
|
153
|
+
};
|
|
154
|
+
}));
|
|
116
155
|
const saved = await ports.value.fs.write(".codictator/manifest.json", encode(`${stableStringify(manifest)}\n`));
|
|
117
156
|
if (!saved.ok) {
|
|
118
157
|
return err(saved.error);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface HarnessFile {
|
|
2
|
+
readonly path: string;
|
|
3
|
+
readonly content: string;
|
|
4
|
+
readonly owner: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function renderHarnessFiles(harnesses: ReadonlyArray<string>, root: string): Array<HarnessFile>;
|
|
7
|
+
export declare function withCodictatorOwner(files: ReadonlyArray<{
|
|
8
|
+
path: string;
|
|
9
|
+
content: string;
|
|
10
|
+
}>): Array<{
|
|
11
|
+
path: string;
|
|
12
|
+
content: string;
|
|
13
|
+
owner: string;
|
|
14
|
+
mode: "file";
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// SPEC §4.5 + §4.13.1 step 2: built-in harness file rendering for the
|
|
2
|
+
// guardrail plan. Each known harness id dispatches to its own adapter's
|
|
3
|
+
// render, so L1 enforcement ships with init and regen; the adapters stay
|
|
4
|
+
// the sole owners of their file shapes (§3.3).
|
|
5
|
+
import { claudeCodeAdapter } from "./claude-code.js";
|
|
6
|
+
import { codexCliAdapter } from "./codex-cli.js";
|
|
7
|
+
import { copilotAdapter } from "./copilot.js";
|
|
8
|
+
import { cursorAdapter } from "./cursor.js";
|
|
9
|
+
import { geminiCliAdapter } from "./gemini-cli.js";
|
|
10
|
+
import { museAdapter } from "./muse-code.js";
|
|
11
|
+
import { opencodeAdapter } from "./opencode.js";
|
|
12
|
+
// SPEC §4.5.8 EC-12: regen input is fixed, so unchanged config reproduces
|
|
13
|
+
// every harness file byte for byte. All built-in renders ignore the input;
|
|
14
|
+
// the cwd is still threaded through honestly for the adapters that read it.
|
|
15
|
+
function regenInput(harness, root) {
|
|
16
|
+
return {
|
|
17
|
+
event: {
|
|
18
|
+
harness,
|
|
19
|
+
event: "session.start",
|
|
20
|
+
trigger: "startup",
|
|
21
|
+
sessionId: "regen",
|
|
22
|
+
cwd: root,
|
|
23
|
+
},
|
|
24
|
+
decision: { decision: "allow", eventId: "regen" },
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// SPEC §4.14.1: every generated artifact carries its adapter owner.
|
|
28
|
+
// Unknown non-custom ids are skipped here because config validation owns
|
|
29
|
+
// CDX-E301 and rejects them there; this render path never emits that error.
|
|
30
|
+
export function renderHarnessFiles(harnesses, root) {
|
|
31
|
+
const ids = [...new Set(harnesses)].sort();
|
|
32
|
+
const files = [];
|
|
33
|
+
for (const harness of ids) {
|
|
34
|
+
if (harness.startsWith("custom:")) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const input = regenInput(harness, root);
|
|
38
|
+
if (harness === museAdapter.id) {
|
|
39
|
+
for (const artifact of museAdapter.render(input)) {
|
|
40
|
+
files.push({ ...artifact, owner: harness });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else if (harness === claudeCodeAdapter.id) {
|
|
44
|
+
for (const artifact of claudeCodeAdapter.render(input)) {
|
|
45
|
+
files.push({ ...artifact, owner: harness });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else if (harness === opencodeAdapter.id) {
|
|
49
|
+
for (const artifact of opencodeAdapter.render(input)) {
|
|
50
|
+
files.push({ ...artifact, owner: harness });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (harness === copilotAdapter.id) {
|
|
54
|
+
for (const artifact of copilotAdapter.render(input)) {
|
|
55
|
+
files.push({ ...artifact, owner: harness });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else if (harness === codexCliAdapter.id) {
|
|
59
|
+
for (const artifact of codexCliAdapter.render(input)) {
|
|
60
|
+
files.push({ ...artifact, owner: harness });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else if (harness === cursorAdapter.id) {
|
|
64
|
+
for (const artifact of cursorAdapter.render(input)) {
|
|
65
|
+
files.push({ ...artifact, owner: harness });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else if (harness === geminiCliAdapter.id) {
|
|
69
|
+
for (const artifact of geminiCliAdapter.render(input)) {
|
|
70
|
+
files.push({ ...artifact, owner: harness });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
75
|
+
return files;
|
|
76
|
+
}
|
|
77
|
+
// SPEC §4.14.1: custom fragments carry no owner of their own, so they keep
|
|
78
|
+
// the historical codictator owner instead of inheriting a harness.
|
|
79
|
+
export function withCodictatorOwner(files) {
|
|
80
|
+
return files.map((entry) => ({
|
|
81
|
+
...entry,
|
|
82
|
+
owner: "codictator",
|
|
83
|
+
mode: "file",
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
@@ -10,6 +10,7 @@ import { CURSOR_CAPABILITIES, cursorAdapter, normalizeCursorToolName, parseCurso
|
|
|
10
10
|
import { checkCustomContract, loadCustomAdapter, renderCustomHarnessFiles, renderCustomRegenFragment } from "./custom.ts";
|
|
11
11
|
import { geminiCliAdapter, parseGeminiEvent, replyGemini, staticBlockGemini } from "./gemini-cli.ts";
|
|
12
12
|
import { GEMINI_CAPABILITIES, normalizeGeminiToolName } from "./gemini-tables.ts";
|
|
13
|
+
import { renderHarnessFiles, withCodictatorOwner } from "./harness-files.ts";
|
|
13
14
|
import { MUSE_CAPABILITIES, museAdapter, normalizeMuseToolName, parseMuseEvent, replyMuse, staticBlockMuse } from "./muse-code.ts";
|
|
14
15
|
import { isOpenCodeVersionSupported, normalizeOpenCodeToolName, OPENCODE_VERSION_MAXIMUM_EXCLUSIVE, OPENCODE_VERSION_MINIMUM, opencodeAdapter, parseOpenCodeEvent, replyOpenCode, staticBlockOpenCode } from "./opencode.ts";
|
|
15
16
|
import { OPENCODE_CAPABILITIES } from "./opencode-capabilities.ts";
|
|
@@ -17,6 +18,6 @@ import { parseUniversalEvent, replyUniversal, staticBlockUniversal, UNIVERSAL_CA
|
|
|
17
18
|
export { hookBudgetMs } from "./hook-dispatch.ts";
|
|
18
19
|
export { type HookAppend, type HookBasePorts, type HookEnforcerLoader, type HookRunRequest, runHookEvent, staticBlock, supportedHarness, } from "./runtime.ts";
|
|
19
20
|
export type { Adapter, CapabilityDeclaration, Decision, FileArtifact, HookDecision, HookEvent, HookReply, HookToolCall, RenderInput, ToolKind, Trigger, };
|
|
20
|
-
export { CLAUDE_CODE_CAPABILITIES, CODEX_CAPABILITIES, COPILOT_CAPABILITIES, COPILOT_TIMEOUT_SEC, CURSOR_CAPABILITIES, checkCustomContract, claudeCodeAdapter, codexCliAdapter, copilotAdapter, cursorAdapter, GEMINI_CAPABILITIES, geminiCliAdapter, HOOK_PAYLOAD_MAX_BYTES, isOpenCodeVersionSupported, loadCustomAdapter, MUSE_CAPABILITIES, museAdapter, normalizeClaudeCodeToolName, normalizeCodexToolName, normalizeCopilotToolName, normalizeCursorToolName, normalizeGeminiToolName, normalizeMuseToolName, normalizeOpenCodeToolName, OPENCODE_CAPABILITIES, OPENCODE_VERSION_MAXIMUM_EXCLUSIVE, OPENCODE_VERSION_MINIMUM, opencodeAdapter, parseClaudeCodeEvent, parseCodexEvent, parseCopilotEvent, parseCursorEvent, parseGeminiEvent, parseMuseEvent, parseOpenCodeEvent, parseUniversalEvent, renderCustomHarnessFiles, renderCustomRegenFragment, replyClaudeCode, replyCodex, replyCopilot, replyCursor, replyGemini, replyMuse, replyOpenCode, replyUniversal, staticBlockClaudeCode, staticBlockCodex, staticBlockCopilot, staticBlockCursor, staticBlockGemini, staticBlockMuse, staticBlockOpenCode, staticBlockUniversal, UNIVERSAL_CAPABILITIES, universalAdapter, };
|
|
21
|
+
export { CLAUDE_CODE_CAPABILITIES, CODEX_CAPABILITIES, COPILOT_CAPABILITIES, COPILOT_TIMEOUT_SEC, CURSOR_CAPABILITIES, checkCustomContract, claudeCodeAdapter, codexCliAdapter, copilotAdapter, cursorAdapter, GEMINI_CAPABILITIES, geminiCliAdapter, HOOK_PAYLOAD_MAX_BYTES, isOpenCodeVersionSupported, loadCustomAdapter, MUSE_CAPABILITIES, museAdapter, normalizeClaudeCodeToolName, normalizeCodexToolName, normalizeCopilotToolName, normalizeCursorToolName, normalizeGeminiToolName, normalizeMuseToolName, normalizeOpenCodeToolName, OPENCODE_CAPABILITIES, OPENCODE_VERSION_MAXIMUM_EXCLUSIVE, OPENCODE_VERSION_MINIMUM, opencodeAdapter, parseClaudeCodeEvent, parseCodexEvent, parseCopilotEvent, parseCursorEvent, parseGeminiEvent, parseMuseEvent, parseOpenCodeEvent, parseUniversalEvent, renderCustomHarnessFiles, renderCustomRegenFragment, renderHarnessFiles, replyClaudeCode, replyCodex, replyCopilot, replyCursor, replyGemini, replyMuse, replyOpenCode, replyUniversal, staticBlockClaudeCode, staticBlockCodex, staticBlockCopilot, staticBlockCursor, staticBlockGemini, staticBlockMuse, staticBlockOpenCode, staticBlockUniversal, UNIVERSAL_CAPABILITIES, universalAdapter, withCodictatorOwner, };
|
|
21
22
|
export declare function parseEvent(harness: string, event: string, raw: unknown): Result<HookEvent>;
|
|
22
23
|
export declare function renderReply(harness: string, decision: HookDecision): HookReply;
|
|
@@ -8,13 +8,14 @@ import { CURSOR_CAPABILITIES, cursorAdapter, normalizeCursorToolName, parseCurso
|
|
|
8
8
|
import { checkCustomContract, loadCustomAdapter, renderCustomHarnessFiles, renderCustomRegenFragment, } from "./custom.js";
|
|
9
9
|
import { geminiCliAdapter, parseGeminiEvent, replyGemini, staticBlockGemini, } from "./gemini-cli.js";
|
|
10
10
|
import { GEMINI_CAPABILITIES, normalizeGeminiToolName, } from "./gemini-tables.js";
|
|
11
|
+
import { renderHarnessFiles, withCodictatorOwner } from "./harness-files.js";
|
|
11
12
|
import { MUSE_CAPABILITIES, museAdapter, normalizeMuseToolName, parseMuseEvent, replyMuse, staticBlockMuse, } from "./muse-code.js";
|
|
12
13
|
import { isOpenCodeVersionSupported, normalizeOpenCodeToolName, OPENCODE_VERSION_MAXIMUM_EXCLUSIVE, OPENCODE_VERSION_MINIMUM, opencodeAdapter, parseOpenCodeEvent, replyOpenCode, staticBlockOpenCode, } from "./opencode.js";
|
|
13
14
|
import { OPENCODE_CAPABILITIES } from "./opencode-capabilities.js";
|
|
14
15
|
import { parseUniversalEvent, replyUniversal, staticBlockUniversal, UNIVERSAL_CAPABILITIES, universalAdapter, } from "./universal.js";
|
|
15
16
|
export { hookBudgetMs } from "./hook-dispatch.js";
|
|
16
17
|
export { runHookEvent, staticBlock, supportedHarness, } from "./runtime.js";
|
|
17
|
-
export { CLAUDE_CODE_CAPABILITIES, CODEX_CAPABILITIES, COPILOT_CAPABILITIES, COPILOT_TIMEOUT_SEC, CURSOR_CAPABILITIES, checkCustomContract, claudeCodeAdapter, codexCliAdapter, copilotAdapter, cursorAdapter, GEMINI_CAPABILITIES, geminiCliAdapter, HOOK_PAYLOAD_MAX_BYTES, isOpenCodeVersionSupported, loadCustomAdapter, MUSE_CAPABILITIES, museAdapter, normalizeClaudeCodeToolName, normalizeCodexToolName, normalizeCopilotToolName, normalizeCursorToolName, normalizeGeminiToolName, normalizeMuseToolName, normalizeOpenCodeToolName, OPENCODE_CAPABILITIES, OPENCODE_VERSION_MAXIMUM_EXCLUSIVE, OPENCODE_VERSION_MINIMUM, opencodeAdapter, parseClaudeCodeEvent, parseCodexEvent, parseCopilotEvent, parseCursorEvent, parseGeminiEvent, parseMuseEvent, parseOpenCodeEvent, parseUniversalEvent, renderCustomHarnessFiles, renderCustomRegenFragment, replyClaudeCode, replyCodex, replyCopilot, replyCursor, replyGemini, replyMuse, replyOpenCode, replyUniversal, staticBlockClaudeCode, staticBlockCodex, staticBlockCopilot, staticBlockCursor, staticBlockGemini, staticBlockMuse, staticBlockOpenCode, staticBlockUniversal, UNIVERSAL_CAPABILITIES, universalAdapter, };
|
|
18
|
+
export { CLAUDE_CODE_CAPABILITIES, CODEX_CAPABILITIES, COPILOT_CAPABILITIES, COPILOT_TIMEOUT_SEC, CURSOR_CAPABILITIES, checkCustomContract, claudeCodeAdapter, codexCliAdapter, copilotAdapter, cursorAdapter, GEMINI_CAPABILITIES, geminiCliAdapter, HOOK_PAYLOAD_MAX_BYTES, isOpenCodeVersionSupported, loadCustomAdapter, MUSE_CAPABILITIES, museAdapter, normalizeClaudeCodeToolName, normalizeCodexToolName, normalizeCopilotToolName, normalizeCursorToolName, normalizeGeminiToolName, normalizeMuseToolName, normalizeOpenCodeToolName, OPENCODE_CAPABILITIES, OPENCODE_VERSION_MAXIMUM_EXCLUSIVE, OPENCODE_VERSION_MINIMUM, opencodeAdapter, parseClaudeCodeEvent, parseCodexEvent, parseCopilotEvent, parseCursorEvent, parseGeminiEvent, parseMuseEvent, parseOpenCodeEvent, parseUniversalEvent, renderCustomHarnessFiles, renderCustomRegenFragment, renderHarnessFiles, replyClaudeCode, replyCodex, replyCopilot, replyCursor, replyGemini, replyMuse, replyOpenCode, replyUniversal, staticBlockClaudeCode, staticBlockCodex, staticBlockCopilot, staticBlockCursor, staticBlockGemini, staticBlockMuse, staticBlockOpenCode, staticBlockUniversal, UNIVERSAL_CAPABILITIES, universalAdapter, withCodictatorOwner, };
|
|
18
19
|
// SPEC §4.5.3: native harness payload to the normalised event. Payloads
|
|
19
20
|
// over the 8 MiB cap block CDX-E305 here too (§4.5 EC-11), so the parse
|
|
20
21
|
// layer enforces the same limit the runtime stdin read enforces.
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
// The clock arrives only through CheckContext.
|
|
7
7
|
import { ok, sha256Text, stableStringify, } from "../../kernel/index.js";
|
|
8
8
|
import { buildMessage } from "./message.js";
|
|
9
|
-
import { appendFailedFinding,
|
|
10
|
-
import { DEFAULT_WAIVER_STORE,
|
|
9
|
+
import { appendFailedFinding, collectFindings } from "./run-enforcer.js";
|
|
10
|
+
import { DEFAULT_WAIVER_STORE, } from "./waivers.js";
|
|
11
11
|
// SPEC §4.3.2: each engine trigger evaluates at one layer. Coarse triggers
|
|
12
12
|
// map to the layer of their first scope; finer hook names arrive with the
|
|
13
13
|
// adapter contract (phase 5).
|
|
@@ -52,38 +52,6 @@ function makeVerdictId(now, trigger, scope, decision, findings, waiversApplied,
|
|
|
52
52
|
});
|
|
53
53
|
return `${stamp}-${sha256Text(canonical).slice(0, 12)}`;
|
|
54
54
|
}
|
|
55
|
-
// SPEC §4.3.4: suppress findings covered by an active waiver, recording
|
|
56
|
-
// the waiver ids. Synthetic fail-closed findings (CDX-E*) never suppress,
|
|
57
|
-
// and an invalid floor-rule waiver blocks loudly instead of suppressing.
|
|
58
|
-
async function applyWaivers(findings, store, now) {
|
|
59
|
-
const kept = [];
|
|
60
|
-
const applied = new Set();
|
|
61
|
-
for (const finding of findings) {
|
|
62
|
-
if (finding.code?.startsWith("CDX-E") === true) {
|
|
63
|
-
kept.push(finding);
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
const matched = matchWaivers(store, finding.ruleId, finding.path ?? "", now);
|
|
67
|
-
if (!matched.ok) {
|
|
68
|
-
kept.push(finding);
|
|
69
|
-
kept.push({
|
|
70
|
-
ruleId: finding.ruleId,
|
|
71
|
-
code: matched.error.code,
|
|
72
|
-
message: matched.error.message,
|
|
73
|
-
remediation: matched.error.remediation,
|
|
74
|
-
});
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
if (matched.value.length === 0) {
|
|
78
|
-
kept.push(finding);
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
for (const waiver of matched.value) {
|
|
82
|
-
applied.add(waiver.id);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return { kept, waiversApplied: [...applied].sort() };
|
|
86
|
-
}
|
|
87
55
|
function ruleIdsOf(findings) {
|
|
88
56
|
const seen = new Set();
|
|
89
57
|
for (const finding of findings) {
|
|
@@ -91,22 +59,18 @@ function ruleIdsOf(findings) {
|
|
|
91
59
|
}
|
|
92
60
|
return [...seen].sort();
|
|
93
61
|
}
|
|
94
|
-
async function collectFindings(ordered, ctx, store) {
|
|
95
|
-
const runnable = ordered.filter((enforcer) => enforcer.kinds.includes(ctx.trigger));
|
|
96
|
-
// SPEC §4.3.3 step 2: bounded concurrency — one guard at a time keeps every
|
|
97
|
-
// guard's event-loop share (no E902 herd); sortFindings keeps the verdict byte-identical (§4.3.6).
|
|
98
|
-
const ran = [];
|
|
99
|
-
for (const enforcer of runnable) {
|
|
100
|
-
ran.push(await runEnforcer(enforcer, ctx));
|
|
101
|
-
}
|
|
102
|
-
return applyWaivers(sortFindings(ran.flat()), store, ctx.now);
|
|
103
|
-
}
|
|
104
62
|
function decideDecision(kept, trigger) {
|
|
105
63
|
if (kept.length === 0) {
|
|
106
64
|
return "allow";
|
|
107
65
|
}
|
|
108
66
|
return ADVISORY_TRIGGERS.includes(trigger) ? "advise" : "block";
|
|
109
67
|
}
|
|
68
|
+
// SPEC §4.3.8 + §4.3.9 EC-19: zero effective enforcers block, except the
|
|
69
|
+
// never-gating L2 layer, which advises. No engine trigger maps to L2 yet
|
|
70
|
+
// (see TRIGGER_LAYERS), so every trigger blocks until that phase lands.
|
|
71
|
+
function decideZeroEnforcer(trigger) {
|
|
72
|
+
return TRIGGER_LAYERS[trigger] === "L2" ? "advise" : "block";
|
|
73
|
+
}
|
|
110
74
|
function buildVerdictEvent(trigger, now, decision, kept, waiversApplied, configHash, packs, message, clockSkew) {
|
|
111
75
|
return {
|
|
112
76
|
kind: "verdict",
|
|
@@ -135,6 +99,28 @@ function blockedVerdict(now, trigger, scope, configHash) {
|
|
|
135
99
|
waiversApplied: [],
|
|
136
100
|
};
|
|
137
101
|
}
|
|
102
|
+
// SPEC §4.3.3 steps 4-6: decide the verdict from the collected findings,
|
|
103
|
+
// build the agent message, append the verdict event before returning, and
|
|
104
|
+
// fail closed when the append fails (PRD §6 P5).
|
|
105
|
+
async function finishEvaluation(input, append) {
|
|
106
|
+
const decision = input.zeroEnforcers
|
|
107
|
+
? decideZeroEnforcer(input.trigger)
|
|
108
|
+
: decideDecision(input.kept, input.trigger);
|
|
109
|
+
const eventId = makeVerdictId(input.now, input.trigger, input.scope, decision, input.kept, input.waiversApplied, input.configHash);
|
|
110
|
+
const message = buildMessage(decision, input.kept, eventId);
|
|
111
|
+
const event = buildVerdictEvent(input.trigger, input.now, decision, input.kept, input.waiversApplied, input.configHash, input.packs, message, input.clockSkew);
|
|
112
|
+
const appended = await append(event);
|
|
113
|
+
if (!appended.ok) {
|
|
114
|
+
return blockedVerdict(input.now, input.trigger, input.scope, input.configHash);
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
decision,
|
|
118
|
+
findings: input.kept,
|
|
119
|
+
message,
|
|
120
|
+
eventId,
|
|
121
|
+
waiversApplied: input.waiversApplied,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
138
124
|
// SPEC §4.3.3: pure evaluation over registered enforcers.
|
|
139
125
|
export function createEngine(enforcers, options = {}) {
|
|
140
126
|
const ordered = [...enforcers].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
@@ -155,16 +141,18 @@ export function createEngine(enforcers, options = {}) {
|
|
|
155
141
|
lastNow = now;
|
|
156
142
|
}
|
|
157
143
|
const ctx = { trigger, scope, now, configHash };
|
|
158
|
-
const { kept, waiversApplied } = await collectFindings(ordered, ctx, store);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
144
|
+
const { kept, waiversApplied, zeroEnforcers } = await collectFindings(ordered, ctx, store);
|
|
145
|
+
return finishEvaluation({
|
|
146
|
+
trigger,
|
|
147
|
+
scope,
|
|
148
|
+
now,
|
|
149
|
+
configHash,
|
|
150
|
+
packs,
|
|
151
|
+
kept,
|
|
152
|
+
waiversApplied,
|
|
153
|
+
zeroEnforcers,
|
|
154
|
+
clockSkew,
|
|
155
|
+
}, append);
|
|
168
156
|
},
|
|
169
157
|
};
|
|
170
158
|
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { CheckContext, Enforcer, Finding } from "./index.ts";
|
|
2
|
+
import { type WaiverStore } from "./waivers.ts";
|
|
2
3
|
export declare function runEnforcer(enforcer: Enforcer, ctx: CheckContext): Promise<Array<Finding>>;
|
|
3
4
|
export declare function sortFindings(findings: Array<Finding>): Array<Finding>;
|
|
4
5
|
export declare function appendFailedFinding(): Finding;
|
|
6
|
+
export declare function zeroEnforcerFinding(): Finding;
|
|
7
|
+
export declare function collectFindings(ordered: ReadonlyArray<Enforcer>, ctx: CheckContext, store: WaiverStore): Promise<{
|
|
8
|
+
kept: Array<Finding>;
|
|
9
|
+
waiversApplied: Array<string>;
|
|
10
|
+
zeroEnforcers: boolean;
|
|
11
|
+
}>;
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { applyWaivers } from "./waivers.js";
|
|
2
|
+
// SPEC §4.3.9 EC-1: the feedback-only triggers (tool.post.write and stop
|
|
3
|
+
// both arrive here as post-tool) allow with the event recorded when no
|
|
4
|
+
// enforcer runs; every other trigger with zero enforcers fails closed.
|
|
5
|
+
const FEEDBACK_ONLY_TRIGGERS = ["post-tool"];
|
|
1
6
|
function failFinding(enforcerId, code, detail, remediation) {
|
|
2
7
|
return {
|
|
3
8
|
ruleId: enforcerId,
|
|
@@ -80,3 +85,33 @@ function compareFindings(a, b) {
|
|
|
80
85
|
export function appendFailedFinding() {
|
|
81
86
|
return failFinding("core/engine", "CDX-E801", "could not record its verdict event (CDX-E801)", "Free disk space and fix permissions on .codictator/state/, then retry.");
|
|
82
87
|
}
|
|
88
|
+
// SPEC §4.3.8: zero effective enforcers fail closed with a synthetic CDX-E904
|
|
89
|
+
// finding. No guard ran, so the finding names the engine, not a rule.
|
|
90
|
+
export function zeroEnforcerFinding() {
|
|
91
|
+
return failFinding("core/engine", "CDX-E904", "has no effective enforcer for this trigger (CDX-E904)", "Configure a pack with a rule for this trigger, then run codictator verify.");
|
|
92
|
+
}
|
|
93
|
+
// SPEC §4.3.3 steps 1-3: run the trigger's enforcers, then apply waivers to
|
|
94
|
+
// the findings. Zero effective enforcers fail closed with the synthetic
|
|
95
|
+
// CDX-E904 finding, except the feedback-only triggers, which allow with the
|
|
96
|
+
// event recorded (§4.3.9 EC-1/EC-19).
|
|
97
|
+
export async function collectFindings(ordered, ctx, store) {
|
|
98
|
+
const runnable = ordered.filter((enforcer) => enforcer.kinds.includes(ctx.trigger));
|
|
99
|
+
if (runnable.length === 0) {
|
|
100
|
+
if (FEEDBACK_ONLY_TRIGGERS.includes(ctx.trigger)) {
|
|
101
|
+
return { kept: [], waiversApplied: [], zeroEnforcers: false };
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
kept: [zeroEnforcerFinding()],
|
|
105
|
+
waiversApplied: [],
|
|
106
|
+
zeroEnforcers: true,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// SPEC §4.3.3 step 2: bounded concurrency — one guard at a time keeps every
|
|
110
|
+
// guard's event-loop share (no E902 herd); sortFindings keeps the verdict byte-identical (§4.3.6).
|
|
111
|
+
const ran = [];
|
|
112
|
+
for (const enforcer of runnable) {
|
|
113
|
+
ran.push(await runEnforcer(enforcer, ctx));
|
|
114
|
+
}
|
|
115
|
+
const { kept, waiversApplied } = await applyWaivers(sortFindings(ran.flat()), store, ctx.now);
|
|
116
|
+
return { kept, waiversApplied, zeroEnforcers: false };
|
|
117
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Instant } from "../../kernel/index.ts";
|
|
1
|
+
import type { Finding, Instant } from "../../kernel/index.ts";
|
|
2
2
|
import { type Result } from "../../kernel/index.ts";
|
|
3
3
|
export interface Waiver {
|
|
4
4
|
readonly id: string;
|
|
@@ -21,3 +21,7 @@ export declare const MAX_WAIVER_DAYS = 90;
|
|
|
21
21
|
export declare const DEFAULT_WAIVER_STORE: WaiverStore;
|
|
22
22
|
export declare function matchWaivers(store: WaiverStore, ruleId: string, path: string, now: Instant): Result<ReadonlyArray<Waiver>>;
|
|
23
23
|
export declare function isWaived(ruleId: string, path: string, now: Instant, store?: WaiverStore): Promise<Result<boolean>>;
|
|
24
|
+
export declare function applyWaivers(findings: ReadonlyArray<Finding>, store: WaiverStore, now: Instant): Promise<{
|
|
25
|
+
kept: Array<Finding>;
|
|
26
|
+
waiversApplied: Array<string>;
|
|
27
|
+
}>;
|
|
@@ -167,3 +167,35 @@ export function isWaived(ruleId, path, now, store = DEFAULT_WAIVER_STORE) {
|
|
|
167
167
|
}
|
|
168
168
|
return Promise.resolve(ok(matched.value.length > 0));
|
|
169
169
|
}
|
|
170
|
+
// SPEC §4.3.4: suppress findings covered by an active waiver, recording
|
|
171
|
+
// the waiver ids. Synthetic fail-closed findings (CDX-E*) never suppress,
|
|
172
|
+
// and an invalid floor-rule waiver blocks loudly instead of suppressing.
|
|
173
|
+
export async function applyWaivers(findings, store, now) {
|
|
174
|
+
const kept = [];
|
|
175
|
+
const applied = new Set();
|
|
176
|
+
for (const finding of findings) {
|
|
177
|
+
if (finding.code?.startsWith("CDX-E") === true) {
|
|
178
|
+
kept.push(finding);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const matched = matchWaivers(store, finding.ruleId, finding.path ?? "", now);
|
|
182
|
+
if (!matched.ok) {
|
|
183
|
+
kept.push(finding);
|
|
184
|
+
kept.push({
|
|
185
|
+
ruleId: finding.ruleId,
|
|
186
|
+
code: matched.error.code,
|
|
187
|
+
message: matched.error.message,
|
|
188
|
+
remediation: matched.error.remediation,
|
|
189
|
+
});
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (matched.value.length === 0) {
|
|
193
|
+
kept.push(finding);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
for (const waiver of matched.value) {
|
|
197
|
+
applied.add(waiver.id);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { kept, waiversApplied: [...applied].sort() };
|
|
201
|
+
}
|