simbiot-harness/venv/lib/python3.12/site-packages/strands/sandbox/docker.py
Simbiot Harness b60439152b Simbiot Harness v0.1: тактовая лестница на Strands + CQRS/ES-спина
- harness/es.py: append-only event store + проекции (образец — кооп-портал)
- harness/levels.py: уровни 1с/1м/1ч/1д, контекст ∝ масштабу
- harness/scheduler.py: уровневые потоки, идемпотентные тики, absorbed-стабы
- harness/isolated_runner.py + _llm_worker.py: LLM-тик в сабпроцессе
  (глобальный asyncio-рантайм Strands не переживает 2+ create_harness)
- cli.py: init/task/tasks/status/events + requeue (написан самим минутным агентом)
- 9router: отдельный ключ sk-simbiot, модель auto (glm-5.3 первым)
2026-09-29 15:28:50 +00:00

117 lines
4.8 KiB
Python

"""Docker sandbox -- executes commands in a Docker container via ``docker exec``.
Mirrors ``strands-ts/src/sandbox/docker.ts``.
"""
from collections.abc import AsyncGenerator
from typing import Any
from ..types.tools import AgentTool
from ..vended_tools.file_editor import make_file_editor
from ..vended_tools.file_editor.file_editor import DEFAULT_FILE_EDITOR_DESCRIPTION
from ..vended_tools.shell import make_shell
from ..vended_tools.shell.types import SANDBOX_SHELL_DESCRIPTION
from .posix_shell import PosixShellSandbox, validate_env_keys
from .stream_process import _stream_process
from .types import ExecutionResult, StreamChunk
class DockerSandbox(PosixShellSandbox):
"""Execute commands in a Docker container via ``docker exec``.
A thin :class:`PosixShellSandbox` backend: file and code operations are
inherited (run as shell commands), and only :meth:`execute_streaming` is
implemented, building the ``docker exec`` argv.
"""
def __init__(self, container: str, *, working_dir: str | None = None, user: str | None = None) -> None:
"""Initialize the Docker sandbox.
Args:
container: ID or name of a running Docker container.
working_dir: Working directory for executed commands. If ``None``, no
``-w`` flag is set and commands run in the container's configured
working directory. The path must exist and be writable by the
effective ``user``.
user: User to run commands as, in ``"uid"``, ``"uid:gid"``, or
``"name"`` form. If ``None``, no ``--user`` flag is set and
commands run as the container's configured user.
"""
self.container = container
self.working_dir = working_dir
self._user = user
async def execute_streaming(
self,
command: str,
*,
timeout: float | None = None,
cwd: str | None = None,
env: dict[str, str] | None = None,
**kwargs: Any,
) -> AsyncGenerator[StreamChunk | ExecutionResult, None]:
"""Execute a command in the container, streaming output.
Args:
command: The shell command to execute.
timeout: Maximum execution time in seconds. ``None`` means no timeout.
cwd: Working directory for this command, overriding ``working_dir``.
env: Environment variables to set, passed as ``docker exec -e`` flags.
**kwargs: Additional keyword arguments for forward compatibility.
Yields:
:class:`StreamChunk` objects for output, then a final
:class:`ExecutionResult`.
Raises:
ValueError: If an environment variable name is invalid.
SandboxTimeoutError: If execution exceeds ``timeout`` seconds.
"""
args = ["exec"]
# An unset user/cwd defers to the container's own configuration.
if self._user is not None:
args += ["--user", self._user]
effective_cwd = cwd if cwd is not None else self.working_dir
if effective_cwd is not None:
args += ["-w", effective_cwd]
if env:
validate_env_keys(env)
for key, value in env.items():
# Values are passed as process argv (not through a shell), so no escaping
# is needed -- Docker stores them verbatim. This is why values are not
# shell-quoted here, unlike the SSH backend which builds a shell string.
args += ["-e", f"{key}={value}"]
# docker exec requires the container and command after all flags. '--' terminates
# flag parsing so the container is always treated as a positional argument; without
# it, a name like '--privileged' would be parsed as a flag and override the options
# above.
args += ["--", self.container, "sh", "-c", command]
async for chunk in _stream_process(
"docker", args, timeout=timeout, enoent_message="docker is not installed or not on PATH"
):
yield chunk
def get_tools(self) -> list[AgentTool]:
"""Default sandbox-compatible tools auto-registered with this sandbox.
Returns:
The tools bound to this sandbox, with descriptions naming this container.
"""
cwd = f" Working directory: {self.working_dir}." if self.working_dir else ""
return [
make_file_editor(
sandbox=self,
name="sandbox_file_editor",
description=f'{DEFAULT_FILE_EDITOR_DESCRIPTION} Files are in Docker container "{self.container}".',
),
make_shell(
sandbox=self,
name="sandbox_shell",
description=f'{SANDBOX_SHELL_DESCRIPTION} Runs in Docker container "{self.container}".{cwd}',
),
]