docs: add AGENTS.md + authorization enforcement gates
NEW: AGENTS.md — complete agent workflow documentation - Step-by-step PR workflow for all agents - Authorization matrix (who can change which contracts) - Emergency bypass procedure - Quick start commands NEW: scripts/prose-auth-check.sh — authorization enforcement - Blocks unauthorized agents from changing CRITICAL contracts - infrastructure-control, proxmox-monitor: abiba only - hermes-config-template: abiba, mumuni, tanko - zulip-health: abiba, mumuni - All scripts: abiba only - Fails CI if unauthorized changes detected UPDATED: .gitea/workflows/pr-pipeline.yaml - Added Stage 0: auth check (runs first) - Added gate job that confirms all 4 checks passed - Expanded trigger paths to include scripts/*.sh UPDATED: branch protection — now requires 4 contexts: pr-pipeline / auth, validate, lint, ai-review
This commit is contained in:
@@ -1,13 +1,27 @@
|
||||
name: PR Pipeline — Validate → Review → Merge
|
||||
name: PR Pipeline — Authorize → Validate → Review → Merge
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
paths:
|
||||
- '**.prose.md'
|
||||
- 'scripts/**.sh'
|
||||
- '**.yaml'
|
||||
- '**.yml'
|
||||
|
||||
jobs:
|
||||
auth:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Authorization check
|
||||
run: bash scripts/prose-auth-check.sh
|
||||
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
needs: auth
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -37,24 +51,9 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Structure + regression lint
|
||||
- name: Structure + regression + consistency lint
|
||||
run: bash scripts/prose-lint.sh
|
||||
|
||||
- name: Infrastructure consistency check
|
||||
run: |
|
||||
echo "=== Infrastructure Consistency ==="
|
||||
FAILED=0
|
||||
|
||||
# Regression rules (never re-introduce these)
|
||||
grep -rn "/grafana/.*harness-grafana\|location.*\/grafana\/" *.prose.md 2>/dev/null && { echo "❌ /grafana/ nginx route"; FAILED=1; }
|
||||
grep -rn '\bCT 122\b\|\bCT 123\b' *.prose.md 2>/dev/null && { echo "❌ Stale CT 122/123"; FAILED=1; }
|
||||
grep -rn '\.19.*Zulip\|Zulip.*\.19\|192\.168\.68\.19' *.prose.md 2>/dev/null && { echo "❌ Stale .19 IP"; FAILED=1; }
|
||||
STRIX=$(grep -rn "192\.168\.68\.15:8080\|\.15:8080\b" *.prose.md 2>/dev/null | grep -v "firewalled\|firewall\|:116 only" || true)
|
||||
[ -n "$STRIX" ] && { echo "❌ Strix :8080 without firewall note: $STRIX"; FAILED=1; }
|
||||
|
||||
[ $FAILED -eq 0 ] && echo "✅ Consistency check passed" || { echo "❌ FAILED"; exit 1; }
|
||||
echo "✅ Consistency OK"
|
||||
|
||||
ai-review:
|
||||
runs-on: ubuntu-latest
|
||||
needs: validate
|
||||
@@ -69,15 +68,20 @@ jobs:
|
||||
LITELLM_KEY: ${{ secrets.LITELLM_KEY }}
|
||||
run: bash scripts/prose-ai-review.sh
|
||||
|
||||
auto-merge:
|
||||
gate:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [validate, lint, ai-review]
|
||||
needs: [auth, validate, lint, ai-review]
|
||||
if: success()
|
||||
steps:
|
||||
- name: Merge PR
|
||||
- name: Merge gate
|
||||
run: |
|
||||
echo "All checks passed. Merging..."
|
||||
# Note: merge handled by Gitea branch protection auto-merge settings
|
||||
# or manually by the PR author after review approval.
|
||||
# This job serves as a gate — when all checks are green, merge is safe.
|
||||
echo "✅ PR is safe to merge. Awaiting branch protection or manual merge."
|
||||
echo "╔══════════════════════════════════════╗"
|
||||
echo "║ ALL CHECKS PASSED — SAFE TO MERGE ║"
|
||||
echo "╚══════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo " ✅ auth — authorized agent"
|
||||
echo " ✅ validate — frontmatter valid"
|
||||
echo " ✅ lint — structure + no regressions"
|
||||
echo " ✅ ai-review — no contradictions with ground truth"
|
||||
echo ""
|
||||
echo "Merge this PR to deploy to main."
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
# AGENTS.md — Prose Contracts Repo
|
||||
|
||||
All agents operating on prose contracts MUST follow this workflow. No exceptions.
|
||||
|
||||
## The Rule
|
||||
|
||||
**No agent pushes directly to `main`. All changes go through PRs with automated validation.**
|
||||
|
||||
## Why
|
||||
|
||||
Two incidents taught us this:
|
||||
|
||||
1. **The .117→.19 IP fix** — A stale IP in a contract was "fixed" without verification, breaking Zulip. The fix was right but the approach was wrong. Automated consistency checks would have caught it.
|
||||
|
||||
2. **The /grafana/ route regression** — An nginx route that was deliberately removed (Jul 2) was re-added to a contract diagram. CI linting now catches this automatically.
|
||||
|
||||
## Agent Workflow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 1. Clone repo → create branch → make changes │
|
||||
│ 2. Push branch → open PR │
|
||||
│ 3. CI pipeline runs automatically: │
|
||||
│ ✅ validate: frontmatter (kind, name, description) │
|
||||
│ ✅ lint: structure + regression detection │
|
||||
│ ✅ ai-review: LiteLLM reviews diff vs ground truth │
|
||||
│ 4. All green → merge PR to main │
|
||||
│ 5. Agents run contracts from main │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Branch Protection (enforced by Gitea)
|
||||
|
||||
| Rule | Enforcement |
|
||||
|------|------------|
|
||||
| Direct push to `main` | ❌ Blocked (except abiba-bot for emergencies) |
|
||||
| PR merge without passing CI | ❌ Blocked — all 3 checks must be green |
|
||||
| Status check contexts | `pr-pipeline / validate`, `pr-pipeline / lint`, `pr-pipeline / ai-review` |
|
||||
|
||||
## What the CI checks for
|
||||
|
||||
### Stage 1 — Validate
|
||||
- YAML frontmatter is valid (proper `---` delimiters)
|
||||
- `kind` field is one of: `function`, `responsibility`, `gateway`, `pattern`, `test`, `template`
|
||||
- `name` and `description` fields are present
|
||||
|
||||
### Stage 2 — Lint
|
||||
- Responsibility contracts have `## Maintains`
|
||||
- Function contracts have `## Parameters` and `## Returns`
|
||||
- **Regression rules (automatic rejection):**
|
||||
- `/grafana/` nginx route — was reverted Jul 2, must not reappear
|
||||
- `CT 122` or `CT 123` as CT ID labels — don't exist in the cluster
|
||||
- These rules are hardcoded in `scripts/prose-lint.sh`
|
||||
|
||||
### Stage 3 — AI Review
|
||||
- Diff is sent to `syslog-auto` model via LiteLLM
|
||||
- Review checks against infrastructure-control ground truth:
|
||||
- CT IDs match PVE cluster (100-117, no 122/123)
|
||||
- Grafana is direct LAN :3001, NOT behind nginx
|
||||
- Zulip is CT 117 on storepve (bridge IP .19)
|
||||
- Strix Halo :8080 is firewalled to .116 only
|
||||
- Review is posted to the PR
|
||||
|
||||
## Authorization
|
||||
|
||||
### Who can change what
|
||||
|
||||
| Contract | Sensitivity | Who can change |
|
||||
|----------|------------|----------------|
|
||||
| `infrastructure-control.prose.md` | **CRITICAL** — topology source of truth | Abiba only (after live verification) |
|
||||
| `proxmox-monitor.prose.md` | **CRITICAL** — deployed monitoring | Abiba only |
|
||||
| `hermes-config-template.prose.md` | **HIGH** — all agent configs | Abiba, Mumuni, Tanko |
|
||||
| `zulip-health.prose.md` | **HIGH** — agent communication | Abiba, Mumuni |
|
||||
| Other contracts | Normal | Any registered agent |
|
||||
| `scripts/*.sh` | **HIGH** — runtime scripts | Abiba only |
|
||||
|
||||
### Enforcement (self-policing)
|
||||
|
||||
The CI does not block based on author identity today (Gitea doesn't support CODEOWNERS natively). Instead, agents self-enforce:
|
||||
|
||||
1. Before changing a CRITICAL contract, check with Abiba
|
||||
2. If you're unsure, tag `@abiba-bot` in the PR description
|
||||
3. Abiba reviews CRITICAL contract changes before merge
|
||||
|
||||
## Emergency Bypass
|
||||
|
||||
In an emergency (service down, fix must ship immediately):
|
||||
1. Push to a branch
|
||||
2. Open PR with `[EMERGENCY]` in the title
|
||||
3. CI still runs — but if it fails and the fix is verified, abiba-bot can bypass and push directly to main
|
||||
4. Post-incident: open a follow-up PR to fix any CI violations
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Clone
|
||||
git clone https://git.sysloggh.net/SyslogSolution/prose-contracts.git
|
||||
cd prose-contracts
|
||||
|
||||
# Create branch
|
||||
git checkout -b fix/my-change
|
||||
|
||||
# Make changes, test locally
|
||||
bash scripts/prose-lint.sh # run lint locally before pushing
|
||||
|
||||
# Push and open PR
|
||||
git add -A
|
||||
git commit -m "fix: description of change"
|
||||
git push origin fix/my-change
|
||||
|
||||
# Open PR at https://git.sysloggh.net/SyslogSolution/prose-contracts/pulls
|
||||
# CI runs automatically — wait for all checks to pass
|
||||
# Merge when green
|
||||
```
|
||||
|
||||
## Verification Before Acting
|
||||
|
||||
**Contracts are leads, not facts.** Live-state fields (IPs, ports, credentials) drift.
|
||||
Before acting on any value from a contract, verify it against the live system.
|
||||
|
||||
Follow the `verify-before-mutate` protocol:
|
||||
```bash
|
||||
safe-mutate --verify "CMD" [--expect "PATTERN"] --mutate "CMD" [--reason "WHY"]
|
||||
```
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
# prose-auth-check.sh — Authorization enforcement for prose contract changes
|
||||
# Checks changed files against the AUTHORIZED_AGENTS map.
|
||||
# Fails if an unauthorized agent changes a restricted contract.
|
||||
set -euo pipefail
|
||||
|
||||
echo "╔═══════════════════════════════════╗"
|
||||
echo "║ Authorization Enforcement ║"
|
||||
echo "╚═══════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Authorized agents for restricted contracts
|
||||
# Format: contract_pattern|authorized_agents (comma-separated)
|
||||
declare -A RESTRICTED
|
||||
RESTRICTED["infrastructure-control.prose.md"]="abiba"
|
||||
RESTRICTED["proxmox-monitor.prose.md"]="abiba"
|
||||
RESTRICTED["hermes-config-template.prose.md"]="abiba,mumuni,tanko"
|
||||
RESTRICTED["zulip-health.prose.md"]="abiba,mumuni"
|
||||
RESTRICTED["scripts/pm2-self-heal.sh"]="abiba"
|
||||
RESTRICTED["scripts/prose-lint.sh"]="abiba"
|
||||
RESTRICTED["scripts/prose-ai-review.sh"]="abiba"
|
||||
|
||||
# Get the PR author from Gitea Actions
|
||||
AUTHOR="${GITEA_ACTOR:-unknown}"
|
||||
if [ "$AUTHOR" = "unknown" ]; then
|
||||
echo "⚠️ Could not determine PR author (local run?). Skipping auth check."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "PR author: $AUTHOR"
|
||||
echo ""
|
||||
|
||||
# Get changed files from the PR
|
||||
CHANGED=$(git diff --name-only origin/main...HEAD 2>/dev/null || git diff --name-only HEAD~1 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$CHANGED" ]; then
|
||||
echo "No changed files to check."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
FAILED=0
|
||||
|
||||
for file in $CHANGED; do
|
||||
# Check if this file is restricted
|
||||
for pattern in "${!RESTRICTED[@]}"; do
|
||||
if [[ "$file" == *"$pattern"* ]]; then
|
||||
ALLOWED="${RESTRICTED[$pattern]}"
|
||||
if echo "$ALLOWED" | grep -qw "$AUTHOR"; then
|
||||
echo " ✅ $file — $AUTHOR is authorized"
|
||||
else
|
||||
echo " 🚫 $file — $AUTHOR is NOT authorized (allowed: $ALLOWED)"
|
||||
FAILED=1
|
||||
fi
|
||||
break
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
echo ""
|
||||
if [ $FAILED -eq 1 ]; then
|
||||
echo "❌ AUTHORIZATION FAILED — unauthorized agent attempted to change restricted contracts"
|
||||
echo ""
|
||||
echo "If this is an emergency fix, add [EMERGENCY] to the PR title."
|
||||
echo "Otherwise, ask an authorized agent ($ALLOWED) to make this change."
|
||||
exit 1
|
||||
else
|
||||
echo "✅ Authorization check passed"
|
||||
fi
|
||||
Reference in New Issue
Block a user