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/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

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

Give a job a credential for the bot it acts as

6e1fca5115b709d31eeb51864b187d79aa104e5f · Keenan Nemetz · 6d ago · parent 446542e6

Give a job a credential for the bot it acts as

A workflow can name a bot account with `bot:`. When the job is leased the
instance generates a key for that run, stores the public half with the run,
repository and expiry, and sends the private half on the lease channel. The
runner writes it into the job's run directory with a known_hosts entry for the
instance, so a step's `git push` or `vole issue comment` authenticates as the
account and nothing has to be wired per job.

The account's role is the permission, in full. The credential replaces exactly one
requirement — that writing to the issues branch needs `maintain` — and adds no rule
of its own. What a job may write there is what its account may do, asked the same
way the browser asks: discuss, file an issue, manage an item it wrote, open a
change from a branch it may push, triage and approve when the role allows it. Those
are the ACL's own capabilities (`CanDiscuss`, `CanOpenRequest`, `CanTriage`,
`CanApprove`), so a maintainer's job may do everything a maintainer's key may do,
including labels, states, and other people's items.

A session is scoped only when the fingerprint it authenticated with is a live
credential row; an expired one reads as absent, so the push falls back to the
account's own role and is refused where a contribute account cannot write.

Two things end the key: the run finishing, which drops the row, and the expiry,
which is what stops a credential whose runner was killed and never reported. The
key is also masked in the log like a secret, since a step that cats it should not
leave it in the attempt.

Closes #165.

ci/workflow_test.go +42

@@ -540,6 +540,48 @@ jobs:
540 540 }
541 541 }
542 542
543 // bot names the account a job acts as. The name is checked at push and the account
544 // itself when the job is leased, because a roster changes without a push.
545 func TestJobBotIsParsedAndValidated(t *testing.T) {
546 t.Parallel()
547 wf, problems := Parse("ci.yaml", `version: 1
548 jobs:
549 agent:
550 image: alpine:3.22
551 bot: coding-agent
552 steps:
553 - run: true
554 `)
555 if len(problems) > 0 {
556 t.Fatalf("problems: %v", problems)
557 }
558 if got := wf.Jobs[0].Bot; got != "coding-agent" {
559 t.Fatalf("bot = %q", got)
560 }
561 for _, bad := range []string{"Coding-Agent", "a", "not an account", "bot!"} {
562 src := "version: 1\njobs:\n agent:\n image: alpine:3.22\n bot: " + strconv.Quote(bad) +
563 "\n steps:\n - run: true\n"
564 if _, problems := Parse("ci.yaml", src); len(problems) == 0 {
565 t.Fatalf("bot %q was accepted", bad)
566 } else if !strings.Contains(strings.Join(problems, " "), "bot") {
567 t.Fatalf("the problem does not name bot: %v", problems)
568 }
569 }
570 // A workflow cannot replace the variables that point at the credential.
571 src := `version: 1
572 jobs:
573 agent:
574 image: alpine:3.22
575 env:
576 VOLE_SSH_COMMAND: ssh -i /tmp/other
577 steps:
578 - run: true
579 `
580 if _, problems := Parse("ci.yaml", src); len(problems) == 0 {
581 t.Fatal("a workflow replaced a credential variable")
582 }
583 }
584
543 585 // StepLimit is the one rule the runner and a local run share, so it is checked on
544 586 // its own rather than through either of them.
545 587 func TestStepLimit(t *testing.T) {