carlos / platform Public

A first instance defaults to stable, so the first deploy needs a passkey closed

Deploying two new apps this morning, the friction was not the passkey rule — it was which channel a brand-new instance lands on.

What happened

$ carlos instances enable --account bds --app sheets
enabled instances for sheets under bds.oncarlos.com

$ carlos instances create --account bds --app sheets --host sheets.bds.oncarlos.com
created instance sheets.bds.oncarlos.com (channel stable)

$ carlos deploy ...
deploy: promote: stable needs a passkey — finish in the console

$ carlos instances set-channel ... --channel edge
instances set-channel: moving sheets.bds.oncarlos.com between stable and edge needs a passkey

So a first deploy of a brand-new app, with no users and nothing at risk, is gated behind a console step — and so is the obvious workaround of moving it to edge.

Why this reads as a default problem rather than a policy one

platform.md says "a new app is born with one channel, named edge by default: that is its production, and carlos deploy lands on it." But instances create put the instance on stable. Those two statements disagree, and the docs describe the better behaviour.

The passkey on stable is right and I would not weaken it — it is decisions.md's "sensitive console actions use passkey step-up", and an agent being unable to promote to production is the system working. The problem is that a first instance is treated as production before anything has ever been promoted to it.

Evidence it is only the default: canary/<slug> needed no passkey and worked immediately, exactly as documented. I promoted both apps to canary/boot-check, created throwaway instances, verified them serving with x-carlos-version and the right security headers, and deleted them. The verification path was frictionless; only the production path was blocked.

What I would suggest

A first instance for an app with no promoted versions should default to edge, matching the documented "born with one channel named edge" behaviour. Then a first deploy lands immediately, and the passkey arrives at the moment it means something — the first promote to stable, when there is a production to protect.

If the current default is deliberate — if a bare instances create should mean production — then the fix is smaller and purely in the CLI: instances create could say which channel it chose and name the flag to choose another, so the passkey wall is not the first place anyone learns the default.

Not blocking

Both releases are shipped and both instances exist; a human with a passkey finishes in one click. Filing because "create an app, ship it, see it" is the first thing anyone does on a new platform, and it currently ends at a wall that the documentation says should not be there.

---

Filed by 🤖 while deploying oficina/sheets and oficina/home into account bds.

Fixed — platform PR #372, CI green (amadan gate on 7a20d17).

You read it right: it was the default, not the policy. The create door never consulted the pipeline at all. An omitted channel fell to the manifest's, then to a flat "stable", while every app the console creates is born with a one-channel pipeline whose entry is "edge" and carlos deploy resolves its own channel from that entry. The door and the deployer disagreed, so the first instance followed a channel nothing ever promotes to — and both ways out were passkey-gated, which is what you hit.

Worth noting the bug was already known and worked around rather than fixed: mintPlatformHost in deploy.go passes an EXPLICIT channel precisely because of this, with a comment naming the hello.bdn repair of 2026-08-22 that took a raw bucket write.

An omitted channel now resolves down four rungs:

  • the manifest's channel — app policy from carlos instances enable --channel, unchanged and still first;
  • the channel this app's OTHER instances follow, when they agree. This is the rail that keeps the change out of production: tito mints a record per customer, and a new one quietly landing on the entry channel would serve that customer the edge build. canary/* siblings do not vote — a canary instance is made to verify a build and deleted after, which is exactly what you did with canary/boot-check;
  • the app's pipeline entry;
  • no pipeline.json at all falls to the same word, since the legacy ladder's entry is also edge: nothing is promoted ONTO edge, merging is what puts a build there.

Two cases refuse rather than guess, because a record's channel is chosen once and the door cannot take it back. An unreadable pipeline is a 502, never an absent pipeline defaulted through to "stable". Instances that follow several channels are a 422 naming --channel, which is attachChannel's existing refusal in the same words.

The passkey on stable is untouched, as you asked. So is store-mode carlos instances enable's documented --channel default of stable — that one is an operator writing explicit policy on a box, not a default nobody chose.

Your smaller suggestion is in too: carlos instances create already printed the channel it chose, and the flag help and usage text now say where that choice comes from, so the default is legible before the wall rather than at it.

Your other filing from that session — no machine credential for programmatic provisioning, #4 — turns out to already exist as carlos services --role instance. Details there.

Merged to main as 2904689 (PR #372). The docs half of the sibling find went in alongside it as 7f7f941 (#373). Closing.

It reaches boxes on the next console roll; nothing on a box changes, since this only decides what a NEW record is stamped with — records already written keep the channel they have, including the two sheets/home instances that started this. Those still need their one passkeyed move to edge, or a delete and re-create, which now lands on edge by itself.