Skip to main content

runtime/
offload.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! The shared shrink primitive: lossy-but-restorable,
7//! owned here and nowhere else. Four invariants, each enforced in code:
8//! store-before-cut; substitute (hint included) never larger than the
9//! original nor the cap; only lossy transforms store; the substitute
10//! always carries a materialized read-only rest path. CAS dedup makes
11//! repeated offloads of the same bytes idempotent.
12
13use std::path::Path;
14
15use kernel::{Address, AxCode, AxError, Locator};
16use storage::Cas;
17
18/// The directory inside the room that holds materialized rest files.
19pub const REST_DIR: &str = ".rest";
20
21/// Where offloaded bytes live: the CAS for the original, and the room
22/// whose `REST_DIR` holds the materialized rest file. The room travels
23/// as its city address because the model's read tool resolves every
24/// path from the city root. `origin` is the run and building the
25/// original is pinned for, so that a `cas:` read of it is judged where
26/// the output was made.
27pub struct OffloadSite<'a> {
28    pub cas: &'a mut Cas,
29    pub city_root: &'a Path,
30    pub room: &'a Address,
31    pub origin: storage::BlockOrigin,
32}
33
34/// The outcome: a substitute that fits the cap, the original pinned in
35/// CAS, and a read-only path the model can keep reading from.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct OffloadRecord {
38    pub substitute: Vec<u8>,
39    pub original: Locator,
40    pub rest_path: String,
41    pub original_len: u64,
42}
43
44fn hint_line(total: u64, rest_path: &str, locator: &Locator) -> String {
45    format!("\n[offloaded: total {total} bytes; rest at {rest_path}; original {locator}]")
46}
47
48/// Writes the rest file under the room and answers its city address,
49/// `<room>/.rest/rest-<hash>.dat`: the spelling the model's read tool
50/// admits and resolves from the city root, and the same bytes in the
51/// ledger on every machine.
52fn materialized(
53    bytes: &[u8],
54    site: &OffloadSite<'_>,
55    locator: &Locator,
56) -> Result<String, AxError> {
57    let name = rest_file_name(locator);
58    let room = site.room.as_str();
59    materialize(bytes, &site.city_root.join(room).join(REST_DIR), &name)?;
60    Ok(format!("{room}/{REST_DIR}/{name}"))
61}
62
63fn rest_file_name(locator: &Locator) -> String {
64    let mut tail: String = locator
65        .to_string()
66        .chars()
67        .rev()
68        .take(16)
69        .collect::<String>()
70        .chars()
71        .rev()
72        .collect();
73    tail.retain(|c| c.is_ascii_alphanumeric());
74    format!("rest-{tail}.dat")
75}
76
77fn materialize(bytes: &[u8], dir: &Path, name: &str) -> Result<(), AxError> {
78    let io = |err: std::io::Error| {
79        AxError::failure(
80            AxCode::StorageFatal,
81            "materialize rest file",
82            err.to_string(),
83        )
84        .with_recovery(
85            "free space on the disk holding the city, or fix the permissions of the \
86             run's environment directory, then ask for the result again",
87        )
88    };
89    let path = dir.join(name);
90    if path.exists() {
91        return Ok(());
92    }
93    std::fs::create_dir_all(dir).map_err(io)?;
94    std::fs::write(&path, bytes).map_err(io)?;
95    let mut perms = std::fs::metadata(&path).map_err(io)?.permissions();
96    perms.set_readonly(true);
97    std::fs::set_permissions(&path, perms).map_err(io)?;
98    Ok(())
99}
100
101/// The way back from a cut: the original pinned in the CAS and
102/// materialized as a read-only rest file.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub(crate) struct Tee {
105    pub(crate) original: Locator,
106    pub(crate) rest_path: String,
107}
108
109/// Pins the full original before anything is cut from it: invariants 1
110/// and 4 in one call, so no cutter can run without the way back
111/// existing first. Repeating it on the same bytes is idempotent.
112pub(crate) fn tee(bytes: &[u8], site: &mut OffloadSite<'_>) -> Result<Tee, AxError> {
113    let hash = site
114        .cas
115        .put_for(bytes, &site.origin)
116        .map_err(storage::StorageError::into_ax)?;
117    let original = Locator::cas(hash);
118    let rest_path = materialized(bytes, site, &original)?;
119    Ok(Tee {
120        original,
121        rest_path,
122    })
123}
124
125/// Shrinks `bytes` to at most `cap_bytes`. Callers only come here for
126/// lossy cases (`len > cap`); a lossless call is refused — invariant 3
127/// says only lossy transforms may store.
128pub fn offload(
129    bytes: &[u8],
130    cap_bytes: u64,
131    site: &mut OffloadSite<'_>,
132) -> Result<OffloadRecord, AxError> {
133    let original_len = u64::try_from(bytes.len()).map_err(|_| {
134        AxError::failure(AxCode::InvalidArgs, "offload result", "length exceeds u64").with_recovery(
135            "ask the tool for less at a time: this result is larger than a byte \
136                 count this city can hold",
137        )
138    })?;
139    if original_len <= cap_bytes {
140        return Err(AxError::failure(
141            AxCode::InvalidArgs,
142            "offload result",
143            "lossless input: only lossy transforms store (invariant 3)",
144        )
145        .with_recovery(
146            "pass this result through unchanged; it already fits the cap, and offload \
147             stores only what it has to cut",
148        ));
149    }
150    // Invariant 1: the full original enters the CAS before any cut.
151    // Invariant 4: the substitute always points at a readable rest file.
152    let Tee {
153        original,
154        rest_path,
155    } = tee(bytes, site)?;
156    let hint = hint_line(original_len, &rest_path, &original);
157    let hint_len = u64::try_from(hint.len()).map_err(|_| {
158        AxError::failure(AxCode::InvalidArgs, "offload result", "hint exceeds u64").with_recovery(
159            "report this against runtime::offload: the hint line names one path \
160                     and two byte counts and cannot reach this length",
161        )
162    })?;
163    let head_budget = cap_bytes.checked_sub(hint_len).ok_or_else(|| {
164        AxError::failure(
165            AxCode::InvalidArgs,
166            "offload result",
167            format!("cap {cap_bytes} cannot even hold the {hint_len}-byte hint"),
168        )
169        .with_recovery("raise the cap or skip offload for this result")
170    })?;
171    let head_len = usize::try_from(head_budget.min(original_len)).map_err(|_| {
172        AxError::failure(AxCode::InvalidArgs, "offload result", "head exceeds usize").with_recovery(
173            "lower `[tool] result_cap_bytes`: the kept head would be longer than \
174                 this machine can address",
175        )
176    })?;
177    let mut substitute = bytes.get(..head_len).unwrap_or_default().to_vec();
178    substitute.extend_from_slice(hint.as_bytes());
179    // Invariant 2: substitute (hint included) within both bounds.
180    let substitute_len = u64::try_from(substitute.len()).map_err(|_| {
181        AxError::failure(
182            AxCode::InvalidArgs,
183            "offload result",
184            "substitute exceeds u64",
185        )
186        .with_recovery(
187            "lower `[tool] result_cap_bytes`: the shortened result is longer than a \
188             byte count this city can hold",
189        )
190    })?;
191    if substitute_len > cap_bytes || substitute_len > original_len {
192        return Err(AxError::failure(
193            AxCode::InvalidArgs,
194            "offload result",
195            "substitute outgrew its bounds (invariant 2)",
196        )
197        .with_recovery(
198            "report this against runtime::offload: the shortened result must stay \
199             within both the cap and the original length, and it did not",
200        ));
201    }
202    Ok(OffloadRecord {
203        substitute,
204        original,
205        rest_path,
206        original_len,
207    })
208}
209
210/// Restores the rest file from the CAS after external cleanup; the bytes
211/// are the original, verbatim (A7's third assertion).
212pub fn rematerialize(locator: &Locator, site: &mut OffloadSite<'_>) -> Result<String, AxError> {
213    let Locator::Cas { hash, .. } = locator else {
214        return Err(AxError::failure(
215            AxCode::InvalidArgs,
216            "rematerialize rest file",
217            "locator is not cas-addressed",
218        )
219        .with_recovery(
220            "pass the `cas:` locator the offload record carries under `original`; a \
221             `file:` locator names a path that cleanup already removed",
222        ));
223    };
224    let bytes = site.cas.get(hash).map_err(storage::StorageError::into_ax)?;
225    materialized(&bytes, site, locator)
226}
227
228#[cfg(test)]
229#[allow(
230    clippy::unwrap_used,
231    clippy::expect_used,
232    clippy::panic,
233    clippy::indexing_slicing,
234    reason = "test code"
235)]
236pub(crate) mod tests {
237    use super::*;
238
239    pub(crate) fn origin() -> storage::BlockOrigin {
240        storage::BlockOrigin {
241            run: kernel::RunId::from_bytes([7; 16]),
242            building: kernel::Address::parse("lab").unwrap(),
243        }
244    }
245
246    fn site(dir: &tempfile::TempDir) -> (Cas, Address) {
247        let cas = Cas::open(&dir.path().join("cas")).unwrap();
248        (cas, Address::parse("room").unwrap())
249    }
250
251    #[test]
252    fn a7_roundtrip_all_four_assertions() {
253        let dir = tempfile::tempdir().unwrap();
254        let (mut cas, room) = site(&dir);
255        let mut s = OffloadSite {
256            cas: &mut cas,
257            city_root: dir.path(),
258            room: &room,
259            origin: origin(),
260        };
261        let original: Vec<u8> = (0..40_000u32)
262            .map(|i| u8::try_from(i % 251).unwrap())
263            .collect();
264        let record = offload(&original, 4_096, &mut s).unwrap();
265        // Substitute within both bounds, hint included.
266        assert!(record.substitute.len() <= 4_096);
267        assert!(record.substitute.len() < original.len());
268        assert!(
269            String::from_utf8_lossy(&record.substitute).contains("[offloaded: total 40000 bytes")
270        );
271        assert!(
272            String::from_utf8_lossy(&record.substitute).contains("; rest at room/.rest/rest-"),
273            "the hint names the rest file by its address in the room"
274        );
275        // The rest path serves the full original.
276        assert_eq!(
277            std::fs::read(dir.path().join(&record.rest_path)).unwrap(),
278            original
279        );
280        // The CAS serves the full original by locator.
281        let Locator::Cas { hash, .. } = &record.original else {
282            panic!("expected a cas locator");
283        };
284        assert_eq!(s.cas.get(hash).unwrap(), original);
285        // External cleanup, then rematerialize: bytes identical.
286        let mut perms = std::fs::metadata(dir.path().join(&record.rest_path))
287            .unwrap()
288            .permissions();
289        #[allow(clippy::permissions_set_readonly_false, reason = "test cleanup")]
290        perms.set_readonly(false);
291        std::fs::set_permissions(dir.path().join(&record.rest_path), perms).unwrap();
292        std::fs::remove_file(dir.path().join(&record.rest_path)).unwrap();
293        let back = rematerialize(&record.original, &mut s).unwrap();
294        assert_eq!(std::fs::read(dir.path().join(&back)).unwrap(), original);
295    }
296
297    #[test]
298    fn the_rest_address_is_one_the_read_tool_admits_and_resolves() {
299        let dir = tempfile::tempdir().unwrap();
300        let (mut cas, room) = site(&dir);
301        let mut s = OffloadSite {
302            cas: &mut cas,
303            city_root: dir.path(),
304            room: &room,
305            origin: origin(),
306        };
307        let original = vec![3u8; 30_000];
308        let record = offload(&original, 2_048, &mut s).unwrap();
309        let admitted =
310            crate::tools::admit(&record.rest_path, "read", &|_| kernel::ReadVerdict::Open).unwrap();
311        assert_eq!(
312            std::fs::read(dir.path().join(admitted.as_str())).unwrap(),
313            original,
314            "the read tool resolves the rest address from the city root"
315        );
316    }
317
318    #[test]
319    fn the_original_is_pinned_for_the_run_and_building_it_was_cut_for() {
320        let dir = tempfile::tempdir().unwrap();
321        let (mut cas, room) = site(&dir);
322        let mut s = OffloadSite {
323            cas: &mut cas,
324            city_root: dir.path(),
325            room: &room,
326            origin: origin(),
327        };
328        let record = offload(&vec![3u8; 30_000], 2_048, &mut s).unwrap();
329        let Locator::Cas { hash, .. } = &record.original else {
330            panic!("expected a cas locator");
331        };
332        assert_eq!(s.cas.origins(hash).unwrap(), vec![origin()]);
333    }
334
335    #[test]
336    fn same_bytes_offload_to_the_same_locator() {
337        let dir = tempfile::tempdir().unwrap();
338        let (mut cas, room) = site(&dir);
339        let mut s = OffloadSite {
340            cas: &mut cas,
341            city_root: dir.path(),
342            room: &room,
343            origin: origin(),
344        };
345        let original = vec![7u8; 30_000];
346        let one = offload(&original, 2_048, &mut s).unwrap();
347        let two = offload(&original, 2_048, &mut s).unwrap();
348        assert_eq!(
349            one.original, two.original,
350            "CAS dedup makes offload idempotent"
351        );
352        assert_eq!(one.rest_path, two.rest_path);
353    }
354
355    #[test]
356    fn lossless_input_and_hopeless_caps_are_refused() {
357        let dir = tempfile::tempdir().unwrap();
358        let (mut cas, room) = site(&dir);
359        let mut s = OffloadSite {
360            cas: &mut cas,
361            city_root: dir.path(),
362            room: &room,
363            origin: origin(),
364        };
365        let err = offload(b"small", 4_096, &mut s).unwrap_err();
366        assert_eq!(*err.code(), AxCode::InvalidArgs);
367        let big = vec![1u8; 20_000];
368        let err = offload(&big, 16, &mut s).unwrap_err();
369        assert!(err.subject().contains("hint"));
370    }
371}