auto-mode 3.1.4 → 3.1.5

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,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as pickJudgeLane, E as updateJSONFile, S as CLI_LIMITS, T as resolveXDGDir, _ as readCheckout, a as resolveSessionScopePath, b as loadConfig, c as splitGitCommand, d as loadCheckoutFacts, f as writeActionDiagnostic, g as isUnresolvedWord, h as resolveShellPath, i as classifyAction, l as loadPolicy, m as collectShellCommands, n as readRuntime, o as loadSessionScope, p as toRepositorySlug, s as EMPTY_SESSION_SCOPE, t as parseActionRequest, u as resolveShippedPolicyDir, v as readGitEntry, w as M, x as runCapped, y as DEFAULT_SCOPE_SOURCES } from "./parse-action-request-DlsgKqiF.js";
2
+ import { C as pickJudgeLane, E as updateJSONFile, S as CLI_LIMITS, T as resolveXDGDir, _ as readCheckout, a as resolveSessionScopePath, b as loadConfig, c as splitGitCommand, d as loadCheckoutFacts, f as writeActionDiagnostic, g as isUnresolvedWord, h as resolveShellPath, i as classifyAction, l as loadPolicy, m as collectShellCommands, n as readRuntime, o as loadSessionScope, p as toRepositorySlug, s as EMPTY_SESSION_SCOPE, t as parseActionRequest, u as resolveShippedPolicyDir, v as readGitEntry, w as M, x as runCapped, y as DEFAULT_SCOPE_SOURCES } from "./parse-action-request-CHuF6LYV.js";
3
3
  import { a as literal, d as string, i as discriminatedUnion, l as record, p as unknown, r as boolean, s as number, u as strictObject } from "./schemas-B80Yxv6m.js";
4
4
  import { createHash, randomUUID } from "node:crypto";
5
5
  import { chmod, mkdir, open, readFile, realpath, stat } from "node:fs/promises";
package/dist/index.d.ts CHANGED
@@ -147,10 +147,10 @@ type MessageOrigin = (typeof MESSAGE_ORIGINS)[number];
147
147
  interface UserTask {
148
148
  readonly text: string;
149
149
  readonly origin: MessageOrigin;
150
+ readonly position?: 'opening' | undefined;
150
151
  }
151
152
  interface DirectUserMessage extends UserTask {
152
153
  readonly freshness?: 'stale' | undefined;
153
- readonly position?: 'opening' | undefined;
154
154
  }
