run402-mcp
Version:
MCP server for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.
341 lines • 16.9 kB
TypeScript
/**
* vault — prune (protocol rev 40 §7.3; task 5.12a).
*
* The two-phase, ACYCLIC prune the client half owns:
*
* 1. plan — walk the verified chain, compute the GC ROOT SET (what must
* never be deleted) and the pruneable universe, and subtract.
* 2. attest — TWO `verifier_receipt`s over the SAME `intent_core_sha256`,
* one per CLOSED implementation identity
* (`run402-cli` | `r402s-verify`). This SDK is `run402-cli`
* and produces exactly ONE of them; the other MUST come from
* `r402s-verify`, which by spec shares no implementation code
* with the SDK — differential verification is the whole point,
* so this module will never synthesize the second receipt.
* 3. submit — sign the core, wrap it, sign the wrapper, and POST the
* EXACT BYTES. The gateway reads this route through
* `express.raw` and verifies the owner signature over the
* bytes as sent, so a re-serialized object is a different
* object. {@link pruneIntentBytes} is the one serializer.
* 4. confirm — poll the intent until its control-plane-signed
* `prune_completion` appears, then believe ONLY that: per-object
* outcomes are THREE-VALUED (`deleted` | `present_not_attempted`
* | `present_after_attempt`) and only `deleted` means the bytes
* are gone. `present_after_attempt` is NOT a deletion.
*
* What this module deliberately does NOT do:
* - decide retention eligibility. The ≥90-day schedule is measured against a
* control-plane-signed `retention_cutoff` ticket and the server's own
* admission times; a client cannot prove either. The plan is a PROPOSAL and
* the gateway is the authority — it refuses any candidate that is not
* retention-eligible against the bound ticket.
* - attest anything it did not check. A receipt this module builds carries
* `result: "failed"` (and both booleans as observed) whenever the chain
* cannot prove the property, rather than a convenient `true`.
*/
import { VAULT_FORMAT, VAULT_SUITE } from "../namespaces/vault.crypto.js";
import type { VaultCheckpointClaimSet, VaultCheckpointManifestReceipt, VaultCheckpointPackReceipt, VaultCheckpointClaimSetReceipt, VaultHead, VaultRefStateReceipt, VaultRetentionRootsReceipt, VaultWalPackReceipt } from "../namespaces/vault.types.js";
/** §7.3: at most 10 000 candidates per intent; larger prunes chunk into successive intents. */
export declare const VAULT_MAX_PRUNE_CANDIDATES = 10000;
/** The CLOSED V0 verifier identity set (`common.json#/$defs/implementation_id`). */
export declare const VAULT_VERIFIER_IMPLEMENTATIONS: readonly ["run402-cli", "r402s-verify"];
export type VaultVerifierImplementation = (typeof VAULT_VERIFIER_IMPLEMENTATIONS)[number];
/** This SDK's identity when it attests. The other half of the pair is never ours to produce. */
export declare const VAULT_SDK_VERIFIER_IMPLEMENTATION: VaultVerifierImplementation;
/**
* What this lineage calls itself in a receipt's `implementation_version`.
*
* The LINEAGE and the PROTOCOL REVISION, not the npm version: a receipt records
* which verifier produced it against which frozen wire, and the npm number
* moves for reasons that have nothing to do with either. Bump this when the
* verification logic or the revision it verifies changes.
*/
export declare const VAULT_SDK_VERIFIER_VERSION = "run402-sdk/r402s-v0-rev40";
/** `common.json#/$defs/receipt_pruneable` — the CLOSED union a delete set may name. */
export type VaultPruneableReceipt = VaultWalPackReceipt | VaultRefStateReceipt | VaultRetentionRootsReceipt | VaultCheckpointManifestReceipt | VaultCheckpointPackReceipt | VaultCheckpointClaimSetReceipt;
/** One verified chain link plus its resolved checkpoint claim set (null when the head bears no checkpoint). */
export interface VaultChainEntry {
head: VaultHead;
head_sha256: string;
claim_set: VaultCheckpointClaimSet | null;
}
/** `common.json#/$defs/verifier_receipt_ref` — what the intent carries, not the receipt itself. */
export interface VaultVerifierReceiptRef {
object_id: string;
object_kind: "verifier_receipt";
stored_bytes_sha256: string;
size_bytes: string;
implementation_id: VaultVerifierImplementation;
}
/** `verifier_receipt.json` — owner-signed, uploaded, then claimed at the intent's fence. */
export interface VaultVerifierReceipt {
format: typeof VAULT_FORMAT;
object_kind: "verifier_receipt";
suite: typeof VAULT_SUITE;
repo_id: string;
object_id: string;
intent_core_sha256: string;
checkpoint_head_sha256: string;
cutoff_ticket_sha256: string | null;
restored_object_set_hmac: string;
retention_evolution_ok: boolean;
candidates_outside_roots_ok: boolean;
implementation_id: VaultVerifierImplementation;
implementation_version: string;
result: "restored_and_verified" | "failed";
signature: string;
}
/** `prune_intent_core.json` — owner-signed; the acyclic base BOTH receipts sign. */
export interface VaultPruneIntentCore {
format: typeof VAULT_FORMAT;
object_kind: "prune_intent_core";
suite: typeof VAULT_SUITE;
repo_id: string;
object_id: string;
gc_epoch: string;
maintenance_cycle_id: string | null;
maintenance_prune_role: "intermediate" | "final" | null;
stage_claim_set_sha256: string | null;
batch_index: string | null;
batch_count: string | null;
authorizing_head_sha256: string;
checkpoint_claim_set_sha256: string;
gc_root_set_hmac: string;
retention_state_hmac: string;
delete_set: VaultPruneableReceipt[];
nonce: string;
signature: string;
}
/** `prune_intent.json` — the wrapper the gateway publishes create-only under `prune/`. */
export interface VaultPruneIntent {
format: typeof VAULT_FORMAT;
object_kind: "prune_intent";
suite: typeof VAULT_SUITE;
repo_id: string;
object_id: string;
core: VaultPruneIntentCore;
intent_core_sha256: string;
verifier_receipts: VaultVerifierReceiptRef[];
signature: string;
}
/** One entry of the completion's three-valued vector. */
export interface VaultPruneOutcome {
object_id: string;
result: "deleted" | "present_not_attempted" | "present_after_attempt";
}
/** `GET …/prune-intents/:id` — the gateway's secret-free public view (`toPublicIntent`). */
export interface VaultPruneIntentRecord {
object_id: string;
repo_id: string;
state: string;
gc_epoch: string;
intent_sha256: string | null;
intent_core_sha256: string | null;
candidate_count: number;
next_candidate_index: number;
maintenance_cycle_id: string | null;
maintenance_prune_role: string | null;
stage_claim_set_sha256: string | null;
batch_index: number | null;
batch_count: number | null;
completion: {
object_id: string | null;
sha256: string | null;
per_object: VaultPruneOutcome[] | null;
deleted_count: number;
present_after_attempt_count: number;
present_not_attempted_count: number;
gc_epoch_at_completion: string;
cycle_event_seq: string | null;
completed_at: string | null;
} | null;
prepared_at: string | null;
intent_put_issued_at: string | null;
intent_stored_at: string | null;
deleting_started_at: string | null;
}
/**
* A checkpoint's FULL RECOVERY BUNDLE: the claim set, the manifest, every pack,
* AND its `covers_through` head's `ref_state` + `retention_roots` carriers.
* A bundle missing its carriers is not a recovery path, so the carriers are
* part of the protection, not adjacent to it.
*/
export declare function checkpointRecoveryBundle(entry: VaultChainEntry): VaultPruneableReceipt[];
/** Every pruneable object the chain has ever named, sorted by `object_id`, deduplicated. */
export declare function collectPruneUniverse(entries: readonly VaultChainEntry[]): VaultPruneableReceipt[];
/** Pairwise-distinct, sorted by `object_id` — the schema's canonical order, not an incidental one. */
export declare function sortPruneReceipts(receipts: readonly VaultPruneableReceipt[]): VaultPruneableReceipt[];
export interface VaultGcRootSet {
/** The protected receipts, sorted — the `gcrootset` commitment's preimage content. */
receipts: VaultPruneableReceipt[];
/** The newest checkpoint-bearing entry, or `null` when the chain has none. */
latest_checkpoint: VaultChainEntry | null;
/** The one before it, or `null`. */
prior_checkpoint: VaultChainEntry | null;
/**
* Why nothing may be pruned yet, or `null` when a prune is structurally
* possible. Stated rather than left to be inferred from an empty candidate
* list, which would read as "already clean".
*/
blocked_reason: string | null;
}
/**
* The GC root set: the latest checkpoint's recovery bundle + its WAL suffix +
* the immediately-prior checkpoint's recovery bundle + every carrier at or
* after the prior checkpoint.
*
* DELIBERATELY conservative in two places, because under-protecting deletes
* history and over-protecting only costs storage:
*
* - with FEWER THAN TWO checkpoints nothing is prunable at all. The
* protection rule exists so a bad latest checkpoint still has a second
* recovery path; with a single checkpoint that second path IS the
* pre-checkpoint WAL chain, so pruning it would leave exactly one.
* - every `ref_state` / `retention_roots` carrier at or after the PRIOR
* checkpoint is protected, not just the unexpired-root carriers. Root
* expiry is measured against a server-signed ticket this client cannot
* evaluate, so it never guesses a carrier is dead.
*/
export declare function computeGcRootSet(entries: readonly VaultChainEntry[]): VaultGcRootSet;
export interface VaultPrunePlan {
/** Candidates in canonical order, capped at {@link VAULT_MAX_PRUNE_CANDIDATES}. */
candidates: VaultPruneableReceipt[];
/** How many pruneable objects were left out by the per-intent cap (chunk into a later intent). */
deferred_count: number;
root_set: VaultGcRootSet;
universe_count: number;
}
/** universe − GC root set, canonical order, capped. Never mutates its inputs. */
export declare function planPruneCandidates(entries: readonly VaultChainEntry[]): VaultPrunePlan;
export interface VaultRetentionEvolutionCheck {
ok: boolean;
/** Every departure this client could not justify from the chain alone. */
unproven: Array<{
generation: string;
ref: string;
oid: string;
reason: string;
}>;
}
/**
* Verify that every retention root which LEFT the map left legally: only at a
* checkpoint-bearing generation carrying a cutoff ticket, and only when its
* `effective_admitted_at + retentionDays < cutoff_at` strictly.
*
* `effectiveAdmittedAt` returning `null` is not a failure of the vault — it is
* a failure of THIS CLIENT to prove the departure, and the check reports `ok:
* false` so the receipt attests `false` rather than a convenient `true`.
*/
export declare function checkRetentionEvolution(entries: readonly VaultChainEntry[], roots: (entry: VaultChainEntry) => Array<{
ref: string;
oid: string;
dropped_at_generation: string;
}>, effectiveAdmittedAt: (droppedAtGeneration: string) => string | null, isEligible: (effectiveAdmittedAtIso: string, cutoffAtIso: string) => boolean): VaultRetentionEvolutionCheck;
export interface VaultBuildPruneCoreInput {
repo_id: string;
gc_epoch: string;
authorizing_head_sha256: string;
checkpoint_claim_set_sha256: string;
gc_root_set_hmac: string;
retention_state_hmac: string;
delete_set: readonly VaultPruneableReceipt[];
/** All five cycle fields are non-null together iff the prune advances a maintenance cycle (§7.2). */
cycle?: {
maintenance_cycle_id: string;
maintenance_prune_role: "intermediate" | "final";
stage_claim_set_sha256: string;
batch_index: string;
batch_count: string;
} | null;
object_id?: string;
nonce?: string;
}
/**
* Build + sign `prune_intent_core`. The core is signed on its own AND embedded
* in a signed wrapper (the double signature of §7.3) — `intent_core_sha256` is
* the stored-bytes hash of the COMPLETE signed core, which is why the core must
* be finished before either verifier can attest anything about it.
*/
export declare function buildPruneIntentCore(input: VaultBuildPruneCoreInput, signingSeed: Uint8Array): VaultPruneIntentCore;
/** The stored-bytes hash of the complete signed core — what BOTH verifier receipts sign. */
export declare function pruneIntentCoreSha256(core: VaultPruneIntentCore): string;
export interface VaultBuildVerifierReceiptInput {
repo_id: string;
intent_core_sha256: string;
checkpoint_head_sha256: string;
cutoff_ticket_sha256: string | null;
restored_object_set_hmac: string;
retention_evolution_ok: boolean;
candidates_outside_roots_ok: boolean;
implementation_id: VaultVerifierImplementation;
implementation_version: string;
object_id?: string;
}
/**
* Build + sign a `verifier_receipt`.
*
* `result` is DERIVED, never passed in: a receipt is `restored_and_verified`
* exactly when both attestation booleans are true, and `failed` otherwise. A
* caller cannot hand this function `failed` booleans and a passing result.
*/
export declare function buildVerifierReceipt(input: VaultBuildVerifierReceiptInput, signingSeed: Uint8Array): VaultVerifierReceipt;
/** The `verifier_receipt_ref` an intent carries for an uploaded receipt. */
export declare function verifierReceiptRef(receipt: VaultVerifierReceipt): VaultVerifierReceiptRef;
/**
* Wrap + sign the intent.
*
* Every gate the gateway applies to the pair is applied here first, so a
* malformed pair never reaches the wire as a wasted round trip: exactly two
* refs, one per CLOSED implementation identity, distinct ids AND distinct
* hashes, each attesting `restored_and_verified` with both booleans true, and
* each signing THIS core.
*/
export declare function buildPruneIntent(core: VaultPruneIntentCore, receipts: readonly VaultVerifierReceipt[], signingSeed: Uint8Array): VaultPruneIntent;
/**
* Refuse a supplied core that the chain has moved past — BEFORE the receipts
* are uploaded.
*
* A prune is planned, verified out-of-band by a second implementation, and only
* then submitted, so there is a real window in which a push or a compaction
* lands underneath it. The gateway would refuse such an intent at its fence,
* but by then both receipts are stored objects the fence CLAIMS, so a stale
* submission burns evidence for nothing. Same registry codes the gateway uses,
* raised one step earlier.
*/
export declare function assertPruneCoreStillCurrent(core: VaultPruneIntentCore, current: {
repo_id: string;
gc_epoch: string;
checkpoint_claim_set_sha256: string;
}): void;
/**
* THE serializer for the submit route.
*
* `POST …/prune-intents` is parsed with `express.raw`: the service strict-parses
* these bytes and verifies the owner signature over them. Anything that
* re-serializes the object between here and the socket — a JSON body helper, a
* proxy that reformats — changes what was signed. Send exactly this.
*/
export declare function pruneIntentBytes(intent: VaultPruneIntent): Uint8Array;
export interface VaultPruneConfirmation {
/** `null` until the control-plane-signed completion exists. */
outcome: "completed" | "superseded_no_delete" | "superseded_partial_delete" | null;
/** Object ids the completion confirms are GONE. Only `deleted` qualifies. */
deleted: string[];
/** Still present — attempted or not. Never reported as removed. */
present: VaultPruneOutcome[];
/** Candidates the completion did not adjudicate at all (a defect; reported, never assumed deleted). */
unadjudicated: string[];
}
/**
* Read the completion's three-valued vector against the intent's canonical
* candidate order.
*
* The one rule this function exists to enforce: **only `deleted` means gone.**
* `present_after_attempt` looks like a deletion in the logs and is not one —
* treating it as one is how a client reports history removed that is still
* there. A candidate with no entry is `unadjudicated`, never inferred.
*/
export declare function summarizePruneCompletion(candidateIds: readonly string[], record: VaultPruneIntentRecord | null): VaultPruneConfirmation;
/** Run `fn` in a fresh temp directory and always remove it, even on refusal. */
export declare function withScratchDir<T>(prefix: string, fn: (dir: string) => Promise<T>): Promise<T>;
//# sourceMappingURL=vault-prune.d.ts.map