@@ -34,7 +34,7 @@ class SandboxPathGrant(BaseModel):
3434 @field_validator ("path" , mode = "before" )
3535 @classmethod
3636 def _coerce_path (cls , value : object ) -> str :
37- if isinstance (value , Path ):
37+ if isinstance (value , PurePath ):
3838 return value .as_posix ()
3939 if isinstance (value , str ):
4040 return value
@@ -56,10 +56,11 @@ class WorkspacePathPolicy:
5656 def __init__ (
5757 self ,
5858 * ,
59- root : str | Path ,
59+ root : str | PurePath ,
6060 extra_path_grants : tuple [SandboxPathGrant , ...] = (),
6161 ) -> None :
6262 self ._root = Path (root )
63+ self ._sandbox_root = self ._coerce_posix_path (root )
6364 self ._extra_path_grants = extra_path_grants
6465
6566 def absolute_workspace_path (self , path : str | Path ) -> Path :
@@ -105,11 +106,31 @@ def normalize_path(
105106 if resolve_symlinks :
106107 result , grant = self ._resolved_host_path_and_grant (original )
107108 else :
108- result , grant = self ._sandbox_path_and_grant (original )
109+ sandbox_result , grant = self ._sandbox_path_and_grant (self ._coerce_posix_path (original ))
110+ result = Path (sandbox_result .as_posix ())
109111 if for_write :
110112 self ._raise_if_read_only_grant (result , grant )
111113 return result
112114
115+ def normalize_sandbox_path (
116+ self ,
117+ path : str | PurePath ,
118+ * ,
119+ for_write : bool = False ,
120+ ) -> PurePosixPath :
121+ """Return a validated POSIX path for a Unix-like remote sandbox filesystem."""
122+
123+ original = self ._coerce_posix_path (path )
124+ result , grant = self ._sandbox_path_and_grant (original )
125+ if for_write :
126+ self ._raise_if_read_only_grant (Path (result .as_posix ()), grant )
127+ return result
128+
129+ def sandbox_root (self ) -> PurePosixPath :
130+ """Return the workspace root as a POSIX path for remote sandbox commands."""
131+
132+ return self ._normalized_root ()
133+
113134 def _resolved_host_path_and_grant (
114135 self ,
115136 original : Path ,
@@ -130,18 +151,18 @@ def _resolved_host_path_and_grant(
130151
131152 def _sandbox_path_and_grant (
132153 self ,
133- original : Path ,
134- ) -> tuple [Path , SandboxPathGrant | None ]:
154+ original : PurePath ,
155+ ) -> tuple [PurePosixPath , SandboxPathGrant | None ]:
135156 normalized = (
136157 self ._absolute_posix_path (original )
137158 if original .is_absolute ()
138159 else self ._absolute_workspace_posix_path (original )
139160 )
140161 if self ._is_under (normalized , self ._normalized_root ()):
141- return Path ( str ( normalized )) , None
162+ return normalized , None
142163 grant = self ._matching_grant (normalized )
143164 if original .is_absolute () and grant is not None :
144- return Path ( str ( normalized )) , grant
165+ return normalized , grant
145166 raise self ._invalid_path_error (original )
146167
147168 def _raise_if_read_only_grant (
@@ -159,17 +180,17 @@ def _raise_if_read_only_grant(
159180 },
160181 )
161182
162- def extra_path_grant_rules (self ) -> tuple [tuple [Path , bool ], ...]:
183+ def extra_path_grant_rules (self ) -> tuple [tuple [PurePosixPath , bool ], ...]:
163184 """Return normalized extra grant roots and access modes for remote realpath checks."""
164185
165- rules : list [tuple [Path , bool ]] = []
186+ rules : list [tuple [PurePosixPath , bool ]] = []
166187 for grant in self ._extra_path_grants :
167- root = Path (grant .path )
188+ root = self . _coerce_posix_path (grant .path )
168189 _raise_if_filesystem_root (root )
169190 rules .append ((root , grant .read_only ))
170191 return tuple (rules )
171192
172- def _absolute_workspace_posix_path (self , path : Path ) -> PurePosixPath :
193+ def _absolute_workspace_posix_path (self , path : PurePath ) -> PurePosixPath :
173194 normalized = self ._absolute_posix_path (path )
174195 root = self ._normalized_root ()
175196 try :
@@ -178,13 +199,13 @@ def _absolute_workspace_posix_path(self, path: Path) -> PurePosixPath:
178199 raise self ._invalid_path_error (path , cause = exc ) from exc
179200 return normalized
180201
181- def _absolute_posix_path (self , path : Path ) -> PurePosixPath :
202+ def _absolute_posix_path (self , path : PurePath ) -> PurePosixPath :
182203 root = self ._normalized_root ()
183204 raw_candidate = path .as_posix () if path .is_absolute () else str (root / path .as_posix ())
184205 return PurePosixPath (posixpath .normpath (str (raw_candidate )))
185206
186207 def _normalized_root (self ) -> PurePosixPath :
187- return PurePosixPath (posixpath .normpath (self ._root .as_posix ()))
208+ return PurePosixPath (posixpath .normpath (self ._sandbox_root .as_posix ()))
188209
189210 def _matching_grant (
190211 self ,
@@ -212,11 +233,17 @@ def _is_under(path: PurePath, root: PurePath) -> bool:
212233
213234 def _invalid_path_error (
214235 self ,
215- path : Path ,
236+ path : PurePath ,
216237 * ,
217238 cause : BaseException | None = None ,
218239 ) -> InvalidManifestPathError :
219240 reason : Literal ["absolute" , "escape_root" ] = (
220241 "absolute" if path .is_absolute () else "escape_root"
221242 )
222- return InvalidManifestPathError (rel = path , reason = reason , cause = cause )
243+ return InvalidManifestPathError (rel = path .as_posix (), reason = reason , cause = cause )
244+
245+ @staticmethod
246+ def _coerce_posix_path (path : str | PurePath ) -> PurePosixPath :
247+ if isinstance (path , PurePath ):
248+ path = path .as_posix ()
249+ return PurePosixPath (posixpath .normpath (path ))
0 commit comments