instar 0.28.106 → 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.
Files changed (31) hide show
  1. package/dist/monitoring/AttributionResolver.d.ts +46 -0
  2. package/dist/monitoring/AttributionResolver.d.ts.map +1 -0
  3. package/dist/monitoring/AttributionResolver.js +107 -0
  4. package/dist/monitoring/AttributionResolver.js.map +1 -0
  5. package/dist/monitoring/BurnDetector.d.ts +84 -0
  6. package/dist/monitoring/BurnDetector.d.ts.map +1 -0
  7. package/dist/monitoring/BurnDetector.js +169 -0
  8. package/dist/monitoring/BurnDetector.js.map +1 -0
  9. package/dist/monitoring/BurnThrottleRunbook.d.ts +97 -0
  10. package/dist/monitoring/BurnThrottleRunbook.d.ts.map +1 -0
  11. package/dist/monitoring/BurnThrottleRunbook.js +202 -0
  12. package/dist/monitoring/BurnThrottleRunbook.js.map +1 -0
  13. package/dist/monitoring/LlmRateGate.d.ts +133 -33
  14. package/dist/monitoring/LlmRateGate.d.ts.map +1 -1
  15. package/dist/monitoring/LlmRateGate.js +179 -46
  16. package/dist/monitoring/LlmRateGate.js.map +1 -1
  17. package/dist/monitoring/TokenLedger.d.ts +16 -0
  18. package/dist/monitoring/TokenLedger.d.ts.map +1 -1
  19. package/dist/monitoring/TokenLedger.js +28 -0
  20. package/dist/monitoring/TokenLedger.js.map +1 -1
  21. package/dist/monitoring/attribution-manifest.d.ts +39 -0
  22. package/dist/monitoring/attribution-manifest.d.ts.map +1 -0
  23. package/dist/monitoring/attribution-manifest.js +83 -0
  24. package/dist/monitoring/attribution-manifest.js.map +1 -0
  25. package/package.json +1 -1
  26. package/src/data/builtin-manifest.json +2 -2
  27. package/upgrades/0.28.107.md +40 -0
  28. package/upgrades/0.28.108.md +50 -0
  29. package/upgrades/side-effects/token-burn-detection-phase-2.md +44 -0
  30. package/upgrades/side-effects/token-burn-detection-phase-3.md +59 -0
  31. package/upgrades/side-effects/token-burn-detection-phase-4.md +78 -0
