agent-exec transport b11015860dac4bb8a1943889211374b7/0dd2eb729ad2

This commit is contained in:
agent-exec-transport
2026-10-15 20:02:36 +00:00
parent 93aba6ba95
commit 6b00f4f8b0
8 changed files with 832 additions and 353 deletions
+196 -230
View File
@@ -1,4 +1,10 @@
"""Recoverable agent-exec controller: validate, run, apply transports, admit provenance."""
"""Recoverable agent-exec controller: validate, run, apply transports, admit staged content.
Local history MACs and the pre-commit ``admit`` check are local integrity and
tamper evidence only (SELF). They are not GATE, not a trusted identity, and do
not protect against the same OS user. Delivery trust comes from exact-HEAD CI
or an independent REPLAY.
"""
from __future__ import annotations
import argparse
@@ -29,7 +35,11 @@ _REPO_ROOT = Path(__file__).resolve().parents[1]
if str(_REPO_ROOT) not in sys.path:
sys.path.insert(0, str(_REPO_ROOT))
from scripts.agent_exec_adapters import build_invocation, probe_adapter
from scripts.agent_exec_adapters import (
build_invocation,
decode_terminal_record,
probe_adapter,
)
from scripts.agent_exec_config import (
DEFAULT_IDLE_TIMEOUT_SECONDS,
SECRET_VALUE_RE,
@@ -183,7 +193,10 @@ def main(argv: list[str] | None = None) -> int:
p_admit = sub.add_parser(
"admit",
help="Admit staged paths backed by a success transport or host exemption",
help=(
"Locally check staged paths against a success transport or host "
"exemption (local integrity evidence, not GATE)"
),
)
p_admit.add_argument("--repo", required=True)
@@ -791,7 +804,7 @@ def _run_attempt(
pointer_delta = _snapshot_delta(pointer_before, pointer_after)
canonical_delta = _snapshot_delta(canonical_before, canonical_after)
worker_result = _load_worker_result(result_path, proc_meta["stdout_tail"])
worker_result = _load_worker_result(profile.adapter, result_path, proc_meta)
summary = _bounded_summary(
proc_meta.get("summary_source") or (worker_result or {}).get("summary") or "",
cfg.execution.summary_bytes,
@@ -922,7 +935,17 @@ def _run_attempt(
cleanup_extra={"reason": "worker fail-stop terminal"},
)
# Never report success from worker prose alone — require exit + admin + in-scope diff + transport.
completed = bool(
worker_result and worker_result.get("terminal_status") == "completed"
)
if clean_exit and changed and not completed:
return _preserved(
"failed-unknown",
"worker exited 0 with changes but no valid completed terminal record",
cleanup_extra={"reason": "missing or invalid terminal record"},
)
# Success requires exit 0 + completed terminal record + clean admin + in-scope diff + transport.
if clean_exit and changed and not out_of_scope:
# Recheck path bounds immediately before host-owned transport.
try:
@@ -975,121 +998,23 @@ def _run_attempt(
observed_version=probe.observed_version,
)
if clean_exit and not changed:
# No diff does not prove an authorized external effect was reconciled.
if _external_side_effects_unproven(unit, worker_result):
return _preserved(
"failed-unknown",
(
"worker exited 0 without in-scope changes; "
"external side effects cannot be reconciled"
),
cleanup_extra={
"reason": "network or external_cost authorized without trustworthy receipt",
"network": unit.network,
"external_cost": unit.external_cost,
},
)
restored = _restore_worktree(repo, worktree, base["head"])
if not restored:
return _preserved(
"failed-unknown",
"exact restore failed",
cleanup_extra={"reason": "exact restore failed"},
)
return AttemptRecord(
attempt_id=attempt_id,
profile=profile.name,
adapter=profile.adapter,
terminal_status="failed-clean",
reason="worker exited 0 without in-scope changes",
worktree=str(worktree),
pid=proc_meta["pid"],
exit_code=proc_meta["exit_code"],
changed_paths=[],
inventory_paths=[],
raw_log=str(raw_log),
summary=summary,
cleanup={"exact_restore": True},
observed_version=probe.observed_version,
)
# Nonzero / aborted without trustworthy external reconciliation.
if _external_side_effects_unproven(unit, worker_result):
return _preserved(
"failed-unknown",
(
f"worker failed with exit {proc_meta['exit_code']}; "
"external side effects cannot be reconciled"
),
cleanup_extra={
"reason": "network or external_cost authorized without trustworthy receipt",
"network": unit.network,
"external_cost": unit.external_cost,
},
)
# Controlled case: network=false and external_cost=false may become failed-clean
# only after exact worktree restoration proves no unknown authorized external object.
if changed:
restored = _restore_worktree(repo, worktree, base["head"])
if restored:
return AttemptRecord(
attempt_id=attempt_id,
profile=profile.name,
adapter=profile.adapter,
terminal_status="failed-clean",
reason=f"worker failed with exit {proc_meta['exit_code']}; restored",
worktree=str(worktree),
pid=proc_meta["pid"],
exit_code=proc_meta["exit_code"],
changed_paths=changed,
inventory_paths=list(changed),
raw_log=str(raw_log),
summary=summary,
cleanup={
"exact_restore": True,
"external_objects_reconciled": True,
"network": False,
"external_cost": False,
},
observed_version=probe.observed_version,
)
return _preserved(
"failed-unknown",
"worker failed; exact restore not proven",
)
restored = _restore_worktree(repo, worktree, base["head"])
if not restored:
return _preserved(
"failed-unknown",
"exact restore failed",
cleanup_extra={"reason": "exact restore failed"},
)
return AttemptRecord(
attempt_id=attempt_id,
profile=profile.name,
adapter=profile.adapter,
terminal_status="failed-clean",
reason=f"worker failed with exit {proc_meta['exit_code']}",
worktree=str(worktree),
pid=proc_meta["pid"],
exit_code=proc_meta["exit_code"],
changed_paths=[],
inventory_paths=[],
raw_log=str(raw_log),
summary=summary,
cleanup={
"exact_restore": True,
"external_objects_reconciled": True,
"network": False,
"external_cost": False,
# Launched attempts have no enforced network or external-cost isolation, and
# the unit's declared network/external_cost booleans are not proof. A
# restored private clone therefore cannot make a launched failure clean.
if clean_exit:
reason = "worker exited 0 without in-scope changes"
else:
reason = f"worker failed with exit {proc_meta['exit_code']}"
return _preserved(
"failed-unknown",
f"{reason}; external side effects are not mechanically reconciled",
cleanup_extra={
"reason": "external side effects not mechanically reconciled",
"declared_network": unit.network,
"declared_external_cost": unit.external_cost,
},
observed_version=probe.observed_version,
)
def _supervise(
argv: tuple[str, ...],
*,
@@ -1113,7 +1038,7 @@ def _supervise(
exit_code: int | None = None
sig: int | None = None
pid: int | None = None
stdout_tail = _ByteTail(OUTPUT_TAIL_BYTES)
stdout_tail = _StdoutTail(OUTPUT_TAIL_BYTES)
stderr_tail = _ByteTail(OUTPUT_TAIL_BYTES)
# Bounded prefix: raw_cap + exact-secret lookahead. Redact the complete
# prefix once, write at most raw_cap, then discard further drained bytes.
@@ -1386,6 +1311,7 @@ def _supervise(
"signal": sig,
"stdout_tail": stdout_text,
"stderr_tail": stderr_text,
"terminal_lines": stdout_tail.terminal_lines(),
"summary_source": summary_source,
}
@@ -1408,6 +1334,74 @@ class _ByteTail:
return bytes(self._data).decode("utf-8", "replace")
TERMINAL_LINE_MAX_BYTES = 1 << 20
TERMINAL_LINE_KEEP = 32
_TERMINAL_ENVELOPE_RE = re.compile(
rb'"(?:type|event)"\s*:\s*"(?:result|message_end)"'
)
_TERMINAL_HINT_RE = re.compile(rb'terminal_status|"is_error"')
class _StdoutTail(_ByteTail):
"""Byte tail that also keeps recent complete lines that may be terminal envelopes.
A final envelope can sit behind a later event larger than the byte tail
(for example PI ``agent_end``), so candidate lines are kept separately.
An oversized envelope line is recorded as ``None`` so decoding fails closed.
"""
def __init__(self, limit: int) -> None:
super().__init__(limit)
self._line = bytearray()
self._overflow_head: bytes | None = None
self._lines: list[bytes | None] = []
def extend(self, chunk: bytes) -> None:
super().extend(chunk)
start = 0
while start <= len(chunk):
nl = chunk.find(b"\n", start)
piece = chunk[start:] if nl == -1 else chunk[start:nl]
self._append_piece(piece)
if nl == -1:
return
self._end_line()
start = nl + 1
def _append_piece(self, piece: bytes) -> None:
if self._overflow_head is not None:
return
if len(self._line) + len(piece) > TERMINAL_LINE_MAX_BYTES:
self._overflow_head = bytes(self._line[:256] + piece[:256])[:256]
self._line.clear()
return
self._line.extend(piece)
def _end_line(self) -> None:
if self._overflow_head is not None:
if _TERMINAL_ENVELOPE_RE.search(self._overflow_head):
self._keep(None)
self._overflow_head = None
return
line = bytes(self._line)
self._line.clear()
if _TERMINAL_ENVELOPE_RE.search(line) or _TERMINAL_HINT_RE.search(line):
self._keep(line)
def _keep(self, line: bytes | None) -> None:
self._lines.append(line)
if len(self._lines) > TERMINAL_LINE_KEEP:
del self._lines[0]
def terminal_lines(self) -> list[str | None]:
if self._line or self._overflow_head is not None:
self._end_line()
return [
None if line is None else line.decode("utf-8", "replace")
for line in self._lines
]
def _kill_process_group(pid: int) -> None:
try:
os.killpg(pid, signal.SIGKILL)
@@ -1695,20 +1689,6 @@ def _worktree_pointer_snapshot(
return snap
def _external_side_effects_unproven(
unit: UnitPacket, worker_result: dict[str, Any] | None
) -> bool:
"""Network or external_cost units cannot be failed-clean without reconciliation.
A clean exit with no file diff is included. failed-clean still requires exact
restore plus affirmative evidence that no authorized external object remains
unknown. Worker prose is not that evidence; only an explicit trustworthy
reconciliation record would clear the unit, and none is accepted here.
"""
del worker_result # Worker prose alone never proves external reconciliation.
return bool(unit.network or unit.external_cost)
def _is_within_root(path: Path, root: Path) -> bool:
try:
path.resolve().relative_to(root.resolve())
@@ -1818,20 +1798,6 @@ def _create_transport_commit(
return ref
def _restore_worktree(repo: Path, worktree: Path, base_head: str) -> bool:
"""Exact-restore the private clone then remove it after verification."""
try:
_git(worktree, "reset", "--hard", base_head)
_git(worktree, "clean", "-fdx")
head = _git(worktree, "rev-parse", "HEAD").stdout.strip()
dirty = _git(worktree, "status", "--porcelain").stdout.strip()
ok = head == base_head and not dirty
_safe_remove_worktree(repo, worktree)
return ok
except Exception:
return False
def _safe_remove_worktree(repo: Path, worktree: Path) -> None:
"""Remove a private attempt clone directory (not a linked worktree)."""
del repo # Private clones are not registered linked worktrees.
@@ -1840,29 +1806,21 @@ def _safe_remove_worktree(repo: Path, worktree: Path) -> None:
def _load_worker_result(
result_path: Path, stdout_tail: str
adapter: str, result_path: Path, proc_meta: dict[str, Any]
) -> dict[str, Any] | None:
"""Return the worker's valid terminal record, or None when absent/invalid."""
result_text: str | None = None
if result_path.is_file():
try:
data = json.loads(result_path.read_text(encoding="utf-8"))
if isinstance(data, dict):
return data
except (OSError, json.JSONDecodeError):
pass
# Try last JSON object in stdout.
text = stdout_tail.strip()
if not text:
return None
for candidate in (text, text[text.rfind("{") :] if "{" in text else ""):
if not candidate.startswith("{"):
continue
try:
data = json.loads(candidate)
if isinstance(data, dict):
return data
except json.JSONDecodeError:
continue
return None
result_text = result_path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
result_text = None
return decode_terminal_record(
adapter,
event_lines=proc_meta.get("terminal_lines") or [],
stdout_tail=proc_meta.get("stdout_tail") or "",
result_text=result_text,
)
def _base_receipt(
@@ -1958,7 +1916,7 @@ def _finish_run(
)
history_path: Path | None = None
try:
history_path, attestation = _append_execution_history(
history_path, integrity_mac = _append_execution_history(
repo, receipt, secret_values
)
recorded: dict[str, Any] = {
@@ -1967,8 +1925,8 @@ def _finish_run(
"record_id": receipt.get("run_id"),
"run_id": receipt.get("run_id"),
}
if attestation:
recorded["attestation"] = attestation
if integrity_mac:
recorded["local_integrity_mac"] = integrity_mac
receipt["history"] = recorded
except Exception as exc:
receipt["history"] = {
@@ -2001,12 +1959,12 @@ def _append_execution_history(
history_path = history_dir / "execution-history.jsonl"
if history_path.is_symlink():
raise RuntimeError("agent-exec history file must not be a symlink")
attestation: str | None = None
integrity_mac: str | None = None
if receipt.get("terminal_status") == "success":
_bind_success_transport_commit(repo, receipt)
key = _ensure_attestation_key(repo)
key = _ensure_local_integrity_key(repo)
record = _execution_history_record(repo, receipt)
line, attestation = _signed_history_line(record, key, secret_values)
line, integrity_mac = _integrity_history_line(record, key, secret_values)
else:
record = _execution_history_record(repo, receipt)
line = _redact_text(_canonical_json(record), secret_values) + "\n"
@@ -2027,7 +1985,7 @@ def _append_execution_history(
fcntl.flock(fd, fcntl.LOCK_UN)
finally:
os.close(fd)
return history_path, attestation
return history_path, integrity_mac
def _execution_history_record(repo: Path, receipt: dict[str, Any]) -> dict[str, Any]:
@@ -2335,8 +2293,12 @@ def _valid_transport_ref(ref: object) -> str | None:
return ref
_ATTESTATION_KEY_NAME = "attestation.key"
_ATTESTATION_KEY_BYTES = 32
# Legacy on-disk names keep existing local history readable. The MAC is local
# integrity/tamper evidence only: any process running as the same OS user can
# read the key and forge records, so it is never GATE or a trusted identity.
_LOCAL_INTEGRITY_KEY_NAME = "attestation.key"
_LOCAL_INTEGRITY_FIELD = "attestation"
_LOCAL_INTEGRITY_KEY_BYTES = 32
def _git_common_dir(repo: Path) -> Path:
@@ -2363,27 +2325,27 @@ 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"}
def _local_integrity_mac(key: bytes, record: dict[str, Any]) -> str:
body = {field: value for field, value in record.items() if field != _LOCAL_INTEGRITY_FIELD}
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")
def _local_integrity_ok(key: bytes, record: dict[str, Any]) -> bool:
presented = record.get(_LOCAL_INTEGRITY_FIELD)
if not isinstance(presented, str) or len(presented) != 64:
return False
return hmac.compare_digest(_history_attestation(key, record), presented)
return hmac.compare_digest(_local_integrity_mac(key, record), presented)
def _signed_history_line(
def _integrity_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
mac = _local_integrity_mac(key, body)
body[_LOCAL_INTEGRITY_FIELD] = mac
return _canonical_json(body) + "\n", mac
@@ -2456,22 +2418,22 @@ def _locked_agent_exec_dir(repo: Path, *, create: bool) -> Iterator[int | None]:
def _read_key_at(dir_fd: int) -> bytes:
fd = os.open(
_ATTESTATION_KEY_NAME,
_LOCAL_INTEGRITY_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")
raise RuntimeError("local integrity 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")
raise RuntimeError("local integrity key mode must be 0600")
if current.st_size != _LOCAL_INTEGRITY_KEY_BYTES:
raise RuntimeError("local integrity 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")
key = os.read(fd, _LOCAL_INTEGRITY_KEY_BYTES)
if len(key) != _LOCAL_INTEGRITY_KEY_BYTES:
raise RuntimeError("local integrity key must be 32 bytes")
return key
finally:
os.close(fd)
@@ -2479,63 +2441,63 @@ def _read_key_at(dir_fd: int) -> bytes:
def _key_is_symlink(dir_fd: int) -> bool:
try:
info = os.stat(_ATTESTATION_KEY_NAME, dir_fd=dir_fd, follow_symlinks=False)
info = os.stat(_LOCAL_INTEGRITY_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."""
def _read_local_integrity_key(repo: Path) -> bytes | None:
"""Read the local integrity 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")
raise RuntimeError("local integrity 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
raise _symlink_oserror(exc, "local integrity key must not be a symlink") from exc
def _ensure_attestation_key(repo: Path) -> bytes:
def _ensure_local_integrity_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.
The MAC lets later apply/admit checks detect accidental or casual edits of
local history. It is not an identity: the same OS user can read the key.
"""
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")
raise RuntimeError("local integrity 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)
fd = os.open(_LOCAL_INTEGRITY_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
raise _symlink_oserror(exc, "local integrity 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")
raise RuntimeError("local integrity key must be a regular file")
if current.st_size == 0:
key = os.urandom(_ATTESTATION_KEY_BYTES)
key = os.urandom(_LOCAL_INTEGRITY_KEY_BYTES)
written = os.write(fd, key)
if written != _ATTESTATION_KEY_BYTES:
raise RuntimeError("attestation key write was short")
if written != _LOCAL_INTEGRITY_KEY_BYTES:
raise RuntimeError("local integrity key write was short")
os.fsync(fd)
return key
if current.st_size != _ATTESTATION_KEY_BYTES:
raise RuntimeError("attestation key must be 32 bytes")
if current.st_size != _LOCAL_INTEGRITY_KEY_BYTES:
raise RuntimeError("local integrity 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")
key = os.read(fd, _LOCAL_INTEGRITY_KEY_BYTES)
if len(key) != _LOCAL_INTEGRITY_KEY_BYTES:
raise RuntimeError("local integrity key must be 32 bytes")
if stat.S_IMODE(os.fstat(fd).st_mode) != 0o600:
raise RuntimeError("attestation key mode must be 0600")
raise RuntimeError("local integrity key mode must be 0600")
return key
finally:
os.close(fd)
@@ -2574,7 +2536,7 @@ def _transport_covers(
required: set[str],
key: bytes | None,
) -> bool:
if key is None or not _history_attestation_ok(key, record):
if key is None or not _local_integrity_ok(key, record):
return False
if record.get("schema") != HISTORY_SCHEMA:
return False
@@ -2587,32 +2549,32 @@ def _transport_covers(
return False
ref = _valid_transport_ref(transport.get("ref"))
parent = transport.get("parent")
signed_commit = transport.get("commit")
if ref is None or not isinstance(parent, str) or not isinstance(signed_commit, str):
recorded_commit = transport.get("commit")
if ref is None or not isinstance(parent, str) or not isinstance(recorded_commit, str):
return False
if _resolve_commit(repo, ref) != signed_commit:
if _resolve_commit(repo, ref) != recorded_commit:
return False
if _resolve_commit(repo, parent) != head:
return False
if _commit_parents(repo, signed_commit) != [head]:
if _commit_parents(repo, recorded_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, signed_commit)
diff_paths = _diff_paths(repo, head, recorded_commit)
if _non_exempt(changed) != required or _non_exempt(diff_paths) != required:
return False
return _index_matches_commit(repo, signed_commit, required)
return _index_matches_commit(repo, recorded_commit, required)
def _apply_signed_history_ok(
def _apply_local_integrity_ok(
repo: Path,
receipt: dict[str, Any],
*,
commit: str,
base_head: str,
) -> bool:
key = _read_attestation_key(repo)
key = _read_local_integrity_key(repo)
if key is None:
return False
run_id = receipt.get("run_id")
@@ -2648,7 +2610,7 @@ def _apply_signed_history_ok(
continue
if rec_transport.get("commit") != commit:
continue
if _history_attestation_ok(key, record):
if _local_integrity_ok(key, record):
return True
return False
@@ -2717,13 +2679,13 @@ def cmd_apply(repo: Path, receipt_path: Path) -> int:
return _reject("apply: mismatched transport", secrets)
try:
authorized = _apply_signed_history_ok(
authorized = _apply_local_integrity_ok(
repo, receipt, commit=commit, base_head=resolved_base
)
except Exception as exc:
return _reject(f"apply: no signed history: {exc}", secrets)
return _reject(f"apply: no local integrity record: {exc}", secrets)
if not authorized:
return _reject("apply: no signed history", secrets)
return _reject("apply: no local integrity record", secrets)
merge = _git(repo, "merge", "--ff-only", "--quiet", commit, check=False)
if merge.returncode != 0:
@@ -2736,7 +2698,11 @@ def cmd_apply(repo: Path, receipt_path: Path) -> int:
def cmd_admit(repo: Path) -> int:
"""Accept staged host-managed paths, or paths exactly covered by one transport."""
"""Accept staged host-managed paths, or paths exactly covered by one transport.
A pass is local integrity evidence (SELF), not GATE; invalid or missing
local records still reject.
"""
repo = repo.resolve()
secrets = _secret_set()
try:
@@ -2747,13 +2713,13 @@ def cmd_admit(repo: Path) -> int:
return EXIT_SUCCESS
head = _resolve_commit(repo, "HEAD")
if head is None:
return _reject("admit: no provenance", secrets)
return _reject("admit: no local integrity record", secrets)
history_text = ""
history_path = _history_file(repo)
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)
key = _read_local_integrity_key(repo)
for record in _load_history_records(repo):
if _transport_covers(
repo, record, head=head, required=required, key=key
@@ -2761,9 +2727,9 @@ def cmd_admit(repo: Path) -> int:
print("admit: ok")
return EXIT_SUCCESS
except Exception as exc:
return _reject(f"admit: no provenance: {exc}", secrets)
return _reject(f"admit: no local integrity record: {exc}", secrets)
listed = ", ".join(sorted(required))
return _reject(f"admit: no provenance for {listed}", secrets)
return _reject(f"admit: no local integrity record for {listed}", secrets)
if __name__ == "__main__":
+195
View File
@@ -5,11 +5,14 @@ does not contact providers or launch real agent runs.
"""
from __future__ import annotations
import json
import re
import shutil
import subprocess
from collections.abc import Sequence
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from scripts.agent_exec_config import (
ALLOWED_PLACEHOLDERS,
@@ -23,6 +26,34 @@ _PROBE_TIMEOUT_SECONDS = 5
MAX_UNIT_PACKET_BYTES = 262_144
UNIT_PACKET_DISPLAY_MARKER = "<unit-packet>"
TERMINAL_STATUSES = (
"completed",
"failed",
"blocked",
"cancelled",
"failed-unknown",
"scope-expansion",
)
TERMINAL_RECORD_SCHEMA: dict[str, Any] = {
"type": "object",
"required": [
"terminal_status",
"summary",
"changed_files",
"evidence",
"scope_expansion",
],
"properties": {
"terminal_status": {"enum": list(TERMINAL_STATUSES)},
"summary": {"type": "string"},
"changed_files": {"type": "array", "items": {"type": "string"}},
"evidence": {"type": "array", "items": {"type": "string"}},
"scope_expansion": {"type": ["string", "null"]},
},
}
# Bounds the backwards brace scan over one final message.
_MAX_TERMINAL_TEXT_CHARS = 262_144
_PLACEHOLDER_MAP = {
"${workspace}": "workspace",
"${unit_packet}": "unit_packet",
@@ -378,17 +409,177 @@ def _read_unit_packet_text(path: Path) -> str:
def _build_bounded_unit_prompt(packet_text: str) -> str:
schema = json.dumps(TERMINAL_RECORD_SCHEMA, separators=(",", ":"))
return (
"Implement exactly the supplied bounded implementation unit. "
"Work only inside the current workspace. "
"Leave all changes uncommitted. "
"Do not expand scope beyond the unit packet. "
"Return a structured final result matching the unit's required schema.\n\n"
"--- TERMINAL RECORD ---\n\n"
"End your final message with exactly one terminal record: a single-line "
"JSON object that validates against this JSON Schema:\n"
f"{schema}\n"
"Use terminal_status completed only when the unit is fully implemented. "
"Use blocked, cancelled, failed-unknown, failed, or scope-expansion "
"otherwise. A missing or invalid terminal record fails the attempt.\n\n"
"--- BOUNDED IMPLEMENTATION UNIT PACKET ---\n\n"
f"{packet_text}"
)
def extract_terminal_record(text: str | None) -> dict[str, Any] | None:
"""Return the last valid terminal record embedded in one final message."""
if not isinstance(text, str) or not text:
return None
text = text[-_MAX_TERMINAL_TEXT_CHARS:]
decoder = json.JSONDecoder()
pos = text.rfind("{")
while pos != -1:
try:
candidate, _ = decoder.raw_decode(text, pos)
except json.JSONDecodeError:
candidate = None
record = _valid_terminal_record(candidate)
if record is not None:
return record
pos = text.rfind("{", 0, pos)
return None
def _valid_terminal_record(obj: object) -> dict[str, Any] | None:
if not isinstance(obj, dict):
return None
status = obj.get("terminal_status")
if not isinstance(status, str):
return None
status = status.strip().replace("_", "-")
if status not in TERMINAL_STATUSES:
return None
if not isinstance(obj.get("summary"), str):
return None
for key in ("changed_files", "evidence"):
items = obj.get(key)
if not isinstance(items, list) or any(not isinstance(i, str) for i in items):
return None
if "scope_expansion" not in obj or not (
obj["scope_expansion"] is None or isinstance(obj["scope_expansion"], str)
):
return None
record = dict(obj)
record["terminal_status"] = status
return record
def decode_terminal_record(
adapter: str,
*,
event_lines: Sequence[str | None],
stdout_tail: str,
result_text: str | None,
) -> dict[str, Any] | None:
"""Decode the worker terminal record from the adapter's real output.
``event_lines`` are recent complete stdout lines that may carry a terminal
envelope, oldest first; ``None`` marks an envelope line too large to keep.
Event-stream adapters are read only from their final assistant/result
envelope, so prompt echoes and tool output never count as a record.
"""
if adapter in ("cursor", "claude"):
return extract_terminal_record(_cursor_final_text(event_lines))
if adapter == "antigravity":
return extract_terminal_record(_agy_final_text(event_lines, stdout_tail))
if adapter == "pi":
return extract_terminal_record(_pi_final_text(event_lines))
record = extract_terminal_record(result_text)
if record is None:
record = extract_terminal_record(stdout_tail)
return record
def _event_objects(event_lines: Sequence[str | None]) -> list[dict[str, Any] | None]:
"""Newest-first parsed events; ``None`` keeps the oversized-envelope marker."""
out: list[dict[str, Any] | None] = []
for line in reversed(event_lines):
if line is None:
out.append(None)
continue
try:
obj = json.loads(line)
except json.JSONDecodeError:
continue
if isinstance(obj, dict):
out.append(obj)
return out
def _cursor_final_text(event_lines: Sequence[str | None]) -> str | None:
for obj in _event_objects(event_lines):
if obj is None:
return None
if obj.get("type") != "result":
continue
if obj.get("is_error") is not False or obj.get("subtype") != "success":
return None
text = obj.get("result")
return text if isinstance(text, str) else None
return None
def _agy_final_text(
event_lines: Sequence[str | None], stdout_tail: str
) -> str | None:
candidates = _event_objects(event_lines)
try:
whole = json.loads(stdout_tail.strip())
except json.JSONDecodeError:
whole = None
if isinstance(whole, dict):
candidates.append(whole)
for obj in candidates:
if obj is None:
return None
if obj.get("event") == "result":
payload = obj.get("result")
elif "event" not in obj and "response" in obj:
payload = obj
else:
continue
if not isinstance(payload, dict):
return None
if payload.get("is_error") or payload.get("error"):
return None
text = payload.get("response")
return text if isinstance(text, str) else None
return None
def _pi_final_text(event_lines: Sequence[str | None]) -> str | None:
for obj in _event_objects(event_lines):
if obj is None:
return None
if obj.get("type") != "message_end":
continue
message = obj.get("message")
if not isinstance(message, dict) or message.get("role") != "assistant":
continue
if message.get("stopReason") in ("error", "aborted"):
return None
content = message.get("content")
if isinstance(content, str):
return content
if not isinstance(content, list):
return None
return "".join(
block["text"]
for block in content
if isinstance(block, dict)
and block.get("type") == "text"
and isinstance(block.get("text"), str)
)
return None
def _expand_argv(
items: tuple[str, ...],
values: dict[str, str],
@@ -480,11 +671,15 @@ def _sanitize_display(
__all__ = [
"MAX_UNIT_PACKET_BYTES",
"TERMINAL_RECORD_SCHEMA",
"TERMINAL_STATUSES",
"UNIT_PACKET_DISPLAY_MARKER",
"AdapterProbe",
"AdapterSpec",
"BUILTIN_SPECS",
"Invocation",
"build_invocation",
"decode_terminal_record",
"extract_terminal_record",
"probe_adapter",
]
+3 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# 本地硬闸(pre-commit):集成分支直提拒绝 + 分支命名。权威准入在 PR(baseline.md:26)。
# 本地检查(pre-commit):集成分支直提拒绝 + 分支命名 + 本地完整性核对。权威准入在 PR(baseline.md:26)。
branch=$(git rev-parse --abbrev-ref HEAD)
case "$branch" in
main|master)
@@ -11,4 +11,6 @@ echo "$branch" | grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' || {
exit 1
}
root=$(git rev-parse --show-toplevel)
# admit 只是本地完整性/篡改证据(SELF):同一 OS 用户可读取本地 HMAC 密钥并绕过本钩子,
# 它不是 GATE,也不是可信身份。缺失或无效的本地记录仍拒绝;交付信任来自精确 HEAD 的 CI 或独立 REPLAY。
PYTHONDONTWRITEBYTECODE=1 python3 "$root/scripts/agent_exec.py" admit --repo "$root"