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>
This commit is contained in:
commit
df0fd9a9ba
32 changed files with 2698 additions and 0 deletions
73
scripts/reforge-compare.sh
Normal file
73
scripts/reforge-compare.sh
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
#!/usr/bin/env bash
|
||||
# Reforge acceptance check (docs/reforge.md): byte-for-byte TREE diff of
|
||||
# each sandbox repo's `main` against its declared target — the known
|
||||
# working state — from $REFORGE_CONFIG_DIR/manifest.txt.
|
||||
#
|
||||
# Targets are fetched shallow from their real remotes (may require your ssh
|
||||
# key, depending on the target URL); nothing is pushed or written anywhere.
|
||||
# History is deliberately ignored: the run re-derives the tree through its
|
||||
# own PR history, so only content parity counts.
|
||||
#
|
||||
# Exit 0 = every repo with a declared target is IDENTICAL.
|
||||
#
|
||||
# reforge-compare
|
||||
set -euo pipefail
|
||||
|
||||
FORGE_URL=${REFORGE_FORGE_URL:-http://localhost:3030}
|
||||
ORG=${REFORGE_ORG:-sandbox-team}
|
||||
ADMIN_USER=${REFORGE_ADMIN_USER:-sandbox-admin}
|
||||
TOKENS_DIR=${REFORGE_TOKENS_DIR:-/var/lib/forgejo-sandbox/tokens}
|
||||
CONFIG_DIR=${REFORGE_CONFIG_DIR:?set REFORGE_CONFIG_DIR to your run config dir}
|
||||
TOKEN_FILE=${REFORGE_ADMIN_TOKEN_FILE:-$TOKENS_DIR/${ADMIN_USER}.token}
|
||||
TOKEN=$(cat "$TOKEN_FILE")
|
||||
|
||||
MANIFEST="$CONFIG_DIR/manifest.txt"
|
||||
[ -r "$MANIFEST" ] || { echo "compare: no readable manifest at $MANIFEST" >&2; exit 1; }
|
||||
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
auth_url() { # name
|
||||
echo "http://$ADMIN_USER:$TOKEN@${FORGE_URL#http://}/$ORG/$1.git"
|
||||
}
|
||||
|
||||
fail=0
|
||||
mapfile -t ROWS < <(grep -Ev '^[[:space:]]*(#|$)' "$MANIFEST")
|
||||
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
|
||||
echo "SKIP $name — no target declared in the manifest"
|
||||
continue
|
||||
fi
|
||||
|
||||
d="$WORK/$name"
|
||||
git init --quiet "$d"
|
||||
|
||||
if ! git -C "$d" fetch --quiet --depth 1 "$(auth_url "$name")" main 2>/dev/null; then
|
||||
echo "MISSING $name — no main in the sandbox (not seeded / not scaffolded yet)"
|
||||
fail=1
|
||||
continue
|
||||
fi
|
||||
sandbox_sha=$(git -C "$d" rev-parse FETCH_HEAD)
|
||||
|
||||
git -C "$d" fetch --quiet --depth 1 "$target_url" "$target_ref"
|
||||
target_sha=$(git -C "$d" rev-parse FETCH_HEAD)
|
||||
|
||||
if git -C "$d" diff --quiet "$sandbox_sha" "$target_sha"; then
|
||||
echo "IDENTICAL $name — matches $target_url @ $target_ref"
|
||||
else
|
||||
n=$(git -C "$d" diff --name-only "$sandbox_sha" "$target_sha" | wc -l)
|
||||
summary=$(git -C "$d" diff --shortstat "$sandbox_sha" "$target_sha")
|
||||
echo "DIFFERS $name — $n files vs $target_ref:$summary"
|
||||
fail=1
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
if [ "$fail" = 0 ]; then
|
||||
echo "compare: every targeted repo is byte-for-byte identical to its target."
|
||||
else
|
||||
echo "compare: divergence remains (DIFFERS/MISSING above)."
|
||||
fi
|
||||
exit "$fail"
|
||||
104
scripts/reforge-fetch-targets.sh
Normal file
104
scripts/reforge-fetch-targets.sh
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
#!/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; }
|
||||
80
scripts/reforge-kickoff.sh
Normal file
80
scripts/reforge-kickoff.sh
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
#!/usr/bin/env bash
|
||||
# Reforge lifecycle step 3 (docs/reforge.md): file the run agenda as issues
|
||||
# in the relevant sandbox repos. The issue rows live in
|
||||
# $REFORGE_CONFIG_DIR/issues.tsv (one per line):
|
||||
#
|
||||
# repo|title|body
|
||||
#
|
||||
# ('#' and blank lines are ignored; body is single-line markdown.) Keep
|
||||
# this file as the machine-readable twin of your agenda.md — titles should
|
||||
# carry an agenda item id (e.g. "[A1] ...") for traceability.
|
||||
#
|
||||
# Idempotent: an issue whose exact title already exists in the repo (any
|
||||
# state) is skipped, so re-running converges.
|
||||
#
|
||||
# reforge-kickoff
|
||||
set -euo pipefail
|
||||
|
||||
FORGE_URL=${REFORGE_FORGE_URL:-http://localhost:3030}
|
||||
ORG=${REFORGE_ORG:-sandbox-team}
|
||||
ADMIN_USER=${REFORGE_ADMIN_USER:-sandbox-admin}
|
||||
TOKENS_DIR=${REFORGE_TOKENS_DIR:-/var/lib/forgejo-sandbox/tokens}
|
||||
CONFIG_DIR=${REFORGE_CONFIG_DIR:?set REFORGE_CONFIG_DIR to your run config dir}
|
||||
TOKEN_FILE=${REFORGE_ADMIN_TOKEN_FILE:-$TOKENS_DIR/${ADMIN_USER}.token}
|
||||
|
||||
API="$FORGE_URL/api/v1"
|
||||
TOKEN=$(cat "$TOKEN_FILE")
|
||||
ISSUES_FILE="$CONFIG_DIR/issues.tsv"
|
||||
[ -r "$ISSUES_FILE" ] || { echo "kickoff: no readable issues file at $ISSUES_FILE" >&2; exit 1; }
|
||||
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
RESP="$WORK/resp"
|
||||
|
||||
api() { # method path [json-body] -> echoes HTTP code, body in $RESP
|
||||
local method=$1 path=$2 data=${3:-}
|
||||
local args=(
|
||||
-sS -o "$RESP" -w '%{http_code}' -X "$method"
|
||||
-H "Authorization: token $TOKEN"
|
||||
-H 'Content-Type: application/json'
|
||||
)
|
||||
if [ -n "$data" ]; then args+=(--data "$data"); fi
|
||||
curl "${args[@]}" "$API$path"
|
||||
}
|
||||
|
||||
filed=0 skipped=0 failed=0
|
||||
mapfile -t ROWS < <(grep -Ev '^[[:space:]]*(#|$)' "$ISSUES_FILE")
|
||||
current_repo=
|
||||
for row in "${ROWS[@]}"; do
|
||||
IFS='|' read -r repo title body <<<"$row"
|
||||
|
||||
# one issue-list fetch per repo (titles for the idempotency check)
|
||||
if [ "$repo" != "$current_repo" ]; then
|
||||
current_repo=$repo
|
||||
if [ "$(api GET "/repos/$ORG/$repo/issues?state=all&type=issues&limit=50")" = 200 ]; then
|
||||
jq -r '.[].title' <"$RESP" >"$WORK/titles" || : >"$WORK/titles"
|
||||
else
|
||||
: >"$WORK/titles"
|
||||
fi
|
||||
fi
|
||||
|
||||
if grep -qxF "$title" "$WORK/titles"; then
|
||||
skipped=$((skipped + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
payload=$(jq -n --arg t "$title" --arg b "$body" '{title: $t, body: $b}')
|
||||
code=$(api POST "/repos/$ORG/$repo/issues" "$payload")
|
||||
if [ "$code" = 201 ]; then
|
||||
echo " filed $repo: $title"
|
||||
filed=$((filed + 1))
|
||||
else
|
||||
echo " FAILED $repo: $title (HTTP $code)" >&2
|
||||
cat "$RESP" >&2
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
echo "kickoff: $filed filed, $skipped already present, $failed failed."
|
||||
[ "$failed" = 0 ]
|
||||
72
scripts/reforge-orchestrator.sh
Normal file
72
scripts/reforge-orchestrator.sh
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
#!/usr/bin/env bash
|
||||
# Launch the reforge ORCHESTRATOR session (docs/reforge.md, autonomous
|
||||
# mode). The agent drives the whole run: schedules role turns, routes work
|
||||
# by issue/PR number, judges convergence, checkpoints — the job the human
|
||||
# operator otherwise does. It launches fresh headless role sessions (via
|
||||
# reforge-role) and coordinates only through the forge; it never reviews or
|
||||
# implements itself, and the review gate stays inviolable (its policy denies
|
||||
# curl/push/reset — it can only merge already-approved PRs via MCP).
|
||||
#
|
||||
# reforge-orchestrator [agent args...]
|
||||
#
|
||||
# Interactive (watch it work): reforge-orchestrator
|
||||
# Then tell it e.g. "Drive Phase A to completion, stopping at the A->B
|
||||
# boundary." For unattended operation, add your own -p/--permission-mode
|
||||
# args or wrap in a loop — but read the risk notes in docs/reforge.md first
|
||||
# (nested sessions, cost, model-nondeterminism per run).
|
||||
#
|
||||
# Prepares $REFORGE_TEAM_DIR/orchestrator/ fresh each launch:
|
||||
# .claude/settings.json <- $REFORGE_SETTINGS_DIR/orchestrator-settings.json
|
||||
# .mcp.json forge MCP wired to the ADMIN token
|
||||
# CLAUDE.md <- $REFORGE_AGENTS_DIR/orchestrator.md (the playbook)
|
||||
set -euo pipefail
|
||||
|
||||
FORGE_URL=${REFORGE_FORGE_URL:-http://localhost:3030}
|
||||
ORG=${REFORGE_ORG:-sandbox-team}
|
||||
ADMIN_USER=${REFORGE_ADMIN_USER:-sandbox-admin}
|
||||
TOKENS_DIR=${REFORGE_TOKENS_DIR:-/var/lib/forgejo-sandbox/tokens}
|
||||
AGENTS_DIR=${REFORGE_AGENTS_DIR:?set REFORGE_AGENTS_DIR to the agent briefs dir}
|
||||
SETTINGS_DIR=${REFORGE_SETTINGS_DIR:?set REFORGE_SETTINGS_DIR to the permission-policy dir}
|
||||
TEAM_DIR=${REFORGE_TEAM_DIR:-$HOME/sandbox-team}
|
||||
AGENT_CMD=${REFORGE_AGENT_CMD:-claude}
|
||||
|
||||
TOKEN_FILE=${REFORGE_ADMIN_TOKEN_FILE:-$TOKENS_DIR/${ADMIN_USER}.token}
|
||||
if [ ! -r "$TOKEN_FILE" ]; then
|
||||
echo "reforge-orchestrator: no readable admin token at $TOKEN_FILE — is the sandbox provisioned?" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MCP_BIN=${REFORGE_MCP_BIN:-$(command -v forgejo-mcp || true)}
|
||||
if [ -z "$MCP_BIN" ]; then
|
||||
echo "reforge-orchestrator: no forgejo-mcp — set REFORGE_MCP_BIN or put forgejo-mcp on PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORKDIR="$TEAM_DIR/orchestrator"
|
||||
mkdir -p "$WORKDIR/.claude"
|
||||
|
||||
cp "$SETTINGS_DIR/orchestrator-settings.json" "$WORKDIR/.claude/settings.json"
|
||||
sed -e "s|@FORGE_URL@|$FORGE_URL|g" \
|
||||
-e "s|@ORG@|$ORG|g" \
|
||||
-e "s|@TOKENS_DIR@|$TOKENS_DIR|g" \
|
||||
"$AGENTS_DIR/orchestrator.md" >"$WORKDIR/CLAUDE.md"
|
||||
|
||||
cat >"$WORKDIR/.mcp.json" <<EOF
|
||||
{
|
||||
"mcpServers": {
|
||||
"forgejo-sandbox": {
|
||||
"type": "stdio",
|
||||
"command": "bash",
|
||||
"args": [
|
||||
"-c",
|
||||
"FORGEJO_ACCESS_TOKEN=\$(cat $TOKEN_FILE) FORGEJO_URL=$FORGE_URL exec $MCP_BIN --transport stdio"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "→ launching orchestrator session in $WORKDIR"
|
||||
echo " (it drives role turns via reforge-role; gate stays enforced)"
|
||||
cd "$WORKDIR"
|
||||
exec "$AGENT_CMD" "$@"
|
||||
93
scripts/reforge-reset.sh
Normal file
93
scripts/reforge-reset.sh
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
#!/usr/bin/env bash
|
||||
# Run lifecycle for the reforge sandbox (docs/reforge.md):
|
||||
#
|
||||
# reforge-reset backup [name] cold archive of the current run
|
||||
# reforge-reset reset [name] archive, then wipe to zero and reprovision
|
||||
# reforge-reset restore <tarball> put an archived run back
|
||||
#
|
||||
# Archives land in /var/lib/forgejo-sandbox-archive/<name>-<stamp>.tar.gz
|
||||
# (root-owned, 0600). They capture the FULL run record: the forge state
|
||||
# (repos, issues, PRs, reviews, users — /var/lib/forgejo) plus the
|
||||
# credentials (/var/lib/forgejo-sandbox), taken cold (services stopped) so
|
||||
# the sqlite snapshot is consistent.
|
||||
#
|
||||
# `reset` always archives first — a run is never destroyed, only closed.
|
||||
# After a reset the provisioning oneshot recreates users, tokens, org and
|
||||
# the working repo from zero; re-seed the stack repos with reforge-seed.
|
||||
#
|
||||
# Needs root (stops services, wipes /var/lib) — re-execs under sudo.
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then exec sudo "$0" "$@"; fi
|
||||
|
||||
ARCHIVE_DIR=/var/lib/forgejo-sandbox-archive
|
||||
STATE_DIRS=(forgejo forgejo-sandbox) # relative to /var/lib
|
||||
STAMP=$(date +%Y%m%d-%H%M%S)
|
||||
|
||||
usage() {
|
||||
echo "usage: $0 backup [name] | reset [name] | restore <tarball>" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
confirm() { # prompt
|
||||
local reply
|
||||
read -r -p "$1 [y/N] " reply
|
||||
case $reply in y | Y) ;; *)
|
||||
echo "aborted."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# forgejo-secrets is included: it's a RemainAfterExit oneshot, so after a
|
||||
# wipe it still reads "active" and Requires= won't re-run it — forgejo then
|
||||
# fails on the missing instance secrets. Explicitly restarting it
|
||||
# regenerates them (it converges: only writes files that are absent).
|
||||
stop_stack() { systemctl stop reforge-provision forgejo forgejo-secrets; }
|
||||
|
||||
start_stack() {
|
||||
# Recreate the /var/lib skeletons + perms (forgejo's own dirs and the
|
||||
# sandbox token dir are both tmpfiles-managed).
|
||||
systemd-tmpfiles --create
|
||||
systemctl restart forgejo-secrets
|
||||
systemctl start forgejo
|
||||
# oneshot with RemainAfterExit: restart re-runs it; converges on existing
|
||||
# state, recreates users/tokens/org/working-repo on zero.
|
||||
systemctl restart reforge-provision
|
||||
}
|
||||
|
||||
do_backup() { # name
|
||||
local out="$ARCHIVE_DIR/${1:-run}-$STAMP.tar.gz"
|
||||
mkdir -p "$ARCHIVE_DIR"
|
||||
stop_stack
|
||||
tar -C /var/lib -czf "$out" "${STATE_DIRS[@]}"
|
||||
chmod 600 "$out"
|
||||
echo "archived -> $out"
|
||||
}
|
||||
|
||||
case ${1:-} in
|
||||
backup)
|
||||
do_backup "${2:-run}"
|
||||
start_stack
|
||||
;;
|
||||
reset)
|
||||
confirm "Archive then WIPE the sandbox forge (repos, issues, PRs, users)?"
|
||||
do_backup "${2:-run}"
|
||||
rm -rf /var/lib/forgejo /var/lib/forgejo-sandbox
|
||||
start_stack
|
||||
echo "reset: fresh instance provisioned."
|
||||
echo "next: reforge-seed (as your normal user)"
|
||||
;;
|
||||
restore)
|
||||
[ -n "${2:-}" ] && [ -f "${2:-}" ] || usage
|
||||
confirm "Replace the CURRENT sandbox state with $2?"
|
||||
stop_stack
|
||||
rm -rf /var/lib/forgejo /var/lib/forgejo-sandbox
|
||||
tar -C /var/lib -xzf "$2"
|
||||
start_stack
|
||||
echo "restored $2 (tokens/passwords from that run are live again)"
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
91
scripts/reforge-role.sh
Normal file
91
scripts/reforge-role.sh
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
#!/usr/bin/env bash
|
||||
# Launch an agent session as a reforge role (docs/reforge.md,
|
||||
# docs/architecture.md). One terminal per role.
|
||||
#
|
||||
# reforge-role <role> [agent args...]
|
||||
#
|
||||
# Prepares $REFORGE_TEAM_DIR/<role>/ as the session workdir, fresh on every
|
||||
# launch (policy and briefs come from the engine / your config — hand-edits
|
||||
# to the runtime copies are overwritten):
|
||||
#
|
||||
# .claude/settings.json <- $REFORGE_SETTINGS_DIR/role-settings.json
|
||||
# .mcp.json forge MCP wired to the ROLE's token — same server
|
||||
# name as any admin-scope instance, so the role
|
||||
# token SHADOWS admin in this dir
|
||||
# CLAUDE.md role brief <- $REFORGE_AGENTS_DIR/{common,<role>}.md
|
||||
#
|
||||
# Isolation is best-effort (same unix user): the policy denies the obvious
|
||||
# escapes (ssh, git remotes, non-localhost clones/pushes, curl, other MCP
|
||||
# servers) and GIT_SSH_COMMAND=false blocks git's internal ssh — but this is
|
||||
# a discipline boundary, not a security one.
|
||||
set -euo pipefail
|
||||
|
||||
FORGE_URL=${REFORGE_FORGE_URL:-http://localhost:3030}
|
||||
ORG=${REFORGE_ORG:-sandbox-team}
|
||||
TOKENS_DIR=${REFORGE_TOKENS_DIR:-/var/lib/forgejo-sandbox/tokens}
|
||||
AGENTS_DIR=${REFORGE_AGENTS_DIR:?set REFORGE_AGENTS_DIR to the agent briefs dir}
|
||||
SETTINGS_DIR=${REFORGE_SETTINGS_DIR:?set REFORGE_SETTINGS_DIR to the permission-policy dir}
|
||||
TEAM_DIR=${REFORGE_TEAM_DIR:-$HOME/sandbox-team}
|
||||
AGENT_CMD=${REFORGE_AGENT_CMD:-claude}
|
||||
|
||||
usage() {
|
||||
echo "usage: reforge-role <role> [agent args...]" >&2
|
||||
echo -n "roles:" >&2
|
||||
for f in "$AGENTS_DIR"/*.md; do
|
||||
b=$(basename "$f" .md)
|
||||
case $b in common | orchestrator) ;; *) echo -n " $b" >&2 ;; esac
|
||||
done
|
||||
echo >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
ROLE=${1:-}
|
||||
[ -n "$ROLE" ] || usage
|
||||
shift
|
||||
BRIEF="$AGENTS_DIR/$ROLE.md"
|
||||
case $ROLE in common | orchestrator) usage ;; esac
|
||||
[ -f "$BRIEF" ] || usage
|
||||
|
||||
TOKEN_FILE="$TOKENS_DIR/$ROLE.token"
|
||||
if [ ! -r "$TOKEN_FILE" ]; then
|
||||
echo "reforge-role: no readable token at $TOKEN_FILE — is the sandbox provisioned (and does the role exist)?" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# forgejo-mcp binary: explicit override, else whatever is on PATH (the
|
||||
# NixOS module installs it). No named-host / flake resolution.
|
||||
MCP_BIN=${REFORGE_MCP_BIN:-$(command -v forgejo-mcp || true)}
|
||||
if [ -z "$MCP_BIN" ]; then
|
||||
echo "reforge-role: no forgejo-mcp — set REFORGE_MCP_BIN or put forgejo-mcp on PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORKDIR="$TEAM_DIR/$ROLE"
|
||||
mkdir -p "$WORKDIR/.claude"
|
||||
|
||||
cp "$SETTINGS_DIR/role-settings.json" "$WORKDIR/.claude/settings.json"
|
||||
sed -e "s|@ROLE@|$ROLE|g" \
|
||||
-e "s|@FORGE_URL@|$FORGE_URL|g" \
|
||||
-e "s|@FORGE_HOST@|${FORGE_URL#*://}|g" \
|
||||
-e "s|@ORG@|$ORG|g" \
|
||||
-e "s|@TOKENS_DIR@|$TOKENS_DIR|g" \
|
||||
"$AGENTS_DIR/common.md" "$BRIEF" >"$WORKDIR/CLAUDE.md"
|
||||
|
||||
cat >"$WORKDIR/.mcp.json" <<EOF
|
||||
{
|
||||
"mcpServers": {
|
||||
"forgejo-sandbox": {
|
||||
"type": "stdio",
|
||||
"command": "bash",
|
||||
"args": [
|
||||
"-c",
|
||||
"FORGEJO_ACCESS_TOKEN=\$(cat $TOKEN_FILE) FORGEJO_URL=$FORGE_URL exec $MCP_BIN --transport stdio"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "→ launching $ROLE session in $WORKDIR"
|
||||
cd "$WORKDIR"
|
||||
exec "$AGENT_CMD" "$@"
|
||||
151
scripts/reforge-seed.sh
Normal file
151
scripts/reforge-seed.sh
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
#!/usr/bin/env bash
|
||||
# Seed the sandbox forge for a reforge run (docs/reforge.md). The repo set,
|
||||
# pinned bases and declared targets live in the run manifest,
|
||||
# $REFORGE_CONFIG_DIR/manifest.txt:
|
||||
#
|
||||
# - kind=fork -> clone of the upstream at base_ref (branch, tag, or
|
||||
# commit SHA; full history), pushed as `main`, then
|
||||
# protected (no direct push, security-lead approval
|
||||
# required).
|
||||
# - kind=original -> empty placeholder repo, unprotected until a first
|
||||
# scaffold exists.
|
||||
# - charter -> in-forge copy of $REFORGE_CONFIG_DIR/charter.md
|
||||
# (README.md) + agenda.md (AGENDA.md).
|
||||
#
|
||||
# Targets are never pushed into the sandbox — the acceptance diff against
|
||||
# them is reforge-compare.
|
||||
#
|
||||
# Idempotent: every step is check-before-create / skip-if-pushed, so
|
||||
# re-running converges. NOTE: changing base_ref does NOT re-seed an
|
||||
# existing repo — reset first (reforge-reset reset).
|
||||
#
|
||||
# reforge-seed
|
||||
set -euo pipefail
|
||||
|
||||
FORGE_URL=${REFORGE_FORGE_URL:-http://localhost:3030}
|
||||
ORG=${REFORGE_ORG:-sandbox-team}
|
||||
ADMIN_USER=${REFORGE_ADMIN_USER:-sandbox-admin}
|
||||
TOKENS_DIR=${REFORGE_TOKENS_DIR:-/var/lib/forgejo-sandbox/tokens}
|
||||
CONFIG_DIR=${REFORGE_CONFIG_DIR:?set REFORGE_CONFIG_DIR to your run config dir (manifest.txt, charter.md, agenda.md)}
|
||||
TOKEN_FILE=${REFORGE_ADMIN_TOKEN_FILE:-$TOKENS_DIR/${ADMIN_USER}.token}
|
||||
|
||||
API="$FORGE_URL/api/v1"
|
||||
TOKEN=$(cat "$TOKEN_FILE")
|
||||
MANIFEST="$CONFIG_DIR/manifest.txt"
|
||||
[ -r "$MANIFEST" ] || { echo "seed: no readable manifest at $MANIFEST" >&2; exit 1; }
|
||||
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
# Loud abort: a mid-run failure (bad pin, unreachable upstream) must not
|
||||
# scroll by unnoticed — the run is incomplete until "converged" prints.
|
||||
trap 'echo; echo "seed: ABORTED — fix the error above and re-run (completed repos are skipped)." >&2' ERR
|
||||
RESP="$WORK/resp"
|
||||
|
||||
api() { # method path [json-body] -> echoes HTTP code, body in $RESP
|
||||
local method=$1 path=$2 data=${3:-}
|
||||
local args=(
|
||||
-sS -o "$RESP" -w '%{http_code}' -X "$method"
|
||||
-H "Authorization: token $TOKEN"
|
||||
-H 'Content-Type: application/json'
|
||||
)
|
||||
if [ -n "$data" ]; then args+=(--data "$data"); fi
|
||||
curl "${args[@]}" "$API$path"
|
||||
}
|
||||
ok() { case $1 in 200 | 201 | 204) return 0 ;; *) return 1 ;; esac }
|
||||
must() { # code context
|
||||
if ! ok "$1"; then
|
||||
echo "seed: $2 failed (HTTP $1):" >&2
|
||||
cat "$RESP" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
auth_url() { # name
|
||||
echo "http://$ADMIN_USER:$TOKEN@${FORGE_URL#http://}/$ORG/$1.git"
|
||||
}
|
||||
|
||||
ensure_repo() { # name description
|
||||
if [ "$(api GET "/repos/$ORG/$1")" = 404 ]; then
|
||||
must "$(api POST "/orgs/$ORG/repos" \
|
||||
"{\"name\":\"$1\",\"private\":true,\"auto_init\":false,\"default_branch\":\"main\",\"description\":\"$2\"}")" \
|
||||
"create repo $ORG/$1"
|
||||
echo " created repo $ORG/$1"
|
||||
fi
|
||||
}
|
||||
|
||||
protect_main() { # name (same rule the module puts on the working repo)
|
||||
if [ "$(api GET "/repos/$ORG/$1/branch_protections/main")" = 404 ]; then
|
||||
must "$(api POST "/repos/$ORG/$1/branch_protections" \
|
||||
'{"branch_name":"main","rule_name":"main","enable_push":false,"required_approvals":1,"enable_approvals_whitelist":true,"approvals_whitelist_username":["security-lead"],"block_on_rejected_reviews":true,"dismiss_stale_approvals":true}')" \
|
||||
"protect $ORG/$1 main"
|
||||
echo " protected main"
|
||||
fi
|
||||
}
|
||||
|
||||
seed_fork() { # name upstream ref
|
||||
local name=$1 url=$2 ref=$3 sha
|
||||
echo "-- $name <- $url @ $ref"
|
||||
ensure_repo "$name" "clean base of $url (no downstream work)"
|
||||
|
||||
if [ -n "$(git ls-remote "$(auth_url "$name")" refs/heads/main)" ]; then
|
||||
echo " main already seeded, skipping (reset to re-seed at a new base_ref)"
|
||||
else
|
||||
if [ "$ref" = auto ]; then
|
||||
ref=$(git ls-remote --symref "$url" HEAD |
|
||||
awk '/^ref:/ {sub("refs/heads/", "", $2); print $2}')
|
||||
fi
|
||||
echo " cloning at $ref…"
|
||||
if git clone --quiet --single-branch --branch "$ref" "$url" "$WORK/$name" 2>/dev/null; then
|
||||
: # branch or tag — single-branch history
|
||||
else
|
||||
# bare commit SHA — needs a full clone, then detached checkout
|
||||
git clone --quiet "$url" "$WORK/$name"
|
||||
git -C "$WORK/$name" checkout --quiet "$ref"
|
||||
fi
|
||||
sha=$(git -C "$WORK/$name" rev-parse HEAD)
|
||||
git -C "$WORK/$name" push --quiet "$(auth_url "$name")" "HEAD:refs/heads/main"
|
||||
rm -rf "${WORK:?}/$name"
|
||||
echo " seeded main = $sha ($ref)"
|
||||
fi
|
||||
|
||||
protect_main "$name"
|
||||
}
|
||||
|
||||
echo "== charter: in-forge copy of the standard + run agenda =="
|
||||
ensure_repo charter "project charter — the standard changes are judged against, plus the run agenda"
|
||||
if [ -z "$(git ls-remote "$(auth_url charter)" refs/heads/main)" ]; then
|
||||
git init --quiet -b main "$WORK/charter"
|
||||
cp "$CONFIG_DIR/charter.md" "$WORK/charter/README.md"
|
||||
cp "$CONFIG_DIR/agenda.md" "$WORK/charter/AGENDA.md"
|
||||
git -C "$WORK/charter" add README.md AGENDA.md
|
||||
git -C "$WORK/charter" \
|
||||
-c user.name="$ADMIN_USER" -c user.email="$ADMIN_USER@sandbox.invalid" \
|
||||
commit --quiet -m "charter: the standard + run agenda"
|
||||
git -C "$WORK/charter" push --quiet "$(auth_url charter)" main
|
||||
rm -rf "${WORK:?}/charter"
|
||||
echo " seeded charter (README.md + AGENDA.md <- $CONFIG_DIR)"
|
||||
fi
|
||||
protect_main charter
|
||||
|
||||
echo
|
||||
echo "== stack repos (from the manifest) =="
|
||||
mapfile -t ROWS < <(grep -Ev '^[[:space:]]*(#|$)' "$MANIFEST")
|
||||
for row in "${ROWS[@]}"; do
|
||||
IFS='|' read -r name kind upstream base_ref _target_url _target_ref <<<"$row"
|
||||
case $kind in
|
||||
fork)
|
||||
seed_fork "$name" "$upstream" "$base_ref"
|
||||
;;
|
||||
original)
|
||||
echo "-- $name (placeholder)"
|
||||
ensure_repo "$name" "placeholder — original software, to be rebuilt"
|
||||
;;
|
||||
*)
|
||||
echo "seed: unknown kind '$kind' for $name in $MANIFEST" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo
|
||||
echo "seed: converged. Repos: $FORGE_URL/$ORG"
|
||||
165
scripts/reforge-smoke.sh
Normal file
165
scripts/reforge-smoke.sh
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
#!/usr/bin/env bash
|
||||
# End-to-end smoke test of the sandbox forge toolchain (docs/architecture.md).
|
||||
# Run after every reset, before starting a run: exercises the full issue ->
|
||||
# branch -> PR -> review-gated merge flow against the working repo with the
|
||||
# provisioned role accounts, asserting both the happy path AND the gates:
|
||||
#
|
||||
# 1. backend-dev files an issue
|
||||
# 2. backend-dev pushes a feature branch
|
||||
# 3. backend-dev opens a PR referencing the issue
|
||||
# 4. merging WITHOUT approval is BLOCKED (branch protection)
|
||||
# 5. direct push to main is REJECTED (branch protection)
|
||||
# 6. reviewer leaves a comment review
|
||||
# 7. security-lead approves
|
||||
# 8. merge after approval succeeds
|
||||
# 9. main contains the change
|
||||
#
|
||||
# Leaves the issue/PR in the working repo as a record; exits non-zero if
|
||||
# any step fails.
|
||||
#
|
||||
# reforge-smoke
|
||||
set -euo pipefail
|
||||
|
||||
FORGE_URL=${REFORGE_FORGE_URL:-http://localhost:3030}
|
||||
ORG=${REFORGE_ORG:-sandbox-team}
|
||||
REPO=${REFORGE_REPO_NAME:-sandbox-project}
|
||||
TOKENS_DIR=${REFORGE_TOKENS_DIR:-/var/lib/forgejo-sandbox/tokens}
|
||||
API="$FORGE_URL/api/v1"
|
||||
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
RESP="$WORK/resp"
|
||||
STAMP=$(date +%s)
|
||||
|
||||
tok() { cat "$TOKENS_DIR/$1.token"; }
|
||||
|
||||
api() { # user method path [json-body] -> echoes HTTP code, body in $RESP
|
||||
local user=$1 method=$2 path=$3 data=${4:-}
|
||||
local args=(
|
||||
-sS -o "$RESP" -w '%{http_code}' -X "$method"
|
||||
-H "Authorization: token $(tok "$user")"
|
||||
-H 'Content-Type: application/json'
|
||||
)
|
||||
if [ -n "$data" ]; then args+=(--data "$data"); fi
|
||||
curl "${args[@]}" "$API$path"
|
||||
}
|
||||
|
||||
pass=0 fail=0
|
||||
PASS() {
|
||||
echo " PASS $*"
|
||||
pass=$((pass + 1))
|
||||
}
|
||||
FAIL() {
|
||||
echo " FAIL $*"
|
||||
fail=$((fail + 1))
|
||||
}
|
||||
die() {
|
||||
FAIL "$*"
|
||||
cat "$RESP" >&2 || true
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ── wait for the API ────────────────────────────────────────────────
|
||||
ready=
|
||||
for _ in $(seq 1 30); do
|
||||
if curl -sf "$FORGE_URL/api/healthz" >/dev/null 2>&1; then
|
||||
ready=1
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
[ "$ready" = 1 ] || {
|
||||
echo "smoke: forge at $FORGE_URL is not healthy" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "== 1. backend-dev files an issue"
|
||||
code=$(api backend-dev POST "/repos/$ORG/$REPO/issues" \
|
||||
"{\"title\":\"smoke $STAMP: verify PR flow\",\"body\":\"Tooling smoke test — issue/branch/PR/review/merge round-trip.\"}")
|
||||
[ "$code" = 201 ] || die "issue create (HTTP $code)"
|
||||
issue=$(jq .number <"$RESP")
|
||||
PASS "issue #$issue created"
|
||||
|
||||
echo "== 2. backend-dev pushes a feature branch"
|
||||
clone_url="http://backend-dev:$(tok backend-dev)@${FORGE_URL#http://}/$ORG/$REPO.git"
|
||||
git clone --quiet "$clone_url" "$WORK/repo"
|
||||
git -C "$WORK/repo" checkout --quiet -b "smoke/$STAMP"
|
||||
echo "smoke $STAMP" >"$WORK/repo/smoke-$STAMP.txt"
|
||||
git -C "$WORK/repo" add "smoke-$STAMP.txt"
|
||||
git -C "$WORK/repo" -c user.name=backend-dev -c user.email=backend-dev@sandbox.invalid \
|
||||
commit --quiet -m "smoke: add smoke-$STAMP.txt (#$issue)"
|
||||
if git -C "$WORK/repo" push --quiet origin "smoke/$STAMP"; then
|
||||
PASS "branch smoke/$STAMP pushed"
|
||||
else
|
||||
FAIL "branch push"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "== 3. backend-dev opens a PR"
|
||||
code=$(api backend-dev POST "/repos/$ORG/$REPO/pulls" \
|
||||
"{\"title\":\"smoke $STAMP: PR flow\",\"head\":\"smoke/$STAMP\",\"base\":\"main\",\"body\":\"Closes #$issue\"}")
|
||||
[ "$code" = 201 ] || die "PR create (HTTP $code)"
|
||||
pr=$(jq .number <"$RESP")
|
||||
PASS "PR #$pr opened"
|
||||
|
||||
echo "== 4. merge WITHOUT approval must be blocked"
|
||||
code=$(api backend-dev POST "/repos/$ORG/$REPO/pulls/$pr/merge" '{"Do":"merge"}')
|
||||
case $code in
|
||||
200) FAIL "unapproved merge WENT THROUGH — branch protection is not active" ;;
|
||||
*) PASS "unapproved merge blocked (HTTP $code)" ;;
|
||||
esac
|
||||
|
||||
echo "== 5. direct push to main must be rejected"
|
||||
git -C "$WORK/repo" checkout --quiet main
|
||||
echo "direct $STAMP" >"$WORK/repo/direct-$STAMP.txt"
|
||||
git -C "$WORK/repo" add "direct-$STAMP.txt"
|
||||
git -C "$WORK/repo" -c user.name=backend-dev -c user.email=backend-dev@sandbox.invalid \
|
||||
commit --quiet -m "smoke: direct push probe"
|
||||
if git -C "$WORK/repo" push --quiet origin main 2>/dev/null; then
|
||||
FAIL "direct push to main was ACCEPTED — branch protection is not active"
|
||||
else
|
||||
PASS "direct push to main rejected"
|
||||
fi
|
||||
|
||||
echo "== 6. reviewer leaves a comment review"
|
||||
code=$(api reviewer POST "/repos/$ORG/$REPO/pulls/$pr/reviews" \
|
||||
'{"event":"COMMENT","body":"smoke: quality/alignment lens present (comment review)."}')
|
||||
if [ "$code" = 200 ] || [ "$code" = 201 ]; then
|
||||
PASS "reviewer comment review posted"
|
||||
else
|
||||
FAIL "reviewer review (HTTP $code)"
|
||||
fi
|
||||
|
||||
echo "== 7. security-lead approves"
|
||||
code=$(api security-lead POST "/repos/$ORG/$REPO/pulls/$pr/reviews" \
|
||||
'{"event":"APPROVED","body":"smoke: approved by the security gate."}')
|
||||
if [ "$code" = 200 ] || [ "$code" = 201 ]; then
|
||||
PASS "security-lead approval posted"
|
||||
else
|
||||
die "security-lead approval (HTTP $code)"
|
||||
fi
|
||||
|
||||
echo "== 8. merge after approval"
|
||||
code=$(api backend-dev POST "/repos/$ORG/$REPO/pulls/$pr/merge" '{"Do":"merge"}')
|
||||
if [ "$code" = 200 ]; then
|
||||
PASS "PR #$pr merged"
|
||||
else
|
||||
die "approved merge (HTTP $code)"
|
||||
fi
|
||||
|
||||
echo "== 9. main contains the change"
|
||||
git -C "$WORK/repo" fetch --quiet origin main
|
||||
if git -C "$WORK/repo" cat-file -e "FETCH_HEAD:smoke-$STAMP.txt" 2>/dev/null; then
|
||||
PASS "smoke-$STAMP.txt present on main"
|
||||
else
|
||||
FAIL "merged file missing from main"
|
||||
fi
|
||||
|
||||
# Soft check — informational, not a gate: "Closes #N" should have
|
||||
# auto-closed the issue on merge to the default branch.
|
||||
api backend-dev GET "/repos/$ORG/$REPO/issues/$issue" >/dev/null
|
||||
echo " INFO issue #$issue state after merge: $(jq -r .state <"$RESP")"
|
||||
|
||||
echo
|
||||
echo "smoke: $pass passed, $fail failed."
|
||||
[ "$fail" = 0 ]
|
||||
64
scripts/settings/orchestrator-settings.json
Normal file
64
scripts/settings/orchestrator-settings.json
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"enableAllProjectMcpServers": true,
|
||||
"permissions": {
|
||||
"defaultMode": "acceptEdits",
|
||||
"allow": [
|
||||
"Read",
|
||||
"Glob",
|
||||
"Grep",
|
||||
"Edit",
|
||||
"Write",
|
||||
|
||||
"mcp__forgejo-sandbox",
|
||||
|
||||
"Bash(reforge-role:*)",
|
||||
"Bash(nix build:*)",
|
||||
|
||||
"Bash(git -C:*)",
|
||||
"Bash(git status)",
|
||||
"Bash(git status:*)",
|
||||
"Bash(git log:*)",
|
||||
"Bash(git diff:*)",
|
||||
"Bash(git show:*)",
|
||||
|
||||
"Bash(rg:*)",
|
||||
"Bash(fd:*)",
|
||||
"Bash(jq:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(head:*)",
|
||||
"Bash(tail:*)",
|
||||
"Bash(wc:*)",
|
||||
"Bash(echo:*)",
|
||||
"Bash(pwd)",
|
||||
"Bash(sleep:*)",
|
||||
"Bash(date)",
|
||||
"Bash(mkdir:*)"
|
||||
],
|
||||
"ask": [
|
||||
"Bash(rm:*)",
|
||||
"Bash(mv:*)",
|
||||
"Bash(cp:*)"
|
||||
],
|
||||
"deny": [
|
||||
"Bash(sudo:*)",
|
||||
"Bash(sudo)",
|
||||
"Bash(doas:*)",
|
||||
"Bash(nixos-rebuild:*)",
|
||||
"Bash(nh:*)",
|
||||
"Bash(systemctl:*)",
|
||||
"Bash(curl:*)",
|
||||
"Bash(wget:*)",
|
||||
"Bash(ssh:*)",
|
||||
"Bash(scp:*)",
|
||||
"Bash(rsync:*)",
|
||||
"Bash(nc:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(git remote:*)",
|
||||
"Bash(reforge-reset:*)",
|
||||
"WebFetch",
|
||||
"WebSearch",
|
||||
"mcp__forgejo-mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
133
scripts/settings/role-settings.json
Normal file
133
scripts/settings/role-settings.json
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
{
|
||||
"enableAllProjectMcpServers": true,
|
||||
"env": {
|
||||
"GIT_SSH_COMMAND": "false"
|
||||
},
|
||||
"permissions": {
|
||||
"defaultMode": "acceptEdits",
|
||||
"allow": [
|
||||
"Read",
|
||||
"Glob",
|
||||
"Grep",
|
||||
"Edit",
|
||||
"Write",
|
||||
"NotebookEdit",
|
||||
|
||||
"mcp__forgejo-sandbox",
|
||||
|
||||
"Bash(git status)",
|
||||
"Bash(git status:*)",
|
||||
"Bash(git diff)",
|
||||
"Bash(git diff:*)",
|
||||
"Bash(git log)",
|
||||
"Bash(git log:*)",
|
||||
"Bash(git show)",
|
||||
"Bash(git show:*)",
|
||||
"Bash(git branch)",
|
||||
"Bash(git branch:*)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git checkout:*)",
|
||||
"Bash(git switch:*)",
|
||||
"Bash(git restore:*)",
|
||||
"Bash(git stash)",
|
||||
"Bash(git stash:*)",
|
||||
"Bash(git rev-parse:*)",
|
||||
"Bash(git merge:*)",
|
||||
"Bash(git rebase:*)",
|
||||
"Bash(git clone http://localhost:*)",
|
||||
"Bash(git fetch)",
|
||||
"Bash(git fetch origin:*)",
|
||||
"Bash(git pull)",
|
||||
"Bash(git pull origin:*)",
|
||||
"Bash(git push)",
|
||||
"Bash(git push origin:*)",
|
||||
"Bash(git push --set-upstream origin:*)",
|
||||
"Bash(git push -u origin:*)",
|
||||
"Bash(git config user.name:*)",
|
||||
"Bash(git config user.email:*)",
|
||||
|
||||
"Bash(nix build:*)",
|
||||
"Bash(nix develop:*)",
|
||||
"Bash(nix flake check:*)",
|
||||
"Bash(nix flake show:*)",
|
||||
|
||||
"Bash(python3:*)",
|
||||
"Bash(pytest:*)",
|
||||
"Bash(uv run:*)",
|
||||
"Bash(uv sync:*)",
|
||||
"Bash(poetry run:*)",
|
||||
"Bash(poetry install:*)",
|
||||
"Bash(npm run:*)",
|
||||
"Bash(npm test:*)",
|
||||
"Bash(npm ci)",
|
||||
"Bash(npm install)",
|
||||
"Bash(pnpm run:*)",
|
||||
"Bash(pnpm test:*)",
|
||||
"Bash(pnpm install)",
|
||||
"Bash(node:*)",
|
||||
"Bash(make:*)",
|
||||
|
||||
"Bash(rg:*)",
|
||||
"Bash(fd:*)",
|
||||
"Bash(jq:*)",
|
||||
"Bash(diff:*)",
|
||||
"Bash(sort:*)",
|
||||
"Bash(uniq:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(xargs:*)",
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(touch:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(pwd)",
|
||||
"Bash(which:*)",
|
||||
"Bash(type:*)",
|
||||
"Bash(env)",
|
||||
"Bash(echo:*)",
|
||||
"Bash(file:*)",
|
||||
"Bash(stat:*)",
|
||||
"Bash(wc:*)",
|
||||
"Bash(head:*)",
|
||||
"Bash(tail:*)",
|
||||
"Bash(cat:*)"
|
||||
],
|
||||
"ask": [
|
||||
"Bash(rm:*)",
|
||||
"Bash(rmdir:*)",
|
||||
"Bash(mv:*)",
|
||||
"Bash(cp:*)",
|
||||
"Bash(chmod:*)",
|
||||
"Bash(chown:*)"
|
||||
],
|
||||
"deny": [
|
||||
"Bash(sudo:*)",
|
||||
"Bash(sudo)",
|
||||
"Bash(doas:*)",
|
||||
"Bash(su:*)",
|
||||
"Bash(nixos-rebuild:*)",
|
||||
"Bash(nh:*)",
|
||||
"Bash(nix run:*)",
|
||||
"Bash(git remote:*)",
|
||||
"Bash(git push ssh:*)",
|
||||
"Bash(git push git@:*)",
|
||||
"Bash(git push https:*)",
|
||||
"Bash(git clone ssh:*)",
|
||||
"Bash(git clone git@:*)",
|
||||
"Bash(git clone https:*)",
|
||||
"Bash(git fetch https:*)",
|
||||
"Bash(git fetch ssh:*)",
|
||||
"Bash(curl:*)",
|
||||
"Bash(wget:*)",
|
||||
"Bash(ssh:*)",
|
||||
"Bash(scp:*)",
|
||||
"Bash(rsync:*)",
|
||||
"Bash(nc:*)",
|
||||
"Bash(systemctl:*)",
|
||||
"Bash(docker:*)",
|
||||
"Bash(podman:*)",
|
||||
"WebFetch",
|
||||
"WebSearch",
|
||||
"mcp__forgejo-mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue