@agentguard-run/burn 0.2.0 → 0.2.2
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 +38 -0
- package/README.md +47 -10
- package/dist/src/adapters/codex.js +4 -1
- package/dist/src/adapters/cursor.js +4 -1
- package/dist/src/cli.js +59 -24
- package/dist/src/gateway.d.ts +7 -0
- package/dist/src/gateway.js +18 -4
- package/dist/src/hook/pre-tool-use.d.ts +25 -1
- package/dist/src/hook/pre-tool-use.js +64 -16
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +17 -1
- package/dist/src/install.d.ts +29 -0
- package/dist/src/install.js +145 -0
- package/dist/src/override.d.ts +32 -0
- package/dist/src/override.js +72 -0
- package/dist/src/status.d.ts +8 -0
- package/dist/src/status.js +75 -5
- package/fixtures/codex-0.151.0-pretooluse.json +49 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.2 (2026-09-03)
|
|
4
|
+
|
|
5
|
+
- Codex: `status` and `init codex --write` now say, every time, that the
|
|
6
|
+
hook must be trusted once with `/hooks` inside Codex. Verified that an
|
|
7
|
+
untrusted hook stalls `codex exec` with no output the first time a spawn
|
|
8
|
+
would be gated, from `hooks.json` and from `config.toml` alike.
|
|
9
|
+
- Verified the installed Claude Code hook end to end from the global
|
|
10
|
+
install: 0.2s on a 1.95B-token transcript, decision recorded.
|
|
11
|
+
|
|
12
|
+
## 0.2.1 (2026-09-03)
|
|
13
|
+
|
|
14
|
+
Finishing the product so it can be used, and a live Codex canary.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- `agentguard-burn resume --once` is now real. The STOP box has advertised it
|
|
18
|
+
since 0.1.0; nothing read the file. Every host consults the override only
|
|
19
|
+
when a block is due, consumes a `--once` atomically (rename), records the
|
|
20
|
+
reason in the decisions ledger, and `resume --reason` without `--once`
|
|
21
|
+
opens a 15-minute window; `--clear` ends it.
|
|
22
|
+
- `init [claude|cursor|codex] --write`: merges the hook into the host's
|
|
23
|
+
config with a timestamped backup, finds an existing AgentGuard entry by
|
|
24
|
+
its command and upgrades it in place, refuses to touch a file it cannot
|
|
25
|
+
parse.
|
|
26
|
+
- `status` shows hook health per host (installed, command exists on disk;
|
|
27
|
+
a missing script is called out in capitals because that hook fails open),
|
|
28
|
+
Claude Code sessions alongside gateway sessions, and any active override.
|
|
29
|
+
- Warnings are announced once per change in the finding set, on every host.
|
|
30
|
+
- Captured codex-cli 0.151.0 payloads as a fixture; a test runs on them.
|
|
31
|
+
|
|
32
|
+
### Verified
|
|
33
|
+
- Codex: live canary on codex-cli 0.151.0. `spawn_agent` denied with the
|
|
34
|
+
alarm box as the reason; `Bash` passed untouched; `resume --once` let the
|
|
35
|
+
next spawn through with the reason on the ledger. Codex moves from
|
|
36
|
+
experimental to beta. The wire sends `tool_name: "spawn_agent"` (the docs
|
|
37
|
+
say it matches as `Agent`; the matcher covers both). Project-local hooks
|
|
38
|
+
load only when the project is trusted; the user-layer file needs a
|
|
39
|
+
one-time `/hooks` trust inside Codex.
|
|
40
|
+
|
|
3
41
|
## 0.2.0 (2026-09-03)
|
|
4
42
|
|
|
5
43
|
The cross-tool layer. One policy, one lock, one receipt across hosts.
|
package/README.md
CHANGED
|
@@ -33,14 +33,32 @@ have intercepted, when, and the observed tail after each stop. It is an upper
|
|
|
33
33
|
bound, labelled as such. Nobody installs a blocker cold.
|
|
34
34
|
|
|
35
35
|
```
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
npm i -g @agentguard-run/burn
|
|
37
|
+
agentguard-burn init --write # merge the hook into ~/.claude/settings.json (backup taken)
|
|
38
|
+
agentguard-burn init codex --write # same for ~/.codex/hooks.json
|
|
39
|
+
agentguard-burn status # hook health, shadow observations, every live session
|
|
40
|
+
agentguard-burn enforce # after 7 days and 50 decisions
|
|
40
41
|
```
|
|
41
42
|
|
|
42
43
|
The hook installs in **shadow mode**: every decision is recorded, nothing is
|
|
43
|
-
blocked, until you have seen it be right.
|
|
44
|
+
blocked, until you have seen it be right. `status` shows what it would have
|
|
45
|
+
done so far, which sessions are live on every host, and whether each hook's
|
|
46
|
+
command still exists on disk (a hook whose script is gone fails open, and
|
|
47
|
+
`status` says so in capitals).
|
|
48
|
+
|
|
49
|
+
### When it stops you and you disagree
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
agentguard-burn resume --once --reason "these 60 agents are the plan" # the next STOP passes, once, on any host
|
|
53
|
+
agentguard-burn resume --reason "load test" # every STOP passes for 15 minutes
|
|
54
|
+
agentguard-burn resume --clear
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Every override is written to the decisions ledger with its reason. A
|
|
58
|
+
`--once` override is consumed atomically: two hooks racing for it cannot
|
|
59
|
+
both pass. Warnings are spoken once per change in the finding set, not once
|
|
60
|
+
per spawn; eighteen identical banners train you to stop reading the
|
|
61
|
+
nineteenth.
|
|
44
62
|
|
|
45
63
|
## One policy across hosts (0.2.0)
|
|
46
64
|
|
|
@@ -62,7 +80,7 @@ What each host can actually see is stated, not implied:
|
|
|
62
80
|
| Ollama proxy | none | none | authoritative | `prompt_eval_count` + `eval_count` on the final chunk |
|
|
63
81
|
| vLLM / LM Studio / OpenAI-compatible proxy | none | none | authoritative when the server sends `usage`, else reported missing | non-streaming `usage`, or the final SSE usage event |
|
|
64
82
|
| Cursor (beta) | authoritative | estimated | none | native `subagentStart` deny; hosted-model usage is never exposed |
|
|
65
|
-
| Codex (
|
|
83
|
+
| Codex (beta) | authoritative | estimated | estimated | `PreToolUse` on `spawn_agent`; live deny, allow and override canary passed on codex-cli 0.151.0; transcript parsed best-effort |
|
|
66
84
|
|
|
67
85
|
An `OK` from a host that cannot see usage is an OK about spawns, and `status`
|
|
68
86
|
says `usage:n/a` next to it. Missing usage never becomes a guessed zero.
|
|
@@ -128,10 +146,29 @@ agentguard-burn init codex # ~/.codex/hooks.json snippet
|
|
|
128
146
|
Both renderers are one page each and emit only their host's documented output
|
|
129
147
|
object. Codex fails the whole hook on Claude's common fields (`continue`,
|
|
130
148
|
`stopReason`, `suppressOutput`), and a failed hook is a fail-open hook, so the
|
|
131
|
-
Codex renderer never emits them and a test forbids them by name.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
149
|
+
Codex renderer never emits them and a test forbids them by name.
|
|
150
|
+
|
|
151
|
+
**Codex** passed a live canary on the installed codex-cli 0.151.0 on
|
|
152
|
+
2026-09-03: a `spawn_agent` call was denied with the alarm box as the reason,
|
|
153
|
+
a shell call passed untouched, and a `resume --once` override let the next
|
|
154
|
+
spawn through with the reason on the ledger. Two things the docs got wrong
|
|
155
|
+
and the wire settled: the tool arrives as `spawn_agent` (the docs say it
|
|
156
|
+
"matches as Agent"; the matcher covers both), and project-local hooks only
|
|
157
|
+
load when the project is trusted. The captured payloads are in
|
|
158
|
+
`fixtures/codex-0.151.0-pretooluse.json` and drive a test.
|
|
159
|
+
|
|
160
|
+
One step Codex makes you do by hand: after `init codex --write`, open
|
|
161
|
+
`codex`, run `/hooks`, and trust the AgentGuard hook. Codex requires this
|
|
162
|
+
once per hook source, and there is no CLI for it. Until it is done,
|
|
163
|
+
`codex exec` stalls the first time a spawn would be gated (verified: it
|
|
164
|
+
hangs with no output; the same with hooks defined in `config.toml`).
|
|
165
|
+
`status` repeats this under the codex line because it cannot see whether
|
|
166
|
+
trust was granted.
|
|
167
|
+
|
|
168
|
+
**Cursor** is verified against the documented schema (`permission`,
|
|
169
|
+
`user_message`, `agent_message`; `~/.cursor/hooks.json` with `failClosed`),
|
|
170
|
+
not yet against an installed build. It stays beta until a live deny canary
|
|
171
|
+
passes.
|
|
135
172
|
|
|
136
173
|
### Receipts
|
|
137
174
|
|
|
@@ -75,7 +75,10 @@ function handleCodexHook(raw, gateway, now = Date.now()) {
|
|
|
75
75
|
// An unrecorded start leaves its reservation pending until TTL, which is
|
|
76
76
|
// the conservative direction.
|
|
77
77
|
quietly(() => gateway.observe([{ schemaVersion: 1, kind: 'spawn_started', eventId: `codex:start:${spawnId}`, host: HOST, sessionId: input.sessionId, at: now, spawnId, depth: 1 }]));
|
|
78
|
-
if (decision.
|
|
78
|
+
if (decision.overridden) {
|
|
79
|
+
return { systemMessage: `AgentGuard STOP overridden${decision.overridden.once ? ' once' : ''} ("${decision.overridden.reason}"): ${decision.report.findings[0]?.summary ?? ''}` };
|
|
80
|
+
}
|
|
81
|
+
if (decision.notify && decision.verdict !== 'OK') {
|
|
79
82
|
return {
|
|
80
83
|
systemMessage: `AgentGuard ${decision.verdict}${decision.mode === 'shadow' && decision.wouldBlock ? ' (shadow: would have blocked)' : ''}: ${decision.report.findings[0]?.summary ?? ''}`,
|
|
81
84
|
};
|
|
@@ -64,7 +64,10 @@ function handleCursorHook(raw, gateway, now = Date.now()) {
|
|
|
64
64
|
// hook after it has decided; a crash is a fail-open on most hosts.
|
|
65
65
|
const depth = decision.proposedDepth ?? 1;
|
|
66
66
|
quietly(() => gateway.observe([{ schemaVersion: 1, kind: 'spawn_started', eventId: `cursor:start:${spawnId}`, host: HOST, sessionId: input.sessionId, at, spawnId, depth }]));
|
|
67
|
-
if (decision.
|
|
67
|
+
if (decision.overridden) {
|
|
68
|
+
return { permission: 'allow', agent_message: `AgentGuard STOP overridden${decision.overridden.once ? ' once' : ''} ("${decision.overridden.reason}"): ${decision.report.findings[0]?.summary ?? ''}` };
|
|
69
|
+
}
|
|
70
|
+
if (decision.notify && decision.verdict !== 'OK') {
|
|
68
71
|
return {
|
|
69
72
|
permission: 'allow',
|
|
70
73
|
agent_message: `AgentGuard ${decision.verdict}${decision.mode === 'shadow' && decision.wouldBlock ? ' (shadow: would have blocked)' : ''}: ${decision.report.findings[0]?.summary ?? ''}`,
|
package/dist/src/cli.js
CHANGED
|
@@ -27,6 +27,8 @@ const conformance_1 = require("./conformance");
|
|
|
27
27
|
const defaults_1 = require("./defaults");
|
|
28
28
|
const gateway_1 = require("./gateway");
|
|
29
29
|
const pre_tool_use_1 = require("./hook/pre-tool-use");
|
|
30
|
+
const install_1 = require("./install");
|
|
31
|
+
const override_1 = require("./override");
|
|
30
32
|
const server_1 = require("./proxy/server");
|
|
31
33
|
const render_1 = require("./replay/render");
|
|
32
34
|
const simulate_1 = require("./replay/simulate");
|
|
@@ -131,40 +133,59 @@ async function main(argv) {
|
|
|
131
133
|
const e = shadowEligibility();
|
|
132
134
|
const gateway = new gateway_1.Gateway(HOME, { sign: false });
|
|
133
135
|
const lc = policy.thresholds.localCompute;
|
|
136
|
+
const override = (0, override_1.readOverride)(HOME);
|
|
134
137
|
process.stdout.write([
|
|
135
|
-
`mode: ${policy.mode}`,
|
|
138
|
+
`mode: ${policy.mode}${policy.mode === 'enforce' ? ' (the next STOP blocks)' : ' (recording only; nothing is blocked)'}`,
|
|
136
139
|
`thresholds: fan-out ${policy.thresholds.fanout.warn}/${policy.thresholds.fanout.stop} sustained ${(policy.thresholds.sustained.warnTokens / 1e9).toFixed(1)}B/${(policy.thresholds.sustained.stopTokens / 1e9).toFixed(1)}B local-compute warn at ${lc?.warnConcurrent ?? 4} concurrent${lc?.stopConcurrent ? `, stop at ${lc.stopConcurrent}` : ' (no stop set)'}`,
|
|
137
140
|
policy.calibration ? `calibrated from ${policy.calibration.sessionsSampled} sessions` : 'using shipped defaults (run: agentguard-burn calibrate)',
|
|
138
141
|
`shadow observation: ${e.decisions} decisions over ${e.days.toFixed(1)} days`,
|
|
139
142
|
` would have warned: ${e.warns} would have blocked: ${e.wouldBlock}`,
|
|
140
|
-
`eligible for enforcement: ${e.eligible ? 'yes' : `no (need ${defaults_1.SHADOW_MIN_DECISIONS} decisions and ${defaults_1.SHADOW_MIN_DAYS} days)`}`,
|
|
143
|
+
`eligible for enforcement: ${e.eligible ? 'yes (agentguard-burn enforce)' : `no (need ${defaults_1.SHADOW_MIN_DECISIONS} decisions and ${defaults_1.SHADOW_MIN_DAYS} days)`}`,
|
|
144
|
+
override ? `override ACTIVE: ${override.once ? 'the next STOP' : `every STOP until ${new Date(override.until).toLocaleTimeString()}`} passes ("${override.reason}")` : '',
|
|
141
145
|
'',
|
|
142
|
-
(0, status_1.
|
|
146
|
+
(0, status_1.renderHostHealth)(['claude', 'cursor', 'codex'].map((h) => (0, install_1.health)(h))),
|
|
147
|
+
'',
|
|
148
|
+
(0, status_1.renderMachineStatus)([...(0, status_1.readHookSessions)(HOME), ...gateway.sessions()], gateway.compute()),
|
|
143
149
|
'',
|
|
144
150
|
'content logging: off · telemetry: none · state: this machine only',
|
|
145
|
-
]
|
|
151
|
+
]
|
|
152
|
+
.filter((l) => l !== '' || true)
|
|
153
|
+
.join('\n')
|
|
154
|
+
.replace(/\n\n\n/g, '\n\n') + '\n');
|
|
146
155
|
return 0;
|
|
147
156
|
}
|
|
148
157
|
case 'init': {
|
|
149
158
|
const policy = (0, pre_tool_use_1.loadPolicy)(HOME);
|
|
150
159
|
savePolicy(policy);
|
|
151
160
|
const cli = (0, node_path_1.join)(__dirname, 'cli.js');
|
|
152
|
-
const target = rest.find((a) => !a.startsWith('--')) ?? 'claude';
|
|
153
|
-
if (
|
|
154
|
-
process.
|
|
155
|
-
|
|
156
|
-
`"failClosed": true means a crashed hook denies the subagent.\n` +
|
|
157
|
-
`Installed in ${policy.mode} mode. Nothing is blocked until you run: agentguard-burn enforce\n`);
|
|
158
|
-
return 0;
|
|
161
|
+
const target = (rest.find((a) => !a.startsWith('--')) ?? 'claude');
|
|
162
|
+
if (!['claude', 'cursor', 'codex'].includes(target)) {
|
|
163
|
+
process.stderr.write(`init: host must be claude, cursor or codex\n`);
|
|
164
|
+
return 64;
|
|
159
165
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
166
|
+
const label = { claude: '', cursor: 'Cursor support is BETA: verified against the documented hook schema, not yet against every installed build. "failClosed": true means a crashed hook denies the subagent.', codex: 'Codex support is BETA: a live deny, allow and override canary passed on codex-cli 0.151.0; transcript usage is best-effort and marked estimated.\nONE MORE STEP: open codex, run /hooks, and trust the AgentGuard hook. Codex requires this once per hook source; until then `codex exec` stalls the first time a spawn would be gated.' }[target];
|
|
167
|
+
const tail = `Installed in ${policy.mode} mode. Nothing is blocked until you run: agentguard-burn enforce\n`;
|
|
168
|
+
if (has('--write')) {
|
|
169
|
+
let result;
|
|
170
|
+
try {
|
|
171
|
+
result = (0, install_1.install)(target, cli);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
process.stderr.write(`init: refusing to touch ${(0, install_1.configPath)(target)}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
175
|
+
return 1;
|
|
176
|
+
}
|
|
177
|
+
process.stdout.write((result.changed ? `Wrote ${result.file}${result.backup ? ` (backup: ${result.backup})` : ''}\n` : `${result.file} already has the current hook; nothing changed.\n`) +
|
|
178
|
+
` ${result.command}\n` +
|
|
179
|
+
(label ? `${label}\n` : '') +
|
|
180
|
+
tail +
|
|
181
|
+
`Check it: agentguard-burn status\n`);
|
|
164
182
|
return 0;
|
|
165
183
|
}
|
|
166
|
-
|
|
167
|
-
|
|
184
|
+
const snippet = target === 'cursor' ? (0, cursor_1.cursorHooksSnippet)(`node ${cli} cursor-hook`) : target === 'codex' ? (0, codex_1.codexHooksSnippet)(`node ${cli} codex-hook`) : (0, pre_tool_use_1.settingsSnippet)(`node ${cli} hook`);
|
|
185
|
+
process.stdout.write(`Add this to ${(0, install_1.configPath)(target).replace(process.env.HOME ?? '', '~')} (merge into existing "hooks"), or run: agentguard-burn init ${target} --write\n\n${JSON.stringify(snippet, null, 2)}\n\n` +
|
|
186
|
+
(label ? `${label}\n` : '') +
|
|
187
|
+
tail +
|
|
188
|
+
(target === 'claude' ? `Other hosts: agentguard-burn init cursor | init codex | proxy --upstream http://127.0.0.1:11434 --host ollama\n` : ''));
|
|
168
189
|
return 0;
|
|
169
190
|
}
|
|
170
191
|
case 'cursor-hook': {
|
|
@@ -230,20 +251,34 @@ async function main(argv) {
|
|
|
230
251
|
return 0;
|
|
231
252
|
}
|
|
232
253
|
case 'resume': {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
254
|
+
if (has('--clear')) {
|
|
255
|
+
(0, override_1.clearOverride)(HOME);
|
|
256
|
+
process.stdout.write('Override cleared. The next STOP blocks.\n');
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
const reason = flag('--reason');
|
|
260
|
+
if (!reason) {
|
|
261
|
+
process.stderr.write('resume: --reason "..." is required; every override is written to the decisions ledger with it.\n');
|
|
262
|
+
return 64;
|
|
263
|
+
}
|
|
264
|
+
const once = has('--once');
|
|
265
|
+
const now = Date.now();
|
|
266
|
+
(0, override_1.writeOverride)(HOME, once ? { at: now, once: true, reason } : { at: now, once: false, reason, until: now + override_1.OVERRIDE_WINDOW_MS });
|
|
267
|
+
process.stdout.write(once
|
|
268
|
+
? `Override recorded for the next STOP only, on any host: "${reason}"\n`
|
|
269
|
+
: `Override recorded for the next ${override_1.OVERRIDE_WINDOW_MS / 60000} minutes, on any host: "${reason}"\n (agentguard-burn resume --clear to end it early)\n`);
|
|
237
270
|
return 0;
|
|
238
271
|
}
|
|
239
272
|
default:
|
|
240
273
|
process.stdout.write('agentguard-burn <replay|calibrate|status|init|enforce|shadow|resume|proxy|conformance>\n' +
|
|
241
274
|
' replay [files...] [--json] [--top N] [--min-tokens N]\n' +
|
|
242
275
|
' calibrate fit thresholds to your history (writes shadow policy)\n' +
|
|
243
|
-
' status mode, shadow observations, eligibility, every host\n' +
|
|
244
|
-
' init [claude|cursor|codex] print the hook snippet
|
|
276
|
+
' status mode, shadow observations, eligibility, hook health, every host\n' +
|
|
277
|
+
' init [claude|cursor|codex] [--write] print the hook snippet, or merge it into the config (with a backup)\n' +
|
|
245
278
|
' enforce [--force] shadow -> enforce\n' +
|
|
246
|
-
'
|
|
279
|
+
' shadow enforce -> shadow\n' +
|
|
280
|
+
' resume --once --reason "..." let the next STOP through, once, on any host\n' +
|
|
281
|
+
' resume --reason "..." let STOPs through for 15 minutes; --clear ends it\n' +
|
|
247
282
|
' proxy --upstream http://127.0.0.1:11434 --host ollama|vllm|lm-studio|openai-compatible [--listen 127.0.0.1:18080] [--session ID]\n' +
|
|
248
283
|
' conformance replay the storm and the grind through every adapter shape\n');
|
|
249
284
|
return command ? 64 : 0;
|
package/dist/src/gateway.d.ts
CHANGED
|
@@ -39,6 +39,13 @@ export interface Decision {
|
|
|
39
39
|
wouldBlock: boolean;
|
|
40
40
|
/** Enforcing, and blocking. */
|
|
41
41
|
blocked: boolean;
|
|
42
|
+
/** A block was due and the user's recorded override let it through. */
|
|
43
|
+
overridden: {
|
|
44
|
+
once: boolean;
|
|
45
|
+
reason: string;
|
|
46
|
+
} | null;
|
|
47
|
+
/** The finding set changed since the user was last told. Adapters speak only when true. */
|
|
48
|
+
notify: boolean;
|
|
42
49
|
mode: Mode;
|
|
43
50
|
report: BurnReport;
|
|
44
51
|
capabilities: HostCapabilities;
|
package/dist/src/gateway.js
CHANGED
|
@@ -38,6 +38,7 @@ const evaluate_1 = require("./detectors/evaluate");
|
|
|
38
38
|
const local_compute_1 = require("./detectors/local-compute");
|
|
39
39
|
const events_1 = require("./events");
|
|
40
40
|
const pre_tool_use_1 = require("./hook/pre-tool-use");
|
|
41
|
+
const override_1 = require("./override");
|
|
41
42
|
const receipt_1 = require("./receipt");
|
|
42
43
|
const reservations_1 = require("./state/reservations");
|
|
43
44
|
const session_1 = require("./state/session");
|
|
@@ -224,8 +225,9 @@ class Gateway {
|
|
|
224
225
|
const wouldBlock = report.verdict === 'STOP' || !reservation.allowed;
|
|
225
226
|
const verdict = wouldBlock ? 'STOP' : report.verdict;
|
|
226
227
|
// Session-scope STOPs need a session we trust. Fan-out is session scope.
|
|
227
|
-
const
|
|
228
|
-
const
|
|
228
|
+
const due = wouldBlock && policy.mode === 'enforce' && event.attribution === 'high';
|
|
229
|
+
const override = due ? (0, override_1.consumeOverride)(this.home, event.at) : null;
|
|
230
|
+
const decision = this.finish({ action: 'spawn', host: event.host, sessionId: event.sessionId, at: event.at, proposedDepth, verdict, wouldBlock, blocked: due && !override, override, report, effectiveSpawns: reservation.effectiveSpawns, compute: null }, policy, meta, state);
|
|
229
231
|
this.save(meta, state);
|
|
230
232
|
return decision;
|
|
231
233
|
});
|
|
@@ -269,8 +271,10 @@ class Gateway {
|
|
|
269
271
|
// A low-confidence session (proxy without a session header) can be
|
|
270
272
|
// stopped only on machine-scope grounds. Its session count is a guess.
|
|
271
273
|
const machineStop = local.verdict === 'STOP';
|
|
272
|
-
const
|
|
273
|
-
const
|
|
274
|
+
const due = wouldBlock && policy.mode === 'enforce' && (event.attribution === 'high' || machineStop);
|
|
275
|
+
const override = due ? (0, override_1.consumeOverride)(this.home, event.at) : null;
|
|
276
|
+
const blocked = due && !override;
|
|
277
|
+
const decision = this.finish({ action: 'model_call', host: event.host, sessionId: event.sessionId, at: event.at, proposedDepth: null, verdict: report.verdict, wouldBlock, blocked, override, report, effectiveSpawns: state.spawnCount, compute }, policy, meta, state);
|
|
274
278
|
if (blocked) {
|
|
275
279
|
// A denied call neither occupies the machine nor spends tokens.
|
|
276
280
|
tx.finishCall(event.callId, windowMs(policy), event.at);
|
|
@@ -327,6 +331,11 @@ class Gateway {
|
|
|
327
331
|
if (d.wouldBlock)
|
|
328
332
|
meta.wouldBlock += 1;
|
|
329
333
|
const decisionId = (0, events_1.eventId)();
|
|
334
|
+
// Speak once per change, not once per spawn. A block or an override is
|
|
335
|
+
// always spoken; it is the event the user needs to see.
|
|
336
|
+
const signature = d.verdict === 'OK' ? '' : (0, pre_tool_use_1.findingSignature)(d.report);
|
|
337
|
+
const notify = d.blocked || d.override !== null || (signature !== '' && signature !== (meta.notified ?? ''));
|
|
338
|
+
meta.notified = signature;
|
|
330
339
|
// Sign what matters: every spawn admission, and any call that is not OK.
|
|
331
340
|
// Signing every OK call through a busy proxy would be thousands of
|
|
332
341
|
// receipts an hour saying nothing.
|
|
@@ -367,6 +376,7 @@ class Gateway {
|
|
|
367
376
|
verdict: d.verdict,
|
|
368
377
|
wouldDeny: d.wouldBlock,
|
|
369
378
|
enforced: d.blocked,
|
|
379
|
+
overridden: d.override ?? undefined,
|
|
370
380
|
mode: policy.mode,
|
|
371
381
|
findings: d.report.findings.map((f) => ({ detector: f.detector, verdict: f.verdict, observed: f.observed, threshold: f.threshold })),
|
|
372
382
|
effectiveSpawns: d.effectiveSpawns,
|
|
@@ -380,6 +390,8 @@ class Gateway {
|
|
|
380
390
|
verdict: d.verdict,
|
|
381
391
|
wouldBlock: d.wouldBlock,
|
|
382
392
|
blocked: d.blocked,
|
|
393
|
+
overridden: d.override,
|
|
394
|
+
notify,
|
|
383
395
|
mode: policy.mode,
|
|
384
396
|
report: d.report,
|
|
385
397
|
capabilities: meta.capabilities,
|
|
@@ -407,6 +419,8 @@ class Gateway {
|
|
|
407
419
|
verdict: 'STOP',
|
|
408
420
|
wouldBlock: true,
|
|
409
421
|
blocked: policy.mode === 'enforce',
|
|
422
|
+
overridden: null,
|
|
423
|
+
notify: true,
|
|
410
424
|
mode: policy.mode,
|
|
411
425
|
report,
|
|
412
426
|
capabilities: events_1.CAPABILITIES[event.host],
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* as little as possible.
|
|
16
16
|
*/
|
|
17
17
|
import { type ReaderCursor } from '../history/claude-transcript';
|
|
18
|
-
import type { Policy, SessionState } from '../types';
|
|
18
|
+
import type { BurnReport, Policy, SessionState } from '../types';
|
|
19
19
|
export interface HookInput {
|
|
20
20
|
session_id?: string;
|
|
21
21
|
transcript_path?: string;
|
|
@@ -35,12 +35,36 @@ export interface HookOutput {
|
|
|
35
35
|
permissionDecisionReason: string;
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
+
export interface PersistedSession {
|
|
39
|
+
cursor: {
|
|
40
|
+
offset: number;
|
|
41
|
+
size: number;
|
|
42
|
+
malformedLines: number;
|
|
43
|
+
depthByUuid: [string, number][];
|
|
44
|
+
};
|
|
45
|
+
state: Omit<SessionState, 'tokensByActiveMinute' | 'spawnsByActiveMinute' | 'surfaceReaders'> & {
|
|
46
|
+
tokensByActiveMinute: [number, number][];
|
|
47
|
+
spawnsByActiveMinute: [number, number][];
|
|
48
|
+
surfaceReaders: [string, number[]][];
|
|
49
|
+
};
|
|
50
|
+
/** Signature of the last finding set the user was told about. */
|
|
51
|
+
notified?: string;
|
|
52
|
+
}
|
|
53
|
+
/** Inflate a persisted Claude Code session. Shared with status. */
|
|
54
|
+
export declare function inflateHookSession(raw: PersistedSession): SessionState;
|
|
55
|
+
/**
|
|
56
|
+
* Tell the user once per change, not once per spawn. Eighteen identical
|
|
57
|
+
* "WARN: 30 spawns" banners in a row train the user to stop reading them,
|
|
58
|
+
* and the one that matters is the nineteenth.
|
|
59
|
+
*/
|
|
60
|
+
export declare function findingSignature(report: BurnReport): string;
|
|
38
61
|
export declare function loadPolicy(home: string): Policy;
|
|
39
62
|
/** Refresh session state from the transcript. Cheap: only new bytes are read. */
|
|
40
63
|
export declare function refreshSession(home: string, sessionId: string, transcriptPath: string): {
|
|
41
64
|
cursor: ReaderCursor;
|
|
42
65
|
state: SessionState;
|
|
43
66
|
newSpawns: number;
|
|
67
|
+
notified: string;
|
|
44
68
|
};
|
|
45
69
|
export declare function handlePreToolUse(input: HookInput, home: string, now?: number): HookOutput;
|
|
46
70
|
/** The settings.json fragment users paste in. Only PreToolUse can block. */
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* as little as possible.
|
|
17
17
|
*/
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.inflateHookSession = inflateHookSession;
|
|
20
|
+
exports.findingSignature = findingSignature;
|
|
19
21
|
exports.loadPolicy = loadPolicy;
|
|
20
22
|
exports.refreshSession = refreshSession;
|
|
21
23
|
exports.handlePreToolUse = handlePreToolUse;
|
|
@@ -27,28 +29,32 @@ const claude_transcript_1 = require("../history/claude-transcript");
|
|
|
27
29
|
const reservations_1 = require("../state/reservations");
|
|
28
30
|
const session_1 = require("../state/session");
|
|
29
31
|
const defaults_1 = require("../defaults");
|
|
32
|
+
const override_1 = require("../override");
|
|
30
33
|
const render_1 = require("../replay/render");
|
|
31
34
|
const SPAWN_TOOLS = new Set(['Agent', 'Task']);
|
|
32
35
|
function sessionFile(home, sessionId) {
|
|
33
36
|
return (0, node_path_1.join)(home, 'sessions', `${sessionId.replace(/[^a-zA-Z0-9_-]/g, '_')}.json`);
|
|
34
37
|
}
|
|
38
|
+
/** Inflate a persisted Claude Code session. Shared with status. */
|
|
39
|
+
function inflateHookSession(raw) {
|
|
40
|
+
return {
|
|
41
|
+
...raw.state,
|
|
42
|
+
tokensByActiveMinute: new Map(raw.state.tokensByActiveMinute),
|
|
43
|
+
spawnsByActiveMinute: new Map(raw.state.spawnsByActiveMinute),
|
|
44
|
+
surfaceReaders: new Map(raw.state.surfaceReaders.map(([k, v]) => [k, new Set(v)])),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
35
47
|
function loadSession(home, sessionId, firstEventAt) {
|
|
36
48
|
try {
|
|
37
49
|
const raw = JSON.parse((0, node_fs_1.readFileSync)(sessionFile(home, sessionId), 'utf8'));
|
|
38
50
|
const cursor = { ...raw.cursor, depthByUuid: new Map(raw.cursor.depthByUuid) };
|
|
39
|
-
|
|
40
|
-
...raw.state,
|
|
41
|
-
tokensByActiveMinute: new Map(raw.state.tokensByActiveMinute),
|
|
42
|
-
spawnsByActiveMinute: new Map(raw.state.spawnsByActiveMinute),
|
|
43
|
-
surfaceReaders: new Map(raw.state.surfaceReaders.map(([k, v]) => [k, new Set(v)])),
|
|
44
|
-
};
|
|
45
|
-
return { cursor, state };
|
|
51
|
+
return { cursor, state: inflateHookSession(raw), notified: raw.notified ?? '' };
|
|
46
52
|
}
|
|
47
53
|
catch {
|
|
48
|
-
return { cursor: (0, claude_transcript_1.newCursor)(), state: (0, session_1.newSessionState)(sessionId, firstEventAt) };
|
|
54
|
+
return { cursor: (0, claude_transcript_1.newCursor)(), state: (0, session_1.newSessionState)(sessionId, firstEventAt), notified: '' };
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
|
-
function saveSession(home, cursor, state) {
|
|
57
|
+
function saveSession(home, cursor, state, notified) {
|
|
52
58
|
(0, node_fs_1.mkdirSync)((0, node_path_1.join)(home, 'sessions'), { recursive: true, mode: 0o700 });
|
|
53
59
|
const persisted = {
|
|
54
60
|
cursor: { offset: cursor.offset, size: cursor.size, malformedLines: cursor.malformedLines, depthByUuid: [...cursor.depthByUuid] },
|
|
@@ -58,12 +64,24 @@ function saveSession(home, cursor, state) {
|
|
|
58
64
|
spawnsByActiveMinute: [...state.spawnsByActiveMinute],
|
|
59
65
|
surfaceReaders: [...state.surfaceReaders].map(([k, v]) => [k, [...v]]),
|
|
60
66
|
},
|
|
67
|
+
notified,
|
|
61
68
|
};
|
|
62
69
|
const file = sessionFile(home, state.sessionId);
|
|
63
70
|
(0, node_fs_1.writeFileSync)(`${file}.tmp`, JSON.stringify(persisted), { mode: 0o600 });
|
|
64
71
|
// rename is atomic; a concurrent reader never sees a half-written file.
|
|
65
72
|
require('node:fs').renameSync(`${file}.tmp`, file);
|
|
66
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Tell the user once per change, not once per spawn. Eighteen identical
|
|
76
|
+
* "WARN: 30 spawns" banners in a row train the user to stop reading them,
|
|
77
|
+
* and the one that matters is the nineteenth.
|
|
78
|
+
*/
|
|
79
|
+
function findingSignature(report) {
|
|
80
|
+
return report.findings
|
|
81
|
+
.map((f) => `${f.detector}:${f.verdict}`)
|
|
82
|
+
.sort()
|
|
83
|
+
.join('|');
|
|
84
|
+
}
|
|
67
85
|
function loadPolicy(home) {
|
|
68
86
|
try {
|
|
69
87
|
const parsed = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(home, 'burn-policy.json'), 'utf8'));
|
|
@@ -81,15 +99,27 @@ function recordDecision(home, entry) {
|
|
|
81
99
|
}
|
|
82
100
|
/** Refresh session state from the transcript. Cheap: only new bytes are read. */
|
|
83
101
|
function refreshSession(home, sessionId, transcriptPath) {
|
|
84
|
-
const { cursor, state } = loadSession(home, sessionId, Date.now());
|
|
102
|
+
const { cursor, state, notified } = loadSession(home, sessionId, Date.now());
|
|
85
103
|
const before = state.spawnCount;
|
|
86
104
|
const events = (0, claude_transcript_1.readIncremental)(transcriptPath, cursor);
|
|
87
105
|
for (const event of events)
|
|
88
106
|
(0, session_1.applyEvent)(state, event);
|
|
89
107
|
if (events.length > 0 && state.startedAt > events[0].at)
|
|
90
108
|
state.startedAt = events[0].at;
|
|
91
|
-
saveSession(home, cursor, state);
|
|
92
|
-
return { cursor, state, newSpawns: state.spawnCount - before };
|
|
109
|
+
saveSession(home, cursor, state, notified);
|
|
110
|
+
return { cursor, state, newSpawns: state.spawnCount - before, notified };
|
|
111
|
+
}
|
|
112
|
+
function rememberNotified(home, sessionId, signature) {
|
|
113
|
+
try {
|
|
114
|
+
const raw = JSON.parse((0, node_fs_1.readFileSync)(sessionFile(home, sessionId), 'utf8'));
|
|
115
|
+
raw.notified = signature;
|
|
116
|
+
const file = sessionFile(home, sessionId);
|
|
117
|
+
(0, node_fs_1.writeFileSync)(`${file}.tmp`, JSON.stringify(raw), { mode: 0o600 });
|
|
118
|
+
require('node:fs').renameSync(`${file}.tmp`, file);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
/* best effort; the worst case is one repeated banner */
|
|
122
|
+
}
|
|
93
123
|
}
|
|
94
124
|
function handlePreToolUse(input, home, now = Date.now()) {
|
|
95
125
|
const toolName = input.tool_name ?? '';
|
|
@@ -100,8 +130,11 @@ function handlePreToolUse(input, home, now = Date.now()) {
|
|
|
100
130
|
const store = new reservations_1.ReservationStore(home);
|
|
101
131
|
let report;
|
|
102
132
|
let reservation;
|
|
133
|
+
let notified = '';
|
|
103
134
|
try {
|
|
104
|
-
const
|
|
135
|
+
const refreshed = refreshSession(home, input.session_id, input.transcript_path);
|
|
136
|
+
const { state, newSpawns } = refreshed;
|
|
137
|
+
notified = refreshed.notified;
|
|
105
138
|
if (newSpawns > 0)
|
|
106
139
|
store.reconcile(input.session_id, state.spawnCount, state.spawnCount - newSpawns);
|
|
107
140
|
// Depth of the proposed child: the issuing agent's depth plus one. A hook
|
|
@@ -124,26 +157,41 @@ function handlePreToolUse(input, home, now = Date.now()) {
|
|
|
124
157
|
}
|
|
125
158
|
const shouldDeny = report.verdict === 'STOP' || !reservation.allowed;
|
|
126
159
|
const reason = shouldDeny ? buildDenyReason(report, reservation) : '';
|
|
160
|
+
// The audited override: only consulted when a block is about to happen.
|
|
161
|
+
const override = shouldDeny && policy.mode === 'enforce' ? (0, override_1.consumeOverride)(home, now) : null;
|
|
127
162
|
recordDecision(home, {
|
|
128
163
|
at: now,
|
|
129
164
|
sessionId: input.session_id,
|
|
130
165
|
toolUseId: input.tool_use_id ?? null,
|
|
131
166
|
verdict: report.verdict,
|
|
132
167
|
wouldDeny: shouldDeny,
|
|
133
|
-
enforced: policy.mode === 'enforce' && shouldDeny,
|
|
168
|
+
enforced: policy.mode === 'enforce' && shouldDeny && !override,
|
|
169
|
+
overridden: override ? { once: override.once, reason: override.reason } : undefined,
|
|
134
170
|
mode: policy.mode,
|
|
135
171
|
findings: report.findings.map((f) => ({ detector: f.detector, verdict: f.verdict, observed: f.observed, threshold: f.threshold })),
|
|
136
172
|
effectiveSpawns: reservation.effectiveSpawns,
|
|
137
173
|
totals: report.totals,
|
|
138
174
|
});
|
|
139
|
-
if (shouldDeny && policy.mode === 'enforce')
|
|
140
|
-
|
|
175
|
+
if (shouldDeny && policy.mode === 'enforce') {
|
|
176
|
+
if (!override)
|
|
177
|
+
return deny(reason);
|
|
178
|
+
return {
|
|
179
|
+
continue: true,
|
|
180
|
+
systemMessage: `AgentGuard STOP overridden${override.once ? ' once' : ''} ("${override.reason}"): ${report.findings[0]?.summary ?? ''}`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
141
183
|
if (report.verdict !== 'OK') {
|
|
184
|
+
const signature = findingSignature(report);
|
|
185
|
+
if (signature === notified)
|
|
186
|
+
return { continue: true, suppressOutput: true };
|
|
187
|
+
rememberNotified(home, input.session_id, signature);
|
|
142
188
|
return {
|
|
143
189
|
continue: true,
|
|
144
190
|
systemMessage: `AgentGuard ${report.verdict}${policy.mode === 'shadow' && shouldDeny ? ' (shadow: would have blocked)' : ''}: ${report.findings[0]?.summary ?? ''}`,
|
|
145
191
|
};
|
|
146
192
|
}
|
|
193
|
+
if (notified)
|
|
194
|
+
rememberNotified(home, input.session_id, '');
|
|
147
195
|
return { continue: true, suppressOutput: true };
|
|
148
196
|
}
|
|
149
197
|
function buildDenyReason(report, _reservation) {
|
package/dist/src/index.d.ts
CHANGED
|
@@ -19,3 +19,7 @@ export { startProxy, profileFor, type ProxyOptions, type ProxyHost, type Running
|
|
|
19
19
|
export { UsageObserver, type ProxyProfile, type ObservedUsage } from './proxy/usage-observer';
|
|
20
20
|
export { renderMachineStatus } from './status';
|
|
21
21
|
export { runConformance, type ConformanceResult } from './conformance';
|
|
22
|
+
export { writeOverride, readOverride, consumeOverride, clearOverride, OVERRIDE_WINDOW_MS, type Override } from './override';
|
|
23
|
+
export { install, health, configPath, type InstallHost, type InstallResult, type HostHealth } from './install';
|
|
24
|
+
export { readHookSessions, renderHostHealth } from './status';
|
|
25
|
+
export { findingSignature, inflateHookSession, type PersistedSession } from './hook/pre-tool-use';
|
package/dist/src/index.js
CHANGED
|
@@ -15,7 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
exports.UsageObserver = exports.profileFor = exports.startProxy = exports.CODEX_FORBIDDEN_FIELDS = exports.readCodexTranscriptUsage = exports.parseCodexTranscript = exports.codexHooksSnippet = exports.handleCodexHook = exports.cursorHooksSnippet = exports.handleCursorHook = exports.BurnStopError = exports.createRawApiGuard = exports.sha256 = exports.canonical = exports.receiptDigest = exports.verifyReceipt = exports.ReceiptSigner = exports.mergeCapabilities = exports.Gateway = exports.settingsSnippet = exports.loadPolicy = exports.refreshSession = exports.handlePreToolUse = exports.calibrate = exports.renderStop = exports.renderReplay = exports.discoverTranscripts = exports.replayEvents = exports.replaySession = exports.replayAll = exports.RESERVATION_TTL_MS = exports.Transaction = exports.ReservationStore = exports.cacheReadRatio = exports.medianCompletedMinute = exports.windowSum = exports.newSessionState = exports.applyCorrection = exports.applyEvent = exports.normaliseLine = exports.newCursor = exports.readAll = exports.readIncremental = exports.evaluateLocalCompute = exports.fmt = exports.evaluate = exports.CALL_RESERVATION_TTL_MS = exports.DEFAULT_LOCAL_COMPUTE = exports.DEFAULT_THRESHOLDS = exports.DEFAULT_POLICY = void 0;
|
|
18
|
-
exports.runConformance = exports.renderMachineStatus = void 0;
|
|
18
|
+
exports.inflateHookSession = exports.findingSignature = exports.renderHostHealth = exports.readHookSessions = exports.configPath = exports.health = exports.install = exports.OVERRIDE_WINDOW_MS = exports.clearOverride = exports.consumeOverride = exports.readOverride = exports.writeOverride = exports.runConformance = exports.renderMachineStatus = void 0;
|
|
19
19
|
__exportStar(require("./types"), exports);
|
|
20
20
|
var defaults_1 = require("./defaults");
|
|
21
21
|
Object.defineProperty(exports, "DEFAULT_POLICY", { enumerable: true, get: function () { return defaults_1.DEFAULT_POLICY; } });
|
|
@@ -90,3 +90,19 @@ var status_1 = require("./status");
|
|
|
90
90
|
Object.defineProperty(exports, "renderMachineStatus", { enumerable: true, get: function () { return status_1.renderMachineStatus; } });
|
|
91
91
|
var conformance_1 = require("./conformance");
|
|
92
92
|
Object.defineProperty(exports, "runConformance", { enumerable: true, get: function () { return conformance_1.runConformance; } });
|
|
93
|
+
var override_1 = require("./override");
|
|
94
|
+
Object.defineProperty(exports, "writeOverride", { enumerable: true, get: function () { return override_1.writeOverride; } });
|
|
95
|
+
Object.defineProperty(exports, "readOverride", { enumerable: true, get: function () { return override_1.readOverride; } });
|
|
96
|
+
Object.defineProperty(exports, "consumeOverride", { enumerable: true, get: function () { return override_1.consumeOverride; } });
|
|
97
|
+
Object.defineProperty(exports, "clearOverride", { enumerable: true, get: function () { return override_1.clearOverride; } });
|
|
98
|
+
Object.defineProperty(exports, "OVERRIDE_WINDOW_MS", { enumerable: true, get: function () { return override_1.OVERRIDE_WINDOW_MS; } });
|
|
99
|
+
var install_1 = require("./install");
|
|
100
|
+
Object.defineProperty(exports, "install", { enumerable: true, get: function () { return install_1.install; } });
|
|
101
|
+
Object.defineProperty(exports, "health", { enumerable: true, get: function () { return install_1.health; } });
|
|
102
|
+
Object.defineProperty(exports, "configPath", { enumerable: true, get: function () { return install_1.configPath; } });
|
|
103
|
+
var status_2 = require("./status");
|
|
104
|
+
Object.defineProperty(exports, "readHookSessions", { enumerable: true, get: function () { return status_2.readHookSessions; } });
|
|
105
|
+
Object.defineProperty(exports, "renderHostHealth", { enumerable: true, get: function () { return status_2.renderHostHealth; } });
|
|
106
|
+
var pre_tool_use_2 = require("./hook/pre-tool-use");
|
|
107
|
+
Object.defineProperty(exports, "findingSignature", { enumerable: true, get: function () { return pre_tool_use_2.findingSignature; } });
|
|
108
|
+
Object.defineProperty(exports, "inflateHookSession", { enumerable: true, get: function () { return pre_tool_use_2.inflateHookSession; } });
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installing the hook into each host's config, and checking it is there.
|
|
3
|
+
*
|
|
4
|
+
* Merging into a config file the user also edits by hand is the one place
|
|
5
|
+
* this tool can do damage, so: always a timestamped backup first, always a
|
|
6
|
+
* parse-before-write, always an atomic rename, and the AgentGuard entry is
|
|
7
|
+
* found by its command (any command mentioning agentguard-burn), so a
|
|
8
|
+
* re-run upgrades the path in place instead of stacking duplicates.
|
|
9
|
+
*/
|
|
10
|
+
export type InstallHost = 'claude' | 'cursor' | 'codex';
|
|
11
|
+
export interface InstallResult {
|
|
12
|
+
file: string;
|
|
13
|
+
backup: string | null;
|
|
14
|
+
changed: boolean;
|
|
15
|
+
command: string;
|
|
16
|
+
}
|
|
17
|
+
export interface HostHealth {
|
|
18
|
+
host: InstallHost;
|
|
19
|
+
file: string;
|
|
20
|
+
installed: boolean;
|
|
21
|
+
command: string | null;
|
|
22
|
+
/** The script the command points at exists on disk. */
|
|
23
|
+
commandExists: boolean;
|
|
24
|
+
/** The host itself is present (its config dir or binary). */
|
|
25
|
+
hostPresent: boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare function configPath(host: InstallHost, home?: string): string;
|
|
28
|
+
export declare function install(host: InstallHost, cliPath: string, home?: string): InstallResult;
|
|
29
|
+
export declare function health(host: InstallHost, home?: string): HostHealth;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Installing the hook into each host's config, and checking it is there.
|
|
4
|
+
*
|
|
5
|
+
* Merging into a config file the user also edits by hand is the one place
|
|
6
|
+
* this tool can do damage, so: always a timestamped backup first, always a
|
|
7
|
+
* parse-before-write, always an atomic rename, and the AgentGuard entry is
|
|
8
|
+
* found by its command (any command mentioning agentguard-burn), so a
|
|
9
|
+
* re-run upgrades the path in place instead of stacking duplicates.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.configPath = configPath;
|
|
13
|
+
exports.install = install;
|
|
14
|
+
exports.health = health;
|
|
15
|
+
const node_fs_1 = require("node:fs");
|
|
16
|
+
const node_os_1 = require("node:os");
|
|
17
|
+
const node_path_1 = require("node:path");
|
|
18
|
+
const MARK = /agentguard-burn|agentguard-run\/burn/;
|
|
19
|
+
function configPath(host, home = (0, node_os_1.homedir)()) {
|
|
20
|
+
return host === 'claude' ? (0, node_path_1.join)(home, '.claude', 'settings.json') : host === 'cursor' ? (0, node_path_1.join)(home, '.cursor', 'hooks.json') : (0, node_path_1.join)(home, '.codex', 'hooks.json');
|
|
21
|
+
}
|
|
22
|
+
function readJson(file) {
|
|
23
|
+
if (!(0, node_fs_1.existsSync)(file))
|
|
24
|
+
return {};
|
|
25
|
+
const text = (0, node_fs_1.readFileSync)(file, 'utf8');
|
|
26
|
+
if (!text.trim())
|
|
27
|
+
return {};
|
|
28
|
+
const parsed = JSON.parse(text); // throws on a corrupt file: we refuse to overwrite what we cannot read
|
|
29
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
30
|
+
throw new Error(`${file} is not a JSON object`);
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
function writeJson(file, value) {
|
|
34
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.join)(file, '..'), { recursive: true });
|
|
35
|
+
let backup = null;
|
|
36
|
+
if ((0, node_fs_1.existsSync)(file)) {
|
|
37
|
+
backup = `${file}.bak-${new Date().toISOString().replace(/[:.]/g, '').slice(0, 15)}`;
|
|
38
|
+
(0, node_fs_1.copyFileSync)(file, backup);
|
|
39
|
+
}
|
|
40
|
+
const tmp = `${file}.agentguard.tmp`;
|
|
41
|
+
(0, node_fs_1.writeFileSync)(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
42
|
+
(0, node_fs_1.renameSync)(tmp, file);
|
|
43
|
+
return backup;
|
|
44
|
+
}
|
|
45
|
+
/** Claude Code and Codex share the { hooks: { Event: [ { matcher, hooks: [ {command} ] } ] } } shape. */
|
|
46
|
+
function mergeMatcherStyle(cfg, event, matcher, command, timeout) {
|
|
47
|
+
const hooks = (cfg.hooks ??= {});
|
|
48
|
+
const list = (hooks[event] ??= []);
|
|
49
|
+
let changed = false;
|
|
50
|
+
let found = false;
|
|
51
|
+
for (const m of list) {
|
|
52
|
+
for (const h of m.hooks ?? []) {
|
|
53
|
+
if (typeof h.command === 'string' && MARK.test(h.command)) {
|
|
54
|
+
found = true;
|
|
55
|
+
if (h.command !== command || m.matcher !== matcher || h.timeout !== timeout) {
|
|
56
|
+
h.command = command;
|
|
57
|
+
h.timeout = timeout;
|
|
58
|
+
m.matcher = matcher;
|
|
59
|
+
changed = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (!found) {
|
|
65
|
+
list.push({ matcher, hooks: [{ type: 'command', command, timeout }] });
|
|
66
|
+
changed = true;
|
|
67
|
+
}
|
|
68
|
+
return changed;
|
|
69
|
+
}
|
|
70
|
+
/** Cursor: { version: 1, hooks: { subagentStart: [ {command, timeout, failClosed} ] } } */
|
|
71
|
+
function mergeCursor(cfg, command) {
|
|
72
|
+
let changed = false;
|
|
73
|
+
if (cfg.version !== 1) {
|
|
74
|
+
cfg.version = 1;
|
|
75
|
+
changed = true;
|
|
76
|
+
}
|
|
77
|
+
const hooks = (cfg.hooks ??= {});
|
|
78
|
+
const want = {
|
|
79
|
+
subagentStart: { command, timeout: 5, failClosed: true },
|
|
80
|
+
subagentStop: { command, timeout: 5 },
|
|
81
|
+
sessionEnd: { command, timeout: 3 },
|
|
82
|
+
};
|
|
83
|
+
for (const [event, entry] of Object.entries(want)) {
|
|
84
|
+
const list = (hooks[event] ??= []);
|
|
85
|
+
const mine = list.find((h) => typeof h.command === 'string' && MARK.test(h.command));
|
|
86
|
+
if (!mine) {
|
|
87
|
+
list.push(entry);
|
|
88
|
+
changed = true;
|
|
89
|
+
}
|
|
90
|
+
else if (JSON.stringify(mine) !== JSON.stringify({ ...mine, ...entry })) {
|
|
91
|
+
Object.assign(mine, entry);
|
|
92
|
+
changed = true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return changed;
|
|
96
|
+
}
|
|
97
|
+
function install(host, cliPath, home = (0, node_os_1.homedir)()) {
|
|
98
|
+
const file = configPath(host, home);
|
|
99
|
+
const cfg = readJson(file);
|
|
100
|
+
let command;
|
|
101
|
+
let changed;
|
|
102
|
+
if (host === 'claude') {
|
|
103
|
+
command = `node ${cliPath} hook`;
|
|
104
|
+
changed = mergeMatcherStyle(cfg, 'PreToolUse', '^(Agent|Task)$', command, 5);
|
|
105
|
+
}
|
|
106
|
+
else if (host === 'codex') {
|
|
107
|
+
command = `node ${cliPath} codex-hook`;
|
|
108
|
+
changed = mergeMatcherStyle(cfg, 'PreToolUse', '^(Agent|spawn_agent)$', command, 5);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
command = `node ${cliPath} cursor-hook`;
|
|
112
|
+
changed = mergeCursor(cfg, command);
|
|
113
|
+
}
|
|
114
|
+
const backup = changed ? writeJson(file, cfg) : null;
|
|
115
|
+
return { file, backup, changed, command };
|
|
116
|
+
}
|
|
117
|
+
function findCommand(cfg) {
|
|
118
|
+
const hooks = cfg.hooks;
|
|
119
|
+
if (!hooks || typeof hooks !== 'object')
|
|
120
|
+
return null;
|
|
121
|
+
for (const list of Object.values(hooks)) {
|
|
122
|
+
if (!Array.isArray(list))
|
|
123
|
+
continue;
|
|
124
|
+
for (const item of list) {
|
|
125
|
+
const entries = Array.isArray(item.hooks) ? item.hooks : [item];
|
|
126
|
+
for (const h of entries)
|
|
127
|
+
if (typeof h.command === 'string' && MARK.test(h.command))
|
|
128
|
+
return h.command;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
function health(host, home = (0, node_os_1.homedir)()) {
|
|
134
|
+
const file = configPath(host, home);
|
|
135
|
+
let command = null;
|
|
136
|
+
try {
|
|
137
|
+
command = findCommand(readJson(file));
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
command = null;
|
|
141
|
+
}
|
|
142
|
+
const script = command?.match(/^node (\S+)/)?.[1] ?? null;
|
|
143
|
+
const hostPresent = host === 'claude' ? (0, node_fs_1.existsSync)((0, node_path_1.join)(home, '.claude')) : host === 'cursor' ? (0, node_fs_1.existsSync)((0, node_path_1.join)(home, '.cursor')) : (0, node_fs_1.existsSync)((0, node_path_1.join)(home, '.codex'));
|
|
144
|
+
return { host, file, installed: command !== null, command, commandExists: script !== null && (0, node_fs_1.existsSync)(script), hostPresent };
|
|
145
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The audited override.
|
|
3
|
+
*
|
|
4
|
+
* The STOP box says "override once: agentguard-burn resume --once". That
|
|
5
|
+
* has to be real, and it has to be single-use across every host on the
|
|
6
|
+
* machine: two hooks that both see the file must not both let a spawn
|
|
7
|
+
* through. Consumption is a rename, which is atomic; the loser of the race
|
|
8
|
+
* finds nothing to rename and blocks.
|
|
9
|
+
*
|
|
10
|
+
* Without --once the override is a window, 15 minutes by default, for the
|
|
11
|
+
* case where the user has decided the session is fine and does not want to
|
|
12
|
+
* type the command forty times. Every overridden STOP is written to the
|
|
13
|
+
* decisions ledger with the reason, so "why did this get through" always
|
|
14
|
+
* has an answer.
|
|
15
|
+
*/
|
|
16
|
+
export interface Override {
|
|
17
|
+
at: number;
|
|
18
|
+
once: boolean;
|
|
19
|
+
reason: string;
|
|
20
|
+
/** Absent for --once. */
|
|
21
|
+
until?: number;
|
|
22
|
+
}
|
|
23
|
+
export declare const OVERRIDE_WINDOW_MS: number;
|
|
24
|
+
export declare function writeOverride(home: string, o: Override): void;
|
|
25
|
+
export declare function readOverride(home: string, now?: number): Override | null;
|
|
26
|
+
/**
|
|
27
|
+
* Called only when a STOP is about to be enforced. Returns the override to
|
|
28
|
+
* apply, or null. A --once override is consumed here and cannot be used
|
|
29
|
+
* twice, however many processes ask at the same moment.
|
|
30
|
+
*/
|
|
31
|
+
export declare function consumeOverride(home: string, now?: number): Override | null;
|
|
32
|
+
export declare function clearOverride(home: string): void;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The audited override.
|
|
4
|
+
*
|
|
5
|
+
* The STOP box says "override once: agentguard-burn resume --once". That
|
|
6
|
+
* has to be real, and it has to be single-use across every host on the
|
|
7
|
+
* machine: two hooks that both see the file must not both let a spawn
|
|
8
|
+
* through. Consumption is a rename, which is atomic; the loser of the race
|
|
9
|
+
* finds nothing to rename and blocks.
|
|
10
|
+
*
|
|
11
|
+
* Without --once the override is a window, 15 minutes by default, for the
|
|
12
|
+
* case where the user has decided the session is fine and does not want to
|
|
13
|
+
* type the command forty times. Every overridden STOP is written to the
|
|
14
|
+
* decisions ledger with the reason, so "why did this get through" always
|
|
15
|
+
* has an answer.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.OVERRIDE_WINDOW_MS = void 0;
|
|
19
|
+
exports.writeOverride = writeOverride;
|
|
20
|
+
exports.readOverride = readOverride;
|
|
21
|
+
exports.consumeOverride = consumeOverride;
|
|
22
|
+
exports.clearOverride = clearOverride;
|
|
23
|
+
const node_fs_1 = require("node:fs");
|
|
24
|
+
const node_path_1 = require("node:path");
|
|
25
|
+
exports.OVERRIDE_WINDOW_MS = 15 * 60 * 1000;
|
|
26
|
+
function file(home) {
|
|
27
|
+
return (0, node_path_1.join)(home, 'override.json');
|
|
28
|
+
}
|
|
29
|
+
function writeOverride(home, o) {
|
|
30
|
+
(0, node_fs_1.mkdirSync)(home, { recursive: true, mode: 0o700 });
|
|
31
|
+
(0, node_fs_1.writeFileSync)(file(home), JSON.stringify(o), { mode: 0o600 });
|
|
32
|
+
}
|
|
33
|
+
function readOverride(home, now = Date.now()) {
|
|
34
|
+
try {
|
|
35
|
+
const o = JSON.parse((0, node_fs_1.readFileSync)(file(home), 'utf8'));
|
|
36
|
+
if (typeof o.reason !== 'string' || typeof o.at !== 'number')
|
|
37
|
+
return null;
|
|
38
|
+
if (!o.once && (o.until === undefined || o.until < now))
|
|
39
|
+
return null;
|
|
40
|
+
return o;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Called only when a STOP is about to be enforced. Returns the override to
|
|
48
|
+
* apply, or null. A --once override is consumed here and cannot be used
|
|
49
|
+
* twice, however many processes ask at the same moment.
|
|
50
|
+
*/
|
|
51
|
+
function consumeOverride(home, now = Date.now()) {
|
|
52
|
+
const o = readOverride(home, now);
|
|
53
|
+
if (!o) {
|
|
54
|
+
if ((0, node_fs_1.existsSync)(file(home)))
|
|
55
|
+
(0, node_fs_1.rmSync)(file(home), { force: true }); // expired or malformed
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
if (!o.once)
|
|
59
|
+
return o;
|
|
60
|
+
const consumed = `${file(home)}.used.${process.pid}.${now.toString(36)}`;
|
|
61
|
+
try {
|
|
62
|
+
(0, node_fs_1.renameSync)(file(home), consumed);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null; // a sibling consumed it first
|
|
66
|
+
}
|
|
67
|
+
(0, node_fs_1.rmSync)(consumed, { force: true });
|
|
68
|
+
return o;
|
|
69
|
+
}
|
|
70
|
+
function clearOverride(home) {
|
|
71
|
+
(0, node_fs_1.rmSync)(file(home), { force: true });
|
|
72
|
+
}
|
package/dist/src/status.d.ts
CHANGED
|
@@ -7,5 +7,13 @@
|
|
|
7
7
|
* middleware and the proxy shows full coverage, because it has it.
|
|
8
8
|
*/
|
|
9
9
|
import type { GatewaySessionView } from './gateway';
|
|
10
|
+
import type { HostHealth } from './install';
|
|
10
11
|
import type { ComputeSnapshot } from './state/reservations';
|
|
12
|
+
/**
|
|
13
|
+
* Claude Code sessions live in the hook's own files (they carry a transcript
|
|
14
|
+
* cursor the gateway sessions do not). Read them into the same view so the
|
|
15
|
+
* table shows every host, not just the new ones.
|
|
16
|
+
*/
|
|
17
|
+
export declare function readHookSessions(home: string): GatewaySessionView[];
|
|
18
|
+
export declare function renderHostHealth(hosts: HostHealth[]): string;
|
|
11
19
|
export declare function renderMachineStatus(sessions: GatewaySessionView[], compute: ComputeSnapshot, now?: number): string;
|
package/dist/src/status.js
CHANGED
|
@@ -8,19 +8,85 @@
|
|
|
8
8
|
* middleware and the proxy shows full coverage, because it has it.
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.readHookSessions = readHookSessions;
|
|
12
|
+
exports.renderHostHealth = renderHostHealth;
|
|
11
13
|
exports.renderMachineStatus = renderMachineStatus;
|
|
14
|
+
const node_fs_1 = require("node:fs");
|
|
15
|
+
const node_path_1 = require("node:path");
|
|
12
16
|
const evaluate_1 = require("./detectors/evaluate");
|
|
17
|
+
const events_1 = require("./events");
|
|
18
|
+
const pre_tool_use_1 = require("./hook/pre-tool-use");
|
|
13
19
|
const LIVE_WINDOW_MS = 30 * 60 * 1000;
|
|
20
|
+
/**
|
|
21
|
+
* Claude Code sessions live in the hook's own files (they carry a transcript
|
|
22
|
+
* cursor the gateway sessions do not). Read them into the same view so the
|
|
23
|
+
* table shows every host, not just the new ones.
|
|
24
|
+
*/
|
|
25
|
+
function readHookSessions(home) {
|
|
26
|
+
const dir = (0, node_path_1.join)(home, 'sessions');
|
|
27
|
+
const out = [];
|
|
28
|
+
let names = [];
|
|
29
|
+
try {
|
|
30
|
+
names = (0, node_fs_1.readdirSync)(dir).filter((n) => n.endsWith('.json') && !n.startsWith('gw-'));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
for (const name of names) {
|
|
36
|
+
try {
|
|
37
|
+
const raw = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(dir, name), 'utf8'));
|
|
38
|
+
if (!raw.cursor || !raw.state)
|
|
39
|
+
continue;
|
|
40
|
+
const state = (0, pre_tool_use_1.inflateHookSession)(raw);
|
|
41
|
+
out.push({
|
|
42
|
+
sessionId: state.sessionId,
|
|
43
|
+
hosts: ['claude-code'],
|
|
44
|
+
capabilities: events_1.CAPABILITIES['claude-code'],
|
|
45
|
+
state,
|
|
46
|
+
liveSpawns: 0,
|
|
47
|
+
usage: { authoritative: state.totalTokens > 0 ? 1 : 0, estimated: 0, missing: 0 },
|
|
48
|
+
decisions: 0,
|
|
49
|
+
wouldBlock: 0,
|
|
50
|
+
closedAt: null,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
/* half-written or foreign file: skip */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
function renderHostHealth(hosts) {
|
|
60
|
+
const lines = ['hosts:'];
|
|
61
|
+
for (const h of hosts) {
|
|
62
|
+
const name = h.host === 'claude' ? 'claude-code' : h.host;
|
|
63
|
+
let state;
|
|
64
|
+
if (!h.installed)
|
|
65
|
+
state = h.hostPresent ? `not installed (agentguard-burn init ${h.host} --write)` : 'not present on this machine';
|
|
66
|
+
else if (!h.commandExists)
|
|
67
|
+
state = `INSTALLED BUT BROKEN: hook command not found on disk, host will fail open (re-run: agentguard-burn init ${h.host} --write)`;
|
|
68
|
+
else
|
|
69
|
+
state = `installed, hook command ok (${h.file.replace(process.env.HOME ?? '', '~')})`;
|
|
70
|
+
lines.push(` ${name.padEnd(13)} ${state}`);
|
|
71
|
+
if (h.host === 'codex' && h.installed && h.commandExists) {
|
|
72
|
+
// Codex requires a one-time trust of each hook source. We cannot see
|
|
73
|
+
// whether it has been granted, and an untrusted hook stalls codex exec
|
|
74
|
+
// the first time it would fire, so say it every time.
|
|
75
|
+
lines.push(` ${''.padEnd(13)} trust it once: open codex, run /hooks, trust the AgentGuard hook. Until then codex exec stalls when a spawn would be gated.`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return lines.join('\n');
|
|
79
|
+
}
|
|
14
80
|
function renderMachineStatus(sessions, compute, now = Date.now()) {
|
|
15
|
-
const live = sessions.filter((s) => s.closedAt === null && now - s.state.lastEventAt <= LIVE_WINDOW_MS);
|
|
81
|
+
const live = sessions.filter((s) => s.closedAt === null && now - s.state.lastEventAt <= LIVE_WINDOW_MS).sort((a, b) => b.state.lastEventAt - a.state.lastEventAt);
|
|
16
82
|
const totalTokens = live.reduce((n, s) => n + s.state.totalTokens, 0);
|
|
17
83
|
const lines = [];
|
|
18
|
-
lines.push(`
|
|
84
|
+
lines.push(`sessions: ${live.length} active in the last 30 min · ${(0, evaluate_1.fmt)(totalTokens)} tokens · ${compute.inFlight} model call(s) in flight · ${Math.round(compute.occupiedMs / 1000)}s occupied in last ${Math.round(compute.windowMs / 60000)} min`);
|
|
19
85
|
if (live.length === 0) {
|
|
20
|
-
lines.push(' (none
|
|
86
|
+
lines.push(' (none)');
|
|
21
87
|
return lines.join('\n');
|
|
22
88
|
}
|
|
23
|
-
lines.push(' ' + pad('HOST(S)', 22) + pad('SESSION', 20) + pad('TOKENS', 9) + pad('SPAWNS', 8) + pad('DEPTH', 7) + pad('
|
|
89
|
+
lines.push(' ' + pad('HOST(S)', 22) + pad('SESSION', 20) + pad('TOKENS', 9) + pad('SPAWNS', 8) + pad('DEPTH', 7) + pad('LAST', 8) + 'COVERAGE');
|
|
24
90
|
for (const s of live) {
|
|
25
91
|
lines.push(' ' +
|
|
26
92
|
pad(s.hosts.join('+'), 22) +
|
|
@@ -28,11 +94,15 @@ function renderMachineStatus(sessions, compute, now = Date.now()) {
|
|
|
28
94
|
pad((0, evaluate_1.fmt)(s.state.totalTokens), 9) +
|
|
29
95
|
pad(String(s.state.spawnCount), 8) +
|
|
30
96
|
pad(String(s.state.maxDepth), 7) +
|
|
31
|
-
pad(
|
|
97
|
+
pad(ago(now - s.state.lastEventAt), 8) +
|
|
32
98
|
coverageLine(s));
|
|
33
99
|
}
|
|
34
100
|
return lines.join('\n');
|
|
35
101
|
}
|
|
102
|
+
function ago(ms) {
|
|
103
|
+
const m = Math.floor(ms / 60000);
|
|
104
|
+
return m < 1 ? 'now' : `${m}m ago`;
|
|
105
|
+
}
|
|
36
106
|
function coverageLine(s) {
|
|
37
107
|
const usage = s.usage.missing > 0 ? `usage:partial(${s.usage.missing} missing)` : s.usage.authoritative > 0 ? 'usage:auth' : s.usage.estimated > 0 ? 'usage:est' : `usage:${short(s.capabilities.usage)}`;
|
|
38
108
|
return `spawns:${short(s.capabilities.spawns)} depth:${short(s.capabilities.depth)} ${usage}`;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"captured_from": "codex-cli 0.151.0, 2026-09-03, codex exec with -c hooks.PreToolUse inline; --dangerously-bypass-hook-trust",
|
|
3
|
+
"payloads": [
|
|
4
|
+
{
|
|
5
|
+
"session_id": "01a069d3-5fc8-7470-8c95-1b42e7206746",
|
|
6
|
+
"turn_id": "01a069d3-6018-7523-a26e-eea10123676c",
|
|
7
|
+
"transcript_path": "/Users/example/.codex/sessions/2026/09/03/rollout-example.jsonl",
|
|
8
|
+
"cwd": "/Users/example/project",
|
|
9
|
+
"hook_event_name": "PreToolUse",
|
|
10
|
+
"model": "gpt-5.4-mini",
|
|
11
|
+
"permission_mode": "bypassPermissions",
|
|
12
|
+
"tool_name": "Bash",
|
|
13
|
+
"tool_input": {
|
|
14
|
+
"command": "echo CANARY_OK"
|
|
15
|
+
},
|
|
16
|
+
"tool_use_id": "call_NmxrZob0I2KFC8ChNe2PduGX"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"session_id": "01a069d4-d356-73b3-9440-1c4440319de8",
|
|
20
|
+
"turn_id": "01a069d4-d3b2-7593-85ec-8a0281f51bf1",
|
|
21
|
+
"transcript_path": "/Users/example/.codex/sessions/2026/09/03/rollout-example.jsonl",
|
|
22
|
+
"cwd": "/Users/example/project",
|
|
23
|
+
"hook_event_name": "PreToolUse",
|
|
24
|
+
"model": "gpt-5.4-mini",
|
|
25
|
+
"permission_mode": "bypassPermissions",
|
|
26
|
+
"tool_name": "spawn_agent",
|
|
27
|
+
"tool_input": {
|
|
28
|
+
"message": "<redacted>",
|
|
29
|
+
"fork_context": "<redacted>"
|
|
30
|
+
},
|
|
31
|
+
"tool_use_id": "call_oktUPQ5jcosNPOcGnEaWqRcv"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"session_id": "01a069d4-d356-73b3-9440-1c4440319de8",
|
|
35
|
+
"turn_id": "01a069d4-d3b2-7593-85ec-8a0281f51bf1",
|
|
36
|
+
"transcript_path": "/Users/example/.codex/sessions/2026/09/03/rollout-example.jsonl",
|
|
37
|
+
"cwd": "/Users/example/project",
|
|
38
|
+
"hook_event_name": "PreToolUse",
|
|
39
|
+
"model": "gpt-5.4-mini",
|
|
40
|
+
"permission_mode": "bypassPermissions",
|
|
41
|
+
"tool_name": "multi_agent_v1wait_agent",
|
|
42
|
+
"tool_input": {
|
|
43
|
+
"targets": "<redacted>",
|
|
44
|
+
"timeout_ms": "<redacted>"
|
|
45
|
+
},
|
|
46
|
+
"tool_use_id": "call_RK95A6i4UUhTigIGXF24gMMO"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentguard-run/burn",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Local runaway-agent circuit breaker for AI coding agents and local model runtimes. One policy across Claude Code, Cursor, Codex, Ollama, vLLM, LM Studio and raw orchestrators: detects fan-out storms and sustained token burn, blocks the next spawn, and proves what happened with content-free signed receipts. Nothing leaves the machine.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"files": [
|
|
36
36
|
"dist/src",
|
|
37
|
+
"fixtures",
|
|
37
38
|
"README.md",
|
|
38
39
|
"CHANGELOG.md",
|
|
39
40
|
"LICENSE"
|