dsh-plugin-prompt-tool 2.1.0 → 3.0.0

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.
Files changed (204) hide show
  1. package/README.md +100 -233
  2. package/engine/actions/append-context.mjs +3 -24
  3. package/engine/actions/assembly.mjs +70 -94
  4. package/engine/actions/catalog.mjs +65 -28
  5. package/engine/actions/content.mjs +8 -152
  6. package/engine/actions/examples.mjs +8 -4
  7. package/engine/actions/inbox-prepend.mjs +4 -5
  8. package/engine/actions/index.mjs +17 -9
  9. package/engine/actions/inject-text.mjs +24 -4
  10. package/engine/actions/pre-step-filter.mjs +3 -4
  11. package/engine/actions/registration.mjs +20 -28
  12. package/engine/actions/request-params.mjs +21 -1
  13. package/engine/actions/shared.mjs +22 -13
  14. package/engine/actions/stream-output.mjs +26 -0
  15. package/engine/actions/tool-gate.mjs +31 -0
  16. package/engine/actions/tool-result.mjs +36 -0
  17. package/engine/actions/tool-visibility.mjs +248 -0
  18. package/engine/agent-capability.mjs +67 -0
  19. package/engine/anchor-match.mjs +14 -21
  20. package/engine/audience.d.mts +2 -0
  21. package/engine/audience.mjs +102 -0
  22. package/engine/branch.d.mts +4 -0
  23. package/engine/branch.mjs +61 -18
  24. package/engine/compaction-epoch.d.mts +25 -0
  25. package/engine/compaction-epoch.mjs +27 -4
  26. package/engine/compositions/source/local/filesystem-editor.yml +3 -3
  27. package/engine/compositions/source/local/instruction-hint.yml +2 -4
  28. package/engine/compositions/source/local/module-file-tools.yml +2 -0
  29. package/engine/compositions/source/local/module-import-tools.yml +4 -0
  30. package/engine/compositions/source/local/module-manifest-tools.yml +2 -0
  31. package/engine/compositions/source/local/module-rule-tools.yml +5 -0
  32. package/engine/compositions/source/local/module-variable-tools.yml +5 -0
  33. package/engine/compositions/source/local/present-as.yml +26 -0
  34. package/engine/compositions/source/local/tool-config-engine.yml +2 -3
  35. package/engine/compositions/source/local/tool-search.yml +11 -0
  36. package/engine/condition-facts.mjs +84 -0
  37. package/engine/conditions/anchor.mjs +63 -23
  38. package/engine/conditions/availability.mjs +1 -1
  39. package/engine/conditions/composite.mjs +3 -3
  40. package/engine/conditions/count.mjs +42 -11
  41. package/engine/conditions/evaluation.mjs +74 -19
  42. package/engine/conditions/examples.mjs +2 -1
  43. package/engine/conditions/index.d.mts +15 -0
  44. package/engine/conditions/index.mjs +23 -9
  45. package/engine/conditions/operators.mjs +4 -0
  46. package/engine/conditions/phase.mjs +9 -3
  47. package/engine/conditions/preset.mjs +1 -1
  48. package/engine/conditions/session.mjs +8 -2
  49. package/engine/conditions/subject.mjs +144 -65
  50. package/engine/conditions/text.mjs +26 -8
  51. package/engine/conditions/variable.mjs +83 -0
  52. package/engine/context-activation.d.mts +11 -0
  53. package/engine/{st-world-book.mjs → context-activation.mjs} +104 -64
  54. package/engine/delivery.d.mts +2 -0
  55. package/engine/delivery.mjs +93 -0
  56. package/engine/discovery.d.mts +38 -0
  57. package/engine/discovery.mjs +146 -0
  58. package/engine/env-safety.mjs +14 -0
  59. package/engine/executor.d.mts +28 -0
  60. package/engine/executor.mjs +290 -205
  61. package/engine/fillers.mjs +39 -8
  62. package/engine/fixed-registration.mjs +12 -0
  63. package/engine/history.mjs +71 -0
  64. package/engine/instruction-blocks.d.mts +13 -0
  65. package/engine/instruction-blocks.mjs +21 -0
  66. package/engine/instruction-files.d.mts +5 -0
  67. package/engine/instruction-files.mjs +97 -0
  68. package/engine/instruction-hint.mjs +69 -103
  69. package/engine/interpolate.d.mts +7 -0
  70. package/engine/interpolate.mjs +44 -13
  71. package/engine/layers.mjs +163 -227
  72. package/engine/machine-paths.mjs +40 -0
  73. package/engine/module-file-tools.mjs +8 -0
  74. package/engine/module-import-tools.mjs +10 -0
  75. package/engine/module-manifest-tools.mjs +8 -0
  76. package/engine/module-rule-tools.mjs +10 -0
  77. package/engine/module-variable-tools.mjs +10 -0
  78. package/engine/order.d.mts +14 -0
  79. package/engine/order.mjs +0 -3
  80. package/engine/path-safety.d.mts +2 -0
  81. package/engine/path-safety.mjs +58 -0
  82. package/engine/pre-step-source.d.mts +69 -0
  83. package/engine/pre-step-source.mjs +103 -0
  84. package/engine/present-as.mjs +77 -0
  85. package/engine/promotion-pool.d.mts +9 -0
  86. package/engine/promotion-pool.mjs +69 -0
  87. package/engine/record.mjs +2 -0
  88. package/engine/rule-engine.mjs +3 -2
  89. package/engine/rule-facts.d.mts +11 -0
  90. package/engine/rule-facts.mjs +60 -0
  91. package/engine/rule-runtime.d.mts +46 -0
  92. package/engine/rule-runtime.mjs +98 -13
  93. package/engine/rule-spec.d.mts +106 -0
  94. package/engine/rule-spec.mjs +301 -27
  95. package/engine/run-code-env.mjs +73 -19
  96. package/engine/schema.mjs +165 -243
  97. package/engine/sdk-strip.mjs +2 -2
  98. package/engine/service-names.d.mts +10 -0
  99. package/engine/service-names.mjs +10 -0
  100. package/engine/shared.d.mts +21 -0
  101. package/engine/shared.mjs +46 -13
  102. package/engine/skill-search.mjs +125 -53
  103. package/engine/source-kind.d.mts +9 -0
  104. package/engine/source-kind.mjs +16 -0
  105. package/engine/strategy-ids.mjs +5 -0
  106. package/engine/template-evaluation.d.mts +16 -0
  107. package/engine/template-evaluation.mjs +178 -0
  108. package/engine/template-frame.d.mts +16 -0
  109. package/engine/template-frame.mjs +69 -0
  110. package/engine/tool-config-engine.mjs +38 -34
  111. package/engine/tool-definition.d.mts +4 -0
  112. package/engine/tool-definition.mjs +5 -1
  113. package/engine/tool-git-bash.mjs +5 -1
  114. package/engine/tool-scope.mjs +22 -0
  115. package/engine/{dev-tool-search.mjs → tool-search.mjs} +63 -65
  116. package/engine/trigger.mjs +15 -270
  117. package/engine/vendor/yaml/dist/compose/resolve-flow-scalar.js +19 -18
  118. package/engine/vendor/yaml/dist/nodes/Alias.js +25 -23
  119. package/engine/vendor/yaml/package.json +1 -1
  120. package/lib/client.js +8312 -6724
  121. package/lib/client.js.map +1 -1
  122. package/lib/index.d.mts +16 -2120
  123. package/lib/index.mjs +26914 -5816
  124. package/modules/ponytail/module.yml +83 -203
  125. package/modules/ptc-mode/module.yml +45 -0
  126. package/modules/soul/assets/core-hard.md +6 -0
  127. package/modules/soul/assets/criteria.md +12 -0
  128. package/modules/soul/assets/echo.md +12 -0
  129. package/modules/soul/assets/layers.md +11 -0
  130. package/modules/soul/assets/ledger.md +9 -0
  131. package/modules/soul/assets/manifest.yml +20 -0
  132. package/modules/soul/assets/reference/00-layers.md +27 -0
  133. package/modules/soul/assets/reference/01-criteria.md +32 -0
  134. package/modules/soul/assets/reference/02-echo.md +158 -0
  135. package/modules/soul/assets/reference/03-vital.md +183 -0
  136. package/modules/soul/assets/reference/04-manifest.json +106 -0
  137. package/modules/soul/assets/reference/05-doctor.md +67 -0
  138. package/modules/soul/assets/reference/06-reference.md +189 -0
  139. package/modules/soul/assets/reference/07-bundle.json +431 -0
  140. package/modules/soul/assets/reference/08-/350/270/251/345/235/221/345/256/236/345/275/225.md +59 -0
  141. package/modules/soul/assets/reference/09-dev-workflow.md +109 -0
  142. package/modules/soul/assets/reference/10-echo-anatomy.md +100 -0
  143. package/modules/soul/assets/reference/11-self.md +80 -0
  144. package/modules/soul/assets/reference/12-ledger.md +159 -0
  145. package/modules/soul/assets/reference/README.md +10 -0
  146. package/modules/soul/assets/templates/agents-template.md +6 -0
  147. package/modules/soul/assets/vital.md +9 -0
  148. package/modules/soul/module.yml +311 -0
  149. package/modules/superpowers/module.yml +122 -0
  150. package/modules/tool-surface/module.yml +51 -47
  151. package/package.json +37 -40
  152. package/skills/dsh-module/SKILL.md +67 -147
  153. package/skills/dsh-module/reference.md +113 -86
  154. package/templates/10-pre-step.yml +2 -3
  155. package/templates/14-first-turn-anchor.yml +10 -14
  156. package/templates/15-guide-auto.yml +32 -28
  157. package/templates/16-custom-fallback.yml +33 -21
  158. package/templates/20-system-section.yml +1 -4
  159. package/templates/50-llm-stream.yml +4 -10
  160. package/templates/60-tool-pipeline.yml +12 -10
  161. package/templates/63-append-context.yml +4 -6
  162. package/templates/65-turn-stop.yml +2 -2
  163. package/templates/67-inbox-prepend.yml +2 -2
  164. package/templates/70-subagent-maintenance.yml +9 -10
  165. package/templates/75-subagent-end.yml +2 -2
  166. package/templates/80-tool-surface.yml +41 -57
  167. package/templates/tools/40-delegate-module-import.yml +22 -0
  168. package/templates/tools/50-delegate-module-rules.yml +14 -0
  169. package/templates/tools/60-delegate-module-variables.yml +17 -0
  170. package/templates/tools/70-delegate-module-rules-edit.yml +31 -0
  171. package/engine/actions/decision.mjs +0 -54
  172. package/engine/actions/guard.mjs +0 -80
  173. package/engine/actions/sdk-strip.mjs +0 -30
  174. package/engine/character-tools.mjs +0 -8
  175. package/engine/classify-task.mjs +0 -53
  176. package/engine/compositions/source/local/character-tools.yml +0 -5
  177. package/engine/compositions/source/local/dev-tool-search.yml +0 -11
  178. package/engine/compositions/source/local/session-var-tools.yml +0 -5
  179. package/engine/compositions/source/local/subagent-tool-policy.yml +0 -21
  180. package/engine/compositions/source/local/world-book-tools.yml +0 -5
  181. package/engine/condition.mjs +0 -96
  182. package/engine/declared-triggers.mjs +0 -93
  183. package/engine/session-var-tools.mjs +0 -8
  184. package/engine/session-vars.mjs +0 -54
  185. package/engine/st-macros.d.mts +0 -22
  186. package/engine/st-macros.mjs +0 -208
  187. package/engine/st-render.mjs +0 -109
  188. package/engine/st-world-book.d.mts +0 -10
  189. package/engine/subagent-tool-policy-core.mjs +0 -400
  190. package/engine/subagent-tool-policy.mjs +0 -352
  191. package/engine/trigger-spec.mjs +0 -173
  192. package/engine/world-book-tools.mjs +0 -8
  193. package/lib/migrate-rules.d.mts +0 -1
  194. package/lib/migrate-rules.mjs +0 -610
  195. package/lib/module-package-DqJZaE71.mjs +0 -2
  196. package/lib/module-package-q2gDRu9H.mjs +0 -2869
  197. package/lib/rule-builder-DMrJten6.mjs +0 -14767
  198. package/templates/61-deny-tools-presentation.yml +0 -27
  199. package/templates/62-guard-deny-tools.yml +0 -20
  200. package/templates/policies/legacy-defaults.yml +0 -34
  201. package/templates/tools/40-delegate-character.yml +0 -15
  202. package/templates/tools/50-delegate-worldbook.yml +0 -15
  203. package/templates/tools/60-delegate-session-var.yml +0 -15
  204. package/templates/tools/70-delegate-worldbook-upsert.yml +0 -36
