auto-mode 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -10
- package/dist/cli.js +222 -11
- package/dist/index.d.ts +22 -2
- package/dist/index.js +1 -1
- package/dist/{render-verdict-9pnm4JBS.js → render-verdict-DePMDXKz.js} +325 -83
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,14 +31,14 @@ Create the classifier configuration:
|
|
|
31
31
|
|
|
32
32
|
```json
|
|
33
33
|
{
|
|
34
|
-
"
|
|
35
|
-
"
|
|
34
|
+
"classifiers": { "jev": { "apiKeyEnv": "TYPESAFE_API_KEY" } },
|
|
35
|
+
"decision": { "classifier": "jev" }
|
|
36
36
|
}
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
Save it as `~/.config/auto-mode/config.json` and export your TypeSafe API key, or set
|
|
40
|
-
`
|
|
41
|
-
|
|
39
|
+
Save it as `~/.config/auto-mode/config.json` and export your TypeSafe API key, or set the entry's
|
|
40
|
+
`apiKeyCommand` to a command that prints it. [Configuration](./docs/guides/configuration.md) covers
|
|
41
|
+
the registries, credentials, and migrating an older file.
|
|
42
42
|
|
|
43
43
|
## Standing permissions
|
|
44
44
|
|
|
@@ -63,9 +63,10 @@ approval when every rule confidently allows the action, and a denial for uncerta
|
|
|
63
63
|
denial names its rule, the harm, and what clears it, and the agent continues on another path. The
|
|
64
64
|
confidence threshold is configurable and needs evaluation against your actions.
|
|
65
65
|
|
|
66
|
-
A missing key, failed API call, malformed response, or oversized request follows
|
|
67
|
-
default `defer` writes no verdict and keeps the prompt; `deny` fails
|
|
68
|
-
diagnostic to stderr. auto-mode never truncates a Jev action to make it
|
|
66
|
+
A missing key, failed API call, malformed response, or oversized request follows
|
|
67
|
+
`decision.onFailure`. The default `defer` writes no verdict and keeps the prompt; `deny` fails
|
|
68
|
+
closed. Failures write a diagnostic to stderr. auto-mode never truncates a Jev action to make it
|
|
69
|
+
fit.
|
|
69
70
|
|
|
70
71
|
## Commands
|
|
71
72
|
|
|
@@ -76,10 +77,11 @@ diagnostic to stderr. auto-mode never truncates a Jev action to make it fit.
|
|
|
76
77
|
| `auto-mode run --local-only` | Skip the model tier |
|
|
77
78
|
| `auto-mode run --jev-only` | Require Jev with a 5-second API timeout |
|
|
78
79
|
| `auto-mode print-prompt` | Print the selected provider's base policy |
|
|
80
|
+
| `auto-mode config migrate` | Rewrite an older config file in place |
|
|
79
81
|
|
|
80
82
|
The mod evaluates only ask decisions and preserves existing allow and deny decisions. It runs the
|
|
81
|
-
CLI as a bounded child process with `--jev-only`. The `spark`, `claude`, and `
|
|
82
|
-
Messages API and serve library callers.
|
|
83
|
+
CLI as a bounded child process with `--jev-only`. The `spark`, `claude`, `glm`, and `messages`
|
|
84
|
+
classifier kinds use the Messages API and serve library callers.
|
|
83
85
|
|
|
84
86
|
## Library
|
|
85
87
|
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,147 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as parseActionRequest, r as classifyAction, s as loadPolicy, t as renderVerdict, u as loadConfig } from "./render-verdict-
|
|
3
|
-
import { appendFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { _ as string, f as resolveConfigPath, g as strictObject, h as number, l as DEFAULT_DENIAL_BUDGET, n as parseActionRequest, p as normalizeConfigFile, r as classifyAction, s as loadPolicy, t as renderVerdict, u as loadConfig } from "./render-verdict-DePMDXKz.js";
|
|
3
|
+
import { appendFile, constants, copyFile, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
7
|
import { text } from "node:stream/consumers";
|
|
8
8
|
import { parseArgs } from "node:util";
|
|
9
|
+
//#region src/budget/build-retry-key.ts
|
|
10
|
+
const DESCRIPTIVE_FIELDS = { Bash: [
|
|
11
|
+
"description",
|
|
12
|
+
"timeout",
|
|
13
|
+
"run_in_background"
|
|
14
|
+
] };
|
|
15
|
+
function buildRetryKey(request) {
|
|
16
|
+
const ignored = Object.hasOwn(DESCRIPTIVE_FIELDS, request.toolName) ? DESCRIPTIVE_FIELDS[request.toolName] ?? [] : [];
|
|
17
|
+
const input = Object.fromEntries(Object.entries(request.toolInput).filter(([key]) => !ignored.includes(key)));
|
|
18
|
+
const canonical = JSON.stringify(sortKeys({
|
|
19
|
+
cwd: request.cwd,
|
|
20
|
+
toolName: request.toolName,
|
|
21
|
+
toolInput: input,
|
|
22
|
+
lastDirectUserMessage: request.decisionContext?.lastDirectUserMessage?.text ?? null
|
|
23
|
+
}));
|
|
24
|
+
return createHash("sha256").update(canonical).digest("hex");
|
|
25
|
+
}
|
|
26
|
+
function sortKeys(value) {
|
|
27
|
+
if (Array.isArray(value)) return value.map((item) => sortKeys(item));
|
|
28
|
+
if (typeof value === "object" && value !== null) return Object.fromEntries(Object.entries(value).toSorted(([a], [b]) => a < b ? -1 : 1).map(([key, item]) => [key, sortKeys(item)]));
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/budget/find-retry-deny.ts
|
|
33
|
+
function findRetryDeny(state, retryKey) {
|
|
34
|
+
if (state.lastDenied === null || state.lastDenied.retryKey !== retryKey) return null;
|
|
35
|
+
return {
|
|
36
|
+
kind: "deny",
|
|
37
|
+
rule: state.lastDenied.rule,
|
|
38
|
+
reason: state.lastDenied.reason
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/budget/types.ts
|
|
43
|
+
const EMPTY_DENIAL_STATE = {
|
|
44
|
+
consecutive: 0,
|
|
45
|
+
session: 0,
|
|
46
|
+
lastDenied: null
|
|
47
|
+
};
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/budget/load-denial-state.ts
|
|
50
|
+
const count = number().int().nonnegative();
|
|
51
|
+
const stateSchema = strictObject({
|
|
52
|
+
consecutive: count,
|
|
53
|
+
session: count,
|
|
54
|
+
lastDenied: strictObject({
|
|
55
|
+
retryKey: string().min(1),
|
|
56
|
+
rule: string(),
|
|
57
|
+
reason: string()
|
|
58
|
+
}).nullable()
|
|
59
|
+
});
|
|
60
|
+
async function loadDenialState(path) {
|
|
61
|
+
let raw;
|
|
62
|
+
try {
|
|
63
|
+
raw = await readFile(path, "utf8");
|
|
64
|
+
} catch {
|
|
65
|
+
return EMPTY_DENIAL_STATE;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const parsed = stateSchema.safeParse(JSON.parse(raw));
|
|
69
|
+
return parsed.success ? parsed.data : EMPTY_DENIAL_STATE;
|
|
70
|
+
} catch {
|
|
71
|
+
return EMPTY_DENIAL_STATE;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/budget/plan-denial-budget.ts
|
|
76
|
+
function planDenialBudget(state, verdict, retryKey, budget) {
|
|
77
|
+
if (verdict === null) return {
|
|
78
|
+
verdict: null,
|
|
79
|
+
state,
|
|
80
|
+
escalation: false
|
|
81
|
+
};
|
|
82
|
+
if (verdict.kind === "allow") return {
|
|
83
|
+
verdict,
|
|
84
|
+
state: {
|
|
85
|
+
...state,
|
|
86
|
+
consecutive: 0,
|
|
87
|
+
lastDenied: null
|
|
88
|
+
},
|
|
89
|
+
escalation: false
|
|
90
|
+
};
|
|
91
|
+
if (state.consecutive >= budget.consecutive || state.session >= budget.perSession) return {
|
|
92
|
+
verdict: null,
|
|
93
|
+
state: EMPTY_DENIAL_STATE,
|
|
94
|
+
escalation: true
|
|
95
|
+
};
|
|
96
|
+
const next = {
|
|
97
|
+
consecutive: state.consecutive + 1,
|
|
98
|
+
session: state.session + 1,
|
|
99
|
+
lastDenied: {
|
|
100
|
+
retryKey,
|
|
101
|
+
rule: verdict.rule,
|
|
102
|
+
reason: verdict.reason
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const left = Math.min(budget.consecutive - next.consecutive, budget.perSession - next.session);
|
|
106
|
+
return {
|
|
107
|
+
verdict: {
|
|
108
|
+
...verdict,
|
|
109
|
+
reason: `${verdict.reason} ${formatDenialsLeft(left)}`
|
|
110
|
+
},
|
|
111
|
+
state: next,
|
|
112
|
+
escalation: false
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function formatDenialsLeft(left) {
|
|
116
|
+
if (left > 0) return `Denials left before auto-mode asks the user: ${left}.`;
|
|
117
|
+
return "This is the last denial before auto-mode asks the user. Stop now, without finishing the rest of the task, and tell the user what consent you need to continue.";
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/state/resolve-state-dir.ts
|
|
121
|
+
function resolveStateDir() {
|
|
122
|
+
const stateHome = process.env["XDG_STATE_HOME"];
|
|
123
|
+
const base = stateHome === void 0 || stateHome === "" ? join(homedir(), ".local", "state") : stateHome;
|
|
124
|
+
return join(base, "auto-mode");
|
|
125
|
+
}
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/budget/resolve-denial-state-path.ts
|
|
128
|
+
function resolveDenialStatePath(request) {
|
|
129
|
+
const agentID = request.decisionContext?.agentID ?? "";
|
|
130
|
+
const key = createHash("sha256").update(`${request.sessionID}\0${agentID}`).digest("hex").slice(0, 32);
|
|
131
|
+
return join(resolveStateDir(), "denials", `${key}.json`);
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/budget/write-denial-state.ts
|
|
135
|
+
async function writeDenialState(path, state) {
|
|
136
|
+
const staged = `${path}.${randomUUID()}.tmp`;
|
|
137
|
+
await mkdir(dirname(path), {
|
|
138
|
+
recursive: true,
|
|
139
|
+
mode: 448
|
|
140
|
+
});
|
|
141
|
+
await writeFile(staged, `${JSON.stringify(state)}\n`, { mode: 384 });
|
|
142
|
+
await rename(staged, path);
|
|
143
|
+
}
|
|
144
|
+
//#endregion
|
|
9
145
|
//#region src/config/build-jev-only-config.ts
|
|
10
146
|
function buildJevOnlyConfig(config) {
|
|
11
147
|
if (config.provider.protocol !== "system-one") return null;
|
|
@@ -18,20 +154,51 @@ function buildJevOnlyConfig(config) {
|
|
|
18
154
|
};
|
|
19
155
|
}
|
|
20
156
|
//#endregion
|
|
157
|
+
//#region src/config/write-migrated-config.ts
|
|
158
|
+
async function writeMigratedConfig(path) {
|
|
159
|
+
let raw;
|
|
160
|
+
try {
|
|
161
|
+
raw = await readFile(path, "utf8");
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return `${path} does not exist; nothing to migrate`;
|
|
164
|
+
throw new Error(`${path} is unreadable`, { cause: error });
|
|
165
|
+
}
|
|
166
|
+
let json;
|
|
167
|
+
try {
|
|
168
|
+
json = JSON.parse(raw);
|
|
169
|
+
} catch {
|
|
170
|
+
throw new Error(`${path} is not valid JSON`);
|
|
171
|
+
}
|
|
172
|
+
const normalized = normalizeConfigFile(json, path);
|
|
173
|
+
if (!normalized.isLegacy) return `${path} already uses the current shape`;
|
|
174
|
+
const backup = `${path}.bak`;
|
|
175
|
+
const staged = `${path}.migrating`;
|
|
176
|
+
try {
|
|
177
|
+
await copyFile(path, backup, constants.COPYFILE_EXCL);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (error instanceof Error && "code" in error && error.code === "EEXIST") throw new Error(`${backup} already exists; move it aside and run the migration again`, { cause: error });
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
await writeFile(staged, `${JSON.stringify(normalized.file, null, 2)}\n`, { mode: 384 });
|
|
183
|
+
await rename(staged, path);
|
|
184
|
+
return `rewrote ${path} in the current shape; the old file is at ${backup}`;
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
21
187
|
//#region src/diagnostics/write-action-diagnostic.ts
|
|
22
188
|
async function writeActionDiagnostic(payload, entry) {
|
|
23
|
-
const
|
|
24
|
-
const stateDir = stateHome === void 0 || stateHome === "" ? join(homedir(), ".local", "state") : stateHome;
|
|
25
|
-
const path = process.env["AUTO_MODE_DIAGNOSTICS_PATH"] ?? join(stateDir, "auto-mode", "actions.jsonl");
|
|
189
|
+
const path = process.env["AUTO_MODE_DIAGNOSTICS_PATH"] ?? join(resolveStateDir(), "actions.jsonl");
|
|
26
190
|
if (path === "") return;
|
|
27
191
|
const record = {
|
|
28
|
-
schemaVersion:
|
|
192
|
+
schemaVersion: 3,
|
|
29
193
|
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
30
194
|
invocationID: entry.invocationID,
|
|
31
195
|
sessionHash: toHash(payload.sessionID),
|
|
32
196
|
actionHash: payload.toolUseID === void 0 ? null : toHash(payload.toolUseID),
|
|
33
197
|
status: entry.status,
|
|
34
198
|
verdict: entry.verdict ?? null,
|
|
199
|
+
decidingStage: entry.decidingStage ?? null,
|
|
200
|
+
denials: entry.denials ?? null,
|
|
201
|
+
escalation: entry.escalation ?? false,
|
|
35
202
|
diagnostics: entry.diagnostics ?? null
|
|
36
203
|
};
|
|
37
204
|
try {
|
|
@@ -54,6 +221,7 @@ const USAGE = `auto-mode — a permission classifier for the auto-mode Claude Co
|
|
|
54
221
|
Usage:
|
|
55
222
|
auto-mode run Read an action request on stdin, write a verdict on stdout
|
|
56
223
|
auto-mode print-prompt Print the system prompt the classifier receives
|
|
224
|
+
auto-mode config migrate Rewrite the config file from the old keys into the current shape
|
|
57
225
|
|
|
58
226
|
Options:
|
|
59
227
|
--classifier <path> Use this framework file instead of the shipped one
|
|
@@ -92,6 +260,7 @@ async function run(explain, localOnly, jevOnly, deadline) {
|
|
|
92
260
|
});
|
|
93
261
|
return printNote(true, "configuration unreadable; no verdict");
|
|
94
262
|
}
|
|
263
|
+
printWarnings(loaded.warnings);
|
|
95
264
|
const config = jevOnly ? buildJevOnlyConfig(loaded) : loaded;
|
|
96
265
|
if (config === null) {
|
|
97
266
|
await writeActionDiagnostic(request, {
|
|
@@ -119,27 +288,56 @@ async function run(explain, localOnly, jevOnly, deadline) {
|
|
|
119
288
|
process.on("SIGINT", stopEvaluation);
|
|
120
289
|
}
|
|
121
290
|
try {
|
|
122
|
-
const
|
|
291
|
+
const statePath = resolveDenialStatePath(request);
|
|
292
|
+
const retryKey = buildRetryKey(request);
|
|
293
|
+
const before = await loadDenialState(statePath);
|
|
294
|
+
const retry = findRetryDeny(before, retryKey);
|
|
295
|
+
const outcome = retry === null ? await classifyAction(request, config, {
|
|
123
296
|
localOnly,
|
|
124
297
|
deadlineAt,
|
|
125
298
|
signal: controller.signal
|
|
126
|
-
})
|
|
127
|
-
|
|
299
|
+
}) : {
|
|
300
|
+
verdict: retry,
|
|
301
|
+
decidingStage: "retry",
|
|
302
|
+
note: "retry of the action just denied; denied again",
|
|
303
|
+
status: "deny"
|
|
304
|
+
};
|
|
305
|
+
const latest = retry === null ? await loadDenialState(statePath) : before;
|
|
306
|
+
const plan = planDenialBudget(latest, outcome.verdict, retryKey, config.denialBudget ?? DEFAULT_DENIAL_BUDGET);
|
|
307
|
+
if (plan.state !== latest) await tryWriteDenialState(statePath, plan.state);
|
|
308
|
+
if (plan.verdict !== null) writeVerdict(plan.verdict);
|
|
128
309
|
await writeActionDiagnostic(request, {
|
|
129
310
|
invocationID,
|
|
130
311
|
status: outcome.status,
|
|
131
|
-
verdict:
|
|
312
|
+
verdict: plan.verdict?.kind ?? "defer",
|
|
313
|
+
decidingStage: plan.escalation ? "budget" : outcome.decidingStage,
|
|
314
|
+
denials: {
|
|
315
|
+
consecutive: plan.state.consecutive,
|
|
316
|
+
session: plan.state.session
|
|
317
|
+
},
|
|
318
|
+
escalation: plan.escalation,
|
|
132
319
|
...outcome.diagnostics === void 0 ? {} : { diagnostics: outcome.diagnostics }
|
|
133
320
|
});
|
|
134
|
-
|
|
321
|
+
const note = plan.escalation ? "denial budget exhausted; the user decides this action" : outcome.note;
|
|
322
|
+
return printNote(explain || outcome.unavailable === true, note);
|
|
135
323
|
} finally {
|
|
136
324
|
process.off("SIGTERM", stopEvaluation);
|
|
137
325
|
process.off("SIGINT", stopEvaluation);
|
|
138
326
|
}
|
|
139
327
|
}
|
|
328
|
+
async function tryWriteDenialState(path, state) {
|
|
329
|
+
try {
|
|
330
|
+
await writeDenialState(path, state);
|
|
331
|
+
} catch {
|
|
332
|
+
process.stderr.write("auto-mode: denial counts unavailable\n");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
140
335
|
function writeVerdict(verdict) {
|
|
141
336
|
process.stdout.write(renderVerdict(verdict));
|
|
142
337
|
}
|
|
338
|
+
function printWarnings(warnings = []) {
|
|
339
|
+
for (const warning of warnings) process.stderr.write(`auto-mode: ${warning}\n`);
|
|
340
|
+
}
|
|
143
341
|
function printNote(explain, message) {
|
|
144
342
|
if (explain) process.stderr.write(`auto-mode: ${message}\n`);
|
|
145
343
|
return 0;
|
|
@@ -174,6 +372,7 @@ async function main(argv) {
|
|
|
174
372
|
}
|
|
175
373
|
if (command === "print-prompt") {
|
|
176
374
|
const config = await loadConfig();
|
|
375
|
+
printWarnings(config.warnings);
|
|
177
376
|
const framework = config.provider.protocol === "system-one" ? "decision.md" : "classifier.md";
|
|
178
377
|
const prompt = await loadPolicy({
|
|
179
378
|
classifierPath: args.values.classifier ?? config.classifierPath,
|
|
@@ -182,9 +381,21 @@ async function main(argv) {
|
|
|
182
381
|
process.stdout.write(`${prompt}\n`);
|
|
183
382
|
return 0;
|
|
184
383
|
}
|
|
384
|
+
if (command === "config" && args.positionals[1] === "migrate") return runConfigMigration();
|
|
185
385
|
process.stderr.write(`auto-mode: unknown command '${command}'\n\n${USAGE}`);
|
|
186
386
|
return 2;
|
|
187
387
|
}
|
|
388
|
+
async function runConfigMigration() {
|
|
389
|
+
const path = resolveConfigPath();
|
|
390
|
+
try {
|
|
391
|
+
const result = await writeMigratedConfig(path);
|
|
392
|
+
process.stdout.write(`${result}\n`);
|
|
393
|
+
return 0;
|
|
394
|
+
} catch (error) {
|
|
395
|
+
process.stderr.write(`auto-mode: ${error instanceof Error ? error.message : "config migration failed"}\n`);
|
|
396
|
+
return 1;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
188
399
|
process.exitCode = await main(process.argv.slice(2));
|
|
189
400
|
//#endregion
|
|
190
401
|
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
//#region src/budget/types.d.ts
|
|
2
|
+
interface DenialBudget {
|
|
3
|
+
readonly consecutive: number;
|
|
4
|
+
readonly perSession: number;
|
|
5
|
+
}
|
|
6
|
+
//#endregion
|
|
1
7
|
//#region src/config/types.d.ts
|
|
2
8
|
interface EvaluationOptions {
|
|
3
9
|
readonly deadlineAt?: number | undefined;
|
|
4
10
|
readonly signal?: Readonly<AbortSignal> | undefined;
|
|
5
11
|
}
|
|
6
12
|
//#endregion
|
|
13
|
+
//#region src/config/presets.d.ts
|
|
14
|
+
export declare const PRESETS: Readonly<Record<string, ProviderConfig>>;
|
|
15
|
+
//#endregion
|
|
7
16
|
//#region src/config/config.d.ts
|
|
8
17
|
interface ProviderConfig {
|
|
9
18
|
readonly protocol?: 'messages' | 'system-one';
|
|
@@ -15,16 +24,25 @@ interface ProviderConfig {
|
|
|
15
24
|
readonly maxTokens: number;
|
|
16
25
|
readonly timeoutMs: number;
|
|
17
26
|
}
|
|
27
|
+
interface ScopeSource {
|
|
28
|
+
readonly kind: ScopeSourceKind;
|
|
29
|
+
readonly paths?: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
type ScopeSourceKind = (typeof SCOPE_SOURCE_KINDS)[number];
|
|
32
|
+
declare const SCOPE_SOURCE_KINDS: readonly ['cwd', 'session', 'globs', 'atc'];
|
|
18
33
|
interface Config {
|
|
19
34
|
readonly provider: ProviderConfig;
|
|
35
|
+
readonly judge?: ProviderConfig | null;
|
|
36
|
+
readonly scopeSources?: Readonly<Record<string, ScopeSource>>;
|
|
20
37
|
readonly classifierPath?: string | undefined;
|
|
21
38
|
readonly rulesPath?: string | undefined;
|
|
22
39
|
readonly onFailure: 'defer' | 'deny';
|
|
23
40
|
readonly claudeSettingsPath?: string | null | undefined;
|
|
24
41
|
readonly minConfidence?: number | undefined;
|
|
42
|
+
readonly denialBudget?: DenialBudget;
|
|
43
|
+
readonly warnings?: readonly string[];
|
|
25
44
|
}
|
|
26
45
|
export declare const DEFAULT_CONFIG: Config;
|
|
27
|
-
export declare const PRESETS: Readonly<Record<string, ProviderConfig>>;
|
|
28
46
|
export declare function resolveConfigPath(): string;
|
|
29
47
|
export declare function loadConfig(path?: string): Promise<Config>;
|
|
30
48
|
export declare function resolveApiKey(provider: ProviderConfig, options?: EvaluationOptions): Promise<string | null>;
|
|
@@ -97,8 +115,10 @@ interface DecisionDiagnostics {
|
|
|
97
115
|
}
|
|
98
116
|
//#endregion
|
|
99
117
|
//#region src/classify-action.d.ts
|
|
118
|
+
type DecidingStage = 'local' | 'jev' | 'messages' | 'retry';
|
|
100
119
|
interface ActionOutcome {
|
|
101
120
|
readonly verdict: Verdict | null;
|
|
121
|
+
readonly decidingStage: DecidingStage;
|
|
102
122
|
readonly note: string;
|
|
103
123
|
readonly status: 'allow' | 'skipped' | 'failure' | DecisionDiagnostics['status'];
|
|
104
124
|
readonly unavailable?: boolean;
|
|
@@ -143,4 +163,4 @@ type LocalVerdict = {
|
|
|
143
163
|
};
|
|
144
164
|
export declare function classifyLocally(payload: ActionRequest): LocalVerdict;
|
|
145
165
|
//#endregion
|
|
146
|
-
export type { ActionOutcome, ActionRequest, ClassifyOptions, Config, DecisionContext, LocalVerdict, ModelOutcome, PolicyPaths, ProviderConfig, Verdict };
|
|
166
|
+
export type { ActionOutcome, ActionRequest, ClassifyOptions, Config, DecisionContext, LocalVerdict, ModelOutcome, PolicyPaths, ProviderConfig, ScopeSource, Verdict };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as classifyWithModel, c as DEFAULT_CONFIG, d as resolveApiKey, f as resolveConfigPath, i as classifyLocally,
|
|
1
|
+
import { a as classifyWithModel, c as DEFAULT_CONFIG, d as resolveApiKey, f as resolveConfigPath, i as classifyLocally, m as PRESETS, n as parseActionRequest, o as parseModelVerdict, r as classifyAction, s as loadPolicy, t as renderVerdict, u as loadConfig } from "./render-verdict-DePMDXKz.js";
|
|
2
2
|
export { DEFAULT_CONFIG, PRESETS, classifyAction, classifyLocally, classifyWithModel, loadConfig, loadPolicy, parseActionRequest, parseModelVerdict, renderVerdict, resolveApiKey, resolveConfigPath };
|
|
@@ -252,7 +252,7 @@ function safeExtend(schema, shape) {
|
|
|
252
252
|
if (!isPlainObject(shape)) throw new Error("Invalid input to safeExtend: expected a plain object");
|
|
253
253
|
return clone(schema, mergeDefs(schema._zod.def, { shape: extended(schema, shape) }));
|
|
254
254
|
}
|
|
255
|
-
function merge
|
|
255
|
+
function merge(a, b) {
|
|
256
256
|
if (!b?._zod?.def) throw new Error("Invalid input to merge: expected an object schema. To merge a plain shape, use `.extend()`.");
|
|
257
257
|
if (a._zod.def.checks?.length) throw new Error(".merge() cannot be used on object schemas containing refinements. Use .safeExtend() instead.");
|
|
258
258
|
const newShape = {};
|
|
@@ -5218,7 +5218,7 @@ const ZodObject = /*@__PURE__*/ $constructor("ZodObject", (inst, def) => {
|
|
|
5218
5218
|
return safeExtend(this, incoming);
|
|
5219
5219
|
},
|
|
5220
5220
|
merge(other) {
|
|
5221
|
-
return merge
|
|
5221
|
+
return merge(this, other);
|
|
5222
5222
|
},
|
|
5223
5223
|
pick(mask) {
|
|
5224
5224
|
return pick(this, mask);
|
|
@@ -5569,6 +5569,171 @@ async function loadClaudeRules(path) {
|
|
|
5569
5569
|
hard_deny: (rules.autoMode?.hard_deny ?? []).filter((entry) => entry !== "$defaults")
|
|
5570
5570
|
};
|
|
5571
5571
|
}
|
|
5572
|
+
const PRESETS = {
|
|
5573
|
+
jev: {
|
|
5574
|
+
protocol: "system-one",
|
|
5575
|
+
baseURL: "https://api.typesafe.ai",
|
|
5576
|
+
model: "jev-1.13.0",
|
|
5577
|
+
apiKeyEnv: "TYPESAFE_API_KEY",
|
|
5578
|
+
reasoning: false,
|
|
5579
|
+
maxTokens: 3e3,
|
|
5580
|
+
timeoutMs: 5e3
|
|
5581
|
+
},
|
|
5582
|
+
spark: {
|
|
5583
|
+
baseURL: "https://api.meta.ai",
|
|
5584
|
+
model: "muse-spark-1.3-contributor",
|
|
5585
|
+
apiKeyEnv: "META_API_KEY",
|
|
5586
|
+
reasoning: true,
|
|
5587
|
+
maxTokens: 3e3,
|
|
5588
|
+
timeoutMs: 45e3
|
|
5589
|
+
},
|
|
5590
|
+
claude: {
|
|
5591
|
+
baseURL: "https://api.anthropic.com",
|
|
5592
|
+
model: "claude-haiku-4-5-20251001",
|
|
5593
|
+
apiKeyEnv: "ANTHROPIC_API_KEY",
|
|
5594
|
+
reasoning: true,
|
|
5595
|
+
maxTokens: 3e3,
|
|
5596
|
+
timeoutMs: 45e3
|
|
5597
|
+
},
|
|
5598
|
+
glm: {
|
|
5599
|
+
baseURL: "https://api.z.ai/api/anthropic",
|
|
5600
|
+
model: "glm-5.3-flash",
|
|
5601
|
+
apiKeyEnv: "ZAI_API_KEY",
|
|
5602
|
+
reasoning: true,
|
|
5603
|
+
maxTokens: 3e3,
|
|
5604
|
+
timeoutMs: 6e4
|
|
5605
|
+
}
|
|
5606
|
+
};
|
|
5607
|
+
const MESSAGES_DEFAULTS = {
|
|
5608
|
+
protocol: "messages",
|
|
5609
|
+
baseURL: "https://api.anthropic.com",
|
|
5610
|
+
reasoning: true,
|
|
5611
|
+
maxTokens: 3e3,
|
|
5612
|
+
timeoutMs: 45e3
|
|
5613
|
+
};
|
|
5614
|
+
function findPreset(id) {
|
|
5615
|
+
return Object.hasOwn(PRESETS, id) ? PRESETS[id] : void 0;
|
|
5616
|
+
}
|
|
5617
|
+
//#endregion
|
|
5618
|
+
//#region src/config/normalize-config-file.ts
|
|
5619
|
+
function normalizeConfigFile(json, path) {
|
|
5620
|
+
if (!isRecord(json)) return {
|
|
5621
|
+
file: json,
|
|
5622
|
+
isLegacy: false,
|
|
5623
|
+
warnings: []
|
|
5624
|
+
};
|
|
5625
|
+
const legacyKeys = Object.keys(json).filter((key) => LEGACY_KEYS.has(key));
|
|
5626
|
+
if (legacyKeys.length === 0) return {
|
|
5627
|
+
file: json,
|
|
5628
|
+
isLegacy: false,
|
|
5629
|
+
warnings: []
|
|
5630
|
+
};
|
|
5631
|
+
const currentKeys = Object.keys(json).filter((key) => !LEGACY_KEYS.has(key));
|
|
5632
|
+
if (currentKeys.length > 0) throw new Error(`${path} mixes the old keys (${legacyKeys.join(", ")}) with the new ones (${currentKeys.join(", ")}); run \`auto-mode config migrate\` on a copy without the new keys`);
|
|
5633
|
+
const parsed = legacyConfigSchema.safeParse(json);
|
|
5634
|
+
if (!parsed.success) throw new Error(`${path} is not a valid config: ${prettifyError(parsed.error)}`);
|
|
5635
|
+
const legacy = parsed.data;
|
|
5636
|
+
const warnings = [`${path} uses the old config keys (${legacyKeys.join(", ")}); run \`auto-mode config migrate\` to rewrite it`, ...legacy.transcriptEntries === void 0 ? [] : [`${path} sets transcriptEntries, which has no effect; migration drops it`]];
|
|
5637
|
+
return {
|
|
5638
|
+
file: buildFileFromLegacy(legacy, path),
|
|
5639
|
+
isLegacy: true,
|
|
5640
|
+
warnings
|
|
5641
|
+
};
|
|
5642
|
+
}
|
|
5643
|
+
function isRecord(value) {
|
|
5644
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5645
|
+
}
|
|
5646
|
+
const LEGACY_KEYS = /* @__PURE__ */ new Set([
|
|
5647
|
+
"preset",
|
|
5648
|
+
"provider",
|
|
5649
|
+
"classifierPath",
|
|
5650
|
+
"rulesPath",
|
|
5651
|
+
"transcriptEntries",
|
|
5652
|
+
"onFailure",
|
|
5653
|
+
"claudeSettingsPath",
|
|
5654
|
+
"minConfidence"
|
|
5655
|
+
]);
|
|
5656
|
+
const text$1 = string().min(1);
|
|
5657
|
+
const positive$1 = number().positive();
|
|
5658
|
+
const legacyConfigSchema = strictObject({
|
|
5659
|
+
preset: text$1.optional(),
|
|
5660
|
+
provider: strictObject({
|
|
5661
|
+
protocol: _enum(["messages", "system-one"]).optional(),
|
|
5662
|
+
baseURL: text$1.optional(),
|
|
5663
|
+
model: text$1.optional(),
|
|
5664
|
+
apiKeyEnv: text$1.optional(),
|
|
5665
|
+
apiKeyCommand: text$1.optional(),
|
|
5666
|
+
reasoning: boolean().optional(),
|
|
5667
|
+
maxTokens: positive$1.optional(),
|
|
5668
|
+
timeoutMs: positive$1.optional()
|
|
5669
|
+
}).optional(),
|
|
5670
|
+
classifierPath: text$1.optional(),
|
|
5671
|
+
rulesPath: text$1.optional(),
|
|
5672
|
+
transcriptEntries: number().nonnegative().optional(),
|
|
5673
|
+
onFailure: _enum(["defer", "deny"]).optional(),
|
|
5674
|
+
claudeSettingsPath: text$1.nullable().optional(),
|
|
5675
|
+
minConfidence: number().min(.5).max(1).optional()
|
|
5676
|
+
});
|
|
5677
|
+
function buildFileFromLegacy(legacy, path) {
|
|
5678
|
+
const hasClassifier = legacy.preset !== void 0 || legacy.provider !== void 0;
|
|
5679
|
+
const decision = {
|
|
5680
|
+
...hasClassifier ? { classifier: getLegacyClassifierID(legacy) } : {},
|
|
5681
|
+
...legacy.minConfidence === void 0 ? {} : { minConfidence: legacy.minConfidence },
|
|
5682
|
+
...legacy.onFailure === void 0 ? {} : { onFailure: legacy.onFailure }
|
|
5683
|
+
};
|
|
5684
|
+
const policy = {
|
|
5685
|
+
...legacy.rulesPath === void 0 ? {} : { rulesPath: legacy.rulesPath },
|
|
5686
|
+
...legacy.classifierPath === void 0 ? {} : { frameworkPath: legacy.classifierPath },
|
|
5687
|
+
...legacy.claudeSettingsPath === void 0 ? {} : { claudeSettingsPath: legacy.claudeSettingsPath }
|
|
5688
|
+
};
|
|
5689
|
+
return {
|
|
5690
|
+
...hasClassifier ? { classifiers: { [getLegacyClassifierID(legacy)]: buildClassifierEntry(getLegacyClassifierID(legacy), buildLegacyProvider(legacy, path)) } } : {},
|
|
5691
|
+
...Object.keys(decision).length === 0 ? {} : { decision },
|
|
5692
|
+
...Object.keys(policy).length === 0 ? {} : { policy }
|
|
5693
|
+
};
|
|
5694
|
+
}
|
|
5695
|
+
function getLegacyClassifierID(legacy) {
|
|
5696
|
+
if (legacy.preset !== void 0) return legacy.preset;
|
|
5697
|
+
return legacy.provider !== void 0 && legacy.provider.protocol !== "system-one" ? "spark" : "jev";
|
|
5698
|
+
}
|
|
5699
|
+
function buildLegacyProvider(legacy, path) {
|
|
5700
|
+
const preset = findPreset(getLegacyClassifierID(legacy));
|
|
5701
|
+
if (preset === void 0) throw new Error(`${path} names an unknown preset '${legacy.preset}'; known presets are ${Object.keys(PRESETS).join(", ")}`);
|
|
5702
|
+
const override = legacy.provider ?? {};
|
|
5703
|
+
return {
|
|
5704
|
+
protocol: override.protocol ?? preset.protocol ?? "messages",
|
|
5705
|
+
baseURL: override.baseURL ?? preset.baseURL,
|
|
5706
|
+
model: override.model ?? preset.model,
|
|
5707
|
+
apiKeyEnv: override.apiKeyEnv ?? preset.apiKeyEnv,
|
|
5708
|
+
apiKeyCommand: override.apiKeyCommand ?? preset.apiKeyCommand,
|
|
5709
|
+
reasoning: override.reasoning ?? preset.reasoning,
|
|
5710
|
+
maxTokens: override.maxTokens ?? preset.maxTokens,
|
|
5711
|
+
timeoutMs: override.timeoutMs ?? preset.timeoutMs
|
|
5712
|
+
};
|
|
5713
|
+
}
|
|
5714
|
+
const PROVIDER_FIELDS = [
|
|
5715
|
+
"baseURL",
|
|
5716
|
+
"model",
|
|
5717
|
+
"apiKeyEnv",
|
|
5718
|
+
"apiKeyCommand",
|
|
5719
|
+
"reasoning",
|
|
5720
|
+
"maxTokens",
|
|
5721
|
+
"timeoutMs"
|
|
5722
|
+
];
|
|
5723
|
+
function buildClassifierEntry(id, provider) {
|
|
5724
|
+
const kind = pickEntryKind(id, provider);
|
|
5725
|
+
const base = kind === "messages" ? MESSAGES_DEFAULTS : findPreset(kind) ?? {};
|
|
5726
|
+
const fields = Object.fromEntries(PROVIDER_FIELDS.filter((field) => provider[field] !== void 0 && provider[field] !== base[field]).map((field) => [field, provider[field]]));
|
|
5727
|
+
return {
|
|
5728
|
+
...kind === id ? {} : { kind },
|
|
5729
|
+
...fields
|
|
5730
|
+
};
|
|
5731
|
+
}
|
|
5732
|
+
function pickEntryKind(id, provider) {
|
|
5733
|
+
const preset = findPreset(id);
|
|
5734
|
+
if (preset !== void 0 && (preset.protocol ?? "messages") === provider.protocol) return id;
|
|
5735
|
+
return provider.protocol === "system-one" ? "jev" : "messages";
|
|
5736
|
+
}
|
|
5572
5737
|
//#endregion
|
|
5573
5738
|
//#region src/config/read-api-key-from-command.ts
|
|
5574
5739
|
function readApiKeyFromCommand(command, options = {}) {
|
|
@@ -5624,73 +5789,29 @@ function readApiKeyFromCommand(command, options = {}) {
|
|
|
5624
5789
|
}
|
|
5625
5790
|
//#endregion
|
|
5626
5791
|
//#region src/config/config.ts
|
|
5792
|
+
const SCOPE_SOURCE_KINDS = [
|
|
5793
|
+
"cwd",
|
|
5794
|
+
"session",
|
|
5795
|
+
"globs",
|
|
5796
|
+
"atc"
|
|
5797
|
+
];
|
|
5798
|
+
const DEFAULT_PROVIDER = findPreset("jev");
|
|
5799
|
+
if (DEFAULT_PROVIDER === void 0) throw new Error("the jev preset is missing");
|
|
5800
|
+
const DEFAULT_DENIAL_BUDGET = {
|
|
5801
|
+
consecutive: 3,
|
|
5802
|
+
perSession: 20
|
|
5803
|
+
};
|
|
5627
5804
|
const DEFAULT_CONFIG = {
|
|
5628
|
-
provider:
|
|
5629
|
-
protocol: "system-one",
|
|
5630
|
-
baseURL: "https://api.typesafe.ai",
|
|
5631
|
-
model: "jev-1.13.0",
|
|
5632
|
-
apiKeyEnv: "TYPESAFE_API_KEY",
|
|
5633
|
-
reasoning: false,
|
|
5634
|
-
maxTokens: 3e3,
|
|
5635
|
-
timeoutMs: 5e3
|
|
5636
|
-
},
|
|
5805
|
+
provider: DEFAULT_PROVIDER,
|
|
5637
5806
|
onFailure: "defer",
|
|
5638
|
-
minConfidence: .8
|
|
5639
|
-
|
|
5640
|
-
const SPARK_PROVIDER = {
|
|
5641
|
-
baseURL: "https://api.meta.ai",
|
|
5642
|
-
model: "muse-spark-1.3-contributor",
|
|
5643
|
-
apiKeyEnv: "META_API_KEY",
|
|
5644
|
-
reasoning: true,
|
|
5645
|
-
maxTokens: 3e3,
|
|
5646
|
-
timeoutMs: 45e3
|
|
5647
|
-
};
|
|
5648
|
-
const PRESETS = {
|
|
5649
|
-
jev: DEFAULT_CONFIG.provider,
|
|
5650
|
-
spark: SPARK_PROVIDER,
|
|
5651
|
-
claude: {
|
|
5652
|
-
baseURL: "https://api.anthropic.com",
|
|
5653
|
-
model: "claude-haiku-4-5-20251001",
|
|
5654
|
-
apiKeyEnv: "ANTHROPIC_API_KEY",
|
|
5655
|
-
reasoning: true,
|
|
5656
|
-
maxTokens: 3e3,
|
|
5657
|
-
timeoutMs: 45e3
|
|
5658
|
-
},
|
|
5659
|
-
glm: {
|
|
5660
|
-
baseURL: "https://api.z.ai/api/anthropic",
|
|
5661
|
-
model: "glm-5.3-flash",
|
|
5662
|
-
apiKeyEnv: "ZAI_API_KEY",
|
|
5663
|
-
reasoning: true,
|
|
5664
|
-
maxTokens: 3e3,
|
|
5665
|
-
timeoutMs: 6e4
|
|
5666
|
-
}
|
|
5807
|
+
minConfidence: .8,
|
|
5808
|
+
denialBudget: DEFAULT_DENIAL_BUDGET
|
|
5667
5809
|
};
|
|
5668
5810
|
function resolveConfigPath() {
|
|
5669
5811
|
const xdg = process.env["XDG_CONFIG_HOME"];
|
|
5670
5812
|
const base = xdg !== void 0 && xdg !== "" ? xdg : join(homedir(), ".config");
|
|
5671
5813
|
return join(base, "auto-mode", "config.json");
|
|
5672
5814
|
}
|
|
5673
|
-
const text = string().min(1);
|
|
5674
|
-
const positive = number().positive();
|
|
5675
|
-
const configFileSchema = strictObject({
|
|
5676
|
-
preset: text.optional(),
|
|
5677
|
-
provider: strictObject({
|
|
5678
|
-
protocol: _enum(["messages", "system-one"]).optional(),
|
|
5679
|
-
baseURL: text.optional(),
|
|
5680
|
-
model: text.optional(),
|
|
5681
|
-
apiKeyEnv: text.optional(),
|
|
5682
|
-
apiKeyCommand: text.optional(),
|
|
5683
|
-
reasoning: boolean().optional(),
|
|
5684
|
-
maxTokens: positive.optional(),
|
|
5685
|
-
timeoutMs: positive.optional()
|
|
5686
|
-
}).optional(),
|
|
5687
|
-
classifierPath: text.optional(),
|
|
5688
|
-
rulesPath: text.optional(),
|
|
5689
|
-
transcriptEntries: number().nonnegative().optional(),
|
|
5690
|
-
onFailure: _enum(["defer", "deny"]).optional(),
|
|
5691
|
-
claudeSettingsPath: text.nullable().optional(),
|
|
5692
|
-
minConfidence: number().min(.5).max(1).optional()
|
|
5693
|
-
});
|
|
5694
5815
|
async function loadConfig(path = resolveConfigPath()) {
|
|
5695
5816
|
let raw;
|
|
5696
5817
|
try {
|
|
@@ -5705,33 +5826,150 @@ async function loadConfig(path = resolveConfigPath()) {
|
|
|
5705
5826
|
} catch {
|
|
5706
5827
|
throw new Error(`${path} is not valid JSON`);
|
|
5707
5828
|
}
|
|
5829
|
+
const normalized = normalizeConfigFile(json, path);
|
|
5830
|
+
return buildConfig(normalized.file, path, normalized.warnings);
|
|
5831
|
+
}
|
|
5832
|
+
const text = string().min(1);
|
|
5833
|
+
const positive = number().positive();
|
|
5834
|
+
const denialBudgetSchema = strictObject({
|
|
5835
|
+
consecutive: number().int().positive().optional(),
|
|
5836
|
+
perSession: number().int().positive().optional()
|
|
5837
|
+
});
|
|
5838
|
+
const configFileSchema = strictObject({
|
|
5839
|
+
classifiers: record(text, unknown()).optional(),
|
|
5840
|
+
scopeSources: record(text, unknown()).optional(),
|
|
5841
|
+
decision: strictObject({
|
|
5842
|
+
classifier: text.optional(),
|
|
5843
|
+
judge: text.nullable().optional(),
|
|
5844
|
+
minConfidence: number().min(.5).max(1).optional(),
|
|
5845
|
+
onFailure: _enum(["defer", "deny"]).optional(),
|
|
5846
|
+
denialBudget: denialBudgetSchema.optional()
|
|
5847
|
+
}).optional(),
|
|
5848
|
+
policy: strictObject({
|
|
5849
|
+
rulesPath: text.nullable().optional(),
|
|
5850
|
+
frameworkPath: text.nullable().optional(),
|
|
5851
|
+
claudeSettingsPath: text.nullable().optional()
|
|
5852
|
+
}).optional()
|
|
5853
|
+
});
|
|
5854
|
+
function buildConfig(json, path, legacyWarnings) {
|
|
5708
5855
|
const parsed = configFileSchema.safeParse(json);
|
|
5709
5856
|
if (!parsed.success) throw new Error(`${path} is not a valid config: ${prettifyError(parsed.error)}`);
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
const
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
const
|
|
5857
|
+
const file = parsed.data;
|
|
5858
|
+
const warnings = [...legacyWarnings];
|
|
5859
|
+
const classifiers = buildRegistry(file.classifiers ?? {}, (id, entry) => parseClassifierEntry(id, entry));
|
|
5860
|
+
const scopeSources = buildRegistry(file.scopeSources ?? {}, (id, entry) => parseScopeSourceEntry(id, entry));
|
|
5861
|
+
warnings.push(...classifiers.dropped.map((line) => `${path}: classifiers.${line}`), ...scopeSources.dropped.map((line) => `${path}: scopeSources.${line}`));
|
|
5862
|
+
const decision = file.decision ?? {};
|
|
5863
|
+
const policy = file.policy ?? {};
|
|
5864
|
+
const judgeID = decision.judge ?? null;
|
|
5717
5865
|
return {
|
|
5718
|
-
provider:
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5866
|
+
provider: resolveRole("classifier", decision.classifier ?? "jev", classifiers.entries, classifiers.droppedIDs, path),
|
|
5867
|
+
judge: judgeID === null ? null : resolveRole("judge", judgeID, classifiers.entries, classifiers.droppedIDs, path),
|
|
5868
|
+
scopeSources: scopeSources.entries,
|
|
5869
|
+
classifierPath: expandHomePath(policy.frameworkPath ?? void 0),
|
|
5870
|
+
rulesPath: expandHomePath(policy.rulesPath ?? void 0),
|
|
5871
|
+
onFailure: decision.onFailure ?? DEFAULT_CONFIG.onFailure,
|
|
5872
|
+
claudeSettingsPath: policy.claudeSettingsPath === null ? null : expandHomePath(policy.claudeSettingsPath),
|
|
5873
|
+
minConfidence: decision.minConfidence ?? DEFAULT_CONFIG.minConfidence,
|
|
5874
|
+
denialBudget: {
|
|
5875
|
+
consecutive: decision.denialBudget?.consecutive ?? DEFAULT_DENIAL_BUDGET.consecutive,
|
|
5876
|
+
perSession: decision.denialBudget?.perSession ?? DEFAULT_DENIAL_BUDGET.perSession
|
|
5727
5877
|
},
|
|
5728
|
-
|
|
5729
|
-
rulesPath: file.rulesPath,
|
|
5730
|
-
onFailure: file.onFailure ?? DEFAULT_CONFIG.onFailure,
|
|
5731
|
-
claudeSettingsPath: file.claudeSettingsPath,
|
|
5732
|
-
minConfidence: file.minConfidence ?? DEFAULT_CONFIG.minConfidence
|
|
5878
|
+
warnings
|
|
5733
5879
|
};
|
|
5734
5880
|
}
|
|
5881
|
+
function buildRegistry(raw, parseEntry) {
|
|
5882
|
+
const entries = /* @__PURE__ */ new Map();
|
|
5883
|
+
const dropped = [];
|
|
5884
|
+
const droppedIDs = [];
|
|
5885
|
+
for (const [id, entry] of Object.entries(raw)) {
|
|
5886
|
+
const result = parseEntry(id, entry);
|
|
5887
|
+
if (typeof result === "string") {
|
|
5888
|
+
dropped.push(`${id} dropped: ${result}`);
|
|
5889
|
+
droppedIDs.push(id);
|
|
5890
|
+
} else entries.set(id, result);
|
|
5891
|
+
}
|
|
5892
|
+
return {
|
|
5893
|
+
entries: Object.fromEntries(entries),
|
|
5894
|
+
dropped,
|
|
5895
|
+
droppedIDs
|
|
5896
|
+
};
|
|
5897
|
+
}
|
|
5898
|
+
const CLASSIFIER_KINDS = [...Object.keys(PRESETS), "messages"];
|
|
5899
|
+
const classifierEntrySchema = strictObject({
|
|
5900
|
+
kind: text.optional(),
|
|
5901
|
+
baseURL: text.optional(),
|
|
5902
|
+
model: text.optional(),
|
|
5903
|
+
apiKeyEnv: text.optional(),
|
|
5904
|
+
apiKeyCommand: text.optional(),
|
|
5905
|
+
reasoning: boolean().optional(),
|
|
5906
|
+
maxTokens: positive.optional(),
|
|
5907
|
+
timeoutMs: positive.optional()
|
|
5908
|
+
});
|
|
5909
|
+
const CREDENTIAL_KEYS = /* @__PURE__ */ new Set([
|
|
5910
|
+
"apiKey",
|
|
5911
|
+
"key",
|
|
5912
|
+
"token",
|
|
5913
|
+
"secret"
|
|
5914
|
+
]);
|
|
5915
|
+
function parseClassifierEntry(id, entry) {
|
|
5916
|
+
if (typeof entry === "object" && entry !== null) {
|
|
5917
|
+
const credential = Object.keys(entry).find((key) => CREDENTIAL_KEYS.has(key));
|
|
5918
|
+
if (credential !== void 0) return `holds a literal ${credential}; name the key with apiKeyEnv or apiKeyCommand`;
|
|
5919
|
+
}
|
|
5920
|
+
const parsed = classifierEntrySchema.safeParse(entry);
|
|
5921
|
+
if (!parsed.success) return formatFirstIssue(parsed.error.issues);
|
|
5922
|
+
const kind = parsed.data.kind ?? id;
|
|
5923
|
+
if (!CLASSIFIER_KINDS.includes(kind)) return `unknown kind '${kind}'; known kinds are ${CLASSIFIER_KINDS.join(", ")}`;
|
|
5924
|
+
const base = kind === "messages" ? MESSAGES_DEFAULTS : findPreset(kind);
|
|
5925
|
+
const model = parsed.data.model ?? base?.model;
|
|
5926
|
+
if (base === void 0 || model === void 0) return `kind '${kind}' needs a model`;
|
|
5927
|
+
return {
|
|
5928
|
+
protocol: base.protocol ?? "messages",
|
|
5929
|
+
baseURL: parsed.data.baseURL ?? base.baseURL,
|
|
5930
|
+
model,
|
|
5931
|
+
apiKeyEnv: parsed.data.apiKeyEnv ?? base.apiKeyEnv,
|
|
5932
|
+
apiKeyCommand: parsed.data.apiKeyCommand ?? base.apiKeyCommand,
|
|
5933
|
+
reasoning: parsed.data.reasoning ?? base.reasoning,
|
|
5934
|
+
maxTokens: parsed.data.maxTokens ?? base.maxTokens,
|
|
5935
|
+
timeoutMs: parsed.data.timeoutMs ?? base.timeoutMs
|
|
5936
|
+
};
|
|
5937
|
+
}
|
|
5938
|
+
const scopeSourceEntrySchema = strictObject({
|
|
5939
|
+
kind: text.optional(),
|
|
5940
|
+
paths: array(text).min(1).optional()
|
|
5941
|
+
});
|
|
5942
|
+
function parseScopeSourceEntry(id, entry) {
|
|
5943
|
+
const parsed = scopeSourceEntrySchema.safeParse(entry);
|
|
5944
|
+
if (!parsed.success) return formatFirstIssue(parsed.error.issues);
|
|
5945
|
+
const kind = SCOPE_SOURCE_KINDS.find((known) => known === (parsed.data.kind ?? id));
|
|
5946
|
+
if (kind === void 0) return `unknown kind '${parsed.data.kind ?? id}'; known kinds are ${SCOPE_SOURCE_KINDS.join(", ")}`;
|
|
5947
|
+
if (kind === "globs") return parsed.data.paths === void 0 ? "kind 'globs' needs paths" : {
|
|
5948
|
+
kind,
|
|
5949
|
+
paths: parsed.data.paths
|
|
5950
|
+
};
|
|
5951
|
+
return parsed.data.paths === void 0 ? { kind } : `kind '${kind}' takes no paths`;
|
|
5952
|
+
}
|
|
5953
|
+
function formatFirstIssue(issues) {
|
|
5954
|
+
const [issue] = issues;
|
|
5955
|
+
if (issue === void 0) return "invalid entry";
|
|
5956
|
+
return issue.path.length === 0 ? issue.message : `${issue.path.map(String).join(".")}: ${issue.message}`;
|
|
5957
|
+
}
|
|
5958
|
+
function resolveRole(role, id, entries, droppedIDs, path) {
|
|
5959
|
+
const entry = Object.hasOwn(entries, id) ? entries[id] : void 0;
|
|
5960
|
+
if (entry !== void 0) return entry;
|
|
5961
|
+
if (droppedIDs.includes(id)) throw new Error(`${path}: decision.${role} names '${id}', whose entry was dropped`);
|
|
5962
|
+
if (findPreset(id) === void 0) throw new Error(`${path}: decision.${role} names '${id}', which is neither a classifiers entry nor a built-in kind (${Object.keys(PRESETS).join(", ")})`);
|
|
5963
|
+
return parseBuiltIn(id);
|
|
5964
|
+
}
|
|
5965
|
+
function parseBuiltIn(id) {
|
|
5966
|
+
const resolved = parseClassifierEntry(id, {});
|
|
5967
|
+
if (typeof resolved === "string") throw new TypeError(`built-in kind ${id} does not resolve: ${resolved}`);
|
|
5968
|
+
return resolved;
|
|
5969
|
+
}
|
|
5970
|
+
function expandHomePath(path) {
|
|
5971
|
+
return path?.startsWith("~/") === true ? join(homedir(), path.slice(2)) : path;
|
|
5972
|
+
}
|
|
5735
5973
|
function resolveApiKey(provider, options = {}) {
|
|
5736
5974
|
const fromEnv = provider.apiKeyEnv === void 0 ? void 0 : process.env[provider.apiKeyEnv];
|
|
5737
5975
|
if (fromEnv !== void 0 && fromEnv !== "") return Promise.resolve(fromEnv);
|
|
@@ -6980,6 +7218,7 @@ async function classifyAction(request, config, options = {}) {
|
|
|
6980
7218
|
const guidance = config.onFailure === "deny" ? await tryReadDenialGuidance() : null;
|
|
6981
7219
|
return {
|
|
6982
7220
|
verdict: config.onFailure === "deny" ? buildGuidedDeny("Classifier Unavailable", "Claude settings unreadable.", guidance) : null,
|
|
7221
|
+
decidingStage: "jev",
|
|
6983
7222
|
note: "Claude settings unreadable; classifier unavailable",
|
|
6984
7223
|
status: "failure",
|
|
6985
7224
|
unavailable: true
|
|
@@ -6988,17 +7227,20 @@ async function classifyAction(request, config, options = {}) {
|
|
|
6988
7227
|
const local = configured !== null && (configured.hard_deny.length > 0 || configured.soft_deny.length > 0) ? { kind: "escalate" } : classifyLocally(request);
|
|
6989
7228
|
if (local.kind === "allow") return {
|
|
6990
7229
|
verdict: { kind: "allow" },
|
|
7230
|
+
decidingStage: "local",
|
|
6991
7231
|
note: `allowed by ${local.exception} (local)`,
|
|
6992
7232
|
status: "allow"
|
|
6993
7233
|
};
|
|
6994
7234
|
if (options.localOnly === true) return {
|
|
6995
7235
|
verdict: null,
|
|
7236
|
+
decidingStage: "local",
|
|
6996
7237
|
note: `${request.toolName} needs the model tier, which this run skipped`,
|
|
6997
7238
|
status: "skipped"
|
|
6998
7239
|
};
|
|
6999
7240
|
const [outcome, guidance] = await Promise.all([classifyWithModel(request, config, options), tryReadDenialGuidance()]);
|
|
7000
7241
|
return {
|
|
7001
7242
|
...outcome,
|
|
7243
|
+
decidingStage: config.provider.protocol === "system-one" ? "jev" : "messages",
|
|
7002
7244
|
verdict: outcome.verdict?.kind === "deny" ? buildGuidedDeny(outcome.verdict.rule, outcome.verdict.reason, guidance) : outcome.verdict,
|
|
7003
7245
|
status: outcome.diagnostics?.status ?? outcome.verdict?.kind ?? "failure"
|
|
7004
7246
|
};
|
|
@@ -7076,4 +7318,4 @@ function renderVerdict(verdict) {
|
|
|
7076
7318
|
})).exhaustive();
|
|
7077
7319
|
}
|
|
7078
7320
|
//#endregion
|
|
7079
|
-
export { classifyWithModel as a, DEFAULT_CONFIG as c, resolveApiKey as d, resolveConfigPath as f, classifyLocally as i,
|
|
7321
|
+
export { string as _, classifyWithModel as a, DEFAULT_CONFIG as c, resolveApiKey as d, resolveConfigPath as f, strictObject as g, number as h, classifyLocally as i, DEFAULT_DENIAL_BUDGET as l, PRESETS as m, parseActionRequest as n, parseModelVerdict as o, normalizeConfigFile as p, classifyAction as r, loadPolicy as s, renderVerdict as t, loadConfig as u };
|