Skip to content
Merged
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
69 changes: 69 additions & 0 deletions .github/workflows/sandbox.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: sandbox

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
bwrap:
runs-on: ubuntu-22.04
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"
cache: "pip"
cache-dependency-path: pyproject.toml
- name: Install bubblewrap
run: |
sudo apt-get update
sudo apt-get install -y bubblewrap
- run: python -m pip install -e ".[dev]"
- run: MCA_SANDBOX_BACKEND=bwrap python -m pytest tests/test_sandbox_integration.py -q
- run: mca sandbox probe --sandbox bwrap

docker:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"
cache: "pip"
cache-dependency-path: pyproject.toml
- run: docker pull python:3.11-slim
- run: python -m pip install -e ".[dev]"
- run: MCA_SANDBOX_BACKEND=docker python -m pytest tests/test_sandbox_integration.py -q
- run: mca sandbox probe --sandbox docker

sandbox-exec:
runs-on: macos-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"
cache: "pip"
cache-dependency-path: pyproject.toml
- run: command -v sandbox-exec
- run: python -m pip install -e ".[dev]"
- run: MCA_SANDBOX_BACKEND=sandbox-exec python -m pytest tests/test_sandbox_integration.py -q
- run: mca sandbox probe --sandbox sandbox-exec
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,29 @@ All notable changes to this project are documented in this file. The format foll

## [Unreleased]

## [0.3.3] - 2026-07-22

### Added

- Added `mca sandbox probe`, a provider-free disposable capability check for workspace writes, backend-specific outside-write and Unix-socket boundaries, denial or unavailability of a usable outbound route, and denial of a controlled TCP connection, plus dedicated real-backend CI that runs automatically on pushes and pull requests.

### Changed

- Hardened Linux `bwrap` with fully unshared namespaces and a read-only host root. The workspace and executor runtime are the only writable host paths; private `/run`, `/tmp`, and home tmpfs mounts are also writable inside the sandbox, with private `/dev` and fresh `/proc` views.
- Limited macOS `sandbox-exec` writes to the workspace and its private runtime tree, removing shared `/tmp` and `/private/tmp` write exceptions.
- Changed Docker execution on POSIX to use the invoking numeric UID:GID, a private size-limited `/tmp`, and explicit private `HOME`/`TMPDIR`, Python-bytecode, and Git environment values.

### Security

- Added backend-specific boundary tests and documented that native process-group cleanup remains best effort: a double-fork can escape into a new session, and `sandbox-exec` does not provide PID-namespace, cgroup, or container-equivalent descendant containment.
- Made probe results require reserved, cause-specific evidence exits: native mutation accepts only `EPERM`, `EACCES`, or `EROFS`, while Docker verifies the root mount's `ST_RDONLY` flag instead of inferring read-only state from a failed write. Other positive exits, negative executor returns, timeouts, exceptions, missing sentinel preconditions, and unavailable or `/tmp`-aliased host temp bases fail closed.
- A passing capability probe demonstrates only its bounded checks; it is not a guarantee that an arbitrary repository, command, dependency, image, daemon, kernel, or host is safe.

### Compatibility

- Preserved the existing `auto` selection order and explicit backend names. Every Docker image used for coding or tests must provide `/bin/sh`; `mca sandbox probe` additionally requires `python3`. Normal coding runs may still use another pre-pulled custom image when it satisfies the `/bin/sh` requirement.
- POSIX Docker workspaces now produce files as the invoking host UID:GID rather than container root; images that require root must be adjusted or replaced.

## [0.3.2] - 2026-07-20

### Added
Expand Down
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ mca demo

```bash
mca doctor --cwd /path/to/repo --sandbox auto --provider auto
mca sandbox probe --sandbox auto
```

