auto-mode 0.18.0 → 0.19.1
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 +48 -78
- package/dist/cli.js +60 -590
- package/dist/index.d.ts +4536 -219
- package/dist/index.js +2 -2
- package/dist/{render-verdict-lM31QV2u.js → render-verdict-aWHhUzvw.js} +1285 -579
- package/mods/auto-mode/contract/types.ts +1 -1
- package/package.json +10 -9
- package/policy/rules.md +27 -15
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { t as getEditFields } from "./get-edit-fields-BtAvHIsq.js";
|
|
2
|
-
import { lstat, readFile, realpath, stat } from "node:fs/promises";
|
|
2
|
+
import { appendFile, lstat, mkdir, open, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, isAbsolute, join, matchesGlob, normalize, parse, relative, resolve } from "node:path";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { homedir, tmpdir } from "node:os";
|
|
6
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
+
import { setTimeout } from "node:timers/promises";
|
|
6
8
|
import { existsSync } from "node:fs";
|
|
7
|
-
import { createHash } from "node:crypto";
|
|
8
9
|
//#region node_modules/.bun/zod@4.6.2/node_modules/zod/v4/core/util.js
|
|
9
10
|
function getEnumValues(entries) {
|
|
10
11
|
const numericValues = Object.values(entries).filter((v) => typeof v === "number");
|
|
@@ -5529,6 +5530,24 @@ function superRefine(fn, params) {
|
|
|
5529
5530
|
return /* @__PURE__ */ _superRefine(fn, params);
|
|
5530
5531
|
}
|
|
5531
5532
|
//#endregion
|
|
5533
|
+
//#region src/state/resolve-xdg-dir.ts
|
|
5534
|
+
const BASE_DIRS = {
|
|
5535
|
+
config: {
|
|
5536
|
+
variable: "XDG_CONFIG_HOME",
|
|
5537
|
+
fallback: [".config"]
|
|
5538
|
+
},
|
|
5539
|
+
state: {
|
|
5540
|
+
variable: "XDG_STATE_HOME",
|
|
5541
|
+
fallback: [".local", "state"]
|
|
5542
|
+
}
|
|
5543
|
+
};
|
|
5544
|
+
function resolveXDGDir(host, kind) {
|
|
5545
|
+
const dir = BASE_DIRS[kind];
|
|
5546
|
+
const base = host.env[dir.variable];
|
|
5547
|
+
const root = base === void 0 || base === "" ? join(host.home, ...dir.fallback) : base;
|
|
5548
|
+
return join(root, "auto-mode");
|
|
5549
|
+
}
|
|
5550
|
+
//#endregion
|
|
5532
5551
|
//#region src/config/presets.ts
|
|
5533
5552
|
const MESSAGES_DEFAULTS = {
|
|
5534
5553
|
protocol: "messages",
|
|
@@ -5580,40 +5599,36 @@ function findPreset(id) {
|
|
|
5580
5599
|
return Object.hasOwn(PRESETS, id) ? PRESETS[id] : void 0;
|
|
5581
5600
|
}
|
|
5582
5601
|
//#endregion
|
|
5583
|
-
//#region src/
|
|
5584
|
-
function
|
|
5585
|
-
return Math.max(1, Math.ceil(ms));
|
|
5586
|
-
}
|
|
5587
|
-
//#endregion
|
|
5588
|
-
//#region src/config/read-api-key-from-command.ts
|
|
5589
|
-
function readApiKeyFromCommand(command, options = {}) {
|
|
5590
|
-
const now = options.now ?? Date.now;
|
|
5591
|
-
const remainingMs = options.deadlineAt === void 0 ? 5e3 : options.deadlineAt - now();
|
|
5592
|
-
if (remainingMs <= 0 || options.signal?.aborted === true) return Promise.resolve(null);
|
|
5602
|
+
//#region src/process/run-capped.ts
|
|
5603
|
+
function runCapped(file, args, options) {
|
|
5593
5604
|
return new Promise((resolve) => {
|
|
5594
5605
|
const grouped = process.platform !== "win32";
|
|
5595
|
-
const child = spawn(
|
|
5606
|
+
const child = spawn(file, args, {
|
|
5596
5607
|
detached: grouped,
|
|
5597
5608
|
stdio: [
|
|
5598
|
-
"ignore",
|
|
5609
|
+
options.input === void 0 ? "ignore" : "pipe",
|
|
5599
5610
|
"pipe",
|
|
5600
5611
|
"ignore"
|
|
5601
5612
|
],
|
|
5602
|
-
...options.
|
|
5603
|
-
|
|
5604
|
-
...options.host.env
|
|
5605
|
-
} }
|
|
5613
|
+
...options.cwd === void 0 ? {} : { cwd: options.cwd },
|
|
5614
|
+
...options.env === void 0 ? {} : { env: options.env }
|
|
5606
5615
|
});
|
|
5607
|
-
|
|
5616
|
+
const chunks = [];
|
|
5617
|
+
let size = 0;
|
|
5608
5618
|
let settled = false;
|
|
5609
5619
|
let stopped = false;
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5620
|
+
let exited;
|
|
5621
|
+
const setResult = (exitCode, failedToStart) => {
|
|
5622
|
+
if (settled) return;
|
|
5623
|
+
settled = true;
|
|
5624
|
+
options.signal.removeEventListener("abort", stopChild);
|
|
5625
|
+
if (stopped) child.stdout?.destroy();
|
|
5626
|
+
resolve({
|
|
5627
|
+
exitCode,
|
|
5628
|
+
stdout: stopped || failedToStart ? "" : Buffer.concat(chunks).toString("utf8"),
|
|
5629
|
+
stopped,
|
|
5630
|
+
failedToStart
|
|
5631
|
+
});
|
|
5617
5632
|
};
|
|
5618
5633
|
const stopChild = () => {
|
|
5619
5634
|
if (stopped) return;
|
|
@@ -5624,28 +5639,57 @@ function readApiKeyFromCommand(command, options = {}) {
|
|
|
5624
5639
|
} catch {
|
|
5625
5640
|
child.kill("SIGKILL");
|
|
5626
5641
|
}
|
|
5642
|
+
if (exited !== void 0) setResult(exited, false);
|
|
5627
5643
|
};
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
if (timer.aborted) stopChild();
|
|
5632
|
-
child.stdout.setEncoding("utf8");
|
|
5633
|
-
child.stdout.on("data", (chunk) => {
|
|
5644
|
+
options.signal.addEventListener("abort", stopChild, { once: true });
|
|
5645
|
+
if (options.signal.aborted) stopChild();
|
|
5646
|
+
child.stdout?.on("data", (chunk) => {
|
|
5634
5647
|
if (stopped) return;
|
|
5635
|
-
|
|
5636
|
-
|
|
5648
|
+
chunks.push(chunk);
|
|
5649
|
+
size += chunk.length;
|
|
5650
|
+
if (size > options.maxOutputBytes) stopChild();
|
|
5637
5651
|
});
|
|
5652
|
+
child.stdin?.on("error", () => {});
|
|
5653
|
+
child.stdin?.end(options.input);
|
|
5638
5654
|
child.on("error", () => {
|
|
5639
|
-
setResult(null);
|
|
5655
|
+
setResult(null, !stopped);
|
|
5656
|
+
});
|
|
5657
|
+
child.on("exit", (code) => {
|
|
5658
|
+
exited = code;
|
|
5659
|
+
if (stopped) setResult(code, false);
|
|
5640
5660
|
});
|
|
5641
5661
|
child.on("close", (code) => {
|
|
5642
|
-
|
|
5643
|
-
setResult(key);
|
|
5662
|
+
setResult(code, false);
|
|
5644
5663
|
});
|
|
5645
5664
|
});
|
|
5646
5665
|
}
|
|
5647
5666
|
//#endregion
|
|
5648
|
-
//#region src/
|
|
5667
|
+
//#region src/config/to-timer-delay.ts
|
|
5668
|
+
function toTimerDelay(ms) {
|
|
5669
|
+
return Math.max(1, Math.ceil(ms));
|
|
5670
|
+
}
|
|
5671
|
+
//#endregion
|
|
5672
|
+
//#region src/config/read-api-key-from-command.ts
|
|
5673
|
+
const KEY_COMMAND_TIMEOUT_MS = 5e3;
|
|
5674
|
+
const MAX_KEY_OUTPUT_BYTES = 65536;
|
|
5675
|
+
async function readApiKeyFromCommand(command, options = {}) {
|
|
5676
|
+
const now = options.now ?? Date.now;
|
|
5677
|
+
const remainingMs = options.deadlineAt === void 0 ? KEY_COMMAND_TIMEOUT_MS : options.deadlineAt - now();
|
|
5678
|
+
if (remainingMs <= 0 || options.signal?.aborted === true) return null;
|
|
5679
|
+
const timer = (options.timeout ?? ((ms) => AbortSignal.timeout(ms)))(toTimerDelay(Math.min(KEY_COMMAND_TIMEOUT_MS, remainingMs)));
|
|
5680
|
+
const result = await runCapped("/bin/sh", ["-c", command], {
|
|
5681
|
+
signal: options.signal === void 0 ? timer : AbortSignal.any([timer, options.signal]),
|
|
5682
|
+
maxOutputBytes: MAX_KEY_OUTPUT_BYTES,
|
|
5683
|
+
env: options.host === void 0 ? void 0 : {
|
|
5684
|
+
HOME: options.host.home,
|
|
5685
|
+
...options.host.env
|
|
5686
|
+
}
|
|
5687
|
+
});
|
|
5688
|
+
const key = result.stdout.trim();
|
|
5689
|
+
return !result.stopped && result.exitCode === 0 && key !== "" ? key : null;
|
|
5690
|
+
}
|
|
5691
|
+
//#endregion
|
|
5692
|
+
//#region src/config/scratch-paths.ts
|
|
5649
5693
|
const SCRATCH_PATHS = [
|
|
5650
5694
|
"/tmp",
|
|
5651
5695
|
"/dev/null",
|
|
@@ -5684,16 +5728,14 @@ const DEFAULT_SCOPE_SOURCES = {
|
|
|
5684
5728
|
const DEFAULT_CONFIG = {
|
|
5685
5729
|
provider: DEFAULT_PROVIDER,
|
|
5686
5730
|
judge: DEFAULT_JUDGE,
|
|
5687
|
-
judgeOverturns: "
|
|
5731
|
+
judgeOverturns: "any",
|
|
5688
5732
|
onFailure: "defer",
|
|
5689
5733
|
blockThreshold: .2,
|
|
5690
5734
|
denialBudget: DEFAULT_DENIAL_BUDGET,
|
|
5691
5735
|
scopeSources: DEFAULT_SCOPE_SOURCES
|
|
5692
5736
|
};
|
|
5693
5737
|
function resolveConfigPath(host = readHostEnvironment()) {
|
|
5694
|
-
|
|
5695
|
-
const base = xdg !== void 0 && xdg !== "" ? xdg : join(host.home, ".config");
|
|
5696
|
-
return join(base, "auto-mode", "config.json");
|
|
5738
|
+
return join(resolveXDGDir(host, "config"), "config.json");
|
|
5697
5739
|
}
|
|
5698
5740
|
async function loadConfig(configPath, host = readHostEnvironment()) {
|
|
5699
5741
|
const path = configPath ?? resolveConfigPath(host);
|
|
@@ -5758,7 +5800,7 @@ function buildConfig(json, path, home) {
|
|
|
5758
5800
|
return {
|
|
5759
5801
|
provider,
|
|
5760
5802
|
judge,
|
|
5761
|
-
judgeOverturns: decision.judgeOverturns ?? "
|
|
5803
|
+
judgeOverturns: decision.judgeOverturns ?? "any",
|
|
5762
5804
|
scopeSources: configuredSources ?? DEFAULT_SCOPE_SOURCES,
|
|
5763
5805
|
classifierPath: expandHomePath(policy.frameworkPath ?? void 0, home),
|
|
5764
5806
|
rulesPath: expandHomePath(policy.rulesPath ?? void 0, home),
|
|
@@ -5884,26 +5926,44 @@ function resolveApiKey(provider, options = {}) {
|
|
|
5884
5926
|
});
|
|
5885
5927
|
}
|
|
5886
5928
|
//#endregion
|
|
5887
|
-
//#region src/
|
|
5929
|
+
//#region src/git/find-git-entry.ts
|
|
5930
|
+
async function findGitEntry(start, stopDir = parse(start).root) {
|
|
5931
|
+
for (let directory = start;; directory = dirname(directory)) {
|
|
5932
|
+
const gitPath = join(directory, ".git");
|
|
5933
|
+
if (await hasEntry(gitPath)) return {
|
|
5934
|
+
directory,
|
|
5935
|
+
gitPath
|
|
5936
|
+
};
|
|
5937
|
+
if (dirname(directory) === directory || directory === stopDir) return null;
|
|
5938
|
+
}
|
|
5939
|
+
}
|
|
5940
|
+
const ABSENT_CODES = /* @__PURE__ */ new Set(["ENOENT", "ENOTDIR"]);
|
|
5941
|
+
async function hasEntry(path) {
|
|
5942
|
+
try {
|
|
5943
|
+
await lstat(path);
|
|
5944
|
+
return true;
|
|
5945
|
+
} catch (error) {
|
|
5946
|
+
return !(error instanceof Error && "code" in error && ABSENT_CODES.has(String(error.code)));
|
|
5947
|
+
}
|
|
5948
|
+
}
|
|
5949
|
+
//#endregion
|
|
5950
|
+
//#region src/git/find-checkout.ts
|
|
5888
5951
|
const GIT_OVERRIDES$1 = [
|
|
5889
5952
|
"GIT_DIR",
|
|
5890
5953
|
"GIT_WORK_TREE",
|
|
5891
5954
|
"GIT_COMMON_DIR"
|
|
5892
5955
|
];
|
|
5893
|
-
async function findCheckout(start, env, stopDir
|
|
5956
|
+
async function findCheckout(start, env, stopDir) {
|
|
5894
5957
|
if (GIT_OVERRIDES$1.some((name) => env[name] !== void 0)) return null;
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
}
|
|
5905
|
-
if (dirname(directory) === directory || directory === stopDir) return null;
|
|
5906
|
-
}
|
|
5958
|
+
const entry = await findGitEntry(start, stopDir);
|
|
5959
|
+
const info = entry === null ? null : await stat(entry.gitPath).catch(() => null);
|
|
5960
|
+
if (entry === null || info === null) return null;
|
|
5961
|
+
const gitDir = info.isDirectory() ? entry.gitPath : await findLinkedGitDir(entry.directory, entry.gitPath);
|
|
5962
|
+
return gitDir === null ? null : {
|
|
5963
|
+
worktree: entry.directory,
|
|
5964
|
+
gitDir,
|
|
5965
|
+
commonDir: await findCommonDir(gitDir)
|
|
5966
|
+
};
|
|
5907
5967
|
}
|
|
5908
5968
|
async function findLinkedGitDir(directory, gitPath) {
|
|
5909
5969
|
const link = await readFile(gitPath, "utf8").catch(() => "");
|
|
@@ -5915,13 +5975,6 @@ async function findCommonDir(gitDir) {
|
|
|
5915
5975
|
return common === null ? gitDir : resolve(gitDir, common.trim());
|
|
5916
5976
|
}
|
|
5917
5977
|
//#endregion
|
|
5918
|
-
//#region src/state/resolve-state-dir.ts
|
|
5919
|
-
function resolveStateDir(host) {
|
|
5920
|
-
const stateHome = host.env["XDG_STATE_HOME"];
|
|
5921
|
-
const base = stateHome === void 0 || stateHome === "" ? join(host.home, ".local", "state") : stateHome;
|
|
5922
|
-
return join(base, "auto-mode");
|
|
5923
|
-
}
|
|
5924
|
-
//#endregion
|
|
5925
5978
|
//#region src/bypass/resolve-edit-target.ts
|
|
5926
5979
|
async function resolveEditTarget(path) {
|
|
5927
5980
|
const missing = [];
|
|
@@ -5945,12 +5998,16 @@ const GIT_OVERRIDES = [
|
|
|
5945
5998
|
"GIT_WORK_TREE",
|
|
5946
5999
|
"GIT_COMMON_DIR"
|
|
5947
6000
|
];
|
|
5948
|
-
async function tryClassifyEdit(request, scope, host,
|
|
6001
|
+
async function tryClassifyEdit(request, scope, host, reader = {}) {
|
|
6002
|
+
const files = {
|
|
6003
|
+
...DISK_EDIT_FILE_READER,
|
|
6004
|
+
...reader
|
|
6005
|
+
};
|
|
5949
6006
|
const fields = getEditFields(request.toolName);
|
|
5950
6007
|
const path = fields === null ? void 0 : request.toolInput[fields.path];
|
|
5951
6008
|
if (typeof path !== "string" || path === "") return null;
|
|
5952
6009
|
try {
|
|
5953
|
-
const ownDirs = [dirname(resolveConfigPath(host)),
|
|
6010
|
+
const ownDirs = [dirname(resolveConfigPath(host)), resolveXDGDir(host, "state")];
|
|
5954
6011
|
const requested = resolve(request.cwd, path);
|
|
5955
6012
|
const [target, worktrees, protectedDirs] = await Promise.all([
|
|
5956
6013
|
files.resolveEditTarget(requested),
|
|
@@ -5975,11 +6032,192 @@ async function tryClassifyEdit(request, scope, host, files = DISK_EDIT_FILE_READ
|
|
|
5975
6032
|
}
|
|
5976
6033
|
}
|
|
5977
6034
|
//#endregion
|
|
6035
|
+
//#region src/budget/build-retry-key.ts
|
|
6036
|
+
const DESCRIPTIVE_FIELDS = { Bash: [
|
|
6037
|
+
"description",
|
|
6038
|
+
"timeout",
|
|
6039
|
+
"run_in_background"
|
|
6040
|
+
] };
|
|
6041
|
+
function buildRetryKey(request) {
|
|
6042
|
+
const ignored = Object.hasOwn(DESCRIPTIVE_FIELDS, request.toolName) ? DESCRIPTIVE_FIELDS[request.toolName] ?? [] : [];
|
|
6043
|
+
const input = Object.fromEntries(Object.entries(request.toolInput).filter(([key]) => !ignored.includes(key)));
|
|
6044
|
+
const message = request.decisionContext?.lastDirectUserMessage ?? null;
|
|
6045
|
+
const canonical = JSON.stringify(sortKeys({
|
|
6046
|
+
cwd: request.cwd,
|
|
6047
|
+
toolName: request.toolName,
|
|
6048
|
+
toolInput: input,
|
|
6049
|
+
lastDirectUserMessage: message?.text ?? null,
|
|
6050
|
+
...message?.freshness === "stale" ? { lastDirectUserMessageFreshness: "stale" } : {}
|
|
6051
|
+
}));
|
|
6052
|
+
return createHash("sha256").update(canonical).digest("hex");
|
|
6053
|
+
}
|
|
6054
|
+
function sortKeys(value) {
|
|
6055
|
+
if (Array.isArray(value)) return value.map((item) => sortKeys(item));
|
|
6056
|
+
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)]));
|
|
6057
|
+
return value;
|
|
6058
|
+
}
|
|
6059
|
+
//#endregion
|
|
6060
|
+
//#region src/budget/find-retry-deny.ts
|
|
6061
|
+
function findRetryDeny(state, retryKey) {
|
|
6062
|
+
if (state.lastDenied === null || state.lastDenied.retryKey !== retryKey) return null;
|
|
6063
|
+
return {
|
|
6064
|
+
kind: "deny",
|
|
6065
|
+
rule: state.lastDenied.rule,
|
|
6066
|
+
reason: state.lastDenied.reason
|
|
6067
|
+
};
|
|
6068
|
+
}
|
|
6069
|
+
//#endregion
|
|
6070
|
+
//#region src/state/load-json-file.ts
|
|
6071
|
+
async function loadJSONFile(path, schema, fallback) {
|
|
6072
|
+
try {
|
|
6073
|
+
const text = await readFile(path, "utf8");
|
|
6074
|
+
const parsed = schema.safeParse(JSON.parse(text));
|
|
6075
|
+
return parsed.success ? parsed.data : fallback;
|
|
6076
|
+
} catch {
|
|
6077
|
+
return fallback;
|
|
6078
|
+
}
|
|
6079
|
+
}
|
|
6080
|
+
//#endregion
|
|
6081
|
+
//#region src/budget/types.ts
|
|
6082
|
+
const EMPTY_DENIAL_STATE = {
|
|
6083
|
+
consecutive: 0,
|
|
6084
|
+
session: 0,
|
|
6085
|
+
lastDenied: null
|
|
6086
|
+
};
|
|
6087
|
+
//#endregion
|
|
6088
|
+
//#region src/budget/load-denial-state.ts
|
|
6089
|
+
const count = number().int().nonnegative();
|
|
6090
|
+
const stateSchema$1 = strictObject({
|
|
6091
|
+
consecutive: count,
|
|
6092
|
+
session: count,
|
|
6093
|
+
lastDenied: strictObject({
|
|
6094
|
+
retryKey: string().min(1),
|
|
6095
|
+
rule: string(),
|
|
6096
|
+
reason: string()
|
|
6097
|
+
}).nullable()
|
|
6098
|
+
});
|
|
6099
|
+
function loadDenialState(path) {
|
|
6100
|
+
return loadJSONFile(path, stateSchema$1, EMPTY_DENIAL_STATE);
|
|
6101
|
+
}
|
|
6102
|
+
//#endregion
|
|
6103
|
+
//#region src/budget/plan-denial-budget.ts
|
|
6104
|
+
function planDenialBudget(state, verdict, retryKey, budget) {
|
|
6105
|
+
if (verdict === null) return {
|
|
6106
|
+
verdict: null,
|
|
6107
|
+
state,
|
|
6108
|
+
escalation: false
|
|
6109
|
+
};
|
|
6110
|
+
if (verdict.kind === "allow") return {
|
|
6111
|
+
verdict,
|
|
6112
|
+
state: {
|
|
6113
|
+
...state,
|
|
6114
|
+
consecutive: 0,
|
|
6115
|
+
lastDenied: null
|
|
6116
|
+
},
|
|
6117
|
+
escalation: false
|
|
6118
|
+
};
|
|
6119
|
+
if (state.consecutive >= budget.consecutive || state.session >= budget.perSession) return {
|
|
6120
|
+
verdict: null,
|
|
6121
|
+
state: EMPTY_DENIAL_STATE,
|
|
6122
|
+
escalation: true
|
|
6123
|
+
};
|
|
6124
|
+
const next = {
|
|
6125
|
+
consecutive: state.consecutive + 1,
|
|
6126
|
+
session: state.session + 1,
|
|
6127
|
+
lastDenied: {
|
|
6128
|
+
retryKey,
|
|
6129
|
+
rule: verdict.rule,
|
|
6130
|
+
reason: verdict.reason
|
|
6131
|
+
}
|
|
6132
|
+
};
|
|
6133
|
+
const left = Math.min(budget.consecutive - next.consecutive, budget.perSession - next.session);
|
|
6134
|
+
return {
|
|
6135
|
+
verdict: {
|
|
6136
|
+
...verdict,
|
|
6137
|
+
reason: `${verdict.reason} ${formatDenialsLeft(left)}`
|
|
6138
|
+
},
|
|
6139
|
+
state: next,
|
|
6140
|
+
escalation: false
|
|
6141
|
+
};
|
|
6142
|
+
}
|
|
6143
|
+
function formatDenialsLeft(left) {
|
|
6144
|
+
if (left > 0) return `Denials left before auto-mode asks the user: ${left}.`;
|
|
6145
|
+
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.";
|
|
6146
|
+
}
|
|
6147
|
+
//#endregion
|
|
6148
|
+
//#region src/budget/resolve-denial-state-path.ts
|
|
6149
|
+
function resolveDenialStatePath(request, stateDir) {
|
|
6150
|
+
const agentID = request.decisionContext?.agentID ?? "";
|
|
6151
|
+
const key = createHash("sha256").update(`${request.sessionID}\0${agentID}`).digest("hex").slice(0, 32);
|
|
6152
|
+
return join(stateDir, "denials", `${key}.json`);
|
|
6153
|
+
}
|
|
6154
|
+
//#endregion
|
|
6155
|
+
//#region src/state/update-json-file.ts
|
|
6156
|
+
const SYSTEM_CLOCK = {
|
|
6157
|
+
now: Date.now,
|
|
6158
|
+
wait: (ms) => setTimeout(ms)
|
|
6159
|
+
};
|
|
6160
|
+
async function updateJSONFile(path, load, update, clock = SYSTEM_CLOCK) {
|
|
6161
|
+
await mkdir(dirname(path), {
|
|
6162
|
+
recursive: true,
|
|
6163
|
+
mode: 448
|
|
6164
|
+
});
|
|
6165
|
+
const lock = `${path}.lock`;
|
|
6166
|
+
await claimLock(lock, clock);
|
|
6167
|
+
try {
|
|
6168
|
+
const previous = await load(path);
|
|
6169
|
+
const next = update(previous);
|
|
6170
|
+
if (next !== previous) {
|
|
6171
|
+
const staged = `${path}.${randomUUID()}.tmp`;
|
|
6172
|
+
await writeFile(staged, `${JSON.stringify(next)}\n`, { mode: 384 });
|
|
6173
|
+
await rename(staged, path);
|
|
6174
|
+
}
|
|
6175
|
+
return {
|
|
6176
|
+
previous,
|
|
6177
|
+
next
|
|
6178
|
+
};
|
|
6179
|
+
} finally {
|
|
6180
|
+
await rm(lock, { force: true });
|
|
6181
|
+
}
|
|
6182
|
+
}
|
|
6183
|
+
const STALE_LOCK_MS = 1e4;
|
|
6184
|
+
const LOCK_WAIT_MS = 3e3;
|
|
6185
|
+
const LOCK_POLL_MS = 10;
|
|
6186
|
+
async function claimLock(lock, clock) {
|
|
6187
|
+
const deadline = clock.now() + LOCK_WAIT_MS;
|
|
6188
|
+
for (;;) {
|
|
6189
|
+
try {
|
|
6190
|
+
await (await open(lock, "wx", 384)).close();
|
|
6191
|
+
return;
|
|
6192
|
+
} catch (error) {
|
|
6193
|
+
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
|
|
6194
|
+
}
|
|
6195
|
+
const held = await stat(lock).catch(() => null);
|
|
6196
|
+
if (held !== null && clock.now() - held.mtimeMs > STALE_LOCK_MS) await rm(lock, { force: true });
|
|
6197
|
+
else if (clock.now() > deadline) throw new Error("state file lock unavailable");
|
|
6198
|
+
else await clock.wait(LOCK_POLL_MS);
|
|
6199
|
+
}
|
|
6200
|
+
}
|
|
6201
|
+
//#endregion
|
|
6202
|
+
//#region src/budget/update-denial-state.ts
|
|
6203
|
+
function updateDenialState(path, update, clock) {
|
|
6204
|
+
return updateJSONFile(path, loadDenialState, update, clock);
|
|
6205
|
+
}
|
|
6206
|
+
//#endregion
|
|
6207
|
+
//#region src/config/resolve-claude-config-path.ts
|
|
6208
|
+
const DEFAULT_DIRS = {
|
|
6209
|
+
"settings.json": [".claude"],
|
|
6210
|
+
".claude.json": []
|
|
6211
|
+
};
|
|
6212
|
+
function resolveClaudeConfigPath(host, file) {
|
|
6213
|
+
const configDir = host.env["CLAUDE_CONFIG_DIR"];
|
|
6214
|
+
const dir = configDir === void 0 || configDir === "" ? join(host.home, ...DEFAULT_DIRS[file]) : configDir;
|
|
6215
|
+
return join(dir, file);
|
|
6216
|
+
}
|
|
6217
|
+
//#endregion
|
|
5978
6218
|
//#region src/config/load-claude-settings.ts
|
|
5979
6219
|
async function loadClaudeSettings(path, host) {
|
|
5980
|
-
const
|
|
5981
|
-
const base = configDir === void 0 || configDir === "" ? join(host.home, ".claude") : configDir;
|
|
5982
|
-
const userPath = join(base, "settings.json");
|
|
6220
|
+
const userPath = resolveClaudeConfigPath(host, "settings.json");
|
|
5983
6221
|
if (path === void 0) {
|
|
5984
6222
|
const body = await readSettings(userPath);
|
|
5985
6223
|
return {
|
|
@@ -6032,108 +6270,259 @@ function parseRules(body) {
|
|
|
6032
6270
|
};
|
|
6033
6271
|
}
|
|
6034
6272
|
//#endregion
|
|
6035
|
-
//#region src/
|
|
6036
|
-
function
|
|
6037
|
-
const
|
|
6273
|
+
//#region src/git/to-repository-slug.ts
|
|
6274
|
+
function toRepositorySlug(reference) {
|
|
6275
|
+
const match = /^(?:[a-z+]+:\/\/)?(?:[^@/]+@)?(?<host>[^/:]+)[:/](?<owner>[^/]+)\/(?<name>[^/]+?)(?:\.git)?\/?$/iu.exec(reference);
|
|
6276
|
+
if (match?.groups === void 0) return null;
|
|
6277
|
+
const parts = match.groups;
|
|
6278
|
+
return `${parts["host"] ?? ""}/${parts["owner"] ?? ""}/${parts["name"] ?? ""}`.toLowerCase();
|
|
6279
|
+
}
|
|
6280
|
+
//#endregion
|
|
6281
|
+
//#region src/shell/is-unresolved-word.ts
|
|
6282
|
+
function isUnresolvedWord(word) {
|
|
6283
|
+
return /[$`]/u.test(word);
|
|
6284
|
+
}
|
|
6285
|
+
//#endregion
|
|
6286
|
+
//#region src/shell/resolve-shell-path.ts
|
|
6287
|
+
function resolveShellPath(directory, path, home) {
|
|
6288
|
+
const expanded = path.replace(/^(?:~|\$HOME|\$\{HOME\})(?=\/|$)/u, home);
|
|
6289
|
+
if (isUnresolvedWord(expanded) || expanded.startsWith("~") || expanded === "") return null;
|
|
6290
|
+
if (directory === null && !isAbsolute(expanded)) return null;
|
|
6291
|
+
const absolute = isAbsolute(expanded) || directory === null ? expanded : join(directory, expanded);
|
|
6292
|
+
return normalize(absolute).replace(/(?<=.)\/$/u, "");
|
|
6293
|
+
}
|
|
6294
|
+
//#endregion
|
|
6295
|
+
//#region src/shell/split-shell-tokens.ts
|
|
6296
|
+
function splitShellTokens(text) {
|
|
6297
|
+
const tokens = [];
|
|
6298
|
+
let hasSubstitution = false;
|
|
6038
6299
|
let index = 0;
|
|
6039
|
-
while (index <
|
|
6040
|
-
const
|
|
6041
|
-
if (
|
|
6042
|
-
else if (word === "env") {
|
|
6300
|
+
while (index < text.length) {
|
|
6301
|
+
const ch = text.charAt(index);
|
|
6302
|
+
if (/\s/u.test(ch) && ch !== "\n") {
|
|
6043
6303
|
index += 1;
|
|
6044
|
-
|
|
6045
|
-
}
|
|
6046
|
-
|
|
6304
|
+
continue;
|
|
6305
|
+
}
|
|
6306
|
+
const separator = findOperator(SEPARATORS, text, index);
|
|
6307
|
+
if (separator !== void 0 && !text.startsWith("&>", index)) {
|
|
6308
|
+
tokens.push({
|
|
6309
|
+
kind: "separator",
|
|
6310
|
+
operator: separator,
|
|
6311
|
+
start: index,
|
|
6312
|
+
end: index + separator.length
|
|
6313
|
+
});
|
|
6314
|
+
index += separator.length;
|
|
6315
|
+
continue;
|
|
6316
|
+
}
|
|
6317
|
+
const word = readWord(text, index);
|
|
6318
|
+
hasSubstitution ||= word.hasSubstitution;
|
|
6319
|
+
const operator = findOperator(REDIRECTS, text, word.end);
|
|
6320
|
+
const isDescriptor = operator !== void 0 && /^\d+$/u.test(word.raw);
|
|
6321
|
+
if (word.raw !== "" && !isDescriptor) {
|
|
6322
|
+
if (word.isUnterminated) return {
|
|
6323
|
+
tokens: [...tokens, buildWordToken(word, index)],
|
|
6324
|
+
hasSubstitution,
|
|
6325
|
+
isUnterminated: true
|
|
6326
|
+
};
|
|
6327
|
+
tokens.push(buildWordToken(word, index));
|
|
6328
|
+
index = word.end;
|
|
6329
|
+
continue;
|
|
6330
|
+
}
|
|
6331
|
+
if (operator === void 0) {
|
|
6332
|
+
index += 1;
|
|
6333
|
+
continue;
|
|
6334
|
+
}
|
|
6335
|
+
const redirect = readRedirect(text, word.end, operator);
|
|
6336
|
+
hasSubstitution ||= redirect.hasSubstitution;
|
|
6337
|
+
tokens.push({
|
|
6338
|
+
kind: "redirect",
|
|
6339
|
+
operator: `${word.raw}${operator}`,
|
|
6340
|
+
target: redirect.target,
|
|
6341
|
+
start: index,
|
|
6342
|
+
end: redirect.end
|
|
6343
|
+
});
|
|
6344
|
+
if (redirect.isUnterminated) return {
|
|
6345
|
+
tokens,
|
|
6346
|
+
hasSubstitution,
|
|
6347
|
+
isUnterminated: true
|
|
6348
|
+
};
|
|
6349
|
+
index = redirect.end;
|
|
6047
6350
|
}
|
|
6048
|
-
return
|
|
6351
|
+
return {
|
|
6352
|
+
tokens,
|
|
6353
|
+
hasSubstitution,
|
|
6354
|
+
isUnterminated: false
|
|
6355
|
+
};
|
|
6356
|
+
}
|
|
6357
|
+
const SEPARATORS = [
|
|
6358
|
+
"&&",
|
|
6359
|
+
"||",
|
|
6360
|
+
";",
|
|
6361
|
+
"|",
|
|
6362
|
+
"&",
|
|
6363
|
+
"\n"
|
|
6364
|
+
];
|
|
6365
|
+
const REDIRECTS = [
|
|
6366
|
+
"&>>",
|
|
6367
|
+
"&>",
|
|
6368
|
+
"<<<",
|
|
6369
|
+
"<<-",
|
|
6370
|
+
"<<",
|
|
6371
|
+
"<&",
|
|
6372
|
+
"<>",
|
|
6373
|
+
"<(",
|
|
6374
|
+
"<",
|
|
6375
|
+
">>",
|
|
6376
|
+
">|",
|
|
6377
|
+
">&",
|
|
6378
|
+
">(",
|
|
6379
|
+
">"
|
|
6380
|
+
];
|
|
6381
|
+
function findOperator(operators, text, index) {
|
|
6382
|
+
return operators.find((operator) => text.startsWith(operator, index));
|
|
6049
6383
|
}
|
|
6050
|
-
function
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
} else if (ch === "'" || ch === "\"") {
|
|
6059
|
-
quote = ch;
|
|
6060
|
-
started = true;
|
|
6061
|
-
} else if (/\s/u.test(ch)) {
|
|
6062
|
-
if (started) words.push(current);
|
|
6063
|
-
current = "";
|
|
6064
|
-
started = false;
|
|
6065
|
-
} else {
|
|
6066
|
-
current += ch;
|
|
6067
|
-
started = true;
|
|
6068
|
-
}
|
|
6069
|
-
if (started) words.push(current);
|
|
6070
|
-
return words;
|
|
6384
|
+
function buildWordToken(word, start) {
|
|
6385
|
+
return {
|
|
6386
|
+
kind: "word",
|
|
6387
|
+
value: word.value,
|
|
6388
|
+
raw: word.raw,
|
|
6389
|
+
start,
|
|
6390
|
+
end: word.end
|
|
6391
|
+
};
|
|
6071
6392
|
}
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
function splitShellCommand(command) {
|
|
6075
|
-
const segments = [];
|
|
6076
|
-
let current = "";
|
|
6393
|
+
function readWord(text, start) {
|
|
6394
|
+
let value = "";
|
|
6077
6395
|
let quote = null;
|
|
6078
|
-
let
|
|
6079
|
-
let
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
current = "";
|
|
6084
|
-
};
|
|
6085
|
-
while (i < command.length) {
|
|
6086
|
-
const ch = command.charAt(i);
|
|
6396
|
+
let hasSubstitution = false;
|
|
6397
|
+
let parenAt = null;
|
|
6398
|
+
let index = start;
|
|
6399
|
+
while (index < text.length) {
|
|
6400
|
+
const ch = text.charAt(index);
|
|
6087
6401
|
if (quote !== null) {
|
|
6088
|
-
if (quote === "\"" && (
|
|
6402
|
+
if (quote === "\"" && (text.startsWith("$(", index) || ch === "`")) hasSubstitution = true;
|
|
6089
6403
|
if (ch === quote) quote = null;
|
|
6090
|
-
|
|
6091
|
-
|
|
6404
|
+
else value += ch;
|
|
6405
|
+
index += 1;
|
|
6092
6406
|
continue;
|
|
6093
6407
|
}
|
|
6094
6408
|
if (ch === "'" || ch === "\"") {
|
|
6095
6409
|
quote = ch;
|
|
6096
|
-
|
|
6097
|
-
i += 1;
|
|
6410
|
+
index += 1;
|
|
6098
6411
|
continue;
|
|
6099
6412
|
}
|
|
6100
6413
|
if (ch === "\\") {
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
continue;
|
|
6104
|
-
}
|
|
6105
|
-
if (command.startsWith("$(", i) || ch === "`" || command.startsWith("<(", i) || ch === ">") {
|
|
6106
|
-
unparsed = true;
|
|
6107
|
-
current += ch;
|
|
6108
|
-
i += 1;
|
|
6414
|
+
value += text.slice(index, index + 2);
|
|
6415
|
+
index += 2;
|
|
6109
6416
|
continue;
|
|
6110
6417
|
}
|
|
6111
|
-
if (
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6418
|
+
if (/\s/u.test(ch) || /[;&|<>]/u.test(ch)) break;
|
|
6419
|
+
if (text.startsWith("$(", index) || ch === "`") hasSubstitution = true;
|
|
6420
|
+
if ((ch === "(" || ch === ")") && parenAt === null) parenAt = value.length;
|
|
6421
|
+
value += ch;
|
|
6422
|
+
index += 1;
|
|
6423
|
+
}
|
|
6424
|
+
return {
|
|
6425
|
+
value,
|
|
6426
|
+
raw: text.slice(start, Math.min(index, text.length)),
|
|
6427
|
+
end: Math.min(index, text.length),
|
|
6428
|
+
parenAt,
|
|
6429
|
+
hasSubstitution,
|
|
6430
|
+
isUnterminated: quote !== null
|
|
6431
|
+
};
|
|
6432
|
+
}
|
|
6433
|
+
function readRedirect(text, start, operator) {
|
|
6434
|
+
let index = start + operator.length;
|
|
6435
|
+
while (index < text.length && /\s/u.test(text.charAt(index)) && text.charAt(index) !== "\n") index += 1;
|
|
6436
|
+
const word = readWord(text, index);
|
|
6437
|
+
const isProcess = operator.endsWith("(");
|
|
6438
|
+
if (word.raw === "") return {
|
|
6439
|
+
target: null,
|
|
6440
|
+
end: index,
|
|
6441
|
+
hasSubstitution: isProcess,
|
|
6442
|
+
isUnterminated: false
|
|
6443
|
+
};
|
|
6444
|
+
return {
|
|
6445
|
+
target: word.parenAt === null ? word.value : word.value.slice(0, word.parenAt),
|
|
6446
|
+
end: word.end,
|
|
6447
|
+
hasSubstitution: isProcess || word.hasSubstitution,
|
|
6448
|
+
isUnterminated: word.isUnterminated
|
|
6449
|
+
};
|
|
6450
|
+
}
|
|
6451
|
+
//#endregion
|
|
6452
|
+
//#region src/shell/split-command-words.ts
|
|
6453
|
+
function splitCommandWords(text) {
|
|
6454
|
+
const words = splitShellTokens(text).tokens.flatMap((token) => token.kind === "word" ? [token.value] : []);
|
|
6455
|
+
let index = 0;
|
|
6456
|
+
while (index < words.length) {
|
|
6457
|
+
const word = words[index] ?? "";
|
|
6458
|
+
if (/^\w+=/u.test(word) || word === "sudo" || word === "command") index += 1;
|
|
6459
|
+
else if (word === "env") {
|
|
6460
|
+
index += 1;
|
|
6461
|
+
while (words[index] === "-u" || /^\w+=/u.test(words[index] ?? "")) index += words[index] === "-u" ? 2 : 1;
|
|
6462
|
+
} else if (word === "timeout") index += 2;
|
|
6463
|
+
else break;
|
|
6464
|
+
}
|
|
6465
|
+
return words.slice(index);
|
|
6466
|
+
}
|
|
6467
|
+
//#endregion
|
|
6468
|
+
//#region src/shell/split-shell-command.ts
|
|
6469
|
+
function splitShellCommand(command) {
|
|
6470
|
+
const scanned = splitShellTokens(command);
|
|
6471
|
+
const segments = [];
|
|
6472
|
+
let start = null;
|
|
6473
|
+
let end = 0;
|
|
6474
|
+
for (const token of [...scanned.tokens, null]) if (token === null || token.kind === "separator") {
|
|
6475
|
+
if (start !== null) segments.push({ text: command.slice(start, end) });
|
|
6476
|
+
start = null;
|
|
6477
|
+
} else {
|
|
6478
|
+
start ??= token.start;
|
|
6479
|
+
end = token.end;
|
|
6123
6480
|
}
|
|
6124
|
-
collectSegment();
|
|
6125
6481
|
return {
|
|
6126
6482
|
segments,
|
|
6127
|
-
hasUnparsedConstruct:
|
|
6483
|
+
hasUnparsedConstruct: scanned.hasSubstitution || scanned.isUnterminated || scanned.tokens.some((token) => token.kind === "redirect" && token.operator.includes(">"))
|
|
6128
6484
|
};
|
|
6129
6485
|
}
|
|
6130
6486
|
//#endregion
|
|
6131
|
-
//#region src/
|
|
6132
|
-
function
|
|
6133
|
-
const
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6487
|
+
//#region src/shell/collect-shell-commands.ts
|
|
6488
|
+
function collectShellCommands(command, cwd, home) {
|
|
6489
|
+
const hasPipe = splitShellTokens(command).tokens.some((token) => token.kind === "separator" && token.operator === "|");
|
|
6490
|
+
let directory = cwd;
|
|
6491
|
+
return splitShellCommand(command).segments.map((segment) => {
|
|
6492
|
+
const words = splitCommandWords(segment.text);
|
|
6493
|
+
const entry = {
|
|
6494
|
+
words,
|
|
6495
|
+
directory,
|
|
6496
|
+
writeTargets: collectWriteTargets(segment.text)
|
|
6497
|
+
};
|
|
6498
|
+
if (words[0] === "cd") directory = hasPipe ? null : resolveShellPath(directory, words[1] ?? home, home);
|
|
6499
|
+
return entry;
|
|
6500
|
+
});
|
|
6501
|
+
}
|
|
6502
|
+
const WRITE_REDIRECTS = /* @__PURE__ */ new Set([
|
|
6503
|
+
">",
|
|
6504
|
+
">>",
|
|
6505
|
+
">|",
|
|
6506
|
+
"&>",
|
|
6507
|
+
"&>>"
|
|
6508
|
+
]);
|
|
6509
|
+
function collectWriteTargets(text) {
|
|
6510
|
+
return splitShellTokens(text).tokens.flatMap((token) => token.kind === "redirect" && token.target !== null && token.target !== "" && WRITE_REDIRECTS.has(token.operator.replace(/^\d+/u, "")) ? [token.target] : []);
|
|
6511
|
+
}
|
|
6512
|
+
//#endregion
|
|
6513
|
+
//#region src/shell/split-git-command.ts
|
|
6514
|
+
function splitGitCommand(args, directory, home) {
|
|
6515
|
+
let gitDirectory = directory;
|
|
6516
|
+
let index = 0;
|
|
6517
|
+
while (index < args.length && args[index]?.startsWith("-") === true) {
|
|
6518
|
+
const option = args[index];
|
|
6519
|
+
if (option === "-C") gitDirectory = resolveShellPath(gitDirectory, args[index + 1] ?? "", home);
|
|
6520
|
+
index += option === "-C" || option === "-c" ? 2 : 1;
|
|
6521
|
+
}
|
|
6522
|
+
return {
|
|
6523
|
+
directory: gitDirectory,
|
|
6524
|
+
args: args.slice(index)
|
|
6525
|
+
};
|
|
6137
6526
|
}
|
|
6138
6527
|
//#endregion
|
|
6139
6528
|
//#region src/containment/collect-scope-findings.ts
|
|
@@ -6154,7 +6543,7 @@ const FILE_WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
|
6154
6543
|
"NotebookEdit"
|
|
6155
6544
|
]);
|
|
6156
6545
|
function collectTargetFindings(directory, target, scope, scratchPaths) {
|
|
6157
|
-
const path =
|
|
6546
|
+
const path = resolveShellPath(directory, target, scope.home);
|
|
6158
6547
|
return path === null ? [] : collectPathFindings(path, scope, scratchPaths);
|
|
6159
6548
|
}
|
|
6160
6549
|
const CREDENTIAL_PATHS = [
|
|
@@ -6204,15 +6593,10 @@ const DESTINATION_WRITERS = /* @__PURE__ */ new Set([
|
|
|
6204
6593
|
]);
|
|
6205
6594
|
function collectCommandFindings(command, cwd, scope, scratchPaths) {
|
|
6206
6595
|
const findings = [];
|
|
6207
|
-
const
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
for (const target of collectRedirectTargets(segment.text)) findings.push(...collectTargetFindings(directory, target, scope, scratchPaths));
|
|
6212
|
-
if (name === void 0) continue;
|
|
6213
|
-
if (name === "cd" && hasPipe) directory = null;
|
|
6214
|
-
else if (name === "cd") directory = args[0] === void 0 ? scope.home : resolvePath(directory, args[0], scope.home);
|
|
6215
|
-
else findings.push(...collectProgramFindings(name, args, directory, cwd, scope, scratchPaths));
|
|
6596
|
+
for (const entry of collectShellCommands(command, cwd, scope.home)) {
|
|
6597
|
+
const [name, ...args] = entry.words;
|
|
6598
|
+
for (const target of entry.writeTargets) findings.push(...collectTargetFindings(entry.directory, target, scope, scratchPaths));
|
|
6599
|
+
if (name !== void 0 && name !== "cd") findings.push(...collectProgramFindings(name, args, entry.directory, cwd, scope, scratchPaths));
|
|
6216
6600
|
}
|
|
6217
6601
|
return findings;
|
|
6218
6602
|
}
|
|
@@ -6358,15 +6742,9 @@ const GIT_DIRECTORY_WRITES = /* @__PURE__ */ new Set([
|
|
|
6358
6742
|
"push"
|
|
6359
6743
|
]);
|
|
6360
6744
|
function collectGitFindings(args, directory, cwd, scope, scratchPaths) {
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
const option = args[index];
|
|
6365
|
-
if (option === "-C") gitDirectory = resolvePath(gitDirectory, args[index + 1] ?? "", scope.home);
|
|
6366
|
-
index += option === "-C" || option === "-c" ? 2 : 1;
|
|
6367
|
-
}
|
|
6368
|
-
const subcommand = args[index] ?? "";
|
|
6369
|
-
const rest = args.slice(index + 1);
|
|
6745
|
+
const git = splitGitCommand(args, directory, scope.home);
|
|
6746
|
+
const gitDirectory = git.directory;
|
|
6747
|
+
const [subcommand = "", ...rest] = git.args;
|
|
6370
6748
|
const operands = rest.filter((word) => !word.startsWith("-"));
|
|
6371
6749
|
const findings = [];
|
|
6372
6750
|
if (gitDirectory !== null && GIT_DIRECTORY_WRITES.has(subcommand) && !isInScope(gitDirectory, scope, scratchPaths)) findings.push({
|
|
@@ -6396,7 +6774,7 @@ function collectPushFindings(rest, operands, currentBranch, scope) {
|
|
|
6396
6774
|
kind: "branch",
|
|
6397
6775
|
target: "--all"
|
|
6398
6776
|
});
|
|
6399
|
-
if (remote !== void 0 && !
|
|
6777
|
+
if (remote !== void 0 && !isUnresolvedWord(remote) && !isOwnedRemote(remote, scope)) findings.push({
|
|
6400
6778
|
kind: "remote-write",
|
|
6401
6779
|
target: remote
|
|
6402
6780
|
});
|
|
@@ -6404,16 +6782,13 @@ function collectPushFindings(rest, operands, currentBranch, scope) {
|
|
|
6404
6782
|
for (const refspec of destinations) {
|
|
6405
6783
|
const named = (refspec.split(":").at(-1) ?? refspec).replace(/^\+/u, "").replace(/^refs\/heads\//u, "");
|
|
6406
6784
|
const branch = named === "HEAD" ? currentBranch : named;
|
|
6407
|
-
if (branch !== null && !
|
|
6785
|
+
if (branch !== null && !isUnresolvedWord(branch) && !branch.startsWith("refs/tags/") && !scope.branches.includes(branch)) findings.push({
|
|
6408
6786
|
kind: "branch",
|
|
6409
6787
|
target: branch
|
|
6410
6788
|
});
|
|
6411
6789
|
}
|
|
6412
6790
|
return findings;
|
|
6413
6791
|
}
|
|
6414
|
-
function isUnresolved(word) {
|
|
6415
|
-
return /[$`]/u.test(word);
|
|
6416
|
-
}
|
|
6417
6792
|
function isOwnedRemote(remote, scope) {
|
|
6418
6793
|
if (scope.remotes.some((entry) => entry.name === remote)) return true;
|
|
6419
6794
|
const slug = toRepositorySlug(remote);
|
|
@@ -6544,49 +6919,6 @@ function findProgramFinding(name, args) {
|
|
|
6544
6919
|
};
|
|
6545
6920
|
return null;
|
|
6546
6921
|
}
|
|
6547
|
-
function collectRedirectTargets(text) {
|
|
6548
|
-
const targets = [];
|
|
6549
|
-
let quote = null;
|
|
6550
|
-
let index = 0;
|
|
6551
|
-
while (index < text.length) {
|
|
6552
|
-
const ch = text.charAt(index);
|
|
6553
|
-
if (quote !== null) {
|
|
6554
|
-
quote = ch === quote ? null : quote;
|
|
6555
|
-
index += 1;
|
|
6556
|
-
} else if (ch === "'" || ch === "\"") {
|
|
6557
|
-
quote = ch;
|
|
6558
|
-
index += 1;
|
|
6559
|
-
} else if (ch === ">" && text.charAt(index - 1) !== "<") {
|
|
6560
|
-
let next = index + 1;
|
|
6561
|
-
if (text.charAt(next) === ">" || text.charAt(next) === "|") next += 1;
|
|
6562
|
-
if (text.charAt(next) === "&") {
|
|
6563
|
-
index = next + 1;
|
|
6564
|
-
continue;
|
|
6565
|
-
}
|
|
6566
|
-
const [target, end] = readWord(text, next);
|
|
6567
|
-
if (target !== "") targets.push(target);
|
|
6568
|
-
index = end;
|
|
6569
|
-
} else index += 1;
|
|
6570
|
-
}
|
|
6571
|
-
return targets;
|
|
6572
|
-
}
|
|
6573
|
-
function readWord(text, start) {
|
|
6574
|
-
let index = start;
|
|
6575
|
-
while (/\s/u.test(text.charAt(index))) index += 1;
|
|
6576
|
-
let word = "";
|
|
6577
|
-
let quote = null;
|
|
6578
|
-
while (index < text.length) {
|
|
6579
|
-
const ch = text.charAt(index);
|
|
6580
|
-
if (quote !== null) {
|
|
6581
|
-
if (ch === quote) quote = null;
|
|
6582
|
-
else word += ch;
|
|
6583
|
-
} else if (ch === "'" || ch === "\"") quote = ch;
|
|
6584
|
-
else if (/[\s;&|<>()]/u.test(ch)) break;
|
|
6585
|
-
else word += ch;
|
|
6586
|
-
index += 1;
|
|
6587
|
-
}
|
|
6588
|
-
return [word, index];
|
|
6589
|
-
}
|
|
6590
6922
|
function collectInPlaceFiles(args) {
|
|
6591
6923
|
const files = [];
|
|
6592
6924
|
let skipNext = false;
|
|
@@ -6601,13 +6933,6 @@ function collectInPlaceFiles(args) {
|
|
|
6601
6933
|
}
|
|
6602
6934
|
return files;
|
|
6603
6935
|
}
|
|
6604
|
-
function resolvePath(cwd, path, home) {
|
|
6605
|
-
const expanded = path.replace(/^(?:~|\$HOME|\$\{HOME\})(?=\/|$)/u, home);
|
|
6606
|
-
if (/[$`]|^~/u.test(expanded) || expanded === "") return null;
|
|
6607
|
-
if (cwd === null && !isAbsolute(expanded)) return null;
|
|
6608
|
-
const absolute = isAbsolute(expanded) || cwd === null ? expanded : join(cwd, expanded);
|
|
6609
|
-
return normalize(absolute).replace(/(?<=.)\/$/u, "");
|
|
6610
|
-
}
|
|
6611
6936
|
function isInScope(path, scope, scratchPaths) {
|
|
6612
6937
|
if (scratchPaths.some((entry) => isUnder(path, entry)) || scope.pathGlobs.some((glob) => matchesGlob(path, glob))) return true;
|
|
6613
6938
|
return scope.worktrees.some((worktree) => {
|
|
@@ -6652,10 +6977,116 @@ function formatList(one, many, values) {
|
|
|
6652
6977
|
return `the ${values.length === 1 ? one : many} ${values.join(", ")}`;
|
|
6653
6978
|
}
|
|
6654
6979
|
//#endregion
|
|
6655
|
-
//#region
|
|
6656
|
-
function
|
|
6657
|
-
|
|
6658
|
-
|
|
6980
|
+
//#region src/diagnostics/write-action-diagnostic.ts
|
|
6981
|
+
async function writeActionDiagnostic(payload, entry, host, warnings) {
|
|
6982
|
+
const path = host.env["AUTO_MODE_DIAGNOSTICS_PATH"] ?? join(resolveXDGDir(host, "state"), "actions.jsonl");
|
|
6983
|
+
if (path === "") return;
|
|
6984
|
+
const record = {
|
|
6985
|
+
schemaVersion: 4,
|
|
6986
|
+
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6987
|
+
invocationID: entry.invocationID,
|
|
6988
|
+
sessionHash: toHash(payload.sessionID),
|
|
6989
|
+
actionHash: payload.toolUseID === void 0 ? null : toHash(payload.toolUseID),
|
|
6990
|
+
status: entry.status,
|
|
6991
|
+
verdict: entry.verdict ?? null,
|
|
6992
|
+
decidingStage: entry.decidingStage ?? null,
|
|
6993
|
+
denials: entry.denials ?? null,
|
|
6994
|
+
escalation: entry.escalation ?? false,
|
|
6995
|
+
diagnostics: entry.diagnostics ?? null,
|
|
6996
|
+
judge: entry.judge ?? null
|
|
6997
|
+
};
|
|
6998
|
+
try {
|
|
6999
|
+
await mkdir(dirname(path), {
|
|
7000
|
+
recursive: true,
|
|
7001
|
+
mode: 448
|
|
7002
|
+
});
|
|
7003
|
+
await appendFile(path, `${JSON.stringify(record)}\n`, { mode: 384 });
|
|
7004
|
+
} catch {
|
|
7005
|
+
warnings.write("auto-mode: diagnostics unavailable\n");
|
|
7006
|
+
}
|
|
7007
|
+
}
|
|
7008
|
+
function toHash(value) {
|
|
7009
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
7010
|
+
}
|
|
7011
|
+
//#endregion
|
|
7012
|
+
//#region src/git/load-checkout-remotes.ts
|
|
7013
|
+
async function loadCheckoutRemotes(commonDir) {
|
|
7014
|
+
const config = await readFile(join(commonDir, "config"), "utf8").catch(() => "");
|
|
7015
|
+
const remotes = [];
|
|
7016
|
+
let section = null;
|
|
7017
|
+
for (const line of config.split("\n")) {
|
|
7018
|
+
const header = /^\s*\[remote "(?<name>[^"]+)"\]/u.exec(line)?.groups?.["name"];
|
|
7019
|
+
if (header !== void 0) section = header;
|
|
7020
|
+
else if (/^\s*\[/u.test(line)) section = null;
|
|
7021
|
+
else if (section !== null) {
|
|
7022
|
+
const value = /^\s*(?:push)?url\s*=(?<value>.*)$/iu.exec(line)?.groups?.["value"];
|
|
7023
|
+
const url = value === void 0 ? "" : parseConfigValue(value);
|
|
7024
|
+
if (url !== "") remotes.push({
|
|
7025
|
+
name: section,
|
|
7026
|
+
url
|
|
7027
|
+
});
|
|
7028
|
+
}
|
|
7029
|
+
}
|
|
7030
|
+
return remotes;
|
|
7031
|
+
}
|
|
7032
|
+
function parseConfigValue(raw) {
|
|
7033
|
+
let value = "";
|
|
7034
|
+
let isQuoted = false;
|
|
7035
|
+
for (let index = 0; index < raw.length; index += 1) {
|
|
7036
|
+
const char = raw[index] ?? "";
|
|
7037
|
+
if (char === "\\") {
|
|
7038
|
+
const next = raw[index + 1] ?? "";
|
|
7039
|
+
value += {
|
|
7040
|
+
n: "\n",
|
|
7041
|
+
t: " ",
|
|
7042
|
+
b: ""
|
|
7043
|
+
}[next] ?? next;
|
|
7044
|
+
index += 1;
|
|
7045
|
+
} else if (char === "\"") isQuoted = !isQuoted;
|
|
7046
|
+
else if (!isQuoted && (char === "#" || char === ";")) break;
|
|
7047
|
+
else value += char;
|
|
7048
|
+
}
|
|
7049
|
+
return value.trim();
|
|
7050
|
+
}
|
|
7051
|
+
//#endregion
|
|
7052
|
+
//#region src/git/load-checkout-facts.ts
|
|
7053
|
+
async function loadCheckoutFacts(cwd, env, options = {}) {
|
|
7054
|
+
const directory = resolve(cwd);
|
|
7055
|
+
const stopDir = options.stopDir === void 0 ? void 0 : resolve(options.stopDir);
|
|
7056
|
+
const checkout = await (options.findCheckout === void 0 ? findCheckout(directory, env, stopDir) : options.findCheckout(directory, env));
|
|
7057
|
+
if (checkout === null) return {
|
|
7058
|
+
cwd: directory,
|
|
7059
|
+
checkout,
|
|
7060
|
+
head: null,
|
|
7061
|
+
remotes: []
|
|
7062
|
+
};
|
|
7063
|
+
const [head, originHead, remotes] = await Promise.all([
|
|
7064
|
+
readFile(join(checkout.gitDir, "HEAD"), "utf8").catch(() => null),
|
|
7065
|
+
readFile(join(checkout.commonDir, "refs", "remotes", "origin", "HEAD"), "utf8").catch(() => null),
|
|
7066
|
+
loadCheckoutRemotes(checkout.commonDir)
|
|
7067
|
+
]);
|
|
7068
|
+
return {
|
|
7069
|
+
cwd: directory,
|
|
7070
|
+
checkout,
|
|
7071
|
+
head: head === null ? null : {
|
|
7072
|
+
branch: parseReference(head, "refs/heads/"),
|
|
7073
|
+
defaultBranch: originHead === null ? null : parseReference(originHead, "refs/remotes/origin/")
|
|
7074
|
+
},
|
|
7075
|
+
remotes
|
|
7076
|
+
};
|
|
7077
|
+
}
|
|
7078
|
+
function parseReference(content, prefix) {
|
|
7079
|
+
const value = content.trim();
|
|
7080
|
+
const marker = `ref: ${prefix}`;
|
|
7081
|
+
if (!value.startsWith(marker)) return null;
|
|
7082
|
+
const branch = value.slice(marker.length);
|
|
7083
|
+
return branch !== "" && branch.length <= 256 && !/\s/u.test(branch) ? branch : null;
|
|
7084
|
+
}
|
|
7085
|
+
//#endregion
|
|
7086
|
+
//#region mods/auto-mode/contract/find-current-direct-user-message.ts
|
|
7087
|
+
function findCurrentDirectUserMessage(message) {
|
|
7088
|
+
return message === null || message === void 0 || message.freshness === "stale" ? null : message.text;
|
|
7089
|
+
}
|
|
6659
7090
|
//#endregion
|
|
6660
7091
|
//#region src/policy/resolve-shipped-policy-dir.ts
|
|
6661
7092
|
function resolveShippedPolicyDir(startDir = import.meta.dirname, stopDir = parse(startDir).root) {
|
|
@@ -7127,7 +7558,7 @@ function parseJudgeReply(text) {
|
|
|
7127
7558
|
};
|
|
7128
7559
|
}
|
|
7129
7560
|
//#endregion
|
|
7130
|
-
//#region src/
|
|
7561
|
+
//#region src/messages/anthropic-client.ts
|
|
7131
7562
|
async function sendMessage(provider, apiKey, request, signal) {
|
|
7132
7563
|
const response = await fetch(`${provider.baseURL.replace(/\/$/, "")}/v1/messages`, {
|
|
7133
7564
|
method: "POST",
|
|
@@ -7199,8 +7630,9 @@ function readResult(body) {
|
|
|
7199
7630
|
}
|
|
7200
7631
|
//#endregion
|
|
7201
7632
|
//#region src/judge/run-claude-code.ts
|
|
7633
|
+
const MAX_REPLY_BYTES = 1048576;
|
|
7202
7634
|
async function runClaudeCode(provider, request, env, signal) {
|
|
7203
|
-
const
|
|
7635
|
+
const result = await runCapped("claude", [
|
|
7204
7636
|
"-p",
|
|
7205
7637
|
"--model",
|
|
7206
7638
|
provider.model,
|
|
@@ -7215,29 +7647,14 @@ async function runClaudeCode(provider, request, env, signal) {
|
|
|
7215
7647
|
"--output-format",
|
|
7216
7648
|
"json"
|
|
7217
7649
|
], {
|
|
7218
|
-
cwd: tmpdir(),
|
|
7219
|
-
env: buildChildEnv(env),
|
|
7220
7650
|
signal,
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
"ignore"
|
|
7226
|
-
]
|
|
7227
|
-
});
|
|
7228
|
-
const chunks = [];
|
|
7229
|
-
child.stdout.on("data", (chunk) => {
|
|
7230
|
-
chunks.push(chunk);
|
|
7231
|
-
});
|
|
7232
|
-
const closed = new Promise((resolve, reject) => {
|
|
7233
|
-
child.once("error", reject);
|
|
7234
|
-
child.once("close", resolve);
|
|
7651
|
+
maxOutputBytes: MAX_REPLY_BYTES,
|
|
7652
|
+
input: request.user,
|
|
7653
|
+
cwd: tmpdir(),
|
|
7654
|
+
env: buildChildEnv(env)
|
|
7235
7655
|
});
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
const exitCode = await closed;
|
|
7239
|
-
const body = CLAUDE_OUTPUT.safeParse(parseOutputJSON(Buffer.concat(chunks).toString("utf8")));
|
|
7240
|
-
if (exitCode !== 0 || !body.success || body.data.is_error || body.data.result === void 0) throw new Error(`claude -p failed with exit code ${String(exitCode)}`);
|
|
7656
|
+
const body = CLAUDE_OUTPUT.safeParse(parseOutputJSON(result.stdout));
|
|
7657
|
+
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)}`);
|
|
7241
7658
|
return {
|
|
7242
7659
|
text: body.data.result,
|
|
7243
7660
|
model: provider.model
|
|
@@ -7346,6 +7763,116 @@ function buildTaskScopeSummary(scope) {
|
|
|
7346
7763
|
};
|
|
7347
7764
|
}
|
|
7348
7765
|
//#endregion
|
|
7766
|
+
//#region src/git/build-repository-context.ts
|
|
7767
|
+
function buildRepositoryContext(facts) {
|
|
7768
|
+
if (facts.head === null) return null;
|
|
7769
|
+
return {
|
|
7770
|
+
cwd: facts.cwd,
|
|
7771
|
+
branch: facts.head.branch,
|
|
7772
|
+
defaultBranch: facts.head.defaultBranch,
|
|
7773
|
+
remotes: facts.remotes.flatMap((remote) => {
|
|
7774
|
+
const url = normalizeRemoteURL(remote.url);
|
|
7775
|
+
return url === null ? [] : [{
|
|
7776
|
+
name: remote.name,
|
|
7777
|
+
url
|
|
7778
|
+
}];
|
|
7779
|
+
})
|
|
7780
|
+
};
|
|
7781
|
+
}
|
|
7782
|
+
function normalizeRemoteURL(url) {
|
|
7783
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//iu.test(url)) {
|
|
7784
|
+
const parsed = URL.parse(url);
|
|
7785
|
+
if (parsed === null) return null;
|
|
7786
|
+
parsed.username = "";
|
|
7787
|
+
parsed.password = "";
|
|
7788
|
+
parsed.search = "";
|
|
7789
|
+
parsed.hash = "";
|
|
7790
|
+
return parsed.href;
|
|
7791
|
+
}
|
|
7792
|
+
const slash = url.indexOf("/");
|
|
7793
|
+
const end = slash === -1 ? url.length : slash;
|
|
7794
|
+
const at = url.lastIndexOf("@", end);
|
|
7795
|
+
return at === -1 ? url : url.slice(at + 1);
|
|
7796
|
+
}
|
|
7797
|
+
//#endregion
|
|
7798
|
+
//#region src/git/build-repository-evidence.ts
|
|
7799
|
+
function buildRepositoryEvidence(facts, taskScope) {
|
|
7800
|
+
const context = buildRepositoryContext(facts);
|
|
7801
|
+
if (taskScope === void 0) return context;
|
|
7802
|
+
return {
|
|
7803
|
+
...context ?? {
|
|
7804
|
+
cwd: facts.cwd,
|
|
7805
|
+
branch: null,
|
|
7806
|
+
defaultBranch: null
|
|
7807
|
+
},
|
|
7808
|
+
taskScope
|
|
7809
|
+
};
|
|
7810
|
+
}
|
|
7811
|
+
//#endregion
|
|
7812
|
+
//#region src/git/load-mcp-servers.ts
|
|
7813
|
+
const serverSchema = looseObject({
|
|
7814
|
+
type: string().optional(),
|
|
7815
|
+
url: string().optional()
|
|
7816
|
+
});
|
|
7817
|
+
const serversSchema = record(string(), unknown()).optional();
|
|
7818
|
+
const approvalSchema = looseObject({
|
|
7819
|
+
enableAllProjectMcpServers: boolean().optional(),
|
|
7820
|
+
enabledMcpjsonServers: array(string()).optional(),
|
|
7821
|
+
disabledMcpjsonServers: array(string()).optional()
|
|
7822
|
+
});
|
|
7823
|
+
const stateSchema = looseObject({
|
|
7824
|
+
mcpServers: serversSchema,
|
|
7825
|
+
projects: record(string(), looseObject({
|
|
7826
|
+
...approvalSchema.shape,
|
|
7827
|
+
mcpServers: serversSchema
|
|
7828
|
+
})).optional()
|
|
7829
|
+
});
|
|
7830
|
+
const projectFileSchema = looseObject({ mcpServers: serversSchema });
|
|
7831
|
+
async function loadMCPServers(cwd, host, userSettings, checkoutFacts) {
|
|
7832
|
+
const state = await loadJSONFile(resolveClaudeConfigPath(host, ".claude.json"), stateSchema, null);
|
|
7833
|
+
const settings = approvalSchema.safeParse(userSettings).data;
|
|
7834
|
+
const directory = resolve(cwd);
|
|
7835
|
+
const projectDir = (checkoutFacts === void 0 ? await findCheckout(directory, host.env) : checkoutFacts.checkout)?.worktree ?? directory;
|
|
7836
|
+
const project = state?.projects?.[projectDir];
|
|
7837
|
+
const projectFile = await loadJSONFile(join(projectDir, ".mcp.json"), projectFileSchema, null);
|
|
7838
|
+
const approvals = [settings, project].filter((entry) => entry !== void 0 && entry !== null);
|
|
7839
|
+
const isDisabled = (name) => approvals.some((entry) => entry.disabledMcpjsonServers?.includes(name) === true);
|
|
7840
|
+
const isEnabled = (name) => approvals.some((entry) => entry.enableAllProjectMcpServers === true || entry.enabledMcpjsonServers?.includes(name) === true);
|
|
7841
|
+
const facts = [
|
|
7842
|
+
...buildServerFacts(project?.mcpServers, "local"),
|
|
7843
|
+
...buildServerFacts(projectFile?.mcpServers, "project").filter((server) => isEnabled(server.name) && !isDisabled(server.name)),
|
|
7844
|
+
...buildServerFacts(state?.mcpServers, "user")
|
|
7845
|
+
];
|
|
7846
|
+
return facts.filter((server, index) => facts.findIndex((other) => other.name === server.name) === index);
|
|
7847
|
+
}
|
|
7848
|
+
const MAX_NAME_LENGTH = 128;
|
|
7849
|
+
const TRANSPORTS = {
|
|
7850
|
+
stdio: "stdio",
|
|
7851
|
+
http: "http",
|
|
7852
|
+
"streamable-http": "http",
|
|
7853
|
+
sse: "sse",
|
|
7854
|
+
ws: "ws"
|
|
7855
|
+
};
|
|
7856
|
+
function buildServerFacts(servers, scope) {
|
|
7857
|
+
return Object.entries(servers ?? {}).flatMap(([name, entry]) => {
|
|
7858
|
+
const server = serverSchema.safeParse(entry);
|
|
7859
|
+
if (!server.success || name === "" || name.length > MAX_NAME_LENGTH) return [];
|
|
7860
|
+
const transport = TRANSPORTS[server.data.type ?? (server.data.url === void 0 ? "stdio" : "")];
|
|
7861
|
+
if (transport === void 0) return [];
|
|
7862
|
+
return [{
|
|
7863
|
+
name,
|
|
7864
|
+
scope,
|
|
7865
|
+
transport,
|
|
7866
|
+
host: transport === "stdio" || server.data.url === void 0 ? null : findEndpointHost(server.data.url)
|
|
7867
|
+
}];
|
|
7868
|
+
});
|
|
7869
|
+
}
|
|
7870
|
+
function findEndpointHost(url) {
|
|
7871
|
+
const parsed = URL.parse(url);
|
|
7872
|
+
if (parsed === null || !/^(?:https?|wss?):$/u.test(parsed.protocol) || /[${}]/u.test(parsed.host)) return null;
|
|
7873
|
+
return parsed.host;
|
|
7874
|
+
}
|
|
7875
|
+
//#endregion
|
|
7349
7876
|
//#region src/policy/build-unavailable-verdict.ts
|
|
7350
7877
|
function buildUnavailableVerdict(onFailure, reason) {
|
|
7351
7878
|
return onFailure === "deny" ? {
|
|
@@ -7527,219 +8054,6 @@ function formatClassifierNote(note, apiKey) {
|
|
|
7527
8054
|
return apiKey === null || apiKey === "" ? note : note.split(apiKey).join("[redacted]");
|
|
7528
8055
|
}
|
|
7529
8056
|
//#endregion
|
|
7530
|
-
//#region src/model/load-mcp-servers.ts
|
|
7531
|
-
const serverSchema = looseObject({
|
|
7532
|
-
type: string().optional(),
|
|
7533
|
-
url: string().optional()
|
|
7534
|
-
});
|
|
7535
|
-
const serversSchema = record(string(), unknown()).optional();
|
|
7536
|
-
const approvalSchema = looseObject({
|
|
7537
|
-
enableAllProjectMcpServers: boolean().optional(),
|
|
7538
|
-
enabledMcpjsonServers: array(string()).optional(),
|
|
7539
|
-
disabledMcpjsonServers: array(string()).optional()
|
|
7540
|
-
});
|
|
7541
|
-
const stateSchema = looseObject({
|
|
7542
|
-
mcpServers: serversSchema,
|
|
7543
|
-
projects: record(string(), looseObject({
|
|
7544
|
-
...approvalSchema.shape,
|
|
7545
|
-
mcpServers: serversSchema
|
|
7546
|
-
})).optional()
|
|
7547
|
-
});
|
|
7548
|
-
const projectFileSchema = looseObject({ mcpServers: serversSchema });
|
|
7549
|
-
async function loadMCPServers(cwd, host, userSettings) {
|
|
7550
|
-
const configDir = host.env["CLAUDE_CONFIG_DIR"];
|
|
7551
|
-
const state = await readJSON(configDir !== void 0 && configDir !== "" ? join(configDir, ".claude.json") : join(host.home, ".claude.json"), stateSchema);
|
|
7552
|
-
const settings = approvalSchema.safeParse(userSettings).data;
|
|
7553
|
-
const directory = resolve(cwd);
|
|
7554
|
-
const projectDir = (await findCheckout(directory, host.env))?.worktree ?? directory;
|
|
7555
|
-
const project = state?.projects?.[projectDir];
|
|
7556
|
-
const projectFile = await readJSON(join(projectDir, ".mcp.json"), projectFileSchema);
|
|
7557
|
-
const approvals = [settings, project].filter((entry) => entry !== void 0 && entry !== null);
|
|
7558
|
-
const isDisabled = (name) => approvals.some((entry) => entry.disabledMcpjsonServers?.includes(name) === true);
|
|
7559
|
-
const isEnabled = (name) => approvals.some((entry) => entry.enableAllProjectMcpServers === true || entry.enabledMcpjsonServers?.includes(name) === true);
|
|
7560
|
-
const facts = [
|
|
7561
|
-
...buildServerFacts(project?.mcpServers, "local"),
|
|
7562
|
-
...buildServerFacts(projectFile?.mcpServers, "project").filter((server) => isEnabled(server.name) && !isDisabled(server.name)),
|
|
7563
|
-
...buildServerFacts(state?.mcpServers, "user")
|
|
7564
|
-
];
|
|
7565
|
-
return facts.filter((server, index) => facts.findIndex((other) => other.name === server.name) === index);
|
|
7566
|
-
}
|
|
7567
|
-
async function readJSON(path, schema) {
|
|
7568
|
-
try {
|
|
7569
|
-
const text = await readFile(path, "utf8");
|
|
7570
|
-
const parsed = schema.safeParse(JSON.parse(text));
|
|
7571
|
-
return parsed.success ? parsed.data : null;
|
|
7572
|
-
} catch {
|
|
7573
|
-
return null;
|
|
7574
|
-
}
|
|
7575
|
-
}
|
|
7576
|
-
const MAX_NAME_LENGTH = 128;
|
|
7577
|
-
const TRANSPORTS = {
|
|
7578
|
-
stdio: "stdio",
|
|
7579
|
-
http: "http",
|
|
7580
|
-
"streamable-http": "http",
|
|
7581
|
-
sse: "sse",
|
|
7582
|
-
ws: "ws"
|
|
7583
|
-
};
|
|
7584
|
-
function buildServerFacts(servers, scope) {
|
|
7585
|
-
return Object.entries(servers ?? {}).flatMap(([name, entry]) => {
|
|
7586
|
-
const server = serverSchema.safeParse(entry);
|
|
7587
|
-
if (!server.success || name === "" || name.length > MAX_NAME_LENGTH) return [];
|
|
7588
|
-
const transport = TRANSPORTS[server.data.type ?? (server.data.url === void 0 ? "stdio" : "")];
|
|
7589
|
-
if (transport === void 0) return [];
|
|
7590
|
-
return [{
|
|
7591
|
-
name,
|
|
7592
|
-
scope,
|
|
7593
|
-
transport,
|
|
7594
|
-
host: transport === "stdio" || server.data.url === void 0 ? null : findEndpointHost(server.data.url)
|
|
7595
|
-
}];
|
|
7596
|
-
});
|
|
7597
|
-
}
|
|
7598
|
-
function findEndpointHost(url) {
|
|
7599
|
-
const parsed = URL.parse(url);
|
|
7600
|
-
if (parsed === null || !/^(?:https?|wss?):$/u.test(parsed.protocol) || /[${}]/u.test(parsed.host)) return null;
|
|
7601
|
-
return parsed.host;
|
|
7602
|
-
}
|
|
7603
|
-
//#endregion
|
|
7604
|
-
//#region src/scope/load-checkout-remotes.ts
|
|
7605
|
-
async function loadCheckoutRemotes(commonDir) {
|
|
7606
|
-
const config = await readFile(join(commonDir, "config"), "utf8").catch(() => "");
|
|
7607
|
-
const remotes = [];
|
|
7608
|
-
let section = null;
|
|
7609
|
-
for (const line of config.split("\n")) {
|
|
7610
|
-
const header = /^\s*\[remote "(?<name>[^"]+)"\]/u.exec(line)?.groups?.["name"];
|
|
7611
|
-
if (header !== void 0) section = header;
|
|
7612
|
-
else if (/^\s*\[/u.test(line)) section = null;
|
|
7613
|
-
else if (section !== null) {
|
|
7614
|
-
const value = /^\s*(?:push)?url\s*=(?<value>.*)$/iu.exec(line)?.groups?.["value"];
|
|
7615
|
-
const url = value === void 0 ? "" : parseConfigValue(value);
|
|
7616
|
-
if (url !== "") remotes.push({
|
|
7617
|
-
name: section,
|
|
7618
|
-
url
|
|
7619
|
-
});
|
|
7620
|
-
}
|
|
7621
|
-
}
|
|
7622
|
-
return remotes;
|
|
7623
|
-
}
|
|
7624
|
-
function parseConfigValue(raw) {
|
|
7625
|
-
let value = "";
|
|
7626
|
-
let isQuoted = false;
|
|
7627
|
-
for (let index = 0; index < raw.length; index += 1) {
|
|
7628
|
-
const char = raw[index] ?? "";
|
|
7629
|
-
if (char === "\\") {
|
|
7630
|
-
const next = raw[index + 1] ?? "";
|
|
7631
|
-
value += {
|
|
7632
|
-
n: "\n",
|
|
7633
|
-
t: " ",
|
|
7634
|
-
b: ""
|
|
7635
|
-
}[next] ?? next;
|
|
7636
|
-
index += 1;
|
|
7637
|
-
} else if (char === "\"") isQuoted = !isQuoted;
|
|
7638
|
-
else if (!isQuoted && (char === "#" || char === ";")) break;
|
|
7639
|
-
else value += char;
|
|
7640
|
-
}
|
|
7641
|
-
return value.trim();
|
|
7642
|
-
}
|
|
7643
|
-
//#endregion
|
|
7644
|
-
//#region src/model/load-repository-context.ts
|
|
7645
|
-
async function loadRepositoryContext(cwd, env, stopDir = parse(resolve(cwd)).root) {
|
|
7646
|
-
if ([
|
|
7647
|
-
"GIT_DIR",
|
|
7648
|
-
"GIT_WORK_TREE",
|
|
7649
|
-
"GIT_COMMON_DIR"
|
|
7650
|
-
].some((name) => env[name] !== void 0)) return null;
|
|
7651
|
-
let directory = resolve(cwd);
|
|
7652
|
-
for (;;) {
|
|
7653
|
-
const gitPath = join(directory, ".git");
|
|
7654
|
-
try {
|
|
7655
|
-
await stat(gitPath);
|
|
7656
|
-
} catch (error) {
|
|
7657
|
-
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) return null;
|
|
7658
|
-
const parent = dirname(directory);
|
|
7659
|
-
if (parent === directory || directory === resolve(stopDir)) return null;
|
|
7660
|
-
directory = parent;
|
|
7661
|
-
continue;
|
|
7662
|
-
}
|
|
7663
|
-
let gitDir = gitPath;
|
|
7664
|
-
let head;
|
|
7665
|
-
try {
|
|
7666
|
-
try {
|
|
7667
|
-
head = await readFile(join(gitDir, "HEAD"), "utf8");
|
|
7668
|
-
} catch {
|
|
7669
|
-
const link = await readFile(gitPath, "utf8");
|
|
7670
|
-
const path = /^gitdir: (?<path>.+)\r?\n?$/u.exec(link)?.groups?.["path"];
|
|
7671
|
-
if (path === void 0) return null;
|
|
7672
|
-
gitDir = resolve(directory, path.trim());
|
|
7673
|
-
head = await readFile(join(gitDir, "HEAD"), "utf8");
|
|
7674
|
-
}
|
|
7675
|
-
} catch {
|
|
7676
|
-
return null;
|
|
7677
|
-
}
|
|
7678
|
-
let commonDir = gitDir;
|
|
7679
|
-
try {
|
|
7680
|
-
const common = await readFile(join(gitDir, "commondir"), "utf8");
|
|
7681
|
-
commonDir = resolve(gitDir, common.trim());
|
|
7682
|
-
} catch {
|
|
7683
|
-
commonDir = gitDir;
|
|
7684
|
-
}
|
|
7685
|
-
let defaultBranch = null;
|
|
7686
|
-
try {
|
|
7687
|
-
defaultBranch = parseReference(await readFile(join(commonDir, "refs", "remotes", "origin", "HEAD"), "utf8"), "refs/remotes/origin/");
|
|
7688
|
-
} catch {
|
|
7689
|
-
defaultBranch = null;
|
|
7690
|
-
}
|
|
7691
|
-
const remotes = await loadCheckoutRemotes(commonDir);
|
|
7692
|
-
return {
|
|
7693
|
-
cwd: resolve(cwd),
|
|
7694
|
-
branch: parseReference(head, "refs/heads/"),
|
|
7695
|
-
defaultBranch,
|
|
7696
|
-
remotes: remotes.flatMap((remote) => {
|
|
7697
|
-
const url = normalizeRemoteURL(remote.url);
|
|
7698
|
-
return url === null ? [] : [{
|
|
7699
|
-
name: remote.name,
|
|
7700
|
-
url
|
|
7701
|
-
}];
|
|
7702
|
-
})
|
|
7703
|
-
};
|
|
7704
|
-
}
|
|
7705
|
-
}
|
|
7706
|
-
function parseReference(content, prefix) {
|
|
7707
|
-
const value = content.trim();
|
|
7708
|
-
const marker = `ref: ${prefix}`;
|
|
7709
|
-
if (!value.startsWith(marker)) return null;
|
|
7710
|
-
const branch = value.slice(marker.length);
|
|
7711
|
-
return branch !== "" && branch.length <= 256 && !/\s/u.test(branch) ? branch : null;
|
|
7712
|
-
}
|
|
7713
|
-
function normalizeRemoteURL(url) {
|
|
7714
|
-
if (/^[a-z][a-z0-9+.-]*:\/\//iu.test(url)) {
|
|
7715
|
-
const parsed = URL.parse(url);
|
|
7716
|
-
if (parsed === null) return null;
|
|
7717
|
-
parsed.username = "";
|
|
7718
|
-
parsed.password = "";
|
|
7719
|
-
parsed.search = "";
|
|
7720
|
-
parsed.hash = "";
|
|
7721
|
-
return parsed.href;
|
|
7722
|
-
}
|
|
7723
|
-
const slash = url.indexOf("/");
|
|
7724
|
-
const end = slash === -1 ? url.length : slash;
|
|
7725
|
-
const at = url.lastIndexOf("@", end);
|
|
7726
|
-
return at === -1 ? url : url.slice(at + 1);
|
|
7727
|
-
}
|
|
7728
|
-
//#endregion
|
|
7729
|
-
//#region src/model/load-repository-evidence.ts
|
|
7730
|
-
async function loadRepositoryEvidence(cwd, taskScope, env, stopDir) {
|
|
7731
|
-
const context = await loadRepositoryContext(cwd, env, stopDir);
|
|
7732
|
-
if (taskScope === void 0) return context;
|
|
7733
|
-
return {
|
|
7734
|
-
...context ?? {
|
|
7735
|
-
cwd: resolve(cwd),
|
|
7736
|
-
branch: null,
|
|
7737
|
-
defaultBranch: null
|
|
7738
|
-
},
|
|
7739
|
-
taskScope
|
|
7740
|
-
};
|
|
7741
|
-
}
|
|
7742
|
-
//#endregion
|
|
7743
8057
|
//#region src/model/find-denied-rule.ts
|
|
7744
8058
|
function findDeniedRule(request, result, blockThreshold) {
|
|
7745
8059
|
const [top] = collectBlockingRules(request, result, blockThreshold);
|
|
@@ -7761,6 +8075,74 @@ function pickDecisionVerdict(request, result, blockThreshold) {
|
|
|
7761
8075
|
};
|
|
7762
8076
|
}
|
|
7763
8077
|
//#endregion
|
|
8078
|
+
//#region src/model/build-jev-outcome.ts
|
|
8079
|
+
function buildJevOutcome(attempt, facts, config) {
|
|
8080
|
+
const elapsedMs = Math.round(facts.elapsedMs);
|
|
8081
|
+
if (attempt.kind === "reply") {
|
|
8082
|
+
const verdict = pickDecisionVerdict(attempt.request, attempt.result, facts.blockThreshold);
|
|
8083
|
+
const denied = findReviewedRule(attempt.request, attempt.result, facts.blockThreshold);
|
|
8084
|
+
return {
|
|
8085
|
+
verdict,
|
|
8086
|
+
...denied === null ? {} : { review: {
|
|
8087
|
+
...denied,
|
|
8088
|
+
repositoryContext: attempt.repositoryContext
|
|
8089
|
+
} },
|
|
8090
|
+
diagnostics: {
|
|
8091
|
+
status: verdict.kind,
|
|
8092
|
+
stage: "response",
|
|
8093
|
+
keyResolved: true,
|
|
8094
|
+
keySource: facts.keySource,
|
|
8095
|
+
failureReason: null,
|
|
8096
|
+
requestBytes: attempt.result.requestBytes,
|
|
8097
|
+
elapsedMs,
|
|
8098
|
+
blockThreshold: facts.blockThreshold,
|
|
8099
|
+
contributors: collectDecisionContributors(attempt.request, attempt.result, facts.blockThreshold)
|
|
8100
|
+
},
|
|
8101
|
+
note: formatClassifierNote(`${config.provider.model}: ${verdict.kind} (${elapsedMs}ms, ${attempt.result.inputTokens} input tokens)`, facts.key)
|
|
8102
|
+
};
|
|
8103
|
+
}
|
|
8104
|
+
const failure = classifyFailure(attempt, config.provider.timeoutMs);
|
|
8105
|
+
const note = formatClassifierNote(`${config.provider.model} unavailable: ${failure.reason}`, facts.key);
|
|
8106
|
+
return {
|
|
8107
|
+
verdict: buildUnavailableVerdict(config.onFailure, note),
|
|
8108
|
+
note,
|
|
8109
|
+
unavailable: true,
|
|
8110
|
+
diagnostics: {
|
|
8111
|
+
status: failure.status,
|
|
8112
|
+
stage: attempt.stage,
|
|
8113
|
+
keyResolved: facts.key !== null,
|
|
8114
|
+
keySource: facts.keySource,
|
|
8115
|
+
failureReason: attempt.error instanceof DecisionRequestError ? attempt.error.reason : null,
|
|
8116
|
+
requestBytes: attempt.error instanceof DecisionRequestError ? attempt.error.requestBytes : null,
|
|
8117
|
+
elapsedMs,
|
|
8118
|
+
blockThreshold: facts.blockThreshold,
|
|
8119
|
+
contributors: []
|
|
8120
|
+
}
|
|
8121
|
+
};
|
|
8122
|
+
}
|
|
8123
|
+
function classifyFailure(attempt, timeoutMs) {
|
|
8124
|
+
if (attempt.cancelled) return {
|
|
8125
|
+
reason: "evaluation cancelled",
|
|
8126
|
+
status: "cancelled"
|
|
8127
|
+
};
|
|
8128
|
+
if (attempt.deadlineExpired) return {
|
|
8129
|
+
reason: "evaluation deadline expired",
|
|
8130
|
+
status: "timeout"
|
|
8131
|
+
};
|
|
8132
|
+
const error = attempt.error;
|
|
8133
|
+
if (!(error instanceof Error)) return {
|
|
8134
|
+
reason: "classifier failed",
|
|
8135
|
+
status: "failure"
|
|
8136
|
+
};
|
|
8137
|
+
return error.name === "AbortError" || error instanceof DecisionRequestError && error.reason === "aborted" ? {
|
|
8138
|
+
reason: `timed out after ${timeoutMs}ms`,
|
|
8139
|
+
status: "timeout"
|
|
8140
|
+
} : {
|
|
8141
|
+
reason: error.message,
|
|
8142
|
+
status: "failure"
|
|
8143
|
+
};
|
|
8144
|
+
}
|
|
8145
|
+
//#endregion
|
|
7764
8146
|
//#region src/model/probability-schema.ts
|
|
7765
8147
|
const probabilitySchema = number().min(0).max(1);
|
|
7766
8148
|
//#endregion
|
|
@@ -7854,14 +8236,12 @@ async function classifyWithJev(payload, config, options) {
|
|
|
7854
8236
|
stage = "evidence";
|
|
7855
8237
|
const directUserText = findCurrentDirectUserMessage(payload.decisionContext?.lastDirectUserMessage);
|
|
7856
8238
|
const settings = await (options.claudeSettings ?? loadClaudeSettings(config.claudeSettingsPath, options.host));
|
|
7857
|
-
const
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
loadMCPServers(payload.cwd, options.host, settings.userSettings).catch(() => [])
|
|
7864
|
-
]);
|
|
8239
|
+
const checkout = await (options.checkout ?? loadCheckoutFacts(payload.cwd, options.host.env, { findCheckout: options.findCheckout }).catch(() => null));
|
|
8240
|
+
const repositoryContext = checkout === null ? null : buildRepositoryEvidence(checkout, options.taskScope);
|
|
8241
|
+
const [policy, mcpServers] = await Promise.all([loadPolicy({
|
|
8242
|
+
classifierPath: config.classifierPath,
|
|
8243
|
+
rulesPath: config.rulesPath
|
|
8244
|
+
}, "decision.md"), loadMCPServers(payload.cwd, options.host, settings.userSettings, checkout ?? void 0).catch(() => [])]);
|
|
7865
8245
|
const rulesSource = config.rulesPath === void 0 ? "shipped" : "replacement";
|
|
7866
8246
|
const request = buildDecisionRequest(payload, policy, settings.rules, directUserText, rulesSource, repositoryContext, mcpServers);
|
|
7867
8247
|
const remainingMs = options.deadlineAt === void 0 ? config.provider.timeoutMs : options.deadlineAt - now();
|
|
@@ -7871,58 +8251,30 @@ async function classifyWithJev(payload, config, options) {
|
|
|
7871
8251
|
stage = "request";
|
|
7872
8252
|
const result = await sendDecision(config.provider, key, request, signal);
|
|
7873
8253
|
stage = "response";
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
keyResolved: true,
|
|
7886
|
-
keySource,
|
|
7887
|
-
failureReason: null,
|
|
7888
|
-
requestBytes: result.requestBytes,
|
|
7889
|
-
elapsedMs: Math.round(performance.now() - start),
|
|
7890
|
-
blockThreshold,
|
|
7891
|
-
contributors: collectDecisionContributors(request, result, blockThreshold)
|
|
7892
|
-
},
|
|
7893
|
-
note: formatClassifierNote(`${config.provider.model}: ${verdict.kind} (${Math.round(performance.now() - start)}ms, ${result.inputTokens} input tokens)`, key)
|
|
7894
|
-
};
|
|
8254
|
+
return buildJevOutcome({
|
|
8255
|
+
kind: "reply",
|
|
8256
|
+
request,
|
|
8257
|
+
result,
|
|
8258
|
+
repositoryContext
|
|
8259
|
+
}, {
|
|
8260
|
+
key,
|
|
8261
|
+
keySource,
|
|
8262
|
+
elapsedMs: performance.now() - start,
|
|
8263
|
+
blockThreshold
|
|
8264
|
+
}, config);
|
|
7895
8265
|
} catch (error) {
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
}
|
|
7909
|
-
const note = formatClassifierNote(`${config.provider.model} unavailable: ${reason}`, key);
|
|
7910
|
-
return {
|
|
7911
|
-
verdict: buildUnavailableVerdict(config.onFailure, note),
|
|
7912
|
-
note,
|
|
7913
|
-
unavailable: true,
|
|
7914
|
-
diagnostics: {
|
|
7915
|
-
status,
|
|
7916
|
-
stage,
|
|
7917
|
-
keyResolved: key !== null,
|
|
7918
|
-
keySource,
|
|
7919
|
-
failureReason: error instanceof DecisionRequestError ? error.reason : null,
|
|
7920
|
-
requestBytes: error instanceof DecisionRequestError ? error.requestBytes : null,
|
|
7921
|
-
elapsedMs: Math.round(performance.now() - start),
|
|
7922
|
-
blockThreshold,
|
|
7923
|
-
contributors: []
|
|
7924
|
-
}
|
|
7925
|
-
};
|
|
8266
|
+
return buildJevOutcome({
|
|
8267
|
+
kind: "failure",
|
|
8268
|
+
error,
|
|
8269
|
+
stage,
|
|
8270
|
+
cancelled: options.signal?.aborted === true,
|
|
8271
|
+
deadlineExpired: options.deadlineAt !== void 0 && now() >= options.deadlineAt
|
|
8272
|
+
}, {
|
|
8273
|
+
key,
|
|
8274
|
+
keySource,
|
|
8275
|
+
elapsedMs: performance.now() - start,
|
|
8276
|
+
blockThreshold
|
|
8277
|
+
}, config);
|
|
7926
8278
|
}
|
|
7927
8279
|
}
|
|
7928
8280
|
//#endregion
|
|
@@ -7980,7 +8332,9 @@ async function classifyWithModel(payload, config, options = {}) {
|
|
|
7980
8332
|
text: directMessage
|
|
7981
8333
|
}];
|
|
7982
8334
|
const settings = await (options.claudeSettings ?? loadClaudeSettings(null, host));
|
|
7983
|
-
const
|
|
8335
|
+
const checkout = await (options.checkout ?? loadCheckoutFacts(payload.cwd, host.env, { findCheckout: options.findCheckout }).catch(() => null));
|
|
8336
|
+
const repositoryContext = checkout === null ? null : buildRepositoryEvidence(checkout, options.taskScope);
|
|
8337
|
+
const mcpServers = await loadMCPServers(payload.cwd, host, settings.userSettings, checkout ?? void 0).catch(() => []);
|
|
7984
8338
|
const user = buildUserMessage(payload, transcript, config.provider.reasoning, repositoryContext, mcpServers);
|
|
7985
8339
|
try {
|
|
7986
8340
|
const timeout = options.timeout ?? ((ms) => AbortSignal.timeout(ms));
|
|
@@ -8152,25 +8506,26 @@ function classifyLocally(payload) {
|
|
|
8152
8506
|
}
|
|
8153
8507
|
function classifySegment(text, cwd) {
|
|
8154
8508
|
const words = splitWords(text);
|
|
8155
|
-
const start = words.findIndex((word) => !/^[A-Za-z_][A-Za-z0-9_]*=/.test(word));
|
|
8509
|
+
const start = words.findIndex((word) => !/^[A-Za-z_][A-Za-z0-9_]*=/.test(word.raw));
|
|
8156
8510
|
if (start === -1) return "Read-only actions";
|
|
8157
8511
|
const head = words[start];
|
|
8158
8512
|
invariant(head !== void 0, "findIndex returned an index the array holds");
|
|
8159
8513
|
const rest = words.slice(start + 1);
|
|
8160
|
-
const name = head.slice(head.lastIndexOf("/") + 1);
|
|
8514
|
+
const name = head.raw.slice(head.raw.lastIndexOf("/") + 1);
|
|
8161
8515
|
if (READ_ONLY_COMMANDS.has(name)) return "Read-only actions";
|
|
8162
8516
|
if (name === "git") {
|
|
8163
|
-
const sub = rest.find((word) => !word.startsWith("-"));
|
|
8164
|
-
return sub !== void 0 && READ_ONLY_GIT_SUBCOMMANDS.has(sub) ? "Read-only actions" : null;
|
|
8517
|
+
const sub = rest.find((word) => !word.raw.startsWith("-"));
|
|
8518
|
+
return sub !== void 0 && READ_ONLY_GIT_SUBCOMMANDS.has(sub.raw) ? "Read-only actions" : null;
|
|
8165
8519
|
}
|
|
8166
8520
|
if (name === "rm") return classifyRemove(rest, cwd);
|
|
8167
8521
|
return null;
|
|
8168
8522
|
}
|
|
8169
8523
|
function classifyRemove(args, cwd) {
|
|
8170
|
-
const paths = args.filter((arg) => !arg.startsWith("-"));
|
|
8524
|
+
const paths = args.filter((arg) => !arg.raw.startsWith("-"));
|
|
8171
8525
|
if (paths.length === 0) return null;
|
|
8172
8526
|
for (const path of paths) {
|
|
8173
|
-
|
|
8527
|
+
if (!isPlainOperand(path)) return null;
|
|
8528
|
+
const stripped = path.value;
|
|
8174
8529
|
if (stripped.includes("*") || stripped.includes("~") || stripped.includes("$")) return null;
|
|
8175
8530
|
const resolved = isAbsolute(stripped) ? normalize(stripped) : normalize(`${cwd}/${stripped}`);
|
|
8176
8531
|
const within = relative(cwd, resolved);
|
|
@@ -8181,11 +8536,17 @@ function classifyRemove(args, cwd) {
|
|
|
8181
8536
|
return "Regenerable output";
|
|
8182
8537
|
}
|
|
8183
8538
|
function splitWords(text) {
|
|
8184
|
-
return text
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8539
|
+
return splitShellTokens(text).tokens.flatMap((token) => token.kind === "word" ? [{
|
|
8540
|
+
raw: token.raw,
|
|
8541
|
+
value: token.value
|
|
8542
|
+
}] : []);
|
|
8543
|
+
}
|
|
8544
|
+
function isPlainOperand(word) {
|
|
8545
|
+
return [
|
|
8546
|
+
word.value,
|
|
8547
|
+
`'${word.value}'`,
|
|
8548
|
+
`"${word.value}"`
|
|
8549
|
+
].includes(word.raw) && !/\s/u.test(word.value);
|
|
8189
8550
|
}
|
|
8190
8551
|
//#endregion
|
|
8191
8552
|
//#region src/scope/merge-scope-facts.ts
|
|
@@ -8355,19 +8716,8 @@ const sessionScopeSchema = strictObject({
|
|
|
8355
8716
|
branches: array(branchSchema),
|
|
8356
8717
|
pullRequests: array(pullRequestSchema)
|
|
8357
8718
|
});
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
try {
|
|
8361
|
-
raw = await readFile(path, "utf8");
|
|
8362
|
-
} catch {
|
|
8363
|
-
return EMPTY_SESSION_SCOPE;
|
|
8364
|
-
}
|
|
8365
|
-
try {
|
|
8366
|
-
const parsed = sessionScopeSchema.safeParse(JSON.parse(raw));
|
|
8367
|
-
return parsed.success ? parsed.data : EMPTY_SESSION_SCOPE;
|
|
8368
|
-
} catch {
|
|
8369
|
-
return EMPTY_SESSION_SCOPE;
|
|
8370
|
-
}
|
|
8719
|
+
function loadSessionScope(path) {
|
|
8720
|
+
return loadJSONFile(path, sessionScopeSchema, EMPTY_SESSION_SCOPE);
|
|
8371
8721
|
}
|
|
8372
8722
|
//#endregion
|
|
8373
8723
|
//#region src/scope/resolve-session-scope-path.ts
|
|
@@ -8378,33 +8728,31 @@ function resolveSessionScopePath(stateDir, sessionID) {
|
|
|
8378
8728
|
//#endregion
|
|
8379
8729
|
//#region src/scope/pick-scope-source-reader.ts
|
|
8380
8730
|
function pickScopeSourceReader(source, findSourceCheckout = findCheckout) {
|
|
8381
|
-
|
|
8731
|
+
return M(source.kind).returnType().with("cwd", () => (context) => Promise.resolve(buildFacts({
|
|
8382
8732
|
worktrees: [context.worktree],
|
|
8383
8733
|
branches: context.branch === null ? [] : [context.branch]
|
|
8384
|
-
}))
|
|
8385
|
-
if (source.kind === "session") return async (context) => {
|
|
8734
|
+
}))).with("session", () => async (context) => {
|
|
8386
8735
|
const scope = await loadSessionScope(resolveSessionScopePath(context.stateDir, context.sessionID));
|
|
8387
8736
|
return buildFacts({
|
|
8388
8737
|
worktrees: scope.worktrees,
|
|
8389
8738
|
branches: scope.branches.filter((branch) => branch.commonDir === context.commonDir).map((branch) => branch.name),
|
|
8390
8739
|
pullRequests: scope.pullRequests
|
|
8391
8740
|
});
|
|
8392
|
-
};
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
};
|
|
8741
|
+
}).with("globs", () => () => Promise.resolve(buildFacts({ pathGlobs: source.paths ?? [] }))).with("atc", () => (context) => readAtcScopeFacts(context, findSourceCheckout)).exhaustive();
|
|
8742
|
+
}
|
|
8743
|
+
async function readAtcScopeFacts(context, findSourceCheckout) {
|
|
8744
|
+
const loaded = await loadAtcSessionRecord(context.atcRecordPath, context.atcSessionID);
|
|
8745
|
+
if (loaded.kind === "malformed") context.stderr.write(`auto-mode: ${loaded.diagnostic}\n`);
|
|
8746
|
+
if (loaded.kind !== "record") return EMPTY_SCOPE_FACTS;
|
|
8747
|
+
const scope = loaded.record.scope;
|
|
8748
|
+
const paths = /* @__PURE__ */ new Set([
|
|
8749
|
+
scope.workspace.path,
|
|
8750
|
+
...scope.worktrees.map((worktree) => worktree.path),
|
|
8751
|
+
...scope.branches.map((branch) => branch.repo ?? scope.workspace.path)
|
|
8752
|
+
]);
|
|
8753
|
+
const entries = await Promise.all([...paths].map((path) => findCommonDirEntry(path, context.env, findSourceCheckout)));
|
|
8754
|
+
const commonDirs = new Map(entries);
|
|
8755
|
+
return buildAtcScopeFacts(loaded.record, (path) => context.commonDir !== null && commonDirs.get(path) === context.commonDir);
|
|
8408
8756
|
}
|
|
8409
8757
|
async function findCommonDirEntry(path, env, findSourceCheckout) {
|
|
8410
8758
|
return [path, (await findSourceCheckout(path, env))?.commonDir ?? null];
|
|
@@ -8419,8 +8767,9 @@ function buildFacts(facts) {
|
|
|
8419
8767
|
//#region src/scope/load-task-scope.ts
|
|
8420
8768
|
async function loadTaskScope(request, sources, host, stderr) {
|
|
8421
8769
|
const cwd = resolve(request.cwd);
|
|
8422
|
-
const
|
|
8423
|
-
const
|
|
8770
|
+
const facts = await (request.checkout ?? loadCheckoutFacts(cwd, host.env, { findCheckout: request.findCheckout }));
|
|
8771
|
+
const checkout = facts.checkout;
|
|
8772
|
+
const branch = facts.head?.branch ?? null;
|
|
8424
8773
|
const sourceContext = {
|
|
8425
8774
|
env: host.env,
|
|
8426
8775
|
sessionID: request.sessionID,
|
|
@@ -8433,19 +8782,71 @@ async function loadTaskScope(request, sources, host, stderr) {
|
|
|
8433
8782
|
atcSessionID: host.env["ATC_SESSION_ID"],
|
|
8434
8783
|
stderr
|
|
8435
8784
|
};
|
|
8436
|
-
const
|
|
8785
|
+
const sourceFacts = await Promise.all(Object.values(sources).map((source) => pickScopeSourceReader(source, request.findCheckout)(sourceContext)));
|
|
8437
8786
|
return buildTaskScope({
|
|
8438
8787
|
home: host.home,
|
|
8439
8788
|
currentBranch: branch,
|
|
8440
|
-
defaultBranch:
|
|
8441
|
-
remotes,
|
|
8442
|
-
facts
|
|
8789
|
+
defaultBranch: facts.head?.defaultBranch ?? null,
|
|
8790
|
+
remotes: facts.remotes,
|
|
8791
|
+
facts: sourceFacts
|
|
8443
8792
|
});
|
|
8444
8793
|
}
|
|
8445
8794
|
//#endregion
|
|
8446
8795
|
//#region src/classify-action.ts
|
|
8447
8796
|
async function classifyAction(request, config, options = {}) {
|
|
8448
8797
|
const host = options.host ?? readHostEnvironment();
|
|
8798
|
+
const stderr = options.stderr ?? process.stderr;
|
|
8799
|
+
const invocationID = randomUUID();
|
|
8800
|
+
if (options.actionLog === true) await writeActionDiagnostic(request, {
|
|
8801
|
+
invocationID,
|
|
8802
|
+
status: "started"
|
|
8803
|
+
}, host, stderr);
|
|
8804
|
+
const outcome = options.budget === true ? await classifyWithBudget(request, config, {
|
|
8805
|
+
...options,
|
|
8806
|
+
host,
|
|
8807
|
+
stderr
|
|
8808
|
+
}) : await classifyStages(request, config, {
|
|
8809
|
+
...options,
|
|
8810
|
+
host,
|
|
8811
|
+
stderr
|
|
8812
|
+
});
|
|
8813
|
+
if (options.actionLog === true) await writeActionDiagnostic(request, {
|
|
8814
|
+
invocationID,
|
|
8815
|
+
status: outcome.status,
|
|
8816
|
+
verdict: outcome.verdict?.kind ?? "defer",
|
|
8817
|
+
decidingStage: outcome.decidingStage,
|
|
8818
|
+
...outcome.denials === void 0 ? {} : { denials: outcome.denials },
|
|
8819
|
+
...outcome.escalation === void 0 ? {} : { escalation: outcome.escalation },
|
|
8820
|
+
...outcome.diagnostics === void 0 ? {} : { diagnostics: outcome.diagnostics },
|
|
8821
|
+
...outcome.judge === void 0 ? {} : { judge: outcome.judge }
|
|
8822
|
+
}, host, stderr);
|
|
8823
|
+
return outcome;
|
|
8824
|
+
}
|
|
8825
|
+
async function classifyWithBudget(request, config, options) {
|
|
8826
|
+
const statePath = resolveDenialStatePath(request, resolveXDGDir(options.host, "state"));
|
|
8827
|
+
const retryKey = buildRetryKey(request);
|
|
8828
|
+
const retry = findRetryDeny(await loadDenialState(statePath), retryKey);
|
|
8829
|
+
const outcome = retry === null ? await classifyStages(request, config, options) : {
|
|
8830
|
+
verdict: retry,
|
|
8831
|
+
decidingStage: "retry",
|
|
8832
|
+
note: "retry of the action just denied; denied again",
|
|
8833
|
+
status: "deny"
|
|
8834
|
+
};
|
|
8835
|
+
const plan = await tryUpdateDenialBudget(options.stderr, statePath, (state) => planDenialBudget(state, outcome.verdict, retryKey, config.denialBudget ?? DEFAULT_DENIAL_BUDGET));
|
|
8836
|
+
return {
|
|
8837
|
+
...outcome,
|
|
8838
|
+
verdict: plan.verdict,
|
|
8839
|
+
decidingStage: plan.escalation ? "budget" : outcome.decidingStage,
|
|
8840
|
+
note: plan.escalation ? "denial budget exhausted; the user decides this action" : outcome.note,
|
|
8841
|
+
denials: {
|
|
8842
|
+
consecutive: plan.state.consecutive,
|
|
8843
|
+
session: plan.state.session
|
|
8844
|
+
},
|
|
8845
|
+
escalation: plan.escalation
|
|
8846
|
+
};
|
|
8847
|
+
}
|
|
8848
|
+
async function classifyStages(request, config, options) {
|
|
8849
|
+
const host = options.host;
|
|
8449
8850
|
let claudeSettings = options.claudeSettings;
|
|
8450
8851
|
if (config.provider.protocol === "system-one" && claudeSettings === void 0) try {
|
|
8451
8852
|
claudeSettings = await loadClaudeSettings(config.claudeSettingsPath, host);
|
|
@@ -8464,7 +8865,14 @@ async function classifyAction(request, config, options = {}) {
|
|
|
8464
8865
|
const hasConfiguredDenies = configured !== void 0 && (configured.hard_deny.length > 0 || configured.soft_deny.length > 0);
|
|
8465
8866
|
const local = hasConfiguredDenies ? { kind: "escalate" } : classifyLocally(request);
|
|
8466
8867
|
if (local.kind === "allow" && local.exception === READ_ONLY_EXCEPTION) return buildLocalAllow(local.exception);
|
|
8467
|
-
const
|
|
8868
|
+
const findActionCheckout = makeOnceCheckoutFinder(options.findCheckout ?? findCheckout);
|
|
8869
|
+
const checkout = await loadCheckoutFacts(request.cwd, host.env, { findCheckout: findActionCheckout }).catch(() => null);
|
|
8870
|
+
const scope = await tryLoadTaskScope({
|
|
8871
|
+
sessionID: request.sessionID,
|
|
8872
|
+
cwd: request.cwd,
|
|
8873
|
+
checkout,
|
|
8874
|
+
findCheckout: findActionCheckout
|
|
8875
|
+
}, config, host, options.stderr);
|
|
8468
8876
|
const containment = scope === null ? null : checkContainment(request, scope, host.scratchPaths);
|
|
8469
8877
|
if (containment !== null) {
|
|
8470
8878
|
const guidance = await tryReadDenialGuidance();
|
|
@@ -8476,7 +8884,7 @@ async function classifyAction(request, config, options = {}) {
|
|
|
8476
8884
|
};
|
|
8477
8885
|
}
|
|
8478
8886
|
if (local.kind === "allow") return buildLocalAllow(local.exception);
|
|
8479
|
-
const edit = hasConfiguredDenies || scope === null ? null : await tryClassifyEdit(request, scope, host);
|
|
8887
|
+
const edit = hasConfiguredDenies || scope === null ? null : await tryClassifyEdit(request, scope, host, { findCheckout: findActionCheckout });
|
|
8480
8888
|
if (edit?.kind === "bypass") return {
|
|
8481
8889
|
verdict: { kind: "allow" },
|
|
8482
8890
|
decidingStage: "bypass",
|
|
@@ -8494,7 +8902,9 @@ async function classifyAction(request, config, options = {}) {
|
|
|
8494
8902
|
...options,
|
|
8495
8903
|
taskScope,
|
|
8496
8904
|
host,
|
|
8497
|
-
claudeSettings
|
|
8905
|
+
claudeSettings,
|
|
8906
|
+
checkout: checkout ?? void 0,
|
|
8907
|
+
findCheckout: findActionCheckout
|
|
8498
8908
|
}), tryReadDenialGuidance()]);
|
|
8499
8909
|
const judge = config.judge ?? null;
|
|
8500
8910
|
if (outcome.verdict?.kind === "deny" && outcome.review !== void 0 && judge !== null) {
|
|
@@ -8502,7 +8912,7 @@ async function classifyAction(request, config, options = {}) {
|
|
|
8502
8912
|
...options,
|
|
8503
8913
|
host,
|
|
8504
8914
|
rulesPath: config.rulesPath,
|
|
8505
|
-
overturns: config.judgeOverturns ?? "
|
|
8915
|
+
overturns: config.judgeOverturns ?? "any"
|
|
8506
8916
|
});
|
|
8507
8917
|
const { review: _review, ...rest } = outcome;
|
|
8508
8918
|
return {
|
|
@@ -8526,14 +8936,22 @@ const READ_ONLY_EXCEPTION = "Read-only actions";
|
|
|
8526
8936
|
async function tryLoadTaskScope(request, config, host, stderr) {
|
|
8527
8937
|
try {
|
|
8528
8938
|
return await loadTaskScope({
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
stateDir:
|
|
8939
|
+
...request,
|
|
8940
|
+
checkout: request.checkout ?? void 0,
|
|
8941
|
+
stateDir: resolveXDGDir(host, "state")
|
|
8532
8942
|
}, config.scopeSources ?? DEFAULT_SCOPE_SOURCES, host, stderr);
|
|
8533
8943
|
} catch {
|
|
8534
8944
|
return null;
|
|
8535
8945
|
}
|
|
8536
8946
|
}
|
|
8947
|
+
function makeOnceCheckoutFinder(find) {
|
|
8948
|
+
const found = /* @__PURE__ */ new Map();
|
|
8949
|
+
return (start, env) => {
|
|
8950
|
+
const cached = found.get(start) ?? find(start, env);
|
|
8951
|
+
found.set(start, cached);
|
|
8952
|
+
return cached;
|
|
8953
|
+
};
|
|
8954
|
+
}
|
|
8537
8955
|
function buildLocalAllow(exception) {
|
|
8538
8956
|
return {
|
|
8539
8957
|
verdict: { kind: "allow" },
|
|
@@ -8557,6 +8975,279 @@ async function tryReadDenialGuidance() {
|
|
|
8557
8975
|
return null;
|
|
8558
8976
|
}
|
|
8559
8977
|
}
|
|
8978
|
+
async function tryUpdateDenialBudget(stderr, path, plan) {
|
|
8979
|
+
const unlocked = await loadDenialState(path);
|
|
8980
|
+
const unlockedPlan = plan(unlocked);
|
|
8981
|
+
if (unlockedPlan.state === unlocked) return unlockedPlan;
|
|
8982
|
+
try {
|
|
8983
|
+
return plan((await updateDenialState(path, (state) => plan(state).state)).previous);
|
|
8984
|
+
} catch {
|
|
8985
|
+
stderr.write("auto-mode: denial counts unavailable\n");
|
|
8986
|
+
return unlockedPlan;
|
|
8987
|
+
}
|
|
8988
|
+
}
|
|
8989
|
+
//#endregion
|
|
8990
|
+
//#region src/scope/read-pull-request.ts
|
|
8991
|
+
const LOOKUP_TIMEOUT_MS = 5e3;
|
|
8992
|
+
const MAX_LOOKUP_OUTPUT_BYTES = 65536;
|
|
8993
|
+
async function readPullRequest(repository, number, options = {}) {
|
|
8994
|
+
const [file, ...prefix] = options.command ?? ["gh"];
|
|
8995
|
+
const args = [
|
|
8996
|
+
"pr",
|
|
8997
|
+
"view",
|
|
8998
|
+
String(number),
|
|
8999
|
+
"--repo",
|
|
9000
|
+
repository,
|
|
9001
|
+
"--json",
|
|
9002
|
+
"headRefName,createdAt"
|
|
9003
|
+
];
|
|
9004
|
+
const result = await runCapped(file, [
|
|
9005
|
+
...prefix,
|
|
9006
|
+
...args,
|
|
9007
|
+
"--jq",
|
|
9008
|
+
"[.headRefName, .createdAt] | @tsv"
|
|
9009
|
+
], {
|
|
9010
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? LOOKUP_TIMEOUT_MS),
|
|
9011
|
+
maxOutputBytes: MAX_LOOKUP_OUTPUT_BYTES
|
|
9012
|
+
});
|
|
9013
|
+
const [head = "", created = ""] = result.stdout.trim().split(" ");
|
|
9014
|
+
const createdAt = Date.parse(created);
|
|
9015
|
+
return result.exitCode === 0 && head !== "" && !Number.isNaN(createdAt) ? {
|
|
9016
|
+
head,
|
|
9017
|
+
createdAt
|
|
9018
|
+
} : null;
|
|
9019
|
+
}
|
|
9020
|
+
//#endregion
|
|
9021
|
+
//#region src/git/read-branch-created-at.ts
|
|
9022
|
+
async function readBranchCreatedAt(commonDir, branch) {
|
|
9023
|
+
if (!/^[\w./-]+$/u.test(branch) || branch.split("/").includes("..")) return null;
|
|
9024
|
+
const [first = ""] = (await readFile(join(commonDir, "logs", "refs", "heads", branch), "utf8").catch(() => "")).split("\n");
|
|
9025
|
+
const seconds = /^0+ [\da-f]+ .* (?<time>\d+) [+-]\d{4}\t/u.exec(first)?.groups?.["time"];
|
|
9026
|
+
return seconds === void 0 ? null : Number(seconds) * 1e3;
|
|
9027
|
+
}
|
|
9028
|
+
//#endregion
|
|
9029
|
+
//#region src/scope/collect-pull-request-addresses.ts
|
|
9030
|
+
function collectPullRequestAddresses(text) {
|
|
9031
|
+
const addresses = /* @__PURE__ */ new Map();
|
|
9032
|
+
for (const match of text.matchAll(PULL_REQUEST_URL)) {
|
|
9033
|
+
const repository = toRepositorySlug(`https://${match.groups?.["repository"] ?? ""}`);
|
|
9034
|
+
const number = Number(match.groups?.["number"]);
|
|
9035
|
+
if (repository !== null) addresses.set(`${repository}#${String(number)}`, {
|
|
9036
|
+
repository,
|
|
9037
|
+
number
|
|
9038
|
+
});
|
|
9039
|
+
}
|
|
9040
|
+
return [...addresses.values()];
|
|
9041
|
+
}
|
|
9042
|
+
const PULL_REQUEST_URL = /https:\/\/(?<repository>[^/\s]+\/[^/\s]+\/[^/\s]+)\/pull\/(?<number>[1-9]\d*)\b/gu;
|
|
9043
|
+
//#endregion
|
|
9044
|
+
//#region mods/auto-mode/contract/scope-commands.ts
|
|
9045
|
+
const SCOPE_COMMANDS = {
|
|
9046
|
+
git: [
|
|
9047
|
+
"worktree add",
|
|
9048
|
+
"checkout",
|
|
9049
|
+
"switch",
|
|
9050
|
+
"branch"
|
|
9051
|
+
],
|
|
9052
|
+
gh: ["pr create"]
|
|
9053
|
+
};
|
|
9054
|
+
//#endregion
|
|
9055
|
+
//#region src/scope/collect-scope-events.ts
|
|
9056
|
+
function collectScopeEvents(command, cwd, home) {
|
|
9057
|
+
return collectShellCommands(command, cwd, home).flatMap((entry) => {
|
|
9058
|
+
const [name, ...args] = entry.words;
|
|
9059
|
+
const directory = entry.directory;
|
|
9060
|
+
if (directory === null) return [];
|
|
9061
|
+
if (name === "git") return collectGitEvents(args, directory, home);
|
|
9062
|
+
return name === "gh" && findScopeCommand(SCOPE_COMMANDS.gh, args) !== null ? [{
|
|
9063
|
+
kind: "pull-request",
|
|
9064
|
+
directory,
|
|
9065
|
+
head: findOptionValue(args, ["--head", "-H"])
|
|
9066
|
+
}] : [];
|
|
9067
|
+
});
|
|
9068
|
+
}
|
|
9069
|
+
function collectGitEvents(args, cwd, home) {
|
|
9070
|
+
const git = splitGitCommand(args, cwd, home);
|
|
9071
|
+
const found = findScopeCommand(SCOPE_COMMANDS.git, git.args);
|
|
9072
|
+
if (git.directory === null || found === null) return [];
|
|
9073
|
+
return GIT_EVENT_COLLECTORS[found.command](found.rest, git.directory, home);
|
|
9074
|
+
}
|
|
9075
|
+
function findScopeCommand(commands, args) {
|
|
9076
|
+
for (const command of commands) {
|
|
9077
|
+
const words = command.split(" ");
|
|
9078
|
+
if (words.every((word, index) => args[index] === word)) return {
|
|
9079
|
+
command,
|
|
9080
|
+
rest: args.slice(words.length)
|
|
9081
|
+
};
|
|
9082
|
+
}
|
|
9083
|
+
return null;
|
|
9084
|
+
}
|
|
9085
|
+
const GIT_EVENT_COLLECTORS = {
|
|
9086
|
+
"worktree add": collectWorktreeEvents,
|
|
9087
|
+
checkout: collectSwitchEvents,
|
|
9088
|
+
switch: collectSwitchEvents,
|
|
9089
|
+
branch: collectBranchEvents
|
|
9090
|
+
};
|
|
9091
|
+
function collectWorktreeEvents(rest, directory, home) {
|
|
9092
|
+
const path = findWorktreePath(rest);
|
|
9093
|
+
const resolved = path === null ? null : resolveShellPath(directory, path, home);
|
|
9094
|
+
return resolved === null ? [] : [{
|
|
9095
|
+
kind: "worktree",
|
|
9096
|
+
path: resolved
|
|
9097
|
+
}];
|
|
9098
|
+
}
|
|
9099
|
+
const WORKTREE_VALUE_OPTIONS = /* @__PURE__ */ new Set([
|
|
9100
|
+
"-b",
|
|
9101
|
+
"-B",
|
|
9102
|
+
"--reason"
|
|
9103
|
+
]);
|
|
9104
|
+
function findWorktreePath(args) {
|
|
9105
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
9106
|
+
const word = args[index] ?? "";
|
|
9107
|
+
if (WORKTREE_VALUE_OPTIONS.has(word)) index += 1;
|
|
9108
|
+
else if (!word.startsWith("-")) return word;
|
|
9109
|
+
}
|
|
9110
|
+
return null;
|
|
9111
|
+
}
|
|
9112
|
+
const BRANCH_CREATE_OPTIONS = /* @__PURE__ */ new Set([
|
|
9113
|
+
"-b",
|
|
9114
|
+
"-B",
|
|
9115
|
+
"-c",
|
|
9116
|
+
"-C",
|
|
9117
|
+
"--create",
|
|
9118
|
+
"--force-create"
|
|
9119
|
+
]);
|
|
9120
|
+
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;
|
|
9121
|
+
function collectSwitchEvents(rest, directory) {
|
|
9122
|
+
const name = findOptionValue(rest, [...BRANCH_CREATE_OPTIONS]);
|
|
9123
|
+
return name === null ? [] : [{
|
|
9124
|
+
kind: "branch",
|
|
9125
|
+
name,
|
|
9126
|
+
directory
|
|
9127
|
+
}];
|
|
9128
|
+
}
|
|
9129
|
+
function collectBranchEvents(rest, directory) {
|
|
9130
|
+
if (rest.some((word) => BRANCH_OTHER_OPTIONS.test(word))) return [];
|
|
9131
|
+
const name = rest.find((word) => !word.startsWith("-"));
|
|
9132
|
+
return name === void 0 || isUnresolvedWord(name) ? [] : [{
|
|
9133
|
+
kind: "branch",
|
|
9134
|
+
name,
|
|
9135
|
+
directory
|
|
9136
|
+
}];
|
|
9137
|
+
}
|
|
9138
|
+
function findOptionValue(args, options) {
|
|
9139
|
+
const index = args.findIndex((word) => options.includes(word));
|
|
9140
|
+
const value = index === -1 ? void 0 : args[index + 1];
|
|
9141
|
+
return value === void 0 || isUnresolvedWord(value) ? null : value;
|
|
9142
|
+
}
|
|
9143
|
+
//#endregion
|
|
9144
|
+
//#region src/scope/merge-session-scope.ts
|
|
9145
|
+
function mergeSessionScope(all) {
|
|
9146
|
+
const branches = /* @__PURE__ */ new Map();
|
|
9147
|
+
const pullRequests = /* @__PURE__ */ new Map();
|
|
9148
|
+
for (const branch of all.flatMap((scope) => scope.branches)) branches.set(`${branch.commonDir}\0${branch.name}`, branch);
|
|
9149
|
+
for (const pull of all.flatMap((scope) => scope.pullRequests)) pullRequests.set(`${pull.repository}#${String(pull.number)}`, pull);
|
|
9150
|
+
return {
|
|
9151
|
+
worktrees: [...new Set(all.flatMap((scope) => scope.worktrees))],
|
|
9152
|
+
branches: [...branches.values()],
|
|
9153
|
+
pullRequests: [...pullRequests.values()]
|
|
9154
|
+
};
|
|
9155
|
+
}
|
|
9156
|
+
//#endregion
|
|
9157
|
+
//#region src/scope/write-session-scope.ts
|
|
9158
|
+
async function writeSessionScope(path, added, clock) {
|
|
9159
|
+
await updateJSONFile(path, loadSessionScope, (current) => mergeSessionScope([current, added]), clock);
|
|
9160
|
+
}
|
|
9161
|
+
//#endregion
|
|
9162
|
+
//#region src/scope/update-session-scope.ts
|
|
9163
|
+
const MAX_CALL_AGE_MS = 9e5;
|
|
9164
|
+
async function updateSessionScope(request, options) {
|
|
9165
|
+
const age = options.now - request.startedAt;
|
|
9166
|
+
if (age < -6e4 || age > MAX_CALL_AGE_MS) return EMPTY_SESSION_SCOPE;
|
|
9167
|
+
const events = collectScopeEvents(request.command, request.cwd, options.home);
|
|
9168
|
+
const scope = mergeSessionScope(await Promise.all(events.map((event) => tryVerifyScopeEvent(event, request, options))));
|
|
9169
|
+
if (scope.worktrees.length + scope.branches.length + scope.pullRequests.length > 0) await writeSessionScope(resolveSessionScopePath(options.stateDir, request.sessionID), scope, options.lockClock);
|
|
9170
|
+
return scope;
|
|
9171
|
+
}
|
|
9172
|
+
async function tryVerifyScopeEvent(event, request, options) {
|
|
9173
|
+
try {
|
|
9174
|
+
return await (options.verifyEvent === void 0 ? verifyScopeEvent(event, request, options) : options.verifyEvent(event, request));
|
|
9175
|
+
} catch {
|
|
9176
|
+
return EMPTY_SESSION_SCOPE;
|
|
9177
|
+
}
|
|
9178
|
+
}
|
|
9179
|
+
const FORGE_CLOCK_SKEW_MS = 6e4;
|
|
9180
|
+
const FILE_CLOCK_SLACK_MS = 50;
|
|
9181
|
+
function verifyScopeEvent(event, request, options) {
|
|
9182
|
+
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();
|
|
9183
|
+
}
|
|
9184
|
+
async function verifyWorktreeEvent(path, request, options) {
|
|
9185
|
+
const link = await stat(join(path, ".git"));
|
|
9186
|
+
const facts = await loadCheckoutFacts(path, options.env);
|
|
9187
|
+
const checkout = facts.checkout;
|
|
9188
|
+
if (!link.isFile() || link.mtimeMs < request.startedAt - FILE_CLOCK_SLACK_MS || checkout?.worktree !== path) return EMPTY_SESSION_SCOPE;
|
|
9189
|
+
const branch = facts.head?.branch ?? null;
|
|
9190
|
+
if (branch === null) return {
|
|
9191
|
+
...EMPTY_SESSION_SCOPE,
|
|
9192
|
+
worktrees: [path]
|
|
9193
|
+
};
|
|
9194
|
+
const isNew = await isBranchCreatedSince(checkout.commonDir, branch, request.startedAt);
|
|
9195
|
+
return {
|
|
9196
|
+
...EMPTY_SESSION_SCOPE,
|
|
9197
|
+
worktrees: [path],
|
|
9198
|
+
branches: isNew ? [{
|
|
9199
|
+
name: branch,
|
|
9200
|
+
commonDir: checkout.commonDir
|
|
9201
|
+
}] : []
|
|
9202
|
+
};
|
|
9203
|
+
}
|
|
9204
|
+
async function verifyBranchEvent(event, request, options) {
|
|
9205
|
+
const checkout = await findCheckout(event.directory, options.env);
|
|
9206
|
+
if (checkout === null) return EMPTY_SESSION_SCOPE;
|
|
9207
|
+
return await isBranchCreatedSince(checkout.commonDir, event.name, request.startedAt) ? {
|
|
9208
|
+
...EMPTY_SESSION_SCOPE,
|
|
9209
|
+
branches: [{
|
|
9210
|
+
name: event.name,
|
|
9211
|
+
commonDir: checkout.commonDir
|
|
9212
|
+
}]
|
|
9213
|
+
} : EMPTY_SESSION_SCOPE;
|
|
9214
|
+
}
|
|
9215
|
+
async function verifyPullRequestEvent(directory, request, options) {
|
|
9216
|
+
const facts = await loadCheckoutFacts(directory, options.env);
|
|
9217
|
+
if (facts.checkout === null) return EMPTY_SESSION_SCOPE;
|
|
9218
|
+
const repositories = new Set(facts.remotes.map((remote) => toRepositorySlug(remote.url)));
|
|
9219
|
+
const addresses = collectPullRequestAddresses(request.resultText).filter((address) => repositories.has(address.repository));
|
|
9220
|
+
const pulls = await Promise.all(addresses.map(async (address) => {
|
|
9221
|
+
const pull = await options.readPullRequest(address.repository, address.number);
|
|
9222
|
+
return pull !== null && pull.createdAt >= request.startedAt - FORGE_CLOCK_SKEW_MS ? [{
|
|
9223
|
+
...address,
|
|
9224
|
+
head: pull.head
|
|
9225
|
+
}] : [];
|
|
9226
|
+
}));
|
|
9227
|
+
return {
|
|
9228
|
+
...EMPTY_SESSION_SCOPE,
|
|
9229
|
+
pullRequests: pulls.flat()
|
|
9230
|
+
};
|
|
9231
|
+
}
|
|
9232
|
+
async function isBranchCreatedSince(commonDir, branch, since) {
|
|
9233
|
+
const createdAt = await readBranchCreatedAt(commonDir, branch);
|
|
9234
|
+
if (createdAt === null || createdAt < Math.floor(since / 1e3) * 1e3) return false;
|
|
9235
|
+
const ref = await stat(join(commonDir, "refs", "heads", branch)).catch(() => null);
|
|
9236
|
+
return ref !== null && ref.mtimeMs >= since - FILE_CLOCK_SLACK_MS;
|
|
9237
|
+
}
|
|
9238
|
+
//#endregion
|
|
9239
|
+
//#region src/record-scope.ts
|
|
9240
|
+
async function recordScope(request, config, options = {}) {
|
|
9241
|
+
const host = options.host ?? readHostEnvironment();
|
|
9242
|
+
if (!Object.values(config.scopeSources ?? DEFAULT_SCOPE_SOURCES).some((source) => source.kind === "session")) return EMPTY_SESSION_SCOPE;
|
|
9243
|
+
return await updateSessionScope(request, {
|
|
9244
|
+
now: options.now ?? Date.now(),
|
|
9245
|
+
stateDir: resolveXDGDir(host, "state"),
|
|
9246
|
+
home: host.home,
|
|
9247
|
+
env: host.env,
|
|
9248
|
+
readPullRequest
|
|
9249
|
+
});
|
|
9250
|
+
}
|
|
8560
9251
|
//#endregion
|
|
8561
9252
|
//#region src/request/action-request-schema.ts
|
|
8562
9253
|
const userTaskSchema = strictObject({
|
|
@@ -8613,6 +9304,21 @@ function parseActionRequest(body) {
|
|
|
8613
9304
|
};
|
|
8614
9305
|
}
|
|
8615
9306
|
//#endregion
|
|
9307
|
+
//#region src/request/scope-record-request-schema.ts
|
|
9308
|
+
const scopeRecordRequestSchema = strictObject({
|
|
9309
|
+
sessionID: string().min(1),
|
|
9310
|
+
cwd: string().min(1),
|
|
9311
|
+
startedAt: number().int().positive(),
|
|
9312
|
+
command: string().min(1),
|
|
9313
|
+
resultText: string()
|
|
9314
|
+
});
|
|
9315
|
+
//#endregion
|
|
9316
|
+
//#region src/request/parse-scope-record-request.ts
|
|
9317
|
+
function parseScopeRecordRequest(body) {
|
|
9318
|
+
const parsed = scopeRecordRequestSchema.safeParse(body);
|
|
9319
|
+
return parsed.success ? parsed.data : null;
|
|
9320
|
+
}
|
|
9321
|
+
//#endregion
|
|
8616
9322
|
//#region src/request/render-verdict.ts
|
|
8617
9323
|
function renderVerdict(verdict) {
|
|
8618
9324
|
const rendered = M(verdict).returnType().with({ kind: "allow" }, () => ({ decision: "allow" })).with({ kind: "deny" }, (denied) => ({
|
|
@@ -8622,4 +9328,4 @@ function renderVerdict(verdict) {
|
|
|
8622
9328
|
return JSON.stringify(rendered);
|
|
8623
9329
|
}
|
|
8624
9330
|
//#endregion
|
|
8625
|
-
export {
|
|
9331
|
+
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 };
|