@tangle-network/agent-app 0.45.25 → 0.45.27

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 (44) hide show
  1. package/dist/assistant/index.js +5 -5
  2. package/dist/chat-routes/index.js +21 -21
  3. package/dist/chat-store/index.js +2 -2
  4. package/dist/{chunk-B2UEW62O.js → chunk-5ZTFZBS6.js} +4 -4
  5. package/dist/chunk-N3G3FSM4.js +509 -0
  6. package/dist/chunk-N3G3FSM4.js.map +1 -0
  7. package/dist/chunk-NEJ7OMQS.js +351 -0
  8. package/dist/chunk-NEJ7OMQS.js.map +1 -0
  9. package/dist/{chunk-KOMP6NDW.js → chunk-ODYE4A7L.js} +6 -6
  10. package/dist/{chunk-7V2I3JGZ.js → chunk-OTLEYHOG.js} +50 -12
  11. package/dist/chunk-OTLEYHOG.js.map +1 -0
  12. package/dist/chunk-WD2B6HGY.js +1046 -0
  13. package/dist/chunk-WD2B6HGY.js.map +1 -0
  14. package/dist/design-canvas-react/index.js +4 -4
  15. package/dist/run-tEsZUhAf.d.ts +40 -0
  16. package/dist/sandbox/index.d.ts +53 -1
  17. package/dist/sandbox/index.js +1 -1
  18. package/dist/signoff/cli.d.ts +12 -0
  19. package/dist/signoff/cli.js +166 -0
  20. package/dist/signoff/cli.js.map +1 -0
  21. package/dist/signoff/index.d.ts +430 -0
  22. package/dist/signoff/index.js +63 -0
  23. package/dist/signoff/index.js.map +1 -0
  24. package/dist/signoff/proof-cli.d.ts +1 -0
  25. package/dist/signoff/proof-cli.js +92 -0
  26. package/dist/signoff/proof-cli.js.map +1 -0
  27. package/dist/signoff/proof.d.ts +515 -0
  28. package/dist/signoff/proof.js +148 -0
  29. package/dist/signoff/proof.js.map +1 -0
  30. package/dist/spend/cli.d.ts +1 -0
  31. package/dist/spend/cli.js +119 -0
  32. package/dist/spend/cli.js.map +1 -0
  33. package/dist/spend/index.d.ts +640 -0
  34. package/dist/spend/index.js +196 -0
  35. package/dist/spend/index.js.map +1 -0
  36. package/dist/stream/index.js +14 -14
  37. package/dist/teams/index.js +9 -9
  38. package/dist/teams/invitations-api.js +7 -7
  39. package/dist/types-U7Nz-txa.d.ts +233 -0
  40. package/dist/web-react/index.js +11 -11
  41. package/package.json +22 -3
  42. package/dist/chunk-7V2I3JGZ.js.map +0 -1
  43. /package/dist/{chunk-B2UEW62O.js.map → chunk-5ZTFZBS6.js.map} +0 -0
  44. /package/dist/{chunk-KOMP6NDW.js.map → chunk-ODYE4A7L.js.map} +0 -0
