Sign execution history for transport admission

Why: Success-shaped receipts and caller-writable history must not authorize apply/admit without controller-originated provenance.

Evidence: Fabricated, unsigned, tampered-history, and moved-ref tests were RED before HMAC binding; 52 controller/history/admission tests now pass.

Impact: Stores a local 0600 attestation key, signs successful history including transport commit SHA, and verifies it before apply/admit.

Refs: independent Codex review of feature/agent-cli-enforcement
This commit is contained in:
2026-10-15 18:13:01 +00:00
parent 16fb93a300
commit 28f000af8b
3 changed files with 645 additions and 50 deletions
+307 -33
View File
@@ -2,18 +2,23 @@
from __future__ import annotations
import argparse
import contextlib
import errno
import fcntl
import hashlib
import hmac
import json
import os
import re
import select
import shutil
import signal
import stat
import subprocess
import sys
import time
import uuid
from collections.abc import Iterator
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
@@ -1971,11 +1976,18 @@ def _finish_run(
)
history_path: Path | None = None
try:
history_path = _append_execution_history(repo, receipt, secret_values)
receipt["history"] = {
history_path, attestation = _append_execution_history(
repo, receipt, secret_values
)
recorded: dict[str, Any] = {
"path": str(history_path),
"status": "recorded",
"record_id": receipt.get("run_id"),
"run_id": receipt.get("run_id"),
}
if attestation:
recorded["attestation"] = attestation
receipt["history"] = recorded
except Exception as exc:
receipt["history"] = {
"path": str(history_path) if history_path is not None else None,
@@ -1995,32 +2007,32 @@ def _append_execution_history(
repo: Path,
receipt: dict[str, Any],
secret_values: set[str],
) -> Path:
common = _git(
repo.resolve(),
"rev-parse",
"--path-format=absolute",
"--git-common-dir",
check=False,
)
if common.returncode != 0 or not common.stdout.strip():
raise RuntimeError(
"unable to resolve git common dir: "
+ (common.stderr.strip() or common.stdout.strip())
)
common_dir = Path(common.stdout.strip())
if not common_dir.is_absolute():
common_dir = (repo.resolve() / common_dir).resolve()
history_dir = common_dir / "agent-exec"
) -> tuple[Path, str | None]:
repo = repo.resolve()
history_dir = _git_common_dir(repo) / "agent-exec"
if history_dir.is_symlink():
raise RuntimeError("agent-exec history directory must not be a symlink")
history_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
if history_dir.is_symlink() or not history_dir.is_dir():
raise RuntimeError("agent-exec history directory must not be a symlink")
os.chmod(history_dir, 0o700)
history_path = history_dir / "execution-history.jsonl"
record = _execution_history_record(repo, receipt)
line = json.dumps(record, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
line = _redact_text(line, secret_values) + "\n"
fd = os.open(history_path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
if history_path.is_symlink():
raise RuntimeError("agent-exec history file must not be a symlink")
attestation: str | None = None
if receipt.get("terminal_status") == "success":
_bind_success_transport_commit(repo, receipt)
key = _ensure_attestation_key(repo)
record = _execution_history_record(repo, receipt)
line, attestation = _signed_history_line(record, key, secret_values)
else:
record = _execution_history_record(repo, receipt)
line = _redact_text(_canonical_json(record), secret_values) + "\n"
fd = os.open(
history_path,
os.O_WRONLY | os.O_CREAT | os.O_APPEND | os.O_NOFOLLOW | os.O_CLOEXEC,
0o600,
)
try:
os.fchmod(fd, 0o600)
with os.fdopen(fd, "a", encoding="utf-8", closefd=False) as handle:
@@ -2033,7 +2045,7 @@ def _append_execution_history(
fcntl.flock(fd, fcntl.LOCK_UN)
finally:
os.close(fd)
return history_path
return history_path, attestation
def _execution_history_record(repo: Path, receipt: dict[str, Any]) -> dict[str, Any]:
@@ -2073,6 +2085,9 @@ def _execution_history_record(repo: Path, receipt: dict[str, Any]) -> dict[str,
"ref": transport.get("ref"),
"parent": transport.get("parent"),
}
commit = transport.get("commit")
if isinstance(commit, str) and commit:
safe_transport["commit"] = commit
terminal = receipt.get("terminal_status")
return {
"schema": HISTORY_SCHEMA,
@@ -2338,7 +2353,12 @@ def _valid_transport_ref(ref: object) -> str | None:
return ref
def _history_file(repo: Path) -> Path | None:
_ATTESTATION_KEY_NAME = "attestation.key"
_ATTESTATION_KEY_BYTES = 32
def _git_common_dir(repo: Path) -> Path:
repo = repo.resolve()
common = _git(
repo,
"rev-parse",
@@ -2347,11 +2367,200 @@ def _history_file(repo: Path) -> Path | None:
check=False,
)
if common.returncode != 0 or not common.stdout.strip():
raise RuntimeError(common.stderr.strip() or "unable to resolve git common dir")
raise RuntimeError(
"unable to resolve git common dir: "
+ (common.stderr.strip() or common.stdout.strip() or "empty")
)
common_dir = Path(common.stdout.strip())
if not common_dir.is_absolute():
common_dir = (repo / common_dir).resolve()
history_dir = common_dir / "agent-exec"
return common_dir
def _canonical_json(data: dict[str, Any]) -> str:
return json.dumps(data, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def _history_attestation(key: bytes, record: dict[str, Any]) -> str:
body = {field: value for field, value in record.items() if field != "attestation"}
return hmac.new(key, _canonical_json(body).encode("utf-8"), hashlib.sha256).hexdigest()
def _history_attestation_ok(key: bytes, record: dict[str, Any]) -> bool:
presented = record.get("attestation")
if not isinstance(presented, str) or len(presented) != 64:
return False
return hmac.compare_digest(_history_attestation(key, record), presented)
def _signed_history_line(
record: dict[str, Any], key: bytes, secret_values: set[str]
) -> tuple[str, str]:
text = _redact_text(_canonical_json(record), secret_values)
body = json.loads(text)
if not isinstance(body, dict):
raise RuntimeError("history record redaction produced non-object JSON")
mac = _history_attestation(key, body)
body["attestation"] = mac
return _canonical_json(body) + "\n", mac
def _bind_success_transport_commit(repo: Path, receipt: dict[str, Any]) -> None:
transport = receipt.get("transport")
if not isinstance(transport, dict):
raise RuntimeError("success history requires transport")
ref = transport.get("ref")
parent = transport.get("parent")
if not isinstance(ref, str) or not isinstance(parent, str):
raise RuntimeError("success history requires transport ref and parent")
commit = _resolve_commit(repo, ref)
resolved_parent = _resolve_commit(repo, parent)
if commit is None or resolved_parent is None:
raise RuntimeError("success transport ref did not resolve")
if _commit_parents(repo, commit) != [resolved_parent]:
raise RuntimeError("success transport parent does not match the commit")
transport["commit"] = commit
transport["parent"] = resolved_parent
def _symlink_oserror(exc: OSError, message: str) -> OSError | RuntimeError:
if exc.errno == errno.ELOOP:
return RuntimeError(message)
return exc
@contextlib.contextmanager
def _locked_agent_exec_dir(repo: Path, *, create: bool) -> Iterator[int | None]:
"""Lock the real agent-exec directory. Missing dirs yield None unless creating.
Callers share this lock so the first key write cannot race another reader.
"""
history_dir = _git_common_dir(repo) / "agent-exec"
if history_dir.is_symlink():
raise RuntimeError("agent-exec history directory must not be a symlink")
if not create and not history_dir.exists():
yield None
return
if create:
history_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
if history_dir.is_symlink() or not history_dir.is_dir():
raise RuntimeError("agent-exec history directory must not be a symlink")
os.chmod(history_dir, 0o700)
elif not history_dir.is_dir():
raise RuntimeError("agent-exec history directory must not be a symlink")
try:
fd = os.open(
history_dir,
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC,
)
except OSError as exc:
raise _symlink_oserror(
exc, "agent-exec history directory must not be a symlink"
) from exc
try:
info = os.fstat(fd)
if not stat.S_ISDIR(info.st_mode):
raise RuntimeError("agent-exec history directory must not be a symlink")
if stat.S_IMODE(info.st_mode) != 0o700:
raise RuntimeError("agent-exec history directory mode must be 0700")
fcntl.flock(fd, fcntl.LOCK_EX)
try:
yield fd
finally:
fcntl.flock(fd, fcntl.LOCK_UN)
finally:
os.close(fd)
def _read_key_at(dir_fd: int) -> bytes:
fd = os.open(
_ATTESTATION_KEY_NAME,
os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC,
dir_fd=dir_fd,
)
try:
current = os.fstat(fd)
if not stat.S_ISREG(current.st_mode):
raise RuntimeError("attestation key must be a regular file")
if stat.S_IMODE(current.st_mode) != 0o600:
raise RuntimeError("attestation key mode must be 0600")
if current.st_size != _ATTESTATION_KEY_BYTES:
raise RuntimeError("attestation key must be 32 bytes")
os.lseek(fd, 0, os.SEEK_SET)
key = os.read(fd, _ATTESTATION_KEY_BYTES)
if len(key) != _ATTESTATION_KEY_BYTES:
raise RuntimeError("attestation key must be 32 bytes")
return key
finally:
os.close(fd)
def _key_is_symlink(dir_fd: int) -> bool:
try:
info = os.stat(_ATTESTATION_KEY_NAME, dir_fd=dir_fd, follow_symlinks=False)
except FileNotFoundError:
return False
return stat.S_ISLNK(info.st_mode)
def _read_attestation_key(repo: Path) -> bytes | None:
"""Read the local attestation key. Never creates one and never follows links."""
with _locked_agent_exec_dir(repo, create=False) as dir_fd:
if dir_fd is None:
return None
if _key_is_symlink(dir_fd):
raise RuntimeError("attestation key must not be a symlink")
try:
return _read_key_at(dir_fd)
except FileNotFoundError:
return None
except OSError as exc:
raise _symlink_oserror(exc, "attestation key must not be a symlink") from exc
def _ensure_attestation_key(repo: Path) -> bytes:
"""Create or read the 32-byte key under the Git common dir.
This binds controller history to later apply/admit checks. It does not
protect against the same OS user deliberately reading the key file.
"""
with _locked_agent_exec_dir(repo, create=True) as dir_fd:
if dir_fd is None:
raise RuntimeError("agent-exec history directory is unavailable")
if _key_is_symlink(dir_fd):
raise RuntimeError("attestation key must not be a symlink")
flags = os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW | os.O_CLOEXEC
try:
fd = os.open(_ATTESTATION_KEY_NAME, flags, 0o600, dir_fd=dir_fd)
except OSError as exc:
raise _symlink_oserror(exc, "attestation key must not be a symlink") from exc
try:
os.fchmod(fd, 0o600)
current = os.fstat(fd)
if not stat.S_ISREG(current.st_mode):
raise RuntimeError("attestation key must be a regular file")
if current.st_size == 0:
key = os.urandom(_ATTESTATION_KEY_BYTES)
written = os.write(fd, key)
if written != _ATTESTATION_KEY_BYTES:
raise RuntimeError("attestation key write was short")
os.fsync(fd)
return key
if current.st_size != _ATTESTATION_KEY_BYTES:
raise RuntimeError("attestation key must be 32 bytes")
os.lseek(fd, 0, os.SEEK_SET)
key = os.read(fd, _ATTESTATION_KEY_BYTES)
if len(key) != _ATTESTATION_KEY_BYTES:
raise RuntimeError("attestation key must be 32 bytes")
if stat.S_IMODE(os.fstat(fd).st_mode) != 0o600:
raise RuntimeError("attestation key mode must be 0600")
return key
finally:
os.close(fd)
def _history_file(repo: Path) -> Path | None:
history_dir = _git_common_dir(repo) / "agent-exec"
history_path = history_dir / "execution-history.jsonl"
if history_dir.is_symlink() or history_path.is_symlink():
return None
@@ -2381,7 +2590,10 @@ def _transport_covers(
*,
head: str,
required: set[str],
key: bytes | None,
) -> bool:
if key is None or not _history_attestation_ok(key, record):
return False
if record.get("schema") != HISTORY_SCHEMA:
return False
if record.get("terminal_status") != "success":
@@ -2393,20 +2605,70 @@ def _transport_covers(
return False
ref = _valid_transport_ref(transport.get("ref"))
parent = transport.get("parent")
if ref is None or not isinstance(parent, str):
signed_commit = transport.get("commit")
if ref is None or not isinstance(parent, str) or not isinstance(signed_commit, str):
return False
if _resolve_commit(repo, ref) != signed_commit:
return False
if _resolve_commit(repo, parent) != head:
return False
commit = _resolve_commit(repo, ref)
if commit is None or _commit_parents(repo, commit) != [head]:
if _commit_parents(repo, signed_commit) != [head]:
return False
changed = record.get("changed_paths")
if not isinstance(changed, list) or any(not isinstance(path, str) for path in changed):
return False
diff_paths = _diff_paths(repo, head, commit)
diff_paths = _diff_paths(repo, head, signed_commit)
if _non_exempt(changed) != required or _non_exempt(diff_paths) != required:
return False
return _index_matches_commit(repo, commit, required)
return _index_matches_commit(repo, signed_commit, required)
def _apply_signed_history_ok(
repo: Path,
receipt: dict[str, Any],
*,
commit: str,
base_head: str,
) -> bool:
key = _read_attestation_key(repo)
if key is None:
return False
run_id = receipt.get("run_id")
transport = receipt.get("transport")
changed = receipt.get("changed_paths")
if not isinstance(run_id, str) or not run_id or not isinstance(transport, dict):
return False
ref = transport.get("ref")
parent = transport.get("parent")
claimed = transport.get("commit")
if not isinstance(ref, str) or parent != base_head or claimed != commit:
return False
if _resolve_commit(repo, ref) != commit:
return False
if _commit_parents(repo, commit) != [base_head]:
return False
if not isinstance(changed, list) or any(not isinstance(path, str) for path in changed):
return False
if _non_exempt(changed) != _non_exempt(_diff_paths(repo, base_head, commit)):
return False
for record in _load_history_records(repo):
if record.get("run_id") != run_id or record.get("terminal_status") != "success":
continue
if not _same_repo(repo, record.get("repo")):
continue
record_paths = record.get("changed_paths")
if record_paths != changed:
continue
rec_transport = record.get("transport")
if not isinstance(rec_transport, dict):
continue
if rec_transport.get("ref") != ref or rec_transport.get("parent") != parent:
continue
if rec_transport.get("commit") != commit:
continue
if _history_attestation_ok(key, record):
return True
return False
def cmd_apply(repo: Path, receipt_path: Path) -> int:
@@ -2472,6 +2734,15 @@ def cmd_apply(repo: Path, receipt_path: Path) -> int:
):
return _reject("apply: mismatched transport", secrets)
try:
authorized = _apply_signed_history_ok(
repo, receipt, commit=commit, base_head=resolved_base
)
except Exception as exc:
return _reject(f"apply: no signed history: {exc}", secrets)
if not authorized:
return _reject("apply: no signed history", secrets)
merge = _git(repo, "merge", "--ff-only", "--quiet", commit, check=False)
if merge.returncode != 0:
return _reject(
@@ -2500,8 +2771,11 @@ def cmd_admit(repo: Path) -> int:
if history_path is not None and history_path.is_file():
history_text = history_path.read_text(encoding="utf-8")
secrets = _secret_set(history_text)
key = _read_attestation_key(repo)
for record in _load_history_records(repo):
if _transport_covers(repo, record, head=head, required=required):
if _transport_covers(
repo, record, head=head, required=required, key=key
):
print("admit: ok")
return EXIT_SUCCESS
except Exception as exc:
+120
View File
@@ -5,6 +5,8 @@ Set RUN_REAL_AGENT_EXEC=1 and AGENT_EXEC_REAL_PROFILE to exactly one profile.
"""
from __future__ import annotations
import hashlib
import hmac
import json
import os
import re
@@ -2342,6 +2344,124 @@ class AgentExecHistoryTests(unittest.TestCase):
self.assertEqual(records[0]["terminal_status"], "invalid-config")
self.assertEqual(records[0]["attempts"], [])
def _run_marker_success(self, name: str) -> tuple[Path, dict[str, object]]:
_write_fake_worker(
self.bindir,
name,
body=textwrap.dedent(
"""\
workspace="$1"
result="$3"
mkdir -p "$workspace/src"
printf 'ok\\n' > "$workspace/src/marker.txt"
printf '%s\\n' '{"terminal_status":"completed","summary":"done","changed_files":["src/marker.txt"],"evidence":[],"scope_expansion":null}' > "$result"
exit 0
"""
),
)
_write_config(
self.repo,
profiles={name: {"adapter": "custom", "executable": name}},
role_order=[name],
)
unit = _write_unit(self.root / f"{name}.json", unit_id=f"unit-{name}")
receipt_path = self.receipts / f"{name}.json"
code = agent_exec.cmd_run(
self.repo,
role="implementation",
unit_path=unit,
receipt_path=receipt_path,
)
receipt_text = (
receipt_path.read_text(encoding="utf-8") if receipt_path.is_file() else ""
)
self.assertEqual(code, agent_exec.EXIT_SUCCESS, receipt_text)
return receipt_path, json.loads(receipt_text)
def test_success_history_is_mode_correct_and_signed(self) -> None:
receipt_path, receipt = self._run_marker_success("signedhistory")
key_path = self.history_path.parent / "attestation.key"
self.assertTrue(key_path.is_file())
self.assertFalse(key_path.is_symlink())
self.assertFalse(key_path.parent.is_symlink())
self.assertEqual(stat.S_IMODE(key_path.parent.stat().st_mode), 0o700)
self.assertEqual(stat.S_IMODE(key_path.stat().st_mode), 0o600)
key = key_path.read_bytes()
self.assertEqual(len(key), 32)
self.assertNotIn(key, receipt_path.read_bytes())
self.assertNotIn(key, self.history_path.read_bytes())
records = [
json.loads(line)
for line in self.history_path.read_text(encoding="utf-8").splitlines()
]
self.assertEqual(len(records), 1)
record = records[0]
transport = record["transport"]
ref = transport["ref"]
commit = _git(self.repo, "rev-parse", ref).stdout.strip()
self.assertEqual(record["terminal_status"], "success")
self.assertEqual(record["run_id"], receipt["run_id"])
self.assertEqual(transport["parent"], receipt["transport"]["parent"])
self.assertEqual(transport["commit"], commit)
self.assertEqual(receipt["transport"]["commit"], commit)
self.assertEqual(record["changed_paths"], receipt["changed_paths"])
body = {field: value for field, value in record.items() if field != "attestation"}
payload = json.dumps(
body, ensure_ascii=False, sort_keys=True, separators=(",", ":")
).encode("utf-8")
self.assertEqual(
hmac.new(key, payload, hashlib.sha256).hexdigest(),
record["attestation"],
)
identity = receipt["history"]
self.assertEqual(identity["status"], "recorded")
self.assertEqual(identity["run_id"], receipt["run_id"])
self.assertEqual(identity["record_id"], record["record_id"])
self.assertEqual(identity["attestation"], record["attestation"])
self.assertNotIn(key.hex(), json.dumps(identity))
def test_attestation_key_symlink_is_not_followed(self) -> None:
history_dir = self.history_path.parent
history_dir.mkdir(mode=0o700)
outside = self.root / "outside-key"
outside.write_bytes(b"A" * 32)
key_path = history_dir / "attestation.key"
key_path.symlink_to(outside)
receipt_path, receipt = self._run_marker_success("symlinkkey")
self.assertTrue(key_path.is_symlink())
self.assertEqual(outside.read_bytes(), b"A" * 32)
self.assertEqual(receipt["history"]["status"], "failed")
self.assertNotIn(b"A" * 32, receipt_path.read_bytes())
def test_attestation_key_first_run_race_shares_one_key(self) -> None:
found: list[bytes] = []
errors: list[BaseException] = []
barrier = threading.Barrier(8)
def load() -> None:
try:
barrier.wait(timeout=5)
found.append(agent_exec._ensure_attestation_key(self.repo))
except BaseException as exc: # noqa: BLE001 - race failures are the assertion
errors.append(exc)
threads = [threading.Thread(target=load) for _ in range(8)]
for thread in threads:
thread.start()
for thread in threads:
thread.join(timeout=10)
self.assertFalse(thread.is_alive())
self.assertEqual(errors, [])
self.assertEqual(len(found), 8)
self.assertEqual(len({item for item in found}), 1)
self.assertEqual(len(found[0]), 32)
key_path = self.history_path.parent / "attestation.key"
self.assertFalse(key_path.is_symlink())
self.assertEqual(key_path.read_bytes(), found[0])
self.assertEqual(stat.S_IMODE(key_path.stat().st_mode), 0o600)
self.assertEqual(stat.S_IMODE(key_path.parent.stat().st_mode), 0o700)
# Opt-in real CLI probes (U5). Default unittest must not contact providers.
U5_PROFILES = (
+218 -17
View File
@@ -1,6 +1,8 @@
"""Provenance admission: apply a success transport, or admit exact staged paths."""
from __future__ import annotations
import hashlib
import hmac
import json
import os
import shutil
@@ -148,6 +150,8 @@ class AgentExecAdmissionTests(unittest.TestCase):
reason: str | None = None,
transport_parent: str | None = None,
omit_transport: bool = False,
run_id: str | None = None,
commit: str | None = None,
) -> Path:
data: dict[str, object] = {
"schema": agent_exec.RECEIPT_SCHEMA,
@@ -161,16 +165,65 @@ class AgentExecAdmissionTests(unittest.TestCase):
},
"changed_paths": paths,
}
if run_id is not None:
data["run_id"] = run_id
if not omit_transport:
data["transport"] = {
transport: dict[str, object] = {
"ref": ref,
"parent": parent if transport_parent is None else transport_parent,
"attempt_id": "attempt-1",
}
if commit is not None:
transport["commit"] = commit
data["transport"] = transport
path = self.root / f"receipt-{uuid.uuid4().hex}.json"
path.write_text(json.dumps(data), encoding="utf-8")
return path
def _common_dir(self) -> Path:
return Path(
_git(
self.repo,
"rev-parse",
"--path-format=absolute",
"--git-common-dir",
).stdout.strip()
)
def _history_path(self) -> Path:
return self._common_dir() / "agent-exec" / "execution-history.jsonl"
def _attestation_key(self) -> bytes:
dest = self._common_dir() / "agent-exec"
dest.mkdir(mode=0o700, exist_ok=True)
os.chmod(dest, 0o700)
path = dest / "attestation.key"
if path.is_symlink():
raise RuntimeError("test attestation key must not be a symlink")
if path.exists():
key = path.read_bytes()
if len(key) != 32:
raise RuntimeError("test attestation key must be 32 bytes")
return key
key = os.urandom(32)
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
try:
os.write(fd, key)
os.fchmod(fd, 0o600)
finally:
os.close(fd)
return key
@staticmethod
def _sign_history_record(record: dict[str, object], key: bytes) -> dict[str, object]:
body = {field: value for field, value in record.items() if field != "attestation"}
payload = json.dumps(
body, ensure_ascii=False, sort_keys=True, separators=(",", ":")
).encode("utf-8")
signed = dict(body)
signed["attestation"] = hmac.new(key, payload, hashlib.sha256).hexdigest()
return signed
def _history(
self,
*,
@@ -179,26 +232,39 @@ class AgentExecAdmissionTests(unittest.TestCase):
paths: list[str],
status: str = "success",
repo: str | None = None,
) -> None:
record = {
commit: str | None = None,
run_id: str | None = None,
sign: bool | None = None,
) -> dict[str, object]:
if commit is None:
commit = _git(self.repo, "rev-parse", f"{ref}^{{commit}}").stdout.strip()
identity = run_id or uuid.uuid4().hex
record: dict[str, object] = {
"schema": agent_exec.HISTORY_SCHEMA,
"terminal_status": status,
"record_id": identity,
"run_id": identity,
"repo": repo if repo is not None else str(self.repo.resolve()),
"changed_paths": paths,
"transport": {"ref": ref, "parent": parent},
"transport": {"ref": ref, "parent": parent, "commit": commit},
}
common = Path(
_git(
self.repo,
"rev-parse",
"--path-format=absolute",
"--git-common-dir",
).stdout.strip()
)
dest = common / "agent-exec"
if sign is None:
sign = status == "success"
if sign:
record = self._sign_history_record(record, self._attestation_key())
dest = self._history_path().parent
dest.mkdir(mode=0o700, exist_ok=True)
with (dest / "execution-history.jsonl").open("a", encoding="utf-8") as handle:
handle.write(json.dumps(record, sort_keys=True) + "\n")
os.chmod(dest, 0o700)
line = json.dumps(
record, ensure_ascii=False, sort_keys=True, separators=(",", ":")
)
with self._history_path().open("a", encoding="utf-8") as handle:
handle.write(line + "\n")
return record
def _reset_branch(self) -> None:
_git(self.repo, "reset", "--hard", self.head)
_git(self.repo, "clean", "-fd")
def _stage(self, rel: str, content: str) -> None:
path = self.repo / rel
@@ -208,9 +274,23 @@ class AgentExecAdmissionTests(unittest.TestCase):
def test_apply_fast_forwards_clean_matching_transport(self) -> None:
commit, ref = self._transport_commit({"src/marker.txt": "shipped\n"})
receipt = self._receipt(
parent=self.head, ref=ref, paths=["src/marker.txt"]
run_id = uuid.uuid4().hex
signed = self._history(
parent=self.head,
ref=ref,
paths=["src/marker.txt"],
commit=commit,
run_id=run_id,
)
receipt = self._receipt(
parent=self.head,
ref=ref,
paths=["src/marker.txt"],
run_id=run_id,
commit=commit,
)
self.assertEqual(signed["transport"]["commit"], commit)
self.assertIn("attestation", signed)
proc = self._cli(
"apply", "--repo", str(self.repo), "--receipt", str(receipt)
)
@@ -309,6 +389,64 @@ class AgentExecAdmissionTests(unittest.TestCase):
),
)
def test_apply_rejects_fabricated_unsigned_or_tampered_history(self) -> None:
commit, ref = self._transport_commit({"src/marker.txt": "shipped\n"})
run_id = uuid.uuid4().hex
paths = ["src/marker.txt"]
def reject(label: str) -> None:
self._reset_branch()
before = self._state()
receipt = self._receipt(
parent=self.head,
ref=ref,
paths=paths,
run_id=run_id,
commit=commit,
)
proc = self._cli(
"apply", "--repo", str(self.repo), "--receipt", str(receipt)
)
combined = proc.stdout + proc.stderr
self.assertEqual(proc.returncode, agent_exec.EXIT_BLOCKED, combined)
self.assertIn("apply: no signed history", proc.stderr)
self.assertNotIn("apply: fast-forwarded", proc.stdout)
self.assertEqual(self._state(), before, label)
with self.subTest(label="fabricated receipt"):
reject("fabricated receipt")
with self.subTest(label="unsigned history"):
self._history(
parent=self.head,
ref=ref,
paths=paths,
commit=commit,
run_id=run_id,
sign=False,
)
reject("unsigned history")
with self.subTest(label="tampered history"):
self._reset_branch()
signed = self._history(
parent=self.head,
ref=ref,
paths=paths,
commit=commit,
run_id=run_id,
)
history_path = self._history_path()
lines = history_path.read_text(encoding="utf-8").splitlines()
tampered = json.loads(lines[-1])
self.assertEqual(tampered["attestation"], signed["attestation"])
tampered["record_id"] = "tampered-record"
lines[-1] = json.dumps(
tampered, ensure_ascii=False, sort_keys=True, separators=(",", ":")
)
history_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
reject("tampered history")
def test_apply_redacts_secret_shaped_receipt_text(self) -> None:
_commit, ref = self._transport_commit({"src/marker.txt": "shipped\n"})
secret = "sk-admissionsecret"
@@ -360,6 +498,69 @@ class AgentExecAdmissionTests(unittest.TestCase):
self.assertEqual(partial.returncode, agent_exec.EXIT_BLOCKED, partial.stderr)
self.assertIn("admit: no provenance", partial.stderr)
def test_admit_rejects_tampered_record_and_moved_ref(self) -> None:
commit, ref = self._transport_commit({"src/marker.txt": "shipped\n"})
signed = self._history(parent=self.head, ref=ref, paths=["src/marker.txt"], commit=commit)
history_path = self._history_path()
lines = history_path.read_text(encoding="utf-8").splitlines()
tampered = json.loads(lines[-1])
self.assertEqual(tampered["attestation"], signed["attestation"])
tampered["record_id"] = "tampered-record"
lines[-1] = json.dumps(
tampered, ensure_ascii=False, sort_keys=True, separators=(",", ":")
)
history_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
self._stage("src/marker.txt", "shipped\n")
before = self._state()
proc = self._cli("admit", "--repo", str(self.repo))
combined = proc.stdout + proc.stderr
self.assertEqual(proc.returncode, agent_exec.EXIT_BLOCKED, combined)
self.assertIn("admit: no provenance", proc.stderr)
self.assertNotIn("admit: ok", proc.stdout)
self.assertEqual(self._state(), before)
self._reset_branch()
tree = _git(self.repo, "rev-parse", f"{commit}^{{tree}}").stdout.strip()
moved = _git(
self.repo,
"commit-tree",
tree,
"-p",
self.head,
"-m",
"moved ref",
).stdout.strip()
self.assertNotEqual(moved, commit)
restored = self._sign_history_record(
{
"schema": agent_exec.HISTORY_SCHEMA,
"terminal_status": "success",
"record_id": signed["record_id"],
"run_id": signed["run_id"],
"repo": str(self.repo.resolve()),
"changed_paths": ["src/marker.txt"],
"transport": {"commit": commit, "parent": self.head, "ref": ref},
},
self._attestation_key(),
)
history_path.write_text(
json.dumps(
restored, ensure_ascii=False, sort_keys=True, separators=(",", ":")
)
+ "\n",
encoding="utf-8",
)
_git(self.repo, "update-ref", ref, moved)
self._stage("src/marker.txt", "shipped\n")
moved_before = self._state()
moved_proc = self._cli("admit", "--repo", str(self.repo))
moved_text = moved_proc.stdout + moved_proc.stderr
self.assertEqual(moved_proc.returncode, agent_exec.EXIT_BLOCKED, moved_text)
self.assertIn("admit: no provenance", moved_proc.stderr)
self.assertNotIn("admit: ok", moved_proc.stdout)
self.assertEqual(self._state(), moved_before)
self.assertEqual(_git(self.repo, "rev-parse", ref).stdout.strip(), moved)
def test_admit_rejects_different_content_at_transport_path(self) -> None:
_commit, ref = self._transport_commit({"src/marker.txt": "shipped\n"})
self._history(parent=self.head, ref=ref, paths=["src/marker.txt"])