Agents Forgejo Bot
The Forgejo identity that agent-workspaces use to push work. Agents commit
and push branches; branch protection on main and human PR review are the
gate before anything deploys.
Identity
- Forgejo user:
agents - SSH key: stored in the
agents1Password vault as itemagents-forgejo-bot(fieldsprivate key,public key). The private key is deployed to ringtail at/etc/agents/ssh/id_ed25519(mode 0400, owneragent) byansible/playbooks/ringtail.yml. - Public key:
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMxDHumC8G7oWH7ivpQLKvBs8MRjf3nBlQmPBXMdduTF agents-forgejo-bot@ringtail
Access policy
- Write (push branches, open PRs) to the repos agents work in:
hephaestus,hephaestus.nvim,research,myeve,timberborn-parsimony,gamedev,talos. Access is per-repo collaborator grants, not an org-wide role — the bot sees nothing it was not explicitly added to. blumeops: read-only, author via fork. The bot has read on the canonicaleblume/blumeopsand pushes to its own forkagents/blumeops, opening cross-repo PRs. Read (not write) is load-bearing:workflow_dispatchis write-gated, so a read-only bot cannot trigger blumeops’ CI and thus cannot reach the deploy-credentialed Actions secrets (ARGOCD_AUTH_TOKEN,FLY_DEPLOY_TOKEN,ZOT_CI_API_KEY,MAIN_PUSH_TOKEN) — see agent-workspaces §Isolation.mainis additionally branch-protected (push + merge whitelisted toeblume).- Not an admin. No org/settings/runner access. No deploy credentials.
Credentials
The bot has two, both in the agents vault, both bounded to its own collaborations:
agents-forgejo-bot— SSH keypair. The public half is on theagentsuser; the private half (concealed field) is deployed to/etc/agents/ssh/id_ed25519and used forgit push.agents-forgejo-token— a Forgejo PAT, scopeswrite:repository+write:issue(tea’spr createneeds the issue scope — PRs are issues in Forgejo), minted as theagentsuser so it can’t exceed the bot’s own repo access. Read by the workspace launcher via the op shim fortea pr create/FORGEJO_TOKEN. A PAT minted this way is self-bounding — the ownership is the guardrail, not the scope list.
Sharing a repo with the bot
Most of Erich’s repos are private, and the bot is a plain Forgejo user — it gets no access by owning nothing. Both halves — the forge grant and the workspace checkout — are declared in one file:
repos.json (argocd/manifests/talos/)
{ "name": "myeve", "access": "write", "pool": "canonical" }access(write|read|none) is reconciled onto the forge as a collaborator grant bymise run agent-repo-access, which the Agent Repo Access workflow runs on merge tomain.pool(canonical|fork|none) drives the pod’s clone loop: the talos entrypoint (default.nix in the eblume/talos repo) fetches this file from blumeopsmainat pod start — a pool change reaches pods on restart, no image rebuild needed.- Every pool repo also gets the forge → talos webhook and the
agentsengagement label, reconciled by the same task. The label is how a human engages talos on an issue from the UI: Forgejo’s assignee dropdown only offers write collaborators, so on the read-only repos (blumeops,agents,horkos) the bot cannot be assigned — applying theagentslabel is the trigger instead (talos’FORGE_BOT_LABELdefaults to the bot login). A pool repo with the hook but without the label looks wired-up yet cannot be engaged from the UI — that gap is howhorkos#4sat inert until 2026-08-25. Labels are create-if-missing and never deleted (deleting a label strips it from closed issues), and label API calls need the issue scope the CI PAT lacks, so in CI the label half skips with a warning and is applied by a local run from gilbert — same follow-up as hook creation.
So adding a repo is: edit the file, open a PR, merge. No clicking in the forge
UI — and nothing to forget, which is the point. See agent-containerization
§“The repo pool” for what the pool values mean.
Reconcile is authoritative. A repo absent from
repos.jsonhas itsagentscollaboration removed. The PR job runs--check, so any revocation shows up in review before the merge that applies it. Grants made by hand in the forge UI will be reverted on the next run.
Three repos are pinned read-only in code, not data.
blumeops,agents, andhorkoscannot be grantedwriteno matter whatrepos.jsonsays — the reconciler refuses and exits non-zero (PINNED_READ_ONLYinmise-tasks/agent-repo-access). Their read-only-on-canonical status is what keeps blumeops CI, and its deploy-credentialed Actions secrets, out of agent reach; that fence should not be flippable by a one-line edit to a data file in a routine-looking PR.
Why a missing grant is hard to diagnose
Forgejo returns 404, not 403, for a private repo the caller cannot see. So a
missing collaboration is indistinguishable from a misspelled repo name: the
pod’s clone loop (deliberately non-fatal, so one bad repo can’t crashloop the
workspace) logs talos: clone <repo> failed (continuing) and the repo is
simply absent from ~/code/personal. timberborn-parsimony sat documented as a
sibling checkout for three weeks while being absent for exactly this reason —
which is what motivated collapsing the two steps into one file.
If an agent reports a repo missing, check the grant before debugging the clone. To see what the bot can currently reach, from an agent session:
curl -s --socks5-hostname localhost:1055 -H "Authorization: token $FORGEJO_TOKEN" \
"https://forge.ops.eblu.me/api/v1/repos/search?limit=100" \
| jq -r '.data[] | "\(.full_name)\t\(.permissions)"'Revoking is "access": "none", or deleting the entry outright. It takes
effect for new clones immediately — but the pod’s PVC keeps any checkout it
already made, so also delete ~/code/personal/<repo> in the pod if the intent
is to actually take it away.
The reconciler’s user needs admin on the target repos — collaborator
management is repo-admin-level, which eblume holds as owner. The token
scope only needs write:repository (verified empirically 2026-08-22; an
older note here claimed such a token 403s, which is no longer true). In CI
that arrives as the FORGE_REPO_WRITE_TOKEN Actions secret — an eblume PAT
scoped to write:repository,read:user (the read:user half is for
warrant-bot-drift’s site-admin check, not the reconciler; 1Password item
forge-repo-write-token, replacing the all-scopes admin PAT that previously
sat in CI) — declared in
the forgejo_actions_secrets ansible role and pushed by a human:
mise run provision-indri -- --tags forgejo_actions_secretsLocally the task falls back to op read on that same 1Password item.
The reconciler deliberately avoids /api/v1/user* (those need the read:user
scope, which a repo-scoped PAT may not carry) and enumerates via
/api/v1/repos/search instead. It also reads only $FORGE_REPO_WRITE_TOKEN,
never $FORGEJO_TOKEN: the indri runner is host-mode, so jobs inherit
erichblume’s LaunchAgent environment, and an earlier version silently picked
up a token the workflow never passed it.
Why a Forgejo secret and not
opin CI. The vault split is the boundary: theagentsvault is the agent’s, the blumeops vault is privileged and a human is meant to be part of any decision to use it. Forgejo Actions secrets are the curated subset that crosses that line, andprovision-indri— run from gilbert under biometricop— is the human step that moves them. Giving a runner a blumeops-vault service account would erase exactly that gate, since 1Password service accounts scope per vault, not per item.
Rotating the key
- Generate a new
ed25519keypair; update theagents-forgejo-botitem’sprivate key/public keyfields in theagentsvault. - Replace the public key on the
agentsForgejo user. mise run provision-ringtailto redeploy/etc/agents/ssh/id_ed25519.
Related
- agent-workspaces — what uses this identity
- bootstrap-agent-workspaces — first-time creation steps
- forgejo — the forge