gg 架构
gg 把产品行为收敛到可复用的 Go 核心里,每个界面都只是适配器。CLI、Web、Electron 客户端因此共享会话语义、并发规则、审批处理和工具执行。
包职责
| 包 | 职责 |
|---|---|
internal/app | 应用用例:会话操作、轮次执行、运行生命周期、审批、事件回放,以及传输层安全的 DTO。 |
internal/session | JSONL 持久化、会话树重建、迁移、持久化的分支 head、跨进程写锁、冲突检测,以及按稳定会话 ID 查找仓库。 |
internal/agent | provider 主循环、工具调用、审批、流式 agent 事件。 |
internal/transport/jsonrpc | 稳定的方法名和请求/响应校验。不含 UI 或文件系统策略。 |
internal/transport/stdio | 面向本地子进程的并发换行分隔 JSON-RPC。 |
internal/transport/httpapi | Bearer 保护的 RPC + 可回放的 SSE 事件,供浏览器部署。 |
internal/daemon 与 cmd/ggd | 配置、provider、仓库、工作区、传输层的组装根。 |
internal/cliapp | 只做 CLI 组装(一次性 prompt、按行交互、管理命令)。业务行为委托给 app.Service。 |
internal/userprofile | 用户画像(~/.gg/USER.md):加载、解析、渲染 "who you are" 系统块。 |
internal/memory | 结构化记忆存储(~/.gg/memory/):精选 MEMORY.md、日常日志 YYYY-MM-DD.md、people/ 和 groups/ 笔记、旧版 memory.md 迁移、日常日志裁剪、关键词检索。 |
internal/scheduler | Cron/单次任务定义、文件锁保护的 JSON 存储(~/.gg/scheduler/)、触发循环、无人值守审批策略。daemon 提供 Executor;核心除了 approver 类型外绝不 import agent 运行时。 |
internal/artifact | 版本化的 agent 产出物:~/.gg/artifacts/<id>/ 放 artifact.json 加不可变的 v1.md、v2.md……创建/版本/发布/删除;支持 markdown 和 HTML 类型;owner-only 权限(文件 0600、目录 0700)。版本分配和发布标记用 artifacts.lock 上的 flock 串行化。 |
internal/library | 用户的精选文件集(~/.gg/library/):扁平文件 + index.json(id、name、source、size、added_at)。接受上传(Add)和内存内容(AddBytes,artifact 发布时用)。修改用 library.lock 上的 flock 串行化;index.json 是保留名,冲突检测大小写不敏感。 |
internal/filelock | 跨进程独占文件锁(flock),供 artifact 和 library 存储共用;不支持 flock 的平台显式报错。 |
internal/connector | 第三方连接框架:OAuth 2.0 授权码流程(localhost 回调 + PKCE,state 防 CSRF)、token 持久化(~/.gg/connectors/<name>.json,0600,flock 串行化)、按需刷新。 |
internal/connector/google | 第一个 provider:Gmail(搜索/读/发)和 Google Calendar(查/建)走纯 net/http;自动刷新 transport,401 重试一次。Scope:gmail.readonly + gmail.send + calendar.events。 |
internal/tools | 内置 agent 工具实现,包括电脑操作工具(computer.go 加各 OS 后端 computer_linux.go / computer_darwin.go;Windows 和其他 Unix 用 unsupported stub)。信任边界:工具跑在用户自己的 OS 权限下——任何写操作或外部副作用都必须实现 agent.ApprovalDescriber,让每次调用都过审批流水线。 |
internal/mcp | 基于官方 modelcontextprotocol/go-sdk 的 MCP 客户端:配置(~/.gg/mcp.json)、stdio/HTTP 建连、schema 适配、mcp_<server>_<tool> 工具桥。信任边界:server 是第三方代码——stdio 子进程只继承最小环境,每个 MCP 工具都走 approval,建连或 list 失败的 server 在 Service 生命周期内被跳过。 |
ui/src | 共享 React UI;Web 与 Electron 共用同一份 HTTP transport。 |
ui/electron | 原生窗口、工作区选择和 sidecar 生命周期:以 --http + 每次启动随机 token 拉起 ggd,只把 endpoint、token 和工作区标签经窄而上下文隔离的 bridge 交给 renderer。 |
依赖指向内层:UI 和传输层依赖应用用例;应用层依赖 agent 和持久化抽象;核心绝不 import UI 或网络包。
应用组件
日志字段、debug 配置和可复现的长会话基准见运行诊断与性能基线。
Service 是供适配器调用的用例入口,内部组件各自承担明确职责:
| 组件 | 职责 |
|---|---|
conversation | 会话与分支状态、轮次串行化、持久化提交;模型和摘要在写入成功后才更新内存。 |
turnExecutor | provider/runner 编排和上下文压缩,不处理传输与命令解析。 |
promptBuilder | 读取个人、项目和 skill 提示词,不创建工具执行资源。 |
conversationTools | 工具能力组装、browser/MCP/media 资源缓存与关闭;Options.ToolProviders 可供调用方和测试注入工具注册表。 |
NewSessionService | CLI、daemon 和子会话共享的状态恢复与模型选择规则;显式模型参数优先于持久化选择。 |
Run / eventReplay | 执行结果与审批 / 有界事件回放;定时任务和 Telegram 通过 Run.Await 等待结果,事件过期不会把成功执行变成失败。 |
Service 的所有者负责关闭资源:CLI 延迟关闭,Manager 关闭退出缓存的会话,并在停机时取消、等待工作协程。工具定义查询使用临时工具资源所有者,查询后关闭;MCP 的动态定义仍可能需要实际连接。
CLI 与 Runtime 通过 app.PublishArtifact 共用版本校验和失败补偿;补偿失败会和原始发布错误一起报告。JSONL 记录共用 EntryMetadata 和一个实际载荷,编解码、文件写入和纯会话树投影分别维护,保留现有 v3 的扁平格式。
个人层(画像 + 记忆)
app.SetupPersonal(cfg) 在每次 CLI 和 daemon 启动时运行,把个人层接入系统提示词:
internal/userprofile:加载~/.gg/USER.md(gg init创建;文件缺失没关系,失败也绝不阻塞启动),渲染紧凑的 "who you are" 块:名字/称呼、时区、语言、备注。internal/memory:确保~/.gg/memory/存在,把旧版~/.gg/memory.md迁移成memory/MEMORY.md(旧文件改名为memory.md.bak,已有精选内容时绝不覆盖),裁剪过期的日常日志,给提示词内容做快照。
系统提示词组装顺序(代码指令之后):用户画像、精选记忆(MEMORY.md)、今天的日常日志尾部(YYYY-MM-DD.md),然后是项目指令(AGENTS.md)和 skills。工具面:memory_add 接受 scope=general|daily|person:<name>|group:<name>(默认 general);memory_search 跨 scope 做大小写不敏感的关键词检索,最多返回 10 条 path:line: snippet。记忆关闭时两者都隐藏。
定时任务(cron / 单次)
internal/scheduler 拥有任务定义和触发循环;internal/daemon 把它接入运行中的 daemon(ggd),gg job ...(在 internal/cliapp)从任意进程管理任务:
- 存储:
~/.gg/scheduler/jobs.json(任务定义,rename 原子写)和runs.jsonl(append-only 运行日志,每行一个 JSON)。所有jobs.json的读-改-写都用jobs.lock上的flock串行化(仅 Linux/macOS),CLI 和 daemon 可以并发改任务。纯读走View,绝不重写文件。状态文件 owner-only(0600,目录0700),和会话、记忆持久化一致。 - 循环:daemon 在后台跑
Scheduler.Run(ctx)(ggd --no-scheduler关掉)。等下一次触发的等待上限 30s,循环睡觉时 CLI 新增/恢复的任务能及时被捡起。每次 tick 先把到期任务的持久化NextRun往前推,再派发 goroutine,所以长任务不会让循环在同一次触发上打转;上一轮还没跑完又到触发时间的,记一次 "skipped" 并同样推进NextRun。每个任务还有内存级 guard 防止重叠执行。瞬时的存储错误(包括启动时 reconcile 失败)报到 daemon 的 stderr 并重试,不杀循环。 - 执行:每次触发在一个新会话(
scheduler/<name>-<timestamp>)里跑一轮 agent,经app.Runtime.StartTurnWithApprover,可审计、可恢复。任务的Timeout(默认 10m)约束这一轮。任务记录创建时的工作区目录;daemon 只触发工作区和自己一致的任务(工作区跟踪之前建的任务,值为空,哪里都触发)。 - 审批策略:
scheduler.UnattendedApprover默认拒绝所有走 approval 的工具;建任务时加--allow-all才放行。危险 shell 命令即使加了--allow-all也会被拒绝。这里是显式注入——scheduler 绝不依赖 nil approver(runner 会把 nil 当"全放行")。 - 重启语义:cron 任务从当前时间重新排(错过的触发不补);从没跑过、已过期的单次任务在 daemon 启动时补跑一次。daemon 写
~/.gg/ggd.pid,没 daemon 活着时gg job add会警告。
Daemon 通道与监管
常驻的 daemon 服务(定时任务调度器、Telegram 机器人)都作为 channel,走 internal/daemon 里统一的启动路径:
- panic 隔离:每个 channel goroutine、每个 Telegram update 处理、每个 stdio JSON-RPC 请求处理都有 recover,记下堆栈后继续服务——一条坏消息或一个坏请求永远拖不死整个 daemon。panic 的 Telegram update 会被 ack(不再重投),因为对该输入 panic 是确定性的;panic 的 stdio 请求会收到 JSON-RPC
-32603内部错误响应(notification 即使 panic 也不回包,保持 response-free)。更深一层也有兜底:panic 的定时任务执行会被记为一次失败的 run 并上报;panic 的 agent turn 会把这一 run 记为失败,而不是拖死 daemon。 - 存活状态:运行时失败的 channel 会上报并保持停止(不会拖 daemon 下水)。每个 channel 的状态(
running/failed/stopped,含错误和时间戳)由 monitor 跟踪,在已认证的GET /health接口以channels字段暴露;未认证的/healthz保持为纯进程存活探针。 - 单实例:
ggd在整个生命周期内持有~/.gg/ggd.pid上的排他flock,且结果是 fail-closed:第二个完整 daemon 会收到 "already running" 并拒绝启动;锁根本建不起来时也拒绝启动,而不是无保护地跑(否则会有重复的定时任务和重复的 Telegram 回复)。起保护作用的是锁本身而不是文件里的 pid,所以崩溃的 daemon 永远不会挡住新实例。唯一的例外是父进程监管的 sidecar(--exit-on-stdin-eof,如 Electron 应用):已有实例持有 channel 时,sidecar 不启动 scheduler/消息 channel、只提供 API 服务,而不是直接启动失败。~/.gg和 pid 文件都是 owner-only(0700/0600),已存在的路径也会被纠正,和其他状态存储一致。
Artifacts 与 library
internal/artifact 管版本化产出物;internal/library 管用户的文件集;两者在发布时交汇:
- 存储:
~/.gg/artifacts/<id>/artifact.json(id、title、type、version、published_version、时间戳)加不可变的v<n>.md/v<n>.html,全部原子写(临时文件 + rename)。artifact_edit追加一个完整新版本——绝不做 diff 合并——每个版本都可复现。支持markdown和html两种类型;单版本内容上限 1 MiB,artifact 撑不爆 agent 上下文。所有读-改-写用artifacts.lock上的flock(经internal/filelock)串行化,CLI 和 daemon 并发编辑不会分到同一个版本号。List永远返回非 nil 切片,空库时 JSON 调用方看到[]而不是null。 - Agent 工具:
artifact_create(title, type, content)和artifact_edit(artifact_id, content),store 打开时经app.toolProviders的artifact条目注册。两者都实现ApprovalRequest(title/type/size 加修改前后内容预览),因为它们写工作区之外;无人值守审批器默认拒绝,除非任务开了--allow-all。 - 发布:
gg artifact publish <id>和 Web 的 Publish 按钮(JSON-RPCartifact.publish)走同一条app.PublishArtifact:先把最新版本的字节存进 library(library.AddBytes→~/.gg/library/<slug>.<ext>,sourceartifact:<id>),再推进 artifact 的published_version。调用方读和标记之间 artifact 出了新版本时,Store.Publish(id, expectedVersion)报ErrVersionChanged拒绝——半存的 library 拷贝删掉,调用方重试,而不是记一个字节从没存过的 published 版本。 - Library:
gg library add <path> [--name]把普通文件(≤ 50 MiB,按实际拷贝字节数算,不看拷贝前 stat)拷进~/.gg/library/,命名防冲突;index.json是唯一真相,原子更新。命名冲突大小写不敏感检测(macOS 默认文件系统大小写不敏感),index.json是保留名会被拒绝,上传永远盖不掉也删不掉索引。所有修改走library.lock上的flock,并发添加不会抢到同一个名字或丢条目。library list|remove|path按 id 或大小写不敏感的 name 管理条目。 - Web:
artifact.list/artifact.get/artifact.publish走现有 JSON-RPC 传输;system.info广告artifact能力,Web 客户端只在连上的 daemon 报告该能力时才显示 Artifacts 页签。React Artifacts 页签用marked渲染 markdown,HTML 放在<iframe sandbox="">里(不执行脚本)。阅读器永远显示最新版本(发布后加的草稿也看得见);publishedVersion < version时出现 Publish 按钮。信任边界:artifact 内容来自本地用户自己的 agent 或文件,和聊天记录同等信任。
运行时模型
Runtime按稳定 ID 打开会话,绝不把会话文件路径暴露过网络边界。Manager拥有打开的会话服务。不同会话可以并发跑,一个会话同时只允许一轮。不活跃会话按 LRU 驱逐,已完成的 run 按数量和时间过期。- 每轮有一个 run ID。事件带单调递增序号,在有界保留窗口内可通过
run.wait或/events回放。SSE 帧带序号和 idle 心跳,Web 客户端可以指数退避重连、从上一个事件续。掉队太多的客户端收到event_history_expired,重载会话快照。 run.active和run.get暴露可重连的 run 状态,包括保留的序号窗口和待审批。Web 和 Electron 客户端重载后打开上次选中的会话、重新 attach 到它的 active run。- 工具审批是异步的:
approval_requested事件暂停工具,run.approve放行。取消和 steering 走同样的 run/session 边界。 - fork 和 clone 创建独立的 service 和 store。checkout 写一条
head记录,重启后选中的树位置还在,即使没发新消息。 - JSONL 写用短生命周期的跨进程 lease 加乐观 head 校验。写过期了的 CLI 或 daemon 收到
session_conflict,而不是悄悄把分支交错写进同一个文件。
公开 JSON-RPC 接口
传输无关的方法:
system.info:gg 协议版本和能力协商;同时报告degradedProviders—— 最近一次构建失败的工具能力及其原因(协议 1.3 新增,健康时省略)。 Provider 区分"预期禁用"(未装 Chromium、未连接 Google、无 MCP 配置) 与"运维失败"(store 打不开、MCP server 连不上):只有后者会被记日志 并上报。所有 session 把失败和恢复都上报到同一个 workspace 级注册表, 任意 session 的一次成功构建就能清除该 provider 的 degraded 状态。 原因在发给客户端前会脱敏(home 目录替换为~),完整错误保留在 daemon 日志中session.list、session.create、session.open、session.get、session.renamesession.action带tree、fork或clonerun.start、run.wait、run.get、run.active、run.cancel、run.approve、run.steerartifact.list、artifact.get、artifact.publish
stdio 每行一个 JSON-RPC 对象,支持并发请求(等审批时另一个请求必须能进)。HTTP 在 POST /rpc 接受 JSON-RPC;GET /events 以 SSE 流式推送 run 事件。HTTP 模式要求 Bearer token,默认只绑 loopback,除非显式 --allow-remote。打包后的 Electron renderer 从 file:// 加载(origin 为 null),因此 daemon 对 null 和 loopback http(s) 来源的 CORS 预检(OPTIONS)在 bearer-token 鉴权之外直接应答;token 仍是真正的鉴权边界。线格式保持 JSON-RPC 2.0;system.info.protocolVersion 独立给 gg 方法、事件、DTO 和稳定应用错误码做版本。
客户端边界
React 应用依赖一个小 Transport 接口。HttpTransport 经带 Bearer <redacted> 的 HTTP 与 ggd 通信(POST /rpc + SSE /events),Web 与 Electron 共用。桌面端由 Electron 主进程以 ggd --http 127.0.0.1:<端口> --token <随机> --exit-on-stdin-eof 拉起,只把 endpoint、token 和工作区标签暴露给 renderer;renderer 拿不到 Node.js。sidecar 的 stdin 是一条主进程持有的控制管道:父进程异常退出、没走 before-quit 清理(崩溃/SIGKILL)时,stdin 到达 EOF,daemon 自行退出,不会变成拿着丢失 token 和端口的孤儿进程。
CLI(gg -p、脚本模式、管理命令)有意不做成 thin client:它经 internal/cliapp 在进程内直接运行 agent,从不经 HTTP 连 ggd。好处是无 daemon 也可用、延迟更低、对脚本友好;代价是同一份 agent 逻辑存在两条执行路径(进程内 vs. ggd 承载的 Web/Electron)。这是已知取舍,不是疏漏:只有当两条路径出现可观测的行为漂移时,才把 CLI 收敛到 HTTP 传输上。
扩展规则
加行为时:
- 用例和测试放
internal/app。 - 持久化改动走
session.Repository。 - 客户端需要时才暴露传输无关的方法/事件。
- Web 或 Electron 特有的策略留在各自 adapter。
- 文件路径、provider 密钥、不受限的 IPC 不进公开 DTO。
- 新 agent 能力做成
ToolProvider条目、MCP server 声明或 skill——绝不写中央 if/else 链。写操作或外部副作用要实现agent.ApprovalDescriber,让无人值守审批器默认拒绝。 - 平台/环境相关的工具经
Available/Build降级,不可用时绝不广告;ToolCapabilities()永远从 registry 推导。 - 外部代码和提示词内容(MCP server、skill)跑在最低信任级:清洗过的子进程环境、approval-gated 的工具、fail-skip 而不是 fail-fatal。
这样未来的移动客户端、远程 Web 部署或另一种终端 UI 都只是再一个 adapter,而不是再实现一遍 agent。
添加 agent 工具
工具由 ToolProvider 注册表(internal/app/toolregistry.go)每轮组装:每个 provider 的 Build 返回一个能力的工具,返回 nil 切片或报错就把该能力降级为不存在,不影响其他。要加工具:
- 在
internal/tools(或该能力自己的包)实现agent.Tool。 - 在
toolProviders里新增或扩展一个ToolProvider条目——绝不写中央 if/else 链。 - 工具做写操作或外部副作用就实现
agent.ApprovalDescriber,让每次调用都过审批流水线(无人值守审批器默认拒绝 approval-gated 的工具,除非任务 opt in)。
传输层经 app.ToolCapabilities() 广告能力,它从注册表推导,广告的集合不可能和实际注册的工具脱节。
MCP 服务器
MCP 的外部工具来自 ~/.gg/mcp.json 里声明的服务器:
{"servers": {
"filesystem": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"], "env": {"API_KEY": "..."}},
"remote": {"url": "https://example.com/mcp"}
}}internal/mcp 连接每个服务器(stdio 子进程或 streamable HTTP),把它的工具适配成 mcp_<server>_<tool>(清洗成 function-calling 安全的名字,最长 64 字符,冲突加数字后缀)。server 工具永远实现 ApprovalDescriber:每个 MCP 调用都走 approval。连接按 Service 缓存(一轮会话只 dial 一次),Service.Close 时回收。建连或 list 失败的服务器在 Service 生命周期内被跳过,一个坏服务器拖不慢每一轮。stdio 服务器只继承最小环境(PATH/HOME 加服务器自己的 env),父进程环境绝不整份透传。
Skills
Skills(internal/skills)是第三层扩展:markdown playbook,从项目 skill 根和 ~/.agents/skills 加载,模型按需读。skill 只加知识和流程,不加代码执行——agent 用现有工具面执行 skill 步骤,审批策略继承所用工具的。信任边界:skill 是来自仓库或用户自己文件的提示词内容;第三方 skill 文本按不可信的提示词输入对待。
扩展层级
三层,按信任从高到低:
- 内置 provider(
internal/app/toolregistry.go):编译进 gg,每轮构建,前置条件缺失(没装 Chromium、Google 没连接)或平台不支持时经Available门降级为不存在。写操作和外部副作用实现agent.ApprovalDescriber。现成例子:电脑操作工具——internal/tools/computer.go加各 OS 后端;Windows 和其他 Unix 上这个能力既不构建也不广告。 - MCP 服务器(
~/.gg/mcp.json、internal/mcp):外部进程暴露工具为mcp_<server>_<tool>;永远走 approval;连接按Service缓存;坏服务器被跳过,绝不致命。 - Skills(
internal/skills):模型按需读的 markdown 流程;不跑新代码;审批来自步骤所用的工具。
有意的决策——browser 导航需要审批。browser_navigate 需要用户审批:审批提示里展示的 URL 会先过 SSRF 防护(拒绝云元数据、localhost、内网等非公开目标),Chromium 建连前用户再确认一次确切目的地。收紧的原因是 SSRF 防护只检查初始 URL——Chromium 会自己跟随重定向和页面子资源,光靠初始 URL 检查挡不住内网目标。browser_read 和 browser_screenshot 仍豁免:它们只操作用户已批准的页面,不会触发新的导航。残留风险:(1) 批准后的重定向仍可能打到内网(Chromium 跟随重定向时不再复查);(2) 上下文里的数据可以拼进 URL 带出去——已接受,机密本来就不该不设防地躺在 agent 上下文里。只对导航(而不是每次读取)加审批,自主调研依然可用:一次审批打开页面,后续读取不再打扰。
app.ToolCapabilities() 从注册表推导广告集合,客户端永远看不到建不出来的能力的工具。
安全模型
工具以当前用户的权限运行。写操作和其他副作用要过审批;agent runner 里 nil approver 仍然表示"放行",所以每个会跑工具的调用方都显式传入 approver。CLI 的 --approval auto 不再使用 nil approver。
审批模式
CLI 策略在 internal/cliapp,覆盖一次性 prompt(gg -p 和 gg <prompt>)以及按行交互模式。
| 模式 | 有终端 | 其他需要审批的工具 | 危险 bash |
|---|---|---|---|
auto(默认) | 询问 | 拒绝,并说明可以用 --yolo 或 --approval on-request | 有终端就询问,否则拒绝 |
on-request | 询问;没有终端时进程直接退出 | 同上 | 询问 |
yolo(--yolo 或 --approval yolo) | 不询问直接放行 | 不询问直接放行 | 有终端就询问,否则拒绝 |
never | 不询问直接放行 | 不询问直接放行 | 拒绝(这个模式从不询问) |
"有终端"指 stdin 和 stderr 都是终端,行内提问实际用得到它们。明显只读的 bash 命令在所有模式下都不询问。edit、write 和其他需要审批的工具都不是只读。写到工作区 agent 目录(.gg/agent/)仍像以前一样预先放行。
Web 和桌面客户端继续用 daemon 的 runApprover:界面上询问,没人回答不会变成放行。定时任务用 scheduler.UnattendedApprover(默认拒绝,--allow-all 才放行,危险 bash 仍然拒绝)。
命令分类
tools.ClassifyCommand 在 approver 之前给每条 bash 命令打标签。标签和原因写在 agent.ApprovalRequest 的 risk、riskReason 上,并出现在审批详情里。
- 只读(
ls、cat、head、tail、grep、rg、不带-exec/-delete的find、不带前置-c/--config-env且不带--ext-diff/--textconv的git status/diff/log/show、go version/env、仅-mod=readonly或-mod=vendor的go list、只做格式化或解析的date,以及一小份其他查看类命令)。不询问。git -c不是只读:core.fsmonitor的值可以是 hook 命令。go list -mod=mod和未约束的go list不是只读。date -s、date --set和位置参数形式的时间戳会改系统时钟,不是只读。 - 危险。
rm -rf指向/、/*、~、$HOME、/home、/Users或/root;rm以及其他写到工作目录之外的操作(>、cp/mv/tee/chmod/chown等);curl/wget/fetch管道进 shell,包括经过env(curl … | env sh);sudo/doas/pkexec/su;磁盘和格式化工具(mkfs、fdisk、dd of=/dev/…、diskutil等);电源命令(shutdown、reboot等)。原因显示在审批请求里。--yolo和任务的--allow-all都不能跳过。写入路径会先沿着最近的已存在目录解析(含符号链接),再和工作目录比较。 - 其余命令(测试、构建、普通编辑、工作目录内的
rm)走该模式的普通策略。
&&、||、|、; 按段分类。下载内容管道进 shell 算危险,即使每一段单独看都普通。$(...) 和反引号会按引号和转义再分类,所以 echo "$(printf ')'; rm -rf /)" 是危险的,而单引号里的 '$(rm -rf /)' 不会执行。bash -c 和 eval 会把脚本文本重新送进分类器。
沙箱
--sandbox 默认关闭。打开后,bash 会尝试放进操作系统提供的 helper:
- Linux:
bwrap。文件系统只读,工作目录可写,/tmp是新的 tmpfs,并带--die-with-parent。tmpfs 挂在绑定工作目录之前,所以位于/tmp下的工作区仍然可见。 - macOS:
sandbox-exec。禁止写文件,但允许工作目录、/tmp和用户临时目录。 - 其他平台,或 helper 不在
PATH里、或起不来(用户命名空间关闭、seatbelt 被拒):命令照常执行,输出里会说明沙箱不可用。
探测每个进程只做一次。沙箱里的命令用 shell -c,避免登录 profile 往工作目录外写文件而导致沙箱失败。网络不隔离。
已知限制
- 分类器不是 shell。别名、编码过的 payload,以及解释器(
python -c、perl -e、busybox)可以做出同样的事而不命中模式。混淆不在范围内;目标是抓住明显的情况,并且在一行无法当成只读时失败关闭(询问或拒绝)。 - 只有
~和$HOME/${HOME}被当成家目录。rm -rf $DIR不会被当成灾难性删除。 - 只读名单按程序名匹配。
PATH里更靠前的同名二进制也会被信任。启用并且真正生效的沙箱不依赖这个名字。 --yolo和--approval never会以用户身份运行普通写入,以及分类器没有标成危险的任何 shell 命令。- 沙箱不拦截网络。macOS 上
/tmp和用户临时目录仍可写。helper 探测失败时降级为无沙箱,而不是拒绝命令。 - Windows 没有沙箱 helper。
- 在提示里批准、传入
--yolo,或给任务加--allow-all,工具都以用户自己的权限运行。审批是检查点,不是隔离边界;可选沙箱只额外限制文件系统写入。
质量门禁
每个 PR 都要带着绿色 CI 来、保持可 review:
- CI:6 个 check(ubuntu-latest 和 macos-latest 的 Go、Web、Electron)必须过;
go vet ./...在 CI 跑,必须干净。 - Race:
internal/app、internal/session、internal/transport/...跑go test -race(CI 在 Linux 跑)。 - 格式:
gofmt -l干净。push 前本地跑make check——它跑格式、vet 和全量测试。 - 新包:接口先行、核心路径单测、类型化错误、导出符号写 doc 注释。
- 大小:PR 控制在 ~500 行以内,一个 reviewer 能装进脑子;大的拆成 stacked phases。
- Review:Codex review 意见独立核实——先对照代码确认再修,不唯 review 是从。有效的 P0/P1/P2 修掉;P3 看判断。CI 全绿才合。
- 平台:正式支持 Linux 和 macOS。
internal/tools和internal/app还必须能为 Windows 编译(GOOS=windows go build),每个平台拆分的新文件都要配otherstub,保证任意 Unix 目标都能编译。