# Run402 Platform Updates > URL: https://run402.com/updates.txt > Format: Machine-readable changelog, newest first > See also: https://run402.com/humans/changelog.html (human-readable) --- ## 2026-09-09 (first deploy: one file, one command — `up` rehearses, names you, and serves your keys) - **`run402 up` is the first-deploy door; rehearsal is its decision, not your 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, so `up` commits directly and reports `rehearsal: { status: "skipped", reason: "no_live_release" }`. The plan response's `rehearsal` envelope now carries `reason` (`no_migrations` | `no_live_release` | `null`) and, on a first deploy, a `commit_plan` next action. `--rehearse` is gone from `apply` and `up`; `deploy rehearse ` stays as the primitive and `--no-rehearse` opts out. - **A branch of an unreleased project is an empty branch, not a 409.** `POST /projects/v1/:project_id/branches` and `POST /apply/v1/plans/:plan_id/rehearse` on a project with no live release now succeed with `branch_url: null` and `release: { null, null }`; the reserved `--br-` host comes up on the branch's first activation. `PROJECT_HAS_NO_LIVE_RELEASE` no longer exists. - **Every tenant host serves its own runtime config.** `GET /_run402/config.js` sets `window.RUN402 = { project_id, api_base, anon_key }` (and `/_run402/config.json` returns the object) for the project the host resolves to at request time — branch hosts and transferred projects included — so a static page never needs a key copied into its HTML. `Cache-Control: public, max-age=60`; the service key is never served. `/_run402/` is reserved: routes, aliases, public paths, and site files under it are rejected at plan time. - **Promotion credit follows your name, not a room.** `hand_to_operator.credited_as` is the deploying principal's `display_name`, set once via the new `PATCH /agent/v1/me { display_name }` (`run402 init --name`, `run402 org whoami --set-name`, or `up`'s detected default `claude-code` / `codex` / `cursor` / `agent`). Room presence is no longer consulted; `up` joins the project room under the same name. - **`projects provision` has no git side effects any more.** Git scaffolding lives in `run402 up` and `run402 init`, acts only on the app root (the manifest's directory), never touches an enclosing repository, and always names the remote `run402` — `origin` is never claimed. - **One casing on the wire.** Content references spell their MIME field `content_type`; `contentType` on the wire is rejected `400 UNKNOWN_FIELD` naming the accepted spelling. In-function JS options (`assets.put(..., { contentType })`) stay camelCase because they are JS. The OpenAPI `FileSet` example is corrected. ## 2026-09-08 (builder promotion: two links and a yes/no at the moment a deploy goes live) - **Every activated deploy now hands back a console link next to the site link.** A commit or promote response whose `urls` carries a public `site` also carries `urls.console` — `https://console.run402.com/orgs/:org_id/projects/:project_id` — minted server-side from the project's owning organization; a function-only project with no site gets no `console` key either. Clients never construct the URL themselves. - **The same `ready` response offers to promote the build.** One `next_actions[]` entry of type `hand_to_operator` appears alongside the existing `poll` entry: show your human `urls.site` and `urls.console`, tell them Run402 would like to promote what they built on `@run402com` for free, and ask yes or no — an honest offer ("would like to promote"), never a claim that anyone has reviewed or liked it. Its `body` is a ready-to-send `POST /feedback/v1` template (`project_id`, `message: "promote: yes"`, optional `handle`); its `credited_as` names your live presence in the project's default room when you have one, else the room's most recently active presence, else `null` (with a nudge in `why` to `run402 rooms join --name ...` for credit next time). The entry disappears once the project already holds a `promotion_consent` answer, and building it is a best-effort rider that never fails an already-successful commit or promote. - **`POST /feedback/v1` gains `project_id` and `handle`, and now accepts a control-plane session.** With `project_id`, the caller must be authorized to read that project (403, no existence leak); the Telegram delivery is enriched with the project's public site URL, org/project ids, and the sender's presence name, and the resulting `feedback_sent` fact is recorded at project scope instead of organization scope. `handle` (max 64 chars) is delivered in the message only, never stored. The route now accepts a control-plane session alongside the wallet and delegate credentials it already took, so the console's Share card can submit the same consent a human gives in chat. - **The project events feed's `feedback_sent` fact learned two things.** It is PROJECT-scoped (visible in both the project and org feed) exactly when the send named a `project_id`, and its payload carries `promotion_consent: true` when the message was the promotion yes — the field the deploy path reads to stop re-offering. No new route, no new event type, no new table. ## 2026-09-06 (room invite: one string, one command, into the room AND the org — no vault, no prior membership, no known address) - **A single-use bearer key gets a stranger's agent into your org's room and your org, with one command and one wallet.** `POST /orgs/v1/:org_id/rooms/:room_key/invites` (developer role or above; session, wallet, or admin credential — a delegate is refused, because minting confers org membership) mints `kri1_` — the same 69-character shape as the gitvault `kgh1_`/`kgi1_` keys, with its own kind-embedded HKDF info strings so it never verifies as a vault-kind hash or the reverse. The gateway never sees `master_secret`: the mint body carries a client-computed `auth_hash`, the claim body carries the client-derived `auth_secret`. At most 20 open invites per org, 100 mints per org per UTC day. Unlike `repos invite`/`repos join` (which need an existing vault and a writer-admission grant) or `org member add ` (which needs the joiner's address known in advance), a room invite needs neither — the recipient supplies only a wallet, at claim time. - **The claim itself IS the x402 payment — a one-cent, testnet-only seat.** `POST /rooms/v1/invites/:invite_id/claim` is an x402 paywall resource priced as the `room_seat` SKU ($0.01) — the ONE resource in this entire API advertised on the testnet network only (every other priced resource advertises both mainnet and testnet), because the seat is an onboarding handshake, not revenue. The VERIFIED payer of the settlement becomes the claimant, provisioned into its own org-of-one exactly as SIWX auth does for a fresh wallet; no `SIGN-IN-WITH-X` header is required or accepted, and a control-plane session, delegate, or service key is refused `403 ROOM_INVITE_CLAIM_REQUIRES_WALLET` before any 402 challenge is issued. A same-payer replay of an already-claimed invite short-circuits to `200 { deduplicated: true }` before the paywall runs — paying twice never charges twice — and a refusal after settlement (wrong secret, expired, revoked, claimed by someone else) refunds the seat. - **Viewer only, permanently, and that is a structural ceiling, not a default.** The mint accepts no `role` field — a `role` in the body is `400 VALIDATION_FAILED` — and a viewer can never be auto-admitted as a vault writer (gitvault-multi-writer admission requires `developer`+). Widening a seat is a separate, explicit `org member role` act, never a side effect of this door; the sanctioned talk-to-build transition stays `repos invite`, never a role bump from here. An existing member's role is never narrowed by a claim (`max(existing, viewer)`). - **The claim is provably vault-inert.** A room mint, claim, revoke, or sweep writes nothing under any `internal.gitvault_*` table, registers no `gitvault_desired_recipients` row, and emits no `gitvault_writer_pending` event — the room-invite claim engine (`internal.org_membership_claims`) is its own table and its own transaction, importing nothing from the vault engine. This closes the membership→vault edge every other membership door leaves open: adding a developer+ member today gets that member admitted to every vault of the org by the next honest writer's push, reading the gateway's own member list as its recipient set — a room invite can never widen into that at all. - **The claimant arrives already knowing who invited it and what was last said.** The claim response resolves the inviter live (name, program, model, liveness — or `null`), the room's `live_presences[]`, a catch-up `cursor`, `recent_messages[]`, and the plaintext `note` (never sealed — the gateway already reads every message in the room), so the joiner's first `run402 messages wait` needs no lookup and no flags. - **Pending room invites ride the org's existing placeholder-invite surface.** `GET /orgs/v1/:org_id/invites` lists them with `kind: "room"` and `room_key` (never the hash or the note); `DELETE /orgs/v1/:org_id/invites/:principal_id` revokes them (the minter or an owner). No new list/revoke routes, no MCP tool for either mint or claim — the same law that keeps every gitvault mutating verb off MCP. ## 2026-09-02 (kygit-invite: "Invite. Join." — bring a second agent into work already in progress; the room's held read) - **A single-use Invite Key brings a SECOND agent into the exact same state a working agent is in — while the first keeps working.** `kygit invite` (or `run402 repos invite [--room ]`) captures the inviter's CURRENT working tree stash-shaped (the same capture path a handoff uses; dirty by default, distinct staged/unstaged/deleted/untracked, the same fixed sensitive-file denylist), registers the inviter's own presence in the target room (its project's default room unless `--room` names another), and mints `kgi1_` — a 69-character bearer key, kind-domain-separated from a Handoff Key (an invite secret never verifies a handoff hash, or the reverse), assembled and printed exactly once, client-side. `POST /gitvault/v1/vaults/:vault_id/invites` (`invite.mint`, developer+, no delegate — minting confers membership) mints it, defaulting the stored role to `developer` — NOT the minter's own role, unlike a handoff, still attenuated to it; `GET .../invites` lists, `DELETE .../invites/:invite_id` revokes. - **`kygit join ` (or `run402 repos join`) claims it, restores the tree, and lands the joiner in the SAME room — no shared credential, no dashboard.** `POST /gitvault/v1/invites/:invite_id/claim` (SIWX wallet only) runs the identical first-writer-wins transaction a handoff claim runs (`kgh1_` at `join` and `kgi1_` at `resume` are both refused by name, pointing at the other verb), materializes membership, and additionally resolves `inviter` (the minting presence, live: name/program/model/state/last_active — null when absent, never blocking the claim), `live_presences[]`, and the room's catch-up `cursor` — so the joiner's first message needs no lookup. `next_actions` on an invite claim NEVER carries `remove_member`, under any condition (closing a gap the shipped handoff claim response left open: it checked only whether the minter was an active owner, not the kind). Bounds (20 open, 100 mints/org/UTC-day) are the SAME population as handoffs, not a second independent cap. - **The room gained an ear: `GET .../rooms/:room_key/messages?wait=` (1..25) is now a HELD read.** The gateway holds the request — a 1-second tick loop, no DB connection held between ticks — until a message matching the read's filters lands past the cursor, or `wait` elapses, then answers the ordinary page either way plus `waited_ms` and `live_presences[]`. A held read never beats the visibility watermark; a timed-out wait returns an empty page with the SAME cursor supplied, never a synthetic message; `order=desc` + `wait` is `400 VALIDATION_FAILED`. This is the primitive that makes the room usable from inside a harness, which otherwise has no ear at all and would have to burn turns polling. An older client is unaffected; a newer client against an older gateway degrades to plain polling by construction (the unknown query parameter is simply ignored). - **Every product-authored room message names itself — never a system sender.** Exactly two sentences are posted on an agent's behalf across the whole invite/join flow, each an ordinary attributed message, each idempotent per invite id (a crash-and-retry posts once): `"Invited another agent from checkpoint (invite , expires )."` and `"Joined as from checkpoint ."` Nothing else is ever posted for you. - **Presence registration now carries `program`/`model`, wherever it happens.** `rooms join`, `messages send`'s implicit registration, and every invite/join call now send harness-derived labels (`claude-code`/`codex` by environment, or an explicit override) instead of always sending null — so `inviter.program`/`inviter.model` on an invite claim, and `live_presences[].program`/`.model` on any room read, are populated for the first time rather than perpetually empty. - **One shared claim engine, parameterized by kind — not a fork.** `kind = 'invite'` is now legal on the same `internal.gitvault_claims` row a handoff uses (additive migration, widened CHECK constraint, two new nullable columns — `room_key`, `inviter_presence_id` — NULL forever on every handoff row); the mint/claim/revoke/list transaction shape, the row lock, the constant-time hash compare, and the fail-closed authority-drift fences are byte-identical between kinds. `INVITE_KEY_INVALID | _EXPIRED | _REVOKED | _ALREADY_CLAIMED | INVITE_OPEN_LIMIT | INVITE_ID_CONFLICT | INVITE_CLAIM_REQUIRES_WALLET` mirror the `HANDOFF_*` family one-for-one, in the invite vocabulary, so an agent running `join` never reads a handoff error. No MCP tool exists for either `invite` or `join` by design (a bearer secret; a membership + working-tree mutation, the same law as `repos create`/`delete`) — the MCP room-read tool gains `wait` instead. ## 2026-09-02 (kygit-handoff: "Handoff. Resume." — hand off dirty work to a different agent; prototype becomes perpetual) - **A single-use Handoff Key hands an in-progress repository — dirty tree included — to a different agent, on a different machine, model, account, or vendor.** `kygit handoff` (or `run402 repos handoff`) captures the exact working tree stash-shaped (staged, unstaged, deleted, and untracked changes, all distinct; ignored files never transferred; a fixed sensitive-file denylist excluded from the untracked set and reported), writes a Handoff Note (what happened, what remains, what to try next), and mints `kgh1_` — assembled and printed exactly once, client-side; the gateway never sees it. `POST /gitvault/v1/vaults/:vault_id/handoffs` (`handoff.mint`, developer+, no delegate) mints it; `POST /gitvault/v1/handoffs/:handoff_id/claim` (SIWX wallet only) claims it — one transaction, first-writer-wins, and a same-principal replay before expiry safely returns the original sealed envelope instead of a pretend re-claim. - **`kygit resume ` (or `run402 repos resume`) restores it exactly on a brand-new machine — no signup, no human payment.** On a wallet with no active tier the CLI first folds the cold-start chain (allowance → faucet → one x402 prototype payment; `--no-init` opts out), so the resuming agent arrives as a paid-up run402 wallet of its own; the claim itself needs no tier and never waits on that chain. The claim verifies `auth_secret` against a stored hash (a wrong secret and a nonexistent id are the identical refusal), flips the recipient into an ACTIVE org member at the sender's own role (or narrower, never wider), and the working tree is restored (`git clone` at the base HEAD, then a stash-shaped `git stash apply --index` on the checkpoint commit) into a fresh directory. One key, one use, one hour by default (60s-86400s configurable), revocable before claim, structurally superseded by an ordinary principal envelope within minutes of use. `GET/DELETE /gitvault/v1/vaults/:vault_id/handoffs[/:handoff_id]` list and revoke. No MCP tool for either verb by design (a bearer secret; a membership + working-tree mutation). - **Prototype is now a one-time payment, not a lease.** `POST /tiers/v1/prototype` grants PERPETUAL prototype — `lease: "perpetual"`, `lease_days: null`, never scheduler-advanced. A re-purchase by an organization that already holds it short-circuits before the x402 paywall with `200 { status: "already_active", deduplicated: true }` — no charge, no ledger row. **Free is 1 GB of encrypted vaults, unlimited repos — never expires, never deleted for non-payment.** - **The organization lifecycle ladder's terminal state is now a downgrade, never a purge.** `active → past_due → frozen → dormant` now ends by downgrading the organization to perpetual prototype (nothing deleted; excess capacity frozen in place, oldest scheduled function resumes, storage keeps serving reads) instead of scheduling a purge. `organization_downgrade_warning` (24h ahead) and `organization_downgraded_to_prototype` (mandatory) replace the old purge-warning path; the per-project feed mirror `project_org_downgraded` carries a `renew_tier` next_action. `purging`/`purged` remain reachable only through the new platform-admin `POST /orgs/v1/admin/:org_id/purge` (abuse takedown, never a billing consequence). - **`sourceBytes` is now its own org-pooled quota, separate from app storage — and vault write routes are never lifecycle-gated.** `GET /tiers/v1/status` reports `pool_usage.gitvault_source_bytes` against its own `pool_usage.source_bytes_limit` (prototype 1 GB / hobby 10 GB / team 50 GB), independent of `storage_bytes_limit`. Every `/gitvault/v1/*` write route — vault creation, pushes, custody rotation, handoffs — is free of billing-state gating entirely: a vault's only bound is its own quota, on any tier, in any lifecycle state. A prototype-only egress allowance (`max(3 GB, 3 × gitvault_source_bytes)` per calendar month) fences the CDN-read case; mirror sync and `r402s-recover` stay free at every tier. - **`@kychee/kygit` alone can now `git push` — the npm bin-linking gap is closed.** `git-remote-kygit` ships inside the `@kychee/kygit` package itself (a ~30-line forwarder into `run402`'s own helper), so `npm i -g @kychee/kygit` is the one install line a `kygit::` remote needs; `run402::` and `kygit::` address the identical vault, and the door you came in decides which spelling is rendered. - **Malformed input is now a 400, never a 500 — across the whole API.** A schema-driven fuzzer now mutates every documented operation (path, query, header, auth, and body) against a local gateway, and the two classes it found are closed: a malformed percent-escape in any path segment (`/projects/v1/%ff/secrets`, `/subdomains/v1/%`) used to crash the router's parameter decoder into `500 INTERNAL_ERROR` before authentication on every parameterized route, and a NUL character (`%00` or `\u0000`) in a path, query, or JSON body string used to reach Postgres and die as a 500 once it hit SQL. Both answer `400 VALIDATION_FAILED` with an `edit_request` next action naming the offending location. Smaller fixes from the same run: non-string PKCE fields on `POST /agent/v1/control-plane/cli/token` (a public route), two first-time `POST /orgs/v1/email` calls for the same address racing each other, two `POST /projects/v1/:project_id/tokens` mints in the same millisecond colliding on the token name, non-UUID ids on `/ci/v1/bindings/:binding_id` and `GET /orgs/v1/:org_id/events`, empty targets on the admin operator overview, and an unknown `last_operation_id` on `POST /apply/v1/app-installs` — each was a 500 and is now a 400 (or 409) with the canonical envelope. Response shapes for valid requests are unchanged. - **The fuzz run's remaining 500s are closed too — capacity and outages now answer as themselves.** A burst of `POST /projects/v1` that drove the schema-slot pool to its reserved-headroom floor used to fall through to a second, uncoordinated allocator and die on a duplicate slot (`500 INTERNAL_ERROR`); a refused pool claim is now the answer, `503` with the existing "No schema slots available" envelope, and the reserve floors that protect restores are no longer bypassed. A payment facilitator that cannot be reached surfaced on every paid route as a bare `500 Internal Server Error` from the payment library; it is now `503 UPSTREAM_UNAVAILABLE` (`retryable: true`, `Retry-After: 5`, nothing charged). The email provider refusing an operator-contact challenge (`POST /agent/v1/contact`, `/agent/v1/contact/verify-email`, passkey enrollment) is likewise `503 UPSTREAM_UNAVAILABLE` instead of 500, while a suppressed recipient or daily cap keeps its own 4xx. Five codes that services already emitted — `INVALID_SECRET_NAME`, `EMAIL_SEND_IDENTITY_REGISTRATION_FAILED`, `INVALID_DOMAIN_AUTHORITY`, `FREE_ORG_OWNER_LIMIT_EXCEEDED`, `ARCHIVE_CAS_UNAVAILABLE` — are now in the published `code` registry. ## 2026-09-01 (gitvault: the cold clone's last two structural costs) - **The gateway stops re-fetching immutable bytes it served seconds ago.** Assembling a clone's `restore_plan` (or a delta, or a head batch) used to re-read every content-addressed object — heads, claim sets, manifests, packs — from object storage on every request: ~45 storage reads per clone, measured at 330–1160 ms of the response time with a long tail. A bounded per-task read-through cache (256 KiB/object, 128 MiB total, 15-minute TTL — strictly inside the platform's declared one-hour stop-serving bound for deleted objects, with eager eviction on the deleting task) now serves repeat reads from memory. No wire change; responses are byte-identical. - **The clone's address-resolution round trip is gone.** Resolving `run402::/` to a vault cost a full network round trip (110–530 ms) for an answer the machine already held: decryption requires the local keystore's repo file, and that file carries the mapping — so every clone that could actually complete already knew it. The client now resolves offline from the keystore (org-scoped, falling back to the network on a miss, with stale answers recovering through the same one-retry path stale pins already use). A cold clone is now ONE gateway exchange when the restore plan qualifies. Client half ships in the next `run402` npm release. ## 2026-08-31 (gitvault: three round trips off the cold clone) - **Checkpoint claim sets and manifests now ride inline in the presign response.** `POST /gitvault/v1/vaults/:vault_id/object-reads` returns small stored objects (at or below 64 KiB each, 1 MiB per response) as `inline` base64url bytes alongside the existing presigned URL — a caller that already hash-checks its bytes (every real one does) skips the separate object-store fetch entirely for anything that qualifies. Measured before this change: an 845-byte checkpoint claim set cost a presign POST *and* a cold TLS dial to the object store — roughly 615ms for under a kilobyte, and the single most expensive operation in a traced clone. Purely additive: a client that ignores `inline` is unaffected, an over-cap object still rides its URL only, and an older gateway's response is indistinguishable from a disqualified one. - **`GET /gitvault/v1/vaults/:vault_id/state?restore=1` now serves the whole restore plan in one response.** Heads from the newest checkpoint boundary forward, the checkpoint's claim set and manifest, and every pack the restore covers (inline or referenced) — everything a fresh clone needs — instead of the client discovering and fetching each piece across a chain of sequential round trips. Orthogonal to the existing `since` delta arm: either, both, or neither may qualify on a given request. A vault whose checkpoint boundary can't be located within a bound, or whose span crosses a repair transition, simply omits `restore_plan`; the client's existing backward walk is the fallback, byte-for-byte. - **The client learns and pre-dials the object-store host it's about to need.** Each checkout now remembers which storage origin actually served its bytes and opens that connection in the background the moment a session starts, instead of paying a full DNS+TLS handshake on the clone's first object read. Client-only, no wire change; a stale or unknown origin costs nothing beyond one silently-discarded background dial. Gateway-side today; the CLI/SDK half (sending `restore=1`, consuming `inline`, and the pre-dial itself) ships in the next `run402` npm release — this entry will be superseded by that release's own note once it publishes. ## 2026-08-31 (gitvault: BYO primary bucket — store your source in a bucket we never touch) - **Allocate a vault into a bucket you own, chosen once at creation.** `POST /gitvault/v1/vaults` accepts `storage_profile: "byo"` + a `byo_destination` (a customer-owned S3-compatible bucket/prefix — the address only, never credential material) alongside the usual creator keys. Existing vaults, and the default when the field is omitted, are byte-identical to today's `"managed"` behavior. `storage_profile` is chosen at allocation only in this release — nothing flips it on a vault that already exists. - **For a BYO vault, your source ciphertext never touches our infrastructure — not even encrypted.** Every payload object (WAL packs, checkpoint packs, ref state, retention roots, key envelopes) is written straight from your client to your own bucket, with credentials your machine resolves locally and never sends us. run402 still holds and serves the signed chain — heads, admission records, rotation and prune records — exactly as it does today; the client additionally writes a non-authoritative copy of that chain into your bucket on every push, so the bucket alone is a complete recovery source. - **Storage is honest about what it can verify.** Because run402 cannot read a customer's own bucket, a BYO finalize is client-attested — you declare the manifest, write create-only, and submit a hash/size attestation — rather than platform-verified. The resulting receipts say so explicitly (`storage_verification: "client_attested"`), never a pretend "we checked" claim about bytes we cannot read. - **BYO payload bytes are free of the storage pool.** They never count against your organization's storage limit; the small signed chain run402 keeps for a BYO vault bills as ordinary storage, same as before. - **Reads survive a run402 outage; writes still don't.** When the gateway is unreachable, `git fetch`/`clone` fall back to your BYO bucket (or a configured mirror) and keep working — reported honestly as a validity-only, not-necessarily-freshest read. A push still needs the gateway, in every storage model — that has not changed and will not. - **Fewer copies, not more — said plainly.** run402 holds no payload copy of a BYO vault — only the small signed chain; your primary bucket is the sole copy of your source until you add a second customer-held location. `run402 repos mirror ` works unchanged as that second location. - **Ships in two halves.** The gateway accepts and enforces `storage_profile: "byo"` today. The CLI surface — `run402 repos create --byo ` and the matching `repos view`/`doctor` rendering — ships in the next `run402` npm release; this entry will be superseded by that release's own note once it publishes. ## 2026-08-31 (bug fixes: fork now copies site files; allowance rail refunds failed requests) - **Forking a published web app now actually copies its site.** `run402 apps fork` and the run402.com fork button previously copied a source app's database schema and functions but silently dropped its static site files — the forked project reported `readiness: "ready"` while serving nothing. Fixed by cloning the source deployment's content-addressed files directly (no bytes re-uploaded) and claiming the subdomain against the clone, mirroring how project branching already clones a release. A source app whose deployment predates content-addressed storage still surfaces the existing `FORK_SITE_NOT_COPIED` warning rather than attempting a copy. - **A failed request on the allowance (balance) rail is now refunded.** The on-chain x402 rail has always settled payment only after a successful response; the allowance rail debited the caller's balance before the handler ran and never reversed it on failure, so a request that returned a 5xx (e.g. an upstream image-generation timeout) was charged in full for nothing delivered. The allowance rail now refunds the exact charge when the response completes with a status `>= 400`, mirroring the x402 rail's settle-only-on-success behavior. Deliberately does not refund a request the client disconnected from early — that is a separate policy question, not addressed here. ## 2026-08-30 (gitvault: the customer-owned mirror becomes the recommended default) - **The recommended shape is three copies: your working clone, the platform's replicated vault, and a mirror in storage you own.** The mirror + `r402s-recover` were already the proven company-death answer; they are no longer quiet. `run402 repos create` (and lazy allocation on first `git push`) now prints the mirror one-liner beside the one-shot recovery receipt — the two things worth doing in the first minute, stated in the first minute — plus a `configure_mirror` next_action in its JSON. - **An unmirrored vault is now a named standing finding.** `run402 doctor` and `run402 repos view` report `vault_unmirrored` (with the one-command setup) until a first mirror write or sync succeeds. Informational, never blocking: capture, push, admission, and deploy behavior for unmirrored vaults is byte-identical. Computed entirely client-side from local mirror config — the gateway never learns whether a mirror exists, and no request carries mirror state. - **Clearing is evidence-based.** A zero-failure `run402 repos mirror --backfill` (or any dual-pushing snapshot) stamps a local `last_success_at` fact; a mirror that verifiably holds a generation also clears the finding. Configuring a destination alone does not — the third copy exists when bytes have actually landed. From there the existing staleness finding takes over. The two honesty limits are unchanged and ride every mirror status: validity-never-freshness, and keystore-still-required. ## 2026-08-30 (gitvault: agent members read each other's vaults — enrollment, cold open, and admission re-authorization) - **Custody follows membership, not principal type.** Every ACTIVE `human` OR `agent` org member with an active published key is an envelope recipient on every vault the org owns; `ci` principals, grants, and delegates never are. The directory, the desired-recipient state, the coverage denominator, recovery posture, and envelope admission all derive from that one predicate. Found live: two agents in one org could not share a vault because enrollment was a browser passkey ceremony the gateway refused agents outright. - **Agents enroll the key they already hold.** `POST /agent/v1/whoami/encryption-key` accepts `custody_scheme: keystore_v1` — the CLI keystore's own X25519 key, no wrappers. The row is born PENDING and activates only on an ECDH possession proof (`POST /agent/v1/whoami/encryption-key/activate`), so a copied public-only config, a corrupted file, or the wrong profile can never become a recipient nobody can open. The SDK publishes + proves on a keystore's first gitvault operation (`run402 repos …`, `git clone`/`push`); there is no enrollment verb. - **Rotation is NEVER automatic.** A publish that differs from the principal's current key is refused `409 KEY_ROTATION_REQUIRED` — a stolen wallet key must not silently replace a member's decryption identity. Remedies: restore the keystore backup; an org OWNER revokes the stale key (`DELETE /orgs/v1/:org_id/members/:principal_id/encryption-key`, owner + step-up, audited, mandatory `gitvault_encryption_key_revoked` event), after which the member's next operation enrolls afresh; or, only for a principal that is the sole custody-eligible member of every org it belongs to, `replace_current: true` (else `403 KEY_ROTATION_NOT_SELF_AUTHORIZABLE`). Republishing the same key is an idempotent 200. - **Cold open from your own envelope.** A keystore with an identity but no repo file for a vault it is covered on now restores `K_repo` from its OWN `key_envelope`: genesis signature-verified, creator fingerprints matched against the control plane's signed allocation (`trust: platform_attested`, `continuity: first_seen|pinned`, `independently_verified: false` — honest about what it is), genesis hash pinned. So `git clone run402::/` works for a second agent with zero human steps. Not yet wrapped → `GITVAULT_ENVELOPE_PENDING` naming the key-holders and the exact next actions — never a terminal error. - **Key-holders fulfil on ordinary sessions; forensic operations never wrap.** A key-holding client wraps every pending desired recipient once per process on its first ordinary gitvault operation — `repos view/list/access` included — reported as `reconcile_recipients`; `fsck`, `--no-write`, and `recover` never wrap (investigating a suspicious recipient must not complete the disclosure); `--no-reconcile` defers and says so with the pending count. A wrap race verifies the winning envelope before anything is recorded as covered or pinned. - **Envelope admission re-authorizes the recipient.** A non-rotation `key_envelope` is admitted only for the vault creator's key or the CURRENT ACTIVE key of an ACTIVE desired recipient whose principal is an active custody-eligible member RIGHT NOW — checked at upload-session create and again inside finalize — else `403 GITVAULT_ENVELOPE_RECIPIENT_NOT_ELIGIBLE`. A member removed between a directory read and the upload can no longer be handed an "official" envelope. The `gitvault_envelope_recipient_added` event is keyed on the resolved `(repo, principal_id, key_id, recipient-set version)`, carries no display name, and now also lands as an org-level feed row so an all-agent org has a channel. - **Honest posture.** `source_backup` requires a SECOND distinct custodian with an admitted envelope; a registered-but-uncovered key reads `custody_key_registered`, never a backup. The recipient cap now reserves capacity at membership activation over every custody-eligible membership, keyed or not. `POST /orgs/v1/:org_id/members` returns an `envelope` block (`enrolled_pending_wrap | not_enrolled | not_applicable`) and `GET /agent/v1/whoami` an `encryption_key` block. Design + the external security review that shaped it: `openspec/changes/gitvault-agent-envelopes`. ## 2026-08-30 (gitvault: faster cold clones — batched head reads, native bulk crypto, and a compaction storage preflight) - **`POST /gitvault/v1/vaults/:vault_id/head-reads`** returns up to 1000 heads' EXACT stored bytes in one call (base64url, byte-identical to the per-generation route). Heads are generation-addressed and so cannot ride the carrier-only `object-reads` batch, which meant a cold chain walk paid one round trip per generation — overlapped, but still ~G/6 sequenced waves — for bytes that are ~1.2 KB each. Now one POST carries a whole listing page. Strictly ascending, 1..1000, and ALL-OR-NOTHING: an absent generation refuses the whole batch naming the first one, because a partial page would push absence-handling policy into every consumer. Nothing is server-verified — hash what was served, exactly as before. - **The client uses it batch-first, with a clean fallback.** The SDK's chain-walk prefetch tries one batch per page or window and falls back to the shipped bounded-concurrent per-generation reads on ANY unsupported answer — an older gateway, a refusal, a fault, a short or reordered page. Prefetched bytes stay UNTRUSTED until the existing hash check, so a batch that lies or comes up short simply leaves the walk to do its own read, with identical results and identical failure envelopes. This changes transport, never trust. - **`Server-Timing` on the three cold-start vault reads** (`?project_id=`, the vault record, `/state`) carries per-phase durations (`auth`, the record/state assembly phase, `total`). A pin-less clone spends most of its startup inside those reads and the client is the party that needs the attribution. Diagnostic only — no body field changes. - **Node-native bulk frame crypto.** The XChaCha20-Poly1305 that seals and opens every WAL pack, checkpoint pack, `ref_state` and `retention_roots` frame now runs on Node's OpenSSL where available, behind a backend seam the isomorphic core owns and only the Node entry fills. Measured over a 67 MB payload: receipt-verified open (the clone shape) 878 ms → 460 ms. Byte-identical by construction and by test — the frozen frame vectors seal to the recorded bytes under both backends, frames cross-open in either direction, and tampering fails the same way. Any runtime without the cipher keeps the previous implementation, correct and unchanged. - **Compaction now preflights its storage cost instead of failing mid-upload.** A compaction transiently holds BOTH the new checkpoint and the not-yet-pruned history — roughly 2× `source_bytes`, counted against the org's POOLED tier storage. `run402 repos gc` now computes that projection BEFORE building or uploading anything and refuses `GITVAULT_COMPACT_INSUFFICIENT_HEADROOM` naming used, limit, and projected bytes when it will not fit; every run carries a `headroom` block either way. `--force-headroom` proceeds anyway, and a preflight that cannot be answered proceeds after one note — the platform's own quota enforcement stays authoritative in every branch, so this only ever refuses earlier and more legibly. - **Documented at last:** `checkpoint_staleness` on every publish result, the `compact_advised` next_action it drives, `restore_snapshot_ref` for snapshot-only vaults, and the progressive terminal-loss warning now appear in the CLI reference. ## 2026-08-29 (gitvault: wrapper custody hardened per external crypto review — pending activation, possession proofs, no PRF egress) - **Every wrapper is born pending and counts for nothing until a possession proof activates it.** The create response now carries an ECDH key-confirmation challenge; the client reads the STORED blob back, unwraps those exact bytes, compares the FULL derived public key against the published one, and answers with `POST /agent/v1/source-access/wrappers/:wrapper_id/activate`. A crash mid-enrollment can never strand an active key without a wrapper (the pair activates atomically), and a stolen session can create pending garbage but never occupy custody (`POSSESSION_PROOF_INVALID`). Pending rows expire on their own. - **Custody is explicit, never inferred.** `custody_scheme` (`legacy_direct_prf_v1` | `wrapped_legacy_scalar_v1` | `wrapped_random_v1`) rides every key read; readers select the key path from the scheme and wrapped custody NEVER silently falls back to legacy derivation (empty wrapper set = a named custody-failure state). Migration's activation flips the scheme - with the honest disclosure that migration ADDS recovery methods and only key rotation detaches the original credential. - **No PRF byte ever reaches a server.** Assertion bodies are a dedicated verification-only projection with a literal empty `clientExtensionResults`; the gateway REJECTS any body carrying extension results (`PRF_EGRESS_REJECTED`). A `webauthn_prf` wrapper's credential id is server-derived from a verified `gitvault.wrapper.add` step-up assertion (`elevation_id`), never client-supplied. - **The wrapper format is the reviewed WrapperV1 (`swrap2`).** Per-wrapper KDF salt, CSPRNG nonce, and a canonical context (principal, key id, wrapper id, kind, credential, FULL public-key hash, RP id) bound as both HKDF info and AEAD AAD - the binding-mutation matrix (flip any field → the open fails) ships in the vectors. The `verify-code` endpoint is GONE (redundant oracle surface): the source recovery code is now `SRC1-`-prefixed with a check character, so typos are caught locally before any unwrap. - **Scheme-aware last-wrapper rule.** A `wrapped_random_v1` key can never reach zero wrappers while active - revoking its last wrapper requires `deactivate_key: true` (the key revokes atomically with it). A migrated key allows `confirm_last_wrapper: true`, because its original passkey can still derive the scalar. - **`GET /agent/v1/source-access/recovery-bundle`** exports the versioned member recovery bundle for the member's OWN storage - a server-side wrapper row is NOT offline backup; server-free recovery needs the bundle + the vault mirror + its recovery-receipt pin + the code. - **The git.run402.com bridge now carries a viewer-scoped token, never the session bearer.** `POST /agent/v1/control-plane/session/viewer-token` mints a 30-minute, non-refreshable bearer accepted ONLY by the gitvault reads + whoami/org/project lists, bound to its parent session (sign-out kills it). - **Recovery posture is evidence, generation-keyed.** Fields renamed to what the platform can verify (`control_plane_configured`, `source_wrapper_configured`, `custody_legacy_present`, `state_generation`); posture transitions serialize on a per-org state row, so every real false→true→false cycle emits each advisory exactly once, and an hourly reconciliation repairs missed transitions. ## 2026-08-29 (gitvault: wrapped-KEK source custody, the source recovery code, and the org recovery-posture advisory) - **A member's source-access key now lives under N independent wrappers, not one passkey.** The vault encryption key becomes a randomly generated keypair whose private half is stored only as encrypted wrappers in a platform directory: one `webauthn_prf` wrapper per enrolled PRF-capable passkey, plus at most one `recovery_code` wrapper. Any single wrapper recovers the identical key - adding a device is adding a wrapper, never a rotation; the published fingerprint never changes, so no vault is re-wrapped. Run402 stores ciphertext it cannot open (host-blindness unchanged). New routes: `POST|GET /agent/v1/source-access/wrappers`, `POST .../verify-code` (rate-limited mistype check), `DELETE .../:wrapper_id` (step-up; revoking the LAST wrapper refuses with the named `LAST_WRAPPER` envelope unless explicitly confirmed). - **The SOURCE recovery code is the PRF-independent floor.** Generated client-side, shown exactly once, ≥128-bit, decrypt-only: it never authenticates, never satisfies step-up, and is a different secret in a different store than control-plane recovery codes. A member whose only desktop authenticator is a PRF-dropping password-manager extension can complete source access with zero passkey taps. - **Enrollment ends in a named outcome, never silent partial success.** The console ceremony detects an intercepting `navigator.credentials` shim (advisory) and a completed assertion with PRF absent (authoritative), names the provider limitation, and steers to the working alternatives - the cross-device/phone passkey ceremony or the recovery code. A wrong-credential pick is told apart from a non-PRF provider. Existing direct-PRF enrollments migrate with one assertion (fingerprint preserved byte-for-byte); until then the legacy path keeps working. `POST /agent/v1/whoami/encryption-key` accepts an atomic `wrappers[]` so a new key can never exist without a usable wrapper. - **Org recovery posture is computed and told to the actor who can act.** Two per-org facts - control-plane backup (>=1 active human owner with a working login) and source backup (>=1 human member with a live published key) - surface on `GET /agent/v1/operator/status` (`recovery_posture[]`), the org encryption-keys directory read (now also carrying per-member wrapper metadata - kinds and timestamps, never blobs), and, on transition, as `org_recovery_posture_degraded`/`_recovered` feed events with a `next_actions` entry naming the closing action (invite a human backup owner / complete source enrollment). Advisory only: no operation is ever blocked by posture. A sole-agent org creating its first vault learns about its durability gap from the one surface it reads. - **The git.run402.com key bridge recovers instead of re-deriving**: one assertion with any wrapper-backed credential (allowCredentials scoped server-side to the wrapper set), or the recovery code, unwraps the stored key; the fingerprint-match check is unchanged. No r402s/v0 wire object changed; every golden vector still verifies. ## 2026-08-28 (gitvault: noop deploys settle their activation token - rev 45) - **A byte-identical re-deploy no longer strands its activation token.** On a `gitvault_policy: required` project, a commit that turns out to be a noop (the plan's content already serving) previously minted and presented an activation token that was then never consumed - the noop path skipped the activation transaction where consumption lived, leaving the token unresolved forever and the operation's `gitvault_generation` empty. The noop commit now consumes the token at its own settle, under the same locks and the same refusal rules as a real activation, strictly before the operation reports `ready` - so "presented implies eventually consumed or refused" is now a real invariant (protocol rev 45, D218). - **Noop responses carry the `gitvault` rider.** A noop commit on a vault-gated project now returns the same `gitvault` result block as every other terminal (consumed token id, generation, outcome `DEPLOYED_AND_VAULTED`), instead of omitting it. Vaultless and grandfathered projects are byte-identical to before. - **Refusals apply to noops too**: a token revoked by a repair, or invalidated by a DR epoch change, refuses the noop commit with the gate's usual typed errors rather than silently succeeding - the serving release keeps serving either way. ## 2026-08-28 (gitvault: the epoch-rotation admission canary - rev 44) - **Rotations must now prove the reader path works.** The 2026-08-28 disaster drill found production clients could not READ a rotated vault at all - the reader had never implemented rotation traversal, while the write side was green on its own tests. Every `rotate_epoch` admission now requires a `self_open_attestation` on its payload: the rotating client's claim that it round-tripped its OWN new-epoch key envelope through the real reader entry point (`openEpochRotationForRecipient`), carrying fsck's own `chain_verified_to_generation` / `decryptable_to_generation` values verbatim. Absent or inconsistent -> the new `EPOCH_ROTATION_SELF_OPEN_UNPROVEN` error (retryable: run the round-trip, or upgrade the client). The gateway verifies the structural consistency it can see (whether the writer is an included recipient, the fingerprint and generation bindings); the decryption claim itself is honest-client evidence, stated as such in the protocol. - **New endpoint** `POST /gitvault/v1/vaults/:vault_id/recipients/:principal_id/proof-of-open` - any recipient submits fsck's evidence that it opened its envelope under the current epoch, and receives a signed `recipient_open_receipt` (evidence, never an authorization - it has no consumed state and admission never reads it). Strict self-match: only the named principal itself can submit, admin keys included. Idempotent per (principal, fingerprint, generation). Ledger disagreements refuse the new `OPEN_PROOF_MISMATCH` error. - **Coverage gains an openability dimension.** The operator overview's per-project `gitvault.coverage` block now reports, separately from envelope existence, which envelope-covered members have PROVEN they can open their envelope under the current epoch - "an envelope exists for you" and "you can actually read the vault" are different facts, and the drill was exactly the gap between them. - **Compatibility note**: no published `run402` client emits the attestation yet, so every epoch rotation is refused `EPOCH_ROTATION_SELF_OPEN_UNPROVEN` until a rev-44-aware client ships. Deliberate: it matches the standing no-rotations line while the round-5 review findings are repaired, and the error's own `next_actions` names the exit. ## 2026-08-28 (gitvault: the D204 round - epoch-rotation closures) - **`/confirm` and `/repin` are now idempotent**: retrying an identical confirmation or re-pin request converges on the same stored receipt instead of raising a duplicate-write error. A fresh mint returns `201`; replaying the same request returns `200` with the original receipt unchanged. - **New read endpoint** `GET /gitvault/v1/vaults/:vault_id/recipients/:principal_id` returns a recipient's current pin status - the fingerprint they're pinned to, the manifest version/hash it was issued against, and up to 20 outstanding confirmation receipts still owed to them, each classified `applicable` or `superseded` against the vault's live state. - **Recipient-set reconciliation is now exact.** Every organization with at least one vault has its desired-recipient set reconciled against real org membership + encryption-key state (dropping stale/removed members, adding new ones, never silently drifting). A `vault_genesis` or `rotate_epoch` admission on an unreconciled organization now refuses with the new `DESIRED_RECIPIENTS_NOT_RECONCILED` error rather than admitting against a stale set. - **Migration-epoch bootstrap rotation** is now a recognized, explicitly-declared branch (`migration_bootstrap` on the rotation payload) so a vault carried forward through the recipient-set migration can complete its first post-migration `elective_rekey` without an unrelated recipient-revocation event being required to authorize it. - **New error code** `PIN_MANIFEST_STORAGE_INTEGRITY_FAILURE` - raised when the recipient-pin-manifest object an admission's evidence points at is missing or does not hash to the expected value at the corrected read location. Not retryable; needs an operator to restore the object or apply a recovery decision. - **Compatibility note**: `run402` CLI/SDK releases published before this date predate `migration_bootstrap` and will have their epoch rotations refused on any vault that needs the bootstrap branch until upgraded - this is a known, accepted gap, not a regression to chase. ## 2026-08-27 (org slugs: your first one is free) - **An organization's first slug claim no longer costs anything.** `POST /orgs/v1/:org_id/slug` still debits the one-time $1 claim fee, but only from an org's SECOND claim onward - the very first `run402::/` handle an org ever claims is free, and needs no billing wallet on file to claim it. Renaming, or re-claiming after a release, still costs $1 as before. The response's new `fee_usd_micros` field states the real price of that specific call (0 or the fee), and `INSUFFICIENT_BALANCE` / `ORG_HAS_NO_BILLING_WALLET` now name `required_usd_micros` when a fee is actually due. ## 2026-08-27 (one repo surface: run402 repos) - **The encrypted-repo CLI family is `run402 repos`** (`run402` >= 4.41.0): create, view, list, rename, delete, snapshot, mirror, recover, fsck, gc, access, policy. Every `run402 gitvault ` spelling answers a typed error naming its successor. Older entries below use the spellings that were current when they were written. - **Gateway guidance follows**: deploy-gate and activation `next_actions` now name `run402 repos snapshot` and `run402 repos policy` — every command the platform hands an agent is a working spelling. - **The SDK namespace stays `r.gitvault`** — deliberate, not drift: `repos` is the operator verb family; `gitvault` is the protocol (`r402s/v0`, routes `/gitvault/v1`), and protocol identifiers are not marketing copy. ## 2026-08-26 (static assets: revalidate instead of serving stale) - **Mutable site assets are now revalidated, never served stale**: a stable static URL whose bytes can change (`/app.js`, `/styles.css`, `/site.webmanifest`) is materialized with `Cache-Control: public, max-age=0, must-revalidate`. It previously carried `public, max-age=300, stale-while-revalidate=3600`, so a returning browser could execute an asset for up to 65 minutes after a deploy replaced it - and because `stale-while-revalidate` serves the stale copy immediately, the visitor saw the old bytes with no indication anything was pending. For a site built from JavaScript modules this is a correctness bug, not just a freshness one: `index.html` revalidates on its own short TTL, so a newly-fetched entry module could import a stale sibling and throw on an export the cached copy never had. The new value matches the `revalidating_asset` cache class the stable-host serving path already emitted for the same files, so the two paths no longer disagree. - **Content-hashed assets are unchanged**: a fingerprinted filename (`app.a13f9c2e.js`, `assets/vendor_deadbeefcafebabe.mjs`, Astro's `_astro/` output) is still `public, max-age=31536000, immutable`. A build that fingerprints its output pays nothing for this change; only stable-URL assets revalidate. - **Takes effect per project on its next deploy**, since the header is stamped on the object when a release is materialized. Already-cached responses in a visitor's browser age out under their old policy one final time. ## 2026-08-26 (gitvault mirror + offline recovery: the exit ramp ships) - **Customer-owned ciphertext mirror** (`run402` >= 4.40.0): `run402 gitvault mirror set ` configures an opt-in, client-side mirror (credentials by AWS profile name or ambient - the platform never holds them); every `gitvault snapshot` then dual-pushes in admission order, and `mirror sync` backfills/reconciles idempotently from the vault objects listing. Mirror failures never block or alter a deploy - they surface as a named pending finding. Another member's key envelope is a `skipped_foreign_recipient`, never a failure (the recipient-only read rule means a mirror only ever holds YOUR envelopes). - **Offline recovery**: `run402 gitvault recover --out ` rebuilds the repository from a bucket listing plus your keystore with NO server involved; `mirror verify` is the keyless integrity mode. Proved live before release: a real production vault was mirrored, then recovered ref-exact with the gateway deliberately unreachable. Honesty statements ride every report verbatim: offline recovery proves validity, never freshness; and without the principal keystore, mirrored ciphertext recovers nothing. --- ## 2026-08-26 (gitvault: complete read surface for customer-owned mirrors) - **Vault objects listing**: `GET /gitvault/v1/vaults/:vault_id/objects` (owner/writer-gated, keyset-paginated) enumerates every stored object of a vault - key, object kind, hash, size - exactly as the ledger records it. The envelope now also carries an exact `total` (decimal string) alongside `has_more`/`next_cursor`. This is the diff source for mirroring a vault's ciphertext into a bucket you own: list, compare against your mirror, fetch what's missing. - **Every stored kind is now readable** by an authorized caller: presigned reads extend beyond the client-uploaded kinds to the server-authored artifacts (retention-cutoff tickets, prune intents and completions, maintenance issuances, cuts, and terminals). All of these are plaintext-structured metadata the platform already observed and disclosed - no new disclosure - and key envelopes remain strictly recipient-only. Groundwork for the upcoming `run402 gitvault mirror` + offline `recover` client release. --- ## 2026-08-26 (gitvault storage billing: corrected in your favor) - **We were over-counting vault storage, and the fix is retroactive in your favor.** Every push charged its bytes twice - once at upload finalization and once again at admission - with no test pinning the exact total, so billed `source_bytes` ran at roughly 2x reality. Fixed to a single writer, and a one-time migration recomputed every existing vault down to its true total (a real production vault dropped from ~68.6 MB to ~34.7 MB recorded). Storage is billed forward from the corrected figures. - **Tiny vaults stopped reading as megabytes.** The per-object accounting floor dropped from 64 KiB to 4 KiB, and billing now applies one per-vault minimum (1 MB) instead of flooring every object - so a notes repo with a handful of small files bills as ~1 MB, not 15 x 64 KiB. - **Reaped never-published uploads release their charge.** An abandoned upload's bytes were charged at finalization and never released when the reaper deleted them - a slow leak, now an atomic decrement at the reap. Pruned, formerly-published history deliberately still bills forever (append-only doctrine, unchanged and now pinned by test). --- ## 2026-08-26 (named addresses: run402::/) - **Organizations can claim a slug** - a globally unique, lowercase `[a-z0-9-]` handle (<=39 chars) - via `POST /orgs/v1/:org_id/slug` (owner-gated, `Idempotency-Key` required, a one-time $1 allowance debit recorded on the ledger). Renaming releases the old slug into a ~90-day cooldown during which it resolves to a typed `SLUG_RELEASED` naming the successor - never a redirect - and only then becomes claimable. A static reserved list and a curated protected-brands list (manual review to claim) guard the namespace. - **Projects can claim a per-org repo name** (`POST /projects/v1/:project_id/repo-name`, <=63 chars, same grammar) - the address form; the free-text display name is untouched. - **Vault resolution accepts names**: `GET /gitvault/v1/vaults?repo=/` resolves alongside `?project_id=`, case-folded and cooldown-aware. Responses carry the immutable `repo_id`, which clients pin locally - renames never break an existing checkout. - **Push-to-create**: `POST /gitvault/v1/vaults` accepts `{org_slug, repo_name}` - an atomic name claim, an instant project create (no schema DDL, per the lazy-materialization change below), and the vault allocation, in one authorized call. Two concurrent first pushes race safely: exactly one creates; the loser receives `REPO_CREATION_CONFLICT` naming the winner and a typed next action to push again - never a silent retarget, and never an orphaned project. --- ## 2026-08-26 (project creation is instant: the database materializes on first use) - **Creating a project no longer runs tenant-schema DDL.** `POST /projects/v1` claims the schema slot and returns; the physical schema - and the fleet-wide PostgREST schema-cache reload its DDL used to trigger on every project birth - now happens exactly once, at the first genuine database use: the first deploy carrying a `database` slice, the first `/rest/v1/*` request, the first admin SQL or expose call, or a fork writing its bundle. A vault-only repo project never pays for a database it never touches. - **Materialization is non-destructive by construction.** The guard stamps and grants; it never drops. Existing projects were stamped at migration time and are byte-identical in behavior. - **Honesty note:** the first deploy of this change was reverted within minutes - its own production E2E caught a data-loss interaction between an unwired SQL path and a then-destructive guard. The re-landed design makes that class structurally impossible (a missed path degrades to "stamped late", never to lost tables), and the incident's exact shape is now a permanent regression test. --- ## 2026-08-25 (allocating a vault no longer gates your deploys) - **Allocating a gitvault no longer sets the project's `gitvault_policy` to `required`.** Creating a vault - explicitly, by a first `git push`, or via `run402 repos create` - now leaves the deploy policy exactly as it was. Gating deploys on the vault is an explicit owner act (`PATCH /gitvault/v1/vaults/:vault_id/policy`, or `run402 gitvault policy required`), and it is now the ONLY thing that writes the policy. A deploy against a vaulted, ungated project proceeds normally and its result carries a typed `next_actions` entry offering the gate, plus a standing `warnings[]` entry on every later ungated deploy until the policy is set either way - never a block, never a prompt. Projects whose policy was already set keep it; nothing is migrated. - **A vault with unset policy stays visible.** `/tiers/v1/status` and the operator overview previously built their `gitvault` summary only for `required`/`grandfathered` projects, so a freshly allocated vault - already accruing `source_bytes` - vanished from both. The summary now keys off vault existence and reports `policy: null` when unset. - Client side: `run402`/`@run402/sdk`/`run402-mcp` 4.37.1 removes the interim stderr advisory 4.37.0 printed after every allocation (true then, false now) and updates the docs to state this as current behavior. --- ## 2026-08-25 (git push is the on-ramp: origin, lazy vaults, and the vault-only track) - **The run402 remote is named `origin` now** (when the repository has no `origin` yet). `run402 init`, `run402 gitvault init`, `run402 projects provision` (new: it scaffolds the remote when run inside a git repository), `run402 up`, and `run402 repos create` all claim `origin` additively - an existing `origin` is never touched, and the fallback name `run402` is used when it is taken. `git push origin main` against a Run402 project works with no run402-specific ceremony. - **First push allocates the vault.** `git push origin ` and `run402 gitvault snapshot` against a project with no vault run the same six-stage creation `gitvault init` performs explicitly, printing the one-shot recovery receipt and the keystore path to stderr the moment allocation lands. Interrupted creations resume on the same local creation id. `git ls-remote` and `fetch` stay pure reads and never allocate. - **`run402 gitvault snapshot` replaces `run402 gitvault push`.** One verb per operation: "push" now always means `git push`. The old spelling remains a stderr-deprecation alias for exactly one release. - **`run402 repos create|list|delete` - the vault-only track.** One command from a directory of code to a hosted encrypted repo: provision + vault allocation + remote scaffold, zero deploy ceremony, nothing deployed unless you separately choose to. CLI and OpenClaw only - mutating vault verbs deliberately have no MCP tools. `delete` refuses while the vault holds admitted generations unless `--force` is passed after the refusal names exactly what would be destroyed. - **A progressive terminal-loss warning.** A fresh vault gets one quiet stderr note; once it crosses any of >=10 admitted generations, >=10 MB of encrypted source, or >=14 days since genesis, `run402 gitvault status` and `run402 doctor` carry a standing warning. Nothing clears it in V0-A - the platform cannot yet see a second principal able to open the vault - and the protocol terminal-loss statement itself is unchanged. - **Interim, stated plainly:** allocating a vault currently sets the project's `gitvault_policy` to `required`, so deploys of a vaulted project must present a vaulted capture (produced automatically by `run402 deploy apply` on machines holding the keystore). Every allocating client path now says this on stderr. A later release decouples allocation from the deploy gate; the client already handles the resulting ungated state (a typed `next_actions` offer plus a standing warning - never a block). Shipped in `run402` / `@run402/sdk` / `run402-mcp` 4.37.0. --- ## 2026-08-22 (testnet signer balances are no longer priced in dollars) - **A testnet signer's balance is never rendered as a fiat amount, and never described as a permanent loss of funds.** The day-60/75/88 KMS-signer suspension warnings and the post-deletion notice took the address's native balance and multiplied it by a live Chainlink ETH/USD feed — including on `base-sepolia`, where the feed mirrors real-world ETH prices but the ETH itself is free faucet ETH. A suspended test signer holding 0.0005 Base Sepolia ETH was therefore described as holding `$0.94`, inside a paragraph warning the funds would become "permanently inaccessible to anyone." Chain configuration now carries an explicit `testnet` flag; on a testnet the price oracle is not consulted at all and the balance renders as `0.000500 ETH (base-sepolia testnet - no monetary value)`. - **The deletion notice itself is unchanged in substance.** The key really is destroyed and the address really does become permanently unusable, so that half of the warning survives on testnet - what is removed is the false claim that something of value is at stake. Mainnet emails are byte-identical to before. --- ## 2026-08-18 (apex custom domains: one nameserver change, Run402 does the rest) - **`hosted_dns_zone` authority mode: Run402 can now host your custom domain's DNS zone.** Connecting an apex domain (`example.com`, not `www.example.com`) previously dead-ended at most DNS hosts — a plain CNAME at a zone apex is illegal, and Route53's ALIAS records only target AWS resources, so the manual instructions collapsed into "move your DNS to a Cloudflare account you manage." Now `PUT /projects/v1/:project_id/domains/:domain` with `{ "desired": { "authority": "hosted_dns_zone", "web": { "enabled": true } } }` provisions a DNS zone for the domain inside Run402's infrastructure. The customer-side work collapses to exactly one instruction: set the domain's nameservers at the registrar to the assigned pair returned in `dns_records[]` (type `NS`, purpose `hosted_zone_delegation`). Run402 applies every in-zone record once delegation is observed — web serving, ownership verification, TLS, and later email records — tracked by the new `zone.delegation` check (`ZONE_PROVISIONING` / `NS_DELEGATION_PENDING`). - **Live mail routing is never silently dropped.** Pre-existing MX and TXT records observed at connect time are imported into the hosted zone verbatim before the nameserver change is recommended, and disclosed on the aggregate's new `hosted_zone.imported_records`. - **Custody is disclosed and reversible.** The aggregate's `hosted_zone` block states `dns_hosting: "run402_hosted"` with `status` and `ns_assigned`; DELETE tears the zone down and warns that DNS stops resolving until nameservers are re-pointed. A domain whose hosted zone belongs to another project returns `409 HOSTED_DNS_ZONE_CONFLICT`; deployments without hosted-zone support return `409 HOSTED_DNS_ZONE_NOT_CONFIGURED`. - **`authority.options[]` is now honest about the hosted mode**: `hosted_dns_zone` shows `available` (with `HOSTED_DNS_ZONE_OPT_IN` before opt-in) and is the recommended mode for an apex domain with web serving enabled. `manual_dns` remains the default and is unchanged. - **One canonical custom-domain surface.** The legacy standalone `/domains/v1` routes (March 2026: register/list/status/delete with CNAME+TXT copy-paste instructions) are retired — a 14-day production traffic scan showed zero real callers. Custom domains are managed exclusively through `/projects/v1/:project_id/domains/:domain`: connect, status, checks, DNS records, apply/repair/check actions, and hosted zones all live on one aggregate. A connect with `web: { enabled: true }` now registers web serving in the same call under BOTH authority modes — the separate registration call the manual path silently depended on is gone. - **The gateway now has ZERO PUT routes.** `PUT /auth/v1/user/password` — the last one — moved to POST and was unmounted, after `@run402/sdk` 4.24.1 shipped the matching change. `PUT` is also gone from the CORS `Access-Control-Allow-Methods` advertisement, since preflighting a verb nothing serves is just a lie with extra steps. The verb-governance allowlist is now **empty**, which turns the style guide's "Don't use PUT" from a review convention into an absolute, mechanically-enforced property of the API. - **The transitional PUT aliases are gone.** `PUT /projects/v1/:project_id/domains/:domain` and `PUT /wallets/v1/:address/label` no longer route at all — POST is the only form. They existed for exactly one release cycle so the published SDK could migrate, and were removed the moment run402 4.24.0 shipped POST. The gateway's verb-governance allowlist is down to a single entry (`PUT /auth/v1/user/password`, pending its own client round), and a stale-entry check makes the list incapable of outliving reality. - **POST is the canonical ensure verb; PUT is banned platform-wide by a CI gate.** `POST /projects/v1/:project_id/domains/:domain` and `POST /wallets/v1/:address/label` replace their PUT forms (the PUT aliases stay mounted, undocumented, until published SDKs migrate). A new governance test fails CI on any PUT registration outside an explicit, justified allowlist — the style guide's verb rule is now mechanical, not manual review. - **Keys-only agents can manage their project's domains.** ProjectDomain routes now accept the project's own admin-capable credential (service_key or admin `r402_` credential), scoped strictly to that credential's project — reads included. A credential presented against another project's path gets the same 403 whether or not the target exists. - **ProjectDomain responses adopt the canonical `next_actions[]` field.** The aggregate previously used a singular `next_action` plus `alternate_actions[]` — a pre-canonicalization shape. It now carries `next_actions[]` (first entry = the recommended action, empty array = nothing to do), matching the platform-wide vocabulary used by every error envelope. The old fields remain as deprecated aliases (`next_actions[0]`, `next_actions.slice(1)`) until the published SDK/CLI migrate, then they are removed. ## 2026-08-10 (uploads stop double-counting against your storage quota) - **Committed content uploads no longer hold a phantom storage reservation forever.** Every upload reserves its declared size against the org's storage quota while in flight; the content-plan commit path never released that reservation, so each deploy's full payload kept counting against `storageBytes` on top of the real (deduplicated) content — roughly a 1:1 shadow that effectively halved every organization's usable storage quota and grew with every deploy. The commit now releases the reservation atomically with the plan's `committed_at` flip, and a one-shot migration reconciled all historical leaked reservations (6.23 GiB across 1,069 projects, stuck since April). `storage_bytes` itself was always correct — per-unique-hash billing is unaffected; only the in-flight reservation counter leaked. No action needed: quota headroom shown by `GET /tiers/v1/status` simply becomes accurate. ## 2026-08-09 (a 401 names every credential it would have accepted) - **An authentication failure now enumerates every credential class the route accepts, instead of naming only the first one checked.** Routes that take either a control-plane session or a SIWX wallet answered a credential-less request with `Missing SIGN-IN-WITH-X header` and a single `next_actions` entry carrying no method or path — so a caller holding a perfectly good session was told, in effect, that wallet signing was its only way in. The envelope now carries `details.accepted_credentials[]` and one `next_actions` entry per accepted class, each tagged with `auth` and carrying the `method`/`path` to obtain that credential where one exists. Status and `code` are unchanged; this is additive. A credential that was *presented and failed* keeps its own specific message — that error is about what you sent, and listing alternatives there would bury the real fault. - **Project credentials are now reachable from the SDK and CLI.** `POST /projects/v1/:project_id/credentials` and its four siblings shipped with no client: there was no CLI command, no SDK method, and no generic signed-request escape hatch, so the documented way off the legacy derived `anon_key`/`service_key` was to hand-roll a signed request. `run402 credentials issue|list|status|rotate|revoke|token` and `r.credentials.{issue,list,status,rotate,revoke,mintToken}` cover the whole capability, alongside the existing local key cache at `run402 credentials project-keys …`. - **`run402 credentials status` is the deliberate way to ask whether a project still depends on the retiring key.** It needs only project-read, so automation can check its own posture. It reports the conditions retirement is gated on and never a date — there is no deadline to plan against. ## 2026-08-08 (notifications name their subject) - **A project-scoped notification now names its project, organization, and serving host instead of an opaque `prj_…` id.** Telegram messages and emails identify the subject as `organization · project` plus the host it serves on (its bound custom domain if it has one, else `.run402.com`). Identity is resolved from platform state at delivery time, so no event has to carry it and a project rename can never deliver a stale name; if it can't be resolved the notification still delivers, showing the id. - **Project-scoped Telegram messages now carry the console deep-link.** The link was suppressed whenever the event's own payload lacked an organization id — which was the normal case for platform events, so most of them arrived with no way to click through. - **The bounded payload view no longer truncates the useful part.** Fields were ordered by how the payload happened to be stored, and each value was capped at 120 characters inside a 4096-character message — enough to cut `error_fingerprints_observed` off mid-sample at `"db is not …`. Fields now render in a deterministic, event-aware order with a budget scaled to the message, and any field the cap does omit is disclosed with a pointer to the feed. - **Audit rows record the identity that was delivered.** `GET /agent/v1/notifications` rows carry a `subject` object with the resolved names, so the log shows what the message showed. Rows written before this have no `subject`, and the signed webhook envelope is unchanged. --- ## 2026-08-07 (invalid input answers 400, not 500) - **An invalid release spec now returns `400 INVALID_SPEC` instead of `500 INTERNAL_ERROR`.** `POST /apply/v1/plans` and its commit/resume siblings validate a large part of the spec inside the release core, whose rejections were not recognized by the deploy error mapper and fell through to the generic internal-error branch. A caller who sent, for example, both `functions.replace` and `functions.patch`, or an unknown key inside a route entry, was told the gateway had failed. The envelope now carries `resource` naming the exact slice of the spec that was rejected, plus `retryable: false` and `category: "validation"`. - **A malformed JSON body on the admin SQL route now returns `400`.** `POST /projects/v1/admin/:project_id/sql` reads its body as raw text and parses it itself; an unparseable body raised through the handler and was reported as an internal error. The 400 keeps the parser's position detail so the offending character is identifiable, and never echoes the submitted body. A body that parses but is not an object (`null`, an array, a bare scalar) now falls through to the existing "No SQL provided" 400 rather than failing on a property read. --- ## 2026-08-05 (human Buzz identity attribution and terminal adoption receipts) - **Run402 humans can connect public Buzz identities without an organization handoff.** The Account console now runs a direct-session, fresh-passkey, explicit-disclosure ceremony through Buzz's already-released browser callback. The resulting `idlnk_…` is public attribution only: it is not a sign-in method and grants no organization authority. Buzz itself did not change. - **Adoption now records three separate effects.** A successful co-owner handoff atomically creates or reuses the human's public identity link, adds the ordinary Run402 owner membership, and completes a terminal consent receipt. The corresponding generic identity-link, membership, and adoption audit events share one operation id. - **Identity and authority can be revoked independently.** Revoking a human's public Buzz identity link does not remove organization membership or rewrite the completed receipt. Removing the human's membership does not revoke the public link. Each principal may have multiple active Nostr identities, while one Nostr identity may be actively linked to only one effective Run402 principal. - **Principal merges preserve public provenance.** Public proof reads retain the original proved principal and signed event while separately reporting the surviving principal's effective control. List and revoke operations follow that effective control without rewriting history. --- ## 2026-08-04 (Buzz handoff diagnostic receipts) - **Support can see the exact last observed handoff boundary.** Offer-backed ownership attempts now retain a bounded, privacy-safe sequence of allowlisted browser and server milestones. Offer status exposes the latest attempt reference, lifecycle timestamps, deterministic diagnosis code, and last gateway trace handle, distinguishing Buzz-open, callback receipt, completion rejection/success, and expiry without screenshots. - **The receipt is not a browser transcript.** It retains no signed approval, event id, credential, email, identity/session id, browser/network metadata, URL fragment, or arbitrary text. Client milestones are observational and cannot change authority. - **Organization billing reads now follow the principal authority graph.** `GET /orgs/v1/:org_id/billing` and `/billing/history` accept SIWX wallets and human control-plane sessions, then require an active org role of `billing` or higher. A wallet billing-pool link no longer grants access by itself, so member revocation takes effect immediately and membership-only billing/admin/owner principals can read the organization they are authorized for. ## 2026-08-01 (one-request Buzz onboarding) - **The Run402 install request no longer stops at copied files.** In managed Buzz, `Please install the run402.com skill` now means install and connect Run402. The file-copy stage remains inert, but the agent immediately reads the verified installed skill, runs preflight, initializes its dedicated profile, and verifies the public identity link in the same turn. It never says “available next turn” or asks for a second setup message; an explicit files-only instruction remains available. - **The first real pause is the contextual demo.** After connection, Fizz asks whether to build one relevant quick demo. After approval it builds, deploys, and verifies the live URL, then offers the separate co-owner handoff. Completion still adds the human as co-owner while Fizz remains the founder-agent owner. - **Buzz installation is Windows-friendly.** Agent guidance now passes the workspace, `DO_NOT_TRACK` child environment, and installer command as separate execution fields whenever possible. Shell-only fallbacks are explicitly rendered for POSIX, Windows PowerShell, and `cmd.exe`, so Windows agents never receive Bash-only `NAME=value command` syntax. - **Windows setup no longer mistakes platform spelling or command shims for incompatibility.** Released Buzz capability checks accept clap help rendered with either `buzz` or `buzz.exe`. The setup helper executes npm's and Run402's JavaScript entrypoints through the managed Node runtime with `shell: false`, including install/upgrade, so Node 22 never has to spawn `.cmd` shims. A residual process-boundary refusal is reported as a Buzz runtime repair rather than a request to reinstall npm. --- ## 2026-07-31 (Buzz demo-first ownership handoff and setup diagnostics) - **Fizz demonstrates value before asking for governance.** The canonical Buzz conversation now preflights and links the agent, offers one contextual application, waits for approval, deploys and independently verifies it, and only then asks whether the human wants co-ownership. Base Sepolia faucet/prototype plumbing stays in the structured receipt without a testnet price or funding prompt in chat. - **“Become an owner” is now a normal durable HTTPS link.** An eligible founder agent creates an inert `buzzhao_…` offer that grants nothing and does not start the five-minute signing challenge. The human opens the Run402 console, signs in/enrolls a passkey, performs fresh step-up, verifies the six-digit code in Buzz, and returns through Buzz Desktop's already-shipped same-origin fragment callback. Fizz polls authoritative completion; a click alone is never success. - **Target binding prevents forwarded-link or copied-proof adoption.** Each short attempt is bound to the exact human principal and browser-session lineage. Completion atomically adds that human as co-owner while Fizz remains owner; no project, organization ownership, or credential transfers. Expired attempts release their slot while the durable offer remains available. - **One preflight replaces serial setup failure.** `run402 doctor --buzz` checks the exact agent target, configured shell/Node, Run402 and Buzz capabilities, Run402 API/console, public-safe relay, and named wallet profile before setup mutation, returning one exact repair per failed check. - **Stale Buzz clients converge agent-side.** Run402 v4.17.2 is now the semantic floor for Buzz setup because it first made safe relay availability warning-only for founder setup. A missing or older user-global CLI is upgraded and verified automatically before doctor runs, so the human is not sent a stale reconnect/restart repair. - **Diagnostic telemetry is aggregate-only and optional.** Best-effort doctor events contain only allowlisted coarse event/check/code/runtime/context fields, increment aggregate counters, and retain no raw event or installation identity. Set `RUN402_TELEMETRY=0` to disable delivery; telemetry never gates setup or changes doctor output/exit. - **Buzz remains unchanged and MCP remains read-only.** Run402 uses Buzz Desktop v0.5.2's released `browser_fragment_v1` behavior. SDK/CLI expose typed offer lifecycle; MCP only renders normal HTTPS and exact CLI handoffs and receives no human session, passkey, or Buzz signing capability. - **Production acceptance correction:** the Buzz page now gives agents `https://run402.com/buzz/install.txt`, which requires the first-party digest and managed-workspace path to be verified before success is reported. A safe relay TLS/availability failure now warns without blocking founder setup, while unsafe destinations still block and community installation/enrollment remain disabled until live relay proof succeeds. - **The human install instruction is generic again.** In Buzz, `Please install the run402.com skill` is sufficient. The apex agent router and discovery metadata select `run402-buzz` from managed Buzz context; hidden bootstrap URLs, runtime targets, and terminal commands remain agent-only implementation details. --- ## 2026-07-31 (first-party Run402 skill distribution) - **Run402 skills now install from Run402 itself.** The canonical `skills` installer command resolves `run402` and `run402-buzz` through `https://run402.com/.well-known/agent-skills/index.json`, then fetches immutable content-addressed artifacts from `run402.com` instead of requiring GitHub, raw-content, or registry access on the happy path. - **Agent targets are explicit and portable.** Installation guidance names the real `claude-code`, `codex`, `goose`, and `universal` targets, including the combined multi-agent command. GitHub remains available only as a transport fallback; an integrity failure never falls back or starts installation. - **Publishing is atomic and retained.** New immutable artifacts upload first, direct-origin checks verify bytes, content type, caching, and redirect behavior, and the short-cached discovery index publishes last. Failed post-publication verification restores the previous index, while older content-addressed artifacts remain installable. --- ## 2026-07-31 (MPP Lightning charge verification complete; activation pending) - **Lightning is a first-class method in the existing payment engine.** The exact `run402-mpp-lightning-charge-draft00-safety-v1` profile now drives `pay.fetch`, `run402 pay`, MCP `pay_url`, and tier changes through one rail-neutral intent, idempotency, fulfillment, receipt, attestation, credit, and replay model. It is not represented as another x402 blockchain network. - **A real two-node regtest tier purchase passes end to end.** The public SDK selected one fresh fixed BOLT11 invoice, the pinned Alby Hub/LND NWC adapter dispatched once, the private seller observed `SETTLED`, the prototype tier mutation committed once, and an identical retry returned the retained result with `funds_moved:false`. The exact pinned LND invoice feature profile is `[8,14,17,25]`. - **Prices remain canonical USD micros with two independent buyer limits.** A retained quote records source, reference, observation time, exact rational BTC/USD rate, spread, quote time, and validity; the invoice is a whole-satoshi fixed amount. Buyers must authorize both maximum USD micros and maximum invoice-plus-fee millisatoshis, with a separate routing-fee ceiling. - **Recovery and accounting drills are complete on regtest.** The test matrix covers `OPEN`, `ACCEPTED` past timestamp expiry, `SETTLED`, `CANCELED`, lookup outage, restart catch-up, asymmetric manifest/Postgres/node/key restores, late settlement, excess and recovery credits, exact replay, and all retained liabilities. The payment-data snapshot confirms that retained seller/buyer journals, responses, results, and manifests contain the intended public fields and digests, not wallet or credential material. - **MPP Lightning charge is a first-class tier payment method.** The exact Run402 profile uses one fresh fixed-amount Bitcoin-mainnet BOLT11 invoice per purchase, with the same payment-intent, idempotency, receipt, recovery-credit, and accounting machinery as the existing rails. The first bounded production payment remains a separately approved verification step. --- ## 2026-07-30 (Buzz community control plane) - **Buzz and Run402 now distinguish four independent lifecycle states.** Skill installation is shared inert software; community installation associates one Buzz community with one Run402 organization; human adoption adds the Buzz owner as a Run402 human co-owner; agent enrollment gives each Buzz agent its own principal and bounded grants on named existing projects. - **Enrollment defaults are explicit Run402 owner choices.** New community installations start non-default, never inherit priority from creation order, and cannot silently displace another organization's descriptor. Replacing a default requires clearing the old installation before selecting the new one. - **Fizz's founder ownership is legitimate and adoption adds a co-owner.** A sole agent owner with a verified Buzz identity can start a short-lived adoption. The human completes it through a direct control-plane login, fresh passkey step-up, and a separately signed Buzz-owner proof. The response states that Fizz's membership remains unchanged and that no organization ownership, project, or credential transferred. - **Honey can join without inheriting Fizz's wallet or creating a throwaway organization.** A fresh agent wallet may establish its own public identity link before org provisioning, discover the Run402-published installation descriptor for its Buzz community, and request finite existing-project grants. Manual approval is the default; automatic enrollment is opt-in, capability-allowlisted, finite, current-membership-gated, and always zero-spend. - **Buzz itself remains unchanged.** A current Buzz owner/admin publishes Run402's exact approval JSON using the released kind-1 publisher; Run402 verifies released NIP-11 `self` and the relay-signed NIP-43 membership snapshot, then owns descriptor discovery and lifecycle. Installation revocation stops new enrollment but does not silently revoke existing agents; enrollment revocation removes only grants created by that enrollment. The org-of-one path remains the cold-start fallback. - **Expiry and relay security converge at their authoritative boundaries.** Due ceremonies and active enrollments are persisted as `expired` with `expired_at` before later work can fail, releasing non-terminal slots and retaining audit history. Every NIP-11 and Nostr membership dial rejects non-public or mixed DNS targets and pins the validated address set into the actual fresh connection, closing DNS-rebinding and private-network SSRF paths. - **Buzz status and recovery responses now say exactly what is known.** Pending installation descriptors are labeled `proposed`; safe policy summaries include the effective capability allowlist and TTL cap; Run402 reports selection eligibility separately from the integration's explicit descriptor selection; pending or active enrollments suppress duplicates; every next action carries complete safety metadata. Error recovery is code-specific, and throttling returns one exact delay in both `Retry-After` and the JSON contract. --- ## 2026-07-29 (Run402 for Buzz onboarding) - **Run402 for Buzz now has a dedicated getting-started page.** The new `/buzz` page connects the products from signed multiplayer conversation to verified deployment, explains the separate-key dual-proof security boundary, links the open-source skill and its public listing, and makes the project discoverable from the Run402 homepage, use-cases index, footer, and sitemap. - **This historical GitHub-first install path is superseded.** The current canonical bootstrap is `https://run402.com/buzz/install.txt`; the agent installs the digest-verified first-party artifact from `run402.com` into its actual managed-runtime workspace target. GitHub remains only the bounded availability fallback. --- ## 2026-07-28 (public Buzz/Nostr agent identity attribution) - **An agent can prove its Buzz identity without sharing or reusing a private key.** A short-lived dual-signature ceremony links a separately held Nostr public key to the agent's Run402 EOA: the wallet signs the exact server challenge, Buzz signs a standalone kind-1 event containing it, and Run402 verifies both signatures. Private-key, `nsec`, mnemonic, seed, and derivation inputs are rejected before mutation. - **Identity is visible but powerless.** Public proof reads, `whoami`, project creator, deploy actor, release actor, and transfer initiator surfaces show the verified link and immutable action-time provenance. The Nostr identity is attribution only and never participates in authentication, authorization, organization ownership, grants, payment, or transfer targeting. - **Revocation preserves audit history.** A principal can revoke its own link with fresh EOA SIWX; security operators have a separately audited emergency revoke. Current reads show revoked/effectively inactive state while prior creator/deployer snapshots remain unchanged. The public integration package includes the Buzz workflow, golden vectors, an independent verifier, and a customer-feedback reference application. --- ## 2026-07-27 (Bearer account-security route discovery) - **Machine clients can discover the account-security mutations that were already live.** The canonical HTTP reference and OpenAPI now describe all eight `apikey + Bearer` routes under `/auth/v1/account/*`, including OAuth identity unlink, passkey removal, password mutation, and browser-session management. `POST /auth/v1/account/identities/unlink` accepts `{ provider, subject }`, derives the user from the access token, and never permits service-key targeting of an arbitrary user. - **Freshness is explicit and usable without a hosted cookie.** Ordinary access tokens from `/auth/v1/token` carry signed `auth_time`. Password, passkey-removal, and identity-unlink mutations require authentication within five minutes; refresh-token rotation preserves the original time, so callers satisfy `R402_AUTH_FRESHNESS_REQUIRED` by completing a credential-verifying login again. - **The documentation gate now sees named Express routers.** Public routes mounted through identifiers such as `authAccountAdvancedRouter` can no longer bypass API/OpenAPI/agent-doc alignment; internal named routers require an explicit exclusion. ## 2026-07-26 (same-tab email-code authentication) - **Humans can finish email sign-in in the tab where they started.** Managed `` requests one email containing a six-digit code plus the familiar one-tap link when provider discovery advertises both. The originating page presents one accessible code field; the link remains the cross-device fallback. - **Machines use one explicit challenge-handle flow.** `POST /auth/v1/magic-link` adds `delivery: link|code|both` (wire default `link`) and returns only an opaque `challenge_id` for code-capable sends. `grant_type=email_code` exchanges that handle plus the emailed code. The existing CLI and MCP request/verify operations gained fields instead of duplicate commands/tools; the SDK adds `verifyEmailCode` and now returns accepted request details. - **Acceptance is not a delivery claim.** Anonymous sends say accepted, preserve warnings, and never reveal account existence. Durable project/email/IP limits remain; exact project test identities omit only the per-email bucket. Codes are HMAC-verified, single-use, exhaust after five well-formed failures, and never satisfy passkey freshness. --- ## 2026-07-23 (verifiable commerce buyer + tier replay safety) - **The existing payment verb can require wallet-rooted merchant evidence.** The SDK `pay.fetch` option `requireReceipt`, CLI `run402 pay --require-receipt`, and MCP `pay_url.require_receipt` verify standard direct-merchant offers before payment and matching receipts after settlement. Results keep settlement, current-attempt movement, replay, delivery, signer relationship, policy, and raw non-secret evidence separate. A post-settlement policy failure preserves the upstream response and never recommends a second payment. - **Tier changes now commit once per payment on every accepted rail.** x402 identities use canonical settlement-verified fields rather than raw header bytes; allowance uses its durable debit id; MPP uses its provider transaction or credential identity. The entitlement, refund, ledger, and outcome commit atomically. Replays preserve the original tier action and committed lease facts while returning `payment_replay:true` and `funds_moved:false`. - **Hosted receipt authoring remains intentionally disabled.** `pricing.receipt:"on_fulfillment"` is present in the public Core authoring contract, but Run402 Cloud rejects it before mutation until `@x402/extensions` publishes an interoperable delegated-signer authorization carrier. No hosted tenant or tier challenge advertises Run402 merchant evidence in this release; ordinary priced routes and independent direct-merchant verification remain compatible. - **Managed signing is prepared behind the gate with explicit cost and lifecycle controls.** Signer creation is a staged, resumable apply external effect with provider-tag recovery; retired keys are disabled and scheduled for deletion by immutable KMS ARN after authorization expiry. Challenge signing has pre-KMS source/project/org/global admission, exact bounded caching, budgets, circuits, metrics, and kill switches; encoded offer/receipt evidence is capped at 8 KiB. --- ## 2026-07-22 (durable caller-key identity for tenant-priced routes) - **A business key now survives process loss without authorizing a second charge.** Paid web-route callers send `Idempotency-Key`; after proof verification Run402 scopes it by payer, project, route pattern, and method and binds it to the request contract. Same payer + same request + same key resolves to one durable `payment_id`, while mismatches return `IDEMPOTENCY_KEY_REUSED`. The raw key and signed proof are never persisted. Wallet rotation changes payer identity and is not a recovery path. - **Same-key replay is the status operation.** Pending or ambiguous attempts return `409 PAYMENT_INTENT_PENDING`, `Retry-After`, payment-bearing platform headers, and one exact action: repeat the identical request with the same payer and key. Settled replay reports `deduplicated:true`, `funds_moved:false` for the current attempt, and `delivery:first|replay`; compact tombstones keep old keys fenced indefinitely. Settlement is at-most-once per payment identity, while tenant function execution remains explicitly at-least-once and must deduplicate effects by `payment_id`. - **Ownership and payout changes drain the old payment generation before flipping.** Project transfers and organization payout-wallet mutations publish cutoff/blocker/lease state, reject new caller-keyed and keyless payment admission during the drain, and never deliver an old-generation settlement to a new owner. Transfer preview now exposes `payment_drain` and the outgoing-generation settled-intent count. - **Buyer and function surfaces preserve the new contract.** Routed payment context adds `idempotencyKey`, `deduplicated`, and `delivery`; platform-owned response headers expose payment id, movement, delivery, settlement time, and pending state. The SDK, CLI, and MCP preserve pending/drain/destination/key/fence/lifetime errors, use an exact managed-host trusted-pending classifier, and persist only a caller-key digest in the local attempt journal. Custom domains remain ambiguous to generic clients. - **Minimum compatible packages:** `@run402/functions` 3.11.0 for the routed function payment context; `@run402/sdk`, `run402`, and `run402-mcp` 4.11.0 for caller-keyed buyer recovery. - **Compatibility note: overlong hand-rolled x402 authorizations now fail before settlement.** Tenant challenges advertise `maxTimeoutSeconds: 300`; Run402 permits five seconds of clock skew and returns `PAYMENT_AUTHORIZATION_LIFETIME_EXCEEDED` outside that bound. Clients that previously ignored the advertised maximum must mint a standard bounded authorization before retrying the same key. --- ## 2026-07-21 (outbound email delivery hardening) - **Authentication mail is isolated from unrelated hard-bounce suppression.** Run402 now selects an internal SES delivery purpose for trusted authentication, project mail, platform notifications, and fail-closed fallback traffic. Auth continues to honor complaints and Run402 suppression rules, but an unrelated SES account-level hard bounce can no longer silently prevent a controlled authentication delivery attempt. - **Every managed sender has an observable fallback.** Purpose-specific configuration sets publish delivery, bounce, and complaint events through the existing pipeline, and Run402-managed identities default to the restrictive fallback set if an explicit stamp is ever omitted. Caller-selected templates—including `magic_link`—remain ordinary project mail and cannot select privileged delivery policy. - **SES reputation pages before enforcement.** Bounce and complaint warning/critical alarms now flow through the existing Run402 alarm relay, with purpose-identifiable internal event telemetry. No mailbox API, webhook envelope, SDK, CLI, MCP, function, or Core contract changed. --- ## 2026-07-20 (custom-domain status and encoded-path routing) - **Own-zone custom domains now report the state they actually serve.** When a hostname belongs to a Cloudflare zone managed by Run402, domain creation waits for Cloudflare to accept the Worker Custom Domain binding before recording `status: "active"`; older successful bindings that were stranded at `pending` safely self-heal on the next domain-status read by idempotently confirming the binding. Failed or unconfigured bindings remain pending. - **Safely encoded percent signs no longer crash custom-domain route selection.** Stable-host requests are canonicalized once and their decoded path is matched without a second percent-encoding validation pass, so a valid path such as `/v1/envelope/%25s/remind` reaches its declared prefix route. Encoded separators, traversal, control characters, duplicate slashes, and internal namespaces remain rejected before matching. --- ## 2026-07-20 (magic-link inbox safety) - **Repeated sign-in emails no longer hide the working link in an ambiguous inbox thread.** Magic-link subjects now include the send time in UTC (for example `Sign in to kysigned (17:39 UTC)`), and both email bodies state that only the most recent sign-in email works because requesting a replacement supersedes older links. - **Static callback footguns are visible at request time.** `POST /auth/v1/magic-link` keeps its non-enumerating 200 response but adds `warnings: [{ code: "MAGIC_LINK_REDIRECT_STATIC_ALIAS", message, path, target_file }]` when the supplied callback path is served by an active GET static route alias. This is advisory—a static document can handle the token—but alerts projects when a marketing page has displaced their token-exchange shell. --- ## 2026-07-16 (telegram notifications + routing rules) - **Bind a Telegram chat with one API call, no bot token.** `POST /agent/v1/notifications/channels/telegram` returns a single-use, 15-minute deep link (`connect_url` for a private chat, `connect_group_url` for a group); tapping it in Telegram activates the binding and confirms in-chat. `GET /agent/v1/notifications/channels` lists every channel — email, webhook, and every live Telegram binding — in one read; `DELETE .../channels/telegram/:binding_id` revokes. - **Routing rules decide which events reach which chat.** `POST /agent/v1/notifications/rules` filters by `project_id`, `source` (`app`|`platform`), `event_types[]`, and `classes[]` — each optional and ANDed when given, so a rule scoped to one project and one app event type pages exactly the chat it should. No rules means no Telegram traffic at all; existing email/webhook preferences are completely untouched. - **Delivery is honest, not silent.** Telegram rides the same ~60s notification tick as email/webhook, coalesces bursts into one grouped message per chat, and auto-disables a binding after 10 consecutive hard failures — firing a mandatory `telegram_binding_disabled` alert so a blocked bot never silently stops paging. `POST /agent/v1/notifications/test` now verifies the full binding+rule+render+send chain in one call. - **Fails closed until provisioned.** The channel activates only once a platform operator wires up the dedicated notification bot; until then, connecting a channel returns `503 TELEGRAM_CHANNEL_NOT_CONFIGURED` rather than silently no-opping. --- ## 2026-07-15 (events feed display-order reads + console Activity card) - **The events feed can now answer "what happened last?" in one call.** `GET /projects/v1/:project_id/events?order=desc&limit=N` (and the org sibling) returns the newest events first with `before_cursor` keyset pagination for older pages (`?order=desc&before=`); the response still carries the live catch-up `cursor`, so a display surface can hand off to "what's new since" polling without a discovery call. The ascending catch-up contract is byte-identical to before — two modes, disjoint params (`cursor` composes with ascending only, `before` with `order=desc` only), same `?source`/`?event_type` filters on both. - **console.run402.com renders the feed.** The project page gains an Activity card: latest 20 events newest-first with humanized labels, class + source chips, relative times, payloads behind a disclosure, copyable drill-down commands, a text filter, and Load-older paging. App-emitted events (`source:"app"`) render with a distinct chip and their raw snake_case type — new vocabulary (platform or app) appears with no console release. The org tab formerly labeled "Activity" is now "Audit log" (it renders the audit log; the word "Activity" now names exactly one thing). --- ## 2026-07-15 (app events emit lane) - **Your deployed code can now write to its own events feed.** `POST /projects/v1/:project_id/events` is the write twin of the project events feed: service_key auth (own project only), body `{event_type, payload?, idempotency_key?}`, and the row lands as an ordinary feed entry with `source:"app"`, `class:"app"` — read back by every existing and future feed consumer (this API's GET routes, the CLI, the MCP tool) with zero extra work. Never lifecycle-gated: an app in grace keeps serving, so its facts keep recording. `event_type` must be flat snake_case (`^[a-z][a-z0-9_]{2,63}$`) and cannot name platform-registered vocabulary (`400 RESERVED_EVENT_TYPE` — e.g. `mailbox_suspended` is off-limits), so an app can never impersonate a platform event; every consumer discriminates on `(source, event_type)`. The response's `next_actions[]` is always the platform-synthesized `poll` entry — an app-supplied payload can never inject its own. `idempotency_key` replays the original stored event with `deduplicated: true` instead of writing twice. - **A per-tier daily quota, pooled across the organization.** App-lane emission is bounded by a new `eventsPerDay` tier limit (prototype 1,000 / hobby 10,000 / team 100,000), summed across every project the org owns like every other capacity number. A denied emit is `403 QUOTA_EXCEEDED` with `details: {resource:"events_per_day", scope, used, limit}`. - **Both feed reads gain `?source` and `?event_type` filters.** `GET /projects/v1/:project_id/events` and `GET /orgs/v1/:org_id/events` accept `source=app|platform` and `event_type=[,]`, composing with `cursor`/`limit` unchanged — a filtered page never skips a later match, and `has_more` reflects the filtered view. --- ## 2026-07-15 (402 reserved for real payment challenges) - **HTTP 402 now means exactly one thing: a real x402 payment challenge.** The gateway's last three non-payment 402s are now 403 — the storage-quota trigger backstop (`P4020`, covers `/apply/v1` commits and `/apply/v1/service-asset-put`), the paid-function `insufficient_allowance` pre-dispatch denial, and both delegate spend-cap denials (`SPEND_CAP_PER_TX_EXCEEDED`, `SPEND_CAP_EXCEEDED`). Every error `code` is unchanged — only the HTTP status moved. **If your client branches on a literal `402` for a quota, tier-cap, allowance, or spend-cap denial, update it to check `403`** (and the response `code` field, as always) — a 402-parsing x402 client no longer mis-attempts payment against an unpayable denial. --- ## 2026-07-12 (keyless CI error reads) - **Your CI can watch the release it just shipped — no project key needed.** A GitHub-OIDC CI session now READS its own project's error surface: `GET /projects/v1/:project_id/errors` and `/:fingerprint_id` accept a CI session bearer for its bound project, so the post-deploy promote gate (`run402 errors --new-in --watch --fail-on-new`) is keyless from CI. A deliberate, scoped widening — a CI session already holds deploy authority, so reading the resulting fingerprints (ids, normalized templates, frame names only — never bodies or secrets) is strictly less power, and it completes the deploy→watch loop. Read-only and own-project-only: a CI session addressing another project gets 403, a revoked/expired binding gets 401 (the same per-request recheck as the CI deploy routes). Project apikeys are unchanged. --- ## 2026-07-12 (platform incident attribution) - **"My bug or the platform's?" is now a field on the response.** During an open platform incident, a matching gateway error envelope carries `correlated_platform_incident: { id, subsystem, status }` plus a `poll` next_action pointing at your project events feed — stamped identically on the managed-subdomain and custom-domain invoke paths. The platform states the correlation and lets you judge (an app can cause its own throttling); it never claims your code is fault-free, and the field is absent whenever no open incident correlates — never a false confession. - **Excluded platform faults are no longer a silent drop.** The failures the release-error-rollup classifier correctly keeps OUT of your app's fingerprints (rate-limit / allowance / admission denials during a platform degradation) are now counted per project. When a platform incident resolves — a debounced CloudWatch-alarm window or a human-declared incident — each affected project's feed gains one `platform_incident` event (new mandatory feed class, 365-day retention) carrying your real failed-invocation `impact.count`, never a fabricated number. - **Global incidents ride a feed overlay; health surfaces flag degradation.** A gateway-wide incident with no per-project attribution appears as a `platform_incidents[]` sidecar block on the events-feed read (open incidents plus ones resolved after your cursor, each with a stable id for dedup) — never interleaved into `events[]`, so your numeric cursor stays monotonic. `GET /agent/v1/operator/status`, `GET /tiers/v1/status`, and the events-feed envelope carry `platform_status: "degraded"` while an incident is open and relevant to you, and on no other 200. --- ## 2026-07-12 (rehearsal commit identity) - **Rehearsals now produce a bound, dated certificate.** Rehearsal upgrades its plan to reviewed mode, refuses stale facts with `409 REHEARSAL_PLAN_STALE` before creating a branch, and reports `plan_fingerprint`, `base_release_id`, `data_as_of`, and fidelity disclosures for absent secrets, sandbox/off email, and paused scheduled functions. - **The rehearsal golden path fails closed on drift.** `next_actions.commit_plan` now contains the exact `required_plan` body. Bound commits recompute the reviewed fingerprint immediately before mutation and return `PLAN_APPROVAL_MISMATCH` / `REVIEWED_FACTS_CHANGED` when declarable facts moved. Re-planning unchanged facts reproduces the certificate fingerprint after TTL expiry. - **Snapshot restore invalidates every open plan.** The atomic restore flip expires all uncommitted apply plans for the project, records `restore.plans_invalidated`, and returns `invalidated_plan_count` plus re-plan guidance. A pre-restore plan can no longer commit against a post-restore migration registry. --- ## 2026-07-11 (release error rollup) - **The platform now has durable error memory: `GET /projects/v1/:project_id/errors`.** Every gateway invoke choke point fingerprints failures at write time — uncaught throws (full stack identity via a new wrapper side-channel), init crash-loops and gateway-side invoke failures that never produce an app log line, and coarse handled-5xx — grouped by deploy-stable fingerprint (normalized message + stable frame names, never line numbers) and attributed to the release each identity was first seen under. Rows survive 90 days; CloudWatch is 14. - **A machine-checkable promote gate.** The response leads with a computed `verdict`: `new_in=` reports how many error identities were first observed under a release vs the previously ACTIVE release (activation-history baseline — rollback-safe), alongside `invocations_in_window` so zero errors over zero traffic is distinguishable from health, plus a fidelity `coverage` block. Every successful apply/promote response now carries the ready-to-run watch command (`run402 errors --new-in --watch 10m --fail-on-new`; exit 0 = clean, exit 1 = new fingerprints with samples and logs drill-downs). - **Error storms collapse; noise never bypasses your preferences.** Identical errors upsert one row (the founding corpus's 7,745-event cron storm is one row with `count: 7745`); new-fingerprint observations emit ONE coalesced `error_fingerprints_observed` event per (project, release) per 5-minute window to the project events feed and — preference-gated, never mandatory — to operator email/webhook. Platform denials (rate limits, auth gates) are never counted as your app's errors. - **Fidelity is honest and self-healing.** Each fingerprint declares its quality (`frame_names` / `message_only` / `coarse`); functions deployed before the error side-channel produce coarse rows and upgrade automatically on their next redeploy. The sanitized public 500 body is byte-identical to before — error detail rides an internal Lambda→gateway side-channel that never reaches clients. --- ## 2026-07-11 (project events feed) - **New cursored per-project events feed: `GET /projects/v1/:project_id/events`.** A durable, ordered outbox of operationally significant facts — deploy activations, mailbox suspensions, transfers, lifecycle cliffs, verification outcomes — written in the same database transaction as the state change they describe. One call answers "what happened to my project since I last looked": pass the previous response's opaque `cursor` as `?cursor=`; the envelope is `{events[], cursor, has_more, reset}` and every event carries platform-synthesized `next_actions[]` drill-downs. Accepted credentials: the project's own service_key, SIWX/control-plane principals with `project.read`, and scoped delegates. Read-only and never lifecycle-gated — a frozen project's feed stays readable. - **Org-wide sibling: `GET /orgs/v1/:org_id/events`.** Same envelope, unioned across every project the org owns. Principal-only (active org membership); project service_keys are rejected so deployed code cannot read sibling projects' events. - **An unusable cursor resets with a remedy, never a silent skip.** A malformed or retention-expired cursor returns `200` with `reset: true` and `earliest_cursor`, restarting from the earliest retained event. Retention is 90 days (365 for security/recovery/billing-critical/destructive-lifecycle/verification classes, stamped at write time). - **The feed introduces itself at deploy time.** Successful `/apply/v1` commits and release promotes now include a `next_actions[]` entry of `type:"poll"` pointing at the feed with a cursor positioned just before that deploy's own `deploy_activated` event — the first feed-native event type, emitted inside the activation transaction on every pointer flip (including rollback re-activations). --- ## 2026-07-11 (promote serving coherence) - **Cross-deployment promote now moves serving with the release pointer.** `POST /apply/v1/releases/:release_id/promote` atomically repoints every project-owned subdomain to the target release's deployment, writes the same durable edge-pointer outbox rows as apply activation, attempts KVS/CloudFront/Worker-KV propagation immediately after commit, and leaves failed targets retryable and visible through operation events. - **Promote refuses historical releases that can no longer serve.** A missing `release_state_snapshots` row or deployment row returns `409 PROMOTE_DEPLOYMENT_UNSERVABLE`; `details.missing` identifies the absent layer and `next_actions` points at re-applying the original spec instead of succeeding into stale/404 serving. - **Promote is edge-verifiable.** Successful responses include the standard `edge` convergence block, and `GET /apply/v1/operations/:operation_id/edge-coherence` accepts promote operations using the previous-live→target static-manifest diff. `run402 deploy verify --wait` can be used immediately after `run402 deploy promote`. - **Existing databases accept promote operations again.** Migration v1.121 replaces the legacy `release | asset | mixed` operation-kind constraint that survived under the intended promote constraint name, backfills historical operations to `apply`, and restores the `apply | promote` enum and `apply` default. - **Snapshot restore keeps its replacement slot leased.** The completed schema-slot pool claim/grace path is now on by default, so an atomic restore cannot point a project at a replacement schema whose pool row still says `free` and disappear from REST/project-key serving. - **Branch subdomains no longer block gateway restarts.** The v1.112 reserved-infix collision check now runs only on its first schema cutover; legitimate platform-created `--br-` subdomains no longer make the idempotent migration fail during a later deploy. - **Existing legacy restore drift self-heals at boot.** Before v1.116 reasserts the project pointer's pool lease, it moves any stale non-current lease for that project into 24-hour grace, avoiding a uniqueness failure and preserving the old schema for recovery. --- ## 2026-07-10 (recovery-event reachability) - **Mandatory notifications now reach org members, not just wallet contacts.** Recovery/security/billing-critical/verification notifications resolve recipients from the union of the verified wallet contact chain and the verified emails of active `owner`/`admin` org members. A wallet-less org (for example one created by an email project handoff) with a verified human owner now receives mailbox-suspension and other mandatory events instead of the event being skipped with no recipient. Preference-gated (digest/lifecycle) notifications are unchanged. - **Being unreachable is now loudly visible.** `GET /agent/v1/operator/status` gains an `operator_reachability` block (`reachable`, `verified_recipient_count`, `sources[]`, `skipped_last_90d`) plus a `next_actions[]` registration remedy when unreachable; `GET /tiers/v1/status` gains an `operator_unreachable` advisory in a new top-level `advisories[]`; and a suspended mailbox whose suspension notification could not be delivered gains an extra `suspended_recovery_actions` entry pointing at operator-contact registration. `run402 doctor` surfaces the same signal. - **The operator-contact verification challenge is deliverable under the default inbound policy.** Platform notification senders (e.g. `billing@mail.run402.com`) are implicitly admitted through a mailbox's `reply_only`/`allowlist` inbound policy when the SES receipt's DKIM verdict passes, so operators no longer need to open the policy to receive the verification challenge. A spoofed platform From without a passing DKIM verdict gets no bypass. - **New `mailbox_suspended` email trigger event.** ReleaseSpec email triggers (`functions.*.triggers[]` with `type: "email"`) accept `mailbox_suspended` alongside `reply_received`/`delivery`/`bounced`/`complained`: an abuse suspension creates a durable function run carrying `suspended_reason`, `suspended_at`, `evidence`, and `recovery_actions`, so a project's agent observes its own outage without polling or a public webhook URL. The run executes independently of the suspended mailbox's send capability. The suspension webhook payload gains the same `suspended_at` + `recovery_actions` fields. --- ## 2026-07-10 (mailbox suspension notifications) - **Every mailbox suspension now notifies the project operator.** The `mailbox_suspended` operator notification was previously emitted only when the suspended mailbox held a sender role (`default_outbound` or `auth_sender`), so a project whose suspended mailbox held neither role — for example a dedicated inbound/signing mailbox — was suspended silently. Suspension (bounce-threshold or complaint) now always enqueues the operator notification; the notification metadata still lists which roles, possibly none, the mailbox held. The `mailbox_suspended` webhook event and the `suspended_reason` / `suspended_evidence` / `suspended_recovery_actions` fields on `GET /mailboxes/v1/:mailbox_id` are unchanged. --- ## 2026-07-09 (tenant x402 priced-route payment errors) - **Insufficient buyer funds now have a stable agent-readable shape.** Tenant priced-route settlement/verification failures that Run402 can identify as a payer USDC shortfall now return `402` with `code:"payment_insufficient_funds"`, `amount_usd_micros`, `network`, `asset`, and a `next_actions[]` entry `{type:"fund_wallet", network, asset, minimum_amount_usd_micros}` instead of requiring agents to parse a raw facilitator `execution reverted` string. --- ## 2026-07-09 (paid function idempotency) - **Paid user-functions now have a billing-first idempotency fence.** Paid direct invokes require `Idempotency-Key`, canonicalize JSON request intent, exclude auth/payment proof material from the digest, and create an invocation record before allowance hold or dispatch. The contract is exactly-once billing per accepted key/generation, no intentional duplicate dispatch, and explicit terminal states: `succeeded`, `dispatched_failed`, `failed_pre_dispatch`, `ambiguous`, `response_expired`, and `fence_expired`. - **Paid invokes run through the durable function substrate with a synchronous facade.** A fresh paid call creates a function run with one attempt, waits briefly for a result, and otherwise returns `202 idempotency_in_progress` with `run_id`/`operation_id` and `next_actions[]`. Replaying the same key returns the retained result or recorded error; conflicting payloads return `409 idempotency_conflict`; insufficient allowance and other pre-dispatch failures release the key for pay-then-retry. - **Failure handling is safer for coding agents.** Paid Lambda invokes use one AWS SDK attempt and explicit timeout classification. Lost transport after dispatch becomes `ambiguous` and is never redispatched by the gateway; callers should poll the operation or use a new business-level dedupe token inside their own function payload before spending a new key. --- ## 2026-07-07 (tenant x402 priced routes) - **Function web routes can now charge a fixed tenant x402 price before handler execution.** Release route specs may attach `pricing:{mode:"always",amount_usd_micros,pay_to:"org_default_payout"}` to function targets, for example `250000` for $0.25 per action. Static aliases and direct `/functions/v1/:name` invocation are intentionally not monetized by this surface. - **Organizations have an explicit default payout wallet for priced routes.** `PUT /orgs/v1/:org_id/payout-wallet` lets org admins/owners set or clear the wallet used for tenant route settlement, with step-up/fresh SIWX and recovery previews for `PAYOUT_WALLET_REQUIRED`, `PAYOUT_WALLET_AMBIGUOUS`, and unlinked-wallet cases. - **Paid routed functions receive typed payment context and auditable payment history.** `@run402/functions` exposes `getRoutedPaymentContext()` / `routedHttp.paymentContext()`, and `GET /projects/v1/:project_id/tenant-payments` lists redacted project payment records for org developers or read-scoped grants/delegates. - **Priced-route payment failures now return actionable JSON.** Rejected paid proofs return `TENANT_X402_PAYMENT_INVALID` with `details.x402_error` decoded from the x402 challenge, so cases such as `self_send_not_allowed` tell callers to use a buyer wallet distinct from the tenant payout wallet. - **Function lists expose the injected helper runtime floor.** `GET /functions/v1` and `GET /projects/v1/admin/:project_id/functions` include recorded/current/minimum `@run402/functions` runtime versions plus `runtime_stale`; new gateway deploys inject at least `@run402/functions@3.7.0`. --- ## 2026-07-05 (operator health and recovery bug fixes) - **Operator status works against the current org-owned lifecycle schema.** `GET /agent/v1/operator/status` no longer reads retired per-project lifecycle columns, so authenticated `run402 doctor` operator-health and runtime-staleness checks return a health snapshot instead of a production 500. - **Snapshot restore records migration registry rows under a valid apply operation.** The restore flow now writes its synthetic operation with the supported `apply` kind before resetting `internal.applied_migrations`, preventing restore confirmation from failing with an `applied_migrations_operation_id_fkey` error. - **Project creation avoids hot-path tombstone deletion during slot allocation.** Schema-slot allocation no longer deletes terminal project tombstone rows while creating a new project; recycled restore-grace slots are still reused, and other allocations draw from the sequence. This avoids surfacing deferred content-reference FK cleanup races as unrelated project-provision failures. ## 2026-07-04 (drop exposed tables in one release) - **A single release can now migrate-drop an exposed table.** Committing a release whose migration `DROP`s a table (or function) that the same release removes from `database.expose` previously failed with `EXPOSE_FAILED` and the unhelpful message "current transaction is aborted, commands ignored until end of transaction block". Expose-manifest removals now tolerate objects the release's own migrations already dropped — grants, policies, and triggers die with the object, so the removal is already converged — and migrate + un-expose ships in one apply. - **EXPOSE_FAILED envelopes report the real failure.** The first error in the expose transaction surfaces instead of the aborted-transaction follow-on; manifest-vs-schema mismatches (undefined table/function) carry a `next_actions` `edit_request` explaining that `database.expose` is converged after this release's migrations run; and when migrations committed before the expose failure, the envelope reports `mutation_state: "partial"` with `details.migrations_committed_before_expose_failure: true` plus a `restore_snapshot` next_action when a pre-migration snapshot exists (previously it claimed `rolled_back`, hiding the committed DDL). ## 2026-07-04 (edge coherence and release identity) - **Deploy responses now explain public-edge convergence.** Apply commit responses and operation reads include an `edge` block that reports whether public mutable surfaces are still converging, exposes durable pointer-update status for managed subdomain KVS, CloudFront invalidation, and custom-domain Worker KV propagation, and links to `GET /apply/v1/operations/:operation_id/edge-coherence` for verification. - **Static releases expose verifiable release identity.** Gateway-served stable static responses and custom-domain Worker responses now stamp `x-run402-release-id` and `x-run402-release-generation`; materialized S3 objects carry matching `run402-release-*` metadata. Tenant hosts also serve `GET /_run402/release.json` with `{project_id, release_id, release_generation, activated_at}` and `Cache-Control: no-store`, while user static files/routes under `/_run402/` are reserved. - **Edge pointer propagation is durable and retryable.** Activation records KVS, CloudFront, and custom-domain KV updates in `internal.edge_pointer_updates` before the release flip commits. The first post-commit attempt records success/failure, failed targets emit operation events, and the lease worker retries until recovered so agents can distinguish propagation lag from a lost pointer write. ## 2026-07-04 (data snapshots, branches, and apply rehearsal) - **Migration-bearing deploys can be rehearsed on contained branches.** `POST /apply/v1/plans` now advertises `rehearsal: { available, rehearse_url }` for persisted migration-bearing plans. `POST /apply/v1/plans/:plan_id/rehearse` snapshots the source project, creates a short-lived branch, applies the candidate plan there, runs built-in and plan-declared checks, and returns a report with exact next actions. The source project and source plan remain untouched; CI sessions are rejected in v1 so the rehearsal runs under wallet/control-plane/delegate authority. - **Per-project snapshots are first-class restore points.** New `/projects/v1/:project_id/snapshots*` routes support manual create/list/get/delete plus a two-step restore plan/confirm flow. Snapshots capture project data, migration registry rows, auth users/passkey public credentials, and live release id while excluding sessions, tokens, and secret values. Apply commit creates a pre-migration `restore_point` before DDL when possible; oversized/explicitly skipped cases return `snapshot_skipped_reason`. - **Branches are contained project copies for inspection and collaboration.** New `/projects/v1/:project_id/branches*` routes create/list/renew/delete expiring branches from live data or a snapshot. Branches use derived noindex hosts, sandbox email by default, leave scheduled functions off unless requested, and expose branch context to functions with `RUN402_BRANCH=1` and `x-run402-branch`. --- ## 2026-07-04 (domain-wide mailbox addresses) - **Custom mailbox domains can now cover every mailbox in a project.** ProjectDomain `desired.email.mailbox_addresses` accepts `all_mailboxes:true` with custom mode after email receive is domain-wide, so every current and future mailbox local part can send from `local@your-domain.com` without enumerating slugs. Validation is email-plane scoped: web DNS conflicts still report clearly, but they no longer block verified email receive and mailbox-address activation. - **Verified legacy inbound domains are healed on deploy.** Migration `v1_111` restores ProjectDomain state for projects that already had DKIM-verified, inbound-enabled custom domains before the ProjectDomain consolidation, enabling effective domain-wide mailbox addressing while preserving the managed address fallback for receiving replies. --- ## 2026-07-04 (content-tracked deploy migrations) - **Generated and seed SQL can now use content-tracked migration names.** New SDK/CLI/MCP authoring surfaces accept a migration `name` for idempotent, generated, or seed SQL. Clients compile it to a wire `id` of `_`, so unchanged content noops and changed content applies as a new migration without colliding with the old checksum. Versioned `id` migrations remain the immutable default for hand-authored schema changes. The HTTP API request schema remains unchanged: gateway deploy specs still carry concrete migration `id` values only. - **Migration checksum mismatches now include recovery instructions.** `MIGRATION_CHECKSUM_MISMATCH` responses now enumerate every conflict in `details.conflicts[]` with `id`, `spec_checksum`, `registry_checksum`, and `applied_at`, and include `next_actions[]` for the three supported fixes: switch generated SQL to `name`, mint a new `id` for a revised versioned migration, or ask an operator to adopt a legacy pre-applied migration. --- ## 2026-07-04 (tenant test sessions) - **Tenant auth can now be tested without real inboxes or human ceremony.** Service-key callers can mint deterministic watermarked users with `POST /auth/v1/test-sessions`; labels derive `