`doctor` performs static prerequisite checks. `run` and coding-enabled `chat` sessions perform the authoritative sandbox usability probe at startup; a coding-enabled chat is one started with `--test-command`. An `/ask`-only chat started without `--test-command` skips the sandbox probe because it cannot run tests, shell commands, or coding tools. Doctor checks whether a provider key is present in the current process environment without printing its value, and inspects private env-file metadata without opening the file.
`doctor` performs static prerequisite checks. `run` and coding-enabled `chat` sessions perform the authoritative backend startup check; a coding-enabled chat is one started with `--test-command`. An `/ask`-only chat started without `--test-command` skips that check because it cannot run tests, shell commands, or coding tools. Doctor checks whether a provider key is present in the current process environment without printing its value, and inspects private env-file metadata without opening the file.

`mca sandbox probe` goes further: without a provider key or target repository, it creates disposable data and checks a workspace write plus backend-specific outside-write, Unix-socket, and network boundaries. Native backends must read an exact host sentinel and return the probe's reserved evidence code only when mutation is blocked by `EPERM`, `EACCES`, or `EROFS`; Docker must not see that sentinel and must separately report the `ST_RDONLY` mount flag for `/`. Other positive exits, launch failures, exceptions, and timeouts are failures rather than denial evidence. `bwrap` and Docker must hide the controlled and known host Unix sockets, while `sandbox-exec` may expose a path only if connection remains denied. The network check first attempts a no-packet UDP `connect` to a TEST-NET address and, only when the process cannot obtain or use an outbound route, requires denial of a controlled loopback TCP connection. It prints one `[PASS]` or `[FAIL]` result per check and rejects `--sandbox none`, which cannot demonstrate isolation. The probe itself is bounded and useful for validating a local setup, but a passing result is evidence only for these checks, not proof that arbitrary untrusted code is safe.

## Run and chat

Expand Down Expand Up @@ -59,6 +62,16 @@ mca undo /path/to/run.traj.json --dry-run
- `--allow-zero-tests` explicitly weakens verification by allowing a recognized zero-test result to satisfy the gate. `--sandbox none`, `--allow-shell`, `--allow-dirty`, `--yes`, and force/legacy Undo options also deliberately weaken protections; `--sandbox auto` fails closed if no usable backend is found.
- Native Windows supports informational CLI and configuration paths only. Run the full agent, structured tools, and `mca demo` from macOS, Linux, or WSL2. macOS uses `sandbox-exec`; Linux uses `bwrap` or Docker when available.

Backend boundaries differ:

| Backend | Enforced boundary | Important limit |
| --- | --- | --- |
| Linux `bwrap` | Unshares namespaces and keeps the host root read-only. The workspace and executor runtime tree are the only writable host paths; private writable tmpfs mounts provide `/run`, `/tmp`, and home, alongside private `/dev` and fresh `/proc` views. | Relies on the host kernel and installed Bubblewrap; its PID namespace strengthens descendant containment but is not a guarantee against a compromised host. |
| macOS `sandbox-exec` | Denies network and default writes, hides the real home except for a workspace below it, and limits writes to the workspace plus an executor-owned private runtime tree used for `HOME` and `TMPDIR`; shared `/tmp` and `/private/tmp` are not writable. | It is an OS policy profile, not a PID namespace, cgroup, or container boundary, and Apple may deprecate or restrict it. |
| Docker | Uses no network, a read-only/capability-free container, resource limits, a single writable workspace bind, and a private size-limited `/tmp`; on POSIX it maps the invoking numeric UID:GID and explicitly sets private `HOME`/`TMPDIR`, Python-bytecode, and Git environment values. | Relies on a trusted daemon, image, host kernel, and configuration. Every coding/test image must contain `/bin/sh`; `mca sandbox probe` additionally requires `python3`. |

Native process-group cleanup after timeout, interruption, or exceptions is best effort: a double-forked process can create a new session and escape that process group. Bubblewrap's PID namespace and Docker's container boundary provide stronger descendant containment, but no backend provides an absolute OS/process-containment guarantee.

