dsh-model-switch 0.4.38 → 0.4.39
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 +25 -0
- package/lib/{auto-review-CvmQaRVf.js → auto-review-C61WZIYI.js} +365 -10
- package/lib/auto-review.js +1 -1
- package/lib/client.js +708 -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 +113 -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/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { o as normalizeProviderGroups } from "./session-route-D1zoZgf3.js";
|
|
2
|
-
import { i as autoReviewStatus } from "./auto-review-
|
|
2
|
+
import { h as resolveToolSource, i as autoReviewStatus, m as DEFAULT_READ_ONLY_TOOLS } from "./auto-review-C61WZIYI.js";
|
|
3
3
|
import { ModelSwitchAdapterRegistry } from "./adapter-registry.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import z from "@deepseek-ai/schemastery";
|
|
@@ -28,6 +28,8 @@ const Config = z.object({
|
|
|
28
28
|
imageModel: z.string().volatile(),
|
|
29
29
|
autoReviewProvider: z.string().volatile(),
|
|
30
30
|
autoReviewModel: z.string().volatile(),
|
|
31
|
+
autoReviewSkipReadOnly: z.boolean().default(false).volatile(),
|
|
32
|
+
autoReviewAllowlist: z.array(z.string()).default([...DEFAULT_READ_ONLY_TOOLS]).volatile(),
|
|
31
33
|
compactProvider: z.string().volatile(),
|
|
32
34
|
compactModel: z.string().volatile(),
|
|
33
35
|
titleProvider: z.string().volatile(),
|
|
@@ -55,6 +57,8 @@ function readConfig(config) {
|
|
|
55
57
|
const imageModel = config.imageModel.get();
|
|
56
58
|
const autoReviewProvider = config.autoReviewProvider.get();
|
|
57
59
|
const autoReviewModel = config.autoReviewModel.get();
|
|
60
|
+
const autoReviewSkipReadOnly = config.autoReviewSkipReadOnly.get();
|
|
61
|
+
const autoReviewAllowlist = config.autoReviewAllowlist.get();
|
|
58
62
|
const compactProvider = config.compactProvider.get();
|
|
59
63
|
const compactModel = config.compactModel.get();
|
|
60
64
|
const titleProvider = config.titleProvider.get();
|
|
@@ -71,6 +75,8 @@ function readConfig(config) {
|
|
|
71
75
|
...imageModel === void 0 ? {} : { imageModel },
|
|
72
76
|
...autoReviewProvider === void 0 || autoReviewProvider === "" ? {} : { autoReviewProvider },
|
|
73
77
|
...autoReviewModel === void 0 || autoReviewModel === "" ? {} : { autoReviewModel },
|
|
78
|
+
...autoReviewSkipReadOnly ? { autoReviewSkipReadOnly } : {},
|
|
79
|
+
...Array.isArray(autoReviewAllowlist) ? { autoReviewAllowlist: [...autoReviewAllowlist] } : {},
|
|
74
80
|
...compactProvider === void 0 || compactProvider === "" ? {} : { compactProvider },
|
|
75
81
|
...compactModel === void 0 || compactModel === "" ? {} : { compactModel },
|
|
76
82
|
...titleProvider === void 0 || titleProvider === "" ? {} : { titleProvider },
|
|
@@ -320,6 +326,52 @@ function installGenerateImageTool(ctx, runtime) {
|
|
|
320
326
|
});
|
|
321
327
|
return controller;
|
|
322
328
|
}
|
|
329
|
+
//#endregion
|
|
330
|
+
//#region lib/types/tool-catalog.js
|
|
331
|
+
function descriptionOf(entry) {
|
|
332
|
+
const raw = entry.description;
|
|
333
|
+
if (typeof raw !== "string") return "";
|
|
334
|
+
return (raw.split("\n", 1)[0] ?? "").trim();
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Read the Host's current tool set.
|
|
338
|
+
*
|
|
339
|
+
* Structural and best-effort: a deployment without the tools registry (or a
|
|
340
|
+
* registry that throws while listing) yields an empty catalog instead of failing
|
|
341
|
+
* the settings page.
|
|
342
|
+
*
|
|
343
|
+
* @param ctx - any host context on the profile.
|
|
344
|
+
* @returns Every exposed tool with its resolved origin, name-sorted.
|
|
345
|
+
*/
|
|
346
|
+
function readToolCatalog(ctx) {
|
|
347
|
+
let tools;
|
|
348
|
+
try {
|
|
349
|
+
tools = ctx.get("tools", false);
|
|
350
|
+
} catch {
|
|
351
|
+
return [];
|
|
352
|
+
}
|
|
353
|
+
const schemas = tools?.schemas;
|
|
354
|
+
if (typeof schemas !== "function") return [];
|
|
355
|
+
let listed;
|
|
356
|
+
try {
|
|
357
|
+
listed = schemas.call(tools);
|
|
358
|
+
} catch {
|
|
359
|
+
return [];
|
|
360
|
+
}
|
|
361
|
+
if (!Array.isArray(listed)) return [];
|
|
362
|
+
const catalog = [];
|
|
363
|
+
for (const entry of listed) {
|
|
364
|
+
const name = entry.name;
|
|
365
|
+
if (typeof name !== "string" || name === "") continue;
|
|
366
|
+
catalog.push({
|
|
367
|
+
name,
|
|
368
|
+
description: descriptionOf(entry),
|
|
369
|
+
source: resolveToolSource(name)
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
catalog.sort((left, right) => left.name.localeCompare(right.name));
|
|
373
|
+
return catalog;
|
|
374
|
+
}
|
|
323
375
|
function statusResponse(status) {
|
|
324
376
|
return new Response(status === 415 ? "Unsupported Media Type" : status === 500 ? "Internal Server Error" : "Bad Request", { status });
|
|
325
377
|
}
|
|
@@ -375,7 +427,14 @@ async function handleFetchRpc(request, handler, operator) {
|
|
|
375
427
|
}
|
|
376
428
|
}
|
|
377
429
|
/** Bounded long-poll on the existing registry; each caller owns and disposes its subscription. */
|
|
378
|
-
function capabilitiesRpc(registry, snapshot, lifetime) {
|
|
430
|
+
function capabilitiesRpc(registry, snapshot, lifetime, catalog = () => ({
|
|
431
|
+
tools: [],
|
|
432
|
+
recommended: [...DEFAULT_READ_ONLY_TOOLS]
|
|
433
|
+
}), status = () => ({
|
|
434
|
+
held: false,
|
|
435
|
+
allowedCalls: 0,
|
|
436
|
+
skippedReadOnly: 0
|
|
437
|
+
})) {
|
|
379
438
|
let revision = 0;
|
|
380
439
|
let previous;
|
|
381
440
|
const read = () => {
|
|
@@ -389,6 +448,23 @@ function capabilitiesRpc(registry, snapshot, lifetime) {
|
|
|
389
448
|
};
|
|
390
449
|
};
|
|
391
450
|
return async (endpoint, payload, signal) => {
|
|
451
|
+
if (endpoint === "tools") {
|
|
452
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload) || Object.keys(payload).length !== 0) return {
|
|
453
|
+
ok: false,
|
|
454
|
+
error: {
|
|
455
|
+
details: {},
|
|
456
|
+
code: "invalid-request",
|
|
457
|
+
message: "Expected an empty payload"
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
return {
|
|
461
|
+
ok: true,
|
|
462
|
+
value: {
|
|
463
|
+
...catalog(),
|
|
464
|
+
status: status()
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
}
|
|
392
468
|
if (endpoint !== "capabilities") return {
|
|
393
469
|
ok: false,
|
|
394
470
|
error: {
|
|
@@ -444,7 +520,14 @@ function capabilitiesRpc(registry, snapshot, lifetime) {
|
|
|
444
520
|
function installCapabilitiesRpc(ctx, registry, snapshot) {
|
|
445
521
|
ctx.inject(["connection"], (scope) => scope.effect(() => {
|
|
446
522
|
const lifetime = new AbortController();
|
|
447
|
-
const handler = capabilitiesRpc(registry, snapshot, lifetime.signal)
|
|
523
|
+
const handler = capabilitiesRpc(registry, snapshot, lifetime.signal, () => ({
|
|
524
|
+
tools: readToolCatalog(ctx),
|
|
525
|
+
recommended: [...DEFAULT_READ_ONLY_TOOLS]
|
|
526
|
+
}), () => ({
|
|
527
|
+
held: autoReviewStatus.held,
|
|
528
|
+
allowedCalls: autoReviewStatus.allowedCalls,
|
|
529
|
+
skippedReadOnly: autoReviewStatus.skippedReadOnly
|
|
530
|
+
}));
|
|
448
531
|
const dispose = scope.connection.fetch.register({
|
|
449
532
|
path: "/api/plugin-rpc/model-switch",
|
|
450
533
|
methods: ["POST"],
|
|
@@ -59,11 +59,34 @@ export declare const inject: string[];
|
|
|
59
59
|
* tool-call safety net allows unreviewed calls instead of failing them.
|
|
60
60
|
* `allowedCalls` counts the calls the net let through in that state, so the
|
|
61
61
|
* settings card can tell the human how much review was skipped.
|
|
62
|
+
* `skippedReadOnly` counts the calls the allowlist took off the reviewer, which
|
|
63
|
+
* is the number the settings card shows next to the switch.
|
|
62
64
|
*/
|
|
63
65
|
export declare const autoReviewStatus: {
|
|
64
66
|
held: boolean;
|
|
65
67
|
allowedCalls: number;
|
|
68
|
+
skippedReadOnly: number;
|
|
66
69
|
};
|
|
70
|
+
/**
|
|
71
|
+
* What one `tools/pre-execute` call carries into the reviewer's async context.
|
|
72
|
+
*
|
|
73
|
+
* The official gate reads `permissionPresets.current(session)` at the top of its
|
|
74
|
+
* listener; this store is what lets that read answer "no review needed" for one
|
|
75
|
+
* call without touching the rest of the pipeline (see
|
|
76
|
+
* {@link defensivePermissionPresets}).
|
|
77
|
+
*/
|
|
78
|
+
export interface AutoCallContext {
|
|
79
|
+
readonly session: Session | undefined;
|
|
80
|
+
/** `exec.name` — the tool under review. */
|
|
81
|
+
readonly tool: string;
|
|
82
|
+
}
|
|
83
|
+
/** The read-only skip: which tools, and whether the switch is on. */
|
|
84
|
+
export interface ReadOnlyBypass {
|
|
85
|
+
/** The call currently executing in this async context, if any. */
|
|
86
|
+
ambient(): AutoCallContext | undefined;
|
|
87
|
+
/** Whether this tool name is on the allowlist. */
|
|
88
|
+
allows(tool: string): boolean;
|
|
89
|
+
}
|
|
67
90
|
/** Pinned reviewer route; both identifiers are non-empty. */
|
|
68
91
|
export interface AutoReviewRoute {
|
|
69
92
|
readonly provider: string;
|
|
@@ -108,13 +131,14 @@ export declare function resolveSessionAutoReviewRoute(ctx: Context, session?: Se
|
|
|
108
131
|
* Because Cordis `on(..., { prepend: true })` unshifts onto the event hook list,
|
|
109
132
|
* registering this listener AFTER `official.apply(scope)` places it at index 0
|
|
110
133
|
* — directly ahead of the official gate's own prepended listener — so
|
|
111
|
-
* `activeSession.run(
|
|
112
|
-
*
|
|
134
|
+
* `activeSession.run({ session, tool }, next)` wraps the entire official review
|
|
135
|
+
* execution. The stored tool name is what lets the reviewer be skipped for an
|
|
136
|
+
* allowlisted read without the gate ever seeing a fake preset.
|
|
113
137
|
*
|
|
114
138
|
* @param scope - the review scope context where the gate is mounted.
|
|
115
|
-
* @param activeSession - AsyncLocalStorage holding the
|
|
139
|
+
* @param activeSession - AsyncLocalStorage holding the call under review.
|
|
116
140
|
*/
|
|
117
|
-
export declare function bindAutoReviewSessionCorrelation(scope: Context, activeSession: AsyncLocalStorage<
|
|
141
|
+
export declare function bindAutoReviewSessionCorrelation(scope: Context, activeSession: AsyncLocalStorage<AutoCallContext | undefined>): void;
|
|
118
142
|
/**
|
|
119
143
|
* Whether a reviewer-path throw is a broken seam rather than a real decision.
|
|
120
144
|
* @param cause - whatever the waterfall threw.
|
|
@@ -188,14 +212,23 @@ export interface ManagedPermissionPresetsFace extends PermissionPresetsFace {
|
|
|
188
212
|
* displacing row and take it back without unmounting the gate (the gate's
|
|
189
213
|
* disposal would move live `auto` sessions to `danger-full-access`).
|
|
190
214
|
*
|
|
215
|
+
* `current()` also answers the read-only skip. The official gate's first line is
|
|
216
|
+
* `if (permissionPresets.current(session) !== 'auto') return next()`, so a call
|
|
217
|
+
* whose tool is allowlisted can be taken off the reviewer by answering with
|
|
218
|
+
* anything else — the gate itself then runs the normal `next()` path, leaving
|
|
219
|
+
* every later listener and guard in place. The answer is scoped to the call in
|
|
220
|
+
* flight through the async store, so outside a tool call this method reports the
|
|
221
|
+
* truth (`rememberAuto()` and the settings card read it for accounting).
|
|
222
|
+
*
|
|
191
223
|
* @param resolve - resolves the host's permission-preset service per call.
|
|
192
224
|
* @param log - the row logger used for the single degradation report.
|
|
193
225
|
* @param chain - diagnostic dump of the resolution chain; defaults to empty.
|
|
226
|
+
* @param bypass - optional read-only skip; absent when the feature is off.
|
|
194
227
|
* @returns A managed preset face.
|
|
195
228
|
*/
|
|
196
229
|
export declare function defensivePermissionPresets(resolve: () => PermissionPresetsFace | undefined, log: {
|
|
197
230
|
warn(line: string): void;
|
|
198
|
-
}, chain?: () => string): ManagedPermissionPresetsFace;
|
|
231
|
+
}, chain?: () => string, bypass?: ReadOnlyBypass): ManagedPermissionPresetsFace;
|
|
199
232
|
/**
|
|
200
233
|
* Build the scoped context the official gate runs under.
|
|
201
234
|
* @param ctx - host context whose modelSwitch settings pin the reviewer route.
|
|
@@ -204,7 +237,7 @@ export declare function defensivePermissionPresets(resolve: () => PermissionPres
|
|
|
204
237
|
* @returns A child context shadowing the llm service and the permission-preset
|
|
205
238
|
* service (always: a gate that sees the raw service can fail a tool call).
|
|
206
239
|
*/
|
|
207
|
-
export declare function reviewScope(ctx: Context, activeSession?: AsyncLocalStorage<
|
|
240
|
+
export declare function reviewScope(ctx: Context, activeSession?: AsyncLocalStorage<AutoCallContext | undefined>, presets?: ManagedPermissionPresetsFace): Context;
|
|
208
241
|
/**
|
|
209
242
|
* Structural view of the official gate's plugin face.
|
|
210
243
|
*
|
|
@@ -2,11 +2,20 @@ import type { Context } from '@deepseek-ai/cordis';
|
|
|
2
2
|
import { type ConnectionRpcHandler } from '@deepseek-ai/dsh-client-connection';
|
|
3
3
|
import type { ModelSwitchAdapterRegistry } from './adapter-registry.js';
|
|
4
4
|
import type { RuntimeCapabilities } from './runtime-capabilities.js';
|
|
5
|
+
import { type ToolCatalogSnapshot } from './tool-catalog.js';
|
|
6
|
+
/** Live reviewer counters surfaced next to the allowlist editor. */
|
|
7
|
+
export interface ReviewStatus {
|
|
8
|
+
readonly held: boolean;
|
|
9
|
+
readonly allowedCalls: number;
|
|
10
|
+
readonly skippedReadOnly: number;
|
|
11
|
+
}
|
|
5
12
|
export declare const CAPABILITIES_RPC_METHOD = "plugin-rpc/model-switch";
|
|
6
13
|
export interface CapabilitiesSnapshot {
|
|
7
14
|
readonly revision: number;
|
|
8
15
|
readonly capabilities: RuntimeCapabilities;
|
|
9
16
|
}
|
|
10
17
|
/** Bounded long-poll on the existing registry; each caller owns and disposes its subscription. */
|
|
11
|
-
export declare function capabilitiesRpc(registry: ModelSwitchAdapterRegistry, snapshot: () => RuntimeCapabilities, lifetime: AbortSignal)
|
|
18
|
+
export declare function capabilitiesRpc(registry: ModelSwitchAdapterRegistry, snapshot: () => RuntimeCapabilities, lifetime: AbortSignal, catalog?: () => ToolCatalogSnapshot,
|
|
19
|
+
/** Live Auto-reviewer counters (see `autoReviewStatus`), shown next to the allowlist. */
|
|
20
|
+
status?: () => ReviewStatus): ConnectionRpcHandler;
|
|
12
21
|
export declare function installCapabilitiesRpc(ctx: Context, registry: ModelSwitchAdapterRegistry, snapshot: () => RuntimeCapabilities): void;
|
|
@@ -2,7 +2,7 @@ import { type ReactNode } from 'react';
|
|
|
2
2
|
import type { ModelProviderGroup } from '@deepseek-ai/dsh-api-session-controller/types';
|
|
3
3
|
import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client';
|
|
4
4
|
import type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
5
|
-
import { type CapabilityRouteView, type ModelSwitchSettingsView, type SubagentSettingsView } from '../client-contract.js';
|
|
5
|
+
import { type CapabilityRouteView, type ModelSwitchSettingsView, type ReviewStatusView, type SubagentSettingsView, type ToolCatalogRow } from '../client-contract.js';
|
|
6
6
|
import { type ProviderGroupConfig } from '../provider-group.js';
|
|
7
7
|
import type { RuntimeCapabilities } from '../runtime-capabilities.js';
|
|
8
8
|
import { type CapabilitiesSnapshot } from './search-capabilities.js';
|
|
@@ -26,6 +26,16 @@ export interface ModelSwitchSettingsFace {
|
|
|
26
26
|
*/
|
|
27
27
|
saveSubagent: (ops: readonly ConfigViewOp<SubagentSettingsView>[]) => Promise<void>;
|
|
28
28
|
saveCapability: (route: 'search' | 'image' | 'autoReview' | 'compaction' | 'title', ops: readonly ConfigViewOp<CapabilityRouteView>[]) => Promise<void>;
|
|
29
|
+
/** Persist the read-only skip switch and its allowlist as one atomic write. */
|
|
30
|
+
saveReviewAllowlist: (value: {
|
|
31
|
+
enabled: boolean;
|
|
32
|
+
tools: readonly string[];
|
|
33
|
+
}) => Promise<void>;
|
|
34
|
+
/** The Host's live tool catalog; absent on a Host without the `tools` RPC endpoint. */
|
|
35
|
+
scanTools?: () => Promise<{
|
|
36
|
+
rows: readonly ToolCatalogRow[];
|
|
37
|
+
status?: ReviewStatusView;
|
|
38
|
+
}>;
|
|
29
39
|
/** Replace or remove (undefined) one provider's saved capability group. */
|
|
30
40
|
saveProviderGroup: (provider: string, group: ProviderGroupConfig | undefined) => Promise<void>;
|
|
31
41
|
loadCatalog: () => Promise<readonly ModelProviderGroup[]>;
|
|
@@ -53,6 +53,24 @@ export declare const zh: {
|
|
|
53
53
|
readonly groupOverridesMore: "组覆盖:{listed} 等 {count} 个组;无组时用上方默认值";
|
|
54
54
|
readonly saveTimeout: "保存超时,请重试";
|
|
55
55
|
readonly saveRejected: "保存被拒绝,请重试";
|
|
56
|
+
readonly reviewAllowlistGroup: "审查豁免";
|
|
57
|
+
readonly reviewAllowlist: "纯读工具免审查";
|
|
58
|
+
readonly reviewAllowlistBadge: "已开启";
|
|
59
|
+
readonly reviewAllowlistOnSummary: "名单内 {count} 条免审查";
|
|
60
|
+
readonly reviewAllowlistOffSummary: "未开启:每次调用都经审查";
|
|
61
|
+
readonly reviewAllowlistHelp: "开启后,名单内的纯读工具直接执行,不再调用审查模型;名单外的写操作、命令执行与删除仍照常审查。";
|
|
62
|
+
readonly reviewAllowlistNote: "来源按工具名解析(MCP 前缀 / 插件前缀 / 扩展后缀);同名工具可以冒充来源,徽章只是提示,不是安全边界。";
|
|
63
|
+
readonly reviewAllowlistSearch: "搜索工具名或来源";
|
|
64
|
+
readonly reviewAllowlistScan: "扫描本机工具";
|
|
65
|
+
readonly reviewAllowlistScanning: "扫描中…";
|
|
66
|
+
readonly reviewAllowlistScanFailed: "扫描失败:Host 没有提供工具目录接口。";
|
|
67
|
+
readonly reviewAllowlistReset: "恢复默认名单";
|
|
68
|
+
readonly reviewAllowlistRemove: "移除";
|
|
69
|
+
readonly reviewAllowlistAdd: "加入";
|
|
70
|
+
readonly reviewAllowlistNone: "名单为空:没有任何工具免审查。";
|
|
71
|
+
readonly reviewAllowlistAvailable: "本机存在但未放行的工具:";
|
|
72
|
+
readonly reviewAllowlistRepo: "仓库";
|
|
73
|
+
readonly reviewAllowlistSkipped: "本 Host 进程已为纯读调用跳过 {count} 次审查。";
|
|
56
74
|
readonly 'reason.central-subagent-routing': "Alpha.4 没有全局子代理启动路由接口。";
|
|
57
75
|
readonly 'reason.packaged-preset-roots': "Alpha.4 不支持插件提供额外 preset root。";
|
|
58
76
|
readonly 'reason.tool-owner-suppression': "Alpha.4 没有工具所有者或来源抑制接口。";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PendingQuestion } from '@deepseek-ai/dsh-client-ui-user-questions/client';
|
|
2
2
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
3
|
+
import { type CancelTurn } from '../../picker/discuss-plan.ts';
|
|
3
4
|
import { type GoalRemote } from '../../picker/plan-goal.ts';
|
|
4
5
|
import type { PlanPreviewBinding } from './plan-preview.ts';
|
|
5
6
|
import { type PickerDirectoryFace } from './PickerDirectory.ts';
|
|
@@ -26,6 +27,13 @@ export interface PlanReviewFace extends PickerDirectoryFace {
|
|
|
26
27
|
planPreview: PlanPreviewBinding;
|
|
27
28
|
/** Live catalog-group-id → card-key map from ProviderDirectory. */
|
|
28
29
|
catalogRoutes?: () => Readonly<Record<string, string>>;
|
|
30
|
+
/**
|
|
31
|
+
* Session-scoped turn cancellation (the official Stop path).
|
|
32
|
+
*
|
|
33
|
+
* Absent on a Host that exposes no conversation service; the discuss action
|
|
34
|
+
* then falls back to withdrawing the pending question.
|
|
35
|
+
*/
|
|
36
|
+
cancelTurn?: CancelTurn;
|
|
29
37
|
}
|
|
30
38
|
export type PlanReviewCardProps = PropsRuntime<'conversation.composer'> & PropsLocale<'composer-picker'> & InjectFace<PlanReviewFace> & {
|
|
31
39
|
matched: PendingQuestion;
|
|
@@ -12,6 +12,10 @@ export interface ModelSwitchSettingsView {
|
|
|
12
12
|
imageModel?: string;
|
|
13
13
|
autoReviewProvider?: string;
|
|
14
14
|
autoReviewModel?: string;
|
|
15
|
+
/** Opt-in read-only skip for the Auto reviewer; absent/false keeps every call reviewed. */
|
|
16
|
+
autoReviewSkipReadOnly?: boolean;
|
|
17
|
+
/** Exact tool names, or trailing-`*` patterns, that skip the reviewer when enabled. */
|
|
18
|
+
autoReviewAllowlist?: readonly string[];
|
|
15
19
|
compactProvider?: string;
|
|
16
20
|
compactModel?: string;
|
|
17
21
|
titleProvider?: string;
|
|
@@ -50,6 +54,23 @@ export declare const AUTO_REVIEW_SETTINGS_FIELDS: Readonly<{
|
|
|
50
54
|
readonly provider: "autoReviewProvider";
|
|
51
55
|
readonly model: "autoReviewModel";
|
|
52
56
|
}>;
|
|
57
|
+
/** Auto-reviewer read-only skip: the switch and its allowlist are edited as one unit. */
|
|
58
|
+
export declare const AUTO_REVIEW_ALLOWLIST_FIELDS: Readonly<{
|
|
59
|
+
readonly enabled: "autoReviewSkipReadOnly";
|
|
60
|
+
readonly tools: "autoReviewAllowlist";
|
|
61
|
+
}>;
|
|
62
|
+
/** One allowlist editor snapshot (switch + entries). */
|
|
63
|
+
export interface AutoReviewAllowlistView {
|
|
64
|
+
enabled: boolean;
|
|
65
|
+
tools: readonly string[];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Derive the allowlist editor's value.
|
|
69
|
+
*
|
|
70
|
+
* An absent list means "never configured" and shows the shipped defaults; an
|
|
71
|
+
* explicitly emptied list stays empty, so clearing it is not undone by a reload.
|
|
72
|
+
*/
|
|
73
|
+
export declare function deriveAutoReviewAllowlist(settings: ModelSwitchSettingsView): AutoReviewAllowlistView;
|
|
53
74
|
/** Compaction summarization route for `/compact` and automatic compaction; both unset means the official default (the conversation target). */
|
|
54
75
|
export declare const COMPACTION_SETTINGS_FIELDS: Readonly<{
|
|
55
76
|
readonly provider: "compactProvider";
|
|
@@ -68,3 +89,36 @@ export declare function deriveCompactionSettings(settings: ModelSwitchSettingsVi
|
|
|
68
89
|
export declare function deriveTitleSettings(settings: ModelSwitchSettingsView): CapabilityRouteView;
|
|
69
90
|
/** Whether both fields of a model route form a complete pair; an incomplete pair stays off. */
|
|
70
91
|
export declare function routePairConfigured(route: CapabilityRouteView | undefined): boolean;
|
|
92
|
+
/** Live reviewer counters shown beside the allowlist editor. */
|
|
93
|
+
export interface ReviewStatusView {
|
|
94
|
+
readonly held: boolean;
|
|
95
|
+
readonly allowedCalls: number;
|
|
96
|
+
readonly skippedReadOnly: number;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Decode the reviewer counters from a `tools` RPC payload.
|
|
100
|
+
* @param value - the RPC result value.
|
|
101
|
+
* @returns The counters, or undefined when the Host does not report them.
|
|
102
|
+
*/
|
|
103
|
+
export declare function decodeReviewStatus(value: unknown): ReviewStatusView | undefined;
|
|
104
|
+
/** One tool row in the allowlist editor, as the Host reports it. */
|
|
105
|
+
export interface ToolCatalogRow {
|
|
106
|
+
readonly name: string;
|
|
107
|
+
readonly description: string;
|
|
108
|
+
/** Human source label, e.g. `MCP · wincu`. */
|
|
109
|
+
readonly sourceLabel: string;
|
|
110
|
+
/** Repository of the providing package, when known. */
|
|
111
|
+
readonly repo?: string;
|
|
112
|
+
/** Whether the shipped defaults already allow it. */
|
|
113
|
+
readonly recommended: boolean;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Decode the `tools` RPC payload.
|
|
117
|
+
*
|
|
118
|
+
* Malformed entries are skipped rather than failing the page: the catalog is a
|
|
119
|
+
* convenience view over the Host's tools, and a partial list is still useful.
|
|
120
|
+
*
|
|
121
|
+
* @param value - the RPC result value.
|
|
122
|
+
* @returns The rows, or undefined when the payload is not a catalog at all.
|
|
123
|
+
*/
|
|
124
|
+
export declare function decodeToolCatalog(value: unknown): readonly ToolCatalogRow[] | undefined;
|
|
@@ -16,6 +16,19 @@ export interface ModelSwitchSettings {
|
|
|
16
16
|
autoReviewProvider?: string;
|
|
17
17
|
/** Reviewer model for the Auto preset; set together with `autoReviewProvider`. */
|
|
18
18
|
autoReviewModel?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Opt-in: pure-read tools from {@link autoReviewAllowlist} run without the Auto
|
|
21
|
+
* reviewer. Absent/false keeps every call reviewed (the shipped default).
|
|
22
|
+
*/
|
|
23
|
+
autoReviewSkipReadOnly?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Tool names that skip the reviewer when {@link autoReviewSkipReadOnly} is on.
|
|
26
|
+
*
|
|
27
|
+
* Exact names, plus a trailing `*` pattern for tools whose name carries a
|
|
28
|
+
* renameable owner (the QQ bot extension suffixes its tools with the bot's
|
|
29
|
+
* name). A bare `*` is rejected by the settings editor and never matches.
|
|
30
|
+
*/
|
|
31
|
+
autoReviewAllowlist?: readonly string[];
|
|
19
32
|
/** Summarization provider for `/compact` and automatic compaction; both fields unset = official default (the conversation target). */
|
|
20
33
|
compactProvider?: string;
|
|
21
34
|
/** Summarization model for `/compact` and automatic compaction; set together with `compactProvider`. */
|
|
@@ -39,6 +52,10 @@ export interface Config {
|
|
|
39
52
|
imageModel: Volatile<string | undefined>;
|
|
40
53
|
autoReviewProvider: Volatile<string | undefined>;
|
|
41
54
|
autoReviewModel: Volatile<string | undefined>;
|
|
55
|
+
/** Opt-in: skip the Auto reviewer for allowlisted pure-read tools (default off). */
|
|
56
|
+
autoReviewSkipReadOnly: Volatile<boolean>;
|
|
57
|
+
/** Tool names (exact, or a trailing `*` pattern) that skip the Auto reviewer. */
|
|
58
|
+
autoReviewAllowlist: Volatile<readonly string[]>;
|
|
42
59
|
compactProvider: Volatile<string | undefined>;
|
|
43
60
|
compactModel: Volatile<string | undefined>;
|
|
44
61
|
titleProvider: Volatile<string | undefined>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Withdraw a pending plan review so the human can speak instead.
|
|
3
|
+
*
|
|
4
|
+
* The card used to only dismiss the pending question. Inside plan mode that
|
|
5
|
+
* dismissal makes the tool call return `ASK_CANCELLED`, the model answers, and
|
|
6
|
+
* the turn ends as **completed** — and the transcript folds every completed
|
|
7
|
+
* turn's work details at the configured timing (`collapseTiming: 'completion'`
|
|
8
|
+
* by default). The human then has to re-expand the steps they were reading.
|
|
9
|
+
*
|
|
10
|
+
* Interrupting the turn instead ends it as **aborted**, which the transcript's
|
|
11
|
+
* own always-open rule keeps permanently expanded:
|
|
12
|
+
*
|
|
13
|
+
* ```js
|
|
14
|
+
* const reason = turnLocation?.end?.data.reason.kind
|
|
15
|
+
* const alwaysOpen = presentation?.turnClosed === false
|
|
16
|
+
* || presentation?.hasInterleavedInput === true
|
|
17
|
+
* || reason === 'aborted' || reason === 'interrupted' || reason === 'error'
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* It also returns the composer, because the pending question settles either way.
|
|
21
|
+
* The official Stop button takes the same route (`conversation.cancel()`), so
|
|
22
|
+
* this is a shipped seam rather than a special case.
|
|
23
|
+
*
|
|
24
|
+
* @module dsh-model-switch/picker/discuss-plan
|
|
25
|
+
*/
|
|
26
|
+
/** How the review was withdrawn. */
|
|
27
|
+
export type DiscussOutcome = 'interrupted' | 'dismissed';
|
|
28
|
+
/** Session-scoped turn cancellation, when the Host exposes one. */
|
|
29
|
+
export type CancelTurn = (sessionId: string) => Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Withdraw the review by interrupting the turn, falling back to dismissal.
|
|
32
|
+
*
|
|
33
|
+
* Order matters: cancellation is what keeps the work details expanded, so it is
|
|
34
|
+
* tried first and only genuinely unavailable/refused cancellation falls through
|
|
35
|
+
* to the released withdrawal (which still gives the human the composer back, just
|
|
36
|
+
* without the expansion guarantee).
|
|
37
|
+
*
|
|
38
|
+
* @param cancelTurn - the session cancel seam; absent on a Host without one.
|
|
39
|
+
* @param sessionId - the session whose turn is being withdrawn.
|
|
40
|
+
* @param dismiss - the released `PendingQuestion` withdrawal.
|
|
41
|
+
* @returns Which route withdrew the review.
|
|
42
|
+
*/
|
|
43
|
+
export declare function discussPlan(cancelTurn: CancelTurn | undefined, sessionId: string, dismiss: () => Promise<void>): Promise<DiscussOutcome>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure-read tool allowlist for the Auto reviewer.
|
|
3
|
+
*
|
|
4
|
+
* DSH's official Auto gate reviews **every** tool call with the model unless the
|
|
5
|
+
* session is off the `auto` preset, so a `read` or `grep` costs a reviewer round
|
|
6
|
+
* trip. This module owns the data behind skipping that review: which tool names
|
|
7
|
+
* are safe to run unreviewed, how a name maps back to the plugin/MCP that
|
|
8
|
+
* provides it, and the pattern syntax that keeps a re-named MCP/extension tool
|
|
9
|
+
* covered without editing the list.
|
|
10
|
+
*
|
|
11
|
+
* Everything here is pure: no `ctx`, no filesystem, no settings. The caller
|
|
12
|
+
* decides when the list applies (see `./auto-review.ts`), which keeps the rules
|
|
13
|
+
* testable without a Host.
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-model-switch/tool-allowlist
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Where one tool comes from.
|
|
19
|
+
*
|
|
20
|
+
* - `core`: a released `@deepseek-ai/dsh-*` package (ships with the Host).
|
|
21
|
+
* - `plugin`: a profile plugin that registers model tools.
|
|
22
|
+
* - `mcp`: a tool bridged from an MCP server (`mcp__<server>__<tool>`).
|
|
23
|
+
* - `extension`: a `<tool>__<name>` tool contributed through an extension
|
|
24
|
+
* directory — the suffix is the *owner's* name (a QQ bot persona), not a
|
|
25
|
+
* fixed string, which is why the defaults use a pattern.
|
|
26
|
+
* - `unknown`: not in this module's calibrated catalog.
|
|
27
|
+
*/
|
|
28
|
+
export type ToolSourceKind = 'core' | 'plugin' | 'mcp' | 'extension' | 'unknown';
|
|
29
|
+
/** One resolved tool origin, ready to render next to a tool name. */
|
|
30
|
+
export interface ToolSource {
|
|
31
|
+
readonly kind: ToolSourceKind;
|
|
32
|
+
/** Short human label, e.g. `插件 · dsh-ops` or `MCP · wincu`. */
|
|
33
|
+
readonly label: string;
|
|
34
|
+
/** Repository for the providing package, when known. */
|
|
35
|
+
readonly repo?: string;
|
|
36
|
+
/** Package / server / owner name, when known. */
|
|
37
|
+
readonly owner?: string;
|
|
38
|
+
}
|
|
39
|
+
/** One catalogued origin: the single source of truth for labels and repos. */
|
|
40
|
+
export interface SourceCatalogEntry {
|
|
41
|
+
readonly id: string;
|
|
42
|
+
readonly kind: Exclude<ToolSourceKind, 'unknown'>;
|
|
43
|
+
readonly label: string;
|
|
44
|
+
readonly repo: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Every origin this allowlist ships defaults for.
|
|
48
|
+
*
|
|
49
|
+
* The release notes render this same table, so a new default entry cannot be
|
|
50
|
+
* added without naming the package and its repository.
|
|
51
|
+
*/
|
|
52
|
+
export declare const SOURCE_CATALOG: readonly SourceCatalogEntry[];
|
|
53
|
+
/** Catalog lookup by id. */
|
|
54
|
+
export declare const SOURCE_BY_ID: ReadonlyMap<string, SourceCatalogEntry>;
|
|
55
|
+
/**
|
|
56
|
+
* Default allowlist: every pure-read tool this deployment exposes, calibrated on
|
|
57
|
+
* a live desktop profile (105 tools) at dsh 0.2.1-rc.2.
|
|
58
|
+
*
|
|
59
|
+
* Grouped by origin so a reader can trace each entry through {@link SOURCE_CATALOG}.
|
|
60
|
+
* Entries ending in `*` are patterns: the QQ bot extension suffixes its tools with
|
|
61
|
+
* the bot's own name, so `sessions_list__*` keeps working when the persona is
|
|
62
|
+
* renamed.
|
|
63
|
+
*/
|
|
64
|
+
export declare const DEFAULT_READ_ONLY_TOOLS: readonly string[];
|
|
65
|
+
/** Why one allowlist entry was refused. */
|
|
66
|
+
export type AllowEntryProblem = 'empty' | 'bare-wildcard' | 'leading-wildcard';
|
|
67
|
+
/**
|
|
68
|
+
* Validate one allowlist entry.
|
|
69
|
+
*
|
|
70
|
+
* `*` is a suffix wildcard: the entry must keep at least one literal leading
|
|
71
|
+
* character, so a stray `*` can never turn the list into "allow everything".
|
|
72
|
+
*
|
|
73
|
+
* @param entry - the raw entry as typed in settings.
|
|
74
|
+
* @returns `ok`, or the specific problem.
|
|
75
|
+
*/
|
|
76
|
+
export declare function validateAllowEntry(entry: string): 'ok' | AllowEntryProblem;
|
|
77
|
+
/**
|
|
78
|
+
* Whether a tool name matches one allowlist entry.
|
|
79
|
+
*
|
|
80
|
+
* Exact names match exactly; an entry may end in `*`, which matches any
|
|
81
|
+
* remaining characters (including none). Invalid entries never match.
|
|
82
|
+
*
|
|
83
|
+
* @param name - the tool name from `exec.name`.
|
|
84
|
+
* @param entry - one allowlist entry.
|
|
85
|
+
* @returns true when the call may skip review.
|
|
86
|
+
*/
|
|
87
|
+
export declare function matchesAllowPattern(name: string, entry: string): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Whether a tool name is covered by an allowlist.
|
|
90
|
+
* @param name - the tool name under review.
|
|
91
|
+
* @param allowlist - configured entries, patterns included.
|
|
92
|
+
* @returns true when at least one entry matches.
|
|
93
|
+
*/
|
|
94
|
+
export declare function isAllowedByList(name: string, allowlist: readonly string[]): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Resolve the origin of one tool name for display.
|
|
97
|
+
*
|
|
98
|
+
* Resolution order: MCP bridge → `<tool>__<owner>` extension → plugin prefix →
|
|
99
|
+
* plugin table → core catalog → unknown. Only the first two are structural; the
|
|
100
|
+
* rest are calibrated names, so an unlisted tool is reported as unknown rather
|
|
101
|
+
* than guessed into a package.
|
|
102
|
+
*
|
|
103
|
+
* @param name - the tool name.
|
|
104
|
+
* @returns The origin, with its repository when known.
|
|
105
|
+
*/
|
|
106
|
+
export declare function resolveToolSource(name: string): ToolSource;
|
|
107
|
+
/**
|
|
108
|
+
* The `<tool>__<owner>` base names this deployment's extension directory ships.
|
|
109
|
+
*
|
|
110
|
+
* Used by the settings editor to explain what a pattern covers; it is not a
|
|
111
|
+
* security boundary — the allowlist itself is.
|
|
112
|
+
*/
|
|
113
|
+
export declare const EXTENSION_TOOL_BASES: readonly string[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Host's live tool catalog, annotated with where each tool comes from.
|
|
3
|
+
*
|
|
4
|
+
* The settings editor needs to show what actually exists on this machine — not
|
|
5
|
+
* just the shipped defaults — so a human can tick a tool the defaults miss (a new
|
|
6
|
+
* MCP server, a renamed bot extension) without editing YAML. The only public
|
|
7
|
+
* source for that list is `ctx.tools.schemas()`, which is the same model-facing
|
|
8
|
+
* set the reviewer sees.
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-model-switch/tool-catalog
|
|
11
|
+
*/
|
|
12
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
13
|
+
import { type ToolSource } from './tool-allowlist.js';
|
|
14
|
+
/** One tool the running Host currently exposes. */
|
|
15
|
+
export interface ToolCatalogEntry {
|
|
16
|
+
readonly name: string;
|
|
17
|
+
/** First line of the tool description, for the settings list. */
|
|
18
|
+
readonly description: string;
|
|
19
|
+
readonly source: ToolSource;
|
|
20
|
+
}
|
|
21
|
+
/** The wire shape of the `tools` RPC endpoint. */
|
|
22
|
+
export interface ToolCatalogSnapshot {
|
|
23
|
+
readonly tools: readonly ToolCatalogEntry[];
|
|
24
|
+
/** Names that matched the shipped defaults, for the "已放行" mark. */
|
|
25
|
+
readonly recommended: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Read the Host's current tool set.
|
|
29
|
+
*
|
|
30
|
+
* Structural and best-effort: a deployment without the tools registry (or a
|
|
31
|
+
* registry that throws while listing) yields an empty catalog instead of failing
|
|
32
|
+
* the settings page.
|
|
33
|
+
*
|
|
34
|
+
* @param ctx - any host context on the profile.
|
|
35
|
+
* @returns Every exposed tool with its resolved origin, name-sorted.
|
|
36
|
+
*/
|
|
37
|
+
export declare function readToolCatalog(ctx: Context): ToolCatalogEntry[];
|