rto_exec/lib.rs
1//! The analyzer execution seam: one contract, interchangeable backends.
2//!
3//! Running an external analyzer (`cargo-audit`, `semgrep`, successors) can happen
4//! in CI, on a developer's machine, or — later — locally inside a sandbox. This
5//! crate exists so those stop being competing architectures: every backend
6//! implements one [`AnalyzerRunner`] trait, takes one [`AnalysisRequest`], and
7//! returns one [`AnalysisResponse`] of normalized findings plus run evidence. A
8//! caller never learns which backend produced a result, so adding the sandboxed
9//! and subprocess backends later changes no call site.
10//!
11//! Today there is exactly one implementation, [`IngestRunner`], which consumes a
12//! normalized report produced elsewhere. It is the zero-install default, not a
13//! fallback: it needs no container runtime and adds no isolation surface, and
14//! what it produces is byte-for-byte the shape a sandboxed run will produce.
15//!
16//! # What this crate does not do
17//!
18//! It does not decide how results are *stored*. Persistence lives in `rto-graph`,
19//! which files findings in their own tables — never `nodes`/`edges`, never a
20//! provenance class, never in the exported graph artifact (ADR-0012). Nothing
21//! here can move the published `GraphArtifact` by a byte, and that is checked by
22//! test rather than assumed.
23//!
24//! No analyzer is implemented here, and no sandbox dependency is pulled in; the
25//! backends arrive behind their own features (ADR-0014).
26//!
27//! @rto:0014
28//! @rto:0012
29//!
30//! # Example
31//!
32//! ```
33//! use rto_exec::{AnalysisRequest, AnalyzerRunner, Consent, IngestRunner, Worktree};
34//! use rto_graph::SourceIdentity;
35//!
36//! let report = br#"{
37//! "schema": "roteiro.findings/v1",
38//! "analyzer": "cargo-audit",
39//! "analyzer_version": "0.21.0",
40//! "started_at": "2026-08-15T09:00:00Z",
41//! "ended_at": "2026-08-15T09:00:04Z",
42//! "exit_status": 1,
43//! "findings": [{
44//! "identity": ["RUSTSEC-2024-0001", "openssl", "0.10.5", "lock123"],
45//! "rule": "RUSTSEC-2024-0001",
46//! "severity": "high",
47//! "title": "openssl is vulnerable",
48//! "message": "upgrade to 0.10.66"
49//! }]
50//! }"#;
51//!
52//! let request = AnalysisRequest {
53//! analyzer: "cargo-audit".to_owned(),
54//! worktree: Worktree::read_only("/repo".as_ref()).expect("worktree"),
55//! network: rto_graph::NetworkPolicy::Deny,
56//! consent: Consent::Granted,
57//! source: SourceIdentity::default(),
58//! };
59//! let response = IngestRunner::new(report.to_vec()).run(&request).expect("ingest");
60//! assert_eq!(response.findings.len(), 1);
61//! assert_eq!(response.run.isolation, rto_graph::Isolation::Ingested);
62//! ```
63
64pub mod adapter;
65/// Where the pinned-asset cache lives, and the precedence that decides it.
66///
67/// Its source carries no `//!` header because `build.rs` pulls the same file in
68/// with `include!`, where an inner doc comment is a syntax error — so the module
69/// documentation lives here instead. `build.rs` needs it to find the sandbox
70/// runtime `roteiro security prefetch` installed, which is the same cache
71/// [`asset_paths::asset_root`] names; read the file's own comments for why that
72/// is shared rather than copied.
73pub mod asset_paths;
74// Asset provisioning is **always compiled**, behind no feature at all.
75//
76// It used to be `cfg(any(exec-subprocess, exec-boxlite))`, on the reading that
77// provisioning belongs to whichever backend consumes the assets. That was the
78// wrong shape and this module half-said so already: it is shared between the
79// backends and owned by neither, and the note on `SANDBOX_RUNTIME_NOTICE` below
80// records that an `exec-subprocess`-only build provisions *for a later
81// `exec-boxlite` build* — provisioning already served a backend that was not
82// compiled in.
83//
84// The bootstrap argument settles it. `AGENTS.md` tells a contributor to run
85// `roteiro security prefetch --allow-download` *before* building
86// `--features exec-boxlite`, because that build script requires the verified
87// archive at compile time. If prefetch lived behind an execution feature, you
88// would need a build with a *different* execution backend compiled in before you
89// could provision the one you actually wanted. That is circular.
90//
91// Nothing here executes anything: it downloads, digests, pins and reports. Every
92// `Command::new` in this crate is in `subprocess.rs` or `boxlite.rs`, and both
93// stay behind their features. Provisioning is not execution.
94pub mod assets;
95#[cfg(feature = "exec-boxlite")]
96pub mod boxlite;
97mod clock;
98pub mod crossref;
99/// Emitting a `file://` URL for a local path, and reading one back.
100///
101/// Its source carries no `//!` header because `build.rs` pulls the same file in
102/// with `include!`, where an inner doc comment is a syntax error — so the module
103/// documentation lives here instead. `build.rs` is this crate's only emitter: it
104/// prints the `BOXLITE_RUNTIME_URL=` recipe an operator pastes, and parses that
105/// variable back when it is set. What reads the URL in between is `boxlite`'s
106/// own `curl`, which percent-decodes and rejects an unencoded space outright —
107/// read the file's own comments for the measurements, and for why the encoder
108/// and the decoder have to be one file rather than two.
109pub mod file_url;
110mod ingest;
111mod runner;
112/// The per-file digests of the extracted sandbox runtime — **generated**.
113///
114/// Derived from the archives in [`runtime_pins`] by
115/// `scripts/derive-runtime-file-pins.py`, and verified by `build.rs` against
116/// what `boxlite` actually extracted, since those files rather than the archive
117/// are what `include_bytes!` puts in the binary. Same `include!` arrangement,
118/// and so the same standalone constraint; its module documentation lives here
119/// for the same reason [`runtime_pins`]'s does.
120pub mod runtime_file_pins;
121/// The pinned sandbox-runtime archives, and the host-platform selection.
122///
123/// Its source carries no `//!` header because `build.rs` pulls the same file in
124/// with `include!`, where an inner doc comment is a syntax error — so the module
125/// documentation lives here instead. Read the file's own comments for what is
126/// pinned and why it has to be.
127pub mod runtime_pins;
128pub mod snippet;
129#[cfg(feature = "exec-subprocess")]
130pub mod subprocess;
131
132pub use adapter::{
133 ADAPTERS, Adapter, AssetPaths, Invocation, NO_SNIPPET, NativeContext, UNKNOWN_VERSION,
134 adapter_for, known_analyzers, snippet_hash, snippet_hash_at,
135};
136pub use assets::{
137 ASSETS, AssetKind, AssetSource, AssetSpec, AssetStatus, DownloadFile, Fetcher, InstalledAsset,
138 MissingAsset, SANDBOX, asset, asset_path, asset_root, assets_for, provision, provision_with,
139 resolve, status,
140};
141#[cfg(feature = "exec-boxlite")]
142pub use boxlite::{BoxliteRunner, SandboxError, SandboxProbe, sandbox_probe};
143pub use clock::{age_in_days, rfc3339_from_unix, rfc3339_utc, unix_from_rfc3339};
144pub use crossref::{Correspondence, Report, cross_reference};
145pub use ingest::{
146 IngestRunner, MAX_REPORT_FINDINGS, NormalizedReport, REPORT_SCHEMA, ReportFinding,
147 normalize_native,
148};
149pub use runner::{
150 AnalysisRequest, AnalysisResponse, AnalyzerRunner, Consent, ExecError, Worktree,
151 check_reported_path, check_request, worktree_id,
152};
153pub use runtime_file_pins::{
154 PinnedFile, PinnedRuntimeFiles, RUNTIME_FILES, RUNTIME_FILES_VERSION, runtime_files_for,
155};
156pub use runtime_pins::{
157 PinnedArchive, RUNTIME_ARCHIVES, RUNTIME_ASSET, RUNTIME_FILE, RUNTIME_VERSION, archive_for,
158 runtime_target,
159};
160pub use snippet::{NoSnippets, SnippetSource, WorktreeSnippets};
161#[cfg(feature = "exec-subprocess")]
162pub use subprocess::{SubprocessError, SubprocessRunner};
163
164/// The licence notice for the third-party binaries an `exec-boxlite` build
165/// embeds, compiled in so it cannot be separated from what it describes.
166///
167/// `roteiro security prefetch` prints it before installing the sandbox runtime,
168/// which is the same disclose-then-consent shape `roteiro model pull` uses. It
169/// is compiled into every build, because every build can provision the runtime —
170/// including one with no execution backend at all, which prefetches it for a
171/// later `exec-boxlite` build — so the obligations travel with the artifact
172/// rather than living only in the repository.
173pub const SANDBOX_RUNTIME_NOTICE: &str = include_str!("../NOTICE-boxlite-runtime.md");
174
175/// Lowercase hex SHA-256 of `bytes`.
176///
177/// Used for the report digest that ties an `AnalysisRun` to the exact bytes it
178/// was derived from, and for deriving an opaque worktree id from a path.
179#[must_use]
180pub fn sha256_hex(bytes: &[u8]) -> String {
181 use sha2::{Digest, Sha256};
182 let digest = Sha256::digest(bytes);
183 let mut out = String::with_capacity(64);
184 for byte in digest {
185 use std::fmt::Write as _;
186 let _ = write!(out, "{byte:02x}");
187 }
188 out
189}
190
191#[cfg(test)]
192mod tests {
193 use super::sha256_hex;
194
195 #[test]
196 fn hashes_the_known_vector() {
197 assert_eq!(
198 sha256_hex(b"abc"),
199 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
200 );
201 }
202}