auto-mode 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/classify-edit-O8JNCVde.js +138 -0
- package/dist/classify-reply-message-CrZ6S-AR.js +28 -0
- package/dist/cli.js +3 -3
- package/dist/{classify-edit-B660kX2I.js → find-secret-DUEWxfkc.js} +945 -718
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -1
- package/dist/{parse-action-request-C55m5DlM.js → parse-action-request-DySYvZxl.js} +1099 -142
- package/dist/{schemas-B_w83Dze.js → schemas-B80Yxv6m.js} +19 -19
- package/dist/{start-judge-server-BlhDO_4A.js → start-judge-server-CDYdRR4I.js} +2 -2
- package/dist/start-judge-session-C0eRJFlg.js +1 -0
- package/dist/{start-judge-session-vxiG-kDp.js → start-judge-session-vhUxe9-V.js} +14482 -13079
- package/mods/auto-mode/contract/cli-limits.ts +0 -6
- package/mods/auto-mode/contract/message-origins.ts +1 -1
- package/mods/auto-mode/contract/scope-commands.ts +0 -2
- package/mods/auto-mode/hooks/is-scope-command.ts +1 -2
- package/mods/auto-mode/hooks/parse-decision.ts +0 -2
- package/mods/auto-mode/hooks/register.ts +0 -2
- package/package.json +23 -17
- package/policy/judge.md +2 -0
- package/dist/start-judge-session-B-KYQG13.js +0 -1
package/README.md
CHANGED
|
@@ -33,6 +33,8 @@ undo.
|
|
|
33
33
|
([containment](./docs/architecture/stages/containment.md)).
|
|
34
34
|
- **The edit bypass** allows an in-scope file edit once a secret scan finds nothing
|
|
35
35
|
([the edit bypass](./docs/architecture/stages/edit-bypass.md)).
|
|
36
|
+
- **The reply channel** allows a message to a recipient the session's launcher declared as its own
|
|
37
|
+
([the reply channel](./docs/architecture/stages/reply-channel.md)).
|
|
36
38
|
- **Jev** judges the remaining calls against the shipped rules and your own
|
|
37
39
|
([Jev](./docs/architecture/stages/jev.md)).
|
|
38
40
|
- **The judge**, on by default, reviews every Jev deny, and overturns it when your last message
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { t as findEditFields } from "./find-edit-fields-CzkhPA4j.js";
|
|
2
|
+
import { t as findSecret } from "./find-secret-DUEWxfkc.js";
|
|
3
|
+
import { basename, relative, sep } from "node:path";
|
|
4
|
+
import { Buffer } from "node:buffer";
|
|
5
|
+
//#region src/bypass/classify-edit.ts
|
|
6
|
+
const MAX_SCANNED_BYTES = 262144;
|
|
7
|
+
function classifyEdit(action, scope) {
|
|
8
|
+
const fields = findEditFields(action.toolName);
|
|
9
|
+
if (fields === null) return {
|
|
10
|
+
kind: "jev",
|
|
11
|
+
reason: "not a file-tool edit"
|
|
12
|
+
};
|
|
13
|
+
const worktree = findWorktree(action.target, scope.worktrees);
|
|
14
|
+
if (worktree === null) return {
|
|
15
|
+
kind: "jev",
|
|
16
|
+
reason: "target outside every in-scope worktree"
|
|
17
|
+
};
|
|
18
|
+
const path = relative(worktree, action.target).split(sep).join("/");
|
|
19
|
+
if (path.startsWith(".worktrees/")) return {
|
|
20
|
+
kind: "jev",
|
|
21
|
+
reason: "target in a nested worktree outside the scope"
|
|
22
|
+
};
|
|
23
|
+
if (action.checkout !== null && action.checkout !== worktree) return {
|
|
24
|
+
kind: "jev",
|
|
25
|
+
reason: "target in a checkout outside the scope"
|
|
26
|
+
};
|
|
27
|
+
const exclusion = [action.target, action.requested].some((each) => scope.protectedDirs.some((directory) => isWithin(each, directory))) ? "auto-mode configuration or state" : findExclusion(path) ?? findExclusion(toWorktreePath(action.requested, scope.worktrees));
|
|
28
|
+
if (exclusion !== null) return {
|
|
29
|
+
kind: "jev",
|
|
30
|
+
reason: `target is ${exclusion}`
|
|
31
|
+
};
|
|
32
|
+
const text = buildScanText(action, fields.content);
|
|
33
|
+
if (typeof text !== "string") return {
|
|
34
|
+
kind: "jev",
|
|
35
|
+
reason: text.reason
|
|
36
|
+
};
|
|
37
|
+
if (Buffer.byteLength(text, "utf8") > MAX_SCANNED_BYTES) return {
|
|
38
|
+
kind: "jev",
|
|
39
|
+
reason: "content larger than the secret scan reads"
|
|
40
|
+
};
|
|
41
|
+
const secret = findSecret({
|
|
42
|
+
text,
|
|
43
|
+
path
|
|
44
|
+
});
|
|
45
|
+
return secret === null ? {
|
|
46
|
+
kind: "bypass",
|
|
47
|
+
target: action.target
|
|
48
|
+
} : {
|
|
49
|
+
kind: "jev",
|
|
50
|
+
reason: `secret scan matched ${secret.rule}`
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function findWorktree(target, worktrees) {
|
|
54
|
+
return worktrees.filter((worktree) => isWithin(target, worktree)).toSorted((left, right) => right.length - left.length)[0] ?? null;
|
|
55
|
+
}
|
|
56
|
+
function isWithin(path, directory) {
|
|
57
|
+
const offset = relative(directory, path);
|
|
58
|
+
return offset !== "" && !offset.startsWith("..") && !offset.startsWith(sep);
|
|
59
|
+
}
|
|
60
|
+
const EXCLUDED_PATHS = [
|
|
61
|
+
[".git", "git metadata"],
|
|
62
|
+
[".claude", "agent configuration"],
|
|
63
|
+
[".codex", "agent configuration"],
|
|
64
|
+
[".muse", "agent configuration"],
|
|
65
|
+
[".husky", "a hook file"],
|
|
66
|
+
[".githooks", "a hook file"],
|
|
67
|
+
[".vscode/tasks.json", "a hook file"],
|
|
68
|
+
[".github/workflows", "a CI workflow"],
|
|
69
|
+
[".circleci", "a CI workflow"],
|
|
70
|
+
[".ssh", "a credential file"],
|
|
71
|
+
[".aws", "a credential file"],
|
|
72
|
+
[".kube", "a credential file"],
|
|
73
|
+
[".docker", "a credential file"],
|
|
74
|
+
[".gnupg", "a credential file"]
|
|
75
|
+
];
|
|
76
|
+
const EXCLUDED_NAMES = [
|
|
77
|
+
[/^(?:agents|claude)(?:\.[\w-]+)?\.md$/iu, "agent configuration"],
|
|
78
|
+
[/^\.mcp\.json$/iu, "agent configuration"],
|
|
79
|
+
[/^settings(?:\.[\w-]+)?\.json$/iu, "a settings file"],
|
|
80
|
+
[/^(?:hooks\.json|\.?lefthook(?:-local)?\.ya?ml|\.pre-commit-config\.ya?ml)$/iu, "a hook file"],
|
|
81
|
+
[/^(?:package\.json|\.?bunfig\.toml|\.gitattributes)$/iu, "a hook file"],
|
|
82
|
+
[/^\.gitlab-ci\.ya?ml$/iu, "a CI workflow"],
|
|
83
|
+
[/^\.env(?:\..*)?$|^\.envrc$/iu, "an env file"],
|
|
84
|
+
[/^(?:\.netrc|\.npmrc|\.pypirc|\.git-credentials|\.dockercfg|kubeconfig)$/iu, "a credential file"],
|
|
85
|
+
[/^credentials(?:\.\w+)?$|^secrets?\.(?:json|ya?ml|toml|env)$/iu, "a credential file"],
|
|
86
|
+
[/^id_(?:rsa|dsa|ecdsa|ed25519)|\.(?:pem|key|p12|pfx|jks|keystore|tfvars)$/iu, "a credential file"]
|
|
87
|
+
];
|
|
88
|
+
function findExclusion(path) {
|
|
89
|
+
const lower = path.toLowerCase();
|
|
90
|
+
const excludedPath = EXCLUDED_PATHS.find(([prefix]) => lower === prefix || lower.endsWith(`/${prefix}`) || lower.startsWith(`${prefix}/`) || lower.includes(`/${prefix}/`));
|
|
91
|
+
if (excludedPath !== void 0) return excludedPath[1];
|
|
92
|
+
const name = basename(path);
|
|
93
|
+
return EXCLUDED_NAMES.find(([pattern]) => pattern.test(name))?.[1] ?? null;
|
|
94
|
+
}
|
|
95
|
+
function toWorktreePath(path, worktrees) {
|
|
96
|
+
const worktree = findWorktree(path, worktrees);
|
|
97
|
+
return worktree === null ? path : relative(worktree, path).split(sep).join("/");
|
|
98
|
+
}
|
|
99
|
+
function buildScanText(action, field) {
|
|
100
|
+
const content = action.toolInput[field] ?? "";
|
|
101
|
+
if (typeof content !== "string") return { reason: "edit content is not text" };
|
|
102
|
+
if (action.toolName !== "Edit") return content;
|
|
103
|
+
const old = action.toolInput["old_string"];
|
|
104
|
+
if (typeof old !== "string" || old === "" || action.current === null) return { reason: "the edit cannot be read in the context of its file" };
|
|
105
|
+
const parts = action.current.split(old);
|
|
106
|
+
if (parts.length === 1) return { reason: "the edit text is not in the file" };
|
|
107
|
+
const replaced = action.toolInput["replace_all"] === true ? parts.length - 1 : 1;
|
|
108
|
+
const spans = [];
|
|
109
|
+
let produced = parts[0] ?? "";
|
|
110
|
+
for (let index = 1; index < parts.length; index += 1) {
|
|
111
|
+
const isReplaced = index <= replaced;
|
|
112
|
+
if (isReplaced) spans.push({
|
|
113
|
+
start: produced.length,
|
|
114
|
+
end: produced.length + content.length
|
|
115
|
+
});
|
|
116
|
+
produced += `${isReplaced ? content : old}${parts[index] ?? ""}`;
|
|
117
|
+
}
|
|
118
|
+
return collectContext(produced, spans);
|
|
119
|
+
}
|
|
120
|
+
const CONTEXT_LINES = 12;
|
|
121
|
+
function collectContext(text, spans) {
|
|
122
|
+
const lines = text.split("\n");
|
|
123
|
+
const starts = [];
|
|
124
|
+
let offset = 0;
|
|
125
|
+
for (const line of lines) {
|
|
126
|
+
starts.push(offset);
|
|
127
|
+
offset += line.length + 1;
|
|
128
|
+
}
|
|
129
|
+
const keep = /* @__PURE__ */ new Set();
|
|
130
|
+
for (const span of spans) {
|
|
131
|
+
const first = starts.findLastIndex((start) => start <= span.start);
|
|
132
|
+
const last = starts.findLastIndex((start) => start <= span.end);
|
|
133
|
+
for (let line = first - CONTEXT_LINES; line <= last + CONTEXT_LINES; line += 1) keep.add(line);
|
|
134
|
+
}
|
|
135
|
+
return lines.filter((_, index) => keep.has(index)).join("\n");
|
|
136
|
+
}
|
|
137
|
+
//#endregion
|
|
138
|
+
export { classifyEdit };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { t as findSecret } from "./find-secret-DUEWxfkc.js";
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
//#region src/bypass/classify-reply-message.ts
|
|
4
|
+
const MAX_SCANNED_BYTES = 262144;
|
|
5
|
+
function classifyReplyMessage(toolInput) {
|
|
6
|
+
const text = [JSON.stringify(toolInput), ...collectStrings(toolInput)].join("\n");
|
|
7
|
+
if (Buffer.byteLength(text, "utf8") > MAX_SCANNED_BYTES) return {
|
|
8
|
+
kind: "jev",
|
|
9
|
+
reason: "message larger than the secret scan reads"
|
|
10
|
+
};
|
|
11
|
+
const secret = findSecret({
|
|
12
|
+
text,
|
|
13
|
+
path: null
|
|
14
|
+
});
|
|
15
|
+
return secret === null ? { kind: "bypass" } : {
|
|
16
|
+
kind: "jev",
|
|
17
|
+
reason: `secret scan matched ${secret.rule}`
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function collectStrings(value) {
|
|
21
|
+
if (typeof value === "string") return [value];
|
|
22
|
+
if (typeof value !== "object" || value === null) return [];
|
|
23
|
+
const strings = [];
|
|
24
|
+
for (const [key, entry] of Object.entries(value)) strings.push(key, ...collectStrings(entry));
|
|
25
|
+
return strings;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { classifyReplyMessage };
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as pickJudgeLane, E as updateJSONFile, S as CLI_LIMITS, T as resolveXDGDir, _ as readCheckout, a as resolveSessionScopePath, b as loadConfig, c as splitGitCommand, d as loadCheckoutFacts, f as writeActionDiagnostic, g as isUnresolvedWord, h as resolveShellPath, i as classifyAction, l as loadPolicy, m as collectShellCommands, n as readRuntime, o as loadSessionScope, p as toRepositorySlug, s as EMPTY_SESSION_SCOPE, t as parseActionRequest, u as resolveShippedPolicyDir, v as readGitEntry, w as M, x as runCapped, y as DEFAULT_SCOPE_SOURCES } from "./parse-action-request-
|
|
3
|
-
import { a as literal, d as string, i as discriminatedUnion, r as boolean, s as number, u as strictObject } from "./schemas-
|
|
2
|
+
import { C as pickJudgeLane, E as updateJSONFile, S as CLI_LIMITS, T as resolveXDGDir, _ as readCheckout, a as resolveSessionScopePath, b as loadConfig, c as splitGitCommand, d as loadCheckoutFacts, f as writeActionDiagnostic, g as isUnresolvedWord, h as resolveShellPath, i as classifyAction, l as loadPolicy, m as collectShellCommands, n as readRuntime, o as loadSessionScope, p as toRepositorySlug, s as EMPTY_SESSION_SCOPE, t as parseActionRequest, u as resolveShippedPolicyDir, v as readGitEntry, w as M, x as runCapped, y as DEFAULT_SCOPE_SOURCES } from "./parse-action-request-DySYvZxl.js";
|
|
3
|
+
import { a as literal, d as string, i as discriminatedUnion, r as boolean, s as number, u as strictObject } from "./schemas-B80Yxv6m.js";
|
|
4
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
5
|
import { chmod, mkdir, open, readFile, realpath, stat } from "node:fs/promises";
|
|
6
6
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
@@ -3283,7 +3283,7 @@ async function runJudgeServer(io, socketPath) {
|
|
|
3283
3283
|
io.stderr.write(`auto-mode: judge-server needs a socket path\n\n${USAGE}`);
|
|
3284
3284
|
return 2;
|
|
3285
3285
|
}
|
|
3286
|
-
await (await (await import("./start-judge-server-
|
|
3286
|
+
await (await (await import("./start-judge-server-CDYdRR4I.js")).startJudgeServer({
|
|
3287
3287
|
socketPath,
|
|
3288
3288
|
env: io.runtime.host.env,
|
|
3289
3289
|
idleMs: JUDGE_IDLE_MS
|