Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ JUPYTER_PROXY_BASE=/jupyter
RUNTIME_DRIVER=docker
DEFAULT_IMAGE=ghcr.io/chaitin/agent-compose-guest:latest

# Internal sandbox port-mapping addresses. Leave both unset for a daemon binary
# or a daemon container using host networking; agent-compose then uses the
# Docker default bridge gateway for both. A bridge-network daemon commonly uses
# the physical-host bridge gateway for Docker and its own container address for
# BoxLite/Microsandbox, for example:
# NETWORK_DOCKER_PUBLISH_ADDRESS=172.23.0.1
# NETWORK_RUNTIME_PUBLISH_ADDRESS=172.23.0.2

# ---------------------------------------------------------------------------
# Runtime internals (advanced)
# Change these only when your deployment environment requires custom runtime
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Chinese documentation is available at [zh-CN/README.md](zh-CN/README.md).

- [Project overview and quick start](../README.md)
- [Command line manual](command-line-manual.md)
- [Sandbox networks](sandbox-networks.md)
- [Configuration example](../.env.example)
- [Loader script API](../examples/scheduler-script/README.md)
- [Security policy](../SECURITY.md)
Expand Down
98 changes: 98 additions & 0 deletions docs/sandbox-networks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Sandbox networks

Agent-compose connects sandboxes through TCP port mappings. Docker, BoxLite,
and Microsandbox consume the same normalized bindings, while each runtime uses
its own port-publishing implementation.

## Project configuration

Declare logical networks at the project level and attach agents by name:

```yaml
name: network-demo

networks:
frontend: {}
backend:
driver: port_mapping

agents:
api:
networks: [frontend, backend]
expose:
- "8080"
- target: 9090
host_port: 19090
protocol: tcp

worker:
networks: [backend]

dashboard:
ports:
- "127.0.0.1:18080:8080"
```

`port_mapping` is currently the only supported network driver. When internal
networking is enabled, an agent without an explicit attachment joins the
implicit `default` network.

## `expose`

`expose` creates an internal listener that forwards to a service port inside
the target sandbox.

- `"8080"` forwards to sandbox port 8080 and allocates the listener port
dynamically.
- The long form accepts `target`, optional `host_port`, and `protocol`.
- `host_port` fixes the listener port. A bind conflict fails sandbox startup;
agent-compose does not silently select another port.
- Only TCP is currently supported.

The listener address is selected by the target runtime. It is not configured
per agent.

## `ports`

`ports` publishes a sandbox service with an explicit host address. Supported
short forms are:

```yaml
ports:
- "8080" # dynamic host port -> sandbox 8080
- "18080:8080" # 127.0.0.1:18080 -> sandbox 8080
- "0.0.0.0:18080:8080" # all host interfaces -> sandbox 8080
```

The default host IP is `127.0.0.1`. Host addresses must currently be IPv4, and
only TCP is supported.

## Publish addresses

Internal `expose` listeners use two optional daemon settings:

```text
NETWORK_DOCKER_PUBLISH_ADDRESS
NETWORK_RUNTIME_PUBLISH_ADDRESS
```

Docker targets use the Docker address. BoxLite and Microsandbox targets use
the runtime address. If an address is omitted, agent-compose reads the IPv4
gateway of Docker's default `bridge` network.

A daemon running as a native process or in host network mode usually needs no
override. A daemon running in a regular Docker bridge network may need both
addresses configured so every source sandbox can route to the resulting
listener.

## Current boundary

Named networks record logical membership and annotate internal bindings. The
`port_mapping` driver does not create a Docker network, install firewall rules,
provide DNS names, or transparently rewrite `api:8080`. Sandboxes on different
logical networks may therefore still be able to reach the same underlying
listener.

Applications use the actual `host_ip:host_port` recorded in sandbox network
state. Service discovery and access-policy enforcement are outside the current
network driver.
3 changes: 2 additions & 1 deletion docs/zh-CN/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ agents:
prompt: "Review the current project state and summarize changes."
```

完整字段说明见[命令行使用手册](command-line-manual.md)。
完整字段说明见[命令行使用手册](command-line-manual.md)。Sandbox 互通配置见
[Sandbox 网络](sandbox-networks.md)。

## CLI 概览

Expand Down
88 changes: 88 additions & 0 deletions docs/zh-CN/sandbox-networks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Sandbox 网络

Agent-compose 通过 TCP 端口映射连接 sandbox。Docker、BoxLite 和
Microsandbox 消费相同的标准化 binding,再由各 runtime 使用自己的端口发布实现。

## Project 配置

在 project 顶层声明逻辑 network,并按名称把 agent 加入 network:

```yaml
name: network-demo

