dsh-plugin-prompt-tool 0.7.2 → 0.8.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 (169) hide show
  1. package/README.md +185 -46
  2. package/engine/THIRD_PARTY_LICENSES +25 -0
  3. package/engine/actions.mjs +931 -0
  4. package/engine/anchor-match.mjs +6 -3
  5. package/engine/character-tools.mjs +11 -0
  6. package/engine/classify-task.mjs +18 -0
  7. package/engine/compaction-epoch.mjs +41 -20
  8. package/engine/compositions/library/agent-instructions.yml +9 -0
  9. package/engine/compositions/library/command-goal.yml +2 -6
  10. package/engine/compositions/library/compaction.yml +2 -32
  11. package/engine/compositions/library/delegation-ptc.yml +58 -0
  12. package/engine/compositions/library/delegation.yml +5 -38
  13. package/engine/compositions/library/persistent-shell.yml +4 -13
  14. package/engine/compositions/library/planning.yml +8 -23
  15. package/engine/compositions/library/skill-filesystem-cordis.yml +10 -0
  16. package/engine/compositions/library/skill-filesystem.yml +2 -8
  17. package/engine/compositions/library/tool-ask-user.yml +2 -3
  18. package/engine/compositions/library/tool-bash.yml +4 -12
  19. package/engine/compositions/library/tool-cordis.yml +7 -0
  20. package/engine/compositions/library/tool-fs-search.yml +2 -11
  21. package/engine/compositions/library/tool-fs.yml +2 -5
  22. package/engine/compositions/library/tool-goal.yml +2 -6
  23. package/engine/compositions/library/tool-jobs.yml +2 -18
  24. package/engine/compositions/library/tool-plugin-manager-disabled.yml +8 -0
  25. package/engine/compositions/library/tool-plugin-manager.yml +8 -0
  26. package/engine/compositions/library/tool-present.yml +7 -0
  27. package/engine/compositions/library/tool-presentation.yml +9 -0
  28. package/engine/compositions/library/tool-pwsh.yml +2 -6
  29. package/engine/compositions/library/tool-skill.yml +7 -0
  30. package/engine/compositions/library/tool-todo.yml +2 -4
  31. package/engine/compositions/library/tool-web.yml +2 -1
  32. package/engine/compositions/source/local/character-tools.yml +5 -0
  33. package/engine/compositions/source/local/declared-triggers.yml +12 -0
  34. package/engine/compositions/source/local/filesystem-editor.yml +30 -0
  35. package/engine/compositions/source/local/instruction-hint.yml +15 -0
  36. package/engine/compositions/{library/official-persistent-shell.yml → source/local/persistent-shell-posix.yml} +10 -8
  37. package/engine/compositions/source/local/prompt-config-engine.yml +3 -9
  38. package/engine/compositions/source/local/run-code-env.yml +4 -7
  39. package/engine/compositions/source/local/session-var-tools.yml +5 -0
  40. package/engine/compositions/source/local/skill-search.yml +3 -9
  41. package/engine/compositions/source/local/subagent-tool-policy.yml +21 -0
  42. package/engine/compositions/source/local/tool-bash-disabled.yml +9 -0
  43. package/engine/compositions/source/local/tool-config-engine.yml +8 -0
  44. package/engine/compositions/source/local/tool-git-bash.yml +15 -0
  45. package/engine/compositions/source/local/world-book-tools.yml +5 -0
  46. package/engine/condition.mjs +83 -0
  47. package/engine/declared-triggers.mjs +93 -0
  48. package/engine/executor.mjs +364 -185
  49. package/engine/fields.mjs +219 -0
  50. package/engine/fillers.mjs +12 -141
  51. package/engine/host-package.mjs +51 -0
  52. package/engine/instruction-hint.mjs +285 -0
  53. package/engine/interpolate.mjs +157 -29
  54. package/engine/layers.mjs +495 -75
  55. package/engine/predicates.mjs +753 -0
  56. package/engine/prompt-config-engine.mjs +63 -8
  57. package/engine/run-code-env.mjs +48 -31
  58. package/engine/schema.mjs +444 -103
  59. package/engine/sdk-strip.mjs +365 -0
  60. package/engine/session-var-tools.mjs +11 -0
  61. package/engine/session-vars.mjs +8 -6
  62. package/engine/shared.mjs +137 -0
  63. package/engine/skill-search.mjs +34 -21
  64. package/engine/st-macros.d.mts +22 -0
  65. package/engine/st-macros.mjs +208 -0
  66. package/engine/st-render.mjs +105 -0
  67. package/engine/st-world-book.d.mts +10 -0
  68. package/engine/st-world-book.mjs +259 -0
  69. package/engine/strategies.mjs +31 -20
  70. package/engine/subagent-tool-policy-core.mjs +400 -0
  71. package/engine/subagent-tool-policy.mjs +327 -0
  72. package/engine/tool-config-engine.mjs +207 -145
  73. package/engine/tool-definition.mjs +143 -0
  74. package/engine/{custom-bash.mjs → tool-git-bash.mjs} +38 -13
  75. package/engine/trigger-editor-meta.mjs +48 -0
  76. package/engine/trigger-spec.mjs +257 -0
  77. package/engine/trigger.mjs +304 -0
  78. package/engine/world-book-tools.mjs +11 -0
  79. package/lib/client.js +16495 -5456
  80. package/lib/client.js.map +1 -1
  81. package/lib/index.d.mts +628 -382
  82. package/lib/index.mjs +5391 -2942
  83. package/lib/paths-CY5psBqE.d.mts +1233 -0
  84. package/lib/preset-package-BnJqrU4R.mjs +16792 -0
  85. package/lib/preset-transfer.d.mts +2 -0
  86. package/lib/preset-transfer.mjs +2 -0
  87. package/package.json +91 -49
  88. package/preset/pt-cordis/preset.yml +43 -0
  89. package/preset/pt-cordis/skills/cordis-composition-reference/SKILL.md +29 -0
  90. package/preset/pt-cordis/skills/cordis-composition-reference/references/packages.md +501 -0
  91. package/preset/pt-cordis/skills/cordis-plugin-development/SKILL.md +38 -0
  92. package/preset/pt-cordis/skills/cordis-plugin-development/references/host-plugin.md +60 -0
  93. package/preset/pt-cordis/skills/cordis-plugin-development/references/mcp-bundle.md +5 -0
  94. package/preset/pt-cordis/skills/cordis-plugin-development/references/ui-plugin.md +13 -0
  95. package/preset/pt-cordis/skills/cordis-plugin-development/references/verification.md +5 -0
  96. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/client.js +22 -0
  97. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/cordis.patch.yml +3 -0
  98. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/index.js +2 -0
  99. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/package.json +15 -0
  100. package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/cordis.patch.yml +8 -0
  101. package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/package.json +7 -0
  102. package/preset/pt-cordis/skills/editing-cordis-compositions/SKILL.md +84 -0
  103. package/preset/pt-custom/preset.yml +15 -0
  104. package/preset/pt-minimal/preset.yml +27 -0
  105. package/preset/pt-ptc/preset.yml +45 -0
  106. package/preset/pt-standard/preset.yml +43 -0
  107. package/templates/14-first-turn-anchor.yml +4 -4
  108. package/templates/15-guide-auto.yml +3 -3
  109. package/templates/16-custom-fallback.yml +1 -0
  110. package/templates/18-placeholder-env-facts.yml +3 -3
  111. package/templates/20-system-section.yml +3 -2
  112. package/templates/65-turn-stop.yml +18 -0
  113. package/templates/66-subagent-start.yml +14 -0
  114. package/templates/75-subagent-end.yml +15 -0
  115. package/templates/tools/10-shell-command.yml +1 -1
  116. package/templates/tools/40-delegate-character.yml +1 -1
  117. package/templates/tools/50-delegate-worldbook.yml +1 -1
  118. package/templates/tools/60-delegate-session-var.yml +1 -1
  119. package/templates/tools/70-delegate-worldbook-upsert.yml +1 -1
  120. package/engine/anchor-turn.mjs +0 -68
  121. package/engine/code-presentation.mjs +0 -103
  122. package/engine/compositions/library/anchor-turn.yml +0 -10
  123. package/engine/compositions/library/bootstrap-filesystem.yml +0 -19
  124. package/engine/compositions/library/code-presentation.yml +0 -8
  125. package/engine/compositions/library/context-gate.yml +0 -39
  126. package/engine/compositions/library/cot-drip.yml +0 -11
  127. package/engine/compositions/library/custom-bash.yml +0 -14
  128. package/engine/compositions/library/deliberation-gate.yml +0 -11
  129. package/engine/compositions/library/official-agent-instructions.yml +0 -17
  130. package/engine/compositions/library/official-skill-filesystem-cordis.yml +0 -10
  131. package/engine/compositions/library/official-tool-bash.yml +0 -6
  132. package/engine/compositions/library/official-tool-cordis.yml +0 -13
  133. package/engine/compositions/library/official-tool-presentation.yml +0 -7
  134. package/engine/compositions/library/official-tool-skill.yml +0 -14
  135. package/engine/compositions/library/persona.yml +0 -13
  136. package/engine/compositions/library/prompt-config-engine.yml +0 -16
  137. package/engine/compositions/library/run-code-env.yml +0 -15
  138. package/engine/compositions/library/skill-search.yml +0 -14
  139. package/engine/compositions/library/str-replace-editor.yml +0 -10
  140. package/engine/compositions/library/tool-bootstrap.yml +0 -14
  141. package/engine/compositions/library/tool-config-engine.yml +0 -9
  142. package/engine/compositions/library/tool-filter.yml +0 -8
  143. package/engine/compositions/source/local/code-presentation.yml +0 -8
  144. package/engine/compositions/source/local/context-gate.yml +0 -36
  145. package/engine/compositions/source/local/custom-bash.yml +0 -11
  146. package/engine/compositions/source/local/str-replace-editor.yml +0 -10
  147. package/engine/compositions/source/local/tool-bootstrap.yml +0 -11
  148. package/engine/compositions/source/local/tool-filter.yml +0 -8
  149. package/engine/context-gate.mjs +0 -305
  150. package/engine/cot-drip.mjs +0 -137
  151. package/engine/deliberation-gate.mjs +0 -127
  152. package/engine/tool-bootstrap.mjs +0 -476
  153. package/engine/tool-filter.mjs +0 -80
  154. package/lib/preset-core.d.mts +0 -10
  155. package/lib/preset-core.mjs +0 -77
  156. package/lib/prompt-configs-BnQKRHVb.mjs +0 -884
  157. package/lib/prompt-configs-CN0sJTUL.d.mts +0 -193
  158. package/preset/anchored/preset.yml +0 -296
  159. package/preset/creative/preset.yml +0 -67
  160. package/preset/creative/skills/cordis-plugin-development/SKILL.md +0 -420
  161. package/preset/creative/skills/editing-cordis-compositions/SKILL.md +0 -165
  162. package/preset/custom/preset.yml +0 -12
  163. package/preset/liangshen/preset.yml +0 -75
  164. package/preset/minimal/preset.yml +0 -45
  165. package/preset/ptc/preset.yml +0 -58
  166. package/preset/standard/preset.yml +0 -57
  167. package/skills/manifest.json +0 -7
  168. package/skills/sandboxmod/SKILL.md +0 -49
  169. package/skills/web ui/SKILL.md +0 -42
