evolcore 0.0.22 → 0.0.23

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 (75) hide show
  1. package/AGENTS.md +62 -0
  2. package/CHANGELOG.md +24 -14
  3. package/README.md +2 -0
  4. package/bin/codex-managed-acl.ps1 +155 -0
  5. package/bin/install-codex-managed-hooks.mjs +373 -95
  6. package/dist/agents/baseagent-availability.js +48 -0
  7. package/dist/agents/baseagent.js +1 -1
  8. package/dist/agents/claude-runner.js +30 -226
  9. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +73 -13
  10. package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
  11. package/dist/agents/codex/catalog.js +70 -0
  12. package/dist/agents/codex/diagnostics.js +154 -0
  13. package/dist/agents/codex-runner.js +407 -139
  14. package/dist/agents/ecagent-runner.js +10 -31
  15. package/dist/agents/gemini-runner.js +2 -6
  16. package/dist/aun/aid/store.js +2 -0
  17. package/dist/aun/menu-client.js +272 -0
  18. package/dist/aun/msg/group.js +65 -4
  19. package/dist/aun/msg/index.js +1 -1
  20. package/dist/aun/msg/managed-operation.js +51 -4
  21. package/dist/aun/rpc/client.js +8 -1
  22. package/dist/channels/aun.js +31 -1
  23. package/dist/channels/wechat.js +81 -10
  24. package/dist/cli/aun-commands.js +48 -4
  25. package/dist/cli/command-log.js +21 -2
  26. package/dist/cli/daemon-commands.js +142 -45
  27. package/dist/cli/index.js +20 -0
  28. package/dist/cli/init.js +2 -10
  29. package/dist/cli/menu-client.js +95 -0
  30. package/dist/cli/model.js +23 -8
  31. package/dist/cli/task-context.js +12 -2
  32. package/dist/cli/trigger-command.js +9 -1
  33. package/dist/config/schema-migration.js +14 -2
  34. package/dist/core/auth/agent-delegation.js +10 -7
  35. package/dist/core/auth/authorization-audit.js +4 -0
  36. package/dist/core/auth/operation-catalog.js +4 -0
  37. package/dist/core/auth/remote-access-store.js +135 -0
  38. package/dist/core/command/command-handler.js +41 -5
  39. package/dist/core/command/daemon-diagnostics.js +448 -0
  40. package/dist/core/command/menu-catalog.js +3 -0
  41. package/dist/core/command/menu-handler.js +141 -33
  42. package/dist/core/command/menu-protocol.js +119 -0
  43. package/dist/core/command/slash-gate.js +3 -3
  44. package/dist/core/command/slash-handler.js +42 -22
  45. package/dist/core/message/message-queue.js +20 -15
  46. package/dist/core/message/response-engine.js +74 -73
  47. package/dist/core/model/catalog-service.js +308 -0
  48. package/dist/core/model/model-catalog.js +124 -334
  49. package/dist/core/model/model-diagnostics.js +4 -5
  50. package/dist/core/model/model-fallback.js +9 -16
  51. package/dist/core/model/model-permission.js +1 -1
  52. package/dist/core/permission/approval-gateway.js +16 -1
  53. package/dist/core/permission/protected-paths.js +63 -4
  54. package/dist/core/permission/sandbox-runtime.js +4 -2
  55. package/dist/core/permission/tool-policy.js +244 -25
  56. package/dist/core/runtime-lock.js +13 -8
  57. package/dist/index.js +40 -29
  58. package/dist/ipc.js +123 -81
  59. package/dist/trigger/manager.js +3 -1
  60. package/dist/trigger/parser.js +77 -13
  61. package/dist/trigger/patch.js +104 -1
  62. package/dist/utils/ecweb-supervisor.js +11 -2
  63. package/dist/utils/ecweb-utils.js +8 -26
  64. package/dist/utils/error-utils.js +7 -0
  65. package/dist/utils/npm-ops.js +47 -15
  66. package/kits/docs/evolcore/INDEX.md +1 -0
  67. package/kits/docs/evolcore/agent-create.md +13 -0
  68. package/kits/docs/evolcore/group-collaboration.md +7 -2
  69. package/kits/docs/evolcore/menu-client.md +34 -0
  70. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  71. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  72. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  73. package/kits/templates/roles/visitor.json +1 -0
  74. package/kits/templates/system-fragments/bootstrap.md +8 -1
  75. package/package.json +11 -3
@@ -3,7 +3,7 @@ import { hasTrustedPrincipal } from '../auth/authenticated-actor.js';
3
3
  import { renderActionAsText } from '../interaction-router.js';
4
4
  import { buildEnvelope, isInteractionSendAccepted, sendInteractionPayload } from '../message/message-utils.js';
5
5
  import { logger } from '../../utils/logger.js';
