@agentwares/agentguard 0.1.7 → 0.1.9
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/CHANGELOG.md +75 -0
- package/README.md +223 -16
- package/dist/audit-2YCMHMCP.js +17 -0
- package/dist/audit-2YCMHMCP.js.map +1 -0
- package/dist/chunk-3O6KWT3E.js +345 -0
- package/dist/chunk-3O6KWT3E.js.map +1 -0
- package/dist/chunk-5WRI5ZAA.js +31 -0
- package/dist/chunk-5WRI5ZAA.js.map +1 -0
- package/dist/chunk-6S7LYVOC.js +1033 -0
- package/dist/chunk-6S7LYVOC.js.map +1 -0
- package/dist/chunk-6TUQD666.js +828 -0
- package/dist/chunk-6TUQD666.js.map +1 -0
- package/dist/chunk-BKXXTWIS.js +119 -0
- package/dist/chunk-BKXXTWIS.js.map +1 -0
- package/dist/chunk-D66U3PVB.js +529 -0
- package/dist/chunk-D66U3PVB.js.map +1 -0
- package/dist/chunk-HPYZ3JDX.js +206 -0
- package/dist/chunk-HPYZ3JDX.js.map +1 -0
- package/dist/chunk-KRKZGAJ3.js +769 -0
- package/dist/chunk-KRKZGAJ3.js.map +1 -0
- package/dist/chunk-SUXQTT3X.js +1268 -0
- package/dist/chunk-SUXQTT3X.js.map +1 -0
- package/dist/chunk-TW5KJREH.js +190 -0
- package/dist/chunk-TW5KJREH.js.map +1 -0
- package/dist/chunk-VQWQ5TOM.js +113 -0
- package/dist/chunk-VQWQ5TOM.js.map +1 -0
- package/dist/chunk-X4M7YWQA.js +270 -0
- package/dist/chunk-X4M7YWQA.js.map +1 -0
- package/dist/chunk-ZSLWPKXI.js +1144 -0
- package/dist/chunk-ZSLWPKXI.js.map +1 -0
- package/dist/cli.js +8 -4775
- package/dist/cli.js.map +1 -1
- package/dist/fixtures/crm-server.js +6 -1272
- package/dist/fixtures/crm-server.js.map +1 -1
- package/dist/fixtures/demo-agent.js +4 -115
- package/dist/fixtures/demo-agent.js.map +1 -1
- package/dist/index.d.ts +69 -10
- package/dist/index.js +245 -5138
- package/dist/index.js.map +1 -1
- package/dist/init-CWROGV5I.js +15 -0
- package/dist/init-CWROGV5I.js.map +1 -0
- package/dist/proxy-3Z5YEZ4R.js +154 -0
- package/dist/proxy-3Z5YEZ4R.js.map +1 -0
- package/dist/tools-3LLDG3XQ.js +15 -0
- package/dist/tools-3LLDG3XQ.js.map +1 -0
- package/llms.txt +4 -0
- package/package.json +9 -3
- package/server.json +2 -2
|
@@ -0,0 +1,1033 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HOOKS_POLICY_BLOCK,
|
|
3
|
+
hookTarget,
|
|
4
|
+
hooksCommand,
|
|
5
|
+
installedHandlers,
|
|
6
|
+
renderHooksStarterPolicy
|
|
7
|
+
} from "./chunk-3O6KWT3E.js";
|
|
8
|
+
import {
|
|
9
|
+
flagBool,
|
|
10
|
+
flagList,
|
|
11
|
+
flagString,
|
|
12
|
+
homeFrom,
|
|
13
|
+
parseArgs,
|
|
14
|
+
version
|
|
15
|
+
} from "./chunk-SUXQTT3X.js";
|
|
16
|
+
|
|
17
|
+
// src/commands/audit.ts
|
|
18
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
19
|
+
import { tmpdir } from "os";
|
|
20
|
+
import { join as join4, resolve as resolve3 } from "path";
|
|
21
|
+
import {
|
|
22
|
+
NOT_CHECKABLE_HINT,
|
|
23
|
+
applicableNoun,
|
|
24
|
+
describeStep,
|
|
25
|
+
evaluateChecks,
|
|
26
|
+
extractRules,
|
|
27
|
+
planEnforcement,
|
|
28
|
+
stableHash as stableHash2,
|
|
29
|
+
summarizeAudit
|
|
30
|
+
} from "@agentwares/agentguard-core";
|
|
31
|
+
|
|
32
|
+
// src/audit/cache.ts
|
|
33
|
+
import { createHash } from "crypto";
|
|
34
|
+
import { mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "fs";
|
|
35
|
+
import { join } from "path";
|
|
36
|
+
var CACHE_DIR = "audit-cache-v1";
|
|
37
|
+
var ENGINE = "1";
|
|
38
|
+
var sha = (text) => createHash("sha256").update(text).digest("hex");
|
|
39
|
+
var WHAT = ["command", "path", "call"];
|
|
40
|
+
function encodeBreak(b) {
|
|
41
|
+
return [
|
|
42
|
+
b.at,
|
|
43
|
+
b.session,
|
|
44
|
+
b.step,
|
|
45
|
+
b.file,
|
|
46
|
+
b.line,
|
|
47
|
+
WHAT.indexOf(b.what),
|
|
48
|
+
b.index,
|
|
49
|
+
b.asked === null ? 2 : b.asked ? 1 : 0,
|
|
50
|
+
b.refused ? 1 : 0
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
function decodeBreak(w) {
|
|
54
|
+
return {
|
|
55
|
+
at: w[0],
|
|
56
|
+
session: w[1],
|
|
57
|
+
step: w[2],
|
|
58
|
+
file: w[3],
|
|
59
|
+
line: w[4],
|
|
60
|
+
what: WHAT[w[5]] ?? "call",
|
|
61
|
+
index: w[6],
|
|
62
|
+
asked: w[7] === 2 ? null : w[7] === 1,
|
|
63
|
+
refused: w[8] === 1
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function fileSignature(group2) {
|
|
67
|
+
return group2.files.map((f) => [sha(f.path), f.size, Math.round(f.mtimeMs)]);
|
|
68
|
+
}
|
|
69
|
+
var AuditCache = class {
|
|
70
|
+
constructor(home, repo, enabled = true) {
|
|
71
|
+
this.repo = repo;
|
|
72
|
+
this.enabled = enabled;
|
|
73
|
+
this.dir = join(home, CACHE_DIR);
|
|
74
|
+
if (!enabled) return;
|
|
75
|
+
let names;
|
|
76
|
+
try {
|
|
77
|
+
names = readdirSync(this.dir);
|
|
78
|
+
} catch {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
for (const name of names) {
|
|
82
|
+
if (!name.endsWith(".json")) continue;
|
|
83
|
+
try {
|
|
84
|
+
const w = JSON.parse(readFileSync(join(this.dir, name), "utf8"));
|
|
85
|
+
if (w.v !== 1 || w.engine !== ENGINE || w.repo !== repo) continue;
|
|
86
|
+
this.entries.set(w.group, {
|
|
87
|
+
repo: w.repo,
|
|
88
|
+
group: w.group,
|
|
89
|
+
client: w.client,
|
|
90
|
+
files: w.files,
|
|
91
|
+
audit: {
|
|
92
|
+
sessions: w.sessions.map(([id, client, start, end, known]) => ({
|
|
93
|
+
id,
|
|
94
|
+
client,
|
|
95
|
+
start,
|
|
96
|
+
end,
|
|
97
|
+
turnsKnown: known === 1
|
|
98
|
+
})),
|
|
99
|
+
unread: w.unread,
|
|
100
|
+
checks: Object.fromEntries(
|
|
101
|
+
Object.entries(w.checks).map(([k, c]) => [
|
|
102
|
+
k,
|
|
103
|
+
{ applicable: c.a, breaks: c.b.map(decodeBreak) }
|
|
104
|
+
])
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
} catch {
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
repo;
|
|
113
|
+
enabled;
|
|
114
|
+
dir;
|
|
115
|
+
entries = /* @__PURE__ */ new Map();
|
|
116
|
+
groupKey(group2) {
|
|
117
|
+
return sha(`${this.repo}\0${group2.key}`);
|
|
118
|
+
}
|
|
119
|
+
/** The cached counts for a group, if its files are unchanged and every check is answered. */
|
|
120
|
+
lookup(group2, keys) {
|
|
121
|
+
const hit = this.entries.get(this.groupKey(group2));
|
|
122
|
+
if (!hit) return void 0;
|
|
123
|
+
if (JSON.stringify(hit.files) !== JSON.stringify(fileSignature(group2))) return void 0;
|
|
124
|
+
if (!keys.every((k) => hit.audit.checks[k])) return void 0;
|
|
125
|
+
return hit.audit;
|
|
126
|
+
}
|
|
127
|
+
/** Counts kept from groups whose transcripts are gone (deleted by the client's cleanup). */
|
|
128
|
+
retained(present, client) {
|
|
129
|
+
const out = [];
|
|
130
|
+
for (const e of this.entries.values())
|
|
131
|
+
if (e.client === client && !present.has(e.group)) out.push(e.audit);
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
/** Best effort: a cache that cannot be written only costs speed and history. */
|
|
135
|
+
store(group2, audit) {
|
|
136
|
+
const key = this.groupKey(group2);
|
|
137
|
+
const files = fileSignature(group2);
|
|
138
|
+
const prev = this.entries.get(key);
|
|
139
|
+
const checks = prev && JSON.stringify(prev.files) === JSON.stringify(files) ? { ...prev.audit.checks, ...audit.checks } : audit.checks;
|
|
140
|
+
const merged = { ...audit, checks };
|
|
141
|
+
this.entries.set(key, {
|
|
142
|
+
repo: this.repo,
|
|
143
|
+
group: key,
|
|
144
|
+
client: group2.client,
|
|
145
|
+
files,
|
|
146
|
+
audit: merged
|
|
147
|
+
});
|
|
148
|
+
if (!this.enabled) return;
|
|
149
|
+
const wire = {
|
|
150
|
+
v: 1,
|
|
151
|
+
engine: ENGINE,
|
|
152
|
+
repo: this.repo,
|
|
153
|
+
group: key,
|
|
154
|
+
client: group2.client,
|
|
155
|
+
files,
|
|
156
|
+
sessions: merged.sessions.map((s) => [s.id, s.client, s.start, s.end, s.turnsKnown ? 1 : 0]),
|
|
157
|
+
unread: merged.unread,
|
|
158
|
+
checks: Object.fromEntries(
|
|
159
|
+
Object.entries(merged.checks).map(([k, c]) => [
|
|
160
|
+
k,
|
|
161
|
+
{ a: c.applicable, b: c.breaks.map(encodeBreak) }
|
|
162
|
+
])
|
|
163
|
+
)
|
|
164
|
+
};
|
|
165
|
+
try {
|
|
166
|
+
mkdirSync(this.dir, { recursive: true, mode: 448 });
|
|
167
|
+
const file = join(this.dir, `${key.slice(0, 32)}.json`);
|
|
168
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
169
|
+
writeFileSync(tmp, JSON.stringify(wire), { mode: 384 });
|
|
170
|
+
renameSync(tmp, file);
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// src/audit/enforce.ts
|
|
177
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
178
|
+
import YAML, { isMap, isScalar, isSeq } from "yaml";
|
|
179
|
+
var origin = (rule) => ` R${rule.n} ${rule.file}:${rule.line}`;
|
|
180
|
+
function seqAt(doc, path) {
|
|
181
|
+
let node = doc.getIn(path, true);
|
|
182
|
+
if (!isSeq(node)) {
|
|
183
|
+
doc.setIn(path, doc.createNode([]));
|
|
184
|
+
node = doc.getIn(path, true);
|
|
185
|
+
}
|
|
186
|
+
const seq = node;
|
|
187
|
+
seq.flow = false;
|
|
188
|
+
return seq;
|
|
189
|
+
}
|
|
190
|
+
var values = (seq) => seq.items.map((n) => isScalar(n) ? String(n.value) : "").map((v) => v.toLowerCase());
|
|
191
|
+
function applyEnforcePlan(policyPath, plan, now = /* @__PURE__ */ new Date()) {
|
|
192
|
+
const created = !existsSync(policyPath);
|
|
193
|
+
const before = created ? "" : readFileSync2(policyPath, "utf8");
|
|
194
|
+
let text = created ? renderHooksStarterPolicy(now) : before;
|
|
195
|
+
if (!/^hooks\s*:/m.test(text)) text = `${text.replace(/\n*$/, "\n")}${HOOKS_POLICY_BLOCK}`;
|
|
196
|
+
const doc = YAML.parseDocument(text);
|
|
197
|
+
if (doc.errors.length > 0)
|
|
198
|
+
throw new Error(`${policyPath} is not valid YAML: ${doc.errors[0].message}`);
|
|
199
|
+
const added = [];
|
|
200
|
+
for (const item of plan.items) {
|
|
201
|
+
const seq = seqAt(doc, item.list.split("."));
|
|
202
|
+
if (values(seq).includes(item.pattern.toLowerCase())) continue;
|
|
203
|
+
const node = doc.createNode(item.pattern);
|
|
204
|
+
node.comment = origin(item.rule);
|
|
205
|
+
seq.items.push(node);
|
|
206
|
+
added.push(`${item.list}: ${item.pattern} (R${item.rule.n}, ${item.check.label})`);
|
|
207
|
+
}
|
|
208
|
+
if (plan.builtins.length > 0) {
|
|
209
|
+
const path = ["hooks", "irreversible", "builtins"];
|
|
210
|
+
const node = doc.getIn(path, true);
|
|
211
|
+
const enabled = isScalar(node) ? node.value : void 0;
|
|
212
|
+
if (enabled === false || isSeq(node)) {
|
|
213
|
+
const seq = seqAt(doc, path);
|
|
214
|
+
for (const b of plan.builtins) {
|
|
215
|
+
if (values(seq).includes(b.name)) continue;
|
|
216
|
+
const scalar = doc.createNode(b.name);
|
|
217
|
+
scalar.comment = origin(b.rule);
|
|
218
|
+
seq.items.push(scalar);
|
|
219
|
+
added.push(`hooks.irreversible.builtins: ${b.name} (R${b.rule.n}, ${b.check.label})`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (plan.irreversible.length > 0) {
|
|
224
|
+
const seq = seqAt(doc, ["hooks", "irreversible", "rules"]);
|
|
225
|
+
const names = new Set(
|
|
226
|
+
seq.items.map((n) => isMap(n) ? String(n.get("name") ?? "") : "").filter(Boolean)
|
|
227
|
+
);
|
|
228
|
+
for (const r of plan.irreversible) {
|
|
229
|
+
if (names.has(r.name)) continue;
|
|
230
|
+
const node = doc.createNode({ name: r.name, shell: r.shell, mentions: r.mentions });
|
|
231
|
+
if (isMap(node)) node.commentBefore = origin(r.rule);
|
|
232
|
+
seq.items.push(node);
|
|
233
|
+
added.push(
|
|
234
|
+
`hooks.irreversible.rules: ${r.name} (${r.shell.length} commands) (R${r.rule.n}, ${r.check.label})`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const mode = doc.getIn(["hooks", "mode"]) === "enforce" ? "enforce" : "dry-run";
|
|
239
|
+
return { before, after: doc.toString({ lineWidth: 0 }), created, added, mode };
|
|
240
|
+
}
|
|
241
|
+
function lineDiff(a, b, context = 2) {
|
|
242
|
+
const x = a === "" ? [] : a.replace(/\n$/, "").split("\n");
|
|
243
|
+
const y = b.replace(/\n$/, "").split("\n");
|
|
244
|
+
const n = x.length;
|
|
245
|
+
const m = y.length;
|
|
246
|
+
const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
247
|
+
for (let i2 = n - 1; i2 >= 0; i2 -= 1)
|
|
248
|
+
for (let j2 = m - 1; j2 >= 0; j2 -= 1)
|
|
249
|
+
lcs[i2][j2] = x[i2] === y[j2] ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
|
|
250
|
+
const ops = [];
|
|
251
|
+
let i = 0;
|
|
252
|
+
let j = 0;
|
|
253
|
+
while (i < n || j < m) {
|
|
254
|
+
if (i < n && j < m && x[i] === y[j]) {
|
|
255
|
+
ops.push({ op: " ", line: x[i] });
|
|
256
|
+
i += 1;
|
|
257
|
+
j += 1;
|
|
258
|
+
} else if (j < m && (i >= n || lcs[i][j + 1] >= lcs[i + 1][j])) {
|
|
259
|
+
ops.push({ op: "+", line: y[j] });
|
|
260
|
+
j += 1;
|
|
261
|
+
} else {
|
|
262
|
+
ops.push({ op: "-", line: x[i] });
|
|
263
|
+
i += 1;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const keep = /* @__PURE__ */ new Set();
|
|
267
|
+
ops.forEach((o, k) => {
|
|
268
|
+
if (o.op === " ") return;
|
|
269
|
+
for (let d = -context; d <= context; d += 1) keep.add(k + d);
|
|
270
|
+
});
|
|
271
|
+
const out = [];
|
|
272
|
+
let last = -2;
|
|
273
|
+
ops.forEach((o, k) => {
|
|
274
|
+
if (!keep.has(k)) return;
|
|
275
|
+
if (k !== last + 1 && out.length > 0) out.push(" \u2026");
|
|
276
|
+
out.push(`${o.op} ${o.line}`);
|
|
277
|
+
last = k;
|
|
278
|
+
});
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/audit/read.ts
|
|
283
|
+
import { createReadStream } from "fs";
|
|
284
|
+
import { createInterface } from "readline";
|
|
285
|
+
import {
|
|
286
|
+
ClaudeCodeAuditReader,
|
|
287
|
+
CodexAuditReader,
|
|
288
|
+
stableHash
|
|
289
|
+
} from "@agentwares/agentguard-core";
|
|
290
|
+
var CLAUDE_WANTED = (raw) => raw.includes('"type":"assistant"') || raw.includes('"type":"user"');
|
|
291
|
+
var CODEX_WANTED = (raw) => raw.includes('"session_meta"') || raw.includes('"turn_context"') || raw.includes('"function_call"') || raw.includes('"local_shell_call"') || raw.includes('"custom_tool_call"');
|
|
292
|
+
async function eachLine(path, wanted, onLine) {
|
|
293
|
+
const stream = createReadStream(path, { encoding: "utf8", highWaterMark: 1 << 20 });
|
|
294
|
+
const lines = createInterface({ input: stream, crlfDelay: Infinity });
|
|
295
|
+
let n = 0;
|
|
296
|
+
try {
|
|
297
|
+
for await (const raw of lines) {
|
|
298
|
+
n += 1;
|
|
299
|
+
if (raw.length === 0 || !wanted(raw, n)) continue;
|
|
300
|
+
let value;
|
|
301
|
+
try {
|
|
302
|
+
value = JSON.parse(raw);
|
|
303
|
+
} catch {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (onLine(value, n) === false) break;
|
|
307
|
+
}
|
|
308
|
+
} finally {
|
|
309
|
+
lines.close();
|
|
310
|
+
stream.destroy();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async function readGroup(group2, opts) {
|
|
314
|
+
if (group2.client === "codex") {
|
|
315
|
+
const reader2 = new CodexAuditReader(opts);
|
|
316
|
+
await eachLine(group2.files[0].path, CODEX_WANTED, (value, line) => {
|
|
317
|
+
reader2.add(value, line, 0);
|
|
318
|
+
return reader2.relevant;
|
|
319
|
+
});
|
|
320
|
+
return reader2.result();
|
|
321
|
+
}
|
|
322
|
+
const reader = new ClaudeCodeAuditReader(opts);
|
|
323
|
+
for (const [i, file] of group2.files.entries())
|
|
324
|
+
await eachLine(file.path, CLAUDE_WANTED, (value, line) => reader.add(value, line, i));
|
|
325
|
+
return reader.result();
|
|
326
|
+
}
|
|
327
|
+
async function fetchSteps(group2, wanted, opts) {
|
|
328
|
+
const out = /* @__PURE__ */ new Map();
|
|
329
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
330
|
+
for (const w of wanted) {
|
|
331
|
+
const set = byFile.get(w.file) ?? /* @__PURE__ */ new Set();
|
|
332
|
+
set.add(w.line);
|
|
333
|
+
byFile.set(w.file, set);
|
|
334
|
+
}
|
|
335
|
+
for (const [fileIndex, lines] of byFile) {
|
|
336
|
+
const file = group2.files[fileIndex];
|
|
337
|
+
if (!file) continue;
|
|
338
|
+
const last = Math.max(...lines);
|
|
339
|
+
const reader = group2.client === "codex" ? new CodexAuditReader(opts) : new ClaudeCodeAuditReader(opts);
|
|
340
|
+
await eachLine(
|
|
341
|
+
file.path,
|
|
342
|
+
(raw, n) => lines.has(n) || group2.client === "codex" && (raw.includes('"session_meta"') || raw.includes('"turn_context"')),
|
|
343
|
+
(value, n) => {
|
|
344
|
+
reader.add(value, n, fileIndex);
|
|
345
|
+
return n < last;
|
|
346
|
+
}
|
|
347
|
+
);
|
|
348
|
+
for (const step of reader.result().steps) out.set(stableHash(step.id), step);
|
|
349
|
+
}
|
|
350
|
+
return out;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/audit/repo.ts
|
|
354
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
|
|
355
|
+
import { dirname, isAbsolute, join as join3, relative, resolve as resolve2, sep } from "path";
|
|
356
|
+
import YAML2 from "yaml";
|
|
357
|
+
import {
|
|
358
|
+
AuditRulesSchema,
|
|
359
|
+
isWithin,
|
|
360
|
+
normalizePath
|
|
361
|
+
} from "@agentwares/agentguard-core";
|
|
362
|
+
|
|
363
|
+
// src/audit/sources.ts
|
|
364
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, realpathSync, statSync } from "fs";
|
|
365
|
+
import { basename, join as join2, resolve } from "path";
|
|
366
|
+
function sourceDirs(env, home, overrides = {}) {
|
|
367
|
+
const claude = overrides.claudeDir ? [resolve(overrides.claudeDir)] : env.CLAUDE_CONFIG_DIR ? env.CLAUDE_CONFIG_DIR.split(",").map((d) => d.trim()).filter(Boolean).map((d) => join2(resolve(d), "projects")) : [join2(home, ".claude", "projects"), join2(home, ".config", "claude", "projects")];
|
|
368
|
+
const codexHome = env.CODEX_HOME ? resolve(env.CODEX_HOME) : join2(home, ".codex");
|
|
369
|
+
const codex = overrides.codexDir ? [resolve(overrides.codexDir)] : [join2(codexHome, "sessions"), join2(codexHome, "archived_sessions")];
|
|
370
|
+
return { "claude-code": claude, codex };
|
|
371
|
+
}
|
|
372
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["tool-results", "memory", "node_modules", ".git"]);
|
|
373
|
+
function stat(path) {
|
|
374
|
+
try {
|
|
375
|
+
const s = statSync(path);
|
|
376
|
+
return s.isFile() ? { path, size: s.size, mtimeMs: s.mtimeMs } : void 0;
|
|
377
|
+
} catch {
|
|
378
|
+
return void 0;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function jsonlUnder(dir, out, depth = 0) {
|
|
382
|
+
if (depth > 6) return;
|
|
383
|
+
let entries;
|
|
384
|
+
try {
|
|
385
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
386
|
+
} catch {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
for (const e of entries) {
|
|
390
|
+
const p = join2(dir, e.name);
|
|
391
|
+
if (e.isDirectory()) {
|
|
392
|
+
if (!SKIP_DIRS.has(e.name)) jsonlUnder(p, out, depth + 1);
|
|
393
|
+
} else if (e.isFile() && e.name.endsWith(".jsonl")) {
|
|
394
|
+
const f = stat(p);
|
|
395
|
+
if (f) out.push(f);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function claudeProjectName(dir) {
|
|
400
|
+
return dir.replace(/[^a-zA-Z0-9]/g, "-");
|
|
401
|
+
}
|
|
402
|
+
function pathForms(dir) {
|
|
403
|
+
const forms = /* @__PURE__ */ new Set([resolve(dir)]);
|
|
404
|
+
try {
|
|
405
|
+
forms.add(realpathSync(dir));
|
|
406
|
+
} catch {
|
|
407
|
+
}
|
|
408
|
+
return [...forms];
|
|
409
|
+
}
|
|
410
|
+
function claudeProjectDirs(roots, repoRoot) {
|
|
411
|
+
const names = pathForms(repoRoot).map(claudeProjectName);
|
|
412
|
+
const out = [];
|
|
413
|
+
for (const root of roots) {
|
|
414
|
+
let entries;
|
|
415
|
+
try {
|
|
416
|
+
entries = readdirSync2(root, { withFileTypes: true });
|
|
417
|
+
} catch {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
for (const e of entries) {
|
|
421
|
+
if (!e.isDirectory()) continue;
|
|
422
|
+
if (names.some((n) => e.name === n || e.name.startsWith(`${n}-`)))
|
|
423
|
+
out.push(join2(root, e.name));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return out;
|
|
427
|
+
}
|
|
428
|
+
function claudeGroups(projectDirs) {
|
|
429
|
+
const groups = [];
|
|
430
|
+
for (const dir of projectDirs) {
|
|
431
|
+
let entries;
|
|
432
|
+
try {
|
|
433
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
434
|
+
} catch {
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
const mains = /* @__PURE__ */ new Map();
|
|
438
|
+
const subdirs = [];
|
|
439
|
+
for (const e of entries) {
|
|
440
|
+
const p = join2(dir, e.name);
|
|
441
|
+
if (e.isFile() && e.name.endsWith(".jsonl")) {
|
|
442
|
+
const f = stat(p);
|
|
443
|
+
if (f) mains.set(basename(e.name, ".jsonl"), f);
|
|
444
|
+
} else if (e.isDirectory() && !SKIP_DIRS.has(e.name)) subdirs.push(e.name);
|
|
445
|
+
}
|
|
446
|
+
for (const [id, main] of mains) {
|
|
447
|
+
const files = [main];
|
|
448
|
+
if (subdirs.includes(id)) jsonlUnder(join2(dir, id), files);
|
|
449
|
+
groups.push(group("claude-code", main.path, files));
|
|
450
|
+
}
|
|
451
|
+
for (const id of subdirs) {
|
|
452
|
+
if (mains.has(id)) continue;
|
|
453
|
+
const files = [];
|
|
454
|
+
jsonlUnder(join2(dir, id), files);
|
|
455
|
+
if (files.length > 0) groups.push(group("claude-code", join2(dir, id), files));
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return groups;
|
|
459
|
+
}
|
|
460
|
+
function codexGroups(roots) {
|
|
461
|
+
const files = [];
|
|
462
|
+
for (const root of roots) if (existsSync2(root)) jsonlUnder(root, files);
|
|
463
|
+
return files.map((f) => group("codex", f.path, [f]));
|
|
464
|
+
}
|
|
465
|
+
function group(client, key, files) {
|
|
466
|
+
files.sort((a, b) => a.path === key ? -1 : b.path === key ? 1 : a.path.localeCompare(b.path));
|
|
467
|
+
return { client, key, files, lastWrite: Math.max(...files.map((f) => f.mtimeMs)) };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// src/audit/repo.ts
|
|
471
|
+
function findRepo(cwd) {
|
|
472
|
+
let dir = resolve2(cwd);
|
|
473
|
+
for (let i = 0; i < 64; i += 1) {
|
|
474
|
+
const dotgit = join3(dir, ".git");
|
|
475
|
+
if (existsSync3(dotgit)) {
|
|
476
|
+
let root = dir;
|
|
477
|
+
try {
|
|
478
|
+
if (statSync2(dotgit).isFile()) {
|
|
479
|
+
const m = /^gitdir:\s*(.+)$/m.exec(readFileSync3(dotgit, "utf8"));
|
|
480
|
+
const gitdir = m ? resolve2(dir, m[1].trim()) : "";
|
|
481
|
+
const at = gitdir.lastIndexOf(`${sep}.git${sep}worktrees${sep}`);
|
|
482
|
+
if (at > 0) root = gitdir.slice(0, at);
|
|
483
|
+
}
|
|
484
|
+
} catch {
|
|
485
|
+
}
|
|
486
|
+
return { checkout: dir, root };
|
|
487
|
+
}
|
|
488
|
+
const parent = dirname(dir);
|
|
489
|
+
if (parent === dir) break;
|
|
490
|
+
dir = parent;
|
|
491
|
+
}
|
|
492
|
+
return { checkout: resolve2(cwd), root: resolve2(cwd) };
|
|
493
|
+
}
|
|
494
|
+
function repoMatcher(root) {
|
|
495
|
+
const forms = pathForms(root).map(normalizePath);
|
|
496
|
+
return (cwd) => forms.some((f) => isWithin(normalizePath(cwd), f));
|
|
497
|
+
}
|
|
498
|
+
var RULE_FILES = [
|
|
499
|
+
"CLAUDE.md",
|
|
500
|
+
"CLAUDE.local.md",
|
|
501
|
+
".claude/CLAUDE.md",
|
|
502
|
+
"AGENTS.md",
|
|
503
|
+
"GEMINI.md",
|
|
504
|
+
".cursorrules",
|
|
505
|
+
".github/copilot-instructions.md",
|
|
506
|
+
".windsurfrules",
|
|
507
|
+
".clinerules"
|
|
508
|
+
];
|
|
509
|
+
var MAX_CURSOR_RULES = 20;
|
|
510
|
+
var MAX_BYTES = 512 * 1024;
|
|
511
|
+
function cursorRules(dir, out, depth = 0) {
|
|
512
|
+
if (depth > 3 || out.length >= MAX_CURSOR_RULES) return;
|
|
513
|
+
let entries;
|
|
514
|
+
try {
|
|
515
|
+
entries = readdirSync3(dir, { withFileTypes: true });
|
|
516
|
+
} catch {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
520
|
+
const p = join3(dir, e.name);
|
|
521
|
+
if (e.isDirectory()) cursorRules(p, out, depth + 1);
|
|
522
|
+
else if (e.isFile() && /\.(mdc|md)$/.test(e.name) && out.length < MAX_CURSOR_RULES) out.push(p);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function readRuleFiles(checkout, extra = []) {
|
|
526
|
+
const paths = RULE_FILES.map((f) => join3(checkout, f));
|
|
527
|
+
cursorRules(join3(checkout, ".cursor", "rules"), paths);
|
|
528
|
+
for (const e of extra) paths.push(isAbsolute(e) ? e : resolve2(checkout, e));
|
|
529
|
+
const out = [];
|
|
530
|
+
const seen = /* @__PURE__ */ new Set();
|
|
531
|
+
for (const p of paths) {
|
|
532
|
+
if (seen.has(p)) continue;
|
|
533
|
+
seen.add(p);
|
|
534
|
+
try {
|
|
535
|
+
if (!statSync2(p).isFile()) continue;
|
|
536
|
+
const text = readFileSync3(p, "utf8").slice(0, MAX_BYTES);
|
|
537
|
+
const rel = relative(checkout, p);
|
|
538
|
+
out.push({ path: rel.startsWith("..") ? p : rel.split(sep).join("/"), text });
|
|
539
|
+
} catch {
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return out;
|
|
543
|
+
}
|
|
544
|
+
function readConfigRules(policyPath) {
|
|
545
|
+
if (!existsSync3(policyPath)) return { rules: [] };
|
|
546
|
+
let raw;
|
|
547
|
+
try {
|
|
548
|
+
raw = YAML2.parse(readFileSync3(policyPath, "utf8"));
|
|
549
|
+
} catch (err) {
|
|
550
|
+
return {
|
|
551
|
+
rules: [],
|
|
552
|
+
warning: `${policyPath} is not valid YAML (${err instanceof Error ? err.message : String(err)}); its audit.rules were skipped`
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
const audit = raw && typeof raw === "object" ? raw.audit : void 0;
|
|
556
|
+
const rules = audit && typeof audit === "object" ? audit.rules : void 0;
|
|
557
|
+
if (rules === void 0) return { rules: [] };
|
|
558
|
+
const parsed = AuditRulesSchema.safeParse(rules);
|
|
559
|
+
if (!parsed.success)
|
|
560
|
+
return {
|
|
561
|
+
rules: [],
|
|
562
|
+
warning: `audit.rules in ${policyPath} is invalid (${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}); skipped`
|
|
563
|
+
};
|
|
564
|
+
return { rules: parsed.data };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// src/commands/audit.ts
|
|
568
|
+
var TEAM_HISTORY_URL = "https://agentwares-agentguard.vercel.app/team-history";
|
|
569
|
+
var TEAM_HISTORY_LINE = `Team adherence history across repos and developers is coming \u2014 ${TEAM_HISTORY_URL}`;
|
|
570
|
+
var CLIENT_LABEL = { "claude-code": "Claude Code", codex: "Codex" };
|
|
571
|
+
function parseSince(value, now) {
|
|
572
|
+
const v = (value ?? "7d").trim().toLowerCase();
|
|
573
|
+
if (v === "all") return void 0;
|
|
574
|
+
const rel = /^(\d+)\s*([dhw])$/.exec(v);
|
|
575
|
+
if (rel) {
|
|
576
|
+
const n = Number(rel[1]);
|
|
577
|
+
const unit = rel[2] === "h" ? 36e5 : rel[2] === "w" ? 7 * 864e5 : 864e5;
|
|
578
|
+
return now.getTime() - n * unit;
|
|
579
|
+
}
|
|
580
|
+
const t = Date.parse(/^\d{4}-\d{2}-\d{2}$/.test(v) ? `${v}T00:00:00` : v);
|
|
581
|
+
if (!Number.isFinite(t))
|
|
582
|
+
throw new Error(`--since "${value}": use 7d, 30d, 24h, all or a date (YYYY-MM-DD)`);
|
|
583
|
+
return t;
|
|
584
|
+
}
|
|
585
|
+
function clientsFrom(args) {
|
|
586
|
+
const names = flagList(args, "client").map((n) => n.trim().toLowerCase());
|
|
587
|
+
if (names.length === 0 || names.includes("all")) return ["claude-code", "codex"];
|
|
588
|
+
return names.map((n) => {
|
|
589
|
+
if (n === "claude" || n === "claude-code") return "claude-code";
|
|
590
|
+
if (n === "codex") return "codex";
|
|
591
|
+
throw new Error(`unknown --client "${n}" (claude, codex or all)`);
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
var pad = (n) => String(n).padStart(2, "0");
|
|
595
|
+
var localTime = (ms) => {
|
|
596
|
+
const d = new Date(ms);
|
|
597
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
598
|
+
};
|
|
599
|
+
var localDay = (ms) => localTime(ms).slice(0, 10);
|
|
600
|
+
var plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
601
|
+
var clip = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
|
|
602
|
+
async function pool(items, size, work) {
|
|
603
|
+
let next = 0;
|
|
604
|
+
const run = async () => {
|
|
605
|
+
while (next < items.length) await work(items[next++]);
|
|
606
|
+
};
|
|
607
|
+
await Promise.all(Array.from({ length: Math.min(size, items.length) }, run));
|
|
608
|
+
}
|
|
609
|
+
function enforceTarget(check) {
|
|
610
|
+
switch (check.spec.kind) {
|
|
611
|
+
case "run-before":
|
|
612
|
+
case "no-commit-to":
|
|
613
|
+
return "audit-only";
|
|
614
|
+
case "no-edit":
|
|
615
|
+
return "hooks.paths.deny";
|
|
616
|
+
case "no-new-deps":
|
|
617
|
+
return check.spec.unlessAsked ? "hooks.shell.approval" : "hooks.shell.deny";
|
|
618
|
+
case "no-rm-outside":
|
|
619
|
+
case "no-publish":
|
|
620
|
+
return "hooks.irreversible.builtins";
|
|
621
|
+
case "no-deploy":
|
|
622
|
+
return "hooks.irreversible.rules";
|
|
623
|
+
default:
|
|
624
|
+
return "hooks.shell.deny";
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
async function auditCommand(args, io) {
|
|
628
|
+
const now = /* @__PURE__ */ new Date();
|
|
629
|
+
const json = flagBool(args, "json") === true;
|
|
630
|
+
const since = parseSince(flagString(args, "since"), now);
|
|
631
|
+
const clients = clientsFrom(args);
|
|
632
|
+
const home = homeFrom(io);
|
|
633
|
+
const repo = findRepo(resolve3(io.cwd, flagString(args, "repo") ?? "."));
|
|
634
|
+
const policyPath = resolve3(
|
|
635
|
+
repo.checkout,
|
|
636
|
+
flagString(args, "config") ?? io.env.AGENTGUARD_CONFIG ?? "agentguard.yaml"
|
|
637
|
+
);
|
|
638
|
+
const files = readRuleFiles(repo.checkout, flagList(args, "rules"));
|
|
639
|
+
const config = readConfigRules(policyPath);
|
|
640
|
+
const { rules, errors } = extractRules(files, config.rules, "agentguard.yaml");
|
|
641
|
+
const warnings = [
|
|
642
|
+
...config.warning ? [config.warning] : [],
|
|
643
|
+
...errors.map((e) => `${e.file}:${e.line}: ${e.message}`)
|
|
644
|
+
];
|
|
645
|
+
const checks = rules.flatMap((r) => r.checks);
|
|
646
|
+
const keys = [...new Set(checks.map((c) => c.key))];
|
|
647
|
+
if (flagBool(args, "enforce")) return enforce(args, io, rules, policyPath, repo.checkout, json);
|
|
648
|
+
const dirs = sourceDirs(io.env, home, {
|
|
649
|
+
claudeDir: flagString(args, "claude-dir"),
|
|
650
|
+
codexDir: flagString(args, "codex-dir")
|
|
651
|
+
});
|
|
652
|
+
const groups = [
|
|
653
|
+
...clients.includes("claude-code") ? claudeGroups(claudeProjectDirs(dirs["claude-code"], repo.root)) : [],
|
|
654
|
+
...clients.includes("codex") ? codexGroups(dirs.codex) : []
|
|
655
|
+
];
|
|
656
|
+
const cache = new AuditCache(
|
|
657
|
+
resolve3(io.cwd, flagString(args, "cache-dir") ?? join4(home, ".agentguard")),
|
|
658
|
+
sha(repo.root),
|
|
659
|
+
flagBool(args, "cache") !== false
|
|
660
|
+
);
|
|
661
|
+
const inRepo = repoMatcher(repo.root);
|
|
662
|
+
const ctx = {
|
|
663
|
+
repoRoot: repo.root,
|
|
664
|
+
home,
|
|
665
|
+
tmpDirs: [
|
|
666
|
+
tmpdir(),
|
|
667
|
+
io.env.TMPDIR ?? "",
|
|
668
|
+
"/tmp",
|
|
669
|
+
"/private/tmp",
|
|
670
|
+
"/var/folders",
|
|
671
|
+
"/private/var/folders"
|
|
672
|
+
].filter(Boolean)
|
|
673
|
+
};
|
|
674
|
+
const counts = {
|
|
675
|
+
"claude-code": { transcripts: 0, read: 0, cached: 0, retained: 0, older: 0 },
|
|
676
|
+
codex: { transcripts: 0, read: 0, cached: 0, retained: 0, older: 0 }
|
|
677
|
+
};
|
|
678
|
+
const entries = [];
|
|
679
|
+
const evidence = !json && flagBool(args, "evidence") !== false;
|
|
680
|
+
await pool(groups, 6, async (group2) => {
|
|
681
|
+
const c = counts[group2.client];
|
|
682
|
+
c.transcripts += group2.files.length;
|
|
683
|
+
const hit = cache.lookup(group2, keys);
|
|
684
|
+
if (hit) {
|
|
685
|
+
c.cached += group2.files.length;
|
|
686
|
+
entries.push({ group: group2, audit: hit });
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (since !== void 0 && group2.lastWrite < since) {
|
|
690
|
+
c.older += group2.files.length;
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
let read;
|
|
694
|
+
try {
|
|
695
|
+
read = await readGroup(group2, { inRepo });
|
|
696
|
+
} catch {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
c.read += group2.files.length;
|
|
700
|
+
const audit = evaluateChecks(checks, read, ctx);
|
|
701
|
+
cache.store(group2, audit);
|
|
702
|
+
const hasBreaks = Object.values(audit.checks).some((o) => o.breaks.length > 0);
|
|
703
|
+
entries.push({
|
|
704
|
+
group: group2,
|
|
705
|
+
audit,
|
|
706
|
+
...evidence && hasBreaks ? { steps: new Map(read.steps.map((s) => [stableHash2(s.id), s])) } : {}
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
const present = new Set(groups.map((g) => cache.groupKey(g)));
|
|
710
|
+
for (const client of clients)
|
|
711
|
+
for (const audit of cache.retained(present, client)) {
|
|
712
|
+
counts[client].retained += 1;
|
|
713
|
+
entries.push({ audit });
|
|
714
|
+
}
|
|
715
|
+
const summary = summarizeAudit(
|
|
716
|
+
rules,
|
|
717
|
+
entries.map((e) => e.audit),
|
|
718
|
+
{ since }
|
|
719
|
+
);
|
|
720
|
+
const plan = planEnforcement(rules);
|
|
721
|
+
const enforceable = /* @__PURE__ */ new Set([
|
|
722
|
+
...plan.items.map((i) => i.rule.id),
|
|
723
|
+
...plan.builtins.map((b) => b.rule.id),
|
|
724
|
+
...plan.irreversible.map((r) => r.rule.id)
|
|
725
|
+
]);
|
|
726
|
+
if (json) {
|
|
727
|
+
io.out(
|
|
728
|
+
JSON.stringify(
|
|
729
|
+
{
|
|
730
|
+
tool: "agentguard audit",
|
|
731
|
+
version: version(),
|
|
732
|
+
repo: repo.root,
|
|
733
|
+
checkout: repo.checkout,
|
|
734
|
+
since: since === void 0 ? null : new Date(since).toISOString(),
|
|
735
|
+
until: now.toISOString(),
|
|
736
|
+
rule_files: files.map((f) => f.path),
|
|
737
|
+
sources: counts,
|
|
738
|
+
sessions: {
|
|
739
|
+
total: summary.sessions,
|
|
740
|
+
"claude-code": summary.sessionsByClient["claude-code"],
|
|
741
|
+
codex: summary.sessionsByClient.codex
|
|
742
|
+
},
|
|
743
|
+
unread_tool_calls: summary.unread,
|
|
744
|
+
rules: summary.rules.map((r) => ({
|
|
745
|
+
id: r.rule.id,
|
|
746
|
+
n: r.rule.n,
|
|
747
|
+
file: r.rule.file,
|
|
748
|
+
line: r.rule.line,
|
|
749
|
+
text: r.rule.text,
|
|
750
|
+
status: r.status,
|
|
751
|
+
checkable: r.rule.checks.length > 0,
|
|
752
|
+
...r.rule.silenced ? { marked_not_checkable: true } : {},
|
|
753
|
+
sessions_broken: r.sessionsBroken,
|
|
754
|
+
last_broken: r.last.map((b) => new Date(b.at).toISOString()),
|
|
755
|
+
checks: r.checks.map((c, k) => ({
|
|
756
|
+
check: c.label,
|
|
757
|
+
kind: c.kind,
|
|
758
|
+
via: c.via,
|
|
759
|
+
enforce: enforceTarget(r.rule.checks[k]),
|
|
760
|
+
sessions_applicable: c.sessionsApplicable,
|
|
761
|
+
sessions_broken: c.sessionsBroken,
|
|
762
|
+
sessions_on_request: c.sessionsOnRequest,
|
|
763
|
+
sessions_undetermined: c.sessionsUndetermined,
|
|
764
|
+
breaks: c.breaks,
|
|
765
|
+
refused: c.refused
|
|
766
|
+
}))
|
|
767
|
+
})),
|
|
768
|
+
not_checkable: {
|
|
769
|
+
count: summary.rules.filter((r) => r.status === "not-checkable").length,
|
|
770
|
+
hint: NOT_CHECKABLE_HINT
|
|
771
|
+
},
|
|
772
|
+
warnings,
|
|
773
|
+
enforce: {
|
|
774
|
+
command: "agentguard audit --enforce",
|
|
775
|
+
rules: enforceable.size,
|
|
776
|
+
note: "writes hook-mode rules into agentguard.yaml in dry-run; nothing is blocked until hooks.mode is enforce"
|
|
777
|
+
},
|
|
778
|
+
team_history: { available: false, url: TEAM_HISTORY_URL },
|
|
779
|
+
privacy: "counts, rule ids and dates only: no command, path or prompt from a transcript is in this output, and nothing was sent anywhere"
|
|
780
|
+
},
|
|
781
|
+
null,
|
|
782
|
+
2
|
|
783
|
+
)
|
|
784
|
+
);
|
|
785
|
+
return 0;
|
|
786
|
+
}
|
|
787
|
+
const lines = /* @__PURE__ */ new Map();
|
|
788
|
+
if (evidence) {
|
|
789
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
790
|
+
for (const r of summary.rules)
|
|
791
|
+
for (const b of r.last) {
|
|
792
|
+
const list = wanted.get(b.group) ?? [];
|
|
793
|
+
list.push(b);
|
|
794
|
+
wanted.set(b.group, list);
|
|
795
|
+
}
|
|
796
|
+
for (const [gi, breaks] of wanted) {
|
|
797
|
+
const entry = entries[gi];
|
|
798
|
+
if (!entry?.group) continue;
|
|
799
|
+
let steps = entry.steps;
|
|
800
|
+
if (!steps) {
|
|
801
|
+
try {
|
|
802
|
+
steps = await fetchSteps(entry.group, breaks, { inRepo });
|
|
803
|
+
} catch {
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
for (const b of breaks) {
|
|
808
|
+
const step = steps.get(b.step);
|
|
809
|
+
if (step) lines.set(`${gi}:${b.step}:${b.what}:${b.index}`, describeStep(step, b, ctx));
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
renderHuman(io, {
|
|
814
|
+
root: repo.root,
|
|
815
|
+
since,
|
|
816
|
+
files: files.map((f) => f.path),
|
|
817
|
+
counts,
|
|
818
|
+
clients,
|
|
819
|
+
summary,
|
|
820
|
+
evidence: evidence ? (b) => lines.get(`${b.group}:${b.step}:${b.what}:${b.index}`) ?? "(transcript no longer on disk)" : () => void 0,
|
|
821
|
+
enforceable: enforceable.size,
|
|
822
|
+
warnings
|
|
823
|
+
});
|
|
824
|
+
return 0;
|
|
825
|
+
}
|
|
826
|
+
function ruleHead(r) {
|
|
827
|
+
const id = `R${r.rule.n}`.padEnd(4);
|
|
828
|
+
return ` ${id} ${clip(r.rule.text, 78)} (${r.rule.file}:${r.rule.line})`;
|
|
829
|
+
}
|
|
830
|
+
function renderHuman(io, v) {
|
|
831
|
+
const s = v.summary;
|
|
832
|
+
const window = v.since === void 0 ? "all history" : `since ${localDay(v.since)}`;
|
|
833
|
+
io.out(`agentguard audit \u2014 ${v.root} \xB7 ${window}`);
|
|
834
|
+
const read = v.clients.map((c) => {
|
|
835
|
+
const n = v.counts[c];
|
|
836
|
+
const parts = [`${n.read} read`, `${n.cached} from cache`];
|
|
837
|
+
if (n.retained) parts.push(`${n.retained} kept from deleted transcripts`);
|
|
838
|
+
if (n.older) parts.push(`${n.older} older than the window`);
|
|
839
|
+
return `${CLIENT_LABEL[c]}: ${plural(n.transcripts, "transcript")} (${parts.join(", ")})`;
|
|
840
|
+
});
|
|
841
|
+
io.out(read.join(" \xB7 "));
|
|
842
|
+
const byClient = v.clients.filter((c) => s.sessionsByClient[c] > 0).map((c) => `${CLIENT_LABEL[c]} ${s.sessionsByClient[c]}`);
|
|
843
|
+
io.out(
|
|
844
|
+
`${plural(s.sessions, "session")} in this repository${byClient.length ? ` (${byClient.join(", ")})` : ""}. Read on this machine; nothing was sent anywhere.`
|
|
845
|
+
);
|
|
846
|
+
if (s.unread > 0)
|
|
847
|
+
io.out(
|
|
848
|
+
`${plural(s.unread, "Codex tool call")} ran JavaScript (\`exec\`), not a command line, and ${s.unread === 1 ? "was" : "were"} not read.`
|
|
849
|
+
);
|
|
850
|
+
for (const w of v.warnings) io.out(`warning: ${w}`);
|
|
851
|
+
io.out("");
|
|
852
|
+
if (v.files.length === 0 && s.rules.length === 0) {
|
|
853
|
+
io.out(
|
|
854
|
+
"No rule files here (CLAUDE.md, AGENTS.md, GEMINI.md, .cursor/rules, \u2026) and no audit.rules in agentguard.yaml: nothing to audit."
|
|
855
|
+
);
|
|
856
|
+
io.out("");
|
|
857
|
+
io.out(TEAM_HISTORY_LINE);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
const checkable = s.rules.filter((r) => r.status !== "not-checkable");
|
|
861
|
+
const perFile = /* @__PURE__ */ new Map();
|
|
862
|
+
for (const r of s.rules) perFile.set(r.rule.file, (perFile.get(r.rule.file) ?? 0) + 1);
|
|
863
|
+
io.out(
|
|
864
|
+
`${plural(s.rules.length, "rule")} found (${[...perFile].map(([f, n]) => `${n} in ${f}`).join(", ")}); ${checkable.length} checkable.`
|
|
865
|
+
);
|
|
866
|
+
if (s.sessions === 0)
|
|
867
|
+
io.out(
|
|
868
|
+
`No Claude Code or Codex sessions in ${v.root} ${window}. Transcripts are read from ~/.claude/projects and ~/.codex/sessions (--claude-dir, --codex-dir); try --since 30d or --since all.`
|
|
869
|
+
);
|
|
870
|
+
const section = (title, list, body) => {
|
|
871
|
+
if (list.length === 0) return;
|
|
872
|
+
io.out("");
|
|
873
|
+
io.out(title);
|
|
874
|
+
for (const r of list) {
|
|
875
|
+
io.out(ruleHead(r));
|
|
876
|
+
body(r);
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
const checkLines = (r) => {
|
|
880
|
+
r.rule.checks.forEach((check, k) => {
|
|
881
|
+
const c = r.checks[k];
|
|
882
|
+
const noun = applicableNoun(c.kind, check.spec);
|
|
883
|
+
const scope = noun ? ` (${c.sessionsApplicable} ${noun})` : "";
|
|
884
|
+
let verdict;
|
|
885
|
+
if (c.sessionsBroken > 0) {
|
|
886
|
+
verdict = `broken in ${c.sessionsBroken} of ${s.sessions} sessions${scope}`;
|
|
887
|
+
const extra = [];
|
|
888
|
+
if (c.breaks > c.sessionsBroken) extra.push(`${c.breaks} times`);
|
|
889
|
+
if (c.sessionsOnRequest > 0)
|
|
890
|
+
extra.push(`${c.sessionsOnRequest} on a turn that asked for it`);
|
|
891
|
+
if (c.refused > 0) extra.push(`${c.refused} refused by the harness`);
|
|
892
|
+
if (extra.length) verdict += `; ${extra.join(", ")}`;
|
|
893
|
+
} else if (c.sessionsApplicable > 0 || !noun) {
|
|
894
|
+
const n = noun ? c.sessionsApplicable : s.sessions;
|
|
895
|
+
verdict = n === 1 ? `kept in the 1 session${noun ? ` that ${noun}` : ""}` : `kept in all ${n} sessions${noun ? ` that ${noun}` : ""}`;
|
|
896
|
+
} else verdict = `nothing in these sessions could break it`;
|
|
897
|
+
if (c.sessionsUndetermined > 0)
|
|
898
|
+
verdict += `; ${c.sessionsUndetermined} more in sessions that do not say who asked (Codex)`;
|
|
899
|
+
io.out(` ${c.label} \u2014 ${verdict}`);
|
|
900
|
+
});
|
|
901
|
+
for (const b of r.last) {
|
|
902
|
+
const what = v.evidence(b);
|
|
903
|
+
io.out(
|
|
904
|
+
` ${localTime(b.at)}${what ? ` ${what}` : ""}${b.refused ? " [refused]" : ""}${b.asked ? " [asked]" : ""} \xB7 session ${b.session.slice(0, 8)}`
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
section(
|
|
909
|
+
"BROKEN",
|
|
910
|
+
s.rules.filter((r) => r.status === "broken").sort((a, b) => b.sessionsBroken - a.sessionsBroken),
|
|
911
|
+
checkLines
|
|
912
|
+
);
|
|
913
|
+
section(
|
|
914
|
+
"KEPT",
|
|
915
|
+
s.rules.filter((r) => r.status === "kept"),
|
|
916
|
+
checkLines
|
|
917
|
+
);
|
|
918
|
+
section(
|
|
919
|
+
"NOTHING IN THESE SESSIONS COULD BREAK IT",
|
|
920
|
+
s.rules.filter((r) => r.status === "not-applicable"),
|
|
921
|
+
checkLines
|
|
922
|
+
);
|
|
923
|
+
const notCheckable = s.rules.filter((r) => r.status === "not-checkable");
|
|
924
|
+
if (notCheckable.length > 0) {
|
|
925
|
+
io.out("");
|
|
926
|
+
io.out(
|
|
927
|
+
`NOT CHECKABLE MECHANICALLY (${notCheckable.length}) \u2014 ${NOT_CHECKABLE_HINT.replace(/^not checkable mechanically — /, "")}`
|
|
928
|
+
);
|
|
929
|
+
for (const r of notCheckable)
|
|
930
|
+
io.out(`${ruleHead(r)}${r.rule.silenced ? " [marked none]" : ""}`);
|
|
931
|
+
}
|
|
932
|
+
io.out("");
|
|
933
|
+
if (v.enforceable > 0)
|
|
934
|
+
io.out(
|
|
935
|
+
`Enforce: agentguard audit --enforce writes ${plural(v.enforceable, "rule")} into agentguard.yaml as hook rules, in dry-run (nothing is blocked until hooks.mode: enforce)`
|
|
936
|
+
);
|
|
937
|
+
io.out(TEAM_HISTORY_LINE);
|
|
938
|
+
}
|
|
939
|
+
async function enforce(args, io, rules, policyPath, checkout, json) {
|
|
940
|
+
const plan = planEnforcement(rules);
|
|
941
|
+
const preview = flagBool(args, "preview") === true;
|
|
942
|
+
const edit = applyEnforcePlan(policyPath, plan);
|
|
943
|
+
const diff = lineDiff(edit.before, edit.after);
|
|
944
|
+
const changed = edit.before !== edit.after;
|
|
945
|
+
const blocked = edit.mode === "enforce" && flagBool(args, "yes") !== true;
|
|
946
|
+
const write = changed && !preview && !blocked;
|
|
947
|
+
if (write) writeFileSync2(policyPath, edit.after);
|
|
948
|
+
const home = homeFrom(io);
|
|
949
|
+
const installed = ["project", "user"].some((scope) => {
|
|
950
|
+
try {
|
|
951
|
+
return installedHandlers(hookTarget("claude-code", scope, { cwd: checkout, home }, "")).length > 0;
|
|
952
|
+
} catch {
|
|
953
|
+
return true;
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
let hookInstalled = installed;
|
|
957
|
+
const hookOut = [];
|
|
958
|
+
if (!installed && !preview && !blocked) {
|
|
959
|
+
const sub = { ...io, cwd: checkout, out: (l) => hookOut.push(l) };
|
|
960
|
+
const passthrough = ["hooks", "install"];
|
|
961
|
+
const config = flagString(args, "config");
|
|
962
|
+
if (config) passthrough.push("--config", resolve3(io.cwd, config));
|
|
963
|
+
const command = flagString(args, "command");
|
|
964
|
+
if (command) passthrough.push("--command", command);
|
|
965
|
+
await hooksCommand(parseArgs(passthrough), sub);
|
|
966
|
+
hookInstalled = true;
|
|
967
|
+
}
|
|
968
|
+
if (json) {
|
|
969
|
+
io.out(
|
|
970
|
+
JSON.stringify(
|
|
971
|
+
{
|
|
972
|
+
policy: policyPath,
|
|
973
|
+
written: write,
|
|
974
|
+
preview,
|
|
975
|
+
hooks_mode: edit.mode,
|
|
976
|
+
added: edit.added,
|
|
977
|
+
audit_only: plan.auditOnly.map((a) => ({
|
|
978
|
+
rule: a.rule.id,
|
|
979
|
+
check: a.check.label,
|
|
980
|
+
why: a.why
|
|
981
|
+
})),
|
|
982
|
+
notes: plan.notes,
|
|
983
|
+
hook_installed: hookInstalled
|
|
984
|
+
},
|
|
985
|
+
null,
|
|
986
|
+
2
|
|
987
|
+
)
|
|
988
|
+
);
|
|
989
|
+
return blocked && changed ? 1 : 0;
|
|
990
|
+
}
|
|
991
|
+
if (!changed)
|
|
992
|
+
io.out(`agentguard.yaml already has a hook rule for every enforceable rule (${policyPath}).`);
|
|
993
|
+
else {
|
|
994
|
+
io.out(
|
|
995
|
+
`${write ? edit.created ? "wrote" : "updated" : "would update"} ${policyPath} \u2014 ${plural(edit.added.length, "hook rule")}:`
|
|
996
|
+
);
|
|
997
|
+
for (const line of diff) io.out(` ${line}`);
|
|
998
|
+
}
|
|
999
|
+
if (plan.auditOnly.length > 0) {
|
|
1000
|
+
io.out("");
|
|
1001
|
+
io.out("Audit only (a pre-execution hook cannot check these):");
|
|
1002
|
+
for (const a of plan.auditOnly) io.out(` R${a.rule.n} ${a.check.label} \u2014 ${a.why}`);
|
|
1003
|
+
}
|
|
1004
|
+
if (plan.notes.length > 0) {
|
|
1005
|
+
io.out("");
|
|
1006
|
+
for (const n of plan.notes) io.out(`note: ${n}`);
|
|
1007
|
+
}
|
|
1008
|
+
io.out("");
|
|
1009
|
+
if (blocked && changed) {
|
|
1010
|
+
io.out(
|
|
1011
|
+
"hooks.mode is enforce in this agentguard.yaml, so these rules would block at once. Nothing was written. Rerun with --enforce --yes to add them anyway, or set hooks.mode: dry-run first."
|
|
1012
|
+
);
|
|
1013
|
+
return 1;
|
|
1014
|
+
}
|
|
1015
|
+
if (preview) {
|
|
1016
|
+
io.out("Preview only: nothing was written. Run agentguard audit --enforce to write it.");
|
|
1017
|
+
return 0;
|
|
1018
|
+
}
|
|
1019
|
+
for (const l of hookOut) io.out(l);
|
|
1020
|
+
if (installed) io.out("The agentguard hook is already installed (agentguard hooks status).");
|
|
1021
|
+
io.out(
|
|
1022
|
+
`hooks.mode is ${edit.mode}: ${edit.mode === "dry-run" ? "every decision is logged and nothing is blocked. Read agentguard report, then set hooks.mode: enforce yourself." : "the rules apply from the next session."}`
|
|
1023
|
+
);
|
|
1024
|
+
return 0;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
export {
|
|
1028
|
+
TEAM_HISTORY_URL,
|
|
1029
|
+
TEAM_HISTORY_LINE,
|
|
1030
|
+
parseSince,
|
|
1031
|
+
auditCommand
|
|
1032
|
+
};
|
|
1033
|
+
//# sourceMappingURL=chunk-6S7LYVOC.js.map
|