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#[cfg(feature = "exec-subprocess")]
66pub mod assets;
67mod clock;
68mod ingest;
69mod runner;
70pub mod snippet;
71#[cfg(feature = "exec-subprocess")]
72pub mod subprocess;
73
74pub use adapter::{
75 ADAPTERS, Adapter, AssetPaths, Invocation, NO_SNIPPET, NativeContext, UNKNOWN_VERSION,
76 adapter_for, known_analyzers, snippet_hash, snippet_hash_at,
77};
78#[cfg(feature = "exec-subprocess")]
79pub use assets::{
80 ASSETS, AssetKind, AssetSpec, AssetStatus, InstalledAsset, MissingAsset, asset, asset_path,
81 asset_root, assets_for, provision, resolve, status,
82};
83pub use clock::{age_in_days, rfc3339_from_unix, rfc3339_utc, unix_from_rfc3339};
84pub use ingest::{
85 IngestRunner, MAX_REPORT_FINDINGS, NormalizedReport, REPORT_SCHEMA, ReportFinding,
86 normalize_native,
87};
88pub use runner::{
89 AnalysisRequest, AnalysisResponse, AnalyzerRunner, Consent, ExecError, Worktree,
90 check_reported_path, check_request, worktree_id,
91};
92pub use snippet::{NoSnippets, SnippetSource, WorktreeSnippets};
93#[cfg(feature = "exec-subprocess")]
94pub use subprocess::{SubprocessError, SubprocessRunner};
95
96/// Lowercase hex SHA-256 of `bytes`.
97///
98/// Used for the report digest that ties an `AnalysisRun` to the exact bytes it
99/// was derived from, and for deriving an opaque worktree id from a path.
100#[must_use]
101pub fn sha256_hex(bytes: &[u8]) -> String {
102 use sha2::{Digest, Sha256};
103 let digest = Sha256::digest(bytes);
104 let mut out = String::with_capacity(64);
105 for byte in digest {
106 use std::fmt::Write as _;
107 let _ = write!(out, "{byte:02x}");
108 }
109 out
110}
111
112#[cfg(test)]
113mod tests {
114 use super::sha256_hex;
115
116 #[test]
117 fn hashes_the_known_vector() {
118 assert_eq!(
119 sha256_hex(b"abc"),
120 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
121 );
122 }
123}