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.
- package/dist/cli.js +320 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/{render-verdict-CK9sRJpa.js → render-verdict-BiGjYb1x.js} +977 -179
- package/mods/auto-mode/hooks/register.claude-check.ts +97 -0
- package/mods/auto-mode/hooks/register.ts +36 -2
- package/mods/auto-mode/hooks/types.ts +13 -2
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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:
|
|
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
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
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,
|
|
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 };
|