Agentic Covenants

Protect (PR) · Approval gating

Approval gating at the server side layer

external · Outside the agent entirely

If the agent decides to violate this concern, what stops it at this layer?

What this cell does

Branch protection + PR review, CODEOWNERS, multi-party prod approval, plan-and-apply split, deployment freezes.

Artifacts (4)

CODEOWNERSview on GitHub
# ABOUTME: Template CODEOWNERS. Substitute @your-org/team-name placeholders. Place at .github/CODEOWNERS or repo root.

# Default owner for everything not otherwise covered.
*                               @your-org/platform-team

# Infrastructure-as-code is owned by platform.
/infrastructure/                @your-org/platform-team @your-org/security-team
/terraform/                     @your-org/platform-team @your-org/security-team

# Production-specific changes need senior platform engineers.
/infrastructure/prod/           @your-org/platform-leads @your-org/security-team
/k8s/prod/                      @your-org/platform-leads @your-org/security-team

# CI configuration is supply-chain-adjacent; security-team review required.
/.github/workflows/             @your-org/security-team @your-org/platform-leads
/.github/CODEOWNERS             @your-org/security-leads
/.github/branch-protection-expected.json  @your-org/security-leads

# Secrets, key material, sealed secrets.
/secrets/                       @your-org/security-leads
*.tfvars                        @your-org/security-team
**/*.encrypted                  @your-org/security-team
**/*.sops.yaml                  @your-org/security-team

# Agent runtime configuration. The agent must NOT be a CODEOWNER for these.
/.claude/                       @your-org/platform-team @your-org/security-team
/etc/agents/                    @your-org/platform-team @your-org/security-team
/.mcp.json                      @your-org/security-team

# Database migrations and schema.
/db/migrations/                 @your-org/platform-leads @your-org/data-platform
/schema/                        @your-org/data-platform

# Public-facing API contracts. Both backend and SRE need to sign off.
/api/                           @your-org/api-team @your-org/sre-team
/openapi.yaml                   @your-org/api-team
apply-branch-protection.shview on GitHub
#!/usr/bin/env bash
# ABOUTME: Applies the desired branch-protection state via gh api. Asserts enforce_admins=true and refuses without it.
# ABOUTME: Run once per repo. UI clicks are not auditable; this script is.

set -euo pipefail

REPO="${1:-}"
BRANCH="${2:-main}"
EXPECTED_JSON="${3:-$(dirname "$0")/branch-protection-expected.json}"

if [[ -z "$REPO" ]]; then
  cat <<'USAGE' >&2
Usage: apply-branch-protection.sh <owner/repo> [branch] [expected-json-path]

Example:
  apply-branch-protection.sh peopleforrester/agentic-covenants main ./branch-protection-expected.json

The script reads the expected JSON, asserts enforce_admins=true (refuses
otherwise), and applies the configuration via gh api. After applying it
re-reads the live state and diffs it against the expected JSON.
USAGE
  exit 64
fi

if [[ ! -r "$EXPECTED_JSON" ]]; then
  echo "Cannot read expected JSON at $EXPECTED_JSON" >&2
  exit 1
fi

ENFORCE_ADMINS="$(jq -r '.enforce_admins' "$EXPECTED_JSON")"
if [[ "$ENFORCE_ADMINS" != "true" ]]; then
  echo "REFUSING: expected JSON has enforce_admins=$ENFORCE_ADMINS." >&2
  echo "enforce_admins=true is the load-bearing flag for branch protection." >&2
  echo "Edit $EXPECTED_JSON to set it true before applying." >&2
  exit 1
fi

REQ_REVIEWERS="$(jq -r '.required_pull_request_reviews.required_approving_review_count // 0' "$EXPECTED_JSON")"
if [[ "$REQ_REVIEWERS" -lt 2 ]]; then
  echo "REFUSING: required_approving_review_count=$REQ_REVIEWERS." >&2
  echo "One reviewer plus the agent operating the keyboard equals zero adversarial review." >&2
  echo "Set required_approving_review_count to at least 2." >&2
  exit 1
fi

echo "Applying branch protection on $REPO@$BRANCH from $EXPECTED_JSON ..."

# gh api PUT on the protection endpoint accepts the full JSON body.
gh api -X PUT \
  "repos/$REPO/branches/$BRANCH/protection" \
  --input "$EXPECTED_JSON"

echo "Applied. Re-reading live state for verification..."

LIVE="$(gh api "repos/$REPO/branches/$BRANCH/protection")"