These controls are defense in depth, not a guarantee that an untrusted repository, command, dependency, image, host, or provider is safe. Do not run it in a workspace containing production credentials. Read the complete [security policy](https://github.com/wusuiling-if/mini-code-agent-langgraph/blob/main/SECURITY.md) before use.

## Offline verified-patch benchmark
Expand Down
15 changes: 10 additions & 5 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,14 @@ mca demo

```bash
mca doctor --cwd /path/to/repo --sandbox auto --provider auto
mca sandbox probe --sandbox auto
```

这条无 Key 诊断会把 provider 记为 warning 而不是失败。`mca demo` 当前支持 macOS、Linux 与 WSL2;原生 Windows 请使用 WSL2/Linux 环境运行完整 Agent。

`doctor` 只静态检查 sandbox 可执行文件是否在 PATH。`run` 和启用了编码能力的 `chat` 会话会在启动时执行权威 sandbox 可用性 probe;这里“启用编码能力”指启动时传入了 `--test-command`。未提供 `--test-command` 的纯 `/ask` 会话会跳过 sandbox probe,因为它不能运行测试、shell 或编码工具。`doctor` 只检查当前进程环境中是否存在 provider key,不会打印 key 值;对于私有 env 文件,它只检查元数据而不会打开文件。
`doctor` 只静态检查 sandbox 可执行文件是否在 PATH。`run` 和启用了编码能力的 `chat` 会话会在启动时执行权威后端启动检查;这里“启用编码能力”指启动时传入了 `--test-command`。未提供 `--test-command` 的纯 `/ask` 会话会跳过该检查,因为它不能运行测试、shell 或编码工具。`doctor` 只检查当前进程环境中是否存在 provider key,不会打印 key 值;对于私有 env 文件,它只检查元数据而不会打开文件。

`mca sandbox probe` 会在不需要 provider key、也不接触目标仓库的前提下创建可丢弃数据,并检查工作区写入以及后端特定的外部写入、Unix socket 和网络边界。原生后端必须先精确读到宿主 sentinel,并且只有修改被 `EPERM`、`EACCES` 或 `EROFS` 阻止时才返回 probe 保留的证据退出码;Docker 必须看不到该 sentinel,并另外报告 `/` 挂载的 `ST_RDONLY` 标志。其他正数退出码、启动失败、异常和超时均视为失败,而不是拒绝证据。`bwrap` 与 Docker 必须隐藏受控及已知的宿主 Unix socket;`sandbox-exec` 可以看到路径,但连接必须被拒绝。网络检查先对 TEST-NET 地址尝试不发送数据包的 UDP `connect`,仅在进程无法取得或使用出站路由时继续要求受控 loopback TCP 连接被拒绝。每项检查输出一行 `[PASS]` 或 `[FAIL]`;`--sandbox none` 会被拒绝,因为它不能证明隔离。该 probe 有超时边界,可用于验证本机配置,但全部通过也只证明这些检查,并不代表任意不可信代码都是安全的。

## 安全与可靠性边界

Expand All @@ -38,7 +41,7 @@ mca doctor --cwd /path/to/repo --sandbox auto --provider auto
| 崩溃恢复 | run/chat 从完整工具边界恢复,并使恢复前的验证结果失效 | 被强制终止的外部命令可能已经产生部分副作用 |
| HMAC 认证撤销 | 私有 Undo journal 以 HMAC 绑定轨迹、工作区、路径和内容 hash,并在覆盖前检查冲突 | HMAC 校验可检测本机 journal 是否被篡改,不证明修改在语义上安全 |
| Fail-closed 隔离 | `auto` 实际探测后端;没有可用后端时拒绝执行命令,除非用户显式选择 `none` | macOS 使用 `sandbox-exec`,Linux 使用 `bwrap` 或 Docker;原生 Windows 的完整 Agent runtime 尚不支持 |
| 进程清理 | 超时、Ctrl-C、SIGTERM 和异常后回收命令进程组及本次 Docker 容器 | 宿主内核、Docker daemon 或依赖链失陷不在保证范围内 |
| 进程清理 | 超时、Ctrl-C、SIGTERM 和异常后尝试回收命令进程组及本次 Docker 容器 | 原生进程组清理是 best effort;double-fork 进程可创建新 session,`sandbox-exec` 不提供 PID namespace、cgroup 或容器等价的完整后代进程收容 |

这些机制是纵深防御,不是绝对安全沙箱。不要把不受信任的仓库与生产凭证放在同一工作区,也不要未经检查运行仓库自带的构建或测试命令;完整威胁模型见 [SECURITY.md](SECURITY.md)。

Expand Down Expand Up @@ -317,12 +320,14 @@ Undo 原始恢复内容保存在状态根目录的私有 `undo/` 中,使用 `0

沙箱可用性依赖操作系统:

- macOS:优先尝试系统 `sandbox-exec`(系统可能弃用或限制它)
- Linux:优先尝试 `bwrap`
- macOS / Linux:安装并启动 Docker,并预先拉取沙箱镜像后可选择 `--sandbox docker`;默认镜像是 `python:3.11-slim`,可用 `--docker-image` 或 `MCA_DOCKER_IMAGE` 指向带目标项目依赖的预构建镜像,运行时不会隐式拉镜像
- macOS:优先尝试系统 `sandbox-exec`。它拒绝网络和默认写入,隐藏真实 home(其中的目标工作区除外),只允许写工作区和 executor 所有的私有 runtime tree;`HOME`、`TMPDIR` 指向该私有目录,共享 `/tmp` 与 `/private/tmp` 不可写。它是系统策略 profile,不是 PID namespace、cgroup 或容器边界,而且系统可能弃用或限制它
- Linux:优先尝试 `bwrap`。它 unshare namespaces 并保持宿主根目录只读;workspace 与 executor runtime tree 是仅有的可写宿主路径,沙箱内的私有 `/run`、`/tmp` 和 home tmpfs 也可写,并使用私有 `/dev` 与全新 `/proc`。其 PID namespace 对后代进程的收容强于普通进程组,但仍依赖宿主内核与 Bubblewrap
- macOS / Linux:安装并启动 Docker,并预先拉取沙箱镜像后可选择 `--sandbox docker`;容器无网络、根文件系统只读、丢弃 capabilities、设置资源上限,只有工作区 bind mount 可写,`/tmp` 是私有且有大小限制的 tmpfs。在 POSIX 宿主上以调用者的数字 UID:GID 运行,并显式设置私有 `HOME`/`TMPDIR`、Python bytecode 与 Git 环境。默认镜像是 `python:3.11-slim`,可用 `--docker-image` 或 `MCA_DOCKER_IMAGE` 指向带目标项目依赖的预构建镜像,运行时不会隐式拉镜像
- 原生 Windows:`0.3.x` 不支持完整的 Agent runtime;请在 WSL2/Linux 中使用上述隔离后端
- 没有可用后端时,只有显式 `--sandbox none` 才允许不隔离执行

可用 `mca sandbox probe --sandbox auto` 检查上述有限边界,也可显式指定 `sandbox-exec`、`bwrap` 或 `docker`。所有 Docker coding/test 镜像都必须提供 `/bin/sh`,probe 还额外需要 `python3`;普通编码运行仍可使用满足 `/bin/sh` 要求的其他预拉取自定义镜像。原生进程组清理无法证明 double-fork 后创建新 session 的进程已被回收;Bubblewrap 的 PID namespace 和 Docker 容器边界提供更强的后代进程收容,但所有后端都不是完整 OS/process containment 保证。

沙箱、路径检查和脱敏都不是运行不可信仓库的绝对安全边界。不要让 Agent 在包含生产凭证、SSH 私钥或不应被模型读取的数据目录中运行。

## 项目结构
Expand Down
Loading