diff --git a/pyproject.toml b/pyproject.toml index 27f02257..c926f7c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ testpaths = [ ] pythonpath = [ "yuqei-contract-platform/services/contract-api/src", + "yuqei-contract-platform/services/contract-api/tests", "yuqei-ai-platform/services/ai-platform-api/src", "yuqei-ai-platform/services/worker/src", "yuqei-ai-sdk-python/src", diff --git a/yuqei-ai-platform/packages/ai-openapi/openapi.yaml b/yuqei-ai-platform/packages/ai-openapi/openapi.yaml index ec7ab5ec..a5b68ff5 100644 --- a/yuqei-ai-platform/packages/ai-openapi/openapi.yaml +++ b/yuqei-ai-platform/packages/ai-openapi/openapi.yaml @@ -1,5 +1,10 @@ components: schemas: + PromptCapabilityRequest: {"properties":{"capability":{"type":"string","maxLength":128,"minLength":43,"title":"Capability"}},"additionalProperties":false,"type":"object","required":["capability"],"title":"PromptCapabilityRequest"} + PromptModuleInput: {"properties":{"category":{"type":"string","enum":["drafting","review"],"title":"Category"},"key":{"type":"string","maxLength":128,"minLength":1,"title":"Key"},"version":{"type":"string","maxLength":128,"minLength":1,"title":"Version"},"content":{"type":"string","maxLength":200000,"minLength":1,"title":"Content"},"content_sha256":{"type":"string","pattern":"^[a-f0-9]{64}$","title":"Content Sha256"},"publication_mapping":{"additionalProperties":{"type":"string"},"type":"object","title":"Publication Mapping"},"module_payload_sha256":{"type":"string","pattern":"^[a-f0-9]{64}$","title":"Module Payload Sha256"}},"additionalProperties":false,"type":"object","required":["category","key","version","content","content_sha256","publication_mapping","module_payload_sha256"],"title":"PromptModuleInput"} + PromptPrepareRequest: {"properties":{"transaction_id":{"type":"string","pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{7,63}$","title":"Transaction Id"},"idempotency_key":{"type":"string","maxLength":160,"minLength":8,"title":"Idempotency Key"},"candidate_commit":{"type":"string","pattern":"^(?:[a-f0-9]{40}|[a-f0-9]{64})$","title":"Candidate Commit"},"release_image_digest":{"type":"string","pattern":"^sha256:[a-f0-9]{64}$","title":"Release Image Digest"},"candidate_tag":{"type":"string","maxLength":128,"minLength":1,"title":"Candidate Tag"},"batch_manifest_raw_sha256":{"type":"string","pattern":"^[a-f0-9]{64}$","title":"Batch Manifest Raw Sha256"},"batch_manifest_payload_sha256":{"type":"string","pattern":"^[a-f0-9]{64}$","title":"Batch Manifest Payload Sha256"},"approval_receipt_sha256":{"type":"string","pattern":"^[a-f0-9]{64}$","title":"Approval Receipt Sha256"},"authority_expires_at":{"type":"string","format":"date-time","title":"Authority Expires At"},"compensation_capability_sha256":{"type":"string","pattern":"^[a-f0-9]{64}$","title":"Compensation Capability Sha256"},"modules":{"items":{"$ref":"#/components/schemas/PromptModuleInput"},"type":"array","title":"Modules"},"runtime_identities":{"items":{"$ref":"#/components/schemas/RuntimeIdentityInput"},"type":"array","title":"Runtime Identities"}},"additionalProperties":false,"type":"object","required":["transaction_id","idempotency_key","candidate_commit","release_image_digest","candidate_tag","batch_manifest_raw_sha256","batch_manifest_payload_sha256","approval_receipt_sha256","authority_expires_at","compensation_capability_sha256","modules","runtime_identities"],"title":"PromptPrepareRequest"} + PromptTransactionStatus: {"properties":{"transaction_id":{"type":"string","title":"Transaction Id"},"state":{"type":"string","title":"State"},"candidate_commit":{"type":"string","title":"Candidate Commit"},"release_image_digest":{"type":"string","title":"Release Image Digest"},"candidate_tag":{"type":"string","title":"Candidate Tag"},"batch_manifest_raw_sha256":{"type":"string","title":"Batch Manifest Raw Sha256"},"batch_manifest_payload_sha256":{"type":"string","title":"Batch Manifest Payload Sha256"},"approval_receipt_sha256":{"type":"string","title":"Approval Receipt Sha256"},"authority_expires_at":{"type":"string","format":"date-time","title":"Authority Expires At"},"item_results":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Item Results"},"runtime_snapshots":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Runtime Snapshots"},"write_count":{"type":"integer","title":"Write Count","default":0},"runtime_snapshot_count":{"type":"integer","title":"Runtime Snapshot Count","default":0},"no_go":{"type":"boolean","title":"No Go","default":false},"committed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Committed At"},"sealed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Sealed At"},"compensated_from_state":{"anyOf":[{"type":"string","enum":["prepared","committed"]},{"type":"null"}],"title":"Compensated From State"},"compensation_item_results":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Compensation Item Results"},"runtime_snapshot_compensation_results":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Runtime Snapshot Compensation Results"},"restored_item_count":{"type":"integer","title":"Restored Item Count","default":0},"removed_item_count":{"type":"integer","title":"Removed Item Count","default":0},"unchanged_item_count":{"type":"integer","title":"Unchanged Item Count","default":0},"compensated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Compensated At"},"compensation_evidence_sha256":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Compensation Evidence Sha256"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},"type":"object","required":["transaction_id","state","candidate_commit","release_image_digest","candidate_tag","batch_manifest_raw_sha256","batch_manifest_payload_sha256","approval_receipt_sha256","authority_expires_at"],"title":"PromptTransactionStatus"} + RuntimeIdentityInput: {"properties":{"name":{"type":"string","maxLength":128,"minLength":1,"title":"Name"},"version":{"type":"string","maxLength":128,"minLength":1,"title":"Version"},"content_sha256":{"type":"string","pattern":"^[a-f0-9]{64}$","title":"Content Sha256"},"source_kind":{"type":"string","enum":["composed_drafting_modules","new_review_module_composite","existing_domain_prompt_asset"],"title":"Source Kind"}},"additionalProperties":false,"type":"object","required":["name","version","content_sha256","source_kind"],"title":"RuntimeIdentityInput"} AiQualityAttribution: properties: by_knowledge_base: @@ -6870,6 +6875,11 @@ info: description: V2 AI platform contract for model routing, legal QA, legal review, knowledge bases, and AI audit. openapi: 3.1.0 paths: + /api/v1/internal/rc06-publications/prepare: {"post":{"tags":["internal","rc06"],"summary":"Prepare","operationId":"prepare_api_v1_internal_rc06_publications_prepare_post","parameters":[{"name":"x-rc06-publication-service-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Rc06-Publication-Service-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptPrepareRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptTransactionStatus"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}} + /api/v1/internal/rc06-publications/{transaction_id}/commit: {"post":{"tags":["internal","rc06"],"summary":"Commit","operationId":"commit_api_v1_internal_rc06_publications__transaction_id__commit_post","parameters":[{"name":"transaction_id","in":"path","required":true,"schema":{"type":"string","title":"Transaction Id"}},{"name":"x-rc06-publication-service-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Rc06-Publication-Service-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptTransactionStatus"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}} + /api/v1/internal/rc06-publications/{transaction_id}: {"get":{"tags":["internal","rc06"],"summary":"Status","operationId":"status_api_v1_internal_rc06_publications__transaction_id__get","parameters":[{"name":"transaction_id","in":"path","required":true,"schema":{"type":"string","title":"Transaction Id"}},{"name":"x-rc06-publication-service-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Rc06-Publication-Service-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptTransactionStatus"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}} + /api/v1/internal/rc06-publications/{transaction_id}/seal: {"post":{"tags":["internal","rc06"],"summary":"Seal","operationId":"seal_api_v1_internal_rc06_publications__transaction_id__seal_post","parameters":[{"name":"transaction_id","in":"path","required":true,"schema":{"type":"string","title":"Transaction Id"}},{"name":"x-rc06-publication-service-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Rc06-Publication-Service-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptCapabilityRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptTransactionStatus"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}} + /api/v1/internal/rc06-publications/{transaction_id}/compensate: {"post":{"tags":["internal","rc06"],"summary":"Compensate","operationId":"compensate_api_v1_internal_rc06_publications__transaction_id__compensate_post","parameters":[{"name":"transaction_id","in":"path","required":true,"schema":{"type":"string","title":"Transaction Id"}},{"name":"x-rc06-publication-service-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Rc06-Publication-Service-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptCapabilityRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptTransactionStatus"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}} /api/v1/audit/ai-runs: get: operationId: list_ai_runs_api_v1_audit_ai_runs_get diff --git a/yuqei-ai-platform/services/ai-platform-api/tests/test_rc06_publication.py b/yuqei-ai-platform/services/ai-platform-api/tests/test_rc06_publication.py index 55859884..6c4dc159 100644 --- a/yuqei-ai-platform/services/ai-platform-api/tests/test_rc06_publication.py +++ b/yuqei-ai-platform/services/ai-platform-api/tests/test_rc06_publication.py @@ -3,6 +3,7 @@ from __future__ import annotations import hashlib import json from datetime import UTC, datetime, timedelta +from pathlib import Path import pytest from fastapi.testclient import TestClient @@ -32,7 +33,7 @@ from yuqei_ai_platform_api.rc06_publication import ( Rc06PublicationError, _source_inventory, ) -from yuqei_ai_platform_api.repository import SqlAlchemyAiPlatformRepository +from yuqei_ai_platform_api.repository import InMemoryAiPlatformRepository, SqlAlchemyAiPlatformRepository CANDIDATE_COMMIT = "a" * 40 @@ -95,6 +96,43 @@ def _request(transaction_id: str = "rc06-ai-transaction-0001") -> PromptPrepareR ) +def test_internal_rc06_openapi_fragment_matches_current_participant(tmp_path): + app = create_app( + AiPlatformSettings( + _env_file=None, + environment="test", + repository_backend="memory", + provider_call_mode="disabled", + object_storage_local_dir=str(tmp_path / "objects"), + ), + repository=InMemoryAiPlatformRepository(), + ) + runtime = app.openapi() + published = ( + Path(__file__).resolve().parents[3] / "packages/ai-openapi/openapi.yaml" + ).read_text(encoding="utf-8") + decoder = json.JSONDecoder() + paths = { + path: value for path, value in runtime["paths"].items() + if path.startswith("/api/v1/internal/rc06-publications") + } + assert len(paths) == 5 + # The internal fragment uses JSON, which is valid YAML, without a test-only parser dependency. + for path, expected in paths.items(): + marker = f" {path}: " + start = published.index(marker) + len(marker) + actual, _ = decoder.raw_decode(published[start:]) + assert actual == expected + for name in ( + "PromptCapabilityRequest", "PromptModuleInput", "PromptPrepareRequest", + "PromptTransactionStatus", "RuntimeIdentityInput", + ): + marker = f" {name}: " + start = published.index(marker) + len(marker) + actual, _ = decoder.raw_decode(published[start:]) + assert actual == runtime["components"]["schemas"][name] + + def test_prepare_rejects_expired_authority_without_journal_or_prompt_writes(tmp_path): repository = _repository(tmp_path) try: diff --git a/yuqei-infra/scripts/rc06-release-trust-bootstrap.py b/yuqei-infra/scripts/rc06-release-trust-bootstrap.py index ab568efb..b549aa84 100644 --- a/yuqei-infra/scripts/rc06-release-trust-bootstrap.py +++ b/yuqei-infra/scripts/rc06-release-trust-bootstrap.py @@ -2516,16 +2516,42 @@ def _exec_verified_module(name: str, path: Path, raw: bytes) -> Any: """ +_VERIFIED_SNAPSHOT_MODULE_EXEC_TEMPLATE = """ +def _exec_verified_module(raw: bytes, path: Path, module_name: str) -> Any: + try: + code = compile(raw, os.fspath(path), "exec", dont_inherit=True) + except (SyntaxError, UnicodeError, ValueError) as exc: + raise SnapshotRestorePlanError("verified validator source cannot be compiled") from exc + module = types.ModuleType(module_name) + module.__file__ = os.fspath(path) + module.__package__ = "" + module.__loader__ = None + module.__spec__ = importlib.util.spec_from_loader( + module_name, loader=None, origin=os.fspath(path) + ) + sys.modules[module_name] = module + try: + exec(code, module.__dict__) + except Exception as exc: + sys.modules.pop(module_name, None) + raise SnapshotRestorePlanError("verified validator source failed to load") from exc + return module +""" + + def _safe_verified_module_exec(call: ast.Call, parents: Mapping[ast.AST, ast.AST]) -> bool: parent: ast.AST | None = call while parent is not None and not isinstance(parent, (ast.FunctionDef, ast.AsyncFunctionDef)): parent = parents.get(parent) if not isinstance(parent, ast.FunctionDef): return False - expected = ast.parse(_VERIFIED_MODULE_EXEC_TEMPLATE).body[0] - return ast.dump(parent, include_attributes=False) == ast.dump( - expected, - include_attributes=False, + observed = ast.dump(parent, include_attributes=False) + return any( + observed == ast.dump(ast.parse(template).body[0], include_attributes=False) + for template in ( + _VERIFIED_MODULE_EXEC_TEMPLATE, + _VERIFIED_SNAPSHOT_MODULE_EXEC_TEMPLATE, + ) ) diff --git a/yuqei-infra/tests/test_multiscene_base_journeys.py b/yuqei-infra/tests/test_multiscene_base_journeys.py index fa0eed71..3f0d0b1e 100644 --- a/yuqei-infra/tests/test_multiscene_base_journeys.py +++ b/yuqei-infra/tests/test_multiscene_base_journeys.py @@ -8,8 +8,6 @@ from pathlib import Path import pytest -import multiscene_eval_plan - ROOT = Path(__file__).resolve().parents[2] RUNNER_PATH = ROOT / "yuqei-infra/scripts/run-core-drafting-smoke.py" @@ -18,6 +16,8 @@ assert SPEC and SPEC.loader RUNNER = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(RUNNER) +import multiscene_eval_plan + @pytest.mark.parametrize( ("raw", "matches"), diff --git a/yuqei-infra/tests/test_rc06_publication_batch.py b/yuqei-infra/tests/test_rc06_publication_batch.py index 04a14d99..4e11f9f9 100644 --- a/yuqei-infra/tests/test_rc06_publication_batch.py +++ b/yuqei-infra/tests/test_rc06_publication_batch.py @@ -2876,13 +2876,16 @@ def test_two_process_legacy_adoption_never_deletes_the_other_process_marker( state_directory, key_directory = _make_legacy_default_state(tmp_path, monkeypatch) ready = tmp_path / "holder-ready" release = tmp_path / "holder-release" + # Windows validates owner ACLs in child processes before reaching the barrier. + timeout_seconds = 120 if os.name == "nt" else 20 child = r''' import importlib.util import pathlib import sys import time -script, state_raw, ready_raw, release_raw, mode = sys.argv[1:] +script, state_raw, ready_raw, release_raw, timeout_raw, mode = sys.argv[1:] +timeout_seconds = float(timeout_raw) spec = importlib.util.spec_from_file_location("signer_concurrency_fixture", script) module = importlib.util.module_from_spec(spec) assert spec is not None and spec.loader is not None @@ -2898,7 +2901,7 @@ if mode == "holder": original(directory, **kwargs) if directory == state / "keys" and not ready.exists(): ready.write_text("ready", encoding="utf-8") - deadline = time.monotonic() + 20 + deadline = time.monotonic() + timeout_seconds while not release.exists() and time.monotonic() < deadline: time.sleep(0.05) if not release.exists(): @@ -2923,30 +2926,37 @@ print("ACQUIRED") os.fspath(state_directory), os.fspath(ready), os.fspath(release), + str(timeout_seconds), ] - holder = subprocess.Popen( - [*command, "holder"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - try: - deadline = time.monotonic() + 20 - while not ready.exists() and time.monotonic() < deadline and holder.poll() is None: - time.sleep(0.05) - assert ready.is_file(), holder.communicate(timeout=5) - contender = subprocess.run( - [*command, "contender"], - check=False, - capture_output=True, - text=True, - timeout=30, - ) - assert contender.returncode == 0, contender.stderr or contender.stdout - assert "BUSY" in contender.stdout - finally: - release.write_text("release", encoding="utf-8") - holder_stdout, holder_stderr = holder.communicate(timeout=30) + stdout_path = tmp_path / "holder.stdout" + stderr_path = tmp_path / "holder.stderr" + with stdout_path.open("w", encoding="utf-8") as stdout, stderr_path.open( + "w", encoding="utf-8" + ) as stderr: + holder = subprocess.Popen([*command, "holder"], stdout=stdout, stderr=stderr) + try: + deadline = time.monotonic() + timeout_seconds + while not ready.exists() and time.monotonic() < deadline and holder.poll() is None: + time.sleep(0.05) + assert ready.is_file(), ( + stderr_path.read_text(encoding="utf-8") + or stdout_path.read_text(encoding="utf-8") + or "holder did not reach the owner-marker barrier" + ) + contender = subprocess.run( + [*command, "contender"], + check=False, + capture_output=True, + text=True, + timeout=timeout_seconds, + ) + assert contender.returncode == 0, contender.stderr or contender.stdout + assert "BUSY" in contender.stdout + finally: + release.write_text("release", encoding="utf-8") + holder.wait(timeout=timeout_seconds) + holder_stdout = stdout_path.read_text(encoding="utf-8") + holder_stderr = stderr_path.read_text(encoding="utf-8") assert holder.returncode == 0, holder_stderr or holder_stdout assert "ACQUIRED" in holder_stdout assert (key_directory / signer.OWNER_MARKER_FILENAME).is_file() diff --git a/yuqei-infra/tests/test_rc06_release_trust_bootstrap.py b/yuqei-infra/tests/test_rc06_release_trust_bootstrap.py index 363e72b6..35a4c5e8 100644 --- a/yuqei-infra/tests/test_rc06_release_trust_bootstrap.py +++ b/yuqei-infra/tests/test_rc06_release_trust_bootstrap.py @@ -1121,13 +1121,22 @@ def test_pin_planner_isolated_runtime_detects_import_side_effect_override( ) +@pytest.mark.parametrize( + ("filename", "module_name"), + ( + ("plan-rollback-rehearsal.py", "name"), + ("plan-first-release-snapshot-restore.py", "module_name"), + ), +) def test_pin_planner_rejects_modified_verified_module_exec_loader( tmp_path: Path, + filename: str, + module_name: str, ) -> None: state, repo, _plan = _pin_plan_fixture(tmp_path, include_online=False) - rollback = repo / "yuqei-infra/deploy/plan-rollback-rehearsal.py" - original = rollback.read_text(encoding="utf-8") - needle = ' module = types.ModuleType(name)\n' + planner = repo / "yuqei-infra/deploy" / filename + original = planner.read_text(encoding="utf-8") + needle = f' module = types.ModuleType({module_name})\n' replacement = ( " if sys.argv[0] != '-c':\n" " module = sys.modules[__name__]\n" @@ -1135,10 +1144,10 @@ def test_pin_planner_rejects_modified_verified_module_exec_loader( + "f" * 64 + "'\"\n" " else:\n" - " module = types.ModuleType(name)\n" + f" module = types.ModuleType({module_name})\n" ) assert needle in original - rollback.write_text(original.replace(needle, replacement, 1), encoding="utf-8") + planner.write_text(original.replace(needle, replacement, 1), encoding="utf-8") with pytest.raises(authority.AuthorityError, match="untrusted dynamic exec"): authority.plan_pin_updates(