Metadata-Version: 2.5
Name: sandbox-agent
Version: 0.2.2
Summary: Run LLM agents inside disposable Docker sandboxes.
Project-URL: Homepage, https://cnb.cool/maikebuke/Tools/DockerHub/opencode
Project-URL: Repository, https://cnb.cool/maikebuke/Tools/DockerHub/opencode.git
Author-email: Birkhoff <admin@maikebuke.com>
License-Expression: GPL-3.0-or-later
License-File: LICENSE
Keywords: agent,claude-code,codex,docker,llm,opencode,sandbox
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.12
Description-Content-Type: text/markdown

# sandbox-agent

把 LLM agent CLI（opencode / codex / claude / pi / dsh）关进一次性 Docker 容器里跑，
用完即毁，产出安全地取回宿主机。零运行时依赖，宿主机只需要 `docker`。

```python
from sandbox_agent import SandboxAgent, OpenCodeDriver

driver = OpenCodeDriver(provider="zhipuai-coding-plan", api_key=API_KEY)

with SandboxAgent(driver=driver, model="zhipuai-coding-plan/glm-5.3") as agent:
    print(agent.ask("用 Python 写个快排, 存到 /tmp/qs.py"))

    code, out = agent.container.exec(["python", "/tmp/qs.py"])  # 验收产出
    agent.container.download("/tmp/qs.py", "./qs.py")           # 取回宿主机
# 退出 with 块 -> 容器销毁
```

## 安装

```bash
uv add sandbox-agent      # 或 pip install sandbox-agent
```

还需要装好了 agent CLI 的镜像，见 [`docker/`](docker/) 与 [`.ci/build_docker.sh`](.ci/build_docker.sh)。
镜像 tag 由包内的 [`versions.json`](src/sandbox_agent/versions.json) 唯一推导，格式
`{repo}:{cli}-{cli版本}-{image_date}`，不用 `latest`。装好的包指向哪个镜像，直接问它：

```bash
python -c "from sandbox_agent import versions; print(versions.CLAUDE_IMAGE)"
```

镜像优先级：显式 `image=` > `SANDBOX_AGENT_IMAGE_NAME`（换私有 registry 用）>
`driver.default_image` > 内置默认值。

## 能干什么

| | |
|---|---|
| **对话** | `chat()` 拿可迭代的流式事件，推进主线；`ask()` 开旁支问一句，问完即扔 |
| **会话** | `sessions.list()` / `.fork()` / `.dump()` / `.load()` —— 会话能跨容器搬 |
| **容器操作** | `container.upload()` / `.download()` / `.read()` / `.write()` / `.exec()` —— 取回时只落普通文件和目录，链接不落地 |
| **能力注入** | `skills.add()` / `.add_many()` / `.list()`，`rules.install()` |
| **可观测** | `token_usage`、`AgentEvent` 流、按 tag 筛容器（`list_containers`） |
| **可审计** | 每次会话自动归档到 `/tmp/sandbox-agent-<uid>/`，默认开 |

## 扩展点

- **新 agent CLI**：继承 `BaseDriver`，实现 11 个抽象成员。driver 只通过 `ContainerOps`
  的三个原语（`run` / `read_file` / `write_file`）碰容器，单测伪造这三个方法即可。
- **新容器后端**（podman、远程执行器）：实现 `ContainerRuntime` 协议，传给
  `SandboxAgent(runtime_factory=...)`。

完整 API 见 [docs/sandbox_agent.md](docs/sandbox_agent.md)。

## 端到端冒烟

[main.py](main.py) 对真 CLI 跑一遍主要行为（容器 I/O、工具事件、ask、续接、fork 隔离、
skill / rules、delete / dump、审计、跨容器 load、假 key 抛 `DriverError`），多个 driver 并行：

```bash
uv run python main.py                 # 默认只跑 opencode
uv run python main.py claude codex    # 指定若干个 (opencode|codex|claude|pi|dsh)
uv run python main.py all             # 五个全跑
```

