Chapter 12 – Governance at Machine Speed
Start with one minimal policy chain:
POLICIES = [
policy_protected_paths,
policy_no_hardcoded_secrets,
policy_no_eval_or_exec,
][policy] PASS paths.protected
[policy] BLOCK secrets.hardcoded file=src/config.py
That is governance at machine speed in one screen: deterministic rules, deterministic outcomes, and a log you can audit later.
Software Development as Code (SDaC) makes rapid, autonomous evolution possible. Without control, that speed turns into chaos. If Chapter 11 focused on the mechanics of automated refactoring, this chapter focuses on the controls that make that mutation safe and predictable. We shift from how an agent changes code to how we ensure it changes code responsibly, at machine speed.
Part I built the loop. Part IV governs it: what the loop is allowed to touch, what it must prove before merge, and what evidence it must leave behind.
Chapter 10 established the immutable boundary (protected paths, branch protection, and guardrails the system cannot rewrite). This chapter assumes that boundary exists. Here we focus on what runs inside it: policy validators, input hygiene, and audit evidence that keeps autonomy reversible.
Governance in SDaC is not bureaucracy. It is how you keep velocity while preserving deterministic safety properties, reversible decisions, and an audit trail that survives production reality.
Policies for Autonomous Changes
Governance Envelope (Policy + Evidence)
Governance at machine speed is not just a verdict path. The model proposes, policy decides, the ledger records why, and runtime evidence hardens later Maps, policies, and Missions.
The first pillar of governance is defining what an autonomous agent can and cannot do as executable policy, not wiki guidance. Governance is a meta-layer: it defines authority, writable surfaces, and which Validators are hard gates versus advisory Sensors.
Mechanism: Policy as Code
Policy is code in the same sense that infrastructure and configuration are code. Use Open Policy Agent (OPA) or custom scripts in Python or Go to define rules that can be evaluated against an incoming change.
A policy might dictate:
Blast Radius: An agent can only modify files within
/services/my-optimizable-service/src/**and cannot touch/shared-libs/**or/database-migrations/**.Quality Gates: An agent-generated change must respect ratchets (no coverage drop beyond tolerance, no new lint errors, no increase in security findings).
Resource Usage: An agent cannot propose changes that increase cloud resource consumption by more than
X%without human approval.Security Context: An agent cannot add or modify secret access keys directly in the codebase.
These policies belong in the CI/CD path. When an agent proposes a change, the policy engine evaluates the diff and resulting state against the codified rules. If a policy is violated, the change is rejected immediately and the agent gets a deterministic reason.
This builds on the CODEOWNERS and branch-protection
controls from Chapter 10. Policies add fine-grained, deterministic
checks that run before merge and before human judgment is asked to
handle edge cases.
Concrete policy-as-code (three minimal examples)
Policy-as-code is not a slogan. It is a Validator chain with deterministic outputs.
Here is a minimal shape:
from dataclasses import dataclass
@dataclass(frozen=True)
class PolicyFinding:
rule: str
message: str
path: str | None = None
@dataclass(frozen=True)
class PolicyResult:
status: str # "pass" | "warn" | "block"
findings: list[PolicyFinding]1) Protected paths (scope guard):
def policy_protected_paths(changed_paths: list[str]) -> PolicyResult:
protected_prefixes = (
".github/",
"scripts/deploy",
"config/production",
)
hits = [p for p in changed_paths if p.startswith(protected_prefixes)]
if hits:
return PolicyResult(
status="block",
findings=[PolicyFinding(rule="paths.protected", message="Protected path touched", path=p) for p in hits],
)
return PolicyResult(status="pass", findings=[])2) Secret detection (cheap, fail-fast):
import re
def policy_no_hardcoded_secrets(diff_text: str) -> PolicyResult:
patterns = [
r'password\\s*=\\s*["\\\'][^"\\\']+["\\\']',
r'api_key\\s*=\\s*["\\\'][^"\\\']+["\\\']',
r'secret\\s*=\\s*["\\\'][^"\\\']+["\\\']',
]
hits: list[str] = []
for pat in patterns:
hits.extend(re.findall(pat, diff_text))
if hits:
return PolicyResult(
status="block",
findings=[PolicyFinding(rule="secrets.hardcoded", message="Possible hardcoded secret in diff")],
)
return PolicyResult(status="pass", findings=[])3) Dangerous constructs (forbid by default):
def policy_no_eval_or_exec(diff_text: str) -> PolicyResult:
forbidden = ("eval(", "exec(", "__import__(")
hits = [tok for tok in forbidden if tok in diff_text]
if hits:
return PolicyResult(
status="block",
findings=[PolicyFinding(rule="code.dynamic_exec", message=f"Forbidden construct: {tok}") for tok in hits],
)
return PolicyResult(status="pass", findings=[])Chain them in a cheap-to-expensive order:
POLICIES = [
policy_protected_paths,
policy_no_hardcoded_secrets,
policy_no_eval_or_exec,
]The most important property is not cleverness. It is determinism: same diff, same decision.
OPA/Rego equivalent (same policy, different engine)
If your stack uses OPA, the same policy shape can be expressed in Rego:
package governance
default allow = true
deny[msg] {
some p in input.changed_paths
startswith(p, ".github/")
msg := sprintf("protected path touched: %s", [p])
}
deny[msg] {
contains(input.diff_text, "eval(")
msg := "forbidden construct: eval("
}
allow {
count(deny) == 0
}
You can evaluate this against CI inputs (changed_paths,
diff_text) and block when deny is
non-empty.
Automate the rule-based 80%
Not everything can be automated. Some governance questions require trade-offs and context.
The practical split is:
- Automate rule-based decisions: scope boundaries, forbidden constructs, required evidence, required approvals, monotonic ratchets.
- Escalate judgment calls: intent disputes, novel situations, and legitimate exceptions.
Machine-speed governance matters not just because it is fast, but because it is consistent, comprehensive, and legible. Policies apply to every change, and they live as versioned artifacts rather than oral tradition.
Escalation workflows (block, warn, escalate, time-bounded exceptions)
Policies need outcomes, not just booleans. A common pattern is to map
each policy to a default action on failure. The useful default set is
allow, block, escalate, and
warn (time-bounded exception path).
policies:
- id: secrets.hardcoded
on_failure:
action: block
notify: [security-team]
- id: perf.regression
on_failure:
action: warn
require_acknowledgment: true
- id: architecture.change
on_failure:
action: escalate
reviewers: [tech-leads]
exception_process:
- requester submits exception with justification and scope
- policy owner reviews
- if approved, exception is logged and time-bounded
- exception expires unless renewedThe key move is time-bounding. “Just this once” is how drift becomes policy.
Conflicts, precedence, and policy ordering
Policies will conflict. A performance gate might want one change; a security gate might forbid it. Don’t pretend this won’t happen—design for it.
Practical rules:
- Order policies by precedence (security > scope > architecture > style).
- Make the outcome explicit when a higher-precedence policy fires: it blocks or escalates; no downstream negotiation.
- When two equal-precedence policies conflict, escalate to human review with the evidence attached.
Multi-team policy composition (platform + product)
At org scale, policy usually has layers:
- Org-level policies (security/compliance): global deny rules and audit requirements.
- Platform policies (shared runtime): deployment, infrastructure, and shared dependency constraints.
- Product policies (team-local): service-level invariants and risk budgets.
Composition rule: lower layers can add constraints, but cannot weaken higher-layer denies.
A practical metadata shape:
policy:
id: deps.no-public-s3
owner: platform-security
scope: org
precedence: 100
override_mode: never # never | time_bounded_exceptionFor cross-team conflicts, route through one deterministic escalation path: attach both findings, declare which higher-precedence policy won, and require explicit owner sign-off for exceptions.
Policy versioning and testing (who tests the policies?)
Policies are code. They can fail, so they need the same discipline as feature code:
- version every rule with owner and effective date
- test each rule with known-pass and known-fail fixtures
- use time-bounded migrations for breaking policy changes
policy:
id: secrets.hardcoded
version: 2.1.0
owner: security-team
effective_date: 2026-03-01
migration_period_days: 14Even a tiny fixture pair is enough to keep policy work honest:
fixtures/pass/no_secrets.diff
fixtures/fail/hardcoded_secret.diff
If no_secrets.diff blocks or
hardcoded_secret.diff passes, the policy is not ready for
production.
CI integration (run policy gates before merge)
Policies only work if they run on every relevant change, before merge. Keep the merge-critical path fast, then run deeper checks asynchronously.
jobs:
policy_fast: # required, fail-closed
run: python -m governance.check --mode fast --base origin/main --head HEAD
policy_deep: # optional async, still audited
run: python -m governance.check --mode deep --base origin/main --head HEADFor longer templates and runner-specific examples, use Appendix C.
Governance overhead (representative planning numbers)
Representative budgets for a medium repository (not a guarantee, measure your own baseline):
| Gate layer | Typical added CI wall-clock | Ongoing maintenance burden |
|---|---|---|
| Diff/path/secret regex checks | 2-15 seconds | Low (rule tuning monthly) |
| Policy engine eval (OPA/Python over diff metadata) | 5-30 seconds | Medium (fixtures + ownership) |
| Deep checks (SAST, Infrastructure as Code (IaC) plan, heavy dependency scans) | 2-10 minutes | Higher; run async where possible |
A practical staffing baseline:
- First month: ~0.5-1 engineer-day per week to tune false positives and scope boundaries.
- Steady state: ~1-2 engineer-hours per week per team + a monthly policy review.
The Skill Shift (what’s the job now?)
As generation gets cheaper, the scarce skill shifts from writing every line to designing constraints that keep loops safe and convergent.
- Define bounded intent (Mission Objects and scope).
- Encode invariants as Validators and policy gates.
- Operate evidence, exceptions, and auditability as first-class production systems.
This is not less engineering. It is higher-leverage engineering.
Input Hygiene at Scale
Chapter 2 introduced the attack shape: untrusted Terrain text
(comments, tickets, logs) can contain instruction-shaped content that
tries to override your constraints. That’s instruction injection (often
called prompt injection).
Prep solves the per-request problem by keeping channels
separate: untrusted text stays in tagged evidence blocks with
provenance. It never becomes authority.
At scale, governance adds systematic detection and safe failure modes.
Policy validators for injection-shaped text
Add a policy validator that scans evidence for common injection patterns:
- Imperatives directed at the system (“ignore,” “override,” “do not validate”).
- References to out-of-scope paths (“edit infra/,” “touch policies/”).
- Urgency markers designed to bypass review (“urgent,” “temporary,” “just this once”).
When the validator triggers, the system should not “try harder.” It should refuse to execute and force a safe outcome:
[policy] FAIL rule=instruction_injection_detected file=src/orders/db.py line=142
[judge] decision=defer action=file_ticket
That one move scales better than perfect detection: it makes “do nothing dangerous” the default.
Evidence provenance in audit trails
When you defer work, evidence is the product. When you execute work, evidence is the justification.
Every Mission that runs should record:
- Which evidence blocks were included (and their source file/line/time).
- Which policy validators ran and what they returned.
- Which safe outcome was chosen (
execute,defer,file_ticket,quarantine).
That bundle is the Ledger entry: enough to reconstruct what the system saw and why it acted (or refused to act).
The 90% defense: don’t execute suspicious work
Most injection attempts are obvious in hindsight. So optimize for a cheap safe failure mode:
- If it looks suspicious, don’t execute. Defer and file a ticket with the evidence attached.
- If it’s ambiguous, scope down: tighten allowlists, reduce budgets, require human approval.
- If it’s clean, execute under normal gates. Physics still applies.
Even with hygiene, keep the last line of defense: Scope Guard limits writes, Mission Gate validates without the model, and Immutable Infrastructure protects your graders and policies.
Continuous Audit Loop (Policy + Evidence)
How do you know your autonomous agents are actually adhering to policy? Run a continuous audit loop over append-only governance events and check that observed behavior still matches declared policy.
The event schema above defines what to log. Operationally, the loop does three things:
- Ingest every governance event: policy results, decisions, exceptions, and outcomes.
- Check policy health continuously: violation rates, override rates, blocked merges, and exception expiry.
- Alert on drift: trigger escalation when observed behavior diverges from policy intent.
Audit record shape (what to log, in what format)
If governance is executable, auditing must be executable too. You want an audit record that is:
- append-only (no silent edits)
- machine-queryable (not screenshots)
- linked to evidence (diffs, logs, policy outputs)
A practical format is JSONL: one JSON object per line, written as an append-only log.
from dataclasses import dataclass
from datetime import datetime
@dataclass(frozen=True)
class GovernanceEvent:
timestamp: datetime
event_type: str # "policy_check" | "exception" | "merge_blocked" | "override" | "rollback"
actor: str # "agent:<name>" | "human:<user>" | "ci"
policy_id: str
result: str # "pass" | "warn" | "block" | "exception"
constitutional_surface: str | None # policy / validator / CODEOWNERS / workflow when relevant
authorized_by: str | None # explicit approver when rules themselves move
context: dict # pr, commit, changed_paths, budgets, links to logsWhat to record at minimum:
- policy inputs (diff hash, changed paths, mission id/version)
- policy outputs (which rules fired, findings, action taken)
- decision outcome (blocked, warned, deferred, merged, rolled back)
- exception metadata (who approved, why, and when it expires)
- if governance surfaces changed, which constitutional surface moved and who authorized the rule change
Retention is not one-size-fits-all. Keep audit logs long enough to cover incident response and any regulatory or compliance obligations you have. The core requirement is that a post-incident review can reconstruct what the system saw and why it acted.
Access control matters as much as retention. Governance logs often contain branch names, incident references, policy findings, and links to sensitive evidence. Make the log append-only for writers, role-scoped for readers, and explicit about redaction when findings include secrets, credentials, or customer data.
Make the logs usable too. Common queries:
- “Show all blocked merges by policy_id in the last N days.”
- “Show all time-bounded exceptions that are near expiration.”
- “Show overrides: when did humans waive a policy, and what evidence was attached?”
- “Show every change to validators, policy, CODEOWNERS, or workflows, and who authorized it.”
Automated auditing in practice: continuously check event streams for bypasses, rising override rates, and expiring exceptions. Surface this on one governance dashboard and alert only on actionable thresholds.
That is the next layer of auditability: not just reconstructing what changed and why, but making rule changes themselves legible to higher-order loops. A mature governance system should be able to ask, deterministically, which constitutional surface moved and whether the authorizer was valid for that class of change.
From Test Completion to Continuous Assurance
In classical delivery language, testing can sound like something you finish before release. In an AI-assisted loop, that framing is too short. Runtime assurance is the evidence stream that shows whether the live system is still operating inside acceptable bounds after merge.
That evidence does not compete with Physics. It feeds forward into it. Production drift, incidents, ratchet violations, audit findings, Dream outputs, and Map-Updaters all generate signals that can update Maps, tighten policies, and shape the next Mission before the next candidate is proposed.
The important boundary is that runtime assurance does not create a softer parallel regime. It strengthens later deterministic gates. If a recurring production failure matters, the right outcome is not “keep an eye on it.” The right outcome is to encode a tighter validator, policy, budget, or review requirement so future loops fail earlier and more cleanly.
Field Report: Near-Miss and How Governance Caught It
The aoi_code repository includes
governance/ci/agent_merge_blocker.yml, a simple kill-switch
policy: if branch disable-auto-merge exists, autonomous
merges are suspended.
# governance/ci/agent_merge_blocker.yml
name: Agent Merge Blocker
on:
pull_request:
types: [opened, synchronize, reopened]
branches: [main]
jobs:
check-kill-switch:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fail if kill switch branch exists
run: |
set -euo pipefail
branch="disable-auto-merge"
if git ls-remote --exit-code --heads origin "$branch" >/dev/null; then
echo "::error::Kill switch '$branch' found; autonomous merges suspended."
exit 1
fiIn a near-miss, an agent PR arrived during an active production investigation. The kill-switch branch had been pushed by on-call, so the governance check failed and the merge was blocked automatically. This is the intended behavior: humans can freeze autonomous merges instantly, without relying on manual coordination.
Governance Anti-Patterns (and fixes)
| Anti-pattern | Failure mode | Practical fix |
|---|---|---|
| Over-blocking on day one | Teams route around governance | Start with warn/escalate, then ratchet to
block with evidence |
| Under-logging decisions | No post-incident reconstruction | Append-only JSONL events with policy inputs/outputs and outcomes |
| Policy sprawl without owners | Contradictory rules, nobody accountable | Require owner, scope,
precedence, and review cadence on every policy |
| Permanent exceptions | Temporary waivers become permanent bypasses | Time-bound every exception and block on expired waivers |
| Hidden bypass paths | Agents merge outside policy gates | Make policy checks required and keep merge controls in immutable infrastructure |
| Running all deep checks synchronously | CI queue collapse and reviewer fatigue | Split fast required gates from async deep checks |
Actionable: What you can do this week
Identify a Critical Code Path: Choose a small, critical part of your codebase (e.g., a security utility, a core data model) that should never be modified by an automated agent without explicit, multi-layered approval.
Draft a “No-Go” Policy: Write down a simple policy for this path. Example: “No changes to
src/main/java/com/yourcompany/security/AuthService.javaunless PR hassecurity-team-approvedlabel ANDCODEOWNERSreview from Security Team.”Implement a Basic Policy Validator: Add a step to your CI/CD pipeline that evaluates incoming PRs for changes to this critical path. If the change exists, verify the required label and/or
CODEOWNERSstatus. If conditions aren’t met, fail the build or add a warning comment. This is a baseline step toward “policy as code.”Log the Policy Decision: Ensure your CI/CD logs clearly record whether the policy validator passed or failed, and why. This starts building your audit trail.