112 lines
5.7 KiB
Python
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())
|