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 { d as resolveConfigPath, f as normalizeConfigFile, l as loadConfig, n as parseActionRequest, r as classifyAction, s as loadPolicy, t as renderVerdict } from "./render-verdict-CAg3AKWH.js";
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 stateHome = process.env["XDG_STATE_HOME"];
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: 2,
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 outcome = await classifyAction(request, config, {
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
- if (outcome.verdict !== null) writeVerdict(outcome.verdict);
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: outcome.verdict?.kind ?? "defer",
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
- return printNote(explain || outcome.unavailable === true, outcome.note);
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, l as loadConfig, n as parseActionRequest, o as parseModelVerdict, p as PRESETS, r as classifyAction, s as loadPolicy, t as renderVerdict, u as resolveApiKey } from "./render-verdict-CAg3AKWH.js";
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, resolveConfigPath as d, normalizeConfigFile as f, classifyLocally as i, loadConfig as l, parseActionRequest as n, parseModelVerdict as o, PRESETS as p, classifyAction as r, loadPolicy as s, renderVerdict as t, resolveApiKey as u };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auto-mode",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "A permission classifier library and Claude Code mod, so agents that are not Claude can run unattended",
5
5
  "keywords": [
6
6
  "agent",