@@ -0,0 +1,202 @@
1
+ /**
2
+ * BurnThrottleRunbook — Tier-2 authority surface for burn-detection signals.
3
+ *
4
+ * Phase 4 of docs/specs/token-burn-detection-and-self-heal.md. This is the
5
+ * ONLY blocking surface in the burn-detection pipeline. The BurnDetector
6
+ * (Phase 3) emits structured signals via DegradationReporter; this runbook
7
+ * subscribes via the existing handler hook and decides:
8
+ * - alert-only (default for `unknown::*` keys and any caller config disables)
9
+ * - throttle (default for known component names)
10
+ * - both (the umbrella spec's primary policy)
11
+ *
12
+ * Authority shape (per umbrella spec §"Signal-vs-Authority Decomposition"):
13
+ * the runbook is the only piece in this pipeline with blocking authority.
14
+ * The detector emits; the gate enforces; the runbook decides.
15
+ *
16
+ * Self-reinforcing-loop guard: the runbook never throttles a key starting
17
+ * with `burn-throttle-runbook::*` — defence-in-depth (the gate also refuses
18
+ * to install such throttles, the detector also exempts the prefix). This
19
+ * three-layer guard is from the convergence audit.
20
+ */
21
+ export const DEFAULT_BURN_THROTTLE_CONFIG = {
22
+ autoThrottle: true,
23
+ autoThrottleOnUnknown: false,
24
+ throttleDurationMs: 60 * 60 * 1000,
25
+ };
26
+ /** Identity tag the runbook uses when installing throttles. */
27
+ export const RUNBOOK_ISSUER = 'burn-throttle-runbook';
28
+ export class BurnThrottleRunbook {
29
+ gate;
30
+ config;
31
+ sendTelegram;
32
+ alertTopicId;
33
+ now;
34
+ constructor(deps) {
35
+ this.gate = deps.gate;
36
+ this.config = { ...DEFAULT_BURN_THROTTLE_CONFIG, ...(deps.config ?? {}) };
37
+ this.sendTelegram = deps.sendTelegram;
38
+ this.alertTopicId = deps.alertTopicId ?? 8615;
39
+ this.now = deps.now ?? (() => Date.now());
40
+ }
41
+ /**
42
+ * Handle a burn-detection signal. Called from a subscriber hook on
43
+ * DegradationReporter for feature='token-burn-detection' events.
44
+ *
45
+ * Returns the outcome so tests + audit + the verification step (Phase 6)
46
+ * can inspect what happened.
47
+ */
48
+ handle(event) {
49
+ const attributionKey = extractAttributionKey(event);
50
+ const trigger = extractTrigger(event);
51
+ const decidedAt = new Date(this.now()).toISOString();
52
+ const signalId = `${event.timestamp}:${attributionKey}`;
53
+ // 1. Self-attribution: refuse to throttle the runbook itself, AND emit
54
+ // a high-severity escalation alert. Per Phase 4 second-pass review §3:
55
+ // silent swallow is the wrong response here — if the runbook itself
56
+ // is being attributed-to as a burn source, something is structurally
57
+ // wrong (a bug in the runbook's own LLM-bound code, or attribution
58
+ // being mis-applied to a legitimate runbook call). Either way, that's
59
+ // exactly the case a user needs to know about. So we DO send Telegram.
60
+ if (attributionKey.startsWith(`${RUNBOOK_ISSUER}::`)) {
61
+ this.fireTelegram(`URGENT: the burn-throttle runbook itself is being flagged as a burn source ` +
62
+ `(${attributionKey}). I refused to throttle myself by design, but this likely ` +
63
+ `means either the runbook has a bug producing too many LLM calls, or attribution ` +
64
+ `is being mis-applied to legitimate runbook work. Please investigate.`);
65
+ return {
66
+ kind: 'alert-only-self-attribution',
67
+ attributionKey,
68
+ decidedAt,
69
+ trigger,
70
+ reason: 'Refusing to throttle the runbook itself (self-reinforcing-loop guard); escalation alert sent.',
71
+ };
72
+ }
73
+ // Compose the alert text now so we send it regardless of throttle outcome.
74
+ const alertText = composeAlertText(event, attributionKey, trigger);
75
+ // 2. Caller config disables auto-throttle.
76
+ if (!this.config.autoThrottle) {
77
+ this.fireTelegram(alertText);
78
+ return {
79
+ kind: 'alert-only-config-disabled',
80
+ attributionKey,
81
+ decidedAt,
82
+ trigger,
83
+ reason: 'autoThrottle is disabled in config; alert sent but no throttle installed.',
84
+ };
85
+ }
86
+ // 3. Unknown attribution + caller has not opted in to auto-throttle on unknown.
87
+ if (isUnknownKey(attributionKey) && !this.config.autoThrottleOnUnknown) {
88
+ this.fireTelegram(alertText);
89
+ return {
90
+ kind: 'alert-only-unknown',
91
+ attributionKey,
92
+ decidedAt,
93
+ trigger,
94
+ reason: 'Attribution key has no known component; operator must opt in to auto-throttle on unknown.',
95
+ };
96
+ }
97
+ // 4. Install throttle, send alert.
98
+ const token = this.gate.computeCapabilityToken({
99
+ attributionKey,
100
+ durationMs: this.config.throttleDurationMs,
101
+ issuer: RUNBOOK_ISSUER,
102
+ signalId,
103
+ }) ?? undefined;
104
+ try {
105
+ const throttle = this.gate.installThrottle({
106
+ attributionKey,
107
+ durationMs: this.config.throttleDurationMs,
108
+ reason: `${trigger} trigger crossed threshold; runbook installed bounded throttle.`,
109
+ issuer: RUNBOOK_ISSUER,
110
+ signalId,
111
+ capabilityToken: token,
112
+ });
113
+ this.fireTelegram(`${alertText}\n\nI have slowed this component down. It will resume automatically in ${Math.round(this.config.throttleDurationMs / 60000)} minutes, or you can reply STOP to release it sooner.`);
114
+ return {
115
+ kind: 'throttle-installed',
116
+ attributionKey,
117
+ decidedAt,
118
+ trigger,
119
+ throttle,
120
+ reason: 'Throttle installed, alert sent.',
121
+ };
122
+ }
123
+ catch (err) {
124
+ this.fireTelegram(`${alertText}\n\nI tried to slow it down automatically but the slowdown did not take effect. You may need to look at this one.`);
125
+ return {
126
+ kind: 'throttle-failed',
127
+ attributionKey,
128
+ decidedAt,
129
+ trigger,
130
+ reason: `Throttle install failed: ${err.message}`,
131
+ };
132
+ }
133
+ }
134
+ fireTelegram(text) {
135
+ if (!this.sendTelegram)
136
+ return;
137
+ try {
138
+ // Caller may return a promise; we don't await — the runbook is on the
139
+ // signal-handling hot path and must not block. Errors are swallowed:
140
+ // a failed alert can't block the throttle decision, which is the
141
+ // important part.
142
+ void this.sendTelegram(this.alertTopicId, text);
143
+ }
144
+ catch {
145
+ // Intentional swallow.
146
+ }
147
+ }
148
+ }
149
+ /* ---------- pure helpers (exported for tests) ---------- */
150
+ /**
151
+ * Extract the attribution key from a burn-detection DegradationEvent. The
152
+ * BurnDetector formats the `primary` field as "attribution_key <KEY>
153
+ * sustained spend within thresholds" — we parse it back here. The wire
154
+ * shape is internal to the burn-detection pipeline; if it changes, change
155
+ * both ends in the same PR.
156
+ */
157
+ export function extractAttributionKey(event) {
158
+ const m = /attribution_key\s+(\S+)/.exec(event.primary || '');
159
+ return m && m[1] ? m[1] : 'unknown::no-key';
160
+ }
161
+ /**
162
+ * Extract the trigger label from the event's reason field. Returns
163
+ * 'absolute-share' or 'baseline-divergence' or 'unknown-trigger'.
164
+ */
165
+ export function extractTrigger(event) {
166
+ if (/% of 24h spend/.test(event.reason || ''))
167
+ return 'absolute-share';
168
+ if (/baseline/i.test(event.reason || ''))
169
+ return 'baseline-divergence';
170
+ return 'unknown-trigger';
171
+ }
172
+ /** Is the attribution key one of the unknown::* fallback shapes? */
173
+ export function isUnknownKey(attributionKey) {
174
+ return attributionKey.startsWith('unknown::');
175
+ }
176
+ /** Compose the ELI16 alert text for Telegram. No backticks, no camelCase config keys, narrative tone. */
177
+ export function composeAlertText(event, attributionKey, trigger) {
178
+ const friendlyName = humanize(attributionKey);
179
+ const triggerNarrative = trigger === 'absolute-share'
180
+ ? 'is using more than a quarter of the agent\'s total token budget over the last day'
181
+ : trigger === 'baseline-divergence'
182
+ ? 'has more than doubled its normal rate in the last hour, and the absolute amount is large enough to matter'
183
+ : 'crossed one of the burn thresholds';
184
+ return (`Heads up: ${friendlyName} ${triggerNarrative}.\n\n` +
185
+ `${event.reason}\n\n` +
186
+ `${event.impact}`);
187
+ }
188
+ function humanize(attributionKey) {
189
+ if (attributionKey.startsWith('unknown::'))
190
+ return 'an unknown component';
191
+ if (attributionKey.startsWith('user-job:')) {
192
+ const m = /^user-job:([^:]+)/.exec(attributionKey);
193
+ return m ? `your scheduled job "${m[1]}"` : 'a scheduled job';
194
+ }
195
+ if (attributionKey.startsWith('user-hook:')) {
196
+ const m = /^user-hook:([^:]+)/.exec(attributionKey);
197
+ return m ? `your hook "${m[1]}"` : 'a hook';
198
+ }
199
+ const m = /^([^:]+)::/.exec(attributionKey);
200
+ return m ? `the ${m[1]} component` : attributionKey;
201
+ }
202
+ //# sourceMappingURL=BurnThrottleRunbook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BurnThrottleRunbook.js","sourceRoot":"","sources":["../../src/monitoring/BurnThrottleRunbook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAoBH,MAAM,CAAC,MAAM,4BAA4B,GAAuB;IAC9D,YAAY,EAAE,IAAI;IAClB,qBAAqB,EAAE,KAAK;IAC5B,kBAAkB,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI;CACnC,CAAC;AAkCF,+DAA+D;AAC/D,MAAM,CAAC,MAAM,cAAc,GAAG,uBAAuB,CAAC;AAEtD,MAAM,OAAO,mBAAmB;IACb,IAAI,CAAc;IAClB,MAAM,CAAqB;IAC3B,YAAY,CAAkC;IAC9C,YAAY,CAAS;IACrB,GAAG,CAAe;IAEnC,YAAY,IAA6B;QACvC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,MAAM,GAAG,EAAE,GAAG,4BAA4B,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,CAAC;QAC1E,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC;QACtC,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC;QAC9C,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAC5C,CAAC;IAED;;;;;;OAMG;IACH,MAAM,CAAC,KAAuB;QAC5B,MAAM,cAAc,GAAG,qBAAqB,CAAC,KAAK,CAAC,CAAC;QACpD,MAAM,OAAO,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;QACtC,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;QACrD,MAAM,QAAQ,GAAG,GAAG,KAAK,CAAC,SAAS,IAAI,cAAc,EAAE,CAAC;QAExD,uEAAuE;QACvE,0EAA0E;QAC1E,uEAAuE;QACvE,wEAAwE;QACxE,sEAAsE;QACtE,yEAAyE;QACzE,0EAA0E;QAC1E,IAAI,cAAc,CAAC,UAAU,CAAC,GAAG,cAAc,IAAI,CAAC,EAAE,CAAC;YACrD,IAAI,CAAC,YAAY,CACf,6EAA6E;gBAC7E,IAAI,cAAc,6DAA6D;gBAC/E,kFAAkF;gBAClF,sEAAsE,CACvE,CAAC;YACF,OAAO;gBACL,IAAI,EAAE,6BAA6B;gBACnC,cAAc;gBACd,SAAS;gBACT,OAAO;gBACP,MAAM,EAAE,+FAA+F;aACxG,CAAC;QACJ,CAAC;QAED,2EAA2E;QAC3E,MAAM,SAAS,GAAG,gBAAgB,CAAC,KAAK,EAAE,cAAc,EAAE,OAAO,CAAC,CAAC;QAEnE,2CAA2C;QAC3C,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;YAC9B,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;YAC7B,OAAO;gBACL,IAAI,EAAE,4BAA4B;gBAClC,cAAc;gBACd,SAAS;gBACT,OAAO;gBACP,MAAM,EAAE,2EAA2E;aACpF,CAAC;QACJ,CAAC;QAED,gFAAgF;QAChF,IAAI,YAAY,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,qBAAqB,EAAE,CAAC;YACvE,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;YAC7B,OAAO;gBACL,IAAI,EAAE,oBAAoB;gBAC1B,cAAc;gBACd,SAAS;gBACT,OAAO;gBACP,MAAM,EAAE,2FAA2F;aACpG,CAAC;QACJ,CAAC;QAED,mCAAmC;QACnC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,sBAAsB,CAAC;YAC7C,cAAc;YACd,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,kBAAkB;YAC1C,MAAM,EAAE,cAAc;YACtB,QAAQ;SACT,CAAC,IAAI,SAAS,CAAC;QAEhB,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC;gBACzC,cAAc;gBACd,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,kBAAkB;gBAC1C,MAAM,EAAE,GAAG,OAAO,iEAAiE;gBACnF,MAAM,EAAE,cAAc;gBACtB,QAAQ;gBACR,eAAe,EAAE,KAAK;aACvB,CAAC,CAAC;YACH,IAAI,CAAC,YAAY,CAAC,GAAG,SAAS,0EAA0E,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,kBAAkB,GAAG,KAAK,CAAC,uDAAuD,CAAC,CAAC;YACnN,OAAO;gBACL,IAAI,EAAE,oBAAoB;gBAC1B,cAAc;gBACd,SAAS;gBACT,OAAO;gBACP,QAAQ;gBACR,MAAM,EAAE,iCAAiC;aAC1C,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,YAAY,CAAC,GAAG,SAAS,mHAAmH,CAAC,CAAC;YACnJ,OAAO;gBACL,IAAI,EAAE,iBAAiB;gBACvB,cAAc;gBACd,SAAS;gBACT,OAAO;gBACP,MAAM,EAAE,4BAA6B,GAAa,CAAC,OAAO,EAAE;aAC7D,CAAC;QACJ,CAAC;IACH,CAAC;IAEO,YAAY,CAAC,IAAY;QAC/B,IAAI,CAAC,IAAI,CAAC,YAAY;YAAE,OAAO;QAC/B,IAAI,CAAC;YACH,sEAAsE;YACtE,qEAAqE;YACrE,iEAAiE;YACjE,kBAAkB;YAClB,KAAK,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;QAClD,CAAC;QAAC,MAAM,CAAC;YACP,uBAAuB;QACzB,CAAC;IACH,CAAC;CACF;AAED,6DAA6D;AAE7D;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAuB;IAC3D,MAAM,CAAC,GAAG,yBAAyB,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;IAC9D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC;AAC9C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,KAAuB;IACpD,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC;QAAE,OAAO,gBAAgB,CAAC;IACvE,IAAI,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC;QAAE,OAAO,qBAAqB,CAAC;IACvE,OAAO,iBAAiB,CAAC;AAC3B,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,YAAY,CAAC,cAAsB;IACjD,OAAO,cAAc,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;AAChD,CAAC;AAED,yGAAyG;AACzG,MAAM,UAAU,gBAAgB,CAAC,KAAuB,EAAE,cAAsB,EAAE,OAAe;IAC/F,MAAM,YAAY,GAAG,QAAQ,CAAC,cAAc,CAAC,CAAC;IAC9C,MAAM,gBAAgB,GACpB,OAAO,KAAK,gBAAgB;QAC1B,CAAC,CAAC,mFAAmF;QACrF,CAAC,CAAC,OAAO,KAAK,qBAAqB;YACnC,CAAC,CAAC,2GAA2G;YAC7G,CAAC,CAAC,oCAAoC,CAAC;IAC3C,OAAO,CACL,aAAa,YAAY,IAAI,gBAAgB,OAAO;QACpD,GAAG,KAAK,CAAC,MAAM,MAAM;QACrB,GAAG,KAAK,CAAC,MAAM,EAAE,CAClB,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ,CAAC,cAAsB;IACtC,IAAI,cAAc,CAAC,UAAU,CAAC,WAAW,CAAC;QAAE,OAAO,sBAAsB,CAAC;IAC1E,IAAI,cAAc,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAC3C,MAAM,CAAC,GAAG,mBAAmB,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QACnD,OAAO,CAAC,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,iBAAiB,CAAC;IAChE,CAAC;IACD,IAAI,cAAc,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QAC5C,MAAM,CAAC,GAAG,oBAAoB,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QACpD,OAAO,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC;IAC9C,CAAC;IACD,MAAM,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IAC5C,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,cAAc,CAAC;AACtD,CAAC"}
@@ -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"}