otto-intel-mcp 0.1.2 → 0.1.3

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 (51) hide show
  1. package/README.md +4 -2
  2. package/dist/adapter/cdp-signer.d.ts +44 -5
  3. package/dist/adapter/cdp-signer.js +168 -18
  4. package/dist/adapter/evm-call-failure.d.ts +29 -0
  5. package/dist/adapter/evm-call-failure.js +109 -0
  6. package/dist/adapter/refusal.d.ts +1 -1
  7. package/dist/adapter/refusal.js +13 -0
  8. package/dist/adapter/route-simulator.d.ts +91 -0
  9. package/dist/adapter/route-simulator.js +143 -0
  10. package/dist/executable-equities.d.ts +55 -0
  11. package/dist/executable-equities.js +116 -0
  12. package/dist/executable-yield-markets.d.ts +261 -0
  13. package/dist/executable-yield-markets.js +154 -0
  14. package/dist/execution-delegated-definition.d.ts +1 -0
  15. package/dist/execution-delegated-definition.js +13 -2
  16. package/dist/execution-delegation-admin-definition.d.ts +126 -5
  17. package/dist/execution-delegation-admin-definition.js +154 -5
  18. package/dist/execution-delegation-admin.d.ts +48 -2
  19. package/dist/execution-delegation-admin.js +314 -12
  20. package/dist/execution-delegation-cap-store.d.ts +369 -0
  21. package/dist/execution-delegation-cap-store.js +1083 -0
  22. package/dist/execution-delegation-policy.d.ts +153 -6
  23. package/dist/execution-delegation-policy.js +260 -16
  24. package/dist/execution-delegation.d.ts +35 -0
  25. package/dist/execution-delegation.js +224 -9
  26. package/dist/execution-errors.d.ts +1 -1
  27. package/dist/execution-errors.js +98 -0
  28. package/dist/execution-index.d.ts +3 -2
  29. package/dist/execution-index.js +3 -2
  30. package/dist/execution-registration.d.ts +51 -2
  31. package/dist/execution-registration.js +45 -2
  32. package/dist/execution-tools.d.ts +44 -1
  33. package/dist/execution-tools.js +157 -13
  34. package/dist/lifi-execution-client.d.ts +6 -0
  35. package/dist/lifi-execution-client.js +3 -0
  36. package/dist/stock-buy-definition.d.ts +338 -0
  37. package/dist/stock-buy-definition.js +127 -0
  38. package/dist/stock-buy-oracle.d.ts +8 -0
  39. package/dist/stock-buy-oracle.js +22 -0
  40. package/dist/stock-buy.d.ts +17 -0
  41. package/dist/stock-buy.js +164 -0
  42. package/dist/tool-definitions.js +1 -1
  43. package/dist/yield-deposit-definition.d.ts +323 -0
  44. package/dist/yield-deposit-definition.js +121 -0
  45. package/dist/yield-deposit.d.ts +16 -0
  46. package/dist/yield-deposit.js +141 -0
  47. package/dist/yield-withdraw-definition.d.ts +306 -0
  48. package/dist/yield-withdraw-definition.js +88 -0
  49. package/dist/yield-withdraw.d.ts +21 -0
  50. package/dist/yield-withdraw.js +120 -0
  51. package/package.json +28 -4
