> ## Documentation Index
> Fetch the complete documentation index at: https://niceeval.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 让 Sandbox 使用 Docker

> 根据任务信任边界选择 Docker socket、raw privileged DinD 或受管 rootless DinD，并为 Agent 准备 Docker CLI 与 daemon。

当评估任务要求 Agent执行 `docker build`、`docker run`或 `docker compose`时，先按任务的信任边界
选择 Docker access：

| 模式                    | 什么时候用                                      | 启动成本                      | 安全边界                                                           |
| --------------------- | ------------------------------------------ | ------------------------- | -------------------------------------------------------------- |
| Docker socket         | 可信 Agent、个人开发机、已有 daemon，优先启动速度            | 最低，共用已有 daemon            | Agent拥有该 daemon的完整控制权。rootful socket通常等价宿主 root                |
| Raw privileged DinD   | 一次性 VM或专用 runner，需要独立 image、network和 cache | 每个 Sandbox启动 inner daemon | outer container是 raw privileged，不是安全隔离方案                       |
| Managed rootless DinD | 不可信 Agent、共享宿主、并发评估、需要资源准入和强杀恢复            | 最高，需要宿主 profile           | privileged限制在受管 rootless user namespace或专用 VM，并有 watchdog与容量契约 |

三种模式都要求镜像预装 Docker CLI。两种 DinD模式只接受从官方
`docker:<version>-dind`派生的兼容镜像。镜像需要 Docker daemon、Node和评估工具，
但不需要自定义 NiceEval `ENTRYPOINT`。NiceEval负责启动与监督 inner daemon、
Sandbox保活、容器内 TTL和 `docker info`探活，不会向任意镜像动态安装 Docker。

不能直接把未经派生的 `docker:<version>-dind`写成 `source.image`。该镜像没有 NiceEval supervisor
需要的 Node，也没有被测 Agent的工具。创建时会以
`dind-image-incompatible: missing node`失败。请使用下面的 Dockerfile，或先发布等价的派生镜像，
再让 `source`引用它。

## 方案一：直接挂 Docker socket

先准备只有 CLI、Node和评估工具的镜像：

```dockerfile title="sandbox/Dockerfile" theme={null}
FROM docker:29-cli

RUN apk add --no-cache ca-certificates git nodejs npm python3 \
  && addgroup -g 1000 node \
  && adduser -D -u 1000 -G node node
```

在 Experiment里显式填写宿主 Unix socket。NiceEval不会从进程环境或 Docker context猜这个路径：

```ts title="experiments/docker-socket.ts" theme={null}
import { defineExperiment } from "niceeval";
import { codexAgent } from "niceeval/adapter";
import { dockerSandbox } from "niceeval/sandbox";

export default defineExperiment({
  agent: codexAgent(),
  model: "gpt-5.4",
  sandbox: dockerSandbox({
    source: {
      type: "dockerfile",
      context: new URL("../sandbox/", import.meta.url),
    },
    user: "node",
    dockerAccess: {
      mode: "socket",
      socketPath: "/var/run/docker.sock",
    },
  }),
});
```

NiceEval会解析 symlink、确认最终目标是 Unix socket，把它挂到 Sandbox内固定的
`/var/run/docker.sock`，并按 socket的数值 GID给 `node`添加补充组。启动 Agent前，NiceEval会确认
镜像没有预设 Docker endpoint或 context，而且默认 `docker info`与显式访问这个 Unix socket得到同一个
daemon。CLI缺失、用户无法访问 socket或镜像把默认 endpoint改到别处时，Sandbox创建失败。

这项检查只固定 Agent启动时的默认用法，不是安全边界。Agent之后仍可显式传 `docker --host`访问另一个
endpoint。不可信任务需要依赖 managed模式的网络策略，而不是依赖 Docker CLI的默认值。

<Warning>
  这不是隔离方案。Agent可以通过 socket创建 privileged容器、挂宿主目录，并操作同一 daemon上的
  其它容器、image、volume和 network。不要给不可信 Agent挂 rootful宿主 socket。
</Warning>

## 方案二：Docker-in-Docker

DinD让每个外层 Sandbox拥有自己的 inner daemon。先创建镜像：

```dockerfile title="sandbox/Dockerfile" theme={null}
FROM docker:29-dind

RUN apk add --no-cache ca-certificates git nodejs npm python3 \
  && addgroup -g 1000 node \
  && adduser -D -u 1000 -G node node \
  && addgroup node docker
```

生产评估应把 `FROM`钉到审核过的 digest，并把被测 Agent需要的固定 CLI烘焙进镜像。