@@ -0,0 +1,16 @@
1
+ import type { TemplateSnapshot } from './template-evaluation.mjs'
2
+ export interface TemplateVariableStore {
3
+ read(moduleId: string): { module: Record<string, string>; global: Record<string, string>; enabled?: boolean; revisions: { module: string; global: string | null; settings?: string } }
4
+ commit(moduleId: string, request: { scope: 'global' | 'module'; changes: Record<string, string>; expectedRevisions: { module?: string; global?: string | null; settings?: string } }): { publicationPending?: boolean }
5
+ publish(moduleId: string): void
6
+ }
7
+ export interface TemplateConfig {
8
+ sourceModuleId: string
9
+ ruleId?: string
10
+ id: string
11
+ texts: string[]
12
+ variables?: TemplateSnapshot['module']
13
+ variableStore?: TemplateVariableStore
14
+ }
15
+ export function templateExecutionKey(agent: unknown, signal?: AbortSignal): string | undefined
16
+ export function renderTemplate(config: TemplateConfig, context?: { agent?: unknown; signal?: AbortSignal }, options?: { variableStore?: TemplateVariableStore; enabled?: boolean; warn?: (message: string) => void }): string
@@ -0,0 +1,69 @@
1
+ /** 执行凭据独立于规则 mount;变量真值始终归显式宿主 adapter。 */
2
+ import { historyEvents } from './history.mjs'
3
+ import { evaluateTemplate } from './template-evaluation.mjs'
4
+ import { interpolateVariables, stripUnresolvedRefs } from './interpolate.mjs'
5
+
6
+ const frames = new WeakMap()
7
+ const MAX_CARDS = 4096
8
+
9
+ /** 官方循环先 assemble、再 pre-step、最后打开 step/start;两出口共享已提交边界。 */
10
+ export function templateExecutionKey(agent, signal) {
11
+ if (agent?.status !== 'running' || !signal || signal.aborted) return undefined
12
+ const events = historyEvents(agent.session)
13
+ const turn = events.findLast(event => event.type === 'turn/start' || event.type === 'turn/end')
14
+ if (turn?.type !== 'turn/start' || !Number.isSafeInteger(turn.seq)) return undefined
15
+ const step = events.findLast(event => event.type === 'step/start' || event.type === 'step/end')
16
+ if (step?.type === 'step/start' && step.seq > turn.seq) return undefined
17
+ return `${turn.seq}:${step?.seq ?? -1}`
18
+ }
19
+
20
+ export function renderTemplate(config, context = {}, { variableStore = config.variableStore, enabled = true, warn = () => {} } = {}) {
21
+ const text = config.texts.join('\n\n'), agent = context.agent, signal = context.signal
22
+ signal?.throwIfAborted()
23
+ if (!enabled) return stripUnresolvedRefs(interpolateVariables(text, {}, agent?.session, undefined, config.id)).text
24
+ const execution = templateExecutionKey(agent, signal)
25
+ let frame
26
+ if (execution !== undefined) {
27
+ frame = frames.get(agent)
28
+ if (frame?.execution !== execution) { frame = { execution, cards: new Map() }; frames.set(agent, frame) }
29
+ }
30
+ const identity = JSON.stringify([config.sourceModuleId, config.ruleId, config.id])
31
+ const previous = frame?.cards.get(identity)
32
+ const publish = entry => {
33
+ if (!entry.pending) return
34
+ try {
35
+ if (!variableStore?.publish) throw new Error('Variable publication adapter unavailable')
36
+ variableStore.publish(config.sourceModuleId)
37
+ entry.pending = false
38
+ } catch (error) { warn(`Template ${config.id}: variables saved; publication pending: ${String(error?.message ?? error)}`) }
39
+ }
40
+ if (previous) { publish(previous); return previous.text }
41
+ if (frame?.cards.size >= MAX_CARDS) throw new RangeError('Template execution card limit exceeded')
42
+ const samples = new Map()
43
+ for (let attempt = 0; attempt < 3; attempt++) {
44
+ const snapshot = variableStore?.read(config.sourceModuleId) ?? { module: config.variables ?? {}, global: {} }
45
+ if (snapshot.enabled === false) return stripUnresolvedRefs(interpolateVariables(text, {}, agent?.session, undefined, config.id)).text
46
+ const result = evaluateTemplate(text, { module: snapshot.module, global: snapshot.global, session: agent?.session,
47
+ sourceId: `${config.sourceModuleId ?? ''}:${config.id}`, readOnly: execution === undefined, samples })
48
+ const entry = { text: stripUnresolvedRefs(result.text).text, pending: false }
49
+ if (result.changes) {
50
+ if (!variableStore?.commit) throw new Error('Persistent template writes require a variable adapter')
51
+ signal?.throwIfAborted()
52
+ const domains = new Set([...result.reads, result.changes.scope])
53
+ const expectedRevisions = Object.fromEntries([...domains].map(domain => [domain, snapshot.revisions[domain]]))
54
+ if (snapshot.revisions.settings !== undefined) expectedRevisions.settings = snapshot.revisions.settings
55
+ try {
56
+ const saved = variableStore.commit(config.sourceModuleId, { scope: result.changes.scope, changes: result.changes.values, expectedRevisions })
57
+ entry.pending = saved.publicationPending === true
58
+ } catch (error) {
59
+ if (error?.persisted === true) entry.pending = true
60
+ else if (error?.status === 409 && attempt < 2) continue
61
+ else throw error
62
+ }
63
+ }
64
+ frame?.cards.set(identity, entry)
65
+ if (entry.pending) warn(`Template ${config.id}: variables saved; publication pending`)
66
+ return entry.text
67
+ }
68
+ throw new Error('Template variable commit conflict')
69
+ }
@@ -14,7 +14,7 @@
14
14
  * delegate — 委托已注册工具(经 ctx.tools.execute 的 nested dispatch 走
15
15
  * 完整官方管线:参数/输出校验、allow/deny/ask、pre/execute/post
16
16
  * waterfall、timeout、nested token 与 durable result)
