auto-mode 0.21.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -3
- package/dist/cli.js +313 -8
- package/dist/index.d.ts +101 -4637
- package/dist/index.js +2 -7
- package/dist/{render-verdict-CE3ojHJk.js → parse-action-request-i-A64CGg.js} +1949 -1231
- package/dist/secret-rules.json +1 -0
- package/mods/auto-mode/contract/cli-limits.test.ts +11 -0
- package/mods/auto-mode/contract/cli-limits.ts +17 -0
- package/mods/auto-mode/hooks/register.ts +2 -5
- package/mods/auto-mode/hooks/testing.d.ts +2 -3
- package/package.json +2 -3
|
@@ -1,11 +1,51 @@
|
|
|
1
1
|
import { t as getEditFields } from "./get-edit-fields-BtAvHIsq.js";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import { appendFile, link, lstat, mkdir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
4
|
import { basename, dirname, isAbsolute, join, matchesGlob, normalize, parse, relative, resolve } from "node:path";
|
|
5
|
+
import { setTimeout } from "node:timers/promises";
|
|
4
6
|
import { spawn } from "node:child_process";
|
|
5
7
|
import { homedir, tmpdir } from "node:os";
|
|
6
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
-
import { setTimeout } from "node:timers/promises";
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
|
+
//#region mods/auto-mode/contract/find-current-direct-user-message.ts
|
|
10
|
+
function findCurrentDirectUserMessage(message) {
|
|
11
|
+
return message === null || message === void 0 || message.freshness === "stale" ? null : message.text;
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/budget/build-retry-key.ts
|
|
15
|
+
const DESCRIPTIVE_FIELDS = { Bash: [
|
|
16
|
+
"description",
|
|
17
|
+
"timeout",
|
|
18
|
+
"run_in_background"
|
|
19
|
+
] };
|
|
20
|
+
function buildRetryKey(request) {
|
|
21
|
+
const ignored = Object.hasOwn(DESCRIPTIVE_FIELDS, request.toolName) ? DESCRIPTIVE_FIELDS[request.toolName] ?? [] : [];
|
|
22
|
+
const input = Object.fromEntries(Object.entries(request.toolInput).filter(([key]) => !ignored.includes(key)));
|
|
23
|
+
const message = request.decisionContext?.lastDirectUserMessage ?? null;
|
|
24
|
+
const canonical = JSON.stringify(sortKeys({
|
|
25
|
+
cwd: request.cwd,
|
|
26
|
+
toolName: request.toolName,
|
|
27
|
+
toolInput: input,
|
|
28
|
+
lastDirectUserMessage: message?.text ?? null,
|
|
29
|
+
...message?.freshness === "stale" ? { lastDirectUserMessageFreshness: "stale" } : {}
|
|
30
|
+
}));
|
|
31
|
+
return createHash("sha256").update(canonical).digest("hex");
|
|
32
|
+
}
|
|
33
|
+
function sortKeys(value) {
|
|
34
|
+
if (Array.isArray(value)) return value.map((item) => sortKeys(item));
|
|
35
|
+
if (typeof value === "object" && value !== null) return Object.fromEntries(Object.entries(value).toSorted(([a], [b]) => a < b ? -1 : 1).map(([key, item]) => [key, sortKeys(item)]));
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/budget/find-retry-deny.ts
|
|
40
|
+
function findRetryDeny(state, retryKey) {
|
|
41
|
+
if (state.lastDenied === null || state.lastDenied.retryKey !== retryKey) return null;
|
|
42
|
+
return {
|
|
43
|
+
kind: "deny",
|
|
44
|
+
rule: state.lastDenied.rule,
|
|
45
|
+
reason: state.lastDenied.reason
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
9
49
|
//#region node_modules/.bun/zod@4.6.2/node_modules/zod/v4/core/util.js
|
|
10
50
|
function getEnumValues(entries) {
|
|
11
51
|
const numericValues = Object.values(entries).filter((v) => typeof v === "number");
|
|
@@ -5530,6 +5570,196 @@ function superRefine(fn, params) {
|
|
|
5530
5570
|
return /* @__PURE__ */ _superRefine(fn, params);
|
|
5531
5571
|
}
|
|
5532
5572
|
//#endregion
|
|
5573
|
+
//#region src/state/load-json-file.ts
|
|
5574
|
+
async function loadJSONFile(path, schema, fallback) {
|
|
5575
|
+
try {
|
|
5576
|
+
const text = await readFile(path, "utf8");
|
|
5577
|
+
const parsed = schema.safeParse(JSON.parse(text));
|
|
5578
|
+
return parsed.success ? parsed.data : fallback;
|
|
5579
|
+
} catch {
|
|
5580
|
+
return fallback;
|
|
5581
|
+
}
|
|
5582
|
+
}
|
|
5583
|
+
//#endregion
|
|
5584
|
+
//#region src/budget/types.ts
|
|
5585
|
+
const EMPTY_DENIAL_STATE = {
|
|
5586
|
+
consecutive: 0,
|
|
5587
|
+
session: 0,
|
|
5588
|
+
lastDenied: null
|
|
5589
|
+
};
|
|
5590
|
+
//#endregion
|
|
5591
|
+
//#region src/budget/load-denial-state.ts
|
|
5592
|
+
const count = number().int().nonnegative();
|
|
5593
|
+
const stateSchema$1 = strictObject({
|
|
5594
|
+
consecutive: count,
|
|
5595
|
+
session: count,
|
|
5596
|
+
lastDenied: strictObject({
|
|
5597
|
+
retryKey: string().min(1),
|
|
5598
|
+
rule: string(),
|
|
5599
|
+
reason: string()
|
|
5600
|
+
}).nullable()
|
|
5601
|
+
});
|
|
5602
|
+
function loadDenialState(path) {
|
|
5603
|
+
return loadJSONFile(path, stateSchema$1, EMPTY_DENIAL_STATE);
|
|
5604
|
+
}
|
|
5605
|
+
//#endregion
|
|
5606
|
+
//#region src/budget/plan-denial-budget.ts
|
|
5607
|
+
function planDenialBudget(state, verdict, retryKey, budget) {
|
|
5608
|
+
if (verdict === null) return {
|
|
5609
|
+
verdict: null,
|
|
5610
|
+
state,
|
|
5611
|
+
escalation: false
|
|
5612
|
+
};
|
|
5613
|
+
if (verdict.kind === "allow") return {
|
|
5614
|
+
verdict,
|
|
5615
|
+
state: {
|
|
5616
|
+
...state,
|
|
5617
|
+
consecutive: 0,
|
|
5618
|
+
lastDenied: null
|
|
5619
|
+
},
|
|
5620
|
+
escalation: false
|
|
5621
|
+
};
|
|
5622
|
+
if (state.consecutive >= budget.consecutive || state.session >= budget.perSession) return {
|
|
5623
|
+
verdict: null,
|
|
5624
|
+
state: EMPTY_DENIAL_STATE,
|
|
5625
|
+
escalation: true
|
|
5626
|
+
};
|
|
5627
|
+
const next = {
|
|
5628
|
+
consecutive: state.consecutive + 1,
|
|
5629
|
+
session: state.session + 1,
|
|
5630
|
+
lastDenied: {
|
|
5631
|
+
retryKey,
|
|
5632
|
+
rule: verdict.rule,
|
|
5633
|
+
reason: verdict.reason
|
|
5634
|
+
}
|
|
5635
|
+
};
|
|
5636
|
+
const left = Math.min(budget.consecutive - next.consecutive, budget.perSession - next.session);
|
|
5637
|
+
return {
|
|
5638
|
+
verdict: {
|
|
5639
|
+
...verdict,
|
|
5640
|
+
reason: `${verdict.reason} ${formatDenialsLeft(left)}`
|
|
5641
|
+
},
|
|
5642
|
+
state: next,
|
|
5643
|
+
escalation: false
|
|
5644
|
+
};
|
|
5645
|
+
}
|
|
5646
|
+
function formatDenialsLeft(left) {
|
|
5647
|
+
if (left > 0) return `Denials left before auto-mode asks the user: ${left}.`;
|
|
5648
|
+
return "This is the last denial before auto-mode asks the user. Stop now, without finishing the rest of the task, and tell the user what consent you need to continue.";
|
|
5649
|
+
}
|
|
5650
|
+
//#endregion
|
|
5651
|
+
//#region src/budget/resolve-denial-state-path.ts
|
|
5652
|
+
function resolveDenialStatePath(request, stateDir) {
|
|
5653
|
+
const agentID = request.decisionContext?.agentID ?? "";
|
|
5654
|
+
const key = createHash("sha256").update(`${request.sessionID}\0${agentID}`).digest("hex").slice(0, 32);
|
|
5655
|
+
return join(stateDir, "denials", `${key}.json`);
|
|
5656
|
+
}
|
|
5657
|
+
//#endregion
|
|
5658
|
+
//#region src/state/update-json-file.ts
|
|
5659
|
+
const SYSTEM_CLOCK = {
|
|
5660
|
+
now: Date.now,
|
|
5661
|
+
wait: (ms) => setTimeout(ms)
|
|
5662
|
+
};
|
|
5663
|
+
async function updateJSONFile(path, load, update, clock = SYSTEM_CLOCK) {
|
|
5664
|
+
await mkdir(dirname(path), {
|
|
5665
|
+
recursive: true,
|
|
5666
|
+
mode: 448
|
|
5667
|
+
});
|
|
5668
|
+
const lock = `${path}.lock`;
|
|
5669
|
+
const owner = await claimLock(lock, clock);
|
|
5670
|
+
try {
|
|
5671
|
+
const previous = await load(path);
|
|
5672
|
+
const next = update(previous);
|
|
5673
|
+
if (next !== previous) {
|
|
5674
|
+
const staged = `${path}.${randomUUID()}.tmp`;
|
|
5675
|
+
await writeFile(staged, `${JSON.stringify(next)}\n`, { mode: 384 });
|
|
5676
|
+
await rename(staged, path);
|
|
5677
|
+
}
|
|
5678
|
+
return {
|
|
5679
|
+
previous,
|
|
5680
|
+
next
|
|
5681
|
+
};
|
|
5682
|
+
} finally {
|
|
5683
|
+
await removeOwnLock(lock, owner);
|
|
5684
|
+
}
|
|
5685
|
+
}
|
|
5686
|
+
const STALE_LOCK_MS = 1e4;
|
|
5687
|
+
const LOCK_WAIT_MS = 3e3;
|
|
5688
|
+
const LOCK_POLL_MS = 10;
|
|
5689
|
+
async function claimLock(lock, clock) {
|
|
5690
|
+
const deadline = clock.now() + LOCK_WAIT_MS;
|
|
5691
|
+
const owner = randomUUID();
|
|
5692
|
+
const staged = `${lock}.${owner}`;
|
|
5693
|
+
await writeFile(staged, owner, {
|
|
5694
|
+
flag: "wx",
|
|
5695
|
+
mode: 384
|
|
5696
|
+
});
|
|
5697
|
+
try {
|
|
5698
|
+
for (;;) {
|
|
5699
|
+
try {
|
|
5700
|
+
await link(staged, lock);
|
|
5701
|
+
return owner;
|
|
5702
|
+
} catch (error) {
|
|
5703
|
+
if (!hasErrorCode(error, "EEXIST")) throw error;
|
|
5704
|
+
}
|
|
5705
|
+
const held = await stat(lock).catch(() => null);
|
|
5706
|
+
if (held !== null && isStale(held, clock)) {
|
|
5707
|
+
if (await claimStaleLock(lock, staged, held, clock)) return owner;
|
|
5708
|
+
}
|
|
5709
|
+
if (clock.now() > deadline) throw new Error("state file lock unavailable");
|
|
5710
|
+
await clock.wait(LOCK_POLL_MS);
|
|
5711
|
+
}
|
|
5712
|
+
} finally {
|
|
5713
|
+
await rm(staged, { force: true });
|
|
5714
|
+
}
|
|
5715
|
+
}
|
|
5716
|
+
function hasErrorCode(error, code) {
|
|
5717
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
5718
|
+
}
|
|
5719
|
+
function isStale(file, clock) {
|
|
5720
|
+
return clock.now() - file.mtimeMs > STALE_LOCK_MS;
|
|
5721
|
+
}
|
|
5722
|
+
async function claimStaleLock(lock, staged, held, clock) {
|
|
5723
|
+
const prefix = `${lock}.${String(held.ino)}-${String(held.mtimeMs)}`;
|
|
5724
|
+
const generation = await claimTakeoverMarker(prefix, clock);
|
|
5725
|
+
if (generation === null) return false;
|
|
5726
|
+
try {
|
|
5727
|
+
const current = await stat(lock).catch(() => null);
|
|
5728
|
+
if (current?.ino !== held.ino || current.mtimeMs !== held.mtimeMs) return false;
|
|
5729
|
+
await rename(staged, lock);
|
|
5730
|
+
return true;
|
|
5731
|
+
} finally {
|
|
5732
|
+
await Promise.all(Array.from({ length: generation + 1 }, (_, index) => rm(buildTakeoverMarkerPath(prefix, index), { force: true })));
|
|
5733
|
+
}
|
|
5734
|
+
}
|
|
5735
|
+
async function claimTakeoverMarker(prefix, clock) {
|
|
5736
|
+
for (let generation = 0;; generation += 1) {
|
|
5737
|
+
const marker = buildTakeoverMarkerPath(prefix, generation);
|
|
5738
|
+
try {
|
|
5739
|
+
await writeFile(marker, "", {
|
|
5740
|
+
flag: "wx",
|
|
5741
|
+
mode: 384
|
|
5742
|
+
});
|
|
5743
|
+
return generation;
|
|
5744
|
+
} catch (error) {
|
|
5745
|
+
if (!hasErrorCode(error, "EEXIST")) throw error;
|
|
5746
|
+
}
|
|
5747
|
+
const existing = await stat(marker).catch(() => null);
|
|
5748
|
+
if (existing === null || !isStale(existing, clock)) return null;
|
|
5749
|
+
}
|
|
5750
|
+
}
|
|
5751
|
+
function buildTakeoverMarkerPath(prefix, generation) {
|
|
5752
|
+
return `${prefix}-${String(generation)}.takeover`;
|
|
5753
|
+
}
|
|
5754
|
+
async function removeOwnLock(lock, owner) {
|
|
5755
|
+
if (await readFile(lock, "utf8").catch(() => null) === owner) await rm(lock, { force: true });
|
|
5756
|
+
}
|
|
5757
|
+
//#endregion
|
|
5758
|
+
//#region src/budget/update-denial-state.ts
|
|
5759
|
+
function updateDenialState(path, update, clock) {
|
|
5760
|
+
return updateJSONFile(path, loadDenialState, update, clock);
|
|
5761
|
+
}
|
|
5762
|
+
//#endregion
|
|
5533
5763
|
//#region src/state/resolve-xdg-dir.ts
|
|
5534
5764
|
const BASE_DIRS = {
|
|
5535
5765
|
config: {
|
|
@@ -5547,6 +5777,12 @@ function resolveXDGDir(host, kind) {
|
|
|
5547
5777
|
const root = base === void 0 || base === "" ? join(host.home, ...dir.fallback) : base;
|
|
5548
5778
|
return join(root, "auto-mode");
|
|
5549
5779
|
}
|
|
5780
|
+
const CLI_LIMITS = {
|
|
5781
|
+
keyCommandMs: 5e3,
|
|
5782
|
+
jevMs: 5e3,
|
|
5783
|
+
judgeMs: 12e4,
|
|
5784
|
+
evaluationMs: 265e3
|
|
5785
|
+
};
|
|
5550
5786
|
//#endregion
|
|
5551
5787
|
//#region src/config/presets.ts
|
|
5552
5788
|
const MESSAGES_DEFAULTS = {
|
|
@@ -5556,32 +5792,34 @@ const MESSAGES_DEFAULTS = {
|
|
|
5556
5792
|
maxTokens: 3e3,
|
|
5557
5793
|
timeoutMs: 45e3
|
|
5558
5794
|
};
|
|
5795
|
+
const JEV_PROVIDER = {
|
|
5796
|
+
protocol: "system-one",
|
|
5797
|
+
baseURL: "https://api.typesafe.ai",
|
|
5798
|
+
model: "jev-1.13.0",
|
|
5799
|
+
apiKeyEnv: "TYPESAFE_API_KEY",
|
|
5800
|
+
reasoning: false,
|
|
5801
|
+
maxTokens: 3e3,
|
|
5802
|
+
timeoutMs: CLI_LIMITS.jevMs
|
|
5803
|
+
};
|
|
5804
|
+
const SPARK_PROVIDER = {
|
|
5805
|
+
...MESSAGES_DEFAULTS,
|
|
5806
|
+
baseURL: "https://api.meta.ai",
|
|
5807
|
+
model: "muse-spark-1.3-contributor",
|
|
5808
|
+
apiKeyEnv: "META_API_KEY",
|
|
5809
|
+
maxTokens: 3e3,
|
|
5810
|
+
timeoutMs: 45e3
|
|
5811
|
+
};
|
|
5559
5812
|
const PRESETS = {
|
|
5560
|
-
jev:
|
|
5561
|
-
protocol: "system-one",
|
|
5562
|
-
baseURL: "https://api.typesafe.ai",
|
|
5563
|
-
model: "jev-1.13.0",
|
|
5564
|
-
apiKeyEnv: "TYPESAFE_API_KEY",
|
|
5565
|
-
reasoning: false,
|
|
5566
|
-
maxTokens: 3e3,
|
|
5567
|
-
timeoutMs: 5e3
|
|
5568
|
-
},
|
|
5813
|
+
jev: JEV_PROVIDER,
|
|
5569
5814
|
"claude-code": {
|
|
5570
5815
|
protocol: "claude-code",
|
|
5571
5816
|
baseURL: "https://api.anthropic.com",
|
|
5572
5817
|
model: "claude-sonnet-5-5",
|
|
5573
5818
|
reasoning: false,
|
|
5574
5819
|
maxTokens: 3e3,
|
|
5575
|
-
timeoutMs:
|
|
5576
|
-
},
|
|
5577
|
-
spark: {
|
|
5578
|
-
...MESSAGES_DEFAULTS,
|
|
5579
|
-
baseURL: "https://api.meta.ai",
|
|
5580
|
-
model: "muse-spark-1.3-contributor",
|
|
5581
|
-
apiKeyEnv: "META_API_KEY",
|
|
5582
|
-
maxTokens: 3e3,
|
|
5583
|
-
timeoutMs: 45e3
|
|
5820
|
+
timeoutMs: CLI_LIMITS.judgeMs
|
|
5584
5821
|
},
|
|
5822
|
+
spark: SPARK_PROVIDER,
|
|
5585
5823
|
claude: {
|
|
5586
5824
|
...MESSAGES_DEFAULTS,
|
|
5587
5825
|
model: "claude-haiku-4-5-20251001",
|
|
@@ -5670,7 +5908,7 @@ function toTimerDelay(ms) {
|
|
|
5670
5908
|
}
|
|
5671
5909
|
//#endregion
|
|
5672
5910
|
//#region src/config/read-api-key-from-command.ts
|
|
5673
|
-
const KEY_COMMAND_TIMEOUT_MS =
|
|
5911
|
+
const KEY_COMMAND_TIMEOUT_MS = CLI_LIMITS.keyCommandMs;
|
|
5674
5912
|
const MAX_KEY_OUTPUT_BYTES = 65536;
|
|
5675
5913
|
async function readApiKeyFromCommand(command, options = {}) {
|
|
5676
5914
|
const now = options.now ?? Date.now;
|
|
@@ -5763,6 +6001,7 @@ const denialBudgetSchema = strictObject({
|
|
|
5763
6001
|
const configFileSchema = strictObject({
|
|
5764
6002
|
classifiers: record(text, unknown()).optional(),
|
|
5765
6003
|
scopeSources: record(text, unknown()).optional(),
|
|
6004
|
+
protectedBranches: array(text).optional(),
|
|
5766
6005
|
decision: strictObject({
|
|
5767
6006
|
classifier: text.optional(),
|
|
5768
6007
|
judge: text.nullable().optional(),
|
|
@@ -5802,6 +6041,7 @@ function buildConfig(json, path, home) {
|
|
|
5802
6041
|
judge,
|
|
5803
6042
|
judgeOverturns: decision.judgeOverturns ?? "any",
|
|
5804
6043
|
scopeSources: configuredSources ?? DEFAULT_SCOPE_SOURCES,
|
|
6044
|
+
...file.protectedBranches === void 0 ? {} : { protectedBranches: file.protectedBranches },
|
|
5805
6045
|
classifierPath: expandHomePath(policy.frameworkPath ?? void 0, home),
|
|
5806
6046
|
rulesPath: expandHomePath(policy.rulesPath ?? void 0, home),
|
|
5807
6047
|
onFailure: decision.onFailure ?? DEFAULT_CONFIG.onFailure,
|
|
@@ -6032,236 +6272,6 @@ async function tryClassifyEdit(request, scope, host, reader = {}) {
|
|
|
6032
6272
|
}
|
|
6033
6273
|
}
|
|
6034
6274
|
//#endregion
|
|
6035
|
-
//#region mods/auto-mode/contract/find-current-direct-user-message.ts
|
|
6036
|
-
function findCurrentDirectUserMessage(message) {
|
|
6037
|
-
return message === null || message === void 0 || message.freshness === "stale" ? null : message.text;
|
|
6038
|
-
}
|
|
6039
|
-
//#endregion
|
|
6040
|
-
//#region src/budget/build-retry-key.ts
|
|
6041
|
-
const DESCRIPTIVE_FIELDS = { Bash: [
|
|
6042
|
-
"description",
|
|
6043
|
-
"timeout",
|
|
6044
|
-
"run_in_background"
|
|
6045
|
-
] };
|
|
6046
|
-
function buildRetryKey(request) {
|
|
6047
|
-
const ignored = Object.hasOwn(DESCRIPTIVE_FIELDS, request.toolName) ? DESCRIPTIVE_FIELDS[request.toolName] ?? [] : [];
|
|
6048
|
-
const input = Object.fromEntries(Object.entries(request.toolInput).filter(([key]) => !ignored.includes(key)));
|
|
6049
|
-
const message = request.decisionContext?.lastDirectUserMessage ?? null;
|
|
6050
|
-
const canonical = JSON.stringify(sortKeys({
|
|
6051
|
-
cwd: request.cwd,
|
|
6052
|
-
toolName: request.toolName,
|
|
6053
|
-
toolInput: input,
|
|
6054
|
-
lastDirectUserMessage: message?.text ?? null,
|
|
6055
|
-
...message?.freshness === "stale" ? { lastDirectUserMessageFreshness: "stale" } : {}
|
|
6056
|
-
}));
|
|
6057
|
-
return createHash("sha256").update(canonical).digest("hex");
|
|
6058
|
-
}
|
|
6059
|
-
function sortKeys(value) {
|
|
6060
|
-
if (Array.isArray(value)) return value.map((item) => sortKeys(item));
|
|
6061
|
-
if (typeof value === "object" && value !== null) return Object.fromEntries(Object.entries(value).toSorted(([a], [b]) => a < b ? -1 : 1).map(([key, item]) => [key, sortKeys(item)]));
|
|
6062
|
-
return value;
|
|
6063
|
-
}
|
|
6064
|
-
//#endregion
|
|
6065
|
-
//#region src/budget/find-retry-deny.ts
|
|
6066
|
-
function findRetryDeny(state, retryKey) {
|
|
6067
|
-
if (state.lastDenied === null || state.lastDenied.retryKey !== retryKey) return null;
|
|
6068
|
-
return {
|
|
6069
|
-
kind: "deny",
|
|
6070
|
-
rule: state.lastDenied.rule,
|
|
6071
|
-
reason: state.lastDenied.reason
|
|
6072
|
-
};
|
|
6073
|
-
}
|
|
6074
|
-
//#endregion
|
|
6075
|
-
//#region src/state/load-json-file.ts
|
|
6076
|
-
async function loadJSONFile(path, schema, fallback) {
|
|
6077
|
-
try {
|
|
6078
|
-
const text = await readFile(path, "utf8");
|
|
6079
|
-
const parsed = schema.safeParse(JSON.parse(text));
|
|
6080
|
-
return parsed.success ? parsed.data : fallback;
|
|
6081
|
-
} catch {
|
|
6082
|
-
return fallback;
|
|
6083
|
-
}
|
|
6084
|
-
}
|
|
6085
|
-
//#endregion
|
|
6086
|
-
//#region src/budget/types.ts
|
|
6087
|
-
const EMPTY_DENIAL_STATE = {
|
|
6088
|
-
consecutive: 0,
|
|
6089
|
-
session: 0,
|
|
6090
|
-
lastDenied: null
|
|
6091
|
-
};
|
|
6092
|
-
//#endregion
|
|
6093
|
-
//#region src/budget/load-denial-state.ts
|
|
6094
|
-
const count = number().int().nonnegative();
|
|
6095
|
-
const stateSchema$1 = strictObject({
|
|
6096
|
-
consecutive: count,
|
|
6097
|
-
session: count,
|
|
6098
|
-
lastDenied: strictObject({
|
|
6099
|
-
retryKey: string().min(1),
|
|
6100
|
-
rule: string(),
|
|
6101
|
-
reason: string()
|
|
6102
|
-
}).nullable()
|
|
6103
|
-
});
|
|
6104
|
-
function loadDenialState(path) {
|
|
6105
|
-
return loadJSONFile(path, stateSchema$1, EMPTY_DENIAL_STATE);
|
|
6106
|
-
}
|
|
6107
|
-
//#endregion
|
|
6108
|
-
//#region src/budget/plan-denial-budget.ts
|
|
6109
|
-
function planDenialBudget(state, verdict, retryKey, budget) {
|
|
6110
|
-
if (verdict === null) return {
|
|
6111
|
-
verdict: null,
|
|
6112
|
-
state,
|
|
6113
|
-
escalation: false
|
|
6114
|
-
};
|
|
6115
|
-
if (verdict.kind === "allow") return {
|
|
6116
|
-
verdict,
|
|
6117
|
-
state: {
|
|
6118
|
-
...state,
|
|
6119
|
-
consecutive: 0,
|
|
6120
|
-
lastDenied: null
|
|
6121
|
-
},
|
|
6122
|
-
escalation: false
|
|
6123
|
-
};
|
|
6124
|
-
if (state.consecutive >= budget.consecutive || state.session >= budget.perSession) return {
|
|
6125
|
-
verdict: null,
|
|
6126
|
-
state: EMPTY_DENIAL_STATE,
|
|
6127
|
-
escalation: true
|
|
6128
|
-
};
|
|
6129
|
-
const next = {
|
|
6130
|
-
consecutive: state.consecutive + 1,
|
|
6131
|
-
session: state.session + 1,
|
|
6132
|
-
lastDenied: {
|
|
6133
|
-
retryKey,
|
|
6134
|
-
rule: verdict.rule,
|
|
6135
|
-
reason: verdict.reason
|
|
6136
|
-
}
|
|
6137
|
-
};
|
|
6138
|
-
const left = Math.min(budget.consecutive - next.consecutive, budget.perSession - next.session);
|
|
6139
|
-
return {
|
|
6140
|
-
verdict: {
|
|
6141
|
-
...verdict,
|
|
6142
|
-
reason: `${verdict.reason} ${formatDenialsLeft(left)}`
|
|
6143
|
-
},
|
|
6144
|
-
state: next,
|
|
6145
|
-
escalation: false
|
|
6146
|
-
};
|
|
6147
|
-
}
|
|
6148
|
-
function formatDenialsLeft(left) {
|
|
6149
|
-
if (left > 0) return `Denials left before auto-mode asks the user: ${left}.`;
|
|
6150
|
-
return "This is the last denial before auto-mode asks the user. Stop now, without finishing the rest of the task, and tell the user what consent you need to continue.";
|
|
6151
|
-
}
|
|
6152
|
-
//#endregion
|
|
6153
|
-
//#region src/budget/resolve-denial-state-path.ts
|
|
6154
|
-
function resolveDenialStatePath(request, stateDir) {
|
|
6155
|
-
const agentID = request.decisionContext?.agentID ?? "";
|
|
6156
|
-
const key = createHash("sha256").update(`${request.sessionID}\0${agentID}`).digest("hex").slice(0, 32);
|
|
6157
|
-
return join(stateDir, "denials", `${key}.json`);
|
|
6158
|
-
}
|
|
6159
|
-
//#endregion
|
|
6160
|
-
//#region src/state/update-json-file.ts
|
|
6161
|
-
const SYSTEM_CLOCK = {
|
|
6162
|
-
now: Date.now,
|
|
6163
|
-
wait: (ms) => setTimeout(ms)
|
|
6164
|
-
};
|
|
6165
|
-
async function updateJSONFile(path, load, update, clock = SYSTEM_CLOCK) {
|
|
6166
|
-
await mkdir(dirname(path), {
|
|
6167
|
-
recursive: true,
|
|
6168
|
-
mode: 448
|
|
6169
|
-
});
|
|
6170
|
-
const lock = `${path}.lock`;
|
|
6171
|
-
const owner = await claimLock(lock, clock);
|
|
6172
|
-
try {
|
|
6173
|
-
const previous = await load(path);
|
|
6174
|
-
const next = update(previous);
|
|
6175
|
-
if (next !== previous) {
|
|
6176
|
-
const staged = `${path}.${randomUUID()}.tmp`;
|
|
6177
|
-
await writeFile(staged, `${JSON.stringify(next)}\n`, { mode: 384 });
|
|
6178
|
-
await rename(staged, path);
|
|
6179
|
-
}
|
|
6180
|
-
return {
|
|
6181
|
-
previous,
|
|
6182
|
-
next
|
|
6183
|
-
};
|
|
6184
|
-
} finally {
|
|
6185
|
-
await removeOwnLock(lock, owner);
|
|
6186
|
-
}
|
|
6187
|
-
}
|
|
6188
|
-
const STALE_LOCK_MS = 1e4;
|
|
6189
|
-
const LOCK_WAIT_MS = 3e3;
|
|
6190
|
-
const LOCK_POLL_MS = 10;
|
|
6191
|
-
async function claimLock(lock, clock) {
|
|
6192
|
-
const deadline = clock.now() + LOCK_WAIT_MS;
|
|
6193
|
-
const owner = randomUUID();
|
|
6194
|
-
const staged = `${lock}.${owner}`;
|
|
6195
|
-
await writeFile(staged, owner, {
|
|
6196
|
-
flag: "wx",
|
|
6197
|
-
mode: 384
|
|
6198
|
-
});
|
|
6199
|
-
try {
|
|
6200
|
-
for (;;) {
|
|
6201
|
-
try {
|
|
6202
|
-
await link(staged, lock);
|
|
6203
|
-
return owner;
|
|
6204
|
-
} catch (error) {
|
|
6205
|
-
if (!hasErrorCode(error, "EEXIST")) throw error;
|
|
6206
|
-
}
|
|
6207
|
-
const held = await stat(lock).catch(() => null);
|
|
6208
|
-
if (held !== null && isStale(held, clock)) {
|
|
6209
|
-
if (await claimStaleLock(lock, staged, held, clock)) return owner;
|
|
6210
|
-
}
|
|
6211
|
-
if (clock.now() > deadline) throw new Error("state file lock unavailable");
|
|
6212
|
-
await clock.wait(LOCK_POLL_MS);
|
|
6213
|
-
}
|
|
6214
|
-
} finally {
|
|
6215
|
-
await rm(staged, { force: true });
|
|
6216
|
-
}
|
|
6217
|
-
}
|
|
6218
|
-
function hasErrorCode(error, code) {
|
|
6219
|
-
return error instanceof Error && "code" in error && error.code === code;
|
|
6220
|
-
}
|
|
6221
|
-
function isStale(file, clock) {
|
|
6222
|
-
return clock.now() - file.mtimeMs > STALE_LOCK_MS;
|
|
6223
|
-
}
|
|
6224
|
-
async function claimStaleLock(lock, staged, held, clock) {
|
|
6225
|
-
const prefix = `${lock}.${String(held.ino)}-${String(held.mtimeMs)}`;
|
|
6226
|
-
const generation = await claimTakeoverMarker(prefix, clock);
|
|
6227
|
-
if (generation === null) return false;
|
|
6228
|
-
try {
|
|
6229
|
-
const current = await stat(lock).catch(() => null);
|
|
6230
|
-
if (current?.ino !== held.ino || current.mtimeMs !== held.mtimeMs) return false;
|
|
6231
|
-
await rename(staged, lock);
|
|
6232
|
-
return true;
|
|
6233
|
-
} finally {
|
|
6234
|
-
await Promise.all(Array.from({ length: generation + 1 }, (_, index) => rm(buildTakeoverMarkerPath(prefix, index), { force: true })));
|
|
6235
|
-
}
|
|
6236
|
-
}
|
|
6237
|
-
async function claimTakeoverMarker(prefix, clock) {
|
|
6238
|
-
for (let generation = 0;; generation += 1) {
|
|
6239
|
-
const marker = buildTakeoverMarkerPath(prefix, generation);
|
|
6240
|
-
try {
|
|
6241
|
-
await writeFile(marker, "", {
|
|
6242
|
-
flag: "wx",
|
|
6243
|
-
mode: 384
|
|
6244
|
-
});
|
|
6245
|
-
return generation;
|
|
6246
|
-
} catch (error) {
|
|
6247
|
-
if (!hasErrorCode(error, "EEXIST")) throw error;
|
|
6248
|
-
}
|
|
6249
|
-
const existing = await stat(marker).catch(() => null);
|
|
6250
|
-
if (existing === null || !isStale(existing, clock)) return null;
|
|
6251
|
-
}
|
|
6252
|
-
}
|
|
6253
|
-
function buildTakeoverMarkerPath(prefix, generation) {
|
|
6254
|
-
return `${prefix}-${String(generation)}.takeover`;
|
|
6255
|
-
}
|
|
6256
|
-
async function removeOwnLock(lock, owner) {
|
|
6257
|
-
if (await readFile(lock, "utf8").catch(() => null) === owner) await rm(lock, { force: true });
|
|
6258
|
-
}
|
|
6259
|
-
//#endregion
|
|
6260
|
-
//#region src/budget/update-denial-state.ts
|
|
6261
|
-
function updateDenialState(path, update, clock) {
|
|
6262
|
-
return updateJSONFile(path, loadDenialState, update, clock);
|
|
6263
|
-
}
|
|
6264
|
-
//#endregion
|
|
6265
6275
|
//#region src/config/resolve-claude-config-path.ts
|
|
6266
6276
|
const DEFAULT_DIRS = {
|
|
6267
6277
|
"settings.json": [".claude"],
|
|
@@ -6328,14 +6338,6 @@ function parseRules(body) {
|
|
|
6328
6338
|
};
|
|
6329
6339
|
}
|
|
6330
6340
|
//#endregion
|
|
6331
|
-
//#region src/git/to-repository-slug.ts
|
|
6332
|
-
function toRepositorySlug(reference) {
|
|
6333
|
-
const match = /^(?:[a-z+]+:\/\/)?(?:[^@/]+@)?(?<host>[^/:]+)[:/](?<owner>[^/]+)\/(?<name>[^/]+?)(?:\.git)?\/?$/iu.exec(reference);
|
|
6334
|
-
if (match?.groups === void 0) return null;
|
|
6335
|
-
const parts = match.groups;
|
|
6336
|
-
return `${parts["host"] ?? ""}/${parts["owner"] ?? ""}/${parts["name"] ?? ""}`.toLowerCase();
|
|
6337
|
-
}
|
|
6338
|
-
//#endregion
|
|
6339
6341
|
//#region src/shell/is-unresolved-word.ts
|
|
6340
6342
|
function isUnresolvedWord(word) {
|
|
6341
6343
|
return /[$`]/u.test(word);
|
|
@@ -6352,72 +6354,403 @@ function resolveShellPath(directory, path, home) {
|
|
|
6352
6354
|
//#endregion
|
|
6353
6355
|
//#region src/shell/split-shell-tokens.ts
|
|
6354
6356
|
function splitShellTokens(text) {
|
|
6355
|
-
const
|
|
6356
|
-
|
|
6357
|
-
let
|
|
6358
|
-
while (
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
index += 1;
|
|
6362
|
-
continue;
|
|
6363
|
-
}
|
|
6364
|
-
const separator = findOperator(SEPARATORS, text, index);
|
|
6365
|
-
if (separator !== void 0 && !text.startsWith("&>", index)) {
|
|
6366
|
-
tokens.push({
|
|
6367
|
-
kind: "separator",
|
|
6368
|
-
operator: separator,
|
|
6369
|
-
start: index,
|
|
6370
|
-
end: index + separator.length
|
|
6371
|
-
});
|
|
6372
|
-
index += separator.length;
|
|
6373
|
-
continue;
|
|
6374
|
-
}
|
|
6375
|
-
const word = readWord(text, index);
|
|
6376
|
-
hasSubstitution ||= word.hasSubstitution;
|
|
6377
|
-
const operator = findOperator(REDIRECTS, text, word.end);
|
|
6378
|
-
const isDescriptor = operator !== void 0 && /^\d+$/u.test(word.raw);
|
|
6379
|
-
if (word.raw !== "" && !isDescriptor) {
|
|
6380
|
-
if (word.isUnterminated) return {
|
|
6381
|
-
tokens: [...tokens, buildWordToken(word, index)],
|
|
6382
|
-
hasSubstitution,
|
|
6383
|
-
isUnterminated: true
|
|
6384
|
-
};
|
|
6385
|
-
tokens.push(buildWordToken(word, index));
|
|
6386
|
-
index = word.end;
|
|
6387
|
-
continue;
|
|
6388
|
-
}
|
|
6389
|
-
if (operator === void 0) {
|
|
6390
|
-
index += 1;
|
|
6391
|
-
continue;
|
|
6392
|
-
}
|
|
6393
|
-
const redirect = readRedirect(text, word.end, operator);
|
|
6394
|
-
hasSubstitution ||= redirect.hasSubstitution;
|
|
6395
|
-
tokens.push({
|
|
6396
|
-
kind: "redirect",
|
|
6397
|
-
operator: `${word.raw}${operator}`,
|
|
6398
|
-
target: redirect.target,
|
|
6399
|
-
start: index,
|
|
6400
|
-
end: redirect.end
|
|
6401
|
-
});
|
|
6402
|
-
if (redirect.isUnterminated) return {
|
|
6403
|
-
tokens,
|
|
6404
|
-
hasSubstitution,
|
|
6405
|
-
isUnterminated: true
|
|
6406
|
-
};
|
|
6407
|
-
index = redirect.end;
|
|
6357
|
+
const scanned = collectTextTokens(text, []);
|
|
6358
|
+
const literals = [];
|
|
6359
|
+
let recovered = scanned;
|
|
6360
|
+
while (recovered.unclosedAt !== null && !literals.includes(recovered.unclosedAt)) {
|
|
6361
|
+
literals.push(recovered.unclosedAt);
|
|
6362
|
+
recovered = collectTextTokens(text, literals);
|
|
6408
6363
|
}
|
|
6409
6364
|
return {
|
|
6410
|
-
tokens,
|
|
6411
|
-
|
|
6412
|
-
|
|
6365
|
+
tokens: scanned.tokens,
|
|
6366
|
+
substitutions: recovered.substitutions,
|
|
6367
|
+
hasSubstitution: scanned.hasSubstitution,
|
|
6368
|
+
isUnterminated: scanned.unclosedAt !== null
|
|
6413
6369
|
};
|
|
6414
6370
|
}
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6371
|
+
function collectTextTokens(text, literalOffsets) {
|
|
6372
|
+
const literals = new Set(literalOffsets);
|
|
6373
|
+
const top = buildLevel(text.length, null);
|
|
6374
|
+
let level = top;
|
|
6375
|
+
let hasSubstitution = false;
|
|
6376
|
+
let unclosedAt = null;
|
|
6377
|
+
const commandSubstitutions = /* @__PURE__ */ new Map();
|
|
6378
|
+
const withLevel = (limit, closer, run) => {
|
|
6379
|
+
const outer = level;
|
|
6380
|
+
const inner = buildLevel(limit, closer);
|
|
6381
|
+
level = inner;
|
|
6382
|
+
const result = run();
|
|
6383
|
+
level = outer;
|
|
6384
|
+
return {
|
|
6385
|
+
result,
|
|
6386
|
+
substitutions: inner.substitutions
|
|
6387
|
+
};
|
|
6388
|
+
};
|
|
6389
|
+
const collectTokens = (start, isArithmeticStart = false) => {
|
|
6390
|
+
const tokens = [];
|
|
6391
|
+
const heredocs = [];
|
|
6392
|
+
let arithmeticDepth = isArithmeticStart ? 1 : 0;
|
|
6393
|
+
let index = start;
|
|
6394
|
+
while (index < level.limit) {
|
|
6395
|
+
const ch = text.charAt(index);
|
|
6396
|
+
const isLiteral = literals.has(index);
|
|
6397
|
+
if (!isLiteral && ch === level.closer && level.depth === 0) return {
|
|
6398
|
+
tokens,
|
|
6399
|
+
end: index
|
|
6400
|
+
};
|
|
6401
|
+
if (text.startsWith("\\\n", index)) {
|
|
6402
|
+
index += 2;
|
|
6403
|
+
continue;
|
|
6404
|
+
}
|
|
6405
|
+
if (/\s/u.test(ch) && ch !== "\n") {
|
|
6406
|
+
index += 1;
|
|
6407
|
+
continue;
|
|
6408
|
+
}
|
|
6409
|
+
if (ch === "#") {
|
|
6410
|
+
const newline = text.indexOf("\n", index);
|
|
6411
|
+
const end = newline === -1 || newline > level.limit ? level.limit : newline;
|
|
6412
|
+
tokens.push({
|
|
6413
|
+
kind: "comment",
|
|
6414
|
+
text: text.slice(index, end),
|
|
6415
|
+
start: index,
|
|
6416
|
+
end
|
|
6417
|
+
});
|
|
6418
|
+
index = end;
|
|
6419
|
+
continue;
|
|
6420
|
+
}
|
|
6421
|
+
const separator = isLiteral ? void 0 : findOperator(SEPARATORS, text, index);
|
|
6422
|
+
if (separator !== void 0 && !text.startsWith("&>", index)) {
|
|
6423
|
+
tokens.push({
|
|
6424
|
+
kind: "separator",
|
|
6425
|
+
operator: separator,
|
|
6426
|
+
start: index,
|
|
6427
|
+
end: index + separator.length
|
|
6428
|
+
});
|
|
6429
|
+
index += separator.length;
|
|
6430
|
+
if (separator === "\n" && heredocs.length > 0) {
|
|
6431
|
+
const read = readHeredocs(index, heredocs.splice(0));
|
|
6432
|
+
tokens.push(...read.tokens);
|
|
6433
|
+
index = read.end;
|
|
6434
|
+
}
|
|
6435
|
+
continue;
|
|
6436
|
+
}
|
|
6437
|
+
const balance = level.balance;
|
|
6438
|
+
const word = readWord(index);
|
|
6439
|
+
const operator = findOperator(REDIRECTS, text, word.end);
|
|
6440
|
+
const isDescriptor = operator !== void 0 && /^\d+$/u.test(word.raw);
|
|
6441
|
+
if (word.raw !== "" && !isDescriptor) {
|
|
6442
|
+
tokens.push(buildWordToken(word, index));
|
|
6443
|
+
if (arithmeticDepth > 0 || word.raw.startsWith("((")) arithmeticDepth += level.balance - balance;
|
|
6444
|
+
if (word.isUnterminated) return {
|
|
6445
|
+
tokens,
|
|
6446
|
+
end: -1
|
|
6447
|
+
};
|
|
6448
|
+
index = word.end;
|
|
6449
|
+
continue;
|
|
6450
|
+
}
|
|
6451
|
+
if (operator === void 0) {
|
|
6452
|
+
index += 1;
|
|
6453
|
+
continue;
|
|
6454
|
+
}
|
|
6455
|
+
const opensHeredoc = (operator === "<<" || operator === "<<-") && arithmeticDepth <= 0;
|
|
6456
|
+
const redirect = readRedirect(word.end, operator, opensHeredoc);
|
|
6457
|
+
tokens.push({
|
|
6458
|
+
kind: "redirect",
|
|
6459
|
+
operator: `${word.raw}${operator}`,
|
|
6460
|
+
target: redirect.target,
|
|
6461
|
+
start: index,
|
|
6462
|
+
end: redirect.end
|
|
6463
|
+
});
|
|
6464
|
+
if (redirect.isUnterminated) return {
|
|
6465
|
+
tokens,
|
|
6466
|
+
end: -1
|
|
6467
|
+
};
|
|
6468
|
+
if (opensHeredoc && redirect.raw !== "") heredocs.push(buildHeredoc(redirect.raw, operator === "<<-", index));
|
|
6469
|
+
if (arithmeticDepth > 0) arithmeticDepth += level.balance - balance;
|
|
6470
|
+
index = redirect.end;
|
|
6471
|
+
}
|
|
6472
|
+
return {
|
|
6473
|
+
tokens,
|
|
6474
|
+
end: level.closer === null ? index : -1
|
|
6475
|
+
};
|
|
6476
|
+
};
|
|
6477
|
+
const readWord = (start) => {
|
|
6478
|
+
let value = "";
|
|
6479
|
+
let paren = null;
|
|
6480
|
+
let parenEnd = -1;
|
|
6481
|
+
let index = start;
|
|
6482
|
+
while (index < level.limit) {
|
|
6483
|
+
const ch = text.charAt(index);
|
|
6484
|
+
if (literals.has(index)) {
|
|
6485
|
+
value += ch;
|
|
6486
|
+
index += 1;
|
|
6487
|
+
continue;
|
|
6488
|
+
}
|
|
6489
|
+
if (ch === "\\") {
|
|
6490
|
+
value += text.slice(index, index + 2);
|
|
6491
|
+
index += 2;
|
|
6492
|
+
continue;
|
|
6493
|
+
}
|
|
6494
|
+
if (ch === "'" || ch === "\"" || text.startsWith("$'", index) && !literals.has(index + 1)) {
|
|
6495
|
+
const isANSI = ch === "$";
|
|
6496
|
+
const quoteAt = isANSI ? index + 1 : index;
|
|
6497
|
+
const quoted = readQuoted(quoteAt, isANSI);
|
|
6498
|
+
value += `${isANSI ? "$" : ""}${quoted.value}`;
|
|
6499
|
+
index = quoted.end;
|
|
6500
|
+
if (quoted.isUnterminated) return buildScannedWord(text, start, index, value, paren, true);
|
|
6501
|
+
continue;
|
|
6502
|
+
}
|
|
6503
|
+
if (text.startsWith("$$", index)) {
|
|
6504
|
+
value += "$$";
|
|
6505
|
+
index += 2;
|
|
6506
|
+
continue;
|
|
6507
|
+
}
|
|
6508
|
+
const expansion = readExpansion(index, false);
|
|
6509
|
+
if (expansion !== null) {
|
|
6510
|
+
value += text.slice(index, expansion.end);
|
|
6511
|
+
index = expansion.end;
|
|
6512
|
+
if (expansion.isUnterminated) return buildScannedWord(text, start, index, value, paren, true);
|
|
6513
|
+
continue;
|
|
6514
|
+
}
|
|
6515
|
+
if (/\s/u.test(ch) || /[;&|<>]/u.test(ch) || ch === "#" && index === parenEnd) break;
|
|
6516
|
+
if (ch === "(" || ch === ")") {
|
|
6517
|
+
if (level.closer === ")" && ch === ")" && level.depth === 0) break;
|
|
6518
|
+
if (level.closer === ")") level.depth += ch === "(" ? 1 : -1;
|
|
6519
|
+
level.balance += ch === "(" ? 1 : -1;
|
|
6520
|
+
paren ??= {
|
|
6521
|
+
value: value.length,
|
|
6522
|
+
raw: index - start
|
|
6523
|
+
};
|
|
6524
|
+
parenEnd = index + 1;
|
|
6525
|
+
}
|
|
6526
|
+
value += ch;
|
|
6527
|
+
index += 1;
|
|
6528
|
+
}
|
|
6529
|
+
return buildScannedWord(text, start, Math.min(index, level.limit), value, paren, false);
|
|
6530
|
+
};
|
|
6531
|
+
const readQuoted = (start, isANSI) => {
|
|
6532
|
+
const quote = text.charAt(start);
|
|
6533
|
+
let value = "";
|
|
6534
|
+
let index = start + 1;
|
|
6535
|
+
while (index < level.limit) {
|
|
6536
|
+
const ch = text.charAt(index);
|
|
6537
|
+
if (literals.has(index)) {
|
|
6538
|
+
value += ch;
|
|
6539
|
+
index += 1;
|
|
6540
|
+
} else if (ch === quote) return {
|
|
6541
|
+
value,
|
|
6542
|
+
end: index + 1,
|
|
6543
|
+
isUnterminated: false
|
|
6544
|
+
};
|
|
6545
|
+
else if (ch === "\\" && (quote === "\"" || isANSI)) {
|
|
6546
|
+
value += text.slice(index, index + 2);
|
|
6547
|
+
index += 2;
|
|
6548
|
+
} else {
|
|
6549
|
+
const expansion = quote === "\"" ? readExpansion(index, true) : null;
|
|
6550
|
+
const end = expansion?.end ?? index + 1;
|
|
6551
|
+
value += text.slice(index, end);
|
|
6552
|
+
index = end;
|
|
6553
|
+
if (expansion?.isUnterminated === true) return {
|
|
6554
|
+
value,
|
|
6555
|
+
end: index,
|
|
6556
|
+
isUnterminated: true
|
|
6557
|
+
};
|
|
6558
|
+
}
|
|
6559
|
+
}
|
|
6560
|
+
unclosedAt ??= start;
|
|
6561
|
+
return {
|
|
6562
|
+
value,
|
|
6563
|
+
end: level.limit,
|
|
6564
|
+
isUnterminated: true
|
|
6565
|
+
};
|
|
6566
|
+
};
|
|
6567
|
+
const readExpansion = (index, isInDoubleQuotes) => {
|
|
6568
|
+
if (text.startsWith("$(", index) || text.charAt(index) === "`") {
|
|
6569
|
+
hasSubstitution = true;
|
|
6570
|
+
const isBacktick = text.charAt(index) === "`";
|
|
6571
|
+
const bodyStart = index + (isBacktick ? 1 : 2);
|
|
6572
|
+
if (isBacktick) return collectExpansionBody(index, bodyStart, findBacktickEnd(bodyStart));
|
|
6573
|
+
return readCommandSubstitution(index, bodyStart);
|
|
6574
|
+
}
|
|
6575
|
+
if (text.startsWith("${", index)) return collectExpansionBody(index, -1, findParameterEnd(index + 2, isInDoubleQuotes));
|
|
6576
|
+
return null;
|
|
6577
|
+
};
|
|
6578
|
+
const readCommandSubstitution = (index, bodyStart) => {
|
|
6579
|
+
const key = `${index}:${level.limit}`;
|
|
6580
|
+
const scan = commandSubstitutions.get(key);
|
|
6581
|
+
if (scan !== void 0) {
|
|
6582
|
+
level.substitutions.push(...scan.substitutions);
|
|
6583
|
+
unclosedAt ??= scan.unclosedAt;
|
|
6584
|
+
return {
|
|
6585
|
+
end: scan.end,
|
|
6586
|
+
isUnterminated: scan.isUnterminated
|
|
6587
|
+
};
|
|
6588
|
+
}
|
|
6589
|
+
const collected = level.substitutions.length;
|
|
6590
|
+
const wasUnclosed = unclosedAt !== null;
|
|
6591
|
+
const read = readCommandSubstitutionBody(index, bodyStart);
|
|
6592
|
+
commandSubstitutions.set(key, {
|
|
6593
|
+
...read,
|
|
6594
|
+
substitutions: level.substitutions.slice(collected),
|
|
6595
|
+
unclosedAt: wasUnclosed ? null : unclosedAt
|
|
6596
|
+
});
|
|
6597
|
+
return read;
|
|
6598
|
+
};
|
|
6599
|
+
const readCommandSubstitutionBody = (index, bodyStart) => {
|
|
6600
|
+
if (text.charAt(bodyStart) === "(") {
|
|
6601
|
+
const arithmetic = withLevel(level.limit, ")", () => collectTokens(bodyStart + 1, true).end);
|
|
6602
|
+
if (arithmetic.result !== -1 && text.charAt(arithmetic.result + 1) === ")") {
|
|
6603
|
+
level.substitutions.push(...arithmetic.substitutions);
|
|
6604
|
+
return {
|
|
6605
|
+
end: arithmetic.result + 2,
|
|
6606
|
+
isUnterminated: false
|
|
6607
|
+
};
|
|
6608
|
+
}
|
|
6609
|
+
}
|
|
6610
|
+
const end = withLevel(level.limit, ")", () => collectTokens(bodyStart).end).result;
|
|
6611
|
+
return collectExpansionBody(index, bodyStart, end);
|
|
6612
|
+
};
|
|
6613
|
+
const collectExpansionBody = (index, bodyStart, end) => {
|
|
6614
|
+
if (end === -1) {
|
|
6615
|
+
unclosedAt ??= index;
|
|
6616
|
+
return {
|
|
6617
|
+
end: level.limit,
|
|
6618
|
+
isUnterminated: true
|
|
6619
|
+
};
|
|
6620
|
+
}
|
|
6621
|
+
if (bodyStart !== -1) level.substitutions.push(text.slice(bodyStart, end));
|
|
6622
|
+
return {
|
|
6623
|
+
end: end + 1,
|
|
6624
|
+
isUnterminated: false
|
|
6625
|
+
};
|
|
6626
|
+
};
|
|
6627
|
+
const findBacktickEnd = (start) => {
|
|
6628
|
+
for (let index = start; index < level.limit; index += 1) {
|
|
6629
|
+
if (literals.has(index)) continue;
|
|
6630
|
+
if (text.charAt(index) === "\\") index += 1;
|
|
6631
|
+
else if (text.charAt(index) === "`") return index;
|
|
6632
|
+
}
|
|
6633
|
+
return -1;
|
|
6634
|
+
};
|
|
6635
|
+
const findParameterEnd = (start, isInDoubleQuotes) => {
|
|
6636
|
+
let isDoubleQuoted = false;
|
|
6637
|
+
let isSingleQuoted = false;
|
|
6638
|
+
let index = start;
|
|
6639
|
+
while (index < level.limit) {
|
|
6640
|
+
const ch = text.charAt(index);
|
|
6641
|
+
if (literals.has(index)) {
|
|
6642
|
+
index += 1;
|
|
6643
|
+
continue;
|
|
6644
|
+
}
|
|
6645
|
+
if (ch === "\\") {
|
|
6646
|
+
index += 2;
|
|
6647
|
+
continue;
|
|
6648
|
+
}
|
|
6649
|
+
const expansion = readExpansion(index, isInDoubleQuotes || isDoubleQuoted);
|
|
6650
|
+
if (expansion !== null) {
|
|
6651
|
+
if (expansion.isUnterminated) return -1;
|
|
6652
|
+
index = expansion.end;
|
|
6653
|
+
continue;
|
|
6654
|
+
}
|
|
6655
|
+
if (ch === "'" && !isInDoubleQuotes && !isDoubleQuoted) {
|
|
6656
|
+
const close = text.indexOf("'", index + 1);
|
|
6657
|
+
if (close === -1 || close >= level.limit) return -1;
|
|
6658
|
+
index = close + 1;
|
|
6659
|
+
continue;
|
|
6660
|
+
}
|
|
6661
|
+
if (ch === "'" && !isDoubleQuoted) isSingleQuoted = !isSingleQuoted;
|
|
6662
|
+
else if (ch === "\"" && !isSingleQuoted) isDoubleQuoted = !isDoubleQuoted;
|
|
6663
|
+
else if (!isDoubleQuoted && !isSingleQuoted && ch === "}") return index;
|
|
6664
|
+
index += 1;
|
|
6665
|
+
}
|
|
6666
|
+
return -1;
|
|
6667
|
+
};
|
|
6668
|
+
const readRedirect = (start, operator, opensHeredoc) => {
|
|
6669
|
+
let index = start + operator.length;
|
|
6670
|
+
if (operator.endsWith("(")) {
|
|
6671
|
+
hasSubstitution = true;
|
|
6672
|
+
return {
|
|
6673
|
+
target: null,
|
|
6674
|
+
raw: "",
|
|
6675
|
+
...collectExpansionBody(start, index, withLevel(level.limit, ")", () => collectTokens(index).end).result)
|
|
6676
|
+
};
|
|
6677
|
+
}
|
|
6678
|
+
while (index < level.limit) if (text.startsWith("\\\n", index)) index += 2;
|
|
6679
|
+
else if (/\s/u.test(text.charAt(index)) && text.charAt(index) !== "\n") index += 1;
|
|
6680
|
+
else break;
|
|
6681
|
+
const collected = level.substitutions.length;
|
|
6682
|
+
const hadSubstitution = hasSubstitution;
|
|
6683
|
+
const word = readWord(index);
|
|
6684
|
+
if (opensHeredoc) {
|
|
6685
|
+
level.substitutions.splice(collected);
|
|
6686
|
+
hasSubstitution = hadSubstitution;
|
|
6687
|
+
}
|
|
6688
|
+
if (word.raw === "") return {
|
|
6689
|
+
target: null,
|
|
6690
|
+
raw: "",
|
|
6691
|
+
end: index,
|
|
6692
|
+
isUnterminated: false
|
|
6693
|
+
};
|
|
6694
|
+
return {
|
|
6695
|
+
target: word.paren === null ? word.value : word.value.slice(0, word.paren.value),
|
|
6696
|
+
raw: word.paren === null ? word.raw : word.raw.slice(0, word.paren.raw),
|
|
6697
|
+
end: word.end,
|
|
6698
|
+
isUnterminated: word.isUnterminated
|
|
6699
|
+
};
|
|
6700
|
+
};
|
|
6701
|
+
const readHeredocs = (start, heredocs) => {
|
|
6702
|
+
const tokens = [];
|
|
6703
|
+
let bodyStart = start;
|
|
6704
|
+
let end = start;
|
|
6705
|
+
for (const heredoc of heredocs) {
|
|
6706
|
+
const [bodyEnd, next] = findHeredocEnd(text, bodyStart, level.limit, heredoc);
|
|
6707
|
+
const substitutions = heredoc.isQuoted ? [] : collectHeredocSubstitutions(bodyStart, bodyEnd);
|
|
6708
|
+
tokens.push({
|
|
6709
|
+
kind: "heredoc",
|
|
6710
|
+
body: text.slice(bodyStart, bodyEnd),
|
|
6711
|
+
isQuoted: heredoc.isQuoted,
|
|
6712
|
+
opener: heredoc.opener,
|
|
6713
|
+
substitutions,
|
|
6714
|
+
start: bodyStart,
|
|
6715
|
+
end: next
|
|
6716
|
+
});
|
|
6717
|
+
level.substitutions.push(...substitutions);
|
|
6718
|
+
end = next;
|
|
6719
|
+
bodyStart = Math.min(next + 1, level.limit);
|
|
6720
|
+
}
|
|
6721
|
+
return {
|
|
6722
|
+
tokens,
|
|
6723
|
+
end
|
|
6724
|
+
};
|
|
6725
|
+
};
|
|
6726
|
+
const collectHeredocSubstitutions = (start, end) => withLevel(end, null, () => {
|
|
6727
|
+
let index = start;
|
|
6728
|
+
while (index < end) if (literals.has(index)) index += 1;
|
|
6729
|
+
else if (text.charAt(index) === "\\") index += 2;
|
|
6730
|
+
else index = readExpansion(index, true)?.end ?? index + 1;
|
|
6731
|
+
}).substitutions;
|
|
6732
|
+
return {
|
|
6733
|
+
tokens: collectTokens(0).tokens,
|
|
6734
|
+
substitutions: top.substitutions,
|
|
6735
|
+
hasSubstitution,
|
|
6736
|
+
unclosedAt
|
|
6737
|
+
};
|
|
6738
|
+
}
|
|
6739
|
+
function buildLevel(limit, closer) {
|
|
6740
|
+
return {
|
|
6741
|
+
substitutions: [],
|
|
6742
|
+
limit,
|
|
6743
|
+
closer,
|
|
6744
|
+
depth: 0,
|
|
6745
|
+
balance: 0
|
|
6746
|
+
};
|
|
6747
|
+
}
|
|
6748
|
+
const SEPARATORS = [
|
|
6749
|
+
"&&",
|
|
6750
|
+
"||",
|
|
6751
|
+
";",
|
|
6752
|
+
"|",
|
|
6753
|
+
"&",
|
|
6421
6754
|
"\n"
|
|
6422
6755
|
];
|
|
6423
6756
|
const REDIRECTS = [
|
|
@@ -6448,63 +6781,119 @@ function buildWordToken(word, start) {
|
|
|
6448
6781
|
end: word.end
|
|
6449
6782
|
};
|
|
6450
6783
|
}
|
|
6451
|
-
function
|
|
6452
|
-
|
|
6784
|
+
function buildScannedWord(text, start, end, value, paren, isUnterminated) {
|
|
6785
|
+
return {
|
|
6786
|
+
value,
|
|
6787
|
+
raw: text.slice(start, end),
|
|
6788
|
+
end,
|
|
6789
|
+
paren,
|
|
6790
|
+
isUnterminated
|
|
6791
|
+
};
|
|
6792
|
+
}
|
|
6793
|
+
function buildHeredoc(raw, stripsTabs, opener) {
|
|
6794
|
+
let delimiter = "";
|
|
6453
6795
|
let quote = null;
|
|
6454
|
-
let
|
|
6455
|
-
let
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
const
|
|
6459
|
-
if (quote
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6796
|
+
let isQuoted = false;
|
|
6797
|
+
let index = 0;
|
|
6798
|
+
while (index < raw.length) {
|
|
6799
|
+
const ch = raw.charAt(index);
|
|
6800
|
+
const next = raw.charAt(index + 1);
|
|
6801
|
+
if (quote === "$'" && ch === "\\") {
|
|
6802
|
+
const escape = parseANSIEscape(raw, index);
|
|
6803
|
+
delimiter += escape.text;
|
|
6804
|
+
index = escape.end;
|
|
6805
|
+
} else if (quote === "$'" && ch === "'") {
|
|
6806
|
+
quote = null;
|
|
6463
6807
|
index += 1;
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
if (ch === "'" || ch === "\"") {
|
|
6467
|
-
quote = ch;
|
|
6808
|
+
} else if (quote === "$'") {
|
|
6809
|
+
delimiter += ch;
|
|
6468
6810
|
index += 1;
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
value += text.slice(index, index + 2);
|
|
6811
|
+
} else if (quote === null && ch === "$" && (next === "'" || next === "\"")) {
|
|
6812
|
+
quote = next === "'" ? "$'" : "\"";
|
|
6813
|
+
isQuoted = true;
|
|
6473
6814
|
index += 2;
|
|
6474
|
-
|
|
6815
|
+
} else if (quote === "'") {
|
|
6816
|
+
quote = ch === "'" ? null : quote;
|
|
6817
|
+
delimiter += ch === "'" ? "" : ch;
|
|
6818
|
+
index += 1;
|
|
6819
|
+
} else if (ch === quote) {
|
|
6820
|
+
quote = null;
|
|
6821
|
+
index += 1;
|
|
6822
|
+
} else if (ch === "\\" && (quote === null || /[$`"\\\n]/u.test(next))) {
|
|
6823
|
+
isQuoted ||= next !== "\n";
|
|
6824
|
+
delimiter += next === "\n" ? "" : next;
|
|
6825
|
+
index += 2;
|
|
6826
|
+
} else if (quote === null && (ch === "'" || ch === "\"")) {
|
|
6827
|
+
quote = ch;
|
|
6828
|
+
isQuoted = true;
|
|
6829
|
+
index += 1;
|
|
6830
|
+
} else {
|
|
6831
|
+
delimiter += ch;
|
|
6832
|
+
index += 1;
|
|
6475
6833
|
}
|
|
6476
|
-
if (/\s/u.test(ch) || /[;&|<>]/u.test(ch)) break;
|
|
6477
|
-
if (text.startsWith("$(", index) || ch === "`") hasSubstitution = true;
|
|
6478
|
-
if ((ch === "(" || ch === ")") && parenAt === null) parenAt = value.length;
|
|
6479
|
-
value += ch;
|
|
6480
|
-
index += 1;
|
|
6481
6834
|
}
|
|
6482
6835
|
return {
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6836
|
+
delimiter,
|
|
6837
|
+
isQuoted,
|
|
6838
|
+
stripsTabs,
|
|
6839
|
+
opener
|
|
6840
|
+
};
|
|
6841
|
+
}
|
|
6842
|
+
const ANSI_ESCAPES = {
|
|
6843
|
+
a: "\x07",
|
|
6844
|
+
b: "\b",
|
|
6845
|
+
e: "\x1B",
|
|
6846
|
+
E: "\x1B",
|
|
6847
|
+
f: "\f",
|
|
6848
|
+
n: "\n",
|
|
6849
|
+
r: "\r",
|
|
6850
|
+
t: " ",
|
|
6851
|
+
v: "\v",
|
|
6852
|
+
"\\": "\\",
|
|
6853
|
+
"'": "'",
|
|
6854
|
+
"\"": "\"",
|
|
6855
|
+
"?": "?"
|
|
6856
|
+
};
|
|
6857
|
+
function parseANSIEscape(raw, index) {
|
|
6858
|
+
const next = raw.charAt(index + 1);
|
|
6859
|
+
const named = ANSI_ESCAPES[next];
|
|
6860
|
+
const hex = /^x(?<digits>[0-9A-Fa-f]{1,2})/u.exec(raw.slice(index + 1));
|
|
6861
|
+
const octal = /^[0-7]{1,3}/u.exec(raw.slice(index + 1));
|
|
6862
|
+
if (named !== void 0) return {
|
|
6863
|
+
text: named,
|
|
6864
|
+
end: index + 2
|
|
6865
|
+
};
|
|
6866
|
+
if (hex !== null) return {
|
|
6867
|
+
text: String.fromCodePoint(Number.parseInt(hex.groups?.["digits"] ?? "", 16)),
|
|
6868
|
+
end: index + 1 + hex[0].length
|
|
6869
|
+
};
|
|
6870
|
+
if (octal !== null) return {
|
|
6871
|
+
text: String.fromCodePoint(Number.parseInt(octal[0], 8)),
|
|
6872
|
+
end: index + 1 + octal[0].length
|
|
6501
6873
|
};
|
|
6502
6874
|
return {
|
|
6503
|
-
|
|
6504
|
-
end:
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6875
|
+
text: raw.slice(index, index + 2),
|
|
6876
|
+
end: index + 2
|
|
6877
|
+
};
|
|
6878
|
+
}
|
|
6879
|
+
function findHeredocEnd(text, bodyStart, limit, heredoc) {
|
|
6880
|
+
let lineStart = bodyStart;
|
|
6881
|
+
while (lineStart < limit) {
|
|
6882
|
+
let lineEnd = findLineEnd(text, lineStart, limit);
|
|
6883
|
+
let line = text.slice(lineStart, lineEnd);
|
|
6884
|
+
while (!heredoc.isQuoted && /(?<!\\)(?:\\\\)*\\$/u.test(line) && lineEnd < limit) {
|
|
6885
|
+
const nextEnd = findLineEnd(text, lineEnd + 1, limit);
|
|
6886
|
+
line = `${line.slice(0, -1)}${text.slice(lineEnd + 1, nextEnd)}`;
|
|
6887
|
+
lineEnd = nextEnd;
|
|
6888
|
+
}
|
|
6889
|
+
if ((heredoc.stripsTabs ? line.replace(/^\t+/u, "") : line) === heredoc.delimiter) return [lineStart, lineEnd];
|
|
6890
|
+
lineStart = lineEnd + 1;
|
|
6891
|
+
}
|
|
6892
|
+
return [limit, limit];
|
|
6893
|
+
}
|
|
6894
|
+
function findLineEnd(text, start, limit) {
|
|
6895
|
+
const newline = text.indexOf("\n", start);
|
|
6896
|
+
return newline === -1 || newline > limit ? limit : newline;
|
|
6508
6897
|
}
|
|
6509
6898
|
//#endregion
|
|
6510
6899
|
//#region src/shell/split-command-words.ts
|
|
@@ -6530,9 +6919,13 @@ function splitShellCommand(command) {
|
|
|
6530
6919
|
let start = null;
|
|
6531
6920
|
let end = 0;
|
|
6532
6921
|
for (const token of [...scanned.tokens, null]) if (token === null || token.kind === "separator") {
|
|
6533
|
-
if (start !== null) segments.push({
|
|
6922
|
+
if (start !== null) segments.push({
|
|
6923
|
+
text: command.slice(start, end),
|
|
6924
|
+
start,
|
|
6925
|
+
end
|
|
6926
|
+
});
|
|
6534
6927
|
start = null;
|
|
6535
|
-
} else {
|
|
6928
|
+
} else if (token.kind !== "comment" && token.kind !== "heredoc") {
|
|
6536
6929
|
start ??= token.start;
|
|
6537
6930
|
end = token.end;
|
|
6538
6931
|
}
|
|
@@ -6568,52 +6961,110 @@ function collectWriteTargets(text) {
|
|
|
6568
6961
|
return splitShellTokens(text).tokens.flatMap((token) => token.kind === "redirect" && token.target !== null && token.target !== "" && WRITE_REDIRECTS.has(token.operator.replace(/^\d+/u, "")) ? [token.target] : []);
|
|
6569
6962
|
}
|
|
6570
6963
|
//#endregion
|
|
6571
|
-
//#region src/shell/
|
|
6572
|
-
function
|
|
6573
|
-
|
|
6964
|
+
//#region src/shell/collect-shell-scripts.ts
|
|
6965
|
+
function collectShellScripts(command) {
|
|
6966
|
+
const segments = splitShellCommand(command).segments;
|
|
6967
|
+
const tokens = splitShellTokens(command).tokens;
|
|
6968
|
+
return [...segments.flatMap((range, segment) => splitShellTokens(range.text).substitutions.map((body) => ({
|
|
6969
|
+
text: body,
|
|
6970
|
+
segment
|
|
6971
|
+
}))), ...tokens.flatMap((token) => {
|
|
6972
|
+
if (token.kind !== "heredoc") return [];
|
|
6973
|
+
const segment = segments.findIndex((range) => range.start <= token.opener && token.opener < range.end);
|
|
6974
|
+
const range = segments[segment];
|
|
6975
|
+
const descriptor = range === void 0 ? null : findScriptDescriptor(range.text);
|
|
6976
|
+
return (range !== void 0 && descriptor !== null && findDescriptorSource(tokens, range.start, range.end, descriptor) === token.opener && (token.isQuoted || token.substitutions.length === 0) ? [...token.substitutions, token.body] : token.substitutions).map((body) => ({
|
|
6977
|
+
text: body,
|
|
6978
|
+
segment
|
|
6979
|
+
}));
|
|
6980
|
+
})];
|
|
6981
|
+
}
|
|
6982
|
+
function findDescriptorSource(tokens, start, end, descriptor) {
|
|
6983
|
+
const descriptors = /* @__PURE__ */ new Map();
|
|
6984
|
+
for (const token of tokens) {
|
|
6985
|
+
if (token.kind !== "redirect" || token.operator.endsWith("(") || token.start < start || token.start >= end) continue;
|
|
6986
|
+
const number = /^\d*/u.exec(token.operator)?.[0] ?? "";
|
|
6987
|
+
const operator = token.operator.slice(number.length);
|
|
6988
|
+
const implied = operator.startsWith("<") ? "0" : "1";
|
|
6989
|
+
const assigned = number === "" ? implied : String(Number(number));
|
|
6990
|
+
const move = /^(?<source>\d+)(?<close>-?)$/u.exec(token.target ?? "")?.groups;
|
|
6991
|
+
const path = operator === "<" || operator === "<>" ? findPathDescriptor(token.target ?? "") : null;
|
|
6992
|
+
if (operator.endsWith("&") && move !== void 0) {
|
|
6993
|
+
const source = String(Number(move["source"]));
|
|
6994
|
+
descriptors.set(assigned, descriptors.get(source) ?? null);
|
|
6995
|
+
if (move["close"] === "-") descriptors.delete(source);
|
|
6996
|
+
} else if (path === null) descriptors.set(assigned, token.start);
|
|
6997
|
+
else descriptors.set(assigned, descriptors.get(path) ?? null);
|
|
6998
|
+
}
|
|
6999
|
+
return descriptors.get(descriptor) ?? null;
|
|
7000
|
+
}
|
|
7001
|
+
const STANDARD_PATHS = /* @__PURE__ */ new Map([
|
|
7002
|
+
["/dev/stdin", "0"],
|
|
7003
|
+
["/dev/stdout", "1"],
|
|
7004
|
+
["/dev/stderr", "2"]
|
|
7005
|
+
]);
|
|
7006
|
+
function findPathDescriptor(path) {
|
|
7007
|
+
const descriptor = /^\/(?:dev|proc\/self)\/fd\/(?<number>\d+)$/u.exec(path)?.groups?.["number"];
|
|
7008
|
+
if (descriptor !== void 0) return String(Number(descriptor));
|
|
7009
|
+
return STANDARD_PATHS.get(path) ?? null;
|
|
7010
|
+
}
|
|
7011
|
+
const SHELLS = /* @__PURE__ */ new Set([
|
|
7012
|
+
"bash",
|
|
7013
|
+
"sh",
|
|
7014
|
+
"zsh",
|
|
7015
|
+
"dash",
|
|
7016
|
+
"ksh"
|
|
7017
|
+
]);
|
|
7018
|
+
const VALUE_OPTIONS = /* @__PURE__ */ new Set([
|
|
7019
|
+
"--rcfile",
|
|
7020
|
+
"--init-file",
|
|
7021
|
+
"--emulate"
|
|
7022
|
+
]);
|
|
7023
|
+
const VALUE_OPTION_GROUP = /^[-+][^-]*[oO]$/u;
|
|
7024
|
+
function findScriptDescriptor(segment) {
|
|
7025
|
+
const [program = "", ...args] = splitCommandWords(segment);
|
|
7026
|
+
if (!SHELLS.has(basename(program))) return null;
|
|
7027
|
+
let readsInput = false;
|
|
6574
7028
|
let index = 0;
|
|
6575
|
-
while (index < args.length
|
|
6576
|
-
const
|
|
6577
|
-
if (
|
|
6578
|
-
|
|
7029
|
+
while (index < args.length) {
|
|
7030
|
+
const arg = args[index] ?? "";
|
|
7031
|
+
if (/^-[^-]*c/u.test(arg)) return null;
|
|
7032
|
+
if (arg === "--" || arg === "-") {
|
|
7033
|
+
const script = args[index + 1];
|
|
7034
|
+
return readsInput || script === void 0 ? "0" : findPathDescriptor(script);
|
|
7035
|
+
}
|
|
7036
|
+
if (!/^[-+]/u.test(arg)) return readsInput ? "0" : findPathDescriptor(arg);
|
|
7037
|
+
readsInput ||= /^-[^-]*s/u.test(arg);
|
|
7038
|
+
index += VALUE_OPTIONS.has(arg) || VALUE_OPTION_GROUP.test(arg) ? 2 : 1;
|
|
6579
7039
|
}
|
|
6580
|
-
return
|
|
6581
|
-
directory: gitDirectory,
|
|
6582
|
-
args: args.slice(index)
|
|
6583
|
-
};
|
|
7040
|
+
return "0";
|
|
6584
7041
|
}
|
|
6585
7042
|
//#endregion
|
|
6586
|
-
//#region src/
|
|
6587
|
-
function
|
|
6588
|
-
const
|
|
6589
|
-
|
|
6590
|
-
const
|
|
6591
|
-
|
|
6592
|
-
...action.tool.startsWith("mcp__") ? collectToolFindings(action, scope) : [],
|
|
6593
|
-
...action.tool === "Bash" && typeof command === "string" ? collectCommandFindings(command, action.cwd, scope, scratchPaths) : []
|
|
6594
|
-
];
|
|
6595
|
-
return findings.filter((finding, index) => findings.findIndex((other) => other.kind === finding.kind && other.target === finding.target) === index);
|
|
7043
|
+
//#region src/git/to-repository-slug.ts
|
|
7044
|
+
function toRepositorySlug(reference) {
|
|
7045
|
+
const match = /^(?:[a-z+]+:\/\/)?(?:[^@/]+@)?(?<host>[^/:]+)[:/](?<owner>[^/]+)\/(?<name>[^/]+?)(?:\.git)?\/?$/iu.exec(reference);
|
|
7046
|
+
if (match?.groups === void 0) return null;
|
|
7047
|
+
const parts = match.groups;
|
|
7048
|
+
return `${parts["host"] ?? ""}/${parts["owner"] ?? ""}/${parts["name"] ?? ""}`.toLowerCase();
|
|
6596
7049
|
}
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
7050
|
+
//#endregion
|
|
7051
|
+
//#region src/containment/is-in-scope.ts
|
|
7052
|
+
function isInScope(path, scope, scratchPaths) {
|
|
7053
|
+
if (scratchPaths.some((entry) => isUnder$2(path, entry)) || scope.pathGlobs.some((glob) => matchesGlob(path, glob))) return true;
|
|
7054
|
+
return scope.worktrees.some((worktree) => {
|
|
7055
|
+
const isNested = isUnder$2(path, join(worktree, ".worktrees"));
|
|
7056
|
+
const isOtherOwned = scope.worktrees.some((other) => other !== worktree && isUnder$2(path, other));
|
|
7057
|
+
return isUnder$2(path, worktree) && (!isNested || isOtherOwned);
|
|
7058
|
+
});
|
|
7059
|
+
}
|
|
7060
|
+
function isUnder$2(path, root) {
|
|
7061
|
+
return path === root || path.startsWith(`${root}/`);
|
|
7062
|
+
}
|
|
7063
|
+
//#endregion
|
|
7064
|
+
//#region src/containment/collect-target-findings.ts
|
|
6603
7065
|
function collectTargetFindings(directory, target, scope, scratchPaths) {
|
|
6604
7066
|
const path = resolveShellPath(directory, target, scope.home);
|
|
6605
|
-
|
|
6606
|
-
}
|
|
6607
|
-
const CREDENTIAL_PATHS = [
|
|
6608
|
-
".ssh",
|
|
6609
|
-
".aws",
|
|
6610
|
-
".gnupg",
|
|
6611
|
-
".gitconfig",
|
|
6612
|
-
".netrc",
|
|
6613
|
-
".npmrc",
|
|
6614
|
-
".config/gh"
|
|
6615
|
-
];
|
|
6616
|
-
function collectPathFindings(path, scope, scratchPaths) {
|
|
7067
|
+
if (path === null) return [];
|
|
6617
7068
|
if (CREDENTIAL_PATHS.some((entry) => isUnder$1(path, join(scope.home, entry)))) return [{
|
|
6618
7069
|
kind: "credential",
|
|
6619
7070
|
target: path
|
|
@@ -6624,213 +7075,46 @@ function collectPathFindings(path, scope, scratchPaths) {
|
|
|
6624
7075
|
grant: { kind: "path" }
|
|
6625
7076
|
}];
|
|
6626
7077
|
}
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
|
|
6638
|
-
grant: {
|
|
6639
|
-
kind: "pull-request",
|
|
6640
|
-
number
|
|
6641
|
-
}
|
|
6642
|
-
}] : [{
|
|
6643
|
-
kind: "remote-write",
|
|
6644
|
-
target: action.tool
|
|
6645
|
-
}];
|
|
7078
|
+
const CREDENTIAL_PATHS = [
|
|
7079
|
+
".ssh",
|
|
7080
|
+
".aws",
|
|
7081
|
+
".gnupg",
|
|
7082
|
+
".gitconfig",
|
|
7083
|
+
".netrc",
|
|
7084
|
+
".npmrc",
|
|
7085
|
+
".config/gh"
|
|
7086
|
+
];
|
|
7087
|
+
function isUnder$1(path, root) {
|
|
7088
|
+
return path === root || path.startsWith(`${root}/`);
|
|
6646
7089
|
}
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
"
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
"chown"
|
|
6654
|
-
]);
|
|
6655
|
-
const DESTINATION_WRITERS = /* @__PURE__ */ new Set([
|
|
6656
|
-
"cp",
|
|
6657
|
-
"mv",
|
|
6658
|
-
"rsync",
|
|
6659
|
-
"ln"
|
|
6660
|
-
]);
|
|
6661
|
-
function collectCommandFindings(command, cwd, scope, scratchPaths) {
|
|
6662
|
-
const findings = [];
|
|
6663
|
-
const entries = collectShellCommands(command, cwd, scope.home);
|
|
6664
|
-
const piped = collectPipedSegments(command);
|
|
6665
|
-
const segments = splitShellCommand(command).segments;
|
|
6666
|
-
for (const [index, entry] of entries.entries()) {
|
|
6667
|
-
const [name, ...args] = entry.words;
|
|
6668
|
-
const fixed = findFixedXargsCommand(entry.words, segments[index]?.text ?? "");
|
|
6669
|
-
if (fixed !== null) findings.push(...collectProgramFindings(fixed.name, fixed.args, entry.directory, cwd, scope, scratchPaths));
|
|
6670
|
-
else if (isBranchSweep(entries.slice(0, index + 1), piped.slice(0, index + 1))) findings.push({
|
|
6671
|
-
kind: "discard",
|
|
6672
|
-
target: "git branch delete by exclusion"
|
|
6673
|
-
});
|
|
6674
|
-
for (const target of entry.writeTargets) findings.push(...collectTargetFindings(entry.directory, target, scope, scratchPaths));
|
|
6675
|
-
if (name !== void 0 && name !== "cd") findings.push(...collectProgramFindings(name, args, entry.directory, cwd, scope, scratchPaths));
|
|
6676
|
-
}
|
|
6677
|
-
return findings;
|
|
7090
|
+
//#endregion
|
|
7091
|
+
//#region src/containment/is-owned-pull-request.ts
|
|
7092
|
+
function isOwnedPullRequest(number, repo, scope) {
|
|
7093
|
+
const wanted = repo?.toLowerCase().replace(/\.git$/u, "");
|
|
7094
|
+
const slugs = new Set(scope.remotes.map((remote) => toRepositorySlug(remote.url)));
|
|
7095
|
+
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}`)));
|
|
6678
7096
|
}
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
let start = 1;
|
|
6701
|
-
while (words[start]?.startsWith("-") === true) {
|
|
6702
|
-
const word = words[start] ?? "";
|
|
6703
|
-
const replace = XARGS_REPLACE.exec(word)?.groups;
|
|
6704
|
-
if (word === "--") {
|
|
6705
|
-
start += 1;
|
|
6706
|
-
break;
|
|
6707
|
-
}
|
|
6708
|
-
if (word === "-I") {
|
|
6709
|
-
token = words[start + 1] ?? null;
|
|
6710
|
-
start += 2;
|
|
6711
|
-
} else if (replace !== void 0) {
|
|
6712
|
-
const short = replace["short"];
|
|
6713
|
-
token = replace["attached"] ?? replace["long"] ?? (short === void 0 || short === "" ? "{}" : short);
|
|
6714
|
-
start += 1;
|
|
6715
|
-
} else if (word.startsWith("-J")) {
|
|
6716
|
-
token = null;
|
|
6717
|
-
start += word === "-J" ? 2 : 1;
|
|
6718
|
-
} else if (XARGS_VALUE_OPTION.test(word)) start += 2;
|
|
6719
|
-
else if (XARGS_FLAG.test(word) || XARGS_ATTACHED_VALUE.test(word)) start += 1;
|
|
6720
|
-
else return null;
|
|
6721
|
-
}
|
|
6722
|
-
const [name, ...args] = words.slice(start);
|
|
6723
|
-
if (token === null || token === "" || name === void 0 || [name, ...args].some((word) => word.includes(token))) return null;
|
|
6724
|
-
return {
|
|
6725
|
-
name,
|
|
6726
|
-
args
|
|
6727
|
-
};
|
|
6728
|
-
}
|
|
6729
|
-
const XARGS_VALUE_OPTIONS = /* @__PURE__ */ new Set([
|
|
6730
|
-
"-I",
|
|
6731
|
-
"-J",
|
|
6732
|
-
"-L",
|
|
6733
|
-
"-n",
|
|
6734
|
-
"-P",
|
|
6735
|
-
"-s",
|
|
6736
|
-
"-d",
|
|
6737
|
-
"-E"
|
|
6738
|
-
]);
|
|
6739
|
-
function isBranchSweep(entries, piped) {
|
|
6740
|
-
const deleter = entries.at(-1)?.words ?? [];
|
|
6741
|
-
if (deleter[0] !== "xargs" || piped.at(-1) !== true) return false;
|
|
6742
|
-
let start = 1;
|
|
6743
|
-
while (deleter[start]?.startsWith("-") === true) {
|
|
6744
|
-
if (/^(?:-a|--arg-file)/u.test(deleter[start] ?? "")) return false;
|
|
6745
|
-
start += XARGS_VALUE_OPTIONS.has(deleter[start] ?? "") ? 2 : 1;
|
|
6746
|
-
}
|
|
6747
|
-
const [program, subcommand, ...rest] = deleter.slice(start);
|
|
6748
|
-
if (program !== "git" || subcommand !== "branch" || !rest.some((word) => /^-(?:d|D|-delete)$/u.test(word))) return false;
|
|
6749
|
-
let first = entries.length - 2;
|
|
6750
|
-
while (first > 0 && piped[first] === true) first -= 1;
|
|
6751
|
-
const [listing, ...filters] = entries.slice(first, -1).map((entry) => entry.words);
|
|
6752
|
-
return listing !== void 0 && isWholeBranchListing(listing) && filters.every((filter) => isExcludingFilter(filter));
|
|
6753
|
-
}
|
|
6754
|
-
function isWholeBranchListing(words) {
|
|
6755
|
-
const [program, subcommand, ...rest] = words;
|
|
6756
|
-
if (program !== "git") return false;
|
|
6757
|
-
if (subcommand === "branch") return rest.every((word) => /^(?:-a|-r|-v|-vv|--all|--remotes|--no-color|--format=.*)$/u.test(word));
|
|
6758
|
-
if (subcommand !== "for-each-ref") return false;
|
|
6759
|
-
return rest.filter((_word, index) => !/^--(?:format|sort)$/u.test(rest[index - 1] ?? "")).every((word) => /^--(?:format|sort)(?:=.*)?$/u.test(word) || /^refs\/heads\/?$/u.test(word));
|
|
6760
|
-
}
|
|
6761
|
-
const NAME_EDITORS = /* @__PURE__ */ new Set([
|
|
6762
|
-
"tr",
|
|
6763
|
-
"cut",
|
|
6764
|
-
"sort"
|
|
6765
|
-
]);
|
|
6766
|
-
function isExcludingFilter(words) {
|
|
6767
|
-
const [program, ...args] = words;
|
|
6768
|
-
if (program === "grep" || program === "egrep") return args.some((word) => /^-[a-zA-Z]*v[a-zA-Z]*$|^--invert-match$/u.test(word));
|
|
6769
|
-
if (program === "sed") return isSubstitutionOnly(args);
|
|
6770
|
-
if (program === "uniq") return args.every((word) => !/^-[a-zA-Z]*[dDu]/u.test(word));
|
|
6771
|
-
return program !== void 0 && NAME_EDITORS.has(program);
|
|
6772
|
-
}
|
|
6773
|
-
function isSubstitutionOnly(args) {
|
|
6774
|
-
const scripts = args.filter((word) => word !== "-e" && word !== "-E" && word !== "-r");
|
|
6775
|
-
return scripts.length > 0 && scripts.every((script) => /^s(?<delimiter>[^\w\s;])(?:(?!\k<delimiter>).)*\k<delimiter>(?:(?!\k<delimiter>).)*\k<delimiter>g?$/u.test(script));
|
|
6776
|
-
}
|
|
6777
|
-
function collectProgramFindings(name, args, directory, cwd, scope, scratchPaths) {
|
|
6778
|
-
const resolve = (path) => collectTargetFindings(directory, path, scope, scratchPaths);
|
|
6779
|
-
const operands = args.filter((word) => !word.startsWith("-"));
|
|
6780
|
-
if (name === "git") return collectGitFindings(args, directory, cwd, scope, scratchPaths);
|
|
6781
|
-
if (name === "gh") return collectGhFindings(args, scope);
|
|
6782
|
-
if (PATH_WRITERS.has(name)) return operands.flatMap((path) => resolve(path));
|
|
6783
|
-
if (DESTINATION_WRITERS.has(name)) return collectCopyFindings(name, args, operands).flatMap((path) => resolve(path));
|
|
6784
|
-
if (name === "find" && args.some((word) => word === "-delete" || word === "-exec")) return operands.slice(0, 1).flatMap((path) => resolve(path));
|
|
6785
|
-
if ((name === "perl" || name === "sed") && args.some((word) => /^-\w*i/u.test(word))) return collectInPlaceFiles(args).flatMap((path) => resolve(path));
|
|
6786
|
-
if (name === "ssh-keygen") return collectKeygenFiles(args).flatMap((path) => resolve(path));
|
|
6787
|
-
if (name === "ssh") {
|
|
6788
|
-
const logIndex = args.indexOf("-E");
|
|
6789
|
-
const log = logIndex === -1 ? void 0 : args[logIndex + 1];
|
|
6790
|
-
return [...log === void 0 ? [] : resolve(log), ...collectSSHFindings(args)];
|
|
6791
|
-
}
|
|
6792
|
-
if (name === "scp") {
|
|
6793
|
-
const destination = collectSSHOperands(args).at(-1);
|
|
6794
|
-
if (destination === void 0 || /^[^/]+:/u.test(destination)) return destination === void 0 ? [] : [{
|
|
6795
|
-
kind: "remote-write",
|
|
6796
|
-
target: "scp"
|
|
6797
|
-
}];
|
|
6798
|
-
return resolve(destination);
|
|
6799
|
-
}
|
|
6800
|
-
if (name === "curl") return collectCurlFindings(args);
|
|
6801
|
-
const launcher = [name, ...args].findIndex((word) => word.includes("auto-mode"));
|
|
6802
|
-
if (launcher !== -1 && [name, ...args].slice(launcher + 1).includes("record")) return [{
|
|
6803
|
-
kind: "credential",
|
|
6804
|
-
target: "auto-mode record"
|
|
6805
|
-
}];
|
|
6806
|
-
const finding = findProgramFinding(name, args);
|
|
6807
|
-
return finding === null ? [] : [finding];
|
|
6808
|
-
}
|
|
6809
|
-
function collectCopyFindings(name, args, operands) {
|
|
6810
|
-
const optionIndex = args.findIndex((word) => word === "-t" || word === "--target-directory");
|
|
6811
|
-
let targetDirectory = args.find((word) => word.startsWith("--target-directory="))?.slice(19);
|
|
6812
|
-
if (targetDirectory === void 0 && optionIndex !== -1) targetDirectory = args[optionIndex + 1];
|
|
6813
|
-
if (targetDirectory !== void 0) {
|
|
6814
|
-
const sources = operands.filter((word) => word !== targetDirectory);
|
|
6815
|
-
return [targetDirectory, ...name === "mv" ? sources : []];
|
|
6816
|
-
}
|
|
6817
|
-
if (operands.length < 2) return [];
|
|
6818
|
-
return [operands.at(-1) ?? "", ...name === "mv" ? operands.slice(0, -1) : []];
|
|
6819
|
-
}
|
|
6820
|
-
const KEYGEN_READ_OPTIONS = /* @__PURE__ */ new Set([
|
|
6821
|
-
"-l",
|
|
6822
|
-
"-y",
|
|
6823
|
-
"-F",
|
|
6824
|
-
"-B",
|
|
6825
|
-
"-R"
|
|
6826
|
-
]);
|
|
6827
|
-
function collectKeygenFiles(args) {
|
|
6828
|
-
if (args.some((word) => KEYGEN_READ_OPTIONS.has(word))) return [];
|
|
6829
|
-
const index = args.indexOf("-f");
|
|
6830
|
-
const file = index === -1 ? void 0 : args[index + 1];
|
|
6831
|
-
return file === void 0 ? [] : [file];
|
|
6832
|
-
}
|
|
6833
|
-
const SSH_VALUE_OPTIONS = /* @__PURE__ */ new Set([
|
|
7097
|
+
//#endregion
|
|
7098
|
+
//#region src/containment/program-table.ts
|
|
7099
|
+
const PATH_OPERANDS = { writes: [{ operands: ["all"] }] };
|
|
7100
|
+
const TARGET_DIRECTORY = ["-t", "--target-directory"];
|
|
7101
|
+
const COPY = {
|
|
7102
|
+
valueOptions: TARGET_DIRECTORY,
|
|
7103
|
+
writes: [{
|
|
7104
|
+
options: TARGET_DIRECTORY,
|
|
7105
|
+
operands: ["destination"]
|
|
7106
|
+
}]
|
|
7107
|
+
};
|
|
7108
|
+
const IN_PLACE_SCRIPT = /^-\w*e$/u;
|
|
7109
|
+
const IN_PLACE_EDIT = {
|
|
7110
|
+
valueOptions: [IN_PLACE_SCRIPT],
|
|
7111
|
+
writes: [{
|
|
7112
|
+
flags: [/^-\w*i/u],
|
|
7113
|
+
operands: ["after-script"],
|
|
7114
|
+
scriptOptions: [IN_PLACE_SCRIPT]
|
|
7115
|
+
}]
|
|
7116
|
+
};
|
|
7117
|
+
const SSH_VALUE_OPTIONS = [
|
|
6834
7118
|
"-b",
|
|
6835
7119
|
"-c",
|
|
6836
7120
|
"-D",
|
|
@@ -6840,9 +7124,7 @@ const SSH_VALUE_OPTIONS = /* @__PURE__ */ new Set([
|
|
|
6840
7124
|
"-J",
|
|
6841
7125
|
"-l",
|
|
6842
7126
|
"-L",
|
|
6843
|
-
"-m"
|
|
6844
|
-
]);
|
|
6845
|
-
const SSH_MORE_VALUE_OPTIONS = /* @__PURE__ */ new Set([
|
|
7127
|
+
"-m",
|
|
6846
7128
|
"-o",
|
|
6847
7129
|
"-p",
|
|
6848
7130
|
"-P",
|
|
@@ -6851,17 +7133,20 @@ const SSH_MORE_VALUE_OPTIONS = /* @__PURE__ */ new Set([
|
|
|
6851
7133
|
"-S",
|
|
6852
7134
|
"-W",
|
|
6853
7135
|
"-w"
|
|
6854
|
-
]
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
7136
|
+
];
|
|
7137
|
+
const SCP_VALUE_OPTIONS = [
|
|
7138
|
+
"-c",
|
|
7139
|
+
"-D",
|
|
7140
|
+
"-F",
|
|
7141
|
+
"-i",
|
|
7142
|
+
"-J",
|
|
7143
|
+
"-l",
|
|
7144
|
+
"-o",
|
|
7145
|
+
"-P",
|
|
7146
|
+
"-S",
|
|
7147
|
+
"-X"
|
|
7148
|
+
];
|
|
7149
|
+
const SSH_READS = [
|
|
6865
7150
|
"cat",
|
|
6866
7151
|
"df",
|
|
6867
7152
|
"du",
|
|
@@ -6876,336 +7161,880 @@ const SSH_READS = /* @__PURE__ */ new Set([
|
|
|
6876
7161
|
"uname",
|
|
6877
7162
|
"uptime",
|
|
6878
7163
|
"whoami"
|
|
6879
|
-
]
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
7164
|
+
];
|
|
7165
|
+
const CURL_UPLOAD = {
|
|
7166
|
+
kind: "remote-write",
|
|
7167
|
+
target: "curl upload"
|
|
7168
|
+
};
|
|
7169
|
+
const CREDENTIAL_WRITE = { writes: [{ finding: { kind: "credential" } }] };
|
|
7170
|
+
const DOCKER_PRUNE = { writes: [{ finding: {
|
|
7171
|
+
kind: "prune",
|
|
7172
|
+
quote: "call"
|
|
7173
|
+
} }] };
|
|
7174
|
+
const IAM_WRITE = /^(?:add|attach|change|create|deactivate|delete|detach|enable|put|remove|reset|set|tag|untag|update|upload)-/u;
|
|
7175
|
+
const KUBECTL_WRITE = { writes: [{ finding: {
|
|
7176
|
+
kind: "remote-write",
|
|
7177
|
+
target: "kubectl write"
|
|
7178
|
+
} }] };
|
|
7179
|
+
const TERRAFORM_WRITE = { writes: [{ finding: {
|
|
7180
|
+
kind: "remote-write",
|
|
7181
|
+
target: "terraform write"
|
|
7182
|
+
} }] };
|
|
7183
|
+
const PRODUCTION_DEPLOY = {
|
|
7184
|
+
words: [/\bdeploy\b/u, /prod/u],
|
|
7185
|
+
finding: {
|
|
6886
7186
|
kind: "remote-write",
|
|
6887
|
-
target: "
|
|
6888
|
-
}
|
|
7187
|
+
target: "production deploy"
|
|
7188
|
+
}
|
|
7189
|
+
};
|
|
7190
|
+
const CHECKOUT_WRITE = { writes: [{ directory: true }] };
|
|
7191
|
+
const REFSPEC_WRITE = {
|
|
7192
|
+
operands: ["after-first"],
|
|
7193
|
+
role: "refspec"
|
|
7194
|
+
};
|
|
7195
|
+
const GIT = {
|
|
7196
|
+
directoryOptions: ["-C"],
|
|
7197
|
+
valueOptions: ["-c"],
|
|
7198
|
+
subcommands: {
|
|
7199
|
+
clean: CHECKOUT_WRITE,
|
|
7200
|
+
checkout: CHECKOUT_WRITE,
|
|
7201
|
+
switch: CHECKOUT_WRITE,
|
|
7202
|
+
merge: CHECKOUT_WRITE,
|
|
7203
|
+
commit: CHECKOUT_WRITE,
|
|
7204
|
+
rm: CHECKOUT_WRITE,
|
|
7205
|
+
stash: CHECKOUT_WRITE,
|
|
7206
|
+
reset: { writes: [{ directory: true }, {
|
|
7207
|
+
flags: ["--hard"],
|
|
7208
|
+
operands: ["first"],
|
|
7209
|
+
role: "commit",
|
|
7210
|
+
finding: {
|
|
7211
|
+
kind: "discard",
|
|
7212
|
+
target: "git reset --hard"
|
|
7213
|
+
}
|
|
7214
|
+
}] },
|
|
7215
|
+
push: { writes: [
|
|
7216
|
+
{ directory: true },
|
|
7217
|
+
{
|
|
7218
|
+
flags: ["--all", "--mirror"],
|
|
7219
|
+
finding: {
|
|
7220
|
+
kind: "branch",
|
|
7221
|
+
target: "--all"
|
|
7222
|
+
}
|
|
7223
|
+
},
|
|
7224
|
+
{
|
|
7225
|
+
operands: ["first"],
|
|
7226
|
+
role: "remote"
|
|
7227
|
+
},
|
|
7228
|
+
{
|
|
7229
|
+
...REFSPEC_WRITE,
|
|
7230
|
+
readFlags: ["--tags"],
|
|
7231
|
+
whenNone: "HEAD"
|
|
7232
|
+
},
|
|
7233
|
+
{
|
|
7234
|
+
...REFSPEC_WRITE,
|
|
7235
|
+
flags: ["--tags"]
|
|
7236
|
+
}
|
|
7237
|
+
] },
|
|
7238
|
+
worktree: { subcommands: { remove: { writes: [{ operands: ["first"] }] } } },
|
|
7239
|
+
branch: { writes: [{
|
|
7240
|
+
flags: [
|
|
7241
|
+
"-d",
|
|
7242
|
+
"-D",
|
|
7243
|
+
"--delete"
|
|
7244
|
+
],
|
|
7245
|
+
operands: ["all"],
|
|
7246
|
+
role: "branch"
|
|
7247
|
+
}] },
|
|
7248
|
+
config: { writes: [{
|
|
7249
|
+
flags: ["--global"],
|
|
7250
|
+
readFlags: [
|
|
7251
|
+
"--get",
|
|
7252
|
+
"--get-all",
|
|
7253
|
+
"--get-regexp",
|
|
7254
|
+
"--list",
|
|
7255
|
+
"-l"
|
|
7256
|
+
],
|
|
7257
|
+
finding: {
|
|
7258
|
+
kind: "credential",
|
|
7259
|
+
quote: "operand"
|
|
7260
|
+
}
|
|
7261
|
+
}] }
|
|
7262
|
+
}
|
|
7263
|
+
};
|
|
7264
|
+
const GH_REPO_OPTIONS = ["-R", "--repo"];
|
|
7265
|
+
const PULL_REQUEST_WRITE = {
|
|
7266
|
+
operands: ["first"],
|
|
7267
|
+
role: "pull-request",
|
|
7268
|
+
repoOptions: GH_REPO_OPTIONS,
|
|
7269
|
+
finding: { kind: "remote-write" }
|
|
7270
|
+
};
|
|
7271
|
+
const GH_PULL_REQUEST = { subcommands: {
|
|
7272
|
+
comment: {
|
|
7273
|
+
valueOptions: [
|
|
7274
|
+
...GH_REPO_OPTIONS,
|
|
7275
|
+
"-b",
|
|
7276
|
+
"--body",
|
|
7277
|
+
"-F",
|
|
7278
|
+
"--body-file",
|
|
7279
|
+
"--attach"
|
|
7280
|
+
],
|
|
7281
|
+
writes: [PULL_REQUEST_WRITE]
|
|
7282
|
+
},
|
|
7283
|
+
merge: {
|
|
7284
|
+
valueOptions: [
|
|
7285
|
+
...GH_REPO_OPTIONS,
|
|
7286
|
+
"-A",
|
|
7287
|
+
"--author-email",
|
|
7288
|
+
"-b",
|
|
7289
|
+
"--body",
|
|
7290
|
+
"-F",
|
|
7291
|
+
"--body-file",
|
|
7292
|
+
"--match-head-commit",
|
|
7293
|
+
"-t",
|
|
7294
|
+
"--subject"
|
|
7295
|
+
],
|
|
7296
|
+
writes: [{
|
|
7297
|
+
...PULL_REQUEST_WRITE,
|
|
7298
|
+
role: "pull-request-merge",
|
|
7299
|
+
ungrantFlags: [/^--admin(?:=|$)/u]
|
|
7300
|
+
}]
|
|
7301
|
+
},
|
|
7302
|
+
edit: {
|
|
7303
|
+
valueOptions: [
|
|
7304
|
+
...GH_REPO_OPTIONS,
|
|
7305
|
+
"--add-assignee",
|
|
7306
|
+
"--add-label",
|
|
7307
|
+
"--add-project",
|
|
7308
|
+
"--add-reviewer",
|
|
7309
|
+
"--attach",
|
|
7310
|
+
"-B",
|
|
7311
|
+
"--base",
|
|
7312
|
+
"-b",
|
|
7313
|
+
"--body",
|
|
7314
|
+
"-F",
|
|
7315
|
+
"--body-file",
|
|
7316
|
+
"-m",
|
|
7317
|
+
"--milestone",
|
|
7318
|
+
"--remove-assignee",
|
|
7319
|
+
"--remove-label",
|
|
7320
|
+
"--remove-project",
|
|
7321
|
+
"--remove-reviewer",
|
|
7322
|
+
"-t",
|
|
7323
|
+
"--title"
|
|
7324
|
+
],
|
|
7325
|
+
writes: [PULL_REQUEST_WRITE]
|
|
7326
|
+
},
|
|
7327
|
+
close: {
|
|
7328
|
+
valueOptions: [
|
|
7329
|
+
...GH_REPO_OPTIONS,
|
|
7330
|
+
"-c",
|
|
7331
|
+
"--comment"
|
|
7332
|
+
],
|
|
7333
|
+
writes: [PULL_REQUEST_WRITE]
|
|
7334
|
+
},
|
|
7335
|
+
review: {
|
|
7336
|
+
valueOptions: [
|
|
7337
|
+
...GH_REPO_OPTIONS,
|
|
7338
|
+
"-b",
|
|
7339
|
+
"--body",
|
|
7340
|
+
"-F",
|
|
7341
|
+
"--body-file"
|
|
7342
|
+
],
|
|
7343
|
+
writes: [PULL_REQUEST_WRITE]
|
|
7344
|
+
},
|
|
7345
|
+
ready: {
|
|
7346
|
+
valueOptions: GH_REPO_OPTIONS,
|
|
7347
|
+
writes: [PULL_REQUEST_WRITE]
|
|
7348
|
+
},
|
|
7349
|
+
reopen: {
|
|
7350
|
+
valueOptions: [
|
|
7351
|
+
...GH_REPO_OPTIONS,
|
|
7352
|
+
"-c",
|
|
7353
|
+
"--comment"
|
|
7354
|
+
],
|
|
7355
|
+
writes: [PULL_REQUEST_WRITE]
|
|
7356
|
+
}
|
|
7357
|
+
} };
|
|
7358
|
+
const GH_API_FIELDS = [
|
|
7359
|
+
"-f",
|
|
7360
|
+
"-F",
|
|
7361
|
+
"--field",
|
|
7362
|
+
"--raw-field",
|
|
7363
|
+
"--input"
|
|
7364
|
+
];
|
|
7365
|
+
const FORGE_WRITE = {
|
|
7366
|
+
operands: ["first"],
|
|
7367
|
+
role: "forge-path",
|
|
7368
|
+
credentialPattern: /secret|collaborator|protection|key|permission/u,
|
|
7369
|
+
finding: { kind: "remote-write" }
|
|
7370
|
+
};
|
|
7371
|
+
const GH_API = {
|
|
7372
|
+
valueOptions: [
|
|
7373
|
+
...GH_API_FIELDS,
|
|
7374
|
+
"-X",
|
|
7375
|
+
"--method",
|
|
7376
|
+
"-H",
|
|
7377
|
+
"--header",
|
|
7378
|
+
"-q",
|
|
7379
|
+
"--jq",
|
|
7380
|
+
"-t",
|
|
7381
|
+
"--template",
|
|
7382
|
+
"--hostname",
|
|
7383
|
+
"-p",
|
|
7384
|
+
"--preview",
|
|
7385
|
+
"--cache"
|
|
7386
|
+
],
|
|
7387
|
+
subcommands: { graphql: { writes: [{
|
|
7388
|
+
words: [/\bmutation\b/u],
|
|
7389
|
+
finding: {
|
|
7390
|
+
kind: "remote-write",
|
|
7391
|
+
target: "gh api graphql mutation"
|
|
7392
|
+
}
|
|
7393
|
+
}] } },
|
|
7394
|
+
writes: [{
|
|
7395
|
+
...FORGE_WRITE,
|
|
7396
|
+
optionValue: {
|
|
7397
|
+
options: ["-X", "--method"],
|
|
7398
|
+
value: /^(?:POST|PUT|PATCH|DELETE)$/iu
|
|
7399
|
+
}
|
|
7400
|
+
}, {
|
|
7401
|
+
...FORGE_WRITE,
|
|
7402
|
+
flags: GH_API_FIELDS,
|
|
7403
|
+
readFlags: ["-X", "--method"]
|
|
7404
|
+
}]
|
|
7405
|
+
};
|
|
7406
|
+
const GH_READS = {
|
|
7407
|
+
list: {},
|
|
7408
|
+
view: {},
|
|
7409
|
+
clone: {},
|
|
7410
|
+
status: {},
|
|
7411
|
+
checks: {},
|
|
7412
|
+
diff: {}
|
|
7413
|
+
};
|
|
7414
|
+
const GH_REMOTE_GROUP = {
|
|
7415
|
+
subcommands: GH_READS,
|
|
7416
|
+
otherSubcommand: { entry: { writes: [{ finding: { kind: "remote-write" } }] } }
|
|
7417
|
+
};
|
|
7418
|
+
const GH_CREDENTIAL_GROUP = {
|
|
7419
|
+
subcommands: GH_READS,
|
|
7420
|
+
otherSubcommand: { entry: CREDENTIAL_WRITE }
|
|
7421
|
+
};
|
|
7422
|
+
const PROGRAM_TABLE = {
|
|
7423
|
+
git: GIT,
|
|
7424
|
+
gh: { subcommands: {
|
|
7425
|
+
pr: GH_PULL_REQUEST,
|
|
7426
|
+
issue: GH_REMOTE_GROUP,
|
|
7427
|
+
api: GH_API,
|
|
7428
|
+
auth: { subcommands: {
|
|
7429
|
+
refresh: CREDENTIAL_WRITE,
|
|
7430
|
+
login: CREDENTIAL_WRITE,
|
|
7431
|
+
logout: CREDENTIAL_WRITE
|
|
7432
|
+
} },
|
|
7433
|
+
secret: GH_CREDENTIAL_GROUP,
|
|
7434
|
+
variable: GH_CREDENTIAL_GROUP,
|
|
7435
|
+
"ssh-key": GH_CREDENTIAL_GROUP,
|
|
7436
|
+
"gpg-key": GH_CREDENTIAL_GROUP,
|
|
7437
|
+
gist: GH_REMOTE_GROUP,
|
|
7438
|
+
release: GH_REMOTE_GROUP,
|
|
7439
|
+
repo: GH_REMOTE_GROUP,
|
|
7440
|
+
ruleset: GH_REMOTE_GROUP,
|
|
7441
|
+
label: GH_REMOTE_GROUP,
|
|
7442
|
+
workflow: GH_REMOTE_GROUP
|
|
7443
|
+
} },
|
|
7444
|
+
rm: PATH_OPERANDS,
|
|
7445
|
+
rmdir: PATH_OPERANDS,
|
|
7446
|
+
shred: PATH_OPERANDS,
|
|
7447
|
+
truncate: PATH_OPERANDS,
|
|
7448
|
+
chmod: PATH_OPERANDS,
|
|
7449
|
+
chown: PATH_OPERANDS,
|
|
7450
|
+
cp: COPY,
|
|
7451
|
+
ln: COPY,
|
|
7452
|
+
mv: {
|
|
7453
|
+
valueOptions: TARGET_DIRECTORY,
|
|
7454
|
+
writes: [{
|
|
7455
|
+
options: TARGET_DIRECTORY,
|
|
7456
|
+
operands: ["destination", "sources"]
|
|
7457
|
+
}]
|
|
7458
|
+
},
|
|
7459
|
+
rsync: { writes: [{ operands: ["destination"] }] },
|
|
7460
|
+
find: { writes: [{
|
|
7461
|
+
flags: ["-delete", "-exec"],
|
|
7462
|
+
operands: ["first"]
|
|
7463
|
+
}] },
|
|
7464
|
+
sed: IN_PLACE_EDIT,
|
|
7465
|
+
perl: IN_PLACE_EDIT,
|
|
7466
|
+
"ssh-keygen": {
|
|
7467
|
+
valueOptions: ["-f"],
|
|
7468
|
+
writes: [{
|
|
7469
|
+
readFlags: [
|
|
7470
|
+
"-l",
|
|
7471
|
+
"-y",
|
|
7472
|
+
"-F",
|
|
7473
|
+
"-B",
|
|
7474
|
+
"-R"
|
|
7475
|
+
],
|
|
7476
|
+
options: ["-f"]
|
|
7477
|
+
}]
|
|
7478
|
+
},
|
|
7479
|
+
ssh: {
|
|
7480
|
+
valueOptions: SSH_VALUE_OPTIONS,
|
|
7481
|
+
optionsEndAtOperand: true,
|
|
7482
|
+
writes: [{ options: ["-E"] }, {
|
|
7483
|
+
operands: ["all"],
|
|
7484
|
+
role: "remote-command",
|
|
7485
|
+
reads: SSH_READS,
|
|
7486
|
+
finding: { kind: "remote-write" }
|
|
7487
|
+
}]
|
|
7488
|
+
},
|
|
7489
|
+
scp: {
|
|
7490
|
+
valueOptions: SCP_VALUE_OPTIONS,
|
|
7491
|
+
optionsEndAtOperand: true,
|
|
7492
|
+
writes: [{
|
|
7493
|
+
operands: ["destination"],
|
|
7494
|
+
role: "remote-path",
|
|
7495
|
+
finding: { kind: "remote-write" }
|
|
7496
|
+
}]
|
|
7497
|
+
},
|
|
7498
|
+
curl: {
|
|
7499
|
+
valueOptions: ["-X", "--request"],
|
|
7500
|
+
writes: [{
|
|
7501
|
+
flags: [/^-[a-zA-Z]*[dFT]$/u, /^--(?:data\S*|form|upload-file|json)$/u],
|
|
7502
|
+
operands: ["all"],
|
|
7503
|
+
role: "url",
|
|
7504
|
+
finding: CURL_UPLOAD
|
|
7505
|
+
}, {
|
|
7506
|
+
optionValue: {
|
|
7507
|
+
options: ["-X", "--request"],
|
|
7508
|
+
value: /^(?:POST|PUT|PATCH|DELETE)$/u
|
|
7509
|
+
},
|
|
7510
|
+
operands: ["all"],
|
|
7511
|
+
role: "url",
|
|
7512
|
+
finding: CURL_UPLOAD
|
|
7513
|
+
}]
|
|
7514
|
+
},
|
|
7515
|
+
op: { subcommands: {
|
|
7516
|
+
item: { subcommands: {
|
|
7517
|
+
edit: CREDENTIAL_WRITE,
|
|
7518
|
+
create: CREDENTIAL_WRITE,
|
|
7519
|
+
delete: CREDENTIAL_WRITE
|
|
7520
|
+
} },
|
|
7521
|
+
vault: { subcommands: {
|
|
7522
|
+
edit: CREDENTIAL_WRITE,
|
|
7523
|
+
create: CREDENTIAL_WRITE,
|
|
7524
|
+
delete: CREDENTIAL_WRITE
|
|
7525
|
+
} },
|
|
7526
|
+
document: { subcommands: {
|
|
7527
|
+
edit: CREDENTIAL_WRITE,
|
|
7528
|
+
create: CREDENTIAL_WRITE,
|
|
7529
|
+
delete: CREDENTIAL_WRITE
|
|
7530
|
+
} }
|
|
7531
|
+
} },
|
|
7532
|
+
aws: { subcommands: { iam: { otherSubcommand: {
|
|
7533
|
+
pattern: IAM_WRITE,
|
|
7534
|
+
entry: CREDENTIAL_WRITE
|
|
7535
|
+
} } } },
|
|
7536
|
+
docker: {
|
|
7537
|
+
valueOptions: [
|
|
7538
|
+
"--config",
|
|
7539
|
+
"-c",
|
|
7540
|
+
"--context",
|
|
7541
|
+
"-H",
|
|
7542
|
+
"--host",
|
|
7543
|
+
"-l",
|
|
7544
|
+
"--log-level",
|
|
7545
|
+
"--tlscacert",
|
|
7546
|
+
"--tlscert",
|
|
7547
|
+
"--tlskey"
|
|
7548
|
+
],
|
|
7549
|
+
subcommands: {
|
|
7550
|
+
volume: { subcommands: {
|
|
7551
|
+
prune: DOCKER_PRUNE,
|
|
7552
|
+
rm: DOCKER_PRUNE
|
|
7553
|
+
} },
|
|
7554
|
+
system: { subcommands: { prune: DOCKER_PRUNE } },
|
|
7555
|
+
image: { subcommands: { prune: DOCKER_PRUNE } },
|
|
7556
|
+
container: { subcommands: { prune: DOCKER_PRUNE } },
|
|
7557
|
+
compose: {
|
|
7558
|
+
valueOptions: [
|
|
7559
|
+
"--ansi",
|
|
7560
|
+
"--env-file",
|
|
7561
|
+
"-f",
|
|
7562
|
+
"--file",
|
|
7563
|
+
"--parallel",
|
|
7564
|
+
"--profile",
|
|
7565
|
+
"--progress",
|
|
7566
|
+
"--project-directory",
|
|
7567
|
+
"-p",
|
|
7568
|
+
"--project-name"
|
|
7569
|
+
],
|
|
7570
|
+
subcommands: { down: { writes: [{
|
|
7571
|
+
flags: ["-v", "--volumes"],
|
|
7572
|
+
finding: {
|
|
7573
|
+
kind: "prune",
|
|
7574
|
+
quote: "call"
|
|
7575
|
+
}
|
|
7576
|
+
}] } }
|
|
7577
|
+
}
|
|
7578
|
+
}
|
|
7579
|
+
},
|
|
7580
|
+
kubectl: {
|
|
7581
|
+
valueOptions: [
|
|
7582
|
+
"--as",
|
|
7583
|
+
"--as-group",
|
|
7584
|
+
"--cache-dir",
|
|
7585
|
+
"--certificate-authority",
|
|
7586
|
+
"--client-certificate",
|
|
7587
|
+
"--client-key",
|
|
7588
|
+
"--cluster",
|
|
7589
|
+
"--context",
|
|
7590
|
+
"--kubeconfig",
|
|
7591
|
+
"-n",
|
|
7592
|
+
"--namespace",
|
|
7593
|
+
"--request-timeout",
|
|
7594
|
+
"-s",
|
|
7595
|
+
"--server",
|
|
7596
|
+
"--tls-server-name",
|
|
7597
|
+
"--token",
|
|
7598
|
+
"--user"
|
|
7599
|
+
],
|
|
7600
|
+
subcommands: {
|
|
7601
|
+
apply: KUBECTL_WRITE,
|
|
7602
|
+
delete: KUBECTL_WRITE,
|
|
7603
|
+
replace: KUBECTL_WRITE,
|
|
7604
|
+
patch: KUBECTL_WRITE,
|
|
7605
|
+
scale: KUBECTL_WRITE
|
|
7606
|
+
}
|
|
7607
|
+
},
|
|
7608
|
+
terraform: { subcommands: {
|
|
7609
|
+
apply: TERRAFORM_WRITE,
|
|
7610
|
+
destroy: TERRAFORM_WRITE,
|
|
7611
|
+
import: TERRAFORM_WRITE
|
|
7612
|
+
} },
|
|
7613
|
+
codex: { subcommands: { exec: {
|
|
7614
|
+
valueOptions: ["-o", "--output-last-message"],
|
|
7615
|
+
writes: [{ options: ["-o", "--output-last-message"] }]
|
|
7616
|
+
} } },
|
|
7617
|
+
psql: { writes: [{
|
|
7618
|
+
words: [/\b(?:UPDATE|DELETE|DROP|INSERT|TRUNCATE|ALTER)\b/iu],
|
|
7619
|
+
finding: {
|
|
7620
|
+
kind: "remote-write",
|
|
7621
|
+
target: "database write"
|
|
7622
|
+
}
|
|
7623
|
+
}] },
|
|
7624
|
+
npm: {
|
|
7625
|
+
subcommands: { publish: { writes: [{ finding: { kind: "remote-write" } }] } },
|
|
7626
|
+
writes: [PRODUCTION_DEPLOY]
|
|
7627
|
+
},
|
|
7628
|
+
bun: { writes: [PRODUCTION_DEPLOY] },
|
|
7629
|
+
pnpm: { writes: [PRODUCTION_DEPLOY] },
|
|
7630
|
+
yarn: { writes: [PRODUCTION_DEPLOY] }
|
|
7631
|
+
};
|
|
7632
|
+
//#endregion
|
|
7633
|
+
//#region src/containment/collect-program-findings.ts
|
|
7634
|
+
function collectProgramFindings(name, args, context, table = PROGRAM_TABLE) {
|
|
7635
|
+
const entry = findEntry(table, name);
|
|
7636
|
+
return entry === void 0 ? [] : collectEntryFindings(entry, [name], args, {
|
|
7637
|
+
...context,
|
|
7638
|
+
call: [name, ...args]
|
|
7639
|
+
});
|
|
6889
7640
|
}
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
"127.0.0.1",
|
|
6893
|
-
"[::1]",
|
|
6894
|
-
"0.0.0.0"
|
|
6895
|
-
]);
|
|
6896
|
-
function collectCurlFindings(args) {
|
|
6897
|
-
const joined = args.join(" ");
|
|
6898
|
-
const isUpload = args.some((word) => /^-[a-zA-Z]*[dFT]$|^--(?:data\S*|form|upload-file|json)$/u.test(word));
|
|
6899
|
-
const isWriteMethod = /(?:-X\s*|--request\s+)(?:POST|PUT|PATCH|DELETE)/u.test(joined);
|
|
6900
|
-
if (!isUpload && !isWriteMethod) return [];
|
|
6901
|
-
const hosts = args.filter((word) => /^(?:https?:\/\/|localhost\b|127\.)/u.test(word)).map((word) => /^(?:https?:\/\/)?(?<host>\[[^\]]+\]|[^/:?#\s]+)/u.exec(word)?.groups?.["host"]?.toLowerCase() ?? "");
|
|
6902
|
-
return hosts.length > 0 && hosts.every((host) => LOCAL_HOSTS.has(host)) ? [] : [{
|
|
6903
|
-
kind: "remote-write",
|
|
6904
|
-
target: "curl upload"
|
|
6905
|
-
}];
|
|
7641
|
+
function findEntry(entries, name) {
|
|
7642
|
+
return entries !== void 0 && Object.hasOwn(entries, name) ? entries[name] : void 0;
|
|
6906
7643
|
}
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
]
|
|
6918
|
-
|
|
6919
|
-
const
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
7644
|
+
function collectEntryFindings(entry, command, words, context) {
|
|
7645
|
+
const call = splitProgramCall(entry, words, context.directory, context.scope.home);
|
|
7646
|
+
const entryContext = {
|
|
7647
|
+
...context,
|
|
7648
|
+
directory: call.directory
|
|
7649
|
+
};
|
|
7650
|
+
if (call.subcommand !== null) return collectEntryFindings(call.subcommand.entry, [...command, call.subcommand.name], call.subcommand.words, entryContext);
|
|
7651
|
+
return (entry.writes ?? []).filter((rule) => isRuleApplied(rule, call, context.call)).flatMap((rule) => collectRuleFindings(rule, call, command, entryContext));
|
|
7652
|
+
}
|
|
7653
|
+
function splitProgramCall(entry, words, start, home) {
|
|
7654
|
+
const options = [];
|
|
7655
|
+
const operands = [];
|
|
7656
|
+
const directoryOptions = entry.directoryOptions ?? [];
|
|
7657
|
+
let directory = start;
|
|
7658
|
+
let isOptionsEnd = false;
|
|
7659
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
7660
|
+
const word = words[index] ?? "";
|
|
7661
|
+
if (isOptionsEnd || !word.startsWith("-") || word === "-") {
|
|
7662
|
+
const subcommand = operands.length === 0 ? findSubcommand(entry, word) : void 0;
|
|
7663
|
+
if (subcommand !== void 0) return {
|
|
7664
|
+
options,
|
|
7665
|
+
operands,
|
|
7666
|
+
directory,
|
|
7667
|
+
subcommand: {
|
|
7668
|
+
name: word,
|
|
7669
|
+
entry: subcommand,
|
|
7670
|
+
words: words.slice(index + 1)
|
|
7671
|
+
}
|
|
7672
|
+
};
|
|
7673
|
+
operands.push(word);
|
|
7674
|
+
isOptionsEnd ||= entry.optionsEndAtOperand === true;
|
|
7675
|
+
} else if (word === "--") isOptionsEnd = true;
|
|
7676
|
+
else {
|
|
7677
|
+
const split = splitOption([...directoryOptions, ...entry.valueOptions ?? []], word, words[index + 1]);
|
|
7678
|
+
for (const option of split.options) if (directoryOptions.includes(option.name)) directory = resolveShellPath(directory, option.value ?? "", home);
|
|
7679
|
+
options.push(...split.options);
|
|
7680
|
+
index += split.isValueNext ? 1 : 0;
|
|
7681
|
+
}
|
|
7682
|
+
}
|
|
7683
|
+
return {
|
|
7684
|
+
options,
|
|
7685
|
+
operands,
|
|
7686
|
+
directory,
|
|
7687
|
+
subcommand: null
|
|
7688
|
+
};
|
|
7689
|
+
}
|
|
7690
|
+
function findSubcommand(entry, word) {
|
|
7691
|
+
const other = entry.otherSubcommand;
|
|
7692
|
+
return findEntry(entry.subcommands, word) ?? (other !== void 0 && (other.pattern?.test(word) ?? true) ? other.entry : void 0);
|
|
7693
|
+
}
|
|
7694
|
+
function splitOption(valueOptions, word, next) {
|
|
7695
|
+
for (const pattern of valueOptions) if (typeof pattern !== "string") {
|
|
7696
|
+
if (pattern.test(word)) return {
|
|
7697
|
+
options: [{
|
|
7698
|
+
name: word,
|
|
7699
|
+
value: next
|
|
7700
|
+
}],
|
|
7701
|
+
isValueNext: true
|
|
7702
|
+
};
|
|
7703
|
+
} else if (word === pattern) return {
|
|
7704
|
+
options: [{
|
|
7705
|
+
name: pattern,
|
|
7706
|
+
value: next
|
|
7707
|
+
}],
|
|
7708
|
+
isValueNext: true
|
|
7709
|
+
};
|
|
7710
|
+
else if (pattern.startsWith("--") ? word.startsWith(`${pattern}=`) : word.startsWith(pattern)) return {
|
|
7711
|
+
options: [{
|
|
7712
|
+
name: pattern,
|
|
7713
|
+
value: word.slice(pattern.length).replace(/^=/u, "")
|
|
7714
|
+
}],
|
|
7715
|
+
isValueNext: false
|
|
7716
|
+
};
|
|
7717
|
+
return splitOptionCluster(valueOptions, word, next);
|
|
7718
|
+
}
|
|
7719
|
+
function splitOptionCluster(valueOptions, word, next) {
|
|
7720
|
+
const letters = /^-[^-]./u.test(word) ? word.slice(1).match(/./gu) ?? [] : [];
|
|
7721
|
+
const at = letters.findIndex((letter) => valueOptions.includes(`-${letter}`));
|
|
7722
|
+
if (at === -1) return {
|
|
7723
|
+
options: [{
|
|
7724
|
+
name: word,
|
|
7725
|
+
value: void 0
|
|
7726
|
+
}],
|
|
7727
|
+
isValueNext: false
|
|
7728
|
+
};
|
|
7729
|
+
const attached = word.slice(at + 2);
|
|
7730
|
+
return {
|
|
7731
|
+
options: [...letters.slice(0, at).map((letter) => ({
|
|
7732
|
+
name: `-${letter}`,
|
|
7733
|
+
value: void 0
|
|
7734
|
+
})), {
|
|
7735
|
+
name: `-${letters[at] ?? ""}`,
|
|
7736
|
+
value: attached === "" ? next : attached
|
|
7737
|
+
}],
|
|
7738
|
+
isValueNext: attached === ""
|
|
7739
|
+
};
|
|
7740
|
+
}
|
|
7741
|
+
function isRuleApplied(rule, call, words) {
|
|
7742
|
+
const optionValue = rule.optionValue;
|
|
7743
|
+
return (rule.flags === void 0 || hasOption(call, rule.flags)) && !hasOption(call, rule.readFlags ?? []) && (rule.words ?? []).every((pattern) => words.some((word) => pattern.test(word))) && (optionValue === void 0 || call.options.some((option) => optionValue.options.includes(option.name) && option.value !== void 0 && optionValue.value.test(option.value)));
|
|
7744
|
+
}
|
|
7745
|
+
function hasOption(call, patterns) {
|
|
7746
|
+
return call.options.some((option) => patterns.some((pattern) => typeof pattern === "string" ? option.name === pattern : pattern.test(option.name)));
|
|
7747
|
+
}
|
|
7748
|
+
function collectRuleFindings(rule, call, command, context) {
|
|
7749
|
+
const finding = rule.finding === void 0 ? null : buildFinding(rule.finding, command, call, context.call);
|
|
7750
|
+
const directory = context.directory;
|
|
7751
|
+
if (rule.directory === true) return directory === null || isInScope(directory, context.scope, context.scratchPaths) ? [] : [{
|
|
6925
7752
|
kind: "path",
|
|
6926
|
-
target:
|
|
6927
|
-
}
|
|
6928
|
-
if (
|
|
6929
|
-
|
|
7753
|
+
target: directory
|
|
7754
|
+
}];
|
|
7755
|
+
if (rule.options === void 0 && rule.operands === void 0) return finding === null ? [] : [finding];
|
|
7756
|
+
const targets = [...call.options.flatMap((option) => rule.options?.includes(option.name) === true && option.value !== void 0 ? [option.value] : []), ...(rule.operands ?? []).flatMap((selector) => pickOperands(selector, rule, call))];
|
|
7757
|
+
return collectRoleFindings(rule, targets.length === 0 && rule.whenNone !== void 0 ? [rule.whenNone] : targets, finding, call, context);
|
|
7758
|
+
}
|
|
7759
|
+
function buildFinding(finding, command, call, words) {
|
|
7760
|
+
const quoted = {
|
|
7761
|
+
call: words,
|
|
7762
|
+
operand: [...command, ...call.operands.slice(0, 1)],
|
|
7763
|
+
command
|
|
7764
|
+
};
|
|
7765
|
+
return {
|
|
7766
|
+
kind: finding.kind,
|
|
7767
|
+
target: finding.target ?? quoted[finding.quote ?? "command"].join(" ")
|
|
7768
|
+
};
|
|
7769
|
+
}
|
|
7770
|
+
function collectRoleFindings(rule, targets, finding, call, context) {
|
|
7771
|
+
const scope = context.scope;
|
|
7772
|
+
const resolve = (target) => collectTargetFindings(context.directory, target, scope, context.scratchPaths);
|
|
7773
|
+
const role = rule.role ?? "path";
|
|
7774
|
+
if (role === "path") return targets.flatMap((target) => resolve(target));
|
|
7775
|
+
if (role === "branch") return targets.filter((branch) => !scope.branches.includes(branch)).map((branch) => ({
|
|
6930
7776
|
kind: "branch",
|
|
6931
7777
|
target: branch,
|
|
6932
7778
|
grant: { kind: "branch" }
|
|
6933
|
-
});
|
|
6934
|
-
|
|
6935
|
-
if (subcommand === "reset" && rest.includes("--hard") && resetTarget !== void 0 && resetTarget !== "HEAD" && resetTarget !== "@" && !isUnresolvedWord(resetTarget)) findings.push({
|
|
6936
|
-
kind: "discard",
|
|
6937
|
-
target: `git reset --hard ${resetTarget}`
|
|
6938
|
-
});
|
|
6939
|
-
const isConfigRead = rest.some((word) => /^--(?:get|get-all|get-regexp|list)$|^-l$/u.test(word));
|
|
6940
|
-
if (subcommand === "config" && rest.includes("--global") && !isConfigRead) findings.push({
|
|
6941
|
-
kind: "credential",
|
|
6942
|
-
target: `git config ${operands[0] ?? ""}`
|
|
6943
|
-
});
|
|
6944
|
-
if (subcommand === "push") {
|
|
6945
|
-
const currentBranch = gitDirectory === cwd ? scope.currentBranch : null;
|
|
6946
|
-
findings.push(...collectPushFindings(rest, operands, currentBranch, scope));
|
|
6947
|
-
}
|
|
6948
|
-
return findings;
|
|
6949
|
-
}
|
|
6950
|
-
function collectPushFindings(rest, operands, currentBranch, scope) {
|
|
6951
|
-
const [remote, ...refspecs] = operands;
|
|
6952
|
-
const findings = [];
|
|
6953
|
-
if (rest.includes("--all") || rest.includes("--mirror")) findings.push({
|
|
6954
|
-
kind: "branch",
|
|
6955
|
-
target: "--all"
|
|
6956
|
-
});
|
|
6957
|
-
if (remote !== void 0 && !isUnresolvedWord(remote) && !isOwnedRemote(remote, scope)) findings.push({
|
|
7779
|
+
}));
|
|
7780
|
+
if (role === "remote") return targets.filter((remote) => !isUnresolvedWord(remote) && !isOwnedRemote(remote, scope)).map((remote) => ({
|
|
6958
7781
|
kind: "remote-write",
|
|
6959
7782
|
target: remote
|
|
6960
|
-
});
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
const branch = named === "HEAD" ? currentBranch : named;
|
|
6965
|
-
if (branch !== null && !isUnresolvedWord(branch) && !branch.startsWith("refs/tags/") && !scope.branches.includes(branch)) findings.push({
|
|
6966
|
-
kind: "branch",
|
|
6967
|
-
target: branch,
|
|
6968
|
-
grant: { kind: "branch" }
|
|
6969
|
-
});
|
|
7783
|
+
}));
|
|
7784
|
+
if (role === "refspec") {
|
|
7785
|
+
const current = context.directory === context.cwd ? scope.currentBranch : null;
|
|
7786
|
+
return targets.flatMap((refspec) => collectRefspecFindings(refspec, current, scope));
|
|
6970
7787
|
}
|
|
6971
|
-
return
|
|
7788
|
+
return finding === null ? [] : collectNamedFindings(role, rule, targets, finding, call, context);
|
|
6972
7789
|
}
|
|
6973
7790
|
function isOwnedRemote(remote, scope) {
|
|
6974
7791
|
if (scope.remotes.some((entry) => entry.name === remote)) return true;
|
|
6975
7792
|
const slug = toRepositorySlug(remote);
|
|
6976
7793
|
return slug !== null && scope.remotes.some((entry) => toRepositorySlug(entry.url) === slug);
|
|
6977
7794
|
}
|
|
6978
|
-
|
|
6979
|
-
"
|
|
6980
|
-
"
|
|
6981
|
-
"
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
6988
|
-
"
|
|
6989
|
-
"
|
|
6990
|
-
"
|
|
6991
|
-
"
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
"
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
|
|
7795
|
+
function collectRefspecFindings(refspec, current, scope) {
|
|
7796
|
+
const named = (refspec.split(":").at(-1) ?? refspec).replace(/^\+/u, "").replace(/^refs\/heads\//u, "");
|
|
7797
|
+
const branch = named === "HEAD" ? current : named;
|
|
7798
|
+
return branch === null || isUnresolvedWord(branch) || branch.startsWith("refs/tags/") || scope.branches.includes(branch) ? [] : [{
|
|
7799
|
+
kind: "branch",
|
|
7800
|
+
target: branch,
|
|
7801
|
+
grant: { kind: "branch" }
|
|
7802
|
+
}];
|
|
7803
|
+
}
|
|
7804
|
+
function collectNamedFindings(role, rule, targets, finding, call, context) {
|
|
7805
|
+
if (role === "remote-path") return targets.flatMap((target) => /^[^/]+:/u.test(target) ? [finding] : collectTargetFindings(context.directory, target, context.scope, context.scratchPaths));
|
|
7806
|
+
if (role === "url") return isLocalUpload(targets) ? [] : [finding];
|
|
7807
|
+
if (role === "remote-command") return isRemoteRead(targets, rule.reads ?? []) ? [] : [finding];
|
|
7808
|
+
if (role === "commit") return targets.filter((commit) => commit !== "HEAD" && commit !== "@" && !isUnresolvedWord(commit)).map((commit) => ({
|
|
7809
|
+
kind: finding.kind,
|
|
7810
|
+
target: `${finding.target} ${commit}`
|
|
7811
|
+
}));
|
|
7812
|
+
if (role === "pull-request" || role === "pull-request-merge") return collectPullRequestFindings(rule, targets[0], finding, call, context.scope);
|
|
7813
|
+
return collectForgeFindings(rule, targets[0] ?? "", finding, context.scope);
|
|
7814
|
+
}
|
|
7815
|
+
function collectPullRequestFindings(rule, selector, finding, call, scope) {
|
|
7816
|
+
const repoOptions = rule.repoOptions ?? [];
|
|
7817
|
+
const url = /^https?:\/\/(?<repo>[^/]+\/[^/]+\/[^/]+)\/pull\/(?<number>\d+)(?:[/?#]|$)/u.exec(selector ?? "")?.groups;
|
|
7818
|
+
const repo = url?.["repo"] ?? call.options.findLast((option) => repoOptions.includes(option.name))?.value;
|
|
7819
|
+
let number = NaN;
|
|
7820
|
+
if (url !== void 0) number = Number(url["number"]);
|
|
7821
|
+
else if (selector !== void 0 && /^#?\d+$/u.test(selector)) number = Number(selector.replace("#", ""));
|
|
7822
|
+
if (rule.role === "pull-request" && isOwnedPullRequest(number, repo, scope)) return [];
|
|
7823
|
+
return Number.isInteger(number) && !hasOption(call, rule.ungrantFlags ?? []) ? [{
|
|
7824
|
+
...finding,
|
|
7825
|
+
grant: {
|
|
7826
|
+
kind: "pull-request",
|
|
7827
|
+
number
|
|
7828
|
+
}
|
|
7829
|
+
}] : [finding];
|
|
7830
|
+
}
|
|
7831
|
+
function collectForgeFindings(rule, path, finding, scope) {
|
|
7832
|
+
const pull = /^\/?repos\/(?<repo>[^/]+\/[^/]+)\/(?:pulls|issues)\/(?<number>\d+)(?:\/|$)/u.exec(path)?.groups;
|
|
7833
|
+
if (pull !== void 0 && isOwnedPullRequest(Number(pull["number"]), pull["repo"] ?? "", scope) && !path.includes("/merge")) return [];
|
|
7834
|
+
return [{
|
|
7835
|
+
kind: rule.credentialPattern?.test(path) === true ? "credential" : finding.kind,
|
|
7836
|
+
target: `${finding.target} ${path}`
|
|
7837
|
+
}];
|
|
7838
|
+
}
|
|
7839
|
+
function pickOperands(selector, rule, call) {
|
|
7840
|
+
const operands = call.operands;
|
|
7841
|
+
const isNamed = hasOption(call, rule.options ?? []);
|
|
7842
|
+
if (selector === "all") return operands;
|
|
7843
|
+
if (selector === "first") return operands.slice(0, 1);
|
|
7844
|
+
if (selector === "after-first") return operands.slice(1);
|
|
7845
|
+
if (selector === "after-script") return hasOption(call, rule.scriptOptions ?? []) ? operands : operands.slice(1);
|
|
7846
|
+
if (isNamed) return selector === "sources" ? operands : [];
|
|
7847
|
+
if (operands.length < 2) return [];
|
|
7848
|
+
return selector === "destination" ? operands.slice(-1) : operands.slice(0, -1);
|
|
7849
|
+
}
|
|
7850
|
+
const LOCAL_HOSTS = /* @__PURE__ */ new Set([
|
|
7851
|
+
"localhost",
|
|
7852
|
+
"127.0.0.1",
|
|
7853
|
+
"[::1]",
|
|
7854
|
+
"0.0.0.0"
|
|
7000
7855
|
]);
|
|
7001
|
-
|
|
7002
|
-
"
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7856
|
+
function isLocalUpload(words) {
|
|
7857
|
+
const hosts = words.filter((word) => /^(?:https?:\/\/|localhost\b|127\.)/u.test(word)).map((word) => /^(?:https?:\/\/)?(?<host>\[[^\]]+\]|[^/:?#\s]+)/u.exec(word)?.groups?.["host"]?.toLowerCase() ?? "");
|
|
7858
|
+
return hosts.length > 0 && hosts.every((host) => LOCAL_HOSTS.has(host));
|
|
7859
|
+
}
|
|
7860
|
+
function isRemoteRead(words, reads) {
|
|
7861
|
+
const [host, ...remote] = words;
|
|
7862
|
+
if (host === void 0) return true;
|
|
7863
|
+
const command = remote.join(" ");
|
|
7864
|
+
const [program] = command.trim().split(/\s+/u);
|
|
7865
|
+
return reads.includes(program ?? "") && !/[;&|<>`\n\r]|\$\(/u.test(command);
|
|
7866
|
+
}
|
|
7867
|
+
//#endregion
|
|
7868
|
+
//#region src/containment/collect-scope-findings.ts
|
|
7869
|
+
function collectScopeFindings(action, scope, scratchPaths = SCRATCH_PATHS) {
|
|
7870
|
+
const filePath = action.input["file_path"] ?? action.input["notebook_path"];
|
|
7871
|
+
const command = action.input["command"];
|
|
7872
|
+
const findings = [
|
|
7873
|
+
...FILE_WRITE_TOOLS.has(action.tool) && typeof filePath === "string" ? collectTargetFindings(action.cwd, filePath, scope, scratchPaths) : [],
|
|
7874
|
+
...action.tool.startsWith("mcp__") ? collectToolFindings(action, scope) : [],
|
|
7875
|
+
...action.tool === "Bash" && typeof command === "string" ? collectCommandFindings(command, action.cwd, action.cwd, scope, scratchPaths) : []
|
|
7876
|
+
];
|
|
7877
|
+
return findings.filter((finding, index) => findings.findIndex((other) => other.kind === finding.kind && other.target === finding.target) === index);
|
|
7878
|
+
}
|
|
7879
|
+
const FILE_WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
7880
|
+
"Edit",
|
|
7881
|
+
"MultiEdit",
|
|
7882
|
+
"Write",
|
|
7883
|
+
"NotebookEdit"
|
|
7008
7884
|
]);
|
|
7009
|
-
function
|
|
7010
|
-
const
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7885
|
+
function collectToolFindings(action, scope) {
|
|
7886
|
+
const verb = action.tool.split("__").at(-1) ?? "";
|
|
7887
|
+
if (!/merge|create|comment|update|delete|push|close|review/u.test(verb)) return [];
|
|
7888
|
+
const number = action.input["pullNumber"] ?? action.input["pull_number"] ?? action.input["issue_number"];
|
|
7889
|
+
const owner = action.input["owner"];
|
|
7890
|
+
const name = action.input["repo"];
|
|
7891
|
+
const repo = typeof owner === "string" && typeof name === "string" ? `${owner}/${name}` : void 0;
|
|
7892
|
+
if (!verb.includes("merge") && typeof number === "number" && isOwnedPullRequest(number, repo, scope)) return [];
|
|
7893
|
+
return typeof number === "number" ? [{
|
|
7894
|
+
kind: "remote-write",
|
|
7895
|
+
target: action.tool,
|
|
7896
|
+
grant: {
|
|
7897
|
+
kind: "pull-request",
|
|
7898
|
+
number
|
|
7899
|
+
}
|
|
7900
|
+
}] : [{
|
|
7901
|
+
kind: "remote-write",
|
|
7902
|
+
target: action.tool
|
|
7903
|
+
}];
|
|
7904
|
+
}
|
|
7905
|
+
function collectCommandFindings(command, directory, cwd, scope, scratchPaths) {
|
|
7906
|
+
const findings = [];
|
|
7907
|
+
const entries = collectShellCommands(command, directory, scope.home);
|
|
7908
|
+
const piped = collectPipedSegments(command);
|
|
7909
|
+
const segments = splitShellCommand(command).segments;
|
|
7910
|
+
const scripts = collectShellScripts(command);
|
|
7911
|
+
for (const [index, entry] of entries.entries()) {
|
|
7912
|
+
const [name, ...args] = entry.words;
|
|
7913
|
+
const fixed = findFixedXargsCommand(entry.words, segments[index]?.text ?? "");
|
|
7914
|
+
if (fixed !== null) findings.push(...collectCallFindings(fixed.name, fixed.args, entry.directory, cwd, scope, scratchPaths));
|
|
7915
|
+
else if (isBranchSweep(entries.slice(0, index + 1), piped.slice(0, index + 1))) findings.push({
|
|
7916
|
+
kind: "discard",
|
|
7917
|
+
target: "git branch delete by exclusion"
|
|
7918
|
+
});
|
|
7919
|
+
for (const target of entry.writeTargets) findings.push(...collectTargetFindings(entry.directory, target, scope, scratchPaths));
|
|
7920
|
+
if (name !== void 0 && name !== "cd") findings.push(...collectCallFindings(name, args, entry.directory, cwd, scope, scratchPaths));
|
|
7921
|
+
for (const script of scripts.filter((candidate) => candidate.segment === index)) findings.push(...collectCommandFindings(script.text, entry.directory, cwd, scope, scratchPaths));
|
|
7922
|
+
}
|
|
7923
|
+
return findings;
|
|
7924
|
+
}
|
|
7925
|
+
function collectPipedSegments(command) {
|
|
7926
|
+
const piped = [];
|
|
7927
|
+
let separator = null;
|
|
7928
|
+
let isInSegment = false;
|
|
7929
|
+
for (const token of splitShellTokens(command).tokens) if (token.kind === "separator") {
|
|
7930
|
+
separator = token.operator;
|
|
7931
|
+
isInSegment = false;
|
|
7932
|
+
} else if (!isInSegment && token.kind !== "comment" && token.kind !== "heredoc") {
|
|
7933
|
+
piped.push(separator === "|");
|
|
7934
|
+
isInSegment = true;
|
|
7935
|
+
}
|
|
7936
|
+
return piped;
|
|
7937
|
+
}
|
|
7938
|
+
const XARGS_FLAG = /^(?:-[0oprtx]+|--(?:null|no-run-if-empty|verbose|interactive|exit|open-tty))$/u;
|
|
7939
|
+
const XARGS_VALUE_OPTION = /^-[EPads]$/u;
|
|
7940
|
+
const XARGS_ATTACHED_VALUE = /^(?:-[EPads].|--(?:eof|max-procs|delimiter|arg-file|max-chars|process-slot-var)=)/u;
|
|
7941
|
+
const XARGS_REPLACE = /^(?:-I(?<attached>.+)|-i(?<short>.*)|--replace(?:=(?<long>.*))?)$/u;
|
|
7942
|
+
const PLAIN_WORD = /^(?:[\w./:@%+=-]|\{\})+$/u;
|
|
7943
|
+
function findFixedXargsCommand(words, text) {
|
|
7944
|
+
if (words[0] !== "xargs" || !splitShellTokens(text).tokens.every((token) => token.kind !== "word" || token.raw === token.value && PLAIN_WORD.test(token.value))) return null;
|
|
7945
|
+
let token = null;
|
|
7946
|
+
let start = 1;
|
|
7947
|
+
while (words[start]?.startsWith("-") === true) {
|
|
7948
|
+
const word = words[start] ?? "";
|
|
7949
|
+
const replace = XARGS_REPLACE.exec(word)?.groups;
|
|
7950
|
+
if (word === "--") {
|
|
7951
|
+
start += 1;
|
|
7952
|
+
break;
|
|
7953
|
+
}
|
|
7954
|
+
if (word === "-I") {
|
|
7955
|
+
token = words[start + 1] ?? null;
|
|
7956
|
+
start += 2;
|
|
7957
|
+
} else if (replace !== void 0) {
|
|
7958
|
+
const short = replace["short"];
|
|
7959
|
+
token = replace["attached"] ?? replace["long"] ?? (short === void 0 || short === "" ? "{}" : short);
|
|
7960
|
+
start += 1;
|
|
7961
|
+
} else if (word.startsWith("-J")) {
|
|
7962
|
+
token = null;
|
|
7963
|
+
start += word === "-J" ? 2 : 1;
|
|
7964
|
+
} else if (XARGS_VALUE_OPTION.test(word)) start += 2;
|
|
7965
|
+
else if (XARGS_FLAG.test(word) || XARGS_ATTACHED_VALUE.test(word)) start += 1;
|
|
7966
|
+
else return null;
|
|
7029
7967
|
}
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
target: `gh issue ${verb}`
|
|
7033
|
-
}];
|
|
7034
|
-
if (group === "api") return collectGhAPIFindings(args.slice(1), scope);
|
|
7035
|
-
if (group === "auth" && [
|
|
7036
|
-
"refresh",
|
|
7037
|
-
"login",
|
|
7038
|
-
"logout"
|
|
7039
|
-
].includes(verb)) return [{
|
|
7040
|
-
kind: "credential",
|
|
7041
|
-
target: `gh auth ${verb}`
|
|
7042
|
-
}];
|
|
7043
|
-
if (GH_CREDENTIAL_WRITES.has(group) && !GH_READS.has(verb)) return [{
|
|
7044
|
-
kind: "credential",
|
|
7045
|
-
target: `gh ${group} ${verb}`
|
|
7046
|
-
}];
|
|
7047
|
-
if (GH_REMOTE_WRITES.has(group) && !GH_READS.has(verb)) return [{
|
|
7048
|
-
kind: "remote-write",
|
|
7049
|
-
target: `gh ${group} ${verb}`
|
|
7050
|
-
}];
|
|
7051
|
-
return [];
|
|
7052
|
-
}
|
|
7053
|
-
const GH_PR_VALUE_OPTIONS = {
|
|
7054
|
-
comment: /* @__PURE__ */ new Set([
|
|
7055
|
-
"-b",
|
|
7056
|
-
"--body",
|
|
7057
|
-
"-F",
|
|
7058
|
-
"--body-file",
|
|
7059
|
-
"--attach"
|
|
7060
|
-
]),
|
|
7061
|
-
merge: /* @__PURE__ */ new Set([
|
|
7062
|
-
"-A",
|
|
7063
|
-
"--author-email",
|
|
7064
|
-
"-b",
|
|
7065
|
-
"--body",
|
|
7066
|
-
"-F",
|
|
7067
|
-
"--body-file",
|
|
7068
|
-
"--match-head-commit",
|
|
7069
|
-
"-t",
|
|
7070
|
-
"--subject"
|
|
7071
|
-
]),
|
|
7072
|
-
edit: /* @__PURE__ */ new Set([
|
|
7073
|
-
"--add-assignee",
|
|
7074
|
-
"--add-label",
|
|
7075
|
-
"--add-project",
|
|
7076
|
-
"--add-reviewer",
|
|
7077
|
-
"--attach",
|
|
7078
|
-
"-B",
|
|
7079
|
-
"--base",
|
|
7080
|
-
"-b",
|
|
7081
|
-
"--body",
|
|
7082
|
-
"-F",
|
|
7083
|
-
"--body-file",
|
|
7084
|
-
"-m",
|
|
7085
|
-
"--milestone",
|
|
7086
|
-
"--remove-assignee",
|
|
7087
|
-
"--remove-label",
|
|
7088
|
-
"--remove-project",
|
|
7089
|
-
"--remove-reviewer",
|
|
7090
|
-
"-t",
|
|
7091
|
-
"--title"
|
|
7092
|
-
]),
|
|
7093
|
-
close: /* @__PURE__ */ new Set(["-c", "--comment"]),
|
|
7094
|
-
review: /* @__PURE__ */ new Set([
|
|
7095
|
-
"-b",
|
|
7096
|
-
"--body",
|
|
7097
|
-
"-F",
|
|
7098
|
-
"--body-file"
|
|
7099
|
-
]),
|
|
7100
|
-
ready: /* @__PURE__ */ new Set(),
|
|
7101
|
-
reopen: /* @__PURE__ */ new Set(["-c", "--comment"])
|
|
7102
|
-
};
|
|
7103
|
-
const GH_REPO_OPTIONS = /* @__PURE__ */ new Set(["-R", "--repo"]);
|
|
7104
|
-
function findPullRequestSelector(verb, args) {
|
|
7105
|
-
const valueOptions = GH_PR_VALUE_OPTIONS[verb] ?? /* @__PURE__ */ new Set();
|
|
7106
|
-
let repo;
|
|
7107
|
-
let selector;
|
|
7108
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
7109
|
-
const word = args[index] ?? "";
|
|
7110
|
-
if (GH_REPO_OPTIONS.has(word)) repo = args[index + 1];
|
|
7111
|
-
else if (word.startsWith("--repo=")) repo = word.slice(7);
|
|
7112
|
-
if (GH_REPO_OPTIONS.has(word) || valueOptions.has(word)) index += 1;
|
|
7113
|
-
else if (selector === void 0 && !word.startsWith("-")) selector = word;
|
|
7114
|
-
}
|
|
7115
|
-
if (selector === void 0) return null;
|
|
7116
|
-
const url = /^https?:\/\/(?<repo>[^/]+\/[^/]+\/[^/]+)\/pull\/(?<number>\d+)(?:[/?#]|$)/u.exec(selector)?.groups;
|
|
7117
|
-
if (url !== void 0) return {
|
|
7118
|
-
number: Number(url["number"]),
|
|
7119
|
-
repo: url["repo"]
|
|
7120
|
-
};
|
|
7968
|
+
const [name, ...args] = words.slice(start);
|
|
7969
|
+
if (token === null || token === "" || name === void 0 || [name, ...args].some((word) => word.includes(token))) return null;
|
|
7121
7970
|
return {
|
|
7122
|
-
|
|
7123
|
-
|
|
7971
|
+
name,
|
|
7972
|
+
args
|
|
7124
7973
|
};
|
|
7125
7974
|
}
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7975
|
+
const XARGS_VALUE_OPTIONS = /* @__PURE__ */ new Set([
|
|
7976
|
+
"-I",
|
|
7977
|
+
"-J",
|
|
7978
|
+
"-L",
|
|
7979
|
+
"-n",
|
|
7980
|
+
"-P",
|
|
7981
|
+
"-s",
|
|
7982
|
+
"-d",
|
|
7983
|
+
"-E"
|
|
7984
|
+
]);
|
|
7985
|
+
function isBranchSweep(entries, piped) {
|
|
7986
|
+
const deleter = entries.at(-1)?.words ?? [];
|
|
7987
|
+
if (deleter[0] !== "xargs" || piped.at(-1) !== true) return false;
|
|
7988
|
+
let start = 1;
|
|
7989
|
+
while (deleter[start]?.startsWith("-") === true) {
|
|
7990
|
+
if (/^(?:-a|--arg-file)/u.test(deleter[start] ?? "")) return false;
|
|
7991
|
+
start += XARGS_VALUE_OPTIONS.has(deleter[start] ?? "") ? 2 : 1;
|
|
7992
|
+
}
|
|
7993
|
+
const [program, subcommand, ...rest] = deleter.slice(start);
|
|
7994
|
+
if (program !== "git" || subcommand !== "branch" || !rest.some((word) => /^-(?:d|D|-delete)$/u.test(word))) return false;
|
|
7995
|
+
let first = entries.length - 2;
|
|
7996
|
+
while (first > 0 && piped[first] === true) first -= 1;
|
|
7997
|
+
const [listing, ...filters] = entries.slice(first, -1).map((entry) => entry.words);
|
|
7998
|
+
return listing !== void 0 && isWholeBranchListing(listing) && filters.every((filter) => isExcludingFilter(filter));
|
|
7130
7999
|
}
|
|
7131
|
-
function
|
|
7132
|
-
const
|
|
7133
|
-
if (
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
const methodIndex = args.findIndex((word) => word === "-X" || word === "--method");
|
|
7138
|
-
const method = methodIndex === -1 ? null : (args[methodIndex + 1] ?? "").toUpperCase();
|
|
7139
|
-
const hasFields = args.some((word) => /^(?:-f|-F|--field|--raw-field|--input)$/u.test(word));
|
|
7140
|
-
if (method === "GET" || method === null && !hasFields) return [];
|
|
7141
|
-
const pull = /^\/?repos\/(?<repo>[^/]+\/[^/]+)\/(?:pulls|issues)\/(?<number>\d+)(?:\/|$)/u.exec(endpoint)?.groups;
|
|
7142
|
-
if (pull !== void 0 && isOwnedPullRequest(Number(pull["number"]), pull["repo"] ?? "", scope) && !endpoint.includes("/merge")) return [];
|
|
7143
|
-
return [{
|
|
7144
|
-
kind: /secret|collaborator|protection|key|permission/u.test(endpoint) ? "credential" : "remote-write",
|
|
7145
|
-
target: `gh api ${endpoint}`
|
|
7146
|
-
}];
|
|
8000
|
+
function isWholeBranchListing(words) {
|
|
8001
|
+
const [program, subcommand, ...rest] = words;
|
|
8002
|
+
if (program !== "git") return false;
|
|
8003
|
+
if (subcommand === "branch") return rest.every((word) => /^(?:-a|-r|-v|-vv|--all|--remotes|--no-color|--format=.*)$/u.test(word));
|
|
8004
|
+
if (subcommand !== "for-each-ref") return false;
|
|
8005
|
+
return rest.filter((_word, index) => !/^--(?:format|sort)$/u.test(rest[index - 1] ?? "")).every((word) => /^--(?:format|sort)(?:=.*)?$/u.test(word) || /^refs\/heads\/?$/u.test(word));
|
|
7147
8006
|
}
|
|
7148
|
-
const
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
if (
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
if (name === "docker" && /\b(?:volume|system|image|container) prune\b|\bvolume rm\b|\bdown\b.*(?:-v|--volumes)\b/u.test(joined)) return {
|
|
7160
|
-
kind: "prune",
|
|
7161
|
-
target: `docker ${joined}`
|
|
7162
|
-
};
|
|
7163
|
-
if (name === "npm" && args[0] === "publish") return {
|
|
7164
|
-
kind: "remote-write",
|
|
7165
|
-
target: "npm publish"
|
|
7166
|
-
};
|
|
7167
|
-
if (name === "kubectl" && /\b(?:apply|delete|replace|patch|scale)\b/u.test(joined)) return {
|
|
7168
|
-
kind: "remote-write",
|
|
7169
|
-
target: "kubectl write"
|
|
7170
|
-
};
|
|
7171
|
-
if (name === "terraform" && /\b(?:apply|destroy|import)\b/u.test(joined)) return {
|
|
7172
|
-
kind: "remote-write",
|
|
7173
|
-
target: "terraform write"
|
|
7174
|
-
};
|
|
7175
|
-
if (name === "psql" && /\b(?:UPDATE|DELETE|DROP|INSERT|TRUNCATE|ALTER)\b/iu.test(joined)) return {
|
|
7176
|
-
kind: "remote-write",
|
|
7177
|
-
target: "database write"
|
|
7178
|
-
};
|
|
7179
|
-
if (/^(?:bun|npm|pnpm|yarn)$/u.test(name) && /\bdeploy\b/u.test(joined) && /prod/u.test(joined)) return {
|
|
7180
|
-
kind: "remote-write",
|
|
7181
|
-
target: "production deploy"
|
|
7182
|
-
};
|
|
7183
|
-
return null;
|
|
8007
|
+
const NAME_EDITORS = /* @__PURE__ */ new Set([
|
|
8008
|
+
"tr",
|
|
8009
|
+
"cut",
|
|
8010
|
+
"sort"
|
|
8011
|
+
]);
|
|
8012
|
+
function isExcludingFilter(words) {
|
|
8013
|
+
const [program, ...args] = words;
|
|
8014
|
+
if (program === "grep" || program === "egrep") return args.some((word) => /^-[a-zA-Z]*v[a-zA-Z]*$|^--invert-match$/u.test(word));
|
|
8015
|
+
if (program === "sed") return isSubstitutionOnly(args);
|
|
8016
|
+
if (program === "uniq") return args.every((word) => !/^-[a-zA-Z]*[dDu]/u.test(word));
|
|
8017
|
+
return program !== void 0 && NAME_EDITORS.has(program);
|
|
7184
8018
|
}
|
|
7185
|
-
function
|
|
7186
|
-
const
|
|
7187
|
-
|
|
7188
|
-
let sawScript = false;
|
|
7189
|
-
for (const word of args) if (skipNext) {
|
|
7190
|
-
skipNext = false;
|
|
7191
|
-
sawScript = true;
|
|
7192
|
-
} else if (/^-\w*e$/u.test(word)) skipNext = true;
|
|
7193
|
-
else if (!word.startsWith("-")) {
|
|
7194
|
-
if (sawScript) files.push(word);
|
|
7195
|
-
else sawScript = true;
|
|
7196
|
-
}
|
|
7197
|
-
return files;
|
|
8019
|
+
function isSubstitutionOnly(args) {
|
|
8020
|
+
const scripts = args.filter((word) => word !== "-e" && word !== "-E" && word !== "-r");
|
|
8021
|
+
return scripts.length > 0 && scripts.every((script) => /^s(?<delimiter>[^\w\s;])(?:(?!\k<delimiter>).)*\k<delimiter>(?:(?!\k<delimiter>).)*\k<delimiter>g?$/u.test(script));
|
|
7198
8022
|
}
|
|
7199
|
-
function
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
});
|
|
8023
|
+
function collectCallFindings(name, args, directory, cwd, scope, scratchPaths) {
|
|
8024
|
+
return [...collectProgramFindings(name, args, {
|
|
8025
|
+
directory,
|
|
8026
|
+
cwd,
|
|
8027
|
+
scope,
|
|
8028
|
+
scratchPaths
|
|
8029
|
+
}), ...collectRecordFindings(name, args)];
|
|
7206
8030
|
}
|
|
7207
|
-
function
|
|
7208
|
-
|
|
8031
|
+
function collectRecordFindings(name, args) {
|
|
8032
|
+
const words = [name, ...args];
|
|
8033
|
+
const launcher = words.findIndex((word) => word.includes("auto-mode"));
|
|
8034
|
+
return launcher !== -1 && words.slice(launcher + 1).includes("record") ? [{
|
|
8035
|
+
kind: "credential",
|
|
8036
|
+
target: "auto-mode record"
|
|
8037
|
+
}] : [];
|
|
7209
8038
|
}
|
|
7210
8039
|
//#endregion
|
|
7211
8040
|
//#region src/containment/check-containment.ts
|
|
@@ -7248,6 +8077,83 @@ function formatList(one, many, values) {
|
|
|
7248
8077
|
return `the ${values.length === 1 ? one : many} ${values.join(", ")}`;
|
|
7249
8078
|
}
|
|
7250
8079
|
//#endregion
|
|
8080
|
+
//#region src/scope/is-protected-branch.ts
|
|
8081
|
+
function isProtectedBranch(branch, patterns) {
|
|
8082
|
+
return patterns.some((pattern) => buildBranchPattern(pattern).test(branch));
|
|
8083
|
+
}
|
|
8084
|
+
function buildBranchPattern(pattern) {
|
|
8085
|
+
let source = "";
|
|
8086
|
+
let index = 0;
|
|
8087
|
+
while (index < pattern.length) {
|
|
8088
|
+
const char = pattern[index] ?? "";
|
|
8089
|
+
if (pattern.startsWith("**/", index)) {
|
|
8090
|
+
source += "(?:.*/)?";
|
|
8091
|
+
index += 3;
|
|
8092
|
+
} else if (pattern.startsWith("**", index)) {
|
|
8093
|
+
source += ".*";
|
|
8094
|
+
index += 2;
|
|
8095
|
+
} else if (char === "*") {
|
|
8096
|
+
source += "[^/]*";
|
|
8097
|
+
index += 1;
|
|
8098
|
+
} else if (char === "?") {
|
|
8099
|
+
source += "[^/]";
|
|
8100
|
+
index += 1;
|
|
8101
|
+
} else if (char === "[") {
|
|
8102
|
+
const bracket = buildBracketPattern(pattern, index);
|
|
8103
|
+
source += bracket?.source ?? String.raw`\[`;
|
|
8104
|
+
index = bracket?.end ?? index + 1;
|
|
8105
|
+
} else if (char === "\\" && index + 1 < pattern.length) {
|
|
8106
|
+
source += toLiteralPattern(pattern[index + 1] ?? "");
|
|
8107
|
+
index += 2;
|
|
8108
|
+
} else {
|
|
8109
|
+
source += toLiteralPattern(char);
|
|
8110
|
+
index += 1;
|
|
8111
|
+
}
|
|
8112
|
+
}
|
|
8113
|
+
return new RegExp(`^${source}$`, "u");
|
|
8114
|
+
}
|
|
8115
|
+
function buildBracketPattern(pattern, start) {
|
|
8116
|
+
let index = start + 1;
|
|
8117
|
+
const negated = pattern[index] === "!" || pattern[index] === "^";
|
|
8118
|
+
if (negated) index += 1;
|
|
8119
|
+
let members = "";
|
|
8120
|
+
while (index < pattern.length) {
|
|
8121
|
+
if (pattern[index] === "]") return {
|
|
8122
|
+
source: buildClassSource(members, negated),
|
|
8123
|
+
end: index + 1
|
|
8124
|
+
};
|
|
8125
|
+
const low = readClassChar(pattern, index);
|
|
8126
|
+
index = low.end;
|
|
8127
|
+
if (pattern[index] === "-" && index + 1 < pattern.length && pattern[index + 1] !== "]") {
|
|
8128
|
+
const high = readClassChar(pattern, index + 1);
|
|
8129
|
+
index = high.end;
|
|
8130
|
+
members += buildRangeSource(low.char, high.char);
|
|
8131
|
+
} else members += toClassMember(low.char);
|
|
8132
|
+
}
|
|
8133
|
+
return null;
|
|
8134
|
+
}
|
|
8135
|
+
function readClassChar(pattern, index) {
|
|
8136
|
+
const start = pattern[index] === "\\" && index + 1 < pattern.length ? index + 1 : index;
|
|
8137
|
+
const char = String.fromCodePoint(pattern.codePointAt(start) ?? 0);
|
|
8138
|
+
return {
|
|
8139
|
+
char,
|
|
8140
|
+
end: start + char.length
|
|
8141
|
+
};
|
|
8142
|
+
}
|
|
8143
|
+
function buildRangeSource(low, high) {
|
|
8144
|
+
return (low.codePointAt(0) ?? 0) <= (high.codePointAt(0) ?? 0) ? `${toClassMember(low)}-${toClassMember(high)}` : "";
|
|
8145
|
+
}
|
|
8146
|
+
function buildClassSource(members, negated) {
|
|
8147
|
+
if (negated) return `[^/${members}]`;
|
|
8148
|
+
return members === "" ? "(?!)" : `(?!/)[${members}]`;
|
|
8149
|
+
}
|
|
8150
|
+
function toClassMember(char) {
|
|
8151
|
+
return /[\\\][^-]/u.test(char) ? `\\${char}` : char;
|
|
8152
|
+
}
|
|
8153
|
+
function toLiteralPattern(char) {
|
|
8154
|
+
return char.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`);
|
|
8155
|
+
}
|
|
8156
|
+
//#endregion
|
|
7251
8157
|
//#region src/scope/protected-branch-names.ts
|
|
7252
8158
|
const PROTECTED_BRANCH_NAMES = /* @__PURE__ */ new Set([
|
|
7253
8159
|
"main",
|
|
@@ -7279,7 +8185,7 @@ function findNamingWord(finding, words, context) {
|
|
|
7279
8185
|
if (grant === void 0) return null;
|
|
7280
8186
|
if (grant.kind === "pull-request") return findPullRequestWord(grant.number, words);
|
|
7281
8187
|
if (grant.kind === "path") return findPathWord(finding.target, words, context.home);
|
|
7282
|
-
if (PROTECTED_BRANCH_NAMES.has(finding.target) || finding.target === context.defaultBranch) return null;
|
|
8188
|
+
if (PROTECTED_BRANCH_NAMES.has(finding.target) || finding.target === context.defaultBranch || isProtectedBranch(finding.target, context.protectedBranches ?? [])) return null;
|
|
7283
8189
|
return words.find((word) => word === finding.target) ?? null;
|
|
7284
8190
|
}
|
|
7285
8191
|
const PULL_REQUEST_NOUNS = /^(?:pr|pull|request)$/iu;
|
|
@@ -7924,10 +8830,19 @@ const VERDICT = /<verdict>\s*(?<verdict>[^<]*?)\s*<\/verdict>/giu;
|
|
|
7924
8830
|
const REASON$1 = /<reason>(?<reason>[\s\S]*?)<\/reason>/giu;
|
|
7925
8831
|
const BASIS = /<basis>\s*(?<basis>[^<]*?)\s*<\/basis>/giu;
|
|
7926
8832
|
function parseJudgeReply(text) {
|
|
7927
|
-
const
|
|
8833
|
+
const verdictMatches = [...text.matchAll(VERDICT)];
|
|
8834
|
+
const verdicts = verdictMatches.map((match) => match.groups?.["verdict"]?.toLowerCase());
|
|
7928
8835
|
const reasons = [...text.matchAll(REASON$1)].map((match) => match.groups?.["reason"]?.trim());
|
|
8836
|
+
const [verdictMatch] = verdictMatches;
|
|
7929
8837
|
const [verdict] = verdicts;
|
|
7930
8838
|
const [reason] = reasons;
|
|
8839
|
+
if (verdictMatch !== void 0 && verdicts.length === 1 && verdict === "confirm" && reasons.length === 0) {
|
|
8840
|
+
const unclosedReason = findUnclosedReason(text, verdictMatch.index + verdictMatch[0].length);
|
|
8841
|
+
return unclosedReason === void 0 ? { kind: "unreadable" } : {
|
|
8842
|
+
kind: "confirm",
|
|
8843
|
+
reason: unclosedReason
|
|
8844
|
+
};
|
|
8845
|
+
}
|
|
7931
8846
|
if (verdicts.length !== 1 || reasons.length !== 1 || reason === void 0 || reason === "" || verdict !== "confirm" && verdict !== "overturn") return { kind: "unreadable" };
|
|
7932
8847
|
if (verdict === "confirm") return {
|
|
7933
8848
|
kind: "confirm",
|
|
@@ -7942,6 +8857,15 @@ function parseJudgeReply(text) {
|
|
|
7942
8857
|
reason
|
|
7943
8858
|
};
|
|
7944
8859
|
}
|
|
8860
|
+
const REASON_OPENING = /<reason>/giu;
|
|
8861
|
+
const REASON_CLOSING = /<\/reason>/iu;
|
|
8862
|
+
function findUnclosedReason(text, verdictEnd) {
|
|
8863
|
+
const openings = [...text.matchAll(REASON_OPENING)];
|
|
8864
|
+
const [opening] = openings;
|
|
8865
|
+
if (openings.length !== 1 || opening === void 0 || opening.index < verdictEnd || REASON_CLOSING.test(text)) return;
|
|
8866
|
+
const reason = text.slice(opening.index + opening[0].length).trim();
|
|
8867
|
+
return reason === "" ? void 0 : reason;
|
|
8868
|
+
}
|
|
7945
8869
|
//#endregion
|
|
7946
8870
|
//#region src/messages/anthropic-client.ts
|
|
7947
8871
|
async function sendMessage(provider, apiKey, request, signal) {
|
|
@@ -8038,11 +8962,16 @@ async function runClaudeCode(provider, request, env, signal) {
|
|
|
8038
8962
|
cwd: tmpdir(),
|
|
8039
8963
|
env: buildChildEnv(env)
|
|
8040
8964
|
});
|
|
8041
|
-
const
|
|
8965
|
+
const output = parseOutputJSON(result.stdout);
|
|
8966
|
+
const body = CLAUDE_OUTPUT.safeParse(output);
|
|
8042
8967
|
if (result.exitCode !== 0 || !body.success || body.data.is_error || body.data.result === void 0) throw new Error(`claude -p failed with exit code ${String(result.exitCode)}`);
|
|
8968
|
+
const usage = MODEL_USAGE_OUTPUT.safeParse(output).data?.modelUsage;
|
|
8969
|
+
const stops = SAFETY_STOPS_OUTPUT.safeParse(output).data?.safety_stops;
|
|
8043
8970
|
return {
|
|
8044
8971
|
text: body.data.result,
|
|
8045
|
-
model: provider.model
|
|
8972
|
+
model: pickAnsweringModel(usage, provider.model),
|
|
8973
|
+
safetyStops: stops ?? null,
|
|
8974
|
+
fallback: isFallbackReply(usage, stops)
|
|
8046
8975
|
};
|
|
8047
8976
|
}
|
|
8048
8977
|
function buildChildEnv(env) {
|
|
@@ -8061,6 +8990,8 @@ const CLAUDE_OUTPUT = object({
|
|
|
8061
8990
|
is_error: boolean(),
|
|
8062
8991
|
result: string().optional()
|
|
8063
8992
|
});
|
|
8993
|
+
const MODEL_USAGE_OUTPUT = object({ modelUsage: record(string(), object({ outputTokens: number() })) });
|
|
8994
|
+
const SAFETY_STOPS_OUTPUT = object({ safety_stops: number() });
|
|
8064
8995
|
function parseOutputJSON(text) {
|
|
8065
8996
|
try {
|
|
8066
8997
|
return JSON.parse(text);
|
|
@@ -8068,13 +8999,27 @@ function parseOutputJSON(text) {
|
|
|
8068
8999
|
return null;
|
|
8069
9000
|
}
|
|
8070
9001
|
}
|
|
9002
|
+
function pickAnsweringModel(usage, requested) {
|
|
9003
|
+
const [top] = Object.entries(usage ?? {}).filter(([, entry]) => entry.outputTokens > 0).toSorted(([, left], [, right]) => right.outputTokens - left.outputTokens);
|
|
9004
|
+
return top === void 0 ? requested : top[0];
|
|
9005
|
+
}
|
|
9006
|
+
function isFallbackReply(usage, stops) {
|
|
9007
|
+
if (stops !== void 0) return stops > 0;
|
|
9008
|
+
const entries = Object.values(usage ?? {});
|
|
9009
|
+
return entries.some((entry) => entry.outputTokens === 0) && entries.some((entry) => entry.outputTokens > 0);
|
|
9010
|
+
}
|
|
8071
9011
|
//#endregion
|
|
8072
9012
|
//#region src/judge/send-judge-message.ts
|
|
8073
9013
|
async function sendJudgeMessage(provider, request, transport, signal) {
|
|
8074
|
-
if (provider.protocol === "claude-code") return
|
|
9014
|
+
if (provider.protocol === "claude-code") return runClaudeCode(provider, request, transport.env, signal);
|
|
8075
9015
|
if (provider.protocol === "system-one") throw new Error("Jev returns typed answers and cannot write a judge reason");
|
|
8076
9016
|
if (transport.apiKey === null) throw new Error("no API key for the judge");
|
|
8077
|
-
return
|
|
9017
|
+
return {
|
|
9018
|
+
text: (await sendMessage(provider, transport.apiKey, request, signal)).text,
|
|
9019
|
+
model: provider.model,
|
|
9020
|
+
safetyStops: null,
|
|
9021
|
+
fallback: false
|
|
9022
|
+
};
|
|
8078
9023
|
}
|
|
8079
9024
|
//#endregion
|
|
8080
9025
|
//#region src/judge/classify-with-judge.ts
|
|
@@ -8083,6 +9028,7 @@ async function classifyWithJudge(payload, evidence, judge, options) {
|
|
|
8083
9028
|
const now = options.now ?? Date.now;
|
|
8084
9029
|
const overturns = evidence.containment === void 0 ? options.overturns : "consent";
|
|
8085
9030
|
let failure = null;
|
|
9031
|
+
let answered = null;
|
|
8086
9032
|
let verdict;
|
|
8087
9033
|
try {
|
|
8088
9034
|
failure = "credential";
|
|
@@ -8109,7 +9055,7 @@ async function classifyWithJudge(payload, evidence, judge, options) {
|
|
|
8109
9055
|
const timer = (options.timeout ?? ((ms) => AbortSignal.timeout(ms)))(toTimerDelay(Math.min(judge.timeoutMs, remainingMs)));
|
|
8110
9056
|
const signal = options.signal === void 0 ? timer : AbortSignal.any([timer, options.signal]);
|
|
8111
9057
|
failure = "request";
|
|
8112
|
-
|
|
9058
|
+
answered = await sendJudgeMessage(judge, {
|
|
8113
9059
|
system,
|
|
8114
9060
|
user
|
|
8115
9061
|
}, {
|
|
@@ -8118,7 +9064,8 @@ async function classifyWithJudge(payload, evidence, judge, options) {
|
|
|
8118
9064
|
}, signal).catch((error) => {
|
|
8119
9065
|
if (signal.aborted) failure = "timeout";
|
|
8120
9066
|
throw error;
|
|
8121
|
-
})
|
|
9067
|
+
});
|
|
9068
|
+
const reply = parseJudgeReply(answered.text);
|
|
8122
9069
|
failure = reply.kind === "unreadable" ? "unreadable" : null;
|
|
8123
9070
|
verdict = buildJudgedVerdict(evidence.rule, evidence.basis, reply, overturns, evidence.containment?.reason);
|
|
8124
9071
|
} catch {
|
|
@@ -8131,6 +9078,8 @@ async function classifyWithJudge(payload, evidence, judge, options) {
|
|
|
8131
9078
|
failureReason: failure,
|
|
8132
9079
|
overturnBasis: verdict.overturnBasis,
|
|
8133
9080
|
model: judge.model,
|
|
9081
|
+
answeringModel: answered?.model ?? null,
|
|
9082
|
+
safetyStops: answered?.safetyStops ?? null,
|
|
8134
9083
|
rule: evidence.rule.source === "shipped" ? evidence.rule.name : evidence.rule.source,
|
|
8135
9084
|
tier: evidence.rule.tier,
|
|
8136
9085
|
elapsedMs: Math.round(performance.now() - start)
|
|
@@ -8268,6 +9217,21 @@ function buildTaskContext(context) {
|
|
|
8268
9217
|
return result;
|
|
8269
9218
|
}
|
|
8270
9219
|
//#endregion
|
|
9220
|
+
//#region src/shell/split-git-command.ts
|
|
9221
|
+
function splitGitCommand(args, directory, home) {
|
|
9222
|
+
let gitDirectory = directory;
|
|
9223
|
+
let index = 0;
|
|
9224
|
+
while (index < args.length && args[index]?.startsWith("-") === true) {
|
|
9225
|
+
const option = args[index];
|
|
9226
|
+
if (option === "-C") gitDirectory = resolveShellPath(gitDirectory, args[index + 1] ?? "", home);
|
|
9227
|
+
index += option === "-C" || option === "-c" ? 2 : 1;
|
|
9228
|
+
}
|
|
9229
|
+
return {
|
|
9230
|
+
directory: gitDirectory,
|
|
9231
|
+
args: args.slice(index)
|
|
9232
|
+
};
|
|
9233
|
+
}
|
|
9234
|
+
//#endregion
|
|
8271
9235
|
//#region src/model/collect-hook-bypasses.ts
|
|
8272
9236
|
function collectHookBypasses(command) {
|
|
8273
9237
|
return splitShellCommand(command).segments.flatMap((segment) => collectSegmentBypasses(segment.text));
|
|
@@ -8308,6 +9272,7 @@ const COMMIT_VALUED_LONG = /* @__PURE__ */ new Set([
|
|
|
8308
9272
|
"file",
|
|
8309
9273
|
"fixup",
|
|
8310
9274
|
"message",
|
|
9275
|
+
"pathspec-from-file",
|
|
8311
9276
|
"reedit-message",
|
|
8312
9277
|
"reuse-message",
|
|
8313
9278
|
"squash",
|
|
@@ -8323,7 +9288,7 @@ function findNoVerifyOption(args, subcommand) {
|
|
|
8323
9288
|
const [option = "", ...value] = arg.slice(2).split("=");
|
|
8324
9289
|
if (option.startsWith("no-ve") && "no-verify".startsWith(option)) found = arg;
|
|
8325
9290
|
else if (option.startsWith("veri") && "verify".startsWith(option)) found = null;
|
|
8326
|
-
if (subcommand === "commit" && value.length === 0 && COMMIT_VALUED_LONG.has(option)) index += 1;
|
|
9291
|
+
if (subcommand === "commit" && value.length === 0 && COMMIT_VALUED_LONG.has(findCommitOption(option) ?? "")) index += 1;
|
|
8327
9292
|
} else if (subcommand === "commit" && arg.startsWith("-") && arg !== "-") {
|
|
8328
9293
|
const cluster = parseCommitCluster(arg);
|
|
8329
9294
|
if (cluster.hasNoVerify) found = arg;
|
|
@@ -8333,6 +9298,42 @@ function findNoVerifyOption(args, subcommand) {
|
|
|
8333
9298
|
}
|
|
8334
9299
|
return found;
|
|
8335
9300
|
}
|
|
9301
|
+
const COMMIT_FLAG_LONG = /* @__PURE__ */ new Set([
|
|
9302
|
+
"ahead-behind",
|
|
9303
|
+
"all",
|
|
9304
|
+
"allow-empty",
|
|
9305
|
+
"allow-empty-message",
|
|
9306
|
+
"amend",
|
|
9307
|
+
"branch",
|
|
9308
|
+
"dry-run",
|
|
9309
|
+
"edit",
|
|
9310
|
+
"gpg-sign",
|
|
9311
|
+
"include",
|
|
9312
|
+
"interactive",
|
|
9313
|
+
"long",
|
|
9314
|
+
"no-post-rewrite",
|
|
9315
|
+
"no-verify",
|
|
9316
|
+
"null",
|
|
9317
|
+
"only",
|
|
9318
|
+
"patch",
|
|
9319
|
+
"pathspec-file-nul",
|
|
9320
|
+
"porcelain",
|
|
9321
|
+
"post-rewrite",
|
|
9322
|
+
"quiet",
|
|
9323
|
+
"reset-author",
|
|
9324
|
+
"short",
|
|
9325
|
+
"signoff",
|
|
9326
|
+
"status",
|
|
9327
|
+
"untracked-files",
|
|
9328
|
+
"verbose",
|
|
9329
|
+
"verify"
|
|
9330
|
+
]);
|
|
9331
|
+
function findCommitOption(option) {
|
|
9332
|
+
const names = [...COMMIT_VALUED_LONG, ...COMMIT_FLAG_LONG];
|
|
9333
|
+
if (names.includes(option)) return option;
|
|
9334
|
+
const matches = names.filter((name) => name.startsWith(option));
|
|
9335
|
+
return matches.length === 1 ? matches[0] ?? null : null;
|
|
9336
|
+
}
|
|
8336
9337
|
function parseCommitCluster(arg) {
|
|
8337
9338
|
let hasNoVerify = false;
|
|
8338
9339
|
for (let index = 1; index < arg.length; index += 1) {
|
|
@@ -9211,7 +10212,7 @@ function mergeScopeFacts(all) {
|
|
|
9211
10212
|
//#region src/scope/build-task-scope.ts
|
|
9212
10213
|
function buildTaskScope(input) {
|
|
9213
10214
|
const merged = mergeScopeFacts(input.facts);
|
|
9214
|
-
const branches = merged.branches.filter((branch) => branch !== input.defaultBranch && !(input.defaultBranch === null && PROTECTED_BRANCH_NAMES.has(branch)));
|
|
10215
|
+
const branches = merged.branches.filter((branch) => branch !== input.defaultBranch && !(input.defaultBranch === null && PROTECTED_BRANCH_NAMES.has(branch)) && !isProtectedBranch(branch, input.protectedBranches ?? []));
|
|
9215
10216
|
const repositories = new Set(input.remotes.map((remote) => toRepositorySlug(remote.url)));
|
|
9216
10217
|
const pullRequests = merged.pullRequests.filter((pull) => branches.includes(pull.head) && repositories.has(pull.repository)).map((pull) => ({
|
|
9217
10218
|
number: pull.number,
|
|
@@ -9428,6 +10429,7 @@ async function loadTaskScope(request, sources, host, stderr) {
|
|
|
9428
10429
|
home: host.home,
|
|
9429
10430
|
currentBranch: branch,
|
|
9430
10431
|
defaultBranch: facts.head?.defaultBranch ?? null,
|
|
10432
|
+
protectedBranches: request.protectedBranches,
|
|
9431
10433
|
remotes: facts.remotes,
|
|
9432
10434
|
facts: sourceFacts
|
|
9433
10435
|
});
|
|
@@ -9616,7 +10618,8 @@ async function classifyUserNamedGrant(request, config, options, facts) {
|
|
|
9616
10618
|
if (judge === null || message === null || options.localOnly === true) return null;
|
|
9617
10619
|
const targets = findUserNamedGrant(containment.findings, message, {
|
|
9618
10620
|
home: facts.scope.home,
|
|
9619
|
-
defaultBranch: facts.checkout?.head?.defaultBranch ?? null
|
|
10621
|
+
defaultBranch: facts.checkout?.head?.defaultBranch ?? null,
|
|
10622
|
+
protectedBranches: config.protectedBranches
|
|
9620
10623
|
});
|
|
9621
10624
|
if (targets === null) return null;
|
|
9622
10625
|
const text = await readContainmentRule().catch(() => null);
|
|
@@ -9645,7 +10648,8 @@ async function tryLoadTaskScope(request, config, host, stderr) {
|
|
|
9645
10648
|
return await loadTaskScope({
|
|
9646
10649
|
...request,
|
|
9647
10650
|
checkout: request.checkout ?? void 0,
|
|
9648
|
-
stateDir: resolveXDGDir(host, "state")
|
|
10651
|
+
stateDir: resolveXDGDir(host, "state"),
|
|
10652
|
+
protectedBranches: config.protectedBranches
|
|
9649
10653
|
}, config.scopeSources ?? DEFAULT_SCOPE_SOURCES, host, stderr);
|
|
9650
10654
|
} catch {
|
|
9651
10655
|
return null;
|
|
@@ -9694,268 +10698,6 @@ async function tryUpdateDenialBudget(stderr, path, plan) {
|
|
|
9694
10698
|
}
|
|
9695
10699
|
}
|
|
9696
10700
|
//#endregion
|
|
9697
|
-
//#region src/scope/read-pull-request.ts
|
|
9698
|
-
const LOOKUP_TIMEOUT_MS = 5e3;
|
|
9699
|
-
const MAX_LOOKUP_OUTPUT_BYTES = 65536;
|
|
9700
|
-
async function readPullRequest(repository, number, options = {}) {
|
|
9701
|
-
const [file, ...prefix] = options.command ?? ["gh"];
|
|
9702
|
-
const args = [
|
|
9703
|
-
"pr",
|
|
9704
|
-
"view",
|
|
9705
|
-
String(number),
|
|
9706
|
-
"--repo",
|
|
9707
|
-
repository,
|
|
9708
|
-
"--json",
|
|
9709
|
-
"headRefName,createdAt"
|
|
9710
|
-
];
|
|
9711
|
-
const result = await runCapped(file, [
|
|
9712
|
-
...prefix,
|
|
9713
|
-
...args,
|
|
9714
|
-
"--jq",
|
|
9715
|
-
"[.headRefName, .createdAt] | @tsv"
|
|
9716
|
-
], {
|
|
9717
|
-
signal: AbortSignal.timeout(options.timeoutMs ?? LOOKUP_TIMEOUT_MS),
|
|
9718
|
-
maxOutputBytes: MAX_LOOKUP_OUTPUT_BYTES
|
|
9719
|
-
});
|
|
9720
|
-
const [head = "", created = ""] = result.stdout.trim().split(" ");
|
|
9721
|
-
const createdAt = Date.parse(created);
|
|
9722
|
-
return result.exitCode === 0 && head !== "" && !Number.isNaN(createdAt) ? {
|
|
9723
|
-
head,
|
|
9724
|
-
createdAt
|
|
9725
|
-
} : null;
|
|
9726
|
-
}
|
|
9727
|
-
//#endregion
|
|
9728
|
-
//#region src/git/read-branch-created-at.ts
|
|
9729
|
-
async function readBranchCreatedAt(commonDir, branch) {
|
|
9730
|
-
if (!/^[\w./-]+$/u.test(branch) || branch.split("/").includes("..")) return null;
|
|
9731
|
-
const [first = ""] = (await readFile(join(commonDir, "logs", "refs", "heads", branch), "utf8").catch(() => "")).split("\n");
|
|
9732
|
-
const seconds = /^0+ [\da-f]+ .* (?<time>\d+) [+-]\d{4}\t/u.exec(first)?.groups?.["time"];
|
|
9733
|
-
return seconds === void 0 ? null : Number(seconds) * 1e3;
|
|
9734
|
-
}
|
|
9735
|
-
//#endregion
|
|
9736
|
-
//#region src/scope/collect-pull-request-addresses.ts
|
|
9737
|
-
function collectPullRequestAddresses(text) {
|
|
9738
|
-
const addresses = /* @__PURE__ */ new Map();
|
|
9739
|
-
for (const match of text.matchAll(PULL_REQUEST_URL)) {
|
|
9740
|
-
const repository = toRepositorySlug(`https://${match.groups?.["repository"] ?? ""}`);
|
|
9741
|
-
const number = Number(match.groups?.["number"]);
|
|
9742
|
-
if (repository !== null) addresses.set(`${repository}#${String(number)}`, {
|
|
9743
|
-
repository,
|
|
9744
|
-
number
|
|
9745
|
-
});
|
|
9746
|
-
}
|
|
9747
|
-
return [...addresses.values()];
|
|
9748
|
-
}
|
|
9749
|
-
const PULL_REQUEST_URL = /https:\/\/(?<repository>[^/\s]+\/[^/\s]+\/[^/\s]+)\/pull\/(?<number>[1-9]\d*)\b/gu;
|
|
9750
|
-
//#endregion
|
|
9751
|
-
//#region mods/auto-mode/contract/scope-commands.ts
|
|
9752
|
-
const SCOPE_COMMANDS = {
|
|
9753
|
-
git: [
|
|
9754
|
-
"worktree add",
|
|
9755
|
-
"checkout",
|
|
9756
|
-
"switch",
|
|
9757
|
-
"branch"
|
|
9758
|
-
],
|
|
9759
|
-
gh: ["pr create"]
|
|
9760
|
-
};
|
|
9761
|
-
//#endregion
|
|
9762
|
-
//#region src/scope/collect-scope-events.ts
|
|
9763
|
-
function collectScopeEvents(command, cwd, home) {
|
|
9764
|
-
return collectShellCommands(command, cwd, home).flatMap((entry) => {
|
|
9765
|
-
const [name, ...args] = entry.words;
|
|
9766
|
-
const directory = entry.directory;
|
|
9767
|
-
if (directory === null) return [];
|
|
9768
|
-
if (name === "git") return collectGitEvents(args, directory, home);
|
|
9769
|
-
return name === "gh" && findScopeCommand(SCOPE_COMMANDS.gh, args) !== null ? [{
|
|
9770
|
-
kind: "pull-request",
|
|
9771
|
-
directory,
|
|
9772
|
-
head: findOptionValue(args, ["--head", "-H"])
|
|
9773
|
-
}] : [];
|
|
9774
|
-
});
|
|
9775
|
-
}
|
|
9776
|
-
function collectGitEvents(args, cwd, home) {
|
|
9777
|
-
const git = splitGitCommand(args, cwd, home);
|
|
9778
|
-
const found = findScopeCommand(SCOPE_COMMANDS.git, git.args);
|
|
9779
|
-
if (git.directory === null || found === null) return [];
|
|
9780
|
-
return GIT_EVENT_COLLECTORS[found.command](found.rest, git.directory, home);
|
|
9781
|
-
}
|
|
9782
|
-
function findScopeCommand(commands, args) {
|
|
9783
|
-
for (const command of commands) {
|
|
9784
|
-
const words = command.split(" ");
|
|
9785
|
-
if (words.every((word, index) => args[index] === word)) return {
|
|
9786
|
-
command,
|
|
9787
|
-
rest: args.slice(words.length)
|
|
9788
|
-
};
|
|
9789
|
-
}
|
|
9790
|
-
return null;
|
|
9791
|
-
}
|
|
9792
|
-
const GIT_EVENT_COLLECTORS = {
|
|
9793
|
-
"worktree add": collectWorktreeEvents,
|
|
9794
|
-
checkout: collectSwitchEvents,
|
|
9795
|
-
switch: collectSwitchEvents,
|
|
9796
|
-
branch: collectBranchEvents
|
|
9797
|
-
};
|
|
9798
|
-
function collectWorktreeEvents(rest, directory, home) {
|
|
9799
|
-
const path = findWorktreePath(rest);
|
|
9800
|
-
const resolved = path === null ? null : resolveShellPath(directory, path, home);
|
|
9801
|
-
return resolved === null ? [] : [{
|
|
9802
|
-
kind: "worktree",
|
|
9803
|
-
path: resolved
|
|
9804
|
-
}];
|
|
9805
|
-
}
|
|
9806
|
-
const WORKTREE_VALUE_OPTIONS = /* @__PURE__ */ new Set([
|
|
9807
|
-
"-b",
|
|
9808
|
-
"-B",
|
|
9809
|
-
"--reason"
|
|
9810
|
-
]);
|
|
9811
|
-
function findWorktreePath(args) {
|
|
9812
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
9813
|
-
const word = args[index] ?? "";
|
|
9814
|
-
if (WORKTREE_VALUE_OPTIONS.has(word)) index += 1;
|
|
9815
|
-
else if (!word.startsWith("-")) return word;
|
|
9816
|
-
}
|
|
9817
|
-
return null;
|
|
9818
|
-
}
|
|
9819
|
-
const BRANCH_CREATE_OPTIONS = /* @__PURE__ */ new Set([
|
|
9820
|
-
"-b",
|
|
9821
|
-
"-B",
|
|
9822
|
-
"-c",
|
|
9823
|
-
"-C",
|
|
9824
|
-
"--create",
|
|
9825
|
-
"--force-create"
|
|
9826
|
-
]);
|
|
9827
|
-
const BRANCH_OTHER_OPTIONS = /^-(?:d|D|m|M|c|C|l|a|r|v|vv|u)$|^--(?:delete|move|copy|list|all|remotes|verbose|show-current|contains|no-contains|merged|no-merged|points-at|format|sort|set-upstream-to|unset-upstream|edit-description)/u;
|
|
9828
|
-
function collectSwitchEvents(rest, directory) {
|
|
9829
|
-
const name = findOptionValue(rest, [...BRANCH_CREATE_OPTIONS]);
|
|
9830
|
-
return name === null ? [] : [{
|
|
9831
|
-
kind: "branch",
|
|
9832
|
-
name,
|
|
9833
|
-
directory
|
|
9834
|
-
}];
|
|
9835
|
-
}
|
|
9836
|
-
function collectBranchEvents(rest, directory) {
|
|
9837
|
-
if (rest.some((word) => BRANCH_OTHER_OPTIONS.test(word))) return [];
|
|
9838
|
-
const name = rest.find((word) => !word.startsWith("-"));
|
|
9839
|
-
return name === void 0 || isUnresolvedWord(name) ? [] : [{
|
|
9840
|
-
kind: "branch",
|
|
9841
|
-
name,
|
|
9842
|
-
directory
|
|
9843
|
-
}];
|
|
9844
|
-
}
|
|
9845
|
-
function findOptionValue(args, options) {
|
|
9846
|
-
const index = args.findIndex((word) => options.includes(word));
|
|
9847
|
-
const value = index === -1 ? void 0 : args[index + 1];
|
|
9848
|
-
return value === void 0 || isUnresolvedWord(value) ? null : value;
|
|
9849
|
-
}
|
|
9850
|
-
//#endregion
|
|
9851
|
-
//#region src/scope/merge-session-scope.ts
|
|
9852
|
-
function mergeSessionScope(all) {
|
|
9853
|
-
const branches = /* @__PURE__ */ new Map();
|
|
9854
|
-
const pullRequests = /* @__PURE__ */ new Map();
|
|
9855
|
-
for (const branch of all.flatMap((scope) => scope.branches)) branches.set(`${branch.commonDir}\0${branch.name}`, branch);
|
|
9856
|
-
for (const pull of all.flatMap((scope) => scope.pullRequests)) pullRequests.set(`${pull.repository}#${String(pull.number)}`, pull);
|
|
9857
|
-
return {
|
|
9858
|
-
worktrees: [...new Set(all.flatMap((scope) => scope.worktrees))],
|
|
9859
|
-
branches: [...branches.values()],
|
|
9860
|
-
pullRequests: [...pullRequests.values()]
|
|
9861
|
-
};
|
|
9862
|
-
}
|
|
9863
|
-
//#endregion
|
|
9864
|
-
//#region src/scope/write-session-scope.ts
|
|
9865
|
-
async function writeSessionScope(path, added, clock) {
|
|
9866
|
-
await updateJSONFile(path, loadSessionScope, (current) => mergeSessionScope([current, added]), clock);
|
|
9867
|
-
}
|
|
9868
|
-
//#endregion
|
|
9869
|
-
//#region src/scope/update-session-scope.ts
|
|
9870
|
-
const MAX_CALL_AGE_MS = 9e5;
|
|
9871
|
-
async function updateSessionScope(request, options) {
|
|
9872
|
-
const age = options.now - request.startedAt;
|
|
9873
|
-
if (age < -6e4 || age > MAX_CALL_AGE_MS) return EMPTY_SESSION_SCOPE;
|
|
9874
|
-
const events = collectScopeEvents(request.command, request.cwd, options.home);
|
|
9875
|
-
const scope = mergeSessionScope(await Promise.all(events.map((event) => tryVerifyScopeEvent(event, request, options))));
|
|
9876
|
-
if (scope.worktrees.length + scope.branches.length + scope.pullRequests.length > 0) await writeSessionScope(resolveSessionScopePath(options.stateDir, request.sessionID), scope, options.lockClock);
|
|
9877
|
-
return scope;
|
|
9878
|
-
}
|
|
9879
|
-
async function tryVerifyScopeEvent(event, request, options) {
|
|
9880
|
-
try {
|
|
9881
|
-
return await (options.verifyEvent === void 0 ? verifyScopeEvent(event, request, options) : options.verifyEvent(event, request));
|
|
9882
|
-
} catch {
|
|
9883
|
-
return EMPTY_SESSION_SCOPE;
|
|
9884
|
-
}
|
|
9885
|
-
}
|
|
9886
|
-
const FORGE_CLOCK_SKEW_MS = 6e4;
|
|
9887
|
-
const FILE_CLOCK_SLACK_MS = 50;
|
|
9888
|
-
function verifyScopeEvent(event, request, options) {
|
|
9889
|
-
return M(event).returnType().with({ kind: "worktree" }, (worktree) => verifyWorktreeEvent(worktree.path, request, options)).with({ kind: "branch" }, (branch) => verifyBranchEvent(branch, request, options)).with({ kind: "pull-request" }, (pull) => verifyPullRequestEvent(pull.directory, request, options)).exhaustive();
|
|
9890
|
-
}
|
|
9891
|
-
async function verifyWorktreeEvent(path, request, options) {
|
|
9892
|
-
const link = await stat(join(path, ".git"));
|
|
9893
|
-
const facts = await loadCheckoutFacts(path, options.env);
|
|
9894
|
-
const checkout = facts.checkout;
|
|
9895
|
-
if (!link.isFile() || link.mtimeMs < request.startedAt - FILE_CLOCK_SLACK_MS || checkout?.worktree !== path) return EMPTY_SESSION_SCOPE;
|
|
9896
|
-
const branch = facts.head?.branch ?? null;
|
|
9897
|
-
if (branch === null) return {
|
|
9898
|
-
...EMPTY_SESSION_SCOPE,
|
|
9899
|
-
worktrees: [path]
|
|
9900
|
-
};
|
|
9901
|
-
const isNew = await isBranchCreatedSince(checkout.commonDir, branch, request.startedAt);
|
|
9902
|
-
return {
|
|
9903
|
-
...EMPTY_SESSION_SCOPE,
|
|
9904
|
-
worktrees: [path],
|
|
9905
|
-
branches: isNew ? [{
|
|
9906
|
-
name: branch,
|
|
9907
|
-
commonDir: checkout.commonDir
|
|
9908
|
-
}] : []
|
|
9909
|
-
};
|
|
9910
|
-
}
|
|
9911
|
-
async function verifyBranchEvent(event, request, options) {
|
|
9912
|
-
const checkout = await findCheckout(event.directory, options.env);
|
|
9913
|
-
if (checkout === null) return EMPTY_SESSION_SCOPE;
|
|
9914
|
-
return await isBranchCreatedSince(checkout.commonDir, event.name, request.startedAt) ? {
|
|
9915
|
-
...EMPTY_SESSION_SCOPE,
|
|
9916
|
-
branches: [{
|
|
9917
|
-
name: event.name,
|
|
9918
|
-
commonDir: checkout.commonDir
|
|
9919
|
-
}]
|
|
9920
|
-
} : EMPTY_SESSION_SCOPE;
|
|
9921
|
-
}
|
|
9922
|
-
async function verifyPullRequestEvent(directory, request, options) {
|
|
9923
|
-
const facts = await loadCheckoutFacts(directory, options.env);
|
|
9924
|
-
if (facts.checkout === null) return EMPTY_SESSION_SCOPE;
|
|
9925
|
-
const repositories = new Set(facts.remotes.map((remote) => toRepositorySlug(remote.url)));
|
|
9926
|
-
const addresses = collectPullRequestAddresses(request.resultText).filter((address) => repositories.has(address.repository));
|
|
9927
|
-
const pulls = await Promise.all(addresses.map(async (address) => {
|
|
9928
|
-
const pull = await options.readPullRequest(address.repository, address.number);
|
|
9929
|
-
return pull !== null && pull.createdAt >= request.startedAt - FORGE_CLOCK_SKEW_MS ? [{
|
|
9930
|
-
...address,
|
|
9931
|
-
head: pull.head
|
|
9932
|
-
}] : [];
|
|
9933
|
-
}));
|
|
9934
|
-
return {
|
|
9935
|
-
...EMPTY_SESSION_SCOPE,
|
|
9936
|
-
pullRequests: pulls.flat()
|
|
9937
|
-
};
|
|
9938
|
-
}
|
|
9939
|
-
async function isBranchCreatedSince(commonDir, branch, since) {
|
|
9940
|
-
const createdAt = await readBranchCreatedAt(commonDir, branch);
|
|
9941
|
-
if (createdAt === null || createdAt < Math.floor(since / 1e3) * 1e3) return false;
|
|
9942
|
-
const ref = await stat(join(commonDir, "refs", "heads", branch)).catch(() => null);
|
|
9943
|
-
return ref !== null && ref.mtimeMs >= since - FILE_CLOCK_SLACK_MS;
|
|
9944
|
-
}
|
|
9945
|
-
//#endregion
|
|
9946
|
-
//#region src/record-scope.ts
|
|
9947
|
-
async function recordScope(request, config, options = {}) {
|
|
9948
|
-
const host = options.host ?? readHostEnvironment();
|
|
9949
|
-
if (!Object.values(config.scopeSources ?? DEFAULT_SCOPE_SOURCES).some((source) => source.kind === "session")) return EMPTY_SESSION_SCOPE;
|
|
9950
|
-
return await updateSessionScope(request, {
|
|
9951
|
-
now: options.now ?? Date.now(),
|
|
9952
|
-
stateDir: resolveXDGDir(host, "state"),
|
|
9953
|
-
home: host.home,
|
|
9954
|
-
env: host.env,
|
|
9955
|
-
readPullRequest
|
|
9956
|
-
});
|
|
9957
|
-
}
|
|
9958
|
-
//#endregion
|
|
9959
10701
|
//#region src/request/action-request-schema.ts
|
|
9960
10702
|
const userTaskSchema = strictObject({
|
|
9961
10703
|
text: string(),
|
|
@@ -10011,28 +10753,4 @@ function parseActionRequest(body) {
|
|
|
10011
10753
|
};
|
|
10012
10754
|
}
|
|
10013
10755
|
//#endregion
|
|
10014
|
-
|
|
10015
|
-
const scopeRecordRequestSchema = strictObject({
|
|
10016
|
-
sessionID: string().min(1),
|
|
10017
|
-
cwd: string().min(1),
|
|
10018
|
-
startedAt: number().int().positive(),
|
|
10019
|
-
command: string().min(1),
|
|
10020
|
-
resultText: string()
|
|
10021
|
-
});
|
|
10022
|
-
//#endregion
|
|
10023
|
-
//#region src/request/parse-scope-record-request.ts
|
|
10024
|
-
function parseScopeRecordRequest(body) {
|
|
10025
|
-
const parsed = scopeRecordRequestSchema.safeParse(body);
|
|
10026
|
-
return parsed.success ? parsed.data : null;
|
|
10027
|
-
}
|
|
10028
|
-
//#endregion
|
|
10029
|
-
//#region src/request/render-verdict.ts
|
|
10030
|
-
function renderVerdict(verdict) {
|
|
10031
|
-
const rendered = M(verdict).returnType().with({ kind: "allow" }, () => ({ decision: "allow" })).with({ kind: "deny" }, (denied) => ({
|
|
10032
|
-
decision: "deny",
|
|
10033
|
-
reason: `[${denied.rule}] ${denied.reason}`
|
|
10034
|
-
})).exhaustive();
|
|
10035
|
-
return JSON.stringify(rendered);
|
|
10036
|
-
}
|
|
10037
|
-
//#endregion
|
|
10038
|
-
export { readHostEnvironment as C, resolveConfigPath as S, resolveXDGDir as T, tryClassifyEdit as _, classifyAction as a, loadConfig as b, classifyWithModel as c, DecisionRequestError as d, buildDecisionRequest as f, loadClaudeSettings as g, checkContainment as h, recordScope as i, parseModelVerdict as l, writeActionDiagnostic as m, parseScopeRecordRequest as n, buildTaskScope as o, loadPolicy as p, parseActionRequest as r, classifyLocally as s, renderVerdict as t, sendDecision as u, findGitEntry as v, PRESETS as w, resolveApiKey as x, DEFAULT_CONFIG as y };
|
|
10756
|
+
export { resolveXDGDir as C, string as D, strictObject as E, CLI_LIMITS as S, number as T, findGitEntry as _, EMPTY_SESSION_SCOPE as a, readHostEnvironment as b, loadPolicy as c, writeActionDiagnostic as d, toRepositorySlug as f, findCheckout as g, isUnresolvedWord as h, loadSessionScope as i, resolveShippedPolicyDir as l, resolveShellPath as m, classifyAction as n, splitGitCommand as o, collectShellCommands as p, resolveSessionScopePath as r, M as s, parseActionRequest as t, loadCheckoutFacts as u, DEFAULT_SCOPE_SOURCES as v, updateJSONFile as w, runCapped as x, loadConfig as y };
|