dsh-plugin-prompt-tool 3.0.1 → 3.0.2
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/engine/actions/catalog.mjs +2 -1
- package/engine/actions/shared.mjs +26 -12
- package/engine/actions/tool-visibility.mjs +27 -13
- package/engine/fillers.mjs +26 -2
- package/lib/client.js +4 -4
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +96 -49
- package/modules/soul/assets/reference/00-layers.md +27 -27
- package/modules/soul/assets/reference/02-echo.md +161 -161
- package/modules/soul/assets/reference/05-doctor.md +67 -67
- package/modules/soul/assets/reference/10-echo-anatomy.md +109 -109
- package/modules/soul/assets/reference/ECHO-/346/235/203/345/250/201/345/217/243/345/276/204.md +202 -202
- package/modules/soul/assets/reference/ECHO/345/205/255/345/261/202/344/270/216mnemon/345/215/217/345/220/214/350/257/264/346/230/216/344/271/246.md +229 -229
- package/modules/tool-surface/module.yml +7 -4
- package/package.json +1 -1
- package/skills/dsh-module/reference.md +2 -2
- package/templates/80-tool-surface.yml +7 -6
|
@@ -74,7 +74,8 @@ export const ACTION_KINDS = Object.freeze({
|
|
|
74
74
|
note: '一份声明拥有工具的可见面:allow/deny 名单、requireMatch、发现工具。名单语义对齐官方 '
|
|
75
75
|
+ 'ToolRestriction 的 and/or(同时给时先按 allow 保留、再按 deny 剔除)。deny 走 guard(本层与晚到工具)'
|
|
76
76
|
+ '与 restrict(继承的全局工具);allow 只有 restrict 能覆盖全局工具,本层注册无法在执行层「只保留」——'
|
|
77
|
-
+ '这是官方 restrict 的边界,不是本动作的取舍。'
|
|
77
|
+
+ '这是官方 restrict 的边界,不是本动作的取舍。requireMatch 表达「名单与装配面整体错位」:'
|
|
78
|
+
+ '名单里的名字一个都不存在才整面放宽,部分缺失只留诊断;解锁名不参与判定。',
|
|
78
79
|
},
|
|
79
80
|
'request-params': {
|
|
80
81
|
title: '改模型请求参数',
|
|
@@ -37,20 +37,28 @@ export function createMask(source, label, plugin, allowExtra) {
|
|
|
37
37
|
/** 动态白名单的落点(活集合,由调用方每轮重填);未声明 allowFrom 时不存在。 */
|
|
38
38
|
allowExtra,
|
|
39
39
|
/**
|
|
40
|
-
* fail-open 兜底开关(**可选**,缺省 false
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
40
|
+
* fail-open 兜底开关(**可选**,缺省 false):判据是「这份名单与装配面整体错位」——
|
|
41
|
+
* 名单里的名字**一个都不存在**时放弃裁剪、暴露完整目录;部分缺失只留诊断,收窄照常
|
|
42
|
+
* 生效(提供方被替换或关闭是正常部署差异,见 {@link toolSurfacePlan})。原
|
|
43
|
+
* `tool-bootstrap` 的同名语义就是「一个都不存在 → 放弃裁剪」,而原 `tool-filter` 是
|
|
44
|
+
* 「永远照名单裁」;两种语义都真实存在,所以这里**不设默认**——要兜底的声明自己写
|
|
45
|
+
* `requireMatch: true`。把某一个模块的特有语义当成通用默认,会让另一个声明路径的行为
|
|
46
|
+
* 被悄悄改掉(这正是对拍抓出来的)。
|
|
45
47
|
*/
|
|
46
48
|
requireMatch: source?.requireMatch === true,
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
+
/**
|
|
50
|
+
* 与 tool-filter 的名单判据 / Layer.admits 同一判据(tool-filter 模块已删除,判据保留)。
|
|
51
|
+
*
|
|
52
|
+
* 第二个参数是**解锁项快照**:allowExtra 是跨 agent 共享的活集合,每次装配都会 clear 后
|
|
53
|
+
* 按当前会话重填;执行闸在该装配之后才被调用,必须读自己会话的冻结快照,否则别的会话
|
|
54
|
+
* (或探针装配)会把本会话的解锁名抹掉。缺省仍是活集合,供同一次装配内的裁剪使用。
|
|
55
|
+
*/
|
|
56
|
+
blocks: (toolName, extra = allowExtra) => {
|
|
49
57
|
if (typeof toolName !== 'string' || toolName.length === 0) return false
|
|
50
58
|
// 保留传输不参与任何名单:allow 没点名也必须留下,deny 已在挂载期拒绝。
|
|
51
59
|
if (toolName === RUN_CODE) return false
|
|
52
60
|
if (deny !== undefined && deny.has(toolName)) return true
|
|
53
|
-
if (
|
|
61
|
+
if (extra?.has(toolName) === true) return false
|
|
54
62
|
// 声明了 allow、或声明了 allowFrom(= 白名单模式,静态名单视为空集),未点名即裁掉。
|
|
55
63
|
return (allow !== undefined || allowExtra !== undefined) && allow?.has(toolName) !== true
|
|
56
64
|
},
|
|
@@ -60,17 +68,23 @@ export function createMask(source, label, plugin, allowExtra) {
|
|
|
60
68
|
/**
|
|
61
69
|
* 工具可见面的单一判据:装配目录、`tools:sdk` 正文与执行闸共用同一份 fail-open 决定。
|
|
62
70
|
*
|
|
63
|
-
* `available` = 目录名 ∪ SDK 正文名;`required`
|
|
71
|
+
* `available` = 目录名 ∪ SDK 正文名;`required` 只取**静态 allow**,再剔掉
|
|
64
72
|
* `run_code`(保留传输)与 `mask.blocks()` 为真的名字(deny 覆盖者不该被要求存在)。
|
|
65
|
-
*
|
|
73
|
+
*
|
|
74
|
+
* 放宽条件是「名单里的名字**一个都不存在**」:那才说明装配面与声明整体错位(换了一套
|
|
75
|
+
* 部署/平台)。只缺一部分是提供方被替换或关掉的正常部署差异——照常收窄,缺的工具由发现
|
|
76
|
+
* 通道补位,替换提供方的名字由部署层名单登记。解锁名(allowExtra)**不参与判定**:它是只做
|
|
77
|
+
* 加法的提示,若参与,模型解锁一个拼错或已改名的工具就能关掉收窄。
|
|
78
|
+
*
|
|
79
|
+
* 放宽时三面必须一起放宽——只放宽一面会让模型看到的与能调的不一致。
|
|
66
80
|
*/
|
|
67
81
|
export function toolSurfacePlan(mask, available) {
|
|
68
82
|
// 动作层的挂载期校验(prepareToolVisibility)已拒绝 requireMatch + 无 allow。
|
|
69
83
|
const required = mask.requireMatch
|
|
70
|
-
? [...new Set(
|
|
84
|
+
? [...new Set(mask.allow ?? [])].filter((name) => name !== RUN_CODE && !mask.blocks(name))
|
|
71
85
|
: []
|
|
72
86
|
const missing = required.filter((name) => !available.has(name))
|
|
73
|
-
return { failOpen:
|
|
87
|
+
return { failOpen: required.length > 0 && missing.length === required.length, missing }
|
|
74
88
|
}
|
|
75
89
|
|
|
76
90
|
export function requireString(value, label, plugin) {
|
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
*
|
|
14
14
|
* 名单语义对齐官方 ToolRestriction(deepseek-harness packages/core/tools/src/index.ts:701-706):
|
|
15
15
|
* allow 与 deny 可以同时给,判据是「先按 allow 保留,再按 deny 剔除」。requireMatch 只在
|
|
16
|
-
* allow
|
|
17
|
-
*
|
|
16
|
+
* allow 模式有意义,判据是「这份名单与装配面整体错位」:名单里的名字**一个都不在**目录与
|
|
17
|
+
* SDK 正文的并集里,才放弃裁剪、暴露完整目录(fail-open)并告警。只缺一部分是提供方被
|
|
18
|
+
* 替换或关闭的正常部署差异,收窄照常生效、缺项只留一条诊断;解锁名是只做加法的提示,
|
|
19
|
+
* 不参与这个判定。
|
|
18
20
|
*
|
|
19
21
|
* 执行边界:deny 同时用 restrict(继承面)与 guard(本层/晚到);allow 不用 restrict——
|
|
20
22
|
* 它的 allow 只能收继承面名字,而 `schemas()` 全局视图会漏掉预设/祖先层,局部 allow 会
|
|
@@ -110,7 +112,8 @@ export function prepareToolVisibility(action, plugin) {
|
|
|
110
112
|
const capability = createAgentCapability((agent) => {
|
|
111
113
|
const tools = agent?.ctx?.tools
|
|
112
114
|
if (tools === undefined) return undefined
|
|
113
|
-
|
|
115
|
+
// unlocked:本会话装配时冻结的解锁名快照(await next() 期间别的装配会重填共享集合)。
|
|
116
|
+
const state = { failOpen: false, unlocked: undefined }
|
|
114
117
|
const disposers = []
|
|
115
118
|
try {
|
|
116
119
|
const known = toolScopeSnapshot(tools).globalNames
|
|
@@ -135,7 +138,7 @@ export function prepareToolVisibility(action, plugin) {
|
|
|
135
138
|
const toolName = exec?.name
|
|
136
139
|
if (toolName === RUN_CODE || typeof toolName !== 'string' || toolName.length === 0) return undefined
|
|
137
140
|
if (deny?.has(toolName)) return `${label}: tool ${JSON.stringify(toolName)} is denied by tool-visibility`
|
|
138
|
-
if (mask.allow !== undefined && !state.failOpen && mask.blocks(toolName)) {
|
|
141
|
+
if (mask.allow !== undefined && !state.failOpen && mask.blocks(toolName, state.unlocked)) {
|
|
139
142
|
return `${label}: tool ${JSON.stringify(toolName)} is not allowed by tool-visibility`
|
|
140
143
|
}
|
|
141
144
|
return undefined
|
|
@@ -148,8 +151,9 @@ export function prepareToolVisibility(action, plugin) {
|
|
|
148
151
|
return { disposers, data: state }
|
|
149
152
|
})
|
|
150
153
|
|
|
151
|
-
|
|
152
|
-
|
|
154
|
+
// 重挂判定用**本会话的快照**:共享集合在别的装配里会被重填,不能当身份。
|
|
155
|
+
const syncGate = (agent, unlocked) => {
|
|
156
|
+
const key = `${maskKey(mask)}|unlock:${unlocked === undefined ? '' : [...unlocked].sort().join(',')}`
|
|
153
157
|
capability.sync(agent, key)
|
|
154
158
|
}
|
|
155
159
|
|
|
@@ -183,8 +187,11 @@ export function prepareToolVisibility(action, plugin) {
|
|
|
183
187
|
fillUnlocked(allowExtra, context?.session ?? context?.agent?.session, unlock.tool, unlock.key, warnOnce, plugin, label)
|
|
184
188
|
}
|
|
185
189
|
}
|
|
190
|
+
// 在 await next() 之前冻结本会话的解锁名:之后的裁剪与执行闸都读这一份,
|
|
191
|
+
// 共享的 allowExtra 可能被并发的另一次装配(别的会话 / 探针)清空重填。
|
|
192
|
+
const unlocked = allowExtra === undefined ? undefined : new Set(allowExtra)
|
|
186
193
|
try {
|
|
187
|
-
syncGate(context?.agent)
|
|
194
|
+
syncGate(context?.agent, unlocked)
|
|
188
195
|
} catch (error) {
|
|
189
196
|
// 闸注册失败必须留痕;呈现面照常结算,绝不把异常抛给装配。
|
|
190
197
|
warnOnce(`${plugin}: tool-visibility action ${label} failed to sync execution gate: ${String(error?.message ?? error)}`)
|
|
@@ -204,16 +211,23 @@ export function prepareToolVisibility(action, plugin) {
|
|
|
204
211
|
// 没有工具面可观(既无目录也无 SDK 正文)时不裁剪、不告警、不动执行闸:
|
|
205
212
|
// 空 surface 不等于「名单全缺」;空目录(tools=[])仍是真实观测,照旧 fail-open。
|
|
206
213
|
if (!Array.isArray(result.tools) && sdkSection === undefined) return result
|
|
207
|
-
// 三面(目录 / 正文 /
|
|
214
|
+
// 三面(目录 / 正文 / 执行闸)共用这一份判断:名单全缺才整体放宽,呈现与执行不得两套口径。
|
|
208
215
|
const plan = toolSurfacePlan(mask, available)
|
|
209
216
|
const state = capability.get(context?.session ?? context?.agent?.session)
|
|
210
|
-
if (state !== undefined)
|
|
217
|
+
if (state !== undefined) {
|
|
218
|
+
state.data.failOpen = plan.failOpen
|
|
219
|
+
state.data.unlocked = unlocked
|
|
220
|
+
}
|
|
211
221
|
if (plan.failOpen) {
|
|
212
|
-
//
|
|
213
|
-
warnOnce(`${plugin}: tool-visibility action ${label}:
|
|
222
|
+
// 去重键固定到动作:缺项名随部署变化,不能让每个新集合各占一条 warnOnce。
|
|
223
|
+
warnOnce(`${plugin}: tool-visibility action ${label}: none of the resident tools exist (${plan.missing.join(', ')}) — exposing the full tool set (fail-open)`, `${plugin}: tool-visibility action ${label}: requireMatch fail-open`)
|
|
214
224
|
} else {
|
|
225
|
+
// 部分缺失不改变裁剪,但必须留痕:否则「某个常驻工具为什么不见了」只能靠猜。
|
|
226
|
+
if (plan.missing.length > 0) {
|
|
227
|
+
warnOnce(`${plugin}: tool-visibility action ${label}: resident tools absent (${plan.missing.join(', ')}) — narrowing continues; register replacement providers in the deployment allow list`, `${plugin}: tool-visibility action ${label}: resident tools absent`)
|
|
228
|
+
}
|
|
215
229
|
if (Array.isArray(result.tools)) {
|
|
216
|
-
const kept = result.tools.filter((tool) => !mask.blocks(tool?.name))
|
|
230
|
+
const kept = result.tools.filter((tool) => !mask.blocks(tool?.name, unlocked))
|
|
217
231
|
if (kept.length !== result.tools.length) result = { ...result, tools: kept }
|
|
218
232
|
}
|
|
219
233
|
if (Array.isArray(result.sections)) {
|
|
@@ -227,7 +241,7 @@ export function prepareToolVisibility(action, plugin) {
|
|
|
227
241
|
}
|
|
228
242
|
const names = mask.allow === undefined
|
|
229
243
|
? mask.deny
|
|
230
|
-
: new Set([...presentNames()].filter((name) => mask.blocks(name)))
|
|
244
|
+
: new Set([...presentNames()].filter((name) => mask.blocks(name, unlocked)))
|
|
231
245
|
if (names === undefined || names.size === 0) return section
|
|
232
246
|
const stripped = stripSdkDeclarations(section.text, names)
|
|
233
247
|
if (stripped === section.text) return section
|
package/engine/fillers.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { getService, parseToolNames } from './shared.mjs'
|
|
7
|
+
import { toolScopeSnapshot } from './tool-scope.mjs'
|
|
7
8
|
import { machinePaths } from './machine-paths.mjs'
|
|
8
9
|
import { SAFE_ENV_KEYS } from './env-safety.mjs'
|
|
9
10
|
import { createInstructionHintResolver } from './instruction-hint.mjs'
|
|
@@ -16,7 +17,22 @@ function explicitTemplate(config) {
|
|
|
16
17
|
return texts.length > 0 ? texts.join('\n\n') : typeof config.params?.text === 'string' ? config.params.text : ''
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* 只读同规则工具面声明;文案、条件和投递仍归普通注入动作。
|
|
22
|
+
* RESIDENT_TOOLS 只列**当前可见**的常驻工具:部署层名单可以登记替换提供方(如 dsh-ops 的
|
|
23
|
+
* ops_bash),但该插件未必安装;向模型宣告一个不存在的常驻工具会把「解锁」变成错误前提。
|
|
24
|
+
* 观测到工具面但一个常驻名都不在时(requireMatch 全缺/放宽场景)跳过提示;
|
|
25
|
+
* 观测不到工具面(服务缺失或读取失败)才回落声明名单——「没看到」不等于「都没有」。
|
|
26
|
+
*/
|
|
27
|
+
function visibleToolNames(ctx, agent) {
|
|
28
|
+
try {
|
|
29
|
+
const tools = getService(ctx, 'tools')
|
|
30
|
+
if (tools === undefined || typeof tools.schemas !== 'function') return undefined
|
|
31
|
+
return new Set(toolScopeSnapshot(tools, agent).visible.map((schema) => schema?.name))
|
|
32
|
+
} catch {
|
|
33
|
+
return undefined
|
|
34
|
+
}
|
|
35
|
+
}
|
|
20
36
|
function createToolCatalogResolver(config) {
|
|
21
37
|
if (config.params?.templateEvaluation === true) throw new TypeError('tool-catalog is read-only and does not support templateEvaluation')
|
|
22
38
|
if (typeof config.params?.action !== 'string' || config.params.action.trim().length === 0) {
|
|
@@ -28,12 +44,20 @@ function createToolCatalogResolver(config) {
|
|
|
28
44
|
const source = config.toolCatalogSource
|
|
29
45
|
if (source === undefined) throw new TypeError('tool-catalog requires a same-rule tool-visibility action')
|
|
30
46
|
const deny = new Set(source.deny ?? [])
|
|
31
|
-
const
|
|
47
|
+
const declared = [...new Set(source.allow)].filter(tool => !deny.has(tool))
|
|
32
48
|
const adapter = source.discovery === undefined ? undefined
|
|
33
49
|
: getService(ctx, DISCOVERY_SERVICE)?.get?.(agent, source.discovery.kind)
|
|
34
50
|
if (source.discovery !== undefined && typeof adapter?.tool !== 'string') {
|
|
35
51
|
throw new Error(`tool-catalog discovery adapter ${source.discovery.kind} is unavailable`)
|
|
36
52
|
}
|
|
53
|
+
let resident = declared
|
|
54
|
+
const visible = visibleToolNames(ctx, agent)
|
|
55
|
+
if (visible !== undefined) {
|
|
56
|
+
const present = declared.filter((name) => visible.has(name))
|
|
57
|
+
// 观测到全缺时不宣告任何常驻工具:与 requireMatch 放宽同源,避免「解锁」建在错误前提上。
|
|
58
|
+
if (present.length === 0) return null
|
|
59
|
+
resident = present
|
|
60
|
+
}
|
|
37
61
|
return { text, variables: { RESIDENT_TOOLS: resident.join(', '), DISCOVERY_TOOL: adapter?.tool ?? '' } }
|
|
38
62
|
}
|
|
39
63
|
}
|
package/lib/client.js
CHANGED
|
@@ -10549,7 +10549,7 @@ window.__ModuleLoader__.load({
|
|
|
10549
10549
|
"triggers.label.allowFrom": "按需解锁",
|
|
10550
10550
|
"triggers.label.tool": "发现工具",
|
|
10551
10551
|
"triggers.label.key": "键名",
|
|
10552
|
-
"triggers.label.requireMatch": "
|
|
10552
|
+
"triggers.label.requireMatch": "名单工具全部缺失时保留完整目录",
|
|
10553
10553
|
"triggers.label.keep": "仅保留",
|
|
10554
10554
|
"triggers.label.remove": "移除",
|
|
10555
10555
|
"triggers.label.clear": "清空全部",
|
|
@@ -10773,7 +10773,7 @@ window.__ModuleLoader__.load({
|
|
|
10773
10773
|
"triggers.label.allowFrom": "On-demand unlocking",
|
|
10774
10774
|
"triggers.label.tool": "Discovery tool",
|
|
10775
10775
|
"triggers.label.key": "Key",
|
|
10776
|
-
"triggers.label.requireMatch": "Keep the full catalog if
|
|
10776
|
+
"triggers.label.requireMatch": "Keep the full catalog only if no listed tool exists",
|
|
10777
10777
|
"triggers.label.keep": "Keep only",
|
|
10778
10778
|
"triggers.label.remove": "Remove",
|
|
10779
10779
|
"triggers.label.clear": "Clear all",
|
|
@@ -18457,7 +18457,7 @@ window.__ModuleLoader__.load({
|
|
|
18457
18457
|
"rules.surface.dynamic": "按需解锁",
|
|
18458
18458
|
"rules.surface.discoveryTool": "发现工具",
|
|
18459
18459
|
"rules.surface.argumentKey": "名单参数",
|
|
18460
|
-
"rules.surface.requireMatch": "
|
|
18460
|
+
"rules.surface.requireMatch": "工具全部缺失时保留全部",
|
|
18461
18461
|
"rules.surface.textMode": "过滤方式",
|
|
18462
18462
|
"rules.surface.unrestricted": "不过滤",
|
|
18463
18463
|
"rules.surface.keep": "仅保留",
|
|
@@ -18582,7 +18582,7 @@ window.__ModuleLoader__.load({
|
|
|
18582
18582
|
"rules.surface.dynamic": "Unlock on demand",
|
|
18583
18583
|
"rules.surface.discoveryTool": "Discovery tool",
|
|
18584
18584
|
"rules.surface.argumentKey": "List argument",
|
|
18585
|
-
"rules.surface.requireMatch": "Keep all if
|
|
18585
|
+
"rules.surface.requireMatch": "Keep all only if no tool exists",
|
|
18586
18586
|
"rules.surface.textMode": "Filter",
|
|
18587
18587
|
"rules.surface.unrestricted": "No filter",
|
|
18588
18588
|
"rules.surface.keep": "Keep only",
|