Files

166 lines
5.8 KiB
Python

#!/usr/bin/env python3
"""Run one verifier and append a typed, context-bound evidence receipt.
Claude Code's generic PostToolUse response does not always expose a process exit
code. This sanctioned runner owns the subprocess, so exit status, assertion
status, command argv, output hashes, workflow/session/agent context, and subject
are recorded together. It never invokes a shell.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import subprocess
import sys
import time
import uuid
from datetime import datetime, timezone
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import _workspace as W # noqa: E402
CATEGORIES = (
"acceptance-criteria", "test", "security", "privacy", "data-quality",
"reliability", "release-readiness",
)
TRIVIAL_EXECUTABLES = {"true", "false", "echo", "printf", "ls", "cat", "grep", "pwd"}
def _now():
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def _sha(value):
return hashlib.sha256(value).hexdigest()
def _safe_cwd(raw):
root = os.path.realpath(W.work_root())
candidate = raw if os.path.isabs(raw) else os.path.join(root, raw)
candidate = os.path.realpath(candidate)
if os.path.commonpath([root, candidate]) != root or not os.path.isdir(candidate):
raise ValueError(f"--cwd must be an existing directory inside workspace: {raw}")
return candidate
def _append_receipt(receipt):
evidence_dir = W.evidence_dir()
os.makedirs(evidence_dir, exist_ok=True)
path = os.path.join(evidence_dir, "ledger.jsonl")
with open(path, "a", encoding="utf-8") as fh:
try:
import fcntl
fcntl.flock(fh.fileno(), fcntl.LOCK_EX)
except Exception:
pass
fh.write(json.dumps(receipt, ensure_ascii=False) + "\n")
fh.flush()
os.fsync(fh.fileno())
try:
import fcntl
fcntl.flock(fh.fileno(), fcntl.LOCK_UN)
except Exception:
pass
def build_parser():
parser = argparse.ArgumentParser(description="execute a verifier and mint a typed receipt")
parser.add_argument("--workflow", required=True)
parser.add_argument("--agent", required=True)
parser.add_argument("--session", required=True)
parser.add_argument("--category", choices=CATEGORIES, required=True)
parser.add_argument("--subject", required=True,
help="specific criterion/component being verified")
parser.add_argument("--source-revision-sha256")
parser.add_argument("--cwd", default=".")
parser.add_argument("command", nargs=argparse.REMAINDER)
return parser
def main(argv=None):
args = build_parser().parse_args(argv)
command = list(args.command)
if command and command[0] == "--":
command = command[1:]
if not command:
sys.stderr.write("[verify_run] verifier command required after --\n")
return 2
executable = os.path.basename(command[0]).lower()
if executable in TRIVIAL_EXECUTABLES:
sys.stderr.write(f"[verify_run] trivial command cannot prove verification: {executable}\n")
return 2
if executable in {"sh", "bash", "zsh", "fish"}:
sys.stderr.write("[verify_run] shell interpreters are forbidden; pass verifier argv directly\n")
return 2
if args.source_revision_sha256 and (
len(args.source_revision_sha256) != 64
or any(ch not in "0123456789abcdef" for ch in args.source_revision_sha256.lower())):
sys.stderr.write("[verify_run] --source-revision-sha256 must be 64-hex\n")
return 2
try:
cwd = _safe_cwd(args.cwd)
except Exception as exc:
sys.stderr.write(f"[verify_run] {exc}\n")
return 2
started_at = _now()
started = time.monotonic()
try:
completed = subprocess.run(command, cwd=cwd, capture_output=True, check=False)
exit_code = completed.returncode
stdout = completed.stdout or b""
stderr = completed.stderr or b""
except OSError as exc:
exit_code, stdout, stderr = 127, b"", str(exc).encode("utf-8", "replace")
finished_at = _now()
receipt_id = f"vr-{int(time.time())}-{uuid.uuid4().hex[:12]}"
receipt = {
"receipt_id": receipt_id,
"tool_use_id": receipt_id,
"receipt_type": "verification-run",
"tool_name": "VerifyRun",
"workflow_id": args.workflow,
"session_id": args.session,
"agent_id": args.agent,
"verification_category": args.category,
"verification_subject": args.subject,
"assertion_status": "passed" if exit_code == 0 else "failed",
"exit_code": exit_code,
"command_argv": command,
"command_argv_sha256": _sha(
json.dumps(command, ensure_ascii=False, separators=(",", ":")).encode("utf-8")),
"cwd": cwd,
"started_at": started_at,
"ts": finished_at,
"duration_ms": round((time.monotonic() - started) * 1000),
"stdout_sha256": _sha(stdout),
"stderr_sha256": _sha(stderr),
}
if args.source_revision_sha256:
receipt["source_revision_sha256"] = args.source_revision_sha256.lower()
try:
_append_receipt(receipt)
except Exception as exc:
sys.stderr.write(f"[verify_run] receipt append failed: {exc}\n")
return 125
if stdout:
sys.stdout.buffer.write(stdout)
if not stdout.endswith(b"\n"):
sys.stdout.buffer.write(b"\n")
if stderr:
sys.stderr.buffer.write(stderr)
if not stderr.endswith(b"\n"):
sys.stderr.buffer.write(b"\n")
sys.stderr.write(
f"[verify_run] receipt-id={receipt_id} status={receipt['assertion_status']} "
f"exit={exit_code} category={args.category} subject={args.subject}\n")
return exit_code if 0 <= exit_code <= 124 else 124
if __name__ == "__main__":
raise SystemExit(main())