17
- * fs — 文件操作(read/write/append/list/delete,路径限定 cwd 内)
17
+ * fs — 文件操作(read/write/append/list/delete,真实路径限定 cwd 内)
18
18
  * ask-user — approval 通道询问用户(结果文本化)
19
19
  *
20
20
  * 安全:行 config.requireApproval = [kind...] 时该执行器先过 approval 门
@@ -22,8 +22,8 @@
22
22
  * 工具注册经 disposer 契约(keepDisposer),插件卸载自动撤销。
23
23
  *
24
24
  * ── 能力提供者边界(T4)────────────────────────────────────────────────────
25
- * 本模块是**能力提供者**,不是触发器。分类判据(与组合源 tool-config-engine.yml、
26
- * subagent-tool-policy.yml 及 test/engine/provider-boundary.test.mjs 同一份措辞):
25
+ * 本模块是**能力提供者**,不是触发器。分类判据(与组合源 tool-config-engine.yml
26
+ * 同一份措辞):
27
27
  * - 会给模型提供可调用能力(注册工具 / 域 / 服务)的 → 能力提供者;
28
28
  * - 干预流程(改提示词、改装配、裁决、追加)的 → 声明式触发器。
29
29
  * 因此本模块**不接入** engine/trigger.mjs:它不订阅装配 waterfall,也没有
