instar 0.28.107 → 0.28.108

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.
@@ -1,28 +1,28 @@
1
1
  /**
2
2
  * LlmRateGate — actuator primitive for the burn-detection auto-heal system.
3
3
  *
4
- * Phase 1 (this file): ships as a NO-OP. The primitive exists, the shape is
5
- * stable, and instar-internal LLM callers can already consult it before each
6
- * call — but no decision-maker is wired in yet, so `shouldFire(key)` always
7
- * returns true. This deliberate sequencing avoids a regression window where
8
- * the BurnDetector emits signals with no actuator to honour them.
4
+ * Phase 1 shipped the no-op shape. Phase 4 upgrades the gate into a stateful
5
+ * actuator: the burn-throttle runbook installs throttles via `installThrottle`,
6
+ * the LLM callers consult `shouldFire` / `decide`, and a throttle auto-expires
7
+ * after its TTL elapses. Per the umbrella spec §"Signal-vs-Authority
8
+ * Decomposition", the gate enforces decisions made elsewhere — it never
9
+ * decides anything on its own.
9
10
  *
10
- * Future phases (per docs/specs/token-burn-detection-and-self-heal.md):
11
- * - Phase 4 (burn-throttle runbook): the Remediator-Tier-2 runbook stores
12
- * signed throttle decisions; the gate consults that store.
13
- * - Phase 4 (HMAC-signed jobs.json.throttle-overrides): scheduled-job
14
- * overrides land via this gate so the same actuator covers cron entries.
11
+ * State: in-memory only. The throttle store is process-local. If the agent
12
+ * restarts, any in-memory throttle is dropped — which is the right default
13
+ * (a restart is itself a reset, and the burn-detector will re-emit any
14
+ * surviving signal within 60s). Persistent throttles for scheduled-job cron
15
+ * entries live in a separate file (`.instar/jobs.json.throttle-overrides`,
16
+ * HMAC-signed) handled by the scheduler — out of scope for this primitive.
15
17
  *
16
- * Authority shape: the gate ENFORCES decisions that were made elsewhere. It
17
- * does NOT decide. Brittle threshold detectors must never write to this
18
- * gate directly — only the Remediator (with signed-context Tier-2 capability
19
- * tokens) can install a throttle. This honours the signal-vs-authority
20
- * boundary in docs/signal-vs-authority.md.
21
- *
22
- * Singleton-per-process: every LLM caller in the process shares one gate so
23
- * a single throttle decision covers every code path that uses the same
24
- * attribution key. The class exposes a static `instance()` accessor; tests
25
- * use the public constructor + `reset()` to isolate from the singleton.
18
+ * Authority: the gate accepts throttles only via `installThrottle`, which the
19
+ * runbook calls with a capability token. The token is verified by the gate
20
+ * against the agent's HMAC key (passed at constructor time when the gate
21
+ * needs to enforce, or skipped for tests). When no key is configured, the
22
+ * gate accepts throttles without verification — that's the "trusted local
23
+ * caller" mode used by the runbook before the F-8 capability surface fully
24
+ * lands. The verification path is in place so the runbook can switch to
25
+ * signed tokens without API churn.
26
26
  */
