@agentguard-run/burn 0.2.7 → 0.3.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +69 -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 +111 -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 +38 -0
  11. package/dist/src/frames.js +98 -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/policy.js +47 -1
  35. package/dist/src/presentation.d.ts +5 -0
  36. package/dist/src/presentation.js +23 -0
  37. package/dist/src/recording.d.ts +22 -0
  38. package/dist/src/recording.js +80 -0
  39. package/dist/src/render-recording.d.ts +13 -0
  40. package/dist/src/render-recording.js +193 -0
  41. package/dist/src/replay/render.js +5 -0
  42. package/dist/src/types.d.ts +6 -0
  43. package/docs/burn-idle-audit.md +71 -0
  44. package/docs/burn-render.md +37 -0
  45. package/package.json +4 -2
@@ -0,0 +1,38 @@
1
+ /** Recorded frames carry observations, not ANSI, screen text or process arguments. */
2
+ import { Canvas } from './canvas';
3
+ import type { AuditReport, AuditRow } from './idle/types';
4
+ import type { BurnReport } from './types';
5
+ import type { ReplaySummary } from './replay/simulate';
6
+ export type AuditFrameReport = Omit<AuditReport, 'processes' | 'rows'> & {
7
+ rows: Array<Omit<AuditRow, 'processes'>>;
8
+ };
9
+ export type BurnFrame = {
10
+ kind: 'audit';
11
+ command: 'ps' | 'reap';
12
+ report: AuditFrameReport;
13
+ page?: number;
14
+ } | {
15
+ kind: 'report';
16
+ report: BurnReport;
17
+ subject?: 'spawn' | 'call';
18
+ outcome?: 'blocked' | 'shadow' | 'overridden' | 'allowed';
19
+ } | {
20
+ kind: 'replay';
21
+ summary: ReplaySummary;
22
+ page?: number;
23
+ } | {
24
+ kind: 'command';
25
+ command: string;
26
+ values: Record<string, unknown>;
27
+ page?: number;
28
+ };
29
+ export interface FrameOptions {
30
+ recorded?: boolean;
31
+ at?: string;
32
+ width?: number;
33
+ height?: number;
34
+ colour?: boolean;
35
+ }
36
+ export declare function auditFrame(command: 'ps' | 'reap', report: AuditReport): BurnFrame;
37
+ export declare function framePageCount(data: BurnFrame, options?: FrameOptions): number;
38
+ export declare function renderFrame(data: BurnFrame, options?: FrameOptions): Canvas;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.auditFrame = auditFrame;
4
+ exports.framePageCount = framePageCount;
5
+ exports.renderFrame = renderFrame;
6
+ /** Recorded frames carry observations, not ANSI, screen text or process arguments. */
7
+ const canvas_1 = require("./canvas");
8
+ function auditFrame(command, report) {
9
+ const { processes: ignored, rows, ...metadata } = report;
10
+ return { kind: 'audit', command, report: { ...metadata, rows: rows.map(({ processes: omitted, ...row }) => row) } };
11
+ }
12
+ const memory = (n) => n === null ? '?' : n >= 1e9 ? `${(n / 1e9).toFixed(2)}G` : `${(n / 1e6).toFixed(1)}M`;
13
+ const duration = (n) => n === null ? '?' : n >= 86400 ? `${(n / 86400).toFixed(1)}d` : n >= 3600 ? `${(n / 3600).toFixed(1)}h` : `${Math.floor(n / 60)}m`;
14
+ const flag = (v) => v === null ? '?' : v ? 'yes' : 'no';
15
+ const fit = (v, n) => String(v ?? '?').slice(0, n).padEnd(n);
16
+ function framePageCount(data, options = {}) {
17
+ const c = new canvas_1.Canvas(options);
18
+ if (data.kind === 'audit')
19
+ return Math.max(1, Math.ceil(data.report.rows.length / Math.max(1, Math.floor((c.height - 18) / 2))));
20
+ if (data.kind === 'command')
21
+ return Math.max(1, Math.ceil(valueRows(data.values).length / Math.max(1, c.height - 6)));
22
+ if (data.kind === 'replay')
23
+ return Math.max(1, Math.ceil(data.summary.sessions.length / Math.max(1, c.height - 16)));
24
+ return 1;
25
+ }
26
+ function valueRows(values, prefix = '', depth = 0) {
27
+ return Object.entries(values).flatMap(([key, value]) => {
28
+ const name = prefix ? `${prefix}.${key}` : key;
29
+ if (value && typeof value === 'object' && depth < 3)
30
+ return valueRows(value, name, depth + 1);
31
+ return [`${name}: ${typeof value === 'object' ? JSON.stringify(value) : String(value)}`];
32
+ });
33
+ }
34
+ function renderFrame(data, options = {}) {
35
+ const canvas = new canvas_1.Canvas(options), w = canvas.width, h = canvas.height;
36
+ const replay = options.recorded || data.kind === 'replay';
37
+ const at = options.at ?? new Date().toISOString();
38
+ const pages = framePageCount(data, options), page = 'page' in data ? Math.max(0, Math.min(pages - 1, data.page ?? 0)) : 0;
39
+ canvas.header('AGENTGUARD / BURN', replay ? 'RECORDED RUN · 1x' : 'LIVE');
40
+ canvas.footer(`${replay ? 'recorded run' : 'local only'} · ${page + 1}/${pages}`, at.slice(11, 19) + ' UTC');
41
+ if (data.kind === 'audit') {
42
+ const report = data.report, slots = Math.max(1, Math.floor((h - 18) / 2));
43
+ canvas.put(2, 0, `${data.command.toUpperCase()} · LOCAL COST EXPOSURE · ${report.platform}`, 'mint');
44
+ canvas.put(3, 0, `Observed ${report.generatedAt} · memory first, then workspace disk`, 'slate');
45
+ const wide = w >= 100;
46
+ canvas.put(5, 0, wide ? '# HOST PID MEMORY UPTIME IDLE METHOD WIN WORKING DIRECTORY' : '# HOST PID MEMORY IDLE METHOD WIN', 'slate');
47
+ report.rows.slice(page * slots, (page + 1) * slots).forEach((row, i) => {
48
+ const y = 6 + i * 2, n = page * slots + i + 1;
49
+ const common = `${fit(n, 3)} ${fit(row.host ?? row.kind, 9)} ${fit(row.pid, 7)} ${fit(memory(row.kind === 'workspace' ? row.sizeBytes : row.rssBytes), 7)} `;
50
+ const details = wide ? `${fit(duration(row.uptimeSeconds), 6)} ${fit(duration(row.idleSeconds), 6)} ${fit(row.idleMethod, 10)} ${fit(row.kind === 'browser' ? row.windows : '', 4)} ${fit(row.cwd, Math.max(0, w - 66))}`
51
+ : `${fit(duration(row.idleSeconds), 6)} ${fit(row.idleMethod, 10)} ${fit(row.kind === 'browser' ? row.windows : '', 4)}`;
52
+ canvas.put(y, 0, (common + details).slice(0, w - 6), row.warn ? 'amber' : 'white');
53
+ if (row.warn)
54
+ canvas.put(y, w - 5, 'WARN', 'amber');
55
+ canvas.put(y + 1, 4, row.kind === 'workspace'
56
+ ? `dirty:${flag(row.dirty)} open:${flag(row.openHandles)} ${row.orphan ? 'orphan' : 'registered/in use'} modified:${row.modifiedAt ?? '?'} ${row.cwd ?? row.label}`
57
+ : `${wide ? '' : `up:${duration(row.uptimeSeconds)} · `}${row.cwd ?? '?'} · ${row.pids.length} process(es)${row.reasons.length ? ' · ' + row.reasons.join('; ') : ''}`, 'slate');
58
+ });
59
+ if (!report.rows.length)
60
+ canvas.put(6, 0, 'No agent-owned resources found in the readable scope.', 'slate');
61
+ const y = h - 11, half = Math.floor(w / 2);
62
+ canvas.rule(y, 0, w);
63
+ canvas.put(y + 1, 0, 'IDLE AGENT MEMORY · GB', 'slate');
64
+ canvas.put(y + 1, half, 'ORPHAN DISK · GB', 'slate');
65
+ canvas.digits(y + 2, 0, (report.totals.idleAgentMemoryBytes / 1e9).toFixed(2), report.totals.idleAgentMemoryBytes ? 'amber' : 'mint');
66
+ canvas.digits(y + 2, half, (report.totals.orphanWorkspaceBytes / 1e9).toFixed(2), report.totals.orphanWorkspaceBytes ? 'amber' : 'mint');
67
+ canvas.put(y + 5, 0, `Swap ${memory(report.swapUsedBytes)} · Only the operating system releases swap on reboot.`, 'slate');
68
+ canvas.put(y + 6, 0, 'Burn does not touch swap or delete workspaces. RSS may count shared pages twice.', 'slate');
69
+ canvas.put(y + 7, 0, report.skipped.length ? `Skipped ${report.skipped.length}: ${report.skipped[0]} · full detail: ps --json` : 'Idle method shown per row. Unknown evidence never authorizes closing a session.', report.skipped.length ? 'amber' : 'slate');
70
+ }
71
+ else if (data.kind === 'report') {
72
+ const r = data.report, color = r.verdict === 'STOP' ? 'red' : r.verdict === 'WARN' ? 'amber' : 'mint';
73
+ canvas.put(3, 0, `AGENTGUARD ${r.verdict}`, color);
74
+ canvas.put(4, 0, data.outcome === 'blocked' ? (data.subject === 'call' ? 'model call blocked' : 'agent spawn blocked') : data.outcome === 'shadow' ? 'shadow: would have blocked, call allowed' : data.outcome === 'overridden' ? 'override: call allowed' : r.verdict === 'STOP' ? 'STOP boundary · enforcement outcome not recorded' : 'session advisory', color);
75
+ canvas.digits(6, 0, r.totals.spawns, color);
76
+ canvas.put(10, 0, `spawns · ${r.totals.tokens.toLocaleString('en-US')} tokens · depth ${r.totals.maxDepth}`, 'slate');
77
+ r.findings.slice(0, 5).forEach((f, i) => canvas.put(12 + i, 0, `${f.detector}: ${f.summary}`, color));
78
+ canvas.put(18, 0, 'DO NOW', 'white');
79
+ r.prescriptions.slice(0, Math.max(0, h - 23)).forEach((p, i) => canvas.put(20 + i, 0, `${i + 1}. ${p}`, 'white'));
80
+ canvas.put(h - 4, 0, 'override once: agentguard-burn resume --once --reason "..."', 'slate');
81
+ }
82
+ else if (data.kind === 'replay') {
83
+ const s = data.summary, slots = Math.max(1, h - 16);
84
+ canvas.put(3, 0, 'REPLAY · OBSERVED HISTORY', 'mint');
85
+ canvas.digits(5, 0, Math.round(s.catchableShare * 100), 'amber');
86
+ canvas.put(9, 0, 'percent of recorded tokens after the first STOP boundary', 'slate');
87
+ canvas.put(10, 0, `${s.totalTokens.toLocaleString('en-US')} tokens · ${s.sessions.length} sessions`, 'white');
88
+ canvas.bar(11, 0, Math.min(65, w), s.catchableShare, 'amber');
89
+ s.sessions.slice(page * slots, (page + 1) * slots).forEach((row, i) => canvas.put(13 + i, 0, `${row.sessionId.slice(0, 8)} ${row.totalTokens.toLocaleString('en-US')} tokens ${row.spawns} spawns ${row.finalVerdict}`, row.finalVerdict === 'STOP' ? 'red' : row.finalVerdict === 'WARN' ? 'amber' : 'white'));
90
+ canvas.put(h - 3, 0, 'Upper bound; assumes no override or restart. Nothing left this machine.', 'slate');
91
+ }
92
+ else {
93
+ canvas.put(3, 0, data.command.toUpperCase(), 'mint');
94
+ const slots = Math.max(1, h - 6);
95
+ valueRows(data.values).slice(page * slots, (page + 1) * slots).forEach((line, i) => canvas.put(4 + i, 0, line, 'white'));
96
+ }
97
+ return canvas;
98
+ }
@@ -31,6 +31,7 @@
31
31
  Object.defineProperty(exports, "__esModule", { value: true });
