field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

commit 7e5e049bf3d2cf6b400848c25d4d4a89b6cd4895
parent 73f16412a4fe3cf8ad02c9502c2690115bf73433
Author: triesap <tyson@radroots.org>
Date:   Tue,  8 Sep 2026 19:35:06 +0000

tera: bind ffi provenance to its application producer

- separate the exact foundation lock from the owned native producer contract
- identify staged producer inputs without a self-referential commit marker
- record target feature graphs and Rust and Apple toolchains under extbuild
- reject dirty or mismatched source while preserving installed provenance

Diffstat:
MAGENTS.md | 5+++++
MMakefile | 8++++++++
MREADME.md | 7+++++++
ARadrootsFFI/producer.toml | 27+++++++++++++++++++++++++++
Arust-toolchain.toml | 3+++
Ascripts/ffi-provenance.sh | 6++++++
Ascripts/ffi_provenance.py | 172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ascripts/ffi_source.py | 349+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mscripts/swift-quality.sh | 3+++
Ascripts/test_ffi_provenance.py | 181+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mscripts/verify-package-contract.sh | 3+++
11 files changed, 764 insertions(+), 0 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -20,6 +20,11 @@ This file applies to the complete standalone iOS app repository. A closer generated project/source inputs, and the package locks are machine evidence. Do not hand-edit generated bindings, XCFramework contents, provenance, SBOM, project output, or API snapshots. +- `RadrootsFFI/producer.toml` separately governs the owned Tera producer. + Stage its declared source inputs before `make ffi-source-write`; check with + `make ffi-source-check`. Source records identify a staged input tree and exact + build tuple under extbuild output. They do not establish installed artifacts + or remote release qualification; installed provenance remains separate. - Human specifications, decisions, migration history, runbooks, and qualification evidence are parent-owned under `docs/oss/ios_app/**`. They are absent from a standalone clone and must never become a build, test, diff --git a/Makefile b/Makefile @@ -2,12 +2,14 @@ SHELL := /bin/bash .SHELLFLAGS := -eu -o pipefail -c FFI_ROOT := RadrootsFFI +FFI_TARGET ?= aarch64-apple-ios SIMULATOR_NAME ?= iPhone 17 Pro SIMULATOR_DESTINATION := platform=iOS Simulator,name=$(SIMULATOR_NAME) .NOTPARALLEL: .PHONY: all doctor bootstrap persona-verifier-bootstrap ffi-bootstrap artifact-check package-contract-check \ + ffi-source-write ffi-source-check \ swift-quality maintainability-check \ linux-shared-rust \ package-resolve package-build package-test project xcodegen xcode-resolve \ @@ -28,6 +30,12 @@ persona-verifier-bootstrap: doctor artifact-check: doctor cargo extbuild run -- $(FFI_ROOT)/scripts/verify-installed-artifacts.sh +ffi-source-write: doctor + cargo extbuild run -- scripts/ffi-provenance.sh write --target '$(FFI_TARGET)' + +ffi-source-check: doctor + cargo extbuild run -- scripts/ffi-provenance.sh check --target '$(FFI_TARGET)' + package-contract-check: doctor cargo extbuild run -- scripts/verify-package-contract.sh diff --git a/README.md b/README.md @@ -20,6 +20,13 @@ preserves the current five creation families, Today/Add tabs, installed identity and persisted operation formats. Each moved package replaces its old source only with verified history, compatibility and generated-artifact evidence. +`RadrootsFFI/producer.toml` separately governs the owned Tera FFI producer. +After staging its source inputs, `make ffi-source-write ffi-source-check` +captures and checks the exact source tree, foundation lock, target, features and +toolchains under extbuild output. Select a supported target with `FFI_TARGET`. +This is local source evidence; the installed native artifacts retain their +existing source lock until the native cutover. + ## Requirements - macOS with Xcode and an iOS 18-or-newer simulator diff --git a/RadrootsFFI/producer.toml b/RadrootsFFI/producer.toml @@ -0,0 +1,27 @@ +schema = "tera.native-producer.v1" +repository = "https://github.com/radrootslabs/tera" +foundation_lock = "radroots.lib.source-lock.v1.toml" +source_inputs = [ + "Cargo.toml", "Cargo.lock", "rust-toolchain.toml", "Makefile", + "core", "scripts", "RadrootsFFI/Makefile", "RadrootsFFI/scripts", + "RadrootsFFI/producer.toml", "radroots.lib.source-lock.v1.toml", +] + +[ffi] +package = "tera_ffi" +default_features = true +features = [] +config = "core/crates/tera_ffi/uniffi.toml" + +[generator] +package = "tera_bindgen" +default_features = true +features = [] + +[build] +rust_version = "1.97.1" +profile = "release" +ios_deployment_target = "18.0" +source_date_epoch = 1787871027 +host = "aarch64-apple-darwin" +targets = ["aarch64-apple-ios", "aarch64-apple-ios-sim", "aarch64-apple-darwin"] diff --git a/rust-toolchain.toml b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.97.1" +profile = "minimal" diff --git a/scripts/ffi-provenance.sh b/scripts/ffi-provenance.sh @@ -0,0 +1,6 @@ +#!/bin/sh +set -eu + +repo_root=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) +exec uv run --offline --frozen --project "$repo_root/scripts/persona-verifier" \ + python "$repo_root/scripts/ffi_provenance.py" --repo-root "$repo_root" "$@" diff --git a/scripts/ffi_provenance.py b/scripts/ffi_provenance.py @@ -0,0 +1,172 @@ +"""Generate or check staged producer evidence, separate from installed artifacts.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import sys +import tempfile +from pathlib import Path +from typing import Any + +import ffi_source as source +import package_contract as contract + + +def capture(root: Path, target: str) -> dict[str, Any]: + source.reject_build_overrides() + source.reject_cargo_configuration(root) + config = source.producer_contract(root) + build = config["build"] + if target not in build["targets"]: + raise source.ProvenanceError("producer target is not governed") + snapshot = source.source_snapshot(root, config["source_inputs"]) + rustc = source.command(root, ["rustc", "-Vv"]).decode().strip() + if f"release: {build['rust_version']}\n" not in rustc + "\n": + raise source.ProvenanceError( + "active Rust compiler differs from producer contract" + ) + if f"host: {build['host']}\n" not in rustc + "\n": + raise source.ProvenanceError("active Rust host differs from producer contract") + result = { + "schema": "tera.producer-source.v1", + "repository": config["repository"], + "source": snapshot, + "foundation": contract._read_toml(root / config["foundation_lock"]), + "cargo_lock_sha256": hashlib.sha256( + source.read_source(root, "Cargo.lock") + ).hexdigest(), + "build": { + **config["ffi"], + "target": target, + "profile": build["profile"], + "ios_deployment_target": build["ios_deployment_target"], + "source_date_epoch": build["source_date_epoch"], + "rustc": rustc, + "apple_toolchain": apple_toolchain(root), + "feature_graph": source.feature_graph( + root, config["ffi"]["package"], target + ), + }, + "generator": { + **config["generator"], + "target": build["host"], + "profile": "dev", + "feature_graph": source.feature_graph( + root, config["generator"]["package"], build["host"] + ), + }, + "disposition": "local_staged_source_only", + } + if source.source_snapshot(root, config["source_inputs"]) != snapshot: + raise source.ProvenanceError("producer source changed during capture") + return result + + +def apple_toolchain(root: Path) -> dict[str, str]: + commands = { + "xcode": ["xcodebuild", "-version"], + "swift": ["xcrun", "swiftc", "--version"], + "swiftformat": ["swiftformat", "--version"], + "iphoneos_sdk": ["xcrun", "--sdk", "iphoneos", "--show-sdk-build-version"], + "iphonesimulator_sdk": [ + "xcrun", + "--sdk", + "iphonesimulator", + "--show-sdk-build-version", + ], + } + return { + name: source.command(root, argv).decode().strip() + for name, argv in commands.items() + } + + +def encoded(value: dict[str, Any]) -> bytes: + return (json.dumps(value, sort_keys=True, indent=2) + "\n").encode() + + +def verify_record(actual: bytes, expected: dict[str, Any]) -> None: + if actual != encoded(expected): + raise source.ProvenanceError( + "producer provenance differs from the exact source/build tuple" + ) + + +def output_path(root: Path, target: str, requested: str | None) -> Path: + external = os.environ.get("EXT_BUILD_PROJECT_DIR") + if not external or not os.environ.get("EXT_BUILD_RUN_ACTIVE"): + raise source.ProvenanceError("producer provenance requires extbuild") + base = Path(external).resolve() + path = ( + Path(requested) + if requested + else base / "target/tera_ffi/source" / f"{target}.json" + ) + path = path.absolute() + if not path.is_relative_to(base) or path.is_relative_to(root): + raise source.ProvenanceError("producer evidence must use external build output") + if path.resolve() != path or path.is_symlink(): + raise source.ProvenanceError("producer evidence output contains a symlink") + return path + + +def write_atomic(path: Path, data: bytes) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + temporary: str | None = None + try: + with tempfile.NamedTemporaryFile(dir=path.parent, delete=False) as handle: + temporary = handle.name + handle.write(data) + handle.flush() + os.fsync(handle.fileno()) + os.replace(temporary, path) + temporary = None + finally: + if temporary: + Path(temporary).unlink(missing_ok=True) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("mode", choices=("contract-check", "write", "check")) + parser.add_argument("--target") + parser.add_argument( + "--repo-root", type=Path, default=Path(__file__).resolve().parent.parent + ) + parser.add_argument("--output") + args = parser.parse_args() + try: + root = args.repo_root.resolve() + if args.mode == "contract-check": + source.producer_contract(root) + print( + "FFI producer contract verified; installed artifacts remain separately governed" + ) + return 0 + if not args.target: + raise source.ProvenanceError("producer target is required") + path = output_path(root, args.target, args.output) + record = capture(root, args.target) + if args.mode == "write": + write_atomic(path, encoded(record)) + verify_record(contract._read_regular(path), record) + except ( + source.ProvenanceError, + contract.PackageContractError, + OSError, + ValueError, + KeyError, + ) as error: + print(f"FFI producer provenance: {error}", file=sys.stderr) + return 1 + print( + f"FFI producer source {args.mode}: {args.target}; tree={record['source']['tree']}; local staged source only" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/ffi_source.py b/scripts/ffi_source.py @@ -0,0 +1,349 @@ +"""Bounded source capture for the application-owned native producer.""" + +from __future__ import annotations + +import hashlib +import os +import re +import stat +import subprocess +from pathlib import Path +from typing import Any + +import package_contract as contract + +MAX_BYTES = 2 * 1024 * 1024 +MAX_INPUTS = 2048 +INPUTS = [ + "Cargo.toml", + "Cargo.lock", + "rust-toolchain.toml", + "Makefile", + "core", + "scripts", + "RadrootsFFI/Makefile", + "RadrootsFFI/scripts", + "RadrootsFFI/producer.toml", + "radroots.lib.source-lock.v1.toml", +] +PRODUCER_PATH = "RadrootsFFI/producer.toml" + + +class ProvenanceError(Exception): + """A source-free, fail-closed provenance rejection.""" + + +def command(root: Path, argv: list[str], data: bytes | None = None) -> bytes: + if argv[0] == "git": + argv = ["git", "--no-replace-objects", *argv[1:]] + try: + result = subprocess.run( + argv, cwd=root, input=data, capture_output=True, check=False, timeout=120 + ) + except (OSError, subprocess.TimeoutExpired) as error: + raise ProvenanceError("producer inspection command unavailable") from error + if result.returncode or len(result.stdout) > MAX_BYTES: + raise ProvenanceError("producer inspection command failed or exceeded bound") + return result.stdout + + +def read_source(root: Path, relative: str) -> bytes: + path = Path(relative) + if path.is_absolute() or str(path) != relative or ".." in path.parts: + raise ProvenanceError("producer input path is invalid") + current = root + for part in path.parts: + current = current / part + if current.is_symlink(): + raise ProvenanceError("producer input contains a symlink") + return contract._read_regular(current, maximum=MAX_BYTES) + + +def producer_contract(root: Path) -> dict[str, Any]: + value = contract._read_toml(root / PRODUCER_PATH) + expected = { + "schema", + "repository", + "foundation_lock", + "source_inputs", + "ffi", + "generator", + "build", + } + contract._exact(set(value), expected, "producer contract fields") + contract._exact(value["schema"], "tera.native-producer.v1", "producer schema") + contract._exact( + value["repository"], + "https://github.com/radrootslabs/tera", + "producer repository", + ) + contract._exact(value["source_inputs"], INPUTS, "producer input inventory") + contract._exact( + value["foundation_lock"], + "radroots.lib.source-lock.v1.toml", + "foundation lock path", + ) + contract._exact( + value["ffi"], + { + "package": "tera_ffi", + "default_features": True, + "features": [], + "config": "core/crates/tera_ffi/uniffi.toml", + }, + "FFI producer selection", + ) + contract._exact( + value["generator"], + { + "package": "tera_bindgen", + "default_features": True, + "features": [], + }, + "generator selection", + ) + validate_build(value["build"]) + cargo = contract._read_toml(root / "Cargo.toml") + lock = contract._read_toml(root / value["foundation_lock"]) + validate_foundation(cargo, lock) + toolchain = contract._read_toml(root / "rust-toolchain.toml") + contract._exact( + toolchain, + { + "toolchain": { + "channel": value["build"]["rust_version"], + "profile": "minimal", + } + }, + "producer toolchain", + ) + return value + + +def validate_build(value: Any) -> None: + value = contract._mapping(value, "producer build") + contract._exact( + set(value), + { + "rust_version", + "profile", + "ios_deployment_target", + "source_date_epoch", + "host", + "targets", + }, + "producer build fields", + ) + contract._exact(value["rust_version"], "1.97.1", "producer Rust version") + contract._exact(value["profile"], "release", "producer build profile") + contract._exact( + value["ios_deployment_target"], "18.0", "producer deployment target" + ) + contract._exact(value["host"], "aarch64-apple-darwin", "producer host") + contract._exact( + value["targets"], + [ + "aarch64-apple-ios", + "aarch64-apple-ios-sim", + "aarch64-apple-darwin", + ], + "producer targets", + ) + if type(value["source_date_epoch"]) is not int or value["source_date_epoch"] <= 0: + raise ProvenanceError("producer source epoch is invalid") + + +def validate_foundation(cargo: dict[str, Any], lock: dict[str, Any]) -> None: + contract._exact( + lock.get("schema"), "radroots.lib.source-lock.v1", "foundation schema" + ) + contract._exact(lock.get("repository"), contract.LIB_REMOTE, "foundation remote") + if not contract.GIT_REVISION.fullmatch(str(lock.get("revision", ""))): + raise ProvenanceError("foundation revision is invalid") + for field in ( + "workspace_catalog_sha256", + "source_archive_sha256", + "lockfile_sha256", + ): + if not contract.SHA256.fullmatch(str(lock.get(field, ""))): + raise ProvenanceError("foundation digest is invalid") + dependencies = cargo["workspace"]["dependencies"] + selected = { + name: value + for name, value in dependencies.items() + if name.startswith("radroots_") + } + if not selected: + raise ProvenanceError("foundation dependency inventory is empty") + for value in selected.values(): + value = contract._mapping(value, "foundation Cargo dependency") + contract._exact(value.get("git"), lock["repository"], "foundation Cargo remote") + contract._exact(value.get("rev"), lock["revision"], "foundation Cargo revision") + contract._exact( + value.get("version"), "=" + lock["version"], "foundation Cargo version" + ) + + +def source_snapshot(root: Path, inputs: list[str]) -> dict[str, Any]: + actual_root = ( + command(root, ["git", "rev-parse", "--show-toplevel"]).decode().strip() + ) + if actual_root != str(root): + raise ProvenanceError("producer source must use its own repository root") + if command( + root, ["git", "ls-files", "--others", "--exclude-standard", "-z", "--", *inputs] + ): + raise ProvenanceError("producer inputs contain untracked source") + reject_ignored_source(root, inputs) + raw = command(root, ["git", "ls-files", "--stage", "-z", "--", *inputs]) + rows = raw.rstrip(b"\0").split(b"\0") if raw else [] + if not rows or len(rows) > MAX_INPUTS: + raise ProvenanceError("producer input inventory exceeds bounds") + files: dict[str, Any] = {} + tree: dict[str, Any] = {} + for row in rows: + relative, entry = staged_file(root, row) + files[relative] = entry + insert_tree(tree, relative, entry["mode"], entry["git_blob"]) + require_input_inventory(inputs, files) + return {"policy": "staged_inputs", "tree": tree_identity(tree), "files": files} + + +def require_input_inventory(inputs: list[str], files: dict[str, Any]) -> None: + for relative in inputs: + if not any( + name == relative or name.startswith(relative + "/") for name in files + ): + raise ProvenanceError("required producer input is missing from index") + + +def reject_ignored_source(root: Path, inputs: list[str]) -> None: + ignored = command( + root, + [ + "git", + "ls-files", + "--others", + "--ignored", + "--exclude-standard", + "-z", + "--", + *inputs, + ":(exclude)scripts/persona-verifier/.venv/**", + ":(exclude,glob)**/__pycache__/**", + ], + ) + if any(is_input(path, inputs) for path in ignored.decode().split("\0") if path): + raise ProvenanceError( + "producer inputs contain ignored source outside tool caches" + ) + + +def is_input(relative: str, inputs: list[str]) -> bool: + return any(relative == name or relative.startswith(name + "/") for name in inputs) + + +def staged_file(root: Path, row: bytes) -> tuple[str, dict[str, Any]]: + header, encoded_path = row.split(b"\t", 1) + mode, oid, stage = header.decode("ascii").split() + relative = encoded_path.decode("utf-8") + if stage != "0" or mode not in ("100644", "100755"): + raise ProvenanceError("producer input has unresolved or unsupported mode") + data = read_source(root, relative) + blob = command(root, ["git", "cat-file", "blob", oid]) + actual_mode = ( + "100755" if (root / relative).stat().st_mode & stat.S_IXUSR else "100644" + ) + if data != blob or mode != actual_mode: + raise ProvenanceError("producer worktree differs from staged source") + return relative, { + "mode": mode, + "git_blob": oid, + "bytes": len(data), + "sha256": hashlib.sha256(data).hexdigest(), + } + + +def reject_cargo_configuration(root: Path) -> None: + locations = [path / ".cargo" for path in (root, *root.parents)] + locations.append(Path(os.environ.get("CARGO_HOME", str(Path.home() / ".cargo")))) + for location in locations: + for name in ("config", "config.toml"): + path = location / name + if path.exists(): + value = contract._read_toml(path) + if {"build", "target", "env", "profile", "patch", "unstable"} & set( + value + ): + raise ProvenanceError( + "ungoverned Cargo configuration affects producer build" + ) + + +def insert_tree(tree: dict[str, Any], relative: str, mode: str, oid: str) -> None: + parts = relative.split("/") + for part in parts[:-1]: + tree = tree.setdefault(part, {}) + tree[parts[-1]] = (mode, oid) + + +def tree_identity(tree: dict[str, Any]) -> str: + rows = [] + for name, value in tree.items(): + if isinstance(value, dict): + mode, oid, key = "40000", tree_identity(value), name.encode() + b"/" + else: + mode, oid = value + key = name.encode() + rows.append((key, f"{mode} {name}".encode() + b"\0" + bytes.fromhex(oid))) + body = b"".join(row for _, row in sorted(rows)) + return hashlib.sha1( + b"tree " + str(len(body)).encode() + b"\0" + body, usedforsecurity=False + ).hexdigest() + + +def reject_build_overrides() -> None: + forbidden = ( + "RUSTFLAGS", + "RUSTC", + "RUSTC_WRAPPER", + "RUSTC_WORKSPACE_WRAPPER", + "CARGO_ENCODED_RUSTFLAGS", + "GIT_INDEX_FILE", + "GIT_DIR", + "GIT_WORK_TREE", + "GIT_OBJECT_DIRECTORY", + "GIT_ALTERNATE_OBJECT_DIRECTORIES", + "GIT_CONFIG_COUNT", + "GIT_CONFIG_PARAMETERS", + ) + if any(os.environ.get(name) for name in forbidden): + raise ProvenanceError("ungoverned Rust build override is active") + if any( + re.fullmatch(r"CARGO_(BUILD_.*|TARGET_.*_(RUSTFLAGS|LINKER|RUNNER))", name) + for name in os.environ + ): + raise ProvenanceError("ungoverned Cargo build override is active") + + +def feature_graph(root: Path, package: str, target: str) -> list[str]: + raw = command( + root, + [ + "cargo", + "tree", + "--locked", + "--offline", + "-p", + package, + "--target", + target, + "--edges", + "normal,build", + "--prefix", + "none", + "--format", + "{p}|{f}", + ], + ).decode() + return sorted(set(raw.replace(str(root), "<producer-root>").splitlines())) diff --git a/scripts/swift-quality.sh b/scripts/swift-quality.sh @@ -20,6 +20,9 @@ readonly -a MAINTAINABILITY_RULES=( type_body_length ) readonly -a PYTHON_QUALITY_PATHS=( + scripts/ffi_source.py + scripts/ffi_provenance.py + scripts/test_ffi_provenance.py scripts/maintainability_ratchet.py scripts/package_contract.py scripts/test_maintainability_ratchet.py diff --git a/scripts/test_ffi_provenance.py b/scripts/test_ffi_provenance.py @@ -0,0 +1,181 @@ +from __future__ import annotations + +import copy +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +SCRIPTS = Path(__file__).resolve().parent +if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) + +import ffi_provenance as provenance # noqa: E402 +import ffi_source as source # noqa: E402 +import package_contract as contract # noqa: E402 + + +class ProducerSourceTests(unittest.TestCase): + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name).resolve() + self.git("init", "--quiet") + (self.root / "core").mkdir() + (self.root / "core/lib.rs").write_text("pub fn value() -> u8 { 1 }\n") + (self.root / "Cargo.lock").write_text("# synthetic source-capture fixture\n") + self.git("add", "core", "Cargo.lock") + self.inputs = ["core", "Cargo.lock"] + + def git(self, *args: str) -> str: + return subprocess.check_output( + ["git", *args], cwd=self.root, text=True, stderr=subprocess.DEVNULL + ).strip() + + def snapshot(self) -> dict: + return source.source_snapshot(self.root, self.inputs) + + def test_tree_is_real_and_deterministic_without_an_introducing_commit(self) -> None: + first = self.snapshot() + self.assertEqual(first, self.snapshot()) + self.assertEqual(self.git("write-tree"), first["tree"]) + self.assertEqual(self.git("cat-file", "-t", first["tree"]), "tree") + # Generated evidence never changes its own source tree identity. + (self.root / "provenance.json").write_bytes(provenance.encoded(first)) + self.git("add", "provenance.json") + self.assertEqual(first, self.snapshot()) + + def test_worktree_change_requires_an_explicit_staged_transaction(self) -> None: + first = self.snapshot() + (self.root / "core/lib.rs").write_text("pub fn value() -> u8 { 2 }\n") + with self.assertRaisesRegex(source.ProvenanceError, "differs from staged"): + self.snapshot() + self.git("add", "core/lib.rs") + self.assertNotEqual(first["tree"], self.snapshot()["tree"]) + + def test_untracked_and_missing_source_fail_closed(self) -> None: + (self.root / "core/extra.rs").write_text("// untracked input\n") + with self.assertRaisesRegex(source.ProvenanceError, "untracked"): + self.snapshot() + (self.root / "core/extra.rs").unlink() + (self.root / "Cargo.lock").unlink() + self.git("add", "-u", "Cargo.lock") + with self.assertRaisesRegex(source.ProvenanceError, "missing from index"): + self.snapshot() + + def test_symlink_and_file_mode_changes_are_rejected(self) -> None: + path = self.root / "core/lib.rs" + path.chmod(0o755) + with self.assertRaisesRegex(source.ProvenanceError, "differs from staged"): + self.snapshot() + path.unlink() + path.symlink_to("../Cargo.lock") + with self.assertRaisesRegex(source.ProvenanceError, "symlink"): + self.snapshot() + self.git("add", "core/lib.rs") + with self.assertRaisesRegex(source.ProvenanceError, "unsupported mode"): + self.snapshot() + + def test_source_byte_bound_prevents_unbounded_capture(self) -> None: + (self.root / "core/lib.rs").write_bytes(b"x" * (source.MAX_BYTES + 1)) + with self.assertRaisesRegex(contract.PackageContractError, "byte limit"): + self.snapshot() + + def test_gitignore_cannot_hide_a_producer_input(self) -> None: + (self.root / ".gitignore").write_text("core/hidden.rs\n") + (self.root / "core/hidden.rs").write_text("// ignored source input\n") + with self.assertRaisesRegex(source.ProvenanceError, "ignored source"): + self.snapshot() + + def test_provenance_rejects_every_source_or_build_tuple_mutation(self) -> None: + expected = { + "source": self.snapshot(), + "cargo_lock_sha256": "a" * 64, + "foundation": {"revision": "b" * 40}, + "build": { + "target": "aarch64-apple-ios", + "rustc": "rustc 1.97.1", + "features": [], + "feature_graph": ["tera_core|mobile-social"], + }, + } + provenance.verify_record(provenance.encoded(expected), expected) + for section, key, value in ( + ("source", "tree", "c" * 40), + ("foundation", "revision", "d" * 40), + ("build", "target", "aarch64-apple-ios-sim"), + ("build", "rustc", "rustc 1.96.0"), + ("build", "features", ["extra"]), + ("build", "feature_graph", []), + ): + with self.subTest(section=section, key=key): + changed = copy.deepcopy(expected) + changed[section][key] = value + with self.assertRaisesRegex( + source.ProvenanceError, "exact source/build tuple" + ): + provenance.verify_record(provenance.encoded(changed), expected) + changed = {**expected, "cargo_lock_sha256": "f" * 64} + with self.assertRaises(source.ProvenanceError): + provenance.verify_record(provenance.encoded(changed), expected) + + def test_output_rejects_repository_paths_escapes_and_symlinks(self) -> None: + external = self.root / "external" + external.mkdir() + repository = self.root / "repository" + repository.mkdir() + with patch.dict( + os.environ, + { + "EXT_BUILD_RUN_ACTIVE": "1", + "EXT_BUILD_PROJECT_DIR": str(external), + }, + ): + for output in (repository / "evidence.json", external / "../escape.json"): + with self.assertRaises(source.ProvenanceError): + provenance.output_path(repository, "aarch64-apple-ios", str(output)) + (external / "link").symlink_to(repository, target_is_directory=True) + with self.assertRaises(source.ProvenanceError): + provenance.output_path( + repository, "aarch64-apple-ios", str(external / "link/value.json") + ) + + def test_ungoverned_build_flags_are_rejected(self) -> None: + for key in ( + "RUSTFLAGS", + "RUSTC_WRAPPER", + "CARGO_BUILD_RUSTFLAGS", + "CARGO_TARGET_AARCH64_APPLE_IOS_LINKER", + ): + with self.subTest(key=key), patch.dict(os.environ, {key: "synthetic"}): + with self.assertRaisesRegex(source.ProvenanceError, "ungoverned"): + source.reject_build_overrides() + + def test_local_cargo_build_configuration_cannot_escape_source_identity( + self, + ) -> None: + (self.root / ".cargo").mkdir() + config = self.root / ".cargo/config.toml" + config.write_text('[build]\nrustflags = ["--cfg", "unrecorded"]\n') + with self.assertRaisesRegex(source.ProvenanceError, "Cargo configuration"): + source.reject_cargo_configuration(self.root) + + def test_current_contract_separates_foundation_and_application(self) -> None: + config = source.producer_contract(SCRIPTS.parent) + self.assertEqual(config["ffi"]["package"], "tera_ffi") + self.assertEqual(config["repository"], "https://github.com/radrootslabs/tera") + cargo = contract._read_toml(SCRIPTS.parent / "Cargo.toml") + lock = contract._read_toml(SCRIPTS.parent / config["foundation_lock"]) + changed = copy.deepcopy(cargo) + changed["workspace"]["dependencies"]["radroots_sdk"]["rev"] = "f" * 40 + with self.assertRaisesRegex( + contract.PackageContractError, "foundation Cargo revision" + ): + source.validate_foundation(changed, lock) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/verify-package-contract.sh b/scripts/verify-package-contract.sh @@ -7,11 +7,14 @@ python_project="$repo_root/scripts/persona-verifier" uv run --project "$python_project" --offline --frozen \ python "$repo_root/scripts/package_contract.py" --repo-root "$repo_root" +sh "$repo_root/scripts/ffi-provenance.sh" contract-check + ( cd "$repo_root" uv run --project "$python_project" --offline --frozen \ python -m unittest \ scripts/test_package_contract.py \ + scripts/test_ffi_provenance.py \ scripts/test_local_social_fixture.py )