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>
104 lines
4.2 KiB
Bash
104 lines
4.2 KiB
Bash
#!/usr/bin/env bash
|
|
# Reforge Phase B — local target-mirror fetcher (docs/reforge.md).
|
|
#
|
|
# Phase B opens end-state visibility. But role sessions AND the orchestrator
|
|
# are policy-barred from reaching the real target remotes, so the targets
|
|
# cannot be fetched from inside a run. This OPERATOR step makes each
|
|
# manifest target available as a LOCAL, SOURCE-STRIPPED copy roles can read
|
|
# while converging.
|
|
#
|
|
# Safety: after each clone ALL git metadata is deleted (`rm -rf .git`). The
|
|
# copies have NO remote, NO cached credentials, and NO push path — plain
|
|
# read-only trees. Nothing this produces can push to, or otherwise affect,
|
|
# the real target repos. The only network touch is the read-only clone,
|
|
# with your credentials (operator step by design).
|
|
#
|
|
# reforge-fetch-targets # fetch/refresh all
|
|
# REFORGE_TARGETS_DIR=/some/path reforge-fetch-targets
|
|
#
|
|
# Roles then read $REFORGE_TARGETS_DIR/<repo> (default ~/reforge-targets/<repo>)
|
|
# as the end-state; provenance is in each <repo>/.REFORGE_TARGET_SOURCE.txt.
|
|
set -euo pipefail
|
|
|
|
CONFIG_DIR=${REFORGE_CONFIG_DIR:?set REFORGE_CONFIG_DIR to your run config dir}
|
|
MANIFEST=${REFORGE_MANIFEST:-$CONFIG_DIR/manifest.txt}
|
|
DEST=${REFORGE_TARGETS_DIR:-$HOME/reforge-targets}
|
|
|
|
# Never hang on an interactive prompt: no password/host-key questions, fail
|
|
# fast on an unreachable host, auto-accept a new host key.
|
|
export GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15}"
|
|
export GIT_TERMINAL_PROMPT=0
|
|
|
|
[ -r "$MANIFEST" ] || { echo "no readable manifest at $MANIFEST" >&2; exit 1; }
|
|
|
|
# rm that first restores write bits (copies are made read-only below).
|
|
# NOTE: must always return 0 — a non-zero return here would trip `set -e`.
|
|
safe_rm() { if [ -e "$1" ]; then chmod -R u+w "$1" 2>/dev/null || true; rm -rf "$1"; fi; return 0; }
|
|
|
|
echo "→ reforge target mirrors → $DEST"
|
|
echo " source-stripped: .git removed after clone; no remote, no push path."
|
|
echo
|
|
mkdir -p "$DEST"
|
|
|
|
fetched=0 skipped=0 failed=0
|
|
mapfile -t ROWS < <(grep -Ev '^[[:space:]]*(#|$)' "$MANIFEST")
|
|
echo "parsed ${#ROWS[@]} manifest rows; ssh: $GIT_SSH_COMMAND"
|
|
echo
|
|
err="$DEST/.clone-err.log"; : > "$err"
|
|
for row in "${ROWS[@]}"; do
|
|
IFS='|' read -r name _kind _upstream _base_ref target_url target_ref <<<"$row"
|
|
|
|
if [ "$target_url" = "-" ] || [ -z "$target_url" ]; then
|
|
printf 'SKIP %-14s no target declared\n' "$name"
|
|
skipped=$((skipped+1)); continue
|
|
fi
|
|
|
|
tmp="$DEST/.tmp-$name"
|
|
dest="$DEST/$name"
|
|
safe_rm "$tmp"
|
|
|
|
printf 'CLONE %-14s %s @ %s\n' "$name" "$target_url" "$target_ref"
|
|
if ! git clone --quiet --depth 1 --branch "$target_ref" "$target_url" "$tmp" 2>>"$err"; then
|
|
# fallback: full clone then checkout (handles non-branch refs)
|
|
safe_rm "$tmp"
|
|
if ! git clone --quiet "$target_url" "$tmp" 2>>"$err"; then
|
|
printf 'FAIL %-14s clone failed — last error:\n' "$name"
|
|
tail -n 3 "$err" | sed 's/^/ /'
|
|
failed=$((failed+1)); continue
|
|
fi
|
|
git -C "$tmp" checkout --quiet "$target_ref" 2>>"$err" || true
|
|
fi
|
|
|
|
sha=$(git -C "$tmp" rev-parse HEAD)
|
|
|
|
# provenance recorded BEFORE we strip git metadata
|
|
cat > "$tmp/.REFORGE_TARGET_SOURCE.txt" <<EOF
|
|
repo: $name
|
|
target_url: $target_url
|
|
target_ref: $target_ref
|
|
commit: $sha
|
|
note: Source-stripped local mirror for reforge Phase B convergence.
|
|
No git remote remains. Read-only. DO NOT push anywhere.
|
|
EOF
|
|
|
|
# STRIP ALL SOURCE INFO — the guarantee: no remote, no creds, no push path.
|
|
rm -rf "$tmp/.git"
|
|
|
|
safe_rm "$dest"
|
|
mv "$tmp" "$dest"
|
|
chmod -R a-w "$dest" 2>/dev/null || true # read-only reference tree
|
|
|
|
printf 'OK %-14s %s → %s (git stripped)\n' "$name" "${sha:0:12}" "$dest"
|
|
fetched=$((fetched+1))
|
|
done
|
|
|
|
echo
|
|
echo "done: $fetched fetched, $skipped skipped, $failed failed → $DEST"
|
|
|
|
# hard verification that nothing retained a source/remote
|
|
if find "$DEST" -maxdepth 3 -name .git -print 2>/dev/null | grep -q .; then
|
|
echo "WARNING: a .git directory remains under $DEST — investigate before using." >&2
|
|
exit 2
|
|
fi
|
|
echo "verified: no .git metadata anywhere under $DEST (no push path exists)."
|
|
[ "$failed" = 0 ] || { echo "note: some clones failed — re-run after checking access." >&2; exit 1; }
|