auto-mode 0.7.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/dist/cli.js
CHANGED
|
@@ -1,11 +1,147 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
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
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;
|
|
@@ -50,18 +186,19 @@ async function writeMigratedConfig(path) {
|
|
|
50
186
|
//#endregion
|
|
51
187
|
//#region src/diagnostics/write-action-diagnostic.ts
|
|
52
188
|
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");
|
|
189
|
+
const path = process.env["AUTO_MODE_DIAGNOSTICS_PATH"] ?? join(resolveStateDir(), "actions.jsonl");
|
|
56
190
|
if (path === "") return;
|
|
57
191
|
const record = {
|
|
58
|
-
schemaVersion:
|
|
192
|
+
schemaVersion: 3,
|
|
59
193
|
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
60
194
|
invocationID: entry.invocationID,
|
|
61
195
|
sessionHash: toHash(payload.sessionID),
|
|
62
196
|
actionHash: payload.toolUseID === void 0 ? null : toHash(payload.toolUseID),
|
|
63
197
|
status: entry.status,
|
|
64
198
|
verdict: entry.verdict ?? null,
|
|
199
|
+
decidingStage: entry.decidingStage ?? null,
|
|
200
|
+
denials: entry.denials ?? null,
|
|
201
|
+
escalation: entry.escalation ?? false,
|
|
65
202
|
diagnostics: entry.diagnostics ?? null
|
|
66
203
|
};
|
|
67
204
|
try {
|
|
@@ -151,24 +288,50 @@ async function run(explain, localOnly, jevOnly, deadline) {
|
|
|
151
288
|
process.on("SIGINT", stopEvaluation);
|
|
152
289
|
}
|
|
153
290
|
try {
|
|
154
|
-
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, {
|
|
155
296
|
localOnly,
|
|
156
297
|
deadlineAt,
|
|
157
298
|
signal: controller.signal
|
|
158
|
-
})
|
|
159
|
-
|
|
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);
|
|
160
309
|
await writeActionDiagnostic(request, {
|
|
161
310
|
invocationID,
|
|
162
311
|
status: outcome.status,
|
|
163
|
-
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,
|
|
164
319
|
...outcome.diagnostics === void 0 ? {} : { diagnostics: outcome.diagnostics }
|
|
165
320
|
});
|
|
166
|
-
|
|
321
|
+
const note = plan.escalation ? "denial budget exhausted; the user decides this action" : outcome.note;
|
|
322
|
+
return printNote(explain || outcome.unavailable === true, note);
|
|
167
323
|
} finally {
|
|
168
324
|
process.off("SIGTERM", stopEvaluation);
|
|
169
325
|
process.off("SIGINT", stopEvaluation);
|
|
170
326
|
}
|
|
171
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
|
+
}
|
|
172
335
|
function writeVerdict(verdict) {
|
|
173
336
|
process.stdout.write(renderVerdict(verdict));
|
|
174
337
|
}
|
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 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 };
|
|
@@ -5797,10 +5797,15 @@ const SCOPE_SOURCE_KINDS = [
|
|
|
5797
5797
|
];
|
|
5798
5798
|
const DEFAULT_PROVIDER = findPreset("jev");
|
|
5799
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
|
+
};
|
|
5800
5804
|
const DEFAULT_CONFIG = {
|
|
5801
5805
|
provider: DEFAULT_PROVIDER,
|
|
5802
5806
|
onFailure: "defer",
|
|
5803
|
-
minConfidence: .8
|
|
5807
|
+
minConfidence: .8,
|
|
5808
|
+
denialBudget: DEFAULT_DENIAL_BUDGET
|
|
5804
5809
|
};
|
|
5805
5810
|
function resolveConfigPath() {
|
|
5806
5811
|
const xdg = process.env["XDG_CONFIG_HOME"];
|
|
@@ -5826,6 +5831,10 @@ async function loadConfig(path = resolveConfigPath()) {
|
|
|
5826
5831
|
}
|
|
5827
5832
|
const text = string().min(1);
|
|
5828
5833
|
const positive = number().positive();
|
|
5834
|
+
const denialBudgetSchema = strictObject({
|
|
5835
|
+
consecutive: number().int().positive().optional(),
|
|
5836
|
+
perSession: number().int().positive().optional()
|
|
5837
|
+
});
|
|
5829
5838
|
const configFileSchema = strictObject({
|
|
5830
5839
|
classifiers: record(text, unknown()).optional(),
|
|
5831
5840
|
scopeSources: record(text, unknown()).optional(),
|
|
@@ -5833,7 +5842,8 @@ const configFileSchema = strictObject({
|
|
|
5833
5842
|
classifier: text.optional(),
|
|
5834
5843
|
judge: text.nullable().optional(),
|
|
5835
5844
|
minConfidence: number().min(.5).max(1).optional(),
|
|
5836
|
-
onFailure: _enum(["defer", "deny"]).optional()
|
|
5845
|
+
onFailure: _enum(["defer", "deny"]).optional(),
|
|
5846
|
+
denialBudget: denialBudgetSchema.optional()
|
|
5837
5847
|
}).optional(),
|
|
5838
5848
|
policy: strictObject({
|
|
5839
5849
|
rulesPath: text.nullable().optional(),
|
|
@@ -5861,6 +5871,10 @@ function buildConfig(json, path, legacyWarnings) {
|
|
|
5861
5871
|
onFailure: decision.onFailure ?? DEFAULT_CONFIG.onFailure,
|
|
5862
5872
|
claudeSettingsPath: policy.claudeSettingsPath === null ? null : expandHomePath(policy.claudeSettingsPath),
|
|
5863
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
|
|
5877
|
+
},
|
|
5864
5878
|
warnings
|
|
5865
5879
|
};
|
|
5866
5880
|
}
|
|
@@ -7204,6 +7218,7 @@ async function classifyAction(request, config, options = {}) {
|
|
|
7204
7218
|
const guidance = config.onFailure === "deny" ? await tryReadDenialGuidance() : null;
|
|
7205
7219
|
return {
|
|
7206
7220
|
verdict: config.onFailure === "deny" ? buildGuidedDeny("Classifier Unavailable", "Claude settings unreadable.", guidance) : null,
|
|
7221
|
+
decidingStage: "jev",
|
|
7207
7222
|
note: "Claude settings unreadable; classifier unavailable",
|
|
7208
7223
|
status: "failure",
|
|
7209
7224
|
unavailable: true
|
|
@@ -7212,17 +7227,20 @@ async function classifyAction(request, config, options = {}) {
|
|
|
7212
7227
|
const local = configured !== null && (configured.hard_deny.length > 0 || configured.soft_deny.length > 0) ? { kind: "escalate" } : classifyLocally(request);
|
|
7213
7228
|
if (local.kind === "allow") return {
|
|
7214
7229
|
verdict: { kind: "allow" },
|
|
7230
|
+
decidingStage: "local",
|
|
7215
7231
|
note: `allowed by ${local.exception} (local)`,
|
|
7216
7232
|
status: "allow"
|
|
7217
7233
|
};
|
|
7218
7234
|
if (options.localOnly === true) return {
|
|
7219
7235
|
verdict: null,
|
|
7236
|
+
decidingStage: "local",
|
|
7220
7237
|
note: `${request.toolName} needs the model tier, which this run skipped`,
|
|
7221
7238
|
status: "skipped"
|
|
7222
7239
|
};
|
|
7223
7240
|
const [outcome, guidance] = await Promise.all([classifyWithModel(request, config, options), tryReadDenialGuidance()]);
|
|
7224
7241
|
return {
|
|
7225
7242
|
...outcome,
|
|
7243
|
+
decidingStage: config.provider.protocol === "system-one" ? "jev" : "messages",
|
|
7226
7244
|
verdict: outcome.verdict?.kind === "deny" ? buildGuidedDeny(outcome.verdict.rule, outcome.verdict.reason, guidance) : outcome.verdict,
|
|
7227
7245
|
status: outcome.diagnostics?.status ?? outcome.verdict?.kind ?? "failure"
|
|
7228
7246
|
};
|
|
@@ -7300,4 +7318,4 @@ function renderVerdict(verdict) {
|
|
|
7300
7318
|
})).exhaustive();
|
|
7301
7319
|
}
|
|
7302
7320
|
//#endregion
|
|
7303
|
-
export { classifyWithModel as a, DEFAULT_CONFIG as c,
|
|
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 };
|