fix: execute local reins through the sandbox owner
Some checks failed
ci / validate (push) Has been cancelled

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0726e-5232-73f2-aaca-2c05ceb62efb
This commit is contained in:
tegwick 2026-09-05 19:16:22 +02:00
parent 92392f75f1
commit 63a7f9f160
14 changed files with 463 additions and 81 deletions

View file

@ -11,7 +11,7 @@ import re
from abc import ABC, abstractmethod
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator, model_validator
CONTRACT_VERSION = "1.0"
_SEMVER = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:[-+][0-9A-Za-z.-]+)?$")
@ -114,6 +114,10 @@ class ReinDescriptor(ContractModel):
class SandboxHandle(ContractModel):
# Process-local owner binding; never serialized as reachability or evidence.
_owner_execute: Any = PrivateAttr(default=None)
_timeout_seconds: int = PrivateAttr(default=600)
sandbox_id: str
host: str
reachability: dict[str, Any] = Field(default_factory=dict)

View file

@ -12,7 +12,7 @@ from datetime import UTC, datetime
from pydantic import ValidationError
from sandboxer.core.manager import SandboxManager
from sandboxer.models import Consumer, SandboxCreateRequest
from sandboxer.models import Consumer, SandboxCreateRequest, SandboxExecRequest
from glas_harness import hub
from glas_harness.contract import (
@ -82,7 +82,7 @@ def run_execution(
error: str | None = None
try:
consumer = _consumer_from_request(request)
consumer = _consumer_from_request(request).model_copy(update={"run_id": request_id})
profile, descriptor = catalog.resolve(request.harness_profile_ref)
catalog.require_operational(profile)
selected_rein = rein or catalog.build_rein(profile, descriptor)
@ -138,6 +138,21 @@ def run_execution(
reachability=reachability,
)
def owner_execute(argv, input_text, timeout):
return manager.execute(
sandbox.sandbox_id,
SandboxExecRequest(
command=list(argv),
consumer=consumer,
credential_route_refs=profile.credential_route_refs,
stdin_text=input_text,
timeout_seconds=timeout,
),
)
sandbox._owner_execute = owner_execute
sandbox._timeout_seconds = profile.limits.timeout_seconds or 600
try:
session = selected_rein.start_session(
profile,

View file

@ -2,21 +2,20 @@
The source checkout is an input to sandbox provisioning, never an execution
workspace. Once sand-boxer returns READY, every rein subprocess crosses exactly
one declared boundary: nsenter for a same-host namespace or SSH for a remote
workspace.
one declared boundary: owner-mediated execution for a same-host namespace or
SSH for a remote workspace.
"""
from __future__ import annotations
import json
import os
import re
import shlex
import subprocess
import uuid
from dataclasses import dataclass
from pathlib import Path, PurePosixPath
from typing import Any, Literal, Sequence
from typing import Any, Callable, Literal, Sequence
from glas_harness.contract import SandboxHandle
@ -36,6 +35,8 @@ class ExecutionTransport:
workspace: str
pid: int | None = None
ssh_target: str | None = None
owner_execute: Callable | None = None
timeout_seconds: int = 600
def command(self, argv: Sequence[str]) -> list[str]:
if not argv:
@ -50,20 +51,7 @@ class ExecutionTransport:
*command,
]
if self.kind == "local_namespace":
if self.pid is None:
raise TransportError("local namespace transport is missing pid")
return [
"nsenter",
"--target",
str(self.pid),
"--mount",
"--pid",
"--net",
"--uts",
"--ipc",
"--",
*scoped_command,
]
raise TransportError("local commands require sandbox owner execution")
if self.kind == "ssh":
if not self.ssh_target or not _SSH_TARGET.fullmatch(self.ssh_target):
raise TransportError(
@ -79,12 +67,31 @@ class ExecutionTransport:
input_text: str | None = None,
timeout: float | None = None,
) -> subprocess.CompletedProcess[str]:
if not argv:
raise TransportError("cannot execute an empty command")
bounded_timeout = min(
timeout if timeout is not None else self.timeout_seconds,
self.timeout_seconds,
)
if self.kind == "local_namespace":
if self.owner_execute is None:
raise TransportError("local sandbox requires an owner execution binding")
if bounded_timeout < 1:
raise TransportError("owner execution timeout must be at least one second")
result = self.owner_execute(list(argv), input_text, int(bounded_timeout))
if result.timed_out:
raise subprocess.TimeoutExpired(list(argv), bounded_timeout)
if result.output_truncated:
raise TransportError("sandbox owner execution output was truncated")
return subprocess.CompletedProcess(
list(argv), result.exit_code, result.stdout, result.stderr
)
return subprocess.run(
self.command(argv),
input=input_text,
capture_output=True,
text=True,
timeout=timeout,
timeout=bounded_timeout,
)
def resolve_executable(self, name: str, *, timeout: float = 15.0) -> str:
@ -117,33 +124,20 @@ class ExecutionTransport:
# agent's broad `git add -A` cannot accidentally commit its prompt.
task_path = str(PurePosixPath(self.workspace) / ".git" / task_name)
content = json.dumps(payload)
if self.kind == "local_namespace":
path = Path(task_path)
try:
descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(descriptor, "w") as task_file:
task_file.write(content)
except OSError as exc:
raise TransportError(f"cannot create sandbox task file: {exc}") from exc
return task_path
script = 'umask 077; cat > "$1"'
script = 'umask 077; set -C; cat > "$1"'
proc = self.run(["sh", "-c", script, "sh", task_path], input_text=content, timeout=15)
if proc.returncode != 0:
raise TransportError(
"cannot create remote sandbox task file: "
"cannot create sandbox task file: "
+ (proc.stderr.strip() or f"exit {proc.returncode}")
)
return task_path
def remove_file(self, path: str) -> None:
if self.kind == "local_namespace":
Path(path).unlink(missing_ok=True)
return
proc = self.run(["rm", "-f", "--", path], timeout=15)
if proc.returncode != 0:
raise TransportError(
"cannot remove remote sandbox task file: "
"cannot remove sandbox task file: "
+ (proc.stderr.strip() or f"exit {proc.returncode}")
)
@ -170,8 +164,12 @@ def transport_from_sandbox(sandbox: SandboxHandle) -> ExecutionTransport:
raise TransportError("local sandbox reachability pid must be an integer") from exc
if parsed_pid <= 0 or not Path(str(workspace)).is_absolute():
raise TransportError("local sandbox reachability has invalid pid or workspace_dir")
if sandbox._owner_execute is None:
raise TransportError("local sandbox requires an owner execution binding")
return ExecutionTransport(
kind="local_namespace", workspace=str(workspace), pid=parsed_pid
kind="local_namespace", workspace=str(workspace), pid=parsed_pid,
owner_execute=sandbox._owner_execute,
timeout_seconds=sandbox._timeout_seconds,
)
if has_remote:
if not ssh_target or not remote_dir:
@ -185,6 +183,7 @@ def transport_from_sandbox(sandbox: SandboxHandle) -> ExecutionTransport:
if not PurePosixPath(str(remote_dir)).is_absolute():
raise TransportError("remote sandbox workspace must be an absolute path")
return ExecutionTransport(
kind="ssh", workspace=str(remote_dir), ssh_target=str(ssh_target)
kind="ssh", workspace=str(remote_dir), ssh_target=str(ssh_target),
timeout_seconds=sandbox._timeout_seconds
)
raise TransportError("sandbox exposes no supported execution reachability")