diff --git a/docs/plans/CONDUCTOR.md b/docs/plans/CONDUCTOR.md index 4f047924..0d5deff5 100644 --- a/docs/plans/CONDUCTOR.md +++ b/docs/plans/CONDUCTOR.md @@ -2,6 +2,13 @@ How the stack orchestrates headless pi workers to do work on itself. +## Role contracts + +Role authority is declared in role contracts, one file per role, under +`roles/` (e.g. `roles/conductor-policy.json`). The repository root holds +only first-class, bootstrap-required configuration; role contracts are +tracked, versioned files whose changes arrive as reviewed commits. + ## Roles | Role | Runs where | Powers | Never has | diff --git a/conductor-policy.json b/roles/conductor-policy.json similarity index 100% rename from conductor-policy.json rename to roles/conductor-policy.json diff --git a/scripts/conductor-apply.sh b/scripts/conductor-apply.sh index 3492dd92..0b305473 100755 --- a/scripts/conductor-apply.sh +++ b/scripts/conductor-apply.sh @@ -3,7 +3,7 @@ # # Usage: scripts/conductor-apply.sh [--dry-run] # -# Policy (conductor-policy.json in the target repo, strictly validated): +# Policy (roles/conductor-policy.json in the target repo, strictly validated): # autoApply.enabled master switch # autoApply.allowedPaths glob allowlist ('dir/**' = everything under dir) # autoApply.suites suite scripts that must pass AFTER applying @@ -27,19 +27,19 @@ cd "$TARGET_ROOT" fail() { echo "conductor-apply: $*" >&2; exit "${2:-1}"; } [ -d .git ] || fail "target is not a git repository: $TARGET_ROOT" 4 -[ -f conductor-policy.json ] || fail "no conductor-policy.json in target" 2 +[ -f roles/conductor-policy.json ] || fail "no roles/conductor-policy.json in target" 2 # ---- policy (strict) ---- POLICY_JSON="$(node -e ' const fs = require("fs"); -const p = JSON.parse(fs.readFileSync("conductor-policy.json", "utf8")); +const p = JSON.parse(fs.readFileSync("roles/conductor-policy.json", "utf8")); if (p.policyVersion !== 1) process.exit(3); if (!p.autoApply || typeof p.autoApply.enabled !== "boolean" || !Array.isArray(p.autoApply.allowedPaths) || !Array.isArray(p.autoApply.suites)) process.exit(3); for (const g of p.autoApply.allowedPaths) { if (typeof g !== "string" || !/^[A-Za-z0-9_.*/-]+$/.test(g) || g.startsWith("/") || g.includes("..")) process.exit(3); } console.log(JSON.stringify(p.autoApply)); -')" || fail "invalid conductor-policy.json" 2 +')" || fail "invalid roles/conductor-policy.json" 2 ENABLED="$(node -e 'console.log(JSON.parse(process.argv[1]).enabled)' "$POLICY_JSON")" [ "$ENABLED" = "true" ] || fail "auto-apply is disabled by policy" 2 diff --git a/scripts/test-conductor.sh b/scripts/test-conductor.sh index e93fea34..da81800d 100755 --- a/scripts/test-conductor.sh +++ b/scripts/test-conductor.sh @@ -40,8 +40,9 @@ check() { 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 +mkdir -p "$SANDBOX/repo/roles" +cp roles/conductor-policy.json "$SANDBOX/repo/roles/conductor-policy.json" +git -C "$SANDBOX/repo" add roles/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" @@ -66,8 +67,8 @@ 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 + > "$TARGET/roles/conductor-policy.json" + git -C "$TARGET" add roles/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" @@ -131,9 +132,9 @@ 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" +printf '{"policyVersion":9}' > "$TARGET/roles/conductor-policy.json" check_rc "invalid policy exits 2" 2 scripts/conductor-apply.sh "$RUN_OK" -git -C "$TARGET" checkout -q -- conductor-policy.json +git -C "$TARGET" checkout -q -- roles/conductor-policy.json echo echo "selftest: $PASS passed, $FAIL failed"