From 72085868504e338c569383c50455dde5d74c336e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AgentDock=20=E6=96=BD=E5=B7=A5=E5=91=98?= Date: Tue, 25 Aug 2026 00:38:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(wave-1):=20=E9=80=82=E9=85=8D=E5=99=A8?= =?UTF-8?q?=E6=A1=86=E6=9E=B6=E4=B8=8E=E5=AE=89=E5=85=A8=E5=9F=BA=E5=BA=A7?= =?UTF-8?q?=EF=BC=88schema=E6=A0=A1=E9=AA=8C/dry-run/exec=E6=B2=99?= =?UTF-8?q?=E7=AE=B1/=E5=AF=86=E9=92=A5=E5=BA=93/=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E8=84=B1=E6=95=8F/=E5=AD=97=E6=AE=B5=E6=8B=86=E5=88=86?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adapters/schema/adapter.schema.json | 263 ++++++++++- adapters/tools/aider.yaml | 24 +- adapters/tools/claude-code.yaml | 24 +- adapters/tools/cline.yaml | 24 +- adapters/tools/codebuddy.yaml | 24 +- adapters/tools/codex.yaml | 24 +- adapters/tools/copilot.yaml | 24 +- adapters/tools/crush.yaml | 24 +- adapters/tools/cursor.yaml | 24 +- adapters/tools/gemini.yaml | 24 +- adapters/tools/goose.yaml | 24 +- adapters/tools/kimi.yaml | 24 +- adapters/tools/opencode.yaml | 24 +- adapters/tools/qwen.yaml | 24 +- adapters/tools/warp.yaml | 24 +- apps/desktop/src/ipc/index.ts | 2 + apps/desktop/src/ipc/types.ts | 3 + apps/desktop/src/tokens/effects.css | 1 + apps/desktop/src/tokens/tiers.css | 2 +- crates/agentdock-adapter/src/catalog.rs | 120 +++-- crates/agentdock-adapter/src/danger.rs | 56 +++ crates/agentdock-adapter/src/error.rs | 9 + crates/agentdock-adapter/src/executor.rs | 292 ++++++++++++ crates/agentdock-adapter/src/lib.rs | 12 +- crates/agentdock-adapter/src/schema.rs | 557 +++++++++++++++++++++++ crates/agentdock-exec/src/error.rs | 31 ++ crates/agentdock-exec/src/exec.rs | 141 ++++++ crates/agentdock-exec/src/lib.rs | 21 +- crates/agentdock-exec/src/slots.rs | 89 ++++ crates/agentdock-platform/src/detect.rs | 64 ++- crates/agentdock-platform/src/model.rs | 8 +- crates/agentdock-secrets/Cargo.toml | 10 + crates/agentdock-secrets/src/error.rs | 25 + crates/agentdock-secrets/src/keyring.rs | 93 ++++ crates/agentdock-secrets/src/lib.rs | 27 +- crates/agentdock-secrets/src/mock.rs | 96 ++++ crates/agentdock-secrets/src/redact.rs | 121 +++++ crates/agentdock-secrets/src/store.rs | 21 + docs/architecture.md | 4 +- 39 files changed, 2323 insertions(+), 81 deletions(-) create mode 100644 crates/agentdock-adapter/src/danger.rs create mode 100644 crates/agentdock-adapter/src/executor.rs create mode 100644 crates/agentdock-adapter/src/schema.rs create mode 100644 crates/agentdock-exec/src/error.rs create mode 100644 crates/agentdock-exec/src/exec.rs create mode 100644 crates/agentdock-exec/src/slots.rs create mode 100644 crates/agentdock-secrets/src/error.rs create mode 100644 crates/agentdock-secrets/src/keyring.rs create mode 100644 crates/agentdock-secrets/src/mock.rs create mode 100644 crates/agentdock-secrets/src/redact.rs create mode 100644 crates/agentdock-secrets/src/store.rs diff --git a/adapters/schema/adapter.schema.json b/adapters/schema/adapter.schema.json index 3168452..fb23854 100644 --- a/adapters/schema/adapter.schema.json +++ b/adapters/schema/adapter.schema.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://agentdock.local/schemas/adapter.schema.json", "title": "AgentDock Adapter", - "description": "Agent CLI 适配器定义(v1)。Wave 0 仅含占位字段;完整 schema 见架构 §3.1,随 Wave 1 落地。", + "description": "Agent CLI 适配器定义(v1,对齐架构 §3.1)。首批 14 工具在 Wave 1 仅保留 id/name/name_zh/vendor/status 五字段占位;其余字段(platforms/official/runtime_deps/install/detect/update/uninstall/authorization/configuration/diagnostics/documentation)为可选,供 Wave 2 起逐工具填充。所有 command 必须是 argv 数组,禁止 shell 元字符与字符串拼接。", "type": "object", "additionalProperties": false, "required": ["id", "name", "name_zh", "vendor", "status"], @@ -28,6 +28,267 @@ "type": "string", "enum": ["available", "watch"], "description": "目录状态:available=可安装列表;watch=观察中(第二批)" + }, + "adapter_version": { + "type": "string", + "description": "适配器自身版本(semver,如 1.2.0)", + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?(\\+[0-9A-Za-z.-]+)?$" + }, + "license": { + "type": "string", + "description": "展示用许可;专有许可注明「仅官方渠道安装、不重打包」" + }, + "platforms": { + "type": "object", + "additionalProperties": false, + "properties": { + "windows": { + "type": "object", + "additionalProperties": false, + "properties": { + "architectures": { + "type": "array", + "items": { "type": "string", "enum": ["x64", "arm64"] } + }, + "notes": { + "type": "string", + "description": "如 Gemini 要求 Win11 24H2+" + } + } + }, + "linux": { + "type": "object", + "additionalProperties": false, + "properties": { + "distributions": { + "type": "array", + "items": { "type": "string", "enum": ["ubuntu", "debian"] } + }, + "architectures": { + "type": "array", + "items": { "type": "string", "enum": ["x64", "arm64"] } + }, + "min_ubuntu": { + "type": "string", + "description": "最低 Ubuntu 版本,如 22.04" + } + } + } + } + }, + "official": { + "type": "object", + "additionalProperties": false, + "properties": { + "homepage": { "type": "string", "format": "uri" }, + "docs": { "type": "string", "format": "uri" }, + "allowed_hosts": { + "type": "array", + "items": { "type": "string" }, + "description": "网络白名单(诊断/下载仅可访问这些主机)" + } + } + }, + "runtime_deps": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id"], + "properties": { + "id": { "type": "string", "enum": ["node", "python", "git", "powershell", "uv", "bash"] }, + "semver_range": { "type": "string", "description": "如 >=20" }, + "required_for": { + "type": "array", + "items": { "type": "string", "enum": ["install", "run"] } + } + } + } + }, + "install": { + "type": "object", + "additionalProperties": false, + "properties": { + "preferred": { "type": "string", "description": "默认渠道 channel id" }, + "channels": { + "type": "array", + "items": { "$ref": "#/definitions/channel" } + } + } + }, + "detect": { + "type": "object", + "additionalProperties": false, + "required": ["executable"], + "properties": { + "executable": { "type": "string", "description": "PATH 上的命令名;Cursor 为 agent" }, + "version_args": { "type": "array", "items": { "type": "string" } }, + "version_regex": { "type": "string" }, + "version_unconfirmed": { "type": "boolean", "description": "调研标注「文档未能确认」时 true" }, + "path_hints": { "type": "array", "items": { "type": "string" }, "description": "非 PATH 常见位置" } + } + }, + "update": { + "type": "object", + "additionalProperties": false, + "properties": { + "method": { + "type": "string", + "enum": ["npm_update", "self_update_cmd", "channel_reinstall", "winget_upgrade", "pypi_upgrade", "manual"] + }, + "command": { "type": "array", "items": { "type": "string" } } + } + }, + "uninstall": { + "type": "object", + "additionalProperties": false, + "properties": { + "method": { "type": "string", "enum": ["npm_uninstall", "package_manager", "manual_delete"] }, + "command": { "type": "array", "items": { "type": "string" } }, + "keep_config_default": { "type": "boolean", "default": true } + } + }, + "authorization": { + "type": "object", + "additionalProperties": false, + "properties": { + "modes": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["mode"], + "properties": { + "mode": { "type": "string", "enum": ["browser_oauth", "device_code", "api_key", "local_tui"] }, + "command": { "type": "array", "items": { "type": "string" } }, + "env_keys": { "type": "array", "items": { "type": "string" } }, + "status_command": { "type": "array", "items": { "type": "string" } }, + "notes_zh": { "type": "string" } + } + } + } + } + }, + "configuration": { + "type": "object", + "additionalProperties": false, + "properties": { + "files": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["path", "format"], + "properties": { + "path": { "type": "string", "description": "支持 ~ 与平台变量" }, + "format": { "type": "string", "enum": ["toml", "json", "jsonc", "yaml", "env", "crushrc"] }, + "scope": { "type": "string", "enum": ["user", "project", "system"] } + } + } + }, + "environment": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["key"], + "properties": { + "key": { "type": "string" }, + "sensitive": { "type": "boolean", "default": false }, + "maps_to_field": { "type": "string" } + } + } + }, + "fields": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "label_zh", "type", "storage"], + "properties": { + "id": { "type": "string" }, + "label_zh": { "type": "string" }, + "help_zh": { "type": "string" }, + "required": { "type": "boolean", "default": false }, + "sensitive": { "type": "boolean", "default": false }, + "type": { "type": "string", "enum": ["string", "url", "enum", "bool"] }, + "storage": { "type": "string", "enum": ["file", "env", "keyring"], "description": "keyring 永不进普通备份" }, + "platforms": { "type": "array", "items": { "type": "string", "enum": ["windows", "linux"] } }, + "docs_url": { "type": "string", "format": "uri" } + } + } + } + } + }, + "diagnostics": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["rule_id"], + "properties": { + "rule_id": { "type": "string", "description": "引用规则库或内联" } + } + } + }, + "documentation": { + "type": "object", + "additionalProperties": false, + "properties": { + "quickstart_zh": { "type": "string" }, + "commands": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "cmd": { "type": "string" }, + "desc_zh": { "type": "string" } + } + } + }, + "updated_at": { "type": "string", "format": "date" }, + "risks_zh": { "type": "array", "items": { "type": "string" } } + } + } + }, + "definitions": { + "channel": { + "type": "object", + "additionalProperties": false, + "required": ["id"], + "properties": { + "id": { + "type": "string", + "enum": ["npm", "official_script", "winget", "choco", "scoop", "brew", "apt", "pypi_uv", "github_release"] + }, + "platforms": { + "type": "array", + "items": { "type": "string", "enum": ["windows", "linux"] } + }, + "command": { + "type": "array", + "items": { "type": "string" }, + "description": "命令必须是 argv 数组,禁止字符串拼接与 shell 元字符(| & ; $ \\ > < ( `)" + }, + "script": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { "type": "string", "format": "uri" }, + "kind": { "type": "string", "enum": ["powershell_irm", "bash_pipe", "ps1_file"] }, + "integrity": { + "type": "object", + "additionalProperties": false, + "properties": { "sha256": { "type": "string", "pattern": "^[0-9a-fA-F]{64}$" } } + } + } + }, + "package": { "type": "string", "description": "npm/pypi 包名" }, + "elevate": { "type": "string", "enum": ["never", "if_needed", "required"] }, + "elevate_reason_zh": { "type": "string" }, + "post_checks": { "type": "array", "items": { "type": "string" } } + } } } } diff --git a/adapters/tools/aider.yaml b/adapters/tools/aider.yaml index 19c1ff9..965163e 100644 --- a/adapters/tools/aider.yaml +++ b/adapters/tools/aider.yaml @@ -1,4 +1,26 @@ -# aider 适配器占位(Wave 0) +# ============================================================ +# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema) +# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空, +# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。 +# +# 完整字段(全部留空占位,Wave 2 填写): +# adapter_version # semver,如 1.2.0 +# license # 展示用;专有许可注明「仅官方渠道安装、不重打包」 +# platforms # windows/linux:architectures、notes、min_ubuntu +# official # homepage / docs / allowed_hosts(网络白名单) +# runtime_deps # [ { id, semver_range, required_for: [install|run] } ] +# install # preferred + channels[ {id, platforms, command[], script, package, elevate, elevate_reason_zh, post_checks} ] +# detect # executable / version_args / version_regex / version_unconfirmed / path_hints +# update # method + command[] +# uninstall # method + command[] + keep_config_default +# authorization # modes[ {mode, command[], env_keys[], status_command[], notes_zh} ] +# configuration # files[] / environment[] / fields[] +# diagnostics # [ { rule_id } ] +# documentation # quickstart_zh / commands[] / updated_at / risks_zh[] +# +# 铁律:所有 command 一律 argv 数组,禁止 shell 元字符(| & ; $ \ > < (`); +# 本波不实现任何真实安装/检测/配置/授权命令(那是 Wave 2 的事)。 +# ============================================================ id: aider name: Aider name_zh: Aider diff --git a/adapters/tools/claude-code.yaml b/adapters/tools/claude-code.yaml index 294f326..8bb8700 100644 --- a/adapters/tools/claude-code.yaml +++ b/adapters/tools/claude-code.yaml @@ -1,4 +1,26 @@ -# claude-code 适配器占位(Wave 0) +# ============================================================ +# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema) +# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空, +# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。 +# +# 完整字段(全部留空占位,Wave 2 填写): +# adapter_version # semver,如 1.2.0 +# license # 展示用;专有许可注明「仅官方渠道安装、不重打包」 +# platforms # windows/linux:architectures、notes、min_ubuntu +# official # homepage / docs / allowed_hosts(网络白名单) +# runtime_deps # [ { id, semver_range, required_for: [install|run] } ] +# install # preferred + channels[ {id, platforms, command[], script, package, elevate, elevate_reason_zh, post_checks} ] +# detect # executable / version_args / version_regex / version_unconfirmed / path_hints +# update # method + command[] +# uninstall # method + command[] + keep_config_default +# authorization # modes[ {mode, command[], env_keys[], status_command[], notes_zh} ] +# configuration # files[] / environment[] / fields[] +# diagnostics # [ { rule_id } ] +# documentation # quickstart_zh / commands[] / updated_at / risks_zh[] +# +# 铁律:所有 command 一律 argv 数组,禁止 shell 元字符(| & ; $ \ > < (`); +# 本波不实现任何真实安装/检测/配置/授权命令(那是 Wave 2 的事)。 +# ============================================================ id: claude-code name: Claude Code name_zh: Claude Code diff --git a/adapters/tools/cline.yaml b/adapters/tools/cline.yaml index 0b6e194..ad479f1 100644 --- a/adapters/tools/cline.yaml +++ b/adapters/tools/cline.yaml @@ -1,4 +1,26 @@ -# cline 适配器占位(Wave 0) +# ============================================================ +# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema) +# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空, +# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。 +# +# 完整字段(全部留空占位,Wave 2 填写): +# adapter_version # semver,如 1.2.0 +# license # 展示用;专有许可注明「仅官方渠道安装、不重打包」 +# platforms # windows/linux:architectures、notes、min_ubuntu +# official # homepage / docs / allowed_hosts(网络白名单) +# runtime_deps # [ { id, semver_range, required_for: [install|run] } ] +# install # preferred + channels[ {id, platforms, command[], script, package, elevate, elevate_reason_zh, post_checks} ] +# detect # executable / version_args / version_regex / version_unconfirmed / path_hints +# update # method + command[] +# uninstall # method + command[] + keep_config_default +# authorization # modes[ {mode, command[], env_keys[], status_command[], notes_zh} ] +# configuration # files[] / environment[] / fields[] +# diagnostics # [ { rule_id } ] +# documentation # quickstart_zh / commands[] / updated_at / risks_zh[] +# +# 铁律:所有 command 一律 argv 数组,禁止 shell 元字符(| & ; $ \ > < (`); +# 本波不实现任何真实安装/检测/配置/授权命令(那是 Wave 2 的事)。 +# ============================================================ id: cline name: Cline name_zh: Cline diff --git a/adapters/tools/codebuddy.yaml b/adapters/tools/codebuddy.yaml index 6e248ba..75b8182 100644 --- a/adapters/tools/codebuddy.yaml +++ b/adapters/tools/codebuddy.yaml @@ -1,4 +1,26 @@ -# codebuddy 适配器占位(Wave 0) +# ============================================================ +# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema) +# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空, +# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。 +# +# 完整字段(全部留空占位,Wave 2 填写): +# adapter_version # semver,如 1.2.0 +# license # 展示用;专有许可注明「仅官方渠道安装、不重打包」 +# platforms # windows/linux:architectures、notes、min_ubuntu +# official # homepage / docs / allowed_hosts(网络白名单) +# runtime_deps # [ { id, semver_range, required_for: [install|run] } ] +# install # preferred + channels[ {id, platforms, command[], script, package, elevate, elevate_reason_zh, post_checks} ] +# detect # executable / version_args / version_regex / version_unconfirmed / path_hints +# update # method + command[] +# uninstall # method + command[] + keep_config_default +# authorization # modes[ {mode, command[], env_keys[], status_command[], notes_zh} ] +# configuration # files[] / environment[] / fields[] +# diagnostics # [ { rule_id } ] +# documentation # quickstart_zh / commands[] / updated_at / risks_zh[] +# +# 铁律:所有 command 一律 argv 数组,禁止 shell 元字符(| & ; $ \ > < (`); +# 本波不实现任何真实安装/检测/配置/授权命令(那是 Wave 2 的事)。 +# ============================================================ id: codebuddy name: CodeBuddy Code name_zh: CodeBuddy Code diff --git a/adapters/tools/codex.yaml b/adapters/tools/codex.yaml index 4fddbe6..242950f 100644 --- a/adapters/tools/codex.yaml +++ b/adapters/tools/codex.yaml @@ -1,4 +1,26 @@ -# codex 适配器占位(Wave 0) +# ============================================================ +# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema) +# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空, +# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。 +# +# 完整字段(全部留空占位,Wave 2 填写): +# adapter_version # semver,如 1.2.0 +# license # 展示用;专有许可注明「仅官方渠道安装、不重打包」 +# platforms # windows/linux:architectures、notes、min_ubuntu +# official # homepage / docs / allowed_hosts(网络白名单) +# runtime_deps # [ { id, semver_range, required_for: [install|run] } ] +# install # preferred + channels[ {id, platforms, command[], script, package, elevate, elevate_reason_zh, post_checks} ] +# detect # executable / version_args / version_regex / version_unconfirmed / path_hints +# update # method + command[] +# uninstall # method + command[] + keep_config_default +# authorization # modes[ {mode, command[], env_keys[], status_command[], notes_zh} ] +# configuration # files[] / environment[] / fields[] +# diagnostics # [ { rule_id } ] +# documentation # quickstart_zh / commands[] / updated_at / risks_zh[] +# +# 铁律:所有 command 一律 argv 数组,禁止 shell 元字符(| & ; $ \ > < (`); +# 本波不实现任何真实安装/检测/配置/授权命令(那是 Wave 2 的事)。 +# ============================================================ id: codex name: Codex CLI name_zh: Codex CLI diff --git a/adapters/tools/copilot.yaml b/adapters/tools/copilot.yaml index d25617d..f38b655 100644 --- a/adapters/tools/copilot.yaml +++ b/adapters/tools/copilot.yaml @@ -1,4 +1,26 @@ -# copilot 适配器占位(Wave 0) +# ============================================================ +# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema) +# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空, +# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。 +# +# 完整字段(全部留空占位,Wave 2 填写): +# adapter_version # semver,如 1.2.0 +# license # 展示用;专有许可注明「仅官方渠道安装、不重打包」 +# platforms # windows/linux:architectures、notes、min_ubuntu +# official # homepage / docs / allowed_hosts(网络白名单) +# runtime_deps # [ { id, semver_range, required_for: [install|run] } ] +# install # preferred + channels[ {id, platforms, command[], script, package, elevate, elevate_reason_zh, post_checks} ] +# detect # executable / version_args / version_regex / version_unconfirmed / path_hints +# update # method + command[] +# uninstall # method + command[] + keep_config_default +# authorization # modes[ {mode, command[], env_keys[], status_command[], notes_zh} ] +# configuration # files[] / environment[] / fields[] +# diagnostics # [ { rule_id } ] +# documentation # quickstart_zh / commands[] / updated_at / risks_zh[] +# +# 铁律:所有 command 一律 argv 数组,禁止 shell 元字符(| & ; $ \ > < (`); +# 本波不实现任何真实安装/检测/配置/授权命令(那是 Wave 2 的事)。 +# ============================================================ id: copilot name: Copilot CLI name_zh: Copilot CLI diff --git a/adapters/tools/crush.yaml b/adapters/tools/crush.yaml index d3b7ed1..af6f3ca 100644 --- a/adapters/tools/crush.yaml +++ b/adapters/tools/crush.yaml @@ -1,4 +1,26 @@ -# crush 适配器占位(Wave 0) +# ============================================================ +# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema) +# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空, +# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。 +# +# 完整字段(全部留空占位,Wave 2 填写): +# adapter_version # semver,如 1.2.0 +# license # 展示用;专有许可注明「仅官方渠道安装、不重打包」 +# platforms # windows/linux:architectures、notes、min_ubuntu +# official # homepage / docs / allowed_hosts(网络白名单) +# runtime_deps # [ { id, semver_range, required_for: [install|run] } ] +# install # preferred + channels[ {id, platforms, command[], script, package, elevate, elevate_reason_zh, post_checks} ] +# detect # executable / version_args / version_regex / version_unconfirmed / path_hints +# update # method + command[] +# uninstall # method + command[] + keep_config_default +# authorization # modes[ {mode, command[], env_keys[], status_command[], notes_zh} ] +# configuration # files[] / environment[] / fields[] +# diagnostics # [ { rule_id } ] +# documentation # quickstart_zh / commands[] / updated_at / risks_zh[] +# +# 铁律:所有 command 一律 argv 数组,禁止 shell 元字符(| & ; $ \ > < (`); +# 本波不实现任何真实安装/检测/配置/授权命令(那是 Wave 2 的事)。 +# ============================================================ id: crush name: Crush name_zh: Crush diff --git a/adapters/tools/cursor.yaml b/adapters/tools/cursor.yaml index 33fa755..c4fcb76 100644 --- a/adapters/tools/cursor.yaml +++ b/adapters/tools/cursor.yaml @@ -1,4 +1,26 @@ -# cursor 适配器占位(Wave 0) +# ============================================================ +# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema) +# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空, +# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。 +# +# 完整字段(全部留空占位,Wave 2 填写): +# adapter_version # semver,如 1.2.0 +# license # 展示用;专有许可注明「仅官方渠道安装、不重打包」 +# platforms # windows/linux:architectures、notes、min_ubuntu +# official # homepage / docs / allowed_hosts(网络白名单) +# runtime_deps # [ { id, semver_range, required_for: [install|run] } ] +# install # preferred + channels[ {id, platforms, command[], script, package, elevate, elevate_reason_zh, post_checks} ] +# detect # executable / version_args / version_regex / version_unconfirmed / path_hints +# update # method + command[] +# uninstall # method + command[] + keep_config_default +# authorization # modes[ {mode, command[], env_keys[], status_command[], notes_zh} ] +# configuration # files[] / environment[] / fields[] +# diagnostics # [ { rule_id } ] +# documentation # quickstart_zh / commands[] / updated_at / risks_zh[] +# +# 铁律:所有 command 一律 argv 数组,禁止 shell 元字符(| & ; $ \ > < (`); +# 本波不实现任何真实安装/检测/配置/授权命令(那是 Wave 2 的事)。 +# ============================================================ id: cursor name: Cursor CLI name_zh: Cursor CLI diff --git a/adapters/tools/gemini.yaml b/adapters/tools/gemini.yaml index 5d8de6f..52cef2d 100644 --- a/adapters/tools/gemini.yaml +++ b/adapters/tools/gemini.yaml @@ -1,4 +1,26 @@ -# gemini 适配器占位(Wave 0) +# ============================================================ +# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema) +# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空, +# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。 +# +# 完整字段(全部留空占位,Wave 2 填写): +# adapter_version # semver,如 1.2.0 +# license # 展示用;专有许可注明「仅官方渠道安装、不重打包」 +# platforms # windows/linux:architectures、notes、min_ubuntu +# official # homepage / docs / allowed_hosts(网络白名单) +# runtime_deps # [ { id, semver_range, required_for: [install|run] } ] +# install # preferred + channels[ {id, platforms, command[], script, package, elevate, elevate_reason_zh, post_checks} ] +# detect # executable / version_args / version_regex / version_unconfirmed / path_hints +# update # method + command[] +# uninstall # method + command[] + keep_config_default +# authorization # modes[ {mode, command[], env_keys[], status_command[], notes_zh} ] +# configuration # files[] / environment[] / fields[] +# diagnostics # [ { rule_id } ] +# documentation # quickstart_zh / commands[] / updated_at / risks_zh[] +# +# 铁律:所有 command 一律 argv 数组,禁止 shell 元字符(| & ; $ \ > < (`); +# 本波不实现任何真实安装/检测/配置/授权命令(那是 Wave 2 的事)。 +# ============================================================ id: gemini name: Gemini CLI name_zh: Gemini CLI diff --git a/adapters/tools/goose.yaml b/adapters/tools/goose.yaml index 7f7dd38..c1db7dc 100644 --- a/adapters/tools/goose.yaml +++ b/adapters/tools/goose.yaml @@ -1,4 +1,26 @@ -# goose 适配器占位(Wave 0) +# ============================================================ +# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema) +# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空, +# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。 +# +# 完整字段(全部留空占位,Wave 2 填写): +# adapter_version # semver,如 1.2.0 +# license # 展示用;专有许可注明「仅官方渠道安装、不重打包」 +# platforms # windows/linux:architectures、notes、min_ubuntu +# official # homepage / docs / allowed_hosts(网络白名单) +# runtime_deps # [ { id, semver_range, required_for: [install|run] } ] +# install # preferred + channels[ {id, platforms, command[], script, package, elevate, elevate_reason_zh, post_checks} ] +# detect # executable / version_args / version_regex / version_unconfirmed / path_hints +# update # method + command[] +# uninstall # method + command[] + keep_config_default +# authorization # modes[ {mode, command[], env_keys[], status_command[], notes_zh} ] +# configuration # files[] / environment[] / fields[] +# diagnostics # [ { rule_id } ] +# documentation # quickstart_zh / commands[] / updated_at / risks_zh[] +# +# 铁律:所有 command 一律 argv 数组,禁止 shell 元字符(| & ; $ \ > < (`); +# 本波不实现任何真实安装/检测/配置/授权命令(那是 Wave 2 的事)。 +# ============================================================ id: goose name: Goose name_zh: Goose diff --git a/adapters/tools/kimi.yaml b/adapters/tools/kimi.yaml index 2afaed9..83869dc 100644 --- a/adapters/tools/kimi.yaml +++ b/adapters/tools/kimi.yaml @@ -1,4 +1,26 @@ -# kimi 适配器占位(Wave 0) +# ============================================================ +# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema) +# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空, +# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。 +# +# 完整字段(全部留空占位,Wave 2 填写): +# adapter_version # semver,如 1.2.0 +# license # 展示用;专有许可注明「仅官方渠道安装、不重打包」 +# platforms # windows/linux:architectures、notes、min_ubuntu +# official # homepage / docs / allowed_hosts(网络白名单) +# runtime_deps # [ { id, semver_range, required_for: [install|run] } ] +# install # preferred + channels[ {id, platforms, command[], script, package, elevate, elevate_reason_zh, post_checks} ] +# detect # executable / version_args / version_regex / version_unconfirmed / path_hints +# update # method + command[] +# uninstall # method + command[] + keep_config_default +# authorization # modes[ {mode, command[], env_keys[], status_command[], notes_zh} ] +# configuration # files[] / environment[] / fields[] +# diagnostics # [ { rule_id } ] +# documentation # quickstart_zh / commands[] / updated_at / risks_zh[] +# +# 铁律:所有 command 一律 argv 数组,禁止 shell 元字符(| & ; $ \ > < (`); +# 本波不实现任何真实安装/检测/配置/授权命令(那是 Wave 2 的事)。 +# ============================================================ id: kimi name: Kimi CLI name_zh: Kimi CLI diff --git a/adapters/tools/opencode.yaml b/adapters/tools/opencode.yaml index 120d647..971640e 100644 --- a/adapters/tools/opencode.yaml +++ b/adapters/tools/opencode.yaml @@ -1,4 +1,26 @@ -# opencode 适配器占位(Wave 0) +# ============================================================ +# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema) +# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空, +# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。 +# +# 完整字段(全部留空占位,Wave 2 填写): +# adapter_version # semver,如 1.2.0 +# license # 展示用;专有许可注明「仅官方渠道安装、不重打包」 +# platforms # windows/linux:architectures、notes、min_ubuntu +# official # homepage / docs / allowed_hosts(网络白名单) +# runtime_deps # [ { id, semver_range, required_for: [install|run] } ] +# install # preferred + channels[ {id, platforms, command[], script, package, elevate, elevate_reason_zh, post_checks} ] +# detect # executable / version_args / version_regex / version_unconfirmed / path_hints +# update # method + command[] +# uninstall # method + command[] + keep_config_default +# authorization # modes[ {mode, command[], env_keys[], status_command[], notes_zh} ] +# configuration # files[] / environment[] / fields[] +# diagnostics # [ { rule_id } ] +# documentation # quickstart_zh / commands[] / updated_at / risks_zh[] +# +# 铁律:所有 command 一律 argv 数组,禁止 shell 元字符(| & ; $ \ > < (`); +# 本波不实现任何真实安装/检测/配置/授权命令(那是 Wave 2 的事)。 +# ============================================================ id: opencode name: OpenCode name_zh: OpenCode diff --git a/adapters/tools/qwen.yaml b/adapters/tools/qwen.yaml index 87c73f6..9438304 100644 --- a/adapters/tools/qwen.yaml +++ b/adapters/tools/qwen.yaml @@ -1,4 +1,26 @@ -# qwen 适配器占位(Wave 0) +# ============================================================ +# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema) +# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空, +# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。 +# +# 完整字段(全部留空占位,Wave 2 填写): +# adapter_version # semver,如 1.2.0 +# license # 展示用;专有许可注明「仅官方渠道安装、不重打包」 +# platforms # windows/linux:architectures、notes、min_ubuntu +# official # homepage / docs / allowed_hosts(网络白名单) +# runtime_deps # [ { id, semver_range, required_for: [install|run] } ] +# install # preferred + channels[ {id, platforms, command[], script, package, elevate, elevate_reason_zh, post_checks} ] +# detect # executable / version_args / version_regex / version_unconfirmed / path_hints +# update # method + command[] +# uninstall # method + command[] + keep_config_default +# authorization # modes[ {mode, command[], env_keys[], status_command[], notes_zh} ] +# configuration # files[] / environment[] / fields[] +# diagnostics # [ { rule_id } ] +# documentation # quickstart_zh / commands[] / updated_at / risks_zh[] +# +# 铁律:所有 command 一律 argv 数组,禁止 shell 元字符(| & ; $ \ > < (`); +# 本波不实现任何真实安装/检测/配置/授权命令(那是 Wave 2 的事)。 +# ============================================================ id: qwen name: Qwen Code name_zh: Qwen Code diff --git a/adapters/tools/warp.yaml b/adapters/tools/warp.yaml index 81d551b..10e8e55 100644 --- a/adapters/tools/warp.yaml +++ b/adapters/tools/warp.yaml @@ -1,4 +1,26 @@ -# warp 适配器占位(Wave 0) +# ============================================================ +# AgentDock 适配器占位(Wave 1,对齐架构 §3.1 完整 schema) +# 本波仅填五字段(id/name/name_zh/vendor/status);其余字段留空, +# Wave 2 起按调研底稿 agent-cli-survey-2026-08-24.md 逐项填充,禁止编造。 +# +# 完整字段(全部留空占位,Wave 2 填写): +# adapter_version # semver,如 1.2.0 +# license # 展示用;专有许可注明「仅官方渠道安装、不重打包」 +# platforms # windows/linux:architectures、notes、min_ubuntu +# official # homepage / docs / allowed_hosts(网络白名单) +# runtime_deps # [ { id, semver_range, required_for: [install|run] } ] +# install # preferred + channels[ {id, platforms, command[], script, package, elevate, elevate_reason_zh, post_checks} ] +# detect # executable / version_args / version_regex / version_unconfirmed / path_hints +# update # method + command[] +# uninstall # method + command[] + keep_config_default +# authorization # modes[ {mode, command[], env_keys[], status_command[], notes_zh} ] +# configuration # files[] / environment[] / fields[] +# diagnostics # [ { rule_id } ] +# documentation # quickstart_zh / commands[] / updated_at / risks_zh[] +# +# 铁律:所有 command 一律 argv 数组,禁止 shell 元字符(| & ; $ \ > < (`); +# 本波不实现任何真实安装/检测/配置/授权命令(那是 Wave 2 的事)。 +# ============================================================ id: warp name: Warp Agent CLI name_zh: Warp Agent CLI diff --git a/apps/desktop/src/ipc/index.ts b/apps/desktop/src/ipc/index.ts index 1b12d66..38bd28e 100644 --- a/apps/desktop/src/ipc/index.ts +++ b/apps/desktop/src/ipc/index.ts @@ -43,6 +43,8 @@ const mockPlatformEnv = (): PlatformEnv => ({ os: "windows", os_version: "Windows 11 24H2 (Build 26100)", arch: "x86_64", + distro: null, + distro_version: null, shells: { powershell_version: "5.1.26100.1", pwsh_version: null, bash_available: true }, runtimes: { node: { status: "installed", version: "24.18.0", path: "C:\\Program Files\\nodejs\\node.exe" }, diff --git a/apps/desktop/src/ipc/types.ts b/apps/desktop/src/ipc/types.ts index e1ced7a..f681c83 100644 --- a/apps/desktop/src/ipc/types.ts +++ b/apps/desktop/src/ipc/types.ts @@ -5,6 +5,8 @@ export interface PlatformEnv { os: string; os_version: string; arch: string; + distro: string | null; + distro_version: string | null; shells: Shells; runtimes: Runtimes; path_entries: string[]; @@ -31,6 +33,7 @@ export type RuntimeStatus = | "installed" | "not_installed" | "not_in_path" + | "permission_denied" | "exec_failed" | "version_unparseable"; diff --git a/apps/desktop/src/tokens/effects.css b/apps/desktop/src/tokens/effects.css index e1195d0..572bea8 100644 --- a/apps/desktop/src/tokens/effects.css +++ b/apps/desktop/src/tokens/effects.css @@ -14,6 +14,7 @@ --ad-glow-accent: 0 0 12px rgba(255, 77, 219, 0.35); --ad-glow-warning: 0 0 10px rgba(255, 176, 32, 0.28); /* 仅警告 chip / 警告状态灯 */ --ad-glow-danger: 0 0 12px rgba(255, 77, 94, 0.35); + --ad-glow-balanced: 0 0 6px rgba(0, 229, 255, 0.3); /* 中档单层辉光(§5 半径减半),tiers.css 引用 */ /* ---- 主按钮(§2.6,v1.2 hover 双层扩散辉光)---- */ --ad-btn-primary-bg: linear-gradient(180deg, #33D9EE 0%, #06BCD9 55%, #049EBB 100%); diff --git a/apps/desktop/src/tokens/tiers.css b/apps/desktop/src/tokens/tiers.css index 5c23ce5..1575528 100644 --- a/apps/desktop/src/tokens/tiers.css +++ b/apps/desktop/src/tokens/tiers.css @@ -31,7 +31,7 @@ /* ---- 中档:只保留单层辉光(半径减半),循环动画停用 ---- */ [data-fx-tier="balanced"] .fx-multilayer-glow { - box-shadow: 0 0 6px rgba(0, 229, 255, 0.3); + box-shadow: var(--ad-glow-balanced); } /* 背景网格漂移(§4.8,60s 一周,仅高档/中档) */ diff --git a/crates/agentdock-adapter/src/catalog.rs b/crates/agentdock-adapter/src/catalog.rs index 5b28db3..4135457 100644 --- a/crates/agentdock-adapter/src/catalog.rs +++ b/crates/agentdock-adapter/src/catalog.rs @@ -1,7 +1,8 @@ -//! 目录索引与占位条目加载(Wave 0) +//! 目录索引与条目加载(Wave 1) //! -//! 从 `adapters/catalog.yaml` 读取索引,再逐个读取 `tools/*.yaml`, -//! 返回目录条目。仅消费 `id / name / name_zh / vendor / status` 五个字段。 +//! 从 `adapters/catalog.yaml` 读取索引,再逐个读取 `tools/*.yaml`,对每个工具 +//! 做完整 schema 校验(含危险命令拒载、版本号校验),最后返回 UI 侧的五字段 +//! `CatalogEntry` 视图(`load_catalog`)或完整 `Adapter`(`load_adapters`)。 use std::fs; use std::path::Path; @@ -9,6 +10,7 @@ use std::path::Path; use serde::{Deserialize, Serialize}; use crate::error::AdapterError; +use crate::schema::{Adapter, parse_adapter}; /// 目录索引(adapters/catalog.yaml) #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] @@ -24,7 +26,7 @@ pub struct CatalogRef { pub file: String, } -/// 目录条目(占位 schema,Wave 0 仅五字段;完整字段见架构 §3.1) +/// 目录条目(五字段视图,供 UI 展示) #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct CatalogEntry { pub id: String, @@ -36,40 +38,58 @@ pub struct CatalogEntry { pub status: String, } -/// 加载目录索引与全部工具占位 YAML -pub fn load_catalog>(adapters_dir: P) -> Result, AdapterError> { +impl From for CatalogEntry { + fn from(a: Adapter) -> Self { + CatalogEntry { + id: a.id, + name: a.name, + name_zh: a.name_zh, + vendor: a.vendor, + status: a.status, + } + } +} + +/// 读取目录索引。 +fn read_index>(adapters_dir: P) -> Result { let dir = adapters_dir.as_ref(); let catalog_text = fs::read_to_string(dir.join("catalog.yaml")) .map_err(|e| AdapterError::Io(format!("读取 catalog.yaml 失败: {e}")))?; - let catalog: Catalog = serde_yaml::from_str(&catalog_text) - .map_err(|e| AdapterError::Parse(format!("解析 catalog.yaml 失败: {e}")))?; + serde_yaml::from_str(&catalog_text) + .map_err(|e| AdapterError::Parse(format!("解析 catalog.yaml 失败: {e}"))) +} - let mut entries = Vec::with_capacity(catalog.tools.len()); +/// 加载并校验全部工具适配器(完整 schema)。 +pub fn load_adapters>(adapters_dir: P) -> Result, AdapterError> { + let dir = adapters_dir.as_ref(); + let catalog = read_index(dir)?; + + let mut adapters = Vec::with_capacity(catalog.tools.len()); for r in &catalog.tools { let text = fs::read_to_string(dir.join(&r.file)) .map_err(|e| AdapterError::Io(format!("读取 {} 失败: {e}", r.file)))?; - let entry: CatalogEntry = serde_yaml::from_str(&text) - .map_err(|e| AdapterError::Parse(format!("解析 {} 失败: {e}", r.file)))?; - if entry.id != r.id { - return Err(AdapterError::Parse(format!( + let adapter = parse_adapter(&text) + .map_err(|e| AdapterError::Parse(format!("{} 校验未通过: {e}", r.file)))?; + if adapter.id != r.id { + return Err(AdapterError::Validation(format!( "索引 id 与文件内 id 不一致: 索引={} 文件={}", - r.id, entry.id + r.id, adapter.id ))); } - if entry.status != "available" && entry.status != "watch" { - return Err(AdapterError::Parse(format!( - "{} 的 status 非法: {}(应为 available | watch)", - entry.id, entry.status - ))); - } - entries.push(entry); + adapters.push(adapter); } - Ok(entries) + Ok(adapters) +} + +/// 加载目录并返回 UI 五字段视图(内部已做完整 schema 校验)。 +pub fn load_catalog>(adapters_dir: P) -> Result, AdapterError> { + Ok(load_adapters(adapters_dir)?.into_iter().map(CatalogEntry::from).collect()) } #[cfg(test)] mod tests { use super::*; + use crate::error::AdapterError; #[test] fn parses_minimal_tool_yaml() { @@ -81,19 +101,11 @@ mod tests { assert_eq!(entry.status, "available"); } - #[test] - fn rejects_invalid_status() { - let yaml = "id: x\nname: X\nname_zh: X\nvendor: V\nstatus: unknown\n"; - let entry: Result = serde_yaml::from_str(yaml); - // 解析本身成功,非法 status 由 load_catalog 校验;此处确认 schema 字段可读 - assert!(entry.is_ok()); - } - #[test] fn loads_real_catalog_from_repo() { // 以真实 adapters/ 目录做集成测试(相对本 crate 位于 ../../adapters) let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../adapters"); - let entries = load_catalog(&dir).expect("真实目录应可加载"); + let entries = load_catalog(&dir).expect("真实目录应可加载并通过校验"); assert_eq!(entries.len(), 14, "第一批应为 14 个工具"); for e in &entries { assert!(!e.id.is_empty()); @@ -107,4 +119,50 @@ mod tests { assert!(ids.contains(&want), "目录应包含 {want}"); } } + + /// 加载器拒载危险适配器(含 shell 元字符)并给中文错误。 + #[test] + fn loader_rejects_dangerous_adapter() { + let dir = std::env::temp_dir().join(format!("agentdock-adapters-danger-{}", std::process::id())); + let _ = fs::remove_dir_all(&dir); + fs::create_dir_all(dir.join("tools")).unwrap(); + fs::write( + dir.join("catalog.yaml"), + "catalog_version: 1\ntools:\n - id: evil\n file: tools/evil.yaml\n", + ) + .unwrap(); + fs::write( + dir.join("tools/evil.yaml"), + "id: evil\nname: Evil\nname_zh: Evil\nvendor: X\nstatus: available\ninstall:\n channels:\n - id: official_script\n command: [\"curl\", \"x | sh\"]\n", + ) + .unwrap(); + + match load_adapters(&dir) { + Err(AdapterError::Parse(m)) => assert!(m.contains('|'), "错误应含元字符: {m}"), + other => panic!("应因危险命令拒载,实际 {other:?}"), + } + + let _ = fs::remove_dir_all(&dir); + } + + /// 加载器校验版本号:非法 adapter_version 拒载。 + #[test] + fn loader_rejects_bad_version() { + let dir = std::env::temp_dir().join(format!("agentdock-adapters-ver-{}", std::process::id())); + let _ = fs::remove_dir_all(&dir); + fs::create_dir_all(dir.join("tools")).unwrap(); + fs::write( + dir.join("catalog.yaml"), + "catalog_version: 1\ntools:\n - id: bad\n file: tools/bad.yaml\n", + ) + .unwrap(); + fs::write( + dir.join("tools/bad.yaml"), + "id: bad\nname: Bad\nname_zh: Bad\nvendor: X\nstatus: available\nadapter_version: not-semver\n", + ) + .unwrap(); + + assert!(matches!(load_adapters(&dir), Err(AdapterError::Parse(_)))); + let _ = fs::remove_dir_all(&dir); + } } diff --git a/crates/agentdock-adapter/src/danger.rs b/crates/agentdock-adapter/src/danger.rs new file mode 100644 index 0000000..40da4fb --- /dev/null +++ b/crates/agentdock-adapter/src/danger.rs @@ -0,0 +1,56 @@ +//! 危险命令检测(架构 §4.2) +//! +//! 默认禁止 shell 拼接:管道、重定向、`$()`、反引号、`&&` 链、`;`、`\`。 +//! 适配器声明里的所有 `command` argv 都必须在加载期通过本检测, +//! 否则整条适配器拒载并给出中文错误。 + +/// shell 元字符集合(架构 §4.2;`(` `)` 覆盖 `$()`,反引号覆盖命令替换) +const SHELL_METACHARS: &[char] = &['|', '&', ';', '$', '\\', '>', '<', '(', ')', '`']; + +/// 返回第一个命中的 shell 元字符;无则返回 None。 +pub fn first_shell_metachar(s: &str) -> Option { + s.chars().find(|c| SHELL_METACHARS.contains(c)) +} + +/// 判断一个字符串是否含 shell 元字符。 +pub fn contains_shell_metachar(s: &str) -> bool { + first_shell_metachar(s).is_some() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn detects_common_metachars() { + for (sample, expected) in [ + ("curl | sh", Some('|')), + ("a && b", Some('&')), + ("a; rm -rf /", Some(';')), + ("$(id)", Some('$')), + ("echo `whoami`", Some('`')), + ("ls > out", Some('>')), + ("cat < in", Some('<')), + ("a\\b", Some('\\')), + ("echo (x)", Some('(')), + ("echo )", Some(')')), + ] { + assert_eq!(first_shell_metachar(sample), expected, "样本 {sample:?}"); + } + } + + #[test] + fn allows_plain_argv() { + for sample in [ + "npm", + "install", + "-g", + "@openai/codex", + "codex", + "--version", + "https://example.com/install.ps1", + ] { + assert!(!contains_shell_metachar(sample), "普通参数 {sample:?} 不应被判危险"); + } + } +} diff --git a/crates/agentdock-adapter/src/error.rs b/crates/agentdock-adapter/src/error.rs index 1e6c70d..2fa41c7 100644 --- a/crates/agentdock-adapter/src/error.rs +++ b/crates/agentdock-adapter/src/error.rs @@ -6,6 +6,12 @@ use std::fmt; pub enum AdapterError { Io(String), Parse(String), + /// schema / 语义校验失败(中文) + Validation(String), + /// 命令含 shell 元字符等危险输入(中文) + DangerousCommand(String), + /// 尚未实现的能力(Wave 2 起逐波落地) + NotImplemented(String), } impl fmt::Display for AdapterError { @@ -13,6 +19,9 @@ impl fmt::Display for AdapterError { match self { AdapterError::Io(m) => write!(f, "IO: {m}"), AdapterError::Parse(m) => write!(f, "Parse: {m}"), + AdapterError::Validation(m) => write!(f, "校验失败: {m}"), + AdapterError::DangerousCommand(m) => write!(f, "危险命令: {m}"), + AdapterError::NotImplemented(m) => write!(f, "未实现: {m}"), } } } diff --git a/crates/agentdock-adapter/src/executor.rs b/crates/agentdock-adapter/src/executor.rs new file mode 100644 index 0000000..9a9fb88 --- /dev/null +++ b/crates/agentdock-adapter/src/executor.rs @@ -0,0 +1,292 @@ +//! 统一执行器接口与 dry-run(架构 §3.2) +//! +//! `AdapterExecutor` 是各适配器的统一操作面;本波只落地骨架与 +//! `preview_action`(返回 `DryRunPlan`),真实安装/检测/配置/授权/诊断 +//! 命令的执行在 Wave 2 起由具体实现补全。所有 command 一律走 `agentdock-exec` +//! 的 argv 数组执行,禁止 shell 拼接。 + +use serde::{Deserialize, Serialize}; + +use crate::error::AdapterError; +use crate::schema::Adapter; + +/// 动作类型(对应 IPC 契约 previewAction/runAction 的 action) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AdapterAction { + Install, + Update, + Uninstall, + WriteConfig, + Authorize, + Repair, +} + +impl AdapterAction { + /// 动作的中文名(用于 dry-run 说明与 UI) + pub fn label_zh(&self) -> &'static str { + match self { + AdapterAction::Install => "安装", + AdapterAction::Update => "更新", + AdapterAction::Uninstall => "卸载", + AdapterAction::WriteConfig => "写配置", + AdapterAction::Authorize => "授权", + AdapterAction::Repair => "修复", + } + } +} + +/// 干燥运行计划:命令 argv、权限、影响文件、回滚说明(架构 §3.2 dry_run) +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DryRunPlan { + /// 将执行的命令(argv 数组,禁止 shell) + pub commands: Vec>, + /// 是否需要权限提升 + pub elevate: bool, + /// 权限提升说明(中文),需要提升时必填 + pub elevate_reason_zh: Option, + /// 将影响/写入的文件路径 + pub affected_files: Vec, + /// 回滚说明(中文) + pub rollback_zh: String, +} + +/// 统一执行器接口(架构 §3.2) +/// +/// 除 `dry_run` 外,其余方法为骨架:默认返回 `NotImplemented`,由 Wave 2 起 +/// 各适配器实现补全。任何实现都不得绕过 `agentdock-exec` 拼接 shell。 +pub trait AdapterExecutor { + /// 返回适配器定义 + fn adapter(&self) -> &Adapter; + + /// 干燥运行:不真正执行,返回将执行的命令与影响面 + fn dry_run(&self, action: AdapterAction) -> Result { + preview_action(self.adapter(), action) + } + + fn detect(&self) -> Result<(), AdapterError> { + Err(AdapterError::NotImplemented("detect 自 Wave 2 起实现".into())) + } + + fn install(&self) -> Result<(), AdapterError> { + Err(AdapterError::NotImplemented("install 自 Wave 2 起实现".into())) + } + + fn update(&self) -> Result<(), AdapterError> { + Err(AdapterError::NotImplemented("update 自 Wave 2 起实现".into())) + } + + fn uninstall(&self) -> Result<(), AdapterError> { + Err(AdapterError::NotImplemented("uninstall 自 Wave 2 起实现".into())) + } + + fn read_config(&self) -> Result<(), AdapterError> { + Err(AdapterError::NotImplemented("read_config 自 Wave 2 起实现".into())) + } + + fn write_config(&self, _patch: &str) -> Result<(), AdapterError> { + Err(AdapterError::NotImplemented("write_config 自 Wave 2 起实现".into())) + } + + fn authorization_status(&self) -> Result<(), AdapterError> { + Err(AdapterError::NotImplemented("authorization_status 自 Wave 2 起实现".into())) + } + + fn authorize(&self, _mode: &str) -> Result<(), AdapterError> { + Err(AdapterError::NotImplemented("authorize 自 Wave 2 起实现".into())) + } + + fn diagnose(&self) -> Result<(), AdapterError> { + Err(AdapterError::NotImplemented("diagnose 自 Wave 2 起实现".into())) + } +} + +/// 根据适配器声明 + 动作生成干燥运行计划(不执行)。 +pub fn preview_action(adapter: &Adapter, action: AdapterAction) -> Result { + let mut commands: Vec> = Vec::new(); + let mut elevate = false; + let mut elevate_reason_zh: Option = None; + let mut affected_files: Vec = Vec::new(); + + // 影响文件:配置文件路径(写配置/授权/修复都会触碰) + if let Some(cfg) = &adapter.configuration { + for f in &cfg.files { + affected_files.push(f.path.clone()); + } + } + + match action { + AdapterAction::Install => { + if let Some(install) = &adapter.install { + if let Some(channel) = pick_channel(install) { + if !channel.command.is_empty() { + commands.push(channel.command.clone()); + } + elevate = channel_elevates(channel); + elevate_reason_zh = channel.elevate_reason_zh.clone(); + if let Some(script) = &channel.script { + if let Some(url) = &script.url { + affected_files.push(format!("下载脚本: {url}")); + } + } + } + } + } + AdapterAction::Update => { + if let Some(update) = &adapter.update { + if !update.command.is_empty() { + commands.push(update.command.clone()); + } + } + } + AdapterAction::Uninstall => { + if let Some(uninstall) = &adapter.uninstall { + if !uninstall.command.is_empty() { + commands.push(uninstall.command.clone()); + } + } + } + AdapterAction::Authorize => { + if let Some(auth) = &adapter.authorization { + if let Some(mode) = auth.modes.first() { + if !mode.command.is_empty() { + commands.push(mode.command.clone()); + } + } + } + } + AdapterAction::WriteConfig => { + // 写配置无外部命令,仅落盘配置文件 + } + AdapterAction::Repair => { + // 修复由诊断规则驱动,本波仅占位 + } + } + + let rollback_zh = rollback_note(adapter, action); + + Ok(DryRunPlan { + commands, + elevate, + elevate_reason_zh, + affected_files, + rollback_zh, + }) +} + +/// 取首选渠道,无 preferred 时回退到第一个声明了命令的渠道。 +fn pick_channel(install: &crate::schema::Install) -> Option<&crate::schema::Channel> { + if let Some(preferred) = &install.preferred { + if let Some(ch) = install.channels.iter().find(|c| &c.id == preferred) { + return Some(ch); + } + } + install.channels.first() +} + +/// 渠道是否需要权限提升(never → false,其余 true)。 +fn channel_elevates(channel: &crate::schema::Channel) -> bool { + matches!(channel.elevate.as_deref(), Some("if_needed") | Some("required")) +} + +/// 生成中文回滚说明。 +fn rollback_note(adapter: &Adapter, action: AdapterAction) -> String { + match action { + AdapterAction::Install | AdapterAction::Update => { + format!("如需回退,可重新运行卸载({});已保留原配置文件不动。", adapter.id) + } + AdapterAction::Uninstall => { + format!("卸载默认保留配置文件({});如需彻底移除请手动删除配置文件。", adapter.id) + } + AdapterAction::WriteConfig => { + format!("写配置前会自动备份原文件为 .bak.<时间戳>,可随时恢复。") + } + AdapterAction::Authorize => { + format!("授权信息仅写入系统密钥库,不落盘;如需撤销可删除对应密钥条目。") + } + AdapterAction::Repair => { + format!("修复动作前会生成干燥运行计划,确认后才执行。") + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::schema::parse_adapter; + + #[test] + fn preview_install_builds_argv_plan() { + let adapter = parse_adapter( + r#" +id: claude-code +name: Claude Code +name_zh: Claude Code +vendor: Anthropic +status: available +install: + preferred: npm + channels: + - id: npm + platforms: [windows] + command: [npm, install, -g, "@anthropic-ai/claude-code"] + elevate: never +configuration: + files: + - path: "~/.claude/settings.json" + format: json +"#, + ) + .unwrap(); + + let plan = preview_action(&adapter, AdapterAction::Install).unwrap(); + assert_eq!(plan.commands, vec![vec!["npm", "install", "-g", "@anthropic-ai/claude-code"]]); + assert!(!plan.elevate); + assert!(plan.affected_files.iter().any(|f| f.contains("settings.json"))); + assert!(!plan.rollback_zh.is_empty()); + } + + #[test] + fn preview_elevates_when_required() { + let adapter = parse_adapter( + r#" +id: crush +name: Crush +name_zh: Crush +vendor: Charm +status: available +install: + preferred: apt + channels: + - id: apt + command: [apt, install, crush] + elevate: required + elevate_reason_zh: "需要管理员权限写入系统目录" +"#, + ) + .unwrap(); + let plan = preview_action(&adapter, AdapterAction::Install).unwrap(); + assert!(plan.elevate); + assert_eq!(plan.elevate_reason_zh.as_deref(), Some("需要管理员权限写入系统目录")); + } + + #[test] + fn action_label_zh_is_chinese() { + assert_eq!(AdapterAction::Install.label_zh(), "安装"); + assert_eq!(AdapterAction::WriteConfig.label_zh(), "写配置"); + } + + #[test] + fn wave2_methods_return_not_implemented() { + let adapter = parse_adapter("id: x\nname: X\nname_zh: X\nvendor: V\nstatus: available\n").unwrap(); + struct NoopExecutor(Adapter); + impl AdapterExecutor for NoopExecutor { + fn adapter(&self) -> &Adapter { + &self.0 + } + } + let ex = NoopExecutor(adapter); + assert!(matches!(ex.detect(), Err(AdapterError::NotImplemented(_)))); + assert!(matches!(ex.install(), Err(AdapterError::NotImplemented(_)))); + } +} diff --git a/crates/agentdock-adapter/src/lib.rs b/crates/agentdock-adapter/src/lib.rs index 133a4d7..c3d95cd 100644 --- a/crates/agentdock-adapter/src/lib.rs +++ b/crates/agentdock-adapter/src/lib.rs @@ -1,11 +1,17 @@ //! agentdock-adapter —— 适配器层 //! //! 负责适配器 schema 加载、版本校验、dry-run 与执行器接口(架构 §3)。 -//! Wave 0:仅落地目录索引与占位条目加载(`catalog` 模块); -//! 完整 schema 校验、执行器接口随 Wave 1 实现。 +//! Wave 1:完整 schema 定义与校验(含危险命令拒载)、统一执行器骨架、 +//! dry-run 计划(`preview_action`)。真实安装/检测/配置命令 Wave 2 起落地。 pub mod catalog; +pub mod danger; pub mod error; +pub mod executor; +pub mod schema; -pub use catalog::{Catalog, CatalogEntry, CatalogRef, load_catalog}; +pub use catalog::{Catalog, CatalogEntry, CatalogRef, load_adapters, load_catalog}; +pub use danger::{contains_shell_metachar, first_shell_metachar}; pub use error::AdapterError; +pub use executor::{AdapterAction, AdapterExecutor, DryRunPlan, preview_action}; +pub use schema::{Adapter, parse_adapter}; diff --git a/crates/agentdock-adapter/src/schema.rs b/crates/agentdock-adapter/src/schema.rs new file mode 100644 index 0000000..46268ab --- /dev/null +++ b/crates/agentdock-adapter/src/schema.rs @@ -0,0 +1,557 @@ +//! 适配器完整 schema(对齐架构 §3.1) +//! +//! 与 `adapters/schema/adapter.schema.json` 同源:JSON Schema 是声明式契约, +//! 本文件用 Rust 强类型结构体做运行时校验(deny_unknown_fields + 语义校验), +//! 两者字段一一对应。Wave 1 的 14 个占位 YAML 仅填五字段,其余字段均可选。 + +use serde::{Deserialize, Serialize}; + +use crate::danger::first_shell_metachar; +use crate::error::AdapterError; + +/// 适配器定义(完整字段,§3.1) +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct Adapter { + pub id: String, + pub name: String, + #[serde(rename = "name_zh")] + pub name_zh: String, + pub vendor: String, + /// available | watch + pub status: String, + /// semver,如 1.2.0 + #[serde(rename = "adapter_version", default)] + pub adapter_version: Option, + #[serde(default)] + pub license: Option, + #[serde(default)] + pub platforms: Option, + #[serde(default)] + pub official: Option, + #[serde(rename = "runtime_deps", default)] + pub runtime_deps: Vec, + #[serde(default)] + pub install: Option, + #[serde(default)] + pub detect: Option, + #[serde(default)] + pub update: Option, + #[serde(default)] + pub uninstall: Option, + #[serde(default)] + pub authorization: Option, + #[serde(default)] + pub configuration: Option, + #[serde(default)] + pub diagnostics: Vec, + #[serde(default)] + pub documentation: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct Platforms { + #[serde(default)] + pub windows: Option, + #[serde(default)] + pub linux: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct PlatformWindows { + #[serde(default)] + pub architectures: Vec, + #[serde(default)] + pub notes: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct PlatformLinux { + #[serde(default)] + pub distributions: Vec, + #[serde(default)] + pub architectures: Vec, + #[serde(rename = "min_ubuntu", default)] + pub min_ubuntu: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct Official { + #[serde(default)] + pub homepage: Option, + #[serde(default)] + pub docs: Option, + #[serde(rename = "allowed_hosts", default)] + pub allowed_hosts: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct RuntimeDep { + pub id: String, + #[serde(rename = "semver_range", default)] + pub semver_range: Option, + #[serde(rename = "required_for", default)] + pub required_for: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct Install { + #[serde(default)] + pub preferred: Option, + #[serde(default)] + pub channels: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct Channel { + pub id: String, + #[serde(default)] + pub platforms: Vec, + #[serde(default)] + pub command: Vec, + #[serde(default)] + pub script: Option