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.
- package/dist/monitoring/AttributionResolver.d.ts +46 -0
- package/dist/monitoring/AttributionResolver.d.ts.map +1 -0
- package/dist/monitoring/AttributionResolver.js +107 -0
- package/dist/monitoring/AttributionResolver.js.map +1 -0
- package/dist/monitoring/BurnDetector.d.ts +84 -0
- package/dist/monitoring/BurnDetector.d.ts.map +1 -0
- package/dist/monitoring/BurnDetector.js +169 -0
- package/dist/monitoring/BurnDetector.js.map +1 -0
- package/dist/monitoring/BurnThrottleRunbook.d.ts +97 -0
- package/dist/monitoring/BurnThrottleRunbook.d.ts.map +1 -0
- package/dist/monitoring/BurnThrottleRunbook.js +202 -0
- package/dist/monitoring/BurnThrottleRunbook.js.map +1 -0
- package/dist/monitoring/LlmRateGate.d.ts +133 -33
- package/dist/monitoring/LlmRateGate.d.ts.map +1 -1
- package/dist/monitoring/LlmRateGate.js +179 -46
- package/dist/monitoring/LlmRateGate.js.map +1 -1
- package/dist/monitoring/TokenLedger.d.ts +16 -0
- package/dist/monitoring/TokenLedger.d.ts.map +1 -1
- package/dist/monitoring/TokenLedger.js +28 -0
- package/dist/monitoring/TokenLedger.js.map +1 -1
- package/dist/monitoring/attribution-manifest.d.ts +39 -0
- package/dist/monitoring/attribution-manifest.d.ts.map +1 -0
- package/dist/monitoring/attribution-manifest.js +83 -0
- package/dist/monitoring/attribution-manifest.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/0.28.107.md +40 -0
- package/upgrades/0.28.108.md +50 -0
- package/upgrades/side-effects/token-burn-detection-phase-2.md +44 -0
- package/upgrades/side-effects/token-burn-detection-phase-3.md +59 -0
- 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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* the
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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: '
|
|
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
|
-
*
|
|
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
|
|
47
|
-
*
|
|
48
|
-
*
|
|
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
|
-
*
|
|
54
|
-
*
|
|
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
|
-
|
|
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.
|
|
59
|
-
*
|
|
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;
|
|
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"}
|