dsh-acp-agentmode
Manifest validGive DeepSeek Harness's ACP profile a --agentmode <preset> flag: choose the agent preset every new ACP session runs. Vite+/pnpm/TypeScript, zero runtime deps.
dsh-acp-agentmode
给 dsh --profile acp(Agent Client Protocol 自动化模式)增加一个参数:启动时直接选定 agent preset。
dsh --profile acp --agentmode cordis # 本次进程的每个新会话都用 cordis preset
dsh --profile acp # 用 profile 配置的默认 preset(本包默认 standard)
dsh --profile acp --list-agentmodes # 列出本 profile 声明的 preset
默认不持久化:不带参数时复用当前 profile 里用户已设定的 preset(并在日志里标明来源);--agentmode 只覆盖本次启动;只有显式 --remember 才会把选择写进当前 profile。
⚠️ 设定按 profile 隔离(harness 设计,不是本插件的取舍):Web 界面里的"默认 preset"由 settings 服务写入当前 profile 的
cordis.patch.yml。桌面端 Web 界面 =desktopprofile,所以在那里设的值不会自动带到dsh --profile acp;acp 会退回本 bundle 声明的部署默认standard。要让 ACP 跟随某个默认,用--remember写入 acp profile(推荐),或把值放进共享 home patch$DSH_HOME/cordis.patch.yml(它会压住 Web 表单对该字段的写入)。
工具链:Vite+ 1.3(vp check / vp test / vp pack)+ pnpm + TypeScript 7,零运行时依赖。
安装、构建与卸载
pnpm install # 只装工具链(vite-plus / typescript / vitest / @types/node)
pnpm build # vp pack → dist/index.mjs + dist/index.d.mts + sourcemap
git clone https://github.com/MangMax/dsh-acp-agentmode.git
dsh plugin --profile acp add ./dsh-acp-agentmode
- 也可以直接从 GitHub Releases 下载 tarball:
dsh plugin --profile acp add ./dsh-acp-agentmode-0.3.0.tgz(未发布到 npm,刻意为之:这个包只在 acp profile 里有意义)。 - 该 bundle 只属于 acp profile。界面里的
plugin_manager只作用于当前 profile(本机是desktop),必须用dsh plugin --profile acp …。 - 安装会写 profile 的
dependencies并把dsh-acp-agentmode追加到dsh.profile.bundles末尾;patch 层按顺序应用,只有排在@deepseek-ai/dsh-acp-app之后的层才能覆盖它的行。 - profile 以 symlink 方式引用本目录,因此改完代码必须
pnpm build(harness 通过 packageexports加载dist/index.mjs)。 - 卸载:
dsh plugin --profile acp remove dsh-acp-agentmode,profile 回到“零参数 ACP”。
命令
| 命令 | 作用 |
|---|---|
pnpm check / pnpm check:fix | vp check:格式化 + oxlint(含类型感知与 tsc 类型检查) |
pnpm test | vp test:69 个单元测试(纯逻辑 + 假端口,无真实 harness) |
pnpm build | vp pack:ESM 库构建(含 .d.mts 与 sourcemap) |
pnpm smoke -- --agentmode minimal | 真实端到端冒烟:拉起 dsh --profile acp,走 initialize → session/new,再从会话日志断言 preset 真的挂上了 |
pnpm smoke | 同上,但走"无参数"路径,验证 profile 设定的复用 |
pnpm sync-presets | 从已安装的 @deepseek-ai/dsh-web-app 重新快照四个官方 preset 声明 |
pnpm verify | check && test && build,即 prepack 的完整门禁 |
目录与模块
src/constants.ts 限制、退出码、计时预算、preset id 语法(安全策略集中处)
src/ports.ts 四个协作方的最小结构接口(无 @deepseek-ai/cordis 版本绑定)
src/args.ts 严格参数解析(纯函数)
src/sanitize.ts 诊断文本净化(ANSI/控制字符/换行/路径)
src/errors.ts 错误码 + 退出码映射的唯一位置
src/log.ts 唯一写 stderr 的出口
src/selection.ts roster 等待与 resolve(带回退重试与截止时间)
src/compose.ts 会话与 preset 的绑定策略(select / mount / 降级)
src/plugin.ts Cordis 适配层:守卫、发布就绪、事件、清理
src/index.ts 包入口(导出 name/inject/apply 及公开错误类型)
tests/unit/*.test.ts 纯逻辑单测;tests/helpers/fakes.ts 手写假端口
scripts/sync-presets.ts / scripts/smoke-acp.ts
cordis.patch.yml + presets/*.patch.yml 组合层(见下)
设计要点:ports.ts 让插件不依赖具体 harness 版本的类型,也让人工假端口可以完整单测;plugin.ts 是唯一接触真实 Cordis 上下文的地方。
复用你的设定:读取 / 覆盖 / 持久化
registry 解析"新会话用哪个 preset"的公式是 selectedDefault ?? default:
default是本 bundle 的补丁声明的部署默认(standard),只是兜底;selectedDefault是 volatile 用户字段——Web 设置界面里"默认 preset"写的就是它,由 settings 服务持久化到当前 profile 的 Cordis 补丁层。
所以插件本来就是"读用户设定"的,--agentmode 只是本次覆盖。三种用法与来源标注:
| 用法 | 行为 | 启动日志 |
|---|---|---|
dsh --profile acp | 读 selectedDefault ?? default | agent preset: ptc (user setting) / (home/command-line override) / (deployment default) |
dsh --profile acp --agentmode cordis | 仅本次覆盖 | agent preset: cordis (from --agentmode) |
dsh --profile acp --agentmode ptc --remember | 通过官方 settings.update('agent-preset-registry', { selectedDefault }) 写入当前 profile 补丁,之后无参数即复用 | remembered agent preset: ptc |
--list-agentmodes 也会标出 (default, user setting) / (default, override)。
为什么 desktop/web 里的选择不会自动带到 ACP:settings 是按 profile 持久化的(写入 profileContext.patchPath),而 dsh --profile acp 是另一个 profile,有独立的补丁层与存储。想跨 profile 共用一份,两条路:① 在 acp profile 里 --remember 一次;② 写共享的 home patch $DSH_HOME/cordis.patch.yml(优先级高于 profile 补丁;注意此时 settings 表单写入会被拒绝,见上游 dsh-settings README)。插件不会去读别的 profile 的文件——那会破坏 profile 隔离。
它在 profile 里改了什么
cordis.patch.yml 四段,全部是普通 Loader patch:
- 接管命令行:
disable官方acp-app-startup行(零参数 commander program,任何额外参数都会error: unknown option),插入本包的acp-agentmode行,由它提供官方acp行 inject 的acpAppStartup服务。 - agent 平面搬进 preset:禁用 base 里 23 条“每 agent 各自贡献”的行(
tool-bash/tool-fs/tool-skill/tool-todo/tool-web/tool-subagent*/compaction-basic/agent-instructions/skill-filesystem…),与@deepseek-ai/dsh-web-app/cordis.patch.yml对 Web 面的拆分同构——ACP 一个进程服务多个会话,而这些 base 行是为“单会话、进程级组合”的表面保留的。 - 插入 preset 选择注册表:
agent-preset-registry,default: standard(仅在没有--agentmode时兜底)。 - 插入 host 支撑行:
subagent-model-selection-settings、plugin-inventory—— preset 子行要消费、base 不提供的 Host 服务。
工作原理
命令行:inject: ['cmdlineArgs'] → 严格解析 → assertAcpProfile() 守卫 → 同步发布 acpAppStartup → 后台解析 preset → 成功后才 arm stdin EOF。
就绪必须同步发布:App 层启动审计会独立结算 Loader 树,若让 readiness 等 registry I/O,慢一点就会被判成“启动失败”。解析失败则打印分类错误并以对应退出码退出。
会话绑定:dsh-acp 建 agent 时只传 meta: { cwd },从不挂 preset(Web 的 session controller 是在自己的 setup 里 agentPresets.mount)。所以从外部接上:监听 serial 事件 agent/created(agent loop 在释放排队输入之前 await 它),对空白会话调 agentPresets.select(agent, id) —— 这正是 Web UI 给“活着的空白会话”换 preset 用的公开 API,且 select 只在 recompose/mount 成功后写入 agent-preset/selected。session/resume 的会话已过首轮(select 会以 agent-preset/locked 拒绝),因此沿用日志记录的 preset 并用 agentPresets.mount() 组合。
两个启动期竞态:① 声明是异步注册的 —— 按 Loader 中声明行数等待 roster 到齐(有截止时间);② profile 的启动 patch 重载会重建 registry 行 —— 只对 agent-preset/not-found 做抖动退避重试,INACTIVE_EFFECT(实例被替换)交给替换实例。另有一个 watchdog:registry 始终不激活时报 E_ENVIRONMENT 并退出,而不是让客户端在“永远不会被组合”的会话上打转。
安全加固
| 面 | 措施 |
|---|---|
| 参数面 | 只接受 --agentmode / --agentmode= / --list-agentmodes / -h;重复参数直接报错(不做 last-wins);-- 之后不允许位置参数;未知选项不静默忽略 |
| 输入校验 | preset id 在进入 registry 之前按 ^[A-Za-z0-9][A-Za-z0-9._-]*$、长度 ≤ 64 校验:不允许空白、控制字符、前导分隔符(防止被下游当成 flag) |
| 组合守卫 | 启动时检查组合里确实有 @deepseek-ai/dsh-acp 行、且官方零参数启动行已被禁用;否则以 E_ENVIRONMENT 退出——误装到别的 profile 会响亮地失败而不是劫持它的 stdin/退出语义 |
| 服务发布 | 只发布一次;若别的 row 已发布 acpAppStartup,记警告并让位,不抛错、不崩溃 |
| 诊断卫生 | 所有 stderr 输出经 oneLine + redactPaths:剥离 ANSI/OSC 与控制字符(防终端劫持)、压成单行(防日志伪造)、长度截断、把 $HOME/$DSH_HOME/绝对路径替换为 ~ / <path> |
| 代码面 | 无 eval/Function/动态 import;不读 stdin(只挂 end 监听并在卸载时移除);不调用 process.exit(走 launcher 的 appExit);零运行时依赖 |
| 生命周期 | 每个实例一个 AbortController,卸载即中止所有等待并清理全部定时器,不留悬挂句柄 |
| stdout 纯净 | stdout 只允许 --help 与 --list-agentmodes 两个出口;ACP 协议帧独占 stdout,冒烟脚本会断言 stdout 不含诊断文本 |
性能
- 启动零阻塞:readiness 同步发布;preset 解析在后台,首个
session/new会 await 同一个 promise(只解析一次)。 - 不做忙等:等待策略是“先让 Loader 静止(有界 race)→ 再按声明行数等 roster”;退避 20ms 起、160ms 封顶,全部有截止时间;不存在固定轮询热路径。
- 零热路径开销:已绑定 preset 的 agent 只做一次
composedPreset()判断即返回;subagent 子代理在自身 setup 里继承父级,直接跳过。 - 构建产物:单文件 ESM 31.6 kB(gzip 10.5 kB),无运行时依赖、无 top-level 副作用。
- 清理:定时器统一由实例时钟登记,卸载即
clearTimeout。
错误模型
| 错误码 | 含义 | 退出码 |
|---|---|---|
E_USAGE | 调用本身不对(未知选项、缺值、非法 id、重复参数) | 2 |
E_PRESET_UNKNOWN | id 不在 roster(消息附可用列表) | 3 |
E_PRESET_UNUSABLE | 声明存在但无法激活(消息附 registry 给出的原因,如缺 cordisInspect) | 3 |
E_REGISTRY | registry 未在预算内响应 | 3 |
E_ENVIRONMENT | 组合不对(不是 ACP app、官方启动行仍激活、注入缺失、registry 从未激活) | 4 |
E_BIND | 单个会话绑定失败——只 warning,降级为“无 preset 组合”,绝不让 session/new 失败 | — |
DSH_ACP_AGENTMODE_DEBUG=1 会额外输出 cause 链与(净化后的)栈,默认只输出一行分类错误。
已验证
单测:vp test — 7 个文件 / 85 个用例全绿(参数语法、诊断净化、roster 等待、resolve 分类与重试、空白会话判定、绑定降级、设定来源判定与 --remember 写入、apply() 的 help/守卫/发布/列表/重复发布等)。
静态门禁:vp check — 格式化 + oxlint(类型感知)+ tsc 类型检查,0 error / 0 warning。
真实端到端(本机 0.2.1-rc.2,最小 ACP 客户端,不触发模型调用):
| 用例 | 结果 |
|---|---|
--agentmode standard/minimal/ptc/cordis | 会话创建成功;会话日志分别写入 agent-preset/selected: standard/minimal/ptc/cordis |
| 不带参数 | 默认 standard |
--agentmode nope | exit 3,E_PRESET_UNKNOWN: unknown agent preset "nope"; available: standard, ptc, minimal, cordis |
--bogus | exit 2,E_USAGE: unknown option "--bogus" |
--list-agentmodes / --help | exit 0,输出正确 |
session/resume(空白会话) | 成功,组合沿用日志记录的 preset |
pnpm smoke -- --agentmode minimal | 通过(脚本自行读会话日志断言) |
| stdout 纯净 | 客户端只收到 JSON-RPC 帧 |
未实测:session/resume 的非空白会话(mount 分支)需要先产生一轮真实模型对话。
权限(沙箱 / 审批)
ACP profile 里权限由 permission 行(@deepseek-ai/dsh-permission-presets)决定,它把 sandbox 与 approval 绑成一个预设。acp profile 默认没有 defaultPreset,所以默认值是推断出来的:
| 项 | 默认值 |
|---|---|
permission/preset | workspace-write |
sandbox/mode | workspace-write |
approval/policy | ask |
要改成完全访问,在 ~/.dsh/profiles/acp/cordis.patch.yml 里补 defaultPreset(config 是整体替换,整张 presets 表必须重述):
- id: permission
name: '@deepseek-ai/dsh-permission-presets'
config:
presets:
read-only:
sandbox: read-only
approval: ask
workspace-write:
sandbox: workspace-write
approval: ask
danger-full-access:
sandbox: danger-full-access
approval: never
defaultPreset: danger-full-access
只想本次生效就不改 profile,把它存成文件后用 launcher flag 叠加(--patch 必须在 app 参数之前):
dsh --profile acp --patch ~/.dsh/full-access.yml
验证方式是读会话日志的前三个事件:
permission/preset : danger-full-access
sandbox/mode : danger-full-access
approval/policy : never
三个要注意的点:ACP 客户端的 session/new 只暴露 model 与 reasoning_effort,没有权限选择面(/permission 这类命令 ACP 也不支持),所以权限只能在 profile 默认或 --patch 上设;danger-full-access 自带 approval: never,因此不再产生审批请求;一个 ACP 进程服务多个会话,该默认对全部会话生效。
已知能力边界
- 设定按 profile 隔离:
selectedDefault只存在于写入它的那个 profile;跨 profile 没有共享存储(同机 desktop/web/acp 各自独立)。要跨 profile 统一,用共享 home patch,代价是 Web 表单此后改不动该字段。 - 粒度是进程级:一次
dsh --profile acp一个 preset。要让 ACP 客户端按会话指定,必须改 harness 的dsh-acp(session/new的_meta被 SDK 保留但 bridge 完全忽略),纯插件做不到。 - preset 不含模型:preset 组合工具/提示词段/skills/压缩/委派;模型仍由
acp行与agent-default-model决定,客户端还能用session/set_config_option改model/reasoning_effort。 - 两个子行在该表面不可用,已在快照中
disabled: true并保留上游原文:tool-schedule(需要 Web 专属sessionController支撑的schedule)、tool-cordis(需要 Web 专属 inspector 的cordisInspect)。因此cordispreset 在 ACP 面上可用但缺少 cordis 运行时查询工具。 - 禁用清单镜像 Web 面的 23 行:上游改名只会产生 “warn + skip”(不致命);上游新增 agent 平面行会留在 host 平面并被 preset 同名行就近遮蔽,但可能带来额外 token 开销——升级后建议对照一次。
- 桌面界面的
plugin_manager看不到它(不同 profile);locale/、icon.svg只为清单完整性准备,渲染效果无法在桌面 UI 验证。
升级维护
pnpm sync-presets # 从当前安装的 harness 重新快照 presets/
pnpm sync-presets /path/to/dsh-web-app # 或显式指定包目录
脚本会按当前 harness 版本重新生成四个 preset 声明,并自动给“本表面不可用”的子行加 disabled: true(清单与原因写在脚本里)。升级 harness 后建议再跑一次 pnpm verify && pnpm smoke。
依赖的公开 API
ctx.cmdlineArgs / ctx.appExit / ctx.appReady(启动器值),ctx.agentPresets.{list,resolve,select,mount,composedPreset,defaultId},ctx.sessionProjections.stateOf(session, 'agentPreset' | 'turnBoundary'),serial 事件 agent/created,Loader 的 entries() / await()。全部为公开契约,未触碰内部实现。
贡献与安全
- 参与开发:见 CONTRIBUTING.md(本地门禁是
pnpm verify;dist/不入库,Release 构建产出)。 - 报告漏洞:请走 SECURITY.md 的私密渠道,不要开公开 issue。
- 行为准则:CODE_OF_CONDUCT.md。
main 受保护:所有改动经 PR + CI 全绿 + 维护者审核后合并。
MIT License.
Comments
Loading…
Similar plugins
by openma-ai
ACP server implementation for DeepSeek harness. dsh-acp
★ 49
↓ 1.4k/wk
NOASSERTION
TypeScript
Oct 19, 2026
dsh plugin --profile web add @openma/deepseek-harness-acpby dushaobindoudou
Agent Client Protocol (ACP) server plugin for the DeepSeek Harness (dsh) - drive dsh agents from Zed, any ACP v1 client, or the built-in web UI over stdio / HTTP+SSE, with sessions, jobs, goals, skill
★ 9
↓ 135/wk
MIT
TypeScript
Oct 14, 2026
dsh plugin --profile web add dsh-acp-serverby grunmin
Enhanced ACP (Agent Client Protocol) server for DeepSeek Harness (dsh) — drop-in bridge for the Zed editor: block-level streaming, usage/stat telemetry, model & reasoning-effort switching, permission
★ 7
↓ 634/wk
MIT
JavaScript
Oct 22, 2026
dsh plugin --profile web add dsh-acp-enhancedby SilenZerOrz
ACP adapter to drive DeepSeek Harness from Obsidian Agent Client (and other ACP clients): list, resume and fork sessions, run the harness, and archive conversations.
★ 4
↓ 629/wk
MIT
JavaScript
Oct 12, 2026
dsh plugin --profile web add obsidian-dsh-acpby wangqingdhuo
Run opencode as an ACP subagent in DeepSeek Harness — delegation tool, settings page, run history.
★ 1
MIT
JavaScript
Oct 20, 2026
dsh plugin --profile web add dsh-opencode-acpby AndPuQing
Extended Agent Client Protocol server bundle for DeepSeek Harness (dsh)
★ 0
TypeScript
Oct 17, 2026
dsh plugin --profile web add dsh-acp-plus