@@ -3,20 +3,46 @@
3
3
  *
4
4
  * 工具定义从 configsDir 目录加载(writePreset 渲染 custom-tools/*.yml,
5
5
  * 源 = preset.yml 顶层 customTools 段),每份一个工具定义:
6
- * id / name / description / scope / timeoutMs / parameters(dsh-tools schema
7
- * DSL 的 JSON 形态)/ output.schema / execute.{kind, ...}
6
+ * id / name / description / timeoutMs / parameters(标准 JSON Schema
7
+ * object,writePreset 已用官方 @deepseek-ai/dsh-tools 的
8
+ * parameterSchemaSpecToJsonSchema / valueSchemaSpecToJsonSchema 把
9
+ * preset.yml 的 dsh-tools DSL 转换)/ output.schema / execute.{kind, ...}
8
10
  *
9
11
  * 执行器(kind):
10
12
  * shell — 命令执行(execFile 无 shell 解析,env 白名单,cwd=会话工作区)
11
13
  * http — fetch 请求(method/url/headers/body,{{args.x}} 插值)
12
- * delegate — 委托已注册工具(ctx.tools.get + 透传参数,高级用法:
13
- * 目标工具 execute 直接调用,不经 registry 校验管线)
14
+ * delegate — 委托已注册工具(经 ctx.tools.execute 的 nested dispatch 走
15
+ * 完整官方管线:参数/输出校验、allow/deny/ask、pre/execute/post
16
+ * waterfall、timeout、nested token 与 durable result)
14
17
  * fs — 文件操作(read/write/append/list/delete,路径限定 cwd 内)
15
18
  * ask-user — approval 通道询问用户(结果文本化)
16
19
  *
17
20
  * 安全:行 config.requireApproval = [kind...] 时该执行器先过 approval 门
18
21
  * (无 approval 服务则拒绝);单条定义非法 warnOnce 跳过(不挂整行)。
19
- * 工具注册经 ctx.effect,插件卸载自动撤销。
22
+ * 工具注册经 disposer 契约(keepDisposer),插件卸载自动撤销。
23
+ *
24
+ * ── 能力提供者边界(T4)────────────────────────────────────────────────────
25
+ * 本模块是**能力提供者**,不是触发器。分类判据(与组合源 tool-config-engine.yml、
26
+ * subagent-tool-policy.yml 及 test/engine/provider-boundary.test.mjs 同一份措辞):
27
+ * - 会给模型提供可调用能力(注册工具 / 域 / 服务)的 → 能力提供者;
28
+ * - 干预流程(改提示词、改装配、裁决、追加)的 → 声明式触发器。
29
+ * 因此本模块**不接入** engine/trigger.mjs:它不订阅装配 waterfall,也没有
30
+ * when/do 声明,只在装配期把 preset.yml 的 customTools 段物化出的工具注册进官方
31
+ * registry。强行并进触发器会让引擎同时承担「提供能力」与「干预流程」两种职责。
32
+ *
33
+ * 样板收敛:
34
+ * - 配置声明走 fields.mjs 的 `defineConfig`(未知键在挂载期 fail loud);
35
+ * - 注册走 disposer 契约:单次注册用 shared.keepDisposer(register() 返回的正是
36
+ * 它自己的 effect disposer);需要整组原子回滚或定向撤销句柄时才用 ctx.effect;
37
+ * - 降级告警统一 `${name}: <what>; <fallback>` 一个前缀格式(见 provider-boundary 用例)。
38
+ *
39
+ * 登记入口:`engineProvider`(数据导出),供边界守卫消费。提供者登记与声明式触发器
40
+ * 声明(约定为 `engineTriggers`)**互斥**,同一模块不得同时导出两者。
41
+ *
42
+ * 文件归属校正(PLAN T4):第三个指定提供者 str-replace-editor 由
43
+ * engine/compositions/source/local/filesystem-editor.yml:20 引用**官方**包
44
+ * `@deepseek-ai/dsh-tool-str-replace-editor`,本仓库没有对应 `.mjs`;
45
+ * engine/tool-git-bash.mjs 提供的是 `bash` 工具,**不是**第三个指定提供者。
20
46
  */
21
47
 
22
48
  import { readFileSync, readdirSync, writeFileSync, appendFileSync, rmSync, statSync } from 'node:fs'
@@ -24,18 +50,18 @@ import { execFile } from 'node:child_process'
24
50
  import { isAbsolute, join, resolve, sep } from 'node:path'
25
51
  import { fileURLToPath, pathToFileURL } from 'node:url'
26
52
  import { parse as parseYaml } from './vendor/yaml/index.js'
53
+ import { importHostPackage } from './host-package.mjs'
54
+ import { validateDefinition } from './tool-definition.mjs'
55
+ import { defineConfig, passthrough } from './fields.mjs'
56
+ import { keepDisposer } from './shared.mjs'
57
+
58
+ const { ToolArgsError } = await importHostPackage('@deepseek-ai/dsh-tools')
27
59
 
28
60
  /** Cordis 插件名,供 loader 诊断使用。 */
29
61
  export const name = 'tool-config-engine'
30
62
 
31
63
  export const inject = ['tools']
32
64
 
33
- /** 工具名规范(模型可见,schemas() 白名单要求)。 */
34
- const TOOL_NAME_RE = /^[a-z][a-z0-9_]*$/
35
-
36
- /** 合法参数节点类型(dsh-tools ValueSchemaSpec 的 type 枚举)。 */
37
- const SCHEMA_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'null', 'array', 'object', 'json', 'oneOf'])
38
-
39
65
  /** shell 执行器的 env 白名单(无凭据形态;与 run-code-env 同基调)。 */
