Skip to main content

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// Asset provisioning serves both execution backends. It is deliberately *not*
66// gated on `exec-boxlite` alone: a build with only `exec-subprocess` can still
67// `prefetch` the sandbox runtime, which is how you obtain the verified archive
68// that an `exec-boxlite` build then requires at compile time. Bootstrapping the
69// stricter feature from the looser one is the point.
70#[cfg(any(feature = "exec-subprocess", feature = "exec-boxlite"))]
71pub mod assets;
72#[cfg(feature = "exec-boxlite")]
73pub mod boxlite;
74mod clock;
75pub mod crossref;
76mod ingest;
77mod runner;
78/// The pinned sandbox-runtime archives, and the host-platform selection.
79///
80/// Its source carries no `//!` header because `build.rs` pulls the same file in
81/// with `include!`, where an inner doc comment is a syntax error — so the module
82/// documentation lives here instead. Read the file's own comments for what is
83/// pinned and why it has to be.
84#[cfg(any(feature = "exec-subprocess", feature = "exec-boxlite"))]
85pub mod runtime_pins;
86pub mod snippet;
87#[cfg(feature = "exec-subprocess")]
88pub mod subprocess;
89
90pub use adapter::{
91    ADAPTERS, Adapter, AssetPaths, Invocation, NO_SNIPPET, NativeContext, UNKNOWN_VERSION,
92    adapter_for, known_analyzers, snippet_hash, snippet_hash_at,
93};
94#[cfg(any(feature = "exec-subprocess", feature = "exec-boxlite"))]
95pub use assets::{
96    ASSETS, AssetKind, AssetSource, AssetSpec, AssetStatus, DownloadFile, Fetcher, InstalledAsset,
97    MissingAsset, SANDBOX, asset, asset_path, asset_root, assets_for, provision, provision_with,
98    resolve, status,
99};
100#[cfg(feature = "exec-boxlite")]
101pub use boxlite::{BoxliteRunner, SandboxError, SandboxProbe, sandbox_probe};
102pub use clock::{age_in_days, rfc3339_from_unix, rfc3339_utc, unix_from_rfc3339};
103pub use crossref::{Correspondence, Report, cross_reference};
104pub use ingest::{
105    IngestRunner, MAX_REPORT_FINDINGS, NormalizedReport, REPORT_SCHEMA, ReportFinding,
106    normalize_native,
107};
108pub use runner::{
109    AnalysisRequest, AnalysisResponse, AnalyzerRunner, Consent, ExecError, Worktree,
110    check_reported_path, check_request, worktree_id,
111};
112#[cfg(any(feature = "exec-subprocess", feature = "exec-boxlite"))]
113pub use runtime_pins::{
114    PinnedArchive, RUNTIME_ARCHIVES, RUNTIME_ASSET, RUNTIME_FILE, RUNTIME_VERSION, archive_for,
115    runtime_target,
116};
117pub use snippet::{NoSnippets, SnippetSource, WorktreeSnippets};
118#[cfg(feature = "exec-subprocess")]
119pub use subprocess::{SubprocessError, SubprocessRunner};
120
121/// The licence notice for the third-party binaries an `exec-boxlite` build
122/// embeds, compiled in so it cannot be separated from what it describes.
123///
124/// `roteiro security prefetch` prints it before installing the sandbox runtime,
125/// which is the same disclose-then-consent shape `roteiro model pull` uses. It
126/// is compiled into every build that can provision the runtime — including an
127/// `exec-subprocess`-only one, which can prefetch it for a later `exec-boxlite`
128/// build — so the obligations travel with the artifact rather than living only
129/// in the repository.
130#[cfg(any(feature = "exec-subprocess", feature = "exec-boxlite"))]
131pub const SANDBOX_RUNTIME_NOTICE: &str = include_str!("../NOTICE-boxlite-runtime.md");
132
133/// Lowercase hex SHA-256 of `bytes`.
134///
135/// Used for the report digest that ties an `AnalysisRun` to the exact bytes it
136/// was derived from, and for deriving an opaque worktree id from a path.
137#[must_use]
138pub fn sha256_hex(bytes: &[u8]) -> String {
139    use sha2::{Digest, Sha256};
140    let digest = Sha256::digest(bytes);
141    let mut out = String::with_capacity(64);
142    for byte in digest {
143        use std::fmt::Write as _;
144        let _ = write!(out, "{byte:02x}");
145    }
146    out
147}
148
149#[cfg(test)]
150mod tests {
151    use super::sha256_hex;
152
153    #[test]
154    fn hashes_the_known_vector() {
155        assert_eq!(
156            sha256_hex(b"abc"),
157            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
158        );
159    }
160}