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>
142 lines
6.1 KiB
Markdown
142 lines
6.1 KiB
Markdown
# 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:
|
|
|
|
```nix
|
|
{
|
|
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
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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):
|
|
|
|
```bash
|
|
reforge-seed
|
|
```
|
|
|
|
- The `charter` repo → in-forge copy of `charter.md` (README.md) +
|
|
`agenda.md` (AGENDA.md), protected like everything else.
|
|
- Forked software (`kind=fork`) → clone of the upstream at the manifest's
|
|
`base_ref` (branch, tag, or SHA; full history pushed as `main`, 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:
|
|
|
|
```bash
|
|
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=false` so git can't tunnel out.
|
|
- `.mcp.json` — a `forgejo-sandbox` MCP 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
|
|
|
|
1. **Implementer session**: pull `main`, branch, implement, push, open PR.
|
|
2. **security-lead session**: fetch the PR diff independently, review,
|
|
`REQUEST_CHANGES`/`APPROVED` via API.
|
|
3. Changes requested → implementer pushes fixes to the same branch (stale
|
|
approvals auto-dismiss, so re-review is forced).
|
|
4. On approval, merge — branch protection has been satisfied.
|
|
5. 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.
|