40
66
  const ENV_ALLOWLIST = [
41
67
  'PATH', 'PATHEXT', 'SystemRoot', 'COMSPEC', 'TEMP', 'TMP',
@@ -54,91 +80,67 @@ function interpolateArgs(text, args) {
54
80
  })
55
81
  }
56
82
 
57
- /** 递归校验 schema 节点(宽松:type 枚举 + object 必须声明 additionalProperties + required 必须 true)。 */
58
- function validateSchemaNode(node, path, seen) {
59
- if (node === null || typeof node !== 'object' || Array.isArray(node)) {
60
- throw new TypeError(`${path} must be an object`)
61
- }
62
- if (seen.has(node)) throw new TypeError(`${path} is circular`)
63
- seen.add(node)
64
- try {
65
- if (typeof node.type !== 'string' || !SCHEMA_TYPES.has(node.type)) {
66
- throw new TypeError(`${path}.type must be one of ${[...SCHEMA_TYPES].join('/')}`)
67
- }
68
- if (node.type === 'object' && typeof node.additionalProperties !== 'boolean') {
69
- throw new TypeError(`${path}.additionalProperties must be explicitly true or false`)
70
- }
71
- if (node.required !== undefined && node.required !== true) {
72
- throw new TypeError(`${path}.required must be true when present`)
83
+ /**
84
+ * 按 JSON Schema 子集校验参数值(引擎侧输入校验;官方 defineTool 的 validateArgs
85
+ * 同样校验——registry 对裸 register 工具不校验参数,引擎自持保证"非法参数在执行前
86
+ * 失败且实现不运行")。object required/additionalProperties:false、array items、
87
+ * scalar type/enum/const、oneOf 覆盖;annotation-only(无 type)视为任意 JSON。
88
+ */
89
+ function validateJsonSchemaValue(schema, value) {
90
+ const violations = []
91
+ const walk = (node, value, path) => {
92
+ if (node === null || typeof node !== 'object' || Array.isArray(node)) {
93
+ violations.push(path + ': invalid schema node')
94
+ return
73
95
  }
74
- if (node.type === 'object' && node.properties !== undefined) {
75
- for (const [key, child] of Object.entries(node.properties)) {
76
- validateSchemaNode(child, `${path}.properties.${key}`, seen)
96
+ if (Array.isArray(node.oneOf)) {
97
+ for (const branch of node.oneOf) {
98
+ const before = violations.length
99
+ walk(branch, value, path)
100
+ if (violations.length === before) return
101
+ violations.length = before
77
102
  }
103
+ violations.push(path + ': does not match any oneOf branch')
104
+ return
78
105
  }
79
- if (node.type === 'array' && node.items !== undefined) {
80
- validateSchemaNode(node.items, `${path}.items`, seen)
106
+ const type = node.type
107
+ if (type === undefined) return // annotation-only = unconstrained
108
+ const actualType = Array.isArray(value) ? 'array' : value === null ? 'null' : typeof value
109
+ const matchesType = actualType === type
110
+ || (type === 'integer' && actualType === 'number' && Number.isInteger(value))
111
+ if (!matchesType) {
112
+ violations.push(path + ': expected ' + type + ', got ' + actualType)
113
+ return
81
114
  }
82
- if (node.type === 'oneOf' && Array.isArray(node.oneOf)) {
83
- if (node.oneOf.length < 2) throw new TypeError(`${path}.oneOf needs at least two branches`)
84
- for (const [index, branch] of node.oneOf.entries()) {
85
- validateSchemaNode(branch, `${path}.oneOf[${index}]`, seen)
86
- }
115
+ if (node.enum !== undefined && !node.enum.some((item) => Object.is(item, value))) {
116
+ violations.push(path + ': value not in enum')
87
117
  }
88
- } finally {
89
- seen.delete(node)
90
- }
91
- }
92
-
93
- /** 工具定义轻校验(fail loud → 调用方 warnOnce 跳过该条)。 */
94
- function validateDefinition(def) {
95
- if (def === null || typeof def !== 'object' || Array.isArray(def)) throw new TypeError('tool definition must be an object')
96
- if (typeof def.id !== 'string' || def.id.length === 0) throw new TypeError('tool definition needs a non-empty string id')
97
- if (typeof def.name !== 'string' || !TOOL_NAME_RE.test(def.name)) {
98
- throw new TypeError(`tool name ${JSON.stringify(def.name)} must match ${TOOL_NAME_RE}`)
99
- }
100
- if (typeof def.description !== 'string' || def.description.trim().length === 0) {
101
- throw new TypeError(`tool ${def.id}: description is required`)
102
- }
103
- if (def.parameters !== undefined) {
104
- if (def.parameters === null || typeof def.parameters !== 'object' || Array.isArray(def.parameters)) {
105
- throw new TypeError(`tool ${def.id}: parameters must be an object map`)
118
+ if (node.const !== undefined && !Object.is(node.const, value)) {
119
+ violations.push(path + ': value not equal to const')
106
120
  }
107
- for (const [key, child] of Object.entries(def.parameters)) {
108
- validateSchemaNode(child, `parameters.${key}`, new Set())
121
+ if (type === 'array' && node.items !== undefined && Array.isArray(value)) {
122
+ value.forEach((item, index) => walk(node.items, item, path + '[' + index + ']'))
123
+ }
124
+ if (type === 'object' && typeof value === 'object' && value !== null && !Array.isArray(value)) {
125
+ if (Array.isArray(node.required)) {
126
+ for (const key of node.required) {
127
+ if (!(key in value)) violations.push(path + ': missing required property \"' + key + '\"')
128
+ }
129
+ }
130
+ if (node.properties !== undefined && typeof node.properties === 'object' && !Array.isArray(node.properties)) {
131
+ for (const [key, child] of Object.entries(node.properties)) {
132
+ if (key in value) walk(child, value[key], path + '.' + key)
133
+ }
134
+ }
135
+ if (node.additionalProperties === false && node.properties !== undefined) {
136
+ for (const key of Object.keys(value)) {
137
+ if (!(key in node.properties)) violations.push(path + ': unexpected property \"' + key + '\"')
138
+ }
139
+ }
109
140
  }
110
141
  }
111
- const output = def.output
112
- if (output === null || typeof output !== 'object' || Array.isArray(output)) {
113
- throw new TypeError(`tool ${def.id}: output is required`)
114
- }
115
- validateSchemaNode(output.schema, 'output.schema', new Set())
116
- const exec = def.execute
117
- if (exec === null || typeof exec !== 'object' || Array.isArray(exec)) {
118
- throw new TypeError(`tool ${def.id}: execute is required`)
119
- }
120
- if (typeof exec.kind !== 'string' || !['shell', 'http', 'delegate', 'fs', 'ask-user'].includes(exec.kind)) {
121
- throw new TypeError(`tool ${def.id}: execute.kind must be shell/http/delegate/fs/ask-user`)
122
- }
123
- if (exec.kind === 'shell' && (typeof exec.command !== 'string' || exec.command.trim().length === 0)) {
124
- throw new TypeError(`tool ${def.id}: execute.command is required for shell`)
125
- }
126
- if (exec.kind === 'http' && (typeof exec.url !== 'string' || exec.url.trim().length === 0)) {
127
- throw new TypeError(`tool ${def.id}: execute.url is required for http`)
128
- }
129
- if (exec.kind === 'delegate' && (typeof exec.tool !== 'string' || exec.tool.length === 0)) {
130
- throw new TypeError(`tool ${def.id}: execute.tool is required for delegate`)
131
- }
132
- if (exec.kind === 'fs' && (typeof exec.action !== 'string'
133
- || (!['read', 'write', 'append', 'list', 'delete'].includes(exec.action) && !exec.action.includes('{{args.')))) {
134
- throw new TypeError(`tool ${def.id}: execute.action must be read/write/append/list/delete (or {{args.*}} template) for fs`)
135
- }
136
- if (def.scope !== undefined && !['main', 'subagent', 'both'].includes(def.scope)) {
137
- throw new TypeError(`tool ${def.id}: scope must be main/subagent/both`)
138
- }
139
- if (def.enabled !== undefined && typeof def.enabled !== 'boolean') {
140
- throw new TypeError(`tool ${def.id}: enabled must be a boolean`)
141
- }
142
+ walk(schema, value, 'arguments')
143
+ return violations
142
144
  }
143
145
 
144
146
  /** approval 门:requireApproval 含该 kind 时先请求批准;无 approval 服务拒绝。 */
@@ -171,10 +173,47 @@ function withinCwd(cwd, target) {
171
173
  return resolved
172
174
  }
173
175
 
176
+ /** delegate args 映射:完整引用({{args.x}})透传原始类型(数组/数字/布尔不字符串化);
177
+ * 部分引用(前缀-{{args.x}})插值为字符串;非插值值原样。 */
178
+ function mapDelegateArgs(execArgs, args) {
179
+ const FULL_REF_RE = /^\{\{args\.([A-Za-z0-9_.]+)\}\}$/
180
+ const mapValue = (value) => {
181
+ if (Array.isArray(value)) return value.map(mapValue)
182
+ if (value !== null && typeof value === 'object') {
183
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, mapValue(child)]))
184
+ }
185
+ if (typeof value === 'string') {
186
+ const ref = FULL_REF_RE.exec(value)
187
+ if (ref !== null) {
188
+ let resolved = args
189
+ for (const part of ref[1].split('.')) {
190
+ if (resolved === null || typeof resolved !== 'object') {
191
+ resolved = undefined
192
+ break
193
+ }
194
+ resolved = resolved[part]
195
+ }
196
+ return resolved === undefined ? value : resolved
197
+ }
198
+ return interpolateArgs(value, args)
199
+ }
200
+ return value
201
+ }
202
+ return execArgs !== undefined && execArgs !== null && typeof execArgs === 'object'
203
+ ? Object.fromEntries(Object.entries(execArgs).map(([key, value]) => [key, mapValue(value)]))
204
+ : args
205
+ }
206
+
174
207
  /** 执行器分发:返回成功值(registry 按 output.schema 校验)。 */
175
208
  function createExecute(ctx, def, requireApproval) {
176
209
  const exec = def.execute
177
210
  return async (args, run) => {
211
+ if (def.parameters !== undefined) {
212
+ const violations = validateJsonSchemaValue(def.parameters, args)
213
+ if (violations.length > 0) {
214
+ throw new ToolArgsError(violations)
215
+ }
216
+ }
178
217
  if (requireApproval.includes(exec.kind)) {
179
218
  const gate = await approvalGate(ctx, run, exec.kind, `custom tool ${def.name} (${exec.kind})`)
180
219
  if (!gate.ok) return { ok: false, error: gate.message }
@@ -198,7 +237,7 @@ function createExecute(ctx, def, requireApproval) {
198
237
  exitCode: error === null ? 0 : (typeof error.code === 'number' ? error.code : 1),
199
238
  stdout: String(stdout ?? ''),
200
239
  stderr: String(stderr ?? ''),
201
- error: error === null ? undefined : String(error.message),
240
+ ...(error === null ? {} : { error: String(error.message) }),
202
241
  })
203
242
  })
204
243
  const timeout = def.timeoutMs
@@ -224,55 +263,31 @@ function createExecute(ctx, def, requireApproval) {
224
263
  return { status: response.status, ok: response.ok, body: text }
225
264
  }
226
265
  if (exec.kind === 'delegate') {
227
- const tools = ctx.get('tools')
228
- const target = tools?.get?.(exec.tool)
229
- if (target === undefined || typeof target.execute !== 'function') {
230
- return { ok: false, error: `delegated tool ${exec.tool} is not registered` }
231
- }
232
- // 高级用法:透传参数直接调用目标 execute(不经 registry 校验管线)。
233
- // args 映射:完整引用({{args.x}})透传原始类型(数组/数字/布尔不字符串化);
234
- // 部分引用(前缀-{{args.x}})插值为字符串;非插值值原样。
235
- const FULL_REF_RE = /^\{\{args\.([A-Za-z0-9_.]+)\}\}$/
236
- const mapDelegateArg = (value) => {
237
- if (Array.isArray(value)) return value.map(mapDelegateArg)
238
- if (value !== null && typeof value === 'object') {
239
- return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, mapDelegateArg(child)]))
240
- }
241
- if (typeof value === 'string') {
242
- const ref = FULL_REF_RE.exec(value)
243
- if (ref !== null) {
244
- let resolved = args
245
- for (const part of ref[1].split('.')) {
246
- if (resolved === null || typeof resolved !== 'object') {
247
- resolved = undefined
248
- break
249
- }
250
- resolved = resolved[part]
251
- }
252
- return resolved === undefined ? value : resolved
253
- }
254
- return interpolateArgs(value, args)
255
- }
256
- return value
266
+ const tools = ctx.tools ?? ctx.get('tools')
267
+ if (tools === undefined || typeof tools.execute !== 'function') {
268
+ return { ok: false, error: 'no tool runtime is available for delegate' }
257
269
  }
258
- const delegatedArgs = exec.args !== undefined && exec.args !== null && typeof exec.args === 'object'
259
- ? Object.fromEntries(Object.entries(exec.args).map(([key, value]) => [key, mapDelegateArg(value)]))
260
- : args
261
- const minimalExec = {
262
- name: def.name,
263
- callId: `delegate-${def.name}-${Date.now().toString(36)}`,
270
+ const delegatedArgs = mapDelegateArgs(exec.args, args)
271
+ // nested dispatch:走完整官方管线(校验/allow-deny-ask/pre-execute/post/
272
+ // timeout/nested token/durable result),调用身份由外层 callId 稳定派生,
273
+ // 禁止 Date.now() 生成调用身份。
274
+ const result = await tools.execute({
275
+ callId: `${run.callId}:delegate:${exec.tool}`,
276
+ rootCallId: run.rootCallId,
277
+ name: exec.tool,
264
278
  agent: run.agent,
265
- parent: run.parent,
266
- signal: run.signal,
279
+ parent: run.token,
267
280
  arguments: delegatedArgs,
268
- deferContext: () => {},
269
- concludeTurn: () => {},
281
+ signal: run.signal,
282
+ })
283
+ if (result.isError) {
284
+ return { ok: false, error: String(result.error?.message ?? result.error ?? `delegated tool ${exec.tool} failed`) }
270
285
  }
271
- try {
272
- return { ok: true, value: await target.execute(delegatedArgs, minimalExec) }
273
- } catch (error) {
274
- return { ok: false, error: error instanceof Error ? error.message : String(error) }
286
+ for (const context of result.additionalContexts ?? []) {
287
+ run.deferContext(context)
275
288
  }
289
+ if (result.concludesTurn === true) run.concludeTurn()
290
+ return { ok: true, value: result.value }
276
291
  }
277
292
  if (exec.kind === 'fs') {
278
293
  const cwd = run.agent?.session?.header?.cwd ?? process.cwd()
@@ -324,12 +339,22 @@ function createExecute(ctx, def, requireApproval) {
324
339
  }
325
340
  }
326
341
 
327
- /** 单份工具定义文件 → 编译为 ToolDefinition 结构(不含 id)。 */
342
+ /** 宿主 system-prompt 对 section 文本做 {{var}} 变量校验(变量名须匹配 [a-z][a-z0-9_]* 且已注册),
343
+ * 工具 description 进入 tools:sdk 段;description 中的双花括号字面量会炸掉整轮提示词组装
344
+ * (malformed / unknown prompt variable reference)。把配对的 {{...}} 统一降级为单花括号
345
+ * 示意({...}),其余字面保留;不配对的 {{ 宿主按字面处理,无需处理。 */
346
+ function sanitizeDescription(text) {
347
+ return text.replace(/\{\{([^{}]*)\}\}/g, '{$1}')
348
+ }
349
+
350
+ /** 单份工具定义文件 → 编译为完整官方 ToolDefinition(不含 id)。
351
+ * parameters/output.schema 已是 writePreset 物化的标准 JSON Schema;无参数
352
+ * 工具补空 object Schema(真实 ctx.tools.schemas() 需要 parameters 存在)。 */
328
353
  function compileTool(ctx, def, requireApproval) {
329
354
  return {
330
355
  name: def.name,
331
- description: def.description,
332
- ...(def.parameters !== undefined && Object.keys(def.parameters).length > 0 ? { parameters: def.parameters } : {}),
356
+ description: sanitizeDescription(def.description),
357
+ parameters: def.parameters ?? { type: 'object', properties: {} },
333
358
  output: {
334
359
  schema: def.output.schema,
335
360
  render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
@@ -340,12 +365,21 @@ function compileTool(ctx, def, requireApproval) {
340
365
  }
341
366
 
342
367
  /** 工具定义文件加载(*.yml / *.json;与 prompt-config-engine configsDir 同构)。 */
343
- function loadToolFiles(dirUrl) {
368
+ function loadToolFiles(dirUrl, presetRootUrl) {
344
369
  const resolved = isAbsolute(dirUrl)
345
370
  ? pathToFileURL(dirUrl).href
346
371
  : dirUrl
347
372
  const dir = new URL(resolved.endsWith('/') ? resolved : `${resolved}/`, import.meta.url)
348
373
  const localDir = fileURLToPath(dir)
374
+ // 相对 configsDir 只允许解析到预设根内:防组合行声明越界目录;绝对路径(显式配置/
375
+ // 测试桩)保持允许。预设根基准显式注入优先(引擎由插件包提供时必需),缺省沿用
376
+ // 「引擎上一级目录」这一历史布局推导。
377
+ if (!isAbsolute(dirUrl)) {
378
+ const presetRoot = fileURLToPath(presetRootUrl ?? new URL('../..', import.meta.url)).replace(/[\\\\/]$/, '')
379
+ if (localDir !== presetRoot && !localDir.startsWith(presetRoot + sep)) {
380
+ throw new Error(`${name}: configsDir ${JSON.stringify(dirUrl)} escapes preset root`)
381
+ }
382
+ }
349
383
  const files = readdirSync(localDir, { withFileTypes: true })
350
384
  .filter((entry) => entry.isFile() && /\.(ya?ml|json)$/i.test(entry.name))
351
385
  .sort((a, b) => a.name.localeCompare(b.name))
@@ -358,17 +392,41 @@ function loadToolFiles(dirUrl) {
358
392
  return out
359
393
  }
360
394
 
395
+ /**
396
+ * 配置契约:白名单由字段声明派生(此前没有白名单,未知键被静默忽略)。
397
+ * requireApproval 迁移前对**非数组取 `[]`**、对数组**过滤掉非字符串项**(宽容而非报错),
398
+ * configsDir 对非字符串/空串取默认值——两者都用 passthrough 保住该语义。
399
+ * 本任务只新增「未知键报错」,缺键与错类型的既有行为逐字段不变。
400
+ */
401
+ export const configContract = defineConfig({
402
+ configsDir: passthrough((value) => (typeof value === 'string' && value.length > 0 ? value : './custom-tools')),
403
+ requireApproval: passthrough((value) => (Array.isArray(value) ? value.filter((kind) => typeof kind === 'string') : [])),
404
+ // 预设根基准(绝对 file URL):引擎由插件包提供时由装配期注入,替代引擎位置推导。
405
+ presetRoot: passthrough((value) => (typeof value === 'string' && value.length > 0 ? value : undefined)),
406
+ })
407
+
408
+ /**
409
+ * 能力提供者登记(T4 边界守卫的数据源)。
410
+ * `provides.kind === 'dynamic'`:模型可见的工具名由 preset.yml 的 customTools 段在
411
+ * 装配期决定(物化为 custom-tools/*.yml),引擎侧无法静态枚举——守卫只断言登记形状、
412
+ * 注册通道,以及「提供者不导出触发器声明」这条互斥。
413
+ */
414
+ export const engineProvider = {
415
+ kind: 'provider',
416
+ moduleId: name,
417
+ registers: 'tools',
418
+ provides: { kind: 'dynamic', source: 'preset.yml#customTools → custom-tools/*.yml' },
419
+ }
420
+
361
421
  /** 插件入口:扫描 configsDir 注册全部自定义工具。 */
362
422
  export function apply(ctx, config) {
363
- const dirName = typeof config?.configsDir === 'string' && config.configsDir.length > 0
364
- ? config.configsDir
365
- : './custom-tools'
366
- const requireApproval = Array.isArray(config?.requireApproval)
367
- ? config.requireApproval.filter((kind) => typeof kind === 'string')
368
- : []
423
+ const { configsDir: dirName, requireApproval, presetRoot: rawPresetRoot } = configContract.parse(config, name)
424
+ const presetRootUrl = rawPresetRoot === undefined
425
+ ? undefined
426
+ : new URL(rawPresetRoot.endsWith('/') ? rawPresetRoot : `${rawPresetRoot}/`)
369
427
  let definitions = []
370
428
  try {
371
- definitions = loadToolFiles(dirName)
429
+ definitions = loadToolFiles(dirName, presetRootUrl)
372
430
  } catch (error) {
373
431
  ctx.logger?.warn(`${name}: cannot load ${dirName}: ${error?.message ?? error}`)
374
432
  return
@@ -381,7 +439,11 @@ export function apply(ctx, config) {
381
439
  continue
382
440
  }
383
441
  const tool = compileTool(ctx, def, requireApproval)
384
- ctx.effect(() => ctx.tools.register(tool), `${name}: ${def.name}`)
442
+ // 注册走 disposer 契约:register() 返回的正是它自己的 effect disposer,
443
+ // keepDisposer 把该 disposer 纳入本插件的资源所有权(卸载即撤销)。fiber 已卸载时
444
+ // register() 内部先抛 INACTIVE_EFFECT(与改写前 ctx.effect 的前置断言同一错误),
445
+ // 因此注册不会发生——降级语义与错误消息逐字不变。
446
+ keepDisposer(ctx, ctx.tools.register(tool), `${name}: ${def.name}`)
385
447
  ctx.logger?.info(`${name}: registered custom tool ${def.name} (${def.execute.kind})`)
386
448
  } catch (error) {
387
449
  ctx.logger?.warn(`${name}: skipping tool ${JSON.stringify(def?.id ?? def?.name)}: ${error?.message ?? error}`)
@@ -0,0 +1,143 @@
1
+ /** 自定义工具物化定义的纯校验:host 保存/物化与运行时共用,不加载宿主服务。 */
2
+
3
+ const TOOL_NAME_RE = /^[a-z][a-z0-9_]*$/
4
+ const JSON_SCHEMA_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'null', 'array', 'object'])
5
+ const JSON_SCHEMA_ANNOTATIONS = new Set(['description', 'title', 'default', 'examples'])
6
+
7
+ /** JSON Schema 结构校验;DSL 转换与官方 registry 仍负责 enforced subset 校验。 */
8
+ export function validateJsonSchemaNode(node, path, seen = new Set()) {
9
+ if (node === null || typeof node !== 'object' || Array.isArray(node)) {
10
+ throw new TypeError(`${path} must be an object`)
11
+ }
12
+ if (seen.has(node)) throw new TypeError(`${path} is circular`)
13
+ seen.add(node)
14
+ try {
15
+ if (node.type !== undefined) {
16
+ if (!JSON_SCHEMA_TYPES.has(node.type)) {
17
+ throw new TypeError(`${path}.type must be one of ${[...JSON_SCHEMA_TYPES].join('/')} (dsh-tools DSL 形态 'json'/'oneOf' 未在物化时转换)`)
18
+ }
19
+ if (node.type === 'object') {
20
+ if (node.properties !== undefined) {
21
+ if (node.properties === null || typeof node.properties !== 'object' || Array.isArray(node.properties)) {
22
+ throw new TypeError(`${path}.properties must be an object of schemas`)
23
+ }
24
+ for (const [key, child] of Object.entries(node.properties)) {
25
+ validateJsonSchemaNode(child, `${path}.properties.${key}`, seen)
26
+ }
27
+ }
28
+ if (node.required !== undefined) {
29
+ if (!Array.isArray(node.required) || node.required.some((item) => typeof item !== 'string')) {
30
+ throw new TypeError(`${path}.required must be an array of property names`)
31
+ }
32
+ }
33
+ if (node.additionalProperties !== undefined && typeof node.additionalProperties !== 'boolean') {
34
+ throw new TypeError(`${path}.additionalProperties must be a boolean`)
35
+ }
36
+ }
37
+ if (node.type === 'array' && node.items !== undefined) {
38
+ validateJsonSchemaNode(node.items, `${path}.items`, seen)
39
+ }
40
+ } else if (Array.isArray(node.oneOf)) {
41
+ if (node.oneOf.length < 2) throw new TypeError(`${path}.oneOf needs at least two branches`)
42
+ for (const [index, branch] of node.oneOf.entries()) {
43
+ validateJsonSchemaNode(branch, `${path}.oneOf[${index}]`, seen)
44
+ }
45
+ } else if (!Object.keys(node).every((key) => JSON_SCHEMA_ANNOTATIONS.has(key))) {
46
+ // 官方 DSL type: json 物化为 {} 或仅含 annotation 的节点;不能误判为未转换的 DSL。
47
+ throw new TypeError(`${path} must declare type or oneOf (or contain annotations only)`)
48
+ }
49
+ } finally {
50
+ seen.delete(node)
51
+ }
52
+ }
53
+
54
+ /** 完整定义校验;保存方拒绝写盘,writer / runtime 按单条告警跳过。 */
55
+ export function validateDefinition(def) {
56
+ if (def === null || typeof def !== 'object' || Array.isArray(def)) throw new TypeError('tool definition must be an object')
57
+ if (typeof def.id !== 'string' || def.id.trim().length === 0) throw new TypeError('tool definition needs a non-empty string id')
58
+ if (typeof def.name !== 'string' || !TOOL_NAME_RE.test(def.name)) {
59
+ throw new TypeError(`tool name ${JSON.stringify(def.name)} must match ${TOOL_NAME_RE}`)
60
+ }
61
+ if (typeof def.description !== 'string' || def.description.trim().length === 0) {
62
+ throw new TypeError(`tool ${def.id}: description is required`)
63
+ }
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)`)
66
+ }
67
+ if (def.parameters !== undefined) {
68
+ validateJsonSchemaNode(def.parameters, `tool ${def.id}: parameters`)
69
+ }
70
+ const output = def.output
71
+ if (output === null || typeof output !== 'object' || Array.isArray(output)) {
72
+ throw new TypeError(`tool ${def.id}: output is required`)
73
+ }
74
+ validateJsonSchemaNode(output.schema, `tool ${def.id}: output.schema`)
75
+ const exec = def.execute
76
+ if (exec === null || typeof exec !== 'object' || Array.isArray(exec)) {
77
+ throw new TypeError(`tool ${def.id}: execute is required`)
78
+ }
79
+ if (typeof exec.kind !== 'string' || !['shell', 'http', 'delegate', 'fs', 'ask-user'].includes(exec.kind)) {
80
+ throw new TypeError(`tool ${def.id}: execute.kind must be shell/http/delegate/fs/ask-user`)
81
+ }
82
+ if (exec.kind === 'shell' && (typeof exec.command !== 'string' || exec.command.trim().length === 0)) {
83
+ throw new TypeError(`tool ${def.id}: execute.command is required for shell`)
84
+ }
85
+ if (exec.kind === 'http' && (typeof exec.url !== 'string' || exec.url.trim().length === 0)) {
86
+ throw new TypeError(`tool ${def.id}: execute.url is required for http`)
87
+ }
88
+ if (exec.kind === 'delegate' && (typeof exec.tool !== 'string' || exec.tool.length === 0)) {
89
+ throw new TypeError(`tool ${def.id}: execute.tool is required for delegate`)
90
+ }
91
+ if (exec.kind === 'fs' && (typeof exec.action !== 'string'
92
+ || (!['read', 'write', 'append', 'list', 'delete'].includes(exec.action) && !exec.action.includes('{{args.')))) {
93
+ throw new TypeError(`tool ${def.id}: execute.action must be read/write/append/list/delete (or {{args.*}} template) for fs`)
94
+ }
95
+ if (def.enabled !== undefined && typeof def.enabled !== 'boolean') {
96
+ throw new TypeError(`tool ${def.id}: enabled must be a boolean`)
97
+ }
98
+ // 与 Node 定时器的有符号 32 位上限一致,避免溢出后变成 1 ms。
99
+ if (def.timeoutMs !== undefined && (!Number.isSafeInteger(def.timeoutMs) || def.timeoutMs <= 0 || def.timeoutMs > 2_147_483_647)) {
100
+ throw new TypeError(`tool ${def.id}: timeoutMs must be an integer from 1 to 2147483647`)
101
+ }
102
+ for (const field of ['command', 'shell', 'url', 'method', 'tool', 'action', 'path', 'content', 'question']) {
103
+ if (exec[field] !== undefined && typeof exec[field] !== 'string') {
104
+ throw new TypeError(`tool ${def.id}: execute.${field} must be a string`)
105
+ }
106
+ }
107
+ if (exec.shell !== undefined && (exec.shell.trim().length === 0 || exec.shell.includes('\0'))) {
108
+ throw new TypeError(`tool ${def.id}: execute.shell must be a non-empty executable name or path without NUL`)
109
+ }
110
+ if (exec.method !== undefined && !/^\{\{args\.[A-Za-z0-9_.]+\}\}$/.test(exec.method) && (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(exec.method)
111
+ || ['CONNECT', 'TRACE', 'TRACK'].includes(exec.method.toUpperCase()))) {
112
+ throw new TypeError(`tool ${def.id}: execute.method must be a supported HTTP method token`)
113
+ }
114
+ if (exec.kind === 'fs' && (typeof exec.path !== 'string' || exec.path.includes('\0'))) {
115
+ throw new TypeError(`tool ${def.id}: execute.path must be a string without NUL for fs`)
116
+ }
117
+ for (const field of ['env', 'headers', 'args']) {
118
+ const value = exec[field]
119
+ if (value === undefined) continue
120
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
121
+ throw new TypeError(`tool ${def.id}: execute.${field} must be an object`)
122
+ }
123
+ if (field !== 'args' && Object.values(value).some((entry) => typeof entry !== 'string')) {
124
+ throw new TypeError(`tool ${def.id}: execute.${field} values must be strings`)
125
+ }
126
+ if (field === 'env' && Object.entries(value).some(([key, entry]) => key.length === 0 || /[=\0]/.test(key) || entry.includes('\0'))) {
127
+ throw new TypeError(`tool ${def.id}: execute.env must contain valid environment names and values`)
128
+ }
129
+ if (field === 'headers') {
130
+ try { new Headers(value) } catch {
131
+ throw new TypeError(`tool ${def.id}: execute.headers must contain valid HTTP names and values`)
132
+ }
133
+ }
134
+ }
135
+ // 单条上限覆盖命令、文件内容及嵌套 body/args/schema;不丢弃模板未知字段。
136
+ let serialized
137
+ try { serialized = JSON.stringify(def) } catch {
138
+ throw new TypeError(`tool ${def.id}: definition must be JSON serializable`)
139
+ }
140
+ if (Buffer.byteLength(serialized, 'utf8') > 1_048_576) {
141
+ throw new TypeError(`tool ${def.id}: definition must not exceed 1048576 UTF-8 bytes`)
142
+ }
143
+ }