@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 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 {};
@@ -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;