Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

#167

Give a job a credential for the bot it acts as

merged · by keenan 6d ago · merged into main at 6e1fca51 by keenan 6d ago

keenan opened this 6d ago

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 reserved env names.
  • Queue (ci/runs): bot pinned with the run; a run_credentials table holding the public half, with SetRunCredential, 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; exec allows a credential session only git and vole-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 on credential_error, and masks the key in the log.
  • Docs: the reference’s bot section and reserved variables, the agent example with bot:/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 verify passes 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.

Discussion (1)

keenan 6d ago

Correcting the authorization table in the description: the first version refused every change to the issues branch, including labels and state, for every credential. That was wrong — it was the browser’s write surface, not the role, and the decision on #165 was that a bot inherits its role.

The rule now is the account’s capabilities, asked the same way the browser asks them (the ACL’s own CanDiscuss, CanOpenRequest, CanTriage, CanApprove):

  • a maintainer’s job may do everything a maintainer’s key may do: triage any issue, set labels and state, edit any change, approve. Nothing extra is refused;
  • a contributor’s job may discuss, file an issue, manage the item it wrote, and open a change from its own branch. Somebody else’s issue is still a maintainer’s;
  • neither may reach admin surfaces, secrets, the queue, or a merge, because those are not repository writes.

The hook gains no second permission model: the credential removes the one requirement that made every metadata write a maintainer’s act, and the role decides the rest. Tests now cover the cases that distinguish the two — a contributor closing its own issue (allowed), a contributor closing somebody else’s (refused), and a maintainer closing somebody else’s (allowed).

Everything else in the description stands: minting, delivery, expiry, the per-run binding, the job environment, and the run-page line.