auto-mode 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,7 +38,7 @@ Create the classifier configuration:
38
38
 
39
39
  Save it as `~/.config/auto-mode/config.json` and export your TypeSafe API key, or set the entry's
40
40
  `apiKeyCommand` to a command that prints it. [Configuration](./docs/guides/configuration.md) covers
41
- the registries, credentials, and migrating an older file.
41
+ the registries and credentials.
42
42
 
43
43
  ## Standing permissions
44
44
 
@@ -77,7 +77,6 @@ fit.
77
77
  | `auto-mode run --local-only` | Skip the model tier |
78
78
  | `auto-mode run --jev-only` | Require Jev with a 5-second API timeout |
79
79
  | `auto-mode print-prompt` | Print the selected provider's base policy |
80
- | `auto-mode config migrate` | Rewrite an older config file in place |
81
80
 
82
81
  The mod evaluates only ask decisions and preserves existing allow and deny decisions. It runs the
83
82
  CLI as a bounded child process with `--jev-only`. The `spark`, `claude`, `glm`, and `messages`
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
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
- import { appendFile, constants, copyFile, mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import { g as string, h as strictObject, l as DEFAULT_DENIAL_BUDGET, m as number, n as parseActionRequest, r as classifyAction, s as loadPolicy, t as renderVerdict, u as loadConfig } from "./render-verdict-1xtCI2is.js";
3
+ import { appendFile, mkdir, readFile, rename, writeFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, join } from "node:path";
6
6
  import { createHash, randomUUID } from "node:crypto";
@@ -154,36 +154,6 @@ function buildJevOnlyConfig(config) {
154
154
  };
155
155
  }
156
156
  //#endregion
157
- //#region src/config/write-migrated-config.ts
158
- async function writeMigratedConfig(path) {
159
- let raw;
160
- try {
161
- raw = await readFile(path, "utf8");
162
- } catch (error) {
163
- if (error instanceof Error && "code" in error && error.code === "ENOENT") return `${path} does not exist; nothing to migrate`;
164
- throw new Error(`${path} is unreadable`, { cause: error });
165
- }
166
- let json;
167
- try {
168
- json = JSON.parse(raw);
169
- } catch {
170
- throw new Error(`${path} is not valid JSON`);
171
- }
172
- const normalized = normalizeConfigFile(json, path);
173
- if (!normalized.isLegacy) return `${path} already uses the current shape`;
174
- const backup = `${path}.bak`;
175
- const staged = `${path}.migrating`;
176
- try {
177
- await copyFile(path, backup, constants.COPYFILE_EXCL);
178
- } catch (error) {
179
- if (error instanceof Error && "code" in error && error.code === "EEXIST") throw new Error(`${backup} already exists; move it aside and run the migration again`, { cause: error });
180
- throw error;
181
- }
182
- await writeFile(staged, `${JSON.stringify(normalized.file, null, 2)}\n`, { mode: 384 });
183
- await rename(staged, path);
184
- return `rewrote ${path} in the current shape; the old file is at ${backup}`;
185
- }
186
- //#endregion
187
157
  //#region src/diagnostics/write-action-diagnostic.ts
