这套流程跑完之后,你会得到什么
先把终点说清楚。按下面的步骤做完,你手里会有四个能直接跑的东西:
1. 一个跑不出网的沙箱容器:编码智能体(下文简称“智能体”)在里面读代码、改代码。容器只连一条内部网络,唯一的出口是一个带白名单的 HTTP 代理。
2. 一份出网审计日志:智能体每一次对外请求都会留下“谁、什么时候、访问了哪个域名、成功还是被拒”的记录,按域名聚合一眼就能看出它有没有把代码往奇怪的地方送。
3. 一道密钥扫描闸门:仓库进门之前扫一遍、提交出门之前再扫一遍,把“智能体顺手把 .env 内容写进补丁”这类事故挡在合并之前。
4. 一套收敛过的权限:单一仓库、只读起步、只能开 PR 不能推主干、密钥不落在镜像层里。
一句话总结目标:让智能体在真实仓库里干活,但它的能见范围和能去范围都被写死。
前置条件清单
动手前确认这些都在手边:
- 一台 Linux 或 macOS 开发机 / CI runner,装好 Docker,当前用户能跑
docker且免 sudo; - 一个可以克隆的 Git 仓库,最好已经接了 CI;
- 一个 Grok 的 API Key(在 xAI 控制台申请,具体入口和额度以官方页面为准);
- 智能体本身的安装方式与启动命令:以官方文档当前版本为准。下文用
$AGENT_CMD代指官方给出的启动命令,你把它替换成实际的那条即可; - 开源工具:
gitleaks(密钥扫描)、squid或mitmproxy(出网代理)、jq(日志处理)。安装方式以各自官方文档为准; - 仓库的 fine-grained token 权限:
Contents: Read(先只给读)、Pull requests: Write、Metadata: Read。
建议先在个人测试仓库上走一遍,跑通再换生产仓库。
步骤一:先建隔离网络,再谈接仓库
顺序很重要。很多人是先接仓库、后想隔离,结果在“已经能跑”的路径上再加限制,很容易被自己绕过去。
先建两条网络:一条内部网络(无外网),一条给代理用:
```bash
内部网络:没有默认路由,容器里 curl 任何外网地址都会失败
docker network create --internal agent-internal
代理网络:普通 bridge,代理容器同时接入两条网络
docker network create agent-egress
```
写一个 Dockerfile,把智能体和常用工具装进去,注意三件事:非 root 用户、不把密钥写进镜像、工作目录固定:
```dockerfile
Dockerfile.agent
FROM node:20-bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
git curl ca-certificates jq python3 \
&& rm -rf /var/lib/apt/lists/*
安装智能体:按官方文档给出的方式,这里用占位
RUN npm i -g <官方包名> # 以官方文档当前版本为准
RUN useradd -m -u 1000 agent
USER agent
WORKDIR /work/repo
```
构建:
```bash
docker build -f Dockerfile.agent -t agent-sandbox:local .
```
关键点:容器启动时用 --network agent-internal,它连 DNS 都解析不出去,所有出网只能走代理。这不是“建议”,是物理隔离。
步骤二:用白名单代理兜住出网,并留下日志
代理容器是唯一同时接两条网络的角色。用 squid 做一个只放行必要域名的正向代理,并把日志打成制表符分隔,方便后面 awk。
```conf
squid.conf
logformat agentlog %ts.%03tu%t%>a%t%rm%t%ru%t%<st%t%>st
access_log /var/log/squid/access.log agentlog
http_port 3128
只允许访问模型 API 和代码托管,其余一律拒绝
acl allowed_domains dstdomain .x.ai .github.com .githubusercontent.com
acl safe_ports port 443
acl safe_ports port 80
http_access allow allowed_domains safe_ports
http_access deny all
关闭缓存,审计日志要的是全量流水
cache deny all
```
启动代理,把日志目录挂出来:
```bash
docker run -d --name agent-proxy \
--network agent-egress \
-v "$PWD/squid.conf:/etc/squid/squid.conf:ro" \
-v "$PWD/logs:/var/log/squid" \
ubuntu/squid
让代理容器也能进内部网络
docker network connect agent-internal agent-proxy
```
这里有个容易被忽略的点:不需要解密 TLS 也能做审计。代理在 CONNECT 阶段就能看到目标域名、时间、字节数和状态码,对“有没有外传”这个判断已经够了。真要看完整 URL 才需要做 TLS 中间人,那要额外分发 CA 证书,复杂度高一截,建议先用前者。
步骤三:进仓库前先做密钥扫描
顺序上,扫描要在“把仓库交给智能体”之前,而不是之后。因为智能体读到的每一个文件,都可能被它写进提示词、写进日志、写进补丁。
在仓库根目录跑一次全量扫描,把当前基线存下来:
```bash
全量扫描,输出 JSON 报告
gitleaks detect --source . --report-format json --report-path leaks-baseline.json
看看有多少条
jq 'length' leaks-baseline.json
```
如果历史提交里本来就有历史遗留的密钥(很常见),别急着全量清理,先建一个基线:把已确认的历史条目写进 .gitleaksignore,让后续扫描只对新增内容报警。
提交前的增量扫描接到 pre-commit 上(用 pre-commit 框架或直接写 git hook,具体命令以 gitleaks 官方 README 当前版本为准):
```yaml
.pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: <以官方仓库当前 release 为准>
hooks:
- id: gitleaks
```
再补一条:智能体的会话记录也要扫。它把 .env 读进去之后,内容可能出现在 transcript 里,而不是代码里。跑完任务后对日志目录再扫一次:
```bash
gitleaks detect --source ./logs --no-git --report-format json --report-path leaks-agent.json
```
步骤四:收敛权限,token 不进镜像
三件事。
第一,token 用文件挂载,不用环境变量明文。 环境变量容易被打进 docker inspect、CI 日志和崩溃转储:
```bash
mkdir -p /run/secrets && chmod 700 /run/secrets
printf '%s' "$XAI_API_KEY" > /run/secrets/xai_key
chmod 400 /run/secrets/xai_key
```
第二,Git 用 HTTPS remote,不用 SSH。 SSH 不走 HTTP_PROXY,会直接绕过你的审计代理;HTTPS 会尊重 https_proxy:
```bash
git remote set-url origin https://github.com/<org>/<repo>.git
```
第三,仓库侧收口。 开分支保护:主干禁止直接 push、必须走 PR、必须过 CI。给智能体的 token 只覆盖这一个仓库。这样即使它在容器里撒野,能造成的最大破坏也就是“开了一个需要人点合并的 PR”。
顺手加个 .agentignore,把不该进上下文的东西排掉:
```gitignore
.agentignore
.env
.env.*
secrets/
*.pem
*.key
node_modules/
dist/
```
步骤五:下发任务,用文件约束范围
别在聊天框里打字下任务——不可复现、不可审计。把任务写成仓库里的一个文件:
```markdown
<!-- tasks/TASK-123.md -->
任务:为 /api/orders 增加分页参数
允许修改
- src/api/orders.ts
- src/api/orders.test.ts
- tests/fixtures/orders.json
禁止修改
- .github/**
- package.json / lock 文件
- 任何以 .env 开头的文件
验收
- 新增单测覆盖 page/pageSize 的边界值
npm test全绿- 改动不超过 300 行
交付方式
新建分支 agent/task-123,开 PR,不要合并。
```
然后把容器拉起来跑:
```bash
docker run --rm -it \
--name "agent-run-$(date +%s)" \
--network agent-internal \
--user 1000:1000 \
-e HTTP_PROXY=http://agent-proxy:3128 \
-e HTTPS_PROXY=http://agent-proxy:3128 \
-e NO_PROXY=agent-proxy,localhost \
-v /run/secrets/xai_key:/run/secrets/xai_key:ro \
-v "$PWD/repo:/work/repo:rw" \
-v "$PWD/tasks:/work/tasks:ro" \
-v "$PWD/logs:/work/logs:rw" \
-w /work/repo \
agent-sandbox:local \
bash -lc "$AGENT_CMD --task /work/tasks/TASK-123.md"
```
$AGENT_CMD 换成官方文档给的实际启动命令,参数名以官方文档当前版本为准。注意 tasks/ 是只读挂载——任务描述本身就是约束,不该被智能体改写。
步骤六:看审计日志,回收结果
跑完之后,第一件事不是看 diff,是看出网日志。
```bash
#!/usr/bin/env bash
scripts/egress-report.sh
set -euo pipefail
LOG="${1:-./logs/access.log}"
echo "== 按域名聚合(次数 / 字节) =="
awk -F'\t' '{c[$4]++; b[$4]+=$6} END {for (k in c) printf "%6d %10d %s\n", c[k], b[k], k}' "$LOG" \
| sort -rn | head -30 |
|---|
echo
echo "== 被拒绝的请求 =="
awk -F'\t' '$5 ~ /^403|^5/ {print $1, $3, $4, $5}' "$LOG" | head -20
```
判断标准很简单:
- 域名列表里出现
api.x.ai、github.com之外的任何东西,就要问一句“为什么”; - 出现大量 403,说明智能体在尝试访问白名单外的地址,这本身就是一个信号;
- 单次任务的出网字节数突然比平时大一个数量级,去看它到底发了什么。
然后再看 diff。给 diff 设个软上限(比如 300 行),超了就不自动开 PR,转人工。
常见坑与排错
curl 报 Could not resolve host
内部网络没有 DNS,这是设计如此。让所有请求走代理:确认 HTTP_PROXY / HTTPS_PROXY 指向 http://agent-proxy:3128,并且 NO_PROXY 里包含代理自己的主机名,否则代理请求会被自己代理掉。
HTTPS 请求报证书错误
容器里缺 CA 证书。在 Dockerfile 里装 ca-certificates。如果后面上了 TLS 中间人代理,还得把自签 CA 装进容器的信任库。
git push 失败,提示 403
两种可能:token 权限给窄了(需要目标仓库的 Contents: Write + Pull requests: Write),或者分支保护拦住了直推主干。后者是好事,让它走 PR 就行。
出网日志是空的
三个常见原因:容器没走代理(检查 --network 是不是 agent-internal);squid 日志路径没挂载出来;access_log 那行的 logformat 名字和上面定义的对不上。
密钥扫描全是误报
先建 .gitleaksignore 基线,别急着把规则关掉。规则一关,真漏的也就扫不出来了。测试用的假密钥统一加 EXAMPLE 前缀,多数扫描器会自动跳过。
智能体把 .env 读进上下文了
--network 隔离只能防外传,防不了“读进来”。真正管用的是把敏感文件放在仓库外、用 .agentignore 排除,以及给容器用独立用户、只挂载必需的子目录而不是整个 home。
容器跑完文件属主变成 root
忘了 --user 1000:1000。宿主机上的仓库目录被 root 写过之后,下一次普通用户改不动,CI 也会挂。
大仓库上下文爆掉
把 node_modules、dist、大体积 fixtures 排进 .agentignore。任务文件里显式写“允许修改的文件”,比让它自己找要省得多。
一个任务跑太久
给容器加超时:docker run 外层套 timeout 1800,或者用 CI 的 job 超时。编码智能体在错误的路上会一直走下去,超时是最便宜的止损。
下一步建议
把日志接出去。 把 access.log 定期推到集中日志(Loki、ELK 都行),设一条告警:出现白名单外域名、或单任务出网字节数超过阈值就通知。日志留在本地磁盘上,出事时往往已经滚掉了。
把白名单做成仓库里的文件。 现在域名写在 squid.conf 里,改成从 egress-allowlist.txt 读取,任何新增域名都要走一次 PR 评审。这样“智能体为什么能访问这个域名”永远有一个可追溯的答案。
多仓库时按仓库发 token。 不要图省事用一个组织级 token 打通全部仓库。每个仓库一个 fine-grained token、一份白名单、一个日志前缀,出问题时能立刻定位到是哪条流水线。
定期轮换密钥,并且演练一次吊销。 智能体接触过的 key 都应该有明确的轮换周期。写个脚本,能在 5 分钟内把 token 吊销、把容器网络断开、把相关分支冻结——演练过一次,真出事时才不会手忙脚乱。
最后是心态上的建议。 编码智能体的价值在于“能连续做很多小事”,风险也在这里:单步都合理,串起来就跑偏了。所以别指望一条 prompt 约束住它,把约束放在环境里——网络、权限、扫描闸门——这些它绕不过去。
