From e8ecdbd57d2ed230760177c521fc532ea462ba77 Mon Sep 17 00:00:00 2001 From: Jer Miller Date: Sun, 19 Jul 2026 18:25:29 -0600 Subject: [PATCH] feat(release): harden Rust policy baseline Require cargo-deny 0.20.2 through the release preflight helper and use it from the Rust dependency gates instead of the presence-only Makefile guard. Run dependency policy locked/offline, split RustSec refresh from the offline advisory check, and run a fresh audit before release artifact construction. Model the supported Rust target graph in deny.toml, deny unknown git sources, and add a workspace unsafe_code = "forbid" lint inherited by all five core crates. Document the target evidence matrix and make CI print only the evidence classes it actually establishes. No lockfile, toolchain, rust-version, or application behavior changes. --- Makefile | 14 +- core/Cargo.toml | 3 + core/crates/solstone-core-cli/Cargo.toml | 3 + .../solstone-core-indexer-store/Cargo.toml | 3 + core/crates/solstone-core-indexer/Cargo.toml | 3 + core/crates/solstone-core-journal/Cargo.toml | 3 + core/crates/solstone-core/Cargo.toml | 3 + core/deny.toml | 10 + docs/PORTING.md | 11 ++ scripts/check_release_preflight.py | 57 ++++++ scripts/release.sh | 4 + tests/test_check_release_preflight.py | 21 ++ tests/test_rust_policy_baseline.py | 181 ++++++++++++++++++ 13 files changed, 310 insertions(+), 6 deletions(-) create mode 100644 tests/test_rust_policy_baseline.py diff --git a/Makefile b/Makefile index cc7904b58..b453c6238 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,6 @@ RUST_MANIFEST := core/Cargo.toml IOS_TARGET := aarch64-apple-ios REQUIRE_CARGO := command -v cargo >/dev/null 2>&1 || { echo "cargo is required for Rust checks; install cargo and retry" >&2; exit 1; } REQUIRE_RUSTUP := command -v rustup >/dev/null 2>&1 || { echo "rustup is required for the iOS gate; install rustup and retry" >&2; exit 1; } -REQUIRE_CARGO_DENY := command -v cargo-deny >/dev/null 2>&1 || { echo "cargo-deny is required for Rust dependency policy; install cargo-deny and retry" >&2; exit 1; } # Pick the GPU (CUDA) journal runtime only on x86_64 NVIDIA hosts. The # CUDA bundle resolves onnxruntime-gpu, which ships NO aarch64 wheel on PyPI, so # an aarch64 NVIDIA host (e.g. DGX Spark / GB10) that auto-selected `cuda` would @@ -169,13 +168,14 @@ check-rust-ios: check-rust-deny: @$(REQUIRE_CARGO) - @$(REQUIRE_CARGO_DENY) - cargo deny --manifest-path $(RUST_MANIFEST) --locked check bans licenses sources + @python3 scripts/check_release_preflight.py cargo-deny + cargo deny --manifest-path $(RUST_MANIFEST) --locked --offline check bans licenses sources audit: @$(REQUIRE_CARGO) - @$(REQUIRE_CARGO_DENY) - cargo deny --manifest-path $(RUST_MANIFEST) --locked check advisories + @python3 scripts/check_release_preflight.py cargo-deny + @cargo deny --manifest-path $(RUST_MANIFEST) fetch db || { echo "ERROR: RustSec advisory refresh failed; no current advisory result was produced. Restore network access and rerun 'make audit'." >&2; exit 1; } + cargo deny --manifest-path $(RUST_MANIFEST) --locked --offline check advisories # Setup skill symlinks skills: @@ -562,7 +562,9 @@ ci: install-checks @echo "=== Running tests ===" @$(MAKE) test @echo "" - @echo "All CI checks passed!" + @echo "All CI checks passed; evidence classes:" + @echo " GNU-host checks: fmt, MSRV, clippy, tests, dependency policy" + @echo " iOS cross-target canary: check-rust-ios" verify: install-checks @echo "=== Running tests ===" diff --git a/core/Cargo.toml b/core/Cargo.toml index 71c95f580..6081cbaa8 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -14,6 +14,9 @@ edition = "2024" rust-version = "1.95" license = "AGPL-3.0-only" +[workspace.lints.rust] +unsafe_code = "forbid" + [workspace.dependencies] solstone-core-cli = { path = "crates/solstone-core-cli" } solstone-core-indexer = { path = "crates/solstone-core-indexer" } diff --git a/core/crates/solstone-core-cli/Cargo.toml b/core/crates/solstone-core-cli/Cargo.toml index bec654c99..659bcf386 100644 --- a/core/crates/solstone-core-cli/Cargo.toml +++ b/core/crates/solstone-core-cli/Cargo.toml @@ -6,4 +6,7 @@ rust-version.workspace = true license.workspace = true publish = false +[lints] +workspace = true + [dependencies] diff --git a/core/crates/solstone-core-indexer-store/Cargo.toml b/core/crates/solstone-core-indexer-store/Cargo.toml index 38de900af..c7de7dbbd 100644 --- a/core/crates/solstone-core-indexer-store/Cargo.toml +++ b/core/crates/solstone-core-indexer-store/Cargo.toml @@ -6,6 +6,9 @@ rust-version.workspace = true license.workspace = true publish = false +[lints] +workspace = true + [dependencies] rusqlite.workspace = true solstone-core-indexer.workspace = true diff --git a/core/crates/solstone-core-indexer/Cargo.toml b/core/crates/solstone-core-indexer/Cargo.toml index dc74455f2..2fc204dcc 100644 --- a/core/crates/solstone-core-indexer/Cargo.toml +++ b/core/crates/solstone-core-indexer/Cargo.toml @@ -6,6 +6,9 @@ rust-version.workspace = true license.workspace = true publish = false +[lints] +workspace = true + [dependencies] chrono.workspace = true chrono-tz.workspace = true diff --git a/core/crates/solstone-core-journal/Cargo.toml b/core/crates/solstone-core-journal/Cargo.toml index 5db721fa3..1f4fb11ac 100644 --- a/core/crates/solstone-core-journal/Cargo.toml +++ b/core/crates/solstone-core-journal/Cargo.toml @@ -6,6 +6,9 @@ rust-version.workspace = true license.workspace = true publish = false +[lints] +workspace = true + [dependencies] toml_edit.workspace = true diff --git a/core/crates/solstone-core/Cargo.toml b/core/crates/solstone-core/Cargo.toml index b752e20eb..5d46d1715 100644 --- a/core/crates/solstone-core/Cargo.toml +++ b/core/crates/solstone-core/Cargo.toml @@ -6,6 +6,9 @@ rust-version.workspace = true license.workspace = true publish = false +[lints] +workspace = true + [dependencies] chrono.workspace = true solstone-core-cli.workspace = true diff --git a/core/deny.toml b/core/deny.toml index ffe4b745c..3b76ec267 100644 --- a/core/deny.toml +++ b/core/deny.toml @@ -18,7 +18,17 @@ wildcards = "deny" allow-wildcard-paths = true deny = [{ name = "pyo3" }, { name = "pyo3-ffi" }, { name = "cpython" }] +[graph] +targets = [ + "x86_64-unknown-linux-gnu", + "x86_64-unknown-linux-musl", + "aarch64-unknown-linux-musl", + "aarch64-apple-darwin", + "aarch64-apple-ios", +] + [sources] # Protects Rust dependencies from unapproved registries. unknown-registry = "deny" +unknown-git = "deny" allow-registry = ["https://github.com/rust-lang/crates.io-index"] diff --git a/docs/PORTING.md b/docs/PORTING.md index b28d6a152..7d4aa1e46 100644 --- a/docs/PORTING.md +++ b/docs/PORTING.md @@ -36,6 +36,17 @@ required toolchain, target, and linker behavior in checked-in repository release paths, not in a local shell profile. If a dependency cannot satisfy a supported target, document the blocker and stop the conversion before merging it. +| Evidence | Repository command | Class | Notes | +|----------|--------------------|-------|-------| +| Rust formatting | `make check-rust-fmt` | GNU-host check | Host source-format evidence only. | +| Rust MSRV | `make check-rust-msrv` | GNU-host check | Verifies the pinned MSRV rail without changing `rust-version`. | +| Rust lint | `make check-rust-clippy` | GNU-host check | Runs the existing clippy `-D warnings` gate. | +| Rust tests | `make check-rust-test` | GNU-host check | Runs workspace Rust tests on the GNU host. | +| Rust dependency policy | `make check-rust-deny` | GNU-host check | Locked, offline bans/licenses/sources policy over the supported cargo-deny graph. | +| Rust advisories | `make audit` | GNU-host check | Refreshes the advisory DB, then performs a locked offline advisory check. | +| iOS canary | `make check-rust-ios` | iOS cross-target canary | Cross-target drift evidence for eligible library crates; explicitly excludes `solstone-core-indexer-store` because the native SQLite store is not yet in the iOS gate. | +| No-upload release rail | `scripts/release.sh --dry-run-all-hosts` | Structural only — known gap | Builds and structurally validates Linux x86_64 musl, Linux aarch64 musl, and macOS arm64 artifacts. No retained install/smoke evidence exists on any native host; that gap is deferred to the provenance wave. | + ## Owner Timezone The Python owner-timezone fallback is effectively `identity.timezone` from diff --git a/scripts/check_release_preflight.py b/scripts/check_release_preflight.py index b90b3cba2..97ffe52bb 100644 --- a/scripts/check_release_preflight.py +++ b/scripts/check_release_preflight.py @@ -17,6 +17,7 @@ from pathlib import Path from typing import Callable, Mapping, Sequence EXPECTED_ZIG_VERSION = "0.16.0" +EXPECTED_CARGO_DENY_VERSION = "0.20.2" TOOLCHAIN_FILE = "rust-toolchain.toml" COMPONENT_BINARIES = { "rustfmt": "rustfmt", @@ -255,6 +256,50 @@ def check_zig( return [] +def check_cargo_deny( + expected: str = EXPECTED_CARGO_DENY_VERSION, + *, + which: Callable[[str], str | None] = shutil.which, + runner: Runner = subprocess.run, +) -> list[Failure]: + cargo_deny = which("cargo-deny") + repair = f"cargo install cargo-deny@{expected} --locked --force" + if cargo_deny is None: + return [ + Failure( + error="cargo-deny is not on PATH", + expected=expected, + actual="not found", + repair=repair, + ) + ] + result = runner( + [cargo_deny, "--version"], + capture_output=True, + text=True, + check=False, + ) + actual = ( + result.stdout.strip() or result.stderr.strip() or f"exit {result.returncode}" + ) + parts = actual.split() + if ( + result.returncode != 0 + or len(parts) < 2 + or parts[0] != "cargo-deny" + or parts[1] != expected + ): + return [ + Failure( + error="cargo-deny version does not match the Rust dependency policy baseline", + expected=expected, + actual=actual, + repair=repair, + ) + ] + return [] + + def check_local_clean_status(status_output: str) -> list[Failure]: paths = [line for line in status_output.splitlines() if line.strip()] if not paths: @@ -382,6 +427,15 @@ def _cmd_msrv(args: argparse.Namespace) -> int: return 0 +def _cmd_cargo_deny(_args: argparse.Namespace) -> int: + failures = check_cargo_deny() + if failures: + _format_failures(failures) + return 1 + print(f"cargo-deny {EXPECTED_CARGO_DENY_VERSION} ok") + return 0 + + def _cmd_remote_state(args: argparse.Namespace) -> int: status = args.status_file.read_text(encoding="utf-8") failures = check_remote_state( @@ -410,6 +464,9 @@ def main(argv: list[str] | None = None) -> int: msrv.add_argument("--toolchain", required=True) msrv.set_defaults(func=_cmd_msrv) + cargo_deny = subparsers.add_parser("cargo-deny") + cargo_deny.set_defaults(func=_cmd_cargo_deny) + remote = subparsers.add_parser("remote-state") remote.add_argument("--label", required=True) remote.add_argument("--expected-ref", required=True) diff --git a/scripts/release.sh b/scripts/release.sh index 4a6dd6211..2cbda98e7 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -142,6 +142,10 @@ if [[ "$MODE" != "dry-run-linux" ]]; then GIT_REF=$(git rev-parse HEAD) fi +echo "==> running Rust advisory audit" +make audit +echo + # 1. Local lockstep artifacts: root + journal leaves + models echo "==> [1/5] building local lockstep artifacts" python3 scripts/render_packaging.py --check diff --git a/tests/test_check_release_preflight.py b/tests/test_check_release_preflight.py index 4dea01e42..46b55b34b 100644 --- a/tests/test_check_release_preflight.py +++ b/tests/test_check_release_preflight.py @@ -148,6 +148,27 @@ def test_mismatched_zig_reports_expected_actual_and_repair() -> None: assert "ziglang==0.16.0" in failures[0].repair +def test_missing_cargo_deny_reports_force_install_repair() -> None: + failures = preflight.check_cargo_deny(which=lambda _name: None) + + assert failures + assert failures[0].expected == preflight.EXPECTED_CARGO_DENY_VERSION + assert failures[0].actual == "not found" + assert failures[0].repair == "cargo install cargo-deny@0.20.2 --locked --force" + + +def test_mismatched_cargo_deny_reports_expected_actual_and_repair() -> None: + failures = preflight.check_cargo_deny( + which=lambda _name: "/usr/bin/cargo-deny", + runner=lambda *_args, **_kwargs: _completed("cargo-deny 0.19.9"), + ) + + assert failures + assert failures[0].expected == preflight.EXPECTED_CARGO_DENY_VERSION + assert failures[0].actual == "cargo-deny 0.19.9" + assert failures[0].repair == "cargo install cargo-deny@0.20.2 --locked --force" + + def test_dirty_local_status_names_offending_paths() -> None: failures = preflight.check_local_clean_status( " M core/Cargo.toml\n?? scratch.txt\n" diff --git a/tests/test_rust_policy_baseline.py b/tests/test_rust_policy_baseline.py new file mode 100644 index 000000000..f099fe29c --- /dev/null +++ b/tests/test_rust_policy_baseline.py @@ -0,0 +1,181 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +from __future__ import annotations + +import tomllib +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parents[1] +EXPECTED_GRAPH_TARGETS = { + "x86_64-unknown-linux-gnu", + "x86_64-unknown-linux-musl", + "aarch64-unknown-linux-musl", + "aarch64-apple-darwin", + "aarch64-apple-ios", +} + + +def _read_toml(path: Path) -> dict[str, Any]: + return tomllib.loads(path.read_text(encoding="utf-8")) + + +def _makefile_block(name: str, next_name: str) -> str: + text = (ROOT / "Makefile").read_text(encoding="utf-8") + start = text.index(f"\n{name}:") + end = text.index(f"\n{next_name}:", start + 1) + return text[start:end] + + +def _workspace_member_manifest_paths() -> list[Path]: + workspace = _read_toml(ROOT / "core" / "Cargo.toml") + members = workspace["workspace"]["members"] + assert isinstance(members, list) + assert members, "workspace member enumeration must not be empty" + + paths: list[Path] = [] + for member in members: + assert isinstance(member, str) + path = ROOT / "core" / member / "Cargo.toml" + assert path.is_file(), f"workspace member manifest is missing: {path}" + paths.append(path) + assert len(paths) == len(members) + return paths + + +def _rust_source_paths() -> list[Path]: + paths = sorted((ROOT / "core" / "crates").rglob("*.rs")) + assert paths, "Rust source enumeration must not be empty" + return paths + + +def test_check_rust_deny_recipe_is_version_asserted_locked_and_offline() -> None: + block = _makefile_block("check-rust-deny", "audit") + required_commands = [ + "scripts/check_release_preflight.py cargo-deny", + "--locked", + "--offline", + "check bans licenses sources", + ] + + assert required_commands + for command in required_commands: + assert command in block + + +def test_audit_recipe_refreshes_then_checks_offline_fail_closed() -> None: + block = _makefile_block("audit", "skills") + fetch = "cargo deny --manifest-path $(RUST_MANIFEST) fetch db" + check = ( + "cargo deny --manifest-path $(RUST_MANIFEST) --locked --offline " + "check advisories" + ) + required_commands = [ + "scripts/check_release_preflight.py cargo-deny", + fetch, + check, + ] + + assert required_commands + for command in required_commands: + assert command in block + assert block.index(fetch) < block.index(check) + + fetch_line = next(line for line in block.splitlines() if fetch in line) + assert "no current advisory result was produced" in fetch_line + assert "ERROR: RustSec advisory refresh failed" in fetch_line + assert "exit 1" in fetch_line + + +def test_release_rail_runs_audit_before_artifact_construction() -> None: + text = (ROOT / "scripts" / "release.sh").read_text(encoding="utf-8") + audit = "\nmake audit\n" + artifact = 'echo "==> [1/5] building local lockstep artifacts"' + inspected_commands = [audit, artifact] + + assert inspected_commands + for command in inspected_commands: + assert command in text + assert text.index(audit) < text.index(artifact) + + +def test_ci_summary_names_only_established_evidence_classes() -> None: + block = _makefile_block("ci", "verify") + summary_start = block.index("All CI checks passed; evidence classes:") + summary = block[summary_start:] + expected_lines = [ + "All CI checks passed; evidence classes:", + " GNU-host checks: fmt, MSRV, clippy, tests, dependency policy", + " iOS cross-target canary: check-rust-ios", + ] + + assert expected_lines + for line in expected_lines: + assert line in summary + lower_summary = summary.lower() + assert "advis" not in lower_summary + assert "release" not in lower_summary + assert "artifact" not in lower_summary + assert "native-host" not in lower_summary + + +def test_deny_toml_models_supported_graph_and_unknown_git_policy() -> None: + deny = _read_toml(ROOT / "core" / "deny.toml") + targets = deny["graph"]["targets"] + sources = deny["sources"] + + assert isinstance(targets, list) + assert len(targets) == len(EXPECTED_GRAPH_TARGETS) + assert set(targets) == EXPECTED_GRAPH_TARGETS + assert sources["unknown-git"] == "deny" + assert "allow-git" not in sources + + +def test_workspace_forbids_unsafe_and_members_inherit_lints() -> None: + workspace = _read_toml(ROOT / "core" / "Cargo.toml") + member_paths = _workspace_member_manifest_paths() + + assert workspace["workspace"]["lints"]["rust"]["unsafe_code"] == "forbid" + assert member_paths + for path in member_paths: + member = _read_toml(path) + assert member["lints"]["workspace"] is True + + +def test_member_manifests_do_not_shadow_workspace_unsafe_floor() -> None: + member_paths = _workspace_member_manifest_paths() + + assert member_paths + for path in member_paths: + member = _read_toml(path) + rust_lints = member.get("lints", {}).get("rust", {}) + assert "unsafe_code" not in rust_lints, path + + +def test_core_crates_have_zero_unsafe_inventory() -> None: + paths = _rust_source_paths() + hits: list[str] = [] + + assert paths + for path in paths: + lines = path.read_text(encoding="utf-8").splitlines() + for line_number, line in enumerate(lines, 1): + if "unsafe" in line: + hits.append(f"{path.relative_to(ROOT)}:{line_number}:{line.strip()}") + + assert not hits, "\n".join(hits) + + +def test_core_crates_do_not_allow_unsafe_code() -> None: + paths = _rust_source_paths() + hits: list[str] = [] + + assert paths + for path in paths: + lines = path.read_text(encoding="utf-8").splitlines() + for line_number, line in enumerate(lines, 1): + if "allow(unsafe_code)" in line: + hits.append(f"{path.relative_to(ROOT)}:{line_number}:{line.strip()}") + + assert not hits, "\n".join(hits) -- 2.51.2