- 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 первым)
117 lines
4.8 KiB
Python
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}',
|
|
),
|
|
]
|