feat: route scope=global stamp/verify to api.veraetime.net

Evercycle-style local facade: GlobalClient posts /api/timestamp and
/api/verify with Bearer from VERAE_GLOBAL_TOKEN or ~/.verae/auth.properties.
This commit is contained in:
George Lambert 2026-09-20 10:07:41 -04:00
commit 107fa785b0
5 changed files with 341 additions and 7 deletions

View file

@ -4,7 +4,7 @@
Verae MCP timestamping server. Hash-only stamps via leaf locald/NATS.
The MCP process never hashes content. Clients send a SHA-256 hex digest. Tools `stamp`, `verify`, and `hash_report` use an in-memory ledger over stdio, or `NatsLedger` against `verae.ts.request` / `verae.ts.verify` / `verae.hash.report` (leaf `locald`). Plan and product model: [verae-mcp-research](../verae-mcp-research).
The MCP process never hashes content. Clients send a SHA-256 hex digest. `scope=local` writes `~/.verae/mcp-timestamp-local.json`. `scope=global` POSTs to `https://api.veraetime.net/api/timestamp` (same wire as `verae-timestamp/global.Client`). `want_scope=global` verifies against that API as the central source of truth. Bearer token: `VERAE_GLOBAL_TOKEN` or `~/.verae/auth.properties`.
## Install
@ -33,9 +33,15 @@ Tests:
uv run pytest
```
Point a later HTTP deploy at a leaf with `NatsLedger` and `VERAE_NATS_URL=nats://verae-nats-bus:4222`. Do not send file bytes to `stamp`.
Global stamp (central VeraeTime):
Local receipts persist at `~/.verae/mcp-timestamp-local.json` (override with `VERAE_LEDGER_PATH`).
```sh
export VERAE_GLOBAL_URL=https://api.veraetime.net
export VERAE_GLOBAL_TOKEN= # or put access_token= in ~/.verae/auth.properties
# then call tool stamp with scope=global
```
Do not send file bytes to `stamp`. Local receipts persist at `~/.verae/mcp-timestamp-local.json` (`VERAE_LEDGER_PATH`).
## Contributing

View file

