7.1 KiB
Execution Environment Reference
This document describes how CleverAgents routes tool execution between host and container environments, including the container-aware tool executor, path mapping, and I/O forwarding.
Overview
When a plan or tool specifies execution_environment: container,
the tool runner delegates execution to a ContainerToolExecutor that:
- Maps host-side file paths to container-side equivalents.
- Executes the tool via
devcontainer exec. - Retrieves results and maps output paths back to the host.
- Records container metadata on the
ToolInvocationaudit trail.
Execution Environment Resolution
The execution environment is resolved using a priority chain:
| Priority | Source | Example |
|---|---|---|
| 1 (highest) | Tool-level override | tool_env="container" |
| 2 | Plan-level override | --execution-environment container |
| 3 | Project-level default | ContextConfig.execution_environment |
| 4 (lowest) | System default | HOST |
If CONTAINER is resolved, the system validates that a container
resource (devcontainer-instance, container-instance, or
devcontainer-file) is linked to the project.
Path Mapping
The PathMapper utility translates file paths between host and
container filesystems:
from cleveragents.tool.path_mapper import PathMapper
mapper = PathMapper(
host_root="/tmp/sandbox/project-1",
container_root="/workspace",
)
# Host to container
mapper.host_to_container("/tmp/sandbox/project-1/src/main.py")
# => "/workspace/src/main.py"
# Container to host
mapper.container_to_host("/workspace/src/main.py")
# => "/tmp/sandbox/project-1/src/main.py"
# Paths outside root are returned unchanged
mapper.host_to_container("/usr/lib/libc.so")
# => "/usr/lib/libc.so"
Path Mapping in Tool Inputs
File path arguments in tool inputs are automatically mapped before container execution. The executor recursively traverses the input dict, mapping any string values that fall under the host root:
# Before mapping
{"path": "/tmp/sandbox/src/main.py", "count": 5}
# After mapping
{"path": "/workspace/src/main.py", "count": 5}
Nested dicts and lists are also traversed.
Container Tool Executor
The ContainerToolExecutor wraps tool execution via devcontainer exec:
from cleveragents.tool.container_executor import (
ContainerConfig,
ContainerToolExecutor,
)
config = ContainerConfig(
workspace_folder="/workspace",
container_id="my-container",
image="ghcr.io/org/devimage:latest",
host_sandbox_path="/tmp/sandbox/project-1",
timeout_seconds=120,
)
executor = ContainerToolExecutor(config)
result = executor.execute_tool("file_read", {"path": "/tmp/sandbox/project-1/src/main.py"})
Configuration
| Parameter | Type | Default | Description |
|---|---|---|---|
workspace_folder |
str |
/workspace |
Workspace root inside the container |
container_id |
str |
"" |
Docker/Podman container ID |
image |
str |
"" |
Container image reference |
timeout_seconds |
int |
120 |
Max execution time (seconds) |
host_sandbox_path |
str |
"" |
Host-side sandbox root |
Timeout Handling
If a container tool execution exceeds timeout_seconds, the
executor returns a ToolResult with success=False and an error
message indicating the timeout. The metadata.container dict on
the result will have timed_out=True.
Error Reporting
Container execution failures produce structured ToolResult objects:
- Timeout:
error="Container execution timed out after Ns" - Non-zero exit:
error="Container execution failed (exit code N): stderr" - OS error:
error="Container execution failed (exit code -1): error message"
All errors include container_metadata with execution details.
Result Retrieval
After container execution, file-based outputs can be synced back to
the host using sync_results_to_host:
host_path = executor.sync_results_to_host("/workspace/build/output.txt")
# => "/tmp/sandbox/project-1/build/output.txt"
Audit Trail
When a tool executes inside a container, the ToolResult.metadata
dict includes a container key with execution details, and the
ToolInvocation record stores this as the container_metadata field:
{
"metadata": {
"container": {
"container_id": "abc123",
"image": "ghcr.io/org/devimage:latest",
"workspace_folder": "/workspace",
"exec_time_ms": 1500.0,
"exit_code": 0,
"timed_out": false
}
}
}
This provides full traceability for container-routed tool executions.
Security Model
Container tool execution enforces several layers of protection:
Environment Variable Filtering
Only a minimal allowlist of environment variables (PATH, LANG,
TERM) is forwarded to the subprocess that invokes devcontainer exec. All other host environment variables—including secrets,
credentials, and HOME—are excluded.
Symlink and Path Traversal Protection
- Sandbox root validation:
sync_results_to_hostresolves the mapped host path and verifies it falls within the sandbox root before writing any data. Path traversal via..is rejected. - Symlink guard: The default sandbox fallback (
/tmp/sandbox) is rejected if it is a symlink. Explicithost_sandbox_pathis required in that case. O_NOFOLLOWwrites: Files written bysync_results_to_hostuseO_NOFOLLOWto prevent symlink-following at write time.- Root mapping rejection:
PathMapperrejects/and//as host or container roots to prevent mapping every absolute path.
Output Caps
Captured stdout and stderr are bounded to 50 MiB per stream. Output exceeding this limit is truncated with a warning log entry. This prevents a misbehaving container from exhausting host memory.
Permission Model
- Files written by
sync_results_to_hostare created with mode0o600(owner read/write only). - Parent directories are created with mode
0o700(owner only). - The
devcontainer execbinary is resolved at init time viashutil.whichto prevent PATH hijacking. If the binary is not found, all execution calls fail fast with a descriptive error.
Input Validation
- Null bytes in paths are rejected at all entry points.
- Tool inputs are validated as JSON-serialisable before container
execution (with
allow_nan=Falseper RFC 7159). - JSON inputs are piped via stdin rather than shell arguments to
avoid
ARG_MAXlimits and shell injection.
ToolRunner Integration
The ToolRunner accepts an optional container_executor parameter:
from cleveragents.tool.runner import ToolRunner
from cleveragents.tool.container_executor import (
ContainerConfig,
ContainerToolExecutor,
)
config = ContainerConfig(...)
executor = ContainerToolExecutor(config)
runner = ToolRunner(
registry=registry,
container_executor=executor,
)
When the execution environment resolves to CONTAINER and a
ContainerToolExecutor is configured, the runner delegates to it
automatically. If no executor is configured, the runner returns an
error result indicating that container execution is not available.