@@ -0,0 +1,640 @@
1
+ /**
2
+ * The vocabulary of consumer-side spend verification.
3
+ *
4
+ * Kept in its own module with ZERO imports so a product can type its storage
5
+ * rows and its reconciliation config without pulling in `node:fs` through the
6
+ * CLI half.
7
+ *
8
+ * The model in one paragraph: the platform's ledger is authoritative about what
9
+ * was CHARGED. A product knows something the ledger does not — what it ASKED
10
+ * for. Recording that second view, and diffing it against the first, is what
11
+ * turns a platform billing defect from silent money into an alert. Nothing here
12
+ * lets a product self-certify a charge away; the output is a discrepancy a human
13
+ * disputes.
14
+ */
15
+ /**
16
+ * One box, as the PRODUCT understands it. Folded, not an append-only log: a
17
+ * product runs one row per sandbox, and every field below is derived by a
18
+ * monotonic fold (see `foldSpendBoxRecord`) so two concurrent writers cannot
19
+ * produce a wrong answer, only a stale one.
20
+ *
21
+ * Timestamps are epoch ms throughout, matching the platform's own
22
+ * `sandbox_meta.last_started_at`.
23
+ */
24
+ interface SpendBoxRecord {
25
+ /** The platform's sandbox id — the join key to every settlement row. */
26
+ readonly sandboxId: string;
27
+ /** The product's own tenancy unit, which the platform does not model. */
28
+ readonly workspaceId: string;
29
+ /** First moment the product knew this box existed. */
30
+ readonly createdAt: number;
31
+ /**
32
+ * The idle timeout the product ASKED the platform for, seconds. This is the
33
+ * width of the grace window between the last thing the product saw and the
34
+ * moment the platform should have stopped billing.
35
+ */
36
+ readonly idleTimeoutSeconds: number;
37
+ /**
38
+ * The maximum lifetime the product asked for, seconds, when it asked for one.
39
+ * This is the strongest bound a product holds: the platform destroys the box
40
+ * at `createdAt + maxLifetimeSeconds` regardless of what the product observed,
41
+ * so it caps the ceiling even when nothing else can (see `computeExpectedCeiling`).
42
+ */
43
+ readonly maxLifetimeSeconds: number | null;
44
+ /** Latest moment the product OBSERVED the box doing work. */
45
+ readonly lastActivityAt: number;
46
+ /**
47
+ * Detached runs dispatched but never observed to finish, by run id.
48
+ *
49
+ * Non-empty means the product genuinely cannot bound this box from its own
50
+ * observations: it handed the platform work and disconnected. The ceiling
51
+ * degrades accordingly rather than pretending to a tightness it did not earn.
52
+ */
53
+ readonly openDetachedRunIds: readonly string[];
54
+ /** When the product knows the box stopped. Cleared by later activity. */
55
+ readonly stoppedAt: number | null;
56
+ /** When the product knows the box was deleted. Set once — a deleted id never returns. */
57
+ readonly deletedAt: number | null;
58
+ /** Opaque product-column values, written verbatim and never read here. */
59
+ readonly extras?: Record<string, unknown>;
60
+ }
61
+ /**
62
+ * A fold step. Every field states its own merge rule, so a SQL implementation
63
+ * can apply it in one statement and reach the same record an in-memory
64
+ * read-modify-write reaches.
65
+ */
66
+ interface SpendBoxPatch {
67
+ /** Advance `lastActivityAt` to the max of stored and this. Never moves backward. */
68
+ readonly observedActivityAt?: number;
69
+ /** Add a run id to `openDetachedRunIds` (set semantics — re-adding is a no-op). */
70
+ readonly openDetachedRunAdd?: string;
71
+ /** Remove a run id from `openDetachedRunIds`. Removing an absent id is a no-op. */
72
+ readonly openDetachedRunRemove?: string;
73
+ /**
74
+ * Latest-wins. Activity observed AFTER a recorded stop clears it: a box that
75
+ * worked after we thought it stopped is running again, and a stale stop would
76
+ * make the ceiling too tight.
77
+ */
78
+ readonly stoppedAt?: number;
79
+ /** Set-once. A later delete observation does not move the first one. */
80
+ readonly deletedAt?: number;
81
+ }
82
+ /** Which fact bounds a box's billable time, weakest last. */
83
+ type CeilingBasis =
84
+ /** The product observed deletion. Billing cannot run past a box that is gone. */
85
+ 'deleted'
86
+ /** The product observed a stop. Billing should have closed there. */
87
+ | 'stopped'
88
+ /** No stop seen, but the platform destroys the box at its max lifetime. */
89
+ | 'max-lifetime'
90
+ /** No stop seen; the platform's idle timer is what should have closed billing. */
91
+ | 'idle-timeout'
92
+ /**
93
+ * An unfinished detached run and no max lifetime — the product cannot bound
94
+ * this box at all, so the ceiling degrades to the reconciliation instant.
95
+ * A finding on this basis is weak evidence and says so.
96
+ */
97
+ | 'open-detached-run';
98
+ /** The upper bound on one box's billable duration, and what earned it. */
99
+ interface ExpectedCeiling {
100
+ readonly sandboxId: string;
101
+ readonly basis: CeilingBasis;
102
+ /** The latest instant this box could still have been billable, epoch ms. */
103
+ readonly horizonAt: number;
104
+ /** `horizonAt - createdAt + toleranceMs`. The upper bound on billable ms. */
105
+ readonly ceilingMs: number;
106
+ readonly toleranceMs: number;
107
+ /**
108
+ * False when the basis is `open-detached-run` — the ceiling then rests on the
109
+ * reconciliation instant rather than on anything the product observed, so an
110
+ * overage means the platform billed outside the box's own lifetime, not merely
111
+ * longer than expected.
112
+ */
113
+ readonly bounded: boolean;
114
+ }
115
+ /**
116
+ * One settled ledger row, in the shape the platform's `credit_transactions`
117
+ * table stores it. The product supplies these through its own fetch (the
118
+ * platform's credit-history API, an export, a mirror) — this package never
119
+ * reaches for them, because the ledger is the counterparty's record and reading
120
+ * it is the product's authenticated business.
121
+ *
122
+ * The product's fetch MUST scope rows to boxes it owns. Products bill to a
123
+ * shared company key, so an unscoped fetch returns every sibling product's
124
+ * settlements and every one of them is a correct `unknown-box` finding.
125
+ */
126
+ interface SettlementRow {
127
+ /** The ledger row id, for the dispute. */
128
+ readonly id: string;
129
+ /**
130
+ * `sandbox:<kind>:<sandboxId>:<intervalStartMs>` — the platform's idempotency
131
+ * key, and the only place the billed interval's START is recorded.
132
+ */
133
+ readonly referenceId: string | null;
134
+ /**
135
+ * Signed nanodollars, exactly as the ledger stores it: negative is a charge,
136
+ * positive is a credit or refund.
137
+ */
138
+ readonly amountNanoUsd: number;
139
+ /** `compute` | `refund` | `inference` | … */
140
+ readonly type: string;
141
+ /** `sandbox` | `router` | … */
142
+ readonly product: string | null;
143
+ /** `sandbox:<sandboxId>` — the platform's aggregation unit. */
144
+ readonly groupKey: string | null;
145
+ /** Settlement instant, epoch ms. The product normalizes the stored text. */
146
+ readonly createdAt: number;
147
+ readonly description: string | null;
148
+ /** Provider at-cost basis, unsigned nanodollars. Null when unattributed. */
149
+ readonly costBasisNanoUsd: number | null;
150
+ /**
151
+ * The billed duration, when the product's ledger view exposes it directly.
152
+ * Null is the common case: the platform does not store duration on the row.
153
+ */
154
+ readonly billedMs: number | null;
155
+ }
156
+ /** The parts of a settlement reference id, once parsed. */
157
+ interface SettlementReference {
158
+ /** `stop` | `compute` | `egress` | `gpu-lease` | anything the platform adds. */
159
+ readonly kind: string;
160
+ /** For compute kinds, the sandbox id. For `gpu-lease`, the lease id. */
161
+ readonly resourceId: string;
162
+ /** The interval's start, epoch ms. Null for kinds that carry no interval. */
163
+ readonly intervalStartMs: number | null;
164
+ }
165
+ /** How a settled duration was arrived at — every duration finding carries one. */
166
+ type BilledDurationBasis =
167
+ /** The ledger row carried the duration. Exact. */
168
+ 'reported'
169
+ /** `amount ÷ the product's stated hourly rate`. Exact when the rate is right. */
170
+ | 'rate'
171
+ /**
172
+ * `settledAt - intervalStart`. An UPPER bound, not the billed duration: a
173
+ * correct settlement posted late by the platform's durable settlement queue
174
+ * reads longer here than it billed. Findings on this basis say so.
175
+ */
176
+ | 'reference-span'
177
+ /** No basis available — duration rules are skipped for this row. */
178
+ | 'unknown';
179
+ /** The checks this reconciler runs. Each is individually skippable, by name. */
180
+ type SpendCheckId =
181
+ /** A settlement against a box the product has no record of ever asking for. */
182
+ 'unknown-box'
183
+ /** A settled duration longer than the product's own upper bound allows. */
184
+ | 'over-ceiling'
185
+ /** A spend window far above the trailing median — the burst shape of a defect. */
186
+ | 'velocity'
187
+ /** The balance the product observes has gone below its floor. */
188
+ | 'negative-balance';
189
+ declare const SPEND_CHECKS: readonly SpendCheckId[];
190
+ /**
191
+ * One discrepancy, with every number the rule compared.
192
+ *
193
+ * Nullable fields are per-check and deliberately present-but-null rather than
194
+ * absent: a reader scanning a JSON dump can tell "this rule does not measure
195
+ * that" from "that measurement is missing".
196
+ */
197
+ interface SpendFinding {
198
+ readonly check: SpendCheckId;
199
+ /** What is wrong, in one sentence, with the numbers in it. */
200
+ readonly message: string;
201
+ /** What to do about it. A finding without a remedy is a complaint. */
202
+ readonly remedy: string;
203
+ readonly sandboxId: string | null;
204
+ readonly workspaceId: string | null;
205
+ /** The ledger rows that evidence this finding — the dispute's exhibit list. */
206
+ readonly referenceIds: readonly string[];
207
+ /** Nanodollars this finding puts in question, unsigned. */
208
+ readonly settledNanoUsd: number;
209
+ /** `over-ceiling` — the duration actually settled, and how that was derived. */
210
+ readonly settledMs: number | null;
211
+ readonly durationBasis: BilledDurationBasis | null;
212
+ /** `over-ceiling` — the bound it broke, and what earned that bound. */
213
+ readonly ceilingMs: number | null;
214
+ readonly overageMs: number | null;
215
+ readonly ceilingBasis: CeilingBasis | null;
216
+ /** `velocity` — the window, its trailing median, and the ratio between them. */
217
+ readonly windowNanoUsd: number | null;
218
+ readonly trailingMedianNanoUsd: number | null;
219
+ readonly velocityRatio: number | null;
220
+ readonly windowStartAt: number | null;
221
+ /** `negative-balance` — the observed balance and the floor it broke. */
222
+ readonly balanceNanoUsd: number | null;
223
+ readonly balanceFloorNanoUsd: number | null;
224
+ }
225
+ /** What one reconciliation pass concluded. */
226
+ interface SpendReport {
227
+ /** True when nothing fired. `ok === findings.length === 0`. */
228
+ readonly ok: boolean;
229
+ readonly findings: readonly SpendFinding[];
230
+ readonly checksRun: readonly SpendCheckId[];
231
+ /** Rows the pass read, including the ones no rule looked at. */
232
+ readonly rowsExamined: number;
233
+ /** Distinct boxes those rows settled against. */
234
+ readonly boxesExamined: number;
235
+ /** Total charged across every examined row, unsigned nanodollars. */
236
+ readonly settledNanoUsd: number;
237
+ /** Total credited back across every examined row, unsigned nanodollars. */
238
+ readonly creditedNanoUsd: number;
239
+ /** The instant the pass treated as "now". */
240
+ readonly asOf: number;
241
+ }
242
+
243
+ /**
244
+ * Persistence seam for the expectation ledger — the product implements it over
245
+ * its own tables.
246
+ *
247
+ * Deliberately NOT compare-and-set, unlike `MissionStorePort`. A mission has one
248
+ * serialized owner and a lost write corrupts a state machine; a box record is a
249
+ * MONOTONIC FOLD (activity takes a max, a detached-run id joins or leaves a set,
250
+ * delete is set-once) so concurrent writers converge no matter what order they
251
+ * land in. The worst a lost race can do here is leave `lastActivityAt` behind
252
+ * the truth — which makes the derived ceiling TIGHTER, so the failure mode is a
253
+ * false alarm a human dismisses, never a missed charge. That asymmetry is the
254
+ * whole reason the fold is shaped this way.
255
+ *
256
+ * `update` returns null when the row does not exist, never a throw.
257
+ */
258
+ interface SpendLedgerStorePort {
259
+ load(sandboxId: string): Promise<SpendBoxRecord | null>;
260
+ /** `extras` are the opaque product-column values — write them in the SAME
261
+ * statement as the record, or ignore them if the table has no extra columns. */
262
+ insert(record: SpendBoxRecord, extras?: Record<string, unknown>): Promise<SpendBoxRecord>;
263
+ update(sandboxId: string, patch: SpendBoxPatch): Promise<SpendBoxRecord | null>;
264
+ }
265
+ /**
266
+ * Apply one fold step. Exported so a SQL implementation and an in-memory one
267
+ * reach the same record, and so a product can unit-test its own store against
268
+ * the canonical answer.
269
+ *
270
+ * The two rules worth stating out loud:
271
+ *
272
+ * - `observedActivityAt` only ever moves `lastActivityAt` FORWARD. A replayed
273
+ * or out-of-order event cannot rewind the ceiling.
274
+ * - activity later than a recorded `stoppedAt` CLEARS the stop. A box that
275
+ * worked after the product thought it stopped is running again, and keeping
276
+ * the stale stop would make the ceiling too tight — inventing an over-ceiling
277
+ * finding out of the product's own bookkeeping rather than the platform's.
278
+ */
279
+ declare function foldSpendBoxRecord(record: SpendBoxRecord, patch: SpendBoxPatch): SpendBoxRecord;
280
+ /** An in-memory store that also lets a test inspect and force state. */
281
+ interface InMemorySpendLedgerStore extends SpendLedgerStorePort {
282
+ /** Every record, insertion order. */
283
+ records(): SpendBoxRecord[];
284
+ /** Unguarded direct write — simulates a crash-shaped or platform-seeded row. */
285
+ put(record: SpendBoxRecord): void;
286
+ }
287
+ /** Create an in-memory expectation ledger. Production writers use the same port. */
288
+ declare function createInMemorySpendLedgerStore(): InMemorySpendLedgerStore;
289
+ /** What the product tells the ledger when it first sees a box. */
290
+ interface ObserveSandboxInput {
291
+ readonly sandboxId: string;
292
+ readonly workspaceId: string;
293
+ /** The idle timeout the product asked the platform for, seconds. */
294
+ readonly idleTimeoutSeconds: number;
295
+ /** The max lifetime the product asked for, seconds, when it asked for one. */
296
+ readonly maxLifetimeSeconds?: number | null;
297
+ /** Defaults to the ledger's clock. */
298
+ readonly at?: number;
299
+ }
300
+ interface SpendLedgerOptions {
301
+ readonly store: SpendLedgerStorePort;
302
+ /** Injectable clock (epoch ms). Default `Date.now`. */
303
+ readonly now?: () => number;
304
+ /** Product columns written verbatim on every insert. */
305
+ readonly extras?: Record<string, unknown>;
306
+ }
307
+ /**
308
+ * The recording half of spend verification: the product's own account of what
309
+ * it asked the platform for.
310
+ *
311
+ * Every method is best-effort from the caller's point of view — a product wires
312
+ * these into paths that must not fail because bookkeeping failed. They still
313
+ * reject on a store error rather than swallowing it, so a caller that wants
314
+ * fire-and-forget says so at the call site (`/sandbox`'s hook does).
315
+ */
316
+ interface SpendLedger {
317
+ /**
318
+ * Record that a box exists and is billable from now. Inserts on first sight,
319
+ * and otherwise records activity — reuse and resume are both "the platform is
320
+ * charging for this box again", and the record's own existence is what
321
+ * distinguishes them, so no caller has to know which happened.
322
+ */
323
+ observeSandbox(input: ObserveSandboxInput): Promise<SpendBoxRecord>;
324
+ /** Record that the product saw this box do work. */
325
+ recordActivity(sandboxId: string, at?: number): Promise<SpendBoxRecord | null>;
326
+ /**
327
+ * Record that the product handed the platform work it will NOT watch finish.
328
+ * Until the matching end is recorded, this box's ceiling cannot rest on
329
+ * observed activity — see `computeExpectedCeiling`.
330
+ */
331
+ recordDetachedRunStarted(sandboxId: string, runId: string, at?: number): Promise<SpendBoxRecord | null>;
332
+ /** Record that a detached run was confirmed finished. */
333
+ recordDetachedRunEnded(sandboxId: string, runId: string, at?: number): Promise<SpendBoxRecord | null>;
334
+ /** Record that the product knows this box stopped. */
335
+ recordStopped(sandboxId: string, at?: number): Promise<SpendBoxRecord | null>;
336
+ /** Record that the product knows this box was deleted. */
337
+ recordDeleted(sandboxId: string, at?: number): Promise<SpendBoxRecord | null>;
338
+ }
339
+ /** Create the recording half over a product-supplied store. */
340
+ declare function createSpendLedger(options: SpendLedgerOptions): SpendLedger;
341
+
342
+ /**
343
+ * Slack allowed between the product's bound and what the platform settled,
344
+ * before an overage is called a discrepancy. 15 minutes.
345
+ *
346
+ * Not a guess: it is the platform's OWN staleness threshold for compute
347
+ * settlement. Its runbook clears an incident when
348
+ * `/health computeSettlement.oldestAgeSeconds` is "back under 900" — so 900 s is
349
+ * the age the platform itself treats as normal settlement lag, and anything
350
+ * inside it is drift the platform has already declared acceptable. Below that a
351
+ * product would alert on the platform's ordinary queue behaviour; far above it
352
+ * the tolerance starts eating the signal, because the idle window it must stay
353
+ * well under is 3600 s in every shipped product.
354
+ *
355
+ * It is a caller parameter because a product that asks for a shorter idle
356
+ * timeout must shrink this with it.
357
+ */
358
+ declare const DEFAULT_CEILING_TOLERANCE_MS = 900000;
359
+ interface ComputeExpectedCeilingOptions {
360
+ /** The instant the reconciliation treats as "now", epoch ms. */
361
+ readonly asOf: number;
362
+ /** Slack before an overage counts. Default {@link DEFAULT_CEILING_TOLERANCE_MS}. */
363
+ readonly toleranceMs?: number;
364
+ }
365
+ /**
366
+ * The upper bound on how long one box could honestly have been billable.
367
+ *
368
+ * The whole design constraint is that this must stay an UPPER bound under
369
+ * everything the product cannot see. Three such blind spots exist, and they
370
+ * pull in different directions:
371
+ *
372
+ * - **Platform-side suspends.** The platform can park a box the product never
373
+ * hears about. That only ever REDUCES real billable time, so an upper bound
374
+ * is unaffected and nothing here widens for it.
375
+ * - **Detached runs.** The product dispatches work and disconnects. The box
376
+ * keeps working — and billing — after the last activity the product saw, so
377
+ * `lastActivityAt` understates the truth. An unfinished detached run
378
+ * therefore abandons the activity-based bound entirely rather than reporting
379
+ * a bound it cannot support.
380
+ * - **Reconnects.** A browser or worker re-attaches and work resumes. This
381
+ * needs no special case: a reconnect is recorded as activity, the fold takes
382
+ * the max, and the horizon moves out on its own.
383
+ *
384
+ * The bound that rescues the detached case is `maxLifetimeSeconds`. The platform
385
+ * destroys the box at `createdAt + maxLifetimeSeconds` no matter what anyone
386
+ * observed, so a product that asks for one holds a hard bound that survives
387
+ * every blind spot above. Both shipped products ask for 86 400 s, which is why
388
+ * the incident — 124 to 268 hours settled against boxes with a 24-hour
389
+ * lifetime — is detectable with no lifecycle bookkeeping at all.
390
+ */
391
+ declare function computeExpectedCeiling(record: SpendBoxRecord, options: ComputeExpectedCeilingOptions): ExpectedCeiling;
392
+
393
+ /**
394
+ * Parse the platform's settlement idempotency key.
395
+ *
396
+ * The platform mints it as `sandbox:<kind>:<resourceId>:<intervalStart>`
397
+ * (`d1-usage-service.ts`), where `intervalStart` is the interval cursor in epoch
398
+ * ms — the SAME `last_started_at` the settlement subtracts from to get its
399
+ * billed duration. That makes this string the only place a consumer can read the
400
+ * billed interval's start, because the ledger row itself stores no duration.
401
+ *
402
+ * Kinds seen in production: `stop` (an interval closing), `compute` (a heartbeat
403
+ * claim), `egress`, `gpu-lease`. `stop` deliberately covers both a settle and a
404
+ * late stop racing over the same claim, so the two derive one reference id and
405
+ * the ledger's uniqueness constraint makes the overlap safe.
406
+ *
407
+ * Returns null for anything that is not a sandbox reference — a router
408
+ * inference row, a grant, a refund — rather than guessing.
409
+ */
410
+ declare function parseSettlementReference(referenceId: string | null | undefined): SettlementReference | null;
411
+ /**
412
+ * Read the sandbox id out of the platform's aggregation key, `sandbox:<id>`.
413
+ *
414
+ * Distinct from the reference id: `groupKey` is the unit a billing statement
415
+ * groups by and is deliberately NOT unique per row, while `referenceId` is
416
+ * unique per interval. A null group key means "do not aggregate" (grants,
417
+ * top-ups, refunds, transfers) and is not an error.
418
+ */
419
+ declare function parseSandboxGroupKey(groupKey: string | null | undefined): string | null;
420
+ /**
421
+ * The sandbox a settlement row is attributable to.
422
+ *
423
+ * The reference id wins over the group key because it is the field the platform
424
+ * dedups on, so it is the one guaranteed present and correct on a compute
425
+ * settlement; the group key is the fallback for rows written before a producer
426
+ * stamped a reference, and for kinds whose reference names something else (a GPU
427
+ * lease id, not a box).
428
+ */
429
+ declare function settlementSandboxId(row: SettlementRow): string | null;
430
+ /** True when a row is a charge (the ledger stores charges as negative amounts). */
431
+ declare function isCharge(row: SettlementRow): boolean;
432
+ /** A charge's magnitude in unsigned nanodollars; 0 for credits. */
433
+ declare function chargeNanoUsd(row: SettlementRow): number;
434
+
435
+ interface VelocityOptions {
436
+ /** Bucket width for a spend window, ms. Default 24 h. */
437
+ readonly windowMs?: number;
438
+ /** Fire when a window exceeds this multiple of the trailing median. Default 5. */
439
+ readonly multiple?: number;
440
+ /**
441
+ * Windows of history required before a median means anything. Default 3.
442
+ * Below this the rule stays silent, so a product's genuine first days of
443
+ * usage are not reported as an anomaly.
444
+ */
445
+ readonly minTrailingWindows?: number;
446
+ /**
447
+ * A window under this never fires, whatever the ratio. Default $1.00.
448
+ *
449
+ * Without a floor the rule is useless: a trailing median of a tenth of a cent
450
+ * makes every ordinary day a 5x outlier. $1.00 is set from the incident's own
451
+ * distribution — the smallest of the eight affected wallets took $1.98, and
452
+ * the two rows in the same window that were GENUINE were sub-cent. So the
453
+ * floor sits above the noise and below every real finding.
454
+ */
455
+ readonly minAbsoluteNanoUsd?: number;
456
+ }
457
+ /** The balance the product observes, and the floor it must not cross. */
458
+ interface ObservedBalance {
459
+ /** Signed nanodollars, as the platform reports it. */
460
+ readonly nanoUsd: number;
461
+ /** Below this is a finding. Default 0. */
462
+ readonly floorNanoUsd?: number;
463
+ }
464
+ /**
465
+ * A box's price, nanodollars per hour, used to derive an EXACT billed duration
466
+ * from a charge. Return null when the product does not know the box's rate; the
467
+ * reconciler then falls back to the reference span.
468
+ */
469
+ type BoxRateResolver = (record: SpendBoxRecord | null, sandboxId: string) => number | null | undefined;
470
+ interface ReconcileSpendOptions {
471
+ /**
472
+ * Settled ledger rows, supplied by the product's own authenticated fetch.
473
+ *
474
+ * MUST be scoped to boxes this product owns. Products bill to a shared company
475
+ * key, so an unscoped fetch returns every sibling product's settlements and
476
+ * every one is a correct — and useless — `unknown-box` finding.
477
+ */
478
+ readonly rows: readonly SettlementRow[];
479
+ /** The product's expectation ledger. */
480
+ readonly store: SpendLedgerStorePort;
481
+ /** Treated as "now". Default `Date.now()`. */
482
+ readonly asOf?: number;
483
+ /** Ceiling slack. Default {@link DEFAULT_CEILING_TOLERANCE_MS}. */
484
+ readonly toleranceMs?: number;
485
+ /** Box price, for the exact duration basis. A number applies to every box. */
486
+ readonly nanoUsdPerHour?: number | BoxRateResolver;
487
+ /** Velocity tuning, or `false` to skip the rule. */
488
+ readonly velocity?: VelocityOptions | false;
489
+ /** The workspace balance, when the product can see one. Omitted skips the rule. */
490
+ readonly balance?: ObservedBalance;
491
+ /** Stamped onto findings so an alert names the tenant. */
492
+ readonly workspaceId?: string;
493
+ /** Checks to leave out of this pass. */
494
+ readonly skip?: readonly SpendCheckId[];
495
+ }
496
+ /**
497
+ * Diff what the platform charged against what the product believes it asked for.
498
+ *
499
+ * Never disputes anything and never writes: the output is a report a human acts
500
+ * on. The platform's ledger stays authoritative — this only ever produces the
501
+ * evidence for a conversation with it.
502
+ */
503
+ declare function reconcileSpend(options: ReconcileSpendOptions): Promise<SpendReport>;
504
+
505
+ /** Why provisioning was refused, with every number the decision used. */
506
+ interface ComputeBudgetRefusal {
507
+ readonly workspaceId: string;
508
+ /** The cap, unsigned nanodollars. */
509
+ readonly limitNanoUsd: number;
510
+ /** Cumulative settled compute spend for this workspace, unsigned nanodollars. */
511
+ readonly settledNanoUsd: number;
512
+ /** How far past the cap it already is. */
513
+ readonly overageNanoUsd: number;
514
+ readonly at: number;
515
+ }
516
+ /**
517
+ * Provisioning refused because the workspace is already past its compute cap.
518
+ *
519
+ * Correctable by design: every number the decision used is on the error, so a
520
+ * product can render "this workspace has spent $X of its $Y compute budget" and
521
+ * an operator can raise the cap or investigate without reading logs.
522
+ *
523
+ * This is the failure mode the module exists to produce. A platform billing
524
+ * defect that used to end in a silent negative balance now ends in provisioning
525
+ * stopping and something loud happening instead.
526
+ */
527
+ declare class ComputeBudgetExceededError extends Error {
528
+ readonly workspaceId: string;
529
+ readonly limitNanoUsd: number;
530
+ readonly settledNanoUsd: number;
531
+ readonly overageNanoUsd: number;
532
+ constructor(refusal: ComputeBudgetRefusal);
533
+ }
534
+ /**
535
+ * A per-workspace cap on sandbox compute.
536
+ *
537
+ * `/billing`'s budget primitive caps MODEL keys, and it works because the
538
+ * platform enforces the cap at the key it minted. Sandbox compute has no such
539
+ * key: a box bills the shared company wallet, so nothing upstream refuses. This
540
+ * carries the same shape to the one place a consumer can still act — the moment
541
+ * before it asks for another box.
542
+ *
543
+ * `settledNanoUsd` is a callback rather than a number because the authority is
544
+ * the platform ledger, not this package: the product reads the same rows it
545
+ * hands the reconciler. Cache it if the read is expensive; a cap is a
546
+ * coarse-grained control and a slightly stale total still refuses.
547
+ */
548
+ interface ComputeBudget {
549
+ /** The cap, unsigned nanodollars. */
550
+ readonly limitNanoUsd: number;
551
+ /** Cumulative settled compute spend for the workspace, unsigned nanodollars. */
552
+ readonly settledNanoUsd: (workspaceId: string) => Promise<number> | number;
553
+ /**
554
+ * Called on every refusal, before the error is thrown. This is the alert
555
+ * seam: a refusal nobody hears is a product that silently stopped working.
556
+ */
557
+ readonly onRefusal?: (refusal: ComputeBudgetRefusal) => void;
558
+ /** Injectable clock (epoch ms). Default `Date.now`. */
559
+ readonly now?: () => number;
560
+ }
561
+ /**
562
+ * Throw {@link ComputeBudgetExceededError} when the workspace is already past
563
+ * its cap. Returns normally — and reads nothing — when no budget is configured.
564
+ *
565
+ * Deliberately a pre-check against spend ALREADY SETTLED, not a reservation
566
+ * against spend about to happen: settlement lags provisioning by design (the
567
+ * platform's durable settlement queue), so there is no instant at which a
568
+ * consumer could hold an accurate running total. The cap therefore overshoots by
569
+ * at most the unsettled tail, which is bounded by the box's own idle timeout.
570
+ * A cap that refuses one box late is worth far more than one that cannot be
571
+ * implemented honestly.
572
+ */
573
+ declare function assertComputeBudget(budget: ComputeBudget | undefined, workspaceId: string): Promise<void>;
574
+ /**
575
+ * What `/sandbox` reports once a box is provisioned, reused or resumed.
576
+ *
577
+ * Structurally identical to `SandboxProvisionedObservation` in `/sandbox`, and
578
+ * deliberately re-declared rather than imported: `/spend` composes `/sandbox`,
579
+ * so a type import in the other direction would invert the dependency. The two
580
+ * are pinned together by a compile-time assignment in this module's tests.
581
+ */
582
+ interface SpendProvisionObservation {
583
+ readonly workspaceId: string;
584
+ readonly userId?: string;
585
+ readonly sandboxId: string;
586
+ readonly boxKey?: string | undefined;
587
+ readonly idleTimeoutSeconds: number;
588
+ readonly maxLifetimeSeconds?: number | undefined;
589
+ readonly at: number;
590
+ }
591
+ /**
592
+ * The optional seam `EnsureWorkspaceSandboxOptions.spend` and the turn
593
+ * primitives' `spend` option both accept. One object, wired in both places.
594
+ */
595
+ interface SandboxSpendSeam {
596
+ beforeProvision?(input: {
597
+ workspaceId: string;
598
+ userId?: string;
599
+ }): Promise<void> | void;
600
+ onProvisioned?(observation: SpendProvisionObservation): Promise<void> | void;
601
+ /** Synchronous by contract — it sits on the turn path. See `createSandboxSpendHooks`. */
602
+ onActivity?(input: {
603
+ sandboxId: string;
604
+ at: number;
605
+ }): void;
606
+ }
607
+ interface SandboxSpendHooksOptions {
608
+ /** Records box lifecycle. Omit to run the budget guard alone. */
609
+ readonly ledger?: SpendLedger;
610
+ /** Refuses provisioning past a cap. Omit to record alone. */
611
+ readonly budget?: ComputeBudget;
612
+ /**
613
+ * Called when RECORDING fails. Recording is best-effort — a bookkeeping
614
+ * failure must never take down the provisioning it is bookkeeping — so this
615
+ * is the only place such a failure is visible. A refusal is NOT routed here;
616
+ * refusals throw, by design.
617
+ */
618
+ readonly onError?: (error: unknown) => void;
619
+ }
620
+ /**
621
+ * Build the object to hand `ensureWorkspaceSandbox`'s `spend` option.
622
+ *
623
+ * Wiring it is the entire adoption cost: one field, and the product's boxes are
624
+ * both budget-capped and recorded.
625
+ */
626
+ declare function createSandboxSpendHooks(options: SandboxSpendHooksOptions): SandboxSpendSeam;
627
+
628
+ /**
629
+ * Render a reconciliation for a human deciding whether to open a dispute.
630
+ *
631
+ * Every finding prints its numbers, not a summary of them: the reader's next
632
+ * action is a conversation with the platform about specific reference ids, and a
633
+ * report that made them re-derive the durations would just be re-read alongside
634
+ * the raw rows anyway.
635
+ */
636
+ declare function formatSpendReport(report: SpendReport): string;
637
+ /** The report as a plain JSON value, for an alerting pipeline. */
638
+ declare function spendReportToJson(report: SpendReport): string;
639
+
640
+ export { type BilledDurationBasis, type BoxRateResolver, type CeilingBasis, type ComputeBudget, ComputeBudgetExceededError, type ComputeBudgetRefusal, type ComputeExpectedCeilingOptions, DEFAULT_CEILING_TOLERANCE_MS, type ExpectedCeiling, type InMemorySpendLedgerStore, type ObserveSandboxInput, type ObservedBalance, type ReconcileSpendOptions, SPEND_CHECKS, type SandboxSpendHooksOptions, type SandboxSpendSeam, type SettlementReference, type SettlementRow, type SpendBoxPatch, type SpendBoxRecord, type SpendCheckId, type SpendFinding, type SpendLedger, type SpendLedgerOptions, type SpendLedgerStorePort, type SpendProvisionObservation, type SpendReport, type VelocityOptions, assertComputeBudget, chargeNanoUsd, computeExpectedCeiling, createInMemorySpendLedgerStore, createSandboxSpendHooks, createSpendLedger, foldSpendBoxRecord, formatSpendReport, isCharge, parseSandboxGroupKey, parseSettlementReference, reconcileSpend, settlementSandboxId, spendReportToJson };