@ashlr/hub 3.2.6 → 3.2.7

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 (73) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/build-identity.json +2 -2
  3. package/dist/cli/completions.js +3 -1
  4. package/dist/cli/completions.js.map +1 -1
  5. package/dist/cli/conductor-permit.d.ts +1 -0
  6. package/dist/cli/conductor-permit.js +122 -0
  7. package/dist/cli/conductor-permit.js.map +1 -0
  8. package/dist/cli/goals.d.ts +2 -0
  9. package/dist/cli/goals.js +90 -5
  10. package/dist/cli/goals.js.map +1 -1
  11. package/dist/cli/help.js +2 -0
  12. package/dist/cli/help.js.map +1 -1
  13. package/dist/cli/index.js +6 -0
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/cli/loop.js +26 -5
  16. package/dist/cli/loop.js.map +1 -1
  17. package/dist/core/comms/elon-dialogue.js +16 -7
  18. package/dist/core/comms/elon-dialogue.js.map +1 -1
  19. package/dist/core/daemon/activation-permit.d.ts +89 -0
  20. package/dist/core/daemon/activation-permit.js +397 -0
  21. package/dist/core/daemon/activation-permit.js.map +1 -1
  22. package/dist/core/daemon/goal-conductor-permit-operator.d.ts +75 -0
  23. package/dist/core/daemon/goal-conductor-permit-operator.js +842 -0
  24. package/dist/core/daemon/goal-conductor-permit-operator.js.map +1 -0
  25. package/dist/core/daemon/loop.js +102 -20
  26. package/dist/core/daemon/loop.js.map +1 -1
  27. package/dist/core/fleet/quota.d.ts +74 -7
  28. package/dist/core/fleet/quota.js +688 -34
  29. package/dist/core/fleet/quota.js.map +1 -1
  30. package/dist/core/fleet/status.js +6 -5
  31. package/dist/core/fleet/status.js.map +1 -1
  32. package/dist/core/goals/advance.js +34 -5
  33. package/dist/core/goals/advance.js.map +1 -1
  34. package/dist/core/goals/conductor-quota.d.ts +38 -0
  35. package/dist/core/goals/conductor-quota.js +110 -0
  36. package/dist/core/goals/conductor-quota.js.map +1 -0
  37. package/dist/core/goals/conductor-target.d.ts +24 -0
  38. package/dist/core/goals/conductor-target.js +35 -0
  39. package/dist/core/goals/conductor-target.js.map +1 -0
  40. package/dist/core/goals/conductor.d.ts +15 -0
  41. package/dist/core/goals/conductor.js +116 -9
  42. package/dist/core/goals/conductor.js.map +1 -1
  43. package/dist/core/goals/store.d.ts +29 -5
  44. package/dist/core/goals/store.js +179 -26
  45. package/dist/core/goals/store.js.map +1 -1
  46. package/dist/core/goals/timestamp-repair.d.ts +50 -0
  47. package/dist/core/goals/timestamp-repair.js +740 -0
  48. package/dist/core/goals/timestamp-repair.js.map +1 -0
  49. package/dist/core/integrations/pulse-sync.js +13 -4
  50. package/dist/core/integrations/pulse-sync.js.map +1 -1
  51. package/dist/core/run/best-of-n.d.ts +18 -0
  52. package/dist/core/run/best-of-n.js +163 -18
  53. package/dist/core/run/best-of-n.js.map +1 -1
  54. package/dist/core/run/orchestrator.js +54 -0
  55. package/dist/core/run/orchestrator.js.map +1 -1
  56. package/dist/core/strategy/goal-planner.js +7 -1
  57. package/dist/core/strategy/goal-planner.js.map +1 -1
  58. package/dist/core/swarm/planner.d.ts +2 -2
  59. package/dist/core/swarm/planner.js +52 -5
  60. package/dist/core/swarm/planner.js.map +1 -1
  61. package/dist/core/swarm/runner.js +29 -3
  62. package/dist/core/swarm/runner.js.map +1 -1
  63. package/dist/core/types.d.ts +39 -0
  64. package/dist/core/types.js.map +1 -1
  65. package/dist/core/web/control.js +5 -2
  66. package/dist/core/web/control.js.map +1 -1
  67. package/dist/release-dependency-inventory.json +1 -1
  68. package/dist/tui/app.js +2 -2
  69. package/dist/tui/app.js.map +1 -1
  70. package/dist/tui/render.d.ts +2 -0
  71. package/dist/tui/render.js +162 -4
  72. package/dist/tui/render.js.map +1 -1
  73. package/package.json +1 -1
@@ -6,25 +6,51 @@
6
6
  * ledger records each dispatch and answers "how many in the last window?" and
7
7
  * "are we still under the configured cap?".
8
8
  *
9
- * Persistence discipline mirrors daemon/state.ts EXACTLY:
10
- * - Atomic writes (tmp file + POSIX rename).
11
- * - NEVER throws — load returns a fresh empty ledger on missing/corrupt file;
12
- * record swallows any persistence error.
9
+ * Observation compatibility remains deliberately tolerant: load returns a fresh
10
+ * empty ledger on missing/corrupt state and recordUse remains best-effort. The
11
+ * authority-bearing reservation API is stricter:
12
+ * - One local-store lock serializes read/check/append publication.
13
+ * - Configured limits fail closed on ambiguous or unavailable persistence.
14
+ * - Reservation identities are hashed and idempotent across retries.
15
+ * - The replacement file and containing directory are fsynced before success.
13
16
  * - mkdir -p the parent dir.
