claude-forgejo-sandbox/docs/architecture.md
Padreug df0fd9a9ba feat: extract reforge engine into a standalone consumable flake
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>
2026-07-18 22:51:47 +02:00

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 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:

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.jsonrole-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.mdagents/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.