You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Build a fully self-hosted, phone-operable agentic coding loop on brutus:
File issues and review/merge PRs from my phone (public + mTLS).
Self-hosted Forgejo forge. Each AI agent gets its own scoped bot account that can push feature branches and open PRs but cannot push to protected branches — I review & merge as human-in-the-loop.
Per-task Coder workspaces (podman containers) running a CLI coding agent (pi), driven for now via Coder's built-in web terminal.
Agents reach the local vLLM OpenAI endpoint over HTTP (no GPU passthrough needed) and Forgejo over a scoped token, and nothing else (host egress allowlist).
A "Open in Coder" button in each Forgejo repo (à la GitHub Codespaces) that launches a workspace pre-pointed at that repo, gated on login.
Everything declarative per repo conventions (.claude/CLAUDE.md): prefer existing NixOS module options, expose tunables as options (not let), secrets via agenix (never in the store), and write NixOS VM tests for assumptions (no tests that merely assert a config value).
Decisions (locked)
Area
Decision
Agent
pi — a CLI coding agent baked into the workspace image; pluggable
Agent UI (v1)
Coder built-in web terminal (run pi interactively in-browser); AgentAPI/opencode-web deferred
Isolation
Coder + podman containers (podman already enabled on brutus), ephemeral per task
settings.service.DISABLE_REGISTRATION = true (accounts created declaratively).
Secrets via services.forgejo.secrets.* (file paths → systemd LoadCredential), wired to agenix in hosts/brutus/secrets/secrets.nix. Do not put secrets in settings.
1.3 nginx vhost git.sunnycareboo.com via the existing homelab proxy helper (internal :443 + external :8443 mTLS, websockets on). Reuse recommendedProxySettings; no hand-written /etc.
1.4 Enable git.enable = true in hosts/brutus/services/homelab.nix. First-boot admin = me (Pocket ID OIDC for human login; keep one local admin).
1.5 Bind-mount Forgejo state onto a ZFS dataset (mirror immich/opencloud pattern in hosts/brutus/services/storage.nix) and add to restic backup set.
1.6 VM test: Forgejo boots, serves over nginx, Postgres reachable, git push/clone over HTTPS works.
2.1 Idempotent oneshot systemd unit (after forgejo, User=forgejo) that runs forgejo admin user create --restricted --username pi-bot --must-change-password=false --access-token --access-token-name pi --access-token-scopes "write:repository,write:issue" — guarded so re-runs don't error.
2.2 Capture the generated token into an agenix secret consumed by the workspace template / agent env (the --raw token via generate-access-token).
2.3 Same unit drives the Forgejo API to set branch protection on default branches: POST /repos/{owner}/{repo}/branch_protections with enable_push=true, enable_push_whitelist=true, push_whitelist_usernames=["jasonboukheir"] (excludes pi-bot), required_approvals=1, enable_approvals_whitelist (me). Net effect: pi-bot can push feature/* and open PRs but cannot push to main and cannot self-merge.
2.4 Topology: branch-and-PR, pi-bot is a Write collaborator (NOT admin, so it can't edit protection rules). Document fork-and-PR as the upgrade path for less-trusted agents.
2.5 VM test: pi-bot token can create an issue + push a feature branch + open a PR, and is rejected pushing to main.
Phase 3 — pi agent packaging + workspace image
3.1 Package/source pi (flake input or overlay). Pin it; expose its version/source as an option.
3.2 Build the workspace image with dockerTools.buildLayeredImage: pi + git + coreutils + the Coder agent deps. Reference it from a Coder Docker/podman template (virtualisation.oci-containers not needed — Coder provisions it).
3.3 Configure pi to use the local LLM: OPENAI_BASE_URL → LiteLLM :3200/v1 (preferred, model-routing) or vLLM :8000/v1; key from agenix/EMPTY. Confirm pi speaks OpenAI-compatible API + tool-calling against Qwen3.6.
3.4 Inject the pi-bot Forgejo token + git identity into the workspace env so pi can git push / open PRs via API.
4.1 terranix/HCL template in modules/homelab/services/coder/ using the kreuzwerker/docker provider against the podman socket (/run/podman/podman.sock), count = start_count for ephemerality, persistent home volume.
4.2coder_agent runs the image; web terminal is the primary control surface (run pi interactively in-browser). Verify phone usability of the Coder web terminal.
4.3 Autostop/dormancy to auto-teardown idle workspaces; consider a prebuild warm pool later.
4.4 (Deferred) Wrap pi with AgentAPI behind a coder_app (share=owner) for an opencode-like chat UI once the terminal flow is proven. Coder Tasks is deprecating (ESR Jun 2026, removed ~Sep 2026) — prefer coder_app or Coder Agents (Beta), not Tasks.
Phase 5 — Forgejo ↔ Coder "Open in Coder" button
5.1 Register a Forgejo OAuth2 app; configure Coder external-authTYPE=gitea, ID=forgejo, explicit AUTH_URL/TOKEN_URL=/login/oauth/access_token/VALIDATE_URL=/login/oauth/userinfo, REGEX=git\.sunnycareboo\.com, redirect https://code.sunnycareboo.com/external-auth/forgejo/callback. So workspaces clone private Forgejo repos with the logged-in user's creds.
5.2 One shared templatedevcontainer-forgejo with a coder_parameter "git_repo" + coder_external_auth "forgejo"; build from the repo's devcontainer.json via Coder Dev Containers integration (or envbuilder). Repo "owns its workspace" via devcontainer.json.
5.3 Inject the launch button in Forgejo, gated on {{if .IsSigned}}: prefer the native "Open with" applications admin entry (no restart) with OpenURL → https://code.sunnycareboo.com/templates/devcontainer-forgejo/workspace?param.git_repo={{.CloneButtonOriginLink.HTTPS}}; fall back to overriding custom/templates/repo/clone_panel.tmpl. (No template helper to test file existence — optionally hide via client-side JS check of the contents API.)
5.4 E2E test the private-repo clone path early (known envbuilder + gitea external-auth gotchas: coder#13367, coder#12291).
6.1 Host nftables egress allowlist so agent containers can reach only Forgejo :3000 + vLLM/LiteLLM :8000/:3200. Note podman/netavark has no DOCKER-USER chain — supply host forward rules (networking.firewall.filterForward = true for default-deny; extraForwardRules to allow the two destinations).
6.2 Confirm networking.nat alone is not default-deny; the drop policy comes from the nftables firewall forward chain.
6.3VM test proving default-deny + the two allowlisted destinations behave as intended (per repo conventions — test the assumption, not a config value).
Phase 7 — Phone access (public + mTLS)
7.1 Expose git.sunnycareboo.com + code.sunnycareboo.com on the external :8443 mTLS listener; install a client cert on the phone.
7.2 Verify on a phone: Coder dashboard + web terminal + Forgejo issue/PR review/merge all work behind mTLS. Confirm Coder wildcard access URL + TLS cert covers *.code.sunnycareboo.com for apps/subdomain.
Phase 8 — End-to-end agentic loop
8.1 Demo: file a Forgejo issue from phone → launch a Coder workspace via "Open in Coder" → drive pi in the web terminal to implement it → pi pushes feature/* + opens a PR as pi-bot → I review & merge from phone. main stays protected throughout.
8.2 (Future) Forgejo webhook on issues:opened (HMAC-SHA256 verified) → auto-provision a workspace/agent for the issue. Out of scope for v1.
Research references
Coder NixOS module nixos/modules/services/web-apps/coder.nix; coder_app/external-auth/Open-in-Coder deep links (param.<name>=), Dev Containers/envbuilder, git-clone module registry.coder.com/coder/git-clone/coder@2.0.1. Forgejo services.forgejo (secrets.* → LoadCredential), admin CLI user/token creation, branch-protection API, OAuth2 provider, "Open with" apps. Isolation: podman + dockerTools, nftables filterForward/extraForwardRules. vLLM client needs no GPU. (Full source URLs gathered during research — available on request.)
Note
Filed agentically per request. pi is not yet in the repo — Phase 3.1 must source/package it before the loop is functional. The rest is buildable with stock, currently-documented Coder + Forgejo features.
Epic: Agentic remote coding platform on
brutus(Coder + Forgejo + per-agent podman workspaces)Build a fully self-hosted, phone-operable agentic coding loop on
brutus:pi), driven for now via Coder's built-in web terminal.Everything declarative per repo conventions (
.claude/CLAUDE.md): prefer existing NixOS module options, expose tunables as options (notlet), secrets via agenix (never in the store), and write NixOS VM tests for assumptions (no tests that merely assert a config value).Decisions (locked)
pi— a CLI coding agent baked into the workspace image; pluggablepiinteractively in-browser); AgentAPI/opencode-web deferredbrutus), ephemeral per taskservices.forgejo), Postgres, behind nginx:8443) + client-cert setup:8000) / LiteLLM proxy (:3200) — agents are HTTP clients, no GPU in containersCurrent state (what already exists on
brutus)services.coderviamodules/homelab/services/coder/, Pocket ID OIDC, podman socket,*.code.sunnycareboo.comwildcard, terranix templates staged to~coder/templates/.dockerCompat, socket at/run/podman/podman.sock(hosts/brutus/virtualization.nix).:8000+ LiteLLM:3200(hosts/brutus/services/vllm-xpu.nix,modules/homelab/services/litellm.nix).:443+ external:8443mTLS,recommendedProxySettings(modules/homelab/services.nix).modules/homelab/registry.nix).piagent — not present anywhere in the repo (needs packaging/sourcing).Tasks
Phase 1 — Forgejo forge (declarative)
forge/gitservice tomodules/homelab/registry.nix(external service →git.sunnycareboo.com, allocate HTTP port).modules/homelab/services/forgejo/default.nixusingservices.forgejo:database.type = "postgres"(peer/socket auth,createDatabase = true);lfs.enable = true.settings.server:HTTP_ADDR=127.0.0.1,DOMAIN/ROOT_URL=https://git.sunnycareboo.com/,SSH_PORT/SSH_DOMAIN(reuse host OpenSSH,START_SSH_SERVER=false).settings.service.DISABLE_REGISTRATION = true(accounts created declaratively).services.forgejo.secrets.*(file paths → systemdLoadCredential), wired to agenix inhosts/brutus/secrets/secrets.nix. Do not put secrets insettings.git.sunnycareboo.comvia the existing homelab proxy helper (internal:443+ external:8443mTLS, websockets on). ReuserecommendedProxySettings; no hand-written/etc.git.enable = trueinhosts/brutus/services/homelab.nix. First-boot admin = me (Pocket ID OIDC for human login; keep one local admin).hosts/brutus/services/storage.nix) and add to restic backup set.Phase 2 — Agent bot account, scoped token & branch protection (the human-in-the-loop guarantee)
User=forgejo) that runsforgejo admin user create --restricted --username pi-bot --must-change-password=false --access-token --access-token-name pi --access-token-scopes "write:repository,write:issue"— guarded so re-runs don't error.--rawtoken viagenerate-access-token).POST /repos/{owner}/{repo}/branch_protectionswithenable_push=true,enable_push_whitelist=true,push_whitelist_usernames=["jasonboukheir"](excludespi-bot),required_approvals=1,enable_approvals_whitelist(me). Net effect:pi-botcan pushfeature/*and open PRs but cannot push tomainand cannot self-merge.pi-botis a Write collaborator (NOT admin, so it can't edit protection rules). Document fork-and-PR as the upgrade path for less-trusted agents.pi-bottoken can create an issue + push a feature branch + open a PR, and is rejected pushing tomain.Phase 3 —
piagent packaging + workspace imagepi(flake input or overlay). Pin it; expose its version/source as an option.dockerTools.buildLayeredImage:pi+ git + coreutils + the Coder agent deps. Reference it from a Coder Docker/podman template (virtualisation.oci-containersnot needed — Coder provisions it).pito use the local LLM:OPENAI_BASE_URL→ LiteLLM:3200/v1(preferred, model-routing) or vLLM:8000/v1; key from agenix/EMPTY. Confirmpispeaks OpenAI-compatible API + tool-calling against Qwen3.6.pi-botForgejo token + git identity into the workspace env sopicangit push/ open PRs via API.Phase 4 — Coder workspace template (podman, ephemeral) + web-terminal control
modules/homelab/services/coder/using thekreuzwerker/dockerprovider against the podman socket (/run/podman/podman.sock),count = start_countfor ephemerality, persistent home volume.coder_agentruns the image; web terminal is the primary control surface (runpiinteractively in-browser). Verify phone usability of the Coder web terminal.piwith AgentAPI behind acoder_app(share=owner) for an opencode-like chat UI once the terminal flow is proven. Coder Tasks is deprecating (ESR Jun 2026, removed ~Sep 2026) — prefercoder_appor Coder Agents (Beta), not Tasks.Phase 5 — Forgejo ↔ Coder "Open in Coder" button
TYPE=gitea,ID=forgejo, explicitAUTH_URL/TOKEN_URL=/login/oauth/access_token/VALIDATE_URL=/login/oauth/userinfo,REGEX=git\.sunnycareboo\.com, redirecthttps://code.sunnycareboo.com/external-auth/forgejo/callback. So workspaces clone private Forgejo repos with the logged-in user's creds.devcontainer-forgejowith acoder_parameter "git_repo"+coder_external_auth "forgejo"; build from the repo'sdevcontainer.jsonvia Coder Dev Containers integration (or envbuilder). Repo "owns its workspace" viadevcontainer.json.{{if .IsSigned}}: prefer the native "Open with" applications admin entry (no restart) withOpenURL→https://code.sunnycareboo.com/templates/devcontainer-forgejo/workspace?param.git_repo={{.CloneButtonOriginLink.HTTPS}}; fall back to overridingcustom/templates/repo/clone_panel.tmpl. (No template helper to test file existence — optionally hide via client-side JS check of the contents API.)Phase 6 — Network egress isolation (allowlist Forgejo + vLLM only)
:3000+ vLLM/LiteLLM:8000/:3200. Note podman/netavark has noDOCKER-USERchain — supply host forward rules (networking.firewall.filterForward = truefor default-deny;extraForwardRulesto allow the two destinations).networking.natalone is not default-deny; the drop policy comes from the nftables firewall forward chain.Phase 7 — Phone access (public + mTLS)
git.sunnycareboo.com+code.sunnycareboo.comon the external:8443mTLS listener; install a client cert on the phone.*.code.sunnycareboo.comfor apps/subdomain.Phase 8 — End-to-end agentic loop
piin the web terminal to implement it →pipushesfeature/*+ opens a PR aspi-bot→ I review & merge from phone.mainstays protected throughout.issues:opened(HMAC-SHA256 verified) → auto-provision a workspace/agent for the issue. Out of scope for v1.Research references
Coder NixOS module
nixos/modules/services/web-apps/coder.nix;coder_app/external-auth/Open-in-Coder deep links (param.<name>=), Dev Containers/envbuilder, git-clone moduleregistry.coder.com/coder/git-clone/coder@2.0.1. Forgejoservices.forgejo(secrets.*→LoadCredential), admin CLI user/token creation, branch-protection API, OAuth2 provider, "Open with" apps. Isolation: podman +dockerTools, nftablesfilterForward/extraForwardRules. vLLM client needs no GPU. (Full source URLs gathered during research — available on request.)Note
Filed agentically per request.
piis not yet in the repo — Phase 3.1 must source/package it before the loop is functional. The rest is buildable with stock, currently-documented Coder + Forgejo features.