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 *always* produce something to store, either. `lint` — named
19//! rather than linked, because that module is behind `exec-subprocess` and this
20//! page is read from builds without it — runs a linter and returns a report the
21//! caller prints: no [`AnalysisRun`], no layer, no store. A lint name is a
22//! symbol in a compiler rather than an assigned identifier, so it is an opinion
23//! about the code as it stands today rather than a durable fact about the
24//! repository (ADR-0020 v1.1) — and everything below about persistence simply
25//! does not apply to it.
26//!
27//! [`AnalysisRun`]: rto_graph::AnalysisRun
28//!
29//! It does not decide how results are *stored*. Persistence lives in `rto-graph`,
30//! which files findings in their own tables — never `nodes`/`edges`, never a
31//! provenance class, never in the exported graph artifact (ADR-0012). Nothing
32//! here can move the published `GraphArtifact` by a byte, and that is checked by
33//! test rather than assumed.
34//!
35//! No analyzer is implemented here, and no sandbox dependency is pulled in; the
36//! backends arrive behind their own features (ADR-0014).
37//!
38//! @rto:0014
39//! @rto:0012
40//!
41//! # Example
42//!
43//! ```
44//! use rto_exec::{AnalysisRequest, AnalyzerRunner, Consent, IngestRunner, Worktree};
45//! use rto_graph::SourceIdentity;
46//!
47//! let report = br#"{
48//!   "schema": "roteiro.findings/v1",
49//!   "analyzer": "cargo-audit",
50//!   "analyzer_version": "0.21.0",
51//!   "started_at": "2026-08-15T09:00:00Z",
52//!   "ended_at": "2026-08-15T09:00:04Z",
53//!   "exit_status": 1,
54//!   "findings": [{
55//!     "identity": ["RUSTSEC-2024-0001", "openssl", "0.10.5", "lock123"],
56//!     "rule": "RUSTSEC-2024-0001",
57//!     "severity": "high",
58//!     "title": "openssl is vulnerable",
59//!     "message": "upgrade to 0.10.66"
60//!   }]
61//! }"#;
62//!
63//! let request = AnalysisRequest {
64//!     analyzer: "cargo-audit".to_owned(),
65//!     worktree: Worktree::read_only("/repo".as_ref()).expect("worktree"),
66//!     network: rto_graph::NetworkPolicy::Deny,
67//!     consent: Consent::Granted,
68//!     source: SourceIdentity::default(),
69//! };
70//! let response = IngestRunner::new(report.to_vec()).run(&request).expect("ingest");
71//! assert_eq!(response.findings.len(), 1);
72//! assert_eq!(response.run.isolation, rto_graph::Isolation::Ingested);
73//! ```
74
75pub mod adapter;
76/// Where the pinned-asset cache lives, and the precedence that decides it.
77///
78/// Its source carries no `//!` header because `build.rs` pulls the same file in
79/// with `include!`, where an inner doc comment is a syntax error — so the module
80/// documentation lives here instead. `build.rs` needs it to find the sandbox
81/// runtime `roteiro security prefetch` installed, which is the same cache
82/// [`asset_paths::asset_root`] names; read the file's own comments for why that
83/// is shared rather than copied.
84pub mod asset_paths;
85// Asset provisioning is **always compiled**, behind no feature at all.
86//
87// It used to be `cfg(any(exec-subprocess, exec-boxlite))`, on the reading that
88// provisioning belongs to whichever backend consumes the assets. That was the
89// wrong shape and this module half-said so already: it is shared between the
90// backends and owned by neither, and the note on `SANDBOX_RUNTIME_NOTICE` below
91// records that an `exec-subprocess`-only build provisions *for a later
92// `exec-boxlite` build* — provisioning already served a backend that was not
93// compiled in.
94//
95// The bootstrap argument settles it. `AGENTS.md` tells a contributor to run
96// `roteiro security prefetch --allow-download` *before* building
97// `--features exec-boxlite`, because that build script requires the verified
98// archive at compile time. If prefetch lived behind an execution feature, you
99// would need a build with a *different* execution backend compiled in before you
100// could provision the one you actually wanted. That is circular.
101//
102// Nothing here executes anything: it downloads, digests, pins and reports. Every
103// `Command::new` in this crate is in `subprocess.rs` or `boxlite.rs`, and both
104// stay behind their features. Provisioning is not execution.
105pub mod assets;
106#[cfg(feature = "exec-boxlite")]
107pub mod boxlite;
108#[cfg(any(feature = "exec-subprocess", feature = "exec-boxlite"))]
109mod child_env;
110pub mod crossref;
111/// Emitting a `file://` URL for a local path, and reading one back.
112///
113/// Its source carries no `//!` header because `build.rs` pulls the same file in
114/// with `include!`, where an inner doc comment is a syntax error — so the module
115/// documentation lives here instead. `build.rs` is this crate's only emitter: it
116/// prints the `BOXLITE_RUNTIME_URL=` recipe an operator pastes, and parses that
117/// variable back when it is set. What reads the URL in between is `boxlite`'s
118/// own `curl`, which percent-decodes and rejects an unencoded space outright —
119/// read the file's own comments for the measurements, and for why the encoder
120/// and the decoder have to be one file rather than two.
121pub mod file_url;
122pub mod guidance;
123pub mod image_ref;
124mod ingest;
125#[cfg(feature = "exec-subprocess")]
126pub mod lint;
127pub mod lint_grant;
128#[cfg(all(feature = "exec-boxlite", feature = "exec-subprocess"))]
129pub mod lint_sandbox;
130mod runner;
131/// The per-file digests of the extracted sandbox runtime — **generated**.
132///
133/// Derived from the archives in [`runtime_pins`] by
134/// `scripts/derive-runtime-file-pins.py`, and verified by `build.rs` against
135/// what `boxlite` actually extracted, since those files rather than the archive
136/// are what `include_bytes!` puts in the binary. Same `include!` arrangement,
137/// and so the same standalone constraint; its module documentation lives here
138/// for the same reason [`runtime_pins`]'s does.
139pub mod runtime_file_pins;
140/// The pinned sandbox-runtime archives, and the host-platform selection.
141///
142/// Its source carries no `//!` header because `build.rs` pulls the same file in
143/// with `include!`, where an inner doc comment is a syntax error — so the module
144/// documentation lives here instead. Read the file's own comments for what is
145/// pinned and why it has to be.
146pub mod runtime_pins;
147pub mod sandbox_store;
148pub mod snippet;
149#[cfg(feature = "exec-subprocess")]
150pub mod subprocess;
151pub mod tool_security;
152
153pub use adapter::{
154    ADAPTERS, Adapter, AssetPaths, Invocation, LINT_ANALYZERS, NO_SNIPPET, NativeContext,
155    UNKNOWN_VERSION, adapter_for, known_analyzers, snippet_hash, snippet_hash_at,
156};
157// The linter's adapter is re-exported like any other, and — unlike any other —
158// is **not** in [`ADAPTERS`], so `ingest` cannot resolve it and nothing can file
159// its output as a layer. See [`adapter::clippy`].
160pub use adapter::clippy::{Clippy, FeatureSet};
161pub use assets::{
162    ASSETS, AssetKind, AssetSource, AssetSpec, AssetStatus, DownloadFile, Fetcher, InstalledAsset,
163    MissingAsset, SANDBOX, asset, asset_path, asset_root, assets_for, provision, provision_with,
164    resolve, status,
165};
166#[cfg(feature = "exec-boxlite")]
167pub use boxlite::{BoxliteRunner, SandboxError, SandboxProbe, sandbox_probe};
168// The RFC 3339 UTC formatter moved to `rto-graph` in #667: `rto-exec` is an
169// optional dependency of the CLI, and the CLI's render path — which is gated on
170// nothing — needs the same function to stamp a bundle's timestamps. It is
171// `okf_instant` that needs it today; the workspace-vault renderer that needed it
172// when #667 was filed was deleted by #671, whose OKF replacement reintroduced the
173// same unlinked-crate error at a new line. The move is aimed at that recurrence,
174// not at either call site.
175//
176// Re-exported under the names it has always had so this crate's own callers, and
177// `rto_exec::rfc3339_utc` in the CLI's `execution`/`remote` paths, did not have
178// to move with it.
179pub use crossref::{
180    Correspondence, Report, across_analyzers as cross_reference_across_analyzers, cross_reference,
181};
182pub use guidance::{Guidance, Line as GuidanceLine};
183pub use image_ref::{NotPinned, PinDefect, pinned_digest as image_pinned_digest};
184pub use ingest::{
185    IngestRunner, MAX_REPORT_FINDINGS, NormalizedReport, REPORT_SCHEMA, ReportFinding,
186    normalize_native,
187};
188#[cfg(feature = "exec-subprocess")]
189pub use lint::{LintError, LintOutcome, Toolchain, invocation as lint_invocation, run as run_lint};
190pub use rto_graph::{age_in_days, rfc3339_from_unix, rfc3339_utc, unix_from_rfc3339};
191// ADR-0020 §6's grant. Re-exported under `lint_`-prefixed names because the
192// concepts have twins in `rto-remote` (ADR-0019 §3) and a reader who meets
193// `ConfigGrant` in the binary must be able to see which of the two it is.
194pub use lint_grant::{
195    Backend as LintBackend, ConfigGrant as LintConfigGrant, Decision as LintDecision,
196    Reason as LintReason, Requested as LintRequested, decide as decide_lint_host,
197};
198#[cfg(all(feature = "exec-boxlite", feature = "exec-subprocess"))]
199pub use lint_sandbox::BuilderError as LintBuilderError;
200pub use runner::{
201    AnalysisRequest, AnalysisResponse, AnalyzerRunner, Consent, ExecError, Worktree,
202    check_reported_path, check_request, worktree_id,
203};
204pub use runtime_file_pins::{
205    PinnedFile, PinnedRuntimeFiles, RUNTIME_FILES, RUNTIME_FILES_VERSION, runtime_files_for,
206};
207pub use runtime_pins::{
208    PinnedArchive, RUNTIME_ARCHIVES, RUNTIME_ASSET, RUNTIME_FILE, RUNTIME_VERSION, archive_for,
209    runtime_target,
210};
211pub use sandbox_store::{
212    Attribution, CachedImage, ClearReport, ImageBytes, Objects, Preserved, RemovedImage,
213    SANDBOX_CLEAR_SCHEMA, SANDBOX_STATUS_SCHEMA, SANDBOX_STORE_DIR, SandboxStatus, Scope,
214    StoreError, Unattributed, VerifiedImage, clear as sandbox_clear, plan as sandbox_plan,
215    status as sandbox_status, store_root as sandbox_store_root,
216};
217pub use snippet::{NoSnippets, SnippetSource, WorktreeSnippets};
218#[cfg(feature = "exec-subprocess")]
219pub use subprocess::{SubprocessError, SubprocessRunner};
220pub use tool_security::{
221    AnalyzerCoverage, Coverage, CrossReference, CrossReferenceReport, LayerStaleness, MachineScope,
222    Readiness, RepositoryScope, SecurityListReport, TOOL_SECURITY_LIST_SCHEMA,
223    TOOL_SECURITY_STATUS_SCHEMA, ToolFindingsLayer, ToolSecurityList, ToolSecurityStatus,
224    coverage_matrix, coverage_matrix_with, layer_staleness, security_list, security_status,
225};
226
227/// The licence notice for the third-party binaries an `exec-boxlite` build
228/// embeds, compiled in so it cannot be separated from what it describes.
229///
230/// `roteiro security prefetch` prints it before installing the sandbox runtime,
231/// which is the same disclose-then-consent shape `roteiro model pull` uses. It
232/// is compiled into every build, because every build can provision the runtime —
233/// including one with no execution backend at all, which prefetches it for a
234/// later `exec-boxlite` build — so the obligations travel with the artifact
235/// rather than living only in the repository.
236pub const SANDBOX_RUNTIME_NOTICE: &str = include_str!("../NOTICE-boxlite-runtime.md");
237
238/// Lowercase hex SHA-256 of `bytes`.
239///
240/// Used for the report digest that ties an `AnalysisRun` to the exact bytes it
241/// was derived from, and for deriving an opaque worktree id from a path.
242#[must_use]
243pub fn sha256_hex(bytes: &[u8]) -> String {
244    use sha2::{Digest, Sha256};
245    let digest = Sha256::digest(bytes);
246    let mut out = String::with_capacity(64);
247    for byte in digest {
248        use std::fmt::Write as _;
249        let _ = write!(out, "{byte:02x}");
250    }
251    out
252}
253
254#[cfg(test)]
255mod tests {
256    use super::sha256_hex;
257
258    #[test]
259    fn hashes_the_known_vector() {
260        assert_eq!(
261            sha256_hex(b"abc"),
262            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
263        );
264    }
265}