WhatToChange: recommended-changes/2026-09-21-event-loop-and-ledger-perms/ What-to-Change.md What-to-Change-To.md How-to-implement-the-changes.md What was actually done: - Global HTTP stamp and verify run via asyncio.to_thread. - File ledger saves chmod 0600. - .gitignore covers coverage, mypy, logs, and editor directories. What was not done: - (none)
242 lines
7.4 KiB
Python
242 lines
7.4 KiB
Python
"""Hash-only timestamp ledger. Memory implementation for tests and stdio."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Protocol
|
|
|
|
from pydantic import BaseModel
|
|
|
|
SHA256_EMPTY = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
_SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
|
|
_SCOPE_RANK = {"local": 1, "shared": 2, "global": 3}
|
|
|
|
|
|
class Receipt(BaseModel):
|
|
receipt_id: str
|
|
hash: str
|
|
scope: str
|
|
cube_id: str = ""
|
|
org_id: str = ""
|
|
created_at: str
|
|
merkle: str
|
|
restamp_of: str = ""
|
|
|
|
|
|
class Receipts(BaseModel):
|
|
receipts: list[Receipt]
|
|
|
|
|
|
class VerifyResult(BaseModel):
|
|
valid: bool
|
|
error: str | None = None
|
|
receipt: Receipt | None = None
|
|
|
|
|
|
class Ledger(Protocol):
|
|
async def stamp(
|
|
self,
|
|
sha256: str,
|
|
scope: str = "local",
|
|
cube_id: str = "",
|
|
org_id: str = "",
|
|
) -> list[Receipt]: ...
|
|
|
|
async def verify(self, receipt_id: str, want_scope: str = "") -> VerifyResult: ...
|
|
|
|
async def hash_report(
|
|
self, sha256: str, org_id: str = "", want_scope: str = ""
|
|
) -> list[Receipt]: ...
|
|
|
|
|
|
def validate_sha256(value: str) -> str:
|
|
digest = value.strip().lower()
|
|
if digest.startswith("sha256:"):
|
|
digest = digest[7:]
|
|
if not _SHA256_RE.match(digest):
|
|
raise ValueError("sha256 must be 64 lowercase hex characters")
|
|
return digest
|
|
|
|
|
|
def _merkle(ids: list[str]) -> str:
|
|
if not ids:
|
|
return "sha256:" + SHA256_EMPTY
|
|
level = [hashlib.sha256(i.encode()).digest() for i in sorted(ids)]
|
|
while len(level) > 1:
|
|
nxt: list[bytes] = []
|
|
for i in range(0, len(level), 2):
|
|
if i + 1 == len(level):
|
|
nxt.append(level[i])
|
|
else:
|
|
nxt.append(hashlib.sha256(level[i] + level[i + 1]).digest())
|
|
level = nxt
|
|
return "sha256:" + level[0].hex()
|
|
|
|
|
|
class MemoryLedger:
|
|
def __init__(self) -> None:
|
|
self._by_id: dict[str, Receipt] = {}
|
|
self._order: list[str] = []
|
|
|
|
def _key(self, sha256: str, org_id: str, scope: str) -> tuple[str, str, str]:
|
|
return (sha256, org_id, scope)
|
|
|
|
async def stamp(
|
|
self,
|
|
sha256: str,
|
|
scope: str = "local",
|
|
cube_id: str = "",
|
|
org_id: str = "",
|
|
) -> list[Receipt]:
|
|
digest = validate_sha256(sha256)
|
|
if scope not in _SCOPE_RANK:
|
|
raise ValueError("scope must be local, shared, or global")
|
|
for rec in self._by_id.values():
|
|
if rec.hash == digest and rec.org_id == org_id and rec.scope == scope:
|
|
return [rec]
|
|
created = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
receipt_id = hashlib.sha256(
|
|
f"{digest}|{created}|{scope}|{org_id}".encode()
|
|
).hexdigest()
|
|
rec = Receipt(
|
|
receipt_id=receipt_id,
|
|
hash=digest,
|
|
scope=scope,
|
|
cube_id=cube_id,
|
|
org_id=org_id,
|
|
created_at=created,
|
|
merkle="",
|
|
)
|
|
rec.merkle = _merkle([*self._order, rec.receipt_id])
|
|
self._by_id[rec.receipt_id] = rec
|
|
self._order.append(rec.receipt_id)
|
|
return [rec]
|
|
|
|
async def verify(self, receipt_id: str, want_scope: str = "") -> VerifyResult:
|
|
rec = self._by_id.get(receipt_id)
|
|
if rec is None:
|
|
return VerifyResult(valid=False, error="unknown receipt")
|
|
if want_scope:
|
|
if _SCOPE_RANK.get(rec.scope, 0) < _SCOPE_RANK.get(want_scope, 99):
|
|
return VerifyResult(
|
|
valid=False,
|
|
error=f"receipt scope {rec.scope} cannot satisfy {want_scope}",
|
|
receipt=rec,
|
|
)
|
|
return VerifyResult(valid=True, receipt=rec)
|
|
|
|
async def hash_report(
|
|
self, sha256: str, org_id: str = "", want_scope: str = ""
|
|
) -> list[Receipt]:
|
|
digest = validate_sha256(sha256)
|
|
out: list[Receipt] = []
|
|
for rid in self._order:
|
|
rec = self._by_id[rid]
|
|
if rec.hash != digest:
|
|
continue
|
|
if org_id and rec.org_id != org_id:
|
|
continue
|
|
if want_scope and _SCOPE_RANK.get(rec.scope, 0) < _SCOPE_RANK.get(
|
|
want_scope, 99
|
|
):
|
|
continue
|
|
out.append(rec)
|
|
return out
|
|
|
|
def remember(self, rec: Receipt) -> None:
|
|
if rec.receipt_id in self._by_id:
|
|
return
|
|
self._by_id[rec.receipt_id] = rec
|
|
self._order.append(rec.receipt_id)
|
|
|
|
|
|
def default_ledger_path() -> Path:
|
|
override = os.environ.get("VERAE_LEDGER_PATH")
|
|
if override:
|
|
return Path(override)
|
|
return Path.home() / ".verae" / "mcp-timestamp-local.json"
|
|
|
|
|
|
class FileLedger(MemoryLedger):
|
|
"""Append-only JSON file. Same hash still idempotent."""
|
|
|
|
def __init__(self, path: Path | str | None = None) -> None:
|
|
super().__init__()
|
|
self.path = Path(path) if path else default_ledger_path()
|
|
self._load()
|
|
|
|
def _load(self) -> None:
|
|
if not self.path.is_file():
|
|
return
|
|
raw = json.loads(self.path.read_text())
|
|
items = raw if isinstance(raw, list) else raw.get("receipts", [])
|
|
for item in items:
|
|
rec = Receipt.model_validate(item)
|
|
self._by_id[rec.receipt_id] = rec
|
|
self._order.append(rec.receipt_id)
|
|
|
|
def _save(self) -> None:
|
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
payload = [self._by_id[rid].model_dump() for rid in self._order]
|
|
self.path.write_text(json.dumps(payload, indent=2) + "\n")
|
|
os.chmod(self.path, 0o600)
|
|
|
|
async def stamp(
|
|
self,
|
|
sha256: str,
|
|
scope: str = "local",
|
|
cube_id: str = "",
|
|
org_id: str = "",
|
|
) -> list[Receipt]:
|
|
before = set(self._by_id)
|
|
receipts = await super().stamp(sha256, scope=scope, cube_id=cube_id, org_id=org_id)
|
|
if receipts[0].receipt_id not in before:
|
|
self._save()
|
|
return receipts
|
|
|
|
def remember(self, rec: Receipt) -> None:
|
|
super().remember(rec)
|
|
self._save()
|
|
|
|
|
|
class RoutingLedger:
|
|
"""Local/shared stay on the leaf ledger; global goes to VeraeTime HTTP."""
|
|
|
|
def __init__(self, local: Ledger, global_ledger: Ledger) -> None:
|
|
self.local = local
|
|
self.global_ledger = global_ledger
|
|
|
|
async def stamp(
|
|
self,
|
|
sha256: str,
|
|
scope: str = "local",
|
|
cube_id: str = "",
|
|
org_id: str = "",
|
|
) -> list[Receipt]:
|
|
if scope == "global":
|
|
return await self.global_ledger.stamp(
|
|
sha256, scope=scope, cube_id=cube_id, org_id=org_id
|
|
)
|
|
return await self.local.stamp(
|
|
sha256, scope=scope, cube_id=cube_id, org_id=org_id
|
|
)
|
|
|
|
async def verify(self, receipt_id: str, want_scope: str = "") -> VerifyResult:
|
|
if want_scope == "global":
|
|
return await self.global_ledger.verify(receipt_id, want_scope=want_scope)
|
|
local_result = await self.local.verify(receipt_id, want_scope=want_scope)
|
|
if local_result.valid:
|
|
return local_result
|
|
return await self.global_ledger.verify(receipt_id, want_scope=want_scope)
|
|
|
|
async def hash_report(
|
|
self, sha256: str, org_id: str = "", want_scope: str = ""
|
|
) -> list[Receipt]:
|
|
return await self.local.hash_report(
|
|
sha256, org_id=org_id, want_scope=want_scope
|
|
)
|