dsh-plugin-prompt-tool 2.0.1 → 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 (231) 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 +5 -7
  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 +23 -15
  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 +62 -19
  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 +4 -7
  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 +4 -4
  40. package/engine/conditions/count.mjs +43 -13
  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 +10 -5
  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 +165 -230
  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 +302 -28
  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 +47 -26
  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 +39 -35
  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 +37 -762
  123. package/lib/index.mjs +26919 -5791
  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 +40 -41
  152. package/skills/dsh-module/SKILL.md +119 -0
  153. package/skills/dsh-module/reference.md +185 -0
  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 +20 -0
  162. package/templates/64-pre-step-filter.yml +23 -0
  163. package/templates/65-turn-stop.yml +2 -2
  164. package/templates/67-inbox-prepend.yml +22 -0
  165. package/templates/70-subagent-maintenance.yml +9 -10
  166. package/templates/75-subagent-end.yml +2 -2
  167. package/templates/80-tool-surface.yml +41 -57
  168. package/templates/tools/40-delegate-module-import.yml +22 -0
  169. package/templates/tools/50-delegate-module-rules.yml +14 -0
  170. package/templates/tools/60-delegate-module-variables.yml +17 -0
  171. package/templates/tools/70-delegate-module-rules-edit.yml +31 -0
  172. package/engine/actions/decision.mjs +0 -54
  173. package/engine/actions/guard.mjs +0 -80
  174. package/engine/actions/sdk-strip.mjs +0 -30
  175. package/engine/character-tools.mjs +0 -8
  176. package/engine/classify-task.mjs +0 -53
  177. package/engine/compositions/library/agent-instructions.yml +0 -9
  178. package/engine/compositions/library/command-goal.yml +0 -7
  179. package/engine/compositions/library/compaction.yml +0 -22
  180. package/engine/compositions/library/delegation-ptc.yml +0 -58
  181. package/engine/compositions/library/delegation.yml +0 -56
  182. package/engine/compositions/library/persistent-shell.yml +0 -50
  183. package/engine/compositions/library/planning.yml +0 -26
  184. package/engine/compositions/library/skill-filesystem-cordis.yml +0 -10
  185. package/engine/compositions/library/skill-filesystem.yml +0 -7
  186. package/engine/compositions/library/tool-ask-user.yml +0 -7
  187. package/engine/compositions/library/tool-bash.yml +0 -8
  188. package/engine/compositions/library/tool-cordis.yml +0 -7
  189. package/engine/compositions/library/tool-fs-search.yml +0 -9
  190. package/engine/compositions/library/tool-fs.yml +0 -7
  191. package/engine/compositions/library/tool-goal.yml +0 -7
  192. package/engine/compositions/library/tool-jobs.yml +0 -7
  193. package/engine/compositions/library/tool-plugin-manager-disabled.yml +0 -8
  194. package/engine/compositions/library/tool-plugin-manager.yml +0 -8
  195. package/engine/compositions/library/tool-present.yml +0 -7
  196. package/engine/compositions/library/tool-presentation.yml +0 -9
  197. package/engine/compositions/library/tool-pwsh.yml +0 -8
  198. package/engine/compositions/library/tool-skill.yml +0 -7
  199. package/engine/compositions/library/tool-todo.yml +0 -9
  200. package/engine/compositions/library/tool-web.yml +0 -10
  201. package/engine/compositions/source/local/character-tools.yml +0 -5
  202. package/engine/compositions/source/local/declared-triggers.yml +0 -12
  203. package/engine/compositions/source/local/dev-tool-search.yml +0 -11
  204. package/engine/compositions/source/local/session-var-tools.yml +0 -5
  205. package/engine/compositions/source/local/subagent-tool-policy.yml +0 -21
  206. package/engine/compositions/source/local/world-book-tools.yml +0 -5
  207. package/engine/condition.mjs +0 -96
  208. package/engine/declared-triggers.mjs +0 -93
  209. package/engine/session-var-tools.mjs +0 -8
  210. package/engine/session-vars.mjs +0 -54
  211. package/engine/st-macros.d.mts +0 -22
  212. package/engine/st-macros.mjs +0 -208
  213. package/engine/st-render.mjs +0 -109
  214. package/engine/st-world-book.d.mts +0 -10
  215. package/engine/subagent-tool-policy-core.mjs +0 -400
  216. package/engine/subagent-tool-policy.mjs +0 -352
  217. package/engine/trigger-spec.mjs +0 -173
  218. package/engine/world-book-tools.mjs +0 -8
  219. package/lib/migrate-rules.d.mts +0 -1
  220. package/lib/migrate-rules.mjs +0 -39
  221. package/lib/module-package-CHPFweqK.mjs +0 -2
  222. package/lib/module-package-Cnd1_mwx.mjs +0 -2872
  223. package/lib/paths-CyP0IkaM.d.mts +0 -1390
  224. package/lib/preset-transfer.d.mts +0 -2
  225. package/lib/preset-transfer.mjs +0 -3
  226. package/lib/rules-migration-DJ6vASB5.mjs +0 -15334
  227. package/templates/policies/legacy-defaults.yml +0 -34
  228. package/templates/tools/40-delegate-character.yml +0 -15
  229. package/templates/tools/50-delegate-worldbook.yml +0 -15
  230. package/templates/tools/60-delegate-session-var.yml +0 -15
  231. package/templates/tools/70-delegate-worldbook-upsert.yml +0 -36
