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.

273 lines • 18.5 kB
/** * The secret gate: every SDK method whose result carries a one-time secret the * caller would otherwise receive (a grant key, a Handoff or Invite Key, a Room * Invite Key, project credentials, a private key, a Lightning pairing, a * sign-in session or write-approval token), or whose input IS such a bearer * secret, calls {@link gateSecret} as its first statement. On a client whose * `capabilities.returnSecrets` is false — the `sandbox` surface an MCP `run` * snippet executes on — that call throws `SECRET_REQUIRES_CLI` before any * request, naming the exact CLI command for the same operation. On every * other surface it is a no-op. * * The gate is the method itself, keyed on a client capability: there is no * tool-name denylist and no path filter anywhere else, so every future door * that builds a `sandbox` client inherits it for free. * * {@link SECRET_RETURNING_METHODS} is the registry: one entry per gated * method (by its dotted path from the client root), with the command builder * and, for a method that carries a secret only when a particular option is * supplied, the predicate that says so. `sdk/src/node/secret-gate.test.ts` * owns the inventory: it fails when a method's types carry a secret-named * field and no entry names it, and when a registered method reaches the * network on a `sandbox` client. * * A command never embeds a secret value: where the input is the secret, the * command carries its placeholder (`<kgh1_…>`), never the key. */ import { SecretRequiresCliError } from "./errors.js"; const str = (v) => (typeof v === "string" && v.trim() ? v.trim() : null); const obj = (v) => (v && typeof v === "object" ? v : {}); /** Quote a shell word only when it needs it. */ const word = (v) => (/^[A-Za-z0-9_./:=@+,-]+$/.test(v) ? v : `'${v.replaceAll("'", "'\\''")}'`); /** A caller-supplied value as a shell word, or the placeholder (never quoted) when absent. */ const arg = (v, placeholder) => { const s = str(v); return s ? word(s) : placeholder; }; const projectFlag = (projectId) => { const id = str(projectId); return id ? ` --project ${word(id)}` : ""; }; const orgFlag = (orgId) => { const id = str(orgId); return id ? ` --org ${word(id)}` : ""; }; const vaultFlags = (opts) => { const o = obj(opts); const repo = str(o.repo_id); return `${projectFlag(o.project_id)}${repo ? ` --repo ${word(repo)}` : ""}`; }; const hasToken = (opts) => Boolean(str(obj(opts).token)); /** A wallet name only when it is plainly one; anything else (a pasted key) becomes the placeholder. */ const walletName = (v) => { const s = str(v); return s && s.length <= 40 && /^[a-z0-9][a-z0-9_-]*$/.test(s) ? s : "<name>"; }; const SIGN_IN_WHY = "A sign-in session token is a person's credential; the CLI holds it for them."; const PROJECT_KEYS_WHY = "Project keys are credentials; the CLI stores them in the local key cache without handing them to a snippet."; /** * Every gated method, by its dotted path from the client root. The command is * the CLI line for the same operation; the drift test calls each one on a * `sandbox` client and requires the refusal before any request. */ export const SECRET_RETURNING_METHODS = { // ── grants: a grant key's bearer prints once ──────────────────────────── "grants.create": { when: (_projectId, input) => obj(input).key != null && obj(input).key !== false, command: (projectId, input) => { const i = obj(input); return `run402 grants create ${arg(i.wallet, "<wallet>")} --capability ${arg(i.capability, "<capability>")} --key${projectFlag(projectId)}`; }, why: "A grant key's bearer is returned once; the CLI prints it to the person who will hand it on.", }, "grants.createKey": { command: (projectId) => `run402 grants create <wallet> --capability <capability> --key${projectFlag(projectId)}`, why: "A grant key's bearer is returned once; the CLI mints keys by issuing the grant with --key.", }, "grants.rotateKey": { command: (projectId, keyId) => `run402 grants rotate-key ${arg(keyId, "<key_id>")}${projectFlag(projectId)}`, why: "The rotated grant key's bearer is returned once; the CLI prints it to the person who will hand it on.", }, // ── KyGit: Handoff and Invite Keys ─────────────────────────────────────── "repos.handoff": { command: (opts) => `run402 repos handoff${vaultFlags(opts)}` }, "repos.invite": { command: (opts) => { const room = str(obj(opts).roomKey); return `run402 repos invite${vaultFlags(opts)}${room ? ` --room ${word(room)}` : ""}`; }, }, "repos.resume": { command: () => "run402 repos resume <kgh1_…>", why: "A Handoff Key is a bearer secret; redeem it from the CLI so it never enters a snippet." }, "repos.join": { command: () => "run402 repos join <kgi1_…>", why: "An Invite Key is a bearer secret; redeem it from the CLI so it never enters a snippet." }, // ── rooms: Room Invite Keys ────────────────────────────────────────────── "rooms.invite": { command: (orgId, roomKey) => { const room = str(roomKey); return `run402 rooms invite${orgFlag(orgId)}${room ? ` --room ${word(room)}` : ""}`; }, }, "rooms.join": { command: () => "run402 rooms join <kri1_…>", why: "A Room Invite Key is a bearer secret; redeem it from the CLI so it never enters a snippet." }, // ── projects and their credentials ─────────────────────────────────────── "projects.provision": { command: (opts) => { const o = obj(opts); const name = str(o.name); const tier = str(o.tier); return `run402 projects provision${tier ? ` --tier ${word(tier)}` : ""}${name ? ` --name ${word(name)}` : ""}`; }, why: PROJECT_KEYS_WHY, }, "projects.list": { when: (opts) => hasToken(opts), command: () => "run402 projects list --all", why: SIGN_IN_WHY, }, "projects.info": { command: (id) => `run402 credentials project-keys status${projectFlag(id)}`, why: PROJECT_KEYS_WHY }, "projects.keys": { command: (id) => `run402 credentials project-keys export${projectFlag(id)} --reveal`, why: PROJECT_KEYS_WHY }, "credentials.projectKeys.import": { command: (projectId) => `run402 credentials project-keys import${projectFlag(projectId)} --service-key-stdin`, why: PROJECT_KEYS_WHY, }, "credentials.projectKeys.export": { command: (projectId) => `run402 credentials project-keys export${projectFlag(projectId)} --reveal`, why: PROJECT_KEYS_WHY }, "credentials.issue": { command: (projectId, input) => { const i = obj(input); return `run402 credentials issue --kind ${arg(i.kind, "<anon|service>")} --name ${arg(i.name, "<name>")}${projectFlag(projectId)}`; }, why: "A project credential's secret is returned once.", }, "credentials.rotate": { command: (projectId, credentialId) => `run402 credentials rotate ${arg(credentialId, "<credential_id>")}${projectFlag(projectId)}`, why: "The rotated credential's secret is returned once.", }, "credentials.mintToken": { command: (projectId, opts) => { const kind = str(obj(opts).kind); return `run402 credentials token${projectFlag(projectId)}${kind ? ` --kind ${word(kind)}` : ""}`; }, why: "A project token is a bearer credential.", }, "apps.fork": { command: (opts) => { const o = obj(opts); const sub = str(o.subdomain); return `run402 apps fork ${arg(o.versionId, "<version_id>")} ${arg(o.name, "<name>")}${sub ? ` --subdomain ${word(sub)}` : ""}`; }, why: PROJECT_KEYS_WHY, }, "branches.create": { command: (projectId) => `run402 branches create${projectFlag(projectId)}`, why: PROJECT_KEYS_WHY }, "admin.transfers.initiate": { when: (input) => Boolean(str(obj(input).toOrgId)), command: (input) => { const i = obj(input); return `run402 transfer init --to-org ${arg(i.toOrgId, "<org_id>")}${projectFlag(i.projectId)}`; }, why: PROJECT_KEYS_WHY, }, "admin.transfers.accept": { command: (transferId) => `run402 transfer accept ${arg(transferId, "<transfer_id>")}`, why: PROJECT_KEYS_WHY, }, "admin.rotateWebhookSecret": { command: () => "run402 webhook-secret rotate", why: "The new webhook signing secret is returned once.", }, // ── the local wallet ───────────────────────────────────────────────────── "wallets.create": { command: (name) => (str(name) ? `run402 wallets new ${walletName(name)}` : "run402 init"), why: "Creating a wallet writes a private key; the CLI creates wallets for a person.", }, "wallets.import": { command: (name) => `run402 wallets import ${walletName(name)} --key <path|->`, why: "Importing a wallet hands over a private key; read it from a file or stdin in the CLI, never from a snippet.", }, "agent.lightningWallet.mint": { command: () => "run402 init lightning", why: "The Lightning wallet's pairing secret is returned once." }, "agent.lightningWallet.get": { command: () => "run402 wallets lightning status", why: "The first read of an active Lightning wallet hands out its pairing secret." }, "agent.lightningWallet.waitForActive": { command: () => "run402 init lightning", why: "The first read of an active Lightning wallet hands out its pairing secret." }, "init": { when: (opts) => obj(opts).rail === "lightning", command: () => "run402 init lightning", why: "The Lightning wallet's pairing secret is returned once and stored beside the wallet key.", }, // ── actions that provision and hand back project keys ──────────────────── "actions.run": { when: (input) => obj(input).type !== "tier.set", command: (input) => (obj(input).type === "projects.provision" ? "run402 projects provision" : "run402 up"), why: "up and project provisioning hand back project keys; run them from the CLI or the MCP up tool.", }, "actions.up": { command: () => "run402 up", why: "up provisions projects and handles their keys; run it from the CLI or the MCP up tool." }, // ── a person's sign-in session and write approvals ─────────────────────── "session.exchangeCliToken": { command: () => "run402 login", why: SIGN_IN_WHY }, "session.devicePoll": { command: () => "run402 login --device", why: SIGN_IN_WHY }, "session.verifyEmail": { command: () => "run402 login", why: SIGN_IN_WHY }, "session.passkeyVerify": { command: () => "run402 login", why: SIGN_IN_WHY }, "session.consumeRecoveryCode": { command: () => "run402 login", why: SIGN_IN_WHY }, "session.whoami": { when: hasToken, command: () => "run402 whoami", why: SIGN_IN_WHY }, "session.refresh": { command: () => "run402 login", why: SIGN_IN_WHY }, "session.revoke": { when: hasToken, command: () => "run402 logout", why: SIGN_IN_WHY }, "session.enrollPasskeyOptions": { when: hasToken, command: () => "run402 login", why: SIGN_IN_WHY }, "session.enrollPasskeyVerify": { when: hasToken, command: () => "run402 login", why: SIGN_IN_WHY }, "session.stepUpOptions": { when: hasToken, command: () => "run402 login", why: SIGN_IN_WHY }, "session.stepUpVerify": { when: hasToken, command: () => "run402 login", why: SIGN_IN_WHY }, "session.issueRecoveryCodes": { command: () => "run402 login", why: "Recovery codes are shown once." }, "session.listAuthenticators": { when: hasToken, command: () => "run402 whoami", why: SIGN_IN_WHY }, "session.revokeAuthenticator": { when: hasToken, command: () => "run402 login", why: SIGN_IN_WHY }, "session.sourceAccessWrappers": { when: hasToken, command: () => "run402 repos recovery-bundle", why: SIGN_IN_WHY }, "session.sourceAccessRecoveryBundle": { when: hasToken, command: () => "run402 repos recovery-bundle", why: SIGN_IN_WHY }, "writeApproval.requestChallenge": { command: () => "run402 approve", why: "A write approval is a person's passkey-signed credential." }, "writeApproval.exchangeClaimCode": { command: () => "run402 approve", why: "A write approval is a person's passkey-signed credential." }, "me.overview": { when: hasToken, command: () => "run402 orgs list", why: SIGN_IN_WHY }, "me.status": { when: hasToken, command: () => "run402 whoami", why: SIGN_IN_WHY }, "orgs.adopt.challenge": { when: hasToken, command: () => "run402 orgs adopt", why: SIGN_IN_WHY }, "orgs.adopt.submit": { when: hasToken, command: () => "run402 orgs adopt", why: SIGN_IN_WHY }, "admin.channels.connectTelegram": { when: (_opts, proofs) => hasToken(proofs), command: () => "run402 contacts connect telegram", why: SIGN_IN_WHY }, "admin.channels.revokeTelegram": { when: (_id, proofs) => hasToken(proofs), command: (id) => `run402 contacts rm ${arg(id, "<binding_id>")}`, why: SIGN_IN_WHY }, "admin.rules.create": { when: (_i, proofs) => hasToken(proofs), command: () => "run402 subscriptions add", why: SIGN_IN_WHY }, "admin.rules.update": { when: (_id, _p, proofs) => hasToken(proofs), command: () => "run402 subscriptions add", why: SIGN_IN_WHY }, "admin.rules.delete": { when: (_id, proofs) => hasToken(proofs), command: (id) => `run402 subscriptions rm ${arg(id, "<rule_id>")}`, why: SIGN_IN_WHY }, "admin.setNotificationPreferences": { when: (_p, proofs) => hasToken(proofs), command: () => "run402 contacts preferences", why: SIGN_IN_WHY }, // ── project end users' session tokens (the project's own auth) ─────────── "auth.verifyMagicLink": { command: (projectId) => `run402 auth verify --token <token>${projectFlag(projectId)}`, why: "The end user's access token is returned once." }, "auth.verify": { command: (projectId) => `run402 auth verify --token <token>${projectFlag(projectId)}`, why: "The end user's access token is returned once." }, "auth.verifyEmailCode": { command: (projectId, opts) => { const challenge = str(obj(opts).challengeId); return `run402 auth verify --challenge-id ${word(challenge ?? "<challenge_id>")} --code <code>${projectFlag(projectId)}`; }, why: "The end user's access token is returned once.", }, "auth.setUserPassword": { command: (projectId) => `run402 auth set-password --token <bearer> --new <password>${projectFlag(projectId)}`, why: "An end user's access token is a bearer credential." }, "auth.setPassword": { command: (projectId) => `run402 auth set-password --token <bearer> --new <password>${projectFlag(projectId)}`, why: "An end user's access token is a bearer credential." }, "auth.createPasskeyRegistrationOptions": { command: (projectId) => `run402 auth passkey-register-options --token <bearer> --app-origin <origin>${projectFlag(projectId)}`, why: "An end user's access token is a bearer credential." }, "auth.verifyPasskeyRegistration": { command: (projectId) => `run402 auth passkey-register-verify --token <bearer>${projectFlag(projectId)}`, why: "An end user's access token is a bearer credential." }, "auth.verifyPasskeyLogin": { command: (projectId) => `run402 auth passkey-login-verify${projectFlag(projectId)}`, why: "The end user's access token is returned once." }, "auth.listPasskeys": { command: (projectId) => `run402 auth passkeys --token <bearer>${projectFlag(projectId)}`, why: "An end user's access token is a bearer credential." }, "auth.deletePasskey": { command: (projectId) => `run402 auth delete-passkey --token <bearer>${projectFlag(projectId)}`, why: "An end user's access token is a bearer credential." }, "live.changes": { when: (_projectId, opts) => obj(opts).as === "user", command: (projectId, opts) => `run402 live --tables ${arg((obj(opts).tables ?? []).join(","), "<a,b>")}${projectFlag(projectId)}`, why: "An end user's access token is a bearer credential.", }, "live.subscribe": { when: (_projectId, opts) => obj(opts).as === "user", command: (projectId, opts) => `run402 live --tables ${arg((obj(opts).tables ?? []).join(","), "<a,b>")}${projectFlag(projectId)}`, why: "An end user's access token is a bearer credential.", }, // ── bearer inputs from outside ─────────────────────────────────────────── "escalations.ackWithToken": { command: () => "run402 escalations ack <escalation_id>", why: "An acknowledgement token is a bearer secret from a notification; acknowledge by id instead.", }, "ci.exchangeToken": { command: () => "run402 ci link github", why: "The GitHub OIDC exchange mints a Run402 CI session inside GitHub Actions; link the repository from the CLI.", }, }; /** Dotted path → entry, typed loosely for callers that look methods up by string. */ export const SECRET_GATE_REGISTRY = SECRET_RETURNING_METHODS; /** * Refuse this call before any request when the client may not return * secrets. The first statement of every registered method. The command is * built only on refusal, so the check costs nothing on the permissive path. */ export function gateSecret(client, method, ...args) { if (client.capabilities?.returnSecrets !== false) return; const entry = SECRET_GATE_REGISTRY[method]; if (entry.when && !entry.when(...args)) return; const refusal = { command: entry.command(...args), ...(entry.why ? { why: entry.why } : {}) }; if (client.assertSecretReturn) client.assertSecretReturn(refusal); throw new SecretRequiresCliError(refusal.command, refusal.why); } //# sourceMappingURL=secret-gate.js.map