@agentguard-run/burn 0.2.7 → 0.3.1

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.
Files changed (60) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +78 -0
  3. package/dist/src/adapters/codex.js +6 -0
  4. package/dist/src/canvas.d.ts +36 -0
  5. package/dist/src/canvas.js +132 -0
  6. package/dist/src/cli.d.ts +2 -0
  7. package/dist/src/cli.js +116 -25
  8. package/dist/src/defaults.d.ts +1 -1
  9. package/dist/src/defaults.js +4 -1
  10. package/dist/src/frames.d.ts +42 -0
  11. package/dist/src/frames.js +108 -0
  12. package/dist/src/gateway.js +3 -0
  13. package/dist/src/hook/pre-tool-use.d.ts +1 -0
  14. package/dist/src/hook/pre-tool-use.js +12 -1
  15. package/dist/src/idle/cache.d.ts +8 -0
  16. package/dist/src/idle/cache.js +70 -0
  17. package/dist/src/idle/classify.d.ts +13 -0
  18. package/dist/src/idle/classify.js +133 -0
  19. package/dist/src/idle/cli.d.ts +2 -0
  20. package/dist/src/idle/cli.js +72 -0
  21. package/dist/src/idle/collect.d.ts +38 -0
  22. package/dist/src/idle/collect.js +543 -0
  23. package/dist/src/idle/hook.d.ts +17 -0
  24. package/dist/src/idle/hook.js +61 -0
  25. package/dist/src/idle/platform.d.ts +25 -0
  26. package/dist/src/idle/platform.js +154 -0
  27. package/dist/src/idle/reap.d.ts +41 -0
  28. package/dist/src/idle/reap.js +262 -0
  29. package/dist/src/idle/render.d.ts +10 -0
  30. package/dist/src/idle/render.js +108 -0
  31. package/dist/src/idle/types.d.ts +56 -0
  32. package/dist/src/idle/types.js +8 -0
  33. package/dist/src/install.js +4 -2
  34. package/dist/src/live-panel.d.ts +42 -0
  35. package/dist/src/live-panel.js +283 -0
  36. package/dist/src/policy.d.ts +3 -1
  37. package/dist/src/policy.js +49 -3
  38. package/dist/src/presentation.d.ts +5 -0
  39. package/dist/src/presentation.js +23 -0
  40. package/dist/src/recording.d.ts +22 -0
  41. package/dist/src/recording.js +102 -0
  42. package/dist/src/render-recording.d.ts +15 -0
  43. package/dist/src/render-recording.js +200 -0
  44. package/dist/src/replay/render.js +5 -0
  45. package/dist/src/types.d.ts +6 -0
  46. package/docs/LIVE_PANEL_2026_09.md +55 -0
  47. package/docs/assets/burn-live-sep19-stop-1920.png +0 -0
  48. package/docs/assets/burn-live-sep19-stop-390.png +0 -0
  49. package/docs/assets/burn-live-sep19.evidence.json +288 -0
  50. package/docs/assets/burn-live-sep19.gif +0 -0
  51. package/docs/assets/burn-live-sep19.jsonl +45 -0
  52. package/docs/assets/burn-live-sep19.mp4 +0 -0
  53. package/docs/assets/burn-live-sep19.mp4.provenance.json +292 -0
  54. package/docs/assets/burn-live-sep19.observations.jsonl +45 -0
  55. package/docs/assets/burn-live-sep19.receipt.json +38 -0
  56. package/docs/burn-idle-audit.md +71 -0
  57. package/docs/burn-render.md +37 -0
  58. package/fixtures/live-panel-104x35.txt +35 -0
  59. package/fixtures/live-panel-80x24.txt +24 -0
  60. package/package.json +4 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.1 (2026-09-20)
4
+
5
+ - Add the live session screen: block-digit counters for spawns, tokens in window and replay share, a spawn-rate bar against the policy ceiling, the last decisions with verdict chips, and a two-second STOP hold, all through the canvas. The same screen renders recorded runs.
6
+ - Ship the September 19 recorded run as docs/assets/burn-live-sep19 with an mp4, gif, evidence and provenance files.
7
+
8
+ ## 0.3.0 (2026-09-20)
9
+
10
+ - Add local ps and JSON audits for Claude Code and Codex processes, leftover plugin daemons, automation browsers, workspace disk and current swap. Report held memory and cost exposure with explicit unknown evidence.
11
+ - Add interactive reap with typed row selection, SIGTERM only, a bounded wait and a result for each selection. Never delete workspaces, force-kill processes or close reap itself or its ancestors. Refuse recent terminal activity, dirty or unverified working directories and changed process identities.
12
+ - Add configurable idle-session, browser and orphan-workspace WARN thresholds, including team threshold files. SessionStart uses a five-minute metadata cache and a bounded advisory check that skips on timeout and never blocks.
13
+ - Keep existing plain STOP and WARN hook cards and no-color command bytes unchanged. Add a fixed-grid colored Canvas for terminal screens, with clipped cells, three-row block digits, bars and smaller-terminal layouts.
14
+ - Add opt-in timestamped structured frame recording for commands and offline recorded-run MP4/GIF rendering, local system fonts, numbered PNG fallback and SHA256 provenance. No audit or rendering data leaves the machine.
15
+ - Validation: 248 tests pass with no skips, including the 240-process stress test: 40 admitted, 200 denied, 240 signed receipts and zero lock failures.
16
+
3
17
  ## 0.2.7 (2026-09-19)
4
18
 
5
19
  - Fix the STOP badge padding so every card row is 66 visible characters wide. Preserve the card wording.
package/README.md CHANGED
@@ -16,6 +16,56 @@ The two init commands print hook configuration for review. Merge the relevant sn
16
16
 
