Agent -> Builder handoff pipeline, first cut: - agents/agent.md: documents the ===BUILDER-HANDOFF=== marker format the Agent uses once the user explicitly wants an elaborated spec implemented. - worker.py: extract_handoff() detects the marker, strips it from the user-facing reply, and enqueue_builder_job() writes the spec as a new job into a separate builder-inbox queue. - builder.py (new): path-triggered oneshot worker for that queue. Test stage only -- runs Codex read-only against the same agents/ role dir and is told to relay the received spec back unchanged, to validate the pipeline before any real write capability is built. No execution, no filesystem changes. - New Prosody account builder@bot.0ff.space (own XMPP identity, own secrets/builderXmpp.age, own go-sendxmpp config) so Builder replies are visibly distinct from Agent replies instead of reusing agent@'s identity with a text prefix. Verified end to end against the live services: a real Agent job that requests a test handoff produces the marker, gets detected, enqueues a Builder job, and the Builder relays it back -- confirmed in Prosody's log as a message from builder@bot.0ff.space to lukas@0ff.space. Builder does not yet get real technical execution capability or a "build with lukas's permission" dialog loop -- that needs its own sandbox/approval design and is explicitly not built here. |
||
|---|---|---|
| .. | ||
| agentXmpp.age | ||
| bindPW.age | ||
| builderXmpp.age | ||
| dns-challenge.age | ||
| domain.age | ||
| forgejoToken.age | ||
| go-sendxmpp.age | ||
| headscalePreAuthKey.age | ||
| ipv4.age | ||
| ipv6.age | ||
| lldapAdmin.age | ||
| mailPW.age | ||
| motd.age | ||
| oauth2Cookie.age | ||
| oidcAudiobookshelf.age | ||
| oidcForgejo.age | ||
| oidcGTS.age | ||
| oidcGTSEnv.age | ||
| oidcHeadscale.age | ||
| oidcOauth2.age | ||
| oidcQuantum.age | ||
| oidcVikunja.age | ||
| README.md | ||
| resticPW.age | ||
| secrets.nix | ||
| secrets.yaml | ||
| traefikBasicAuth.age | ||
| traefikDashboardPassword.age | ||
| webdav.age | ||
| wirelessSecretsFile.age | ||
Secrets
This directory contains credentials encrypted with
agenix. Only encrypted *.age files belong
in Git. Never commit plaintext passwords, tokens, private keys, recovery codes,
or decrypted copies.
secrets/secrets.nix defines which user and host SSH public keys may decrypt
each file. The public keys are not secrets. Host-specific ownership and runtime
paths are configured in:
hosts/globalServices/secretFiles.nixhosts/tnix/localServices/secrets.nixhosts/pinix/localServices/secrets.nixhosts/blunix/localServices/secrets.nix
Active secrets
| File | Consumer | Expected plaintext format |
|---|---|---|
bindPW.age |
Prosody LDAP authentication | Prosody Lua configuration containing the LDAP password |
dns-challenge.age |
Traefik/Let's Encrypt | systemd environment file containing the Cloudflare DNS challenge credentials |
forgejoToken.age |
Manual/ad-hoc Forgejo API calls (drone, on any host) | Raw Forgejo API access token for the lukas account |
agentXmpp.age |
blunix agent-xmpp-listener systemd service |
Raw XMPP account password for agent@bot.0ff.space |
builderXmpp.age |
blunix agent-codex-builder systemd service |
Raw XMPP account password for builder@bot.0ff.space |
go-sendxmpp.age |
pinix webhook notifications | Raw XMPP account password |
headscalePreAuthKey.age |
Tailscale clients on tnix and pinix | Raw Headscale pre-authentication key |
lldapAdmin.age |
LLDAP, Dex, and Radicale | Raw LLDAP administrator password |
mailPW.age |
aerc and msmtp on tnix | Raw mailbox password |
motd.age |
Prosody | Prosody Lua configuration for the message of the day |
oauth2Cookie.age |
oauth2-proxy protecting 0ff.space |
Raw cookie encryption secret |
oidcAudiobookshelf.age |
Dex client for Audiobookshelf | Raw OIDC client secret |
oidcForgejo.age |
Dex and Forgejo | Raw OIDC client secret |
oidcGTS.age |
Dex client for GoToSocial | Raw OIDC client secret |
oidcGTSEnv.age |
GoToSocial | Environment file containing GTS_OIDC_CLIENT_SECRET=...; the value must match oidcGTS.age |
oidcHeadscale.age |
Dex and Headscale | Raw OIDC client secret |
oidcOauth2.age |
Dex and oauth2-proxy protecting 0ff.space |
Raw OIDC client secret |
oidcQuantum.age |
Dex client for Quantum | Raw OIDC client secret |
oidcVikunja.age |
Dex and Vikunja | Raw OIDC client secret |
resticPW.age |
Restic on pinix and blunix | Raw Restic repository password |
traefikBasicAuth.age |
Traefik dashboard middleware | htpasswd line in user:password-hash format |
webdav.age |
WebDAV on pinix | Environment file with ENV_USERNAME=... and ENV_PASSWORD=... |
wirelessSecretsFile.age |
wpa_supplicant on tnix and pinix | key=value lines referenced through ext:key, currently psk_mango and psk_home |
Audiobookshelf and Quantum currently run on DietPi, which is offline. Their OIDC client entries remain prepared in Dex, but the applications are not deployed by the active NixOS hosts. Their rotated client secrets must be entered in the applications when DietPi is next powered on.
Raw passwords and tokens must not end with a newline unless the consuming
service explicitly expects one. In particular, lldapAdmin.age is read
verbatim when LLDAP resets the administrator password.
Prepared or unused files
These encrypted files currently have no active consumer:
| File | Intended use or status |
|---|---|
domain.age |
Legacy declaration; no service currently reads it |
ipv4.age |
Legacy file; not declared by agenix or read by a service |
ipv6.age |
Legacy file; not declared by agenix or read by a service |
traefikDashboardPassword.age |
Legacy file replaced by traefikBasicAuth.age |
secrets/secrets.yaml is an older SOPS file and is not consumed by the current
agenix configuration. Do not add new values to it. It can be removed after
confirming that no external workflow still depends on it.
Creating or editing a secret
From the repository root:
agenix -e secrets/<name>.age
To add a new secret:
- Add its recipient list to
secrets/secrets.nix. - Create the encrypted file with
agenix -e. - Declare its runtime owner and path in the appropriate NixOS secret module.
- Pass only the resulting runtime path to the service.
- Evaluate the affected host before deployment.
Do not interpolate plaintext secrets into Nix strings. Doing so copies them into the world-readable Nix store.
Recipients and key changes
After adding, removing, or replacing a recipient key, re-encrypt all registered files:
agenix -r
Removing a recipient does not invalidate copies that person or host already decrypted. Rotate the underlying credential as well if access must be revoked.
agenix on blunix is ragenix
agenix (from ragenix in hosts/blunix/localServices/packages.nix) is
installed on blunix so agenix -e/agenix -r work as documented above. It
is a Rust reimplementation and, unlike the original CLI, has no
-d/--decrypt flag — running agenix -d on blunix fails with
unexpected argument '-d' found. For decrypting to stdout (verification,
or reading a secret's value in a script/session), use the original CLI via
the pinned flake input instead:
nix run github:ryantm/agenix -- -d secrets/<name>.age -i <private-key>
Safe checks
Validate that every registered age file can be decrypted without printing its content:
for secret in secrets/*.age; do
nix run github:ryantm/agenix -- -d "$secret" -i <private-key> >/dev/null
done
Scan the working tree for accidentally committed plaintext:
nix run nixpkgs#gitleaks -- dir --redact .