auto-mode 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -2
- package/dist/cli.js +172 -52
- package/dist/index.d.ts +9 -0
- package/dist/index.js +1 -1
- package/dist/{render-verdict-CAg3AKWH.js → render-verdict-CK9sRJpa.js} +24 -127
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ Create the classifier configuration:
|
|
|
38
38
|
|
|
39
39
|
Save it as `~/.config/auto-mode/config.json` and export your TypeSafe API key, or set the entry's
|
|
40
40
|
`apiKeyCommand` to a command that prints it. [Configuration](./docs/guides/configuration.md) covers
|
|
41
|
-
the registries
|
|
41
|
+
the registries and credentials.
|
|
42
42
|
|
|
43
43
|
## Standing permissions
|
|
44
44
|
|
|
@@ -77,7 +77,6 @@ fit.
|
|
|
77
77
|
| `auto-mode run --local-only` | Skip the model tier |
|
|
78
78
|
| `auto-mode run --jev-only` | Require Jev with a 5-second API timeout |
|
|
79
79
|
| `auto-mode print-prompt` | Print the selected provider's base policy |
|
|
80
|
-
| `auto-mode config migrate` | Rewrite an older config file in place |
|
|
81
80
|
|
|
82
81
|
The mod evaluates only ask decisions and preserves existing allow and deny decisions. It runs the
|
|
83
82
|
CLI as a bounded child process with `--jev-only`. The `spark`, `claude`, `glm`, and `messages`
|
package/dist/cli.js
CHANGED
|
@@ -1,67 +1,174 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { appendFile,
|
|
2
|
+
import { g as string, h as strictObject, l as DEFAULT_DENIAL_BUDGET, m as number, n as parseActionRequest, r as classifyAction, s as loadPolicy, t as renderVerdict, u as loadConfig } from "./render-verdict-CK9sRJpa.js";
|
|
3
|
+
import { appendFile, 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/
|
|
10
|
-
|
|
11
|
-
|
|
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;
|
|
12
35
|
return {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
timeoutMs: Math.min(config.provider.timeoutMs, 5e3)
|
|
17
|
-
}
|
|
36
|
+
kind: "deny",
|
|
37
|
+
rule: state.lastDenied.rule,
|
|
38
|
+
reason: state.lastDenied.reason
|
|
18
39
|
};
|
|
19
40
|
}
|
|
20
41
|
//#endregion
|
|
21
|
-
//#region src/
|
|
22
|
-
|
|
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) {
|
|
23
61
|
let raw;
|
|
24
62
|
try {
|
|
25
63
|
raw = await readFile(path, "utf8");
|
|
26
|
-
} catch (error) {
|
|
27
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") return `${path} does not exist; nothing to migrate`;
|
|
28
|
-
throw new Error(`${path} is unreadable`, { cause: error });
|
|
29
|
-
}
|
|
30
|
-
let json;
|
|
31
|
-
try {
|
|
32
|
-
json = JSON.parse(raw);
|
|
33
64
|
} catch {
|
|
34
|
-
|
|
65
|
+
return EMPTY_DENIAL_STATE;
|
|
35
66
|
}
|
|
36
|
-
const normalized = normalizeConfigFile(json, path);
|
|
37
|
-
if (!normalized.isLegacy) return `${path} already uses the current shape`;
|
|
38
|
-
const backup = `${path}.bak`;
|
|
39
|
-
const staged = `${path}.migrating`;
|
|
40
67
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
68
|
+
const parsed = stateSchema.safeParse(JSON.parse(raw));
|
|
69
|
+
return parsed.success ? parsed.data : EMPTY_DENIAL_STATE;
|
|
70
|
+
} catch {
|
|
71
|
+
return EMPTY_DENIAL_STATE;
|
|
45
72
|
}
|
|
46
|
-
|
|
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 });
|
|
47
142
|
await rename(staged, path);
|
|
48
|
-
|
|
143
|
+
}
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/config/build-jev-only-config.ts
|
|
146
|
+
function buildJevOnlyConfig(config) {
|
|
147
|
+
if (config.provider.protocol !== "system-one") return null;
|
|
148
|
+
return {
|
|
149
|
+
...config,
|
|
150
|
+
provider: {
|
|
151
|
+
...config.provider,
|
|
152
|
+
timeoutMs: Math.min(config.provider.timeoutMs, 5e3)
|
|
153
|
+
}
|
|
154
|
+
};
|
|
49
155
|
}
|
|
50
156
|
//#endregion
|
|
51
157
|
//#region src/diagnostics/write-action-diagnostic.ts
|
|
52
158
|
async function writeActionDiagnostic(payload, entry) {
|
|
53
|
-
const
|
|
54
|
-
const stateDir = stateHome === void 0 || stateHome === "" ? join(homedir(), ".local", "state") : stateHome;
|
|
55
|
-
const path = process.env["AUTO_MODE_DIAGNOSTICS_PATH"] ?? join(stateDir, "auto-mode", "actions.jsonl");
|
|
159
|
+
const path = process.env["AUTO_MODE_DIAGNOSTICS_PATH"] ?? join(resolveStateDir(), "actions.jsonl");
|
|
56
160
|
if (path === "") return;
|
|
57
161
|
const record = {
|
|
58
|
-
schemaVersion:
|
|
162
|
+
schemaVersion: 3,
|
|
59
163
|
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
60
164
|
invocationID: entry.invocationID,
|
|
61
165
|
sessionHash: toHash(payload.sessionID),
|
|
62
166
|
actionHash: payload.toolUseID === void 0 ? null : toHash(payload.toolUseID),
|
|
63
167
|
status: entry.status,
|
|
64
168
|
verdict: entry.verdict ?? null,
|
|
169
|
+
decidingStage: entry.decidingStage ?? null,
|
|
170
|
+
denials: entry.denials ?? null,
|
|
171
|
+
escalation: entry.escalation ?? false,
|
|
65
172
|
diagnostics: entry.diagnostics ?? null
|
|
66
173
|
};
|
|
67
174
|
try {
|
|
@@ -84,7 +191,6 @@ const USAGE = `auto-mode — a permission classifier for the auto-mode Claude Co
|
|
|
84
191
|
Usage:
|
|
85
192
|
auto-mode run Read an action request on stdin, write a verdict on stdout
|
|
86
193
|
auto-mode print-prompt Print the system prompt the classifier receives
|
|
87
|
-
auto-mode config migrate Rewrite the config file from the old keys into the current shape
|
|
88
194
|
|
|
89
195
|
Options:
|
|
90
196
|
--classifier <path> Use this framework file instead of the shipped one
|
|
@@ -151,24 +257,50 @@ async function run(explain, localOnly, jevOnly, deadline) {
|
|
|
151
257
|
process.on("SIGINT", stopEvaluation);
|
|
152
258
|
}
|
|
153
259
|
try {
|
|
154
|
-
const
|
|
260
|
+
const statePath = resolveDenialStatePath(request);
|
|
261
|
+
const retryKey = buildRetryKey(request);
|
|
262
|
+
const before = await loadDenialState(statePath);
|
|
263
|
+
const retry = findRetryDeny(before, retryKey);
|
|
264
|
+
const outcome = retry === null ? await classifyAction(request, config, {
|
|
155
265
|
localOnly,
|
|
156
266
|
deadlineAt,
|
|
157
267
|
signal: controller.signal
|
|
158
|
-
})
|
|
159
|
-
|
|
268
|
+
}) : {
|
|
269
|
+
verdict: retry,
|
|
270
|
+
decidingStage: "retry",
|
|
271
|
+
note: "retry of the action just denied; denied again",
|
|
272
|
+
status: "deny"
|
|
273
|
+
};
|
|
274
|
+
const latest = retry === null ? await loadDenialState(statePath) : before;
|
|
275
|
+
const plan = planDenialBudget(latest, outcome.verdict, retryKey, config.denialBudget ?? DEFAULT_DENIAL_BUDGET);
|
|
276
|
+
if (plan.state !== latest) await tryWriteDenialState(statePath, plan.state);
|
|
277
|
+
if (plan.verdict !== null) writeVerdict(plan.verdict);
|
|
160
278
|
await writeActionDiagnostic(request, {
|
|
161
279
|
invocationID,
|
|
162
280
|
status: outcome.status,
|
|
163
|
-
verdict:
|
|
281
|
+
verdict: plan.verdict?.kind ?? "defer",
|
|
282
|
+
decidingStage: plan.escalation ? "budget" : outcome.decidingStage,
|
|
283
|
+
denials: {
|
|
284
|
+
consecutive: plan.state.consecutive,
|
|
285
|
+
session: plan.state.session
|
|
286
|
+
},
|
|
287
|
+
escalation: plan.escalation,
|
|
164
288
|
...outcome.diagnostics === void 0 ? {} : { diagnostics: outcome.diagnostics }
|
|
165
289
|
});
|
|
166
|
-
|
|
290
|
+
const note = plan.escalation ? "denial budget exhausted; the user decides this action" : outcome.note;
|
|
291
|
+
return printNote(explain || outcome.unavailable === true, note);
|
|
167
292
|
} finally {
|
|
168
293
|
process.off("SIGTERM", stopEvaluation);
|
|
169
294
|
process.off("SIGINT", stopEvaluation);
|
|
170
295
|
}
|
|
171
296
|
}
|
|
297
|
+
async function tryWriteDenialState(path, state) {
|
|
298
|
+
try {
|
|
299
|
+
await writeDenialState(path, state);
|
|
300
|
+
} catch {
|
|
301
|
+
process.stderr.write("auto-mode: denial counts unavailable\n");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
172
304
|
function writeVerdict(verdict) {
|
|
173
305
|
process.stdout.write(renderVerdict(verdict));
|
|
174
306
|
}
|
|
@@ -218,21 +350,9 @@ async function main(argv) {
|
|
|
218
350
|
process.stdout.write(`${prompt}\n`);
|
|
219
351
|
return 0;
|
|
220
352
|
}
|
|
221
|
-
if (command === "config" && args.positionals[1] === "migrate") return runConfigMigration();
|
|
222
353
|
process.stderr.write(`auto-mode: unknown command '${command}'\n\n${USAGE}`);
|
|
223
354
|
return 2;
|
|
224
355
|
}
|
|
225
|
-
async function runConfigMigration() {
|
|
226
|
-
const path = resolveConfigPath();
|
|
227
|
-
try {
|
|
228
|
-
const result = await writeMigratedConfig(path);
|
|
229
|
-
process.stdout.write(`${result}\n`);
|
|
230
|
-
return 0;
|
|
231
|
-
} catch (error) {
|
|
232
|
-
process.stderr.write(`auto-mode: ${error instanceof Error ? error.message : "config migration failed"}\n`);
|
|
233
|
-
return 1;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
356
|
process.exitCode = await main(process.argv.slice(2));
|
|
237
357
|
//#endregion
|
|
238
358
|
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
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;
|
|
@@ -33,6 +39,7 @@ interface Config {
|
|
|
33
39
|
readonly onFailure: 'defer' | 'deny';
|
|
34
40
|
readonly claudeSettingsPath?: string | null | undefined;
|
|
35
41
|
readonly minConfidence?: number | undefined;
|
|
42
|
+
readonly denialBudget?: DenialBudget;
|
|
36
43
|
readonly warnings?: readonly string[];
|
|
37
44
|
}
|
|
38
45
|
export declare const DEFAULT_CONFIG: Config;
|
|
@@ -108,8 +115,10 @@ interface DecisionDiagnostics {
|
|
|
108
115
|
}
|
|
109
116
|
//#endregion
|
|
110
117
|
//#region src/classify-action.d.ts
|
|
118
|
+
type DecidingStage = 'local' | 'jev' | 'messages' | 'retry';
|
|
111
119
|
interface ActionOutcome {
|
|
112
120
|
readonly verdict: Verdict | null;
|
|
121
|
+
readonly decidingStage: DecidingStage;
|
|
113
122
|
readonly note: string;
|
|
114
123
|
readonly status: 'allow' | 'skipped' | 'failure' | DecisionDiagnostics['status'];
|
|
115
124
|
readonly unavailable?: boolean;
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as classifyWithModel, c as DEFAULT_CONFIG, d as
|
|
1
|
+
import { a as classifyWithModel, c as DEFAULT_CONFIG, d as resolveApiKey, f as resolveConfigPath, i as classifyLocally, n as parseActionRequest, o as parseModelVerdict, p as PRESETS, r as classifyAction, s as loadPolicy, t as renderVerdict, u as loadConfig } from "./render-verdict-CK9sRJpa.js";
|
|
2
2
|
export { DEFAULT_CONFIG, PRESETS, classifyAction, classifyLocally, classifyWithModel, loadConfig, loadPolicy, parseActionRequest, parseModelVerdict, renderVerdict, resolveApiKey, resolveConfigPath };
|
|
@@ -5615,126 +5615,6 @@ function findPreset(id) {
|
|
|
5615
5615
|
return Object.hasOwn(PRESETS, id) ? PRESETS[id] : void 0;
|
|
5616
5616
|
}
|
|
5617
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
|
-
}
|
|
5737
|
-
//#endregion
|
|
5738
5618
|
//#region src/config/read-api-key-from-command.ts
|
|
5739
5619
|
function readApiKeyFromCommand(command, options = {}) {
|
|
5740
5620
|
const remainingMs = options.deadlineAt === void 0 ? 5e3 : options.deadlineAt - Date.now();
|
|
@@ -5797,10 +5677,15 @@ const SCOPE_SOURCE_KINDS = [
|
|
|
5797
5677
|
];
|
|
5798
5678
|
const DEFAULT_PROVIDER = findPreset("jev");
|
|
5799
5679
|
if (DEFAULT_PROVIDER === void 0) throw new Error("the jev preset is missing");
|
|
5680
|
+
const DEFAULT_DENIAL_BUDGET = {
|
|
5681
|
+
consecutive: 3,
|
|
5682
|
+
perSession: 20
|
|
5683
|
+
};
|
|
5800
5684
|
const DEFAULT_CONFIG = {
|
|
5801
5685
|
provider: DEFAULT_PROVIDER,
|
|
5802
5686
|
onFailure: "defer",
|
|
5803
|
-
minConfidence: .8
|
|
5687
|
+
minConfidence: .8,
|
|
5688
|
+
denialBudget: DEFAULT_DENIAL_BUDGET
|
|
5804
5689
|
};
|
|
5805
5690
|
function resolveConfigPath() {
|
|
5806
5691
|
const xdg = process.env["XDG_CONFIG_HOME"];
|
|
@@ -5821,11 +5706,14 @@ async function loadConfig(path = resolveConfigPath()) {
|
|
|
5821
5706
|
} catch {
|
|
5822
5707
|
throw new Error(`${path} is not valid JSON`);
|
|
5823
5708
|
}
|
|
5824
|
-
|
|
5825
|
-
return buildConfig(normalized.file, path, normalized.warnings);
|
|
5709
|
+
return buildConfig(json, path);
|
|
5826
5710
|
}
|
|
5827
5711
|
const text = string().min(1);
|
|
5828
5712
|
const positive = number().positive();
|
|
5713
|
+
const denialBudgetSchema = strictObject({
|
|
5714
|
+
consecutive: number().int().positive().optional(),
|
|
5715
|
+
perSession: number().int().positive().optional()
|
|
5716
|
+
});
|
|
5829
5717
|
const configFileSchema = strictObject({
|
|
5830
5718
|
classifiers: record(text, unknown()).optional(),
|
|
5831
5719
|
scopeSources: record(text, unknown()).optional(),
|
|
@@ -5833,7 +5721,8 @@ const configFileSchema = strictObject({
|
|
|
5833
5721
|
classifier: text.optional(),
|
|
5834
5722
|
judge: text.nullable().optional(),
|
|
5835
5723
|
minConfidence: number().min(.5).max(1).optional(),
|
|
5836
|
-
onFailure: _enum(["defer", "deny"]).optional()
|
|
5724
|
+
onFailure: _enum(["defer", "deny"]).optional(),
|
|
5725
|
+
denialBudget: denialBudgetSchema.optional()
|
|
5837
5726
|
}).optional(),
|
|
5838
5727
|
policy: strictObject({
|
|
5839
5728
|
rulesPath: text.nullable().optional(),
|
|
@@ -5841,11 +5730,11 @@ const configFileSchema = strictObject({
|
|
|
5841
5730
|
claudeSettingsPath: text.nullable().optional()
|
|
5842
5731
|
}).optional()
|
|
5843
5732
|
});
|
|
5844
|
-
function buildConfig(json, path
|
|
5733
|
+
function buildConfig(json, path) {
|
|
5845
5734
|
const parsed = configFileSchema.safeParse(json);
|
|
5846
5735
|
if (!parsed.success) throw new Error(`${path} is not a valid config: ${prettifyError(parsed.error)}`);
|
|
5847
5736
|
const file = parsed.data;
|
|
5848
|
-
const warnings = [
|
|
5737
|
+
const warnings = [];
|
|
5849
5738
|
const classifiers = buildRegistry(file.classifiers ?? {}, (id, entry) => parseClassifierEntry(id, entry));
|
|
5850
5739
|
const scopeSources = buildRegistry(file.scopeSources ?? {}, (id, entry) => parseScopeSourceEntry(id, entry));
|
|
5851
5740
|
warnings.push(...classifiers.dropped.map((line) => `${path}: classifiers.${line}`), ...scopeSources.dropped.map((line) => `${path}: scopeSources.${line}`));
|
|
@@ -5861,6 +5750,10 @@ function buildConfig(json, path, legacyWarnings) {
|
|
|
5861
5750
|
onFailure: decision.onFailure ?? DEFAULT_CONFIG.onFailure,
|
|
5862
5751
|
claudeSettingsPath: policy.claudeSettingsPath === null ? null : expandHomePath(policy.claudeSettingsPath),
|
|
5863
5752
|
minConfidence: decision.minConfidence ?? DEFAULT_CONFIG.minConfidence,
|
|
5753
|
+
denialBudget: {
|
|
5754
|
+
consecutive: decision.denialBudget?.consecutive ?? DEFAULT_DENIAL_BUDGET.consecutive,
|
|
5755
|
+
perSession: decision.denialBudget?.perSession ?? DEFAULT_DENIAL_BUDGET.perSession
|
|
5756
|
+
},
|
|
5864
5757
|
warnings
|
|
5865
5758
|
};
|
|
5866
5759
|
}
|
|
@@ -7204,6 +7097,7 @@ async function classifyAction(request, config, options = {}) {
|
|
|
7204
7097
|
const guidance = config.onFailure === "deny" ? await tryReadDenialGuidance() : null;
|
|
7205
7098
|
return {
|
|
7206
7099
|
verdict: config.onFailure === "deny" ? buildGuidedDeny("Classifier Unavailable", "Claude settings unreadable.", guidance) : null,
|
|
7100
|
+
decidingStage: "jev",
|
|
7207
7101
|
note: "Claude settings unreadable; classifier unavailable",
|
|
7208
7102
|
status: "failure",
|
|
7209
7103
|
unavailable: true
|
|
@@ -7212,17 +7106,20 @@ async function classifyAction(request, config, options = {}) {
|
|
|
7212
7106
|
const local = configured !== null && (configured.hard_deny.length > 0 || configured.soft_deny.length > 0) ? { kind: "escalate" } : classifyLocally(request);
|
|
7213
7107
|
if (local.kind === "allow") return {
|
|
7214
7108
|
verdict: { kind: "allow" },
|
|
7109
|
+
decidingStage: "local",
|
|
7215
7110
|
note: `allowed by ${local.exception} (local)`,
|
|
7216
7111
|
status: "allow"
|
|
7217
7112
|
};
|
|
7218
7113
|
if (options.localOnly === true) return {
|
|
7219
7114
|
verdict: null,
|
|
7115
|
+
decidingStage: "local",
|
|
7220
7116
|
note: `${request.toolName} needs the model tier, which this run skipped`,
|
|
7221
7117
|
status: "skipped"
|
|
7222
7118
|
};
|
|
7223
7119
|
const [outcome, guidance] = await Promise.all([classifyWithModel(request, config, options), tryReadDenialGuidance()]);
|
|
7224
7120
|
return {
|
|
7225
7121
|
...outcome,
|
|
7122
|
+
decidingStage: config.provider.protocol === "system-one" ? "jev" : "messages",
|
|
7226
7123
|
verdict: outcome.verdict?.kind === "deny" ? buildGuidedDeny(outcome.verdict.rule, outcome.verdict.reason, guidance) : outcome.verdict,
|
|
7227
7124
|
status: outcome.diagnostics?.status ?? outcome.verdict?.kind ?? "failure"
|
|
7228
7125
|
};
|
|
@@ -7300,4 +7197,4 @@ function renderVerdict(verdict) {
|
|
|
7300
7197
|
})).exhaustive();
|
|
7301
7198
|
}
|
|
7302
7199
|
//#endregion
|
|
7303
|
-
export { classifyWithModel as a, DEFAULT_CONFIG as c,
|
|
7200
|
+
export { classifyWithModel as a, DEFAULT_CONFIG as c, resolveApiKey as d, resolveConfigPath as f, string as g, strictObject as h, classifyLocally as i, DEFAULT_DENIAL_BUDGET as l, number as m, parseActionRequest as n, parseModelVerdict as o, PRESETS as p, classifyAction as r, loadPolicy as s, renderVerdict as t, loadConfig as u };
|