17
17
  For Claude Code, configure its `statusLine` command as `agentguard-burn statusline`. It receives the host JSON on stdin. Codex 0.154.0 has no external status command slot. Use `agentguard-burn statusline SESSION_ID` in a companion terminal and the native Codex limit indicators. See [host fields and setup](docs/USAGE_AND_PRICING.md#what-the-hosts-expose).
18
18
 
19
+ ## Local idle audit
20
+
21
+ Burn 0.3.0 reports held memory and disk cost exposure on this machine:
22
+
23
+ ```sh
24
+ agentguard-burn ps
25
+ agentguard-burn ps --json
26
+ agentguard-burn reap
27
+ ```
28
+
29
+ `ps` lists Claude Code and Codex sessions with their Node children, leftover plugin daemons, automation-owned Chrome browsers, and candidate workspaces. Process rows are ranked by resident memory, then workspace rows by disk size. Normal Chrome profiles are excluded. Browser rows show their process count, total resident memory and available window count. Unknown values are explicit. RSS can count shared memory more than once; it is not a bill.
30
+
31
+ Session idle time uses the controlling terminal's access time when readable, otherwise transcript file-write metadata. The table labels the method. A transcript fallback can warn but cannot prove that a terminal has been inactive for 24 hours. Workspace rows show size, last modification, dirty status and open-handle evidence. No transcript content or process arguments are printed or uploaded. Swap is reported only: swap is only released by the operating system on reboot. Burn does not touch it.
32
+
33
+ `reap` prints the same numbered audit, then requires an interactive terminal and numbers typed at the prompt. There is no `--yes` flag and piped confirmation is refused. It sends only SIGTERM to the selected eligible processes, waits, and reports whether they exited. It never sends SIGKILL and never deletes files or directories.
34
+
35
+ Reap refuses:
36
+
37
+ - Any session with terminal input in the last 24 hours, or unavailable terminal-idle evidence. Changing a warning threshold does not change this safety limit.
38
+ - Any process whose working directory has modified or untracked files, or whose directory safety cannot be verified.
39
+ - Every workspace, including any workspace with open file handles. Workspace rows are information only.
40
+ - The process running reap and all its ancestors, including a selected group containing one of them.
41
+ - A process whose identity, ownership or safety evidence changed while confirmation was pending. New or replaced PIDs are never added to the confirmed selection.
42
+
43
+ It rechecks the selected processes before signaling. Permission errors and processes that ignore SIGTERM are reported. They are not force-closed.
44
+
45
+ Add these fields under `thresholds` in the existing `~/.agentguard/burn-policy.json` (or `AGENTGUARD_HOME/burn-policy.json`):
46
+
47
+ ```json
48
+ {
49
+ "thresholds": {
50
+ "idle_session_warn_hours": 24,
51
+ "idle_browser_warn_minutes": 60,
52
+ "orphan_workspace_warn_days": 2
53
+ }
54
+ }
55
+ ```
56
+
57
+ Merge the fields into your existing policy. Missing or invalid values use the shipped defaults. A local `teamPolicyFile` can point to a JSON file containing `thresholds`; relative paths resolve from the Burn home. Team threshold values take precedence over local threshold values, including these three audit settings. Neither policy loading nor the audit rewrites your policy.
58
+
59
+ The Claude Code and Codex hook snippets now include SessionStart. Review the new `agentguard-burn init claude` or `agentguard-burn init codex` output to update existing installations. At session start, a fresh five-minute local audit cache is checked. A cache miss gets a bounded scan with a 150 ms budget; timeout or unavailable evidence skips the advisory. A warning is one line, never a table and never a deny:
60
+
61
+ ```text
62
+ N idle agent sessions holding X GB, run agentguard-burn ps
63
+ ```
64
+
65
+ The line is WARN-only in both shadow and enforce modes. If only browser or workspace rows cross their thresholds, the session count can be zero; run `ps` for those details. The full `ps` command refreshes the cache. All collection and caching stays local.
66
+
67
+ macOS uses local process, file-handle and swap metadata. Linux uses `/proc` and available local tools, and says what could not be read. Windows prints `not supported on this platform yet` and exits successfully. See the [docs page draft](docs/burn-idle-audit.md) for the audit boundary and limitations.
68
+
19
69
  ## Why this session
20
70
 
21
71
  ```sh
@@ -311,7 +361,35 @@ Both token limits default to `null`. Closed gateway sessions are excluded;
311
361
  hook and gateway session files are both read. WARN never blocks. STOP records
312
362
  a would-block decision in shadow and blocks new work only in enforce mode.
313
363
 
364
+ ## Terminal canvas and local recordings
365
+
366
+ Interactive screens use a fixed 104 by 35 cell canvas, with mint for good, slate for neutral, amber for WARN and red for STOP. Smaller terminals drop side detail and paginate rows without wrapping. Use --no-color for the existing plain-text commands and hook cards. Hook JSON and the one-line statusline remain host protocols, with no canvas control sequences inserted.
367
+
368
+ Any command accepts an explicit local recording path:
369
+
370
+ ```sh
371
+ agentguard-burn ps --record ./idle-run.jsonl
372
+ agentguard-burn status --no-color --record ./status-run.jsonl
373
+ agentguard-burn replay --record ./history-run.jsonl
374
+ agentguard-burn render ./idle-run.jsonl --mp4 ./idle-run.mp4 --gif ./idle-run.gif
375
+ ```
376
+
377
+ Each JSONL frame contains a wall-clock timestamp and structured observations, never rendered terminal text. Audit recordings omit raw process arguments and the full process table. They can contain local paths, PIDs, identifiers and resource measurements, so review them before sharing. Recording is opt-in and stays on disk. A recording failure prints a notice without changing a hook decision or cleanup result. Appended recordings must have nondecreasing timestamps.
378
+
379
+ The render command draws recorded Canvas cells at 1920 by 1080 using installed Menlo on macOS or DejaVu Sans Mono on Linux. The title says RECORDED RUN and the footer says recorded run. It preserves frame timestamp intervals and holds the final frame for one second. ffmpeg encodes the MP4 and optional GIF; without it, numbered PNG files and a reproducible ffmpeg command remain. No fonts or assets are fetched. The native PNG dependency loads only for render, never during session-start checks.
380
+
381
+ A provenance JSON beside the requested MP4 records the source recording's SHA256, renderer version, render time, font and frame times. Existing outputs are refused. The source recording stays unchanged. See [the render draft](docs/burn-render.md) for the format and limits.
382
+
314
383
  ## What it never does
315
384
 
316
385
  No prompts, responses, file contents, or tool inputs are persisted or rendered.
317
386
  No telemetry. No provider-quota guesses. Account forecasts use only observed host percentages, never an invented allowance or cache weighting.
387
+
388
+ ### Local live panel
389
+
390
+ `agentguard-burn live` displays spawn and token counts within the active window,
391
+ session cache-read share, the policy rate bar and the last eight local decisions.
392
+ Use `--session ID`, `--once`, `--record run.jsonl`, or `--replay run.jsonl`.
393
+ STOP reasons hold for two seconds while observations continue. The display is
394
+ local and does not affect admission. Missing observations remain unknown.
395
+ See [the live panel and recorded September 19 evidence](docs/LIVE_PANEL_2026_09.md).
@@ -34,6 +34,7 @@ const node_fs_1 = require("node:fs");
34
34
  const events_1 = require("../events");
35
35
  const render_1 = require("../replay/render");
36
36
  const live_1 = require("../insights/live");
37
+ const hook_1 = require("../idle/hook");
37
38
  const pre_tool_use_1 = require("../hook/pre-tool-use");
38
39
  /** Fields this renderer must never emit. Codex rejects the whole object if they appear. */
39
40
  exports.CODEX_FORBIDDEN_FIELDS = ['continue', 'stopReason', 'suppressOutput', 'decision', 'reason'];
@@ -43,6 +44,10 @@ function handleCodexHook(raw, gateway, now = Date.now()) {
43
44
  const parsed = parse(raw);
44
45
  if (!parsed)
45
46
  return {};
47
+ if (parsed.event === 'SessionStart') {
48
+ const warning = (0, hook_1.sessionStartWarning)(gateway.dataDirectory, (0, pre_tool_use_1.loadPolicy)(gateway.dataDirectory), now);
49
+ return warning ? { systemMessage: warning } : {};
50
+ }
46
51
  const observation = (0, live_1.observeTool)(gateway.dataDirectory, { session_id: parsed.sessionId, transcript_path: parsed.transcriptPath, tool_name: parsed.toolName }, 'codex', (0, pre_tool_use_1.loadPolicy)(gateway.dataDirectory), now);
47
52
  const output = parsed.event === 'PreToolUse' && SPAWN_TOOLS.has(parsed.toolName) ? handleCodexSpawnHook(raw, gateway, now) : {};
48
53
  return observation.messages.length ? { ...output, systemMessage: [output.systemMessage, ...observation.messages].filter(Boolean).join('\n') } : output;
@@ -200,6 +205,7 @@ function walk(value, path) {
200
205
  function codexHooksSnippet(command) {
201
206
  return {
202
207
  hooks: {
208
+ SessionStart: [{ matcher: '.*', hooks: [{ type: 'command', command, timeout: 1 }] }],
203
209
  PreToolUse: [{ matcher: '.*', hooks: [{ type: 'command', command, timeout: 15 }] }],
204
210
  },
205
211
  };
@@ -0,0 +1,36 @@
1
+ export type CanvasColor = 'mint' | 'slate' | 'amber' | 'red' | 'white' | 'none';
2
+ export type CanvasColorMode = 'truecolor' | '256' | 'none';
3
+ export interface CanvasCell {
4
+ char: string;
5
+ color: CanvasColor;
6
+ }
7
+ export interface CanvasSnapshot {
8
+ width: number;
9
+ height: number;
10
+ cells: CanvasCell[][];
11
+ }
12
+ export declare const CANVAS_PALETTE: Record<Exclude<CanvasColor, 'none'>, {
13
+ hex: string;
14
+ rgb: [number, number, number];
15
+ ansi256: number;
16
+ }>;
17
+ export declare class Canvas {
18
+ readonly width: number;
19
+ readonly height: number;
20
+ readonly colorMode: CanvasColorMode;
21
+ private readonly cells;
22
+ constructor(options?: {
23
+ width?: number;
24
+ height?: number;
25
+ colour?: boolean;
26
+ colorMode?: CanvasColorMode;
27
+ });
28
+ put(row: number, col: number, text: string, color?: CanvasColor): this;
29
+ digits(row: number, col: number, value: string | number, color?: CanvasColor): this;
30
+ bar(row: number, col: number, width: number, fraction: number, color?: CanvasColor): this;
31
+ rule(row: number, col: number, width?: number): this;
32
+ header(left?: string, right?: string): this;
33
+ footer(hints: string, clock: string): this;
34
+ toJSON(): CanvasSnapshot;
35
+ render(): string;
36
+ }
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Canvas = exports.CANVAS_PALETTE = void 0;
4
+ /** A clipped grid of terminal cells. Rendering adds only palette-owned ANSI. */
5
+ const node_util_1 = require("node:util");
6
+ exports.CANVAS_PALETTE = {
7
+ mint: { hex: '#2ABCB4', rgb: [42, 188, 180], ansi256: 37 },
8
+ slate: { hex: '#64748B', rgb: [100, 116, 139], ansi256: 66 },
9
+ amber: { hex: '#F59E0B', rgb: [245, 158, 11], ansi256: 208 },
10
+ red: { hex: '#EF4444', rgb: [239, 68, 68], ansi256: 203 },
11
+ white: { hex: '#E2E8F0', rgb: [226, 232, 240], ansi256: 254 },
12
+ };
13
+ const GLYPHS = {
14
+ '0': ['█▀█', '█ █', '█▄█'], '1': [' ▀█', ' █', ' █'],
15
+ '2': ['▀▀█', '▄▀▀', '█▄▄'], '3': ['▀▀█', ' ▀█', '▄▄█'],
16
+ '4': ['█ █', '▀▀█', ' █'], '5': ['█▀▀', '▀▀█', '▄▄█'],
17
+ '6': ['█▀▀', '█▀█', '█▄█'], '7': ['▀▀█', ' █', ' █'],
18
+ '8': ['█▀█', '█▀█', '█▄█'], '9': ['█▀█', '▀▀█', '▄▄█'],
19
+ '.': [' ', ' ', '▄'], '-': [' ', '▀▀▀', ' '],
20
+ };
21
+ const colors = new Set(['mint', 'slate', 'amber', 'red', 'white', 'none']);
22
+ function dimension(value, fallback, limit) {
23
+ return Math.max(1, Math.min(limit, Math.floor(typeof value === 'number' && Number.isFinite(value) ? value : fallback)));
24
+ }
25
+ function wide(code) {
26
+ return code >= 0x1100 && (code <= 0x115f || code === 0x2329 || code === 0x232a
27
+ || (code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) || (code >= 0xac00 && code <= 0xd7a3)
28
+ || (code >= 0xf900 && code <= 0xfaff) || (code >= 0xfe10 && code <= 0xfe19)
29
+ || (code >= 0xfe30 && code <= 0xfe6f) || (code >= 0xff00 && code <= 0xff60)
30
+ || (code >= 0xffe0 && code <= 0xffe6) || (code >= 0x1f000 && code <= 0x1faff)
31
+ || (code >= 0x20000 && code <= 0x3fffd));
32
+ }
33
+ /** Keep one printable column per cell; preserve bullets and box-drawing rules. */
34
+ function characters(text) {
35
+ return Array.from((0, node_util_1.stripVTControlCharacters)(String(text)).replace(/[\r\n\t]/g, ' ').replace(/[\p{Cc}\p{Cf}\p{M}]/gu, ''))
36
+ .map(char => wide(char.codePointAt(0)) ? '?' : char);
37
+ }
38
+ class Canvas {
39
+ width;
40
+ height;
41
+ colorMode;
42
+ cells;
43
+ constructor(options = {}) {
44
+ this.width = dimension(options.width, Math.min(104, process.stdout.columns || 104), 500);
45
+ this.height = dimension(options.height, Math.min(35, process.stdout.rows || 35), 200);
46
+ this.colorMode = options.colour === false ? 'none' : options.colorMode
47
+ ?? (options.colour !== true && process.env.NO_COLOR !== undefined ? 'none' : /^(truecolor|24bit)$/i.test(process.env.COLORTERM || '') ? 'truecolor' : '256');
48
+ this.cells = Array.from({ length: this.height }, () => Array.from({ length: this.width }, () => ({ char: ' ', color: 'none' })));
49
+ }
50
+ put(row, col, text, color = 'white') {
51
+ if (!Number.isFinite(row) || !Number.isFinite(col))
52
+ return this;
53
+ row = Math.floor(row);
54
+ col = Math.floor(col);
55
+ if (row < 0 || row >= this.height || col >= this.width)
56
+ return this;
57
+ const selected = colors.has(color) ? color : 'none';
58
+ for (const [index, char] of characters(text).entries()) {
59
+ const x = col + index;
60
+ if (x >= this.width)
61
+ break;
62
+ if (x >= 0)
63
+ this.cells[row][x] = { char, color: selected };
64
+ }
65
+ return this;
66
+ }
67
+ digits(row, col, value, color = 'mint') {
68
+ let x = col;
69
+ for (const char of characters(String(value))) {
70
+ const glyph = GLYPHS[char] ?? [' ', ' ', ' '];
71
+ for (let y = 0; y < 3; y++)
72
+ this.put(row + y, x, glyph[y], color);
73
+ x += glyph[0].length + 1;
74
+ if (x >= this.width)
75
+ break;
76
+ }
77
+ return this;
78
+ }
79
+ bar(row, col, width, fraction, color = 'mint') {
80
+ if (!Number.isFinite(width) || width <= 0)
81
+ return this;
82
+ const length = Math.min(500, Math.floor(width));
83
+ const ratio = Number.isFinite(fraction) ? Math.max(0, Math.min(1, fraction)) : 0;
84
+ const filled = Math.round(length * ratio);
85
+ return this.put(row, col, '█'.repeat(filled) + '░'.repeat(length - filled), color);
86
+ }
87
+ rule(row, col, width = this.width - col) {
88
+ if (!Number.isFinite(width) || width <= 0)
89
+ return this;
90
+ return this.put(row, col, '─'.repeat(Math.min(500, Math.floor(width))), 'slate');
91
+ }
92
+ header(left = 'AGENTGUARD / BURN', right = 'LIVE') {
93
+ const rightChars = characters(right).slice(0, this.width);
94
+ const start = this.width - rightChars.length;
95
+ this.put(0, 0, characters(left).slice(0, Math.max(0, start - (rightChars.length ? 2 : 0))).join(''), 'mint');
96
+ this.put(0, start, rightChars.join(''), 'slate');
97
+ if (this.height > 1)
98
+ this.rule(1, 0);
99
+ return this;
100
+ }
101
+ footer(hints, clock) {
102
+ if (this.height > 1)
103
+ this.rule(this.height - 2, 0);
104
+ const clockChars = characters(clock).slice(0, this.width), start = this.width - clockChars.length;
105
+ this.put(this.height - 1, 0, characters(hints).slice(0, Math.max(0, start - (clockChars.length ? 2 : 0))).join(''), 'slate');
106
+ return this.put(this.height - 1, start, clockChars.join(''), 'white');
107
+ }
108
+ toJSON() {
109
+ return { width: this.width, height: this.height, cells: this.cells.map(row => row.map(cell => ({ ...cell }))) };
110
+ }
111
+ render() {
112
+ return this.cells.map(row => {
113
+ if (this.colorMode === 'none')
114
+ return row.map(cell => cell.char).join('');
115
+ let current = 'none', text = '';
116
+ for (const cell of row) {
117
+ if (cell.color !== current) {
118
+ current = cell.color;
119
+ if (current === 'none')
120
+ text += '\x1b[0m';
121
+ else {
122
+ const color = exports.CANVAS_PALETTE[current];
123
+ text += this.colorMode === 'truecolor' ? `\x1b[38;2;${color.rgb.join(';')}m` : `\x1b[38;5;${color.ansi256}m`;
124
+ }
125
+ }
126
+ text += cell.char;
127
+ }
128
+ return text + (current !== 'none' ? '\x1b[0m' : '');
129
+ }).join('\n');
130
+ }
131
+ }
132
+ exports.Canvas = Canvas;
package/dist/src/cli.d.ts CHANGED
@@ -2,6 +2,8 @@
2
2
  /**
3
3
  * agentguard-burn
4
4
  *
5
+ * ps local agent processes, held memory and orphan workspace disk
6
+ * reap interactively SIGTERM selected safe processes, never delete
5
7
  * replay what enforcement would have stopped, on your history
6
8
  * calibrate fit thresholds to your own usage, write shadow policy
7
9
  * status mode, shadow decisions, eligibility, every host's sessions
package/dist/src/cli.js CHANGED
@@ -3,6 +3,8 @@
3
3
  /**
4
4
  * agentguard-burn
5
5
  *
6
+ * ps local agent processes, held memory and orphan workspace disk
7
+ * reap interactively SIGTERM selected safe processes, never delete
6
8
  * replay what enforcement would have stopped, on your history
7
9
  * calibrate fit thresholds to your own usage, write shadow policy
8
10
  * status mode, shadow decisions, eligibility, every host's sessions
@@ -16,7 +18,42 @@
16
18
  * cursor-hook (internal) Cursor hook entry point
17
19
  * codex-hook (internal) Codex hook entry point
18
20
  */
21
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ var desc = Object.getOwnPropertyDescriptor(m, k);
24
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
25
+ desc = { enumerable: true, get: function() { return m[k]; } };
26
+ }
27
+ Object.defineProperty(o, k2, desc);
28
+ }) : (function(o, m, k, k2) {
29
+ if (k2 === undefined) k2 = k;
30
+ o[k2] = m[k];
31
+ }));
32
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
33
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
34
+ }) : function(o, v) {
35
+ o["default"] = v;
36
+ });
37
+ var __importStar = (this && this.__importStar) || (function () {
38
+ var ownKeys = function(o) {
39
+ ownKeys = Object.getOwnPropertyNames || function (o) {
40
+ var ar = [];
41
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
42
+ return ar;
43
+ };
44
+ return ownKeys(o);
45
+ };
46
+ return function (mod) {
47
+ if (mod && mod.__esModule) return mod;
48
+ var result = {};
49
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
50
+ __setModuleDefault(result, mod);
51
+ return result;
52
+ };
53
+ })();
19
54
  Object.defineProperty(exports, "__esModule", { value: true });
55
+ const presentation_1 = require("./presentation");
56
+ const recording_1 = require("./recording");
20
57
  const node_fs_1 = require("node:fs");
21
58
  const node_os_1 = require("node:os");
22
59
  const node_path_1 = require("node:path");
@@ -37,8 +74,12 @@ const eligibility_1 = require("./eligibility");
37
74
  const sessions_1 = require("./insights/sessions");
38
75
  const transcript_1 = require("./insights/transcript");
39
76
  const live_1 = require("./insights/live");
77
+ const attribution_1 = require("./insights/attribution");
78
+ const pricing_1 = require("./insights/pricing");
79
+ const transcript_2 = require("./insights/transcript");
40
80
  const render_2 = require("./insights/render");
41
81
  const blocks_1 = require("./insights/blocks");
82
+ const cli_1 = require("./idle/cli");
42
83
  const HOME = process.env.AGENTGUARD_HOME ?? (0, node_path_1.join)((0, node_os_1.homedir)(), '.agentguard');
43
84
  const PROXY_HOSTS = ['ollama', 'vllm', 'lm-studio', 'openai-compatible'];
44
85
  function readStdinJson() {
@@ -75,23 +116,46 @@ function shadowEligibility() {
75
116
  return (0, eligibility_1.computeShadowEligibility)(readDecisions());
76
117
  }
77
118
  async function main(argv) {
78
- const [command, ...rest] = argv;
119
+ const [command, ...rawRest] = argv;
120
+ const noColor = rawRest.includes('--no-color');
121
+ const rest = rawRest.filter(value => value !== '--no-color');
122
+ const show = (data, plain, protocol = false) => (0, presentation_1.present)(data, plain, { colour: Boolean(process.stdout.isTTY) && !noColor && process.env.NO_COLOR === undefined, protocol });
79
123
  const flag = (name) => {
80
124
  const i = rest.indexOf(name);
81
125
  return i >= 0 ? rest[i + 1] : undefined;
82
126
  };
83
- const has = (name) => rest.includes(name);
127
+ const has = (name) => rawRest.includes(name);
84
128
  switch (command) {
129
+ case 'live': {
130
+ const { runLivePanel } = await Promise.resolve().then(() => __importStar(require('./live-panel')));
131
+ return runLivePanel(HOME, rawRest);
132
+ }
133
+ case 'ps':
134
+ case 'reap':
135
+ return (0, cli_1.runIdleCommand)(command, rawRest, HOME);
136
+ case 'render': {
137
+ if (process.platform === 'win32') {
138
+ process.stdout.write('not supported on this platform yet\n');
139
+ return 0;
140
+ }
141
+ const source = rest[0], mp4 = flag('--mp4'), gif = flag('--gif');
142
+ if (!source || source.startsWith('--') || !mp4 || mp4.startsWith('--') || (has('--gif') && (!gif || gif.startsWith('--'))))
143
+ throw new Error('Usage: agentguard-burn render <run.jsonl> --mp4 <out.mp4> [--gif <out.gif>]');
144
+ const { renderRecording } = await Promise.resolve().then(() => __importStar(require('./render-recording')));
145
+ const result = await renderRecording(source, { mp4, gif });
146
+ show({ kind: 'command', command: 'render', values: { ...result } }, `PNG frames: ${result.framesDir}\n${result.mp4 ? `MP4: ${result.mp4}\n` : `ffmpeg unavailable. Run: ${result.ffmpegCommand}\n`}${result.gif ? `GIF: ${result.gif}\n` : ''}Provenance: ${result.provenancePath}\n`);
147
+ return 0;
148
+ }
85
149
  case 'blocks': {
86
150
  const session = rest.find(value => value !== 'json' && value !== '--json');
87
151
  const report = (0, blocks_1.blocksReport)(HOME, session, { rates: (0, live_1.readPricing)(HOME, (0, pre_tool_use_1.loadPolicy)(HOME)) });
88
- process.stdout.write((rest.includes('json') || has('--json') ? JSON.stringify(report, null, 2) : (0, blocks_1.renderBlocks)(report)) + '\n');
152
+ show({ kind: 'command', command, values: { ...report } }, (rest.includes('json') || has('--json') ? JSON.stringify(report, null, 2) : (0, blocks_1.renderBlocks)(report)) + '\n', rest.includes('json') || has('--json'));
89
153
  return 0;
90
154
  }
91
155
  case 'why': {
92
156
  const location = (0, sessions_1.selectInsightTranscript)(rest.find(value => value !== 'json'));
93
157
  const transcript = (0, transcript_1.readInsightSession)(location.path, { host: location.host, sessionId: location.sessionId });
94
- process.stdout.write(rest.includes('json') ? JSON.stringify(transcript, null, 2) + '\n' : (0, render_2.renderWhy)(transcript, (0, live_1.readPricing)(HOME, (0, pre_tool_use_1.loadPolicy)(HOME))) + '\n');
158
+ show({ kind: 'command', command, values: { session: transcript.sessionId, observations: transcript.observations, diagnostics: transcript.diagnostics, attribution: (0, attribution_1.attributeTurns)((0, transcript_2.deduplicateTurns)(transcript.turns)).buckets, prices: (0, transcript_2.deduplicateTurns)(transcript.turns).map(turn => (0, pricing_1.priceTurn)(turn, (0, live_1.readPricing)(HOME, (0, pre_tool_use_1.loadPolicy)(HOME)))) } }, rest.includes('json') ? JSON.stringify(transcript, null, 2) + '\n' : (0, render_2.renderWhy)(transcript, (0, live_1.readPricing)(HOME, (0, pre_tool_use_1.loadPolicy)(HOME))) + '\n', rest.includes('json'));
95
159
  return 0;
96
160
  }
97
161
  case 'rewrites': {
@@ -99,11 +163,12 @@ async function main(argv) {
99
163
  const locations = session === 'all' ? (0, sessions_1.discoverInsightTranscripts)() : [(0, sessions_1.selectInsightTranscript)(session)];
100
164
  const transcripts = locations.map(location => (0, transcript_1.readInsightTranscript)(location.path, { host: location.host, sessionId: location.sessionId }));
101
165
  const rates = (0, live_1.readPricing)(HOME, (0, pre_tool_use_1.loadPolicy)(HOME));
102
- process.stdout.write(rest.includes('json') ? JSON.stringify((0, render_2.rewriteReport)(transcripts, rates), null, 2) + '\n' : (0, render_2.renderRewrites)(transcripts, rates) + '\n');
166
+ show({ kind: 'command', command, values: (0, render_2.rewriteReport)(transcripts, rates) }, rest.includes('json') ? JSON.stringify((0, render_2.rewriteReport)(transcripts, rates), null, 2) + '\n' : (0, render_2.renderRewrites)(transcripts, rates) + '\n', rest.includes('json'));
103
167
  return 0;
104
168
  }
105
169
  case 'pricing': {
106
- process.stdout.write((0, render_2.renderPricing)((0, live_1.readPricing)(HOME, (0, pre_tool_use_1.loadPolicy)(HOME))) + '\n');
170
+ const rates = (0, live_1.readPricing)(HOME, (0, pre_tool_use_1.loadPolicy)(HOME));
171
+ show({ kind: 'command', command, values: { rates: { ...pricing_1.MODEL_PRICING, ...rates } } }, (0, render_2.renderPricing)(rates) + '\n');
107
172
  return 0;
108
173
  }
109
174
  case 'statusline':
@@ -112,7 +177,7 @@ async function main(argv) {
112
177
  const payload = raw && typeof raw === 'object' ? raw : {};
113
178
  const location = (0, sessions_1.selectInsightTranscript)(payload.transcript_path || rest[0] || payload.session_id);
114
179
  const observation = (0, live_1.observeTool)(HOME, { ...payload, session_id: payload.session_id || location.sessionId, transcript_path: location.path }, location.host, (0, pre_tool_use_1.loadPolicy)(HOME));
115
- process.stdout.write((observation.snapshot ? (0, live_1.renderStatusLine)(observation.snapshot) : 'AgentGuard pace unavailable: local usage metadata is not readable.') + '\n');
180
+ show({ kind: 'command', command, values: { snapshot: observation.snapshot ? { host: observation.snapshot.host, session: observation.snapshot.sessionId, pace: observation.snapshot.pace, forecast: observation.snapshot.forecast, lastTurnTokens: observation.snapshot.lastTurnTokens, windows: observation.snapshot.windows } : null } }, (observation.snapshot ? (0, live_1.renderStatusLine)(observation.snapshot) : 'AgentGuard pace unavailable: local usage metadata is not readable.') + '\n', command === 'statusline');
116
181
  return observation.snapshot ? 0 : 1;
117
182
  }
118
183
  case 'hook': {
@@ -124,6 +189,11 @@ async function main(argv) {
124
189
  catch {
125
190
  // Unparseable payload: allow. We never break the host over our own bug.
126
191
  }
192
+ if (input?.hook_event_name === 'SessionStart') {
193
+ const warning = await (0, cli_1.prepareSessionStart)(input, HOME);
194
+ process.stdout.write(JSON.stringify(warning ? { continue: true, suppressOutput: false, systemMessage: warning } : { continue: true, suppressOutput: true }));
195
+ return 0;
196
+ }
127
197
  process.stdout.write(JSON.stringify((0, pre_tool_use_1.handlePreToolUse)(input, HOME)));
128
198
  return 0;
129
199
  }
@@ -132,12 +202,7 @@ async function main(argv) {
132
202
  const paths = rest.filter((a) => a.endsWith('.jsonl'));
133
203
  const targets = paths.length ? paths : (0, simulate_1.discoverTranscripts)();
134
204
  const summary = (0, simulate_1.replayAll)(targets, policy.thresholds, Number(flag('--min-tokens') ?? 0));
135
- if (has('--json')) {
136
- process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
137
- }
138
- else {
139
- process.stdout.write(`${(0, render_1.renderReplay)(summary, { colour: !has('--no-color'), top: Number(flag('--top') ?? 8) })}\n`);
140
- }
205
+ show({ kind: 'replay', summary }, has('--json') ? `${JSON.stringify(summary, null, 2)}\n` : `${(0, render_1.renderReplay)(summary, { colour: false, top: Number(flag('--top') ?? 8) })}\n`, has('--json'));
141
206
  return 0;
142
207
  }
143
208
  case 'calibrate': {
@@ -145,7 +210,7 @@ async function main(argv) {
145
210
  const result = (0, calibrate_1.calibrate)(summary.sessions);
146
211
  savePolicy(result.policy);
147
212
  const t = result.policy.thresholds;
148
- process.stdout.write([
213
+ show({ kind: 'command', command, values: { ...result } }, [
149
214
  `Calibrated from ${result.sessionsUsed} of ${result.sessionsTotal} sessions (${result.sessionsExcluded} excluded as pathological).`,
150
215
  ` raw p90 ${(result.rawWarnP90 / 1e9).toFixed(2)}B raw p99 ${(result.rawStopP99 / 1e9).toFixed(2)}B`,
151
216
  ` sustained WARN ${(t.sustained.warnTokens / 1e9).toFixed(2)}B STOP ${(t.sustained.stopTokens / 1e9).toFixed(2)}B`,
@@ -163,7 +228,7 @@ async function main(argv) {
163
228
  const gateway = new gateway_1.Gateway(HOME, { sign: false });
164
229
  const lc = policy.thresholds.localCompute;
165
230
  const override = (0, override_1.readOverride)(HOME);
166
- process.stdout.write([
231
+ show({ kind: 'command', command, values: { mode: policy.mode, thresholds: policy.thresholds, eligibility: e, override: override ? { active: true, once: override.once, until: override.until } : null, hosts: ['claude', 'cursor', 'codex'].map(h => { const { command: omitted, ...metadata } = (0, install_1.health)(h); return metadata; }), sessions: [...(0, status_1.readHookSessions)(HOME), ...gateway.sessions()].map(row => ({ sessionId: row.sessionId, hosts: row.hosts, tokens: row.state.totalTokens, spawns: row.state.spawnCount, liveSpawns: row.liveSpawns, capabilities: row.capabilities })) } }, [
167
232
  `mode: ${policy.mode}${policy.mode === 'enforce' ? ' (the next STOP blocks)' : ' (recording only; nothing is blocked)'}`,
168
233
  `thresholds: fan-out lifetime WARN ${policy.thresholds.fanout.warn}; STOP above ${policy.thresholds.fanout.stop} spawns in ${policy.thresholds.fanout.windowActiveMinutes ?? 120} active minutes; depth above ${policy.thresholds.fanout.maxDepth}`,
169
234
  `spawn-rate: WARN ${policy.thresholds.spawnRate?.warn ?? 8}; STOP ${policy.thresholds.spawnRate?.stop ?? 16} in ${policy.thresholds.spawnRate?.windowActiveMinutes ?? 15} active minutes; ${policy.thresholds.spawnRate?.enforce === false ? 'shadow only' : 'enforcing in enforce mode'}`,
@@ -206,7 +271,7 @@ async function main(argv) {
206
271
  process.stderr.write(`init: refusing to touch ${(0, install_1.configPath)(target)}: ${error instanceof Error ? error.message : String(error)}\n`);
207
272
  return 1;
208
273
  }
209
- process.stdout.write((result.changed ? `Wrote ${result.file}${result.backup ? ` (backup: ${result.backup})` : ''}\n` : `${result.file} already has the current hook; nothing changed.\n`) +
274
+ show({ kind: 'command', command, values: { host: target, changed: result.changed, file: result.file, mode: policy.mode } }, (result.changed ? `Wrote ${result.file}${result.backup ? ` (backup: ${result.backup})` : ''}\n` : `${result.file} already has the current hook; nothing changed.\n`) +
210
275
  ` ${result.command}\n` +
211
276
  (label ? `${label}\n` : '') +
212
277
  tail +
@@ -214,7 +279,7 @@ async function main(argv) {
214
279
  return 0;
215
280
  }
216
281
  const snippet = target === 'cursor' ? (0, cursor_1.cursorHooksSnippet)(`node ${cli} cursor-hook`) : target === 'codex' ? (0, codex_1.codexHooksSnippet)(`node ${cli} codex-hook`) : (0, pre_tool_use_1.settingsSnippet)(`node ${cli} hook`);
217
- process.stdout.write(`Add this to ${(0, install_1.configPath)(target).replace(process.env.HOME ?? '', '~')} (merge into existing "hooks"), or run: agentguard-burn init ${target} --write\n\n${JSON.stringify(snippet, null, 2)}\n\n` +
282
+ show({ kind: 'command', command, values: { host: target, config: (0, install_1.configPath)(target), snippet, mode: policy.mode } }, `Add this to ${(0, install_1.configPath)(target).replace(process.env.HOME ?? '', '~')} (merge into existing "hooks"), or run: agentguard-burn init ${target} --write\n\n${JSON.stringify(snippet, null, 2)}\n\n` +
218
283
  (label ? `${label}\n` : '') +
219
284
  tail +
220
285
  (target === 'claude' ? `Other hosts: agentguard-burn init cursor | init codex | proxy --upstream http://127.0.0.1:11434 --host ollama\n` : ''));
@@ -226,8 +291,14 @@ async function main(argv) {
226
291
  return 0;
227
292
  }
228
293
  case 'codex-hook': {
294
+ const input = readStdinJson();
295
+ if (input && typeof input === 'object' && input.hook_event_name === 'SessionStart') {
296
+ const warning = await (0, cli_1.prepareSessionStart)(input, HOME);
297
+ process.stdout.write(JSON.stringify(warning ? { systemMessage: warning } : {}));
298
+ return 0;
299
+ }
229
300
  const gateway = new gateway_1.Gateway(HOME);
230
- process.stdout.write(JSON.stringify((0, codex_1.handleCodexHook)(readStdinJson(), gateway)));
301
+ process.stdout.write(JSON.stringify((0, codex_1.handleCodexHook)(input, gateway)));
231
302
  return 0;
232
303
  }
233
304
  case 'proxy': {
@@ -251,6 +322,7 @@ async function main(argv) {
251
322
  log: (l) => process.stderr.write(`${l}\n`),
252
323
  });
253
324
  const policy = (0, pre_tool_use_1.loadPolicy)(HOME);
325
+ (0, recording_1.recordFrame)({ kind: 'command', command, values: { listening: running.address.origin, host, mode: policy.mode } });
254
326
  process.stderr.write(`agentguard-burn proxy ${running.address.origin} -> ${upstreamRaw} (${host}, ${policy.mode} mode)\n` +
255
327
  `Point your agent at ${running.address.origin}. Send x-agentguard-session: <id> so calls join a session; without it each client port is its own low-confidence session.\n` +
256
328
  `Ctrl-C to stop.\n`);
@@ -263,7 +335,7 @@ async function main(argv) {
263
335
  }
264
336
  case 'conformance': {
265
337
  const result = await (0, conformance_1.runConformance)();
266
- process.stdout.write(result.text + '\n');
338
+ show({ kind: 'command', command, values: { ok: result.ok, checks: result.checks } }, result.text + '\n');
267
339
  return result.ok ? 0 : 1;
268
340
  }
269
341
  case 'enforce': {
@@ -274,18 +346,18 @@ async function main(argv) {
274
346
  return 2;
275
347
  }
276
348
  savePolicy({ ...policy, mode: 'enforce' });
277
- process.stdout.write('Enforcement ON. The next STOP will block the spawn.\n');
349
+ show({ kind: 'command', command, values: { mode: 'enforce' } }, 'Enforcement ON. The next STOP will block the spawn.\n');
278
350
  return 0;
279
351
  }
280
352
  case 'shadow': {
281
353
  savePolicy({ ...(0, pre_tool_use_1.loadPolicy)(HOME), mode: 'shadow' });
282
- process.stdout.write('Back to shadow mode. Decisions are recorded, nothing is blocked.\n');
354
+ show({ kind: 'command', command, values: { mode: 'shadow' } }, 'Back to shadow mode. Decisions are recorded, nothing is blocked.\n');
283
355
  return 0;
284
356
  }
285
357
  case 'resume': {
286
358
  if (has('--clear')) {
287
359
  (0, override_1.clearOverride)(HOME);
288
- process.stdout.write('Override cleared. The next STOP blocks.\n');
360
+ show({ kind: 'command', command, values: { override: false } }, 'Override cleared. The next STOP blocks.\n');
289
361
  return 0;
290
362
  }
291
363
  const reason = flag('--reason');
@@ -296,13 +368,18 @@ async function main(argv) {
296
368
  const once = has('--once');
297
369
  const now = Date.now();
298
370
  (0, override_1.writeOverride)(HOME, once ? { at: now, once: true, reason } : { at: now, once: false, reason, until: now + override_1.OVERRIDE_WINDOW_MS });
299
- process.stdout.write(once
371
+ show({ kind: 'command', command, values: { once, at: now, until: once ? null : now + override_1.OVERRIDE_WINDOW_MS } }, once
300
372
  ? `Override recorded for the next STOP only, on any host: "${reason}"\n`
301
373
  : `Override recorded for the next ${override_1.OVERRIDE_WINDOW_MS / 60000} minutes, on any host: "${reason}"\n (agentguard-burn resume --clear to end it early)\n`);
302
374
  return 0;
303
375
  }
304
376
  default:
305
- process.stdout.write('agentguard-burn <blocks|why|rewrites|pace|statusline|pricing|replay|calibrate|status|init|enforce|shadow|resume|proxy|conformance>\n' +
377
+ show({ kind: 'command', command: 'help', values: { commands: ['ps', 'reap', 'live', 'render', 'blocks', 'why', 'rewrites', 'pace', 'statusline', 'pricing', 'replay', 'calibrate', 'status', 'init', 'enforce', 'shadow', 'resume', 'proxy', 'conformance'] } }, 'agentguard-burn <ps|reap|live|render|blocks|why|rewrites|pace|statusline|pricing|replay|calibrate|status|init|enforce|shadow|resume|proxy|conformance>\n' +
378
+ ' ps [--json] local agent processes, held memory and workspace disk\n' +
379
+ ' render <run.jsonl> --mp4 <out.mp4> [--gif <out.gif>] local recorded-run export\n' +
380
+ ' live [--session ID] [--once] [--replay run.jsonl] local instrument panel\n' +
381
+ ' --record <file.jsonl> append structured frames for any command; --no-color keeps plain output\n' +
382
+ ' reap numbered interactive SIGTERM, never delete or force-kill\n' +
306
383
  ' blocks [session|all] stored WARN/STOP receipts and that session\'s child usage\n' +
307
384
  ' why [session] token attribution and API list-price equivalents\n' +
308
385
  ' rewrites [session|all] full-prefix cache writes by cause\n' +
@@ -322,7 +399,21 @@ async function main(argv) {
322
399
  return command ? 64 : 0;
323
400
  }
324
401
  }
325
- main(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
402
+ async function run() {
403
+ const parsed = (0, recording_1.recordingArguments)(process.argv.slice(2));
404
+ (0, recording_1.configureRecording)(parsed.file);
405
+ try {
406
+ const code = await main(parsed.args);
407
+ if (!(0, recording_1.recordedFrameCount)())
408
+ (0, recording_1.recordFrame)({ kind: 'command', command: parsed.args[0] ?? 'help', values: { exitCode: code } });
409
+ return code;
410
+ }
411
+ catch (error) {
412
+ (0, recording_1.recordFrame)({ kind: 'command', command: parsed.args[0] ?? 'help', values: { exitCode: 4, failed: true } });
413
+ throw error;
414
+ }
415
+ }
416
+ run().then((code) => process.exit(code), (error) => {
326
417
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
327
418
  process.exit(4);
328
419
  });
@@ -11,7 +11,7 @@ import type { Policy, Thresholds } from './types';
11
11
  * Spawn rate 8/16 over 15 active minutes warns/stops by default. Sustained
12
12
  * thresholds remain 3.5B/5B pending review of deduplicated calibration.
13
13
  * This is a dated replay snapshot and an upper bound, not labelled evidence
14
- * of false-positive rates or guaranteed savings.
14
+ * of false-positive rates or guaranteed reductions in cost exposure.
15
15
  *
16
16
  * A previous draft shipped a 50M session budget. Routine sessions on the same
17
17
  * machine run 1.4B to 3.5B, so that default would have fired within minutes on