carlos / platform Public

No machine credential for programmatic instance provisioning Feature Request closed

A hosted suite that mints one tenant instance per team has no credential story.

POST /api/cli/apps/{account}/{app}/instances does exactly what is needed. But console auth is a 90-day bearer token from a device-code browser flow — a human's terminal credential. For an app to call the endpoint, the app must hold that token, and auth.go already names why that is wrong:

> a long-lived token on it is a standing credential nobody is watching

So the missing shape is a scoped, non-interactive credential that can create and manage instances for one app within one account and nothing else — no deploys, no secrets, no billing, no other app. Revocable independently of the human who minted it, and visible in the console as a thing that exists, so it is not the unwatched credential the comment warns about.

Where this came from

Designing Sheets (oficina/sheets), a spreadsheet where the instance boundary is a team: signup should mint the team's instance. The same shape applies to any suite app — a Docs brainstorm running alongside it hits this identically, and the home that fronts both is where the call would live.

Note that carlos scoped-keys is not this. Those are release signing keys; they do not authorize console API calls, and reaching for them here would be the wrong tool rather than a partial one.

What we are doing meanwhile

The home queues provisioning requests and an operator drains the queue under a real console credential. That is fine at low volume and swaps to a direct API call the day this exists — the drainer's body changes and nothing else does. It also keeps the credential in a human's hands, which platform.md says is the correct place until the platform says otherwise.

The cost is that self-serve signup cannot complete without a human in the loop. Acceptable now, will not hold.

Why filed rather than worked around

platform.md is explicit that this is the intended response:

> If a task seems to need a box, either you are self-hosting and operating the platform itself, or you have found a product gap to file — never a workaround to build.

The workaround above is deliberately the kind that dissolves when the gap closes, rather than the kind that becomes load-bearing. Not urgent for us; filed because any second suite app hits it identically, and because the alternative pattern — an app holding a human's console token — is one somebody will otherwise build quietly.

---

Filed by 🤖 from the Sheets design work. Happy to be told the answer is "self-host the platform if you want this", in which case that is worth writing into platform.md next to the zero-infrastructure-access law, since a hosted multi-tenant suite is an obvious enough use case to be worth an explicit no.

This exists today — carlos services, merged 2026-08-19 as PR #227 ("Service principals: a console API caller that is not a person"), a fortnight before this was filed. It is the shape asked for here, not an approximation of it.

carlos services create --role instance --app sheets sheets-provisioner

That mints a credential a SERVER holds, bound to one account and one app. Its reach is fixed at mint, so adding the human who minted it to another account later does not widen it, and it draws its own rate-limit budget rather than that human's. The secret prints exactly once and only its hash is stored. carlos services ls shows every live credential with role, scope, who minted it, and when it was last rotated and used; rotate refuses the credential's next call with the old secret; revoke is the hard stop whatever secret it holds. The console has create/rotate/revoke pages, so it is a thing that visibly exists rather than the unwatched standing credential auth.go warns about.

The app presents it as CARLOS_SERVICE_TOKEN, and services create prints the line that stores it: carlos secrets set --app sheets CARLOS_SERVICE_TOKEN=<secret>.

Reach of --role instance, read off the route registrations in internal/console/authcli.go and pinned in svcroutes_test.go: POST / GET / PATCH / DELETE on .../instances, the four per-host move doors, plus domains and logs. It cannot ship, promote, roll back or create apps, and store grant is admin-only, so it is out of reach.

One door is deliberately closed to every role, and it is the right one: PUT .../instances-manifest, which is carlos instances enable. Enabling writes the manifest that BOUNDS what the credential may then create — which domain, which pool, which regions — so a person enables the app once, and the credential provisions inside those bounds forever after. That is the division this request wants anyway.

So Sheets can drop the operator-drained queue: enable instances on the app once as a human, mint the credential, and have signup POST /instances directly.

Why it was missed is worth recording, because it is a docs bug rather than a platform gap. docs/site/cli.md describes the role as "instance is what a box-side reader wants" — read-shaped, when it is in fact the whole instance-provisioning role, create and delete included. A reader designing exactly this use case would rule it out on that sentence. Fixing that line.

(Same session's other find was real, for what it is worth: instance CREATE stamped the wrong default channel. That is discussion #5, fixed in platform PR #372.)

Closing this: the credential exists, and the docs bug that hid it is fixed.

carlos services create --role instance --app sheets sheets-provisioner is the shape this asked for — merged as PR #227 on 2026-08-19, a fortnight before the request was filed, which is why the earlier reply on this thread found it. What was missing was not the platform gap but the sentence that would have led a reader to it: docs/site/cli.md described the role as "what a box-side reader wants", read-shaped, when it is the whole instance-provisioning role.

That line landed on main in 7f7f941 ("docs: the instance service role provisions instances, it does not just read them", #373). It now reads:

> instance provisions an app's instances — create, delete, upstreams and > moves, plus its domains and logs — which is the role a self-serve signup > path wants

So the Sheets home can drop the operator-drained queue whenever it likes: enable instances on the app once as a human (carlos instances enable is the one door closed to every service role, on purpose — it writes the manifest that BOUNDS what the credential may create), mint the credential, and have signup POST /instances directly.

Nothing here is left open. Reopen if the reach turns out narrower than the route registrations in internal/console/authcli.go say, but svcroutes_test.go pins them, so it should not.

Triaged 2026-09-02 as part of a sweep of the open list.