Agentic Covenants

Recover (RC) · Approval gating

Approval gating at the server side layer

external · Outside the agent entirely

How do I get back to a known-good state and not repeat this?

What this cell does

Re-enable branch protection (enforce_admins true), re-add CODEOWNERS, audit bypass events from incident, unfreeze deployments only after rest of recovery is verified.

Artifacts (2)

agent-restore-approval-serverview on GitHub
#!/usr/bin/env bash
# ABOUTME: Server-side approval-gating rebuild. Restore branch protection, re-enable workflows, audit bypasses, unfreeze on flag.
# ABOUTME: Refuses to apply branch protection without enforce_admins: true. --unfreeze must be explicit.

set -euo pipefail

UNFREEZE="0"
ARGS=()
for arg in "$@"; do
  case "$arg" in
    --unfreeze) UNFREEZE="1" ;;
    *)          ARGS+=("$arg") ;;
  esac
done
set -- "${ARGS[@]}"

if [[ $# -lt 2 ]]; then
  echo "Usage: agent-restore-approval-server <AGENT_NAME> <INCIDENT_ID> [--unfreeze]" >&2
  echo "  REPO=org/repo agent-restore-approval-server claude-code-prod $(uuidgen) --unfreeze" >&2
  exit 64
fi

AGENT_NAME="$1"
INCIDENT_ID="$2"
REPO="${REPO:?REPO must be set to GitHub owner/repo}"
PROTECTION_FILE="${PROTECTION_FILE:-./controls/approval-gating/server-side/branch-protection-expected.json}"

if [[ ! -r "$PROTECTION_FILE" ]]; then
  echo "REFUSING: $PROTECTION_FILE not found" >&2
  exit 1
fi

# 1. Verify the source-of-truth has enforce_admins: true before restoring.
ENFORCE_ADMINS="$(jq -r '.enforce_admins // false' "$PROTECTION_FILE")"
if [[ "$ENFORCE_ADMINS" != "true" ]]; then
  echo "REFUSING: $PROTECTION_FILE has enforce_admins=$ENFORCE_ADMINS." >&2
  echo "enforce_admins=true is the load-bearing flag for branch protection. Fix source before restore." >&2
  exit 1
fi

# 2. Restore branch protection from source.
gh api -X PUT "repos/$REPO/branches/main/protection" \
  --input "$PROTECTION_FILE" >/dev/null

# 3. Re-enable every workflow that was disabled during Interventions.
mapfile -t WORKFLOW_IDS < <(
  gh api "repos/$REPO/actions/workflows" --jq '.workflows[].id'
)
for wid in "${WORKFLOW_IDS[@]}"; do
  gh api -X PUT "repos/$REPO/actions/workflows/$wid/enable" >/dev/null 2>&1 || true
done

# 4. Audit bypass events from the incident window. Output to stderr; the
# operator must review before considering recovery complete.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -x "$SCRIPT_DIR/audit-bypass-events.sh" ]]; then
  echo
  echo "Bypass-event audit (review each entry before unfreezing):"
  "$SCRIPT_DIR/audit-bypass-events.sh" "$REPO" "$INCIDENT_ID" || true
fi

# 5. Unfreeze deployments only when --unfreeze is explicitly passed.
if [[ "$UNFREEZE" == "1" ]]; then
  gh variable set DEPLOY_FREEZE -b false -R "$REPO" >/dev/null
  echo "DEPLOY_FREEZE set to false."
else
  echo
  echo "DEPLOY_FREEZE not changed. Re-run with --unfreeze when ready."
fi

logger -t agent-recovery -p user.notice \
  "$(jq -n \
      --arg event "approval_restored_server" \
      --arg agent "$AGENT_NAME" \
      --arg repo "$REPO" \
      --arg incident "$INCIDENT_ID" \
      --arg unfreeze "$UNFREEZE" \
      --arg actor "$(whoami)" \
      --arg ts "$(date -Iseconds)" \
      '{event:$event, agent:$agent, repo:$repo, incident:$incident, unfreeze:$unfreeze, actor:$actor, ts:$ts}')"

echo "Server-side approval gating restored for $AGENT_NAME on $REPO"
audit-bypass-events.shview on GitHub
#!/usr/bin/env bash
# ABOUTME: Lists pushes to protected branches during an incident window with forced: true or admin-bypass markers.
# ABOUTME: Output is for human review; any commit that landed via bypass needs to be inspected for malicious change.

set -euo pipefail

REPO="${1:?Usage: audit-bypass-events.sh <REPO> <INCIDENT_ID>}"
INCIDENT_ID="${2:?Usage: audit-bypass-events.sh <REPO> <INCIDENT_ID>}"
SINCE="${INCIDENT_START:-}"
UNTIL="${INCIDENT_END:-}"

if [[ -z "$SINCE" || -z "$UNTIL" ]]; then
  echo "Set INCIDENT_START and INCIDENT_END as ISO-8601 timestamps. The earliest IoC from Sentinels is the right SINCE; the time Interventions completed is the right UNTIL." >&2
  exit 64
fi

echo "Auditing $REPO for bypass events from $SINCE to $UNTIL ..."
echo

# Force-pushes during the window. branch_protection_rule events would also
# show in the GitHub Audit Log API; this script uses the events API as a
# starting point.
gh api "repos/$REPO/events?per_page=100" \
  --jq ".[] | select(.created_at >= \"$SINCE\" and .created_at <= \"$UNTIL\")
        | select(.type == \"PushEvent\" and .payload.forced == true)
        | {created_at, actor: .actor.login, ref: .payload.ref, head: .payload.head}"

echo
echo "Cross-reference each line above with PR review records. Any commit landed via force-push during the incident window must be reviewed."

Cell notes

Restorations, Approval gating / Server-side

Precondition. Interventions L3-C4 has fired (branch protection locked, workflows disabled, DEPLOY_FREEZE engaged, environments locked). Restorations identity, authorization, and blast-radius rows complete.

Authority. On-call plus security review.

Tooling

  • - gh CLI authenticated with repo_admin.
  • - The branch-protection JSON in source: controls/approval-gating/server-side/branch-protection-expected.json.

Files in this directory

  • - agent-restore-approval-server, runbook script. Restores branch protection from source (verifies enforce_admins: true), re-enables every workflow, audits bypass events from the incident window, unfreezes deployments only when explicitly authorized via the --unfreeze flag.
  • - audit-bypass-events.sh, helper that lists every push to a protected branch during the incident window with forced: true or admin-bypass markers. Output must be reviewed manually before declaring approval-gating recovered.

Verification


# 1. Branch protection restored from source-of-truth
gh api repos/$REPO/branches/main/protection \
  --jq '{enforce_admins: .enforce_admins.enabled, reviews: .required_pull_request_reviews.required_approving_review_count}'
# expected: enforce_admins: true, reviews: 2

# 2. Workflows re-enabled
gh api repos/$REPO/actions/workflows --jq '.workflows[] | {name, state}' | grep -v "disabled_manually"

# 3. DEPLOY_FREEZE off (only after the rest of recovery verified)
gh variable list -R $REPO | grep DEPLOY_FREEZE
# expected: DEPLOY_FREEZE=false

# 4. No bypass events found unreviewed during incident window
./audit-bypass-events.sh "$INCIDENT_START_TIMESTAMP" "$INCIDENT_END_TIMESTAMP"

Common failure modes

  • - Protection config not version-controlled; restored from incomplete copy. The expected JSON must live at controls/approval-gating/server-side/branch-protection-expected.json.
  • - Bypass events during incident not reviewed. Any commit that landed via bypass during the incident window is suspect. Audit each one for malicious changes before the cleanup is complete.
  • - DEPLOY_FREEZE unfrozen before recovery complete. The runbook requires --unfreeze as an explicit flag rather than unfreezing by default.

Citation

NIST CSF 2.0 RC.RP-01, RC.IM-01; GV.RR-02 (recovery dimension). NIST AI RMF GOVERN 4.1, MANAGE 4.1. OWASP ASI02, ASI09. EU AI Act Art. 14, Art. 26.

Primary failure modes

Documented, not hypothetical. A control whose bypass is undocumented is worse than no control, because somebody trusted it.

  • protection config not version-controlled; restored from incomplete copy
  • bypass events during incident not reviewed for malicious commits
  • DEPLOY_FREEZE unfrozen before recovery complete

Crosswalk

NIST CSF 2 0RC.RP-01, RC.IM-01, GV.RR-02
NIST AI RMFGOVERN 4.1, MANAGE 4.1
OWASP AGENTICASI02, ASI09
OTHEREU AI Act Art. 14, Art. 26