auto-mode 3.0.1 → 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 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-CaQy7FA3.js";
3
- import { a as literal, d as string, i as discriminatedUnion, r as boolean, s as number, u as strictObject } from "./schemas-B_w83Dze.js";
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-BlhDO_4A.js")).startJudgeServer({
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