auto-mode 0.16.0 → 0.17.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/cli.js +103 -7
- package/dist/index.d.ts +44 -4
- package/dist/index.js +1 -1
- package/dist/{render-verdict-u3_ftSEp.js → render-verdict-Bx9Lw7gO.js} +1089 -790
- package/mods/auto-mode/hooks/build-prompt-context.ts +18 -8
- package/mods/auto-mode/hooks/register.ts +6 -13
- package/mods/auto-mode/hooks/types.ts +5 -1
- package/package.json +4 -2
- package/policy/judge.md +57 -0
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as resolveStateDir, D as DEFAULT_SCOPE_SOURCES, E as DEFAULT_DENIAL_BUDGET, F as string, N as number, O as loadConfig, P as strictObject, a as loadSessionScope, b as splitCommandWords, f as loadRepositoryContext, g as loadPolicy, i as resolveSessionScopePath, j as readHostEnvironment, n as parseActionRequest, o as EMPTY_SESSION_SCOPE, p as loadCheckoutRemotes, r as classifyAction, t as renderVerdict, v as toRepositorySlug, w as findCheckout, y as splitShellCommand } from "./render-verdict-
|
|
3
|
-
import { appendFile, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
|
-
import { dirname, isAbsolute, join, normalize } from "node:path";
|
|
2
|
+
import { C as resolveStateDir, D as DEFAULT_SCOPE_SOURCES, E as DEFAULT_DENIAL_BUDGET, F as string, N as number, O as loadConfig, P as strictObject, a as loadSessionScope, b as splitCommandWords, f as loadRepositoryContext, g as loadPolicy, i as resolveSessionScopePath, j as readHostEnvironment, n as parseActionRequest, o as EMPTY_SESSION_SCOPE, p as loadCheckoutRemotes, r as classifyAction, t as renderVerdict, v as toRepositorySlug, w as findCheckout, y as splitShellCommand } from "./render-verdict-Bx9Lw7gO.js";
|
|
3
|
+
import { appendFile, chmod, lstat, mkdir, open, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, normalize, resolve } from "node:path";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
+
import { constants } from "node:fs";
|
|
6
7
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
8
|
import { text } from "node:stream/consumers";
|
|
8
9
|
import { parseArgs } from "node:util";
|
|
@@ -16,11 +17,13 @@ const DESCRIPTIVE_FIELDS = { Bash: [
|
|
|
16
17
|
function buildRetryKey(request) {
|
|
17
18
|
const ignored = Object.hasOwn(DESCRIPTIVE_FIELDS, request.toolName) ? DESCRIPTIVE_FIELDS[request.toolName] ?? [] : [];
|
|
18
19
|
const input = Object.fromEntries(Object.entries(request.toolInput).filter(([key]) => !ignored.includes(key)));
|
|
20
|
+
const message = request.decisionContext?.lastDirectUserMessage ?? null;
|
|
19
21
|
const canonical = JSON.stringify(sortKeys({
|
|
20
22
|
cwd: request.cwd,
|
|
21
23
|
toolName: request.toolName,
|
|
22
24
|
toolInput: input,
|
|
23
|
-
lastDirectUserMessage:
|
|
25
|
+
lastDirectUserMessage: message?.text ?? null,
|
|
26
|
+
...message?.freshness === "stale" ? { lastDirectUserMessageFreshness: "stale" } : {}
|
|
24
27
|
}));
|
|
25
28
|
return createHash("sha256").update(canonical).digest("hex");
|
|
26
29
|
}
|
|
@@ -136,6 +139,82 @@ async function writeDenialState(path, state) {
|
|
|
136
139
|
await rename(staged, path);
|
|
137
140
|
}
|
|
138
141
|
//#endregion
|
|
142
|
+
//#region src/capture/resolve-capture-dir.ts
|
|
143
|
+
function resolveCaptureDir(capture, host) {
|
|
144
|
+
return capture.dir ?? join(resolveStateDir(host), "captures");
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/capture/find-enclosing-work-tree.ts
|
|
148
|
+
async function findEnclosingWorkTree(path) {
|
|
149
|
+
const real = await resolveExistingPrefix(resolve(path));
|
|
150
|
+
for (let directory = real;; directory = dirname(directory)) {
|
|
151
|
+
if (await hasGitEntry(directory)) return directory;
|
|
152
|
+
if (dirname(directory) === directory) return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function resolveExistingPrefix(path) {
|
|
156
|
+
const missing = [];
|
|
157
|
+
for (let current = path;; current = dirname(current)) {
|
|
158
|
+
const real = await realpath(current).catch(() => null);
|
|
159
|
+
if (real !== null) return join(real, ...missing.toReversed());
|
|
160
|
+
if (dirname(current) === current) return path;
|
|
161
|
+
missing.push(basename(current));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const ABSENT_CODES = /* @__PURE__ */ new Set(["ENOENT", "ENOTDIR"]);
|
|
165
|
+
async function hasGitEntry(directory) {
|
|
166
|
+
try {
|
|
167
|
+
await lstat(join(directory, ".git"));
|
|
168
|
+
return true;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return !(error instanceof Error && "code" in error && ABSENT_CODES.has(String(error.code)));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region src/capture/try-write-request-capture.ts
|
|
175
|
+
async function tryWriteRequestCapture(dir, record) {
|
|
176
|
+
if (!isAbsolute(dir)) return {
|
|
177
|
+
kind: "refused",
|
|
178
|
+
reason: `the capture dir ${dir} is not an absolute path`
|
|
179
|
+
};
|
|
180
|
+
try {
|
|
181
|
+
const before = await findEnclosingWorkTree(dir);
|
|
182
|
+
if (before !== null) return {
|
|
183
|
+
kind: "refused",
|
|
184
|
+
reason: `the capture dir is inside the git work tree ${before}`
|
|
185
|
+
};
|
|
186
|
+
await mkdir(dir, {
|
|
187
|
+
recursive: true,
|
|
188
|
+
mode: 448
|
|
189
|
+
});
|
|
190
|
+
const real = await realpath(dir);
|
|
191
|
+
const after = await findEnclosingWorkTree(real);
|
|
192
|
+
if (after !== null) return {
|
|
193
|
+
kind: "refused",
|
|
194
|
+
reason: `the capture dir is inside the git work tree ${after}`
|
|
195
|
+
};
|
|
196
|
+
if (((await stat(real)).mode & 63) !== 0) await chmod(real, 448);
|
|
197
|
+
await writeOwnerFile(join(real, ".gitignore"), "*\n", constants.O_TRUNC);
|
|
198
|
+
const path = join(real, `requests-${record.time.slice(0, 10)}.jsonl`);
|
|
199
|
+
await writeOwnerFile(path, `${JSON.stringify(record)}\n`, constants.O_APPEND);
|
|
200
|
+
return {
|
|
201
|
+
kind: "written",
|
|
202
|
+
path
|
|
203
|
+
};
|
|
204
|
+
} catch {
|
|
205
|
+
return { kind: "failed" };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function writeOwnerFile(path, text, flag) {
|
|
209
|
+
const handle = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_NOFOLLOW | flag, 384);
|
|
210
|
+
try {
|
|
211
|
+
if (((await handle.stat()).mode & 63) !== 0) await handle.chmod(384);
|
|
212
|
+
await handle.writeFile(text);
|
|
213
|
+
} finally {
|
|
214
|
+
await handle.close();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
//#endregion
|
|
139
218
|
//#region src/config/build-jev-only-config.ts
|
|
140
219
|
function buildJevOnlyConfig(config) {
|
|
141
220
|
if (config.provider.protocol !== "system-one") return null;
|
|
@@ -153,7 +232,7 @@ async function writeActionDiagnostic(payload, entry, host, warnings) {
|
|
|
153
232
|
const path = host.env["AUTO_MODE_DIAGNOSTICS_PATH"] ?? join(resolveStateDir(host), "actions.jsonl");
|
|
154
233
|
if (path === "") return;
|
|
155
234
|
const record = {
|
|
156
|
-
schemaVersion:
|
|
235
|
+
schemaVersion: 4,
|
|
157
236
|
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
158
237
|
invocationID: entry.invocationID,
|
|
159
238
|
sessionHash: toHash(payload.sessionID),
|
|
@@ -163,7 +242,8 @@ async function writeActionDiagnostic(payload, entry, host, warnings) {
|
|
|
163
242
|
decidingStage: entry.decidingStage ?? null,
|
|
164
243
|
denials: entry.denials ?? null,
|
|
165
244
|
escalation: entry.escalation ?? false,
|
|
166
|
-
diagnostics: entry.diagnostics ?? null
|
|
245
|
+
diagnostics: entry.diagnostics ?? null,
|
|
246
|
+
judge: entry.judge ?? null
|
|
167
247
|
};
|
|
168
248
|
try {
|
|
169
249
|
await mkdir(dirname(path), {
|
|
@@ -617,8 +697,20 @@ async function run(io, explain, localOnly, jevOnly, deadline) {
|
|
|
617
697
|
session: plan.state.session
|
|
618
698
|
},
|
|
619
699
|
escalation: plan.escalation,
|
|
620
|
-
...outcome.diagnostics === void 0 ? {} : { diagnostics: outcome.diagnostics }
|
|
700
|
+
...outcome.diagnostics === void 0 ? {} : { diagnostics: outcome.diagnostics },
|
|
701
|
+
...outcome.judge === void 0 ? {} : { judge: outcome.judge }
|
|
621
702
|
}, host, io.stderr);
|
|
703
|
+
if (config.capture?.enabled === true) {
|
|
704
|
+
const captured = await tryWriteRequestCapture(resolveCaptureDir(config.capture, host), {
|
|
705
|
+
schemaVersion: 1,
|
|
706
|
+
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
707
|
+
request: body,
|
|
708
|
+
verdict: plan.verdict,
|
|
709
|
+
decidingStage: plan.escalation ? "budget" : outcome.decidingStage,
|
|
710
|
+
escalation: plan.escalation
|
|
711
|
+
});
|
|
712
|
+
printCaptureNote(io.stderr, captured);
|
|
713
|
+
}
|
|
622
714
|
const note = plan.escalation ? "denial budget exhausted; the user decides this action" : outcome.note;
|
|
623
715
|
return printNote(io.stderr, explain || outcome.unavailable === true, note);
|
|
624
716
|
} finally {
|
|
@@ -642,6 +734,10 @@ async function tryWriteDenialState(stderr, path, state) {
|
|
|
642
734
|
function printVerdict(stdout, verdict) {
|
|
643
735
|
stdout.write(renderVerdict(verdict));
|
|
644
736
|
}
|
|
737
|
+
function printCaptureNote(stderr, captured) {
|
|
738
|
+
if (captured.kind === "refused") stderr.write(`auto-mode: capture skipped: ${captured.reason}\n`);
|
|
739
|
+
if (captured.kind === "failed") stderr.write("auto-mode: capture unavailable\n");
|
|
740
|
+
}
|
|
645
741
|
async function runRecord(io) {
|
|
646
742
|
const raw = await io.stdin();
|
|
647
743
|
let body;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,9 @@ interface UserTask {
|
|
|
3
3
|
readonly text: string;
|
|
4
4
|
readonly origin: 'composer' | 'bridge' | 'sdk';
|
|
5
5
|
}
|
|
6
|
+
interface DirectUserMessage extends UserTask {
|
|
7
|
+
readonly freshness?: 'stale' | undefined;
|
|
8
|
+
}
|
|
6
9
|
interface DecisionContext {
|
|
7
10
|
readonly agentID: string | null;
|
|
8
11
|
readonly originalUserTask: UserTask | null;
|
|
@@ -10,7 +13,7 @@ interface DecisionContext {
|
|
|
10
13
|
readonly text: string;
|
|
11
14
|
readonly origin: 'agent.spawn';
|
|
12
15
|
} | null;
|
|
13
|
-
readonly lastDirectUserMessage:
|
|
16
|
+
readonly lastDirectUserMessage: DirectUserMessage | null;
|
|
14
17
|
readonly omittedTaskContext: readonly {
|
|
15
18
|
readonly field: 'originalUserTask' | 'delegatedTask';
|
|
16
19
|
readonly reason: 'unavailable' | 'budget';
|
|
@@ -205,12 +208,27 @@ interface DenialBudget {
|
|
|
205
208
|
readonly perSession: number;
|
|
206
209
|
}
|
|
207
210
|
//#endregion
|
|
211
|
+
//#region src/judge/types.d.ts
|
|
212
|
+
type OverturnBasis = 'consent' | 'misread';
|
|
213
|
+
type JudgeOverturns = 'consent' | 'any';
|
|
214
|
+
type JudgeStatus = 'confirmed' | 'overturned' | 'held' | 'failed';
|
|
215
|
+
type JudgeFailure = 'credential' | 'policy' | 'request' | 'timeout' | 'unreadable';
|
|
216
|
+
interface JudgeDiagnostics {
|
|
217
|
+
readonly status: JudgeStatus;
|
|
218
|
+
readonly failureReason: JudgeFailure | null;
|
|
219
|
+
readonly overturnBasis: OverturnBasis | null;
|
|
220
|
+
readonly model: string;
|
|
221
|
+
readonly rule: string;
|
|
222
|
+
readonly tier: 'hard' | 'soft';
|
|
223
|
+
readonly elapsedMs: number;
|
|
224
|
+
}
|
|
225
|
+
//#endregion
|
|
208
226
|
//#region src/config/presets.d.ts
|
|
209
227
|
export declare const PRESETS: Readonly<Record<string, ProviderConfig>>;
|
|
210
228
|
//#endregion
|
|
211
229
|
//#region src/config/config.d.ts
|
|
212
230
|
interface ProviderConfig {
|
|
213
|
-
readonly protocol?: 'messages' | 'system-one';
|
|
231
|
+
readonly protocol?: 'messages' | 'system-one' | 'claude-code';
|
|
214
232
|
readonly baseURL: string;
|
|
215
233
|
readonly model: string;
|
|
216
234
|
readonly apiKeyEnv?: string | undefined;
|
|
@@ -225,9 +243,14 @@ interface ScopeSource {
|
|
|
225
243
|
}
|
|
226
244
|
type ScopeSourceKind = (typeof SCOPE_SOURCE_KINDS)[number];
|
|
227
245
|
declare const SCOPE_SOURCE_KINDS: readonly ['cwd', 'session', 'globs', 'atc'];
|
|
246
|
+
interface CaptureConfig {
|
|
247
|
+
readonly enabled: boolean;
|
|
248
|
+
readonly dir?: string | undefined;
|
|
249
|
+
}
|
|
228
250
|
interface Config {
|
|
229
251
|
readonly provider: ProviderConfig;
|
|
230
252
|
readonly judge?: ProviderConfig | null;
|
|
253
|
+
readonly judgeOverturns?: JudgeOverturns;
|
|
231
254
|
readonly scopeSources?: Readonly<Record<string, ScopeSource>>;
|
|
232
255
|
readonly classifierPath?: string | undefined;
|
|
233
256
|
readonly rulesPath?: string | undefined;
|
|
@@ -235,6 +258,7 @@ interface Config {
|
|
|
235
258
|
readonly claudeSettingsPath?: string | null | undefined;
|
|
236
259
|
readonly blockThreshold?: number | undefined;
|
|
237
260
|
readonly denialBudget?: DenialBudget;
|
|
261
|
+
readonly capture?: CaptureConfig;
|
|
238
262
|
readonly warnings?: readonly string[];
|
|
239
263
|
}
|
|
240
264
|
export declare const DEFAULT_CONFIG: Config;
|
|
@@ -243,7 +267,7 @@ export declare function loadConfig(configPath?: string, host?: Readonly<HostEnvi
|
|
|
243
267
|
export declare function resolveApiKey(provider: ProviderConfig, options?: EvaluationOptions): Promise<string | null>;
|
|
244
268
|
//#endregion
|
|
245
269
|
//#region src/classify-action.d.ts
|
|
246
|
-
type DecidingStage = 'local' | 'containment' | 'bypass' | 'jev' | 'messages' | 'retry';
|
|
270
|
+
type DecidingStage = 'local' | 'containment' | 'bypass' | 'jev' | 'judge' | 'messages' | 'retry';
|
|
247
271
|
interface ActionOutcome {
|
|
248
272
|
readonly verdict: Verdict | null;
|
|
249
273
|
readonly decidingStage: DecidingStage;
|
|
@@ -251,6 +275,7 @@ interface ActionOutcome {
|
|
|
251
275
|
readonly status: 'allow' | 'skipped' | 'failure' | DecisionDiagnostics['status'];
|
|
252
276
|
readonly unavailable?: boolean;
|
|
253
277
|
readonly diagnostics?: DecisionDiagnostics;
|
|
278
|
+
readonly judge?: JudgeDiagnostics;
|
|
254
279
|
}
|
|
255
280
|
interface ClassifyOptions extends EvaluationOptions {
|
|
256
281
|
readonly localOnly?: boolean;
|
|
@@ -272,12 +297,27 @@ export declare function checkContainment(request: Readonly<ActionRequest>, scope
|
|
|
272
297
|
//#region src/model/build-decision-request.d.ts
|
|
273
298
|
export declare function buildDecisionRequest(payload: ActionRequest, policy: string, configuredRules: ClaudeRules, lastUserMessage: string | null, rulesSource?: 'shipped' | 'replacement', repositoryContext?: RepositoryContext | null, mcpServers?: readonly MCPServerFact[]): DecisionRequest;
|
|
274
299
|
//#endregion
|
|
300
|
+
//#region src/policy/build-deny-reason.d.ts
|
|
301
|
+
type DenyBasis = 'matched' | 'unresolved';
|
|
302
|
+
//#endregion
|
|
303
|
+
//#region src/model/find-denied-rule.d.ts
|
|
304
|
+
interface DeniedRule {
|
|
305
|
+
readonly rule: DecisionRule;
|
|
306
|
+
readonly basis: DenyBasis;
|
|
307
|
+
}
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region src/judge/classify-with-judge.d.ts
|
|
310
|
+
interface JudgeEvidence extends DeniedRule {
|
|
311
|
+
readonly repositoryContext: RepositoryContext | null;
|
|
312
|
+
}
|
|
313
|
+
//#endregion
|
|
275
314
|
//#region src/model/classify-with-model.d.ts
|
|
276
315
|
interface ModelOutcome {
|
|
277
316
|
readonly verdict: Verdict | null;
|
|
278
317
|
readonly note: string;
|
|
279
318
|
readonly unavailable?: boolean;
|
|
280
319
|
readonly diagnostics?: DecisionDiagnostics;
|
|
320
|
+
readonly review?: JudgeEvidence;
|
|
281
321
|
}
|
|
282
322
|
export declare function classifyWithModel(payload: ActionRequest, config: Config, options?: EvaluationOptions): Promise<ModelOutcome>;
|
|
283
323
|
//#endregion
|
|
@@ -306,7 +346,7 @@ interface PolicyPaths {
|
|
|
306
346
|
readonly classifierPath?: string | undefined;
|
|
307
347
|
readonly rulesPath?: string | undefined;
|
|
308
348
|
}
|
|
309
|
-
export declare function loadPolicy(paths?: PolicyPaths, framework?: 'classifier.md' | 'decision.md'): Promise<string>;
|
|
349
|
+
export declare function loadPolicy(paths?: PolicyPaths, framework?: 'classifier.md' | 'decision.md' | 'judge.md'): Promise<string>;
|
|
310
350
|
//#endregion
|
|
311
351
|
//#region src/request/parse-action-request.d.ts
|
|
312
352
|
export declare function parseActionRequest(body: unknown): ActionRequest | null;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as resolveConfigPath, M as PRESETS, O as loadConfig, S as tryClassifyEdit, T as DEFAULT_CONFIG, _ as checkContainment, c as classifyLocally, d as sendDecision, g as loadPolicy, h as buildDecisionRequest, k as resolveApiKey, l as classifyWithModel, m as DecisionRequestError, n as parseActionRequest, r as classifyAction, s as buildTaskScope, t as renderVerdict, u as parseModelVerdict, x as loadClaudeSettings } from "./render-verdict-
|
|
1
|
+
import { A as resolveConfigPath, M as PRESETS, O as loadConfig, S as tryClassifyEdit, T as DEFAULT_CONFIG, _ as checkContainment, c as classifyLocally, d as sendDecision, g as loadPolicy, h as buildDecisionRequest, k as resolveApiKey, l as classifyWithModel, m as DecisionRequestError, n as parseActionRequest, r as classifyAction, s as buildTaskScope, t as renderVerdict, u as parseModelVerdict, x as loadClaudeSettings } from "./render-verdict-Bx9Lw7gO.js";
|
|
2
2
|
//#region src/config/load-claude-rules.ts
|
|
3
3
|
async function loadClaudeRules(path, host) {
|
|
4
4
|
return (await loadClaudeSettings(path, host)).rules;
|