# 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 > HTTP API reference: https://run402.com/llms-full.txt > Operated by: 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, grant keys, freshness, and spend policy determine what that principal may do. A founder agent may remain owner of its org-of-one. Agent-critical events: - Shared full-stack workflow: `run402 up` coordinates planning, staging, migrations, activation and verification. Failures can leave resumable or partial work; promoting an earlier release does not undo migrations. Inspect returned operation state and next actions. `deploy` is the advanced apply primitive. - No platform token: the local wallet (`~/.config/run402/wallet.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, MPP pathUSD on Tempo, or sats over Bitcoin Lightning (MPP) from the agent's platform-minted wallet, signed by the local wallet. The organization's allowance (card, Lightning, or voucher funded) settles first when it covers the price; CLI behavior is unchanged. `run402 wallets ` — `current` reports the active wallet's address, `rail`, file path, and faucet use; `balance` reads on-chain funds plus the organization's `allowance_usd_micros`; `run402 wallets lightning status` reports a Lightning profile's `lightning` block (wallet id, status, custody `run402_hub`, address, budget, starter, `balance_sats`, `budget_remaining_sats`; never the pairing). Install + deploy — the only first-deploy path (the front door at is the same three lines with a full manifest): ```bash npm install -g run402@latest run402 up --name "my-app" -y # wallet, tier, project, your name, rehearsal when warranted, deploy run402 redeem # only if you were given a promo code — see Promo codes below ``` `up` is a thin CLI shim over the SDK action runner; the SDK owns manifest validation, project resolution, recursive prerequisites, idempotency-key derivation, and deploy. A static page never needs a key copied into it: every Run402 host serves `/_run402/config.js` (`window.RUN402 = { project_id, api_base, anon_key }`) — see "Runtime config on every host" below. `prototype` is free with the testnet faucet; use `hobby` / `team` for mainnet. Rehearsal is `up`'s decision, not a flag: a migration-bearing plan against a project with a live release is rehearsed on a contained branch and committed only on a passing report; a first deploy has nothing to protect and commits directly — see "Protecting a live database" below. `run402 deploy`, `run402 projects provision`, and `run402 deploy rehearse` are the primitives underneath `up`: `up` does any missing setup and then runs the same deploy `run402 deploy` runs on its own. 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. 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 events - Wallet: `~/.config/run402/wallet.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 / MPP 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` with `permissions: id-token: write`. - Projects, sites, subdomains, forks, functions, secrets, asset 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_WALLET_PATH` (custom wallet file path, default `{config_dir}/wallet.json`), `RUN402_GRANT_KEY` (a grant-key bearer from `run402 grants create --key`; when set it is the only credential sent, so a process with no wallet can deploy), `RUN402_AGENT_NAME` (the name this agent runtime declares for itself; `run402 up` sets it as an agent principal's display name, overriding an existing agent name; human/unknown principals are preserved — reported as `identity.source: "explicit"`; promotion credit and room presence use it; equivalent to `run402 whoami --set-name `), `RUN402_CLIENT` (declares the coding-agent client when `up` has no environment marker for it, e.g. `RUN402_CLIENT=grok`; checked before every marker; it names an unnamed agent principal but never overrides an existing name — use `RUN402_AGENT_NAME` for that), `RUN402_PROGRAM` / `RUN402_MODEL` (room-presence `program` / `model` labels; `program` is otherwise inferred as `claude-code`, `codex`, `cursor`, or `grok`), `RUN402_TRACE` (any non-empty value: one stderr trace line per SDK request — see Observability below), `RUN402_VAULT_TRACE` (debug-only, `1`: one stderr line per vault transport operation — 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`. ## 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`, `wallets current`): nullable typed fields, e.g. `{ wallet: null, hint: "Run: run402 init" }` or `configured: false`; 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, `wallets current` emits `{ "local_label": "default", "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 list`). `--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`'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`, 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_VAULT_TRACE`** — debug-only, vault-specific, and out of scope of the two kernel diagnostics above: set it to `1` and every `git push`/`git fetch`/`run402 repos ` call against a vault writes one stderr line per vault transport OPERATION (`vault-trace: [path=… | paths=N | objects=N | gen=… | repo=…] [bytes=N] ms`), plus a session summary at process exit (op count, time spent in the vault transport, wall-clock). This is the one place presigned-object PUT/GET traffic is visible at all — `RUN402_TRACE` and `sdk.stats()` above are scoped to the request kernel's own `apiBase`-relative calls, so they never see the direct-to-bucket presigned reads/writes the vault's carrier, WAL, and checkpoint objects ride. A traced GET for one of the five edge-eligible kinds (`wal_pack`, `ref_state`, `retention_roots`, `checkpoint_manifest`, `checkpoint_pack`) may in event be answered by the platform's CDN edge rather than the bucket directly when the gateway offers one for that read (vault-read-edge-cache, design D5) — automatic, verified byte-for-byte identically either way, and falling back silently to the direct presigned URL on any edge failure; the trace line reports the same op regardless of which one actually served it. Not a canonical surface, not what any round-trip budget is measured against — it is a debugging aid for understanding WHERE a slow `git push`/`fetch` spent its round trips. ## `run402 up` (SDK action runner) `run402 up [repo-or-path] [--name ] [--project ] [--manifest ] [--dir ] [--tier ] [-y|--yes] [--check|--print-spec|--print-manifest|--plan|--require-plan ] [--no-rehearse] [--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; when none is there it looks one directory down (immediate children, skipping `node_modules` and dot-directories) and reports what it finds in `UP_MANIFEST_REQUIRED` `details.nearby_manifests[]` with a leading `run_in_directory` next action (`run402 up --check --dir `) — `up` never walks into subdirectories on its own. `--manifest ` naming a file that does not exist fails with `MANIFEST_NOT_FOUND` (`details.path` is the resolved absolute path) and a `create_manifest` next action, for `.json` manifests, `run402.json`, and executable `.ts`/`.js` configs alike. 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`. 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: every local file the manifest references — migration `sql_path`/`sql_file`, function `source.path` and `files`, site `{ path }` entries and `dir()` targets, `assets.put[].source` — is checked to exist, and a missing one fails with `MANIFEST_FILE_MISSING` (exit 1) whose `details.missing[]` lists `{ field_path, path, kind }` (`migration_sql` | `function_source` | `site_file` | `site_dir` | `asset`) and whose `next_actions[]` carries one `create_file` entry per file. The same check runs in every mode, before any gateway call. 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` Conflicting selectors fail before wallet, tier, identity, link or Git mutations. Missing intent returns `UP_PROJECT_REQUIRED` with one `select_project` action. `-y` approves work only after the target is known. On an interactive terminal without `-y`, enter an existing project ID or `new `; there is no default project choice. Use `RUN402_PROJECT_ID` as an explicit CLI environment selector. `--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 wallet/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 wallet creation, faucet request, tier payment, project creation, workspace-link write, upload, gateway plan, or deploy commit. It is not a file-exists check: it normalizes the manifest and verifies every referenced file (see above), so a missing `sql_path`, function source, or site file fails here with `MANIFEST_FILE_MISSING` instead of at upload. For a `run402.json` app manifest that declares no `build.commands`, the release slice's references are verified before any build; a declared build may produce those files, so the check is deferred until after the build runs (the step says `file_references: "deferred_to_post_build"`). - `--print-spec` performs the same local validation and prints advanced SDK-native `ReleaseSpec` JSON. Use `--print-manifest` for reloadable snake_case authoring JSON, relative to the original manifest directory. Unsupported dynamic/secret values fail explicitly. `result.preflight` carries nullable target selection, local evidence and `gateway_validated: false`; see the deploy slice for deferred checks. - `--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 a local wallet, ensure a prototype tier by default, create a project from `--name`, write the workspace link, make sure this principal has a display name, join the project room under it, then apply the manifest. - Identity: `result.identity.principal` records authenticated principal ID/type/name; `result.identity.client` records detected client and declared agent name separately. Human and unknown principals are preserved even with `RUN402_AGENT_NAME`. For agent principals only, that explicit name wins; otherwise a detected client names an unnamed principal. Detection never overwrites an existing name. `detection.reason: principal_identity_preserved` explains human/unknown cases. Failed identity lookup reports unavailable and does not rename. Promotion credit uses the principal name, never room presence or client detection. An intentional principal rename uses `run402 whoami --set-name `; `up --name` is project metadata. - Rehearsal: `result.deploy.rehearsal` is `{ status: "passed", report, … }` when the plan was rehearsed on a contained branch first, or `{ status: "skipped", reason: "no_live_release" | "no_migrations" | "migrations_unchanged" | "disabled" | "reviewed_plan" | "unsupported" }` (`migrations_unchanged`: every migration in the manifest is already applied with an identical checksum, so a page-only redeploy that still carries `schema`/`seed` ships in seconds instead of spending a branch). A failed rehearsal exits non-zero with `REHEARSAL_FAILED` and the report; nothing is committed. `--no-rehearse` skips it. - Git: on a local directory `up` scaffolds git on the APP ROOT only (the manifest's directory): `git init` when it is not a repository, a `run402` remote (`origin` is never claimed), and a first vault push. A repository `up` just created is all-untracked by definition, so that first push captures it as a synthetic commit (`first_push.captured_dirty: true` with the `modified_captured` / `untracked_captured` disclosure); an existing repository keeps the clean-tree rule and a `SNAPSHOT_DIRTY_TREE` refusal is reported under `first_push_error`, never overridden. An app root that lies inside ANOTHER repository (a monorepo workspace — how most agent workspaces look) is left untouched by default: `result.repo` is `{ status: "skipped", reason: "inside_other_repository", toplevel, next_actions: [{ type: "create_nested_repo", command: "run402 up --nested" }] }`, and `--human` prints the same line. `--nested` gives that app its own nested repository with the encrypted remote: `git init -b main` there, the `run402` remote and first push from there, and exactly one line (`//`) appended to the enclosing repository's local `.git/info/exclude` — nothing else in the enclosing repository is touched (no `.gitignore`, index, or submodule), it never shows the app as untracked noise, and the flag is a no-op when the app root is already its own repository. A nested scaffold reports `result.repo.vault.nested: true`, `enclosing_toplevel`, and `excluded_in_enclosing`. `projects provision` never touches git. - Run402 Core `up` skips Cloud wallet/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`: 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 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 only in explicit stream/human mode, 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`. ## Protecting a live database (rehearsal is automatic) When a plan carries migrations and the target project already has a live release, `run402 up` and `run402 deploy` rehearse it before committing: a snapshot of the project becomes a contained branch, the candidate plan is applied there, built-in checks run (static 200s, function invocability, migration registry) plus any `checks` the spec declares, and only a `passed` report is committed — bound to the report's `required_plan`, so the plan that was rehearsed is the plan that ships. A first deploy has no live release to protect, so nothing is rehearsed and the result says `rehearsal: { status: "skipped", reason: "no_live_release" }`. A redeploy whose migrations are all checksum-identical noops (the plan's `migrations.new` is empty) has nothing to rehearse either: `rehearsal: { status: "skipped", reason: "migrations_unchanged" }` — keep `schema`/`seed` in the manifest and iterate on pages in seconds. `--no-rehearse` opts out for speed. The gateway decides, in the plan response: `rehearsal: { available, rehearse_url, reason: null | "no_migrations" | "no_live_release" | "migrations_unchanged", next_actions? }`. The SDK honours that verdict and, against an older gateway that still advertises rehearsal for an all-noop plan, derives `migrations_unchanged` from the plan's own `migrations: { new: [], noop: [...] }` buckets — the only client-side re-derivation, and it can only ever skip work. Primitives, for when you want the pieces: `run402 deploy rehearse ` rehearses an already-persisted plan without committing (a project with no live release rehearses on an EMPTY branch — never a refusal); `run402 snapshots …` are manual restore points; `run402 branches …` are contained, expiring data copies. ## Runtime config on every host (`/_run402/config.js`) Every host that serves a project's site — managed subdomain, branch's host, custom domain — answers `GET /_run402/config.js` with `window.RUN402 = { project_id, api_base, anon_key };` and `GET /_run402/config.json` with the same object, for the project that host resolves to at request time. A branch's host answers for the branch; a transferred project answers with its rotated anon key; the HTML never changes. `Cache-Control: public, max-age=60`. The service key is never served. ```html ``` `/_run402/` is reserved: a route pattern, static alias, public path, or site file under it is rejected at plan time. ## 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_WALLET`, `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 list`) 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` is walked to a deployed result by following `next_actions` — no local wallet -> `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` 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` -> top up the allowance, redeem a voucher, or fund the wallet. - `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 id 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 by signing in again with `run402 login` (a fresh passkey) on the same client, then retry. A `run402 login --device` session and a write approval never satisfy it. Distinct from `NOT_AUTHORIZED` (a role/grant gap, not a freshness gap). - `WRITE_APPROVAL_REQUIRED` / `WRITE_APPROVAL_SCOPE_MISMATCH` / `WRITE_APPROVAL_BINDING_MISMATCH` (HTTP 403) and `WRITE_APPROVAL_SESSION_INVALID` (HTTP 401) — a person signed in without a wallet needs a passkey **write approval** scoped to this `(action, target)` (the SIWX wallet path never hits this). The SDK raises a typed `WriteApprovalRequiredError` (`isWriteApprovalRequired()` guard) carrying `capability`, `target`, and a fully-resolved `approveCommand` / `approve_write` next action (e.g. `run402 approve --action project.deploy --project prj_x`). `SCOPE_MISMATCH` = the attached approval covers another action or target; `BINDING_MISMATCH` = it belongs to another sign-in session; `SESSION_INVALID` = it expired or was revoked. Resolve by running the surfaced `run402 approve` command (or let an interactive `provision`/`deploy` auto-approve). - `SESSION_READ_ONLY` (HTTP 403) — the sign-in session came from `run402 login --device` and can only read. Run `run402 login` (the browser passkey sign-in) to write. - 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 the Wallet and Funding ```bash run402 init # creates the wallet if absent; checks balance; faucets testnet USDC if zero; shows tier/projects run402 wallets current # The active wallet: address, rail, file path, faucet use run402 wallets fund # Get free testnet USDC (Base Sepolia) run402 wallets balance # On-chain USDC (mainnet + testnet) plus the organization's allowance ``` The wallet lives at `~/.config/run402/wallet.json` (0600). CLI signs x402 / MPP payments automatically; never handle private keys/payment libs manually. For a self-hosted Run402 Core Gateway, skip Cloud wallet 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` 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 adds to your organization's allowance, and the allowance 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 adds the amount 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 for that code's issuer; a code from another issuer has its own). Minting codes is not an agent operation — it needs an issuer key no tenant holds. ## Step 3: Set a Tier ```bash run402 tier set prototype # FREE on testnet — faucet USDC verifies your x402 setup ($0 real money); free tier, no lease run402 tier set hobby # $5 for 30 days (real money) run402 tier set team # $20 for 30 days (real money) ``` The allowance pays first. A promo code (`run402 redeem `) or a top-up adds to the organization's allowance, and `tier set` settles from it ahead of the payment paywall: no 402, no signed authorization, no USDC in the wallet, receipt `paid_with: "allowance"` with `allowance_used_usd_micros` and `allowance_remaining_usd_micros`. Only an allowance that falls short goes to x402 / MPP, and that `X402_INSUFFICIENT_FUNDS` error carries `details.allowance` (`allowance_usd_micros`, `price_usd_micros`, `shortfall_usd_micros`) with `redeem_voucher` / `top_up` next actions. Tier is organization-scoped. A start/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 start/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 (reported as `action: start | renew | upgrade`): - No tier or expired -> start - Same tier, active -> renew (extends from current expiry) - Higher tier -> upgrade (prorated refund to the 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). --- Use `run402 up` for normal releases. The commands below expose advanced planning, apply, inspection and recovery primitives. They share the same release model; none promises to reverse committed database migrations. ## 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(...)`. Runtime config tokens are opaque credentials: equivalent claims can have different ES256 signatures. Do not compare token strings to determine project identity; use `project_id` and `api_base`. Your HTML never needs a pasted key: load `/_run402/config.js` and read `window.RUN402.anon_key` (see "Runtime config on every host"). Wire fields — manifests, plan and commit bodies — are `snake_case` (`content_type`, never `contentType`; `contentType` is only the in-function `assets.put` JS option). 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. The example uses prototype-compatible function limits: 10 seconds and 128 MB. Larger values require a tier that supports them. `--check` validates locally; it does not certify the target organization's tier limits. ```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": 10, "memory_mb": 128 }, "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"`). Binary files are safe in every form. A `{ "path": ... }` entry is read from disk as bytes and uploaded as-is, never decoded as text, with the content type inferred from the extension (`.webp` → `image/webp`, `.png`, `.woff2`, `.pdf`, ...) unless `content_type` says otherwise; only a bare string is treated as UTF-8 text, and a string paired with a binary path or type is refused locally as `BINARY_CONTENT_REQUIRES_BYTES` before any request. So an image ships as `"sigil.webp": { "path": "assets/sigil.webp" }` next to `"index.html": { "path": "site/index.html" }`. To see what a manifest will ship before it ships, `run402 up --manifest run402.json --check` prints the preflight with `summary.site` (path count and a per-content-type tally) and makes no gateway call. 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. Embedding (framing) opt-in: every your host denies framing (`frame-ancestors 'none'` + `X-Frame-Options: DENY`) unless the release declares `site.embedding.frame_ancestors`, a non-empty list of platform catalog **keys** (never raw origins). Catalog: `localhost` -> `http://localhost:*` and `http://127.0.0.1:*`. With it, every response of the host (site files, public paths, SPA fallback, routed functions, `/_run402/*`, on every edge) sends `Content-Security-Policy: frame-ancestors ` and no `X-Frame-Options`. Omitted on a later apply = carried forward from the base release (a one-file `site.patch` never re-denies); `null` = back to deny; unknown key, duplicate, empty array, or extra member -> `INVALID_SPEC` on `site.embedding.frame_ancestors` naming the valid keys. Embedding-only specs are deploy content: `{ "site": { "embedding": { "frame_ancestors": ["localhost"] } } }`. Readback: `release active` -> `embedding` (keys or `null`), `deploy resolve` -> `embedding`, and the host's `/_run402/config.json` -> `embedding: { frame_ancestors: [] } | null` with `Access-Control-Allow-Origin: *`. Inside a third-party frame the app's cookies are third-party (a session cookie without `SameSite=None; Secure` is not sent) and hosted-auth pages stay unframable. 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/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": [] }`. Deploy it (`run402 up` does any missing setup first and then runs this same deploy; `run402 deploy` only deploys): ```bash run402 deploy --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 --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` 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`. - `--print-spec`: advanced SDK-native inspection JSON; this is not a reloadable authoring manifest. - `--print-manifest`: canonical snake_case authoring JSON, backed by `serializeDeployManifest`. Save it in the original manifest directory so relative paths keep their meaning. Reloading supported release inputs preserves selectors, file paths, content types and function configuration. Unsupported streams, dynamic directory references, environment-derived values, app/build resources or embedded secrets fail with `MANIFEST_EXPORT_UNSUPPORTED` and `details.field_paths`, with no partial output. Local success includes `gateway_validated: false`, the app root, nullable target/provenance, file/source evidence counts, local route warnings and deferred gateway policy/quota/cost/secret/migration/content/drift checks. `up` carries this as `result.preflight`; primitive apply includes these fields in its check response. Unresolved intent directs selection before planning. Build output checks are deferred until an approved build. Explicit typed configs execute trusted local code; check/export never executes a build. - `--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 --plan` returns a `deploy --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_id: 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 --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 --dir ./dist --project prj_... # Astro + cross-cutting slices: --dir owns site/functions/routes, --manifest owns the rest run402 deploy --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 releases active --project prj_... --site-limit 5000 run402 deploy releases get rel_... --project prj_... run402 deploy releases diff --from empty --to active --project prj_... --limit 1000 run402 deploy status --project prj_... run402 deploy resolve https://example.com/events --project prj_123 --method GET run402 deploy resolve --url https://example.com/events?utm=x#hero --project prj_123 --method GET run402 deploy resolve --host example.com --path /events --project prj_123 --method GET ``` `list` -> `{ operations, cursor }`; SDK/MCP accept non-null cursor. `status` -> one operation's snapshot (`status`, `release_id`, `urls`, `error`, `rehearsal_report`). `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. `releases 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`), `embedding` (`{ frame_ancestors: [] }` or `null`; absent on an older gateway), 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. `releases 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 resolve` takes a URL (positional or `--url`) OR `--host` + optional `--path`. It outputs `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. | Informational (`severity: "info"`, `requires_confirmation: false`): it never blocks a deploy and needs no `--allow-warning`. Review app auth, CSRF, CORS/`OPTIONS`, and cookies; direct `/functions/v1/:name` remains API-key protected. Only warnings with `requires_confirmation: true` need `--allow-warning `. | | `ROUTE_TARGET_CARRIED_FORWARD` | Carried-forward route still targets a base-release function. | Inspect `run402 deploy releases 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, wallet files, or API keys in GitHub secrets. KISS rule: link once locally, then CI runs the same `run402 deploy` 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 --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` 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 wallet 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 --manifest app.json` for full-stack releases; see the Unified Apply example above. Use `project_id`, `--project`, `RUN402_PROJECT_ID`, or an app-local `.run402/project.json` link. Global active state is never a deploy selector; conflicting selectors fail before mutations. Omitted top-level sections carry forward. Strict adapter: only top-level `$schema` ignored; typo/no-op fields fail before planning. Function specs add 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 }` needs 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 end-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` stages content-addressed asset entries with the release. Activation changes the active release pointer; applied migrations and external side effects are not undone. ```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` 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. Verification-only output reports `mode: "verify"`, `read_only: true`, and `dry_run: false` because it performs real probes without applying a release. Each check: `id` (unique, required), `path` (resolved against the project public origin) or `url`, `expect: { status }` (snake alias `expected_status`), optional `retries`. Each executed HTTP check includes `observed_release` with nullable `release_id` and `generation`, response `url`, `observed_at`, `source: "response_headers"`, and `unavailable_reason`. These are observations from that response, not proof that all routes agree or that a release stayed unchanged throughout the run. Missing or malformed headers remain unknown and do not fail an otherwise successful HTTP check. ```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/` — `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 --manifest app.json ``` Deploy stages migrations, `database.expose`, functions/site/assets, subdomains and routes before activation. Inspect each stage and any resumable operation; activation is not a transaction that rolls back every prior effect. 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 + its live release automatically) run402 subdomains add my-app ``` --- ### Interpreting deploy evidence `up --verify` replaces the current `deploy.edge` summary with the latest probe result and retains the earlier summary in `activation_snapshot`, whose timestamp can be null for an older gateway. Reports disclose path sampling, vantage, check time, and `verification_basis`: release identity, content hash, or inconclusive weak metadata. Identity checks may legitimately have `observed_sha256: null`; a hash is never inferred from HTTP 200. `no_mutable_paths` is explicit. A successful first vault push reports `repo.first_push.snapshot` (ID, kind, backup status) separately from `repo.local_git` (branch, HEAD, unborn, dirty). A synthetic backup does not advance your branch or stage files. An unborn branch with untracked files after backup is expected. Function HTTP 400/429 responses can carry `x-cache: Error from cloudfront`. Judge them against the application’s expected status and body; preserve `Retry-After` for rate limits. That header alone does not establish edge failure. Source-resolution steps report `source_available: true` after resolving an existing directory. `git_state` distinguishes `has_commit`, `unborn`, `not_repository`, and `unavailable`. A null `commit` does not mean the source is missing; an unborn repository has local files but no local commit. Resolution does not create a commit. ## Deploy output `run402 up` returns JSON with the destination, release, verification and backup outcomes, warnings and next actions. Repetitive progress is opt-in with `--json-stream`. Applied runs can include a local `result_ref` pointing to redacted detail in `.run402/diagnostics/`; read the returned file to inspect that same execution. Diagnostics are private, bounded and excluded from source snapshots. Check, plan-only and read-only output does not create diagnostic files. MCP uses its existing `expand_result` handle instead of a local path. SDK scripts retain the full typed result. An initial connection failure before payment dispatch is classified as `network`, preserving `X402_INITIAL_REQUEST_FAILED` and safe retry details. A failure after possible payment dispatch still requires reconciliation. An explicitly declared append-only policy on a new table deploys without a second acknowledgment. Public-write widening on existing tables, changed custom policies and legacy custom-policy convergence still require review; the CLI reports the precise `--allow-warning` flag. An unchanged compiler-applied custom policy does not prompt again. Default results contain one verification summary, release URLs and backup outcome; repeated inventories remain in `result_ref`. ## Command Reference ### up - `run402 up [repo-or-path] [--name ] [--project ] [--manifest ] [--dir ] [--tier ] [-y|--yes] [--check|--print-spec|--print-manifest|--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 the local wallet, requests faucet, checks tier, lists projects. A blocked or unconfirmed faucet is reported in `funding`; next actions carry the server cooldown or balance-check recovery instead of recommending immediate deploy. Identity setup can succeed while funding remains blocked. - `run402 init --api-base ` — configure the active profile to target a Run402 Core/API base. For Core, this does not create a wallet, request faucet funds, or require a Cloud tier. - `run402 init mpp` — set up with MPP (Tempo Moderato testnet). Same steps, different payment rail. - `run402 init lightning` — the Lightning wallet: the same Base wallet (the x402 fallback) plus a budgeted Lightning wallet the platform mints on Run402's own Hub (`POST /agent/v1/lightning-wallet`; custody `run402_hub`, a starter amount funded by the platform). The one-time pairing is stored in `wallet.json` beside the Base key and never printed; `rail` becomes `lightning`, so tier purchases and image generation answer a Lightning challenge first and fall back to x402. The summary carries `lightning: { wallet_id, status, lightning_address, budget_sats, starter_sats, balance_sats, custody, outcome }`; `outcome: "minting"` means rerun in a few seconds, `"unavailable"` means no Hub on this gateway (x402 keeps paying). - `run402 init --switch-rail` — switch the persisted rail; re-running with the same rail is idempotent. ### 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, allowance_usd_micros, held_usd_micros }` object: `on_chain_*` is the wallet's on-chain balance, `allowance_usd_micros` the organization's allowance. The on-chain token tracks the rail (USDC on x402, pathUSD on mpp); the allowance is rail-independent. Remote reads carry `remote_status` entries for tier, billing and projects (`available` or `unavailable`, with safe error code/category). `projects_source` distinguishes server inventory from `local_cache`. Null remote values after failed reads are unknown, not evidence that initialization or funding is needed. ### 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 `{ local_label, source, source_detail, address, server_label, configured, created, rail, faucet_used, path, warnings }`; with no local wallet yet, `configured: false` and an `initialize_wallet` next action. `faucet_used` records faucet use, not pay-readiness. `source` ∈ flag|env|binding|config|default. `warnings` surfaces env-vs-binding conflicts and local-name-vs-server-label drift. - `run402 wallets new [--mpp | --rail ]` — create a new wallet (generates a key); `wallets new default` creates the root wallet without funding it. `{ local_label, address, rail, created: true, next_actions }`. A `lightning` wallet is minted on the platform by `run402 --wallet init lightning`. - `run402 wallets fund` — request testnet funds from the faucet into the active wallet (Base Sepolia USDC, or Tempo pathUSD on the `mpp` rail) and wait briefly for them to land. - `run402 wallets balance` — the active wallet's on-chain balances (`onchain`: Base mainnet, Base Sepolia, Tempo) plus its organization's `allowance_usd_micros` (null with no organization). - `run402 wallets lightning [status|mint|revoke]` — the active wallet's Lightning wallet. `status` reads the platform record plus a best-effort balance and remaining budget over NWC (`{ rail, lightning: { wallet_id, status, custody, lightning_address, budget_sats, starter_sats, has_pairing, balance_sats, budget_remaining_sats } }`); `mint` is `init lightning`'s wallet step alone; `revoke` deletes the sub-wallet on Run402's Hub, forgets the pairing, and returns the rail to x402. The pairing secret is never printed. - `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 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`. ### 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 derived `anon_key`/`service_key`, which come from the platform signing key, never expire, and cannot be revoked one at a time. 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 project migrated, 30 consecutive days of zero legacy-key use, explicit owner 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 ] [--import]` — 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. `--import` additionally writes the minted secret into this machine's local key cache (what deploys and data-plane commands read) — the cold-restart re-key path in one command per kind instead of issue-then-`project-keys import`. A first `--kind anon --import` on a machine with no cached entry still needs a service key first (the same rule as `project-keys import`), and that refusal comes BEFORE the mint, so a show-once secret is never burned on a usage error. - `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 grant key 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 grant key can NEVER satisfy them — otherwise a scoped agent credential could escalate itself into a permanent root. Authenticate with a wallet (SIWX) or a sign-in session (`run402 login`). `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 every project you can reach across all your orgs (`GET /agent/v1/me/projects`) — as your sign-in session after `run402 login`, else as the current wallet — 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