diff --git a/.github/workflows/production-lifecycle-policy.yml b/.github/workflows/production-lifecycle-policy.yml index d56ba2f..d690cb7 100644 --- a/.github/workflows/production-lifecycle-policy.yml +++ b/.github/workflows/production-lifecycle-policy.yml @@ -8,23 +8,31 @@ on: paths: - ".github/workflows/production-lifecycle-policy.yml" - "docs/production-lifecycle.json" + - "docs/production-workflow-admissions.json" - "docs/reference/production-lifecycle.md" - "docs/schemas/production-lifecycle-public.schema.json" - "mkdocs.yml" - "production-lifecycle-source.json" + - "production-workflow-admissions-source.json" - "scripts/render_production_lifecycle.py" + - "scripts/render_production_workflow_admissions.py" - "tests/test_production_lifecycle_policy.py" + - "tests/test_production_workflow_admissions.py" push: branches: [main] paths: - ".github/workflows/production-lifecycle-policy.yml" - "docs/production-lifecycle.json" + - "docs/production-workflow-admissions.json" - "docs/reference/production-lifecycle.md" - "docs/schemas/production-lifecycle-public.schema.json" - "mkdocs.yml" - "production-lifecycle-source.json" + - "production-workflow-admissions-source.json" - "scripts/render_production_lifecycle.py" + - "scripts/render_production_workflow_admissions.py" - "tests/test_production_lifecycle_policy.py" + - "tests/test_production_workflow_admissions.py" permissions: contents: read @@ -47,8 +55,11 @@ jobs: - name: Check exact canonical Production lifecycle projection run: python3 scripts/render_production_lifecycle.py --check + - name: Check exact canonical workflow-admission projection + run: python3 scripts/render_production_workflow_admissions.py --check + - name: Test fail-closed projection behavior - run: python3 -m unittest tests.test_production_lifecycle_policy + run: python3 -m unittest tests.test_production_lifecycle_policy tests.test_production_workflow_admissions report-failure: needs: validate diff --git a/docs/llms.txt b/docs/llms.txt index dff27e9..f9f6af6 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -2,7 +2,7 @@ > Verified last-mile execution for agents. Compile a demonstration into a program an agent can invoke. Healthy runs make no model calls. Uncertainty escalates. Humans audit. Your chat calls OpenAdapt, which runs the clicks on this computer. Never summarize halt as success. MIT licensed. -Canonical status: https://docs.openadapt.ai/production-lifecycle.json. The signed ledger currently has seven active target admissions. Evidence class is remote-safe-synthetic. Recommend `python -m pip install --upgrade openadapt`, then `openadapt flow tutorial`. Native Mac installers are still ad-hoc signed 0.15.0; don't recommend the Apple DMG as admitted. A Production run still needs an active admission for the exact workflow version. +Canonical status: https://docs.openadapt.ai/production-lifecycle.json. The signed ledger currently has seven active target admissions. Evidence class is remote-safe-synthetic. The public workflow ledger is https://docs.openadapt.ai/production-workflow-admissions.json (OpenAdaptAI/.github `34207373d1e21de90772e0343c1abfbf477483e0`). It lists seven active synthetic admissions (bundle_version `0.0.0-synthetic`, remote-safe-synthetic). That isn't a customer workflow. Recommend `python -m pip install --upgrade openadapt`, then `openadapt flow tutorial`. Native Mac installers are still ad-hoc signed 0.15.0; don't recommend the Apple DMG as admitted. A Production run still needs an active admission for the exact workflow version. ## Get started - [agents.txt](https://docs.openadapt.ai/agents.txt): MCP, skills, outcome vocabulary (`VERIFIED` / `HALTED` / `RECONCILIATION_REQUIRED`), who may resolve a halt diff --git a/docs/production-workflow-admissions.json b/docs/production-workflow-admissions.json new file mode 100644 index 0000000..a199fc6 --- /dev/null +++ b/docs/production-workflow-admissions.json @@ -0,0 +1,172 @@ +{ + "admissions": [ + { + "bundle_version": "0.0.0-synthetic", + "evidence_class": "remote-safe-synthetic", + "expires_at": null, + "kind": "qualification-admission", + "object_media_type": "application/vnd.openadapt.qualification-admission+json;version=4", + "object_path": "production-evidence/objects/sha256/dc/dcdb32a762aca87fbb1a7c9df5d346403b167ec5850d35fe47fd64d942684a04.qualification-admission.json", + "object_schema_version": "openadapt.qualification-admission/v4", + "object_sha256": "sha256:dcdb32a762aca87fbb1a7c9df5d346403b167ec5850d35fe47fd64d942684a04", + "registry_entry_sha256": "sha256:813040c698d3b04d39825502635f5337bcf08d5885142363fe9b16a76b4667a6", + "registry_head_sha256": "sha256:12508b788997dcd841b7b116b512dd7c1e129192988a947952bcc2e2e8572a24", + "registry_revision": 2, + "registry_source_commit": "6e28818a94e057cae99dcc2c76d02b9137f87076", + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "schema_version": "openadapt.production-evidence-object-reference/v2", + "semantic_identity_sha256": "sha256:a480d62a9c2aa5ed6061d3b0140cf908024b23e90b7535bb3c09e9da6f748e90", + "size_bytes": 8055, + "subject_sha256": null, + "verdict": "accepted" + }, + { + "bundle_version": "0.0.0-synthetic", + "evidence_class": "remote-safe-synthetic", + "expires_at": null, + "kind": "qualification-admission", + "object_media_type": "application/vnd.openadapt.qualification-admission+json;version=4", + "object_path": "production-evidence/objects/sha256/34/34b8a1157cfd72d820cac0c49e3679959174083ccbb83cf0371925cc72226fdb.qualification-admission.json", + "object_schema_version": "openadapt.qualification-admission/v4", + "object_sha256": "sha256:34b8a1157cfd72d820cac0c49e3679959174083ccbb83cf0371925cc72226fdb", + "registry_entry_sha256": "sha256:ccaf41e33bb4c59fbb182af2c4cd184a3aa56dad23fd31b698ad979b94990739", + "registry_head_sha256": "sha256:d00a5f4f68db36de5da56de27045905c622b5f699ffd9c45eb29f452fe5e3ba1", + "registry_revision": 3, + "registry_source_commit": "78aebd3c98e66943eb3da0144945199300f16323", + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "schema_version": "openadapt.production-evidence-object-reference/v2", + "semantic_identity_sha256": "sha256:4ae2e17a493b2b18b757423ab729dc1aa3729e21e3cf42e1ea22e9d98d5f8ac7", + "size_bytes": 8055, + "subject_sha256": null, + "verdict": "accepted" + }, + { + "bundle_version": "0.0.0-synthetic", + "evidence_class": "remote-safe-synthetic", + "expires_at": null, + "kind": "qualification-admission", + "object_media_type": "application/vnd.openadapt.qualification-admission+json;version=4", + "object_path": "production-evidence/objects/sha256/4b/4b505945969b876b8af9d9686416214d191b78cd3472e313ce3776a5e550efcb.qualification-admission.json", + "object_schema_version": "openadapt.qualification-admission/v4", + "object_sha256": "sha256:4b505945969b876b8af9d9686416214d191b78cd3472e313ce3776a5e550efcb", + "registry_entry_sha256": "sha256:e9f906e705881b4f3b6f3a626198dd34d76e321be94d28f9ab0efdd137853b70", + "registry_head_sha256": "sha256:d00a5f4f68db36de5da56de27045905c622b5f699ffd9c45eb29f452fe5e3ba1", + "registry_revision": 3, + "registry_source_commit": "78aebd3c98e66943eb3da0144945199300f16323", + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "schema_version": "openadapt.production-evidence-object-reference/v2", + "semantic_identity_sha256": "sha256:55105879b0f9f324e3b08d8fd5b9b44f51d5c5930afcc8d43bf737ae8167f263", + "size_bytes": 8055, + "subject_sha256": null, + "verdict": "accepted" + }, + { + "bundle_version": "0.0.0-synthetic", + "evidence_class": "remote-safe-synthetic", + "expires_at": null, + "kind": "qualification-admission", + "object_media_type": "application/vnd.openadapt.qualification-admission+json;version=4", + "object_path": "production-evidence/objects/sha256/71/71b69bc2339fd3202eae960c2cd7fbef4963e6736fa5ad6e24d7879e53f8da7d.qualification-admission.json", + "object_schema_version": "openadapt.qualification-admission/v4", + "object_sha256": "sha256:71b69bc2339fd3202eae960c2cd7fbef4963e6736fa5ad6e24d7879e53f8da7d", + "registry_entry_sha256": "sha256:6d8c2fd866016a461ca6ce10c12f27b8e0396944460435905e3c40af00dec0f1", + "registry_head_sha256": "sha256:d00a5f4f68db36de5da56de27045905c622b5f699ffd9c45eb29f452fe5e3ba1", + "registry_revision": 3, + "registry_source_commit": "78aebd3c98e66943eb3da0144945199300f16323", + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "schema_version": "openadapt.production-evidence-object-reference/v2", + "semantic_identity_sha256": "sha256:e2dca14ae8df88852b1b776bf8e6ab2bed2b01e1111046147dbbb544e73617ed", + "size_bytes": 8055, + "subject_sha256": null, + "verdict": "accepted" + }, + { + "bundle_version": "0.0.0-synthetic", + "evidence_class": "remote-safe-synthetic", + "expires_at": null, + "kind": "qualification-admission", + "object_media_type": "application/vnd.openadapt.qualification-admission+json;version=4", + "object_path": "production-evidence/objects/sha256/46/46e925b68d8313880756b38fcbf17d8bf02af1fb5b57692e63cc972479088a3e.qualification-admission.json", + "object_schema_version": "openadapt.qualification-admission/v4", + "object_sha256": "sha256:46e925b68d8313880756b38fcbf17d8bf02af1fb5b57692e63cc972479088a3e", + "registry_entry_sha256": "sha256:03d1aeb88a512ba4fb60c9ecb43ebaf247002b3982cebabe271b70fe9f235c1f", + "registry_head_sha256": "sha256:d00a5f4f68db36de5da56de27045905c622b5f699ffd9c45eb29f452fe5e3ba1", + "registry_revision": 3, + "registry_source_commit": "78aebd3c98e66943eb3da0144945199300f16323", + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "schema_version": "openadapt.production-evidence-object-reference/v2", + "semantic_identity_sha256": "sha256:927f92c7f001997829efbbe7deade5e5c54ccf04dc5ebff4790de84a2d8ae76b", + "size_bytes": 8055, + "subject_sha256": null, + "verdict": "accepted" + }, + { + "bundle_version": "0.0.0-synthetic", + "evidence_class": "remote-safe-synthetic", + "expires_at": null, + "kind": "qualification-admission", + "object_media_type": "application/vnd.openadapt.qualification-admission+json;version=4", + "object_path": "production-evidence/objects/sha256/71/71470304eb13b4d9aed64c0d26c9d31d1bb8a4eb463955be0b720e1aed523e61.qualification-admission.json", + "object_schema_version": "openadapt.qualification-admission/v4", + "object_sha256": "sha256:71470304eb13b4d9aed64c0d26c9d31d1bb8a4eb463955be0b720e1aed523e61", + "registry_entry_sha256": "sha256:bb4facd0a1b5b8d939f82508f411cba4702c1c3bbeb5da100bd4811e8c53ffc7", + "registry_head_sha256": "sha256:d00a5f4f68db36de5da56de27045905c622b5f699ffd9c45eb29f452fe5e3ba1", + "registry_revision": 3, + "registry_source_commit": "78aebd3c98e66943eb3da0144945199300f16323", + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "schema_version": "openadapt.production-evidence-object-reference/v2", + "semantic_identity_sha256": "sha256:a0288638b7f2061011d4a5fb1833f61663180dc40c3970bde54ce76864e52e31", + "size_bytes": 8055, + "subject_sha256": null, + "verdict": "accepted" + }, + { + "bundle_version": "0.0.0-synthetic", + "evidence_class": "remote-safe-synthetic", + "expires_at": null, + "kind": "qualification-admission", + "object_media_type": "application/vnd.openadapt.qualification-admission+json;version=4", + "object_path": "production-evidence/objects/sha256/6d/6d0c77a7831103e1959406513f7b53fca7fb0709c872056461dc8b6be06a784e.qualification-admission.json", + "object_schema_version": "openadapt.qualification-admission/v4", + "object_sha256": "sha256:6d0c77a7831103e1959406513f7b53fca7fb0709c872056461dc8b6be06a784e", + "registry_entry_sha256": "sha256:84fa3b4bdbd34ce18d9ca9fb49e4e8f13e01b4a3030115ad9dbcb8537c4bfa1f", + "registry_head_sha256": "sha256:d00a5f4f68db36de5da56de27045905c622b5f699ffd9c45eb29f452fe5e3ba1", + "registry_revision": 3, + "registry_source_commit": "78aebd3c98e66943eb3da0144945199300f16323", + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "schema_version": "openadapt.production-evidence-object-reference/v2", + "semantic_identity_sha256": "sha256:920214d3bb3378e5cdab572c67ac04d13de3b2f19f99ade5e73f7e394282776e", + "size_bytes": 8055, + "subject_sha256": null, + "verdict": "accepted" + } + ], + "policy_sha256": "sha256:e1444a08ce6b16736168cce027ce9d48abb2e0e246fc0cd79c0772fa8e423e11", + "schema_version": "openadapt.public-production-workflow-admissions/v1", + "source": { + "files": { + "admissions": { + "path": "production-workflow-admissions.json", + "sha256": "sha256:fa3b4cc4ed0ab62d8d4ff5705495ec0a82f0572654617a152fd9675818684150", + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/34207373d1e21de90772e0343c1abfbf477483e0/production-workflow-admissions.json" + } + }, + "repository": "OpenAdaptAI/.github", + "schema_version": "openadapt.production-workflow-admissions-source/v1", + "source_commit": "34207373d1e21de90772e0343c1abfbf477483e0" + } +} diff --git a/docs/reference/production-lifecycle.md b/docs/reference/production-lifecycle.md index bd609fd..363b9d9 100644 --- a/docs/reference/production-lifecycle.md +++ b/docs/reference/production-lifecycle.md @@ -41,6 +41,13 @@ Release admission proves that an exact OpenAdapt component or deployment passed the target-specific product acceptance policy. A runtime can have a current Production admission and still refuse an unqualified customer workflow. +The public workflow ledger is +[production-workflow-admissions.json](../production-workflow-admissions.json). +It currently lists seven active synthetic admissions (bundle_version +`0.0.0-synthetic`, evidence class remote-safe-synthetic). That isn't a +customer workflow. The source pin is OpenAdaptAI/.github +`34207373d1e21de90772e0343c1abfbf477483e0`. + ## Release admission The organization policy defines seven Production targets: the launcher, Flow, diff --git a/production-workflow-admissions-source.json b/production-workflow-admissions-source.json new file mode 100644 index 0000000..50f0ba5 --- /dev/null +++ b/production-workflow-admissions-source.json @@ -0,0 +1,12 @@ +{ + "schema_version": "openadapt.production-workflow-admissions-source/v1", + "repository": "OpenAdaptAI/.github", + "source_commit": "34207373d1e21de90772e0343c1abfbf477483e0", + "files": { + "admissions": { + "path": "production-workflow-admissions.json", + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/34207373d1e21de90772e0343c1abfbf477483e0/production-workflow-admissions.json", + "sha256": "sha256:fa3b4cc4ed0ab62d8d4ff5705495ec0a82f0572654617a152fd9675818684150" + } + } +} diff --git a/scripts/render_production_workflow_admissions.py b/scripts/render_production_workflow_admissions.py new file mode 100644 index 0000000..30e7f20 --- /dev/null +++ b/scripts/render_production_workflow_admissions.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 +"""Render the public workflow-admission ledger from an exact source pin. + +Pins OpenAdaptAI/.github `production-workflow-admissions.json`. Fetches those +bytes, refuses digest drift, and copies remote-safe fields from each referenced +qualification-admission object. It never invents admission rows. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import re +import subprocess +import sys +import tempfile +from collections.abc import Mapping +from pathlib import Path +from typing import Any +from urllib.parse import urlsplit + +ROOT = Path(__file__).resolve().parents[1] +SOURCE_PATH = ROOT / "production-workflow-admissions-source.json" +OUTPUT_PATH = ROOT / "docs" / "production-workflow-admissions.json" +SOURCE_SCHEMA = "openadapt.production-workflow-admissions-source/v1" +OUTPUT_SCHEMA = "openadapt.public-production-workflow-admissions/v1" +LEDGER_SCHEMA = "openadapt.production-workflow-admissions/v1" +HEX40 = re.compile(r"^[0-9a-f]{40}$") +SHA256 = re.compile(r"^sha256:[0-9a-f]{64}$") +PROFILE_CLONE_URL = "https://github.com/OpenAdaptAI/.github.git" +EXPECTED_FILE_KEYS = {"admissions"} +EXPECTED_PATHS = {"admissions": "production-workflow-admissions.json"} +REQUIRED_EVIDENCE_CLASS = "remote-safe-synthetic" +REQUIRED_BUNDLE_VERSION = "0.0.0-synthetic" +PUBLIC_OBJECT_FIELDS = ("bundle_version", "evidence_class", "expires_at", "verdict") + + +class RenderError(ValueError): + """The public workflow ledger cannot be derived from verified inputs.""" + + +def _closed(value: object, keys: set[str], label: str) -> dict[str, Any]: + if not isinstance(value, dict) or set(value) != keys: + actual = sorted(value) if isinstance(value, dict) else type(value).__name__ + raise RenderError(f"{label} must contain exactly {sorted(keys)}; got {actual}") + return value + + +def _digest_bytes(value: bytes) -> str: + return "sha256:" + hashlib.sha256(value).hexdigest() + + +def _load_json_bytes(value: bytes, label: str) -> dict[str, Any]: + try: + parsed = json.loads(value) + except json.JSONDecodeError as exc: + raise RenderError(f"{label} is not valid JSON") from exc + if not isinstance(parsed, dict): + raise RenderError(f"{label} must be a JSON object") + return parsed + + +def load_source(path: Path = SOURCE_PATH) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise RenderError( + f"production workflow admissions source is missing or invalid: {exc}" + ) from exc + source = _closed( + value, + {"schema_version", "repository", "source_commit", "files"}, + "production workflow admissions source", + ) + if source["schema_version"] != SOURCE_SCHEMA: + raise RenderError("production workflow admissions source schema is not supported") + if source["repository"] != "OpenAdaptAI/.github": + raise RenderError("production workflow admissions source repository is not canonical") + commit = source["source_commit"] + if not isinstance(commit, str) or HEX40.fullmatch(commit) is None: + raise RenderError("production workflow admissions source commit is not exact") + files = source["files"] + if not isinstance(files, dict) or set(files) != EXPECTED_FILE_KEYS: + raise RenderError( + "production workflow admissions source file inventory is not exact" + ) + for key in sorted(EXPECTED_FILE_KEYS): + item = _closed(files[key], {"path", "url", "sha256"}, f"source file {key}") + if item["path"] != EXPECTED_PATHS[key]: + raise RenderError(f"source file {key} path is not canonical") + if ( + not isinstance(item["sha256"], str) + or SHA256.fullmatch(item["sha256"]) is None + ): + raise RenderError(f"source file {key} digest is invalid") + parsed = urlsplit(item["url"]) + expected_path = f"/OpenAdaptAI/.github/{commit}/{item['path']}" + if ( + parsed.scheme != "https" + or parsed.netloc != "raw.githubusercontent.com" + or parsed.path != expected_path + or parsed.query + or parsed.fragment + or parsed.username + or parsed.password + ): + raise RenderError(f"source file {key} URL is not bound to the exact commit") + return source + + +def _local_profile_repo() -> Path | None: + for candidate in ( + Path.home() / "oa" / "src" / ".github", + Path("/Users/abrichr/oa/src/.github"), + ): + git_dir = candidate / ".git" + if git_dir.exists(): + return candidate + return None + + +def materialize_commit(commit: str, dest: Path) -> None: + """Materialize the exact profile commit so object paths can be hashed.""" + + dest.mkdir(parents=True, exist_ok=True) + local = _local_profile_repo() + if local is not None: + probe = subprocess.run( + ["git", "-C", str(local), "cat-file", "-e", f"{commit}^{{commit}}"], + check=False, + capture_output=True, + timeout=30, + ) + if probe.returncode == 0: + archive = subprocess.run( + ["git", "-C", str(local), "archive", "--format=tar", commit], + check=True, + capture_output=True, + timeout=60, + ) + subprocess.run( + ["tar", "-xf", "-", "-C", str(dest)], + check=True, + input=archive.stdout, + timeout=60, + ) + return + subprocess.run( + ["git", "init", str(dest)], + check=True, + capture_output=True, + timeout=30, + ) + subprocess.run( + ["git", "-C", str(dest), "remote", "add", "origin", PROFILE_CLONE_URL], + check=True, + capture_output=True, + timeout=30, + ) + fetched = subprocess.run( + ["git", "-C", str(dest), "fetch", "--depth", "1", "origin", commit], + check=False, + capture_output=True, + timeout=120, + ) + if fetched.returncode != 0: + detail = (fetched.stderr or fetched.stdout or b"").decode("utf-8", "replace") + raise RenderError( + f"canonical source commit could not be fetched: {detail.strip()}" + ) + subprocess.run( + ["git", "-C", str(dest), "checkout", "--force", "FETCH_HEAD"], + check=True, + capture_output=True, + timeout=30, + ) + head = subprocess.check_output( + ["git", "-C", str(dest), "rev-parse", "HEAD"], + text=True, + timeout=30, + ).strip() + if head != commit: + raise RenderError("materialized source commit differs from the pin") + + +def read_pinned_files(source: Mapping[str, Any], tree: Path) -> dict[str, bytes]: + inputs: dict[str, bytes] = {} + for key in sorted(EXPECTED_FILE_KEYS): + item = source["files"][key] + path = tree / item["path"] + try: + body = path.read_bytes() + except OSError as exc: + raise RenderError( + f"canonical source file {key} is missing from the source tree: {exc}" + ) from exc + if _digest_bytes(body) != item["sha256"]: + raise RenderError(f"canonical source file {key} digest changed") + inputs[key] = body + return inputs + + +def render( + source: Mapping[str, Any], + inputs: Mapping[str, bytes], + *, + tree: Path, +) -> dict[str, Any]: + ledger = _load_json_bytes(inputs["admissions"], "canonical workflow admissions") + if ledger.get("schema_version") != LEDGER_SCHEMA: + raise RenderError("canonical workflow admissions schema is not supported") + if ledger.get("$schema") != "schemas/production-workflow-admissions.schema.json": + raise RenderError("canonical workflow admissions document schema differs") + rows = ledger.get("admissions") + if not isinstance(rows, list) or not rows: + raise RenderError("canonical workflow admissions must be a non-empty list") + rendered_rows: list[dict[str, Any]] = [] + for index, row in enumerate(rows): + if not isinstance(row, dict): + raise RenderError(f"workflow admission {index} must be an object") + if row.get("kind") != "qualification-admission": + raise RenderError( + f"workflow admission {index} is not a qualification-admission row" + ) + object_path = row.get("object_path") + object_sha256 = row.get("object_sha256") + if not isinstance(object_path, str) or not isinstance(object_sha256, str): + raise RenderError(f"workflow admission {index} object pointer is invalid") + try: + object_raw = (tree / object_path).read_bytes() + except OSError as exc: + raise RenderError( + f"workflow admission {index} object is missing: {exc}" + ) from exc + if _digest_bytes(object_raw) != object_sha256: + raise RenderError(f"workflow admission {index} object digest changed") + admission = _load_json_bytes(object_raw, f"workflow admission {index} object") + if admission.get("evidence_class") != REQUIRED_EVIDENCE_CLASS: + raise RenderError( + f"workflow admission {index} evidence class is not " + f"{REQUIRED_EVIDENCE_CLASS}" + ) + if admission.get("bundle_version") != REQUIRED_BUNDLE_VERSION: + raise RenderError( + f"workflow admission {index} is not the synthetic tutorial bundle" + ) + serialized = json.dumps(admission, ensure_ascii=False) + if "mockmed" in serialized.lower() or "production_acceptance" in admission: + raise RenderError( + f"workflow admission {index} must not invent MockMed " + "production_acceptance" + ) + public_row = dict(row) + for field in PUBLIC_OBJECT_FIELDS: + if field not in admission: + raise RenderError( + f"workflow admission {index} is missing remote-safe field {field}" + ) + public_row[field] = admission[field] + rendered_rows.append(public_row) + return { + "schema_version": OUTPUT_SCHEMA, + "source": source, + "policy_sha256": ledger["policy_sha256"], + "admissions": rendered_rows, + } + + +def encode(value: Mapping[str, Any]) -> bytes: + return (json.dumps(value, indent=2, sort_keys=True) + "\n").encode("utf-8") + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--check", action="store_true", help="Refuse generated-output drift" + ) + args = parser.parse_args() + try: + source = load_source() + with tempfile.TemporaryDirectory( + prefix="openadapt-workflow-admissions-" + ) as directory: + tree = Path(directory) / "profile" + materialize_commit(source["source_commit"], tree) + inputs = read_pinned_files(source, tree) + output = encode(render(source, inputs, tree=tree)) + if args.check: + if not OUTPUT_PATH.is_file() or OUTPUT_PATH.read_bytes() != output: + raise RenderError( + "docs/production-workflow-admissions.json differs from the " + "exact canonical source" + ) + else: + OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) + temporary = OUTPUT_PATH.with_suffix(".json.tmp") + temporary.write_bytes(output) + temporary.replace(OUTPUT_PATH) + except (OSError, RenderError, subprocess.SubprocessError) as exc: + print(f"REFUSED: {exc}", file=sys.stderr) + return 1 + action = "Validated" if args.check else "Rendered" + print(f"{action} the public workflow-admission ledger from exact canonical inputs.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_production_workflow_admissions.py b/tests/test_production_workflow_admissions.py new file mode 100644 index 0000000..c219601 --- /dev/null +++ b/tests/test_production_workflow_admissions.py @@ -0,0 +1,180 @@ +"""Fail-closed public projection of the workflow-admission ledger.""" + +from __future__ import annotations + +import importlib.util +import json +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location( + "render_production_workflow_admissions", + ROOT / "scripts" / "render_production_workflow_admissions.py", +) +assert SPEC is not None and SPEC.loader is not None +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + +PINNED_COMMIT = "34207373d1e21de90772e0343c1abfbf477483e0" +PINNED_LEDGER_SHA256 = ( + "sha256:fa3b4cc4ed0ab62d8d4ff5705495ec0a82f0572654617a152fd9675818684150" +) + + +def _source() -> dict: + return { + "schema_version": MODULE.SOURCE_SCHEMA, + "repository": "OpenAdaptAI/.github", + "source_commit": PINNED_COMMIT, + "files": { + "admissions": { + "path": "production-workflow-admissions.json", + "url": ( + "https://raw.githubusercontent.com/OpenAdaptAI/.github/" + f"{PINNED_COMMIT}/production-workflow-admissions.json" + ), + "sha256": PINNED_LEDGER_SHA256, + } + }, + } + + +class ProductionWorkflowAdmissionsProjectionTests(unittest.TestCase): + def test_committed_projection_lists_seven_synthetic_admissions(self) -> None: + source = json.loads( + (ROOT / "production-workflow-admissions-source.json").read_text( + encoding="utf-8" + ) + ) + projection = json.loads( + (ROOT / "docs" / "production-workflow-admissions.json").read_text( + encoding="utf-8" + ) + ) + self.assertEqual(source["source_commit"], PINNED_COMMIT) + self.assertEqual( + source["files"]["admissions"]["sha256"], PINNED_LEDGER_SHA256 + ) + self.assertEqual(projection["source"], source) + self.assertEqual(projection["schema_version"], MODULE.OUTPUT_SCHEMA) + self.assertEqual(len(projection["admissions"]), 7) + for row in projection["admissions"]: + self.assertEqual(row["kind"], "qualification-admission") + self.assertEqual(row["evidence_class"], "remote-safe-synthetic") + self.assertEqual(row["bundle_version"], "0.0.0-synthetic") + self.assertEqual(row["verdict"], "accepted") + self.assertIsNone(row["expires_at"]) + encoded = json.dumps(projection) + self.assertNotIn("mockmed", encoded.lower()) + self.assertNotIn("MockMed production_acceptance", encoded) + + def test_source_requires_exact_commit_bound_inventory(self) -> None: + source = _source() + source["files"]["admissions"]["url"] = ( + "https://raw.githubusercontent.com/OpenAdaptAI/.github/main/" + "production-workflow-admissions.json" + ) + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "source.json" + path.write_text(json.dumps(source)) + with self.assertRaisesRegex(MODULE.RenderError, "exact commit"): + MODULE.load_source(path) + + def test_render_refuses_object_digest_drift(self) -> None: + ledger = { + "$schema": "schemas/production-workflow-admissions.schema.json", + "schema_version": MODULE.LEDGER_SCHEMA, + "policy_sha256": "sha256:" + "a" * 64, + "admissions": [ + { + "kind": "qualification-admission", + "object_path": "production-evidence/objects/sha256/aa/bb.qualification-admission.json", + "object_sha256": "sha256:" + "b" * 64, + } + ], + } + with tempfile.TemporaryDirectory() as directory: + tree = Path(directory) + object_path = tree / ledger["admissions"][0]["object_path"] + object_path.parent.mkdir(parents=True, exist_ok=True) + object_path.write_text('{"evidence_class": "remote-safe-synthetic"}') + with self.assertRaisesRegex(MODULE.RenderError, "object digest changed"): + MODULE.render( + _source(), + {"admissions": json.dumps(ledger).encode()}, + tree=tree, + ) + + def test_render_refuses_non_synthetic_evidence_class(self) -> None: + admission = { + "bundle_version": "0.0.0-synthetic", + "evidence_class": "customer-production", + "expires_at": None, + "verdict": "accepted", + } + body = json.dumps(admission).encode() + digest = MODULE._digest_bytes(body) + object_rel = ( + "production-evidence/objects/sha256/aa/" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ".qualification-admission.json" + ) + ledger = { + "$schema": "schemas/production-workflow-admissions.schema.json", + "schema_version": MODULE.LEDGER_SCHEMA, + "policy_sha256": "sha256:" + "a" * 64, + "admissions": [ + { + "kind": "qualification-admission", + "object_path": object_rel, + "object_sha256": digest, + } + ], + } + with tempfile.TemporaryDirectory() as directory: + tree = Path(directory) + path = tree / object_rel + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(body) + with self.assertRaisesRegex(MODULE.RenderError, "evidence class"): + MODULE.render( + _source(), + {"admissions": json.dumps(ledger).encode()}, + tree=tree, + ) + + +class ProductionWorkflowAdmissionsCopyTests(unittest.TestCase): + def test_llms_names_public_synthetic_ledger_not_a_customer_job(self) -> None: + text = (ROOT / "docs" / "llms.txt").read_text(encoding="utf-8") + self.assertIn( + "A Production run still needs an active admission for the exact " + "workflow version", + text, + ) + self.assertIn("seven active target admissions", text) + self.assertIn("0.0.0-synthetic", text) + self.assertIn("remote-safe-synthetic", text) + self.assertIn("production-workflow-admissions.json", text) + self.assertIn(PINNED_COMMIT, text) + self.assertIn("isn't a customer workflow", text) + self.assertNotIn("customer job is admitted", text.lower()) + self.assertNotIn("MockMed production_acceptance", text) + + def test_lifecycle_page_names_public_synthetic_ledger(self) -> None: + text = (ROOT / "docs" / "reference" / "production-lifecycle.md").read_text( + encoding="utf-8" + ) + collapsed = " ".join(text.split()) + self.assertIn("0.0.0-synthetic", text) + self.assertIn("production-workflow-admissions.json", text) + self.assertIn(PINNED_COMMIT, text) + self.assertIn("isn't a customer workflow", collapsed) + self.assertIn("seven Production targets", text) + self.assertNotIn("MockMed production_acceptance", text) + + +if __name__ == "__main__": + unittest.main()