agent-compose English GitHub

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

通用规则

严格解析

环境变量来源和优先级

env_file 决定 ${NAME} 插值时可读取哪些 dotenv 文件。加载顺序如下:

  1. 按 env_file 列表顺序加载,后面的文件覆盖前面的同名值。
  2. 运行 agent-compose CLI 的进程环境最后覆盖 dotenv 文件。

若未配置 env_file,CLI 先查配置文件所在目录的 .env,不存在时再查当前工作目录的 .env。显式写 env_file: [] 表示不自动加载任何 dotenv 文件。显式文件不存在、不可读或列表中有空路径都会失败。

${NAME} 只支持简单形式,不支持 shell 的 ${NAME:-default}、命令替换或递归展开。引用的变量不存在时配置校验失败。

当前支持插值的位置包括:

其他字符串字段不会自动插值,例如 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

行为说明:

Workspace 选择规则:

挂载本地 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 到底执行什么。

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) 仍会原样转发。其他方法使用同样的服务路径前缀。

这些接口不会通过 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(默认值):

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 展示生命周期记录。状态含义如下:

对于 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 的能力报告会如实说明它实际执行的强度,而不会把部分机制呈现为完整机制:

准入阶段会报告这些信息,你不必自行推断。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 下给出同样的信息。

声明何时生效。 取决于由哪个机制执行它:

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 相同的来源字段:

应用项目时 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."