1use std::sync::Arc;
14
15use anyhow::{Context, Result};
16use async_trait::async_trait;
17use base64::engine::general_purpose::STANDARD as BASE64;
18use base64::Engine as _;
19use serde::{Deserialize, Serialize};
20
21use secure_exec_client::wire::{
22 self, GuestFilesystemCallRequest, GuestFilesystemOperation, GuestFilesystemResultResponse,
23 GuestFilesystemStat, RootFilesystemEntry, RootFilesystemEntryEncoding, RootFilesystemEntryKind,
24};
25
26use crate::agent_os::AgentOs;
27use crate::error::ClientError;
28
29#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum FileContent {
36 Text(String),
37 Bytes(Vec<u8>),
38}
39
40impl From<String> for FileContent {
41 fn from(value: String) -> Self {
42 FileContent::Text(value)
43 }
44}
45
46impl From<&str> for FileContent {
47 fn from(value: &str) -> Self {
48 FileContent::Text(value.to_string())
49 }
50}
51
52impl From<Vec<u8>> for FileContent {
53 fn from(value: Vec<u8>) -> Self {
54 FileContent::Bytes(value)
55 }
56}
57
58impl From<&[u8]> for FileContent {
59 fn from(value: &[u8]) -> Self {
60 FileContent::Bytes(value.to_vec())
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66pub struct DirEntry {
67 pub path: String,
68 #[serde(rename = "type")]
69 pub entry_type: DirEntryType,
70 pub size: u64,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(rename_all = "lowercase")]
76pub enum DirEntryType {
77 File,
78 Directory,
79 Symlink,
80}
81
82#[derive(Debug, Clone, Default, PartialEq, Eq)]
85pub struct ReaddirRecursiveOptions {
86 pub max_depth: Option<u32>,
87 pub exclude: Vec<String>,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct BatchWriteEntry {
93 pub path: String,
94 pub content: FileContent,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct BatchWriteResult {
100 pub path: String,
101 pub success: bool,
102 pub error: Option<String>,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct BatchReadResult {
108 pub path: String,
109 pub content: Option<Vec<u8>>,
110 pub error: Option<String>,
111}
112
113#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
115pub struct MkdirOptions {
116 pub recursive: bool,
117}
118
119#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
121pub struct DeleteOptions {
122 pub recursive: bool,
123}
124
125#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
127pub struct MountFsOptions {
128 pub read_only: bool,
129}
130
131#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
133pub struct VirtualStat {
134 pub mode: u32,
135 pub size: u64,
136 pub blocks: u64,
137 pub dev: u64,
138 pub rdev: u64,
139 #[serde(rename = "isDirectory")]
140 pub is_directory: bool,
141 #[serde(rename = "isSymbolicLink")]
142 pub is_symbolic_link: bool,
143 #[serde(rename = "atimeMs")]
144 pub atime_ms: f64,
145 #[serde(rename = "mtimeMs")]
146 pub mtime_ms: f64,
147 #[serde(rename = "ctimeMs")]
148 pub ctime_ms: f64,
149 #[serde(rename = "birthtimeMs")]
150 pub birthtime_ms: f64,
151 pub ino: u64,
152 pub nlink: u64,
153 pub uid: u32,
154 pub gid: u32,
155}
156
157#[derive(Clone)]
162pub struct MountedFs {
163 pub driver: Arc<dyn VirtualFileSystem>,
164 pub read_only: bool,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct VirtualDirEntry {
170 pub name: String,
171 pub is_directory: bool,
172 pub is_symbolic_link: bool,
173}
174
175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181pub struct RootSnapshotExport {
182 pub kind: SnapshotExportKind,
183 pub source: FilesystemSnapshotExport,
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
188pub enum SnapshotExportKind {
189 #[serde(rename = "snapshot-export")]
190 SnapshotExport,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195pub struct FilesystemSnapshotExport {
196 pub format: String,
197 pub filesystem: FilesystemSnapshotEntries,
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
202pub struct FilesystemSnapshotEntries {
203 pub entries: Vec<FilesystemEntry>,
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
208pub struct FilesystemEntry {
209 pub path: String,
210 #[serde(rename = "type")]
211 pub entry_type: DirEntryType,
212 pub mode: String,
213 pub uid: u32,
214 pub gid: u32,
215 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub content: Option<String>,
217 #[serde(default, skip_serializing_if = "Option::is_none")]
218 pub encoding: Option<FilesystemEntryEncoding>,
219 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub target: Option<String>,
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
225#[serde(rename_all = "lowercase")]
226pub enum FilesystemEntryEncoding {
227 Utf8,
228 Base64,
229}
230
231#[async_trait]
240pub trait VirtualFileSystem: Send + Sync {
241 async fn read_file(&self, path: &str) -> Result<Vec<u8>>;
242 async fn read_text_file(&self, path: &str) -> Result<String>;
243 async fn read_dir(&self, path: &str) -> Result<Vec<String>>;
244 async fn read_dir_with_types(&self, path: &str) -> Result<Vec<VirtualDirEntry>>;
245 async fn write_file(&self, path: &str, content: &[u8]) -> Result<()>;
246 async fn create_dir(&self, path: &str) -> Result<()>;
247 async fn mkdir(&self, path: &str, recursive: bool) -> Result<()>;
248 async fn exists(&self, path: &str) -> Result<bool>;
249 async fn stat(&self, path: &str) -> Result<VirtualStat>;
250 async fn lstat(&self, path: &str) -> Result<VirtualStat>;
251 async fn remove_file(&self, path: &str) -> Result<()>;
252 async fn remove_dir(&self, path: &str) -> Result<()>;
253 async fn rename(&self, from: &str, to: &str) -> Result<()>;
254 async fn realpath(&self, path: &str) -> Result<String>;
255 async fn symlink(&self, target: &str, path: &str) -> Result<()>;
256 async fn readlink(&self, path: &str) -> Result<String>;
257 async fn link(&self, existing: &str, new_path: &str) -> Result<()>;
258 async fn chmod(&self, path: &str, mode: u32) -> Result<()>;
259 async fn chown(&self, path: &str, uid: u32, gid: u32) -> Result<()>;
260 async fn utimes(&self, path: &str, atime_ms: f64, mtime_ms: f64) -> Result<()>;
261 async fn truncate(&self, path: &str, len: u64) -> Result<()>;
262 async fn pread(&self, path: &str, offset: u64, length: u64) -> Result<Vec<u8>>;
263 async fn pwrite(&self, path: &str, offset: u64, data: &[u8]) -> Result<u64>;
264}
265
266impl AgentOs {
271 pub(crate) fn posix_normalize(path: &str) -> String {
278 if path.is_empty() {
279 return String::from(".");
280 }
281
282 let is_absolute = path.starts_with('/');
283 let trailing_slash = path.ends_with('/');
284
285 let mut segments: Vec<&str> = Vec::new();
286 for part in path.split('/') {
287 match part {
288 "" | "." => {}
289 ".." => {
290 match segments.last().copied() {
291 Some(last) if last != ".." => {
292 segments.pop();
293 }
294 Some(_) | None => {
295 if !is_absolute {
298 segments.push("..");
299 }
300 }
301 }
302 }
303 other => segments.push(other),
304 }
305 }
306
307 let mut joined = segments.join("/");
308 if joined.is_empty() {
309 if is_absolute {
310 return String::from("/");
311 }
312 return String::from(".");
313 }
314
315 if trailing_slash {
316 joined.push('/');
317 }
318 if is_absolute {
319 let mut absolute = String::from("/");
320 absolute.push_str(&joined);
321 absolute
322 } else {
323 joined
324 }
325 }
326
327 pub(crate) fn assert_safe_absolute_path(path: &str) -> std::result::Result<(), ClientError> {
329 if !path.starts_with('/') {
330 return Err(ClientError::PathNotAbsolute(path.to_string()));
331 }
332 if Self::posix_normalize(path) != path {
333 return Err(ClientError::PathNotNormalized(path.to_string()));
334 }
335 Ok(())
336 }
337
338 pub(crate) fn assert_writable_absolute_path(
340 path: &str,
341 ) -> std::result::Result<(), ClientError> {
342 Self::assert_safe_absolute_path(path)?;
343 if path == "/proc"
344 || path.starts_with("/proc/")
345 || path == "/etc/agentos"
346 || path.starts_with("/etc/agentos/")
347 {
348 return Err(ClientError::PathReadOnly(path.to_string()));
349 }
350 Ok(())
351 }
352}
353
354impl AgentOs {
359 fn batch_error_message(err: &anyhow::Error) -> String {
365 match err.downcast_ref::<ClientError>() {
366 Some(client_error) => client_error.batch_message(),
367 None => err.to_string(),
368 }
369 }
370
371 fn fs_vm_scope(&self) -> wire::OwnershipScope {
373 wire::OwnershipScope::VmOwnership(wire::VmOwnership {
374 connection_id: self.connection_id().to_string(),
375 session_id: self.wire_session_id().to_string(),
376 vm_id: self.vm_id().to_string(),
377 })
378 }
379
380 fn posix_dirname(path: &str) -> String {
382 match path.rfind('/') {
383 None => String::from("."),
384 Some(0) => String::from("/"),
385 Some(idx) => path[..idx].to_string(),
386 }
387 }
388
389 fn join_child(dir: &str, child: &str) -> String {
392 if dir == "/" {
393 format!("/{child}")
394 } else {
395 format!("{dir}/{child}")
396 }
397 }
398
399 async fn guest_fs_call(
402 &self,
403 request: GuestFilesystemCallRequest,
404 ) -> Result<GuestFilesystemResultResponse> {
405 let scope = self.fs_vm_scope();
406 let response = self
407 .transport()
408 .request_wire(
409 scope,
410 wire::RequestPayload::GuestFilesystemCallRequest(request),
411 )
412 .await
413 .context("guest filesystem call failed")?;
414 match response {
415 wire::ResponsePayload::GuestFilesystemResultResponse(result) => Ok(result),
416 wire::ResponsePayload::RejectedResponse(wire::RejectedResponse { code, message }) => {
417 Err(ClientError::Kernel { code, message }.into())
418 }
419 other => Err(anyhow::anyhow!(
420 "unexpected response to guest filesystem call: {other:?}"
421 )),
422 }
423 }
424
425 fn fs_request(
427 operation: GuestFilesystemOperation,
428 path: impl Into<String>,
429 ) -> GuestFilesystemCallRequest {
430 GuestFilesystemCallRequest {
431 operation,
432 path: path.into(),
433 destination_path: None,
434 target: None,
435 content: None,
436 encoding: None,
437 recursive: false,
438 mode: None,
439 uid: None,
440 gid: None,
441 atime_ms: None,
442 mtime_ms: None,
443 len: None,
444 offset: None,
445 }
446 }
447
448 fn virtual_stat_from(stat: GuestFilesystemStat) -> VirtualStat {
451 VirtualStat {
452 mode: stat.mode,
453 size: stat.size,
454 blocks: stat.blocks,
455 dev: stat.dev,
456 rdev: stat.rdev,
457 is_directory: stat.is_directory,
458 is_symbolic_link: stat.is_symbolic_link,
459 atime_ms: stat.atime_ms as f64,
460 mtime_ms: stat.mtime_ms as f64,
461 ctime_ms: stat.ctime_ms as f64,
462 birthtime_ms: stat.birthtime_ms as f64,
463 ino: stat.ino,
464 nlink: stat.nlink,
465 uid: stat.uid,
466 gid: stat.gid,
467 }
468 }
469
470 async fn kernel_read_file(&self, path: &str) -> Result<Vec<u8>> {
476 let result = self
477 .guest_fs_call(Self::fs_request(GuestFilesystemOperation::ReadFile, path))
478 .await?;
479 let content = result
480 .content
481 .with_context(|| format!("sidecar returned no file content for {path}"))?;
482 match result.encoding {
483 Some(RootFilesystemEntryEncoding::Base64) => BASE64
484 .decode(content.as_bytes())
485 .context("decoding base64 file content"),
486 Some(RootFilesystemEntryEncoding::Utf8) | None => Ok(content.into_bytes()),
487 }
488 }
489
490 async fn kernel_write_file(&self, path: &str, content: &FileContent) -> Result<()> {
494 let (encoded, encoding) = match content {
495 FileContent::Text(text) => (text.clone(), None),
496 FileContent::Bytes(bytes) => (
497 BASE64.encode(bytes),
498 Some(RootFilesystemEntryEncoding::Base64),
499 ),
500 };
501 let mut request = Self::fs_request(GuestFilesystemOperation::WriteFile, path);
502 request.content = Some(encoded);
503 request.encoding = encoding;
504 self.guest_fs_call(request).await?;
505 Ok(())
506 }
507
508 async fn kernel_mkdir(&self, path: &str) -> Result<()> {
514 self.guest_fs_call(Self::fs_request(GuestFilesystemOperation::CreateDir, path))
515 .await?;
516 Ok(())
517 }
518
519 async fn kernel_exists(&self, path: &str) -> Result<bool> {
520 let result = self
521 .guest_fs_call(Self::fs_request(GuestFilesystemOperation::Exists, path))
522 .await?;
523 Ok(result.exists.unwrap_or(false))
524 }
525
526 async fn kernel_readdir(&self, path: &str) -> Result<Vec<String>> {
527 let result = self
528 .guest_fs_call(Self::fs_request(GuestFilesystemOperation::ReadDir, path))
529 .await?;
530 Ok(result
535 .entries
536 .unwrap_or_default()
537 .into_iter()
538 .map(|entry| entry.name)
539 .collect())
540 }
541
542 async fn kernel_stat(&self, path: &str) -> Result<VirtualStat> {
543 let result = self
544 .guest_fs_call(Self::fs_request(GuestFilesystemOperation::Stat, path))
545 .await?;
546 let stat = result.stat.context("stat response missing stat payload")?;
547 Ok(Self::virtual_stat_from(stat))
548 }
549
550 async fn kernel_lstat(&self, path: &str) -> Result<VirtualStat> {
551 let result = self
552 .guest_fs_call(Self::fs_request(GuestFilesystemOperation::Lstat, path))
553 .await?;
554 let stat = result.stat.context("lstat response missing stat payload")?;
555 Ok(Self::virtual_stat_from(stat))
556 }
557
558 async fn kernel_readlink(&self, path: &str) -> Result<String> {
559 let result = self
560 .guest_fs_call(Self::fs_request(GuestFilesystemOperation::ReadLink, path))
561 .await?;
562 result.target.context("readlink response missing target")
563 }
564
565 async fn kernel_symlink(&self, target: &str, path: &str) -> Result<()> {
566 let mut request = Self::fs_request(GuestFilesystemOperation::Symlink, path);
567 request.target = Some(target.to_string());
568 self.guest_fs_call(request).await?;
569 Ok(())
570 }
571
572 async fn kernel_rename(&self, from: &str, to: &str) -> Result<()> {
573 let mut request = Self::fs_request(GuestFilesystemOperation::Rename, from);
574 request.destination_path = Some(to.to_string());
575 self.guest_fs_call(request).await?;
576 Ok(())
577 }
578
579 async fn kernel_chmod(&self, path: &str, mode: u32) -> Result<()> {
580 let mut request = Self::fs_request(GuestFilesystemOperation::Chmod, path);
581 request.mode = Some(mode);
582 self.guest_fs_call(request).await?;
583 Ok(())
584 }
585
586 async fn kernel_chown(&self, path: &str, uid: u32, gid: u32) -> Result<()> {
587 let mut request = Self::fs_request(GuestFilesystemOperation::Chown, path);
588 request.uid = Some(uid);
589 request.gid = Some(gid);
590 self.guest_fs_call(request).await?;
591 Ok(())
592 }
593
594 async fn kernel_remove_file(&self, path: &str) -> Result<()> {
595 self.guest_fs_call(Self::fs_request(GuestFilesystemOperation::RemoveFile, path))
596 .await?;
597 Ok(())
598 }
599
600 async fn kernel_remove_dir(&self, path: &str) -> Result<()> {
601 self.guest_fs_call(Self::fs_request(GuestFilesystemOperation::RemoveDir, path))
602 .await?;
603 Ok(())
604 }
605
606 async fn mkdirp(&self, path: &str) -> Result<()> {
609 Self::assert_writable_absolute_path(path)?;
610 let mut current = String::new();
611 for part in path.split('/').filter(|p| !p.is_empty()) {
612 current.push('/');
613 current.push_str(part);
614 if !self.kernel_exists(¤t).await? {
615 self.kernel_mkdir(¤t).await?;
616 }
617 }
618 Ok(())
619 }
620
621 fn copy_path<'a>(
624 &'a self,
625 from: &'a str,
626 to: &'a str,
627 ) -> futures::future::BoxFuture<'a, Result<()>> {
628 Box::pin(async move {
629 Self::assert_writable_absolute_path(to)?;
630 let stat = self.kernel_lstat(from).await?;
631 if stat.is_symbolic_link {
632 let target = self.kernel_readlink(from).await?;
633 self.kernel_symlink(&target, to).await?;
634 return Ok(());
635 }
636 if stat.is_directory {
637 self.mkdirp(&Self::posix_dirname(to)).await?;
638 if !self.kernel_exists(to).await? {
639 self.kernel_mkdir(to).await?;
640 }
641 self.kernel_chmod(to, stat.mode).await?;
642 self.kernel_chown(to, stat.uid, stat.gid).await?;
643 let entries = self.kernel_readdir(from).await?;
644 for entry in entries {
645 if entry == "." || entry == ".." {
646 continue;
647 }
648 let from_path = Self::join_child(from, &entry);
649 let to_path = Self::join_child(to, &entry);
650 self.copy_path(&from_path, &to_path).await?;
651 }
652 return Ok(());
653 }
654 let content = self.kernel_read_file(from).await?;
655 self.write_file(to, content).await?;
656 self.kernel_chmod(to, stat.mode).await?;
657 self.kernel_chown(to, stat.uid, stat.gid).await?;
658 Ok(())
659 })
660 }
661
662 fn delete_inner<'a>(
664 &'a self,
665 path: &'a str,
666 recursive: bool,
667 ) -> futures::future::BoxFuture<'a, Result<()>> {
668 Box::pin(async move {
669 let stat = self.kernel_lstat(path).await?;
670 if stat.is_directory {
671 if recursive {
672 let entries = self.kernel_readdir(path).await?;
673 for entry in entries {
674 if entry == "." || entry == ".." {
675 continue;
676 }
677 let child = format!("{path}/{entry}");
678 Self::assert_safe_absolute_path(&child)?;
681 self.delete_inner(&child, true).await?;
682 }
683 }
684 return self.kernel_remove_dir(path).await;
685 }
686 self.kernel_remove_file(path).await
687 })
688 }
689}
690
691impl AgentOs {
696 pub async fn read_file(&self, path: &str) -> Result<Vec<u8>> {
698 Self::assert_safe_absolute_path(path)?;
699 self.kernel_read_file(path).await
700 }
701
702 pub async fn write_file(&self, path: &str, content: impl Into<FileContent>) -> Result<()> {
704 Self::assert_writable_absolute_path(path)?;
705 let content = content.into();
706 self.kernel_write_file(path, &content).await
707 }
708
709 pub async fn write_files(&self, entries: Vec<BatchWriteEntry>) -> Vec<BatchWriteResult> {
711 let mut results = Vec::with_capacity(entries.len());
712 for entry in entries {
713 let outcome: Result<()> = async {
714 Self::assert_writable_absolute_path(&entry.path)?;
715 if let Some(idx) = entry.path.rfind('/') {
718 let parent = &entry.path[..idx];
719 if !parent.is_empty() {
720 self.mkdirp(parent).await?;
721 }
722 }
723 self.kernel_write_file(&entry.path, &entry.content).await?;
724 Ok(())
725 }
726 .await;
727 match outcome {
728 Ok(()) => results.push(BatchWriteResult {
729 path: entry.path,
730 success: true,
731 error: None,
732 }),
733 Err(err) => results.push(BatchWriteResult {
734 path: entry.path,
735 success: false,
736 error: Some(Self::batch_error_message(&err)),
737 }),
738 }
739 }
740 results
741 }
742
743 pub async fn read_files(&self, paths: Vec<String>) -> Vec<BatchReadResult> {
745 let mut results = Vec::with_capacity(paths.len());
746 for path in paths {
747 let outcome: Result<Vec<u8>> = async {
748 Self::assert_safe_absolute_path(&path)?;
749 self.kernel_read_file(&path).await
750 }
751 .await;
752 match outcome {
753 Ok(content) => results.push(BatchReadResult {
754 path,
755 content: Some(content),
756 error: None,
757 }),
758 Err(err) => results.push(BatchReadResult {
759 path,
760 content: None,
761 error: Some(Self::batch_error_message(&err)),
762 }),
763 }
764 }
765 results
766 }
767
768 pub async fn mkdir(&self, path: &str, options: MkdirOptions) -> Result<()> {
771 if options.recursive {
772 return self.mkdirp(path).await;
773 }
774 Self::assert_writable_absolute_path(path)?;
775 self.kernel_mkdir(path).await
776 }
777
778 pub async fn readdir(&self, path: &str) -> Result<Vec<String>> {
780 Self::assert_safe_absolute_path(path)?;
781 self.kernel_readdir(path).await
782 }
783
784 pub(crate) async fn acp_read_dir_with_types(&self, path: &str) -> Result<Vec<VirtualDirEntry>> {
788 Self::assert_safe_absolute_path(path)?;
789 let names = self.kernel_readdir(path).await?;
790 let mut entries = Vec::with_capacity(names.len());
791 for name in names {
792 if name == "." || name == ".." {
793 continue;
794 }
795 let full_path = Self::join_child(path, &name);
796 let stat = self.kernel_lstat(&full_path).await?;
797 entries.push(VirtualDirEntry {
798 name,
799 is_directory: stat.is_directory,
800 is_symbolic_link: stat.is_symbolic_link,
801 });
802 }
803 Ok(entries)
804 }
805
806 pub async fn read_dir_with_types(&self, path: &str) -> Result<Vec<VirtualDirEntry>> {
811 self.acp_read_dir_with_types(path).await
812 }
813
814 pub async fn readdir_recursive(
816 &self,
817 path: &str,
818 options: ReaddirRecursiveOptions,
819 ) -> Result<Vec<DirEntry>> {
820 Self::assert_safe_absolute_path(path)?;
821 let max_depth = options.max_depth;
822 let exclude: std::collections::HashSet<&str> =
823 options.exclude.iter().map(String::as_str).collect();
824 let mut results: Vec<DirEntry> = Vec::new();
825
826 let mut queue: std::collections::VecDeque<(String, u32)> =
828 std::collections::VecDeque::new();
829 queue.push_back((path.to_string(), 0));
830
831 while let Some((dir_path, depth)) = queue.pop_front() {
832 let entries = self.kernel_readdir(&dir_path).await?;
833 for name in entries {
834 if name == "." || name == ".." {
835 continue;
836 }
837 if exclude.contains(name.as_str()) {
838 continue;
839 }
840 let full_path = Self::join_child(&dir_path, &name);
841 let s = self.kernel_lstat(&full_path).await?;
842 if s.is_symbolic_link {
843 results.push(DirEntry {
844 path: full_path,
845 entry_type: DirEntryType::Symlink,
846 size: s.size,
847 });
848 } else if s.is_directory {
849 results.push(DirEntry {
850 path: full_path.clone(),
851 entry_type: DirEntryType::Directory,
852 size: s.size,
853 });
854 if max_depth.is_none() || depth < max_depth.unwrap() {
855 queue.push_back((full_path, depth + 1));
856 }
857 } else {
858 results.push(DirEntry {
859 path: full_path,
860 entry_type: DirEntryType::File,
861 size: s.size,
862 });
863 }
864 }
865 }
866
867 Ok(results)
868 }
869
870 pub async fn stat(&self, path: &str) -> Result<VirtualStat> {
872 Self::assert_safe_absolute_path(path)?;
873 self.kernel_stat(path).await
874 }
875
876 pub async fn exists(&self, path: &str) -> Result<bool> {
878 Self::assert_safe_absolute_path(path)?;
879 self.kernel_exists(path).await
880 }
881
882 pub async fn snapshot_root_filesystem(&self) -> Result<RootSnapshotExport> {
884 let scope = self.fs_vm_scope();
885 let response = self
886 .transport()
887 .request_wire(scope, wire::RequestPayload::SnapshotRootFilesystemRequest)
888 .await
889 .context("snapshot root filesystem failed")?;
890 let snapshot = match response {
891 wire::ResponsePayload::RootFilesystemSnapshotResponse(snapshot) => snapshot,
892 wire::ResponsePayload::RejectedResponse(wire::RejectedResponse { code, message }) => {
893 return Err(ClientError::Kernel { code, message }.into());
894 }
895 other => {
896 return Err(anyhow::anyhow!(
897 "unexpected response to snapshot root filesystem: {other:?}"
898 ));
899 }
900 };
901
902 let entries = snapshot
903 .entries
904 .into_iter()
905 .map(Self::snapshot_entry_from)
906 .collect::<Result<Vec<_>>>()?;
907
908 Ok(RootSnapshotExport {
909 kind: SnapshotExportKind::SnapshotExport,
910 source: FilesystemSnapshotExport {
911 format: String::from("agentos-filesystem-snapshot-v1"),
912 filesystem: FilesystemSnapshotEntries { entries },
913 },
914 })
915 }
916
917 pub fn mount_fs(
922 &self,
923 path: &str,
924 driver: Arc<dyn VirtualFileSystem>,
925 options: MountFsOptions,
926 ) -> std::result::Result<(), ClientError> {
927 Self::assert_safe_absolute_path(path)?;
928 let _ = self.inner().in_process_mounts.insert(
929 path.to_string(),
930 MountedFs {
931 driver,
932 read_only: options.read_only,
933 },
934 );
935 Ok(())
936 }
937
938 pub fn unmount_fs(&self, path: &str) -> std::result::Result<(), ClientError> {
940 Self::assert_safe_absolute_path(path)?;
941 self.inner().in_process_mounts.remove(path);
942 Ok(())
943 }
944
945 pub async fn move_path(&self, from: &str, to: &str) -> Result<()> {
948 Self::assert_writable_absolute_path(from)?;
949 Self::assert_writable_absolute_path(to)?;
950 let source_stat = self.kernel_lstat(from).await?;
951 if !source_stat.is_directory || source_stat.is_symbolic_link {
952 return self.kernel_rename(from, to).await;
953 }
954 self.copy_path(from, to).await?;
955 self.delete(from, DeleteOptions { recursive: true }).await
956 }
957
958 pub async fn delete(&self, path: &str, options: DeleteOptions) -> Result<()> {
961 Self::assert_writable_absolute_path(path)?;
962 self.delete_inner(path, options.recursive).await
963 }
964
965 fn snapshot_entry_from(entry: RootFilesystemEntry) -> Result<FilesystemEntry> {
975 let entry_type = match entry.kind {
976 RootFilesystemEntryKind::File => DirEntryType::File,
977 RootFilesystemEntryKind::Directory => DirEntryType::Directory,
978 RootFilesystemEntryKind::Symlink => DirEntryType::Symlink,
979 };
980 let fallback_mode = match entry.kind {
983 RootFilesystemEntryKind::Directory => 0o755,
984 RootFilesystemEntryKind::Symlink => 0o777,
985 RootFilesystemEntryKind::File => 0o644,
986 };
987 let mode = format!("0{:o}", entry.mode.unwrap_or(fallback_mode) & 0o7777);
988 let uid = entry.uid.unwrap_or(0);
989 let gid = entry.gid.unwrap_or(0);
990
991 match entry.kind {
992 RootFilesystemEntryKind::File => {
993 let encoding = match entry.encoding {
994 Some(RootFilesystemEntryEncoding::Utf8) | None => FilesystemEntryEncoding::Utf8,
995 Some(RootFilesystemEntryEncoding::Base64) => FilesystemEntryEncoding::Base64,
996 };
997 Ok(FilesystemEntry {
998 path: entry.path,
999 entry_type,
1000 mode,
1001 uid,
1002 gid,
1003 content: Some(entry.content.unwrap_or_default()),
1004 encoding: Some(encoding),
1005 target: None,
1006 })
1007 }
1008 RootFilesystemEntryKind::Symlink => {
1009 let target = entry.target.with_context(|| {
1010 format!(
1011 "sidecar root snapshot for {} is missing a symlink target",
1012 entry.path
1013 )
1014 })?;
1015 Ok(FilesystemEntry {
1016 path: entry.path,
1017 entry_type,
1018 mode,
1019 uid,
1020 gid,
1021 content: None,
1022 encoding: None,
1023 target: Some(target),
1024 })
1025 }
1026 RootFilesystemEntryKind::Directory => Ok(FilesystemEntry {
1027 path: entry.path,
1028 entry_type,
1029 mode,
1030 uid,
1031 gid,
1032 content: None,
1033 encoding: None,
1034 target: None,
1035 }),
1036 }
1037 }
1038}