33//! These operations do not embed model-visible tool names, schemas, or result
44//! formatting. Every effect requires a valid execution token.
55
6+ use std:: collections:: HashMap ;
7+ use std:: sync:: { Arc , Mutex } ;
8+
69use rustscript_vm:: {
710 ConfinedFileType , ConfinedFsError , ConfinedFsErrorKind , ConfinedFsLimits , ConfinedFsRoot ,
8- ConfinedMetadata , EnumerationBudget , MAX_ENUM_ENTRIES ,
11+ ConfinedMetadata , EnumerationBudget , MAX_COMPONENT_BYTES , MAX_ENUM_ENTRIES , MAX_READ_BYTES ,
12+ MAX_WRITE_BYTES ,
913} ;
1014
1115use super :: hash:: content_hash;
@@ -76,10 +80,14 @@ pub struct FilesystemCapability {
7680 lifecycle : CapabilityLifecycle ,
7781 owner : CapabilityOwner ,
7882 limits : FilesystemLimits ,
83+ root : Arc < ConfinedFsRoot > ,
84+ cas_locks : Arc < Mutex < HashMap < String , Arc < Mutex < ( ) > > > > > ,
7985}
8086
8187impl FilesystemCapability {
8288 /// Constructs a filesystem capability. Limits must be positive.
89+ ///
90+ /// Opens and validates the confined workspace root once at admission.
8391 pub fn new (
8492 lifecycle : CapabilityLifecycle ,
8593 owner : CapabilityOwner ,
@@ -92,18 +100,30 @@ impl FilesystemCapability {
92100 "filesystem limits must be positive" ,
93101 ) ) ;
94102 }
103+ let root = ConfinedFsRoot :: with_limits (
104+ lifecycle. workspace ( ) ,
105+ ConfinedFsLimits {
106+ max_read_bytes : MAX_READ_BYTES ,
107+ max_write_bytes : limits. max_write_bytes . min ( MAX_WRITE_BYTES ) ,
108+ max_entries : MAX_ENUM_ENTRIES ,
109+ max_entry_name_bytes : MAX_COMPONENT_BYTES ,
110+ max_temp_attempts : 32 ,
111+ } ,
112+ )
113+ . map_err ( map_fs_error) ?;
95114 Ok ( Self {
96115 lifecycle,
97116 owner,
98117 limits,
118+ root : Arc :: new ( root) ,
119+ cas_locks : Arc :: new ( Mutex :: new ( HashMap :: new ( ) ) ) ,
99120 } )
100121 }
101122
102123 /// Stats a workspace-relative path without following a leaf symlink.
103124 pub fn metadata ( & self , token : & str , path : & str ) -> Result < FsMetadata , CapabilityError > {
104- let claims = self . authorize ( token, CapabilityRisk :: Read ) ?;
105- let root = open_root ( & claims) ?;
106- let meta = deny_symlink ( root. metadata ( path) . map_err ( map_fs_error) ?) ?;
125+ let _claims = self . authorize ( token, CapabilityRisk :: Read ) ?;
126+ let meta = deny_symlink ( self . root . metadata ( path) . map_err ( map_fs_error) ?) ?;
107127 Ok ( FsMetadata {
108128 file_type : file_type_name ( meta. file_type ( ) ) ,
109129 len : meta. len ( ) ,
@@ -118,38 +138,53 @@ impl FilesystemCapability {
118138 offset : u64 ,
119139 limit : usize ,
120140 ) -> Result < FsRead , CapabilityError > {
121- let claims = self . authorize ( token, CapabilityRisk :: Read ) ?;
141+ let _claims = self . authorize ( token, CapabilityRisk :: Read ) ?;
122142 if limit > self . limits . max_read_bytes {
123143 return Err ( CapabilityError :: new (
124144 "budget_exceeded" ,
125145 "requested read exceeds the configured bound" ,
126146 ) ) ;
127147 }
128- let root = open_root ( & claims) ?;
129- let meta = deny_symlink ( root. metadata ( path) . map_err ( map_fs_error) ?) ?;
148+ let meta = deny_symlink ( self . root . metadata ( path) . map_err ( map_fs_error) ?) ?;
130149 if meta. file_type ( ) != ConfinedFileType :: File {
131150 return Err ( CapabilityError :: new (
132151 "wrong_type" ,
133152 "path is not a regular file" ,
134153 ) ) ;
135154 }
136- let contents = root. read_file ( path) . map_err ( map_fs_error) ?;
137- let hash = Some ( content_hash ( & contents) ) ;
138- let start = usize:: try_from ( offset) . unwrap_or ( usize:: MAX ) ;
139- if start >= contents. len ( ) {
155+ let file_len = meta. len ( ) ;
156+ let start = offset. min ( file_len) ;
157+ let want = u64:: try_from ( limit) . unwrap_or ( u64:: MAX ) ;
158+ let end = start. saturating_add ( want) . min ( file_len) ;
159+ let window_len = usize:: try_from ( end. saturating_sub ( start) ) . unwrap_or ( 0 ) ;
160+ if window_len == 0 {
140161 return Ok ( FsRead {
141162 bytes : Vec :: new ( ) ,
142163 offset,
143164 truncated : false ,
144- hash,
165+ hash : Some ( bounded_identity ( offset, 0 , file_len) ) ,
166+ } ) ;
167+ }
168+ let mut file = self . root . open_read ( path) . map_err ( map_fs_error) ?;
169+ let contents = file. read_to_end ( ) . map_err ( map_fs_error) ?;
170+ let read_len = u64:: try_from ( contents. len ( ) ) . unwrap_or ( u64:: MAX ) ;
171+ let start_idx = usize:: try_from ( start) . unwrap_or ( usize:: MAX ) ;
172+ if start_idx >= contents. len ( ) {
173+ return Ok ( FsRead {
174+ bytes : Vec :: new ( ) ,
175+ offset,
176+ truncated : file_len > read_len,
177+ hash : Some ( identity_for ( & contents, start, 0 , file_len) ) ,
145178 } ) ;
146179 }
147- let end = start. saturating_add ( limit) . min ( contents. len ( ) ) ;
180+ let end_idx = start_idx. saturating_add ( window_len) . min ( contents. len ( ) ) ;
181+ let bytes = contents[ start_idx..end_idx] . to_vec ( ) ;
182+ let truncated = start. saturating_add ( bytes. len ( ) as u64 ) < file_len;
148183 Ok ( FsRead {
149- bytes : contents[ start..end] . to_vec ( ) ,
184+ hash : Some ( identity_for ( & contents, start, bytes. len ( ) , file_len) ) ,
185+ bytes,
150186 offset,
151- truncated : end < contents. len ( ) ,
152- hash,
187+ truncated,
153188 } )
154189 }
155190
@@ -161,16 +196,15 @@ impl FilesystemCapability {
161196 cursor : u64 ,
162197 limit : usize ,
163198 ) -> Result < FsList , CapabilityError > {
164- let claims = self . authorize ( token, CapabilityRisk :: Read ) ?;
199+ let _claims = self . authorize ( token, CapabilityRisk :: Read ) ?;
165200 if limit > self . limits . max_list_entries {
166201 return Err ( CapabilityError :: new (
167202 "budget_exceeded" ,
168203 "requested listing exceeds the configured bound" ,
169204 ) ) ;
170205 }
171- let root = open_root ( & claims) ?;
172206 if !path. is_empty ( ) {
173- let meta = deny_symlink ( root. metadata ( path) . map_err ( map_fs_error) ?) ?;
207+ let meta = deny_symlink ( self . root . metadata ( path) . map_err ( map_fs_error) ?) ?;
174208 if meta. file_type ( ) != ConfinedFileType :: Directory {
175209 return Err ( CapabilityError :: new (
176210 "wrong_type" ,
@@ -179,10 +213,11 @@ impl FilesystemCapability {
179213 }
180214 }
181215 let budget = EnumerationBudget {
182- max_entries : MAX_ENUM_ENTRIES ,
183- max_name_bytes : 255 ,
216+ max_entries : listing_entry_budget ( cursor , limit , self . limits . max_list_entries ) ,
217+ max_name_bytes : MAX_COMPONENT_BYTES ,
184218 } ;
185- let mut entries = root
219+ let mut entries = self
220+ . root
186221 . enumerate_with_budget ( path, budget)
187222 . map_err ( map_fs_error) ?;
188223 entries. retain ( |entry| entry. name ( ) != "." && entry. name ( ) != ".." ) ;
@@ -218,15 +253,29 @@ impl FilesystemCapability {
218253 expected_hash : & str ,
219254 bytes : & [ u8 ] ,
220255 ) -> Result < FsWrite , CapabilityError > {
221- let claims = self . authorize ( token, CapabilityRisk :: Write ) ?;
256+ let _claims = self . authorize ( token, CapabilityRisk :: Write ) ?;
222257 if bytes. len ( ) > self . limits . max_write_bytes {
223258 return Err ( CapabilityError :: new (
224259 "budget_exceeded" ,
225260 "requested write exceeds the configured bound" ,
226261 ) ) ;
227262 }
228- let root = open_root ( & claims) ?;
229- match root. metadata ( path) {
263+ let lock = self . lock_for ( path) ;
264+ let _guard = lock. lock ( ) . unwrap_or_else ( |poisoned| poisoned. into_inner ( ) ) ;
265+ self . validate_expected_hash ( path, expected_hash) ?;
266+ self . root . write_file ( path, bytes) . map_err ( map_fs_error) ?;
267+ Ok ( FsWrite {
268+ hash : content_hash ( bytes) ,
269+ len : bytes. len ( ) ,
270+ } )
271+ }
272+
273+ fn validate_expected_hash (
274+ & self ,
275+ path : & str ,
276+ expected_hash : & str ,
277+ ) -> Result < ( ) , CapabilityError > {
278+ match self . root . metadata ( path) {
230279 Ok ( meta) => {
231280 if meta. file_type ( ) == ConfinedFileType :: Symlink {
232281 return Err ( CapabilityError :: new (
@@ -246,7 +295,7 @@ impl FilesystemCapability {
246295 "destination is not a regular file" ,
247296 ) ) ;
248297 }
249- let current = root. read_file ( path) . map_err ( map_fs_error) ?;
298+ let current = self . root . read_file ( path) . map_err ( map_fs_error) ?;
250299 if content_hash ( & current) != expected_hash {
251300 return Err ( CapabilityError :: new (
252301 "cas_mismatch" ,
@@ -264,11 +313,18 @@ impl FilesystemCapability {
264313 }
265314 Err ( error) => return Err ( map_fs_error ( error) ) ,
266315 }
267- root. write_file ( path, bytes) . map_err ( map_fs_error) ?;
268- Ok ( FsWrite {
269- hash : content_hash ( bytes) ,
270- len : bytes. len ( ) ,
271- } )
316+ Ok ( ( ) )
317+ }
318+
319+ fn lock_for ( & self , path : & str ) -> Arc < Mutex < ( ) > > {
320+ let mut locks = self
321+ . cas_locks
322+ . lock ( )
323+ . unwrap_or_else ( |poisoned| poisoned. into_inner ( ) ) ;
324+ locks
325+ . entry ( path. to_string ( ) )
326+ . or_insert_with ( || Arc :: new ( Mutex :: new ( ( ) ) ) )
327+ . clone ( )
272328 }
273329
274330 fn authorize ( & self , token : & str , risk : CapabilityRisk ) -> Result < TokenClaims , CapabilityError > {
@@ -278,9 +334,28 @@ impl FilesystemCapability {
278334 }
279335}
280336
281- fn open_root ( claims : & TokenClaims ) -> Result < ConfinedFsRoot , CapabilityError > {
282- ConfinedFsRoot :: with_limits ( & claims. workspace , ConfinedFsLimits :: default ( ) )
283- . map_err ( map_fs_error)
337+ fn listing_entry_budget ( cursor : u64 , limit : usize , max_list_entries : usize ) -> usize {
338+ let page = limit. min ( max_list_entries) ;
339+ let start = usize:: try_from ( cursor) . unwrap_or ( usize:: MAX ) ;
340+ let observe = start
341+ . saturating_add ( page)
342+ . saturating_add ( 1 )
343+ . saturating_add ( 2 ) ;
344+ let cap = max_list_entries. saturating_add ( 3 ) ;
345+ observe. min ( cap)
346+ }
347+
348+ fn identity_for ( contents : & [ u8 ] , offset : u64 , window_len : usize , file_len : u64 ) -> String {
349+ let read_len = u64:: try_from ( contents. len ( ) ) . unwrap_or ( u64:: MAX ) ;
350+ if read_len == file_len {
351+ content_hash ( contents)
352+ } else {
353+ bounded_identity ( offset, window_len, file_len)
354+ }
355+ }
356+
357+ fn bounded_identity ( offset : u64 , window_len : usize , file_len : u64 ) -> String {
358+ format ! ( "range:{offset}:{window_len}:{file_len}" )
284359}
285360
286361fn deny_symlink ( meta : ConfinedMetadata ) -> Result < ConfinedMetadata , CapabilityError > {
0 commit comments