What
A workflow can name the bot a job acts as:
jobs:
agent:
image-file: agent
network: bridge
timeout: 2h
bot: coding-agent
steps:
- run: run-the-session
At lease time the instance generates a key for that run, stores the public half with
(account, repository, run, expiry), and sends the private half on the lease channel.
The runner writes it to /run/vole/id_bot with a /run/vole/known_hosts entry for the
instance, points GIT_SSH_COMMAND/VOLE_SSH_COMMAND at both, and sets VOLE_RUN_ID and
VOLE_REPO. A step’s git push and vole issue comment then authenticate as the
account with nothing wired per job.
The account’s role is still the permission. The credential replaces exactly one thing:
writing to the issues branch no longer requires maintain. That is decided in
pre-receive, because every write passes it.
The authorization, in one table
| A job’s credential may | Condition |
|---|---|
| add a comment to an issue or a change | the comment’s author is the account |
| open a change request | the author is the account, the change follows a branch, and the branch is one the account could push |
| approve a change | the account is an eligible approver (maintain), and the file names it |
| change or remove anything else | never |
| touch an issue’s state, labels, or assignees | never |
| reach an admin surface, secrets, the queue, or a merge | never |
Only a live credential row scopes a session: an expired key reads as absent, so the
push falls back to the account’s own role and is refused for anything a contribute
account cannot do. A credential is also bound to one repository, so a job cannot reach
outside the work it was given.
Why
This is the last piece the agent loop needs. Before it, a job that wrote back needed
the account’s long-lived key in a repository secret, so the account had to be
maintain — push to shared branches, approve anyone, close anyone’s issue — and any
job declaring the secret could read the key. Now the key exists for one run, is scoped
to what the account may already do through the browser, and is gone when the run ends.
Shape
- Format (
ci/workflow.go):Job.Bot, name-shape validation at push, and the four credential variables added to the reservedenvnames. - Queue (
ci/runs):botpinned with the run; arun_credentialstable holding the public half, withSetRunCredential,RunCredential,DropRunCredentials, and an expiry sweep. - Server (
server/ssh): the lease resolves the account (exists, active, a bot), mints the key, and fails the job with a line when it cannot; the host key line rides with the job; the auth callback resolves a credential fingerprint to the account and marks the session;execallows a credential session only git andvole-whoami, and only for its own repository; the finish drops the key. - Hook (
server/hook/scope.go): the additive rule above, reusing the existing approval and comment ownership checks rather than replacing them. - Runner (
ci/agent): writes the key and known_hosts, sets the environment, fails early oncredential_error, and masks the key in the log. - Docs: the reference’s
botsection and reserved variables, the agent example withbot:/network:/timeout:, and the bot section in Accounts and access.
Not in this slice, and worth saying
- There is no end-to-end test of a job writing back: that needs a runner and a container,
which the server tests do not have. The hook’s authorization, the credential store, the
auth path and the job’s environment are tested separately; the dogfood workflow is what
proves the whole loop, and it is blocked on the instance running a binary that knows
network:. - No UI beyond one line on the run page saying which account the run acted as.
- A repository rename during a run leaves the credential naming the old name, so it is refused. Failing closed is the right answer for a rename mid-run; a follow-up can move the row.
Checks
go build ./...,go vet ./...,gofmt -l .clean.vole workflow run verifypasses in the pinned image.- New tests: the credential store (scope, expiry, reissue, sweep),
bot:parsing and the reserved variables, the job’s environment and known_hosts line, and six hook tests covering the table above — own comment accepted, comment as someone else refused, issue edit refused, own-branch change accepted, another account’s branch refused, approval without maintain refused, another repository refused, expired credential refused.
Closes #165.