14
- * - Bounded history (last ~2000 events).
17
+ * - Bounded authority history (last 2000 events per known backend, max 30d).
15
18
  * - Homedir re-resolved at call time so tests can relocate HOME.
16
19
  *
17
20
  * No new runtime deps; node builtins only. evalQuota mirrors evalBudget's
18
21
  * three-level (ok/warn/over) semantics with a null-cap-disabled path.
19
22
  */
20
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
23
+ import { closeSync, constants as fsConstants, existsSync, fchmodSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readSync, readFileSync, renameSync, unlinkSync, writeFileSync, writeSync, } from 'node:fs';
24
+ import { createHash, randomUUID } from 'node:crypto';
21
25
  import { homedir } from 'node:os';
22
26
  import { join } from 'node:path';
27
+ import { acquireLocalStoreLockWithOutcome, releaseLocalStoreLock, } from './local-store-lock.js';
28
+ import { fsyncDirectory } from '../util/durability.js';
29
+ import { assurePrivateStoragePath } from '../util/private-storage.js';
23
30
  // ---------------------------------------------------------------------------
24
31
  // Constants
25
32
  // ---------------------------------------------------------------------------
26
33
  /** Maximum number of dispatch events retained in quota.json. */
27
34
  const MAX_EVENTS = 2000;
35
+ const MAX_LEDGER_BYTES = 1024 * 1024;
36
+ const MAX_AUTHORITY_LEDGER_BYTES = 8 * 1024 * 1024;
37
+ const MAX_DISPATCH_ID_BYTES = 4_096;
38
+ const MAX_BATCH_RESERVATIONS = 64;
39
+ const RESERVATION_DOMAIN = 'ashlr:fleet-quota-reservation:v1';
40
+ const SHA256_RE = /^[a-f0-9]{64}$/;
41
+ const O_NOFOLLOW = typeof fsConstants.O_NOFOLLOW === 'number' ? fsConstants.O_NOFOLLOW : 0;
42
+ const ENGINE_IDS = new Set([
43
+ 'builtin', 'local-coder', 'ashlrcode', 'aw', 'claude', 'codex', 'hermes',
44
+ 'kimi', 'nim', 'opencode', 'grok',
45
+ ]);
46
+ const MAX_AUTHORITY_EVENTS = MAX_EVENTS * ENGINE_IDS.size;
47
+ let fleetQuotaTestHooks;
48
+ /** Test-only fault injection; production callers must never configure this. */
49
+ export function setFleetQuotaTestHooksForTests(hooks) {
50
+ if (process.env.NODE_ENV !== 'test')
51
+ throw new Error('fleet quota test hooks are test-only');
52
+ fleetQuotaTestHooks = hooks;
53
+ }
28
54
  // ---------------------------------------------------------------------------
29
55
  // windowToMs — EXTENDS rollup's vocabulary with sub-day windows (local copy)
30
56
  // ---------------------------------------------------------------------------
@@ -39,12 +65,26 @@ export function windowToMs(window) {
39
65
  case '5m': return 5 * 60_000;
40
66
  case '15m': return 15 * 60_000;
41
67
  case '1h': return 3_600_000;
68
+ case '5h': return 5 * 3_600_000;
42
69
  case '1d': return 86_400_000;
43
70
  case '7d': return 7 * 86_400_000;
44
71
  case '30d': return 30 * 86_400_000;
45
72
  default: return 3_600_000;
46
73
  }
47
74
  }
75
+ function reservationWindowMs(window) {
76
+ switch (window) {
77
+ case '1m': return 60_000;
78
+ case '5m': return 5 * 60_000;
79
+ case '15m': return 15 * 60_000;
80
+ case '1h': return 3_600_000;
81
+ case '5h': return 5 * 3_600_000;
82
+ case '1d': return 86_400_000;
83
+ case '7d': return 7 * 86_400_000;
84
+ case '30d': return 30 * 86_400_000;
85
+ default: return null;
86
+ }
87
+ }
48
88
  // ---------------------------------------------------------------------------
49
89
  // Path helpers (re-resolved at call time so tests can relocate HOME)
50
90
  // ---------------------------------------------------------------------------