@@ -34,10 +34,10 @@
34
34
  * - 配置声明走 fields.mjs 的 `defineConfig`(未知键在挂载期 fail loud);
35
35
  * - 注册走 disposer 契约:单次注册用 shared.keepDisposer(register() 返回的正是
36
36
  * 它自己的 effect disposer);需要整组原子回滚或定向撤销句柄时才用 ctx.effect;
37
- * - 降级告警统一 `${name}: <what>; <fallback>` 一个前缀格式(见 provider-boundary 用例)。
37
+ * - 降级告警统一 `${name}: <what>; <fallback>` 一个前缀格式。
38
38
  *
39
- * 登记入口:`engineProvider`(数据导出),供边界守卫消费。提供者登记与声明式触发器
40
- * 声明(约定为 `engineTriggers`)**互斥**,同一模块不得同时导出两者。
39
+ * 能力提供:把 module.yml 的 customTools 段注册进官方 tools registry;模型可见工具名
40
+ * 在装配期决定,引擎侧无法静态枚举(本模块不导出 provider 登记)。
41
41
  *
42
42
  * 文件归属校正(PLAN T4):第三个指定提供者 str-replace-editor 由
43
43
  * engine/compositions/source/local/filesystem-editor.yml:20 引用**官方**包
@@ -46,11 +46,12 @@
46
46
  */
47
47
 
48
48
  import { readFileSync, readdirSync, writeFileSync, appendFileSync, rmSync, statSync } from 'node:fs'
49
- import { execFile } from 'node:child_process'
49
+ import { execFile, execFileSync } from 'node:child_process'
50
50
  import { isAbsolute, join, resolve, sep } from 'node:path'
51
51
  import { fileURLToPath, pathToFileURL } from 'node:url'
52
52
  import { parse as parseYaml } from './vendor/yaml/index.js'
53
53
  import { importHostPackage } from './host-package.mjs'
54
+ import { resolveRealWithin } from './path-safety.mjs'
54
55
  import { validateDefinition } from './tool-definition.mjs'
55
56
  import { defineConfig, passthrough } from './fields.mjs'
56
57
  import { keepDisposer } from './shared.mjs'
@@ -68,6 +69,24 @@ const ENV_ALLOWLIST = [
68
69
  'USERPROFILE', 'HOME', 'DSH_HOME', 'DSH_WORKSPACE', 'DSH_SESSION_ID',
69
70
  ]
70
71
 
