Skip to main content

codex_file_system/
lib.rs

1mod find_up;
2
3use bytes::Bytes;
4use codex_protocol::config_types::WindowsSandboxLevel;
5use codex_protocol::models::ManagedFileSystemPermissions;
6use codex_protocol::models::PermissionProfile;
7use codex_protocol::models::SandboxEnforcement;
8use codex_protocol::permissions::FileSystemAccessMode;
9use codex_protocol::permissions::FileSystemPath;
10use codex_protocol::permissions::FileSystemSandboxEntry;
11use codex_protocol::permissions::FileSystemSandboxEntryMissingPathBehavior;
12use codex_protocol::permissions::FileSystemSandboxKind;
13use codex_protocol::permissions::FileSystemSandboxPolicy;
14use codex_protocol::permissions::FileSystemSpecialPath;
15use codex_protocol::permissions::NetworkSandboxPolicy;
16use codex_protocol::protocol::SandboxPolicy;
17use codex_utils_path_uri::PathUri;
18pub use find_up::FindUpErrorPolicy;
19pub use find_up::find_nearest_ancestor_with_markers;
20pub use find_up::find_nearest_native_ancestor_with_markers;
21use futures::Stream;
22use serde::Deserialize;
23use serde::Serialize;
24use std::collections::HashSet;
25use std::collections::VecDeque;
26use std::future::Future;
27use std::io;
28use std::num::NonZeroUsize;
29use std::path::Path;
30use std::pin::Pin;
31use std::task::Context;
32use std::task::Poll;
33
34/// Maximum chunk size returned by [`ExecutorFileSystem::read_file_stream`].
35pub const FILE_READ_CHUNK_SIZE: usize = 1024 * 1024;
36const MAX_WALK_DEPTH: usize = 64;
37const MAX_WALK_DIRECTORIES: usize = 10_000;
38const MAX_WALK_ENTRIES: usize = 50_000;
39const MAX_WALK_RESPONSE_BYTES: usize = 4 * 1024 * 1024;
40const WALK_RESPONSE_ITEM_OVERHEAD_BYTES: usize = 64;
41
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub struct CreateDirectoryOptions {
44    pub recursive: bool,
45}
46
47#[derive(Clone, Copy, Debug, Eq, PartialEq)]
48pub struct RemoveOptions {
49    pub recursive: bool,
50    pub force: bool,
51}
52
53#[derive(Clone, Copy, Debug, Eq, PartialEq)]
54pub struct CopyOptions {
55    pub recursive: bool,
56}
57
58#[derive(Clone, Debug, Eq, PartialEq)]
59pub struct FileMetadata {
60    pub is_directory: bool,
61    pub is_file: bool,
62    pub is_symlink: bool,
63    /// Size in bytes.
64    pub size: u64,
65    pub created_at_ms: i64,
66    pub modified_at_ms: i64,
67}
68
69#[derive(Clone, Debug, Eq, PartialEq)]
70pub struct ReadDirectoryEntry {
71    pub file_name: String,
72    pub is_directory: bool,
73    pub is_file: bool,
74}
75
76/// Bounds for a recursive filesystem walk.
77#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
78#[serde(rename_all = "camelCase")]
79pub struct WalkOptions {
80    /// Maximum directory depth below the root that may be traversed.
81    pub max_depth: usize,
82    /// Maximum number of directories that may be traversed, including the root.
83    pub max_directories: usize,
84    /// Maximum number of directory entries that may be examined.
85    pub max_entries: usize,
86    /// Whether directory symlinks should be followed.
87    pub follow_directory_symlinks: bool,
88    /// Whether directories whose names start with `.` should be returned but not traversed.
89    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
90    pub prune_hidden_directories: bool,
91}
92
93/// Type of a filesystem entry returned by a walk.
94#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
95#[serde(rename_all = "camelCase")]
96pub enum WalkEntryKind {
97    Directory,
98    File,
99}
100
101/// One entry returned by a walk.
102#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
103#[serde(rename_all = "camelCase")]
104pub struct WalkEntry {
105    pub path: PathUri,
106    pub kind: WalkEntryKind,
107}
108
109/// A descendant that could not be inspected during a walk.
110#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
111#[serde(rename_all = "camelCase")]
112pub struct WalkError {
113    pub path: PathUri,
114    pub message: String,
115}
116
117/// Entries and recoverable errors collected by a bounded walk.
118#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
119#[serde(rename_all = "camelCase")]
120pub struct WalkOutcome {
121    pub entries: Vec<WalkEntry>,
122    pub errors: Vec<WalkError>,
123    pub truncated: bool,
124}
125
126#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
127#[serde(tag = "type", rename_all = "snake_case")]
128pub enum ExecFileSystemPath {
129    Path { path: PathUri },
130    GlobPattern { pattern: String },
131    Special { value: FileSystemSpecialPath },
132}
133
134impl From<FileSystemPath> for ExecFileSystemPath {
135    fn from(value: FileSystemPath) -> Self {
136        match value {
137            FileSystemPath::Path { path } => Self::Path {
138                path: PathUri::from_abs_path(&path),
139            },
140            FileSystemPath::GlobPattern { pattern } => Self::GlobPattern { pattern },
141            FileSystemPath::Special { value } => Self::Special { value },
142        }
143    }
144}
145
146impl TryFrom<ExecFileSystemPath> for FileSystemPath {
147    type Error = io::Error;
148
149    fn try_from(value: ExecFileSystemPath) -> Result<Self, Self::Error> {
150        Ok(match value {
151            ExecFileSystemPath::Path { path } => Self::Path {
152                path: path.to_abs_path()?,
153            },
154            ExecFileSystemPath::GlobPattern { pattern } => Self::GlobPattern { pattern },
155            ExecFileSystemPath::Special { value } => Self::Special { value },
156        })
157    }
158}
159
160#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
161pub struct ExecFileSystemSandboxEntry {
162    pub path: ExecFileSystemPath,
163    pub access: FileSystemAccessMode,
164    #[serde(default, skip_serializing_if = "Option::is_none")]
165    pub missing_path_behavior: Option<FileSystemSandboxEntryMissingPathBehavior>,
166}
167
168impl From<FileSystemSandboxEntry> for ExecFileSystemSandboxEntry {
169    fn from(value: FileSystemSandboxEntry) -> Self {
170        Self {
171            path: value.path.into(),
172            access: value.access,
173            missing_path_behavior: value.missing_path_behavior,
174        }
175    }
176}
177
178impl TryFrom<ExecFileSystemSandboxEntry> for FileSystemSandboxEntry {
179    type Error = io::Error;
180
181    fn try_from(value: ExecFileSystemSandboxEntry) -> Result<Self, Self::Error> {
182        Ok(Self {
183            path: value.path.try_into()?,
184            access: value.access,
185            missing_path_behavior: value.missing_path_behavior,
186        })
187    }
188}
189
190#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
191#[serde(tag = "type", rename_all = "snake_case")]
192pub enum ExecManagedFileSystemPermissions {
193    Restricted {
194        entries: Vec<ExecFileSystemSandboxEntry>,
195        #[serde(default, skip_serializing_if = "Option::is_none")]
196        glob_scan_max_depth: Option<NonZeroUsize>,
197    },
198    Unrestricted,
199}
200
201impl From<ManagedFileSystemPermissions> for ExecManagedFileSystemPermissions {
202    fn from(value: ManagedFileSystemPermissions) -> Self {
203        match value {
204            ManagedFileSystemPermissions::Restricted {
205                entries,
206                glob_scan_max_depth,
207            } => Self::Restricted {
208                entries: entries.into_iter().map(Into::into).collect(),
209                glob_scan_max_depth,
210            },
211            ManagedFileSystemPermissions::Unrestricted => Self::Unrestricted,
212        }
213    }
214}
215
216impl TryFrom<ExecManagedFileSystemPermissions> for ManagedFileSystemPermissions {
217    type Error = io::Error;
218
219    fn try_from(value: ExecManagedFileSystemPermissions) -> Result<Self, Self::Error> {
220        Ok(match value {
221            ExecManagedFileSystemPermissions::Restricted {
222                entries,
223                glob_scan_max_depth,
224            } => Self::Restricted {
225                entries: entries
226                    .into_iter()
227                    .map(TryInto::try_into)
228                    .collect::<io::Result<_>>()?,
229                glob_scan_max_depth,
230            },
231            ExecManagedFileSystemPermissions::Unrestricted => Self::Unrestricted,
232        })
233    }
234}
235
236#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
237#[serde(tag = "type", rename_all = "snake_case")]
238pub enum ExecPermissionProfile {
239    Managed {
240        file_system: ExecManagedFileSystemPermissions,
241        network: NetworkSandboxPolicy,
242    },
243    Disabled,
244    External {
245        network: NetworkSandboxPolicy,
246    },
247}
248
249impl From<PermissionProfile> for ExecPermissionProfile {
250    fn from(value: PermissionProfile) -> Self {
251        match value {
252            PermissionProfile::Managed {
253                file_system,
254                network,
255            } => Self::Managed {
256                file_system: file_system.into(),
257                network,
258            },
259            PermissionProfile::Disabled => Self::Disabled,
260            PermissionProfile::External { network } => Self::External { network },
261        }
262    }
263}
264
265impl TryFrom<ExecPermissionProfile> for PermissionProfile {
266    type Error = io::Error;
267
268    fn try_from(value: ExecPermissionProfile) -> Result<Self, Self::Error> {
269        Ok(match value {
270            ExecPermissionProfile::Managed {
271                file_system,
272                network,
273            } => Self::Managed {
274                file_system: file_system.try_into()?,
275                network,
276            },
277            ExecPermissionProfile::Disabled => Self::Disabled,
278            ExecPermissionProfile::External { network } => Self::External { network },
279        })
280    }
281}
282
283#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
284#[serde(rename_all = "camelCase")]
285pub struct FileSystemSandboxContext {
286    pub permissions: ExecPermissionProfile,
287    #[serde(default, skip_serializing_if = "Option::is_none")]
288    pub cwd: Option<PathUri>,
289    #[serde(default, skip_serializing_if = "Vec::is_empty")]
290    pub workspace_roots: Vec<PathUri>,
291    pub windows_sandbox_level: WindowsSandboxLevel,
292    #[serde(default)]
293    pub windows_sandbox_private_desktop: bool,
294    #[serde(default)]
295    pub use_legacy_landlock: bool,
296}
297
298impl FileSystemSandboxContext {
299    pub fn from_legacy_sandbox_policy(
300        sandbox_policy: SandboxPolicy,
301        cwd: PathUri,
302    ) -> io::Result<Self> {
303        // Legacy policy projection materializes native roots, so convert at the receiving-host
304        // boundary while retaining the URI in the resulting sandbox context.
305        let native_cwd = cwd.to_abs_path()?;
306        let file_system_sandbox_policy =
307            FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
308                &sandbox_policy,
309                &native_cwd,
310            );
311        let permissions = PermissionProfile::from_runtime_permissions_with_enforcement(
312            SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy),
313            &file_system_sandbox_policy,
314            NetworkSandboxPolicy::from(&sandbox_policy),
315        );
316        Ok(Self::from_permission_profile_with_cwd(permissions, cwd))
317    }
318
319    pub fn from_permission_profile(permissions: PermissionProfile) -> Self {
320        Self::from_permissions_and_cwd(permissions, /*cwd*/ None)
321    }
322
323    pub fn from_permission_profile_with_cwd(permissions: PermissionProfile, cwd: PathUri) -> Self {
324        Self::from_permissions_and_cwd(permissions, Some(cwd))
325    }
326
327    fn from_permissions_and_cwd(permissions: PermissionProfile, cwd: Option<PathUri>) -> Self {
328        let workspace_roots = cwd.iter().cloned().collect();
329        Self {
330            permissions: permissions.into(),
331            cwd,
332            workspace_roots,
333            windows_sandbox_level: WindowsSandboxLevel::Disabled,
334            windows_sandbox_private_desktop: false,
335            use_legacy_landlock: false,
336        }
337    }
338
339    pub fn should_run_in_sandbox(&self) -> bool {
340        let Ok(permissions) = PermissionProfile::try_from(self.permissions.clone()) else {
341            // A sandbox context for another host must not select the unsandboxed filesystem.
342            return true;
343        };
344        let file_system_policy = permissions.file_system_sandbox_policy();
345        matches!(file_system_policy.kind, FileSystemSandboxKind::Restricted)
346            && !file_system_policy.has_full_disk_write_access()
347    }
348
349    pub fn has_cwd_dependent_permissions(&self) -> bool {
350        match &self.permissions {
351            ExecPermissionProfile::Managed {
352                file_system: ExecManagedFileSystemPermissions::Restricted { entries, .. },
353                ..
354            } => entries.iter().any(|entry| match &entry.path {
355                ExecFileSystemPath::GlobPattern { pattern } => !Path::new(pattern).is_absolute(),
356                ExecFileSystemPath::Special {
357                    value: FileSystemSpecialPath::ProjectRoots { .. },
358                } => true,
359                ExecFileSystemPath::Path { .. } | ExecFileSystemPath::Special { .. } => false,
360            }),
361            ExecPermissionProfile::Managed {
362                file_system: ExecManagedFileSystemPermissions::Unrestricted,
363                ..
364            }
365            | ExecPermissionProfile::Disabled
366            | ExecPermissionProfile::External { .. } => false,
367        }
368    }
369
370    pub fn drop_cwd_if_unused(mut self) -> Self {
371        if !self.has_cwd_dependent_permissions() {
372            self.cwd = None;
373            self.workspace_roots.clear();
374        }
375        self
376    }
377}
378
379pub type FileSystemResult<T> = io::Result<T>;
380
381/// Future returned by [`ExecutorFileSystem`] operations.
382pub type ExecutorFileSystemFuture<'a, T> =
383    Pin<Box<dyn Future<Output = FileSystemResult<T>> + Send + 'a>>;
384
385/// Stream of immutable chunks read from an [`ExecutorFileSystem`].
386pub struct FileSystemReadStream {
387    inner: Pin<Box<dyn Stream<Item = FileSystemResult<Bytes>> + Send + 'static>>,
388}
389
390impl FileSystemReadStream {
391    /// Wraps a filesystem byte stream.
392    pub fn new(stream: impl Stream<Item = FileSystemResult<Bytes>> + Send + 'static) -> Self {
393        Self {
394            inner: Box::pin(stream),
395        }
396    }
397}
398
399impl Stream for FileSystemReadStream {
400    type Item = FileSystemResult<Bytes>;
401
402    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
403        self.inner.as_mut().poll_next(cx)
404    }
405}
406
407/// Abstract filesystem access used by components that may operate locally or via
408/// a remote environment.
409pub trait ExecutorFileSystem: Send + Sync {
410    /// Resolves a path within this filesystem.
411    fn canonicalize<'a>(
412        &'a self,
413        path: &'a PathUri,
414        sandbox: Option<&'a FileSystemSandboxContext>,
415    ) -> ExecutorFileSystemFuture<'a, PathUri>;
416
417    fn read_file<'a>(
418        &'a self,
419        path: &'a PathUri,
420        sandbox: Option<&'a FileSystemSandboxContext>,
421    ) -> ExecutorFileSystemFuture<'a, Vec<u8>>;
422
423    /// Reads a file as a stream of chunks no larger than [`FILE_READ_CHUNK_SIZE`].
424    fn read_file_stream<'a>(
425        &'a self,
426        path: &'a PathUri,
427        sandbox: Option<&'a FileSystemSandboxContext>,
428    ) -> ExecutorFileSystemFuture<'a, FileSystemReadStream>;
429
430    /// Reads a file and decodes it as UTF-8 text.
431    fn read_file_text<'a>(
432        &'a self,
433        path: &'a PathUri,
434        sandbox: Option<&'a FileSystemSandboxContext>,
435    ) -> ExecutorFileSystemFuture<'a, String> {
436        Box::pin(async move {
437            let bytes = self.read_file(path, sandbox).await?;
438            String::from_utf8(bytes).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
439        })
440    }
441
442    fn write_file<'a>(
443        &'a self,
444        path: &'a PathUri,
445        contents: Vec<u8>,
446        sandbox: Option<&'a FileSystemSandboxContext>,
447    ) -> ExecutorFileSystemFuture<'a, ()>;
448
449    fn create_directory<'a>(
450        &'a self,
451        path: &'a PathUri,
452        create_directory_options: CreateDirectoryOptions,
453        sandbox: Option<&'a FileSystemSandboxContext>,
454    ) -> ExecutorFileSystemFuture<'a, ()>;
455
456    fn get_metadata<'a>(
457        &'a self,
458        path: &'a PathUri,
459        sandbox: Option<&'a FileSystemSandboxContext>,
460    ) -> ExecutorFileSystemFuture<'a, FileMetadata>;
461
462    fn read_directory<'a>(
463        &'a self,
464        path: &'a PathUri,
465        sandbox: Option<&'a FileSystemSandboxContext>,
466    ) -> ExecutorFileSystemFuture<'a, Vec<ReadDirectoryEntry>>;
467
468    /// Recursively lists descendants, optionally following directory symlinks.
469    fn walk<'a>(
470        &'a self,
471        path: &'a PathUri,
472        options: WalkOptions,
473        sandbox: Option<&'a FileSystemSandboxContext>,
474    ) -> ExecutorFileSystemFuture<'a, WalkOutcome> {
475        self.walk_via_directory_reads(path, options, sandbox)
476    }
477
478    /// Performs a bounded walk using the primitive filesystem operations.
479    ///
480    /// Implementations with an optimized walk transport can use this as a compatibility fallback.
481    fn walk_via_directory_reads<'a>(
482        &'a self,
483        path: &'a PathUri,
484        options: WalkOptions,
485        sandbox: Option<&'a FileSystemSandboxContext>,
486    ) -> ExecutorFileSystemFuture<'a, WalkOutcome> {
487        Box::pin(walk_via_directory_reads(self, path, options, sandbox))
488    }
489
490    fn remove<'a>(
491        &'a self,
492        path: &'a PathUri,
493        remove_options: RemoveOptions,
494        sandbox: Option<&'a FileSystemSandboxContext>,
495    ) -> ExecutorFileSystemFuture<'a, ()>;
496
497    fn copy<'a>(
498        &'a self,
499        source_path: &'a PathUri,
500        destination_path: &'a PathUri,
501        copy_options: CopyOptions,
502        sandbox: Option<&'a FileSystemSandboxContext>,
503    ) -> ExecutorFileSystemFuture<'a, ()>;
504}
505
506async fn walk_via_directory_reads<F: ExecutorFileSystem + ?Sized>(
507    file_system: &F,
508    root: &PathUri,
509    options: WalkOptions,
510    sandbox: Option<&FileSystemSandboxContext>,
511) -> FileSystemResult<WalkOutcome> {
512    if options.max_directories == 0 || options.max_entries == 0 {
513        return Err(io::Error::new(
514            io::ErrorKind::InvalidInput,
515            "filesystem walk limits must be greater than zero",
516        ));
517    }
518    if options.max_depth > MAX_WALK_DEPTH
519        || options.max_directories > MAX_WALK_DIRECTORIES
520        || options.max_entries > MAX_WALK_ENTRIES
521    {
522        return Err(io::Error::new(
523            io::ErrorKind::InvalidInput,
524            format!(
525                "filesystem walk limits exceed maximums: depth={MAX_WALK_DEPTH}, directories={MAX_WALK_DIRECTORIES}, entries={MAX_WALK_ENTRIES}"
526            ),
527        ));
528    }
529
530    let root_metadata = file_system.get_metadata(root, sandbox).await?;
531    if !root_metadata.is_directory
532        || (root_metadata.is_symlink && !options.follow_directory_symlinks)
533    {
534        return Ok(WalkOutcome::default());
535    }
536
537    let root_identity = if options.follow_directory_symlinks {
538        file_system.canonicalize(root, sandbox).await?
539    } else {
540        root.clone()
541    };
542    let mut outcome = WalkOutcome::default();
543    let mut queue = VecDeque::from([(root.clone(), 0usize)]);
544    let mut visited_directories = HashSet::from([root_identity]);
545    let mut directory_count = 1usize;
546    let mut entry_count = 0usize;
547    let mut response_bytes = 0usize;
548
549    while let Some((directory, depth)) = queue.pop_front() {
550        let mut entries = match file_system.read_directory(&directory, sandbox).await {
551            Ok(entries) => entries,
552            Err(error) => {
553                if !push_walk_error(
554                    &mut outcome,
555                    &mut response_bytes,
556                    directory,
557                    error.to_string(),
558                ) {
559                    return Ok(outcome);
560                }
561                continue;
562            }
563        };
564        entries.sort_by(|left, right| left.file_name.cmp(&right.file_name));
565
566        for entry in entries {
567            if entry_count == options.max_entries {
568                outcome.truncated = true;
569                return Ok(outcome);
570            }
571            entry_count += 1;
572
573            let path = match directory.join(&entry.file_name) {
574                Ok(path) => path,
575                Err(error) => {
576                    if !push_walk_error(
577                        &mut outcome,
578                        &mut response_bytes,
579                        directory.clone(),
580                        error.to_string(),
581                    ) {
582                        return Ok(outcome);
583                    }
584                    continue;
585                }
586            };
587            let metadata = match file_system.get_metadata(&path, sandbox).await {
588                Ok(metadata) => metadata,
589                Err(error) => {
590                    if !push_walk_error(&mut outcome, &mut response_bytes, path, error.to_string())
591                    {
592                        return Ok(outcome);
593                    }
594                    continue;
595                }
596            };
597            if metadata.is_symlink && (!options.follow_directory_symlinks || !metadata.is_directory)
598            {
599                continue;
600            }
601
602            let kind = if metadata.is_directory {
603                WalkEntryKind::Directory
604            } else if metadata.is_file {
605                WalkEntryKind::File
606            } else {
607                continue;
608            };
609            if !reserve_walk_response_bytes(
610                &mut outcome,
611                &mut response_bytes,
612                path.to_string().len(),
613            ) {
614                return Ok(outcome);
615            }
616            outcome.entries.push(WalkEntry {
617                path: path.clone(),
618                kind,
619            });
620
621            if kind == WalkEntryKind::Directory && depth < options.max_depth {
622                if options.prune_hidden_directories && entry.file_name.starts_with('.') {
623                    continue;
624                }
625                let directory_identity = if options.follow_directory_symlinks {
626                    match file_system.canonicalize(&path, sandbox).await {
627                        Ok(path) => path,
628                        Err(error) => {
629                            if !push_walk_error(
630                                &mut outcome,
631                                &mut response_bytes,
632                                path,
633                                error.to_string(),
634                            ) {
635                                return Ok(outcome);
636                            }
637                            continue;
638                        }
639                    }
640                } else {
641                    path.clone()
642                };
643                if !visited_directories.insert(directory_identity) {
644                    continue;
645                }
646                if directory_count == options.max_directories {
647                    outcome.truncated = true;
648                } else {
649                    directory_count += 1;
650                    queue.push_back((path, depth + 1));
651                }
652            }
653        }
654    }
655
656    Ok(outcome)
657}
658
659fn push_walk_error(
660    outcome: &mut WalkOutcome,
661    response_bytes: &mut usize,
662    path: PathUri,
663    message: String,
664) -> bool {
665    let item_bytes = path.to_string().len().saturating_add(message.len());
666    if !reserve_walk_response_bytes(outcome, response_bytes, item_bytes) {
667        return false;
668    }
669    outcome.errors.push(WalkError { path, message });
670    true
671}
672
673fn reserve_walk_response_bytes(
674    outcome: &mut WalkOutcome,
675    response_bytes: &mut usize,
676    content_bytes: usize,
677) -> bool {
678    let item_bytes = content_bytes.saturating_add(WALK_RESPONSE_ITEM_OVERHEAD_BYTES);
679    let Some(total_bytes) = response_bytes.checked_add(item_bytes) else {
680        outcome.truncated = true;
681        return false;
682    };
683    if total_bytes > MAX_WALK_RESPONSE_BYTES {
684        outcome.truncated = true;
685        return false;
686    }
687    *response_bytes = total_bytes;
688    true
689}