dsh-plugin-prompt-tool 1.0.0 → 2.0.1
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 +38 -29
- package/engine/actions/append-context.mjs +49 -0
- package/engine/actions/assembly.mjs +150 -0
- package/engine/actions/catalog.mjs +92 -0
- package/engine/actions/content.mjs +205 -0
- package/engine/actions/decision.mjs +54 -0
- package/engine/actions/examples.mjs +11 -0
- package/engine/actions/guard.mjs +80 -0
- package/engine/actions/inbox-prepend.mjs +37 -0
- package/engine/actions/index.mjs +100 -0
- package/engine/actions/inject-text.mjs +22 -0
- package/engine/actions/pre-step-filter.mjs +55 -0
- package/engine/actions/registration.mjs +168 -0
- package/engine/actions/request-params.mjs +32 -0
- package/engine/actions/sdk-strip.mjs +30 -0
- package/engine/actions/shared.mjs +71 -0
- package/engine/actions.mjs +2 -931
- package/engine/branch.mjs +44 -0
- package/engine/compaction-epoch.mjs +31 -13
- package/engine/compositions/source/local/dev-tool-search.yml +11 -0
- package/engine/compositions/source/local/instruction-hint.yml +22 -1
- package/engine/compositions/source/local/rule-engine.yml +6 -0
- package/engine/condition.mjs +16 -3
- package/engine/conditions/anchor.mjs +32 -0
- package/engine/conditions/availability.mjs +47 -0
- package/engine/conditions/composite.mjs +83 -0
- package/engine/conditions/count.mjs +140 -0
- package/engine/conditions/evaluation.mjs +41 -0
- package/engine/conditions/examples.mjs +11 -0
- package/engine/conditions/index.mjs +85 -0
- package/engine/conditions/names.mjs +21 -0
- package/engine/conditions/phase.mjs +54 -0
- package/engine/conditions/preset.mjs +94 -0
- package/engine/conditions/scope.mjs +18 -0
- package/engine/conditions/session.mjs +22 -0
- package/engine/conditions/source.mjs +63 -0
- package/engine/conditions/subject.mjs +121 -0
- package/engine/conditions/text.mjs +44 -0
- package/engine/conditions/values.mjs +32 -0
- package/engine/dev-tool-search.mjs +213 -0
- package/engine/executor.mjs +55 -6
- package/engine/fillers.mjs +23 -16
- package/engine/instruction-hint.mjs +34 -22
- package/engine/layers.mjs +135 -49
- package/engine/order.mjs +4 -1
- package/engine/predicates.mjs +2 -753
- package/engine/rule-engine.mjs +40 -0
- package/engine/rule-runtime.mjs +110 -0
- package/engine/rule-spec.mjs +187 -0
- package/engine/schema.mjs +16 -5
- package/engine/shared.mjs +1 -1
- package/engine/strategies.mjs +2 -235
- package/engine/subagent-tool-policy-core.mjs +7 -7
- package/engine/subagent-tool-policy.mjs +20 -10
- package/engine/tool-config-engine.mjs +53 -32
- package/engine/trigger-spec.mjs +49 -133
- package/lib/client.js +11117 -10860
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +82 -139
- package/lib/index.mjs +826 -1087
- package/lib/migrate-rules.d.mts +1 -0
- package/lib/migrate-rules.mjs +39 -0
- package/lib/module-package-CHPFweqK.mjs +2 -0
- package/lib/module-package-Cnd1_mwx.mjs +2872 -0
- package/lib/{paths-BgL42sSM.d.mts → paths-CyP0IkaM.d.mts} +223 -108
- package/lib/preset-transfer.d.mts +2 -2
- package/lib/preset-transfer.mjs +3 -2
- package/lib/{module-package-Cm3fHHlp.mjs → rules-migration-DJ6vASB5.mjs} +13696 -15557
- package/modules/ponytail/module.yml +381 -147
- package/modules/skill-surface/module.yml +74 -0
- package/modules/tool-surface/module.yml +67 -0
- package/package.json +5 -1
- package/templates/10-pre-step.yml +26 -22
- package/templates/14-first-turn-anchor.yml +25 -21
- package/templates/15-guide-auto.yml +34 -19
- package/templates/16-custom-fallback.yml +27 -15
- package/templates/18-placeholder-env-facts.yml +23 -14
- package/templates/20-system-section.yml +16 -10
- package/templates/30-runtime-context.yml +13 -7
- package/templates/40-agent-request.yml +11 -8
- package/templates/50-llm-stream.yml +14 -6
- package/templates/60-tool-pipeline.yml +15 -9
- package/templates/65-turn-stop.yml +19 -12
- package/templates/66-subagent-start.yml +12 -7
- package/templates/70-subagent-maintenance.yml +35 -29
- package/templates/75-subagent-end.yml +10 -5
- package/templates/80-tool-surface.yml +66 -0
- package/templates/policies/legacy-defaults.yml +34 -0
- package/engine/compositions/source/local/prompt-config-engine.yml +0 -7
- package/engine/prompt-config-engine.mjs +0 -104
- package/engine/trigger-editor-meta.mjs +0 -48
- package/modules/ponytail/configs/0000-ponytail-rules.yml +0 -102
- package/modules/ponytail/configs/0010-ponytail-level-full.yml +0 -19
- package/modules/ponytail/configs/0020-ponytail-level-lite.yml +0 -19
- package/modules/ponytail/configs/0030-ponytail-level-ultra.yml +0 -19
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { MAX_TRACKED_SESSIONS } from '../shared.mjs'
|
|
2
|
+
import { subjectOf } from '../conditions/subject.mjs'
|
|
3
|
+
import { ACTION_KINDS } from './catalog.mjs'
|
|
4
|
+
|
|
5
|
+
export function channelBinder(ctx, kind, plugin, prepend) {
|
|
6
|
+
const legal = new Set(ACTION_KINDS[kind].events)
|
|
7
|
+
return (event, handler, options) => {
|
|
8
|
+
if (!legal.has(event)) {
|
|
9
|
+
throw new TypeError(`${plugin}: action ${kind} must not register on ${JSON.stringify(event)} — legal channel(s): ${[...legal].join(', ')}`)
|
|
10
|
+
}
|
|
11
|
+
// `prepend` 只表达「落在宿主 waterfall 的最外层」(否决型动作如预算剥离需要它),
|
|
12
|
+
// **不承担声明之间的排序**——同通道顺序由注册先后决定(见 trigger.mjs 的 R13 依据)。
|
|
13
|
+
return ctx.on(event, handler, prepend === true ? { ...options, prepend: true } : options)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const ON_REGISTERED_KINDS = new Set(['assembly', 'decision', 'append-context', 'sdk-strip', 'request-params', 'inbox-prepend', 'pre-step-filter'])
|
|
18
|
+
|
|
19
|
+
/** 编辑目录与注册校验共用支持面;不另给客户端维护一份名单。 */
|
|
20
|
+
export const actionSupportsWhen = (kind) => ON_REGISTERED_KINDS.has(kind)
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* **没有 `next` 的通道**(官方 `@mode emit` / `@mode serial`):注册在这些通道上的动作
|
|
24
|
+
* 不能靠「`args` 的最后一个参数是 `next`」工作——判定路径与载荷切分都要走无 `next` 分支。
|
|
25
|
+
*
|
|
26
|
+
* 依据(`@deepseek-ai/dsh-agent/lib/types/runtime-types.d.ts` 的逐事件 `@mode` 注释):
|
|
27
|
+
* **只有 `waterfall` 才有 `next`**;`agent/pre-step`/`agent/request` 是 waterfall,
|
|
28
|
+
* 而 `agent/inbox/inserted`(B7 新增动作的通道)是 **emit**、
|
|
29
|
+
* `agent/turn-stopping`(既有 append-context「续跑」的通道)是 **serial**——
|
|
30
|
+
* 两者都没有 `next`。第三项此前是个隐患:`append-context(continue)` 一旦带 `when`,
|
|
31
|
+
* 旧实现会把 `turn` 当成 `next` 调掉。
|
|
32
|
+
*/
|
|
33
|
+
export const NEXT_FREE_CHANNELS = new Set([
|
|
34
|
+
'session/event',
|
|
35
|
+
'agent/inbox/inserted',
|
|
36
|
+
'agent/turn-stopping',
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
/** 动作的固定执行点;声明编译与 when 接线共用,不能用声明字段改变注册器。 */
|
|
40
|
+
export function actionExecutionPoint(action) {
|
|
41
|
+
if (action.kind === 'decision') {
|
|
42
|
+
return { channel: action.phase === 'post' ? 'tools/post-execute' : 'tools/pre-execute', phase: 'before-next' }
|
|
43
|
+
}
|
|
44
|
+
if (action.kind === 'append-context') {
|
|
45
|
+
return action.mode === 'continue'
|
|
46
|
+
? { channel: 'agent/turn-stopping', phase: 'before-next' }
|
|
47
|
+
: { channel: 'tools/post-execute', phase: 'after-next' }
|
|
48
|
+
}
|
|
49
|
+
if (action.kind === 'inject-text') {
|
|
50
|
+
const layer = action.config?.layer
|
|
51
|
+
const channel = {
|
|
52
|
+
'pre-step': 'agent/pre-step',
|
|
53
|
+
'system-section': 'system-prompt/assemble',
|
|
54
|
+
'runtime-context': 'system-prompt/assemble',
|
|
55
|
+
'agent-request': 'agent/request',
|
|
56
|
+
'llm-stream': 'llm/stream',
|
|
57
|
+
'turn-stop': 'agent/turn-stopping',
|
|
58
|
+
'subagent-start': 'subagent/start',
|
|
59
|
+
'subagent-end': 'subagent/end',
|
|
60
|
+
}[layer]
|
|
61
|
+
if (channel === undefined) {
|
|
62
|
+
throw new TypeError(`inject-text layer ${JSON.stringify(layer)} has no single trigger channel; tool-pipeline uses separate decision actions`)
|
|
63
|
+
}
|
|
64
|
+
return { channel, phase: ['pre-step', 'agent-request'].includes(layer) ? 'after-next' : 'before-next' }
|
|
65
|
+
}
|
|
66
|
+
const channel = ACTION_KINDS[action.kind].events[0]
|
|
67
|
+
return { channel, phase: NEXT_FREE_CHANNELS.has(channel) ? 'before-next' : 'after-next' }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 每轮生效预算(动作声明的 `maxPerTurn`)。
|
|
72
|
+
*
|
|
73
|
+
* **为什么必须是动作侧、不能用 `count` 原语**:原 `deliberation-gate` 的 `gates` 与原
|
|
74
|
+
* `progress-reminder` 的 `drips` 数的是**本动作自己生效了几次**,不是 durable 事件数。
|
|
75
|
+
* 它们是纯增量状态(重启后从 0 开始、每轮重置),而 `count` 的模型是冷扫 durable 事件流重建
|
|
76
|
+
* ——两者在重启/压缩后给出的答案不同,所以这是独立的一类计数。
|
|
77
|
+
*
|
|
78
|
+
* **轮号**取自 durable 事件的 `data.turn`(与 `predicates.mjs` 的 `trackTurn` 同规则:任何带
|
|
79
|
+
* 有限轮号的事件都推进当前轮并取最大值),所以重启后计数虽从 0 开始,轮边界立刻是对的。
|
|
80
|
+
*
|
|
81
|
+
* **同步消耗**:动作通过目标与有效结果检查后,紧邻实际效果同步 `take()`;
|
|
82
|
+
* claim 与效果之间没有 await,并行调用不得越过预算。
|
|
83
|
+
*
|
|
84
|
+
* @returns {{take: (subject: unknown) => boolean, observe: Function}|undefined} 无 `maxPerTurn` 时为 undefined
|
|
85
|
+
*/
|
|
86
|
+
export function createTurnBudget(max) {
|
|
87
|
+
if (max === undefined) return undefined
|
|
88
|
+
/** sessionId -> { turn, used }(纯增量,与原模块同语义:重启从 0 开始)。 */
|
|
89
|
+
const budgets = new Map()
|
|
90
|
+
const entryOf = (session) => {
|
|
91
|
+
if (session?.id === undefined) return undefined
|
|
92
|
+
let entry = budgets.get(session.id)
|
|
93
|
+
if (entry === undefined) {
|
|
94
|
+
if (budgets.size >= MAX_TRACKED_SESSIONS) budgets.clear()
|
|
95
|
+
entry = { turn: undefined, used: 0 }
|
|
96
|
+
budgets.set(session.id, entry)
|
|
97
|
+
}
|
|
98
|
+
return entry
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
/** 同步查/增;`false` = 本轮预算已尽,调用方按「不命中」放行下游。 */
|
|
102
|
+
take(subject) {
|
|
103
|
+
const session = subject?.session ?? subject?.agent?.session
|
|
104
|
+
// 无会话不设限:与原模块「取不到会话条目即放行」的降级一致。
|
|
105
|
+
const entry = entryOf(session)
|
|
106
|
+
if (entry === undefined) return true
|
|
107
|
+
if (entry.used >= max) return false
|
|
108
|
+
entry.used += 1
|
|
109
|
+
return true
|
|
110
|
+
},
|
|
111
|
+
/** 轮边界重置(取最大值,容忍乱序/重复事件)。 */
|
|
112
|
+
observe(session, event) {
|
|
113
|
+
const turn = event?.data?.turn
|
|
114
|
+
if (typeof turn !== 'number' || !Number.isFinite(turn)) return
|
|
115
|
+
const entry = entryOf(session)
|
|
116
|
+
if (entry === undefined) return
|
|
117
|
+
if (entry.turn === undefined || turn > entry.turn) {
|
|
118
|
+
entry.turn = turn
|
|
119
|
+
entry.used = 0
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 按动作实际阶段求值 when。after-next 先结算下游,再读取当前 epoch,
|
|
127
|
+
* 并向动作提供已结算结果,保证宿主 next 只调用一次;未命中或异常保持下游结果。
|
|
128
|
+
* 每轮预算由各动作在目标匹配且即将生效时同步 claim,不在谓词入口扣减。
|
|
129
|
+
*
|
|
130
|
+
* **同步谓词走同步路径**:`tools/pre-execute` 这类通道的裁决靠「deny 前无 await」保证
|
|
131
|
+
* 并行调用不越过预算(见 `deliberation-gate.mjs` 的注释),无条件 `await` 会破坏它。
|
|
132
|
+
* 返回 Promise 的谓词退回异步路径——声明编译出的谓词恒同步,这条兜底是给手写调用方的。
|
|
133
|
+
*/
|
|
134
|
+
export function withWhen(on, when, warnOnce, phase) {
|
|
135
|
+
if (typeof when !== 'function') return on
|
|
136
|
+
// 注意参数个数:`on` 的签名是 `(event, handler, options)`——包装后的处理器必须占据
|
|
137
|
+
// **第二个**位置,多传一个参数会把 options 挤到错位(首版就踩了这个,被测试抓出)。
|
|
138
|
+
return (event, handler, options) => on(event, (...args) => {
|
|
139
|
+
const nextFree = NEXT_FREE_CHANNELS.has(event)
|
|
140
|
+
const payload = nextFree ? args : args.slice(0, -1)
|
|
141
|
+
const pass = () => (nextFree ? undefined : args[args.length - 1]())
|
|
142
|
+
// 谓词契约是**单个载荷对象**(见 predicates.mjs 头部的统一接口),而处理器收到的是
|
|
143
|
+
// 事件参数表:按通道归一后再交给谓词——否则除 `names` 外,各原语在真实载荷上都
|
|
144
|
+
// 取不到它们要的域(`phase` 曾因此恒为「已晋升」、`count` 恒 0)。
|
|
145
|
+
const evaluate = (invoke, fallback) => {
|
|
146
|
+
const decide = (decided) => decided === true ? invoke() : fallback()
|
|
147
|
+
const failed = (error) => {
|
|
148
|
+
warnOnce(`when predicate failed: ${String(error?.message ?? error)}`)
|
|
149
|
+
return fallback()
|
|
150
|
+
}
|
|
151
|
+
let decided
|
|
152
|
+
try {
|
|
153
|
+
decided = when(subjectOf(event, payload, warnOnce))
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return failed(error)
|
|
156
|
+
}
|
|
157
|
+
return decided !== null && typeof decided === 'object' && typeof decided.then === 'function'
|
|
158
|
+
? decided.then(decide, failed)
|
|
159
|
+
: decide(decided)
|
|
160
|
+
}
|
|
161
|
+
if (!nextFree && phase === 'after-next') {
|
|
162
|
+
return Promise.resolve(pass()).then((result) => evaluate(
|
|
163
|
+
() => handler(...payload, () => result), () => result,
|
|
164
|
+
))
|
|
165
|
+
}
|
|
166
|
+
return evaluate(() => handler(...args), pass)
|
|
167
|
+
}, options)
|
|
168
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { labelOf } from './shared.mjs'
|
|
2
|
+
import { applyAgentRequestParams, matchesAgentScope } from '../layers.mjs'
|
|
3
|
+
|
|
4
|
+
export function prepareRequestParams(action, plugin) {
|
|
5
|
+
const label = labelOf(action)
|
|
6
|
+
const patch = action.patch
|
|
7
|
+
if (patch !== undefined && (patch === null || typeof patch !== 'object' || Array.isArray(patch))) {
|
|
8
|
+
throw new TypeError(`${plugin}: ${label}.patch must be an object`)
|
|
9
|
+
}
|
|
10
|
+
const unset = action.unset
|
|
11
|
+
if (unset !== undefined && (unset === null || typeof unset !== 'object' || Array.isArray(unset))) {
|
|
12
|
+
throw new TypeError(`${plugin}: ${label}.unset must be an object of { key: expectedValue }`)
|
|
13
|
+
}
|
|
14
|
+
if (action.replace === true && unset !== undefined) {
|
|
15
|
+
throw new TypeError(`${plugin}: ${label} cannot combine replace with unset — 整体替换没有可比较的下游值`)
|
|
16
|
+
}
|
|
17
|
+
const params = { ...(patch !== undefined ? { patch } : {}), ...(unset !== undefined ? { unset } : {}), ...(action.replace === true ? { replace: true } : {}) }
|
|
18
|
+
return (_ctx, { warnOnce, on, collect, take }) => collect(on('agent/request', async (payload, next) => {
|
|
19
|
+
const base = await next()
|
|
20
|
+
try {
|
|
21
|
+
if (!matchesAgentScope(action, payload?.agent)) return base
|
|
22
|
+
const result = applyAgentRequestParams(params, base)
|
|
23
|
+
const keys = Object.keys(result)
|
|
24
|
+
const changed = base == null || keys.length !== Object.keys(base).length
|
|
25
|
+
|| keys.some((key) => !Object.hasOwn(base, key) || !Object.is(result[key], base[key]))
|
|
26
|
+
return changed && take(payload) ? result : base
|
|
27
|
+
} catch (error) {
|
|
28
|
+
warnOnce(`${plugin}: request-params action ${label} failed: ${String(error?.message ?? error)}`)
|
|
29
|
+
return base
|
|
30
|
+
}
|
|
31
|
+
}))
|
|
32
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { labelOf, createMask } from './shared.mjs'
|
|
2
|
+
import { SDK_SECTION_NAME, sdkToolNames, stripSdkDeclarations } from '../sdk-strip.mjs'
|
|
3
|
+
|
|
4
|
+
export function prepareSdkStrip(action, plugin) {
|
|
5
|
+
const label = labelOf(action)
|
|
6
|
+
const mask = createMask(action.mask ?? action, `${label}.mask`, plugin)
|
|
7
|
+
return (_ctx, { warnOnce, on, collect, take }) => collect(on('system-prompt/assemble', async (assembly, context, next) => {
|
|
8
|
+
// Downstream errors propagate untouched; only this action's own logic is guarded.
|
|
9
|
+
const assembled = await next()
|
|
10
|
+
try {
|
|
11
|
+
if (!Array.isArray(assembled.sections)) return assembled
|
|
12
|
+
let changed = false
|
|
13
|
+
const sections = assembled.sections.map((section) => {
|
|
14
|
+
if (section?.name !== SDK_SECTION_NAME || typeof section.text !== 'string') return section
|
|
15
|
+
const names = mask.allow === undefined
|
|
16
|
+
? mask.deny
|
|
17
|
+
: new Set([...sdkToolNames(section.text)].filter((toolName) => mask.blocks(toolName)))
|
|
18
|
+
if (names === undefined || names.size === 0) return section
|
|
19
|
+
const stripped = stripSdkDeclarations(section.text, names)
|
|
20
|
+
if (stripped === section.text) return section
|
|
21
|
+
changed = true
|
|
22
|
+
return { ...section, text: stripped }
|
|
23
|
+
})
|
|
24
|
+
return changed && take(context) ? { ...assembled, sections } : assembled
|
|
25
|
+
} catch (error) {
|
|
26
|
+
warnOnce(`${plugin}: sdk-strip action ${label} failed, keeping the original SDK text: ${String(error?.message ?? error)}`)
|
|
27
|
+
return assembled
|
|
28
|
+
}
|
|
29
|
+
}))
|
|
30
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { stringList } from '../fields.mjs'
|
|
2
|
+
import { parseToolNames } from '../shared.mjs'
|
|
3
|
+
export const RUN_CODE = 'run_code'
|
|
4
|
+
|
|
5
|
+
export const NAME_LIST = stringList({ as: 'set', dedupe: true, allowEmpty: true })
|
|
6
|
+
|
|
7
|
+
/** 动作标签(错误消息与 source.plugin 命名空间)。 */
|
|
8
|
+
export const labelOf = (action) => (typeof action?.id === 'string' && action.id.length > 0 ? action.id : String(action?.kind ?? 'action'))
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 名单判据的唯一实现:presentation 过滤与执行 guard 共用(呈现与执行不得两套口径)。
|
|
12
|
+
*
|
|
13
|
+
* 第四个参数 `allowExtra` 是**可选的活集合**(`assembly.tools.allowFrom` 的动态白名单):
|
|
14
|
+
* 调用方持有它,在每个请求解析完历史工具调用后 `clear()` + 填充。它**只做加法**——多一个
|
|
15
|
+
* 名字就多一个工具通过,永远不能剔除任何工具。这不是巧合,而是判据方向决定的:`blocks()`
|
|
16
|
+
* 的语义是「**在** allow 里就放行」,所以任何动态集合都只可能解锁。
|
|
17
|
+
*
|
|
18
|
+
* 未声明静态 `allow`、只声明 `allowFrom` 时,allow 视为空集(`blocks` 仍会裁掉没被解锁的)。
|
|
19
|
+
*/
|
|
20
|
+
export function createMask(source, label, plugin, allowExtra) {
|
|
21
|
+
const allow = NAME_LIST.parse(source?.allow, plugin, `${label}.allow`)
|
|
22
|
+
const deny = NAME_LIST.parse(source?.deny, plugin, `${label}.deny`)
|
|
23
|
+
if (allow !== undefined && deny !== undefined) {
|
|
24
|
+
throw new TypeError(`${plugin}: ${label} cannot combine allow with deny`)
|
|
25
|
+
}
|
|
26
|
+
if (allow === undefined && deny === undefined && allowExtra === undefined) {
|
|
27
|
+
throw new TypeError(`${plugin}: ${label} needs allow and/or deny — 空名单无法表达「剔哪些工具」`)
|
|
28
|
+
}
|
|
29
|
+
// `run_code` 是 PTC 呈现**唯一**的可调用入口(见本文件顶部)。把它写进 **deny** 名单会让
|
|
30
|
+
// 整个 PTC 面失效,一律挂载期拒绝。写进 **allow** 名单相反是**必需**的:allow 是
|
|
31
|
+
// fail-closed(未列出即剔出),不点名它就会被连带剔掉——PTC 预设因此失去唯一入口。
|
|
32
|
+
// 2026-09-22 用户拍板:allow 模式允许点名它(旧模块本就能把它写进白名单)。
|
|
33
|
+
if (deny?.has(RUN_CODE)) {
|
|
34
|
+
throw new TypeError(`${plugin}: ${label}.deny must not name the reserved ${RUN_CODE} transport — it is the only callable entry of the PTC presentation`)
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
/** 静态 allow(原样保留;动态项不并入,免得调用方误以为它是静态声明)。 */
|
|
38
|
+
allow,
|
|
39
|
+
deny,
|
|
40
|
+
/** 动态白名单的落点(活集合,由调用方每轮重填);未声明 allowFrom 时不存在。 */
|
|
41
|
+
allowExtra,
|
|
42
|
+
/**
|
|
43
|
+
* fail-open 兜底开关(**可选**,缺省 false):原 `tool-bootstrap` 的语义是
|
|
44
|
+
* 「keep 名单里的工具在本次装配目录里一个都不存在 → 放弃裁剪、暴露完整目录」(声明侧见
|
|
45
|
+
* `test/engine/declarations/tool-bootstrap.yml` 的 `requireMatch: true`),而原
|
|
46
|
+
* `tool-filter` 的同名场景是**照名单裁成空目录**。两种语义都真实存在,所以这里
|
|
47
|
+
* **不设默认**——要兜底的声明自己写 `requireMatch: true`。把某一个模块的特有语义当成
|
|
48
|
+
* 通用默认,会让另一个声明路径的行为被悄悄改掉(这正是对拍抓出来的)。
|
|
49
|
+
*/
|
|
50
|
+
requireMatch: source?.requireMatch === true,
|
|
51
|
+
/** 与 tool-filter 的名单判据 / Layer.admits 同一判据(tool-filter 模块已删除,判据保留)。 */
|
|
52
|
+
blocks: (toolName) => {
|
|
53
|
+
if (typeof toolName !== 'string' || toolName.length === 0) return false
|
|
54
|
+
if (deny !== undefined && deny.has(toolName)) return true
|
|
55
|
+
if (allowExtra?.has(toolName) === true) return false
|
|
56
|
+
// 声明了 allow、或声明了 allowFrom(= 白名单模式,静态名单视为空集),未点名即裁掉。
|
|
57
|
+
return (allow !== undefined || allowExtra !== undefined) && allow?.has(toolName) !== true
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function requireString(value, label, plugin) {
|
|
63
|
+
if (typeof value !== 'string') throw new TypeError(`${plugin}: ${label} must be a string`)
|
|
64
|
+
return value
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function toolNameSet(value, plugin, field) {
|
|
68
|
+
if (value === undefined) return undefined
|
|
69
|
+
const names = NAME_LIST.parse(typeof value === 'string' ? parseToolNames(value) : value, plugin, field)
|
|
70
|
+
return names !== undefined && names.size > 0 ? names : undefined
|
|
71
|
+
}
|