@agentguard-run/burn 0.2.1 → 0.2.3
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 +30 -2
- package/LICENSE +9 -10
- package/README.md +43 -10
- package/dist/src/cli.js +1 -1
- package/dist/src/conformance.js +3 -1
- package/dist/src/defaults.d.ts +7 -8
- package/dist/src/defaults.js +7 -8
- package/dist/src/detectors/evaluate.d.ts +5 -2
- package/dist/src/detectors/evaluate.js +26 -2
- package/dist/src/gateway.js +4 -3
- package/dist/src/hook/pre-tool-use.d.ts +2 -0
- package/dist/src/hook/pre-tool-use.js +84 -62
- package/dist/src/replay/render.js +1 -1
- package/dist/src/state/account.d.ts +3 -0
- package/dist/src/state/account.js +39 -0
- package/dist/src/state/reservations.js +5 -1
- package/dist/src/status.js +6 -0
- package/fixtures/codex-0.151.0-pretooluse.json +11 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
|
+
## 0.2.3 (2026-09-15)
|
|
2
|
+
|
|
3
|
+
- Replay evidence refreshed from this machine: 4 STOP, 3 WARN, 431 clean across
|
|
4
|
+
438 sessions and 616 spawns. Hero: 32%, 32.58B observed, 10.27B after STOP,
|
|
5
|
+
$4,639 API-list scenario. Fan-out sessions have 59, 190 and 226 spawns;
|
|
6
|
+
the sustained-burn session has 9.15B tokens and 26 spawns. These are a dated
|
|
7
|
+
snapshot and an upper bound, without a false-positive classification.
|
|
8
|
+
- Account concurrency warnings and optional active-window token WARN/STOP
|
|
9
|
+
budgets read both hook and gateway session files. Shadow never blocks.
|
|
10
|
+
- Claude hook decisions now write the same content-free, chained Ed25519
|
|
11
|
+
receipts as gateway decisions, with host/action on ledger rows. Old rows and
|
|
12
|
+
receipts remain readable. Session refresh and receipt chaining share the lock.
|
|
13
|
+
- Add an opt-in 240-hook-process stress test at cap 40. Staggered lock retries
|
|
14
|
+
reduce contention while preserving the existing fail-closed deadline.
|
|
15
|
+
- Correct the license package/file enumeration, use synthetic Codex fixture
|
|
16
|
+
identifiers, and stop labelling every omitted replay session as clean.
|
|
17
|
+
- Tests: 47 passing by default, one stress test skipped unless AGENTGUARD_STRESS=1.
|
|
18
|
+
|
|
1
19
|
# Changelog
|
|
2
20
|
|
|
21
|
+
## 0.2.2 (2026-09-03)
|
|
22
|
+
|
|
23
|
+
- Codex: `status` and `init codex --write` now say, every time, that the
|
|
24
|
+
hook must be trusted once with `/hooks` inside Codex. Verified that an
|
|
25
|
+
untrusted hook stalls `codex exec` with no output the first time a spawn
|
|
26
|
+
would be gated, from `hooks.json` and from `config.toml` alike.
|
|
27
|
+
- Verified the installed Claude Code hook end to end from the global
|
|
28
|
+
install: 0.2s on a 1.95B-token transcript, decision recorded.
|
|
29
|
+
|
|
3
30
|
## 0.2.1 (2026-09-03)
|
|
4
31
|
|
|
5
32
|
Finishing the product so it can be used, and a live Codex canary.
|
|
@@ -66,7 +93,7 @@ The cross-tool layer. One policy, one lock, one receipt across hosts.
|
|
|
66
93
|
contention). Under 240 concurrent hook processes this admitted 41 to 46
|
|
67
94
|
spawns against a cap of 40. Lock instances now carry a nonce; reclaims
|
|
68
95
|
are verified against the instance judged; writes are fenced on the
|
|
69
|
-
holder's nonce.
|
|
96
|
+
holder's nonce. See 0.2.3 for the reproducible 240-process audit result.
|
|
70
97
|
- A lock owner record could be read half-written and parsed as "held since
|
|
71
98
|
1970". The record is now written atomically and malformed records are
|
|
72
99
|
never trusted.
|
|
@@ -83,7 +110,8 @@ The cross-tool layer. One policy, one lock, one receipt across hosts.
|
|
|
83
110
|
|
|
84
111
|
### Unchanged
|
|
85
112
|
- The Claude Code hook, transcript reader, detectors and thresholds. The
|
|
86
|
-
|
|
113
|
+
original tests remain covered; 0.2.2 has 40 passing tests, and 0.2.3 has
|
|
114
|
+
47 default passing tests plus one opt-in stress test.
|
|
87
115
|
|
|
88
116
|
## 0.1.1 (2026-09-03)
|
|
89
117
|
- The artifact: block-digit hero, per-session sparklines with the STOP marked,
|
package/LICENSE
CHANGED
|
@@ -2,13 +2,12 @@ AgentGuard(R) Burn - Alpha License
|
|
|
2
2
|
Copyright (c) 2026 Dunecrest Ventures Inc.
|
|
3
3
|
|
|
4
4
|
1. SCOPE.
|
|
5
|
-
This software is the npm package @agentguard-run/
|
|
5
|
+
This software is the npm package @agentguard-run/burn, comprising every file
|
|
6
6
|
present in an installed copy of that package. That includes the TypeScript
|
|
7
|
-
|
|
8
|
-
point
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
expressly name is licensed under this Section 1; the enumeration above is
|
|
7
|
+
type definitions and compiled output under dist/src/, the command-line entry
|
|
8
|
+
point dist/src/cli.js, the fixtures under fixtures/, and the root files
|
|
9
|
+
README.md, CHANGELOG.md, LICENSE and package.json. Any file the package ships
|
|
10
|
+
that Section 3 does not expressly name is licensed under this Section 1; the enumeration above is
|
|
12
11
|
illustrative, and adding a file to the package does not place it outside these
|
|
13
12
|
terms.
|
|
14
13
|
|
|
@@ -40,8 +39,8 @@ Licensor. All patent rights are expressly reserved.
|
|
|
40
39
|
The following assets, and ONLY these assets, are released under the BSD
|
|
41
40
|
2-Clause License, reproduced in full below:
|
|
42
41
|
|
|
43
|
-
- The documentation
|
|
44
|
-
- The test
|
|
42
|
+
- The documentation file README.md
|
|
43
|
+
- The test fixtures under fixtures/
|
|
45
44
|
|
|
46
45
|
Copyright (c) 2026 Dunecrest Ventures Inc.
|
|
47
46
|
|
|
@@ -74,8 +73,8 @@ consistent with Section 2 above. The BSD 2-Clause License is used for these
|
|
|
74
73
|
assets precisely because it is silent as to patents.
|
|
75
74
|
|
|
76
75
|
Nothing else the package ships is included in the grant above. The code under
|
|
77
|
-
src/,
|
|
78
|
-
|
|
76
|
+
dist/src/, and the root files CHANGELOG.md, LICENSE and package.json remain
|
|
77
|
+
licensed only under the alpha evaluation
|
|
79
78
|
terms of Section 1. The TypeScript type definitions, policy engine, decision
|
|
80
79
|
log, store implementation, cost table, and wrapper code are in that group.
|
|
81
80
|
|
package/README.md
CHANGED
|
@@ -9,17 +9,22 @@ to do about it. Nothing leaves the machine.
|
|
|
9
9
|
|
|
10
10
|
## Two safety planes
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Originally fitted against 412 real Claude Code sessions, then replayed on
|
|
13
|
+
2026-09-15 against 438 sessions. The history shows two independent
|
|
13
14
|
failure shapes that one detector cannot cover:
|
|
14
15
|
|
|
15
16
|
| Plane | Catches | Rule | Validated on |
|
|
16
17
|
|---|---|---|---|
|
|
17
|
-
| Structural | fan-out storms: many agents re-sending context | WARN 24 spawns, STOP before spawn 41, no agents-spawning-agents past depth 2 |
|
|
18
|
+
| Structural | fan-out storms: many agents re-sending context | WARN 24 spawns, STOP before spawn 41, no agents-spawning-agents past depth 2 | 59-, 190- and 226-spawn sessions; the 190-spawn session has a 4.35B post-STOP tail (92%) |
|
|
18
19
|
| Economic | slow grinds: few agents, enormous total | WARN 3.5B, STOP 5B tokens per session | the 9.15B / 26-spawn session the spawn cap could not see |
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
The 2026-09-15 replay snapshot: **4 STOP, 3 WARN, 431 clean across 438
|
|
22
|
+
sessions and 616 spawns**. Replay hero: **32%** — **10.27B** of **32.58B**
|
|
23
|
+
observed tokens came after a STOP boundary; API-list scenario **$4,639**.
|
|
24
|
+
This is an upper bound assuming no override or restart. Replay does not label
|
|
25
|
+
false positives. Active histories can change the next replay result.
|
|
21
26
|
|
|
22
|
-
Cache-read ratio was 98%
|
|
27
|
+
Cache-read ratio was about 98% in the original calibration, healthy and pathological alike. It
|
|
23
28
|
is shown as an explanation and never used to decide.
|
|
24
29
|
|
|
25
30
|
## Start with replay, not with the hook
|
|
@@ -155,8 +160,15 @@ spawn through with the reason on the ledger. Two things the docs got wrong
|
|
|
155
160
|
and the wire settled: the tool arrives as `spawn_agent` (the docs say it
|
|
156
161
|
"matches as Agent"; the matcher covers both), and project-local hooks only
|
|
157
162
|
load when the project is trusted. The captured payloads are in
|
|
158
|
-
`fixtures/codex-0.151.0-pretooluse.json` and drive a test.
|
|
159
|
-
|
|
163
|
+
`fixtures/codex-0.151.0-pretooluse.json` and drive a test.
|
|
164
|
+
|
|
165
|
+
One step Codex makes you do by hand: after `init codex --write`, open
|
|
166
|
+
`codex`, run `/hooks`, and trust the AgentGuard hook. Codex requires this
|
|
167
|
+
once per hook source, and there is no CLI for it. Until it is done,
|
|
168
|
+
`codex exec` stalls the first time a spawn would be gated (verified: it
|
|
169
|
+
hangs with no output; the same with hooks defined in `config.toml`).
|
|
170
|
+
`status` repeats this under the codex line because it cannot see whether
|
|
171
|
+
trust was granted.
|
|
160
172
|
|
|
161
173
|
**Cursor** is verified against the documented schema (`permission`,
|
|
162
174
|
`user_message`, `agent_message`; `~/.cursor/hooks.json` with `failClosed`),
|
|
@@ -178,20 +190,41 @@ Ten parallel `Agent` calls launch ten hook processes that all read the same
|
|
|
178
190
|
transcript and all see the same count. A naive cap is cosmetic during exactly
|
|
179
191
|
the burst it exists for. Spawns are admitted through an atomic, cross-process
|
|
180
192
|
reservation under a machine-wide lock; the test suite launches 60 real OS
|
|
181
|
-
processes against a cap of 40 and asserts exactly 40 are admitted,
|
|
182
|
-
|
|
193
|
+
reservation processes against a cap of 40 and asserts exactly 40 are admitted,
|
|
194
|
+
and separately exercises 60 Cursor hook processes. The opt-in stress test runs
|
|
195
|
+
240 actual Claude hook processes against cap 40:
|
|
196
|
+
|
|
197
|
+
```sh
|
|
198
|
+
npm run build
|
|
199
|
+
AGENTGUARD_STRESS=1 node --test dist/tests/stress.test.js
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
It checks exactly 40 admitted and 200 denied, verifies the receipt chain for
|
|
203
|
+
coordinated decisions, and reports lock failures separately. Infrastructure
|
|
204
|
+
failures deny in enforce mode before a receipt can be chained, as in the gateway.
|
|
183
205
|
|
|
184
206
|
0.2.0 fixed the lock itself. Under 240 concurrent hook processes the 0.1
|
|
185
207
|
lock could tear down a live sibling's lock (a waiter judged "owner is dead"
|
|
186
208
|
about an instance that had already been released and replaced) and admit
|
|
187
209
|
41 to 46. Lock instances now carry a nonce; a reclaim only counts if it
|
|
188
210
|
grabbed the instance it judged, and every write is fenced on the holder's
|
|
189
|
-
own nonce still being on the path.
|
|
190
|
-
|
|
211
|
+
own nonce still being on the path. In 0.2.3, bounded, staggered retries prevent
|
|
212
|
+
lock waiters from starving the holder. The Sep 15 audit includes one successful
|
|
213
|
+
240-process run after that fix; it does not establish a 192-run guarantee.
|
|
191
214
|
|
|
192
215
|
Single-machine by design. Two laptops on one account do not share state, and
|
|
193
216
|
that is stated rather than hidden.
|
|
194
217
|
|
|
218
|
+
## Account thresholds
|
|
219
|
+
|
|
220
|
+
`account.warnConcurrentSessions` warns when more than that many sessions have
|
|
221
|
+
been active on this machine in the last 30 minutes, matching `status` liveness.
|
|
222
|
+
Optional `account.warnTokens` and `account.stopTokens` sum the last
|
|
223
|
+
`account.windowActiveMinutes` of token buckets from each active session.
|
|
224
|
+
Both token limits default to `null`. Closed gateway sessions are excluded;
|
|
225
|
+
hook and gateway session files are both read. WARN never blocks. STOP records
|
|
226
|
+
a would-block decision in shadow and blocks new work only in enforce mode.
|
|
227
|
+
|
|
195
228
|
## What it never does
|
|
196
229
|
|
|
197
230
|
No prompts, responses, file contents, or tool inputs are persisted or rendered.
|
package/dist/src/cli.js
CHANGED
|
@@ -163,7 +163,7 @@ async function main(argv) {
|
|
|
163
163
|
process.stderr.write(`init: host must be claude, cursor or codex\n`);
|
|
164
164
|
return 64;
|
|
165
165
|
}
|
|
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
|
|
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
167
|
const tail = `Installed in ${policy.mode} mode. Nothing is blocked until you run: agentguard-burn enforce\n`;
|
|
168
168
|
if (has('--write')) {
|
|
169
169
|
let result;
|
package/dist/src/conformance.js
CHANGED
|
@@ -37,7 +37,9 @@ const GRIND_WARN_CALL = Math.floor(defaults_1.DEFAULT_THRESHOLDS.sustained.warnT
|
|
|
37
37
|
const GRIND_STOP_CALL = Math.floor(defaults_1.DEFAULT_THRESHOLDS.sustained.stopTokens / CALL_TOKENS) + 1; // 21
|
|
38
38
|
function freshHome() {
|
|
39
39
|
const home = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'agb-conf-'));
|
|
40
|
-
|
|
40
|
+
// These fixtures share a home to inspect all receipts. Test only the
|
|
41
|
+
// session boundaries here; account concurrency has its own integration tests.
|
|
42
|
+
const policy = { mode: 'enforce', thresholds: { ...defaults_1.DEFAULT_THRESHOLDS, account: { ...defaults_1.DEFAULT_THRESHOLDS.account, warnConcurrentSessions: Number.MAX_SAFE_INTEGER } } };
|
|
41
43
|
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(home, 'burn-policy.json'), JSON.stringify(policy));
|
|
42
44
|
return home;
|
|
43
45
|
}
|
package/dist/src/defaults.d.ts
CHANGED
|
@@ -2,14 +2,13 @@ import type { Policy, Thresholds } from './types';
|
|
|
2
2
|
/**
|
|
3
3
|
* Shipped thresholds.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* 409 clean sessions. Zero false STOPs.
|
|
5
|
+
* Originally fitted against 412 real Claude Code sessions. Replayed on the
|
|
6
|
+
* author's machine on 2026-09-15: 438 sessions, 616 spawns, 32.58B tokens;
|
|
7
|
+
* 4 STOP, 3 WARN, 431 clean. The observed post-STOP tail is 10.27B (32%).
|
|
8
|
+
* Fan-out 24/40 stops before spawn 41 on the 59-, 190- and 226-spawn
|
|
9
|
+
* sessions; sustained 3.5B/5B catches the 9.15B / 26-spawn slow grind.
|
|
10
|
+
* This is a dated replay snapshot and an upper bound, not labelled evidence
|
|
11
|
+
* of false-positive rates or guaranteed savings.
|
|
13
12
|
*
|
|
14
13
|
* A previous draft shipped a 50M session budget. Routine sessions on the same
|
|
15
14
|
* machine run 1.4B to 3.5B, so that default would have fired within minutes on
|
package/dist/src/defaults.js
CHANGED
|
@@ -4,14 +4,13 @@ exports.SHADOW_MIN_DAYS = exports.SHADOW_MIN_DECISIONS = exports.ACTIVE_GAP_CAP_
|
|
|
4
4
|
/**
|
|
5
5
|
* Shipped thresholds.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* 409 clean sessions. Zero false STOPs.
|
|
7
|
+
* Originally fitted against 412 real Claude Code sessions. Replayed on the
|
|
8
|
+
* author's machine on 2026-09-15: 438 sessions, 616 spawns, 32.58B tokens;
|
|
9
|
+
* 4 STOP, 3 WARN, 431 clean. The observed post-STOP tail is 10.27B (32%).
|
|
10
|
+
* Fan-out 24/40 stops before spawn 41 on the 59-, 190- and 226-spawn
|
|
11
|
+
* sessions; sustained 3.5B/5B catches the 9.15B / 26-spawn slow grind.
|
|
12
|
+
* This is a dated replay snapshot and an upper bound, not labelled evidence
|
|
13
|
+
* of false-positive rates or guaranteed savings.
|
|
15
14
|
*
|
|
16
15
|
* A previous draft shipped a 50M session budget. Routine sessions on the same
|
|
17
16
|
* machine run 1.4B to 3.5B, so that default would have fired within minutes on
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Two independent safety planes, because the data showed two independent
|
|
5
5
|
* failure modes:
|
|
6
6
|
*
|
|
7
|
-
* structural - fan-out. Many agents, each re-sending context. The
|
|
7
|
+
* structural - fan-out. Many agents, each re-sending context. The 190-spawn
|
|
8
8
|
* session. Caught by an absolute spawn cap.
|
|
9
9
|
* economic - sustained burn. Few agents, long session, enormous total. The
|
|
10
10
|
* 9.15B session with only 26 spawns, which a spawn cap cannot
|
|
@@ -21,5 +21,8 @@ import type { BurnReport, SessionState, Thresholds } from '../types';
|
|
|
21
21
|
* would have if the pending spawn were allowed; the decision must be about the
|
|
22
22
|
* proposal, not about a violation the transcript has already recorded.
|
|
23
23
|
*/
|
|
24
|
-
export declare function evaluate(state: SessionState, thresholds: Thresholds, proposedSpawnDepth?: number | null
|
|
24
|
+
export declare function evaluate(state: SessionState, thresholds: Thresholds, proposedSpawnDepth?: number | null, account?: {
|
|
25
|
+
sessions: SessionState[];
|
|
26
|
+
now: number;
|
|
27
|
+
}): BurnReport;
|
|
25
28
|
export declare function fmt(n: number): string;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Two independent safety planes, because the data showed two independent
|
|
6
6
|
* failure modes:
|
|
7
7
|
*
|
|
8
|
-
* structural - fan-out. Many agents, each re-sending context. The
|
|
8
|
+
* structural - fan-out. Many agents, each re-sending context. The 190-spawn
|
|
9
9
|
* session. Caught by an absolute spawn cap.
|
|
10
10
|
* economic - sustained burn. Few agents, long session, enormous total. The
|
|
11
11
|
* 9.15B session with only 26 spawns, which a spawn cap cannot
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
20
|
exports.evaluate = evaluate;
|
|
21
21
|
exports.fmt = fmt;
|
|
22
|
+
const account_1 = require("../state/account");
|
|
22
23
|
const session_1 = require("../state/session");
|
|
23
24
|
const RANK = { OK: 0, WARN: 1, STOP: 2 };
|
|
24
25
|
function worst(a, b) {
|
|
@@ -29,7 +30,7 @@ function worst(a, b) {
|
|
|
29
30
|
* would have if the pending spawn were allowed; the decision must be about the
|
|
30
31
|
* proposal, not about a violation the transcript has already recorded.
|
|
31
32
|
*/
|
|
32
|
-
function evaluate(state, thresholds, proposedSpawnDepth = null) {
|
|
33
|
+
function evaluate(state, thresholds, proposedSpawnDepth = null, account) {
|
|
33
34
|
const findings = [];
|
|
34
35
|
let verdict = 'OK';
|
|
35
36
|
// ---- structural plane: fan-out --------------------------------------
|
|
@@ -132,6 +133,27 @@ function evaluate(state, thresholds, proposedSpawnDepth = null) {
|
|
|
132
133
|
});
|
|
133
134
|
verdict = worst(verdict, 'WARN');
|
|
134
135
|
}
|
|
136
|
+
// Machine activity uses status's 30-minute liveness window. Token budgets
|
|
137
|
+
// sum each live session's last configured number of active-minute buckets.
|
|
138
|
+
if (account && thresholds.account) {
|
|
139
|
+
const t = thresholds.account;
|
|
140
|
+
const live = account.sessions.filter((s) => account.now - s.lastEventAt <= account_1.ACTIVE_SESSION_WINDOW_MS);
|
|
141
|
+
if (live.length > t.warnConcurrentSessions) {
|
|
142
|
+
findings.push({ detector: 'account', verdict: 'WARN',
|
|
143
|
+
summary: `${live.length} concurrent active sessions on this machine; warning threshold is ${t.warnConcurrentSessions}.`,
|
|
144
|
+
observed: live.length, threshold: t.warnConcurrentSessions });
|
|
145
|
+
verdict = worst(verdict, 'WARN');
|
|
146
|
+
}
|
|
147
|
+
const tokens = live.reduce((sum, s) => sum + (0, session_1.windowSum)(s.tokensByActiveMinute, s.activeMinutes, t.windowActiveMinutes), 0);
|
|
148
|
+
const severity = t.stopTokens != null && tokens >= t.stopTokens ? 'STOP'
|
|
149
|
+
: t.warnTokens != null && tokens >= t.warnTokens ? 'WARN' : null;
|
|
150
|
+
if (severity) {
|
|
151
|
+
findings.push({ detector: 'account', verdict: severity,
|
|
152
|
+
summary: `Active sessions consumed ${fmt(tokens)} tokens over their last ${t.windowActiveMinutes} active minutes.`,
|
|
153
|
+
observed: tokens, threshold: (severity === 'STOP' ? t.stopTokens : t.warnTokens) });
|
|
154
|
+
verdict = worst(verdict, severity);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
135
157
|
// ---- advisory: duplicate work --------------------------------------
|
|
136
158
|
let duplicated = 0;
|
|
137
159
|
let worstSurfaceReaders = 0;
|
|
@@ -192,6 +214,8 @@ function prescribe(state, findings, t) {
|
|
|
192
214
|
if (has('burn_debt')) {
|
|
193
215
|
out.push('Burn is sustained above your own baseline. Narrow the task or split it into a new session.');
|
|
194
216
|
}
|
|
217
|
+
if (has('account'))
|
|
218
|
+
out.push('Pause new work across active sessions on this machine until the account window clears.');
|
|
195
219
|
if (has('duplicate_work')) {
|
|
196
220
|
out.push('Assign one reader per file and pass a short digest down, instead of letting every agent re-read it.');
|
|
197
221
|
}
|
package/dist/src/gateway.js
CHANGED
|
@@ -41,6 +41,7 @@ const pre_tool_use_1 = require("./hook/pre-tool-use");
|
|
|
41
41
|
const override_1 = require("./override");
|
|
42
42
|
const receipt_1 = require("./receipt");
|
|
43
43
|
const reservations_1 = require("./state/reservations");
|
|
44
|
+
const account_1 = require("./state/account");
|
|
44
45
|
const session_1 = require("./state/session");
|
|
45
46
|
const MAX_SEEN_EVENTS = 4000;
|
|
46
47
|
const FILE_PREFIX = 'gw-';
|
|
@@ -214,7 +215,7 @@ class Gateway {
|
|
|
214
215
|
}
|
|
215
216
|
const live = new Map(meta.liveSpawns);
|
|
216
217
|
const proposedDepth = event.proposedDepth ?? (event.issuerId !== undefined && live.has(event.issuerId) ? live.get(event.issuerId) + 1 : 1);
|
|
217
|
-
const report = (0, evaluate_1.evaluate)(state, policy.thresholds, proposedDepth);
|
|
218
|
+
const report = (0, evaluate_1.evaluate)(state, policy.thresholds, proposedDepth, { sessions: (0, account_1.readAccountSessions)(this.home, state, event.at), now: event.at });
|
|
218
219
|
const reservation = tx.reserve({
|
|
219
220
|
sessionId: event.sessionId,
|
|
220
221
|
toolUseId: event.spawnId,
|
|
@@ -264,13 +265,13 @@ class Gateway {
|
|
|
264
265
|
this.applyUsage(event.callId, event.estimatedTokens, 0, event.at, state, calls);
|
|
265
266
|
}
|
|
266
267
|
meta.calls = [...calls];
|
|
267
|
-
const session = (0, evaluate_1.evaluate)(state, policy.thresholds, null);
|
|
268
|
+
const session = (0, evaluate_1.evaluate)(state, policy.thresholds, null, { sessions: (0, account_1.readAccountSessions)(this.home, state, event.at), now: event.at });
|
|
268
269
|
const local = (0, local_compute_1.evaluateLocalCompute)(compute, policy.thresholds);
|
|
269
270
|
const report = merge(session, local.findings);
|
|
270
271
|
const wouldBlock = report.verdict === 'STOP';
|
|
271
272
|
// A low-confidence session (proxy without a session header) can be
|
|
272
273
|
// stopped only on machine-scope grounds. Its session count is a guess.
|
|
273
|
-
const machineStop = local.verdict === 'STOP';
|
|
274
|
+
const machineStop = local.verdict === 'STOP' || session.findings.some((f) => f.detector === 'account' && f.verdict === 'STOP');
|
|
274
275
|
const due = wouldBlock && policy.mode === 'enforce' && (event.attribution === 'high' || machineStop);
|
|
275
276
|
const override = due ? (0, override_1.consumeOverride)(this.home, event.at) : null;
|
|
276
277
|
const blocked = due && !override;
|
|
@@ -49,6 +49,8 @@ export interface PersistedSession {
|
|
|
49
49
|
};
|
|
50
50
|
/** Signature of the last finding set the user was told about. */
|
|
51
51
|
notified?: string;
|
|
52
|
+
/** Additive chain head; old session files begin at genesis. */
|
|
53
|
+
lastReceipt?: string | null;
|
|
52
54
|
}
|
|
53
55
|
/** Inflate a persisted Claude Code session. Shared with status. */
|
|
54
56
|
export declare function inflateHookSession(raw: PersistedSession): SessionState;
|
|
@@ -22,6 +22,10 @@ exports.loadPolicy = loadPolicy;
|
|
|
22
22
|
exports.refreshSession = refreshSession;
|
|
23
23
|
exports.handlePreToolUse = handlePreToolUse;
|
|
24
24
|
exports.settingsSnippet = settingsSnippet;
|
|
25
|
+
const node_crypto_1 = require("node:crypto");
|
|
26
|
+
const events_1 = require("../events");
|
|
27
|
+
const receipt_1 = require("../receipt");
|
|
28
|
+
const account_1 = require("../state/account");
|
|
25
29
|
const node_fs_1 = require("node:fs");
|
|
26
30
|
const node_path_1 = require("node:path");
|
|
27
31
|
const evaluate_1 = require("../detectors/evaluate");
|
|
@@ -48,13 +52,13 @@ function loadSession(home, sessionId, firstEventAt) {
|
|
|
48
52
|
try {
|
|
49
53
|
const raw = JSON.parse((0, node_fs_1.readFileSync)(sessionFile(home, sessionId), 'utf8'));
|
|
50
54
|
const cursor = { ...raw.cursor, depthByUuid: new Map(raw.cursor.depthByUuid) };
|
|
51
|
-
return { cursor, state: inflateHookSession(raw), notified: raw.notified ?? '' };
|
|
55
|
+
return { cursor, state: inflateHookSession(raw), notified: raw.notified ?? '', lastReceipt: raw.lastReceipt ?? null };
|
|
52
56
|
}
|
|
53
57
|
catch {
|
|
54
|
-
return { cursor: (0, claude_transcript_1.newCursor)(), state: (0, session_1.newSessionState)(sessionId, firstEventAt), notified: '' };
|
|
58
|
+
return { cursor: (0, claude_transcript_1.newCursor)(), state: (0, session_1.newSessionState)(sessionId, firstEventAt), notified: '', lastReceipt: null };
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
|
-
function saveSession(home, cursor, state, notified) {
|
|
61
|
+
function saveSession(home, cursor, state, notified, lastReceipt) {
|
|
58
62
|
(0, node_fs_1.mkdirSync)((0, node_path_1.join)(home, 'sessions'), { recursive: true, mode: 0o700 });
|
|
59
63
|
const persisted = {
|
|
60
64
|
cursor: { offset: cursor.offset, size: cursor.size, malformedLines: cursor.malformedLines, depthByUuid: [...cursor.depthByUuid] },
|
|
@@ -65,6 +69,7 @@ function saveSession(home, cursor, state, notified) {
|
|
|
65
69
|
surfaceReaders: [...state.surfaceReaders].map(([k, v]) => [k, [...v]]),
|
|
66
70
|
},
|
|
67
71
|
notified,
|
|
72
|
+
lastReceipt,
|
|
68
73
|
};
|
|
69
74
|
const file = sessionFile(home, state.sessionId);
|
|
70
75
|
(0, node_fs_1.writeFileSync)(`${file}.tmp`, JSON.stringify(persisted), { mode: 0o600 });
|
|
@@ -99,14 +104,14 @@ function recordDecision(home, entry) {
|
|
|
99
104
|
}
|
|
100
105
|
/** Refresh session state from the transcript. Cheap: only new bytes are read. */
|
|
101
106
|
function refreshSession(home, sessionId, transcriptPath) {
|
|
102
|
-
const { cursor, state, notified } = loadSession(home, sessionId, Date.now());
|
|
107
|
+
const { cursor, state, notified, lastReceipt } = loadSession(home, sessionId, Date.now());
|
|
103
108
|
const before = state.spawnCount;
|
|
104
109
|
const events = (0, claude_transcript_1.readIncremental)(transcriptPath, cursor);
|
|
105
110
|
for (const event of events)
|
|
106
111
|
(0, session_1.applyEvent)(state, event);
|
|
107
112
|
if (events.length > 0 && state.startedAt > events[0].at)
|
|
108
113
|
state.startedAt = events[0].at;
|
|
109
|
-
saveSession(home, cursor, state, notified);
|
|
114
|
+
saveSession(home, cursor, state, notified, lastReceipt);
|
|
110
115
|
return { cursor, state, newSpawns: state.spawnCount - before, notified };
|
|
111
116
|
}
|
|
112
117
|
function rememberNotified(home, sessionId, signature) {
|
|
@@ -128,71 +133,88 @@ function handlePreToolUse(input, home, now = Date.now()) {
|
|
|
128
133
|
}
|
|
129
134
|
const policy = loadPolicy(home);
|
|
130
135
|
const store = new reservations_1.ReservationStore(home);
|
|
131
|
-
let report;
|
|
132
|
-
let reservation;
|
|
133
|
-
let notified = '';
|
|
134
136
|
try {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
137
|
+
const signer = receipt_1.ReceiptSigner.loadOrCreate(home);
|
|
138
|
+
return store.withLock((tx) => {
|
|
139
|
+
const refreshed = refreshSession(home, input.session_id, input.transcript_path);
|
|
140
|
+
const { state, newSpawns } = refreshed;
|
|
141
|
+
const notified = refreshed.notified;
|
|
142
|
+
if (newSpawns > 0)
|
|
143
|
+
tx.reconcile(input.session_id, state.spawnCount, state.spawnCount - newSpawns);
|
|
144
|
+
// Depth of the proposed child: the issuing agent's depth plus one. A hook
|
|
145
|
+
// fired inside a subagent carries agent_id; treat that as depth 1 issuer.
|
|
146
|
+
const proposedDepth = input.agent_id ? 2 : 1;
|
|
147
|
+
const report = (0, evaluate_1.evaluate)(state, policy.thresholds, proposedDepth, { sessions: (0, account_1.readAccountSessions)(home, state, now), now });
|
|
148
|
+
const reservation = tx.reserve({
|
|
149
|
+
sessionId: input.session_id,
|
|
150
|
+
toolUseId: input.tool_use_id ?? `${input.session_id}:${now}`,
|
|
151
|
+
observedSpawns: state.spawnCount,
|
|
152
|
+
ceiling: policy.thresholds.fanout.stop,
|
|
153
|
+
now,
|
|
154
|
+
});
|
|
155
|
+
const shouldDeny = report.verdict === 'STOP' || !reservation.allowed;
|
|
156
|
+
const reason = shouldDeny ? buildDenyReason(report, reservation) : '';
|
|
157
|
+
// The audited override: only consulted when a block is about to happen.
|
|
158
|
+
const override = shouldDeny && policy.mode === 'enforce' ? (0, override_1.consumeOverride)(home, now) : null;
|
|
159
|
+
const blocked = policy.mode === 'enforce' && shouldDeny && !override;
|
|
160
|
+
const verdict = shouldDeny ? 'STOP' : report.verdict;
|
|
161
|
+
const raw = JSON.parse((0, node_fs_1.readFileSync)(sessionFile(home, input.session_id), 'utf8'));
|
|
162
|
+
const receipt = signer.sign({
|
|
163
|
+
schema: 'agentguard.burn.decision.v1', decisionId: (0, node_crypto_1.randomUUID)(), at: now,
|
|
164
|
+
host: 'claude-code', action: 'spawn', sessionDigest: (0, receipt_1.sha256)(input.session_id),
|
|
165
|
+
policy: { mode: policy.mode, digest: (0, receipt_1.sha256)((0, receipt_1.canonical)(policy)) },
|
|
166
|
+
measured: { sessionTokens: state.totalTokens, sessionSpawns: reservation.effectiveSpawns,
|
|
167
|
+
proposedDepth, inFlight: 0, occupiedMs: 0 },
|
|
168
|
+
coverage: events_1.CAPABILITIES['claude-code'], verdict, blocked,
|
|
169
|
+
reasons: [...report.findings.map((f) => `${f.detector}:${f.verdict}`), ...(!reservation.allowed ? ['fanout:STOP'] : [])],
|
|
170
|
+
previous: raw.lastReceipt ?? null,
|
|
171
|
+
});
|
|
172
|
+
store.assertHeld();
|
|
173
|
+
(0, node_fs_1.appendFileSync)((0, node_path_1.join)(home, 'receipts.ndjson'), `${JSON.stringify(receipt)}\n`, { mode: 0o600 });
|
|
174
|
+
saveSession(home, refreshed.cursor, state, notified, (0, receipt_1.receiptDigest)(receipt));
|
|
175
|
+
recordDecision(home, {
|
|
176
|
+
host: 'claude-code', action: 'spawn',
|
|
177
|
+
at: now,
|
|
178
|
+
sessionId: input.session_id,
|
|
179
|
+
toolUseId: input.tool_use_id ?? null,
|
|
180
|
+
verdict,
|
|
181
|
+
wouldDeny: shouldDeny,
|
|
182
|
+
enforced: policy.mode === 'enforce' && shouldDeny && !override,
|
|
183
|
+
overridden: override ? { once: override.once, reason: override.reason } : undefined,
|
|
184
|
+
mode: policy.mode,
|
|
185
|
+
findings: report.findings.map((f) => ({ detector: f.detector, verdict: f.verdict, observed: f.observed, threshold: f.threshold })),
|
|
186
|
+
effectiveSpawns: reservation.effectiveSpawns,
|
|
187
|
+
totals: report.totals,
|
|
188
|
+
});
|
|
189
|
+
if (shouldDeny && policy.mode === 'enforce') {
|
|
190
|
+
if (!override)
|
|
191
|
+
return deny(reason);
|
|
192
|
+
return {
|
|
193
|
+
continue: true,
|
|
194
|
+
systemMessage: `AgentGuard STOP overridden${override.once ? ' once' : ''} ("${override.reason}"): ${report.findings[0]?.summary ?? ''}`,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
if (report.verdict !== 'OK') {
|
|
198
|
+
const signature = findingSignature(report);
|
|
199
|
+
if (signature === notified)
|
|
200
|
+
return { continue: true, suppressOutput: true };
|
|
201
|
+
rememberNotified(home, input.session_id, signature);
|
|
202
|
+
return {
|
|
203
|
+
continue: true,
|
|
204
|
+
systemMessage: `AgentGuard ${report.verdict}${policy.mode === 'shadow' && shouldDeny ? ' (shadow: would have blocked)' : ''}: ${report.findings[0]?.summary ?? ''}`,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
if (notified)
|
|
208
|
+
rememberNotified(home, input.session_id, '');
|
|
209
|
+
return { continue: true, suppressOutput: true };
|
|
150
210
|
});
|
|
151
211
|
}
|
|
152
212
|
catch (error) {
|
|
153
213
|
// Could not coordinate. Deny the spawn; never let a burst through blind.
|
|
154
214
|
const reason = `AgentGuard failed closed: ${error instanceof Error ? error.message : 'unknown error'}`;
|
|
155
|
-
recordDecision(home, { at: now, sessionId: input.session_id, verdict: 'STOP', enforced: policy.mode === 'enforce', reason, failClosed: true });
|
|
215
|
+
recordDecision(home, { host: 'claude-code', action: 'spawn', at: now, sessionId: input.session_id, verdict: 'STOP', enforced: policy.mode === 'enforce', reason, failClosed: true });
|
|
156
216
|
return policy.mode === 'enforce' ? deny(reason) : { continue: true, suppressOutput: true };
|
|
157
217
|
}
|
|
158
|
-
const shouldDeny = report.verdict === 'STOP' || !reservation.allowed;
|
|
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;
|
|
162
|
-
recordDecision(home, {
|
|
163
|
-
at: now,
|
|
164
|
-
sessionId: input.session_id,
|
|
165
|
-
toolUseId: input.tool_use_id ?? null,
|
|
166
|
-
verdict: report.verdict,
|
|
167
|
-
wouldDeny: shouldDeny,
|
|
168
|
-
enforced: policy.mode === 'enforce' && shouldDeny && !override,
|
|
169
|
-
overridden: override ? { once: override.once, reason: override.reason } : undefined,
|
|
170
|
-
mode: policy.mode,
|
|
171
|
-
findings: report.findings.map((f) => ({ detector: f.detector, verdict: f.verdict, observed: f.observed, threshold: f.threshold })),
|
|
172
|
-
effectiveSpawns: reservation.effectiveSpawns,
|
|
173
|
-
totals: report.totals,
|
|
174
|
-
});
|
|
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
|
-
}
|
|
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);
|
|
188
|
-
return {
|
|
189
|
-
continue: true,
|
|
190
|
-
systemMessage: `AgentGuard ${report.verdict}${policy.mode === 'shadow' && shouldDeny ? ' (shadow: would have blocked)' : ''}: ${report.findings[0]?.summary ?? ''}`,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
if (notified)
|
|
194
|
-
rememberNotified(home, input.session_id, '');
|
|
195
|
-
return { continue: true, suppressOutput: true };
|
|
196
218
|
}
|
|
197
219
|
function buildDenyReason(report, _reservation) {
|
|
198
220
|
// Claude Code shows this reason to the user. A box reads as an alarm; a
|
|
@@ -160,7 +160,7 @@ function renderReplay(summary, opts = {}) {
|
|
|
160
160
|
out.push(row(paint(on, C.dim, `${s.sessionId.slice(0, 8)} ${(0, evaluate_1.fmt)(s.totalTokens).padStart(6)} · ${String(s.spawns).padStart(3)} spawns ${detail}`)));
|
|
161
161
|
}
|
|
162
162
|
if (summary.sessions.length > top) {
|
|
163
|
-
out.push(row(paint(on, C.dim, `… ${summary.sessions.length - top} more
|
|
163
|
+
out.push(row(paint(on, C.dim, `… ${summary.sessions.length - top} more sessions (included in totals)`)));
|
|
164
164
|
}
|
|
165
165
|
out.push(MID);
|
|
166
166
|
out.push(row(paint(on, C.dim, 'Upper bound; assumes no override or restart. Nothing left this machine.')));
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ACTIVE_SESSION_WINDOW_MS = void 0;
|
|
4
|
+
exports.readAccountSessions = readAccountSessions;
|
|
5
|
+
/** Read the same hook and gateway session files used by machine status. */
|
|
6
|
+
const node_fs_1 = require("node:fs");
|
|
7
|
+
const node_path_1 = require("node:path");
|
|
8
|
+
exports.ACTIVE_SESSION_WINDOW_MS = 30 * 60 * 1000;
|
|
9
|
+
function readAccountSessions(home, current, now) {
|
|
10
|
+
const sessions = new Map();
|
|
11
|
+
let names;
|
|
12
|
+
try {
|
|
13
|
+
names = (0, node_fs_1.readdirSync)((0, node_path_1.join)(home, 'sessions'));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
names = [];
|
|
17
|
+
}
|
|
18
|
+
for (const name of names) {
|
|
19
|
+
if (!name.endsWith('.json'))
|
|
20
|
+
continue;
|
|
21
|
+
try {
|
|
22
|
+
const raw = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(home, 'sessions', name), 'utf8'));
|
|
23
|
+
if (!raw.state || (raw.closedAt !== undefined && raw.closedAt !== null))
|
|
24
|
+
continue;
|
|
25
|
+
const state = raw.state;
|
|
26
|
+
if (typeof state.sessionId !== 'string' || !Number.isFinite(state.lastEventAt))
|
|
27
|
+
continue;
|
|
28
|
+
const prior = sessions.get(state.sessionId);
|
|
29
|
+
if (!prior || state.lastEventAt > prior.lastEventAt) {
|
|
30
|
+
sessions.set(state.sessionId, { ...state, tokensByActiveMinute: new Map(state.tokensByActiveMinute) });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch { /* Ignore malformed or foreign session files, as status does. */ }
|
|
34
|
+
}
|
|
35
|
+
// Include this admission, even when its transcript has no new usage yet.
|
|
36
|
+
// Replacing by ID prevents double counting the caller's persisted snapshot.
|
|
37
|
+
sessions.set(current.sessionId, { ...current, lastEventAt: now });
|
|
38
|
+
return [...sessions.values()];
|
|
39
|
+
}
|
|
@@ -69,6 +69,7 @@ class ReservationStore {
|
|
|
69
69
|
/** Acquire the lock or throw. Callers must fail closed on throw. */
|
|
70
70
|
acquire() {
|
|
71
71
|
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
72
|
+
let attempt = 0;
|
|
72
73
|
for (;;) {
|
|
73
74
|
try {
|
|
74
75
|
(0, node_fs_1.mkdirSync)(this.lockDir, { mode: 0o700 });
|
|
@@ -90,7 +91,10 @@ class ReservationStore {
|
|
|
90
91
|
if (Date.now() > deadline) {
|
|
91
92
|
throw new Error('AgentGuard could not acquire the reservation lock; failing closed.');
|
|
92
93
|
}
|
|
93
|
-
|
|
94
|
+
// Hundreds of waiters polling together can starve the holder. Spread
|
|
95
|
+
// retries with bounded backoff while retaining the existing deadline.
|
|
96
|
+
const backoff = Math.min(250, LOCK_SPIN_MS * 2 ** Math.min(attempt++, 4));
|
|
97
|
+
sleepSync(Math.min(Math.max(1, deadline - Date.now()), backoff * (0.5 + Math.random() / 2)));
|
|
94
98
|
}
|
|
95
99
|
}
|
|
96
100
|
}
|
package/dist/src/status.js
CHANGED
|
@@ -68,6 +68,12 @@ function renderHostHealth(hosts) {
|
|
|
68
68
|
else
|
|
69
69
|
state = `installed, hook command ok (${h.file.replace(process.env.HOME ?? '', '~')})`;
|
|
70
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
|
+
}
|
|
71
77
|
}
|
|
72
78
|
return lines.join('\n');
|
|
73
79
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"captured_from": "codex-cli 0.151.0, 2026-09-03
|
|
2
|
+
"captured_from": "Synthetic identifiers; payload shape based on codex-cli 0.151.0 PreToolUse, 2026-09-03",
|
|
3
3
|
"payloads": [
|
|
4
4
|
{
|
|
5
|
-
"session_id": "
|
|
6
|
-
"turn_id": "
|
|
5
|
+
"session_id": "00000000-0000-7000-8000-000000000001",
|
|
6
|
+
"turn_id": "00000000-0000-7000-8000-000000000011",
|
|
7
7
|
"transcript_path": "/Users/example/.codex/sessions/2026/09/03/rollout-example.jsonl",
|
|
8
8
|
"cwd": "/Users/example/project",
|
|
9
9
|
"hook_event_name": "PreToolUse",
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
"tool_input": {
|
|
14
14
|
"command": "echo CANARY_OK"
|
|
15
15
|
},
|
|
16
|
-
"tool_use_id": "
|
|
16
|
+
"tool_use_id": "call_SYNTHETIC000000000000001"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
|
-
"session_id": "
|
|
20
|
-
"turn_id": "
|
|
19
|
+
"session_id": "00000000-0000-7000-8000-000000000002",
|
|
20
|
+
"turn_id": "00000000-0000-7000-8000-000000000012",
|
|
21
21
|
"transcript_path": "/Users/example/.codex/sessions/2026/09/03/rollout-example.jsonl",
|
|
22
22
|
"cwd": "/Users/example/project",
|
|
23
23
|
"hook_event_name": "PreToolUse",
|
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
"message": "<redacted>",
|
|
29
29
|
"fork_context": "<redacted>"
|
|
30
30
|
},
|
|
31
|
-
"tool_use_id": "
|
|
31
|
+
"tool_use_id": "call_SYNTHETIC000000000000002"
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
|
-
"session_id": "
|
|
35
|
-
"turn_id": "
|
|
34
|
+
"session_id": "00000000-0000-7000-8000-000000000002",
|
|
35
|
+
"turn_id": "00000000-0000-7000-8000-000000000012",
|
|
36
36
|
"transcript_path": "/Users/example/.codex/sessions/2026/09/03/rollout-example.jsonl",
|
|
37
37
|
"cwd": "/Users/example/project",
|
|
38
38
|
"hook_event_name": "PreToolUse",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"targets": "<redacted>",
|
|
44
44
|
"timeout_ms": "<redacted>"
|
|
45
45
|
},
|
|
46
|
-
"tool_use_id": "
|
|
46
|
+
"tool_use_id": "call_SYNTHETIC000000000000003"
|
|
47
47
|
}
|
|
48
48
|
]
|
|
49
|
-
}
|
|
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.3",
|
|
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",
|