### NiceEval 如何启动 DinD

选择 `dockerAccess: { mode: "dind", ... }` 就同时选择了 NiceEval 的 DinD 镜像协议。
provider 会覆盖派生镜像的原有 `ENTRYPOINT` 和 `CMD`，先检查 `docker-init`、
`node`、`docker`、`dockerd-entrypoint.sh`、`timeout` 和 `tail`，再启动自己的
supervisor。supervisor 同时监督官方 `dockerd-entrypoint.sh dockerd`与 Sandbox 保活进程。
任一进程提前退出都会停止 outer container，而不会留下一台看似存活但无法使用
Docker 的 Sandbox。

inner daemon 只监听 `/var/run/docker.sock`，不开放 2375/2376 TCP endpoint。
Agent 仍以 `user: "node"` 运行。上面 Dockerfile 在构建期把 `node` 加入
`docker` 组，因此不需要 `chown root:node` 或 `chmod 666` socket。如果镜像缺少工具、
用户不在 `docker` 组、daemon 提前退出或 readiness 超时，NiceEval 会在删除创建失败的
容器前收集有界日志尾部并报出可操作的原因。

派生镜像不要设置 `DOCKER_HOST` 或 `DOCKER_CONTEXT`。NiceEval 会先确认默认 context，
再核对不带 endpoint 选项的 `docker info` 与显式 `/var/run/docker.sock` 是否到达同一个 daemon。
这项 compatibility check 通过后，才会执行作者声明的 readiness。

这个协议不承诺保留任意 service image 的启动语义。如果你要评测一个必须依赖自己
`ENTRYPOINT` / `CMD` 的服务组，应该使用 Compose Sandbox，由 Compose 声明服务进程。

### 在 setup 中准备 Attempt 运行时

Dockerfile 只烘焙固定工具、只读归档和项目初始文件。需要 inner daemon 的操作放进 Sandbox 级
`setup`，不要放进镜像 `ENTRYPOINT`。下面的 setup 可以导入离线 image、把项目初始文件复制到可写
workspace，并运行项目自己的 smoke check：

```ts title="lib/dind-sandbox.ts" theme={null}
import { dockerfileSandbox } from "niceeval/sandbox";

export const dindSandbox = dockerfileSandbox({
  context: new URL("../sandbox/", import.meta.url),
  user: "node",
  dockerAccess: {
    mode: "dind",
    isolation: "raw-privileged",
  },
  readiness: {
    command: ["docker", "info"],
    user: "node",
    timeoutMs: 30_000,
  },
}).setup(async (sandbox, ctx) => {
  ctx.progress({ message: "准备 inner Docker 运行时" });
  const result = await sandbox.runCommand("prepare-inner-runtime", [], {
    user: "root",
    stream: true,
  });
  if (result.exitCode !== 0) {
    throw new Error(result.stderr || "inner Docker 运行时准备失败");
  }
});
```

NiceEval 会先启动并验证 inner daemon，再执行这个 setup，最后运行 Agent。setup 非零退出时，
Attempt 在 `sandbox.create` 阶段记为 `errored`，不会把半成品 Sandbox 交给 Agent。固定内容留在 image layer，
每个 Attempt 只复制或导入必须写入 tmpfs 或 inner data-root 的状态，可以减少重复安装和网络漂移。

### Raw privileged DinD

在一次性 VM或专用 runner上，可以显式选择 raw privileged：

```ts title="experiments/dind-raw.ts" theme={null}
import { defineExperiment } from "niceeval";
import { codexAgent } from "niceeval/adapter";
import { dockerSandbox } from "niceeval/sandbox";

export default defineExperiment({
  agent: codexAgent(),
  model: "gpt-5.4",
  sandbox: dockerSandbox({
    source: {
      type: "dockerfile",
      context: new URL("../sandbox/", import.meta.url),
    },
    user: "node",
    dockerAccess: {
      mode: "dind",
      isolation: "raw-privileged",
    },
  }),
});
```

必填的 `raw-privileged`字面量是风险授权。NiceEval不会把 raw模式描述成 rootless，也不会在失败时
自动改用 managed profile。

### Managed rootless DinD

共享宿主或不可信 Agent使用 managed模式。它在 inner daemon之外增加 profile attestation、单容器
资源限制、跨进程容量准入、独占外层网络和 watchdog恢复：

