codictator 0.1.3 → 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/commands.d.ts +1 -1
- package/dist/cli/commands.js +3 -1
- package/dist/cli/guardrail.d.ts +2 -0
- package/dist/cli/guardrail.js +17 -15
- package/dist/cli/init-plan.d.ts +7 -0
- package/dist/cli/init-plan.js +21 -2
- package/dist/cli/init.d.ts +2 -0
- package/dist/cli/init.js +8 -17
- package/dist/cli/plan.d.ts +3 -1
- package/dist/cli/plan.js +48 -9
- package/dist/cli/serve-run.js +1 -0
- package/dist/cli/setup-gitignore.d.ts +7 -0
- package/dist/cli/setup-gitignore.js +41 -0
- package/dist/cli/setup-handoff.d.ts +7 -0
- package/dist/cli/setup-handoff.js +44 -0
- package/dist/cli/setup-install.d.ts +11 -0
- package/dist/cli/setup-install.js +84 -0
- package/dist/cli/setup.d.ts +9 -0
- package/dist/cli/setup.js +143 -0
- 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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// SPEC §4.19.6: one-shot adoption. Flags mirror init (§4.19.2); the run
|
|
2
|
+
// is the init path (or regen over an existing config) plus install, hooks
|
|
3
|
+
// and verify, ending at the extension handoff (§4.20.1). Every step
|
|
4
|
+
// delegates to the existing init, guardrail and verify handlers; no step
|
|
5
|
+
// logic is duplicated here.
|
|
6
|
+
import { ENGINE_VERSION } from "../features/config/index.js";
|
|
7
|
+
import { err, ok } from "../kernel/index.js";
|
|
8
|
+
import { configureHooksPath, handleRegen, writeHookFiles, } from "./guardrail.js";
|
|
9
|
+
import { parseInitFlags } from "./init.js";
|
|
10
|
+
import { defaultHarnesses, initPreamble, runInitSequence, } from "./init-plan.js";
|
|
11
|
+
import { handleVerify } from "./inspect.js";
|
|
12
|
+
import { setupGitignoreStep } from "./setup-gitignore.js";
|
|
13
|
+
import { hasEffectivePacks, SETUP_EXTENSION_ID, SETUP_EXTENSION_INSTALL, setupHandoffLines, } from "./setup-handoff.js";
|
|
14
|
+
import { setupInstallHere, setupInstallLayout } from "./setup-install.js";
|
|
15
|
+
import { fail, MIN_NODE_MAJOR, nodeMajor, nodeTooOldMessage, printHuman, printJson, readStateText, usageError, } from "./shared.js";
|
|
16
|
+
export const SETUP_USAGE = "usage: codictator setup [--harness <id> ...] [--profile <app|library|cli|service>] [--pack <id> ...] [--tool <role>=<command>@<pin> ...] [--repair]";
|
|
17
|
+
// SPEC §4.19.6 step 1: the Node floor, the human gate and the repo root.
|
|
18
|
+
async function setupPreflight(req) {
|
|
19
|
+
const version = req.nodeVersion ?? process.version;
|
|
20
|
+
if (nodeMajor(version) < MIN_NODE_MAJOR) {
|
|
21
|
+
return fail("CDX-E002", nodeTooOldMessage(version), "Install a newer Node, then retry the tool call.", req.json);
|
|
22
|
+
}
|
|
23
|
+
return initPreamble(req);
|
|
24
|
+
}
|
|
25
|
+
// SPEC §4.19.6 step 2: the adoption path. An existing config without
|
|
26
|
+
// --repair re-renders from config; otherwise the init path runs.
|
|
27
|
+
async function setupRegenPath(req, steps) {
|
|
28
|
+
const regen = await handleRegen({ ...req, args: [] });
|
|
29
|
+
if (!regen.ok) {
|
|
30
|
+
return regen;
|
|
31
|
+
}
|
|
32
|
+
steps.push({ step: "init", ok: true, detail: { mode: "regen" } });
|
|
33
|
+
printHuman("setup: guardrails regenerated from the existing config", req.quiet);
|
|
34
|
+
return ok(0);
|
|
35
|
+
}
|
|
36
|
+
async function setupInitPath(req, root, flags, steps) {
|
|
37
|
+
const engineVersion = req.engineVersion ?? ENGINE_VERSION;
|
|
38
|
+
const sequenced = await runInitSequence(root, req, flags, defaultHarnesses(flags), engineVersion);
|
|
39
|
+
if (!sequenced.ok) {
|
|
40
|
+
return sequenced;
|
|
41
|
+
}
|
|
42
|
+
steps.push({
|
|
43
|
+
step: "init",
|
|
44
|
+
ok: true,
|
|
45
|
+
detail: {
|
|
46
|
+
mode: flags.repair ? "repair" : "fresh",
|
|
47
|
+
files: sequenced.value.written.map((entry) => entry.path),
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
printHuman("setup: guardrails written from the init path", req.quiet);
|
|
51
|
+
return ok(0);
|
|
52
|
+
}
|
|
53
|
+
async function setupAdopt(req, root, flags, steps) {
|
|
54
|
+
const existing = await readStateText(root, ".codictator/config.json");
|
|
55
|
+
if (existing !== undefined && !flags.repair) {
|
|
56
|
+
return setupRegenPath(req, steps);
|
|
57
|
+
}
|
|
58
|
+
return setupInitPath(req, root, flags, steps);
|
|
59
|
+
}
|
|
60
|
+
// SPEC §4.19.6 step 6: hooks via the install-hooks writers.
|
|
61
|
+
async function setupHooksStep(req, root, steps) {
|
|
62
|
+
const written = await writeHookFiles(root);
|
|
63
|
+
if (!written.ok) {
|
|
64
|
+
return fail("CDX-E903", written.error.message, written.error.remediation, req.json);
|
|
65
|
+
}
|
|
66
|
+
await configureHooksPath(root, req);
|
|
67
|
+
steps.push({ step: "hooks", ok: true });
|
|
68
|
+
printHuman("setup: hooks installed", req.quiet);
|
|
69
|
+
return ok(0);
|
|
70
|
+
}
|
|
71
|
+
// SPEC §4.19.6 step 7: the first evaluation over the adopted repo. A
|
|
72
|
+
// blocking verdict stops the run with verify's own mapping (exit 1).
|
|
73
|
+
async function setupVerifyStep(req, steps) {
|
|
74
|
+
const verdict = await handleVerify({ ...req, args: [] });
|
|
75
|
+
if (!verdict.ok || verdict.value !== 0) {
|
|
76
|
+
return verdict;
|
|
77
|
+
}
|
|
78
|
+
steps.push({ step: "verify", ok: true });
|
|
79
|
+
return ok(0);
|
|
80
|
+
}
|
|
81
|
+
// SPEC §4.19.6 step 8 + §4.20.1: the views connect once a config exists;
|
|
82
|
+
// the human installs the extension with the printed command.
|
|
83
|
+
function setupHandoff(req, root, steps, emptyPacks) {
|
|
84
|
+
steps.push({
|
|
85
|
+
step: "handoff",
|
|
86
|
+
ok: true,
|
|
87
|
+
detail: { extension: SETUP_EXTENSION_ID },
|
|
88
|
+
});
|
|
89
|
+
if (req.json) {
|
|
90
|
+
printJson({
|
|
91
|
+
setup: true,
|
|
92
|
+
root,
|
|
93
|
+
steps,
|
|
94
|
+
extension: { id: SETUP_EXTENSION_ID, install: SETUP_EXTENSION_INSTALL },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
for (const line of setupHandoffLines(emptyPacks)) {
|
|
99
|
+
printHuman(line, req.quiet);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return ok(0);
|
|
103
|
+
}
|
|
104
|
+
// SPEC §4.19.6: the eight adoption steps in order; a step failure stops
|
|
105
|
+
// the run with that step's §8.2 mapping.
|
|
106
|
+
export async function handleSetup(req) {
|
|
107
|
+
const flags = parseInitFlags(req.args);
|
|
108
|
+
if (flags === undefined) {
|
|
109
|
+
return usageError(SETUP_USAGE);
|
|
110
|
+
}
|
|
111
|
+
const preflight = await setupPreflight(req);
|
|
112
|
+
if (typeof preflight !== "string") {
|
|
113
|
+
return preflight;
|
|
114
|
+
}
|
|
115
|
+
const root = preflight;
|
|
116
|
+
const steps = [{ step: "preflight", ok: true }];
|
|
117
|
+
const adopted = await setupAdopt(req, root, flags, steps);
|
|
118
|
+
if (!adopted.ok) {
|
|
119
|
+
return adopted;
|
|
120
|
+
}
|
|
121
|
+
const ignored = await setupGitignoreStep(req, root, steps);
|
|
122
|
+
if (!ignored.ok) {
|
|
123
|
+
return ignored;
|
|
124
|
+
}
|
|
125
|
+
const layout = await setupInstallLayout(req, root, steps);
|
|
126
|
+
if (!layout.ok) {
|
|
127
|
+
return err(layout.error);
|
|
128
|
+
}
|
|
129
|
+
const installed = await setupInstallHere(req, root, layout.value, steps);
|
|
130
|
+
if (!installed.ok) {
|
|
131
|
+
return installed;
|
|
132
|
+
}
|
|
133
|
+
const hooked = await setupHooksStep(req, root, steps);
|
|
134
|
+
if (!hooked.ok) {
|
|
135
|
+
return hooked;
|
|
136
|
+
}
|
|
137
|
+
const verified = await setupVerifyStep(req, steps);
|
|
138
|
+
if (!verified.ok) {
|
|
139
|
+
return verified;
|
|
140
|
+
}
|
|
141
|
+
const emptyPacks = !(await hasEffectivePacks(root, flags));
|
|
142
|
+
return setupHandoff(req, root, steps, emptyPacks);
|
|
143
|
+
}
|
|
@@ -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
|
+
}
|