@ -0,0 +1,173 @@
"""HTTPS client for api.veraetime.net — same shape as verae-timestamp/global.Client.
Evercycle-style: local process holds a bearer token and posts to the central API.
The global server is the source of truth for scope=global.
"""
from __future__ import annotations
import json
import os
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any, Protocol
from verae_mcp_timestamping_server.ledger import (
Receipt,
VerifyResult,
validate_sha256,
)
DEFAULT_GLOBAL_URL = "https://api.veraetime.net"
class HttpTransport(Protocol):
def request(
self, method: str, url: str, json: dict[str, Any], headers: dict[str, str]
) -> Any: ...
class _UrllibResp:
def __init__(self, status_code: int, body: Any, text: str):
self.status_code = status_code
self._body = body
self.text = text
def json(self) -> Any:
return self._body
class UrllibHttp:
def request(
self, method: str, url: str, json: dict[str, Any], headers: dict[str, str]
) -> _UrllibResp:
data = json_dumps(json).encode()
req = urllib.request.Request(url, data=data, method=method)
for key, value in headers.items():
req.add_header(key, value)
try:
with urllib.request.urlopen(req, timeout=15) as res:
raw = res.read().decode("utf-8", errors="replace")
status = getattr(res, "status", 200)
except urllib.error.HTTPError as exc:
raw = exc.read().decode("utf-8", errors="replace")
status = exc.code
try:
body = json.loads(raw) if raw else {}
except json.JSONDecodeError:
body = {"error": raw}
return _UrllibResp(status, body, raw)
def json_dumps(payload: dict[str, Any]) -> str:
return json.dumps(payload)
def load_auth_token() -> str:
env = os.environ.get("VERAE_GLOBAL_TOKEN", "").strip()
if env:
return env
path = Path(os.environ.get("VERAE_AUTH_PROPERTIES", Path.home() / ".verae" / "auth.properties"))
if not path.is_file():
return ""
props: dict[str, str] = {}
for line in path.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
props[key.strip()] = value.strip()
return props.get("access_token") or props.get("id_token") or props.get("auth_token") or ""
class GlobalClient:
def __init__(
self,
base_url: str = DEFAULT_GLOBAL_URL,
token: str = "",
http: HttpTransport | None = None,
) -> None:
self.base_url = base_url.rstrip("/")
self.token = token
self.http = http or UrllibHttp()
@classmethod
def from_env(cls) -> GlobalClient:
return cls(
base_url=os.environ.get("VERAE_GLOBAL_URL", DEFAULT_GLOBAL_URL),
token=load_auth_token(),
)
def _headers(self) -> dict[str, str]:
headers = {"Content-Type": "application/json"}
if self.token:
headers["Authorization"] = f"Bearer {self.token}"
return headers
def submit(self, digest: str, cube_id: str = "") -> Receipt:
url = f"{self.base_url}/api/timestamp"
res = self.http.request(
"POST", url, {"hash": digest, "sync": True}, self._headers()
)
if res.status_code >= 300:
raise ValueError(f"global timestamp HTTP {res.status_code}: {res.text}")
parsed = res.json()
job_id = parsed.get("jobId") or parsed.get("blockHash") or ""
if not job_id:
raise ValueError("global timestamp response missing jobId")
return Receipt(
receipt_id=str(job_id),
hash=digest,
scope="global",
cube_id=cube_id,
created_at=str(parsed.get("timestamp") or ""),
merkle=str(parsed.get("blockHash") or ""),
)
def verify_certificate(self, receipt_id: str) -> VerifyResult:
url = f"{self.base_url}/api/verify"
res = self.http.request(
"POST", url, {"certificate": receipt_id}, self._headers()
)
if res.status_code >= 300:
return VerifyResult(valid=False, error=f"verify HTTP {res.status_code}")
parsed = res.json()
if not parsed.get("valid"):
return VerifyResult(valid=False, error="not valid")
return VerifyResult(
valid=True,
receipt=Receipt(
receipt_id=receipt_id,
hash=str(parsed.get("hash") or ""),
scope="global",
created_at=str(parsed.get("timestamp") or ""),
merkle=str(parsed.get("blockHash") or parsed.get("blockIndex") or ""),
),
)
class GlobalLedger:
def __init__(self, client: GlobalClient) -> None:
self.client = client
async def stamp(
self,
sha256: str,
scope: str = "global",
cube_id: str = "",
org_id: str = "",
) -> list[Receipt]:
del org_id
digest = validate_sha256(sha256)
return [self.client.submit(digest, cube_id=cube_id)]
async def verify(self, receipt_id: str, want_scope: str = "") -> VerifyResult:
del want_scope
return self.client.verify_certificate(receipt_id)
async def hash_report(
self, sha256: str, org_id: str = "", want_scope: str = ""
) -> list[Receipt]:
del sha256, org_id, want_scope
return []

View file

@ -148,6 +148,12 @@ class MemoryLedger:
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")
@ -191,3 +197,50 @@ class FileLedger(MemoryLedger):
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":
receipts = await self.global_ledger.stamp(
sha256, scope=scope, cube_id=cube_id, org_id=org_id
)
remember = getattr(self.local, "remember", None)
if callable(remember):
for rec in receipts:
remember(rec)
return receipts
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
)

View file

