调用方(自己的上层平台)可信,但 Agent 的行为不完全可信:它会执行命令、读写文件、访问网络,而且可能被网页或文档中的提示注入误导。要防的是 Agent 越界。
| 风险 | 例子 |
|---|---|
| R1 读取敏感文件 | 读 ~/.ssh、CLI 登录凭证、个人文档、其他会话的工作区 |
| R2 越界写入/破坏 | 删改工作区以外的文件 |
| R3 访问局域网 | 访问路由器、NAS、内网服务的管理接口 |
| R4 数据外传 | 把资料内容发到外部网站 |
| R5 资源耗尽 | 死循环脚本、大文件把机器拖垮 |
L3 应用层 文件访问策略校验(canUseTool)· 审批 · WebFetch 私网拦截
L2 引擎沙箱 Claude Bash 沙箱 · Codex 权限配置(bubblewrap)
L1 systemd ProtectHome=tmpfs / ProtectSystem=strict / 资源上限
(L0 专用用户:暂不启用,见 §5.3)
@anthropic-ai/sandbox-runtime,Linux 上用 bubblewrap + socat)| 方面 | 默认行为 | 配置 |
|---|---|---|
| 作用范围 | 只作用于 Bash 等 shell 命令及其子进程 | — |
| 写 | 工作目录、临时目录、additionalDirectories |
sandbox.filesystem.allowWrite / denyWrite |
| 读 | 默认能读机器上大部分路径,包括 ~/.ssh |
sandbox.filesystem.denyRead / allowRead(范围更窄的规则优先) |
| 网络 | 没有直连出口,经本机代理按域名白名单放行(默认为空) | sandbox.network.allowedDomains / deniedDomains |
| 逃逸口 | Claude 可以申请 dangerouslyDisableSandbox 在沙箱外重试 |
allowUnsandboxedCommands: false |
| 不可用时 | 默认静默降级为无沙箱运行 | failIfUnavailable: true |
不在沙箱内:Read/Edit/Write/Grep/Glob/WebFetch 只受权限规则约束(denyRead 拦不住 Read 工具);MCP、hooks 以完整权限运行。
read / write / deny,范围更窄的路径优先;同一路径上 deny 优先于 write,write 优先于 read。可以从 ":root" = "deny" 开始做"默认拒绝"。:minimal(运行时必需的系统路径)、:workspace_roots、:tmpdir。sandbox_mode,两者混用时 Codex 会退回旧的沙箱设置。RestrictNamespaces。可以设定,而且两个引擎都有对应的原生机制。读写范围作为 Profile 的一部分,在后台编辑。
interface FileAccessPolicy {
mode: 'restricted' | 'broad' // restricted:只能读列出的路径(推荐);broad:全盘可读,只排除 deny
read: PathRule[] // 额外可读路径:资料库引用 或 绝对路径
write: PathRule[] // 额外可写路径(少用;工作区始终可写)
deny: string[] // 额外禁止路径/通配,如 "**/*.env"
}
type PathRule = { knowledgeRootId: string } | { path: string }
最终生效的权限由三层合并而成,上层不能被下层放宽:
| 层 | 内容 | 谁来设定 |
|---|---|---|
| ① 全局强制禁止(不可覆盖) | ~/.claude/.credentials.json、~/.claude.json、~/.codex/auth.json、~/.ssh、~/.gnupg、/srv/agent-server/data、其他会话的工作区 |
后台"设置 → 安全",可追加,不能删除内置项 |
| ② Profile 策略 | 上面的 FileAccessPolicy |
后台"Profiles" |
| ③ 基线(始终存在) | 当前工作区读写;Python 运行环境、技能目录只读;:minimal 系统路径只读 |
内置 |
| 合并后的策略 | Claude | Codex |
|---|---|---|
| 可读路径 | additionalDirectories(资料库)+ sandbox.filesystem.allowRead + canUseTool 对 Read/Grep/Glob 的路径白名单校验 |
权限配置:":root"="deny"(restricted 模式)、":minimal"="read"、各可读路径 ="read" |
| 可写路径 | 工作区 + sandbox.filesystem.allowWrite + canUseTool 对 Edit/Write 的校验 |
":workspace_roots": "."="write" + 额外可写路径 |
| 禁止路径 | sandbox.filesystem.denyRead + permissions.deny: Read(...) + canUseTool |
各禁止路径 ="deny" |
| restricted 模式下的 Bash | denyRead 覆盖 home 和 /srv/agent-server 等大块目录,再用 allowRead 重新开放工作区和可读路径 |
由 ":root"="deny" 天然覆盖 |
canUseTool 中的路径校验是 Claude 侧的权威判断:路径先 realpath 规范化(防止 .. 和软链接绕过),再与合并后的策略比对。命中 deny 时直接拒绝,不在策略范围内的路径按配置拒绝或转人工审批。
jerry-remote 身份运行,用 systemd 加固CLI 登录态保存在你的 home 目录中,所以服务需要访问 ~/.claude、~/.claude.json、~/.codex 和 CLI 安装目录。做法是先用 ProtectHome=tmpfs 把整个 home 隐藏起来,再只把需要的路径绑定回来:
[Service]
User=jerry-remote
Group=jerry-remote
WorkingDirectory=/srv/agent-server
ExecStart=/usr/bin/node /srv/agent-server/app/apps/server/dist/main.js
Environment=HOME=/home/jerry-remote
# 文件系统:整盘只读,home 不可见,只回绑必要路径
ProtectSystem=strict
ProtectHome=tmpfs
BindPaths=/home/jerry-remote/.claude /home/jerry-remote/.claude.json /home/jerry-remote/.codex
BindReadOnlyPaths=/home/jerry-remote/.local/bin /home/jerry-remote/.local/share/claude
ReadWritePaths=/srv/agent-server
PrivateTmp=yes
# 内核与权限
NoNewPrivileges=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
LockPersonality=yes
# RestrictNamespaces 不能开:bubblewrap 需要 user/mount/net namespace
# 资源上限
MemoryMax=8G
CPUQuota=600%
TasksMax=512
效果:Agent 看不到你的 ~/web、~/.ssh、~/Documents 等目录。CLI 凭证目录虽然对进程可见,但由 §4.1 第①层的全局强制禁止在引擎沙箱和应用层两处拦截。
在 home 目录下的资料(例如 ~/Documents/资料)默认对服务不可见。有两种处理方式:
/srv/agent-server/knowledge、/data/...)。BindReadOnlyPaths=,然后重启服务。后台在保存资料库时会检测路径是否可见,不可见时直接给出需要追加的配置行。下面是启用前的分析,保留作为背景。当前的部署方式见 10-deployment.md。
同 UID 运行时仍存在的缺口:服务可以向你的其他进程发信号(systemd 249 没有 PrivatePIDs);home 以外属于你的文件按权限仍可访问;加固配置出错时没有兜底。以后需要时,迁移步骤是:创建 agentsvc 用户 → 用该用户重新登录 CLI → 修改 unit 的 User 和绑定路径。服务代码不受影响,因为所有路径都从配置中读取。
| 通道 | 控制方式 |
|---|---|
| Claude Bash 命令 | 沙箱代理 + 域名白名单(后台可配,默认只放行 pip 源) |
| Claude WebFetch | 在 canUseTool 中解析域名,拒绝私网、回环和链路本地地址 |
| Claude WebSearch / Codex web_search | 由厂商服务端执行,不经过本机网络 |
| Codex 命令 | 权限配置中 network.enabled=false;需要时走审批 |
claude 和 codex,服务直接复用这份登录态,供你自己的平台使用。按 Anthropic 条款的划分,这属于订阅所有者本人的使用;不涉及他人使用你的订阅。ANTHROPIC_API_KEY / OPENAI_API_KEY,避免意外切换到 API 计费。~/.codex,因此会加载你个人 config.toml 中的插件和 MCP(当前有 browser、computer-use、node_repl 等)。app-server 启动时必须用 -c 覆盖并禁用这些项,只启用服务需要的配置。实现时用 --strict-config 加启动自检来验证。settingSources: [] 不加载个人的 settings、插件和 hooks。socket(AF_UNIX) = EPERM),LibreOffice 因此无法在 Bash 中运行。
sandbox.network.allowAllUnixSockets: true:运行账号在 docker、lxd 组中,一旦放开,Agent 就能连接 /var/run/docker.sock,等同获得宿主 root 权限。convert_document(services/office.ts):服务进程自行用 bubblewrap 启动 LibreOffice,--unshare-all(断网、独立 IPC/PID),白名单挂载(只有 /usr、/etc、字体缓存和当前工作区),/usr、/etc 中零星的 socket 用 /dev/null 覆盖;输入和输出路径都必须在工作区内。/var/snap/lxd/common/lxd/unix.socket)在沙箱内仍然可以连通(实测 API 返回 200),所以改成了白名单方式,并加了回归测试(test/office.test.ts)。/srv、/mnt、/media、/root),/tmp、/etc 等系统路径对 Bash 仍然可读。文件工具(Read/Grep/Glob)由 hook 严格限制在白名单内。这是已知、可接受的缺口;systemd 部署时 PrivateTmp=yes 会让 /tmp 成为服务私有。/tmp/claude-<uid>/<工作目录转义>)已加入可写基线,否则模型会在写临时脚本时被拦截。docker / lxd 组,或者尽早迁移到专用用户(§5.3)。这两个组本身就是本机上权限最大的入口。agentsvc 运行:不在 docker/lxd/sudo 组;程序、运行环境和技能由 root 所有、服务只读;数据目录 0700~ 现在指 /srv/agent-server/home:~/.claude/.credentials.json、~/.claude.json 保护的是服务账号自己的登录态;home 位于数据目录内,同时受"服务数据目录"的软禁止保护(其他会话的 Claude 会话记录也在这里)