/** * In-memory persistence shims for the adapter loop. * * Two stores live here: * - **DeferRedis + ParkRedis** — implements the combined runtime persistence * surface so `parkDeferredIntent` (write) and `resumeDeferredIntent` * (read + idempotent claim) both work against a single backing object. * Production wires real Redis; the in-memory shim is for tests + the * quickstart. * * - **ConfirmationStore** — separate by design. DEFER persists by * `(session, intentHash)`; REQUEST_CONFIRMATION persists by a * user-held token (the user clicks "yes/no" at an arbitrary later time). * Conflating them muddles both shapes. * * The persistence layer is provider-neutral — the `assistantHistorySnapshot` * on pending confirmations is typed as a generic `H` so adapters thread * their SDK's conversation-history shape through unchanged. */ import type { Capability, IntentEnvelope, SessionContamination } from "@adjudicate/core"; /** * Read + claim surface used by `resumeDeferredIntent`. Mirrors the * `DeferRedis` interface in `@adjudicate/runtime`. */ export interface DeferRedis { get(key: string): Promise; set(key: string, value: string, options: { NX: true; EX: number; }): Promise; del(key: string): Promise; incr?(key: string): Promise; decr?(key: string): Promise; expire?(key: string, seconds: number): Promise; } /** * Write + counter surface used by `parkDeferredIntent`. Mirrors the * `ParkRedis` interface in `@adjudicate/runtime`. */ export interface ParkRedis { incr(key: string): Promise; decr(key: string): Promise; expire(key: string, seconds: number, mode?: "NX"): Promise; set(key: string, value: string, options: { EX: number; }): Promise; evalIncrCheck?(counterKey: string, ttlSeconds: number, max: number): Promise; } /** * Combined in-memory implementation of `DeferRedis` AND `ParkRedis`. * Suitable for tests and the quickstart. NOT suitable for production — * lacks persistence, fan-out, and cross-process coordination. */ export declare function createInMemoryDeferStore(): DeferRedis & ParkRedis; export interface PendingConfirmation { readonly envelope: IntentEnvelope; readonly sessionId: string; readonly assistantHistorySnapshot: H; readonly toolUseId: string; readonly prompt: string; } /** * Persistence for REQUEST_CONFIRMATION pauses. `take()` is get-and-delete: * a confirmation token is single-use. A repeated take after the first * resolution returns `null` (idempotent yes-then-yes). */ export interface ConfirmationStore { put(token: string, pending: PendingConfirmation, ttlSeconds: number): Promise; take(token: string): Promise | null>; } export declare function createInMemoryConfirmationStore(): ConfirmationStore; /** * Single-use capability "burn" store (022) — the AUTHORIZATION-BEARING * at-most-once redemption primitive a cap-gated executor (024) consumes. * * Unlike the lossy display-only approval registry and unlike * `ConfirmationStore` (which is a transport for a user-held token), this store * OWNS the single-use guarantee for a kernel-minted `Capability` (021): a * capability minted on EXECUTE may be redeemed EXACTLY ONCE before its executor * honors it. It mirrors the `ConfirmationStore.take` single-use contract above * — get-and-delete, idempotent yes-then-yes, TTL-expiring — but it is * authoritative, not display-only. * * **Atomicity — the headline guarantee.** `burn(nonce)` is a single * claim-and-burn primitive: the read of the bound record AND the delete that * consumes it are ONE atomic step. The in-memory reference is atomic within the * single-threaded event loop (the read+delete is synchronous between `await` * points); the Redis backing (`createRedisBurnStore`, `persistence-redis.ts`) * makes it atomic ACROSS replicas with a Lua `EVAL` get-and-delete, closing the * non-atomic GET-then-DEL double-spend race the production confirmation store * documents. A non-atomic GET-then-DEL would let two concurrent `burn(nonce)` * calls both observe the same pending grant (double-spend) — the burn store * forbids that. * * **Fail-closed (§D #6 / index §C).** A burn MISS (key absent), an EXPIRED * grant (past TTL), or a store/IO error yields NO redemption (`null` / * rejection) — never a fail-open grant. Removing the store cannot loosen any * guard: no guard authorizes on a SUCCESSFUL burn (the burn gates an executor, * not a kernel decision), so the failure mode is friction, never bypass. * * Generic over the bound record `R` (defaults to `Capability`) so 024 can store * whatever grant shape it redeems while keeping the interface reusable. */ export interface BurnStore { /** * Persist a single-use, claimable grant keyed by `nonce` with a TTL. Writing * is first-writer-wins (mirroring the `Ledger` `"acquired"`/`"exists"` * idempotent-claim, `core/src/ledger.ts`): a second `mint` of a LIVE key is * suppressed and returns `false`, so a grant cannot be silently overwritten * (which would otherwise resurrect an already-burned single-use key). Returns * `true` when the grant was claimed, `false` when the key is already live. */ mint(nonce: string, record: R, ttlSeconds: number): Promise; /** * Atomically claim-and-burn the grant bound to `nonce`. Returns the bound * record EXACTLY ONCE; every subsequent `burn(nonce)` returns `null` * (idempotent yes-then-yes, single-use). Returns `null` for an unknown nonce * and for an EXPIRED grant (fail-closed past TTL). The read and the burn are * one atomic step — no GET-then-DEL race. */ burn(nonce: string): Promise; } /** * In-memory reference `BurnStore` (022 T1). Atomic single-use within the * single-threaded event loop: `burn` reads, validates TTL, and deletes * synchronously between `await` points, so two `burn(nonce)` calls cannot both * observe the same live grant — exactly the `ConfirmationStore.take` single-use * contract, but authoritative. Opportunistic `sweepExpired` on `mint` bounds the * map without a background timer (mirrors `createInMemoryConfirmationStore`, * MemoryReviewer-005). Suitable for tests + quickstart; NOT for cross-process * production (use `createRedisBurnStore`). */ export declare function createInMemoryBurnStore(): BurnStore; /** * Authoritative, single-use-COUNTED budget burn-down store (025 — * capabilities-as-budgets). The IMPURE-shell authority over a human-granted, * BOUNDED, STANDING pre-authorization: it METERS at-most-`limit` threshold * substitutions per `intentKind` within a rolling window, so a class of intents * can satisfy the "ask first" threshold WITHOUT a per-intent confirmation * receipt — up to the declared ceiling. * * **Justified DISTINCT store (vs 022 BurnStore).** This is a deliberately * different primitive from the single-use `BurnStore`: * - `BurnStore.burn(nonce)` BURNS a single capability token (claim-and-delete, * atomic Lua GET+DEL) — at-most-ONCE. * - `BudgetStore.tryBurnDown(...)` METERS N substitutions against a `limit` * (atomic Lua INCREMENT-and-check, `evalIncrCheck`) — at-most-`limit`. * A per-token burn cannot express an N-use budget, so 022 is NOT reused; the two * are different mechanisms for different jobs, each authoritative for its own * scope. * * **Atomicity — the headline guarantee (plan §3 / §6).** `tryBurnDown` decrements * the budget via the atomic `evalIncrCheck` Lua primitive (`ParkRedis`, * `defer-park`): the increment AND the limit check are ONE indivisible step, so * concurrent burn-downs over a `limit`-N budget yield AT MOST N grants across * replicas. This deliberately does NOT mirror the non-atomic GET+DEL caveat the * production confirmation store documents (`persistence-redis.ts`) — copying that * sequence would re-introduce the over-grant race this store exists to close. * * **Authority stays OUT of the lossy projection.** The authoritative counter * lives here, in the single-use-counted store — NEVER the lossy display-only * approval registry (`approval-engine/registry-redis.ts`, which never stores the * authoritative envelope). The shell asserts a kernel budget grant ONLY after * `tryBurnDown` returns `true`. * * **Fail-closed (§D #6 / index §C).** Over-limit, an expired/refilled window * miscount, or a store/IO error yields NO grant (`false` / rejection) — the * kernel then returns the original REQUEST_CONFIRMATION (friction, never bypass). * Removing the store cannot loosen any guard: no guard authorizes on a successful * burn-down (it gates a threshold substitution, not a state/taint/auth/business * guard). */ export interface BudgetStore { /** * Atomically burn down ONE unit of the budget for `(budgetId, intentKind)` * against `limit`, expiring the counter under `windowSeconds`. Returns: * - `true` — the decrement stayed AT OR UNDER `limit`; the caller MAY assert * the kernel budget grant for this substitution. * - `false` — the budget is exhausted for this window (over `limit`), so NO * grant is asserted (fail-closed to friction). The atomic * primitive has already rolled the over-limit increment back. * * The read-modify-write is ONE atomic step (the `evalIncrCheck` Lua eval), so * concurrent calls cannot both push the counter past `limit` (at-most-`limit`). */ tryBurnDown(input: { readonly budgetId: string; readonly intentKind: string; readonly limit: number; readonly windowSeconds: number; }): Promise; } /** * Construct a `BudgetStore` backed by a `ParkRedis`-shaped client's ATOMIC * `evalIncrCheck` Lua primitive (`persistence.ts` `ParkRedis.evalIncrCheck`, * declared HERE, exercised by `defer-park`). The counter key namespaces the * grant by `(budgetId, intentKind)` so two grants never share a counter. * * `evalIncrCheck(counterKey, ttlSeconds, max)` returns `0` when the increment * would exceed `max` (the Lua script already DECR'd back — atomic), or the new * count (`>= 1`) otherwise. So `result !== 0` ⇔ the substitution is in-budget. * The `evalIncrCheck` hook is REQUIRED for this store: a single-use-COUNTED * authority store has NO safe non-atomic fallback (a bare INCR→check→DECR would * re-introduce the over-grant race), so a client without it throws at * construction — exactly the fail-closed posture (§D #6). */ export declare function createBudgetStore(opts: { readonly client: Pick; /** * Key namespacer — Adjudicate convention wraps the raw suffix into a * tenant/env-namespaced key (e.g. `${APP_ENV}:adjudicate:${suffix}`). * Defaults to identity. */ readonly keyFor?: (suffix: string) => string; }): BudgetStore; /** * A memory value paired with its optimistic-concurrency version. `version` is * monotonic per key; `0` means the key is absent. Used by the CAS seam * (`getVersioned`/`putIfVersion`) to make concurrent writebacks safe. */ export interface VersionedMemory { readonly value: M; readonly version: number; } /** * Cross-session memory store. Read-many (non-destructive `get`), unlike the * single-use `ConfirmationStore.take`. Used ONLY to enrich the planner/renderer * context — never the kernel decision (see loop `resolveContext`). * * FIREWALL — memory is **NOT audit evidence** (ADR-126 lifecycle addendum). It * never enters `intentHash`, state `S`, any guard, the taint gate, or the * `auditHash` pre-image. It is mutable and fail-open by design, and is never * replayed; routing a memory value into the decision path would silently break * deterministic replay. Treat it as best-effort prompt context only. */ export interface MemoryStore { get(sessionId: string): Promise; put(sessionId: string, memory: M, ttlSeconds: number): Promise; merge?(sessionId: string, patch: Partial, ttlSeconds: number): Promise; /** * Optimistic-concurrency read: the value (or `null`) plus the current * version (`0` when absent). Optional — backends without versioning omit it * and callers fall back to `get`/`put`. Out of the decision path like `get`. */ getVersioned?(sessionId: string): Promise>; /** * Compare-and-set write: applies only if the stored version equals * `expectedVersion`. Returns the new version on success, or `null` on a * version conflict (the caller should re-read and retry). Never throws on * conflict. Optional companion to `getVersioned`. */ putIfVersion?(sessionId: string, memory: M, expectedVersion: number, ttlSeconds: number): Promise; } export interface CreateInMemoryMemoryStoreOptions { readonly defaultTtlSeconds?: number; /** * Hard cap on live entries. When `put`/`putIfVersion` would exceed it, the * least-recently-used entry is evicted (Map insertion order; `get`/`put` * bump recency). Bounds growth for long-lived processes. Default: unbounded. */ readonly maxEntries?: number; /** * Namespacing transform applied to `sessionId` before it is used as the map * key — for cross-tenant isolation and symmetry with the Redis store's * `keyFor`. Default: identity. */ readonly keyFor?: (sessionId: string) => string; } /** * In-memory reference MemoryStore. Non-destructive get; opportunistic TTL * sweep; optional `maxEntries` LRU cap, `keyFor` namespacing, and CAS. */ export declare function createInMemoryMemoryStore(opts?: CreateInMemoryMemoryStoreOptions): MemoryStore; /** * 042 / H4 — durable, session-scoped store for the contamination flag. * * The contamination flag (`SessionContamination`, set when an untrusted-origin * datum entered the session) is SESSION-scoped, but the laundered datum it * guards is appended to session-scoped conversation history that is re-supplied * across turns. Holding the flag only in a turn-local variable (the pre-H4 * loop) let a multi-turn launder slip the origin gate: contaminate on turn 1, * propose off the poisoned history on turn 2 — minted clean because turn 2's * variable started `undefined`. This store gives the loop a place to PERSIST the * flag across turns so the gate sees it on every subsequent turn. * * FIREWALL (mirrors `MemoryStore`): this is an IMPURE-SHELL store, NOT a kernel * input. The flag it holds is folded into the minted taint at the * envelope-minting seam BEFORE the `intentHash` is computed (so it IS inside the * hashed pre-image, invariant #4), but the store itself never enters the kernel * decision, state `S`, or the `auditHash` pre-image, and is never replayed. * * MONOTONIC (§C / invariant #7): the only mutator the loop calls is `put` with * the meet-folded flag (`contaminateSession`), which can only LOWER trust. The * sole trust-RAISING operation is `clear`, which the loop invokes ONLY on the * adopter-authenticated `resume()` path — never from an LLM action. */ export interface SessionContaminationStore { /** Load the persisted flag for a session (`null` when uncontaminated). */ get(sessionId: string): Promise; /** * Persist the (monotonically meet-folded) flag for a session. Callers MUST * only ever `put` a flag whose trust is ≤ the currently-stored flag's (the * loop folds via `contaminateSession` from the loaded value, guaranteeing it). */ put(sessionId: string, contamination: SessionContamination, ttlSeconds: number): Promise; /** * Drop the flag for a session — the trust-RAISING operation. The loop calls * this ONLY on the authenticated `resume()` path (never an LLM action). */ clear(sessionId: string): Promise; } export interface CreateInMemorySessionContaminationStoreOptions { readonly defaultTtlSeconds?: number; /** Namespacing transform applied to `sessionId` (cross-tenant isolation). */ readonly keyFor?: (sessionId: string) => string; } /** * In-memory reference `SessionContaminationStore` (tests + quickstart). TTL'd, * with an opportunistic sweep. Production wires Redis with the same surface. */ export declare function createInMemorySessionContaminationStore(opts?: CreateInMemorySessionContaminationStoreOptions): SessionContaminationStore; //# sourceMappingURL=persistence.d.ts.map