# Run402 CLI -- Agent Reference > CLI: `npm install -g run402@latest` > SDK: `npm install @run402/sdk` (typed TS client; same namespaces; Node/Deno/Bun/V8 isolates) > Docs URL: https://docs.run402.com/llms-cli.txt > API Docs: https://run402.com/llms.txt > Operator: Kychee, Inc. > Terms: https://run402.com/humans/terms.html > Contact: `run402 feedback send "your message"` (requires active tier) ## TL;DR Run402 = Postgres + REST + Auth + Storage + static & Astro-SSR site hosting + same-origin routes + Node 22 functions + email + image generation behind one CLI. Run402 is agent-first because agents are first-class participants, not because people disappear. Use your own principal and authenticator rather than a borrowed human account. Identity records who invoked the command; organization roles, grants, delegates, freshness, and spend policy determine what that principal may do. A founder agent may remain owner of its org-of-one. Agent-critical facts: - Atomic full-stack apply: `run402 deploy apply --manifest app.json` ships DB migrations, site files, function code, secrets, assets, subdomains, i18n, and routes as one transaction; partial failures roll back. - No platform token: local allowance (`~/.config/run402/allowance.json`) signs requests. Per-project `anon_key` / `service_key` are runtime data-plane keys (PostgREST/Storage/Functions), permanent, and embeddable/server-side respectively. - Agent-paid usage: x402 USDC on Base or MPP pathUSD on Tempo, signed by allowance. Humans may fund via Stripe credits; CLI behavior is unchanged. Install + deploy: ```bash npm install -g run402@latest run402 up --name "my-app" -y # validates manifest, bootstraps prerequisites, deploys run402 apply --manifest app.json --rehearse --json run402 redeem # only if you were given a promo code — see Promo codes below ``` `up` is the agent-first path when the repo has `run402.deploy.json` or `app.json`. It is a thin CLI shim over the SDK action runner; the SDK owns manifest validation, project resolution, recursive prerequisites, idempotency-key derivation, and deploy apply. Provision before writing frontend code when you need the real `anon_key` embedded. `prototype` is free with the testnet faucet; use `hobby` / `team` for mainnet. For database-bearing deploys, rehearse before commit. `run402 apply --manifest app.json --rehearse --json` plans, uploads missing CAS bytes, creates a contained branch, applies migrations and checks there, and prints a rehearsal report without touching the source project. If the report passes, re-run with `--commit` or commit the reviewed plan with `run402 deploy apply --require-plan `. Manual restore points live under `run402 snapshots`; contained, expiring data branches live under `run402 branches`. CLI update awareness is advisory and fail-open. Normal commands never wait for npm; they use cached update state and keep success stdout as the command payload. Stale notices appear as structured JSON on stderr, or as `{"type":"cli.update_available",...}` in `--json-stream`. `RUN402_NO_UPDATE_CHECK=1` suppresses notices/checks; CI skips live checks unless `RUN402_UPDATE_CHECK=1`; `run402 doctor --refresh` is the explicit bounded live check. Self-hosted Run402 Core target: ```bash npm install -g run402@latest run402 init --api-base=http://my-core:4020 run402 projects provision --name "my-app" # returns anon_key, service_key, project_id run402 deploy apply --manifest app.json # uses the active Core project ``` `init --api-base` stores the API base in the active profile (`target.json`) so the CLI, Node SDK, and MCP use the same target by default. Against Core, `projects provision` and `deploy apply` do not require Cloud tier, allowance, or x402 setup. Unsupported Cloud-only manifest slices fail as Core capability errors; they are not silently deployed to Run402 Cloud. App build scripts should read the same target/profile store through `resolveRun402TargetProfile()` from `@run402/sdk/node`, not by parsing `target.json` or local project-key cache files themselves. ## Core facts - Allowance: `~/.config/run402/allowance.json` (0600); active project state: profile `state.json`; local project-key cache: profile `credentials/project-keys.v1.json` (0600). Legacy `projects.json` is migration input only. - Project keys are cached automatically after provision or fork for operations that truly need anon/service keys. They are not project inventory. - `` in commands = `project_id` from `run402 projects list` - Output: JSON stdout on success; JSON stderr on failure; exit 0 success, non-zero error. See Output Contract. - CLI handles x402 signing; do not request private keys or payment libraries. - `run402 up` is the only compound CLI command. It emits natural JSON with `steps[]` (no top-level success `status`). Use `--check` for local-only validation, `--plan` for gateway-reviewed intent, and `--require-plan` for exact reviewed apply. - GitHub Actions deploys use OIDC: link once with `run402 ci link github`; generated workflow calls `run402 deploy apply` with `permissions: id-token: write`. - Projects, sites, subdomains, forks, functions, secrets, blob storage: free with active tier. Only image generation ($0.03) is per-call - Env overrides: `RUN402_API_BASE` (overrides stored target; default `https://api.run402.com`), `RUN402_CONFIG_DIR` (base credential dir, default `~/.config/run402`), `RUN402_WALLET` (active named wallet/profile, default `default`; alias `RUN402_PROFILE`), `RUN402_ALLOWANCE_PATH` (custom allowance file path, default `{config_dir}/allowance.json`), `RUN402_TRACE` (any non-empty value: one stderr trace line per SDK request — see Observability below). `run402 init --api-base=` persists the active target in `{config_dir}/target.json` or `{config_dir}/profiles//target.json`. - Wallets: `run402 wallets` manages named profiles. Select via `--wallet ` (`--profile`), `RUN402_WALLET`, or nearest `.run402.json` binding (commit-safe name only). Precedence: flag > env > `.run402.json`/`.run402.local.json` > `wallets use` default > `default`. Env/binding conflict hard-fails unless flag passed. `default` stays at config root; named wallets live under `{base}/profiles//`. Non-default active wallet is echoed on stderr and shown in `status` / `wallets current`. ## Public Buzz/Nostr identity links Human and agent principals use one common public identity-link resource with a discriminated proof protocol. A principal may have multiple active Nostr subjects; one active subject belongs to only one principal. Links are attribution only. They never change authentication, organization ownership, grants, delegates, spending, deployment authority, or transfer targeting. For a human account, open . That normal browser flow requires the direct Run402 session, fresh passkey, explicit public-correlation disclosure, and released Buzz approval UI. It never asks the human to paste a raw event, handle an `idlnk_…`, or provide a passkey/session/private key. Human link revocation is also browser-canonical. Revoking a public link never removes an organization membership, and removing membership never revokes the link. The CLI begin/complete ceremony below is for an agent's Run402 EOA: ```bash run402 identity link nostr begin \ --pubkey \ --visibility public > challenge.json # Publish challenge.json's proof_content as a standalone Buzz kind-1 message. # Fetch the raw event, preserving exactly id,pubkey,created_at,kind,tags,content,sig. run402 identity link nostr complete --event-file raw-event.json # Or: buzz social event --event | run402 identity link nostr complete --event-stdin run402 identity link list run402 identity link show idlnk_... run402 identity link revoke idlnk_... ``` `begin` uses the active Run402 wallet to EIP-191-sign the exact server payload and prints `proof_content`; it never handles a Nostr secret. Sign and publish that content through Buzz with `buzz social publish --content`, then recover the raw seven-field envelope with `buzz social event --event`. `identity link list` uses the active CLI identity (agent wallet when present, otherwise the signed-in human control-plane session) and preserves every active/revoked record plus `proof_protocol`. Do not use the desktop `buzz://nostr-bind` owner flow for an agent link: it signs as the human Buzz principal. ## Buzz community control plane ```sh run402 buzz status run402 buzz adopt offer --org --identity-link [--deployment-context-file ] run402 buzz adopt offer show run402 buzz adopt offer cancel run402 buzz install --org --community --authority run402 buzz enroll --installation --identity-link --grants-file --expires-at ``` The status response preserves independent inert skill installation, durable human-adoption offers, completed/attempted human adoption, Buzz-community ↔ Run402-organization installation, and this distinct agent's enrollment. `buzz adopt offer` capability-checks before mutation and creates no challenge or authority; its `handoff_url` is the normal browser/passkey path. `--org` takes the Run402 organization id exactly as `run402 org whoami` and `run402 projects get` return it — a UUID, never transformed. `--deployment-context-file` takes a JSON object of exactly these five non-empty strings and no others: `project_id`, `release_id`, `live_url` (public HTTPS origin, no credentials or fragment), `source_revision`, `verified_at` (ISO-8601, not in the future); the gateway checks them against the org's active release and its claimed subdomain, custom domain, or deployment host, and a rejection names the offending fields. Poll authoritative state with `offer show`; a click is not completion. A completed poll reports a terminal consent receipt, public human `idlnk_…`, and ordinary owner membership separately. The membership alone grants organization authority; link and membership revocation are independent and the receipt remains completed. `run402 buzz adopt direct --org … --identity-link …`, raw `complete`, and clipboard/event handling are advanced compatibility paths. Other consent/decision commands are `buzz install activate|update|revoke` and `buzz approve|deny|revoke`; `buzz install discover --community ` is the unauthenticated descriptor index. MCP intentionally omits Buzz signing/passkey mutations and renders exact HTTPS/CLI handoffs. JSON is stdout, advice is stderr, every action has zero spend impact, and secret-shaped request fields fail locally. Older gateways fail the offer capability check without mutation and name the advanced direct fallback. Enrollment grants only finite named existing-project scopes and never agent org membership, future-project creation, owner role, delegates, or payment authority. On failure, branch on the stable code and preserve the exact repair `field` and complete `next_actions`; retry an unchanged command only when `safe_to_retry: true`, never through a generic edit fallback. The CLI rejects `--nostr-key`, `--nsec`, private-key, mnemonic, seed, derivation, display-name, label, and signed-label inputs locally before network access. It accepts raw events only through `--event-file` or `--event-stdin`, verifies the event id and BIP-340 signature locally, and sends no workspace/channel context. The event must be standalone kind 1 with either no tags or exactly one valid NIP-OA `auth` tag. Public proof bytes remain available after revocation. ## Output Contract Uniform contract: - Success: stdout emits the natural payload, never wrapped; no top-level `status`. - Reads/lists: resource directly, e.g. `projects get` -> `{ project_id, public_id, name, ... }`, `projects list` -> `{ projects: [...], scope?, has_more?, next_cursor? }`, `credentials project-keys status` -> local-cache provenance. - Mutations without natural payload: affected ids + boolean action field, e.g. `{ key, project_id, set: true }`, `{ name, project_id, deleted: true }`, `{ domain, project_id, released: true }`; never `{}`. - Local-state reads (`status`, `allowance status`): nullable typed fields, e.g. `{ wallet: null, hint: "Run: run402 init" }`; absence exits 0. - Raw/text stdout is opt-in only (`functions invoke --raw`, file-output commands, help/version/dev human surfaces). Machine-readable command defaults emit parseable JSON; for example, `allowance export` emits `{ "address": "0x..." }`. - Failure: stderr JSON envelope with top-level `status: "error"` + non-zero exit. That sentinel appears on stderr only. - Validation commands may exit 0 with payload issues, e.g. `validate-expose` prints `has_errors: true`; branch on payload fields. - Payload-internal `status` fields are not envelopes, e.g. `doctor.checks[].status`. - JSON is ALWAYS the default on stdout. `--json` is a universally-accepted NO-OP: every command takes it, and passing it never changes stdout. Never pass it to "get JSON" — you already have JSON. The one exception is `assets put --json`, a deprecated alias for `--stream` (NDJSON progress). - Human-readable rendering is an explicit opt-out, spelled `--human` (`run402 up`, `run402 errors`). `--human` combined with `--json` is a `BAD_USAGE` error. - `cli-output-contract.test.mjs` and `cli-json-noop-contract.test.mjs` guard this; violations are regressions. - v3.0 breaking change: success wrapper `{ status: "ok", ...payload }` removed; gate on exit code. Stderr error envelope unchanged. ## Observability The SDK's request kernel (`sdk/src/kernel.ts` — the one place that touches `fetch`) carries two always-available diagnostics, for every SDK caller (CLI, MCP, `git-remote-run402`): - **`RUN402_TRACE`** — set it to any non-empty value and every request writes one line to stderr: `r402 -> ms attempt=`. `` never carries its query string, and the line never carries headers, bodies, or tokens (the same redaction posture as the payment-attempt journal). `` is `ERR` for a request that never got a response (network failure). - **Per-instance stats** — every SDK instance accumulates `round_trips`, `wire_ms` (summed), `bytes_up`, and `bytes_down` (`Content-Length` when present, measured otherwise), monotonic for the instance's lifetime, read via `sdk.stats()`. Every `run402 repos ` result and `run402 deploy apply`'s final result carry this as a `stats: { round_trips, wire_ms, bytes_up, bytes_down }` block, always — no flag needed. It reflects only the calls that one command's own SDK instance made; a helper that resolves its own SDK internally (e.g. org/wallet context resolution shared across command families) is not reflected in that command's `stats`. - **`-v` / `--verbose`** — on `repos ` and `deploy apply`, prints one extra stderr summary line with the same numbers (`stats: round_trips=… wire_ms=… bytes_up=… bytes_down=…`). Coexists with `--human`. MCP tool output is markdown, not the `stats` envelope field — this is a CLI/SDK-edge feature by design. ## `run402 up` (SDK action runner) `run402 up [repo-or-path] [--name ] [--project ] [--manifest ] [--dir ] [--tier ] [-y|--yes] [--check|--print-spec|--plan|--require-plan ] [--verify] [--propagation-budget-s ] [--no-propagation-wait] [--json|--json-stream|--human] [--quiet] [--allow-warning ...] [--allow-warnings]` `run402 up verify [repo-or-path] [--project ] [--manifest ] [--dir ] [--propagation-budget-s ] [--no-propagation-wait] [--json|--json-stream|--human] [--quiet]` Use `up` for a repo-level app deploy when the workspace has a deploy manifest. It discovers `run402.json`, `run402.deploy.json`, then `app.json` under `--dir` / cwd. A `run402.json` with the app schema or app-specific markers uses the app-install graph; a release-shaped `run402.json` uses the same ReleaseSpec normalization accepted by `deploy apply`. Malformed app manifests return `APP_SPEC_INVALID` with the failing field instead of an internal JavaScript exception. Validation and filesystem-reference checks finish before any mutation, then the SDK action plan executes. Project resolution order: - explicit `--project` - workspace link `.run402/project.json` (`schema_version: "run402.workspace-project.v1"`, `project_id`, optional `name`, `target`) - manifest `project_id` - approved project creation from `--name` - approved active-project fallback `--name` is only project creation/link metadata. It is not part of the deploy manifest, does not select a project when another selector already resolved one, and never renames an existing project. The workspace link is a local convenience file; it is written atomically and skipped in local check / reviewed-plan modes. Approval and recursion: - Non-interactive recursive mutations require `-y/--yes`; without it the command fails before mutating and returns a structured approval-required error. - If allowance/tier/project/workspace link are already configured, plain `run402 up` runs the requested deploy without `-y`. - In a TTY, the CLI prompts for SDK-planned mutations. In SDK code, pass `{ approval: "yes" }`, `{ approval: "never" }`, or an interactive approval callback. - `--check` returns local validation `steps[]` without allowance creation, faucet request, tier payment, project creation, workspace-link write, upload, gateway plan, or deploy commit. - `--print-spec` performs the same local validation and prints normalized `ReleaseSpec` JSON. - `--plan` calls the gateway reviewed-plan mode without upload or commit; it does not provision projects or write workspace links. The response includes a require-able `plan_id`, `plan_fingerprint`, expiration, warnings, diff, and `next_actions[]`. - `--require-plan ` applies only if the reviewed plan still matches; optional `--plan-fingerprint ` tightens the check. - Run402 Cloud `up` can create/fund an allowance, ensure a prototype tier by default, create a project from `--name`, write the workspace link, then apply the manifest. - Run402 Core `up` skips Cloud allowance/tier prerequisites and fails closed if no Core project is selected by `--project`, workspace link, or manifest. - The SDK derives child idempotency keys for recursive gateway mutations from the root action key; pass `--idempotency-key` when you need a stable external key. - Deploy warnings use the same review surface as `deploy apply`: prefer repeatable `--allow-warning ` and reserve broad `--allow-warnings` for reviewed exceptional cases. - App manifests AND deploy manifests can define `verify.http[]`. After apply, `up` fetches those URLs and records per-check status in `result.app_result.verification.http[]` (app manifests) or `result.verification.http[]` + a `result.verify` rollup (deploy manifests; a hard verify failure exits 1). Fresh managed-subdomain or custom-domain misses that carry Run402 edge sentinels (`x-run402-edge` or JSON codes such as `SUBDOMAIN_NOT_CONFIGURED`) are treated as propagation, not as permanent verify failure, while the deploy binding is fresh or `deploy resolve` reports `edge_propagation.status !== "settled"`. - `--propagation-budget-s` controls the wall-clock wait for those fresh edge misses (default 120 seconds). `--no-propagation-wait` returns immediately with app `status: "propagation_pending"` and `verify.status: "propagation_pending"`; the result includes `propagation_wait_ms`, warnings, `next_action`, and diagnostic `edge_propagation` / `resolve` payloads when available. - `--verify` waits after a successful deploy apply for gateway/edge release coherence and attaches `result.edge_coherence` plus `result.deploy.edge_coherence`. It uses the same `--propagation-budget-s` budget (default 120 seconds), emits `deploy.verify.poll` progress events, and exits 2 if the report is valid but still not coherent. - `run402 up verify` reruns the manifest HTTP verification (app or deploy manifest) without resource mutation, upload, deploy, or project creation. It resolves the project from `--project`, `.run402/project.json`, the manifest project id, then active project, and is the recovery command to run after propagation settles. A manifest without `verify.http[]` fails `VERIFY_CHECKS_REQUIRED`. Output: stdout is the action result, e.g. `{ "action": "up", "dry_run": false, "target": "cloud", "steps": [...], "result": { "project_id": "prj_...", "manifest_path": "...", "deploy": {...} } }`. Stderr carries JSON action-step events unless `--quiet`. ## Error JSON and Safe Retry CLI errors: JSON stderr with outer `"status": "error"`. Run402 JSON bodies may merge into the envelope. Branch on `code`, not `message`/legacy `error`. Canonical fields: - `code`: stable machine-readable reason, e.g. `PROJECT_FROZEN`, `PAYMENT_REQUIRED`, `MIGRATION_FAILED`, `MIGRATE_GATE_ACTIVE`. Client-side validation failures (missing flag, malformed JSON) default to `BAD_USAGE`; specific client-side cases use richer codes e.g. `UNKNOWN_FLAG`, `BAD_FLAG`, `PROJECT_CREDENTIAL_NOT_FOUND` (with `details.source: "local_cache"`), `NO_DEPLOYMENT`, `NO_ALLOWANCE`, `BAD_JSON_FLAG`, `CONFIRMATION_REQUIRED`. - `retryable`: the same request may succeed later - `safe_to_retry`: repeating the same request should not duplicate or corrupt a mutation - `mutation_state`: one of `none`, `not_started`, `committed`, `rolled_back`, `partial`, `unknown` - `trace_id`: include this when reporting the issue - `request_id`: routed/function handle; diagnose with `run402 functions logs --request-id `. Distinct from gateway `trace_id`. - `details`: structured route-specific context - `next_actions`: advisory typed suggestions e.g. `authenticate`, `submit_payment`, `renew_tier`, `check_usage`, `retry`, `resume_deploy`, `edit_request`, `edit_migration`, `create_project`, `initialize_wallet`, `deploy`, `deploy_site_first`, or `poll`. CLI-resolvable entries carry a literal `command`, e.g. `{ "type": "create_project", "command": "run402 projects provision" }`. Do not execute route-like suggestions without validating method/path/auth/safety. - `correlated_platform_incident`: present ONLY while an OPEN platform incident correlates with this error's `code` — `{ id: "inc_…", subsystem, status: "ongoing" | "resolved" }`, with a `poll` entry appended to `next_actions`. It is a CORRELATION, not an exoneration: the platform states it was degraded when your call failed and lets you judge (an app can still cause its own throttling). Poll the events feed (`run402 events`) and check `platform_status` before debugging your own code; when the incident resolves, the matching `platform_incident` feed event carries your project's real failed-invocation count. Absent when no open incident correlates — never a false confession. - Cold-start chain: a fresh agent that knows only `run402 deploy apply` is walked to a deployed result by following `next_actions` — no allowance -> `run402 init`, no tier -> `run402 tier set prototype`, no project -> `run402 projects provision` — each step idempotent, then retry the deploy. You do not need to memorize the sequence; follow what each failure hands back. - Prefer `run402 up` when starting from a local repo: it plans and runs that same cold-start chain through the SDK instead of executing advisory `next_actions[].command` strings. Retry policy: - Retry same request only when `retryable: true` and `safe_to_retry: true`; reuse idempotency key for mutations when available. - `safe_to_retry: true` alone means duplicate-safe, not likely-to-succeed. Lifecycle-gated writes, auth token exchanges, and passkey verifies need the indicated action first. - `run402 deploy apply` already handles safe `BASE_RELEASE_CONFLICT` release races for omitted/current-base deploy specs: it re-plans, emits `deploy.retry` events on stderr, and stops after its bounded SDK retry budget. Exhausted deploy retries include `attempts`, `max_retries`, and `last_retry_code` in the error envelope. Do not hand-roll this specific retry loop around the CLI unless you intentionally disabled SDK retries upstream. - For mutating 5xx with `safe_to_retry: false`, or `mutation_state` in `committed|partial|unknown`, inspect/poll/reconcile before retry. For deploys prefer `deploy events`/`deploy resume` over duplicate apply. - Lifecycle/payment: `PROJECT_FROZEN`/`PROJECT_DORMANT`/`PROJECT_PAST_DUE` -> `projects usage ` or `tier set `; `PAYMENT_REQUIRED`/`INSUFFICIENT_FUNDS` -> submit payment/fund allowance. - `NOT_AUTHORIZED` (HTTP 403) is an org-owned-control-plane authorization denial, distinct from auth or payment: the wallet *authenticated*, but its resolved principal lacks the org role or per-project grant the action needs. `details` carries `required_role` / `required_capability` / `reason`. Not retryable without obtaining a covering org membership/role or grant; high-stakes ops (delete, transfer-of-ownership, membership change) require an active `owner` membership. The gateway returns 403 even when the project does not exist (so existence isn't leaked) — re-check the `` too. The CLI envelope adds an actionable `hint`. - `STEP_UP_REQUIRED` (HTTP 403) is a freshness/provenance demand for a high-stakes control-plane op: the session is valid but not fresh enough, or was minted by a read/device-flow path that can't satisfy a passkey step-up. `details` carries `required_amr` / `max_age_seconds` / `challenge_url` / `reason`, plus `next_actions[]`. The SDK raises a typed `StepUpRequiredError` (`isStepUpRequired()` guard). Resolve with `run402 operator login --step-up` on the same client, then retry. Distinct from `NOT_AUTHORIZED` (a role/grant gap, not a freshness gap). - `WRITE_AUTH_REQUIRED` / `WRITE_AUTH_BINDING_MISMATCH` / `WRITE_AUTH_SESSION_INVALID` (HTTP 403) — a wallet-less human's control-plane session needs a passkey **operator approval** scoped to this `(action, target)` (the SIWX wallet path never hits this). The SDK raises a typed `OperatorApprovalRequiredError` (`isOperatorApprovalRequired()` guard) carrying `capability`, `target`, and a fully-resolved `approveCommand` / `nextActions[]` (e.g. `run402 operator approve --action project.deploy --project prj_x`). `BINDING_MISMATCH` = a cached approval targeted the wrong org/project; `SESSION_INVALID` = it's stale. Resolve by running the surfaced `operator approve` command (or let an interactive `provision`/`deploy` auto-approve). - Client-side `BAD_JSON_FLAG` errors include `details.flag` (the offending flag, e.g. `--abi`) and `details.value_preview` (truncated value) so callers know which flag value to fix. - CLI commands reject unknown flags and missing flag values locally with `UNKNOWN_FLAG` or `BAD_FLAG` before network work. Numeric and wei-like flags are strict decimal integers: malformed, fractional, negative, and scientific-notation values fail locally instead of being forwarded to the API. - Commands with fixed positional shapes also reject extra positional arguments locally. This includes deploy resume/list/events/release subcommands, functions list/delete, and blob get/rm/sign/diagnose. Examples: ```json { "status": "error", "http": 403, "message": "Project is frozen.", "code": "PROJECT_FROZEN", "category": "lifecycle", "retryable": false, "safe_to_retry": true, "mutation_state": "none", "next_actions": [{ "type": "renew_tier" }, { "type": "check_usage" }] } ``` ```json { "status": "error", "http": 402, "message": "Payment required.", "code": "PAYMENT_REQUIRED", "category": "payment", "retryable": true, "safe_to_retry": true, "next_actions": [{ "type": "submit_payment" }] } ``` ```json { "status": "error", "message": "Migration failed.", "code": "MIGRATION_FAILED", "category": "deploy", "retryable": false, "safe_to_retry": true, "mutation_state": "rolled_back", "trace_id": "trc_...", "details": { "operation_id": "op_...", "phase": "migrate" }, "next_actions": [{ "type": "edit_migration" }] } ``` --- ## Step 1: Install ```bash npm install -g run402 ``` ## Step 2: Set Up Allowance and Funding ```bash run402 init # creates allowance if absent; checks balance; faucets testnet USDC if zero; shows tier/projects run402 allowance create # Generate a new allowance run402 allowance fund # Get free testnet USDC (Base Sepolia) run402 allowance balance # Check USDC balance (mainnet + testnet + billing) ``` Allowance lives at `~/.config/run402/allowance.json` (0600). CLI signs x402 automatically; never handle private keys/payment libs manually. For a self-hosted Run402 Core Gateway, skip Cloud allowance setup and configure the target instead: ```bash run402 init --api-base=http://my-core:4020 ``` After that, the same `run402 projects provision` and `run402 deploy apply` commands target Core. ### Promo codes (vouchers) If you were given a **promo code** — a string like `R402-K8F3-Q2W9`, sometimes called a voucher or a gift/starter code — redeem it. It credits your organization with run402 prepaid credit, and that credit pays for the next step (a tier) with no on-chain payment. ```bash run402 redeem R402-K8F3-Q2W9 # any time: before or after `run402 init` run402 init --voucher R402-K8F3-Q2W9 # same redemption, folded into setup ``` - **Order does not matter.** Redeeming works as your very first authenticated call (the organization is created on demand) or long after `run402 init`. - **Codes are forgiving.** Case-insensitive, hyphens optional — `R402-K8F3-Q2W9` and `r402k8f3q2w9` are the same code. Send it verbatim; the server normalizes. - **Retrying is safe.** A repeat by the same organization returns the original result with `already_redeemed: true` and never credits twice. - **A bad code never blocks setup.** `init --voucher` warns, records `voucher_error` in its JSON summary, and finishes normally. - Failures: `404 VOUCHER_NOT_FOUND` (unknown or malformed), `410 VOUCHER_EXPIRED`, `409 VOUCHER_ALREADY_REDEEMED` (a different organization used it), `403 PROMO_LIMIT_REACHED` (this org is at its lifetime ceiling). Minting codes is not an agent operation — it needs an issuer key no tenant holds. ## Step 3: Subscribe to a Tier ```bash run402 tier set prototype # FREE on testnet — faucet USDC verifies your x402 setup ($0 real money); 7-day lease run402 tier set hobby # $5 for 30 days (real money) run402 tier set team # $20 for 30 days (real money) ``` Tier is organization-scoped. Subscribe/renew/upgrade applies to every project in the org; `api_calls` / `storage_bytes` quota is org-pooled across linked wallets (`billing link-wallet`). Quota errors include `details.scope: "organization" | "project"` (`project` = orphan fallback after org purge before cascade). `tier set` refetches status and returns `status_after` with refreshed pool usage. Retry-safety: `tier set` and `projects provision` accept `--idempotency-key ` so a retried subscribe/renew/create collapses onto one charge instead of double-billing. `provision` auto-derives the key from `--name` when omitted (re-running `provision --name X` returns the same project); `tier set` is caller-supplied only — use a fresh key for a deliberate second renewal. Server action detection: - No tier or expired -> subscribe - Same tier, active -> renew (extends from current expiry) - Higher tier -> upgrade (prorated refund to billing allowance) - Lower tier, active -> downgrade (prorated refund if usage fits) ```bash run402 tier status ``` `tier status` `pool_usage` sums `api_calls` and `storage_bytes` across every project on the organization (across every linked wallet), not the requesting wallet's projects. With active tier: unlimited projects/sites/forks/functions/secrets/storage subject to org-pooled `api_calls`/`storage_bytes`; only image generation is per-call ($0.03/image). --- ## Portable Project Archives (Cloud -> Core) Portable archives are the vendor-lock-in escape hatch: Cloud is the easiest place to start, not the only place the supported application can run. This is separate from allowance/spend-cap financial-risk controls. Archive v1 exports the supported Run402 Core runtime slice of a Cloud project, not an entire Cloud project. Canonical agent path: ```bash run402 cloud archives create prj_... \ --scope portable-runtime-v1 \ --auth stubs \ --consistency pause-writes \ --wait \ --output ./project.r402ar \ --json run402 archives inspect ./project.r402ar --json run402 archives verify ./project.r402ar --json # Create ./required.env from required_secrets or secrets/required.env.template. run402 core projects import ./project.r402ar \ --name imported-project \ --env-file ./required.env \ --json ``` `cloud archives create` creates an operation-backed Cloud export, waits when `--wait` or `--output` is present, downloads bytes when `--output` is set, and returns `archive_id`, `operation_id`, `archive_status`, `sha256`, `expires_at`, `portability_report`, `export_report`, `verify_command`, and `import_command`. Use `--idempotency-key ` for safe retries, `--poll-interval ` and `--timeout ` for waits, and `--json-stream` for NDJSON progress. Progress events are one JSON object per line: ```json {"event":"archive_export_created","stage":"create","resource_type":"project_archive","resource_id":"arc_...","project_id":"prj_...","status":"running","completed_units":0,"total_units":1,"code":null,"message":"Archive export status: running","next_action":{"type":"none"},"retryable":true} ``` Every event and diagnostic uses stable agent fields: `code`, `severity`, `resource_type`, `resource_id`, `message`, `next_action`, `retryable`, and safe `context`. `archives inspect` and `archives verify` are local and offline. They do not require Cloud credentials. `verify` checks descriptor/blob integrity, format compatibility, required capabilities, size/path safety, required secrets, auth stub counts, and portability diagnostics. Verification means integrity and compatibility, not trust; archives remain untrusted input. `core projects import` verifies before import, targets a new Core project only, and calls a local Core gateway (`RUN402_CORE_URL` or `--core-url`, default `http://127.0.0.1:4020`). It supports `--dry-run`, `--require-runnable`, `--env-file`, and repeated `--secret KEY=VALUE` overrides. Required secret names are reported by inspect/verify and in the archive's `secrets/required.env.template`; secret values are never exported. Expected v1 exclusions: secret values, password hashes, sessions, refresh/access/OAuth tokens, MFA secrets, signed URLs, logs, billing/allowance/spend state, fleet/Aurora/global-routing/provider operations, managed backups, monitoring, abuse/compliance/support metadata, Cloud import, and existing-project merge import. Stable archive codes include `EXPORT_CONSISTENCY_UNAVAILABLE`, `EXPORT_SCOPE_UNSUPPORTED`, `ARCHIVE_EXPIRED`, `ARCHIVE_DIGEST_MISMATCH`, `ARCHIVE_UNSUPPORTED_VERSION`, `ARCHIVE_UNSUPPORTED_REQUIRED_CAPABILITY`, `ARCHIVE_PATH_UNSAFE`, `ARCHIVE_BLOB_MISSING`, `SECRET_VALUES_REQUIRED`, `AUTH_CREDENTIALS_NOT_EXPORTED`, `AUTH_SUBJECT_STUBS_IMPORTED`, `CLOUD_ONLY_FEATURE_EXCLUDED`, `PROJECT_ALREADY_EXISTS`, `IMPORT_VERIFY_FAILED`, and `IMPORT_CONFORMANCE_FAILED`. --- ## Deploying Apps ### Unified Apply Canonical deploy primitive: CAS bytes (no inline-body cap), per-resource `replace`/`patch`, atomic multi-resource activation, resumable failures. SDK: `r.project(id).apply(...)`. ⚠️ You still need the `anon_key` BEFORE writing your manifest -- provision first, then embed the real key in your HTML. ```bash run402 projects provision --name "my-app" # → copy anon_key from output into your HTML ``` Manifest format mirrors a v2 `ReleaseSpec`. For editor autocomplete, use top-level `"$schema": "https://run402.com/schemas/release-spec.v1.json"`; the CLI accepts that metadata and strips it before planning. ```json { "$schema": "https://run402.com/schemas/release-spec.v1.json", "project_id": "prj_1741340000_42", "database": { "migrations": [ { "id": "001_init", "sql": "CREATE TABLE IF NOT EXISTS items (id serial PRIMARY KEY, title text NOT NULL); INSERT INTO items (title) VALUES ('Buy groceries');" }, { "name": "seed_items", "sql": "INSERT INTO items (title) SELECT 'Welcome' WHERE NOT EXISTS (SELECT 1 FROM items WHERE title = 'Welcome');" } ], "expose": { "version": "1", "tables": [ { "name": "items", "expose": true, "policy": "public_read_authenticated_write" } ] } }, "secrets": { "require": ["OPENAI_API_KEY"], "delete": ["OLD_KEY"] }, "functions": { "replace": { "api": { "runtime": "node22", "source": { "data": "export default async (req) => new Response('ok')" }, "config": { "timeout_seconds": 30, "memory_mb": 256 }, "triggers": [{ "id": "api_every_15m", "type": "schedule", "cron": "*/15 * * * *", "run": { "event_type": "api.tick", "payload": {} } }] } } }, "site": { "replace": { "index.html": { "data": "..." }, "assets/logo.png": { "data": "iVBORw0KGgo...", "encoding": "base64" } } }, "subdomains": { "set": ["my-app"] }, "routes": { "replace": [ { "pattern": "/api/*", "methods": ["GET", "POST"], "target": { "type": "function", "name": "api" } } ] }, "i18n": { "default_locale": "en", "locales": ["en", "es", "fr"], "detect": ["cookie:wl_locale", "accept-language"] } } ``` File entries: bare UTF-8 string; `{ "data": "...", "encoding": "utf-8" | "base64", "content_type": "..." }`; or `{ "path": "dist/index.html", "content_type": "text/html" }`. `site.replace` / `site.patch.put` may also be `{ "__source": "local-dir", "path": "dist/client" }` for a static-site directory. Function `source` may be `{ "path": "dist/run402/functions/api.js" }`. `--manifest` relative paths resolve from manifest dir; `--spec`/stdin paths resolve from cwd. Authoring-only local paths and `__source` markers are stripped/staged before the apply request. Migrations may use `"sql_path"` / `"sql_file"` instead of `"sql"`. Each migration declares exactly one of `"id"` or `"name"`: use `id` for immutable versioned migrations, and `name` for generated/idempotent SQL whose compiled id should track content changes. CLI/MCP share SDK `normalizeDeployManifest`; JSON can become SDK-native `ReleaseSpec`. Strict adapter: only top-level `$schema` and app-kit evidence `x-run402-omitted_features` are ignored before planning; unknown fields/no-op specs fail (`"subdomain"`, `"site.replcae"`, `"functions.replace.api.deps"`, `"functions.replace.api.config.schedule"`). Function specs: `runtime: "node22"`, exactly one code source (`source` or `files`+`entrypoint`), `config.timeout_seconds`, `config.memory_mb`, and optional `triggers[]`. Schedule triggers require a stable `id`, `type: "schedule"`, 5-field `cron`, and nested `run: { event_type, payload?, retry?, expires_after_seconds? }`; each tick creates a durable function run. Email triggers use `{ id, type: "email", mailbox, events, run }`, where `mailbox` is a mailbox slug/id and `events` is any of `reply_received`, `delivery`, `bounced`, `complained`, `mailbox_suspended`; each matching email event creates a durable function run with the canonical event payload under `payload.event`. A `mailbox_suspended` trigger fires when the mailbox is abuse-suspended (payload event carries `suspended_reason`, `suspended_at`, `evidence`, `recovery_actions`) — the run executes independently of the suspended mailbox's send capability, so an app can observe its own outage without polling or a public webhook URL. `deps: string[]` works under `apply-v1-function-deps`; gateway installs/bundles. `run402 functions deploy --deps` builds one `functions.patch.set` and uses unified apply; legacy standalone deploy route removed. Deploy preflights literal function caps after normalization before CAS upload/plan: timeout, memory, schedule-trigger cron interval, scheduled-trigger count. Local failures: `code: "BAD_FIELD"` with `details.field/value/tier`, limit (`tier_max` or `min_interval_minutes`), `details.limit_source` (`tier_status` or `local_static_fallback`). Current caps: prototype 10s/128 MB/1 scheduled trigger/15 min; hobby 30s/256 MB/3/5 min; team 60s/512 MB/10/1 min. `tier status` shows live caps/usage when returned. Subdomains: one mode per deploy. `"set"` replaces release managed subdomains, `"add"` appends, `"remove"` deletes. Current gateway supports at most one `subdomains.set`; multi-set fails locally with `SUBDOMAIN_MULTI_NOT_SUPPORTED`. Complete static site + function + route manifest: ```json { "project_id": "prj_...", "site": { "replace": { "index.html": { "data": "
" }, "events.html": { "data": "