networks:
frontend: {}
backend:
driver: port_mapping

agents:
api:
networks: [frontend, backend]
expose:
- "8080"
- target: 9090
host_port: 19090
protocol: tcp

worker:
networks: [backend]

dashboard:
ports:
- "127.0.0.1:18080:8080"
```

当前只支持 `port_mapping` network driver。启用内部网络后,没有显式声明
network 的 agent 会加入隐式的 `default` network。

## `expose`

`expose` 创建内部 listener,并把连接转发到目标 sandbox 内的服务端口。

- `"8080"` 转发到 sandbox 的 8080 端口,listener 端口由系统动态分配。
- 长写法支持 `target`、可选的 `host_port` 和 `protocol`。
- `host_port` 用来固定 listener 端口。发生端口冲突时 sandbox 启动失败,
agent-compose 不会悄悄改用其他端口。
- 当前只支持 TCP。

Listener 地址由目标 runtime 选择,不需要在每个 agent 中重复配置。

## `ports`

`ports` 使用明确的 host 地址发布 sandbox 服务。支持以下短写法:

```yaml
ports:
- "8080" # 动态 host 端口 -> sandbox 8080
- "18080:8080" # 127.0.0.1:18080 -> sandbox 8080
- "0.0.0.0:18080:8080" # 所有 host 接口 -> sandbox 8080
```

默认 host IP 是 `127.0.0.1`。当前 host 地址只支持 IPv4,协议只支持 TCP。

## 发布地址

内部 `expose` listener 使用两个可选的 daemon 配置:

```text
NETWORK_DOCKER_PUBLISH_ADDRESS
NETWORK_RUNTIME_PUBLISH_ADDRESS
```

Docker target 使用 Docker 地址;BoxLite 和 Microsandbox target 使用 runtime
地址。没有配置某个地址时,agent-compose 会读取 Docker 默认 `bridge` network
的 IPv4 gateway。

Daemon 作为宿主机进程运行或使用 host network 时通常无需覆盖默认值。Daemon
运行在普通 Docker bridge network 中时,可能需要显式配置两个地址,确保所有
source sandbox 都能路由到生成的 listener。

## 当前能力边界

命名 network 用于记录逻辑归属并标注内部 binding。`port_mapping` driver 不创建
Docker network、不安装防火墙规则、不提供 DNS 名称,也不会透明改写
`api:8080`。因此,属于不同逻辑 network 的 sandbox 在底层仍可能访问同一个
listener。

应用通过 sandbox network state 中记录的实际 `host_ip:host_port` 访问服务。
服务发现和访问策略强制执行不属于当前 network driver 的能力。
73 changes: 73 additions & 0 deletions pkg/agentcompose/adapters/network_docker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package adapters

import (
"context"
"fmt"
"net/netip"
"strings"

networkapi "github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
)

type dockerNetworkAPI interface {
NetworkInspect(context.Context, string, networkapi.InspectOptions) (networkapi.Inspect, error)
Close() error
}

type dockerNetworkClientFactory func() (dockerNetworkAPI, error)

type DockerPublishAddressProvider struct {
client dockerNetworkClientFactory
}

func NewDockerPublishAddressProvider() *DockerPublishAddressProvider {
return &DockerPublishAddressProvider{client: newDockerNetworkAPI}
}

func newDockerNetworkAPI() (dockerNetworkAPI, error) {
dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return nil, fmt.Errorf("connect docker daemon: %w", err)
}
return dockerClient, nil
}

func (p *DockerPublishAddressProvider) DefaultPublishAddress(ctx context.Context) (string, error) {
dockerClient, err := p.openClient()
if err != nil {
return "", err
}
defer func() { _ = dockerClient.Close() }()
network, err := dockerClient.NetworkInspect(ctx, "bridge", networkapi.InspectOptions{})
if err != nil {
return "", fmt.Errorf("inspect Docker default bridge network: %w", err)
}
return ipv4Gateway(network, "Docker default bridge")
}

func (p *DockerPublishAddressProvider) openClient() (dockerNetworkAPI, error) {
if p == nil || p.client == nil {
return nil, fmt.Errorf("docker network client is required")
}
return p.client()
}

func ipv4Gateway(network networkapi.Inspect, description string) (string, error) {
var gateway string
for _, config := range network.IPAM.Config {
candidate := strings.TrimSpace(config.Gateway)
address, err := netip.ParseAddr(candidate)
if err != nil || !address.Is4() {
continue
}
if gateway != "" {
return "", fmt.Errorf("%s network has multiple IPv4 gateways", description)
}
gateway = address.String()
}
if gateway == "" {
return "", fmt.Errorf("%s network has no IPv4 gateway", description)
}
return gateway, nil
}
Loading
Loading