From 9ab64249db29b5dd9c8e11f8390e374981d542ed Mon Sep 17 00:00:00 2001 From: DandreYang <13072547+Dandre126@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:44:28 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E5=B0=86=20DeepSeek=20Harness=20?= =?UTF-8?q?=E4=B8=8E=20Pi=20=E7=BA=B3=E5=85=A5=E7=BC=96=E7=A0=81=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把 dsh 和 pi 放到与 Grok / OpenCode 同等位置:Home 启动、 Profile preset、dispatch 仅发现。Skill 分身分别挂到 DSH_HOME/~/.dsh 与 PI_CODING_AGENT_DIR/~/.pi/agent。 二者都没有经过审计的非交互 adapter,不会进入 REAL_PROVIDER_IDS。 --- CHANGELOG.md | 7 ++++ docs/designs/optional-local-agent-dispatch.md | 2 +- docs/tool-catalog.md | 2 +- experiments/local_agent_dispatch/README.md | 2 +- .../local_agent_dispatch/adapters/registry.py | 2 ++ src/dyro/integrations/manager.py | 8 +++-- src/dyro/tooling.py | 34 +++++++++++++++++++ tests/test_integrations.py | 33 +++++++++++++++++- tests/test_local_agent_dispatch_l1_l4.py | 5 +++ tests/test_tooling.py | 25 ++++++++++++++ 10 files changed, 114 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 871d3dc..0e30636 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +- Add DeepSeek Harness (`dsh`) and Pi (`pi`) to the coding-tool catalog, + Profile presets, and dispatch discovery. Neither has an audited + non-interactive adapter yet, so `dispatch` can see them but will not + route work. Install the control-plane Skill avatar for Pi when + `~/.pi/agent` is present, and for DeepSeek Harness when `~/.dsh` is + present. + ## 0.6.7 - 2026-08-13 - Make bare `dyro update` check, confirm, and install (same path as diff --git a/docs/designs/optional-local-agent-dispatch.md b/docs/designs/optional-local-agent-dispatch.md index eb95630..035f7fd 100644 --- a/docs/designs/optional-local-agent-dispatch.md +++ b/docs/designs/optional-local-agent-dispatch.md @@ -239,7 +239,7 @@ process-tree 与 pipe 后端。 安装/刷新时: 1. `dyro dispatch backends` 探测本机命令与登录态;当前受审计、可执行的集成为 `codex` 和 `claude`。 -2. `cursor-agent`、`opencode`、`grok`、`hermes`、`kimi` 会被展示为“已发现但未集成”,不得被自动或手动路由,直到各自拥有经过审计的非交互协议 adapter。 +2. `cursor-agent`、`opencode`、`grok`、`hermes`、`kimi`、`dsh`、`pi` 会被展示为“已发现但未集成”,不得被自动或手动路由,直到各自拥有经过审计的非交互协议 adapter。 3. 当只有一个已认证 Provider 时,`backend: auto` 可以选择它;当有多个时,用户必须使用 `dyro dispatch route add default ` 选择默认路由;一个也没有时 fail-closed 并给出发现结果。 4. `echo` 仅用于显式 `--backend echo --allow-offline-simulation` 的确定性测试,输出的 `execution_kind=offline-simulation`、低置信度和非生产警告不得被上游当作真实模型结论。 5. 渲染 skill 正文只含已准备 Provider、发现但未集成的命令、路由表与上述限制,再分发到各宿主 skills 目录。 diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index 1f08be3..a5170ae 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -77,7 +77,7 @@ page. Current catalog entries include Antigravity CLI, Codex CLI, Codex App, Claude Code CLI, Claude Code Desktop, Cursor Desktop, Cursor CLI, Grok, OpenCode, -OpenClaw, Hermes, Kimi Code, Qoder CLI, ZCode, and Shell. Antigravity uses the +OpenClaw, Hermes, Kimi Code, DeepSeek Harness, Pi, Qoder CLI, ZCode, and Shell. Antigravity uses the official `agy` command; Qoder uses `qodercli`; ZCode is opened as a desktop workspace tool. An entry can be known without having an audited installation recipe; such entries fail closed with an actionable message. diff --git a/experiments/local_agent_dispatch/README.md b/experiments/local_agent_dispatch/README.md index 3e13425..6ad266d 100644 --- a/experiments/local_agent_dispatch/README.md +++ b/experiments/local_agent_dispatch/README.md @@ -48,7 +48,7 @@ closed until a Windows process-tree backend is implemented. See design §4 (five-part contract). `auto` considers only integrated, authenticated Providers; with several it requires `dyro dispatch route add default `, and -with none it fails closed. `cursor-agent`, `opencode`, `grok`, `hermes`, and `kimi` +with none it fails closed. `cursor-agent`, `opencode`, `grok`, `hermes`, `kimi`, `dsh`, and `pi` may be discovered, but cannot run until an audited adapter exists. Backend `echo` is an explicit offline simulation: task JSON must set `allow_offline_simulation: true` and callers must not treat its low-confidence result as a Provider conclusion. diff --git a/experiments/local_agent_dispatch/adapters/registry.py b/experiments/local_agent_dispatch/adapters/registry.py index 87f0cbd..18b9f27 100644 --- a/experiments/local_agent_dispatch/adapters/registry.py +++ b/experiments/local_agent_dispatch/adapters/registry.py @@ -18,6 +18,8 @@ "grok": "grok", "hermes": "hermes", "kimi": "kimi", + "dsh": "dsh", + "pi": "pi", } diff --git a/src/dyro/integrations/manager.py b/src/dyro/integrations/manager.py index 9d4bee1..a4ddecb 100644 --- a/src/dyro/integrations/manager.py +++ b/src/dyro/integrations/manager.py @@ -56,6 +56,8 @@ class HostSpec: HostSpec("agents", "AGENTS_HOME", ".agents"), HostSpec("cursor", "CURSOR_HOME", ".cursor"), HostSpec("grok", "GROK_HOME", ".grok"), + HostSpec("pi", "PI_CODING_AGENT_DIR", ".pi/agent"), + HostSpec("dsh", "DSH_HOME", ".dsh"), ) @@ -874,9 +876,11 @@ def plan_integration( changes = ("无需写入;Integration 已是当前版本",) elif status.state is IntegrationState.ABSENT: if not status.avatars: + host_envs = " / ".join( + spec.env_var for spec in HOSTS if spec.env_var + ) changes = ( - "无法安装:未检测到宿主目录;需先创建或设置 " - "CODEX_HOME / CLAUDE_HOME / AGENTS_HOME / CURSOR_HOME", + "无法安装:未检测到宿主目录;需先创建或设置 " + host_envs, "不会创建孤立镜像或分身", ) else: diff --git a/src/dyro/tooling.py b/src/dyro/tooling.py index c61d6d3..10d00ca 100644 --- a/src/dyro/tooling.py +++ b/src/dyro/tooling.py @@ -206,6 +206,40 @@ class ToolPreferences: risk=_NPM_RISK, ), ), + ToolDefinition( + "dsh", + "DeepSeek Harness", + "dsh", + "terminal", + ("dsh", "web"), + install=InstallGuide( + "https://github.com/deepseek-ai/deepseek-harness", + _NPM_SCOPE, + ("npm", "install", "-g", "@deepseek-ai/dsh@latest"), + prerequisite="npm", + risk=_NPM_RISK, + ), + ), + ToolDefinition( + "pi", + "Pi", + "pi", + "terminal", + ("pi",), + install=InstallGuide( + "https://pi.dev/docs/latest", + _NPM_SCOPE, + ( + "npm", + "install", + "-g", + "--ignore-scripts", + "@earendil-works/pi-coding-agent@latest", + ), + prerequisite="npm", + risk=_NPM_RISK, + ), + ), ToolDefinition( "qoder", "Qoder CLI", diff --git a/tests/test_integrations.py b/tests/test_integrations.py index bf0bbb9..3cf86af 100644 --- a/tests/test_integrations.py +++ b/tests/test_integrations.py @@ -690,7 +690,15 @@ def test_sync_managed_skill_upgrades_outdated(self) -> None: def test_plan_surfaces_missing_host_blocker(self) -> None: with patch.dict(os.environ): - for key in ("CODEX_HOME", "CLAUDE_HOME", "AGENTS_HOME", "CURSOR_HOME"): + for key in ( + "CODEX_HOME", + "CLAUDE_HOME", + "AGENTS_HOME", + "CURSOR_HOME", + "GROK_HOME", + "PI_CODING_AGENT_DIR", + "DSH_HOME", + ): os.environ.pop(key, None) plan = install_integration("skill", yes=False, dry_run=True) self.assertEqual(plan.status.state, IntegrationState.ABSENT) @@ -759,5 +767,28 @@ def test_cli_status_dry_run_install_and_confirmation_gate(self) -> None: self.assertEqual(integration_status("skill").state, IntegrationState.ABSENT) + def test_pi_skill_host_uses_coding_agent_dir(self) -> None: + spec = next(host for host in manager.HOSTS if host.host_id == "pi") + self.assertEqual(spec.env_var, "PI_CODING_AGENT_DIR") + self.assertEqual(spec.default_dirname, ".pi/agent") + + pi_home = self.root / "pi-agent" + install_integration("skill", yes=True, host_homes={"pi": pi_home}) + avatar = pi_home / "skills" / "dyro-control-plane" + self.assertTrue(avatar.is_symlink() or avatar.is_dir()) + self.assertTrue((avatar / "SKILL.md").is_file()) + + def test_dsh_skill_host_uses_dsh_home(self) -> None: + spec = next(host for host in manager.HOSTS if host.host_id == "dsh") + self.assertEqual(spec.env_var, "DSH_HOME") + self.assertEqual(spec.default_dirname, ".dsh") + + dsh_home = self.root / "dsh-home" + install_integration("skill", yes=True, host_homes={"dsh": dsh_home}) + avatar = dsh_home / "skills" / "dyro-control-plane" + self.assertTrue(avatar.is_symlink() or avatar.is_dir()) + self.assertTrue((avatar / "SKILL.md").is_file()) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_local_agent_dispatch_l1_l4.py b/tests/test_local_agent_dispatch_l1_l4.py index 0377de1..5fdd4db 100644 --- a/tests/test_local_agent_dispatch_l1_l4.py +++ b/tests/test_local_agent_dispatch_l1_l4.py @@ -242,6 +242,11 @@ def test_auto_and_echo(self) -> None: rows = probe_backends() self.assertTrue(any(r["id"] == "echo" for r in rows)) self.assertTrue(any(r["id"] == "opencode" and not r["supported"] for r in rows)) + for provider_id in ("dsh", "pi"): + row = next(r for r in rows if r["id"] == provider_id) + self.assertFalse(row["supported"]) + self.assertEqual(row["execution_kind"], "unintegrated") + self.assertEqual(row["command"], provider_id) def test_routes_reject_simulation_and_unknown_providers(self) -> None: with tempfile.TemporaryDirectory() as tmp: diff --git a/tests/test_tooling.py b/tests/test_tooling.py index 5e29090..5a7b462 100644 --- a/tests/test_tooling.py +++ b/tests/test_tooling.py @@ -10,6 +10,7 @@ from unittest.mock import patch from dyro.errors import DyroError, ValidationError +from dyro.profile import launchable_preset_ids, preset_adapter from dyro.tooling import ( ToolPreferences, install_tool, @@ -59,10 +60,30 @@ def test_catalog_includes_desktop_and_new_terminal_tools(self) -> None: antigravity = tool_definition("antigravity") codex_desktop = tool_definition("codex-desktop") claude_desktop = tool_definition("claude-desktop") + dsh = tool_definition("dsh") + pi = tool_definition("pi") qoder = tool_definition("qoder") zcode = tool_definition("zcode") self.assertEqual(antigravity.command if antigravity else "", "agy") + self.assertEqual(dsh.command if dsh else "", "dsh") + self.assertEqual(dsh.launch if dsh else (), ("dsh", "web")) + self.assertEqual( + dsh.install.argv if dsh and dsh.install else (), + ("npm", "install", "-g", "@deepseek-ai/dsh@latest"), + ) + self.assertEqual(pi.command if pi else "", "pi") + self.assertEqual(pi.launch if pi else (), ("pi",)) + self.assertEqual( + pi.install.argv if pi and pi.install else (), + ( + "npm", + "install", + "-g", + "--ignore-scripts", + "@earendil-works/pi-coding-agent@latest", + ), + ) self.assertEqual(qoder.command if qoder else "", "qodercli") self.assertEqual(zcode.interface if zcode else "", "desktop") self.assertEqual(codex_desktop.interface if codex_desktop else "", "desktop") @@ -71,6 +92,10 @@ def test_catalog_includes_desktop_and_new_terminal_tools(self) -> None: qoder.install.argv if qoder and qoder.install else (), ("npm", "install", "-g", "@qoder-ai/qodercli"), ) + self.assertIn("dsh", launchable_preset_ids()) + self.assertIn("pi", launchable_preset_ids()) + self.assertEqual(preset_adapter("dsh", "dsh").launch, ("dsh", "web")) + self.assertEqual(preset_adapter("pi", "pi").launch, ("pi",)) def test_command_recipe_is_explicit_argv_and_requires_confirmation(self) -> None: calls: list[tuple[str, ...]] = [] From 862649844fb5a5fd0c8bc10414e2cfae9c7d5177 Mon Sep 17 00:00:00 2001 From: DandreYang <13072547+Dandre126@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:02:29 +0800 Subject: [PATCH 2/2] =?UTF-8?q?test:=20=E6=8C=89=20HOSTS=20=E9=9A=94?= =?UTF-8?q?=E7=A6=BB=20Skill=20=E5=AE=BF=E4=B8=BB=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IntegrationManagerTests 不再手写宿主环境变量名单。setUp 清空 HOSTS 里的全部 env,避免本机已设置 DSH_HOME / PI_CODING_AGENT_DIR 时把分身写进真实目录。 --- tests/test_integrations.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/tests/test_integrations.py b/tests/test_integrations.py index 3cf86af..3b183d1 100644 --- a/tests/test_integrations.py +++ b/tests/test_integrations.py @@ -32,9 +32,13 @@ def setUp(self) -> None: self.dyro_home = self.root / "dyro" self.fake_home = self.root / "home" self.fake_home.mkdir() + isolated_hosts = { + spec.env_var: "" for spec in manager.HOSTS if spec.env_var + } self.environment = patch.dict( os.environ, { + **isolated_hosts, "CODEX_HOME": str(self.codex_home), "DYRO_HOME": str(self.dyro_home), "HOME": str(self.fake_home), @@ -688,18 +692,23 @@ def test_sync_managed_skill_upgrades_outdated(self) -> None: assert plan is not None self.assertEqual(plan.status.state, IntegrationState.CURRENT) + def test_setup_isolates_every_host_env_var(self) -> None: + for spec in manager.HOSTS: + if not spec.env_var: + continue + if spec.env_var == "CODEX_HOME": + self.assertEqual(os.environ.get(spec.env_var), str(self.codex_home)) + continue + self.assertFalse( + os.environ.get(spec.env_var, "").strip(), + msg=spec.env_var, + ) + def test_plan_surfaces_missing_host_blocker(self) -> None: with patch.dict(os.environ): - for key in ( - "CODEX_HOME", - "CLAUDE_HOME", - "AGENTS_HOME", - "CURSOR_HOME", - "GROK_HOME", - "PI_CODING_AGENT_DIR", - "DSH_HOME", - ): - os.environ.pop(key, None) + for spec in manager.HOSTS: + if spec.env_var: + os.environ.pop(spec.env_var, None) plan = install_integration("skill", yes=False, dry_run=True) self.assertEqual(plan.status.state, IntegrationState.ABSENT) self.assertFalse(plan.status.avatars)