dsh-model-switch 0.4.38 → 0.4.40
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 +37 -0
- package/lib/{auto-review-CvmQaRVf.js → auto-review-BDx3LjVL.js} +385 -10
- package/lib/auto-review.js +1 -1
- package/lib/client.js +728 -4
- package/lib/index.js +86 -3
- package/lib/types/auto-review.d.ts +39 -6
- package/lib/types/capabilities-rpc.d.ts +10 -1
- package/lib/types/client/ModelSwitchSettings.d.ts +11 -1
- package/lib/types/client/locales.d.ts +18 -0
- package/lib/types/client/picker/PlanReviewCard.d.ts +8 -0
- package/lib/types/client-contract.d.ts +54 -0
- package/lib/types/host-settings.d.ts +17 -0
- package/lib/types/picker/discuss-plan.d.ts +43 -0
- package/lib/types/tool-allowlist.d.ts +122 -0
- package/lib/types/tool-catalog.d.ts +37 -0
- package/package.json +1 -1
- package/lib/auto-review-9SreBh-0.js +0 -855
- package/lib/auto-review-BLtwr83V.js +0 -738
- package/lib/auto-review-BoF2_YtP.js +0 -745
- package/lib/auto-review-CMHNfRuV.js +0 -1015
- package/lib/auto-review-D-0A5Amx.js +0 -859
- package/lib/auto-review-oan7iXY7.js +0 -987
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,39 @@
|
|
|
1
|
+
## v0.4.40
|
|
2
|
+
|
|
3
|
+
按你的点名,默认豁免名单从 47 条扩到 **58 条**:新增 11 个工具,全部在「设置 → 模型切换 → 审查豁免」里可查、可删、可恢复默认。
|
|
4
|
+
|
|
5
|
+
- **纯读 / 无副作用(6 个)**:`load_workspace_dependencies`(只返回打包依赖路径与版本)、`wait_agent`(只等待队友变化)、`ask_user_question`(把问题抛给人,决策权本来就在人手里)、`present`(只登记交付物卡片,不复制内容)、`sidebar_open`(只在侧栏打开文件/文件夹/URL)、`exit_plan_mode`(计划门由人审批,第二个审查者多余)。
|
|
6
|
+
- **只派发、副作用仍留在审查内(3 个)**:`send_message`(给队友投递消息,目标会话的预设是它被创建时定好的)、`team_task_create`(只写共享任务板)、`terminal_open`(只起 shell,后续 `terminal_send` 仍在审查)。
|
|
7
|
+
- **自治项(2 个,按你的明确决定放出)**:`create_goal`(创建持久目标,让本会话跨轮继续)、`schedule_create`(排一条未来投递的提示)。它们触发的新回合仍跑同一个 auto 预设,所以每轮里的工具调用照样逐次审查;风险是无人值守的 token 消耗,已在名单注释里标明。
|
|
8
|
+
- **明确排除:`subagent` / `subagent_fork` / `spawn_teammate` 永不进默认名单。** `dsh-subagent` 生成子会话时会读 `permissionPresets.current(parent.session)`,只有 `auto` / `danger-full-access` 会被继承;而本插件在"名单命中的调用正在飞"时把 `current()` 答成 `'custom'`——放行这三个会让子会话拿不到 auto 预设,等于造出**无审查的子 agent**。`tests/tool-allowlist.spec.ts` 新增 `DELEGATION_SPAWNS` 专项守卫,防止以后被顺手加进去。
|
|
9
|
+
- **守卫调整**:`MUTATING` 里移除 `send_message`、`team_task_create`(现按"派发类"放行),补入 `subagent`、`subagent_fork`。
|
|
10
|
+
- **注意**:默认名单只是"出厂值"。已经保存过名单的部署(含桌面)升级后不会自动多出这 11 条——需要点一次「恢复默认名单」,或手动加入。
|
|
11
|
+
- 测试:59 个测试文件 / **541** 测试(新增 delegation 守卫与"用户放行清单"两例)。
|
|
12
|
+
|
|
13
|
+
## v0.4.39
|
|
14
|
+
|
|
15
|
+
「自动审查」下,**纯读工具不再调用审查模型**:`read` / `grep` / `glob` 这类只读调用直接执行,写操作、命令执行与删除仍然逐次审查。功能默认**关闭**,在「设置 → 模型切换 → 审查豁免」一键开启。
|
|
16
|
+
|
|
17
|
+
- **机制:让官方 gate 自己走 `next()`,不截断流水线。** 官方 `@deepseek-ai/dsh-experimental-auto-review` 的 `tools/pre-execute` 监听器首行是 `if (permissionPresets.current(session) !== 'auto') return next()`——它对本插件挂载的 `permissionPresets` 影子提问。`defensivePermissionPresets().current()` 现在只在「当前有工具调用在飞 ∧ 该调用名命中名单 ∧ 真实预设就是 auto」时回答 `'custom'`,gate 因此自己跳过审查并调用 `next()`:其它 listener、`ctx.tools.guard` 与沙箱策略全部照常执行。只在飞行的异步上下文内生效(`AsyncLocalStorage`),工具调用之外的 `current()`(teardown 记账、设置卡片)仍报告真实值。读设置失败、名单为空、工具名为空一律 fail closed(照常审查)。
|
|
18
|
+
- **默认名单(47 条)按本机实测校准,覆盖本机 105 个工具中的全部纯读项**,每条都能回溯到来源与仓库:
|
|
19
|
+
|
|
20
|
+
| 来源 | 仓库 | 默认放行 |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| DSH 核心 | https://github.com/deepseek-ai/deepseek-harness | 25 条(`read` `grep` `glob` `read_image` `skill` `web_search` `web_search_pro` `web_fetch` `job_list` `job_output` `terminal_list` `terminal_read` `list_agents` `team_task_list` `team_task_get` `get_goal` `schedule_list` `list_mcp_resources` `list_mcp_resource_templates` `read_mcp_resource` `session_search` `session_event_search` `session_event_read` `session_event_trace` `session_trace`) |
|
|
23
|
+
| dsh-ops | https://github.com/T-Auto/dsh-ops | 2 条(`ops_read_files` `ops_discover_tools`) |
|
|
24
|
+
| dsh-browser-cdp | https://github.com/drscrewdriver/dsh-browser-cdp | 5 条(`bcdp_status` `bcdp_help` `bcdp_doctor` `bcdp_snapshot` `browser_window_status`) |
|
|
25
|
+
| dsh-auto-memory | https://github.com/AskTheWay/dsh-auto-memory | 2 条(`memory_list` `memory_read`) |
|
|
26
|
+
| MCP `wincu`(dsh-computer-use-win) | https://github.com/Yu-tao-Li/dsh-computer-use-win | 11 条(`accessibility_tree` `element_info` `find` `health` `list_windows` `ocr` `screenshot` `snapshot` `tools` `wait` `wait_for`) |
|
|
27
|
+
| QQ 机器人扩展(框架 @zaofan/dsh-qqbot) | https://github.com/zaofan-make/dsh-qqbot | 2 条模式(`sessions_list__*` `bing_search__*`) |
|
|
28
|
+
|
|
29
|
+
插件本身不注册纯读工具(`generate_image` 有副作用),因此不贡献默认条目。
|
|
30
|
+
- **通配符后缀**:QQ 机器人扩展的工具名是 `<工具>__<机器人名>`,机器人名可自由修改,所以默认用 `sessions_list__*` / `bing_search__*` 而不是写死 `__YiXuan`;换 bot 名无需改配置。裸 `*` 与 `*开头` 的条目在校验层直接拒绝,防止把名单配成"全部放行"。
|
|
31
|
+
- **设置界面可编辑并声明来源**:新增「审查豁免」卡片——开关即时保存(开关与名单作为一次原子写入,永不出现"开着但名单是旧的");每一行显示工具名 + 来源徽章 + 仓库链接;可按工具名/来源搜索;「恢复默认名单」;「扫描本机工具」调用新增的 `tools` RPC(`ctx.tools.schemas()` 公开接口)列出本机存在但未放行的工具,一键加入;扫描后还会显示本 Host 进程的实时计数「已为纯读调用跳过 N 次审查」,用来确认豁免真的生效。
|
|
32
|
+
- **测试**:新增 `tests/tool-allowlist.spec.ts`(16 例:默认名单规模与禁项、通配符匹配与裸 `*` 拒绝、来源解析六类分支、仓库表完备、RPC 负载与计数解码)与 `tests/auto-review.spec.ts` 的 5 例豁免语义(在飞/非在飞、非 auto 预设、计数、拒绝)。`pnpm run check` 全绿(59 个测试文件 / **539** 测试 + pack 闸门)。
|
|
33
|
+
- **lab 实测(`DSH_HOME=~/.dsh-lab`,3082,0.2.1-rc.2 官方 Host)**:手动选「自动审查」预设 + 开关开启后,一轮里 `read`×2 + `glob`×1 让计数变成 3(确实跳过了审查);同一会话再 `write` 一次,计数仍是 3(写操作没有被豁免,仍走审查并被放行);把开关关掉后再跑 `glob`+`read`,计数保持 3(审查恢复)。
|
|
34
|
+
- **本地留档**:`review/allowlist-excluded.md` 列出了本机 105 个工具中未放行的 58 个及其来源与原因,`.gitignore` 已忽略 `review/`,不推送、不进包。
|
|
35
|
+
- **「去聊天里说」改为中断回合,不再收起工作步骤**(`src/picker/discuss-plan.ts`、`src/client/picker/PlanReviewCard.tsx`、`src/client/picker/install.tsx`)。之前该按钮只做 `PendingQuestion.dismiss()`:计划工具返回 `ASK_CANCELLED`,模型再回一句,回合以 **completed** 结束,于是按官方「工作步骤收起时机=回答结束后」把步骤折叠——想边看计划边讨论就得手动再展开。官方 transcript 的折叠判断是 `alwaysOpen = … || reason === 'aborted' || reason === 'interrupted' || reason === 'error'`(asar `ui-chat` 的 `ChatGroupSeat`),所以改成走官方「停止」同款路径 `conversation.cancel()`(会话作用域公开方法),回合以 **aborted** 结束:输入框照样立刻回来、工作步骤保持展开、并显示「已停止」标记;取消接口不可用或被拒时自动回落到原来的 dismiss,不会让按钮失效。新增 `tests/picker/plan-discuss.spec.ts` 5 例覆盖中断 / 回落 / 无会话 id / 取消失败。lab 实测:计划卡上点「去聊天里说」后,会话日志的回合结束原因是 `aborted`(不再是 `completed`),输入框立即回来、会话显示「已停止」,工作步骤保持展开。
|
|
36
|
+
|
|
1
37
|
## v0.4.38
|
|
2
38
|
|
|
3
39
|
计划卡的「设置为目标并执行」现在会**真的顶掉**旧目标:宿主把 `GoalError` 经 RPC 网关中转成 `gateway/internal`,而插件只认 `GOAL_ALREADY_EXISTS`,于是 get→clear→create 的替换分支一直是死代码,点一次报一次「设置目标失败」。
|
|
@@ -7,6 +43,7 @@
|
|
|
7
43
|
- **Fix 2: transport failures no longer escape.** `create` / `get` / `clear` 全部经 `guarded()`:被 reject 的调用转成 `{ code: 'GOAL_TRANSPORT', message }` 的拒绝信封,不再冒泡成未处理异常(否则卡片丢掉错误态、计划停在待审却没有解释)。
|
|
8
44
|
- **Fix 3: readable messages.** `normalizeHostMessage()` 剥掉 `gateway/xxx:` 前缀,`failure()` 丢弃传输层码,卡片不会再出现双重前缀;失败信息在源头就干净,卡片无需改动。
|
|
9
45
|
- **Tests.** `tests/picker/plan-goal.spec.ts` 新增 6 例:网关形状的 paused / blocked 替换、非冲突网关拒绝不清目标、create / get / clear 三处 reject。`pnpm run check` 全绿(56 测试文件 / **510** 测试 + pack 闸门)。
|
|
46
|
+
- **Lab verification(0.2.1-rc.2,`DSH_HOME=~/.dsh-lab`,端口 3082,`mimo-v2.6-flash`)——端到端跑通了修复后的路径。** 在装了 0.4.38 的 lab Host 里用真实计划卡走完两轮:第一轮会话无目标,点「设置为目标并执行」→ `21:35:05 goal/change create goal-720a677c active`,计划执行完后自动 `complete`;随后用 `create_goal` 造了一个**未完成**的旧目标(`21:36:04 create goal-95e79b84 active`),再提交第二个计划、点同一个按钮 → 会话日志记录 **`21:39:54 goal/change clear`** 紧跟 **`21:39:54 goal/change create goal-358a71b2 active`**,卡片没有任何报错。也就是说 get→clear→create 的替换分支在真实 Host 上确实跑起来了。说明:本次没有做 0.4.37 的 A/B 对照(修复前该分支不可达,用户桌面上报出的 `gateway/internal: … already exists with phase "paused"` 就是它失败的现场);另外 lab 宿主本身有两处损坏,先修好才跑通:`session-query-sqlite@0.2.1-alpha.2` 与运行时 0.2.1-rc.2 不兼容导致 `sessionController` 不可用(升级 `dsh-experimental-session-search` 到 rc.2 解决),以及工作区注册表为空导致无法新建会话。
|
|
10
47
|
|
|
11
48
|
## v0.4.37
|
|
12
49
|
|
|
@@ -3,6 +3,337 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
3
3
|
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
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
|
|
6
337
|
//#region lib/types/auto-sessions-state.js
|
|
7
338
|
/**
|
|
8
339
|
* Durable snapshot of the sessions that were on the Auto preset when this row
|
|
@@ -352,10 +683,13 @@ const AUTO_PRESET = "auto";
|
|
|
352
683
|
* tool-call safety net allows unreviewed calls instead of failing them.
|
|
353
684
|
* `allowedCalls` counts the calls the net let through in that state, so the
|
|
354
685
|
* settings card can tell the human how much review was skipped.
|
|
686
|
+
* `skippedReadOnly` counts the calls the allowlist took off the reviewer, which
|
|
687
|
+
* is the number the settings card shows next to the switch.
|
|
355
688
|
*/
|
|
356
689
|
const autoReviewStatus = {
|
|
357
690
|
held: false,
|
|
358
|
-
allowedCalls: 0
|
|
691
|
+
allowedCalls: 0,
|
|
692
|
+
skippedReadOnly: 0
|
|
359
693
|
};
|
|
360
694
|
/**
|
|
361
695
|
* Resolve the pinned reviewer route from live settings.
|
|
@@ -419,15 +753,19 @@ function resolveSessionAutoReviewRoute(ctx, session) {
|
|
|
419
753
|
* Because Cordis `on(..., { prepend: true })` unshifts onto the event hook list,
|
|
420
754
|
* registering this listener AFTER `official.apply(scope)` places it at index 0
|
|
421
755
|
* — directly ahead of the official gate's own prepended listener — so
|
|
422
|
-
* `activeSession.run(
|
|
423
|
-
*
|
|
756
|
+
* `activeSession.run({ session, tool }, next)` wraps the entire official review
|
|
757
|
+
* execution. The stored tool name is what lets the reviewer be skipped for an
|
|
758
|
+
* allowlisted read without the gate ever seeing a fake preset.
|
|
424
759
|
*
|
|
425
760
|
* @param scope - the review scope context where the gate is mounted.
|
|
426
|
-
* @param activeSession - AsyncLocalStorage holding the
|
|
761
|
+
* @param activeSession - AsyncLocalStorage holding the call under review.
|
|
427
762
|
*/
|
|
428
763
|
function bindAutoReviewSessionCorrelation(scope, activeSession) {
|
|
429
764
|
scope.effect(function* () {
|
|
430
|
-
yield scope.on("tools/pre-execute", (exec, next) => activeSession.run(
|
|
765
|
+
yield scope.on("tools/pre-execute", (exec, next) => activeSession.run({
|
|
766
|
+
session: exec.agent?.session,
|
|
767
|
+
tool: exec.name
|
|
768
|
+
}, next), { prepend: true });
|
|
431
769
|
}, "model-switch: auto-review session correlation");
|
|
432
770
|
}
|
|
433
771
|
/**
|
|
@@ -546,12 +884,21 @@ function fiberChainOf(ctx) {
|
|
|
546
884
|
* displacing row and take it back without unmounting the gate (the gate's
|
|
547
885
|
* disposal would move live `auto` sessions to `danger-full-access`).
|
|
548
886
|
*
|
|
887
|
+
* `current()` also answers the read-only skip. The official gate's first line is
|
|
888
|
+
* `if (permissionPresets.current(session) !== 'auto') return next()`, so a call
|
|
889
|
+
* whose tool is allowlisted can be taken off the reviewer by answering with
|
|
890
|
+
* anything else — the gate itself then runs the normal `next()` path, leaving
|
|
891
|
+
* every later listener and guard in place. The answer is scoped to the call in
|
|
892
|
+
* flight through the async store, so outside a tool call this method reports the
|
|
893
|
+
* truth (`rememberAuto()` and the settings card read it for accounting).
|
|
894
|
+
*
|
|
549
895
|
* @param resolve - resolves the host's permission-preset service per call.
|
|
550
896
|
* @param log - the row logger used for the single degradation report.
|
|
551
897
|
* @param chain - diagnostic dump of the resolution chain; defaults to empty.
|
|
898
|
+
* @param bypass - optional read-only skip; absent when the feature is off.
|
|
552
899
|
* @returns A managed preset face.
|
|
553
900
|
*/
|
|
554
|
-
function defensivePermissionPresets(resolve, log, chain = () => "") {
|
|
901
|
+
function defensivePermissionPresets(resolve, log, chain = () => "", bypass) {
|
|
555
902
|
let reported = false;
|
|
556
903
|
let admit;
|
|
557
904
|
let release;
|
|
@@ -567,12 +914,18 @@ function defensivePermissionPresets(resolve, log, chain = () => "") {
|
|
|
567
914
|
};
|
|
568
915
|
return {
|
|
569
916
|
current(session) {
|
|
917
|
+
let preset;
|
|
570
918
|
try {
|
|
571
|
-
|
|
919
|
+
preset = requireService().current(session);
|
|
572
920
|
} catch (error) {
|
|
573
921
|
degrade("current()", error);
|
|
574
922
|
return CUSTOM_PRESET;
|
|
575
923
|
}
|
|
924
|
+
if (preset !== AUTO_PRESET || bypass === void 0) return preset;
|
|
925
|
+
const ambient = bypass.ambient();
|
|
926
|
+
if (ambient === void 0 || !bypass.allows(ambient.tool)) return preset;
|
|
927
|
+
autoReviewStatus.skippedReadOnly += 1;
|
|
928
|
+
return CUSTOM_PRESET;
|
|
576
929
|
},
|
|
577
930
|
set(session, preset) {
|
|
578
931
|
try {
|
|
@@ -622,7 +975,7 @@ function defensivePermissionPresets(resolve, log, chain = () => "") {
|
|
|
622
975
|
*/
|
|
623
976
|
function reviewScope(ctx, activeSession = new AsyncLocalStorage(), presets = defensivePermissionPresets(() => permissionPresetsOf(ctx), ctx.logger, () => fiberChainOf(ctx))) {
|
|
624
977
|
return ctx.extend({
|
|
625
|
-
llm: reviewLlm(ctx.llm, () => resolveSessionAutoReviewRoute(ctx, activeSession.getStore())),
|
|
978
|
+
llm: reviewLlm(ctx.llm, () => resolveSessionAutoReviewRoute(ctx, activeSession.getStore()?.session)),
|
|
626
979
|
permissionPresets: presets
|
|
627
980
|
});
|
|
628
981
|
}
|
|
@@ -709,8 +1062,30 @@ async function apply(ctx) {
|
|
|
709
1062
|
ctx.logger.error(cause);
|
|
710
1063
|
return;
|
|
711
1064
|
}
|
|
712
|
-
const presets = defensivePermissionPresets(() => permissionPresetsOf(ctx), ctx.logger, () => fiberChainOf(ctx));
|
|
713
1065
|
const activeSession = new AsyncLocalStorage();
|
|
1066
|
+
/**
|
|
1067
|
+
* The read-only skip, read live so a settings change applies to the next call.
|
|
1068
|
+
*
|
|
1069
|
+
* Fails closed: an unreadable setting, a disabled switch, or an empty list all
|
|
1070
|
+
* mean "review it". The allowlist is the user's explicit opt-in, so a broken
|
|
1071
|
+
* read must never widen what runs unreviewed.
|
|
1072
|
+
*/
|
|
1073
|
+
const skipReadOnly = (tool) => {
|
|
1074
|
+
let settings;
|
|
1075
|
+
try {
|
|
1076
|
+
settings = ctx.modelSwitch.currentSettings();
|
|
1077
|
+
} catch {
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
if (settings?.autoReviewSkipReadOnly !== true) return false;
|
|
1081
|
+
const list = settings.autoReviewAllowlist;
|
|
1082
|
+
if (!Array.isArray(list) || list.length === 0) return false;
|
|
1083
|
+
return isAllowedByList(tool, list);
|
|
1084
|
+
};
|
|
1085
|
+
const presets = defensivePermissionPresets(() => permissionPresetsOf(ctx), ctx.logger, () => fiberChainOf(ctx), {
|
|
1086
|
+
ambient: () => activeSession.getStore(),
|
|
1087
|
+
allows: skipReadOnly
|
|
1088
|
+
});
|
|
714
1089
|
const scope = reviewScope(ctx, activeSession, presets);
|
|
715
1090
|
installToolCallSafetyNet(ctx, {
|
|
716
1091
|
fiberChain: () => fiberChainOf(ctx),
|
|
@@ -1019,4 +1394,4 @@ async function apply(ctx) {
|
|
|
1019
1394
|
}, "model-switch: release the Auto registration before anything else");
|
|
1020
1395
|
}
|
|
1021
1396
|
//#endregion
|
|
1022
|
-
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 };
|
|
1397
|
+
export { bindAutoReviewSessionCorrelation as a, installToolCallSafetyNet as c, resolveSessionAutoReviewRoute as d, reviewLlm as f, resolveToolSource as h, autoReviewStatus as i, isReviewerInfrastructureFailure as l, DEFAULT_READ_ONLY_TOOLS as m, 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-BDx3LjVL.js";
|
|
2
2
|
export { apply, autoReviewGate, autoReviewRouteOf, autoReviewStatus, bindAutoReviewSessionCorrelation, defensivePermissionPresets, inject, installToolCallSafetyNet, isReviewerInfrastructureFailure, name, resolveSessionAutoReviewRoute, reviewLlm, reviewScope };
|