dsh-model-switch 0.4.41 → 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 +12 -0
- package/README.md +2 -0
- package/README.zh.md +2 -0
- 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 +5 -1
- package/lib/session-route-BCQFzqRP.js +0 -172
- package/lib/session-route-BjNPlliu.js +0 -172
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## v0.4.42
|
|
2
|
+
|
|
3
|
+
与第三方自动模式插件 `@log.li/dsh-automode` 的插件层适配:**不改用户配置、不新增依赖、不安装任何东西**——本插件在自己的 loader 行里,把「自动审查路由」与「纯读名单」注入到 `@log.li/dsh-automode` 行的**内存配置**,让它的 `auto-mode` 档位沿用我们的路由与豁免。
|
|
4
|
+
|
|
5
|
+
- **新行 `model-switch-automode-compat`**(`dsh-model-switch/automode-compat`):`inject = ['loader','modelSwitch']`,在 `loader/patch-context` 里于该行挂载前改写 `entry.options.config`;行不存在时整行空转。可在插件管理器里单独停用,不影响审查闸门。
|
|
6
|
+
- **路由**:仅当 automode 的 `classifier.provider` / `classifier.model` **都为空**时填入我们的 `autoReviewProvider/Model`;任一字段已有值即视为用户意图,绝不覆盖(状态里标 `user-set`);我们将自己上次写入的值记为可重写,设置变更时能跟随更新。
|
|
7
|
+
- **纯读豁免**:把我们的名单(默认 58 条)并入它的 `readOnlyTools`,并在「纯读工具免审查」开关关闭时**不注入**。它用精确匹配,所以 `sessions_list__*` 这类通配条目会跳过并在卡片上计数(默认名单里是 1 条)。注入后的工具仍会先过它自己的 deny 正则——不会削弱它的硬底线。
|
|
8
|
+
- **不落盘**:只改运行时 `entry.options`,从不调用 `Entry.update()`;启动前后 profile 的 `cordis.patch.yml` 不变,重启自动重放。
|
|
9
|
+
- **可见性**:「审查豁免」卡片新增一行 `Auto mode 已接入:审查路由 <provider>/<model>,直通 +N 个纯读工具(M 条通配未同步)`,未安装时不显示;`tools` RPC 的 `status.automode` 提供同数字段。
|
|
10
|
+
- **rc.3 lab 实测**(3083 / `~/.dsh-lab3`,automode `0.17.0` + 本插件 0.4.42):automode 的 `boot` 审计行出现;在 `auto-mode` 会话的工具调用里,它的闸门实际读到 `preset="auto-mode"`、`classifier={"provider":"apigoto","model":"mimo-v2.6-flash",…}`、`readOnlyTools=58`、`preExecuteGate=true`(即注入生效);启动前后 profile `cordis.patch.yml` 的 sha256 一致(未落盘);卡片显示「Auto mode 已接入:审查路由 apigoto/mimo-v2.6-flash,直通 +53 个纯读工具(2 条通配未同步)」。注:automode 的逐调用审计行本次未出现,是因为它对未升级的 workspace-write 调用按设计静默放行,与兼容性无关。
|
|
11
|
+
- **边界**:它的分类器只有一个静态路由,逐步 provider 分组无法映射过去(取顶层自动审查路由);它的 `readOnlyTools` 精确匹配,通配条目转不过去;这些都在文案与文档里写明。
|
|
12
|
+
|
|
1
13
|
## v0.4.41
|
|
2
14
|
|
|
3
15
|
DSH `0.2.1-rc.3`(2026-10-23 发布,当前在 `next` 通道)兼容性更新:**插件本体零改动**,本轮做的是实证与声明;旧线(`0.1.7-alpha.2` … `0.2.1-rc.2`)的声明与依赖下界原样保留。
|
package/README.md
CHANGED
|
@@ -193,6 +193,8 @@ Verified releases: `0.1.7-alpha.2`, `0.1.7-rc.1`, `0.1.7-rc.2`, `0.2.0-rc.2`. Th
|
|
|
193
193
|
|
|
194
194
|
The verified releases in `package.json#dsh.compatibility.dshReleases` are evidence, not an allowlist. Review public API changes and test new Host releases before adding a verification claim.
|
|
195
195
|
|
|
196
|
+
**Third-party auto modes.** When [`@log.li/dsh-automode`](https://github.com/log-li/dsh-automode) is installed, the `model-switch-automode-compat` row injects this plugin's Auto-review route and pure-read list into that row's **in-memory** config — no profile write, nothing to configure by hand. Its `auto-mode` preset then reviews with your configured route and passes your allowlisted tools, while the `auto` preset keeps using this plugin's own gate. Explicit `classifier.provider/model` values in that row always win, pattern-shaped allowlist entries cannot transfer (its check is exact-match), and its hard-deny bands still run first.
|
|
197
|
+
|
|
196
198
|
## Development
|
|
197
199
|
|
|
198
200
|
Requires Node 22.19+ and pnpm.
|
package/README.zh.md
CHANGED
|
@@ -191,6 +191,8 @@ DSH Host peer 和开发依赖接受 `>=0.1.7-alpha.2`,包括 rc.1 和后续版
|
|
|
191
191
|
|
|
192
192
|
`package.json#dsh.compatibility.dshReleases` 中的已验证版本是证据,不是允许列表。新增验证声明前要审查公开 API 变化并测试新 Host 版本。
|
|
193
193
|
|
|
194
|
+
**第三方自动模式。** 安装 [`@log.li/dsh-automode`](https://github.com/log-li/dsh-automode) 后,`model-switch-automode-compat` 行会把本插件的自动审查路由与纯读名单注入到该行的**内存配置**里——不写 profile、不需要手工配置。它的 `auto-mode` 档位会用你配置的路由审查、并按你的名单直通纯读工具;`auto` 档位仍然走本插件自己的闸门。该行里显式写过的 `classifier.provider/model` 永远优先,带通配的名单条目无法转过去(它是精确匹配),它的硬 deny 正则也照旧先跑。
|
|
195
|
+
|
|
194
196
|
## 开发
|
|
195
197
|
|
|
196
198
|
需要 Node 22.19+ 和 pnpm。
|
package/cordis.patch.yml
CHANGED
|
@@ -73,3 +73,10 @@
|
|
|
73
73
|
# The official naming provider under the configurable naming route.
|
|
74
74
|
- id: model-switch-session-title
|
|
75
75
|
name: 'dsh-model-switch/session-title'
|
|
76
|
+
|
|
77
|
+
# Inject this plugin's Auto-review route and pure-read list into the
|
|
78
|
+
# third-party @log.li/dsh-automode row's in-memory config (no profile write,
|
|
79
|
+
# no-op when that plugin is not installed). Kept as its own row so it can be
|
|
80
|
+
# disabled without touching the reviewer gate.
|
|
81
|
+
- id: model-switch-automode-compat
|
|
82
|
+
name: 'dsh-model-switch/automode-compat'
|
|
@@ -1,339 +1,9 @@
|
|
|
1
1
|
import { i as sessionProviderOf, r as resolveCapabilityRoute } from "./session-route-D1zoZgf3.js";
|
|
2
|
+
import { n as isAllowedByList } from "./tool-allowlist-DlzB5GlQ.js";
|
|
2
3
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
4
|
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
5
|
import { homedir } from "node:os";
|
|
5
6
|
import { dirname, join } from "node:path";
|
|
6
|
-
/** Catalog lookup by id. */
|
|
7
|
-
const SOURCE_BY_ID = new Map([
|
|
8
|
-
{
|
|
9
|
-
id: "core",
|
|
10
|
-
kind: "core",
|
|
11
|
-
label: "DSH 核心",
|
|
12
|
-
repo: "https://github.com/deepseek-ai/deepseek-harness"
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
id: "dsh-ops",
|
|
16
|
-
kind: "plugin",
|
|
17
|
-
label: "插件 · dsh-ops",
|
|
18
|
-
repo: "https://github.com/T-Auto/dsh-ops"
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
id: "dsh-browser-cdp",
|
|
22
|
-
kind: "plugin",
|
|
23
|
-
label: "插件 · dsh-browser-cdp",
|
|
24
|
-
repo: "https://github.com/drscrewdriver/dsh-browser-cdp"
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
id: "dsh-auto-memory",
|
|
28
|
-
kind: "plugin",
|
|
29
|
-
label: "插件 · dsh-auto-memory",
|
|
30
|
-
repo: "https://github.com/AskTheWay/dsh-auto-memory"
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
id: "dsh-model-switch",
|
|
34
|
-
kind: "plugin",
|
|
35
|
-
label: "插件 · dsh-model-switch",
|
|
36
|
-
repo: "https://github.com/vandoor7-droid/dsh-model-switch"
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
id: "mcp:wincu",
|
|
40
|
-
kind: "mcp",
|
|
41
|
-
label: "MCP · wincu(dsh-computer-use-win)",
|
|
42
|
-
repo: "https://github.com/Yu-tao-Li/dsh-computer-use-win"
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
id: "qqbot-extension",
|
|
46
|
-
kind: "extension",
|
|
47
|
-
label: "QQ 机器人扩展(框架 @zaofan/dsh-qqbot)",
|
|
48
|
-
repo: "https://github.com/zaofan-make/dsh-qqbot"
|
|
49
|
-
}
|
|
50
|
-
].map((entry) => [entry.id, entry]));
|
|
51
|
-
/**
|
|
52
|
-
* Tool names the released Host owns, calibrated against dsh 0.2.1-rc.2.
|
|
53
|
-
*
|
|
54
|
-
* A name outside the catalog would otherwise be reported as core, which is the
|
|
55
|
-
* one guess a reader could act on wrongly; it resolves to `unknown` instead.
|
|
56
|
-
*/
|
|
57
|
-
const CORE_TOOLS = /* @__PURE__ */ new Set([
|
|
58
|
-
"ask_user_question",
|
|
59
|
-
"create_goal",
|
|
60
|
-
"create_skill",
|
|
61
|
-
"create_worktree",
|
|
62
|
-
"edit",
|
|
63
|
-
"exit_plan_mode",
|
|
64
|
-
"get_goal",
|
|
65
|
-
"glob",
|
|
66
|
-
"grep",
|
|
67
|
-
"interrupt_agent",
|
|
68
|
-
"job_kill",
|
|
69
|
-
"job_list",
|
|
70
|
-
"job_output",
|
|
71
|
-
"list_agents",
|
|
72
|
-
"list_mcp_resource_templates",
|
|
73
|
-
"list_mcp_resources",
|
|
74
|
-
"load_workspace_dependencies",
|
|
75
|
-
"present",
|
|
76
|
-
"pwsh",
|
|
77
|
-
"ralph",
|
|
78
|
-
"read",
|
|
79
|
-
"read_image",
|
|
80
|
-
"read_mcp_resource",
|
|
81
|
-
"schedule_create",
|
|
82
|
-
"schedule_delete",
|
|
83
|
-
"schedule_list",
|
|
84
|
-
"schedule_update",
|
|
85
|
-
"send_message",
|
|
86
|
-
"session_event_read",
|
|
87
|
-
"session_event_search",
|
|
88
|
-
"session_event_trace",
|
|
89
|
-
"session_search",
|
|
90
|
-
"session_trace",
|
|
91
|
-
"sidebar_open",
|
|
92
|
-
"skill",
|
|
93
|
-
"spawn_teammate",
|
|
94
|
-
"subagent",
|
|
95
|
-
"subagent_fork",
|
|
96
|
-
"team_task_create",
|
|
97
|
-
"team_task_get",
|
|
98
|
-
"team_task_list",
|
|
99
|
-
"team_task_update",
|
|
100
|
-
"terminal_close",
|
|
101
|
-
"terminal_list",
|
|
102
|
-
"terminal_open",
|
|
103
|
-
"terminal_read",
|
|
104
|
-
"terminal_send",
|
|
105
|
-
"terminal_signal",
|
|
106
|
-
"todo_write",
|
|
107
|
-
"update_goal",
|
|
108
|
-
"wait_agent",
|
|
109
|
-
"web_fetch",
|
|
110
|
-
"web_search",
|
|
111
|
-
"web_search_pro",
|
|
112
|
-
"workflow",
|
|
113
|
-
"working_directory",
|
|
114
|
-
"write"
|
|
115
|
-
]);
|
|
116
|
-
/** MCP server name → catalog id, for the servers this allowlist knows. */
|
|
117
|
-
const MCP_SERVERS = /* @__PURE__ */ new Map([["wincu", "mcp:wincu"]]);
|
|
118
|
-
/** Plugin identity by tool-name prefix (checked before the core catalog). */
|
|
119
|
-
const PLUGIN_PREFIXES = [
|
|
120
|
-
["bcdp_", "dsh-browser-cdp"],
|
|
121
|
-
["browser_window_", "dsh-browser-cdp"],
|
|
122
|
-
["ops_", "dsh-ops"],
|
|
123
|
-
["memory_", "dsh-auto-memory"]
|
|
124
|
-
];
|
|
125
|
-
/** Single plugin tools that carry no prefix. */
|
|
126
|
-
const PLUGIN_TOOLS = /* @__PURE__ */ new Map([["generate_image", "dsh-model-switch"]]);
|
|
127
|
-
/** `<base>__<owner>` — the extension-tool naming the QQ bot framework uses. */
|
|
128
|
-
const EXTENSION_TOOL = /^([A-Za-z0-9_-]+)__(.+)$/;
|
|
129
|
-
/**
|
|
130
|
-
* Default allowlist: every pure-read tool this deployment exposes plus the
|
|
131
|
-
* no-side-effect and autonomy entries the user explicitly released, calibrated
|
|
132
|
-
* on a live desktop profile (105 tools) at dsh 0.2.1-rc.2.
|
|
133
|
-
*
|
|
134
|
-
* Grouped by origin so a reader can trace each entry through {@link SOURCE_CATALOG}.
|
|
135
|
-
* Entries ending in `*` are patterns: the QQ bot extension suffixes its tools with
|
|
136
|
-
* the bot's own name, so `sessions_list__*` keeps working when the persona is
|
|
137
|
-
* renamed.
|
|
138
|
-
*
|
|
139
|
-
* Everything here follows one rule: releasing it must not let a side effect escape
|
|
140
|
-
* the reviewer. Calls that merely dispatch, ask, present, wait or navigate qualify
|
|
141
|
-
* because the work they trigger still runs under the same auto preset. Delegation
|
|
142
|
-
* tools (`subagent`, `subagent_fork`, `spawn_teammate`) deliberately stay out: a
|
|
143
|
-
* child session seeds its preset from `permissionPresets.current(parent.session)`,
|
|
144
|
-
* and this bypass answers `'custom'` for an allowlisted call in flight — releasing
|
|
145
|
-
* them would hand the child no `auto` preset at all, i.e. no review.
|
|
146
|
-
*/
|
|
147
|
-
const DEFAULT_READ_ONLY_TOOLS = [
|
|
148
|
-
"read",
|
|
149
|
-
"grep",
|
|
150
|
-
"glob",
|
|
151
|
-
"read_image",
|
|
152
|
-
"skill",
|
|
153
|
-
"web_search",
|
|
154
|
-
"web_search_pro",
|
|
155
|
-
"web_fetch",
|
|
156
|
-
"load_workspace_dependencies",
|
|
157
|
-
"ask_user_question",
|
|
158
|
-
"present",
|
|
159
|
-
"sidebar_open",
|
|
160
|
-
"exit_plan_mode",
|
|
161
|
-
"job_list",
|
|
162
|
-
"job_output",
|
|
163
|
-
"terminal_list",
|
|
164
|
-
"terminal_read",
|
|
165
|
-
"list_agents",
|
|
166
|
-
"team_task_list",
|
|
167
|
-
"team_task_get",
|
|
168
|
-
"get_goal",
|
|
169
|
-
"schedule_list",
|
|
170
|
-
"wait_agent",
|
|
171
|
-
"send_message",
|
|
172
|
-
"team_task_create",
|
|
173
|
-
"list_mcp_resources",
|
|
174
|
-
"list_mcp_resource_templates",
|
|
175
|
-
"read_mcp_resource",
|
|
176
|
-
"session_search",
|
|
177
|
-
"session_event_search",
|
|
178
|
-
"session_event_read",
|
|
179
|
-
"session_event_trace",
|
|
180
|
-
"session_trace",
|
|
181
|
-
"create_goal",
|
|
182
|
-
"schedule_create",
|
|
183
|
-
"terminal_open",
|
|
184
|
-
"ops_read_files",
|
|
185
|
-
"ops_discover_tools",
|
|
186
|
-
"bcdp_status",
|
|
187
|
-
"bcdp_help",
|
|
188
|
-
"bcdp_doctor",
|
|
189
|
-
"bcdp_snapshot",
|
|
190
|
-
"browser_window_status",
|
|
191
|
-
"memory_list",
|
|
192
|
-
"memory_read",
|
|
193
|
-
"mcp__wincu__windows_computer_use_accessibility_tree",
|
|
194
|
-
"mcp__wincu__windows_computer_use_element_info",
|
|
195
|
-
"mcp__wincu__windows_computer_use_find",
|
|
196
|
-
"mcp__wincu__windows_computer_use_health",
|
|
197
|
-
"mcp__wincu__windows_computer_use_list_windows",
|
|
198
|
-
"mcp__wincu__windows_computer_use_ocr",
|
|
199
|
-
"mcp__wincu__windows_computer_use_screenshot",
|
|
200
|
-
"mcp__wincu__windows_computer_use_snapshot",
|
|
201
|
-
"mcp__wincu__windows_computer_use_tools",
|
|
202
|
-
"mcp__wincu__windows_computer_use_wait",
|
|
203
|
-
"mcp__wincu__windows_computer_use_wait_for",
|
|
204
|
-
"sessions_list__*",
|
|
205
|
-
"bing_search__*"
|
|
206
|
-
];
|
|
207
|
-
/**
|
|
208
|
-
* Validate one allowlist entry.
|
|
209
|
-
*
|
|
210
|
-
* `*` is a suffix wildcard: the entry must keep at least one literal leading
|
|
211
|
-
* character, so a stray `*` can never turn the list into "allow everything".
|
|
212
|
-
*
|
|
213
|
-
* @param entry - the raw entry as typed in settings.
|
|
214
|
-
* @returns `ok`, or the specific problem.
|
|
215
|
-
*/
|
|
216
|
-
function validateAllowEntry(entry) {
|
|
217
|
-
const trimmed = entry.trim();
|
|
218
|
-
if (trimmed === "") return "empty";
|
|
219
|
-
if (/^\*+$/.test(trimmed)) return "bare-wildcard";
|
|
220
|
-
if (trimmed.startsWith("*")) return "leading-wildcard";
|
|
221
|
-
return "ok";
|
|
222
|
-
}
|
|
223
|
-
/**
|
|
224
|
-
* Whether a tool name matches one allowlist entry.
|
|
225
|
-
*
|
|
226
|
-
* Exact names match exactly; an entry may end in `*`, which matches any
|
|
227
|
-
* remaining characters (including none). Invalid entries never match.
|
|
228
|
-
*
|
|
229
|
-
* @param name - the tool name from `exec.name`.
|
|
230
|
-
* @param entry - one allowlist entry.
|
|
231
|
-
* @returns true when the call may skip review.
|
|
232
|
-
*/
|
|
233
|
-
function matchesAllowPattern(name, entry) {
|
|
234
|
-
if (name === "" || validateAllowEntry(entry) !== "ok") return false;
|
|
235
|
-
const trimmed = entry.trim();
|
|
236
|
-
const star = trimmed.indexOf("*");
|
|
237
|
-
if (star === -1) return name === trimmed;
|
|
238
|
-
const prefix = trimmed.slice(0, star);
|
|
239
|
-
const suffix = trimmed.slice(star + 1);
|
|
240
|
-
if (!name.startsWith(prefix) || !name.endsWith(suffix)) return false;
|
|
241
|
-
return name.length >= prefix.length + suffix.length;
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Whether a tool name is covered by an allowlist.
|
|
245
|
-
* @param name - the tool name under review.
|
|
246
|
-
* @param allowlist - configured entries, patterns included.
|
|
247
|
-
* @returns true when at least one entry matches.
|
|
248
|
-
*/
|
|
249
|
-
function isAllowedByList(name, allowlist) {
|
|
250
|
-
return allowlist.some((entry) => matchesAllowPattern(name, entry));
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Resolve the origin of one tool name for display.
|
|
254
|
-
*
|
|
255
|
-
* Resolution order: MCP bridge → `<tool>__<owner>` extension → plugin prefix →
|
|
256
|
-
* plugin table → core catalog → unknown. Only the first two are structural; the
|
|
257
|
-
* rest are calibrated names, so an unlisted tool is reported as unknown rather
|
|
258
|
-
* than guessed into a package.
|
|
259
|
-
*
|
|
260
|
-
* @param name - the tool name.
|
|
261
|
-
* @returns The origin, with its repository when known.
|
|
262
|
-
*/
|
|
263
|
-
function resolveToolSource(name) {
|
|
264
|
-
const trimmed = name.trim().replace(/\*+$/, "");
|
|
265
|
-
const mcp = /^mcp__([A-Za-z0-9_-]+)__/.exec(trimmed);
|
|
266
|
-
if (mcp !== null) {
|
|
267
|
-
const server = mcp[1];
|
|
268
|
-
const entry = SOURCE_BY_ID.get(MCP_SERVERS.get(server) ?? "");
|
|
269
|
-
if (entry !== void 0) return {
|
|
270
|
-
kind: "mcp",
|
|
271
|
-
label: entry.label,
|
|
272
|
-
repo: entry.repo,
|
|
273
|
-
owner: server
|
|
274
|
-
};
|
|
275
|
-
return {
|
|
276
|
-
kind: "mcp",
|
|
277
|
-
label: `MCP · ${server}`,
|
|
278
|
-
owner: server
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
const extension = EXTENSION_TOOL.exec(trimmed);
|
|
282
|
-
if (extension !== null && !trimmed.startsWith("mcp__")) {
|
|
283
|
-
const entry = SOURCE_BY_ID.get("qqbot-extension");
|
|
284
|
-
const base = extension[1];
|
|
285
|
-
const owner = extension[2];
|
|
286
|
-
return {
|
|
287
|
-
kind: "extension",
|
|
288
|
-
label: `扩展 · ${base}__${owner}`,
|
|
289
|
-
...entry === void 0 ? {} : { repo: entry.repo },
|
|
290
|
-
owner
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
if (trimmed.endsWith("__")) {
|
|
294
|
-
const entry = SOURCE_BY_ID.get("qqbot-extension");
|
|
295
|
-
return {
|
|
296
|
-
kind: "extension",
|
|
297
|
-
label: `扩展 · ${trimmed.slice(0, -2)}__*`,
|
|
298
|
-
...entry === void 0 ? {} : { repo: entry.repo },
|
|
299
|
-
owner: "*"
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
for (const [prefix, id] of PLUGIN_PREFIXES) {
|
|
303
|
-
if (!trimmed.startsWith(prefix)) continue;
|
|
304
|
-
const entry = SOURCE_BY_ID.get(id);
|
|
305
|
-
if (entry !== void 0) return {
|
|
306
|
-
kind: "plugin",
|
|
307
|
-
label: entry.label,
|
|
308
|
-
repo: entry.repo,
|
|
309
|
-
owner: id
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
const plugin = PLUGIN_TOOLS.get(trimmed);
|
|
313
|
-
if (plugin !== void 0) {
|
|
314
|
-
const entry = SOURCE_BY_ID.get(plugin);
|
|
315
|
-
if (entry !== void 0) return {
|
|
316
|
-
kind: "plugin",
|
|
317
|
-
label: entry.label,
|
|
318
|
-
repo: entry.repo,
|
|
319
|
-
owner: plugin
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
if (CORE_TOOLS.has(trimmed)) {
|
|
323
|
-
const entry = SOURCE_BY_ID.get("core");
|
|
324
|
-
return {
|
|
325
|
-
kind: "core",
|
|
326
|
-
label: entry?.label ?? "DSH 核心",
|
|
327
|
-
...entry === void 0 ? {} : { repo: entry.repo },
|
|
328
|
-
owner: "core"
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
return {
|
|
332
|
-
kind: "unknown",
|
|
333
|
-
label: "未知来源"
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
//#endregion
|
|
337
7
|
//#region lib/types/auto-sessions-state.js
|
|
338
8
|
/**
|
|
339
9
|
* Durable snapshot of the sessions that were on the Auto preset when this row
|
|
@@ -1394,4 +1064,4 @@ async function apply(ctx) {
|
|
|
1394
1064
|
}, "model-switch: release the Auto registration before anything else");
|
|
1395
1065
|
}
|
|
1396
1066
|
//#endregion
|
|
1397
|
-
export { bindAutoReviewSessionCorrelation as a, installToolCallSafetyNet as c, resolveSessionAutoReviewRoute as d, reviewLlm as f,
|
|
1067
|
+
export { bindAutoReviewSessionCorrelation as a, installToolCallSafetyNet as c, resolveSessionAutoReviewRoute as d, reviewLlm as f, autoReviewStatus as i, isReviewerInfrastructureFailure as l, autoReviewGate as n, defensivePermissionPresets as o, reviewScope as p, autoReviewRouteOf as r, inject as s, apply as t, name as u };
|
package/lib/auto-review.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as bindAutoReviewSessionCorrelation, c as installToolCallSafetyNet, d as resolveSessionAutoReviewRoute, f as reviewLlm, i as autoReviewStatus, l as isReviewerInfrastructureFailure, n as autoReviewGate, o as defensivePermissionPresets, p as reviewScope, r as autoReviewRouteOf, s as inject, t as apply, u as name } from "./auto-review-
|
|
1
|
+
import { a as bindAutoReviewSessionCorrelation, c as installToolCallSafetyNet, d as resolveSessionAutoReviewRoute, f as reviewLlm, i as autoReviewStatus, l as isReviewerInfrastructureFailure, n as autoReviewGate, o as defensivePermissionPresets, p as reviewScope, r as autoReviewRouteOf, s as inject, t as apply, u as name } from "./auto-review-QDEFUjT2.js";
|
|
2
2
|
export { apply, autoReviewGate, autoReviewRouteOf, autoReviewStatus, bindAutoReviewSessionCorrelation, defensivePermissionPresets, inject, installToolCallSafetyNet, isReviewerInfrastructureFailure, name, resolveSessionAutoReviewRoute, reviewLlm, reviewScope };
|
|
@@ -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 };
|