@ -7,11 +7,13 @@ import os
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
from verae_mcp_timestamping_server.global_client import GlobalClient, GlobalLedger
from verae_mcp_timestamping_server.ledger import (
FileLedger,
Ledger,
MemoryLedger,
Receipts,
RoutingLedger,
VerifyResult,
)
@ -19,9 +21,16 @@ mcp = MCPServer("verae-timestamping")
_ledger: Ledger = MemoryLedger()
def default_ledger() -> Ledger:
return RoutingLedger(
local=FileLedger(),
global_ledger=GlobalLedger(GlobalClient.from_env()),
)
def build_mcp(ledger: Ledger | None = None) -> MCPServer:
global _ledger
_ledger = ledger or FileLedger()
_ledger = ledger or default_ledger()
return mcp
@ -33,7 +42,7 @@ def _tool_error(exc: ValueError) -> ToolError:
async def stamp(
sha256: str, scope: str = "local", cube_id: str = "", org_id: str = ""
) -> Receipts:
"""Commit a client-computed SHA-256 to the Verae leaf ledger. Hex only."""
"""Commit a SHA-256. scope=local is the leaf file ledger; scope=global is api.veraetime.net."""
try:
receipts = await _ledger.stamp(
sha256, scope=scope, cube_id=cube_id, org_id=org_id
@ -45,7 +54,7 @@ async def stamp(
@mcp.tool()
async def verify(receipt_id: str, want_scope: str = "") -> VerifyResult:
"""Check a timestamp receipt. want_scope=global rejects a weaker local receipt."""
"""Check a receipt. want_scope=global asks api.veraetime.net (central source of truth)."""
return await _ledger.verify(receipt_id, want_scope=want_scope)
@ -64,7 +73,7 @@ async def hash_report(
def main() -> None:
build_mcp(FileLedger())
build_mcp(default_ledger())
transport = os.environ.get("VERAE_MCP_TRANSPORT", "stdio")
if transport == "streamable-http":
host = os.environ.get("VERAE_MCP_HOST", "127.0.0.1")

View file

@ -0,0 +1,93 @@
"""Global VeraeTime client: same wire shape as verae-timestamp/global.Client."""
import pytest
from verae_mcp_timestamping_server.global_client import GlobalClient, GlobalLedger
from verae_mcp_timestamping_server.ledger import SHA256_EMPTY, MemoryLedger, RoutingLedger
pytestmark = pytest.mark.anyio
class _Resp:
def __init__(self, status_code: int, body: dict):
self.status_code = status_code
self.body = body
self.text = str(body)
def json(self):
return self.body
class FakeHttp:
def __init__(self):
self.calls: list[tuple[str, str, dict, dict]] = []
def request(self, method: str, url: str, json: dict, headers: dict) -> _Resp:
self.calls.append((method, url, json, headers))
if url.endswith("/api/timestamp"):
return _Resp(
200,
{
"jobId": "job-1",
"hash": json["hash"],
"timestamp": "2026-08-15T00:00:00Z",
"blockHash": "0xdead",
},
)
if url.endswith("/api/verify"):
if json.get("certificate") == "job-1":
return _Resp(200, {"valid": True, "hash": SHA256_EMPTY})
return _Resp(200, {"valid": False})
return _Resp(404, {"error": "nope"})
async def test_global_stamp_posts_hash_sync_and_maps_receipt():
http = FakeHttp()
client = GlobalClient("https://api.veraetime.net", token="secret", http=http)
ledger = GlobalLedger(client)
[rec] = await ledger.stamp(SHA256_EMPTY, scope="global", cube_id="cube-1")
method, url, payload, headers = http.calls[0]
assert method == "POST"
assert url == "https://api.veraetime.net/api/timestamp"
assert payload == {"hash": SHA256_EMPTY, "sync": True}
assert headers["Authorization"] == "Bearer secret"
assert rec.scope == "global"
assert rec.receipt_id == "job-1"
assert rec.merkle == "0xdead"
assert rec.cube_id == "cube-1"
async def test_global_verify_posts_certificate():
http = FakeHttp()
ledger = GlobalLedger(GlobalClient("https://api.veraetime.net", http=http))
ok = await ledger.verify("job-1", want_scope="global")
assert ok.valid is True
assert ok.receipt is not None
assert ok.receipt.hash == SHA256_EMPTY
assert ok.receipt.scope == "global"
_, url, payload, _ = http.calls[0]
assert url.endswith("/api/verify")
assert payload == {"certificate": "job-1"}
async def test_routing_sends_only_global_scope_to_central():
http = FakeHttp()
local = MemoryLedger()
routed = RoutingLedger(
local=local,
global_ledger=GlobalLedger(GlobalClient("https://api.veraetime.net", http=http)),
)
[local_rec] = await routed.stamp(SHA256_EMPTY, scope="local")
assert local_rec.scope == "local"
assert http.calls == []
[g] = await routed.stamp(SHA256_EMPTY, scope="global", cube_id="cube-1")
assert g.scope == "global"
assert g.receipt_id == "job-1"
assert len(http.calls) == 1
cached = await routed.hash_report(SHA256_EMPTY, want_scope="global")
assert cached[0].receipt_id == "job-1"
weak = await routed.verify(local_rec.receipt_id, want_scope="global")
assert weak.valid is False