6
- import { summarizeToolInput } from '../../utils/tool-summary.js';
6
+ import { summarizeToolInput, summarizeToolInputForAudit } from '../../utils/tool-summary.js';
7
7
  import { createRootCausation, deriveCausation, normalizeCausation } from '../causation/context.js';
8
8
  import { recordCausationSpan } from '../causation/audit.js';
9
9
  import { checkDangerousCommand } from './tool-policy.js';
@@ -31,6 +31,7 @@ export async function requestDangerousCommandPermission(gateway, sessionId, tool
31
31
  agentName: context?.agentName,
32
32
  role: context?.role,
33
33
  taskId: context?.taskId,
34
+ summary: summarizeToolInputForAudit(toolName, input).slice(0, 512),
34
35
  });
35
36
  return { matched: true, decision: 'deny' };
36
37
  }
@@ -132,6 +133,7 @@ function truncateApprovalDetail(value) {
132
133
  ? `${trimmed.slice(0, APPROVAL_DETAIL_LIMIT - 3)}...`
133
134
  : trimmed;
134
135
  }
136
+ /** Keep unattended execution anomalies useful without copying command values. */
135
137
  function structuredApprovalAuditSummary(input) {
136
138
  const fields = [...new Set(Object.keys(input)
137
139
  .map(key => key.replace(/[^A-Za-z0-9_.-]/g, '_').slice(0, 48))
@@ -459,6 +461,7 @@ export class PermissionGateway {
459
461
  agentName: pending.agentName,
460
462
  role: pending.role,
461
463
  taskId: pending.taskId,
464
+ summary: pending.auditSummary,
462
465
  });
463
466
  }
464
467
  void this.notifyPermissionState(pending, approved
@@ -511,6 +514,7 @@ export class PermissionGateway {
511
514
  agentName: pending.agentName,
512
515
  role: pending.role,
513
516
  taskId: pending.taskId,
517
+ summary: pending.auditSummary,
514
518
  });
515
519
  if (notifyState) {
516
520
  const failure = permissionFailure(reason);
@@ -558,6 +562,7 @@ export class PermissionGateway {
558
562
  async requestCrossSessionPermission(sessionId, challenge, route, sendPrompt, context, grantScope, requestCausation, grantMatch) {
559
563
  const approval = context.approvalRouting;
560
564
  const interactionRouter = context.interactionRouter;
565
+ const auditSummary = summarizeToolInputForAudit(challenge.toolName, challenge.toolInput).slice(0, 512);
561
566
  if (!approval || !interactionRouter) {
562
567
  await sendPrompt('当前会话权限不足,且没有可用的 owner 审批通道。');
563
568
  auditPermissionDecision({
@@ -571,6 +576,7 @@ export class PermissionGateway {
571
576
  agentName: context.agentName,
572
577
  role: context.role,
573
578
  taskId: context.taskId,
579
+ summary: structuredApprovalAuditSummary(challenge.toolInput),
574
580
  });
575
581
  return 'deny';
576
582
  }
@@ -673,6 +679,7 @@ export class PermissionGateway {
673
679
  interactionRouter,
674
680
  causation: requestCausation,
675
681
  permissionStateChanged: context.permissionStateChanged,
682
+ auditSummary,
676
683
  };
677
684
  this.crossSessionPending.set(requestId, pending);
678
685
  this.syncManagementRevalidationTimer();
@@ -788,6 +795,7 @@ export class PermissionGateway {
788
795
  agentName: pending.agentName,
789
796
  role: pending.role,
790
797
  taskId: pending.taskId,
798
+ summary: pending.auditSummary,
791
799
  });
792
800
  const decisionCausation = deriveCausation(pending.causation);
793
801
  recordCausationSpan(decisionCausation, 'permission.decision', {
@@ -815,6 +823,7 @@ export class PermissionGateway {
815
823
  const effectiveApproverPolicy = approverPolicy
816
824
  ?? context?.approvalRouting?.approverPolicy
817
825
  ?? 'requester';
826
+ const auditSummary = summarizeToolInputForAudit(toolName, toolInput).slice(0, 512);
818
827
  if (context?.approvalInteractionPolicy === 'deny') {
819
828
  const denialRequestId = `perm-policy-${randomUUID()}`;
820
829
  const anomaly = {
@@ -852,6 +861,7 @@ export class PermissionGateway {
852
861
  agentName: context.agentName,
853
862
  role: context.role,
854
863
  taskId: context.taskId,
864
+ summary: auditSummary,
855
865
  });
856
866
  return 'deny';
857
867
  }
@@ -868,6 +878,7 @@ export class PermissionGateway {
868
878
  agentName: context?.agentName,
869
879
  role: context?.role,
870
880
  taskId: context?.taskId,
881
+ summary: auditSummary,
871
882
  });
872
883
  return 'deny';
873
884
  }
@@ -942,6 +953,7 @@ export class PermissionGateway {
942
953
  agentName: context.agentName,
943
954
  role: context.role,
944
955
  taskId: context.taskId,
956
+ summary: summarizeToolInputForAudit(toolName, toolInput).slice(0, 512),
945
957
  });
946
958
  return 'deny';
947
959
  }
@@ -1006,6 +1018,7 @@ export class PermissionGateway {
1006
1018
  permissionMode: context?.permissionMode,
1007
1019
  role: context?.role,
1008
1020
  taskId: context?.taskId,
1021
+ auditSummary,
1009
1022
  };
1010
1023
  pending.timeout = setTimeout(() => {
1011
1024
  this.failPendingPermission(sessionId, requestId, 'expired');
@@ -1136,6 +1149,7 @@ export class PermissionGateway {
1136
1149
  agentName: pending.agentName,
1137
1150
  role: pending.role,
1138
1151
  taskId: pending.taskId,
1152
+ summary: pending.auditSummary,
1139
1153
  });
1140
1154
  }
1141
1155
  this.eventBus?.publish({ type: 'permission:resolved', sessionId, requestId, toolName: pending.toolName, approved: normalizedDecision !== 'deny', ...(policyCode ? { decisionSource, policyCode } : { decisionSource }), causation: decisionCausation });
@@ -1213,6 +1227,7 @@ export class PermissionGateway {
1213
1227
  agentName: pending.agentName,
1214
1228
  role: pending.role,
1215
1229
  taskId: pending.taskId,
1230
+ summary: pending.auditSummary,
1216
1231
  });
1217
1232
  this.pending.delete(requestId);
1218
1233
  const decisionCausation = deriveCausation(pending.causation);
@@ -302,6 +302,7 @@ export function getLClassWritePatterns(root = resolveRoot()) {
302
302
  }
303
303
  const CLAUDE_PROTECTED_TARGET_LIMIT = 8192;
304
304
  const CLAUDE_GLOB_MAGIC = /[*?[\]]/;
305
+ const CODEX_GLOB_MAGIC = /[*?[\]]/;
305
306
  function addExistingTarget(targets, value) {
306
307
  try {
307
308
  const stat = fs.lstatSync(value);
@@ -427,12 +428,70 @@ export function buildClaudeProtectedFilesystem(root = resolveRoot(), options = {
427
428
  };
428
429
  }
429
430
  export function buildCodexHClassFilesystemRules(root = resolveRoot()) {
430
- return {
431
+ const filesystem = {
431
432
  // Codex pre-expands unbounded deny globs on Linux/WSL/Windows. Without a
432
433
  // bound, patterns such as **/*.json_ are accepted but may not be enforced.
433
434
  glob_scan_max_depth: 64,
434
- ...Object.fromEntries(getHClassSandboxPatterns(root).map(pattern => [pattern, 'deny'])),
435
+ ...Object.fromEntries(filterWindowsCodexFilesystemTargets(getHClassSandboxPatterns(root)).map(pattern => [pattern, 'deny'])),
435
436
  };
437
+ return compactWindowsCodexFilesystemRules(filesystem);
438
+ }
439
+ /**
440
+ * Windows Codex sandbox setup materializes a path while applying its deny-read
441
+ * ACL. A nonexistent exact file target can therefore become a directory on
442
+ * disk (for example `daemon.pid`). Keep existing paths and globs, but omit
443
+ * nonexistent exact paths so the sandbox never creates the wrong filesystem
444
+ * object. Non-Windows callers retain the historical rule set.
445
+ */
446
+ export function filterWindowsCodexFilesystemTargets(patterns, platform = process.platform) {
447
+ if (platform !== 'win32')
448
+ return patterns;
449
+ return patterns.filter(pattern => {
450
+ if (CODEX_GLOB_MAGIC.test(pattern))
451
+ return true;
452
+ try {
453
+ fs.lstatSync(pattern);
454
+ return true;
455
+ }
456
+ catch (error) {
457
+ const code = error && typeof error === 'object' && 'code' in error
458
+ ? error.code
459
+ : undefined;
460
+ return code === 'ENOENT' ? false : true;
461
+ }
462
+ });
463
+ }
464
+ /**
465
+ * Codex expands recursive deny globs into individual targets. Large protected
466
+ * trees on Windows can then fail during AGENTS.md loading with os error 206.
467
+ * Directory denies inherit to descendants through the Windows sandbox ACLs,
468
+ * avoiding redundant targets. Keep broad recursive filename globs intact.
469
+ */
470
+ export function compactWindowsCodexFilesystemRules(filesystem, platform = process.platform) {
471
+ if (platform !== 'win32')
472
+ return filesystem;
473
+ const compacted = {};
474
+ for (const [pattern, access] of Object.entries(filesystem)) {
475
+ const isTerminalRecursiveDeny = access === 'deny'
476
+ && (pattern.endsWith('/**') || pattern.endsWith('\\**'));
477
+ const recursiveRoot = isTerminalRecursiveDeny ? pattern.slice(0, -3) : undefined;
478
+ // Only collapse a recursive deny when its root is a real directory. A
479
+ // missing root must remain a glob: turning it into an exact target can
480
+ // make the Windows sandbox materialize that path as a directory.
481
+ const canCompact = !!recursiveRoot
482
+ && (CODEX_GLOB_MAGIC.test(recursiveRoot) || (() => {
483
+ try {
484
+ return fs.lstatSync(recursiveRoot).isDirectory();
485
+ }
486
+ catch {
487
+ return false;
488
+ }
489
+ })());
490
+ const compactPattern = canCompact ? recursiveRoot : pattern;
491
+ if (compacted[compactPattern] !== 'deny')
492
+ compacted[compactPattern] = access;
493
+ }
494
+ return compacted;
436
495
  }
437
496
  export function buildCodexProtectedFilesystemRules(root = resolveRoot(), options = {}) {
438
497
  const lClassRules = {};
@@ -459,10 +518,10 @@ export function buildCodexProtectedFilesystemRules(root = resolveRoot(), options
459
518
  lClassRules[target.path] = lClassAccess;
460
519
  }
461
520
  }
462
- return {
521
+ return compactWindowsCodexFilesystemRules({
463
522
  ...buildCodexHClassFilesystemRules(root),
464
523
  ...lClassRules,
465
- };
524
+ });
466
525
  }
467
526
  export function hClassGrantIncludesProtectedPath(value, options = {}) {
468
527
  const root = resolveThroughExistingAncestor(options.root ?? resolveRoot());
@@ -497,8 +497,10 @@ export function buildHClassGuardCommand(executable, executableArgs, root = resol
497
497
  '--dev-bind', '/dev', '/dev',
498
498
  '--proc', '/proc',
499
499
  ];
500
- appendLClassReadOnlyBinds(args, path.resolve(root));
501
- appendHClassMasks(args, path.resolve(root), true);
500
+ if (options.protectEvolcoreFilesystem !== false) {
501
+ appendLClassReadOnlyBinds(args, path.resolve(root));
502
+ appendHClassMasks(args, path.resolve(root), true);
503
+ }
502
504
  if (options.managedCodexRequirements) {
503
505
  const requirements = path.resolve(options.managedCodexRequirements);
504
506
  if (!fs.statSync(requirements).isFile()) {
@@ -91,15 +91,179 @@ export function maskManagedTempShellPaths(command, options = {}) {
91
91
  return `__managed_tmp_${index}__`;
92
92
  });
93
93
  }
94
- // 绝对禁止命令(所有角色、所有权限模式下都禁止,不可授权)
95
- // 这些是系统级破坏操作,任何情况下都不应该允许
96
94
  const ABSOLUTE_FORBIDDEN = [
97
- /\bshutdown\b/, // 关机
98
- /\breboot\b/, // 重启
99
- /\bmkfs\b/, // mkfs (格式化文件系统)
100
- /\bformat\s+[a-zA-Z]:/i, // format C: (格式化磁盘)
101
- /\bdd\s+if=.*of=\/dev/, // dd 写入磁盘设备(读取操作允许)
95
+ /\bshutdown\b/i,
96
+ /\breboot\b/i,
97
+ /\bmkfs(?:\.[A-Za-z0-9._-]+)?\b/i,
98
+ /\bformat\s+[a-zA-Z]:/i,
99
+ /\bdd\s+if=.*of=\/dev(?:\/|\b)/i,
102
100
  ];
101
+ const SCRIPT_EXECUTION_RE = /(?:\bos\.system\s*\(|\bsubprocess\.(?:run|call|Popen)\s*\(|\bchild_process\.(?:exec|spawn|fork)\s*\(|\b(?:system|exec|spawn|popen)\s*\()/i;
102
+ function hereDocDelimiter(line) {
103
+ let quote;
104
+ let escaped = false;
105
+ for (let index = 0; index < line.length - 1; index++) {
106
+ const char = line[index];
107
+ if (escaped) {
108
+ escaped = false;
109
+ continue;
110
+ }
111
+ if (char === '\\' && quote !== 'single') {
112
+ escaped = true;
113
+ continue;
114
+ }
115
+ if (quote === 'single') {
116
+ if (char === "'")
117
+ quote = undefined;
118
+ continue;
119
+ }
120
+ if (quote === 'double') {
121
+ if (char === '"')
122
+ quote = undefined;
123
+ continue;
124
+ }
125
+ if (char === "'") {
126
+ quote = 'single';
127
+ continue;
128
+ }
129
+ if (char === '"') {
130
+ quote = 'double';
131
+ continue;
132
+ }
133
+ if (char !== '<' || line[index + 1] !== '<' || line[index + 2] === '<')
134
+ continue;
135
+ let cursor = index + 2;
136
+ if (line[cursor] === '-')
137
+ cursor++;
138
+ while (cursor < line.length && /\s/.test(line[cursor]))
139
+ cursor++;
140
+ if (line[cursor] === "'" || line[cursor] === '"') {
141
+ const delimiterQuote = line[cursor++];
142
+ const start = cursor;
143
+ while (cursor < line.length && line[cursor] !== delimiterQuote)
144
+ cursor++;
145
+ return cursor > start && cursor < line.length ? line.slice(start, cursor) : undefined;
146
+ }
147
+ const match = line.slice(cursor).match(/^[A-Za-z_][A-Za-z0-9_-]*/);
148
+ return match?.[0];
149
+ }
150
+ return undefined;
151
+ }
152
+ function splitHereDocBodies(command) {
153
+ const lines = command.split(/\r?\n/);
154
+ let delimiter;
155
+ let body;
156
+ const bodies = [];
157
+ const retained = [];
158
+ for (const line of lines) {
159
+ if (delimiter) {
160
+ const candidate = line.replace(/^\t+/, '').trim();
161
+ if (candidate === delimiter) {
162
+ bodies.push((body ?? []).join('\n'));
163
+ delimiter = undefined;
164
+ body = undefined;
165
+ }
166
+ else {
167
+ body?.push(line);
168
+ }
169
+ continue;
170
+ }
171
+ retained.push(line);
172
+ delimiter = hereDocDelimiter(line);
173
+ if (delimiter)
174
+ body = [];
175
+ }
176
+ if (delimiter && body)
177
+ bodies.push(body.join('\n'));
178
+ return { command: retained.join('\n'), bodies };
179
+ }
180
+ function containsAbsoluteForbiddenText(value) {
181
+ return ABSOLUTE_FORBIDDEN.some(pattern => pattern.test(value));
182
+ }
183
+ function interpreterScript(args) {
184
+ const index = args.findIndex(argument => argument === '-c' || argument === '-e' || argument === '--eval');
185
+ return index >= 0 ? args[index + 1] : undefined;
186
+ }
187
+ function scriptContainsAbsoluteForbidden(script) {
188
+ return SCRIPT_EXECUTION_RE.test(script) && containsAbsoluteForbiddenText(script);
189
+ }
190
+ /** Match only executable argv, never regex/search text or script literals. */
191
+ function containsAbsoluteForbiddenCommand(command, depth = 0) {
192
+ const hereDocs = splitHereDocBodies(command);
193
+ if (containsShellSubstitution(hereDocs.command) && containsAbsoluteForbiddenText(hereDocs.command))
194
+ return true;
195
+ return splitShellCommandSegments(hereDocs.command).some(segment => {
196
+ const parsed = parseEffectiveCommand(segment);
197
+ if (!parsed)
198
+ return false;
199
+ const { executable, args } = parsed;
200
+ if (executable === 'shutdown' || executable === 'reboot')
201
+ return true;
202
+ if (/^mkfs(?:\..*)?$/.test(executable))
203
+ return true;
204
+ if (executable === 'format' && args.some(argument => /^[A-Za-z]:$/.test(argument)))
205
+ return true;
206
+ if (executable === 'dd'
207
+ && args.some(argument => /^if=/.test(argument))
208
+ && args.some(argument => /^of=\/dev(?:\/|$)/.test(argument)))
209
+ return true;
210
+ if (executable === 'eval' && containsAbsoluteForbiddenText(args.join(' ')))
211
+ return true;
212
+ if (['node', 'nodejs', 'python', 'python3', 'perl'].includes(executable)) {
213
+ const script = interpreterScript(args);
214
+ if (script && scriptContainsAbsoluteForbidden(script))
215
+ return true;
216
+ if (hereDocs.bodies.some(scriptContainsAbsoluteForbidden))
217
+ return true;
218
+ }
219
+ if (['bash', 'sh', 'zsh', 'dash'].includes(executable)
220
+ && hereDocs.bodies.some(body => containsAbsoluteForbiddenCommand(body, depth + 1)))
221
+ return true;
222
+ if (depth >= 2 || !['bash', 'sh', 'zsh', 'dash', 'cmd', 'cmd.exe'].includes(executable))
223
+ return false;
224
+ const commandFlag = executable.startsWith('cmd') ? '/c' : '-c';
225
+ const commandIndex = args.findIndex(argument => argument.toLowerCase() === commandFlag);
226
+ return commandIndex >= 0
227
+ && !!args[commandIndex + 1]
228
+ && containsAbsoluteForbiddenCommand(args.slice(commandIndex + 1).join(' '), depth + 1);
229
+ });
230
+ }
231
+ function containsShellSubstitution(command) {
232
+ let quote;
233
+ let escaped = false;
234
+ for (let index = 0; index < command.length; index++) {
235
+ const char = command[index];
236
+ if (escaped) {
237
+ escaped = false;
238
+ continue;
239
+ }
240
+ if (char === '\\' && quote !== 'single') {
241
+ escaped = true;
242
+ continue;
243
+ }
244
+ if (quote === 'single') {
245
+ if (char === "'")
246
+ quote = undefined;
247
+ continue;
248
+ }
249
+ if (quote === 'double') {
250
+ if (char === '"')
251
+ quote = undefined;
252
+ continue;
253
+ }
254
+ if (char === "'") {
255
+ quote = 'single';
256
+ continue;
257
+ }
258
+ if (char === '"') {
259
+ quote = 'double';
260
+ continue;
261
+ }
262
+ if (char === '`' || (char === '$' && command[index + 1] === '('))
263
+ return true;
264
+ }
265
+ return false;
266
+ }
103
267
  // 危险操作(需要用户审批才能执行)
104
268
  // 这些操作有合理使用场景,但需要用户明确授权
105
269
  const DANGEROUS_PATTERNS = [
@@ -386,7 +550,8 @@ function classifyStructuredCommand(command, depth = 0) {
386
550
  return matches.find(match => match.approvalScope === 'all') ?? matches[0];
387
551
  }
388
552
  function containsNetStopCommand(command, depth = 0) {
389
- return splitShellCommandSegments(command).some(segment => {
553
+ const hereDocs = splitHereDocBodies(command);
554
+ return splitShellCommandSegments(hereDocs.command).some(segment => {
390
555
  const parsed = parseEffectiveCommand(segment);
391
556
  if (!parsed)
392
557
  return false;
@@ -394,6 +559,9 @@ function containsNetStopCommand(command, depth = 0) {
394
559
  && parsed.args[0]?.toLowerCase() === 'stop') {
395
560
  return true;
396
561
  }
562
+ if (['bash', 'sh', 'zsh', 'dash'].includes(parsed.executable)
563
+ && hereDocs.bodies.some(body => containsNetStopCommand(body, depth + 1)))
564
+ return true;
397
565
  if (depth >= 2 || !['bash', 'sh', 'zsh', 'dash', 'cmd', 'cmd.exe'].includes(parsed.executable)) {
398
566
  return false;
399
567
  }
@@ -1192,6 +1360,54 @@ function readonlySourceDiagnosticAllowed(analysis, projectPath) {
1192
1360
  && !isLClassPath(candidate, { root: resolveRoot() });
1193
1361
  });
1194
1362
  }
1363
+ /**
1364
+ * A narrow exception for the common repository source inventory query. The
1365
+ * general recursive H-class rule remains fail-closed; only an opt-in,
1366
+ * single-find query whose output is limited to TypeScript/JavaScript files at
1367
+ * the project root can use this path.
1368
+ */
1369
+ function boundedRootSourceEnumeration(analysis, projectPath) {
1370
+ if (analysis.kind !== 'proven-readonly' || analysis.ir.pipelines.length !== 1)
1371
+ return false;
1372
+ const pipeline = analysis.ir.pipelines[0];
1373
+ const find = pipeline[0];
1374
+ if (!find || find.executable !== 'find' || find.pathOperands.length !== 1 || find.pathOperands[0].access !== 'recursive') {
1375
+ return false;
1376
+ }
1377
+ const workspace = resolveProtectedCandidate(projectPath);
1378
+ if (workspace === path.parse(workspace).root)
1379
+ return false;
1380
+ const scope = resolveProtectedCandidate(find.pathOperands[0].value, projectPath);
1381
+ if (scope !== workspace)
1382
+ return false;
1383
+ const allowedPredicates = new Set(['-o', '-or', '-a', '-and', '-print', '-print0']);
1384
+ let sawSourceSuffix = false;
1385
+ const scopeIndex = find.argv.indexOf(find.pathOperands[0].value);
1386
+ if (scopeIndex < 1)
1387
+ return false;
1388
+ const args = find.argv.slice(scopeIndex + 1);
1389
+ for (let index = 0; index < args.length; index++) {
1390
+ const argument = args[index];
1391
+ if (argument === '-name' || argument === '-iname') {
1392
+ const pattern = args[++index];
1393
+ if (!pattern || !/^\*\.(?:ts|js)$/i.test(pattern))
1394
+ return false;
1395
+ sawSourceSuffix = true;
1396
+ continue;
1397
+ }
1398
+ if (argument === '-type') {
1399
+ if (args[++index] !== 'f')
1400
+ return false;
1401
+ continue;
1402
+ }
1403
+ if (!allowedPredicates.has(argument))
1404
+ return false;
1405
+ }
1406
+ if (!sawSourceSuffix)
1407
+ return false;
1408
+ return pipeline.slice(1).every(command => ['head', 'sort', 'grep', 'wc'].includes(command.executable)
1409
+ && command.pathOperands.length === 0);
1410
+ }
1195
1411
  /** A lock filename used as a query pattern is safe; an operand naming the
1196
1412
  * actual lock path is still protected. */
1197
1413
  function containsLockPathReference(value) {
@@ -1246,10 +1462,13 @@ export function checkReadonly(toolName, input, projectPath, context) {
1246
1462
  managedTempDir,
1247
1463
  allowProtectedMetadata: context?.allowProtectedMetadata,
1248
1464
  });
1465
+ const sourceQueryAllowed = readonlySourceDiagnosticAllowed(protectedOperands.analysis, projectPath);
1466
+ const boundedSourceQueryAllowed = boundedRootSourceEnumeration(protectedOperands.analysis, projectPath);
1249
1467
  const sourceDiagnosticAllowed = context?.allowReadonlySourceDiagnostics
1250
- && readonlySourceDiagnosticAllowed(protectedOperands.analysis, projectPath)
1251
- && !protectedOperands.lClass
1252
- && (!protectedOperands.hClass || (protectedOperands.lockDiagnostic && !protectedOperands.pathHClass));
1468
+ && (!protectedOperands.lClass || boundedSourceQueryAllowed)
1469
+ && ((sourceQueryAllowed
1470
+ && (!protectedOperands.hClass || (protectedOperands.lockDiagnostic && !protectedOperands.pathHClass)))
1471
+ || boundedSourceQueryAllowed);
1253
1472
  if (sourceDiagnosticAllowed) {
1254
1473
  return { behavior: 'allow' };
1255
1474
  }
@@ -1556,11 +1775,15 @@ export function checkHClassWrite(toolName, input, context) {
1556
1775
  managedTempDir: context?.managedTempDir,
1557
1776
  allowProtectedMetadata: context?.allowProtectedMetadata,
1558
1777
  });
1778
+ const sourceDiagnosticProjectPath = context?.projectPath ?? process.cwd();
1779
+ const sourceQueryAllowed = readonlySourceDiagnosticAllowed(protectedOperands.analysis, sourceDiagnosticProjectPath);
1780
+ const boundedSourceQueryAllowed = boundedRootSourceEnumeration(protectedOperands.analysis, sourceDiagnosticProjectPath);
1559
1781
  const sourceDiagnosticAllowed = context?.permissionMode === 'readonly'
1560
1782
  && context.allowReadonlySourceDiagnostics === true
1561
- && readonlySourceDiagnosticAllowed(protectedOperands.analysis, context.projectPath ?? process.cwd())
1562
- && !protectedOperands.lClass
1563
- && (!protectedOperands.hClass || (protectedOperands.lockDiagnostic && !protectedOperands.pathHClass));
1783
+ && (!protectedOperands.lClass || boundedSourceQueryAllowed)
1784
+ && ((sourceQueryAllowed
1785
+ && (!protectedOperands.hClass || (protectedOperands.lockDiagnostic && !protectedOperands.pathHClass)))
1786
+ || boundedSourceQueryAllowed);
1564
1787
  if (protectedOperands.hClass && !sourceDiagnosticAllowed) {
1565
1788
  logger.warn(`[H-Class Protection] 🔒 Protected filesystem operand in shell command: tool=${toolName} ` +
1566
1789
  `session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
@@ -1721,7 +1944,10 @@ export function checkLClassWrite(toolName, input, context) {
1721
1944
  ...pathOptions,
1722
1945
  managedTempDir: context?.managedTempDir,
1723
1946
  });
1947
+ const boundedSourceQueryAllowed = context?.allowReadonlySourceDiagnostics === true
1948
+ && boundedRootSourceEnumeration(protectedOperands.analysis, context?.projectPath ?? process.cwd());
1724
1949
  const lClassDenied = protectedOperands.lClass
1950
+ && !boundedSourceQueryAllowed
1725
1951
  && (context?.permissionMode === 'readonly' || protectedOperands.analysis.kind === 'unproven');
1726
1952
  if (lClassDenied) {
1727
1953
  logger.warn(`[L-Class Protection] L-class shell access denied: tool=${toolName} ` +
@@ -1789,23 +2015,15 @@ export function checkBlacklist(toolName, input) {
1789
2015
  if (!cmd || cmd.trim() === '') {
1790
2016
  return { behavior: 'allow', updatedInput: input };
1791
2017
  }
1792
- if (containsNetStopCommand(cmd)) {
2018
+ // 只根据解析后的 executable 和 argv 判断,避免把搜索词、正则和
2019
+ // heredoc/script 正文当成实际命令。
2020
+ if (containsNetStopCommand(cmd) || containsAbsoluteForbiddenCommand(cmd)) {
1793
2021
  const cmdPreview = cmd.length > 60 ? cmd.substring(0, 60) + '...' : cmd;
1794
2022
  return {
1795
2023
  behavior: 'deny',
1796
2024
  message: `🚫 系统级危险操作已拦截\n命令:${cmdPreview}\n\n此类操作可能导致系统损坏,任何权限模式下都不允许执行。`,
1797
2025
  };
1798
2026
  }
1799
- // 检查绝对禁止命令(系统级破坏操作)
1800
- for (const pattern of ABSOLUTE_FORBIDDEN) {
1801
- if (pattern.test(cmd)) {
1802
- const cmdPreview = cmd.length > 60 ? cmd.substring(0, 60) + '...' : cmd;
1803
- return {
1804
- behavior: 'deny',
1805
- message: `🚫 系统级危险操作已拦截\n命令:${cmdPreview}\n\n此类操作可能导致系统损坏,任何权限模式下都不允许执行。`
1806
- };
1807
- }
1808
- }
1809
2027
  }
1810
2028
  // 默认允许
1811
2029
  return { behavior: 'allow', updatedInput: input };
@@ -2136,6 +2354,7 @@ export function evaluateToolPreflight(toolName, input, context) {
2136
2354
  workspacePath: context.workspacePath,
2137
2355
  root: context.root,
2138
2356
  managedTempDir: context.managedTempDir,
2357
+ allowReadonlySourceDiagnostics: context.allowReadonlySourceDiagnostics,
2139
2358
  });
2140
2359
  if (lClass.behavior === 'deny') {
2141
2360
  return { behavior: 'deny', input: checkedInput, message: lClass.message, policyCode: 'l_class_protection', matchedPath: lClass.matchedPath };
@@ -41,28 +41,33 @@ function isTrustedRuntimeLockDir(candidate) {
41
41
  if (!isWithinTmp && !isManagedSessionSibling)
42
42
  return false;
43
43
  try {
44
+ // Windows Stats.mode does not describe ACL privacy. The managed runtime
45
+ // roots are protected by Windows ACL inheritance, so apply POSIX
46
+ // ownership and permission-bit checks only where those fields are
47
+ // meaningful.
48
+ const isWindows = process.platform === 'win32';
44
49
  const tmpStat = fs.lstatSync(resolvedTmpRoot);
45
50
  if (!tmpStat.isDirectory() || tmpStat.isSymbolicLink())
46
51
  return false;
47
- if (typeof process.getuid === 'function' && tmpStat.uid !== process.getuid())
52
+ if (!isWindows && typeof process.getuid === 'function' && tmpStat.uid !== process.getuid())
48
53
  return false;
49
- if ((tmpStat.mode & 0o077) !== 0)
54
+ if (!isWindows && (tmpStat.mode & 0o077) !== 0)
50
55
  return false;
51
56
  if (isManagedSessionSibling) {
52
57
  const parentStat = fs.lstatSync(tmpParent);
53
58
  if (!parentStat.isDirectory() || parentStat.isSymbolicLink())
54
59
  return false;
55
- if (typeof process.getuid === 'function' && parentStat.uid !== process.getuid())
60
+ if (!isWindows && typeof process.getuid === 'function' && parentStat.uid !== process.getuid())
56
61
  return false;
57
- if ((parentStat.mode & 0o077) !== 0)
62
+ if (!isWindows && (parentStat.mode & 0o077) !== 0)
58
63
  return false;
59
64
  }
60
65
  const stat = fs.lstatSync(resolved);
61
66
  if (!stat.isDirectory() || stat.isSymbolicLink())
62
67
  return false;
63
- if (typeof process.getuid === 'function' && stat.uid !== process.getuid())
68
+ if (!isWindows && typeof process.getuid === 'function' && stat.uid !== process.getuid())
64
69
  return false;
65
- return (stat.mode & 0o077) === 0;
70
+ return isWindows || (stat.mode & 0o077) === 0;
66
71
  }
67
72
  catch {
68
73
  return false;
@@ -93,8 +98,8 @@ export function ensureManagedLockParent(lockPath) {
93
98
  }
94
99
  const stat = fs.lstatSync(current);
95
100
  if (!stat.isDirectory() || stat.isSymbolicLink()
96
- || (typeof process.getuid === 'function' && stat.uid !== process.getuid())
97
- || (stat.mode & 0o077) !== 0) {
101
+ || (process.platform !== 'win32' && typeof process.getuid === 'function' && stat.uid !== process.getuid())
102
+ || (process.platform !== 'win32' && (stat.mode & 0o077) !== 0)) {
98
103
  throw new Error(`managed runtime lock path is not a private real directory: ${current}`);
99
104
  }
100
105
  }