Skip to content

feat(coding-agent): OpenCode CLI 适配器,语音 Agent 可选 OpenCode 后端 (Refs #579)#638

Open
appergb wants to merge 3 commits into
betafrom
feat/issue-579-opencode-adapter
Open

feat(coding-agent): OpenCode CLI 适配器,语音 Agent 可选 OpenCode 后端 (Refs #579)#638
appergb wants to merge 3 commits into
betafrom
feat/issue-579-opencode-adapter

Conversation

@appergb

@appergb appergb commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

User description

背景

Refs #579#579 要的「语音快捷键触发 Claude Code / OpenCode Agent」中,Claude Code 后端已随 fast-agent / less-computer 落地,但 OpenCode 后端只有占位:coding_agent_provider 已有 opencode-cli 选项(标「即将支持」),但后端从不读它、永远跑 claude。本 PR 把 OpenCode 真正接上,照既有 coding_agent 适配模式实现,不另造架构

改动清单

后端

  • coding_agent/opencode.rs(新增):OpenCode 适配器,与 Claude 适配器同形、复用同一套 CodingAgentRequest / CodingAgentEvent / CodingAgentError:
    • detect_opencode:opencode --version(复用 parse_claude_versionx.y.z)。
    • build_opencode_args:opencode run --format json --model <provider/model> --dir <cwd> [--dangerously-skip-permissions];prompt 作为 argv 位置参数(OpenCode 从命令行读,不像 claude 从 stdin)。
    • parse_opencode_json_line:解析 OpenCode NDJSON(text → Delta、tool_use → ToolUse、error → Error);OpenCode 的 text 是完整文本块且无带 cost 的终局 result 事件,故累计文本、EOF 处合成 Completed(cost_usd = None)。
    • run_opencode_agent:取消/超时 kill 子进程;护栏经 OPENCODE_CONFIG_CONTENT 注入。
  • coding_agent/args.rs:CodingAgentProvider 枚举(claude-code-cli / opencode-cli,未知回落 Claude)+ from_pref / default_exe
  • coding_agent/guard.rs:build_opencode_guard_config —— 把高风险 bash 前缀翻译成 OpenCode permission.bash 的 deny glob(如 "rm -rf *": "deny")+ webfetch: deny,审批放行的前缀显式 allow。这是 Claude --settings deny 护栏在 OpenCode 的等价物。
  • coordinator/dictation_voice_agent.rs:run_less_computer_oncecoding_agent_provider 分派 Claude / OpenCode 运行器。两条路径都 fail-closed(护栏配置生成失败即中止,绝不无护栏裸跑)。审批拦截探测 + 重跑放行链路本就 provider 无关,自动复用(OpenCode 重跑时把放行前缀传入 guard builder)。
  • coding_agent/commands.rs + lib.rs:coding_agent_detect_opencode 命令。

前端

  • CodingAgentSection.tsx:OpenCode 选项从「即将支持」改为可用;选中 OpenCode 后端时探测安装状态并提示(已安装显示版本 / 未安装提示先 npm i -g opencode-ai + opencode auth login)。
  • lib/ipc.ts:codingAgentDetectOpencode + OpenCodeDetection
  • i18n:opencodeReady / opencodeMissing(zh-CN / en / ja / ko / zh-TW;替换原 providerOpenCodeSoon)。

安全

  • 沿用语音→shell 路径既有安全姿态:bypassPermissions 在语音路径降级为 acceptEdits 等价(保留护栏);OpenCode 用 permission deny 高风险 bash + 禁 webfetch,fail-closed。
  • prompt 不进 argv 泄露? 注:OpenCode 设计上 prompt 走 argv(CLI 约定),这与 claude 走 stdin 不同 —— 会出现在进程列表里。语音转写一般非敏感,但已在 PR 描述标注;如需可后续探索 opencode serve + stdin 方案。

测试情况

  • cargo check(macOS)✅
  • cargo test --lib:492 通过(新增 10 个单测:provider 解析/默认 exe、OpenCode args/stream 解析、OpenCode 护栏 deny/放行)✅
  • npm run build(tsc + vite)✅
  • 真机实测待办:① 安装 opencodeopencode auth login;② 设置→Less Computer 选 OpenCode 后端,确认安装提示正确;③ 语音触发一次任务,确认流式输出进聊天浮窗、能落地改动;④ 触发一次高风险命令(如 rm -rf)确认被护栏拦截并弹审批卡,Approve 后重跑放行。

为什么是 Refs 而非 Closes #579

#579 还含「语音润色模型配置」一项:prefs 字段(coding_agent_use_voice_polish / voice_polish_model_mode / voice_polish_provider_id / voice_polish_thinking_enabled)已存在但全链路未实现——语音 Agent 当前把原始转写直接送 agent,未经语音润色模型整理,也无对应 UI。该项不在本 PR 范围,故用 Refs #579。建议作为 #579 的剩余子项单独跟进。

🤖 Generated with Claude Code


PR Type

Enhancement, Tests


Description

  • OpenCode CLI 适配器,语音 Agent 可选 OpenCode 后端

  • 护栏与参数注入防护(-- 标记)

  • 新增可执行路径偏好配置

  • Agent 运行器按 provider 分派(Claude / OpenCode)


Diagram Walkthrough

flowchart LR
  User["用户语音"] --> ASR["ASR 转写"]
  ASR --> Polish["语音润色(可选)"]
  Polish --> AgentRun["Agent 运行器"]
  AgentRun --> Provider{"Provider"}
  Provider -- "Claude Code CLI" --> Claude["claude -p --settings"]
  Provider -- "OpenCode CLI" --> OpenCode["opencode run --format json"]
  Claude --> Stream["流式事件"]
  OpenCode --> Stream
  Stream --> Panel["Agent 面板"]
Loading

File Walkthrough

Relevant files
Enhancement
9 files
args.rs
添加 CodingAgentProvider 枚举及其解析与默认可执行路径                                       
+55/-0   
commands.rs
添加 coding_agent_detect_opencode 命令                                             
+40/-0   
guard.rs
添加 OpenCode 护栏配置函数与高风险命令前缀                                                             
+107/-0 
opencode.rs
新文件:OpenCode CLI 适配器(检测、参数、解析、运行器)                                             
+368/-0 
dictation.rs
按 provider 分派运行器,支持 coding_agent_exe 配置                                   
+117/-61
types.rs
添加 coding_agent_exe 字段到 UserPreferences                                   
+9/-0     
coding-agent.ts
添加 OpenCodeDetection 接口和检测函数                                                         
+20/-0   
types.ts
添加 codingAgentExe 字段到 TypeScript 接口                                           
+2/-0     
CodingAgentSection.tsx
更新 UI:OpenCode 选项、安装提示、可执行路径输入                                                     
+51/-1   
Miscellaneous
3 files
mod.rs
导出 opencode 模块、修改可见性                                                                         
+8/-4     
lib.rs
注册新命令 coding_agent_detect_opencode                                             
+1/-0     
index.ts
导出新类型和函数                                                                                                 
+2/-0     
Documentation
5 files
en.ts
更新英文 i18n:OpenCode 提示与可执行路径                                                           
+3/-1     
ja.ts
更新日文 i18n:OpenCode 提示与可执行路径                                                           
+3/-1     
ko.ts
更新韩文 i18n:OpenCode 提示与可执行路径                                                           
+3/-1     
zh-CN.ts
更新简体中文 i18n:OpenCode 提示与可执行路径                                                       
+3/-1     
zh-TW.ts
更新繁体中文 i18n:OpenCode 提示与可执行路径                                                       
+3/-1     
Tests
2 files
mock-data.ts
添加 codingAgentExe 字段到 mock 数据                                                       
+1/-0     
stylePrefs.test.ts
添加 codingAgentExe 字段到测试数据                                                               
+1/-0     

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit 331bafe)

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

579 - Partially compliant

Compliant requirements:

  • OpenCode CLI 适配器实现(opencode.rs):检测、参数构建、NDJSON 解析、运行器。
  • 护栏配置函数 build_opencode_guard_config:高风险 bash 前缀 deny、webfetch deny、文件级 deny。
  • Provider 枚举 CodingAgentProvider 及 from_pref / default_exe。
  • 检测命令 coding_agent_detect_opencode 并返回版本/安装状态。
  • 设置页添加 OpenCode 选项,并显示安装/未安装提示。
  • 可执行文件路径配置字段 coding_agent_exe 前后端。
  • 权限模式映射:Plan 时不传 --dangerously-skip-permissions,其他模式传。

Non-compliant requirements:

  • 语音润色大语言模型配置(voice polish model mode / custom provider etc.)。
  • Voice Coding Agent 前端面板(VoiceCodingAgentPanel.tsx)。
  • 语音 Agent 热键触发状态机(协调器部分未改动)。
  • 历史记录、超时/取消前端处理(后端已实现但前端无对应 UI)。

Requires further human verification:

  • OpenCode 权限模式 --dangerously-skip-permissions 与 guard 配置的交互行为:该 flag 是否完全绕过权限系统,导致护栏配置被忽略,需要实际测试验证。
  • OpenCode NDJSON 输出格式是否与解析匹配(text 块是否完整?是否有其他字段?)。
  • 文件级 deny 中的 tilde 表达式(~/Library/LaunchAgents/**)在 OpenCode 中是否支持。
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 Security concerns

Sensitive information exposure:
The prompt is passed as a command-line argument via .arg(&req.prompt). On Linux, command arguments may be visible to other processes via /proc/self/cmdline. This could leak sensitive user prompts. Consider passing prompt via stdin (like the Claude adapter does) or using platform-specific argument hiding mechanisms. Additionally, the --dangerously-skip-permissions flag may bypass the guard config's deny rules (see key issue above), which is a security concern that requires verification against OpenCode's actual behavior.

⚡ Recommended focus areas for review

Missing final event on error

In run_opencode_agent, when an error event is parsed from the NDJSON stream (e.g., agent returns an error), the code sets got_error = true and sends the error event to the sink. However, after EOF, the function checks outcome.is_ok() and then conditionally sends a Completed or Error event only if status.success() && !got_error, or if !status.success() && !got_error. When got_error is true but the process exits successfully (status.success() == true), neither branch executes, and the function returns Ok(()) without sending any final event (Completed or Error) to the sink. This leaves the frontend without a terminal event, potentially causing the session to hang indefinitely.

loop {
    tokio::select! {
        biased;
        _ = wait_cancel(&cancel) => {
            let _ = child.start_kill();
            let _ = sink.send(CodingAgentEvent::Cancelled { session_id: req.session_id.clone() });
            outcome = Err(CodingAgentError::Cancelled);
            break;
        }
        _ = tokio::time::sleep_until(deadline) => {
            let _ = child.start_kill();
            let _ = sink.send(CodingAgentEvent::Error {
                session_id: req.session_id.clone(),
                message: format!("运行超时({}s)", req.timeout_secs),
            });
            got_error = true;
            outcome = Err(CodingAgentError::Timeout(req.timeout_secs));
            break;
        }
        line = lines.next_line() => {
            match line {
                Ok(Some(l)) => {
                    if let Some(ev) = parse_opencode_json_line(&req.session_id, &l) {
                        match &ev {
                            CodingAgentEvent::Delta { text, .. } => accumulated.push_str(text),
                            CodingAgentEvent::Error { .. } => got_error = true,
                            _ => {}
                        }
                        let _ = sink.send(ev);
                    }
                }
                Ok(None) => break, // EOF:正常结束
                Err(e) => {
                    outcome = Err(CodingAgentError::Io(e.to_string()));
                    break;
                }
            }
        }
    }
}

let status = child
    .wait()
    .await
    .map_err(|e| CodingAgentError::Io(e.to_string()))?;

// 正常 EOF 收尾(没取消/超时/IO 错):进程成功且没报错 → 合成 Completed。
if outcome.is_ok() {
    if status.success() && !got_error {
        let _ = sink.send(CodingAgentEvent::Completed {
            session_id: req.session_id.clone(),
            text: accumulated.trim().to_string(),
            cost_usd: None,
            duration_ms: None,
        });
        return Ok(());
    }
    if !status.success() && !got_error {
        // 非 0 退出且没解析到 error 事件:补一条 Error(取 stderr 末行作摘要)。
        let stderr = match stderr_task {
            Some(t) => t.await.unwrap_or_default(),
            None => String::new(),
        };
        let summary = stderr.lines().last().unwrap_or("").trim().to_string();
        let _ = sink.send(CodingAgentEvent::Error {
            session_id: req.session_id.clone(),
            message: if summary.is_empty() {
                format!("agent 异常退出 (code={:?})", status.code())
            } else {
                summary
            },
        });
        return Err(CodingAgentError::ProcessExit(status.code()));
    }
}
Security: --dangerously-skip-permissions may bypass guard config

For permission modes other than Plan, the code passes --dangerously-skip-permissions to opencode. The comment assumes that the injected guard config (via OPENCODE_CONFIG_CONTENT) still applies even with this flag. However, if OpenCode's --dangerously-skip-permissions disables all permission checks including deny rules in the config, the guard becomes ineffective. This could allow unrestricted command execution. The behavior needs to be verified against OpenCode's actual implementation before this code is used with non-Plan modes in production.

if let Some(cwd) = &req.cwd {
    args.push("--dir".into());
    args.push(cwd.to_string_lossy().into_owned());
}
if skip_permissions_flag(req.permission_mode) {
    args.push("--dangerously-skip-permissions".into());
}
// end-of-options:其后由运行器追加的 prompt 一律按位置参数处理,不再解析成 flag。
args.push("--".into());
args

@H-Chris233

Copy link
Copy Markdown
Collaborator

@appergb 看一下这个参数注入风险

@H-Chris233 H-Chris233 added enhancement New feature or request area:agent Agent area needs-tests Needs tests labels Jun 11, 2026
appergb pushed a commit that referenced this pull request Jun 13, 2026
注入风险(@H-Chris233 评审):`opencode run` 的 prompt 作为最后位置参数,但之前缺
end-of-options 分隔。以 `-` / `--` 开头的 prompt(语音转写或被 prompt 注入诱导)会被
OpenCode 当作 flag 解析,可绕过护栏(混入 --dangerously-skip-permissions、--dir 改写
工作目录等)。修复:build_opencode_args 末尾追加 `--`,运行器把 prompt 接在其后;新增单测。

可配置路径(@pr-agent 指出之前 hardcode "opencode"):新增 prefs.coding_agent_exe,
语音 Agent 路径据此取 exe(留空回退默认 claude / opencode),检测命令亦用配置路径;
设置「高级 → Less Computer」新增「可执行文件路径」输入;补 5 语言 i18n。
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 7960ac4

吕柏青 and others added 2 commits June 13, 2026 18:30
)

把 OpenCode 当作与 Claude Code 同类的 coding agent CLI 接入,照既有 coding_agent
适配模式(不另造架构),复用同一套 CodingAgentRequest / CodingAgentEvent /
CodingAgentError / 审批护栏链路。

后端:
- coding_agent/opencode.rs(新):detect_opencode(opencode --version)、
  build_opencode_args(opencode run --format json --model --dir
  [--dangerously-skip-permissions],prompt 作为 argv)、parse_opencode_json_line
  (NDJSON:text/tool_use/error;text 块累计,EOF 合成 Completed)、
  run_opencode_agent(取消/超时 kill,护栏经 OPENCODE_CONFIG_CONTENT 注入)。
- coding_agent/args.rs:CodingAgentProvider 枚举(claude-code-cli/opencode-cli,
  未知回落 Claude)+ from_pref/default_exe。
- coding_agent/guard.rs:build_opencode_guard_config —— 把高风险 bash 前缀翻译成
  OpenCode permission.bash deny glob + webfetch deny,审批放行的前缀显式 allow。
- coordinator/dictation_voice_agent: run_less_computer_once 按 coding_agent_provider
  分派 Claude / OpenCode 运行器;两路都 fail-closed(护栏生成失败即中止,不裸跑)。
  审批拦截探测/重跑放行链路 provider 无关,自动复用。
- commands: coding_agent_detect_opencode 命令(已注册 lib.rs)。

前端:
- CodingAgentSection: OpenCode 选项从「即将支持」改为可用 + 选中后探测安装状态并提示。
- ipc: codingAgentDetectOpencode + OpenCodeDetection。
- i18n: opencodeReady / opencodeMissing(5 语言,替换 providerOpenCodeSoon)。

验证:cargo check + cargo test(492 通过)+ npm run build 均通过。

Refs #579#579 还含「语音润色模型配置」:prefs 字段已存在但全链路未实现/未接 UI,
转写直接进 agent 未经语音润色模型整理 —— 故用 Refs 不用 Closes。)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
注入风险(@H-Chris233 评审):`opencode run` 的 prompt 作为最后位置参数,但之前缺
end-of-options 分隔。以 `-` / `--` 开头的 prompt(语音转写或被 prompt 注入诱导)会被
OpenCode 当作 flag 解析,可绕过护栏(混入 --dangerously-skip-permissions、--dir 改写
工作目录等)。修复:build_opencode_args 末尾追加 `--`,运行器把 prompt 接在其后;新增单测。

可配置路径(@pr-agent 指出之前 hardcode "opencode"):新增 prefs.coding_agent_exe,
语音 Agent 路径据此取 exe(留空回退默认 claude / opencode),检测命令亦用配置路径;
设置「高级 → Less Computer」新增「可执行文件路径」输入;补 5 语言 i18n。
@appergb appergb force-pushed the feat/issue-579-opencode-adapter branch from 7960ac4 to 55cc808 Compare June 13, 2026 10:32
@H-Chris233

Copy link
Copy Markdown
Collaborator

@appergb 冲突解决一下

@HKLHaoBin

Copy link
Copy Markdown
Contributor

PR 合并冲突提示

当前状态: 本 PR 与 �eta 已为 CONFLICTING,合并前必须 rebase。

与其他 open PR 的重叠:

文件 相关 PR 说明
coding_agent/guard.rs #688 双方均大改此文件,#688 合入后 rebase 本 PR 需手工合并
coordinator/dictation_voice_agent.rs 与当前 �eta 已冲突
ypes.rs、i18n/*、ipc.ts #666 改动区域不同,一般可自动合并
lib.rs #690#666 改动较小,风险低

建议: 先 rebase 到最新 �eta;若 #688 已合,重点检查 guard.rs 与 dictation_voice_agent.rs。

@H-Chris233

Copy link
Copy Markdown
Collaborator

作者久不修复,开始认领并修复问题以合入

Conflict resolution:
- dictation_voice_agent.rs: deleted (beta moved logic to dictation.rs),
  ported provider dispatch to dictation.rs run_less_computer_once
- ipc.ts: deleted (beta split into lib/ipc/ modules),
  added OpenCodeDetection + codingAgentDetectOpencode to ipc/coding-agent.ts

Review fixes (post-merge):
- guard: add file-level deny rules (edit/write .env/.git/**/macOS
  persistence) to build_opencode_guard_config, matching Claude parity
- opencode: remove dead cancel.load() call after process exit
- dictation: replace hardcoded 'Claude' with provider-agnostic 'Agent'
  in 3 error/status messages reachable from both backends
- commands: add ensure_main_window guard + path-traversal rejection
  to coding_agent_detect_opencode
- dictation: add deny_rule_for_pattern safety gate to approved_patterns
  filtering, preventing malformed OpenCode allow globs from
  unapprovable patterns (e.g. 'sudo ' → double-space glob)
- mock-data: add missing codingAgentExe field
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 331bafe

@H-Chris233 H-Chris233 self-assigned this Jun 22, 2026
@H-Chris233 H-Chris233 self-requested a review June 22, 2026 03:23
@H-Chris233

Copy link
Copy Markdown
Collaborator

@appergb 需要审核

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:agent Agent area enhancement New feature or request needs-tests Needs tests Review effort 3/5

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feature] 语音快捷键触发 Claude Code / OpenCode Agent,并支持语音润色模型配置

3 participants