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.

303 lines • 18.7 kB
/** * vault D6 — named addressing resolution + id-pinning (repo-first-onramp * task 4.5). * * Two things live here, both Node-only (local git state): * * - PIN read/write: `r402.repoId` (+ `r402.repoAddress`, the human name it * was resolved from) in the repository's LOCAL git config * (`git config --local` — this checkout's own `.git/config`, never * global). "Local git state" per design D6's id-pinning paragraph. * - `resolveVaultAddress`: the orchestrator the remote helper and * `vault snapshot` drive — pinned id first, then dispatch on the * address's form (id vs slug — {@link vaultRemoteAddressForm}), then * (slug-form only, opt-in via `allow_create`) push-to-create on a miss. * * ID-PINNING RATIONALE (design D6, "resolved"): "on first contact the helper * resolves slug/name → repo_id and pins the immutable id in local git state. * Names are sugar for humans and first-time clones; every existing checkout * follows the id and survives any rename." Concretely: an id-form address * (`org_id`/`prj_...`) already survives a rename (project ids never change), * so pinning buys it nothing and this module never pins one. A SLUG-form * address re-resolves `org-slug/name → repo_id` over the network on every * open unless pinned — and a rename of either half would silently break that * resolution for an EXISTING checkout the moment it takes effect. Pinning * `repo_id` the first time a slug-form address resolves means every later * open on this checkout goes straight to `repo_id` (no resolution round-trip, * and no exposure to a later rename at all) — structurally better than a * redirect, per the design's own framing. */ import { hardenedGit } from "./vault-snapshot.js"; import { vaultRemoteAddressForm } from "../namespaces/repos.js"; import { pushToCreateVault } from "./vault-push-to-create.js"; const CONFIG_KEY_REPO_ID = "r402.repoId"; /** The `org-slug/name` text the pin was resolved from — diagnostic only; resolution never reads it back. */ const CONFIG_KEY_ADDRESS = "r402.repoAddress"; /** * The resolved ids beside the pin (vault-force-spelling-and-pin-fold). * A pin carrying BOTH resolves fully OFFLINE — zero transport operations — * which is what removes the standing per-invocation validation read; a * legacy pin without them self-upgrades through one validation read. */ const CONFIG_KEY_PROJECT_ID = "r402.projectId"; const CONFIG_KEY_ORG_ID = "r402.orgId"; /** * kygit-handoff design D10: the coordination-room key, written ALONGSIDE * `r402.projectId` (same value — a project's DEFAULT room is keyed by its * project id, p0016) so `cli/lib/org-context.mjs`'s room resolution has a * dedicated config key to read rather than special-casing vault's own * `r402.projectId` name. Every existing pin site gets this for free, not * just `resume` — one write, every vault checkout benefits. */ const CONFIG_KEY_ROOM = "r402.room"; async function readLocalGitConfig(repoDir, key) { try { const out = (await hardenedGit(repoDir, ["config", "--local", "--get", key])).text().trim(); return out.length > 0 ? out : null; } catch { // `git config --get` exits non-zero both when the key is absent and when // this is not a repository at all — either way, "nothing pinned" is the // correct read for a resolver that must degrade gracefully. return null; } } /** Read this checkout's pinned `repo_id`, or `null` when nothing is pinned. */ export async function readPinnedVaultRepo(repoDir) { const repoId = await readLocalGitConfig(repoDir, CONFIG_KEY_REPO_ID); if (!repoId) return null; const address = await readLocalGitConfig(repoDir, CONFIG_KEY_ADDRESS); const parts = address ? address.split("/") : []; const resolvedFrom = parts.length === 2 && parts[0] && parts[1] ? { org_slug: parts[0], repo_name: parts.slice(1).join("/") } : null; const projectId = await readLocalGitConfig(repoDir, CONFIG_KEY_PROJECT_ID); const orgId = await readLocalGitConfig(repoDir, CONFIG_KEY_ORG_ID); const room = await readLocalGitConfig(repoDir, CONFIG_KEY_ROOM); return { repo_id: repoId, resolved_from: resolvedFrom, project_id: projectId, org_id: orgId, room }; } /** * Pin `repo_id` into this checkout's LOCAL git config — and, when resolved * from a slug-form address, the address it was resolved from (diagnostic * only). `resolvedFrom` is omitted for an id-form pin (vault-client- * round-trips design D4): an id-form address has no org-slug/name pair to * record, and — per {@link resolveVaultAddress}'s own doc comment — the * design's instruction is that pinning "only resolves names"; id-form is * already rename-proof, so this pin exists purely to skip the resolution * ROUND TRIP, not to survive a rename. */ export async function pinVaultRepo(repoDir, repoId, resolvedFrom, ids) { await hardenedGit(repoDir, ["config", "--local", CONFIG_KEY_REPO_ID, repoId]); if (resolvedFrom) await hardenedGit(repoDir, ["config", "--local", CONFIG_KEY_ADDRESS, `${resolvedFrom.org_slug}/${resolvedFrom.repo_name}`]); if (ids) { await hardenedGit(repoDir, ["config", "--local", CONFIG_KEY_PROJECT_ID, ids.project_id]); await hardenedGit(repoDir, ["config", "--local", CONFIG_KEY_ORG_ID, ids.org_id]); // kygit-invite design D3/D5: the room pin defaults to the project id (a // project's default room) but a joiner pins the INVITE's own room key // when the mint named one — `ids.room_key` overrides, never silently // widens what an ordinary resume/push pin writes. await hardenedGit(repoDir, ["config", "--local", CONFIG_KEY_ROOM, ids.room_key ?? ids.project_id]); } } /** * add-room-invite design D10: a bare `rooms join <key>` arrival standing * inside a git repository pins `r402.orgId`/`r402.room` in this checkout's * LOCAL git config — the SAME two keys {@link pinVaultRepo} writes * alongside a vault's `r402.repoId` — but WITHOUT one, since a room-only * join has no vault to pin. Read back by {@link readPinnedRoomBinding}, * independent of {@link readPinnedVaultRepo}'s own gate on `r402.repoId` * being present (a room-only checkout never sets it, so that reader would * otherwise see this pin as absent). */ export async function pinRoomBinding(repoDir, ids) { await hardenedGit(repoDir, ["config", "--local", CONFIG_KEY_ORG_ID, ids.org_id]); await hardenedGit(repoDir, ["config", "--local", CONFIG_KEY_ROOM, ids.room_key]); } /** Read `r402.orgId`/`r402.room` directly, with NO gate on `r402.repoId` (design D10) — the counterpart read for {@link pinRoomBinding}. Either field is `null` when unset (no repository, or never pinned). */ export async function readPinnedRoomBinding(repoDir) { const org_id = await readLocalGitConfig(repoDir, CONFIG_KEY_ORG_ID); const room = await readLocalGitConfig(repoDir, CONFIG_KEY_ROOM); return { org_id, room }; } /** Clear a pin this checkout no longer trusts (design D4: a pinned id that 404s). Tolerates an absent key — `git config --unset` on a key that was never set is not a failure here. */ async function clearPinnedVaultRepo(repoDir) { for (const key of [CONFIG_KEY_REPO_ID, CONFIG_KEY_ADDRESS, CONFIG_KEY_PROJECT_ID, CONFIG_KEY_ORG_ID, CONFIG_KEY_ROOM]) { try { await hardenedGit(repoDir, ["config", "--local", "--unset", key]); } catch { // already absent — nothing to clear } } } /** `RESOURCE_NOT_FOUND` (or a bare 404) — the "this pin no longer resolves" signal, never any other failure. */ function isPinStale(e) { const err = e; return Boolean(err && (err.status === 404 || err.code === "RESOURCE_NOT_FOUND" || err.code === "ROUTE_NOT_FOUND")); } /** * Resolve a parsed remote address to `{repo_id, project_id, org_id}`, * pinning `repo_id` in local git state on the first successful resolution — * of EITHER form (vault-client-round-trips design D4 widens the * pre-existing slug-form pin, task 4.5, to id-form too) — and, when * `allow_create` is set and resolution misses, push-to-create it (task * 4.4/4.5, design D6). A pinned id that no longer resolves (404) clears the * pin and re-resolves ONCE through the ordinary form-dispatch path below, * rather than failing on a stale local pointer. */ export async function resolveVaultAddress(options) { if (options.repo_dir && !options.bypass_local_answers) { const pinned = await readPinnedVaultRepo(options.repo_dir); if (pinned) { // `form` below describes what THIS call was asked to resolve, not how // the pin originally came to exist (`resolved_from` alone can't tell — // it is `null` for both an id-form pin and a pin that predates the // field/was written by hand). if (pinned.project_id && pinned.org_id) { // Id-carrying pin: fully OFFLINE — zero transport operations. The // stale-pin case (the vault deleted out from under this checkout) // surfaces on the verb's first repo-scoped read instead; the caller // arms {@link recoverStaleVaultPin} for that (client-surface // spec, "Id-form remotes pin repo_id like slug-form remotes"). return { repo_id: pinned.repo_id, project_id: pinned.project_id, org_id: pinned.org_id, form: vaultRemoteAddressForm(options.address), via: "pin", address: pinned.resolved_from, created: null, offline: true }; } try { // Legacy id-less pin: one validation read, then REWRITE the pin with // the ids so every later invocation on this checkout is offline — // self-upgrading, no migration step. const record = await options.transport.getVaultRecord({ repo_id: pinned.repo_id }); await pinVaultRepo(options.repo_dir, record.repo_id, pinned.resolved_from ?? undefined, { project_id: record.project_id, org_id: record.org_id }); return { repo_id: record.repo_id, project_id: record.project_id, org_id: record.org_id, form: vaultRemoteAddressForm(options.address), via: "pin", address: pinned.resolved_from, created: null, offline: false }; } catch (e) { if (!isPinStale(e)) throw e; await clearPinnedVaultRepo(options.repo_dir); // fall through to an ordinary (non-pinned) resolution, below } } } const form = vaultRemoteAddressForm(options.address); if (form === "id") { if (!options.bypass_local_answers) { // vault-offline-clone-resolve (design D1/D2): consult the LOCAL // keystore before the network. D2's completeness argument: decrypting // anything from this vault requires exactly the keystore's repo file // (`k_repo_hex`), so a machine without one fails the clone at // `VAULT_REPO_STATE_MISSING` no matter how resolution went — // "keystore-known" is therefore exactly the set of clones that can // actually complete, and the network read was only ever confirming a // fact this machine already held (110-530ms of RTT for 13-22ms of // server work). An id-form address always // carries a real `org_id` (see `vaultRemoteAddressForm`), so the // lookup is always org-scoped — a project-id match under a different // org is a miss, never a guess. Deliberately runs even with NO // `repo_dir` (a repo-free `git ls-remote`): the keystore is // cwd-independent, unlike the git-config pin above. const keystoreMatch = options.keystore.findRepoByProject(options.address.project_id, options.address.org_id); if (keystoreMatch) { // Same offline handle shape the git-config pin branch above builds; // `via: "keystore"` is the only distinguishing mark, so {@link // recoverStaleVaultPin}'s generic `resolution.offline` check // covers this source unchanged. The git-config pin is written // immediately — "on first successful use" of this answer, exactly // where the network-resolved branch below writes it — so every // LATER invocation on this checkout short-circuits through the pin // check above instead of this keystore scan. if (options.repo_dir) await pinVaultRepo(options.repo_dir, keystoreMatch.repo_id, undefined, { project_id: keystoreMatch.project_id, org_id: keystoreMatch.org_id }); return { repo_id: keystoreMatch.repo_id, project_id: keystoreMatch.project_id, org_id: keystoreMatch.org_id, form: "id", via: "keystore", address: null, created: null, offline: true }; } } const record = await options.transport.findVaultByProject({ project_id: options.address.project_id }); // Design D4: an id-form address is already rename-proof (a project id // never changes), so this pin exists purely to skip the RESOLUTION // round trip on every later invocation — never to survive a rename, per // the design's own "pin only resolves names" framing for WHY a pin is // needed at all; id-form gets one anyway because the round trip itself // is the cost this change removes. if (options.repo_dir) await pinVaultRepo(options.repo_dir, record.repo_id, undefined, { project_id: record.project_id, org_id: record.org_id }); return { repo_id: record.repo_id, project_id: record.project_id, org_id: record.org_id, form: "id", via: "resolved", address: null, created: null, offline: false }; } const orgSlug = options.address.org_id; const repoName = options.address.project_id; // `allow_create` delegates straight to `pushToCreateVault`, which // already opens with its OWN fast-path `findVaultByRepo` read (step 1 of // its own doc comment) — probing here first too would just double the // network read on every ordinary (non-creating) push. if (options.allow_create) { const result = await pushToCreateVault({ keystore: options.keystore, transport: options.transport, org_slug: orgSlug, repo_name: repoName, ...(options.client_creation_id !== undefined ? { client_creation_id: options.client_creation_id } : {}), ...(options.service_public_key !== undefined ? { service_public_key: options.service_public_key } : {}), }); if (options.repo_dir) await pinVaultRepo(options.repo_dir, result.repo_id, { org_slug: orgSlug, repo_name: repoName }, { project_id: result.project_id, org_id: result.org_id }); if (!result.found && result.created) await options.onVaultCreated?.(result.created); return { repo_id: result.repo_id, project_id: result.project_id, org_id: result.org_id, form: "slug", via: result.found ? "resolved" : "created", address: { org_slug: orgSlug, repo_name: repoName }, created: result.created, offline: false }; } // Read-only path (`list`/`fetch`, plain `status`): a miss is an ordinary // not-found refusal — including `SLUG_RELEASED`, which is NEVER // auto-followed and rethrows unchanged either way. const record = await options.transport.findVaultByRepo({ org_slug: orgSlug, repo_name: repoName }); if (options.repo_dir) await pinVaultRepo(options.repo_dir, record.repo_id, { org_slug: orgSlug, repo_name: repoName }, { project_id: record.project_id, org_id: record.org_id }); return { repo_id: record.repo_id, project_id: record.project_id, org_id: record.org_id, form: "slug", via: "resolved", address: { org_slug: orgSlug, repo_name: repoName }, created: null, offline: false }; } /** * Stale-pin recovery for OFFLINE resolutions (client-surface spec, * "Id-form remotes pin repo_id like slug-form remotes"; widened by * vault-offline-clone-resolve design D3 to the keystore-sourced offline * answer too): a pin or a keystore hit resolves with no network read, so the * once-in-a-lifetime stale case (a vault deleted and re-allocated out from * under this checkout, or a keystore file left behind by an old allocation) * surfaces as the VERB's first repo-scoped read failing instead of a * resolution failure. * * Given that failure, answer the fresh resolution to retry the verb against — * or `null` when there is nothing to recover: * * - a non-vault-absent error (anything outside the not-found family and the * gateway's authorize-before-reveal `VAULT_ACCESS_DENIED` fold, which * deliberately makes "gone" and "not yours" one envelope) → `null`, * rethrow yours; * - re-resolution yielding the SAME `repo_id` → the local answer was fine * and the refusal is real: the pin is restored (re-resolution re-pins * it) and the answer is `null` — recovery never widens what an * unauthorized caller learns (both probes return the same refusal * family it already saw); * - a DIFFERENT `repo_id` → the fresh resolution, pin already rewritten; * the caller retries the verb exactly once. * * The re-resolve MUST bypass every local answer (`bypass_local_answers: * true`) — a stale keystore file (or, before this change, a stale pin) is * exactly the fact recovery exists to get past, so re-running the ordinary * local-first path would just consult the SAME stale local answer again and * never reach the network. This is why the retry always costs one real * transport operation, deliberately outside the counted budgets (D3, D5). * * Re-resolution failures are swallowed in favor of `null` (the caller's * original error is the truthful one to surface). */ export async function recoverStaleVaultPin(options) { if (!options.resolution.offline) return null; const err = options.error; const vaultAbsentSignal = Boolean(err && (isPinStale(err) || err.code === "VAULT_ACCESS_DENIED" || err.status === 403)); if (!vaultAbsentSignal) return null; try { await clearPinnedVaultRepo(options.repo_dir); const fresh = await resolveVaultAddress({ keystore: options.keystore, transport: options.transport, address: options.address, repo_dir: options.repo_dir, bypass_local_answers: true }); return fresh.repo_id === options.resolution.repo_id ? null : fresh; } catch { return null; } } //# sourceMappingURL=vault-address.js.map