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.entries.unwrap_or_default())
531 }
532
533 async fn kernel_stat(&self, path: &str) -> Result<VirtualStat> {
534 let result = self
535 .guest_fs_call(Self::fs_request(GuestFilesystemOperation::Stat, path))
536 .await?;
537 let stat = result.stat.context("stat response missing stat payload")?;
538 Ok(Self::virtual_stat_from(stat))
539 }
540
541 async fn kernel_lstat(&self, path: &str) -> Result<VirtualStat> {
542 let result = self
543 .guest_fs_call(Self::fs_request(GuestFilesystemOperation::Lstat, path))
544 .await?;
545 let stat = result.stat.context("lstat response missing stat payload")?;
546 Ok(Self::virtual_stat_from(stat))
547 }
548
549 async fn kernel_readlink(&self, path: &str) -> Result<String> {
550 let result = self
551 .guest_fs_call(Self::fs_request(GuestFilesystemOperation::ReadLink, path))
552 .await?;
553 result.target.context("readlink response missing target")
554 }
555
556 async fn kernel_symlink(&self, target: &str, path: &str) -> Result<()> {
557 let mut request = Self::fs_request(GuestFilesystemOperation::Symlink, path);
558 request.target = Some(target.to_string());
559 self.guest_fs_call(request).await?;
560 Ok(())
561 }
562
563 async fn kernel_rename(&self, from: &str, to: &str) -> Result<()> {
564 let mut request = Self::fs_request(GuestFilesystemOperation::Rename, from);
565 request.destination_path = Some(to.to_string());
566 self.guest_fs_call(request).await?;
567 Ok(())
568 }
569
570 async fn kernel_chmod(&self, path: &str, mode: u32) -> Result<()> {
571 let mut request = Self::fs_request(GuestFilesystemOperation::Chmod, path);
572 request.mode = Some(mode);
573 self.guest_fs_call(request).await?;
574 Ok(())
575 }
576
577 async fn kernel_chown(&self, path: &str, uid: u32, gid: u32) -> Result<()> {
578 let mut request = Self::fs_request(GuestFilesystemOperation::Chown, path);
579 request.uid = Some(uid);
580 request.gid = Some(gid);
581 self.guest_fs_call(request).await?;
582 Ok(())
583 }
584
585 async fn kernel_remove_file(&self, path: &str) -> Result<()> {
586 self.guest_fs_call(Self::fs_request(GuestFilesystemOperation::RemoveFile, path))
587 .await?;
588 Ok(())
589 }
590
591 async fn kernel_remove_dir(&self, path: &str) -> Result<()> {
592 self.guest_fs_call(Self::fs_request(GuestFilesystemOperation::RemoveDir, path))
593 .await?;
594 Ok(())
595 }
596
597 async fn mkdirp(&self, path: &str) -> Result<()> {
600 Self::assert_writable_absolute_path(path)?;
601 let mut current = String::new();
602 for part in path.split('/').filter(|p| !p.is_empty()) {
603 current.push('/');
604 current.push_str(part);
605 if !self.kernel_exists(¤t).await? {
606 self.kernel_mkdir(¤t).await?;
607 }
608 }
609 Ok(())
610 }
611
612 fn copy_path<'a>(
615 &'a self,
616 from: &'a str,
617 to: &'a str,
618 ) -> futures::future::BoxFuture<'a, Result<()>> {
619 Box::pin(async move {
620 Self::assert_writable_absolute_path(to)?;
621 let stat = self.kernel_lstat(from).await?;
622 if stat.is_symbolic_link {
623 let target = self.kernel_readlink(from).await?;
624 self.kernel_symlink(&target, to).await?;
625 return Ok(());
626 }
627 if stat.is_directory {
628 self.mkdirp(&Self::posix_dirname(to)).await?;
629 if !self.kernel_exists(to).await? {
630 self.kernel_mkdir(to).await?;
631 }
632 self.kernel_chmod(to, stat.mode).await?;
633 self.kernel_chown(to, stat.uid, stat.gid).await?;
634 let entries = self.kernel_readdir(from).await?;
635 for entry in entries {
636 if entry == "." || entry == ".." {
637 continue;
638 }
639 let from_path = Self::join_child(from, &entry);
640 let to_path = Self::join_child(to, &entry);
641 self.copy_path(&from_path, &to_path).await?;
642 }
643 return Ok(());
644 }
645 let content = self.kernel_read_file(from).await?;
646 self.write_file(to, content).await?;
647 self.kernel_chmod(to, stat.mode).await?;
648 self.kernel_chown(to, stat.uid, stat.gid).await?;
649 Ok(())
650 })
651 }
652
653 fn delete_inner<'a>(
655 &'a self,
656 path: &'a str,
657 recursive: bool,
658 ) -> futures::future::BoxFuture<'a, Result<()>> {
659 Box::pin(async move {
660 let stat = self.kernel_lstat(path).await?;
661 if stat.is_directory {
662 if recursive {
663 let entries = self.kernel_readdir(path).await?;
664 for entry in entries {
665 if entry == "." || entry == ".." {
666 continue;
667 }
668 let child = format!("{path}/{entry}");
669 Self::assert_safe_absolute_path(&child)?;
672 self.delete_inner(&child, true).await?;
673 }
674 }
675 return self.kernel_remove_dir(path).await;
676 }
677 self.kernel_remove_file(path).await
678 })
679 }
680}
681
682impl AgentOs {
687 pub async fn read_file(&self, path: &str) -> Result<Vec<u8>> {
689 Self::assert_safe_absolute_path(path)?;
690 self.kernel_read_file(path).await
691 }
692
693 pub async fn write_file(&self, path: &str, content: impl Into<FileContent>) -> Result<()> {
695 Self::assert_writable_absolute_path(path)?;
696 let content = content.into();
697 self.kernel_write_file(path, &content).await
698 }
699
700 pub async fn write_files(&self, entries: Vec<BatchWriteEntry>) -> Vec<BatchWriteResult> {
702 let mut results = Vec::with_capacity(entries.len());
703 for entry in entries {
704 let outcome: Result<()> = async {
705 Self::assert_writable_absolute_path(&entry.path)?;
706 if let Some(idx) = entry.path.rfind('/') {
709 let parent = &entry.path[..idx];
710 if !parent.is_empty() {
711 self.mkdirp(parent).await?;
712 }
713 }
714 self.kernel_write_file(&entry.path, &entry.content).await?;
715 Ok(())
716 }
717 .await;
718 match outcome {
719 Ok(()) => results.push(BatchWriteResult {
720 path: entry.path,
721 success: true,
722 error: None,
723 }),
724 Err(err) => results.push(BatchWriteResult {
725 path: entry.path,
726 success: false,
727 error: Some(Self::batch_error_message(&err)),
728 }),
729 }
730 }
731 results
732 }
733
734 pub async fn read_files(&self, paths: Vec<String>) -> Vec<BatchReadResult> {
736 let mut results = Vec::with_capacity(paths.len());
737 for path in paths {
738 let outcome: Result<Vec<u8>> = async {
739 Self::assert_safe_absolute_path(&path)?;
740 self.kernel_read_file(&path).await
741 }
742 .await;
743 match outcome {
744 Ok(content) => results.push(BatchReadResult {
745 path,
746 content: Some(content),
747 error: None,
748 }),
749 Err(err) => results.push(BatchReadResult {
750 path,
751 content: None,
752 error: Some(Self::batch_error_message(&err)),
753 }),
754 }
755 }
756 results
757 }
758
759 pub async fn mkdir(&self, path: &str, options: MkdirOptions) -> Result<()> {
762 if options.recursive {
763 return self.mkdirp(path).await;
764 }
765 Self::assert_writable_absolute_path(path)?;
766 self.kernel_mkdir(path).await
767 }
768
769 pub async fn readdir(&self, path: &str) -> Result<Vec<String>> {
771 Self::assert_safe_absolute_path(path)?;
772 self.kernel_readdir(path).await
773 }
774
775 pub(crate) async fn acp_read_dir_with_types(&self, path: &str) -> Result<Vec<VirtualDirEntry>> {
779 Self::assert_safe_absolute_path(path)?;
780 let names = self.kernel_readdir(path).await?;
781 let mut entries = Vec::with_capacity(names.len());
782 for name in names {
783 if name == "." || name == ".." {
784 continue;
785 }
786 let full_path = Self::join_child(path, &name);
787 let stat = self.kernel_lstat(&full_path).await?;
788 entries.push(VirtualDirEntry {
789 name,
790 is_directory: stat.is_directory,
791 is_symbolic_link: stat.is_symbolic_link,
792 });
793 }
794 Ok(entries)
795 }
796
797 pub async fn readdir_recursive(
799 &self,
800 path: &str,
801 options: ReaddirRecursiveOptions,
802 ) -> Result<Vec<DirEntry>> {
803 Self::assert_safe_absolute_path(path)?;
804 let max_depth = options.max_depth;
805 let exclude: std::collections::HashSet<&str> =
806 options.exclude.iter().map(String::as_str).collect();
807 let mut results: Vec<DirEntry> = Vec::new();
808
809 let mut queue: std::collections::VecDeque<(String, u32)> =
811 std::collections::VecDeque::new();
812 queue.push_back((path.to_string(), 0));
813
814 while let Some((dir_path, depth)) = queue.pop_front() {
815 let entries = self.kernel_readdir(&dir_path).await?;
816 for name in entries {
817 if name == "." || name == ".." {
818 continue;
819 }
820 if exclude.contains(name.as_str()) {
821 continue;
822 }
823 let full_path = Self::join_child(&dir_path, &name);
824 let s = self.kernel_lstat(&full_path).await?;
825 if s.is_symbolic_link {
826 results.push(DirEntry {
827 path: full_path,
828 entry_type: DirEntryType::Symlink,
829 size: s.size,
830 });
831 } else if s.is_directory {
832 results.push(DirEntry {
833 path: full_path.clone(),
834 entry_type: DirEntryType::Directory,
835 size: s.size,
836 });
837 if max_depth.is_none() || depth < max_depth.unwrap() {
838 queue.push_back((full_path, depth + 1));
839 }
840 } else {
841 results.push(DirEntry {
842 path: full_path,
843 entry_type: DirEntryType::File,
844 size: s.size,
845 });
846 }
847 }
848 }
849
850 Ok(results)
851 }
852
853 pub async fn stat(&self, path: &str) -> Result<VirtualStat> {
855 Self::assert_safe_absolute_path(path)?;
856 self.kernel_stat(path).await
857 }
858
859 pub async fn exists(&self, path: &str) -> Result<bool> {
861 Self::assert_safe_absolute_path(path)?;
862 self.kernel_exists(path).await
863 }
864
865 pub async fn snapshot_root_filesystem(&self) -> Result<RootSnapshotExport> {
867 let scope = self.fs_vm_scope();
868 let response = self
869 .transport()
870 .request_wire(scope, wire::RequestPayload::SnapshotRootFilesystemRequest)
871 .await
872 .context("snapshot root filesystem failed")?;
873 let snapshot = match response {
874 wire::ResponsePayload::RootFilesystemSnapshotResponse(snapshot) => snapshot,
875 wire::ResponsePayload::RejectedResponse(wire::RejectedResponse { code, message }) => {
876 return Err(ClientError::Kernel { code, message }.into());
877 }
878 other => {
879 return Err(anyhow::anyhow!(
880 "unexpected response to snapshot root filesystem: {other:?}"
881 ));
882 }
883 };
884
885 let entries = snapshot
886 .entries
887 .into_iter()
888 .map(Self::snapshot_entry_from)
889 .collect::<Result<Vec<_>>>()?;
890
891 Ok(RootSnapshotExport {
892 kind: SnapshotExportKind::SnapshotExport,
893 source: FilesystemSnapshotExport {
894 format: String::from("agentos-filesystem-snapshot-v1"),
895 filesystem: FilesystemSnapshotEntries { entries },
896 },
897 })
898 }
899
900 pub fn mount_fs(
905 &self,
906 path: &str,
907 driver: Arc<dyn VirtualFileSystem>,
908 options: MountFsOptions,
909 ) -> std::result::Result<(), ClientError> {
910 Self::assert_safe_absolute_path(path)?;
911 let _ = self.inner().in_process_mounts.insert(
912 path.to_string(),
913 MountedFs {
914 driver,
915 read_only: options.read_only,
916 },
917 );
918 Ok(())
919 }
920
921 pub fn unmount_fs(&self, path: &str) -> std::result::Result<(), ClientError> {
923 Self::assert_safe_absolute_path(path)?;
924 self.inner().in_process_mounts.remove(path);
925 Ok(())
926 }
927
928 pub async fn move_path(&self, from: &str, to: &str) -> Result<()> {
931 Self::assert_writable_absolute_path(from)?;
932 Self::assert_writable_absolute_path(to)?;
933 let source_stat = self.kernel_lstat(from).await?;
934 if !source_stat.is_directory || source_stat.is_symbolic_link {
935 return self.kernel_rename(from, to).await;
936 }
937 self.copy_path(from, to).await?;
938 self.delete(from, DeleteOptions { recursive: true }).await
939 }
940
941 pub async fn delete(&self, path: &str, options: DeleteOptions) -> Result<()> {
944 Self::assert_writable_absolute_path(path)?;
945 self.delete_inner(path, options.recursive).await
946 }
947
948 fn snapshot_entry_from(entry: RootFilesystemEntry) -> Result<FilesystemEntry> {
958 let entry_type = match entry.kind {
959 RootFilesystemEntryKind::File => DirEntryType::File,
960 RootFilesystemEntryKind::Directory => DirEntryType::Directory,
961 RootFilesystemEntryKind::Symlink => DirEntryType::Symlink,
962 };
963 let fallback_mode = match entry.kind {
966 RootFilesystemEntryKind::Directory => 0o755,
967 RootFilesystemEntryKind::Symlink => 0o777,
968 RootFilesystemEntryKind::File => 0o644,
969 };
970 let mode = format!("0{:o}", entry.mode.unwrap_or(fallback_mode) & 0o7777);
971 let uid = entry.uid.unwrap_or(0);
972 let gid = entry.gid.unwrap_or(0);
973
974 match entry.kind {
975 RootFilesystemEntryKind::File => {
976 let encoding = match entry.encoding {
977 Some(RootFilesystemEntryEncoding::Utf8) | None => FilesystemEntryEncoding::Utf8,
978 Some(RootFilesystemEntryEncoding::Base64) => FilesystemEntryEncoding::Base64,
979 };
980 Ok(FilesystemEntry {
981 path: entry.path,
982 entry_type,
983 mode,
984 uid,
985 gid,
986 content: Some(entry.content.unwrap_or_default()),
987 encoding: Some(encoding),
988 target: None,
989 })
990 }
991 RootFilesystemEntryKind::Symlink => {
992 let target = entry.target.with_context(|| {
993 format!(
994 "sidecar root snapshot for {} is missing a symlink target",
995 entry.path
996 )
997 })?;
998 Ok(FilesystemEntry {
999 path: entry.path,
1000 entry_type,
1001 mode,
1002 uid,
1003 gid,
1004 content: None,
1005 encoding: None,
1006 target: Some(target),
1007 })
1008 }
1009 RootFilesystemEntryKind::Directory => Ok(FilesystemEntry {
1010 path: entry.path,
1011 entry_type,
1012 mode,
1013 uid,
1014 gid,
1015 content: None,
1016 encoding: None,
1017 target: None,
1018 }),
1019 }
1020 }
1021}