@agentguard-run/burn 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +166 -0
- package/README.md +60 -0
- package/dist/src/calibrate.d.ts +33 -0
- package/dist/src/calibrate.js +124 -0
- package/dist/src/cli.d.ts +14 -0
- package/dist/src/cli.js +166 -0
- package/dist/src/defaults.d.ts +30 -0
- package/dist/src/defaults.js +53 -0
- package/dist/src/detectors/evaluate.d.ts +25 -0
- package/dist/src/detectors/evaluate.js +217 -0
- package/dist/src/history/claude-transcript.d.ts +48 -0
- package/dist/src/history/claude-transcript.js +175 -0
- package/dist/src/hook/pre-tool-use.d.ts +47 -0
- package/dist/src/hook/pre-tool-use.js +166 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +52 -0
- package/dist/src/replay/render.d.ts +11 -0
- package/dist/src/replay/render.js +89 -0
- package/dist/src/replay/simulate.d.ts +54 -0
- package/dist/src/replay/simulate.js +144 -0
- package/dist/src/state/reservations.d.ts +64 -0
- package/dist/src/state/reservations.js +176 -0
- package/dist/src/state/session.d.ts +19 -0
- package/dist/src/state/session.js +91 -0
- package/dist/src/types.d.ts +128 -0
- package/dist/src/types.js +11 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
AgentGuard(R) Burn - Alpha License
|
|
2
|
+
Copyright (c) 2026 Dunecrest Ventures Inc.
|
|
3
|
+
|
|
4
|
+
1. SCOPE.
|
|
5
|
+
This software is the npm package @agentguard-run/spend, comprising every file
|
|
6
|
+
present in an installed copy of that package. That includes the TypeScript
|
|
7
|
+
sources under src/, the compiled output under dist/, the command-line entry
|
|
8
|
+
point under bin/, the documentation under docs/, the fixtures under tests/, and
|
|
9
|
+
the root files README.md, README.es-419.md, README.pt-BR.md, CHANGELOG.md,
|
|
10
|
+
PATENTS.md and package.json. Any file the package ships that Section 3 does not
|
|
11
|
+
expressly name is licensed under this Section 1; the enumeration above is
|
|
12
|
+
illustrative, and adding a file to the package does not place it outside these
|
|
13
|
+
terms.
|
|
14
|
+
|
|
15
|
+
This software is licensed by Dunecrest Ventures Inc. ("Licensor") subject to the
|
|
16
|
+
following thresholds:
|
|
17
|
+
|
|
18
|
+
(a) Evaluation Use. Internal evaluation, prototyping, and non-commercial
|
|
19
|
+
development at any call volume.
|
|
20
|
+
|
|
21
|
+
(b) Free Production Threshold. Production deployments processing 10,000
|
|
22
|
+
or fewer enforcement calls per calendar month, in aggregate across
|
|
23
|
+
all instances operated by the licensee, are permitted under this
|
|
24
|
+
License without additional fee.
|
|
25
|
+
|
|
26
|
+
(c) Commercial License Required. Production deployments processing more
|
|
27
|
+
than 10,000 enforcement calls per calendar month, deployments
|
|
28
|
+
operated for the benefit of third parties as a service, redistribution,
|
|
29
|
+
sublicensing, public hosting, and republication each require a
|
|
30
|
+
separate commercial license agreement with Licensor.
|
|
31
|
+
|
|
32
|
+
Commercial-license inquiries: invest@agentguard.run
|
|
33
|
+
|
|
34
|
+
2. NO PATENT LICENSE GRANTED.
|
|
35
|
+
Nothing in this License grants, expressly or by implication, any patent license
|
|
36
|
+
to any patent, patent application, or other intellectual property right of
|
|
37
|
+
Licensor. All patent rights are expressly reserved.
|
|
38
|
+
|
|
39
|
+
3. SEPARATE GRANT FOR DEMONSTRATION ASSETS.
|
|
40
|
+
The following assets, and ONLY these assets, are released under the BSD
|
|
41
|
+
2-Clause License, reproduced in full below:
|
|
42
|
+
|
|
43
|
+
- The documentation files README.md, README.es-419.md and README.pt-BR.md
|
|
44
|
+
- The test fixture under tests/
|
|
45
|
+
|
|
46
|
+
Copyright (c) 2026 Dunecrest Ventures Inc.
|
|
47
|
+
|
|
48
|
+
Redistribution and use in source and binary forms, with or without
|
|
49
|
+
modification, are permitted provided that the following conditions are met:
|
|
50
|
+
|
|
51
|
+
1. Redistributions of source code must retain the above copyright notice,
|
|
52
|
+
this list of conditions and the following disclaimer.
|
|
53
|
+
|
|
54
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
55
|
+
this list of conditions and the following disclaimer in the documentation
|
|
56
|
+
and/or other materials provided with the distribution.
|
|
57
|
+
|
|
58
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
59
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
60
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
61
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
62
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
63
|
+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
64
|
+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
65
|
+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
66
|
+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
67
|
+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
68
|
+
POSSIBILITY OF SUCH DAMAGE.
|
|
69
|
+
|
|
70
|
+
This grant conveys copyright permission only. It grants no patent license,
|
|
71
|
+
express or implied, no covenant not to sue, and no exhaustion, estoppel, or
|
|
72
|
+
implied-license rights under any patent or patent application of Licensor,
|
|
73
|
+
consistent with Section 2 above. The BSD 2-Clause License is used for these
|
|
74
|
+
assets precisely because it is silent as to patents.
|
|
75
|
+
|
|
76
|
+
Nothing else the package ships is included in the grant above. The code under
|
|
77
|
+
src/, dist/ and bin/, the documentation under docs/, and the root files
|
|
78
|
+
CHANGELOG.md and PATENTS.md remain licensed only under the alpha evaluation
|
|
79
|
+
terms of Section 1. The TypeScript type definitions, policy engine, decision
|
|
80
|
+
log, store implementation, cost table, and wrapper code are in that group.
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
4. WARRANTY DISCLAIMER.
|
|
84
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
85
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
86
|
+
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
87
|
+
DUNECREST VENTURES INC. BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY
|
|
88
|
+
ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
89
|
+
DEALINGS IN THE SOFTWARE.
|
|
90
|
+
|
|
91
|
+
5. SUCCESSORS AND ASSIGNS.
|
|
92
|
+
This License binds and benefits the parties' respective successors and assigns.
|
|
93
|
+
In the event of an asset sale, merger, change of control, or other transfer of
|
|
94
|
+
the Licensor's rights in this software, all rights and obligations under this
|
|
95
|
+
License inure to the benefit of and are binding upon Licensor's successor or
|
|
96
|
+
assignee. Outstanding evaluation grants survive change-of-control, but the
|
|
97
|
+
successor or assignee may, upon thirty (30) days' written notice, terminate
|
|
98
|
+
ongoing evaluation grants in favor of a commercial-license requirement.
|
|
99
|
+
|
|
100
|
+
6. TERMINATION.
|
|
101
|
+
Licensor may terminate this License with thirty (30) days' written notice for
|
|
102
|
+
any reason or no reason. Upon termination, Licensee shall cease all use of the
|
|
103
|
+
software under src/, dist/ and bin/ and shall destroy all copies in Licensee's possession.
|
|
104
|
+
|
|
105
|
+
7. PATENT NOTICE (35 U.S.C. § 287).
|
|
106
|
+
Certain AgentGuard technologies are patent pending. All patent rights are
|
|
107
|
+
expressly reserved per Section 2 above.
|
|
108
|
+
|
|
109
|
+
AgentGuard(R) is a registered trademark of Dunecrest Ventures Inc.
|
|
110
|
+
(U.S. Reg. No. 8281464). MerchantGuard(TM) is a trademark of Dunecrest
|
|
111
|
+
Ventures Inc. (USPTO Serial No. 99051215, pending).
|
|
112
|
+
|
|
113
|
+
For commercial licensing: invest@agentguard.run
|
|
114
|
+
|
|
115
|
+
FUNCTIONAL-USE DISCLAIMER ADDENDUM
|
|
116
|
+
|
|
117
|
+
DISCLAIMER: All terminology and labels used in AgentGuard’s materials are descriptive of
|
|
118
|
+
software functionality only, not legal definitions or guarantees of compliance. For example, the
|
|
119
|
+
terms “receipt,” “audit log,” “evidence,” “audit trail,” and “attestation” refer solely to
|
|
120
|
+
cryptographically-signed records produced by AgentGuard’s software. These terms do not mean
|
|
121
|
+
that any record is legally binding evidence, certified by any authority, or equivalent to records
|
|
122
|
+
maintained by banks, auditors, or courts. Similarly, references to “signed,” “verified,” or “attested”
|
|
123
|
+
pertain to digital signature processes, not to notarization or governmental attestation. Any use of
|
|
124
|
+
the word “compliance,” “compliant,” “outcome,” or “settlement” describes the software’s features in
|
|
125
|
+
a non-regulatory sense. In particular, “capability tier” is merely an internal category of agent
|
|
126
|
+
permission levels, and does not correspond to any government or industry regulatory classification.
|
|
127
|
+
Likewise, any statement that an operation “settles” or an outcome is “settled” refers only to the
|
|
128
|
+
software’s final state, not to any legal settlement. No AgentGuard tool or document should be
|
|
129
|
+
interpreted as providing legal, financial, or regulatory advice, or as a certified compliance
|
|
130
|
+
certificate. AgentGuard is not a law firm or auditor and does not guarantee adherence to laws or
|
|
131
|
+
standards. All use of AgentGuard software is at the user’s risk, and users must ensure
|
|
132
|
+
independently that their use of AI agents complies with all applicable laws and
|
|
133
|
+
regulations.* (CONTINUED IN NEXT PARAGRAPH)
|
|
134
|
+
Throughout this documentation, package metadata, CLI banners, and related materials, any word
|
|
135
|
+
drawn from compliance or legal contexts (such as “audit,” “evidence,” “settlement,” “credit,” “debt,”
|
|
136
|
+
“market,” “liquidity,” “maturity,” or similar) is used only in a functional, descriptive sense. For
|
|
137
|
+
instance, “audit log” simply means a sequence of recorded events; it does not imply an official
|
|
138
|
+
financial or regulatory audit. The phrase “satisfies” is used to indicate that one software condition
|
|
139
|
+
meets a programmed check (e.g. satisfying a budget cap), and is not intended to suggest that any
|
|
140
|
+
legal requirement has been fulfilled. Under no circumstances should AgentGuard’s terminology be
|
|
141
|
+
taken to indicate that our software is executing regulated financial or legal processes.*
|
|
142
|
+
No term in our code, documentation, or marketing is meant to designate or characterize any
|
|
143
|
+
AgentGuard feature as a service subject to banking, securities, derivatives, insurance, real estate, or
|
|
144
|
+
other regulated activities. For example, “trade,” “trading,” “liquidity,” “maturity,” and similar terms
|
|
145
|
+
(if ever used) describe algorithmic budget or token flows, not financial transactions. AgentGuard
|
|
146
|
+
does not act as a broker-dealer, clearinghouse, insurance issuer, or government agency. References
|
|
147
|
+
to “market” or “liquidation” are metaphorical descriptions of how the software handles tokens or
|
|
148
|
+
budgets. No AgentGuard action should be construed as offering financial services or operating a
|
|
149
|
+
regulated marketplace.*
|
|
150
|
+
Likewise, words like “offer,” “obligation,” “credit,” “debt,” “payment,” or “settle” in our text refer only
|
|
151
|
+
to hypothetical or illustrative actions of a user’s AI agent or simulated transaction flows. They do
|
|
152
|
+
not mean that AgentGuard is engaging in any money-handling, offering financial instruments, or
|
|
153
|
+
guaranteeing any settlement of obligations. If AgentGuard generates an invoice or cost-of-service
|
|
154
|
+
figure, it is purely illustrative of resource usage, not a binding financial bill. The term “maker/taker”
|
|
155
|
+
(if used in examples) is purely a naming convention and has no relation to exchange regulation.
|
|
156
|
+
|
|
157
|
+
The software’s “payment execution” tier is only a capability checkbox, not an actual payment
|
|
158
|
+
processor. AgentGuard does not transmit money or credit.*
|
|
159
|
+
Finally, any references to “certify,” “verify,” or “attest” should be read in context of cryptography and
|
|
160
|
+
computing. For example, when we say a receipt is “signed” or “verified,” we mean via public-key
|
|
161
|
+
cryptography. AgentGuard does not claim that receipts are admissible legal evidence by default.
|
|
162
|
+
Users remain responsible for any legal implications of presenting AgentGuard logs or receipts in
|
|
163
|
+
regulatory audits or court. This disclaimer is intended to ensure AgentGuard’s terms (in this
|
|
164
|
+
README, license, package descriptions, CLI banner, customer agreements, and /llms.txt) make clear
|
|
165
|
+
we provide a technical audit tool only. The functionality is delivered “as-is” with no implied
|
|
166
|
+
regulatory endorsement.
|
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# @agentguard-run/burn
|
|
2
|
+
|
|
3
|
+
Local runaway-agent circuit breaker for AI coding agents.
|
|
4
|
+
|
|
5
|
+
Raw token counters did not help. You see "360k tokens" and keep going. This
|
|
6
|
+
tool does the three things a counter does not: it decides whether a session is
|
|
7
|
+
pathological, it blocks the next agent spawn when it is, and it tells you what
|
|
8
|
+
to do about it. Nothing leaves the machine.
|
|
9
|
+
|
|
10
|
+
## Two safety planes
|
|
11
|
+
|
|
12
|
+
Fitted against 412 real Claude Code sessions, which showed two independent
|
|
13
|
+
failure shapes that one detector cannot cover:
|
|
14
|
+
|
|
15
|
+
| Plane | Catches | Rule | Validated on |
|
|
16
|
+
|---|---|---|---|
|
|
17
|
+
| Structural | fan-out storms: many agents re-sending context | WARN 24 spawns, STOP before spawn 41, no agents-spawning-agents past depth 2 | 174- and 226-spawn sessions; STOP would have intercepted 91% of the runaway |
|
|
18
|
+
| 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
|
+
Across all 412 sessions: 3 STOP, 2 WARN, 407 clean. Zero false STOPs.
|
|
21
|
+
|
|
22
|
+
Cache-read ratio was 98% on every session, healthy and pathological alike. It
|
|
23
|
+
is shown as an explanation and never used to decide.
|
|
24
|
+
|
|
25
|
+
## Start with replay, not with the hook
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
npx @agentguard-run/burn replay
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Runs the detectors over your existing history and shows what enforcement would
|
|
32
|
+
have intercepted, when, and the observed tail after each stop. It is an upper
|
|
33
|
+
bound, labelled as such. Nobody installs a blocker cold.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
npx @agentguard-run/burn calibrate # fit thresholds to your own usage
|
|
37
|
+
npx @agentguard-run/burn init # print the settings.json hook snippet
|
|
38
|
+
npx @agentguard-run/burn status # shadow observations, eligibility
|
|
39
|
+
npx @agentguard-run/burn enforce # after 7 days and 50 decisions
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The hook installs in **shadow mode**: every decision is recorded, nothing is
|
|
43
|
+
blocked, until you have seen it be right.
|
|
44
|
+
|
|
45
|
+
## The concurrency guarantee
|
|
46
|
+
|
|
47
|
+
Ten parallel `Agent` calls launch ten hook processes that all read the same
|
|
48
|
+
transcript and all see the same count. A naive cap is cosmetic during exactly
|
|
49
|
+
the burst it exists for. Spawns are admitted through an atomic, cross-process
|
|
50
|
+
reservation under a machine-wide lock; the test suite launches 60 real OS
|
|
51
|
+
processes against a cap of 40 and asserts exactly 40 are admitted.
|
|
52
|
+
|
|
53
|
+
Single-machine by design. Two laptops on one account do not share state, and
|
|
54
|
+
that is stated rather than hidden.
|
|
55
|
+
|
|
56
|
+
## What it never does
|
|
57
|
+
|
|
58
|
+
No prompts, responses, file contents, or tool inputs are persisted or rendered.
|
|
59
|
+
No telemetry. No provider-quota guesses: it projects against your configured
|
|
60
|
+
policy, never against a subscription allowance it cannot see.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calibration: derive thresholds from the user's own history.
|
|
3
|
+
*
|
|
4
|
+
* Shipped defaults solve scale mismatch for one machine. Calibration solves it
|
|
5
|
+
* for everyone else. The statistics are chosen to survive the one input that
|
|
6
|
+
* would otherwise poison them: a user whose history is *already* pathological.
|
|
7
|
+
*
|
|
8
|
+
* Procedure:
|
|
9
|
+
* 1. Exclude sessions that breach immutable safety boundaries (they are the
|
|
10
|
+
* thing we are trying to catch, so they must not define "normal").
|
|
11
|
+
* 2. Winsorise the healthy set with median/MAD so one huge session cannot
|
|
12
|
+
* drag the percentile.
|
|
13
|
+
* 3. WARN at p90, STOP at p99 of the healthy set (type-7 quantiles).
|
|
14
|
+
* 4. Shrink toward the shipped defaults when the sample is small.
|
|
15
|
+
* 5. Clamp into a safe band, and keep WARN and STOP apart.
|
|
16
|
+
*
|
|
17
|
+
* What this does and does not solve, honestly: it removes the two-orders-of-
|
|
18
|
+
* magnitude scale error that a global default carries. It does not remove the
|
|
19
|
+
* policy choice. p90, p99, the band, and the exclusion boundary are decisions,
|
|
20
|
+
* and the report says so.
|
|
21
|
+
*/
|
|
22
|
+
import type { Policy, Thresholds } from './types';
|
|
23
|
+
import type { SessionReplay } from './replay/simulate';
|
|
24
|
+
export interface CalibrationResult {
|
|
25
|
+
policy: Policy;
|
|
26
|
+
sessionsTotal: number;
|
|
27
|
+
sessionsExcluded: number;
|
|
28
|
+
sessionsUsed: number;
|
|
29
|
+
rawWarnP90: number;
|
|
30
|
+
rawStopP99: number;
|
|
31
|
+
notes: string[];
|
|
32
|
+
}
|
|
33
|
+
export declare function calibrate(sessions: SessionReplay[], base?: Thresholds): CalibrationResult;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Calibration: derive thresholds from the user's own history.
|
|
4
|
+
*
|
|
5
|
+
* Shipped defaults solve scale mismatch for one machine. Calibration solves it
|
|
6
|
+
* for everyone else. The statistics are chosen to survive the one input that
|
|
7
|
+
* would otherwise poison them: a user whose history is *already* pathological.
|
|
8
|
+
*
|
|
9
|
+
* Procedure:
|
|
10
|
+
* 1. Exclude sessions that breach immutable safety boundaries (they are the
|
|
11
|
+
* thing we are trying to catch, so they must not define "normal").
|
|
12
|
+
* 2. Winsorise the healthy set with median/MAD so one huge session cannot
|
|
13
|
+
* drag the percentile.
|
|
14
|
+
* 3. WARN at p90, STOP at p99 of the healthy set (type-7 quantiles).
|
|
15
|
+
* 4. Shrink toward the shipped defaults when the sample is small.
|
|
16
|
+
* 5. Clamp into a safe band, and keep WARN and STOP apart.
|
|
17
|
+
*
|
|
18
|
+
* What this does and does not solve, honestly: it removes the two-orders-of-
|
|
19
|
+
* magnitude scale error that a global default carries. It does not remove the
|
|
20
|
+
* policy choice. p90, p99, the band, and the exclusion boundary are decisions,
|
|
21
|
+
* and the report says so.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.calibrate = calibrate;
|
|
25
|
+
const defaults_1 = require("./defaults");
|
|
26
|
+
const IMMUTABLE_MAX_SPAWNS = 40;
|
|
27
|
+
const IMMUTABLE_MAX_TOKENS = 6_000_000_000;
|
|
28
|
+
/**
|
|
29
|
+
* Sessions below this are not evidence about what a working session costs.
|
|
30
|
+
* On the author's machine 391 of 394 sessions were open-ask-close and sat
|
|
31
|
+
* around 10M tokens; percentiles over that distribution described nothing and
|
|
32
|
+
* produced a STOP that would have fired on every routine working session.
|
|
33
|
+
* Calibration must be fitted to the sessions the alarm is actually for.
|
|
34
|
+
*/
|
|
35
|
+
const MIN_WORKING_SESSION_TOKENS = 50_000_000;
|
|
36
|
+
const BAND = {
|
|
37
|
+
warn: { lo: 1_500_000_000, hi: 3_500_000_000 },
|
|
38
|
+
stop: { lo: 3_000_000_000, hi: 5_000_000_000 },
|
|
39
|
+
separation: 500_000_000,
|
|
40
|
+
};
|
|
41
|
+
const MIN_SESSIONS_FOR_FULL_TRUST = 20;
|
|
42
|
+
function quantile(sorted, p) {
|
|
43
|
+
if (sorted.length === 0)
|
|
44
|
+
return 0;
|
|
45
|
+
const h = (sorted.length - 1) * p;
|
|
46
|
+
const lo = Math.floor(h);
|
|
47
|
+
const hi = Math.ceil(h);
|
|
48
|
+
return sorted[lo] + (sorted[hi] - sorted[lo]) * (h - lo);
|
|
49
|
+
}
|
|
50
|
+
function median(values) {
|
|
51
|
+
const s = [...values].sort((a, b) => a - b);
|
|
52
|
+
return quantile(s, 0.5);
|
|
53
|
+
}
|
|
54
|
+
function winsorise(values) {
|
|
55
|
+
if (values.length < 4)
|
|
56
|
+
return values;
|
|
57
|
+
const m = median(values);
|
|
58
|
+
const mad = median(values.map((v) => Math.abs(v - m))) || 1;
|
|
59
|
+
const cap = m + 5 * 1.4826 * mad;
|
|
60
|
+
return values.map((v) => Math.min(v, cap));
|
|
61
|
+
}
|
|
62
|
+
function clamp(v, lo, hi) {
|
|
63
|
+
return Math.max(lo, Math.min(hi, v));
|
|
64
|
+
}
|
|
65
|
+
function calibrate(sessions, base = defaults_1.DEFAULT_THRESHOLDS) {
|
|
66
|
+
const notes = [];
|
|
67
|
+
const working = sessions.filter((s) => s.totalTokens >= MIN_WORKING_SESSION_TOKENS);
|
|
68
|
+
const trivial = sessions.length - working.length;
|
|
69
|
+
if (trivial > 0) {
|
|
70
|
+
notes.push(`${trivial} short session(s) under ${MIN_WORKING_SESSION_TOKENS / 1e6}M tokens ignored; they are not evidence about working-session cost.`);
|
|
71
|
+
}
|
|
72
|
+
const healthy = working.filter((s) => s.spawns <= IMMUTABLE_MAX_SPAWNS && s.totalTokens <= IMMUTABLE_MAX_TOKENS);
|
|
73
|
+
const excluded = working.length - healthy.length;
|
|
74
|
+
if (excluded > 0) {
|
|
75
|
+
notes.push(`${excluded} session(s) excluded as already pathological (>${IMMUTABLE_MAX_SPAWNS} spawns or >6B tokens). They must not define normal.`);
|
|
76
|
+
}
|
|
77
|
+
const tokens = winsorise(healthy.map((s) => s.totalTokens)).sort((a, b) => a - b);
|
|
78
|
+
const rawWarn = quantile(tokens, 0.9);
|
|
79
|
+
const rawStop = quantile(tokens, 0.99);
|
|
80
|
+
// Shrink toward defaults when the sample is thin.
|
|
81
|
+
const trust = Math.min(1, healthy.length / MIN_SESSIONS_FOR_FULL_TRUST);
|
|
82
|
+
const blendedWarn = trust * rawWarn + (1 - trust) * base.sustained.warnTokens;
|
|
83
|
+
const blendedStop = trust * rawStop + (1 - trust) * base.sustained.stopTokens;
|
|
84
|
+
if (trust < 1)
|
|
85
|
+
notes.push(`Only ${healthy.length} usable sessions; thresholds shrunk ${Math.round((1 - trust) * 100)}% toward shipped defaults.`);
|
|
86
|
+
let warn = clamp(blendedWarn, BAND.warn.lo, BAND.warn.hi);
|
|
87
|
+
let stop = clamp(blendedStop, BAND.stop.lo, BAND.stop.hi);
|
|
88
|
+
if (stop - warn < BAND.separation) {
|
|
89
|
+
stop = Math.min(BAND.stop.hi, warn + BAND.separation);
|
|
90
|
+
warn = Math.min(warn, stop - BAND.separation);
|
|
91
|
+
}
|
|
92
|
+
// Burn-debt baseline: tokens per active minute across healthy sessions.
|
|
93
|
+
const perMinute = healthy
|
|
94
|
+
.filter((s) => s.activeMinutes > 5)
|
|
95
|
+
.map((s) => s.totalTokens / s.activeMinutes);
|
|
96
|
+
const debtEnabled = perMinute.length >= MIN_SESSIONS_FOR_FULL_TRUST;
|
|
97
|
+
const baseline = debtEnabled ? median(perMinute) : 0;
|
|
98
|
+
if (!debtEnabled) {
|
|
99
|
+
notes.push(`Burn-debt detector stays off: needs ${MIN_SESSIONS_FOR_FULL_TRUST}+ sessions with >5 active minutes, have ${perMinute.length}.`);
|
|
100
|
+
}
|
|
101
|
+
const thresholds = {
|
|
102
|
+
...base,
|
|
103
|
+
sustained: { warnTokens: Math.round(warn), stopTokens: Math.round(stop) },
|
|
104
|
+
burnDebt: {
|
|
105
|
+
...base.burnDebt,
|
|
106
|
+
baselinePerActiveMinute: Math.round(baseline),
|
|
107
|
+
enabled: debtEnabled,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
notes.push('Calibration fixes the scale of the alarm. Policy still decides how much unusual is allowed: p90/p99, the safety band, and the exclusion boundary are choices, not measurements.');
|
|
111
|
+
return {
|
|
112
|
+
policy: {
|
|
113
|
+
mode: 'shadow',
|
|
114
|
+
thresholds,
|
|
115
|
+
calibration: { sessionsSampled: healthy.length, generatedAt: Date.now(), method: 'winsorised p90/p99 with shrinkage and safety band' },
|
|
116
|
+
},
|
|
117
|
+
sessionsTotal: sessions.length,
|
|
118
|
+
sessionsExcluded: excluded,
|
|
119
|
+
sessionsUsed: healthy.length,
|
|
120
|
+
rawWarnP90: rawWarn,
|
|
121
|
+
rawStopP99: rawStop,
|
|
122
|
+
notes,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* agentguard-burn
|
|
4
|
+
*
|
|
5
|
+
* replay what enforcement would have stopped, on your history
|
|
6
|
+
* calibrate fit thresholds to your own usage, write shadow policy
|
|
7
|
+
* status current mode, shadow decisions, promotion eligibility
|
|
8
|
+
* init install the PreToolUse hook (shadow mode)
|
|
9
|
+
* enforce promote shadow -> enforce, once eligible
|
|
10
|
+
* shadow demote back to shadow
|
|
11
|
+
* resume --once one audited override of the next STOP
|
|
12
|
+
* hook (internal) stdin -> stdout hook entry point
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* agentguard-burn
|
|
5
|
+
*
|
|
6
|
+
* replay what enforcement would have stopped, on your history
|
|
7
|
+
* calibrate fit thresholds to your own usage, write shadow policy
|
|
8
|
+
* status current mode, shadow decisions, promotion eligibility
|
|
9
|
+
* init install the PreToolUse hook (shadow mode)
|
|
10
|
+
* enforce promote shadow -> enforce, once eligible
|
|
11
|
+
* shadow demote back to shadow
|
|
12
|
+
* resume --once one audited override of the next STOP
|
|
13
|
+
* hook (internal) stdin -> stdout hook entry point
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
const node_fs_1 = require("node:fs");
|
|
17
|
+
const node_os_1 = require("node:os");
|
|
18
|
+
const node_path_1 = require("node:path");
|
|
19
|
+
const calibrate_1 = require("./calibrate");
|
|
20
|
+
const defaults_1 = require("./defaults");
|
|
21
|
+
const pre_tool_use_1 = require("./hook/pre-tool-use");
|
|
22
|
+
const render_1 = require("./replay/render");
|
|
23
|
+
const simulate_1 = require("./replay/simulate");
|
|
24
|
+
const HOME = process.env.AGENTGUARD_HOME ?? (0, node_path_1.join)((0, node_os_1.homedir)(), '.agentguard');
|
|
25
|
+
function savePolicy(policy) {
|
|
26
|
+
(0, node_fs_1.mkdirSync)(HOME, { recursive: true, mode: 0o700 });
|
|
27
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(HOME, 'burn-policy.json'), JSON.stringify(policy, null, 2), { mode: 0o600 });
|
|
28
|
+
}
|
|
29
|
+
function readDecisions() {
|
|
30
|
+
const file = (0, node_path_1.join)(HOME, 'decisions.ndjson');
|
|
31
|
+
if (!(0, node_fs_1.existsSync)(file))
|
|
32
|
+
return [];
|
|
33
|
+
return (0, node_fs_1.readFileSync)(file, 'utf8')
|
|
34
|
+
.split('\n')
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.map((l) => {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(l);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
.filter((x) => x !== null);
|
|
45
|
+
}
|
|
46
|
+
function shadowEligibility() {
|
|
47
|
+
const decisions = readDecisions();
|
|
48
|
+
const first = decisions.length ? Number(decisions[0].at) : Date.now();
|
|
49
|
+
const days = (Date.now() - first) / 86_400_000;
|
|
50
|
+
const wouldBlock = decisions.filter((d) => d.wouldDeny === true).length;
|
|
51
|
+
const warns = decisions.filter((d) => d.verdict === 'WARN').length;
|
|
52
|
+
return {
|
|
53
|
+
eligible: decisions.length >= defaults_1.SHADOW_MIN_DECISIONS && days >= defaults_1.SHADOW_MIN_DAYS,
|
|
54
|
+
decisions: decisions.length,
|
|
55
|
+
days,
|
|
56
|
+
wouldBlock,
|
|
57
|
+
warns,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async function main(argv) {
|
|
61
|
+
const [command, ...rest] = argv;
|
|
62
|
+
const flag = (name) => {
|
|
63
|
+
const i = rest.indexOf(name);
|
|
64
|
+
return i >= 0 ? rest[i + 1] : undefined;
|
|
65
|
+
};
|
|
66
|
+
const has = (name) => rest.includes(name);
|
|
67
|
+
switch (command) {
|
|
68
|
+
case 'hook': {
|
|
69
|
+
const raw = (0, node_fs_1.readFileSync)(0, 'utf8');
|
|
70
|
+
let input = {};
|
|
71
|
+
try {
|
|
72
|
+
input = JSON.parse(raw);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Unparseable payload: allow. We never break the host over our own bug.
|
|
76
|
+
}
|
|
77
|
+
process.stdout.write(JSON.stringify((0, pre_tool_use_1.handlePreToolUse)(input, HOME)));
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
case 'replay': {
|
|
81
|
+
const policy = (0, pre_tool_use_1.loadPolicy)(HOME);
|
|
82
|
+
const paths = rest.filter((a) => a.endsWith('.jsonl'));
|
|
83
|
+
const targets = paths.length ? paths : (0, simulate_1.discoverTranscripts)();
|
|
84
|
+
const summary = (0, simulate_1.replayAll)(targets, policy.thresholds, Number(flag('--min-tokens') ?? 0));
|
|
85
|
+
if (has('--json')) {
|
|
86
|
+
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
process.stdout.write(`${(0, render_1.renderReplay)(summary, { colour: !has('--no-color'), top: Number(flag('--top') ?? 8) })}\n`);
|
|
90
|
+
}
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
case 'calibrate': {
|
|
94
|
+
const summary = (0, simulate_1.replayAll)((0, simulate_1.discoverTranscripts)(), defaults_1.DEFAULT_POLICY.thresholds);
|
|
95
|
+
const result = (0, calibrate_1.calibrate)(summary.sessions);
|
|
96
|
+
savePolicy(result.policy);
|
|
97
|
+
const t = result.policy.thresholds;
|
|
98
|
+
process.stdout.write([
|
|
99
|
+
`Calibrated from ${result.sessionsUsed} of ${result.sessionsTotal} sessions (${result.sessionsExcluded} excluded as pathological).`,
|
|
100
|
+
` raw p90 ${(result.rawWarnP90 / 1e9).toFixed(2)}B raw p99 ${(result.rawStopP99 / 1e9).toFixed(2)}B`,
|
|
101
|
+
` sustained WARN ${(t.sustained.warnTokens / 1e9).toFixed(2)}B STOP ${(t.sustained.stopTokens / 1e9).toFixed(2)}B`,
|
|
102
|
+
` fan-out WARN ${t.fanout.warn} STOP ${t.fanout.stop} (not calibrated; validated on real runaway)`,
|
|
103
|
+
` burn-debt ${t.burnDebt.enabled ? `ON, baseline ${Math.round(t.burnDebt.baselinePerActiveMinute / 1e6)}M/active-min` : 'off'}`,
|
|
104
|
+
...result.notes.map((n) => ` note: ${n}`),
|
|
105
|
+
`Policy written to ${(0, node_path_1.join)(HOME, 'burn-policy.json')} in SHADOW mode.`,
|
|
106
|
+
].join('\n') + '\n');
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
case 'status': {
|
|
110
|
+
const policy = (0, pre_tool_use_1.loadPolicy)(HOME);
|
|
111
|
+
const e = shadowEligibility();
|
|
112
|
+
process.stdout.write([
|
|
113
|
+
`mode: ${policy.mode}`,
|
|
114
|
+
`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`,
|
|
115
|
+
policy.calibration ? `calibrated from ${policy.calibration.sessionsSampled} sessions` : 'using shipped defaults (run: agentguard-burn calibrate)',
|
|
116
|
+
`shadow observation: ${e.decisions} decisions over ${e.days.toFixed(1)} days`,
|
|
117
|
+
` would have warned: ${e.warns} would have blocked: ${e.wouldBlock}`,
|
|
118
|
+
`eligible for enforcement: ${e.eligible ? 'yes' : `no (need ${defaults_1.SHADOW_MIN_DECISIONS} decisions and ${defaults_1.SHADOW_MIN_DAYS} days)`}`,
|
|
119
|
+
].join('\n') + '\n');
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
case 'init': {
|
|
123
|
+
const policy = (0, pre_tool_use_1.loadPolicy)(HOME);
|
|
124
|
+
savePolicy(policy);
|
|
125
|
+
const command = `node ${(0, node_path_1.join)(__dirname, 'cli.js')} hook`;
|
|
126
|
+
process.stdout.write(`Add this to ~/.claude/settings.json (merge into existing "hooks"):\n\n${JSON.stringify((0, pre_tool_use_1.settingsSnippet)(command), null, 2)}\n\nInstalled in ${policy.mode} mode. Nothing is blocked until you run: agentguard-burn enforce\n`);
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
case 'enforce': {
|
|
130
|
+
const policy = (0, pre_tool_use_1.loadPolicy)(HOME);
|
|
131
|
+
const e = shadowEligibility();
|
|
132
|
+
if (!e.eligible && !has('--force')) {
|
|
133
|
+
process.stderr.write(`Not eligible yet: ${e.decisions}/${defaults_1.SHADOW_MIN_DECISIONS} decisions, ${e.days.toFixed(1)}/${defaults_1.SHADOW_MIN_DAYS} days. Use --force to override.\n`);
|
|
134
|
+
return 2;
|
|
135
|
+
}
|
|
136
|
+
savePolicy({ ...policy, mode: 'enforce' });
|
|
137
|
+
process.stdout.write('Enforcement ON. The next STOP will block the spawn.\n');
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
case 'shadow': {
|
|
141
|
+
savePolicy({ ...(0, pre_tool_use_1.loadPolicy)(HOME), mode: 'shadow' });
|
|
142
|
+
process.stdout.write('Back to shadow mode. Decisions are recorded, nothing is blocked.\n');
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
case 'resume': {
|
|
146
|
+
const reason = flag('--reason') ?? 'no reason given';
|
|
147
|
+
(0, node_fs_1.mkdirSync)(HOME, { recursive: true, mode: 0o700 });
|
|
148
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(HOME, 'override.json'), JSON.stringify({ at: Date.now(), once: has('--once'), reason }), { mode: 0o600 });
|
|
149
|
+
process.stdout.write(`Override recorded${has('--once') ? ' for the next STOP only' : ''}: ${reason}\n`);
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
default:
|
|
153
|
+
process.stdout.write('agentguard-burn <replay|calibrate|status|init|enforce|shadow|resume|hook>\n' +
|
|
154
|
+
' replay [files...] [--json] [--top N] [--min-tokens N]\n' +
|
|
155
|
+
' calibrate fit thresholds to your history (writes shadow policy)\n' +
|
|
156
|
+
' status mode, shadow observations, eligibility\n' +
|
|
157
|
+
' init print the settings.json hook snippet\n' +
|
|
158
|
+
' enforce [--force] shadow -> enforce\n' +
|
|
159
|
+
' resume --once --reason "..."\n');
|
|
160
|
+
return command ? 64 : 0;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
main(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
|
|
164
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
165
|
+
process.exit(4);
|
|
166
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Policy, Thresholds } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Shipped thresholds.
|
|
4
|
+
*
|
|
5
|
+
* These are not intuition. They were fitted against 412 real Claude Code
|
|
6
|
+
* sessions on the author's machine on 2026-09-03:
|
|
7
|
+
*
|
|
8
|
+
* fan-out 24/40 : on the 172-spawn runaway, STOP at spawn 40 would have
|
|
9
|
+
* prevented 91% of that session's tokens.
|
|
10
|
+
* sustained 3.5B/5B : across all 412 sessions this produced exactly 1 STOP,
|
|
11
|
+
* the 9.15B slow grind the fan-out cap missed, 2 WARNs, and
|
|
12
|
+
* 409 clean sessions. Zero false STOPs.
|
|
13
|
+
*
|
|
14
|
+
* A previous draft shipped a 50M session budget. Routine sessions on the same
|
|
15
|
+
* machine run 1.4B to 3.5B, so that default would have fired within minutes on
|
|
16
|
+
* everything and trained the user to ignore the tool. Wrong defaults that get
|
|
17
|
+
* disabled are worse than no defaults.
|
|
18
|
+
*
|
|
19
|
+
* Cache-read ratio was 98% on every one of the 412 sessions, healthy and
|
|
20
|
+
* pathological alike. It carries no discriminating signal and is deliberately
|
|
21
|
+
* absent from these thresholds.
|
|
22
|
+
*/
|
|
23
|
+
export declare const DEFAULT_THRESHOLDS: Thresholds;
|
|
24
|
+
/** First-run policy. Shadow: evaluate and record, never block. */
|
|
25
|
+
export declare const DEFAULT_POLICY: Policy;
|
|
26
|
+
/** Idle gaps longer than this do not count as active time. */
|
|
27
|
+
export declare const ACTIVE_GAP_CAP_MS: number;
|
|
28
|
+
/** Shadow must observe this much before enforcement is allowed. */
|
|
29
|
+
export declare const SHADOW_MIN_DECISIONS = 50;
|
|
30
|
+
export declare const SHADOW_MIN_DAYS = 7;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SHADOW_MIN_DAYS = exports.SHADOW_MIN_DECISIONS = exports.ACTIVE_GAP_CAP_MS = exports.DEFAULT_POLICY = exports.DEFAULT_THRESHOLDS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Shipped thresholds.
|
|
6
|
+
*
|
|
7
|
+
* These are not intuition. They were fitted against 412 real Claude Code
|
|
8
|
+
* sessions on the author's machine on 2026-09-03:
|
|
9
|
+
*
|
|
10
|
+
* fan-out 24/40 : on the 172-spawn runaway, STOP at spawn 40 would have
|
|
11
|
+
* prevented 91% of that session's tokens.
|
|
12
|
+
* sustained 3.5B/5B : across all 412 sessions this produced exactly 1 STOP,
|
|
13
|
+
* the 9.15B slow grind the fan-out cap missed, 2 WARNs, and
|
|
14
|
+
* 409 clean sessions. Zero false STOPs.
|
|
15
|
+
*
|
|
16
|
+
* A previous draft shipped a 50M session budget. Routine sessions on the same
|
|
17
|
+
* machine run 1.4B to 3.5B, so that default would have fired within minutes on
|
|
18
|
+
* everything and trained the user to ignore the tool. Wrong defaults that get
|
|
19
|
+
* disabled are worse than no defaults.
|
|
20
|
+
*
|
|
21
|
+
* Cache-read ratio was 98% on every one of the 412 sessions, healthy and
|
|
22
|
+
* pathological alike. It carries no discriminating signal and is deliberately
|
|
23
|
+
* absent from these thresholds.
|
|
24
|
+
*/
|
|
25
|
+
exports.DEFAULT_THRESHOLDS = {
|
|
26
|
+
fanout: { warn: 24, stop: 40, maxDepth: 2 },
|
|
27
|
+
sustained: { warnTokens: 3_500_000_000, stopTokens: 5_000_000_000 },
|
|
28
|
+
burnDebt: {
|
|
29
|
+
baselinePerActiveMinute: 0,
|
|
30
|
+
tolerance: 1.25,
|
|
31
|
+
warnDebt: 750_000_000,
|
|
32
|
+
stopDebt: 1_500_000_000,
|
|
33
|
+
enabled: false,
|
|
34
|
+
},
|
|
35
|
+
spawnRate: { windowActiveMinutes: 15, warn: 8, stop: 16, enforce: false },
|
|
36
|
+
duplicate: { warnReaders: 3 },
|
|
37
|
+
account: {
|
|
38
|
+
stopTokens: null,
|
|
39
|
+
warnTokens: null,
|
|
40
|
+
windowActiveMinutes: 300,
|
|
41
|
+
warnConcurrentSessions: 4,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
/** First-run policy. Shadow: evaluate and record, never block. */
|
|
45
|
+
exports.DEFAULT_POLICY = {
|
|
46
|
+
mode: 'shadow',
|
|
47
|
+
thresholds: exports.DEFAULT_THRESHOLDS,
|
|
48
|
+
};
|
|
49
|
+
/** Idle gaps longer than this do not count as active time. */
|
|
50
|
+
exports.ACTIVE_GAP_CAP_MS = 5 * 60 * 1000;
|
|
51
|
+
/** Shadow must observe this much before enforcement is allowed. */
|
|
52
|
+
exports.SHADOW_MIN_DECISIONS = 50;
|
|
53
|
+
exports.SHADOW_MIN_DAYS = 7;
|