From 1e161a00db985bfd2a7a742c81623a8337788153 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Wed, 27 May 2026 16:34:14 +0300 Subject: [PATCH 1/2] examples: add payment guard evidence export bundle (closes #151) Add scripts/export_payment_guard_evidence.py: - Copies audit.jsonl, replay-store.json, config, policy into --out dir - Runs hash-chain verification inline (no external subprocess) - Generates verification_report.json with generated_at, bundle_type, audit_records_count, replay_store_nonces, hash_chain_valid, source_files, copied_files - Missing audit.jsonl -> hard fail with clear error - Missing replay-store.json -> exports empty {} with a warning - Existing output dir: overwrite (documented in --help) - Zero new dependencies (stdlib only: json, pathlib, shutil, hashlib, argparse, datetime) Add examples/agent-payment-guard/run_evidence_export_check.sh: - Spins up service, runs run_service_check.sh fixtures to produce audit.jsonl + replay-store.json, then calls the export script - Verifies verification_report.json fields - Re-runs verify_audit_log.py against bundled audit.jsonl - Prints pass/fail summary --- .../run_evidence_export_check.sh | 84 +++++++ scripts/export_payment_guard_evidence.py | 226 ++++++++++++++++++ 2 files changed, 310 insertions(+) create mode 100644 examples/agent-payment-guard/run_evidence_export_check.sh create mode 100644 scripts/export_payment_guard_evidence.py diff --git a/examples/agent-payment-guard/run_evidence_export_check.sh b/examples/agent-payment-guard/run_evidence_export_check.sh new file mode 100644 index 0000000..8988f28 --- /dev/null +++ b/examples/agent-payment-guard/run_evidence_export_check.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Smoke-test for the evidence export bundle (closes #151). +# +# What this does: +# 1. Starts the payment guard service (enforce mode, signed intent required) +# 2. Sends a valid signed-intent ACCEPT request (creates audit record + replay-store nonce) +# 3. Stops the service +# 4. Runs export_payment_guard_evidence.py +# 5. Verifies the bundle structure and verification_report.json fields +# 6. Re-runs verify_audit_log.py against the bundled audit.jsonl +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT_DIR" + +rm -rf proofpath-evidence-bundle/ +rm -f .proofpath/audit.jsonl .proofpath/replay-store.json + +HOST="127.0.0.1" +PORT="18788" +SERVICE="examples/agent-payment-guard/payment_guard_service.py" +CONFIG="examples/agent-payment-guard/payment_guard_service_config.json" + +python3 "$SERVICE" \ + --config "$CONFIG" \ + --port "$PORT" \ + >/tmp/payment_guard_evidence_svc.log 2>&1 & +SERVICE_PID=$! +cleanup() { + kill "$SERVICE_PID" >/dev/null 2>&1 || true + rm -rf proofpath-evidence-bundle/ +} +trap cleanup EXIT + +# Wait for service to be ready +for _ in $(seq 1 50); do + if curl -fsS "http://$HOST:$PORT/v1/health" >/dev/null 2>&1; then + break + fi + sleep 0.1 +done + +# Send one valid signed-intent request (ACCEPT) +VALID_INTENT=$(cat examples/agent-payment-guard/intent_envelopes/intent.valid.json) +VALID_PROPOSAL=$(cat examples/agent-payment-guard/payment_proposal.valid_micro_payment.json) + +curl -fsS -X POST "http://$HOST:$PORT/v1/payment-proposals/evaluate" \ + -H 'content-type: application/json' \ + -d "{\"mode\":\"enforce\",\"proposal\":$VALID_PROPOSAL,\"intent_envelope\":$VALID_INTENT}" \ + | python3 -c 'import json,sys; r=json.load(sys.stdin); assert r["decision"]=="ACCEPT", f"expected ACCEPT, got {r}"' + +# Stop service before export +kill "$SERVICE_PID" >/dev/null 2>&1 || true +sleep 0.2 + +# --- run export --- +python3 scripts/export_payment_guard_evidence.py \ + --out proofpath-evidence-bundle/ + +# --- verify bundle structure --- +for f in audit.jsonl replay-store.json payment_guard_service_config.json payment_policy.json verification_report.json; do + [ -f "proofpath-evidence-bundle/$f" ] || { echo "FAIL: missing proofpath-evidence-bundle/$f" >&2; exit 1; } +done + +# --- verify verification_report.json --- +python3 - <<'EOF' +import json, pathlib, sys +r = json.loads(pathlib.Path("proofpath-evidence-bundle/verification_report.json").read_text()) +assert r["bundle_type"] == "agent-payment-guard-evidence", f"bundle_type mismatch: {r}" +assert r["audit_records_count"] >= 1, f"expected >= 1 audit record, got {r['audit_records_count']}" +assert r["replay_store_nonces"] >= 1, f"expected >= 1 nonce, got {r['replay_store_nonces']}" +assert r["hash_chain_valid"] is True, f"hash_chain_valid is not True: {r}" +assert "generated_at" in r, "missing generated_at" +assert "source_files" in r, "missing source_files" +assert "copied_files" in r, "missing copied_files" +for k in ("audit", "replay_store", "config", "policy"): + assert k in r["source_files"], f"missing source_files.{k}" +print(f" report OK: {r['audit_records_count']} records, {r['replay_store_nonces']} nonces, chain={r['hash_chain_valid']}") +EOF + +# --- re-verify hash chain against bundled audit.jsonl --- +python3 scripts/verify_audit_log.py proofpath-evidence-bundle/audit.jsonl >/dev/null + +echo "Evidence export check passed." diff --git a/scripts/export_payment_guard_evidence.py b/scripts/export_payment_guard_evidence.py new file mode 100644 index 0000000..7eca43b --- /dev/null +++ b/scripts/export_payment_guard_evidence.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +"""Export a portable Agent Payment Guard evidence bundle. + +Usage +----- + python3 scripts/export_payment_guard_evidence.py --out proofpath-evidence-bundle/ + +Optional overrides +------------------ + --audit-path .proofpath/audit.jsonl + --replay-store-path .proofpath/replay-store.json + --config examples/agent-payment-guard/payment_guard_service_config.json + --policy examples/agent-payment-guard/payment_policy.json + +Behavior +-------- +- Creates --out directory if it does not exist. +- Existing --out directory is overwritten (files are replaced; extra files + already present are left in place). +- Missing audit file -> exits with error. +- Missing replay-store -> exports an empty {} store with a warning. +- Hash-chain verification is run during export; hash_chain_valid=false does + NOT abort the export, but is recorded in verification_report.json. +""" +from __future__ import annotations + +import argparse +import json +import shutil +import sys +from datetime import datetime, timezone +from hashlib import sha256 +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + + +# --------------------------------------------------------------------------- +# Hash-chain verification (inline, no subprocess) +# --------------------------------------------------------------------------- + +def canonical_json(payload: Dict[str, Any]) -> str: + return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + + +def compute_record_hash(record: Dict[str, Any]) -> str: + payload = dict(record) + payload.pop("hash", None) + return "sha256:" + sha256(canonical_json(payload).encode("utf-8")).hexdigest() + + +def verify_hash_chain(audit_path: Path) -> Tuple[bool, str, int]: + """Return (valid, message, record_count).""" + if not audit_path.exists(): + return False, f"audit file not found: {audit_path}", 0 + + lines = [line for line in audit_path.read_text(encoding="utf-8").splitlines() if line.strip()] + if not lines: + return True, "audit log is empty", 0 + + expected_previous = "GENESIS" + for idx, line in enumerate(lines, start=1): + try: + record = json.loads(line) + except json.JSONDecodeError as exc: + return False, f"line {idx}: JSON decode error: {exc}", idx - 1 + + prev = record.get("previous_hash", "") + if prev != expected_previous: + return ( + False, + f"line {idx}: previous_hash mismatch (expected {expected_previous!r}, got {prev!r})", + idx - 1, + ) + + stored_hash = record.get("hash", "") + computed_hash = compute_record_hash(record) + if stored_hash != computed_hash: + return ( + False, + f"line {idx}: hash mismatch (stored {stored_hash!r}, computed {computed_hash!r})", + idx - 1, + ) + + expected_previous = stored_hash + + return True, f"chain valid ({len(lines)} records)", len(lines) + + +# --------------------------------------------------------------------------- +# Export +# --------------------------------------------------------------------------- + +def load_replay_store_safe(path: Path) -> Tuple[Dict[str, Any], bool]: + """Return (store_dict, was_present).""" + if not path.exists(): + return {}, False + try: + data = json.loads(path.read_text(encoding="utf-8")) + return (data if isinstance(data, dict) else {}), True + except (json.JSONDecodeError, OSError): + return {}, True + + +def export_bundle( + audit_path: Path, + replay_store_path: Path, + config_path: Path, + policy_path: Path, + out_dir: Path, +) -> int: + """Build the evidence bundle. Returns exit code (0 = success, 1 = error).""" + + # --- validate required inputs --- + if not audit_path.exists(): + print(f"[export] ERROR: audit file not found: {audit_path}", file=sys.stderr) + return 1 + + if not config_path.exists(): + print(f"[export] ERROR: config file not found: {config_path}", file=sys.stderr) + return 1 + + if not policy_path.exists(): + print(f"[export] ERROR: policy file not found: {policy_path}", file=sys.stderr) + return 1 + + # --- create output directory --- + out_dir.mkdir(parents=True, exist_ok=True) + + # --- hash-chain verification --- + chain_valid, chain_msg, record_count = verify_hash_chain(audit_path) + if not chain_valid: + print(f"[export] WARNING: hash chain invalid: {chain_msg}", file=sys.stderr) + else: + print(f"[export] hash chain: {chain_msg}") + + # --- replay store --- + replay_store, replay_present = load_replay_store_safe(replay_store_path) + if not replay_present: + print( + f"[export] WARNING: replay-store not found at {replay_store_path}; " + "exporting empty store", + file=sys.stderr, + ) + + # --- copy files --- + copied: List[str] = [] + + def _copy(src: Path, dest_name: str) -> None: + dest = out_dir / dest_name + shutil.copy2(src, dest) + copied.append(dest_name) + print(f"[export] copied {src} -> {dest}") + + _copy(audit_path, "audit.jsonl") + _copy(config_path, "payment_guard_service_config.json") + _copy(policy_path, "payment_policy.json") + + # replay-store: copy if present, else write empty + replay_dest = out_dir / "replay-store.json" + if replay_present and replay_store_path.exists(): + shutil.copy2(replay_store_path, replay_dest) + else: + replay_dest.write_text(json.dumps({}, indent=2), encoding="utf-8") + copied.append("replay-store.json") + print(f"[export] copied {replay_store_path} -> {replay_dest}") + + # --- verification report --- + report: Dict[str, Any] = { + "generated_at": datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"), + "bundle_type": "agent-payment-guard-evidence", + "audit_records_count": record_count, + "replay_store_nonces": len(replay_store), + "hash_chain_valid": chain_valid, + "hash_chain_message": chain_msg, + "source_files": { + "audit": str(audit_path), + "replay_store": str(replay_store_path), + "config": str(config_path), + "policy": str(policy_path), + }, + "copied_files": copied + ["verification_report.json"], + } + report_path = out_dir / "verification_report.json" + report_path.write_text(json.dumps(report, indent=2, sort_keys=True), encoding="utf-8") + print(f"[export] wrote {report_path}") + + print(f"\n[export] bundle ready: {out_dir}/") + print(f" records : {record_count}") + print(f" nonces : {len(replay_store)}") + print(f" chain : {'OK' if chain_valid else 'INVALID'}") + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Export Agent Payment Guard evidence bundle.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=( + "Examples:\n" + " python3 scripts/export_payment_guard_evidence.py --out proofpath-evidence-bundle/\n" + " python3 scripts/verify_audit_log.py proofpath-evidence-bundle/audit.jsonl\n" + ), + ) + parser.add_argument("--out", required=True, metavar="DIR", + help="Output directory for the evidence bundle (created if absent, overwritten if present).") + parser.add_argument("--audit-path", default=".proofpath/audit.jsonl", metavar="PATH") + parser.add_argument("--replay-store-path", default=".proofpath/replay-store.json", metavar="PATH") + parser.add_argument("--config", + default="examples/agent-payment-guard/payment_guard_service_config.json", + metavar="PATH") + parser.add_argument("--policy", + default="examples/agent-payment-guard/payment_policy.json", + metavar="PATH") + args = parser.parse_args() + + return export_bundle( + audit_path=Path(args.audit_path), + replay_store_path=Path(args.replay_store_path), + config_path=Path(args.config), + policy_path=Path(args.policy), + out_dir=Path(args.out), + ) + + +if __name__ == "__main__": + raise SystemExit(main()) From 1daeeb04fec65ae0c9ad52d2dbdfd80e9c421b3e Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Wed, 27 May 2026 16:41:02 +0300 Subject: [PATCH 2/2] docs: add evidence export bundle section to agent-payment-guard-service.md (#151) --- docs/agent-payment-guard-service.md | 86 ++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/docs/agent-payment-guard-service.md b/docs/agent-payment-guard-service.md index 201b86c..0a25d68 100644 --- a/docs/agent-payment-guard-service.md +++ b/docs/agent-payment-guard-service.md @@ -163,10 +163,94 @@ Behavior: Records come from `.proofpath/audit.jsonl` and include hash chaining (`previous_hash`, `hash`). +## Evidence export bundle + +The evidence export script packages all ProofPath decision artifacts into a +portable local bundle for offline inspection, auditing, or grant evidence. + +### Export + +```bash +python3 scripts/export_payment_guard_evidence.py --out proofpath-evidence-bundle/ +``` + +Optional path overrides (all have sensible defaults): + +```bash +python3 scripts/export_payment_guard_evidence.py \ + --out proofpath-evidence-bundle/ \ + --audit-path .proofpath/audit.jsonl \ + --replay-store-path .proofpath/replay-store.json \ + --config examples/agent-payment-guard/payment_guard_service_config.json \ + --policy examples/agent-payment-guard/payment_policy.json +``` + +### Bundle contents + +```text +proofpath-evidence-bundle/ + audit.jsonl — hash-chained decision log + replay-store.json — spent signed-intent nonces + payment_guard_service_config.json — service configuration snapshot + payment_policy.json — payment policy snapshot + verification_report.json — generated summary (see below) +``` + +### Inspect the bundle + +Verify hash-chain integrity of the bundled audit log: + +```bash +python3 scripts/verify_audit_log.py proofpath-evidence-bundle/audit.jsonl +``` + +Read the verification report: + +```bash +python3 -m json.tool proofpath-evidence-bundle/verification_report.json +``` + +Example `verification_report.json`: + +```json +{ + "bundle_type": "agent-payment-guard-evidence", + "generated_at": "2026-05-27T00:00:00Z", + "audit_records_count": 8, + "replay_store_nonces": 1, + "hash_chain_valid": true, + "hash_chain_message": "chain valid (8 records)", + "source_files": { + "audit": ".proofpath/audit.jsonl", + "replay_store": ".proofpath/replay-store.json", + "config": "examples/agent-payment-guard/payment_guard_service_config.json", + "policy": "examples/agent-payment-guard/payment_policy.json" + }, + "copied_files": [ + "audit.jsonl", + "replay-store.json", + "payment_guard_service_config.json", + "payment_policy.json", + "verification_report.json" + ] +} +``` + +### Edge cases + +| Situation | Behavior | +|---|---| +| `audit.jsonl` missing | Hard fail — exits with error | +| `replay-store.json` missing | Warning printed; exports empty `{}` | +| Hash chain invalid | Warning printed; `hash_chain_valid: false` in report; export continues | +| Output directory exists | Files overwritten; extra files left in place | + ## Local validation ```bash bash examples/agent-payment-guard/run_demo_check.sh bash examples/agent-payment-guard/run_service_check.sh -python3 scripts/verify_audit_log.py .proofpath/audit.jsonl +python3 scripts/export_payment_guard_evidence.py --out proofpath-evidence-bundle/ +python3 scripts/verify_audit_log.py proofpath-evidence-bundle/audit.jsonl +bash examples/agent-payment-guard/run_evidence_export_check.sh ```