Skip to main content

agent_os_kernel/
root_fs.rs

1use crate::overlay_fs::{OverlayFileSystem, OverlayMode};
2use crate::vfs::{
3    normalize_path, MemoryFileSystem, VfsError, VfsResult, VirtualFileSystem, VirtualUtimeSpec,
4};
5use base64::Engine;
6use serde::Deserialize;
7
8// The base filesystem fixture is staged into OUT_DIR by build.rs: copied from
9// the canonical `packages/core/fixtures/base-filesystem.json` during in-tree
10// builds, or from the vendored `assets/base-filesystem.json` copy bundled in
11// the published crate.
12const BUNDLED_BASE_FILESYSTEM_JSON: &str =
13    include_str!(concat!(env!("OUT_DIR"), "/base-filesystem.json"));
14pub const ROOT_FILESYSTEM_SNAPSHOT_FORMAT: &str = "agent_os_filesystem_snapshot_v1";
15const DEFAULT_ROOT_DIRECTORIES: &[&str] = &[
16    "/",
17    "/dev",
18    "/proc",
19    "/tmp",
20    "/bin",
21    "/lib",
22    "/sbin",
23    "/boot",
24    "/etc",
25    "/root",
26    "/run",
27    "/srv",
28    "/sys",
29    "/opt",
30    "/mnt",
31    "/media",
32    "/home",
33    "/usr",
34    "/usr/bin",
35    "/usr/games",
36    "/usr/include",
37    "/usr/lib",
38    "/usr/libexec",
39    "/usr/man",
40    "/usr/local",
41    "/usr/local/bin",
42    "/usr/sbin",
43    "/usr/share",
44    "/usr/share/man",
45    "/var",
46    "/var/cache",
47    "/var/empty",
48    "/var/lib",
49    "/var/lock",
50    "/var/log",
51    "/var/run",
52    "/var/spool",
53    "/var/tmp",
54    "/etc/agentos",
55];
56const KERNEL_RESERVED_BOOTSTRAP_PATH_PREFIXES: &[&str] = &["/dev", "/proc", "/sys"];
57
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct RootFilesystemError {
60    message: String,
61}
62
63impl RootFilesystemError {
64    fn new(message: impl Into<String>) -> Self {
65        Self {
66            message: message.into(),
67        }
68    }
69}
70
71impl std::fmt::Display for RootFilesystemError {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        f.write_str(&self.message)
74    }
75}
76
77impl std::error::Error for RootFilesystemError {}
78
79impl From<VfsError> for RootFilesystemError {
80    fn from(error: VfsError) -> Self {
81        Self::new(error.to_string())
82    }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub enum FilesystemEntryKind {
87    File,
88    Directory,
89    Symlink,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct FilesystemEntry {
94    pub path: String,
95    pub kind: FilesystemEntryKind,
96    pub mode: u32,
97    pub uid: u32,
98    pub gid: u32,
99    pub content: Option<Vec<u8>>,
100    pub target: Option<String>,
101}
102
103impl FilesystemEntry {
104    pub fn directory(path: impl Into<String>) -> Self {
105        Self {
106            path: path.into(),
107            kind: FilesystemEntryKind::Directory,
108            mode: 0o755,
109            uid: 0,
110            gid: 0,
111            content: None,
112            target: None,
113        }
114    }
115
116    pub fn file(path: impl Into<String>, content: impl Into<Vec<u8>>) -> Self {
117        Self {
118            path: path.into(),
119            kind: FilesystemEntryKind::File,
120            mode: 0o644,
121            uid: 0,
122            gid: 0,
123            content: Some(content.into()),
124            target: None,
125        }
126    }
127
128    pub fn symlink(path: impl Into<String>, target: impl Into<String>) -> Self {
129        Self {
130            path: path.into(),
131            kind: FilesystemEntryKind::Symlink,
132            mode: 0o777,
133            uid: 0,
134            gid: 0,
135            content: None,
136            target: Some(target.into()),
137        }
138    }
139}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct RootFilesystemSnapshot {
143    pub entries: Vec<FilesystemEntry>,
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum RootFilesystemMode {
148    Ephemeral,
149    ReadOnly,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct RootFilesystemDescriptor {
154    pub mode: RootFilesystemMode,
155    pub disable_default_base_layer: bool,
156    pub lowers: Vec<RootFilesystemSnapshot>,
157    pub bootstrap_entries: Vec<FilesystemEntry>,
158}
159
160impl Default for RootFilesystemDescriptor {
161    fn default() -> Self {
162        Self {
163            mode: RootFilesystemMode::Ephemeral,
164            disable_default_base_layer: false,
165            lowers: Vec::new(),
166            bootstrap_entries: Vec::new(),
167        }
168    }
169}
170
171#[derive(Debug)]
172pub struct RootFileSystem {
173    overlay: OverlayFileSystem,
174    mode: RootFilesystemMode,
175    bootstrap_finished: bool,
176}
177
178impl RootFileSystem {
179    pub fn from_descriptor(
180        descriptor: RootFilesystemDescriptor,
181    ) -> Result<Self, RootFilesystemError> {
182        let mut lower_snapshots = descriptor.lowers.clone();
183        if !descriptor.disable_default_base_layer {
184            lower_snapshots.push(load_bundled_base_snapshot()?);
185        } else if lower_snapshots.is_empty() {
186            lower_snapshots.push(minimal_root_snapshot());
187        }
188
189        let lowers = lower_snapshots
190            .iter()
191            .map(snapshot_to_memory_filesystem)
192            .collect::<Result<Vec<_>, _>>()?;
193
194        let mut root = Self {
195            overlay: OverlayFileSystem::new(lowers, OverlayMode::Ephemeral),
196            mode: descriptor.mode,
197            bootstrap_finished: false,
198        };
199        root.apply_bootstrap_entries(&descriptor.bootstrap_entries)?;
200        Ok(root)
201    }
202
203    pub fn apply_bootstrap_entries(
204        &mut self,
205        entries: &[FilesystemEntry],
206    ) -> Result<(), RootFilesystemError> {
207        if self.bootstrap_finished {
208            return Err(RootFilesystemError::new(
209                "root filesystem bootstrap is already finished",
210            ));
211        }
212
213        for entry in sort_entries(entries.to_vec()) {
214            if is_kernel_reserved_bootstrap_path(&entry.path) {
215                continue;
216            }
217            apply_entry(&mut self.overlay, &entry)?;
218        }
219        Ok(())
220    }
221
222    pub fn finish_bootstrap(&mut self) {
223        if self.bootstrap_finished {
224            return;
225        }
226        self.bootstrap_finished = true;
227        if self.mode == RootFilesystemMode::ReadOnly {
228            self.overlay.lock_writes();
229        }
230    }
231
232    pub fn snapshot(&mut self) -> Result<RootFilesystemSnapshot, RootFilesystemError> {
233        Ok(RootFilesystemSnapshot {
234            entries: snapshot_virtual_filesystem(&mut self.overlay, "/")?,
235        })
236    }
237}
238
239impl VirtualFileSystem for RootFileSystem {
240    fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>> {
241        self.overlay.read_file(path)
242    }
243
244    fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>> {
245        self.overlay.read_dir(path)
246    }
247
248    fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
249        self.overlay.read_dir_limited(path, max_entries)
250    }
251
252    fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<crate::vfs::VirtualDirEntry>> {
253        self.overlay.read_dir_with_types(path)
254    }
255
256    fn write_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
257        self.overlay.write_file(path, content.into())
258    }
259
260    fn create_file_exclusive(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
261        self.overlay.create_file_exclusive(path, content.into())
262    }
263
264    fn append_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<u64> {
265        self.overlay.append_file(path, content.into())
266    }
267
268    fn create_dir(&mut self, path: &str) -> VfsResult<()> {
269        self.overlay.create_dir(path)
270    }
271
272    fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()> {
273        self.overlay.mkdir(path, recursive)
274    }
275
276    fn exists(&self, path: &str) -> bool {
277        self.overlay.exists(path)
278    }
279
280    fn stat(&mut self, path: &str) -> VfsResult<crate::vfs::VirtualStat> {
281        self.overlay.stat(path)
282    }
283
284    fn remove_file(&mut self, path: &str) -> VfsResult<()> {
285        self.overlay.remove_file(path)
286    }
287
288    fn remove_dir(&mut self, path: &str) -> VfsResult<()> {
289        self.overlay.remove_dir(path)
290    }
291
292    fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
293        self.overlay.rename(old_path, new_path)
294    }
295
296    fn realpath(&self, path: &str) -> VfsResult<String> {
297        self.overlay.realpath(path)
298    }
299
300    fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()> {
301        self.overlay.symlink(target, link_path)
302    }
303
304    fn read_link(&self, path: &str) -> VfsResult<String> {
305        self.overlay.read_link(path)
306    }
307
308    fn lstat(&self, path: &str) -> VfsResult<crate::vfs::VirtualStat> {
309        self.overlay.lstat(path)
310    }
311
312    fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
313        self.overlay.link(old_path, new_path)
314    }
315
316    fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()> {
317        self.overlay.chmod(path, mode)
318    }
319
320    fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
321        self.overlay.chown(path, uid, gid)
322    }
323
324    fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> {
325        self.overlay.utimes(path, atime_ms, mtime_ms)
326    }
327
328    fn utimes_spec(
329        &mut self,
330        path: &str,
331        atime: VirtualUtimeSpec,
332        mtime: VirtualUtimeSpec,
333        follow_symlinks: bool,
334    ) -> VfsResult<()> {
335        self.overlay
336            .utimes_spec(path, atime, mtime, follow_symlinks)
337    }
338
339    fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()> {
340        self.overlay.truncate(path, length)
341    }
342
343    fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
344        self.overlay.pread(path, offset, length)
345    }
346}
347
348#[derive(Debug, Deserialize)]
349struct RawBaseFilesystemSnapshot {
350    filesystem: RawFilesystemEntries,
351}
352
353#[derive(Debug, Deserialize)]
354struct RawFilesystemEntries {
355    entries: Vec<RawFilesystemEntry>,
356}
357
358#[derive(Debug, Deserialize)]
359struct RawFilesystemEntry {
360    path: String,
361    #[serde(rename = "type")]
362    kind: RawFilesystemEntryKind,
363    mode: String,
364    uid: u32,
365    gid: u32,
366    #[serde(default)]
367    content: Option<String>,
368    #[serde(default)]
369    encoding: Option<String>,
370    #[serde(default)]
371    target: Option<String>,
372}
373
374#[derive(Debug, Deserialize)]
375#[serde(rename_all = "snake_case")]
376enum RawFilesystemEntryKind {
377    File,
378    Directory,
379    Symlink,
380}
381
382#[derive(Debug, Deserialize)]
383struct RawSnapshotExport {
384    format: String,
385    filesystem: RawFilesystemEntries,
386}
387
388#[derive(Debug, serde::Serialize)]
389struct SnapshotExport<'a> {
390    format: &'static str,
391    filesystem: SnapshotFilesystem<'a>,
392}
393
394#[derive(Debug, serde::Serialize)]
395struct SnapshotFilesystem<'a> {
396    entries: Vec<SerializedFilesystemEntry<'a>>,
397}
398
399#[derive(Debug, serde::Serialize)]
400struct SerializedFilesystemEntry<'a> {
401    path: &'a str,
402    #[serde(rename = "type")]
403    kind: &'static str,
404    mode: String,
405    uid: u32,
406    gid: u32,
407    #[serde(skip_serializing_if = "Option::is_none")]
408    content: Option<String>,
409    #[serde(skip_serializing_if = "Option::is_none")]
410    encoding: Option<&'static str>,
411    #[serde(skip_serializing_if = "Option::is_none")]
412    target: Option<&'a str>,
413}
414
415pub fn encode_snapshot(snapshot: &RootFilesystemSnapshot) -> Result<Vec<u8>, RootFilesystemError> {
416    let serialized_entries = snapshot
417        .entries
418        .iter()
419        .map(|entry| SerializedFilesystemEntry {
420            path: &entry.path,
421            kind: match entry.kind {
422                FilesystemEntryKind::File => "file",
423                FilesystemEntryKind::Directory => "directory",
424                FilesystemEntryKind::Symlink => "symlink",
425            },
426            mode: format!("{:o}", entry.mode),
427            uid: entry.uid,
428            gid: entry.gid,
429            content: entry
430                .content
431                .as_ref()
432                .map(|bytes| base64::engine::general_purpose::STANDARD.encode(bytes)),
433            encoding: entry.content.as_ref().map(|_| "base64"),
434            target: entry.target.as_deref(),
435        })
436        .collect::<Vec<_>>();
437
438    serde_json::to_vec(&SnapshotExport {
439        format: ROOT_FILESYSTEM_SNAPSHOT_FORMAT,
440        filesystem: SnapshotFilesystem {
441            entries: serialized_entries,
442        },
443    })
444    .map_err(|error| RootFilesystemError::new(format!("serialize root snapshot: {error}")))
445}
446
447pub fn decode_snapshot(bytes: &[u8]) -> Result<RootFilesystemSnapshot, RootFilesystemError> {
448    let raw: RawSnapshotExport = serde_json::from_slice(bytes)
449        .map_err(|error| RootFilesystemError::new(format!("parse root snapshot: {error}")))?;
450    if raw.format != ROOT_FILESYSTEM_SNAPSHOT_FORMAT {
451        return Err(RootFilesystemError::new(format!(
452            "unsupported root snapshot format: {}",
453            raw.format
454        )));
455    }
456    Ok(RootFilesystemSnapshot {
457        entries: raw
458            .filesystem
459            .entries
460            .into_iter()
461            .map(convert_raw_entry)
462            .collect::<Result<Vec<_>, _>>()?,
463    })
464}
465
466fn load_bundled_base_snapshot() -> Result<RootFilesystemSnapshot, RootFilesystemError> {
467    let raw: RawBaseFilesystemSnapshot = serde_json::from_str(BUNDLED_BASE_FILESYSTEM_JSON)
468        .map_err(|error| {
469            RootFilesystemError::new(format!("parse bundled base filesystem: {error}"))
470        })?;
471    Ok(RootFilesystemSnapshot {
472        entries: raw
473            .filesystem
474            .entries
475            .into_iter()
476            .map(convert_raw_entry)
477            .collect::<Result<Vec<_>, _>>()?,
478    })
479}
480
481fn minimal_root_snapshot() -> RootFilesystemSnapshot {
482    let mut entries = DEFAULT_ROOT_DIRECTORIES
483        .iter()
484        .map(|path| FilesystemEntry::directory(*path))
485        .collect::<Vec<_>>();
486    entries.push(FilesystemEntry::file("/usr/bin/env", Vec::new()));
487    RootFilesystemSnapshot { entries }
488}
489
490fn convert_raw_entry(raw: RawFilesystemEntry) -> Result<FilesystemEntry, RootFilesystemError> {
491    let content = match raw.content {
492        Some(content) => match raw.encoding.as_deref() {
493            Some("base64") => Some(
494                base64::engine::general_purpose::STANDARD
495                    .decode(content)
496                    .map_err(|error| {
497                        RootFilesystemError::new(format!(
498                            "decode base64 content for {}: {error}",
499                            raw.path
500                        ))
501                    })?,
502            ),
503            Some("utf8") | None => Some(content.into_bytes()),
504            Some(other) => {
505                return Err(RootFilesystemError::new(format!(
506                    "unsupported content encoding for {}: {other}",
507                    raw.path
508                )))
509            }
510        },
511        None => None,
512    };
513
514    Ok(FilesystemEntry {
515        path: raw.path,
516        kind: match raw.kind {
517            RawFilesystemEntryKind::File => FilesystemEntryKind::File,
518            RawFilesystemEntryKind::Directory => FilesystemEntryKind::Directory,
519            RawFilesystemEntryKind::Symlink => FilesystemEntryKind::Symlink,
520        },
521        mode: u32::from_str_radix(&raw.mode, 8).map_err(|error| {
522            RootFilesystemError::new(format!("parse mode {}: {error}", raw.mode))
523        })?,
524        uid: raw.uid,
525        gid: raw.gid,
526        content,
527        target: raw.target,
528    })
529}
530
531fn snapshot_to_memory_filesystem(
532    snapshot: &RootFilesystemSnapshot,
533) -> Result<MemoryFileSystem, RootFilesystemError> {
534    let mut filesystem = MemoryFileSystem::new();
535    for entry in sort_entries(snapshot.entries.clone()) {
536        apply_entry_to_memory_filesystem(&mut filesystem, &entry)?;
537    }
538    Ok(filesystem)
539}
540
541fn apply_entry_to_memory_filesystem(
542    filesystem: &mut MemoryFileSystem,
543    entry: &FilesystemEntry,
544) -> Result<(), RootFilesystemError> {
545    ensure_parent_directories(filesystem, &entry.path)?;
546
547    match entry.kind {
548        FilesystemEntryKind::Directory => {
549            filesystem.mkdir(&entry.path, true)?;
550            filesystem.chmod(&entry.path, entry.mode)?;
551            filesystem.chown(&entry.path, entry.uid, entry.gid)?;
552        }
553        FilesystemEntryKind::File => {
554            filesystem.write_file(&entry.path, entry.content.clone().unwrap_or_default())?;
555            filesystem.chmod(&entry.path, entry.mode)?;
556            filesystem.chown(&entry.path, entry.uid, entry.gid)?;
557        }
558        FilesystemEntryKind::Symlink => {
559            let Some(target) = entry.target.as_deref() else {
560                return Err(RootFilesystemError::new(format!(
561                    "missing symlink target for {}",
562                    entry.path
563                )));
564            };
565            filesystem.symlink_with_metadata(
566                target,
567                &entry.path,
568                entry.mode,
569                entry.uid,
570                entry.gid,
571            )?;
572        }
573    }
574
575    Ok(())
576}
577
578fn apply_entry(
579    filesystem: &mut impl VirtualFileSystem,
580    entry: &FilesystemEntry,
581) -> Result<(), RootFilesystemError> {
582    ensure_parent_directories(filesystem, &entry.path)?;
583
584    match entry.kind {
585        FilesystemEntryKind::Directory => {
586            filesystem.mkdir(&entry.path, true)?;
587            filesystem.chmod(&entry.path, entry.mode)?;
588            filesystem.chown(&entry.path, entry.uid, entry.gid)?;
589        }
590        FilesystemEntryKind::File => {
591            filesystem.write_file(&entry.path, entry.content.clone().unwrap_or_default())?;
592            filesystem.chmod(&entry.path, entry.mode)?;
593            filesystem.chown(&entry.path, entry.uid, entry.gid)?;
594        }
595        FilesystemEntryKind::Symlink => {
596            let Some(target) = entry.target.as_deref() else {
597                return Err(RootFilesystemError::new(format!(
598                    "missing symlink target for {}",
599                    entry.path
600                )));
601            };
602            filesystem.symlink(target, &entry.path)?;
603        }
604    }
605
606    Ok(())
607}
608
609fn ensure_parent_directories(
610    filesystem: &mut impl VirtualFileSystem,
611    path: &str,
612) -> Result<(), RootFilesystemError> {
613    let mut current = String::new();
614    let segments = path
615        .split('/')
616        .filter(|segment| !segment.is_empty())
617        .collect::<Vec<_>>();
618
619    for segment in segments.iter().take(segments.len().saturating_sub(1)) {
620        current.push('/');
621        current.push_str(segment);
622
623        if filesystem.exists(&current) {
624            continue;
625        }
626
627        filesystem.create_dir(&current)?;
628        filesystem.chmod(&current, 0o755)?;
629        filesystem.chown(&current, 0, 0)?;
630    }
631
632    Ok(())
633}
634
635fn sort_entries(mut entries: Vec<FilesystemEntry>) -> Vec<FilesystemEntry> {
636    entries.sort_by(|left, right| {
637        let depth_left = if left.path == "/" {
638            0
639        } else {
640            left.path.split('/').filter(|part| !part.is_empty()).count()
641        };
642        let depth_right = if right.path == "/" {
643            0
644        } else {
645            right
646                .path
647                .split('/')
648                .filter(|part| !part.is_empty())
649                .count()
650        };
651        depth_left
652            .cmp(&depth_right)
653            .then_with(|| left.path.cmp(&right.path))
654    });
655    entries
656}
657
658fn snapshot_virtual_filesystem(
659    filesystem: &mut impl VirtualFileSystem,
660    root_path: &str,
661) -> Result<Vec<FilesystemEntry>, RootFilesystemError> {
662    let mut entries = Vec::new();
663    snapshot_path(filesystem, root_path, &mut entries)?;
664    Ok(entries)
665}
666
667fn snapshot_path(
668    filesystem: &mut impl VirtualFileSystem,
669    path: &str,
670    entries: &mut Vec<FilesystemEntry>,
671) -> Result<(), RootFilesystemError> {
672    let stat = if path == "/" {
673        filesystem.stat(path)?
674    } else {
675        filesystem.lstat(path)?
676    };
677
678    if stat.is_symbolic_link {
679        entries.push(FilesystemEntry {
680            path: path.to_owned(),
681            kind: FilesystemEntryKind::Symlink,
682            mode: stat.mode,
683            uid: stat.uid,
684            gid: stat.gid,
685            content: None,
686            target: Some(filesystem.read_link(path)?),
687        });
688        return Ok(());
689    }
690
691    if stat.is_directory {
692        entries.push(FilesystemEntry {
693            path: path.to_owned(),
694            kind: FilesystemEntryKind::Directory,
695            mode: stat.mode,
696            uid: stat.uid,
697            gid: stat.gid,
698            content: None,
699            target: None,
700        });
701
702        let mut children = filesystem
703            .read_dir_with_types(path)?
704            .into_iter()
705            .map(|entry| entry.name)
706            .filter(|name| name != "." && name != "..")
707            .collect::<Vec<_>>();
708        children.sort();
709
710        for child in children {
711            let child_path = if path == "/" {
712                format!("/{child}")
713            } else {
714                format!("{path}/{child}")
715            };
716            snapshot_path(filesystem, &child_path, entries)?;
717        }
718        return Ok(());
719    }
720
721    entries.push(FilesystemEntry {
722        path: path.to_owned(),
723        kind: FilesystemEntryKind::File,
724        mode: stat.mode,
725        uid: stat.uid,
726        gid: stat.gid,
727        content: Some(filesystem.read_file(path)?),
728        target: None,
729    });
730    Ok(())
731}
732
733fn is_kernel_reserved_bootstrap_path(path: &str) -> bool {
734    let normalized = normalize_path(path);
735    KERNEL_RESERVED_BOOTSTRAP_PATH_PREFIXES
736        .iter()
737        .any(|prefix| normalized == *prefix || normalized.starts_with(&format!("{prefix}/")))
738}