feat(release): add fail-closed RC06 apply protocol
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,7 @@ import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path, PurePath
|
||||
from typing import Any, Iterable, Mapping
|
||||
@@ -28,7 +29,7 @@ from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
||||
|
||||
|
||||
SCHEMA_VERSION = "yuqei.rc06-publication-batch.v3"
|
||||
SCHEMA_VERSION = "yuqei.rc06-publication-batch.v4"
|
||||
DISPOSITION_SCHEMA_VERSION = "yuqei.rc06-golden-dispositions.v2"
|
||||
IMAGE_SCHEMA_VERSION = "yuqei.rc06-image-identities.v1"
|
||||
APPROVAL_SCHEMA_VERSION = "yuqei.rc06-publication-batch-approval.v5"
|
||||
@@ -74,7 +75,7 @@ RC05_CAMPAIGN_SCHEMA_VERSION = "multiscene-call-campaign.v1"
|
||||
BLOCKED_CONTROLS = (
|
||||
"single_reviewer_batch_approval_receipt",
|
||||
"system_generated_item_apply_receipts_and_online_snapshots",
|
||||
"runtime_executable_prompt_snapshots_1_drafting_plus_8_review",
|
||||
"runtime_executable_prompt_snapshots_1_drafting_plus_13_review",
|
||||
)
|
||||
# Only the stable key owner identity is frozen in source. A title, organization,
|
||||
# and authorization scope are human assertions for one exact approval and must
|
||||
@@ -106,6 +107,7 @@ EXPECTED_DRAFTING_MODULE_COUNT = 6
|
||||
EXPECTED_REVIEW_MODULE_COUNT = 8
|
||||
EXPECTED_MODULE_COUNT = EXPECTED_DRAFTING_MODULE_COUNT + EXPECTED_REVIEW_MODULE_COUNT
|
||||
EXPECTED_DEFAULT_PROMPT_COUNT = 46
|
||||
EXPECTED_RUNTIME_PROMPT_COUNT = 14
|
||||
EXPECTED_GOLDEN_COUNT = 31
|
||||
EXPECTED_GOLDEN_GENERATION_COUNT = 23
|
||||
EXPECTED_GOLDEN_CLAUSE_COUNT = 8
|
||||
@@ -149,6 +151,21 @@ DEFAULT_PROMPT_PATH = (
|
||||
"yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/"
|
||||
"prompt_assets/v1_phase15/default-prompt-assets.json"
|
||||
)
|
||||
RUNTIME_EXISTING_DOMAIN_PROMPTS = (
|
||||
"contract_review_channel_franchise",
|
||||
"contract_review_employment_hr",
|
||||
"contract_review_investment_finance",
|
||||
"contract_review_real_estate_property",
|
||||
"contract_review_tech_saas_data_compliance",
|
||||
)
|
||||
DRAFTING_RUNTIME_MODULE_ORDER = (
|
||||
"draft_intake",
|
||||
"template_match",
|
||||
"missing_facts_questions",
|
||||
"clause_assembly_plan",
|
||||
"draft_consistency_check",
|
||||
"candidate_revision_explanation",
|
||||
)
|
||||
GOLDEN_INVENTORY_PATH = (
|
||||
"yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/"
|
||||
"golden_assets/v1_phase13_legal_knowledge_template_library/inventories/"
|
||||
@@ -156,6 +173,8 @@ GOLDEN_INVENTORY_PATH = (
|
||||
)
|
||||
FROZEN_IMPLEMENTATION_PATHS = (
|
||||
"yuqei-infra/scripts/build-rc06-publication-batch.py",
|
||||
"yuqei-infra/scripts/apply-rc06-publication-batch.py",
|
||||
"yuqei-infra/scripts/build-scene-source-legal-review.py",
|
||||
"yuqei-infra/scripts/rc06-single-reviewer-signing.py",
|
||||
"yuqei-infra/scripts/rc06-release-trust-bootstrap.py",
|
||||
"yuqei-contract-platform/services/contract-api/src/yuqei_contract_api/contract_templates.py",
|
||||
@@ -172,6 +191,9 @@ CONTRACT_RUNTIME_PACKAGE_ROOT = (
|
||||
"yuqei-contract-platform/services/contract-api/src/yuqei_contract_api"
|
||||
)
|
||||
SDK_RUNTIME_PACKAGE_ROOT = "yuqei-ai-sdk-python/src/yuqei_sdk"
|
||||
AI_RUNTIME_PACKAGE_ROOT = (
|
||||
"yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api"
|
||||
)
|
||||
|
||||
DISPOSITION_STATUSES = {
|
||||
"named_legal_approval",
|
||||
@@ -493,7 +515,11 @@ def _inventory_source_paths(repo: Path, commit: str | None = None) -> list[str]:
|
||||
raise PublicationBatchError(f"template source must not be a symbolic link: {path}")
|
||||
if path.is_file() and path.suffix.lower() in {".json", ".md"}:
|
||||
paths.append(_relative_source_path(repo, path))
|
||||
for package_root in (CONTRACT_RUNTIME_PACKAGE_ROOT, SDK_RUNTIME_PACKAGE_ROOT):
|
||||
for package_root in (
|
||||
CONTRACT_RUNTIME_PACKAGE_ROOT,
|
||||
SDK_RUNTIME_PACKAGE_ROOT,
|
||||
AI_RUNTIME_PACKAGE_ROOT,
|
||||
):
|
||||
root = repo / package_root
|
||||
_assert_no_symlink_components(root, f"runtime package {package_root}")
|
||||
paths.extend(
|
||||
@@ -513,6 +539,7 @@ def _inventory_source_paths(repo: Path, commit: str | None = None) -> list[str]:
|
||||
ASSET_ROOT,
|
||||
CONTRACT_RUNTIME_PACKAGE_ROOT,
|
||||
SDK_RUNTIME_PACKAGE_ROOT,
|
||||
AI_RUNTIME_PACKAGE_ROOT,
|
||||
)
|
||||
)
|
||||
for relative in listing.splitlines():
|
||||
@@ -523,6 +550,7 @@ def _inventory_source_paths(repo: Path, commit: str | None = None) -> list[str]:
|
||||
if (
|
||||
relative.startswith(f"{CONTRACT_RUNTIME_PACKAGE_ROOT}/")
|
||||
or relative.startswith(f"{SDK_RUNTIME_PACKAGE_ROOT}/")
|
||||
or relative.startswith(f"{AI_RUNTIME_PACKAGE_ROOT}/")
|
||||
):
|
||||
paths.append(relative)
|
||||
paths.extend(FROZEN_IMPLEMENTATION_PATHS)
|
||||
@@ -713,6 +741,130 @@ def _load_inventory_symbols(repo: Path) -> tuple[Any, Any, Any, Any, Any]:
|
||||
pass
|
||||
|
||||
|
||||
def _load_executable_prompt_dependencies(repo: Path) -> list[dict[str, Any]]:
|
||||
"""Render the candidate's actual runtime Prompt content for 14 identities."""
|
||||
|
||||
paths = (
|
||||
repo / "yuqei-ai-platform/services/ai-platform-api/src",
|
||||
repo / "yuqei-ai-sdk-python/src",
|
||||
)
|
||||
inserted: list[str] = []
|
||||
for path in reversed(paths):
|
||||
value = str(path)
|
||||
if value not in sys.path:
|
||||
sys.path.insert(0, value)
|
||||
inserted.append(value)
|
||||
prefixes = ("yuqei_ai_platform_api", "yuqei_sdk")
|
||||
previous = {
|
||||
name: module
|
||||
for name, module in list(sys.modules.items())
|
||||
if any(name == prefix or name.startswith(f"{prefix}.") for prefix in prefixes)
|
||||
}
|
||||
for name in previous:
|
||||
sys.modules.pop(name, None)
|
||||
try:
|
||||
from yuqei_ai_platform_api import prompts as prompt_routes # type: ignore
|
||||
from yuqei_ai_platform_api.contract_drafting import ( # type: ignore
|
||||
DRAFT_PROMPT_CONTENT,
|
||||
DRAFT_PROMPT_NAMES,
|
||||
DRAFT_PROMPT_VERSION,
|
||||
)
|
||||
from yuqei_ai_platform_api.drafting_prompt_modules import ( # type: ignore
|
||||
drafting_module_evidence,
|
||||
)
|
||||
from yuqei_ai_platform_api.packaged_prompt_assets_import import ( # type: ignore
|
||||
DEFAULT_VERSION,
|
||||
load_packaged_prompt_assets,
|
||||
)
|
||||
from yuqei_ai_platform_api.review_scene_prompts import ( # type: ignore
|
||||
review_module_evidence,
|
||||
scene_review_template,
|
||||
)
|
||||
|
||||
drafting_evidence = drafting_module_evidence(DRAFT_PROMPT_CONTENT)
|
||||
results: list[dict[str, Any]] = [
|
||||
{
|
||||
"kind": "composed_drafting",
|
||||
"runtime_key": DRAFT_PROMPT_NAMES[0],
|
||||
"version": DRAFT_PROMPT_VERSION,
|
||||
"content_sha256": _sha256_bytes(DRAFT_PROMPT_CONTENT.encode("utf-8")),
|
||||
"publication_dependency": "six_module_composition",
|
||||
"publication_module_dependencies": drafting_evidence,
|
||||
}
|
||||
]
|
||||
review_routes = sorted(
|
||||
item["key"]
|
||||
for item in json.loads((repo / REVIEW_MODULE_PATH).read_text(encoding="utf-8"))[
|
||||
"modules"
|
||||
]
|
||||
)
|
||||
original_candidates = prompt_routes.contract_review_prompt_candidates
|
||||
try:
|
||||
for route in review_routes:
|
||||
prompt_routes.contract_review_prompt_candidates = (
|
||||
lambda *_args, _route=route, **_kwargs: [_route]
|
||||
)
|
||||
request = types.SimpleNamespace(
|
||||
contract_type="rc06_runtime_snapshot",
|
||||
scene_template_id="rc06-runtime-snapshot",
|
||||
review_profile="scene_evaluation_v4",
|
||||
)
|
||||
template = scene_review_template(request)
|
||||
module_evidence = review_module_evidence(template)
|
||||
if len(module_evidence) != 1 or module_evidence[0]["key"] != route:
|
||||
raise PublicationBatchError(
|
||||
f"runtime review Prompt does not contain its exact module: {route}"
|
||||
)
|
||||
results.append(
|
||||
{
|
||||
"kind": "new_review_module",
|
||||
"runtime_key": route,
|
||||
"version": template.version,
|
||||
"content_sha256": template.content_hash,
|
||||
"publication_dependency": "published_module",
|
||||
"publication_module_dependencies": module_evidence,
|
||||
}
|
||||
)
|
||||
finally:
|
||||
prompt_routes.contract_review_prompt_candidates = original_candidates
|
||||
|
||||
packaged, _, duplicates = load_packaged_prompt_assets(repo / DEFAULT_PROMPT_PATH)
|
||||
if duplicates:
|
||||
raise PublicationBatchError("default Prompt runtime inventory contains duplicates")
|
||||
by_key = {item.prompt_key: item for item in packaged}
|
||||
for key in RUNTIME_EXISTING_DOMAIN_PROMPTS:
|
||||
if key not in by_key:
|
||||
raise PublicationBatchError(f"existing runtime domain Prompt is absent: {key}")
|
||||
results.append(
|
||||
{
|
||||
"kind": "existing_domain_prompt",
|
||||
"runtime_key": key,
|
||||
"version": DEFAULT_VERSION,
|
||||
"content_sha256": _sha256_bytes(by_key[key].content.encode("utf-8")),
|
||||
"publication_dependency": "preexisting_domain_prompt",
|
||||
"publication_module_dependencies": [],
|
||||
}
|
||||
)
|
||||
results.sort(key=lambda item: (item["kind"], item["runtime_key"]))
|
||||
if len(results) != EXPECTED_RUNTIME_PROMPT_COUNT or len(
|
||||
{item["runtime_key"] for item in results}
|
||||
) != EXPECTED_RUNTIME_PROMPT_COUNT:
|
||||
raise PublicationBatchError("runtime Prompt dependency inventory must contain 14 identities")
|
||||
return results
|
||||
except (ImportError, OSError, ValueError, KeyError, TypeError) as exc:
|
||||
raise PublicationBatchError(f"cannot render executable runtime Prompts: {exc}") from exc
|
||||
finally:
|
||||
for name in list(sys.modules):
|
||||
if any(name == prefix or name.startswith(f"{prefix}.") for prefix in prefixes):
|
||||
sys.modules.pop(name, None)
|
||||
sys.modules.update(previous)
|
||||
for value in inserted:
|
||||
try:
|
||||
sys.path.remove(value)
|
||||
except ValueError: # pragma: no cover
|
||||
pass
|
||||
|
||||
|
||||
def _asset_hash_projection(asset: Mapping[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"id": asset["template_id"],
|
||||
@@ -911,6 +1063,7 @@ def load_runtime_inventory(
|
||||
return {
|
||||
"assets": assets,
|
||||
"prompt_modules": modules,
|
||||
"runtime_prompt_dependencies": _load_executable_prompt_dependencies(repo),
|
||||
"default_prompt_boundary": {
|
||||
"included_in_publication_batch": False,
|
||||
"count": len(default_inventory),
|
||||
@@ -2103,6 +2256,12 @@ def assemble_manifest(
|
||||
"items": inventory["prompt_modules"],
|
||||
"inventory_sha256": _sha256_json(inventory["prompt_modules"]),
|
||||
},
|
||||
"runtime_prompt_dependencies": {
|
||||
"publication_item_count": EXPECTED_ASSET_COUNT + EXPECTED_MODULE_COUNT,
|
||||
"count": len(inventory["runtime_prompt_dependencies"]),
|
||||
"items": inventory["runtime_prompt_dependencies"],
|
||||
"inventory_sha256": _sha256_json(inventory["runtime_prompt_dependencies"]),
|
||||
},
|
||||
"default_prompt_asset_boundary": inventory["default_prompt_boundary"],
|
||||
"golden_dispositions": {
|
||||
"count": len(dispositions),
|
||||
@@ -2136,7 +2295,7 @@ def assemble_manifest(
|
||||
"No reviewer approval or apply evidence is embedded in this offline manifest.",
|
||||
"One signed batch approval must bind all 60 assets, 14 Prompt modules, and 31 dispositions.",
|
||||
"The apply service must emit 74 machine-verifiable item results and online hashes.",
|
||||
"One drafting plus eight review executable runtime Prompt snapshots are not implemented and remain blocking.",
|
||||
"One composed drafting plus thirteen review runtime Prompt snapshots are not implemented and remain blocking.",
|
||||
],
|
||||
}
|
||||
_assert_no_secret_material(manifest)
|
||||
@@ -2209,7 +2368,8 @@ def validate_manifest(payload: Any) -> dict[str, Any]:
|
||||
"publication batch",
|
||||
(
|
||||
"schema_version", "candidate", "asset_batch", "prompt_module_batch",
|
||||
"default_prompt_asset_boundary", "golden_dispositions", "publication_control",
|
||||
"runtime_prompt_dependencies", "default_prompt_asset_boundary",
|
||||
"golden_dispositions", "publication_control",
|
||||
"side_effects", "claims_not_made", "manifest_payload_sha256",
|
||||
),
|
||||
)
|
||||
@@ -2336,6 +2496,90 @@ def validate_manifest(payload: Any) -> dict[str, Any]:
|
||||
if _sha256_json(modules["items"]) != modules["inventory_sha256"]:
|
||||
raise PublicationBatchError("Prompt module inventory hash drift")
|
||||
|
||||
runtime = _require_keys(
|
||||
manifest["runtime_prompt_dependencies"],
|
||||
"runtime Prompt dependencies",
|
||||
("publication_item_count", "count", "items", "inventory_sha256"),
|
||||
)
|
||||
if (
|
||||
runtime["publication_item_count"] != EXPECTED_ASSET_COUNT + EXPECTED_MODULE_COUNT
|
||||
or runtime["count"] != EXPECTED_RUNTIME_PROMPT_COUNT
|
||||
or not isinstance(runtime["items"], list)
|
||||
or len(runtime["items"]) != EXPECTED_RUNTIME_PROMPT_COUNT
|
||||
):
|
||||
raise PublicationBatchError(
|
||||
"runtime Prompt dependencies must distinguish 74 publication items from 14 snapshots"
|
||||
)
|
||||
runtime_keys: set[str] = set()
|
||||
runtime_kinds: list[str] = []
|
||||
runtime_keys_by_kind: dict[str, set[str]] = {}
|
||||
publication_modules = {
|
||||
item["key"]: {
|
||||
"key": item["key"],
|
||||
"version": item["version"],
|
||||
"content_sha256": item["content_sha256"],
|
||||
}
|
||||
for item in modules["items"]
|
||||
}
|
||||
for item in runtime["items"]:
|
||||
row = _require_keys(
|
||||
item,
|
||||
"runtime Prompt dependency",
|
||||
(
|
||||
"kind",
|
||||
"runtime_key",
|
||||
"version",
|
||||
"content_sha256",
|
||||
"publication_dependency",
|
||||
"publication_module_dependencies",
|
||||
),
|
||||
)
|
||||
key = _require_string(row["runtime_key"], "runtime Prompt key", max_length=128)
|
||||
_require_string(row["version"], f"runtime Prompt {key} version", max_length=128)
|
||||
_require_sha(row["content_sha256"], f"runtime Prompt {key} content SHA-256")
|
||||
if key in runtime_keys:
|
||||
raise PublicationBatchError("runtime Prompt dependency keys must be unique")
|
||||
runtime_keys.add(key)
|
||||
runtime_kinds.append(row["kind"])
|
||||
runtime_keys_by_kind.setdefault(row["kind"], set()).add(key)
|
||||
expected_dependency = {
|
||||
"composed_drafting": "six_module_composition",
|
||||
"new_review_module": "published_module",
|
||||
"existing_domain_prompt": "preexisting_domain_prompt",
|
||||
}.get(row["kind"])
|
||||
if row["publication_dependency"] != expected_dependency:
|
||||
raise PublicationBatchError(f"runtime Prompt dependency kind drift: {key}")
|
||||
dependency_rows = row["publication_module_dependencies"]
|
||||
if not isinstance(dependency_rows, list):
|
||||
raise PublicationBatchError(
|
||||
f"runtime Prompt module dependencies must be a list: {key}"
|
||||
)
|
||||
if row["kind"] == "composed_drafting":
|
||||
expected_module_keys = list(DRAFTING_RUNTIME_MODULE_ORDER)
|
||||
elif row["kind"] == "new_review_module":
|
||||
expected_module_keys = [key]
|
||||
else:
|
||||
expected_module_keys = []
|
||||
expected_dependency_rows = [
|
||||
publication_modules[module_key] for module_key in expected_module_keys
|
||||
]
|
||||
if dependency_rows != expected_dependency_rows:
|
||||
raise PublicationBatchError(
|
||||
f"runtime Prompt is not bound to exact publication module identities: {key}"
|
||||
)
|
||||
if (
|
||||
runtime_kinds.count("composed_drafting") != 1
|
||||
or runtime_kinds.count("new_review_module") != 8
|
||||
or runtime_kinds.count("existing_domain_prompt") != 5
|
||||
or runtime_keys_by_kind.get("composed_drafting") != {"contract_drafting_v1"}
|
||||
or runtime_keys_by_kind.get("new_review_module") != set(review_routes)
|
||||
or runtime_keys_by_kind.get("existing_domain_prompt")
|
||||
!= set(RUNTIME_EXISTING_DOMAIN_PROMPTS)
|
||||
):
|
||||
raise PublicationBatchError("runtime Prompt dependency 1+8+5 inventory drift")
|
||||
if _sha256_json(runtime["items"]) != runtime["inventory_sha256"]:
|
||||
raise PublicationBatchError("runtime Prompt dependency inventory hash drift")
|
||||
|
||||
boundary = manifest["default_prompt_asset_boundary"]
|
||||
required_boundary = {
|
||||
"included_in_publication_batch", "count", "source_path", "source_raw_sha256",
|
||||
|
||||
@@ -0,0 +1,844 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import copy
|
||||
import importlib.util
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
APPLY_SCRIPT = REPO_ROOT / "yuqei-infra" / "scripts" / "apply-rc06-publication-batch.py"
|
||||
FIXTURE_SCRIPT = REPO_ROOT / "yuqei-infra" / "tests" / "test_rc06_publication_batch.py"
|
||||
|
||||
|
||||
def _load(path: Path, name: str):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
apply = _load(APPLY_SCRIPT, "apply_rc06_publication_batch")
|
||||
fixtures = _load(FIXTURE_SCRIPT, "rc06_publication_batch_test_fixtures")
|
||||
|
||||
|
||||
def _raw_public(key) -> bytes:
|
||||
return key.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
|
||||
|
||||
|
||||
def _sign_envelope(private_key, *, schema: str, id_prefix: str, domain: bytes, payload: dict) -> dict:
|
||||
signature = private_key.sign(domain + apply.batch._canonical(payload))
|
||||
return {
|
||||
"schema_version": schema,
|
||||
"receipt_id": id_prefix
|
||||
+ apply.batch._sha256_bytes(apply.batch._canonical(payload) + signature)[:24],
|
||||
"payload": payload,
|
||||
"signature": base64.b64encode(signature).decode("ascii"),
|
||||
}
|
||||
|
||||
|
||||
class _FakeCanonicalLegalReviewValidator:
|
||||
"""Test double for build-scene-source-legal-review.py; never production evidence."""
|
||||
|
||||
@staticmethod
|
||||
def verify_request(request: dict, _repo: Path) -> dict:
|
||||
projection = {key: value for key, value in request.items() if key != "request_sha256"}
|
||||
if request.get("request_sha256") != apply.batch._sha256_json(projection):
|
||||
raise ValueError("review request embedded hash drift")
|
||||
if len(request.get("occurrences", [])) != 43:
|
||||
raise ValueError("review request must contain 43 occurrences")
|
||||
return request
|
||||
|
||||
@staticmethod
|
||||
def verify_signed_decision(
|
||||
request: dict, decision: dict, _keyring: dict, *, checked_at: str
|
||||
) -> dict:
|
||||
if decision.get("request_sha256") != request["request_sha256"]:
|
||||
raise ValueError("decision request binding drift")
|
||||
now = datetime.fromisoformat(checked_at)
|
||||
expires_at = datetime.fromisoformat(decision["expires_at"])
|
||||
if now > expires_at:
|
||||
raise ValueError("decision expired")
|
||||
expected = {
|
||||
item["occurrence_id"]: item["source_record_sha256"]
|
||||
for item in request["occurrences"]
|
||||
}
|
||||
items = decision.get("items")
|
||||
if not isinstance(items, list) or len(items) != 43:
|
||||
raise ValueError("decision must contain 43 items")
|
||||
for item in items:
|
||||
if item.get("source_record_sha256") != expected.get(item.get("occurrence_id")):
|
||||
raise ValueError("source record hash mismatch")
|
||||
if (
|
||||
item.get("current_effectiveness_verified") is not True
|
||||
or item.get("transaction_applicability_verified") is not True
|
||||
or len(item.get("rationale", "")) < 20
|
||||
or not item.get("authoritative_status_url", "").startswith("https://")
|
||||
):
|
||||
raise ValueError("legal source decision is incomplete")
|
||||
return {
|
||||
"status": "verified_qualified_human_review",
|
||||
"candidate_commit": request["candidate_commit"],
|
||||
"request_sha256": request["request_sha256"],
|
||||
"reviewer_name": decision["reviewer"]["name"],
|
||||
"reviewed_occurrence_count": 43,
|
||||
"all_current_effectiveness_verified": True,
|
||||
"all_transaction_applicability_verified": True,
|
||||
"expires_at": decision["expires_at"],
|
||||
"authorizes_publication_by_itself": False,
|
||||
}
|
||||
|
||||
|
||||
def _material(monkeypatch: pytest.MonkeyPatch, now: datetime) -> dict:
|
||||
inventory = apply.batch.load_runtime_inventory(REPO_ROOT)
|
||||
manifest = fixtures._manifest(inventory, resolved=True)
|
||||
manifest_raw = apply.batch._sha256_bytes(apply.batch._canonical(manifest))
|
||||
root_private = Ed25519PrivateKey.generate()
|
||||
reviewer_private = Ed25519PrivateKey.generate()
|
||||
apply_private = Ed25519PrivateKey.generate()
|
||||
online_private = Ed25519PrivateKey.generate()
|
||||
monkeypatch.setattr(
|
||||
apply.batch,
|
||||
"TRUSTED_APPROVAL_ROOT_PUBLIC_KEY_SHA256",
|
||||
apply.batch._sha256_bytes(_raw_public(root_private.public_key())),
|
||||
)
|
||||
monkeypatch.setattr(apply.batch, "FROZEN_APPROVAL_KEYRING_EPOCH", 1)
|
||||
monkeypatch.setattr(
|
||||
apply.batch,
|
||||
"TRUSTED_APPLY_SERVICE_PUBLIC_KEY_SHA256",
|
||||
apply.batch._sha256_bytes(_raw_public(apply_private.public_key())),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
apply.batch,
|
||||
"TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SHA256",
|
||||
apply.batch._sha256_bytes(_raw_public(online_private.public_key())),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
apply.batch,
|
||||
"TRUSTED_ONLINE_APPLY_ENVIRONMENT_IDS",
|
||||
("rc06-test",),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
apply,
|
||||
"_load_scene_legal_review_module",
|
||||
lambda: _FakeCanonicalLegalReviewValidator,
|
||||
)
|
||||
reviewer_pem = reviewer_private.public_key().public_bytes(
|
||||
serialization.Encoding.PEM,
|
||||
serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||
).decode("ascii")
|
||||
keyring_unsigned = {
|
||||
"schema_version": apply.batch.APPROVAL_KEYRING_SCHEMA_VERSION,
|
||||
"root_key_id": apply.batch.TRUSTED_APPROVAL_ROOT_KEY_ID,
|
||||
"epoch": 1,
|
||||
"issued_at": (now - timedelta(days=2)).isoformat(timespec="seconds"),
|
||||
"expires_at": (now + timedelta(days=3)).isoformat(timespec="seconds"),
|
||||
"authorized_batch_manifest_raw_sha256": manifest_raw,
|
||||
"keys": [
|
||||
{
|
||||
"key_id": "rc06-release-owner-test",
|
||||
"algorithm": "Ed25519",
|
||||
"public_key_pem": reviewer_pem,
|
||||
**apply.batch.SOLE_RELEASE_REVIEWER,
|
||||
"not_before": (now - timedelta(days=2)).isoformat(timespec="seconds"),
|
||||
"not_after": (now + timedelta(days=3)).isoformat(timespec="seconds"),
|
||||
"revoked": False,
|
||||
}
|
||||
],
|
||||
}
|
||||
keyring = {
|
||||
**keyring_unsigned,
|
||||
"keyring_payload_sha256": apply.batch._sha256_json(keyring_unsigned),
|
||||
"root_signature_base64": base64.b64encode(
|
||||
root_private.sign(
|
||||
apply.batch.APPROVAL_KEYRING_SIGNATURE_DOMAIN
|
||||
+ apply.batch._canonical(keyring_unsigned)
|
||||
)
|
||||
).decode("ascii"),
|
||||
}
|
||||
approval_payload = apply.batch.build_approval_payload(
|
||||
manifest,
|
||||
manifest_raw_sha256=manifest_raw,
|
||||
key_id="rc06-release-owner-test",
|
||||
approval_keyring_epoch=1,
|
||||
approval_keyring_payload_sha256=keyring["keyring_payload_sha256"],
|
||||
approved_at=now - timedelta(hours=2),
|
||||
expires_at=now + timedelta(days=2),
|
||||
approver={
|
||||
**apply.batch.SOLE_RELEASE_REVIEWER,
|
||||
"role": "测试发布负责人",
|
||||
"organization": "测试组织(非真实法律服务主体)",
|
||||
"authorization_scope": "仅批准本临时 SQLite/HTTP 测试候选,不授权生产发布",
|
||||
"human_confirmed": True,
|
||||
},
|
||||
)
|
||||
approval_signature = reviewer_private.sign(
|
||||
apply.batch.APPROVAL_SIGNATURE_DOMAIN + apply.batch._canonical(approval_payload)
|
||||
)
|
||||
approval = {
|
||||
"schema_version": apply.batch.APPROVAL_SCHEMA_VERSION,
|
||||
"approval_id": "rc06-batch-approval-"
|
||||
+ apply.batch._sha256_bytes(
|
||||
apply.batch._canonical(approval_payload) + approval_signature
|
||||
)[:24],
|
||||
"payload": approval_payload,
|
||||
"signature": base64.b64encode(approval_signature).decode("ascii"),
|
||||
}
|
||||
approval_raw = apply.batch._sha256_bytes(apply.batch._canonical(approval))
|
||||
occurrences = []
|
||||
for asset in sorted(
|
||||
manifest["asset_batch"]["items"], key=lambda item: item["template_id"]
|
||||
):
|
||||
references = ((asset.get("input_schema") or {}).get("scene") or {}).get(
|
||||
"source_references", []
|
||||
)
|
||||
for index, source in enumerate(references):
|
||||
occurrences.append(
|
||||
{
|
||||
"occurrence_id": f"fixture:{asset['template_id']}:{index}",
|
||||
"scene_id": f"fixture-scene-{len(occurrences):02d}",
|
||||
"template_id": asset["template_id"],
|
||||
"source_key": f"source-{index}",
|
||||
"source_pack_path": "fixture-only/not-production.json",
|
||||
"source_record_sha256": apply.batch._sha256_json(source),
|
||||
"source": source,
|
||||
"required_human_decisions": [
|
||||
"current_effectiveness_verified",
|
||||
"transaction_applicability_verified",
|
||||
],
|
||||
}
|
||||
)
|
||||
assert len(occurrences) == 43
|
||||
legal_request = {
|
||||
"schema_version": "yuqei.scene-source-legal-review-request.v1",
|
||||
"status": "pending_qualified_human_review",
|
||||
"candidate_commit": manifest["candidate"]["commit"],
|
||||
"created_at": (now - timedelta(hours=2)).isoformat(timespec="seconds"),
|
||||
"scope": {
|
||||
"scene_count": 30,
|
||||
"reference_occurrence_count": 43,
|
||||
"distinct_source_record_count": 8,
|
||||
"distinct_url_count": 7,
|
||||
},
|
||||
"source_files": [],
|
||||
"occurrences": occurrences,
|
||||
"guardrails": {
|
||||
"not_legal_approval": True,
|
||||
"authorizes_publication": False,
|
||||
"ai_must_not_supply_human_decisions": True,
|
||||
"all_occurrences_require_qualified_human_review": True,
|
||||
},
|
||||
}
|
||||
legal_request["request_sha256"] = apply.batch._sha256_json(legal_request)
|
||||
legal_decision = {
|
||||
"schema_version": "yuqei.scene-source-legal-review-decision.v1",
|
||||
"request_sha256": legal_request["request_sha256"],
|
||||
"candidate_commit": manifest["candidate"]["commit"],
|
||||
"reviewed_at": (now - timedelta(hours=1)).isoformat(timespec="seconds"),
|
||||
"expires_at": (now + timedelta(days=1)).isoformat(timespec="seconds"),
|
||||
"reviewer": {
|
||||
"name": "测试法源复核人(非真实身份)",
|
||||
"organization": "测试组织(非真实法律服务主体)",
|
||||
"qualification": "测试资格声明(不代表真实法律资质)",
|
||||
"credential_reference": "TEST-FIXTURE-NOT-A-CREDENTIAL",
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"occurrence_id": item["occurrence_id"],
|
||||
"source_record_sha256": item["source_record_sha256"],
|
||||
"current_effectiveness_verified": True,
|
||||
"transaction_applicability_verified": True,
|
||||
"checked_at": (now - timedelta(hours=1)).isoformat(timespec="seconds"),
|
||||
"authoritative_status_url": "https://example.gov.cn/test-fixture-status",
|
||||
"rationale": "仅用于验证 fail-closed 协议的合成判断,不构成真实法源复核结论。",
|
||||
}
|
||||
for item in occurrences
|
||||
],
|
||||
"signature": {
|
||||
"algorithm": "Ed25519",
|
||||
"key_id": "fixture-legal-reviewer",
|
||||
"value_base64": base64.b64encode(b"fixture-signature").decode("ascii"),
|
||||
},
|
||||
}
|
||||
legal_keyring = {
|
||||
"schema_version": "yuqei.scene-source-legal-review-keyring.v1",
|
||||
"keys": [{"key_id": "fixture-legal-reviewer", "status": "trusted"}],
|
||||
}
|
||||
return {
|
||||
"now": now,
|
||||
"manifest": manifest,
|
||||
"manifest_raw": manifest_raw,
|
||||
"approval": approval,
|
||||
"approval_raw": approval_raw,
|
||||
"keyring": keyring,
|
||||
"legal_request": legal_request,
|
||||
"legal_decision": legal_decision,
|
||||
"legal_keyring": legal_keyring,
|
||||
"root_private": root_private,
|
||||
"apply_private": apply_private,
|
||||
"online_private": online_private,
|
||||
}
|
||||
|
||||
|
||||
class FakePublicationService:
|
||||
def __init__(
|
||||
self,
|
||||
database: Path,
|
||||
material: dict,
|
||||
*,
|
||||
fail_after: int | None = None,
|
||||
tamper_status: bool = False,
|
||||
tamper_runtime_status: bool = False,
|
||||
) -> None:
|
||||
self.database = database
|
||||
self.material = material
|
||||
self.fail_after = fail_after
|
||||
self.tamper_status = tamper_status
|
||||
self.tamper_runtime_status = tamper_runtime_status
|
||||
self.apply_calls = 0
|
||||
with sqlite3.connect(database) as connection:
|
||||
connection.executescript(
|
||||
"""
|
||||
CREATE TABLE transactions (
|
||||
transaction_id TEXT PRIMARY KEY,
|
||||
idempotency_key TEXT NOT NULL UNIQUE,
|
||||
state TEXT NOT NULL,
|
||||
legal_review_sha256 TEXT NOT NULL,
|
||||
response_json TEXT
|
||||
);
|
||||
CREATE TABLE publication_items (
|
||||
transaction_id TEXT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
item_id TEXT NOT NULL,
|
||||
content_sha256 TEXT NOT NULL,
|
||||
PRIMARY KEY(transaction_id, kind, item_id)
|
||||
);
|
||||
CREATE TABLE runtime_snapshots (
|
||||
transaction_id TEXT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
runtime_key TEXT NOT NULL,
|
||||
content_sha256 TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
PRIMARY KEY(transaction_id, runtime_key)
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
def _receipt_pair(self, request: dict) -> tuple[dict, dict]:
|
||||
material = self.material
|
||||
now = material["now"]
|
||||
challenge_payload = {
|
||||
"schema_version": apply.batch.APPLY_CHALLENGE_PAYLOAD_SCHEMA_VERSION,
|
||||
"nonce_base64": base64.b64encode(b"q" * 32).decode("ascii"),
|
||||
"candidate_commit": request["manifest"]["candidate"]["commit"],
|
||||
"candidate_tag": request["manifest"]["candidate"]["tag"],
|
||||
"batch_manifest_raw_sha256": request["batch_manifest_raw_sha256"],
|
||||
"batch_manifest_payload_sha256": request["batch_manifest_payload_sha256"],
|
||||
"approval_receipt_sha256": request["approval_receipt_sha256"],
|
||||
"operation": apply.batch.APPLY_OPERATION,
|
||||
"environment_id": request["environment_id"],
|
||||
"actor": {
|
||||
"type": "system",
|
||||
"component": apply.batch.TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT,
|
||||
},
|
||||
"issued_at": (now - timedelta(minutes=1)).isoformat(timespec="seconds"),
|
||||
"expires_at": (now + timedelta(minutes=5)).isoformat(timespec="seconds"),
|
||||
}
|
||||
challenge = _sign_envelope(
|
||||
material["apply_private"],
|
||||
schema=apply.batch.APPLY_CHALLENGE_SCHEMA_VERSION,
|
||||
id_prefix="rc06-apply-challenge-",
|
||||
domain=apply.batch.APPLY_CHALLENGE_SIGNATURE_DOMAIN,
|
||||
payload=challenge_payload,
|
||||
)
|
||||
challenge["challenge_id"] = challenge.pop("receipt_id")
|
||||
committed_at = now.isoformat(timespec="seconds")
|
||||
online_payload = {
|
||||
"schema_version": apply.batch.ONLINE_TRANSACTION_PAYLOAD_SCHEMA_VERSION,
|
||||
"key_id": apply.batch.TRUSTED_ONLINE_APPLY_SERVICE_KEY_ID,
|
||||
"batch_manifest_payload_sha256": request["batch_manifest_payload_sha256"],
|
||||
"approval_receipt_sha256": request["approval_receipt_sha256"],
|
||||
"environment_id": request["environment_id"],
|
||||
"transaction_id": request["transaction_id"],
|
||||
"database_commit_id": "sqlite-" + request["idempotency_key"][:24],
|
||||
"committed_at": committed_at,
|
||||
"authority_challenge": challenge,
|
||||
"generated_by": {
|
||||
"type": "system",
|
||||
"component": apply.batch.TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT,
|
||||
},
|
||||
"item_results": request["expected_publication_item_results"],
|
||||
"write_count": 74,
|
||||
"status": "committed_and_verified",
|
||||
}
|
||||
online_receipt = _sign_envelope(
|
||||
material["online_private"],
|
||||
schema=apply.batch.ONLINE_TRANSACTION_RECEIPT_SCHEMA_VERSION,
|
||||
id_prefix="rc06-online-transaction-",
|
||||
domain=apply.batch.ONLINE_TRANSACTION_SIGNATURE_DOMAIN,
|
||||
payload=online_payload,
|
||||
)
|
||||
apply_payload = {
|
||||
"schema_version": apply.batch.APPLY_PAYLOAD_SCHEMA_VERSION,
|
||||
"batch_manifest_payload_sha256": request["batch_manifest_payload_sha256"],
|
||||
"approval_receipt_sha256": request["approval_receipt_sha256"],
|
||||
"environment_id": request["environment_id"],
|
||||
"applied_at": committed_at,
|
||||
"generated_by": {"type": "system", "component": "rc06-apply-authority"},
|
||||
"item_results": request["expected_publication_item_results"],
|
||||
"write_count": 74,
|
||||
"status": "applied_and_verified",
|
||||
"online_transaction_receipt": online_receipt,
|
||||
}
|
||||
apply_receipt = _sign_envelope(
|
||||
material["apply_private"],
|
||||
schema=apply.batch.APPLY_SCHEMA_VERSION,
|
||||
id_prefix="rc06-apply-receipt-",
|
||||
domain=apply.batch.APPLY_SIGNATURE_DOMAIN,
|
||||
payload=apply_payload,
|
||||
)
|
||||
runtime_payload = {
|
||||
"schema_version": apply.RUNTIME_PAYLOAD_SCHEMA_VERSION,
|
||||
"key_id": apply.batch.TRUSTED_ONLINE_APPLY_SERVICE_KEY_ID,
|
||||
"batch_manifest_raw_sha256": request["batch_manifest_raw_sha256"],
|
||||
"batch_manifest_payload_sha256": request["batch_manifest_payload_sha256"],
|
||||
"approval_receipt_sha256": request["approval_receipt_sha256"],
|
||||
"legal_source_review_binding_sha256": request[
|
||||
"legal_source_review_binding_sha256"
|
||||
],
|
||||
"environment_id": request["environment_id"],
|
||||
"transaction_id": request["transaction_id"],
|
||||
"captured_at": committed_at,
|
||||
"generated_by": {
|
||||
"type": "system",
|
||||
"component": apply.batch.TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT,
|
||||
},
|
||||
"transaction_mode": "single_service_atomic",
|
||||
"publication_item_count": 74,
|
||||
"runtime_snapshot_dependencies": request[
|
||||
"expected_runtime_snapshot_dependencies"
|
||||
],
|
||||
"runtime_snapshot_dependency_count": 14,
|
||||
"status": "runtime_snapshots_verified",
|
||||
}
|
||||
runtime_receipt = _sign_envelope(
|
||||
material["online_private"],
|
||||
schema=apply.RUNTIME_RECEIPT_SCHEMA_VERSION,
|
||||
id_prefix="rc06-runtime-snapshot-",
|
||||
domain=apply.RUNTIME_SIGNATURE_DOMAIN,
|
||||
payload=runtime_payload,
|
||||
)
|
||||
return apply_receipt, runtime_receipt
|
||||
|
||||
def apply(self, request: dict, request_sha256: str) -> tuple[int, dict]:
|
||||
self.apply_calls += 1
|
||||
with sqlite3.connect(self.database) as connection:
|
||||
existing = connection.execute(
|
||||
"SELECT response_json FROM transactions WHERE idempotency_key=? AND state='committed_and_verified'",
|
||||
(request["idempotency_key"],),
|
||||
).fetchone()
|
||||
if existing is not None:
|
||||
return 200, json.loads(existing[0])
|
||||
try:
|
||||
connection.execute("BEGIN IMMEDIATE")
|
||||
connection.execute(
|
||||
"INSERT OR REPLACE INTO transactions VALUES (?, ?, 'preparing', ?, NULL)",
|
||||
(
|
||||
request["transaction_id"],
|
||||
request["idempotency_key"],
|
||||
request["legal_source_review_binding_sha256"],
|
||||
),
|
||||
)
|
||||
for index, item in enumerate(request["expected_publication_item_results"]):
|
||||
connection.execute(
|
||||
"INSERT INTO publication_items VALUES (?, ?, ?, ?)",
|
||||
(
|
||||
request["transaction_id"],
|
||||
item["kind"],
|
||||
item["item_id"],
|
||||
item["online_content_sha256"],
|
||||
),
|
||||
)
|
||||
if self.fail_after is not None and index + 1 == self.fail_after:
|
||||
raise RuntimeError("injected transaction failure")
|
||||
for item in request["expected_runtime_snapshot_dependencies"]:
|
||||
connection.execute(
|
||||
"INSERT INTO runtime_snapshots VALUES (?, ?, ?, ?, ?)",
|
||||
(
|
||||
request["transaction_id"],
|
||||
item["kind"],
|
||||
item["runtime_key"],
|
||||
item["online_content_sha256"],
|
||||
item["status"],
|
||||
),
|
||||
)
|
||||
apply_receipt, runtime_receipt = self._receipt_pair(request)
|
||||
response = {
|
||||
"schema_version": apply.APPLY_RESPONSE_SCHEMA_VERSION,
|
||||
"idempotency_key": request["idempotency_key"],
|
||||
"request_sha256": request_sha256,
|
||||
"legal_source_review_binding_sha256": request[
|
||||
"legal_source_review_binding_sha256"
|
||||
],
|
||||
"transaction_mode": "single_service_atomic",
|
||||
"publication_item_count": 74,
|
||||
"runtime_snapshot_dependency_count": 14,
|
||||
"apply_receipt": apply_receipt,
|
||||
"runtime_snapshot_receipt": runtime_receipt,
|
||||
}
|
||||
connection.execute(
|
||||
"UPDATE transactions SET state='committed_and_verified', response_json=? WHERE transaction_id=?",
|
||||
(json.dumps(response, ensure_ascii=False), request["transaction_id"]),
|
||||
)
|
||||
connection.commit()
|
||||
return 200, response
|
||||
except RuntimeError:
|
||||
connection.rollback()
|
||||
return 409, {"error": "injected transaction failure; SQLite rolled back"}
|
||||
|
||||
def status(self, transaction_id: str) -> tuple[int, dict]:
|
||||
with sqlite3.connect(self.database) as connection:
|
||||
transaction = connection.execute(
|
||||
"SELECT idempotency_key, state, legal_review_sha256, response_json FROM transactions WHERE transaction_id=?",
|
||||
(transaction_id,),
|
||||
).fetchone()
|
||||
if transaction is None or transaction[1] == "rolled_back":
|
||||
return 404, {"error": "transaction is not committed"}
|
||||
response = json.loads(transaction[3])
|
||||
publication = response["apply_receipt"]["payload"]["item_results"]
|
||||
runtime = response["runtime_snapshot_receipt"]["payload"][
|
||||
"runtime_snapshot_dependencies"
|
||||
]
|
||||
if self.tamper_status:
|
||||
publication = copy.deepcopy(publication)
|
||||
publication[0]["online_content_sha256"] = "f" * 64
|
||||
if self.tamper_runtime_status:
|
||||
runtime = copy.deepcopy(runtime)
|
||||
runtime[0]["online_content_sha256"] = "f" * 64
|
||||
return 200, {
|
||||
"schema_version": apply.APPLY_STATUS_SCHEMA_VERSION,
|
||||
"idempotency_key": transaction[0],
|
||||
"transaction_id": transaction_id,
|
||||
"state": "committed_and_verified",
|
||||
"legal_source_review_binding_sha256": transaction[2],
|
||||
"publication_item_results": publication,
|
||||
"publication_item_count": len(publication),
|
||||
"runtime_snapshot_dependencies": runtime,
|
||||
"runtime_snapshot_dependency_count": len(runtime),
|
||||
"apply_receipt_sha256": apply.batch._sha256_json(response["apply_receipt"]),
|
||||
"runtime_snapshot_receipt_sha256": apply.batch._sha256_json(
|
||||
response["runtime_snapshot_receipt"]
|
||||
),
|
||||
}
|
||||
|
||||
def rollback(self, transaction_id: str, request: dict) -> tuple[int, dict]:
|
||||
with sqlite3.connect(self.database) as connection:
|
||||
connection.execute("BEGIN IMMEDIATE")
|
||||
connection.execute(
|
||||
"DELETE FROM publication_items WHERE transaction_id=?", (transaction_id,)
|
||||
)
|
||||
connection.execute(
|
||||
"DELETE FROM runtime_snapshots WHERE transaction_id=?", (transaction_id,)
|
||||
)
|
||||
connection.execute(
|
||||
"INSERT OR REPLACE INTO transactions "
|
||||
"(transaction_id,idempotency_key,state,legal_review_sha256,response_json) "
|
||||
"VALUES (?,?,'rolled_back',?,NULL)",
|
||||
(
|
||||
transaction_id,
|
||||
request["idempotency_key"],
|
||||
request["legal_source_review_binding_sha256"],
|
||||
),
|
||||
)
|
||||
connection.commit()
|
||||
evidence = {
|
||||
"transaction_id": transaction_id,
|
||||
"idempotency_key": request["idempotency_key"],
|
||||
"restored_item_count": 0,
|
||||
}
|
||||
return 200, {
|
||||
"schema_version": apply.ROLLBACK_RESPONSE_SCHEMA_VERSION,
|
||||
"idempotency_key": request["idempotency_key"],
|
||||
"transaction_id": transaction_id,
|
||||
"state": "rolled_back",
|
||||
"restored_item_count": 0,
|
||||
"rollback_evidence_sha256": apply.batch._sha256_json(evidence),
|
||||
}
|
||||
|
||||
def counts(self) -> tuple[int, int]:
|
||||
with sqlite3.connect(self.database) as connection:
|
||||
return (
|
||||
connection.execute("SELECT count(*) FROM publication_items").fetchone()[0],
|
||||
connection.execute("SELECT count(*) FROM runtime_snapshots").fetchone()[0],
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _serve(service: FakePublicationService):
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, _format, *_args):
|
||||
return
|
||||
|
||||
def _send(self, status: int, payload: dict) -> None:
|
||||
raw = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(raw)))
|
||||
self.end_headers()
|
||||
self.wfile.write(raw)
|
||||
|
||||
def _authorized(self) -> bool:
|
||||
if self.headers.get("Authorization") != "Bearer test-token":
|
||||
self._send(401, {"error": "unauthorized"})
|
||||
return False
|
||||
return True
|
||||
|
||||
def _body(self) -> dict:
|
||||
return json.loads(self.rfile.read(int(self.headers["Content-Length"])))
|
||||
|
||||
def do_POST(self):
|
||||
if not self._authorized():
|
||||
return
|
||||
if self.path == "/internal/rc06/publication-transactions/apply":
|
||||
request = self._body()
|
||||
if (
|
||||
self.headers.get("Idempotency-Key") != request["idempotency_key"]
|
||||
or self.headers.get("X-RC06-Request-SHA256")
|
||||
!= apply.batch._sha256_json(request)
|
||||
):
|
||||
self._send(400, {"error": "identity header mismatch"})
|
||||
return
|
||||
status, payload = service.apply(
|
||||
request, self.headers["X-RC06-Request-SHA256"]
|
||||
)
|
||||
self._send(status, payload)
|
||||
return
|
||||
if self.path.endswith("/rollback"):
|
||||
request = self._body()
|
||||
transaction_id = self.path.split("/")[-2]
|
||||
status, payload = service.rollback(transaction_id, request)
|
||||
self._send(status, payload)
|
||||
return
|
||||
self._send(404, {"error": "not found"})
|
||||
|
||||
def do_GET(self):
|
||||
if not self._authorized():
|
||||
return
|
||||
if self.path.endswith("/status"):
|
||||
transaction_id = self.path.split("/")[-2]
|
||||
status, payload = service.status(transaction_id)
|
||||
self._send(status, payload)
|
||||
return
|
||||
self._send(404, {"error": "not found"})
|
||||
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
yield f"http://127.0.0.1:{server.server_port}"
|
||||
finally:
|
||||
server.shutdown()
|
||||
thread.join(timeout=5)
|
||||
server.server_close()
|
||||
|
||||
|
||||
def _execute(material: dict, client, ledger) -> dict:
|
||||
return apply.apply_publication_batch(
|
||||
manifest=material["manifest"],
|
||||
manifest_raw_sha256=material["manifest_raw"],
|
||||
approval=material["approval"],
|
||||
approval_raw_sha256=material["approval_raw"],
|
||||
approval_keyring=material["keyring"],
|
||||
approval_root_public_key=material["root_private"].public_key(),
|
||||
apply_service_public_key=material["apply_private"].public_key(),
|
||||
online_service_public_key=material["online_private"].public_key(),
|
||||
legal_review_request=material["legal_request"],
|
||||
legal_review_decision=material["legal_decision"],
|
||||
legal_review_keyring=material["legal_keyring"],
|
||||
environment_id="rc06-test",
|
||||
client=client,
|
||||
ledger=ledger,
|
||||
now=material["now"],
|
||||
)
|
||||
|
||||
|
||||
def test_authenticated_apply_is_atomic_hash_verified_and_idempotent(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
material = _material(monkeypatch, datetime(2026, 10, 6, 8, 0, tzinfo=UTC))
|
||||
runtime = apply.expected_runtime_snapshot_results(material["manifest"])
|
||||
assert len(runtime) == 14
|
||||
drafting = next(item for item in runtime if item["kind"] == "composed_drafting")
|
||||
assert drafting["runtime_key"] == "contract_drafting_v1"
|
||||
assert drafting["version"] == "tp06-23-20260909-1"
|
||||
assert sum(item["kind"] == "new_review_module" for item in runtime) == 8
|
||||
assert sum(item["kind"] == "existing_domain_prompt" for item in runtime) == 5
|
||||
service = FakePublicationService(tmp_path / "remote.sqlite3", material)
|
||||
with _serve(service) as url:
|
||||
client = apply.JsonHttpClient(
|
||||
url, "test-token", allow_http_loopback=True
|
||||
)
|
||||
ledger = apply.ApplyLedger(tmp_path / "ledger.sqlite3")
|
||||
try:
|
||||
evidence = _execute(material, client, ledger)
|
||||
replay = _execute(material, client, ledger)
|
||||
finally:
|
||||
ledger.close()
|
||||
assert replay == evidence
|
||||
assert evidence["publication_item_count"] == 74
|
||||
assert evidence["runtime_snapshot_dependency_count"] == 14
|
||||
assert service.counts() == (74, 14)
|
||||
assert service.apply_calls == 1
|
||||
|
||||
second_ledger = apply.ApplyLedger(tmp_path / "second-ledger.sqlite3")
|
||||
try:
|
||||
assert _execute(material, client, second_ledger) == evidence
|
||||
finally:
|
||||
second_ledger.close()
|
||||
assert service.counts() == (74, 14)
|
||||
assert service.apply_calls == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["missing", "expired", "hash_mismatch"])
|
||||
def test_legal_source_review_is_fail_closed_before_http(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
mode: str,
|
||||
) -> None:
|
||||
material = _material(monkeypatch, datetime(2026, 10, 6, 8, 0, tzinfo=UTC))
|
||||
if mode == "missing":
|
||||
material["legal_request"] = {}
|
||||
elif mode == "expired":
|
||||
material["legal_decision"]["reviewed_at"] = "2026-10-04T08:00:00+00:00"
|
||||
material["legal_decision"]["expires_at"] = "2026-10-05T08:00:00+00:00"
|
||||
else:
|
||||
material["legal_decision"]["items"][0]["source_record_sha256"] = "f" * 64
|
||||
service = FakePublicationService(tmp_path / "remote.sqlite3", material)
|
||||
with _serve(service) as url:
|
||||
client = apply.JsonHttpClient(url, "test-token", allow_http_loopback=True)
|
||||
ledger = apply.ApplyLedger(tmp_path / "ledger.sqlite3")
|
||||
try:
|
||||
with pytest.raises(apply.ApplyPublicationError, match="scene legal review"):
|
||||
_execute(material, client, ledger)
|
||||
finally:
|
||||
ledger.close()
|
||||
assert service.apply_calls == 0
|
||||
assert service.counts() == (0, 0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field", ["version", "content_sha256"])
|
||||
def test_forged_executable_runtime_identity_is_rejected_before_http(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
field: str,
|
||||
) -> None:
|
||||
material = _material(monkeypatch, datetime(2026, 10, 6, 8, 0, tzinfo=UTC))
|
||||
manifest = material["manifest"]
|
||||
drafting = next(
|
||||
item
|
||||
for item in manifest["runtime_prompt_dependencies"]["items"]
|
||||
if item["kind"] == "composed_drafting"
|
||||
)
|
||||
drafting[field] = "forged-version" if field == "version" else "f" * 64
|
||||
manifest["runtime_prompt_dependencies"]["inventory_sha256"] = apply.batch._sha256_json(
|
||||
manifest["runtime_prompt_dependencies"]["items"]
|
||||
)
|
||||
manifest["manifest_payload_sha256"] = apply.batch._sha256_json(
|
||||
{key: value for key, value in manifest.items() if key != "manifest_payload_sha256"}
|
||||
)
|
||||
material["manifest_raw"] = apply.batch._sha256_bytes(apply.batch._canonical(manifest))
|
||||
service = FakePublicationService(tmp_path / "remote.sqlite3", material)
|
||||
with _serve(service) as url:
|
||||
client = apply.JsonHttpClient(url, "test-token", allow_http_loopback=True)
|
||||
ledger = apply.ApplyLedger(tmp_path / "ledger.sqlite3")
|
||||
try:
|
||||
with pytest.raises(apply.ApplyPublicationError, match="candidate executable source"):
|
||||
_execute(material, client, ledger)
|
||||
finally:
|
||||
ledger.close()
|
||||
assert service.apply_calls == 0
|
||||
assert service.counts() == (0, 0)
|
||||
|
||||
|
||||
def test_runtime_snapshot_module_hash_must_match_publication_item(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
material = _material(monkeypatch, datetime(2026, 10, 6, 8, 0, tzinfo=UTC))
|
||||
manifest = material["manifest"]
|
||||
drafting = next(
|
||||
item
|
||||
for item in manifest["runtime_prompt_dependencies"]["items"]
|
||||
if item["kind"] == "composed_drafting"
|
||||
)
|
||||
drafting["publication_module_dependencies"][0]["content_sha256"] = "f" * 64
|
||||
manifest["runtime_prompt_dependencies"]["inventory_sha256"] = apply.batch._sha256_json(
|
||||
manifest["runtime_prompt_dependencies"]["items"]
|
||||
)
|
||||
manifest["manifest_payload_sha256"] = apply.batch._sha256_json(
|
||||
{key: value for key, value in manifest.items() if key != "manifest_payload_sha256"}
|
||||
)
|
||||
material["manifest_raw"] = apply.batch._sha256_bytes(apply.batch._canonical(manifest))
|
||||
service = FakePublicationService(tmp_path / "remote.sqlite3", material)
|
||||
with _serve(service) as url:
|
||||
client = apply.JsonHttpClient(url, "test-token", allow_http_loopback=True)
|
||||
ledger = apply.ApplyLedger(tmp_path / "ledger.sqlite3")
|
||||
try:
|
||||
with pytest.raises(
|
||||
apply.batch.PublicationBatchError,
|
||||
match="exact publication module identities",
|
||||
):
|
||||
_execute(material, client, ledger)
|
||||
finally:
|
||||
ledger.close()
|
||||
assert service.apply_calls == 0
|
||||
assert service.counts() == (0, 0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("failure", ["write", "readback_hash", "runtime_hash"])
|
||||
def test_failure_never_leaves_partial_or_false_success(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
failure: str,
|
||||
) -> None:
|
||||
material = _material(monkeypatch, datetime(2026, 10, 6, 8, 0, tzinfo=UTC))
|
||||
service = FakePublicationService(
|
||||
tmp_path / "remote.sqlite3",
|
||||
material,
|
||||
fail_after=17 if failure == "write" else None,
|
||||
tamper_status=failure == "readback_hash",
|
||||
tamper_runtime_status=failure == "runtime_hash",
|
||||
)
|
||||
with _serve(service) as url:
|
||||
client = apply.JsonHttpClient(url, "test-token", allow_http_loopback=True)
|
||||
ledger = apply.ApplyLedger(tmp_path / "ledger.sqlite3")
|
||||
try:
|
||||
with pytest.raises(apply.ApplyPublicationError):
|
||||
_execute(material, client, ledger)
|
||||
state = ledger.connection.execute(
|
||||
"SELECT state FROM rc06_apply_operations"
|
||||
).fetchone()[0]
|
||||
finally:
|
||||
ledger.close()
|
||||
assert state == "rolled_back"
|
||||
assert service.counts() == (0, 0)
|
||||
|
||||
|
||||
def test_http_and_auth_fail_closed() -> None:
|
||||
with pytest.raises(apply.ApplyPublicationError, match="HTTPS"):
|
||||
apply.JsonHttpClient("http://example.com", "token")
|
||||
with pytest.raises(apply.ApplyPublicationError, match="bearer token"):
|
||||
apply.JsonHttpClient("https://example.com", "")
|
||||
Reference in New Issue
Block a user