feat(conductor): auto-apply policy gate for worker patches (#34)
- conductor-policy.json (tracked, strictly validated): enabled switch, path allowlist globs, gating suites - the autonomy decision lives in a declarative file the owner controls - scripts/conductor-apply.sh <runId> [--dry-run]: succeeded-run check -> clean target tree -> diff from worker workspace -> allowlist -> syntax gates (node/bash/json) -> apply -> policy suites -> attribution commit; ANY failure reverts the tree; push is never automatic - scripts/test-conductor.sh: 17 sandbox cases covering every gate incl. suite-failure auto-revert and disabled policy - policy defaults: scripts/docs/tasks/missions/adapters + README; all three suites gate Closes #34
This commit is contained in:
Executable
+140
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sandboxed selftests for the conductor auto-apply policy gate.
|
||||
#
|
||||
# Builds a throwaway target repo + worker workspace + fake run records, then
|
||||
# exercises every gate: policy validation, allowlist, syntax gates, suite
|
||||
# failure revert, disabled policy, missing/failed runs. No real model calls.
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
SANDBOX="$(mktemp -d)"
|
||||
trap 'rm -rf "$SANDBOX"' EXIT
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
# Status colors: terminal-only, NO_COLOR-respecting; plain when piped.
|
||||
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
|
||||
C_OK=$'\033[0;32m'; C_FAIL=$'\033[0;31m'; C_RESET=$'\033[0m'
|
||||
else
|
||||
C_OK=""; C_FAIL=""; C_RESET=""
|
||||
fi
|
||||
|
||||
check_rc() { # name expectedRc command...
|
||||
local name="$1" expected="$2"
|
||||
shift 2
|
||||
local rc
|
||||
"$@" >/dev/null 2>&1
|
||||
rc=$?
|
||||
if [ "$rc" -eq "$expected" ]; then
|
||||
PASS=$((PASS+1)); echo "${C_OK}OK${C_RESET} $name (exit $rc)"
|
||||
else
|
||||
FAIL=$((FAIL+1)); echo "${C_FAIL}FAIL${C_RESET} $name (exit $rc, expected $expected)"
|
||||
fi
|
||||
}
|
||||
|
||||
check() {
|
||||
if [ "$2" = "0" ]; then PASS=$((PASS+1)); echo "${C_OK}OK${C_RESET} $1"; else FAIL=$((FAIL+1)); echo "${C_FAIL}FAIL${C_RESET} $1"; fi
|
||||
}
|
||||
|
||||
# ---- infrastructure: target repo + worker workspace + fake run ----
|
||||
git clone -q . "$SANDBOX/repo"
|
||||
# The clone carries committed state only - give the target its policy and
|
||||
# commit it so the tree starts clean (untracked policy would fail target_clean).
|
||||
cp conductor-policy.json "$SANDBOX/repo/conductor-policy.json"
|
||||
git -C "$SANDBOX/repo" add conductor-policy.json
|
||||
git -C "$SANDBOX/repo" -c user.name=suite -c user.email=suite@local commit -q -m policy
|
||||
mkdir -p "$SANDBOX/data/workspaces" "$SANDBOX/data/runs"
|
||||
git clone -q "$SANDBOX/repo" "$SANDBOX/data/workspaces/stack-repo"
|
||||
|
||||
TARGET="$SANDBOX/repo"
|
||||
WS="$SANDBOX/data/workspaces/stack-repo"
|
||||
cat > "$SANDBOX/config.json" <<EOF
|
||||
{"configVersion":1,"environment":"development","dataRoot":"$SANDBOX/data","execution":{"backend":"docker","provider":"zai","model":"m","adapter":"mock"}}
|
||||
EOF
|
||||
export MOSAIC_APPLY_TARGET="$TARGET"
|
||||
export MOSAIC_CONFIG="$SANDBOX/config.json"
|
||||
|
||||
RUN_OK="r-20260903T000000000Z-ok0000001"
|
||||
mkdir -p "$SANDBOX/data/runs/$RUN_OK"
|
||||
printf '{"runVersion":1,"runId":"%s","taskId":"t-fake","status":"succeeded","workspace":"stack-repo"}' "$RUN_OK" \
|
||||
> "$SANDBOX/data/runs/$RUN_OK/result.json"
|
||||
|
||||
ws_edit() { printf '\n%s\n' "$2" >> "$WS/$1"; }
|
||||
ws_reset() { git -C "$WS" checkout -q -- . 2>/dev/null; git -C "$WS" clean -qfd; }
|
||||
target_clean() { [ -z "$(git -C "$TARGET" status --porcelain)" ]; }
|
||||
|
||||
set_policy() { # enabled suites (commits: the target tree must stay clean)
|
||||
local suites="[\"$2\"]"
|
||||
printf '{"policyVersion":1,"autoApply":{"enabled":%s,"allowedPaths":["scripts/**","docs/**","README.md"],"suites":%s}}' "$1" "$suites" \
|
||||
> "$TARGET/conductor-policy.json"
|
||||
git -C "$TARGET" add conductor-policy.json
|
||||
git -C "$TARGET" -c user.name=suite -c user.email=suite@local commit -q -m "policy update"
|
||||
}
|
||||
set_policy true "test-config"
|
||||
|
||||
# T1: dry run - allowed change, nothing applied
|
||||
ws_edit "README.md" "worker dry-run line"
|
||||
check_rc "dry-run: allowed change, exit 0, nothing committed" 0 \
|
||||
scripts/conductor-apply.sh "$RUN_OK" --dry-run
|
||||
if git -C "$TARGET" log --format=%s | grep -q "auto-applied"; then
|
||||
check "dry-run committed nothing" 1
|
||||
else
|
||||
check "dry-run committed nothing" 0
|
||||
fi
|
||||
ws_reset
|
||||
|
||||
# T2: apply - allowed change, suites pass, commit created
|
||||
ws_edit "README.md" "worker applied line"
|
||||
check_rc "apply: allowed change exits 0" 0 scripts/conductor-apply.sh "$RUN_OK"
|
||||
git -C "$TARGET" log -1 --format=%s | grep -q "auto-applied patch from run $RUN_OK" \
|
||||
&& check "apply: attribution in commit subject" 0 || check "apply: attribution in commit subject" 1
|
||||
target_clean() { [ -z "$(git -C "$TARGET" status --porcelain)" ]; }
|
||||
target_clean && check "apply: target tree clean after commit" 0 || check "apply: target tree clean after commit" 1
|
||||
git -C "$TARGET" reset -q --hard HEAD~1
|
||||
|
||||
# T3: disallowed path refused
|
||||
ws_edit "Containerfile" "# worker touch"
|
||||
check_rc "disallowed path refused" 1 scripts/conductor-apply.sh "$RUN_OK"
|
||||
target_clean && check "disallowed path: target untouched" 0 || check "disallowed path: target untouched" 1
|
||||
ws_reset
|
||||
|
||||
# T4: syntax gate - broken .mjs on an allowed path
|
||||
printf 'this is not (valid js\n' > "$WS/scripts/broken-worker.mjs"
|
||||
check_rc "syntax gate refused broken .mjs" 1 scripts/conductor-apply.sh "$RUN_OK"
|
||||
target_clean && check "syntax gate: target untouched" 0 || check "syntax gate: target untouched" 1
|
||||
ws_reset
|
||||
|
||||
# T5: suite failure - allowed change breaks a policy suite -> auto-revert
|
||||
printf '\nexit 7\n' >> "$TARGET/scripts/test-config.sh"
|
||||
ws_edit "README.md" "worker change that will fail suites"
|
||||
check_rc "suite failure refused" 1 scripts/conductor-apply.sh "$RUN_OK"
|
||||
git -C "$TARGET" checkout -q -- scripts/test-config.sh
|
||||
target_clean && check "suite failure: target reverted to clean" 0 || check "suite failure: target reverted to clean" 1
|
||||
|
||||
# T6: disabled policy
|
||||
set_policy false "test-config"
|
||||
ws_edit "README.md" "worker line while disabled"
|
||||
check_rc "disabled policy refused" 2 scripts/conductor-apply.sh "$RUN_OK"
|
||||
target_clean && check "disabled policy: target untouched" 0 || check "disabled policy: target untouched" 1
|
||||
ws_reset
|
||||
set_policy true "test-config"
|
||||
|
||||
# T7: failed run refused
|
||||
RUN_FAIL="r-20260903T000000000Z-fail00001"
|
||||
mkdir -p "$SANDBOX/data/runs/$RUN_FAIL"
|
||||
printf '{"runVersion":1,"runId":"%s","taskId":"t","status":"failed","workspace":"stack-repo"}' "$RUN_FAIL" \
|
||||
> "$SANDBOX/data/runs/$RUN_FAIL/result.json"
|
||||
ws_edit "README.md" "worker line from failed run"
|
||||
check_rc "failed run refused" 1 scripts/conductor-apply.sh "$RUN_FAIL"
|
||||
target_clean && check "failed run: target untouched" 0 || check "failed run: target untouched" 1
|
||||
ws_reset
|
||||
|
||||
# T8/T9: missing run + invalid policy
|
||||
check_rc "missing run exits 4" 4 scripts/conductor-apply.sh r-missing
|
||||
printf '{"policyVersion":9}' > "$TARGET/conductor-policy.json"
|
||||
check_rc "invalid policy exits 2" 2 scripts/conductor-apply.sh "$RUN_OK"
|
||||
git -C "$TARGET" checkout -q -- conductor-policy.json
|
||||
|
||||
echo
|
||||
echo "selftest: $PASS passed, $FAIL failed"
|
||||
[ "$FAIL" -eq 0 ]
|
||||
Reference in New Issue
Block a user