@shidesheng0218/agentguard 0.8.0 → 0.9.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 +41 -31
- package/dist/chunk-SACTXNYK.js +1450 -0
- package/dist/claude-ZFYJRREP.js +262 -0
- package/dist/cli.js +173 -1284
- package/package.json +4 -5
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,55 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
HOOKS_STOP_BLOCK_REASON,
|
|
4
|
+
WIRE_VERIFY_CORRECTIVE,
|
|
5
|
+
analyzeCall,
|
|
6
|
+
blockKindStats,
|
|
7
|
+
buildStatus,
|
|
8
|
+
callsSince,
|
|
9
|
+
captureCheckpoint,
|
|
10
|
+
castVetoVote,
|
|
11
|
+
claudeDetected,
|
|
12
|
+
claudeHooksInstalled,
|
|
13
|
+
claudeSettingsPath,
|
|
14
|
+
codexDetected,
|
|
15
|
+
codexHooksPath,
|
|
16
|
+
collectVetoContext,
|
|
17
|
+
countBlocks,
|
|
18
|
+
countEvents,
|
|
19
|
+
detectKimiConfig,
|
|
20
|
+
findClaims,
|
|
21
|
+
fingerprint,
|
|
22
|
+
getMeta,
|
|
23
|
+
guardHome,
|
|
24
|
+
hasEvidence,
|
|
25
|
+
hasRecentEdits,
|
|
26
|
+
hashOutput,
|
|
27
|
+
installClaudeHooks,
|
|
28
|
+
knownSessions,
|
|
29
|
+
latestCheckpointFile,
|
|
30
|
+
latestSessionId,
|
|
31
|
+
listBlocks,
|
|
32
|
+
loadConfig,
|
|
33
|
+
normalizeCall,
|
|
34
|
+
oldestEventTs,
|
|
35
|
+
openDb,
|
|
36
|
+
outputSampleOf,
|
|
37
|
+
probeLogPath,
|
|
38
|
+
recordBlock,
|
|
39
|
+
recordCall,
|
|
40
|
+
recordEvent,
|
|
41
|
+
renderResumeBlock,
|
|
42
|
+
setBlockFeedback,
|
|
43
|
+
setMeta,
|
|
44
|
+
stateDbPath,
|
|
45
|
+
uninstallClaudeHooks,
|
|
46
|
+
userConfigPath,
|
|
47
|
+
vetoKeyConfigured,
|
|
48
|
+
writeConfigTemplate
|
|
49
|
+
} from "./chunk-SACTXNYK.js";
|
|
2
50
|
|
|
3
51
|
// src/cli.ts
|
|
4
|
-
import
|
|
52
|
+
import fs6 from "fs";
|
|
5
53
|
import { Command } from "commander";
|
|
6
54
|
|
|
7
55
|
// src/version.ts
|
|
@@ -10,395 +58,9 @@ var require2 = createRequire(import.meta.url);
|
|
|
10
58
|
var pkg = require2("../package.json");
|
|
11
59
|
var version = pkg.version;
|
|
12
60
|
|
|
13
|
-
// src/
|
|
14
|
-
import fs2 from "fs";
|
|
15
|
-
import { parse as parseToml } from "smol-toml";
|
|
16
|
-
|
|
17
|
-
// src/paths.ts
|
|
61
|
+
// src/installer.ts
|
|
18
62
|
import fs from "fs";
|
|
19
|
-
import os from "os";
|
|
20
63
|
import path from "path";
|
|
21
|
-
function guardHome() {
|
|
22
|
-
const env = process.env.AGENT_GUARD_HOME ?? process.env.KIMI_GUARD_HOME;
|
|
23
|
-
if (env && env.trim()) return path.resolve(env);
|
|
24
|
-
const fresh = path.join(os.homedir(), ".agent-guard");
|
|
25
|
-
const legacy = path.join(os.homedir(), ".kimi-guard");
|
|
26
|
-
if (!fs.existsSync(fresh) && fs.existsSync(legacy)) return legacy;
|
|
27
|
-
return fresh;
|
|
28
|
-
}
|
|
29
|
-
function claudeSettingsPath() {
|
|
30
|
-
const env = process.env.CLAUDE_SETTINGS_PATH;
|
|
31
|
-
if (env && env.trim()) return path.resolve(env);
|
|
32
|
-
return path.join(os.homedir(), ".claude", "settings.json");
|
|
33
|
-
}
|
|
34
|
-
function claudeDetected() {
|
|
35
|
-
return fs.existsSync(claudeSettingsPath()) || fs.existsSync(path.join(os.homedir(), ".claude"));
|
|
36
|
-
}
|
|
37
|
-
function stateDbPath() {
|
|
38
|
-
return path.join(guardHome(), "state.db");
|
|
39
|
-
}
|
|
40
|
-
function probeLogPath() {
|
|
41
|
-
return path.join(guardHome(), "probe.jsonl");
|
|
42
|
-
}
|
|
43
|
-
function userConfigPath() {
|
|
44
|
-
return path.join(guardHome(), "config.toml");
|
|
45
|
-
}
|
|
46
|
-
function detectKimiConfig() {
|
|
47
|
-
const env = process.env.KIMI_CONFIG_PATH;
|
|
48
|
-
if (env && env.trim()) {
|
|
49
|
-
const p = path.resolve(env);
|
|
50
|
-
return { path: p, exists: fs.existsSync(p) };
|
|
51
|
-
}
|
|
52
|
-
const candidates = [
|
|
53
|
-
path.join(os.homedir(), ".kimi-code", "config.toml"),
|
|
54
|
-
path.join(os.homedir(), ".kimi", "config.toml")
|
|
55
|
-
];
|
|
56
|
-
for (const p of candidates) {
|
|
57
|
-
if (fs.existsSync(p)) return { path: p, exists: true };
|
|
58
|
-
}
|
|
59
|
-
return { path: candidates[0], exists: false };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// src/toolsets.ts
|
|
63
|
-
var DEFAULT_EDIT_TOOLS = ["WriteFile", "StrReplaceFile", "Edit", "Write", "MultiEdit", "NotebookEdit"];
|
|
64
|
-
var DEFAULT_READ_TOOLS = ["ReadFile", "Read"];
|
|
65
|
-
var DEFAULT_SEARCH_TOOLS = ["Grep", "Glob"];
|
|
66
|
-
var DEFAULT_SHELL_TOOLS = ["Shell", "Bash"];
|
|
67
|
-
var CLAUDE_EDIT_TOOLS = ["Write", "Edit", "MultiEdit", "NotebookEdit"];
|
|
68
|
-
var CLAUDE_READ_TOOLS = ["Read"];
|
|
69
|
-
var CLAUDE_SEARCH_TOOLS = ["Grep", "Glob"];
|
|
70
|
-
var CLAUDE_SHELL_TOOLS = ["Bash"];
|
|
71
|
-
function toolDefaultsFor(harness) {
|
|
72
|
-
return harness === "claude" ? { edit: [...CLAUDE_EDIT_TOOLS], read: [...CLAUDE_READ_TOOLS], search: [...CLAUDE_SEARCH_TOOLS], shell: [...CLAUDE_SHELL_TOOLS] } : { edit: [...DEFAULT_EDIT_TOOLS], read: [...DEFAULT_READ_TOOLS], search: [...DEFAULT_SEARCH_TOOLS], shell: [...DEFAULT_SHELL_TOOLS] };
|
|
73
|
-
}
|
|
74
|
-
function resolve(list, fallback) {
|
|
75
|
-
return new Set(list.length > 0 ? list : fallback);
|
|
76
|
-
}
|
|
77
|
-
function editTools(cfg) {
|
|
78
|
-
return resolve(cfg.tools.edit, DEFAULT_EDIT_TOOLS);
|
|
79
|
-
}
|
|
80
|
-
function readTools(cfg) {
|
|
81
|
-
return resolve(cfg.tools.read, DEFAULT_READ_TOOLS);
|
|
82
|
-
}
|
|
83
|
-
function searchTools(cfg) {
|
|
84
|
-
return resolve(cfg.tools.search, DEFAULT_SEARCH_TOOLS);
|
|
85
|
-
}
|
|
86
|
-
function shellTools(cfg) {
|
|
87
|
-
return resolve(cfg.tools.shell, DEFAULT_SHELL_TOOLS);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// src/config.ts
|
|
91
|
-
var defaultConfig = {
|
|
92
|
-
harness: "kimi",
|
|
93
|
-
tools: toolDefaultsFor("kimi"),
|
|
94
|
-
repeat: {
|
|
95
|
-
enabled: true,
|
|
96
|
-
maxRepeats: 3,
|
|
97
|
-
warnAt: 2,
|
|
98
|
-
windowMinutes: 30,
|
|
99
|
-
watch: ["Grep", "Glob", "Shell", "Bash", "FetchURL", "SearchWeb", "ReadFile"],
|
|
100
|
-
thresholds: { ReadFile: 5 },
|
|
101
|
-
exemptPatterns: []
|
|
102
|
-
},
|
|
103
|
-
cycle: { enabled: true, windowMinutes: 30 },
|
|
104
|
-
noProgress: { enabled: true, windowMinutes: 30, warnAt: 15, blockAt: 25 },
|
|
105
|
-
nearRepeat: { enabled: true, windowMinutes: 30, warnAt: 6, blockAt: 10 },
|
|
106
|
-
explore: { enabled: true, windowMinutes: 30, warnAt: 10, blockAt: 15 },
|
|
107
|
-
verify: {
|
|
108
|
-
enabled: true,
|
|
109
|
-
blockOnNoEvidence: false,
|
|
110
|
-
evidenceWindowMinutes: 60,
|
|
111
|
-
claimPatterns: [],
|
|
112
|
-
evidencePatterns: [],
|
|
113
|
-
shellTools: ["Shell", "Bash"],
|
|
114
|
-
veto: {
|
|
115
|
-
enabled: false,
|
|
116
|
-
model: "kimi-k3",
|
|
117
|
-
baseUrl: "https://api.moonshot.cn/v1",
|
|
118
|
-
maxCallsPerSession: 3,
|
|
119
|
-
timeoutMs: 1e4
|
|
120
|
-
}
|
|
121
|
-
},
|
|
122
|
-
thinking: { enabled: true, minThinkChars: 2e4, maxTextRatio: 0.1 },
|
|
123
|
-
anchor: { enabled: true, everyNPrompts: 5, maxChars: 1e3 },
|
|
124
|
-
context: { enabled: true, warnPercent: 85 },
|
|
125
|
-
noGain: { enabled: true, windowMinutes: 30, warnAt: 3, blockAt: 4 },
|
|
126
|
-
churn: {
|
|
127
|
-
enabled: true,
|
|
128
|
-
windowMinutes: 30,
|
|
129
|
-
warnAt: 5,
|
|
130
|
-
blockAt: 10,
|
|
131
|
-
tools: ["WriteFile", "StrReplaceFile", "Edit", "Write", "MultiEdit", "NotebookEdit"]
|
|
132
|
-
},
|
|
133
|
-
policy: { killSwitch: true, maxBlocksPerSession: 5, blockWindowMinutes: 60 },
|
|
134
|
-
budget: {
|
|
135
|
-
enabled: true,
|
|
136
|
-
plan: "tier1",
|
|
137
|
-
weekly: 0,
|
|
138
|
-
fiveHour: 0,
|
|
139
|
-
dispatchTools: ["Task", "Agent"],
|
|
140
|
-
reservePercent: 10,
|
|
141
|
-
subagentWeight: 5,
|
|
142
|
-
warnPercent: 80,
|
|
143
|
-
precise: false,
|
|
144
|
-
preciseUrl: "",
|
|
145
|
-
preciseCacheSeconds: 300
|
|
146
|
-
},
|
|
147
|
-
probe: false
|
|
148
|
-
};
|
|
149
|
-
var CONFIG_TEMPLATE = `# agent-guard configuration
|
|
150
|
-
# Docs: https://github.com/shidesheng0218/kimi-guard
|
|
151
|
-
|
|
152
|
-
[tools] # canonical tool-name taxonomy \u2014 if your CLI version renames tools, fix it HERE
|
|
153
|
-
edit = ["WriteFile", "StrReplaceFile", "Edit", "Write", "MultiEdit", "NotebookEdit"]
|
|
154
|
-
read = ["ReadFile", "Read"]
|
|
155
|
-
search = ["Grep", "Glob"]
|
|
156
|
-
shell = ["Shell", "Bash"]
|
|
157
|
-
|
|
158
|
-
[repeat]
|
|
159
|
-
enabled = true
|
|
160
|
-
maxRepeats = 3 # identical (tool, args) calls allowed per window
|
|
161
|
-
warnAt = 2 # soft context warning before the hard block
|
|
162
|
-
windowMinutes = 30
|
|
163
|
-
watch = ["Grep", "Glob", "Shell", "Bash", "FetchURL", "SearchWeb", "ReadFile"]
|
|
164
|
-
# exemptPatterns = ["git status"] # regexes over JSON-serialized args; matching calls are never repeat-blocked (polling commands like git status, sleep)
|
|
165
|
-
|
|
166
|
-
[repeat.thresholds] # per-tool overrides
|
|
167
|
-
ReadFile = 5
|
|
168
|
-
|
|
169
|
-
[cycle] # A->B->A->B oscillation detection
|
|
170
|
-
enabled = true
|
|
171
|
-
windowMinutes = 30
|
|
172
|
-
|
|
173
|
-
[noProgress] # long stretch of calls with no successful edit
|
|
174
|
-
enabled = true
|
|
175
|
-
windowMinutes = 30
|
|
176
|
-
warnAt = 15
|
|
177
|
-
blockAt = 25
|
|
178
|
-
|
|
179
|
-
[nearRepeat] # fuzzy near-duplicates (punctuation/case/order differences)
|
|
180
|
-
enabled = true
|
|
181
|
-
windowMinutes = 30
|
|
182
|
-
warnAt = 6
|
|
183
|
-
blockAt = 10
|
|
184
|
-
|
|
185
|
-
[explore] # pure-exploration streak: reads/searches with no action in between
|
|
186
|
-
enabled = true
|
|
187
|
-
windowMinutes = 30
|
|
188
|
-
warnAt = 10
|
|
189
|
-
blockAt = 15
|
|
190
|
-
|
|
191
|
-
[verify] # completion-claim gate: "tests pass" must be backed by a real run
|
|
192
|
-
enabled = true
|
|
193
|
-
blockOnNoEvidence = false # hooks path: block Stop when edits landed but nothing was verified
|
|
194
|
-
evidenceWindowMinutes = 60
|
|
195
|
-
# deprecated: shell tool names now live in [tools] shell (this key still works)
|
|
196
|
-
|
|
197
|
-
[verify.veto] # optional LLM veto vote to suppress false positives (self-critic style)
|
|
198
|
-
enabled = false # requires KIMI_GUARD_VETO_API_KEY in the environment
|
|
199
|
-
model = "kimi-k3" # use a cheap fast model \u2014 the LLM only votes, never authors
|
|
200
|
-
baseUrl = "https://api.moonshot.cn/v1"
|
|
201
|
-
maxCallsPerSession = 3 # anti "vote-laundering" cap: the model cannot retry its way out
|
|
202
|
-
timeoutMs = 10000
|
|
203
|
-
|
|
204
|
-
[thinking] # thinking-dominance (pure-reasoning turns), Wire mode only
|
|
205
|
-
enabled = true
|
|
206
|
-
minThinkChars = 20000
|
|
207
|
-
maxTextRatio = 0.1
|
|
208
|
-
|
|
209
|
-
[anchor] # goal anchoring: re-inject the original task periodically
|
|
210
|
-
enabled = true
|
|
211
|
-
everyNPrompts = 5 # re-inject the goal every N prompts / steps
|
|
212
|
-
maxChars = 1000
|
|
213
|
-
|
|
214
|
-
[context] # context-fill gate (Wire mode reads StatusUpdate.context_usage)
|
|
215
|
-
enabled = true
|
|
216
|
-
warnPercent = 85 # steer a wrap-up warning when context is this full
|
|
217
|
-
|
|
218
|
-
[noGain] # different args, byte-identical output
|
|
219
|
-
enabled = true
|
|
220
|
-
windowMinutes = 30
|
|
221
|
-
warnAt = 3
|
|
222
|
-
blockAt = 4
|
|
223
|
-
|
|
224
|
-
[churn] # same file edited over and over
|
|
225
|
-
enabled = true
|
|
226
|
-
windowMinutes = 30
|
|
227
|
-
warnAt = 5
|
|
228
|
-
blockAt = 10
|
|
229
|
-
# deprecated: edit tool names now live in [tools] edit (this key still works)
|
|
230
|
-
|
|
231
|
-
[policy]
|
|
232
|
-
killSwitch = true # after maxBlocksPerSession interventions, block ALL tools
|
|
233
|
-
maxBlocksPerSession = 5
|
|
234
|
-
blockWindowMinutes = 60
|
|
235
|
-
|
|
236
|
-
[budget] # request accounting for Kimi Coding Plans
|
|
237
|
-
enabled = true
|
|
238
|
-
plan = "tier1" # tier1: 1024/week | tier2: 2048 | tier3: 7168 (200 per 5h)
|
|
239
|
-
weekly = 0 # override weekly requests (0 = use plan preset)
|
|
240
|
-
fiveHour = 0 # override 5h requests (0 = use plan preset)
|
|
241
|
-
dispatchTools = ["Task", "Agent"]
|
|
242
|
-
reservePercent = 10 # keep this much headroom for you, not the agent
|
|
243
|
-
subagentWeight = 5 # ~requests each dispatched subagent costs
|
|
244
|
-
warnPercent = 80
|
|
245
|
-
precise = false # poll the official Kimi usage API for exact windows (needs KIMI_API_KEY, sk-kimi-...)
|
|
246
|
-
preciseUrl = "" # default https://api.kimi.com/coding/v1
|
|
247
|
-
preciseCacheSeconds = 300 # the API is rate-limited; cache aggressively. Falls back to event-based on any error
|
|
248
|
-
|
|
249
|
-
[probe]
|
|
250
|
-
enabled = false
|
|
251
|
-
`;
|
|
252
|
-
function applyClaudeDefaults(cfg) {
|
|
253
|
-
cfg.tools = toolDefaultsFor("claude");
|
|
254
|
-
cfg.repeat.watch = ["Grep", "Glob", "Bash", "Read", "WebFetch", "WebSearch"];
|
|
255
|
-
cfg.repeat.thresholds = { Read: 5 };
|
|
256
|
-
cfg.budget.dispatchTools = ["Task"];
|
|
257
|
-
}
|
|
258
|
-
function num(v, fallback) {
|
|
259
|
-
return typeof v === "number" && Number.isFinite(v) ? v : fallback;
|
|
260
|
-
}
|
|
261
|
-
function bool(v, fallback) {
|
|
262
|
-
return typeof v === "boolean" ? v : fallback;
|
|
263
|
-
}
|
|
264
|
-
function strArr(v, fallback) {
|
|
265
|
-
return Array.isArray(v) && v.every((x) => typeof x === "string") && v.length > 0 ? v : fallback;
|
|
266
|
-
}
|
|
267
|
-
function strArrOrNull(v) {
|
|
268
|
-
return Array.isArray(v) && v.every((x) => typeof x === "string") && v.length > 0 ? v : null;
|
|
269
|
-
}
|
|
270
|
-
function loadConfig(configPath = userConfigPath(), harness = "kimi") {
|
|
271
|
-
const cfg = structuredClone(defaultConfig);
|
|
272
|
-
cfg.harness = harness;
|
|
273
|
-
if (harness === "claude") applyClaudeDefaults(cfg);
|
|
274
|
-
let raw;
|
|
275
|
-
try {
|
|
276
|
-
raw = fs2.readFileSync(configPath, "utf8");
|
|
277
|
-
} catch {
|
|
278
|
-
return cfg;
|
|
279
|
-
}
|
|
280
|
-
let data;
|
|
281
|
-
try {
|
|
282
|
-
data = parseToml(raw);
|
|
283
|
-
} catch (err) {
|
|
284
|
-
process.stderr.write(`[agent-guard] failed to parse ${configPath}: ${err.message}
|
|
285
|
-
`);
|
|
286
|
-
return cfg;
|
|
287
|
-
}
|
|
288
|
-
const section = (name) => data[name] ?? {};
|
|
289
|
-
const tools = section("tools");
|
|
290
|
-
const toolsEdit = strArrOrNull(tools["edit"]);
|
|
291
|
-
const toolsRead = strArrOrNull(tools["read"]);
|
|
292
|
-
const toolsSearch = strArrOrNull(tools["search"]);
|
|
293
|
-
const toolsShell = strArrOrNull(tools["shell"]);
|
|
294
|
-
if (toolsEdit) cfg.tools.edit = toolsEdit;
|
|
295
|
-
if (toolsRead) cfg.tools.read = toolsRead;
|
|
296
|
-
if (toolsSearch) cfg.tools.search = toolsSearch;
|
|
297
|
-
if (toolsShell) cfg.tools.shell = toolsShell;
|
|
298
|
-
const repeat = section("repeat");
|
|
299
|
-
cfg.repeat.enabled = bool(repeat["enabled"], cfg.repeat.enabled);
|
|
300
|
-
cfg.repeat.maxRepeats = num(repeat["maxRepeats"], cfg.repeat.maxRepeats);
|
|
301
|
-
cfg.repeat.warnAt = num(repeat["warnAt"], cfg.repeat.warnAt);
|
|
302
|
-
cfg.repeat.windowMinutes = num(repeat["windowMinutes"], cfg.repeat.windowMinutes);
|
|
303
|
-
cfg.repeat.watch = strArr(repeat["watch"], cfg.repeat.watch);
|
|
304
|
-
const exempt = repeat["exemptPatterns"];
|
|
305
|
-
if (Array.isArray(exempt)) cfg.repeat.exemptPatterns = exempt.filter((p) => typeof p === "string");
|
|
306
|
-
const th = repeat["thresholds"];
|
|
307
|
-
if (th) {
|
|
308
|
-
for (const [k, v] of Object.entries(th)) if (typeof v === "number") cfg.repeat.thresholds[k] = v;
|
|
309
|
-
}
|
|
310
|
-
const cycle = section("cycle");
|
|
311
|
-
cfg.cycle.enabled = bool(cycle["enabled"], cfg.cycle.enabled);
|
|
312
|
-
cfg.cycle.windowMinutes = num(cycle["windowMinutes"], cfg.cycle.windowMinutes);
|
|
313
|
-
const noProgress = section("noProgress");
|
|
314
|
-
cfg.noProgress.enabled = bool(noProgress["enabled"], cfg.noProgress.enabled);
|
|
315
|
-
cfg.noProgress.windowMinutes = num(noProgress["windowMinutes"], cfg.noProgress.windowMinutes);
|
|
316
|
-
cfg.noProgress.warnAt = num(noProgress["warnAt"], cfg.noProgress.warnAt);
|
|
317
|
-
cfg.noProgress.blockAt = num(noProgress["blockAt"], cfg.noProgress.blockAt);
|
|
318
|
-
const nearRepeat = section("nearRepeat");
|
|
319
|
-
cfg.nearRepeat.enabled = bool(nearRepeat["enabled"], cfg.nearRepeat.enabled);
|
|
320
|
-
cfg.nearRepeat.windowMinutes = num(nearRepeat["windowMinutes"], cfg.nearRepeat.windowMinutes);
|
|
321
|
-
cfg.nearRepeat.warnAt = num(nearRepeat["warnAt"], cfg.nearRepeat.warnAt);
|
|
322
|
-
cfg.nearRepeat.blockAt = num(nearRepeat["blockAt"], cfg.nearRepeat.blockAt);
|
|
323
|
-
const explore = section("explore");
|
|
324
|
-
cfg.explore.enabled = bool(explore["enabled"], cfg.explore.enabled);
|
|
325
|
-
cfg.explore.windowMinutes = num(explore["windowMinutes"], cfg.explore.windowMinutes);
|
|
326
|
-
cfg.explore.warnAt = num(explore["warnAt"], cfg.explore.warnAt);
|
|
327
|
-
cfg.explore.blockAt = num(explore["blockAt"], cfg.explore.blockAt);
|
|
328
|
-
const verify = section("verify");
|
|
329
|
-
cfg.verify.enabled = bool(verify["enabled"], cfg.verify.enabled);
|
|
330
|
-
cfg.verify.blockOnNoEvidence = bool(verify["blockOnNoEvidence"], cfg.verify.blockOnNoEvidence);
|
|
331
|
-
cfg.verify.evidenceWindowMinutes = num(verify["evidenceWindowMinutes"], cfg.verify.evidenceWindowMinutes);
|
|
332
|
-
const claims = verify["claimPatterns"];
|
|
333
|
-
if (Array.isArray(claims)) cfg.verify.claimPatterns = claims.filter((c) => typeof c === "string");
|
|
334
|
-
const evidence = verify["evidencePatterns"];
|
|
335
|
-
if (Array.isArray(evidence)) cfg.verify.evidencePatterns = evidence.filter((c) => typeof c === "string");
|
|
336
|
-
cfg.verify.shellTools = strArr(verify["shellTools"], cfg.verify.shellTools);
|
|
337
|
-
const legacyShellTools = strArrOrNull(verify["shellTools"]);
|
|
338
|
-
if (legacyShellTools && !toolsShell) cfg.tools.shell = legacyShellTools;
|
|
339
|
-
const veto = verify["veto"];
|
|
340
|
-
if (veto) {
|
|
341
|
-
cfg.verify.veto.enabled = bool(veto["enabled"], cfg.verify.veto.enabled);
|
|
342
|
-
cfg.verify.veto.model = typeof veto["model"] === "string" ? veto["model"] : cfg.verify.veto.model;
|
|
343
|
-
cfg.verify.veto.baseUrl = typeof veto["baseUrl"] === "string" ? veto["baseUrl"] : cfg.verify.veto.baseUrl;
|
|
344
|
-
cfg.verify.veto.maxCallsPerSession = num(veto["maxCallsPerSession"], cfg.verify.veto.maxCallsPerSession);
|
|
345
|
-
cfg.verify.veto.timeoutMs = num(veto["timeoutMs"], cfg.verify.veto.timeoutMs);
|
|
346
|
-
}
|
|
347
|
-
const thinking = section("thinking");
|
|
348
|
-
cfg.thinking.enabled = bool(thinking["enabled"], cfg.thinking.enabled);
|
|
349
|
-
cfg.thinking.minThinkChars = num(thinking["minThinkChars"], cfg.thinking.minThinkChars);
|
|
350
|
-
cfg.thinking.maxTextRatio = num(thinking["maxTextRatio"], cfg.thinking.maxTextRatio);
|
|
351
|
-
const anchor = section("anchor");
|
|
352
|
-
cfg.anchor.enabled = bool(anchor["enabled"], cfg.anchor.enabled);
|
|
353
|
-
cfg.anchor.everyNPrompts = num(anchor["everyNPrompts"], cfg.anchor.everyNPrompts);
|
|
354
|
-
cfg.anchor.maxChars = num(anchor["maxChars"], cfg.anchor.maxChars);
|
|
355
|
-
const context = section("context");
|
|
356
|
-
cfg.context.enabled = bool(context["enabled"], cfg.context.enabled);
|
|
357
|
-
cfg.context.warnPercent = num(context["warnPercent"], cfg.context.warnPercent);
|
|
358
|
-
const noGain = section("noGain");
|
|
359
|
-
cfg.noGain.enabled = bool(noGain["enabled"], cfg.noGain.enabled);
|
|
360
|
-
cfg.noGain.windowMinutes = num(noGain["windowMinutes"], cfg.noGain.windowMinutes);
|
|
361
|
-
cfg.noGain.warnAt = num(noGain["warnAt"], cfg.noGain.warnAt);
|
|
362
|
-
cfg.noGain.blockAt = num(noGain["blockAt"], cfg.noGain.blockAt);
|
|
363
|
-
const churn = section("churn");
|
|
364
|
-
cfg.churn.enabled = bool(churn["enabled"], cfg.churn.enabled);
|
|
365
|
-
cfg.churn.windowMinutes = num(churn["windowMinutes"], cfg.churn.windowMinutes);
|
|
366
|
-
cfg.churn.warnAt = num(churn["warnAt"], cfg.churn.warnAt);
|
|
367
|
-
cfg.churn.blockAt = num(churn["blockAt"], cfg.churn.blockAt);
|
|
368
|
-
cfg.churn.tools = strArr(churn["tools"], cfg.churn.tools);
|
|
369
|
-
const legacyChurnTools = strArrOrNull(churn["tools"]);
|
|
370
|
-
if (legacyChurnTools && !toolsEdit) cfg.tools.edit = legacyChurnTools;
|
|
371
|
-
const policy = section("policy");
|
|
372
|
-
cfg.policy.killSwitch = bool(policy["killSwitch"], cfg.policy.killSwitch);
|
|
373
|
-
cfg.policy.maxBlocksPerSession = num(policy["maxBlocksPerSession"], cfg.policy.maxBlocksPerSession);
|
|
374
|
-
cfg.policy.blockWindowMinutes = num(policy["blockWindowMinutes"], cfg.policy.blockWindowMinutes);
|
|
375
|
-
const budget = section("budget");
|
|
376
|
-
cfg.budget.enabled = bool(budget["enabled"], cfg.budget.enabled);
|
|
377
|
-
cfg.budget.plan = typeof budget["plan"] === "string" ? budget["plan"] : cfg.budget.plan;
|
|
378
|
-
cfg.budget.weekly = num(budget["weekly"], cfg.budget.weekly);
|
|
379
|
-
cfg.budget.fiveHour = num(budget["fiveHour"], cfg.budget.fiveHour);
|
|
380
|
-
cfg.budget.dispatchTools = strArr(budget["dispatchTools"], cfg.budget.dispatchTools);
|
|
381
|
-
cfg.budget.reservePercent = num(budget["reservePercent"], cfg.budget.reservePercent);
|
|
382
|
-
cfg.budget.subagentWeight = num(budget["subagentWeight"], cfg.budget.subagentWeight);
|
|
383
|
-
cfg.budget.warnPercent = num(budget["warnPercent"], cfg.budget.warnPercent);
|
|
384
|
-
cfg.budget.precise = bool(budget["precise"], cfg.budget.precise);
|
|
385
|
-
cfg.budget.preciseUrl = typeof budget["preciseUrl"] === "string" ? budget["preciseUrl"] : cfg.budget.preciseUrl;
|
|
386
|
-
cfg.budget.preciseCacheSeconds = num(budget["preciseCacheSeconds"], cfg.budget.preciseCacheSeconds);
|
|
387
|
-
cfg.probe = bool(section("probe")["enabled"], cfg.probe);
|
|
388
|
-
cfg.verify.shellTools = cfg.tools.shell;
|
|
389
|
-
cfg.churn.tools = cfg.tools.edit;
|
|
390
|
-
return cfg;
|
|
391
|
-
}
|
|
392
|
-
function writeConfigTemplate(configPath = userConfigPath()) {
|
|
393
|
-
if (fs2.existsSync(configPath)) return false;
|
|
394
|
-
fs2.mkdirSync(configPath.replace(/[/\\][^/\\]+$/, ""), { recursive: true });
|
|
395
|
-
fs2.writeFileSync(configPath, CONFIG_TEMPLATE, "utf8");
|
|
396
|
-
return true;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// src/installer.ts
|
|
400
|
-
import fs3 from "fs";
|
|
401
|
-
import path2 from "path";
|
|
402
64
|
var MANAGED_BEGIN = "# >>> kimi-guard managed >>> DO NOT EDIT";
|
|
403
65
|
var MANAGED_END = "# <<< kimi-guard <<<";
|
|
404
66
|
function hookRules(commandName = "kguard", compat = false) {
|
|
@@ -443,12 +105,12 @@ function findManagedBlock(text) {
|
|
|
443
105
|
function installHooks(commandName = "kguard", compat = false) {
|
|
444
106
|
const { path: configPath, exists } = detectKimiConfig();
|
|
445
107
|
const created = !exists;
|
|
446
|
-
let text = exists ?
|
|
108
|
+
let text = exists ? fs.readFileSync(configPath, "utf8") : "";
|
|
447
109
|
const block = findManagedBlock(text);
|
|
448
110
|
const replaced = block !== void 0;
|
|
449
111
|
if (exists && !replaced) {
|
|
450
112
|
const backupPath = `${configPath}.kimi-guard.bak`;
|
|
451
|
-
|
|
113
|
+
fs.writeFileSync(backupPath, text, "utf8");
|
|
452
114
|
}
|
|
453
115
|
const newBlock = managedBlock(commandName, compat);
|
|
454
116
|
if (block) {
|
|
@@ -457,8 +119,8 @@ function installHooks(commandName = "kguard", compat = false) {
|
|
|
457
119
|
const prefix = text.length === 0 ? "" : text.endsWith("\n") ? "\n" : "\n\n";
|
|
458
120
|
text = text + prefix + newBlock + "\n";
|
|
459
121
|
}
|
|
460
|
-
|
|
461
|
-
|
|
122
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
123
|
+
fs.writeFileSync(configPath, text, "utf8");
|
|
462
124
|
return {
|
|
463
125
|
configPath,
|
|
464
126
|
created,
|
|
@@ -469,71 +131,71 @@ function installHooks(commandName = "kguard", compat = false) {
|
|
|
469
131
|
function uninstallHooks() {
|
|
470
132
|
const { path: configPath, exists } = detectKimiConfig();
|
|
471
133
|
if (!exists) return { configPath, removed: false };
|
|
472
|
-
const text =
|
|
134
|
+
const text = fs.readFileSync(configPath, "utf8");
|
|
473
135
|
const block = findManagedBlock(text);
|
|
474
136
|
if (!block) return { configPath, removed: false };
|
|
475
137
|
let out = text.slice(0, block.start) + text.slice(block.end);
|
|
476
138
|
out = out.replace(/^\n{2,}/, "\n");
|
|
477
|
-
|
|
139
|
+
fs.writeFileSync(configPath, out, "utf8");
|
|
478
140
|
return { configPath, removed: true };
|
|
479
141
|
}
|
|
480
142
|
function hooksInstalled(configPath) {
|
|
481
143
|
const p = configPath ?? detectKimiConfig().path;
|
|
482
144
|
try {
|
|
483
|
-
return
|
|
145
|
+
return fs.readFileSync(p, "utf8").includes(MANAGED_BEGIN);
|
|
484
146
|
} catch {
|
|
485
147
|
return false;
|
|
486
148
|
}
|
|
487
149
|
}
|
|
488
150
|
|
|
489
|
-
// src/harness/
|
|
490
|
-
import
|
|
491
|
-
import
|
|
492
|
-
var
|
|
493
|
-
var
|
|
151
|
+
// src/harness/codex.ts
|
|
152
|
+
import fs2 from "fs";
|
|
153
|
+
import path2 from "path";
|
|
154
|
+
var CODEX_COMMAND_MARKER = "agentguard hook";
|
|
155
|
+
var CODEX_EVENTS = [
|
|
494
156
|
"PreToolUse",
|
|
495
157
|
"PostToolUse",
|
|
496
|
-
"PostToolUseFailure",
|
|
497
158
|
"UserPromptSubmit",
|
|
498
159
|
"Stop",
|
|
499
160
|
"SubagentStart",
|
|
161
|
+
"SubagentStop",
|
|
500
162
|
"SessionStart",
|
|
501
163
|
"SessionEnd",
|
|
502
164
|
"PreCompact",
|
|
503
165
|
"PostCompact",
|
|
504
|
-
"
|
|
166
|
+
"Interrupt"
|
|
505
167
|
];
|
|
506
|
-
var
|
|
168
|
+
var CODEX_EVENTS_COMPAT = ["PreToolUse", "PostToolUse"];
|
|
507
169
|
function isOurs(group) {
|
|
508
|
-
return (group.hooks ?? []).some((h) => typeof h.command === "string" && h.command.includes(
|
|
170
|
+
return (group.hooks ?? []).some((h) => typeof h.command === "string" && h.command.includes(CODEX_COMMAND_MARKER));
|
|
509
171
|
}
|
|
510
172
|
function ourGroup(event, bin) {
|
|
511
173
|
return {
|
|
512
174
|
matcher: "",
|
|
513
|
-
hooks: [{ type: "command", command: `${bin} hook ${event} --harness
|
|
175
|
+
hooks: [{ type: "command", command: `${bin} hook ${event} --harness codex`, timeout: 5 }]
|
|
514
176
|
};
|
|
515
177
|
}
|
|
516
|
-
function
|
|
517
|
-
const configPath =
|
|
518
|
-
const created = !
|
|
519
|
-
let
|
|
178
|
+
function installCodexHooks(bin = "agentguard", compat = false) {
|
|
179
|
+
const configPath = codexHooksPath();
|
|
180
|
+
const created = !fs2.existsSync(configPath);
|
|
181
|
+
let file = {};
|
|
520
182
|
let backupPath;
|
|
521
183
|
if (!created) {
|
|
522
|
-
const raw =
|
|
184
|
+
const raw = fs2.readFileSync(configPath, "utf8");
|
|
523
185
|
try {
|
|
524
|
-
|
|
186
|
+
file = JSON.parse(raw);
|
|
525
187
|
} catch {
|
|
526
188
|
backupPath = `${configPath}.agentguard.bak`;
|
|
527
|
-
|
|
528
|
-
|
|
189
|
+
fs2.copyFileSync(configPath, backupPath);
|
|
190
|
+
file = {};
|
|
529
191
|
}
|
|
530
192
|
if (!backupPath) {
|
|
531
193
|
backupPath = `${configPath}.agentguard.bak`;
|
|
532
|
-
|
|
194
|
+
fs2.writeFileSync(backupPath, raw, "utf8");
|
|
533
195
|
}
|
|
534
196
|
}
|
|
535
|
-
const events = compat ?
|
|
536
|
-
const hooks =
|
|
197
|
+
const events = compat ? CODEX_EVENTS_COMPAT : CODEX_EVENTS;
|
|
198
|
+
const hooks = file.hooks ??= {};
|
|
537
199
|
let updated = false;
|
|
538
200
|
for (const event of events) {
|
|
539
201
|
const groups = (hooks[event] ?? []).filter((g) => !isOurs(g));
|
|
@@ -542,20 +204,20 @@ function installClaudeHooks(bin = "agentguard", compat = false) {
|
|
|
542
204
|
hooks[event] = groups;
|
|
543
205
|
if (JSON.stringify(groups) !== before) updated = true;
|
|
544
206
|
}
|
|
545
|
-
|
|
546
|
-
|
|
207
|
+
fs2.mkdirSync(path2.dirname(configPath), { recursive: true });
|
|
208
|
+
fs2.writeFileSync(configPath, JSON.stringify(file, null, 2) + "\n", "utf8");
|
|
547
209
|
return { configPath, created, updated: updated || created, backupPath: created ? void 0 : backupPath };
|
|
548
210
|
}
|
|
549
|
-
function
|
|
550
|
-
const configPath =
|
|
551
|
-
if (!
|
|
552
|
-
let
|
|
211
|
+
function uninstallCodexHooks() {
|
|
212
|
+
const configPath = codexHooksPath();
|
|
213
|
+
if (!fs2.existsSync(configPath)) return { configPath, removed: false };
|
|
214
|
+
let file;
|
|
553
215
|
try {
|
|
554
|
-
|
|
216
|
+
file = JSON.parse(fs2.readFileSync(configPath, "utf8"));
|
|
555
217
|
} catch {
|
|
556
218
|
return { configPath, removed: false };
|
|
557
219
|
}
|
|
558
|
-
const hooks =
|
|
220
|
+
const hooks = file.hooks;
|
|
559
221
|
if (!hooks) return { configPath, removed: false };
|
|
560
222
|
let removed = false;
|
|
561
223
|
for (const event of Object.keys(hooks)) {
|
|
@@ -564,565 +226,20 @@ function uninstallClaudeHooks() {
|
|
|
564
226
|
if (kept.length === 0) delete hooks[event];
|
|
565
227
|
else hooks[event] = kept;
|
|
566
228
|
}
|
|
567
|
-
if (removed)
|
|
229
|
+
if (removed) fs2.writeFileSync(configPath, JSON.stringify(file, null, 2) + "\n", "utf8");
|
|
568
230
|
return { configPath, removed };
|
|
569
231
|
}
|
|
570
|
-
function
|
|
232
|
+
function codexHooksInstalled(configPath = codexHooksPath()) {
|
|
571
233
|
try {
|
|
572
|
-
const
|
|
573
|
-
return Object.values(
|
|
234
|
+
const file = JSON.parse(fs2.readFileSync(configPath, "utf8"));
|
|
235
|
+
return Object.values(file.hooks ?? {}).some((groups) => groups.some(isOurs));
|
|
574
236
|
} catch {
|
|
575
237
|
return false;
|
|
576
238
|
}
|
|
577
239
|
}
|
|
578
240
|
|
|
579
241
|
// src/guard.ts
|
|
580
|
-
import
|
|
581
|
-
|
|
582
|
-
// src/store.ts
|
|
583
|
-
import fs5 from "fs";
|
|
584
|
-
import path4 from "path";
|
|
585
|
-
import { createRequire as createRequire2 } from "module";
|
|
586
|
-
var nodeRequire = createRequire2(import.meta.url);
|
|
587
|
-
function sqliteCtor() {
|
|
588
|
-
return nodeRequire("node:sqlite").DatabaseSync;
|
|
589
|
-
}
|
|
590
|
-
var SCHEMA_VERSION = 3;
|
|
591
|
-
var SCHEMA = `
|
|
592
|
-
CREATE TABLE IF NOT EXISTS calls (
|
|
593
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
594
|
-
session_id TEXT NOT NULL,
|
|
595
|
-
tool_name TEXT NOT NULL,
|
|
596
|
-
args_hash TEXT NOT NULL,
|
|
597
|
-
args_json TEXT NOT NULL,
|
|
598
|
-
output_hash TEXT,
|
|
599
|
-
file_path TEXT,
|
|
600
|
-
status TEXT NOT NULL,
|
|
601
|
-
ts INTEGER NOT NULL
|
|
602
|
-
);
|
|
603
|
-
CREATE INDEX IF NOT EXISTS idx_calls_session ON calls(session_id, ts);
|
|
604
|
-
CREATE INDEX IF NOT EXISTS idx_calls_sig ON calls(session_id, tool_name, args_hash, ts);
|
|
605
|
-
CREATE INDEX IF NOT EXISTS idx_calls_out ON calls(session_id, tool_name, output_hash, ts);
|
|
606
|
-
CREATE INDEX IF NOT EXISTS idx_calls_time ON calls(ts);
|
|
607
|
-
CREATE TABLE IF NOT EXISTS events (
|
|
608
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
609
|
-
session_id TEXT NOT NULL,
|
|
610
|
-
kind TEXT NOT NULL,
|
|
611
|
-
meta_json TEXT NOT NULL,
|
|
612
|
-
ts INTEGER NOT NULL
|
|
613
|
-
);
|
|
614
|
-
CREATE INDEX IF NOT EXISTS idx_events_kind ON events(session_id, kind, ts);
|
|
615
|
-
CREATE INDEX IF NOT EXISTS idx_events_time ON events(ts);
|
|
616
|
-
CREATE TABLE IF NOT EXISTS blocks (
|
|
617
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
618
|
-
session_id TEXT NOT NULL,
|
|
619
|
-
tool_name TEXT NOT NULL,
|
|
620
|
-
kind TEXT NOT NULL,
|
|
621
|
-
ts INTEGER NOT NULL,
|
|
622
|
-
feedback TEXT
|
|
623
|
-
);
|
|
624
|
-
CREATE INDEX IF NOT EXISTS idx_blocks_session ON blocks(session_id, ts);
|
|
625
|
-
CREATE TABLE IF NOT EXISTS meta (
|
|
626
|
-
k TEXT PRIMARY KEY,
|
|
627
|
-
v TEXT NOT NULL
|
|
628
|
-
);
|
|
629
|
-
`;
|
|
630
|
-
var db = null;
|
|
631
|
-
function openDb() {
|
|
632
|
-
if (db) return db;
|
|
633
|
-
const file = stateDbPath();
|
|
634
|
-
fs5.mkdirSync(path4.dirname(file), { recursive: true });
|
|
635
|
-
const d = new (sqliteCtor())(file);
|
|
636
|
-
d.exec("PRAGMA journal_mode = WAL;");
|
|
637
|
-
migrate(d);
|
|
638
|
-
db = d;
|
|
639
|
-
return db;
|
|
640
|
-
}
|
|
641
|
-
function migrate(d) {
|
|
642
|
-
d.exec("CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY, v TEXT NOT NULL);");
|
|
643
|
-
const row = d.prepare("SELECT v FROM meta WHERE k = 'schema_version'").get();
|
|
644
|
-
const version2 = row ? Number(row.v) : 0;
|
|
645
|
-
if (version2 === SCHEMA_VERSION) {
|
|
646
|
-
d.exec(SCHEMA);
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
if (version2 === 2) {
|
|
650
|
-
d.exec(SCHEMA);
|
|
651
|
-
try {
|
|
652
|
-
d.exec("ALTER TABLE blocks ADD COLUMN feedback TEXT");
|
|
653
|
-
} catch {
|
|
654
|
-
}
|
|
655
|
-
} else {
|
|
656
|
-
d.exec("DROP TABLE IF EXISTS calls; DROP TABLE IF EXISTS events; DROP TABLE IF EXISTS blocks;");
|
|
657
|
-
d.exec(SCHEMA);
|
|
658
|
-
}
|
|
659
|
-
d.prepare(
|
|
660
|
-
"INSERT INTO meta (k, v) VALUES ('schema_version', ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v"
|
|
661
|
-
).run(String(SCHEMA_VERSION));
|
|
662
|
-
}
|
|
663
|
-
function recordCall(call) {
|
|
664
|
-
openDb().prepare(
|
|
665
|
-
"INSERT INTO calls (session_id, tool_name, args_hash, args_json, output_hash, file_path, status, ts) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
666
|
-
).run(
|
|
667
|
-
call.sessionId,
|
|
668
|
-
call.toolName,
|
|
669
|
-
call.argsHash,
|
|
670
|
-
call.argsJson,
|
|
671
|
-
call.outputHash,
|
|
672
|
-
call.filePath,
|
|
673
|
-
call.status,
|
|
674
|
-
call.ts ?? Date.now()
|
|
675
|
-
);
|
|
676
|
-
}
|
|
677
|
-
function callsSince(sessionId, sinceTs, limit = 500) {
|
|
678
|
-
return openDb().prepare(
|
|
679
|
-
"SELECT tool_name, args_hash, args_json, output_hash, file_path, status, ts FROM calls WHERE session_id = ? AND ts >= ? ORDER BY ts ASC LIMIT ?"
|
|
680
|
-
).all(sessionId, sinceTs, limit);
|
|
681
|
-
}
|
|
682
|
-
function recordEvent(sessionId, kind, meta, ts = Date.now()) {
|
|
683
|
-
openDb().prepare("INSERT INTO events (session_id, kind, meta_json, ts) VALUES (?, ?, ?, ?)").run(sessionId, kind, JSON.stringify(meta), ts);
|
|
684
|
-
}
|
|
685
|
-
function countEvents(sessionId, kinds, sinceTs) {
|
|
686
|
-
const placeholders = kinds.map(() => "?").join(",");
|
|
687
|
-
const row = openDb().prepare(
|
|
688
|
-
`SELECT COUNT(*) AS n FROM events WHERE session_id = ? AND kind IN (${placeholders}) AND ts >= ?`
|
|
689
|
-
).get(sessionId, ...kinds, sinceTs);
|
|
690
|
-
return Number(row?.n ?? 0);
|
|
691
|
-
}
|
|
692
|
-
function oldestEventTs(sessionId, kinds, sinceTs) {
|
|
693
|
-
const placeholders = kinds.map(() => "?").join(",");
|
|
694
|
-
const row = openDb().prepare(
|
|
695
|
-
`SELECT MIN(ts) AS m FROM events WHERE session_id = ? AND kind IN (${placeholders}) AND ts >= ?`
|
|
696
|
-
).get(sessionId, ...kinds, sinceTs);
|
|
697
|
-
return row?.m ?? null;
|
|
698
|
-
}
|
|
699
|
-
function recordBlock(sessionId, toolName, kind, ts = Date.now()) {
|
|
700
|
-
const info = openDb().prepare("INSERT INTO blocks (session_id, tool_name, kind, ts) VALUES (?, ?, ?, ?)").run(sessionId, toolName, kind, ts);
|
|
701
|
-
return Number(info.lastInsertRowid);
|
|
702
|
-
}
|
|
703
|
-
function listBlocks(limit = 20) {
|
|
704
|
-
return openDb().prepare("SELECT id, session_id, tool_name, kind, ts, feedback FROM blocks ORDER BY id DESC LIMIT ?").all(limit);
|
|
705
|
-
}
|
|
706
|
-
function setBlockFeedback(id, verdict) {
|
|
707
|
-
const info = openDb().prepare("UPDATE blocks SET feedback = ? WHERE id = ?").run(verdict, id);
|
|
708
|
-
return Number(info.changes) > 0;
|
|
709
|
-
}
|
|
710
|
-
function blockKindStats() {
|
|
711
|
-
const rows = openDb().prepare(
|
|
712
|
-
`SELECT kind, COUNT(*) AS n,
|
|
713
|
-
SUM(CASE WHEN feedback = 'fp' THEN 1 ELSE 0 END) AS fp,
|
|
714
|
-
SUM(CASE WHEN feedback = 'tp' THEN 1 ELSE 0 END) AS tp
|
|
715
|
-
FROM blocks GROUP BY kind ORDER BY n DESC`
|
|
716
|
-
).all();
|
|
717
|
-
return rows.map((r) => ({ kind: r.kind, n: Number(r.n), fp: Number(r.fp), tp: Number(r.tp) }));
|
|
718
|
-
}
|
|
719
|
-
function countBlocks(sessionId, sinceTs) {
|
|
720
|
-
const row = openDb().prepare("SELECT COUNT(*) AS n FROM blocks WHERE session_id = ? AND ts >= ?").get(sessionId, sinceTs);
|
|
721
|
-
return Number(row?.n ?? 0);
|
|
722
|
-
}
|
|
723
|
-
function getMeta(key) {
|
|
724
|
-
const row = openDb().prepare("SELECT v FROM meta WHERE k = ?").get(key);
|
|
725
|
-
return row?.v;
|
|
726
|
-
}
|
|
727
|
-
function setMeta(key, value) {
|
|
728
|
-
openDb().prepare("INSERT INTO meta (k, v) VALUES (?, ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v").run(key, value);
|
|
729
|
-
}
|
|
730
|
-
function knownSessions(limit = 5) {
|
|
731
|
-
return openDb().prepare(
|
|
732
|
-
`SELECT session_id, MAX(last_ts) AS last_ts, SUM(n) AS n FROM (
|
|
733
|
-
SELECT session_id, MAX(ts) AS last_ts, COUNT(*) AS n FROM calls GROUP BY session_id
|
|
734
|
-
UNION ALL
|
|
735
|
-
SELECT session_id, MAX(ts) AS last_ts, COUNT(*) AS n FROM events GROUP BY session_id
|
|
736
|
-
) GROUP BY session_id ORDER BY last_ts DESC LIMIT ?`
|
|
737
|
-
).all(limit);
|
|
738
|
-
}
|
|
739
|
-
function buildStatus() {
|
|
740
|
-
const d = openDb();
|
|
741
|
-
const now = Date.now();
|
|
742
|
-
const day = now - 864e5;
|
|
743
|
-
const calls24h = Number(d.prepare("SELECT COUNT(*) AS n FROM calls WHERE ts >= ?").get(day).n);
|
|
744
|
-
const blocks24h = d.prepare("SELECT kind, COUNT(*) AS n FROM blocks WHERE ts >= ? GROUP BY kind").all(day).map((r) => ({ kind: r.kind, n: Number(r.n) }));
|
|
745
|
-
const topRepeated = d.prepare(
|
|
746
|
-
"SELECT tool_name, args_hash, COUNT(*) AS n FROM calls WHERE ts >= ? GROUP BY session_id, tool_name, args_hash ORDER BY n DESC LIMIT 5"
|
|
747
|
-
).all(day).map((r) => ({ tool_name: r.tool_name, args_hash: r.args_hash, n: Number(r.n) }));
|
|
748
|
-
const noGainPairs = d.prepare(
|
|
749
|
-
"SELECT tool_name, COUNT(DISTINCT session_id || ':' || output_hash) AS n FROM calls WHERE ts >= ? AND output_hash IS NOT NULL GROUP BY tool_name ORDER BY n DESC LIMIT 5"
|
|
750
|
-
).all(day).map((r) => ({ tool_name: r.tool_name, n: Number(r.n) }));
|
|
751
|
-
const events24h = d.prepare("SELECT kind, COUNT(*) AS n FROM events WHERE ts >= ? GROUP BY kind").all(day).map((r) => ({ kind: r.kind, n: Number(r.n) }));
|
|
752
|
-
const lastActivity = d.prepare("SELECT MAX(m) AS m FROM (SELECT MAX(ts) AS m FROM calls UNION ALL SELECT MAX(ts) AS m FROM events)").get();
|
|
753
|
-
return {
|
|
754
|
-
calls24h,
|
|
755
|
-
blocks24h,
|
|
756
|
-
topRepeated,
|
|
757
|
-
noGainPairs,
|
|
758
|
-
events24h,
|
|
759
|
-
lastActivityTs: lastActivity?.m ?? null
|
|
760
|
-
};
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
// src/events.ts
|
|
764
|
-
import { createHash } from "crypto";
|
|
765
|
-
function pickString(payload, keys) {
|
|
766
|
-
for (const k of keys) {
|
|
767
|
-
const v = payload[k];
|
|
768
|
-
if (typeof v === "string" && v) return v;
|
|
769
|
-
}
|
|
770
|
-
return "";
|
|
771
|
-
}
|
|
772
|
-
function pickField(payload, keys) {
|
|
773
|
-
for (const k of keys) {
|
|
774
|
-
if (payload[k] !== void 0) return payload[k];
|
|
775
|
-
}
|
|
776
|
-
return void 0;
|
|
777
|
-
}
|
|
778
|
-
function collapseWs(s) {
|
|
779
|
-
return s.replace(/\s+/g, " ").trim();
|
|
780
|
-
}
|
|
781
|
-
function sortDeep(value, collapseStrings) {
|
|
782
|
-
if (Array.isArray(value)) return value.map((x) => sortDeep(x, collapseStrings));
|
|
783
|
-
if (value !== null && typeof value === "object") {
|
|
784
|
-
const out = {};
|
|
785
|
-
for (const k of Object.keys(value).sort()) {
|
|
786
|
-
out[k] = sortDeep(value[k], collapseStrings);
|
|
787
|
-
}
|
|
788
|
-
return out;
|
|
789
|
-
}
|
|
790
|
-
if (value === void 0) return null;
|
|
791
|
-
if (typeof value === "string") return collapseStrings ? collapseWs(value) : value;
|
|
792
|
-
return value;
|
|
793
|
-
}
|
|
794
|
-
var WHITESPACE_SENSITIVE = /* @__PURE__ */ new Set(["Shell", "Bash", "Grep", "Glob", "FetchURL", "SearchWeb", "ReadFile", "WriteFile", "StrReplaceFile", "Edit", "Write", "MultiEdit"]);
|
|
795
|
-
function fingerprint(tool, args) {
|
|
796
|
-
let v = args ?? {};
|
|
797
|
-
if (v === null || typeof v !== "object") v = { value: v ?? null };
|
|
798
|
-
const normalized = sortDeep(v, !WHITESPACE_SENSITIVE.has(tool));
|
|
799
|
-
return createHash("sha256").update(JSON.stringify(normalized)).digest("hex").slice(0, 16);
|
|
800
|
-
}
|
|
801
|
-
function hashOutput(output) {
|
|
802
|
-
if (output === void 0 || output === null) return null;
|
|
803
|
-
let s;
|
|
804
|
-
if (typeof output === "string") s = output;
|
|
805
|
-
else {
|
|
806
|
-
try {
|
|
807
|
-
s = JSON.stringify(sortDeep(output, false));
|
|
808
|
-
} catch {
|
|
809
|
-
s = String(output);
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
s = collapseWs(s).slice(0, 4096);
|
|
813
|
-
if (!s) return null;
|
|
814
|
-
return createHash("sha256").update(s).digest("hex").slice(0, 16);
|
|
815
|
-
}
|
|
816
|
-
var FILE_KEYS = ["file_path", "filePath", "path", "file", "filename", "notebook_path", "target"];
|
|
817
|
-
function extractFile(args) {
|
|
818
|
-
if (args === null || typeof args !== "object") return null;
|
|
819
|
-
const obj = args;
|
|
820
|
-
for (const k of FILE_KEYS) {
|
|
821
|
-
const v = obj[k];
|
|
822
|
-
if (typeof v === "string" && v) return v;
|
|
823
|
-
}
|
|
824
|
-
return null;
|
|
825
|
-
}
|
|
826
|
-
function normalizeCall(payload, event, ts = Date.now()) {
|
|
827
|
-
const sessionId = pickString(payload, ["session_id", "sessionId", "session", "sessionID"]) || "unknown";
|
|
828
|
-
const tool = pickString(payload, ["tool_name", "toolName", "tool"]);
|
|
829
|
-
if (!tool) return null;
|
|
830
|
-
const args = pickField(payload, ["tool_input", "toolInput", "input"]) ?? {};
|
|
831
|
-
const outputKeys = ["tool_output", "toolOutput", "tool_response", "output", "result"];
|
|
832
|
-
const output = event === "PostToolUse" ? pickField(payload, outputKeys) : event === "PostToolUseFailure" ? pickField(payload, ["error", "error_message", ...outputKeys]) : void 0;
|
|
833
|
-
const argsJson = JSON.stringify(args).slice(0, 2048);
|
|
834
|
-
return {
|
|
835
|
-
sessionId,
|
|
836
|
-
tool,
|
|
837
|
-
args,
|
|
838
|
-
argsHash: fingerprint(tool, args),
|
|
839
|
-
argsJson,
|
|
840
|
-
outputHash: output !== void 0 ? hashOutput(output) : null,
|
|
841
|
-
filePath: extractFile(args),
|
|
842
|
-
status: event === "PostToolUseFailure" ? "failure" : "ok",
|
|
843
|
-
ts
|
|
844
|
-
};
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
// src/analysis.ts
|
|
848
|
-
var allow = [];
|
|
849
|
-
function isRepeatExempt(proposed, cfg) {
|
|
850
|
-
if (cfg.repeat.exemptPatterns.length === 0) return false;
|
|
851
|
-
let text;
|
|
852
|
-
try {
|
|
853
|
-
text = JSON.stringify(proposed.args ?? {});
|
|
854
|
-
} catch {
|
|
855
|
-
text = String(proposed.args);
|
|
856
|
-
}
|
|
857
|
-
for (const p of cfg.repeat.exemptPatterns) {
|
|
858
|
-
try {
|
|
859
|
-
if (new RegExp(p).test(text)) return true;
|
|
860
|
-
} catch {
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
return false;
|
|
864
|
-
}
|
|
865
|
-
function analyzeRepetition(history, proposed, cfg, now) {
|
|
866
|
-
if (!cfg.repeat.enabled) return allow;
|
|
867
|
-
const watched = cfg.repeat.watch.includes(proposed.tool) || proposed.tool in cfg.repeat.thresholds;
|
|
868
|
-
if (!watched) return allow;
|
|
869
|
-
if (isRepeatExempt(proposed, cfg)) return allow;
|
|
870
|
-
const threshold = cfg.repeat.thresholds[proposed.tool] ?? cfg.repeat.maxRepeats;
|
|
871
|
-
const since = now - cfg.repeat.windowMinutes * 6e4;
|
|
872
|
-
const n = history.filter(
|
|
873
|
-
(r) => r.tool_name === proposed.tool && r.args_hash === proposed.argsHash && r.ts >= since
|
|
874
|
-
).length;
|
|
875
|
-
if (n >= threshold) {
|
|
876
|
-
return [
|
|
877
|
-
{
|
|
878
|
-
kind: "repeat",
|
|
879
|
-
severity: "block",
|
|
880
|
-
tool: proposed.tool,
|
|
881
|
-
message: `"${proposed.tool}" has already been called ${n} times with identical arguments in the last ${cfg.repeat.windowMinutes} minutes. The previous results are already in context \u2014 use them instead of re-running. If a retry is genuinely required, change the arguments or state why the previous result is insufficient.`,
|
|
882
|
-
evidence: `signature count=${n}, threshold=${threshold}`
|
|
883
|
-
}
|
|
884
|
-
];
|
|
885
|
-
}
|
|
886
|
-
if (cfg.repeat.warnAt > 0 && n >= cfg.repeat.warnAt) {
|
|
887
|
-
return [
|
|
888
|
-
{
|
|
889
|
-
kind: "repeat",
|
|
890
|
-
severity: "warn",
|
|
891
|
-
tool: proposed.tool,
|
|
892
|
-
message: `"${proposed.tool}" has been called ${n} times with identical arguments \u2014 the result is already in context. Identical calls are blocked at ${threshold}; make sure any retry adds new information.`,
|
|
893
|
-
evidence: `signature count=${n}, warnAt=${cfg.repeat.warnAt}`
|
|
894
|
-
}
|
|
895
|
-
];
|
|
896
|
-
}
|
|
897
|
-
return allow;
|
|
898
|
-
}
|
|
899
|
-
function analyzeCycles(history, cfg, now) {
|
|
900
|
-
if (!cfg.cycle.enabled) return allow;
|
|
901
|
-
const since = now - cfg.cycle.windowMinutes * 6e4;
|
|
902
|
-
const recent = history.filter((r) => r.ts >= since).slice(-16).map((r) => `${r.tool_name}:${r.args_hash}`);
|
|
903
|
-
if (recent.length < 8) return allow;
|
|
904
|
-
const findings = [];
|
|
905
|
-
for (let period = 1; period <= 3; period++) {
|
|
906
|
-
const minReps = period === 1 ? 5 : 3;
|
|
907
|
-
const needed = period * minReps;
|
|
908
|
-
const tail = recent.slice(-needed);
|
|
909
|
-
if (tail.length < needed) continue;
|
|
910
|
-
const base = tail.slice(0, period);
|
|
911
|
-
let isCycle = true;
|
|
912
|
-
for (let i = period; i < tail.length; i++) {
|
|
913
|
-
if (tail[i] !== base[i % period]) {
|
|
914
|
-
isCycle = false;
|
|
915
|
-
break;
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
if (isCycle) {
|
|
919
|
-
const desc = period === 1 ? `the same call (${base[0]})` : `a ${period}-step cycle (${base.map((s) => s.split(":")[0]).join(" \u2192 ")})`;
|
|
920
|
-
findings.push({
|
|
921
|
-
kind: "cycle",
|
|
922
|
-
severity: "block",
|
|
923
|
-
tool: base[0]?.split(":")[0],
|
|
924
|
-
message: `Loop detected: the agent has repeated ${desc} ${minReps}+ times in a row without progress. Stop re-running this sequence. Re-read the results already in context, reassess the approach, and either proceed differently or end the turn with a summary.`,
|
|
925
|
-
evidence: `period=${period}, reps>=${minReps}`
|
|
926
|
-
});
|
|
927
|
-
break;
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
return findings;
|
|
931
|
-
}
|
|
932
|
-
function analyzeNoGain(history, cfg, now) {
|
|
933
|
-
if (!cfg.noGain.enabled) return allow;
|
|
934
|
-
const since = now - cfg.noGain.windowMinutes * 6e4;
|
|
935
|
-
const byPair = /* @__PURE__ */ new Map();
|
|
936
|
-
for (const r of history) {
|
|
937
|
-
if (r.ts < since || !r.output_hash) continue;
|
|
938
|
-
const key = `${r.tool_name}:${r.output_hash}`;
|
|
939
|
-
byPair.set(key, (byPair.get(key) ?? 0) + 1);
|
|
940
|
-
}
|
|
941
|
-
const findings = [];
|
|
942
|
-
for (const [key, n] of byPair) {
|
|
943
|
-
if (n < cfg.noGain.warnAt) continue;
|
|
944
|
-
const tool = key.split(":")[0];
|
|
945
|
-
if (n >= cfg.noGain.blockAt) {
|
|
946
|
-
findings.push({
|
|
947
|
-
kind: "noGain",
|
|
948
|
-
severity: "block",
|
|
949
|
-
tool,
|
|
950
|
-
message: `No-progress loop: ${tool} returned the exact same output ${n} times despite different arguments. You are not gaining new information. Stop calling ${tool}, analyze the result you already have, change strategy, or report your findings.`,
|
|
951
|
-
evidence: `tool=${tool} identical_output_count=${n}`
|
|
952
|
-
});
|
|
953
|
-
} else {
|
|
954
|
-
findings.push({
|
|
955
|
-
kind: "noGain",
|
|
956
|
-
severity: "warn",
|
|
957
|
-
tool,
|
|
958
|
-
message: `${tool} has returned the same output ${n} times \u2014 verify you are not repeating work.`,
|
|
959
|
-
evidence: `tool=${tool} identical_output_count=${n}`
|
|
960
|
-
});
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
return findings.slice(0, 2);
|
|
964
|
-
}
|
|
965
|
-
function editToolSet(cfg) {
|
|
966
|
-
return editTools(cfg);
|
|
967
|
-
}
|
|
968
|
-
function analyzeNoProgress(history, proposed, cfg, now) {
|
|
969
|
-
if (!cfg.noProgress.enabled) return allow;
|
|
970
|
-
if (editToolSet(cfg).has(proposed.tool)) return allow;
|
|
971
|
-
const since = now - cfg.noProgress.windowMinutes * 6e4;
|
|
972
|
-
const tools = editToolSet(cfg);
|
|
973
|
-
let lastEditTs = -1;
|
|
974
|
-
for (const r of history) {
|
|
975
|
-
if (r.ts < since) continue;
|
|
976
|
-
if (tools.has(r.tool_name) && r.status === "ok") lastEditTs = Math.max(lastEditTs, r.ts);
|
|
977
|
-
}
|
|
978
|
-
const stretch = history.filter((r) => r.ts >= since && r.ts > lastEditTs).length;
|
|
979
|
-
if (stretch < cfg.noProgress.warnAt) return allow;
|
|
980
|
-
if (stretch >= cfg.noProgress.blockAt) {
|
|
981
|
-
return [
|
|
982
|
-
{
|
|
983
|
-
kind: "noProgress",
|
|
984
|
-
severity: "block",
|
|
985
|
-
tool: proposed.tool,
|
|
986
|
-
message: `No progress: ${stretch} tool calls in the last ${cfg.noProgress.windowMinutes} minutes with no successful file edit landing. You are circling, not converging. Stop, pick the single most valuable next change, make it deliberately, or report what is blocking you.`,
|
|
987
|
-
evidence: `stretch=${stretch} warnAt=${cfg.noProgress.warnAt} blockAt=${cfg.noProgress.blockAt}`
|
|
988
|
-
}
|
|
989
|
-
];
|
|
990
|
-
}
|
|
991
|
-
return [
|
|
992
|
-
{
|
|
993
|
-
kind: "noProgress",
|
|
994
|
-
severity: "warn",
|
|
995
|
-
tool: proposed.tool,
|
|
996
|
-
message: `${stretch} calls without a landed edit recently \u2014 make sure the next step actually produces a change.`,
|
|
997
|
-
evidence: `stretch=${stretch}`
|
|
998
|
-
}
|
|
999
|
-
];
|
|
1000
|
-
}
|
|
1001
|
-
function analyzeChurn(history, cfg, now) {
|
|
1002
|
-
if (!cfg.churn.enabled) return allow;
|
|
1003
|
-
const since = now - cfg.churn.windowMinutes * 6e4;
|
|
1004
|
-
const tools = editTools(cfg);
|
|
1005
|
-
const byFile = /* @__PURE__ */ new Map();
|
|
1006
|
-
for (const r of history) {
|
|
1007
|
-
if (r.ts < since || !r.file_path || !tools.has(r.tool_name)) continue;
|
|
1008
|
-
byFile.set(r.file_path, (byFile.get(r.file_path) ?? 0) + 1);
|
|
1009
|
-
}
|
|
1010
|
-
const findings = [];
|
|
1011
|
-
for (const [file, n] of byFile) {
|
|
1012
|
-
if (n >= cfg.churn.blockAt) {
|
|
1013
|
-
findings.push({
|
|
1014
|
-
kind: "churn",
|
|
1015
|
-
severity: "block",
|
|
1016
|
-
tool: "edit",
|
|
1017
|
-
message: `Edit churn: ${file} has been modified ${n} times in the last ${cfg.churn.windowMinutes} minutes without converging. Stop editing. Re-read the file and the error output, form an explicit hypothesis about why previous fixes failed, then make a single deliberate change \u2014 or ask the user for help.`,
|
|
1018
|
-
evidence: `file=${file} edits=${n}`
|
|
1019
|
-
});
|
|
1020
|
-
} else if (n >= cfg.churn.warnAt) {
|
|
1021
|
-
findings.push({
|
|
1022
|
-
kind: "churn",
|
|
1023
|
-
severity: "warn",
|
|
1024
|
-
tool: "edit",
|
|
1025
|
-
message: `${file} has been edited ${n} times recently \u2014 step back and verify your approach before editing again.`,
|
|
1026
|
-
evidence: `file=${file} edits=${n}`
|
|
1027
|
-
});
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
return findings.sort((a, b) => a.severity === b.severity ? 0 : a.severity === "block" ? -1 : 1).slice(0, 1);
|
|
1031
|
-
}
|
|
1032
|
-
function analyzeExplore(history, proposed, cfg, now) {
|
|
1033
|
-
if (!cfg.explore.enabled) return allow;
|
|
1034
|
-
const passive = /* @__PURE__ */ new Set([...readTools(cfg), ...searchTools(cfg)]);
|
|
1035
|
-
if (!passive.has(proposed.tool)) return allow;
|
|
1036
|
-
const since = now - cfg.explore.windowMinutes * 6e4;
|
|
1037
|
-
let streak = 0;
|
|
1038
|
-
for (let i = history.length - 1; i >= 0; i--) {
|
|
1039
|
-
const r = history[i];
|
|
1040
|
-
if (r.ts < since || !passive.has(r.tool_name)) break;
|
|
1041
|
-
streak++;
|
|
1042
|
-
}
|
|
1043
|
-
if (streak < cfg.explore.warnAt) return allow;
|
|
1044
|
-
if (streak >= cfg.explore.blockAt) {
|
|
1045
|
-
return [
|
|
1046
|
-
{
|
|
1047
|
-
kind: "explore",
|
|
1048
|
-
severity: "block",
|
|
1049
|
-
tool: proposed.tool,
|
|
1050
|
-
message: `Exploration without implementation: ${streak} consecutive read/search calls with no action in the last ${cfg.explore.windowMinutes} minutes. You have gathered enough \u2014 pick the most valuable thing you learned and act on it (edit, run, or write). If nothing is actionable, summarize what you found and say so.`,
|
|
1051
|
-
evidence: `streak=${streak} blockAt=${cfg.explore.blockAt}`
|
|
1052
|
-
}
|
|
1053
|
-
];
|
|
1054
|
-
}
|
|
1055
|
-
return [
|
|
1056
|
-
{
|
|
1057
|
-
kind: "explore",
|
|
1058
|
-
severity: "warn",
|
|
1059
|
-
tool: proposed.tool,
|
|
1060
|
-
message: `${streak} consecutive read/search calls \u2014 make sure the next step acts on what you already learned.`,
|
|
1061
|
-
evidence: `streak=${streak}`
|
|
1062
|
-
}
|
|
1063
|
-
];
|
|
1064
|
-
}
|
|
1065
|
-
function fuzzyKey(tool, argsJson) {
|
|
1066
|
-
let text = argsJson;
|
|
1067
|
-
try {
|
|
1068
|
-
const obj = JSON.parse(argsJson);
|
|
1069
|
-
const parts = [];
|
|
1070
|
-
for (const v of Object.values(obj)) {
|
|
1071
|
-
if (typeof v === "string") parts.push(v);
|
|
1072
|
-
else if (v !== null && v !== void 0) parts.push(JSON.stringify(v));
|
|
1073
|
-
}
|
|
1074
|
-
if (parts.length > 0) text = parts.join("|");
|
|
1075
|
-
} catch {
|
|
1076
|
-
}
|
|
1077
|
-
return `${tool}:${text.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fa5]/g, "")}`;
|
|
1078
|
-
}
|
|
1079
|
-
function analyzeNearRepeat(history, cfg, now = Date.now()) {
|
|
1080
|
-
if (!cfg.nearRepeat.enabled) return allow;
|
|
1081
|
-
const since = now - cfg.nearRepeat.windowMinutes * 6e4;
|
|
1082
|
-
const byKey = /* @__PURE__ */ new Map();
|
|
1083
|
-
for (const r of history) {
|
|
1084
|
-
if (r.ts < since) continue;
|
|
1085
|
-
const key = fuzzyKey(r.tool_name, r.args_json);
|
|
1086
|
-
const cur = byKey.get(key) ?? { n: 0, tool: r.tool_name };
|
|
1087
|
-
cur.n++;
|
|
1088
|
-
byKey.set(key, cur);
|
|
1089
|
-
}
|
|
1090
|
-
const findings = [];
|
|
1091
|
-
for (const [, v] of byKey) {
|
|
1092
|
-
if (v.n >= cfg.nearRepeat.blockAt) {
|
|
1093
|
-
findings.push({
|
|
1094
|
-
kind: "nearRepeat",
|
|
1095
|
-
severity: "block",
|
|
1096
|
-
tool: v.tool,
|
|
1097
|
-
message: `Near-duplicate loop: ${v.tool} has been called ${v.n} times with arguments that differ only trivially (punctuation, case, spacing, order). You are not trying anything new. Use the results already in context or change the approach substantially.`,
|
|
1098
|
-
evidence: `fuzzy_count=${v.n} blockAt=${cfg.nearRepeat.blockAt}`
|
|
1099
|
-
});
|
|
1100
|
-
} else if (v.n >= cfg.nearRepeat.warnAt) {
|
|
1101
|
-
findings.push({
|
|
1102
|
-
kind: "nearRepeat",
|
|
1103
|
-
severity: "warn",
|
|
1104
|
-
tool: v.tool,
|
|
1105
|
-
message: `${v.tool} has ${v.n} near-identical calls recently \u2014 verify these calls differ meaningfully.`,
|
|
1106
|
-
evidence: `fuzzy_count=${v.n}`
|
|
1107
|
-
});
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
return findings.sort((a, b) => a.severity === b.severity ? 0 : a.severity === "block" ? -1 : 1).slice(0, 1);
|
|
1111
|
-
}
|
|
1112
|
-
function analyzeCall(history, proposed, cfg, now = Date.now()) {
|
|
1113
|
-
const findings = [
|
|
1114
|
-
...analyzeRepetition(history, proposed, cfg, now),
|
|
1115
|
-
...analyzeCycles(history, cfg, now),
|
|
1116
|
-
...analyzeNoGain(history, cfg, now),
|
|
1117
|
-
...analyzeChurn(history, cfg, now),
|
|
1118
|
-
...analyzeNoProgress(history, proposed, cfg, now),
|
|
1119
|
-
...analyzeNearRepeat(history, cfg, now),
|
|
1120
|
-
...analyzeExplore(history, proposed, cfg, now)
|
|
1121
|
-
];
|
|
1122
|
-
const rank = { block: 0, warn: 1 };
|
|
1123
|
-
findings.sort((a, b) => rank[a.severity] - rank[b.severity]);
|
|
1124
|
-
return { findings: findings.slice(0, 2) };
|
|
1125
|
-
}
|
|
242
|
+
import fs3 from "fs";
|
|
1126
243
|
|
|
1127
244
|
// src/policy.ts
|
|
1128
245
|
var KILL_SWITCH_MESSAGE = "[agent-guard] CIRCUIT BREAK: this session has hit the intervention limit. Stop making tool calls immediately. Do not attempt to work around this guard. Summarize what you have learned so far, state what remains blocked and why, and end your turn so the user can review the situation.";
|
|
@@ -1244,9 +361,9 @@ async function refreshPreciseUsage(cfg, env = process.env) {
|
|
|
1244
361
|
if (fresh) return fresh;
|
|
1245
362
|
const base = (cfg.preciseUrl || DEFAULT_URL).replace(/\/+$/, "");
|
|
1246
363
|
const key = env.KIMI_API_KEY.trim();
|
|
1247
|
-
for (const
|
|
364
|
+
for (const path4 of ["/usages", "/usage"]) {
|
|
1248
365
|
try {
|
|
1249
|
-
const res = await fetch(base +
|
|
366
|
+
const res = await fetch(base + path4, {
|
|
1250
367
|
headers: { Authorization: `Bearer ${key}`, "User-Agent": "KimiCLI/1.6" },
|
|
1251
368
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
1252
369
|
});
|
|
@@ -1383,204 +500,6 @@ function formatSnapshot(snap) {
|
|
|
1383
500
|
].join("\n");
|
|
1384
501
|
}
|
|
1385
502
|
|
|
1386
|
-
// src/checkpoint.ts
|
|
1387
|
-
import fs6 from "fs";
|
|
1388
|
-
import path5 from "path";
|
|
1389
|
-
function argSummary(argsJson, max = 100) {
|
|
1390
|
-
try {
|
|
1391
|
-
const obj = JSON.parse(argsJson);
|
|
1392
|
-
const parts = [];
|
|
1393
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
1394
|
-
const s = typeof v === "string" ? v : JSON.stringify(v);
|
|
1395
|
-
parts.push(`${k}=${s.length > 60 ? s.slice(0, 57) + "..." : s}`);
|
|
1396
|
-
}
|
|
1397
|
-
const joined = parts.join(", ");
|
|
1398
|
-
return joined.length > max ? joined.slice(0, max - 3) + "..." : joined || "{}";
|
|
1399
|
-
} catch {
|
|
1400
|
-
return argsJson.slice(0, max);
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
function buildBrief(sessionId, now = Date.now(), windowMs = 6 * 36e5, cfg = loadConfig()) {
|
|
1404
|
-
const calls = callsSince(sessionId, now - windowMs, 1e3);
|
|
1405
|
-
if (calls.length === 0) return "";
|
|
1406
|
-
const shells = shellTools(cfg);
|
|
1407
|
-
const edits = editTools(cfg);
|
|
1408
|
-
const reads = readTools(cfg);
|
|
1409
|
-
const searchesSet = searchTools(cfg);
|
|
1410
|
-
const files = /* @__PURE__ */ new Map();
|
|
1411
|
-
const commands = [];
|
|
1412
|
-
const searches = [];
|
|
1413
|
-
const failures = [];
|
|
1414
|
-
for (const r of calls) {
|
|
1415
|
-
const summary = argSummary(r.args_json, 90);
|
|
1416
|
-
if (edits.has(r.tool_name) && r.file_path) {
|
|
1417
|
-
const f = files.get(r.file_path) ?? { reads: 0, edits: 0 };
|
|
1418
|
-
f.edits++;
|
|
1419
|
-
files.set(r.file_path, f);
|
|
1420
|
-
} else if (r.file_path && reads.has(r.tool_name)) {
|
|
1421
|
-
const f = files.get(r.file_path) ?? { reads: 0, edits: 0 };
|
|
1422
|
-
f.reads++;
|
|
1423
|
-
files.set(r.file_path, f);
|
|
1424
|
-
}
|
|
1425
|
-
if (shells.has(r.tool_name)) commands.push(summary);
|
|
1426
|
-
if (searchesSet.has(r.tool_name)) searches.push(summary);
|
|
1427
|
-
if (r.status === "failure") failures.push(`${r.tool_name}: ${summary}`);
|
|
1428
|
-
}
|
|
1429
|
-
const lines = [];
|
|
1430
|
-
lines.push("## Observed activity (auto-captured by agent-guard)");
|
|
1431
|
-
lines.push("");
|
|
1432
|
-
if (files.size > 0) {
|
|
1433
|
-
lines.push("### Files touched");
|
|
1434
|
-
for (const [f, c] of [...files.entries()].slice(0, 25)) {
|
|
1435
|
-
lines.push(`- ${f} (read \xD7${c.reads}, edited \xD7${c.edits})`);
|
|
1436
|
-
}
|
|
1437
|
-
lines.push("");
|
|
1438
|
-
}
|
|
1439
|
-
if (commands.length > 0) {
|
|
1440
|
-
lines.push("### Commands run (most recent last)");
|
|
1441
|
-
for (const c of commands.slice(-10)) lines.push(`- ${c}`);
|
|
1442
|
-
lines.push("");
|
|
1443
|
-
}
|
|
1444
|
-
if (searches.length > 0) {
|
|
1445
|
-
lines.push("### Searches performed (results are already known \u2014 do not redo them)");
|
|
1446
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1447
|
-
for (const s of searches.slice(-15)) {
|
|
1448
|
-
if (seen.has(s)) continue;
|
|
1449
|
-
seen.add(s);
|
|
1450
|
-
lines.push(`- ${s}`);
|
|
1451
|
-
}
|
|
1452
|
-
lines.push("");
|
|
1453
|
-
}
|
|
1454
|
-
if (failures.length > 0) {
|
|
1455
|
-
lines.push("### Failed calls (avoid repeating these)");
|
|
1456
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1457
|
-
for (const f of failures.slice(-8)) {
|
|
1458
|
-
if (seen.has(f)) continue;
|
|
1459
|
-
seen.add(f);
|
|
1460
|
-
lines.push(`- ${f}`);
|
|
1461
|
-
}
|
|
1462
|
-
lines.push("");
|
|
1463
|
-
}
|
|
1464
|
-
lines.push(`Total recorded tool calls in window: ${calls.length}`);
|
|
1465
|
-
return lines.join("\n");
|
|
1466
|
-
}
|
|
1467
|
-
function captureCheckpoint(sessionId, reason, now = Date.now(), cfg = loadConfig()) {
|
|
1468
|
-
const brief = buildBrief(sessionId, now, 6 * 36e5, cfg);
|
|
1469
|
-
if (!brief) return null;
|
|
1470
|
-
const dir = path5.join(guardHome(), "checkpoints", sessionId.replace(/[^\w.-]/g, "_"));
|
|
1471
|
-
fs6.mkdirSync(dir, { recursive: true });
|
|
1472
|
-
const file = path5.join(dir, `${now}-${reason.replace(/[^\w-]/g, "_")}.md`);
|
|
1473
|
-
const header = [
|
|
1474
|
-
`# agent-guard checkpoint`,
|
|
1475
|
-
``,
|
|
1476
|
-
`- session: ${sessionId}`,
|
|
1477
|
-
`- time: ${new Date(now).toISOString()}`,
|
|
1478
|
-
`- reason: ${reason}`,
|
|
1479
|
-
``
|
|
1480
|
-
].join("\n");
|
|
1481
|
-
fs6.writeFileSync(file, header + brief + "\n", "utf8");
|
|
1482
|
-
recordEvent(sessionId, "checkpoint", { reason, file }, now);
|
|
1483
|
-
return { sessionId, path: file, brief, reason, ts: now };
|
|
1484
|
-
}
|
|
1485
|
-
function latestSessionId() {
|
|
1486
|
-
const sessions = knownSessions(1);
|
|
1487
|
-
return sessions[0]?.session_id ?? null;
|
|
1488
|
-
}
|
|
1489
|
-
function latestCheckpointFile(sessionId) {
|
|
1490
|
-
const base = path5.join(guardHome(), "checkpoints");
|
|
1491
|
-
if (!fs6.existsSync(base)) return null;
|
|
1492
|
-
let dir = sessionId ? path5.join(base, sessionId.replace(/[^\w.-]/g, "_")) : "";
|
|
1493
|
-
if (!dir || !fs6.existsSync(dir)) {
|
|
1494
|
-
const dirs = fs6.readdirSync(base).map((d) => ({ d, m: fs6.statSync(path5.join(base, d)).mtimeMs })).sort((a, b) => b.m - a.m);
|
|
1495
|
-
if (dirs.length === 0) return null;
|
|
1496
|
-
dir = path5.join(base, dirs[0].d);
|
|
1497
|
-
}
|
|
1498
|
-
const files = fs6.readdirSync(dir).filter((f) => f.endsWith(".md")).sort().reverse();
|
|
1499
|
-
return files[0] ? path5.join(dir, files[0]) : null;
|
|
1500
|
-
}
|
|
1501
|
-
function renderResumeBlock(brief, reason) {
|
|
1502
|
-
return [
|
|
1503
|
-
`<agent-guard-resume reason="${reason}">`,
|
|
1504
|
-
"You are resuming a task that was interrupted. Use the observed state below as verified",
|
|
1505
|
-
"prior knowledge. Do NOT re-explore files you have already read, do NOT redo searches",
|
|
1506
|
-
"listed here, and do NOT repeat failed calls. Continue from the last known state.",
|
|
1507
|
-
"",
|
|
1508
|
-
brief,
|
|
1509
|
-
"</agent-guard-resume>"
|
|
1510
|
-
].join("\n");
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
// src/verify.ts
|
|
1514
|
-
var DEFAULT_CLAIM_PATTERNS = [
|
|
1515
|
-
/\btests?\b.{0,40}\b(pass(?:ed|ing)?|green)\b/i,
|
|
1516
|
-
/\ball\b.{0,24}\btests?\b.{0,24}\bpass/i,
|
|
1517
|
-
/\bbuild\b.{0,30}\b(succeed(?:ed)?|passed|ok)\b/i,
|
|
1518
|
-
/\bcompil(?:es?|ed)\b.{0,20}\bsuccessfully\b/i,
|
|
1519
|
-
/\blint\b.{0,30}\b(clean|passed|no issues)\b/i,
|
|
1520
|
-
/\bfixed\b.{0,40}\b(all|every)\b/i,
|
|
1521
|
-
/测试(全部|都)?通过/,
|
|
1522
|
-
/全部(测试)?通过/,
|
|
1523
|
-
/构建成功/,
|
|
1524
|
-
/编译通过/,
|
|
1525
|
-
/零(错误|警告)/,
|
|
1526
|
-
/问题已全部解决/
|
|
1527
|
-
];
|
|
1528
|
-
var DEFAULT_EVIDENCE_PATTERNS = [
|
|
1529
|
-
/\b(test|tests|vitest|jest|mocha|pytest|cargo test|go test|make test)\b/i,
|
|
1530
|
-
/\b(npm|pnpm|yarn)\s+(run\s+)?(test|check)\b/i,
|
|
1531
|
-
/\b(mvn|gradle|sbt|dotnet\s+test)\b/i,
|
|
1532
|
-
/\b(tsc|pyright|mypy|eslint|biome|ruff|flake8|clippy)\b/i,
|
|
1533
|
-
/\b(build|compile|lint|check|verify)\b/i,
|
|
1534
|
-
/\bmake\b/i
|
|
1535
|
-
];
|
|
1536
|
-
function findClaims(text, cfg) {
|
|
1537
|
-
if (!text) return [];
|
|
1538
|
-
const patterns = cfg.verify.claimPatterns.length > 0 ? cfg.verify.claimPatterns.map((p) => new RegExp(p)) : DEFAULT_CLAIM_PATTERNS;
|
|
1539
|
-
const claims = [];
|
|
1540
|
-
for (const p of patterns) {
|
|
1541
|
-
const m = p.exec(text);
|
|
1542
|
-
if (m) {
|
|
1543
|
-
claims.push({
|
|
1544
|
-
pattern: String(p),
|
|
1545
|
-
snippet: text.slice(Math.max(0, (m.index ?? 0) - 40), (m.index ?? 0) + m[0].length + 40).replace(/\s+/g, " ").trim()
|
|
1546
|
-
});
|
|
1547
|
-
}
|
|
1548
|
-
if (claims.length >= 3) break;
|
|
1549
|
-
}
|
|
1550
|
-
return claims;
|
|
1551
|
-
}
|
|
1552
|
-
function hasEvidence(sessionId, cfg, now = Date.now()) {
|
|
1553
|
-
const vouched = getMeta(`vouched:${sessionId}`) === "1";
|
|
1554
|
-
if (vouched) return true;
|
|
1555
|
-
const since = now - cfg.verify.evidenceWindowMinutes * 6e4;
|
|
1556
|
-
const patterns = cfg.verify.evidencePatterns.length > 0 ? cfg.verify.evidencePatterns.map((p) => new RegExp(p)) : DEFAULT_EVIDENCE_PATTERNS;
|
|
1557
|
-
const shells = shellTools(cfg);
|
|
1558
|
-
const calls = callsSince(sessionId, since, 400);
|
|
1559
|
-
for (const r of calls) {
|
|
1560
|
-
if (r.status !== "ok") continue;
|
|
1561
|
-
if (!shells.has(r.tool_name)) continue;
|
|
1562
|
-
try {
|
|
1563
|
-
const args = JSON.parse(r.args_json);
|
|
1564
|
-
const cmd = args.command ?? "";
|
|
1565
|
-
for (const p of patterns) {
|
|
1566
|
-
if (p.test(cmd)) return true;
|
|
1567
|
-
}
|
|
1568
|
-
} catch {
|
|
1569
|
-
continue;
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
|
-
return false;
|
|
1573
|
-
}
|
|
1574
|
-
function hasRecentEdits(sessionId, cfg, now = Date.now()) {
|
|
1575
|
-
const since = now - cfg.verify.evidenceWindowMinutes * 6e4;
|
|
1576
|
-
const edits = editTools(cfg);
|
|
1577
|
-
return callsSince(sessionId, since, 400).some(
|
|
1578
|
-
(r) => r.status === "ok" && edits.has(r.tool_name)
|
|
1579
|
-
);
|
|
1580
|
-
}
|
|
1581
|
-
var HOOKS_STOP_BLOCK_REASON = "[agent-guard] Blocked (verify): this turn ended after successful file edits with no successful verification command (test/build/lint) in the session. Run your verification before claiming completion, or state explicitly why it cannot run here.";
|
|
1582
|
-
var WIRE_VERIFY_CORRECTIVE = "[agent-guard verification] Your final message claims tests/build pass, but no successful verification command was recorded in this session. Actually run the verification now and base your claims on real results, then restate the conclusion.";
|
|
1583
|
-
|
|
1584
503
|
// src/guard.ts
|
|
1585
504
|
function probeEnabled() {
|
|
1586
505
|
if (process.env.KIMI_GUARD_PROBE === "1") return true;
|
|
@@ -1592,7 +511,7 @@ function probeEnabled() {
|
|
|
1592
511
|
}
|
|
1593
512
|
function appendProbe(event, payload) {
|
|
1594
513
|
const line = JSON.stringify({ ts: Date.now(), event, payload }) + "\n";
|
|
1595
|
-
|
|
514
|
+
fs3.appendFileSync(probeLogPath(), line, "utf8");
|
|
1596
515
|
}
|
|
1597
516
|
async function readStdinJson() {
|
|
1598
517
|
const chunks = [];
|
|
@@ -1626,7 +545,7 @@ function processHookEvent(event, cfg, payload, now = Date.now()) {
|
|
|
1626
545
|
case "PostToolUseFailure":
|
|
1627
546
|
return handlePostTool(event, cfg, payload, sessionId, now);
|
|
1628
547
|
case "Stop": {
|
|
1629
|
-
if (cfg.harness
|
|
548
|
+
if (cfg.harness !== "kimi") recordEvent(sessionId, "turn", { origin: "stop" }, now);
|
|
1630
549
|
if (!cfg.verify.enabled || !cfg.verify.blockOnNoEvidence) return { code: 0 };
|
|
1631
550
|
if (!hasRecentEdits(sessionId, cfg, now)) return { code: 0 };
|
|
1632
551
|
if (hasEvidence(sessionId, cfg, now)) return { code: 0 };
|
|
@@ -1746,6 +665,7 @@ function handlePostTool(event, cfg, payload, sessionId, now) {
|
|
|
1746
665
|
argsHash: call.argsHash,
|
|
1747
666
|
argsJson: call.argsJson,
|
|
1748
667
|
outputHash: call.outputHash,
|
|
668
|
+
outputSample: call.outputSample,
|
|
1749
669
|
filePath: call.filePath,
|
|
1750
670
|
status: call.status,
|
|
1751
671
|
ts: now
|
|
@@ -1755,7 +675,7 @@ function handlePostTool(event, cfg, payload, sessionId, now) {
|
|
|
1755
675
|
|
|
1756
676
|
// src/hook.ts
|
|
1757
677
|
function encodeHint(event, harness, text) {
|
|
1758
|
-
if (harness
|
|
678
|
+
if (harness !== "kimi") {
|
|
1759
679
|
return JSON.stringify({ hookSpecificOutput: { hookEventName: event, additionalContext: text } });
|
|
1760
680
|
}
|
|
1761
681
|
return text;
|
|
@@ -1781,82 +701,9 @@ async function runHook(event, harness = "kimi") {
|
|
|
1781
701
|
}
|
|
1782
702
|
|
|
1783
703
|
// src/status.ts
|
|
1784
|
-
import
|
|
704
|
+
import fs4 from "fs";
|
|
1785
705
|
import { spawnSync } from "child_process";
|
|
1786
706
|
import pc from "picocolors";
|
|
1787
|
-
|
|
1788
|
-
// src/veto.ts
|
|
1789
|
-
function vetoKeyConfigured(env = process.env) {
|
|
1790
|
-
return Boolean(env.KIMI_GUARD_VETO_API_KEY?.trim());
|
|
1791
|
-
}
|
|
1792
|
-
function vetoBaseUrls(cfg, env = process.env) {
|
|
1793
|
-
return {
|
|
1794
|
-
baseUrl: env.KIMI_GUARD_VETO_BASE_URL?.trim() || cfg.baseUrl,
|
|
1795
|
-
model: env.KIMI_GUARD_VETO_MODEL?.trim() || cfg.model
|
|
1796
|
-
};
|
|
1797
|
-
}
|
|
1798
|
-
function collectVetoContext(sessionId, cfg, now = Date.now()) {
|
|
1799
|
-
const since = now - cfg.verify.evidenceWindowMinutes * 6e4;
|
|
1800
|
-
const calls = callsSince(sessionId, since, 400);
|
|
1801
|
-
const shells = shellTools(cfg);
|
|
1802
|
-
const recentCommands = [];
|
|
1803
|
-
const editedFiles = [];
|
|
1804
|
-
for (const r of calls.slice(-40)) {
|
|
1805
|
-
if (shells.has(r.tool_name)) {
|
|
1806
|
-
try {
|
|
1807
|
-
const args = JSON.parse(r.args_json);
|
|
1808
|
-
if (args.command) recentCommands.push(args.command.slice(0, 120));
|
|
1809
|
-
} catch {
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
if (r.file_path && editedFiles.length < 10) editedFiles.push(r.file_path);
|
|
1813
|
-
}
|
|
1814
|
-
return { sessionId, claims: [], goal: "", recentCommands: recentCommands.slice(-5), editedFiles };
|
|
1815
|
-
}
|
|
1816
|
-
var PROMPT_HEADER = "You are a false-positive detector for an AI-agent guardrail. An agent just finished its turn claiming completion, but the session's recorded command history contains NO successful verification command (test/build/lint). Decide whether blocking would be a FALSE POSITIVE \u2014 i.e. the agent has a legitimate reason why verification cannot run in this session.\nRules: base your vote ONLY on the facts below. A claim that verification is unnecessary or happens elsewhere is NOT by itself a reason to veto. Answer with EXACTLY one line:\nVETO: yes (false positive \u2014 allow the completion)\nVETO: no (block stands \u2014 the agent must actually run verification)\nDo not write anything else.\n\nFacts:\n";
|
|
1817
|
-
function buildVetoPrompt(ctx) {
|
|
1818
|
-
const lines = [];
|
|
1819
|
-
lines.push(`- user goal: ${ctx.goal.slice(0, 300) || "(unknown)"}`);
|
|
1820
|
-
lines.push(`- claims made by the agent:`);
|
|
1821
|
-
for (const c of ctx.claims.slice(0, 3)) lines.push(` "${c.snippet}"`);
|
|
1822
|
-
lines.push(`- recent commands the agent ran: ${ctx.recentCommands.length > 0 ? ctx.recentCommands.join(" ; ") : "(none)"}`);
|
|
1823
|
-
lines.push(`- files the agent edited: ${ctx.editedFiles.length > 0 ? ctx.editedFiles.join(", ") : "(none)"}`);
|
|
1824
|
-
lines.push("- recorded successful verification commands in session history: none");
|
|
1825
|
-
return PROMPT_HEADER + lines.join("\n");
|
|
1826
|
-
}
|
|
1827
|
-
async function castVetoVote(ctx, cfg, env = process.env) {
|
|
1828
|
-
if (!cfg.enabled || !vetoKeyConfigured(env)) return { vetoed: false, error: "veto disabled" };
|
|
1829
|
-
const calls = Number(getMeta(`veto_calls:${ctx.sessionId}`) ?? "0");
|
|
1830
|
-
if (calls >= cfg.maxCallsPerSession) return { vetoed: false, error: "session vote budget exhausted" };
|
|
1831
|
-
setMeta(`veto_calls:${ctx.sessionId}`, String(calls + 1));
|
|
1832
|
-
const { baseUrl, model } = vetoBaseUrls(cfg, env);
|
|
1833
|
-
const key = env.KIMI_GUARD_VETO_API_KEY.trim();
|
|
1834
|
-
try {
|
|
1835
|
-
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
1836
|
-
method: "POST",
|
|
1837
|
-
headers: {
|
|
1838
|
-
"Content-Type": "application/json",
|
|
1839
|
-
Authorization: `Bearer ${key}`
|
|
1840
|
-
},
|
|
1841
|
-
body: JSON.stringify({
|
|
1842
|
-
model,
|
|
1843
|
-
messages: [{ role: "user", content: buildVetoPrompt(ctx) }],
|
|
1844
|
-
max_tokens: 8,
|
|
1845
|
-
temperature: 0,
|
|
1846
|
-
stream: false
|
|
1847
|
-
}),
|
|
1848
|
-
signal: AbortSignal.timeout(cfg.timeoutMs)
|
|
1849
|
-
});
|
|
1850
|
-
if (!res.ok) return { vetoed: false, error: `http ${res.status}` };
|
|
1851
|
-
const data = await res.json();
|
|
1852
|
-
const raw = (data.choices?.[0]?.message?.content ?? "").trim();
|
|
1853
|
-
return { vetoed: /^VETO:\s*yes\b/i.test(raw), raw };
|
|
1854
|
-
} catch (err) {
|
|
1855
|
-
return { vetoed: false, error: err.message };
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
// src/status.ts
|
|
1860
707
|
function ok(msg) {
|
|
1861
708
|
console.log(`${pc.green("\u2713")} ${msg}`);
|
|
1862
709
|
}
|
|
@@ -1870,6 +717,7 @@ var CALIBRATION_KEYS = {
|
|
|
1870
717
|
repeat: "repeat.maxRepeats",
|
|
1871
718
|
nearRepeat: "nearRepeat.blockAt",
|
|
1872
719
|
noGain: "noGain.blockAt",
|
|
720
|
+
noGainFuzzy: "noGain.fuzzyBlockAt",
|
|
1873
721
|
churn: "churn.blockAt",
|
|
1874
722
|
noProgress: "noProgress.blockAt",
|
|
1875
723
|
explore: "explore.blockAt",
|
|
@@ -1924,7 +772,7 @@ function agentProcessRunning() {
|
|
|
1924
772
|
return r.stdout.split("\n").some((line) => {
|
|
1925
773
|
const l = line.trim();
|
|
1926
774
|
if (l.includes("agentguard") || l.includes("kimi-guard") || l.includes("kguard")) return false;
|
|
1927
|
-
return /(?:^|[/\s])(kimi|claude)(?:\s|$)/.test(l);
|
|
775
|
+
return /(?:^|[/\s])(kimi|claude|codex)(?:\s|$)/.test(l);
|
|
1928
776
|
});
|
|
1929
777
|
} catch {
|
|
1930
778
|
return false;
|
|
@@ -1993,17 +841,18 @@ function cmdDoctor() {
|
|
|
1993
841
|
`node ${process.versions.node} is too old (need >=22.13 for node:sqlite)`
|
|
1994
842
|
);
|
|
1995
843
|
try {
|
|
1996
|
-
|
|
1997
|
-
|
|
844
|
+
fs4.mkdirSync(guardHome(), { recursive: true });
|
|
845
|
+
fs4.accessSync(guardHome(), fs4.constants.W_OK);
|
|
1998
846
|
check(true, `state dir writable: ${guardHome()}`, "");
|
|
1999
847
|
openDb().prepare("SELECT 1").get();
|
|
2000
|
-
check(true, `state db opens
|
|
848
|
+
check(true, `state db opens: ${stateDbPath()}`, "");
|
|
2001
849
|
} catch (err) {
|
|
2002
850
|
check(false, "", `state dir/db problem: ${err.message}`);
|
|
2003
851
|
}
|
|
2004
852
|
const kimi = detectKimiConfig();
|
|
2005
853
|
const hasClaude = claudeDetected();
|
|
2006
|
-
|
|
854
|
+
const hasCodex = codexDetected();
|
|
855
|
+
if (kimi.exists || !hasClaude && !hasCodex) {
|
|
2007
856
|
check(
|
|
2008
857
|
kimi.exists,
|
|
2009
858
|
`kimi config found: ${kimi.path}`,
|
|
@@ -2024,7 +873,16 @@ function cmdDoctor() {
|
|
|
2024
873
|
} else {
|
|
2025
874
|
console.log(` ${pc.dim("-")} claude code not detected (skipped)`);
|
|
2026
875
|
}
|
|
2027
|
-
|
|
876
|
+
if (codexDetected()) {
|
|
877
|
+
check(
|
|
878
|
+
codexHooksInstalled(),
|
|
879
|
+
`[codex] hooks present in ${codexHooksPath()}`,
|
|
880
|
+
`[codex] hooks not installed \u2014 run: agentguard install --harness codex`
|
|
881
|
+
);
|
|
882
|
+
} else {
|
|
883
|
+
console.log(` ${pc.dim("-")} codex not detected (skipped)`);
|
|
884
|
+
}
|
|
885
|
+
const kimiConfigText = fs4.existsSync(kimi.path) ? fs4.readFileSync(kimi.path, "utf8") : "";
|
|
2028
886
|
const hasSecurityLayer = /kimi-boost managed|destructive|secret-guard|branch-guard|block-dangerous/i.test(kimiConfigText);
|
|
2029
887
|
if (hasSecurityLayer) {
|
|
2030
888
|
ok("security-layer hooks detected (authorization axis covered)");
|
|
@@ -2038,8 +896,8 @@ function cmdDoctor() {
|
|
|
2038
896
|
"agentguard is not on PATH \u2014 hook commands will fail-open. Install globally: npm i -g @shidesheng0218/agentguard"
|
|
2039
897
|
);
|
|
2040
898
|
const probeFile = probeLogPath();
|
|
2041
|
-
if (
|
|
2042
|
-
const lines =
|
|
899
|
+
if (fs4.existsSync(probeFile)) {
|
|
900
|
+
const lines = fs4.readFileSync(probeFile, "utf8").trim().split("\n").filter(Boolean);
|
|
2043
901
|
ok(`probe log has ${lines.length} samples (${probeFile})`);
|
|
2044
902
|
const keys = /* @__PURE__ */ new Map();
|
|
2045
903
|
for (const line of lines.slice(-50)) {
|
|
@@ -2055,7 +913,7 @@ function cmdDoctor() {
|
|
|
2055
913
|
} else {
|
|
2056
914
|
warn("no probe samples yet \u2014 run 'kguard probe on', use Kimi Code a bit, then 'kguard doctor'");
|
|
2057
915
|
}
|
|
2058
|
-
if (
|
|
916
|
+
if (fs4.existsSync(userConfigPath())) ok(`config present: ${userConfigPath()}`);
|
|
2059
917
|
else warn("no user config (defaults in effect) \u2014 run 'kguard config init' to create one");
|
|
2060
918
|
try {
|
|
2061
919
|
const normalizeMisses = Number(getMeta("normalize_misses") ?? "0");
|
|
@@ -2098,8 +956,8 @@ ${failures} check(s) failed.`);
|
|
|
2098
956
|
}
|
|
2099
957
|
|
|
2100
958
|
// src/wire/supervisor.ts
|
|
2101
|
-
import
|
|
2102
|
-
import
|
|
959
|
+
import fs5 from "fs";
|
|
960
|
+
import path3 from "path";
|
|
2103
961
|
|
|
2104
962
|
// src/wire/client.ts
|
|
2105
963
|
import { spawn } from "child_process";
|
|
@@ -2201,12 +1059,12 @@ var WireClient = class {
|
|
|
2201
1059
|
}
|
|
2202
1060
|
async request(method, params, timeoutMs = this.opts.requestTimeoutMs ?? 6e5) {
|
|
2203
1061
|
const id = this.id();
|
|
2204
|
-
const promise = new Promise((
|
|
1062
|
+
const promise = new Promise((resolve, reject) => {
|
|
2205
1063
|
const timer = setTimeout(() => {
|
|
2206
1064
|
this.pending.delete(id);
|
|
2207
1065
|
reject(new Error(`wire request timeout: ${method}`));
|
|
2208
1066
|
}, timeoutMs);
|
|
2209
|
-
this.pending.set(id, { resolve
|
|
1067
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
2210
1068
|
});
|
|
2211
1069
|
this.write({ jsonrpc: "2.0", method, id, params });
|
|
2212
1070
|
return promise;
|
|
@@ -2260,12 +1118,12 @@ function addUsage(acc, u) {
|
|
|
2260
1118
|
async function runSupervised(opts) {
|
|
2261
1119
|
const cfg = opts.config ?? loadConfig();
|
|
2262
1120
|
const runId = `run-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${process.pid}`;
|
|
2263
|
-
const logDir =
|
|
2264
|
-
|
|
2265
|
-
const logPath =
|
|
1121
|
+
const logDir = path3.join(guardHome(), "runs", runId);
|
|
1122
|
+
fs5.mkdirSync(logDir, { recursive: true });
|
|
1123
|
+
const logPath = path3.join(logDir, "wire.jsonl");
|
|
2266
1124
|
const rawLog = (direction, line) => {
|
|
2267
1125
|
try {
|
|
2268
|
-
|
|
1126
|
+
fs5.appendFileSync(logPath, JSON.stringify({ dir: direction, ts: Date.now(), line }) + "\n");
|
|
2269
1127
|
} catch {
|
|
2270
1128
|
}
|
|
2271
1129
|
};
|
|
@@ -2290,7 +1148,7 @@ async function runSupervised(opts) {
|
|
|
2290
1148
|
verifyRounds: 0,
|
|
2291
1149
|
vetoes: 0,
|
|
2292
1150
|
thinkingDominance: 0,
|
|
2293
|
-
reportPath:
|
|
1151
|
+
reportPath: path3.join(logDir, "report.json"),
|
|
2294
1152
|
logPath
|
|
2295
1153
|
};
|
|
2296
1154
|
let steersSent = 0;
|
|
@@ -2474,7 +1332,8 @@ async function runSupervised(opts) {
|
|
|
2474
1332
|
argsHash: fingerprint(call.name, call.args),
|
|
2475
1333
|
argsJson: JSON.stringify(call.args).slice(0, 2048),
|
|
2476
1334
|
outputHash: hashOutput(output),
|
|
2477
|
-
|
|
1335
|
+
outputSample: outputSampleOf(output),
|
|
1336
|
+
filePath: extractFile(call.args),
|
|
2478
1337
|
status: returnValue.is_error ? "failure" : "ok"
|
|
2479
1338
|
});
|
|
2480
1339
|
}
|
|
@@ -2631,12 +1490,12 @@ ${brief.brief}
|
|
|
2631
1490
|
report.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2632
1491
|
recordEvent(sessionId(), "run_end", { endReason: report.endReason, blocks: report.blocks.length });
|
|
2633
1492
|
try {
|
|
2634
|
-
|
|
1493
|
+
fs5.writeFileSync(report.reportPath, JSON.stringify(report, null, 2), "utf8");
|
|
2635
1494
|
} catch {
|
|
2636
1495
|
}
|
|
2637
1496
|
return report;
|
|
2638
1497
|
}
|
|
2639
|
-
function
|
|
1498
|
+
function extractFile(args) {
|
|
2640
1499
|
if (args === null || typeof args !== "object") return null;
|
|
2641
1500
|
const obj = args;
|
|
2642
1501
|
for (const k of ["file_path", "filePath", "path", "file", "filename"]) {
|
|
@@ -2670,15 +1529,17 @@ function formatReport(r) {
|
|
|
2670
1529
|
// src/cli.ts
|
|
2671
1530
|
var program = new Command();
|
|
2672
1531
|
program.name("agentguard").description("Runtime behavior guard for coding agents (Kimi Code CLI & Claude Code): loop detection, quota gates, checkpoints").version(version, "-V, --version", "print version");
|
|
2673
|
-
program.command("install").description("install hook rules into detected agent CLIs (Kimi Code config.toml and/or Claude Code settings.json)").option("--compat", "legacy-safe mode: only the 3 universally supported hook events (for older kimi-cli versions)").option("--harness <name>", "kimi | claude | all (default: auto-detect installed harnesses, fallback kimi)").action((opts) => {
|
|
1532
|
+
program.command("install").description("install hook rules into detected agent CLIs (Kimi Code config.toml and/or Claude Code settings.json)").option("--compat", "legacy-safe mode: only the 3 universally supported hook events (for older kimi-cli versions)").option("--harness <name>", "kimi | claude | codex | all (default: auto-detect installed harnesses, fallback kimi)").action((opts) => {
|
|
2674
1533
|
const which = opts.harness ?? "auto";
|
|
2675
|
-
const
|
|
2676
|
-
|
|
2677
|
-
if (!doKimi && !doClaude) {
|
|
1534
|
+
const known = ["kimi", "claude", "codex", "all", "auto"];
|
|
1535
|
+
if (!known.includes(which)) {
|
|
2678
1536
|
console.error(`unknown harness: ${which}`);
|
|
2679
1537
|
process.exitCode = 1;
|
|
2680
1538
|
return;
|
|
2681
1539
|
}
|
|
1540
|
+
const doKimi = which === "kimi" || which === "all" || which === "auto";
|
|
1541
|
+
const doClaude = which === "claude" || which === "all" || which === "auto" && claudeDetected();
|
|
1542
|
+
const doCodex = which === "codex" || which === "all" || which === "auto" && codexDetected();
|
|
2682
1543
|
if (doKimi) {
|
|
2683
1544
|
const r = installHooks("agentguard", Boolean(opts.compat));
|
|
2684
1545
|
console.log(`\u2713 [kimi] config: ${r.configPath}${r.created ? " (created)" : ""}`);
|
|
@@ -2694,9 +1555,16 @@ program.command("install").description("install hook rules into detected agent C
|
|
|
2694
1555
|
if (r.backupPath) console.log(`\u2713 [claude] backup: ${r.backupPath}`);
|
|
2695
1556
|
console.log(`\u2713 [claude] hooks ${r.updated ? "installed" : "already up to date"}${opts.compat ? " (compat)" : ""}`);
|
|
2696
1557
|
}
|
|
1558
|
+
if (doCodex) {
|
|
1559
|
+
const r = installCodexHooks("agentguard", Boolean(opts.compat));
|
|
1560
|
+
console.log(`\u2713 [codex] config: ${r.configPath}${r.created ? " (created)" : ""}`);
|
|
1561
|
+
if (r.backupPath) console.log(`\u2713 [codex] backup: ${r.backupPath}`);
|
|
1562
|
+
console.log(`\u2713 [codex] hooks ${r.updated ? "installed" : "already up to date"}${opts.compat ? " (compat)" : ""}`);
|
|
1563
|
+
console.log(" note: Codex hooks cover shell/apply_patch/local function tools; hosted tools (e.g. WebSearch) are not observable.");
|
|
1564
|
+
}
|
|
2697
1565
|
console.log(" restart the agent CLI (or /reload) to take effect.");
|
|
2698
1566
|
});
|
|
2699
|
-
program.command("uninstall").description("remove the managed hook entries from Kimi Code config.toml and Claude Code settings.json").option("--harness <name>", "kimi | claude | all (default: all)").action((opts) => {
|
|
1567
|
+
program.command("uninstall").description("remove the managed hook entries from Kimi Code config.toml and Claude Code settings.json").option("--harness <name>", "kimi | claude | codex | all (default: all)").action((opts) => {
|
|
2700
1568
|
const which = opts.harness ?? "all";
|
|
2701
1569
|
if (which === "kimi" || which === "all") {
|
|
2702
1570
|
const r = uninstallHooks();
|
|
@@ -2706,9 +1574,13 @@ program.command("uninstall").description("remove the managed hook entries from K
|
|
|
2706
1574
|
const r = uninstallClaudeHooks();
|
|
2707
1575
|
console.log(r.removed ? `\u2713 [claude] removed hooks from ${r.configPath}` : `[claude] no managed hooks found in ${r.configPath}`);
|
|
2708
1576
|
}
|
|
1577
|
+
if (which === "codex" || which === "all") {
|
|
1578
|
+
const r = uninstallCodexHooks();
|
|
1579
|
+
console.log(r.removed ? `\u2713 [codex] removed hooks from ${r.configPath}` : `[codex] no managed hooks found in ${r.configPath}`);
|
|
1580
|
+
}
|
|
2709
1581
|
});
|
|
2710
|
-
program.command("hook").argument("<event>", "hook event name, e.g. PreToolUse").description("hook entrypoint invoked by the agent CLI (reads JSON payload from stdin)").option("--harness <name>", "kimi | claude (default: kimi)", "kimi").action(async (event, opts) => {
|
|
2711
|
-
const harness = opts.harness === "claude" ? "claude" : "kimi";
|
|
1582
|
+
program.command("hook").argument("<event>", "hook event name, e.g. PreToolUse").description("hook entrypoint invoked by the agent CLI (reads JSON payload from stdin)").option("--harness <name>", "kimi | claude | codex (default: kimi)", "kimi").action(async (event, opts) => {
|
|
1583
|
+
const harness = opts.harness === "claude" ? "claude" : opts.harness === "codex" ? "codex" : "kimi";
|
|
2712
1584
|
process.exitCode = await runHook(event, harness);
|
|
2713
1585
|
});
|
|
2714
1586
|
program.command("status").description("guard activity: calls, interventions, sessions, budget windows").action(() => cmdStatus());
|
|
@@ -2745,11 +1617,11 @@ program.command("checkpoint").description("capture a research-state checkpoint f
|
|
|
2745
1617
|
});
|
|
2746
1618
|
program.command("resume").description("print a paste-ready context block built from the latest checkpoint").option("-f, --file <path>", "use a specific checkpoint file (defaults to the latest)").action((opts) => {
|
|
2747
1619
|
const file = opts.file ?? latestCheckpointFile();
|
|
2748
|
-
if (!file || !
|
|
1620
|
+
if (!file || !fs6.existsSync(file)) {
|
|
2749
1621
|
console.log("no checkpoints found \u2014 run 'kguard checkpoint' first");
|
|
2750
1622
|
return;
|
|
2751
1623
|
}
|
|
2752
|
-
const content =
|
|
1624
|
+
const content = fs6.readFileSync(file, "utf8");
|
|
2753
1625
|
const reason = /- reason: (.*)/.exec(content)?.[1] ?? "interrupted";
|
|
2754
1626
|
const idx = content.indexOf("## Observed activity");
|
|
2755
1627
|
const brief = idx >= 0 ? content.slice(idx) : content;
|
|
@@ -2806,11 +1678,11 @@ probe.command("off").action(() => {
|
|
|
2806
1678
|
console.log("\u2713 probe off");
|
|
2807
1679
|
});
|
|
2808
1680
|
probe.command("show").option("-n, --last <n>", "how many samples to show", "10").action((opts) => {
|
|
2809
|
-
if (!
|
|
1681
|
+
if (!fs6.existsSync(probeLogPath())) {
|
|
2810
1682
|
console.log("no probe samples yet \u2014 run 'kguard probe on' first");
|
|
2811
1683
|
return;
|
|
2812
1684
|
}
|
|
2813
|
-
const lines =
|
|
1685
|
+
const lines = fs6.readFileSync(probeLogPath(), "utf8").trim().split("\n").filter(Boolean);
|
|
2814
1686
|
for (const line of lines.slice(-Number(opts.last))) console.log(line);
|
|
2815
1687
|
});
|
|
2816
1688
|
var cfgCmd = program.command("config").description("manage the guard config.toml");
|
|
@@ -2828,12 +1700,29 @@ cfgCmd.command("get <key>").description("print one effective value, e.g. budget.
|
|
|
2828
1700
|
const value = key.split(".").reduce((acc, k) => acc && typeof acc === "object" ? acc[k] : void 0, cfg);
|
|
2829
1701
|
console.log(value === void 0 ? `<unset: ${key}>` : typeof value === "object" ? JSON.stringify(value) : String(value));
|
|
2830
1702
|
});
|
|
2831
|
-
program.command("run").description("supervised headless run:
|
|
1703
|
+
program.command("run").description("supervised headless run: loop guards, token metering, auto-checkpoints. Kimi: Wire protocol (in-process steer); Claude Code: claude -p stream-json supervision").argument("[prompt...]", "task prompt (or use --prompt)").option("-p, --prompt <text>", "task prompt").option("-e, --exec <command...>", "agent command to supervise (default: kimi --wire / claude)").option("--harness <name>", "kimi | claude (default: kimi; claude uses claude -p stream-json)").option("--max-steps <n>", "hard step cap (per turn)", "200").option("--max-minutes <n>", "hard wall-clock cap for the whole run", "30").option("--auto-resume <n>", "re-prompt with checkpoint brief after max_steps/kill-switch", "0").option("--max-verify-rounds <n>", "corrective rounds when the final message makes unbacked completion claims", "2").option("--no-steer", "disable soft mid-turn corrections").option("--max-steers <n>", "cap on steer injections", "5").option("--yolo", "auto-approve every approval request").option("--json", "print machine-readable report JSON").action(async (promptParts, opts) => {
|
|
2832
1704
|
const prompt = opts.prompt ?? promptParts.join(" ");
|
|
2833
1705
|
if (!prompt.trim()) {
|
|
2834
1706
|
console.error("error: a prompt is required (argument or --prompt)");
|
|
2835
1707
|
process.exit(1);
|
|
2836
1708
|
}
|
|
1709
|
+
if (opts.harness === "claude") {
|
|
1710
|
+
const { runClaudeSupervised } = await import("./claude-ZFYJRREP.js");
|
|
1711
|
+
const report2 = await runClaudeSupervised({
|
|
1712
|
+
prompt,
|
|
1713
|
+
command: opts.exec ?? ["claude"],
|
|
1714
|
+
maxSteps: Number(opts.maxSteps),
|
|
1715
|
+
maxMinutes: Number(opts.maxMinutes),
|
|
1716
|
+
autoResume: Number(opts.autoResume),
|
|
1717
|
+
maxVerifyRounds: Number(opts.maxVerifyRounds),
|
|
1718
|
+
approval: opts.yolo ? "approve" : "reject",
|
|
1719
|
+
json: Boolean(opts.json)
|
|
1720
|
+
});
|
|
1721
|
+
if (opts.json) console.log(JSON.stringify(report2, null, 2));
|
|
1722
|
+
else console.log(formatReport(report2));
|
|
1723
|
+
process.exitCode = report2.endReason === "finished" ? 0 : 2;
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
2837
1726
|
const report = await runSupervised({
|
|
2838
1727
|
prompt,
|
|
2839
1728
|
command: opts.exec ?? ["kimi", "--wire"],
|