72
+ /**
73
+ * shell 输出上限(stdout/stderr 各自):超限时 execFile 以
74
+ * ERR_CHILD_PROCESS_STDIO_MAXBUFFER 失败并保留截断输出,错误文本说明原因。
75
+ */
76
+ const SHELL_MAX_OUTPUT_BYTES = 1024 * 1024
77
+
78
+ /** 超时尽量结束整棵子进程树;Windows 用 taskkill /T,失败再退回 child.kill。 */
79
+ function killShellTree(child) {
80
+ if (child === undefined || child.pid === undefined) return
81
+ if (process.platform === 'win32') {
82
+ try {
83
+ execFileSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true })
84
+ return
85
+ } catch { /* taskkill 不可用时退回单进程 kill */ }
86
+ }
87
+ try { child.kill() } catch { /* 已退出 */ }
88
+ }
89
+
71
90
  /** 插值 {{args.x.y}} → 参数深取值(缺失保留字面)。 */
72
91
  function interpolateArgs(text, args) {
73
92
  return String(text ?? '').replace(/\{\{args\.([A-Za-z0-9_.]+)\}\}/g, (_whole, path) => {
@@ -163,16 +182,6 @@ async function approvalGate(ctx, exec, kind, reason) {
163
182
  return { ok: false, message: `approval for tool ${exec.name} was ${outcome}` }
164
183
  }
165
184
 
166
- /** cwd 内路径限定(防越界;resolve 后必须位于 cwd 之下)。 */
167
- function withinCwd(cwd, target) {
168
- const resolved = resolve(cwd, target)
169
- const root = resolve(cwd)
170
- if (resolved !== root && !resolved.startsWith(root + sep)) {
171
- throw new Error(`path ${JSON.stringify(target)} escapes the workspace`)
172
- }
173
- return resolved
174
- }
175
-
176
185
  /** delegate args 映射:完整引用({{args.x}})透传原始类型(数组/数字/布尔不字符串化);
177
186
  * 部分引用(前缀-{{args.x}})插值为字符串;非插值值原样。 */
178
187
  function mapDelegateArgs(execArgs, args) {
@@ -231,18 +240,25 @@ function createExecute(ctx, def, requireApproval) {
231
240
  const env = { ...Object.fromEntries(ENV_ALLOWLIST.filter((key) => process.env[key] !== undefined).map((key) => [key, process.env[key]])), ...(exec.env ?? {}) }
232
241
  const output = await new Promise((resolvePromise) => {
233
242
  let killTimer
234
- const child = execFile(shell, argv, { cwd, env, windowsHide: true }, (error, stdout, stderr) => {
243
+ let timedOut = false
244
+ const child = execFile(shell, argv, { cwd, env, windowsHide: true, maxBuffer: SHELL_MAX_OUTPUT_BYTES }, (error, stdout, stderr) => {
235
245
  clearTimeout(killTimer)
246
+ // 超时被杀与输出超限都不是普通退出码:error 文本必须能区分二者(不新增输出字段)。
247
+ const reason = error === null ? undefined
248
+ : timedOut ? `shell command timed out after ${def.timeoutMs} ms and was killed`
249
+ : error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER'
250
+ ? `shell command output exceeded ${SHELL_MAX_OUTPUT_BYTES} bytes (truncated)`
251
+ : String(error.message)
236
252
  resolvePromise({
237
253
  exitCode: error === null ? 0 : (typeof error.code === 'number' ? error.code : 1),
238
254
  stdout: String(stdout ?? ''),
239
255
  stderr: String(stderr ?? ''),
240
- ...(error === null ? {} : { error: String(error.message) }),
256
+ ...(reason === undefined ? {} : { error: reason }),
241
257
  })
242
258
  })
243
259
  const timeout = def.timeoutMs
244
260
  if (timeout !== undefined && Number.isSafeInteger(timeout) && timeout > 0) {
245
- killTimer = setTimeout(() => child.kill(), timeout)
261
+ killTimer = setTimeout(() => { timedOut = true; killShellTree(child) }, timeout)
246
262
  }
247
263
  })
248
264
  return output
@@ -292,7 +308,8 @@ function createExecute(ctx, def, requireApproval) {
292
308
  if (exec.kind === 'fs') {
293
309
  const cwd = run.agent?.session?.header?.cwd ?? process.cwd()
294
310
  try {
295
- const file = withinCwd(cwd, interpolateArgs(exec.path, args))
311
+ // 词法 resolve 挡不住 symlink/junction:统一用真实路径判定,不存在的目标按父目录解析。
312
+ const file = resolveRealWithin(resolve(cwd, interpolateArgs(exec.path, args)), [cwd])
296
313
  const action = interpolateArgs(exec.action, args)
297
314
  if (action === 'read') {
298
315
  const raw = readFileSync(file, 'utf8')
@@ -423,19 +440,6 @@ export const configContract = {
423
440
  },
424
441
  }
425
442
 
426
- /**
427
- * 能力提供者登记(T4 边界守卫的数据源)。
428
- * `provides.kind === 'dynamic'`:模型可见的工具名由 module.yml 的 customTools 段在
429
- * 装配期决定,引擎侧无法静态枚举——守卫只断言登记形状、
430
- * 注册通道,以及「提供者不导出触发器声明」这条互斥。
431
- */
432
- export const engineProvider = {
433
- kind: 'provider',
434
- moduleId: name,
435
- registers: 'tools',
436
- provides: { kind: 'dynamic', source: 'module.yml#customTools → tools' },
437
- }
438
-
439
443
  /** 插件入口:优先注册内联 tools,缺字段时兼容 configsDir。 */
440
444
  export function apply(ctx, config) {
441
445
  const source = configContract.parse(config, name)
@@ -0,0 +1,4 @@
1
+ /** `tool-definition.mjs` 的手写声明:只声明 `src/` 消费的导出。 */
2
+
3
+ /** 完整定义校验;非法定义抛 `TypeError`,合法定义无返回值。 */
4
+ export function validateDefinition(def: unknown): void
@@ -62,7 +62,7 @@ export function validateDefinition(def) {
62
62
  throw new TypeError(`tool ${def.id}: description is required`)
63
63
  }
64
64
  if (def.scope !== undefined) {
65
- throw new TypeError(`tool ${def.id}: customTools.scope is not supported (subagent tool policy is configured via subagentToolPolicy, not per-tool scope)`)
65
+ throw new TypeError(`tool ${def.id}: customTools.scope is not supported (a custom tool carries no per-tool scope)`)
66
66
  }
67
67
  if (def.parameters !== undefined) {
68
68
  validateJsonSchemaNode(def.parameters, `tool ${def.id}: parameters`)
@@ -88,6 +88,10 @@ export function validateDefinition(def) {
88
88
  if (exec.kind === 'delegate' && (typeof exec.tool !== 'string' || exec.tool.length === 0)) {
89
89
  throw new TypeError(`tool ${def.id}: execute.tool is required for delegate`)
90
90
  }
91
+ if (exec.kind === 'delegate' && ['character_import', 'character_list', 'character_apply', 'character_remove', 'character_delete',
92
+ 'world_book_list', 'world_book_upsert', 'world_book_delete', 'world_book_read_memory', 'session_var'].includes(exec.tool)) {
93
+ throw new TypeError(`tool ${def.id}: delegate ${exec.tool} 已退役;请重新导入或显式转换为模块工具`)
94
+ }
91
95
  if (exec.kind === 'fs' && (typeof exec.action !== 'string'
92
96
  || (!['read', 'write', 'append', 'list', 'delete'].includes(exec.action) && !exec.action.includes('{{args.')))) {
93
97
  throw new TypeError(`tool ${def.id}: execute.action must be read/write/append/list/delete (or {{args.*}} template) for fs`)
@@ -133,12 +133,16 @@ const commandSchema = {
133
133
  type: 'string',
134
134
  description: 'The bash command to execute (`bash -c` string domain).',
135
135
  },
136
+ description: {
137
+ type: 'string',
138
+ description: 'Clear, concise description of what this command does, 5-10 words (shown in the UI).',
139
+ },
136
140
  workdir: {
137
141
  type: 'string',
138
142
  description: 'Optional working directory; defaults to the session cwd. On Windows, Git Bash paths like /e/foo are accepted and converted to E:\\foo.',
139
143
  },
140
144
  },
141
- required: ['command'],
145
+ required: ['command', 'description'],
142
146
  additionalProperties: false,
143
147
  }
144
148
 
@@ -0,0 +1,22 @@
1
+ /**
2
+ * tool-scope — 工具面的作用域快照,避免各消费方把全局视图当成模型可见面。
3
+ *
4
+ * 返回两个字段,各自只服务一个调用方:
5
+ * - visible:带 agent 的 `tools.schemas(agent)`,包含预设/祖先/本层工具;模型可见面。
6
+ * - globalNames:不带 scope 的 `tools.schemas()` 名字集合;只供 `restrict` 过滤已知名。
7
+ *
8
+ * 全局原始 schema 与 visible-only 差集不外露;等真实诊断消费方出现再加字段。
9
+ */
10
+
11
+ const schemaList = (tools, scope) => {
12
+ if (typeof tools?.schemas !== 'function') return []
13
+ const schemas = tools.schemas(scope)
14
+ return Array.isArray(schemas) ? schemas : []
15
+ }
16
+
17
+ export function toolScopeSnapshot(tools, agent) {
18
+ const global = schemaList(tools)
19
+ const visible = agent === undefined ? global : schemaList(tools, agent)
20
+ const globalNames = new Set(global.map((schema) => schema?.name).filter((name) => typeof name === 'string'))
21
+ return { visible, globalNames }
22
+ }
@@ -6,8 +6,9 @@
6
6
  *
7
7
  * 两个动作合一:
8
8
  * - `query` 关键词搜索**完整装配目录**(只回名字 + 一行截断描述,不回 parameters);
9
- * - `toolNames` 按**精确名**解锁。解锁名写进本次 `tool/call` 的持久参数,
10
- * `assembly.target.tools.allowFrom` 从那里读回 → 下次请求起生效,且跨压缩保留。
9
+ * - `toolNames` 按**精确名**解锁。解锁名写进本次调用的持久参数(顶层 `tool/call`;
10
+ * PTC 嵌套为 `tool/ptc-dispatch`),发现消费方的 allowExtra 从那里读回 → 下次请求起
11
+ * 生效,且跨压缩保留。
11
12
  *
12
13
  * **能力摘要来自运行时截获,不手工维护**(2026-10-05 用户指出):空查询时按前缀把
13
14
  * `ctx.tools.schemas()` 的可见目录折叠成分组摘要(`mcp__github__* ×46`,实测 433 字符
@@ -15,17 +16,22 @@
15
16
  * 名字写错还会让模型照着错名字解锁(那份实现甚至需要一条专门测试来防错名)。
16
17
  * 官方注册表只投影 name/description(无分组字段),所以分组只能按前缀在本地折叠。
17
18
  *
18
- * 与原 `dev-tool-search.mjs` 的两处修正照搬:
19
+ * 与原 `tool-search.mjs` 的两处修正照搬:
19
20
  * 1. 匹配是**打分**而非 AND 过滤——长自然语言查询("file edit write replace")在
20
21
  * AND 口径下命中零个,等于白搜。现在精确名优先,其次按命中 token 数排序。
21
22
  * 2. 描述里显式教解锁路径("搜索为空 ≠ 工具不存在")——实测模型只会搜、不会传
22
23
  * toolNames,于是收窄后永远拿不到工具。
23
24
  */
24
- export const name = 'dev-tool-search'
25
+ export const name = 'tool-search'
25
26
 
26
27
  /** 工具注册表必须先于本工具存在(同 skill-search 的 inject 纪律)。 */
27
28
  export const inject = ['tools']
28
29
 
30
+ import { toJsonSchema } from './shared.mjs'
31
+ import { renderRows, rankRows, scoreRows, queryTokens } from './discovery.mjs'
32
+ import { toolScopeSnapshot } from './tool-scope.mjs'
33
+ import { DISCOVERY_SERVICE } from './service-names.mjs'
34
+
29
35
  /** 单次搜索结果上限:再多就该让模型收窄关键词。 */
30
36
  export const MAX_RESULTS = 25
31
37
 
@@ -36,31 +42,17 @@ const DESC_LIMIT = 90
36
42
  const MAX_GROUPS = 24
37
43
 
38
44
  /** 常驻核心集:由工具面收窄模板声明,这里只在描述里如实告知模型。 */
39
- const RESIDENT = ['pwsh', 'read', 'write', 'edit', 'glob', 'grep', 'todo_write', 'skill_search', 'skill_load']
40
-
41
- /** 零依赖的最小 JSON schema 编译器(与 skill-search 同构)。 */
42
- function toJsonSchema(spec) {
43
- const properties = {}
44
- const required = []
45
- for (const [key, meta] of Object.entries(spec || {})) {
46
- const prop = { type: meta.type }
47
- if (meta.description) prop.description = meta.description
48
- if (meta.items) prop.items = meta.items
49
- properties[key] = prop
50
- if (meta.required) required.push(key)
51
- }
52
- return { type: 'object', properties, required, additionalProperties: false }
53
- }
45
+ const RESIDENT = ['bash', 'pwsh', 'read', 'write', 'edit', 'glob', 'grep', 'todo_write', 'skill_search', 'skill_load']
46
+
47
+ /** 参数 schema 由 `shared.mjs` 的统一实现生成(与 `skill-search` 同一份)。 */
54
48
 
55
49
  /**
56
- * 能力索引:常驻核心集(pwsh / read / write / edit / glob / grep / todo_write /
50
+ * 能力索引:常驻核心集(bash / pwsh / read / write / edit / glob / grep / todo_write /
57
51
  * skill_search / skill_load)覆盖不到的能力。名字全部取自实测工具面。
58
52
  */
59
53
 
60
- /** 把查询切成小写 token(Unicode 字母数字与下划线连字符,与 skill-search 同口径)。 */
61
- export function queryTokens(query) {
62
- return (typeof query === 'string' ? query : '').toLowerCase().split(/[^\p{L}\p{N}_-]+/u).filter(Boolean)
63
- }
54
+ /** 查询分词由发现引擎统一实现(与技能面同一口径)。 */
55
+ export { queryTokens }
64
56
 
65
57
  /**
66
58
  * 一个工具名的分组键:MCP 工具折到服务级(`mcp__github__*`),其余折到首个下划线段
@@ -102,17 +94,10 @@ export function summarizeCatalog(schemas) {
102
94
  * `wanted` 为空时返回空数组——调用方据此区分「没给查询」与「没搜到」。
103
95
  */
104
96
  export function scoreTools(schemas, wanted) {
105
- if (!Array.isArray(schemas) || wanted.length === 0) return []
106
- return schemas
107
- .map((schema) => {
108
- const name = typeof schema?.name === 'string' ? schema.name : ''
109
- const haystack = `${name} ${schema?.description ?? ''}`.toLowerCase()
110
- let score = 0
111
- for (const token of wanted) if (haystack.includes(token)) score += 1
112
- return { schema, name, score, exact: wanted.includes(name.toLowerCase()) }
113
- })
114
- .filter((entry) => entry.name.length > 0 && (entry.exact || entry.score >= 1))
115
- .sort((a, b) => (b.exact - a.exact) || (b.score - a.score) || a.name.localeCompare(b.name))
97
+ return rankRows(scoreRows(schemas, wanted, {
98
+ nameOf: (schema) => schema?.name,
99
+ haystackOf: (schema) => `${schema?.name ?? ''} ${schema?.description ?? ''}`,
100
+ })).map((row) => ({ ...row, schema: row.item }))
116
101
  }
117
102
 
118
103
  /** 解锁名单归一:只留非空字符串,去重保序。 */
@@ -129,46 +114,38 @@ export function unlockNames(value) {
129
114
 
130
115
  /** 把目录搜索结果渲染成给模型的文本(带截断提示与解锁引导)。 */
131
116
  export function renderMatches(scored, query) {
132
- const lines = []
133
- const matches = scored.slice(0, MAX_RESULTS)
134
- if (matches.length === 0) {
135
- lines.push(
136
- `No tools match "${query}". An empty result only means no tool matched your keywords — if you need a specific tool, unlock it directly by exact name, e.g. dev_tool_search({"toolNames":["web_search"]}).`,
137
- )
138
- return lines
139
- }
140
- lines.push(`Matching tools (${matches.length}${scored.length > MAX_RESULTS ? ` of ${scored.length}` : ''}):`)
141
- for (const { schema, name, exact } of matches) {
142
- const desc = String(schema?.description ?? '').split('\n')[0].slice(0, DESC_LIMIT)
143
- lines.push(`- ${name}${exact ? ' (exact)' : ''}: ${desc}`)
144
- }
145
- if (scored.length > MAX_RESULTS) {
146
- lines.push(`(truncated at ${MAX_RESULTS} — add tokens to narrow the query)`)
147
- }
148
- lines.push('Unlock with dev_tool_search({"toolNames": ["<exact name>"]}).')
149
- return lines
117
+ return renderRows(scored, {
118
+ query,
119
+ limit: MAX_RESULTS,
120
+ noun: 'tools',
121
+ emptyText: `No tools match "${query}". An empty result only means no tool matched your keywords — if you need a specific tool, unlock it directly by exact name, e.g. tool_search({"toolNames":["web_search"]}).`,
122
+ lineOf: ({ schema, name, exact }) => `- ${name}${exact ? ' (exact)' : ''}: ${String(schema?.description ?? '').split('\n')[0].slice(0, DESC_LIMIT)}`,
123
+ footer: 'Unlock with tool_search({"toolNames": ["<exact name>"]}).',
124
+ })
150
125
  }
151
126
 
152
- /** 注册模型可见的 `dev_tool_search`。 */
153
- export function apply(ctx) {
154
- ctx.tools.register({
155
- name: 'dev_tool_search',
127
+ /** 注册模型可见的 `tool_search`。 */
128
+ export function createToolSearchDefinition(ctx, options = {}) {
129
+ const resident = options.resident ?? RESIDENT
130
+ const unlockKey = options.unlockKey ?? 'toolNames'
131
+ return {
132
+ name: options.tool ?? 'tool_search',
156
133
  description: [
157
134
  'Discover and unlock tools that are NOT currently available.',
158
135
  '',
159
- `This session starts with a minimal resident set: ${RESIDENT.join(', ')}. Everything else is unlocked on demand through this tool.`,
136
+ `This session starts with a minimal resident set: ${resident.join(', ')}. Everything else is unlocked on demand through this tool.`,
160
137
  '',
161
138
  'Usage:',
162
139
  '- `query` — search the catalog by keyword (matches tool names AND descriptions, 1-2 short words work best).',
163
140
  '- call with NO arguments — list the catalog as group counts (`<prefix>_*(count) ...`), so you can see what exists before searching.',
164
- '- `toolNames` — unlock exact names. Unlocked tools appear from the NEXT request on and stay unlocked for the session.',
141
+ `- \`${unlockKey}\` — unlock exact names. Unlocked tools appear from the NEXT request on and stay unlocked for the session.`,
165
142
  '',
166
- 'Example: dev_tool_search({"query":"pull request","toolNames":["mcp__github__create_pull_request"]}) — search AND unlock in one call.',
167
- 'IMPORTANT: an empty search result does NOT mean the tool does not exist — it only means no tool matched your keywords. Call with no arguments to see the catalog groups, then unlock by exact name.',
143
+ `Example: ${options.tool ?? 'tool_search'}({"query":"pull request","${unlockKey}":["mcp__github__create_pull_request"]}) — search AND unlock in one call.`,
144
+ 'IMPORTANT: if the task needs a capability outside that resident set — web search, subagents, task boards, MCP servers, anything not listed above — search for it here and unlock it. Do NOT make do with a resident tool when the tool you actually need is merely locked; an empty search result does NOT mean the tool does not exist, it only means no tool matched your keywords. Call with no arguments to see the catalog groups, then unlock by exact name.',
168
145
  ].join('\n'),
169
146
  parameters: toJsonSchema({
170
147
  query: { type: 'string', required: false, description: 'search keywords (e.g. "web", "subagent", "github")' },
171
- toolNames: {
148
+ [unlockKey]: {
172
149
  type: 'array',
173
150
  required: false,
174
151
  items: { type: 'string' },
@@ -181,7 +158,7 @@ export function apply(ctx) {
181
158
  },
182
159
  async execute(args, exec) {
183
160
  const query = typeof args?.query === 'string' ? args.query.trim() : ''
184
- const unlock = unlockNames(args?.toolNames)
161
+ const unlock = unlockNames(args?.[unlockKey])
185
162
  const lines = []
186
163
  // 解锁动作本身不需要任何运行时服务:引擎的 allowFrom 只读本工具的持久调用参数,
187
164
  // 所以即便目录不可用(无 tools 服务/无 agent),解锁也必须照样回报成功。
@@ -191,7 +168,7 @@ export function apply(ctx) {
191
168
  try {
192
169
  // 执行中的 agent 就是观察作用域:preset 工具注册在 agent scope 层,
193
170
  // 不带 scope 的 schemas() 只看全局层,会漏掉它们。
194
- schemas = ctx.tools.schemas(exec?.agent)
171
+ schemas = toolScopeSnapshot(ctx.tools, exec?.agent).visible
195
172
  } catch (error) {
196
173
  return { text: [...lines, `catalog search unavailable: ${String(error?.message ?? error)}`].join('\n') }
197
174
  }
@@ -209,5 +186,26 @@ export function apply(ctx) {
209
186
  lines.push(...renderMatches(scoreTools(schemas, queryTokens(query)), query))
210
187
  return { text: lines.join('\n') }
211
188
  },
212
- })
189
+ }
213
190
  }
191
+
192
+ /** 本引擎向发现注册表提供的元数据:工具名与「解锁名单从哪个参数回收」。 */
193
+ export const discoveryAdapter = Object.freeze({
194
+ kind: 'tools',
195
+ tool: 'tool_search',
196
+ unlock: Object.freeze({ tool: 'tool_search', key: 'toolNames' }),
197
+ })
198
+
199
+ /**
200
+ * 组合源入口:注册工具(行语义不变),并向发现注册表登记元数据。
201
+ * 注册表由宿主 root provide;引擎单独使用时它不存在,跳过登记即可。
202
+ */
203
+ export function apply(ctx, _config, options = {}) {
204
+ const registered = ctx.tools.register(createToolSearchDefinition(ctx))
205
+ const registry = typeof ctx.get === 'function' ? ctx.get(DISCOVERY_SERVICE) : undefined
206
+ const disposeAdapter = registry === undefined ? undefined : registry.register(options.agent, 'tools', discoveryAdapter)
207
+ return () => {
208
+ if (typeof disposeAdapter === 'function') disposeAdapter()
209
+ if (typeof registered === 'function') registered()
210
+ }
211
+ }