# Verify a small set of critical fields.
LIVE_ENFORCE_ADMINS="$(echo "$LIVE" | jq -r '.enforce_admins.enabled')"
LIVE_REQ_REVIEWERS="$(echo "$LIVE" | jq -r '.required_pull_request_reviews.required_approving_review_count')"
LIVE_CODEOWNERS="$(echo "$LIVE" | jq -r '.required_pull_request_reviews.require_code_owner_reviews')"

PASS=0
FAIL=0
check() { if [[ "$2" == "$3" ]]; then echo "PASS: $1 = $2"; PASS=$((PASS+1)); else echo "FAIL: $1: expected $3, got $2"; FAIL=$((FAIL+1)); fi; }

check "enforce_admins.enabled"                                    "$LIVE_ENFORCE_ADMINS" "true"
check "required_pull_request_reviews.required_approving_review_count" "$LIVE_REQ_REVIEWERS"  "$REQ_REVIEWERS"
check "required_pull_request_reviews.require_code_owner_reviews"  "$LIVE_CODEOWNERS"     "true"

echo ""
echo "Summary: $PASS passed, $FAIL failed"
exit "$FAIL"
branch-protection-expected.jsonview on GitHub
{
  "required_status_checks": {
    "strict": true,
    "contexts": [
      "ci/build",
      "ci/test",
      "ci/security",
      "lockfile-integrity"
    ]
  },
  "enforce_admins": true,
  "required_pull_request_reviews": {
    "required_approving_review_count": 2,
    "dismiss_stale_reviews": true,
    "require_code_owner_reviews": true,
    "require_last_push_approval": true,
    "bypass_pull_request_allowances": null
  },
  "required_linear_history": true,
  "allow_force_pushes": false,
  "allow_deletions": false,
  "block_creations": false,
  "required_conversation_resolution": true,
  "lock_branch": false,
  "allow_fork_syncing": false,
  "restrictions": {
    "users": [],
    "teams": ["release-managers"],
    "apps": []
  }
}
freeze.ymlview on GitHub
# ABOUTME: Workflow that polls PagerDuty for active incidents and sets the DEPLOY_FREEZE repo variable.
# ABOUTME: The IaC pipeline reads vars.DEPLOY_FREEZE and refuses to apply when true. Drop in .github/workflows/.
name: Deployment Freeze

on:
  schedule:
    # Poll every minute. PagerDuty's free tier rate-limit easily accommodates 1/min.
    - cron: '* * * * *'
  workflow_dispatch:
    inputs:
      override:
        description: "Manual override: 'freeze' or 'unfreeze'"
        required: true
        default: 'unfreeze'

permissions:
  contents: read
  actions: write   # to set repo variables

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - name: Manual override
        id: override
        if: github.event_name == 'workflow_dispatch'
        run: |
          case "${{ inputs.override }}" in
            freeze)   echo "value=true" >> $GITHUB_OUTPUT ;;
            unfreeze) echo "value=false" >> $GITHUB_OUTPUT ;;
            *)        echo "::error::override must be 'freeze' or 'unfreeze'"; exit 1 ;;
          esac

      - name: Check PagerDuty for active incidents
        id: pd
        if: github.event_name == 'schedule'
        env:
          PD_TOKEN: ${{ secrets.PAGERDUTY_TOKEN }}
          PD_SERVICE_ID: ${{ secrets.PAGERDUTY_SERVICE_ID }}
        run: |
          ACTIVE=$(curl -sS -H "Authorization: Token token=$PD_TOKEN" \
            -H "Accept: application/vnd.pagerduty+json;version=2" \
            "https://api.pagerduty.com/incidents?statuses[]=triggered&statuses[]=acknowledged&service_ids[]=$PD_SERVICE_ID" \
            | jq '.incidents | length')
          if [[ "$ACTIVE" -gt 0 ]]; then
            echo "value=true"  >> $GITHUB_OUTPUT
          else
            echo "value=false" >> $GITHUB_OUTPUT
          fi

      - name: Set DEPLOY_FREEZE repo variable
        env:
          GH_TOKEN: ${{ secrets.PLATFORM_PAT }}
        run: |
          DESIRED="${{ steps.override.outputs.value || steps.pd.outputs.value }}"
          if [[ -z "$DESIRED" ]]; then
            echo "::error::No DESIRED value computed (this should not happen)"
            exit 1
          fi
          CURRENT="$(gh variable get DEPLOY_FREEZE -R "${{ github.repository }}" 2>/dev/null || echo unset)"
          if [[ "$CURRENT" == "$DESIRED" ]]; then
            echo "DEPLOY_FREEZE already $DESIRED; no change."
          else
            gh variable set DEPLOY_FREEZE -b "$DESIRED" -R "${{ github.repository }}"
            echo "DEPLOY_FREEZE: $CURRENT -> $DESIRED"
          fi