@@ -55,12 +95,368 @@ function fleetDir() {
55
95
  export function fleetQuotaPath() {
56
96
  return join(fleetDir(), 'quota.json');
57
97
  }
98
+ /**
99
+ * Private authority ledger. It is deliberately separate from quota.json:
100
+ * quota.json remains backwards-compatible, best-effort operator telemetry,
101
+ * while this file is the fail-closed idempotency/capacity boundary.
102
+ */
103
+ export function fleetQuotaReservationPath() {
104
+ return join(fleetDir(), 'quota-reservations.json');
105
+ }
106
+ /** Lock path used by the authority-bearing quota reservation transaction. */
107
+ export function fleetQuotaReservationLockPath() {
108
+ return join(fleetDir(), '.quota-reservation.lock');
109
+ }
58
110
  // ---------------------------------------------------------------------------
59
111
  // Fresh default
60
112
  // ---------------------------------------------------------------------------
61
113
  function freshQuota() {
62
114
  return { events: [] };
63
115
  }
116
+ function ownedByCurrentUser(stat) {
117
+ return typeof process.getuid !== 'function' || stat.uid === BigInt(process.getuid());
118
+ }
119
+ function sameIdentity(left, right) {
120
+ return left.dev === right.dev && left.ino === right.ino;
121
+ }
122
+ function safeQuotaDirectory(stat) {
123
+ return stat.isDirectory() && !stat.isSymbolicLink() && ownedByCurrentUser(stat) &&
124
+ (process.platform === 'win32' || (stat.mode & 511n) === 448n);
125
+ }
126
+ function safeQuotaFile(stat) {
127
+ return stat.isFile() && !stat.isSymbolicLink() && stat.nlink === 1n &&
128
+ ownedByCurrentUser(stat) &&
129
+ (process.platform === 'win32' || (stat.mode & 511n) === 384n);
130
+ }
131
+ // Pre-reservation releases wrote best-effort telemetry with the caller's
132
+ // normal umask (commonly 0755/0644). Accept that owned, non-writable-by-others
133
+ // shape only for a read-only first-use migration; new authority state remains
134
+ // exact-private 0700/0600.
135
+ function safeLegacyQuotaDirectory(stat) {
136
+ return stat.isDirectory() && !stat.isSymbolicLink() && ownedByCurrentUser(stat) &&
137
+ (process.platform === 'win32' || (stat.mode & 18n) === 0n);
138
+ }
139
+ function safeLegacyQuotaFile(stat) {
140
+ return stat.isFile() && !stat.isSymbolicLink() && stat.nlink === 1n &&
141
+ ownedByCurrentUser(stat) &&
142
+ (process.platform === 'win32' || (stat.mode & 18n) === 0n);
143
+ }
144
+ function pinQuotaDirectory() {
145
+ try {
146
+ const path = fleetDir();
147
+ const stat = lstatSync(path, { bigint: true });
148
+ return safeQuotaDirectory(stat) ? { path, dev: stat.dev, ino: stat.ino } : null;
149
+ }
150
+ catch {
151
+ return null;
152
+ }
153
+ }
154
+ function stableQuotaDirectory(authority) {
155
+ try {
156
+ const stat = lstatSync(authority.path, { bigint: true });
157
+ return safeQuotaDirectory(stat) && sameIdentity(authority, stat);
158
+ }
159
+ catch {
160
+ return false;
161
+ }
162
+ }
163
+ function pinLegacyQuotaDirectory() {
164
+ try {
165
+ const path = fleetDir();
166
+ const stat = lstatSync(path, { bigint: true });
167
+ return safeLegacyQuotaDirectory(stat) ? { path, dev: stat.dev, ino: stat.ino } : null;
168
+ }
169
+ catch {
170
+ return null;
171
+ }
172
+ }
173
+ function stableLegacyQuotaDirectory(authority) {
174
+ try {
175
+ const stat = lstatSync(authority.path, { bigint: true });
176
+ return safeLegacyQuotaDirectory(stat) && sameIdentity(authority, stat);
177
+ }
178
+ catch {
179
+ return false;
180
+ }
181
+ }
182
+ function prospectiveQuotaDirectoryAvailable() {
183
+ try {
184
+ const dir = fleetDir();
185
+ if (existsSync(dir)) {
186
+ const stat = lstatSync(dir, { bigint: true });
187
+ return stat.isDirectory() && !stat.isSymbolicLink() && ownedByCurrentUser(stat);
188
+ }
189
+ const parent = join(homedir(), '.ashlr');
190
+ if (!existsSync(parent))
191
+ return true;
192
+ const stat = lstatSync(parent, { bigint: true });
193
+ return stat.isDirectory() && !stat.isSymbolicLink() && ownedByCurrentUser(stat);
194
+ }
195
+ catch {
196
+ return false;
197
+ }
198
+ }
199
+ function strictQuotaEvent(value) {
200
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
201
+ return null;
202
+ const row = value;
203
+ const keys = Object.keys(row).sort().join(',');
204
+ if (keys !== 'backend,ts' && keys !== 'backend,reservationIdHash,ts')
205
+ return null;
206
+ const backend = row['backend'];
207
+ const ts = row['ts'];
208
+ const reservationIdHash = row['reservationIdHash'];
209
+ if (typeof backend !== 'string' || !ENGINE_IDS.has(backend) ||
210
+ typeof ts !== 'string' || !Number.isFinite(Date.parse(ts)) ||
211
+ new Date(Date.parse(ts)).toISOString() !== ts ||
212
+ (reservationIdHash !== undefined &&
213
+ (typeof reservationIdHash !== 'string' || !SHA256_RE.test(reservationIdHash))))
214
+ return null;
215
+ return {
216
+ backend: backend,
217
+ ts,
218
+ ...(typeof reservationIdHash === 'string' ? { reservationIdHash } : {}),
219
+ };
220
+ }
221
+ function parseStrictQuota(bytes, maxEvents = MAX_EVENTS, maxBytes = MAX_LEDGER_BYTES) {
222
+ if (bytes.length > maxBytes)
223
+ return null;
224
+ try {
225
+ const parsed = JSON.parse(bytes.toString('utf8'));
226
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
227
+ return null;
228
+ const row = parsed;
229
+ if (Object.keys(row).join(',') !== 'events' || !Array.isArray(row['events']) ||
230
+ row['events'].length > maxEvents)
231
+ return null;
232
+ const events = [];
233
+ for (const value of row['events']) {
234
+ const event = strictQuotaEvent(value);
235
+ if (!event)
236
+ return null;
237
+ events.push(event);
238
+ }
239
+ return { events };
240
+ }
241
+ catch {
242
+ return null;
243
+ }
244
+ }
245
+ function readBoundQuotaFile(authority) {
246
+ if (!stableQuotaDirectory(authority))
247
+ return null;
248
+ const path = fleetQuotaReservationPath();
249
+ let fd;
250
+ try {
251
+ fd = openSync(path, fsConstants.O_RDONLY | O_NOFOLLOW);
252
+ const opened = fstatSync(fd, { bigint: true });
253
+ if (!safeQuotaFile(opened) || opened.size > BigInt(MAX_AUTHORITY_LEDGER_BYTES) ||
254
+ !assurePrivateStoragePath(path, 'file', 'inspect-existing', { anchorPath: homedir() }).ok)
255
+ return null;
256
+ const named = lstatSync(path, { bigint: true });
257
+ if (!safeQuotaFile(named) || !sameIdentity(opened, named))
258
+ return null;
259
+ const bytes = Buffer.alloc(Number(opened.size));
260
+ let offset = 0;
261
+ while (offset < bytes.length) {
262
+ const count = readSync(fd, bytes, offset, bytes.length - offset, offset);
263
+ if (count <= 0)
264
+ return null;
265
+ offset += count;
266
+ }
267
+ const openedAfter = fstatSync(fd, { bigint: true });
268
+ const namedAfter = lstatSync(path, { bigint: true });
269
+ if (!safeQuotaFile(openedAfter) || !safeQuotaFile(namedAfter) ||
270
+ !sameIdentity(opened, openedAfter) || !sameIdentity(openedAfter, namedAfter) ||
271
+ openedAfter.size !== opened.size || openedAfter.mtimeNs !== opened.mtimeNs ||
272
+ openedAfter.ctimeNs !== opened.ctimeNs || !stableQuotaDirectory(authority))
273
+ return null;
274
+ return {
275
+ found: true,
276
+ bytes,
277
+ dev: openedAfter.dev,
278
+ ino: openedAfter.ino,
279
+ size: openedAfter.size,
280
+ mtimeNs: openedAfter.mtimeNs,
281
+ ctimeNs: openedAfter.ctimeNs,
282
+ };
283
+ }
284
+ catch (error) {
285
+ return fd === undefined && error.code === 'ENOENT' &&
286
+ stableQuotaDirectory(authority)
287
+ ? { found: false }
288
+ : null;
289
+ }
290
+ finally {
291
+ if (fd !== undefined) {
292
+ try {
293
+ closeSync(fd);
294
+ }
295
+ catch { /* best effort */ }
296
+ }
297
+ }
298
+ }
299
+ function readBoundLegacyQuotaFile(authority) {
300
+ if (!stableLegacyQuotaDirectory(authority))
301
+ return null;
302
+ const path = fleetQuotaPath();
303
+ let fd;
304
+ try {
305
+ fd = openSync(path, fsConstants.O_RDONLY | O_NOFOLLOW);
306
+ const opened = fstatSync(fd, { bigint: true });
307
+ if (!safeLegacyQuotaFile(opened) || opened.size > BigInt(MAX_LEDGER_BYTES) ||
308
+ !assurePrivateStoragePath(path, 'file', 'inspect-owned', { anchorPath: homedir() }).ok)
309
+ return null;
310
+ const named = lstatSync(path, { bigint: true });
311
+ if (!safeLegacyQuotaFile(named) || !sameIdentity(opened, named))
312
+ return null;
313
+ const bytes = Buffer.alloc(Number(opened.size));
314
+ let offset = 0;
315
+ while (offset < bytes.length) {
316
+ const count = readSync(fd, bytes, offset, bytes.length - offset, offset);
317
+ if (count <= 0)
318
+ return null;
319
+ offset += count;
320
+ }
321
+ const openedAfter = fstatSync(fd, { bigint: true });
322
+ const namedAfter = lstatSync(path, { bigint: true });
323
+ if (!safeLegacyQuotaFile(openedAfter) || !safeLegacyQuotaFile(namedAfter) ||
324
+ !sameIdentity(opened, openedAfter) || !sameIdentity(openedAfter, namedAfter) ||
325
+ openedAfter.size !== opened.size || openedAfter.mtimeNs !== opened.mtimeNs ||
326
+ openedAfter.ctimeNs !== opened.ctimeNs || !stableLegacyQuotaDirectory(authority))
327
+ return null;
328
+ return {
329
+ found: true,
330
+ bytes,
331
+ dev: openedAfter.dev,
332
+ ino: openedAfter.ino,
333
+ size: openedAfter.size,
334
+ mtimeNs: openedAfter.mtimeNs,
335
+ ctimeNs: openedAfter.ctimeNs,
336
+ };
337
+ }
338
+ catch (error) {
339
+ return fd === undefined && error.code === 'ENOENT' &&
340
+ stableLegacyQuotaDirectory(authority)
341
+ ? { found: false }
342
+ : null;
343
+ }
344
+ finally {
345
+ if (fd !== undefined) {
346
+ try {
347
+ closeSync(fd);
348
+ }
349
+ catch { /* best effort */ }
350
+ }
351
+ }
352
+ }
353
+ function loadStrictQuota(authority) {
354
+ const destination = readBoundQuotaFile(authority);
355
+ if (destination === null)
356
+ return null;
357
+ if (!destination.found) {
358
+ // Upgrade compatibility: seed the first authority transaction from the
359
+ // existing attempt ledger so active-window usage cannot be forgotten.
360
+ const legacy = readBoundLegacyQuotaFile(authority);
361
+ if (legacy === null)
362
+ return null;
363
+ if (!legacy.found)
364
+ return { ledger: freshQuota(), destination };
365
+ const ledger = parseStrictQuota(legacy.bytes);
366
+ return ledger ? { ledger, destination } : null;
367
+ }
368
+ const ledger = parseStrictQuota(destination.bytes, MAX_AUTHORITY_EVENTS, MAX_AUTHORITY_LEDGER_BYTES);
369
+ return ledger ? { ledger, destination } : null;
370
+ }
371
+ function loadQuotaForAuthorityInspection() {
372
+ if (existsSync(fleetQuotaReservationPath())) {
373
+ const authority = pinQuotaDirectory();
374
+ return authority ? loadStrictQuota(authority)?.ledger ?? null : null;
375
+ }
376
+ if (!existsSync(fleetDir()))
377
+ return prospectiveQuotaDirectoryAvailable() ? freshQuota() : null;
378
+ const authority = pinLegacyQuotaDirectory();
379
+ if (!authority)
380
+ return null;
381
+ const legacy = readBoundLegacyQuotaFile(authority);
382
+ if (legacy === null)
383
+ return null;
384
+ if (!legacy.found)
385
+ return freshQuota();
386
+ return parseStrictQuota(legacy.bytes);
387
+ }
388
+ function sameDestination(left, right) {
389
+ if (right === null || left.found !== right.found)
390
+ return false;
391
+ if (!left.found || !right.found)
392
+ return true;
393
+ return left.dev === right.dev && left.ino === right.ino && left.size === right.size &&
394
+ left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs &&
395
+ left.bytes.equals(right.bytes);
396
+ }
397
+ function saveQuotaReservation(ledger, authority, expected) {
398
+ const bytes = Buffer.from(`${JSON.stringify(ledger, null, 2)}\n`, 'utf8');
399
+ if (ledger.events.length > MAX_AUTHORITY_EVENTS ||
400
+ bytes.length > MAX_AUTHORITY_LEDGER_BYTES)
401
+ return false;
402
+ const tmp = join(authority.path, `.quota-${process.pid}-${randomUUID()}.tmp`);
403
+ let fd;
404
+ let tmpIdentity;
405
+ let published = false;
406
+ try {
407
+ if (!stableQuotaDirectory(authority))
408
+ return false;
409
+ fd = openSync(tmp, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | O_NOFOLLOW, 0o600);
410
+ tmpIdentity = fstatSync(fd, { bigint: true });
411
+ if (!safeQuotaFile(tmpIdentity) || tmpIdentity.size !== 0n ||
412
+ !assurePrivateStoragePath(tmp, 'file', 'secure-created', { anchorPath: homedir() }).ok)
413
+ return false;
414
+ let offset = 0;
415
+ while (offset < bytes.length) {
416
+ const count = writeSync(fd, bytes, offset, bytes.length - offset, offset);
417
+ if (count <= 0)
418
+ return false;
419
+ offset += count;
420
+ }
421
+ fchmodSync(fd, 0o600);
422
+ fsyncSync(fd);
423
+ const flushed = fstatSync(fd, { bigint: true });
424
+ const named = lstatSync(tmp, { bigint: true });
425
+ if (!safeQuotaFile(flushed) || !safeQuotaFile(named) ||
426
+ !sameIdentity(tmpIdentity, flushed) || !sameIdentity(flushed, named) ||
427
+ flushed.size !== BigInt(bytes.length) || !stableQuotaDirectory(authority) ||
428
+ !sameDestination(expected, readBoundQuotaFile(authority)))
429
+ return false;
430
+ closeSync(fd);
431
+ fd = undefined;
432
+ renameSync(tmp, fleetQuotaReservationPath());
433
+ published = true;
434
+ fsyncDirectory(authority.path, { expectedIdentity: authority });
435
+ const installed = readBoundQuotaFile(authority);
436
+ return installed !== null && installed.found && sameIdentity(tmpIdentity, installed) &&
437
+ installed.size === BigInt(bytes.length) && installed.bytes.equals(bytes) &&
438
+ stableQuotaDirectory(authority);
439
+ }
440
+ catch {
441
+ return false;
442
+ }
443
+ finally {
444
+ if (fd !== undefined) {
445
+ try {
446
+ closeSync(fd);
447
+ }
448
+ catch { /* best effort */ }
449
+ }
450
+ if (!published && tmpIdentity && stableQuotaDirectory(authority)) {
451
+ try {
452
+ const named = lstatSync(tmp, { bigint: true });
453
+ if (safeQuotaFile(named) && sameIdentity(named, tmpIdentity))
454
+ unlinkSync(tmp);
455
+ }
456
+ catch { /* best effort exact-temp cleanup */ }
457
+ }
458
+ }
459
+ }
64
460
  // ---------------------------------------------------------------------------
65
461
  // Load
66
462
  // ---------------------------------------------------------------------------
@@ -94,46 +490,294 @@ export function loadFleetQuota() {
94
490
  }
95
491
  }
