agent-compose.yml 配置手册
本文档说明当前代码实际接受的 agent-compose.yml / agent-compose.yaml 字段、默认值、约束和使用方式。解析器采用严格字段校验:未知字段、重复字段以及类型不符都会报错,因此字段名必须与本文一致。
重要:项目级工作区只支持顶层复数键
workspaces。顶层workspace已不受支持,会被当作未知字段拒绝。Agent 选择某个工作区时仍使用单数键agents.<agent>.workspace。
可以在应用配置前进行本地校验:
agent-compose config --quiet
agent-compose -f ./path/to/agent-compose.yml config
第一条命令只校验;第二条还会输出归一化后的配置,并对标记为 secret 的值进行脱敏。默认查找当前目录下的 agent-compose.yml,其次查找 agent-compose.yaml;两者同时存在时应使用 -f/--file 明确选择。
兼容性策略
公开 YAML 编写格式自 v2608.1.0 起保持稳定:该版本或后续任一稳定版本能够接受的配置,在之后的稳定版本中仍应有效。这是向后兼容承诺;新版本仍可增加可选字段,而使用严格解析器的旧版本不保证识别这些新字段。
删除或改名稳定字段、进行不兼容的类型变更,以及把可选字段改为必填字段均属于破坏性变更。收窄已有短写形式或校验约束,以及改变默认值、插值或归一化语义,也必须经过兼容性审查。安全修复可以有意拒绝不安全的历史值,但必须说明影响和迁移方式。
仓库通过机器可读的 v2608.1.0 字段契约以及累积的解析和归一化 fixture 执行该策略。agent-compose config --quiet 仍是用户使用已安装版本校验项目配置的入口;跨版本契约比较属于仓库 CI 职责,不新增独立 CLI 命令。
完整结构速览
下面的配置用于展示字段所在位置。它刻意包含了较多能力,实际项目只需保留需要的部分。
name: review-pipeline
env_file:
- .env
- .env.local
variables:
DISPLAY_NAME: review-pipeline
CONTROL_TOKEN:
value: ${CONTROL_TOKEN}
secret: true
workspaces:
source:
provider: file
path: .
upstream:
provider: git
url: https://github.com/example/project.git
ref: main
target: .
mcp_servers:
local-tools:
type: local
command: npx
args: ["-y", "@example/mcp-server"]
env:
API_TOKEN:
value: ${MCP_API_TOKEN}
secret: true
issue-tracker:
type: remote
transport: http
url: ${ISSUE_TRACKER_MCP_URL}
headers:
Authorization:
value: Bearer ${ISSUE_TRACKER_TOKEN}
secret: true
octobus_servers:
internal:
url: https://octobus.internal.example
token: ${OCTOBUS_INTERNAL_TOKEN}
public:
url: https://octobus.example
token: ${OCTOBUS_PUBLIC_TOKEN}
volumes:
cache:
name: review-cache
driver: local
labels:
purpose: agent-cache
options: {}
policies:
locked-down:
stopped_runtime_policy: retain
network:
default: deny
allow:
- host: api.github.com
port: 443
protocol: https
agents:
reviewer:
enabled: true
provider: codex
model: ${REVIEW_MODEL}
system_prompt: |
Review changes carefully and report concrete evidence.
image: chaitin/agent-compose-guest:latest
build:
context: .
dockerfile: guest-images/Dockerfile.agent-compose-guest
target: runtime
args:
CHANNEL: stable
platforms: [linux/amd64]
tags: [review-agent:latest]
no_cache: false
pull: true
driver:
docker: {}
env:
LOG_LEVEL: info
SERVICE_TOKEN:
value: ${SERVICE_TOKEN}
secret: true
mcp_servers:
- local-tools
- name: audit-api
type: remote
transport: sse
url: https://mcp.example.com/sse
headers:
Authorization:
value: Bearer ${AUDIT_TOKEN}
secret: true
capset_ids:
- engineering
- internal/code-review
skills:
- name: review
provider: file
path: ./skills/review
- name: release-check
provider: git
url: https://github.com/example/agent-skills.git
path: skills/release-check
ref: main
volumes:
- cache:/cache
- type: bind
source: ./reports
target: /workspace/reports
read_only: false
workspace:
name: source
policy: locked-down # 引用 `policies` 中的条目;不会与其合并
scheduler:
enabled: true
sandbox_policy: sticky
triggers:
- name: hourly-review
cron: "0 * * * *"
prompt: Review the current workspace.
sandbox_policy: new
jupyter:
enabled: false
guest_port: 8888
# 策略库是目录而不是默认值:这个 Agent 自行声明策略块,不会合并任何库条目。
spot-checker:
provider: codex
policy:
network:
default: allow-all
通用规则
严格解析
- 顶层和各对象中的未知字段会报
unknown field,不会被静默忽略。 - 同一映射中的重复字段会报
duplicate field。 - 布尔字段必须是布尔值,列表字段必须是 YAML sequence,映射字段必须是 YAML mapping。
- 项目名必须匹配
^[a-z0-9][a-z0-9_-]*$。Agent 名、顶层 Workspace 键、MCP 名、Volume 键和最终 Skill 名使用更严格的^[a-z][a-z0-9_-]*$格式。 - 路径和 URL 的相对基准因字段而异,具体见对应章节。
环境变量来源和优先级
env_file 决定 ${NAME} 插值时可读取哪些 dotenv 文件。加载顺序如下:
- 按
env_file列表顺序加载,后面的文件覆盖前面的同名值。 - 运行
agent-composeCLI 的进程环境最后覆盖 dotenv 文件。
若未配置 env_file,CLI 先查配置文件所在目录的 .env,不存在时再查当前工作目录的 .env。显式写 env_file: [] 表示不自动加载任何 dotenv 文件。显式文件不存在、不可读或列表中有空路径都会失败。
${NAME} 只支持简单形式,不支持 shell 的 ${NAME:-default}、命令替换或递归展开。引用的变量不存在时配置校验失败。
当前支持插值的位置包括:
variables.*.valueagents.*.modelagents.*.env.*.value- 项目级和 Agent 内联 MCP 的
url、env.*.value、headers.*.value - 项目级 OctoBus 的
url和token - Skill 的
name、source、url、path、ref、username、password和token;其中password、token必须是完整${NAME}引用
其他字符串字段不会自动插值,例如 name、provider、image、system_prompt、Workspace 的其他字段、Build 字段和 Scheduler 字段。
环境值的两种写法
variables、agents.*.env、MCP env 和 MCP headers 都使用同一个值结构:
PLAIN_VALUE: hello
SECRET_VALUE:
value: ${SECRET_VALUE}
secret: true
| 字段 | 类型 | 默认值 | 作用 |
|---|---|---|---|
value |
string | "" |
实际值;在受支持的位置执行 ${NAME} 插值。 |
secret |
bool | false |
标记敏感值。规范化配置输出会显示 ********,运行时仍使用真实值。 |
secret 是脱敏元数据,不会自行从环境读取值;仍需在 value 中写 ${NAME}。
顶层字段
| 字段 | 类型 | 必填 | 作用 |
|---|---|---|---|
name |
string | 条件必填 | 项目标识。省略时按照 Docker Compose 规则从配置文件目录推导。 |
env_file |
string 或 string[] | 否 | 指定插值使用的 dotenv 文件。相对路径以配置文件目录为基准。 |
variables |
map | 否 | 项目级命名变量及 secret 元数据,写入规范化项目配置。它们不会自动继承到 Agent 的 env,也不会成为其他 ${NAME} 的变量来源。 |
workspaces |
map | 否 | 可复用的项目级 Workspace 定义。只能使用复数形式。 |
mcp_servers |
map | 否 | 可由 Agent 按名称引用的 MCP Server 定义。 |
octobus_servers |
map | 否 | 由 qualified capset_ids 选择的具名项目级 OctoBus Server。 |
volumes |
map | 否 | 项目管理或引用的持久 Volume。 |
policies |
map | 否 | 具名策略块;Agent 按名称引用,无需重复声明。 |
agents |
map | 否 | Agent 定义;map key 是 Agent 名。 |
name
name: code-review
名称必须匹配 ^[a-z0-9][a-z0-9_-]*$。例如 review-v2 和 2-review 合法,Review 和包含空格的名称不合法。省略名称时,目录 basename 会转为小写、移除不支持的字符并裁掉开头的 _ 或 -。最终名称是 daemon 内全局唯一的 project 身份;移动 compose 文件不会创建另一个 project。
env_file
单文件可以写标量:
env_file: .env.production
多文件写列表:
env_file:
- .env
- .env.production
variables
variables:
REGION: cn-hangzhou
RELEASE_TOKEN:
value: ${RELEASE_TOKEN}
secret: true
variables 当前用于保存项目级配置值和脱敏语义。若某个值要传入 sandbox,仍需在对应 Agent 的 env 中声明。
在这里声明的第一方 LLM 凭据不会交给 sandbox:daemon 会把它导入自己的 LLM 连接并代理本次 run,sandbox 只会收到本次 run 的 facade token。可识别的变量名包括 ANTHROPIC_API_KEY、ANTHROPIC_AUTH_TOKEN、OPENAI_API_KEY、CODEX_API_KEY、DEEPSEEK_API_KEY、OPENROUTER_API_KEY,以及可配合 LLM_API_PROTOCOL、LLM_API_ENDPOINT 的 LLM_API_KEY。被吸收的声明会连同它的端点变量一起留在 daemon:LLM_API_ENDPOINT、LLM_API_PROTOCOL、ANTHROPIC_BASE_URL、ANTHROPIC_API_ENDPOINT、OPENAI_BASE_URL、DEEPSEEK_BASE_URL、OPENROUTER_BASE_URL 都不会进入 sandbox,daemon 会在原处装上同一协议对应的 facade 地址。AZURE_OPENAI_API_KEY 能被识别但无法代理:daemon 会把它从 sandbox 环境中移除,而不是转发一个自己无法服务的凭据,因此 agent 同样看不到它。只有 daemon 完全不认识的形似凭据的名字(其它 *_API_KEY、*_AUTH_TOKEN)才会原样进入 sandbox,sandbox 内的进程可以读到。项目检查会分别说明每个声明属于哪种情况,并建议改用 daemon 侧的 LLM 配置——那才是凭据可以被显式管理、轮换和共享的地方。凡是 daemon 不交给 sandbox 的凭据——无论是否被吸收——在工程与 Agent 视图中一律脱敏为 ********,无论声明里有没有写 secret: true;变量名仍然保留,运维能看到自己声明了什么。OPENAI_BASE_URL 这类端点是地址而非凭据,仍按原值显示。这只影响展示,不影响 daemon 解析到的声明;某次 run 实际使用的 facade 地址与 token 只存在于该次 run 的环境中,不会被持久化。
workspaces:项目级工作区
顶层必须使用 workspaces:
workspaces:
source:
provider: file
path: .
以下旧写法无效:
# 错误:顶层 workspace 会被严格解析器拒绝
workspace:
provider: file
path: .
每个 workspaces.<key> 支持:
| 字段 | 类型 | 适用范围 | 作用 |
|---|---|---|---|
name |
string | 兼容字段 | 顶层条目的实际名称由 map key 决定;通常不要重复填写。 |
provider |
string | 必填 | file、git 或 http。 |
url |
string | git、http 必填 |
Git clone URL,或指向 ZIP 压缩包的 http/https URL;file 不允许设置。 |
ref |
string | git 可选 |
Git branch、tag 或 commit。http Workspace 不支持 ref。 |
path |
string | file 必填 |
相对于 compose 文件目录的来源路径,不可逃逸项目根目录;Git Workspace 不支持仓库内子目录。http 可为压缩包内的目录。 |
format |
string | http 必填 |
必须是 zip;系统会下载并解压 URL 作为隔离 Workspace。设置 path 时必须指向压缩包内已存在的目录。 |
target |
string | 可选 | sandbox workspace 根目录下的目标目录,默认 .。 |
mode |
string | 可选 | 默认 copy 创建隔离工作区;mount 将本地 file 来源直接映射到 Docker sandbox。 |
read_only |
bool | mount 可选 |
默认 false;设为 true 禁止 guest 通过该工作区挂载写入。copy 模式不允许设为 true。 |
username |
string | git 可选 |
Git HTTP 用户名。 |
password |
string | git 可选 |
Git 密码,只允许完整环境引用 ${NAME}。 |
token |
string | git 可选 |
Git token,只允许完整环境引用 ${NAME}。 |
本地 Workspace 默认为 mode: copy:每次项目 run 创建时复制为隔离快照,Agent 的修改不会写回源目录。省略 mode 与显式设置 copy 的行为相同。
workspaces:
source:
provider: file
path: ./src
release-branch:
provider: git
url: https://github.com/example/service.git
ref: release
target: .
remote-release:
provider: http
url: https://example.com/releases/service.zip
format: zip
path: service
target: .
通过 HTTP 获取 ZIP Workspace
http Workspace 在该 run 准备 sandbox 时下载 ZIP 压缩包并解压到 run workspace,因此每次 run 都从 URL 当时提供的内容开始:
workspaces:
remote-release:
provider: http
url: https://artifacts.example.com/releases/service.zip
format: zip
token: ${ARTIFACT_TOKEN}
path: service
target: src
行为说明:
- 只接受
http与httpsURL,重定向同样按该规则重新校验,且不允许从https降级到http,凭据不会以明文重发。请求会忽略代理环境变量,避免代理代替 daemon 解析目标地址。支持内网主机:URL 可以解析到 loopback 或私网地址,例如另一个 Docker Compose 服务名、发布到本机环回的端口,或 daemon 私网内的主机。Skill 来源同样遵循这套规则。 path必须指向压缩包内已存在的目录。path指向普通文件或不存在时,run 会直接失败,而不会生成空 Workspace;path逃逸压缩包(../、绝对路径)同样会被拒绝。- 解压前会校验每个条目:逃逸目标目录的条目与符号链接条目会被拒绝,条目权限会被清理(保留可执行位,清除 group/other 写权限,丢弃 setuid、setgid、sticky 位)。
- 单个 Workspace 的限制:下载压缩包 256 MiB、解压后 1 GiB、最多 100,000 个条目、压缩包解压超过 64 MiB 后压缩比上限 100:1、下载超时 10 分钟;超过任一限制都会使该 run 失败。
username、password、token只接受完整环境引用${NAME};配置 token 时以Authorization: Bearer发送,未配置 token 时才使用 Basic 认证。http不支持mode: mount,压缩包内容始终复制到 run workspace。
Workspace 选择规则:
- 顶层
workspaces只是命名定义,不会自动分配给任何 Agent。 - Agent 省略
workspace时,无论顶层定义了多少个 Workspace,该次 run 都不配置 Workspace。 - Agent 需要使用顶层 Workspace 时,必须通过
workspace.name显式引用,或定义内联 Workspace。 - 显式的空对象
workspace: {}非法;不使用 Workspace 时应省略该 key。
挂载本地 Workspace
使用 mode: mount 复用已有本地目录,跳过 run 快照和逐 sandbox 文件复制:
workspaces:
shared-input:
provider: file
path: ./reference-data
mode: mount
read_only: true
target: inputs
agents:
reviewer:
provider: codex
driver:
docker: {}
workspace:
name: shared-input
使用默认 guest 路径时,该示例把来源映射到 /workspace/inputs,其余工作区仍可写入输出。target: . 会挂载整个工作区根目录;如果设为只读,程序需要把输出写到其他可写位置。目标必须位于工作区内,且不可与 Agent 的 Volume 挂载目标重叠。Guest 镜像额外声明的 Volume 也不得覆盖挂载子树;重叠时启动会失败,不相关的镜像 Volume 仍受支持。
挂载模式仅支持 provider: file 和 Docker runtime。Git 来源及 BoxLite、Microsandbox、Kubernetes runtime 会被拒绝,不会静默回退到复制。项目记录中的来源路径必须指向已存在的目录或 Compose 配置文件。来源必须是项目来源目录内已存在的目录,且在 agent-compose daemon 文件系统中可见。远程 CLI 客户端上的路径不会自动上传。
原生运行 agent-compose daemon 时,支持通过本地 Unix socket 连接运行 Linux 容器的 Docker:Linux 上要求 Docker Engine 名称与本机主机名一致;macOS 上要求 Engine 标识为 Docker Desktop 或 OrbStack。原生进程通过 TCP(包括 localhost)或 SSH 连接、转发 socket,以及其他虚拟机集成都不受支持。agent-compose daemon 运行在容器内时,所连接的 Engine 必须能通过该容器的主机名查询它,来源也必须位于该容器已有的 bind 或 volume 挂载内;包含来源的最深层挂载决定宿主来源路径;tmpfs 等不可分享的嵌套挂载会阻止继续按父目录换算。仅存在于 daemon 容器可写层中的来源会被拒绝。DOCKER_HOST_SANDBOX_ROOT 显式转换 SANDBOX_ROOT 内的来源,包括位于其中的工作区,对其他路径不提供任何映射。workspace mount、bind volume 和 local 项目级 volume 共用该路径解析实现:设置了 DOCKER_HOST_SANDBOX_ROOT 时,它们中位于 SANDBOX_ROOT 之外的来源都必须按上述规则通过 daemon 容器的共享挂载(原生 daemon 则需已验证的本地 Engine)映射,否则会被拒绝。local 项目级 volume 存放在 DATA_ROOT 下、与 SANDBOX_ROOT 并列,因此在发布的 Compose 部署中通过 daemon 的数据挂载映射。未设置 DOCKER_HOST_SANDBOX_ROOT 时,bind volume 和项目级 volume 保留配置宿主路径的行为;workspace mount 额外要求满足上述来源共享保证。
默认的 read_only: false 是实时可写映射:Agent 的写入、删除和重命名会直接修改源目录,使用同一目录的 sandbox 也会看到变化。多个写入者需要自行协调。read_only: true 禁止 guest 通过此映射写入,但宿主对源目录的修改仍然可见,因此不是不可变快照。需要隔离编辑或稳定初始快照时继续使用 copy。
read_only: true 要求 Docker Engine API 至少为 1.44,并且 Engine 使用的 Linux 内核支持递归只读 bind 挂载(至少为 5.12;macOS 上指虚拟机内核)。agent-compose 显式要求递归只读,包括来源中已有的子挂载。API 版本过低或 runtime 无法强制执行时,启动会失败,不会降级成仅部分只读的挂载。详见 Docker 的递归挂载要求。
停止、恢复和删除 sandbox 都会保留外部源目录。恢复使用已持久化的来源和交付设置;修改项目不会把已有复制工作区自动转换为挂载。创建或重新启动时,来源缺失或无效会明确报错。Workspace 来源上传/下载 API 继续管理复制模式的 Workspace 预设,不会借此开放已挂载的项目目录。
此选项控制声明的 Workspace 来源。其中的 .codex 目录遵循工作区模式;sandbox 独立的 ~/.codex 等 Provider home 则存放私有配置、凭据与历史,继续在 sandbox 之间隔离。
自动内容复用
复制模式在 sandbox 私有工作区的 Ready 状态成功持久化后,释放内部 run 输入。等待或失败的准备流程保留重试所需输入;复制模式的工作区预设与外部挂载目录保持不变。文件系统支持时优先使用克隆,否则复制字节,两者都保留可独立修改的文件。旧版本遗留的无标记快照不会被自动删除。
Agent skills 也保持私有可写。准备时比较来源与 sandbox 实际内容,包括可执行权限,未变化的文件保留原位。变化的内容先完整暂存再发布;sandbox 内对技能的修改,会在下次准备时按声明的来源恢复。正确的 Claude skills 链接直接复用。Kubernetes 在传输单份 canonical 内容并维护别名之前检查 guest 内容,避免 daemon 目录未变化却漏掉 guest 内容缺失或被修改的情况。Provider 凭据与历史不会被转换为共享技能缓存。
daemon API 属于管理信任边界。API 客户端可以选择项目来源路径,已有 bind volume 也可以暴露宿主目录。Workspace 的项目根包含校验用于验证来源合同,不是面向不可信 API 客户端的逐项目访问控制。应按受信任管理员的权限配置监听地址和 AGENT_COMPOSE_AUTH_TOKEN。
mcp_servers:项目级 MCP
项目级 MCP 是命名映射,名称须符合稳定标识符格式。支持 local 和 remote 两类。
Local MCP
mcp_servers:
filesystem:
type: local
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
env:
NODE_ENV: production
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
type |
string | 是 | 必须为 local。 |
command |
string | 是 | sandbox 内启动 MCP Server 的命令。 |
args |
string[] | 否 | 命令参数;空项和重复项会在规范化时去除。 |
env |
map | 否 | 进程环境,支持值对象和 ${NAME} 插值。 |
transport |
string | 禁止 | Local MCP 不接受该字段的非空值。 |
url |
string | 禁止 | Local MCP 不接受 URL。 |
headers |
map | 禁止 | Local MCP 不接受 HTTP headers。 |
Remote MCP
mcp_servers:
docs:
type: remote
transport: sse
url: https://mcp.example.com/sse
headers:
Authorization:
value: Bearer ${MCP_TOKEN}
secret: true
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
type |
string | 是 | 必须为 remote。 |
transport |
string | 是 | sse 或 http。 |
url |
string | 是 | 远程 MCP 地址,支持 ${NAME} 插值。 |
headers |
map | 否 | 请求头,支持值对象、secret 和插值。 |
command |
string | 禁止 | Remote MCP 不执行本地命令。 |
args |
string[] | 禁止 | Remote MCP 不接受命令参数。 |
env |
map | 禁止 | Remote MCP 不接受进程环境。 |
默认 guest 中的 Pi provider 仅支持 streamable HTTP 远程 MCP,不支持 legacy SSE
transport;供 Pi 使用的远程 Server 应声明 transport: http。声明 transport: sse
会使 Pi run 以不支持该 transport 的错误失败,而不是静默按 HTTP 处理。
项目级 MCP 不会自动注入所有 Agent。Agent 必须在自己的 mcp_servers 中引用或定义需要的 Server。
octobus_servers:项目级 OctoBus Server
项目可以声明多个具名 OctoBus Server:
octobus_servers:
internal:
url: https://octobus.internal.example
token: ${OCTOBUS_INTERNAL_TOKEN}
public:
url: https://octobus.example
token: ${OCTOBUS_PUBLIC_TOKEN}
| 字段 | 类型 | 必填 | 含义 |
|---|---|---|---|
url |
string | 是 | OctoBus Server 的绝对 http 或 https URL;不允许包含用户信息或 fragment。支持 ${NAME} 插值。 |
token |
string | 否 | daemon 连接该 Server 时使用的 Bearer token。支持 ${NAME} 插值;空值表示不配置鉴权 token。 |
map key 是 Server 名称,必须符合稳定标识符格式。Agent 通过 <server>/<capset> 形式限定现有 capset_ids 条目来选择 Server,例如 internal/code-review。仅声明 Server 不会自动向 Agent 授权。
OctoBus token 天然属于敏感信息。应把它保存在环境变量或 dotenv 配置中,并通过 ${NAME} 引用。daemon 会保留解析后的 token 以代理请求,但会在面向用户的规范化输出中将其脱敏,也不会把它注入 sandbox。不要把字面 token 提交到 compose 文件中。
项目 API 或规范化输出中的脱敏值 ******** 并不是凭据本身。ApplyProject 保持原有的完整替换语义,不会把该 marker 解释为“保留”;compose 和 CLI 的 re-apply 流程仍必须从原始、以环境变量为凭据来源的配置中解析并提交真实 secret。
PatchProject 用于基于已脱敏的 GetProject 结果编辑现有项目。请求必须携带完整目标 ProjectSpec、项目引用和当前 spec hash。在现有 secret 的同一稳定位置提交 ******** 会保留已存值;在新增、移动后或非 secret 的位置使用 marker 会被拒绝。提交真实值会替换 secret,省略集合项则会删除该项。Patch 不能创建或重命名项目,也不能修改 source;当前 hash 过期时返回 ABORTED。CLI 仍使用 ApplyProject,这些 Patch 语义不会改变 CLI 行为。
项目 re-apply 与 MCP Server 沿用相同的 managed agent 配置模型。运行中的 sandbox 保留创建时固化的 capset_ids 授权集合,后续调用则从当前 managed agent definition 解析所引用的 Server。因此,更新 Server URL 或 token 无需重建 sandbox 即可生效。新增 capset 只对新 sandbox 可用;如果旧 sandbox 已授权的 capset 所对应 Server 已无法解析,该调用会失败,不会回退到其他 Server。
volumes:项目级 Volume
volumes:
cache: {}
shared-data:
name: existing-data
driver: local
external: true
labels:
owner: platform
options:
tier: fast
| 字段 | 类型 | 默认值 | 作用 |
|---|---|---|---|
name |
string | 由项目和 key 派生 | 指定底层 Volume 名。 |
driver |
string | local |
Volume driver。local 使用 daemon 所在主机目录;agent 使用 k8s runtime 时,k8s 会创建 Kubernetes PersistentVolumeClaim。 |
external |
bool | false |
为 true 时引用已经存在的 Volume,而不是由项目创建。 |
labels |
map[string]string | 空 | 附加标签。key/value 会去除首尾空白。 |
options |
map[string]string | 空 | Driver 选项。k8s 支持 size(默认 1Gi)、storage_class、access_mode(默认 ReadWriteOnce)和 namespace(默认 K8S_NAMESPACE/default)。PVC 使用 daemon 所在的 Kubernetes 集群。 |
Volume map key 必须符合稳定标识符格式。Agent 通过 agents.<name>.volumes 挂载项目 Volume。
policies:项目级策略
policies 是项目级的策略库。每个条目就是一个策略块,与 Agent 内联声明的块完全相同,Agent 通过写名称来引用:
policies:
locked-down:
stopped_runtime_policy: retain
network:
default: deny
allow:
- host: api.github.com
port: 443
protocol: https
agents:
reviewer:
provider: codex
policy: locked-down
名称在项目规范化时解析,与 Agent 的 mcp_servers 引用一致:每个 Agent 最终都携带自己的有效策略块。policy: locked-down 与把该块原样内联,会规范化成完全相同的配置和相同的 spec hash,因此把共享块搬进 policies 不会改变引擎实际执行的内容。
策略库是目录,Agent 的声明是从中做一次选择,或写自己的块。二者不叠加:
| Agent 声明 | 生效策略 |
|---|---|
policy: locked-down |
就是 locked-down 这一条。 |
policy: {…} 内联 |
就是内联的这个块。 |
不写 policy |
没有策略:出网不受限,停止时 remove。 |
同一个 key 只有一种形状——名称是 scalar,块是 mapping——所以一个 Agent 不可能同时写两者,每个 Agent 恰好只有一个生效策略。库条目不会合并进自行声明块的 Agent,没有任何隐式继承,目前也不存在项目级默认策略。因此"大部分 Agent 用同一条策略、个别例外"不需要组合规则:例外自己写块,其余引用条目即可。合并两个块会产生一个文件里根本没写过的第三种块,读者也无法只读一处声明就知道 sandbox 到底执行什么。
- 项目级策略定义的是块本身,不能再引用另一个策略,因此不存在需要解析的引用链。
- 即使没有任何 Agent 引用,每个条目也会被校验,坏定义会直接让项目失败,而不是等到第一个引用它的 Agent。
- 引用未定义的名称会报错,绝不会退化成"没有策略"——未声明的策略意味着出网不受限制。
- 策略库只是编写便利,不属于规范化配置的一部分:
agent-compose config展示的是每个 Agent 的有效policy块,而不是它引用的名称。
agents.<name>
agents 是以 Agent 名为 key 的映射:
agents:
reviewer:
provider: codex
image: chaitin/agent-compose-guest:latest
支持的字段如下:
| 字段 | 类型 | 默认值 | 作用 |
|---|---|---|---|
enabled |
bool | true |
是否启用 Agent。禁用后定义保留但不可按正常流程运行,Scheduler 也不会启用。 |
display_name |
string | 空 | Agent 的可读显示名称。 |
description |
string | 空 | Agent 职责的可读说明。 |
provider |
string | codex |
Agent CLI/provider:codex、claude、opencode、pi 或 dsh。兼容别名会在持久化边界归一化。 |
model |
string | daemon 默认模型 | 不透明的模型名;省略时使用 daemon 的默认模型;支持 ${NAME} 插值。 |
system_prompt |
string | 空 | 附加的系统提示,适合使用 YAML ` |
image |
string | daemon 默认镜像 | Guest 镜像引用,也会作为 build 的一个输出 tag。 |
build |
string/object | 无 | agent-compose build 使用的镜像构建配置。 |
driver |
object | Docker | 运行时 driver,必须且只能选择一个 key。 |
env |
map | 空 | 注入 sandbox 的环境变量。 |
mcp_servers |
scalar/object/list | 空 | 引用项目级 MCP,或声明 Agent 专属 MCP。 |
capset_ids |
string[] | 空 | 允许该 Agent sandbox 使用的 OctoBus capability set 声明;可用 <server>/<capset> 选择项目 Server。 |
skills |
list | 空 | 注入 Agent 的 Skill 来源。 |
volumes |
list | 空 | Volume 或 bind mount 列表。 |
workspace |
object | 无 | 显式引用一个顶层 workspaces 条目,或定义 Agent 内联 Workspace。 |
policy |
object | 删除已停止 runtime | 运行时策略声明:已停止 runtime 的生命周期与出网策略。 |
sandbox |
object | — | policy 的弃用拼写。仍然可用;同时声明两者会报错。 |
scheduler |
object | 无 | 自动触发 Agent 的 Scheduler。 |
jupyter |
object | disabled | Agent run 的 Jupyter 默认配置。 |
enabled、provider、model 和 system_prompt
agents:
reviewer:
enabled: true
provider: claude
model: ${CLAUDE_MODEL}
system_prompt: |
Focus on correctness, security, and regression risk.
Provider 支持 codex、claude、opencode、pi 和 dsh。当前兼容归一化还接受 claude-code / claude_code、open-code / open_code、pi-agent / pi_agent、deepseek / deepseek-harness / deepseek_harness,新配置建议使用规范名称。
Pi、dsh 和 opencode 是多模型 Agent,建议显式声明其模型:
agents:
reviewer:
provider: pi
model: gpt-5.4
model 始终是不透明的:daemon 不会为了选择上游而拆分它。由哪个 Connection 提供该模型属于 daemon 配置——声明该模型的 Connection、默认模型所属的 Connection,或唯一已配置的 Connection。当多个 Connection 都可能提供同一个模型时,daemon 优先选择该 Agent 能原生使用的协议,从而透传而非转换;协议相同的 Connection 彼此等价,会随机选择其中一个,因此多个上游提供同一模型不会导致运行失败。对 pi、dsh 和 opencode,daemon 会根据选中的 Connection 组合出这些 CLI 需要的 <llm-provider-id>/<model> 引用,因此 compose 文件不需要写该前缀。已废弃的 model: <connection>/<model> 写法在其前缀是指向该剩余部分的 Connection 时会被拒绝。
Agent 级 LLM_API_* 环境是优先级更高的兼容路径:自行发布上游的 Agent 由它提供服务,它注入的模型本身也可能是限定名(例如网关发布 <provider>/<model> 形式的逻辑名),此类值会原样到达上游。
Daemon models.json
daemon 在启动时加载一次 $DATA_ROOT/models.json。文件不存在是合法状态:catalog-owned 条目按不存在处理,已有 system 和环境 Provider 的默认值保持不变。修改文件后需要重启 daemon。
{
"default": "gateway/deepseek-v4-flash",
"providers": {
"gateway": {
"baseUrl": "https://gateway.example.com/api/openai",
"protocol": "chat_completions",
"apiKey": "${GATEWAY_API_KEY}",
"models": [
{
"id": "deepseek-v4-flash",
"maxOutputTokens": 8192
}
]
},
"openai": {
"baseUrl": "https://api.openai.com/v1",
"protocol": "responses",
"apiKey": "$OPENAI_API_KEY"
}
}
}
default 是可选的 provider/model 引用。每个 Provider 都必须提供 baseUrl,并选择 responses、chat_completions 或 anthropic_messages。apiKey 和 Header 值可以是字面量,也可以是完整的 $NAME / ${NAME} 引用;daemon 在启动时从自身环境解析。JSON 非法、未知字段、Protocol 不受支持、Token 上限非法或环境变量引用无法解析都会使 daemon 启动失败。enabled 可选,决定该连接能否服务:缺省时沿用「没有 apiKey 即落库为 disabled」的规则,true 表示该连接不需要凭据(例如本地端点),false 则关闭一个本来有凭据的连接。
可选的 models 数组只补充模型级元数据和行为,包括 id、name、baseUrl、protocol、headers 和正整数 maxOutputTokens;maxOutputTokens 不得超过 2147483647,因为 API 以 32 位整数上报该值。Provider 级和模型级 headers 的名字必须是合法的 HTTP 字段名,否则发送请求时会被 transport 拒绝,因此这类声明会作为非法输入被拒绝。模型级 protocol 必须与 Provider 的协议族兼容:OpenAI Provider(responses 或 chat_completions)只允许 responses 和 chat_completions,Anthropic Provider(anthropic_messages)只允许 anthropic_messages。这些属性属于具体的 Provider/Model 部署,共享同一 Model ID 的 Provider 不会相互覆盖;该数组也不是白名单。只要 gateway Provider 已配置,gateway/a-model-not-listed-here 仍会使用 Provider 默认配置,把右侧 Model ID 原样发送给上游。
所有兼容的 Coding Agent 和 scheduler.llm 使用这份目录完成 agent-compose 的 Provider 路由和模型选择;它不替代 Agent 自身的模型能力目录。在项目 variables 或 Agent env 中声明的第一方凭据会被作为 daemon 自有的连接导入同一份目录并经 facade 代理,因此 key 留在 daemon,sandbox 只拿到本次 run 的 facade token;这既是完整 LLM_API_ENDPOINT、LLM_API_PROTOCOL、LLM_API_KEY 声明的兼容路径,也适用于 variables 和 env 下列出的各厂商变量名。可选的 daemon 或 Agent LLM_API_HEADERS 是该 env Provider 上的静态额外 HTTP Header JSON 对象,它不替代 catalog 的 headers,原始值也不会暴露给 guest runtime。该变量由 env-backed OpenAI 和 Anthropic Provider 共用,因此每个 Header 都必须适合发送给所有已配置的上游。daemon 自身的 LLM_* 配置只是默认模型解析上的启动兜底,而不是优先级更高的来源;连接选择是另一条轴,不按配置来源排序,因此两个以相同协议服务同一模型的连接互为对等,与各自来源无关。默认模型按三步解析:models.json 声明了 default 时它优先;否则通过 LLMService.SetDefaultModel 设置的引用生效并在重启后保留;两者都不存在时才轮到环境声明的默认模型,且该模型必须有已启用连接真正服务;被其他来源接管连接后遗留的标记按「无默认」处理。环境标记为默认、但没有任何已启用连接服务的模型同样按「无默认」处理:未声明模型的 run 会报「没有模型」,而不是被路由到一个从未服务过它的连接。models.json 的 default 必须指向文件自己声明且已启用的连接。指向文件未声明的连接、落库为 disabled 的连接(没有 apiKey 也没有显式 enabled)、或已被其他来源占用的连接时,该引用不会被应用、也不会落库:文件其余声明照常生效,若已存储的引用同样解析不了就一并清除,daemon 在日志中说明丢弃了什么以及原因并继续启动——一个不可用的默认模型不该让整个 daemon(连同 cleanup、scheduler、删除恢复)都起不来。仍然可解析的已存储引用(例如运维通过 LLM 服务选择的默认模型)会被保留。models.json 省略 default 时不会清空已存储的引用,因此运行时选择的默认模型不会被下一次启动抹掉;显式写 "default": "" 或调用 LLMService.ClearDefaultModel 可以清空它。环境投影同样不会抢占其他来源当前正在服务的 ID。ID 只在归属方确实能服务它时才留给对方:Provider ID 在其连接 enabled 期间留给对方——未声明 apiKey 的 models.json 声明会被持久化为禁用,确实不需要凭据时应显式写 "enabled": true;Model ID 则在确有「其他来源拥有的」已启用连接绑定它时留给对方。没有任何连接能服务的 ID 不构成归属,环境会接管它,而不是让位到整个部署一个可用连接都不剩;这也包括只有环境自己物化的连接在服务的 ID——文件投影每次启动都会收回该身份的 scope,而那条连接始终属于环境。接管不会让文件失去自己的声明:文件投影会改写任何 scope 的行,因此下一次启动仍按文件投影该声明,一旦文件能服务它,环境就按常规让位并打出告警。文件不再声明的 Provider 或 Model ID 会被释放:行本身保留(已存储的默认模型引用着它,文件以后也可以重新声明),但会被置为禁用并交出 catalog scope,因此不再持有该 ID,环境随后可以接管。由于文件是 daemon 自己的声明、环境只是兜底,在 models.json 中重新声明该 ID 会收回这一行,随后运行的环境投影按既有规则让位并打出常规告警,而不是让启动失败;环境自行物化的 Provider 或 Model ID 同理。只有属于显式来源的连接不在文件的可改写范围内:models.json 的 Provider ID 与 LLMService 管理的连接或 run 声明的连接冲突时,daemon 会在不覆盖原配置的前提下启动失败。如果没有任何默认模型,daemon 会在启动时打出一条告警,未声明模型的 Agent 继续使用自身鉴权。
在启动时,以及每次 LLMService 写入 Provider 之后,daemon 都会校验每个已启用的连接:先请求该连接的 /v1/models,再按该连接所属协议族可服务的每种协议各发送一个最小 ping 请求(输出上限为 1 token)。每种协议都独立探测,因此同一端点同时支持 responses 和 chat_completions 时会同时报告两者,而不会在第一个成功后停止。探测只做诊断:不改变路由或已存储配置,也不用 /v1/models 判断协议支持,结果只写入日志、不落库。较新的结论会以「endpoint + 凭据」为键在内存中短暂保留,避免重复触发产生相同流量;进程重启后即失效并在启动时重新探测。日志形如 endpoint <url> supports model list: <ids> 和 endpoint <url> supports <protocol> protocol,并对被证明确实不支持的协议、不可达端点、超时以及凭据被拒绝分别给出告警;凭据被拒绝时协议支持情况保持未知。写入 Provider 后的探测同步执行,受 LLM_PROBE_TIMEOUT(默认 15s)约束;启动时的一轮探测在后台执行并自带同一个截止时间,因此不会阻塞启动,也不会延迟其后启动的任何组件;截止时间耗尽后探测即停止并记录还有多少连接未能探测,且被截止时间中断的结论不会写入缓存,因此下一次触发会重新真正探测。关闭时会中止进行中的探测并等待其停止,等待时间受关闭预算约束,而不是受探测超时约束。声明了模型的连接会用其中一个模型探测,且使用该模型实际走到的端点:模型级 baseUrl 才是该模型真实请求的端点,因此「每个模型各自一个端点」的部署按模型端点判定,而不是按连接端点。未声明模型时才会使用端点自身公布的模型。最新结论也会通过 GetProvider 和 ListProviders 的 capabilities 返回:probes 区分「已证实不支持」与「无法判定」,models 是端点公布的模型清单,probedModel / probedAt 给出探测所用模型和时间。daemon 尚未探测该连接时 capabilities 为空,且因为探测结果不落库,重启后会消失。探测默认开启,因此升级后每个已启用上游都会收到这些请求;设置 LLM_UPSTREAM_PROBE=false 可完全关闭探测。
通过 RPC 管理 LLM Provider
agentcompose.v2.LLMService 提供 CreateProvider、GetProvider、
ListProviders、UpdateProvider 和 DeleteProvider。这里管理的是上游模型地址和
API Key,与 Agent 的 provider: codex / provider: pi 无关。
例如,通过 Connect JSON 调用 CreateProvider:
{
"provider": {
"id": "team-gateway",
"baseUrl": "https://gateway.example.com/v1",
"protocol": "responses",
"apiKey": "your-upstream-api-key"
}
}
请求路径为 /agentcompose.v2.LLMService/CreateProvider,使用现有 daemon API
鉴权。随后在 Agent 的 model 中直接写模型名(例如 model-id)即可,无需枚举模型。
Connection 由 daemon 选择:新建 Connection 声明了该模型时由它提供,否则使用默认模型
所属的 Connection 或唯一已配置的 Connection。多个 Connection 提供同一模型不是错误——
daemon 优先选择该 Agent 能原生使用的协议,否则在其中随机选择。不要写
<connection>/<model-id>:该写法已废弃,当前缀对应的 Connection 提供了剩余部分时会
被拒绝。本身包含斜杠的字面上游模型 ID(例如 meta-llama/Llama-3.1-8B-Instruct)
仍会原样转发。其他方法使用同样的服务路径前缀。
id不可修改,支持 1–128 个 ASCII 字母、数字、点、下划线和连字符,首位必须是 字母或数字;default、anthropic以及 session 环境 ID 保留给 daemon。protocol必须为responses、chat_completions或anthropic_messages。baseUrl必须为 HTTP(S) 绝对地址,不能包含用户名密码、查询参数或 fragment。auth指定显式覆盖协议约定的凭据呈现方式:OpenAI 系协议的约定为bearer(Authorization: Bearer),anthropic_messages的约定为x-api-key。 当网关使用 Anthropic Messages 协议但要求Authorization: Bearer认证时,设置"auth": "LLM_PROVIDER_AUTH_BEARER";反向场景设置"auth": "LLM_PROVIDER_AUTH_X_API_KEY"。该覆盖只改变认证 Header, 不改变协议或地址;未知取值会被拒绝。显式选择会跨协议变更保留,即使它与当前协议的 默认值相同。响应返回已存储的选择;未指定时跟随协议约定。apiKey是字面量,不解析环境变量引用。创建必须提供非空值;更新省略时保留旧值, 提供非空值时轮换,空值无效。响应仅返回apiKeySet,不会回显密钥。models可选,用来声明操作者手动填写的模型名,每项包含id、name、protocol、baseUrl、headers和不大于 2147483647 的正整数maxOutputTokens。模型级protocol必须与 Provider 处于 同一协议族。声明的模型名是路由元数据,不是白名单:连接未列出的字面量model仍会原样 转发。GetProvider和ListProviders按模型 ID 顺序返回已声明集合。在UpdateProvider中省略models会保留已声明集合,传空数组则清空。修改protocol时保留 的集合会重新校验:若其中有模型固定了旧协议族,更新会以InvalidArgument失败并保持已存储的 协议与模型不变,需要更换协议族请重新提交models。同名模型按 ID 在多个连接间共享:已被其他 来源占用的模型保留其归属与元数据,新连接只增加自己的绑定。- 创建时,空
name默认使用 ID,省略enabled默认为true。anthropic_messages会默认发送anthropic-version: 2023-06-01,其他协议不加额外 Header。 UpdateProvider使用相同的provider对象。省略name、baseUrl、protocol、apiKey、auth、enabled时保留已存储值。提供非空apiKey会轮换密钥,空值无效。 即使修改protocol,省略auth也会保留原选择。设置"auth": "LLM_PROVIDER_AUTH_UNSPECIFIED"才会清除覆盖并重新跟随协议约定。 没有显式认证选择的旧配置保留原有行为;需要固定认证方式时,显式更新auth。- CLI:
agent-compose llm provider ls|create|inspect|update|rm。创建必须提供--base-url、--protocol、--api-key。更新只发送显式设置的 flag,因此update --base-url ...不会把已禁用的 Provider 重新启用。两个命令都支持--auth x-api-key|bearer覆盖协议默认值,--auth protocol-default清除已存储的覆盖。 CLI 暂不支持设置models或默认模型,请使用 RPC 或 UI。 GetProvider/DeleteProvider请求为{"id":"team-gateway"}。ListProviders接受offset/limit,按 ID 排序并包含禁用项,返回providers和total。- 这些接口只管理
api归属的 Provider,不覆盖models.json或环境配置。 重复 ID 返回AlreadyExists,不存在返回NotFound,归属冲突返回FailedPrecondition。 API 配置持久化到数据库,重启后保留;与models.json同名仍会导致启动冲突。 - 地址和密钥更新在后续模型目标解析时生效,无需重启 daemon;已发出的上游请求不受影响。 切换协议可能需要重新启动 Agent run,以刷新客户端协议配置。 删除会清理 provider 绑定的 facade token,同名重建不会恢复旧 token。
- 删除不会修改项目中的模型引用;新请求中的未知
provider/前缀仍遵循现有的字面量模型 解释规则。禁用保留配置和 token,重新启用后可恢复使用。
这些接口不会通过 models 之外的途径修改模型级配置,也不会改写 models.json。密钥沿用
现有数据库存储方式,未增加应用层加密。
通过 RPC 设置默认模型
GetDefaultModel、SetDefaultModel、ClearDefaultModel 管理 daemon 为「未声明模型的
run」使用的默认模型,与 models.json 的 default 写入的是同一个引用。
SetDefaultModel 的请求为 {"model":{"providerId":"team-gateway","modelId":"model-id"}},
要求 Provider 存在且已启用;模型可以是该连接并未声明的字面量,因为模型 ID 是不透明的。
Provider 不存在返回 NotFound,已禁用返回 FailedPrecondition,providerId 或 modelId
为空返回 InvalidArgument,且都不会改变已存储的引用。由 agent 声明派生出的、属于单次 run 的
连接 ID 会以 InvalidArgument 拒绝:它持有某个 sandbox 自己的凭据,不能成为 daemon 的默认
模型。GetDefaultModel 返回 model,没有默认模型时省略该字段。ClearDefaultModel 移除
引用,此后若环境声明了默认模型则重新由它生效。
这里设置的引用会在重启后保留,以下情况会替换或移除它:models.json 自己声明了
default,下次启动会用文件中的引用覆盖它,并在与已存储的引用不同时打出告警;调用
ClearDefaultModel 主动移除;或它指向的
连接被删除、被禁用——daemon 无法服务的引用不是任何 run 实际拿到的默认模型。因此从
models.json 中删掉 default 并不会回到之前的状态,daemon 最后存储的引用仍然生效,直到
发生上述情况之一。
image
agents:
reviewer:
image: chaitin/agent-compose-guest:latest
运行时会确保所选 driver 能使用该镜像。若同时配置 build,image 也会加入构建 tag;只有 image、build.tags 或 --tag 参数至少提供一个 tag 时,agent-compose build 才会成功。
GitHub CI 会向 Docker Hub 发布以下镜像:
| 镜像 | 用途 | Dockerfile | 平台 |
|---|---|---|---|
chaitin/agent-compose |
控制面 daemon | Dockerfile |
linux/amd64、linux/arm64 |
chaitin/agent-compose-guest |
Sandbox guest runtime | guest-images/Dockerfile.agent-compose-guest |
linux/amd64、linux/arm64 |
chaitin/agent-compose-guest:archlinux |
可选的 Arch Linux sandbox guest runtime | guest-images/Dockerfile.agent-compose-guest-archlinux |
linux/amd64 |
Agent 的 image 可以使用任一 guest 镜像。daemon 镜像用于部署控制面,不能作为 guest 镜像使用。两个 guest 都不绑定某一个 driver;CI 不发布 BoxLite-only、Microsandbox-only 或其他 driver 专用 guest 镜像。
build
短写法只指定 context:
build: ./guest
完整写法:
build:
context: ./guest
dockerfile: Dockerfile
target: runtime
args:
VERSION: "1.2.3"
platforms:
- linux/amd64
tags:
- example/guest:latest
no_cache: false
pull: true
| 字段 | 类型 | 默认值 | 作用 |
|---|---|---|---|
context |
string | . |
Build context;相对路径以 compose 文件目录为基准。 |
dockerfile |
string | Dockerfile |
Dockerfile 路径,由镜像构建后端解释。 |
target |
string | 空 | 多阶段构建 target。 |
args |
map[string]string | 空 | Build args;key 去除首尾空白且不能为空。 |
platforms |
string[] | 空 | 目标平台,格式为 os/arch;当前最多一个。 |
tags |
string[] | 空 | 输出镜像 tag,可与 Agent image 以及 CLI --tag 合并。 |
no_cache |
bool | false |
禁用构建缓存。 |
pull |
bool | false |
构建时拉取较新的基础镜像。 |
当前 agent-compose build 使用 Docker daemon image store。CLI 同名选项可以覆盖或追加 YAML 配置。
driver
省略 driver 时默认为:
driver:
docker: {}
必须且只能选择一个 runtime:
driver:
docker:
host: ""
或:
driver:
boxlite:
kernel: ""
rootfs: ""
或:
driver:
microsandbox:
profile: secure
或:
driver:
k8s:
context: production
namespace: agent-compose
| Driver | 子字段 | 当前状态 |
|---|---|---|
docker |
host |
支持的稳定 driver。host 被解析和保留;daemon 的 Docker 边界仍由部署配置决定。 |
boxlite |
kernel, rootfs |
Linux 构建可编译支持;运行时初始化是惰性的。子字段会去除首尾空白。 |
microsandbox |
profile |
Linux 构建可编译支持;运行时初始化是惰性的。profile 会去除首尾空白。 |
k8s |
context, namespace |
通过 Kubernetes 创建 sandbox Pod。context 选择 kubeconfig context;省略时由 client-go 使用 kubeconfig 当前 context 或集群内配置。namespace 覆盖 K8S_NAMESPACE,最终回退到 default。 |
firecracker |
kernel, rootfs |
仅保留在解析 schema 中;当前规范化会明确报 unsupported runtime driver firecracker,不可使用。 |
k8s driver 通过 kubeconfig 构建 client:设置了显式的 K8S_KUBECONFIG/KUBECONFIG
路径时使用该路径,否则使用 ~/.kube/config 的加载规则;只有两者都解析不到配置时才回退到
集群内配置。Sandbox Pod 通过 K8S_RUNTIME_BASE_URL(会覆盖
AGENT_COMPOSE_RUNTIME_BASE_URL)配置的地址访问 daemon。对外支持的安装入口是
charts/agent-compose Helm Chart:
helm install agent-compose ./charts/agent-compose \
--kube-context prod-cluster \
--namespace team-a \
--create-namespace
Helm release 的 namespace 会作为默认 K8S_NAMESPACE;Chart 也会根据这个
namespace 渲染 daemon Service DNS 回调地址和 ClusterRoleBinding subject。Sandbox
Pod 不会挂载 daemon 的数据 PVC,也不使用 hostPath;provision 好的 workspace 和
provider home 配置通过 Kubernetes Exec 流式传入 Pod。
为完整说明 schema,下面的结构可以被解析器识别,但当前会在规范化阶段失败:
# 当前实现中无效。
driver:
firecracker:
kernel: /path/to/kernel
rootfs: /path/to/rootfs
即使 schema 支持某个 driver,当前二进制也必须编译了该 driver。可通过 agent-compose --json version 的 compiled_drivers 查看;这不代表 KVM、Docker daemon 或运行时制品健康。
env
env:
LOG_LEVEL: debug
API_TOKEN:
value: ${API_TOKEN}
secret: true
这些值进入 Agent sandbox。相同名称的空项会在后续边界归一化;secret: true 控制展示脱敏。
在这里声明的可识别第一方 LLM 凭据是例外——ANTHROPIC_API_KEY、ANTHROPIC_AUTH_TOKEN、OPENAI_API_KEY、CODEX_API_KEY、DEEPSEEK_API_KEY、OPENROUTER_API_KEY、LLM_API_KEY:daemon 会把它们导入自己的 LLM 连接并代理本次 run,sandbox 只会收到 facade token。被吸收的声明会连同端点变量一起留在 daemon——LLM_API_ENDPOINT、LLM_API_PROTOCOL、ANTHROPIC_BASE_URL、ANTHROPIC_API_ENDPOINT、OPENAI_BASE_URL、DEEPSEEK_BASE_URL、OPENROUTER_BASE_URL 都会从 sandbox 环境中移除,并在原处装上 facade 地址。daemon 能识别但无法代理的凭据(AZURE_OPENAI_API_KEY)同样会从 sandbox 环境中移除,并在工程/Agent 视图中与被吸收的凭据一起脱敏。只有无法识别的形似凭据的名字(其它 *_API_KEY、*_AUTH_TOKEN)才会原样下发,sandbox 内任何进程都能读到;项目检查会说明每个声明属于哪种情况。
mcp_servers
引用一个项目级 MCP:
mcp_servers: filesystem
引用多个:
mcp_servers:
- filesystem
- issue-tracker
定义 Agent 专属 MCP:
mcp_servers:
- name: private-tools
type: local
command: private-mcp
args: ["serve"]
内联对象支持 name、type、transport、command、args、env、url 和 headers,其 local/remote 约束与项目级 MCP 相同。内联 MCP 必须提供 name;同一 Agent 内不能重复声明同名内联 MCP。重复引用同一个项目级 MCP 会去重。
capset_ids
capset_ids:
- legacy-capset
- internal/engineering
- public/ticketing
列表会去除空值和重复值。真实 capset ID 必须符合 OctoBus 的 ^[a-zA-Z][a-zA-Z0-9_-]{0,62}$ 规则。legacy-capset 这类未限定值使用 daemon 全局 OctoBus 配置,从而保持已有项目文件的行为。internal/engineering 这类限定值选择 octobus_servers.internal;internal 仅供 agent-compose 选择上游,OctoBus 收到的 capset ID 仍为 engineering。限定值引用未声明 Server、包含多个 / 或真实 capset ID 不合法时会产生配置校验错误。
限定值和未限定值可以混用。新增 octobus_servers 绝不会改变未限定值的路由。完整声明仍是 sandbox 的授权边界:授权 internal/engineering 不会同时授权 engineering 或 public/engineering。
所选 ID 用于注入 capability gateway 环境和能力说明。未限定值缺少 daemon 全局 gateway 配置、Server 不可达或能力说明获取失败时采用 best-effort 行为并产生 warning,sandbox 仍会继续创建。项目级 OctoBus URL 和 token 始终留在 daemon 中,不会进入 guest metadata 或能力说明。
skills
Skill 目录必须包含有效的 SKILL.md。provider 支持 file、http 和 git;同一 Agent 的最终 Skill 名不可重复。ZIP 是内容格式,不是 provider。
provider 为 dsh 的 Agent 只接受更窄的 Skill 名:^[a-z0-9]+(?:-[a-z0-9]+)*$,即小写字母和数字,中间用单个连字符连接。DSH 会静默丢弃不符合该规则的 Skill,因此这类名字在项目校验时即被拒绝。Skill 的 SKILL.md 中的 name 必须是同一个值。
交互式会话使用的是沙箱最近一次准备时发布进去的 Skill。附着到一个已在运行的沙箱时,此后新增、删除或修改的 Skill 不会生效;要等沙箱再次准备(例如启动新的沙箱)后才生效。
本地目录:
skills:
- name: review
provider: file
path: ./skills/review
Git 来源:
skills:
- name: review
provider: git
url: https://github.com/example/skills.git
path: review
ref: v1.0.0
username: ${GIT_USERNAME}
token: ${GIT_TOKEN}
远程 ZIP:
skills:
- name: review
provider: http
url: https://downloads.example.com/review.zip
format: zip
path: review
| 字段 | 类型 | 作用 |
|---|---|---|
name |
string | Skill 名;省略时从 path/URL 推导,最终必须符合稳定标识符格式。 |
provider |
string | 必填,支持 file、http 或 git。 |
url |
string | http 和 git 必填。 |
path |
string | file 的本地路径;Git 或 ZIP 内容内的子目录。相对 file 路径以 compose 文件目录为基准。 |
ref |
string | Git branch、tag 或 commit。 |
format |
string | 可选内容格式,目前只支持 zip;HTTP Skill 必须设置。 |
username |
string | HTTP/Git 用户名,可插值。 |
password |
string | HTTP/Git 密码,只允许完整环境引用 ${NAME}。 |
token |
string | HTTP/Git token,只允许完整环境引用 ${NAME}。 |
git 或 http Skill 也可以来自内网主机,例如内网 GitLab 或制品服务器:允许解析到私网或 loopback 地址,因为该主机由 daemon 自己解析。只有 http 与 https URL 会走网络下载;其他 URL 会按本地来源处理,必须位于允许的来源根目录内。
password 和 token 不允许明文。执行 config 或 up 时,CLI 会从项目 dotenv/进程环境解析完整的 ${NAME} 引用,再把项目提交给 daemon;引用对应的变量缺失时保留引用本身而不是报错,并在 clone 时再解析。面向用户的规范化输出和项目 API 会对解析后的凭据脱敏。远程 ZIP 下载限制为 HTTP(S),可以指向内网主机(允许私网与 loopback 地址),并执行大小、压缩包与内容检查。
Git ref 会在各自业务生命周期中解析:Skill 在 Agent run 时解析,Workspace 在 sandbox provisioning 时解析,Scheduler 来源在 config/up 时解析并保存脚本快照。因此 moving branch 在三处可能得到不同 commit;需要严格一致时,应在 ref 中直接填写 commit SHA。
volumes
短写格式是 source:target[:ro|rw]:
volumes:
- cache:/cache
- ./reports:/workspace/reports:ro
长写格式:
volumes:
- type: volume
source: cache
target: /cache
read_only: false
- type: bind
source: ./reports
target: /workspace/reports
read_only: true
| 字段 | 类型 | 必填 | 作用 |
|---|---|---|---|
type |
string | 否 | volume 或 bind。省略时:source 命中顶层 Volume、是绝对路径或以 . 开头时可推断,否则按 Volume。 |
source |
string | 是 | 顶层 Volume key/有效 Volume 名,或 bind 的 host source。 |
target |
string | 是 | Guest 内绝对路径。 |
read_only |
bool | 否 | 只读挂载,默认 false。 |
同一 Agent 不能将多个条目挂到相同 target。短写使用 : 分隔,因此不适合包含冒号的 source/target,此时应使用长写。
Kubernetes(k8s)driver 不支持本地 bind 挂载。bind 的 source 指向 daemon
所在机器上的路径,而该路径在 Pod 内不可用。请改用 driver: k8s 的顶层命名
Volume(由 PVC 提供);配置校验阶段会直接拒绝 bind 挂载。
workspace:Agent 选择或内联工作区
这里是 Agent 内部的单数 workspace,与顶层复数 workspaces 不是同一个层级。
引用顶层条目:
workspace:
name: source
内联本地 Workspace:
workspace:
provider: file
path: ./src
内联 Git Workspace:
workspace:
provider: git
url: https://github.com/example/project.git
ref: main
target: .
内联 Workspace 支持与顶层定义相同的 mode 和 read_only 字段。继承项目条目时只写 name,交付模式和权限会一起继承。只有名称的引用不能覆盖 mode 或设置 read_only: true;需要不同设置时,另建项目 Workspace,或提供完整内联来源。若 name 与来源字段或 target 同时出现,该对象按内联 Workspace 处理,必须自行提供合法来源,不会与项目条目合并字段。
policy:运行时策略声明
policy 集中声明该 Agent 的 sandbox 在运行时的行为:已停止 runtime 的生命周期,以及出网策略。后续新增的运行时策略应继续放进这个块,而不是新增顶层 agent 字段。
Agent 可以内联声明这个块,也可以按名称引用项目级 policies 条目:
policy: locked-down # 引用 `policies` 中的项目级策略
policy: # 或直接内联策略块
stopped_runtime_policy: retain
下文描述的都是策略块本身的字段,两种写法通用。引用了项目级策略中不存在的名称会报错,绝不会被当成"没有策略"。
sandbox是policy的弃用拼写。 为了兼容重命名之前编写的项目,sandbox仍然可用,两个 key 接受完全相同的字段;但新建或修改项目请使用policy。同一 agent 同时声明两者会直接报错,而不是在两个可能互相矛盾的块之间静默选择。
stopped_runtime_policy:已停止 runtime 生命周期
默认情况下,停止 sandbox 会在确认 stop 后删除 driver runtime 及其私有可写状态,同时保留 sandbox 元数据、日志、workspace 和声明的持久挂载;resume 会创建新的 runtime。若需要重新启动同一个容器、Box 或 microVM sandbox,请显式保留私有 runtime 状态:
policy:
stopped_runtime_policy: retain
stopped_runtime_policy 只接受 retain 或 remove(默认值):
retain保留已停止的 runtime 和私有可写层。Resume 必须使用同一个 runtime;runtime 意外丢失时不会静默重建。remove显式删除已停止的 Docker 容器、BoxLite box/私有磁盘,或 Microsandbox sandbox/私有 qcow2 overlay。Sandbox 元数据、事件、日志、workspace 和声明的持久挂载仍会保留。Resume 会创建新 runtime,因此只存在于私有可写层中的数据会丢失。
Kubernetes driver 不接受 retain:Kubernetes Pod 没有“已停止但仍保留”的状态,停止 sandbox 会删除 Pod。请使用 remove,让 resume 根据镜像和持久挂载重新创建 Pod。若后续调度任务需要复用同一个运行中的 Pod,请使用 sandbox_policy: sticky;sticky 完成后会保持 Pod 运行,它与 stopped-runtime retention 是两种不同的生命周期策略。
有效策略会在 sandbox 创建时生成快照;之后修改项目配置只影响新 sandbox。
Daemon 还会记录每个 runtime 创建时所依据的出网策略。启动 sandbox 时只会确保 runtime 存在;如果 driver 启动的是已存在的 runtime,而它的出网配置与当前生效的声明不一致,daemon 会报告这一情况:当运行中的 sandbox 并不由该声明裁决时给出 warning(其 runtime 未携带出网配置,或由 daemon 无法控制的机制执行),当它已经由该声明裁决时给出一条 information(正向代理会在每次请求时读取声明)。修改声明只能通过 policy.network 中列出的机制影响运行中的 sandbox,报告不会声称超出这一范围。没有策略快照的历史 sandbox 会继续保留 runtime。agent-compose inspect sandbox <sandbox> --json 通过 stopped_runtime_policy、stopped_runtime_state、stopped_runtime_last_error 和 stopped_runtime_released_at 展示生命周期记录。状态含义如下:
retained:当前没有主动释放 runtime;已停止 sandbox 应继续使用保留的 runtime 恢复。release_pending:释放意图已经持久化,但 stop、runtime 删除或 ownership 更新尚未全部确认完成。Daemon 会在中断后重试该状态;resume 也会先完成待处理的释放,再创建新 runtime。released:runtime 删除和 ownership 更新都已完成;resume 会创建新 runtime。
对于 remove,daemon 会先持久化 release_pending。如果生命周期记录中存在晚于最近一次确认 stop 的启动或启动尝试,即使粗粒度 VM 状态是 failed 而不是 running,也会先确认 driver stop;之后才删除 runtime 并把记录标记为 released。这个顺序可避免部分启动的 runtime 被跳过 stop,或未经确认便被破坏性释放。磁盘上的 ownership record 格式属于内部恢复状态,不是稳定的运维接口。
policy.network:出网策略声明
network 用于声明该 Agent 的 sandbox 可以访问什么。它是可选的,且“未声明”不等于拒绝:未声明 policy.network 时,引擎行为与今天完全一致,出网不受限制。default: deny 是因为声明了这个块才生效的策略,不是引擎的新默认值(D3)。
| 字段 | 类型 | 默认值 | 作用 |
|---|---|---|---|
default |
string | allow-all |
没有任何 allow 命中时采用的动作:allow-all 或 deny。 |
allow |
list | 空 | 显式放行项。 |
每个 allow 项:
| 字段 | 类型 | 默认值 | 作用 |
|---|---|---|---|
host |
string | 必填 | 主机或主机模式。各段以 . 分隔,* 只匹配一段:*.example.com 匹配 api.example.com,但不匹配 example.com,也不匹配 a.b.example.com。不支持 IPv6 字面量。 |
port |
int | 必填 | 目标端口,取值 1–65535。 |
protocol |
string | any |
http、https、tcp、udp 或 any。只有 http 与 https 在模型中被标记为可在 L7 检查,any、tcp、udp 属于不透明放行,永远不会被呈现为“已检查”。当前没有实现 L7 中间层,因此引擎今天不会检查任何载荷:这个区分是建模与报告属性,不是过滤器。 |
allowed_ips |
list | 空 | 该条目的 host 允许解析到的 IP 地址或 CIDR 范围(可选)。名字解析出的每个地址都必须落在其中,否则该放行项不适用。裸地址就是它自身的单地址范围(10.0.0.5 即 10.0.0.5/32);条目会被规范化并排序。 |
allowed_ips 的作用是把名字与地址绑定起来。没有它时,对 api.example.com 的放行会跟随该名字解析到的任何地方,因此被污染或被攻陷的解析结果可以把 sandbox 引到声明从未指定的地址。有了它,引擎只有在即将使用的地址落在声明范围内时才建立连接;如果一个名字同时解析出允许与不允许的地址,会被拒绝而不是被择优采用。引擎永不转发的地址——loopback、link-local(含元数据地址 169.254.169.254)、multicast,以及控制面端口 2379、2380、6443、10250、10255——无论 allowed_ips 怎么写都保持被拒绝。
以地址命名的目标(host: 10.0.0.5)只由同一地址匹配,永远不会由模式匹配:通配符放行项无法到达声明未点名的地址。
policy:
network:
default: deny
allow:
- host: api.github.com
port: 443
protocol: https
allowed_ips:
- 140.82.112.0/20
- host: "*.internal.example.com"
port: 8080
protocol: tcp
- host: 203.0.113.7
port: 443
protocol: https
主机名会被转为小写,重复项会被拒绝,放行项会被排序,因此规范化后的声明进入 canonical JSON 与 spec hash,且与声明顺序无关。声明 network 会改变 hash;不声明则与引入该字段之前的 spec 逐字节一致。
allow 列表只有在配合 default: deny 时才能限制流量。在默认的 allow-all 下,这些条目仍会被校验、存储并参与 hash,但不会拒绝任何流量;因此只写 allow 列表而不写 default: deny 时,出网仍然不受限制。
当 default: deny 生效时,引擎会自动放行自身的端点:runtime LLM facade 与 guest 上报使用的遥测端点。这些是引擎侧条目,不是用户声明:项目无法声明、删除或覆盖它们;放行范围就是引擎交给 guest 的那个精确端点;每一次放行都会记录在引擎规则(engine.llm-facade、engine.telemetry)名下,使这条例外可见而非隐藏。
该声明会被校验、进入 hash、经 API 往返,并编译进引擎统一的出网判定模型。它是否会限制流量取决于 runtime driver;driver 的能力报告会如实说明它实际执行的强度,而不会把部分机制呈现为完整机制:
- 在 Microsandbox 上,SDK 的网络配置会应用该声明:
allow项成为有序放行规则,在default: deny下,任何既不命中引擎规则也不命中allow项的流量都会被拒绝。其能力报告中带有strength=allowlist以及仍然存在的限制,并注明真实 KVM 验证的结果:拒绝默认值与放行规则成立(未放行的名称无法解析,地址放行只对该地址和端口生效),allowed_ips约束不会被应用,被放行的 HTTPS 目标未能完成 TLS 握手,因此当沙箱能访问守护进程时由引擎的正向代理承载 HTTPS 放行。 - 对于任何 sandbox 能访问到 daemon 的 driver(包括 Docker、Kubernetes 与 BoxLite),引擎会在自己的正向代理中执行该声明。sandbox 会拿到代理作为它的出网路径(
HTTP_PROXY、HTTPS_PROXY、NO_PROXY及其小写形式,项目无法覆盖),代理会认证该 sandbox,并对每个连接做判定:命中allow条目的连接被建立隧道,其余一律以403回复并附带拒绝它的规则。因此在 Docker 上,即使 driver 自身不安装任何过滤器,声明也具有真实强度。 - 无论声明怎么写,代理都拒绝引擎永不转发的目标:loopback、link-local(含
169.254.169.254元数据地址)、unspecified、multicast 地址,以及集群暴露的控制面端口——2379/2380(etcd)、6443(API server)、10250/10255(kubelet)。每条拒绝都记录在各自的引擎规则名下(engine.blocked-address、engine.blocked-port),并写明它拒绝的角色。对default: deny的 sandbox 来说,这堵住了它本来能自己找到的一条路;对任何 sandbox 来说,这是防止代理变成通往 daemon 自身服务或承载它的平台的新通道。 - 代理自行解析目标,并连接它做过判定的那个地址,因此名字无法在判定与建连之间被解析成别的东西。
- 限制是明说的,而不是暗示的。 忽略代理环境变量的进程仍会直连外网、不被判定,直到透明捕获把所有连接都纳入代理;
udp放行项无法由正向代理承载;IPv6 目标会被拒绝,因为规范化端点标识无法表达它。 - 当 sandbox 没有任何可达的代理地址时(例如 daemon 在 sandbox 内部不可达的部署),声明无法被执行,此时会报告该声明未被执行,sandbox 照常运行——平台无法交付的限制不应该变成可用性故障。daemon 会记录一条
declared network policy is not enforced日志,包含 sandbox、driver 与原因;能力报告也写明同样的限制,因此未执行的声明不会被呈现为一项控制。设置EGRESS_UNENFORCEABLE_POLICY=refuse可在这种情况下改为拒绝 sandbox;此时拒绝发生在创建 sandbox 时(一次运行中或一个 scheduler tick 中),因此要求约束必须为真的部署应把项目视为非法,而不是依赖这条报错。 - 默认值为
allow-all的声明(包括只写allow列表而不写default: deny)不会限制流量,不会被注入代理环境变量,也不会导致 sandbox 启动失败。
准入阶段会报告这些信息,你不必自行推断。config 会把声明的策略(各条 allow、是否可检视、以及声明的地址范围)打印到 stderr,同时保持 stdout 上的规范化 spec 不变。up 与 apply 则从 daemon 自身的视角回答准入阶段真正重要的问题——每条声明最终由哪种机制执行:
Network policy
worker (driver docker)
declared default deny, 1 allowance(s)
- api.github.com:443/https (inspectable, addresses 203.0.113.0/24)
enforced forward_proxy (allowlist), the engine's forward proxy
引擎不会执行的声明会打印为 enforced no:,并给出原因以及本部署的处理方式(EGRESS_UNENFORCEABLE_POLICY=warn 或 refuse),同样的文本会作为 warning 写入 stderr。存在同一缺口的多个 agent 共用一条 warning。未声明策略的 agent 不会出现在报告中——未声明不等于拒绝,因此没有需要报告的内容。agent-compose up --json 会在 network_policies 下给出同样的信息。
声明何时生效。 取决于由哪个机制执行它:
- 引擎的正向代理在每个请求上重新读取声明,因此在被代理的 sandbox 上,一次修改立即生效,无需重建或重启任何东西。凭据在 sandbox 创建时签发一次;凭据背后的策略每次都会重读。
- 由 driver 自身应用网络配置的运行时(Microsandbox)在创建 sandbox 时读取声明:修改之后新建的 sandbox 使用新声明,而仅被 resume 的既有 sandbox 保持它创建时的配置。默认的
stopped_runtime_policy: remove会在停止时丢弃 runtime,因此下一次启动会重建并读到新声明;retain的 runtime 不会。 - 在没有可达代理、且当前声明为
default: deny的 sandbox 上,未执行的报告(或EGRESS_UNENFORCEABLE_POLICY=refuse的拒绝)会在下一次启动时生效。
stopped_runtime_policy 的时机不同:它在 sandbox 创建时被快照到 sandbox 上,因此修改它只影响新建的 sandbox。
scheduler
Scheduler 可以使用声明式 triggers,也可以使用 JavaScript script;两者互斥。
| 字段 | 类型 | 默认值 | 作用 |
|---|---|---|---|
enabled |
bool | true |
是否启用该 Agent 的 Scheduler。禁用 Agent 也会使其 Scheduler 无效。 |
sandbox_policy |
string | new |
Scheduler 默认 sandbox 策略:new 或 sticky。 |
concurrency_policy |
string | skip |
整个 Agent Scheduler 的重叠运行策略:skip 或 parallel。 |
run_timeout |
duration | 空 | 单次 scheduler run 的最大时长。为空时继承 SCHEDULER_RUN_TIMEOUT;支持 30m、2h 等 Go duration。 |
model |
string | Agent 的模型 | scheduler.llm 默认使用的 provider/model;调用参数中的 model 或 LLM_MODEL 优先。 |
triggers |
list | 空 | 声明式触发器。 |
script |
string/object | 空 | 内联 JavaScript,或扁平的 file/http/git 来源配置。不能和 triggers 同时使用。 |
new 为每次调用创建新 sandbox;sticky 允许 Scheduler 绑定并复用 sandbox。单个 Trigger 的 sandbox_policy 可覆盖执行 Agent 时的策略。
concurrency_policy 作用于整个 Agent Scheduler,包括全部声明式或脚本注册的 Trigger,以及手动 Scheduler 调用。skip 会把与同一 Scheduler 既有运行重叠的新 run 记录为 skipped,且不会排队补跑;parallel 允许重叠 run 并行执行。它不是 Trigger 级策略。
脚本型 scheduler 可以用 scheduler.llm.async 和 scheduler.agent.async 并行发起 host 调用。两个 scheduler env 用于限制同时进行的数量:
| Env | 默认值 | 说明 |
|---|---|---|
LLM_MAX_CONCURRENCY |
8 |
同时进行的 scheduler.llm.async 调用数。 |
AGENT_MAX_CONCURRENCY |
3 |
同时进行的 scheduler.agent.async 运行数。 |
agent 的上限明显更低:每个并行 agent 运行都会创建独立 sandbox,而 sandbox 创建本身没有数量限制,且每次运行都要经由 SQLite 连接池写入(SQLITE_MAX_OPEN_CONNS,默认 4 条)。非正整数会被忽略并回退到默认值。scheduler.agent.async 始终使用 new sandbox 策略:并行 agent 无法共享同一个 sandbox。
声明式 Trigger
每个 Trigger 必须且只能写一个 kind 字段:cron、interval、timeout 或 event。
scheduler:
enabled: true
sandbox_policy: sticky
concurrency_policy: parallel
triggers:
- name: nightly
cron: "0 2 * * *"
timezone: Asia/Shanghai
prompt: Run the nightly review.
- name: heartbeat
interval: 30m
prompt: Check service health.
- name: startup-once
timeout: 15s
prompt: Perform the startup check.
sandbox_policy: new
- name: webhook-review
event:
topic: webhook.github.push
prompt: Review the pushed changes.
| 字段 | 类型 | 必填 | 作用 |
|---|---|---|---|
name |
string | 否 | 可读的稳定名称;同一 Scheduler 中非空名称不可重复。省略时 ID 会结合列表位置稳定派生。 |
cron |
string | 四选一 | 5 字段 cron,支持可选秒字段和 robfig/cron descriptor。默认使用 daemon 的本地时区。 |
timezone |
string | 否 | cron trigger 使用的 IANA 时区,例如 UTC 或 Asia/Shanghai。Local 或省略该字段时使用 daemon 本地时区;其他 trigger 类型不能使用。 |
interval |
duration | 四选一 | 周期,例如 30s、5m、2h;必须大于 0,实际注册精度至少 1ms。 |
timeout |
duration | 四选一 | 一次性延迟,例如 15s;必须大于 0,实际注册精度至少 1ms。 |
event.topic |
string | 四选一 | 内层 topic 是订阅的非空 topic,可使用如 webhook.github.push。 |
prompt |
string | 否 | 触发后发送给 Agent 的 prompt;空值默认为 Run agent <name>.。 |
include_event |
bool | 否 | 仅用于 event trigger。默认 true,把触发事件追加到 prompt 之后;false 时只发送声明的 prompt。 |
sandbox_policy |
string | 否 | 本次 Agent 调用使用 sticky 或 new;省略时不在生成的调用中显式覆盖。 |
Daemon 本地时区优先取 TZ,未设置时取操作系统的 /etc/localtime。项目提供的 Docker Compose 会以只读方式挂载宿主机 /etc/localtime;仅当 daemon 需要有意使用不同于宿主机的时区时,才在 .env 中设置 TZ。修改时区后需要重启 daemon。持久化时间戳仍统一使用 UTC。
Prompt 中的事件内容
event trigger 会把触发事件追加到 prompt 之后,让 Agent 知道要处理哪个对象。webhook 事件的 payload 是投递记录,请求体在 body 字段中:
Review the pushed changes.
<trigger-event topic="webhook.github.push">
{"body":{"ref":"refs/heads/main",...},"eventId":"...","headers":{"x-github-event":"push",...},"method":"POST","path":"/api/webhooks/...","topic":"webhook.github.push",...}
</trigger-event>
块内是事件 payload 的紧凑 JSON。通过事件总线投递的事件与携带相同 payload 的手动 StartSchedulerRun 生成相同的块:总线信封(topic、createdAt、payload)会被拆开,topic 取实际发布的 topic;手动启动的原始 payload 则搭配声明的 event.topic。只有恰好包含 topic、createdAt(字符串)和 payload 三个键的 JSON 对象才会被当作信封,因此用已保存运行的 payload_json 手动重跑时,渲染结果与原始投递一致。没有 payload、或 payload 为 null 或空对象的运行按声明的 prompt 原样发送。
topic 会按属性值转义,payload 中的 </trigger-event 会写成 <\/trigger-event(解码后是同一个 JSON 值),两者都无法提前结束这个块。块内容最多 65,536 个 UTF-16 码元;超出部分被截掉,此时块内不再是合法 JSON,标签上增加 truncated="true" 和 original-length="<n>"。
在 trigger 上设置 include_event: false 可只发送声明的 prompt、不附加这个块。如果 Agent 需要其他形式的事件内容,请改用 inline scheduler 脚本。
事件投递范围
事件能到达哪些 Project 由 daemon 运维方决定,而不是由 Project 决定。daemon 环境变量 EVENT_DELIVERY_SCOPE 接受两个值:
| 取值 | 行为 |
|---|---|
project(默认) |
某个 Project 发布的事件只投递给同一 Project 中的 event trigger。 |
daemon |
某个 Project 发布的事件投递给 daemon 上所有 Project 中匹配的 event trigger。 |
投递范围作用于 scheduler 脚本通过 scheduler.event.publish 发布的主题(runtime.*、workflow.*、external.*),以及 Engine 代表某个 Project 发出的系统主题(agent-compose.session.*、agent-compose.sandbox.*、agent-compose.agent.completed)。Webhook 主题不是由 Project 发布的,而是由运维方配置的 webhook source 接入,在两种范围下都会投递给所有 Project 中匹配的 trigger。
Webhook 的例外成立的前提是只有运维方能配置 webhook source。只有设置了 AGENT_COMPOSE_AUTH_TOKEN,webhook source 的管理接口才受保护;否则任何能访问 daemon API 的人都能添加 source 并注入 webhook 事件。即使设置了令牌,webhook 事件也是共享的:订阅了 webhook.* 或 * 的每个 Project 都会收到所有 webhook 的内容。由互不信任的使用者共用的 daemon 必须设置 AGENT_COMPOSE_AUTH_TOKEN,并且不应通过 webhook 传递租户私有的数据。
agent-compose.yml 和 scheduler 脚本都不能修改投递范围,主题名也不代表任何权限:在 project 范围下,订阅 * 仍然只会收到本 Project 的事件。控制面可以通过 SettingsService.GetEventDeliveryScope 查询当前生效的范围。单一使用者部署、并且依赖跨 Project 事件的 daemon,应设置 EVENT_DELIVERY_SCOPE=daemon。
内联脚本
scheduler:
enabled: true
script: |
scheduler.interval("review", async function () {
return scheduler.agent("Review the workspace.");
}, 60000);
脚本由 Scheduler runtime 验证,并从脚本注册结果获取触发器。
外部脚本
本地文件:
scheduler:
enabled: true
script:
provider: file
path: ./scheduler.js
HTTP URL:
scheduler:
enabled: true
script:
provider: http
url: https://example.com/scheduler.js
外部脚本 mapping 使用与 Skill、Workspace 相同的来源字段:
- File:
provider: file配合path,相对路径以 compose 文件目录为基准。 - HTTP:
provider: http配合url,可选认证字段。 - Git:
provider: git配合url、可选ref和必填的仓库内path。
应用项目时 CLI 会读取脚本并将内容快照保存到项目规范,而不是让 daemon 以后重新读取来源。HTTP 读取限制包括 10 秒超时、最大 1 MiB、最多 5 次 redirect、UTF-8 校验;HTTPS 不允许降级 redirect 到 HTTP,URL userinfo 不允许使用。
jupyter
jupyter:
enabled: true
guest_port: 8888
| 字段 | 类型 | 默认值 | 作用 |
|---|---|---|---|
enabled |
bool | false |
Agent run 未用 CLI 显式覆盖时,是否启用 Jupyter。 |
guest_port |
int | daemon JUPYTER_GUEST_PORT |
Guest 内监听端口。0 使用 daemon 默认;显式值必须在 1–65535。 |
设置 guest_port 但保持 enabled: false 会保留端口配置,但默认不启动 Jupyter。CLI run --jupyter 可以为单次运行启用。
常见错误与迁移提示
升级时存在同名 project
升级不会归并已有的同名 project,因为名称和 compose 路径相同并不能证明它们的历史数据等价。一个 project 保留原名:优先 active project,其次按最近更新时间排序,最后使用创建时间和完整 ID 做确定性排序。其余 project 依次改为下一个可用的 <name>-N,并跳过已经存在的数字后缀。
每个 project 的 ID 以及 revision、agent、scheduler、run、sandbox 和 volume 关联仍属于原 project。升级后使用 agent-compose project ls 查看分配后的名称;后续如需更新带后缀的 project,应在对应 compose 文件中设置该名称。
顶层误写 workspace
错误:
workspace:
provider: file
path: .
正确:
workspaces:
source:
provider: file
path: .
agents:
reviewer:
workspace:
name: source
同时配置 Scheduler script 和 triggers
二者互斥。把所有注册逻辑放入 script,或完全使用声明式 triggers。
以为 variables 会自动进入 sandbox
variables 不会继承到 Agent。需要显式写:
variables:
API_URL: https://api.example.com
agents:
reviewer:
env:
API_URL: https://api.example.com
如果目标是复用部署环境值,可在两个位置都写 ${API_URL},并通过 env_file 或 CLI 进程环境提供。
以为项目 Workspace 会被自动选择
顶层 workspaces 不会被自动选择。Agent 省略 workspace 时,即使项目只定义了一个条目,也不会配置 Workspace。需要使用时应设置 workspace.name 或内联 Workspace;不使用时应省略该 key,而不是写 workspace: {}。
选择未编译或运行条件不满足的 driver
配置 schema 通过不代表 runtime 一定可启动。BoxLite/Microsandbox 只在 Linux 完整构建中编译,并依赖 KVM 和对应运行时制品;Docker 需要可达的 Docker daemon。
最小配置示例
name: docker-minimal
agents:
reviewer:
provider: codex
image: chaitin/agent-compose-guest:latest
driver:
docker: {}
验证并应用:
agent-compose config --quiet
agent-compose up
agent-compose run reviewer --prompt "Review this project."