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.
@@ -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 { h as resolveToolSource, i as autoReviewStatus, m as DEFAULT_READ_ONLY_TOOLS } from "./auto-review-BDx3LjVL.js";
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 };