carlos / platform Public

CI cannot promote through the console without gaining beta: service credentials need a channel scope (or direct promotes need to reach fast converge) Feature Request open

titogo's CI (teamtito/titogo .github/workflows/deploy.yml) promotes every merge to edge with carlos promote in direct-bucket mode. It holds a scoped release key (release-keygen -scope canary,edge), which structurally cannot sign beta or stable.

Direct mode never calls release.BumpSerial, so fastConvergeLoop (internal/host/agent.go) never fires. Boxes pick the promote up only on carlos-adopt.timer: hourly by default, and on carlos-tito-ie a hand-tightened 2min + 30s drop-in (canary-cadence.conf) that makes it bearable. That adds 1–2.5 minutes to every titogo deploy. Measured on the box 2026-10-05: edge adopts land on the timer's cadence, not within seconds.

Why titogo can't just promote through the console

POST /api/cli/apps/{account}/{app}/promote (handleAPIPromote) applies the ladder, writes the audit line and bumps the serial, so the box would adopt in seconds. But the only credential CI could hold there is a publish service principal (svcrole.Publish, bound to one app):

  • For an app with no pipeline, it can promote and roll back beta. Only stable is refused.
  • Through the same role it can also trash and restore the app, set retention, and set version-target (authcli.go route table).

Moving CI to the console would widen its authority from "canary and edge pointers" to "every rung but stable, plus app lifecycle". The README also tells CI to use direct mode, which is exactly the path that doesn't bump the serial.

(Declaring a titogo pipeline with a passkey rule on beta would block beta for the token. But it changes how humans promote beta and still leaves trash, restore and retention reachable, so we'd rather not lean on it.)

Ask: either of these closes the gap

  1. A channel scope on service credentials, enforced at promote, rollback and ship. - Mint with e.g. --channels canary/*,edge, mirroring the scoped release key's grant, and refuse out-of-scope channels at the door the same way an app mismatch is refused. - Ideally also a role narrower than publish for deploy pipelines: ship, promote and read channels; no trash, restore, retention or version-target.
  2. Or: make direct-bucket promotes visible to fast converge. Either: - the CLI's direct promote bumps control/serial (today CI's IAM is scoped to apps/<sqid>/<app>/* and cannot write it, and that may be the right boundary), or - fastConvergeLoop also polls the channel pointers its routes follow (an ETag GET per followed channel), so any promote path converges in seconds.

(2) would also fix session canary promotes, which take the same direct path. With either fix, carlos-tito-ie can drop its carlos-adopt.timer drop-in.

Context: titogo PR "deploy: restart the router first in the fleet sweep" (teamtito/titogo#3872) fixes the other half of the deploy delay on titogo's side. Until this lands, titogo CI stays on direct-bucket mode with the scoped key; we are not working around it.

Decision from Paul (2026-10-05): option 1, a console credential CI can hold, limited to canary and edge. Not option 2.

The reasoning:

  • Direct-bucket mode stays, as the operator's tool. The signed pointers in the bucket are what boxes trust, and the console sits on top of them. Direct mode is for setting up a deployment before a console exists, deployments with no console, recovery when the console is down, and the platform's own releases.
  • It should not be the normal CI path. A direct promote skips the console's audit line, pipeline rules, pending approvals and the serial bump. Every CI deploy that bypasses the console is one the console can't account for. On Carloku, customers never hold bucket credentials, so the console is the only way in for them, and their CI needs exactly what titogo is asking for.
  • 2a (direct promote bumps the serial) is declined. Making the bypass faster makes it more attractive.
  • 2b (poll every followed channel pointer) is declined. Today an idle box makes one serial check per 2-second tick. 2b multiplies that by every followed channel on every box, and avoiding that cost is why the serial exists.
  • README.md:170 changes with the feature. It currently tells CI to use direct mode ("logging in is for a human at a laptop, never a box or a pipeline"). That was right when the console only had human logins. Once a scoped CI credential exists, the README will point CI at the console and describe direct mode as operator and break-glass only.

Next: a spec for the credential on a design- branch, linked here when it's up. Until it lands, titogo stays on direct mode with the scoped key and the 2-minute timer drop-in. That is slow but correct, and not a workaround.

What I verified on main (8d8e4bae): direct promote never calls release.BumpSerial, and svcrole.Publish reaches promote, rollback, ship, retention, version-target and trash (internal/console/authcli.go:482-549).

The spec is up on branch design-deploy-credential: docs/superpowers/specs/2026-10-05-deploy-credential-design.md (rev 1, d30a74c2). It is awaiting Paul's review and an independent read by Astra. No code yet.

For the titogo session, the parts that touch you:

  • Mint: carlos services create --role deploy --app titogo --rungs canary,edge <name>. Ship, plus promote and rollback on canary and edge only. No beta, no hotfix, no trash, retention, version-target or domains.
  • CI: set CARLOS_SERVICE_TOKEN and CARLOS_CONSOLE, drop CARLOS_DEPLOYMENT_BUCKET, the scoped key and the grant. The CLI refuses if the token and a store variable are both set.
  • What it claims: adoption within seconds of the promote. titogo's units then restart on controld's watch as they do today.
  • Three things to confirm before you switch (§13): the artifact fits the console's 256 MiB ship cap, the runner can reach Tito's console, and which doors carlos canary needs.

Nothing to do on titogo until this is merged, released and rolled to Tito's console.

Implementation is done on design-deploy-credential (5f0e7a3d). It is not merged or released yet; that waits for Paul.

For the titogo session, what you'll use once it's rolled to Tito's console:

  • Mint (an owner, once): carlos services create --role deploy --app titogo --rungs canary,edge <name>.
  • CI: set CARLOS_SERVICE_TOKEN and CARLOS_CONSOLE. Remove CARLOS_DEPLOYMENT_BUCKET, the scoped key and the grant. The CLI refuses if the token and a bucket variable are both set.
  • Commands: carlos ship + carlos promote work, and so does carlos deploy (Paul's ruling R6). The first deploy of a brand-new app still needs a person. A deploy credential can't create the app, enable instances or attach domains, and the watch's log-based diagnosis is skipped.
  • What it can move: promote and rollback on canary/<name> and edge only. No beta, stable, hotfix, trash, retention or domains.
  • Signing: each promote is signed by a scoped grant minted for that request, so the boxes verify the rung limit the same way they verify your scoped key today.
  • Before switching, check two things (spec §13): that titogo's artifact fits the console's 256 MiB ship cap, and that the GitHub runner can reach Tito's console.

Once edge promotes are adopting in seconds on carlos-tito-ie, canary-cadence.conf can go.

Merged to carlos/platform main as d1dab9a, and the GitHub mirror is fast-forwarded. It isn't released or rolled yet. The steps in spec §13 still apply in order: release the platform, roll Tito's console, mint the deploy credential, then switch titogo's CI.