From ff79cb15a58d22107bd8074e4d6421f045b13eb0 Mon Sep 17 00:00:00 2001 From: Agent Date: Wed, 1 Apr 2026 06:46:57 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20feat:=20branch=20protection=20on=20ops?= =?UTF-8?q?=20repo=20=E2=80=94=20require=20admin=20approval=20for=20vault?= =?UTF-8?q?=20PRs=20(#77)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 3 +- README.md | 3 + docs/VAULT.md | 98 ++++++++++++++ lib/branch-protection.sh | 285 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 388 insertions(+), 1 deletion(-) create mode 100644 docs/VAULT.md create mode 100644 lib/branch-protection.sh diff --git a/AGENTS.md b/AGENTS.md index f17b287..a12b61f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ all via cron and `claude -p`. The dispatcher executes formula-based operational tasks. > **Note:** The vault is being redesigned as a PR-based approval workflow on the -> ops repo (see issues #73-#77). Old vault scripts are being removed. +> ops repo (see issues #73-#77). See [docs/VAULT.md](docs/VAULT.md) for details. Old vault scripts are being removed. See `README.md` for the full architecture and `disinto-factory/SKILL.md` for setup. @@ -95,6 +95,7 @@ bash dev/phase-test.sh | Predictor | `predictor/` | Infrastructure pattern detection | [predictor/AGENTS.md](predictor/AGENTS.md) | > **Vault:** Being redesigned as a PR-based approval workflow (issues #73-#77). +> See [docs/VAULT.md](docs/VAULT.md) for the vault PR workflow details. See [lib/AGENTS.md](lib/AGENTS.md) for the full shared helper reference. diff --git a/README.md b/README.md index f6a7165..40c9889 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,8 @@ disinto/ │ └── (formula-driven) # run-planner.toml executed by dispatcher ├── vault/ │ └── vault-env.sh # Shared env setup (vault redesign in progress, see #73-#77) +├── docs/ +│ └── VAULT.md # Vault PR workflow and branch protection documentation └── supervisor/ ├── supervisor-poll.sh # Supervisor: health checks + claude -p ├── update-prompt.sh # Self-learning: append to best-practices @@ -146,6 +148,7 @@ disinto/ | **Planner** | Weekly | Updates AGENTS.md documentation to reflect recent code changes, then gap-analyses VISION.md vs current state and creates up to 5 backlog issues for the highest-leverage gaps. | > **Vault:** Being redesigned as a PR-based approval workflow (issues #73-#77). +> See [docs/VAULT.md](docs/VAULT.md) for the vault PR workflow and branch protection details. ## Design Principles diff --git a/docs/VAULT.md b/docs/VAULT.md new file mode 100644 index 0000000..da2c1a9 --- /dev/null +++ b/docs/VAULT.md @@ -0,0 +1,98 @@ +# Vault PR Workflow + +This document describes the vault PR-based approval workflow for the ops repo. + +## Overview + +The vault system enables agents to request execution of privileged actions (deployments, token operations, etc.) through a PR-based approval process. This replaces the old vault directory structure with a more auditable, collaborative workflow. + +## Branch Protection + +The `main` branch on the ops repo (`johba/disinto-ops`) is protected via Forgejo branch protection to enforce: + +- **Require 1 approval before merge** — All vault PRs must have at least one approval from an admin user +- **Admin-only merge** — Only users with admin role can merge vault PRs (regular collaborators and bot accounts cannot) +- **Block direct pushes** — All changes to `main` must go through PRs + +### Protection Rules + +| Setting | Value | +|---------|-------| +| `enable_push` | `false` | +| `enable_force_push` | `false` | +| `enable_merge_commit` | `true` | +| `required_approvals` | `1` | +| `admin_enforced` | `true` | + +## Vault PR Lifecycle + +1. **Request** — Agent calls `lib/vault.sh:vault_request()` with action TOML content +2. **Validation** — TOML is validated against the schema in `vault/vault-env.sh` +3. **PR Creation** — A PR is created on `disinto-ops` with: + - Branch: `vault/` + - Title: `vault: ` + - Labels: `vault`, `pending-approval` + - File: `vault/actions/.toml` +4. **Approval** — Admin user reviews and approves the PR +5. **Execution** — Dispatcher (issue #76) polls for approved vault PRs and executes them +6. **Cleanup** — Executed vault items are moved to `fired/` (via PR) + +## Bot Account Behavior + +Bot accounts (dev-bot, review-bot, vault-bot, etc.) **cannot merge vault PRs** even if they have approval, due to the `admin_enforced` setting. This ensures: + +- Only human admins can approve sensitive vault actions +- Bot accounts can only create vault PRs, not execute them +- Manual admin review is always required for privileged operations + +## Setup + +To set up branch protection on the ops repo: + +```bash +# Source environment +source lib/env.sh +source lib/branch-protection.sh + +# Set up protection +setup_vault_branch_protection main + +# Verify setup +verify_branch_protection main +``` + +Or use the CLI directly: + +```bash +export FORGE_TOKEN="" +export FORGE_URL="https://codeberg.org" +export FORGE_OPS_REPO="johba/disinto-ops" + +# Set up protection +bash lib/branch-protection.sh setup main + +# Verify +bash lib/branch-protection.sh verify main +``` + +## Testing + +To verify the protection is working: + +1. **Bot cannot merge** — Attempt to merge a PR with a bot token (should fail with HTTP 405) +2. **Admin can merge** — Attempt to merge with admin token (should succeed) +3. **Direct push blocked** — Attempt `git push origin main` (should be rejected) + +## Related Issues + +- #73 — Vault redesign proposal +- #74 — Vault action TOML schema +- #75 — Vault PR creation helper (`lib/vault.sh`) +- #76 — Dispatcher rewrite (poll for merged vault PRs) +- #77 — Branch protection on ops repo (this issue) + +## See Also + +- [`lib/vault.sh`](../lib/vault.sh) — Vault PR creation helper +- [`vault/vault-env.sh`](../vault/vault-env.sh) — TOML validation +- [`lib/branch-protection.sh`](../lib/branch-protection.sh) — Branch protection helper diff --git a/lib/branch-protection.sh b/lib/branch-protection.sh new file mode 100644 index 0000000..340d53a --- /dev/null +++ b/lib/branch-protection.sh @@ -0,0 +1,285 @@ +#!/usr/bin/env bash +# branch-protection.sh — Helper for setting up branch protection on repos +# +# Source after lib/env.sh: +# source "$(dirname "$0")/../lib/env.sh" +# source "$(dirname "$0")/lib/branch-protection.sh" +# +# Required globals: FORGE_TOKEN, FORGE_URL, FORGE_OPS_REPO +# +# Functions: +# setup_vault_branch_protection — Set up admin-only branch protection for main +# verify_branch_protection — Verify protection is configured correctly +# remove_branch_protection — Remove branch protection (for cleanup/testing) +# +# Branch protection settings: +# - Require 1 approval before merge +# - Restrict merge to admin role (not regular collaborators or bots) +# - Block direct pushes to main (all changes must go through PR) + +set -euo pipefail + +# Internal log helper +_bp_log() { + if declare -f log >/dev/null 2>&1; then + log "branch-protection: $*" + else + printf '[%s] branch-protection: %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*" >&2 + fi +} + +# Get ops repo API URL +_ops_api() { + printf '%s' "${FORGE_URL}/api/v1/repos/${FORGE_OPS_REPO}" +} + +# ----------------------------------------------------------------------------- +# setup_vault_branch_protection — Set up admin-only branch protection for main +# +# Configures the following protection rules: +# - Require 1 approval before merge +# - Restrict merge to admin role (not regular collaborators or bots) +# - Block direct pushes to main (all changes must go through PR) +# +# Returns: 0 on success, 1 on failure +# ----------------------------------------------------------------------------- +setup_vault_branch_protection() { + local branch="${1:-main}" + local api_url + api_url="$(_ops_api)" + + _bp_log "Setting up branch protection for ${branch} on ${FORGE_OPS_REPO}" + + # Check if branch exists + local branch_exists + branch_exists=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: token ${FORGE_TOKEN}" \ + "${api_url}/git/branches/${branch}" 2>/dev/null || echo "0") + + if [ "$branch_exists" != "200" ]; then + _bp_log "ERROR: Branch ${branch} does not exist" + return 1 + fi + + # Check if protection already exists + local protection_exists + protection_exists=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: token ${FORGE_TOKEN}" \ + "${api_url}/branches/${branch}/protection" 2>/dev/null || echo "0") + + if [ "$protection_exists" = "200" ]; then + _bp_log "Branch protection already exists for ${branch}" + _bp_log "Updating existing protection rules" + fi + + # Create/update branch protection + # Note: Forgejo API uses "require_signed_commits" and "required_approvals" for approval requirements + # The "admin_enforced" field ensures only admins can merge + local protection_json + protection_json=$(cat </dev/null || true) + + if [ -z "$protection_json" ] || [ "$protection_json" = "null" ]; then + _bp_log "ERROR: No branch protection found for ${branch}" + return 1 + fi + + # Extract and validate settings + local enable_push enable_merge_commit required_approvals admin_enforced + enable_push=$(printf '%s' "$protection_json" | jq -r '.enable_push // true') + enable_merge_commit=$(printf '%s' "$protection_json" | jq -r '.enable_merge_commit // false') + required_approvals=$(printf '%s' "$protection_json" | jq -r '.required_approvals // 0') + admin_enforced=$(printf '%s' "$protection_json" | jq -r '.admin_enforced // false') + + local errors=0 + + # Check push is disabled + if [ "$enable_push" = "true" ]; then + _bp_log "ERROR: enable_push should be false" + errors=$((errors + 1)) + else + _bp_log "OK: Pushes are blocked" + fi + + # Check merge commit is enabled + if [ "$enable_merge_commit" != "true" ]; then + _bp_log "ERROR: enable_merge_commit should be true" + errors=$((errors + 1)) + else + _bp_log "OK: Merge commits are allowed" + fi + + # Check required approvals + if [ "$required_approvals" -lt 1 ]; then + _bp_log "ERROR: required_approvals should be at least 1" + errors=$((errors + 1)) + else + _bp_log "OK: Required approvals: ${required_approvals}" + fi + + # Check admin enforced + if [ "$admin_enforced" != "true" ]; then + _bp_log "ERROR: admin_enforced should be true" + errors=$((errors + 1)) + else + _bp_log "OK: Admin enforcement enabled" + fi + + if [ "$errors" -gt 0 ]; then + _bp_log "Verification failed with ${errors} error(s)" + return 1 + fi + + _bp_log "Branch protection verified successfully" + return 0 +} + +# ----------------------------------------------------------------------------- +# remove_branch_protection — Remove branch protection (for cleanup/testing) +# +# Returns: 0 on success, 1 on failure +# ----------------------------------------------------------------------------- +remove_branch_protection() { + local branch="${1:-main}" + local api_url + api_url="$(_ops_api)" + + _bp_log "Removing branch protection for ${branch}" + + # Check if protection exists + local protection_exists + protection_exists=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: token ${FORGE_TOKEN}" \ + "${api_url}/branches/${branch}/protection" 2>/dev/null || echo "0") + + if [ "$protection_exists" != "200" ]; then + _bp_log "No branch protection found for ${branch}" + return 0 + fi + + # Delete protection + local http_code + http_code=$(curl -s -o /dev/null -w "%{http_code}" \ + -X DELETE \ + -H "Authorization: token ${FORGE_TOKEN}" \ + "${api_url}/branches/${branch}/protection" 2>/dev/null || echo "0") + + if [ "$http_code" != "204" ]; then + _bp_log "ERROR: Failed to remove branch protection (HTTP ${http_code})" + return 1 + fi + + _bp_log "Branch protection removed successfully for ${branch}" + return 0 +} + +# ----------------------------------------------------------------------------- +# Test mode — run when executed directly +# ----------------------------------------------------------------------------- +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + # Check required env vars + if [ -z "${FORGE_TOKEN:-}" ]; then + echo "ERROR: FORGE_TOKEN is required" >&2 + exit 1 + fi + + if [ -z "${FORGE_URL:-}" ]; then + echo "ERROR: FORGE_URL is required" >&2 + exit 1 + fi + + if [ -z "${FORGE_OPS_REPO:-}" ]; then + echo "ERROR: FORGE_OPS_REPO is required" >&2 + exit 1 + fi + + # Parse command line args + case "${1:-help}" in + setup) + setup_vault_branch_protection "${2:-main}" + ;; + verify) + verify_branch_protection "${2:-main}" + ;; + remove) + remove_branch_protection "${2:-main}" + ;; + help|*) + echo "Usage: $0 {setup|verify|remove} [branch]" + echo "" + echo "Commands:" + echo " setup [branch] Set up branch protection (default: main)" + echo " verify [branch] Verify branch protection is configured correctly" + echo " remove [branch] Remove branch protection (for cleanup/testing)" + echo "" + echo "Required environment variables:" + echo " FORGE_TOKEN Forgejo API token (admin user recommended)" + echo " FORGE_URL Forgejo instance URL (e.g., https://codeberg.org)" + echo " FORGE_OPS_REPO Ops repo in format owner/repo (e.g., johba/disinto-ops)" + exit 0 + ;; + esac +fi -- 2.49.1