auto-mode 0.11.0 → 0.13.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 +17848 -0
- package/dist/cli.js +2 -2
- package/dist/get-edit-fields-BtAvHIsq.js +20 -0
- package/dist/index.d.ts +53 -44
- package/dist/index.js +1 -1
- package/dist/{render-verdict-BiGjYb1x.js → render-verdict-De8aTmg-.js} +591 -463
- package/package.json +2 -1
- package/policy/classifier.md +4 -1
- package/policy/decision.md +1 -1
|
@@ -1,9 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as getEditFields } from "./get-edit-fields-BtAvHIsq.js";
|
|
2
|
+
import { lstat, readFile, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, matchesGlob, normalize, parse, relative, resolve } from "node:path";
|
|
2
4
|
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, isAbsolute, join, matchesGlob, normalize, parse, relative, resolve } from "node:path";
|
|
4
5
|
import { spawn } from "node:child_process";
|
|
5
|
-
import { createHash } from "node:crypto";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
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
|
|
7
20
|
//#region node_modules/.bun/zod@4.6.2/node_modules/zod/v4/core/util.js
|
|
8
21
|
function getEnumValues(entries) {
|
|
9
22
|
const numericValues = Object.values(entries).filter((v) => typeof v === "number");
|
|
@@ -5868,66 +5881,100 @@ async function loadClaudeRules(path) {
|
|
|
5868
5881
|
};
|
|
5869
5882
|
}
|
|
5870
5883
|
//#endregion
|
|
5871
|
-
//#region src/
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
"
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5884
|
+
//#region src/rules/split-command-words.ts
|
|
5885
|
+
function splitCommandWords(text) {
|
|
5886
|
+
const words = splitWords$1(text);
|
|
5887
|
+
let index = 0;
|
|
5888
|
+
while (index < words.length) {
|
|
5889
|
+
const word = words[index] ?? "";
|
|
5890
|
+
if (/^\w+=/u.test(word) || word === "sudo" || word === "command") index += 1;
|
|
5891
|
+
else if (word === "env") {
|
|
5892
|
+
index += 1;
|
|
5893
|
+
while (words[index] === "-u" || /^\w+=/u.test(words[index] ?? "")) index += words[index] === "-u" ? 2 : 1;
|
|
5894
|
+
} else if (word === "timeout") index += 2;
|
|
5895
|
+
else break;
|
|
5896
|
+
}
|
|
5897
|
+
return words.slice(index).filter((word, position, all) => !/^\d*[<>]/u.test(word) && !/^\d*>{1,2}\|?$/u.test(all[position - 1] ?? ""));
|
|
5898
|
+
}
|
|
5899
|
+
function splitWords$1(text) {
|
|
5900
|
+
const words = [];
|
|
5901
|
+
let current = "";
|
|
5902
|
+
let quote = null;
|
|
5903
|
+
let started = false;
|
|
5904
|
+
for (const ch of text) if (quote !== null) {
|
|
5905
|
+
if (ch === quote) quote = null;
|
|
5906
|
+
else current += ch;
|
|
5907
|
+
} else if (ch === "'" || ch === "\"") {
|
|
5908
|
+
quote = ch;
|
|
5909
|
+
started = true;
|
|
5910
|
+
} else if (/\s/u.test(ch)) {
|
|
5911
|
+
if (started) words.push(current);
|
|
5912
|
+
current = "";
|
|
5913
|
+
started = false;
|
|
5914
|
+
} else {
|
|
5915
|
+
current += ch;
|
|
5916
|
+
started = true;
|
|
5917
|
+
}
|
|
5918
|
+
if (started) words.push(current);
|
|
5919
|
+
return words;
|
|
5920
|
+
}
|
|
5921
|
+
//#endregion
|
|
5922
|
+
//#region src/rules/split-shell-command.ts
|
|
5923
|
+
function splitShellCommand(command) {
|
|
5924
|
+
const segments = [];
|
|
5925
|
+
let current = "";
|
|
5926
|
+
let quote = null;
|
|
5927
|
+
let unparsed = false;
|
|
5928
|
+
let i = 0;
|
|
5929
|
+
const collectSegment = () => {
|
|
5930
|
+
const text = current.trim();
|
|
5931
|
+
if (text !== "") segments.push({ text });
|
|
5932
|
+
current = "";
|
|
5933
|
+
};
|
|
5934
|
+
while (i < command.length) {
|
|
5935
|
+
const ch = command.charAt(i);
|
|
5936
|
+
if (quote !== null) {
|
|
5937
|
+
if (quote === "\"" && (command.startsWith("$(", i) || ch === "`")) unparsed = true;
|
|
5938
|
+
if (ch === quote) quote = null;
|
|
5939
|
+
current += ch;
|
|
5940
|
+
i += 1;
|
|
5888
5941
|
continue;
|
|
5889
5942
|
}
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
} catch {
|
|
5896
|
-
const link = await readFile(gitPath, "utf8");
|
|
5897
|
-
const path = /^gitdir: (?<path>.+)\r?\n?$/u.exec(link)?.groups?.["path"];
|
|
5898
|
-
if (path === void 0) return null;
|
|
5899
|
-
gitDir = resolve(directory, path.trim());
|
|
5900
|
-
head = await readFile(join(gitDir, "HEAD"), "utf8");
|
|
5901
|
-
}
|
|
5902
|
-
} catch {
|
|
5903
|
-
return null;
|
|
5943
|
+
if (ch === "'" || ch === "\"") {
|
|
5944
|
+
quote = ch;
|
|
5945
|
+
current += ch;
|
|
5946
|
+
i += 1;
|
|
5947
|
+
continue;
|
|
5904
5948
|
}
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
} catch {
|
|
5910
|
-
commonDir = gitDir;
|
|
5949
|
+
if (ch === "\\") {
|
|
5950
|
+
current += command.slice(i, i + 2);
|
|
5951
|
+
i += 2;
|
|
5952
|
+
continue;
|
|
5911
5953
|
}
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5954
|
+
if (command.startsWith("$(", i) || ch === "`" || command.startsWith("<(", i) || ch === ">") {
|
|
5955
|
+
unparsed = true;
|
|
5956
|
+
current += ch;
|
|
5957
|
+
i += 1;
|
|
5958
|
+
continue;
|
|
5917
5959
|
}
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
}
|
|
5960
|
+
if (command.startsWith("&&", i) || command.startsWith("||", i)) {
|
|
5961
|
+
collectSegment();
|
|
5962
|
+
i += 2;
|
|
5963
|
+
continue;
|
|
5964
|
+
}
|
|
5965
|
+
if (ch === ";" || ch === "|" || ch === "\n" || ch === "&") {
|
|
5966
|
+
collectSegment();
|
|
5967
|
+
i += 1;
|
|
5968
|
+
continue;
|
|
5969
|
+
}
|
|
5970
|
+
current += ch;
|
|
5971
|
+
i += 1;
|
|
5923
5972
|
}
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
const branch = value.slice(marker.length);
|
|
5930
|
-
return branch !== "" && branch.length <= 256 && !/\s/u.test(branch) ? branch : null;
|
|
5973
|
+
collectSegment();
|
|
5974
|
+
return {
|
|
5975
|
+
segments,
|
|
5976
|
+
hasUnparsedConstruct: unparsed || quote !== null
|
|
5977
|
+
};
|
|
5931
5978
|
}
|
|
5932
5979
|
//#endregion
|
|
5933
5980
|
//#region src/containment/to-repository-slug.ts
|
|
@@ -5938,346 +5985,57 @@ function toRepositorySlug(reference) {
|
|
|
5938
5985
|
return `${parts["host"] ?? ""}/${parts["owner"] ?? ""}/${parts["name"] ?? ""}`.toLowerCase();
|
|
5939
5986
|
}
|
|
5940
5987
|
//#endregion
|
|
5941
|
-
//#region src/
|
|
5942
|
-
function
|
|
5943
|
-
const
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
}
|
|
5952
|
-
//#endregion
|
|
5953
|
-
//#region src/scope/build-task-scope.ts
|
|
5954
|
-
function buildTaskScope(input) {
|
|
5955
|
-
const merged = mergeScopeFacts(input.facts);
|
|
5956
|
-
const branches = merged.branches.filter((branch) => branch !== input.defaultBranch && !(input.defaultBranch === null && DEFAULT_NAMES.has(branch)));
|
|
5957
|
-
const repositories = new Set(input.remotes.map((remote) => toRepositorySlug(remote.url)));
|
|
5958
|
-
const pullRequests = merged.pullRequests.filter((pull) => branches.includes(pull.head) && repositories.has(pull.repository)).map((pull) => ({
|
|
5959
|
-
number: pull.number,
|
|
5960
|
-
repository: pull.repository
|
|
5961
|
-
}));
|
|
5962
|
-
return {
|
|
5963
|
-
home: input.home,
|
|
5964
|
-
worktrees: merged.worktrees,
|
|
5965
|
-
branches,
|
|
5966
|
-
currentBranch: input.currentBranch,
|
|
5967
|
-
remotes: input.remotes,
|
|
5968
|
-
pullRequests,
|
|
5969
|
-
pathGlobs: merged.pathGlobs
|
|
5970
|
-
};
|
|
5988
|
+
//#region src/containment/collect-scope-findings.ts
|
|
5989
|
+
function collectScopeFindings(action, scope) {
|
|
5990
|
+
const filePath = action.input["file_path"] ?? action.input["notebook_path"];
|
|
5991
|
+
const command = action.input["command"];
|
|
5992
|
+
const findings = [
|
|
5993
|
+
...FILE_WRITE_TOOLS.has(action.tool) && typeof filePath === "string" ? collectTargetFindings(action.cwd, filePath, scope) : [],
|
|
5994
|
+
...action.tool.startsWith("mcp__") ? collectToolFindings(action, scope) : [],
|
|
5995
|
+
...action.tool === "Bash" && typeof command === "string" ? collectCommandFindings(command, action.cwd, scope) : []
|
|
5996
|
+
];
|
|
5997
|
+
return findings.filter((finding, index) => findings.findIndex((other) => other.kind === finding.kind && other.target === finding.target) === index);
|
|
5971
5998
|
}
|
|
5972
|
-
const
|
|
5973
|
-
"
|
|
5974
|
-
"
|
|
5975
|
-
"
|
|
5976
|
-
"
|
|
5999
|
+
const FILE_WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
6000
|
+
"Edit",
|
|
6001
|
+
"MultiEdit",
|
|
6002
|
+
"Write",
|
|
6003
|
+
"NotebookEdit"
|
|
5977
6004
|
]);
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
"GIT_DIR",
|
|
5982
|
-
"GIT_WORK_TREE",
|
|
5983
|
-
"GIT_COMMON_DIR"
|
|
5984
|
-
];
|
|
5985
|
-
async function findCheckout(start) {
|
|
5986
|
-
if (GIT_OVERRIDES.some((name) => process.env[name] !== void 0)) return null;
|
|
5987
|
-
for (let directory = start;; directory = dirname(directory)) {
|
|
5988
|
-
const gitPath = join(directory, ".git");
|
|
5989
|
-
const info = await stat(gitPath).catch(() => null);
|
|
5990
|
-
if (info !== null) {
|
|
5991
|
-
const gitDir = info.isDirectory() ? gitPath : await findLinkedGitDir(directory, gitPath);
|
|
5992
|
-
return gitDir === null ? null : {
|
|
5993
|
-
worktree: directory,
|
|
5994
|
-
commonDir: await findCommonDir(gitDir)
|
|
5995
|
-
};
|
|
5996
|
-
}
|
|
5997
|
-
if (dirname(directory) === directory) return null;
|
|
5998
|
-
}
|
|
6005
|
+
function collectTargetFindings(directory, target, scope) {
|
|
6006
|
+
const path = resolvePath(directory, target, scope.home);
|
|
6007
|
+
return path === null ? [] : collectPathFindings(path, scope);
|
|
5999
6008
|
}
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6009
|
+
const CREDENTIAL_PATHS = [
|
|
6010
|
+
".ssh",
|
|
6011
|
+
".aws",
|
|
6012
|
+
".gnupg",
|
|
6013
|
+
".gitconfig",
|
|
6014
|
+
".netrc",
|
|
6015
|
+
".npmrc",
|
|
6016
|
+
".config/gh"
|
|
6017
|
+
];
|
|
6018
|
+
function collectPathFindings(path, scope) {
|
|
6019
|
+
if (CREDENTIAL_PATHS.some((entry) => isUnder(path, join(scope.home, entry)))) return [{
|
|
6020
|
+
kind: "credential",
|
|
6021
|
+
target: path
|
|
6022
|
+
}];
|
|
6023
|
+
return isInScope(path, scope) ? [] : [{
|
|
6024
|
+
kind: "path",
|
|
6025
|
+
target: path
|
|
6026
|
+
}];
|
|
6004
6027
|
}
|
|
6005
|
-
|
|
6006
|
-
const
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
const header = /^\s*\[remote "(?<name>[^"]+)"\]/u.exec(line)?.groups?.["name"];
|
|
6017
|
-
if (header !== void 0) section = header;
|
|
6018
|
-
else if (/^\s*\[/u.test(line)) section = null;
|
|
6019
|
-
else if (section !== null) {
|
|
6020
|
-
const url = /^\s*(?:push)?url\s*=\s*(?<url>\S+)/u.exec(line)?.groups?.["url"];
|
|
6021
|
-
if (url !== void 0) remotes.push({
|
|
6022
|
-
name: section,
|
|
6023
|
-
url
|
|
6024
|
-
});
|
|
6025
|
-
}
|
|
6026
|
-
}
|
|
6027
|
-
return remotes;
|
|
6028
|
-
}
|
|
6029
|
-
//#endregion
|
|
6030
|
-
//#region src/scope/types.ts
|
|
6031
|
-
const EMPTY_SCOPE_FACTS = {
|
|
6032
|
-
worktrees: [],
|
|
6033
|
-
branches: [],
|
|
6034
|
-
pullRequests: [],
|
|
6035
|
-
pathGlobs: []
|
|
6036
|
-
};
|
|
6037
|
-
const EMPTY_SESSION_SCOPE = {
|
|
6038
|
-
worktrees: [],
|
|
6039
|
-
branches: [],
|
|
6040
|
-
pullRequests: []
|
|
6041
|
-
};
|
|
6042
|
-
//#endregion
|
|
6043
|
-
//#region src/scope/load-session-scope.ts
|
|
6044
|
-
const name = string().min(1);
|
|
6045
|
-
const pullRequestSchema = strictObject({
|
|
6046
|
-
number: number().int().positive(),
|
|
6047
|
-
head: name,
|
|
6048
|
-
repository: name
|
|
6049
|
-
});
|
|
6050
|
-
const branchSchema = strictObject({
|
|
6051
|
-
name,
|
|
6052
|
-
commonDir: name
|
|
6053
|
-
});
|
|
6054
|
-
const sessionScopeSchema = strictObject({
|
|
6055
|
-
worktrees: array(name),
|
|
6056
|
-
branches: array(branchSchema),
|
|
6057
|
-
pullRequests: array(pullRequestSchema)
|
|
6058
|
-
});
|
|
6059
|
-
async function loadSessionScope(path) {
|
|
6060
|
-
let raw;
|
|
6061
|
-
try {
|
|
6062
|
-
raw = await readFile(path, "utf8");
|
|
6063
|
-
} catch {
|
|
6064
|
-
return EMPTY_SESSION_SCOPE;
|
|
6065
|
-
}
|
|
6066
|
-
try {
|
|
6067
|
-
const parsed = sessionScopeSchema.safeParse(JSON.parse(raw));
|
|
6068
|
-
return parsed.success ? parsed.data : EMPTY_SESSION_SCOPE;
|
|
6069
|
-
} catch {
|
|
6070
|
-
return EMPTY_SESSION_SCOPE;
|
|
6071
|
-
}
|
|
6072
|
-
}
|
|
6073
|
-
//#endregion
|
|
6074
|
-
//#region src/scope/resolve-session-scope-path.ts
|
|
6075
|
-
function resolveSessionScopePath(stateDir, sessionID) {
|
|
6076
|
-
const key = createHash("sha256").update(sessionID).digest("hex").slice(0, 32);
|
|
6077
|
-
return join(stateDir, "session-scope", `${key}.json`);
|
|
6078
|
-
}
|
|
6079
|
-
//#endregion
|
|
6080
|
-
//#region src/scope/pick-scope-source-reader.ts
|
|
6081
|
-
function pickScopeSourceReader(source) {
|
|
6082
|
-
if (source.kind === "cwd") return (context) => Promise.resolve(buildFacts({
|
|
6083
|
-
worktrees: [context.worktree],
|
|
6084
|
-
branches: context.branch === null ? [] : [context.branch]
|
|
6085
|
-
}));
|
|
6086
|
-
if (source.kind === "session") return async (context) => {
|
|
6087
|
-
const scope = await loadSessionScope(resolveSessionScopePath(context.stateDir, context.sessionID));
|
|
6088
|
-
return buildFacts({
|
|
6089
|
-
worktrees: scope.worktrees,
|
|
6090
|
-
branches: scope.branches.filter((branch) => branch.commonDir === context.commonDir).map((branch) => branch.name),
|
|
6091
|
-
pullRequests: scope.pullRequests
|
|
6092
|
-
});
|
|
6093
|
-
};
|
|
6094
|
-
if (source.kind === "globs") return () => Promise.resolve(buildFacts({ pathGlobs: source.paths ?? [] }));
|
|
6095
|
-
return () => Promise.resolve(EMPTY_SCOPE_FACTS);
|
|
6096
|
-
}
|
|
6097
|
-
function buildFacts(facts) {
|
|
6098
|
-
return {
|
|
6099
|
-
...EMPTY_SCOPE_FACTS,
|
|
6100
|
-
...facts
|
|
6101
|
-
};
|
|
6102
|
-
}
|
|
6103
|
-
//#endregion
|
|
6104
|
-
//#region src/scope/load-task-scope.ts
|
|
6105
|
-
async function loadTaskScope(request, sources) {
|
|
6106
|
-
const cwd = resolve(request.cwd);
|
|
6107
|
-
const [checkout, context] = await Promise.all([findCheckout(cwd), loadRepositoryContext(cwd)]);
|
|
6108
|
-
const branch = context?.branch ?? null;
|
|
6109
|
-
const sourceContext = {
|
|
6110
|
-
sessionID: request.sessionID,
|
|
6111
|
-
cwd,
|
|
6112
|
-
worktree: checkout?.worktree ?? cwd,
|
|
6113
|
-
commonDir: checkout?.commonDir ?? null,
|
|
6114
|
-
branch,
|
|
6115
|
-
stateDir: request.stateDir
|
|
6116
|
-
};
|
|
6117
|
-
const [remotes, ...facts] = await Promise.all([checkout === null ? [] : loadCheckoutRemotes(checkout.commonDir), ...Object.values(sources).map((source) => pickScopeSourceReader(source)(sourceContext))]);
|
|
6118
|
-
return buildTaskScope({
|
|
6119
|
-
home: homedir(),
|
|
6120
|
-
currentBranch: branch,
|
|
6121
|
-
defaultBranch: context?.defaultBranch ?? null,
|
|
6122
|
-
remotes,
|
|
6123
|
-
facts
|
|
6124
|
-
});
|
|
6125
|
-
}
|
|
6126
|
-
//#endregion
|
|
6127
|
-
//#region src/state/resolve-state-dir.ts
|
|
6128
|
-
function resolveStateDir() {
|
|
6129
|
-
const stateHome = process.env["XDG_STATE_HOME"];
|
|
6130
|
-
const base = stateHome === void 0 || stateHome === "" ? join(homedir(), ".local", "state") : stateHome;
|
|
6131
|
-
return join(base, "auto-mode");
|
|
6132
|
-
}
|
|
6133
|
-
//#endregion
|
|
6134
|
-
//#region src/rules/split-command-words.ts
|
|
6135
|
-
function splitCommandWords(text) {
|
|
6136
|
-
const words = splitWords$1(text);
|
|
6137
|
-
let index = 0;
|
|
6138
|
-
while (index < words.length) {
|
|
6139
|
-
const word = words[index] ?? "";
|
|
6140
|
-
if (/^\w+=/u.test(word) || word === "sudo" || word === "command") index += 1;
|
|
6141
|
-
else if (word === "env") {
|
|
6142
|
-
index += 1;
|
|
6143
|
-
while (words[index] === "-u" || /^\w+=/u.test(words[index] ?? "")) index += words[index] === "-u" ? 2 : 1;
|
|
6144
|
-
} else if (word === "timeout") index += 2;
|
|
6145
|
-
else break;
|
|
6146
|
-
}
|
|
6147
|
-
return words.slice(index).filter((word, position, all) => !/^\d*[<>]/u.test(word) && !/^\d*>{1,2}\|?$/u.test(all[position - 1] ?? ""));
|
|
6148
|
-
}
|
|
6149
|
-
function splitWords$1(text) {
|
|
6150
|
-
const words = [];
|
|
6151
|
-
let current = "";
|
|
6152
|
-
let quote = null;
|
|
6153
|
-
let started = false;
|
|
6154
|
-
for (const ch of text) if (quote !== null) {
|
|
6155
|
-
if (ch === quote) quote = null;
|
|
6156
|
-
else current += ch;
|
|
6157
|
-
} else if (ch === "'" || ch === "\"") {
|
|
6158
|
-
quote = ch;
|
|
6159
|
-
started = true;
|
|
6160
|
-
} else if (/\s/u.test(ch)) {
|
|
6161
|
-
if (started) words.push(current);
|
|
6162
|
-
current = "";
|
|
6163
|
-
started = false;
|
|
6164
|
-
} else {
|
|
6165
|
-
current += ch;
|
|
6166
|
-
started = true;
|
|
6167
|
-
}
|
|
6168
|
-
if (started) words.push(current);
|
|
6169
|
-
return words;
|
|
6170
|
-
}
|
|
6171
|
-
//#endregion
|
|
6172
|
-
//#region src/rules/split-shell-command.ts
|
|
6173
|
-
function splitShellCommand(command) {
|
|
6174
|
-
const segments = [];
|
|
6175
|
-
let current = "";
|
|
6176
|
-
let quote = null;
|
|
6177
|
-
let unparsed = false;
|
|
6178
|
-
let i = 0;
|
|
6179
|
-
const collectSegment = () => {
|
|
6180
|
-
const text = current.trim();
|
|
6181
|
-
if (text !== "") segments.push({ text });
|
|
6182
|
-
current = "";
|
|
6183
|
-
};
|
|
6184
|
-
while (i < command.length) {
|
|
6185
|
-
const ch = command.charAt(i);
|
|
6186
|
-
if (quote !== null) {
|
|
6187
|
-
if (quote === "\"" && (command.startsWith("$(", i) || ch === "`")) unparsed = true;
|
|
6188
|
-
if (ch === quote) quote = null;
|
|
6189
|
-
current += ch;
|
|
6190
|
-
i += 1;
|
|
6191
|
-
continue;
|
|
6192
|
-
}
|
|
6193
|
-
if (ch === "'" || ch === "\"") {
|
|
6194
|
-
quote = ch;
|
|
6195
|
-
current += ch;
|
|
6196
|
-
i += 1;
|
|
6197
|
-
continue;
|
|
6198
|
-
}
|
|
6199
|
-
if (ch === "\\") {
|
|
6200
|
-
current += command.slice(i, i + 2);
|
|
6201
|
-
i += 2;
|
|
6202
|
-
continue;
|
|
6203
|
-
}
|
|
6204
|
-
if (command.startsWith("$(", i) || ch === "`" || command.startsWith("<(", i) || ch === ">") {
|
|
6205
|
-
unparsed = true;
|
|
6206
|
-
current += ch;
|
|
6207
|
-
i += 1;
|
|
6208
|
-
continue;
|
|
6209
|
-
}
|
|
6210
|
-
if (command.startsWith("&&", i) || command.startsWith("||", i)) {
|
|
6211
|
-
collectSegment();
|
|
6212
|
-
i += 2;
|
|
6213
|
-
continue;
|
|
6214
|
-
}
|
|
6215
|
-
if (ch === ";" || ch === "|" || ch === "\n" || ch === "&") {
|
|
6216
|
-
collectSegment();
|
|
6217
|
-
i += 1;
|
|
6218
|
-
continue;
|
|
6219
|
-
}
|
|
6220
|
-
current += ch;
|
|
6221
|
-
i += 1;
|
|
6222
|
-
}
|
|
6223
|
-
collectSegment();
|
|
6224
|
-
return {
|
|
6225
|
-
segments,
|
|
6226
|
-
hasUnparsedConstruct: unparsed || quote !== null
|
|
6227
|
-
};
|
|
6228
|
-
}
|
|
6229
|
-
//#endregion
|
|
6230
|
-
//#region src/containment/collect-scope-findings.ts
|
|
6231
|
-
function collectScopeFindings(action, scope) {
|
|
6232
|
-
const filePath = action.input["file_path"] ?? action.input["notebook_path"];
|
|
6233
|
-
const command = action.input["command"];
|
|
6234
|
-
const findings = [
|
|
6235
|
-
...FILE_WRITE_TOOLS.has(action.tool) && typeof filePath === "string" ? collectTargetFindings(action.cwd, filePath, scope) : [],
|
|
6236
|
-
...action.tool.startsWith("mcp__") ? collectToolFindings(action, scope) : [],
|
|
6237
|
-
...action.tool === "Bash" && typeof command === "string" ? collectCommandFindings(command, action.cwd, scope) : []
|
|
6238
|
-
];
|
|
6239
|
-
return findings.filter((finding, index) => findings.findIndex((other) => other.kind === finding.kind && other.target === finding.target) === index);
|
|
6240
|
-
}
|
|
6241
|
-
const FILE_WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
6242
|
-
"Edit",
|
|
6243
|
-
"MultiEdit",
|
|
6244
|
-
"Write",
|
|
6245
|
-
"NotebookEdit"
|
|
6246
|
-
]);
|
|
6247
|
-
function collectTargetFindings(directory, target, scope) {
|
|
6248
|
-
const path = resolvePath(directory, target, scope.home);
|
|
6249
|
-
return path === null ? [] : collectPathFindings(path, scope);
|
|
6250
|
-
}
|
|
6251
|
-
const CREDENTIAL_PATHS = [
|
|
6252
|
-
".ssh",
|
|
6253
|
-
".aws",
|
|
6254
|
-
".gnupg",
|
|
6255
|
-
".gitconfig",
|
|
6256
|
-
".netrc",
|
|
6257
|
-
".npmrc",
|
|
6258
|
-
".config/gh"
|
|
6259
|
-
];
|
|
6260
|
-
function collectPathFindings(path, scope) {
|
|
6261
|
-
if (CREDENTIAL_PATHS.some((entry) => isUnder(path, join(scope.home, entry)))) return [{
|
|
6262
|
-
kind: "credential",
|
|
6263
|
-
target: path
|
|
6264
|
-
}];
|
|
6265
|
-
return isInScope(path, scope) ? [] : [{
|
|
6266
|
-
kind: "path",
|
|
6267
|
-
target: path
|
|
6268
|
-
}];
|
|
6269
|
-
}
|
|
6270
|
-
function collectToolFindings(action, scope) {
|
|
6271
|
-
const verb = action.tool.split("__").at(-1) ?? "";
|
|
6272
|
-
if (!/merge|create|comment|update|delete|push|close|review/u.test(verb)) return [];
|
|
6273
|
-
const number = action.input["pullNumber"] ?? action.input["pull_number"] ?? action.input["issue_number"];
|
|
6274
|
-
const owner = action.input["owner"];
|
|
6275
|
-
const name = action.input["repo"];
|
|
6276
|
-
const repo = typeof owner === "string" && typeof name === "string" ? `${owner}/${name}` : void 0;
|
|
6277
|
-
return !verb.includes("merge") && typeof number === "number" && isOwnedPullRequest(number, repo, scope) ? [] : [{
|
|
6278
|
-
kind: "remote-write",
|
|
6279
|
-
target: action.tool
|
|
6280
|
-
}];
|
|
6028
|
+
function collectToolFindings(action, scope) {
|
|
6029
|
+
const verb = action.tool.split("__").at(-1) ?? "";
|
|
6030
|
+
if (!/merge|create|comment|update|delete|push|close|review/u.test(verb)) return [];
|
|
6031
|
+
const number = action.input["pullNumber"] ?? action.input["pull_number"] ?? action.input["issue_number"];
|
|
6032
|
+
const owner = action.input["owner"];
|
|
6033
|
+
const name = action.input["repo"];
|
|
6034
|
+
const repo = typeof owner === "string" && typeof name === "string" ? `${owner}/${name}` : void 0;
|
|
6035
|
+
return !verb.includes("merge") && typeof number === "number" && isOwnedPullRequest(number, repo, scope) ? [] : [{
|
|
6036
|
+
kind: "remote-write",
|
|
6037
|
+
target: action.tool
|
|
6038
|
+
}];
|
|
6281
6039
|
}
|
|
6282
6040
|
const PATH_WRITERS = /* @__PURE__ */ new Set([
|
|
6283
6041
|
"rm",
|
|
@@ -6719,17 +6477,7 @@ function isUnder(path, root) {
|
|
|
6719
6477
|
//#endregion
|
|
6720
6478
|
//#region src/containment/check-containment.ts
|
|
6721
6479
|
const CONTAINMENT_RULE = "Outside Task Scope";
|
|
6722
|
-
|
|
6723
|
-
let scope;
|
|
6724
|
-
try {
|
|
6725
|
-
scope = await loadTaskScope({
|
|
6726
|
-
sessionID: request.sessionID,
|
|
6727
|
-
cwd: request.cwd,
|
|
6728
|
-
stateDir: resolveStateDir()
|
|
6729
|
-
}, sources);
|
|
6730
|
-
} catch {
|
|
6731
|
-
return null;
|
|
6732
|
-
}
|
|
6480
|
+
function checkContainment(request, scope) {
|
|
6733
6481
|
const findings = collectScopeFindings({
|
|
6734
6482
|
tool: request.toolName,
|
|
6735
6483
|
cwd: request.cwd,
|
|
@@ -6759,6 +6507,18 @@ function formatList(one, many, values) {
|
|
|
6759
6507
|
return `the ${values.length === 1 ? one : many} ${values.join(", ")}`;
|
|
6760
6508
|
}
|
|
6761
6509
|
//#endregion
|
|
6510
|
+
//#region src/model/build-task-scope-summary.ts
|
|
6511
|
+
function buildTaskScopeSummary(scope) {
|
|
6512
|
+
return {
|
|
6513
|
+
worktrees: scope.worktrees,
|
|
6514
|
+
branches: scope.branches,
|
|
6515
|
+
pullRequests: scope.pullRequests.map((pull) => ({
|
|
6516
|
+
repository: pull.repository,
|
|
6517
|
+
number: pull.number
|
|
6518
|
+
}))
|
|
6519
|
+
};
|
|
6520
|
+
}
|
|
6521
|
+
//#endregion
|
|
6762
6522
|
//#region src/policy/resolve-shipped-policy-dir.ts
|
|
6763
6523
|
function resolveShippedPolicyDir() {
|
|
6764
6524
|
let dir = import.meta.dirname;
|
|
@@ -6862,7 +6622,7 @@ function readResult(body) {
|
|
|
6862
6622
|
//#endregion
|
|
6863
6623
|
//#region src/model/build-request.ts
|
|
6864
6624
|
const MAX_INPUT_CHARS = 4e3;
|
|
6865
|
-
function buildUserMessage(payload, transcript, reasoning) {
|
|
6625
|
+
function buildUserMessage(payload, transcript, reasoning, repositoryContext = null) {
|
|
6866
6626
|
const history = transcript.length === 0 ? "(no transcript available from this harness)" : transcript.map((entry) => `${entry.role}: ${entry.text}`).join("\n\n");
|
|
6867
6627
|
const input = JSON.stringify(payload.toolInput, null, 2).slice(0, MAX_INPUT_CHARS);
|
|
6868
6628
|
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.";
|
|
@@ -6870,7 +6630,7 @@ function buildUserMessage(payload, transcript, reasoning) {
|
|
|
6870
6630
|
${history}
|
|
6871
6631
|
</transcript>
|
|
6872
6632
|
|
|
6873
|
-
<action>
|
|
6633
|
+
${repositoryContext === null ? "" : `<repository>\n${JSON.stringify(repositoryContext, null, 2)}\n</repository>\n\n`}<action>
|
|
6874
6634
|
tool: ${payload.toolName}
|
|
6875
6635
|
cwd: ${payload.cwd}
|
|
6876
6636
|
input:
|
|
@@ -6974,57 +6734,196 @@ function classifyDecisionAnswers(request, result, minConfidence) {
|
|
|
6974
6734
|
const answer = result.answers[id];
|
|
6975
6735
|
if (answer === void 0) throw new Error("Decision answer missing");
|
|
6976
6736
|
return {
|
|
6977
|
-
rule,
|
|
6978
|
-
answer
|
|
6737
|
+
rule,
|
|
6738
|
+
answer
|
|
6739
|
+
};
|
|
6740
|
+
});
|
|
6741
|
+
for (const tier of ["hard", "soft"]) for (const entry of entries) if (entry.rule.tier === tier && entry.answer.choice === "block" && entry.answer.confidence >= minConfidence && entry.answer.probabilities.block >= minConfidence) return {
|
|
6742
|
+
kind: "block",
|
|
6743
|
+
rule: entry.rule
|
|
6744
|
+
};
|
|
6745
|
+
let uncertain = null;
|
|
6746
|
+
for (const entry of entries) {
|
|
6747
|
+
const answer = entry.answer;
|
|
6748
|
+
if (!(answer.choice === "allow" && answer.confidence >= minConfidence && answer.probabilities.allow >= minConfidence) && (uncertain === null || answer.probabilities.block > uncertain.answer.probabilities.block)) uncertain = entry;
|
|
6749
|
+
}
|
|
6750
|
+
return uncertain === null ? { kind: "allow" } : {
|
|
6751
|
+
kind: "uncertain",
|
|
6752
|
+
rule: uncertain.rule
|
|
6753
|
+
};
|
|
6754
|
+
}
|
|
6755
|
+
//#endregion
|
|
6756
|
+
//#region src/model/collect-decision-contributors.ts
|
|
6757
|
+
function collectDecisionContributors(request, result, minConfidence) {
|
|
6758
|
+
const decision = classifyDecisionAnswers(request, result, minConfidence);
|
|
6759
|
+
return Object.entries(request.rules).flatMap(([id, rule]) => {
|
|
6760
|
+
const answer = result.answers[id];
|
|
6761
|
+
if (answer === void 0) throw new Error("Decision answer missing");
|
|
6762
|
+
return (decision.kind === "block" ? rule === decision.rule : decision.kind === "uncertain" && (answer.choice !== "allow" || answer.confidence < minConfidence || answer.probabilities.allow < minConfidence)) ? [{
|
|
6763
|
+
rule: rule.source === "shipped" ? rule.name : id,
|
|
6764
|
+
source: rule.source,
|
|
6765
|
+
tier: rule.tier,
|
|
6766
|
+
choice: answer.choice,
|
|
6767
|
+
confidence: answer.confidence,
|
|
6768
|
+
probability: answer.probabilities[answer.choice]
|
|
6769
|
+
}] : [];
|
|
6770
|
+
});
|
|
6771
|
+
}
|
|
6772
|
+
//#endregion
|
|
6773
|
+
//#region src/model/decision-request-error.ts
|
|
6774
|
+
var DecisionRequestError = class extends Error {
|
|
6775
|
+
reason;
|
|
6776
|
+
requestBytes;
|
|
6777
|
+
constructor(reason, requestBytes, message, options) {
|
|
6778
|
+
super(message, options);
|
|
6779
|
+
this.name = "DecisionRequestError";
|
|
6780
|
+
this.reason = reason;
|
|
6781
|
+
this.requestBytes = requestBytes;
|
|
6782
|
+
}
|
|
6783
|
+
};
|
|
6784
|
+
//#endregion
|
|
6785
|
+
//#region src/model/format-classifier-note.ts
|
|
6786
|
+
function formatClassifierNote(note, apiKey) {
|
|
6787
|
+
return apiKey === null || apiKey === "" ? note : note.split(apiKey).join("[redacted]");
|
|
6788
|
+
}
|
|
6789
|
+
//#endregion
|
|
6790
|
+
//#region src/scope/load-checkout-remotes.ts
|
|
6791
|
+
async function loadCheckoutRemotes(commonDir) {
|
|
6792
|
+
const config = await readFile(join(commonDir, "config"), "utf8").catch(() => "");
|
|
6793
|
+
const remotes = [];
|
|
6794
|
+
let section = null;
|
|
6795
|
+
for (const line of config.split("\n")) {
|
|
6796
|
+
const header = /^\s*\[remote "(?<name>[^"]+)"\]/u.exec(line)?.groups?.["name"];
|
|
6797
|
+
if (header !== void 0) section = header;
|
|
6798
|
+
else if (/^\s*\[/u.test(line)) section = null;
|
|
6799
|
+
else if (section !== null) {
|
|
6800
|
+
const value = /^\s*(?:push)?url\s*=(?<value>.*)$/iu.exec(line)?.groups?.["value"];
|
|
6801
|
+
const url = value === void 0 ? "" : parseConfigValue(value);
|
|
6802
|
+
if (url !== "") remotes.push({
|
|
6803
|
+
name: section,
|
|
6804
|
+
url
|
|
6805
|
+
});
|
|
6806
|
+
}
|
|
6807
|
+
}
|
|
6808
|
+
return remotes;
|
|
6809
|
+
}
|
|
6810
|
+
function parseConfigValue(raw) {
|
|
6811
|
+
let value = "";
|
|
6812
|
+
let isQuoted = false;
|
|
6813
|
+
for (let index = 0; index < raw.length; index += 1) {
|
|
6814
|
+
const char = raw[index] ?? "";
|
|
6815
|
+
if (char === "\\") {
|
|
6816
|
+
const next = raw[index + 1] ?? "";
|
|
6817
|
+
value += {
|
|
6818
|
+
n: "\n",
|
|
6819
|
+
t: " ",
|
|
6820
|
+
b: ""
|
|
6821
|
+
}[next] ?? next;
|
|
6822
|
+
index += 1;
|
|
6823
|
+
} else if (char === "\"") isQuoted = !isQuoted;
|
|
6824
|
+
else if (!isQuoted && (char === "#" || char === ";")) break;
|
|
6825
|
+
else value += char;
|
|
6826
|
+
}
|
|
6827
|
+
return value.trim();
|
|
6828
|
+
}
|
|
6829
|
+
//#endregion
|
|
6830
|
+
//#region src/model/load-repository-context.ts
|
|
6831
|
+
async function loadRepositoryContext(cwd) {
|
|
6832
|
+
if ([
|
|
6833
|
+
"GIT_DIR",
|
|
6834
|
+
"GIT_WORK_TREE",
|
|
6835
|
+
"GIT_COMMON_DIR"
|
|
6836
|
+
].some((name) => process.env[name] !== void 0)) return null;
|
|
6837
|
+
let directory = resolve(cwd);
|
|
6838
|
+
for (;;) {
|
|
6839
|
+
const gitPath = join(directory, ".git");
|
|
6840
|
+
try {
|
|
6841
|
+
await stat(gitPath);
|
|
6842
|
+
} catch (error) {
|
|
6843
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) return null;
|
|
6844
|
+
const parent = dirname(directory);
|
|
6845
|
+
if (parent === directory) return null;
|
|
6846
|
+
directory = parent;
|
|
6847
|
+
continue;
|
|
6848
|
+
}
|
|
6849
|
+
let gitDir = gitPath;
|
|
6850
|
+
let head;
|
|
6851
|
+
try {
|
|
6852
|
+
try {
|
|
6853
|
+
head = await readFile(join(gitDir, "HEAD"), "utf8");
|
|
6854
|
+
} catch {
|
|
6855
|
+
const link = await readFile(gitPath, "utf8");
|
|
6856
|
+
const path = /^gitdir: (?<path>.+)\r?\n?$/u.exec(link)?.groups?.["path"];
|
|
6857
|
+
if (path === void 0) return null;
|
|
6858
|
+
gitDir = resolve(directory, path.trim());
|
|
6859
|
+
head = await readFile(join(gitDir, "HEAD"), "utf8");
|
|
6860
|
+
}
|
|
6861
|
+
} catch {
|
|
6862
|
+
return null;
|
|
6863
|
+
}
|
|
6864
|
+
let commonDir = gitDir;
|
|
6865
|
+
try {
|
|
6866
|
+
const common = await readFile(join(gitDir, "commondir"), "utf8");
|
|
6867
|
+
commonDir = resolve(gitDir, common.trim());
|
|
6868
|
+
} catch {
|
|
6869
|
+
commonDir = gitDir;
|
|
6870
|
+
}
|
|
6871
|
+
let defaultBranch = null;
|
|
6872
|
+
try {
|
|
6873
|
+
defaultBranch = parseReference(await readFile(join(commonDir, "refs", "remotes", "origin", "HEAD"), "utf8"), "refs/remotes/origin/");
|
|
6874
|
+
} catch {
|
|
6875
|
+
defaultBranch = null;
|
|
6876
|
+
}
|
|
6877
|
+
const remotes = await loadCheckoutRemotes(commonDir);
|
|
6878
|
+
return {
|
|
6879
|
+
cwd: resolve(cwd),
|
|
6880
|
+
branch: parseReference(head, "refs/heads/"),
|
|
6881
|
+
defaultBranch,
|
|
6882
|
+
remotes: remotes.flatMap((remote) => {
|
|
6883
|
+
const url = normalizeRemoteURL(remote.url);
|
|
6884
|
+
return url === null ? [] : [{
|
|
6885
|
+
name: remote.name,
|
|
6886
|
+
url
|
|
6887
|
+
}];
|
|
6888
|
+
})
|
|
6979
6889
|
};
|
|
6980
|
-
});
|
|
6981
|
-
for (const tier of ["hard", "soft"]) for (const entry of entries) if (entry.rule.tier === tier && entry.answer.choice === "block" && entry.answer.confidence >= minConfidence && entry.answer.probabilities.block >= minConfidence) return {
|
|
6982
|
-
kind: "block",
|
|
6983
|
-
rule: entry.rule
|
|
6984
|
-
};
|
|
6985
|
-
let uncertain = null;
|
|
6986
|
-
for (const entry of entries) {
|
|
6987
|
-
const answer = entry.answer;
|
|
6988
|
-
if (!(answer.choice === "allow" && answer.confidence >= minConfidence && answer.probabilities.allow >= minConfidence) && (uncertain === null || answer.probabilities.block > uncertain.answer.probabilities.block)) uncertain = entry;
|
|
6989
6890
|
}
|
|
6990
|
-
return uncertain === null ? { kind: "allow" } : {
|
|
6991
|
-
kind: "uncertain",
|
|
6992
|
-
rule: uncertain.rule
|
|
6993
|
-
};
|
|
6994
6891
|
}
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
6892
|
+
function parseReference(content, prefix) {
|
|
6893
|
+
const value = content.trim();
|
|
6894
|
+
const marker = `ref: ${prefix}`;
|
|
6895
|
+
if (!value.startsWith(marker)) return null;
|
|
6896
|
+
const branch = value.slice(marker.length);
|
|
6897
|
+
return branch !== "" && branch.length <= 256 && !/\s/u.test(branch) ? branch : null;
|
|
6898
|
+
}
|
|
6899
|
+
function normalizeRemoteURL(url) {
|
|
6900
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//iu.test(url)) {
|
|
6901
|
+
const parsed = URL.parse(url);
|
|
6902
|
+
if (parsed === null) return null;
|
|
6903
|
+
parsed.username = "";
|
|
6904
|
+
parsed.password = "";
|
|
6905
|
+
parsed.search = "";
|
|
6906
|
+
parsed.hash = "";
|
|
6907
|
+
return parsed.href;
|
|
6908
|
+
}
|
|
6909
|
+
const slash = url.indexOf("/");
|
|
6910
|
+
const end = slash === -1 ? url.length : slash;
|
|
6911
|
+
const at = url.lastIndexOf("@", end);
|
|
6912
|
+
return at === -1 ? url : url.slice(at + 1);
|
|
7011
6913
|
}
|
|
7012
6914
|
//#endregion
|
|
7013
|
-
//#region src/model/
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
//#region src/model/format-classifier-note.ts
|
|
7026
|
-
function formatClassifierNote(note, apiKey) {
|
|
7027
|
-
return apiKey === null || apiKey === "" ? note : note.split(apiKey).join("[redacted]");
|
|
6915
|
+
//#region src/model/load-repository-evidence.ts
|
|
6916
|
+
async function loadRepositoryEvidence(cwd, taskScope) {
|
|
6917
|
+
const context = await loadRepositoryContext(cwd);
|
|
6918
|
+
if (taskScope === void 0) return context;
|
|
6919
|
+
return {
|
|
6920
|
+
...context ?? {
|
|
6921
|
+
cwd: resolve(cwd),
|
|
6922
|
+
branch: null,
|
|
6923
|
+
defaultBranch: null
|
|
6924
|
+
},
|
|
6925
|
+
taskScope
|
|
6926
|
+
};
|
|
7028
6927
|
}
|
|
7029
6928
|
//#endregion
|
|
7030
6929
|
//#region node_modules/.bun/ts-pattern@5.9.0/node_modules/ts-pattern/dist/index.js
|
|
@@ -7537,17 +7436,13 @@ async function classifyWithJev(payload, config, options = {}) {
|
|
|
7537
7436
|
if (key === null) throw new Error("no API key: set the configured environment variable or key command");
|
|
7538
7437
|
stage = "evidence";
|
|
7539
7438
|
const directUserText = payload.decisionContext?.agentID === null ? payload.decisionContext.lastDirectUserMessage?.text ?? null : null;
|
|
7540
|
-
const command = payload.toolInput["command"] ?? payload.toolInput["cmd"];
|
|
7541
|
-
const hasGitCommand = typeof command === "string" && /\bgit\s/u.test(command);
|
|
7542
|
-
const isFileEdit = payload.toolName === "Write" || payload.toolName === "Edit";
|
|
7543
|
-
const needsRepositoryContext = hasGitCommand || isFileEdit;
|
|
7544
7439
|
const [policy, rules, repositoryContext] = await Promise.all([
|
|
7545
7440
|
loadPolicy({
|
|
7546
7441
|
classifierPath: config.classifierPath,
|
|
7547
7442
|
rulesPath: config.rulesPath
|
|
7548
7443
|
}, "decision.md"),
|
|
7549
7444
|
loadClaudeRules(config.claudeSettingsPath),
|
|
7550
|
-
|
|
7445
|
+
loadRepositoryEvidence(payload.cwd, options.taskScope).catch(() => null)
|
|
7551
7446
|
]);
|
|
7552
7447
|
const request = buildDecisionRequest(payload, policy, rules, directUserText, config.rulesPath === void 0 ? "shipped" : "replacement", repositoryContext);
|
|
7553
7448
|
const remainingMs = options.deadlineAt === void 0 ? config.provider.timeoutMs : options.deadlineAt - Date.now();
|
|
@@ -7655,10 +7550,12 @@ async function classifyWithModel(payload, config, options = {}) {
|
|
|
7655
7550
|
return buildFailure(config, `policy unreadable: ${toMessage(error)}`);
|
|
7656
7551
|
}
|
|
7657
7552
|
const directMessage = payload.decisionContext?.agentID === null ? payload.decisionContext.lastDirectUserMessage : null;
|
|
7658
|
-
const
|
|
7553
|
+
const transcript = directMessage === null ? [] : [{
|
|
7659
7554
|
role: "user",
|
|
7660
7555
|
text: directMessage.text
|
|
7661
|
-
}]
|
|
7556
|
+
}];
|
|
7557
|
+
const repositoryContext = await loadRepositoryEvidence(payload.cwd, options.taskScope).catch(() => null);
|
|
7558
|
+
const user = buildUserMessage(payload, transcript, config.provider.reasoning, repositoryContext);
|
|
7662
7559
|
try {
|
|
7663
7560
|
const result = await sendMessage(config.provider, apiKey, {
|
|
7664
7561
|
system,
|
|
@@ -7871,6 +7768,179 @@ function toUnquoted(word) {
|
|
|
7871
7768
|
return (first === "'" || first === "\"") && word.at(-1) === first ? word.slice(1, -1) : word;
|
|
7872
7769
|
}
|
|
7873
7770
|
//#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
|
+
//#region src/scope/merge-scope-facts.ts
|
|
7803
|
+
function mergeScopeFacts(all) {
|
|
7804
|
+
const pullRequests = /* @__PURE__ */ new Map();
|
|
7805
|
+
for (const pull of all.flatMap((facts) => facts.pullRequests)) pullRequests.set(`${pull.repository}#${String(pull.number)}`, pull);
|
|
7806
|
+
return {
|
|
7807
|
+
worktrees: [...new Set(all.flatMap((facts) => facts.worktrees))],
|
|
7808
|
+
branches: [...new Set(all.flatMap((facts) => facts.branches))],
|
|
7809
|
+
pullRequests: [...pullRequests.values()],
|
|
7810
|
+
pathGlobs: [...new Set(all.flatMap((facts) => facts.pathGlobs))]
|
|
7811
|
+
};
|
|
7812
|
+
}
|
|
7813
|
+
//#endregion
|
|
7814
|
+
//#region src/scope/build-task-scope.ts
|
|
7815
|
+
function buildTaskScope(input) {
|
|
7816
|
+
const merged = mergeScopeFacts(input.facts);
|
|
7817
|
+
const branches = merged.branches.filter((branch) => branch !== input.defaultBranch && !(input.defaultBranch === null && DEFAULT_NAMES.has(branch)));
|
|
7818
|
+
const repositories = new Set(input.remotes.map((remote) => toRepositorySlug(remote.url)));
|
|
7819
|
+
const pullRequests = merged.pullRequests.filter((pull) => branches.includes(pull.head) && repositories.has(pull.repository)).map((pull) => ({
|
|
7820
|
+
number: pull.number,
|
|
7821
|
+
repository: pull.repository
|
|
7822
|
+
}));
|
|
7823
|
+
return {
|
|
7824
|
+
home: input.home,
|
|
7825
|
+
worktrees: merged.worktrees,
|
|
7826
|
+
branches,
|
|
7827
|
+
currentBranch: input.currentBranch,
|
|
7828
|
+
remotes: input.remotes,
|
|
7829
|
+
pullRequests,
|
|
7830
|
+
pathGlobs: merged.pathGlobs
|
|
7831
|
+
};
|
|
7832
|
+
}
|
|
7833
|
+
const DEFAULT_NAMES = /* @__PURE__ */ new Set([
|
|
7834
|
+
"main",
|
|
7835
|
+
"master",
|
|
7836
|
+
"trunk",
|
|
7837
|
+
"develop"
|
|
7838
|
+
]);
|
|
7839
|
+
//#endregion
|
|
7840
|
+
//#region src/scope/types.ts
|
|
7841
|
+
const EMPTY_SCOPE_FACTS = {
|
|
7842
|
+
worktrees: [],
|
|
7843
|
+
branches: [],
|
|
7844
|
+
pullRequests: [],
|
|
7845
|
+
pathGlobs: []
|
|
7846
|
+
};
|
|
7847
|
+
const EMPTY_SESSION_SCOPE = {
|
|
7848
|
+
worktrees: [],
|
|
7849
|
+
branches: [],
|
|
7850
|
+
pullRequests: []
|
|
7851
|
+
};
|
|
7852
|
+
//#endregion
|
|
7853
|
+
//#region src/scope/load-session-scope.ts
|
|
7854
|
+
const name = string().min(1);
|
|
7855
|
+
const pullRequestSchema = strictObject({
|
|
7856
|
+
number: number().int().positive(),
|
|
7857
|
+
head: name,
|
|
7858
|
+
repository: name
|
|
7859
|
+
});
|
|
7860
|
+
const branchSchema = strictObject({
|
|
7861
|
+
name,
|
|
7862
|
+
commonDir: name
|
|
7863
|
+
});
|
|
7864
|
+
const sessionScopeSchema = strictObject({
|
|
7865
|
+
worktrees: array(name),
|
|
7866
|
+
branches: array(branchSchema),
|
|
7867
|
+
pullRequests: array(pullRequestSchema)
|
|
7868
|
+
});
|
|
7869
|
+
async function loadSessionScope(path) {
|
|
7870
|
+
let raw;
|
|
7871
|
+
try {
|
|
7872
|
+
raw = await readFile(path, "utf8");
|
|
7873
|
+
} catch {
|
|
7874
|
+
return EMPTY_SESSION_SCOPE;
|
|
7875
|
+
}
|
|
7876
|
+
try {
|
|
7877
|
+
const parsed = sessionScopeSchema.safeParse(JSON.parse(raw));
|
|
7878
|
+
return parsed.success ? parsed.data : EMPTY_SESSION_SCOPE;
|
|
7879
|
+
} catch {
|
|
7880
|
+
return EMPTY_SESSION_SCOPE;
|
|
7881
|
+
}
|
|
7882
|
+
}
|
|
7883
|
+
//#endregion
|
|
7884
|
+
//#region src/scope/resolve-session-scope-path.ts
|
|
7885
|
+
function resolveSessionScopePath(stateDir, sessionID) {
|
|
7886
|
+
const key = createHash("sha256").update(sessionID).digest("hex").slice(0, 32);
|
|
7887
|
+
return join(stateDir, "session-scope", `${key}.json`);
|
|
7888
|
+
}
|
|
7889
|
+
//#endregion
|
|
7890
|
+
//#region src/scope/pick-scope-source-reader.ts
|
|
7891
|
+
function pickScopeSourceReader(source) {
|
|
7892
|
+
if (source.kind === "cwd") return (context) => Promise.resolve(buildFacts({
|
|
7893
|
+
worktrees: [context.worktree],
|
|
7894
|
+
branches: context.branch === null ? [] : [context.branch]
|
|
7895
|
+
}));
|
|
7896
|
+
if (source.kind === "session") return async (context) => {
|
|
7897
|
+
const scope = await loadSessionScope(resolveSessionScopePath(context.stateDir, context.sessionID));
|
|
7898
|
+
return buildFacts({
|
|
7899
|
+
worktrees: scope.worktrees,
|
|
7900
|
+
branches: scope.branches.filter((branch) => branch.commonDir === context.commonDir).map((branch) => branch.name),
|
|
7901
|
+
pullRequests: scope.pullRequests
|
|
7902
|
+
});
|
|
7903
|
+
};
|
|
7904
|
+
if (source.kind === "globs") return () => Promise.resolve(buildFacts({ pathGlobs: source.paths ?? [] }));
|
|
7905
|
+
return () => Promise.resolve(EMPTY_SCOPE_FACTS);
|
|
7906
|
+
}
|
|
7907
|
+
function buildFacts(facts) {
|
|
7908
|
+
return {
|
|
7909
|
+
...EMPTY_SCOPE_FACTS,
|
|
7910
|
+
...facts
|
|
7911
|
+
};
|
|
7912
|
+
}
|
|
7913
|
+
//#endregion
|
|
7914
|
+
//#region src/scope/load-task-scope.ts
|
|
7915
|
+
async function loadTaskScope(request, sources) {
|
|
7916
|
+
const cwd = resolve(request.cwd);
|
|
7917
|
+
const [checkout, context] = await Promise.all([findCheckout(cwd), loadRepositoryContext(cwd)]);
|
|
7918
|
+
const branch = context?.branch ?? null;
|
|
7919
|
+
const sourceContext = {
|
|
7920
|
+
sessionID: request.sessionID,
|
|
7921
|
+
cwd,
|
|
7922
|
+
worktree: checkout?.worktree ?? cwd,
|
|
7923
|
+
commonDir: checkout?.commonDir ?? null,
|
|
7924
|
+
branch,
|
|
7925
|
+
stateDir: request.stateDir
|
|
7926
|
+
};
|
|
7927
|
+
const [remotes, ...facts] = await Promise.all([checkout === null ? [] : loadCheckoutRemotes(checkout.commonDir), ...Object.values(sources).map((source) => pickScopeSourceReader(source)(sourceContext))]);
|
|
7928
|
+
return buildTaskScope({
|
|
7929
|
+
home: homedir(),
|
|
7930
|
+
currentBranch: branch,
|
|
7931
|
+
defaultBranch: context?.defaultBranch ?? null,
|
|
7932
|
+
remotes,
|
|
7933
|
+
facts
|
|
7934
|
+
});
|
|
7935
|
+
}
|
|
7936
|
+
//#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
|
|
7874
7944
|
//#region src/classify-action.ts
|
|
7875
7945
|
async function classifyAction(request, config, options = {}) {
|
|
7876
7946
|
let configured = null;
|
|
@@ -7886,9 +7956,11 @@ async function classifyAction(request, config, options = {}) {
|
|
|
7886
7956
|
unavailable: true
|
|
7887
7957
|
};
|
|
7888
7958
|
}
|
|
7889
|
-
const
|
|
7959
|
+
const hasConfiguredDenies = configured !== null && (configured.hard_deny.length > 0 || configured.soft_deny.length > 0);
|
|
7960
|
+
const local = hasConfiguredDenies ? { kind: "escalate" } : classifyLocally(request);
|
|
7890
7961
|
if (local.kind === "allow" && local.exception === READ_ONLY_EXCEPTION) return buildLocalAllow(local.exception);
|
|
7891
|
-
const
|
|
7962
|
+
const scope = await tryLoadTaskScope(request, config);
|
|
7963
|
+
const containment = scope === null ? null : checkContainment(request, scope);
|
|
7892
7964
|
if (containment !== null) {
|
|
7893
7965
|
const guidance = await tryReadDenialGuidance();
|
|
7894
7966
|
return {
|
|
@@ -7899,13 +7971,24 @@ async function classifyAction(request, config, options = {}) {
|
|
|
7899
7971
|
};
|
|
7900
7972
|
}
|
|
7901
7973
|
if (local.kind === "allow") return buildLocalAllow(local.exception);
|
|
7974
|
+
const edit = hasConfiguredDenies || scope === null ? null : await tryClassifyEdit(request, scope);
|
|
7975
|
+
if (edit?.kind === "bypass") return {
|
|
7976
|
+
verdict: { kind: "allow" },
|
|
7977
|
+
decidingStage: "bypass",
|
|
7978
|
+
note: `allowed by the edit bypass: ${edit.target}`,
|
|
7979
|
+
status: "allow"
|
|
7980
|
+
};
|
|
7902
7981
|
if (options.localOnly === true) return {
|
|
7903
7982
|
verdict: null,
|
|
7904
7983
|
decidingStage: "local",
|
|
7905
7984
|
note: `${request.toolName} needs the model tier, which this run skipped`,
|
|
7906
7985
|
status: "skipped"
|
|
7907
7986
|
};
|
|
7908
|
-
const
|
|
7987
|
+
const taskScope = scope === null ? void 0 : buildTaskScopeSummary(scope);
|
|
7988
|
+
const [outcome, guidance] = await Promise.all([classifyWithModel(request, config, {
|
|
7989
|
+
...options,
|
|
7990
|
+
taskScope
|
|
7991
|
+
}), tryReadDenialGuidance()]);
|
|
7909
7992
|
return {
|
|
7910
7993
|
...outcome,
|
|
7911
7994
|
decidingStage: config.provider.protocol === "system-one" ? "jev" : "messages",
|
|
@@ -7914,6 +7997,51 @@ async function classifyAction(request, config, options = {}) {
|
|
|
7914
7997
|
};
|
|
7915
7998
|
}
|
|
7916
7999
|
const READ_ONLY_EXCEPTION = "Read-only actions";
|
|
8000
|
+
async function tryLoadTaskScope(request, config) {
|
|
8001
|
+
try {
|
|
8002
|
+
return await loadTaskScope({
|
|
8003
|
+
sessionID: request.sessionID,
|
|
8004
|
+
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
|
+
});
|
|
8041
|
+
} catch {
|
|
8042
|
+
return null;
|
|
8043
|
+
}
|
|
8044
|
+
}
|
|
7917
8045
|
function buildLocalAllow(exception) {
|
|
7918
8046
|
return {
|
|
7919
8047
|
verdict: { kind: "allow" },
|
|
@@ -7995,4 +8123,4 @@ function renderVerdict(verdict) {
|
|
|
7995
8123
|
})).exhaustive();
|
|
7996
8124
|
}
|
|
7997
8125
|
//#endregion
|
|
7998
|
-
export { resolveConfigPath as C, string as D, strictObject as E, resolveApiKey as S, number as T,
|
|
8126
|
+
export { resolveConfigPath as C, string as D, strictObject as E, resolveApiKey as S, number as T, splitCommandWords as _, resolveSessionScopePath as a, DEFAULT_SCOPE_SOURCES as b, findCheckout as c, parseModelVerdict as d, loadRepositoryContext as f, splitShellCommand as g, toRepositorySlug as h, resolveStateDir as i, classifyLocally as l, loadPolicy as m, parseActionRequest as n, loadSessionScope as o, loadCheckoutRemotes as p, classifyAction as r, EMPTY_SESSION_SCOPE as s, renderVerdict as t, classifyWithModel as u, DEFAULT_CONFIG as v, PRESETS as w, loadConfig as x, DEFAULT_DENIAL_BUDGET as y };
|