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.)