Skip to content

Latest commit

 

History

History
459 lines (349 loc) · 21.6 KB

File metadata and controls

459 lines (349 loc) · 21.6 KB

run402 logo

run402 — Postgres, storage & deploys for AI agents

Tests CodeQL npm: @run402/sdk npm: run402 npm: run402-mcp npm: @run402/functions License: MIT

Run402 gives an agent a full Postgres database, REST API, user auth, content-addressed file storage, static site hosting, serverless functions, and image generation — provisioned with one call, paid with x402 USDC on Base (or Stripe credits). The prototype tier is free on testnet.

This monorepo ships every surface an agent can pick up:

Package Use when…
@run402/sdk Calling Run402 from TypeScript — typed kernel, isomorphic (Node 22 / Deno / Bun / V8 isolates) with a Node entry that auto-loads the local keystore + allowance + x402 fetch
run402 CLI Terminal, scripts, CI, agent-controlled shells — JSON in, JSON out, exit code on failure
run402-mcp Claude Desktop, Cursor, Cline, Claude Code — every CLI capability as an MCP tool
OpenClaw skill OpenClaw agents (no MCP server required)
@run402/functions Imported inside deployed functions (db(req), adminDb(), getUser(), email, ai) and for TypeScript autocomplete in your editor

All four shipped surfaces release in lockstep at the same version and share a single typed kernel: @run402/sdk. MCP tools, CLI subcommands, and OpenClaw scripts are thin shims over SDK calls. Pick whichever interface fits your runtime.

30-second start

npm install -g run402
run402 init                                          # creates allowance, requests testnet faucet
run402 tier set prototype                            # free on testnet (verifies x402 setup)
run402 projects provision --name my-app              # → anon_key, service_key, project_id
run402 sites deploy-dir ./dist                       # incremental deploy of a directory → live URL
run402 subdomains claim my-app                       # → https://my-app.run402.com

That's a real Postgres database + a deployed static site, paid for autonomously with testnet USDC.

The patterns

Paste-and-go assets — content-addressed URLs with SRI

blobs.put() returns an AssetRef whose scriptTag() / linkTag() / imgTag() emitters produce HTML with the URL, the SRI integrity hash, and modern best-practice attributes (defer, loading="lazy", decoding="async", crossorigin) already wired. The URL is content-addressed (pr-<public_id>.run402.com/_blob/<key>-<8hex>.<ext>), served through the v1.33 CDN, and never needs invalidation:

import { run402 } from "@run402/sdk/node";
const r = run402();

const logo  = await r.blobs.put(projectId, "logo.png", { bytes: pngBytes });
const app   = await r.blobs.put(projectId, "app.js",   { content: jsSource });
const style = await r.blobs.put(projectId, "app.css",  { content: css });

const html = `
<!doctype html>
<html>
  <head>${style.linkTag()}${app.scriptTag({ type: "module" })}</head>
  <body>${logo.imgTag("Company logo")}</body>
</html>
`;

immutable: true is the default — the SDK computes the SHA-256 client-side, the gateway returns a content-hashed URL, and the browser refuses execution on byte mismatch. No cache-invalidation choreography, no waiting, no integrity-attribute construction.

Dark-by-default tables + the expose manifest

