auto-mode 0.14.0 → 0.15.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.
@@ -1,22 +1,10 @@
1
1
  import { t as getEditFields } from "./get-edit-fields-BtAvHIsq.js";
2
2
  import { lstat, readFile, realpath, stat } from "node:fs/promises";
3
3
  import { basename, dirname, isAbsolute, join, matchesGlob, normalize, parse, relative, resolve } from "node:path";
4
- import { homedir } from "node:os";
5
4
  import { spawn } from "node:child_process";
5
+ import { homedir } from "node:os";
6
6
  import { existsSync } from "node:fs";
7
7
  import { createHash } from "node:crypto";
8
- //#region src/bypass/resolve-edit-target.ts
9
- async function resolveEditTarget(path) {
10
- const missing = [];
11
- for (let current = path;; current = dirname(current)) try {
12
- const real = await realpath(current);
13
- return join(real, ...missing.toReversed());
14
- } catch {
15
- if (await lstat(current).catch(() => null) !== null || dirname(current) === current) return null;
16
- missing.push(basename(current));
17
- }
18
- }
19
- //#endregion
20
8
  //#region node_modules/.bun/zod@4.6.2/node_modules/zod/v4/core/util.js
21
9
  function getEnumValues(entries) {
22
10
  const numericValues = Object.values(entries).filter((v) => typeof v === "number");
@@ -5586,9 +5574,15 @@ function findPreset(id) {
5586
5574
  return Object.hasOwn(PRESETS, id) ? PRESETS[id] : void 0;
5587
5575
  }
5588
5576
  //#endregion
5577
+ //#region src/config/to-timer-delay.ts
5578
+ function toTimerDelay(ms) {
5579
+ return Math.max(1, Math.ceil(ms));
5580
+ }
5581
+ //#endregion
5589
5582
  //#region src/config/read-api-key-from-command.ts
5590
5583
  function readApiKeyFromCommand(command, options = {}) {
5591
- const remainingMs = options.deadlineAt === void 0 ? 5e3 : options.deadlineAt - Date.now();
5584
+ const now = options.now ?? Date.now;
5585
+ const remainingMs = options.deadlineAt === void 0 ? 5e3 : options.deadlineAt - now();
5592
5586
  if (remainingMs <= 0 || options.signal?.aborted === true) return Promise.resolve(null);
5593
5587
  return new Promise((resolve) => {
5594
5588
  const grouped = process.platform !== "win32";
@@ -5598,7 +5592,11 @@ function readApiKeyFromCommand(command, options = {}) {
5598
5592
  "ignore",
5599
5593
  "pipe",
5600
5594
  "ignore"
5601
- ]
5595
+ ],
5596
+ ...options.host === void 0 ? {} : { env: {
5597
+ HOME: options.host.home,
5598
+ ...options.host.env
5599
+ } }
5602
5600
  });
5603
5601
  let output = "";
5604
5602
  let settled = false;
@@ -5606,7 +5604,7 @@ function readApiKeyFromCommand(command, options = {}) {
5606
5604
  const setResult = (value) => {
5607
5605
  if (!settled) {
5608
5606
  settled = true;
5609
- clearTimeout(timer);
5607
+ timer.removeEventListener("abort", stopChild);
5610
5608
  options.signal?.removeEventListener("abort", stopChild);
5611
5609
  resolve(value);
5612
5610
  }
@@ -5621,8 +5619,10 @@ function readApiKeyFromCommand(command, options = {}) {
5621
5619
  child.kill("SIGKILL");
5622
5620
  }
5623
5621
  };
5624
- const timer = setTimeout(stopChild, Math.min(5e3, remainingMs));
5622
+ const timer = (options.timeout ?? ((ms) => AbortSignal.timeout(ms)))(toTimerDelay(Math.min(5e3, remainingMs)));
5623
+ timer.addEventListener("abort", stopChild, { once: true });
5625
5624
  options.signal?.addEventListener("abort", stopChild, { once: true });
5625
+ if (timer.aborted) stopChild();
5626
5626
  child.stdout.setEncoding("utf8");
5627
5627
  child.stdout.on("data", (chunk) => {
5628
5628
  if (stopped) return;
@@ -5639,6 +5639,23 @@ function readApiKeyFromCommand(command, options = {}) {
5639
5639
  });
5640
5640
  }
5641
5641
  //#endregion
5642
+ //#region src/containment/scratch-paths.ts
5643
+ const SCRATCH_PATHS = [
5644
+ "/tmp",
5645
+ "/dev/null",
5646
+ "/dev/stdout",
5647
+ "/dev/stderr"
5648
+ ];
5649
+ //#endregion
5650
+ //#region src/config/read-host-environment.ts
5651
+ function readHostEnvironment() {
5652
+ return {
5653
+ env: process.env,
5654
+ home: homedir(),
5655
+ scratchPaths: SCRATCH_PATHS
5656
+ };
5657
+ }
5658
+ //#endregion
5642
5659
  //#region src/config/config.ts
5643
5660
  const SCOPE_SOURCE_KINDS = [
5644
5661
  "cwd",
@@ -5664,12 +5681,13 @@ const DEFAULT_CONFIG = {
5664
5681
  denialBudget: DEFAULT_DENIAL_BUDGET,
5665
5682
  scopeSources: DEFAULT_SCOPE_SOURCES
5666
5683
  };
5667
- function resolveConfigPath() {
5668
- const xdg = process.env["XDG_CONFIG_HOME"];
5669
- const base = xdg !== void 0 && xdg !== "" ? xdg : join(homedir(), ".config");
5684
+ function resolveConfigPath(host = readHostEnvironment()) {
5685
+ const xdg = host.env["XDG_CONFIG_HOME"];
5686
+ const base = xdg !== void 0 && xdg !== "" ? xdg : join(host.home, ".config");
5670
5687
  return join(base, "auto-mode", "config.json");
5671
5688
  }
5672
- async function loadConfig(path = resolveConfigPath()) {
5689
+ async function loadConfig(configPath, host = readHostEnvironment()) {
5690
+ const path = configPath ?? resolveConfigPath(host);
5673
5691
  let raw;
5674
5692
  try {
5675
5693
  raw = await readFile(path, "utf8");
@@ -5683,7 +5701,7 @@ async function loadConfig(path = resolveConfigPath()) {
5683
5701
  } catch {
5684
5702
  throw new Error(`${path} is not valid JSON`);
5685
5703
  }
5686
- return buildConfig(json, path);
5704
+ return buildConfig(json, path, host.home);
5687
5705
  }
5688
5706
  const text = string().min(1);
5689
5707
  const positive = number().positive();
@@ -5707,13 +5725,13 @@ const configFileSchema = strictObject({
5707
5725
  claudeSettingsPath: text.nullable().optional()
5708
5726
  }).optional()
5709
5727
  });
5710
- function buildConfig(json, path) {
5728
+ function buildConfig(json, path, home) {
5711
5729
  const parsed = configFileSchema.safeParse(json);
5712
5730
  if (!parsed.success) throw new Error(`${path} is not a valid config: ${prettifyError(parsed.error)}`);
5713
5731
  const file = parsed.data;
5714
5732
  const warnings = [];
5715
5733
  const classifiers = buildRegistry(file.classifiers ?? {}, (id, entry) => parseClassifierEntry(id, entry));
5716
- const scopeSources = buildRegistry(file.scopeSources ?? {}, (id, entry) => parseScopeSourceEntry(id, entry));
5734
+ const scopeSources = buildRegistry(file.scopeSources ?? {}, (id, entry) => parseScopeSourceEntry(id, entry, home));
5717
5735
  const configuredSources = file.scopeSources === void 0 ? null : scopeSources.entries;
5718
5736
  warnings.push(...classifiers.dropped.map((line) => `${path}: classifiers.${line}`), ...scopeSources.dropped.map((line) => `${path}: scopeSources.${line}`), ...collectScopeWarnings(configuredSources).map((line) => `${path}: scopeSources ${line}`));
5719
5737
  const decision = file.decision ?? {};
@@ -5723,10 +5741,10 @@ function buildConfig(json, path) {
5723
5741
  provider: resolveRole("classifier", decision.classifier ?? "jev", classifiers.entries, classifiers.droppedIDs, path),
5724
5742
  judge: judgeID === null ? null : resolveRole("judge", judgeID, classifiers.entries, classifiers.droppedIDs, path),
5725
5743
  scopeSources: configuredSources ?? DEFAULT_SCOPE_SOURCES,
5726
- classifierPath: expandHomePath(policy.frameworkPath ?? void 0),
5727
- rulesPath: expandHomePath(policy.rulesPath ?? void 0),
5744
+ classifierPath: expandHomePath(policy.frameworkPath ?? void 0, home),
5745
+ rulesPath: expandHomePath(policy.rulesPath ?? void 0, home),
5728
5746
  onFailure: decision.onFailure ?? DEFAULT_CONFIG.onFailure,
5729
- claudeSettingsPath: policy.claudeSettingsPath === null ? null : expandHomePath(policy.claudeSettingsPath),
5747
+ claudeSettingsPath: policy.claudeSettingsPath === null ? null : expandHomePath(policy.claudeSettingsPath, home),
5730
5748
  minConfidence: decision.minConfidence ?? DEFAULT_CONFIG.minConfidence,
5731
5749
  denialBudget: {
5732
5750
  consecutive: decision.denialBudget?.consecutive ?? DEFAULT_DENIAL_BUDGET.consecutive,
@@ -5801,14 +5819,14 @@ const scopeSourceEntrySchema = strictObject({
5801
5819
  kind: text.optional(),
5802
5820
  paths: array(text).min(1).optional()
5803
5821
  });
5804
- function parseScopeSourceEntry(id, entry) {
5822
+ function parseScopeSourceEntry(id, entry, home) {
5805
5823
  const parsed = scopeSourceEntrySchema.safeParse(entry);
5806
5824
  if (!parsed.success) return formatFirstIssue(parsed.error.issues);
5807
5825
  const kind = SCOPE_SOURCE_KINDS.find((known) => known === (parsed.data.kind ?? id));
5808
5826
  if (kind === void 0) return `unknown kind '${parsed.data.kind ?? id}'; known kinds are ${SCOPE_SOURCE_KINDS.join(", ")}`;
5809
5827
  if (kind === "globs") return parsed.data.paths === void 0 ? "kind 'globs' needs paths" : {
5810
5828
  kind,
5811
- paths: parsed.data.paths.map((glob) => expandHomePath(glob) ?? glob)
5829
+ paths: parsed.data.paths.map((glob) => expandHomePath(glob, home) ?? glob)
5812
5830
  };
5813
5831
  return parsed.data.paths === void 0 ? { kind } : `kind '${kind}' takes no paths`;
5814
5832
  }
@@ -5829,18 +5847,113 @@ function parseBuiltIn(id) {
5829
5847
  if (typeof resolved === "string") throw new TypeError(`built-in kind ${id} does not resolve: ${resolved}`);
5830
5848
  return resolved;
5831
5849
  }
5832
- function expandHomePath(path) {
5833
- return path?.startsWith("~/") === true ? join(homedir(), path.slice(2)) : path;
5850
+ function expandHomePath(path, home) {
5851
+ return path?.startsWith("~/") === true ? join(home, path.slice(2)) : path;
5834
5852
  }
5835
5853
  function resolveApiKey(provider, options = {}) {
5836
- const fromEnv = provider.apiKeyEnv === void 0 ? void 0 : process.env[provider.apiKeyEnv];
5854
+ const host = options.host ?? readHostEnvironment();
5855
+ const fromEnv = provider.apiKeyEnv === void 0 ? void 0 : host.env[provider.apiKeyEnv];
5837
5856
  if (fromEnv !== void 0 && fromEnv !== "") return Promise.resolve(fromEnv);
5838
5857
  if (provider.apiKeyCommand === void 0 || provider.apiKeyCommand === "") return Promise.resolve(null);
5839
- return readApiKeyFromCommand(provider.apiKeyCommand, options);
5858
+ return readApiKeyFromCommand(provider.apiKeyCommand, {
5859
+ ...options,
5860
+ host
5861
+ });
5862
+ }
5863
+ //#endregion
5864
+ //#region src/scope/find-checkout.ts
5865
+ const GIT_OVERRIDES$1 = [
5866
+ "GIT_DIR",
5867
+ "GIT_WORK_TREE",
5868
+ "GIT_COMMON_DIR"
5869
+ ];
5870
+ async function findCheckout(start, env) {
5871
+ if (GIT_OVERRIDES$1.some((name) => env[name] !== void 0)) return null;
5872
+ for (let directory = start;; directory = dirname(directory)) {
5873
+ const gitPath = join(directory, ".git");
5874
+ const info = await stat(gitPath).catch(() => null);
5875
+ if (info !== null) {
5876
+ const gitDir = info.isDirectory() ? gitPath : await findLinkedGitDir(directory, gitPath);
5877
+ return gitDir === null ? null : {
5878
+ worktree: directory,
5879
+ commonDir: await findCommonDir(gitDir)
5880
+ };
5881
+ }
5882
+ if (dirname(directory) === directory) return null;
5883
+ }
5884
+ }
5885
+ async function findLinkedGitDir(directory, gitPath) {
5886
+ const link = await readFile(gitPath, "utf8").catch(() => "");
5887
+ const path = /^gitdir: (?<path>.+)$/mu.exec(link)?.groups?.["path"];
5888
+ return path === void 0 ? null : resolve(directory, path.trim());
5889
+ }
5890
+ async function findCommonDir(gitDir) {
5891
+ const common = await readFile(join(gitDir, "commondir"), "utf8").catch(() => null);
5892
+ return common === null ? gitDir : resolve(gitDir, common.trim());
5893
+ }
5894
+ //#endregion
5895
+ //#region src/state/resolve-state-dir.ts
5896
+ function resolveStateDir(host) {
5897
+ const stateHome = host.env["XDG_STATE_HOME"];
5898
+ const base = stateHome === void 0 || stateHome === "" ? join(host.home, ".local", "state") : stateHome;
5899
+ return join(base, "auto-mode");
5900
+ }
5901
+ //#endregion
5902
+ //#region src/bypass/resolve-edit-target.ts
5903
+ async function resolveEditTarget(path) {
5904
+ const missing = [];
5905
+ for (let current = path;; current = dirname(current)) try {
5906
+ const real = await realpath(current);
5907
+ return join(real, ...missing.toReversed());
5908
+ } catch {
5909
+ if (await lstat(current).catch(() => null) !== null || dirname(current) === current) return null;
5910
+ missing.push(basename(current));
5911
+ }
5912
+ }
5913
+ //#endregion
5914
+ //#region src/bypass/try-classify-edit.ts
5915
+ const DISK_EDIT_FILE_READER = {
5916
+ resolveEditTarget,
5917
+ findCheckout,
5918
+ readFile: (path) => readFile(path, "utf8")
5919
+ };
5920
+ const GIT_OVERRIDES = [
5921
+ "GIT_DIR",
5922
+ "GIT_WORK_TREE",
5923
+ "GIT_COMMON_DIR"
5924
+ ];
5925
+ async function tryClassifyEdit(request, scope, host, files = DISK_EDIT_FILE_READER) {
5926
+ const fields = getEditFields(request.toolName);
5927
+ const path = fields === null ? void 0 : request.toolInput[fields.path];
5928
+ if (typeof path !== "string" || path === "") return null;
5929
+ try {
5930
+ const ownDirs = [dirname(resolveConfigPath(host)), resolveStateDir(host)];
5931
+ const requested = resolve(request.cwd, path);
5932
+ const [target, worktrees, protectedDirs] = await Promise.all([
5933
+ files.resolveEditTarget(requested),
5934
+ Promise.all(scope.worktrees.map((worktree) => files.resolveEditTarget(worktree))),
5935
+ Promise.all(ownDirs.map((dir) => files.resolveEditTarget(dir)))
5936
+ ]);
5937
+ if (target === null || GIT_OVERRIDES.some((name) => host.env[name] !== void 0)) return null;
5938
+ const [checkout, current] = await Promise.all([files.findCheckout(dirname(target), host.env), request.toolName === "Edit" ? files.readFile(target).catch(() => null) : null]);
5939
+ return (await import("./classify-edit-LpoeenGt.js")).classifyEdit({
5940
+ toolName: request.toolName,
5941
+ toolInput: request.toolInput,
5942
+ requested,
5943
+ target,
5944
+ checkout: checkout?.worktree ?? null,
5945
+ current
5946
+ }, {
5947
+ worktrees: worktrees.filter((worktree) => worktree !== null),
5948
+ protectedDirs: [...ownDirs, ...protectedDirs.filter((dir) => dir !== null)]
5949
+ });
5950
+ } catch {
5951
+ return null;
5952
+ }
5840
5953
  }
5841
5954
  //#endregion
5842
5955
  //#region src/config/load-claude-rules.ts
5843
- async function loadClaudeRules(path) {
5956
+ async function loadClaudeRules(path, host) {
5844
5957
  const empty = {
5845
5958
  environment: [],
5846
5959
  allow: [],
@@ -5848,8 +5961,8 @@ async function loadClaudeRules(path) {
5848
5961
  hard_deny: []
5849
5962
  };
5850
5963
  if (path === null) return empty;
5851
- const configDir = process.env["CLAUDE_CONFIG_DIR"];
5852
- const base = configDir === void 0 || configDir === "" ? join(homedir(), ".claude") : configDir;
5964
+ const configDir = host.env["CLAUDE_CONFIG_DIR"];
5965
+ const base = configDir === void 0 || configDir === "" ? join(host.home, ".claude") : configDir;
5853
5966
  const settingsPath = path ?? join(base, "settings.json");
5854
5967
  let raw;
5855
5968
  try {
@@ -5987,13 +6100,13 @@ function toRepositorySlug(reference) {
5987
6100
  }
5988
6101
  //#endregion
5989
6102
  //#region src/containment/collect-scope-findings.ts
5990
- function collectScopeFindings(action, scope) {
6103
+ function collectScopeFindings(action, scope, scratchPaths = SCRATCH_PATHS) {
5991
6104
  const filePath = action.input["file_path"] ?? action.input["notebook_path"];
5992
6105
  const command = action.input["command"];
5993
6106
  const findings = [
5994
- ...FILE_WRITE_TOOLS.has(action.tool) && typeof filePath === "string" ? collectTargetFindings(action.cwd, filePath, scope) : [],
6107
+ ...FILE_WRITE_TOOLS.has(action.tool) && typeof filePath === "string" ? collectTargetFindings(action.cwd, filePath, scope, scratchPaths) : [],
5995
6108
  ...action.tool.startsWith("mcp__") ? collectToolFindings(action, scope) : [],
5996
- ...action.tool === "Bash" && typeof command === "string" ? collectCommandFindings(command, action.cwd, scope) : []
6109
+ ...action.tool === "Bash" && typeof command === "string" ? collectCommandFindings(command, action.cwd, scope, scratchPaths) : []
5997
6110
  ];
5998
6111
  return findings.filter((finding, index) => findings.findIndex((other) => other.kind === finding.kind && other.target === finding.target) === index);
5999
6112
  }
@@ -6003,9 +6116,9 @@ const FILE_WRITE_TOOLS = /* @__PURE__ */ new Set([
6003
6116
  "Write",
6004
6117
  "NotebookEdit"
6005
6118
  ]);
6006
- function collectTargetFindings(directory, target, scope) {
6119
+ function collectTargetFindings(directory, target, scope, scratchPaths) {
6007
6120
  const path = resolvePath(directory, target, scope.home);
6008
- return path === null ? [] : collectPathFindings(path, scope);
6121
+ return path === null ? [] : collectPathFindings(path, scope, scratchPaths);
6009
6122
  }
6010
6123
  const CREDENTIAL_PATHS = [
6011
6124
  ".ssh",
@@ -6016,12 +6129,12 @@ const CREDENTIAL_PATHS = [
6016
6129
  ".npmrc",
6017
6130
  ".config/gh"
6018
6131
  ];
6019
- function collectPathFindings(path, scope) {
6132
+ function collectPathFindings(path, scope, scratchPaths) {
6020
6133
  if (CREDENTIAL_PATHS.some((entry) => isUnder(path, join(scope.home, entry)))) return [{
6021
6134
  kind: "credential",
6022
6135
  target: path
6023
6136
  }];
6024
- return isInScope(path, scope) ? [] : [{
6137
+ return isInScope(path, scope, scratchPaths) ? [] : [{
6025
6138
  kind: "path",
6026
6139
  target: path
6027
6140
  }];
@@ -6052,24 +6165,24 @@ const DESTINATION_WRITERS = /* @__PURE__ */ new Set([
6052
6165
  "rsync",
6053
6166
  "ln"
6054
6167
  ]);
6055
- function collectCommandFindings(command, cwd, scope) {
6168
+ function collectCommandFindings(command, cwd, scope, scratchPaths) {
6056
6169
  const findings = [];
6057
6170
  const hasPipe = /(?<!\|)\|(?!\|)/u.test(command);
6058
6171
  let directory = cwd;
6059
6172
  for (const segment of splitShellCommand(command).segments) {
6060
6173
  const [name, ...args] = splitCommandWords(segment.text);
6061
- for (const target of collectRedirectTargets(segment.text)) findings.push(...collectTargetFindings(directory, target, scope));
6174
+ for (const target of collectRedirectTargets(segment.text)) findings.push(...collectTargetFindings(directory, target, scope, scratchPaths));
6062
6175
  if (name === void 0) continue;
6063
6176
  if (name === "cd" && hasPipe) directory = null;
6064
6177
  else if (name === "cd") directory = args[0] === void 0 ? scope.home : resolvePath(directory, args[0], scope.home);
6065
- else findings.push(...collectProgramFindings(name, args, directory, cwd, scope));
6178
+ else findings.push(...collectProgramFindings(name, args, directory, cwd, scope, scratchPaths));
6066
6179
  }
6067
6180
  return findings;
6068
6181
  }
6069
- function collectProgramFindings(name, args, directory, cwd, scope) {
6070
- const resolve = (path) => collectTargetFindings(directory, path, scope);
6182
+ function collectProgramFindings(name, args, directory, cwd, scope, scratchPaths) {
6183
+ const resolve = (path) => collectTargetFindings(directory, path, scope, scratchPaths);
6071
6184
  const operands = args.filter((word) => !word.startsWith("-"));
6072
- if (name === "git") return collectGitFindings(args, directory, cwd, scope);
6185
+ if (name === "git") return collectGitFindings(args, directory, cwd, scope, scratchPaths);
6073
6186
  if (name === "gh") return collectGhFindings(args, scope);
6074
6187
  if (PATH_WRITERS.has(name)) return operands.flatMap((path) => resolve(path));
6075
6188
  if (DESTINATION_WRITERS.has(name)) return collectCopyFindings(name, args, operands).flatMap((path) => resolve(path));
@@ -6207,7 +6320,7 @@ const GIT_DIRECTORY_WRITES = /* @__PURE__ */ new Set([
6207
6320
  "stash",
6208
6321
  "push"
6209
6322
  ]);
6210
- function collectGitFindings(args, directory, cwd, scope) {
6323
+ function collectGitFindings(args, directory, cwd, scope, scratchPaths) {
6211
6324
  let gitDirectory = directory;
6212
6325
  let index = 0;
6213
6326
  while (index < args.length && args[index]?.startsWith("-") === true) {
@@ -6219,11 +6332,11 @@ function collectGitFindings(args, directory, cwd, scope) {
6219
6332
  const rest = args.slice(index + 1);
6220
6333
  const operands = rest.filter((word) => !word.startsWith("-"));
6221
6334
  const findings = [];
6222
- if (gitDirectory !== null && GIT_DIRECTORY_WRITES.has(subcommand) && !isInScope(gitDirectory, scope)) findings.push({
6335
+ if (gitDirectory !== null && GIT_DIRECTORY_WRITES.has(subcommand) && !isInScope(gitDirectory, scope, scratchPaths)) findings.push({
6223
6336
  kind: "path",
6224
6337
  target: gitDirectory
6225
6338
  });
6226
- if (subcommand === "worktree" && operands[0] === "remove" && operands[1] !== void 0) findings.push(...collectTargetFindings(gitDirectory, operands[1], scope));
6339
+ if (subcommand === "worktree" && operands[0] === "remove" && operands[1] !== void 0) findings.push(...collectTargetFindings(gitDirectory, operands[1], scope, scratchPaths));
6227
6340
  if (subcommand === "branch" && rest.some((word) => /^-(?:d|D|-delete)$/u.test(word))) for (const branch of operands.filter((word) => !scope.branches.includes(word))) findings.push({
6228
6341
  kind: "branch",
6229
6342
  target: branch
@@ -6458,14 +6571,8 @@ function resolvePath(cwd, path, home) {
6458
6571
  const absolute = isAbsolute(expanded) || cwd === null ? expanded : join(cwd, expanded);
6459
6572
  return normalize(absolute).replace(/(?<=.)\/$/u, "");
6460
6573
  }
6461
- const SCRATCH_PATHS = [
6462
- "/tmp",
6463
- "/dev/null",
6464
- "/dev/stdout",
6465
- "/dev/stderr"
6466
- ];
6467
- function isInScope(path, scope) {
6468
- if (SCRATCH_PATHS.some((entry) => isUnder(path, entry)) || scope.pathGlobs.some((glob) => matchesGlob(path, glob))) return true;
6574
+ function isInScope(path, scope, scratchPaths) {
6575
+ if (scratchPaths.some((entry) => isUnder(path, entry)) || scope.pathGlobs.some((glob) => matchesGlob(path, glob))) return true;
6469
6576
  return scope.worktrees.some((worktree) => {
6470
6577
  const isNested = isUnder(path, join(worktree, ".worktrees"));
6471
6578
  const isOtherOwned = scope.worktrees.some((other) => other !== worktree && isUnder(path, other));
@@ -6478,12 +6585,12 @@ function isUnder(path, root) {
6478
6585
  //#endregion
6479
6586
  //#region src/containment/check-containment.ts
6480
6587
  const CONTAINMENT_RULE = "Outside Task Scope";
6481
- function checkContainment(request, scope) {
6588
+ function checkContainment(request, scope, scratchPaths) {
6482
6589
  const findings = collectScopeFindings({
6483
6590
  tool: request.toolName,
6484
6591
  cwd: request.cwd,
6485
6592
  input: request.toolInput
6486
- }, scope);
6593
+ }, scope, scratchPaths);
6487
6594
  if (findings.length === 0) return null;
6488
6595
  return {
6489
6596
  rule: CONTAINMENT_RULE,
@@ -6521,12 +6628,12 @@ function buildTaskScopeSummary(scope) {
6521
6628
  }
6522
6629
  //#endregion
6523
6630
  //#region src/policy/resolve-shipped-policy-dir.ts
6524
- function resolveShippedPolicyDir() {
6525
- let dir = import.meta.dirname;
6631
+ function resolveShippedPolicyDir(startDir = import.meta.dirname, stopDir = parse(startDir).root) {
6632
+ let dir = startDir;
6526
6633
  for (;;) {
6527
6634
  if (existsSync(join(dir, "policy", "classifier.md"))) return join(dir, "policy");
6528
6635
  const parent = dirname(dir);
6529
- if (parent === dir || dir === parse(dir).root) throw new Error("auto-mode cannot find its shipped policy/ directory");
6636
+ if (parent === dir || dir === stopDir) throw new Error("auto-mode cannot find its shipped policy/ directory");
6530
6637
  dir = parent;
6531
6638
  }
6532
6639
  }
@@ -6543,11 +6650,13 @@ async function loadPolicy(paths = {}, framework = "classifier.md") {
6543
6650
  }
6544
6651
  //#endregion
6545
6652
  //#region src/model/anthropic-client.ts
6546
- async function sendMessage(provider, apiKey, request) {
6653
+ async function sendMessage(provider, apiKey, request, signal) {
6547
6654
  const controller = new AbortController();
6548
- const timer = setTimeout(() => {
6655
+ const stopRequest = () => {
6549
6656
  controller.abort();
6550
- }, provider.timeoutMs);
6657
+ };
6658
+ signal.addEventListener("abort", stopRequest, { once: true });
6659
+ if (signal.aborted) controller.abort();
6551
6660
  try {
6552
6661
  const response = await fetch(`${provider.baseURL.replace(/\/$/, "")}/v1/messages`, {
6553
6662
  method: "POST",
@@ -6580,7 +6689,7 @@ async function sendMessage(provider, apiKey, request) {
6580
6689
  }
6581
6690
  return readResult(body);
6582
6691
  } finally {
6583
- clearTimeout(timer);
6692
+ signal.removeEventListener("abort", stopRequest);
6584
6693
  }
6585
6694
  }
6586
6695
  const contentBlockSchema = union([object({
@@ -6623,7 +6732,7 @@ function readResult(body) {
6623
6732
  //#endregion
6624
6733
  //#region src/model/build-request.ts
6625
6734
  const MAX_INPUT_CHARS = 4e3;
6626
- function buildUserMessage(payload, transcript, reasoning, repositoryContext = null) {
6735
+ function buildUserMessage(payload, transcript, reasoning, repositoryContext = null, mcpServers = []) {
6627
6736
  const history = transcript.length === 0 ? "(no transcript available from this harness)" : transcript.map((entry) => `${entry.role}: ${entry.text}`).join("\n\n");
6628
6737
  const input = JSON.stringify(payload.toolInput, null, 2).slice(0, MAX_INPUT_CHARS);
6629
6738
  const closing = reasoning ? "Work through the classification process, then end your reply with the output contract tags." : "Reply with the output contract tags and nothing else.";
@@ -6631,7 +6740,7 @@ function buildUserMessage(payload, transcript, reasoning, repositoryContext = nu
6631
6740
  ${history}
6632
6741
  </transcript>
6633
6742
 
6634
- ${repositoryContext === null ? "" : `<repository>\n${JSON.stringify(repositoryContext, null, 2)}\n</repository>\n\n`}<action>
6743
+ ${repositoryContext === null ? "" : `<repository>\n${JSON.stringify(repositoryContext, null, 2)}\n</repository>\n\n`}${mcpServers.length === 0 ? "" : `<mcp-servers>\n${JSON.stringify(mcpServers, null, 2)}\n</mcp-servers>\n\n`}<action>
6635
6744
  tool: ${payload.toolName}
6636
6745
  cwd: ${payload.cwd}
6637
6746
  input:
@@ -6663,7 +6772,7 @@ function buildTaskContext(context) {
6663
6772
  }
6664
6773
  //#endregion
6665
6774
  //#region src/model/build-decision-request.ts
6666
- function buildDecisionRequest(payload, policy, configuredRules, lastUserMessage, rulesSource = "replacement", repositoryContext = null) {
6775
+ function buildDecisionRequest(payload, policy, configuredRules, lastUserMessage, rulesSource = "replacement", repositoryContext = null, mcpServers = []) {
6667
6776
  const rules = {};
6668
6777
  let tier = null;
6669
6778
  for (const section of policy.split(/(?=^## |^### )/mu)) if (section.startsWith("## HARD BLOCK rules")) tier = "hard";
@@ -6711,6 +6820,7 @@ function buildDecisionRequest(payload, policy, configuredRules, lastUserMessage,
6711
6820
  rulesSource,
6712
6821
  configuredRules,
6713
6822
  ...repositoryContext === null ? {} : { repositoryContext },
6823
+ ...mcpServers.length === 0 ? {} : { mcpServers },
6714
6824
  lastUserMessage: payload.decisionContext === void 0 || payload.decisionContext.agentID === null ? lastUserMessage : null,
6715
6825
  ...payload.decisionContext === void 0 ? {} : { taskContext: buildTaskContext(payload.decisionContext) },
6716
6826
  action: {
@@ -6729,6 +6839,17 @@ function buildEvidenceGuidance(name) {
6729
6839
  return "";
6730
6840
  }
6731
6841
  //#endregion
6842
+ //#region node_modules/.bun/tiny-invariant@1.3.3/node_modules/tiny-invariant/dist/esm/tiny-invariant.js
6843
+ var isProduction = process.env.NODE_ENV === "production";
6844
+ var prefix = "Invariant failed";
6845
+ function invariant(condition, message) {
6846
+ if (condition) return;
6847
+ if (isProduction) throw new Error(prefix);
6848
+ var provided = typeof message === "function" ? message() : message;
6849
+ var value = provided ? "".concat(prefix, ": ").concat(provided) : prefix;
6850
+ throw new Error(value);
6851
+ }
6852
+ //#endregion
6732
6853
  //#region src/model/classify-decision-answers.ts
6733
6854
  function classifyDecisionAnswers(request, result, minConfidence) {
6734
6855
  const entries = Object.entries(request.rules).map(([id, rule]) => {
@@ -6759,7 +6880,7 @@ function collectDecisionContributors(request, result, minConfidence) {
6759
6880
  const decision = classifyDecisionAnswers(request, result, minConfidence);
6760
6881
  return Object.entries(request.rules).flatMap(([id, rule]) => {
6761
6882
  const answer = result.answers[id];
6762
- if (answer === void 0) throw new Error("Decision answer missing");
6883
+ invariant(answer, "the classification refused a result with a rule unanswered");
6763
6884
  return (decision.kind === "block" ? rule === decision.rule : decision.kind === "uncertain" && (answer.choice !== "allow" || answer.confidence < minConfidence || answer.probabilities.allow < minConfidence)) ? [{
6764
6885
  rule: rule.source === "shipped" ? rule.name : id,
6765
6886
  source: rule.source,
@@ -6788,6 +6909,83 @@ function formatClassifierNote(note, apiKey) {
6788
6909
  return apiKey === null || apiKey === "" ? note : note.split(apiKey).join("[redacted]");
6789
6910
  }
6790
6911
  //#endregion
6912
+ //#region src/model/load-mcp-servers.ts
6913
+ const serverSchema = looseObject({
6914
+ type: string().optional(),
6915
+ url: string().optional()
6916
+ });
6917
+ const serversSchema = record(string(), unknown()).optional();
6918
+ const approvalSchema = looseObject({
6919
+ enableAllProjectMcpServers: boolean().optional(),
6920
+ enabledMcpjsonServers: array(string()).optional(),
6921
+ disabledMcpjsonServers: array(string()).optional()
6922
+ });
6923
+ const stateSchema = looseObject({
6924
+ mcpServers: serversSchema,
6925
+ projects: record(string(), looseObject({
6926
+ ...approvalSchema.shape,
6927
+ mcpServers: serversSchema
6928
+ })).optional()
6929
+ });
6930
+ const projectFileSchema = looseObject({ mcpServers: serversSchema });
6931
+ async function loadMCPServers(cwd, host) {
6932
+ const configDir = host.env["CLAUDE_CONFIG_DIR"];
6933
+ const hasConfigDir = configDir !== void 0 && configDir !== "";
6934
+ const statePath = hasConfigDir ? join(configDir, ".claude.json") : join(host.home, ".claude.json");
6935
+ const settingsDir = hasConfigDir ? configDir : join(host.home, ".claude");
6936
+ const settingsPath = join(settingsDir, "settings.json");
6937
+ const [state, settings] = await Promise.all([readJSON(statePath, stateSchema), readJSON(settingsPath, approvalSchema)]);
6938
+ const directory = resolve(cwd);
6939
+ const projectDir = (await findCheckout(directory, host.env))?.worktree ?? directory;
6940
+ const project = state?.projects?.[projectDir];
6941
+ const projectFile = await readJSON(join(projectDir, ".mcp.json"), projectFileSchema);
6942
+ const approvals = [settings, project].filter((entry) => entry !== void 0 && entry !== null);
6943
+ const isDisabled = (name) => approvals.some((entry) => entry.disabledMcpjsonServers?.includes(name) === true);
6944
+ const isEnabled = (name) => approvals.some((entry) => entry.enableAllProjectMcpServers === true || entry.enabledMcpjsonServers?.includes(name) === true);
6945
+ const facts = [
6946
+ ...buildServerFacts(project?.mcpServers, "local"),
6947
+ ...buildServerFacts(projectFile?.mcpServers, "project").filter((server) => isEnabled(server.name) && !isDisabled(server.name)),
6948
+ ...buildServerFacts(state?.mcpServers, "user")
6949
+ ];
6950
+ return facts.filter((server, index) => facts.findIndex((other) => other.name === server.name) === index);
6951
+ }
6952
+ async function readJSON(path, schema) {
6953
+ try {
6954
+ const text = await readFile(path, "utf8");
6955
+ const parsed = schema.safeParse(JSON.parse(text));
6956
+ return parsed.success ? parsed.data : null;
6957
+ } catch {
6958
+ return null;
6959
+ }
6960
+ }
6961
+ const MAX_NAME_LENGTH = 128;
6962
+ const TRANSPORTS = {
6963
+ stdio: "stdio",
6964
+ http: "http",
6965
+ "streamable-http": "http",
6966
+ sse: "sse",
6967
+ ws: "ws"
6968
+ };
6969
+ function buildServerFacts(servers, scope) {
6970
+ return Object.entries(servers ?? {}).flatMap(([name, entry]) => {
6971
+ const server = serverSchema.safeParse(entry);
6972
+ if (!server.success || name === "" || name.length > MAX_NAME_LENGTH) return [];
6973
+ const transport = TRANSPORTS[server.data.type ?? (server.data.url === void 0 ? "stdio" : "")];
6974
+ if (transport === void 0) return [];
6975
+ return [{
6976
+ name,
6977
+ scope,
6978
+ transport,
6979
+ host: transport === "stdio" || server.data.url === void 0 ? null : findEndpointHost(server.data.url)
6980
+ }];
6981
+ });
6982
+ }
6983
+ function findEndpointHost(url) {
6984
+ const parsed = URL.parse(url);
6985
+ if (parsed === null || !/^(?:https?|wss?):$/u.test(parsed.protocol) || /[${}]/u.test(parsed.host)) return null;
6986
+ return parsed.host;
6987
+ }
6988
+ //#endregion
6791
6989
  //#region src/scope/load-checkout-remotes.ts
6792
6990
  async function loadCheckoutRemotes(commonDir) {
6793
6991
  const config = await readFile(join(commonDir, "config"), "utf8").catch(() => "");
@@ -6829,12 +7027,12 @@ function parseConfigValue(raw) {
6829
7027
  }
6830
7028
  //#endregion
6831
7029
  //#region src/model/load-repository-context.ts
6832
- async function loadRepositoryContext(cwd) {
7030
+ async function loadRepositoryContext(cwd, env, stopDir = parse(resolve(cwd)).root) {
6833
7031
  if ([
6834
7032
  "GIT_DIR",
6835
7033
  "GIT_WORK_TREE",
6836
7034
  "GIT_COMMON_DIR"
6837
- ].some((name) => process.env[name] !== void 0)) return null;
7035
+ ].some((name) => env[name] !== void 0)) return null;
6838
7036
  let directory = resolve(cwd);
6839
7037
  for (;;) {
6840
7038
  const gitPath = join(directory, ".git");
@@ -6843,7 +7041,7 @@ async function loadRepositoryContext(cwd) {
6843
7041
  } catch (error) {
6844
7042
  if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) return null;
6845
7043
  const parent = dirname(directory);
6846
- if (parent === directory) return null;
7044
+ if (parent === directory || directory === resolve(stopDir)) return null;
6847
7045
  directory = parent;
6848
7046
  continue;
6849
7047
  }
@@ -6914,8 +7112,8 @@ function normalizeRemoteURL(url) {
6914
7112
  }
6915
7113
  //#endregion
6916
7114
  //#region src/model/load-repository-evidence.ts
6917
- async function loadRepositoryEvidence(cwd, taskScope) {
6918
- const context = await loadRepositoryContext(cwd);
7115
+ async function loadRepositoryEvidence(cwd, taskScope, env, stopDir) {
7116
+ const context = await loadRepositoryContext(cwd, env, stopDir);
6919
7117
  if (taskScope === void 0) return context;
6920
7118
  return {
6921
7119
  ...context ?? {
@@ -7324,6 +7522,28 @@ function pickDecisionVerdict(request, result, minConfidence) {
7324
7522
  })).exhaustive();
7325
7523
  }
7326
7524
  //#endregion
7525
+ //#region src/model/decision-response-schema.ts
7526
+ const probability = number().min(0).max(1);
7527
+ const answerSchema = object({
7528
+ type: literal("choice"),
7529
+ choice: _enum([
7530
+ "allow",
7531
+ "block",
7532
+ "ask"
7533
+ ]),
7534
+ confidence: probability,
7535
+ probabilities: strictObject({
7536
+ allow: probability,
7537
+ block: probability,
7538
+ ask: probability
7539
+ })
7540
+ });
7541
+ const decisionResponseSchema = object({
7542
+ model: string().min(1),
7543
+ answers: record(string(), answerSchema),
7544
+ usage: object({ input_tokens: number().int().nonnegative() })
7545
+ });
7546
+ //#endregion
7327
7547
  //#region src/model/send-decision.ts
7328
7548
  async function sendDecision(provider, apiKey, request, signal) {
7329
7549
  const input = {
@@ -7351,11 +7571,8 @@ async function sendDecision(provider, apiKey, request, signal) {
7351
7571
  const stopRequest = () => {
7352
7572
  controller.abort();
7353
7573
  };
7354
- signal?.addEventListener("abort", stopRequest, { once: true });
7355
- if (signal?.aborted === true) controller.abort();
7356
- const timer = setTimeout(() => {
7357
- controller.abort();
7358
- }, provider.timeoutMs);
7574
+ signal.addEventListener("abort", stopRequest, { once: true });
7575
+ if (signal.aborted) controller.abort();
7359
7576
  try {
7360
7577
  let response;
7361
7578
  try {
@@ -7373,26 +7590,6 @@ async function sendDecision(provider, apiKey, request, signal) {
7373
7590
  throw new DecisionRequestError("network", requestBytes, "Decision API request failed");
7374
7591
  }
7375
7592
  if (!response.ok) throw new DecisionRequestError("http-status", requestBytes, `Decision API returned HTTP ${response.status}`);
7376
- const probability = number().min(0).max(1);
7377
- const answer = object({
7378
- type: literal("choice"),
7379
- choice: _enum([
7380
- "allow",
7381
- "block",
7382
- "ask"
7383
- ]),
7384
- confidence: probability,
7385
- probabilities: strictObject({
7386
- allow: probability,
7387
- block: probability,
7388
- ask: probability
7389
- })
7390
- });
7391
- const schema = object({
7392
- model: string().min(1),
7393
- answers: record(string(), answer),
7394
- usage: object({ input_tokens: number().int().nonnegative() })
7395
- });
7396
7593
  let responseBody;
7397
7594
  try {
7398
7595
  responseBody = await response.json();
@@ -7400,7 +7597,7 @@ async function sendDecision(provider, apiKey, request, signal) {
7400
7597
  if (error instanceof Error && error.name === "AbortError") throw new DecisionRequestError("aborted", requestBytes, "Decision request aborted", { cause: error });
7401
7598
  throw new DecisionRequestError("invalid-response", requestBytes, "Decision API returned invalid JSON");
7402
7599
  }
7403
- const parsedResponse = schema.safeParse(responseBody);
7600
+ const parsedResponse = decisionResponseSchema.safeParse(responseBody);
7404
7601
  if (!parsedResponse.success) throw new DecisionRequestError("invalid-response", requestBytes, "Decision API returned a malformed response");
7405
7602
  const parsed = parsedResponse.data;
7406
7603
  if (Object.keys(parsed.answers).length !== Object.keys(request.questions).length) throw new DecisionRequestError("invalid-response", requestBytes, "Decision API returned an incomplete answer set");
@@ -7417,19 +7614,19 @@ async function sendDecision(provider, apiKey, request, signal) {
7417
7614
  requestBytes
7418
7615
  };
7419
7616
  } finally {
7420
- clearTimeout(timer);
7421
- signal?.removeEventListener("abort", stopRequest);
7617
+ signal.removeEventListener("abort", stopRequest);
7422
7618
  }
7423
7619
  }
7424
7620
  //#endregion
7425
7621
  //#region src/model/classify-with-jev.ts
7426
- async function classifyWithJev(payload, config, options = {}) {
7622
+ async function classifyWithJev(payload, config, options) {
7427
7623
  const start = performance.now();
7624
+ const now = options.now ?? Date.now;
7428
7625
  let key = null;
7429
7626
  let stage = "credential";
7430
7627
  const minConfidence = config.minConfidence ?? .8;
7431
7628
  let keySource = "none";
7432
- const envKey = config.provider.apiKeyEnv === void 0 ? void 0 : process.env[config.provider.apiKeyEnv];
7629
+ const envKey = config.provider.apiKeyEnv === void 0 ? void 0 : options.host.env[config.provider.apiKeyEnv];
7433
7630
  if (envKey !== void 0 && envKey !== "") keySource = "environment";
7434
7631
  else if (config.provider.apiKeyCommand !== void 0 && config.provider.apiKeyCommand !== "") keySource = "command";
7435
7632
  try {
@@ -7437,23 +7634,22 @@ async function classifyWithJev(payload, config, options = {}) {
7437
7634
  if (key === null) throw new Error("no API key: set the configured environment variable or key command");
7438
7635
  stage = "evidence";
7439
7636
  const directUserText = payload.decisionContext?.agentID === null ? payload.decisionContext.lastDirectUserMessage?.text ?? null : null;
7440
- const [policy, rules, repositoryContext] = await Promise.all([
7637
+ const [policy, rules, repositoryContext, mcpServers] = await Promise.all([
7441
7638
  loadPolicy({
7442
7639
  classifierPath: config.classifierPath,
7443
7640
  rulesPath: config.rulesPath
7444
7641
  }, "decision.md"),
7445
- loadClaudeRules(config.claudeSettingsPath),
7446
- loadRepositoryEvidence(payload.cwd, options.taskScope).catch(() => null)
7642
+ loadClaudeRules(config.claudeSettingsPath, options.host),
7643
+ loadRepositoryEvidence(payload.cwd, options.taskScope, options.host.env).catch(() => null),
7644
+ loadMCPServers(payload.cwd, options.host).catch(() => [])
7447
7645
  ]);
7448
- const request = buildDecisionRequest(payload, policy, rules, directUserText, config.rulesPath === void 0 ? "shipped" : "replacement", repositoryContext);
7449
- const remainingMs = options.deadlineAt === void 0 ? config.provider.timeoutMs : options.deadlineAt - Date.now();
7646
+ const request = buildDecisionRequest(payload, policy, rules, directUserText, config.rulesPath === void 0 ? "shipped" : "replacement", repositoryContext, mcpServers);
7647
+ const remainingMs = options.deadlineAt === void 0 ? config.provider.timeoutMs : options.deadlineAt - now();
7450
7648
  if (remainingMs <= 0 || options.signal?.aborted === true) throw new DOMException("Evaluation deadline expired", "AbortError");
7451
- const provider = {
7452
- ...config.provider,
7453
- timeoutMs: Math.min(config.provider.timeoutMs, remainingMs)
7454
- };
7649
+ const timer = (options.timeout ?? ((ms) => AbortSignal.timeout(ms)))(toTimerDelay(Math.min(config.provider.timeoutMs, remainingMs)));
7650
+ const signal = options.signal === void 0 ? timer : AbortSignal.any([timer, options.signal]);
7455
7651
  stage = "request";
7456
- const result = await sendDecision(provider, key, request, options.signal);
7652
+ const result = await sendDecision(config.provider, key, request, signal);
7457
7653
  stage = "response";
7458
7654
  const verdict = pickDecisionVerdict(request, result, minConfidence);
7459
7655
  return {
@@ -7477,7 +7673,7 @@ async function classifyWithJev(payload, config, options = {}) {
7477
7673
  if (options.signal?.aborted === true) {
7478
7674
  reason = "evaluation cancelled";
7479
7675
  status = "cancelled";
7480
- } else if (options.deadlineAt !== void 0 && Date.now() >= options.deadlineAt) {
7676
+ } else if (options.deadlineAt !== void 0 && now() >= options.deadlineAt) {
7481
7677
  reason = "evaluation deadline expired";
7482
7678
  status = "timeout";
7483
7679
  } else if (error instanceof Error) {
@@ -7538,8 +7734,15 @@ function parseModelVerdict(text) {
7538
7734
  //#endregion
7539
7735
  //#region src/model/classify-with-model.ts
7540
7736
  async function classifyWithModel(payload, config, options = {}) {
7541
- if (config.provider.protocol === "system-one") return classifyWithJev(payload, config, options);
7542
- const apiKey = await resolveApiKey(config.provider);
7737
+ const host = options.host ?? readHostEnvironment();
7738
+ if (config.provider.protocol === "system-one") return classifyWithJev(payload, config, {
7739
+ ...options,
7740
+ host
7741
+ });
7742
+ const apiKey = await resolveApiKey(config.provider, {
7743
+ ...options,
7744
+ host
7745
+ });
7543
7746
  if (apiKey === null) return buildFailure(config, "no API key: set the configured environment variable or key command");
7544
7747
  let system;
7545
7748
  try {
@@ -7555,13 +7758,14 @@ async function classifyWithModel(payload, config, options = {}) {
7555
7758
  role: "user",
7556
7759
  text: directMessage.text
7557
7760
  }];
7558
- const repositoryContext = await loadRepositoryEvidence(payload.cwd, options.taskScope).catch(() => null);
7559
- const user = buildUserMessage(payload, transcript, config.provider.reasoning, repositoryContext);
7761
+ const [repositoryContext, mcpServers] = await Promise.all([loadRepositoryEvidence(payload.cwd, options.taskScope, host.env).catch(() => null), loadMCPServers(payload.cwd, host).catch(() => [])]);
7762
+ const user = buildUserMessage(payload, transcript, config.provider.reasoning, repositoryContext, mcpServers);
7560
7763
  try {
7764
+ const timeout = options.timeout ?? ((ms) => AbortSignal.timeout(ms));
7561
7765
  const result = await sendMessage(config.provider, apiKey, {
7562
7766
  system,
7563
7767
  user
7564
- });
7768
+ }, timeout(toTimerDelay(config.provider.timeoutMs)));
7565
7769
  if (result.text.trim() === "") return buildFailure(config, `${config.provider.model} returned no text; raise maxTokens`);
7566
7770
  const verdict = parseModelVerdict(formatClassifierNote(result.text, apiKey));
7567
7771
  const cache = `${result.cachedInputTokens} cached / ${result.cacheWriteTokens} written / ${result.newInputTokens} new / ${result.outputTokens} out`;
@@ -7593,19 +7797,8 @@ function toMessage(error) {
7593
7797
  }
7594
7798
  //#endregion
7595
7799
  //#region src/policy/read-denial-guidance.ts
7596
- async function readDenialGuidance() {
7597
- return (await readFile(join(resolveShippedPolicyDir(), "denial.md"), "utf8")).trim();
7598
- }
7599
- //#endregion
7600
- //#region node_modules/.bun/tiny-invariant@1.3.3/node_modules/tiny-invariant/dist/esm/tiny-invariant.js
7601
- var isProduction = process.env.NODE_ENV === "production";
7602
- var prefix = "Invariant failed";
7603
- function invariant(condition, message) {
7604
- if (condition) return;
7605
- if (isProduction) throw new Error(prefix);
7606
- var provided = typeof message === "function" ? message() : message;
7607
- var value = provided ? "".concat(prefix, ": ").concat(provided) : prefix;
7608
- throw new Error(value);
7800
+ async function readDenialGuidance(policyDir = resolveShippedPolicyDir()) {
7801
+ return (await readFile(join(policyDir, "denial.md"), "utf8")).trim();
7609
7802
  }
7610
7803
  //#endregion
7611
7804
  //#region src/rules/local-allowlists.ts
@@ -7769,37 +7962,6 @@ function toUnquoted(word) {
7769
7962
  return (first === "'" || first === "\"") && word.at(-1) === first ? word.slice(1, -1) : word;
7770
7963
  }
7771
7964
  //#endregion
7772
- //#region src/scope/find-checkout.ts
7773
- const GIT_OVERRIDES$1 = [
7774
- "GIT_DIR",
7775
- "GIT_WORK_TREE",
7776
- "GIT_COMMON_DIR"
7777
- ];
7778
- async function findCheckout(start) {
7779
- if (GIT_OVERRIDES$1.some((name) => process.env[name] !== void 0)) return null;
7780
- for (let directory = start;; directory = dirname(directory)) {
7781
- const gitPath = join(directory, ".git");
7782
- const info = await stat(gitPath).catch(() => null);
7783
- if (info !== null) {
7784
- const gitDir = info.isDirectory() ? gitPath : await findLinkedGitDir(directory, gitPath);
7785
- return gitDir === null ? null : {
7786
- worktree: directory,
7787
- commonDir: await findCommonDir(gitDir)
7788
- };
7789
- }
7790
- if (dirname(directory) === directory) return null;
7791
- }
7792
- }
7793
- async function findLinkedGitDir(directory, gitPath) {
7794
- const link = await readFile(gitPath, "utf8").catch(() => "");
7795
- const path = /^gitdir: (?<path>.+)$/mu.exec(link)?.groups?.["path"];
7796
- return path === void 0 ? null : resolve(directory, path.trim());
7797
- }
7798
- async function findCommonDir(gitDir) {
7799
- const common = await readFile(join(gitDir, "commondir"), "utf8").catch(() => null);
7800
- return common === null ? gitDir : resolve(gitDir, common.trim());
7801
- }
7802
- //#endregion
7803
7965
  //#region src/scope/merge-scope-facts.ts
7804
7966
  function mergeScopeFacts(all) {
7805
7967
  const pullRequests = /* @__PURE__ */ new Map();
@@ -7989,7 +8151,7 @@ function resolveSessionScopePath(stateDir, sessionID) {
7989
8151
  }
7990
8152
  //#endregion
7991
8153
  //#region src/scope/pick-scope-source-reader.ts
7992
- function pickScopeSourceReader(source) {
8154
+ function pickScopeSourceReader(source, findSourceCheckout = findCheckout) {
7993
8155
  if (source.kind === "cwd") return (context) => Promise.resolve(buildFacts({
7994
8156
  worktrees: [context.worktree],
7995
8157
  branches: context.branch === null ? [] : [context.branch]
@@ -8005,7 +8167,7 @@ function pickScopeSourceReader(source) {
8005
8167
  if (source.kind === "globs") return () => Promise.resolve(buildFacts({ pathGlobs: source.paths ?? [] }));
8006
8168
  return async (context) => {
8007
8169
  const loaded = await loadAtcSessionRecord(context.atcRecordPath, context.atcSessionID);
8008
- if (loaded.kind === "malformed") process.stderr.write(`auto-mode: ${loaded.diagnostic}\n`);
8170
+ if (loaded.kind === "malformed") context.stderr.write(`auto-mode: ${loaded.diagnostic}\n`);
8009
8171
  if (loaded.kind !== "record") return EMPTY_SCOPE_FACTS;
8010
8172
  const scope = loaded.record.scope;
8011
8173
  const paths = /* @__PURE__ */ new Set([
@@ -8013,13 +8175,13 @@ function pickScopeSourceReader(source) {
8013
8175
  ...scope.worktrees.map((worktree) => worktree.path),
8014
8176
  ...scope.branches.map((branch) => branch.repo ?? scope.workspace.path)
8015
8177
  ]);
8016
- const entries = await Promise.all([...paths].map((path) => findCommonDirEntry(path)));
8178
+ const entries = await Promise.all([...paths].map((path) => findCommonDirEntry(path, context.env, findSourceCheckout)));
8017
8179
  const commonDirs = new Map(entries);
8018
8180
  return buildAtcScopeFacts(loaded.record, (path) => context.commonDir !== null && commonDirs.get(path) === context.commonDir);
8019
8181
  };
8020
8182
  }
8021
- async function findCommonDirEntry(path) {
8022
- return [path, (await findCheckout(path))?.commonDir ?? null];
8183
+ async function findCommonDirEntry(path, env, findSourceCheckout) {
8184
+ return [path, (await findSourceCheckout(path, env))?.commonDir ?? null];
8023
8185
  }
8024
8186
  function buildFacts(facts) {
8025
8187
  return {
@@ -8029,23 +8191,25 @@ function buildFacts(facts) {
8029
8191
  }
8030
8192
  //#endregion
8031
8193
  //#region src/scope/load-task-scope.ts
8032
- async function loadTaskScope(request, sources) {
8194
+ async function loadTaskScope(request, sources, host, stderr) {
8033
8195
  const cwd = resolve(request.cwd);
8034
- const [checkout, context] = await Promise.all([findCheckout(cwd), loadRepositoryContext(cwd)]);
8196
+ const [checkout, context] = await Promise.all([findCheckout(cwd, host.env), loadRepositoryContext(cwd, host.env)]);
8035
8197
  const branch = context?.branch ?? null;
8036
8198
  const sourceContext = {
8199
+ env: host.env,
8037
8200
  sessionID: request.sessionID,
8038
8201
  cwd,
8039
8202
  worktree: checkout?.worktree ?? cwd,
8040
8203
  commonDir: checkout?.commonDir ?? null,
8041
8204
  branch,
8042
8205
  stateDir: request.stateDir,
8043
- atcRecordPath: process.env["ATC_SESSION_RECORD"],
8044
- atcSessionID: process.env["ATC_SESSION_ID"]
8206
+ atcRecordPath: host.env["ATC_SESSION_RECORD"],
8207
+ atcSessionID: host.env["ATC_SESSION_ID"],
8208
+ stderr
8045
8209
  };
8046
8210
  const [remotes, ...facts] = await Promise.all([checkout === null ? [] : loadCheckoutRemotes(checkout.commonDir), ...Object.values(sources).map((source) => pickScopeSourceReader(source)(sourceContext))]);
8047
8211
  return buildTaskScope({
8048
- home: homedir(),
8212
+ home: host.home,
8049
8213
  currentBranch: branch,
8050
8214
  defaultBranch: context?.defaultBranch ?? null,
8051
8215
  remotes,
@@ -8053,18 +8217,12 @@ async function loadTaskScope(request, sources) {
8053
8217
  });
8054
8218
  }
8055
8219
  //#endregion
8056
- //#region src/state/resolve-state-dir.ts
8057
- function resolveStateDir() {
8058
- const stateHome = process.env["XDG_STATE_HOME"];
8059
- const base = stateHome === void 0 || stateHome === "" ? join(homedir(), ".local", "state") : stateHome;
8060
- return join(base, "auto-mode");
8061
- }
8062
- //#endregion
8063
8220
  //#region src/classify-action.ts
8064
8221
  async function classifyAction(request, config, options = {}) {
8222
+ const host = options.host ?? readHostEnvironment();
8065
8223
  let configured = null;
8066
8224
  if (config.provider.protocol === "system-one") try {
8067
- configured = await loadClaudeRules(config.claudeSettingsPath);
8225
+ configured = await loadClaudeRules(config.claudeSettingsPath, host);
8068
8226
  } catch {
8069
8227
  const guidance = config.onFailure === "deny" ? await tryReadDenialGuidance() : null;
8070
8228
  return {
@@ -8078,8 +8236,8 @@ async function classifyAction(request, config, options = {}) {
8078
8236
  const hasConfiguredDenies = configured !== null && (configured.hard_deny.length > 0 || configured.soft_deny.length > 0);
8079
8237
  const local = hasConfiguredDenies ? { kind: "escalate" } : classifyLocally(request);
8080
8238
  if (local.kind === "allow" && local.exception === READ_ONLY_EXCEPTION) return buildLocalAllow(local.exception);
8081
- const scope = await tryLoadTaskScope(request, config);
8082
- const containment = scope === null ? null : checkContainment(request, scope);
8239
+ const scope = await tryLoadTaskScope(request, config, host, options.stderr ?? process.stderr);
8240
+ const containment = scope === null ? null : checkContainment(request, scope, host.scratchPaths);
8083
8241
  if (containment !== null) {
8084
8242
  const guidance = await tryReadDenialGuidance();
8085
8243
  return {
@@ -8090,7 +8248,7 @@ async function classifyAction(request, config, options = {}) {
8090
8248
  };
8091
8249
  }
8092
8250
  if (local.kind === "allow") return buildLocalAllow(local.exception);
8093
- const edit = hasConfiguredDenies || scope === null ? null : await tryClassifyEdit(request, scope);
8251
+ const edit = hasConfiguredDenies || scope === null ? null : await tryClassifyEdit(request, scope, host);
8094
8252
  if (edit?.kind === "bypass") return {
8095
8253
  verdict: { kind: "allow" },
8096
8254
  decidingStage: "bypass",
@@ -8106,7 +8264,8 @@ async function classifyAction(request, config, options = {}) {
8106
8264
  const taskScope = scope === null ? void 0 : buildTaskScopeSummary(scope);
8107
8265
  const [outcome, guidance] = await Promise.all([classifyWithModel(request, config, {
8108
8266
  ...options,
8109
- taskScope
8267
+ taskScope,
8268
+ host
8110
8269
  }), tryReadDenialGuidance()]);
8111
8270
  return {
8112
8271
  ...outcome,
@@ -8116,47 +8275,13 @@ async function classifyAction(request, config, options = {}) {
8116
8275
  };
8117
8276
  }
8118
8277
  const READ_ONLY_EXCEPTION = "Read-only actions";
8119
- async function tryLoadTaskScope(request, config) {
8278
+ async function tryLoadTaskScope(request, config, host, stderr) {
8120
8279
  try {
8121
8280
  return await loadTaskScope({
8122
8281
  sessionID: request.sessionID,
8123
8282
  cwd: request.cwd,
8124
- stateDir: resolveStateDir()
8125
- }, config.scopeSources ?? DEFAULT_SCOPE_SOURCES);
8126
- } catch {
8127
- return null;
8128
- }
8129
- }
8130
- const GIT_OVERRIDES = [
8131
- "GIT_DIR",
8132
- "GIT_WORK_TREE",
8133
- "GIT_COMMON_DIR"
8134
- ];
8135
- async function tryClassifyEdit(request, scope) {
8136
- const fields = getEditFields(request.toolName);
8137
- const path = fields === null ? void 0 : request.toolInput[fields.path];
8138
- if (typeof path !== "string" || path === "") return null;
8139
- try {
8140
- const ownDirs = [dirname(resolveConfigPath()), resolveStateDir()];
8141
- const requested = resolve(request.cwd, path);
8142
- const [target, worktrees, protectedDirs] = await Promise.all([
8143
- resolveEditTarget(requested),
8144
- Promise.all(scope.worktrees.map((worktree) => resolveEditTarget(worktree))),
8145
- Promise.all(ownDirs.map((dir) => resolveEditTarget(dir)))
8146
- ]);
8147
- if (target === null || GIT_OVERRIDES.some((name) => process.env[name] !== void 0)) return null;
8148
- const [checkout, current] = await Promise.all([findCheckout(dirname(target)), request.toolName === "Edit" ? readFile(target, "utf8").catch(() => null) : null]);
8149
- return (await import("./classify-edit-D8GC6b91.js")).classifyEdit({
8150
- toolName: request.toolName,
8151
- toolInput: request.toolInput,
8152
- requested,
8153
- target,
8154
- checkout: checkout?.worktree ?? null,
8155
- current
8156
- }, {
8157
- worktrees: worktrees.filter((worktree) => worktree !== null),
8158
- protectedDirs: [...ownDirs, ...protectedDirs.filter((dir) => dir !== null)]
8159
- });
8283
+ stateDir: resolveStateDir(host)
8284
+ }, config.scopeSources ?? DEFAULT_SCOPE_SOURCES, host, stderr);
8160
8285
  } catch {
8161
8286
  return null;
8162
8287
  }
@@ -8185,7 +8310,7 @@ async function tryReadDenialGuidance() {
8185
8310
  }
8186
8311
  }
8187
8312
  //#endregion
8188
- //#region src/request/parse-action-request.ts
8313
+ //#region src/request/action-request-schema.ts
8189
8314
  const userTaskSchema = strictObject({
8190
8315
  text: string(),
8191
8316
  origin: _enum([
@@ -8209,7 +8334,7 @@ const decisionContextSchema = strictObject({
8209
8334
  lastDirectUserMessage: userTaskSchema.nullable(),
8210
8335
  omittedTaskContext: array(omissionSchema)
8211
8336
  });
8212
- const requestSchema = strictObject({
8337
+ const actionRequestSchema = strictObject({
8213
8338
  sessionID: string().min(1),
8214
8339
  toolUseID: string().min(1).optional(),
8215
8340
  cwd: string().min(1),
@@ -8217,8 +8342,10 @@ const requestSchema = strictObject({
8217
8342
  toolInput: looseObject({}),
8218
8343
  context: decisionContextSchema
8219
8344
  });
8345
+ //#endregion
8346
+ //#region src/request/parse-action-request.ts
8220
8347
  function parseActionRequest(body) {
8221
- const parsed = requestSchema.safeParse(body);
8348
+ const parsed = actionRequestSchema.safeParse(body);
8222
8349
  if (!parsed.success) return null;
8223
8350
  const context = parsed.data.context;
8224
8351
  return {
@@ -8242,4 +8369,4 @@ function renderVerdict(verdict) {
8242
8369
  })).exhaustive();
8243
8370
  }
8244
8371
  //#endregion
8245
- export { resolveConfigPath as C, string as D, strictObject as E, resolveApiKey as S, number as T, splitCommandWords as _, resolveSessionScopePath as a, DEFAULT_SCOPE_SOURCES as b, findCheckout as c, parseModelVerdict as d, loadRepositoryContext as f, splitShellCommand as g, toRepositorySlug as h, resolveStateDir as i, classifyLocally as l, loadPolicy as m, parseActionRequest as n, loadSessionScope as o, loadCheckoutRemotes as p, classifyAction as r, EMPTY_SESSION_SCOPE as s, renderVerdict as t, classifyWithModel as u, DEFAULT_CONFIG as v, PRESETS as w, loadConfig as x, DEFAULT_DENIAL_BUDGET as y };
8372
+ export { resolveConfigPath as C, strictObject as D, number as E, string as O, resolveApiKey as S, PRESETS as T, findCheckout as _, loadSessionScope as a, DEFAULT_SCOPE_SOURCES as b, classifyWithModel as c, loadCheckoutRemotes as d, loadPolicy as f, resolveStateDir as g, splitCommandWords as h, resolveSessionScopePath as i, parseModelVerdict as l, splitShellCommand as m, parseActionRequest as n, EMPTY_SESSION_SCOPE as o, toRepositorySlug as p, classifyAction as r, classifyLocally as s, renderVerdict as t, loadRepositoryContext as u, DEFAULT_CONFIG as v, readHostEnvironment as w, loadConfig as x, DEFAULT_DENIAL_BUDGET as y };