27
27
  export interface LlmRateGateDecision {
28
28
  /** Whether the gate currently allows the next LLM call for this key. */
@@ -30,34 +30,134 @@ export interface LlmRateGateDecision {
30
30
  /** ISO timestamp the decision was made (debug / log). */
31
31
  decidedAt: string;
32
32
  /** Why — for log + verification trace. */
33
- reason: 'phase-1-noop' | 'no-throttle-installed' | 'throttle-active' | 'throttle-expired';
33
+ reason: 'no-throttle-installed' | 'throttle-active' | 'throttle-expired' | 'runbook-self-exempt';
34
+ /** When the active throttle (if any) expires. Phase 4: ISO timestamp. */
35
+ throttleExpiresAt?: string;
36
+ }
37
+ export interface InstalledThrottle {
38
+ attributionKey: string;
39
+ /** ISO timestamp when the throttle was installed. */
40
+ installedAt: string;
41
+ /** ISO timestamp when the throttle auto-reverts. */
42
+ expiresAt: string;
43
+ /** Free-text reason the runbook chose this throttle. */
44
+ reason: string;
45
+ /** Issuer identity (the runbook). Audit trail only. */
46
+ issuer: string;
47
+ }
48
+ export interface InstallThrottleInput {
49
+ attributionKey: string;
50
+ /** Throttle duration in milliseconds (auto-reverts when elapsed). */
51
+ durationMs: number;
52
+ /** Free-text reason for audit. */
53
+ reason: string;
54
+ /** Issuer identity for audit (e.g. 'burn-throttle-runbook'). */
55
+ issuer: string;
56
+ /**
57
+ * Unique identifier for the originating burn signal. Used by the gate as a
58
+ * replay-prevention nonce — once consumed, the same signalId cannot install
59
+ * another throttle. The runbook derives this from the DegradationEvent's
60
+ * monotonic timestamp + attribution key. Reviewer-required (Phase 4
61
+ * second-pass review §1) to close the infinite-replay window on captured
62
+ * capability tokens.
63
+ */
64
+ signalId: string;
65
+ /**
66
+ * Capability token over the canonical install payload. When the gate has
67
+ * an HMAC key configured (production), the token MUST be present and
68
+ * valid. When no key is configured (tests / pre-F-8 wiring), the token
69
+ * is ignored — see the class doc for the "trusted local caller" caveat
70
+ * (the HMAC defends external/cross-process forgery, not in-process mints).
71
+ */
72
+ capabilityToken?: string;
73
+ }
74
+ export interface LlmRateGateOptions {
75
+ /**
76
+ * Optional HMAC key for capability-token verification. When set, every
77
+ * installThrottle call must include a valid `capabilityToken` over the
78
+ * canonical payload.
79
+ */
80
+ capabilityKey?: Buffer | null;
81
+ /** Injectable clock for tests. */
82
+ now?: () => number;
34
83
  }
35
84
  export declare class LlmRateGate {
36
85
  private static singleton;
86
+ private readonly throttles;
87
+ private readonly capabilityKey;
88
+ private readonly now;
89
+ /**
90
+ * Consumed signal IDs — replay-prevention nonces. Per Phase 4 second-pass
91
+ * review §1. Garbage-collected lazily during `installThrottle`: entries
92
+ * older than the maximum throttle TTL (2× the longest seen) can be
93
+ * dropped because their corresponding throttle has long since expired.
94
+ */
95
+ private readonly consumedSignalIds;
96
+ /** Tracks the longest throttle duration seen, used for GC of consumed IDs. */
97
+ private maxThrottleDurationMs;
98
+ constructor(opts?: LlmRateGateOptions);
37
99
  /**
38
100
  * Process-wide singleton. Tests should use `new LlmRateGate()` + `reset()`
39
101
  * rather than this accessor, so they don't bleed throttles into each other.
40
102
  */
41
103
  static instance(): LlmRateGate;
42
104
  /**
43
- * Phase 1: always returns true. The signature is stable so callers can
44
- * adopt the consult-before-call pattern today; future phases enforce it.
105
+ * Read-side: does the gate currently allow a call for this key?
45
106
  *
46
- * @param attributionKey componentName::promptFingerprint key. Reserved
47
- * values: keys starting with `burn-throttle-runbook::`
48
- * are exempt by design (self-reinforcing-loop guard
49
- * per spec §"Self-reinforcing loop guard").
107
+ * @param attributionKey componentName::promptFingerprint. Reserved prefix
108
+ * `burn-throttle-runbook::*` is structurally exempt
109
+ * (self-reinforcing-loop guard).
50
110
  */
51
111
  shouldFire(attributionKey: string): boolean;
112
+ decide(attributionKey: string): LlmRateGateDecision;
113
+ /**
114
+ * Install a throttle for an attribution key. Called by the burn-throttle
115
+ * runbook. Refuses keys with the runbook-self exempt prefix as a defence
116
+ * in depth (the gate also exempts on the read path).
117
+ *
118
+ * Returns the InstalledThrottle if accepted, or throws on:
119
+ * - Self-attribution attempts.
120
+ * - Capability-token verification failure (when key is configured).
121
+ * - Zero/negative duration.
122
+ */
123
+ installThrottle(input: InstallThrottleInput): InstalledThrottle;
124
+ private signWithKey;
52
125
  /**
53
- * Same as shouldFire() but returns the full decision record. Useful for
54
- * tests and for the burn-throttle runbook's verification step.
126
+ * Revoke an active throttle. Idempotent (no-op if no throttle exists).
127
+ * Called by the Telegram inline-button handler in Phase 5 and by the
128
+ * runbook's manual-override path.
55
129
  */
56
- decide(attributionKey: string): LlmRateGateDecision;
130
+ revokeThrottle(attributionKey: string): boolean;
131
+ /**
132
+ * List currently-active throttles. Used by the verification step (Phase 6)
133
+ * and by the dashboard surface.
134
+ */
135
+ listActiveThrottles(): InstalledThrottle[];
136
+ /**
137
+ * Compute the capability token for an install payload. Used by the
138
+ * runbook to sign its own throttle requests. Returns null when no
139
+ * capability key is configured (test mode).
140
+ *
141
+ * **Threat-model note (Phase 4 second-pass review §2):** this mint and the
142
+ * verification it pairs with both live on the same gate object, so any
143
+ * in-process caller holding a gate reference can forge a valid token. The
144
+ * HMAC therefore defends the **external/cross-process boundary** only
145
+ * — chiefly the persistent throttle-overrides file the scheduler reads
146
+ * (Phase 4-extension, not in this PR). For in-process integrity we rely
147
+ * on the fact that all callers of LlmRateGate are instar source under
148
+ * the same `/instar-dev` review gate. If/when the runbook is moved to a
149
+ * separate process, this mint method should be relocated to the runbook
150
+ * service alone and removed from the gate's API surface.
151
+ */
152
+ computeCapabilityToken(input: {
153
+ attributionKey: string;
154
+ durationMs: number;
155
+ issuer: string;
156
+ signalId: string;
157
+ }): string | null;
57
158
  /**
58
- * Reset all installed throttles. Phase 1 has no throttles, so this is a
59
- * no-op; future phases will clear the in-memory throttle map. Exposed for
60
- * tests so a misconfigured singleton can't bleed across `describe` blocks.
159
+ * Reset all installed throttles. Exposed for tests so a misconfigured
160
+ * singleton can't bleed across `describe` blocks.
61
161
  */
62
162
  reset(): void;
63
163
  }
@@ -1 +1 @@
1
- {"version":3,"file":"LlmRateGate.d.ts","sourceRoot":"","sources":["../../src/monitoring/LlmRateGate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,MAAM,WAAW,mBAAmB;IAClC,wEAAwE;IACxE,OAAO,EAAE,OAAO,CAAC;IACjB,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB,0CAA0C;IAC1C,MAAM,EAAE,cAAc,GAAG,uBAAuB,GAAG,iBAAiB,GAAG,kBAAkB,CAAC;CAC3F;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAC,SAAS,CAA4B;IAEpD;;;OAGG;IACH,MAAM,CAAC,QAAQ,IAAI,WAAW;IAO9B;;;;;;;;OAQG;IACH,UAAU,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO;IAI3C;;;OAGG;IACH,MAAM,CAAC,cAAc,EAAE,MAAM,GAAG,mBAAmB;IAmBnD;;;;OAIG;IACH,KAAK,IAAI,IAAI;CAGd"}
1
+ {"version":3,"file":"LlmRateGate.d.ts","sourceRoot":"","sources":["../../src/monitoring/LlmRateGate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH,MAAM,WAAW,mBAAmB;IAClC,wEAAwE;IACxE,OAAO,EAAE,OAAO,CAAC;IACjB,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB,0CAA0C;IAC1C,MAAM,EACF,uBAAuB,GACvB,iBAAiB,GACjB,kBAAkB,GAClB,qBAAqB,CAAC;IAC1B,yEAAyE;IACzE,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,qDAAqD;IACrD,WAAW,EAAE,MAAM,CAAC;IACpB,oDAAoD;IACpD,SAAS,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,cAAc,EAAE,MAAM,CAAC;IACvB,qEAAqE;IACrE,UAAU,EAAE,MAAM,CAAC;IACnB,kCAAkC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,kCAAkC;IAClC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAC,SAAS,CAA4B;IACpD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwC;IAClE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA6B;IAC/D,8EAA8E;IAC9E,OAAO,CAAC,qBAAqB,CAAkB;gBAEnC,IAAI,CAAC,EAAE,kBAAkB;IAKrC;;;OAGG;IACH,MAAM,CAAC,QAAQ,IAAI,WAAW;IAO9B;;;;;;OAMG;IACH,UAAU,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO;IAI3C,MAAM,CAAC,cAAc,EAAE,MAAM,GAAG,mBAAmB;IAuBnD;;;;;;;;;OASG;IACH,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,iBAAiB;IA0D/D,OAAO,CAAC,WAAW;IAUnB;;;;OAIG;IACH,cAAc,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO;IAI/C;;;OAGG;IACH,mBAAmB,IAAI,iBAAiB,EAAE;IAc1C;;;;;;;;;;;;;;;OAeG;IACH,sBAAsB,CAAC,KAAK,EAAE;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,GAAG,IAAI;IAK9H;;;OAGG;IACH,KAAK,IAAI,IAAI;CAGd"}
@@ -1,31 +1,48 @@
1
1
  /**
2
2
  * LlmRateGate — actuator primitive for the burn-detection auto-heal system.
3
3
  *
4
- * Phase 1 (this file): ships as a NO-OP. The primitive exists, the shape is
5
- * stable, and instar-internal LLM callers can already consult it before each
6
- * call — but no decision-maker is wired in yet, so `shouldFire(key)` always
7
- * returns true. This deliberate sequencing avoids a regression window where
8
- * the BurnDetector emits signals with no actuator to honour them.
4
+ * Phase 1 shipped the no-op shape. Phase 4 upgrades the gate into a stateful
5
+ * actuator: the burn-throttle runbook installs throttles via `installThrottle`,
6
+ * the LLM callers consult `shouldFire` / `decide`, and a throttle auto-expires
7
+ * after its TTL elapses. Per the umbrella spec §"Signal-vs-Authority
8
+ * Decomposition", the gate enforces decisions made elsewhere — it never
9
+ * decides anything on its own.
9
10
  *
10
- * Future phases (per docs/specs/token-burn-detection-and-self-heal.md):
11
- * - Phase 4 (burn-throttle runbook): the Remediator-Tier-2 runbook stores
12
- * signed throttle decisions; the gate consults that store.
13
- * - Phase 4 (HMAC-signed jobs.json.throttle-overrides): scheduled-job
14
- * overrides land via this gate so the same actuator covers cron entries.
11
+ * State: in-memory only. The throttle store is process-local. If the agent
12
+ * restarts, any in-memory throttle is dropped — which is the right default
13
+ * (a restart is itself a reset, and the burn-detector will re-emit any
14
+ * surviving signal within 60s). Persistent throttles for scheduled-job cron
15
+ * entries live in a separate file (`.instar/jobs.json.throttle-overrides`,
16
+ * HMAC-signed) handled by the scheduler — out of scope for this primitive.
15
17
  *
16
- * Authority shape: the gate ENFORCES decisions that were made elsewhere. It
17
- * does NOT decide. Brittle threshold detectors must never write to this
18
- * gate directly — only the Remediator (with signed-context Tier-2 capability
19
- * tokens) can install a throttle. This honours the signal-vs-authority
20
- * boundary in docs/signal-vs-authority.md.
21
- *
22
- * Singleton-per-process: every LLM caller in the process shares one gate so
23
- * a single throttle decision covers every code path that uses the same
24
- * attribution key. The class exposes a static `instance()` accessor; tests
25
- * use the public constructor + `reset()` to isolate from the singleton.
18
+ * Authority: the gate accepts throttles only via `installThrottle`, which the
19
+ * runbook calls with a capability token. The token is verified by the gate
20
+ * against the agent's HMAC key (passed at constructor time when the gate
21
+ * needs to enforce, or skipped for tests). When no key is configured, the
22
+ * gate accepts throttles without verification — that's the "trusted local
23
+ * caller" mode used by the runbook before the F-8 capability surface fully
24
+ * lands. The verification path is in place so the runbook can switch to
25
+ * signed tokens without API churn.
26
26
  */
27
+ import crypto from 'node:crypto';
27
28
  export class LlmRateGate {
28
29
  static singleton = null;
30
+ throttles = new Map();
31
+ capabilityKey;
32
+ now;
33
+ /**
34
+ * Consumed signal IDs — replay-prevention nonces. Per Phase 4 second-pass
35
+ * review §1. Garbage-collected lazily during `installThrottle`: entries
36
+ * older than the maximum throttle TTL (2× the longest seen) can be
37
+ * dropped because their corresponding throttle has long since expired.
38
+ */
39
+ consumedSignalIds = new Map();
40
+ /** Tracks the longest throttle duration seen, used for GC of consumed IDs. */
41
+ maxThrottleDurationMs = 60 * 60 * 1000;
42
+ constructor(opts) {
43
+ this.capabilityKey = opts?.capabilityKey ?? null;
44
+ this.now = opts?.now ?? (() => Date.now());
45
+ }
29
46
  /**
30
47
  * Process-wide singleton. Tests should use `new LlmRateGate()` + `reset()`
31
48
  * rather than this accessor, so they don't bleed throttles into each other.
@@ -37,46 +54,162 @@ export class LlmRateGate {
37
54
  return LlmRateGate.singleton;
38
55
  }
39
56
  /**
40
- * Phase 1: always returns true. The signature is stable so callers can
41
- * adopt the consult-before-call pattern today; future phases enforce it.
57
+ * Read-side: does the gate currently allow a call for this key?
42
58
  *
43
- * @param attributionKey componentName::promptFingerprint key. Reserved
44
- * values: keys starting with `burn-throttle-runbook::`
45
- * are exempt by design (self-reinforcing-loop guard
46
- * per spec §"Self-reinforcing loop guard").
59
+ * @param attributionKey componentName::promptFingerprint. Reserved prefix
60
+ * `burn-throttle-runbook::*` is structurally exempt
61
+ * (self-reinforcing-loop guard).
47
62
  */
48
63
  shouldFire(attributionKey) {
49
64
  return this.decide(attributionKey).allowed;
50
65
  }
51
- /**
52
- * Same as shouldFire() but returns the full decision record. Useful for
53
- * tests and for the burn-throttle runbook's verification step.
54
- */
55
66
  decide(attributionKey) {
56
- // Self-attribution exempt (spec §"Self-reinforcing loop guard"): the
57
- // runbook's own LLM calls cannot be throttled by the runbook's own
58
- // decisions, ever. This is the structural floor that prevents a
59
- // deadlock where the throttle stops the alert from being sent.
67
+ const decidedAt = new Date(this.now()).toISOString();
60
68
  if (attributionKey.startsWith('burn-throttle-runbook::')) {
61
- return {
62
- allowed: true,
63
- decidedAt: new Date().toISOString(),
64
- reason: 'phase-1-noop',
65
- };
69
+ return { allowed: true, decidedAt, reason: 'runbook-self-exempt' };
70
+ }
71
+ const throttle = this.throttles.get(attributionKey);
72
+ if (!throttle) {
73
+ return { allowed: true, decidedAt, reason: 'no-throttle-installed' };
74
+ }
75
+ const expiresMs = Date.parse(throttle.expiresAt);
76
+ if (Number.isNaN(expiresMs) || this.now() >= expiresMs) {
77
+ // Auto-revert on read — keeps the map small and avoids stale reads.
78
+ this.throttles.delete(attributionKey);
79
+ return { allowed: true, decidedAt, reason: 'throttle-expired' };
66
80
  }
67
81
  return {
68
- allowed: true,
69
- decidedAt: new Date().toISOString(),
70
- reason: 'phase-1-noop',
82
+ allowed: false,
83
+ decidedAt,
84
+ reason: 'throttle-active',
85
+ throttleExpiresAt: throttle.expiresAt,
86
+ };
87
+ }
88
+ /**
89
+ * Install a throttle for an attribution key. Called by the burn-throttle
90
+ * runbook. Refuses keys with the runbook-self exempt prefix as a defence
91
+ * in depth (the gate also exempts on the read path).
92
+ *
93
+ * Returns the InstalledThrottle if accepted, or throws on:
94
+ * - Self-attribution attempts.
95
+ * - Capability-token verification failure (when key is configured).
96
+ * - Zero/negative duration.
97
+ */
98
+ installThrottle(input) {
99
+ if (input.attributionKey.startsWith('burn-throttle-runbook::')) {
100
+ throw new Error('Refusing to install throttle on runbook-self attribution key (self-reinforcing-loop guard).');
101
+ }
102
+ if (input.durationMs <= 0) {
103
+ throw new Error(`Throttle duration must be positive (got ${input.durationMs}).`);
104
+ }
105
+ if (!input.signalId || input.signalId.length === 0) {
106
+ throw new Error('LlmRateGate.installThrottle: signalId is required (replay-prevention nonce).');
107
+ }
108
+ // Replay-prevention: a signalId may install at most one throttle. The
109
+ // map is GC'd lazily below. Per Phase 4 second-pass review §1.
110
+ if (this.consumedSignalIds.has(input.signalId)) {
111
+ throw new Error(`LlmRateGate.installThrottle: signalId ${input.signalId} has already been consumed (replay refused).`);
112
+ }
113
+ // Capability-token verification when a key is configured. The canonical
114
+ // payload includes signalId so captured tokens cannot be replayed with a
115
+ // different signal context.
116
+ if (this.capabilityKey) {
117
+ const expected = this.signWithKey({
118
+ attributionKey: input.attributionKey,
119
+ durationMs: input.durationMs,
120
+ issuer: input.issuer,
121
+ signalId: input.signalId,
122
+ });
123
+ if (!input.capabilityToken || input.capabilityToken !== expected) {
124
+ throw new Error('LlmRateGate.installThrottle: invalid or missing capability token.');
125
+ }
126
+ }
127
+ const nowMs = this.now();
128
+ const nowIso = new Date(nowMs).toISOString();
129
+ const expiresIso = new Date(nowMs + input.durationMs).toISOString();
130
+ const throttle = {
131
+ attributionKey: input.attributionKey,
132
+ installedAt: nowIso,
133
+ expiresAt: expiresIso,
134
+ reason: input.reason,
135
+ issuer: input.issuer,
71
136
  };
137
+ this.throttles.set(input.attributionKey, throttle);
138
+ this.consumedSignalIds.set(input.signalId, nowMs);
139
+ // Track the longest duration we've ever seen for GC bookkeeping.
140
+ if (input.durationMs > this.maxThrottleDurationMs) {
141
+ this.maxThrottleDurationMs = input.durationMs;
142
+ }
143
+ // GC consumed signal IDs older than 2x the longest throttle TTL.
144
+ const gcCutoff = nowMs - 2 * this.maxThrottleDurationMs;
145
+ for (const [sigId, sigTs] of this.consumedSignalIds.entries()) {
146
+ if (sigTs < gcCutoff)
147
+ this.consumedSignalIds.delete(sigId);
148
+ }
149
+ return throttle;
150
+ }
151
+ signWithKey(payload) {
152
+ const canonical = JSON.stringify({
153
+ attributionKey: payload.attributionKey,
154
+ durationMs: payload.durationMs,
155
+ issuer: payload.issuer,
156
+ signalId: payload.signalId,
157
+ });
158
+ return crypto.createHmac('sha256', this.capabilityKey).update(canonical).digest('hex');
159
+ }
160
+ /**
161
+ * Revoke an active throttle. Idempotent (no-op if no throttle exists).
162
+ * Called by the Telegram inline-button handler in Phase 5 and by the
163
+ * runbook's manual-override path.
164
+ */
165
+ revokeThrottle(attributionKey) {
166
+ return this.throttles.delete(attributionKey);
167
+ }
168
+ /**
169
+ * List currently-active throttles. Used by the verification step (Phase 6)
170
+ * and by the dashboard surface.
171
+ */
172
+ listActiveThrottles() {
173
+ const now = this.now();
174
+ const active = [];
175
+ for (const [key, throttle] of this.throttles.entries()) {
176
+ const expiresMs = Date.parse(throttle.expiresAt);
177
+ if (!Number.isNaN(expiresMs) && now < expiresMs) {
178
+ active.push(throttle);
179
+ }
180
+ else {
181
+ this.throttles.delete(key);
182
+ }
183
+ }
184
+ return active;
185
+ }
186
+ /**
187
+ * Compute the capability token for an install payload. Used by the
188
+ * runbook to sign its own throttle requests. Returns null when no
189
+ * capability key is configured (test mode).
190
+ *
191
+ * **Threat-model note (Phase 4 second-pass review §2):** this mint and the
192
+ * verification it pairs with both live on the same gate object, so any
193
+ * in-process caller holding a gate reference can forge a valid token. The
194
+ * HMAC therefore defends the **external/cross-process boundary** only
195
+ * — chiefly the persistent throttle-overrides file the scheduler reads
196
+ * (Phase 4-extension, not in this PR). For in-process integrity we rely
197
+ * on the fact that all callers of LlmRateGate are instar source under
198
+ * the same `/instar-dev` review gate. If/when the runbook is moved to a
199
+ * separate process, this mint method should be relocated to the runbook
200
+ * service alone and removed from the gate's API surface.
201
+ */
202
+ computeCapabilityToken(input) {
203
+ if (!this.capabilityKey)
204
+ return null;
205
+ return this.signWithKey(input);
72
206
  }
73
207
  /**
74
- * Reset all installed throttles. Phase 1 has no throttles, so this is a
75
- * no-op; future phases will clear the in-memory throttle map. Exposed for
76
- * tests so a misconfigured singleton can't bleed across `describe` blocks.
208
+ * Reset all installed throttles. Exposed for tests so a misconfigured
209
+ * singleton can't bleed across `describe` blocks.
77
210
  */
78
211
  reset() {
79
- // Phase 1: no state to reset. Intentional empty body.
212
+ this.throttles.clear();
80
213
  }
81
214
  }
82
215
  //# sourceMappingURL=LlmRateGate.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"LlmRateGate.js","sourceRoot":"","sources":["../../src/monitoring/LlmRateGate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAWH,MAAM,OAAO,WAAW;IACd,MAAM,CAAC,SAAS,GAAuB,IAAI,CAAC;IAEpD;;;OAGG;IACH,MAAM,CAAC,QAAQ;QACb,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,CAAC;YAC3B,WAAW,CAAC,SAAS,GAAG,IAAI,WAAW,EAAE,CAAC;QAC5C,CAAC;QACD,OAAO,WAAW,CAAC,SAAS,CAAC;IAC/B,CAAC;IAED;;;;;;;;OAQG;IACH,UAAU,CAAC,cAAsB;QAC/B,OAAO,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,OAAO,CAAC;IAC7C,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,cAAsB;QAC3B,qEAAqE;QACrE,mEAAmE;QACnE,gEAAgE;QAChE,+DAA+D;QAC/D,IAAI,cAAc,CAAC,UAAU,CAAC,yBAAyB,CAAC,EAAE,CAAC;YACzD,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;gBACnC,MAAM,EAAE,cAAc;aACvB,CAAC;QACJ,CAAC;QACD,OAAO;YACL,OAAO,EAAE,IAAI;YACb,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,MAAM,EAAE,cAAc;SACvB,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACH,KAAK;QACH,sDAAsD;IACxD,CAAC"}
1
+ {"version":3,"file":"LlmRateGate.js","sourceRoot":"","sources":["../../src/monitoring/LlmRateGate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,MAAM,MAAM,aAAa,CAAC;AAmEjC,MAAM,OAAO,WAAW;IACd,MAAM,CAAC,SAAS,GAAuB,IAAI,CAAC;IACnC,SAAS,GAAG,IAAI,GAAG,EAA6B,CAAC;IACjD,aAAa,CAAgB;IAC7B,GAAG,CAAe;IACnC;;;;;OAKG;IACc,iBAAiB,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC/D,8EAA8E;IACtE,qBAAqB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAE/C,YAAY,IAAyB;QACnC,IAAI,CAAC,aAAa,GAAG,IAAI,EAAE,aAAa,IAAI,IAAI,CAAC;QACjD,IAAI,CAAC,GAAG,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,QAAQ;QACb,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,CAAC;YAC3B,WAAW,CAAC,SAAS,GAAG,IAAI,WAAW,EAAE,CAAC;QAC5C,CAAC;QACD,OAAO,WAAW,CAAC,SAAS,CAAC;IAC/B,CAAC;IAED;;;;;;OAMG;IACH,UAAU,CAAC,cAAsB;QAC/B,OAAO,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,OAAO,CAAC;IAC7C,CAAC;IAED,MAAM,CAAC,cAAsB;QAC3B,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;QACrD,IAAI,cAAc,CAAC,UAAU,CAAC,yBAAyB,CAAC,EAAE,CAAC;YACzD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAAC;QACrE,CAAC;QACD,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QACpD,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,uBAAuB,EAAE,CAAC;QACvE,CAAC;QACD,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;QACjD,IAAI,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,SAAS,EAAE,CAAC;YACvD,oEAAoE;YACpE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;YACtC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;QAClE,CAAC;QACD,OAAO;YACL,OAAO,EAAE,KAAK;YACd,SAAS;YACT,MAAM,EAAE,iBAAiB;YACzB,iBAAiB,EAAE,QAAQ,CAAC,SAAS;SACtC,CAAC;IACJ,CAAC;IAED;;;;;;;;;OASG;IACH,eAAe,CAAC,KAA2B;QACzC,IAAI,KAAK,CAAC,cAAc,CAAC,UAAU,CAAC,yBAAyB,CAAC,EAAE,CAAC;YAC/D,MAAM,IAAI,KAAK,CAAC,6FAA6F,CAAC,CAAC;QACjH,CAAC;QACD,IAAI,KAAK,CAAC,UAAU,IAAI,CAAC,EAAE,CAAC;YAC1B,MAAM,IAAI,KAAK,CAAC,2CAA2C,KAAK,CAAC,UAAU,IAAI,CAAC,CAAC;QACnF,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACnD,MAAM,IAAI,KAAK,CAAC,8EAA8E,CAAC,CAAC;QAClG,CAAC;QAED,sEAAsE;QACtE,+DAA+D;QAC/D,IAAI,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC/C,MAAM,IAAI,KAAK,CAAC,yCAAyC,KAAK,CAAC,QAAQ,8CAA8C,CAAC,CAAC;QACzH,CAAC;QAED,wEAAwE;QACxE,yEAAyE;QACzE,4BAA4B;QAC5B,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACvB,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC;gBAChC,cAAc,EAAE,KAAK,CAAC,cAAc;gBACpC,UAAU,EAAE,KAAK,CAAC,UAAU;gBAC5B,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,QAAQ,EAAE,KAAK,CAAC,QAAQ;aACzB,CAAC,CAAC;YACH,IAAI,CAAC,KAAK,CAAC,eAAe,IAAI,KAAK,CAAC,eAAe,KAAK,QAAQ,EAAE,CAAC;gBACjE,MAAM,IAAI,KAAK,CAAC,mEAAmE,CAAC,CAAC;YACvF,CAAC;QACH,CAAC;QAED,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;QAC7C,MAAM,UAAU,GAAG,IAAI,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC,WAAW,EAAE,CAAC;QACpE,MAAM,QAAQ,GAAsB;YAClC,cAAc,EAAE,KAAK,CAAC,cAAc;YACpC,WAAW,EAAE,MAAM;YACnB,SAAS,EAAE,UAAU;YACrB,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,MAAM,EAAE,KAAK,CAAC,MAAM;SACrB,CAAC;QACF,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;QACnD,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QAElD,iEAAiE;QACjE,IAAI,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC,qBAAqB,EAAE,CAAC;YAClD,IAAI,CAAC,qBAAqB,GAAG,KAAK,CAAC,UAAU,CAAC;QAChD,CAAC;QACD,iEAAiE;QACjE,MAAM,QAAQ,GAAG,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,qBAAqB,CAAC;QACxD,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,iBAAiB,CAAC,OAAO,EAAE,EAAE,CAAC;YAC9D,IAAI,KAAK,GAAG,QAAQ;gBAAE,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC7D,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;IAEO,WAAW,CAAC,OAAyF;QAC3G,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;YAC/B,cAAc,EAAE,OAAO,CAAC,cAAc;YACtC,UAAU,EAAE,OAAO,CAAC,UAAU;YAC9B,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,QAAQ,EAAE,OAAO,CAAC,QAAQ;SAC3B,CAAC,CAAC;QACH,OAAO,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,aAAc,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC1F,CAAC;IAED;;;;OAIG;IACH,cAAc,CAAC,cAAsB;QACnC,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;IAC/C,CAAC;IAED;;;OAGG;IACH,mBAAmB;QACjB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,MAAM,GAAwB,EAAE,CAAC;QACvC,KAAK,MAAM,CAAC,GAAG,EAAE,QAAQ,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,CAAC;YACvD,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;YACjD,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,GAAG,GAAG,SAAS,EAAE,CAAC;gBAChD,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACxB,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACH,sBAAsB,CAAC,KAAuF;QAC5G,IAAI,CAAC,IAAI,CAAC,aAAa;YAAE,OAAO,IAAI,CAAC;QACrC,OAAO,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IACjC,CAAC;IAED;;;OAGG;IACH,KAAK;QACH,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;IACzB,CAAC"}
@@ -35,6 +35,13 @@ export interface ProjectRow {
35
35
  eventCount: number;
36
36
  sessionCount: number;
37
37
  }
38
+ export interface AttributionKeyRow {
39
+ attributionKey: string;
40
+ totalTokens: number;
41
+ eventCount: number;
42
+ firstTs: number;
43
+ lastTs: number;
44
+ }
38
45
  export interface OrphanRow {
39
46
  sessionId: string;
40
47
  projectPath: string | null;
@@ -147,6 +154,15 @@ export declare class TokenLedger {
147
154
  byProject({ sinceMs }?: {
148
155
  sinceMs?: number;
149
156
  }): ProjectRow[];
157
+ /**
158
+ * Aggregate by attribution key (Phase 3 of burn-detection-and-self-heal
159
+ * spec). Returns one row per key with totals, event count, and the time
160
+ * window the key has been seen across the requested period.
161
+ */
162
+ byAttributionKey({ sinceMs, limit }?: {
163
+ sinceMs?: number;
164
+ limit?: number;
165
+ }): AttributionKeyRow[];
150
166
  /** Sessions whose newest event is older than `idleMs` ago. */
151
167
  orphans({ idleMs }: {
152
168
  idleMs: number;
@@ -1 +1 @@
1
- {"version":3,"file":"TokenLedger.d.ts","sourceRoot":"","sources":["../../src/monitoring/TokenLedger.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TokenLedger.d.ts","sourceRoot":"","sources":["../../src/monitoring/TokenLedger.ts"],"names":[],"mappings":"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"}
@@ -551,6 +551,34 @@ export class TokenLedger {
551
551
  sessionCount: Number(r.sessionCount) || 0,
552
552
  }));
553
553
  }
554
+ /**
555
+ * Aggregate by attribution key (Phase 3 of burn-detection-and-self-heal
556
+ * spec). Returns one row per key with totals, event count, and the time
557
+ * window the key has been seen across the requested period.
558
+ */
559
+ byAttributionKey({ sinceMs, limit = 100 } = {}) {
560
+ const since = sinceMs ?? 0;
561
+ const rows = this.db
562
+ .prepare(`SELECT
563
+ attribution_key AS attributionKey,
564
+ SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens) AS totalTokens,
565
+ COUNT(*) AS eventCount,
566
+ MIN(ts) AS firstTs,
567
+ MAX(ts) AS lastTs
568
+ FROM token_events
569
+ WHERE ts >= ?
570
+ GROUP BY attribution_key
571
+ ORDER BY totalTokens DESC
572
+ LIMIT ?`)
573
+ .all(since, limit);
574
+ return rows.map(r => ({
575
+ attributionKey: r.attributionKey,
576
+ totalTokens: Number(r.totalTokens) || 0,
577
+ eventCount: Number(r.eventCount) || 0,
578
+ firstTs: Number(r.firstTs) || 0,
579
+ lastTs: Number(r.lastTs) || 0,
580
+ }));
581
+ }
554
582
  /** Sessions whose newest event is older than `idleMs` ago. */
555
583
  orphans({ idleMs }) {
556
584
  const now = Date.now();