@khorsheed/dsh-capability-catalog 0.1.95 → 0.1.96

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,286 +1,136 @@
1
1
  # @khorsheed/dsh-capability-catalog
2
2
 
3
- A catalog of every skill and tool registered in a running dsh instance, with its
4
- registration channel (official builtin / project / user / custom / plugin runtime).
5
- It drives a standalone settings tab (工具与技能) and a model-facing
6
- `list_capabilities` tool.
7
-
8
- English | [中文](README.zh.md)
9
-
10
- ## What it shows
11
-
12
- - **Skills** as a 3-column preview grid (name + one-line description +
13
- source/provider pill). Click a card to open a centered modal with the full
14
- description, source/provider/invocation meta, a **unified source browser** (left
15
- file tree + right content pane; a content-only skill renders a single virtual
16
- `SKILL.md` node), frontmatter metadata, and a **credential config** block for
17
- every env the skill declares — parsed from `metadata.credentials` and from
18
- `$ENV` / `process.env.X` / `env['X']` / `{{env:X}}` references in the body
19
- (values never cross the wire).
20
- - **Tools** as collapsible cards with channel attribution (`mcp__` prefix /
21
- generated official whitelist / apply-time baseline diff).
22
- - **Add skill** — three install sources in one modal:
23
- - **文件上传 / Upload**: drag-drop (or click) a single `SKILL.md`, a `.zip`
24
- containing `SKILL.md`, or an entire skill folder (dependency-free
25
- `node:zlib` zip reader).
26
- - **命令安装 / Install from source**: give an `owner/repo`, a git URL, or a
27
- whole `npx skills add <repo> [--skill <name>]` command — flags are consumed,
28
- never cloned. A link copied out of a browser works as pasted: a
29
- `github.com/owner/repo` spec, an in-repo path
30
- (`github.com/owner/repo/skills/<name>`), and a `tree/<branch>/…` /
31
- `blob/<branch>/SKILL.md` URL all resolve to the same clone, the leading host
32
- is stripped rather than read as the owner, and the in-repo path is searched
33
- instead of the whole repo. Only `github.com` is split into owner/repo; on any
34
- other host the whole path is the repository (GitLab subgroups), cloned as
35
- given. A host with no repo, a path that is not in the repo, and a failed clone
36
- each come back as their own error. The host clones into a scratch dir and
37
- lifts the chosen skill bundle to `<root>/<name>/` (the dsh-native install; a
38
- real `npx skills add` writes into an external skills dir dsh cannot scan).
39
- `--skill` picks one from a repo carrying several; without it such a repo is
40
- refused with the list.
41
- - **从本机目录 / From directory**: a local skill dir can be listed (each
42
- `<name>/SKILL.md`), the user picks which to install, and the selected ones are
43
- copied into the managed root.
44
-
45
- Each source lets you choose the target root (`$DSH_HOME/skills` / `.agents/skills`)
46
- and whether the skill enters the model catalog. The skill-filesystem watcher
47
- discovers the result.
48
-
49
- The catalog reads the skill registry at the **agent preset's standing scope**
50
- (`agentPresets.standingKeyFor(defaultId)` on the 0.1.5 host line, the leased
51
- `agentPresets.acquireScope(defaultId)` on rc.1 — see [Compatibility](#compatibility))
52
- so it lists the same official/plugin/user
53
- skills the model sees, and the `list_capabilities` tool runs in the caller's agent
54
- scope. `snapshotFor(presetId)` reads any OTHER preset's scope the same way — see
55
- [The capability fingerprint](#the-capability-fingerprint).
56
-
57
- Zero host edits. If `ctx.skills` / `ctx.tools` / `ctx.credentials` /
58
- `ctx.agentPresets` are absent it degrades to an empty state rather than failing
59
- boot.
60
-
61
- ## The mode view
62
-
63
- An instance composes every session from an **agent preset** (「模式」 in the UI):
64
- the shipped standard / minimal / PTC / 创造 presets plus whatever a deployment or
65
- user authored. Each one registers a DIFFERENT set of skills and tools, and the
66
- **mode control beside the search and sort boxes** is how the tab shows that:
67
-
68
- - picking a preset reads the catalog at **that preset's standing scope**
69
- (`snapshotAt(presetId)`), and the grid is *that mode's face and nothing else* —
70
- a skill delivered only to 写作模式 does not appear under 开发模式, and the tab's
71
- counts are the mode's counts;
72
- - **全部模式(对比)** reads every preset's face in one call (`modeFaces`) and shows
73
- the union, with a chip row on each card naming the modes that load it — the
74
- answer to "which modes does this skill/tool appear in?" — and a chip jumps
75
- straight into that mode. The row stays compact at seven modes: a capability in
76
- EVERY readable mode collapses to one `全部模式 · 7` pill, and a long partial list
77
- shows two chips plus `+N`, which expands in place (a title tooltip alone would
78
- answer "which ones?" but not "take me there").
79
-
80
- A mode that cannot be read (a broken composition, a roster that resolves no
81
- standing scope) keeps its row and is reported as unreadable rather than rendered
82
- as an empty face: "this mode loads nothing" and "this mode could not be read" are
83
- different claims, and a comparison that conflated them would understate where a
84
- capability is available.
85
-
86
- An **unscoped** managed skill (no `presetScope`) is delivered to every preset, so
87
- it appears in every mode. A skill scoped to a preset this deployment does not
88
- supply, or one whose delivery a duplicate copy refused, appears in no mode's face
89
- at all; the skills tab then shows it under a diagnostic line (`N 个受管 skill 未在
90
- 任何模式生效`) whose cards open the detail modal, where its scope can be changed or
91
- released. Without that list, filtering by mode would make such a skill unreachable
92
- rather than merely hidden.
93
-
94
- Reading a mode is **not free**: resolving a preset's standing scope MOUNTS its
95
- composition (the roster's single-flight standing mount), so a preset nothing has
96
- composed yet is composed by its first read. The default mode is already mounted in
97
- practice — the session runs on it — while the comparison composes each of the
98
- others, which is why it is an explicit choice and not something the tab does on
99
- open. The client reads faces once per session behind a single-flight cache shared
100
- with the detail modal, so a panel that already compared modes answers instantly.
101
- There is no cheaper honest source: a composition file names the plugins a mode
102
- loads, not the tools and skills those plugins register once they run.
103
-
104
- Detail and delete follow the same read position: opening a card reads that
105
- capability's detail and bundle at the mode the card came from, and a delete
106
- targets the same one (the same skill name can resolve to a different bundle in
107
- another mode).
108
-
109
- Card badges name the **source** (插件 / 内置 / 用户 / 项目 / 自定义); a managed
110
- skill's `preset` scope is policy, not provenance, so it is shown where that policy
111
- is the subject — the orphan list and the detail modal.
112
-
113
- ### A preset scope is editable only where the host will write one
114
-
115
- The skill detail modal's 「生效的 preset」 section writes `presetScope` into the
116
- frontmatter of a skill in the plugin's managed root, so only two shapes are
117
- configurable. The section says which one it is instead of offering a Save the host
118
- refuses (`"<name>" is not a managed skill`):
119
-
120
- | skill | what the section shows |
3
+ [English](README.en.md) | 中文
4
+
5
+ 这个 agent 到底会什么、每个工具是谁装的——一个设置页全列出来,新 skill 和 MCP server 当场就能装;模型自己也有 `list_capabilities` 可查。
6
+
7
+ dsh 的每个会话都由一个 agent preset 组合而成,插件、技能目录、MCP server 都在往里注册能力——但宿主没有任何界面把它们列出来,「这个 agent 能干什么、那个工具从哪来的」以前只能翻日志回答。这个插件补上一个独立设置页(工具与技能)回答给人看,再注册一个 `list_capabilities` 工具回答给模型看。
8
+
9
+ <img src="https://raw.githubusercontent.com/Khorsheed/dsh-web-basic/main/docs/screenshots/capability-catalog-1.png" width="640" alt="「工具与技能」设置页:技能预览网格,带来源徽标、搜索排序与模式选择框">
10
+
11
+ ## 特性
12
+
13
+ - **技能一览**——三列预览网格(名字、一行描述、来源/provider 徽标),带搜索、排序和 内置/插件/其他 分段过滤。点卡片进详情弹窗:完整描述与调用面 meta、统一源码浏览器(左边 bundle 文件树、右边内容窗格——内容合成型 skill 渲染成单个虚拟 `SKILL.md` 节点)、frontmatter metadata(形似密钥的值显示为 `···`),以及凭据表单——从 `metadata.credentials` 与正文里的 `$ENV` / `process.env.X` / `env['X']` / `{{env:X}}` 引用解析出的每个 env 都可在此配置。值落在 dsh 凭据库,不上 wire。
14
+ - **工具带渠道归因**——折叠卡标明每个工具从哪来:作者声明的 origin 标记(精确)、`mcp__` 前缀(精确)、生成的官方工具白名单(精确)、apply 时序差分(推断)。详情弹窗把参数 schema 展示为结构树或原始 JSON。
15
+
16
+ - **三种方式装技能**——一个弹窗:上传(单个 `SKILL.md`、含 SKILL.md 的 `.zip`、或整个技能文件夹,零依赖 `node:zlib` 解压);命令安装(`owner/repo`、git URL,或整条粘贴来的 `npx skills add <repo> [--skill <名字>]` 命令——GitHub 仓库内路径与 `tree/`/`blob/` URL 都归一到同一个克隆,只有 `github.com` 会拆成 owner/repo,GitLab 子组按原样存活);或从本机目录勾选。每个来源都可选目标根(`$DSH_HOME/skills` 或 `.agents/skills`)与是否进模型 catalog;skill-filesystem watcher 会自动发现结果。
17
+
18
+ <img src="https://raw.githubusercontent.com/Khorsheed/dsh-web-basic/main/docs/screenshots/capability-catalog-2.png" width="640" alt="新增 Skill 弹窗:文件上传 / 命令安装 / 从本机目录三种安装方式,每个来源可选目标根与是否进模型 catalog">
19
+
20
+ - **模式视图**——每个会话都由一个 agent preset(「模式」)组合而成,不同模式注册的 skill 与 tool 各不相同。搜索框旁边的模式选择框按**该 preset 的 standing scope** 读(`snapshotAt(presetId)`)——网格就是那个模式的能力面,不再合并任何东西;「全部模式(对比)」一次调用读回每个模式的面(`modeFaces`),给每张卡片标上加载它的模式 chip,「这个能力出现在哪些模式」一点即达。读不到的模式标为「无法读取」,绝不渲染成空面。
21
+ - **按 preset 投递的技能**——插件自管一个受管根(`$DSH_HOME/capability-catalog/skills`),其中技能用 frontmatter `presetScope` 声明属于哪些模式;详情弹窗直接编辑(保存、把已装技能收编进受管根、释放回用户技能目录),不在任何模式生效的受管技能由一条诊断行保持可达,而不是被过滤没了。
22
+ - **MCP server 管理**——粘贴一段 `mcp.json` server 配置,弹窗自动解析传输、识别凭据(形似凭据的字段在存储的配置里变成 `secretRef:` 标记,Remote 只回 `configured` 状态——值永远到不了浏览器)。连接后发现该 server 的工具,整台 server 或单个工具都可开关,每个启用的工具都以 `mcp__<server>__<tool>` 注册到 `ctx.tools`,模型当场可调。配置的 server 经 settings 服务跨重启存活。
23
+
24
+ <img src="https://raw.githubusercontent.com/Khorsheed/dsh-web-basic/main/docs/screenshots/capability-catalog-3.png" width="640" alt="新增 MCP 服务器弹窗:粘贴一段 mcp.json server 配置,自动解析传输与凭据">
25
+
26
+ - **给模型的 `list_capabilities`**——模型可调的工具,列出调用方自己 agent scope 里可见的 skill 与 tool,并盖上能力指纹标签。
27
+ - **能力指纹**——`snapshotFor(presetId?)` 加载每个技能正文,对规范化能力面(名字、来源、渠道、参数、正文哈希——措辞不进)盖 sha256;同样的能力按不同顺序注册,哈希相同。`hashOf` / `capsTag` 导出给宿主侧读者。
28
+ - **凭据 env 注入**——某 skill 声明 env 对应的已配置凭据,以可信、逐执行的 `DSH_<KEY>` 变量暴露给模型 shell,模型用 shell 展开引用,原始值默认不进模型上下文;`skill` 工具加载这类技能时,运行时提示会把每个 `KEY → DSH_<KEY>` 映射告诉模型,从不改动 `SKILL.md`。
29
+ - **工具来源约定**——导出 `setToolOrigin` / `TOOL_ORIGIN`,任何插件在 `ctx.tools.register` 前给工具打标即可获得精确归因;设置页自带一个帮助弹窗,内置可复制的一段话,发给写插件的 agent 就能完成打标。
30
+ - **降级,不爆炸**——零宿主改动,全部经 `ctx.get` 探测现有官方服务;组合缺 `ctx.skills` / `ctx.tools` / `ctx.credentials` / `ctx.agentPresets` 时渲染空态,不拖垮 boot。
31
+
32
+ ## 安装
33
+
34
+ ```sh
35
+ dsh plugin --profile web add @khorsheed/dsh-capability-catalog
36
+ ```
37
+
38
+ 重启 web 实例后生效;「工具与技能」出现在设置页,紧随「插件」区块之后。卸载即精确移除挂载行:
39
+
40
+ ```sh
41
+ dsh plugin --profile web remove @khorsheed/dsh-capability-catalog
42
+ ```
43
+
44
+ ## Compatibility
45
+
46
+ - npm 发布线(`@deepseek-ai/dsh@0.1.5-rc.1`):✅ 完整——0.1.5-rc.1 全量 boot 实证通过(42 包含 capture,2026-09-25),经三层兼容修复:[preset-registry 双名探测](../../.agents/notes/implemented/bug-fix/2026-09-25-preset-registry-dual-name-probe.md)、[typert codec 双形状](../../.agents/notes/implemented/bug-fix/2026-09-25-typert-codec-dual-shape.md)、[face 自带 zod@4](../../.agents/notes/implemented/bug-fix/2026-09-25-typert-faces-carry-zod-v4.md)。`minHost` 即 0.1.5-rc.1,且 0.1.95 是本包首个发布——更早的宿主没有可用发布线。
47
+ - 源码线(deepseek-harness master):✅(verifiedHost: 0.1.7-rc.2)——0.1.7-rc.2 同时是 3080 生产实证线。
48
+
49
+ 按模式的能力面读取(模式下拉、`snapshotAt` / `snapshotFor` / `modeFaces`、按 preset 的技能投递)按宿主线走两条 roster 面解析 preset 的 standing scope:0.1.5 的无租约 `standingKeyFor`,rc.1 的租约式 `acquireScope`——rc.1 删除了 `standingKeyFor`,本次双线修复前的目录版本在 rc.1 上会静默读成全局层。清单与指纹两条路径每次读完都释放租约。机器可读字段见 `package.json` 的 `dsh.compat`(`minHost`、`verifiedHost`、`notes`)。
50
+
51
+ ## 已知限制
52
+
53
+ - **凭据注入是 default-hide,不是秘密边界**——构造的提示仍可让模型 `echo $DSH_KEY`。若要「模型永不持有原始值」,更硬的边界见 [masked-credential-proxy 提案](../../proposals/active/2026-08-29-masked-credential-proxy.md)。
54
+ - **未打标的插件工具退回启发式**——作者没设 origin 标记的工具可能被归到「内置」;设置页的「为什么我的插件工具不在这里?」弹窗(与 [docs/tool-origin-guide.md](../../docs/tool-origin-guide.md))给出了一行的修法。
55
+ - **MCP 桥只覆盖文本结果形态**——发现的工具按纯文本进出注册,无图片/附件投影;组合里没有 settings 服务时,MCP server 状态只活在内存里,重启即重置。
56
+
57
+ ## 实现原理
58
+
59
+ <details>
60
+ <summary>内部结构(点击展开)</summary>
61
+
62
+ **架构。** host 半是 `CapabilityCatalogService`,一个 `capabilityCatalog` 命名空间的 Typert Remote 服务;浏览器半经 `ctx.remote.$mount` 挂载生成的 Remote,再经 `slots.inject` 注册独立 `settings.section` 槽位(id `capabilities`,order 20——紧随「插件」区块),因此 apply 顺序永远不影响。身份三角:`cordis.patch.yml` 的 `capability-catalog` 行 id、npm 包名、`src/invariant.ts` 的 `PACKAGE_NAME`。包根导出服务本体、指纹 helper(`hashOf` / `capsTag` / `canonicalCapabilities`)、preset-scope/投递原语与工具来源 helper;`/client` 导出浏览器插件体。
63
+
64
+ **渠道归因。** `ToolSchema` 不携带来源字段,所以工具渠道按优先级解析:作者声明的 origin 标记(留在注册表按引用保留的 definition 上的 `Symbol.for('dsh.tool.origin')` 属性,经 `ctx.tools.get(name)` 读回——系统提示投影会剥掉它,所以永不上模型 wire),然后 `mcp__` 前缀(按最长已配置前缀匹配 server 名),然后生成的官方工具白名单(`scripts/gen-official-tools.mts`,由锁定的 harness 检出生成),最后 apply 时序差分。基线在延迟的 `ctx.inject(['tools'], …)` 里取,注册表自己的挂载顺序不可能造成空基线、把每个工具误标成插件。
65
+
66
+ **模式视图。** 实例把每个会话组合自一个 agent preset:内置的 标准/极简/PTC/创造,加上部署或用户自建的 preset,各自注册不同的 skill 与 tool。选中某个 preset 就按该 preset 的 standing scope 读(`snapshotAt(presetId)`),网格**就是那个模式的能力面**——只投递给写作模式的 skill 不会出现在开发模式下,标签页计数也是这个模式的计数。「全部模式(对比)」一次调用读回每个 preset 的面(`modeFaces`)显示并集,每张卡片一排模式 chip 标明哪些模式加载它;每个可读模式都加载的能力折叠成一颗 `全部模式 · N`,部分匹配但很长时只显示两颗 chip + `+N`,就地展开。读不到的模式(组合失败、roster 解不出 standing scope)保留自己的行并标为「无法读取」,而不是渲染成空面——「这个模式什么都没有」和「这个模式读不出来」是两回事。没声明 `presetScope` 的受管 skill 投递给每个 preset;范围指向本部署没有的 preset 的受管 skill 不属于任何模式的面,技能页此时给一条诊断行(`N 个受管 skill 未在任何模式生效`),展开点卡片进详情即可改范围或释放——没有这条出口,按模式过滤就不只是隐藏,而是让这类 skill 彻底够不着。读一个模式**不是免费的**:解析 preset 的 standing scope 会**挂载**它的组合(roster 的 single-flight standing mount),所以对比是一次显式选择,打开页面绝不做;客户端每个会话只读一次能力面(single-flight 缓存,和详情弹窗共用)。详情与删除跟着同一个读位置走:点卡片读的是该卡片来源模式下的详情与技能包,删除同样针对这个模式。
67
+
68
+ 卡片徽标标的是**来源**(插件 / 内置 / 用户 / 项目 / 自定义);受管 skill 的 `preset` 范围是策略不是来源,所以只在「范围就是主题」的地方出现——未生效清单和详情弹窗。
69
+
70
+ **只有 host 会写入的地方才可编辑 preset 范围。** 详情弹窗的「生效的 preset」区块是把 `presetScope` 写进受管根里某个 skill 的 frontmatter,所以该区块会说明自己属于哪一种形态,而不是给一个 host 必然拒绝(`"<name>" is not a managed skill`)的「保存」:
71
+
72
+ | skill | 该区块显示 |
121
73
  |---|---|
122
- | in the managed root | the preset grid + 保存 + 释放回用户技能目录 |
123
- | user / project / custom root | the preset grid + 移入受管目录并可限定 preset (no 保存: the selection is the scope adopt installs it with) |
124
- | plugin-provided (`runtime`) | why it cannot be set, plus the modes that actually load it (chips, from the shared faces cache — the question the mode picker answers, asked where the picker cannot: "where does this apply?") |
125
- | built-in (`bundled`) | why it cannot be set — it follows the deployment composition |
126
- | no roster / no managed delivery in this deployment | the unavailable note |
127
-
128
- The modal's **frontmatter metadata** block prints the metadata keys that have no
129
- dedicated surface: `presetScope` (the editor above) and `credentials` (the
130
- credential form) stay out of it, so a skill that merely declares a credential does
131
- not grow a raw JSON block. It sits above the credential form rather than pressed
132
- against the source browser, and secret-shaped VALUES (a key matching
133
- `key|token|secret|password`) render as `···`.
134
-
135
- ## The capability fingerprint
136
-
137
- A snapshot is a LISTING — registration order, human wording, file mtimes. The
138
- capability FACE is what is left when you remove everything that is not a
139
- capability, and `hashOf` is its sha256:
140
-
141
- | Row | What enters | What does not |
74
+ | 在受管根里 | preset 勾选网格 + 保存 + 释放回用户技能目录 |
75
+ | 用户 / 项目 / 自定义根 | preset 勾选网格 + 移入受管目录并可限定 preset(没有「保存」:勾选就是 adopt 装进去时的范围) |
76
+ | 插件提供(`runtime`) | 说明为什么不能设,并列出实际加载它的模式(chip 取自共用的能力面缓存) |
77
+ | 内置(`bundled`) | 说明为什么不能设——跟随部署组合 |
78
+ | 该部署没有 preset 名单 / 没有受管投递 | 不可用提示 |
79
+
80
+ 弹窗的 frontmatter metadata 区块只打印「没有专用界面」的 metadata 键:`presetScope`(上面的编辑器)和 `credentials`(凭据表单)不再重复出现,所以仅仅声明了凭据的 skill 不会多出一块原始 JSON;键名命中 `key|token|secret|password` 的值显示为 `···`。
81
+
82
+ **能力指纹。** snapshot 是一份**清单**——注册序、给人读的措辞、文件 mtime。把「不是能力的东西」去掉之后剩下的才是**能力面**,`hashOf` 是它的 sha256:
83
+
84
+ | 行 | 进哈希 | 不进 |
142
85
  |---|---|---|
143
- | skill | `name`, `source`, sha256 of the SKILL.md body | description, whenToUse, provider, `updatedAt` |
144
- | tool | `name`, `channel`, `parameters` | description, confidence, owner |
145
- | mcpServer | `name` + its tool NAMES | the tool count (derived) |
146
- | channel | the names | the counts (derived) |
147
-
148
- Every list is sorted by name and the digest is taken over canonical JSON, so
149
- two instances registering the same things in a different order hash alike.
150
- The exclusions are the claim: **prose is not a capability**. Rewording a tool
151
- description changes what the model reads, not what it can do — and a
152
- fingerprint that moved when someone fixed a typo would be useless as an
153
- identity. A skill's BODY is the opposite (it is the procedure), so it enters
154
- as a sha.
86
+ | skill | `name`、`source`、SKILL.md 正文的 sha256 | description、whenToUse、provider、`updatedAt` |
87
+ | tool | `name`、`channel`、`parameters` | description、confidence、owner |
88
+ | mcpServer | `name` + 它的工具**名单** | 工具数(可从行推出) |
89
+ | channel | 名单 | 计数(可从行推出) |
90
+
91
+ 每个列表按 name 排序,摘要取在规范 JSON 上——同样的东西按不同顺序注册,哈希相同。排除项本身就是主张:**措辞不是能力**——改一句工具描述改的是模型读到的字,不是它能做的事。技能**正文**相反(它就是那套流程),所以以 sha 进入。
155
92
 
156
93
  ```ts
157
94
  import { hashOf, capsTag } from '@khorsheed/dsh-capability-catalog'
158
95
 
159
- const face = await remote.snapshotFor('eval-lean') // sha already stamped
96
+ const face = await remote.snapshotFor('eval-lean') // sha 已盖好
160
97
  capsTag(face.sha) // 'caps:2f8b6d40…'
161
- hashOf(face) === face.sha // true — the `sha` field is not part of what it digests
98
+ hashOf(face) === face.sha // true——`sha` 字段本身不进它摘要的那份规范形
162
99
  ```
163
100
 
164
- `snapshotFor(presetId?, workdir?)` is the fingerprint verb: it reads the
165
- skill and tool registries at **that preset's** standing scope (the roster's
166
- `standingKeyFor(id)` on 0.1.5, its leased `acquireScope(id)` on rc.1), loads
167
- every skill body so the rows carry
168
- `bodySha`, and stamps `sha`. `presetId` omitted reads the deployment default.
169
- `snapshot()` stays the listing verb — same rows, no body loads, no digest.
170
- `list_capabilities` reports the full face's tag as `capabilities`, even when
171
- the caller filtered the answer to skills or tools.
172
-
173
- **A listing degrades; a fingerprint refuses.** When the preset's scope cannot
174
- be resolved — no roster, unknown id, a composition that will not mount —
175
- `snapshot()` falls back to the global layer and carries NO `preset` label,
176
- because a settings card must not go blank over a bad row. `snapshotFor()`
177
- throws instead, naming the preset and the reason. This is not hypothetical:
178
- on a real sub-dsh whose preset had one invalid row, the degrading version
179
- gave two scopes rostering two *different* presets one identical hash,
180
- silently.
181
-
182
- Two costs are worth naming. Fingerprinting loads one skill body per skill
183
- (the listing path still does not), and asking for a preset nothing has
184
- composed yet MOUNTS it — the roster's standing mount is what "that preset's
185
- scope" means.
186
-
187
- Who uses it: an evaluation records the orchestrating instance's own hash in
188
- `run.meta.orchestrator.capabilities` as provenance, and a condition that
189
- declares a `preset` must carry the hash of its provisioned environment in its
190
- lock — which is what turns that declaration from a claim into a fact.
191
-
192
- ## Skill credential env injection
193
-
194
- A configured credential for a skill's declared env var is exposed to the model's
195
- shell so the skill can query. Two pieces, both service-agnostic and generic
196
- (derived from each skill's own env-decl keys — no hardcoded key):
197
-
198
- - **`ctx.shellEnv`** injects each configured credential as a trusted,
199
- per-execution **`DSH_<KEY>`** variable. The model uses it by shell expansion
200
- (`KEY="$DSH_KEY" <cmd>`), so the raw value never enters the model's context by
201
- default (it only becomes visible if the model actively echoes it — a
202
- *default-hide*, not a hard secret boundary). If `ctx.shellEnv` /
203
- `ctx.credentials` / `ctx.skills` are absent, or a session has no skill, this is
204
- a no-op.
205
- - **Runtime companion hint** — the model won't derive the `KEY → DSH_<KEY>`
206
- alias on its own. When the `skill` tool loads a skill that has configured
207
- credentials, the catalog appends a per-skill note (`tools/post-execute` +
208
- `additionalContexts`) listing each mapping and how to use it. It never touches
209
- the user's `SKILL.md`.
210
-
211
- The credential store is the dsh credential store (`.credentials.yaml`, ref
212
- space); reads use `credentials.resolve(decl.key)` / `describe(decl.key).configured`
213
- (presence only, never the value) and writes `set(request.key, value)`, validated
214
- with a local POSIX ref-name check (no runtime `@deepseek-ai/dsh-credentials`
215
- import keeps graceful degradation when it is absent).
216
-
217
- Security trade-off (honest): this is *default-hide*, not a secret boundary — a
218
- prompt can still make the model `echo $DSH_KEY`. For "the model never holds the
219
- raw value", a catalog-owned narrow tool (`ctx.shell.run({ env })`) or the future
220
- masked-credential-proxy design is the harder edge; see the proposal §4.6.
221
-
222
- ## Plugin skill registration protocol
223
-
224
- This is the contract the catalog relies on to render a skill's file tree.
225
-
226
- A skill is discoverable in two ways:
227
-
228
- - **Filesystem/provider discovery** (`@deepseek-ai/dsh-skill-filesystem` scans a
229
- configured root for `<name>/SKILL.md`). These skills already carry
230
- `resourceBase: { kind: 'directory', path }`, so the catalog lists their bundle
231
- files (SKILL.md + references/scripts/assets) automatically.
232
- - **Runtime plugin registration** (`ctx.skills.register(...)`). The catalog can
233
- only list a runtime skill's bundle files if the plugin **exposes them**.
234
-
235
- ### The rule
236
-
237
- If a plugin ships a skill **whose bundle carries resources** (scripts, assets,
238
- references, or any file beside `SKILL.md`), register it with a resource base:
101
+ `snapshotFor(presetId?, workdir?)` 是指纹动词:按该 preset 的 standing scope 读 skill 与 tool 注册表,加载每个技能正文使行带上 `bodySha`,并盖 `sha`;不给 presetId 就读部署默认 preset。`snapshot()` 仍是清单动词——同样的行,不读正文,不出摘要。`list_capabilities` 以 `capabilities` 带回**整面**的标签,即使调用方只要了 skill 或只要了 tool。
102
+
103
+ **清单降级,指纹拒绝。** preset 的 scope 解析不出来时——没有 roster、id 不存在、composition 挂不起来——`snapshot()` 退回全局层并且不带 `preset` 标签(设置卡不该因为一行配置坏了就变空);`snapshotFor()` 则抛错,带上 preset 名与原因。两处代价要说清楚:算指纹时每个技能多读一次正文(清单那条路不读);问一个还没人组过的 preset 会把它**挂起来**——「该 preset 的 scope」本来就只有挂了才存在。谁在用:评测把编排实例自己的哈希记进 `run.meta.orchestrator.capabilities` 做取证;声明了 `preset` 的条件必须在 lock 里带上其已配环境的哈希——正是这一步把声明从「一句话」变成「一个事实」。
104
+
105
+ **skill 凭据 env 注入。** 把某 skill 声明 env 变量对应的已配置凭据暴露给模型 shell,让 skill 能查询。两块,都**服务无关且通用**(从每个 skill 自己的 env-decl 动态推导,无硬编码 key):
106
+
107
+ - `ctx.shellEnv` 把每个已配置凭据作为可信、逐执行的 `DSH_<KEY>` 变量注入。模型用 shell 展开引用(`KEY="$DSH_KEY" <cmd>`),所以原始值默认不进模型上下文(只有模型主动 echo 时才可见——是 default-hide,不是硬秘密边界)。映射到保留内建名(`DSH_HOME` / `DSH_SHELL` / `DSH_SESSION_ID`)或已被其他贡献者占用的 key 会跳过并告警,一个坏 key 不会拖垮整个注册。`ctx.shellEnv` / `ctx.credentials` / `ctx.skills` 缺失、或会话无 skill 时是 no-op。
108
+ - 运行时 companion 提示——模型不会自己推导 `KEY → DSH_<KEY>` 别名,所以当 `skill` 工具加载某个有已配置凭据的 skill 时,目录通过 `tools/post-execute` + `additionalContexts` 附加按 skill 生成的说明,列出每个映射与用法。**从不改用户的 SKILL.md**。
109
+
110
+ 凭据库是 dsh 官方 store(`.credentials.yaml`,ref 空间);读用 `credentials.resolve(decl.key)` / `describe(decl.key).configured`(仅 presence,绝无值),写 `set(request.key, value)`,用本地 POSIX ref-name 校验——不做运行时 `@deepseek-ai/dsh-credentials` import,保持缺失时优雅降级。
111
+
112
+ **MCP 管理。** 配置的 server 在两条宿上线都经 settings 服务持久化——0.1.5 的自由形 `settings.register` 命名空间,0.1.7 的 volatile 插件 Config 字段(经 SettingsForms,以 `settings/document-updated` 回合)——settings 缺失时降级为进程内 store。持久化块只存工具名与开关(描述和参数 schema 会撑大 `settings.yaml` 且会过期),所以 boot 时异步重连启用的 server 把它们补回来;boot 不被阻塞,各 server 的错误各自隔离。发现的工具按稳定的 `mcp__<serverName>__<rawName>` 契约注册到 `ctx.tools`,按函数名约束归一化(有损归一时追加 sha256 后缀消歧);注册是「先 dispose 再 register」的 reconcile,且始终以可追踪服务代理上的成员调用发起 `register`。形似凭据的配置字段变成 `secretRef:` 标记,连接时经 `ctx.credentials` 解析——`mcpSnapshot` 只回 `configured` 状态,标记永不出宿主。
113
+
114
+ **插件 skill 注册协议。** 目录渲染 skill 文件树所依赖的契约。skill 有两种被发现的方式:文件/提供者发现(`@deepseek-ai/dsh-skill-filesystem` 扫配置根目录里的 `<name>/SKILL.md`,这类自带 `resourceBase: { kind: 'directory', path }`,目录自动列出其 bundle 文件),以及运行时插件注册(`ctx.skills.register(...)`)——后者只有在你暴露了 bundle 时目录才能列出文件:
239
115
 
240
116
  ```ts
241
117
  ctx.skills.register({
242
118
  name: 'my-skill',
243
119
  description: '…',
244
- content: '…', // the SKILL.md body
120
+ content: '…', // SKILL.md 正文
245
121
  source: 'runtime',
246
- provider: 'my-plugin', // optional; labels the catalog card
247
- resourceBase: { // expose the bundle so it can be browsed
122
+ provider: 'my-plugin', // 可选;目录卡片的标签
123
+ resourceBase: { // 暴露 bundle 以便浏览
248
124
  kind: 'directory',
249
- path: <absolute path to the plugin's skills/<name> directory>,
125
+ path: <插件包 skills/<name> 目录的绝对路径>,
250
126
  },
251
127
  })
252
128
  ```
253
129
 
254
- When `resourceBase.kind === 'directory'`, the catalog walks `resourceBase.path`
255
- and shows the bundle as a file tree, and the model's relative-resource
256
- resolution can reach those files too. Asking the catalog to "scan every plugin
257
- package" is deliberately not done: the registry is the source of truth for what is
258
- installed, and it already attributes each skill to its provider.
259
-
260
- A **content-only** skill (a single self-contained `SKILL.md` body with no scripts,
261
- assets, or references beside it — e.g. `inline-html-card`) should **omit**
262
- `resourceBase` rather than point it at a directory that only holds `SKILL.md`.
263
- The browser half renders such a skill as a single virtual `SKILL.md` node whose
264
- content is the body — a unified source browser for every skill, without the host
265
- fabricating a disk path that is not a real resource bundle. The data contract
266
- stays honest: `files` is reported only when a bundle exists.
267
-
268
- The harness's `@deepseek-ai/dsh-skill` already documents `resourceBase` and
269
- `register()` / `registerProvider()`; this section is the catalog-side contract
270
- that spells out the plugin-facing expectation. (Upstream-candidate wording: ask
271
- dsh-skill to state explicitly that a runtime plugin skill should carry a
272
- `resourceBase` directory when it ships bundle resources.)
273
-
274
- ## Compatibility
275
-
276
- | Host line | Verdict |
277
- |---|---|
278
- | npm release (≥ `0.1.5-rc.1`) | supported — 0.1.5-rc.1 full-line boot-verified (42 packages including capture, 2026-09-25) |
279
- | npm `0.1.7-rc.1` / deepseek-harness master | supported (`verifiedHost: 0.1.7-rc.1`) |
280
- | earlier hosts | stay on the previous release line |
130
+ 当 `resourceBase.kind === 'directory'`,目录会 walk `resourceBase.path` 并把 bundle 显示成文件树,模型的相对资源解析也能触达这些文件。**有意不做**「目录去扫所有插件包」:registry 才是「装了哪些」的真相源,它已把每个 skill 归属到 provider。内容合成型 skill(单个自包含的 SKILL.md,无 scripts/assets/references)应**省略** `resourceBase`,而不是指向一个只有 SKILL.md 的目录——浏览器半把它渲染成单个虚拟 `SKILL.md` 节点,且只有存在 bundle 时才报 `files`。
281
131
 
282
- Both host lines are usable: the 0.1.5 boot passes end-to-end through three compat layers — the [preset-registry dual-name probe](../../.agents/notes/implemented/bug-fix/2026-09-25-preset-registry-dual-name-probe.md), [dual-shape typert codecs](../../.agents/notes/implemented/bug-fix/2026-09-25-typert-codec-dual-shape.md), and [typert faces carrying zod@4](../../.agents/notes/implemented/bug-fix/2026-09-25-typert-faces-carry-zod-v4.md).
132
+ </details>
283
133
 
284
- Per-mode capability reads (the mode picker, `snapshotAt` / `snapshotFor` / `modeFaces`, preset-scoped skill delivery) resolve a preset's standing scope through whichever roster face the host line offers: lease-free `standingKeyFor` on 0.1.5, the leased `acquireScope` on rc.1 — rc.1 removed `standingKeyFor`, and catalog releases before this dual-face fix silently read the global layer on rc.1. The lease is released after every read, on the listing and the fingerprint path alike; the strict/degrade wording is identical on both faces.
134
+ ## 开发
285
135
 
286
- Machine-readable: `dsh.compat.minHost` in `package.json` (currently `0.1.5-rc.1`; `verifiedHost` tracks the newest verified line, currently `0.1.7-rc.1`). When a degraded mode is omitted above, note it in `dsh.compat.notes`.
136
+ 隶属 [dsh-plugins](https://github.com/Khorsheed/dsh-plugins) monorepo(`packages/capability-catalog`)。问题与贡献请移步该仓库。