Add bounded stdin to owner execution
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a06def-6490-7033-8448-2eab2d12ed44
This commit is contained in:
tegwick 2026-09-04 22:30:11 +02:00
parent fd9297810c
commit b6655d8859
15 changed files with 74 additions and 17 deletions

View file

@ -3,6 +3,7 @@
from __future__ import annotations
import json
import sys
from typing import Annotated
import typer
@ -157,6 +158,9 @@ def sandbox_exec(
max_output_bytes: Annotated[
int, typer.Option(help="Per-stream captured output limit")
] = 262_144,
stdin: Annotated[
bool, typer.Option("--stdin", help="Read bounded command stdin from this process")
] = False,
) -> None:
"""Run COMMAND inside a bwrap sandbox through its owning process."""
command = list(ctx.args)
@ -164,19 +168,20 @@ def sandbox_exec(
command = command[1:]
if not command:
raise typer.BadParameter("COMMAND is required after --")
request = SandboxExecRequest(
command=command,
consumer=Consumer(
actor=ActorType(actor),
project=project,
session_id=session_id,
run_id=run_id,
),
credential_route_refs=credential_route_ref or [],
timeout_seconds=timeout,
max_output_bytes=max_output_bytes,
)
try:
request = SandboxExecRequest(
command=command,
consumer=Consumer(
actor=ActorType(actor),
project=project,
session_id=session_id,
run_id=run_id,
),
credential_route_refs=credential_route_ref or [],
timeout_seconds=timeout,
max_output_bytes=max_output_bytes,
stdin_text=sys.stdin.read(1_000_001) if stdin else None,
)
result = SandboxManager().execute(sandbox_id, request)
except (KeyError, PermissionError, RuntimeError, ValueError) as exc:
typer.echo(f"Error: {exc}", err=True)

View file

@ -231,6 +231,8 @@ class SandboxManager:
command_size = sum(len(arg.encode("utf-8")) for arg in request.command)
if command_size > 65_536:
raise ValueError("command argument vector exceeds 65536 bytes")
if request.stdin_text is not None and len(request.stdin_text.encode("utf-8")) > 1_000_000:
raise ValueError("command stdin exceeds 1000000 bytes")
invalid_refs = [
ref
for ref in request.credential_route_refs
@ -304,6 +306,7 @@ class SandboxManager:
execution_context=context,
timeout_seconds=request.timeout_seconds,
max_output_bytes=request.max_output_bytes,
stdin_text=request.stdin_text,
)
except Exception as exc:
status.state = SandboxState.READY

View file

@ -60,6 +60,7 @@ class SandboxExtension(ABC):
execution_context: dict[str, str],
timeout_seconds: int,
max_output_bytes: int,
stdin_text: str | None = None,
) -> dict[str, object]:
"""Run a bounded command through an owner-mediated sandbox boundary."""
raise NotImplementedError(f"{type(self).__name__} does not support execution")

View file

@ -184,6 +184,7 @@ class BwrapExtension(SandboxExtension):
execution_context: dict[str, str],
timeout_seconds: int,
max_output_bytes: int,
stdin_text: str | None = None,
) -> dict[str, object]:
"""Ask the broker already inside bwrap to run an argument-vector command."""
pid = int(handle.get("pid", "0"))
@ -197,6 +198,7 @@ class BwrapExtension(SandboxExtension):
"execution_context": execution_context,
"timeout_seconds": timeout_seconds,
"max_output_bytes": max_output_bytes,
"stdin_text": stdin_text,
}
response_limit = max_output_bytes * 2 + 65_536
chunks: list[bytes] = []

View file

@ -10,7 +10,9 @@ import subprocess
import sys
from pathlib import Path
_MAX_REQUEST_BYTES = 1_048_576
# A validated 1 MB stdin can expand sixfold when JSON escapes control bytes;
# leave bounded headroom for the argv, route references, and execution context.
_MAX_REQUEST_BYTES = 7_000_000
def _bounded_output(value: bytes | None, limit: int) -> tuple[str, bool]:
@ -27,6 +29,7 @@ def _run(payload: dict, workspace: Path) -> dict[str, object]:
max_output_bytes = int(payload["max_output_bytes"])
credential_refs = payload.get("credential_route_refs", [])
context = payload.get("execution_context", {})
stdin_text = payload.get("stdin_text")
child_env = {
"HOME": str(workspace),
"LANG": "C.UTF-8",
@ -39,12 +42,16 @@ def _run(payload: dict, workspace: Path) -> dict[str, object]:
command,
cwd=workspace,
env=child_env,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
start_new_session=True,
)
try:
stdout_raw, stderr_raw = process.communicate(timeout=timeout_seconds)
stdin_bytes = stdin_text.encode("utf-8") if stdin_text is not None else None
stdout_raw, stderr_raw = process.communicate(
input=stdin_bytes, timeout=timeout_seconds
)
exit_code = process.returncode
except subprocess.TimeoutExpired:
timed_out = True

View file

@ -36,6 +36,7 @@ class ExtensionBackend(Protocol):
execution_context: dict[str, str],
timeout_seconds: int,
max_output_bytes: int,
stdin_text: str | None = None,
) -> dict[str, object]: ...

View file

@ -153,6 +153,7 @@ class SandboxExecRequest(BaseModel):
command: list[str] = Field(min_length=1, max_length=256)
consumer: Consumer
credential_route_refs: list[str] = Field(default_factory=list, max_length=32)
stdin_text: str | None = Field(default=None, max_length=1_000_000)
timeout_seconds: int = Field(default=900, ge=1, le=3600)
max_output_bytes: int = Field(default=262_144, ge=1, le=1_048_576)