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
8const 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(¤t) {
624 continue;
625 }
626
627 filesystem.create_dir(¤t)?;
628 filesystem.chmod(¤t, 0o755)?;
629 filesystem.chown(¤t, 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}