feat(composer): 运行权限三档 —— 「请求审批」真的逐次询问,「全部批准」给默认行为一个诚实的名字 (#1413) - #1436
Merged
Merged
Conversation
此前 composer 只有两档,「请求审批 · 逐次询问」提交时不带 agent、落到引擎默认 build(`"*": "allow"`), 真 Permission.ask 对 edit/bash 直接放行 —— 副标题是假的。 - 主进程注入第四个 hidden agent `alpha-ask` = build + edit/bash 改 ask(无 prompt,不顶掉底座); - `PermMode = "allow" | "ask" | "readonly"`,提交层按 `PERM_AGENT` 穷举表决定 agent(不再是二元分支); - 新会话默认档 = allow(它就是此前默认档的真实行为);ask / readonly 都压过手选档位; - 引擎层常驻闸 packages/opencode/test/permission/alpha-composer-tiers.test.ts:真 injectAlphaConfig + 真 Agent + 真 Permission.ask,alpha-ask 的 bash/edit 进待批队列并 fail-closed,build 对照臂放行,readonly deny; - ext 登记簿允许没有 prompt 的 agent,只登记 description;库存快照重生。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…、CHANGELOG - docs/design/current/composer/design.html §07:第一档措辞「全部批准 · 改文件/执行命令不再询问」,映射改成 「每条消息自带 agent」的真实机制,写明「全部批准」不取消的那几类审批;components.md 登记 #perm 一行。 - docs/architecture/2026-09-23-runtime-permission-tiers.md 加 §11 后记(以上各节保留为落地前的地面真相); docs/README.md 索引行同步。 - docs/architecture/2026-09-08-context-injection-registry.md:第四个 agent alpha-ask(只登记 description,无 prompt)。 - scripts/gate-files.tsv:登记 packages/opencode/test/permission/alpha-composer-tiers.test.ts(精确 4 条); docs/architecture/quality-gate-environments.md §3.6 同步。 - CHANGELOG [Unreleased] Fixed:用户可见变化与诚实边界。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…:提到的测试文件必须已分类) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…erdict>,让 PR / CI 日志直接读得到实测 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…入 alpha-ask 的非测试新增,理由写在 TSV 同一行 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
jinjunnn
added a commit
that referenced
this pull request
Sep 24, 2026
* feat(egress): 模型即时生成的目的地有了出口 —— 授权判据的第三个半场 = 用户当场批准
`webfetch` 的 URL 与 `bash` 里命令自带的目的地由模型在调用那一刻产生,没有、也不该有
配置来源。而出网放行集合的两个半场共用一条规则「有围栏外的真源才进得来」,于是这两条轴
整类被我们自己的围栏 403 —— 连「用户想让它过」都没有任何地方可以表态。
实测(真 sandbox-exec + 生产渲染 profile + 真 /usr/bin/curl + 生产策略代理):
改前 en.wikipedia.org / example.com / raw.githubusercontent.com → CONNECT tunnel failed, 403
自证臂:无围栏 example.com 200;围栏+代理 github.com(静态表内)200 / 576 869 B
改后 同三个目的地 200,wikipedia 取回 129 603 B、example 559 B
修法照搬文件轴那个已经在跑的形状(有真源 + 有出口):
· 真源 <casBaseRoot>/egress-grants/<env>.json,与 mcp-servers/ 同父目录,只有 main 写;
真 seatbelt 实测:围栏内改写它 / 建它的目录都是 Operation not permitted,
而同一份 profile 下 W2 的 alpha.jsonc 写得进、不套围栏也写得进(三条对照臂)。
· 出口:代理判出 unregistered 的那一刻先问用户(main 的原生对话框,围栏之外),
答「允许」才建隧道;勾「记住」才落盘。
· 判据仍然只有 isEgressAuthorizedForSidecar 一处 = 静态 ∪ 配置派生 ∪ 用户批准。
降级方向全部 fail-closed(没接通道 / 无窗口 / 超时 / 超并发 / 拒绝 ⇒ 仍是那条 403,
字节逐字不变;没问过的不写 grant 记录)。反向实测:私网字面量 192.168.1.1 连问都不问、
loopback 结构上到不了代理、没批准过的 evil.example.com 仍 403。
方案与安全面(十条类边界 + 八条不变量 + 八个被否决的替代 + 四项已知残留)见
docs/design/2026-09-23-model-chosen-egress-baseline.md。
Fixes #1412
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(egress): R1 裁决落地 —— 把 K3/K4 改成实话,更正一条前提为假的理由,撤销文案说出「并重启」
零判据变更:本轮只让记录与文案说实话。
一、K3/K4 的覆盖面此前写过头了。审计实测:准入用**文本形状**判(只认点分四段),
拨号用 `getaddrinfo`(接受 inet_aton 全部短写)—— 两套解析器不一致,于是
`2130706433` / `127.1` / `0x7f.1` / `10.1` / `192.168.1` / `169.254.43518` /
`0300.0250.1.1` 七条**全部**通过 `admitUserEgressGrant` 与 `parseConnectAuthority`,
而本机 `dns.lookup` 把它们归一到 loopback / 私网(`169.254.43518` → 169.254.169.254)。
基线新增 **K3′** 如实登记:成因、七条实测读数、受影响面(`bash` 轴里不归一的客户端:
python3 urllib / nc / openssl / wget / 手写 socket;webfetch / curl / git 因归一不受影响)、
以及将来关它的最小修法与它不影响 198.18/15 的实测。
**owner 2026-09-24 裁决接受这个绕过** —— 代码一行未改,判据刻意不加,§6 判据地图同步写明。
二、§7.2 与 network-egress-grants.ts 里「照解析结果立闸会拒载真实配置」是**前提为假**的论证。
fake-IP 拓扑下 `dns.lookup` 恒返回 198.18.x.x(本轮实测 en.wikipedia.org → 198.18.7.205),
一个不含 198.18/15 的解析后判据在这台机器上**永不触发** —— 它是空转,不是拒载。
真实理由只有一条:那样一道闸只保护非 fake-IP 的机器,而今天没有那样的租户。按实情改写,
并写明将来的落点(defaultDial 给 net.connect 传 lookup 钩子,且必须放行 198.18/15)。
三、撤销要重启才生效,而两处文案说「删掉记录就行」。批准集合只在代理单例创建时装载一次、
没有任何重读 ⇒ 用户删了记录本次运行照样放行。`egress-grant-records.ts` 的日志与基线 §7.4
各加「并重启应用」,server.ts 接线处补一段说明。这是产品在说假话,与安全面无关。
四、顺手:两处注释写「requestGrant 恒答 false」,实际是 `not-asked` 三态。
另:四份勘破已进 alpha(f245627),把此前刻意留成纯文本的两处引用还原成相对链接。
模块体积棘轮基线随 rebase 重算 59,075 → 59,713(+638 = 新增非测试文件 541 + 已有文件净增 97,
逐项对得上,全部是本票的)。
Refs #1412
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(egress test): 「默认接线」这四个字在断言里必须是真的 —— 询问通道是进程级单例,全量跑时会漏
`bun test src/main` 实测(全量,非单跑):network-egress-proxy.test.ts 里两条断言**精确日志**的臂红,
各多出一条 `egress.grant` 记录 —— 而 403 / 零拨号 / 拒绝正文**一个字节都没变**,变的只有
「这一次有没有人被问过」。
成因:`configureEgressGrantApproval` 是**进程级单例**,生产里一个进程一个应用装一次即可;
但全量把所有测试文件跑在同一个进程里,而好几个文件会驱动生产的 `ensureEgressPolicyProxy`
(server.ts,它们没注入 `egressProxy` 替身)⇒ 真 approver 被装上并漏进后面每一个文件。
于是那两条臂测的不是「生产默认接线」,而是「上一个文件剩下什么」。
修法是让那四个字为真:proxy 测试 `beforeEach` 先 `__resetEgressGrantsForTests()`。
不动任何生产判据 —— 本轮 owner 已裁决不改安全面。
`configureEgressGrantApproval` 抬头写明这条陷阱与它的判据落点,免得下一个人重新诊断一遍。
读数:`bun test src/main` 3563 pass / 2 fail → **3565 pass / 0 fail**;proxy 文件单跑仍 17/17
(单跑一直是绿的 —— 这正是它难认的地方)。棘轮 59,713 → 59,720(+7,全是那段注释)。
Refs #1412
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(egress): K3′ 那一格不是假想的 —— 审计用的 11434 上确有 ollama 在跑
上一轮把 K3′ 写成「短写绕过可以到 loopback / 内网」,举的是审计的 `0x7f.1:11434` 实录。
本轮复跑那份探针时它在 `target.listen(11434)` 上失败(EADDRINUSE),追下去发现原因本身就是证据:
`lsof -nP -iTCP:11434 -sTCP:LISTEN` ⇒ `ollama` pid 842 LISTEN 127.0.0.1:11434。
即:审计随手挑的那个端口正是 ollama 的默认端口,而 owner 本机上确实常驻着一个 ollama ——
这一类绕过在这台机器上到得了一个**真在跑的本机服务**,不只是合成靶站。已知接受的残留要按
真实代价记,所以这一句补进 K3′。
顺带钉住它与 network-egress-registry.ts「产品面当前没有任何本地模型入口」**不矛盾**:
那句说的是我们的产品不去连它;这里说的是围栏内的代码借这个绕过可以连它。两句都成立。
裁决不变(owner 2026-09-24 接受绕过),代码一行未改。
Refs #1412
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(ratchet): rebase 后按实测重算 tree 基线 59,117 -> 59,762
#1436 与 #1426 都抬了同一行。rebase 时取 HEAD 侧(59,117,#1436 的值)让 rebase 走完,
再按最终树实测重算 —— 59,762 = 59,117 + 645,本票增量 645 不变(新增文件 541 +
已有文件净增 97 + 注释 7,逐项对得上),变的只是基数。两边的理由都保留在该行。
不这么做的两种错法:选小的一边 ⇒ 门红;选大的一边 ⇒ 悄悄放松棘轮。
Refs #1412, #1413
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: jinjunnn <slmbaovanetti99@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
jinjunnn
pushed a commit
that referenced
this pull request
Sep 24, 2026
由编排者代解冲突(owner 2026-09-24 授权先合本 session、再帮另一 session 合)。 两处冲突: - CHANGELOG.md:两条都是新增条目,都保留。 - scripts/module-size-ratchet.tsv 的 tree 行:基线与此前全部理由取 alpha 那侧 (59,762,含 #1436 / #1426 两次抬高),本票增量按合并后实测重算 = 59,813(+51)。 分支上原写的 59,126 是按旧基数算的,直接取会让门红。 合并后 assert-module-size 三行全 = 基线,且 #1438 新加的「半份登记」交叉判据未告警。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1413
用户看到什么
输入框「运行权限」菜单从两项变成三项,每一项都做它名字说的事:
agentbuild)alpha-askalpha-readonly改了什么(M)
alpha-config-injection.ts:多注入一个 hidden agentalpha-ask= build +edit: ask+bash: ask(显式带上 build 对 defaults 的
question / plan_enter: allow覆盖,否则「请求审批」会比默认档少掉 question 工具;刻意无 prompt —— 给了 prompt 就会在
llm/request.ts:64顶掉整份底座提示词)。ALPHA_INJECTED_AGENTS加入它(治理不许 disable/hide)。composer-state.ts:PermMode = "allow" | "ask" | "readonly";提交层不再是if (readonly) … else …的二元分支,改为穷举表
PERM_AGENT: Record<PermMode, string | null>(少一档 typecheck 红);新会话默认档 =allow。alpha-composer.tsx三个menuitemradio由PERM_MODES生成,标签/副标题/图标同样按Record<PermMode, …>穷举;ask与readonly一样压过手选档位(计划 chip 置灰,title 点名是哪一档压的;Shift+Tab、装配弹窗计划行同步)。context-injection.ts:允许没有 prompt 的 agent(只登记 description),库存快照重生(44 → 45 段);ui-mac 咽喉
config-injection-throat.test.ts改为消费登记簿自己算的 id 集,并断言alpha-ask不写 prompt 键。permAllow/permAllowHint新增;planReadonly→planLocked(带档名参数)、readonlyPlan→permLockedPlan。[data-mode="allow"]用--a-warning(活稿 design.css.chip.perm[data-mode="full"]同款);ask中性色;readonly不变。AC 实测(全部是真跑的输出,不是推导)
AC1 —— 引擎层真
Permission.Service.ask(packages/opencode/test/permission/alpha-composer-tiers.test.ts,4 pass / 0 fail)真
injectAlphaConfig(临时盘,不读 owner 配置)→ 真Agent.Service→ 真Permission.ask,ALPHA_PERMISSION_ASK_TIMEOUT_MS=300:unanswered=Permission.UnansweredError(instanceofRejectedError,processor 据此把回合置 blocked),pending 全程清空。另一条用例 AC1':
alpha-ask+ bash 进list(),reply: once之后 Exit 成功 —— 是问不是拒。矩阵用例:
alpha-ask与build对每一把内置权限键的evaluate只在bash/edit上不同,Permission.disabled工具表逐字相同,external_directory/doom_loop/read *.env两者都仍是ask(没有顺手放宽),alpha-ask.prompt === undefined。变异实测(做完已还原,
git status干净):alpha-config-injection.ts把alpha-ask的bash: "ask"改成"allow"⇒ 该文件 1 pass / 3 fail(AC1、AC1'、矩阵三条当场红)。AC4 —— 三档提交的请求体逐字节不同(生产
buildPromptRequest直接打印)composer-state.test.ts把这三个字符串钉成精确断言(17 pass)。变异实测:PERM_AGENT.ask改成null(= REQ-126 退休掉的那个假档形状)⇒composer-state.test.ts2 条 +shell-commands.test.ts1 条当场红(36 pass / 3 fail),还原后回绿。AC2 —— 真壳里三档各自可选、标签各自正确(
shell-commands.test.ts,22 pass)真
PermChip挂载 → 3 个menuitemradio、标签两两不同、每项都有非空副标题槽 → 逐档真点 → 提交层 agent{allow: undefined, ask: "alpha-ask", readonly: "alpha-readonly"}三个互不相同;composer-a11y.test.ts的可达性用例改成 3 项。AC3 —— 只读逐字不变
注入面
alpha-readonly的 permission 一格未动;config-injection-throat.test.ts13/13、alpha-config-injection.test.ts14/14;引擎层上表两条
denied;真组件 cases(alpha-composer-model.cases.ts#884 ⑤–⑯、new-session-workspace.cases.ts#891/#894)里所有alpha-readonly载荷断言原样保留,只把「默认档」从"ask"改成"allow"、「退出只读的那一行」从「请求审批」改成「全部批准」。本地门(真实输出)
error TS计数;opencode 那份在把injectAlphaConfig改成非字面量路径动态 import 之前是 3 条 —— 静态 import 会把 ui-mac 主进程整张图拖进 opencode 的 tsconfig)packages/extcontext-injection.test.ts:31 pass / 0 fail(条数不变,登记簿快照 diff 只多agent.alpha-ask.description一行)packages/opencodealpha-composer-tiers.test.ts:4 pass / 0 fail(已登记scripts/gate-files.tsv,精确 4 条)packages/ui-mac定点:composer-state 17、throat 13、injection 14、shell-commands 22、a11y 2、autocomplete-core、locale-regression、builtin-policy、cloud-web-search 全绿;两个组件宿主(
alpha-composer-model.component.test.ts/new-session-workspace.component.test.ts)2 pass;gate-file-registry.test.ts22/22packages/ui-mac全量bun test src:见下方「与 base 的差」✓ zero upstream package edits✓ 96 relative link(s) resolve across 6 file(s);git diff --check干净alpha-check.sh14 步):见下方与 base fail-set 的差
base(
origin/alpha@8c8867c90,主 session 给的 fail-set):5321 pass / 0 fail。本分支(pre-push 钩子里跑的那一遍,树 =
14ac7b986,随后99130e4b9只改 TSV 一行、钩子再跑一遍同样 5323 / 0,ALPHA_KNOWN_FAILS_FILE口径):5323 pass / 0 fail(Ran 5323 tests across 382 files)。差集:+2 条新用例(
composer-state.test.ts的「请求审批 → alpha-ask」与「AC4 三份请求体」),0 条红,0 条消失。另:ext 208/208,contracts-consumer 60/60,app 724/724;[6/14] 逐文件精确条数全绿(含新登记的 4 条)。
pre-push 钩子(
alpha-check.sh14 步)结果:[1]–[10]、[12] 全 ✓;[11/14] 3 个 BYOK provider 本机无 key ⇒ 那几条「未验证」(与本票无关);[13/14] 只量不拦;[14/14] 模块体积棘轮响了(不拦 push):
packages/ui-mac/src/main实测 59117 > 基线 59075(+42)—— 这 42 行全是本 PR 的非测试新增(alpha-config-injection.ts+30、alpha-agents.ts+12),按棘轮的规矩已在本 PR 里人手把那一行基线抬到 59117 并在 TSV 同一行写明理由(
99130e4b9;第二次过钩子✓ packages/ui-mac/src/main 59117 行(= 基线),全量仍 5323 / 0);ext-install-planner.ts3539 > 3525(+14)不是本 PR 的(origin/alpha上就是 3539),没有动它。阈值告警(>800 行的被改文件
alpha-composer.tsx1861 /en.ts1887 /zh.ts1847)都是既有大文件各加了几行,本 PR 不拆它们。环境(按《本机验证陷阱》补齐后再量):
electron/{dist,path.txt}软链主 checkout;alpha_fence.node用bun run --cwd packages/ui-mac build:fence-addon在本树现编。第一遍全量是在我边改边跑时量的(污染,15 条红全部可归因于改到一半的默认档),作废不采;上面的数字取自钩子里对最终树的那一遍。
文档影响
design(活稿 §07 改成落地形状 +
components.md登记#perm)、architecture(勘破文档 §11 后记、context-injection 登记簿文档第四个 agent、quality-gate-environments §3.6)、release(
CHANGELOG.md [Unreleased] Fixed)、gate registry(scripts/gate-files.tsv+1 行)。没有出新的设计增量稿:活稿 §07 自最初提交起就是三档,本票只把第一档的措辞与「映射」注改成落地的机制。我主动没做的(请主 session 裁)
composer-state.ts的DEFAULT_PERM一处(测试里的默认断言跟着改)。alpha-plan-ask(票面边界:不动 plan 档)。settings.tsx:561-577,勘破 §8.1 说它今天开了也没有可命中的记录)与本菜单语义重叠,未动。要不要退休它,是另一张票。alpha-readonly注入面上那条与实测矛盾的注释(alpha-config-injection.ts:201-203「question/task 允许」,实测question = deny)未改,只在本 PR 的设计上没有照它做。alpha-ask没有逃生 env(其余三个 alpha agent 各有ALPHA_*_DISABLE):agent 缺席 = 引擎具名报错(fail-closed),不是静默回到全放行;若要对齐可另加。alpha-readonly带自己的prompt,按llm/request.ts:64它会整段顶替底座提示词 —— 只读档今天跑在一句话的系统提示上。本 PR 没有动它。🤖 Generated with Claude Code