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.
230 lines • 12.2 kB
JavaScript
/**
* The vault deploy lane, supplied at last (change `vault-deploy-lane`).
*
* `add-vault` 5.6 built `runVaultDeploy` with the lane INJECTED and never
* supplied one, so a project whose `vault_policy` is `required` could not be
* deployed by any published client. This module is that supplier, plus the
* entry point the CLI calls in place of a bare `apply()`:
*
* - {@link createApplyDeployLane} drives the shipped `/apply/v1` routes —
* plan carrying `{capture_id, snapshot_oid_hmac}`, content upload, then a
* commit carrying the activation token — by REUSING `Deploy.apply()` rather
* than reassembling it. Warning gating, CAS dedup, event emission, asset
* re-plan, and terminal polling are the same code every other deploy runs;
* the only difference is two extra wire fields.
* - {@link applyWithVault} decides whether any of that happens at all. A
* project that is not `required` takes the untouched path: no capture, no
* token, no extra refusal, one policy read.
*
* THE COROUTINE, because it is the non-obvious part. `runVaultDeploy` wants
* two separable steps (plan, then commit) with the vault push running between
* them; `Deploy.apply()` is one call. The lane bridges them with a pair of
* deferreds: `apply()`'s `authorize` hook fires once the plan exists and its
* content is uploaded, which RESOLVES `lane.plan(...)`; it then waits for
* `lane.commit(...)` to hand back the activation block. If no commit is ever
* coming — the push failed, or correspondence refused — {@link abandon} rejects
* that wait so the apply unwinds with nothing committed.
*/
import { LocalError, isRun402Error } from "../errors.js";
function deferred() {
let resolve;
let reject;
const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
// A rejection that lands before anyone awaits is still handled — the awaiter
// arrives a tick later, and an unhandled-rejection crash here would take out
// a deploy that is merely being abandoned.
promise.catch(() => undefined);
return { promise, resolve, reject };
}
export function createApplyDeployLane(options) {
const planned = deferred();
const authorized = deferred();
let applyPromise = null;
let result = null;
let settled = false;
const lane = {
async plan(input) {
applyPromise = options.engine.apply(options.spec, {
...(options.apply ?? {}),
vault: {
declaration: { capture_id: input.capture_id, snapshot_oid_hmac: input.snapshot_oid_hmac },
authorize: async (p) => {
if (!p.apply_plan_sha256) {
// A capture-bearing plan the gateway answered without a canonical
// digest cannot back a token. Fail the PLAN lane rather than
// inventing a placeholder: the capture still publishes (with a
// null digest, so nothing is mintable from it) and the outcome
// is an honest `DEPLOY_FAILED_VAULTED`.
const missing = new LocalError("the gateway accepted a capture-bearing plan but returned no apply_plan_sha256; no activation token can be minted for it", "planning a vault deploy", { code: "VAULT_PLAN_DIGEST_MISSING", details: { plan_id: p.plan_id, operation_id: p.operation_id } });
planned.reject(missing);
throw missing;
}
planned.resolve({ plan_id: p.plan_id, operation_id: p.operation_id, apply_plan_sha256: p.apply_plan_sha256 });
return authorized.promise;
},
},
});
// A failure BEFORE the plan exists (validate, plan, upload) never calls
// `authorize`, so surface it as the plan lane's failure — that is what
// makes it `DEPLOY_FAILED_VAULTED` with a null digest rather than a hang.
applyPromise.then((r) => { settled = true; result = r; }, (e) => { settled = true; planned.reject(e); });
return planned.promise;
},
async commit(input) {
authorized.resolve(commitDeclaration(input));
if (!applyPromise)
throw new LocalError("commit was called before plan", "committing vault deploy", { code: "VAULT_LANE_OUT_OF_ORDER" });
result = await applyPromise;
return result;
},
abandon(reason) {
if (settled)
return;
authorized.reject(reason instanceof Error
? reason
: new LocalError("the vault deploy was abandoned before commit", "committing vault deploy", { code: "VAULT_DEPLOY_ABANDONED", details: { reason } }));
},
result: () => result,
};
return lane;
}
function commitDeclaration(input) {
if (input.allow_unvaulted)
return { allow_unvaulted: true, override_reason: input.allow_unvaulted.reason };
const tokenId = input.activation_token?.object_id;
if (!tokenId) {
throw new LocalError("a vaulted commit needs an activation token and none was minted", "committing vault deploy", { code: "VAULT_ACTIVATION_TOKEN_MISSING" });
}
return { activation_token_id: tokenId };
}
/** `run402 repos policy required` — the offer D3 attaches to every ungated deploy. */
export function vaultPolicyRequiredNextAction(repoId) {
return {
type: "vault_policy_required",
command: "run402 repos policy required",
why: `vault ${repoId} exists but vault_policy was never set, so this deploy ran without requiring a vaulted capture. Set it to require one, or run \`run402 repos policy grandfathered --reason <why>\` to explicitly accept the current state.`,
};
}
/** The standing `warnings[]` entry D3 attaches to every ungated deploy, until the policy is set either way. */
export function vaultUngatedWarning(repoId) {
return {
code: "VAULT_POLICY_UNSET",
severity: "low",
requires_confirmation: false,
message: `vault ${repoId} is not gated: vault_policy was never set, so a deploy can activate without a vaulted capture. Run \`run402 repos policy required\` (or \`grandfathered --reason <why>\` to explicitly accept this) to clear the drift.`,
affected: [repoId],
};
}
/** Attach D3's offer + warning to an ungated deploy's result, without disturbing anything else on it. */
function decorateUngatedResult(result, repoId) {
return {
...result,
warnings: [...(result.warnings ?? []), vaultUngatedWarning(repoId)],
next_actions: [...(result.next_actions ?? []), vaultPolicyRequiredNextAction(repoId)],
};
}
/**
* Deploy, taking the project's `vault_policy` into account.
*
* NOT `required` — no vault, an unreadable policy, a Core target, or an
* explicit `grandfathered` — runs `apply()` with the caller's own options and
* nothing else. That path must stay byte-identical: it is every non-vault
* user's deploy, and the only cost this function may add to it is the single
* policy read that determines the project is not `required`.
*/
export async function applyWithVault(options) {
const applyOpts = {
...(options.apply ?? {}),
...(options.target ? { target: options.target } : {}),
};
const plain = async (mode) => ({
mode,
deploy: await options.sdk._applyEngine.apply(options.spec, applyOpts),
vault: null,
});
// A self-hosted Core gateway has no vault gate; never spend a round trip.
if (options.target === "core")
return plain({ kind: "none", reason: "no_vault" });
const record = await readVaultRecord(options.sdk.repos, options.spec.project_id);
if (!record)
return plain({ kind: "none", reason: "policy_unreadable" });
if (record.vault_policy === "grandfathered") {
return plain({ kind: "grandfathered", repo_id: record.repo_id });
}
if (record.vault_policy !== "required") {
// D3: allocation no longer sets `vault_policy`, so a vault whose
// policy is `null` is the ORDINARY shape, not a weakened one — the
// deploy is never blocked or interactively prompted on this. It still
// runs the untouched plain path (same options object, no vault hooks);
// the only difference from `grandfathered` is that the result carries an
// offer to gate, plus a standing warning naming the drift until the
// policy is set either way (spec scenario "Ungated drift stays visible").
const deploy = await options.sdk._applyEngine.apply(options.spec, applyOpts);
return { mode: { kind: "ungated", repo_id: record.repo_id }, deploy: decorateUngatedResult(deploy, record.repo_id), vault: null };
}
const lane = createApplyDeployLane({ engine: options.sdk._applyEngine, spec: options.spec, apply: applyOpts });
let vault;
try {
vault = await options.sdk.repos.deploy({
lane,
repo_id: record.repo_id,
repo_dir: options.repo_dir ?? process.cwd(),
...(options.keystore_root !== undefined ? { keystore_root: options.keystore_root } : {}),
...(options.allow_unvaulted ? { allow_unvaulted: options.allow_unvaulted } : {}),
...(options.onCommitLine ? { onCommitLine: options.onCommitLine } : {}),
...(options.allowDirty ? { snapshot: { allowDirty: true } } : {}),
});
}
catch (err) {
// Every throw here is a refusal that precedes activation — a snapshot
// refusal, `SNAPSHOT_MOVED_DURING_DEPLOY`, or an unauthorized override.
// Unwind the apply so no plan is left mid-flight, then surface the
// refusal as itself: it is the caller's answer, not something to retry
// under a fresh capture.
lane.abandon(err);
throw enrichCaptureRefusal(err, record.repo_id);
}
finally {
lane.abandon(new LocalError("the deploy ended without committing this plan", "committing vault deploy", { code: "VAULT_DEPLOY_ABANDONED" }));
}
return { mode: { kind: "vaulted", repo_id: record.repo_id }, deploy: lane.result(), vault };
}
/**
* Read the vault record, or `null` when it cannot be read.
*
* A read that fails for ANY reason is not a deploy failure: the gateway is the
* authority on the policy and refuses the commit itself if a capture was
* genuinely required. Turning a transient control-plane blip into a failed
* deploy for a project that has no vault at all would be the worse trade.
*/
async function readVaultRecord(repos, projectId) {
try {
return await repos.forProject(projectId);
}
catch {
return null;
}
}
/**
* A `required` project whose keystore this machine does not hold cannot
* capture. The protocol's own code is PRESERVED (`KEYSTORE_MISSING` /
* `VAULT_REPO_STATE_MISSING` — surfacing the typed outcome rather than
* substituting one), with actions prepended that actually resolve it.
*/
function enrichCaptureRefusal(err, repoId) {
if (!isRun402Error(err))
return err;
const e = err;
if (e.code !== "KEYSTORE_MISSING" && e.code !== "VAULT_REPO_STATE_MISSING")
return err;
return new LocalError(`${e.message} — this project's vault_policy is \`required\`, so a deploy from this machine needs the vault keystore`, "capturing for a vault-required deploy", {
code: e.code,
details: { repo_id: repoId, ...(e.details && typeof e.details === "object" ? e.details : {}) },
next_actions: [
{ action: "restore the vault keystore onto this machine (run402 repos view prints keystore.root; back it up from the machine that holds it)" },
{ action: 'run402 repos policy grandfathered --reason "<why>" — un-gate the project so deploys activate without a capture (owner + step-up, audited, doctor-persistent advisory)' },
...(Array.isArray(e.next_actions) ? e.next_actions : []),
],
});
}
//# sourceMappingURL=vault-apply.js.map