dsh-yolo-mode 0.4.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/CHANGELOG.md +62 -0
- package/LICENSE +21 -0
- package/README.md +130 -0
- package/lib/bridge-entry.js +51 -0
- package/lib/client/index.js +1268 -0
- package/lib/index.js +306 -0
- package/lib/judge.js +240 -0
- package/lib/policy.js +363 -0
- package/lib/remote.js +489 -0
- package/lib/settings.js +102 -0
- package/lib/state.js +92 -0
- package/package.json +73 -0
package/lib/policy.js
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-yolo-mode —— 策略层(纯函数,零依赖零 import)
|
|
3
|
+
*
|
|
4
|
+
* 对应 design.md 第 2 节(配置 Schema)与第 4 节(策略层契约)。
|
|
5
|
+
* 全部导出为纯函数与冻结常量,可独立单测;任何非法输入 fail-loud 抛出。
|
|
6
|
+
*
|
|
7
|
+
* @module lib/policy.js
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** 内置预设枚举(冻结) */
|
|
11
|
+
export const PRESETS = Object.freeze(['off', 'strict', 'balanced', 'permissive', 'yolo', 'custom'])
|
|
12
|
+
|
|
13
|
+
/** 可用的权限策略(冻结) */
|
|
14
|
+
export const POLICIES = Object.freeze(['allow', 'judge', 'delegate', 'deny'])
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 沙箱升权申请 reason 匹配(design.md §0/§4)。
|
|
18
|
+
* 格式经本机 `dsh-sandbox/lib/index.js:101` 核实:
|
|
19
|
+
* `escalate sandbox to ${mode}: ${justification}`,mode ∈ workspace-write | danger-full-access。
|
|
20
|
+
*/
|
|
21
|
+
export const ESCALATION_RE =
|
|
22
|
+
/^escalate sandbox to (workspace-write|danger-full-access): (.+)$/
|
|
23
|
+
|
|
24
|
+
/** 合法沙箱文件模式(冻结) */
|
|
25
|
+
const SANDBOX_MODES = Object.freeze(['read-only', 'workspace-write', 'danger-full-access'])
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 内置预设表(design.md §4,非 custom 使用)。
|
|
29
|
+
* 目标模式 → 策略:
|
|
30
|
+
* off → 全部 delegate;strict → ws-write=judge, dfa=delegate;
|
|
31
|
+
* balanced/permissive → 全部 judge;yolo → 全部 allow。
|
|
32
|
+
*/
|
|
33
|
+
const PRESET_TABLE = Object.freeze({
|
|
34
|
+
off: Object.freeze({ 'workspace-write': 'delegate', 'danger-full-access': 'delegate' }),
|
|
35
|
+
strict: Object.freeze({ 'workspace-write': 'judge', 'danger-full-access': 'delegate' }),
|
|
36
|
+
balanced: Object.freeze({ 'workspace-write': 'judge', 'danger-full-access': 'judge' }),
|
|
37
|
+
permissive: Object.freeze({ 'workspace-write': 'judge', 'danger-full-access': 'judge' }),
|
|
38
|
+
yolo: Object.freeze({ 'workspace-write': 'allow', 'danger-full-access': 'allow' }),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 六预设的默认 levels(design.md §2;设置页预填充/预览用,含 error/unsure 回退行)。
|
|
43
|
+
* 与 PRESET_TABLE 的模式行一致,另按 judgeFallback 语义补齐回退行:
|
|
44
|
+
* strict → error='deny'、unsure='delegate'(strict+error → rejected);
|
|
45
|
+
* permissive → unsure='allow'(permissive+unsure → allowed-once);
|
|
46
|
+
* off/balanced/yolo → 未声明回退行(judgeFallback 缺省 delegate);
|
|
47
|
+
* custom → 空对象(用户自填)。
|
|
48
|
+
*/
|
|
49
|
+
const DEFAULT_LEVELS = Object.freeze({
|
|
50
|
+
off: Object.freeze({ 'workspace-write': 'delegate', 'danger-full-access': 'delegate' }),
|
|
51
|
+
strict: Object.freeze({
|
|
52
|
+
'workspace-write': 'judge',
|
|
53
|
+
'danger-full-access': 'delegate',
|
|
54
|
+
error: 'deny',
|
|
55
|
+
unsure: 'delegate',
|
|
56
|
+
}),
|
|
57
|
+
balanced: Object.freeze({ 'workspace-write': 'judge', 'danger-full-access': 'judge' }),
|
|
58
|
+
permissive: Object.freeze({
|
|
59
|
+
'workspace-write': 'judge',
|
|
60
|
+
'danger-full-access': 'judge',
|
|
61
|
+
unsure: 'allow',
|
|
62
|
+
}),
|
|
63
|
+
yolo: Object.freeze({ 'workspace-write': 'allow', 'danger-full-access': 'allow' }),
|
|
64
|
+
custom: Object.freeze({}),
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 返回某预设的默认 levels 对象(design.md §2;设置页选中预设时预填充)。
|
|
69
|
+
* off/balanced/yolo → 模式行(error/unsure 走 judgeFallback 缺省 delegate);
|
|
70
|
+
* strict → 额外 error='deny'、unsure='delegate';
|
|
71
|
+
* permissive → 额外 unsure='allow';
|
|
72
|
+
* custom → 空对象 {}(用户自填)。
|
|
73
|
+
* 非法 preset fail-loud 抛出(与 resolvePolicy / judgeFallback 一致)。
|
|
74
|
+
* @param {string} preset 预设名(PRESETS 之一)
|
|
75
|
+
* @returns {Readonly<object>} 冻结的默认 levels 对象
|
|
76
|
+
*/
|
|
77
|
+
export function defaultLevelsFor(preset) {
|
|
78
|
+
if (typeof preset !== 'string' || !PRESETS.includes(preset)) {
|
|
79
|
+
throw new Error(`defaultLevelsFor: 非法 preset "${preset}"(允许 ${PRESETS.join('|')})`)
|
|
80
|
+
}
|
|
81
|
+
return DEFAULT_LEVELS[preset]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 默认配置(design.md §2)。每次调用返回新鲜的可变默认值,交由 normalizeConfig 合并与冻结。 */
|
|
85
|
+
function defaultConfig() {
|
|
86
|
+
return {
|
|
87
|
+
preset: 'balanced',
|
|
88
|
+
modes: ['workspace-write'],
|
|
89
|
+
levels: {},
|
|
90
|
+
judge: { provider: '', model: '', systemPrompt: '', timeoutMs: 20000, maxTokens: 256, concurrency: 2 },
|
|
91
|
+
includeSubagents: true,
|
|
92
|
+
auditFile: '',
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 仅接受正整数 */
|
|
97
|
+
function isPositiveInt(v) {
|
|
98
|
+
return Number.isInteger(v) && v > 0
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 深冻结任何对象/数组图(原样返回不可变值)。 */
|
|
102
|
+
function deepFreeze(value) {
|
|
103
|
+
if (value === null || typeof value !== 'object') return value
|
|
104
|
+
if (Array.isArray(value)) {
|
|
105
|
+
for (const item of value) deepFreeze(item)
|
|
106
|
+
return Object.freeze(value)
|
|
107
|
+
}
|
|
108
|
+
for (const key of Object.keys(value)) deepFreeze(value[key])
|
|
109
|
+
return Object.freeze(value)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isPlainObject(v) {
|
|
113
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 读取裁判分级值(levels.error / levels.unsure 之类)。
|
|
118
|
+
* @param {object} levels
|
|
119
|
+
* @param {string} key
|
|
120
|
+
* @returns {string|undefined} 分级 policy 值或 undefined(未配置)
|
|
121
|
+
*/
|
|
122
|
+
function readLevel(levels, key) {
|
|
123
|
+
if (!isPlainObject(levels)) return undefined
|
|
124
|
+
return Object.prototype.hasOwnProperty.call(levels, key) ? levels[key] : undefined
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 配置规范化(design.md §2;fail-loud)。
|
|
129
|
+
* 返回完整默认合并后的深冻结配置对象。所有非法值一律抛出。
|
|
130
|
+
* @param {object|undefined} raw 原始插件 config
|
|
131
|
+
* @returns {Readonly<Config>} 冻结配置对象
|
|
132
|
+
*/
|
|
133
|
+
export function normalizeConfig(raw) {
|
|
134
|
+
const r = raw === undefined || raw === null ? {} : raw
|
|
135
|
+
if (!isPlainObject(r)) throw new TypeError('normalizeConfig: config 必须是一个普通对象')
|
|
136
|
+
|
|
137
|
+
// preset
|
|
138
|
+
const preset = r.preset === undefined ? defaultConfig().preset : r.preset
|
|
139
|
+
if (typeof preset !== 'string' || !PRESETS.includes(preset)) {
|
|
140
|
+
throw new Error(`normalizeConfig: 非法 preset "${preset}"(允许 ${PRESETS.join('|')})`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// modes
|
|
144
|
+
let modes
|
|
145
|
+
if (r.modes === undefined) {
|
|
146
|
+
modes = [...defaultConfig().modes]
|
|
147
|
+
} else {
|
|
148
|
+
if (!Array.isArray(r.modes)) throw new Error('normalizeConfig: modes 必须是数组')
|
|
149
|
+
if (r.modes.length === 0) throw new Error('normalizeConfig: modes 不能为空数组')
|
|
150
|
+
for (const m of r.modes) {
|
|
151
|
+
if (typeof m !== 'string' || !SANDBOX_MODES.includes(m)) {
|
|
152
|
+
throw new Error(`normalizeConfig: 非法沙箱模式 "${m}"(允许 ${SANDBOX_MODES.join('|')})`)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
modes = [...r.modes]
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// levels
|
|
159
|
+
const levels = {}
|
|
160
|
+
if (r.levels !== undefined && r.levels !== null) {
|
|
161
|
+
if (!isPlainObject(r.levels)) throw new Error('normalizeConfig: levels 必须是对象')
|
|
162
|
+
for (const key of Object.keys(r.levels)) {
|
|
163
|
+
const val = r.levels[key]
|
|
164
|
+
if (key === 'tools') {
|
|
165
|
+
if (!isPlainObject(val)) throw new Error('normalizeConfig: levels.tools 必须是对象')
|
|
166
|
+
levels.tools = {}
|
|
167
|
+
for (const tool of Object.keys(val)) {
|
|
168
|
+
if (!POLICIES.includes(val[tool])) {
|
|
169
|
+
throw new Error(`normalizeConfig: levels.tools.${tool} 非法 policy "${val[tool]}"(允许 ${POLICIES.join('|')})`)
|
|
170
|
+
}
|
|
171
|
+
levels.tools[tool] = val[tool]
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
if (!POLICIES.includes(val)) {
|
|
175
|
+
throw new Error(`normalizeConfig: levels.${key} 非法 policy "${val}"(允许 ${POLICIES.join('|')})`)
|
|
176
|
+
}
|
|
177
|
+
levels[key] = val
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// judge
|
|
183
|
+
const jraw = r.judge === undefined || r.judge === null ? {} : r.judge
|
|
184
|
+
if (!isPlainObject(jraw)) throw new Error('normalizeConfig: judge 必须是对象')
|
|
185
|
+
const judge = {
|
|
186
|
+
provider: jraw.provider === undefined ? '' : jraw.provider,
|
|
187
|
+
model: jraw.model === undefined ? '' : jraw.model,
|
|
188
|
+
systemPrompt: jraw.systemPrompt === undefined ? '' : jraw.systemPrompt,
|
|
189
|
+
timeoutMs: jraw.timeoutMs === undefined ? 20000 : jraw.timeoutMs,
|
|
190
|
+
maxTokens: jraw.maxTokens === undefined ? 256 : jraw.maxTokens,
|
|
191
|
+
concurrency: jraw.concurrency === undefined ? 2 : jraw.concurrency,
|
|
192
|
+
}
|
|
193
|
+
for (const field of ['provider', 'model', 'systemPrompt']) {
|
|
194
|
+
if (typeof judge[field] !== 'string') {
|
|
195
|
+
throw new Error(`normalizeConfig: judge.${field} 必须是字符串`)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
for (const field of ['timeoutMs', 'maxTokens', 'concurrency']) {
|
|
199
|
+
if (!isPositiveInt(judge[field])) {
|
|
200
|
+
throw new Error(`normalizeConfig: judge.${field} 必须为正整数`)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// includeSubagents
|
|
205
|
+
const includeSubagents = r.includeSubagents === undefined ? true : r.includeSubagents
|
|
206
|
+
if (typeof includeSubagents !== 'boolean') {
|
|
207
|
+
throw new Error('normalizeConfig: includeSubagents 必须是布尔值')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// auditFile
|
|
211
|
+
const auditFile = r.auditFile === undefined ? '' : r.auditFile
|
|
212
|
+
if (typeof auditFile !== 'string') {
|
|
213
|
+
throw new Error('normalizeConfig: auditFile 必须是字符串')
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const config = { preset, modes, levels, judge, includeSubagents, auditFile }
|
|
217
|
+
return deepFreeze(config)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 解析一次升权请求的策略(design.md §4,优先级从高到低):
|
|
222
|
+
* 1. `levels.tools[toolName]` 存在 → 用它;
|
|
223
|
+
* 2. `preset === 'custom'` → `levels[targetMode]`,缺省 `'delegate'`;
|
|
224
|
+
* 3. 否则按内置预设表(PRESET_TABLE)。
|
|
225
|
+
* @param {{preset: string, levels?: object, targetMode: string, toolName?: string}} param0
|
|
226
|
+
* @returns {'allow'|'judge'|'delegate'|'deny'} 该请求适用的策略
|
|
227
|
+
*/
|
|
228
|
+
export function resolvePolicy({ preset, levels, targetMode, toolName } = {}) {
|
|
229
|
+
if (typeof preset !== 'string' || !PRESETS.includes(preset)) {
|
|
230
|
+
throw new Error(`resolvePolicy: 非法 preset "${preset}"(允许 ${PRESETS.join('|')})`)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 规则 1:逐工具覆盖优先于任何模式行 / 预设表。
|
|
234
|
+
if (toolName !== undefined && toolName !== null && toolName !== '') {
|
|
235
|
+
const tools = isPlainObject(levels) && isPlainObject(levels.tools) ? levels.tools : {}
|
|
236
|
+
if (Object.prototype.hasOwnProperty.call(tools, toolName) && tools[toolName] !== undefined && tools[toolName] !== null) {
|
|
237
|
+
const p = tools[toolName]
|
|
238
|
+
if (!POLICIES.includes(p)) {
|
|
239
|
+
throw new Error(`resolvePolicy: levels.tools.${toolName} 非法 policy "${p}"(允许 ${POLICIES.join('|')})`)
|
|
240
|
+
}
|
|
241
|
+
return p
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 规则 3:custom 预设读取 levels[targetMode],缺省 delegate。
|
|
246
|
+
if (preset === 'custom') {
|
|
247
|
+
const p = readLevel(isPlainObject(levels) ? levels : {}, targetMode)
|
|
248
|
+
if (p === undefined || p === null) return 'delegate'
|
|
249
|
+
if (!POLICIES.includes(p)) {
|
|
250
|
+
throw new Error(`resolvePolicy: levels.${targetMode} 非法 policy "${p}"(允许 ${POLICIES.join('|')})`)
|
|
251
|
+
}
|
|
252
|
+
return p
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 规则 2:内置预设表。
|
|
256
|
+
const table = PRESET_TABLE[preset]
|
|
257
|
+
if (!Object.prototype.hasOwnProperty.call(table, targetMode)) {
|
|
258
|
+
throw new Error(`resolvePolicy: 预设 "${preset}" 不支持的目标模式 "${targetMode}"`)
|
|
259
|
+
}
|
|
260
|
+
return table[targetMode]
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 提取文本中首个配平的 JSON 对象字面量(跨字符串字面量内的 `{}`)。
|
|
265
|
+
* @param {string} text
|
|
266
|
+
* @returns {string|null} 配平的 `{...}` 子串,无 `{` 或无法配平则 null
|
|
267
|
+
*/
|
|
268
|
+
function extractFirstBalancedObject(text) {
|
|
269
|
+
const start = text.indexOf('{')
|
|
270
|
+
if (start === -1) return null
|
|
271
|
+
let depth = 0
|
|
272
|
+
let inString = false
|
|
273
|
+
let escaped = false
|
|
274
|
+
for (let i = start; i < text.length; i++) {
|
|
275
|
+
const ch = text[i]
|
|
276
|
+
if (inString) {
|
|
277
|
+
if (escaped) {
|
|
278
|
+
escaped = false
|
|
279
|
+
continue
|
|
280
|
+
}
|
|
281
|
+
if (ch === '\\') {
|
|
282
|
+
escaped = true
|
|
283
|
+
continue
|
|
284
|
+
}
|
|
285
|
+
if (ch === '"') inString = false
|
|
286
|
+
continue
|
|
287
|
+
}
|
|
288
|
+
if (ch === '"') {
|
|
289
|
+
inString = true
|
|
290
|
+
continue
|
|
291
|
+
}
|
|
292
|
+
if (ch === '{') depth++
|
|
293
|
+
else if (ch === '}') {
|
|
294
|
+
depth--
|
|
295
|
+
if (depth === 0) return text.slice(start, i + 1)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return null
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 解析 LLM 裁判输出(design.md §4):
|
|
303
|
+
* 剥 ``` 代码围栏与首尾空白 → 提取首个配平 `{...}` → JSON.parse →
|
|
304
|
+
* 校验 decision ∈ {allow,deny,unsure}(大小写不敏感归一)且 reason 为字符串(缺失补 '')。
|
|
305
|
+
* 任何失败返回 null。
|
|
306
|
+
* @param {string} text 裁判原始输出
|
|
307
|
+
* @returns {{decision:'allow'|'deny'|'unsure', reason:string}|null}
|
|
308
|
+
*/
|
|
309
|
+
export function parseJudgeOutput(text) {
|
|
310
|
+
if (typeof text !== 'string') return null
|
|
311
|
+
const cleaned = text.replace(/```/g, '').trim()
|
|
312
|
+
const expr = extractFirstBalancedObject(cleaned)
|
|
313
|
+
if (expr === null) return null
|
|
314
|
+
|
|
315
|
+
let parsed
|
|
316
|
+
try {
|
|
317
|
+
parsed = JSON.parse(expr)
|
|
318
|
+
} catch {
|
|
319
|
+
return null
|
|
320
|
+
}
|
|
321
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return null
|
|
322
|
+
|
|
323
|
+
if (typeof parsed.decision !== 'string') return null
|
|
324
|
+
const decision = parsed.decision.toLowerCase()
|
|
325
|
+
if (decision !== 'allow' && decision !== 'deny' && decision !== 'unsure') return null
|
|
326
|
+
|
|
327
|
+
let reason = parsed.reason
|
|
328
|
+
if (reason === undefined) reason = ''
|
|
329
|
+
if (typeof reason !== 'string') return null
|
|
330
|
+
|
|
331
|
+
return { decision, reason }
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* 裁判失败/不确定时的回退决策(design.md §4,修正后返回值域 `'allowed-once'|'rejected'|'delegate'`):
|
|
336
|
+
* strict + error → 'rejected'
|
|
337
|
+
* permissive + unsure → 'allowed-once'(文档警示)
|
|
338
|
+
* custom → levels.error / levels.unsure:allow→'allowed-once'、deny→'rejected'、其余/缺省→'delegate'
|
|
339
|
+
* 其他任何组合 → 'delegate'
|
|
340
|
+
* 调用方将 'delegate' 视为 next()(转人工)。
|
|
341
|
+
* @param {{preset: string, levels?: object, kind:'error'|'unsure'}} param0
|
|
342
|
+
* @returns {'allowed-once'|'rejected'|'delegate'}
|
|
343
|
+
*/
|
|
344
|
+
export function judgeFallback({ preset, levels, kind } = {}) {
|
|
345
|
+
if (typeof preset !== 'string' || !PRESETS.includes(preset)) {
|
|
346
|
+
throw new Error(`judgeFallback: 非法 preset "${preset}"(允许 ${PRESETS.join('|')})`)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const haveLevels = isPlainObject(levels) ? levels : {}
|
|
350
|
+
|
|
351
|
+
// custom:按 error/unsure 独立映射。
|
|
352
|
+
if (preset === 'custom') {
|
|
353
|
+
const key = kind === 'error' || kind === 'unsure' ? kind : 'error'
|
|
354
|
+
const v = readLevel(haveLevels, key)
|
|
355
|
+
if (v === 'allow') return 'allowed-once'
|
|
356
|
+
if (v === 'deny') return 'rejected'
|
|
357
|
+
return 'delegate' // delegate / judge / 缺失 → 转人工
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (preset === 'strict' && kind === 'error') return 'rejected'
|
|
361
|
+
if (preset === 'permissive' && kind === 'unsure') return 'allowed-once'
|
|
362
|
+
return 'delegate'
|
|
363
|
+
}
|