Tables you create are unreachable via /rest/v1/* until you declare them in a manifest. That closes the "agent created a table, forgot to set RLS, data leaked" footgun. The manifest is convergent — applying it twice is a no-op; items removed between applies have their policies, grants, triggers, and views dropped.

cat > manifest.json <<'EOF'
{
  "$schema": "https://run402.com/schemas/manifest.v1.json",
  "version": "1",
  "tables": [
    { "name": "items",  "expose": true,  "policy": "user_owns_rows",
      "owner_column": "user_id", "force_owner_on_insert": true },
    { "name": "audit",  "expose": false }
  ],
  "views": [
    { "name": "leaderboard", "base": "items", "select": ["user_id", "score"], "expose": true }
  ],
  "rpcs": [
    { "name": "compute_streak", "signature": "(user_id uuid)", "grant_to": ["authenticated"] }
  ]
}
EOF

run402 projects apply-expose <project_id> --file manifest.json
run402 projects get-expose   <project_id>

Built-in policies: user_owns_rows (rows where owner_column = auth.uid(); with force_owner_on_insert: true a BEFORE INSERT trigger sets it), public_read_authenticated_write (anyone reads, any authenticated user writes), public_read_write_UNRESTRICTED (fully open; requires i_understand_this_is_unrestricted: true), and custom (escape hatch — your own CREATE POLICY SQL).

Auth-as-SDLC alternative: drop the same JSON as manifest.json into your bundle's files[] and the gateway reads it, validates it against your migration SQL, applies it, and strips it before serving the site — so authorization travels with your code and never gets publicly served. The deploy returns manifest_applied: true; if a table referenced by the manifest isn't in the migration, the deploy is rejected with a structured errors array listing every violation.

Slick deploys — deployDir + plan/commit + progress

deployDir walks a local directory, hashes every file client-side, asks the gateway which bytes it doesn't already have, and PUTs only those. Re-deploying an unchanged tree returns immediately with bytes_uploaded: 0.

import { run402 } from "@run402/sdk/node";

const r = run402();
const { url, bytes_uploaded, bytes_total } = await r.sites.deployDir({
  project: projectId,
  dir: "./dist",
  onEvent: (e) => process.stderr.write(JSON.stringify(e) + "\n"),
});

Progress events stream over onEvent (or stderr from the CLI). Both the unified DeployEvent shapes (from the v2 deploy primitive) and the legacy phase events below are emitted for back-compat:

phase Fires Extra
plan After the plan response is parsed manifest_size (file count)
upload After each missing file's bytes finish PUTing file, sha256, done, total
commit Just before the commit POST
poll Per server-side copy poll tick status, elapsed_ms

CLI:

run402 sites deploy-dir ./dist --project prj_… > result.json 2> events.log

In-function helpers — caller-context vs BYPASSRLS

Inside a deployed function, import from @run402/functions. Two distinct DB clients keep RLS clean:

import { db, adminDb, getUser, email, ai } from "@run402/functions";

export default async (req: Request) => {
  const user = await getUser(req);
  if (!user) return new Response("unauthorized", { status: 401 });

  // Caller-context — Authorization header is forwarded; RLS evaluates against the caller's role.
  const mine = await db(req).from("items").select("*").eq("user_id", user.id);

  // BYPASSRLS — for platform-authored writes (audit logs, cron cleanup, webhook handlers).
  await adminDb().from("audit").insert({ event: "items_read", user_id: user.id });

  // Send mail from the project's mailbox — discovers it automatically.
  if (mine.length === 0) {
    await email.send({ to: user.email, subject: "Welcome", html: "<h1>hi</h1>" });
  }

  return Response.json(mine);
};

adminDb().sql(query, params?) runs raw parameterized SQL and always bypasses RLS. It returns a flat Promise<Record<string, unknown>[]> (just the rows — no envelope):

import { adminDb, getUser } from "@run402/functions";

export default async (req: Request) => {
  const user = await getUser(req);
  if (!user) return new Response("unauthorized", { status: 401 });

  const rows = await adminDb().sql(
    "SELECT count(*)::int AS n FROM items WHERE user_id = $1",
    [user.id],
  );
  const n = (rows[0]?.n as number | undefined) ?? 0;
  return Response.json({ count: n });
};

@run402/functions is auto-bundled into deployed code; install it in your editor for full TypeScript autocomplete (also works at build time for static-site generation with RUN402_SERVICE_KEY + RUN402_PROJECT_ID set).

Calling from outside a function entirely (raw curl/fetch from CI scripts, bash bootstrappers, non-TS runtimes) — service-key writes go to /admin/v1/rest/<table>, not /rest/v1/*. The gateway 403s service-role tokens on /rest/v1/* so a leaked key can't silently bypass RLS, which means curl ... > /dev/null against the wrong path looks like success but writes nothing. SQL-shaped admin work uses POST /projects/v1/admin/:id/sql (or run402 projects sql).

curl -X POST https://api.run402.com/admin/v1/rest/audit \
  -H "Authorization: Bearer $RUN402_SERVICE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"event":"seed","ts":"2026-04-30"}'

SDK — @run402/sdk

npm install @run402/sdk

Two entry points:

  • @run402/sdk — isomorphic. Bring your own CredentialsProvider (a session-token shim, a remote vault, anything that resolves project keys + auth headers). Works in Node 22, Deno, Bun, V8 isolates.
  • @run402/sdk/node — Node-only convenience. Reads ~/.config/run402/projects.json, signs x402 payments from the local allowance, exposes sites.deployDir(...).
import { run402 } from "@run402/sdk/node";

const r = run402();
const project = await r.projects.provision({ tier: "prototype" });
await r.blobs.put(project.project_id, "hello.txt", { content: "hi" });

19 namespaces: projects, deploy, sites, blobs, functions, secrets, subdomains, domains, email (+ webhooks), senderDomain, auth, apps, tier, billing, contracts, ai, allowance, service, admin. Every operation throws a typed Run402Error subclass on failure: PaymentRequired, ProjectNotFound, Unauthorized, ApiError, NetworkError, LocalError, Run402DeployError. See sdk/README.md.

CLI — run402

npm install -g run402

Every subcommand prints JSON to stdout, JSON errors to stderr, exits 0 on success and 1 on failure — designed for an agent shell, not a human. Full reference: cli/llms-cli.txt (also at https://run402.com/llms-cli.txt).

run402 init                              # one-shot allowance + faucet + tier check
run402 status                            # account snapshot (allowance, balance, tier, projects)
run402 projects provision --name my-app
run402 projects sql <id> "CREATE TABLE …"
run402 projects apply-expose <id> --file manifest.json
run402 sites deploy-dir ./dist
run402 functions deploy <id> <name> --file fn.ts
run402 blob put ./asset.png --immutable
run402 blob diagnose <url>               # inspect live CDN state for a public URL
run402 cdn wait-fresh <url> --sha <hex>  # poll until a mutable URL serves the new SHA

The active project is sticky: run402 projects use <id> makes <id> the default for every subsequent <id>-taking subcommand, so most commands work without it.

MCP server — run402-mcp

npx -y run402-mcp                        # standalone test

Claude Desktop

Add to ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "run402": { "command": "npx", "args": ["-y", "run402-mcp"] }
  }
}

Cursor

Add to .cursor/mcp.json:

{
  "mcpServers": {
    "run402": { "command": "npx", "args": ["-y", "run402-mcp"] }
  }
}

Cline

Add to your Cline MCP settings:

{
  "mcpServers": {
    "run402": { "command": "npx", "args": ["-y", "run402-mcp"] }
  }
}

Claude Code

claude mcp add run402 -- npx -y run402-mcp

OpenClaw skill

cp -r openclaw ~/.openclaw/skills/run402
cd ~/.openclaw/skills/run402/scripts && npm install

Each script re-exports from cli/lib/*.mjs — the OpenClaw command surface is identical to the CLI command surface by construction. See openclaw/README.md.

MCP tools

The full MCP surface — every tool is a thin shim over an SDK call.

Database

Tool Description
provision_postgres_project Provision a new database. Auto-handles x402 payment.
run_sql Execute SQL (DDL or queries). Returns a markdown table.
rest_query Query/mutate via PostgREST.
apply_expose Apply the declarative authorization manifest (tables, views, RPCs). Convergent — drops items removed between applies.
get_expose Return the current manifest. source is either applied (from the tracking table) or introspected (regenerated from live DB state).
get_schema Introspect tables, columns, types, constraints, RLS policies.
get_usage Per-project usage report (API calls, storage, lease expiry).
promote_user / demote_user Manage project_admin role on a project user.
delete_project Cascade purge — schema, Lambdas, S3 site files, deployments, secrets, published versions. Irreversible.

Blob storage (content-addressed CDN)

Tool Description
blob_put Upload a blob (any size, up to 5 TiB) via direct-to-S3 presigned URLs. Returns an AssetRef with scriptTag() / linkTag() / imgTag() emitters.
blob_get Download a blob to a local file.
blob_ls Keyset-paginated list with prefix filter.
blob_rm Delete a blob.
blob_sign Time-boxed presigned GET URL for a private blob.
diagnose_public_url Live CDN state for a public URL — expected vs observed SHA, cache headers, invalidation status.
wait_for_cdn_freshness Poll a mutable URL until it serves the expected SHA-256.

Sites & subdomains

Tool Description
deploy_site Deploy a static site from inline file bytes.
deploy_site_dir Deploy a static site from a local directory. Routes through the unified deploy primitive (CAS-backed) — only uploads bytes the gateway doesn't have.
claim_subdomain Claim <name>.run402.com (idempotent; reassigns to latest deployment on subsequent deploys).
list_subdomains / delete_subdomain Manage subdomains.
add_custom_domain / list_custom_domains / check_domain_status / remove_custom_domain Point your own domain at a Run402 subdomain.
bundle_deploy One-call full-stack deploy: database + migrations + authorization manifest (manifest.json in files[] — gateway validates it, applies it, then strips it before serving the site) + secrets + functions + site + subdomain.

Functions & secrets

Tool Description
deploy_function Deploy a Node 22 serverless function. Cron-schedulable.
invoke_function Invoke a deployed function (test path).
get_function_logs Recent logs (CloudWatch).
update_function Update schedule / timeout / memory without redeploying code.
list_functions / delete_function List / remove functions.
set_secret / list_secrets / delete_secret Manage process.env secrets injected into all functions.

Auth & email

Tool Description
request_magic_link Send a passwordless login email.
verify_magic_link Exchange the magic link token for access_token + refresh_token.
set_user_password Change, reset, or set a user's password.
auth_settings Toggle allow_password_set for passwordless users.
create_mailbox / get_mailbox / delete_mailbox Per-project mailbox at <slug>@mail.run402.com.
send_email Template (project_invite, magic_link, notification) or raw HTML. Single recipient.
list_emails / get_email / get_email_raw Read messages. get_email_raw returns RFC-822 bytes for DKIM / zk-email verification.
register_mailbox_webhook / list_mailbox_webhooks / get_mailbox_webhook / update_mailbox_webhook / delete_mailbox_webhook Email-event webhooks (delivery, bounced, complained, reply_received).
register_sender_domain / sender_domain_status / remove_sender_domain Send from your own domain (DKIM verified).
enable_sender_domain_inbound / disable_sender_domain_inbound Receive replies on your custom sender domain.

AI helpers

Tool Description
generate_image Text-to-PNG via x402 ($0.03 / image).
ai_translate Translate text. Metered per project.
ai_moderate Moderate text (free).
ai_usage Translation quota (used / included / remaining).

Apps marketplace

Tool Description
browse_apps Browse public forkable apps.
get_app Inspect an app, including expected bootstrap_variables.
fork_app Clone schema + site + functions into a new project. Runs the app's bootstrap function with provided variables.
publish_app Publish a project as a forkable app.
list_versions / update_version / delete_version Manage published versions.

Tier & billing

Tool Description
set_tier Subscribe / renew / upgrade a tier (auto-detects action). x402 payment.
tier_status Current tier and lease expiry.
get_quote Tier pricing (free, no auth).
tier_checkout Stripe checkout for a tier (alternative to x402).
create_email_billing_account / link_wallet_to_account Email-based billing accounts; hybrid Stripe + x402.
billing_history Ledger history.
buy_email_pack $5 for 10,000 emails (never expire).
set_auto_recharge Auto-buy email packs when credits run low.

KMS contract wallets (on-chain signing)

Tool Description
provision_contract_wallet AWS KMS-backed Ethereum wallet. $0.04/day rental + $0.000005 per call. Private keys never leave KMS.
get_contract_wallet / list_contract_wallets Metadata + live native balance.
set_recovery_address / set_low_balance_alert Optional safety nets.
contract_call Submit a write call (chain gas at-cost + KMS sign fee).
contract_read Read-only call (free).
get_contract_call_status Lifecycle, gas, receipt.
drain_contract_wallet Drain native balance (works on suspended wallets — the safety valve).
delete_contract_wallet Schedule KMS key deletion (refused if balance ≥ dust).

Allowance & account

Tool Description
init One-shot setup: allowance + faucet + tier check + project list.
status Full account snapshot (allowance, balance, tier, projects).
allowance_status / allowance_create / allowance_export Local allowance management.
request_faucet Request testnet USDC.
check_balance USDC balance for an allowance address.
list_projects Active projects for a wallet.
pin_project Pin a project (admin only — bypasses lifecycle state machine).
project_info / project_keys / project_use Inspect / set the active project.
create_checkout Stripe checkout to add cash credit.
send_message / set_agent_contact Send feedback to the Run402 team; register agent contact info.

Service status (no auth)

Tool Description
service_status Public availability report — 24h/7d/30d uptime per capability, operator, deployment topology.
service_health Liveness probe with per-dependency results.

Configuration

Variable Default Purpose
RUN402_API_BASE https://api.run402.com API base URL (override for staging)
RUN402_CONFIG_DIR ~/.config/run402 Local credential storage directory
RUN402_ALLOWANCE_PATH {config_dir}/allowance.json Custom allowance file path

Local state lives at:

  • ~/.config/run402/projects.json (0600) — { projects: { <id>: { anon_key, service_key, tier, lease_expires_at } } }
  • ~/.config/run402/allowance.json (0600) — wallet for x402 signing

anon_key and service_key have no expiry — lease enforcement happens server-side. Rotate them by deleting the project and re-provisioning.

Development

npm run build           # builds core/, sdk/, functions/, then the MCP server
npm test                # SKILL + sync + unit tests
npm run test:e2e        # 47 CLI end-to-end tests
npm run test:sync       # checks MCP/CLI/OpenClaw/SDK stay in sync
npm run test:skill      # validates SKILL.md frontmatter + body

Architecture: every tool / subcommand / skill script is a thin shim over an @run402/sdk call. core/ holds Node-only filesystem primitives (keystore, allowance, SIWE signing) wrapped by the SDK's Node provider. See CLAUDE.md for the full layout.

Links

简体中文

License

MIT