auto-mode 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { g as string, h as strictObject, l as DEFAULT_DENIAL_BUDGET, m as number, n as parseActionRequest, r as classifyAction, s as loadPolicy, t as renderVerdict, u as loadConfig } from "./render-verdict-
|
|
2
|
+
import { g as string, h as strictObject, l as DEFAULT_DENIAL_BUDGET, m as number, n as parseActionRequest, r as classifyAction, s as loadPolicy, t as renderVerdict, u as loadConfig } from "./render-verdict-1xtCI2is.js";
|
|
3
3
|
import { appendFile, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
package/dist/index.d.ts
CHANGED
|
@@ -115,7 +115,7 @@ interface DecisionDiagnostics {
|
|
|
115
115
|
}
|
|
116
116
|
//#endregion
|
|
117
117
|
//#region src/classify-action.d.ts
|
|
118
|
-
type DecidingStage = 'local' | 'jev' | 'messages' | 'retry';
|
|
118
|
+
type DecidingStage = 'local' | 'containment' | 'jev' | 'messages' | 'retry';
|
|
119
119
|
interface ActionOutcome {
|
|
120
120
|
readonly verdict: Verdict | null;
|
|
121
121
|
readonly decidingStage: DecidingStage;
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as classifyWithModel, c as DEFAULT_CONFIG, d as resolveApiKey, f as resolveConfigPath, i as classifyLocally, n as parseActionRequest, o as parseModelVerdict, p as PRESETS, r as classifyAction, s as loadPolicy, t as renderVerdict, u as loadConfig } from "./render-verdict-
|
|
1
|
+
import { a as classifyWithModel, c as DEFAULT_CONFIG, d as resolveApiKey, f as resolveConfigPath, i as classifyLocally, n as parseActionRequest, o as parseModelVerdict, p as PRESETS, r as classifyAction, s as loadPolicy, t as renderVerdict, u as loadConfig } from "./render-verdict-1xtCI2is.js";
|
|
2
2
|
export { DEFAULT_CONFIG, PRESETS, classifyAction, classifyLocally, classifyWithModel, loadConfig, loadPolicy, parseActionRequest, parseModelVerdict, renderVerdict, resolveApiKey, resolveConfigPath };
|
|
@@ -5569,6 +5569,764 @@ async function loadClaudeRules(path) {
|
|
|
5569
5569
|
hard_deny: (rules.autoMode?.hard_deny ?? []).filter((entry) => entry !== "$defaults")
|
|
5570
5570
|
};
|
|
5571
5571
|
}
|
|
5572
|
+
//#endregion
|
|
5573
|
+
//#region src/rules/split-shell-command.ts
|
|
5574
|
+
function splitShellCommand(command) {
|
|
5575
|
+
const segments = [];
|
|
5576
|
+
let current = "";
|
|
5577
|
+
let quote = null;
|
|
5578
|
+
let unparsed = false;
|
|
5579
|
+
let i = 0;
|
|
5580
|
+
const collectSegment = () => {
|
|
5581
|
+
const text = current.trim();
|
|
5582
|
+
if (text !== "") segments.push({ text });
|
|
5583
|
+
current = "";
|
|
5584
|
+
};
|
|
5585
|
+
while (i < command.length) {
|
|
5586
|
+
const ch = command.charAt(i);
|
|
5587
|
+
if (quote !== null) {
|
|
5588
|
+
if (quote === "\"" && (command.startsWith("$(", i) || ch === "`")) unparsed = true;
|
|
5589
|
+
if (ch === quote) quote = null;
|
|
5590
|
+
current += ch;
|
|
5591
|
+
i += 1;
|
|
5592
|
+
continue;
|
|
5593
|
+
}
|
|
5594
|
+
if (ch === "'" || ch === "\"") {
|
|
5595
|
+
quote = ch;
|
|
5596
|
+
current += ch;
|
|
5597
|
+
i += 1;
|
|
5598
|
+
continue;
|
|
5599
|
+
}
|
|
5600
|
+
if (ch === "\\") {
|
|
5601
|
+
current += command.slice(i, i + 2);
|
|
5602
|
+
i += 2;
|
|
5603
|
+
continue;
|
|
5604
|
+
}
|
|
5605
|
+
if (command.startsWith("$(", i) || ch === "`" || command.startsWith("<(", i) || ch === ">") {
|
|
5606
|
+
unparsed = true;
|
|
5607
|
+
current += ch;
|
|
5608
|
+
i += 1;
|
|
5609
|
+
continue;
|
|
5610
|
+
}
|
|
5611
|
+
if (command.startsWith("&&", i) || command.startsWith("||", i)) {
|
|
5612
|
+
collectSegment();
|
|
5613
|
+
i += 2;
|
|
5614
|
+
continue;
|
|
5615
|
+
}
|
|
5616
|
+
if (ch === ";" || ch === "|" || ch === "\n" || ch === "&") {
|
|
5617
|
+
collectSegment();
|
|
5618
|
+
i += 1;
|
|
5619
|
+
continue;
|
|
5620
|
+
}
|
|
5621
|
+
current += ch;
|
|
5622
|
+
i += 1;
|
|
5623
|
+
}
|
|
5624
|
+
collectSegment();
|
|
5625
|
+
return {
|
|
5626
|
+
segments,
|
|
5627
|
+
hasUnparsedConstruct: unparsed || quote !== null
|
|
5628
|
+
};
|
|
5629
|
+
}
|
|
5630
|
+
//#endregion
|
|
5631
|
+
//#region src/containment/collect-scope-findings.ts
|
|
5632
|
+
function collectScopeFindings(action, scope) {
|
|
5633
|
+
const filePath = action.input["file_path"] ?? action.input["notebook_path"];
|
|
5634
|
+
const command = action.input["command"];
|
|
5635
|
+
const findings = [
|
|
5636
|
+
...FILE_WRITE_TOOLS.has(action.tool) && typeof filePath === "string" ? collectTargetFindings(action.cwd, filePath, scope) : [],
|
|
5637
|
+
...action.tool.startsWith("mcp__") ? collectToolFindings(action, scope) : [],
|
|
5638
|
+
...action.tool === "Bash" && typeof command === "string" ? collectCommandFindings(command, action.cwd, scope) : []
|
|
5639
|
+
];
|
|
5640
|
+
return findings.filter((finding, index) => findings.findIndex((other) => other.kind === finding.kind && other.target === finding.target) === index);
|
|
5641
|
+
}
|
|
5642
|
+
const FILE_WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
5643
|
+
"Edit",
|
|
5644
|
+
"MultiEdit",
|
|
5645
|
+
"Write",
|
|
5646
|
+
"NotebookEdit"
|
|
5647
|
+
]);
|
|
5648
|
+
function collectTargetFindings(directory, target, scope) {
|
|
5649
|
+
const path = resolvePath(directory, target, scope.home);
|
|
5650
|
+
return path === null ? [] : collectPathFindings(path, scope);
|
|
5651
|
+
}
|
|
5652
|
+
const CREDENTIAL_PATHS = [
|
|
5653
|
+
".ssh",
|
|
5654
|
+
".aws",
|
|
5655
|
+
".gnupg",
|
|
5656
|
+
".gitconfig",
|
|
5657
|
+
".netrc",
|
|
5658
|
+
".npmrc",
|
|
5659
|
+
".config/gh"
|
|
5660
|
+
];
|
|
5661
|
+
function collectPathFindings(path, scope) {
|
|
5662
|
+
if (CREDENTIAL_PATHS.some((entry) => isUnder(path, join(scope.home, entry)))) return [{
|
|
5663
|
+
kind: "credential",
|
|
5664
|
+
target: path
|
|
5665
|
+
}];
|
|
5666
|
+
return isInScope(path, scope) ? [] : [{
|
|
5667
|
+
kind: "path",
|
|
5668
|
+
target: path
|
|
5669
|
+
}];
|
|
5670
|
+
}
|
|
5671
|
+
function collectToolFindings(action, scope) {
|
|
5672
|
+
const verb = action.tool.split("__").at(-1) ?? "";
|
|
5673
|
+
if (!/merge|create|comment|update|delete|push|close|review/u.test(verb)) return [];
|
|
5674
|
+
const number = action.input["pullNumber"] ?? action.input["pull_number"] ?? action.input["issue_number"];
|
|
5675
|
+
return !verb.includes("merge") && typeof number === "number" && scope.pullRequests.includes(number) ? [] : [{
|
|
5676
|
+
kind: "remote-write",
|
|
5677
|
+
target: action.tool
|
|
5678
|
+
}];
|
|
5679
|
+
}
|
|
5680
|
+
const PATH_WRITERS = /* @__PURE__ */ new Set([
|
|
5681
|
+
"rm",
|
|
5682
|
+
"rmdir",
|
|
5683
|
+
"shred",
|
|
5684
|
+
"truncate",
|
|
5685
|
+
"chmod",
|
|
5686
|
+
"chown"
|
|
5687
|
+
]);
|
|
5688
|
+
const DESTINATION_WRITERS = /* @__PURE__ */ new Set([
|
|
5689
|
+
"cp",
|
|
5690
|
+
"mv",
|
|
5691
|
+
"rsync",
|
|
5692
|
+
"ln"
|
|
5693
|
+
]);
|
|
5694
|
+
function collectCommandFindings(command, cwd, scope) {
|
|
5695
|
+
const findings = [];
|
|
5696
|
+
const hasPipe = /(?<!\|)\|(?!\|)/u.test(command);
|
|
5697
|
+
let directory = cwd;
|
|
5698
|
+
for (const segment of splitShellCommand(command).segments) {
|
|
5699
|
+
const [name, ...args] = getCommandWords(splitWords$1(segment.text));
|
|
5700
|
+
for (const target of collectRedirectTargets(segment.text)) findings.push(...collectTargetFindings(directory, target, scope));
|
|
5701
|
+
if (name === void 0) continue;
|
|
5702
|
+
if (name === "cd" && hasPipe) directory = null;
|
|
5703
|
+
else if (name === "cd") directory = args[0] === void 0 ? scope.home : resolvePath(directory, args[0], scope.home);
|
|
5704
|
+
else findings.push(...collectProgramFindings(name, args, directory, cwd, scope));
|
|
5705
|
+
}
|
|
5706
|
+
return findings;
|
|
5707
|
+
}
|
|
5708
|
+
function collectProgramFindings(name, args, directory, cwd, scope) {
|
|
5709
|
+
const resolve = (path) => collectTargetFindings(directory, path, scope);
|
|
5710
|
+
const operands = args.filter((word) => !word.startsWith("-"));
|
|
5711
|
+
if (name === "git") return collectGitFindings(args, directory, cwd, scope);
|
|
5712
|
+
if (name === "gh") return collectGhFindings(args, scope);
|
|
5713
|
+
if (PATH_WRITERS.has(name)) return operands.flatMap((path) => resolve(path));
|
|
5714
|
+
if (DESTINATION_WRITERS.has(name)) return collectCopyFindings(name, args, operands).flatMap((path) => resolve(path));
|
|
5715
|
+
if (name === "find" && args.some((word) => word === "-delete" || word === "-exec")) return operands.slice(0, 1).flatMap((path) => resolve(path));
|
|
5716
|
+
if ((name === "perl" || name === "sed") && args.some((word) => /^-\w*i/u.test(word))) return collectInPlaceFiles(args).flatMap((path) => resolve(path));
|
|
5717
|
+
if (name === "ssh-keygen") return collectKeygenFiles(args).flatMap((path) => resolve(path));
|
|
5718
|
+
if (name === "ssh") {
|
|
5719
|
+
const logIndex = args.indexOf("-E");
|
|
5720
|
+
const log = logIndex === -1 ? void 0 : args[logIndex + 1];
|
|
5721
|
+
return [...log === void 0 ? [] : resolve(log), ...collectSSHFindings(args)];
|
|
5722
|
+
}
|
|
5723
|
+
if (name === "scp") {
|
|
5724
|
+
const destination = collectSSHOperands(args).at(-1);
|
|
5725
|
+
if (destination === void 0 || /^[^/]+:/u.test(destination)) return destination === void 0 ? [] : [{
|
|
5726
|
+
kind: "remote-write",
|
|
5727
|
+
target: "scp"
|
|
5728
|
+
}];
|
|
5729
|
+
return resolve(destination);
|
|
5730
|
+
}
|
|
5731
|
+
if (name === "curl") return collectCurlFindings(args);
|
|
5732
|
+
const finding = findProgramFinding(name, args);
|
|
5733
|
+
return finding === null ? [] : [finding];
|
|
5734
|
+
}
|
|
5735
|
+
function collectCopyFindings(name, args, operands) {
|
|
5736
|
+
const optionIndex = args.findIndex((word) => word === "-t" || word === "--target-directory");
|
|
5737
|
+
let targetDirectory = args.find((word) => word.startsWith("--target-directory="))?.slice(19);
|
|
5738
|
+
if (targetDirectory === void 0 && optionIndex !== -1) targetDirectory = args[optionIndex + 1];
|
|
5739
|
+
if (targetDirectory !== void 0) {
|
|
5740
|
+
const sources = operands.filter((word) => word !== targetDirectory);
|
|
5741
|
+
return [targetDirectory, ...name === "mv" ? sources : []];
|
|
5742
|
+
}
|
|
5743
|
+
if (operands.length < 2) return [];
|
|
5744
|
+
return [operands.at(-1) ?? "", ...name === "mv" ? operands.slice(0, -1) : []];
|
|
5745
|
+
}
|
|
5746
|
+
const KEYGEN_READ_OPTIONS = /* @__PURE__ */ new Set([
|
|
5747
|
+
"-l",
|
|
5748
|
+
"-y",
|
|
5749
|
+
"-F",
|
|
5750
|
+
"-B",
|
|
5751
|
+
"-R"
|
|
5752
|
+
]);
|
|
5753
|
+
function collectKeygenFiles(args) {
|
|
5754
|
+
if (args.some((word) => KEYGEN_READ_OPTIONS.has(word))) return [];
|
|
5755
|
+
const index = args.indexOf("-f");
|
|
5756
|
+
const file = index === -1 ? void 0 : args[index + 1];
|
|
5757
|
+
return file === void 0 ? [] : [file];
|
|
5758
|
+
}
|
|
5759
|
+
const SSH_VALUE_OPTIONS = /* @__PURE__ */ new Set([
|
|
5760
|
+
"-b",
|
|
5761
|
+
"-c",
|
|
5762
|
+
"-D",
|
|
5763
|
+
"-E",
|
|
5764
|
+
"-F",
|
|
5765
|
+
"-i",
|
|
5766
|
+
"-J",
|
|
5767
|
+
"-l",
|
|
5768
|
+
"-L",
|
|
5769
|
+
"-m"
|
|
5770
|
+
]);
|
|
5771
|
+
const SSH_MORE_VALUE_OPTIONS = /* @__PURE__ */ new Set([
|
|
5772
|
+
"-o",
|
|
5773
|
+
"-p",
|
|
5774
|
+
"-P",
|
|
5775
|
+
"-Q",
|
|
5776
|
+
"-R",
|
|
5777
|
+
"-S",
|
|
5778
|
+
"-W",
|
|
5779
|
+
"-w"
|
|
5780
|
+
]);
|
|
5781
|
+
function collectSSHOperands(args) {
|
|
5782
|
+
const operands = [];
|
|
5783
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
5784
|
+
const word = args[index] ?? "";
|
|
5785
|
+
if (operands.length > 0 || !word.startsWith("-")) operands.push(word);
|
|
5786
|
+
else if (SSH_VALUE_OPTIONS.has(word) || SSH_MORE_VALUE_OPTIONS.has(word)) index += 1;
|
|
5787
|
+
}
|
|
5788
|
+
return operands;
|
|
5789
|
+
}
|
|
5790
|
+
const SSH_READS = /* @__PURE__ */ new Set([
|
|
5791
|
+
"cat",
|
|
5792
|
+
"df",
|
|
5793
|
+
"du",
|
|
5794
|
+
"free",
|
|
5795
|
+
"head",
|
|
5796
|
+
"id",
|
|
5797
|
+
"ls",
|
|
5798
|
+
"ps",
|
|
5799
|
+
"pwd",
|
|
5800
|
+
"stat",
|
|
5801
|
+
"tail",
|
|
5802
|
+
"uname",
|
|
5803
|
+
"uptime",
|
|
5804
|
+
"whoami"
|
|
5805
|
+
]);
|
|
5806
|
+
function collectSSHFindings(args) {
|
|
5807
|
+
const [host, ...remote] = collectSSHOperands(args);
|
|
5808
|
+
if (host === void 0) return [];
|
|
5809
|
+
const command = remote.join(" ");
|
|
5810
|
+
const [program] = command.trim().split(/\s+/u);
|
|
5811
|
+
return SSH_READS.has(program ?? "") && !/[;&|<>`\n\r]|\$\(/u.test(command) ? [] : [{
|
|
5812
|
+
kind: "remote-write",
|
|
5813
|
+
target: "ssh"
|
|
5814
|
+
}];
|
|
5815
|
+
}
|
|
5816
|
+
const LOCAL_HOSTS = /* @__PURE__ */ new Set([
|
|
5817
|
+
"localhost",
|
|
5818
|
+
"127.0.0.1",
|
|
5819
|
+
"[::1]",
|
|
5820
|
+
"0.0.0.0"
|
|
5821
|
+
]);
|
|
5822
|
+
function collectCurlFindings(args) {
|
|
5823
|
+
const joined = args.join(" ");
|
|
5824
|
+
const isUpload = args.some((word) => /^-[a-zA-Z]*[dFT]$|^--(?:data\S*|form|upload-file|json)$/u.test(word));
|
|
5825
|
+
const isWriteMethod = /(?:-X\s*|--request\s+)(?:POST|PUT|PATCH|DELETE)/u.test(joined);
|
|
5826
|
+
if (!isUpload && !isWriteMethod) return [];
|
|
5827
|
+
const hosts = args.filter((word) => /^(?:https?:\/\/|localhost\b|127\.)/u.test(word)).map((word) => /^(?:https?:\/\/)?(?<host>\[[^\]]+\]|[^/:?#\s]+)/u.exec(word)?.groups?.["host"]?.toLowerCase() ?? "");
|
|
5828
|
+
return hosts.length > 0 && hosts.every((host) => LOCAL_HOSTS.has(host)) ? [] : [{
|
|
5829
|
+
kind: "remote-write",
|
|
5830
|
+
target: "curl upload"
|
|
5831
|
+
}];
|
|
5832
|
+
}
|
|
5833
|
+
const GIT_DIRECTORY_WRITES = /* @__PURE__ */ new Set([
|
|
5834
|
+
"clean",
|
|
5835
|
+
"reset",
|
|
5836
|
+
"checkout",
|
|
5837
|
+
"switch",
|
|
5838
|
+
"merge",
|
|
5839
|
+
"commit",
|
|
5840
|
+
"rm",
|
|
5841
|
+
"stash",
|
|
5842
|
+
"push"
|
|
5843
|
+
]);
|
|
5844
|
+
function collectGitFindings(args, directory, cwd, scope) {
|
|
5845
|
+
let gitDirectory = directory;
|
|
5846
|
+
let index = 0;
|
|
5847
|
+
while (index < args.length && args[index]?.startsWith("-") === true) {
|
|
5848
|
+
const option = args[index];
|
|
5849
|
+
if (option === "-C") gitDirectory = resolvePath(gitDirectory, args[index + 1] ?? "", scope.home);
|
|
5850
|
+
index += option === "-C" || option === "-c" ? 2 : 1;
|
|
5851
|
+
}
|
|
5852
|
+
const subcommand = args[index] ?? "";
|
|
5853
|
+
const rest = args.slice(index + 1);
|
|
5854
|
+
const operands = rest.filter((word) => !word.startsWith("-"));
|
|
5855
|
+
const findings = [];
|
|
5856
|
+
if (gitDirectory !== null && GIT_DIRECTORY_WRITES.has(subcommand) && !isInScope(gitDirectory, scope)) findings.push({
|
|
5857
|
+
kind: "path",
|
|
5858
|
+
target: gitDirectory
|
|
5859
|
+
});
|
|
5860
|
+
if (subcommand === "worktree" && operands[0] === "remove" && operands[1] !== void 0) findings.push(...collectTargetFindings(gitDirectory, operands[1], scope));
|
|
5861
|
+
if (subcommand === "branch" && rest.some((word) => /^-(?:d|D|-delete)$/u.test(word))) for (const branch of operands.filter((word) => !scope.branches.includes(word))) findings.push({
|
|
5862
|
+
kind: "branch",
|
|
5863
|
+
target: branch
|
|
5864
|
+
});
|
|
5865
|
+
const isConfigRead = rest.some((word) => /^--(?:get|get-all|get-regexp|list)$|^-l$/u.test(word));
|
|
5866
|
+
if (subcommand === "config" && rest.includes("--global") && !isConfigRead) findings.push({
|
|
5867
|
+
kind: "credential",
|
|
5868
|
+
target: `git config ${operands[0] ?? ""}`
|
|
5869
|
+
});
|
|
5870
|
+
if (subcommand === "push") {
|
|
5871
|
+
const currentBranch = gitDirectory === cwd ? scope.currentBranch : null;
|
|
5872
|
+
findings.push(...collectPushFindings(rest, operands, currentBranch, scope));
|
|
5873
|
+
}
|
|
5874
|
+
return findings;
|
|
5875
|
+
}
|
|
5876
|
+
function collectPushFindings(rest, operands, currentBranch, scope) {
|
|
5877
|
+
const [remote, ...refspecs] = operands;
|
|
5878
|
+
const findings = [];
|
|
5879
|
+
if (rest.includes("--all") || rest.includes("--mirror")) findings.push({
|
|
5880
|
+
kind: "branch",
|
|
5881
|
+
target: "--all"
|
|
5882
|
+
});
|
|
5883
|
+
if (remote !== void 0 && !isUnresolved(remote) && !isOwnedRemote(remote, scope)) findings.push({
|
|
5884
|
+
kind: "remote-write",
|
|
5885
|
+
target: remote
|
|
5886
|
+
});
|
|
5887
|
+
const destinations = refspecs.length === 0 && !rest.includes("--tags") ? ["HEAD"] : refspecs;
|
|
5888
|
+
for (const refspec of destinations) {
|
|
5889
|
+
const named = (refspec.split(":").at(-1) ?? refspec).replace(/^\+/u, "").replace(/^refs\/heads\//u, "");
|
|
5890
|
+
const branch = named === "HEAD" ? currentBranch : named;
|
|
5891
|
+
if (branch !== null && !isUnresolved(branch) && !branch.startsWith("refs/tags/") && !scope.branches.includes(branch)) findings.push({
|
|
5892
|
+
kind: "branch",
|
|
5893
|
+
target: branch
|
|
5894
|
+
});
|
|
5895
|
+
}
|
|
5896
|
+
return findings;
|
|
5897
|
+
}
|
|
5898
|
+
function isUnresolved(word) {
|
|
5899
|
+
return /[$`]/u.test(word);
|
|
5900
|
+
}
|
|
5901
|
+
function isOwnedRemote(remote, scope) {
|
|
5902
|
+
if (scope.remotes.some((entry) => entry.name === remote)) return true;
|
|
5903
|
+
const slug = toRepositorySlug(remote);
|
|
5904
|
+
return slug !== null && scope.remotes.some((entry) => toRepositorySlug(entry.url) === slug);
|
|
5905
|
+
}
|
|
5906
|
+
function toRepositorySlug(reference) {
|
|
5907
|
+
const match = /^(?:[a-z+]+:\/\/)?(?:[^@/]+@)?(?<host>[^/:]+)[:/](?<owner>[^/]+)\/(?<name>[^/]+?)(?:\.git)?\/?$/iu.exec(reference);
|
|
5908
|
+
if (match?.groups === void 0) return null;
|
|
5909
|
+
const parts = match.groups;
|
|
5910
|
+
return `${parts["host"] ?? ""}/${parts["owner"] ?? ""}/${parts["name"] ?? ""}`.toLowerCase();
|
|
5911
|
+
}
|
|
5912
|
+
const GH_PR_WRITES = /* @__PURE__ */ new Set([
|
|
5913
|
+
"comment",
|
|
5914
|
+
"merge",
|
|
5915
|
+
"edit",
|
|
5916
|
+
"close",
|
|
5917
|
+
"review",
|
|
5918
|
+
"ready",
|
|
5919
|
+
"reopen"
|
|
5920
|
+
]);
|
|
5921
|
+
const GH_REMOTE_WRITES = /* @__PURE__ */ new Set([
|
|
5922
|
+
"gist",
|
|
5923
|
+
"release",
|
|
5924
|
+
"repo",
|
|
5925
|
+
"ruleset",
|
|
5926
|
+
"label",
|
|
5927
|
+
"workflow"
|
|
5928
|
+
]);
|
|
5929
|
+
const GH_CREDENTIAL_WRITES = /* @__PURE__ */ new Set([
|
|
5930
|
+
"secret",
|
|
5931
|
+
"variable",
|
|
5932
|
+
"ssh-key",
|
|
5933
|
+
"gpg-key"
|
|
5934
|
+
]);
|
|
5935
|
+
const GH_READS = /* @__PURE__ */ new Set([
|
|
5936
|
+
"list",
|
|
5937
|
+
"view",
|
|
5938
|
+
"clone",
|
|
5939
|
+
"status",
|
|
5940
|
+
"checks",
|
|
5941
|
+
"diff"
|
|
5942
|
+
]);
|
|
5943
|
+
function collectGhFindings(args, scope) {
|
|
5944
|
+
const group = args[0] ?? "";
|
|
5945
|
+
const verb = args[1] ?? "";
|
|
5946
|
+
if (group === "pr" && GH_PR_WRITES.has(verb)) {
|
|
5947
|
+
const number = Number(args.slice(2).find((word) => /^\d+$/u.test(word)));
|
|
5948
|
+
const repoIndex = args.findIndex((word) => word === "--repo" || word === "-R");
|
|
5949
|
+
const repo = repoIndex === -1 ? void 0 : args[repoIndex + 1];
|
|
5950
|
+
return verb !== "merge" && scope.pullRequests.includes(number) && (repo === void 0 || isCheckoutRepository(repo, scope)) ? [] : [{
|
|
5951
|
+
kind: "remote-write",
|
|
5952
|
+
target: `gh pr ${verb}`
|
|
5953
|
+
}];
|
|
5954
|
+
}
|
|
5955
|
+
if (group === "issue" && verb !== "" && !GH_READS.has(verb)) return [{
|
|
5956
|
+
kind: "remote-write",
|
|
5957
|
+
target: `gh issue ${verb}`
|
|
5958
|
+
}];
|
|
5959
|
+
if (group === "api") return collectGhAPIFindings(args.slice(1), scope);
|
|
5960
|
+
if (group === "auth" && [
|
|
5961
|
+
"refresh",
|
|
5962
|
+
"login",
|
|
5963
|
+
"logout"
|
|
5964
|
+
].includes(verb)) return [{
|
|
5965
|
+
kind: "credential",
|
|
5966
|
+
target: `gh auth ${verb}`
|
|
5967
|
+
}];
|
|
5968
|
+
if (GH_CREDENTIAL_WRITES.has(group) && !GH_READS.has(verb)) return [{
|
|
5969
|
+
kind: "credential",
|
|
5970
|
+
target: `gh ${group} ${verb}`
|
|
5971
|
+
}];
|
|
5972
|
+
if (GH_REMOTE_WRITES.has(group) && !GH_READS.has(verb)) return [{
|
|
5973
|
+
kind: "remote-write",
|
|
5974
|
+
target: `gh ${group} ${verb}`
|
|
5975
|
+
}];
|
|
5976
|
+
return [];
|
|
5977
|
+
}
|
|
5978
|
+
function isCheckoutRepository(repo, scope) {
|
|
5979
|
+
const wanted = repo.toLowerCase().replace(/\.git$/u, "");
|
|
5980
|
+
return scope.remotes.some((remote) => {
|
|
5981
|
+
const slug = toRepositorySlug(remote.url);
|
|
5982
|
+
return slug !== null && (slug === wanted || slug.endsWith(`/${wanted}`));
|
|
5983
|
+
});
|
|
5984
|
+
}
|
|
5985
|
+
function collectGhAPIFindings(args, scope) {
|
|
5986
|
+
const endpoint = args.find((word) => !word.startsWith("-")) ?? "";
|
|
5987
|
+
if (endpoint === "graphql") return args.some((word) => /\bmutation\b/u.test(word)) ? [{
|
|
5988
|
+
kind: "remote-write",
|
|
5989
|
+
target: "gh api graphql mutation"
|
|
5990
|
+
}] : [];
|
|
5991
|
+
const methodIndex = args.findIndex((word) => word === "-X" || word === "--method");
|
|
5992
|
+
const method = methodIndex === -1 ? null : (args[methodIndex + 1] ?? "").toUpperCase();
|
|
5993
|
+
const hasFields = args.some((word) => /^(?:-f|-F|--field|--raw-field|--input)$/u.test(word));
|
|
5994
|
+
if (method === "GET" || method === null && !hasFields) return [];
|
|
5995
|
+
const pull = /^\/?repos\/(?<repo>[^/]+\/[^/]+)\/(?:pulls|issues)\/(?<number>\d+)(?:\/|$)/u.exec(endpoint)?.groups;
|
|
5996
|
+
if (pull !== void 0 && scope.pullRequests.includes(Number(pull["number"])) && isCheckoutRepository(pull["repo"] ?? "", scope) && !endpoint.includes("/merge")) return [];
|
|
5997
|
+
return [{
|
|
5998
|
+
kind: /secret|collaborator|protection|key|permission/u.test(endpoint) ? "credential" : "remote-write",
|
|
5999
|
+
target: `gh api ${endpoint}`
|
|
6000
|
+
}];
|
|
6001
|
+
}
|
|
6002
|
+
const IAM_WRITE = /^(?:add|attach|change|create|deactivate|delete|detach|enable|put|remove|reset|set|tag|untag|update|upload)-/u;
|
|
6003
|
+
function findProgramFinding(name, args) {
|
|
6004
|
+
const joined = args.join(" ");
|
|
6005
|
+
if (name === "op" && /^(?:item|vault|document) (?:edit|create|delete)\b/u.test(joined)) return {
|
|
6006
|
+
kind: "credential",
|
|
6007
|
+
target: `op ${args[0] ?? ""} ${args[1] ?? ""}`
|
|
6008
|
+
};
|
|
6009
|
+
if (name === "aws" && args[0] === "iam" && IAM_WRITE.test(args[1] ?? "")) return {
|
|
6010
|
+
kind: "credential",
|
|
6011
|
+
target: `aws iam ${args[1] ?? ""}`
|
|
6012
|
+
};
|
|
6013
|
+
if (name === "docker" && /\b(?:volume|system|image|container) prune\b|\bvolume rm\b|\bdown\b.*(?:-v|--volumes)\b/u.test(joined)) return {
|
|
6014
|
+
kind: "prune",
|
|
6015
|
+
target: `docker ${joined}`
|
|
6016
|
+
};
|
|
6017
|
+
if (name === "npm" && args[0] === "publish") return {
|
|
6018
|
+
kind: "remote-write",
|
|
6019
|
+
target: "npm publish"
|
|
6020
|
+
};
|
|
6021
|
+
if (name === "kubectl" && /\b(?:apply|delete|replace|patch|scale)\b/u.test(joined)) return {
|
|
6022
|
+
kind: "remote-write",
|
|
6023
|
+
target: "kubectl write"
|
|
6024
|
+
};
|
|
6025
|
+
if (name === "terraform" && /\b(?:apply|destroy|import)\b/u.test(joined)) return {
|
|
6026
|
+
kind: "remote-write",
|
|
6027
|
+
target: "terraform write"
|
|
6028
|
+
};
|
|
6029
|
+
if (name === "psql" && /\b(?:UPDATE|DELETE|DROP|INSERT|TRUNCATE|ALTER)\b/iu.test(joined)) return {
|
|
6030
|
+
kind: "remote-write",
|
|
6031
|
+
target: "database write"
|
|
6032
|
+
};
|
|
6033
|
+
if (/^(?:bun|npm|pnpm|yarn)$/u.test(name) && /\bdeploy\b/u.test(joined) && /prod/u.test(joined)) return {
|
|
6034
|
+
kind: "remote-write",
|
|
6035
|
+
target: "production deploy"
|
|
6036
|
+
};
|
|
6037
|
+
return null;
|
|
6038
|
+
}
|
|
6039
|
+
function collectRedirectTargets(text) {
|
|
6040
|
+
const targets = [];
|
|
6041
|
+
let quote = null;
|
|
6042
|
+
let index = 0;
|
|
6043
|
+
while (index < text.length) {
|
|
6044
|
+
const ch = text.charAt(index);
|
|
6045
|
+
if (quote !== null) {
|
|
6046
|
+
quote = ch === quote ? null : quote;
|
|
6047
|
+
index += 1;
|
|
6048
|
+
} else if (ch === "'" || ch === "\"") {
|
|
6049
|
+
quote = ch;
|
|
6050
|
+
index += 1;
|
|
6051
|
+
} else if (ch === ">" && text.charAt(index - 1) !== "<") {
|
|
6052
|
+
let next = index + 1;
|
|
6053
|
+
if (text.charAt(next) === ">" || text.charAt(next) === "|") next += 1;
|
|
6054
|
+
if (text.charAt(next) === "&") {
|
|
6055
|
+
index = next + 1;
|
|
6056
|
+
continue;
|
|
6057
|
+
}
|
|
6058
|
+
const [target, end] = readWord(text, next);
|
|
6059
|
+
if (target !== "") targets.push(target);
|
|
6060
|
+
index = end;
|
|
6061
|
+
} else index += 1;
|
|
6062
|
+
}
|
|
6063
|
+
return targets;
|
|
6064
|
+
}
|
|
6065
|
+
function readWord(text, start) {
|
|
6066
|
+
let index = start;
|
|
6067
|
+
while (/\s/u.test(text.charAt(index))) index += 1;
|
|
6068
|
+
let word = "";
|
|
6069
|
+
let quote = null;
|
|
6070
|
+
while (index < text.length) {
|
|
6071
|
+
const ch = text.charAt(index);
|
|
6072
|
+
if (quote !== null) {
|
|
6073
|
+
if (ch === quote) quote = null;
|
|
6074
|
+
else word += ch;
|
|
6075
|
+
} else if (ch === "'" || ch === "\"") quote = ch;
|
|
6076
|
+
else if (/[\s;&|<>()]/u.test(ch)) break;
|
|
6077
|
+
else word += ch;
|
|
6078
|
+
index += 1;
|
|
6079
|
+
}
|
|
6080
|
+
return [word, index];
|
|
6081
|
+
}
|
|
6082
|
+
function collectInPlaceFiles(args) {
|
|
6083
|
+
const files = [];
|
|
6084
|
+
let skipNext = false;
|
|
6085
|
+
let sawScript = false;
|
|
6086
|
+
for (const word of args) if (skipNext) {
|
|
6087
|
+
skipNext = false;
|
|
6088
|
+
sawScript = true;
|
|
6089
|
+
} else if (/^-\w*e$/u.test(word)) skipNext = true;
|
|
6090
|
+
else if (!word.startsWith("-")) {
|
|
6091
|
+
if (sawScript) files.push(word);
|
|
6092
|
+
else sawScript = true;
|
|
6093
|
+
}
|
|
6094
|
+
return files;
|
|
6095
|
+
}
|
|
6096
|
+
function getCommandWords(words) {
|
|
6097
|
+
let index = 0;
|
|
6098
|
+
while (index < words.length) {
|
|
6099
|
+
const word = words[index] ?? "";
|
|
6100
|
+
if (/^\w+=/u.test(word) || word === "sudo" || word === "command") index += 1;
|
|
6101
|
+
else if (word === "env") {
|
|
6102
|
+
index += 1;
|
|
6103
|
+
while (words[index] === "-u" || /^\w+=/u.test(words[index] ?? "")) index += words[index] === "-u" ? 2 : 1;
|
|
6104
|
+
} else if (word === "timeout") index += 2;
|
|
6105
|
+
else break;
|
|
6106
|
+
}
|
|
6107
|
+
return words.slice(index).filter((word, position, all) => !/^\d*[<>]/u.test(word) && !/^\d*>{1,2}\|?$/u.test(all[position - 1] ?? ""));
|
|
6108
|
+
}
|
|
6109
|
+
function splitWords$1(text) {
|
|
6110
|
+
const words = [];
|
|
6111
|
+
let current = "";
|
|
6112
|
+
let quote = null;
|
|
6113
|
+
let started = false;
|
|
6114
|
+
for (const ch of text) if (quote !== null) {
|
|
6115
|
+
if (ch === quote) quote = null;
|
|
6116
|
+
else current += ch;
|
|
6117
|
+
} else if (ch === "'" || ch === "\"") {
|
|
6118
|
+
quote = ch;
|
|
6119
|
+
started = true;
|
|
6120
|
+
} else if (/\s/u.test(ch)) {
|
|
6121
|
+
if (started) words.push(current);
|
|
6122
|
+
current = "";
|
|
6123
|
+
started = false;
|
|
6124
|
+
} else {
|
|
6125
|
+
current += ch;
|
|
6126
|
+
started = true;
|
|
6127
|
+
}
|
|
6128
|
+
if (started) words.push(current);
|
|
6129
|
+
return words;
|
|
6130
|
+
}
|
|
6131
|
+
function resolvePath(cwd, path, home) {
|
|
6132
|
+
const expanded = path.replace(/^(?:~|\$HOME|\$\{HOME\})(?=\/|$)/u, home);
|
|
6133
|
+
if (/[$`]|^~/u.test(expanded) || expanded === "") return null;
|
|
6134
|
+
if (cwd === null && !isAbsolute(expanded)) return null;
|
|
6135
|
+
const absolute = isAbsolute(expanded) || cwd === null ? expanded : join(cwd, expanded);
|
|
6136
|
+
return normalize(absolute).replace(/(?<=.)\/$/u, "");
|
|
6137
|
+
}
|
|
6138
|
+
const SCRATCH_PATHS = [
|
|
6139
|
+
"/tmp",
|
|
6140
|
+
"/dev/null",
|
|
6141
|
+
"/dev/stdout",
|
|
6142
|
+
"/dev/stderr"
|
|
6143
|
+
];
|
|
6144
|
+
function isInScope(path, scope) {
|
|
6145
|
+
if (SCRATCH_PATHS.some((entry) => isUnder(path, entry))) return true;
|
|
6146
|
+
return scope.worktrees.some((worktree) => {
|
|
6147
|
+
const isNested = isUnder(path, join(worktree, ".worktrees"));
|
|
6148
|
+
const isOtherOwned = scope.worktrees.some((other) => other !== worktree && isUnder(path, other));
|
|
6149
|
+
return isUnder(path, worktree) && (!isNested || isOtherOwned);
|
|
6150
|
+
});
|
|
6151
|
+
}
|
|
6152
|
+
function isUnder(path, root) {
|
|
6153
|
+
return path === root || path.startsWith(`${root}/`);
|
|
6154
|
+
}
|
|
6155
|
+
//#endregion
|
|
6156
|
+
//#region src/model/load-repository-context.ts
|
|
6157
|
+
async function loadRepositoryContext(cwd) {
|
|
6158
|
+
if ([
|
|
6159
|
+
"GIT_DIR",
|
|
6160
|
+
"GIT_WORK_TREE",
|
|
6161
|
+
"GIT_COMMON_DIR"
|
|
6162
|
+
].some((name) => process.env[name] !== void 0)) return null;
|
|
6163
|
+
let directory = resolve(cwd);
|
|
6164
|
+
for (;;) {
|
|
6165
|
+
const gitPath = join(directory, ".git");
|
|
6166
|
+
try {
|
|
6167
|
+
await stat(gitPath);
|
|
6168
|
+
} catch (error) {
|
|
6169
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) return null;
|
|
6170
|
+
const parent = dirname(directory);
|
|
6171
|
+
if (parent === directory) return null;
|
|
6172
|
+
directory = parent;
|
|
6173
|
+
continue;
|
|
6174
|
+
}
|
|
6175
|
+
let gitDir = gitPath;
|
|
6176
|
+
let head;
|
|
6177
|
+
try {
|
|
6178
|
+
try {
|
|
6179
|
+
head = await readFile(join(gitDir, "HEAD"), "utf8");
|
|
6180
|
+
} catch {
|
|
6181
|
+
const link = await readFile(gitPath, "utf8");
|
|
6182
|
+
const path = /^gitdir: (?<path>.+)\r?\n?$/u.exec(link)?.groups?.["path"];
|
|
6183
|
+
if (path === void 0) return null;
|
|
6184
|
+
gitDir = resolve(directory, path.trim());
|
|
6185
|
+
head = await readFile(join(gitDir, "HEAD"), "utf8");
|
|
6186
|
+
}
|
|
6187
|
+
} catch {
|
|
6188
|
+
return null;
|
|
6189
|
+
}
|
|
6190
|
+
let commonDir = gitDir;
|
|
6191
|
+
try {
|
|
6192
|
+
const common = await readFile(join(gitDir, "commondir"), "utf8");
|
|
6193
|
+
commonDir = resolve(gitDir, common.trim());
|
|
6194
|
+
} catch {
|
|
6195
|
+
commonDir = gitDir;
|
|
6196
|
+
}
|
|
6197
|
+
let defaultBranch = null;
|
|
6198
|
+
try {
|
|
6199
|
+
defaultBranch = parseReference(await readFile(join(commonDir, "refs", "remotes", "origin", "HEAD"), "utf8"), "refs/remotes/origin/");
|
|
6200
|
+
} catch {
|
|
6201
|
+
defaultBranch = null;
|
|
6202
|
+
}
|
|
6203
|
+
return {
|
|
6204
|
+
cwd: resolve(cwd),
|
|
6205
|
+
branch: parseReference(head, "refs/heads/"),
|
|
6206
|
+
defaultBranch
|
|
6207
|
+
};
|
|
6208
|
+
}
|
|
6209
|
+
}
|
|
6210
|
+
function parseReference(content, prefix) {
|
|
6211
|
+
const value = content.trim();
|
|
6212
|
+
const marker = `ref: ${prefix}`;
|
|
6213
|
+
if (!value.startsWith(marker)) return null;
|
|
6214
|
+
const branch = value.slice(marker.length);
|
|
6215
|
+
return branch !== "" && branch.length <= 256 && !/\s/u.test(branch) ? branch : null;
|
|
6216
|
+
}
|
|
6217
|
+
//#endregion
|
|
6218
|
+
//#region src/containment/build-cwd-scope.ts
|
|
6219
|
+
function buildCWDScope(input) {
|
|
6220
|
+
const isDefault = input.branch === input.defaultBranch || input.defaultBranch === null && DEFAULT_NAMES.has(input.branch ?? "");
|
|
6221
|
+
return {
|
|
6222
|
+
home: input.home,
|
|
6223
|
+
worktrees: [input.worktree],
|
|
6224
|
+
branches: input.branch !== null && !isDefault ? [input.branch] : [],
|
|
6225
|
+
currentBranch: input.branch,
|
|
6226
|
+
remotes: input.remotes,
|
|
6227
|
+
pullRequests: []
|
|
6228
|
+
};
|
|
6229
|
+
}
|
|
6230
|
+
const DEFAULT_NAMES = /* @__PURE__ */ new Set([
|
|
6231
|
+
"main",
|
|
6232
|
+
"master",
|
|
6233
|
+
"trunk",
|
|
6234
|
+
"develop"
|
|
6235
|
+
]);
|
|
6236
|
+
//#endregion
|
|
6237
|
+
//#region src/containment/load-task-scope.ts
|
|
6238
|
+
async function loadTaskScope(cwd) {
|
|
6239
|
+
const [checkout, context] = await Promise.all([findCheckout(resolve(cwd)), loadRepositoryContext(cwd)]);
|
|
6240
|
+
return buildCWDScope({
|
|
6241
|
+
home: homedir(),
|
|
6242
|
+
worktree: checkout?.worktree ?? resolve(cwd),
|
|
6243
|
+
branch: context?.branch ?? null,
|
|
6244
|
+
defaultBranch: context?.defaultBranch ?? null,
|
|
6245
|
+
remotes: checkout === null ? [] : await readRemotes(checkout.commonDir)
|
|
6246
|
+
});
|
|
6247
|
+
}
|
|
6248
|
+
const GIT_OVERRIDES = [
|
|
6249
|
+
"GIT_DIR",
|
|
6250
|
+
"GIT_WORK_TREE",
|
|
6251
|
+
"GIT_COMMON_DIR"
|
|
6252
|
+
];
|
|
6253
|
+
async function findCheckout(start) {
|
|
6254
|
+
if (GIT_OVERRIDES.some((name) => process.env[name] !== void 0)) return null;
|
|
6255
|
+
for (let directory = start;; directory = dirname(directory)) {
|
|
6256
|
+
const gitPath = join(directory, ".git");
|
|
6257
|
+
const info = await stat(gitPath).catch(() => null);
|
|
6258
|
+
if (info !== null) {
|
|
6259
|
+
const gitDir = info.isDirectory() ? gitPath : await findLinkedGitDir(directory, gitPath);
|
|
6260
|
+
return gitDir === null ? null : {
|
|
6261
|
+
worktree: directory,
|
|
6262
|
+
commonDir: await findCommonDir(gitDir)
|
|
6263
|
+
};
|
|
6264
|
+
}
|
|
6265
|
+
if (dirname(directory) === directory) return null;
|
|
6266
|
+
}
|
|
6267
|
+
}
|
|
6268
|
+
async function findLinkedGitDir(directory, gitPath) {
|
|
6269
|
+
const link = await readFile(gitPath, "utf8").catch(() => "");
|
|
6270
|
+
const path = /^gitdir: (?<path>.+)$/mu.exec(link)?.groups?.["path"];
|
|
6271
|
+
return path === void 0 ? null : resolve(directory, path.trim());
|
|
6272
|
+
}
|
|
6273
|
+
async function findCommonDir(gitDir) {
|
|
6274
|
+
const common = await readFile(join(gitDir, "commondir"), "utf8").catch(() => null);
|
|
6275
|
+
return common === null ? gitDir : resolve(gitDir, common.trim());
|
|
6276
|
+
}
|
|
6277
|
+
async function readRemotes(commonDir) {
|
|
6278
|
+
const config = await readFile(join(commonDir, "config"), "utf8").catch(() => "");
|
|
6279
|
+
const remotes = [];
|
|
6280
|
+
let section = null;
|
|
6281
|
+
for (const line of config.split("\n")) {
|
|
6282
|
+
const header = /^\s*\[remote "(?<name>[^"]+)"\]/u.exec(line)?.groups?.["name"];
|
|
6283
|
+
if (header !== void 0) section = header;
|
|
6284
|
+
else if (/^\s*\[/u.test(line)) section = null;
|
|
6285
|
+
else if (section !== null) {
|
|
6286
|
+
const url = /^\s*(?:push)?url\s*=\s*(?<url>\S+)/u.exec(line)?.groups?.["url"];
|
|
6287
|
+
if (url !== void 0) remotes.push({
|
|
6288
|
+
name: section,
|
|
6289
|
+
url
|
|
6290
|
+
});
|
|
6291
|
+
}
|
|
6292
|
+
}
|
|
6293
|
+
return remotes;
|
|
6294
|
+
}
|
|
6295
|
+
//#endregion
|
|
6296
|
+
//#region src/containment/check-containment.ts
|
|
6297
|
+
const CONTAINMENT_RULE = "Outside Task Scope";
|
|
6298
|
+
async function checkContainment(request) {
|
|
6299
|
+
let scope;
|
|
6300
|
+
try {
|
|
6301
|
+
scope = await loadTaskScope(request.cwd);
|
|
6302
|
+
} catch {
|
|
6303
|
+
return null;
|
|
6304
|
+
}
|
|
6305
|
+
const findings = collectScopeFindings({
|
|
6306
|
+
tool: request.toolName,
|
|
6307
|
+
cwd: request.cwd,
|
|
6308
|
+
input: request.toolInput
|
|
6309
|
+
}, scope);
|
|
6310
|
+
if (findings.length === 0) return null;
|
|
6311
|
+
return {
|
|
6312
|
+
rule: CONTAINMENT_RULE,
|
|
6313
|
+
reason: formatContainmentReason(findings, scope),
|
|
6314
|
+
findings
|
|
6315
|
+
};
|
|
6316
|
+
}
|
|
6317
|
+
const KIND_LABELS = {
|
|
6318
|
+
path: "path",
|
|
6319
|
+
branch: "branch",
|
|
6320
|
+
"remote-write": "remote target",
|
|
6321
|
+
credential: "credential or global setting",
|
|
6322
|
+
prune: "shared resource"
|
|
6323
|
+
};
|
|
6324
|
+
function formatContainmentReason(findings, scope) {
|
|
6325
|
+
const targets = findings.slice(0, 5).map((finding) => `${KIND_LABELS[finding.kind]} ${finding.target}`).join(", ");
|
|
6326
|
+
const more = findings.length > 5 ? ` and ${findings.length - 5} more` : "";
|
|
6327
|
+
const [branch] = scope.branches;
|
|
6328
|
+
return `This action writes outside the task scope: ${targets}${more}. The task owns ${`the worktree ${scope.worktrees.join(", ")}${branch === void 0 ? "" : ` and the branch ${branch}`}`}. Do the work inside the task's own worktree and branch, or ask the user to extend the task scope to this target.`;
|
|
6329
|
+
}
|
|
5572
6330
|
const PRESETS = {
|
|
5573
6331
|
jev: {
|
|
5574
6332
|
protocol: "system-one",
|
|
@@ -6124,68 +6882,6 @@ function formatClassifierNote(note, apiKey) {
|
|
|
6124
6882
|
return apiKey === null || apiKey === "" ? note : note.split(apiKey).join("[redacted]");
|
|
6125
6883
|
}
|
|
6126
6884
|
//#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
6885
|
//#region node_modules/.bun/ts-pattern@5.9.0/node_modules/ts-pattern/dist/index.js
|
|
6190
6886
|
const t = Symbol.for("@ts-pattern/matcher");
|
|
6191
6887
|
const e = Symbol.for("@ts-pattern/isVariadic");
|
|
@@ -6969,64 +7665,6 @@ const REGENERABLE_DIRS = /* @__PURE__ */ new Set([
|
|
|
6969
7665
|
"venv"
|
|
6970
7666
|
]);
|
|
6971
7667
|
//#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
7668
|
//#region src/rules/classify-locally.ts
|
|
7031
7669
|
const ESCALATE = { kind: "escalate" };
|
|
7032
7670
|
function classifyLocally(payload) {
|
|
@@ -7104,12 +7742,18 @@ async function classifyAction(request, config, options = {}) {
|
|
|
7104
7742
|
};
|
|
7105
7743
|
}
|
|
7106
7744
|
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
|
-
|
|
7745
|
+
if (local.kind === "allow" && local.exception === READ_ONLY_EXCEPTION) return buildLocalAllow(local.exception);
|
|
7746
|
+
const containment = await checkContainment(request);
|
|
7747
|
+
if (containment !== null) {
|
|
7748
|
+
const guidance = await tryReadDenialGuidance();
|
|
7749
|
+
return {
|
|
7750
|
+
verdict: buildGuidedDeny(containment.rule, containment.reason, guidance),
|
|
7751
|
+
decidingStage: "containment",
|
|
7752
|
+
note: `denied by the containment check: ${containment.findings.map((finding) => finding.target).join(", ")}`,
|
|
7753
|
+
status: "deny"
|
|
7754
|
+
};
|
|
7755
|
+
}
|
|
7756
|
+
if (local.kind === "allow") return buildLocalAllow(local.exception);
|
|
7113
7757
|
if (options.localOnly === true) return {
|
|
7114
7758
|
verdict: null,
|
|
7115
7759
|
decidingStage: "local",
|
|
@@ -7124,6 +7768,15 @@ async function classifyAction(request, config, options = {}) {
|
|
|
7124
7768
|
status: outcome.diagnostics?.status ?? outcome.verdict?.kind ?? "failure"
|
|
7125
7769
|
};
|
|
7126
7770
|
}
|
|
7771
|
+
const READ_ONLY_EXCEPTION = "Read-only actions";
|
|
7772
|
+
function buildLocalAllow(exception) {
|
|
7773
|
+
return {
|
|
7774
|
+
verdict: { kind: "allow" },
|
|
7775
|
+
decidingStage: "local",
|
|
7776
|
+
note: `allowed by ${exception} (local)`,
|
|
7777
|
+
status: "allow"
|
|
7778
|
+
};
|
|
7779
|
+
}
|
|
7127
7780
|
function buildGuidedDeny(rule, reason, guidance) {
|
|
7128
7781
|
const sentence = /[.!?]$/u.test(reason) ? reason : `${reason}.`;
|
|
7129
7782
|
return {
|