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.

159 lines • 8.3 kB
/** Shared adapter view of a workflow. Detailed SDK results remain unchanged. */ import { randomBytes } from "node:crypto"; import { mkdirSync, writeFileSync, readdirSync, lstatSync, unlinkSync, chmodSync } from "node:fs"; import { join, resolve } from "node:path"; const TTL_MS = 30 * 60 * 1000; const MAX_FILES = 32; const MAX_BYTES = 2 * 1024 * 1024; const PRIVATE_FIELDS = /(?:secret|password|token|authorization|cookie|signature|proof|credential|private.?key|api.?key|service.?key|anon.?key|key$)|^(?:env|headers|body|sql|custom_sql|spec|manifest|value|stdout|stderr|input|message|details)$/i; const object = (v) => !!v && typeof v === "object" && !Array.isArray(v); /** Authored payloads and credential-bearing fields never enter the diagnostic copy. */ export function redactWorkflowDiagnostics(value) { if (Array.isArray(value)) return value.map(redactWorkflowDiagnostics); if (!object(value)) return typeof value === "string" ? value.replace(/https?:\/\/[^\s"\x27]+/gi, (address) => { try { const url = new URL(address); url.username = ""; url.password = ""; url.search = ""; url.hash = ""; return url.toString(); } catch { return "[redacted URL]"; } }).replace(/Bearer\s+[^\s"\x27]+/gi, "Bearer [redacted]").replace(/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, "[redacted]") : value; return Object.fromEntries(Object.entries(value).map(([key, val]) => [key, PRIVATE_FIELDS.test(key) ? "[redacted]" : redactWorkflowDiagnostics(val)])); } function diagnosticPath(dir, value) { const data = JSON.stringify(redactWorkflowDiagnostics(value), null, 2); if (Buffer.byteLength(data) > MAX_BYTES) return null; const root = join(resolve(dir), ".run402"); const folder = join(root, "diagnostics"); for (const path of [root, folder]) { mkdirSync(path, { recursive: true, mode: 0o700 }); if (lstatSync(path).isSymbolicLink() || !lstatSync(path).isDirectory()) return null; chmodSync(path, 0o700); } const now = Date.now(); const files = readdirSync(folder).filter((name) => /^result-[a-f0-9]{32}\.json$/.test(name)) .map((name) => ({ name, stat: lstatSync(join(folder, name)) })) .filter(({ stat }) => stat.isFile()).sort((a, b) => a.stat.mtimeMs - b.stat.mtimeMs); while (files.length && (files.length >= MAX_FILES || now - files[0].stat.mtimeMs > TTL_MS)) unlinkSync(join(folder, files.shift().name)); const path = join(folder, `result-${randomBytes(16).toString("hex")}.json`); writeFileSync(path, data, { flag: "wx", mode: 0o600 }); return path; } const pick = (value, keys) => object(value) ? Object.fromEntries(keys.filter(key => key in value).map(key => [key, value[key]])) : {}; const OUTCOME = ["status", "state", "code", "category", "reason", "message", "error", "mutation_state", "safe_to_retry", "retryable", "warnings", "next_actions", "cost", "costs", "network", "rail", "payment"]; function verification(value) { if (!object(value)) return value; const report = object(value.report) ? value.report : value; const summary = pick(report, [...OUTCOME, "coherent", "checked_at", "pending_count", "path_count", "total_path_count", "vantage", "strength"]); if (Array.isArray(report.paths)) { summary.confidence_counts = report.paths.reduce((counts, path) => { const key = object(path) && typeof path.observed_confidence === "string" ? path.observed_confidence : "unknown"; counts[key] = (counts[key] ?? 0) + 1; return counts; }, {}); const pending = report.paths.filter(path => !object(path) || path.state !== "coherent"); if (pending.length) summary.paths = pending.slice(0, 10).map(path => pick(path, ["path", "state", "status", "observed_confidence"])); if (pending.length > 10) summary.paths_truncated = true; } return { ...summary, ...pick(value, ["attempts", "elapsedMs"]) }; } function deployment(value) { if (!object(value)) return value; const summary = pick(value, [...OUTCOME, "release_id", "operation_id", "release_generation", "urls", "rehearsal", "static_continuity"]); if (object(value.urls)) summary.urls = pick(value.urls, ["site", "console", "mcp", "deployment", "deployment_id"]); if (object(value.diff)) summary.diff = pick(value.diff, ["is_noop", "summary"]); return summary; } function repository(value) { if (!object(value)) return value; const summary = pick(value, [...OUTCOME, "first_push_error", "vault_error", "local_git", "toplevel", "vault", "first_push"]); if (object(value.vault)) summary.vault = pick(value.vault, [...OUTCOME, "name", "url", "allocated", "created_repository", "already_present", "nested", "enclosing_toplevel", "excluded_in_enclosing"]); if (object(value.first_push)) summary.first_push = pick(value.first_push, ["generation", "form", "vault_commit", "snapshot", "captured_dirty"]); return summary; } /** Preserve outcome and recovery fields, and point to the same execution's redacted detail. */ export function prepareWorkflowOutput(value, dir, options = {}) { if (!object(value) || !object(value.result)) return value; // A check, plan-only or read-only call must not start writing local artifacts. if (!options.storeDetails && (value.dry_run === true || value.read_only === true || value.mode !== "apply")) return value; let ref; let action; try { const stored = options.storeDetails?.(redactWorkflowDiagnostics(value)); ref = options.storeDetails ? stored?.ref ?? null : diagnosticPath(dir, value); action = stored?.next_action; } catch { return value; } // never lose detail when local storage is unavailable if (!ref) return value; const result = { ...value.result }; if (object(result.deploy)) result.deploy = deployment(result.deploy); if (object(result.repo)) result.repo = repository(result.repo); if (object(result.edge_coherence)) result.edge_coherence = verification(result.edge_coherence); delete result.app_graph; delete result.spec; delete result.manifest; if (object(result.app_result)) { const app = { ...result.app_result }; delete app.graph; // This is a display projection, not the complete run402.up.result schema. delete app.schema_url; app.kind = "run402.up.summary"; app.schema_version = "run402.up.summary.v1"; if (object(app.release)) app.release = deployment(app.release); if (object(app.verification)) app.verification = verification(app.verification); result.app_result = app; } if (object(result.plan)) { const plan = { ...result.plan }; // Keep summary, permissions, warnings, cost and next actions; omit repeated file inventories. for (const key of ["site", "functions", "routes", "assets", "missing_content"]) delete plan[key]; result.plan = plan; } const steps = Array.isArray(value.steps) ? value.steps.filter(step => object(step) && (step.mutation === true || step.state === "failed")).map((step) => { if (!object(step)) return step; return pick(step, ["id", "action", "state", "mutation", "code", "reason", "next_actions"]); }) : value.steps; return { ...value, steps, step_count: Array.isArray(value.steps) ? value.steps.length : 0, result, result_ref: ref, truncation: { kind: "display", has_more: true, redacted: true }, next_actions: [...(Array.isArray(value.next_actions) ? value.next_actions : []), action ?? { type: "inspect_file", path: ref, why: "Read this execution's redacted detail from the local diagnostic file. It is not a remote URL; retained for at most 30 minutes on subsequent writes, bounded to 32 files.", }], }; } //# sourceMappingURL=workflow-output.js.map