integrity — AgentTeamsModule¶
Hash manifest over the modules that enforce the constitution.
Every control this project relies on — the destructive-action gate, the content scanner, the fence-restore engine, the capability merge — is a Python file sitting in the same working tree the agents edit. A red-team audit on 2026-08-06 recorded that as the umbrella finding: the constitution's referee plays inside the same sandbox as the players. An agent that can write security_gate.py does not need to defeat the gate.
Source:
agentteams/integrity.py
What this is not¶
A hash manifest stored beside the files it protects is a speed bump, not a boundary. An attacker who can edit scan.py can edit enforcement-integrity.json, and one who can edit integrity.py can make verify() return an empty list. This is documented rather than discovered later, because a control whose limits are undocumented gets trusted past them.
What it does buy:
- it detects accidental corruption and unnoticed drift, which is most real-world damage;
- it makes tampering a two-step, recorded act rather than a one-line edit — the attacker must also regenerate the manifest, and that regeneration appears in
git diff; - it gives the harness-level hook (
.claude/hooks/constitutional-gate.py) something concrete to check before it trusts the scanner it is about to call.
The durable fix is elsewhere: move enforcement into the harness, where agents cannot write it, and keep signing keys outside the agent's environment.
Constants¶
ENFORCEMENT_MODULES¶
Source:
agentteams/integrity.py
Modules whose integrity the constitution depends on. Each is listed because a silent edit to it would disable or weaken a control rather than merely change behaviour:
- Destructive-action gate / clearance authenticity (C-2, C-5):
cli/security_gate.py,cli/decision_log.py, and the operator signing pathcli/operator_signing.py(the only code that reads the operator Ed25519 private key; see the pinned operator signing path). - Deterministic content scanner (C-4):
scan.py. - Fence / constraint controls (C-1):
fences.py,unfenced.py. - Capability comparison and grants (C-3, P2):
front_matter_merge.py,front_matter_reconcile.py,cli/grants.py,rank_conformance.py. - OS-confinement emitters and the shipped launcher/gate scripts:
frameworks/_sandbox_emit.py,frameworks/_goose_sandbox_emit.py,frameworks/_linux_sandbox_emit.py, and the tracked templatestemplates/universal/sandbox/confine-run.sh,templates/universal/sandbox/mac-escape-tests.sh,templates/universal/hooks/settings.hooks.example.json. - The standing red-team audit:
redteam/checks_static.py,redteam/checks_report.py,redteam/registry.py. - The harness-level constitutional gate: the tracked template
templates/universal/hooks/constitutional-gate.py, plus its two per-install copies.claude/hooks/constitutional-gate.pyand.github/hooks/constitutional-gate.py. integrity.pyitself, so removing an entry is detectable.
The two per-install hook copies are listed in INSTALLED_COPIES: they are gitignored, machine-local copies of the tracked template, so they are legitimately absent from a fresh checkout or CI. verify treats their absence as benign but still flags a present copy whose bytes differ from the manifest — the tamper tooth for the gate that actually executes. The template itself is a normal present-required pinned module.
MANIFEST_REL_PATH¶
Location of the manifest, relative to the repository root: references/enforcement-integrity.json.
Classes¶
IntegrityFinding¶
Source:
agentteams/integrity.py
One enforcement module whose current digest does not match the manifest.
Attributes:
rel_path(str) — Repository-relative module path.expected(str) — Digest recorded in the manifest.actual(str) — Digest computed now; empty when the file is missing.reason(str) —'modified','missing', or'unmanifested'.
Methods:
describe() -> str— One-line human-readable summary.
Functions¶
compute_digests(repo_root: Path) -> dict[str, str]¶
Return {rel_path: sha256} for every enforcement module. A module that does not exist maps to the empty string rather than being omitted, so its disappearance is a mismatch and not a silent gap in coverage.
write_manifest(repo_root: Path) -> Path¶
Write (or refresh) the manifest and return its path. Regenerating is deliberately a separate, explicit act: it is what an operator does after an intentional change to a control, and what an attacker must additionally do after an unintentional one. An auto-refreshed manifest verifies nothing, which is why this is not wired into the normal build.
Installed hook copies. The two INSTALLED_COPIES are gitignored, so they are absent in every worktree and fresh clone. When one does not exist, it keeps the digest that the existing manifest records for it; a re-record from a worktree no longer blanks it. Absence is judged with exists(), so a present but unreadable copy still records "". The function prints an [INTEGRITY] notice on stderr whenever:
- it keeps a digest;
- a kept digest differs from the current gate template's (a
WARNING, because the kept digest is probably stale or planted); - there is no well-formed prior digest to keep (it records
""); - the existing manifest is unreadable or corrupt (nothing is preserved).
Every other module is re-hashed, and a missing one still records "".
verify(repo_root: Path) -> list[IntegrityFinding]¶
Compare enforcement modules against the manifest; empty when everything matches.
A missing manifest returns empty. An unmanifested repository is not a tampered one, and treating "never set up" as "compromised" would make the check fire on every fresh clone until someone muted it. Callers that require a manifest must check for the file themselves.
Raises: RuntimeError when the manifest exists but cannot be read or parsed.