32
32
  exports.Gateway = void 0;
33
33
  exports.mergeCapabilities = mergeCapabilities;
34
+ const recording_1 = require("./recording");
34
35
  const node_fs_1 = require("node:fs");
35
36
  const node_path_1 = require("node:path");
36
37
  const defaults_1 = require("./defaults");
@@ -381,6 +382,8 @@ class Gateway {
381
382
  compute: d.compute,
382
383
  coverage: meta.capabilities,
383
384
  })}\n`, { mode: 0o600 });
385
+ if (notify || d.blocked)
386
+ (0, recording_1.recordFrame)({ kind: 'report', report: d.report, subject: d.action === 'spawn' ? 'spawn' : 'call', outcome: d.blocked ? 'blocked' : d.override ? 'overridden' : d.wouldBlock ? 'shadow' : 'allowed' });
384
387
  return {
385
388
  decisionId,
386
389
  action: d.action,
@@ -17,6 +17,7 @@
17
17
  import { type ReaderCursor, type UsageSnapshot } from '../history/claude-transcript';
18
18
  import type { BurnReport, SessionState } from '../types';
19
19
  export interface HookInput {
20
+ hook_event_name?: string;
20
21
  session_id?: string;
21
22
  transcript_path?: string;
22
23
  tool_name?: string;
@@ -34,8 +34,10 @@ const reservations_1 = require("../state/reservations");
34
34
  const session_1 = require("../state/session");
35
35
  const policy_1 = require("../policy");
36
36
  const override_1 = require("../override");
37
+ const recording_1 = require("../recording");
37
38
  const render_1 = require("../replay/render");
38
39
  const live_1 = require("../insights/live");
40
+ const hook_1 = require("../idle/hook");
39
41
  const SPAWN_TOOLS = new Set(['Agent', 'Task']);
40
42
  function sessionFile(home, sessionId) {
41
43
  return (0, node_path_1.join)(home, 'sessions', `${sessionId.replace(/[^a-zA-Z0-9_-]/g, '_')}.json`);
@@ -159,6 +161,10 @@ function rememberNotified(home, sessionId, signature) {
159
161
  }
160
162
  }
161
163
  function handlePreToolUse(input, home, now = Date.now()) {
164
+ if (input.hook_event_name === 'SessionStart') {
165
+ const warning = (0, hook_1.sessionStartWarning)(home, (0, policy_1.loadPolicy)(home), now);
166
+ return warning ? { continue: true, suppressOutput: false, systemMessage: warning } : { continue: true, suppressOutput: true };
167
+ }
162
168
  const observation = (0, live_1.observeTool)(home, input, 'claude', (0, policy_1.loadPolicy)(home), now);
163
169
  const output = handleSpawnPreToolUse(input, home, now);
164
170
  if (!observation.messages.length)
@@ -219,8 +225,11 @@ function handleSpawnPreToolUse(input, home, now) {
219
225
  totals: report.totals,
220
226
  });
221
227
  if (shouldDeny && policy.mode === 'enforce') {
222
- if (!override)
228
+ if (!override) {
229
+ (0, recording_1.recordFrame)({ kind: 'report', report, outcome: 'blocked' });
223
230
  return deny(reason);
231
+ }
232
+ (0, recording_1.recordFrame)({ kind: 'report', report, outcome: 'overridden' });
224
233
  return {
225
234
  continue: true,
226
235
  systemMessage: `AgentGuard STOP overridden${override.once ? ' once' : ''} ("${override.reason}"): ${report.findings[0]?.summary ?? ''}`,
@@ -231,6 +240,7 @@ function handleSpawnPreToolUse(input, home, now) {
231
240
  if (signature === notified)
232
241
  return { continue: true, suppressOutput: true };
233
242
  rememberNotified(home, input.session_id, signature);
243
+ (0, recording_1.recordFrame)({ kind: 'report', report, outcome: policy.mode === 'shadow' && shouldDeny ? 'shadow' : 'allowed' });
234
244
  return {
235
245
  continue: true,
236
246
  systemMessage: `AgentGuard ${report.verdict}${policy.mode === 'shadow' && shouldDeny ? ' (shadow: would have blocked)' : ''}: ${report.findings[0]?.summary ?? ''}`,
@@ -265,6 +275,7 @@ function deny(reason) {
265
275
  function settingsSnippet(command) {
266
276
  return {
267
277
  hooks: {
278
+ SessionStart: [{ matcher: '.*', hooks: [{ type: 'command', command, timeout: 1 }] }],
268
279
  PreToolUse: [{ matcher: '.*', hooks: [{ type: 'command', command, timeout: 15 }] }],
269
280
  },
270
281
  };
@@ -0,0 +1,8 @@
1
+ import type { Policy } from '../types';
2
+ import { type AuditReport, type AuditThresholds } from './types';
3
+ export declare const AUDIT_CACHE_TTL_MS: number;
4
+ export declare const AUDIT_CACHE_MAX_BYTES: number;
5
+ export declare const auditCachePath: (home: string) => string;
6
+ export declare function auditThresholds(policy: Policy): AuditThresholds;
7
+ export declare function readAuditCache(home: string, now?: number): AuditReport | null;
8
+ export declare function writeAuditCache(home: string, report: AuditReport): void;
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.auditCachePath = exports.AUDIT_CACHE_MAX_BYTES = exports.AUDIT_CACHE_TTL_MS = void 0;
4
+ exports.auditThresholds = auditThresholds;
5
+ exports.readAuditCache = readAuditCache;
6
+ exports.writeAuditCache = writeAuditCache;
7
+ /** Five-minute local metadata cache. Hooks never perform a synchronous scan. */
8
+ const node_fs_1 = require("node:fs");
9
+ const node_crypto_1 = require("node:crypto");
10
+ const node_path_1 = require("node:path");
11
+ const types_1 = require("./types");
12
+ exports.AUDIT_CACHE_TTL_MS = 5 * 60_000;
13
+ exports.AUDIT_CACHE_MAX_BYTES = 4 * 1024 * 1024;
14
+ const auditCachePath = (home) => (0, node_path_1.join)(home, 'idle-audit-cache.json');
15
+ exports.auditCachePath = auditCachePath;
16
+ function auditThresholds(policy) {
17
+ const result = { ...types_1.DEFAULT_AUDIT_THRESHOLDS };
18
+ for (const field of Object.keys(result)) {
19
+ const value = policy.thresholds[field];
20
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0)
21
+ result[field] = value;
22
+ }
23
+ return result;
24
+ }
25
+ function usable(value) {
26
+ if (!value || typeof value !== 'object')
27
+ return false;
28
+ const report = value;
29
+ return report.version === 1 && typeof report.generatedAt === 'string' && Number.isFinite(Date.parse(report.generatedAt))
30
+ && Array.isArray(report.rows) && report.rows.length <= 10_000 && report.rows.every(row => row && typeof row === 'object'
31
+ && ['session', 'daemon', 'browser', 'workspace'].includes(row.kind)
32
+ && Number.isFinite(row.rssBytes) && row.rssBytes >= 0
33
+ && (row.idleSeconds === null || (Number.isFinite(row.idleSeconds) && row.idleSeconds >= 0)));
34
+ }
35
+ function readAuditCache(home, now = Date.now()) {
36
+ try {
37
+ const file = (0, exports.auditCachePath)(home), stat = (0, node_fs_1.lstatSync)(file);
38
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > exports.AUDIT_CACHE_MAX_BYTES)
39
+ return null;
40
+ const value = JSON.parse((0, node_fs_1.readFileSync)(file, 'utf8'));
41
+ if (!usable(value))
42
+ return null;
43
+ const age = now - Date.parse(value.generatedAt);
44
+ return age >= 0 && age < exports.AUDIT_CACHE_TTL_MS ? value : null;
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }
50
+ function writeAuditCache(home, report) {
51
+ if (!usable(report))
52
+ throw new Error('invalid_idle_audit');
53
+ // Hook summaries do not need command lines; never cache process arguments.
54
+ const summary = { ...report, processes: [], rows: report.rows.map(row => ({ ...row, processes: [] })) };
55
+ const text = JSON.stringify(summary) + '\n';
56
+ if (Buffer.byteLength(text) > exports.AUDIT_CACHE_MAX_BYTES)
57
+ throw new Error('idle_audit_too_large');
58
+ (0, node_fs_1.mkdirSync)(home, { recursive: true, mode: 0o700 });
59
+ const file = (0, exports.auditCachePath)(home), temporary = `${file}.${process.pid}.${(0, node_crypto_1.randomUUID)()}`;
60
+ try {
61
+ (0, node_fs_1.writeFileSync)(temporary, text, { mode: 0o600, flag: 'wx' });
62
+ (0, node_fs_1.renameSync)(temporary, file);
63
+ }
64
+ finally {
65
+ try {
66
+ (0, node_fs_1.unlinkSync)(temporary);
67
+ }
68
+ catch { }
69
+ }
70
+ }
@@ -0,0 +1,13 @@
1
+ import type { ProcessSnapshot } from './types';
2
+ /** Command parsing is identity-only; these values stay on the local machine. */
3
+ export declare function commandWords(command: string): string[];
4
+ export declare function agentHost(command: string): 'claude' | 'codex' | null;
5
+ export declare function isTemporary(path: string | null): boolean;
6
+ export declare function browserProfile(command: string): string | null;
7
+ export declare function isChromeProcess(command: string): boolean;
8
+ export declare function isAutomationBrowser(command: string): boolean;
9
+ export declare function daemonPath(command: string): string | null;
10
+ export declare function isPluginDaemon(command: string): boolean;
11
+ export declare function descendants(root: number, processes: ProcessSnapshot[]): ProcessSnapshot[];
12
+ export declare function hasAncestor(pid: number, candidates: Set<number>, processes: Map<number, ProcessSnapshot>): boolean;
13
+ export declare function inside(path: string, directory: string): boolean;
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.commandWords = commandWords;
4
+ exports.agentHost = agentHost;
5
+ exports.isTemporary = isTemporary;
6
+ exports.browserProfile = browserProfile;
7
+ exports.isChromeProcess = isChromeProcess;
8
+ exports.isAutomationBrowser = isAutomationBrowser;
9
+ exports.daemonPath = daemonPath;
10
+ exports.isPluginDaemon = isPluginDaemon;
11
+ exports.descendants = descendants;
12
+ exports.hasAncestor = hasAncestor;
13
+ exports.inside = inside;
14
+ const node_path_1 = require("node:path");
15
+ /** Command parsing is identity-only; these values stay on the local machine. */
16
+ function commandWords(command) {
17
+ const words = [];
18
+ let word = '', quote = '', escaped = false;
19
+ for (const character of command) {
20
+ if (escaped) {
21
+ word += character;
22
+ escaped = false;
23
+ continue;
24
+ }
25
+ if (character === '\\' && quote !== "'") {
26
+ escaped = true;
27
+ continue;
28
+ }
29
+ if (quote) {
30
+ if (character === quote)
31
+ quote = '';
32
+ else
33
+ word += character;
34
+ continue;
35
+ }
36
+ if (character === '"' || character === "'") {
37
+ quote = character;
38
+ continue;
39
+ }
40
+ if (/\s/.test(character)) {
41
+ if (word) {
42
+ words.push(word);
43
+ word = '';
44
+ }
45
+ }
46
+ else
47
+ word += character;
48
+ }
49
+ if (escaped)
50
+ word += '\\';
51
+ if (word)
52
+ words.push(word);
53
+ return words;
54
+ }
55
+ function agentHost(command) {
56
+ const words = commandWords(command), executable = (0, node_path_1.basename)(words[0] || '');
57
+ if (['claude', 'codex'].includes(executable))
58
+ return executable;
59
+ if (/^node(?:js)?$/.test(executable)) {
60
+ const script = words.slice(1).find(word => !word.startsWith('-')) || '';
61
+ if (/(?:^|\/)codex(?:\.js)?$/.test(script) || /\/@openai\/codex\/bin\/codex\.js$/.test(script))
62
+ return 'codex';
63
+ if (/(?:^|\/)claude(?:\.js)?$/.test(script) || /\/@anthropic-ai\/claude-code\/cli\.js$/.test(script))
64
+ return 'claude';
65
+ }
66
+ return null;
67
+ }
68
+ function isTemporary(path) {
69
+ return Boolean(path && /^(?:\/private)?\/tmp(?:\/|$)|^\/private\/var\/folders\/[^/]+\/[^/]+\/T(?:\/|$)|^\/var\/tmp(?:\/|$)/.test(path));
70
+ }
71
+ function browserProfile(command) {
72
+ const words = commandWords(command);
73
+ const inline = words.find(word => word.startsWith('--user-data-dir='));
74
+ const index = words.indexOf('--user-data-dir');
75
+ const value = inline?.slice('--user-data-dir='.length) || (index >= 0 ? words[index + 1] : null);
76
+ return value && (0, node_path_1.isAbsolute)(value) ? value : null;
77
+ }
78
+ function isChromeProcess(command) {
79
+ // The browser executable must be the actual process, not a shell/node command
80
+ // that merely mentions Chrome or an ordinary helper's ancestor.
81
+ const words = commandWords(command), first = words[0] || '', executable = (0, node_path_1.basename)(first);
82
+ const browser = /^(?:Google Chrome(?: for Testing)?|Chromium)(?: Helper(?: \([^)]*\))?)?$/.test(executable) || /(?:^|\/)(?:chrome|chromium|chromium-browser|headless_shell)$/.test(first)
83
+ || (!/^(?:node(?:js)?|sh|bash|zsh|python3?)$/.test(executable) && /^\/[^\n]*\/(?:Google Chrome(?: for Testing)?|Chromium)(?: Helper(?: \([^)]*\))?)?(?:\s|$)/.test(command))
84
+ || /^(?:Google Chrome(?: for Testing)?|Chromium)(?:\s|$)/.test(command);
85
+ return browser;
86
+ }
87
+ function isAutomationBrowser(command) {
88
+ if (!isChromeProcess(command))
89
+ return false;
90
+ const profile = browserProfile(command);
91
+ return /(?:^|\s)--(?:remote-debugging-(?:port(?:=\d+)?|pipe)|enable-automation)(?:\s|$)/.test(command)
92
+ || Boolean(profile && (isTemporary(profile) || /(?:playwright|puppeteer)[-_]/i.test(profile)));
93
+ }
94
+ function daemonPath(command) {
95
+ const words = commandWords(command);
96
+ return words.slice(1).find(word => (0, node_path_1.isAbsolute)(word) && /(?:\.[cm]?[jt]s|\.py|worker|daemon|server|hook)(?:$|\/)/i.test(word)) || null;
97
+ }
98
+ function isPluginDaemon(command) {
99
+ const words = commandWords(command), executable = (0, node_path_1.basename)(words[0] || '');
100
+ if (!/^(?:node(?:js)?|bun|python(?:3(?:\.\d+)?)?)$/.test(executable))
101
+ return false;
102
+ const source = daemonPath(command);
103
+ return Boolean(source && /(?:agentguard|codex-plugin|claude-code|(?:^|\/)plugins?(?:\/|[-.])|(?:^|\/)hooks?(?:\/|[-.]))/i.test(source)
104
+ && /(?:worker|daemon|hook|mcp(?:[-/]server)?|plugin[-/]server|server\.[cm]?js)/i.test(source));
105
+ }
106
+ function descendants(root, processes) {
107
+ const found = new Set([root]);
108
+ let changed = true;
109
+ while (changed) {
110
+ changed = false;
111
+ for (const process of processes)
112
+ if (found.has(process.ppid) && !found.has(process.pid)) {
113
+ found.add(process.pid);
114
+ changed = true;
115
+ }
116
+ }
117
+ return processes.filter(process => found.has(process.pid));
118
+ }
119
+ function hasAncestor(pid, candidates, processes) {
120
+ const seen = new Set();
121
+ let process = processes.get(pid);
122
+ while (process && process.ppid > 0 && !seen.has(process.ppid)) {
123
+ if (candidates.has(process.ppid))
124
+ return true;
125
+ seen.add(process.ppid);
126
+ process = processes.get(process.ppid);
127
+ }
128
+ return false;
129
+ }
130
+ function inside(path, directory) {
131
+ const suffix = (0, node_path_1.relative)(directory, path);
132
+ return suffix === '' || (!suffix.startsWith('..') && !(0, node_path_1.isAbsolute)(suffix));
133
+ }
@@ -0,0 +1,2 @@
1
+ export declare function prepareSessionStart(input: unknown, home: string): Promise<string | null>;
2
+ export declare function runIdleCommand(command: 'ps' | 'reap', args: string[], home: string): Promise<number>;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prepareSessionStart = prepareSessionStart;
4
+ exports.runIdleCommand = runIdleCommand;
5
+ /** Interactive cleanup is separate from the read-only audit and hook advisory. */
6
+ const presentation_1 = require("../presentation");
7
+ const frames_1 = require("../frames");
8
+ const promises_1 = require("node:readline/promises");
9
+ const policy_1 = require("../policy");
10
+ const collect_1 = require("./collect");
11
+ const cache_1 = require("./cache");
12
+ const render_1 = require("./render");
13
+ const reap_1 = require("./reap");
14
+ const hook_1 = require("./hook");
15
+ async function prepareSessionStart(input, home) {
16
+ if (!input || typeof input !== 'object' || input.hook_event_name !== 'SessionStart')
17
+ return null;
18
+ // A cache miss gets at most 150 ms of audit work. It is advisory in both modes.
19
+ return (0, hook_1.sessionStartCheck)(home, { collect: options => (0, collect_1.collectAudit)(options) });
20
+ }
21
+ async function runIdleCommand(command, args, home) {
22
+ if (process.platform === 'win32') {
23
+ process.stdout.write('not supported on this platform yet\n');
24
+ return 0;
25
+ }
26
+ const allowed = command === 'ps' ? ['--json', '--no-color'] : ['--no-color'];
27
+ if (args.some(arg => !allowed.includes(arg))) {
28
+ process.stderr.write(command === 'reap'
29
+ ? 'reap requires numbers typed at its interactive prompt. There is no --yes flag.\n'
30
+ : 'Usage: agentguard-burn ps [--json] [--no-color]\n');
31
+ return 64;
32
+ }
33
+ if (command === 'reap' && (!process.stdin.isTTY || !process.stdout.isTTY)) {
34
+ process.stderr.write('reap requires an interactive terminal and confirmation. Nothing was closed.\n');
35
+ return 1;
36
+ }
37
+ const thresholds = (0, cache_1.auditThresholds)((0, policy_1.loadPolicy)(home));
38
+ const readAudit = async () => (0, render_1.rankAudit)(await (0, collect_1.collectAudit)({ thresholds }));
39
+ const audit = await readAudit();
40
+ try {
41
+ (0, cache_1.writeAuditCache)(home, audit);
42
+ }
43
+ catch {
44
+ audit.skipped.push('The local five-minute audit cache could not be written.');
45
+ }
46
+ (0, presentation_1.present)((0, frames_1.auditFrame)(command, audit), (args.includes('--json') ? JSON.stringify((0, render_1.auditForJson)(audit), null, 2)
47
+ : (0, render_1.renderAudit)(audit, { colour: false, width: process.stdout.columns })) + '\n', { colour: Boolean(process.stdout.isTTY) && !args.includes('--no-color') && process.env.NO_COLOR === undefined, protocol: args.includes('--json') });
48
+ if (command === 'ps')
49
+ return 0;
50
+ process.stdout.write('Only selected process rows can receive SIGTERM. Workspaces are information only.\n');
51
+ const terminal = (0, promises_1.createInterface)({ input: process.stdin, output: process.stdout });
52
+ try {
53
+ const result = await (0, reap_1.interactiveReap)(audit, {
54
+ inputIsTTY: Boolean(process.stdin.isTTY), outputIsTTY: Boolean(process.stdout.isTTY), currentPid: process.pid,
55
+ readAudit: async () => (0, render_1.rankAudit)(await (0, collect_1.collectAudit)({ thresholds, workspaceRoots: [] })), gitDirty: cwd => (0, collect_1.checkWorkingDirectory)(cwd),
56
+ question: prompt => terminal.question(prompt), signal: (pid, signal) => { process.kill(pid, signal); },
57
+ alive: pid => { try {
58
+ process.kill(pid, 0);
59
+ return true;
60
+ }
61
+ catch (error) {
62
+ return error.code !== 'ESRCH';
63
+ } },
64
+ });
65
+ const plain = (result.cancelled ? 'Cancelled. Nothing was closed.\n' : '') + result.results.map(row => `${row.selection}. ${row.status}: ${row.reason}\n` + row.pids.map(pid => ` ${pid.reason}\n`).join('')).join('');
66
+ (0, presentation_1.present)({ kind: 'command', command: 'reap', values: { cancelled: result.cancelled, results: result.results } }, plain, { colour: Boolean(process.stdout.isTTY) && !args.includes('--no-color') && process.env.NO_COLOR === undefined });
67
+ return 0;
68
+ }
69
+ finally {
70
+ terminal.close();
71
+ }
72
+ }
@@ -0,0 +1,38 @@
1
+ import { type Stats } from 'node:fs';
2
+ import { type CommandRunner } from './platform';
3
+ import { type AuditReport, type AuditThresholds } from './types';
4
+ export type { CommandRunner } from './platform';
5
+ export interface MetadataAccess {
6
+ lstat(path: string): Promise<Stats>;
7
+ readdir(path: string): Promise<string[]>;
8
+ realpath(path: string): Promise<string>;
9
+ readPrefix(path: string, bytes: number): Promise<string>;
10
+ readFirstLine?(path: string, bytes: number, signal?: AbortSignal): Promise<string>;
11
+ }
12
+ export interface CollectAuditOptions {
13
+ thresholds?: Partial<AuditThresholds>;
14
+ now?: number;
15
+ signal?: AbortSignal;
16
+ platform?: string;
17
+ homeDir?: string;
18
+ workspaceRoots?: string[];
19
+ claudeProjectsRoot?: string;
20
+ codexSessionsRoot?: string;
21
+ procRoot?: string;
22
+ runner?: CommandRunner;
23
+ metadata?: MetadataAccess;
24
+ maxProcesses?: number;
25
+ maxScanEntries?: number;
26
+ maxDepth?: number;
27
+ maxWorkspaces?: number;
28
+ maxDurationMs?: number;
29
+ commandTimeoutMs?: number;
30
+ }
31
+ export declare function checkWorkingDirectory(cwd: string, options?: {
32
+ runner?: CommandRunner;
33
+ signal?: AbortSignal;
34
+ commandTimeoutMs?: number;
35
+ }): Promise<boolean | null>;
36
+ export declare const gitDirty: typeof checkWorkingDirectory;
37
+ /** Bounded local OS and filesystem metadata; no network client or transcript parser is imported. */
38
+ export declare function collectAudit(options?: CollectAuditOptions): Promise<AuditReport>;