dsh-workspace-api-key 0.0.0-stage → 0.2.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 COH2357
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,93 @@
1
- # Temporary Holding Version
1
+ # dsh-workspace-api-key
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [English](README.md) | [中文](README.zh.md)
4
+
5
+ A DeepSeek Harness plugin that binds a separate DeepSeek API key to each workspace — or to a single session.
6
+
7
+ DSH resolves the DeepSeek key from one global credential ref (default `DEEPSEEK_API_KEY`), so every session in every workspace spends the same key. This plugin adds a panel that gives individual workspaces — and, when you need it, individual sessions — their own key, so the spend of a key stays with the sessions that ran there. Anything without its own key keeps using the level above it:
8
+
9
+ ```
10
+ session key → workspace key → system default
11
+ ```
12
+
13
+ ## What you get
14
+
15
+ - **A sidebar row** labelled `API Key 分配`, placed directly under the host's own Plugins row, opening a full-area page.
16
+ - **Per-workspace keys** — every workspace registered with the `workspaceRegistry` service is listed, with its own key field.
17
+ - **Per-session keys** — each workspace row expands (collapsed by default) to the sessions that live in that directory, each with its own key. Sessions that inherit a key from the level above say so instead of pretending to have one.
18
+ - **A system-default card** — which ref is in effect, where its value comes from (`file`, `env`, …), and its masked value.
19
+ - **Connection test** — one real `POST <baseURL>/messages` request (`max_tokens: 1`) with that key, reported as `ok`, `invalid`, `quota`, `rate-limit` or `unreachable`. A successful test also clears a stale flag.
20
+ - **Stale-key warning** — 401/403 marks a bound key `invalid`, 402 marks it `quota`, 429 marks it `rate-limit`. When you enter a session whose effective key is flagged, an overlay tells you what happened and offers *go configure*, *fall back to the level above*, or dismiss.
21
+ - **Mismatch warning** — if the key you bound was recorded for one provider (say `deepseek-official`) and the session now runs on another, entering that session reports it, because that request would be sent with a key the provider does not accept.
22
+ - **No silent substitution** — neither warning ever changes which key is used. The decision stays with you, which is the whole point: an automatically swapped key would defeat the billing separation.
23
+ - **One-click revert** — clearing a session entry falls back to the workspace key, clearing a workspace entry falls back to the system default.
24
+
25
+ ## How it works
26
+
27
+ Host half (`lib/index.js`):
28
+
29
+ - wraps `credentials.resolve` once per process (restored on dispose through `ctx.effect`) instead of registering a second service or provider route, neither of which cordis allows for a name that is already taken;
30
+ - reads the active session's directory and id from `agents.currentInitiator()?.session` — the agent loop runs the whole turn inside that async scope, so both are available when the credential is resolved, and the resolver picks the most specific level that has a key;
31
+ - matches that directory against `workspaceRegistry.list()` after normalizing both sides (absolute, trailing separator removed, lower-cased on Windows);
32
+ - derives a stable ref per level — `DEEPSEEK_API_KEY_WS_<first 16 hex chars of sha1(normalized path), upper-cased>` for a workspace and `DEEPSEEK_API_KEY_SS_<first 16 hex chars of sha1(session id), upper-cased>` for a session (a session id contains `-` and cannot be a ref name) — writes the key through the credentials service, and serves that ref's value;
33
+ - rewrites only refs a provider actually reads: the default ref, every adapter's configured `apiKeyEnv`, and its own generated refs. Every other ref passes through untouched;
34
+ - builds the session list from the `sessionController` service (`list()`), so the panel shows the same titles and model projections the sidebar does, and degrades to per-workspace only when that service is absent;
35
+ - reports its own version and a session probe (`available`, `count`, `withoutCwd`, `samples`, `error`, plus sample workspace paths) in `/wsk-api/state`, so the panel can say *why* no sessions are listed: a stale host half (only a page reload instead of an app restart), a missing `sessionController`, a throwing `list()`, or a `cwd` that matches no workspace path;
36
+ - keeps its own bookkeeping (which level maps to which ref, invalid flags, the provider a key was recorded for) in `$DSH_HOME/storages/dsh-workspace-api-key.json`;
37
+ - observes failures through the `agent/request-error` waterfall without ever returning `{kind: 'retry'}` — it only records the verdict and calls `next()`.
38
+
39
+ The browser half (`lib/client.js`) registers into the `sidebar.panellist` slot (`order: 1`), the `main` slot (key `workspace-api-key`) and the `shell.overlay` slot (the entering-a-session warning), the same slots `dsh-skill-mcp-panel` and `dsh-429-guard` use. It is plain `React.createElement` with no build step and no bundled dependencies. The overlay asks `/wsk-api/check` about the session that currently owns the main view, asks at most once per session, and its buttons only ever navigate to the panel — the panel is where a key can actually be changed.
40
+
41
+ ### HTTP API (loopback only)
42
+
43
+ | Method | Path | Purpose |
44
+ | --- | --- | --- |
45
+ | GET | `/wsk-api/state` | default ref status, workspaces with their sessions, overrides, invalid flags, plugin version and session probe |
46
+ | GET | `/wsk-api/resolve` | which level and ref the current session resolves to right now |
47
+ | GET | `/wsk-api/check` | `?sessionId=…&path=…` — read-only verdict for one session: effective level, key, stale flag, provider mismatch, `needsAttention` |
48
+ | POST | `/wsk-api/set` | `{scope: 'workspace' \| 'session', path \| sessionId, title, key, provider?}` binds a key; `{useDefault: true}` or an empty key clears it |
49
+ | POST | `/wsk-api/test` | `{scope, path \| sessionId \| ref}` (or `{key}`) runs one real request and returns the verdict; `invalid` marks the flag, `ok` clears it |
50
+ | POST | `/wsk-api/clear-flag` | `{ref}` or a target drops the invalid/quota flag |
51
+
52
+ Only loopback callers are accepted (`127.0.0.1`, `::1`, `::ffff:127.0.0.1`) with a same-origin or absent `Origin`. No response ever contains a plaintext key, only a mask.
53
+
54
+ Workspace targets may be directories that are not registered workspaces; such entries appear as ungrouped directories next to the workspaces they belong to.
55
+
56
+ ## Install
57
+
58
+ Install through the market (设置 → 插件市场) once the plugin is listed, or manually:
59
+
60
+ ```sh
61
+ # in the profile directory, e.g. ~/.dsh/profiles/desktop
62
+ pnpm add "dsh-workspace-api-key@github:COH2357/dsh-workspace-api-key"
63
+ ```
64
+
65
+ Then add `"dsh-workspace-api-key"` to `dsh.profile.bundles` in that profile's `package.json` and restart DSH. For local development, use a link instead:
66
+
67
+ ```jsonc
68
+ "dependencies": { "dsh-workspace-api-key": "link:/path/to/dsh-workspace-api-key" }
69
+ ```
70
+
71
+ ## Tests
72
+
73
+ ```sh
74
+ node test/host.test.mjs # 111 assertions
75
+ node test/client.test.mjs # 106 assertions
76
+ ```
77
+
78
+ Neither suite needs a running DSH process. The host suite drives `apply()` against a mock context (fake `credentials`, `workspaceRegistry`, `agents`, `sessionController`, `webServer`, stubbed `fetch`) and covers ref derivation, the session → workspace → default cascade, workspace matching, redirecting a second adapter's `apiKeyEnv`, clearing back to the default, 401/402/429/5xx classification, the loopback and origin checks, and state persistence. The client suite loads `lib/client.js` through a minimal `window.__ModuleLoader__` plus a small React runtime, renders the panel and the overlay, and asserts slot registration, the first render, session expand/collapse, per-session saving and testing, the ungrouped-directory card, the stale and mismatch prompts, the "go configure" hand-off, graceful degradation without a session list, and back-navigation when `ctx.get('layout')` only becomes available after the plugin was applied.
79
+
80
+ ## Requirements and limits
81
+
82
+ - Verified against DSH Desktop 2.0.17 with `@deepseek-ai/dsh*` 0.2.0-rc.2 and `@deepseek-ai/cordis` 4.0.4. It has no runtime dependency and imports no `@deepseek-ai/*` package — only the host services (`credentials`, `workspaceRegistry`, `agents`, `sessionController`, `webServer`, `llm`, `settings`) and Node built-ins.
83
+ - One key per level: a session has a single key whichever provider route reads it, and a key recorded for one provider is reported as a mismatch when the session runs on another. Model changes inside one provider are not treated as a mismatch, because a DeepSeek key is account-wide.
84
+ - Sessions are matched to a workspace by their directory (`cwd`). Sessions that never stored a `cwd` are not listed by the host's own `list()`, so they appear in no row.
85
+ - The connection test speaks the Anthropic Messages shape (`POST <baseURL>/messages` with `x-api-key` and `anthropic-version: 2023-06-01`), which is what the DeepSeek route uses. A provider that speaks a different protocol can still have keys bound per level, but the test may report a failure for a key that works.
86
+ - Keys are stored through the credentials service in `$DSH_HOME/.credentials.yaml` under `refs:`, in plaintext, exactly like the default key. They are not written into session logs, the plugin's state file, or the plugin directory.
87
+ - Matching is textual after normalization. If a session's `cwd` is a symlink or junction to a registered workspace path, it will not match and the level above is used.
88
+ - Uninstalling the plugin leaves the generated `*_WS_*` / `*_SS_*` refs in the credentials file; remove them from the Models page.
89
+ - If the panel lists workspaces but no expandable sessions, read the diagnostics line at the bottom of the panel. A red "the host half is still an older version" banner means the DSH process was never restarted: the host half is loaded once at startup, so closing the window or reloading the page is not enough — fully quit DSH Desktop and open it again. Otherwise the diagnostics line shows how many sessions the host sees and, when none of them match, sample session `cwd` values next to sample workspace paths.
90
+
91
+ ## License
92
+
93
+ MIT
package/README.zh.md ADDED
@@ -0,0 +1,93 @@
1
+ # dsh-workspace-api-key
2
+
3
+ [English](README.md) | 中文
4
+
5
+ 一个 DeepSeek Harness 插件:为每个工作区——必要时也可以为单个会话——单独绑定一把 DeepSeek API key。
6
+
7
+ DSH 只从一个全局凭据 ref(默认 `DEEPSEEK_API_KEY`)解析 DeepSeek key,于是所有工作区的所有会话花的都是同一把 key。这个插件加了一个面板,让单个工作区、甚至单个会话可以用自己的 key,这样一把 key 的花费就只落在你指定的会话上。没有单独配置的层级继续用上层设置:
8
+
9
+ ```
10
+ 会话 key → 工作区 key → 系统默认
11
+ ```
12
+
13
+ ## 你会得到
14
+
15
+ - **一行侧栏入口**,叫「API Key 分配」,紧跟在宿主自己的「插件」行下方,点开是全宽页面。
16
+ - **按工作区配置的 key** —— 列出 `workspaceRegistry` 服务里注册的全部工作区,各自一个 key 输入框。
17
+ - **按会话配置的 key** —— 每个工作区行可以展开(默认折叠),列出该目录下的会话,每个会话有自己的 key;从上层继承的会话会如实标注层级,而不是假装自己配了 key。
18
+ - **系统默认卡片** —— 当前生效的 ref、值的来源(`file`、`env` 等)以及打码后的值。
19
+ - **测试连接** —— 用该 key 真发一次 `POST <baseURL>/messages`(`max_tokens: 1`),返回 `ok`、`invalid`、`quota`、`rate-limit` 或 `unreachable`;测试成功会顺手清掉失效标记。
20
+ - **key 失效提醒** —— 401/403 把绑定的 key 标为 `invalid`,402 标为 `quota`,429 标为 `rate-limit`。当你点进一个生效 key 已失效的会话时,覆盖层会说明情况,并给出**去配置**、**改用上层设置**或**稍后**。
21
+ - **不匹配提醒** —— 如果绑 key 时记下的是某个服务商(比如 `deepseek-official`),而该会话现在跑在另一个服务商上,点进这个会话时会提醒你,因为那次请求会拿着对方不认的 key 发出去。
22
+ - **绝不悄悄换 key** —— 两种提醒都不会改动实际使用的 key,决定权始终在你手上;自动替换会直接破坏「按会话分离计费」这个初衷。
23
+ - **一键回退** —— 清空会话条目回到工作区 key,清空工作区条目回到系统默认。
24
+
25
+ ## 实现方式
26
+
27
+ 宿主半边(`lib/index.js`):
28
+
29
+ - 每个进程包装一次 `credentials.resolve`(通过 `ctx.effect` 在卸载时还原),而不是去注册第二个同名服务或第二条 provider 路由——对一个已被占用的名字,cordis 不允许这两者;
30
+ - 从 `agents.currentInitiator()?.session` 取当前会话的目录与 id:agent loop 把整个回合跑在那个异步作用域里,所以解析凭据时这两个值都拿得到,解析器据此选中「有 key 的最具体层级」;
31
+ - 把该目录与 `workspaceRegistry.list()` 做规范化后比对(绝对化、去掉尾部分隔符、Windows 折大小写);
32
+ - 由每一层派生出稳定的 ref —— 工作区是 `DEEPSEEK_API_KEY_WS_<sha1(规范化路径) 前 16 位十六进制大写>`,会话是 `DEEPSEEK_API_KEY_SS_<sha1(会话 id) 前 16 位十六进制大写>`(会话 id 含 `-`,不能直接当 ref 名)——通过凭据服务写入 key,并返回这个 ref 的值;
33
+ - 只改写 provider 真正会读的 ref:默认 ref、每个适配器配置的 `apiKeyEnv`,以及插件自己生成的 ref;其它 ref 原样透传;
34
+ - 会话列表来自 `sessionController` 服务(`list()`),所以面板显示的标题与模型投影和侧栏一致;该服务缺失时自动降级为只能按工作区配置;
35
+ - `/wsk-api/state` 会带上插件自身版本与会话探针(`available`、`count`、`withoutCwd`、`samples`、`error`,以及工作区路径样本),面板据此说明**为什么**没有列出会话:宿主半边是旧版本(只刷新了页面而没重启应用)、没有 `sessionController`、`list()` 抛错,还是会话的 `cwd` 与任何工作区路径都对不上;
36
+ - 自己的账本(哪一层对应哪个 ref、失效标记、key 是给哪个服务商记的)落在 `$DSH_HOME/storages/dsh-workspace-api-key.json`;
37
+ - 通过 `agent/request-error` waterfall 观察失败,但**从不**返回 `{kind: 'retry'}`——只记录判定结果并 `next()` 放行。
38
+
39
+ 浏览器半边(`lib/client.js`)注册到 `sidebar.panellist` 槽(`order: 1`)、`main` 槽(key 为 `workspace-api-key`)与 `shell.overlay` 槽(点进会话时的提醒),和 `dsh-skill-mcp-panel`、`dsh-429-guard` 用的是同一套槽。全部是朴素的 `React.createElement`,没有构建步骤,也不打包任何依赖。覆盖层向 `/wsk-api/check` 查询当前占据主视图的会话,每个会话最多问一次,它的按钮只负责把你带到面板——改 key 只能由面板里的操作完成。
40
+
41
+ ### HTTP 接口(仅限回环)
42
+
43
+ | 方法 | 路径 | 用途 |
44
+ | --- | --- | --- |
45
+ | GET | `/wsk-api/state` | 默认 ref 状态、工作区及其会话、覆盖项、失效标记 |
46
+ | GET | `/wsk-api/resolve` | 当前会话此刻解析到哪一层、哪个 ref |
47
+ | GET | `/wsk-api/check` | `?sessionId=…&path=…` 只读判定:生效层级、key、失效标记、服务商不匹配、`needsAttention` |
48
+ | POST | `/wsk-api/set` | `{scope: 'workspace' \| 'session', path \| sessionId, title, key, provider?}` 绑定 key;`{useDefault: true}` 或空 key 为清除 |
49
+ | POST | `/wsk-api/test` | `{scope, path \| sessionId \| ref}`(或 `{key}`)真发一次请求并返回判定;`invalid` 打标记,`ok` 清标记 |
50
+ | POST | `/wsk-api/clear-flag` | `{ref}` 或一个目标,清掉失效/配额标记 |
51
+
52
+ 只接受本机回环调用(`127.0.0.1`、`::1`、`::ffff:127.0.0.1`),且 `Origin` 必须同源或缺失。任何响应都不会包含明文 key,只有掩码。
53
+
54
+ 工作区目标也接受不是注册工作区的目录;这类条目会作为「未分组目录」和它们所属的工作区一起列在面板里。
55
+
56
+ ## 安装
57
+
58
+ 插件被市场收录后,可以直接在「设置 → 插件市场」里一键安装;也可以手动装:
59
+
60
+ ```sh
61
+ # 在 profile 目录里,例如 ~/.dsh/profiles/desktop
62
+ pnpm add "dsh-workspace-api-key@github:COH2357/dsh-workspace-api-key"
63
+ ```
64
+
65
+ 然后把 `"dsh-workspace-api-key"` 加进该 profile `package.json` 的 `dsh.profile.bundles`,重启 DSH。本地开发用 link 代替:
66
+
67
+ ```jsonc
68
+ "dependencies": { "dsh-workspace-api-key": "link:/path/to/dsh-workspace-api-key" }
69
+ ```
70
+
71
+ ## 测试
72
+
73
+ ```sh
74
+ node test/host.test.mjs # 111 条断言
75
+ node test/client.test.mjs # 106 条断言
76
+ ```
77
+
78
+ 两个套件都不需要启动 DSH。宿主套件用一个假 ctx(假的 `credentials`、`workspaceRegistry`、`agents`、`sessionController`、`webServer`,以及打桩的 `fetch`)驱动 `apply()`,覆盖 ref 派生、会话→工作区→默认的级联、工作区匹配、把第二个适配器的 `apiKeyEnv` 一起重定向、清回默认、401/402/429/5xx 的判定、回环与来源校验、状态落盘。浏览器套件通过一个极小的 `window.__ModuleLoader__` 与自制 React 运行时加载 `lib/client.js`,渲染出面板与覆盖层,断言槽注册、首屏渲染、会话折叠/展开、会话级保存与测试、未分组目录卡片、失效与不匹配提醒、「去配置」的交接、没有会话列表时的降级,以及 `ctx.get('layout')` 在插件 apply 之后才可用时返回键仍然有效。
79
+
80
+ ## 要求与限制
81
+
82
+ - 已在 DSH Desktop 2.0.17(`@deepseek-ai/dsh*` 0.2.0-rc.2、`@deepseek-ai/cordis` 4.0.4)上验证。插件没有运行时依赖,也不 import 任何 `@deepseek-ai/*` 包,只用宿主服务(`credentials`、`workspaceRegistry`、`agents`、`sessionController`、`webServer`、`llm`、`settings`)与 Node 内置模块。
83
+ - 每一层只有一把 key:一个会话无论被哪个 provider 路由读取,用的都是同一把 key;key 记下的服务商与会话当前服务商不一致时会被报为「不匹配」,但同一服务商内换模型不算不匹配,因为 DeepSeek key 是账号级的。
84
+ - 会话按目录(`cwd`)归入工作区。从未存过 `cwd` 的会话不会被宿主自己的 `list()` 列出,因此不会出现在任何行里。
85
+ - 「测试连接」说的是 Anthropic Messages 协议(`POST <baseURL>/messages` 带 `x-api-key` 与 `anthropic-version: 2023-06-01`),也就是 DeepSeek 路由用的协议;换了别的协议的 provider 一样可以按层绑 key,只是测试可能把有效的 key 报成失败。
86
+ - key 通过凭据服务存放在 `$DSH_HOME/.credentials.yaml` 的 `refs:` 里,明文,和默认 key 完全一样。它不会被写进会话日志、插件状态文件或插件目录。
87
+ - 匹配是规范化之后的文本比较。如果某个会话的 `cwd` 是指向已注册工作区路径的符号链接或 junction,则匹配不上,会走上层层级。
88
+ - 卸载插件不会删除已生成的 `*_WS_*` / `*_SS_*` ref,可在「设置 → 模型」里清掉。
89
+ - 如果面板列出了工作区却没有可展开的会话,请看面板底部的诊断行。出现红色「宿主端插件还是旧版本」横幅,说明 DSH 进程没有重启过:宿主半边只在启动时加载,关窗口和刷新页面都不算——请完全退出 DSH Desktop 再打开。其余情况诊断行会写明宿主看到多少个会话;一个都对不上时,还会并排给出会话 `cwd` 样本与工作区路径样本,便于比对。
90
+
91
+ ## 许可
92
+
93
+ MIT
@@ -0,0 +1,9 @@
1
+ # dsh-workspace-api-key bundle patch: contributes one profile row.
2
+ # 宿主半边(lib/index.js)包装 credentials.resolve,按「当前 agent 的
3
+ # session.header.cwd」把 key 解析重定向到工作区专属凭据 ref,并注册
4
+ # /wsk-api 路由供配置页读写;浏览器半边(lib/client.js)通过
5
+ # exports["./client"] 与 package.json 的 dsh.client 声明加载。
6
+
7
+ - insert:
8
+ - id: dsh-workspace-api-key
9
+ name: 'dsh-workspace-api-key'