226 lines
7.1 KiB
Markdown
226 lines
7.1 KiB
Markdown
# 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:
|
|
|
|
1. Maps host-side file paths to container-side equivalents.
|
|
2. Executes the tool via `devcontainer exec`.
|
|
3. Retrieves results and maps output paths back to the host.
|
|
4. Records container metadata on the `ToolInvocation` audit 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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
# 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`:
|
|
|
|
```python
|
|
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`:
|
|
|
|
```python
|
|
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:
|
|
|
|
```json
|
|
{
|
|
"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_host` resolves 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. Explicit `host_sandbox_path` is
|
|
required in that case.
|
|
- **`O_NOFOLLOW` writes**: Files written by `sync_results_to_host`
|
|
use `O_NOFOLLOW` to prevent symlink-following at write time.
|
|
- **Root mapping rejection**: `PathMapper` rejects `/` 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_host` are created with mode
|
|
`0o600` (owner read/write only).
|
|
- Parent directories are created with mode `0o700` (owner only).
|
|
- The `devcontainer exec` binary is resolved at init time via
|
|
`shutil.which` to 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=False` per RFC 7159).
|
|
- JSON inputs are piped via stdin rather than shell arguments to
|
|
avoid `ARG_MAX` limits and shell injection.
|
|
|
|
## ToolRunner Integration
|
|
|
|
The `ToolRunner` accepts an optional `container_executor` parameter:
|
|
|
|
```python
|
|
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.
|