155
155
  interface DelegatedTask {
156
156
  readonly text: string;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { b as loadConfig$1, i as classifyAction$1, n as readRuntime, r as readHostEnvironment, t as parseActionRequest } from "./parse-action-request-DlsgKqiF.js";
1
+ import { b as loadConfig$1, i as classifyAction$1, n as readRuntime, r as readHostEnvironment, t as parseActionRequest } from "./parse-action-request-CHuF6LYV.js";
2
2
  //#region src/index.ts
3
3
  function classifyAction(request, config, options = {}) {
4
4
  return classifyAction$1(request, config, {
@@ -251,6 +251,25 @@ async function tryUpdateDenialBudget(stderr, path, plan) {
251
251
  }
252
252
  }
253
253
  //#endregion
254
+ //#region src/scope/find-reply-channel.ts
255
+ function findReplyChannel(tool, input, channels) {
256
+ return channels.find((channel) => channel.tool === tool && input[channel.field] === channel.recipient) ?? null;
257
+ }
258
+ //#endregion
259
+ //#region src/bypass/classify-reply-call.ts
260
+ async function classifyReplyCall(call, scope, hasConfiguredDenies) {
261
+ const channel = scope === null ? null : findReplyChannel(call.toolName, call.toolInput, scope.replyChannels);
262
+ if (channel === null) return { kind: "none" };
263
+ if ((await import("./classify-reply-message-CdHLlNXA.js")).classifyReplyMessage(call.toolInput).kind !== "bypass") return { kind: "none" };
264
+ return hasConfiguredDenies ? {
265
+ kind: "exempt",
266
+ rules: ["Outbound Communication"]
267
+ } : {
268
+ kind: "bypass",
269
+ channel
270
+ };
271
+ }
272
+ //#endregion
254
273
  //#region src/state/resolve-xdg-dir.ts
255
274
  const BASE_DIRS = {
256
275
  config: {
@@ -1098,11 +1117,6 @@ const SCRATCH_PATHS = [
1098
1117
  "/dev/stderr"
1099
1118
  ];
1100
1119
  //#endregion
1101
- //#region src/scope/find-reply-channel.ts
1102
- function findReplyChannel(tool, input, channels) {
1103
- return channels.find((channel) => channel.tool === tool && input[channel.field] === channel.recipient) ?? null;
1104
- }
1105
- //#endregion
1106
1120
  //#region src/shell/is-unresolved-word.ts
1107
1121
  function isUnresolvedWord(word) {
1108
1122
  return /[$`]/u.test(word);
@@ -3091,7 +3105,7 @@ async function writeActionDiagnostic(payload, entry, host, warnings) {
3091
3105
  const agentKey = agentID === null ? payload.sessionID : `${payload.sessionID}\0${agentID}`;
3092
3106
  const isReviewed = entry.judge !== void 0 || entry.grant !== void 0;
3093
3107
  const record = {
3094
- schemaVersion: 8,
3108
+ schemaVersion: 9,
3095
3109
  time: (/* @__PURE__ */ new Date()).toISOString(),
3096
3110
  invocationID: entry.invocationID,
3097
3111
  sessionHash: toHash(payload.sessionID),
@@ -3105,7 +3119,8 @@ async function writeActionDiagnostic(payload, entry, host, warnings) {
3105
3119
  diagnostics: entry.diagnostics ?? null,
3106
3120
  judge: entry.judge === void 0 ? null : buildLoggedReview(entry.judge),
3107
3121
  grant: entry.grant === void 0 ? null : buildLoggedReview(entry.grant),
3108
- action: isReviewed ? buildLoggedAction(payload) : null
3122
+ action: isReviewed ? buildLoggedAction(payload) : null,
3123
+ taskEvidence: buildTaskEvidence(entry.taskContext ?? payload.decisionContext)
3109
3124
  };
3110
3125
  try {
3111
3126
  await mkdir(dirname(path), {
@@ -3159,6 +3174,15 @@ function hasSecret(value) {
3159
3174
  if (typeof value === "object" && value !== null) return Object.values(value).some((item) => hasSecret(item));
3160
3175
  return false;
3161
3176
  }
3177
+ function buildTaskEvidence(context) {
3178
+ if (context === void 0) return null;
3179
+ const message = context.lastDirectUserMessage;
3180
+ return {
3181
+ userMessage: message === null ? null : message.freshness ?? "current",
3182
+ originalUserTask: context.originalUserTask?.origin ?? null,
3183
+ delegatedTask: context.delegatedTask !== null
3184
+ };
3185
+ }
3162
3186
  //#endregion
3163
3187
  //#region src/diagnostics/write-judge-usage.ts
3164
3188
  async function writeJudgeUsage(usage, host, warnings) {
@@ -3946,6 +3970,31 @@ async function classifyWithJudge(payload, evidence, judge, options) {
3946
3970
  });
3947
3971
  }
3948
3972
  //#endregion
3973
+ //#region src/model/build-budget-omitted-context.ts
3974
+ function buildBudgetOmittedContext(context, field) {
3975
+ return {
3976
+ ...context,
3977
+ [field]: null,
3978
+ omittedTaskContext: [...context.omittedTaskContext.filter((item) => item.field !== field), {
3979
+ field,
3980
+ reason: "budget"
3981
+ }]
3982
+ };
3983
+ }
3984
+ //#endregion
3985
+ //#region src/model/build-task-context.ts
3986
+ function buildTaskContext(context) {
3987
+ let result = {
3988
+ ...context,
3989
+ omittedTaskContext: [...context.omittedTaskContext]
3990
+ };
3991
+ for (const field of ["originalUserTask", "delegatedTask"]) {
3992
+ const task = result[field];
3993
+ if (task !== null && Buffer.byteLength(task.text) > 4096) result = buildBudgetOmittedContext(result, field);
3994
+ }
3995
+ return result;
3996
+ }
3997
+ //#endregion
3949
3998
  //#region src/model/build-task-scope-summary.ts
3950
3999
  function buildTaskScopeSummary(scope) {
3951
4000
  return {
@@ -4230,31 +4279,6 @@ function resolveHooksPath(path, worktree, home) {
4230
4279
  return path.startsWith("~/") ? join(home, path.slice(2)) : resolve(worktree, path);
4231
4280
  }
4232
4281
  //#endregion
4233
- //#region src/model/build-budget-omitted-context.ts
4234
- function buildBudgetOmittedContext(context, field) {
4235
- return {
4236
- ...context,
4237
- [field]: null,
4238
- omittedTaskContext: [...context.omittedTaskContext.filter((item) => item.field !== field), {
4239
- field,
4240
- reason: "budget"
4241
- }]
4242
- };
4243
- }
4244
- //#endregion
4245
- //#region src/model/build-task-context.ts
4246
- function buildTaskContext(context) {
4247
- let result = {
4248
- ...context,
4249
- omittedTaskContext: [...context.omittedTaskContext]
4250
- };
4251
- for (const field of ["originalUserTask", "delegatedTask"]) {
4252
- const task = result[field];
4253
- if (task !== null && Buffer.byteLength(task.text) > 4096) result = buildBudgetOmittedContext(result, field);
4254
- }
4255
- return result;
4256
- }
4257
- //#endregion
4258
4282
  //#region src/git/parse-config-parameters.ts
4259
4283
  function parseConfigParameters(text) {
4260
4284
  const parameters = [];
@@ -4798,6 +4822,7 @@ function buildDecisionRequest(payload, policy, options) {
4798
4822
  const rulesSource = options.rulesSource ?? "replacement";
4799
4823
  const repositoryContext = options.repositoryContext ?? null;
4800
4824
  const mcpServers = options.mcpServers ?? [];
4825
+ const exemptRules = options.exemptRules ?? [];
4801
4826
  const rules = {};
4802
4827
  let tier = null;
4803
4828
  for (const section of policy.split(/(?=^## |^### )/mu)) if (section.startsWith("## HARD BLOCK rules")) tier = "hard";
@@ -4808,6 +4833,7 @@ function buildDecisionRequest(payload, policy, options) {
4808
4833
  const end = newline === -1 ? section.length : newline;
4809
4834
  const name = section.slice(4, end).trim();
4810
4835
  if (name === "" || Object.values(rules).some((rule) => rule.name === name)) throw new Error("Policy has an empty or duplicate rule");
4836
+ if (rulesSource === "shipped" && exemptRules.includes(name)) continue;
4811
4837
  rules[`rule_${Object.keys(rules).length}`] = {
4812
4838
  name,
4813
4839
  tier,
@@ -5033,6 +5059,30 @@ function classifyFailure(attempt, timeoutMs) {
5033
5059
  };
5034
5060
  }
5035
5061
  //#endregion
5062
+ //#region src/model/build-sized-decision-input.ts
5063
+ function buildSizedDecisionInput(model, request) {
5064
+ return buildFittedInput(buildFittedInput(buildInput(model, request.state, request.questions), "delegatedTask"), "originalUserTask");
5065
+ }
5066
+ function buildInput(model, state, questions) {
5067
+ const input = {
5068
+ model,
5069
+ state,
5070
+ questions
5071
+ };
5072
+ return {
5073
+ input,
5074
+ body: JSON.stringify(input)
5075
+ };
5076
+ }
5077
+ function buildFittedInput(sized, field) {
5078
+ const context = sized.input.state.taskContext;
5079
+ if (Buffer.byteLength(sized.body) <= 1e5 || context === void 0 || context[field] === null) return sized;
5080
+ return buildInput(sized.input.model, {
5081
+ ...sized.input.state,
5082
+ taskContext: buildBudgetOmittedContext(context, field)
5083
+ }, sized.input.questions);
5084
+ }
5085
+ //#endregion
5036
5086
  //#region src/model/collect-config-file-paths.ts
5037
5087
  function collectConfigFilePaths(command) {
5038
5088
  const values = splitShellCommand(command).segments.flatMap((segment) => splitShellTokens(segment.text).tokens.flatMap((token) => toWordValues(token))).flatMap((word) => {
@@ -5069,18 +5119,7 @@ function buildDecisionResponseSchema(choices) {
5069
5119
  //#endregion
5070
5120
  //#region src/model/send-decision.ts
5071
5121
  async function sendDecision(provider, apiKey, request, options) {
5072
- const input = {
5073
- model: provider.model,
5074
- state: { ...request.state },
5075
- questions: request.questions
5076
- };
5077
- let body = JSON.stringify(input);
5078
- for (const field of ["delegatedTask", "originalUserTask"]) {
5079
- const context = input.state.taskContext;
5080
- if (Buffer.byteLength(body) <= 1e5 || context === void 0 || context[field] === null) continue;
5081
- input.state.taskContext = buildBudgetOmittedContext(context, field);
5082
- body = JSON.stringify(input);
5083
- }
5122
+ const body = buildSizedDecisionInput(provider.model, request).body;
5084
5123
  const requestBytes = Buffer.byteLength(body);
5085
5124
  if (requestBytes > 1e5) throw new DecisionRequestError("request-too-large", requestBytes, "Decision input exceeds 100000 bytes; refusing to truncate the action or user message");
5086
5125
  let response;
@@ -5165,7 +5204,8 @@ async function classifyWithJev(payload, config, options) {
5165
5204
  repositoryContext,
5166
5205
  mcpServers,
5167
5206
  localHookConfig,
5168
- hookFacts
5207
+ hookFacts,
5208
+ exemptRules: options.exemptRules
5169
5209
  });
5170
5210
  const signal = createDeadlineSignal(config.provider.timeoutMs, options);
5171
5211
  if (signal === null) throw new DOMException("Evaluation deadline expired", "AbortError");
@@ -5175,6 +5215,7 @@ async function classifyWithJev(payload, config, options) {
5175
5215
  fetch: runtime.fetch
5176
5216
  });
5177
5217
  stage = "response";
5218
+ options.onTaskContext?.(buildSizedDecisionInput(config.provider.model, request).input.state.taskContext);
5178
5219
  return buildJevOutcome({
5179
5220
  kind: "reply",
5180
5221
  request,
@@ -5981,31 +6022,65 @@ async function probeLauncherSpawner(location, readTextFile) {
5981
6022
  //#region src/scope/resolve-opening-prompt.ts
5982
6023
  async function resolveOpeningPrompt(request, runtime) {
5983
6024
  const context = request.decisionContext;
5984
- const message = context?.lastDirectUserMessage;
5985
- if (context === void 0 || message?.position !== "opening") return request;
5986
- const { position: _, ...unmarked } = message;
6025
+ const task = context?.originalUserTask ?? null;
6026
+ const message = context?.lastDirectUserMessage ?? null;
6027
+ const opening = task?.position === "opening" ? task : message;
6028
+ if (context === void 0 || opening?.position !== "opening") return request;
5987
6029
  const isSpawned = await probeLauncherSpawner(runtime.host.env["AUTO_MODE_SCOPE_FILE"], runtime.readTextFile);
5988
6030
  return {
5989
6031
  ...request,
5990
- decisionContext: {
6032
+ decisionContext: isSpawned ? buildSpawnedContext(context, opening.text) : {
5991
6033
  ...context,
5992
- lastDirectUserMessage: isSpawned ? null : unmarked
6034
+ originalUserTask: task === null ? null : toUnmarked(task),
6035
+ lastDirectUserMessage: message === null ? null : toUnmarked(message)
5993
6036
  }
5994
6037
  };
5995
6038
  }
6039
+ function buildSpawnedContext(context, openingText) {
6040
+ const isTaskOpening = context.originalUserTask?.position === "opening";
6041
+ const message = context.lastDirectUserMessage;
6042
+ const isOmitted = context.omittedTaskContext.some((omission) => omission.field === "originalUserTask");
6043
+ return {
6044
+ ...context,
6045
+ originalUserTask: isTaskOpening ? null : context.originalUserTask,
6046
+ delegatedTask: context.agentID === null ? context.delegatedTask ?? {
6047
+ text: openingText,
6048
+ origin: "agent.spawn"
6049
+ } : context.delegatedTask,
6050
+ lastDirectUserMessage: message?.position === "opening" ? null : message,
6051
+ omittedTaskContext: isTaskOpening && !isOmitted ? [...context.omittedTaskContext, {
6052
+ field: "originalUserTask",
6053
+ reason: "unavailable"
6054
+ }] : context.omittedTaskContext
6055
+ };
6056
+ }
6057
+ function toUnmarked(value) {
6058
+ const { position: _, ...unmarked } = value;
6059
+ return unmarked;
6060
+ }
5996
6061
  //#endregion
5997
6062
  //#region src/classify-action.ts
5998
6063
  async function classifyAction(request, config, options) {
5999
- const stderr = options.stderr;
6000
- const host = options.runtime.host;
6001
6064
  const invocationID = randomUUID();
6002
- if (options.actionLog === true) await writeActionDiagnostic(request, {
6003
- invocationID,
6004
- status: "started"
6005
- }, host, stderr);
6006
6065
  const resolved = await resolveOpeningPrompt(request, options.runtime);
6007
- const outcome = options.budget === true ? await classifyWithBudget(resolved, config, options) : await classifyStages(resolved, config, options);
6008
- if (options.actionLog === true) await writeActionDiagnostic(request, {
6066
+ const offered = resolved.decisionContext === void 0 ? void 0 : buildTaskContext(resolved.decisionContext);
6067
+ if (options.actionLog === true) await writeActionDiagnostic(resolved, {
6068
+ invocationID,
6069
+ status: "started",
6070
+ taskContext: offered
6071
+ }, options.runtime.host, options.stderr);
6072
+ const received = { context: offered };
6073
+ const onTaskContext = (context) => {
6074
+ received.context = context;
6075
+ };
6076
+ const outcome = options.budget === true ? await classifyWithBudget(resolved, config, {
6077
+ ...options,
6078
+ onTaskContext
6079
+ }) : await classifyStages(resolved, config, {
6080
+ ...options,
6081
+ onTaskContext
6082
+ });
6083
+ if (options.actionLog === true) await writeActionDiagnostic(resolved, {
6009
6084
  invocationID,
6010
6085
  status: outcome.status,
6011
6086
  verdict: outcome.verdict?.kind ?? "defer",
@@ -6014,8 +6089,9 @@ async function classifyAction(request, config, options) {
6014
6089
  ...outcome.escalation === void 0 ? {} : { escalation: outcome.escalation },
6015
6090
  ...outcome.diagnostics === void 0 ? {} : { diagnostics: outcome.diagnostics },
6016
6091
  ...outcome.judge === void 0 ? {} : { judge: outcome.judge },
6017
- ...outcome.grant === void 0 ? {} : { grant: outcome.grant }
6018
- }, host, stderr);
6092
+ ...outcome.grant === void 0 ? {} : { grant: outcome.grant },
6093
+ taskContext: received.context
6094
+ }, options.runtime.host, options.stderr);
6019
6095
  return outcome;
6020
6096
  }
6021
6097
  async function classifyWithBudget(request, config, options) {
@@ -6052,7 +6128,8 @@ async function classifyStages(request, config, options) {
6052
6128
  local: { kind: "escalate" },
6053
6129
  checkout: null,
6054
6130
  scope: null,
6055
- grant: null
6131
+ grant: null,
6132
+ exemptRules: []
6056
6133
  };
6057
6134
  const reviewed = await classifyWithGrant(ctx, await runStages(ctx));
6058
6135
  if (reviewed.verdict?.kind !== "deny") return reviewed;
@@ -6236,9 +6313,12 @@ async function classifyEditBypass(ctx) {
6236
6313
  };
6237
6314
  }
6238
6315
  async function classifyReplyChannel(ctx) {
6239
- const channel = ctx.hasConfiguredDenies || ctx.scope === null ? null : findReplyChannel(ctx.request.toolName, ctx.request.toolInput, ctx.scope.replyChannels);
6240
- if (channel === null) return { kind: "pass" };
6241
- if ((await import("./classify-reply-message-CdHLlNXA.js")).classifyReplyMessage(ctx.request.toolInput).kind !== "bypass") return { kind: "pass" };
6316
+ const reply = await classifyReplyCall(ctx.request, ctx.scope, ctx.hasConfiguredDenies);
6317
+ if (reply.kind !== "bypass") return {
6318
+ kind: "pass",
6319
+ facts: { exemptRules: reply.kind === "exempt" ? reply.rules : [] }
6320
+ };
6321
+ const channel = reply.channel;
6242
6322
  return {
6243
6323
  kind: "decided",
6244
6324
  outcome: {
@@ -6267,7 +6347,8 @@ async function classifyJev(ctx) {
6267
6347
  taskScope: ctx.scope === null ? void 0 : buildTaskScopeSummary(ctx.scope),
6268
6348
  claudeSettings: ctx.claudeSettings,
6269
6349
  checkout: ctx.checkout ?? void 0,
6270
- readCheckout: ctx.readCheckout
6350
+ readCheckout: ctx.readCheckout,
6351
+ exemptRules: ctx.exemptRules
6271
6352
  });
6272
6353
  return {
6273
6354
  ...outcome,
@@ -6359,12 +6440,12 @@ const userTaskSchema = strictObject({
6359
6440
  "composer",
6360
6441
  "bridge",
6361
6442
  "sdk"
6362
- ])
6443
+ ]),
6444
+ position: literal("opening").optional()
6363
6445
  });
6364
6446
  const directUserMessageSchema = strictObject({
6365
6447
  ...userTaskSchema.shape,
6366
- freshness: literal("stale").optional(),
6367
- position: literal("opening").optional()
6448
+ freshness: literal("stale").optional()
6368
6449
  });
6369
6450
  const delegatedTaskSchema = strictObject({
6370
6451
  text: string(),
@@ -5,11 +5,11 @@ export type MessageOrigin = (typeof MESSAGE_ORIGINS)[number];
5
5
  export interface UserTask {
6
6
  readonly text: string;
7
7
  readonly origin: MessageOrigin;
8
+ readonly position?: 'opening' | undefined;
8
9
  }
9
10
 
10
11
  export interface DirectUserMessage extends UserTask {
11
12
  readonly freshness?: 'stale' | undefined;
12
- readonly position?: 'opening' | undefined;
13
13
  }
14
14
 
15
15
  interface DelegatedTask {
@@ -36,13 +36,16 @@ export function buildPromptContext(
36
36
  };
37
37
  }
38
38
 
39
- const message: UserTask = { text: input.text, origin };
40
39
  const isOpeningPrompt = previous?.awaitsOpeningPrompt ?? true;
41
40
 
41
+ const message: UserTask = isOpeningPrompt
42
+ ? { text: input.text, origin, position: 'opening' }
43
+ : { text: input.text, origin };
44
+
42
45
  return {
43
46
  originalUserTask:
44
47
  previous?.originalUserTask ?? (previous?.canCaptureOriginal === true ? message : null),
45
- lastDirectUserMessage: isOpeningPrompt ? { ...message, position: 'opening' } : message,
48
+ lastDirectUserMessage: message,
46
49
  canCaptureOriginal: previous?.canCaptureOriginal ?? false,
47
50
  awaitsOpeningPrompt: false,
48
51
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auto-mode",
3
- "version": "3.1.4",
3
+ "version": "3.1.5",
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",
@@ -52,6 +52,7 @@
52
52
  "eval:question-shape": "bun --no-env-file --conditions=auto-mode-eval evals/runners/run-question-shape-evaluation.ts",
53
53
  "eval:relay-consent": "bun --no-env-file --conditions=auto-mode-eval evals/runners/run-relay-consent-evaluation.ts",
54
54
  "eval:second-judge": "bun --no-env-file --conditions=auto-mode-eval evals/runners/run-second-judge-evaluation.ts",
55
+ "eval:spawned-task": "bun --no-env-file --conditions=auto-mode-eval evals/runners/run-spawned-task-evaluation.ts",
55
56
  "eval:stale-consent": "bun --no-env-file --conditions=auto-mode-eval evals/runners/run-stale-consent-evaluation.ts",
56
57
  "format": "oxfmt . && format-codemod --quiet .",
57
58
  "format:check": "oxfmt --check . && format-codemod --check --quiet .",