opencode 直连智谱，要 `ZHIPUAI_CODING_TOKEN`；其余四个走 [`runtime/litellm/`](runtime/litellm/)
网关（`GATEWAY_URL` / `GATEWAY_KEY` 有默认值），网关怎么起见那边的 README。

## 会话审计

```
/tmp/sandbox-agent-<uid>/<YYYYMMDD>/<session_id>/
    events.jsonl    归一后的 AgentEvent，一行一个；line 字段回指 session.jsonl 的行号
    session.jsonl   CLI 原始 stdout，逐字节照抄，不过滤
    status.json     元数据 + 每一轮的结果
```

同一 `session_id` 的多轮落进同一目录，流式追加，可直接 `tail -f`。`thinking` 事件只在
后端返回推理正文时才有内容。归档失败只发 `RuntimeWarning`，不影响 chat。

```bash
jq -r '"\(.kind)\t\(.payload.command // .payload.path // .payload.text)"' events.jsonl
```

```python
SandboxAgent(driver=driver, audit=False)     # 关闭

from sandbox_agent import audit
audit.AUDIT_ROOT = "/data/sandbox-audit"
audit.RETENTION_DAYS = 30                    # 默认 7; 0 = 永不清理, 按日期目录整个删
```

## 容器生命周期

`close()` / 退出 `with` 即销毁。兜底：容器闲置 `keepalive_seconds`（默认 3600s）后自杀，
有命令在跑就持续续期。

## 配置

优先级：环境变量 > `~/.config/sandbox-agent/config.json` > 内置默认值。

| 环境变量 | JSON key | 默认值 |
|---|---|---|
| `SANDBOX_AGENT_IMAGE_NAME` | `image` | driver 自己的 `default_image` |
| `SANDBOX_AGENT_RESOURCE_PREFIX` | `prefix` | `sandbox-agent` |
| `SANDBOX_AGENT_CONTAINER_MEMORY` | `memory` | `1g` |
| `SANDBOX_AGENT_CONTAINER_CPUS` | `cpus` | `1.0` |
| `SANDBOX_AGENT_KEEPALIVE_SECONDS` | `keepalive_seconds` | `3600`（`0` = 不自杀） |
| `SANDBOX_AGENT_MAX_CONTAINERS` | `max_containers` | `20` |
| `SANDBOX_AGENT_START_TIMEOUT` | `start_timeout` | `120` |
| `SANDBOX_AGENT_STOP_TIMEOUT` | `stop_timeout` | `30` |
| `SANDBOX_AGENT_EXEC_TIMEOUT` | `exec_timeout` | `60` |
| `SANDBOX_AGENT_CHAT_TIMEOUT` | `chat_timeout` | `300` |
| `SANDBOX_AGENT_SESSION_TIMEOUT` | `session_timeout` | `120` |
| `SANDBOX_AGENT_TRANSFER_TIMEOUT` | `transfer_timeout` | `300` |

方法参数 `timeout=None` 即用对应类别的配置值。

## 开发

```bash
uv sync --all-extras
uv run pytest                    # 单元测试, 全 mock, 不碰 docker
uv run pytest -m integration     # 集成测试, 起真容器
docker pull debian:stable-slim   # 集成测试用的镜像, 要事先拉好 (不会自动 pull, 没有则跳过)
```

集成测试镜像可用 `SANDBOX_AGENT_TEST_IMAGE` 替换，别用 alpine（busybox 的 `timeout`
超时不返回 124）。fixture 快照见 [tests/fixtures/README.md](tests/fixtures/README.md)。
已知限制见 [TODO.md](TODO.md)，变更历史见 [CHANGELOG.md](CHANGELOG.md)。

## 发布

先镜像后包：包里的 `versions.json` 指向镜像。改了 CLI 版本或 `Dockerfile.base` 时，
先手动 bump `versions.json` 的 `image_date`（`YYYYMMDD`，不会自动生成）。

```bash
./.ci/build_docker.sh --push     # 1. 镜像; tag 已存在会拒绝, --force 覆盖
./.ci/build_pypi.sh   --push     # 2. 包; registry 上缺镜像会拒绝
```

## License

GPL-3.0-or-later