96
492
  // ---------------------------------------------------------------------------
97
- // Save (atomic) — internal
493
+ // Record a dispatch (compatibility/operator telemetry only)
98
494
  // ---------------------------------------------------------------------------
99
495
  /**
100
- * Atomically write the ledger via tmp-file + rename (POSIX-atomic).
101
- * Creates ~/.ashlr/fleet recursively. Bounds events. Never throws.
496
+ * Append an actual dispatch-attempt event for `backend` (ts = now) and
497
+ * persist. Never throws. This is intentionally not launch authority; the
498
+ * strict reservation ledger above is separate so legacy 0644 telemetry files
499
+ * cannot block or overwrite authority state.
102
500
  */
103
- function saveFleetQuota(q) {
501
+ export function recordUse(backend) {
502
+ let acquired;
104
503
  try {
504
+ if (!ENGINE_IDS.has(backend))
505
+ return;
105
506
  const dir = fleetDir();
106
- if (!existsSync(dir)) {
507
+ if (!existsSync(dir))
107
508
  mkdirSync(dir, { recursive: true });
509
+ acquired = acquireLocalStoreLockWithOutcome(fleetQuotaReservationLockPath(), 250, {
510
+ anchorPath: homedir(),
511
+ exactPrivateStorage: true,
512
+ });
513
+ if (acquired.state !== 'acquired')
514
+ return;
515
+ const ledger = loadFleetQuota();
516
+ ledger.events.push({ backend, ts: new Date().toISOString() });
517
+ const bounded = { events: ledger.events.slice(-MAX_EVENTS) };
518
+ const destination = fleetQuotaPath();
519
+ const temporary = `${destination}.tmp`;
520
+ writeFileSync(temporary, `${JSON.stringify(bounded, null, 2)}\n`, 'utf8');
521
+ renameSync(temporary, destination);
522
+ }
523
+ catch {
524
+ // Never throws.
525
+ }
526
+ finally {
527
+ if (acquired?.state === 'acquired') {
528
+ try {
529
+ releaseLocalStoreLock(acquired.lock);
530
+ }
531
+ catch { /* telemetry only */ }
108
532
  }
109
- const bounded = {
110
- events: q.events.slice(-MAX_EVENTS),
111
- };
112
- const dest = fleetQuotaPath();
113
- const tmp = dest + '.tmp';
114
- writeFileSync(tmp, JSON.stringify(bounded, null, 2) + '\n', 'utf8');
115
- renameSync(tmp, dest);
533
+ }
534
+ }
535
+ function reservationIdHash(dispatchId) {
536
+ return createHash('sha256')
537
+ .update(JSON.stringify([RESERVATION_DOMAIN, dispatchId]))
538
+ .digest('hex');
539
+ }
540
+ function validReservationIdentity(backend, dispatchId) {
541
+ return ENGINE_IDS.has(backend) && typeof dispatchId === 'string' && dispatchId.length > 0 &&
542
+ Buffer.byteLength(dispatchId, 'utf8') <= MAX_DISPATCH_ID_BYTES;
543
+ }
544
+ function configuredReservationLimit(cfg, backend) {
545
+ const configured = cfg?.foundry?.limits?.[backend];
546
+ if (!configured)
547
+ return null;
548
+ const windowMs = typeof configured.window === 'string'
549
+ ? reservationWindowMs(configured.window)
550
+ : null;
551
+ if (windowMs === null || !Number.isSafeInteger(configured.max) ||
552
+ configured.max <= 0 || configured.max > MAX_EVENTS)
553
+ return 'invalid';
554
+ return { max: configured.max, windowMs };
555
+ }
556
+ function compactReservationEvents(events, _cfg, nowMs) {
557
+ const maximumWindowMs = reservationWindowMs('30d');
558
+ const retained = events.filter((event) => {
559
+ const ts = Date.parse(event.ts);
560
+ if (!Number.isFinite(ts))
561
+ return false;
562
+ return ts >= nowMs - maximumWindowMs;
563
+ });
564
+ retained.sort((left, right) => Date.parse(left.ts) - Date.parse(right.ts));
565
+ const byBackend = new Map();
566
+ for (const event of retained) {
567
+ const rows = byBackend.get(event.backend) ?? [];
568
+ rows.push(event);
569
+ if (rows.length > MAX_EVENTS)
570
+ rows.shift();
571
+ byBackend.set(event.backend, rows);
572
+ }
573
+ return [...byBackend.values()].flat().sort((left, right) => Date.parse(left.ts) - Date.parse(right.ts));
574
+ }
575
+ function reserveLimitedQuotaUsesLocked(normalized, limited, cfg, authority, loaded) {
576
+ try {
577
+ fsyncDirectory(authority.path, { expectedIdentity: authority });
116
578
  }
117
579
  catch {
118
- // Persistence failure must not crash the fleet — swallow silently.
580
+ return { kind: 'unavailable', launchAuthorized: false };
581
+ }
582
+ if (!stableQuotaDirectory(authority))
583
+ return { kind: 'unavailable', launchAuthorized: false };
584
+ const nowMs = fleetQuotaTestHooks?.now?.() ?? Date.now();
585
+ const compactedEvents = compactReservationEvents(loaded.ledger.events, cfg, nowMs);
586
+ const byHash = new Map(compactedEvents.flatMap((event) => event.reservationIdHash ? [[event.reservationIdHash, event.backend]] : []));
587
+ for (const request of normalized) {
588
+ const existingBackend = byHash.get(request.hash);
589
+ if (existingBackend !== undefined) {
590
+ return {
591
+ kind: existingBackend === request.backend ? 'duplicate' : 'conflict',
592
+ launchAuthorized: false,
593
+ backend: request.backend,
594
+ };
595
+ }
119
596
  }
597
+ const usage = new Map();
598
+ const requestedByBackend = new Map();
599
+ for (const request of limited) {
600
+ requestedByBackend.set(request.backend, (requestedByBackend.get(request.backend) ?? 0) + 1);
601
+ }
602
+ for (const request of limited) {
603
+ if (usage.has(request.backend))
604
+ continue;
605
+ const limit = request.limit;
606
+ const cutoff = nowMs - limit.windowMs;
607
+ const used = compactedEvents.reduce((count, event) => event.backend === request.backend && Date.parse(event.ts) >= cutoff ? count + 1 : count, 0);
608
+ usage.set(request.backend, used);
609
+ if (used + (requestedByBackend.get(request.backend) ?? 0) > limit.max) {
610
+ return {
611
+ kind: 'exhausted', launchAuthorized: false, backend: request.backend,
612
+ used, limit: limit.max,
613
+ };
614
+ }
615
+ }
616
+ const ts = new Date(nowMs).toISOString();
617
+ const next = {
618
+ events: compactReservationEvents([
619
+ ...compactedEvents,
620
+ ...limited.map(({ backend, hash }) => ({ backend, ts, reservationIdHash: hash })),
621
+ ], cfg, nowMs),
622
+ };
623
+ if (!saveQuotaReservation(next, authority, loaded.destination)) {
624
+ return { kind: 'unavailable', launchAuthorized: false };
625
+ }
626
+ return {
627
+ kind: 'reserved',
628
+ launchAuthorized: true,
629
+ reservations: normalized.map(({ backend, limit }) => {
630
+ if (!limit)
631
+ return { backend, status: 'unlimited', used: 0, limit: null };
632
+ return {
633
+ backend,
634
+ status: 'reserved',
635
+ used: (usage.get(backend) ?? 0) + (requestedByBackend.get(backend) ?? 0),
636
+ limit: limit.max,
637
+ };
638
+ }),
639
+ };
120
640
  }
121
- // ---------------------------------------------------------------------------
122
- // Record a dispatch
123
- // ---------------------------------------------------------------------------
124
641
  /**
125
- * Append a dispatch event for `backend` (ts = now) and persist. Never throws.
642
+ * Atomically reserve a complete set of configured backend dispatches before
643
+ * any provider contact. The set is all-or-nothing.
644
+ *
645
+ * An absent limit remains truly unlimited and does not touch local storage;
646
+ * this quota boundary therefore makes no replay/idempotency claim for that
647
+ * backend. Execution ownership remains a separate authority.
648
+ * Configured limits require an unambiguous private ledger, exclusive mutation
649
+ * lock, and durable publication. Duplicate and cross-backend identity reuse are
650
+ * explicit no-launch results: replay safety belongs to the execution authority,
651
+ * not to this quota ledger.
652
+ * This function never throws.
126
653
  */
127
- export function recordUse(backend) {
654
+ function reserveFleetQuotaUsesInternal(requests, cfg, options = {}) {
655
+ if (!Array.isArray(requests) || requests.length === 0 || requests.length > MAX_BATCH_RESERVATIONS) {
656
+ return { kind: 'invalid', launchAuthorized: false };
657
+ }
658
+ if (options.lockWaitMs !== undefined && !Number.isFinite(options.lockWaitMs)) {
659
+ return { kind: 'invalid', launchAuthorized: false };
660
+ }
661
+ const inputHashes = new Map();
662
+ const normalized = [];
663
+ for (const request of requests) {
664
+ if (!request || !validReservationIdentity(request.backend, request.dispatchId)) {
665
+ return { kind: 'invalid', launchAuthorized: false };
666
+ }
667
+ const hash = reservationIdHash(request.dispatchId);
668
+ const priorBackend = inputHashes.get(hash);
669
+ if (priorBackend !== undefined) {
670
+ return {
671
+ kind: priorBackend === request.backend ? 'duplicate' : 'conflict',
672
+ launchAuthorized: false,
673
+ backend: request.backend,
674
+ };
675
+ }
676
+ inputHashes.set(hash, request.backend);
677
+ const limit = configuredReservationLimit(cfg, request.backend);
678
+ if (limit === null) {
679
+ normalized.push({ backend: request.backend, hash, limit: null });
680
+ continue;
681
+ }
682
+ if (limit === 'invalid') {
683
+ return { kind: 'invalid', launchAuthorized: false, backend: request.backend };
684
+ }
685
+ normalized.push({ backend: request.backend, hash, limit });
686
+ }
687
+ const limited = normalized.filter((request) => request.limit !== null);
688
+ if (limited.length === 0) {
689
+ return {
690
+ kind: 'unlimited',
691
+ launchAuthorized: true,
692
+ reservations: normalized.map(({ backend }) => ({
693
+ backend, status: 'unlimited', used: 0, limit: null,
694
+ })),
695
+ };
696
+ }
697
+ const waitMs = options.lockWaitMs === undefined
698
+ ? 250
699
+ : Math.max(0, Math.min(2_000, Math.floor(options.lockWaitMs)));
700
+ const acquired = acquireLocalStoreLockWithOutcome(fleetQuotaReservationLockPath(), waitMs, {
701
+ anchorPath: homedir(),
702
+ exactPrivateStorage: true,
703
+ });
704
+ if (acquired.state !== 'acquired')
705
+ return { kind: 'unavailable', launchAuthorized: false };
706
+ let result;
128
707
  try {
129
- const q = loadFleetQuota();
130
- q.events.push({ backend, ts: new Date().toISOString() });
131
- saveFleetQuota(q);
708
+ const authority = pinQuotaDirectory();
709
+ if (!authority)
710
+ result = { kind: 'unavailable', launchAuthorized: false };
711
+ else {
712
+ const loaded = loadStrictQuota(authority);
713
+ result = loaded
714
+ ? reserveLimitedQuotaUsesLocked(normalized, limited, cfg, authority, loaded)
715
+ : { kind: 'unavailable', launchAuthorized: false };
716
+ }
132
717
  }
133
718
  catch {
134
- // Never throws.
719
+ result = { kind: 'unavailable', launchAuthorized: false };
720
+ }
721
+ const released = (fleetQuotaTestHooks?.releaseLock ?? releaseLocalStoreLock)(acquired.lock);
722
+ if (!released) {
723
+ return {
724
+ kind: 'unavailable',
725
+ launchAuthorized: false,
726
+ ...(result.kind === 'reserved' ? { reservationConsumed: true } : {}),
727
+ };
728
+ }
729
+ return result;
730
+ }
731
+ export function reserveFleetQuotaUses(requests, cfg, options = {}) {
732
+ try {
733
+ return reserveFleetQuotaUsesInternal(requests, cfg, options);
734
+ }
735
+ catch {
736
+ return { kind: 'invalid', launchAuthorized: false };
737
+ }
738
+ }
739
+ /** Single-dispatch convenience wrapper over the atomic batch transaction. */
740
+ export function reserveFleetQuotaUse(backend, cfg, dispatchId, options = {}) {
741
+ return reserveFleetQuotaUses([{ backend, dispatchId }], cfg, options);
742
+ }
743
+ function evaluateFleetQuotaAuthority(backend, cfg, now) {
744
+ try {
745
+ const limit = configuredReservationLimit(cfg, backend);
746
+ if (limit === null)
747
+ return { health: 'unlimited', standing: 'unlimited' };
748
+ if (limit === 'invalid')
749
+ return { health: 'invalid', standing: 'over' };
750
+ const ledger = loadQuotaForAuthorityInspection();
751
+ if (!ledger)
752
+ return { health: 'unavailable', standing: 'over' };
753
+ const nowMs = now ?? fleetQuotaTestHooks?.now?.() ?? Date.now();
754
+ const compacted = compactReservationEvents(ledger.events, cfg, nowMs);
755
+ if (compacted.length > MAX_AUTHORITY_EVENTS) {
756
+ return { health: 'capacity', standing: 'over' };
757
+ }
758
+ const cutoff = nowMs - limit.windowMs;
759
+ const used = compacted.reduce((count, event) => event.backend === backend && Date.parse(event.ts) >= cutoff ? count + 1 : count, 0);
760
+ return {
761
+ health: 'healthy',
762
+ standing: used >= limit.max ? 'over' : used >= limit.max * 0.8 ? 'warn' : 'ok',
763
+ };
764
+ }
765
+ catch {
766
+ return { health: 'unavailable', standing: 'over' };
135
767
  }
136
768
  }
769
+ /** Read-only conservative health projection for operator/status surfaces. */
770
+ export function inspectFleetQuotaAuthority(backend, cfg, now) {
771
+ return evaluateFleetQuotaAuthority(backend, cfg, now).health;
772
+ }
773
+ /**
774
+ * Quota standing derived from reserved authority slots, not attempt telemetry.
775
+ * This intentionally counts conservative pre-effect reservations that may not
776
+ * have reached a provider: those slots still consume the configured window.
777
+ */
778
+ export function evalFleetQuotaAuthority(backend, cfg, now) {
779
+ return evaluateFleetQuotaAuthority(backend, cfg, now).standing;
780
+ }
137
781
  // ---------------------------------------------------------------------------
138
782
  // Window counting
139
783
  // ---------------------------------------------------------------------------
@@ -166,15 +810,21 @@ export function usesInWindow(backend, windowMs, now) {
166
810
  *
167
811
  * Mirrors evalBudget's null-cap-disabled logic: when no
168
812
  * cfg.foundry.limits[backend] is configured, the backend is unlimited ⇒ always
169
- * within limit. Otherwise true iff usesInWindow(...) < limit.max. `now` is
813
+ * within limit. Healthy configured state follows reservation standing so a
814
+ * consumed slot can route to an eligible peer. Invalid, unavailable, or
815
+ * capacity-ambiguous authority deliberately remains advisory-eligible here so
816
+ * the final provider-effect reservation returns the structured fail-closed
817
+ * refusal instead of silently capability-downgrading to builtin. `now` is
170
818
  * injectable for tests.
171
819
  */
172
820
  export function withinLimit(backend, cfg, now) {
173
821
  const limit = cfg.foundry?.limits?.[backend];
174
822
  if (!limit)
175
823
  return true; // no cap configured ⇒ unlimited
176
- const used = usesInWindow(backend, windowToMs(limit.window), now);
177
- return used < limit.max;
824
+ const evaluation = evaluateFleetQuotaAuthority(backend, cfg, now);
825
+ if (evaluation.health !== 'healthy')
826
+ return true;
827
+ return evaluation.standing !== 'over';
178
828
  }
179
829
  // ---------------------------------------------------------------------------
180
830
  // Three-level quota evaluation (mirrors evalBudget)
@@ -186,9 +836,13 @@ export function withinLimit(backend, cfg, now) {
186
836
  */
187
837
  export function evalQuota(backend, cfg, now) {
188
838
  const limit = cfg.foundry?.limits?.[backend];
189
- if (!limit || limit.max <= 0)
190
- return 'ok'; // unlimited (or nonsensical cap)
191
- const used = usesInWindow(backend, windowToMs(limit.window), now);
839
+ if (!limit)
840
+ return 'ok';
841
+ const windowMs = reservationWindowMs(limit.window);
842
+ if (windowMs === null || !Number.isSafeInteger(limit.max) ||
843
+ limit.max <= 0 || limit.max > MAX_EVENTS)
844
+ return 'over';
845
+ const used = usesInWindow(backend, windowMs, now);
192
846
  if (used >= limit.max)
193
847
  return 'over';
194
848
  if (used >= limit.max * 0.8)