Skip to content

Commit b58d059

Browse files
authored
fix: Trust filesystem permissions for Vercel roots (#2910)
Instead of trying to constraint the agent to only work in the workspace, allow it to fail freely without sudo.
1 parent 3ad12bc commit b58d059

File tree

2 files changed

+45
-59
lines changed

2 files changed

+45
-59
lines changed

src/agents/extensions/sandbox/vercel/sandbox.py

Lines changed: 7 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -125,21 +125,7 @@ def _resolve_manifest_root(manifest: Manifest | None) -> Manifest:
125125

126126
if manifest.root == _DEFAULT_MANIFEST_ROOT:
127127
return manifest.model_copy(update={"root": DEFAULT_VERCEL_WORKSPACE_ROOT})
128-
129-
root = Path(manifest.root)
130-
default_root = Path(DEFAULT_VERCEL_WORKSPACE_ROOT)
131-
if not root.is_absolute() or root == default_root or default_root in root.parents:
132-
return manifest
133-
134-
raise ConfigurationError(
135-
message=(
136-
"Vercel sandboxes require manifest.root to stay within "
137-
f"{DEFAULT_VERCEL_WORKSPACE_ROOT!r}"
138-
),
139-
error_code=ErrorCode.SANDBOX_CONFIG_INVALID,
140-
op="start",
141-
context={"backend": "vercel", "manifest_root": manifest.root},
142-
)
128+
return manifest
143129

144130

145131
def _validate_network_policy(value: object) -> NetworkPolicy | None:
@@ -324,45 +310,24 @@ def _validate_tar_bytes(self, raw: bytes) -> None:
324310
except (tarfile.TarError, OSError) as exc:
325311
raise ValueError("invalid tar stream") from exc
326312

327-
async def _ensure_workspace_root(self) -> None:
328-
root = Path(self.state.manifest.root)
329-
sandbox = await self._ensure_sandbox()
313+
async def _prepare_backend_workspace(self) -> None:
314+
root = PurePosixPath(os.path.normpath(self.state.manifest.root))
330315
try:
316+
sandbox = await self._ensure_sandbox()
331317
finished = await sandbox.run_command("mkdir", ["-p", "--", root.as_posix()])
332318
except Exception as exc:
333-
raise WorkspaceStartError(path=root, cause=exc) from exc
334-
if finished.exit_code != 0:
335-
raise WorkspaceStartError(
336-
path=root,
337-
context={
338-
"exit_code": finished.exit_code,
339-
"stdout": await finished.stdout(),
340-
"stderr": await finished.stderr(),
341-
},
342-
)
343-
try:
344-
finished = await sandbox.run_command("test", ["-d", root.as_posix()])
345-
except Exception as exc:
346-
raise WorkspaceStartError(path=root, cause=exc) from exc
319+
raise WorkspaceStartError(path=Path(str(root)), cause=exc) from exc
320+
347321
if finished.exit_code != 0:
348322
raise WorkspaceStartError(
349-
path=root,
323+
path=Path(str(root)),
350324
context={
351325
"exit_code": finished.exit_code,
352326
"stdout": await finished.stdout(),
353327
"stderr": await finished.stderr(),
354328
},
355329
)
356330

357-
async def start(self) -> None:
358-
try:
359-
await self._ensure_workspace_root()
360-
except WorkspaceStartError:
361-
raise
362-
except Exception as exc:
363-
raise WorkspaceStartError(path=Path(self.state.manifest.root), cause=exc) from exc
364-
await super().start()
365-
366331
async def _ensure_sandbox(self, *, source: Any | None = None) -> Any:
367332
sandbox = self._sandbox
368333
if sandbox is not None:

tests/extensions/test_sandbox_vercel.py

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,7 @@ async def test_vercel_exec_read_write_and_port_resolution(monkeypatch: pytest.Mo
524524

525525

526526
@pytest.mark.asyncio
527-
async def test_vercel_start_creates_workspace_root_before_manifest_apply(
527+
async def test_vercel_start_uses_base_session_contract_and_materializes_workspace(
528528
monkeypatch: pytest.MonkeyPatch,
529529
) -> None:
530530
vercel_module = _load_vercel_module(monkeypatch)
@@ -541,15 +541,14 @@ async def test_vercel_start_creates_workspace_root_before_manifest_apply(
541541
await session.start()
542542
payload = await session.read(Path("notes.txt"))
543543

544-
assert sandbox.run_command_calls[:2] == [
545-
("mkdir", ["-p", "--", "/workspace"], None),
546-
("test", ["-d", "/workspace"], None),
547-
]
544+
assert sandbox.run_command_calls[0] == ("mkdir", ["-p", "--", "/workspace"], None)
545+
assert ("mkdir", ["-p", "/workspace"], "/workspace") in sandbox.run_command_calls
546+
assert session.state.workspace_root_ready is True
548547
assert payload.read() == b"payload"
549548

550549

551550
@pytest.mark.asyncio
552-
async def test_vercel_start_treats_manifest_root_as_literal_path(
551+
async def test_vercel_start_materializes_entries_under_literal_manifest_root(
553552
monkeypatch: pytest.MonkeyPatch,
554553
) -> None:
555554
vercel_module = _load_vercel_module(monkeypatch)
@@ -568,28 +567,50 @@ async def test_vercel_start_treats_manifest_root_as_literal_path(
568567
await session.start()
569568
payload = await session.read(Path("notes.txt"))
570569

571-
assert sandbox.run_command_calls[:2] == [
572-
("mkdir", ["-p", "--", "/workspace/my app"], None),
573-
("test", ["-d", "/workspace/my app"], None),
570+
assert sandbox.run_command_calls[0] == ("mkdir", ["-p", "--", "/workspace/my app"], None)
571+
assert ("mkdir", ["-p", "/workspace/my app"], "/workspace/my app") in sandbox.run_command_calls
572+
assert sandbox.write_files_calls == [
573+
[{"path": "/workspace/my app/notes.txt", "content": b"payload"}]
574574
]
575575
assert payload.read() == b"payload"
576576

577577

578578
@pytest.mark.asyncio
579-
async def test_vercel_create_rejects_manifest_root_outside_provider_workspace(
579+
async def test_vercel_start_bootstraps_arbitrary_absolute_root_before_using_it_as_cwd(
580+
monkeypatch: pytest.MonkeyPatch,
581+
) -> None:
582+
vercel_module = _load_vercel_module(monkeypatch)
583+
584+
state = vercel_module.VercelSandboxSessionState(
585+
session_id="00000000-0000-0000-0000-000000000014",
586+
manifest=Manifest(root="/tmp/outside", entries={"notes.txt": File(content=b"payload")}),
587+
snapshot=NoopSnapshot(id="snapshot"),
588+
sandbox_id="sandbox-start-outside",
589+
)
590+
sandbox = _FakeAsyncSandbox(sandbox_id="sandbox-start-outside")
591+
session = vercel_module.VercelSandboxSession.from_state(state, sandbox=sandbox)
592+
593+
await session.start()
594+
payload = await session.read(Path("notes.txt"))
595+
596+
assert sandbox.run_command_calls[0] == ("mkdir", ["-p", "--", "/tmp/outside"], None)
597+
assert ("mkdir", ["-p", "/tmp/outside"], "/tmp/outside") in sandbox.run_command_calls
598+
assert payload.read() == b"payload"
599+
600+
601+
@pytest.mark.asyncio
602+
async def test_vercel_create_allows_manifest_root_outside_provider_workspace(
580603
monkeypatch: pytest.MonkeyPatch,
581604
) -> None:
582605
vercel_module = _load_vercel_module(monkeypatch)
583606
client = vercel_module.VercelSandboxClient()
584607

585-
with pytest.raises(ConfigurationError) as exc_info:
586-
await client.create(
587-
manifest=Manifest(root="/tmp/outside"),
588-
options=vercel_module.VercelSandboxClientOptions(),
589-
)
608+
session = await client.create(
609+
manifest=Manifest(root="/tmp/outside"),
610+
options=vercel_module.VercelSandboxClientOptions(),
611+
)
590612

591-
assert exc_info.value.context["backend"] == "vercel"
592-
assert exc_info.value.context["manifest_root"] == "/tmp/outside"
613+
assert session._inner.state.manifest.root == "/tmp/outside"
593614

594615

595616
@pytest.mark.asyncio

0 commit comments

Comments
 (0)