内容
# OpenCode 代理容器
自主的 [OpenCode](https://opencode.ai) 代理作为可部署的 HTTP 服务。



一次请求是一个完全自主的代理运行:流式跟踪,然后是最终结果。
容器拥有 OpenCode 运行时、守护进程生命周期、沙盒和提供者密钥。调用者发送任务并获取事件。
```
(提示、 工作空间、 工具、 模型、 模式、 预算)→ 流式事件 → 最终结果
```
## 为什么存在
像 OpenCode 和 Claude Code 这样的 CLI 代理已经具备了困难的部分:原生工具(`读取`、`写入`、`编辑`、`Bash`、`Glob`、`Grep`)、MCP、多轮循环、结构化输出。它们是为终端用户设计的。
这个容器使之可编程化。POST 一个任务;代理计划、运行代码、读取输出并自我纠正,将跟踪流式返回。相同的工具集涵盖编码和非编码工作(参见 [谁在使用它](#谁在使用它))。
- 一个请求是一个自包含的运行。无需逐步审批。
- 领域逻辑归调用者所有。提示、工具和分阶段数据定义作业;容器没有自己的 BGP 或海关知识。
- 每个运行都获得一个临时工作空间,带有真实工具和 MCP。
- 提供者密钥从不离开容器。调用者获取结果,而不是凭证。
## 无状态计算
更接近于无服务器函数而不是有状态服务。每个请求都是自包含的;任何容器都可以服务于任何请求;崩溃不会导致调用者无法重建的损失。没有会话表,没有工作空间缓存,没有容器中的对话历史记录。状态归调用者所有。
| 容器拥有 | 调用者拥有 |
| ---------------------------------- | -------------------------------------------- |
| OpenCode 二进制文件 + 守护进程生命周期 | 缓存/配置工作空间 |
| 代理循环:运行 → 流式传输 → 结果 | 会话历史/先前发现 |
| 临时工作空间 + 清理 | 领域逻辑(提示/工具/模式) |
| 沙盒、权限、出口 | 构建工作空间内容 |
| 提供者密钥(托管边界) | 最终用户身份/身份验证 |
多轮连续性类似于聊天 API 的工作方式:调用者将状态向前传递。
传递工作空间作为引用。传递先前发现的结果作为结构化输出或简短摘要,而不是原始记录。
`工作空间`是一个指针,指向调用者已经分阶段的数据,而不是字节本身:
```json
"workspace": { "source": "https://…/run-42.tgz", "mode": "ro" }
```
容器将该文件分阶段到一个临时目录,运行代理,并在运行结束时删除它。
`https://`(和循环访问 `http://`)通过网络拉取,以便容器与调用者不共享文件系统。本地 `file://` / 裸路径在共享主机时仍然有效。原生 `s3://` 和大数据只读挂载尚未内置。带有 `Bash` 的 LLM 不应与受信任的后端共享磁盘或网络;参见
[设计 → 范围](DESIGN.md#范围-原型-vs-理想部署) 和 [部署.md](DEPLOY.md)。
## 架构
```mermaid
flowchart TB
subgraph 调用者["调用服务(拥有状态 + 领域)"]
direction LR
C1["构建提示/模式<br/>分阶段工作空间<br/>线程先前发现"]
end
subgraph 容器["代理容器(无状态计算)"]
API["FastAPI + SSE<br/>POST /agent/run · 持有者身份验证"]
RUN["OpenCodeRunner<br/>预算 · 轮次 · 超时上限"]
WS["临时工作空间<br/>分阶段 → 运行 → 清理"]
subgraph 守护进程["OpenCode 守护进程(每个请求)"]
AGENT["自主代理循环"]
TOOLS["读取 · 写入 · 编辑<br/>Bash · Glob · Grep · MCP"]
end
PROXY["密钥注入代理<br/>(持有真实密钥)"]
end
PROVIDER["模型提供者<br/>Anthropic · OpenAI · 自定义 vLLM"]
C1 -- "POST 任务 + 工作空间引用" --> API
API --> RUN --> 守护进程
RUN --- WS
AGENT --> TOOLS
守护进程 -- "模型调用,虚拟密钥" --> PROXY
PROXY -- "注入真实密钥" --> PROVIDER
守护进程 -- "令牌/工具事件" --> API
API -- "SSE:令牌 · 工具 · 成本 · 完成" --> C1
```
每个请求都获得自己的守护进程和临时工作空间,因此一个调用者的工具无法访问另一个调用者的文件。两者都在运行结束时被回收,包括在客户端断开连接时。
模型调用让守护进程使用虚拟密钥,并通过进程内代理注入真实密钥。
### 一次运行,端到端
```mermaid
sequenceDiagram
participant 调用者
participant 容器 as 容器 API
participant 守护进程 as OpenCode 守护进程
participant 代理 as 密钥代理
participant 模型
调用者->>容器: POST /agent/run (持有者) {提示, 工作空间, 工具, 模式, 预算}
容器->>容器: 分阶段工作空间 → 临时工作目录
容器->>守护进程: 在当前工作目录启动守护进程,启动运行
loop 自主轮次(上限)
守护进程->>代理: 模型请求(虚拟密钥)
代理->>模型: 请求(注入真实密钥)
模型-->>守护进程: 响应
守护进程->>守护进程: 在沙盒中运行工具(Bash/读取/写入...)
守护进程-->>容器: 令牌/工具事件
容器-->>调用者: SSE: 令牌 · 工具 (+ : 保持连接)
end
容器-->>调用者: SSE: 成本 · 完成 (OpenCodeResult)
容器->>容器: 回收守护进程 + 工作空间
```
## API
### `POST /agent/run` (需要持有者令牌)
```jsonc
{
"模型": "anthropic/claude-haiku-4-5-20251001", // 或自定义 OpenAI 兼容 ID
"系统提示": "…", // 可选
"提示": "…", // 任务
"工具": ["读取", "Bash", "写入"], // 来自工具注册表(空 = 普通推理)
"响应模式": { "…": "JSON 模式" }, // 可选 → 结构化输出
"推理努力": "低|中等|高|最大", // 可选
"最大预算美元": 0.50, // 硬性每请求断路器(夹紧到容器上限)
"工作空间": { "源": "https://…/run-42.tgz", "模式": "ro" } // 可选;file:// 在共享主机上也有效
}
```
响应 `200 text/event-stream`:
| 事件 | 有效载荷 |
| -------------- | ------------------------------------------------------------------------ |
| `令牌` | 助手-文本增量 |
| `工具` | 工具调用 `名称` / `状态` / `输入`,以及 `输出` 当完成 |
| `: 保持连接` | 注释行,当空闲时(保持中间件不超时) |
| `成本` | 运行支出 |
| `完成` | 终端 `OpenCodeResult`(如下) |
`完成` 事件携带:
| 字段 | 含义 |
| ------------------- | ---------------------------------------------------------------------------- |
| `文本` | 免费文本最终答案 |
| `结构化输出` | 验证对象,当请求 `响应模式` |
| `总成本美元` | 总支出 |
| `持续时间毫秒` | 墙上时钟 |
| `轮次` | 内部代理轮次采取 |
| `子类型` | `成功` / `最大轮次` / `错误超时` / `错误最大预算美元` / `错误` |
| `是否错误` | 调用者是否应将运行视为失败 |
没有 `工具` 和没有 `工作空间` 的运行是普通推理。添加工具和工作空间以获取代理循环。
### `GET /health`
`{"状态": "正常"}` 一旦服务启动。
## 快速入门
需要在 `PATH` 上安装 `OpenCode` 二进制文件(Docker 镜像固定一个)和提供者密钥。
`AGENT_POD_TOKEN` 是必需的:没有它,容器会关闭(503)。
```bash
# 本地(Python 3.12),自动加载 .env
cp .env.example .env # 设置 AGENT_POD_TOKEN + 提供者密钥(例如 ANTHROPIC_API_KEY)
python -m venv .venv && .venv/bin/pip install -r requirements.txt
.venv/bin/python -m pod # 在 :8080 上提供服务
# Docker
./run_pod.bash # 构建 + 运行,等待 /health
# 或手动:
docker build -t opencode-agent-pod:dev .
docker run --rm -p 8080:8080 \
-e AGENT_POD_TOKEN=$POD_TOKEN -e ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \
opencode-agent-pod:dev
```
烟雾测试:
```bash
curl -N -X POST http://localhost:8080/agent/run \
-H "Authorization: Bearer $AGENT_POD_TOKEN" -H 'Content-Type: application/json' \
-d '{"模型": "anthropic/claude-haiku-4-5-20251001", "提示": "说你好。"}'
```
## 配置
所有配置都是 `AGENT_POD_*` 环境变量:
| 变量 | 默认 | 目的 |
| ------------------------------ | -------------------- | ------------------------------------------------------------- |
| `AGENT_POD_TOKEN` | *(未设置)* | 共享持有者令牌,必需。未设置意味着每个请求返回 503 |
| `AGENT_POD_MAX_BUDGET_USD` | *(无)* | 每个运行的预算上限 |
| `AGENT_POD_DEFAULT_BUDGET_USD` | *(无)* | 请求未命名时的预算 |
| `AGENT_POD_SESSION_TIMEOUT` | `900` | 每运行的时钟上限(秒) |
| `AGENT_POD_MAX_TURNS` | `30` | 每运行的内部轮次上限 |
| `AGENT_POD_KEEPALIVE_SECONDS` | `15` | SSE 保持连接注释之间的间隔 |
| `AGENT_POD_HOST` / `_PORT` | `127.0.0.1` / `8080` | 监听器绑定 |
| `AGENT_POD_KEY_PROXY` | `1` | 密钥注入代理开启/关闭(开启;仅用于调试) |
| `AGENT_POD_KEY_PROXY_HOST` | `127.0.0.1` | 代理绑定的接口(保持主机本地) |
另外:
- 提供者密钥(`ANTHROPIC_API_KEY`、`OPENAI_API_KEY`,...):作为秘密文件挂载,由密钥代理使用,从不返回。
- `AGENT_CUSTOM_PROVIDERS`:自定义 OpenAI 兼容提供者的 JSON 数组(例如 vLLM):`[{"名称":"vllm","基础网址":"http://主机:8000/v1","模型":["llama-bgp"]}]`。
- `AGENT_MCP_SERVERS`: `{名称, 命令, 工具}` 注册 MCP 服务器。容器内置无;领域工具在此注册。
## 安全
仅限受信任网络使用。 不适用于公共互联网。 容器运行 LLM 编写的代码并持有提供者密钥。
| 控制 | 它能做什么 |
| --------- | ---------------------------------------------------------------------------------------------------- |
| 身份验证 | 需要持有者令牌;如果未设置,则失败关闭(503) |
| 支出 | 每个请求的预算上限 |
| 隔离 | 专用守护进程 + 每个请求的临时工作空间;运行完成后和断开连接时回收 |
| 密钥 | 真实密钥仅存在于进程内代理中;守护进程及其 `Bash` 子进程看到虚拟密钥 |
| 出口 | 进程内密钥盗窃已关闭。阻止工作空间外流到任意主机需要部署时的网络允许列表;参见 [部署.md](DEPLOY.md) |
镜像构建、出口 `NetworkPolicy` 和扩展说明: [部署.md](DEPLOY.md)。
## 谁在使用它
容器本身是领域无关的。这些是真正的调用者,而不是依赖项:
- [BGP-LLaMA](https://github.com/hyonbokan/BGP-LLaMA-webservice):路由分析师,执行代码。后台将范围内的 BGP 数据分阶段为工作空间;代理编写分析脚本,运行它(`Bash`),自我纠正,并将跟踪流式传输到浏览器。
- [ai-customs](https://github.com/hyonbokan/ai-customs):声明交叉检查器。在 30 个真实的海关文件中,脚本化的管道将 8 个估值临界值升级为一个 `POST /agent/run` 每个;代理解决了所有 8 个(5 个提取神器,3 个真实的低申报)。
典型集成:构建提示和模式,分阶段工作空间(`https://` 或本地 `file://`),POST 每个工作单位的运行,将 `令牌`/`工具` SSE 传递给您的 UI,并将结构化输出转发为先前发现的结果。
## 开发
```bash
.venv/bin/python -m pytest tests/ -q # 单元测试(镜像代理/和容器/)
.venv/bin/python -m ruff check . # lint(行长度 100; E,W,F,I,UP,B,C4)
.venv/bin/python -m mypy . # 类型(实用)
```
约定:Python 3.12,Ruff + mypy(参见 `pyproject.toml`),环境驱动配置。保持容器领域无关:没有 BGP 或海关词汇的容器代码。如果更改会教容器什么是“timerange”或“declaration”,它属于调用者。
## 布局
```
opencode-agent-pod/
├── 代理/ # 引擎:OpenCodeRunner,响应契约,
│ └── OpenCode/ # 守护进程生命周期,客户端,驱动程序,事件/预算上限,提供者+密钥映射
├── 容器/ # FastAPI + SSE 服务于引擎
│ ├── app.py # create_app:持有者身份验证,POST /agent/run,/health,生命周期排水
│ ├── service.py # 预算解析,运行程序组装,SSE 生成器,每个运行的回收
│ ├── workspace.py # 按引用分阶段工作空间 → 临时工作目录 → 回收
│ ├── key_proxy.py # 密钥注入反向代理:真实密钥从不进入守护进程
│ └── schema.py # RunRequest + JSON 模式响应传递
├── core/tools/mcp.py # 配置驱动的 MCP 注册表(AGENT_MCP_SERVERS)
├── llm/ # 提供者/推理类型,模型解析,重试配置
├── tests/ # 单元测试
├── Dockerfile # 可部署镜像(固定 OpenCode + 服务)
├── DESIGN.md # 架构和范围
└── DEPLOY.md # 部署 + 出口强化
```