@agentguard-run/burn 0.1.1 → 0.2.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 (38) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +110 -1
  3. package/dist/src/adapters/codex.d.ts +48 -0
  4. package/dist/src/adapters/codex.js +194 -0
  5. package/dist/src/adapters/cursor.d.ts +35 -0
  6. package/dist/src/adapters/cursor.js +132 -0
  7. package/dist/src/adapters/raw-api.d.ts +76 -0
  8. package/dist/src/adapters/raw-api.js +130 -0
  9. package/dist/src/cli.d.ts +7 -3
  10. package/dist/src/cli.js +99 -10
  11. package/dist/src/conformance.d.ts +26 -0
  12. package/dist/src/conformance.js +261 -0
  13. package/dist/src/defaults.d.ts +11 -0
  14. package/dist/src/defaults.js +16 -1
  15. package/dist/src/detectors/local-compute.d.ts +19 -0
  16. package/dist/src/detectors/local-compute.js +66 -0
  17. package/dist/src/events.d.ts +94 -0
  18. package/dist/src/events.js +47 -0
  19. package/dist/src/gateway.d.ts +134 -0
  20. package/dist/src/gateway.js +522 -0
  21. package/dist/src/index.d.ts +15 -4
  22. package/dist/src/index.js +41 -1
  23. package/dist/src/proxy/server.d.ts +45 -0
  24. package/dist/src/proxy/server.js +169 -0
  25. package/dist/src/proxy/usage-observer.d.ts +40 -0
  26. package/dist/src/proxy/usage-observer.js +128 -0
  27. package/dist/src/receipt.d.ts +61 -0
  28. package/dist/src/receipt.js +98 -0
  29. package/dist/src/replay/render.d.ts +1 -0
  30. package/dist/src/replay/render.js +2 -1
  31. package/dist/src/state/reservations.d.ts +115 -11
  32. package/dist/src/state/reservations.js +293 -59
  33. package/dist/src/state/session.d.ts +6 -0
  34. package/dist/src/state/session.js +17 -0
  35. package/dist/src/status.d.ts +11 -0
  36. package/dist/src/status.js +48 -0
  37. package/dist/src/types.d.ts +14 -1
  38. package/package.json +33 -6
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The local-compute plane.
3
+ *
4
+ * On a hosted API the loss is dollars, and the two session detectors in
5
+ * evaluate.ts cover it. On a local runtime the loss is different: the GPU is
6
+ * already paid for, so what a runaway costs you is the machine itself, wedged
7
+ * behind eight concurrent requests for an hour. Concurrency and occupied
8
+ * request time are the honest signals we can see from a proxy.
9
+ *
10
+ * This ships WARN-only. We cannot see the hardware, so a universal STOP would
11
+ * be a guess, and a guessed STOP is what gets a safety tool uninstalled.
12
+ * Operators who know their server set the ceilings in burn-policy.json.
13
+ */
14
+ import type { ComputeSnapshot } from '../state/reservations';
15
+ import type { Finding, Thresholds, Verdict } from '../types';
16
+ export declare function evaluateLocalCompute(snapshot: ComputeSnapshot, thresholds: Thresholds): {
17
+ verdict: Verdict;
18
+ findings: Finding[];
19
+ };
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ /**
3
+ * The local-compute plane.
4
+ *
5
+ * On a hosted API the loss is dollars, and the two session detectors in
6
+ * evaluate.ts cover it. On a local runtime the loss is different: the GPU is
7
+ * already paid for, so what a runaway costs you is the machine itself, wedged
8
+ * behind eight concurrent requests for an hour. Concurrency and occupied
9
+ * request time are the honest signals we can see from a proxy.
10
+ *
11
+ * This ships WARN-only. We cannot see the hardware, so a universal STOP would
12
+ * be a guess, and a guessed STOP is what gets a safety tool uninstalled.
13
+ * Operators who know their server set the ceilings in burn-policy.json.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.evaluateLocalCompute = evaluateLocalCompute;
17
+ const defaults_1 = require("../defaults");
18
+ function evaluateLocalCompute(snapshot, thresholds) {
19
+ const t = thresholds.localCompute ?? defaults_1.DEFAULT_LOCAL_COMPUTE;
20
+ const findings = [];
21
+ let verdict = 'OK';
22
+ const minutes = Math.round(snapshot.windowMs / 60_000);
23
+ // The candidate call is already reserved, so inFlight includes it.
24
+ if (t.stopConcurrent !== null && snapshot.inFlight > t.stopConcurrent) {
25
+ findings.push({
26
+ detector: 'local_compute',
27
+ verdict: 'STOP',
28
+ summary: `${snapshot.inFlight} model calls in flight on this machine; ceiling is ${t.stopConcurrent}.`,
29
+ observed: snapshot.inFlight,
30
+ threshold: t.stopConcurrent,
31
+ });
32
+ verdict = 'STOP';
33
+ }
34
+ else if (snapshot.inFlight >= t.warnConcurrent) {
35
+ findings.push({
36
+ detector: 'local_compute',
37
+ verdict: 'WARN',
38
+ summary: `${snapshot.inFlight} model calls in flight on this machine.`,
39
+ observed: snapshot.inFlight,
40
+ threshold: t.warnConcurrent,
41
+ });
42
+ verdict = 'WARN';
43
+ }
44
+ const occupiedSec = Math.round(snapshot.occupiedMs / 1000);
45
+ if (t.stopOccupiedMs !== null && snapshot.occupiedMs >= t.stopOccupiedMs) {
46
+ findings.push({
47
+ detector: 'local_compute',
48
+ verdict: 'STOP',
49
+ summary: `${occupiedSec}s of occupied request time in the last ${minutes} min; ceiling is ${Math.round(t.stopOccupiedMs / 1000)}s.`,
50
+ observed: snapshot.occupiedMs,
51
+ threshold: t.stopOccupiedMs,
52
+ });
53
+ verdict = 'STOP';
54
+ }
55
+ else if (t.warnOccupiedMs !== null && snapshot.occupiedMs >= t.warnOccupiedMs && verdict === 'OK') {
56
+ findings.push({
57
+ detector: 'local_compute',
58
+ verdict: 'WARN',
59
+ summary: `${occupiedSec}s of occupied request time in the last ${minutes} min.`,
60
+ observed: snapshot.occupiedMs,
61
+ threshold: t.warnOccupiedMs,
62
+ });
63
+ verdict = 'WARN';
64
+ }
65
+ return { verdict, findings };
66
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The host-neutral event vocabulary.
3
+ *
4
+ * Every adapter, whatever it can see, speaks this. A Cursor subagentStart
5
+ * hook, a Codex PreToolUse hook, an Ollama response through the proxy, and an
6
+ * orchestrator's beforeSpawn call all normalise into these shapes, and the
7
+ * detectors never learn which host produced them. That is the whole
8
+ * cross-tool claim in one file: one policy can only be enforced identically
9
+ * if the inputs are identical in kind.
10
+ *
11
+ * What is deliberately absent: prompts, completions, source code, transcript
12
+ * paths, raw tool arguments. Surfaces are digests. Session IDs are opaque.
13
+ *
14
+ * Coverage is part of correctness, not marketing. An adapter that cannot see
15
+ * a signal says so, so an OK verdict never masquerades as full visibility.
16
+ */
17
+ export type HostId = 'claude-code' | 'cursor' | 'codex' | 'ollama' | 'vllm' | 'lm-studio' | 'openai-compatible' | 'raw-api';
18
+ /** How much an adapter can actually see of each policy plane. */
19
+ export type Coverage = 'authoritative' | 'estimated' | 'missing';
20
+ export interface HostCapabilities {
21
+ spawns: Coverage;
22
+ depth: Coverage;
23
+ usage: Coverage;
24
+ }
25
+ /**
26
+ * How sure the adapter is that this event belongs to the session it names.
27
+ * A proxy request carrying x-agentguard-session is high. A proxy request
28
+ * identified only by client port is low, and a low-confidence session can
29
+ * never be hard-STOPped on session-scope grounds: reconnects fragment it.
30
+ */
31
+ export type Attribution = 'high' | 'low';
32
+ interface Base {
33
+ schemaVersion: 1;
34
+ eventId: string;
35
+ host: HostId;
36
+ /** Opaque logical session. Shared across hosts when the integration propagates it. */
37
+ sessionId: string;
38
+ at: number;
39
+ }
40
+ export interface SessionOpened extends Base {
41
+ kind: 'session_opened';
42
+ capabilities: HostCapabilities;
43
+ }
44
+ /** A model call is about to be made. Reserves the estimate under callId. */
45
+ export interface CallRequested extends Base {
46
+ kind: 'call_requested';
47
+ callId: string;
48
+ estimatedTokens: number;
49
+ attribution: Attribution;
50
+ }
51
+ /** Usage was observed. With a callId it *replaces* that call's reservation. */
52
+ export interface ModelUsageObserved extends Base {
53
+ kind: 'model_usage';
54
+ /** input + output. Cache-read is carried for explanation only. */
55
+ tokens: number;
56
+ cacheRead: number;
57
+ usageCoverage: Coverage;
58
+ callId?: string;
59
+ }
60
+ export interface ToolSurfaceRead extends Base {
61
+ kind: 'surface_read';
62
+ /** sha256 of the normalised path or pattern, never the path itself. */
63
+ surfaceDigest: string;
64
+ }
65
+ export interface SpawnRequested extends Base {
66
+ kind: 'spawn_requested';
67
+ spawnId: string;
68
+ /** Depth the child would have, when the host knows it. Root issuer proposes 1. */
69
+ proposedDepth?: number;
70
+ /** The agent issuing the spawn, when the host names it. Used to derive depth. */
71
+ issuerId?: string;
72
+ attribution: Attribution;
73
+ }
74
+ export interface SpawnStarted extends Base {
75
+ kind: 'spawn_started';
76
+ spawnId: string;
77
+ depth: number;
78
+ }
79
+ export interface SpawnFinished extends Base {
80
+ kind: 'spawn_finished';
81
+ spawnId: string;
82
+ }
83
+ export interface SessionClosed extends Base {
84
+ kind: 'session_closed';
85
+ }
86
+ export type AgentEvent = SessionOpened | CallRequested | ModelUsageObserved | ToolSurfaceRead | SpawnRequested | SpawnStarted | SpawnFinished | SessionClosed;
87
+ /** Session IDs are propagated between hosts through these, and only these. */
88
+ export declare const SESSION_HEADER = "x-agentguard-session";
89
+ export declare const CALL_HEADER = "x-agentguard-call";
90
+ export declare const SESSION_ENV = "AGENTGUARD_SESSION_ID";
91
+ export declare function isValidSessionId(value: string): boolean;
92
+ export declare function eventId(): string;
93
+ export declare const CAPABILITIES: Record<HostId, HostCapabilities>;
94
+ export {};
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ /**
3
+ * The host-neutral event vocabulary.
4
+ *
5
+ * Every adapter, whatever it can see, speaks this. A Cursor subagentStart
6
+ * hook, a Codex PreToolUse hook, an Ollama response through the proxy, and an
7
+ * orchestrator's beforeSpawn call all normalise into these shapes, and the
8
+ * detectors never learn which host produced them. That is the whole
9
+ * cross-tool claim in one file: one policy can only be enforced identically
10
+ * if the inputs are identical in kind.
11
+ *
12
+ * What is deliberately absent: prompts, completions, source code, transcript
13
+ * paths, raw tool arguments. Surfaces are digests. Session IDs are opaque.
14
+ *
15
+ * Coverage is part of correctness, not marketing. An adapter that cannot see
16
+ * a signal says so, so an OK verdict never masquerades as full visibility.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.CAPABILITIES = exports.SESSION_ENV = exports.CALL_HEADER = exports.SESSION_HEADER = void 0;
20
+ exports.isValidSessionId = isValidSessionId;
21
+ exports.eventId = eventId;
22
+ /** Session IDs are propagated between hosts through these, and only these. */
23
+ exports.SESSION_HEADER = 'x-agentguard-session';
24
+ exports.CALL_HEADER = 'x-agentguard-call';
25
+ exports.SESSION_ENV = 'AGENTGUARD_SESSION_ID';
26
+ const SESSION_ID_RE = /^[A-Za-z0-9._:@/-]{1,256}$/;
27
+ function isValidSessionId(value) {
28
+ return SESSION_ID_RE.test(value);
29
+ }
30
+ function eventId() {
31
+ // Node built-ins only. Time-ordered enough for dedupe, unique enough for logs.
32
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
33
+ }
34
+ exports.CAPABILITIES = {
35
+ 'claude-code': { spawns: 'authoritative', depth: 'authoritative', usage: 'authoritative' },
36
+ // Cursor names the subagent and its parent; hosted-model usage is never exposed.
37
+ cursor: { spawns: 'authoritative', depth: 'estimated', usage: 'missing' },
38
+ // Codex gates spawn_agent through PreToolUse; the transcript is a locator, not a contract.
39
+ codex: { spawns: 'authoritative', depth: 'estimated', usage: 'estimated' },
40
+ // A proxy sees every token and no tree.
41
+ ollama: { spawns: 'missing', depth: 'missing', usage: 'authoritative' },
42
+ vllm: { spawns: 'missing', depth: 'missing', usage: 'authoritative' },
43
+ 'lm-studio': { spawns: 'missing', depth: 'missing', usage: 'authoritative' },
44
+ 'openai-compatible': { spawns: 'missing', depth: 'missing', usage: 'estimated' },
45
+ // The orchestrator knows everything, because it is the one doing it.
46
+ 'raw-api': { spawns: 'authoritative', depth: 'authoritative', usage: 'authoritative' },
47
+ };
@@ -0,0 +1,134 @@
1
+ /**
2
+ * The gateway: one transaction boundary for every host.
3
+ *
4
+ * Adapters do not evaluate policy. They translate what they can see into
5
+ * AgentEvents and hand them here. The gateway folds events into per-session
6
+ * state, evaluates the same detectors the Claude hook uses, and returns a
7
+ * decision. Cursor, Codex, the Ollama proxy and raw middleware therefore
8
+ * cannot drift from each other, because there is nothing host-specific left
9
+ * to drift.
10
+ *
11
+ * Three properties are load-bearing:
12
+ *
13
+ * 1. A spawn is admitted inside the same machine-wide lock the Claude hook
14
+ * uses, as one transaction: fold, evaluate, reserve, sign. Ten parallel
15
+ * Cursor hooks racing a cap of 40 admit exactly 40, for exactly the
16
+ * reason ten Claude hooks do.
17
+ *
18
+ * 2. Usage is committed by call ID and *replaces* what was reserved under
19
+ * it. When middleware estimated 120K and the proxy later saw 87K for the
20
+ * same call, the session moves by 87K, not 207K. Double counting is the
21
+ * easiest way to make a cross-tool product lie, and it is ruled out here
22
+ * rather than in every adapter.
23
+ *
24
+ * 3. A STOP blocks the next expansion. It never truncates a request that is
25
+ * already streaming and never kills a running agent. Blocking is not
26
+ * killing, and the product does not pretend otherwise.
27
+ *
28
+ * Persisted state is content-free: counts, digests, verdicts.
29
+ */
30
+ import { type AgentEvent, type Attribution, type CallRequested, type Coverage, type HostCapabilities, type HostId, type SpawnRequested } from './events';
31
+ import { type SignedReceipt } from './receipt';
32
+ import { type ComputeSnapshot } from './state/reservations';
33
+ import type { BurnReport, Mode, SessionState, Verdict } from './types';
34
+ export interface Decision {
35
+ decisionId: string;
36
+ action: 'spawn' | 'model_call';
37
+ verdict: Verdict;
38
+ /** The policy would block this if enforcing. */
39
+ wouldBlock: boolean;
40
+ /** Enforcing, and blocking. */
41
+ blocked: boolean;
42
+ mode: Mode;
43
+ report: BurnReport;
44
+ capabilities: HostCapabilities;
45
+ effectiveSpawns: number;
46
+ /** Depth the child would have. Null for model calls. */
47
+ proposedDepth: number | null;
48
+ compute: ComputeSnapshot | null;
49
+ receipt: SignedReceipt | null;
50
+ /** Set when the decision was forced by an infrastructure failure. */
51
+ failedClosed?: string;
52
+ }
53
+ export interface UsageCoverage {
54
+ authoritative: number;
55
+ estimated: number;
56
+ missing: number;
57
+ }
58
+ export interface GatewaySessionView {
59
+ sessionId: string;
60
+ hosts: HostId[];
61
+ capabilities: HostCapabilities;
62
+ state: SessionState;
63
+ liveSpawns: number;
64
+ usage: UsageCoverage;
65
+ decisions: number;
66
+ wouldBlock: number;
67
+ closedAt: number | null;
68
+ }
69
+ export interface GatewayOptions {
70
+ /** Sign receipts. On by default; off keeps tests and hot paths key-free. */
71
+ sign?: boolean;
72
+ now?: () => number;
73
+ }
74
+ export declare class Gateway {
75
+ private readonly home;
76
+ private readonly store;
77
+ private readonly signer;
78
+ private readonly now;
79
+ private readonly sessionsDir;
80
+ constructor(home: string, opts?: GatewayOptions);
81
+ private file;
82
+ private load;
83
+ private save;
84
+ /**
85
+ * Fold observations. Never decides, never blocks. Idempotent per eventId,
86
+ * so a retried hook or a replayed transcript line is a no-op.
87
+ */
88
+ observe(events: AgentEvent[]): void;
89
+ private fold;
90
+ /**
91
+ * Replace-not-add by call ID. An estimate reserved by middleware is
92
+ * superseded by the proxy's authoritative count for the same call, up or
93
+ * down. Without a call ID the usage is simply added.
94
+ */
95
+ private applyUsage;
96
+ private commitUsage;
97
+ /**
98
+ * Admit or deny a spawn. One transaction under the machine lock: fold,
99
+ * evaluate the proposal, reserve, sign.
100
+ */
101
+ beforeSpawn(event: SpawnRequested): Decision;
102
+ /**
103
+ * Admit or deny a model call. Reserves the caller's estimate under callId;
104
+ * completion replaces it. The local-compute plane is evaluated here and
105
+ * only here, because only calls occupy the machine.
106
+ */
107
+ beforeCall(event: CallRequested): Decision;
108
+ /** Real usage for a reserved call. Replaces the estimate; releases the slot. */
109
+ completeCall(args: {
110
+ host: HostId;
111
+ sessionId: string;
112
+ callId: string;
113
+ tokens: number;
114
+ cacheRead?: number;
115
+ usageCoverage: Coverage;
116
+ at?: number;
117
+ }): void;
118
+ /** The call never produced usage. Releases the estimate and the slot. */
119
+ failCall(args: {
120
+ host: HostId;
121
+ sessionId: string;
122
+ callId: string;
123
+ at?: number;
124
+ }): void;
125
+ private finish;
126
+ private failClosed;
127
+ peek(sessionId: string): GatewaySessionView | null;
128
+ /** Every gateway session on this machine, newest activity first. */
129
+ sessions(): GatewaySessionView[];
130
+ compute(sessionId?: string): ComputeSnapshot;
131
+ }
132
+ /** A session fed by several hosts sees the best coverage any of them provides. */
133
+ export declare function mergeCapabilities(hosts: HostId[]): HostCapabilities;
134
+ export type { Attribution };