Files
go-sip/scripts/publish-mq-v3.py
T

112 lines
5.7 KiB
Python

#!/usr/bin/env python3
"""Publish the approved JCS digest revision without modifying older bundles."""
import hashlib
import importlib
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import jsonschema
from referencing import Registry, Resource
ROOT = Path(__file__).resolve().parent.parent
OLD_VERSION = "2026-09-21-p1-v2"
VERSION = "2026-09-21-p1-v3"
OLD = ROOT / "contracts/upstream" / OLD_VERSION
NEW = OLD.parent / VERSION
MODULE = "github.com/cyberphone/json-canonicalization"
REVISION = "v0.0.0-20241213102144-19d51d7fe467"
info = json.loads(subprocess.check_output(["go", "mod", "download", "-json", MODULE + "@" + REVISION], cwd=ROOT))
if info.get("Error") or info.get("Version") != REVISION:
raise RuntimeError("pinned JCS implementation unavailable")
sys.path.insert(0, str(Path(info["Dir"]) / "python3/src"))
canonicalize = importlib.import_module("org.webpki.json.Canonicalize").canonicalize
def encoded(value):
return (json.dumps(value, ensure_ascii=False, indent=2) + "\n").encode()
def digest(value):
return hashlib.sha256(canonicalize(value)).hexdigest()
manifest = json.loads((OLD / "manifest.json").read_text())
files = {}
for name, expected in manifest["files"].items():
raw = (OLD / name).read_bytes()
if hashlib.sha256(raw).hexdigest() != expected:
raise RuntimeError("source bundle hash mismatch: " + name)
files[name] = raw.replace(OLD_VERSION.encode(), VERSION.encode())
result = json.loads(files["examples/ai-config-result.json"])
result["payload"]["snapshot"]["content_sha256"] = digest(result["payload"]["snapshot"]["config"])
files["examples/ai-config-result.json"] = encoded(result)
oldest = ROOT / "contracts/upstream/2026-09-19-p1-v1/examples"
for name in ("agent-version.json", "agent-version-asr-only.json", "agent-version-full-explicit.json"):
files["examples/" + name] = (oldest / name).read_bytes()
authorization = json.loads((oldest / "ai-authorization.json").read_text())
authorization["config_sha256"] = digest(json.loads(files["examples/agent-version-asr-only.json"]))
files["examples/ai-authorization.json"] = encoded(authorization)
vectors = [
{"name": "numbers", "input": '{"b":1.0,"a":[-0.0,1e30,4.50,2e-3]}'},
{"name": "utf16-key-order", "input": '{"\\ue000":1,"\\ud800\\udc00":2}'},
{"name": "strings", "input": '{"s":"\\u20ac/\\n","flag":false,"zero":0}'},
]
for vector in vectors:
value = json.loads(vector["input"])
vector["canonical"] = canonicalize(value).decode()
vector["sha256"] = digest(value)
files["jcs-golden.json"] = encoded(vectors)
files["ai-digest.json"] = encoded({
"scope": "AI snapshot.config and authorization config_sha256 only",
"canonicalization": "RFC 8785 (JCS)", "digest": "SHA-256", "encoding": "lowercase hex",
"implementation": MODULE, "revision": REVISION,
"external_acceptance": False,
"invariants": ["No Unicode normalization", "Reject invalid UTF-8 and duplicate object keys",
"Preserve explicit zero/false versus absent fields", "No old-digest fallback"],
})
files["README.md"] = (f"# {VERSION}\n\nProject-local JCS/SHA-256 revision approved by the user. "
f"Derived from {OLD_VERSION}; older packages are unchanged. Wire schema_version remains 2.0.\n\n"
"AI digests use RFC 8785 canonical UTF-8 bytes followed by SHA-256, lowercase hexadecimal. "
"This does not change file checksums, upload facts, command-body identity, or other hashes. "
"Numbers follow JCS IEEE-754 rules; no Unicode normalization is performed. "
"Explicit zero/false remain distinct from absent fields. No legacy digest fallback.\n\n"
"Local agreement and generated fixtures are not external SaaS acceptance.\n").encode()
# Validate positive and negative fixtures against an entirely offline registry
# before the directory becomes an immutable published bundle.
schemas = {name: json.loads(raw) for name, raw in files.items() if name.endswith(".schema.json")}
registry = Registry().with_resources((value["$id"], Resource.from_contents(value)) for value in schemas.values())
for fixture in json.loads(files["fixtures.json"]):
validator = jsonschema.Draft202012Validator(schemas[fixture["schema"]], registry=registry,
format_checker=jsonschema.Draft202012Validator.FORMAT_CHECKER)
valid = validator.is_valid(json.loads(files[fixture["file"]]))
if valid != fixture["valid"]:
raise RuntimeError("fixture validation mismatch: " + fixture["file"])
for name in ("agent-version.json", "agent-version-asr-only.json", "agent-version-full-explicit.json"):
jsonschema.Draft202012Validator(schemas["ai-config.schema.json"], registry=registry).validate(json.loads(files["examples/" + name]))
jsonschema.Draft202012Validator(schemas["ai-authorization.schema.json"], registry=registry).validate(authorization)
files["manifest.json"] = encoded({
"version": VERSION, "source": "project-approved-jcs-sha256", "derived_from": OLD_VERSION,
"source_manifest_sha256": hashlib.sha256((OLD / "manifest.json").read_bytes()).hexdigest(),
"files": {name: hashlib.sha256(raw).hexdigest() for name, raw in sorted(files.items())},
})
if NEW.exists():
existing = {str(p.relative_to(NEW)): p.read_bytes() for p in NEW.rglob("*") if p.is_file()}
if existing != files:
raise RuntimeError("immutable destination differs; publish a new version instead")
else:
with tempfile.TemporaryDirectory(prefix=".jcs-publish-", dir=OLD.parent) as temporary:
stage = Path(temporary) / VERSION
for name, raw in files.items():
target = stage / name
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(raw)
os.rename(stage, NEW)
print(VERSION, hashlib.sha256(files["manifest.json"]).hexdigest())