@@ -129,6 +129,7 @@ def __init__(
129129 self .next_command_result = _FakeCommandFinished ()
130130 self .run_command_calls : list [tuple [str , list [str ], str | None ]] = []
131131 self .refresh_calls = 0
132+ self .read_file_calls : list [tuple [str , str | None ]] = []
132133 self .stop_calls = 0
133134 self .wait_for_status_calls : list [tuple [object , float | None ]] = []
134135 self .wait_for_status_error : BaseException | None = None
@@ -266,6 +267,7 @@ async def run_command(
266267 return self .next_command_result
267268
268269 async def read_file (self , path : str , * , cwd : str | None = None ) -> bytes | None :
270+ self .read_file_calls .append ((path , cwd ))
269271 resolved = path if path .startswith ("/" ) or cwd is None else f"{ cwd .rstrip ('/' )} /{ path } "
270272 return self .files .get (resolved )
271273
@@ -682,6 +684,34 @@ async def test_vercel_read_and_write_reject_paths_outside_workspace_root(
682684 await session .write ("/etc/passwd" , io .BytesIO (b"nope" ))
683685
684686
687+ @pytest .mark .asyncio
688+ async def test_vercel_read_rejects_workspace_symlink_to_ungranted_path (
689+ monkeypatch : pytest .MonkeyPatch ,
690+ ) -> None :
691+ vercel_module = _load_vercel_module (monkeypatch )
692+
693+ state = vercel_module .VercelSandboxSessionState (
694+ session_id = "00000000-0000-0000-0000-000000000016" ,
695+ manifest = Manifest (root = "/workspace" ),
696+ snapshot = NoopSnapshot (id = "snapshot" ),
697+ sandbox_id = "sandbox-read-escape-link" ,
698+ )
699+ sandbox = _FakeAsyncSandbox (sandbox_id = "sandbox-read-escape-link" )
700+ sandbox .symlinks ["/workspace/link" ] = "/private"
701+ session = vercel_module .VercelSandboxSession .from_state (state , sandbox = sandbox )
702+
703+ with pytest .raises (InvalidManifestPathError ) as exc_info :
704+ await session .read ("link/secret.txt" )
705+
706+ assert sandbox .read_file_calls == []
707+ assert str (exc_info .value ) == "manifest path must not escape root: link/secret.txt"
708+ assert exc_info .value .context == {
709+ "rel" : "link/secret.txt" ,
710+ "reason" : "escape_root" ,
711+ "resolved_path" : "workspace escape: /private/secret.txt" ,
712+ }
713+
714+
685715@pytest .mark .asyncio
686716async def test_vercel_write_rejects_workspace_symlink_to_read_only_extra_path_grant (
687717 monkeypatch : pytest .MonkeyPatch ,
0 commit comments