Skip to content

fix(worker/ocs): read-only 模式未拦截文件写入#913

Merged
hrygo merged 7 commits into
mainfrom
fix/ocs-readonly-permission
Jul 20, 2026
Merged

fix(worker/ocs): read-only 模式未拦截文件写入#913
hrygo merged 7 commits into
mainfrom
fix/ocs-readonly-permission

Conversation

@hrygo

@hrygo hrygo commented Jul 17, 2026

Copy link
Copy Markdown
Owner

问题

OCS (OpenCodeServer) Worker 的 workspace 只读约束失效:workspace 设为 read-only (plan 模式) 后,Agent 仍能直接改文件;workspace / auto-edit 层级的工作区边界也没被实际执行。

根因

OCS 权限系统是 opt-in allowlist:工具调用未命中任何规则时,默认动作是 ALLOW,既不 ask 也不 deny。原代码在 plan 模式只注入了 read: allow 一条规则,假设未匹配的操作会被拦截——实际上 edit / write / bash / shell / patch 全部直接放行。实测确认:即便给 edit 单独 deny,Agent 也会绕道 bash (echo > file) 完成写入。

更深的两个问题:

  1. 首启即漏:workspace 权限上限只在 worker resume 时应用,首个 worker 启动时 CreateWithBot 返回的 snapshot 未同步 WorkspaceID,workspace 边界对首次会话不生效。
  2. 静默拒绝:即使 deny 所有写入,Agent 的写入尝试被无声丢弃,用户看不到任何审批卡,体验上像是"卡住"。

修复

按 6 个维度收紧 OCS 权限执行。

1. plan 模式从 ask-all 基线开始 (而非仅注入单条 read)

internal/worker/opencodeserver/commands.go — plan 模式注入 *:ask 通配基线,再选择性 allow 内建只读工具 (read / glob / grep / list / lsp / webfetch / websearch)。写入操作因此变成权限请求,而非被静默丢弃或绕行。custom / MCP / subagent 工具也落在 ask 基线上,不会形成绕过通道。

演进:本 PR 第一版是 *:deny 基线;实测发现写入被无声拒绝且无审批卡,改为 *:ask 让用户能看到并裁决每一次写入。

2. 工作区边界:外部目录需审批

commands.goacceptEditsworkspace 层级显式注入 external_directory: ask,工作区内的 edit 已 allow,工作区外则触发审批。auto-edit 作为用户明确选择的 no-prompt 层级,external_directory 走 allow,保留其语义。同时把 allowed_tools 白名单的注入限制在非 plan 模式下,避免在 read-only 基线上被一条 caller-supplied allow 规则重新打开写入。

3. 首个 worker 即应用 workspace 上限

internal/gateway/bridge.goSetWorkspaceID 持久化了绑定,但无法改 CreateWithBot 返回的 snapshot。把 si.WorkspaceID 同步回 snapshot,使首个 worker (而非只有后续 resume) 即受 workspace 权限上限约束。

4. 权限应用失败不再被吞掉

internal/worker/opencodeserver/worker.goinitSessionConn 从无返回值改为返回 error;applyPermissions 失败时清理 cmd 并向上冒泡,Start / Resume 路径相应释放资源。原先只打一条 warn 继续跑,worker 实际处于"权限未应用"的裸状态。

5. 命令失败不再误发"已交付"ack

internal/gateway/handler.gohandleInput 在 control command 返回 err 时跳过 ackControlCommand,避免命令失败后 UI 因合成 "delivered" ack 假冻结。

6. webchat 结构化渲染审批参数

webchat/components/assistant-ui/tools/PermissionApprovalCard.tsx — 解析权限工具参数 (JSON string / object / array 三种形态),分区块渲染 command / action / target / reason / 允许目录 / 允许模式,取代原来的原始 JSON dump。extractFilePath / extractFileContent 扩展字段名变体 (filePath / filepath / file / filename / replacementContent / codeContent / replacement / text),兼容 OCS 侧 arg 结构。新增文案同步进 en / zh-CN。

最终规则集

模式 规则
plan (read-only) *:ask 基线 + read/glob/grep/list/lsp/webfetch/websearch allow
acceptEdits / workspace read+edit allow,external_directory: ask
auto-edit read+edit+external_directory 全 allow
bypassPermissions allow-all

测试

  • internal/worker/opencodeserver/commands_test.go / worker_test.go 覆盖各模式规则集、allowed_tools 在 plan 模式被忽略、external_directory 分层、initSessionConn 错误冒泡
  • internal/gateway/conn_test.go 验证首启 workspace 绑定;command_detection_test.go 覆盖命令失败路径
  • 全量 go test -race 通过;webchat tsc + ESLint 通过
  • 人工实测:plan 模式下 Agent 60+ 秒未能改文件,写入以审批卡形式呈现

文档

docs/guides/developer/remote-coding-agent.mdsecurity-model.md 同步更新 OCS 权限语义。


Refs #789

… projectDir to OCS

- Gateway: Send a synthetic delivered InputAck when a control command is intercepted to avoid UI lockup.
- OpenCode Server Worker: Propagate the projectDir to OCS REST API calls via the directory query parameter to satisfy WorkspaceRoutingMiddleware.
OCS's permission system is an opt-in allowlist: when no rule matches a
tool call, the default action is ALLOW — not ask or deny. The previous
plan mode only injected a read-allow rule and assumed unmatched
operations would be blocked. In reality, edit/write/bash/shell/patch
all went through without any approval prompt.

Additionally, each write tool must be denied individually because the
agent can pivot between them (e.g. fall back to bash when edit is
unavailable — confirmed empirically).

Fix: inject explicit deny rules for edit, write, bash, shell, and patch
in plan mode. Updated the misleading comment on the default case as well.
Parse permission tool args (JSON string / object / array) and render
command, action, target, reason, allowed directories and patterns in
dedicated sections instead of a raw JSON dump; surface toolAction /
toolSummary as the card description.

Expand extractFilePath / extractFileContent to accept more field-name
variants (filePath, filepath, file, filename, replacementContent,
codeContent, replacement, text) so OCS-side arg shapes render diffs.

Localize the new labels and the 'unknown file' fallback in en/zh-CN.
@hrygo
hrygo merged commit 41dd527 into main Jul 20, 2026
9 checks passed
@hrygo
hrygo deleted the fix/ocs-readonly-permission branch July 20, 2026 07:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants