dsh-model-switch 0.4.40 → 0.4.42
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 +22 -0
- package/README.md +3 -1
- package/README.zh.md +4 -2
- package/cordis.patch.yml +7 -0
- package/lib/{auto-review-BDx3LjVL.js → auto-review-QDEFUjT2.js} +2 -332
- package/lib/auto-review.js +1 -1
- package/lib/automode-compat.js +266 -0
- package/lib/client.js +23 -1
- package/lib/index.js +5 -2
- package/lib/tool-allowlist-DlzB5GlQ.js +332 -0
- package/lib/types/automode-compat.d.ts +152 -0
- package/lib/types/capabilities-rpc.d.ts +8 -0
- package/lib/types/client/locales.d.ts +3 -0
- package/lib/types/client-contract.d.ts +7 -0
- package/package.json +7 -2
- package/lib/session-route-BCQFzqRP.js +0 -172
- package/lib/session-route-BjNPlliu.js +0 -172
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { t as DEFAULT_READ_ONLY_TOOLS } from "./tool-allowlist-DlzB5GlQ.js";
|
|
2
|
+
//#region lib/types/automode-compat.js
|
|
3
|
+
/** Cordis plugin identity; matches the `cordis.patch.yml` insert row id. */
|
|
4
|
+
const name = "model-switch-automode-compat";
|
|
5
|
+
/** The loader is the only seam this row needs besides this plugin's settings. */
|
|
6
|
+
const inject = ["loader", "modelSwitch"];
|
|
7
|
+
/** Third-party package this row adapts (strict package identity, never a prefix). */
|
|
8
|
+
const AUTOMODE_PACKAGE = "@log.li/dsh-automode";
|
|
9
|
+
/** Live adapter state, surfaced next to the allowlist editor. */
|
|
10
|
+
const automodeCompatStatus = {
|
|
11
|
+
/** A `@log.li/dsh-automode` row exists in this Host. */
|
|
12
|
+
installed: false,
|
|
13
|
+
/** `provider/model` this adapter wrote, when it wrote one. */
|
|
14
|
+
route: void 0,
|
|
15
|
+
/** Why the route was skipped, when it was. */
|
|
16
|
+
routeSkipped: void 0,
|
|
17
|
+
/** Tools merged into the row's `readOnlyTools`. */
|
|
18
|
+
added: [],
|
|
19
|
+
/** Pattern-shaped entries that cannot transfer. */
|
|
20
|
+
skipped: [],
|
|
21
|
+
/** How many rows were rewritten (remounts included). */
|
|
22
|
+
applied: 0
|
|
23
|
+
};
|
|
24
|
+
/** Plain-object guard: arrays and `null` are not config objects. */
|
|
25
|
+
function asRecord(value) {
|
|
26
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
27
|
+
}
|
|
28
|
+
/** Trimmed string or empty, so whitespace-only config reads as "unset". */
|
|
29
|
+
function text(value) {
|
|
30
|
+
return typeof value === "string" ? value.trim() : "";
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Whether one loader entry is the third-party auto-mode row.
|
|
34
|
+
* @param entry - anything the loader hands over (an entry, or an event payload).
|
|
35
|
+
* @returns true only for the exact package name.
|
|
36
|
+
*/
|
|
37
|
+
function isAutomodeEntry(entry) {
|
|
38
|
+
const row = entry?.options;
|
|
39
|
+
return typeof row?.name === "string" && row.name === "@log.li/dsh-automode";
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Decide the config one automode row should run with.
|
|
43
|
+
*
|
|
44
|
+
* Pure and side-effect free so the policy is unit-testable.
|
|
45
|
+
*
|
|
46
|
+
* @param config - the row's current config (any JSON-ish value).
|
|
47
|
+
* @param route - this plugin's Auto-review route; empty strings mean "unset".
|
|
48
|
+
* @param allowlist - tool names to merge into `readOnlyTools`; pass an empty
|
|
49
|
+
* list while the skip switch is off.
|
|
50
|
+
* @param adoptedRoute - a `provider/model` this adapter wrote on an earlier
|
|
51
|
+
* pass; seeing it again means the row is still ours to rewrite.
|
|
52
|
+
* @returns the config to install, whether it changed, and what was applied.
|
|
53
|
+
*/
|
|
54
|
+
function planAutomodeCompat(config, route, allowlist, adoptedRoute) {
|
|
55
|
+
const base = asRecord(config);
|
|
56
|
+
if (base === void 0) return {
|
|
57
|
+
config: {},
|
|
58
|
+
changed: false,
|
|
59
|
+
applied: {
|
|
60
|
+
added: [],
|
|
61
|
+
skipped: []
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const next = { ...base };
|
|
65
|
+
let changed = false;
|
|
66
|
+
const provider = text(route.provider);
|
|
67
|
+
const model = text(route.model);
|
|
68
|
+
const classifier = asRecord(base.classifier) ?? {};
|
|
69
|
+
const declaredProvider = text(classifier.provider);
|
|
70
|
+
const declaredModel = text(classifier.model);
|
|
71
|
+
const declaredRoute = declaredProvider !== "" && declaredModel !== "" ? `${declaredProvider}/${declaredModel}` : void 0;
|
|
72
|
+
const ours = declaredRoute !== void 0 && adoptedRoute !== void 0 && declaredRoute === adoptedRoute;
|
|
73
|
+
let routeApplied;
|
|
74
|
+
let routeSkipped;
|
|
75
|
+
if (!ours && (declaredProvider !== "" || declaredModel !== "")) routeSkipped = "user-set";
|
|
76
|
+
else if (provider === "" || model === "") routeSkipped = "no-route";
|
|
77
|
+
else {
|
|
78
|
+
next.classifier = {
|
|
79
|
+
...classifier,
|
|
80
|
+
provider,
|
|
81
|
+
model
|
|
82
|
+
};
|
|
83
|
+
routeApplied = `${provider}/${model}`;
|
|
84
|
+
changed = true;
|
|
85
|
+
}
|
|
86
|
+
const existing = Array.isArray(base.readOnlyTools) ? base.readOnlyTools.filter((item) => typeof item === "string") : [];
|
|
87
|
+
const added = [];
|
|
88
|
+
const skipped = [];
|
|
89
|
+
for (const candidate of allowlist) {
|
|
90
|
+
const tool = text(candidate);
|
|
91
|
+
if (tool === "") continue;
|
|
92
|
+
if (tool.includes("*")) {
|
|
93
|
+
if (!skipped.includes(tool)) skipped.push(tool);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (!existing.includes(tool) && !added.includes(tool)) added.push(tool);
|
|
97
|
+
}
|
|
98
|
+
if (added.length > 0) {
|
|
99
|
+
next.readOnlyTools = [...existing, ...added];
|
|
100
|
+
changed = true;
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
config: next,
|
|
104
|
+
changed,
|
|
105
|
+
applied: {
|
|
106
|
+
route: routeApplied,
|
|
107
|
+
routeSkipped,
|
|
108
|
+
added,
|
|
109
|
+
skipped
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** The Auto-review route this plugin is configured to use. */
|
|
114
|
+
function routeOf(settings) {
|
|
115
|
+
return {
|
|
116
|
+
provider: text(settings.autoReviewProvider),
|
|
117
|
+
model: text(settings.autoReviewModel)
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** Tools to merge into the third-party row, honouring the skip switch. */
|
|
121
|
+
function allowlistOf(settings) {
|
|
122
|
+
if (settings.autoReviewSkipReadOnly !== true) return [];
|
|
123
|
+
const saved = settings.autoReviewAllowlist;
|
|
124
|
+
return saved !== void 0 && saved.length > 0 ? saved : DEFAULT_READ_ONLY_TOOLS;
|
|
125
|
+
}
|
|
126
|
+
/** Record one applied plan on the shared status object. */
|
|
127
|
+
function reflect(plan) {
|
|
128
|
+
automodeCompatStatus.installed = true;
|
|
129
|
+
if (plan.applied.route !== void 0) {
|
|
130
|
+
automodeCompatStatus.route = plan.applied.route;
|
|
131
|
+
automodeCompatStatus.routeSkipped = void 0;
|
|
132
|
+
} else if (automodeCompatStatus.route === void 0) automodeCompatStatus.routeSkipped = plan.applied.routeSkipped;
|
|
133
|
+
for (const tool of plan.applied.added) if (!automodeCompatStatus.added.includes(tool)) automodeCompatStatus.added.push(tool);
|
|
134
|
+
for (const tool of plan.applied.skipped) if (!automodeCompatStatus.skipped.includes(tool)) automodeCompatStatus.skipped.push(tool);
|
|
135
|
+
}
|
|
136
|
+
/** Read one entry's live config object, if it has one. */
|
|
137
|
+
function configOf(entry) {
|
|
138
|
+
return entry.options.config;
|
|
139
|
+
}
|
|
140
|
+
/** Assign the adapted config back onto the entry's live options. */
|
|
141
|
+
function writeConfig(entry, config) {
|
|
142
|
+
entry.options.config = config;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Adapt one row in memory, containing every failure.
|
|
146
|
+
*
|
|
147
|
+
* @param entry - the loader entry to adapt.
|
|
148
|
+
* @param settings - current plugin settings, read per application.
|
|
149
|
+
* @param log - row logger for the one-line report.
|
|
150
|
+
* @param adoptedRoute - the route this adapter already wrote, when it wrote one.
|
|
151
|
+
* @returns the plan that was applied, or undefined when nothing ran.
|
|
152
|
+
*/
|
|
153
|
+
function adaptEntry(entry, settings, log, adoptedRoute) {
|
|
154
|
+
try {
|
|
155
|
+
const current = settings();
|
|
156
|
+
const plan = planAutomodeCompat(configOf(entry), routeOf(current), allowlistOf(current), adoptedRoute);
|
|
157
|
+
automodeCompatStatus.installed = true;
|
|
158
|
+
if (!plan.changed) {
|
|
159
|
+
reflect(plan);
|
|
160
|
+
return plan;
|
|
161
|
+
}
|
|
162
|
+
writeConfig(entry, plan.config);
|
|
163
|
+
automodeCompatStatus.applied += 1;
|
|
164
|
+
reflect(plan);
|
|
165
|
+
return plan;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
log.warn(`model-switch: could not adapt the dsh-automode row (${String(error)}); its own route and read-only list stay as configured`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** One-line human summary of a plan, for the startup log. */
|
|
172
|
+
function describeAutomodePlan(plan) {
|
|
173
|
+
const parts = [];
|
|
174
|
+
parts.push(plan.applied.route === void 0 ? `route ${plan.applied.routeSkipped ?? "unset"}` : `route ${plan.applied.route}`);
|
|
175
|
+
parts.push(`+${plan.applied.added.length} read-only tools`);
|
|
176
|
+
if (plan.applied.skipped.length > 0) parts.push(`${plan.applied.skipped.length} pattern entries skipped`);
|
|
177
|
+
return parts.join(", ");
|
|
178
|
+
}
|
|
179
|
+
/** Last adaptation summary, reused when a settings change replays the adapter. */
|
|
180
|
+
let lastSummary = "";
|
|
181
|
+
/**
|
|
182
|
+
* Snapshot for the RPC/UI layer.
|
|
183
|
+
* @returns the view, or undefined when this Host has no automode row.
|
|
184
|
+
*/
|
|
185
|
+
function automodeCompatView() {
|
|
186
|
+
if (!automodeCompatStatus.installed) return void 0;
|
|
187
|
+
return {
|
|
188
|
+
installed: true,
|
|
189
|
+
route: automodeCompatStatus.route,
|
|
190
|
+
routeSkipped: automodeCompatStatus.routeSkipped,
|
|
191
|
+
added: automodeCompatStatus.added.length,
|
|
192
|
+
skipped: automodeCompatStatus.skipped.length
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Install the adapter: adapt live rows now, and every row the loader mounts later.
|
|
197
|
+
*
|
|
198
|
+
* The loader applies the profile patch layer and mounts the entry's plugin right
|
|
199
|
+
* after the `loader/patch-context` waterfall returns, so rewriting the entry's
|
|
200
|
+
* `options.config` from that waterfall is what makes the adapted values the ones
|
|
201
|
+
* the plugin activates with. No profile write happens here.
|
|
202
|
+
*
|
|
203
|
+
* @param ctx - this row's context.
|
|
204
|
+
* @param loader - the loader service, when the Host provides it.
|
|
205
|
+
* @param settings - reads the current plugin settings.
|
|
206
|
+
* @returns a disposer that detaches the listeners, plus a replay for live rows.
|
|
207
|
+
*/
|
|
208
|
+
function installAutomodeCompat(ctx, loader, settings) {
|
|
209
|
+
const log = ctx.logger;
|
|
210
|
+
/** Entry → last plan applied, so remounts only log when the outcome changes. */
|
|
211
|
+
const applied = /* @__PURE__ */ new Map();
|
|
212
|
+
/** Entry → the route this adapter wrote, so a replay may rewrite its own value. */
|
|
213
|
+
const adopted = /* @__PURE__ */ new Map();
|
|
214
|
+
const applying = (entry) => {
|
|
215
|
+
const plan = adaptEntry(entry, settings, log, adopted.get(entry));
|
|
216
|
+
if (plan === void 0) return;
|
|
217
|
+
if (plan.applied.route !== void 0) adopted.set(entry, plan.applied.route);
|
|
218
|
+
else if (plan.applied.routeSkipped === "user-set") adopted.delete(entry);
|
|
219
|
+
const summary = describeAutomodePlan(plan);
|
|
220
|
+
lastSummary = summary;
|
|
221
|
+
if (!plan.changed) return;
|
|
222
|
+
if (applied.get(entry) === summary) return;
|
|
223
|
+
applied.set(entry, summary);
|
|
224
|
+
log.info(`model-switch: adapted dsh-automode (${summary})`);
|
|
225
|
+
};
|
|
226
|
+
if (loader !== void 0 && typeof loader.entries === "function") {
|
|
227
|
+
for (const entry of loader.entries()) if (isAutomodeEntry(entry)) applying(entry);
|
|
228
|
+
}
|
|
229
|
+
const dispose = ctx.on("loader/patch-context", function(first, second) {
|
|
230
|
+
const nextFn = typeof first === "function" ? first : second;
|
|
231
|
+
const candidate = typeof first === "function" ? this : first;
|
|
232
|
+
if (isAutomodeEntry(candidate)) applying(candidate);
|
|
233
|
+
return nextFn === void 0 ? void 0 : nextFn();
|
|
234
|
+
});
|
|
235
|
+
/** Re-adapt every live row (settings changed under us). */
|
|
236
|
+
const replay = () => {
|
|
237
|
+
let any = false;
|
|
238
|
+
for (const entry of loader?.entries?.() ?? []) {
|
|
239
|
+
if (!isAutomodeEntry(entry)) continue;
|
|
240
|
+
any = true;
|
|
241
|
+
applying(entry);
|
|
242
|
+
}
|
|
243
|
+
return any;
|
|
244
|
+
};
|
|
245
|
+
return {
|
|
246
|
+
dispose,
|
|
247
|
+
replay
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Row entry point: adapt the third-party row on activation and on remount.
|
|
252
|
+
* @param ctx - this row's context.
|
|
253
|
+
*/
|
|
254
|
+
function apply(ctx) {
|
|
255
|
+
const loader = ctx.get("loader", false);
|
|
256
|
+
const settings = () => ctx.modelSwitch.currentSettings();
|
|
257
|
+
const adapter = installAutomodeCompat(ctx, loader, settings);
|
|
258
|
+
ctx.effect(function* () {
|
|
259
|
+
yield adapter.dispose;
|
|
260
|
+
yield ctx.on("model-switch/settings-update", () => {
|
|
261
|
+
if (adapter.replay()) ctx.logger.info(`model-switch: re-applied the dsh-automode adaptation after a settings change (${lastSummary})`);
|
|
262
|
+
});
|
|
263
|
+
}, "model-switch: keep the dsh-automode adapter aligned with the settings");
|
|
264
|
+
}
|
|
265
|
+
//#endregion
|
|
266
|
+
export { AUTOMODE_PACKAGE, apply, automodeCompatStatus, automodeCompatView, describeAutomodePlan, inject, installAutomodeCompat, isAutomodeEntry, name, planAutomodeCompat };
|
package/lib/client.js
CHANGED
|
@@ -444,10 +444,22 @@ window.__ModuleLoader__.load({
|
|
|
444
444
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
445
445
|
const status = raw;
|
|
446
446
|
const count = (input) => typeof input === "number" && Number.isFinite(input) && input >= 0 ? input : 0;
|
|
447
|
+
const route = (input) => typeof input === "string" && input.trim() !== "" ? input.trim() : void 0;
|
|
448
|
+
const automode = status.automode !== null && typeof status.automode === "object" && !Array.isArray(status.automode) ? (() => {
|
|
449
|
+
const row = status.automode;
|
|
450
|
+
if (row.installed !== true) return void 0;
|
|
451
|
+
return {
|
|
452
|
+
route: route(row.route),
|
|
453
|
+
routeSkipped: route(row.routeSkipped),
|
|
454
|
+
added: count(row.added),
|
|
455
|
+
skipped: count(row.skipped)
|
|
456
|
+
};
|
|
457
|
+
})() : void 0;
|
|
447
458
|
return {
|
|
448
459
|
held: status.held === true,
|
|
449
460
|
allowedCalls: count(status.allowedCalls),
|
|
450
|
-
skippedReadOnly: count(status.skippedReadOnly)
|
|
461
|
+
skippedReadOnly: count(status.skippedReadOnly),
|
|
462
|
+
automode
|
|
451
463
|
};
|
|
452
464
|
}
|
|
453
465
|
/**
|
|
@@ -2603,6 +2615,10 @@ window.__ModuleLoader__.load({
|
|
|
2603
2615
|
className: ModelSwitchSettings_module_css_default.hint,
|
|
2604
2616
|
children: props.t("reviewAllowlistSkipped").replace("{count}", String(reviewStatus.skippedReadOnly))
|
|
2605
2617
|
}),
|
|
2618
|
+
reviewStatus?.automode === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2619
|
+
className: ModelSwitchSettings_module_css_default.hint,
|
|
2620
|
+
children: props.t("reviewAutomodeLinked").replace("{route}", reviewStatus.automode.route ?? (reviewStatus.automode.routeSkipped === "user-set" ? props.t("reviewAutomodeRouteOwn") : props.t("reviewAutomodeRouteUnset"))).replace("{added}", String(reviewStatus.automode.added)).replace("{skipped}", String(reviewStatus.automode.skipped))
|
|
2621
|
+
}),
|
|
2606
2622
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
2607
2623
|
className: ModelSwitchSettings_module_css_default.allowList,
|
|
2608
2624
|
children: allowlistIncluded.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
@@ -3049,6 +3065,9 @@ window.__ModuleLoader__.load({
|
|
|
3049
3065
|
reviewAllowlistAvailable: "本机存在但未放行的工具:",
|
|
3050
3066
|
reviewAllowlistRepo: "仓库",
|
|
3051
3067
|
reviewAllowlistSkipped: "本 Host 进程已为纯读调用跳过 {count} 次审查。",
|
|
3068
|
+
reviewAutomodeLinked: "Auto mode 已接入:审查路由 {route},直通 +{added} 个纯读工具({skipped} 条通配未同步)。",
|
|
3069
|
+
reviewAutomodeRouteOwn: "沿用其自身配置",
|
|
3070
|
+
reviewAutomodeRouteUnset: "未配置(沿用会话模型)",
|
|
3052
3071
|
"reason.central-subagent-routing": "Alpha.4 没有全局子代理启动路由接口。",
|
|
3053
3072
|
"reason.packaged-preset-roots": "Alpha.4 不支持插件提供额外 preset root。",
|
|
3054
3073
|
"reason.tool-owner-suppression": "Alpha.4 没有工具所有者或来源抑制接口。",
|
|
@@ -3129,6 +3148,9 @@ window.__ModuleLoader__.load({
|
|
|
3129
3148
|
reviewAllowlistAvailable: "Present on this machine but not allowed:",
|
|
3130
3149
|
reviewAllowlistRepo: "repo",
|
|
3131
3150
|
reviewAllowlistSkipped: "This Host process has skipped the reviewer {count} times.",
|
|
3151
|
+
reviewAutomodeLinked: "Auto mode linked: reviewer route {route}, passes +{added} read-only tools ({skipped} pattern entries not synced).",
|
|
3152
|
+
reviewAutomodeRouteOwn: "its own settings",
|
|
3153
|
+
reviewAutomodeRouteUnset: "unset (follows the session model)",
|
|
3132
3154
|
"reason.central-subagent-routing": "Alpha.4 exposes no global Subagent start-routing seam.",
|
|
3133
3155
|
"reason.packaged-preset-roots": "Alpha.4 exposes no plugin-owned preset root.",
|
|
3134
3156
|
"reason.tool-owner-suppression": "Alpha.4 exposes no tool owner or provenance suppression seam.",
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { o as normalizeProviderGroups } from "./session-route-D1zoZgf3.js";
|
|
2
|
-
import {
|
|
2
|
+
import { r as resolveToolSource, t as DEFAULT_READ_ONLY_TOOLS } from "./tool-allowlist-DlzB5GlQ.js";
|
|
3
|
+
import { i as autoReviewStatus } from "./auto-review-QDEFUjT2.js";
|
|
3
4
|
import { ModelSwitchAdapterRegistry } from "./adapter-registry.js";
|
|
5
|
+
import { automodeCompatView } from "./automode-compat.js";
|
|
4
6
|
import { createRequire } from "node:module";
|
|
5
7
|
import z from "@deepseek-ai/schemastery";
|
|
6
8
|
import { Service } from "@deepseek-ai/cordis";
|
|
@@ -526,7 +528,8 @@ function installCapabilitiesRpc(ctx, registry, snapshot) {
|
|
|
526
528
|
}), () => ({
|
|
527
529
|
held: autoReviewStatus.held,
|
|
528
530
|
allowedCalls: autoReviewStatus.allowedCalls,
|
|
529
|
-
skippedReadOnly: autoReviewStatus.skippedReadOnly
|
|
531
|
+
skippedReadOnly: autoReviewStatus.skippedReadOnly,
|
|
532
|
+
automode: automodeCompatView()
|
|
530
533
|
}));
|
|
531
534
|
const dispose = scope.connection.fetch.register({
|
|
532
535
|
path: "/api/plugin-rpc/model-switch",
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/** Catalog lookup by id. */
|
|
2
|
+
const SOURCE_BY_ID = new Map([
|
|
3
|
+
{
|
|
4
|
+
id: "core",
|
|
5
|
+
kind: "core",
|
|
6
|
+
label: "DSH 核心",
|
|
7
|
+
repo: "https://github.com/deepseek-ai/deepseek-harness"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
id: "dsh-ops",
|
|
11
|
+
kind: "plugin",
|
|
12
|
+
label: "插件 · dsh-ops",
|
|
13
|
+
repo: "https://github.com/T-Auto/dsh-ops"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: "dsh-browser-cdp",
|
|
17
|
+
kind: "plugin",
|
|
18
|
+
label: "插件 · dsh-browser-cdp",
|
|
19
|
+
repo: "https://github.com/drscrewdriver/dsh-browser-cdp"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "dsh-auto-memory",
|
|
23
|
+
kind: "plugin",
|
|
24
|
+
label: "插件 · dsh-auto-memory",
|
|
25
|
+
repo: "https://github.com/AskTheWay/dsh-auto-memory"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "dsh-model-switch",
|
|
29
|
+
kind: "plugin",
|
|
30
|
+
label: "插件 · dsh-model-switch",
|
|
31
|
+
repo: "https://github.com/vandoor7-droid/dsh-model-switch"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "mcp:wincu",
|
|
35
|
+
kind: "mcp",
|
|
36
|
+
label: "MCP · wincu(dsh-computer-use-win)",
|
|
37
|
+
repo: "https://github.com/Yu-tao-Li/dsh-computer-use-win"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "qqbot-extension",
|
|
41
|
+
kind: "extension",
|
|
42
|
+
label: "QQ 机器人扩展(框架 @zaofan/dsh-qqbot)",
|
|
43
|
+
repo: "https://github.com/zaofan-make/dsh-qqbot"
|
|
44
|
+
}
|
|
45
|
+
].map((entry) => [entry.id, entry]));
|
|
46
|
+
/**
|
|
47
|
+
* Tool names the released Host owns, calibrated against dsh 0.2.1-rc.2.
|
|
48
|
+
*
|
|
49
|
+
* A name outside the catalog would otherwise be reported as core, which is the
|
|
50
|
+
* one guess a reader could act on wrongly; it resolves to `unknown` instead.
|
|
51
|
+
*/
|
|
52
|
+
const CORE_TOOLS = /* @__PURE__ */ new Set([
|
|
53
|
+
"ask_user_question",
|
|
54
|
+
"create_goal",
|
|
55
|
+
"create_skill",
|
|
56
|
+
"create_worktree",
|
|
57
|
+
"edit",
|
|
58
|
+
"exit_plan_mode",
|
|
59
|
+
"get_goal",
|
|
60
|
+
"glob",
|
|
61
|
+
"grep",
|
|
62
|
+
"interrupt_agent",
|
|
63
|
+
"job_kill",
|
|
64
|
+
"job_list",
|
|
65
|
+
"job_output",
|
|
66
|
+
"list_agents",
|
|
67
|
+
"list_mcp_resource_templates",
|
|
68
|
+
"list_mcp_resources",
|
|
69
|
+
"load_workspace_dependencies",
|
|
70
|
+
"present",
|
|
71
|
+
"pwsh",
|
|
72
|
+
"ralph",
|
|
73
|
+
"read",
|
|
74
|
+
"read_image",
|
|
75
|
+
"read_mcp_resource",
|
|
76
|
+
"schedule_create",
|
|
77
|
+
"schedule_delete",
|
|
78
|
+
"schedule_list",
|
|
79
|
+
"schedule_update",
|
|
80
|
+
"send_message",
|
|
81
|
+
"session_event_read",
|
|
82
|
+
"session_event_search",
|
|
83
|
+
"session_event_trace",
|
|
84
|
+
"session_search",
|
|
85
|
+
"session_trace",
|
|
86
|
+
"sidebar_open",
|
|
87
|
+
"skill",
|
|
88
|
+
"spawn_teammate",
|
|
89
|
+
"subagent",
|
|
90
|
+
"subagent_fork",
|
|
91
|
+
"team_task_create",
|
|
92
|
+
"team_task_get",
|
|
93
|
+
"team_task_list",
|
|
94
|
+
"team_task_update",
|
|
95
|
+
"terminal_close",
|
|
96
|
+
"terminal_list",
|
|
97
|
+
"terminal_open",
|
|
98
|
+
"terminal_read",
|
|
99
|
+
"terminal_send",
|
|
100
|
+
"terminal_signal",
|
|
101
|
+
"todo_write",
|
|
102
|
+
"update_goal",
|
|
103
|
+
"wait_agent",
|
|
104
|
+
"web_fetch",
|
|
105
|
+
"web_search",
|
|
106
|
+
"web_search_pro",
|
|
107
|
+
"workflow",
|
|
108
|
+
"working_directory",
|
|
109
|
+
"write"
|
|
110
|
+
]);
|
|
111
|
+
/** MCP server name → catalog id, for the servers this allowlist knows. */
|
|
112
|
+
const MCP_SERVERS = /* @__PURE__ */ new Map([["wincu", "mcp:wincu"]]);
|
|
113
|
+
/** Plugin identity by tool-name prefix (checked before the core catalog). */
|
|
114
|
+
const PLUGIN_PREFIXES = [
|
|
115
|
+
["bcdp_", "dsh-browser-cdp"],
|
|
116
|
+
["browser_window_", "dsh-browser-cdp"],
|
|
117
|
+
["ops_", "dsh-ops"],
|
|
118
|
+
["memory_", "dsh-auto-memory"]
|
|
119
|
+
];
|
|
120
|
+
/** Single plugin tools that carry no prefix. */
|
|
121
|
+
const PLUGIN_TOOLS = /* @__PURE__ */ new Map([["generate_image", "dsh-model-switch"]]);
|
|
122
|
+
/** `<base>__<owner>` — the extension-tool naming the QQ bot framework uses. */
|
|
123
|
+
const EXTENSION_TOOL = /^([A-Za-z0-9_-]+)__(.+)$/;
|
|
124
|
+
/**
|
|
125
|
+
* Default allowlist: every pure-read tool this deployment exposes plus the
|
|
126
|
+
* no-side-effect and autonomy entries the user explicitly released, calibrated
|
|
127
|
+
* on a live desktop profile (105 tools) at dsh 0.2.1-rc.2.
|
|
128
|
+
*
|
|
129
|
+
* Grouped by origin so a reader can trace each entry through {@link SOURCE_CATALOG}.
|
|
130
|
+
* Entries ending in `*` are patterns: the QQ bot extension suffixes its tools with
|
|
131
|
+
* the bot's own name, so `sessions_list__*` keeps working when the persona is
|
|
132
|
+
* renamed.
|
|
133
|
+
*
|
|
134
|
+
* Everything here follows one rule: releasing it must not let a side effect escape
|
|
135
|
+
* the reviewer. Calls that merely dispatch, ask, present, wait or navigate qualify
|
|
136
|
+
* because the work they trigger still runs under the same auto preset. Delegation
|
|
137
|
+
* tools (`subagent`, `subagent_fork`, `spawn_teammate`) deliberately stay out: a
|
|
138
|
+
* child session seeds its preset from `permissionPresets.current(parent.session)`,
|
|
139
|
+
* and this bypass answers `'custom'` for an allowlisted call in flight — releasing
|
|
140
|
+
* them would hand the child no `auto` preset at all, i.e. no review.
|
|
141
|
+
*/
|
|
142
|
+
const DEFAULT_READ_ONLY_TOOLS = [
|
|
143
|
+
"read",
|
|
144
|
+
"grep",
|
|
145
|
+
"glob",
|
|
146
|
+
"read_image",
|
|
147
|
+
"skill",
|
|
148
|
+
"web_search",
|
|
149
|
+
"web_search_pro",
|
|
150
|
+
"web_fetch",
|
|
151
|
+
"load_workspace_dependencies",
|
|
152
|
+
"ask_user_question",
|
|
153
|
+
"present",
|
|
154
|
+
"sidebar_open",
|
|
155
|
+
"exit_plan_mode",
|
|
156
|
+
"job_list",
|
|
157
|
+
"job_output",
|
|
158
|
+
"terminal_list",
|
|
159
|
+
"terminal_read",
|
|
160
|
+
"list_agents",
|
|
161
|
+
"team_task_list",
|
|
162
|
+
"team_task_get",
|
|
163
|
+
"get_goal",
|
|
164
|
+
"schedule_list",
|
|
165
|
+
"wait_agent",
|
|
166
|
+
"send_message",
|
|
167
|
+
"team_task_create",
|
|
168
|
+
"list_mcp_resources",
|
|
169
|
+
"list_mcp_resource_templates",
|
|
170
|
+
"read_mcp_resource",
|
|
171
|
+
"session_search",
|
|
172
|
+
"session_event_search",
|
|
173
|
+
"session_event_read",
|
|
174
|
+
"session_event_trace",
|
|
175
|
+
"session_trace",
|
|
176
|
+
"create_goal",
|
|
177
|
+
"schedule_create",
|
|
178
|
+
"terminal_open",
|
|
179
|
+
"ops_read_files",
|
|
180
|
+
"ops_discover_tools",
|
|
181
|
+
"bcdp_status",
|
|
182
|
+
"bcdp_help",
|
|
183
|
+
"bcdp_doctor",
|
|
184
|
+
"bcdp_snapshot",
|
|
185
|
+
"browser_window_status",
|
|
186
|
+
"memory_list",
|
|
187
|
+
"memory_read",
|
|
188
|
+
"mcp__wincu__windows_computer_use_accessibility_tree",
|
|
189
|
+
"mcp__wincu__windows_computer_use_element_info",
|
|
190
|
+
"mcp__wincu__windows_computer_use_find",
|
|
191
|
+
"mcp__wincu__windows_computer_use_health",
|
|
192
|
+
"mcp__wincu__windows_computer_use_list_windows",
|
|
193
|
+
"mcp__wincu__windows_computer_use_ocr",
|
|
194
|
+
"mcp__wincu__windows_computer_use_screenshot",
|
|
195
|
+
"mcp__wincu__windows_computer_use_snapshot",
|
|
196
|
+
"mcp__wincu__windows_computer_use_tools",
|
|
197
|
+
"mcp__wincu__windows_computer_use_wait",
|
|
198
|
+
"mcp__wincu__windows_computer_use_wait_for",
|
|
199
|
+
"sessions_list__*",
|
|
200
|
+
"bing_search__*"
|
|
201
|
+
];
|
|
202
|
+
/**
|
|
203
|
+
* Validate one allowlist entry.
|
|
204
|
+
*
|
|
205
|
+
* `*` is a suffix wildcard: the entry must keep at least one literal leading
|
|
206
|
+
* character, so a stray `*` can never turn the list into "allow everything".
|
|
207
|
+
*
|
|
208
|
+
* @param entry - the raw entry as typed in settings.
|
|
209
|
+
* @returns `ok`, or the specific problem.
|
|
210
|
+
*/
|
|
211
|
+
function validateAllowEntry(entry) {
|
|
212
|
+
const trimmed = entry.trim();
|
|
213
|
+
if (trimmed === "") return "empty";
|
|
214
|
+
if (/^\*+$/.test(trimmed)) return "bare-wildcard";
|
|
215
|
+
if (trimmed.startsWith("*")) return "leading-wildcard";
|
|
216
|
+
return "ok";
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Whether a tool name matches one allowlist entry.
|
|
220
|
+
*
|
|
221
|
+
* Exact names match exactly; an entry may end in `*`, which matches any
|
|
222
|
+
* remaining characters (including none). Invalid entries never match.
|
|
223
|
+
*
|
|
224
|
+
* @param name - the tool name from `exec.name`.
|
|
225
|
+
* @param entry - one allowlist entry.
|
|
226
|
+
* @returns true when the call may skip review.
|
|
227
|
+
*/
|
|
228
|
+
function matchesAllowPattern(name, entry) {
|
|
229
|
+
if (name === "" || validateAllowEntry(entry) !== "ok") return false;
|
|
230
|
+
const trimmed = entry.trim();
|
|
231
|
+
const star = trimmed.indexOf("*");
|
|
232
|
+
if (star === -1) return name === trimmed;
|
|
233
|
+
const prefix = trimmed.slice(0, star);
|
|
234
|
+
const suffix = trimmed.slice(star + 1);
|
|
235
|
+
if (!name.startsWith(prefix) || !name.endsWith(suffix)) return false;
|
|
236
|
+
return name.length >= prefix.length + suffix.length;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Whether a tool name is covered by an allowlist.
|
|
240
|
+
* @param name - the tool name under review.
|
|
241
|
+
* @param allowlist - configured entries, patterns included.
|
|
242
|
+
* @returns true when at least one entry matches.
|
|
243
|
+
*/
|
|
244
|
+
function isAllowedByList(name, allowlist) {
|
|
245
|
+
return allowlist.some((entry) => matchesAllowPattern(name, entry));
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Resolve the origin of one tool name for display.
|
|
249
|
+
*
|
|
250
|
+
* Resolution order: MCP bridge → `<tool>__<owner>` extension → plugin prefix →
|
|
251
|
+
* plugin table → core catalog → unknown. Only the first two are structural; the
|
|
252
|
+
* rest are calibrated names, so an unlisted tool is reported as unknown rather
|
|
253
|
+
* than guessed into a package.
|
|
254
|
+
*
|
|
255
|
+
* @param name - the tool name.
|
|
256
|
+
* @returns The origin, with its repository when known.
|
|
257
|
+
*/
|
|
258
|
+
function resolveToolSource(name) {
|
|
259
|
+
const trimmed = name.trim().replace(/\*+$/, "");
|
|
260
|
+
const mcp = /^mcp__([A-Za-z0-9_-]+)__/.exec(trimmed);
|
|
261
|
+
if (mcp !== null) {
|
|
262
|
+
const server = mcp[1];
|
|
263
|
+
const entry = SOURCE_BY_ID.get(MCP_SERVERS.get(server) ?? "");
|
|
264
|
+
if (entry !== void 0) return {
|
|
265
|
+
kind: "mcp",
|
|
266
|
+
label: entry.label,
|
|
267
|
+
repo: entry.repo,
|
|
268
|
+
owner: server
|
|
269
|
+
};
|
|
270
|
+
return {
|
|
271
|
+
kind: "mcp",
|
|
272
|
+
label: `MCP · ${server}`,
|
|
273
|
+
owner: server
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
const extension = EXTENSION_TOOL.exec(trimmed);
|
|
277
|
+
if (extension !== null && !trimmed.startsWith("mcp__")) {
|
|
278
|
+
const entry = SOURCE_BY_ID.get("qqbot-extension");
|
|
279
|
+
const base = extension[1];
|
|
280
|
+
const owner = extension[2];
|
|
281
|
+
return {
|
|
282
|
+
kind: "extension",
|
|
283
|
+
label: `扩展 · ${base}__${owner}`,
|
|
284
|
+
...entry === void 0 ? {} : { repo: entry.repo },
|
|
285
|
+
owner
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (trimmed.endsWith("__")) {
|
|
289
|
+
const entry = SOURCE_BY_ID.get("qqbot-extension");
|
|
290
|
+
return {
|
|
291
|
+
kind: "extension",
|
|
292
|
+
label: `扩展 · ${trimmed.slice(0, -2)}__*`,
|
|
293
|
+
...entry === void 0 ? {} : { repo: entry.repo },
|
|
294
|
+
owner: "*"
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
for (const [prefix, id] of PLUGIN_PREFIXES) {
|
|
298
|
+
if (!trimmed.startsWith(prefix)) continue;
|
|
299
|
+
const entry = SOURCE_BY_ID.get(id);
|
|
300
|
+
if (entry !== void 0) return {
|
|
301
|
+
kind: "plugin",
|
|
302
|
+
label: entry.label,
|
|
303
|
+
repo: entry.repo,
|
|
304
|
+
owner: id
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const plugin = PLUGIN_TOOLS.get(trimmed);
|
|
308
|
+
if (plugin !== void 0) {
|
|
309
|
+
const entry = SOURCE_BY_ID.get(plugin);
|
|
310
|
+
if (entry !== void 0) return {
|
|
311
|
+
kind: "plugin",
|
|
312
|
+
label: entry.label,
|
|
313
|
+
repo: entry.repo,
|
|
314
|
+
owner: plugin
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
if (CORE_TOOLS.has(trimmed)) {
|
|
318
|
+
const entry = SOURCE_BY_ID.get("core");
|
|
319
|
+
return {
|
|
320
|
+
kind: "core",
|
|
321
|
+
label: entry?.label ?? "DSH 核心",
|
|
322
|
+
...entry === void 0 ? {} : { repo: entry.repo },
|
|
323
|
+
owner: "core"
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
kind: "unknown",
|
|
328
|
+
label: "未知来源"
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
//#endregion
|
|
332
|
+
export { isAllowedByList as n, resolveToolSource as r, DEFAULT_READ_ONLY_TOOLS as t };
|