auto-mode 0.9.0 → 0.11.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,7 +1,8 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
- import { dirname, isAbsolute, join, normalize, parse, relative, resolve } from "node:path";
3
+ import { dirname, isAbsolute, join, matchesGlob, normalize, parse, relative, resolve } from "node:path";
4
4
  import { spawn } from "node:child_process";
5
+ import { createHash } from "node:crypto";
5
6
  import { existsSync } from "node:fs";
6
7
  //#region node_modules/.bun/zod@4.6.2/node_modules/zod/v4/core/util.js
7
8
  function getEnumValues(entries) {
@@ -2922,7 +2923,7 @@ function bucketFor(state, inst) {
2922
2923
  return bucket;
2923
2924
  }
2924
2925
  let handoff;
2925
- const open = [];
2926
+ const open$1 = [];
2926
2927
  const memo = {
2927
2928
  alloc(_inst, payload, empty) {
2928
2929
  const bucket = handoff;
@@ -2933,7 +2934,7 @@ const memo = {
2933
2934
  issues: null
2934
2935
  };
2935
2936
  bucket.set(payload.value, entry);
2936
- open.push(entry);
2937
+ open$1.push(entry);
2937
2938
  return empty;
2938
2939
  },
2939
2940
  guard(inst) {
@@ -2999,10 +3000,10 @@ const memo = {
2999
3000
  return payload;
3000
3001
  }
3001
3002
  handoff = bucket;
3002
- const depth = open.length;
3003
+ const depth = open$1.length;
3003
3004
  const result = base(payload, ctx);
3004
3005
  handoff = void 0;
3005
- const entry = open.length > depth ? open.pop() : void 0;
3006
+ const entry = open$1.length > depth ? open$1.pop() : void 0;
3006
3007
  if (result instanceof Promise) return result.then((r) => {
3007
3008
  if (entry) entry.issues = r.issues.length ? cloneIssues(r.issues) : NO_ISSUES;
3008
3009
  return r;
@@ -5526,49 +5527,6 @@ function refine(fn, _params = {}) {
5526
5527
  function superRefine(fn, params) {
5527
5528
  return /* @__PURE__ */ _superRefine(fn, params);
5528
5529
  }
5529
- //#endregion
5530
- //#region src/config/load-claude-rules.ts
5531
- async function loadClaudeRules(path) {
5532
- const empty = {
5533
- environment: [],
5534
- allow: [],
5535
- soft_deny: [],
5536
- hard_deny: []
5537
- };
5538
- if (path === null) return empty;
5539
- const configDir = process.env["CLAUDE_CONFIG_DIR"];
5540
- const base = configDir === void 0 || configDir === "" ? join(homedir(), ".claude") : configDir;
5541
- const settingsPath = path ?? join(base, "settings.json");
5542
- let raw;
5543
- try {
5544
- raw = await readFile(settingsPath, "utf8");
5545
- } catch (error) {
5546
- if (error instanceof Error && "code" in error && error.code === "ENOENT") return empty;
5547
- throw new Error("Claude settings unreadable", { cause: error });
5548
- }
5549
- const entries = array(string()).optional();
5550
- const schema = object({ autoMode: object({
5551
- environment: entries,
5552
- allow: entries,
5553
- soft_deny: entries,
5554
- hard_deny: entries
5555
- }).optional() });
5556
- let body;
5557
- try {
5558
- body = JSON.parse(raw);
5559
- } catch {
5560
- throw new Error("Claude settings contain invalid JSON");
5561
- }
5562
- const parsed = schema.safeParse(body);
5563
- if (!parsed.success) throw new Error("Claude autoMode settings are invalid");
5564
- const rules = parsed.data;
5565
- return {
5566
- environment: (rules.autoMode?.environment ?? []).filter((entry) => entry !== "$defaults"),
5567
- allow: (rules.autoMode?.allow ?? []).filter((entry) => entry !== "$defaults"),
5568
- soft_deny: (rules.autoMode?.soft_deny ?? []).filter((entry) => entry !== "$defaults"),
5569
- hard_deny: (rules.autoMode?.hard_deny ?? []).filter((entry) => entry !== "$defaults")
5570
- };
5571
- }
5572
5530
  const PRESETS = {
5573
5531
  jev: {
5574
5532
  protocol: "system-one",
@@ -5681,11 +5639,16 @@ const DEFAULT_DENIAL_BUDGET = {
5681
5639
  consecutive: 3,
5682
5640
  perSession: 20
5683
5641
  };
5642
+ const DEFAULT_SCOPE_SOURCES = {
5643
+ cwd: { kind: "cwd" },
5644
+ session: { kind: "session" }
5645
+ };
5684
5646
  const DEFAULT_CONFIG = {
5685
5647
  provider: DEFAULT_PROVIDER,
5686
5648
  onFailure: "defer",
5687
5649
  minConfidence: .8,
5688
- denialBudget: DEFAULT_DENIAL_BUDGET
5650
+ denialBudget: DEFAULT_DENIAL_BUDGET,
5651
+ scopeSources: DEFAULT_SCOPE_SOURCES
5689
5652
  };
5690
5653
  function resolveConfigPath() {
5691
5654
  const xdg = process.env["XDG_CONFIG_HOME"];
@@ -5737,14 +5700,15 @@ function buildConfig(json, path) {
5737
5700
  const warnings = [];
5738
5701
  const classifiers = buildRegistry(file.classifiers ?? {}, (id, entry) => parseClassifierEntry(id, entry));
5739
5702
  const scopeSources = buildRegistry(file.scopeSources ?? {}, (id, entry) => parseScopeSourceEntry(id, entry));
5740
- warnings.push(...classifiers.dropped.map((line) => `${path}: classifiers.${line}`), ...scopeSources.dropped.map((line) => `${path}: scopeSources.${line}`));
5703
+ const configuredSources = file.scopeSources === void 0 ? null : scopeSources.entries;
5704
+ warnings.push(...classifiers.dropped.map((line) => `${path}: classifiers.${line}`), ...scopeSources.dropped.map((line) => `${path}: scopeSources.${line}`), ...collectScopeWarnings(configuredSources).map((line) => `${path}: scopeSources ${line}`));
5741
5705
  const decision = file.decision ?? {};
5742
5706
  const policy = file.policy ?? {};
5743
5707
  const judgeID = decision.judge ?? null;
5744
5708
  return {
5745
5709
  provider: resolveRole("classifier", decision.classifier ?? "jev", classifiers.entries, classifiers.droppedIDs, path),
5746
5710
  judge: judgeID === null ? null : resolveRole("judge", judgeID, classifiers.entries, classifiers.droppedIDs, path),
5747
- scopeSources: scopeSources.entries,
5711
+ scopeSources: configuredSources ?? DEFAULT_SCOPE_SOURCES,
5748
5712
  classifierPath: expandHomePath(policy.frameworkPath ?? void 0),
5749
5713
  rulesPath: expandHomePath(policy.rulesPath ?? void 0),
5750
5714
  onFailure: decision.onFailure ?? DEFAULT_CONFIG.onFailure,
@@ -5757,6 +5721,11 @@ function buildConfig(json, path) {
5757
5721
  warnings
5758
5722
  };
5759
5723
  }
5724
+ function collectScopeWarnings(entries) {
5725
+ if (entries === null) return [];
5726
+ const sources = Object.values(entries);
5727
+ return [...sources.some((source) => source.kind === "cwd") ? [] : ["has no cwd entry, so the task owns only what the other sources name"], ...sources.flatMap((source) => source.paths ?? []).filter((glob) => glob.includes(".worktrees")).map((glob) => `glob ${glob} covers worktrees that other tasks own`)];
5728
+ }
5760
5729
  function buildRegistry(raw, parseEntry) {
5761
5730
  const entries = /* @__PURE__ */ new Map();
5762
5731
  const dropped = [];
@@ -5825,7 +5794,7 @@ function parseScopeSourceEntry(id, entry) {
5825
5794
  if (kind === void 0) return `unknown kind '${parsed.data.kind ?? id}'; known kinds are ${SCOPE_SOURCE_KINDS.join(", ")}`;
5826
5795
  if (kind === "globs") return parsed.data.paths === void 0 ? "kind 'globs' needs paths" : {
5827
5796
  kind,
5828
- paths: parsed.data.paths
5797
+ paths: parsed.data.paths.map((glob) => expandHomePath(glob) ?? glob)
5829
5798
  };
5830
5799
  return parsed.data.paths === void 0 ? { kind } : `kind '${kind}' takes no paths`;
5831
5800
  }
@@ -5856,6 +5825,940 @@ function resolveApiKey(provider, options = {}) {
5856
5825
  return readApiKeyFromCommand(provider.apiKeyCommand, options);
5857
5826
  }
5858
5827
  //#endregion
5828
+ //#region src/config/load-claude-rules.ts
5829
+ async function loadClaudeRules(path) {
5830
+ const empty = {
5831
+ environment: [],
5832
+ allow: [],
5833
+ soft_deny: [],
5834
+ hard_deny: []
5835
+ };
5836
+ if (path === null) return empty;
5837
+ const configDir = process.env["CLAUDE_CONFIG_DIR"];
5838
+ const base = configDir === void 0 || configDir === "" ? join(homedir(), ".claude") : configDir;
5839
+ const settingsPath = path ?? join(base, "settings.json");
5840
+ let raw;
5841
+ try {
5842
+ raw = await readFile(settingsPath, "utf8");
5843
+ } catch (error) {
5844
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return empty;
5845
+ throw new Error("Claude settings unreadable", { cause: error });
5846
+ }
5847
+ const entries = array(string()).optional();
5848
+ const schema = object({ autoMode: object({
5849
+ environment: entries,
5850
+ allow: entries,
5851
+ soft_deny: entries,
5852
+ hard_deny: entries
5853
+ }).optional() });
5854
+ let body;
5855
+ try {
5856
+ body = JSON.parse(raw);
5857
+ } catch {
5858
+ throw new Error("Claude settings contain invalid JSON");
5859
+ }
5860
+ const parsed = schema.safeParse(body);
5861
+ if (!parsed.success) throw new Error("Claude autoMode settings are invalid");
5862
+ const rules = parsed.data;
5863
+ return {
5864
+ environment: (rules.autoMode?.environment ?? []).filter((entry) => entry !== "$defaults"),
5865
+ allow: (rules.autoMode?.allow ?? []).filter((entry) => entry !== "$defaults"),
5866
+ soft_deny: (rules.autoMode?.soft_deny ?? []).filter((entry) => entry !== "$defaults"),
5867
+ hard_deny: (rules.autoMode?.hard_deny ?? []).filter((entry) => entry !== "$defaults")
5868
+ };
5869
+ }
5870
+ //#endregion
5871
+ //#region src/model/load-repository-context.ts
5872
+ async function loadRepositoryContext(cwd) {
5873
+ if ([
5874
+ "GIT_DIR",
5875
+ "GIT_WORK_TREE",
5876
+ "GIT_COMMON_DIR"
5877
+ ].some((name) => process.env[name] !== void 0)) return null;
5878
+ let directory = resolve(cwd);
5879
+ for (;;) {
5880
+ const gitPath = join(directory, ".git");
5881
+ try {
5882
+ await stat(gitPath);
5883
+ } catch (error) {
5884
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) return null;
5885
+ const parent = dirname(directory);
5886
+ if (parent === directory) return null;
5887
+ directory = parent;
5888
+ continue;
5889
+ }
5890
+ let gitDir = gitPath;
5891
+ let head;
5892
+ try {
5893
+ try {
5894
+ head = await readFile(join(gitDir, "HEAD"), "utf8");
5895
+ } catch {
5896
+ const link = await readFile(gitPath, "utf8");
5897
+ const path = /^gitdir: (?<path>.+)\r?\n?$/u.exec(link)?.groups?.["path"];
5898
+ if (path === void 0) return null;
5899
+ gitDir = resolve(directory, path.trim());
5900
+ head = await readFile(join(gitDir, "HEAD"), "utf8");
5901
+ }
5902
+ } catch {
5903
+ return null;
5904
+ }
5905
+ let commonDir = gitDir;
5906
+ try {
5907
+ const common = await readFile(join(gitDir, "commondir"), "utf8");
5908
+ commonDir = resolve(gitDir, common.trim());
5909
+ } catch {
5910
+ commonDir = gitDir;
5911
+ }
5912
+ let defaultBranch = null;
5913
+ try {
5914
+ defaultBranch = parseReference(await readFile(join(commonDir, "refs", "remotes", "origin", "HEAD"), "utf8"), "refs/remotes/origin/");
5915
+ } catch {
5916
+ defaultBranch = null;
5917
+ }
5918
+ return {
5919
+ cwd: resolve(cwd),
5920
+ branch: parseReference(head, "refs/heads/"),
5921
+ defaultBranch
5922
+ };
5923
+ }
5924
+ }
5925
+ function parseReference(content, prefix) {
5926
+ const value = content.trim();
5927
+ const marker = `ref: ${prefix}`;
5928
+ if (!value.startsWith(marker)) return null;
5929
+ const branch = value.slice(marker.length);
5930
+ return branch !== "" && branch.length <= 256 && !/\s/u.test(branch) ? branch : null;
5931
+ }
5932
+ //#endregion
5933
+ //#region src/containment/to-repository-slug.ts
5934
+ function toRepositorySlug(reference) {
5935
+ const match = /^(?:[a-z+]+:\/\/)?(?:[^@/]+@)?(?<host>[^/:]+)[:/](?<owner>[^/]+)\/(?<name>[^/]+?)(?:\.git)?\/?$/iu.exec(reference);
5936
+ if (match?.groups === void 0) return null;
5937
+ const parts = match.groups;
5938
+ return `${parts["host"] ?? ""}/${parts["owner"] ?? ""}/${parts["name"] ?? ""}`.toLowerCase();
5939
+ }
5940
+ //#endregion
5941
+ //#region src/scope/merge-scope-facts.ts
5942
+ function mergeScopeFacts(all) {
5943
+ const pullRequests = /* @__PURE__ */ new Map();
5944
+ for (const pull of all.flatMap((facts) => facts.pullRequests)) pullRequests.set(`${pull.repository}#${String(pull.number)}`, pull);
5945
+ return {
5946
+ worktrees: [...new Set(all.flatMap((facts) => facts.worktrees))],
5947
+ branches: [...new Set(all.flatMap((facts) => facts.branches))],
5948
+ pullRequests: [...pullRequests.values()],
5949
+ pathGlobs: [...new Set(all.flatMap((facts) => facts.pathGlobs))]
5950
+ };
5951
+ }
5952
+ //#endregion
5953
+ //#region src/scope/build-task-scope.ts
5954
+ function buildTaskScope(input) {
5955
+ const merged = mergeScopeFacts(input.facts);
5956
+ const branches = merged.branches.filter((branch) => branch !== input.defaultBranch && !(input.defaultBranch === null && DEFAULT_NAMES.has(branch)));
5957
+ const repositories = new Set(input.remotes.map((remote) => toRepositorySlug(remote.url)));
5958
+ const pullRequests = merged.pullRequests.filter((pull) => branches.includes(pull.head) && repositories.has(pull.repository)).map((pull) => ({
5959
+ number: pull.number,
5960
+ repository: pull.repository
5961
+ }));
5962
+ return {
5963
+ home: input.home,
5964
+ worktrees: merged.worktrees,
5965
+ branches,
5966
+ currentBranch: input.currentBranch,
5967
+ remotes: input.remotes,
5968
+ pullRequests,
5969
+ pathGlobs: merged.pathGlobs
5970
+ };
5971
+ }
5972
+ const DEFAULT_NAMES = /* @__PURE__ */ new Set([
5973
+ "main",
5974
+ "master",
5975
+ "trunk",
5976
+ "develop"
5977
+ ]);
5978
+ //#endregion
5979
+ //#region src/scope/find-checkout.ts
5980
+ const GIT_OVERRIDES = [
5981
+ "GIT_DIR",
5982
+ "GIT_WORK_TREE",
5983
+ "GIT_COMMON_DIR"
5984
+ ];
5985
+ async function findCheckout(start) {
5986
+ if (GIT_OVERRIDES.some((name) => process.env[name] !== void 0)) return null;
5987
+ for (let directory = start;; directory = dirname(directory)) {
5988
+ const gitPath = join(directory, ".git");
5989
+ const info = await stat(gitPath).catch(() => null);
5990
+ if (info !== null) {
5991
+ const gitDir = info.isDirectory() ? gitPath : await findLinkedGitDir(directory, gitPath);
5992
+ return gitDir === null ? null : {
5993
+ worktree: directory,
5994
+ commonDir: await findCommonDir(gitDir)
5995
+ };
5996
+ }
5997
+ if (dirname(directory) === directory) return null;
5998
+ }
5999
+ }
6000
+ async function findLinkedGitDir(directory, gitPath) {
6001
+ const link = await readFile(gitPath, "utf8").catch(() => "");
6002
+ const path = /^gitdir: (?<path>.+)$/mu.exec(link)?.groups?.["path"];
6003
+ return path === void 0 ? null : resolve(directory, path.trim());
6004
+ }
6005
+ async function findCommonDir(gitDir) {
6006
+ const common = await readFile(join(gitDir, "commondir"), "utf8").catch(() => null);
6007
+ return common === null ? gitDir : resolve(gitDir, common.trim());
6008
+ }
6009
+ //#endregion
6010
+ //#region src/scope/load-checkout-remotes.ts
6011
+ async function loadCheckoutRemotes(commonDir) {
6012
+ const config = await readFile(join(commonDir, "config"), "utf8").catch(() => "");
6013
+ const remotes = [];
6014
+ let section = null;
6015
+ for (const line of config.split("\n")) {
6016
+ const header = /^\s*\[remote "(?<name>[^"]+)"\]/u.exec(line)?.groups?.["name"];
6017
+ if (header !== void 0) section = header;
6018
+ else if (/^\s*\[/u.test(line)) section = null;
6019
+ else if (section !== null) {
6020
+ const url = /^\s*(?:push)?url\s*=\s*(?<url>\S+)/u.exec(line)?.groups?.["url"];
6021
+ if (url !== void 0) remotes.push({
6022
+ name: section,
6023
+ url
6024
+ });
6025
+ }
6026
+ }
6027
+ return remotes;
6028
+ }
6029
+ //#endregion
6030
+ //#region src/scope/types.ts
6031
+ const EMPTY_SCOPE_FACTS = {
6032
+ worktrees: [],
6033
+ branches: [],
6034
+ pullRequests: [],
6035
+ pathGlobs: []
6036
+ };
6037
+ const EMPTY_SESSION_SCOPE = {
6038
+ worktrees: [],
6039
+ branches: [],
6040
+ pullRequests: []
6041
+ };
6042
+ //#endregion
6043
+ //#region src/scope/load-session-scope.ts
6044
+ const name = string().min(1);
6045
+ const pullRequestSchema = strictObject({
6046
+ number: number().int().positive(),
6047
+ head: name,
6048
+ repository: name
6049
+ });
6050
+ const branchSchema = strictObject({
6051
+ name,
6052
+ commonDir: name
6053
+ });
6054
+ const sessionScopeSchema = strictObject({
6055
+ worktrees: array(name),
6056
+ branches: array(branchSchema),
6057
+ pullRequests: array(pullRequestSchema)
6058
+ });
6059
+ async function loadSessionScope(path) {
6060
+ let raw;
6061
+ try {
6062
+ raw = await readFile(path, "utf8");
6063
+ } catch {
6064
+ return EMPTY_SESSION_SCOPE;
6065
+ }
6066
+ try {
6067
+ const parsed = sessionScopeSchema.safeParse(JSON.parse(raw));
6068
+ return parsed.success ? parsed.data : EMPTY_SESSION_SCOPE;
6069
+ } catch {
6070
+ return EMPTY_SESSION_SCOPE;
6071
+ }
6072
+ }
6073
+ //#endregion
6074
+ //#region src/scope/resolve-session-scope-path.ts
6075
+ function resolveSessionScopePath(stateDir, sessionID) {
6076
+ const key = createHash("sha256").update(sessionID).digest("hex").slice(0, 32);
6077
+ return join(stateDir, "session-scope", `${key}.json`);
6078
+ }
6079
+ //#endregion
6080
+ //#region src/scope/pick-scope-source-reader.ts
6081
+ function pickScopeSourceReader(source) {
6082
+ if (source.kind === "cwd") return (context) => Promise.resolve(buildFacts({
6083
+ worktrees: [context.worktree],
6084
+ branches: context.branch === null ? [] : [context.branch]
6085
+ }));
6086
+ if (source.kind === "session") return async (context) => {
6087
+ const scope = await loadSessionScope(resolveSessionScopePath(context.stateDir, context.sessionID));
6088
+ return buildFacts({
6089
+ worktrees: scope.worktrees,
6090
+ branches: scope.branches.filter((branch) => branch.commonDir === context.commonDir).map((branch) => branch.name),
6091
+ pullRequests: scope.pullRequests
6092
+ });
6093
+ };
6094
+ if (source.kind === "globs") return () => Promise.resolve(buildFacts({ pathGlobs: source.paths ?? [] }));
6095
+ return () => Promise.resolve(EMPTY_SCOPE_FACTS);
6096
+ }
6097
+ function buildFacts(facts) {
6098
+ return {
6099
+ ...EMPTY_SCOPE_FACTS,
6100
+ ...facts
6101
+ };
6102
+ }
6103
+ //#endregion
6104
+ //#region src/scope/load-task-scope.ts
6105
+ async function loadTaskScope(request, sources) {
6106
+ const cwd = resolve(request.cwd);
6107
+ const [checkout, context] = await Promise.all([findCheckout(cwd), loadRepositoryContext(cwd)]);
6108
+ const branch = context?.branch ?? null;
6109
+ const sourceContext = {
6110
+ sessionID: request.sessionID,
6111
+ cwd,
6112
+ worktree: checkout?.worktree ?? cwd,
6113
+ commonDir: checkout?.commonDir ?? null,
6114
+ branch,
6115
+ stateDir: request.stateDir
6116
+ };
6117
+ const [remotes, ...facts] = await Promise.all([checkout === null ? [] : loadCheckoutRemotes(checkout.commonDir), ...Object.values(sources).map((source) => pickScopeSourceReader(source)(sourceContext))]);
6118
+ return buildTaskScope({
6119
+ home: homedir(),
6120
+ currentBranch: branch,
6121
+ defaultBranch: context?.defaultBranch ?? null,
6122
+ remotes,
6123
+ facts
6124
+ });
6125
+ }
6126
+ //#endregion
6127
+ //#region src/state/resolve-state-dir.ts
6128
+ function resolveStateDir() {
6129
+ const stateHome = process.env["XDG_STATE_HOME"];
6130
+ const base = stateHome === void 0 || stateHome === "" ? join(homedir(), ".local", "state") : stateHome;
6131
+ return join(base, "auto-mode");
6132
+ }
6133
+ //#endregion
6134
+ //#region src/rules/split-command-words.ts
6135
+ function splitCommandWords(text) {
6136
+ const words = splitWords$1(text);
6137
+ let index = 0;
6138
+ while (index < words.length) {
6139
+ const word = words[index] ?? "";
6140
+ if (/^\w+=/u.test(word) || word === "sudo" || word === "command") index += 1;
6141
+ else if (word === "env") {
6142
+ index += 1;
6143
+ while (words[index] === "-u" || /^\w+=/u.test(words[index] ?? "")) index += words[index] === "-u" ? 2 : 1;
6144
+ } else if (word === "timeout") index += 2;
6145
+ else break;
6146
+ }
6147
+ return words.slice(index).filter((word, position, all) => !/^\d*[<>]/u.test(word) && !/^\d*>{1,2}\|?$/u.test(all[position - 1] ?? ""));
6148
+ }
6149
+ function splitWords$1(text) {
6150
+ const words = [];
6151
+ let current = "";
6152
+ let quote = null;
6153
+ let started = false;
6154
+ for (const ch of text) if (quote !== null) {
6155
+ if (ch === quote) quote = null;
6156
+ else current += ch;
6157
+ } else if (ch === "'" || ch === "\"") {
6158
+ quote = ch;
6159
+ started = true;
6160
+ } else if (/\s/u.test(ch)) {
6161
+ if (started) words.push(current);
6162
+ current = "";
6163
+ started = false;
6164
+ } else {
6165
+ current += ch;
6166
+ started = true;
6167
+ }
6168
+ if (started) words.push(current);
6169
+ return words;
6170
+ }
6171
+ //#endregion
6172
+ //#region src/rules/split-shell-command.ts
6173
+ function splitShellCommand(command) {
6174
+ const segments = [];
6175
+ let current = "";
6176
+ let quote = null;
6177
+ let unparsed = false;
6178
+ let i = 0;
6179
+ const collectSegment = () => {
6180
+ const text = current.trim();
6181
+ if (text !== "") segments.push({ text });
6182
+ current = "";
6183
+ };
6184
+ while (i < command.length) {
6185
+ const ch = command.charAt(i);
6186
+ if (quote !== null) {
6187
+ if (quote === "\"" && (command.startsWith("$(", i) || ch === "`")) unparsed = true;
6188
+ if (ch === quote) quote = null;
6189
+ current += ch;
6190
+ i += 1;
6191
+ continue;
6192
+ }
6193
+ if (ch === "'" || ch === "\"") {
6194
+ quote = ch;
6195
+ current += ch;
6196
+ i += 1;
6197
+ continue;
6198
+ }
6199
+ if (ch === "\\") {
6200
+ current += command.slice(i, i + 2);
6201
+ i += 2;
6202
+ continue;
6203
+ }
6204
+ if (command.startsWith("$(", i) || ch === "`" || command.startsWith("<(", i) || ch === ">") {
6205
+ unparsed = true;
6206
+ current += ch;
6207
+ i += 1;
6208
+ continue;
6209
+ }
6210
+ if (command.startsWith("&&", i) || command.startsWith("||", i)) {
6211
+ collectSegment();
6212
+ i += 2;
6213
+ continue;
6214
+ }
6215
+ if (ch === ";" || ch === "|" || ch === "\n" || ch === "&") {
6216
+ collectSegment();
6217
+ i += 1;
6218
+ continue;
6219
+ }
6220
+ current += ch;
6221
+ i += 1;
6222
+ }
6223
+ collectSegment();
6224
+ return {
6225
+ segments,
6226
+ hasUnparsedConstruct: unparsed || quote !== null
6227
+ };
6228
+ }
6229
+ //#endregion
6230
+ //#region src/containment/collect-scope-findings.ts
6231
+ function collectScopeFindings(action, scope) {
6232
+ const filePath = action.input["file_path"] ?? action.input["notebook_path"];
6233
+ const command = action.input["command"];
6234
+ const findings = [
6235
+ ...FILE_WRITE_TOOLS.has(action.tool) && typeof filePath === "string" ? collectTargetFindings(action.cwd, filePath, scope) : [],
6236
+ ...action.tool.startsWith("mcp__") ? collectToolFindings(action, scope) : [],
6237
+ ...action.tool === "Bash" && typeof command === "string" ? collectCommandFindings(command, action.cwd, scope) : []
6238
+ ];
6239
+ return findings.filter((finding, index) => findings.findIndex((other) => other.kind === finding.kind && other.target === finding.target) === index);
6240
+ }
6241
+ const FILE_WRITE_TOOLS = /* @__PURE__ */ new Set([
6242
+ "Edit",
6243
+ "MultiEdit",
6244
+ "Write",
6245
+ "NotebookEdit"
6246
+ ]);
6247
+ function collectTargetFindings(directory, target, scope) {
6248
+ const path = resolvePath(directory, target, scope.home);
6249
+ return path === null ? [] : collectPathFindings(path, scope);
6250
+ }
6251
+ const CREDENTIAL_PATHS = [
6252
+ ".ssh",
6253
+ ".aws",
6254
+ ".gnupg",
6255
+ ".gitconfig",
6256
+ ".netrc",
6257
+ ".npmrc",
6258
+ ".config/gh"
6259
+ ];
6260
+ function collectPathFindings(path, scope) {
6261
+ if (CREDENTIAL_PATHS.some((entry) => isUnder(path, join(scope.home, entry)))) return [{
6262
+ kind: "credential",
6263
+ target: path
6264
+ }];
6265
+ return isInScope(path, scope) ? [] : [{
6266
+ kind: "path",
6267
+ target: path
6268
+ }];
6269
+ }
6270
+ function collectToolFindings(action, scope) {
6271
+ const verb = action.tool.split("__").at(-1) ?? "";
6272
+ if (!/merge|create|comment|update|delete|push|close|review/u.test(verb)) return [];
6273
+ const number = action.input["pullNumber"] ?? action.input["pull_number"] ?? action.input["issue_number"];
6274
+ const owner = action.input["owner"];
6275
+ const name = action.input["repo"];
6276
+ const repo = typeof owner === "string" && typeof name === "string" ? `${owner}/${name}` : void 0;
6277
+ return !verb.includes("merge") && typeof number === "number" && isOwnedPullRequest(number, repo, scope) ? [] : [{
6278
+ kind: "remote-write",
6279
+ target: action.tool
6280
+ }];
6281
+ }
6282
+ const PATH_WRITERS = /* @__PURE__ */ new Set([
6283
+ "rm",
6284
+ "rmdir",
6285
+ "shred",
6286
+ "truncate",
6287
+ "chmod",
6288
+ "chown"
6289
+ ]);
6290
+ const DESTINATION_WRITERS = /* @__PURE__ */ new Set([
6291
+ "cp",
6292
+ "mv",
6293
+ "rsync",
6294
+ "ln"
6295
+ ]);
6296
+ function collectCommandFindings(command, cwd, scope) {
6297
+ const findings = [];
6298
+ const hasPipe = /(?<!\|)\|(?!\|)/u.test(command);
6299
+ let directory = cwd;
6300
+ for (const segment of splitShellCommand(command).segments) {
6301
+ const [name, ...args] = splitCommandWords(segment.text);
6302
+ for (const target of collectRedirectTargets(segment.text)) findings.push(...collectTargetFindings(directory, target, scope));
6303
+ if (name === void 0) continue;
6304
+ if (name === "cd" && hasPipe) directory = null;
6305
+ else if (name === "cd") directory = args[0] === void 0 ? scope.home : resolvePath(directory, args[0], scope.home);
6306
+ else findings.push(...collectProgramFindings(name, args, directory, cwd, scope));
6307
+ }
6308
+ return findings;
6309
+ }
6310
+ function collectProgramFindings(name, args, directory, cwd, scope) {
6311
+ const resolve = (path) => collectTargetFindings(directory, path, scope);
6312
+ const operands = args.filter((word) => !word.startsWith("-"));
6313
+ if (name === "git") return collectGitFindings(args, directory, cwd, scope);
6314
+ if (name === "gh") return collectGhFindings(args, scope);
6315
+ if (PATH_WRITERS.has(name)) return operands.flatMap((path) => resolve(path));
6316
+ if (DESTINATION_WRITERS.has(name)) return collectCopyFindings(name, args, operands).flatMap((path) => resolve(path));
6317
+ if (name === "find" && args.some((word) => word === "-delete" || word === "-exec")) return operands.slice(0, 1).flatMap((path) => resolve(path));
6318
+ if ((name === "perl" || name === "sed") && args.some((word) => /^-\w*i/u.test(word))) return collectInPlaceFiles(args).flatMap((path) => resolve(path));
6319
+ if (name === "ssh-keygen") return collectKeygenFiles(args).flatMap((path) => resolve(path));
6320
+ if (name === "ssh") {
6321
+ const logIndex = args.indexOf("-E");
6322
+ const log = logIndex === -1 ? void 0 : args[logIndex + 1];
6323
+ return [...log === void 0 ? [] : resolve(log), ...collectSSHFindings(args)];
6324
+ }
6325
+ if (name === "scp") {
6326
+ const destination = collectSSHOperands(args).at(-1);
6327
+ if (destination === void 0 || /^[^/]+:/u.test(destination)) return destination === void 0 ? [] : [{
6328
+ kind: "remote-write",
6329
+ target: "scp"
6330
+ }];
6331
+ return resolve(destination);
6332
+ }
6333
+ if (name === "curl") return collectCurlFindings(args);
6334
+ const launcher = [name, ...args].findIndex((word) => word.includes("auto-mode"));
6335
+ if (launcher !== -1 && [name, ...args].slice(launcher + 1).includes("record")) return [{
6336
+ kind: "credential",
6337
+ target: "auto-mode record"
6338
+ }];
6339
+ const finding = findProgramFinding(name, args);
6340
+ return finding === null ? [] : [finding];
6341
+ }
6342
+ function collectCopyFindings(name, args, operands) {
6343
+ const optionIndex = args.findIndex((word) => word === "-t" || word === "--target-directory");
6344
+ let targetDirectory = args.find((word) => word.startsWith("--target-directory="))?.slice(19);
6345
+ if (targetDirectory === void 0 && optionIndex !== -1) targetDirectory = args[optionIndex + 1];
6346
+ if (targetDirectory !== void 0) {
6347
+ const sources = operands.filter((word) => word !== targetDirectory);
6348
+ return [targetDirectory, ...name === "mv" ? sources : []];
6349
+ }
6350
+ if (operands.length < 2) return [];
6351
+ return [operands.at(-1) ?? "", ...name === "mv" ? operands.slice(0, -1) : []];
6352
+ }
6353
+ const KEYGEN_READ_OPTIONS = /* @__PURE__ */ new Set([
6354
+ "-l",
6355
+ "-y",
6356
+ "-F",
6357
+ "-B",
6358
+ "-R"
6359
+ ]);
6360
+ function collectKeygenFiles(args) {
6361
+ if (args.some((word) => KEYGEN_READ_OPTIONS.has(word))) return [];
6362
+ const index = args.indexOf("-f");
6363
+ const file = index === -1 ? void 0 : args[index + 1];
6364
+ return file === void 0 ? [] : [file];
6365
+ }
6366
+ const SSH_VALUE_OPTIONS = /* @__PURE__ */ new Set([
6367
+ "-b",
6368
+ "-c",
6369
+ "-D",
6370
+ "-E",
6371
+ "-F",
6372
+ "-i",
6373
+ "-J",
6374
+ "-l",
6375
+ "-L",
6376
+ "-m"
6377
+ ]);
6378
+ const SSH_MORE_VALUE_OPTIONS = /* @__PURE__ */ new Set([
6379
+ "-o",
6380
+ "-p",
6381
+ "-P",
6382
+ "-Q",
6383
+ "-R",
6384
+ "-S",
6385
+ "-W",
6386
+ "-w"
6387
+ ]);
6388
+ function collectSSHOperands(args) {
6389
+ const operands = [];
6390
+ for (let index = 0; index < args.length; index += 1) {
6391
+ const word = args[index] ?? "";
6392
+ if (operands.length > 0 || !word.startsWith("-")) operands.push(word);
6393
+ else if (SSH_VALUE_OPTIONS.has(word) || SSH_MORE_VALUE_OPTIONS.has(word)) index += 1;
6394
+ }
6395
+ return operands;
6396
+ }
6397
+ const SSH_READS = /* @__PURE__ */ new Set([
6398
+ "cat",
6399
+ "df",
6400
+ "du",
6401
+ "free",
6402
+ "head",
6403
+ "id",
6404
+ "ls",
6405
+ "ps",
6406
+ "pwd",
6407
+ "stat",
6408
+ "tail",
6409
+ "uname",
6410
+ "uptime",
6411
+ "whoami"
6412
+ ]);
6413
+ function collectSSHFindings(args) {
6414
+ const [host, ...remote] = collectSSHOperands(args);
6415
+ if (host === void 0) return [];
6416
+ const command = remote.join(" ");
6417
+ const [program] = command.trim().split(/\s+/u);
6418
+ return SSH_READS.has(program ?? "") && !/[;&|<>`\n\r]|\$\(/u.test(command) ? [] : [{
6419
+ kind: "remote-write",
6420
+ target: "ssh"
6421
+ }];
6422
+ }
6423
+ const LOCAL_HOSTS = /* @__PURE__ */ new Set([
6424
+ "localhost",
6425
+ "127.0.0.1",
6426
+ "[::1]",
6427
+ "0.0.0.0"
6428
+ ]);
6429
+ function collectCurlFindings(args) {
6430
+ const joined = args.join(" ");
6431
+ const isUpload = args.some((word) => /^-[a-zA-Z]*[dFT]$|^--(?:data\S*|form|upload-file|json)$/u.test(word));
6432
+ const isWriteMethod = /(?:-X\s*|--request\s+)(?:POST|PUT|PATCH|DELETE)/u.test(joined);
6433
+ if (!isUpload && !isWriteMethod) return [];
6434
+ const hosts = args.filter((word) => /^(?:https?:\/\/|localhost\b|127\.)/u.test(word)).map((word) => /^(?:https?:\/\/)?(?<host>\[[^\]]+\]|[^/:?#\s]+)/u.exec(word)?.groups?.["host"]?.toLowerCase() ?? "");
6435
+ return hosts.length > 0 && hosts.every((host) => LOCAL_HOSTS.has(host)) ? [] : [{
6436
+ kind: "remote-write",
6437
+ target: "curl upload"
6438
+ }];
6439
+ }
6440
+ const GIT_DIRECTORY_WRITES = /* @__PURE__ */ new Set([
6441
+ "clean",
6442
+ "reset",
6443
+ "checkout",
6444
+ "switch",
6445
+ "merge",
6446
+ "commit",
6447
+ "rm",
6448
+ "stash",
6449
+ "push"
6450
+ ]);
6451
+ function collectGitFindings(args, directory, cwd, scope) {
6452
+ let gitDirectory = directory;
6453
+ let index = 0;
6454
+ while (index < args.length && args[index]?.startsWith("-") === true) {
6455
+ const option = args[index];
6456
+ if (option === "-C") gitDirectory = resolvePath(gitDirectory, args[index + 1] ?? "", scope.home);
6457
+ index += option === "-C" || option === "-c" ? 2 : 1;
6458
+ }
6459
+ const subcommand = args[index] ?? "";
6460
+ const rest = args.slice(index + 1);
6461
+ const operands = rest.filter((word) => !word.startsWith("-"));
6462
+ const findings = [];
6463
+ if (gitDirectory !== null && GIT_DIRECTORY_WRITES.has(subcommand) && !isInScope(gitDirectory, scope)) findings.push({
6464
+ kind: "path",
6465
+ target: gitDirectory
6466
+ });
6467
+ if (subcommand === "worktree" && operands[0] === "remove" && operands[1] !== void 0) findings.push(...collectTargetFindings(gitDirectory, operands[1], scope));
6468
+ 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({
6469
+ kind: "branch",
6470
+ target: branch
6471
+ });
6472
+ const isConfigRead = rest.some((word) => /^--(?:get|get-all|get-regexp|list)$|^-l$/u.test(word));
6473
+ if (subcommand === "config" && rest.includes("--global") && !isConfigRead) findings.push({
6474
+ kind: "credential",
6475
+ target: `git config ${operands[0] ?? ""}`
6476
+ });
6477
+ if (subcommand === "push") {
6478
+ const currentBranch = gitDirectory === cwd ? scope.currentBranch : null;
6479
+ findings.push(...collectPushFindings(rest, operands, currentBranch, scope));
6480
+ }
6481
+ return findings;
6482
+ }
6483
+ function collectPushFindings(rest, operands, currentBranch, scope) {
6484
+ const [remote, ...refspecs] = operands;
6485
+ const findings = [];
6486
+ if (rest.includes("--all") || rest.includes("--mirror")) findings.push({
6487
+ kind: "branch",
6488
+ target: "--all"
6489
+ });
6490
+ if (remote !== void 0 && !isUnresolved(remote) && !isOwnedRemote(remote, scope)) findings.push({
6491
+ kind: "remote-write",
6492
+ target: remote
6493
+ });
6494
+ const destinations = refspecs.length === 0 && !rest.includes("--tags") ? ["HEAD"] : refspecs;
6495
+ for (const refspec of destinations) {
6496
+ const named = (refspec.split(":").at(-1) ?? refspec).replace(/^\+/u, "").replace(/^refs\/heads\//u, "");
6497
+ const branch = named === "HEAD" ? currentBranch : named;
6498
+ if (branch !== null && !isUnresolved(branch) && !branch.startsWith("refs/tags/") && !scope.branches.includes(branch)) findings.push({
6499
+ kind: "branch",
6500
+ target: branch
6501
+ });
6502
+ }
6503
+ return findings;
6504
+ }
6505
+ function isUnresolved(word) {
6506
+ return /[$`]/u.test(word);
6507
+ }
6508
+ function isOwnedRemote(remote, scope) {
6509
+ if (scope.remotes.some((entry) => entry.name === remote)) return true;
6510
+ const slug = toRepositorySlug(remote);
6511
+ return slug !== null && scope.remotes.some((entry) => toRepositorySlug(entry.url) === slug);
6512
+ }
6513
+ const GH_PR_WRITES = /* @__PURE__ */ new Set([
6514
+ "comment",
6515
+ "merge",
6516
+ "edit",
6517
+ "close",
6518
+ "review",
6519
+ "ready",
6520
+ "reopen"
6521
+ ]);
6522
+ const GH_REMOTE_WRITES = /* @__PURE__ */ new Set([
6523
+ "gist",
6524
+ "release",
6525
+ "repo",
6526
+ "ruleset",
6527
+ "label",
6528
+ "workflow"
6529
+ ]);
6530
+ const GH_CREDENTIAL_WRITES = /* @__PURE__ */ new Set([
6531
+ "secret",
6532
+ "variable",
6533
+ "ssh-key",
6534
+ "gpg-key"
6535
+ ]);
6536
+ const GH_READS = /* @__PURE__ */ new Set([
6537
+ "list",
6538
+ "view",
6539
+ "clone",
6540
+ "status",
6541
+ "checks",
6542
+ "diff"
6543
+ ]);
6544
+ function collectGhFindings(args, scope) {
6545
+ const group = args[0] ?? "";
6546
+ const verb = args[1] ?? "";
6547
+ if (group === "pr" && GH_PR_WRITES.has(verb)) {
6548
+ const number = Number(args.slice(2).find((word) => /^\d+$/u.test(word)));
6549
+ const repoIndex = args.findIndex((word) => word === "--repo" || word === "-R");
6550
+ const repo = repoIndex === -1 ? void 0 : args[repoIndex + 1];
6551
+ return verb !== "merge" && isOwnedPullRequest(number, repo, scope) ? [] : [{
6552
+ kind: "remote-write",
6553
+ target: `gh pr ${verb}`
6554
+ }];
6555
+ }
6556
+ if (group === "issue" && verb !== "" && !GH_READS.has(verb)) return [{
6557
+ kind: "remote-write",
6558
+ target: `gh issue ${verb}`
6559
+ }];
6560
+ if (group === "api") return collectGhAPIFindings(args.slice(1), scope);
6561
+ if (group === "auth" && [
6562
+ "refresh",
6563
+ "login",
6564
+ "logout"
6565
+ ].includes(verb)) return [{
6566
+ kind: "credential",
6567
+ target: `gh auth ${verb}`
6568
+ }];
6569
+ if (GH_CREDENTIAL_WRITES.has(group) && !GH_READS.has(verb)) return [{
6570
+ kind: "credential",
6571
+ target: `gh ${group} ${verb}`
6572
+ }];
6573
+ if (GH_REMOTE_WRITES.has(group) && !GH_READS.has(verb)) return [{
6574
+ kind: "remote-write",
6575
+ target: `gh ${group} ${verb}`
6576
+ }];
6577
+ return [];
6578
+ }
6579
+ function isOwnedPullRequest(number, repo, scope) {
6580
+ const wanted = repo?.toLowerCase().replace(/\.git$/u, "");
6581
+ const slugs = new Set(scope.remotes.map((remote) => toRepositorySlug(remote.url)));
6582
+ return scope.pullRequests.some((pull) => pull.number === number && (wanted === void 0 ? slugs.size === 1 && slugs.has(pull.repository) : pull.repository === wanted || pull.repository.endsWith(`/${wanted}`)));
6583
+ }
6584
+ function collectGhAPIFindings(args, scope) {
6585
+ const endpoint = args.find((word) => !word.startsWith("-")) ?? "";
6586
+ if (endpoint === "graphql") return args.some((word) => /\bmutation\b/u.test(word)) ? [{
6587
+ kind: "remote-write",
6588
+ target: "gh api graphql mutation"
6589
+ }] : [];
6590
+ const methodIndex = args.findIndex((word) => word === "-X" || word === "--method");
6591
+ const method = methodIndex === -1 ? null : (args[methodIndex + 1] ?? "").toUpperCase();
6592
+ const hasFields = args.some((word) => /^(?:-f|-F|--field|--raw-field|--input)$/u.test(word));
6593
+ if (method === "GET" || method === null && !hasFields) return [];
6594
+ const pull = /^\/?repos\/(?<repo>[^/]+\/[^/]+)\/(?:pulls|issues)\/(?<number>\d+)(?:\/|$)/u.exec(endpoint)?.groups;
6595
+ if (pull !== void 0 && isOwnedPullRequest(Number(pull["number"]), pull["repo"] ?? "", scope) && !endpoint.includes("/merge")) return [];
6596
+ return [{
6597
+ kind: /secret|collaborator|protection|key|permission/u.test(endpoint) ? "credential" : "remote-write",
6598
+ target: `gh api ${endpoint}`
6599
+ }];
6600
+ }
6601
+ const IAM_WRITE = /^(?:add|attach|change|create|deactivate|delete|detach|enable|put|remove|reset|set|tag|untag|update|upload)-/u;
6602
+ function findProgramFinding(name, args) {
6603
+ const joined = args.join(" ");
6604
+ if (name === "op" && /^(?:item|vault|document) (?:edit|create|delete)\b/u.test(joined)) return {
6605
+ kind: "credential",
6606
+ target: `op ${args[0] ?? ""} ${args[1] ?? ""}`
6607
+ };
6608
+ if (name === "aws" && args[0] === "iam" && IAM_WRITE.test(args[1] ?? "")) return {
6609
+ kind: "credential",
6610
+ target: `aws iam ${args[1] ?? ""}`
6611
+ };
6612
+ if (name === "docker" && /\b(?:volume|system|image|container) prune\b|\bvolume rm\b|\bdown\b.*(?:-v|--volumes)\b/u.test(joined)) return {
6613
+ kind: "prune",
6614
+ target: `docker ${joined}`
6615
+ };
6616
+ if (name === "npm" && args[0] === "publish") return {
6617
+ kind: "remote-write",
6618
+ target: "npm publish"
6619
+ };
6620
+ if (name === "kubectl" && /\b(?:apply|delete|replace|patch|scale)\b/u.test(joined)) return {
6621
+ kind: "remote-write",
6622
+ target: "kubectl write"
6623
+ };
6624
+ if (name === "terraform" && /\b(?:apply|destroy|import)\b/u.test(joined)) return {
6625
+ kind: "remote-write",
6626
+ target: "terraform write"
6627
+ };
6628
+ if (name === "psql" && /\b(?:UPDATE|DELETE|DROP|INSERT|TRUNCATE|ALTER)\b/iu.test(joined)) return {
6629
+ kind: "remote-write",
6630
+ target: "database write"
6631
+ };
6632
+ if (/^(?:bun|npm|pnpm|yarn)$/u.test(name) && /\bdeploy\b/u.test(joined) && /prod/u.test(joined)) return {
6633
+ kind: "remote-write",
6634
+ target: "production deploy"
6635
+ };
6636
+ return null;
6637
+ }
6638
+ function collectRedirectTargets(text) {
6639
+ const targets = [];
6640
+ let quote = null;
6641
+ let index = 0;
6642
+ while (index < text.length) {
6643
+ const ch = text.charAt(index);
6644
+ if (quote !== null) {
6645
+ quote = ch === quote ? null : quote;
6646
+ index += 1;
6647
+ } else if (ch === "'" || ch === "\"") {
6648
+ quote = ch;
6649
+ index += 1;
6650
+ } else if (ch === ">" && text.charAt(index - 1) !== "<") {
6651
+ let next = index + 1;
6652
+ if (text.charAt(next) === ">" || text.charAt(next) === "|") next += 1;
6653
+ if (text.charAt(next) === "&") {
6654
+ index = next + 1;
6655
+ continue;
6656
+ }
6657
+ const [target, end] = readWord(text, next);
6658
+ if (target !== "") targets.push(target);
6659
+ index = end;
6660
+ } else index += 1;
6661
+ }
6662
+ return targets;
6663
+ }
6664
+ function readWord(text, start) {
6665
+ let index = start;
6666
+ while (/\s/u.test(text.charAt(index))) index += 1;
6667
+ let word = "";
6668
+ let quote = null;
6669
+ while (index < text.length) {
6670
+ const ch = text.charAt(index);
6671
+ if (quote !== null) {
6672
+ if (ch === quote) quote = null;
6673
+ else word += ch;
6674
+ } else if (ch === "'" || ch === "\"") quote = ch;
6675
+ else if (/[\s;&|<>()]/u.test(ch)) break;
6676
+ else word += ch;
6677
+ index += 1;
6678
+ }
6679
+ return [word, index];
6680
+ }
6681
+ function collectInPlaceFiles(args) {
6682
+ const files = [];
6683
+ let skipNext = false;
6684
+ let sawScript = false;
6685
+ for (const word of args) if (skipNext) {
6686
+ skipNext = false;
6687
+ sawScript = true;
6688
+ } else if (/^-\w*e$/u.test(word)) skipNext = true;
6689
+ else if (!word.startsWith("-")) {
6690
+ if (sawScript) files.push(word);
6691
+ else sawScript = true;
6692
+ }
6693
+ return files;
6694
+ }
6695
+ function resolvePath(cwd, path, home) {
6696
+ const expanded = path.replace(/^(?:~|\$HOME|\$\{HOME\})(?=\/|$)/u, home);
6697
+ if (/[$`]|^~/u.test(expanded) || expanded === "") return null;
6698
+ if (cwd === null && !isAbsolute(expanded)) return null;
6699
+ const absolute = isAbsolute(expanded) || cwd === null ? expanded : join(cwd, expanded);
6700
+ return normalize(absolute).replace(/(?<=.)\/$/u, "");
6701
+ }
6702
+ const SCRATCH_PATHS = [
6703
+ "/tmp",
6704
+ "/dev/null",
6705
+ "/dev/stdout",
6706
+ "/dev/stderr"
6707
+ ];
6708
+ function isInScope(path, scope) {
6709
+ if (SCRATCH_PATHS.some((entry) => isUnder(path, entry)) || scope.pathGlobs.some((glob) => matchesGlob(path, glob))) return true;
6710
+ return scope.worktrees.some((worktree) => {
6711
+ const isNested = isUnder(path, join(worktree, ".worktrees"));
6712
+ const isOtherOwned = scope.worktrees.some((other) => other !== worktree && isUnder(path, other));
6713
+ return isUnder(path, worktree) && (!isNested || isOtherOwned);
6714
+ });
6715
+ }
6716
+ function isUnder(path, root) {
6717
+ return path === root || path.startsWith(`${root}/`);
6718
+ }
6719
+ //#endregion
6720
+ //#region src/containment/check-containment.ts
6721
+ const CONTAINMENT_RULE = "Outside Task Scope";
6722
+ async function checkContainment(request, sources) {
6723
+ let scope;
6724
+ try {
6725
+ scope = await loadTaskScope({
6726
+ sessionID: request.sessionID,
6727
+ cwd: request.cwd,
6728
+ stateDir: resolveStateDir()
6729
+ }, sources);
6730
+ } catch {
6731
+ return null;
6732
+ }
6733
+ const findings = collectScopeFindings({
6734
+ tool: request.toolName,
6735
+ cwd: request.cwd,
6736
+ input: request.toolInput
6737
+ }, scope);
6738
+ if (findings.length === 0) return null;
6739
+ return {
6740
+ rule: CONTAINMENT_RULE,
6741
+ reason: formatContainmentReason(findings, scope),
6742
+ findings
6743
+ };
6744
+ }
6745
+ const KIND_LABELS = {
6746
+ path: "path",
6747
+ branch: "branch",
6748
+ "remote-write": "remote target",
6749
+ credential: "credential or global setting",
6750
+ prune: "shared resource"
6751
+ };
6752
+ function formatContainmentReason(findings, scope) {
6753
+ const targets = findings.slice(0, 5).map((finding) => `${KIND_LABELS[finding.kind]} ${finding.target}`).join(", ");
6754
+ const more = findings.length > 5 ? ` and ${findings.length - 5} more` : "";
6755
+ const branches = scope.branches.length === 0 ? "" : ` and ${formatList("branch", "branches", scope.branches)}`;
6756
+ return `This action writes outside the task scope: ${targets}${more}. The task owns ${`${formatList("worktree", "worktrees", scope.worktrees)}${branches}`}. Do the work inside the task's own worktree and branch, or ask the user to extend the task scope to this target.`;
6757
+ }
6758
+ function formatList(one, many, values) {
6759
+ return `the ${values.length === 1 ? one : many} ${values.join(", ")}`;
6760
+ }
6761
+ //#endregion
5859
6762
  //#region src/policy/resolve-shipped-policy-dir.ts
5860
6763
  function resolveShippedPolicyDir() {
5861
6764
  let dir = import.meta.dirname;
@@ -6124,68 +7027,6 @@ function formatClassifierNote(note, apiKey) {
6124
7027
  return apiKey === null || apiKey === "" ? note : note.split(apiKey).join("[redacted]");
6125
7028
  }
6126
7029
  //#endregion
6127
- //#region src/model/load-repository-context.ts
6128
- async function loadRepositoryContext(cwd) {
6129
- if ([
6130
- "GIT_DIR",
6131
- "GIT_WORK_TREE",
6132
- "GIT_COMMON_DIR"
6133
- ].some((name) => process.env[name] !== void 0)) return null;
6134
- let directory = resolve(cwd);
6135
- for (;;) {
6136
- const gitPath = join(directory, ".git");
6137
- try {
6138
- await stat(gitPath);
6139
- } catch (error) {
6140
- if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) return null;
6141
- const parent = dirname(directory);
6142
- if (parent === directory) return null;
6143
- directory = parent;
6144
- continue;
6145
- }
6146
- let gitDir = gitPath;
6147
- let head;
6148
- try {
6149
- try {
6150
- head = await readFile(join(gitDir, "HEAD"), "utf8");
6151
- } catch {
6152
- const link = await readFile(gitPath, "utf8");
6153
- const path = /^gitdir: (?<path>.+)\r?\n?$/u.exec(link)?.groups?.["path"];
6154
- if (path === void 0) return null;
6155
- gitDir = resolve(directory, path.trim());
6156
- head = await readFile(join(gitDir, "HEAD"), "utf8");
6157
- }
6158
- } catch {
6159
- return null;
6160
- }
6161
- let commonDir = gitDir;
6162
- try {
6163
- const common = await readFile(join(gitDir, "commondir"), "utf8");
6164
- commonDir = resolve(gitDir, common.trim());
6165
- } catch {
6166
- commonDir = gitDir;
6167
- }
6168
- let defaultBranch = null;
6169
- try {
6170
- defaultBranch = parseReference(await readFile(join(commonDir, "refs", "remotes", "origin", "HEAD"), "utf8"), "refs/remotes/origin/");
6171
- } catch {
6172
- defaultBranch = null;
6173
- }
6174
- return {
6175
- cwd: resolve(cwd),
6176
- branch: parseReference(head, "refs/heads/"),
6177
- defaultBranch
6178
- };
6179
- }
6180
- }
6181
- function parseReference(content, prefix) {
6182
- const value = content.trim();
6183
- const marker = `ref: ${prefix}`;
6184
- if (!value.startsWith(marker)) return null;
6185
- const branch = value.slice(marker.length);
6186
- return branch !== "" && branch.length <= 256 && !/\s/u.test(branch) ? branch : null;
6187
- }
6188
- //#endregion
6189
7030
  //#region node_modules/.bun/ts-pattern@5.9.0/node_modules/ts-pattern/dist/index.js
6190
7031
  const t = Symbol.for("@ts-pattern/matcher");
6191
7032
  const e = Symbol.for("@ts-pattern/isVariadic");
@@ -6969,64 +7810,6 @@ const REGENERABLE_DIRS = /* @__PURE__ */ new Set([
6969
7810
  "venv"
6970
7811
  ]);
6971
7812
  //#endregion
6972
- //#region src/rules/split-shell-command.ts
6973
- function splitShellCommand(command) {
6974
- const segments = [];
6975
- let current = "";
6976
- let quote = null;
6977
- let unparsed = false;
6978
- let i = 0;
6979
- const collectSegment = () => {
6980
- const text = current.trim();
6981
- if (text !== "") segments.push({ text });
6982
- current = "";
6983
- };
6984
- while (i < command.length) {
6985
- const ch = command.charAt(i);
6986
- if (quote !== null) {
6987
- if (quote === "\"" && (command.startsWith("$(", i) || ch === "`")) unparsed = true;
6988
- if (ch === quote) quote = null;
6989
- current += ch;
6990
- i += 1;
6991
- continue;
6992
- }
6993
- if (ch === "'" || ch === "\"") {
6994
- quote = ch;
6995
- current += ch;
6996
- i += 1;
6997
- continue;
6998
- }
6999
- if (ch === "\\") {
7000
- current += command.slice(i, i + 2);
7001
- i += 2;
7002
- continue;
7003
- }
7004
- if (command.startsWith("$(", i) || ch === "`" || command.startsWith("<(", i) || ch === ">") {
7005
- unparsed = true;
7006
- current += ch;
7007
- i += 1;
7008
- continue;
7009
- }
7010
- if (command.startsWith("&&", i) || command.startsWith("||", i)) {
7011
- collectSegment();
7012
- i += 2;
7013
- continue;
7014
- }
7015
- if (ch === ";" || ch === "|" || ch === "\n" || ch === "&") {
7016
- collectSegment();
7017
- i += 1;
7018
- continue;
7019
- }
7020
- current += ch;
7021
- i += 1;
7022
- }
7023
- collectSegment();
7024
- return {
7025
- segments,
7026
- hasUnparsedConstruct: unparsed || quote !== null
7027
- };
7028
- }
7029
- //#endregion
7030
7813
  //#region src/rules/classify-locally.ts
7031
7814
  const ESCALATE = { kind: "escalate" };
7032
7815
  function classifyLocally(payload) {
@@ -7104,12 +7887,18 @@ async function classifyAction(request, config, options = {}) {
7104
7887
  };
7105
7888
  }
7106
7889
  const local = configured !== null && (configured.hard_deny.length > 0 || configured.soft_deny.length > 0) ? { kind: "escalate" } : classifyLocally(request);
7107
- if (local.kind === "allow") return {
7108
- verdict: { kind: "allow" },
7109
- decidingStage: "local",
7110
- note: `allowed by ${local.exception} (local)`,
7111
- status: "allow"
7112
- };
7890
+ if (local.kind === "allow" && local.exception === READ_ONLY_EXCEPTION) return buildLocalAllow(local.exception);
7891
+ const containment = await checkContainment(request, config.scopeSources ?? DEFAULT_SCOPE_SOURCES);
7892
+ if (containment !== null) {
7893
+ const guidance = await tryReadDenialGuidance();
7894
+ return {
7895
+ verdict: buildGuidedDeny(containment.rule, containment.reason, guidance),
7896
+ decidingStage: "containment",
7897
+ note: `denied by the containment check: ${containment.findings.map((finding) => finding.target).join(", ")}`,
7898
+ status: "deny"
7899
+ };
7900
+ }
7901
+ if (local.kind === "allow") return buildLocalAllow(local.exception);
7113
7902
  if (options.localOnly === true) return {
7114
7903
  verdict: null,
7115
7904
  decidingStage: "local",
@@ -7124,6 +7913,15 @@ async function classifyAction(request, config, options = {}) {
7124
7913
  status: outcome.diagnostics?.status ?? outcome.verdict?.kind ?? "failure"
7125
7914
  };
7126
7915
  }
7916
+ const READ_ONLY_EXCEPTION = "Read-only actions";
7917
+ function buildLocalAllow(exception) {
7918
+ return {
7919
+ verdict: { kind: "allow" },
7920
+ decidingStage: "local",
7921
+ note: `allowed by ${exception} (local)`,
7922
+ status: "allow"
7923
+ };
7924
+ }
7127
7925
  function buildGuidedDeny(rule, reason, guidance) {
7128
7926
  const sentence = /[.!?]$/u.test(reason) ? reason : `${reason}.`;
7129
7927
  return {
@@ -7197,4 +7995,4 @@ function renderVerdict(verdict) {
7197
7995
  })).exhaustive();
7198
7996
  }
7199
7997
  //#endregion
7200
- 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 };
7998
+ export { resolveConfigPath as C, string as D, strictObject as E, resolveApiKey as S, number as T, loadRepositoryContext as _, classifyWithModel as a, DEFAULT_SCOPE_SOURCES as b, splitShellCommand as c, resolveSessionScopePath as d, loadSessionScope as f, toRepositorySlug as g, findCheckout as h, classifyLocally as i, splitCommandWords as l, loadCheckoutRemotes as m, parseActionRequest as n, parseModelVerdict as o, EMPTY_SESSION_SCOPE as p, classifyAction as r, loadPolicy as s, renderVerdict as t, resolveStateDir as u, DEFAULT_CONFIG as v, PRESETS as w, loadConfig as x, DEFAULT_DENIAL_BUDGET as y };