dsh-plugin-tool-management 0.16.6 → 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 +115 -0
- package/README.md +20 -4
- package/README_EN.md +20 -4
- 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 +59 -0
- package/lib/audit-log.js +6 -2
- package/lib/client.js +1615 -148
- 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 +54 -714
- 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 +57 -459
- package/lib/host-names.js +150 -0
- package/lib/http-api.js +229 -0
- package/lib/hub.js +84 -11
- package/lib/imports/parsers.js +40 -15
- package/lib/index.js +235 -843
- package/lib/mcp/inspect-probe.js +199 -0
- package/lib/mcp/loader-token.js +26 -0
- package/lib/mcp/manager.js +267 -558
- package/lib/mcp/patch-yaml.js +207 -40
- package/lib/mcp/secret-guard.js +122 -16
- package/lib/mcp/tool-visibility.js +388 -0
- package/lib/memories/archive-engine.js +163 -16
- 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/index-io.js +125 -12
- package/lib/memories/memories-types.js +1 -0
- package/lib/memories/service.js +167 -78
- package/lib/memories/snapshot.js +41 -260
- package/lib/op-registry.js +72 -8
- package/lib/ops/compat.js +17 -2
- package/lib/ops/prompts.js +18 -5
- package/lib/ops/quick-prompts.js +26 -0
- package/lib/ops/scene-records.js +157 -10
- package/lib/ops/scene-sync.js +71 -1
- package/lib/ops/sessions.js +63 -13
- package/lib/ops/snapshot.js +123 -24
- package/lib/ops/trash.js +45 -9
- package/lib/prompts/quick-service.js +211 -0
- package/lib/prompts/service.js +7 -2
- package/lib/request-gate.js +115 -56
- package/lib/scene-linkage.js +184 -0
- package/lib/scene-prompt-sync.js +46 -7
- package/lib/scenes/candidates.js +21 -3
- package/lib/sessions/archive-schemas.js +121 -0
- package/lib/sessions/path-key.js +46 -0
- package/lib/sessions/workspace.js +45 -168
- 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 +97 -2297
- 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 +4 -234
- 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 +20 -639
- 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
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
// 工具停用的那一层边界:停用表(TTL 缓存)、「已知工具」侧车、以及把停用名单装到每个
|
|
2
|
+
// agent scope 上的可见性半边(restrict)+ 执行侧要用的开关写入口。
|
|
3
|
+
//
|
|
4
|
+
// 正文从 mcp/manager.ts 的 createMcpManager 闭包里整段搬来,一字未改(缩进本来就同层;
|
|
5
|
+
// 改的只有闭包作用域的名字走 deps —— moved-verify 按这两类容差机检):
|
|
6
|
+
// ensurePaths / readJsonFile / writeJsonFile / withWriteLock / message / tools /
|
|
7
|
+
// normalizeKnownTools,以及侧车的三件常量 SIDECAR_TTL_MS / MCP_DISABLED_TOOLS_FILE /
|
|
8
|
+
// MCP_KNOWN_TOOLS_FILE 与落点函数 mcpSidecar。常量按**原名**传进来,是为了让搬来的那些行
|
|
9
|
+
// 不必改名 —— 这一段读起来像「读一次侧车、 TTL 内不再读盘」,靠的就是那几个熟悉的名字。
|
|
10
|
+
// deps 形参仍叫 deps:块里那五处 `deps.carrierHiddenTools` / `deps.pluginHiddenTools` 是
|
|
11
|
+
// 原样文本,换名就要改行。
|
|
12
|
+
//
|
|
13
|
+
// 为什么这一簇必须整体搬:七个可见性状态量(disabledToolsCache / knownMcpToolsCache /
|
|
14
|
+
// restrictTimer / appliedNamesKey / desiredNames / agentRestrictions / appliedByAgent /
|
|
15
|
+
// seenAgents / epoch)里,只有 disabledToolsCache 被块外的 ops 段直接改写(改名、删台、
|
|
16
|
+
// 整表回写三处 + 出口读两处),所以它由本模块交回两个句柄:setDisabledToolsCache(按原
|
|
17
|
+
// 语义刷新 `{at, value}` 那份缓存)与 disabledToolsSnapshot(同步读,无 I/O —— 工具门禁在
|
|
18
|
+
// 热路径上)。其余名字块外零引用,跟着走就行。
|
|
19
|
+
import { MCP_TOOL_PREFIX } from '../host-names.js';
|
|
20
|
+
import { clearRuntimeNote, noteRuntime } from '../compat/runtime-notes.js';
|
|
21
|
+
export function createToolVisibility(deps) {
|
|
22
|
+
// ---------- per-tool enable/disable (execution + visibility boundary) ----------
|
|
23
|
+
// dsh-mcp-client has no per-tool config, but the DSH tool runtime exposes
|
|
24
|
+
// two official seams: `tools.restrict({ deny })` removes a global tool from
|
|
25
|
+
// every scope's model-visible schema list, and `tools.guard` denies the call
|
|
26
|
+
// before the body runs. Disabled tools therefore become invisible AND
|
|
27
|
+
// uncallable — no patch rewrite, no DSH restart.
|
|
28
|
+
let disabledToolsCache = null;
|
|
29
|
+
async function readDisabledTools(force = false) {
|
|
30
|
+
if (disabledToolsCache && !force && Date.now() - disabledToolsCache.at < deps.SIDECAR_TTL_MS)
|
|
31
|
+
return disabledToolsCache.value;
|
|
32
|
+
const p = await deps.ensurePaths();
|
|
33
|
+
const raw = await deps.readJsonFile(deps.mcpSidecar(deps.MCP_DISABLED_TOOLS_FILE));
|
|
34
|
+
const out = {};
|
|
35
|
+
if (raw && typeof raw === 'object') {
|
|
36
|
+
for (const serverName of Object.keys(raw)) {
|
|
37
|
+
const list = raw[serverName];
|
|
38
|
+
if (Array.isArray(list)) {
|
|
39
|
+
// `*` 必须原样保留:它是「整台服务器停用」的通配(场景档案未勾选的服务器写的就是它),
|
|
40
|
+
// 被这里过滤掉的话 guard / restrict / 快照三条链路一起失效——整台停用变成空转。
|
|
41
|
+
const names = list.map((name) => String(name)).filter((name) => name === '*' || /^[A-Za-z0-9_-]{1,128}$/.test(name));
|
|
42
|
+
if (names.length)
|
|
43
|
+
out[serverName] = names;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
disabledToolsCache = { at: Date.now(), value: out };
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
/** Fully-qualified model-facing names (`mcp__<serverName>__<tool>`) of every disabled tool. */
|
|
51
|
+
function disabledToolNames(map) {
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const serverName of Object.keys(map)) {
|
|
54
|
+
for (const tool of map[serverName])
|
|
55
|
+
out.push(MCP_TOOL_PREFIX + serverName + '__' + tool);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
// ---------- 「已知工具」侧车(v0.8.1)-----------------------------------
|
|
60
|
+
// MCP 工具名只在服务器**运行**时可见(tools.schemas() 里才有)。未运行的服务器在
|
|
61
|
+
// 场景档案里既显示 0 工具、工具明细也空空如也 —— 用户没法给未运行服务器挑工具。
|
|
62
|
+
// 每次枚举到 live 工具时按 serverName 记一份「最后见过的工具名」,未运行时用它兜底:
|
|
63
|
+
// 场景档案能列出/勾选,进入场景后按实际注册的工具生效(restrict 会过滤掉不存在的名字)。
|
|
64
|
+
let knownMcpToolsCache = null;
|
|
65
|
+
async function readKnownMcpTools(force = false) {
|
|
66
|
+
if (knownMcpToolsCache && !force && Date.now() - knownMcpToolsCache.at < deps.SIDECAR_TTL_MS)
|
|
67
|
+
return knownMcpToolsCache.value;
|
|
68
|
+
const p = await deps.ensurePaths();
|
|
69
|
+
const raw = await deps.readJsonFile(deps.mcpSidecar(deps.MCP_KNOWN_TOOLS_FILE));
|
|
70
|
+
const out = {};
|
|
71
|
+
if (raw && typeof raw === 'object') {
|
|
72
|
+
for (const serverName of Object.keys(raw)) {
|
|
73
|
+
const list = deps.normalizeKnownTools(raw[serverName]);
|
|
74
|
+
if (list.length)
|
|
75
|
+
out[serverName] = list;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
knownMcpToolsCache = { at: Date.now(), value: out };
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
async function writeKnownMcpTools(value) {
|
|
82
|
+
const p = await deps.ensurePaths();
|
|
83
|
+
await deps.writeJsonFile(deps.mcpSidecar(deps.MCP_KNOWN_TOOLS_FILE), value);
|
|
84
|
+
knownMcpToolsCache = { at: Date.now(), value: value };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 「已知工具」侧车的**读-改-写**,整段在写锁内。
|
|
88
|
+
*
|
|
89
|
+
* 为什么不能「先 readKnownMcpTools() 再 writeKnownMcpTools()」(2026-09-30 审查 P2-15):
|
|
90
|
+
* 这个侧车有两个并发写者 —— 列表轮询(`mcpmListView`,每次发现新工具就回写)与
|
|
91
|
+
* 临时启动(`mcpmTools` 等到工具注册后回写)。两步之间是队外窗口,两个写者各读到同一份
|
|
92
|
+
* 旧表、各并进自己那一台服务器的工具,后写的把先写的整份覆盖掉 —— 先那台的"曾经见过"
|
|
93
|
+
* 记录消失,未运行时场景档案里又变成 0 工具。
|
|
94
|
+
*
|
|
95
|
+
* `mutate` 收到的是**锁内刚读出来**的表(`force` 绕 TTL,否则拿到的还是锁外那份缓存);
|
|
96
|
+
* 返回 `true` 表示有变化、需要落盘。
|
|
97
|
+
* 注意:`withWriteLock` **不可重入** —— 调用方不得已经持有写锁。
|
|
98
|
+
*/
|
|
99
|
+
async function mutateKnownMcpTools(mutate) {
|
|
100
|
+
return deps.withWriteLock(async () => {
|
|
101
|
+
const known = await readKnownMcpTools(true);
|
|
102
|
+
if (!mutate(known))
|
|
103
|
+
return;
|
|
104
|
+
await writeKnownMcpTools(known);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 把「这次 live 见到的工具」并进一份已知工具表;返回是否有变化。
|
|
109
|
+
*
|
|
110
|
+
* 抽出来是为了让"要不要写盘"的判断与"锁内真正合并"用**同一个**函数:调用方先在锁外
|
|
111
|
+
* 对本地缓存跑一遍拿到 `changed`(决定要不要进锁),进锁后再对新鲜表跑一遍(那次的结果
|
|
112
|
+
* 才决定落盘)—— 两边口径不一致的话,要么白写要么漏写。
|
|
113
|
+
*/
|
|
114
|
+
function mergeLiveToolsInto(known, live) {
|
|
115
|
+
let changed = false;
|
|
116
|
+
for (const [server, list] of Object.entries(live)) {
|
|
117
|
+
const prev = known[server] || [];
|
|
118
|
+
const byName = new Map(prev.map((t) => [t.name, t]));
|
|
119
|
+
for (const tool of list) {
|
|
120
|
+
const old = byName.get(tool.name);
|
|
121
|
+
if (!old) {
|
|
122
|
+
byName.set(tool.name, tool);
|
|
123
|
+
changed = true;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (tool.description && old.description !== tool.description) {
|
|
127
|
+
byName.set(tool.name, { ...old, description: tool.description });
|
|
128
|
+
changed = true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const next = [...byName.values()].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
132
|
+
if (next.length !== prev.length)
|
|
133
|
+
changed = true;
|
|
134
|
+
known[server] = next;
|
|
135
|
+
}
|
|
136
|
+
return changed;
|
|
137
|
+
}
|
|
138
|
+
async function mcpmToolEnabled(args) {
|
|
139
|
+
const serverName = String((args && args.serverName) || '').trim();
|
|
140
|
+
const tool = String((args && args.tool) || '').trim();
|
|
141
|
+
if (!/^[A-Za-z0-9_-]{1,32}$/.test(serverName))
|
|
142
|
+
return { ok: false, error: 'serverName 不合法' };
|
|
143
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/.test(tool))
|
|
144
|
+
return { ok: false, error: 'tool 名称不合法' };
|
|
145
|
+
const enabled = (args && args.enabled) !== false;
|
|
146
|
+
const p = await deps.ensurePaths();
|
|
147
|
+
return deps.withWriteLock(async () => {
|
|
148
|
+
// Force re-read so an externally edited disabled-tools file is merged, not clobbered.
|
|
149
|
+
const map = Object.assign({}, await readDisabledTools(true));
|
|
150
|
+
const disabled = new Set(map[serverName] || []);
|
|
151
|
+
if (enabled)
|
|
152
|
+
disabled.delete(tool);
|
|
153
|
+
else
|
|
154
|
+
disabled.add(tool);
|
|
155
|
+
if (disabled.size)
|
|
156
|
+
map[serverName] = [...disabled].sort();
|
|
157
|
+
else
|
|
158
|
+
delete map[serverName];
|
|
159
|
+
try {
|
|
160
|
+
await deps.writeJsonFile(deps.mcpSidecar(deps.MCP_DISABLED_TOOLS_FILE), map);
|
|
161
|
+
}
|
|
162
|
+
catch (e) {
|
|
163
|
+
return { ok: false, error: '保存失败: ' + deps.message(e) };
|
|
164
|
+
}
|
|
165
|
+
disabledToolsCache = { at: Date.now(), value: map };
|
|
166
|
+
await applyToolRestrictions();
|
|
167
|
+
return { ok: true, serverName, disabled: map[serverName] || [] };
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
// Visibility seam: keep one active restriction per **agent scope**, refreshed whenever
|
|
171
|
+
// the tool set / the disabled set / this agent's carrier set changes. Three facts decide
|
|
172
|
+
// this design(都读过官方源码 dsh-tools 0.1.5-rc.2):
|
|
173
|
+
// · `restrict()` **要求 scoped context** —— 插件级 ctx 调用必抛 "requires a scoped
|
|
174
|
+
// context",这正是「停用工具从模型可见 schema 消失」这半边此前从未生效的原因(V1);
|
|
175
|
+
// · 每次 layer 变化(restriction 就是一次 layer effect)官方都会 emit `tools/change`
|
|
176
|
+
// (`layers = new ScopedLayers(…, () => this.ctx.emit("tools/change"))`),而重放正挂在
|
|
177
|
+
// `tools/change` 上 —— 名单没变还重放就是自激;
|
|
178
|
+
// · 表要取自**不受限**的插件级视图:在 agent scope 里取 `schemas()`,第一次限制生效后
|
|
179
|
+
// 表里就没有被停用的工具了,第二次重放会把它从 deny 名单里筛掉 —— 停用静默复活。
|
|
180
|
+
// (2026-09-23:名单有三处来源,其中 `carrierHiddenTools` 逐 agent 不同,所以从"一份名单给
|
|
181
|
+
// 所有 agent"改成逐 agent 各装一层 —— 见下面 restrictAgent / appliedByAgent 的注释。)
|
|
182
|
+
let restrictTimer = null;
|
|
183
|
+
/** 已应用名单的键;相同即返回(防自激,也让每次 tools/change 变成一次廉价判等)。 */
|
|
184
|
+
let appliedNamesKey = null;
|
|
185
|
+
/** 当前期望的**全局** deny 名单(agent 晚到 / 重放时用它,再叠加该 agent 自己那份)。 */
|
|
186
|
+
let desiredNames = [];
|
|
187
|
+
/** agent → 撤掉这层限制的 disposer。 */
|
|
188
|
+
const agentRestrictions = new Map();
|
|
189
|
+
/** agent → 已应用的**合并后**名单键(名单现在与 agent 有关,不能再只记一份全局的)。 */
|
|
190
|
+
const appliedByAgent = new Map();
|
|
191
|
+
/** 见过的 agent(重放时逐个重新应用;disposed 时移除)。 */
|
|
192
|
+
const seenAgents = new Set();
|
|
193
|
+
/**
|
|
194
|
+
* 重放代数:`carrierHiddenTools` 是异步的,算名单期间可能又来了新的一轮重放(或 agent
|
|
195
|
+
* 已经下线)。每次重放自增,回来时代数对不上就作废 —— 否则会用旧名单盖掉新的那一层。
|
|
196
|
+
*/
|
|
197
|
+
let epoch = 0;
|
|
198
|
+
function scopedToolsOf(agent) {
|
|
199
|
+
const scoped = agent && agent.ctx && agent.ctx.tools;
|
|
200
|
+
return scoped && typeof scoped === 'object' ? scoped : undefined;
|
|
201
|
+
}
|
|
202
|
+
/** 撤掉某个 agent 上的限制(名单变了要重装:`restrict` 是叠加层,装新的不撤旧的会越叠越多)。 */
|
|
203
|
+
function liftRestriction(agent) {
|
|
204
|
+
const release = agentRestrictions.get(agent);
|
|
205
|
+
agentRestrictions.delete(agent);
|
|
206
|
+
appliedByAgent.delete(agent);
|
|
207
|
+
if (release) {
|
|
208
|
+
try {
|
|
209
|
+
release();
|
|
210
|
+
}
|
|
211
|
+
catch (e) { /* ignore */ }
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/** 把该 agent 该背的名单装到它 scope 上(全局那份 ∪ 它自己那份;空名单 = 不碰它)。 */
|
|
215
|
+
async function restrictAgent(agent) {
|
|
216
|
+
const mine = epoch;
|
|
217
|
+
let extra = [];
|
|
218
|
+
if (deps.carrierHiddenTools) {
|
|
219
|
+
// 判据失败一律当"没有额外要藏的":藏错了模型就少一条路,留着只是多花 token。
|
|
220
|
+
try {
|
|
221
|
+
extra = await deps.carrierHiddenTools(agent);
|
|
222
|
+
}
|
|
223
|
+
catch (e) {
|
|
224
|
+
extra = [];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (mine !== epoch)
|
|
228
|
+
return;
|
|
229
|
+
if (!seenAgents.has(agent))
|
|
230
|
+
return;
|
|
231
|
+
const names = [...new Set([...desiredNames, ...extra])];
|
|
232
|
+
const scoped = scopedToolsOf(agent);
|
|
233
|
+
if (!scoped || typeof scoped.restrict !== 'function') {
|
|
234
|
+
if (names.length === 0)
|
|
235
|
+
return;
|
|
236
|
+
// agent scope 上没有 tools 面(或它没有 restrict):这半边做不成,如实说 ——
|
|
237
|
+
// 静默跳过会让人以为"停用工具从模型工具表里消失"已经生效。
|
|
238
|
+
noteRuntime({
|
|
239
|
+
id: 'mcp-tool-visibility',
|
|
240
|
+
label: '停用工具的可见性',
|
|
241
|
+
kind: 'write',
|
|
242
|
+
fallback: 'inform-only',
|
|
243
|
+
detail: 'agent scope 上没有可用的 tools.restrict(官方接口变了或该 scope 未暴露 tools):停用的 MCP 工具与兼容页关掉的本插件工具仍会出现在模型可见的工具表里,执行侧拦截仍然生效。',
|
|
244
|
+
detailKey: 'mcp-tool-visibility.no-restrict',
|
|
245
|
+
});
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const key = names.slice().sort().join('\u0000');
|
|
249
|
+
if (appliedByAgent.get(agent) === key)
|
|
250
|
+
return;
|
|
251
|
+
liftRestriction(agent);
|
|
252
|
+
if (names.length === 0) {
|
|
253
|
+
clearRuntimeNote('mcp-tool-visibility');
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
const dispose = scoped.restrict({ deny: names });
|
|
258
|
+
agentRestrictions.set(agent, typeof dispose === 'function' ? dispose : () => { });
|
|
259
|
+
appliedByAgent.set(agent, key);
|
|
260
|
+
clearRuntimeNote('mcp-tool-visibility');
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
// 这个名字在这个 scope 里认不出 / scope 已经收了:可见性半边没生效。执行侧的 guard
|
|
264
|
+
// 仍然拦住调用,所以功能不缺 —— 但必须如实上报,不能假装成功(此前正是静默吞掉)。
|
|
265
|
+
noteRuntime({
|
|
266
|
+
id: 'mcp-tool-visibility',
|
|
267
|
+
label: '停用工具的可见性',
|
|
268
|
+
kind: 'write',
|
|
269
|
+
fallback: 'inform-only',
|
|
270
|
+
detail: '有工具没能从模型可见的工具表里摘掉(' + deps.message(e) + '):执行侧拦截仍然生效,模型仍能看到该工具的名字。',
|
|
271
|
+
detailKey: 'mcp-tool-visibility.partial',
|
|
272
|
+
params: { reason: deps.message(e) },
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/** agent 上线(`agent/created` 或启动期的 agent 列表):登记并立刻应用当前名单。 */
|
|
277
|
+
function attachAgent(agent) {
|
|
278
|
+
if (!agent || (typeof agent !== 'object' && typeof agent !== 'function'))
|
|
279
|
+
return;
|
|
280
|
+
seenAgents.add(agent);
|
|
281
|
+
void restrictAgent(agent).catch(() => { });
|
|
282
|
+
}
|
|
283
|
+
/** agent 下线(`agent/disposed`):先撤限制再销登记。 */
|
|
284
|
+
function detachAgent(agent) {
|
|
285
|
+
seenAgents.delete(agent);
|
|
286
|
+
liftRestriction(agent);
|
|
287
|
+
}
|
|
288
|
+
async function applyToolRestrictions() {
|
|
289
|
+
// 全局 tools 面上没有 restrict → 可见性半边整条做不成,如实上报(2026-09-30 审查 P2-8)。
|
|
290
|
+
// 此前这里是**裸 return**:停用工具仍留在模型可见的工具表里,而兼容页因为没有上报仍报
|
|
291
|
+
// 一行"ok",用户看到的是"停用了但工具还在"。与 `restrictAgent` 里那条 noteRuntime 同文案、
|
|
292
|
+
// 同 id(noteRuntime 按 id 覆盖,两处不会互相刷屏)。
|
|
293
|
+
if (typeof deps.tools.restrict !== 'function') {
|
|
294
|
+
noteRuntime({
|
|
295
|
+
id: 'mcp-tool-visibility',
|
|
296
|
+
label: '停用工具的可见性',
|
|
297
|
+
kind: 'write',
|
|
298
|
+
fallback: 'inform-only',
|
|
299
|
+
detail: '全局 tools 服务上没有可用的 tools.restrict(官方接口变了):停用的 MCP 工具与兼容页关掉的本插件工具仍会出现在模型可见的工具表里,执行侧拦截仍然生效。',
|
|
300
|
+
detailKey: 'mcp-tool-visibility.no-restrict',
|
|
301
|
+
});
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
// Force re-read: this runs on the tools/change path, which is rare, so a
|
|
305
|
+
// stale cache must not keep an externally edited deny list hidden.
|
|
306
|
+
const map = await readDisabledTools(true);
|
|
307
|
+
// 通配 `*`(整服务器停用)先展开成已注册的全名,再与精确名单合并。
|
|
308
|
+
const wanted = disabledToolNames(map);
|
|
309
|
+
let registered;
|
|
310
|
+
try {
|
|
311
|
+
registered = new Set((await deps.tools.schemas()).map((schema) => String(schema.name)));
|
|
312
|
+
}
|
|
313
|
+
catch (e) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
for (const [serverName, list] of Object.entries(map)) {
|
|
317
|
+
if (list.indexOf('*') < 0)
|
|
318
|
+
continue;
|
|
319
|
+
const prefix = MCP_TOOL_PREFIX + serverName + '__';
|
|
320
|
+
for (const fullName of registered)
|
|
321
|
+
if (fullName.startsWith(prefix))
|
|
322
|
+
wanted.push(fullName);
|
|
323
|
+
}
|
|
324
|
+
// 兼容页「模型工具表」关掉的本插件工具并进同一份名单(理由见 deps.pluginHiddenTools)。
|
|
325
|
+
// 交给上面那个 `registered` 过滤是**必要的**而不是保险:官方对认不出的名字直接抛错,
|
|
326
|
+
// 而关掉的工具里可能有一个这次根本没注册成功(注册失败会被子智能体页的黄条报出来)。
|
|
327
|
+
if (deps.pluginHiddenTools) {
|
|
328
|
+
try {
|
|
329
|
+
for (const name of deps.pluginHiddenTools())
|
|
330
|
+
wanted.push(name);
|
|
331
|
+
}
|
|
332
|
+
catch (e) { /* 读设置失败 = 这次不摘它们 */ }
|
|
333
|
+
}
|
|
334
|
+
const names = [...new Set(wanted)].filter((name) => registered.has(name));
|
|
335
|
+
const key = names.join('\u0000');
|
|
336
|
+
// 有 `carrierHiddenTools` 时不拿全局键短路:那份名单与"官方工具此时挂没挂"有关,
|
|
337
|
+
// 而它变了不会改全局键(官方挂载/卸载本身就发 tools/change,正是靠这条通路追上)。
|
|
338
|
+
// 代价是每次 tools/change 都重算一遍 —— 逐 agent 的键判等会把绝大多数挡在装层之前,
|
|
339
|
+
// 事实读取也有 TTL 缓存;装层本身会再发一次 tools/change,那一轮键相同即收敛,不自激。
|
|
340
|
+
if (key === appliedNamesKey && deps.carrierHiddenTools === undefined)
|
|
341
|
+
return;
|
|
342
|
+
appliedNamesKey = key;
|
|
343
|
+
desiredNames = names;
|
|
344
|
+
// 每次重放先撤掉每个 agent 上的旧层:名单变短(重新启用某个工具)时,只有撤掉这层限制
|
|
345
|
+
// 它才会重新可见;名单变了(含逐 agent 那份)也必须重装而不是叠加。
|
|
346
|
+
epoch += 1;
|
|
347
|
+
for (const agent of [...agentRestrictions.keys()])
|
|
348
|
+
liftRestriction(agent);
|
|
349
|
+
for (const agent of seenAgents)
|
|
350
|
+
void restrictAgent(agent).catch(() => { });
|
|
351
|
+
}
|
|
352
|
+
function scheduleToolRestrictions() {
|
|
353
|
+
if (restrictTimer)
|
|
354
|
+
return;
|
|
355
|
+
restrictTimer = setTimeout(() => {
|
|
356
|
+
restrictTimer = null;
|
|
357
|
+
applyToolRestrictions().catch(() => { });
|
|
358
|
+
}, 300);
|
|
359
|
+
}
|
|
360
|
+
/** 卸载清理:清掉重排定时器、把挂在每个 agent scope 上的限制都撤掉(母文件 dispose 的那六行原样搬来)。 */
|
|
361
|
+
function disposeVisibility() {
|
|
362
|
+
if (restrictTimer) {
|
|
363
|
+
clearTimeout(restrictTimer);
|
|
364
|
+
restrictTimer = null;
|
|
365
|
+
}
|
|
366
|
+
// 插件卸载时把挂在每个 agent scope 上的限制都撤掉(否则那些 scope 会继续背着一层名单)。
|
|
367
|
+
epoch += 1;
|
|
368
|
+
for (const agent of [...agentRestrictions.keys()])
|
|
369
|
+
liftRestriction(agent);
|
|
370
|
+
appliedByAgent.clear();
|
|
371
|
+
seenAgents.clear();
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
readDisabledTools,
|
|
375
|
+
readKnownMcpTools,
|
|
376
|
+
writeKnownMcpTools,
|
|
377
|
+
mutateKnownMcpTools,
|
|
378
|
+
mergeLiveToolsInto,
|
|
379
|
+
mcpmToolEnabled,
|
|
380
|
+
attachAgent,
|
|
381
|
+
detachAgent,
|
|
382
|
+
applyToolRestrictions,
|
|
383
|
+
scheduleToolRestrictions,
|
|
384
|
+
setDisabledToolsCache: (value) => { disabledToolsCache = { at: Date.now(), value }; },
|
|
385
|
+
disabledToolsSnapshot: () => (disabledToolsCache ? disabledToolsCache.value : {}),
|
|
386
|
+
disposeVisibility,
|
|
387
|
+
};
|
|
388
|
+
}
|