Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions platform/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ class Settings(BaseSettings):
SANDBOX_AGENT_PORT: int = 9090
SANDBOX_READONLY_ROOT: bool = True
SANDBOX_MAX_CONCURRENT: int = 16
SANDBOX_RUN_AS_WORKSPACE_OWNER: bool = True
SANDBOX_WORKSPACE_BASE_DIR: str = "/tmp/silicon_agent/tasks"
SANDBOX_FALLBACK_MODE: str = "graceful"
SANDBOX_MEMORY_MIB: int = 4096 # BoxLite memory limit (MiB); Docker uses SANDBOX_MEMORY
Expand Down
28 changes: 26 additions & 2 deletions platform/app/worker/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,15 +144,26 @@
error_code="workspace_not_found",
error_message=f"Sandbox workspace path does not exist or is not a directory: {workspace}",
)
workspace_uid: int | None = None
workspace_gid: int | None = None
if settings.SANDBOX_RUN_AS_WORKSPACE_OWNER:
try:
stat_result = workspace_path.stat()
workspace_uid = stat_result.st_uid
workspace_gid = stat_result.st_gid
except OSError:
logger.warning("Failed to stat sandbox workspace owner for %s", workspace, exc_info=True)

docker_cmd = self._build_docker_run_cmd(
container_name=container_name,
image=resolved_image,
workspace=workspace,
task_id=task_id,
workspace_uid=workspace_uid,
workspace_gid=workspace_gid,
)

logger.info("Creating sandbox container: %s (image=%s)", container_name, resolved_image)

Check warning on line 166 in platform/app/worker/sandbox.py

View workflow job for this annotation

GitHub Actions / backend-test

Missing coverage

Missing coverage on lines 147-166
rc, out, err = await _run(docker_cmd, timeout=120)

if rc != 0:
Expand Down Expand Up @@ -323,11 +334,20 @@
),
streamed=True,
)
except httpx.HTTPStatusError as e:
# NOTE: do NOT read e.response.text here — on a streaming response
# the body has not been buffered and accessing .text raises
# ResponseNotRead, which would mask the original status code.
status_code = e.response.status_code
logger.error(
"Sandbox HTTP %d from %s (check container logs for root cause)",
status_code,
info.container_name,
)
return SandboxResult(

Check warning on line 347 in platform/app/worker/sandbox.py

View workflow job for this annotation

GitHub Actions / backend-test

Missing coverage

Missing coverage on lines 337-347
error=(
f"Sandbox HTTP {e.response.status_code} from "
f"{info.container_name}: {e.response.text[:500]}"
f"Sandbox HTTP {status_code} from {info.container_name} "
f"(see container logs for details)"
),
streamed=True,
)
Expand Down Expand Up @@ -390,6 +410,8 @@
image: str,
workspace: str,
task_id: str,
workspace_uid: int | None = None,
workspace_gid: int | None = None,
) -> list[str]:
"""Build the docker run command with security constraints."""
parts = [
Expand All @@ -412,6 +434,8 @@
"--mount",
f"type=bind,src={workspace},dst=/workspace",
]
if settings.SANDBOX_RUN_AS_WORKSPACE_OWNER and workspace_uid is not None and workspace_gid is not None:
parts.extend(["--user", f"{workspace_uid}:{workspace_gid}"])
capture_model_api_raw = bool(settings.SANDBOX_DUMP_MODEL_API_RESPONSE)
container_raw_log_path: str | None = None
if capture_model_api_raw:
Expand Down
44 changes: 44 additions & 0 deletions platform/tests/test_sandbox_env_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ def _extract_mounts_from_docker_cmd(tokens: list[str]) -> list[str]:
return mounts


def _extract_user_from_docker_cmd(tokens: list[str]) -> str | None:
for idx, token in enumerate(tokens):
if token != "--user" or idx + 1 >= len(tokens):
continue
return tokens[idx + 1]
return None


def test_build_docker_run_cmd_includes_skillkit_compat_env(monkeypatch, tmp_path):
from app.worker import sandbox as sandbox_mod

Expand Down Expand Up @@ -90,6 +98,42 @@ def test_build_docker_run_cmd_disables_raw_model_dump_when_config_off(monkeypatc
assert not any(mount.endswith("dst=/model_api_logs") for mount in mounts)


def test_build_docker_run_cmd_uses_workspace_owner_by_default(monkeypatch):
from app.worker import sandbox as sandbox_mod

monkeypatch.setattr(sandbox_mod.settings, "SANDBOX_RUN_AS_WORKSPACE_OWNER", True, raising=False)

backend = DockerSandboxBackend()
cmd = backend._build_docker_run_cmd(
container_name="sbx-test",
image="sandbox-image:latest",
workspace="/tmp/workspace",
task_id="task-123",
workspace_uid=1234,
workspace_gid=2345,
)

assert _extract_user_from_docker_cmd(cmd) == "1234:2345"


def test_build_docker_run_cmd_skips_workspace_owner_when_disabled(monkeypatch):
from app.worker import sandbox as sandbox_mod

monkeypatch.setattr(sandbox_mod.settings, "SANDBOX_RUN_AS_WORKSPACE_OWNER", False, raising=False)

backend = DockerSandboxBackend()
cmd = backend._build_docker_run_cmd(
container_name="sbx-test",
image="sandbox-image:latest",
workspace="/tmp/workspace",
task_id="task-123",
workspace_uid=1234,
workspace_gid=2345,
)

assert _extract_user_from_docker_cmd(cmd) is None


def test_coding_sandbox_image_provides_java_toolchain():
dockerfile_path = Path(__file__).resolve().parents[1] / "sandbox" / "Dockerfile.coding"
content = dockerfile_path.read_text(encoding="utf-8")
Expand Down
Loading