glas-harness/src/glas_harness/transport.py
tegwick 63a7f9f160
Some checks failed
ci / validate (push) Has been cancelled
fix: execute local reins through the sandbox owner
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0726e-5232-73f2-aaca-2c05ceb62efb
2026-09-05 19:16:22 +02:00

189 lines
7.5 KiB
Python

"""Execution transports derived from sand-boxer reachability descriptors.
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: owner-mediated execution for a same-host namespace or
SSH for a remote workspace.
"""
from __future__ import annotations
import json
import re
import shlex
import subprocess
import uuid
from dataclasses import dataclass
from pathlib import Path, PurePosixPath
from typing import Any, Callable, Literal, Sequence
from glas_harness.contract import SandboxHandle
class TransportError(RuntimeError):
"""The sandbox reachability descriptor cannot safely execute commands."""
_SSH_TARGET = re.compile(
r"^(?:[A-Za-z0-9][A-Za-z0-9._-]*@)?[A-Za-z0-9][A-Za-z0-9._-]*$"
)
@dataclass(frozen=True)
class ExecutionTransport:
kind: Literal["local_namespace", "ssh"]
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:
raise TransportError("cannot execute an empty command")
command = [str(item) for item in argv]
scoped_command = [
"sh",
"-c",
'cd "$1" && shift && exec "$@"',
"sh",
self.workspace,
*command,
]
if self.kind == "local_namespace":
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(
"SSH target must be a single non-option host or user@host target"
)
return ["ssh", "--", self.ssh_target, shlex.join(scoped_command)]
raise TransportError(f"unsupported execution transport: {self.kind}")
def run(
self,
argv: Sequence[str],
*,
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=bounded_timeout,
)
def resolve_executable(self, name: str, *, timeout: float = 15.0) -> str:
if not name or "/" in name:
raise TransportError("rein executable must be a command name, not a host path")
proc = self.run(
["sh", "-c", 'command -v -- "$1"', "sh", name], timeout=timeout
)
resolved = proc.stdout.strip()
if proc.returncode != 0 or not resolved:
raise TransportError(
f"'{name}' is not installed inside the selected sandbox transport"
)
return resolved
def git_head(self, *, timeout: float = 15.0) -> str:
proc = self.run(
["git", "-C", self.workspace, "rev-parse", "HEAD"], timeout=timeout
)
head = proc.stdout.strip()
if proc.returncode != 0 or not head:
raise TransportError(
"cannot read git HEAD inside the selected sandbox transport"
)
return head
def write_task_file(self, payload: dict[str, Any]) -> str:
task_name = f".glas-harness-task-{uuid.uuid4().hex}.json"
# Keep the task spec under Git's private metadata directory so an
# agent's broad `git add -A` cannot accidentally commit its prompt.
task_path = str(PurePosixPath(self.workspace) / ".git" / task_name)
content = json.dumps(payload)
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 sandbox task file: "
+ (proc.stderr.strip() or f"exit {proc.returncode}")
)
return task_path
def remove_file(self, path: str) -> None:
proc = self.run(["rm", "-f", "--", path], timeout=15)
if proc.returncode != 0:
raise TransportError(
"cannot remove sandbox task file: "
+ (proc.stderr.strip() or f"exit {proc.returncode}")
)
def transport_from_sandbox(sandbox: SandboxHandle) -> ExecutionTransport:
reachability = sandbox.reachability
pid = reachability.get("pid")
workspace = reachability.get("workspace_dir")
ssh_target = reachability.get("ssh")
remote_dir = reachability.get("remote_dir")
has_local = pid is not None or workspace is not None
has_remote = ssh_target is not None or remote_dir is not None
if has_local and has_remote:
raise TransportError("ambiguous sandbox reachability: local and SSH fields coexist")
if has_local:
if not pid or not workspace:
raise TransportError(
"incomplete local sandbox reachability: pid and workspace_dir are required"
)
try:
parsed_pid = int(str(pid))
except ValueError as exc:
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,
owner_execute=sandbox._owner_execute,
timeout_seconds=sandbox._timeout_seconds,
)
if has_remote:
if not ssh_target or not remote_dir:
raise TransportError(
"incomplete remote sandbox reachability: ssh and remote_dir are required"
)
if not _SSH_TARGET.fullmatch(str(ssh_target)):
raise TransportError(
"SSH reachability must be a single non-option host or user@host target"
)
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),
timeout_seconds=sandbox._timeout_seconds
)
raise TransportError("sandbox exposes no supported execution reachability")