@@ -0,0 +1,369 @@
1
+ /**
2
+ * execution-delegation-cap-store.ts — the durable, TRANSACTIONAL state machine behind a person's chosen
3
+ * per-swap cap (Founder word `delegate cap choice`, 2026-09-02; ROUND 5, after four codex NO-GOs).
4
+ *
5
+ * ══════════════════════════════════════════════════════════════════════════════════════════════════
6
+ * 🔴 THE THREAT MODEL THIS ENFORCES (Founder ruling, 2026-09-03, verbatim)
7
+ * ══════════════════════════════════════════════════════════════════════════════════════════════════
8
+ * "The per-person delegation cap protects every delegated send that goes through Otto's page
9
+ * (/app/delegate) and Otto's CLI (otto-execute delegate). A permission a person mints or revokes
10
+ * directly against Coinbase with their own login, outside Otto's page and CLI, is OUT OF SCOPE: Otto
11
+ * cannot distinguish it (CDP returns no grant identity, only an expiry) and does not defend against it;
12
+ * the CDP project-scope $5,000 ceiling remains the hard backstop for every permission. The delegated
13
+ * agent never holds the person's login."
14
+ *
15
+ * Everything below is built to be correct INSIDE that model. Where a defect is only reachable by a
16
+ * direct-to-Coinbase mint or revoke, the behaviour is stated rather than defended — and every one of
17
+ * those paths still fails CLOSED here (an unrecorded or unconfirmed grant authorises nothing at all),
18
+ * so the out-of-scope actor can shorten what Otto may do, never widen it.
19
+ *
20
+ * ══════════════════════════════════════════════════════════════════════════════════════════════════
21
+ * 🔴 WHY THE MACHINE, AND WHY IT IS SQLITE
22
+ * ══════════════════════════════════════════════════════════════════════════════════════════════════
23
+ * Round 3 made the expiry unique among ROWS and thought that was enough. The gate's two CRITICALs:
24
+ *
25
+ * "The durable row still authenticates an expiry, not a grant. Enforcement requires only
26
+ * (userId, grant.expiresAt); it does not require a successful confirmation, the reservation's
27
+ * mintKey, or an active-grant pointer."
28
+ * "Confirmation is not an enforcement gate. Reserve writes the complete enforceable capability;
29
+ * confirm writes no state at all."
30
+ *
31
+ * Both are the same defect: a reserved row was a BEARER CAPABILITY keyed by an instant. Round 4 took the
32
+ * gate's own minimum safe model —
33
+ *
34
+ * reserved ──confirm──▶ confirmed_active ──┬──▶ superseded (a later confirm won)
35
+ * │ ├──▶ revoked (the person, or a mismatch)
36
+ * └──10 min──▶ expired └──▶ (expiry passes)
37
+ *
38
+ * — and made ONE thing authoritative: `readActive`. A reserved row authorises NOTHING; the send gate
39
+ * reads the confirmed-active record and requires its expiry to equal the live grant's.
40
+ *
41
+ * Round 4 implemented that on filesystem primitives (`open(…,'wx')` + `rename()`), and round 4's gate
42
+ * retired them: *"`rename()` gives atomic visibility of one pointer, not durability or a multi-file
43
+ * transaction: there is no file/directory `fsync`, and confirmation performs three writes … There is
44
+ * also no confirm CAS/serialization. My concurrent-confirm probe returned both calls as `confirmed` and
45
+ * left both reservation files `confirmed_active`, although only one `active.json` won."* Its prescription
46
+ * was explicit: *"Use a transactional shared store with unique `(userId, expiry)` and `(userId, mintKey)`
47
+ * constraints, an active-grant CAS, and atomic confirm/revoke transitions. A database transaction is the
48
+ * simpler safe implementation."*
49
+ *
50
+ * So the primitives are now SQL, on one database file on the mounted Railway volume:
51
+ *
52
+ * · `PRIMARY KEY (user_id, mint_key)` — the mint key is claimed exactly once. Idempotent reserve.
53
+ * · `UNIQUE (user_id, expires_at_ms)` — the reservation instant is claimed exactly once, ACROSS
54
+ * processes, by the database rather than by a directory entry.
55
+ * · `CHECK (state IN …)` — a record can never hold a state this binary does not know.
56
+ * · `active_grants` — ONE ROW PER USER, the only pointer enforcement reads, moved by a
57
+ * COMPARE-AND-SWAP (`UPDATE … WHERE mint_key = <prior> AND expires_at_ms = <prior>`) inside the
58
+ * same transaction that promotes the reservation.
59
+ * · Every transition is a CONDITIONAL UPDATE whose `changes` count decides the winner
60
+ * (`UPDATE … WHERE state = 'reserved'`), so a concurrent double confirm has exactly one winner and
61
+ * the loser is told it was superseded — it is not a race that both sides win.
62
+ * · `BEGIN IMMEDIATE` takes the write lock before the first read, so a reserve/confirm/revoke is
63
+ * serialized against every other writer on the same file, in this process or another.
64
+ * · `journal_mode = WAL`, `synchronous = FULL` — a committed transition survives a crash and a power
65
+ * loss, which is the durability `rename()` did not give.
66
+ *
67
+ * ⚠️ THE PREMISE, STATED. One database file, reached by every worker: `numReplicas: 1` and one mounted
68
+ * volume. SQLite's locking is per file, so two volumes would be two independent stores — and two
69
+ * independent stores can each confirm a different cap for one person, which is the one shape that could
70
+ * widen a permission from inside the model. Nothing inside a process can check this; the deploy does
71
+ * (`railway.json`), and the staged first rollout below is what proves it before minting is even possible.
72
+ *
73
+ * ══════════════════════════════════════════════════════════════════════════════════════════════════
74
+ * 🔴 WHY `node:sqlite` AND NOT `better-sqlite3`
75
+ * ══════════════════════════════════════════════════════════════════════════════════════════════════
76
+ * `better-sqlite3` installs fine on this image (verified: a prebuilt linux-x64 / Node 22 binary, no
77
+ * compile). It is not a Railway problem. It is a PUBLISHED-PACKAGE problem: `otto-intel-mcp` ships to npm
78
+ * and is run as `npx otto-intel-mcp`, and `services/otto-execute` vendors it file-for-file at build time.
79
+ * A hard native dependency becomes a node-gyp source build wherever no prebuild matches the platform
80
+ * (musl/Alpine, a newer Node ABI, some ARM builds) and fails the install for every user of the CLI,
81
+ * including the majority who never touch delegation. `node:sqlite` is in the Node standard library at
82
+ * this package's own engine floor (`node >= 22`), needs no build step, and gives the identical
83
+ * primitives: `BEGIN IMMEDIATE` transactions, UNIQUE/CHECK constraints, conditional UPDATE with a
84
+ * `changes` count, WAL and `synchronous = FULL`.
85
+ *
86
+ * It is imported LAZILY and the failure is named: a runtime without `node:sqlite` opens an UNAVAILABLE
87
+ * store, and every delegated send then refuses by name rather than falling back to anything.
88
+ *
89
+ * 🔴 IT FAILS CLOSED, LOUDLY. No durable path configured, a database that cannot be opened, or a record
90
+ * that cannot be parsed means every delegated send refuses. An unreadable store is NOT an empty one, and
91
+ * the difference decides whether a person's cap is honoured.
92
+ *
93
+ * Nothing here logs a token, an address or a credential. A record is a user id, a mint key, two instants,
94
+ * a number, a version and a state.
95
+ */
96
+ /** The explicit path override; otherwise the mounted Railway volume is used. */
97
+ export declare const DELEGATION_CAP_STORE_PATH_ENV: "OTTO_DELEGATION_CAP_STORE_PATH";
98
+ /** The database file inside that directory. The directory is kept so a legacy import can still read it. */
99
+ export declare const CAP_STORE_DB_FILE: "caps.db";
100
+ /**
101
+ * 🔴 THE STAGED-DEPLOY GATE (gate finding, CRITICAL 2 — the first rolling deployment).
102
+ *
103
+ * "The ruleset handshake cannot fence the first rolling deployment. The version refusal exists only in
104
+ * the new binary … status/reserve/confirm may hit a new worker while prepare/submit hits a still-serving
105
+ * old worker, which cannot interpret `requiredVersion` and executes anyway."
106
+ *
107
+ * The handshake cannot fix that, because the binary that must refuse is the one that has never heard of
108
+ * it. A DEPLOYMENT SEQUENCE can, and this flag is what makes the sequence possible:
109
+ *
110
+ * 1. deploy with `DELEGATION_MINT_ENABLED` unset or false — no new permission can be created at all,
111
+ * so there is no cap-bearing grant for an old worker to execute at the ceiling;
112
+ * 2. confirm exactly one cap-aware worker is serving (`/health` reports `rulesetVersion`);
113
+ * 3. set the flag true and redeploy.
114
+ *
115
+ * DEFAULT FALSE IN CODE. An operator who forgets the variable gets the paused surface, never an
116
+ * unfenced first rollout. Sends under permissions that are ALREADY confirmed keep working under their
117
+ * own cap, and REVOKE always works — pausing the creation of authority must never pause its withdrawal.
118
+ */
119
+ export declare const DELEGATION_MINT_ENABLED_ENV: "DELEGATION_MINT_ENABLED";
120
+ /** True only for an explicit affirmative value. Absent, empty, or anything else is FALSE. */
121
+ export declare function resolveDelegationMintEnabled(env?: NodeJS.ProcessEnv): boolean;
122
+ /** The one sentence every paused surface says. No security narration: it is an update, and it is short. */
123
+ export declare const DELEGATION_MINT_PAUSED_MESSAGE: "Delegation is paused for a few minutes while we update it";
124
+ /**
125
+ * The shape a mint key must have: what `createDelegation({ idempotencyKey })` accepts, bounded so it
126
+ * cannot be used as a path or a smuggling channel. A UUID satisfies it.
127
+ */
128
+ export declare const MINT_KEY_PATTERN: RegExp;
129
+ /** A CDP end-user id, as every schema on this surface bounds it. */
130
+ export declare const END_USER_ID_PATTERN: RegExp;
131
+ /**
132
+ * How far back the offset ladder may walk, in whole seconds, before it refuses.
133
+ *
134
+ * Five minutes of headroom on a grant measured in days: invisible to the reader, far more collisions
135
+ * than a real person can produce, and bounded so a corrupt or adversarially stuffed table cannot turn a
136
+ * request into one that never returns.
137
+ */
138
+ export declare const CAP_EXPIRY_MAX_OFFSET_STEPS: 300;
139
+ /**
140
+ * 🔴 A RESERVATION THAT IS NEVER CONFIRMED DIES (seat ruling, round 4). Ten minutes is long enough for a
141
+ * person to finish an OTP sign-in and a mint, and short enough that an abandoned reservation stops being
142
+ * something a later actor could try to confirm. Its SECOND stays taken — releasing it would let a
143
+ * different permission land on an instant this one may already have minted at.
144
+ */
145
+ export declare const CAP_RESERVATION_WINDOW_MS: number;
146
+ /**
147
+ * The version of the CAP RULESET this binary implements — distinct from the fence policy's version,
148
+ * which describes what Coinbase enforces.
149
+ *
150
+ * 🔴 WHY IT EXISTS (gate finding, HIGH — rolling deploy). "An older worker that accepts the same fence
151
+ * but lacks per-user cap enforcement can execute a new $10 permission up to the engine ceiling." The
152
+ * fence is identical on both binaries, so the fence cannot tell them apart. Every record carries the
153
+ * ruleset version it REQUIRES, a worker refuses a record it is too old to honour, and the page and the
154
+ * CLI refuse to reserve or mint against a server that reports a version below this one.
155
+ *
156
+ * 🔴 AND IT IS DEFENCE IN DEPTH, NOT THE FENCE FOR THE FIRST ROLLOUT — that is
157
+ * `DELEGATION_MINT_ENABLED` above, because a binary that predates the handshake cannot honour it.
158
+ */
159
+ /** v2 (fence v2, 2026-09-03): a permission minted under v2 consents to Yield Copilot vault WITHDRAWS as well as swaps/deposits. */
160
+ export declare const MODEL_B_CAP_RULESET_VERSION: 2;
161
+ /** The lifecycle of one permission, as the gate prescribed it, plus the one state a MIGRATION writes. */
162
+ export declare const CAP_RECORD_STATES: readonly ["reserved", "confirmed_active", "superseded", "revoked", "expired", "legacy"];
163
+ export type CapRecordState = (typeof CAP_RECORD_STATES)[number];
164
+ /** One durable record. A row in `reservations`; `active_grants` points at the live one. */
165
+ export interface DelegationCapRecord {
166
+ readonly userId: string;
167
+ /** The `idempotencyKey` the permission is minted with. Names the reservation; authorises nothing. */
168
+ readonly mintKey: string;
169
+ /** The WHOLE-SECOND instant CDP will echo — the value every enforcement read compares against. */
170
+ readonly expiresAt: string;
171
+ /** What goes on the wire: the same second with milliseconds in 1..999, because CDP refuses `.000`. */
172
+ readonly requestedExpiresAt: string;
173
+ readonly perSwapCapUsd: number;
174
+ /** The cap-ruleset version a worker must implement to honour this record. */
175
+ readonly requiredVersion: number;
176
+ /**
177
+ * 🔴 THE REVOKE GENERATION THIS RESERVATION WAS MADE UNDER (gate finding, r4: "Add a durable revoke
178
+ * generation so Stop cancels already-reserved/in-flight mints without permanently poisoning later
179
+ * reservations"). Every Stop bumps the person's generation; a confirm whose reservation carries an
180
+ * older one is CANCELLED rather than promoted. That is how pressing Stop while a mint is in flight
181
+ * actually stops it — the old design read no grant yet, marked nothing, and the mint landed anyway.
182
+ *
183
+ * It poisons nothing: a reservation made AFTER the Stop carries the new generation and confirms
184
+ * normally, which is exactly what a per-user sticky marker could not do.
185
+ */
186
+ readonly revokeGeneration: number;
187
+ readonly state: CapRecordState;
188
+ readonly reservedAt: string;
189
+ readonly confirmedAt?: string;
190
+ readonly endedAt?: string;
191
+ }
192
+ export interface DelegationCapReservation {
193
+ readonly outcome: 'reserved' | 'already_reserved';
194
+ readonly record: DelegationCapRecord;
195
+ }
196
+ export type DelegationCapConfirm = {
197
+ readonly outcome: 'confirmed';
198
+ readonly record: DelegationCapRecord;
199
+ }
200
+ /** Already confirmed and still the live one: a retry after a lost response. */
201
+ | {
202
+ readonly outcome: 'already_confirmed';
203
+ readonly record: DelegationCapRecord;
204
+ }
205
+ /** Nothing was reserved for this mint key. */
206
+ | {
207
+ readonly outcome: 'unreserved';
208
+ }
209
+ /** The reservation exists but its ten minutes passed, or it was imported, or it was already ended. */
210
+ | {
211
+ readonly outcome: 'not_reservable';
212
+ readonly state: CapRecordState;
213
+ }
214
+ /** It WAS confirmed and a later confirmation replaced it — never revived. */
215
+ | {
216
+ readonly outcome: 'superseded';
217
+ }
218
+ /** The person pressed Stop while this permission was being created. Never promoted. */
219
+ | {
220
+ readonly outcome: 'cancelled';
221
+ }
222
+ /** The live grant's instant is not the reserved one. */
223
+ | {
224
+ readonly outcome: 'expiry_mismatch';
225
+ readonly record: DelegationCapRecord;
226
+ };
227
+ /** What one official Stop did, durably: the generation now in force and the record it took away. */
228
+ export interface DelegationCapStop {
229
+ readonly generation: number;
230
+ /** The instant of the record this Stop moved to `revoked`; absent when nothing was confirmed-active. */
231
+ readonly revokedExpiresAt?: string;
232
+ }
233
+ export declare class DelegationCapExpiryUnavailableError extends Error {
234
+ constructor(steps: number);
235
+ }
236
+ export interface DelegationCapStore {
237
+ /** False when no durable path is configured, or the database could not be opened. Sends refuse while false. */
238
+ readonly available: boolean;
239
+ readonly unavailableReason?: string;
240
+ /**
241
+ * Reserve the first whole second at or BEFORE `latestExpiresAt` that no reservation for this user
242
+ * occupies, and record the cap against it. Idempotent per mint key. THROWS when the store cannot be
243
+ * written; `DelegationCapExpiryUnavailableError` when the ladder is exhausted.
244
+ */
245
+ reserve(userId: string, mintKey: string, latestExpiresAt: string, perSwapCapUsd: number, nowMs: number): Promise<DelegationCapReservation>;
246
+ /** The reservation this mint key holds, with its state as of `nowMs` (the window applied). */
247
+ readReservation(userId: string, mintKey: string, nowMs: number): Promise<DelegationCapRecord | undefined>;
248
+ /**
249
+ * `reserved → confirmed_active`, ONE-SHOT, in a single transaction that also supersedes whatever was
250
+ * confirmed-active for this user before. A concurrent second confirm loses the conditional UPDATE and
251
+ * is answered `superseded` or `already_confirmed` — never a second winner.
252
+ */
253
+ confirm(userId: string, mintKey: string, grantExpiresAt: string, nowMs: number): Promise<DelegationCapConfirm>;
254
+ /**
255
+ * 🔴 THE SEND GATE'S ONLY READ. The confirmed-active record for this user, or undefined. A reserved,
256
+ * superseded, revoked, expired or legacy record is NEVER returned here — that is what makes a
257
+ * reservation unable to authorise anything. THROWS when the store cannot be read.
258
+ */
259
+ readActive(userId: string, nowMs: number): Promise<DelegationCapRecord | undefined>;
260
+ /**
261
+ * Mark the confirmed-active record for this user REVOKED, but only when it is the grant at
262
+ * `expiresAt`. Per-grant by construction: a revoke aimed at a permission that is no longer the live
263
+ * one changes nothing, which is what stops a stale tab taking away a permission it never held.
264
+ *
265
+ * 🔴 THE REVOKED ROW STAYS, AND ITS SECOND STAYS CONSUMED. That is the "consumed-expiry state" the
266
+ * gate asked for: after a revoke, no later grant can be minted at that instant through Otto's page or
267
+ * CLI (the ladder skips it), and one minted there out-of-model matches no confirmed record, so it
268
+ * authorises nothing.
269
+ */
270
+ revoke(userId: string, expiresAt: string, nowMs: number): Promise<boolean>;
271
+ /**
272
+ * 🔴 STOP CANCELS WHAT IS ALREADY IN FLIGHT. Bump this person's revoke generation, so any reservation
273
+ * made before now can never be confirmed. It is not a poison: it is a monotonic counter, and the next
274
+ * reservation carries the new value. Returns the generation now in force.
275
+ *
276
+ * ⚠️ NOT THE OFFICIAL STOP. Every official revoke goes through `stop` below, which does this AND the
277
+ * record transition in ONE transaction. This stays on the interface for the one-shot cancel case a
278
+ * test drives directly; nothing in the admin path calls it on its own any more.
279
+ */
280
+ bumpRevokeGeneration(userId: string, nowMs: number): Promise<number>;
281
+ /**
282
+ * ══════════════════════════════════════════════════════════════════════════════════════════════
283
+ * 🔴 THE OFFICIAL STOP — ONE TRANSACTION, TWO WRITES, NEVER ONE WITHOUT THE OTHER (gate r5 CRITICAL)
284
+ * ══════════════════════════════════════════════════════════════════════════════════════════════
285
+ * "`revokeDelegation` commits the generation bump and then performs the active-record revoke in a
286
+ * separate transaction … The process dies or stalls before `capStore.revoke()` … A fresh worker still
287
+ * reads the old record as `confirmed_active`; the unchanged live grant matches and a send remains
288
+ * admissible."
289
+ *
290
+ * So the Stop is one `BEGIN IMMEDIATE` transaction that (1) bumps the revoke generation — cancelling
291
+ * every reservation taken before now, including a mint already in flight with no grant to find yet —
292
+ * and (2) moves whatever record is confirmed-active for this person to `revoked`. A crash between the
293
+ * two is a crash INSIDE the transaction: nothing is committed, the caller is told to retry, and nothing
294
+ * is revoked at Coinbase either. The revoked row survives and holds its second (the consumed expiry).
295
+ *
296
+ * It takes NO expiry, deliberately. A Stop means "Otto acts under nothing for me": the record that is
297
+ * live goes, whatever tab pressed the button and whatever grant Coinbase happens to hold — exactly what
298
+ * the CDP-side revoke that follows it does. `revoke` above stays per grant for the mismatch path, where
299
+ * the thing being taken away IS one specific grant.
300
+ *
301
+ * Answers the generation now in force and the instant of the record it revoked, if any.
302
+ */
303
+ stop(userId: string, nowMs: number): Promise<DelegationCapStop>;
304
+ /** Release the database handle. Present on the real store; tests call it, production never needs to. */
305
+ close?(): void;
306
+ }
307
+ export declare function resolveDelegationCapStorePath(env?: NodeJS.ProcessEnv): string | null;
308
+ /** The instant `ms` floored to a whole second — the only granularity CDP preserves. */
309
+ export declare function wholeSecondMs(ms: number): number;
310
+ /**
311
+ * The value that goes ON THE WIRE for a reserved second.
312
+ *
313
+ * 🔴 CDP REFUSES a `.000` fraction — 401 `unauthorized` "Wallet authentication error", a code that says
314
+ * nothing about the cause (isolated live on one session, 2026-09-02: `.000Z` refused, `.137Z` accepted
315
+ * on the very next call, `.000Z` refused again). And it TRUNCATES the fraction when it stores the grant
316
+ * (`…:46.001Z` → `…:46Z`, `…:45.999Z` → `…:45Z`), so any millisecond in 1..999 lands on the reserved
317
+ * second and none of them lengthens the permission.
318
+ */
319
+ export declare const MINT_EXPIRY_FILLER_MS: 1;
320
+ export declare function mintExpiryForSecond(wholeSecondIso: string): string;
321
+ /**
322
+ * The offset ladder as a pure function: the first whole second at or BEFORE `latestMs` that `taken`
323
+ * does not hold and that is still in the future, or undefined. It NEVER returns anything later than
324
+ * `latestMs` — a permission must not outlive the duration the person chose, not even by a second.
325
+ */
326
+ export declare function firstFreeExpiryMs(latestMs: number, nowMs: number, taken: (candidateMs: number) => boolean, maxSteps?: number): number | undefined;
327
+ /** A record's state as of `nowMs`: a reservation whose window has passed reads as `expired`. */
328
+ export declare function stateAsOf(record: DelegationCapRecord, nowMs: number): CapRecordState;
329
+ /**
330
+ * The two pragmas the durability claim rests on, read back FROM THE CONNECTION THAT SET THEM.
331
+ *
332
+ * 🔴 `synchronous` IS PER CONNECTION. A test that opens its own handle to the same file always reads the
333
+ * default, so an assertion made that way passes whatever this store did — the mutation that dropped
334
+ * `synchronous` to OFF survived exactly one run because of it. `journal_mode` is persisted in the file;
335
+ * this one is not, so the store has to say what it applied.
336
+ */
337
+ export interface AppliedPragmas {
338
+ readonly journalMode: string;
339
+ readonly synchronous: number;
340
+ }
341
+ /**
342
+ * Everything a pre-SQLite store left in `root`: the round-1/2 append-only JSONL, and the round-4
343
+ * per-instant JSON files under `u/<userId>/r/`. Read best-effort — a migration that THREW on an
344
+ * unreadable leftover would stop the store opening at all, and the leftovers authorise nothing either
345
+ * way.
346
+ */
347
+ export declare function readLegacyCapRecords(root: string): Promise<DelegationCapRecord[]>;
348
+ /** A store that is present but permanently unavailable — what an unconfigured deploy gets. */
349
+ export declare function unavailableDelegationCapStore(reason: string): DelegationCapStore;
350
+ /**
351
+ * Open the SQLite store at `root`, importing anything a pre-SQLite store left behind as `legacy`.
352
+ * Exported so a test can open one on a temp directory without going through the environment.
353
+ */
354
+ export declare function openDelegationCapStore(root: string): Promise<DelegationCapStore>;
355
+ /**
356
+ * The store for this process, from the environment. Unconfigured is NOT a silent in-memory fallback: it
357
+ * is an unavailable store, so the delegated send refuses by name and the ops fix (mount the volume, or
358
+ * set `OTTO_DELEGATION_CAP_STORE_PATH`) is the thing the refusal asks for.
359
+ */
360
+ export declare function createDelegationCapStore(env?: NodeJS.ProcessEnv): Promise<DelegationCapStore>;
361
+ /**
362
+ * An in-memory store — for TESTS and for a local stdio run, where nothing is durable anyway.
363
+ *
364
+ * 🔴 IT IMPLEMENTS THE SAME STATE MACHINE, and the suite runs the SAME conformance cases against both,
365
+ * so a test cannot pass against semantics production does not have. What it cannot model is the
366
+ * cross-process exclusion and the crash durability SQLite provides — those are asserted against a real
367
+ * database file, and against two real processes, only.
368
+ */
369
+ export declare function inMemoryDelegationCapStore(seed?: readonly DelegationCapRecord[]): DelegationCapStore;