Object Store File Iteration#
Mount file iteration in agenta uses object-store listing APIs β not filesystem walk calls β because mount contents live directly in S3-compatible object storage (SeaweedFS in dev, S3/R2/MinIO in production). The FUSE mount (geesefs) gives agents a filesystem view, but all server-side listing, counting, archiving, and browsing goes directly against the object store via three primitives in ObjectStore and orchestration logic in MountsService.
Three Listing Primitives#
All three methods are on ObjectStore in api/oss/src/core/store/storage.py :
| Method | Behavior | When to use |
|---|---|---|
list_objects_v2 | Full recursive flat listing under a prefix. Returns all StoreObject(key, size, mtime) entries. | Archive work list, browse tree, folder deletes. |
list_objects_page | One bounded page in lexicographic order, resuming via start_after. Returns (objects, has_more). Pulls one extra element to detect more without enumerating it. | Bounded count-only scans on large mounts. |
list_objects_shallow | One directory level using S3 delimiter /. Returns immediate files + subdir prefixes separately. | Level-by-level tree descent with directory pruning. |
list_objects_page accepts start_after and max_keys (default 500) and stops after max_keys objects, making it safe against huge mounts.
File Tree Traversal#
_list_pruned_files β git-aware tree descent#
_list_pruned_files in MountsService walks the tree level by level using list_objects_shallow, running sibling directories concurrently (bounded by _LIST_CONCURRENCY = 24). This is the git-aware path :
- A frontier of prefixes is listed concurrently each level; results feed the next level's frontier.
.gitdirectories and.gitignore-matched directories are pruned at the store layer, so dependency dumps likenode_modulesare never enumerated at all..gitignorespecs are read before their level's children are pruned β each level's repo rules apply to that level's subdirectories.- A
visitedset prevents re-listing a prefix, guarding against empty-folder markers (trailing-slash objects) that the store surfaces as subdirectory entries. - The optional
capparameter implements an early exit: oncelen(kept) >= cap, traversal stops and returnstruncated=True.
Flat list_objects_v2 path#
Non-git-aware code paths call list_objects_v2 directly for a full, unfiltered recursive listing. Folder markers (keys ending in /) are filtered client-side. Used for the browse tree, archive work list, and folder deletes.
Bounded Count-Only Scanning (_COUNT_CAP)#
_COUNT_CAP = 20000 caps count-only (limit=0) requests . Two branches:
- git-aware:
_list_pruned_fileswithcap=_COUNT_CAPβ descends level by level, stops when 20 000 files are found. - raw: pages through
list_objects_pagewithmax_keys=max(cap, 200)per page, accumulating file objects (non-/keys) until the count exceeds the cap orhas_more=False.
Both paths set truncated=True and let the caller report total_capped=True, so the UI shows "N+" without ever blocking on a full enumeration. Prior to PR #5411, the raw branch walked every object with no cap and always returned truncated=False.
Archive Iteration and Zip-Slip Prevention#
build_archive_work_list resolves all mounts eagerly (before streaming starts), so errors surface as real HTTP 404/503 rather than silently truncating a 200 response. It calls list_objects_v2 and builds (zip_path, storage_key, size, mtime) tuples.
Because store keys are written by signed-credential callers, they can contain path traversal patterns. Three helpers guard archive extraction :
_zip_segments(path)β splits on both/and\(Windows extractors treat backslash as a separator)._has_unsafe_zip_segment(segments)β detects empty,., or..segments._safe_zip_segments(path)β strips unsafe segments from zip entry names.
When _has_unsafe_zip_segment fires on a store key, the entry is skipped with a warning rather than rewritten. Rewriting a/../report.txt would alias onto the real a/report.txt and overwrite it on extraction.
iter_archive_members consumes the precomputed work list, keeping _ARCHIVE_READ_CONCURRENCY = 8 object reads in flight at once (ordered prefetch via asyncio.deque). On client disconnect, in-flight tasks are cancelled to avoid orphaned reads.
Symlinks#
Object stores have no native symlink concept. The FUSE layer (geesefs) handles symlink resolution transparently for agents reading/writing via the mounted filesystem. Server-side listing treats all keys as plain objects; symlinks written through the FUSE mount appear as regular objects under their key. There is no special symlink handling in MountsService or ObjectStore.
Key Sources#
| Source | Description |
|---|---|
api/oss/src/core/store/storage.py | ObjectStore β list_objects_v2, list_objects_page, list_objects_shallow |
api/oss/src/core/mounts/service.py | MountsService β _list_pruned_files, list_files, build_archive_work_list, iter_archive_members |
| PR #5411 | Introduces bounded list_objects_page primitive, raw count cap, eager archive work list, and zip-slip helpers |