Events

" } }, "public_paths": { "mode": "explicit", "replace": { "/events": { "asset": "events.html", "cache_class": "html" } } } }, "functions": { "replace": { "api": { "runtime": "node22", "source": { "data": "export default async function handler(req) { const url = new URL(req.url); return Response.json({ ok: true, path: url.pathname }); }" } }, "login": { "runtime": "node22", "source": { "data": "export default async function handler(req) { return Response.json({ ok: true }); }" } } } }, "routes": { "replace": [ { "pattern": "/api/*", "methods": ["GET", "POST", "OPTIONS"], "target": { "type": "function", "name": "api" } }, { "pattern": "/login", "methods": ["POST"], "target": { "type": "function", "name": "login" } } ] } } ``` Static public paths: release asset paths and browser paths differ (`events.html` asset -> `/events` public URL). `mode: "explicit"` exposes only `public_paths.replace`; `/events.html` is not public unless declared. `mode: "implicit"` restores filename-derived reachability and can widen access; review warnings. Known `cache_class`: `"html"`, `"immutable_versioned"`, `"revalidating_asset"`; preserve unknown future strings. Public-path-only specs are deploy content: `{ "site": { "public_paths": { "mode": "explicit", "replace": {} } } }` removes direct public static URLs without changing assets. Route semantics: - Omit `routes` or pass `null` to carry forward; `{ "replace": [] }` clears; `{ "replace": [...] }` atomically replaces. - Entries: `pattern`, optional non-empty `methods` (`GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS`), `target`. Function target: `{ "type": "function", "name": "" }`. Prefer `site.public_paths` for ordinary clean static URLs. Static route target = exact, method-aware alias, e.g. `{ "pattern": "/events", "methods": ["GET", "HEAD"], "target": { "type": "static", "file": "events.html" } }`; `file` is release asset path, not public path/URL/CAS/rewrite/redirect. Static targets require exact patterns only, methods `["GET"]` or `["GET","HEAD"]`, no leading slash/wildcard/dir shorthand/query/fragment. Path-keyed maps like `"routes": { "/api/*": { "function": "api" } }` invalid. - Exact patterns look like `/admin`; prefix wildcard patterns use final `/*`, like `/admin/*`. `/admin/*` does not match `/admin`, `/admin/`, `/admin.css`, or `/administrator`, so deploy both `/admin` and `/admin/*` for a dynamic section root. - Query ignored for matching but preserved in full public `req.url`. Exact beats prefix; longest prefix wins; method-compatible dynamic routes beat static assets. - `POST /login` can coexist with static `GET /login`; unsafe method mismatch returns 405, not SPA HTML. Matched dynamic failures fail closed; no static fallback. - Routed ingress uses the Node 22 Fetch Request -> Response contract; `req.url` is full public URL across managed subdomains/deployment hosts/custom domains. Derive OAuth origins from `new URL(req.url).origin`. `run402.routed_http.v1` envelope is internal. Direct `/functions/v1/:name` remains API-key protected. Function owns app auth, CSRF, CORS/`OPTIONS`, cookies, redirects, and forwarding-header hygiene. - Anti-patterns: routing every static file, broad method lists by default, wildcard static route targets, leading-slash static files, directory shorthand, one-static-route-target-per-page route-table exhaustion, wildcard function routes shadowing direct public static paths, and confusing omitted/null `routes` with `routes: { "replace": [] }`. Apply it: ```bash run402 deploy apply --manifest app.json ``` Stdout final result includes `release_id`, `operation_id`, `urls`, etc. Stderr streams JSON-line progress events. `--quiet` / `--final-only` silence stderr while preserving stdout. Recipe — static home page + SPA shell: a SPA site ships `index.html` as the shell serving every unmatched route (match `spa_fallback`), so by default `GET /` serves the shell too. To serve a real static home page at `/` — real bytes under curl and without JavaScript — while keeping the shell for app routes, ship `home.html` at the site root alongside `index.html`, add an exact root static route alias, and `run402 deploy apply --manifest app.json`: ```json { "project_id": "prj_...", "site": { "replace": { "index.html": { "data": "
" }, "home.html": { "data": "

Welcome

Open the app" }, "app.js": { "data": "/* SPA bootstrap */" } } }, "routes": { "replace": [ { "pattern": "/", "target": { "type": "static", "file": "home.html" } } ] } } ``` Route matching runs before all static resolution — including the implicit `/` -> `index.html` root mapping — and SPA-fallback derivation is independent of the route table. So `GET /` serves `home.html` (match `route_static_alias`), unmatched app routes like `/dashboard` still serve the `index.html` shell (match `spa_fallback`), and named static pages keep serving unchanged (match `static_exact`). Root placement of `home.html` keeps its relative asset URLs resolving identically to the direct file and avoids the `STATIC_ALIAS_RELATIVE_ASSET_RISK` warning. Expect two non-blocking plan lints: `STATIC_ALIAS_SHADOWS_STATIC_PATH` (warn — the alias overrides what `/` would otherwise serve; for this recipe that is accurate and expected, and the commit proceeds) and `STATIC_ALIAS_DUPLICATE_CANONICAL_URL` (info — `/home.html` stays directly reachable in implicit public-path mode; add `` to `home.html` if duplicate-content SEO matters). Omitting `routes` on later deploys carries the alias forward (informational `ROUTE_TARGET_CARRIED_FORWARD`); a pipeline that sends `routes.replace` must include the alias every time because replace is total. Verify with `run402 deploy resolve --project prj_123 --url https:/// --method GET` and confirm `match: "route_static_alias"` with `target_file: "home.html"`. Typed deploy configs are an authoring format for the same `deploy apply` and `up` verbs, not a separate command family. JSON data manifests (`run402.deploy.json`, `app.json`) may be auto-discovered. TypeScript/JavaScript configs are executable local code, so v1 requires explicit trust with `--manifest`: ```bash run402 up --manifest run402.deploy.ts --check run402 up --manifest run402.deploy.ts --print-spec run402 up --manifest run402.deploy.ts --plan run402 up --manifest run402.deploy.ts --require-plan plan_... ``` Mode contract: - `--check`: local-only import/normalize/strict field validation plus local file checks. No gateway calls, uploads, tier/project creation, or `.run402/project.json` writes. Success is raw JSON with `mode: "check"` / `dry_run: true` on `up`, or `{ ok: true, mode: "check", project_id, manifest_path }` on `deploy apply`. - `--print-spec`: local-only normalized `ReleaseSpec` JSON to stdout. - `--plan`: gateway-reviewed plan, no upload or commit. Response includes `plan_id`, `plan_fingerprint`, `plan_expires_at`, `manifest_digest`, diff, warnings, and `next_actions[]`. - `--require-plan `: exact reviewed apply. The SDK recompiles locally, verifies the reviewed plan before upload, then commit verifies again before release mutation. Add `--plan-fingerprint ` when it was returned by `--plan`. `run402 up --plan` preserves the `up` surface in `next_actions[0].argv`, e.g. `["run402","up","--manifest","run402.deploy.ts","--require-plan","plan_..."]`. `run402 deploy apply --plan` returns a `deploy apply --require-plan` action. `--allow-warning` / `--allow-warnings` conflict with `--require-plan` because reviewed-plan approval already binds the exact warning/destructive sets. If `run402 up --check` sees only `run402.deploy.ts` and no JSON manifest, it fails with `EXECUTABLE_CONFIG_REQUIRES_EXPLICIT_MANIFEST` and a recovery action to rerun with `--manifest run402.deploy.ts --check`. Minimal typed config: ```ts import { defineConfig, dir, nodeFunction, sqlFile } from "@run402/sdk/config"; export default defineConfig(({ env }) => ({ project: env.required("RUN402_PROJECT_ID"), database: { migrations: [sqlFile("db/001_init.sql")] }, site: { replace: dir("dist"), public_paths: { mode: "implicit" } }, functions: { replace: { api: nodeFunction("dist/functions/api.js") } }, secrets: { require: ["OPENAI_API_KEY"] } })); ``` Helper semantics: `dir()` walks files in stable path order, skips private/dev patterns by default like the existing directory deploy helpers, normalizes `/` separators, rejects symlinks, and infers content types. `file()` resolves relative to the config file directory. `sqlFile()` derives the migration id from the filename unless `id` is supplied; pass `{ name: "seed" }` for generated/idempotent SQL so the SDK compiles `_` from post-build file bytes. `nodeFunction()` stages a Node 22 function from built JavaScript; TypeScript function source paths are rejected with `TYPESCRIPT_FUNCTION_REQUIRES_BUNDLE` until a deterministic bundler path is introduced. Patch semantics — only the listed file changes: ```json { "project_id": "prj_...", "site": { "patch": { "put": { "index.html": { "data": "

