claude-forgejo-sandbox/scripts/reforge-fetch-targets.sh
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

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; }