188
158
  async function writeActionDiagnostic(payload, entry) {
189
159
  const path = process.env["AUTO_MODE_DIAGNOSTICS_PATH"] ?? join(resolveStateDir(), "actions.jsonl");
@@ -221,7 +191,6 @@ const USAGE = `auto-mode — a permission classifier for the auto-mode Claude Co
221
191
  Usage:
222
192
  auto-mode run Read an action request on stdin, write a verdict on stdout
223
193
  auto-mode print-prompt Print the system prompt the classifier receives
224
- auto-mode config migrate Rewrite the config file from the old keys into the current shape
225
194
 
226
195
  Options:
227
196
  --classifier <path> Use this framework file instead of the shipped one
@@ -381,21 +350,9 @@ async function main(argv) {
381
350
  process.stdout.write(`${prompt}\n`);
382
351
  return 0;
383
352
  }
384
- if (command === "config" && args.positionals[1] === "migrate") return runConfigMigration();
385
353
  process.stderr.write(`auto-mode: unknown command '${command}'\n\n${USAGE}`);
386
354
  return 2;
387
355
  }
388
- async function runConfigMigration() {
389
- const path = resolveConfigPath();
390
- try {
391
- const result = await writeMigratedConfig(path);
392
- process.stdout.write(`${result}\n`);
393
- return 0;
394
- } catch (error) {
395
- process.stderr.write(`auto-mode: ${error instanceof Error ? error.message : "config migration failed"}\n`);
396
- return 1;
397
- }
398
- }
399
356
  process.exitCode = await main(process.argv.slice(2));
400
357
  //#endregion
401
358
  export {};
package/dist/index.d.ts CHANGED
@@ -115,7 +115,7 @@ interface DecisionDiagnostics {
115
115
  }
116
116
  //#endregion
117
117
  //#region src/classify-action.d.ts
118
- type DecidingStage = 'local' | 'jev' | 'messages' | 'retry';
118
+ type DecidingStage = 'local' | 'containment' | 'jev' | 'messages' | 'retry';
119
119
  interface ActionOutcome {
120
120
  readonly verdict: Verdict | null;
121
121
  readonly decidingStage: DecidingStage;
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
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";
1
+ import { a as classifyWithModel, c as DEFAULT_CONFIG, d as resolveApiKey, f as resolveConfigPath, i as classifyLocally, n as parseActionRequest, o as parseModelVerdict, p as PRESETS, r as classifyAction, s as loadPolicy, t as renderVerdict, u as loadConfig } from "./render-verdict-1xtCI2is.js";
2
2
  export { DEFAULT_CONFIG, PRESETS, classifyAction, classifyLocally, classifyWithModel, loadConfig, loadPolicy, parseActionRequest, parseModelVerdict, renderVerdict, resolveApiKey, resolveConfigPath };
@@ -5569,6 +5569,764 @@ async function loadClaudeRules(path) {
5569
5569
  hard_deny: (rules.autoMode?.hard_deny ?? []).filter((entry) => entry !== "$defaults")
5570
5570
  };
5571
5571
  }
5572
+ //#endregion
5573
+ //#region src/rules/split-shell-command.ts
5574
+ function splitShellCommand(command) {
5575
+ const segments = [];
5576
+ let current = "";
5577
+ let quote = null;
5578
+ let unparsed = false;
5579
+ let i = 0;
5580
+ const collectSegment = () => {
5581
+ const text = current.trim();
5582
+ if (text !== "") segments.push({ text });
5583
+ current = "";
5584
+ };
5585
+ while (i < command.length) {
5586
+ const ch = command.charAt(i);
5587
+ if (quote !== null) {
5588
+ if (quote === "\"" && (command.startsWith("$(", i) || ch === "`")) unparsed = true;
5589
+ if (ch === quote) quote = null;
5590
+ current += ch;
5591
+ i += 1;
5592
+ continue;
5593
+ }
5594
+ if (ch === "'" || ch === "\"") {
5595
+ quote = ch;
5596
+ current += ch;
5597
+ i += 1;
5598
+ continue;
5599
+ }
5600
+ if (ch === "\\") {
5601
+ current += command.slice(i, i + 2);
5602
+ i += 2;
5603
+ continue;
5604
+ }
5605
+ if (command.startsWith("$(", i) || ch === "`" || command.startsWith("<(", i) || ch === ">") {
5606
+ unparsed = true;
5607
+ current += ch;
5608
+ i += 1;
5609
+ continue;
5610
+ }
5611
+ if (command.startsWith("&&", i) || command.startsWith("||", i)) {
5612
+ collectSegment();
5613
+ i += 2;
5614
+ continue;
5615
+ }
5616
+ if (ch === ";" || ch === "|" || ch === "\n" || ch === "&") {
5617
+ collectSegment();
5618
+ i += 1;
5619
+ continue;
5620
+ }
5621
+ current += ch;
5622
+ i += 1;
5623
+ }
5624
+ collectSegment();
5625
+ return {
5626
+ segments,
5627
+ hasUnparsedConstruct: unparsed || quote !== null
5628
+ };
5629
+ }
5630
+ //#endregion
5631
+ //#region src/containment/collect-scope-findings.ts
5632
+ function collectScopeFindings(action, scope) {
5633
+ const filePath = action.input["file_path"] ?? action.input["notebook_path"];
5634
+ const command = action.input["command"];
5635
+ const findings = [
5636
+ ...FILE_WRITE_TOOLS.has(action.tool) && typeof filePath === "string" ? collectTargetFindings(action.cwd, filePath, scope) : [],
5637
+ ...action.tool.startsWith("mcp__") ? collectToolFindings(action, scope) : [],
5638
+ ...action.tool === "Bash" && typeof command === "string" ? collectCommandFindings(command, action.cwd, scope) : []
5639
+ ];
5640
+ return findings.filter((finding, index) => findings.findIndex((other) => other.kind === finding.kind && other.target === finding.target) === index);
5641
+ }
5642
+ const FILE_WRITE_TOOLS = /* @__PURE__ */ new Set([
5643
+ "Edit",
5644
+ "MultiEdit",
5645
+ "Write",
5646
+ "NotebookEdit"
5647
+ ]);
5648
+ function collectTargetFindings(directory, target, scope) {
5649
+ const path = resolvePath(directory, target, scope.home);
5650
+ return path === null ? [] : collectPathFindings(path, scope);
5651
+ }
5652
+ const CREDENTIAL_PATHS = [
5653
+ ".ssh",
5654
+ ".aws",
5655
+ ".gnupg",
5656
+ ".gitconfig",
5657
+ ".netrc",
5658
+ ".npmrc",
5659
+ ".config/gh"
5660
+ ];
5661
+ function collectPathFindings(path, scope) {
5662
+ if (CREDENTIAL_PATHS.some((entry) => isUnder(path, join(scope.home, entry)))) return [{
5663
+ kind: "credential",
5664
+ target: path
5665
+ }];
5666
+ return isInScope(path, scope) ? [] : [{
5667
+ kind: "path",
5668
+ target: path
5669
+ }];
5670
+ }
5671
+ function collectToolFindings(action, scope) {
5672
+ const verb = action.tool.split("__").at(-1) ?? "";
5673
+ if (!/merge|create|comment|update|delete|push|close|review/u.test(verb)) return [];
5674
+ const number = action.input["pullNumber"] ?? action.input["pull_number"] ?? action.input["issue_number"];
5675
+ return !verb.includes("merge") && typeof number === "number" && scope.pullRequests.includes(number) ? [] : [{
5676
+ kind: "remote-write",
5677
+ target: action.tool
5678
+ }];
5679
+ }
5680
+ const PATH_WRITERS = /* @__PURE__ */ new Set([
5681
+ "rm",
5682
+ "rmdir",
5683
+ "shred",
5684
+ "truncate",
5685
+ "chmod",
5686
+ "chown"
5687
+ ]);
5688
+ const DESTINATION_WRITERS = /* @__PURE__ */ new Set([
5689
+ "cp",
5690
+ "mv",
5691
+ "rsync",
5692
+ "ln"
5693
+ ]);
5694
+ function collectCommandFindings(command, cwd, scope) {
5695
+ const findings = [];
5696
+ const hasPipe = /(?<!\|)\|(?!\|)/u.test(command);
5697
+ let directory = cwd;
5698
+ for (const segment of splitShellCommand(command).segments) {
5699
+ const [name, ...args] = getCommandWords(splitWords$1(segment.text));
5700
+ for (const target of collectRedirectTargets(segment.text)) findings.push(...collectTargetFindings(directory, target, scope));
5701
+ if (name === void 0) continue;
5702
+ if (name === "cd" && hasPipe) directory = null;
5703
+ else if (name === "cd") directory = args[0] === void 0 ? scope.home : resolvePath(directory, args[0], scope.home);
5704
+ else findings.push(...collectProgramFindings(name, args, directory, cwd, scope));
5705
+ }
5706
+ return findings;
5707
+ }
5708
+ function collectProgramFindings(name, args, directory, cwd, scope) {
5709
+ const resolve = (path) => collectTargetFindings(directory, path, scope);
5710
+ const operands = args.filter((word) => !word.startsWith("-"));
5711
+ if (name === "git") return collectGitFindings(args, directory, cwd, scope);
5712
+ if (name === "gh") return collectGhFindings(args, scope);
5713
+ if (PATH_WRITERS.has(name)) return operands.flatMap((path) => resolve(path));
5714
+ if (DESTINATION_WRITERS.has(name)) return collectCopyFindings(name, args, operands).flatMap((path) => resolve(path));
5715
+ if (name === "find" && args.some((word) => word === "-delete" || word === "-exec")) return operands.slice(0, 1).flatMap((path) => resolve(path));
5716
+ if ((name === "perl" || name === "sed") && args.some((word) => /^-\w*i/u.test(word))) return collectInPlaceFiles(args).flatMap((path) => resolve(path));
5717
+ if (name === "ssh-keygen") return collectKeygenFiles(args).flatMap((path) => resolve(path));
5718
+ if (name === "ssh") {
5719
+ const logIndex = args.indexOf("-E");
5720
+ const log = logIndex === -1 ? void 0 : args[logIndex + 1];
5721
+ return [...log === void 0 ? [] : resolve(log), ...collectSSHFindings(args)];
5722
+ }
5723
+ if (name === "scp") {
5724
+ const destination = collectSSHOperands(args).at(-1);
5725
+ if (destination === void 0 || /^[^/]+:/u.test(destination)) return destination === void 0 ? [] : [{
5726
+ kind: "remote-write",
5727
+ target: "scp"
5728
+ }];
5729
+ return resolve(destination);
5730
+ }
5731
+ if (name === "curl") return collectCurlFindings(args);
5732
+ const finding = findProgramFinding(name, args);
5733
+ return finding === null ? [] : [finding];
5734
+ }
5735
+ function collectCopyFindings(name, args, operands) {
5736
+ const optionIndex = args.findIndex((word) => word === "-t" || word === "--target-directory");
5737
+ let targetDirectory = args.find((word) => word.startsWith("--target-directory="))?.slice(19);
5738
+ if (targetDirectory === void 0 && optionIndex !== -1) targetDirectory = args[optionIndex + 1];
5739
+ if (targetDirectory !== void 0) {
5740
+ const sources = operands.filter((word) => word !== targetDirectory);
5741
+ return [targetDirectory, ...name === "mv" ? sources : []];
5742
+ }
5743
+ if (operands.length < 2) return [];
5744
+ return [operands.at(-1) ?? "", ...name === "mv" ? operands.slice(0, -1) : []];
5745
+ }
5746
+ const KEYGEN_READ_OPTIONS = /* @__PURE__ */ new Set([
5747
+ "-l",
5748
+ "-y",
5749
+ "-F",
5750
+ "-B",
5751
+ "-R"
5752
+ ]);
5753
+ function collectKeygenFiles(args) {
5754
+ if (args.some((word) => KEYGEN_READ_OPTIONS.has(word))) return [];
5755
+ const index = args.indexOf("-f");
5756
+ const file = index === -1 ? void 0 : args[index + 1];
5757
+ return file === void 0 ? [] : [file];
5758
+ }
5759
+ const SSH_VALUE_OPTIONS = /* @__PURE__ */ new Set([
5760
+ "-b",
5761
+ "-c",
5762
+ "-D",
5763
+ "-E",
5764
+ "-F",
5765
+ "-i",
5766
+ "-J",
5767
+ "-l",
5768
+ "-L",
5769
+ "-m"
5770
+ ]);
5771
+ const SSH_MORE_VALUE_OPTIONS = /* @__PURE__ */ new Set([
5772
+ "-o",
5773
+ "-p",
5774
+ "-P",
5775
+ "-Q",
5776
+ "-R",
5777
+ "-S",
5778
+ "-W",
5779
+ "-w"
5780
+ ]);
5781
+ function collectSSHOperands(args) {
5782
+ const operands = [];
5783
+ for (let index = 0; index < args.length; index += 1) {
5784
+ const word = args[index] ?? "";
5785
+ if (operands.length > 0 || !word.startsWith("-")) operands.push(word);
5786
+ else if (SSH_VALUE_OPTIONS.has(word) || SSH_MORE_VALUE_OPTIONS.has(word)) index += 1;
5787
+ }
5788
+ return operands;
5789
+ }
5790
+ const SSH_READS = /* @__PURE__ */ new Set([
5791
+ "cat",
5792
+ "df",
5793
+ "du",
5794
+ "free",
5795
+ "head",
5796
+ "id",
5797
+ "ls",
5798
+ "ps",
5799
+ "pwd",
5800
+ "stat",
5801
+ "tail",
5802
+ "uname",
5803
+ "uptime",
5804
+ "whoami"
5805
+ ]);
5806
+ function collectSSHFindings(args) {
5807
+ const [host, ...remote] = collectSSHOperands(args);
5808
+ if (host === void 0) return [];
5809
+ const command = remote.join(" ");
5810
+ const [program] = command.trim().split(/\s+/u);
5811
+ return SSH_READS.has(program ?? "") && !/[;&|<>`\n\r]|\$\(/u.test(command) ? [] : [{
5812
+ kind: "remote-write",
5813
+ target: "ssh"
5814
+ }];
5815
+ }
5816
+ const LOCAL_HOSTS = /* @__PURE__ */ new Set([
5817
+ "localhost",
5818
+ "127.0.0.1",
5819
+ "[::1]",
5820
+ "0.0.0.0"
5821
+ ]);
5822
+ function collectCurlFindings(args) {
5823
+ const joined = args.join(" ");
5824
+ const isUpload = args.some((word) => /^-[a-zA-Z]*[dFT]$|^--(?:data\S*|form|upload-file|json)$/u.test(word));
5825
+ const isWriteMethod = /(?:-X\s*|--request\s+)(?:POST|PUT|PATCH|DELETE)/u.test(joined);
5826
+ if (!isUpload && !isWriteMethod) return [];
5827
+ const hosts = args.filter((word) => /^(?:https?:\/\/|localhost\b|127\.)/u.test(word)).map((word) => /^(?:https?:\/\/)?(?<host>\[[^\]]+\]|[^/:?#\s]+)/u.exec(word)?.groups?.["host"]?.toLowerCase() ?? "");
5828
+ return hosts.length > 0 && hosts.every((host) => LOCAL_HOSTS.has(host)) ? [] : [{
5829
+ kind: "remote-write",
5830
+ target: "curl upload"
5831
+ }];
5832
+ }
5833
+ const GIT_DIRECTORY_WRITES = /* @__PURE__ */ new Set([
5834
+ "clean",
5835
+ "reset",
5836
+ "checkout",
5837
+ "switch",
5838
+ "merge",
5839
+ "commit",
5840
+ "rm",
5841
+ "stash",
5842
+ "push"
5843
+ ]);
5844
+ function collectGitFindings(args, directory, cwd, scope) {
5845
+ let gitDirectory = directory;
5846
+ let index = 0;
5847
+ while (index < args.length && args[index]?.startsWith("-") === true) {
5848
+ const option = args[index];
5849
+ if (option === "-C") gitDirectory = resolvePath(gitDirectory, args[index + 1] ?? "", scope.home);
5850
+ index += option === "-C" || option === "-c" ? 2 : 1;
5851
+ }
5852
+ const subcommand = args[index] ?? "";
5853
+ const rest = args.slice(index + 1);
5854
+ const operands = rest.filter((word) => !word.startsWith("-"));
5855
+ const findings = [];
5856
+ if (gitDirectory !== null && GIT_DIRECTORY_WRITES.has(subcommand) && !isInScope(gitDirectory, scope)) findings.push({
5857
+ kind: "path",
5858
+ target: gitDirectory
5859
+ });
5860
+ if (subcommand === "worktree" && operands[0] === "remove" && operands[1] !== void 0) findings.push(...collectTargetFindings(gitDirectory, operands[1], scope));
5861
+ 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({
5862
+ kind: "branch",
5863
+ target: branch
5864
+ });
5865
+ const isConfigRead = rest.some((word) => /^--(?:get|get-all|get-regexp|list)$|^-l$/u.test(word));
5866
+ if (subcommand === "config" && rest.includes("--global") && !isConfigRead) findings.push({
5867
+ kind: "credential",
5868
+ target: `git config ${operands[0] ?? ""}`
5869
+ });
5870
+ if (subcommand === "push") {
5871
+ const currentBranch = gitDirectory === cwd ? scope.currentBranch : null;
5872
+ findings.push(...collectPushFindings(rest, operands, currentBranch, scope));
5873
+ }
5874
+ return findings;
5875
+ }
5876
+ function collectPushFindings(rest, operands, currentBranch, scope) {
5877
+ const [remote, ...refspecs] = operands;
5878
+ const findings = [];
5879
+ if (rest.includes("--all") || rest.includes("--mirror")) findings.push({
5880
+ kind: "branch",
5881
+ target: "--all"
5882
+ });
5883
+ if (remote !== void 0 && !isUnresolved(remote) && !isOwnedRemote(remote, scope)) findings.push({
5884
+ kind: "remote-write",
5885
+ target: remote
5886
+ });
5887
+ const destinations = refspecs.length === 0 && !rest.includes("--tags") ? ["HEAD"] : refspecs;
5888
+ for (const refspec of destinations) {
5889
+ const named = (refspec.split(":").at(-1) ?? refspec).replace(/^\+/u, "").replace(/^refs\/heads\//u, "");
5890
+ const branch = named === "HEAD" ? currentBranch : named;
5891
+ if (branch !== null && !isUnresolved(branch) && !branch.startsWith("refs/tags/") && !scope.branches.includes(branch)) findings.push({
5892
+ kind: "branch",
5893
+ target: branch
5894
+ });
5895
+ }
5896
+ return findings;
5897
+ }
5898
+ function isUnresolved(word) {
5899
+ return /[$`]/u.test(word);
5900
+ }
5901
+ function isOwnedRemote(remote, scope) {
5902
+ if (scope.remotes.some((entry) => entry.name === remote)) return true;
5903
+ const slug = toRepositorySlug(remote);
5904
+ return slug !== null && scope.remotes.some((entry) => toRepositorySlug(entry.url) === slug);
5905
+ }
5906
+ function toRepositorySlug(reference) {
5907
+ const match = /^(?:[a-z+]+:\/\/)?(?:[^@/]+@)?(?<host>[^/:]+)[:/](?<owner>[^/]+)\/(?<name>[^/]+?)(?:\.git)?\/?$/iu.exec(reference);
5908
+ if (match?.groups === void 0) return null;
5909
+ const parts = match.groups;
5910
+ return `${parts["host"] ?? ""}/${parts["owner"] ?? ""}/${parts["name"] ?? ""}`.toLowerCase();
5911
+ }
5912
+ const GH_PR_WRITES = /* @__PURE__ */ new Set([
5913
+ "comment",
5914
+ "merge",
5915
+ "edit",
5916
+ "close",
5917
+ "review",
5918
+ "ready",
5919
+ "reopen"
5920
+ ]);
5921
+ const GH_REMOTE_WRITES = /* @__PURE__ */ new Set([
5922
+ "gist",
5923
+ "release",
5924
+ "repo",
5925
+ "ruleset",
5926
+ "label",
5927
+ "workflow"
5928
+ ]);
5929
+ const GH_CREDENTIAL_WRITES = /* @__PURE__ */ new Set([
5930
+ "secret",
5931
+ "variable",
5932
+ "ssh-key",
5933
+ "gpg-key"
5934
+ ]);
5935
+ const GH_READS = /* @__PURE__ */ new Set([
5936
+ "list",
5937
+ "view",
5938
+ "clone",
5939
+ "status",
5940
+ "checks",
5941
+ "diff"
5942
+ ]);
5943
+ function collectGhFindings(args, scope) {
5944
+ const group = args[0] ?? "";
5945
+ const verb = args[1] ?? "";
5946
+ if (group === "pr" && GH_PR_WRITES.has(verb)) {
5947
+ const number = Number(args.slice(2).find((word) => /^\d+$/u.test(word)));
5948
+ const repoIndex = args.findIndex((word) => word === "--repo" || word === "-R");
5949
+ const repo = repoIndex === -1 ? void 0 : args[repoIndex + 1];
5950
+ return verb !== "merge" && scope.pullRequests.includes(number) && (repo === void 0 || isCheckoutRepository(repo, scope)) ? [] : [{
5951
+ kind: "remote-write",
5952
+ target: `gh pr ${verb}`
5953
+ }];
5954
+ }
5955
+ if (group === "issue" && verb !== "" && !GH_READS.has(verb)) return [{
5956
+ kind: "remote-write",
5957
+ target: `gh issue ${verb}`
5958
+ }];
5959
+ if (group === "api") return collectGhAPIFindings(args.slice(1), scope);
5960
+ if (group === "auth" && [
5961
+ "refresh",
5962
+ "login",
5963
+ "logout"
5964
+ ].includes(verb)) return [{
5965
+ kind: "credential",
5966
+ target: `gh auth ${verb}`
5967
+ }];
5968
+ if (GH_CREDENTIAL_WRITES.has(group) && !GH_READS.has(verb)) return [{
5969
+ kind: "credential",
5970
+ target: `gh ${group} ${verb}`
5971
+ }];
5972
+ if (GH_REMOTE_WRITES.has(group) && !GH_READS.has(verb)) return [{
5973
+ kind: "remote-write",
5974
+ target: `gh ${group} ${verb}`
5975
+ }];
5976
+ return [];
5977
+ }
5978
+ function isCheckoutRepository(repo, scope) {
5979
+ const wanted = repo.toLowerCase().replace(/\.git$/u, "");
5980
+ return scope.remotes.some((remote) => {
5981
+ const slug = toRepositorySlug(remote.url);
5982
+ return slug !== null && (slug === wanted || slug.endsWith(`/${wanted}`));
5983
+ });
5984
+ }
5985
+ function collectGhAPIFindings(args, scope) {
5986
+ const endpoint = args.find((word) => !word.startsWith("-")) ?? "";
5987
+ if (endpoint === "graphql") return args.some((word) => /\bmutation\b/u.test(word)) ? [{
5988
+ kind: "remote-write",
5989
+ target: "gh api graphql mutation"
5990
+ }] : [];
5991
+ const methodIndex = args.findIndex((word) => word === "-X" || word === "--method");
5992
+ const method = methodIndex === -1 ? null : (args[methodIndex + 1] ?? "").toUpperCase();
5993
+ const hasFields = args.some((word) => /^(?:-f|-F|--field|--raw-field|--input)$/u.test(word));
5994
+ if (method === "GET" || method === null && !hasFields) return [];
5995
+ const pull = /^\/?repos\/(?<repo>[^/]+\/[^/]+)\/(?:pulls|issues)\/(?<number>\d+)(?:\/|$)/u.exec(endpoint)?.groups;
5996
+ if (pull !== void 0 && scope.pullRequests.includes(Number(pull["number"])) && isCheckoutRepository(pull["repo"] ?? "", scope) && !endpoint.includes("/merge")) return [];
5997
+ return [{
5998
+ kind: /secret|collaborator|protection|key|permission/u.test(endpoint) ? "credential" : "remote-write",
5999
+ target: `gh api ${endpoint}`
6000
+ }];
6001
+ }
6002
+ const IAM_WRITE = /^(?:add|attach|change|create|deactivate|delete|detach|enable|put|remove|reset|set|tag|untag|update|upload)-/u;
6003
+ function findProgramFinding(name, args) {
6004
+ const joined = args.join(" ");
6005
+ if (name === "op" && /^(?:item|vault|document) (?:edit|create|delete)\b/u.test(joined)) return {
6006
+ kind: "credential",
6007
+ target: `op ${args[0] ?? ""} ${args[1] ?? ""}`
6008
+ };
6009
+ if (name === "aws" && args[0] === "iam" && IAM_WRITE.test(args[1] ?? "")) return {
6010
+ kind: "credential",
6011
+ target: `aws iam ${args[1] ?? ""}`
6012
+ };
6013
+ if (name === "docker" && /\b(?:volume|system|image|container) prune\b|\bvolume rm\b|\bdown\b.*(?:-v|--volumes)\b/u.test(joined)) return {
6014
+ kind: "prune",
6015
+ target: `docker ${joined}`
6016
+ };
6017
+ if (name === "npm" && args[0] === "publish") return {
6018
+ kind: "remote-write",
6019
+ target: "npm publish"
6020
+ };
6021
+ if (name === "kubectl" && /\b(?:apply|delete|replace|patch|scale)\b/u.test(joined)) return {
6022
+ kind: "remote-write",
6023
+ target: "kubectl write"
6024
+ };
6025
+ if (name === "terraform" && /\b(?:apply|destroy|import)\b/u.test(joined)) return {
6026
+ kind: "remote-write",
6027
+ target: "terraform write"
6028
+ };
6029
+ if (name === "psql" && /\b(?:UPDATE|DELETE|DROP|INSERT|TRUNCATE|ALTER)\b/iu.test(joined)) return {
6030
+ kind: "remote-write",
6031
+ target: "database write"
6032
+ };
6033
+ if (/^(?:bun|npm|pnpm|yarn)$/u.test(name) && /\bdeploy\b/u.test(joined) && /prod/u.test(joined)) return {
6034
+ kind: "remote-write",
6035
+ target: "production deploy"
6036
+ };
6037
+ return null;
6038
+ }
6039
+ function collectRedirectTargets(text) {
6040
+ const targets = [];
6041
+ let quote = null;
6042
+ let index = 0;
6043
+ while (index < text.length) {
6044
+ const ch = text.charAt(index);
6045
+ if (quote !== null) {
6046
+ quote = ch === quote ? null : quote;
6047
+ index += 1;
6048
+ } else if (ch === "'" || ch === "\"") {
6049
+ quote = ch;
6050
+ index += 1;
6051
+ } else if (ch === ">" && text.charAt(index - 1) !== "<") {
6052
+ let next = index + 1;
6053
+ if (text.charAt(next) === ">" || text.charAt(next) === "|") next += 1;
6054
+ if (text.charAt(next) === "&") {
6055
+ index = next + 1;
6056
+ continue;
6057
+ }
6058
+ const [target, end] = readWord(text, next);
6059
+ if (target !== "") targets.push(target);
6060
+ index = end;
6061
+ } else index += 1;
6062
+ }
6063
+ return targets;
6064
+ }
6065
+ function readWord(text, start) {
6066
+ let index = start;
6067
+ while (/\s/u.test(text.charAt(index))) index += 1;
6068
+ let word = "";
6069
+ let quote = null;
6070
+ while (index < text.length) {
6071
+ const ch = text.charAt(index);
6072
+ if (quote !== null) {
6073
+ if (ch === quote) quote = null;
6074
+ else word += ch;
6075
+ } else if (ch === "'" || ch === "\"") quote = ch;
6076
+ else if (/[\s;&|<>()]/u.test(ch)) break;
6077
+ else word += ch;
6078
+ index += 1;
6079
+ }
6080
+ return [word, index];
6081
+ }
6082
+ function collectInPlaceFiles(args) {
6083
+ const files = [];
6084
+ let skipNext = false;
6085
+ let sawScript = false;
6086
+ for (const word of args) if (skipNext) {
6087
+ skipNext = false;
6088
+ sawScript = true;
6089
+ } else if (/^-\w*e$/u.test(word)) skipNext = true;
6090
+ else if (!word.startsWith("-")) {
6091
+ if (sawScript) files.push(word);
6092
+ else sawScript = true;
6093
+ }
6094
+ return files;
6095
+ }
6096
+ function getCommandWords(words) {
6097
+ let index = 0;
6098
+ while (index < words.length) {
6099
+ const word = words[index] ?? "";
6100
+ if (/^\w+=/u.test(word) || word === "sudo" || word === "command") index += 1;
6101
+ else if (word === "env") {
6102
+ index += 1;
6103
+ while (words[index] === "-u" || /^\w+=/u.test(words[index] ?? "")) index += words[index] === "-u" ? 2 : 1;
6104
+ } else if (word === "timeout") index += 2;
6105
+ else break;
6106
+ }
6107
+ return words.slice(index).filter((word, position, all) => !/^\d*[<>]/u.test(word) && !/^\d*>{1,2}\|?$/u.test(all[position - 1] ?? ""));
6108
+ }
6109
+ function splitWords$1(text) {
6110
+ const words = [];
6111
+ let current = "";
6112
+ let quote = null;
6113
+ let started = false;
6114
+ for (const ch of text) if (quote !== null) {
6115
+ if (ch === quote) quote = null;
6116
+ else current += ch;
6117
+ } else if (ch === "'" || ch === "\"") {
6118
+ quote = ch;
6119
+ started = true;
6120
+ } else if (/\s/u.test(ch)) {
6121
+ if (started) words.push(current);
6122
+ current = "";
6123
+ started = false;
6124
+ } else {
6125
+ current += ch;
6126
+ started = true;
6127
+ }
6128
+ if (started) words.push(current);
6129
+ return words;
6130
+ }
6131
+ function resolvePath(cwd, path, home) {
6132
+ const expanded = path.replace(/^(?:~|\$HOME|\$\{HOME\})(?=\/|$)/u, home);
6133
+ if (/[$`]|^~/u.test(expanded) || expanded === "") return null;
6134
+ if (cwd === null && !isAbsolute(expanded)) return null;
6135
+ const absolute = isAbsolute(expanded) || cwd === null ? expanded : join(cwd, expanded);
6136
+ return normalize(absolute).replace(/(?<=.)\/$/u, "");
6137
+ }
6138
+ const SCRATCH_PATHS = [
6139
+ "/tmp",
6140
+ "/dev/null",
6141
+ "/dev/stdout",
6142
+ "/dev/stderr"
6143
+ ];
6144
+ function isInScope(path, scope) {
6145
+ if (SCRATCH_PATHS.some((entry) => isUnder(path, entry))) return true;
6146
+ return scope.worktrees.some((worktree) => {
6147
+ const isNested = isUnder(path, join(worktree, ".worktrees"));
6148
+ const isOtherOwned = scope.worktrees.some((other) => other !== worktree && isUnder(path, other));
6149
+ return isUnder(path, worktree) && (!isNested || isOtherOwned);
6150
+ });
6151
+ }
6152
+ function isUnder(path, root) {
6153
+ return path === root || path.startsWith(`${root}/`);
6154
+ }
6155
+ //#endregion
6156
+ //#region src/model/load-repository-context.ts
6157
+ async function loadRepositoryContext(cwd) {
6158
+ if ([
6159
+ "GIT_DIR",
6160
+ "GIT_WORK_TREE",
6161
+ "GIT_COMMON_DIR"
6162
+ ].some((name) => process.env[name] !== void 0)) return null;
6163
+ let directory = resolve(cwd);
6164
+ for (;;) {
6165
+ const gitPath = join(directory, ".git");
6166
+ try {
6167
+ await stat(gitPath);
6168
+ } catch (error) {
6169
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) return null;
6170
+ const parent = dirname(directory);
6171
+ if (parent === directory) return null;
6172
+ directory = parent;
6173
+ continue;
6174
+ }
6175
+ let gitDir = gitPath;
6176
+ let head;
6177
+ try {
6178
+ try {
6179
+ head = await readFile(join(gitDir, "HEAD"), "utf8");
6180
+ } catch {
6181
+ const link = await readFile(gitPath, "utf8");
6182
+ const path = /^gitdir: (?<path>.+)\r?\n?$/u.exec(link)?.groups?.["path"];
6183
+ if (path === void 0) return null;
6184
+ gitDir = resolve(directory, path.trim());
6185
+ head = await readFile(join(gitDir, "HEAD"), "utf8");
6186
+ }
6187
+ } catch {
6188
+ return null;
6189
+ }
6190
+ let commonDir = gitDir;
6191
+ try {
6192
+ const common = await readFile(join(gitDir, "commondir"), "utf8");
6193
+ commonDir = resolve(gitDir, common.trim());
6194
+ } catch {
6195
+ commonDir = gitDir;
6196
+ }
6197
+ let defaultBranch = null;
6198
+ try {
6199
+ defaultBranch = parseReference(await readFile(join(commonDir, "refs", "remotes", "origin", "HEAD"), "utf8"), "refs/remotes/origin/");
6200
+ } catch {
6201
+ defaultBranch = null;
6202
+ }
6203
+ return {
6204
+ cwd: resolve(cwd),
6205
+ branch: parseReference(head, "refs/heads/"),
6206
+ defaultBranch
6207
+ };
6208
+ }
6209
+ }
6210
+ function parseReference(content, prefix) {
6211
+ const value = content.trim();
6212
+ const marker = `ref: ${prefix}`;
6213
+ if (!value.startsWith(marker)) return null;
6214
+ const branch = value.slice(marker.length);
6215
+ return branch !== "" && branch.length <= 256 && !/\s/u.test(branch) ? branch : null;
6216
+ }
6217
+ //#endregion
6218
+ //#region src/containment/build-cwd-scope.ts
6219
+ function buildCWDScope(input) {
6220
+ const isDefault = input.branch === input.defaultBranch || input.defaultBranch === null && DEFAULT_NAMES.has(input.branch ?? "");
6221
+ return {
6222
+ home: input.home,
6223
+ worktrees: [input.worktree],
6224
+ branches: input.branch !== null && !isDefault ? [input.branch] : [],
6225
+ currentBranch: input.branch,
6226
+ remotes: input.remotes,
6227
+ pullRequests: []
6228
+ };
6229
+ }
6230
+ const DEFAULT_NAMES = /* @__PURE__ */ new Set([
6231
+ "main",
6232
+ "master",
6233
+ "trunk",
6234
+ "develop"
6235
+ ]);
6236
+ //#endregion
6237
+ //#region src/containment/load-task-scope.ts
6238
+ async function loadTaskScope(cwd) {
6239
+ const [checkout, context] = await Promise.all([findCheckout(resolve(cwd)), loadRepositoryContext(cwd)]);
6240
+ return buildCWDScope({
6241
+ home: homedir(),
6242
+ worktree: checkout?.worktree ?? resolve(cwd),
6243
+ branch: context?.branch ?? null,
6244
+ defaultBranch: context?.defaultBranch ?? null,
6245
+ remotes: checkout === null ? [] : await readRemotes(checkout.commonDir)
6246
+ });
6247
+ }
6248
+ const GIT_OVERRIDES = [
6249
+ "GIT_DIR",
6250
+ "GIT_WORK_TREE",
6251
+ "GIT_COMMON_DIR"
6252
+ ];
6253
+ async function findCheckout(start) {
6254
+ if (GIT_OVERRIDES.some((name) => process.env[name] !== void 0)) return null;
6255
+ for (let directory = start;; directory = dirname(directory)) {
6256
+ const gitPath = join(directory, ".git");
6257
+ const info = await stat(gitPath).catch(() => null);
6258
+ if (info !== null) {
6259
+ const gitDir = info.isDirectory() ? gitPath : await findLinkedGitDir(directory, gitPath);
6260
+ return gitDir === null ? null : {
6261
+ worktree: directory,
6262
+ commonDir: await findCommonDir(gitDir)
6263
+ };
6264
+ }
6265
+ if (dirname(directory) === directory) return null;
6266
+ }
6267
+ }
6268
+ async function findLinkedGitDir(directory, gitPath) {
6269
+ const link = await readFile(gitPath, "utf8").catch(() => "");
6270
+ const path = /^gitdir: (?<path>.+)$/mu.exec(link)?.groups?.["path"];
6271
+ return path === void 0 ? null : resolve(directory, path.trim());
6272
+ }
6273
+ async function findCommonDir(gitDir) {
6274
+ const common = await readFile(join(gitDir, "commondir"), "utf8").catch(() => null);
6275
+ return common === null ? gitDir : resolve(gitDir, common.trim());
6276
+ }
6277
+ async function readRemotes(commonDir) {
6278
+ const config = await readFile(join(commonDir, "config"), "utf8").catch(() => "");
6279
+ const remotes = [];
6280
+ let section = null;
6281
+ for (const line of config.split("\n")) {
6282
+ const header = /^\s*\[remote "(?<name>[^"]+)"\]/u.exec(line)?.groups?.["name"];
6283
+ if (header !== void 0) section = header;
6284
+ else if (/^\s*\[/u.test(line)) section = null;
6285
+ else if (section !== null) {
6286
+ const url = /^\s*(?:push)?url\s*=\s*(?<url>\S+)/u.exec(line)?.groups?.["url"];
6287
+ if (url !== void 0) remotes.push({
6288
+ name: section,
6289
+ url
6290
+ });
6291
+ }
6292
+ }
6293
+ return remotes;
6294
+ }
6295
+ //#endregion
6296
+ //#region src/containment/check-containment.ts
6297
+ const CONTAINMENT_RULE = "Outside Task Scope";
6298
+ async function checkContainment(request) {
6299
+ let scope;
6300
+ try {
6301
+ scope = await loadTaskScope(request.cwd);
6302
+ } catch {
6303
+ return null;
6304
+ }
6305
+ const findings = collectScopeFindings({
6306
+ tool: request.toolName,
6307
+ cwd: request.cwd,
6308
+ input: request.toolInput
6309
+ }, scope);
6310
+ if (findings.length === 0) return null;
6311
+ return {
6312
+ rule: CONTAINMENT_RULE,
6313
+ reason: formatContainmentReason(findings, scope),
6314
+ findings
6315
+ };
6316
+ }
6317
+ const KIND_LABELS = {
6318
+ path: "path",
6319
+ branch: "branch",
6320
+ "remote-write": "remote target",
6321
+ credential: "credential or global setting",
6322
+ prune: "shared resource"
6323
+ };
6324
+ function formatContainmentReason(findings, scope) {
6325
+ const targets = findings.slice(0, 5).map((finding) => `${KIND_LABELS[finding.kind]} ${finding.target}`).join(", ");
6326
+ const more = findings.length > 5 ? ` and ${findings.length - 5} more` : "";
6327
+ const [branch] = scope.branches;
6328
+ return `This action writes outside the task scope: ${targets}${more}. The task owns ${`the worktree ${scope.worktrees.join(", ")}${branch === void 0 ? "" : ` and the branch ${branch}`}`}. Do the work inside the task's own worktree and branch, or ask the user to extend the task scope to this target.`;
6329
+ }
5572
6330
  const PRESETS = {
5573
6331
  jev: {
5574
6332
  protocol: "system-one",
@@ -5615,126 +6373,6 @@ function findPreset(id) {
5615
6373
  return Object.hasOwn(PRESETS, id) ? PRESETS[id] : void 0;
5616
6374
  }
5617
6375
  //#endregion
5618
- //#region src/config/normalize-config-file.ts
5619
- function normalizeConfigFile(json, path) {
5620
- if (!isRecord(json)) return {
5621
- file: json,
5622
- isLegacy: false,
5623
- warnings: []
5624
- };
5625
- const legacyKeys = Object.keys(json).filter((key) => LEGACY_KEYS.has(key));
5626
- if (legacyKeys.length === 0) return {
5627
- file: json,
5628
- isLegacy: false,
5629
- warnings: []
5630
- };
5631
- const currentKeys = Object.keys(json).filter((key) => !LEGACY_KEYS.has(key));
5632
- if (currentKeys.length > 0) throw new Error(`${path} mixes the old keys (${legacyKeys.join(", ")}) with the new ones (${currentKeys.join(", ")}); run \`auto-mode config migrate\` on a copy without the new keys`);
5633
- const parsed = legacyConfigSchema.safeParse(json);
5634
- if (!parsed.success) throw new Error(`${path} is not a valid config: ${prettifyError(parsed.error)}`);
5635
- const legacy = parsed.data;
5636
- const warnings = [`${path} uses the old config keys (${legacyKeys.join(", ")}); run \`auto-mode config migrate\` to rewrite it`, ...legacy.transcriptEntries === void 0 ? [] : [`${path} sets transcriptEntries, which has no effect; migration drops it`]];
5637
- return {
5638
- file: buildFileFromLegacy(legacy, path),
5639
- isLegacy: true,
5640
- warnings
5641
- };
5642
- }
5643
- function isRecord(value) {
5644
- return typeof value === "object" && value !== null && !Array.isArray(value);
5645
- }
5646
- const LEGACY_KEYS = /* @__PURE__ */ new Set([
5647
- "preset",
5648
- "provider",
5649
- "classifierPath",
5650
- "rulesPath",
5651
- "transcriptEntries",
5652
- "onFailure",
5653
- "claudeSettingsPath",
5654
- "minConfidence"
5655
- ]);
5656
- const text$1 = string().min(1);
5657
- const positive$1 = number().positive();
5658
- const legacyConfigSchema = strictObject({
5659
- preset: text$1.optional(),
5660
- provider: strictObject({
5661
- protocol: _enum(["messages", "system-one"]).optional(),
5662
- baseURL: text$1.optional(),
5663
- model: text$1.optional(),
5664
- apiKeyEnv: text$1.optional(),
5665
- apiKeyCommand: text$1.optional(),
5666
- reasoning: boolean().optional(),
5667
- maxTokens: positive$1.optional(),
5668
- timeoutMs: positive$1.optional()
5669
- }).optional(),
5670
- classifierPath: text$1.optional(),
5671
- rulesPath: text$1.optional(),
5672
- transcriptEntries: number().nonnegative().optional(),
5673
- onFailure: _enum(["defer", "deny"]).optional(),
5674
- claudeSettingsPath: text$1.nullable().optional(),
5675
- minConfidence: number().min(.5).max(1).optional()
5676
- });
5677
- function buildFileFromLegacy(legacy, path) {
5678
- const hasClassifier = legacy.preset !== void 0 || legacy.provider !== void 0;
5679
- const decision = {
5680
- ...hasClassifier ? { classifier: getLegacyClassifierID(legacy) } : {},
5681
- ...legacy.minConfidence === void 0 ? {} : { minConfidence: legacy.minConfidence },
5682
- ...legacy.onFailure === void 0 ? {} : { onFailure: legacy.onFailure }
5683
- };
5684
- const policy = {
5685
- ...legacy.rulesPath === void 0 ? {} : { rulesPath: legacy.rulesPath },
5686
- ...legacy.classifierPath === void 0 ? {} : { frameworkPath: legacy.classifierPath },
5687
- ...legacy.claudeSettingsPath === void 0 ? {} : { claudeSettingsPath: legacy.claudeSettingsPath }
5688
- };
5689
- return {
5690
- ...hasClassifier ? { classifiers: { [getLegacyClassifierID(legacy)]: buildClassifierEntry(getLegacyClassifierID(legacy), buildLegacyProvider(legacy, path)) } } : {},
5691
- ...Object.keys(decision).length === 0 ? {} : { decision },
5692
- ...Object.keys(policy).length === 0 ? {} : { policy }
5693
- };
5694
- }
5695
- function getLegacyClassifierID(legacy) {
5696
- if (legacy.preset !== void 0) return legacy.preset;
5697
- return legacy.provider !== void 0 && legacy.provider.protocol !== "system-one" ? "spark" : "jev";
5698
- }
5699
- function buildLegacyProvider(legacy, path) {
5700
- const preset = findPreset(getLegacyClassifierID(legacy));
5701
- if (preset === void 0) throw new Error(`${path} names an unknown preset '${legacy.preset}'; known presets are ${Object.keys(PRESETS).join(", ")}`);
5702
- const override = legacy.provider ?? {};
5703
- return {
5704
- protocol: override.protocol ?? preset.protocol ?? "messages",
5705
- baseURL: override.baseURL ?? preset.baseURL,
5706
- model: override.model ?? preset.model,
5707
- apiKeyEnv: override.apiKeyEnv ?? preset.apiKeyEnv,
5708
- apiKeyCommand: override.apiKeyCommand ?? preset.apiKeyCommand,
5709
- reasoning: override.reasoning ?? preset.reasoning,
5710
- maxTokens: override.maxTokens ?? preset.maxTokens,
5711
- timeoutMs: override.timeoutMs ?? preset.timeoutMs
5712
- };
5713
- }
5714
- const PROVIDER_FIELDS = [
5715
- "baseURL",
5716
- "model",
5717
- "apiKeyEnv",
5718
- "apiKeyCommand",
5719
- "reasoning",
5720
- "maxTokens",
5721
- "timeoutMs"
5722
- ];
5723
- function buildClassifierEntry(id, provider) {
5724
- const kind = pickEntryKind(id, provider);
5725
- const base = kind === "messages" ? MESSAGES_DEFAULTS : findPreset(kind) ?? {};
5726
- const fields = Object.fromEntries(PROVIDER_FIELDS.filter((field) => provider[field] !== void 0 && provider[field] !== base[field]).map((field) => [field, provider[field]]));
5727
- return {
5728
- ...kind === id ? {} : { kind },
5729
- ...fields
5730
- };
5731
- }
5732
- function pickEntryKind(id, provider) {
5733
- const preset = findPreset(id);
5734
- if (preset !== void 0 && (preset.protocol ?? "messages") === provider.protocol) return id;
5735
- return provider.protocol === "system-one" ? "jev" : "messages";
5736
- }
5737
- //#endregion
5738
6376
  //#region src/config/read-api-key-from-command.ts
5739
6377
  function readApiKeyFromCommand(command, options = {}) {
5740
6378
  const remainingMs = options.deadlineAt === void 0 ? 5e3 : options.deadlineAt - Date.now();
@@ -5826,8 +6464,7 @@ async function loadConfig(path = resolveConfigPath()) {
5826
6464
  } catch {
5827
6465
  throw new Error(`${path} is not valid JSON`);
5828
6466
  }
5829
- const normalized = normalizeConfigFile(json, path);
5830
- return buildConfig(normalized.file, path, normalized.warnings);
6467
+ return buildConfig(json, path);
5831
6468
  }
5832
6469
  const text = string().min(1);
5833
6470
  const positive = number().positive();
@@ -5851,11 +6488,11 @@ const configFileSchema = strictObject({
5851
6488
  claudeSettingsPath: text.nullable().optional()
5852
6489
  }).optional()
5853
6490
  });
5854
- function buildConfig(json, path, legacyWarnings) {
6491
+ function buildConfig(json, path) {
5855
6492
  const parsed = configFileSchema.safeParse(json);
5856
6493
  if (!parsed.success) throw new Error(`${path} is not a valid config: ${prettifyError(parsed.error)}`);
5857
6494
  const file = parsed.data;
5858
- const warnings = [...legacyWarnings];
6495
+ const warnings = [];
5859
6496
  const classifiers = buildRegistry(file.classifiers ?? {}, (id, entry) => parseClassifierEntry(id, entry));
5860
6497
  const scopeSources = buildRegistry(file.scopeSources ?? {}, (id, entry) => parseScopeSourceEntry(id, entry));
5861
6498
  warnings.push(...classifiers.dropped.map((line) => `${path}: classifiers.${line}`), ...scopeSources.dropped.map((line) => `${path}: scopeSources.${line}`));
@@ -6245,68 +6882,6 @@ function formatClassifierNote(note, apiKey) {
6245
6882
  return apiKey === null || apiKey === "" ? note : note.split(apiKey).join("[redacted]");
6246
6883
  }
6247
6884
  //#endregion
6248
- //#region src/model/load-repository-context.ts
6249
- async function loadRepositoryContext(cwd) {
6250
- if ([
6251
- "GIT_DIR",
6252
- "GIT_WORK_TREE",
6253
- "GIT_COMMON_DIR"
6254
- ].some((name) => process.env[name] !== void 0)) return null;
6255
- let directory = resolve(cwd);
6256
- for (;;) {
6257
- const gitPath = join(directory, ".git");
6258
- try {
6259
- await stat(gitPath);
6260
- } catch (error) {
6261
- if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) return null;
6262
- const parent = dirname(directory);
6263
- if (parent === directory) return null;
6264
- directory = parent;
6265
- continue;
6266
- }
6267
- let gitDir = gitPath;
6268
- let head;
6269
- try {
6270
- try {
6271
- head = await readFile(join(gitDir, "HEAD"), "utf8");
6272
- } catch {
6273
- const link = await readFile(gitPath, "utf8");
6274
- const path = /^gitdir: (?<path>.+)\r?\n?$/u.exec(link)?.groups?.["path"];
6275
- if (path === void 0) return null;
6276
- gitDir = resolve(directory, path.trim());
6277
- head = await readFile(join(gitDir, "HEAD"), "utf8");
6278
- }
6279
- } catch {
6280
- return null;
6281
- }
6282
- let commonDir = gitDir;
6283
- try {
6284
- const common = await readFile(join(gitDir, "commondir"), "utf8");
6285
- commonDir = resolve(gitDir, common.trim());
6286
- } catch {
6287
- commonDir = gitDir;
6288
- }
6289
- let defaultBranch = null;
6290
- try {
6291
- defaultBranch = parseReference(await readFile(join(commonDir, "refs", "remotes", "origin", "HEAD"), "utf8"), "refs/remotes/origin/");
6292
- } catch {
6293
- defaultBranch = null;
6294
- }
6295
- return {
6296
- cwd: resolve(cwd),
6297
- branch: parseReference(head, "refs/heads/"),
6298
- defaultBranch
6299
- };
6300
- }
6301
- }
6302
- function parseReference(content, prefix) {
6303
- const value = content.trim();
6304
- const marker = `ref: ${prefix}`;
6305
- if (!value.startsWith(marker)) return null;
6306
- const branch = value.slice(marker.length);
6307
- return branch !== "" && branch.length <= 256 && !/\s/u.test(branch) ? branch : null;
6308
- }
6309
- //#endregion
6310
6885
  //#region node_modules/.bun/ts-pattern@5.9.0/node_modules/ts-pattern/dist/index.js
6311
6886
  const t = Symbol.for("@ts-pattern/matcher");
6312
6887
  const e = Symbol.for("@ts-pattern/isVariadic");
@@ -7090,64 +7665,6 @@ const REGENERABLE_DIRS = /* @__PURE__ */ new Set([
7090
7665
  "venv"
7091
7666
  ]);
7092
7667
  //#endregion
7093
- //#region src/rules/split-shell-command.ts
7094
- function splitShellCommand(command) {
7095
- const segments = [];
7096
- let current = "";
7097
- let quote = null;
7098
- let unparsed = false;
7099
- let i = 0;
7100
- const collectSegment = () => {
7101
- const text = current.trim();
7102
- if (text !== "") segments.push({ text });
7103
- current = "";
7104
- };
7105
- while (i < command.length) {
7106
- const ch = command.charAt(i);
7107
- if (quote !== null) {
7108
- if (quote === "\"" && (command.startsWith("$(", i) || ch === "`")) unparsed = true;
7109
- if (ch === quote) quote = null;
7110
- current += ch;
7111
- i += 1;
7112
- continue;
7113
- }
7114
- if (ch === "'" || ch === "\"") {
7115
- quote = ch;
7116
- current += ch;
7117
- i += 1;
7118
- continue;
7119
- }
7120
- if (ch === "\\") {
7121
- current += command.slice(i, i + 2);
7122
- i += 2;
7123
- continue;
7124
- }
7125
- if (command.startsWith("$(", i) || ch === "`" || command.startsWith("<(", i) || ch === ">") {
7126
- unparsed = true;
7127
- current += ch;
7128
- i += 1;
7129
- continue;
7130
- }
7131
- if (command.startsWith("&&", i) || command.startsWith("||", i)) {
7132
- collectSegment();
7133
- i += 2;
7134
- continue;
7135
- }
7136
- if (ch === ";" || ch === "|" || ch === "\n" || ch === "&") {
7137
- collectSegment();
7138
- i += 1;
7139
- continue;
7140
- }
7141
- current += ch;
7142
- i += 1;
7143
- }
7144
- collectSegment();
7145
- return {
7146
- segments,
7147
- hasUnparsedConstruct: unparsed || quote !== null
7148
- };
7149
- }
7150
- //#endregion
7151
7668
  //#region src/rules/classify-locally.ts
7152
7669
  const ESCALATE = { kind: "escalate" };
7153
7670
  function classifyLocally(payload) {
@@ -7225,12 +7742,18 @@ async function classifyAction(request, config, options = {}) {
7225
7742
  };
7226
7743
  }
7227
7744
  const local = configured !== null && (configured.hard_deny.length > 0 || configured.soft_deny.length > 0) ? { kind: "escalate" } : classifyLocally(request);
7228
- if (local.kind === "allow") return {
7229
- verdict: { kind: "allow" },
7230
- decidingStage: "local",
7231
- note: `allowed by ${local.exception} (local)`,
7232
- status: "allow"
7233
- };
7745
+ if (local.kind === "allow" && local.exception === READ_ONLY_EXCEPTION) return buildLocalAllow(local.exception);
7746
+ const containment = await checkContainment(request);
7747
+ if (containment !== null) {
7748
+ const guidance = await tryReadDenialGuidance();
7749
+ return {
7750
+ verdict: buildGuidedDeny(containment.rule, containment.reason, guidance),
7751
+ decidingStage: "containment",
7752
+ note: `denied by the containment check: ${containment.findings.map((finding) => finding.target).join(", ")}`,
7753
+ status: "deny"
7754
+ };
7755
+ }
7756
+ if (local.kind === "allow") return buildLocalAllow(local.exception);
7234
7757
  if (options.localOnly === true) return {
7235
7758
  verdict: null,
7236
7759
  decidingStage: "local",
@@ -7245,6 +7768,15 @@ async function classifyAction(request, config, options = {}) {
7245
7768
  status: outcome.diagnostics?.status ?? outcome.verdict?.kind ?? "failure"
7246
7769
  };
7247
7770
  }
7771
+ const READ_ONLY_EXCEPTION = "Read-only actions";
7772
+ function buildLocalAllow(exception) {
7773
+ return {
7774
+ verdict: { kind: "allow" },
7775
+ decidingStage: "local",
7776
+ note: `allowed by ${exception} (local)`,
7777
+ status: "allow"
7778
+ };
7779
+ }
7248
7780
  function buildGuidedDeny(rule, reason, guidance) {
7249
7781
  const sentence = /[.!?]$/u.test(reason) ? reason : `${reason}.`;
7250
7782
  return {
@@ -7318,4 +7850,4 @@ function renderVerdict(verdict) {
7318
7850
  })).exhaustive();
7319
7851
  }
7320
7852
  //#endregion
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 };
7853
+ export { classifyWithModel as a, DEFAULT_CONFIG as c, resolveApiKey as d, resolveConfigPath as f, string as g, strictObject as h, classifyLocally as i, DEFAULT_DENIAL_BUDGET as l, number as m, parseActionRequest as n, parseModelVerdict as o, PRESETS as p, classifyAction as r, loadPolicy as s, renderVerdict as t, loadConfig as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auto-mode",
3
- "version": "0.8.0",
3
+ "version": "0.10.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",