v2

" } } } } } ``` Or via `--spec` for a one-line CLI invocation: ```bash run402 deploy apply --spec '{"project_id":"prj_...","site":{"patch":{"delete":["old.html"]}}}' ``` Astro builds: `--dir ` reads `dist/run402/adapter.json` and merges build ReleaseSpec slices (site/functions/routes). Combine with `--manifest` for cross-cutting slices (database, secrets, subdomains, i18n): ```bash # Astro-only: --dir is the whole spec source (requires @run402/astro installed) run402 deploy apply --dir ./dist --project prj_... # Astro + cross-cutting slices: --dir owns site/functions/routes, --manifest owns the rest run402 deploy apply --dir ./dist --manifest run402.config.json --project prj_... ``` CLI dynamically imports `@run402/astro/release-slice` from the consuming project. Requires `@run402/astro >=1.2.1` + `@run402/sdk >=2.18.0`; older SDKs reject `FunctionSpec.class: 'ssr'`, helper preflights `R402_ASTRO_SDK_VERSION_TOO_OLD` with upgrade command. Helper bundles SSR server with esbuild into single `source`, marks it with `class: "ssr"` and `capabilities: ["astro.ssr.v1"]`, roots site at `build.client` (`dist/run402/client/`, NOT `dist/`), omits `routes` so gateway's SSR catch-all works and base routes carry forward (also CI-safe without route scopes), defaults `site.public_paths: { mode: "implicit" }`, and colocates `_assets-manifest.json` inside `build.client`. Missing/incompatible manifest errors: `R402_ASTRO_ADAPTER_MANIFEST_MISSING` / `R402_ASTRO_ADAPTER_MANIFEST_VERSION_UNSUPPORTED` with `hint`+`docs`. SDK equivalent: `buildAstroReleaseSlice`. Do not hand-roll `site`/`public_paths`; shipping `run402/adapter.json` or `run402/server/**` as site content means source rooted at `dist/` instead of `dist/run402/client/`; SDK rejects `ASTRO_ADAPTER_TREE_IN_SITE`, gateway warns `SITE_NO_REACHABLE_HTML`. Stuck deploys: `activation_pending` (rare transient between SQL commit and pointer-swap) auto-resumes hourly. Static spec/config activation failures throw structured deploy errors promptly. Explicit resume: ```bash run402 deploy resume [--project prj_...] ``` Gateway reruns only failed phase forward; SQL is never replayed. Destructive apply recovery: `run402 deploy promote ` re-points live release at a prior ready row without re-running apply (no bytes/bundling/migration), just `internal.projects.live_release_id` pointer swap + ssr_cache flush. ```bash # rel_old (good) → rel_new (bad, destructive) → promote back run402 deploy promote rel_old_abc123 --project prj_xyz \ --allow-warning MIGRATIONS_NOT_REVERSIBLE # Promotion is origin-active when it returns; wait for public edge coherence. run402 deploy verify --operation op_... --wait ``` Read `operation_id` from the promote result and pass it to `deploy verify`. Promote success means the origin pointer is active; mutable public URLs can still be converging. The additive `edge` block reports `state`, `expected_max_lag_seconds`, and pointer-update status. `edge.verify_url` is the direct operation-scoped HTTP verification endpoint. Promote warnings/errors: `MIGRATIONS_NOT_REVERSIBLE` requires ack when target predates applied migrations; migrations remain applied against current schema. `FUNCTION_VERSION_MISMATCH` informational when overlapping names have different `code_hash` (Lambda code = current `$LATEST`). Rejects: `PROMOTE_TARGET_NOT_FOUND`, `PROMOTE_PROJECT_MISMATCH`, `PROMOTE_RELEASE_NOT_READY` (needs `ready|active|superseded`), `PROMOTE_NO_OP` (use `cache.invalidateAll`), `PROMOTE_WARNING_REQUIRES_ACK`. Deploy history/observability: ```bash run402 deploy list --project prj_... --limit 10 run402 deploy events --project prj_... run402 deploy verify --project prj_... --wait --timeout 120 run402 deploy release active --project prj_... --site-limit 5000 run402 deploy release get rel_... --project prj_... run402 deploy release diff --from empty --to active --project prj_... --limit 1000 run402 deploy diagnose --project prj_123 https://example.com/events --method GET run402 deploy resolve --project prj_123 --url https://example.com/events?utm=x#hero --method GET run402 deploy resolve --project prj_123 --host example.com --path /events --method GET ``` `list` -> `{ operations, cursor }`; SDK/MCP accept non-null cursor. `events` returns same `DeployEvent` shapes as inline apply events. `verify` calls the edge-coherence report endpoint and returns `{ status: "coherent"|"not_coherent", coherent, report }`; with `--wait`, stderr emits per-poll path summaries and exit code 2 means the report was valid but still not coherent before timeout. `release active|get` -> `{ release: ReleaseInventory }`: metadata, `state_kind` (`current_live|effective|desired_manifest`), `site.paths` (capped by `--site-limit`), `static_public_paths`, functions, secret keys only, subdomains, routes, migrations, `release_generation`, `static_manifest_sha256`, nullable `static_manifest_metadata`, `i18n` (`{ defaultLocale, locales, detect }` or `null`), warnings. `static_public_paths[]` has `public_path`, `asset_path`, `reachability_authority`, `direct`, cache class, content type. `static_manifest_metadata: null` = unavailable; when present has `file_count`, `total_bytes`, `cache_classes`, `cache_class_sources`, `spa_fallback`. Verify i18n with `jq '.release.i18n'`; absent field (older gateway) = unknown, not null. `release diff` -> `{ diff: ReleaseToReleaseDiff }`; `--from empty|active|release_id`, `--to active|release_id`. Migrations: `migrations.applied_between_releases`; secrets/subdomains: `added`/`removed`; routes: `added`/`removed`/`changed`; `static_assets`: unchanged/changed/added/removed plus `newly_uploaded_cas_bytes`, `reused_cas_bytes`, `deployment_copy_bytes_eliminated`, `legacy_immutable_warnings`, `previous_immutable_failures`, `cas_authorization_failures`. `deploy diagnose` URL-first; `deploy resolve` lower-level SDK/endpoint parity. Use either `--url` OR `--host` + optional `--path`. Both output `status`, `would_serve`, `diagnostic_status`, `match`, `summary`, normalized `request`, `warnings`, `resolution`, `edge_propagation`, `next_steps`. URL query/fragment ignored for lookup and reported under `request.ignored`. `asset_path`, `reachability_authority`, `direct` identify backing release asset and whether implicit, explicit `site.public_paths`, or route-only alias. Host/path misses exit 0 if resolver succeeded; branch on `would_serve: false`. Diagnostics may include `authorization_result`, `cas_object` (`sha256`, `exists`, `expected_size`, `actual_size`), `response_variant`, `allow`, `route_pattern`, `target_type`, `target_name`, `target_file`, and `edge_propagation` (`status`, `claimed_at`, `kvs_synced_at`, `expected_visible_by`, `hint`). Known `edge_propagation.status`: `settled`, `propagating`, `sync_pending`; non-settled statuses add warnings such as `edge_propagating` / `edge_sync_pending` and next steps to retry or run `run402 up verify`. Known `match`: `host_missing`, `manifest_missing`, `active_release_missing`, `unsupported_manifest_version`, `path_error`, `none`, `static_exact`, `static_index`, `spa_fallback`, `spa_fallback_missing`, `route_function`, `route_static_alias`, `route_method_miss`. Known `authorization_result`: `authorized`, `not_public`, `not_applicable`, `manifest_missing`, `target_missing`, `active_release_missing`, `unsupported_manifest_version`, `path_error`, `missing_cas_object`, `unfinalized_or_deleting_cas_object`, `size_mismatch`, `unauthorized_cas_object`. Known `fallback_state`: `active_release_missing`, `unsupported_manifest_version`, `negative_cache_hit`. Preserve unknown future strings. `result` = diagnostic body status, not HTTP transport. Resolve/diagnose is not fetch, purge, or cache-policy oracle. Route warning guidance: | Code | Meaning | Recover | |---|---|---| | `PUBLIC_ROUTED_FUNCTION` | Function becomes public same-origin browser ingress. | Review app auth, CSRF, CORS/`OPTIONS`, and cookies; direct `/functions/v1/:name` remains API-key protected. Prefer `--allow-warning PUBLIC_ROUTED_FUNCTION` after review; use `--allow-warnings` only after every warning was reviewed. | | `ROUTE_TARGET_CARRIED_FORWARD` | Carried-forward route still targets a base-release function. | Inspect `run402 deploy release active` and deploy a replacement route table if needed. | | `ROUTE_SHADOWS_STATIC_PATH` / `WILDCARD_ROUTE_SHADOWS_STATIC_PATHS` | Dynamic route shadows direct public static content. | Inspect warning details, active routes, `static_public_paths`, and resolve diagnostics; confirm only when intentional. | | `METHOD_SPECIFIC_ROUTE_ALLOWS_GET_STATIC_FALLBACK` | Unmatched methods can serve static content. | Confirm fallback is intended or add method coverage. | | `WILDCARD_ROUTE_EXCLUDES_MUTATION_METHODS` | Wildcard function route only allows `GET`/`HEAD`. | Add mutation methods e.g. `POST`, omit methods for an API prefix, or set `acknowledge_readonly: true` on an intentionally read-only GET/HEAD final-wildcard function route. `--allow-warning WILDCARD_ROUTE_EXCLUDES_MUTATION_METHODS` is a reviewed CLI escape hatch; broad `--allow-warnings` is last resort. | | `ROUTE_TABLE_NEAR_LIMIT` | Route table is near a limit. | Consolidate or remove routes. | | `ROUTES_NOT_ENABLED` | Routes are disabled for the project/environment. | Deploy without `routes` or request enablement; direct function invoke is not a browser-route substitute. | | `STATIC_ALIAS_SHADOWS_STATIC_PATH` / `STATIC_ALIAS_RELATIVE_ASSET_RISK` | Route-only static alias conflicts with a direct public static path or has relative-asset risk. | Inspect active routes, `static_public_paths`, and the backing `asset_path`; prefer `site.public_paths` for ordinary clean URLs and confirm only when intentional. | | `STATIC_ALIAS_DUPLICATE_CANONICAL_URL` / `STATIC_ALIAS_EXTENSIONLESS_NON_HTML` | Route-only static alias may duplicate another direct public path or expose extensionless non-HTML. | Use one canonical public path per page and reserve exact static route targets for method-aware aliases. | | `STATIC_ALIAS_TABLE_NEAR_LIMIT` | Static route targets are near route-table limits. | Avoid one-static-route-target-per-page tables; consolidate. | Runtime route failure codes: `ROUTE_MANIFEST_LOAD_FAILED` (manifest/propagation), `ROUTED_INVOKE_WORKER_SECRET_MISSING` (custom-domain Worker secret), `ROUTED_INVOKE_AUTH_FAILED` (internal invoke signature), `ROUTED_ROUTE_STALE` (release revalidation failed), `ROUTE_METHOD_NOT_ALLOWED`, `ROUTED_RESPONSE_TOO_LARGE` (>6 MiB). **Routed functions: locale awareness.** `spec.i18n` negotiates locale per routed-function request and exposes `x-run402-locale` / `x-run402-default-locale` headers (omitted when active release lacks `i18n`). Carry-forward rules are simpler than routes; no `{ replace }` envelope: ```json { "i18n": { "default_locale": "en", "locales": ["en", "es", "fr", "zh-Hant"], "detect": ["cookie:wl_locale", "accept-language"] } } ``` - Omit `i18n` to carry forward from the base release; pass `"i18n": null` to clear the slice on the new release; pass `{ default_locale, locales, detect? }` to replace. - `default_locale` must byte-match one `locales[]` entry; no silent canonicalization; CLI/SDK validate before planning. - Locale tags must match `/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/` and RFC 5646 canonical casing: primary lowercase, script Titlecase, 2-alpha region uppercase, 3-digit region preserved, variants/extensions lowercase. Examples: `pt-BR`, `zh-Hant`, `zh-Hant-TW`, `de-1996`. Non-canonical deploy error: `R402_LOCALE_NOT_CANONICAL` (400) with `fix: { input, canonical }`. `locales[]` non-empty, max 50. No silent canonicalization because DB translation keys often use literal locale strings. - Negotiation returns canonical casing from `locales[]`, NOT the request's casing. - `detect[]` default `["accept-language"]`, max 10, `[]` = always default; first match wins. Sources: `"accept-language"` (RFC 9110 + RFC 4647 lookup truncation `zh-Hant-TW` -> `zh-Hant` -> `zh`; generic request tag does not match more specific configured tag, e.g. `es` not `es-MX`) and `"cookie:"` (RFC 6265 name regex `/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/`, raw value matched case-insensitively). - Static-route hits do NOT receive locale negotiation; only routed HTTP function invocations do. - Run402 does NOT inject `Vary` headers — apps that return public-cacheable responses varying by locale must set their own `Vary` until per-locale edge caching ships. Routed-function read pattern: single-arg `(req)`, not `(req, ctx)`; bundled runtime translates envelope to Web `Request`, so `context.locale` is not visible. ```ts export default async (req) => { const locale = req.headers.get('x-run402-locale'); const defaultLocale = req.headers.get('x-run402-default-locale'); if (locale && locale !== defaultLocale) { return renderWithTranslations({ locale }); } return renderBase({ locale: defaultLocale ?? 'en' }); }; ``` Language switchers must write a cookie; `localStorage` only is invisible to server-side negotiation. Mirror locale to cookie and declare cookie source in `spec.i18n.detect`: ```js function setLanguage(lang) { localStorage.setItem('wl_locale', lang); document.cookie = `wl_locale=${encodeURIComponent(lang)}; path=/; max-age=31536000; samesite=lax`; } ``` Deploy with `"detect": ["cookie:wl_locale", "accept-language"]`. Migration registry: key = `(id, checksum)`. There are two authoring kinds. Versioned migrations use `id`: same id+SQL = noop; same id+different SQL = `MIGRATION_CHECKSUM_MISMATCH`; if you revise one, ship a new id. Content-tracked migrations use `name`: the SDK compiles `_`, so changed generated SQL applies once under a new id and unchanged re-ups noop. SQL declared with `name` MUST be idempotent (`CREATE TABLE IF NOT EXISTS`, `CREATE OR REPLACE`, upserts, `ADD COLUMN IF NOT EXISTS` in a `DO` block) because changed content re-runs against a database where prior versions may already exist. If generated SQL is trapped behind a static id mismatch, replace `"id": "seed"` with `"name": "seed"` as the primary recovery path; admin checksum adoption is only for legacy/out-of-band cases. --- ### GitHub Actions OIDC Deploys Use this when the same repo should deploy itself from GitHub Actions without storing Run402 service keys, allowance files, or API keys in GitHub secrets. KISS rule: link once locally, then CI runs the same `run402 deploy apply` command agents already know. Local setup: ```bash run402 ci link github --project prj_... --manifest run402.deploy.json run402 ci link github --project prj_... --manifest run402.deploy.json --route-scope /admin --route-scope /api/* ``` Full link syntax: ```bash run402 ci link github \ [--project ] \ [--manifest ] \ [--repo ] \ [--branch | --environment ] \ [--repository-id ] \ [--workflow ] \ [--expires-at ] \ [--route-scope ...] \ [--force] ``` Defaults: - `--project`: active project - `--manifest`: `run402.deploy.json` - `--repo`: inferred from `git remote get-url origin` - `--branch`: current branch from `git branch --show-current` - `--workflow`: `.github/workflows/run402-deploy.yml` - `--route-scope`: omitted by default, which means no CI route-declaration authority; repeat for exact paths like `/admin` or final wildcard prefixes like `/api/*` - allowed events: fixed to `push` and `workflow_dispatch` - allowed action: fixed to `deploy` The command fetches GitHub's numeric repository id using `GITHUB_TOKEN` or `GH_TOKEN` when available. If lookup fails, pass `--repository-id ` explicitly. The subject is generated from `--branch` as `repo::ref:refs/heads/`, or from `--environment` as `repo::environment:`. Generated workflow shape: ```yaml name: Run402 Deploy on: push: branches: ["main"] workflow_dispatch: permissions: contents: read id-token: write jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Deploy to run402 run: npx --yes run402@3.7.5 deploy apply --manifest 'run402.deploy.json' --project 'prj_...' < /dev/null ``` Output on success: ```json { "binding_id": "cib_...", "project_id": "prj_...", "provider": "github-actions", "subject_match": "repo:owner/name:ref:refs/heads/main", "allowed_events": ["push", "workflow_dispatch"], "allowed_actions": ["deploy"], "route_scopes": ["/admin", "/api/*"], "github_repository_id": "123456789", "github_repository_id_status": "verified", "workflow_path": ".github/workflows/run402-deploy.yml", "manifest_path": "run402.deploy.json", "run402_version": "3.7.5", "delegation_chain_id": "eip155:84532", "bootstrap_caveat": "Commit the generated workflow and manifest before expecting GitHub Actions deploys.", "consent_summary": ["..."], "revocation_residuals": ["..."] } ``` Management: ```bash run402 ci list [--project ] run402 ci revoke ``` `list` prints `{ "project_id": "...", "bindings": [...] }`. `revoke` prints `{ "binding": {...}, "revoked": true, "revocation_residuals": [...] }`. Intentional omissions in v1: no raw `--subject`, no wildcard flag, no `--allow-event`, no PR deploy flags, and no `--no-repository-id`. Use `--branch` or `--environment`; create a follow-up design before broadening trust. CI deploy restrictions: when `run402 deploy apply` runs inside GitHub Actions with OIDC env vars present, it uses the GitHub subject token, exchanges it for a Run402 CI session, and skips the local allowance preflight. CI manifests may include only `project_id`, `database`, `functions`, `site`, absent/current `base`, and route declarations covered by the binding's `route_scopes`. Without `--route-scope`, CI cannot ship `routes`. CI cannot ship `secrets`, `subdomains`, `checks`, unknown future top-level fields, non-current base, or oversized manifests that require `manifest_ref`. Common CI error codes: - `invalid_token`: check `permissions: id-token: write` and the workflow's OIDC environment - `access_denied`: no active binding matched this repo/branch/environment - `binding_revoked`: a matching binding existed but was revoked (most often the project was transferred/handed off, which suspends the prior org's CI bindings) — re-run `run402 ci link github` to re-create it; do NOT run `set-asset-scopes` (it 409s on a revoked binding) - `event_not_allowed`: v1 allows only `push` and `workflow_dispatch` - `repository_id_mismatch`: re-link from the current repo or pass the correct numeric `--repository-id` - `forbidden_spec_field` / `forbidden_plan`: remove disallowed CI manifest fields or run the deploy locally - `CI_ROUTE_SCOPE_DENIED`: re-link with covering `--route-scope` patterns e.g. `/admin` or `/api/*`, or run the route-changing deploy locally - `payment_required`: renew/upgrade/fund the project tier outside CI, then rerun the workflow --- ### Unified Deploy Details Use `run402 deploy apply --manifest app.json` for full-stack releases; see the Unified Apply example above. `project_id` is required unless `--project` or active project is used. Omitted top-level sections carry forward. Strict adapter: only top-level `$schema` ignored; typo/no-op fields fail before planning. Function specs add v1.51+ auth gates: - `require_auth: true`: valid project user JWT required; 401 on anonymous; no DB lookup; independent from `require_role`. - `require_role: { table, id_column, role_column, allowed[], cache_ttl? } | null`: implies auth; gateway reads project-schema table with RLS bypass; 403 if role not in `allowed`; `null` removes gate in patch mode; `cache_ttl` default 60, max 600, 0 disables cache. - Passing gate injects `x-run402-user-id` (any gate) and `x-run402-user-role` (`require_role`) into request; read headers directly or use `auth.*`. - Validation: all `require_role` blocks in one release share `(table,id_column,role_column)`; schema-qualified identifiers rejected; `0 <= cache_ttl <= 600`; empty `allowed` rejected; missing table/column fails activation with `DEPLOY_INVALID_ROLE_GATE` (422) before live flip. Auth-gate fragment: ```json { "functions": { "patch": { "set": { "list-my-items": { "source": { "path": "functions/list.ts" }, "require_auth": true }, "delete-content": { "source": { "path": "functions/delete.ts" }, "require_role": { "table": "members", "id_column": "user_id", "role_column": "role", "allowed": ["admin"], "cache_ttl": 60 } }, "moderate-content": { "source": { "path": "functions/moderate.ts" }, "require_role": { "table": "members", "id_column": "user_id", "role_column": "role", "allowed": ["admin", "moderator"] } } } } } } ``` Role reads (`@run402/functions` 3.4.0+, `{ from }` since 3.5.0): edge gate authenticates Bearer JWT and cookie-session SSR browsers (`ssr-aware-role-gate`). Choose by topology: - Dedicated function/route: prefer deploy-spec `require_role`; per-function, pre-dispatch, TTL-cached. `await auth.requireRole("operator")` returns `{ user, role }`; throws distinct `RoleGateNotConfiguredError` (500) vs `InsufficientRoleError` (403). Multi-role: `await auth.role()` and branch. Browser console can set gate `on_deny: "redirect"` + same-origin `sign_in_path` for anonymous HTML 303 to sign-in; authenticated wrong-role remains 403 JSON. - Catch-all SSR/finer per-path control: use in-function `{ from }` guard; edge gate would also gate public catch-all/404 and `/admin/login`. ```ts const { user } = await auth.requireRole("operator", { from: { table: "staff", idColumn: "user_id", roleColumn: "role" } }); // or, for an .astro page (a throw in frontmatter renders a 500, not a redirect) use the non-throwing read: const role = await auth.role({ from: { table: "staff", idColumn: "user_id", roleColumn: "role" } }); if (role !== "operator") return Astro.redirect("/admin/login", 303); ``` `run402 auth scaffold-roles --roles operator` emits conventional `app_roles(user_id uuid, role text)` migration, matching `requireRole` snippet, and service-role `INSERT` for FIRST role (table starts empty; first grant bypasses RLS). Gate keys on tenant user id (`internal.users.id` / JWT `sub`), not wallet. Applies to routed and direct (`POST /functions/v1/:name` with API key + user JWT); direct still requires API key before gate. Binary files (images, fonts, PDFs): Set `"encoding": "base64"` and provide base64-encoded data. MIME types are auto-detected from the file extension (`.png` → `image/png`, `.woff2` → `font/woff2`, etc.). Text files use `"encoding": "utf-8"` (the default — can be omitted). Assets slice: top-level `ReleaseSpec.assets` promotes content-addressed asset entries in the same atomic transaction as site/functions/secrets. ```json "assets": { "put": [ { "key": "static/app.css", "sha256": "<64-hex>", "size_bytes": 1234, "content_type": "text/css", "visibility": "public", "immutable": true } ] } ``` Additive batch: locally computed `sha256`; gateway dedupes CAS; only new shas upload through same S3 presign flow as `assets put`. Defaults `visibility: "public"`, `immutable: true`; other keys untouched. ```json "assets": { "put": [...], "sync": { "prefix": "static/", "prune": true, "confirm": { "base_revision": "", "delete_set_digest": "", "expected_delete_count": 42 } } } ``` Declarative sync: `prune: true` deletes keys under explicit `prefix` absent from new `put`; no implicit project-root prune. First apply without `confirm` returns `asset_sync` (`base_revision`, `delete_set_digest`, `expected_delete_count`, `sample_keys`); re-run with `confirm`. Activation rechecks and fails `ASSET_SYNC_DRIFT` if inventory mutates between commit/activation. No `run402 assets sync`; use manifest + `deploy apply` or SDK helpers (`uploadDir`, `syncDir`, `prepareDir`, `putMany`). Verify block (authoring-only): deploy manifests accept a top-level `verify` with post-apply HTTP checks — the same `verify.http[]` shape app manifests use. It is stripped before the wire `ReleaseSpec` (like `$schema`); `run402 up` runs the checks after a successful apply (propagation-tolerant, results in `result.verification.http[]` + a `result.verify` rollup) and `run402 up verify` reruns them on demand. Each check: `id` (unique, required), `path` (resolved against the project public origin) or `url`, `expect: { status }` (snake alias `expected_status`), optional `retries`. ```json "verify": { "http": [ { "id": "home", "path": "/", "expect": { "status": 200 } }, { "id": "api", "path": "/v1/health", "expected_status": 204 } ] } ``` Migrations: inline `sql` or per-entry `sql_path` / `sql_file`. Make re-runnable: `CREATE TABLE/INDEX IF NOT EXISTS`; new columns need `ALTER TABLE ... ADD COLUMN` in an idempotent `DO` block: ```sql CREATE TABLE IF NOT EXISTS items (id serial PRIMARY KEY, title text NOT NULL); DO $$ BEGIN ALTER TABLE items ADD COLUMN priority int DEFAULT 0; EXCEPTION WHEN duplicate_column THEN NULL; END $$; ``` Authorization manifest (`https://run402.com/schemas/manifest.v1.json`): new tables are dark until declared with `expose: true`. Prefer `database.expose` or a `manifest.json` in bundle `files[]`; platform validates against migration SQL, applies it, and strips `manifest.json` before site deploy so it is never public. Success includes `manifest_applied: true`; missing migration table -> HTTP 400 with structured `errors[]`. Dry feedback: `run402 projects validate-expose [project_id] --file manifest.json [--migration-file setup.sql]`; validates auth/expose manifest only, does not execute SQL, exits 0 even with `has_errors: true`. Built-in table policies: - `user_owns_rows` — owner column matches `auth.uid()`; requires `owner_column`. With `force_owner_on_insert: true`, per-table trigger fills owner only when `NEW.` is `NULL`; explicit different owners still fail `WITH CHECK`. `service_key` bypasses RLS but trigger still runs; admin writes should set owner if no JWT. Best for user-scoped data. `uuid` owner columns get index-friendly policies; other types use `::text` cast with warning; btree index auto-created. - `public_read_authenticated_write` — anyone reads; any authenticated user can INSERT/UPDATE/DELETE any row (not just their own). For collaborative content (shared boards, announcements). - `public_read_write_UNRESTRICTED` — ⚠ fully open; `anon_key` can read AND write any row. For intentionally public tables only (guestbooks, waitlists, feedback forms). Requires `"i_understand_this_is_unrestricted": true` on the table entry. - `custom` — escape hatch. Provide `custom_sql` containing `CREATE POLICY` statements; they run inside the apply transaction after RLS is enabled + forced. | Policy | anon SELECT | anon writes | auth SELECT | auth writes | |---|:---:|:---:|:---:|:---:| | (omitted from manifest) | — | — | — | — | | `user_owns_rows` | — | — | own rows | own rows | | `public_read_authenticated_write` | all | — | all | all rows | | `public_read_write_UNRESTRICTED` | all | yes | all | yes | `—` = denied. `service_key` bypasses all policies. Views are always created with `security_invoker=true` — they inherit the underlying table's RLS. RPCs require an entry in `rpcs[*]` with `grant_to` to be callable as `/rest/v1/rpc/` (since v1.30, `CREATE FUNCTION` revokes PUBLIC EXECUTE automatically). Worked example covering all three slices (drop in as `manifest.json` or under `database.expose` in a deploy manifest): ```json { "$schema": "https://run402.com/schemas/manifest.v1.json", "version": "1", "tables": [ { "name": "posts", "expose": true, "policy": "public_read_authenticated_write" }, { "name": "notes", "expose": true, "policy": "user_owns_rows", "owner_column": "user_id", "force_owner_on_insert": true } ], "views": [ { "name": "posts_public", "base": "posts", "select": ["id", "title", "published_at"], "expose": true } ], "rpcs": [ { "name": "increment_counter", "signature": "(counter_name text)", "grant_to": ["authenticated"] }, { "name": "now_utc", "signature": "()", "grant_to": ["anon", "authenticated"] } ] } ``` `rpcs[*].signature`: parenthesized args (`"()"` for none), regex `^\([^;]*\)$`; `grant_to` non-empty roles (`anon`, `authenticated`, `service_role`, `project_admin`). Function must exist in migration SQL; manifest grants EXECUTE only. Views require `base` + non-empty `select`, are `security_invoker=true`, inherit base RLS. Ad-hoc: `projects apply-expose --file manifest.json`; inspect with `projects get-expose ` (`source: "applied" | "introspected"`). Manifest is convergent; removed items revoke policies/grants/triggers/views, so include full desired exposed surface each apply. Deploy: ```bash run402 deploy apply --manifest app.json ``` Deploy runs migrations, applies `database.expose`, deploys functions/site/assets, claims subdomains, and updates routes atomically. Set secret values first with `run402 secrets set`; deploy manifests only declare value-free `secrets.require` / `secrets.delete`. ### Step-by-Step Deploy If you want more control: ```bash # 1. Provision a database run402 projects provision --name my-app # 2. Create tables run402 projects sql "CREATE TABLE items (id serial PRIMARY KEY, title text NOT NULL, done boolean DEFAULT false)" # 3. Insert seed data run402 projects sql "INSERT INTO items (title) VALUES ('Buy groceries'), ('Read a book')" # 4. Declare authorization. Write manifest.json first: # {"version":"1","tables":[{"name":"items","expose":true,"policy":"public_read_authenticated_write"}]} run402 projects validate-expose --file manifest.json run402 projects apply-expose --file manifest.json # 5. Deploy a static site (uses active project automatically) run402 sites deploy --manifest site.json # 6. Claim a subdomain (uses active project + last deployment automatically) run402 subdomains claim my-app ``` --- ## Command Reference ### up - `run402 up [repo-or-path] [--name ] [--project ] [--manifest ] [--dir ] [--tier ] [-y|--yes] [--check|--print-spec|--plan|--require-plan ] [--verify] [--propagation-budget-s ] [--no-propagation-wait] [--quiet]` — SDK-owned recursive app deploy. Validates `run402.deploy.json`/`app.json`, requires explicit `--manifest` for executable `.ts/.js` configs, ensures missing Cloud prerequisites when approved, resolves/creates/links a project, then applies the manifest. Output includes `steps[]`; success has no top-level `status`. Use `--check` for local-only validation, `--print-spec` for normalized `ReleaseSpec`, `--plan` for a gateway-reviewed non-deploying plan, and `--require-plan` for exact reviewed apply. App HTTP verification reports fresh edge misses as `propagation_pending`, waits up to `--propagation-budget-s` (default 120), and `--no-propagation-wait` returns the pending state immediately. Add `--verify` to wait for gateway/edge release coherence after the deploy and attach `edge_coherence`; non-coherence exits 2. - `run402 up verify [repo-or-path] [--project ] [--manifest ] [--dir ] [--propagation-budget-s ] [--no-propagation-wait] [--quiet]` — rerun manifest HTTP verification (app manifest `verify.http[]` or deploy-manifest top-level `verify`) without deploying, uploading, creating a project, or mutating resources. Use it after `propagation_pending` or before declaring a consumer copy healthy. ### init - `run402 init` — set up with x402 (Base Sepolia). Creates allowance, requests faucet, checks tier, lists projects. - `run402 init --api-base ` — configure the active profile to target a Run402 Core/API base. For Core, this does not create an allowance, request faucet funds, or require a Cloud tier. - `run402 init mpp` — set up with MPP (Tempo Moderato testnet). Same steps, different payment rail. ### pay `run402 pay [--method ] [--body ] [--max-usd ] [--idempotency-key ] [--require-receipt]` calls an arbitrary x402-priced HTTP endpoint through the SDK buyer. The request body goes in `--body` and nowhere else: `--json` is the CLI-wide output-format flag, takes no value, and is a no-op here because `pay` always prints JSON. Writing `--json ''` does not send the payload — it fails `BAD_USAGE` with a hint naming `--body`. The default ceiling is `$0.10`; `--max-usd` accepts up to six decimal places and is converted exactly to USD micros. `--require-receipt` requires a verified wallet-rooted offer before payment and a matching receipt afterward. JSON output is the complete `x402-commerce-result.v1` envelope with settlement, movement/replay, delivery, offer, merchant-receipt, signer-relationship, policy, and portable evidence. Unpriced URLs return `payment: null`. For `PAYMENT_INTENT_PENDING` on a trusted Run402 host, wait for `Retry-After` and repeat the identical command with the same payer, request, and key. Never substitute a fresh key. Custom/arbitrary hosts and other `funds_moved: "unknown"` outcomes remain ambiguous and require reconciliation. ```bash run402 pay https://seller.example/translate --method POST \ --body '{"text":"hello"}' --max-usd 0.05 \ --idempotency-key translation:1 --require-receipt ``` ### status `run402 status` — show full organization state in one shot (wallet, rail, balances, tier, projects, active project). Read-only, JSON output. Includes a `wallet: { local_label, server_label, address }` object naming the active named wallet (`local_label` is the local selector, `server_label` the server-synced display name or null), a top-level `rail`, and a `balances: { on_chain_usd_micros, on_chain_token, prepaid_credit_usd_micros, held_usd_micros }` object. The on-chain token tracks the rail (USDC on x402, pathUSD on mpp); prepaid credit is rail-independent. ### wallets Manage multiple named wallets (profiles) on one machine. Keys never leave the machine (non-custodial). The `default` wallet lives at the config-dir root; named wallets live under `{config_dir}/profiles//`. - `run402 wallets list` — JSON array of `{ local_label, server_label, address, address_short, rail, active }`. Reads non-secret `meta.json`; never loads private keys. - `run402 wallets current` — the resolved active wallet `{ name, source, source_detail, address, label, warnings }`. `source` ∈ flag|env|binding|config|default. `warnings` surfaces env-vs-binding conflicts and local-name-vs-server-label drift. - `run402 wallets new [--mpp]` — create a new named wallet (generates a key). `{ name, address, rail, created: true }`. - `run402 wallets use ` — set the global default wallet (`config.json` `active_wallet`). `{ name, active: true }`. - `run402 wallets rename ` — rename a wallet; renaming `default` migrates its root files into `profiles//`. `{ from, to, renamed: true }`. - `run402 wallets bind []` — write `./.run402.json` binding this directory to a wallet (defaults to the active one). Safe to commit (holds only a name). `{ wallet, file, bound: true, safe_to_commit: true }`. - `run402 wallets unbind` — remove `./.run402.json`. `{ file, unbound }`. - `run402 wallets import --key ` — adopt an existing 0x-prefixed 64-hex private key (file path or `-` for stdin) as a named wallet. `{ name, address, imported: true }`. - `run402 wallets rm --yes` — delete a wallet and its keys. Requires `--yes` (agent-first: no interactive prompt). Refuses to remove `default`. `{ name, removed: true }`. - Server-side display label: `new`/`rename`/`import` push the wallet's name to a server-side label (signed by the wallet — proof of control) so the same name shows cross-machine and in the operator console (WEB). Best-effort and on by default; `RUN402_WALLET_LABEL_SYNC=0` opts out (fully offline wallet ops). The local folder name is the source of truth; the label is a mirror, and `wallets current` flags any drift. - Selection for ANY command: `--wallet ` (alias `--profile`) > `RUN402_WALLET` > nearest `./.run402.json`/`.run402.local.json` > `wallets use` default > `default`. A conflicting env + binding errors with `WALLET_SELECTION_CONFLICT` (resolve via `--wallet`, `unset RUN402_WALLET`, or `wallets unbind`). Selecting a non-existent wallet errors with `WALLET_NOT_FOUND`. ### allowance - `run402 allowance ` - `run402 allowance checkout --amount ` - `run402 allowance history [--limit ]` ### tier Tier and quotas are per organization (not per project) — `set` is organization-wide, `status.pool_usage` is the pooled total across every project in the organization. `set` refetches status after the call and includes it as `status_after`. - `run402 tier status` - `run402 tier set ` ### credentials Two surfaces under one command. `credentials ` acts on PROJECT CREDENTIALS (rows on the gateway); `credentials project-keys ` acts on the LOCAL key cache on this machine. A project credential (`r402_…`) is named, listable, expiring and individually revocable, and several may be live per kind at once — that overlap is how you rotate with no downtime. It replaces the legacy `anon_key`/`service_key`, which are derived from the platform signing key, never expire, cannot be revoked one at a time, and whose signing key is being retired. Secrets are returned EXACTLY ONCE, from `issue`, `rotate` and `token`. Full JSON goes to stdout so it can be piped (`| jq -r .secret`); warnings go to stderr. Never write one of these responses to a result cache or tmp file. - `run402 credentials status [--project ]` — am I still on the retiring key? Returns `state: "legacy"|"rotatable"`, `rotatable_credentials`, `credentials[]`, and `retirement.gated_on[]`. `retirement.deadline` is ALWAYS null and that is deliberate: retirement is gated on conditions (every tenant migrated, 30 consecutive days of zero legacy-key use, explicit operator approval), never a date — do not plan against one. Needs only `project.read`, so automation can check its own posture. - `run402 credentials issue --kind --name [--project ] [--expires ]` — mint one; secret printed ONCE. `--name` must be unique among LIVE credentials; re-using a live name returns `409 CREDENTIAL_NAME_TAKEN`, and that collision IS the idempotency story (a retried create never silently mints a second credential). `--expires` must be in the future and within one year. - `run402 credentials list [--project ] [--include-revoked]` — metadata only; never a secret or a secret hash. - `run402 credentials rotate [--project ]` — mint a replacement and revoke the old one in one transaction, keeping the name and recording `replacement_of`. New secret printed ONCE. For a rotation with NO downtime window, prefer issuing a second credential, deploying it, then revoking the first; use `rotate` when the old secret is already compromised. - `run402 credentials revoke [--project ] [--reason ]` — immediate, and frees the name for reuse. - `run402 credentials token [--project ] [--kind ]` — mint a SHORT-LIVED token (defaults to `service`). This is the cold-restart recovery path and the ONE credential call an agent can make with no human present: a delegate is accepted. No step-up, because there is nobody to prompt; what it returns expires, so it cannot become a durable root. Authority split: `issue`/`rotate`/`revoke` require owner membership on the project's owning org PLUS a fresh step-up, and a delegate can NEVER satisfy them — otherwise a scoped agent credential could escalate itself into a permanent root. Authenticate with a wallet (SIWX) or a control-plane session (`run402 operator login --step-up`). `token` is the deliberate exception. - `run402 credentials project-keys list` — LOCAL CACHE read. Lists cached project-key entries with `source: "local_cache"`, `cache_path`, `wallet`/`profile`, key presence, prefixes, fingerprints, and timestamps. Never prints full keys. - `run402 credentials project-keys status --project ` — LOCAL CACHE read for one project id. `configured: false` means this selected wallet/profile lacks cached keys; it does not mean the server project is missing. - `run402 credentials project-keys import --project --service-key-stdin` — import a service key from stdin. Optional anon key comes from `--anon-key-env `. - `run402 credentials project-keys import --project --service-key-env ` — import a service key from an environment variable. Do not pass service keys as argv values. - `run402 credentials project-keys import --project --anon-key-env ` — anon-only rotation. Import writes the whole cache entry, so the FIRST import for a project must supply a service key; once an entry exists, `--anon-key-env` alone rotates the anon key and keeps the cached service key. Rotating the anon key therefore never requires exporting the service key with `--reveal` and passing it back through a shell. Passing `--anon-key-env` with no cached service key fails with `BAD_USAGE` naming that flag. - `run402 credentials project-keys export --project --reveal` — print cached secret key material. Requires `--reveal`. - `run402 credentials project-keys remove --project ` — remove one local cache entry without deleting or changing the server project. ### projects - `run402 projects quote` - `run402 projects list [--org ] [--all]` — SERVER read of the named, domain-aware inventory (NOT the local project-key cache). Membership-scoped by default: every project owned by an org your wallet is an active member of, each row `{ project_id, name, site_url, custom_domains, org_id, status, active }` (`active` from local state). `--org ` filters to one org (authorize-before-reveal: non-member/guessed id -> 403, non-UUID -> 400). `--all` reads the cross-wallet inventory across every wallet controlling your operator email — run `run402 operator login` first for the union, else it falls back to the current wallet's slice and echoes `scope`. Bare `run402 projects list` is the cold-start path (no login needed). Tier/lifecycle live on the organization — use `run402 status` / `run402 tier status`. - `run402 projects rename --name