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
64mod ingest;
65mod runner;
66
67pub use ingest::{
68 IngestRunner, MAX_REPORT_FINDINGS, NormalizedReport, REPORT_SCHEMA, ReportFinding,
69};
70pub use runner::{
71 AnalysisRequest, AnalysisResponse, AnalyzerRunner, Consent, ExecError, Worktree,
72 check_reported_path, check_request, worktree_id,
73};
74
75/// Lowercase hex SHA-256 of `bytes`.
76///
77/// Used for the report digest that ties an `AnalysisRun` to the exact bytes it
78/// was derived from, and for deriving an opaque worktree id from a path.
79#[must_use]
80pub fn sha256_hex(bytes: &[u8]) -> String {
81 use sha2::{Digest, Sha256};
82 let digest = Sha256::digest(bytes);
83 let mut out = String::with_capacity(64);
84 for byte in digest {
85 use std::fmt::Write as _;
86 let _ = write!(out, "{byte:02x}");
87 }
88 out
89}
90
91#[cfg(test)]
92mod tests {
93 use super::sha256_hex;
94
95 #[test]
96 fn hashes_the_known_vector() {
97 assert_eq!(
98 sha256_hex(b"abc"),
99 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
100 );
101 }
102}