e044a2de0d
---ci--- project: acdl phase: 6 milestone: v1.1 status: plan-as-execute persona: lead-developer tasks: [T-6.1, T-6.2, T-6.3, T-6.4] ---/ci--- Archive the v1.0 demo under demo/ (D-037) and reorient the repo to the real platform. Wave 1 of the Phase 06 plan. - T-6.1: git mv modules/, scripts/, evidence-ui/, contracts/, contracts-repo/, .gitea/ -> demo/; mv ACDL_DEMO.md + runner-data/ -> demo/ - T-6.2: scaffold new v1.1 top-level dirs (platform/, schemas/, adapters/, terraform/, modules-ir/) with .gitkeep - T-6.3: create top-level scripts/verify_phase06.sh (v1.1 verify scripts live at top-level, NOT demo/scripts/ which holds the v1.0 demo verify scripts) - T-6.4: rewrite README.md to reflect the real platform (vision + architecture links, new layout, status v1.1 active); add runner-data/ to .gitignore All moves via git mv (history preserved). Repo root now contains only README.md, demo/, docs/, .ciagent/, and the new empty v1.1 dirs.
123 lines
3.7 KiB
Python
Executable File
123 lines
3.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""evidence_writer.py — REQ-11 / D-023 / D-005
|
|
|
|
Appends a hash-chained event to audit.json.
|
|
|
|
Each event: {"seq": N, "ts": <iso8601 UTC>, "stage": "...", "event": "...",
|
|
"prev_hash": "<sha256 or GENESIS>", "hash": "<sha256 of canonical json of this event with hash empty>"}
|
|
|
|
Hash chain (D-023):
|
|
1. Build event dict with hash = "" (empty string).
|
|
2. canonical = json.dumps(event, sort_keys=True, separators=(",", ":"))
|
|
3. hash = sha256(canonical.encode("utf-8")).hexdigest()
|
|
4. event["hash"] = hash
|
|
5. append to audit.json
|
|
|
|
Auto-genesis: if audit.json is empty/missing and --stage is not "genesis",
|
|
a genesis event (seq 0, prev_hash "GENESIS") is inserted first.
|
|
|
|
Input:
|
|
--stage <dev|qa|prod|finalize|genesis> (required)
|
|
--event "<text>" (required)
|
|
--audit <path> (optional, default ./audit.json)
|
|
Output: stdout {"seq": N, "hash": "..."}
|
|
Exit: 0 on success, 1 on I/O error.
|
|
"""
|
|
import argparse
|
|
import datetime
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
GENESIS_EVENT_TEXT = "audit log initialized"
|
|
|
|
|
|
def now_iso8601_utc() -> str:
|
|
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
|
|
def compute_hash(event: dict) -> str:
|
|
"""Compute the sha256 hash of an event using canonical JSON (D-023)."""
|
|
tmp = dict(event)
|
|
tmp["hash"] = ""
|
|
canonical = json.dumps(tmp, sort_keys=True, separators=(",", ":"))
|
|
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def make_event(seq: int, stage: str, event_text: str, prev_hash: str) -> dict:
|
|
event = {
|
|
"seq": seq,
|
|
"ts": now_iso8601_utc(),
|
|
"stage": stage,
|
|
"event": event_text,
|
|
"prev_hash": prev_hash,
|
|
"hash": "",
|
|
}
|
|
event["hash"] = compute_hash(event)
|
|
return event
|
|
|
|
|
|
def load_audit(audit_path: str) -> list:
|
|
if not os.path.exists(audit_path):
|
|
return []
|
|
try:
|
|
with open(audit_path, "r", encoding="utf-8") as fh:
|
|
data = json.load(fh)
|
|
except (json.JSONDecodeError, ValueError):
|
|
return []
|
|
if not isinstance(data, list):
|
|
return []
|
|
return data
|
|
|
|
|
|
def atomic_write(audit_path: str, data: list) -> None:
|
|
tmp_path = audit_path + ".tmp"
|
|
with open(tmp_path, "w", encoding="utf-8") as fh:
|
|
json.dump(data, fh, indent=2)
|
|
fh.write("\n")
|
|
os.replace(tmp_path, audit_path)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Append a hash-chained event to audit.json")
|
|
parser.add_argument("--stage", required=True,
|
|
choices=["dev", "qa", "prod", "finalize", "genesis"])
|
|
parser.add_argument("--event", required=True)
|
|
parser.add_argument("--audit", default="./audit.json")
|
|
args = parser.parse_args()
|
|
|
|
events = load_audit(args.audit)
|
|
|
|
# Auto-genesis: if the log is empty and the caller did not ask for a
|
|
# genesis event, seed one first.
|
|
if len(events) == 0 and args.stage != "genesis":
|
|
genesis = make_event(seq=0, stage="genesis", event_text=GENESIS_EVENT_TEXT,
|
|
prev_hash="GENESIS")
|
|
events.append(genesis)
|
|
|
|
# Determine the new seq + prev_hash.
|
|
if events:
|
|
last = events[-1]
|
|
seq = last["seq"] + 1
|
|
prev_hash = last["hash"]
|
|
else:
|
|
seq = 0
|
|
prev_hash = "GENESIS"
|
|
|
|
new_event = make_event(seq=seq, stage=args.stage, event_text=args.event,
|
|
prev_hash=prev_hash)
|
|
events.append(new_event)
|
|
|
|
try:
|
|
atomic_write(args.audit, events)
|
|
except OSError as exc:
|
|
print(f"evidence_writer: I/O error: {exc}", file=sys.stderr)
|
|
return 1
|
|
|
|
print(json.dumps({"seq": new_event["seq"], "hash": new_event["hash"]}))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main()) |