UNPKG

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.

170 lines • 8.84 kB
/** * vault-byo-primary-bucket — degraded read mode (design D4, task 3.4, * MIRROR HALF ONLY). Turns a gateway chain/payload read that fails with a * NETWORK-CLASS error into a transparent fallback served from the vault's * client-side mirror, so `git fetch`/`clone` keep working while run402 is * down. * * This is deliberately NOT a second implementation of anything: the fallback * reuses `r402s-recover` (`vault-recover.ts`'s `recoverVaultMirror`) * verbatim — the SAME chain-verification, absence adjudication, and * decrypt/materialize machinery `run402 repos recover` already ships. The * module doc there says it plainly: "recovery is the keyed offline subset * of machinery that already exists." This file's own job is narrow — * trigger discipline (network-class only, never 4xx), a bounded retry * against the gateway first, and resolving WHICH client-side destination to * fall back to — never a second chain-verifier and never a second S3/backend * reader. * * Local trust: this module never touches the LIVE vault's own keystore pins * (`head_pin` / `verified_prefix` / `materialized_pin`, written by * `Vault.verifyToNewest`/`materialize` against the live transport) — * a degraded read runs an entirely separate engine (`recoverVaultMirror`, * which only ever calls `keystore.readIdentity`/`readRecoveryReceipt`, never * `keystore.updateRepo`) against a `VaultMirrorBackend`. So "local trust * pins advance only as far as the copy chain-verifies" holds by * construction: there is no code path here that could advance a live pin * past what the mirror copy itself proved. * * WRITES ARE NEVER REROUTED. This module exports only READ machinery; the * caller's own push/admission path is untouched — see the client-surface * doc's own D194 citation ("admission is irreducibly live-server"). */ import { isRun402Error } from "../errors.js"; import { readByoConfig } from "./vault-byo-config.js"; import { formatMirrorDestination, readMirrorConfig } from "./vault-mirror-config.js"; import { openVaultMirrorBackend } from "./vault-mirror-backend.js"; import { recoverVaultMirror } from "./vault-recover.js"; /** * Trigger discipline (design D4): a chain/payload read falls back ONLY on a * genuinely network-class failure — the underlying `fetch` threw before a * response existed, or the gateway answered with a 5xx. An authorization * refusal (401/403/404, or any other 4xx) must NEVER silently reroute to the * mirror, so it is excluded even when it happens to arrive with no `.status` * (see below). * * Two wrinkles in how a vault read actually fails, both handled here: * * 1. Most vault byte reads (`getGenerationBytes`/`getObjectBytes` in * `vault-publication.ts`, and this file's own mirror backend) call the * injected `fetch` DIRECTLY rather than the kernel's typed * `client.request()` — a raw connection/DNS failure therefore propagates * UNBRANDED (a plain `TypeError`, never a `Run402Error`). That is, in * practice, the primary shape a real "run402 is unreachable" outage * takes, so an unbranded throw is treated as network-class. * 2. When those SAME call sites DO get a response but it is not `ok`, they * throw via `vault-publication.ts`'s own local `fail()` helper — a * `LocalError` whose real HTTP status rides in `details.status`, never on * `e.status` itself (`LocalError` always passes `status: null` to the * `Run402Error` constructor). This function reads BOTH places so a 5xx * reaching through either path is classified alike, and a 4xx through * either path is excluded alike. */ export function isNetworkClassVaultReadError(e) { if (!isRun402Error(e)) return true; if (e.kind === "network_error") return true; // The payment-capable client (paid-fetch) reports a request that failed // BEFORE any response existed as X402_INITIAL_REQUEST_FAILED — the same // connection/DNS failure an unbranded TypeError carries, wrapped. No // response, no status: network-class. if (e.kind === "payment_attempt_error" && e.code === "X402_INITIAL_REQUEST_FAILED" && e.toJSON().category === "network") return true; const status = vaultReadErrorStatus(e); return typeof status === "number" && status >= 500; } function vaultReadErrorStatus(e) { if (typeof e.status === "number") return e.status; const details = e.details; if (details && typeof details === "object" && !Array.isArray(details)) { const s = details.status; if (typeof s === "number") return s; } return null; } /** * The small seam design D4 calls out: which client-side destination a * degraded read should serve from, for one vault. * * Precedence (vault-byo-primary-bucket task 3.4): the opt-in customer * MIRROR wins when BOTH a mirror and a BYO write config are configured * locally on THIS machine. A BYO vault's own primary destination is * ALWAYS a complete `r402s-recover` source on its own (task 3.3's whole * point), so this is not a completeness question — it keeps the * PRE-EXISTING, already-shipped mirror substrate's behavior byte-identical * (a machine that already has a working mirror configured for degraded * reads keeps using it) rather than silently switching sources the moment * a BYO local config also exists. The BYO branch is the fallback: read * ONLY when no mirror is configured — no network call either way, purely * local file reads, matching this function's own "never touches the * network" contract. */ export function resolveDegradedReadSource(keystore, repoId) { const config = readMirrorConfig(keystore, repoId); if (config) { const backend = openVaultMirrorBackend(config.destination, repoId, config.credential); return { kind: "mirror", backend, destination: formatMirrorDestination(config.destination), credential_kind: config.credential?.kind ?? null }; } const byo = readByoConfig(keystore, repoId); if (!byo) return null; const backend = openVaultMirrorBackend(byo.destination, repoId, byo.credential); return { kind: "byo", backend, destination: formatMirrorDestination(byo.destination), credential_kind: byo.credential?.kind ?? null }; } /** * Attempt `options.attemptLive`, with ONE bounded retry on a network-class * failure (so a transient blip does not flap between sources — design D4). * A non-network-class failure (an authorization refusal, or any other 4xx; * a protocol/chain error with no status signal at all) is NEVER retried and * NEVER falls back — it rethrows the ORIGINAL error UNCHANGED, exactly as a * non-degraded caller would see it. * * After the retry budget is exhausted on a genuinely network-class failure: * with no fallback source configured (or `out_dir: null`), the ORIGINAL * error is rethrown UNCHANGED — never wrapped, never re-typed, so its * `code`/`status`/`kind` survive for the caller exactly as they would * without this wrapper. Only the caller's own stderr hint may change. * With a fallback source, this degrades via the SHIPPED `r402s-recover` * engine (`recoverVaultMirror`) against it, returning * `{ degraded: true, result }` — a genuine recovery failure (an incomplete * or unreadable mirror) propagates from THAT call as its own honest error, * never silently swallowed back into the original gateway failure. */ export async function tryVaultDegradedRead(options) { let lastErr; const RETRY_ATTEMPTS = 2; // the live attempt, plus one bounded retry for (let attempt = 0; attempt < RETRY_ATTEMPTS; attempt++) { try { return { degraded: false, live: await options.attemptLive() }; } catch (e) { if (!isNetworkClassVaultReadError(e)) throw e; lastErr = e; } } if (!options.out_dir) throw lastErr; const source = options.backend ? { kind: "mirror", backend: options.backend, destination: options.backend.describe(), credential_kind: null } : resolveDegradedReadSource(options.keystore, options.repo_id); if (!source) throw lastErr; const recovered = await recoverVaultMirror({ backend: source.backend, out_dir: options.out_dir, keystore: options.keystore }); return { degraded: true, result: { degraded: true, source: { kind: source.kind, destination: source.destination, credential_kind: source.credential_kind }, refs: recovered.refs, head_target: recovered.head_target, generation: recovered.recovered_generation, retained_refs: recovered.retained_refs, validity_not_freshness: recovered.validity_not_freshness, }, }; } //# sourceMappingURL=vault-degraded-read.js.map