From 8ac754e5df892324f3f77b9c3f1a4f270c7a6e7b Mon Sep 17 00:00:00 2001 From: Rafael mreis Date: Mon, 5 Oct 2026 11:53:20 -0300 Subject: [PATCH 01/14] governance: add bounded PAT capability router --- .../RLL_CAPABILITY_SECRET_ROUTER_V1.json | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 data/governance/RLL_CAPABILITY_SECRET_ROUTER_V1.json diff --git a/data/governance/RLL_CAPABILITY_SECRET_ROUTER_V1.json b/data/governance/RLL_CAPABILITY_SECRET_ROUTER_V1.json new file mode 100644 index 000000000..b42df46ff --- /dev/null +++ b/data/governance/RLL_CAPABILITY_SECRET_ROUTER_V1.json @@ -0,0 +1,103 @@ +{ + "schema": "rll.capability_secret_router.v1", + "version": "1.0.0", + "date": "2026-10-05", + "canonical_repository": "instituto-Rafael/relativity-living-light", + "relationship_to_credential_authority": "SUPPLEMENTAL_OWNER_DECLARED_CAPABILITY_SURFACE; DOES_NOT_REPLACE_RLL_CREDENTIAL_AUTHORITY_POLICY_V1", + "claim_allowed": false, + "publication_effect": "NONE", + "secret_values_must_never_enter_repository": true, + "secret_values_must_never_enter_receipts": true, + "secret_values_must_never_be_hashed_for_COMPARISON": true, + "same_value_across_names": "TOKEN_VAZIO_EXTERNAL_SETTING_NOT_TESTED", + "profiles": { + "pat_actions": { + "canonical_secret_name": "PAT_ACTIONS", + "legacy_aliases": [], + "purpose": "manual GitHub read/acquisition capability for Actions-scoped source custody", + "runtime_presence": "TOKEN_VAZIO_UNTIL_MANUAL_DISPATCH" + }, + "pat_agents": { + "canonical_secret_name": "PAT_AGENTS", + "legacy_aliases": [], + "purpose": "manual GitHub read/acquisition capability for agent-scoped source custody", + "runtime_presence": "TOKEN_VAZIO_UNTIL_MANUAL_DISPATCH" + }, + "pat_env": { + "canonical_secret_name": "PAT_ENV", + "legacy_aliases": [], + "purpose": "manual environment-scoped GitHub read/acquisition capability", + "runtime_presence": "TOKEN_VAZIO_UNTIL_MANUAL_DISPATCH" + }, + "pat_environments": { + "canonical_secret_name": "PAT_ENVIRONMENTS", + "legacy_aliases": [ + "PAT_ENVIOREMENTS" + ], + "purpose": "manual environments-scoped GitHub read/acquisition capability", + "runtime_presence": "TOKEN_VAZIO_UNTIL_MANUAL_DISPATCH", + "legacy_alias_policy": "canonical name wins; typo alias is fallback only and must never be copied into repository content" + } + }, + "workflow_boundary": { + "allowed_workflow": ".github/workflows/rll-authorial-science-fabric-v1.yml", + "allowed_event_for_secret_consumption": "workflow_dispatch_only", + "repository_permissions": { + "contents": "read" + }, + "allowed_remote_http_methods": [ + "GET" + ], + "credentialed_host": "api.github.com", + "credentialed_path_family": "/repos/{owner}/{repo}/contents/{path}", + "cross_repository_scope": "TOKEN_VAZIO_EXTERNAL_PAT_SETTING", + "write_authority": "NONE_IN_V1", + "delete_authority": "FORBIDDEN", + "force_push": "FORBIDDEN", + "git_ref_delete": "FORBIDDEN", + "shell_trace": "FORBIDDEN", + "environment_dump": "FORBIDDEN" + }, + "receipt_boundary": { + "allowed_fields": [ + "credential_profile", + "credential_present_boolean", + "source_url", + "artifact_sha256", + "artifact_bytes", + "authority", + "license_state", + "decision", + "residuals" + ], + "forbidden_fields": [ + "secret_value", + "secret_length", + "secret_hash", + "authorization_header", + "value_equivalence_between_secret_names" + ] + }, + "invariants": [ + "SOURCE != ARTIFACT != EXECUTION != EVIDENCE != CLAIM", + "TOKEN_VAZIO != 0", + "IMPLEMENTED_UNTESTED != PASS", + "PAT possession != repository authority", + "PAT possession != scientific evidence", + "hosted CI != freestanding runtime", + "Android NDK/JNI/SDK/R8/ART are factory or adapter surfaces, not freestanding runtime dependencies" + ], + "f_ok": [ + "canonical spelling PAT_ENVIRONMENTS established", + "legacy PAT_ENVIOREMENTS retained only as a non-persisted fallback binding", + "all four requested capability profiles are explicitly typed", + "V1 remote authority is read-only and delete-free" + ], + "f_gap": [ + "runtime presence of each secret", + "fine-grained PAT repository scopes and intrinsic permissions", + "whether different secret names contain the same PAT value", + "cross-repository provider enforcement" + ], + "f_next": "Run only the manual acquisition workflow for a manifest that needs a credentialed GitHub contents source; record presence boolean and source/artifact receipt without reading or comparing secret material." +} From 6759d13c7f968bf20480f56411d932cbd0aed1fe Mon Sep 17 00:00:00 2001 From: Rafael mreis Date: Mon, 5 Oct 2026 11:53:53 -0300 Subject: [PATCH 02/14] architecture: add authorial science fabric registry --- services/authorial_fabric/fabric.v1.json | 108 +++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 services/authorial_fabric/fabric.v1.json diff --git a/services/authorial_fabric/fabric.v1.json b/services/authorial_fabric/fabric.v1.json new file mode 100644 index 000000000..60e7eec01 --- /dev/null +++ b/services/authorial_fabric/fabric.v1.json @@ -0,0 +1,108 @@ +{ + "schema": "rll.authorial_fabric.v1", + "version": "1.0.0", + "date": "2026-10-05", + "authority": "instituto-Rafael/relativity-living-light", + "claim_allowed": false, + "design_rule": "ADAPTERS_PREPARE_OR_MEASURE; FREESTANDING_CORE_COMPUTES_WITHOUT_THEM", + "lanes": [ + { + "id": "freestanding_native", + "class": "EXECUTION_CORE", + "producer": "core/lowlevel_runtime", + "runtime_dependency": false, + "factory_dependencies_allowed": ["C compiler", "linker"], + "runtime_dependencies_forbidden": ["libc", "heap allocator", "Python", "JVM", "Android SDK", "Android NDK runtime", "JNI", "R8", "ART JIT", "external native libraries"], + "evidence_ceiling": "SOFTWARE_FREESTANDING_EXECUTION_RECEIPT" + }, + { + "id": "science_ingress", + "class": "SOURCE_CUSTODY_FACTORY", + "producer": "tools/rll_authorial_ingress.py", + "runtime_dependency": false, + "factory_dependencies_allowed": ["Python standard library", "HTTPS"], + "purpose": "bounded acquisition, hashing, provenance and receipt generation for images, PDFs, data, text and archives", + "evidence_ceiling": "SOURCE_AND_ARTIFACT_CUSTODY" + }, + { + "id": "github_actions", + "class": "HOSTED_ORCHESTRATOR", + "producer": ".github/workflows/rll-authorial-science-fabric-v1.yml", + "runtime_dependency": false, + "external_provider": true, + "purpose": "manual acquisition and CI contract validation", + "evidence_ceiling": "HOSTED_CI_ONLY" + }, + { + "id": "android_ndk", + "class": "FACTORY_TOOLCHAIN_ADAPTER", + "runtime_dependency": false, + "status": "ROUTED_NOT_REQUIRED_BY_CORE", + "evidence_ceiling": "ANDROID_NATIVE_BUILD_OR_BENCHMARK_ONLY" + }, + { + "id": "jni_bridge", + "class": "HOSTED_BRIDGE_ADAPTER", + "runtime_dependency": false, + "status": "ROUTED_NOT_REQUIRED_BY_CORE", + "evidence_ceiling": "JNI_BRIDGE_BEHAVIOR_ONLY" + }, + { + "id": "android_sdk_buildtools", + "class": "FACTORY_TOOLCHAIN_ADAPTER", + "runtime_dependency": false, + "status": "ROUTED_NOT_REQUIRED_BY_CORE", + "evidence_ceiling": "ANDROID_PACKAGE_BUILD_ONLY" + }, + { + "id": "r8_dex_optimizer", + "class": "FACTORY_TRANSFORM_ADAPTER", + "runtime_dependency": false, + "status": "ROUTED_NOT_REQUIRED_BY_CORE", + "evidence_ceiling": "DEX_TRANSFORM_ONLY" + }, + { + "id": "art_jit_aot", + "class": "HOSTED_RUNTIME_MEASUREMENT_ADAPTER", + "runtime_dependency": false, + "status": "ROUTED_NOT_REQUIRED_BY_CORE", + "evidence_ceiling": "ANDROID_RUNTIME_MEASUREMENT_ONLY" + }, + { + "id": "python_factory", + "class": "FACTORY_VALIDATION_ADAPTER", + "runtime_dependency": false, + "status": "STDLIB_ONLY_FOR_INGRESS_V1", + "evidence_ceiling": "FACTORY_VALIDATION_ONLY" + }, + { + "id": "physical_termux_native", + "class": "PHYSICAL_EVIDENCE_ADAPTER", + "runtime_dependency": false, + "status": "SEPARATE_GATE", + "evidence_ceiling": "PHYSICAL_USERSPACE_EXECUTION_ONLY" + }, + { + "id": "baremetal_firmware", + "class": "PHYSICAL_BARE_METAL_GATE", + "runtime_dependency": false, + "status": "TOKEN_VAZIO", + "evidence_ceiling": "PHYSICAL_BARE_METAL_ONLY" + } + ], + "source_classes": ["image", "pdf", "data", "text", "webpage", "archive"], + "artifact_policy": { + "repository_stores": ["manifest", "algorithm", "tests", "small receipts"], + "ci_artifact_stores": ["downloaded bytes", "hash inventory", "network receipt"], + "forbidden_promotion": "downloaded bytes do not become evidence or claim solely because download/hash succeeded" + }, + "boundaries": [ + "CI_PASS != PHYSICAL_PASS", + "PHYSICAL_USERSPACE_PASS != BAREMETAL_PASS", + "NDK_BUILD != FREESTANDING_RUNTIME", + "JNI_BRIDGE != NATIVE_CORE_AUTHORITY", + "SDK_OR_R8_TRANSFORM != RUNTIME_DEPENDENCY", + "SOURCE != ARTIFACT != EXECUTION != EVIDENCE != CLAIM" + ], + "next": "materialize one source manifest, pin observed hashes in a reviewed successor, then feed only normalized mathematical features into freestanding computation" +} From 6eb7228a9435d6441275adec37fd4daa35251b27 Mon Sep 17 00:00:00 2001 From: Rafael mreis Date: Mon, 5 Oct 2026 11:54:27 -0300 Subject: [PATCH 03/14] docs: define authorial fabric boundaries --- services/authorial_fabric/README.md | 65 +++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 services/authorial_fabric/README.md diff --git a/services/authorial_fabric/README.md b/services/authorial_fabric/README.md new file mode 100644 index 000000000..28dd493a1 --- /dev/null +++ b/services/authorial_fabric/README.md @@ -0,0 +1,65 @@ +# RLL Authorial Science Fabric V1 + +Status: `IMPLEMENTED_UNTESTED_CI` · `claim_allowed=false` + +This directory gives RLL one low-friction service boundary for scientific source custody, build/measurement adapters and the existing freestanding core. + +## Separation that must not collapse + +```text +SOURCE -> ARTIFACT -> EXECUTION -> EVIDENCE -> CLAIM + | + +-> factory/adapters: Python stdlib, CI, NDK, JNI, SDK, R8, ART/JIT + | + +-> never a runtime dependency of the freestanding ELF +``` + +The canonical freestanding computation remains under `core/lowlevel_runtime`. This fabric does not replace it and does not relabel Android userspace as bare metal. + +## Directory contract + +- `fabric.v1.json` — typed registry of execution/factory/evidence lanes. +- `manifests/` — source manifests. URLs, authority, rights state, byte bounds and expected SHA-256 live here. +- `tools/rll_authorial_ingress.py` — stdlib-only acquisition/custody tool. +- `.github/workflows/rll-authorial-science-fabric-v1.yml` — CI static gate plus manual network materialization. +- `data/governance/RLL_CAPABILITY_SECRET_ROUTER_V1.json` — bounded PAT aliases and non-destructive rules. + +Downloaded PDFs, images and datasets are not committed automatically. A manual acquisition run writes them to an ephemeral CI directory and publishes a GitHub Actions artifact together with a receipt. A hash learned for a previously unpinned source is `OBSERVED_UNPINNED`, not `PASS_PINNED`; promotion requires a reviewed successor manifest. + +## Credential profiles + +Canonical profiles are: + +- `PAT_ACTIONS` -> `pat_actions` +- `PAT_AGENTS` -> `pat_agents` +- `PAT_ENV` -> `pat_env` +- `PAT_ENVIRONMENTS` -> `pat_environments` + +`PAT_ENVIOREMENTS` is retained only as a legacy typo fallback for the last profile. Secret values are never printed, stored, hashed or compared. Whether these names contain the same PAT value is provider-side state and remains `TOKEN_VAZIO` here. + +V1 credentialed acquisition is deliberately narrower than the PAT's possible provider permissions: **GET-only GitHub Contents API**. No push, POST, PUT, PATCH, DELETE, ref deletion or force-push is implemented. + +## Internet/source model + +Public HTTPS sources need no PAT. Credential profiles are for private/cross-repository GitHub source custody only. Each materialized byte stream gets SHA-256, byte count, authority and rights state in the receipt. + +For dynamic/unpinned public sources, use `--allow-unpinned` only for discovery. The next reviewed delta should pin the observed digest if the source is appropriate and legally usable. + +## Voynich seed + +`manifests/voynich_geometry.discovery.v1.json` starts the angular/graph experiment from the Beinecke MS 408 catalog record. It intentionally does **not** claim a high-resolution image endpoint, image rights, folio hashes or a decipherment. Those unresolved items remain `TOKEN_VAZIO` until resolved from the primary collection source. + +## Freestanding meaning + +`freestanding_native` means the execution core can be built and run without libc, heap allocation, Python/JVM/Android runtime and external native libraries. Compiler/linker/CI are factory tools, not runtime dependencies. NDK/JNI/SDK/R8/ART lanes may benchmark or bridge the same mathematics but do not redefine the core. + +## Success criterion V1 + +1. static manifest audit PASS; +2. stdlib unit tests PASS; +3. credential-bearing jobs manual-only and non-destructive; +4. manual materialization emits artifact + receipt without secret material; +5. pinned source bytes must match SHA-256 exactly; +6. scientific claims remain disabled until a separate evidence gate exists. + +Rollback: close/revert only this feature branch/PR. No history rewrite and no deletion of prior receipts. From a02eefe98d64b761af45c0483af3a2f8ea14e015 Mon Sep 17 00:00:00 2001 From: Rafael mreis Date: Mon, 5 Oct 2026 11:54:51 -0300 Subject: [PATCH 04/14] science: add Voynich geometry discovery manifest --- .../voynich_geometry.discovery.v1.json | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 services/authorial_fabric/manifests/voynich_geometry.discovery.v1.json diff --git a/services/authorial_fabric/manifests/voynich_geometry.discovery.v1.json b/services/authorial_fabric/manifests/voynich_geometry.discovery.v1.json new file mode 100644 index 000000000..3d1658afd --- /dev/null +++ b/services/authorial_fabric/manifests/voynich_geometry.discovery.v1.json @@ -0,0 +1,38 @@ +{ + "schema": "rll.science_ingress_manifest.v1", + "version": "1.0.0", + "date": "2026-10-05", + "purpose": "Voynich geometric/angular hypothesis source discovery; no decipherment claim", + "claim_allowed": false, + "hypothesis": "page/diagram orientation, radius and graph adjacency may contain relational information beyond linear text order", + "null_hypothesis": "orientation is explained by layout and scribal ergonomics without inter-page coding", + "sources": [ + { + "id": "voynich_yale_ms408_catalog", + "kind": "webpage", + "url": "https://pre1600ms.beinecke.library.yale.edu/docs/pre1600.ms408.htm", + "filename": "voynich_yale_ms408_catalog.html", + "authority": "Yale University, Beinecke Rare Book and Manuscript Library", + "license": "TOKEN_VAZIO_RIGHTS_REVIEW_REQUIRED_FOR_DERIVATIVE_BULK_USE", + "expected_sha256": "TOKEN_VAZIO", + "max_bytes": 2000000, + "credential_profile": "none", + "role": "PRIMARY_SOURCE_LOCATOR_AND_STRUCTURAL_METADATA" + } + ], + "observed_primary_structure": { + "ms": "Beinecke MS 408", + "foldout_note": "catalog describes multiple double/triple/quadruple foldouts and one sextuple-folio folding leaf", + "astronomical_section": "ff. 67r-73v; circular/concentric/radiating diagrams", + "nine_medallion_section": "ff. 85r-86v; sextuple-folio folding leaf with nine linked medallions", + "source_state": "PRIMARY_CATALOG_OBSERVED_2026-10-05" + }, + "source_resolution_gaps": [ + "TOKEN_VAZIO_DIRECT_CURRENT_HIGH_RES_IMAGE_ENDPOINTS", + "TOKEN_VAZIO_FOLIO_LEVEL_SHA256", + "TOKEN_VAZIO_IMAGE_REUSE_RIGHTS_FOR_BULK_DERIVATIVES", + "TOKEN_VAZIO_CANONICAL_FOLIO_ORIENTATION_COORDINATES", + "TOKEN_VAZIO_TRANSCRIPTION_ALIGNMENT_TO_PIXEL_COORDINATES" + ], + "experiment_next": "resolve authoritative folio image endpoints; acquire without geometric preprocessing; pin hashes; preregister coordinate/orientation extraction before testing cross-folio rotations" +} From c2e33d08aa06add6d1ec259777e36ef1de73b079 Mon Sep 17 00:00:00 2001 From: Rafael mreis Date: Mon, 5 Oct 2026 11:55:45 -0300 Subject: [PATCH 05/14] tools: add stdlib-only bounded science ingress --- tools/rll_authorial_ingress.py | 330 +++++++++++++++++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 tools/rll_authorial_ingress.py diff --git a/tools/rll_authorial_ingress.py b/tools/rll_authorial_ingress.py new file mode 100644 index 000000000..5b4a678c4 --- /dev/null +++ b/tools/rll_authorial_ingress.py @@ -0,0 +1,330 @@ +#!/usr/bin/env python3 +"""RLL authorial science ingress: stdlib-only source custody/materialization. + +Factory/tooling boundary only. This module is not part of the freestanding runtime. +It never promotes downloaded bytes to scientific evidence or a claim by itself. +""" +from __future__ import annotations + +import argparse +import base64 +import hashlib +import json +import os +import re +import urllib.parse +import urllib.request +from pathlib import Path +from typing import Any + +TOKEN_VAZIO = "TOKEN_VAZIO" +SCHEMA = "rll.science_ingress_manifest.v1" +ALLOWED_KINDS = {"image", "pdf", "data", "text", "webpage", "archive"} +ALLOWED_CREDENTIALS = { + "none", + "pat_actions", + "pat_agents", + "pat_env", + "pat_environments", +} +GITHUB_API_HOST = "api.github.com" +SHA256_RE = re.compile(r"^[0-9a-f]{64}$") +SAFE_NAME_RE = re.compile(r"^[A-Za-z0-9._-]+$") + + +def load_json(path: Path) -> dict[str, Any]: + obj = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(obj, dict): + raise ValueError("top-level JSON must be an object") + return obj + + +def validate_manifest(doc: dict[str, Any]) -> list[str]: + errors: list[str] = [] + if doc.get("schema") != SCHEMA: + errors.append(f"schema must be {SCHEMA}") + sources = doc.get("sources") + if not isinstance(sources, list) or not sources: + errors.append("sources must be a non-empty list") + return errors + seen: set[str] = set() + for idx, source in enumerate(sources): + prefix = f"sources[{idx}]" + if not isinstance(source, dict): + errors.append(f"{prefix}: must be object") + continue + sid = source.get("id") + if not isinstance(sid, str) or not sid: + errors.append(f"{prefix}.id: required string") + elif sid in seen: + errors.append(f"{prefix}.id: duplicate {sid}") + else: + seen.add(sid) + kind = source.get("kind") + if kind not in ALLOWED_KINDS: + errors.append(f"{prefix}.kind: invalid {kind!r}") + url = source.get("url") + if not isinstance(url, str): + errors.append(f"{prefix}.url: required string") + else: + parsed = urllib.parse.urlparse(url) + if parsed.scheme != "https" or not parsed.netloc: + errors.append(f"{prefix}.url: only absolute https URLs are allowed") + filename = source.get("filename") + if not isinstance(filename, str) or not SAFE_NAME_RE.fullmatch(filename): + errors.append(f"{prefix}.filename: unsafe or missing") + expected = source.get("expected_sha256") + if expected != TOKEN_VAZIO and not ( + isinstance(expected, str) and SHA256_RE.fullmatch(expected) + ): + errors.append( + f"{prefix}.expected_sha256: must be lowercase sha256 or TOKEN_VAZIO" + ) + max_bytes = source.get("max_bytes") + if ( + not isinstance(max_bytes, int) + or isinstance(max_bytes, bool) + or not (1 <= max_bytes <= 2_147_483_648) + ): + errors.append(f"{prefix}.max_bytes: integer 1..2147483648 required") + credential = source.get("credential_profile", "none") + if credential not in ALLOWED_CREDENTIALS: + errors.append(f"{prefix}.credential_profile: invalid {credential!r}") + if credential != "none": + parsed = urllib.parse.urlparse(str(url)) + if parsed.hostname != GITHUB_API_HOST: + errors.append(f"{prefix}: PAT profiles are restricted to api.github.com") + if "/contents/" not in parsed.path: + errors.append( + f"{prefix}: PAT profiles require GitHub contents API URL" + ) + authority = source.get("authority") + if not isinstance(authority, str) or not authority: + errors.append(f"{prefix}.authority: required") + license_state = source.get("license") + if not isinstance(license_state, str) or not license_state: + errors.append(f"{prefix}.license: required; TOKEN_VAZIO is valid") + return errors + + +def _read_bounded(response: Any, max_bytes: int) -> bytes: + data = response.read(max_bytes + 1) + if len(data) > max_bytes: + raise ValueError(f"source exceeds max_bytes={max_bytes}") + return data + + +def _decode_github_contents(payload: bytes) -> bytes: + doc = json.loads(payload.decode("utf-8")) + if ( + not isinstance(doc, dict) + or doc.get("encoding") != "base64" + or not isinstance(doc.get("content"), str) + ): + raise ValueError("GitHub contents response must contain base64 content") + return base64.b64decode(doc["content"], validate=False) + + +def _kind_check(kind: str, data: bytes) -> bool: + if kind == "pdf": + return data.startswith(b"%PDF-") + if kind == "image": + return ( + data.startswith(b"\x89PNG\r\n\x1a\n") + or data.startswith(b"\xff\xd8\xff") + or data.startswith((b"GIF87a", b"GIF89a")) + ) + if kind == "data": + if not data: + return False + try: + json.loads(data.decode("utf-8")) + return True + except Exception: # noqa: BLE001 + return b"," in data or b"\t" in data or b"\n" in data + if kind in {"text", "webpage"}: + try: + data.decode("utf-8") + return True + except UnicodeDecodeError: + return False + if kind == "archive": + return data.startswith((b"PK\x03\x04", b"\x1f\x8b")) + return False + + +def acquire_source( + source: dict[str, Any], + out_dir: Path, + token: str | None, + allow_unpinned: bool, +) -> dict[str, Any]: + expected = source["expected_sha256"] + if expected == TOKEN_VAZIO and not allow_unpinned: + raise ValueError( + f"{source['id']}: expected_sha256 is TOKEN_VAZIO; " + "pass --allow-unpinned only for discovery" + ) + credential = source.get("credential_profile", "none") + url = source["url"] + headers = {"User-Agent": "RLL-authorial-science-ingress/1"} + if credential != "none": + if not token: + raise ValueError(f"{source['id']}: selected credential profile is absent") + parsed = urllib.parse.urlparse(url) + if parsed.hostname != GITHUB_API_HOST or "/contents/" not in parsed.path: + raise ValueError( + f"{source['id']}: credentialed URL is outside bounded GitHub contents API" + ) + headers["Authorization"] = f"Bearer {token}" + headers["Accept"] = "application/vnd.github+json" + + request = urllib.request.Request(url, headers=headers, method="GET") + with urllib.request.urlopen(request, timeout=30) as response: + final_url = response.geturl() + parsed_final = urllib.parse.urlparse(final_url) + if parsed_final.scheme != "https": + raise ValueError(f"{source['id']}: redirect left https") + payload = _read_bounded(response, int(source["max_bytes"])) + data = _decode_github_contents(payload) if credential != "none" else payload + + if len(data) > int(source["max_bytes"]): + raise ValueError(f"{source['id']}: decoded source exceeds max_bytes") + if not _kind_check(source["kind"], data): + raise ValueError( + f"{source['id']}: kind signature check failed for {source['kind']}" + ) + digest = hashlib.sha256(data).hexdigest() + pinned = expected != TOKEN_VAZIO + if pinned and digest != expected: + raise ValueError(f"{source['id']}: sha256 mismatch") + + target = out_dir / source["filename"] + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(data) + return { + "id": source["id"], + "filename": source["filename"], + "bytes": len(data), + "sha256": digest, + "expected_sha256": expected, + "hash_state": "VERIFIED_PINNED" if pinned else "OBSERVED_UNPINNED", + "credential_profile": credential, + "credential_present_boolean": bool(token) if credential != "none" else False, + "source_url": url, + "authority": source["authority"], + "license": source["license"], + } + + +def audit_payload( + manifest_path: Path, doc: dict[str, Any], errors: list[str] +) -> dict[str, Any]: + return { + "schema": "rll.science_ingress.audit.v1", + "manifest": manifest_path.as_posix(), + "decision": "PASS_STATIC" if not errors else "FAIL", + "claim_allowed": False, + "source_artifact_execution_evidence_claim_separated": True, + "errors": errors, + "source_count": ( + len(doc.get("sources", [])) if isinstance(doc.get("sources"), list) else 0 + ), + } + + +def cmd_audit(args: argparse.Namespace) -> int: + doc = load_json(args.manifest) + errors = validate_manifest(doc) + payload = audit_payload(args.manifest, doc, errors) + print(json.dumps(payload, ensure_ascii=False, indent=2)) + return 0 if not errors else 2 + + +def cmd_materialize(args: argparse.Namespace) -> int: + doc = load_json(args.manifest) + errors = validate_manifest(doc) + receipt: dict[str, Any] = { + "schema": "rll.science_ingress.receipt.v1", + "manifest": args.manifest.as_posix(), + "claim_allowed": False, + "scientific_confirmation": False, + "network_execution": True, + "results": [], + "errors": list(errors), + } + if errors: + receipt["decision"] = "FAIL_STATIC" + args.receipt.parent.mkdir(parents=True, exist_ok=True) + args.receipt.write_text( + json.dumps(receipt, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + return 2 + + profile = args.credential_profile + token = os.environ.get("RLL_INGRESS_TOKEN") if profile != "none" else None + try: + for source in doc["sources"]: + source_profile = source.get("credential_profile", "none") + if source_profile != profile and source_profile != "none": + raise ValueError( + f"{source['id']}: manifest credential_profile={source_profile} " + f"does not match selected profile={profile}" + ) + source_token = token if source_profile != "none" else None + receipt["results"].append( + acquire_source(source, args.out, source_token, args.allow_unpinned) + ) + except Exception as exc: # noqa: BLE001 + receipt["errors"].append(str(exc)) + receipt["decision"] = "FAIL" + else: + states = {item["hash_state"] for item in receipt["results"]} + receipt["decision"] = ( + "PASS_PINNED" if states == {"VERIFIED_PINNED"} else "OBSERVED_UNPINNED" + ) + receipt["credential_profile_selected"] = profile + receipt["credential_value_observed"] = False + receipt["credential_value_hashed"] = False + args.receipt.parent.mkdir(parents=True, exist_ok=True) + args.receipt.write_text( + json.dumps(receipt, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + print( + json.dumps( + {"decision": receipt["decision"], "receipt": args.receipt.as_posix()}, + ensure_ascii=False, + ) + ) + return 0 if receipt["decision"] in {"PASS_PINNED", "OBSERVED_UNPINNED"} else 3 + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser() + sub = parser.add_subparsers(dest="command", required=True) + audit = sub.add_parser("audit") + audit.add_argument("manifest", type=Path) + audit.set_defaults(func=cmd_audit) + materialize = sub.add_parser("materialize") + materialize.add_argument("manifest", type=Path) + materialize.add_argument("--out", type=Path, required=True) + materialize.add_argument("--receipt", type=Path, required=True) + materialize.add_argument( + "--credential-profile", + choices=sorted(ALLOWED_CREDENTIALS), + default="none", + ) + materialize.add_argument("--allow-unpinned", action="store_true") + materialize.set_defaults(func=cmd_materialize) + return parser + + +def main() -> int: + args = build_parser().parse_args() + return args.func(args) + + +if __name__ == "__main__": + raise SystemExit(main()) From 0db78488e692b6d5318724f9d0b84c48e8098fe5 Mon Sep 17 00:00:00 2001 From: Rafael mreis Date: Mon, 5 Oct 2026 11:56:03 -0300 Subject: [PATCH 06/14] test: add authorial ingress falsifiers --- tests/test_rll_authorial_ingress.py | 67 +++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 tests/test_rll_authorial_ingress.py diff --git a/tests/test_rll_authorial_ingress.py b/tests/test_rll_authorial_ingress.py new file mode 100644 index 000000000..495772244 --- /dev/null +++ b/tests/test_rll_authorial_ingress.py @@ -0,0 +1,67 @@ +import importlib.util +import tempfile +import unittest +from pathlib import Path + +MODULE = Path(__file__).resolve().parents[1] / "tools" / "rll_authorial_ingress.py" +spec = importlib.util.spec_from_file_location("rll_authorial_ingress", MODULE) +mod = importlib.util.module_from_spec(spec) +assert spec.loader is not None +spec.loader.exec_module(mod) + + +class TestRllAuthorialIngress(unittest.TestCase): + def good(self): + return { + "schema": mod.SCHEMA, + "sources": [ + { + "id": "x", + "kind": "pdf", + "url": "https://example.org/x.pdf", + "filename": "x.pdf", + "expected_sha256": mod.TOKEN_VAZIO, + "max_bytes": 1024, + "credential_profile": "none", + "authority": "example", + "license": mod.TOKEN_VAZIO, + } + ], + } + + def test_good_manifest(self): + self.assertEqual(mod.validate_manifest(self.good()), []) + + def test_http_rejected(self): + doc = self.good() + doc["sources"][0]["url"] = "http://example.org/x.pdf" + self.assertTrue(any("https" in error for error in mod.validate_manifest(doc))) + + def test_bad_hash_rejected(self): + doc = self.good() + doc["sources"][0]["expected_sha256"] = "abc" + self.assertTrue( + any("expected_sha256" in error for error in mod.validate_manifest(doc)) + ) + + def test_pat_is_github_contents_only(self): + doc = self.good() + doc["sources"][0]["credential_profile"] = "pat_actions" + self.assertTrue( + any("api.github.com" in error for error in mod.validate_manifest(doc)) + ) + + def test_kind_signatures(self): + self.assertTrue(mod._kind_check("pdf", b"%PDF-1.7\n")) + self.assertTrue(mod._kind_check("image", b"\x89PNG\r\n\x1a\nabc")) + self.assertFalse(mod._kind_check("pdf", b"not a pdf")) + + def test_token_vazio_requires_explicit_discovery_flag(self): + source = self.good()["sources"][0] + with tempfile.TemporaryDirectory() as temp_dir: + with self.assertRaises(ValueError): + mod.acquire_source(source, Path(temp_dir), None, False) + + +if __name__ == "__main__": + unittest.main() From 58675991571fb3c40c4ca5b8ce93ec30952f25d0 Mon Sep 17 00:00:00 2001 From: Rafael mreis Date: Mon, 5 Oct 2026 11:56:44 -0300 Subject: [PATCH 07/14] test: guard PAT capability router --- tests/test_rll_capability_secret_router.py | 67 ++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 tests/test_rll_capability_secret_router.py diff --git a/tests/test_rll_capability_secret_router.py b/tests/test_rll_capability_secret_router.py new file mode 100644 index 000000000..9352ca162 --- /dev/null +++ b/tests/test_rll_capability_secret_router.py @@ -0,0 +1,67 @@ +import json +import re +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +POLICY = ROOT / "data/governance/RLL_CAPABILITY_SECRET_ROUTER_V1.json" +WORKFLOW = ROOT / ".github/workflows/rll-authorial-science-fabric-v1.yml" + + +class TestRllCapabilitySecretRouter(unittest.TestCase): + def setUp(self): + self.policy = json.loads(POLICY.read_text(encoding="utf-8")) + self.workflow = WORKFLOW.read_text(encoding="utf-8") + + def test_canonical_profiles_and_legacy_alias(self): + profiles = self.policy["profiles"] + self.assertEqual(profiles["pat_actions"]["canonical_secret_name"], "PAT_ACTIONS") + self.assertEqual(profiles["pat_agents"]["canonical_secret_name"], "PAT_AGENTS") + self.assertEqual(profiles["pat_env"]["canonical_secret_name"], "PAT_ENV") + self.assertEqual( + profiles["pat_environments"]["canonical_secret_name"], + "PAT_ENVIRONMENTS", + ) + self.assertEqual( + profiles["pat_environments"]["legacy_aliases"], + ["PAT_ENVIOREMENTS"], + ) + + def test_secret_references_are_explicit(self): + for name in [ + "PAT_ACTIONS", + "PAT_AGENTS", + "PAT_ENV", + "PAT_ENVIRONMENTS", + "PAT_ENVIOREMENTS", + ]: + self.assertIn(f"secrets.{name}", self.workflow) + + def test_secret_consumption_is_manual_only(self): + self.assertNotIn("pull_request_target", self.workflow) + self.assertIn("github.event_name == 'workflow_dispatch'", self.workflow) + self.assertEqual( + self.policy["workflow_boundary"]["allowed_event_for_secret_consumption"], + "workflow_dispatch_only", + ) + + def test_no_remote_mutation_or_secret_dump(self): + forbidden = [ + r"\bgit\s+push\b", + r"\bprintenv\b", + r"\bset\s+-x\b", + r"(?:-X|--request|--method)\s*(?:POST|PUT|PATCH|DELETE)\b", + ] + for pattern in forbidden: + self.assertIsNone(re.search(pattern, self.workflow, re.IGNORECASE)) + + def test_value_equivalence_is_not_claimed(self): + self.assertEqual( + self.policy["same_value_across_names"], + "TOKEN_VAZIO_EXTERNAL_SETTING_NOT_TESTED", + ) + self.assertFalse(self.policy["claim_allowed"]) + + +if __name__ == "__main__": + unittest.main() From bf52ec8f09b57cc047b39a3681fc66a87315252a Mon Sep 17 00:00:00 2001 From: Rafael mreis Date: Mon, 5 Oct 2026 11:57:55 -0300 Subject: [PATCH 08/14] ci: add manual authorial science fabric gate --- .../rll-authorial-science-fabric-v1.yml | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 .github/workflows/rll-authorial-science-fabric-v1.yml diff --git a/.github/workflows/rll-authorial-science-fabric-v1.yml b/.github/workflows/rll-authorial-science-fabric-v1.yml new file mode 100644 index 000000000..56772f382 --- /dev/null +++ b/.github/workflows/rll-authorial-science-fabric-v1.yml @@ -0,0 +1,217 @@ +name: RLL Authorial Science Fabric V1 + +"on": + pull_request: + branches: + - rll/lab + paths: + - "services/authorial_fabric/**" + - "tools/rll_authorial_ingress.py" + - "tests/test_rll_authorial_ingress.py" + - "tests/test_rll_capability_secret_router.py" + - "data/governance/RLL_CAPABILITY_SECRET_ROUTER_V1.json" + - ".github/workflows/rll-authorial-science-fabric-v1.yml" + workflow_dispatch: + inputs: + manifest: + description: "Manifest under services/authorial_fabric/manifests/" + required: true + default: "services/authorial_fabric/manifests/voynich_geometry.discovery.v1.json" + type: string + credential_profile: + description: "Optional bounded GitHub read credential profile" + required: true + default: none + type: choice + options: + - none + - pat_actions + - pat_agents + - pat_env + - pat_environments + allow_unpinned: + description: "Discovery only: permit TOKEN_VAZIO expected hash; result is OBSERVED_UNPINNED, never PASS_PINNED" + required: true + default: false + type: boolean + +permissions: + contents: read + +concurrency: + group: rll-authorial-science-fabric-${{ github.ref }} + cancel-in-progress: false + +jobs: + contract: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout exact source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Audit bundled discovery manifest + run: | + set -euo pipefail + mkdir -p artifacts/authorial-fabric-contract + python3 tools/rll_authorial_ingress.py audit \ + services/authorial_fabric/manifests/voynich_geometry.discovery.v1.json \ + | tee artifacts/authorial-fabric-contract/manifest-audit.json + + - name: Execute stdlib falsifiers + run: | + set -euo pipefail + python3 -m unittest \ + tests.test_rll_authorial_ingress \ + tests.test_rll_capability_secret_router -v \ + 2>&1 | tee artifacts/authorial-fabric-contract/unittest.log + + - name: Validate fabric JSON contracts + run: | + set -euo pipefail + python3 - <<'PY' | tee artifacts/authorial-fabric-contract/contract-check.json + import json + from pathlib import Path + + fabric = json.loads(Path('services/authorial_fabric/fabric.v1.json').read_text()) + policy = json.loads(Path('data/governance/RLL_CAPABILITY_SECRET_ROUTER_V1.json').read_text()) + assert fabric['schema'] == 'rll.authorial_fabric.v1' + assert fabric['claim_allowed'] is False + assert policy['schema'] == 'rll.capability_secret_router.v1' + assert policy['claim_allowed'] is False + assert policy['workflow_boundary']['write_authority'] == 'NONE_IN_V1' + assert policy['workflow_boundary']['delete_authority'] == 'FORBIDDEN' + assert policy['same_value_across_names'] == 'TOKEN_VAZIO_EXTERNAL_SETTING_NOT_TESTED' + lanes = {item['id']: item for item in fabric['lanes']} + assert lanes['freestanding_native']['class'] == 'EXECUTION_CORE' + for adapter in ['science_ingress', 'github_actions', 'android_ndk', 'jni_bridge', 'android_sdk_buildtools', 'r8_dex_optimizer', 'art_jit_aot', 'python_factory']: + assert lanes[adapter]['runtime_dependency'] is False + print(json.dumps({'decision': 'PASS_STATIC', 'claim_allowed': False, 'lane_count': len(lanes)}, indent=2)) + PY + + - name: Upload contract evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: rll-authorial-fabric-contract-${{ github.run_id }} + path: artifacts/authorial-fabric-contract/ + retention-days: 14 + if-no-files-found: error + + acquire: + if: ${{ github.event_name == 'workflow_dispatch' }} + needs: contract + runs-on: ubuntu-24.04 + timeout-minutes: 20 + env: + MANIFEST: ${{ inputs.manifest }} + ALLOW_UNPINNED: ${{ inputs.allow_unpinned }} + RUN_DIR: artifacts/authorial-fabric-network + steps: + - name: Checkout exact source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Bound manifest path and initialize receipt area + run: | + set -euo pipefail + case "$MANIFEST" in + services/authorial_fabric/manifests/*.json) ;; + *) echo "manifest must remain under services/authorial_fabric/manifests/" >&2; exit 2 ;; + esac + test -f "$MANIFEST" + mkdir -p "$RUN_DIR/downloads" "$RUN_DIR/receipts" + printf '%s\n' "${GITHUB_SHA}" > "$RUN_DIR/source-head.txt" + printf '%s\n' "${{ inputs.credential_profile }}" > "$RUN_DIR/credential-profile.txt" + + - name: Materialize public HTTPS sources + if: ${{ inputs.credential_profile == 'none' }} + run: | + set -euo pipefail + EXTRA="" + if [ "$ALLOW_UNPINNED" = "true" ]; then EXTRA="--allow-unpinned"; fi + python3 tools/rll_authorial_ingress.py materialize "$MANIFEST" \ + --out "$RUN_DIR/downloads" \ + --receipt "$RUN_DIR/receipts/network-receipt.json" \ + --credential-profile none $EXTRA + + - name: Materialize with PAT_ACTIONS + if: ${{ inputs.credential_profile == 'pat_actions' }} + env: + CAPABILITY_PAT: ${{ secrets.PAT_ACTIONS }} + run: | + set -euo pipefail + if [ -z "$CAPABILITY_PAT" ]; then + printf '%s\n' '{"decision":"FAIL_CLOSED","gap":"TOKEN_VAZIO_PAT_ACTIONS_RUNTIME_BINDING","claim_allowed":false}' > "$RUN_DIR/receipts/network-receipt.json" + exit 4 + fi + EXTRA=""; if [ "$ALLOW_UNPINNED" = "true" ]; then EXTRA="--allow-unpinned"; fi + export RLL_INGRESS_TOKEN="$CAPABILITY_PAT" + python3 tools/rll_authorial_ingress.py materialize "$MANIFEST" --out "$RUN_DIR/downloads" --receipt "$RUN_DIR/receipts/network-receipt.json" --credential-profile pat_actions $EXTRA + unset RLL_INGRESS_TOKEN CAPABILITY_PAT + + - name: Materialize with PAT_AGENTS + if: ${{ inputs.credential_profile == 'pat_agents' }} + env: + CAPABILITY_PAT: ${{ secrets.PAT_AGENTS }} + run: | + set -euo pipefail + if [ -z "$CAPABILITY_PAT" ]; then + printf '%s\n' '{"decision":"FAIL_CLOSED","gap":"TOKEN_VAZIO_PAT_AGENTS_RUNTIME_BINDING","claim_allowed":false}' > "$RUN_DIR/receipts/network-receipt.json" + exit 4 + fi + EXTRA=""; if [ "$ALLOW_UNPINNED" = "true" ]; then EXTRA="--allow-unpinned"; fi + export RLL_INGRESS_TOKEN="$CAPABILITY_PAT" + python3 tools/rll_authorial_ingress.py materialize "$MANIFEST" --out "$RUN_DIR/downloads" --receipt "$RUN_DIR/receipts/network-receipt.json" --credential-profile pat_agents $EXTRA + unset RLL_INGRESS_TOKEN CAPABILITY_PAT + + - name: Materialize with PAT_ENV + if: ${{ inputs.credential_profile == 'pat_env' }} + env: + CAPABILITY_PAT: ${{ secrets.PAT_ENV }} + run: | + set -euo pipefail + if [ -z "$CAPABILITY_PAT" ]; then + printf '%s\n' '{"decision":"FAIL_CLOSED","gap":"TOKEN_VAZIO_PAT_ENV_RUNTIME_BINDING","claim_allowed":false}' > "$RUN_DIR/receipts/network-receipt.json" + exit 4 + fi + EXTRA=""; if [ "$ALLOW_UNPINNED" = "true" ]; then EXTRA="--allow-unpinned"; fi + export RLL_INGRESS_TOKEN="$CAPABILITY_PAT" + python3 tools/rll_authorial_ingress.py materialize "$MANIFEST" --out "$RUN_DIR/downloads" --receipt "$RUN_DIR/receipts/network-receipt.json" --credential-profile pat_env $EXTRA + unset RLL_INGRESS_TOKEN CAPABILITY_PAT + + - name: Materialize with PAT_ENVIRONMENTS and legacy typo fallback + if: ${{ inputs.credential_profile == 'pat_environments' }} + env: + CAPABILITY_PAT_CANONICAL: ${{ secrets.PAT_ENVIRONMENTS }} + CAPABILITY_PAT_LEGACY: ${{ secrets.PAT_ENVIOREMENTS }} + run: | + set -euo pipefail + CAPABILITY_PAT="$CAPABILITY_PAT_CANONICAL" + if [ -z "$CAPABILITY_PAT" ]; then CAPABILITY_PAT="$CAPABILITY_PAT_LEGACY"; fi + if [ -z "$CAPABILITY_PAT" ]; then + printf '%s\n' '{"decision":"FAIL_CLOSED","gap":"TOKEN_VAZIO_PAT_ENVIRONMENTS_RUNTIME_BINDING","claim_allowed":false}' > "$RUN_DIR/receipts/network-receipt.json" + exit 4 + fi + EXTRA=""; if [ "$ALLOW_UNPINNED" = "true" ]; then EXTRA="--allow-unpinned"; fi + export RLL_INGRESS_TOKEN="$CAPABILITY_PAT" + python3 tools/rll_authorial_ingress.py materialize "$MANIFEST" --out "$RUN_DIR/downloads" --receipt "$RUN_DIR/receipts/network-receipt.json" --credential-profile pat_environments $EXTRA + unset RLL_INGRESS_TOKEN CAPABILITY_PAT CAPABILITY_PAT_CANONICAL CAPABILITY_PAT_LEGACY + + - name: Build artifact hash inventory + if: always() + run: | + set -euo pipefail + find "$RUN_DIR" -type f -print0 | sort -z | xargs -0 sha256sum > "$RUN_DIR/receipts/SHA256SUMS.txt" + + - name: Upload source custody bundle + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: rll-authorial-science-ingress-${{ github.run_id }} + path: artifacts/authorial-fabric-network/ + retention-days: 14 + if-no-files-found: error From 784cc3d5817e5748829ed96fe0b6bc5ee8b402c8 Mon Sep 17 00:00:00 2001 From: Rafael mreis Date: Mon, 5 Oct 2026 12:00:32 -0300 Subject: [PATCH 09/14] ci: run authorial fabric tests without package assumption --- .github/workflows/rll-authorial-science-fabric-v1.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/rll-authorial-science-fabric-v1.yml b/.github/workflows/rll-authorial-science-fabric-v1.yml index 56772f382..8d288576f 100644 --- a/.github/workflows/rll-authorial-science-fabric-v1.yml +++ b/.github/workflows/rll-authorial-science-fabric-v1.yml @@ -63,10 +63,10 @@ jobs: - name: Execute stdlib falsifiers run: | set -euo pipefail - python3 -m unittest \ - tests.test_rll_authorial_ingress \ - tests.test_rll_capability_secret_router -v \ - 2>&1 | tee artifacts/authorial-fabric-contract/unittest.log + { + python3 tests/test_rll_authorial_ingress.py -v + python3 tests/test_rll_capability_secret_router.py -v + } 2>&1 | tee artifacts/authorial-fabric-contract/unittest.log - name: Validate fabric JSON contracts run: | From 012810a8d1e5bbfbd5e5d07cf04fb59dff37929f Mon Sep 17 00:00:00 2001 From: Rafael mreis Date: Mon, 5 Oct 2026 12:00:56 -0300 Subject: [PATCH 10/14] schema: add reconstructible science ingress manifest contract --- .../source_manifest.schema.json | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 services/authorial_fabric/source_manifest.schema.json diff --git a/services/authorial_fabric/source_manifest.schema.json b/services/authorial_fabric/source_manifest.schema.json new file mode 100644 index 000000000..d88d2025a --- /dev/null +++ b/services/authorial_fabric/source_manifest.schema.json @@ -0,0 +1,63 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "rll.science_ingress_manifest.v1", + "title": "RLL Science Ingress Manifest V1", + "type": "object", + "required": ["schema", "version", "claim_allowed", "sources"], + "properties": { + "schema": {"const": "rll.science_ingress_manifest.v1"}, + "version": {"type": "string"}, + "claim_allowed": {"const": false}, + "sources": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "id", + "kind", + "url", + "filename", + "authority", + "license", + "expected_sha256", + "max_bytes", + "credential_profile" + ], + "properties": { + "id": {"type": "string", "minLength": 1}, + "kind": { + "enum": ["image", "pdf", "data", "text", "webpage", "archive"] + }, + "url": {"type": "string", "pattern": "^https://"}, + "filename": {"type": "string", "pattern": "^[A-Za-z0-9._-]+$"}, + "authority": {"type": "string", "minLength": 1}, + "license": {"type": "string", "minLength": 1}, + "expected_sha256": { + "oneOf": [ + {"const": "TOKEN_VAZIO"}, + {"type": "string", "pattern": "^[0-9a-f]{64}$"} + ] + }, + "max_bytes": { + "type": "integer", + "minimum": 1, + "maximum": 2147483648 + }, + "credential_profile": { + "enum": [ + "none", + "pat_actions", + "pat_agents", + "pat_env", + "pat_environments" + ] + }, + "role": {"type": "string"} + }, + "additionalProperties": true + } + } + }, + "additionalProperties": true +} From d401ebfecafdda744da882352f0b0bf87d3f3e82 Mon Sep 17 00:00:00 2001 From: Rafael mreis Date: Mon, 5 Oct 2026 12:02:13 -0300 Subject: [PATCH 11/14] docs: define authorial fabric receipt custody --- services/authorial_fabric/receipts/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 services/authorial_fabric/receipts/README.md diff --git a/services/authorial_fabric/receipts/README.md b/services/authorial_fabric/receipts/README.md new file mode 100644 index 000000000..3ec7314bb --- /dev/null +++ b/services/authorial_fabric/receipts/README.md @@ -0,0 +1,13 @@ +# Authorial Fabric receipt custody + +This directory is for small reviewed successor receipts that describe durable state transitions of the Authorial Science Fabric. + +Raw downloaded source bytes, network logs and per-run hash inventories belong in GitHub Actions artifacts, not in the repository by default. + +A durable receipt may be committed only when it adds a material state delta and preserves: + +`SOURCE != ARTIFACT != EXECUTION != EVIDENCE != CLAIM` + +Required durable fields: source or run reference, exact repository HEAD, decision, evidence class, gaps/TOKEN_VAZIO, next action and rollback/supersedes reference when applicable. + +Secret values, lengths, hashes, authorization headers and equivalence between secret names are forbidden receipt material. From 3557bf494d49f3a861169c25077e933355fd74d7 Mon Sep 17 00:00:00 2001 From: Rafael mreis Date: Mon, 5 Oct 2026 12:08:18 -0300 Subject: [PATCH 12/14] security: reject credentialed redirects and enforce claim boundary --- tools/rll_authorial_ingress.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tools/rll_authorial_ingress.py b/tools/rll_authorial_ingress.py index 5b4a678c4..f4cc59ab4 100644 --- a/tools/rll_authorial_ingress.py +++ b/tools/rll_authorial_ingress.py @@ -32,6 +32,13 @@ SAFE_NAME_RE = re.compile(r"^[A-Za-z0-9._-]+$") +class _RejectCredentialedRedirect(urllib.request.HTTPRedirectHandler): + """Fail closed before an Authorization-bearing request can follow a redirect.""" + + def redirect_request(self, req, fp, code, msg, headers, newurl): # noqa: ANN001 + raise ValueError("credentialed redirects are forbidden") + + def load_json(path: Path) -> dict[str, Any]: obj = json.loads(path.read_text(encoding="utf-8")) if not isinstance(obj, dict): @@ -43,6 +50,8 @@ def validate_manifest(doc: dict[str, Any]) -> list[str]: errors: list[str] = [] if doc.get("schema") != SCHEMA: errors.append(f"schema must be {SCHEMA}") + if doc.get("claim_allowed") is not False: + errors.append("claim_allowed must be false for source-ingress manifests") sources = doc.get("sources") if not isinstance(sources, list) or not sources: errors.append("sources must be a non-empty list") @@ -180,11 +189,19 @@ def acquire_source( headers["Accept"] = "application/vnd.github+json" request = urllib.request.Request(url, headers=headers, method="GET") - with urllib.request.urlopen(request, timeout=30) as response: + if credential != "none": + opener = urllib.request.build_opener(_RejectCredentialedRedirect()) + response_context = opener.open(request, timeout=30) + else: + response_context = urllib.request.urlopen(request, timeout=30) + + with response_context as response: final_url = response.geturl() parsed_final = urllib.parse.urlparse(final_url) if parsed_final.scheme != "https": raise ValueError(f"{source['id']}: redirect left https") + if credential != "none" and parsed_final.hostname != GITHUB_API_HOST: + raise ValueError(f"{source['id']}: credentialed response left api.github.com") payload = _read_bounded(response, int(source["max_bytes"])) data = _decode_github_contents(payload) if credential != "none" else payload From 96dfa6f19578aacd4a75524d7d9f06646e30e187 Mon Sep 17 00:00:00 2001 From: Rafael mreis Date: Mon, 5 Oct 2026 12:08:42 -0300 Subject: [PATCH 13/14] test: cover claim boundary and PAT redirect fail-closed --- tests/test_rll_authorial_ingress.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_rll_authorial_ingress.py b/tests/test_rll_authorial_ingress.py index 495772244..7a6efd77e 100644 --- a/tests/test_rll_authorial_ingress.py +++ b/tests/test_rll_authorial_ingress.py @@ -14,6 +14,7 @@ class TestRllAuthorialIngress(unittest.TestCase): def good(self): return { "schema": mod.SCHEMA, + "claim_allowed": False, "sources": [ { "id": "x", @@ -32,6 +33,13 @@ def good(self): def test_good_manifest(self): self.assertEqual(mod.validate_manifest(self.good()), []) + def test_claim_boundary_must_be_false(self): + doc = self.good() + doc["claim_allowed"] = True + self.assertTrue( + any("claim_allowed" in error for error in mod.validate_manifest(doc)) + ) + def test_http_rejected(self): doc = self.good() doc["sources"][0]["url"] = "http://example.org/x.pdf" @@ -51,6 +59,18 @@ def test_pat_is_github_contents_only(self): any("api.github.com" in error for error in mod.validate_manifest(doc)) ) + def test_credentialed_redirect_is_rejected_before_follow(self): + handler = mod._RejectCredentialedRedirect() + with self.assertRaisesRegex(ValueError, "credentialed redirects are forbidden"): + handler.redirect_request( + None, + None, + 302, + "Found", + {}, + "https://example.org/leak-target", + ) + def test_kind_signatures(self): self.assertTrue(mod._kind_check("pdf", b"%PDF-1.7\n")) self.assertTrue(mod._kind_check("image", b"\x89PNG\r\n\x1a\nabc")) From e5f4d913481a512be902faae87d2adc66dbc12a8 Mon Sep 17 00:00:00 2001 From: Rafael mreis Date: Mon, 5 Oct 2026 12:09:33 -0300 Subject: [PATCH 14/14] ci: make custody hash inventory self-excluding and deterministic --- .github/workflows/rll-authorial-science-fabric-v1.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rll-authorial-science-fabric-v1.yml b/.github/workflows/rll-authorial-science-fabric-v1.yml index 8d288576f..a188e2d29 100644 --- a/.github/workflows/rll-authorial-science-fabric-v1.yml +++ b/.github/workflows/rll-authorial-science-fabric-v1.yml @@ -205,7 +205,14 @@ jobs: if: always() run: | set -euo pipefail - find "$RUN_DIR" -type f -print0 | sort -z | xargs -0 sha256sum > "$RUN_DIR/receipts/SHA256SUMS.txt" + TMP_SUMS="$RUN_DIR/receipts/.SHA256SUMS.tmp" + find "$RUN_DIR" -type f \ + ! -name 'SHA256SUMS.txt' \ + ! -name '.SHA256SUMS.tmp' \ + -print0 \ + | sort -z \ + | xargs -0 -r sha256sum > "$TMP_SUMS" + mv "$TMP_SUMS" "$RUN_DIR/receipts/SHA256SUMS.txt" - name: Upload source custody bundle if: always()