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.
189 lines • 9.47 kB
JavaScript
/**
* vault — handoff restore (kygit-handoff design D1/D10).
*
* Restore is porcelain git, in two stages that deliberately run through
* DIFFERENT runners:
*
* 1. `git clone <remote-url> <dir>` — a REAL network operation (the vault
* is reached through the `git-remote-run402` remote
* helper), so it must NOT run under `hardenedGit`'s
* `-c protocol.allow=never` (that flag exists to keep LOCAL plumbing
* calls — hash-object, commit-tree, write-tree — from ever touching
* the network by accident; a clone is the one call in this whole
* module that is SUPPOSED to). `--no-checkout` so nothing in the
* target directory's initial default-branch checkout can fire a
* template-installed hook before step 2 neutralizes hooks.
* 2. Every LOCAL-only step after that (checkout the base, apply the
* stash-shaped commit) runs through {@link hardenedGit} exactly like
* every other vault plumbing call — hooks, fsmonitor, and replace
* refs all disabled, argv-only, no shell.
*
* The handoff commit ITSELF needs no separate fetch: it is a `retention
* root` (design D6), and `restoreObjectsInto` — what the remote helper's
* `fetch` verb runs on every clone — materializes the vault's FULL
* retained object set, not just ref-reachable history. By the time step 1
* finishes, `<oid>` is already a loose object in the fresh clone's ODB.
*/
import { execFile } from "node:child_process";
import { mkdtempSync, existsSync, readFileSync, appendFileSync, mkdirSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, isAbsolute, join, resolve } from "node:path";
import { LocalError } from "../errors.js";
import { hardenedGit, hardenedGitEnv, run402PassthroughEnv } from "./vault-snapshot.js";
function fail(code, message, context, details) {
throw new LocalError(message, context, { code, details });
}
let emptyCloneHooksDir = null;
function cloneHooksDir() {
emptyCloneHooksDir ??= mkdtempSync(join(tmpdir(), "run402-vault-resume-clone-nohooks-"));
return emptyCloneHooksDir;
}
/**
* `git clone --no-checkout <remoteUrl> <targetDir>` — protocol-permitting
* (this is the one vault git invocation allowed to touch the network),
* hooks and terminal prompts still neutralized. `targetDir` must not
* already exist (git's own clone precondition).
*/
export function cloneVaultRemote(remoteUrl, targetDir) {
return new Promise((resolvePromise, reject) => {
const argv = ["--no-replace-objects", "-c", `core.hooksPath=${cloneHooksDir()}`, "clone", "--no-checkout", "--", remoteUrl, targetDir];
execFile("git", argv,
// The remote helper this clone spawns authenticates as THIS session: pass
// the RUN402_* selection through (config dir, wallet, api base, trace).
// In-process helper, never the resident daemon: the keystore this clone
// must read was written moments ago by the same resume, and a daemon
// that predates that write serves the vault from what it loaded then
// (live: a post-rotation resume opened only the newest epoch and died
// VAULT_EPOCH_NOT_OPENABLE at generation 1). One clone gains nothing
// from a daemon; every later fetch/push in the restored checkout does.
{ cwd: tmpdir(), env: { ...hardenedGitEnv(run402PassthroughEnv()), RUN402_DAEMON: "0" }, encoding: "buffer", maxBuffer: 1024 * 1024 * 1024, windowsHide: true }, (error, _stdout, stderr) => {
if (error) {
const stderrText = Buffer.from(stderr).toString("utf8");
reject(new LocalError(`git clone ${remoteUrl} failed: ${stderrText.trim().slice(0, 1000) || error.message}`, "cloning the vault for resume", { code: "HANDOFF_CLONE_FAILED", details: { remote_url: remoteUrl, target_dir: targetDir, stderr: stderrText.slice(0, 2000) } }));
return;
}
resolvePromise();
});
});
}
function sanitizeBranchName(name) {
return name && /^[A-Za-z0-9._/-]+$/.test(name) && !name.startsWith("-") ? name : null;
}
/**
* Check out the base, land on a real branch (never left detached), then
* `git stash apply --index <stash_oid>` — staged, unstaged, deleted, and
* untracked changes come back distinctly (verified against real git 2.43
* plumbing during design). Every step here is LOCAL — no network — so it
* runs fully hardened.
*/
export async function applyHandoffCheckpoint(options) {
const { dir } = options;
let baseHeadOid = options.base_head_oid ?? null;
if (!baseHeadOid) {
try {
baseHeadOid = (await hardenedGit(dir, ["rev-parse", "--verify", `${options.stash_oid}^1`])).text().trim();
}
catch (e) {
fail("HANDOFF_RESTORE_APPLY_FAILED", `could not read the base commit (first parent) off ${options.stash_oid} — it may not be present in the clone yet`, "restoring the handoff checkpoint", { stash_oid: options.stash_oid, cause: e instanceof Error ? e.message : String(e) });
}
}
let branch = sanitizeBranchName(options.branch_hint);
if (!branch) {
try {
const symref = (await hardenedGit(dir, ["symbolic-ref", "-q", "--short", "HEAD"], { okStatuses: [1] })).text().trim();
branch = sanitizeBranchName(symref);
}
catch {
branch = null;
}
}
branch ??= "main";
await hardenedGit(dir, ["checkout", "-q", baseHeadOid]);
await hardenedGit(dir, ["checkout", "-q", "-B", branch]);
try {
await hardenedGit(dir, ["stash", "apply", "--index", options.stash_oid]);
}
catch (e) {
fail("HANDOFF_RESTORE_APPLY_FAILED", `\`git stash apply --index\` failed to reconstruct the handed-off state: ${e instanceof Error ? e.message : String(e)}`, "restoring the handoff checkpoint", { base_head_oid: baseHeadOid, stash_oid: options.stash_oid, cause: e instanceof Error ? e.message : String(e) });
}
return { branch, base_head_oid: baseHeadOid, stash_oid: options.stash_oid };
}
/**
* `--to <dir>` wins; otherwise the vault's address-form name (`org/name` →
* `name`) when the redeem response carried one, otherwise the vault id.
* Always absolute — `cloneVaultRemote` runs in `tmpdir()`, so a
* relative destination must be resolved against the CALLER's cwd first.
*/
export function resolveResumeTargetDir(to, address, vaultId) {
if (to)
return isAbsolute(to) ? to : resolve(process.cwd(), to);
const name = address ? address.split("/").pop() : null;
return resolve(process.cwd(), name || vaultId);
}
/** The commit message (the Handoff Note, verbatim) — `null` when `oid` cannot be read (e.g. not yet present). */
export async function readGitCommitMessage(dir, oid) {
try {
return (await hardenedGit(dir, ["log", "-1", "--format=%B", oid])).text().replace(/\n$/, "");
}
catch {
return null;
}
}
/**
* `dir`'s real git directory (`git rev-parse --git-dir`, resolved to an
* absolute path against `dir` when the answer comes back relative) — the
* correct place to write LOCAL, never-committed exclusions even from a
* linked worktree, where `.git` is a file rather than a directory.
*/
async function resolveGitDir(dir) {
const raw = (await hardenedGit(dir, ["rev-parse", "--git-dir"])).text().trim();
return isAbsolute(raw) ? raw : resolve(dir, raw);
}
/**
* Add `.run402/` to `dir`'s LOCAL `.git/info/exclude` — never `.gitignore`
* (kygit-invite design D5): the ignore file is part of the captured tree
* and touching it would break exact-state on the first `git status` after
* `join`/`resume`. `.git/info/exclude` is local-only, per-clone git state,
* exactly like the id-pins in `vault-address.ts`. Idempotent — a second
* call on an already-excluding checkout is a no-op. Used by BOTH `join` and
* `resume` (restore is kind-agnostic, kygit-invite design D5).
*/
export async function excludeMessagingCacheFromGit(dir) {
const gitDir = await resolveGitDir(dir);
const excludePath = join(gitDir, "info", "exclude");
let existing = "";
try {
existing = readFileSync(excludePath, "utf-8");
}
catch {
existing = "";
}
const lines = existing.split("\n");
if (lines.some((l) => l.trim() === ".run402/"))
return; // already excluded
mkdirSync(dirname(excludePath), { recursive: true });
const needsLeadingNewline = existing.length > 0 && !existing.endsWith("\n");
appendFileSync(excludePath, `${needsLeadingNewline ? "\n" : ""}.run402/\n`);
}
/**
* Read-only counterpart to {@link excludeMessagingCacheFromGit} — whether
* `dir`'s LOCAL `.git/info/exclude` already carries `.run402/` (kygit-invite
* design D9's risk list: "`.git/info/exclude` is per-clone and silent" →
* `repos view` reports `messaging_cache_excluded` so the state is visible).
* `false` on any read failure (not a repository, no exclude file yet) —
* never throws.
*/
export async function isMessagingCacheExcludedFromGit(dir) {
try {
const gitDir = await resolveGitDir(dir);
const excludePath = join(gitDir, "info", "exclude");
if (!existsSync(excludePath))
return false;
const existing = readFileSync(excludePath, "utf-8");
return existing.split("\n").some((l) => l.trim() === ".run402/");
}
catch {
return false;
}
}
//# sourceMappingURL=vault-restore.js.map