- Rust 58.6%
- MDX 35.4%
- Python 1.7%
- TypeScript 1%
- Astro 0.7%
- Other 2.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .agents/skills | ||
| .beads | ||
| .claude | ||
| .codex | ||
| .gitignore | ||
| AGENTS.md | ||
| bot-avatar.png | ||
| CLAUDE.md | ||
| create-forge-agent.py | ||
| README.md | ||
| skills-lock.json | ||
forge-agents
Provisioning tooling for the agent ("bot") accounts on https://git.b4mad.industries.
Home: https://git.b4mad.industries/agentic-forges/forge-agents
(ssh://git@git.b4mad.industries:2222/agentic-forges/forge-agents.git).
git.b4mad.industriesis the same Forgejo that used to answer toforgejo.b4mad.net— a rename of the public route, not a migration. Both names serve port 2222 with the same RSA host key (SHA256:LFo9B+TvN+sXwkUBW58RxU4QVyqk8rRaohW5UPFiuYw), and the old one still resolves. So agents provisioned under the old name keep working and their keys and tokens need no backfill.
known_hostsdoes not carry over, though — it is keyed by hostname, so the new name is an unknown host until you add it. That is also your chance to check the rename really is one:ssh-keyscan -p 2222 git.b4mad.industries 2>/dev/null | ssh-keygen -lf - # expect SHA256:LFo9B+TvN+sXwkUBW58RxU4QVyqk8rRaohW5UPFiuYw (RSA) ssh-keyscan -p 2222 git.b4mad.industries >> ~/.ssh/known_hosts
Agents live in Forgejo's own datastore, not in git — re-applying the manifests does not recreate them. This repo is the record of how to recreate them, and the 2026-07-27 move to CloudNativePG is the proof that mattered: it wiped every bot account, key and token, and each had to be rebuilt from tooling like this.
Provision an agent
The script plans first and applies second. On its own it surveys the live state, prints what it would do, and changes nothing:
./create-forge-agent.py b4mad-something something@b4mad.net \
--scopes "write:repository,write:issue,read:user,read:organization"
plan for 'b4mad-something' — would do:
create account b4mad-something <something@b4mad.net>
set avatar from bot-avatar.png
create SSH key 'b4mad-something-ed25519' (ed25519)
create GPG key (ed25519, verified) and upload it
mint token 'b4mad-something' scoped write:repository,…
write …/forgejo-agent-b4mad-something.enc.yaml
write …/forgejo-agent-b4mad-something.yaml
dry run — nothing changed. Re-run with --confirm to apply.
Add --confirm to execute it. Dry run is the default because this script is
routinely pointed at accounts that already exist, and the interesting question
is always what is already there — see Existing agents.
--dry-run also exists as an explicit spelling of the default, for scripts that
want to say so out loud; passing it together with --confirm is an error rather
than a silent winner.
One confirmed run does the whole lifecycle:
| # | Step | How |
|---|---|---|
| 0 | survey what already exists | GET only — a dry run never leaves this row |
| 1 | create the account | POST /admin/users (gitea_admin basic auth) |
| 2 | re-assert the shared avatar | POST /user/avatar (agent basic auth) |
| 3 | generate ed25519 SSH keypair | ssh-keygen |
| 4 | generate ed25519 GPG keypair | gpg --batch --gen-key |
| 5 | upload the SSH public half | POST /admin/users/{u}/keys |
| 6 | upload the GPG public half | POST /user/gpg_keys, with a detached signature over /user/gpg_key_token |
| 7 | mint the PAT | POST /admin/users/{u}/tokens |
| 8 | write forgejo-agent-<u>.enc.yaml |
sops |
| 9 | write forgejo-agent-<u>.yaml |
the manifests repo's scripts/sops2sealedsecret |
| 10 | verify all of it | read back from the API + a real ssh -T on :2222 |
| 11 | shred the plaintext | — |
Step 6 is the one worth understanding: uploading a GPG key without answering the challenge leaves it present but unverified, which buys nothing — Forgejo will not mark the agent's commits as verified. Answering it inline is the whole point.
A successful run ends like this, and any step that cannot be confirmed is a hard failure rather than a warning — a half-provisioned agent that looks provisioned is worse than one that visibly did not get created:
✓ token authenticates as b4mad-something
✓ SSH key registered (SHA256:…)
✓ GPG key registered and VERIFIED (…)
✓ ssh -T git@git.b4mad.industries:2222 authenticates
✓ forgejo-agent-b4mad-something.enc.yaml decrypts and round-trips
✓ forgejo-agent-b4mad-something.yaml generated for namespace b4mad-forgejo
# re-assert the shared avatar on an existing agent, touching nothing else:
./create-forge-agent.py b4mad-something something@b4mad.net --avatar-only --confirm
--help covers the rest. Exit codes: 0 done (or a clean dry run), 1
failure, 2 the plan needs --overwrite and did not get it.
Existing agents
An agent that already exists is not an error, but it is a decision, so the plan spells it out and the run stops until you answer it:
plan for 'b4mad-castra' — would do:
keep account b4mad-castra (id 7) — repos, org membership and commit attribution untouched
rotate its password to a throwaway (the avatar and GPG endpoints are user-scoped)
set avatar from bot-avatar.png
delete SSH key 'b4mad-castra-ed25519' (id 4, SHA256:aaa) [--overwrite]
delete GPG key 1A2B3C4D5E6F7788 (id 3) [--overwrite]
revoke token 'b4mad-castra' (id 12, created 2026-07-01…) [--overwrite]
create SSH key 'b4mad-castra-ed25519' (ed25519)
…
leave 1 SSH key(s) not titled 'b4mad-castra-ed25519' alone — this tool did not put them there
⚠️ 'b4mad-castra' already exists and 4 step(s) above would destroy existing credentials.
--overwrite authorises exactly the [--overwrite] steps: rotation in
place. The account itself is kept, and with it every repo it owns, its commit
attribution and its org membership — only credentials are replaced. There is no
mode that deletes the account; Forgejo refuses to delete a user who owns repos,
and detaching an agent's commit history to re-mint a token would be a wildly
disproportionate trade.
Whether a run needs --overwrite at all is decided by what it would destroy:
| Situation | Needs --overwrite? |
|---|---|
| Account does not exist | no — nothing to destroy |
| Account exists, no keys, no token by that name | no — this is the backfill case below |
Token named --token-name already exists |
yes (Forgejo rejects a duplicate name outright) |
SSH key titled <username>-ed25519 exists |
yes |
| Any GPG key on the account | yes |
forgejo-agent-<u>.enc.yaml / .yaml already written |
yes |
So adding a token alongside the current one needs no --overwrite — pass a
different --token-name and the old one keeps working.
Ownership is decided by name, because the API stores nothing else to go on:
SSH keys are this tool's if titled <username>-ed25519, and any other key is
reported and left alone. GPG keys have no title to filter on, so on a
machine account all of them are treated as this tool's — which is why they are
itemised by key ID in the plan before anything is deleted.
Within a confirmed rotation the token is revoked last: if a later step fails, the agent still holds the credential it had rather than nothing.
⚠️ Rotation is not atomic and Forgejo has no transaction to offer. If a run dies midway the plan is the recovery map — re-run it, read what it now says about the world, and continue.
--force is a deprecated alias for --overwrite.
Where the credentials go
This repo holds the tool. It never holds a credential.
git.b4mad.industries github.com
agentic-forges/forge-agents b4mad/op1st-emea-b4mad
create-forge-agent.py ──▶ manifests/applications/b4mad-forgejo/
forgejo-agent-<name>.enc.yaml (SOPS — source of truth)
forgejo-agent-<name>.yaml (SealedSecret — Argo CD applies this)
README.md (the roster)
--manifests-dir points at a checkout of that GitOps repo; it defaults to
~/Source/github.com/operate-first/op1st-emea-b4mad. Tool and output are
separate on purpose — Argo CD syncs the SealedSecret from the manifests repo's
kustomization, so the credential has to live there.
⚠️ Key custody. The plaintext private keys are ephemeral: they exist in a
0700 temp dir for the duration of the run and are shredded at the end. The
.enc.yaml is the ONE durable copy — deliberately one, not two, because a
credential duplicated across locations rotates in one place and goes silently
stale in the other. --keep-dir is the escape hatch; using it means you now own
a second copy that will not follow rotations.
⚠️ Nothing is committed for you. git add both output files. An uncommitted
run is a lost agent, because the plaintext is already gone.
Rotation
./create-forge-agent.py b4mad-something something@b4mad.net \
--scopes "…" --overwrite # read the plan
./create-forge-agent.py b4mad-something something@b4mad.net \
--scopes "…" --overwrite --confirm # do it
Then commit both regenerated files — the .enc.yaml is the only copy of the
new private keys.
By hand, if you need to revoke something the script does not own (a foreign SSH
key, a token under a name you are not re-minting). The admin endpoints take the
gitea_admin break-glass credentials, so no password rotation on the agent is
needed:
ADMIN=$(oc -n b4mad-forgejo get secret forgejo-admin -o jsonpath='{.data.username}' | base64 -d)
PW=$(oc -n b4mad-forgejo get secret forgejo-admin -o jsonpath='{.data.password}' | base64 -d)
curl -s -u "$ADMIN:$PW" https://git.b4mad.industries/api/v1/admin/users/<agent>/tokens
curl -s -u "$ADMIN:$PW" -X DELETE \
https://git.b4mad.industries/api/v1/admin/users/<agent>/tokens/<id-or-name>
# SSH keys: DELETE /api/v1/admin/users/<agent>/keys/<id>
# GPG keys are user-scoped only — DELETE /api/v1/user/gpg_keys/<id> as the agent
Conventions this enforces
- Every agent wears the same avatar (
bot-avatar.png), so service accounts are distinguishable from humans at a glance. It is re-applied on every run, which is what keeps them uniform.--avataroverrides it — deliberate divergence, don't, unless you mean it. - Keys are ed25519 (GPG: EDDSA + cv25519), passphrase-less, non-expiring — matching the shape of the Forgejo server's own commit-signing key.
- The GPG UID carries
#B4mad Forgejo agent, marking the key as a machine identity the same way the avatar does in the web UI. - There are no agent passwords to store. The script mints a throwaway purely
to authenticate the two user-scoped endpoints admin credentials cannot reach
(avatar upload, GPG key upload), then discards it.
ENABLE_INTERNAL_SIGNIN: falsemeans agents cannot sign in at all. - Token scopes are not secret and belong in the plaintext roster table in the manifests repo, not in the encrypted blob.
⚠️ read:organization is not optional for anything that walks org repos —
Renovate resolves each repo's owning org before processing it, so without that
scope every repository fails with 403 … token does not have at least one of required scope(s): [read:organization].
⚠️ A token scoped write:package is not sufficient to push to an org
package namespace — the account must also be a member of that org. The registry
rejects the push as a bare authentication required, with nothing pointing at
membership as the cause.
Requirements
gpg, ssh-keygen, ssh, oc, sops, kubeseal, Python 3.11+ (stdlib
only). oc must be logged in to nostromo — the admin credentials are read from
the live forgejo-admin Secret in b4mad-forgejo, not from the repo, because
what is deployed is what the API will accept.
Known gap
The four pre-existing bots — b4mad-renovate, b4mad-gitops, b4mad-castra,
b4mad-release-bot — have no SSH and no GPG key (validated 2026-07-29).
They predate this tool. Their commits are unsigned, so anything holding the
token is indistinguishable from the agent itself.
Backfilling one is now close to a plain run: having no keys, they have nothing
for the tool to destroy on the account. Give the run a fresh --token-name and
the agent's live token keeps working while it gains keys and a second one:
./create-forge-agent.py b4mad-renovate renovate@b4mad.net \
--scopes "…" --token-name b4mad-renovate-signed
Read that plan before adding --confirm, and check two things: that it says
keep account rather than create, and whether the output-file steps came out
as write or as overwrite … [--overwrite] — a bot whose credentials already
live in a forgejo-agent-<u>.enc.yaml needs --overwrite for those, and
whatever consumes the old secret has to be moved to the new one in the same
change. Tracked as Systems-3ywf in the b4mad-erdgeschoss-systems beads
tracker.
Provenance
Extracted from b4mad-erdgeschoss-systems at commit 761df40, where it lived
as forgejo/create-forge-agent.py. It replaced create-forge-bot.sh, which
only ever created an account + PAT + avatar.
The forge's public route was renamed forgejo.b4mad.net → git.b4mad.industries
on 2026-07-29; see the note at the top of this file for why that is a rename and
not a migration. Sibling projects under agentic-forges/ — notably
forgejo-mcp — moved
with it.