@@ -0,0 +1,178 @@
1
+ /** 有界模板解析与原生求值;只返回正文和变更,不持有或写入持久状态。 */
2
+ import { Buffer } from 'node:buffer'
3
+ import { interpolateVariables, isReservedInterpolationName, normalizeMacroSyntax } from './interpolate.mjs'
4
+
5
+ const MAX_BYTES = 1024 * 1024
6
+ const MAX_DEPTH = 32
7
+ const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
8
+
9
+ function byteLength(text) {
10
+ const size = Buffer.byteLength(text, 'utf8')
11
+ if (size > MAX_BYTES) throw new RangeError('Template text exceeds 1 MiB')
12
+ return size
13
+ }
14
+
15
+ /** JSON 单花括号及字符串内的右括号不构成模板边界。 */
16
+ function macroEnd(text, start, depth = 0) {
17
+ const frames = [{ braces: 0, brackets: 0, quote: false, escape: false }]
18
+ if (depth >= MAX_DEPTH) throw new RangeError('Template depth exceeds 32')
19
+ for (let index = start + 2; index < text.length; index++) {
20
+ const frame = frames.at(-1), char = text[index]
21
+ if (frame.escape) { frame.escape = false; continue }
22
+ if (frame.quote && char === '\\') { frame.escape = true; continue }
23
+ if (char === '"' && (frame.quote || frame.braces > 0 || frame.brackets > 0)) { frame.quote = !frame.quote; continue }
24
+ if (text.startsWith('{{', index)) {
25
+ if (depth + frames.length >= MAX_DEPTH) throw new RangeError('Template depth exceeds 32')
26
+ frames.push({ braces: 0, brackets: 0, quote: false, escape: false }); index++; continue
27
+ }
28
+ if (frame.quote) continue
29
+ if (char === '{') frame.braces++
30
+ else if (char === '}' && frame.braces > 0) frame.braces--
31
+ else if (text.startsWith('}}', index)) {
32
+ frames.pop()
33
+ if (!frames.length) return index + 2
34
+ index++
35
+ } else if (char === '[') frame.brackets++
36
+ else if (char === ']') frame.brackets = Math.max(0, frame.brackets - 1)
37
+ }
38
+ return -1
39
+ }
40
+
41
+ /** ST 编辑残片不是可执行表达式;只保留顶层尾部未知裸名,不放宽操作或嵌套语法。 */
42
+ function isLiteralFragment(text) {
43
+ const match = /^\{\{\s*([A-Za-z0-9_.\u4e00-\u9fff-]+)\s*\}?$/.exec(text.trimEnd())
44
+ if (!match) return false
45
+ const key = match[1]
46
+ return !FORBIDDEN_KEYS.has(key) && !isReservedInterpolationName(key)
47
+ && !/^(?:(?:module|global)(?:\.|$)|(?:set|add|get|inc|dec)(?:global)?var$|char$|charifnotgroup$|user$|trim$|ERA$)/i.test(key)
48
+ }
49
+
50
+ /** 转换端和求值端共用边界与预算;出现身份不含正文字符偏移。 */
51
+ export function transformTemplate(text, macro) {
52
+ let work = 0, occurrence = 0
53
+ function walk(source, depth = 0) {
54
+ work += byteLength(source)
55
+ if (work > MAX_BYTES * MAX_DEPTH) throw new RangeError('Template expansion work limit exceeded')
56
+ const chunks = []
57
+ let size = 0, cursor = 0
58
+ const append = value => {
59
+ size += byteLength(value)
60
+ if (size > MAX_BYTES) throw new RangeError('Template output exceeds 1 MiB')
61
+ chunks.push(value)
62
+ }
63
+ while (cursor < source.length) {
64
+ const start = source.indexOf('{{', cursor)
65
+ if (start < 0) { append(source.slice(cursor)); break }
66
+ append(source.slice(cursor, start))
67
+ const end = macroEnd(source, start, depth)
68
+ if (end < 0) {
69
+ const rest = source.slice(start)
70
+ if (depth === 0 && isLiteralFragment(rest)) { append(rest); break }
71
+ throw new TypeError('Unclosed template expression')
72
+ }
73
+ append(macro(source.slice(start + 2, end - 2), value => walk(value, depth + 1), occurrence++))
74
+ cursor = end
75
+ }
76
+ return chunks.join('')
77
+ }
78
+ return walk(text)
79
+ }
80
+
81
+ /** 当前层第一个分隔符;嵌套表达式里的分隔符属于子表达式。 */
82
+ export function splitTemplateArgument(text) {
83
+ for (let index = 0; index < text.length; index++) {
84
+ if (text.startsWith('{{', index)) {
85
+ const end = macroEnd(text, index)
86
+ if (end < 0) throw new TypeError('Unclosed template expression')
87
+ index = end - 1
88
+ } else if (text.startsWith('::', index)) return [text.slice(0, index), text.slice(index + 2)]
89
+ }
90
+ return [text, undefined]
91
+ }
92
+
93
+ function addValue(value, increment) {
94
+ const current = value || '0'
95
+ let parsed
96
+ try { parsed = JSON.parse(current) } catch { /* 非 JSON 继续按数值或文本处理。 */ }
97
+ if (Array.isArray(parsed)) { parsed.push(increment); return JSON.stringify(parsed) }
98
+ if (Number.isNaN(Number(increment)) || Number.isNaN(Number(current))) return (value || '') + increment
99
+ const result = Number(current) + Number(increment)
100
+ if (!Number.isFinite(result)) throw new RangeError('Template variable number is not finite')
101
+ return String(result)
102
+ }
103
+
104
+ export function evaluateTemplate(text, { module = {}, global = {}, session, sourceId = '', readOnly = false, samples = new Map() } = {}) {
105
+ const tables = { module: { ...module }, global: { ...global } }
106
+ const reads = new Set(), resolving = new Set(), values = {}
107
+ let scope, pick = 0
108
+ const keyOf = key => {
109
+ if (!key || FORBIDDEN_KEYS.has(key) || isReservedInterpolationName(key)) throw new TypeError(`Template variable key blocked: ${key}`)
110
+ return key
111
+ }
112
+ const raw = (domain, key) => {
113
+ reads.add(domain)
114
+ if (Object.hasOwn(tables[domain], key)) return { domain, value: tables[domain][key] }
115
+ if (domain === 'module') {
116
+ reads.add('global')
117
+ if (Object.hasOwn(tables.global, key)) return { domain: 'global', value: tables.global[key] }
118
+ }
119
+ return undefined
120
+ }
121
+ const read = (found, key, expand) => {
122
+ if (!found) return ''
123
+ const id = `${found.domain}:${key}`
124
+ if (resolving.has(id)) throw new RangeError(`Template variable cycle: ${id}`)
125
+ resolving.add(id)
126
+ try { return expand(String(found.value)) } finally { resolving.delete(id) }
127
+ }
128
+ const write = (domain, key, value) => {
129
+ if (readOnly) throw new TypeError('Read-only template cannot write variables')
130
+ if (scope !== undefined && scope !== domain) throw new TypeError('One template may write only one variable scope')
131
+ byteLength(value)
132
+ scope = domain
133
+ tables[domain][key] = value
134
+ values[key] = value
135
+ }
136
+ const output = transformTemplate(text, (body, expand, occurrence) => {
137
+ const [name, args] = splitTemplateArgument(body)
138
+ const operation = /^(module|global)\.(set|get|add|inc|dec)$/.exec(name.trim())
139
+ if (operation) {
140
+ if (args === undefined) throw new TypeError(`Missing arguments for ${name}`)
141
+ const [rawKey, rawValue] = splitTemplateArgument(args)
142
+ const key = keyOf(expand(rawKey).trim()), domain = operation[1], action = operation[2]
143
+ if (readOnly && (action !== 'get' || rawValue !== undefined)) throw new TypeError('Read-only template cannot write variables')
144
+ if (action === 'get') {
145
+ const found = raw(domain, key)
146
+ if (found) return read(found, key, expand)
147
+ const fallback = rawValue === undefined ? '' : expand(rawValue)
148
+ if (fallback !== '') write(domain, key, fallback)
149
+ return fallback
150
+ }
151
+ if ((action === 'set' || action === 'add') && rawValue !== undefined) {
152
+ const value = expand(rawValue)
153
+ write(domain, key, action === 'set' ? value : addValue(read(raw(domain, key), key, expand), value))
154
+ return ''
155
+ }
156
+ if ((action === 'inc' || action === 'dec') && rawValue === undefined) {
157
+ const value = addValue(read(raw(domain, key), key, expand), action === 'inc' ? '1' : '-1')
158
+ write(domain, key, value)
159
+ return value
160
+ }
161
+ throw new TypeError(`Invalid arguments for ${name}`)
162
+ }
163
+ if (/^(set|add|get|inc|dec)(global)?var$/i.test(name.trim())) throw new TypeError('ST variable operations are retired; reimport or explicitly convert to module/global operations')
164
+ const whole = normalizeMacroSyntax(`{{${expand(body)}}}`)
165
+ const [key] = splitTemplateArgument(whole.slice(2, -2))
166
+ if (FORBIDDEN_KEYS.has(key)) throw new TypeError(`Template variable key blocked: ${key}`)
167
+ if (args === undefined && !isReservedInterpolationName(key)) {
168
+ const found = raw('module', key)
169
+ if (found) return read(found, key, expand)
170
+ }
171
+ if (whole.slice(2, -2).includes('{{')) return whole
172
+ const identity = key.toLowerCase() === 'pick' ? `pick:${pick++}` : String(occurrence)
173
+ const sample = `${identity}:${whole}`
174
+ if (!samples.has(sample)) samples.set(sample, interpolateVariables(whole, {}, session, undefined, `${sourceId}:${identity}`))
175
+ return samples.get(sample)
176
+ })
177
+ return { text: output, reads: [...reads], ...(scope === undefined ? {} : { changes: { scope, values } }) }
178
+ }
@@ -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,22 +22,22 @@
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,也没有
30
- * when/do 声明,只在装配期把 module.yml 的 customTools 段编译出的工具注册进官方
30
+ * 声明式触发器(`if`/`then` 声明),只在装配期把 module.yml 的 customTools 段编译出的工具注册进官方
31
31
  * registry。强行并进触发器会让引擎同时承担「提供能力」与「干预流程」两种职责。
32
32
  *
33
33
  * 样板收敛:
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
+ }