Why
An automation needs an account to act as: to be assigned an issue, to be mentioned, to author a comment, to open a change, and to be attributable in Git and the audit log. An account already gives all of that, so the identity half is free. What is missing is a way for a workflow to act as one without holding a long-lived key that carries more power than the job needs.
Today the only way for a job to write back is a repository secret holding the
account’s SSH key. The account must be maintain, because a push to the metadata
branch requires it — so the bot can also push shared branches, approve other
people’s changes, and close anyone’s issue. The key cannot be revoked when the run
ends, and every job in the repository that declares the secret can read it,
including an approved contributor tip.
This is the managed-bot slice of the agent loop: an account that a maintainer provisions, plus a credential the instance mints per run and the run’s job can use.
Decisions
- Credential: a per-run keypair. The instance generates an ed25519 pair when it
leases the job, stores the public half with
(account, repo, run, expiry), delivers the private half the way a secret is delivered, and deletes the row when the run finishes. No CA, no OpenSSH certificates: it rides the same key lookup the SSH server already does. - Permissions: the account’s role, inherited. No bot-specific CI rule, no
secrets rule, no separate branch namespace. A
contributebot may push onlyrefs/heads/contrib/<name>/, and CI on that tip is held until a maintainer allows it, exactly as for a person. Amaintainbot may push shared branches, its pushes run CI immediately, and its runs receive the secrets its workflows declare. - Approvals: allowed when the role allows it. A
maintainbot may approve a change. It still cannot approve its own, and the approval is recorded against the bot’s name like any other. - Granting: the instance admin who creates the account assigns the repository role. There is no per-bot capability store and no second permission model.
The one rule the credential adds
A run credential lets the CLI do over SSH what the account’s role may already do
through the browser: add a comment, open a change request for a branch the account
may push, and approve when the account is an eligible approver. That replaces “the
metadata branch needs maintain” for automation without widening the account’s
role. A contribute bot can comment and open a change for its own branch; it cannot
touch an issue’s state, labels, or anyone else’s comment, and it cannot approve.
The gate is pre-receive, where every write already passes: the SSH server marks a session that authenticated with a run credential, and the hook authorizes the metadata paths the browser already authorizes for that role. Everything the browser does not offer an account stays refused.
Slices
- Account kind.
kindon the account (person|bot), fixed at creation; a bot has no browser sign-in (no passkey, no invite link) and no instance-admin role; a bot is marked as one in the account directory, the profile,whoamiconsumers and the admin list;vole admin account create --bot. - Run credential. Mint, deliver, expire; the auth callback resolves it; the
hook authorizes by role and path; the job gets
VOLE_RUN_IDandVOLE_REPO, plus a scoped key and known-hosts entry under/run/volesogit pushandvole issue commentwork with no per-job wiring. - Docs and dogfood. Accounts and access gets a bot section; the agent example
in
docs/workflows.mdpoints at it; one agent workflow in this repository proves the loop (blocked on the instance running a binary that knowsnetwork:before it can declare it).
What a maintainer is accepting
- A
maintainbot’s pushed code runs with the repository’s secrets and can approve changes. That is whatmaintainmeans, and granting it is the trust decision. - A
contributebot’s CI is held per exact tip, so an unattended loop needsmaintainor a person clicking Allow CI after each push. - A leaked job tree holds a key that is valid until the run ends, not a key that is valid until someone notices.
- A bot that reads untrusted issue text and then pushes code is a prompt-injection path to whatever its role allows. The role is the blast radius; choose it deliberately.