The forgejo-sandbox / reforge harness, lifted out of the machine config into a host-agnostic, generic engine anyone can consume with Nix. Two layers: - engine (this repo) — nixosModules.reforge stands up the sandbox forge, provisions role accounts + tokens, enforces branch protection, and puts the reforge-* CLI + forgejo-mcp on PATH. Carries no project specifics. - run config — per-project manifest/charter/agenda/issues an adopter fills in; scaffold one with the `reforge` flake template. Portability fixes vs the in-config version: - forgejo-mcp resolved from $REFORGE_MCP_BIN or PATH, never a named host (kills the nixosConfigurations.omni hardcode). - all instance data + paths parameterized via REFORGE_* env, baked into the reforge-scripts wrappers from module options (configDir, agentsDir, refsDir, org, port, tokenOwner, ...). - option namespace neutral (reforge.* not omni.packs.*); settings policies carry no absolute /etc/nixos paths. - role briefs + orchestrator playbook genericized: all project specifics point at the charter; refs corpus optional. Validated: nix flake check (eval) + builds of forgejo-mcp, reforge-scripts, and a module-eval check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6.1 KiB
Architecture — the forgejo sandbox
A local Forgejo instance with role-based accounts, driven by separate agent sessions to simulate a software team with independent review — most importantly a security reviewer whose context is isolated from the implementer's reasoning.
Module: modules/reforge.nix (options under reforge.*). Loopback-only by
design and has nothing to do with any production forge.
What the module provides
| Thing | Value |
|---|---|
| Web UI / API | http://localhost:3030 (loopback only, no firewall hole) |
| Accounts | sandbox-admin (instance admin) + one per role (backend-dev, frontend-dev, security-lead, reviewer, …) |
| Org / repo | sandbox-team/sandbox-project (private, auto-init, default branch main) |
| Tokens | /var/lib/forgejo-sandbox/tokens/<user>.token — 0600, owned by reforge.tokenOwner |
| Admin UI login | sandbox-admin / cat /var/lib/forgejo-sandbox/admin-password (role accounts are token-only) |
| Branch protection | direct push to main blocked; 1 approval required; only security-lead's approval counts; rejected reviews block merge; stale approvals dismissed on new pushes |
| SSH | disabled — everything runs over HTTP with tokens |
| Registration | disabled — accounts exist only via provisioning |
| CLI | reforge-* on PATH (seed/reset/compare/smoke/kickoff/role/orchestrator/fetch-targets) |
Provisioning is reforge-provision.service, a oneshot that re-runs on every
switch and converges (check-before-create everywhere). State lives in
/var/lib/forgejo (sqlite) and survives rebuilds/reboots.
This module owns services.forgejo on the host — it is a dedicated
sandbox forge, not meant to coexist with another Forgejo instance. Knobs
(org, repo name, roles, required approvers, port, config dir, agents dir,
refs dir) are options under reforge.* — see the module.
Enabling it
Import nixosModules.reforge (or use the reforge flake template, which
wires it for you) and set at minimum:
{
reforge.enable = true;
reforge.configDir = ./reforge; # manifest.txt, charter.md, agenda.md, issues.tsv
reforge.tokenOwner = "youruser"; # who launches the sessions
}
The forgejo-mcp server binary and the reforge-* CLI are added to
environment.systemPackages, each wrapped with this host's defaults baked in
as REFORGE_* environment variables (all overridable per-invocation).
Verifying after a rebuild
systemctl status forgejo reforge-provision
curl -s http://localhost:3030/api/healthz
# identity check per role
tok=$(cat /var/lib/forgejo-sandbox/tokens/security-lead.token)
curl -s -H "Authorization: token $tok" http://localhost:3030/api/v1/user | jq .login
Then run the toolchain smoke test — a full issue → branch → PR → review-gated merge round-trip against the working repo, asserting the happy path and the gates (unapproved merge blocked, direct push to main rejected). Run it after every reset, before starting a run:
reforge-smoke
Seeding the stack (reforge-seed)
Beyond the working repo, the org carries the stack under test as clean
bases — the software, seeded at pinned upstream commits, without any
downstream work. The repo set, pinned base refs, and declared targets live
in your manifest.txt. Seeding is an idempotent script run by the human
(network fetches don't belong in the provisioning oneshot):
reforge-seed
- The
charterrepo → in-forge copy ofcharter.md(README.md) +agenda.md(AGENDA.md), protected like everything else. - Forked software (
kind=fork) → clone of the upstream at the manifest'sbase_ref(branch, tag, or SHA; full history pushed asmain, then protected). - Original software (
kind=original) → empty placeholder repos, no protection until a first scaffold exists.
Changing a base_ref does not re-seed an existing repo — reset the sandbox
first (reforge-reset reset, which archives the previous run before wiping).
The acceptance diff against the manifest's targets is reforge-compare.
Running a role session
One agent session per role:
reforge-role <role> # backend-dev | frontend-dev | security-lead | reviewer | …
The launcher prepares $REFORGE_TEAM_DIR/<role>/ (default ~/sandbox-team/<role>/)
fresh on every launch — edits to the runtime copies are overwritten; evolve
the sources in the engine or your agentsDir:
.claude/settings.json←role-settings.json— role policy: git to the sandbox forge only, dev tooling allowed, ssh/remotes/curl/web and other MCP servers denied,GIT_SSH_COMMAND=falseso git can't tunnel out..mcp.json— aforgejo-sandboxMCP server wired to that role's token.CLAUDE.md←agents/common.md+<role>.md— the role brief (identity, disclosure-phase rules, mechanics), with@ROLE@/@FORGE_URL@/@ORG@/@TOKENS_DIR@substituted.
Isolation is best-effort (same unix user) — a discipline boundary, not a security one. Never hand one session another role's token.
Customizing the briefs: point reforge.agentsDir at your own directory
(common.md, one <role>.md per role, orchestrator.md) and add any
project-specific reviewer role to reforge.roles.
Operational loop
- Implementer session: pull
main, branch, implement, push, open PR. - security-lead session: fetch the PR diff independently, review,
REQUEST_CHANGES/APPROVEDvia API. - Changes requested → implementer pushes fixes to the same branch (stale approvals auto-dismiss, so re-review is forced).
- On approval, merge — branch protection has been satisfied.
- Repeat.
The coordination between sessions is you (the human), or the orchestrator in autonomous mode (docs/reforge.md). Sessions are sequential per-role, not parallel autonomous processes.
Non-goals / follow-ups
- No Forgejo Actions/CI runners — if added later, gate review on CI green.
- No Postgres — sqlite is fine for sequential per-role sessions; revisit if sessions ever hammer the API concurrently.
- Not exposed beyond loopback; widen deliberately (WireGuard/LAN) if a session ever needs to run from another machine.