auto-mode 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{classify-edit-D8GC6b91.js → classify-edit-LpoeenGt.js} +4 -1
- package/dist/cli.js +134 -118
- package/dist/index.d.ts +14 -2
- package/dist/index.js +1 -1
- package/dist/{render-verdict-De8aTmg-.js → render-verdict-Bj2tGqBI.js} +474 -228
- package/mods/auto-mode/hooks/testing.d.ts +76 -24
- package/mods/auto-mode/hooks/types.ts +1 -1
- package/package.json +2 -2
- package/policy/classifier.md +4 -1
- package/policy/decision.md +2 -0
- package/policy/rules.md +18 -1
- package/mods/auto-mode/hooks/build-prompt-context.claude-check.ts +0 -93
- package/mods/auto-mode/hooks/register.claude-check.ts +0 -1226
|
@@ -1,22 +1,10 @@
|
|
|
1
1
|
import { t as getEditFields } from "./get-edit-fields-BtAvHIsq.js";
|
|
2
2
|
import { lstat, readFile, realpath, stat } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, isAbsolute, join, matchesGlob, normalize, parse, relative, resolve } from "node:path";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
4
|
import { spawn } from "node:child_process";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
|
-
//#region src/bypass/resolve-edit-target.ts
|
|
9
|
-
async function resolveEditTarget(path) {
|
|
10
|
-
const missing = [];
|
|
11
|
-
for (let current = path;; current = dirname(current)) try {
|
|
12
|
-
const real = await realpath(current);
|
|
13
|
-
return join(real, ...missing.toReversed());
|
|
14
|
-
} catch {
|
|
15
|
-
if (await lstat(current).catch(() => null) !== null || dirname(current) === current) return null;
|
|
16
|
-
missing.push(basename(current));
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
//#endregion
|
|
20
8
|
//#region node_modules/.bun/zod@4.6.2/node_modules/zod/v4/core/util.js
|
|
21
9
|
function getEnumValues(entries) {
|
|
22
10
|
const numericValues = Object.values(entries).filter((v) => typeof v === "number");
|
|
@@ -5586,9 +5574,15 @@ function findPreset(id) {
|
|
|
5586
5574
|
return Object.hasOwn(PRESETS, id) ? PRESETS[id] : void 0;
|
|
5587
5575
|
}
|
|
5588
5576
|
//#endregion
|
|
5577
|
+
//#region src/config/to-timer-delay.ts
|
|
5578
|
+
function toTimerDelay(ms) {
|
|
5579
|
+
return Math.max(1, Math.ceil(ms));
|
|
5580
|
+
}
|
|
5581
|
+
//#endregion
|
|
5589
5582
|
//#region src/config/read-api-key-from-command.ts
|
|
5590
5583
|
function readApiKeyFromCommand(command, options = {}) {
|
|
5591
|
-
const
|
|
5584
|
+
const now = options.now ?? Date.now;
|
|
5585
|
+
const remainingMs = options.deadlineAt === void 0 ? 5e3 : options.deadlineAt - now();
|
|
5592
5586
|
if (remainingMs <= 0 || options.signal?.aborted === true) return Promise.resolve(null);
|
|
5593
5587
|
return new Promise((resolve) => {
|
|
5594
5588
|
const grouped = process.platform !== "win32";
|
|
@@ -5598,7 +5592,11 @@ function readApiKeyFromCommand(command, options = {}) {
|
|
|
5598
5592
|
"ignore",
|
|
5599
5593
|
"pipe",
|
|
5600
5594
|
"ignore"
|
|
5601
|
-
]
|
|
5595
|
+
],
|
|
5596
|
+
...options.host === void 0 ? {} : { env: {
|
|
5597
|
+
HOME: options.host.home,
|
|
5598
|
+
...options.host.env
|
|
5599
|
+
} }
|
|
5602
5600
|
});
|
|
5603
5601
|
let output = "";
|
|
5604
5602
|
let settled = false;
|
|
@@ -5606,7 +5604,7 @@ function readApiKeyFromCommand(command, options = {}) {
|
|
|
5606
5604
|
const setResult = (value) => {
|
|
5607
5605
|
if (!settled) {
|
|
5608
5606
|
settled = true;
|
|
5609
|
-
|
|
5607
|
+
timer.removeEventListener("abort", stopChild);
|
|
5610
5608
|
options.signal?.removeEventListener("abort", stopChild);
|
|
5611
5609
|
resolve(value);
|
|
5612
5610
|
}
|
|
@@ -5621,8 +5619,10 @@ function readApiKeyFromCommand(command, options = {}) {
|
|
|
5621
5619
|
child.kill("SIGKILL");
|
|
5622
5620
|
}
|
|
5623
5621
|
};
|
|
5624
|
-
const timer =
|
|
5622
|
+
const timer = (options.timeout ?? ((ms) => AbortSignal.timeout(ms)))(toTimerDelay(Math.min(5e3, remainingMs)));
|
|
5623
|
+
timer.addEventListener("abort", stopChild, { once: true });
|
|
5625
5624
|
options.signal?.addEventListener("abort", stopChild, { once: true });
|
|
5625
|
+
if (timer.aborted) stopChild();
|
|
5626
5626
|
child.stdout.setEncoding("utf8");
|
|
5627
5627
|
child.stdout.on("data", (chunk) => {
|
|
5628
5628
|
if (stopped) return;
|
|
@@ -5639,6 +5639,23 @@ function readApiKeyFromCommand(command, options = {}) {
|
|
|
5639
5639
|
});
|
|
5640
5640
|
}
|
|
5641
5641
|
//#endregion
|
|
5642
|
+
//#region src/containment/scratch-paths.ts
|
|
5643
|
+
const SCRATCH_PATHS = [
|
|
5644
|
+
"/tmp",
|
|
5645
|
+
"/dev/null",
|
|
5646
|
+
"/dev/stdout",
|
|
5647
|
+
"/dev/stderr"
|
|
5648
|
+
];
|
|
5649
|
+
//#endregion
|
|
5650
|
+
//#region src/config/read-host-environment.ts
|
|
5651
|
+
function readHostEnvironment() {
|
|
5652
|
+
return {
|
|
5653
|
+
env: process.env,
|
|
5654
|
+
home: homedir(),
|
|
5655
|
+
scratchPaths: SCRATCH_PATHS
|
|
5656
|
+
};
|
|
5657
|
+
}
|
|
5658
|
+
//#endregion
|
|
5642
5659
|
//#region src/config/config.ts
|
|
5643
5660
|
const SCOPE_SOURCE_KINDS = [
|
|
5644
5661
|
"cwd",
|
|
@@ -5654,7 +5671,8 @@ const DEFAULT_DENIAL_BUDGET = {
|
|
|
5654
5671
|
};
|
|
5655
5672
|
const DEFAULT_SCOPE_SOURCES = {
|
|
5656
5673
|
cwd: { kind: "cwd" },
|
|
5657
|
-
session: { kind: "session" }
|
|
5674
|
+
session: { kind: "session" },
|
|
5675
|
+
atc: { kind: "atc" }
|
|
5658
5676
|
};
|
|
5659
5677
|
const DEFAULT_CONFIG = {
|
|
5660
5678
|
provider: DEFAULT_PROVIDER,
|
|
@@ -5663,12 +5681,13 @@ const DEFAULT_CONFIG = {
|
|
|
5663
5681
|
denialBudget: DEFAULT_DENIAL_BUDGET,
|
|
5664
5682
|
scopeSources: DEFAULT_SCOPE_SOURCES
|
|
5665
5683
|
};
|
|
5666
|
-
function resolveConfigPath() {
|
|
5667
|
-
const xdg =
|
|
5668
|
-
const base = xdg !== void 0 && xdg !== "" ? xdg : join(
|
|
5684
|
+
function resolveConfigPath(host = readHostEnvironment()) {
|
|
5685
|
+
const xdg = host.env["XDG_CONFIG_HOME"];
|
|
5686
|
+
const base = xdg !== void 0 && xdg !== "" ? xdg : join(host.home, ".config");
|
|
5669
5687
|
return join(base, "auto-mode", "config.json");
|
|
5670
5688
|
}
|
|
5671
|
-
async function loadConfig(
|
|
5689
|
+
async function loadConfig(configPath, host = readHostEnvironment()) {
|
|
5690
|
+
const path = configPath ?? resolveConfigPath(host);
|
|
5672
5691
|
let raw;
|
|
5673
5692
|
try {
|
|
5674
5693
|
raw = await readFile(path, "utf8");
|
|
@@ -5682,7 +5701,7 @@ async function loadConfig(path = resolveConfigPath()) {
|
|
|
5682
5701
|
} catch {
|
|
5683
5702
|
throw new Error(`${path} is not valid JSON`);
|
|
5684
5703
|
}
|
|
5685
|
-
return buildConfig(json, path);
|
|
5704
|
+
return buildConfig(json, path, host.home);
|
|
5686
5705
|
}
|
|
5687
5706
|
const text = string().min(1);
|
|
5688
5707
|
const positive = number().positive();
|
|
@@ -5706,13 +5725,13 @@ const configFileSchema = strictObject({
|
|
|
5706
5725
|
claudeSettingsPath: text.nullable().optional()
|
|
5707
5726
|
}).optional()
|
|
5708
5727
|
});
|
|
5709
|
-
function buildConfig(json, path) {
|
|
5728
|
+
function buildConfig(json, path, home) {
|
|
5710
5729
|
const parsed = configFileSchema.safeParse(json);
|
|
5711
5730
|
if (!parsed.success) throw new Error(`${path} is not a valid config: ${prettifyError(parsed.error)}`);
|
|
5712
5731
|
const file = parsed.data;
|
|
5713
5732
|
const warnings = [];
|
|
5714
5733
|
const classifiers = buildRegistry(file.classifiers ?? {}, (id, entry) => parseClassifierEntry(id, entry));
|
|
5715
|
-
const scopeSources = buildRegistry(file.scopeSources ?? {}, (id, entry) => parseScopeSourceEntry(id, entry));
|
|
5734
|
+
const scopeSources = buildRegistry(file.scopeSources ?? {}, (id, entry) => parseScopeSourceEntry(id, entry, home));
|
|
5716
5735
|
const configuredSources = file.scopeSources === void 0 ? null : scopeSources.entries;
|
|
5717
5736
|
warnings.push(...classifiers.dropped.map((line) => `${path}: classifiers.${line}`), ...scopeSources.dropped.map((line) => `${path}: scopeSources.${line}`), ...collectScopeWarnings(configuredSources).map((line) => `${path}: scopeSources ${line}`));
|
|
5718
5737
|
const decision = file.decision ?? {};
|
|
@@ -5722,10 +5741,10 @@ function buildConfig(json, path) {
|
|
|
5722
5741
|
provider: resolveRole("classifier", decision.classifier ?? "jev", classifiers.entries, classifiers.droppedIDs, path),
|
|
5723
5742
|
judge: judgeID === null ? null : resolveRole("judge", judgeID, classifiers.entries, classifiers.droppedIDs, path),
|
|
5724
5743
|
scopeSources: configuredSources ?? DEFAULT_SCOPE_SOURCES,
|
|
5725
|
-
classifierPath: expandHomePath(policy.frameworkPath ?? void 0),
|
|
5726
|
-
rulesPath: expandHomePath(policy.rulesPath ?? void 0),
|
|
5744
|
+
classifierPath: expandHomePath(policy.frameworkPath ?? void 0, home),
|
|
5745
|
+
rulesPath: expandHomePath(policy.rulesPath ?? void 0, home),
|
|
5727
5746
|
onFailure: decision.onFailure ?? DEFAULT_CONFIG.onFailure,
|
|
5728
|
-
claudeSettingsPath: policy.claudeSettingsPath === null ? null : expandHomePath(policy.claudeSettingsPath),
|
|
5747
|
+
claudeSettingsPath: policy.claudeSettingsPath === null ? null : expandHomePath(policy.claudeSettingsPath, home),
|
|
5729
5748
|
minConfidence: decision.minConfidence ?? DEFAULT_CONFIG.minConfidence,
|
|
5730
5749
|
denialBudget: {
|
|
5731
5750
|
consecutive: decision.denialBudget?.consecutive ?? DEFAULT_DENIAL_BUDGET.consecutive,
|
|
@@ -5800,14 +5819,14 @@ const scopeSourceEntrySchema = strictObject({
|
|
|
5800
5819
|
kind: text.optional(),
|
|
5801
5820
|
paths: array(text).min(1).optional()
|
|
5802
5821
|
});
|
|
5803
|
-
function parseScopeSourceEntry(id, entry) {
|
|
5822
|
+
function parseScopeSourceEntry(id, entry, home) {
|
|
5804
5823
|
const parsed = scopeSourceEntrySchema.safeParse(entry);
|
|
5805
5824
|
if (!parsed.success) return formatFirstIssue(parsed.error.issues);
|
|
5806
5825
|
const kind = SCOPE_SOURCE_KINDS.find((known) => known === (parsed.data.kind ?? id));
|
|
5807
5826
|
if (kind === void 0) return `unknown kind '${parsed.data.kind ?? id}'; known kinds are ${SCOPE_SOURCE_KINDS.join(", ")}`;
|
|
5808
5827
|
if (kind === "globs") return parsed.data.paths === void 0 ? "kind 'globs' needs paths" : {
|
|
5809
5828
|
kind,
|
|
5810
|
-
paths: parsed.data.paths.map((glob) => expandHomePath(glob) ?? glob)
|
|
5829
|
+
paths: parsed.data.paths.map((glob) => expandHomePath(glob, home) ?? glob)
|
|
5811
5830
|
};
|
|
5812
5831
|
return parsed.data.paths === void 0 ? { kind } : `kind '${kind}' takes no paths`;
|
|
5813
5832
|
}
|
|
@@ -5828,18 +5847,113 @@ function parseBuiltIn(id) {
|
|
|
5828
5847
|
if (typeof resolved === "string") throw new TypeError(`built-in kind ${id} does not resolve: ${resolved}`);
|
|
5829
5848
|
return resolved;
|
|
5830
5849
|
}
|
|
5831
|
-
function expandHomePath(path) {
|
|
5832
|
-
return path?.startsWith("~/") === true ? join(
|
|
5850
|
+
function expandHomePath(path, home) {
|
|
5851
|
+
return path?.startsWith("~/") === true ? join(home, path.slice(2)) : path;
|
|
5833
5852
|
}
|
|
5834
5853
|
function resolveApiKey(provider, options = {}) {
|
|
5835
|
-
const
|
|
5854
|
+
const host = options.host ?? readHostEnvironment();
|
|
5855
|
+
const fromEnv = provider.apiKeyEnv === void 0 ? void 0 : host.env[provider.apiKeyEnv];
|
|
5836
5856
|
if (fromEnv !== void 0 && fromEnv !== "") return Promise.resolve(fromEnv);
|
|
5837
5857
|
if (provider.apiKeyCommand === void 0 || provider.apiKeyCommand === "") return Promise.resolve(null);
|
|
5838
|
-
return readApiKeyFromCommand(provider.apiKeyCommand,
|
|
5858
|
+
return readApiKeyFromCommand(provider.apiKeyCommand, {
|
|
5859
|
+
...options,
|
|
5860
|
+
host
|
|
5861
|
+
});
|
|
5862
|
+
}
|
|
5863
|
+
//#endregion
|
|
5864
|
+
//#region src/scope/find-checkout.ts
|
|
5865
|
+
const GIT_OVERRIDES$1 = [
|
|
5866
|
+
"GIT_DIR",
|
|
5867
|
+
"GIT_WORK_TREE",
|
|
5868
|
+
"GIT_COMMON_DIR"
|
|
5869
|
+
];
|
|
5870
|
+
async function findCheckout(start, env) {
|
|
5871
|
+
if (GIT_OVERRIDES$1.some((name) => env[name] !== void 0)) return null;
|
|
5872
|
+
for (let directory = start;; directory = dirname(directory)) {
|
|
5873
|
+
const gitPath = join(directory, ".git");
|
|
5874
|
+
const info = await stat(gitPath).catch(() => null);
|
|
5875
|
+
if (info !== null) {
|
|
5876
|
+
const gitDir = info.isDirectory() ? gitPath : await findLinkedGitDir(directory, gitPath);
|
|
5877
|
+
return gitDir === null ? null : {
|
|
5878
|
+
worktree: directory,
|
|
5879
|
+
commonDir: await findCommonDir(gitDir)
|
|
5880
|
+
};
|
|
5881
|
+
}
|
|
5882
|
+
if (dirname(directory) === directory) return null;
|
|
5883
|
+
}
|
|
5884
|
+
}
|
|
5885
|
+
async function findLinkedGitDir(directory, gitPath) {
|
|
5886
|
+
const link = await readFile(gitPath, "utf8").catch(() => "");
|
|
5887
|
+
const path = /^gitdir: (?<path>.+)$/mu.exec(link)?.groups?.["path"];
|
|
5888
|
+
return path === void 0 ? null : resolve(directory, path.trim());
|
|
5889
|
+
}
|
|
5890
|
+
async function findCommonDir(gitDir) {
|
|
5891
|
+
const common = await readFile(join(gitDir, "commondir"), "utf8").catch(() => null);
|
|
5892
|
+
return common === null ? gitDir : resolve(gitDir, common.trim());
|
|
5893
|
+
}
|
|
5894
|
+
//#endregion
|
|
5895
|
+
//#region src/state/resolve-state-dir.ts
|
|
5896
|
+
function resolveStateDir(host) {
|
|
5897
|
+
const stateHome = host.env["XDG_STATE_HOME"];
|
|
5898
|
+
const base = stateHome === void 0 || stateHome === "" ? join(host.home, ".local", "state") : stateHome;
|
|
5899
|
+
return join(base, "auto-mode");
|
|
5900
|
+
}
|
|
5901
|
+
//#endregion
|
|
5902
|
+
//#region src/bypass/resolve-edit-target.ts
|
|
5903
|
+
async function resolveEditTarget(path) {
|
|
5904
|
+
const missing = [];
|
|
5905
|
+
for (let current = path;; current = dirname(current)) try {
|
|
5906
|
+
const real = await realpath(current);
|
|
5907
|
+
return join(real, ...missing.toReversed());
|
|
5908
|
+
} catch {
|
|
5909
|
+
if (await lstat(current).catch(() => null) !== null || dirname(current) === current) return null;
|
|
5910
|
+
missing.push(basename(current));
|
|
5911
|
+
}
|
|
5912
|
+
}
|
|
5913
|
+
//#endregion
|
|
5914
|
+
//#region src/bypass/try-classify-edit.ts
|
|
5915
|
+
const DISK_EDIT_FILE_READER = {
|
|
5916
|
+
resolveEditTarget,
|
|
5917
|
+
findCheckout,
|
|
5918
|
+
readFile: (path) => readFile(path, "utf8")
|
|
5919
|
+
};
|
|
5920
|
+
const GIT_OVERRIDES = [
|
|
5921
|
+
"GIT_DIR",
|
|
5922
|
+
"GIT_WORK_TREE",
|
|
5923
|
+
"GIT_COMMON_DIR"
|
|
5924
|
+
];
|
|
5925
|
+
async function tryClassifyEdit(request, scope, host, files = DISK_EDIT_FILE_READER) {
|
|
5926
|
+
const fields = getEditFields(request.toolName);
|
|
5927
|
+
const path = fields === null ? void 0 : request.toolInput[fields.path];
|
|
5928
|
+
if (typeof path !== "string" || path === "") return null;
|
|
5929
|
+
try {
|
|
5930
|
+
const ownDirs = [dirname(resolveConfigPath(host)), resolveStateDir(host)];
|
|
5931
|
+
const requested = resolve(request.cwd, path);
|
|
5932
|
+
const [target, worktrees, protectedDirs] = await Promise.all([
|
|
5933
|
+
files.resolveEditTarget(requested),
|
|
5934
|
+
Promise.all(scope.worktrees.map((worktree) => files.resolveEditTarget(worktree))),
|
|
5935
|
+
Promise.all(ownDirs.map((dir) => files.resolveEditTarget(dir)))
|
|
5936
|
+
]);
|
|
5937
|
+
if (target === null || GIT_OVERRIDES.some((name) => host.env[name] !== void 0)) return null;
|
|
5938
|
+
const [checkout, current] = await Promise.all([files.findCheckout(dirname(target), host.env), request.toolName === "Edit" ? files.readFile(target).catch(() => null) : null]);
|
|
5939
|
+
return (await import("./classify-edit-LpoeenGt.js")).classifyEdit({
|
|
5940
|
+
toolName: request.toolName,
|
|
5941
|
+
toolInput: request.toolInput,
|
|
5942
|
+
requested,
|
|
5943
|
+
target,
|
|
5944
|
+
checkout: checkout?.worktree ?? null,
|
|
5945
|
+
current
|
|
5946
|
+
}, {
|
|
5947
|
+
worktrees: worktrees.filter((worktree) => worktree !== null),
|
|
5948
|
+
protectedDirs: [...ownDirs, ...protectedDirs.filter((dir) => dir !== null)]
|
|
5949
|
+
});
|
|
5950
|
+
} catch {
|
|
5951
|
+
return null;
|
|
5952
|
+
}
|
|
5839
5953
|
}
|
|
5840
5954
|
//#endregion
|
|
5841
5955
|
//#region src/config/load-claude-rules.ts
|
|
5842
|
-
async function loadClaudeRules(path) {
|
|
5956
|
+
async function loadClaudeRules(path, host) {
|
|
5843
5957
|
const empty = {
|
|
5844
5958
|
environment: [],
|
|
5845
5959
|
allow: [],
|
|
@@ -5847,8 +5961,8 @@ async function loadClaudeRules(path) {
|
|
|
5847
5961
|
hard_deny: []
|
|
5848
5962
|
};
|
|
5849
5963
|
if (path === null) return empty;
|
|
5850
|
-
const configDir =
|
|
5851
|
-
const base = configDir === void 0 || configDir === "" ? join(
|
|
5964
|
+
const configDir = host.env["CLAUDE_CONFIG_DIR"];
|
|
5965
|
+
const base = configDir === void 0 || configDir === "" ? join(host.home, ".claude") : configDir;
|
|
5852
5966
|
const settingsPath = path ?? join(base, "settings.json");
|
|
5853
5967
|
let raw;
|
|
5854
5968
|
try {
|
|
@@ -5986,13 +6100,13 @@ function toRepositorySlug(reference) {
|
|
|
5986
6100
|
}
|
|
5987
6101
|
//#endregion
|
|
5988
6102
|
//#region src/containment/collect-scope-findings.ts
|
|
5989
|
-
function collectScopeFindings(action, scope) {
|
|
6103
|
+
function collectScopeFindings(action, scope, scratchPaths = SCRATCH_PATHS) {
|
|
5990
6104
|
const filePath = action.input["file_path"] ?? action.input["notebook_path"];
|
|
5991
6105
|
const command = action.input["command"];
|
|
5992
6106
|
const findings = [
|
|
5993
|
-
...FILE_WRITE_TOOLS.has(action.tool) && typeof filePath === "string" ? collectTargetFindings(action.cwd, filePath, scope) : [],
|
|
6107
|
+
...FILE_WRITE_TOOLS.has(action.tool) && typeof filePath === "string" ? collectTargetFindings(action.cwd, filePath, scope, scratchPaths) : [],
|
|
5994
6108
|
...action.tool.startsWith("mcp__") ? collectToolFindings(action, scope) : [],
|
|
5995
|
-
...action.tool === "Bash" && typeof command === "string" ? collectCommandFindings(command, action.cwd, scope) : []
|
|
6109
|
+
...action.tool === "Bash" && typeof command === "string" ? collectCommandFindings(command, action.cwd, scope, scratchPaths) : []
|
|
5996
6110
|
];
|
|
5997
6111
|
return findings.filter((finding, index) => findings.findIndex((other) => other.kind === finding.kind && other.target === finding.target) === index);
|
|
5998
6112
|
}
|
|
@@ -6002,9 +6116,9 @@ const FILE_WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
|
6002
6116
|
"Write",
|
|
6003
6117
|
"NotebookEdit"
|
|
6004
6118
|
]);
|
|
6005
|
-
function collectTargetFindings(directory, target, scope) {
|
|
6119
|
+
function collectTargetFindings(directory, target, scope, scratchPaths) {
|
|
6006
6120
|
const path = resolvePath(directory, target, scope.home);
|
|
6007
|
-
return path === null ? [] : collectPathFindings(path, scope);
|
|
6121
|
+
return path === null ? [] : collectPathFindings(path, scope, scratchPaths);
|
|
6008
6122
|
}
|
|
6009
6123
|
const CREDENTIAL_PATHS = [
|
|
6010
6124
|
".ssh",
|
|
@@ -6015,12 +6129,12 @@ const CREDENTIAL_PATHS = [
|
|
|
6015
6129
|
".npmrc",
|
|
6016
6130
|
".config/gh"
|
|
6017
6131
|
];
|
|
6018
|
-
function collectPathFindings(path, scope) {
|
|
6132
|
+
function collectPathFindings(path, scope, scratchPaths) {
|
|
6019
6133
|
if (CREDENTIAL_PATHS.some((entry) => isUnder(path, join(scope.home, entry)))) return [{
|
|
6020
6134
|
kind: "credential",
|
|
6021
6135
|
target: path
|
|
6022
6136
|
}];
|
|
6023
|
-
return isInScope(path, scope) ? [] : [{
|
|
6137
|
+
return isInScope(path, scope, scratchPaths) ? [] : [{
|
|
6024
6138
|
kind: "path",
|
|
6025
6139
|
target: path
|
|
6026
6140
|
}];
|
|
@@ -6051,24 +6165,24 @@ const DESTINATION_WRITERS = /* @__PURE__ */ new Set([
|
|
|
6051
6165
|
"rsync",
|
|
6052
6166
|
"ln"
|
|
6053
6167
|
]);
|
|
6054
|
-
function collectCommandFindings(command, cwd, scope) {
|
|
6168
|
+
function collectCommandFindings(command, cwd, scope, scratchPaths) {
|
|
6055
6169
|
const findings = [];
|
|
6056
6170
|
const hasPipe = /(?<!\|)\|(?!\|)/u.test(command);
|
|
6057
6171
|
let directory = cwd;
|
|
6058
6172
|
for (const segment of splitShellCommand(command).segments) {
|
|
6059
6173
|
const [name, ...args] = splitCommandWords(segment.text);
|
|
6060
|
-
for (const target of collectRedirectTargets(segment.text)) findings.push(...collectTargetFindings(directory, target, scope));
|
|
6174
|
+
for (const target of collectRedirectTargets(segment.text)) findings.push(...collectTargetFindings(directory, target, scope, scratchPaths));
|
|
6061
6175
|
if (name === void 0) continue;
|
|
6062
6176
|
if (name === "cd" && hasPipe) directory = null;
|
|
6063
6177
|
else if (name === "cd") directory = args[0] === void 0 ? scope.home : resolvePath(directory, args[0], scope.home);
|
|
6064
|
-
else findings.push(...collectProgramFindings(name, args, directory, cwd, scope));
|
|
6178
|
+
else findings.push(...collectProgramFindings(name, args, directory, cwd, scope, scratchPaths));
|
|
6065
6179
|
}
|
|
6066
6180
|
return findings;
|
|
6067
6181
|
}
|
|
6068
|
-
function collectProgramFindings(name, args, directory, cwd, scope) {
|
|
6069
|
-
const resolve = (path) => collectTargetFindings(directory, path, scope);
|
|
6182
|
+
function collectProgramFindings(name, args, directory, cwd, scope, scratchPaths) {
|
|
6183
|
+
const resolve = (path) => collectTargetFindings(directory, path, scope, scratchPaths);
|
|
6070
6184
|
const operands = args.filter((word) => !word.startsWith("-"));
|
|
6071
|
-
if (name === "git") return collectGitFindings(args, directory, cwd, scope);
|
|
6185
|
+
if (name === "git") return collectGitFindings(args, directory, cwd, scope, scratchPaths);
|
|
6072
6186
|
if (name === "gh") return collectGhFindings(args, scope);
|
|
6073
6187
|
if (PATH_WRITERS.has(name)) return operands.flatMap((path) => resolve(path));
|
|
6074
6188
|
if (DESTINATION_WRITERS.has(name)) return collectCopyFindings(name, args, operands).flatMap((path) => resolve(path));
|
|
@@ -6206,7 +6320,7 @@ const GIT_DIRECTORY_WRITES = /* @__PURE__ */ new Set([
|
|
|
6206
6320
|
"stash",
|
|
6207
6321
|
"push"
|
|
6208
6322
|
]);
|
|
6209
|
-
function collectGitFindings(args, directory, cwd, scope) {
|
|
6323
|
+
function collectGitFindings(args, directory, cwd, scope, scratchPaths) {
|
|
6210
6324
|
let gitDirectory = directory;
|
|
6211
6325
|
let index = 0;
|
|
6212
6326
|
while (index < args.length && args[index]?.startsWith("-") === true) {
|
|
@@ -6218,11 +6332,11 @@ function collectGitFindings(args, directory, cwd, scope) {
|
|
|
6218
6332
|
const rest = args.slice(index + 1);
|
|
6219
6333
|
const operands = rest.filter((word) => !word.startsWith("-"));
|
|
6220
6334
|
const findings = [];
|
|
6221
|
-
if (gitDirectory !== null && GIT_DIRECTORY_WRITES.has(subcommand) && !isInScope(gitDirectory, scope)) findings.push({
|
|
6335
|
+
if (gitDirectory !== null && GIT_DIRECTORY_WRITES.has(subcommand) && !isInScope(gitDirectory, scope, scratchPaths)) findings.push({
|
|
6222
6336
|
kind: "path",
|
|
6223
6337
|
target: gitDirectory
|
|
6224
6338
|
});
|
|
6225
|
-
if (subcommand === "worktree" && operands[0] === "remove" && operands[1] !== void 0) findings.push(...collectTargetFindings(gitDirectory, operands[1], scope));
|
|
6339
|
+
if (subcommand === "worktree" && operands[0] === "remove" && operands[1] !== void 0) findings.push(...collectTargetFindings(gitDirectory, operands[1], scope, scratchPaths));
|
|
6226
6340
|
if (subcommand === "branch" && rest.some((word) => /^-(?:d|D|-delete)$/u.test(word))) for (const branch of operands.filter((word) => !scope.branches.includes(word))) findings.push({
|
|
6227
6341
|
kind: "branch",
|
|
6228
6342
|
target: branch
|
|
@@ -6457,14 +6571,8 @@ function resolvePath(cwd, path, home) {
|
|
|
6457
6571
|
const absolute = isAbsolute(expanded) || cwd === null ? expanded : join(cwd, expanded);
|
|
6458
6572
|
return normalize(absolute).replace(/(?<=.)\/$/u, "");
|
|
6459
6573
|
}
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
"/dev/null",
|
|
6463
|
-
"/dev/stdout",
|
|
6464
|
-
"/dev/stderr"
|
|
6465
|
-
];
|
|
6466
|
-
function isInScope(path, scope) {
|
|
6467
|
-
if (SCRATCH_PATHS.some((entry) => isUnder(path, entry)) || scope.pathGlobs.some((glob) => matchesGlob(path, glob))) return true;
|
|
6574
|
+
function isInScope(path, scope, scratchPaths) {
|
|
6575
|
+
if (scratchPaths.some((entry) => isUnder(path, entry)) || scope.pathGlobs.some((glob) => matchesGlob(path, glob))) return true;
|
|
6468
6576
|
return scope.worktrees.some((worktree) => {
|
|
6469
6577
|
const isNested = isUnder(path, join(worktree, ".worktrees"));
|
|
6470
6578
|
const isOtherOwned = scope.worktrees.some((other) => other !== worktree && isUnder(path, other));
|
|
@@ -6477,12 +6585,12 @@ function isUnder(path, root) {
|
|
|
6477
6585
|
//#endregion
|
|
6478
6586
|
//#region src/containment/check-containment.ts
|
|
6479
6587
|
const CONTAINMENT_RULE = "Outside Task Scope";
|
|
6480
|
-
function checkContainment(request, scope) {
|
|
6588
|
+
function checkContainment(request, scope, scratchPaths) {
|
|
6481
6589
|
const findings = collectScopeFindings({
|
|
6482
6590
|
tool: request.toolName,
|
|
6483
6591
|
cwd: request.cwd,
|
|
6484
6592
|
input: request.toolInput
|
|
6485
|
-
}, scope);
|
|
6593
|
+
}, scope, scratchPaths);
|
|
6486
6594
|
if (findings.length === 0) return null;
|
|
6487
6595
|
return {
|
|
6488
6596
|
rule: CONTAINMENT_RULE,
|
|
@@ -6520,12 +6628,12 @@ function buildTaskScopeSummary(scope) {
|
|
|
6520
6628
|
}
|
|
6521
6629
|
//#endregion
|
|
6522
6630
|
//#region src/policy/resolve-shipped-policy-dir.ts
|
|
6523
|
-
function resolveShippedPolicyDir() {
|
|
6524
|
-
let dir =
|
|
6631
|
+
function resolveShippedPolicyDir(startDir = import.meta.dirname, stopDir = parse(startDir).root) {
|
|
6632
|
+
let dir = startDir;
|
|
6525
6633
|
for (;;) {
|
|
6526
6634
|
if (existsSync(join(dir, "policy", "classifier.md"))) return join(dir, "policy");
|
|
6527
6635
|
const parent = dirname(dir);
|
|
6528
|
-
if (parent === dir || dir ===
|
|
6636
|
+
if (parent === dir || dir === stopDir) throw new Error("auto-mode cannot find its shipped policy/ directory");
|
|
6529
6637
|
dir = parent;
|
|
6530
6638
|
}
|
|
6531
6639
|
}
|
|
@@ -6542,11 +6650,13 @@ async function loadPolicy(paths = {}, framework = "classifier.md") {
|
|
|
6542
6650
|
}
|
|
6543
6651
|
//#endregion
|
|
6544
6652
|
//#region src/model/anthropic-client.ts
|
|
6545
|
-
async function sendMessage(provider, apiKey, request) {
|
|
6653
|
+
async function sendMessage(provider, apiKey, request, signal) {
|
|
6546
6654
|
const controller = new AbortController();
|
|
6547
|
-
const
|
|
6655
|
+
const stopRequest = () => {
|
|
6548
6656
|
controller.abort();
|
|
6549
|
-
}
|
|
6657
|
+
};
|
|
6658
|
+
signal.addEventListener("abort", stopRequest, { once: true });
|
|
6659
|
+
if (signal.aborted) controller.abort();
|
|
6550
6660
|
try {
|
|
6551
6661
|
const response = await fetch(`${provider.baseURL.replace(/\/$/, "")}/v1/messages`, {
|
|
6552
6662
|
method: "POST",
|
|
@@ -6579,7 +6689,7 @@ async function sendMessage(provider, apiKey, request) {
|
|
|
6579
6689
|
}
|
|
6580
6690
|
return readResult(body);
|
|
6581
6691
|
} finally {
|
|
6582
|
-
|
|
6692
|
+
signal.removeEventListener("abort", stopRequest);
|
|
6583
6693
|
}
|
|
6584
6694
|
}
|
|
6585
6695
|
const contentBlockSchema = union([object({
|
|
@@ -6622,7 +6732,7 @@ function readResult(body) {
|
|
|
6622
6732
|
//#endregion
|
|
6623
6733
|
//#region src/model/build-request.ts
|
|
6624
6734
|
const MAX_INPUT_CHARS = 4e3;
|
|
6625
|
-
function buildUserMessage(payload, transcript, reasoning, repositoryContext = null) {
|
|
6735
|
+
function buildUserMessage(payload, transcript, reasoning, repositoryContext = null, mcpServers = []) {
|
|
6626
6736
|
const history = transcript.length === 0 ? "(no transcript available from this harness)" : transcript.map((entry) => `${entry.role}: ${entry.text}`).join("\n\n");
|
|
6627
6737
|
const input = JSON.stringify(payload.toolInput, null, 2).slice(0, MAX_INPUT_CHARS);
|
|
6628
6738
|
const closing = reasoning ? "Work through the classification process, then end your reply with the output contract tags." : "Reply with the output contract tags and nothing else.";
|
|
@@ -6630,7 +6740,7 @@ function buildUserMessage(payload, transcript, reasoning, repositoryContext = nu
|
|
|
6630
6740
|
${history}
|
|
6631
6741
|
</transcript>
|
|
6632
6742
|
|
|
6633
|
-
${repositoryContext === null ? "" : `<repository>\n${JSON.stringify(repositoryContext, null, 2)}\n</repository>\n\n`}<action>
|
|
6743
|
+
${repositoryContext === null ? "" : `<repository>\n${JSON.stringify(repositoryContext, null, 2)}\n</repository>\n\n`}${mcpServers.length === 0 ? "" : `<mcp-servers>\n${JSON.stringify(mcpServers, null, 2)}\n</mcp-servers>\n\n`}<action>
|
|
6634
6744
|
tool: ${payload.toolName}
|
|
6635
6745
|
cwd: ${payload.cwd}
|
|
6636
6746
|
input:
|
|
@@ -6662,7 +6772,7 @@ function buildTaskContext(context) {
|
|
|
6662
6772
|
}
|
|
6663
6773
|
//#endregion
|
|
6664
6774
|
//#region src/model/build-decision-request.ts
|
|
6665
|
-
function buildDecisionRequest(payload, policy, configuredRules, lastUserMessage, rulesSource = "replacement", repositoryContext = null) {
|
|
6775
|
+
function buildDecisionRequest(payload, policy, configuredRules, lastUserMessage, rulesSource = "replacement", repositoryContext = null, mcpServers = []) {
|
|
6666
6776
|
const rules = {};
|
|
6667
6777
|
let tier = null;
|
|
6668
6778
|
for (const section of policy.split(/(?=^## |^### )/mu)) if (section.startsWith("## HARD BLOCK rules")) tier = "hard";
|
|
@@ -6710,6 +6820,7 @@ function buildDecisionRequest(payload, policy, configuredRules, lastUserMessage,
|
|
|
6710
6820
|
rulesSource,
|
|
6711
6821
|
configuredRules,
|
|
6712
6822
|
...repositoryContext === null ? {} : { repositoryContext },
|
|
6823
|
+
...mcpServers.length === 0 ? {} : { mcpServers },
|
|
6713
6824
|
lastUserMessage: payload.decisionContext === void 0 || payload.decisionContext.agentID === null ? lastUserMessage : null,
|
|
6714
6825
|
...payload.decisionContext === void 0 ? {} : { taskContext: buildTaskContext(payload.decisionContext) },
|
|
6715
6826
|
action: {
|
|
@@ -6728,6 +6839,17 @@ function buildEvidenceGuidance(name) {
|
|
|
6728
6839
|
return "";
|
|
6729
6840
|
}
|
|
6730
6841
|
//#endregion
|
|
6842
|
+
//#region node_modules/.bun/tiny-invariant@1.3.3/node_modules/tiny-invariant/dist/esm/tiny-invariant.js
|
|
6843
|
+
var isProduction = process.env.NODE_ENV === "production";
|
|
6844
|
+
var prefix = "Invariant failed";
|
|
6845
|
+
function invariant(condition, message) {
|
|
6846
|
+
if (condition) return;
|
|
6847
|
+
if (isProduction) throw new Error(prefix);
|
|
6848
|
+
var provided = typeof message === "function" ? message() : message;
|
|
6849
|
+
var value = provided ? "".concat(prefix, ": ").concat(provided) : prefix;
|
|
6850
|
+
throw new Error(value);
|
|
6851
|
+
}
|
|
6852
|
+
//#endregion
|
|
6731
6853
|
//#region src/model/classify-decision-answers.ts
|
|
6732
6854
|
function classifyDecisionAnswers(request, result, minConfidence) {
|
|
6733
6855
|
const entries = Object.entries(request.rules).map(([id, rule]) => {
|
|
@@ -6758,7 +6880,7 @@ function collectDecisionContributors(request, result, minConfidence) {
|
|
|
6758
6880
|
const decision = classifyDecisionAnswers(request, result, minConfidence);
|
|
6759
6881
|
return Object.entries(request.rules).flatMap(([id, rule]) => {
|
|
6760
6882
|
const answer = result.answers[id];
|
|
6761
|
-
|
|
6883
|
+
invariant(answer, "the classification refused a result with a rule unanswered");
|
|
6762
6884
|
return (decision.kind === "block" ? rule === decision.rule : decision.kind === "uncertain" && (answer.choice !== "allow" || answer.confidence < minConfidence || answer.probabilities.allow < minConfidence)) ? [{
|
|
6763
6885
|
rule: rule.source === "shipped" ? rule.name : id,
|
|
6764
6886
|
source: rule.source,
|
|
@@ -6787,6 +6909,83 @@ function formatClassifierNote(note, apiKey) {
|
|
|
6787
6909
|
return apiKey === null || apiKey === "" ? note : note.split(apiKey).join("[redacted]");
|
|
6788
6910
|
}
|
|
6789
6911
|
//#endregion
|
|
6912
|
+
//#region src/model/load-mcp-servers.ts
|
|
6913
|
+
const serverSchema = looseObject({
|
|
6914
|
+
type: string().optional(),
|
|
6915
|
+
url: string().optional()
|
|
6916
|
+
});
|
|
6917
|
+
const serversSchema = record(string(), unknown()).optional();
|
|
6918
|
+
const approvalSchema = looseObject({
|
|
6919
|
+
enableAllProjectMcpServers: boolean().optional(),
|
|
6920
|
+
enabledMcpjsonServers: array(string()).optional(),
|
|
6921
|
+
disabledMcpjsonServers: array(string()).optional()
|
|
6922
|
+
});
|
|
6923
|
+
const stateSchema = looseObject({
|
|
6924
|
+
mcpServers: serversSchema,
|
|
6925
|
+
projects: record(string(), looseObject({
|
|
6926
|
+
...approvalSchema.shape,
|
|
6927
|
+
mcpServers: serversSchema
|
|
6928
|
+
})).optional()
|
|
6929
|
+
});
|
|
6930
|
+
const projectFileSchema = looseObject({ mcpServers: serversSchema });
|
|
6931
|
+
async function loadMCPServers(cwd, host) {
|
|
6932
|
+
const configDir = host.env["CLAUDE_CONFIG_DIR"];
|
|
6933
|
+
const hasConfigDir = configDir !== void 0 && configDir !== "";
|
|
6934
|
+
const statePath = hasConfigDir ? join(configDir, ".claude.json") : join(host.home, ".claude.json");
|
|
6935
|
+
const settingsDir = hasConfigDir ? configDir : join(host.home, ".claude");
|
|
6936
|
+
const settingsPath = join(settingsDir, "settings.json");
|
|
6937
|
+
const [state, settings] = await Promise.all([readJSON(statePath, stateSchema), readJSON(settingsPath, approvalSchema)]);
|
|
6938
|
+
const directory = resolve(cwd);
|
|
6939
|
+
const projectDir = (await findCheckout(directory, host.env))?.worktree ?? directory;
|
|
6940
|
+
const project = state?.projects?.[projectDir];
|
|
6941
|
+
const projectFile = await readJSON(join(projectDir, ".mcp.json"), projectFileSchema);
|
|
6942
|
+
const approvals = [settings, project].filter((entry) => entry !== void 0 && entry !== null);
|
|
6943
|
+
const isDisabled = (name) => approvals.some((entry) => entry.disabledMcpjsonServers?.includes(name) === true);
|
|
6944
|
+
const isEnabled = (name) => approvals.some((entry) => entry.enableAllProjectMcpServers === true || entry.enabledMcpjsonServers?.includes(name) === true);
|
|
6945
|
+
const facts = [
|
|
6946
|
+
...buildServerFacts(project?.mcpServers, "local"),
|
|
6947
|
+
...buildServerFacts(projectFile?.mcpServers, "project").filter((server) => isEnabled(server.name) && !isDisabled(server.name)),
|
|
6948
|
+
...buildServerFacts(state?.mcpServers, "user")
|
|
6949
|
+
];
|
|
6950
|
+
return facts.filter((server, index) => facts.findIndex((other) => other.name === server.name) === index);
|
|
6951
|
+
}
|
|
6952
|
+
async function readJSON(path, schema) {
|
|
6953
|
+
try {
|
|
6954
|
+
const text = await readFile(path, "utf8");
|
|
6955
|
+
const parsed = schema.safeParse(JSON.parse(text));
|
|
6956
|
+
return parsed.success ? parsed.data : null;
|
|
6957
|
+
} catch {
|
|
6958
|
+
return null;
|
|
6959
|
+
}
|
|
6960
|
+
}
|
|
6961
|
+
const MAX_NAME_LENGTH = 128;
|
|
6962
|
+
const TRANSPORTS = {
|
|
6963
|
+
stdio: "stdio",
|
|
6964
|
+
http: "http",
|
|
6965
|
+
"streamable-http": "http",
|
|
6966
|
+
sse: "sse",
|
|
6967
|
+
ws: "ws"
|
|
6968
|
+
};
|
|
6969
|
+
function buildServerFacts(servers, scope) {
|
|
6970
|
+
return Object.entries(servers ?? {}).flatMap(([name, entry]) => {
|
|
6971
|
+
const server = serverSchema.safeParse(entry);
|
|
6972
|
+
if (!server.success || name === "" || name.length > MAX_NAME_LENGTH) return [];
|
|
6973
|
+
const transport = TRANSPORTS[server.data.type ?? (server.data.url === void 0 ? "stdio" : "")];
|
|
6974
|
+
if (transport === void 0) return [];
|
|
6975
|
+
return [{
|
|
6976
|
+
name,
|
|
6977
|
+
scope,
|
|
6978
|
+
transport,
|
|
6979
|
+
host: transport === "stdio" || server.data.url === void 0 ? null : findEndpointHost(server.data.url)
|
|
6980
|
+
}];
|
|
6981
|
+
});
|
|
6982
|
+
}
|
|
6983
|
+
function findEndpointHost(url) {
|
|
6984
|
+
const parsed = URL.parse(url);
|
|
6985
|
+
if (parsed === null || !/^(?:https?|wss?):$/u.test(parsed.protocol) || /[${}]/u.test(parsed.host)) return null;
|
|
6986
|
+
return parsed.host;
|
|
6987
|
+
}
|
|
6988
|
+
//#endregion
|
|
6790
6989
|
//#region src/scope/load-checkout-remotes.ts
|
|
6791
6990
|
async function loadCheckoutRemotes(commonDir) {
|
|
6792
6991
|
const config = await readFile(join(commonDir, "config"), "utf8").catch(() => "");
|
|
@@ -6828,12 +7027,12 @@ function parseConfigValue(raw) {
|
|
|
6828
7027
|
}
|
|
6829
7028
|
//#endregion
|
|
6830
7029
|
//#region src/model/load-repository-context.ts
|
|
6831
|
-
async function loadRepositoryContext(cwd) {
|
|
7030
|
+
async function loadRepositoryContext(cwd, env, stopDir = parse(resolve(cwd)).root) {
|
|
6832
7031
|
if ([
|
|
6833
7032
|
"GIT_DIR",
|
|
6834
7033
|
"GIT_WORK_TREE",
|
|
6835
7034
|
"GIT_COMMON_DIR"
|
|
6836
|
-
].some((name) =>
|
|
7035
|
+
].some((name) => env[name] !== void 0)) return null;
|
|
6837
7036
|
let directory = resolve(cwd);
|
|
6838
7037
|
for (;;) {
|
|
6839
7038
|
const gitPath = join(directory, ".git");
|
|
@@ -6842,7 +7041,7 @@ async function loadRepositoryContext(cwd) {
|
|
|
6842
7041
|
} catch (error) {
|
|
6843
7042
|
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) return null;
|
|
6844
7043
|
const parent = dirname(directory);
|
|
6845
|
-
if (parent === directory) return null;
|
|
7044
|
+
if (parent === directory || directory === resolve(stopDir)) return null;
|
|
6846
7045
|
directory = parent;
|
|
6847
7046
|
continue;
|
|
6848
7047
|
}
|
|
@@ -6913,8 +7112,8 @@ function normalizeRemoteURL(url) {
|
|
|
6913
7112
|
}
|
|
6914
7113
|
//#endregion
|
|
6915
7114
|
//#region src/model/load-repository-evidence.ts
|
|
6916
|
-
async function loadRepositoryEvidence(cwd, taskScope) {
|
|
6917
|
-
const context = await loadRepositoryContext(cwd);
|
|
7115
|
+
async function loadRepositoryEvidence(cwd, taskScope, env, stopDir) {
|
|
7116
|
+
const context = await loadRepositoryContext(cwd, env, stopDir);
|
|
6918
7117
|
if (taskScope === void 0) return context;
|
|
6919
7118
|
return {
|
|
6920
7119
|
...context ?? {
|
|
@@ -7323,6 +7522,28 @@ function pickDecisionVerdict(request, result, minConfidence) {
|
|
|
7323
7522
|
})).exhaustive();
|
|
7324
7523
|
}
|
|
7325
7524
|
//#endregion
|
|
7525
|
+
//#region src/model/decision-response-schema.ts
|
|
7526
|
+
const probability = number().min(0).max(1);
|
|
7527
|
+
const answerSchema = object({
|
|
7528
|
+
type: literal("choice"),
|
|
7529
|
+
choice: _enum([
|
|
7530
|
+
"allow",
|
|
7531
|
+
"block",
|
|
7532
|
+
"ask"
|
|
7533
|
+
]),
|
|
7534
|
+
confidence: probability,
|
|
7535
|
+
probabilities: strictObject({
|
|
7536
|
+
allow: probability,
|
|
7537
|
+
block: probability,
|
|
7538
|
+
ask: probability
|
|
7539
|
+
})
|
|
7540
|
+
});
|
|
7541
|
+
const decisionResponseSchema = object({
|
|
7542
|
+
model: string().min(1),
|
|
7543
|
+
answers: record(string(), answerSchema),
|
|
7544
|
+
usage: object({ input_tokens: number().int().nonnegative() })
|
|
7545
|
+
});
|
|
7546
|
+
//#endregion
|
|
7326
7547
|
//#region src/model/send-decision.ts
|
|
7327
7548
|
async function sendDecision(provider, apiKey, request, signal) {
|
|
7328
7549
|
const input = {
|
|
@@ -7350,11 +7571,8 @@ async function sendDecision(provider, apiKey, request, signal) {
|
|
|
7350
7571
|
const stopRequest = () => {
|
|
7351
7572
|
controller.abort();
|
|
7352
7573
|
};
|
|
7353
|
-
signal
|
|
7354
|
-
if (signal
|
|
7355
|
-
const timer = setTimeout(() => {
|
|
7356
|
-
controller.abort();
|
|
7357
|
-
}, provider.timeoutMs);
|
|
7574
|
+
signal.addEventListener("abort", stopRequest, { once: true });
|
|
7575
|
+
if (signal.aborted) controller.abort();
|
|
7358
7576
|
try {
|
|
7359
7577
|
let response;
|
|
7360
7578
|
try {
|
|
@@ -7372,26 +7590,6 @@ async function sendDecision(provider, apiKey, request, signal) {
|
|
|
7372
7590
|
throw new DecisionRequestError("network", requestBytes, "Decision API request failed");
|
|
7373
7591
|
}
|
|
7374
7592
|
if (!response.ok) throw new DecisionRequestError("http-status", requestBytes, `Decision API returned HTTP ${response.status}`);
|
|
7375
|
-
const probability = number().min(0).max(1);
|
|
7376
|
-
const answer = object({
|
|
7377
|
-
type: literal("choice"),
|
|
7378
|
-
choice: _enum([
|
|
7379
|
-
"allow",
|
|
7380
|
-
"block",
|
|
7381
|
-
"ask"
|
|
7382
|
-
]),
|
|
7383
|
-
confidence: probability,
|
|
7384
|
-
probabilities: strictObject({
|
|
7385
|
-
allow: probability,
|
|
7386
|
-
block: probability,
|
|
7387
|
-
ask: probability
|
|
7388
|
-
})
|
|
7389
|
-
});
|
|
7390
|
-
const schema = object({
|
|
7391
|
-
model: string().min(1),
|
|
7392
|
-
answers: record(string(), answer),
|
|
7393
|
-
usage: object({ input_tokens: number().int().nonnegative() })
|
|
7394
|
-
});
|
|
7395
7593
|
let responseBody;
|
|
7396
7594
|
try {
|
|
7397
7595
|
responseBody = await response.json();
|
|
@@ -7399,7 +7597,7 @@ async function sendDecision(provider, apiKey, request, signal) {
|
|
|
7399
7597
|
if (error instanceof Error && error.name === "AbortError") throw new DecisionRequestError("aborted", requestBytes, "Decision request aborted", { cause: error });
|
|
7400
7598
|
throw new DecisionRequestError("invalid-response", requestBytes, "Decision API returned invalid JSON");
|
|
7401
7599
|
}
|
|
7402
|
-
const parsedResponse =
|
|
7600
|
+
const parsedResponse = decisionResponseSchema.safeParse(responseBody);
|
|
7403
7601
|
if (!parsedResponse.success) throw new DecisionRequestError("invalid-response", requestBytes, "Decision API returned a malformed response");
|
|
7404
7602
|
const parsed = parsedResponse.data;
|
|
7405
7603
|
if (Object.keys(parsed.answers).length !== Object.keys(request.questions).length) throw new DecisionRequestError("invalid-response", requestBytes, "Decision API returned an incomplete answer set");
|
|
@@ -7416,19 +7614,19 @@ async function sendDecision(provider, apiKey, request, signal) {
|
|
|
7416
7614
|
requestBytes
|
|
7417
7615
|
};
|
|
7418
7616
|
} finally {
|
|
7419
|
-
|
|
7420
|
-
signal?.removeEventListener("abort", stopRequest);
|
|
7617
|
+
signal.removeEventListener("abort", stopRequest);
|
|
7421
7618
|
}
|
|
7422
7619
|
}
|
|
7423
7620
|
//#endregion
|
|
7424
7621
|
//#region src/model/classify-with-jev.ts
|
|
7425
|
-
async function classifyWithJev(payload, config, options
|
|
7622
|
+
async function classifyWithJev(payload, config, options) {
|
|
7426
7623
|
const start = performance.now();
|
|
7624
|
+
const now = options.now ?? Date.now;
|
|
7427
7625
|
let key = null;
|
|
7428
7626
|
let stage = "credential";
|
|
7429
7627
|
const minConfidence = config.minConfidence ?? .8;
|
|
7430
7628
|
let keySource = "none";
|
|
7431
|
-
const envKey = config.provider.apiKeyEnv === void 0 ? void 0 :
|
|
7629
|
+
const envKey = config.provider.apiKeyEnv === void 0 ? void 0 : options.host.env[config.provider.apiKeyEnv];
|
|
7432
7630
|
if (envKey !== void 0 && envKey !== "") keySource = "environment";
|
|
7433
7631
|
else if (config.provider.apiKeyCommand !== void 0 && config.provider.apiKeyCommand !== "") keySource = "command";
|
|
7434
7632
|
try {
|
|
@@ -7436,23 +7634,22 @@ async function classifyWithJev(payload, config, options = {}) {
|
|
|
7436
7634
|
if (key === null) throw new Error("no API key: set the configured environment variable or key command");
|
|
7437
7635
|
stage = "evidence";
|
|
7438
7636
|
const directUserText = payload.decisionContext?.agentID === null ? payload.decisionContext.lastDirectUserMessage?.text ?? null : null;
|
|
7439
|
-
const [policy, rules, repositoryContext] = await Promise.all([
|
|
7637
|
+
const [policy, rules, repositoryContext, mcpServers] = await Promise.all([
|
|
7440
7638
|
loadPolicy({
|
|
7441
7639
|
classifierPath: config.classifierPath,
|
|
7442
7640
|
rulesPath: config.rulesPath
|
|
7443
7641
|
}, "decision.md"),
|
|
7444
|
-
loadClaudeRules(config.claudeSettingsPath),
|
|
7445
|
-
loadRepositoryEvidence(payload.cwd, options.taskScope).catch(() => null)
|
|
7642
|
+
loadClaudeRules(config.claudeSettingsPath, options.host),
|
|
7643
|
+
loadRepositoryEvidence(payload.cwd, options.taskScope, options.host.env).catch(() => null),
|
|
7644
|
+
loadMCPServers(payload.cwd, options.host).catch(() => [])
|
|
7446
7645
|
]);
|
|
7447
|
-
const request = buildDecisionRequest(payload, policy, rules, directUserText, config.rulesPath === void 0 ? "shipped" : "replacement", repositoryContext);
|
|
7448
|
-
const remainingMs = options.deadlineAt === void 0 ? config.provider.timeoutMs : options.deadlineAt -
|
|
7646
|
+
const request = buildDecisionRequest(payload, policy, rules, directUserText, config.rulesPath === void 0 ? "shipped" : "replacement", repositoryContext, mcpServers);
|
|
7647
|
+
const remainingMs = options.deadlineAt === void 0 ? config.provider.timeoutMs : options.deadlineAt - now();
|
|
7449
7648
|
if (remainingMs <= 0 || options.signal?.aborted === true) throw new DOMException("Evaluation deadline expired", "AbortError");
|
|
7450
|
-
const
|
|
7451
|
-
|
|
7452
|
-
timeoutMs: Math.min(config.provider.timeoutMs, remainingMs)
|
|
7453
|
-
};
|
|
7649
|
+
const timer = (options.timeout ?? ((ms) => AbortSignal.timeout(ms)))(toTimerDelay(Math.min(config.provider.timeoutMs, remainingMs)));
|
|
7650
|
+
const signal = options.signal === void 0 ? timer : AbortSignal.any([timer, options.signal]);
|
|
7454
7651
|
stage = "request";
|
|
7455
|
-
const result = await sendDecision(provider, key, request,
|
|
7652
|
+
const result = await sendDecision(config.provider, key, request, signal);
|
|
7456
7653
|
stage = "response";
|
|
7457
7654
|
const verdict = pickDecisionVerdict(request, result, minConfidence);
|
|
7458
7655
|
return {
|
|
@@ -7476,7 +7673,7 @@ async function classifyWithJev(payload, config, options = {}) {
|
|
|
7476
7673
|
if (options.signal?.aborted === true) {
|
|
7477
7674
|
reason = "evaluation cancelled";
|
|
7478
7675
|
status = "cancelled";
|
|
7479
|
-
} else if (options.deadlineAt !== void 0 &&
|
|
7676
|
+
} else if (options.deadlineAt !== void 0 && now() >= options.deadlineAt) {
|
|
7480
7677
|
reason = "evaluation deadline expired";
|
|
7481
7678
|
status = "timeout";
|
|
7482
7679
|
} else if (error instanceof Error) {
|
|
@@ -7537,8 +7734,15 @@ function parseModelVerdict(text) {
|
|
|
7537
7734
|
//#endregion
|
|
7538
7735
|
//#region src/model/classify-with-model.ts
|
|
7539
7736
|
async function classifyWithModel(payload, config, options = {}) {
|
|
7540
|
-
|
|
7541
|
-
|
|
7737
|
+
const host = options.host ?? readHostEnvironment();
|
|
7738
|
+
if (config.provider.protocol === "system-one") return classifyWithJev(payload, config, {
|
|
7739
|
+
...options,
|
|
7740
|
+
host
|
|
7741
|
+
});
|
|
7742
|
+
const apiKey = await resolveApiKey(config.provider, {
|
|
7743
|
+
...options,
|
|
7744
|
+
host
|
|
7745
|
+
});
|
|
7542
7746
|
if (apiKey === null) return buildFailure(config, "no API key: set the configured environment variable or key command");
|
|
7543
7747
|
let system;
|
|
7544
7748
|
try {
|
|
@@ -7554,13 +7758,14 @@ async function classifyWithModel(payload, config, options = {}) {
|
|
|
7554
7758
|
role: "user",
|
|
7555
7759
|
text: directMessage.text
|
|
7556
7760
|
}];
|
|
7557
|
-
const repositoryContext = await loadRepositoryEvidence(payload.cwd, options.taskScope).catch(() => null);
|
|
7558
|
-
const user = buildUserMessage(payload, transcript, config.provider.reasoning, repositoryContext);
|
|
7761
|
+
const [repositoryContext, mcpServers] = await Promise.all([loadRepositoryEvidence(payload.cwd, options.taskScope, host.env).catch(() => null), loadMCPServers(payload.cwd, host).catch(() => [])]);
|
|
7762
|
+
const user = buildUserMessage(payload, transcript, config.provider.reasoning, repositoryContext, mcpServers);
|
|
7559
7763
|
try {
|
|
7764
|
+
const timeout = options.timeout ?? ((ms) => AbortSignal.timeout(ms));
|
|
7560
7765
|
const result = await sendMessage(config.provider, apiKey, {
|
|
7561
7766
|
system,
|
|
7562
7767
|
user
|
|
7563
|
-
});
|
|
7768
|
+
}, timeout(toTimerDelay(config.provider.timeoutMs)));
|
|
7564
7769
|
if (result.text.trim() === "") return buildFailure(config, `${config.provider.model} returned no text; raise maxTokens`);
|
|
7565
7770
|
const verdict = parseModelVerdict(formatClassifierNote(result.text, apiKey));
|
|
7566
7771
|
const cache = `${result.cachedInputTokens} cached / ${result.cacheWriteTokens} written / ${result.newInputTokens} new / ${result.outputTokens} out`;
|
|
@@ -7592,19 +7797,8 @@ function toMessage(error) {
|
|
|
7592
7797
|
}
|
|
7593
7798
|
//#endregion
|
|
7594
7799
|
//#region src/policy/read-denial-guidance.ts
|
|
7595
|
-
async function readDenialGuidance() {
|
|
7596
|
-
return (await readFile(join(
|
|
7597
|
-
}
|
|
7598
|
-
//#endregion
|
|
7599
|
-
//#region node_modules/.bun/tiny-invariant@1.3.3/node_modules/tiny-invariant/dist/esm/tiny-invariant.js
|
|
7600
|
-
var isProduction = process.env.NODE_ENV === "production";
|
|
7601
|
-
var prefix = "Invariant failed";
|
|
7602
|
-
function invariant(condition, message) {
|
|
7603
|
-
if (condition) return;
|
|
7604
|
-
if (isProduction) throw new Error(prefix);
|
|
7605
|
-
var provided = typeof message === "function" ? message() : message;
|
|
7606
|
-
var value = provided ? "".concat(prefix, ": ").concat(provided) : prefix;
|
|
7607
|
-
throw new Error(value);
|
|
7800
|
+
async function readDenialGuidance(policyDir = resolveShippedPolicyDir()) {
|
|
7801
|
+
return (await readFile(join(policyDir, "denial.md"), "utf8")).trim();
|
|
7608
7802
|
}
|
|
7609
7803
|
//#endregion
|
|
7610
7804
|
//#region src/rules/local-allowlists.ts
|
|
@@ -7768,37 +7962,6 @@ function toUnquoted(word) {
|
|
|
7768
7962
|
return (first === "'" || first === "\"") && word.at(-1) === first ? word.slice(1, -1) : word;
|
|
7769
7963
|
}
|
|
7770
7964
|
//#endregion
|
|
7771
|
-
//#region src/scope/find-checkout.ts
|
|
7772
|
-
const GIT_OVERRIDES$1 = [
|
|
7773
|
-
"GIT_DIR",
|
|
7774
|
-
"GIT_WORK_TREE",
|
|
7775
|
-
"GIT_COMMON_DIR"
|
|
7776
|
-
];
|
|
7777
|
-
async function findCheckout(start) {
|
|
7778
|
-
if (GIT_OVERRIDES$1.some((name) => process.env[name] !== void 0)) return null;
|
|
7779
|
-
for (let directory = start;; directory = dirname(directory)) {
|
|
7780
|
-
const gitPath = join(directory, ".git");
|
|
7781
|
-
const info = await stat(gitPath).catch(() => null);
|
|
7782
|
-
if (info !== null) {
|
|
7783
|
-
const gitDir = info.isDirectory() ? gitPath : await findLinkedGitDir(directory, gitPath);
|
|
7784
|
-
return gitDir === null ? null : {
|
|
7785
|
-
worktree: directory,
|
|
7786
|
-
commonDir: await findCommonDir(gitDir)
|
|
7787
|
-
};
|
|
7788
|
-
}
|
|
7789
|
-
if (dirname(directory) === directory) return null;
|
|
7790
|
-
}
|
|
7791
|
-
}
|
|
7792
|
-
async function findLinkedGitDir(directory, gitPath) {
|
|
7793
|
-
const link = await readFile(gitPath, "utf8").catch(() => "");
|
|
7794
|
-
const path = /^gitdir: (?<path>.+)$/mu.exec(link)?.groups?.["path"];
|
|
7795
|
-
return path === void 0 ? null : resolve(directory, path.trim());
|
|
7796
|
-
}
|
|
7797
|
-
async function findCommonDir(gitDir) {
|
|
7798
|
-
const common = await readFile(join(gitDir, "commondir"), "utf8").catch(() => null);
|
|
7799
|
-
return common === null ? gitDir : resolve(gitDir, common.trim());
|
|
7800
|
-
}
|
|
7801
|
-
//#endregion
|
|
7802
7965
|
//#region src/scope/merge-scope-facts.ts
|
|
7803
7966
|
function mergeScopeFacts(all) {
|
|
7804
7967
|
const pullRequests = /* @__PURE__ */ new Map();
|
|
@@ -7837,6 +8000,106 @@ const DEFAULT_NAMES = /* @__PURE__ */ new Set([
|
|
|
7837
8000
|
"develop"
|
|
7838
8001
|
]);
|
|
7839
8002
|
//#endregion
|
|
8003
|
+
//#region src/scope/build-atc-scope-facts.ts
|
|
8004
|
+
function buildAtcScopeFacts(record, isInRepository) {
|
|
8005
|
+
const scope = record.scope;
|
|
8006
|
+
const checkouts = [scope.workspace, ...scope.worktrees];
|
|
8007
|
+
return {
|
|
8008
|
+
worktrees: checkouts.map((checkout) => checkout.path),
|
|
8009
|
+
branches: [...checkouts.flatMap((checkout) => checkout.branch !== null && isInRepository(checkout.path) ? [checkout.branch] : []), ...scope.branches.flatMap((branch) => isInRepository(branch.repo ?? scope.workspace.path) ? [branch.name] : [])],
|
|
8010
|
+
pullRequests: scope.pullRequests.flatMap((pull) => {
|
|
8011
|
+
const repository = toRepositorySlug(`https://${findHost(pull.url)}/${pull.repo}`);
|
|
8012
|
+
return pull.branch === null || repository === null ? [] : [{
|
|
8013
|
+
number: pull.number,
|
|
8014
|
+
head: pull.branch,
|
|
8015
|
+
repository
|
|
8016
|
+
}];
|
|
8017
|
+
}),
|
|
8018
|
+
pathGlobs: []
|
|
8019
|
+
};
|
|
8020
|
+
}
|
|
8021
|
+
function findHost(url) {
|
|
8022
|
+
return (url === void 0 ? null : URL.parse(url)?.host) ?? "github.com";
|
|
8023
|
+
}
|
|
8024
|
+
//#endregion
|
|
8025
|
+
//#region src/scope/load-atc-session-record.ts
|
|
8026
|
+
const name$1 = string().min(1);
|
|
8027
|
+
const path = name$1.refine(isAbsolute, "an absolute path");
|
|
8028
|
+
const checkoutSchema = object({
|
|
8029
|
+
path,
|
|
8030
|
+
branch: name$1.nullable()
|
|
8031
|
+
}).readonly();
|
|
8032
|
+
const branchSchema$1 = object({
|
|
8033
|
+
name: name$1,
|
|
8034
|
+
repo: path.optional()
|
|
8035
|
+
}).readonly();
|
|
8036
|
+
const pullRequestSchema$1 = object({
|
|
8037
|
+
repo: name$1,
|
|
8038
|
+
number: number().int().positive(),
|
|
8039
|
+
url: name$1.optional(),
|
|
8040
|
+
branch: name$1.nullable()
|
|
8041
|
+
}).readonly();
|
|
8042
|
+
const scopeSchema = object({
|
|
8043
|
+
workspace: checkoutSchema,
|
|
8044
|
+
worktrees: array(checkoutSchema).readonly(),
|
|
8045
|
+
branches: array(branchSchema$1).readonly(),
|
|
8046
|
+
pullRequests: array(pullRequestSchema$1).readonly()
|
|
8047
|
+
}).readonly();
|
|
8048
|
+
const recordSchema = object({
|
|
8049
|
+
format: literal("atc.session-record"),
|
|
8050
|
+
version: literal(1),
|
|
8051
|
+
session: name$1,
|
|
8052
|
+
scope: scopeSchema
|
|
8053
|
+
}).readonly();
|
|
8054
|
+
async function loadAtcSessionRecord(location, sessionID) {
|
|
8055
|
+
if (location === void 0 || location === "") return { kind: "absent" };
|
|
8056
|
+
if (!isAbsolute(location)) return {
|
|
8057
|
+
kind: "malformed",
|
|
8058
|
+
diagnostic: `atc session record path is not absolute: ${location}`
|
|
8059
|
+
};
|
|
8060
|
+
let raw;
|
|
8061
|
+
try {
|
|
8062
|
+
raw = await readFile(location, "utf8");
|
|
8063
|
+
} catch (error) {
|
|
8064
|
+
return isMissing(error) ? { kind: "absent" } : {
|
|
8065
|
+
kind: "malformed",
|
|
8066
|
+
diagnostic: `atc session record unreadable: ${location}`
|
|
8067
|
+
};
|
|
8068
|
+
}
|
|
8069
|
+
let parsed;
|
|
8070
|
+
try {
|
|
8071
|
+
parsed = JSON.parse(raw);
|
|
8072
|
+
} catch {
|
|
8073
|
+
return {
|
|
8074
|
+
kind: "malformed",
|
|
8075
|
+
diagnostic: `atc session record is not JSON: ${location}`
|
|
8076
|
+
};
|
|
8077
|
+
}
|
|
8078
|
+
const result = recordSchema.safeParse(parsed);
|
|
8079
|
+
if (!result.success) {
|
|
8080
|
+
const [issue] = result.error.issues;
|
|
8081
|
+
return {
|
|
8082
|
+
kind: "malformed",
|
|
8083
|
+
diagnostic: `atc session record ignored: ${issue === void 0 ? "record" : issue.path.join(".") || "record"} does not match version 1: ${location}`
|
|
8084
|
+
};
|
|
8085
|
+
}
|
|
8086
|
+
if (sessionID === void 0 || sessionID === "") return {
|
|
8087
|
+
kind: "malformed",
|
|
8088
|
+
diagnostic: `atc session record has no session to match: ${location}`
|
|
8089
|
+
};
|
|
8090
|
+
if (result.data.session !== sessionID) return {
|
|
8091
|
+
kind: "malformed",
|
|
8092
|
+
diagnostic: `atc session record belongs to another session: ${location}`
|
|
8093
|
+
};
|
|
8094
|
+
return {
|
|
8095
|
+
kind: "record",
|
|
8096
|
+
record: result.data
|
|
8097
|
+
};
|
|
8098
|
+
}
|
|
8099
|
+
function isMissing(error) {
|
|
8100
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
8101
|
+
}
|
|
8102
|
+
//#endregion
|
|
7840
8103
|
//#region src/scope/types.ts
|
|
7841
8104
|
const EMPTY_SCOPE_FACTS = {
|
|
7842
8105
|
worktrees: [],
|
|
@@ -7888,7 +8151,7 @@ function resolveSessionScopePath(stateDir, sessionID) {
|
|
|
7888
8151
|
}
|
|
7889
8152
|
//#endregion
|
|
7890
8153
|
//#region src/scope/pick-scope-source-reader.ts
|
|
7891
|
-
function pickScopeSourceReader(source) {
|
|
8154
|
+
function pickScopeSourceReader(source, findSourceCheckout = findCheckout) {
|
|
7892
8155
|
if (source.kind === "cwd") return (context) => Promise.resolve(buildFacts({
|
|
7893
8156
|
worktrees: [context.worktree],
|
|
7894
8157
|
branches: context.branch === null ? [] : [context.branch]
|
|
@@ -7902,7 +8165,23 @@ function pickScopeSourceReader(source) {
|
|
|
7902
8165
|
});
|
|
7903
8166
|
};
|
|
7904
8167
|
if (source.kind === "globs") return () => Promise.resolve(buildFacts({ pathGlobs: source.paths ?? [] }));
|
|
7905
|
-
return () =>
|
|
8168
|
+
return async (context) => {
|
|
8169
|
+
const loaded = await loadAtcSessionRecord(context.atcRecordPath, context.atcSessionID);
|
|
8170
|
+
if (loaded.kind === "malformed") context.stderr.write(`auto-mode: ${loaded.diagnostic}\n`);
|
|
8171
|
+
if (loaded.kind !== "record") return EMPTY_SCOPE_FACTS;
|
|
8172
|
+
const scope = loaded.record.scope;
|
|
8173
|
+
const paths = /* @__PURE__ */ new Set([
|
|
8174
|
+
scope.workspace.path,
|
|
8175
|
+
...scope.worktrees.map((worktree) => worktree.path),
|
|
8176
|
+
...scope.branches.map((branch) => branch.repo ?? scope.workspace.path)
|
|
8177
|
+
]);
|
|
8178
|
+
const entries = await Promise.all([...paths].map((path) => findCommonDirEntry(path, context.env, findSourceCheckout)));
|
|
8179
|
+
const commonDirs = new Map(entries);
|
|
8180
|
+
return buildAtcScopeFacts(loaded.record, (path) => context.commonDir !== null && commonDirs.get(path) === context.commonDir);
|
|
8181
|
+
};
|
|
8182
|
+
}
|
|
8183
|
+
async function findCommonDirEntry(path, env, findSourceCheckout) {
|
|
8184
|
+
return [path, (await findSourceCheckout(path, env))?.commonDir ?? null];
|
|
7906
8185
|
}
|
|
7907
8186
|
function buildFacts(facts) {
|
|
7908
8187
|
return {
|
|
@@ -7912,21 +8191,25 @@ function buildFacts(facts) {
|
|
|
7912
8191
|
}
|
|
7913
8192
|
//#endregion
|
|
7914
8193
|
//#region src/scope/load-task-scope.ts
|
|
7915
|
-
async function loadTaskScope(request, sources) {
|
|
8194
|
+
async function loadTaskScope(request, sources, host, stderr) {
|
|
7916
8195
|
const cwd = resolve(request.cwd);
|
|
7917
|
-
const [checkout, context] = await Promise.all([findCheckout(cwd), loadRepositoryContext(cwd)]);
|
|
8196
|
+
const [checkout, context] = await Promise.all([findCheckout(cwd, host.env), loadRepositoryContext(cwd, host.env)]);
|
|
7918
8197
|
const branch = context?.branch ?? null;
|
|
7919
8198
|
const sourceContext = {
|
|
8199
|
+
env: host.env,
|
|
7920
8200
|
sessionID: request.sessionID,
|
|
7921
8201
|
cwd,
|
|
7922
8202
|
worktree: checkout?.worktree ?? cwd,
|
|
7923
8203
|
commonDir: checkout?.commonDir ?? null,
|
|
7924
8204
|
branch,
|
|
7925
|
-
stateDir: request.stateDir
|
|
8205
|
+
stateDir: request.stateDir,
|
|
8206
|
+
atcRecordPath: host.env["ATC_SESSION_RECORD"],
|
|
8207
|
+
atcSessionID: host.env["ATC_SESSION_ID"],
|
|
8208
|
+
stderr
|
|
7926
8209
|
};
|
|
7927
8210
|
const [remotes, ...facts] = await Promise.all([checkout === null ? [] : loadCheckoutRemotes(checkout.commonDir), ...Object.values(sources).map((source) => pickScopeSourceReader(source)(sourceContext))]);
|
|
7928
8211
|
return buildTaskScope({
|
|
7929
|
-
home:
|
|
8212
|
+
home: host.home,
|
|
7930
8213
|
currentBranch: branch,
|
|
7931
8214
|
defaultBranch: context?.defaultBranch ?? null,
|
|
7932
8215
|
remotes,
|
|
@@ -7934,18 +8217,12 @@ async function loadTaskScope(request, sources) {
|
|
|
7934
8217
|
});
|
|
7935
8218
|
}
|
|
7936
8219
|
//#endregion
|
|
7937
|
-
//#region src/state/resolve-state-dir.ts
|
|
7938
|
-
function resolveStateDir() {
|
|
7939
|
-
const stateHome = process.env["XDG_STATE_HOME"];
|
|
7940
|
-
const base = stateHome === void 0 || stateHome === "" ? join(homedir(), ".local", "state") : stateHome;
|
|
7941
|
-
return join(base, "auto-mode");
|
|
7942
|
-
}
|
|
7943
|
-
//#endregion
|
|
7944
8220
|
//#region src/classify-action.ts
|
|
7945
8221
|
async function classifyAction(request, config, options = {}) {
|
|
8222
|
+
const host = options.host ?? readHostEnvironment();
|
|
7946
8223
|
let configured = null;
|
|
7947
8224
|
if (config.provider.protocol === "system-one") try {
|
|
7948
|
-
configured = await loadClaudeRules(config.claudeSettingsPath);
|
|
8225
|
+
configured = await loadClaudeRules(config.claudeSettingsPath, host);
|
|
7949
8226
|
} catch {
|
|
7950
8227
|
const guidance = config.onFailure === "deny" ? await tryReadDenialGuidance() : null;
|
|
7951
8228
|
return {
|
|
@@ -7959,8 +8236,8 @@ async function classifyAction(request, config, options = {}) {
|
|
|
7959
8236
|
const hasConfiguredDenies = configured !== null && (configured.hard_deny.length > 0 || configured.soft_deny.length > 0);
|
|
7960
8237
|
const local = hasConfiguredDenies ? { kind: "escalate" } : classifyLocally(request);
|
|
7961
8238
|
if (local.kind === "allow" && local.exception === READ_ONLY_EXCEPTION) return buildLocalAllow(local.exception);
|
|
7962
|
-
const scope = await tryLoadTaskScope(request, config);
|
|
7963
|
-
const containment = scope === null ? null : checkContainment(request, scope);
|
|
8239
|
+
const scope = await tryLoadTaskScope(request, config, host, options.stderr ?? process.stderr);
|
|
8240
|
+
const containment = scope === null ? null : checkContainment(request, scope, host.scratchPaths);
|
|
7964
8241
|
if (containment !== null) {
|
|
7965
8242
|
const guidance = await tryReadDenialGuidance();
|
|
7966
8243
|
return {
|
|
@@ -7971,7 +8248,7 @@ async function classifyAction(request, config, options = {}) {
|
|
|
7971
8248
|
};
|
|
7972
8249
|
}
|
|
7973
8250
|
if (local.kind === "allow") return buildLocalAllow(local.exception);
|
|
7974
|
-
const edit = hasConfiguredDenies || scope === null ? null : await tryClassifyEdit(request, scope);
|
|
8251
|
+
const edit = hasConfiguredDenies || scope === null ? null : await tryClassifyEdit(request, scope, host);
|
|
7975
8252
|
if (edit?.kind === "bypass") return {
|
|
7976
8253
|
verdict: { kind: "allow" },
|
|
7977
8254
|
decidingStage: "bypass",
|
|
@@ -7987,7 +8264,8 @@ async function classifyAction(request, config, options = {}) {
|
|
|
7987
8264
|
const taskScope = scope === null ? void 0 : buildTaskScopeSummary(scope);
|
|
7988
8265
|
const [outcome, guidance] = await Promise.all([classifyWithModel(request, config, {
|
|
7989
8266
|
...options,
|
|
7990
|
-
taskScope
|
|
8267
|
+
taskScope,
|
|
8268
|
+
host
|
|
7991
8269
|
}), tryReadDenialGuidance()]);
|
|
7992
8270
|
return {
|
|
7993
8271
|
...outcome,
|
|
@@ -7997,47 +8275,13 @@ async function classifyAction(request, config, options = {}) {
|
|
|
7997
8275
|
};
|
|
7998
8276
|
}
|
|
7999
8277
|
const READ_ONLY_EXCEPTION = "Read-only actions";
|
|
8000
|
-
async function tryLoadTaskScope(request, config) {
|
|
8278
|
+
async function tryLoadTaskScope(request, config, host, stderr) {
|
|
8001
8279
|
try {
|
|
8002
8280
|
return await loadTaskScope({
|
|
8003
8281
|
sessionID: request.sessionID,
|
|
8004
8282
|
cwd: request.cwd,
|
|
8005
|
-
stateDir: resolveStateDir()
|
|
8006
|
-
}, config.scopeSources ?? DEFAULT_SCOPE_SOURCES);
|
|
8007
|
-
} catch {
|
|
8008
|
-
return null;
|
|
8009
|
-
}
|
|
8010
|
-
}
|
|
8011
|
-
const GIT_OVERRIDES = [
|
|
8012
|
-
"GIT_DIR",
|
|
8013
|
-
"GIT_WORK_TREE",
|
|
8014
|
-
"GIT_COMMON_DIR"
|
|
8015
|
-
];
|
|
8016
|
-
async function tryClassifyEdit(request, scope) {
|
|
8017
|
-
const fields = getEditFields(request.toolName);
|
|
8018
|
-
const path = fields === null ? void 0 : request.toolInput[fields.path];
|
|
8019
|
-
if (typeof path !== "string" || path === "") return null;
|
|
8020
|
-
try {
|
|
8021
|
-
const ownDirs = [dirname(resolveConfigPath()), resolveStateDir()];
|
|
8022
|
-
const requested = resolve(request.cwd, path);
|
|
8023
|
-
const [target, worktrees, protectedDirs] = await Promise.all([
|
|
8024
|
-
resolveEditTarget(requested),
|
|
8025
|
-
Promise.all(scope.worktrees.map((worktree) => resolveEditTarget(worktree))),
|
|
8026
|
-
Promise.all(ownDirs.map((dir) => resolveEditTarget(dir)))
|
|
8027
|
-
]);
|
|
8028
|
-
if (target === null || GIT_OVERRIDES.some((name) => process.env[name] !== void 0)) return null;
|
|
8029
|
-
const [checkout, current] = await Promise.all([findCheckout(dirname(target)), request.toolName === "Edit" ? readFile(target, "utf8").catch(() => null) : null]);
|
|
8030
|
-
return (await import("./classify-edit-D8GC6b91.js")).classifyEdit({
|
|
8031
|
-
toolName: request.toolName,
|
|
8032
|
-
toolInput: request.toolInput,
|
|
8033
|
-
requested,
|
|
8034
|
-
target,
|
|
8035
|
-
checkout: checkout?.worktree ?? null,
|
|
8036
|
-
current
|
|
8037
|
-
}, {
|
|
8038
|
-
worktrees: worktrees.filter((worktree) => worktree !== null),
|
|
8039
|
-
protectedDirs: [...ownDirs, ...protectedDirs.filter((dir) => dir !== null)]
|
|
8040
|
-
});
|
|
8283
|
+
stateDir: resolveStateDir(host)
|
|
8284
|
+
}, config.scopeSources ?? DEFAULT_SCOPE_SOURCES, host, stderr);
|
|
8041
8285
|
} catch {
|
|
8042
8286
|
return null;
|
|
8043
8287
|
}
|
|
@@ -8066,7 +8310,7 @@ async function tryReadDenialGuidance() {
|
|
|
8066
8310
|
}
|
|
8067
8311
|
}
|
|
8068
8312
|
//#endregion
|
|
8069
|
-
//#region src/request/
|
|
8313
|
+
//#region src/request/action-request-schema.ts
|
|
8070
8314
|
const userTaskSchema = strictObject({
|
|
8071
8315
|
text: string(),
|
|
8072
8316
|
origin: _enum([
|
|
@@ -8090,7 +8334,7 @@ const decisionContextSchema = strictObject({
|
|
|
8090
8334
|
lastDirectUserMessage: userTaskSchema.nullable(),
|
|
8091
8335
|
omittedTaskContext: array(omissionSchema)
|
|
8092
8336
|
});
|
|
8093
|
-
const
|
|
8337
|
+
const actionRequestSchema = strictObject({
|
|
8094
8338
|
sessionID: string().min(1),
|
|
8095
8339
|
toolUseID: string().min(1).optional(),
|
|
8096
8340
|
cwd: string().min(1),
|
|
@@ -8098,8 +8342,10 @@ const requestSchema = strictObject({
|
|
|
8098
8342
|
toolInput: looseObject({}),
|
|
8099
8343
|
context: decisionContextSchema
|
|
8100
8344
|
});
|
|
8345
|
+
//#endregion
|
|
8346
|
+
//#region src/request/parse-action-request.ts
|
|
8101
8347
|
function parseActionRequest(body) {
|
|
8102
|
-
const parsed =
|
|
8348
|
+
const parsed = actionRequestSchema.safeParse(body);
|
|
8103
8349
|
if (!parsed.success) return null;
|
|
8104
8350
|
const context = parsed.data.context;
|
|
8105
8351
|
return {
|
|
@@ -8123,4 +8369,4 @@ function renderVerdict(verdict) {
|
|
|
8123
8369
|
})).exhaustive();
|
|
8124
8370
|
}
|
|
8125
8371
|
//#endregion
|
|
8126
|
-
export { resolveConfigPath as C,
|
|
8372
|
+
export { resolveConfigPath as C, strictObject as D, number as E, string as O, resolveApiKey as S, PRESETS as T, findCheckout as _, loadSessionScope as a, DEFAULT_SCOPE_SOURCES as b, classifyWithModel as c, loadCheckoutRemotes as d, loadPolicy as f, resolveStateDir as g, splitCommandWords as h, resolveSessionScopePath as i, parseModelVerdict as l, splitShellCommand as m, parseActionRequest as n, EMPTY_SESSION_SCOPE as o, toRepositorySlug as p, classifyAction as r, classifyLocally as s, renderVerdict as t, loadRepositoryContext as u, DEFAULT_CONFIG as v, readHostEnvironment as w, loadConfig as x, DEFAULT_DENIAL_BUDGET as y };
|