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
- -
ghCLI authenticated withrepo_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 (verifiesenforce_admins: true), re-enables every workflow, audits bypass events from the incident window, unfreezes deployments only when explicitly authorized via the--unfreezeflag. - -
audit-bypass-events.sh, helper that lists every push to a protected branch during the incident window withforced: trueor 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
--unfreezeas 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 0 | RC.RP-01, RC.IM-01, GV.RR-02 |
|---|---|
| NIST AI RMF | GOVERN 4.1, MANAGE 4.1 |
| OWASP AGENTIC | ASI02, ASI09 |
| OTHER | EU AI Act Art. 14, Art. 26 |
Cite this cell:
https://agenticcovenants.com/recover/approval-gating/server-side/