knosky 0.6.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +149 -93
  2. package/CREDITS.md +14 -14
  3. package/LICENSE.md +76 -76
  4. package/LIMITATIONS.md +33 -23
  5. package/PRIVACY.md +30 -30
  6. package/README.md +170 -117
  7. package/SECURITY.md +78 -46
  8. package/action/post-comment.mjs +94 -89
  9. package/action.yml +62 -62
  10. package/bin/knosky.mjs +279 -105
  11. package/core/CONTRACT.md +70 -70
  12. package/core/append-only-checkpoint.mjs +215 -0
  13. package/core/audit-writer.mjs +317 -0
  14. package/core/benchmark-results.mjs +225 -225
  15. package/core/bundle.mjs +178 -178
  16. package/core/churn.mjs +23 -23
  17. package/core/ci.mjs +268 -268
  18. package/core/comparison.mjs +189 -189
  19. package/core/config.mjs +189 -189
  20. package/core/constants.mjs +13 -13
  21. package/core/contract.mjs +123 -123
  22. package/core/cross-repo.mjs +111 -111
  23. package/core/decision-codes.mjs +92 -0
  24. package/core/destination.mjs +161 -161
  25. package/core/district-classification.mjs +111 -0
  26. package/core/doctor-scorecard.mjs +369 -0
  27. package/core/domain-store.mjs +347 -0
  28. package/core/edges.mjs +43 -43
  29. package/core/escalate.mjs +68 -68
  30. package/core/freshness.mjs +198 -194
  31. package/core/fs-indexer.mjs +218 -218
  32. package/core/key-store.mjs +348 -348
  33. package/core/layout.mjs +46 -46
  34. package/core/ledger.mjs +176 -141
  35. package/core/local-ipc-identity.mjs +500 -0
  36. package/core/lod.mjs +155 -155
  37. package/core/mode-b.mjs +410 -0
  38. package/core/multi-model-benchmark.mjs +405 -405
  39. package/core/net-lockdown.mjs +421 -0
  40. package/core/onboarding.mjs +223 -223
  41. package/core/operator-auth.mjs +317 -0
  42. package/core/overlays.mjs +45 -45
  43. package/core/policy-lattice.mjs +142 -0
  44. package/core/pr-comment.mjs +198 -198
  45. package/core/protocol-spec.mjs +460 -460
  46. package/core/provenance.mjs +320 -0
  47. package/core/retrieve.mjs +63 -63
  48. package/core/route.mjs +304 -304
  49. package/core/schema.mjs +275 -275
  50. package/core/signing-tiers.mjs +1265 -0
  51. package/core/swarm-bench.mjs +106 -0
  52. package/core/swarm-coordinator.mjs +867 -0
  53. package/core/trust-root-rekey.mjs +410 -0
  54. package/mcp/server.mjs +264 -108
  55. package/package.json +56 -46
  56. package/renderer/art/kenney/buildingTiles_sheet.xml +130 -130
  57. package/renderer/art/kenney/cityDetails_sheet.xml +12 -12
  58. package/renderer/art/kenney/landscapeTiles_sheet.xml +129 -129
  59. package/renderer/art/kenney/sheet_allCars.xml +545 -545
  60. package/renderer/build-rich.mjs +43 -43
  61. package/renderer/city.template.html +808 -808
  62. package/ssot/decision-codes.json +133 -0
  63. package/ssot/ladder-l0-l3.md +232 -0
  64. package/ssot/tool-menu.json +130 -0
