dsh-plugin-tool-management 0.17.0 → 0.18.0
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/CHANGELOG.md +39 -0
- package/README.md +18 -3
- package/README_EN.md +18 -3
- package/docs/images/1-EN.png +0 -0
- package/docs/images/1.png +0 -0
- package/docs/images/2-EN.png +0 -0
- package/docs/images/2.png +0 -0
- package/docs/images/3-EN.png +0 -0
- package/docs/images/3.png +0 -0
- package/docs/images/4-EN.png +0 -0
- package/docs/images/4.png +0 -0
- package/docs/images/5-EN.png +0 -0
- package/docs/images/5.png +0 -0
- package/docs/images/6-EN.png +0 -0
- package/docs/images/6.png +0 -0
- package/docs/images/7-EN.png +0 -0
- package/docs/images/7.png +0 -0
- package/docs/images/8-EN.png +0 -0
- package/docs/images/8.png +0 -0
- package/docs/update.md +16 -0
- package/lib/client.js +1573 -133
- package/lib/compat/capability-specs.js +321 -0
- package/lib/compat/host-probe.js +261 -0
- package/lib/compat/host-roots.js +168 -0
- package/lib/compat/probe-types.js +6 -0
- package/lib/compat/probe.js +9 -720
- package/lib/context-inject-contract.js +114 -0
- package/lib/context-inject-frame.js +147 -0
- package/lib/context-inject-select.js +127 -0
- package/lib/context-inject-surface.js +97 -0
- package/lib/context-inject.js +10 -478
- package/lib/http-api.js +229 -0
- package/lib/hub.js +2 -0
- package/lib/index.js +134 -836
- package/lib/mcp/inspect-probe.js +199 -0
- package/lib/mcp/manager.js +30 -548
- package/lib/mcp/tool-visibility.js +388 -0
- package/lib/memories/archive-engine.js +142 -5
- package/lib/memories/archive.js +10 -1
- package/lib/memories/discovery.js +261 -0
- package/lib/memories/export-plan.js +42 -0
- package/lib/memories/memories-types.js +1 -0
- package/lib/memories/service.js +4 -38
- package/lib/memories/snapshot.js +10 -252
- package/lib/op-registry.js +21 -0
- package/lib/ops/compat.js +11 -0
- package/lib/ops/quick-prompts.js +26 -0
- package/lib/ops/scene-records.js +40 -0
- package/lib/ops/scene-sync.js +71 -1
- package/lib/ops/sessions.js +13 -9
- package/lib/ops/snapshot.js +18 -12
- package/lib/prompts/quick-service.js +211 -0
- package/lib/scene-linkage.js +184 -0
- package/lib/sessions/archive-schemas.js +121 -0
- package/lib/sessions/path-key.js +46 -0
- package/lib/sessions/workspace.js +5 -163
- package/lib/settings-sidecars.js +143 -0
- package/lib/skills/core-types.js +8 -0
- package/lib/skills/core-util.js +81 -0
- package/lib/skills/core.js +56 -2326
- package/lib/skills/dir-watch.js +71 -0
- package/lib/skills/frontmatter.js +96 -0
- package/lib/skills/homes.js +14 -0
- package/lib/skills/import-pipeline.js +231 -0
- package/lib/skills/manager-state.js +800 -0
- package/lib/skills/official.js +269 -0
- package/lib/skills/provider-registration.js +89 -0
- package/lib/skills/service.js +5 -240
- package/lib/skills/skill-roots.js +287 -0
- package/lib/skills/skill-trash.js +265 -0
- package/lib/skills/system-trash.js +97 -0
- package/lib/skills/upload-pipeline.js +374 -0
- package/lib/slash-settings.js +25 -0
- package/lib/subagents/persona-parse.js +312 -0
- package/lib/subagents/persona-types.js +5 -0
- package/lib/subagents/persona-write.js +93 -0
- package/lib/subagents/run-provider.js +159 -0
- package/lib/subagents/service.js +14 -638
- package/lib/subagents/tool-filter.js +98 -0
- package/lib/token-config.js +240 -0
- package/lib/tool-table-switch.js +163 -0
- package/lib/tools/scene.js +40 -9
- package/package.json +3 -2
package/lib/http-api.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { noteRuntime } from './compat/runtime-notes.js';
|
|
2
|
+
import { recordOpAudit } from './audit-log.js';
|
|
3
|
+
import { isReadCall } from './op-registry.js';
|
|
4
|
+
import { TOKEN_CODE_BAD, TOKEN_MSG, fenceRejection, secretOpRejection } from './http-fence.js';
|
|
5
|
+
export function installHttpApi(deps) {
|
|
6
|
+
// ---------- HTTP API route (UI half), registered defensively ----------
|
|
7
|
+
if (deps.webServer) {
|
|
8
|
+
// Cap request bodies (88 MiB): skill-upload carries Base64 folder/ZIP
|
|
9
|
+
// payloads (64 MiB of raw content). Reachability is decided by
|
|
10
|
+
// fenceRejection() below — the host's Host/Origin + browser-session fence
|
|
11
|
+
// (or a correct explicit token). `config.maxBodyBytes` overrides the cap
|
|
12
|
+
// (tests use a small value).
|
|
13
|
+
const configuredMaxBody = Number(deps.config?.maxBodyBytes);
|
|
14
|
+
// 默认 88 MiB:够塞下一份带多张大附件(图片 / PDF)的技能或记忆打包体,又不至于让
|
|
15
|
+
// 一个失控的客户端把进程内存吃光。`config.maxBodyBytes` 可覆盖(测试用小值)。
|
|
16
|
+
const DEFAULT_MAX_BODY_BYTES = 88 * 1024 * 1024;
|
|
17
|
+
const MAX_BODY = Number.isFinite(configuredMaxBody) && configuredMaxBody > 0 ? configuredMaxBody : DEFAULT_MAX_BODY_BYTES;
|
|
18
|
+
const readBody = (req) => new Promise((resolve, reject) => {
|
|
19
|
+
// 先看 content-length:注定超限的请求在**读第一个字节之前**就拒掉。以前只能边收边数,
|
|
20
|
+
// 一个 5 GB 的上传会先让我们缓冲满 88 MiB 才回错。读取流照旧 drain(一个字节都不留),
|
|
21
|
+
// 否则响应写完后服务端会直接拆连接,客户端可能在上传中途拿到网络错误而不是下面这句 JSON。
|
|
22
|
+
// 错误消息必须含 `body too large` —— 后面的 catch 靠这个子串分流。
|
|
23
|
+
const declared = Number(req.headers && req.headers['content-length']);
|
|
24
|
+
if (Number.isFinite(declared) && declared > MAX_BODY) {
|
|
25
|
+
const drain = req.resume;
|
|
26
|
+
if (typeof drain === 'function')
|
|
27
|
+
drain.call(req);
|
|
28
|
+
reject(new Error('request body too large'));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// Accumulate Buffers and decode ONCE at the end: decoding each chunk
|
|
32
|
+
// separately corrupts multi-byte UTF-8 characters that straddle a
|
|
33
|
+
// chunk boundary (e.g. long Chinese skill bodies).
|
|
34
|
+
const chunks = [];
|
|
35
|
+
let size = 0;
|
|
36
|
+
req.on('data', (c) => {
|
|
37
|
+
const buf = Buffer.isBuffer(c) ? c : Buffer.from(String(c));
|
|
38
|
+
size += buf.length;
|
|
39
|
+
if (size > MAX_BODY) {
|
|
40
|
+
reject(new Error('request body too large'));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
chunks.push(buf);
|
|
44
|
+
});
|
|
45
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
46
|
+
req.on('error', reject);
|
|
47
|
+
});
|
|
48
|
+
// 宿主 BrowserAuth 栅栏。宿主的鉴权**只包在它自己注册的路由上**——dsh-host-webserver
|
|
49
|
+
// 没有全局中间件,既定约定是每个注册方自己调 connection.requestRejection(req):
|
|
50
|
+
// 它先做 Host/Origin 栅栏(Host 必须是回环或 deployment 派生的 LAN IP 字面量,
|
|
51
|
+
// 这是 DNS rebinding 唯一伪造不了的头),再叠加 browser-session cookie 鉴权。
|
|
52
|
+
// 本插件此前只查公开常量 x-dsh-plugin 与自比对(Origin.host === Host)的 "同源",
|
|
53
|
+
// 两者都挡不住 rebind 后的网页或本机任意进程 —— 等于没鉴权。现在优先走宿主栅栏。
|
|
54
|
+
const connection = (() => {
|
|
55
|
+
try {
|
|
56
|
+
return deps.ctx.get('connection');
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
})();
|
|
62
|
+
try {
|
|
63
|
+
// ctx.effect wires the route's disposer into this plugin's scope, so an
|
|
64
|
+
// unload (HMR removal, disable, update) unregisters the route — the
|
|
65
|
+
// documented cleanup contract (webServer.register does not auto-scope).
|
|
66
|
+
deps.ctx.effect(() => deps.webServer.register({
|
|
67
|
+
kind: 'exact',
|
|
68
|
+
path: '/dsh-plugin-tool-management/api',
|
|
69
|
+
handler: async (req, res) => {
|
|
70
|
+
// 鉴权顺序:POST-only → 插件门禁头(防误触,不是凭证)→ 宿主栅栏
|
|
71
|
+
// (Host/Origin + browser-session cookie)→ 可选访问令牌。令牌是本地工具与
|
|
72
|
+
// LAN 部署的逃生门:**令牌正确即视为已授权**,可越过浏览器鉴权。
|
|
73
|
+
//
|
|
74
|
+
// 「未配置令牌时」别读成"仍有门禁":下面的写 op 判据是
|
|
75
|
+
// `if (TOKEN && WRITE_OPS.has(op))` —— `TOKEN === ''` 时整条短路,79 个写 op
|
|
76
|
+
// 只剩 `fenceRejection` 一道。而它自身有两种形态:宿主栅栏在场时含 cookie 鉴权;
|
|
77
|
+
// 栅栏缺席或抛错时退回本地判定,**只查回环 Host 与(若给了)Origin 自比对**,
|
|
78
|
+
// 没有 cookie 那一层 —— 本机任意进程都能调写 op。所以准确说法是:
|
|
79
|
+
// 未配置令牌时不再有「**无栅栏**放行」的缺口,但进程级边界只有配了令牌才有。
|
|
80
|
+
const hdr = (name) => {
|
|
81
|
+
const v = req.headers?.[name];
|
|
82
|
+
return Array.isArray(v) ? v[0] ?? '' : v ?? '';
|
|
83
|
+
};
|
|
84
|
+
if (String(req.method || 'POST').toUpperCase() !== 'POST') {
|
|
85
|
+
res.writeHead(405, { 'content-type': 'application/json' });
|
|
86
|
+
res.end(JSON.stringify({ ok: false, error: 'method not allowed' }));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (hdr('x-dsh-plugin') !== 'dsh-plugin-tool-management') {
|
|
90
|
+
res.writeHead(403, { 'content-type': 'application/json' });
|
|
91
|
+
res.end(JSON.stringify({ ok: false, error: 'missing plugin gate header' }));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// 「本次带的这串对不对」比的是**存量**令牌:关掉令牌功能之后仍然要凭它才能开回来、
|
|
95
|
+
// 换掉、或看明文(要求 9)。
|
|
96
|
+
const tokenAccepted = deps.CONFIG_TOKEN !== '' && deps.tokenMatches(hdr('x-dsh-token'));
|
|
97
|
+
// 验过就置闩(本次进程一次即可)。`agent/pre-step` 的令牌门禁读的就是它 ——
|
|
98
|
+
// 那一边看不到请求头,只能由这里告诉它"这台机器已经有人验过令牌了"。
|
|
99
|
+
if (tokenAccepted)
|
|
100
|
+
deps.markAccepted();
|
|
101
|
+
if (!tokenAccepted) {
|
|
102
|
+
const fence = fenceRejection(req, connection);
|
|
103
|
+
if (fence) {
|
|
104
|
+
res.writeHead(fence.status, { 'content-type': 'application/json' });
|
|
105
|
+
res.end(JSON.stringify({ ok: false, error: fence.error }));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
110
|
+
try {
|
|
111
|
+
let payload = {};
|
|
112
|
+
try {
|
|
113
|
+
payload = JSON.parse((await readBody(req)) || '{}');
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
if (String(e?.message).includes('body too large')) {
|
|
117
|
+
res.end(JSON.stringify({ ok: false, error: '请求体过大' }));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
/* otherwise fall through with {} */
|
|
121
|
+
}
|
|
122
|
+
const op = String(payload.op || '');
|
|
123
|
+
// 敏感 op(会吐明文密钥与完整配置):**配了令牌就必须带对的**,没配令牌则不设防
|
|
124
|
+
// (判定与理由见 http-fence.ts 的 secretOpRejection —— 用户裁定 2026-09-19:
|
|
125
|
+
// 没配令牌时那道门没有出路,用户无从填一个不存在的令牌)。
|
|
126
|
+
// 之前这里只查"有没有 Origin 头",而 token 为空时它形同虚设 —— 任何能
|
|
127
|
+
// 打开 GUI 的浏览器都拿得到全部密钥原文。
|
|
128
|
+
if (deps.SENSITIVE_OPS.has(op)) {
|
|
129
|
+
// 明文门禁看的是**存量**令牌(CONFIG_TOKEN 而不是生效值 TOKEN):关掉令牌
|
|
130
|
+
// 功能之后仍然"要凭存量令牌才给明文"—— 那条路是通的(填对就能看),
|
|
131
|
+
// 与"从来没配过"不同。
|
|
132
|
+
const gate = secretOpRejection({ tokenConfigured: deps.CONFIG_TOKEN !== '', tokenAccepted });
|
|
133
|
+
if (gate) {
|
|
134
|
+
res.end(JSON.stringify({ ok: false, error: gate.error, code: gate.code }));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// 「读改写」两态 op(`inject-settings` / `tool-table` / `scene-settings` /
|
|
139
|
+
// `mcpm-settings`):同一个名字既读又写,于是整条 op 被列进 WRITE_OPS —— 读侧也
|
|
140
|
+
// 一起被拦,界面表现是"没填令牌时整块设置消失、填了还要刷新才出现"(2026-09-19
|
|
141
|
+
// 用户报的)。判据是**这次调用要不要写**,不是 op 名在不在名单里。
|
|
142
|
+
// 判据本身在登记表(`writeWhen`),这里不再手写一份 —— 手写那份只认 `set:true`,
|
|
143
|
+
// 于是 `tool-table` 的 `presetSave` / `presetDelete` 被当成读放行,配了令牌也能
|
|
144
|
+
// 只凭宿主栅栏改侧车(审查 F2,0.17.0 修)。
|
|
145
|
+
const readOnlyCall = isReadCall(op, payload.args);
|
|
146
|
+
if (deps.TOKEN && deps.WRITE_OPS.has(op) && !readOnlyCall && !deps.tokenMatches(hdr('x-dsh-token'))) {
|
|
147
|
+
// 文案与明文门禁同源(http-fence.ts),code 也统一 —— 界面据此在最右侧挂
|
|
148
|
+
// 「去填令牌」跳转按钮。这条错误会出现在**任意**页面,而入口只有一个。
|
|
149
|
+
res.end(JSON.stringify({ ok: false, code: TOKEN_CODE_BAD, error: TOKEN_MSG }));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// 令牌状态(只读,**不需要**令牌本身):回答"宿主配没配"、"本次带的这串对不对"、
|
|
153
|
+
// "配置文件里的改动是不是还没重启生效"、"本次进程的标识"。就地回答而不是进 handlers 表:
|
|
154
|
+
// 前者的答案来自本次请求头与进程状态,而后者的 handlers 只拿得到 args。
|
|
155
|
+
// 不返回令牌、也不返回它的任何片段 —— 泄给同源页面等于把最后一道门交出去。
|
|
156
|
+
if (op === 'token-status') {
|
|
157
|
+
const state = await deps.tokenConfigState();
|
|
158
|
+
res.end(JSON.stringify({
|
|
159
|
+
ok: true,
|
|
160
|
+
// 「宿主配置了令牌」= 配置里**有**值(关掉也算有:随时能开回来)。
|
|
161
|
+
hostConfigured: deps.CONFIG_TOKEN !== '',
|
|
162
|
+
// 当前进程里令牌功能是开还是关(关掉 = 写操作不要求令牌)。
|
|
163
|
+
disabled: deps.TOKEN_DISABLED,
|
|
164
|
+
// 「当前进程里令牌**在生效**」= 写操作要不要令牌。关掉或没配都是 false。
|
|
165
|
+
// 界面据此判断"填令牌"这件事有没有意义(没生效时填了也没人验,还会把
|
|
166
|
+
// 一句无处可填的提示挂在屏幕上 —— 用户 2026-09-19 报的就是这个)。
|
|
167
|
+
active: deps.TOKEN !== '',
|
|
168
|
+
accepted: tokenAccepted,
|
|
169
|
+
bootId: deps.BOOT_ID,
|
|
170
|
+
configHasToken: state.configHasToken,
|
|
171
|
+
configDisabled: state.configDisabled,
|
|
172
|
+
configFound: state.found,
|
|
173
|
+
pendingRestart: state.pendingRestart,
|
|
174
|
+
}));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// 设置 / 关闭令牌:同样就地处理(理由见 tokenConfigure 的注释 —— 也正因为不进
|
|
178
|
+
// handlers,模型侧没有任何工具能间接关掉它)。
|
|
179
|
+
if (op === 'token-configure') {
|
|
180
|
+
res.end(JSON.stringify(deps.withPatchWarnings(await deps.tokenConfigure(payload.args || {}, tokenAccepted))));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// 「清除令牌」的配套:把「本次启动已验过」的闩重新挂上。没有它,解锁一次之后
|
|
184
|
+
// 清除令牌 + 强刷新,浏览器里已无令牌,闩却挂着 —— pre-step 门禁读闩
|
|
185
|
+
// (tokenGateActive),整个启动期都放行对话(2026-09-19 用户实测的漏洞)。
|
|
186
|
+
// 不要求凭证:它只会收紧(要求重新验令牌),给不了任何人任何权限;同样不进
|
|
187
|
+
// handlers —— 模型不该有能力拨这颗闩。
|
|
188
|
+
if (op === 'token-unaccept') {
|
|
189
|
+
deps.unmarkAccepted();
|
|
190
|
+
res.end(JSON.stringify({ ok: true }));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const fn = deps.handlers[op];
|
|
194
|
+
if (!fn) {
|
|
195
|
+
res.end(JSON.stringify({ ok: false, error: '未知操作: ' + op }));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const result = await fn(payload.args || {});
|
|
199
|
+
// 「最近改动」流水:来源 = 面板(这条 HTTP 路由是界面唯一的入口)。
|
|
200
|
+
// 「这次算不算写」的判据在 recordOpAudit 里(与写门禁同源:登记表的 `writeWhen`)
|
|
201
|
+
// —— 不在这里再传一次 `readOnlyCall`,两处各判一次迟早会分家。
|
|
202
|
+
recordOpAudit(op, payload.args || {}, 'panel', result);
|
|
203
|
+
res.end(JSON.stringify(deps.withPatchWarnings(result === undefined ? { ok: true } : result)));
|
|
204
|
+
}
|
|
205
|
+
catch (e) {
|
|
206
|
+
res.end(JSON.stringify({ ok: false, error: deps.message(e) }));
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
}), 'dsh-plugin-tool-management: api route');
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
// A registration failure must never take down the whole entry: log and continue.
|
|
213
|
+
console.error('[dsh-plugin-tool-management] webServer route registration failed:', deps.message(e));
|
|
214
|
+
// 结构性弱点(2026-09-30 审查 §5 F4):compat-status **本身就走这条路由**,所以路由没注册上
|
|
215
|
+
// 时兼容页也拿不到这条降级行 —— 面板是**整页失联**,不是"少一行"。这里至少把它写进运行期
|
|
216
|
+
// 上报通道(日志里必然有一条,进程内可查),并在 README 里写明"面板空白先看宿主日志"。
|
|
217
|
+
// 之所以不是"必然可见"的告警:可见的通道就是这条路由,它自己挂了就没得显示。
|
|
218
|
+
noteRuntime({
|
|
219
|
+
id: 'web-server-route',
|
|
220
|
+
label: 'HTTP 路由注册',
|
|
221
|
+
kind: 'write',
|
|
222
|
+
fallback: 'inform-only',
|
|
223
|
+
detail: '向宿主 webServer 注册 API 路由失败(' + deps.message(e) + '):插件面板与全部 HTTP op 不可用(兼容页也走这条路由,所以它也打不开)。宿主日志里有同一条记录;模型工具不受影响。',
|
|
224
|
+
detailKey: 'web-server-route.register-failed',
|
|
225
|
+
params: { reason: deps.message(e) },
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
package/lib/hub.js
CHANGED