loki-mode 7.49.0 → 7.50.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.
- package/README.md +2 -2
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/prd-analyzer.py +215 -1
- package/autonomy/prd-checklist.sh +315 -0
- package/autonomy/run.sh +124 -0
- package/autonomy/spec-interrogation.sh +224 -4
- package/autonomy/spec.sh +25 -16
- package/autonomy/verify.sh +108 -26
- package/dashboard/__init__.py +1 -1
- package/dashboard/audit.py +202 -21
- package/docs/INSTALLATION.md +2 -2
- package/docs/siem-integration.md +102 -0
- package/loki-ts/dist/loki.js +231 -230
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/references/invariant-checks.md +109 -0
- package/src/audit/crosslink.js +413 -0
- package/src/audit/index.js +32 -0
- package/src/observability/siem-export.js +424 -0
- package/src/policies/cost.js +270 -1
package/src/policies/cost.js
CHANGED
|
@@ -3,18 +3,31 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Loki Mode Policy Engine - Cost Control System
|
|
5
5
|
*
|
|
6
|
-
* Per-project token budget tracking with configurable alert thresholds
|
|
6
|
+
* Per-project token budget tracking with configurable alert thresholds, plus
|
|
7
|
+
* organization/tenant-level spend rollup and ceiling enforcement.
|
|
7
8
|
*
|
|
8
9
|
* Features:
|
|
9
10
|
* - Per-project token budget tracking
|
|
10
11
|
* - Alerts at configurable thresholds (default: 50%, 80%, 100%)
|
|
11
12
|
* - Per-agent cost tracking (model type, tokens consumed, duration)
|
|
12
13
|
* - Kill switch: emits shutdown event when budget exceeded
|
|
14
|
+
* - Org/tenant-level rollup: aggregates spend across all projects under an org
|
|
15
|
+
* - Org ceiling: pause or deny when aggregate org spend crosses a hard cap
|
|
13
16
|
* - Cost data persisted to .loki/state/costs.json
|
|
17
|
+
*
|
|
18
|
+
* Org governance is an intelligent default: it is OFF (zero overhead, existing
|
|
19
|
+
* per-run behavior unchanged) unless an org ceiling is configured via an
|
|
20
|
+
* `org_max_tokens` resource policy. When configured, the org context is
|
|
21
|
+
* auto-detected (no required flags) from, in priority order:
|
|
22
|
+
* 1. explicit `org_id` on the resource policy
|
|
23
|
+
* 2. LOKI_ORG_ID / LOKI_TENANT_ID environment variables
|
|
24
|
+
* 3. the git remote owner (org/user) of the project directory
|
|
25
|
+
* 4. the literal 'default'
|
|
14
26
|
*/
|
|
15
27
|
|
|
16
28
|
const fs = require('fs');
|
|
17
29
|
const path = require('path');
|
|
30
|
+
const { execFileSync } = require('child_process');
|
|
18
31
|
const { EventEmitter } = require('events');
|
|
19
32
|
|
|
20
33
|
// -------------------------------------------------------------------
|
|
@@ -35,11 +48,19 @@ class CostController extends EventEmitter {
|
|
|
35
48
|
this._stateFile = path.join(this._projectDir, '.loki', 'state', 'costs.json');
|
|
36
49
|
this._state = this._loadState();
|
|
37
50
|
this._budgetConfig = this._extractBudgetConfig(resourcePolicies || []);
|
|
51
|
+
this._orgConfig = this._extractOrgConfig(resourcePolicies || []);
|
|
38
52
|
this._triggeredAlerts = new Set();
|
|
39
53
|
// Per-project shutdown flags (keyed by projectId or 'global').
|
|
40
54
|
// Using a Set instead of a single boolean ensures each project only
|
|
41
55
|
// emits shutdown once even when multiple projects are tracked.
|
|
42
56
|
this._shutdownEmittedProjects = new Set();
|
|
57
|
+
// Per-org shutdown flags (keyed by orgId). Mirrors the per-project set so
|
|
58
|
+
// each org enforces its ceiling at most once per controller lifetime.
|
|
59
|
+
this._shutdownEmittedOrgs = new Set();
|
|
60
|
+
|
|
61
|
+
// Resolve the org id once. Cheap when org governance is off (we still
|
|
62
|
+
// resolve so rollups are attributed, but ceiling logic is a no-op).
|
|
63
|
+
this._orgId = this._orgConfig ? this._detectOrgId() : null;
|
|
43
64
|
|
|
44
65
|
// Restore previously triggered alerts
|
|
45
66
|
if (this._state.triggeredAlerts) {
|
|
@@ -65,6 +86,7 @@ class CostController extends EventEmitter {
|
|
|
65
86
|
return {
|
|
66
87
|
projects: {},
|
|
67
88
|
agents: {},
|
|
89
|
+
orgs: {},
|
|
68
90
|
totalTokens: 0,
|
|
69
91
|
triggeredAlerts: [],
|
|
70
92
|
history: [],
|
|
@@ -95,6 +117,94 @@ class CostController extends EventEmitter {
|
|
|
95
117
|
return null;
|
|
96
118
|
}
|
|
97
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Extract org/tenant ceiling config from resource policies.
|
|
122
|
+
*
|
|
123
|
+
* An org policy is any resource entry that declares `org_max_tokens`. This is
|
|
124
|
+
* intentionally separate from per-run `max_tokens` so the two ceilings can
|
|
125
|
+
* coexist (a run can be capped per-project AND roll up into an org cap).
|
|
126
|
+
*
|
|
127
|
+
* Returns null when no org ceiling is configured -- org governance stays off
|
|
128
|
+
* and per-run behavior is byte-for-byte unchanged.
|
|
129
|
+
*
|
|
130
|
+
* @param {Array} resourcePolicies
|
|
131
|
+
* @returns {{ maxTokens: number, alerts: Array<number>, onExceed: string,
|
|
132
|
+
* name: string, orgId: (string|null) }|null}
|
|
133
|
+
*/
|
|
134
|
+
_extractOrgConfig(resourcePolicies) {
|
|
135
|
+
for (let i = 0; i < resourcePolicies.length; i++) {
|
|
136
|
+
const p = resourcePolicies[i];
|
|
137
|
+
if (p && typeof p.org_max_tokens === 'number' && p.org_max_tokens > 0) {
|
|
138
|
+
return {
|
|
139
|
+
maxTokens: p.org_max_tokens,
|
|
140
|
+
alerts: p.org_alerts || p.alerts || [50, 80, 100],
|
|
141
|
+
// Org ceilings default to 'pause' (deny new spend) rather than a hard
|
|
142
|
+
// shutdown, so a single project cannot tear down the whole org. Both
|
|
143
|
+
// 'pause' and 'shutdown' deny further spend; 'warn' is advisory only.
|
|
144
|
+
onExceed: p.org_on_exceed || 'pause',
|
|
145
|
+
name: p.name || 'org-budget',
|
|
146
|
+
orgId: typeof p.org_id === 'string' && p.org_id ? p.org_id : null,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Auto-detect the org/tenant id with zero required configuration.
|
|
155
|
+
* Priority: explicit policy org_id > env > git remote owner > 'default'.
|
|
156
|
+
*
|
|
157
|
+
* @returns {string}
|
|
158
|
+
*/
|
|
159
|
+
_detectOrgId() {
|
|
160
|
+
if (this._orgConfig && this._orgConfig.orgId) {
|
|
161
|
+
return this._orgConfig.orgId;
|
|
162
|
+
}
|
|
163
|
+
const envId = process.env.LOKI_ORG_ID || process.env.LOKI_TENANT_ID;
|
|
164
|
+
if (envId && String(envId).trim()) {
|
|
165
|
+
return String(envId).trim();
|
|
166
|
+
}
|
|
167
|
+
const remoteOwner = this._detectGitRemoteOwner();
|
|
168
|
+
if (remoteOwner) {
|
|
169
|
+
return remoteOwner;
|
|
170
|
+
}
|
|
171
|
+
return 'default';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Best-effort extraction of the owner segment from the project's git origin
|
|
176
|
+
* remote (e.g. "acme" from git@github.com:acme/repo.git). Returns null on any
|
|
177
|
+
* failure -- never throws, never blocks cost recording.
|
|
178
|
+
*
|
|
179
|
+
* @returns {string|null}
|
|
180
|
+
*/
|
|
181
|
+
_detectGitRemoteOwner() {
|
|
182
|
+
try {
|
|
183
|
+
const url = execFileSync('git', ['config', '--get', 'remote.origin.url'], {
|
|
184
|
+
cwd: this._projectDir,
|
|
185
|
+
encoding: 'utf8',
|
|
186
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
187
|
+
}).trim();
|
|
188
|
+
if (!url) return null;
|
|
189
|
+
// Strip protocol/host and trailing .git, then take the owner segment.
|
|
190
|
+
// Handles: git@host:owner/repo.git, https://host/owner/repo(.git), ssh://...
|
|
191
|
+
let p = url.replace(/\.git$/, '');
|
|
192
|
+
const scp = p.match(/^[^@]+@[^:]+:(.+)$/); // scp-like syntax
|
|
193
|
+
if (scp) {
|
|
194
|
+
p = scp[1];
|
|
195
|
+
} else {
|
|
196
|
+
p = p.replace(/^[a-z]+:\/\/[^/]+\//i, '');
|
|
197
|
+
}
|
|
198
|
+
const segments = p.split('/').filter(Boolean);
|
|
199
|
+
if (segments.length >= 2) {
|
|
200
|
+
return segments[segments.length - 2];
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
} catch (_) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
98
208
|
// -----------------------------------------------------------------
|
|
99
209
|
// Token recording
|
|
100
210
|
// -----------------------------------------------------------------
|
|
@@ -133,9 +243,27 @@ class CostController extends EventEmitter {
|
|
|
133
243
|
// Update global total
|
|
134
244
|
this._state.totalTokens += tokenCount;
|
|
135
245
|
|
|
246
|
+
// Org/tenant rollup (only when an org ceiling is configured). The rollup
|
|
247
|
+
// aggregates this project's spend into the org's running total and records
|
|
248
|
+
// which projects contribute, so operators can see cross-project spend.
|
|
249
|
+
if (this._orgConfig) {
|
|
250
|
+
if (!this._state.orgs) this._state.orgs = {};
|
|
251
|
+
const orgId = this._orgId || 'default';
|
|
252
|
+
if (!this._state.orgs[orgId]) {
|
|
253
|
+
this._state.orgs[orgId] = { totalTokens: 0, projects: {} };
|
|
254
|
+
}
|
|
255
|
+
const org = this._state.orgs[orgId];
|
|
256
|
+
org.totalTokens += tokenCount;
|
|
257
|
+
const pid = projectId || 'unknown';
|
|
258
|
+
org.projects[pid] = (org.projects[pid] || 0) + tokenCount;
|
|
259
|
+
}
|
|
260
|
+
|
|
136
261
|
// Check alerts and budget
|
|
137
262
|
this._checkAlerts(projectId);
|
|
138
263
|
|
|
264
|
+
// Check org ceiling (no-op when org governance is off)
|
|
265
|
+
this._checkOrgCeiling();
|
|
266
|
+
|
|
139
267
|
this._saveState();
|
|
140
268
|
}
|
|
141
269
|
|
|
@@ -183,6 +311,131 @@ class CostController extends EventEmitter {
|
|
|
183
311
|
return { remaining, percentage, alerts, exceeded };
|
|
184
312
|
}
|
|
185
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Check the aggregate org/tenant budget status.
|
|
316
|
+
*
|
|
317
|
+
* Rolls up spend across every project attributed to the org and compares it
|
|
318
|
+
* against the configured org ceiling. When no org ceiling is configured this
|
|
319
|
+
* returns an unlimited, never-exceeded result so callers can treat the org
|
|
320
|
+
* gate as transparently absent.
|
|
321
|
+
*
|
|
322
|
+
* @param {string} [orgId] - Org identifier (defaults to the auto-detected org)
|
|
323
|
+
* @returns {{ orgId: (string|null), consumed: number, remaining: number,
|
|
324
|
+
* percentage: number, alerts: Array, exceeded: boolean,
|
|
325
|
+
* decision: string }}
|
|
326
|
+
*/
|
|
327
|
+
checkOrgBudget(orgId) {
|
|
328
|
+
if (!this._orgConfig) {
|
|
329
|
+
return {
|
|
330
|
+
orgId: null,
|
|
331
|
+
consumed: 0,
|
|
332
|
+
remaining: Infinity,
|
|
333
|
+
percentage: 0,
|
|
334
|
+
alerts: [],
|
|
335
|
+
exceeded: false,
|
|
336
|
+
decision: 'allow',
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const id = orgId || this._orgId || 'default';
|
|
341
|
+
const org = this._state.orgs && this._state.orgs[id];
|
|
342
|
+
const consumed = org ? org.totalTokens : 0;
|
|
343
|
+
|
|
344
|
+
const max = this._orgConfig.maxTokens;
|
|
345
|
+
const percentage = max > 0 ? Math.round((consumed / max) * 100) : 0;
|
|
346
|
+
const remaining = Math.max(0, max - consumed);
|
|
347
|
+
const exceeded = consumed >= max;
|
|
348
|
+
|
|
349
|
+
const alerts = [];
|
|
350
|
+
const thresholds = this._orgConfig.alerts;
|
|
351
|
+
for (let i = 0; i < thresholds.length; i++) {
|
|
352
|
+
if (percentage >= thresholds[i]) {
|
|
353
|
+
alerts.push({
|
|
354
|
+
threshold: thresholds[i],
|
|
355
|
+
message: 'Org token usage at ' + percentage + '% (threshold: ' + thresholds[i] + '%)',
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Decision: 'warn' never blocks; 'pause'/'shutdown' deny once exceeded.
|
|
361
|
+
let decision = 'allow';
|
|
362
|
+
if (exceeded && this._orgConfig.onExceed !== 'warn') {
|
|
363
|
+
decision = this._orgConfig.onExceed === 'shutdown' ? 'deny' : 'pause';
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return { orgId: id, consumed, remaining, percentage, alerts, exceeded, decision };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
_checkOrgCeiling() {
|
|
370
|
+
if (!this._orgConfig) return;
|
|
371
|
+
|
|
372
|
+
const orgId = this._orgId || 'default';
|
|
373
|
+
const budget = this.checkOrgBudget(orgId);
|
|
374
|
+
|
|
375
|
+
// Emit alert events for newly triggered org thresholds (deduped per org).
|
|
376
|
+
const thresholds = this._orgConfig.alerts;
|
|
377
|
+
for (let i = 0; i < thresholds.length; i++) {
|
|
378
|
+
const key = 'org:' + orgId + ':' + thresholds[i];
|
|
379
|
+
if (budget.percentage >= thresholds[i] && !this._triggeredAlerts.has(key)) {
|
|
380
|
+
this._triggeredAlerts.add(key);
|
|
381
|
+
|
|
382
|
+
this._pushHistory({
|
|
383
|
+
type: 'org_alert',
|
|
384
|
+
threshold: thresholds[i],
|
|
385
|
+
percentage: budget.percentage,
|
|
386
|
+
orgId: orgId,
|
|
387
|
+
consumed: budget.consumed,
|
|
388
|
+
max: this._orgConfig.maxTokens,
|
|
389
|
+
timestamp: new Date().toISOString(),
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
this.emit('org_alert', {
|
|
393
|
+
threshold: thresholds[i],
|
|
394
|
+
percentage: budget.percentage,
|
|
395
|
+
orgId: orgId,
|
|
396
|
+
remaining: budget.remaining,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Ceiling enforcement: pause/deny when the org aggregate is exceeded.
|
|
402
|
+
// 'warn' is advisory and never blocks. Each org enforces at most once.
|
|
403
|
+
if (budget.exceeded
|
|
404
|
+
&& this._orgConfig.onExceed !== 'warn'
|
|
405
|
+
&& !this._shutdownEmittedOrgs.has(orgId)) {
|
|
406
|
+
this._shutdownEmittedOrgs.add(orgId);
|
|
407
|
+
|
|
408
|
+
this._pushHistory({
|
|
409
|
+
type: 'org_ceiling_exceeded',
|
|
410
|
+
decision: budget.decision,
|
|
411
|
+
reason: 'Org budget exceeded',
|
|
412
|
+
percentage: budget.percentage,
|
|
413
|
+
orgId: orgId,
|
|
414
|
+
consumed: budget.consumed,
|
|
415
|
+
max: this._orgConfig.maxTokens,
|
|
416
|
+
timestamp: new Date().toISOString(),
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
this._saveState();
|
|
420
|
+
this.emit('org_ceiling', {
|
|
421
|
+
reason: 'Org token budget exceeded',
|
|
422
|
+
decision: budget.decision,
|
|
423
|
+
orgId: orgId,
|
|
424
|
+
percentage: budget.percentage,
|
|
425
|
+
consumed: budget.consumed,
|
|
426
|
+
max: this._orgConfig.maxTokens,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** Append a history record with bounded growth. */
|
|
432
|
+
_pushHistory(record) {
|
|
433
|
+
if (this._state.history.length > MAX_STATE_ENTRIES) {
|
|
434
|
+
this._state.history.splice(0, this._state.history.length - MAX_STATE_ENTRIES);
|
|
435
|
+
}
|
|
436
|
+
this._state.history.push(record);
|
|
437
|
+
}
|
|
438
|
+
|
|
186
439
|
_checkAlerts(projectId) {
|
|
187
440
|
if (!this._budgetConfig) return;
|
|
188
441
|
|
|
@@ -265,6 +518,20 @@ class CostController extends EventEmitter {
|
|
|
265
518
|
return Object.assign({}, this._state.projects);
|
|
266
519
|
}
|
|
267
520
|
|
|
521
|
+
/**
|
|
522
|
+
* Get the org/tenant rollup report.
|
|
523
|
+
*
|
|
524
|
+
* @param {string} [orgId] - When given, returns that org's rollup (or null);
|
|
525
|
+
* otherwise returns a copy of all org rollups.
|
|
526
|
+
*/
|
|
527
|
+
getOrgReport(orgId) {
|
|
528
|
+
const orgs = this._state.orgs || {};
|
|
529
|
+
if (orgId) {
|
|
530
|
+
return orgs[orgId] || null;
|
|
531
|
+
}
|
|
532
|
+
return Object.assign({}, orgs);
|
|
533
|
+
}
|
|
534
|
+
|
|
268
535
|
/**
|
|
269
536
|
* Get the history of alerts and shutdown events.
|
|
270
537
|
*/
|
|
@@ -279,12 +546,14 @@ class CostController extends EventEmitter {
|
|
|
279
546
|
this._state = {
|
|
280
547
|
projects: {},
|
|
281
548
|
agents: {},
|
|
549
|
+
orgs: {},
|
|
282
550
|
totalTokens: 0,
|
|
283
551
|
triggeredAlerts: [],
|
|
284
552
|
history: [],
|
|
285
553
|
};
|
|
286
554
|
this._triggeredAlerts.clear();
|
|
287
555
|
this._shutdownEmittedProjects.clear();
|
|
556
|
+
this._shutdownEmittedOrgs.clear();
|
|
288
557
|
this._saveState();
|
|
289
558
|
}
|
|
290
559
|
}
|