dsh-subagent-profile 0.3.2 → 0.3.3
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/README.zh.md +38 -29
- package/index.mjs +188 -62
- package/lib/client.js +1240 -47
- package/lib/core/catalog-cache.mjs +45 -7
- package/lib/core/catalog.mjs +6 -6
- package/lib/core/cost-guard.mjs +71 -44
- package/lib/core/decision-trace.mjs +433 -0
- package/lib/core/delegation.mjs +106 -48
- package/lib/core/dispatch-gates.mjs +146 -0
- package/lib/core/dispatch-guard.mjs +150 -0
- package/lib/core/dispatch-schema.mjs +98 -14
- package/lib/core/dispatch-tool.mjs +208 -199
- package/lib/core/escape.mjs +130 -0
- package/lib/core/evolution-ledger.mjs +289 -0
- package/lib/core/evolution-summary.mjs +432 -0
- package/lib/core/http-routes.mjs +155 -40
- package/lib/core/intersection.mjs +6 -9
- package/lib/core/presets-sync.mjs +256 -136
- package/lib/core/profile-provider.mjs +41 -39
- package/lib/core/profiles-store.mjs +103 -76
- package/lib/core/pure.mjs +110 -66
- package/lib/core/shims.mjs +56 -75
- package/lib/core/whitelist.mjs +23 -17
- package/package.json +2 -3
- package/presets/orchestrator/agent.cordis.yml +243 -271
- package/presets/orchestrator/NOTICE +0 -3
package/lib/core/http-routes.mjs
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
// lib/core/http-routes.mjs —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// walk the llm directory themselves.
|
|
1
|
+
// lib/core/http-routes.mjs — Client UI 的设置 HTTP loopback 路由。
|
|
2
|
+
// 读路由(list / options 摘要 / 按模型 efforts / tools)+ 写路由
|
|
3
|
+
// (set-enabled / add / remove / reset / reset-all / set-profile-enabled)。
|
|
4
|
+
// /options 目录(models / presets / tools / efforts)由共享 catalog 缓存
|
|
5
|
+
// (lib/core/catalog-cache.mjs)提供,这些路由不再自行遍历 llm 目录。
|
|
7
6
|
//
|
|
8
|
-
//
|
|
9
|
-
// store
|
|
10
|
-
// persistEnabled / deletedBuiltins
|
|
11
|
-
// getEnabled
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// exact original registration shape: effect(() => register + disposer).
|
|
7
|
+
// 注入:所有 apply 闭包 / ctx 依赖都是显式参数——
|
|
8
|
+
// store profile store(profiles Map / persistProfiles /
|
|
9
|
+
// persistEnabled / deletedBuiltins),
|
|
10
|
+
// getEnabled 读取 apply 闭包 `enabled` 标志(被 /set-enabled 改写),
|
|
11
|
+
// setEnabled 写它,
|
|
12
|
+
// syncTool 在 /set-enabled 时注销/注册 dispatch 工具,
|
|
13
|
+
// catalog 共享 catalog 缓存(getSnapshot / invalidate),
|
|
14
|
+
// logger ctx.logger(路由错误告警)。
|
|
15
|
+
// 工厂返回 scope.effect 设置函数,调用方保持原有注册形状:
|
|
16
|
+
// effect(() => register + disposer)。
|
|
19
17
|
//
|
|
20
18
|
// 写路由各抽为模块级处理函数(if 链分发保持);/options 拆三读路由 + 手动刷新,
|
|
21
19
|
// 目录数据统一来自 catalog 快照。
|
|
@@ -24,9 +22,18 @@ import { sanitizeProfile } from './pure.mjs';
|
|
|
24
22
|
import { BUILTIN_SEEDS } from './profiles-store.mjs';
|
|
25
23
|
import { detectVersions } from './shims.mjs';
|
|
26
24
|
|
|
27
|
-
//
|
|
25
|
+
// 只有 loopback 接口可以驱动设置 HTTP 路由。
|
|
28
26
|
const LOOPBACKS = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
|
|
29
27
|
|
|
28
|
+
// 写路由 CSRF 传输授权三件套。loopback 只挡「来源不是本机」;这三件挡「浏览器里的
|
|
29
|
+
// 跨域表单/脚本冒充本机提交」——schema 校验的是数据形状,管不到谁有权写。
|
|
30
|
+
// URL.hostname 对 IPv6 字面量返回带方括号形态(http://[::1]:port → '[::1]'),
|
|
31
|
+
// 两种形态都列入,避免 IPv6 loopback 源被误拒。
|
|
32
|
+
const LOOPBACK_ORIGIN_HOSTS = new Set(['127.0.0.1', 'localhost', '::1', '[::1]']);
|
|
33
|
+
const CSRF_HEADER = 'X-DSH-Plugin';
|
|
34
|
+
const CSRF_HEADER_KEY = CSRF_HEADER.toLowerCase();
|
|
35
|
+
const CSRF_HEADER_VALUE = 'dsh-subagent-profile';
|
|
36
|
+
|
|
30
37
|
function json(res, code, data) {
|
|
31
38
|
res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
32
39
|
res.end(JSON.stringify(data));
|
|
@@ -48,10 +55,9 @@ function readBody(req) {
|
|
|
48
55
|
});
|
|
49
56
|
}
|
|
50
57
|
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
// disk did not update). The client renders that as the amber warning.
|
|
58
|
+
// 写失败契约:写路由返回 HTTP 200 且 `persisted` 恒在场;磁盘写失败时
|
|
59
|
+
// persistWarning 说明「已保存但未持久化」(内存态驱动本进程,磁盘未更新)。
|
|
60
|
+
// 客户端把它渲染为琥珀色警告。
|
|
55
61
|
function persistOk(res, payload, persist) {
|
|
56
62
|
return json(res, 200, {
|
|
57
63
|
ok: true,
|
|
@@ -65,8 +71,8 @@ function listClean(store) {
|
|
|
65
71
|
return [...store.profiles.values()].map((profile) => {
|
|
66
72
|
const clean = {};
|
|
67
73
|
for (const [key, value] of Object.entries(profile)) if (value !== undefined && key !== 'persisted') clean[key] = value;
|
|
68
|
-
//
|
|
69
|
-
//
|
|
74
|
+
// 内部 `persisted` 标志已在上方剥离;对外暴露 UI 面向的 "modified"
|
|
75
|
+
// 信号,供重置面板标注被改过的内置方案。
|
|
70
76
|
if (profile.builtin === true && profile.persisted === true) clean.modified = true;
|
|
71
77
|
return clean;
|
|
72
78
|
});
|
|
@@ -78,10 +84,20 @@ async function handleList(deps, res) {
|
|
|
78
84
|
return json(res, 200, { ok: true, profiles: listClean(deps.store) });
|
|
79
85
|
}
|
|
80
86
|
|
|
81
|
-
// 轻量摘要:enabled + 模型目录(含 provider 信息,客户端据此派生提供方列表)+
|
|
87
|
+
// 轻量摘要:enabled + evolutionAdvice + 模型目录(含 provider 信息,客户端据此派生提供方列表)+ 预设名册
|
|
88
|
+
// + 审计分级(lostTelemetry/lostGovernance/health,供设置页红字与丢失计数展示)。
|
|
82
89
|
async function handleSummary(deps, res) {
|
|
83
90
|
const snapshot = await deps.catalog.getSnapshot();
|
|
84
|
-
return json(res, 200, {
|
|
91
|
+
return json(res, 200, {
|
|
92
|
+
ok: true,
|
|
93
|
+
enabled: deps.getEnabled(),
|
|
94
|
+
evolutionAdvice: deps.getEvolutionAdvice(),
|
|
95
|
+
models: snapshot.models,
|
|
96
|
+
presets: snapshot.presets,
|
|
97
|
+
audit: deps.getAudit(),
|
|
98
|
+
escapeEnabled: deps.getEscapeEnabled(),
|
|
99
|
+
escapePresets: deps.escape.list(),
|
|
100
|
+
});
|
|
85
101
|
}
|
|
86
102
|
|
|
87
103
|
// 每模型 reasoning-effort 等级(懒加载;model 命中快照的 efforts 表)。
|
|
@@ -104,26 +120,82 @@ function handleVersions(res) {
|
|
|
104
120
|
return json(res, 200, { ok: true, versions, warnings });
|
|
105
121
|
}
|
|
106
122
|
|
|
107
|
-
// 手动刷新:清缓存兜底(TTL
|
|
123
|
+
// 手动刷新:清缓存兜底(TTL 过期前的目录变更经此立即生效)+ 重算 evolution:advice
|
|
124
|
+
// 聚合(T1:/options/refresh 此前不触发聚合,summaries.json 永无生成路径)。
|
|
108
125
|
async function handleRefresh(deps, res) {
|
|
109
126
|
deps.catalog.invalidate();
|
|
127
|
+
if (typeof deps.refreshAdvice === 'function') deps.refreshAdvice();
|
|
110
128
|
return json(res, 200, { ok: true, refreshed: true });
|
|
111
129
|
}
|
|
112
130
|
|
|
131
|
+
// 失败台账读取:GET /ledger/failures?session=<id> → { ok:true, failures:[...] }。
|
|
132
|
+
// 未知 session 返回空数组;台账是会话级内存结构,进程重启即失。
|
|
133
|
+
async function handleLedgerFailures(deps, url, res) {
|
|
134
|
+
const session = url.searchParams.get('session') ?? '';
|
|
135
|
+
return json(res, 200, { ok: true, failures: deps.ledger.get(session) });
|
|
136
|
+
}
|
|
137
|
+
|
|
113
138
|
async function handleSetEnabled(deps, req, res) {
|
|
114
139
|
const body = await readBody(req);
|
|
115
140
|
const next = !!(body && body.enabled === true);
|
|
116
141
|
deps.setEnabled(next);
|
|
117
|
-
deps.store.persistEnabled(next);
|
|
142
|
+
const persist = deps.store.persistEnabled(next);
|
|
118
143
|
deps.syncTool();
|
|
119
|
-
return
|
|
144
|
+
return persistOk(res, { enabled: deps.getEnabled() }, persist);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 只读建议注入开关(默认关):改写 apply 闭包 evolutionAdvice 并持久化到 state.json。
|
|
148
|
+
// 不改派发行为(建议只读提示);写路由 CSRF 三件套自动生效。
|
|
149
|
+
async function handleSetEvolutionAdvice(deps, req, res) {
|
|
150
|
+
const body = await readBody(req);
|
|
151
|
+
const next = !!(body && body.advice === true);
|
|
152
|
+
deps.setEvolutionAdvice(next);
|
|
153
|
+
const persist = deps.store.persistEvolutionAdvice(next);
|
|
154
|
+
return persistOk(res, { evolutionAdvice: deps.getEvolutionAdvice() }, persist);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 逃生舱开关(默认关):改写 apply 闭包 escapeEnabled 并持久化到 state.json。
|
|
158
|
+
// 只放行「放行集内且其余三道闸全过」的非 system 预设;非成员即便开关开也恒拒。
|
|
159
|
+
async function handleSetEscape(deps, req, res) {
|
|
160
|
+
const body = await readBody(req);
|
|
161
|
+
const next = !!(body && body.enabled === true);
|
|
162
|
+
deps.setEscapeEnabled(next);
|
|
163
|
+
const persist = deps.store.persistEscapeEnabled(next);
|
|
164
|
+
return persistOk(res, { escapeEnabled: deps.getEscapeEnabled() }, persist);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 放行集写路径校验:解析请求体的 preset id(非字符串/缺失归一为空串,由调用方
|
|
168
|
+
// 400 拒绝)。纯同步、无副作用。
|
|
169
|
+
function escapePresetId(body) {
|
|
170
|
+
const raw = body && typeof body === 'object' && typeof body.preset === 'string' ? body.preset : '';
|
|
171
|
+
return raw.trim();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function handleEscapeAdd(deps, req, res) {
|
|
175
|
+
const body = await readBody(req);
|
|
176
|
+
const preset = escapePresetId(body);
|
|
177
|
+
if (preset === '') return json(res, 400, { ok: false, error: 'subagent-profiles: escape preset 必须为非空字符串' });
|
|
178
|
+
const snapshot = await deps.catalog.getSnapshot();
|
|
179
|
+
if (snapshot.presets.some((p) => p && p.id === preset)) {
|
|
180
|
+
return json(res, 400, { ok: false, error: `subagent-profiles: preset "${preset}" 已是 system-trust,无需加入逃生舱` });
|
|
181
|
+
}
|
|
182
|
+
const persist = deps.escape.add(preset);
|
|
183
|
+
return persistOk(res, { preset, presets: deps.escape.list() }, persist);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function handleEscapeRemove(deps, req, res) {
|
|
187
|
+
const body = await readBody(req);
|
|
188
|
+
const preset = escapePresetId(body);
|
|
189
|
+
if (preset === '') return json(res, 400, { ok: false, error: 'subagent-profiles: escape preset 必须为非空字符串' });
|
|
190
|
+
const persist = deps.escape.remove(preset);
|
|
191
|
+
return persistOk(res, { preset, presets: deps.escape.list() }, persist);
|
|
120
192
|
}
|
|
121
193
|
|
|
122
194
|
async function handleAdd(deps, req, res) {
|
|
123
195
|
const body = await readBody(req);
|
|
124
196
|
const profile = body && typeof body === 'object' ? body : {};
|
|
125
197
|
if (typeof profile.id !== 'string' || profile.id.length === 0) {
|
|
126
|
-
return json(res, 400, { ok: false, error: 'subagent-profiles: profile id
|
|
198
|
+
return json(res, 400, { ok: false, error: 'subagent-profiles: profile id 必须为非空字符串' });
|
|
127
199
|
}
|
|
128
200
|
// 写路径上限:strict=true —— 超限/非法字段直接 400 拒绝,
|
|
129
201
|
// 与 loadProfiles(strict=false 迁移宽松读取)的行为区分。列被拒字段与中文原因。
|
|
@@ -141,9 +213,8 @@ async function handleAdd(deps, req, res) {
|
|
|
141
213
|
const existing = deps.store.profiles.get(clean.id);
|
|
142
214
|
const seed = BUILTIN_SEEDS.find((s) => s.id === clean.id);
|
|
143
215
|
const isBuiltin = (existing !== undefined && existing.builtin === true) || seed !== undefined;
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
// persona/preset) survive an edit or a re-add.
|
|
216
|
+
// 合并而非替换:从现有 profile(或它被删时的 seed)出发,表单未带的字段
|
|
217
|
+
// (如内置方案的 persona/preset)在编辑或重新添加后仍然保留。
|
|
147
218
|
const merged = { ...(existing ?? seed ?? {}) };
|
|
148
219
|
merged.id = clean.id;
|
|
149
220
|
for (const key of ['name', 'description', 'preset', 'provider', 'model', 'reasoningEffort', 'persona', 'enabled']) {
|
|
@@ -173,7 +244,7 @@ async function handleRemove(deps, req, res) {
|
|
|
173
244
|
const id = body && typeof body === 'object' && typeof body.id === 'string' ? body.id : '';
|
|
174
245
|
const existing = deps.store.profiles.get(id);
|
|
175
246
|
if (existing === undefined) {
|
|
176
|
-
return json(res, 404, { ok: false, error: `subagent-profiles: profile "${id}"
|
|
247
|
+
return json(res, 404, { ok: false, error: `subagent-profiles: profile "${id}" 不存在(可在设置页确认可用的 profile id)` });
|
|
177
248
|
}
|
|
178
249
|
deps.store.profiles.delete(id);
|
|
179
250
|
if (existing.builtin === true) deps.store.deletedBuiltins.add(id);
|
|
@@ -185,7 +256,7 @@ async function handleReset(deps, req, res) {
|
|
|
185
256
|
const id = body && typeof body === 'object' && typeof body.id === 'string' ? body.id : '';
|
|
186
257
|
const seed = BUILTIN_SEEDS.find((s) => s.id === id);
|
|
187
258
|
if (seed === undefined) {
|
|
188
|
-
return json(res, 404, { ok: false, error: `subagent-profiles: profile "${id}"
|
|
259
|
+
return json(res, 404, { ok: false, error: `subagent-profiles: profile "${id}" 不是内置方案(无可重置项)` });
|
|
189
260
|
}
|
|
190
261
|
deps.store.profiles.set(id, { ...seed });
|
|
191
262
|
deps.store.deletedBuiltins.delete(id);
|
|
@@ -205,17 +276,45 @@ async function handleSetProfileEnabled(deps, req, res) {
|
|
|
205
276
|
const id = body && typeof body === 'object' && typeof body.id === 'string' ? body.id : '';
|
|
206
277
|
const existing = deps.store.profiles.get(id);
|
|
207
278
|
if (existing === undefined) {
|
|
208
|
-
return json(res, 404, { ok: false, error: `subagent-profiles: profile "${id}"
|
|
279
|
+
return json(res, 404, { ok: false, error: `subagent-profiles: profile "${id}" 不存在(可在设置页确认可用的 profile id)` });
|
|
209
280
|
}
|
|
210
281
|
existing.enabled = body && body.enabled === false ? false : true;
|
|
211
|
-
//
|
|
212
|
-
//
|
|
282
|
+
// 无条件持久化(不只是内置方案):运行时注册的 profile 的启用/禁用也必须
|
|
283
|
+
// 在重启后保留。
|
|
213
284
|
existing.persisted = true;
|
|
214
285
|
return persistOk(res, { id, enabled: existing.enabled }, deps.store.persistProfiles());
|
|
215
286
|
}
|
|
216
287
|
|
|
217
|
-
|
|
218
|
-
|
|
288
|
+
// 写路由 CSRF 传输授权:Origin 白名单 + Content-Type + 自定义头三道检查。
|
|
289
|
+
// 返回 null 表示放行;否则返回 { reason, origin, contentType }——reason 进 403
|
|
290
|
+
// 响应与 warn 日志,origin/contentType 只进日志(二者本就是请求头,非秘密)。
|
|
291
|
+
// GET 只读路由不经过此处(只读无状态变更,不设防)。
|
|
292
|
+
function csrfViolation(req) {
|
|
293
|
+
const headers = req.headers ?? {};
|
|
294
|
+
const origin = typeof headers.origin === 'string' ? headers.origin : '';
|
|
295
|
+
const contentType = typeof headers['content-type'] === 'string' ? headers['content-type'] : '';
|
|
296
|
+
// 1. Origin 白名单:带 Origin 时只放行本机源(hostname 宽松判定,端口不限);
|
|
297
|
+
// 无 Origin 的本地脚本/curl 跳过此道,靠第 3 道自定义头兜底。
|
|
298
|
+
if (origin !== '') {
|
|
299
|
+
let host;
|
|
300
|
+
try { host = new URL(origin).hostname; } catch { host = null; }
|
|
301
|
+
if (host === null || !LOOPBACK_ORIGIN_HOSTS.has(host)) {
|
|
302
|
+
return { reason: `Origin 不在本机白名单(收到:${origin})`, origin, contentType };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// 2. 强制 Content-Type: application/json(text/plain、表单编码等一律拒绝)。
|
|
306
|
+
if (!contentType.toLowerCase().startsWith('application/json')) {
|
|
307
|
+
return { reason: `写请求必须带 Content-Type: application/json(收到:${contentType || '缺失'})`, origin, contentType };
|
|
308
|
+
}
|
|
309
|
+
// 3. 自定义头逼 preflight:跨域表单/脚本无法携带非简单头,缺头即拒。
|
|
310
|
+
if (headers[CSRF_HEADER_KEY] !== CSRF_HEADER_VALUE) {
|
|
311
|
+
return { reason: `写请求必须带 ${CSRF_HEADER}: ${CSRF_HEADER_VALUE} 请求头`, origin, contentType };
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function createHttpRoutes({ webServer, store, getEnabled, setEnabled, syncTool, catalog, ledger, getAudit, getEvolutionAdvice, setEvolutionAdvice, getEscapeEnabled, setEscapeEnabled, escape, refreshAdvice, logger }) {
|
|
317
|
+
const deps = { store, getEnabled, setEnabled, syncTool, catalog, ledger, getAudit, getEvolutionAdvice, setEvolutionAdvice, getEscapeEnabled, setEscapeEnabled, escape, refreshAdvice, logger };
|
|
219
318
|
// 路由分发(if 链保持,判断顺序与 404/500 兜底不变)。
|
|
220
319
|
const handler = async (req, res) => {
|
|
221
320
|
const remote = req.socket?.remoteAddress;
|
|
@@ -223,13 +322,29 @@ export function createHttpRoutes({ webServer, store, getEnabled, setEnabled, syn
|
|
|
223
322
|
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
224
323
|
const sub = (url.pathname.replace(/^\/subagent-profiles/, '') || '/').replace(/\/+$/, '') || '/';
|
|
225
324
|
try {
|
|
325
|
+
// 写路由统一做 CSRF 传输授权(loopback 之后、路由分发之前);GET 只读不设防。
|
|
326
|
+
if (req.method === 'POST') {
|
|
327
|
+
const violation = csrfViolation(req);
|
|
328
|
+
if (violation !== null) {
|
|
329
|
+
deps.logger.warn(
|
|
330
|
+
`[dsh-subagent-profile] CSRF 防护拒绝:${violation.reason}` +
|
|
331
|
+
`(origin=${violation.origin || '无'},contentType=${violation.contentType || '无'})`
|
|
332
|
+
);
|
|
333
|
+
return json(res, 403, { ok: false, error: `CSRF 防护拒绝:${violation.reason}` });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
226
336
|
if (req.method === 'GET' && (sub === '/' || sub === '/list')) return handleList(deps, res);
|
|
227
337
|
if (req.method === 'GET' && sub === '/options/summary') return handleSummary(deps, res);
|
|
228
338
|
if (req.method === 'GET' && sub === '/options/versions') return handleVersions(res);
|
|
229
339
|
if (req.method === 'GET' && sub === '/options/efforts') return handleEfforts(deps, url, res);
|
|
230
340
|
if (req.method === 'GET' && sub === '/options/tools') return handleTools(deps, res);
|
|
341
|
+
if (req.method === 'GET' && sub === '/ledger/failures') return handleLedgerFailures(deps, url, res);
|
|
231
342
|
if (req.method === 'POST' && sub === '/options/refresh') return handleRefresh(deps, res);
|
|
232
343
|
if (req.method === 'POST' && sub === '/set-enabled') return handleSetEnabled(deps, req, res);
|
|
344
|
+
if (req.method === 'POST' && sub === '/set-evolution-advice') return handleSetEvolutionAdvice(deps, req, res);
|
|
345
|
+
if (req.method === 'POST' && sub === '/set-escape') return handleSetEscape(deps, req, res);
|
|
346
|
+
if (req.method === 'POST' && sub === '/escape-add') return handleEscapeAdd(deps, req, res);
|
|
347
|
+
if (req.method === 'POST' && sub === '/escape-remove') return handleEscapeRemove(deps, req, res);
|
|
233
348
|
if (req.method === 'POST' && sub === '/add') return handleAdd(deps, req, res);
|
|
234
349
|
if (req.method === 'POST' && sub === '/remove') return handleRemove(deps, req, res);
|
|
235
350
|
if (req.method === 'POST' && sub === '/reset') return handleReset(deps, req, res);
|
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
// lib/core/intersection.mjs — tool intersection(安全门 1)纯函数核心,从 index.mjs
|
|
2
2
|
// 的 provider start 拆出。无 @deepseek-ai 依赖。此处只做 allow 收窄计算;
|
|
3
3
|
// 调用方(provider start)保留空集 fail-loud throw(错误文案逐字不变)与
|
|
4
4
|
// restrict 的 try/catch 包裹。
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// parent
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// toolset (parentNames ∩ childNames), so it stays correct even when the child
|
|
12
|
-
// composes a different toolset. The two serve different call paths and safety
|
|
13
|
-
// guarantees and must NOT be merged.
|
|
6
|
+
// 与 lib/core/pure.mjs 的 computeContinuableAllow(parentNames, toolFilter) 的关系:
|
|
7
|
+
// continuable 变体没有 childNames——它假设子工具集 ≈ 父工具集(continuable 继承
|
|
8
|
+
// 父预设,那里无法重算真正的父∩子交集),计算 parent − run_code − deny → allow。
|
|
9
|
+
// computeEffectiveAllow 额外与真实子工具集求交(parentNames ∩ childNames),
|
|
10
|
+
// 子组合了不同工具集时仍然正确。两者服务于不同的调用路径与安全保证,不得合并。
|
|
14
11
|
|
|
15
12
|
// parent∩child − run_code − deny → allow 收窄;空集返回 [](fail-loud 由调用方
|
|
16
13
|
// provider start 以逐字不变的 error 文案 throw)。
|