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.
- package/README.md +185 -46
- package/engine/THIRD_PARTY_LICENSES +25 -0
- package/engine/actions.mjs +931 -0
- package/engine/anchor-match.mjs +6 -3
- package/engine/character-tools.mjs +11 -0
- package/engine/classify-task.mjs +18 -0
- package/engine/compaction-epoch.mjs +41 -20
- package/engine/compositions/library/agent-instructions.yml +9 -0
- package/engine/compositions/library/command-goal.yml +2 -6
- package/engine/compositions/library/compaction.yml +2 -32
- package/engine/compositions/library/delegation-ptc.yml +58 -0
- package/engine/compositions/library/delegation.yml +5 -38
- package/engine/compositions/library/persistent-shell.yml +4 -13
- package/engine/compositions/library/planning.yml +8 -23
- package/engine/compositions/library/skill-filesystem-cordis.yml +10 -0
- package/engine/compositions/library/skill-filesystem.yml +2 -8
- package/engine/compositions/library/tool-ask-user.yml +2 -3
- package/engine/compositions/library/tool-bash.yml +4 -12
- package/engine/compositions/library/tool-cordis.yml +7 -0
- package/engine/compositions/library/tool-fs-search.yml +2 -11
- package/engine/compositions/library/tool-fs.yml +2 -5
- package/engine/compositions/library/tool-goal.yml +2 -6
- package/engine/compositions/library/tool-jobs.yml +2 -18
- package/engine/compositions/library/tool-plugin-manager-disabled.yml +8 -0
- package/engine/compositions/library/tool-plugin-manager.yml +8 -0
- package/engine/compositions/library/tool-present.yml +7 -0
- package/engine/compositions/library/tool-presentation.yml +9 -0
- package/engine/compositions/library/tool-pwsh.yml +2 -6
- package/engine/compositions/library/tool-skill.yml +7 -0
- package/engine/compositions/library/tool-todo.yml +2 -4
- package/engine/compositions/library/tool-web.yml +2 -1
- package/engine/compositions/source/local/character-tools.yml +5 -0
- package/engine/compositions/source/local/declared-triggers.yml +12 -0
- package/engine/compositions/source/local/filesystem-editor.yml +30 -0
- package/engine/compositions/source/local/instruction-hint.yml +15 -0
- package/engine/compositions/{library/official-persistent-shell.yml → source/local/persistent-shell-posix.yml} +10 -8
- package/engine/compositions/source/local/prompt-config-engine.yml +3 -9
- package/engine/compositions/source/local/run-code-env.yml +4 -7
- package/engine/compositions/source/local/session-var-tools.yml +5 -0
- package/engine/compositions/source/local/skill-search.yml +3 -9
- package/engine/compositions/source/local/subagent-tool-policy.yml +21 -0
- package/engine/compositions/source/local/tool-bash-disabled.yml +9 -0
- package/engine/compositions/source/local/tool-config-engine.yml +8 -0
- package/engine/compositions/source/local/tool-git-bash.yml +15 -0
- package/engine/compositions/source/local/world-book-tools.yml +5 -0
- package/engine/condition.mjs +83 -0
- package/engine/declared-triggers.mjs +93 -0
- package/engine/executor.mjs +364 -185
- package/engine/fields.mjs +219 -0
- package/engine/fillers.mjs +12 -141
- package/engine/host-package.mjs +51 -0
- package/engine/instruction-hint.mjs +285 -0
- package/engine/interpolate.mjs +157 -29
- package/engine/layers.mjs +495 -75
- package/engine/predicates.mjs +753 -0
- package/engine/prompt-config-engine.mjs +63 -8
- package/engine/run-code-env.mjs +48 -31
- package/engine/schema.mjs +444 -103
- package/engine/sdk-strip.mjs +365 -0
- package/engine/session-var-tools.mjs +11 -0
- package/engine/session-vars.mjs +8 -6
- package/engine/shared.mjs +137 -0
- package/engine/skill-search.mjs +34 -21
- package/engine/st-macros.d.mts +22 -0
- package/engine/st-macros.mjs +208 -0
- package/engine/st-render.mjs +105 -0
- package/engine/st-world-book.d.mts +10 -0
- package/engine/st-world-book.mjs +259 -0
- package/engine/strategies.mjs +31 -20
- package/engine/subagent-tool-policy-core.mjs +400 -0
- package/engine/subagent-tool-policy.mjs +327 -0
- package/engine/tool-config-engine.mjs +207 -145
- package/engine/tool-definition.mjs +143 -0
- package/engine/{custom-bash.mjs → tool-git-bash.mjs} +38 -13
- package/engine/trigger-editor-meta.mjs +48 -0
- package/engine/trigger-spec.mjs +257 -0
- package/engine/trigger.mjs +304 -0
- package/engine/world-book-tools.mjs +11 -0
- package/lib/client.js +16495 -5456
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +628 -382
- package/lib/index.mjs +5391 -2942
- package/lib/paths-CY5psBqE.d.mts +1233 -0
- package/lib/preset-package-BnJqrU4R.mjs +16792 -0
- package/lib/preset-transfer.d.mts +2 -0
- package/lib/preset-transfer.mjs +2 -0
- package/package.json +91 -49
- package/preset/pt-cordis/preset.yml +43 -0
- package/preset/pt-cordis/skills/cordis-composition-reference/SKILL.md +29 -0
- package/preset/pt-cordis/skills/cordis-composition-reference/references/packages.md +501 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/SKILL.md +38 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/references/host-plugin.md +60 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/references/mcp-bundle.md +5 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/references/ui-plugin.md +13 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/references/verification.md +5 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/client.js +22 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/cordis.patch.yml +3 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/index.js +2 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/package.json +15 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/cordis.patch.yml +8 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/package.json +7 -0
- package/preset/pt-cordis/skills/editing-cordis-compositions/SKILL.md +84 -0
- package/preset/pt-custom/preset.yml +15 -0
- package/preset/pt-minimal/preset.yml +27 -0
- package/preset/pt-ptc/preset.yml +45 -0
- package/preset/pt-standard/preset.yml +43 -0
- package/templates/14-first-turn-anchor.yml +4 -4
- package/templates/15-guide-auto.yml +3 -3
- package/templates/16-custom-fallback.yml +1 -0
- package/templates/18-placeholder-env-facts.yml +3 -3
- package/templates/20-system-section.yml +3 -2
- package/templates/65-turn-stop.yml +18 -0
- package/templates/66-subagent-start.yml +14 -0
- package/templates/75-subagent-end.yml +15 -0
- package/templates/tools/10-shell-command.yml +1 -1
- package/templates/tools/40-delegate-character.yml +1 -1
- package/templates/tools/50-delegate-worldbook.yml +1 -1
- package/templates/tools/60-delegate-session-var.yml +1 -1
- package/templates/tools/70-delegate-worldbook-upsert.yml +1 -1
- package/engine/anchor-turn.mjs +0 -68
- package/engine/code-presentation.mjs +0 -103
- package/engine/compositions/library/anchor-turn.yml +0 -10
- package/engine/compositions/library/bootstrap-filesystem.yml +0 -19
- package/engine/compositions/library/code-presentation.yml +0 -8
- package/engine/compositions/library/context-gate.yml +0 -39
- package/engine/compositions/library/cot-drip.yml +0 -11
- package/engine/compositions/library/custom-bash.yml +0 -14
- package/engine/compositions/library/deliberation-gate.yml +0 -11
- package/engine/compositions/library/official-agent-instructions.yml +0 -17
- package/engine/compositions/library/official-skill-filesystem-cordis.yml +0 -10
- package/engine/compositions/library/official-tool-bash.yml +0 -6
- package/engine/compositions/library/official-tool-cordis.yml +0 -13
- package/engine/compositions/library/official-tool-presentation.yml +0 -7
- package/engine/compositions/library/official-tool-skill.yml +0 -14
- package/engine/compositions/library/persona.yml +0 -13
- package/engine/compositions/library/prompt-config-engine.yml +0 -16
- package/engine/compositions/library/run-code-env.yml +0 -15
- package/engine/compositions/library/skill-search.yml +0 -14
- package/engine/compositions/library/str-replace-editor.yml +0 -10
- package/engine/compositions/library/tool-bootstrap.yml +0 -14
- package/engine/compositions/library/tool-config-engine.yml +0 -9
- package/engine/compositions/library/tool-filter.yml +0 -8
- package/engine/compositions/source/local/code-presentation.yml +0 -8
- package/engine/compositions/source/local/context-gate.yml +0 -36
- package/engine/compositions/source/local/custom-bash.yml +0 -11
- package/engine/compositions/source/local/str-replace-editor.yml +0 -10
- package/engine/compositions/source/local/tool-bootstrap.yml +0 -11
- package/engine/compositions/source/local/tool-filter.yml +0 -8
- package/engine/context-gate.mjs +0 -305
- package/engine/cot-drip.mjs +0 -137
- package/engine/deliberation-gate.mjs +0 -127
- package/engine/tool-bootstrap.mjs +0 -476
- package/engine/tool-filter.mjs +0 -80
- package/lib/preset-core.d.mts +0 -10
- package/lib/preset-core.mjs +0 -77
- package/lib/prompt-configs-BnQKRHVb.mjs +0 -884
- package/lib/prompt-configs-CN0sJTUL.d.mts +0 -193
- package/preset/anchored/preset.yml +0 -296
- package/preset/creative/preset.yml +0 -67
- package/preset/creative/skills/cordis-plugin-development/SKILL.md +0 -420
- package/preset/creative/skills/editing-cordis-compositions/SKILL.md +0 -165
- package/preset/custom/preset.yml +0 -12
- package/preset/liangshen/preset.yml +0 -75
- package/preset/minimal/preset.yml +0 -45
- package/preset/ptc/preset.yml +0 -58
- package/preset/standard/preset.yml +0 -57
- package/skills/manifest.json +0 -7
- package/skills/sandboxmod/SKILL.md +0 -49
- 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 /
|
|
7
|
-
*
|
|
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 —
|
|
13
|
-
*
|
|
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
|
-
* 工具注册经
|
|
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
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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 (
|
|
75
|
-
for (const
|
|
76
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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.
|
|
83
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
|
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
|
-
|
|
229
|
-
|
|
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
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
callId:
|
|
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.
|
|
266
|
-
signal: run.signal,
|
|
279
|
+
parent: run.token,
|
|
267
280
|
arguments: delegatedArgs,
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
272
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
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
|
+
}
|