@@ -0,0 +1,867 @@
1
+ // KnoSky L3 swarm coordinator floor (DEC-113) — thin local control plane.
2
+ // Built on Mode B domain leases (domain-store) + audit-writer multi-agent events.
3
+ // Pure ESM, Node stdlib. Windows-safe atomic writes (no mandatory fsync).
4
+
5
+ import {
6
+ existsSync,
7
+ mkdirSync,
8
+ readFileSync,
9
+ writeFileSync,
10
+ renameSync,
11
+ } from 'node:fs';
12
+ import { dirname, join } from 'node:path';
13
+ import { randomUUID } from 'node:crypto';
14
+ import {
15
+ loadDomain,
16
+ resolveDomainRoot,
17
+ registerAgentWithLease,
18
+ } from './domain-store.mjs';
19
+ import { writeDecisionReceipt } from './audit-writer.mjs';
20
+ import { CODES } from './decision-codes.mjs';
21
+ import {
22
+ assertOperator,
23
+ assertOperatorQuorum,
24
+ operatorCount,
25
+ sanitizeClasses,
26
+ ELEVATED,
27
+ } from './operator-auth.mjs';
28
+ import { resolveLeaseIdentity } from './local-ipc-identity.mjs';
29
+
30
+ /** Default climate knobs (simple counter model). */
31
+ export const DEFAULT_SWARM_QUOTAS = Object.freeze({
32
+ maxClaimsPerAgent: 8,
33
+ maxActiveLeasesPerAgent: 4,
34
+ maxActionsPerWindow: 32,
35
+ windowMs: 60_000,
36
+ antiProbeDenyThreshold: 12,
37
+ antiProbeWindowMs: 10_000,
38
+ enableFifoWait: true,
39
+ });
40
+
41
+ /**
42
+ * Soft atomic JSON write — no fsync (Windows EPERM-safe).
43
+ * @param {string} path
44
+ * @param {object} obj
45
+ */
46
+ function atomicWriteSoft(path, obj) {
47
+ mkdirSync(dirname(path), { recursive: true });
48
+ const tmp = path + '.tmp';
49
+ writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', 'utf8');
50
+ renameSync(tmp, path);
51
+ }
52
+
53
+ /**
54
+ * @param {string} domainRoot
55
+ */
56
+ export function swarmPaths(domainRoot) {
57
+ const swarmDir = join(domainRoot, 'swarm');
58
+ return {
59
+ swarmDir,
60
+ heatmapPath: join(swarmDir, 'heatmap.json'),
61
+ claimsPath: join(swarmDir, 'claims.json'),
62
+ };
63
+ }
64
+
65
+ function nowIso() {
66
+ return new Date().toISOString();
67
+ }
68
+
69
+ function mintId(prefix) {
70
+ return `${prefix}_${randomUUID().replace(/-/g, '').slice(0, 16)}`;
71
+ }
72
+
73
+ function pruneWindow(tsArr, windowMs, now = Date.now()) {
74
+ const cut = now - windowMs;
75
+ return tsArr.filter((t) => t >= cut);
76
+ }
77
+
78
+ /**
79
+ * Create a local L3 swarm coordinator bound to a trust domain.
80
+ *
81
+ * @param {object} [opts]
82
+ * @param {string} [opts.domainRoot]
83
+ * @param {string} [opts.cityPath]
84
+ * @param {ReturnType<typeof loadDomain>} [opts.domain]
85
+ * @param {Partial<typeof DEFAULT_SWARM_QUOTAS>} [opts.quotas]
86
+ */
87
+ export function createSwarmCoordinator(opts = {}) {
88
+ const domainRoot = resolveDomainRoot(opts.cityPath, opts.domainRoot);
89
+ const domain = opts.domain || loadDomain(domainRoot);
90
+ const paths = swarmPaths(domainRoot);
91
+ mkdirSync(paths.swarmDir, { recursive: true });
92
+
93
+ const quotas = { ...DEFAULT_SWARM_QUOTAS, ...(opts.quotas || {}) };
94
+
95
+ /** @type {Map<string, { claimId: string, agentId: string, kind: string, resource: string, since: string }>} */
96
+ const claims = new Map();
97
+ /** FIFO waiters per resource key */
98
+ /** @type {Map<string, string[]>} */
99
+ const waiters = new Map();
100
+ /** Per-agent counters / sliding windows */
101
+ /** @type {Map<string, { actionTs: number[], denyTs: number[], probeTrips: number }>} */
102
+ const agentStats = new Map();
103
+
104
+ // Restore durable claims if present (best-effort).
105
+ if (existsSync(paths.claimsPath)) {
106
+ try {
107
+ const doc = JSON.parse(readFileSync(paths.claimsPath, 'utf8'));
108
+ for (const [key, rec] of Object.entries(doc.claims || {})) {
109
+ if (rec && rec.agentId && rec.resource) claims.set(key, rec);
110
+ }
111
+ } catch {
112
+ /* ignore corrupt; rebuild in memory */
113
+ }
114
+ }
115
+
116
+ function statsFor(agentId) {
117
+ if (!agentStats.has(agentId)) {
118
+ agentStats.set(agentId, { actionTs: [], denyTs: [], probeTrips: 0 });
119
+ }
120
+ return agentStats.get(agentId);
121
+ }
122
+
123
+ function claimCount(agentId) {
124
+ let n = 0;
125
+ for (const c of claims.values()) if (c.agentId === agentId) n += 1;
126
+ return n;
127
+ }
128
+
129
+ function activeLeaseCount(agentId) {
130
+ let n = 0;
131
+ for (const rec of domain.leaseStore.values()) {
132
+ if (rec.agentId === agentId && rec.status === 'active') {
133
+ if (rec.expires_at && Date.parse(rec.expires_at) <= Date.now()) continue;
134
+ n += 1;
135
+ }
136
+ }
137
+ return n;
138
+ }
139
+
140
+ function persistClaims() {
141
+ const out = {};
142
+ for (const [k, v] of claims.entries()) out[k] = v;
143
+ atomicWriteSoft(paths.claimsPath, { v: 1, claims: out, updated_at: nowIso() });
144
+ }
145
+
146
+ function emit(decision_code, agentId, tool, meta = {}) {
147
+ return writeDecisionReceipt({
148
+ domainRoot,
149
+ decision_code,
150
+ agent_id: agentId,
151
+ tool,
152
+ mode: 'B',
153
+ destination: null,
154
+ meta: {
155
+ swarm: true,
156
+ layer: 'L3',
157
+ ...meta,
158
+ },
159
+ });
160
+ }
161
+
162
+ function noteAction(agentId) {
163
+ const s = statsFor(agentId);
164
+ const t = Date.now();
165
+ s.actionTs = pruneWindow(s.actionTs.concat(t), quotas.windowMs, t);
166
+ }
167
+
168
+ function noteDeny(agentId, reason) {
169
+ const s = statsFor(agentId);
170
+ const t = Date.now();
171
+ s.denyTs = pruneWindow(s.denyTs.concat(t), quotas.antiProbeWindowMs, t);
172
+ s.actionTs = pruneWindow(s.actionTs.concat(t), quotas.windowMs, t);
173
+ let probe = false;
174
+ if (s.denyTs.length >= quotas.antiProbeDenyThreshold) {
175
+ s.probeTrips += 1;
176
+ probe = true;
177
+ // Clear window so each trip is distinct
178
+ s.denyTs = [];
179
+ }
180
+ return { probe, reason };
181
+ }
182
+
183
+ function backpressureHit(agentId) {
184
+ const s = statsFor(agentId);
185
+ const t = Date.now();
186
+ s.actionTs = pruneWindow(s.actionTs, quotas.windowMs, t);
187
+ return s.actionTs.length >= quotas.maxActionsPerWindow;
188
+ }
189
+
190
+ function resourceKey(kind, resource) {
191
+ return `${kind}:${resource}`;
192
+ }
193
+
194
+ /**
195
+ * Ensure agent is registered in the domain store (idempotent on same id).
196
+ * Elevated classes require TWO distinct operator tokens (same Rule 3 as domain-store).
197
+ * @param {{ agentId: string, role?: string, classes?: string[], operatorToken?: string, operatorToken2?: string }} agent
198
+ */
199
+ function registerAgent(agent) {
200
+ const agentId = agent.agentId;
201
+ if (!agentId || typeof agentId !== 'string') {
202
+ return { ok: false, code: CODES.ERROR_INVALID_INPUT, reason: 'agentId_required' };
203
+ }
204
+
205
+ if (!domain.agents[agentId]) {
206
+ const issued = registerAgentWithLease(
207
+ domain,
208
+ {
209
+ agentId,
210
+ role: agent.role || 'coder',
211
+ classes: Array.isArray(agent.classes) ? agent.classes : ['public', 'internal'],
212
+ },
213
+ {
214
+ operatorToken: agent.operatorToken || process.env.KC_OPERATOR_TOKEN,
215
+ operatorToken2: agent.operatorToken2 || process.env.KC_OPERATOR_TOKEN_2,
216
+ },
217
+ );
218
+ if (!issued.ok) {
219
+ return {
220
+ ok: false,
221
+ code: CODES.DENY_IDENTITY,
222
+ reason: issued.reason,
223
+ next_action: issued.next_action,
224
+ };
225
+ }
226
+ // Reload after registerAgentWithLease mutated disk
227
+ const fresh = loadDomain(domainRoot);
228
+ domain.agents = fresh.agents;
229
+ domain.policy = fresh.policy;
230
+ domain.leaseStore = fresh.leaseStore;
231
+ }
232
+ emit(CODES.ALLOW, agentId, 'swarm_register', { event: 'agent_register' });
233
+ noteAction(agentId);
234
+ return { ok: true, code: CODES.ALLOW, agentId, agent: domain.agents[agentId] };
235
+ }
236
+
237
+ /**
238
+ * Issue a fresh active lease for an agent (wraps domain-store).
239
+ * Registers agent if missing (subject to operator gates / quorum).
240
+ * @param {{ agentId: string, role?: string, classes?: string[], ttlMs?: number|null, operatorToken?: string, operatorToken2?: string }} opts
241
+ */
242
+ function issueLease(optsIn = {}) {
243
+ const agentId = optsIn.agentId;
244
+ if (!agentId || typeof agentId !== 'string') {
245
+ return { ok: false, code: CODES.ERROR_INVALID_INPUT, reason: 'agentId_required' };
246
+ }
247
+
248
+ if (backpressureHit(agentId)) {
249
+ const d = noteDeny(agentId, 'backpressure');
250
+ const rc = emit(CODES.DENY, agentId, 'swarm_lease_issue', {
251
+ event: 'lease_issue_denied',
252
+ reason: 'backpressure',
253
+ probe: d.probe,
254
+ });
255
+ return {
256
+ ok: false,
257
+ code: CODES.DENY,
258
+ reason: 'backpressure',
259
+ backpressure: true,
260
+ probe: d.probe,
261
+ receipt_id: rc.ok ? rc.receipt_id : undefined,
262
+ };
263
+ }
264
+
265
+ if (activeLeaseCount(agentId) >= quotas.maxActiveLeasesPerAgent) {
266
+ const d = noteDeny(agentId, 'quota_leases');
267
+ const rc = emit(CODES.DENY, agentId, 'swarm_lease_issue', {
268
+ event: 'lease_issue_denied',
269
+ reason: 'quota_leases',
270
+ probe: d.probe,
271
+ });
272
+ return {
273
+ ok: false,
274
+ code: CODES.DENY,
275
+ reason: 'quota_leases',
276
+ backpressure: true,
277
+ probe: d.probe,
278
+ receipt_id: rc.ok ? rc.receipt_id : undefined,
279
+ };
280
+ }
281
+
282
+ // Always mint via domain-store for mode-B compatible records.
283
+ // registerAgentWithLease overwrites agent classes — preserve existing if present.
284
+ const existing = domain.agents[agentId];
285
+ const classes = optsIn.classes || existing?.classes || ['public', 'internal'];
286
+ const role = optsIn.role || existing?.role || 'coder';
287
+ const operatorToken = optsIn.operatorToken || process.env.KC_OPERATOR_TOKEN;
288
+ const operatorToken2 = optsIn.operatorToken2 || process.env.KC_OPERATOR_TOKEN_2;
289
+
290
+ // If agent already registered, mint lease without clobbering registry fields hard:
291
+ let leaseId;
292
+ if (existing) {
293
+ leaseId = mintId('lease');
294
+ const ttlMs = optsIn.ttlMs == null ? null : Number(optsIn.ttlMs);
295
+ const rec = {
296
+ leaseId,
297
+ agentId,
298
+ status: 'active',
299
+ created_at: nowIso(),
300
+ expires_at:
301
+ ttlMs && Number.isFinite(ttlMs) && ttlMs > 0
302
+ ? new Date(Date.now() + ttlMs).toISOString()
303
+ : null,
304
+ };
305
+ domain.leaseStore.set(leaseId, rec);
306
+ domain.saveLeases();
307
+ } else {
308
+ const issued = registerAgentWithLease(
309
+ domain,
310
+ { agentId, classes, role },
311
+ { operatorToken, operatorToken2 },
312
+ );
313
+ if (!issued.ok) {
314
+ return {
315
+ ok: false,
316
+ code: CODES.DENY_IDENTITY,
317
+ reason: issued.reason,
318
+ next_action: issued.next_action,
319
+ };
320
+ }
321
+ leaseId = issued.leaseId;
322
+ if (optsIn.ttlMs && Number.isFinite(optsIn.ttlMs) && optsIn.ttlMs > 0) {
323
+ const rec = domain.leaseStore.get(leaseId);
324
+ if (rec) {
325
+ rec.expires_at = new Date(Date.now() + optsIn.ttlMs).toISOString();
326
+ domain.leaseStore.set(leaseId, rec);
327
+ domain.saveLeases();
328
+ }
329
+ }
330
+ }
331
+
332
+ noteAction(agentId);
333
+ const rc = emit(CODES.ALLOW, agentId, 'swarm_lease_issue', {
334
+ event: 'lease_issued',
335
+ lease_id: leaseId,
336
+ });
337
+ writeHeatmap();
338
+ return {
339
+ ok: true,
340
+ code: CODES.ALLOW,
341
+ agentId,
342
+ leaseId,
343
+ receipt_id: rc.ok ? rc.receipt_id : undefined,
344
+ };
345
+ }
346
+
347
+ /**
348
+ * Expire a lease (TTL / sweep). Self-service expiration for system clock only
349
+ * via expireDueLeases; manual expire requires operator OR the holder agentId.
350
+ * @param {string} leaseId
351
+ * @param {{ operatorToken?: string, callerAgentId?: string, system?: boolean }} [auth]
352
+ */
353
+ function expireLease(leaseId, auth = {}) {
354
+ const lease = domain.leaseStore.get(leaseId);
355
+ if (!lease) {
356
+ return { ok: false, code: CODES.DENY_IDENTITY, reason: 'unknown_lease_id' };
357
+ }
358
+ if (lease.status !== 'active') {
359
+ return { ok: true, code: CODES.ALLOW, leaseId, status: lease.status, noop: true };
360
+ }
361
+ if (!auth.system) {
362
+ const gate = authorizeLeaseAdmin(lease, auth);
363
+ if (!gate.ok) return gate;
364
+ }
365
+ lease.status = 'expired';
366
+ lease.expired_at = nowIso();
367
+ domain.leaseStore.set(leaseId, lease);
368
+ domain.saveLeases();
369
+ const rc = emit(CODES.ALLOW, lease.agentId, 'swarm_lease_expire', {
370
+ event: 'lease_expired',
371
+ lease_id: leaseId,
372
+ });
373
+ writeHeatmap();
374
+ return {
375
+ ok: true,
376
+ code: CODES.ALLOW,
377
+ leaseId,
378
+ agentId: lease.agentId,
379
+ status: 'expired',
380
+ receipt_id: rc.ok ? rc.receipt_id : undefined,
381
+ };
382
+ }
383
+
384
+ /**
385
+ * Revoke a lease (security / operator OR holder self-revoke).
386
+ * Foreign revocation requires operatorToken — never unilateral third-party.
387
+ * @param {string} leaseId
388
+ * @param {{ reason?: string, operatorToken?: string, callerAgentId?: string }} [meta]
389
+ */
390
+ function revokeLease(leaseId, meta = {}) {
391
+ const lease = domain.leaseStore.get(leaseId);
392
+ if (!lease) {
393
+ return { ok: false, code: CODES.DENY_IDENTITY, reason: 'unknown_lease_id' };
394
+ }
395
+ const gate = authorizeLeaseAdmin(lease, meta);
396
+ if (!gate.ok) return gate;
397
+
398
+ lease.status = 'revoked';
399
+ lease.revoked_at = nowIso();
400
+ lease.revoke_reason = meta.reason || (gate.mode === 'holder' ? 'self_revoke' : 'operator');
401
+ lease.revoked_by = gate.mode === 'operator' ? gate.operatorId : lease.agentId;
402
+ domain.leaseStore.set(leaseId, lease);
403
+ domain.saveLeases();
404
+ const rc = emit(CODES.ALLOW, lease.agentId, 'swarm_lease_revoke', {
405
+ event: 'lease_revoked',
406
+ lease_id: leaseId,
407
+ reason: lease.revoke_reason,
408
+ by: lease.revoked_by,
409
+ });
410
+ writeHeatmap();
411
+ return {
412
+ ok: true,
413
+ code: CODES.ALLOW,
414
+ leaseId,
415
+ agentId: lease.agentId,
416
+ status: 'revoked',
417
+ receipt_id: rc.ok ? rc.receipt_id : undefined,
418
+ };
419
+ }
420
+
421
+ /** @param {any} lease @param {{ operatorToken?: string, callerAgentId?: string }} auth */
422
+ function authorizeLeaseAdmin(lease, auth = {}) {
423
+ const token = auth.operatorToken || process.env.KC_OPERATOR_TOKEN;
424
+ const op = assertOperator(domainRoot, token);
425
+ if (op.ok) {
426
+ return { ok: true, mode: 'operator', operatorId: op.operatorId };
427
+ }
428
+ // Holder may expire/revoke own lease (self only).
429
+ if (auth.callerAgentId && auth.callerAgentId === lease.agentId) {
430
+ return { ok: true, mode: 'holder' };
431
+ }
432
+ return {
433
+ ok: false,
434
+ code: CODES.DENY_IDENTITY,
435
+ reason: 'operator_or_holder_required',
436
+ next_action: 'Provide operatorToken or callerAgentId matching the lease holder',
437
+ };
438
+ }
439
+
440
+ /**
441
+ * Sweep leases past expires_at → expired (system; no operator needed).
442
+ */
443
+ function expireDueLeases() {
444
+ const out = [];
445
+ const t = Date.now();
446
+ for (const [id, rec] of domain.leaseStore.entries()) {
447
+ if (rec.status !== 'active') continue;
448
+ if (rec.expires_at && Date.parse(rec.expires_at) <= t) {
449
+ const r = expireLease(id, { system: true });
450
+ out.push(r);
451
+ }
452
+ }
453
+ return out;
454
+ }
455
+
456
+ /**
457
+ * File or district claim with conflict deny + optional FIFO wait.
458
+ * leaseId is REQUIRED (Mode B / L3 lease-bound identity).
459
+ * @param {{ agentId: string, leaseId: string, kind?: 'file'|'district', resource: string, enqueue?: boolean }} req
460
+ */
461
+ function claim(req = {}) {
462
+ const agentId = req.agentId;
463
+ const kind = req.kind === 'district' ? 'district' : 'file';
464
+ const resource = typeof req.resource === 'string' ? req.resource.trim() : '';
465
+ if (!agentId || !resource) {
466
+ return { ok: false, code: CODES.ERROR_INVALID_INPUT, reason: 'agentId_and_resource_required' };
467
+ }
468
+ if (!req.leaseId || typeof req.leaseId !== 'string') {
469
+ return {
470
+ ok: false,
471
+ code: CODES.DENY_IDENTITY,
472
+ reason: 'lease_required',
473
+ next_action: 'Pass leaseId from knosky agent-register / issueLease',
474
+ };
475
+ }
476
+
477
+ expireDueLeases();
478
+
479
+ // Required lease bind check (anti-spoof)
480
+ {
481
+ const lease = domain.leaseStore.get(req.leaseId);
482
+ if (!lease || lease.status !== 'active' || lease.agentId !== agentId) {
483
+ const d = noteDeny(agentId, 'bad_lease');
484
+ const rc = emit(CODES.DENY_IDENTITY, agentId, 'swarm_claim', {
485
+ event: 'claim_denied',
486
+ reason: 'bad_lease',
487
+ kind,
488
+ resource: resource.slice(0, 200),
489
+ probe: d.probe,
490
+ });
491
+ writeHeatmap();
492
+ return {
493
+ ok: false,
494
+ code: CODES.DENY_IDENTITY,
495
+ reason: 'bad_lease',
496
+ probe: d.probe,
497
+ receipt_id: rc.ok ? rc.receipt_id : undefined,
498
+ };
499
+ }
500
+ if (lease.expires_at && Date.parse(lease.expires_at) <= Date.now()) {
501
+ expireLease(req.leaseId, { system: true });
502
+ const d = noteDeny(agentId, 'lease_expired');
503
+ const rc = emit(CODES.DENY_IDENTITY, agentId, 'swarm_claim', {
504
+ event: 'claim_denied',
505
+ reason: 'lease_expired',
506
+ kind,
507
+ resource: resource.slice(0, 200),
508
+ probe: d.probe,
509
+ });
510
+ writeHeatmap();
511
+ return {
512
+ ok: false,
513
+ code: CODES.DENY_IDENTITY,
514
+ reason: 'lease_expired',
515
+ probe: d.probe,
516
+ receipt_id: rc.ok ? rc.receipt_id : undefined,
517
+ };
518
+ }
519
+ }
520
+
521
+ if (backpressureHit(agentId)) {
522
+ const d = noteDeny(agentId, 'backpressure');
523
+ const rc = emit(CODES.DENY, agentId, 'swarm_claim', {
524
+ event: 'claim_denied',
525
+ reason: 'backpressure',
526
+ kind,
527
+ resource: resource.slice(0, 200),
528
+ probe: d.probe,
529
+ });
530
+ writeHeatmap();
531
+ return {
532
+ ok: false,
533
+ code: CODES.DENY,
534
+ reason: 'backpressure',
535
+ backpressure: true,
536
+ probe: d.probe,
537
+ receipt_id: rc.ok ? rc.receipt_id : undefined,
538
+ };
539
+ }
540
+
541
+ if (claimCount(agentId) >= quotas.maxClaimsPerAgent) {
542
+ const d = noteDeny(agentId, 'quota_claims');
543
+ const rc = emit(CODES.DENY, agentId, 'swarm_claim', {
544
+ event: 'claim_denied',
545
+ reason: 'quota_claims',
546
+ kind,
547
+ resource: resource.slice(0, 200),
548
+ probe: d.probe,
549
+ });
550
+ writeHeatmap();
551
+ return {
552
+ ok: false,
553
+ code: CODES.DENY,
554
+ reason: 'quota_claims',
555
+ backpressure: true,
556
+ probe: d.probe,
557
+ receipt_id: rc.ok ? rc.receipt_id : undefined,
558
+ };
559
+ }
560
+
561
+ const key = resourceKey(kind, resource);
562
+ const held = claims.get(key);
563
+ if (held && held.agentId !== agentId) {
564
+ const d = noteDeny(agentId, 'claim_conflict');
565
+ const enqueue = req.enqueue !== false && quotas.enableFifoWait;
566
+ if (enqueue) {
567
+ const q = waiters.get(key) || [];
568
+ if (!q.includes(agentId)) q.push(agentId);
569
+ waiters.set(key, q);
570
+ }
571
+ const rc = emit(CODES.DENY, agentId, 'swarm_claim', {
572
+ event: 'claim_conflict',
573
+ reason: 'claim_conflict',
574
+ kind,
575
+ resource: resource.slice(0, 200),
576
+ holder: held.agentId,
577
+ wait_position: enqueue ? (waiters.get(key) || []).indexOf(agentId) + 1 : null,
578
+ probe: d.probe,
579
+ });
580
+ writeHeatmap();
581
+ return {
582
+ ok: false,
583
+ code: CODES.DENY,
584
+ reason: 'claim_conflict',
585
+ holder: held.agentId,
586
+ wait_position: enqueue ? (waiters.get(key) || []).indexOf(agentId) + 1 : null,
587
+ probe: d.probe,
588
+ receipt_id: rc.ok ? rc.receipt_id : undefined,
589
+ };
590
+ }
591
+
592
+ if (held && held.agentId === agentId) {
593
+ noteAction(agentId);
594
+ return { ok: true, code: CODES.ALLOW, claimId: held.claimId, agentId, kind, resource, refreshed: true };
595
+ }
596
+
597
+ const claimId = mintId('claim');
598
+ const rec = {
599
+ claimId,
600
+ agentId,
601
+ kind,
602
+ resource,
603
+ since: nowIso(),
604
+ };
605
+ // Audit first — fail closed before mutating claims map
606
+ const rc = emit(CODES.ALLOW, agentId, 'swarm_claim', {
607
+ event: 'claim_granted',
608
+ claim_id: claimId,
609
+ kind,
610
+ resource: resource.slice(0, 200),
611
+ });
612
+ if (!rc.ok) {
613
+ return {
614
+ ok: false,
615
+ code: CODES.DENY_AUDIT,
616
+ reason: 'audit_write_failed',
617
+ next_action: rc.reason || 'audit ledger unavailable',
618
+ };
619
+ }
620
+ claims.set(key, rec);
621
+ persistClaims();
622
+ noteAction(agentId);
623
+ writeHeatmap();
624
+ return {
625
+ ok: true,
626
+ code: CODES.ALLOW,
627
+ claimId,
628
+ agentId,
629
+ kind,
630
+ resource,
631
+ receipt_id: rc.receipt_id,
632
+ };
633
+ }
634
+
635
+ /**
636
+ * Release a file/district claim; promote FIFO waiter if any.
637
+ * @param {{ agentId: string, kind?: 'file'|'district', resource: string }} req
638
+ */
639
+ function releaseClaim(req = {}) {
640
+ const agentId = req.agentId;
641
+ const kind = req.kind === 'district' ? 'district' : 'file';
642
+ const resource = typeof req.resource === 'string' ? req.resource.trim() : '';
643
+ if (!agentId || !resource) {
644
+ return { ok: false, code: CODES.ERROR_INVALID_INPUT, reason: 'agentId_and_resource_required' };
645
+ }
646
+ const key = resourceKey(kind, resource);
647
+ const held = claims.get(key);
648
+ if (!held) {
649
+ return { ok: true, code: CODES.ALLOW, noop: true, agentId, resource };
650
+ }
651
+ if (held.agentId !== agentId) {
652
+ const d = noteDeny(agentId, 'not_holder');
653
+ const rc = emit(CODES.DENY, agentId, 'swarm_release', {
654
+ event: 'release_denied',
655
+ reason: 'not_holder',
656
+ holder: held.agentId,
657
+ kind,
658
+ resource: resource.slice(0, 200),
659
+ probe: d.probe,
660
+ });
661
+ return {
662
+ ok: false,
663
+ code: CODES.DENY,
664
+ reason: 'not_holder',
665
+ holder: held.agentId,
666
+ receipt_id: rc.ok ? rc.receipt_id : undefined,
667
+ };
668
+ }
669
+ claims.delete(key);
670
+ persistClaims();
671
+ noteAction(agentId);
672
+ const rc = emit(CODES.ALLOW, agentId, 'swarm_release', {
673
+ event: 'claim_released',
674
+ claim_id: held.claimId,
675
+ kind,
676
+ resource: resource.slice(0, 200),
677
+ });
678
+
679
+ // Fairness: promote next FIFO waiter if any (do not auto-grant if they now exceed quota —
680
+ // leave them first in queue and let them call claim()).
681
+ const q = waiters.get(key) || [];
682
+ const next = q.shift();
683
+ if (q.length) waiters.set(key, q);
684
+ else waiters.delete(key);
685
+
686
+ writeHeatmap();
687
+ return {
688
+ ok: true,
689
+ code: CODES.ALLOW,
690
+ released: held.claimId,
691
+ next_waiter: next || null,
692
+ receipt_id: rc.ok ? rc.receipt_id : undefined,
693
+ };
694
+ }
695
+
696
+ /**
697
+ * Build operator heatmap snapshot (in-memory).
698
+ */
699
+ function buildHeatmap() {
700
+ expireDueLeases();
701
+ const agents = {};
702
+ for (const [agentId, rec] of Object.entries(domain.agents || {})) {
703
+ const s = statsFor(agentId);
704
+ const t = Date.now();
705
+ s.actionTs = pruneWindow(s.actionTs, quotas.windowMs, t);
706
+ s.denyTs = pruneWindow(s.denyTs, quotas.antiProbeWindowMs, t);
707
+ agents[agentId] = {
708
+ role: rec.role || null,
709
+ active_leases: activeLeaseCount(agentId),
710
+ active_claims: claimCount(agentId),
711
+ actions_in_window: s.actionTs.length,
712
+ denies_in_probe_window: s.denyTs.length,
713
+ probe_trips: s.probeTrips,
714
+ backpressure:
715
+ s.actionTs.length >= quotas.maxActionsPerWindow ||
716
+ claimCount(agentId) >= quotas.maxClaimsPerAgent,
717
+ };
718
+ }
719
+
720
+ const claimList = [];
721
+ const byDistrict = {};
722
+ const byFilePrefix = {};
723
+ for (const c of claims.values()) {
724
+ claimList.push({
725
+ claim_id: c.claimId,
726
+ agent_id: c.agentId,
727
+ kind: c.kind,
728
+ resource: c.resource,
729
+ since: c.since,
730
+ });
731
+ if (c.kind === 'district') {
732
+ byDistrict[c.resource] = (byDistrict[c.resource] || 0) + 1;
733
+ } else {
734
+ const prefix = c.resource.split(/[\\/]/).slice(0, 2).join('/') || c.resource;
735
+ byFilePrefix[prefix] = (byFilePrefix[prefix] || 0) + 1;
736
+ }
737
+ }
738
+
739
+ let activeLeases = 0;
740
+ let expiredLeases = 0;
741
+ let revokedLeases = 0;
742
+ for (const rec of domain.leaseStore.values()) {
743
+ if (rec.status === 'active') activeLeases += 1;
744
+ else if (rec.status === 'expired') expiredLeases += 1;
745
+ else if (rec.status === 'revoked') revokedLeases += 1;
746
+ }
747
+
748
+ const waiting = {};
749
+ for (const [k, q] of waiters.entries()) waiting[k] = [...q];
750
+
751
+ return {
752
+ v: 1,
753
+ layer: 'L3',
754
+ ts: nowIso(),
755
+ domain_root: domainRoot,
756
+ quotas: { ...quotas },
757
+ totals: {
758
+ agents: Object.keys(agents).length,
759
+ active_leases: activeLeases,
760
+ expired_leases: expiredLeases,
761
+ revoked_leases: revokedLeases,
762
+ active_claims: claimList.length,
763
+ wait_queues: Object.keys(waiting).length,
764
+ },
765
+ agents,
766
+ claims: claimList,
767
+ heat: {
768
+ districts: byDistrict,
769
+ file_prefixes: byFilePrefix,
770
+ },
771
+ waiters: waiting,
772
+ };
773
+ }
774
+
775
+ /**
776
+ * Write heatmap snapshot to `.knosky/swarm/heatmap.json`.
777
+ */
778
+ function writeHeatmap() {
779
+ const snap = buildHeatmap();
780
+ atomicWriteSoft(paths.heatmapPath, snap);
781
+ return { ok: true, path: paths.heatmapPath, snapshot: snap };
782
+ }
783
+
784
+ /**
785
+ * Read heatmap from disk (or build live if missing).
786
+ */
787
+ function status() {
788
+ if (existsSync(paths.heatmapPath)) {
789
+ try {
790
+ const disk = JSON.parse(readFileSync(paths.heatmapPath, 'utf8'));
791
+ // Refresh live counters on read
792
+ const live = buildHeatmap();
793
+ return { ok: true, path: paths.heatmapPath, heatmap: live, disk_ts: disk.ts || null };
794
+ } catch {
795
+ /* fall through */
796
+ }
797
+ }
798
+ const live = writeHeatmap();
799
+ return { ok: true, path: paths.heatmapPath, heatmap: live.snapshot, disk_ts: live.snapshot.ts };
800
+ }
801
+
802
+ function listLeases() {
803
+ expireDueLeases();
804
+ const out = [];
805
+ for (const [id, rec] of domain.leaseStore.entries()) {
806
+ out.push({
807
+ leaseId: id,
808
+ agentId: rec.agentId,
809
+ status: rec.status,
810
+ created_at: rec.created_at || null,
811
+ expires_at: rec.expires_at || null,
812
+ });
813
+ }
814
+ return out;
815
+ }
816
+
817
+ function listAgents() {
818
+ return { ...domain.agents };
819
+ }
820
+
821
+ return {
822
+ domainRoot,
823
+ domain,
824
+ paths,
825
+ quotas,
826
+ registerAgent,
827
+ issueLease,
828
+ expireLease,
829
+ revokeLease,
830
+ expireDueLeases,
831
+ claim,
832
+ releaseClaim,
833
+ buildHeatmap,
834
+ writeHeatmap,
835
+ status,
836
+ listLeases,
837
+ listAgents,
838
+ /** @internal test helpers */
839
+ _claims: claims,
840
+ _waiters: waiters,
841
+ _stats: agentStats,
842
+ };
843
+ }
844
+
845
+ /**
846
+ * Convenience: read heatmap JSON for CLI without creating a full coordinator.
847
+ * @param {string} [domainRoot]
848
+ * @param {string} [cityPath]
849
+ */
850
+ export function readSwarmHeatmap(domainRoot, cityPath) {
851
+ const root = resolveDomainRoot(cityPath, domainRoot);
852
+ const { heatmapPath } = swarmPaths(root);
853
+ if (!existsSync(heatmapPath)) {
854
+ return { ok: false, reason: 'no_heatmap', path: heatmapPath, domainRoot: root };
855
+ }
856
+ try {
857
+ const heatmap = JSON.parse(readFileSync(heatmapPath, 'utf8'));
858
+ return { ok: true, path: heatmapPath, domainRoot: root, heatmap };
859
+ } catch (err) {
860
+ return {
861
+ ok: false,
862
+ reason: err && err.message ? err.message : String(err),
863
+ path: heatmapPath,
864
+ domainRoot: root,
865
+ };
866
+ }
867
+ }