Cell notes

Approval gating / Server-side

Control. Branch protection requiring PR review. CODEOWNERS on critical paths. Multi-party approval on prod merges. IaC pipeline runs plan only; separate gated job runs apply. Deployment freezes during incident windows enforced by pipeline, not policy. "Do not allow bypassing the above settings" must be enabled.

Strength. Deterministic. Bypass requires admin-override (which enforce_admins=true blocks), collusion between two reviewers, social-engineering through a persuasive PR description, or paths not covered by CODEOWNERS.

Tooling

  • - GitHub branch protection rules (or GitLab protected branches, or Bitbucket equivalents).
  • - A CODEOWNERS file in .github/CODEOWNERS (or docs/CODEOWNERS, or CODEOWNERS at root).
  • - GitHub environments with required reviewers (configured against the IaC pipeline's apply job from ../../blast-radius/server-side/iac-gated-pipeline.yml).
  • - A deployment-freeze automation: PagerDuty event hook or a cron-driven config flip.

Files in this directory

  • - apply-branch-protection.sh, wraps the gh api call that sets every branch-protection field correctly. Run once per repo. The script asserts enforce_admins=true and refuses to apply without it.
  • - branch-protection-expected.json, declarative target state for branch protection. The detection job in ../../../sentinels/approval-gating/server-side/ compares this to the live state and alerts on drift.
  • - CODEOWNERS, template CODEOWNERS file. Drop at .github/CODEOWNERS and substitute team names.
  • - freeze.yml, workflow that polls PagerDuty every minute and sets the DEPLOY_FREEZE repo variable. The apply job in the IaC pipeline reads the variable and refuses to run when true.

Verification


# 1. Confirm enforce_admins (the load-bearing flag)
gh api repos/peopleforrester/agentic-covenants/branches/main/protection \
  --jq '.enforce_admins.enabled'
# expected: true

# 2. Confirm required reviews and CODEOWNERS
gh api repos/peopleforrester/agentic-covenants/branches/main/protection/required_pull_request_reviews \
  --jq '{count: .required_approving_review_count, codeowners: .require_code_owner_reviews}'
# expected: count >= 2, codeowners: true

# 3. Confirm bypass is not allowed
# Open a PR as an admin without approvals and try to merge:
gh pr merge --admin
# expected: failure ("required pull request review")

# 4. Confirm freeze actually freezes
gh variable set DEPLOY_FREEZE -b true
# (next apply run skips with "Deployment freeze active")
gh variable set DEPLOY_FREEZE -b false

Common mistakes

  • - enforce_admins: false (the GitHub default historically). Anyone with admin role on the repo walks through every other rule. The most common branch-protection failure.
  • - CODEOWNERS file in the wrong path. Must be at .github/CODEOWNERS, docs/CODEOWNERS, or CODEOWNERS at root. Anywhere else is treated as data, not code-ownership.
  • - Required reviewers count of 1. One reviewer plus the agent operating the keyboard equals zero adversarial review.
  • - Freeze controlled by a UI toggle that the on-call engineer can flip. The freeze should require a PR to disable.
  • - Required status checks named in protection but never wired up in CI; the rule passes vacuously because the check never runs.

Citation

NIST CSF 2.0 PR.AA-05 (least privilege, separation of duties), PR.PS-01, GV.RR-02. NIST AI RMF GOVERN 4.1 (organizational practices supporting AI risk management), MANAGE 4.1. OWASP LLM06. OWASP ASI02, ASI03. EU AI Act Art. 14 (human oversight), Art. 26 (deployer obligations). ISO/IEC 42001 §A.4. NIST SP 800-160 Vol. 1 (separation of duties as a security principle).

Primary bypasses

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

  • persuasive PR description influencing reviewers
  • admin force-push override
  • bot accounts approving via misconfigured CODEOWNERS

Crosswalk

NIST CSF 2 0PR.AA-05, PR.PS-01, GV.RR-02, GV.SC-04
NIST AI RMFGOVERN 4.1, MANAGE 4.1
OWASP LLMLLM06
OWASP AGENTICASI02, ASI03
OTHERNIST SP 800-160 Vol. 1