```ts title="experiments/dind-managed.ts" theme={null}
import { defineExperiment } from "niceeval";
import { codexAgent } from "niceeval/adapter";
import { dockerSandbox } from "niceeval/sandbox";

const GiB = 1024 ** 3;
const MiB = 1024 ** 2;

export default defineExperiment({
  agent: codexAgent(),
  model: "gpt-5.4",
  maxConcurrency: 4,
  sandbox: dockerSandbox({
    source: {
      type: "dockerfile",
      context: new URL("../sandbox/", import.meta.url),
    },
    user: "node",
    dockerAccess: {
      mode: "dind",
      isolation: "managed-rootless",
      profile: "default",
    },
    resources: {
      cpus: 4,
      memoryBytes: 6 * GiB,
      pidsLimit: 2048,
      readOnlyRootfs: true,
      tmpfs: {
        "/var/lib/docker": { sizeBytes: 3 * GiB, mode: 0o711, executable: true },
        "/home/sandbox/workspace": {
          sizeBytes: 2 * GiB,
          mode: 0o755,
          uid: 1000,
          gid: 1000,
          executable: true,
        },
        "/home/node": { sizeBytes: 512 * MiB, mode: 0o700, uid: 1000, gid: 1000 },
        "/tmp": { sizeBytes: 1024 * MiB, mode: 0o1777 },
        "/run": { sizeBytes: 128 * MiB, mode: 0o755 },
      },
    },
  }),
});
```

profile缺失、拼错、attestation失败或容量不足时，NiceEval会在模型调用前失败，绝不降级成 raw
privileged。宿主部署完成后先检查 profile：

```bash theme={null}
npx niceeval docker profile list
npx niceeval docker profile doctor default --smoke
npx niceeval exp dind-managed
```

`doctor --smoke`会在该 profile上启动一次 DinD容器，并实际运行内层
`docker run --rm alpine:3.20 true`。它证明 profile支持 nested Docker，但不会检查项目自己的
Dockerfile。项目镜像仍由默认 `docker info` readiness验证。

### 为 DinD 设置运行并发

`resources.memoryBytes` 限制一台 Sandbox，不会自动设置 Run 的并发数。DinD 还会占用 inner
image、BuildKit、tmpfs 和 page cache，默认并发对本机可能过高。先用较小并发完成 smoke run：

```bash theme={null}
pnpm exec niceeval exp dind-managed --max-concurrency 2
```

再根据宿主的可用内存、CPU 和磁盘吞吐逐步增加。可以先用
`可用内存 ÷ memoryBytes` 估算保守上限，同时为宿主 Docker、NiceEval 和其它进程留余量。
Experiment 只服务这类重型 Sandbox 时，也可以直接设置 `maxConcurrency`。

### 定位 DinD 创建失败

创建失败后使用终端给出的 Attempt 定位符，不要直接翻运行产物：

```bash theme={null}
pnpm exec niceeval show @1ABC234DEF
```

常见错误与处理方式：

| 公开错误                                                         | 处理方式                                                                         |
| ------------------------------------------------------------ | ---------------------------------------------------------------------------- |
| `DOCKER_HOST must be unset` 或 `DOCKER_CONTEXT must be unset` | 删除镜像里的 endpoint/context 环境变量，让默认 endpoint 指向 `/var/run/docker.sock`          |
| `dind-image-incompatible: missing ...`                       | 从固定版本 `docker:<version>-dind` 派生，并补齐错误列出的工具                                  |
| `permission denied`                                          | 在构建期把 `user` 对应的 Agent 用户加入镜像内 `docker` 组                                    |
| Docker access compatibility timeout                          | 从 `niceeval show` 给出的有界 `dockerd.log` 尾部检查 daemon 启动、socket 与 storage driver |
| 启动前提示 orphan Sandbox                                         | 先用 `niceeval sandbox list --orphans` 只读核对，再显式运行 `niceeval sandbox prune`     |

## 在 Eval里验证 Docker任务

三种模式对 Eval暴露相同的 Docker CLI用法：

```ts title="evals/docker-compose.eval.ts" theme={null}
import { defineEval } from "niceeval";
import { commandSucceeded } from "niceeval/expect";

export default defineEval({
  description: "Agent can repair and start a Docker Compose project",
  async test(t) {
    await t.send("修复 Compose 项目，然后启动服务并确认健康检查通过。");

    t.check(
      await t.sandbox.runCommand("docker", ["run", "--rm", "alpine:3.20", "true"]),
      commandSucceeded(),
    );
  },
});
```

socket模式的命令操作显式选择的外层 daemon。两种 DinD模式操作 Sandbox内的 inner daemon。
`sandbox`也可以直接写在 `defineEval({ sandbox: ... })`里。多条 Eval共用配置时写在 Experiment上。
