evolcore 0.0.9 → 0.0.11
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 +29 -0
- package/README.md +3 -3
- package/dist/agents/baseagent.js +4 -0
- package/dist/agents/claude-runner.js +123 -42
- package/dist/agents/codex-app-server-client.js +33 -9
- package/dist/agents/codex-runner.js +58 -8
- package/dist/agents/ecagent-runner.js +17 -2
- package/dist/agents/request-identity.js +55 -0
- package/dist/aun/outbox.js +28 -31
- package/dist/channels/aun.js +131 -128
- package/dist/cli/agent-command.js +16 -9
- package/dist/cli/agent.js +82 -19
- package/dist/cli/daemon-commands.js +21 -2
- package/dist/cli/index.js +76 -61
- package/dist/cli/init-cancel.js +208 -0
- package/dist/cli/init-channel.js +343 -195
- package/dist/cli/init.js +21 -9
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/contact-book-store.js +1 -1
- package/dist/config/gateway-config.js +26 -10
- package/dist/core/agent-reload-coordinator.js +53 -0
- package/dist/core/auth/operation-authorizer.js +32 -147
- package/dist/core/auth/operation-catalog.js +80 -0
- package/dist/core/bootstrap-messages.js +50 -0
- package/dist/core/bootstrap-service.js +85 -10
- package/dist/core/channel-loader.js +23 -6
- package/dist/core/command/agent-control.js +14 -11
- package/dist/core/command/menu-handler.js +67 -76
- package/dist/core/command/slash-handler.js +4 -4
- package/dist/core/data-migration.js +79 -27
- package/dist/core/evolagent-registry.js +125 -35
- package/dist/core/evolagent.js +8 -3
- package/dist/core/inference/text-inference.js +38 -4
- package/dist/core/message/message-bridge.js +1 -1
- package/dist/core/message/message-log.js +22 -0
- package/dist/core/message/message-queue.js +19 -4
- package/dist/core/model/model-catalog.js +143 -24
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/index.js +1 -0
- package/dist/core/permission/readonly-shell-query.js +532 -0
- package/dist/core/permission/shell-environment.js +46 -0
- package/dist/core/permission/tool-policy.js +231 -93
- package/dist/core/protected-paths.js +10 -7
- package/dist/core/runner-reload-transaction.js +57 -0
- package/dist/index.js +262 -84
- package/dist/ipc.js +29 -11
- package/dist/utils/aid-bind.js +3 -8
- package/dist/utils/log-writer.js +6 -10
- package/dist/utils/logger.js +5 -5
- package/kits/docs/evolcore/msg.md +13 -0
- package/kits/rules/01-overview.md +9 -0
- package/kits/schemas/agent-config.schema.3.json +1 -1
- package/kits/schemas/agent-config.schema.4.json +1 -1
- package/kits/schemas/relation-config.schema.2.json +1 -1
- package/kits/schemas/role-config.schema.1.json +1 -1
- package/kits/templates/roles/admin.json +5 -0
- package/kits/templates/roles/member.json +17 -0
- package/kits/templates/roles/visitor.json +8 -0
- package/kits/templates/system-fragments/bootstrap.md +12 -6
- package/kits/templates/system-fragments/channel.md +6 -0
- package/kits/templates/system-fragments/session.md +2 -0
- package/package.json +2 -1
- package/skills/eclink/SKILL.md +15 -3
- package/skills/eclink/agents/openai.yaml +3 -3
|
@@ -3,8 +3,9 @@ import path from 'path';
|
|
|
3
3
|
import { randomUUID } from 'crypto';
|
|
4
4
|
import { logger } from '../../utils/logger.js';
|
|
5
5
|
import { resolveRoot } from '../../paths.js';
|
|
6
|
-
import { containsHClassReference, containsLClassReference, hClassGrantIncludesProtectedPath, isHClassPath, isSameOrDescendant, isLClassPath, lClassGrantIncludesProtectedPath, resolveProtectedCandidate, } from '../protected-paths.js';
|
|
6
|
+
import { containsHClassReference, containsLClassReference, getExistingHClassMaskTargets, hClassGrantIncludesProtectedPath, isHClassPath, isSameOrDescendant, isLClassPath, lClassGrantIncludesProtectedPath, resolveProtectedCandidate, } from '../protected-paths.js';
|
|
7
7
|
import { classifyEvolcoreShellCommand, parseBoundedOutputShellCommand, parseLiteralShellArgv, } from './ec-command-parser.js';
|
|
8
|
+
import { analyzeReadonlyShellQuery, } from './readonly-shell-query.js';
|
|
8
9
|
// 绝对禁止命令(所有角色、所有权限模式下都禁止,不可授权)
|
|
9
10
|
// 这些是系统级破坏操作,任何情况下都不应该允许
|
|
10
11
|
const ABSOLUTE_FORBIDDEN = [
|
|
@@ -14,19 +15,6 @@ const ABSOLUTE_FORBIDDEN = [
|
|
|
14
15
|
/\bformat\s+[a-zA-Z]:/i, // format C: (格式化磁盘)
|
|
15
16
|
/\bdd\s+if=.*of=\/dev/, // dd 写入磁盘设备(读取操作允许)
|
|
16
17
|
];
|
|
17
|
-
const READONLY_SHELL_EXECUTABLES = new Set([
|
|
18
|
-
'ls', '/bin/ls', '/usr/bin/ls',
|
|
19
|
-
'cat', '/bin/cat', '/usr/bin/cat',
|
|
20
|
-
]);
|
|
21
|
-
const L_CLASS_READONLY_SHELL_EXECUTABLES = new Set([
|
|
22
|
-
...READONLY_SHELL_EXECUTABLES,
|
|
23
|
-
'grep', '/bin/grep', '/usr/bin/grep',
|
|
24
|
-
'rg', '/bin/rg', '/usr/bin/rg',
|
|
25
|
-
'find', '/bin/find', '/usr/bin/find',
|
|
26
|
-
'sed', '/bin/sed', '/usr/bin/sed',
|
|
27
|
-
]);
|
|
28
|
-
const FIND_MUTATING_PRIMARY_RE = /^-(?:delete|exec|execdir|ok|okdir|fprint|fprint0|fprintf|fls)$/;
|
|
29
|
-
const RG_PROCESS_SPAWNING_OPTION_RE = /^--pre(?:=|$)|^--pre-glob(?:=|$)/;
|
|
30
18
|
// 危险操作(需要用户审批才能执行)
|
|
31
19
|
// 这些操作有合理使用场景,但需要用户明确授权
|
|
32
20
|
const DANGEROUS_PATTERNS = [
|
|
@@ -331,19 +319,163 @@ function containsNetStopCommand(command, depth = 0) {
|
|
|
331
319
|
&& containsNetStopCommand(parsed.args.slice(commandIndex + 1).join(' '), depth + 1);
|
|
332
320
|
});
|
|
333
321
|
}
|
|
322
|
+
function queryPathOperands(analysis) {
|
|
323
|
+
return analysis.kind === 'proven-readonly' ? analysis.ir.pathOperands : [];
|
|
324
|
+
}
|
|
325
|
+
function shellOperandMatchesClass(operand, className, options) {
|
|
326
|
+
const reference = className === 'h' ? containsHClassReference(operand.value) : containsLClassReference(operand.value);
|
|
327
|
+
if (reference)
|
|
328
|
+
return true;
|
|
329
|
+
if (operand.access === 'recursive') {
|
|
330
|
+
return className === 'h'
|
|
331
|
+
? hClassReadScopeIncludesProtectedPath(operand.value, options)
|
|
332
|
+
: lClassGrantIncludesProtectedPath(operand.value, options);
|
|
333
|
+
}
|
|
334
|
+
return className === 'h'
|
|
335
|
+
? isHClassPath(operand.value, options)
|
|
336
|
+
: isLClassPath(operand.value, options);
|
|
337
|
+
}
|
|
334
338
|
/**
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
+
* A read-only recursive query only needs to cover H-class targets that exist
|
|
340
|
+
* now. This deliberately differs from a filesystem *grant*: a grant below
|
|
341
|
+
* any directory could later create a protected backup name, while a proven
|
|
342
|
+
* query cannot write and should still be able to inspect an ordinary logs/
|
|
343
|
+
* directory.
|
|
344
|
+
*/
|
|
345
|
+
function hClassReadScopeIncludesProtectedPath(value, options) {
|
|
346
|
+
const root = resolveProtectedCandidate(options.root ?? resolveRoot());
|
|
347
|
+
const scope = resolveProtectedCandidate(value, options.cwd);
|
|
348
|
+
if (isSameOrDescendant(root, scope))
|
|
349
|
+
return true;
|
|
350
|
+
if (!isSameOrDescendant(scope, root))
|
|
351
|
+
return false;
|
|
352
|
+
return getExistingHClassMaskTargets(root).some(target => isSameOrDescendant(target.path, scope));
|
|
353
|
+
}
|
|
354
|
+
function analyzeShellProtectedOperands(command, options) {
|
|
355
|
+
const analysis = analyzeReadonlyShellQuery(command);
|
|
356
|
+
if (analysis.kind === 'unproven') {
|
|
357
|
+
return {
|
|
358
|
+
analysis,
|
|
359
|
+
hClass: containsHClassReference(command),
|
|
360
|
+
lClass: containsLClassReference(command),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
const operands = queryPathOperands(analysis);
|
|
364
|
+
return {
|
|
365
|
+
analysis,
|
|
366
|
+
hClass: operands.some(operand => shellOperandMatchesClass(operand, 'h', options)),
|
|
367
|
+
lClass: operands.some(operand => shellOperandMatchesClass(operand, 'l', options)),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function protectedReadToolPaths(toolName, input) {
|
|
371
|
+
if (toolName === 'Read') {
|
|
372
|
+
return typeof input.file_path === 'string' ? [input.file_path] : [];
|
|
373
|
+
}
|
|
374
|
+
if (toolName === 'Glob') {
|
|
375
|
+
return [input.pattern, input.path].filter((value) => typeof value === 'string' && value.length > 0);
|
|
376
|
+
}
|
|
377
|
+
if (toolName === 'Grep') {
|
|
378
|
+
return typeof input.path === 'string' ? [input.path] : [];
|
|
379
|
+
}
|
|
380
|
+
return [];
|
|
381
|
+
}
|
|
382
|
+
function protectedReadToolMatchesLClass(toolName, input, options) {
|
|
383
|
+
const paths = protectedReadToolPaths(toolName, input);
|
|
384
|
+
if (paths.some(filePath => containsLClassReference(filePath) || isLClassPath(filePath, options))) {
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
if (toolName === 'Grep') {
|
|
388
|
+
const scope = typeof input.path === 'string' && input.path ? input.path : '.';
|
|
389
|
+
return lClassGrantIncludesProtectedPath(scope, options);
|
|
390
|
+
}
|
|
391
|
+
if (toolName === 'Glob') {
|
|
392
|
+
const scope = globReadScope(input, options);
|
|
393
|
+
return lClassGrantIncludesProtectedPath(scope, { root: options.root });
|
|
394
|
+
}
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
function protectedReadToolMatchesHClass(toolName, input, options) {
|
|
398
|
+
const paths = protectedReadToolPaths(toolName, input);
|
|
399
|
+
if (paths.some(filePath => containsHClassReference(filePath) || isHClassPath(filePath, options))) {
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
if (toolName === 'Grep') {
|
|
403
|
+
const scope = typeof input.path === 'string' && input.path ? input.path : '.';
|
|
404
|
+
return hClassReadScopeIncludesProtectedPath(scope, options);
|
|
405
|
+
}
|
|
406
|
+
if (toolName === 'Glob') {
|
|
407
|
+
const scope = globReadScope(input, options);
|
|
408
|
+
return hClassReadScopeIncludesProtectedPath(scope, options);
|
|
409
|
+
}
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
function globLiteralPrefix(pattern) {
|
|
413
|
+
const normalized = pattern.replace(/\\/g, '/');
|
|
414
|
+
const globIndex = normalized.search(/[*?[{]/);
|
|
415
|
+
return globIndex < 0 ? normalized : normalized.slice(0, globIndex).replace(/\/+$/, '');
|
|
416
|
+
}
|
|
417
|
+
function globReadScope(input, options) {
|
|
418
|
+
const pattern = typeof input.pattern === 'string' && input.pattern ? input.pattern : '**/*';
|
|
419
|
+
const baseValue = typeof input.path === 'string' && input.path ? input.path : '.';
|
|
420
|
+
const base = resolveProtectedCandidate(baseValue, options.cwd);
|
|
421
|
+
const prefix = globLiteralPrefix(pattern);
|
|
422
|
+
return path.isAbsolute(pattern)
|
|
423
|
+
? resolveProtectedCandidate(prefix || path.parse(pattern).root)
|
|
424
|
+
: resolveProtectedCandidate(prefix || '.', base);
|
|
425
|
+
}
|
|
426
|
+
function protectedReadToolStaysInWorkspace(toolName, input, projectPath) {
|
|
427
|
+
const workspace = resolveProtectedCandidate(projectPath);
|
|
428
|
+
if (toolName === 'Read') {
|
|
429
|
+
if (typeof input.file_path !== 'string' || !input.file_path)
|
|
430
|
+
return false;
|
|
431
|
+
return isSameOrDescendant(resolveProtectedCandidate(input.file_path, projectPath), workspace);
|
|
432
|
+
}
|
|
433
|
+
if (toolName === 'Grep') {
|
|
434
|
+
const scope = typeof input.path === 'string' && input.path ? input.path : '.';
|
|
435
|
+
return isSameOrDescendant(resolveProtectedCandidate(scope, projectPath), workspace);
|
|
436
|
+
}
|
|
437
|
+
if (toolName === 'Glob') {
|
|
438
|
+
const baseValue = typeof input.path === 'string' && input.path ? input.path : '.';
|
|
439
|
+
const base = resolveProtectedCandidate(baseValue, projectPath);
|
|
440
|
+
if (!isSameOrDescendant(base, workspace))
|
|
441
|
+
return false;
|
|
442
|
+
const scope = globReadScope(input, { cwd: projectPath });
|
|
443
|
+
return isSameOrDescendant(scope, workspace);
|
|
444
|
+
}
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
function readonlyShellStaysInWorkspace(analysis, projectPath) {
|
|
448
|
+
if (analysis.kind !== 'proven-readonly')
|
|
449
|
+
return false;
|
|
450
|
+
const workspace = resolveProtectedCandidate(projectPath);
|
|
451
|
+
return analysis.ir.pathOperands.every(operand => isSameOrDescendant(resolveProtectedCandidate(operand.value, projectPath), workspace));
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* 只读模式检查(用于 PreToolUse hook 和 canUseTool callback)。普通
|
|
455
|
+
* 路径上的受限查询 Bash 可自动执行;H/L-class 和未建模 Shell 仍拒绝。
|
|
339
456
|
*/
|
|
340
457
|
export function checkReadonly(toolName, input, projectPath, context) {
|
|
341
458
|
const readOnlyTools = new Set([
|
|
342
459
|
'Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch',
|
|
343
460
|
'TaskList', 'TaskGet', 'ToolSearch',
|
|
344
461
|
]);
|
|
345
|
-
if (readOnlyTools.has(toolName))
|
|
462
|
+
if (readOnlyTools.has(toolName)) {
|
|
463
|
+
const paths = protectedReadToolPaths(toolName, input);
|
|
464
|
+
const pathOptions = { cwd: projectPath, root: context?.root };
|
|
465
|
+
if (!protectedReadToolStaysInWorkspace(toolName, input, projectPath)) {
|
|
466
|
+
return { behavior: 'deny', message: '🔒 只读模式:文件读取范围不能越出当前项目目录' };
|
|
467
|
+
}
|
|
468
|
+
if (paths.some(filePath => containsHClassReference(filePath) || isHClassPath(filePath, pathOptions))) {
|
|
469
|
+
return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径' };
|
|
470
|
+
}
|
|
471
|
+
if (protectedReadToolMatchesHClass(toolName, input, pathOptions)) {
|
|
472
|
+
return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径' };
|
|
473
|
+
}
|
|
474
|
+
if (protectedReadToolMatchesLClass(toolName, input, pathOptions)) {
|
|
475
|
+
return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径' };
|
|
476
|
+
}
|
|
346
477
|
return { behavior: 'allow' };
|
|
478
|
+
}
|
|
347
479
|
if (toolName === 'Write' || toolName === 'Edit' || toolName === 'NotebookEdit') {
|
|
348
480
|
const filePath = (input.file_path || input.notebook_path);
|
|
349
481
|
logger.warn(`[ReadonlyCheck] 🔒 File write blocked: tool=${toolName} path=${filePath} project=${projectPath} session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
@@ -351,25 +483,27 @@ export function checkReadonly(toolName, input, projectPath, context) {
|
|
|
351
483
|
}
|
|
352
484
|
if (toolName === 'Bash') {
|
|
353
485
|
const cmd = input.command || '';
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
|
|
486
|
+
const protectedOperands = analyzeShellProtectedOperands(cmd, { cwd: projectPath, root: context?.root });
|
|
487
|
+
if (protectedOperands.analysis.kind === 'proven-readonly'
|
|
488
|
+
&& !protectedOperands.hClass
|
|
489
|
+
&& !protectedOperands.lClass
|
|
490
|
+
&& readonlyShellStaysInWorkspace(protectedOperands.analysis, projectPath)) {
|
|
491
|
+
return { behavior: 'allow' };
|
|
492
|
+
}
|
|
493
|
+
if (protectedOperands.hClass) {
|
|
494
|
+
logger.warn(`[ReadonlyCheck] 🔒 H-class path blocked in readonly shell: cmd="${cmd}" session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
495
|
+
return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径' };
|
|
496
|
+
}
|
|
497
|
+
if (protectedOperands.lClass) {
|
|
498
|
+
logger.warn(`[ReadonlyCheck] 🔒 L-class path blocked in readonly shell: cmd="${cmd}" session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
499
|
+
return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径' };
|
|
500
|
+
}
|
|
501
|
+
if (protectedOperands.analysis.kind === 'proven-readonly') {
|
|
502
|
+
return { behavior: 'deny', message: '🔒 只读模式:Shell 查询范围不能越出当前项目目录' };
|
|
369
503
|
}
|
|
370
504
|
const cmdPreview = cmd.length > 80 ? cmd.substring(0, 80) + '...' : cmd;
|
|
371
505
|
logger.warn(`[ReadonlyCheck] 🔒 Bash blocked by default: cmd="${cmdPreview}" session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
372
|
-
return { behavior: 'deny', message: '🔒 只读模式:Shell
|
|
506
|
+
return { behavior: 'deny', message: '🔒 只读模式:Shell 仅允许受限、可证明只读的普通路径查询' };
|
|
373
507
|
}
|
|
374
508
|
logger.warn(`[ReadonlyCheck] 🔒 Unknown tool blocked: tool=${toolName} session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
375
509
|
return { behavior: 'deny', message: `🔒 只读模式:未声明为只读的工具 ${toolName} 已拒绝执行` };
|
|
@@ -577,53 +711,6 @@ function collectFileChangePaths(value, output) {
|
|
|
577
711
|
}
|
|
578
712
|
}
|
|
579
713
|
}
|
|
580
|
-
function shellCommandReferencesLClass(command, options) {
|
|
581
|
-
const normalized = command.replace(/\\/g, '/');
|
|
582
|
-
if (containsLClassReference(normalized))
|
|
583
|
-
return true;
|
|
584
|
-
const argv = parseLiteralShellArgv(command);
|
|
585
|
-
return argv?.slice(1).some(argument => {
|
|
586
|
-
if (argument === '-' || argument === '--' || argument.startsWith('-'))
|
|
587
|
-
return false;
|
|
588
|
-
return isLClassPath(argument, options);
|
|
589
|
-
}) ?? false;
|
|
590
|
-
}
|
|
591
|
-
function isVerifiedLClassReadOnlyShellCommand(command) {
|
|
592
|
-
const argv = parseLiteralShellArgv(command);
|
|
593
|
-
if (!argv || !L_CLASS_READONLY_SHELL_EXECUTABLES.has(argv[0]))
|
|
594
|
-
return false;
|
|
595
|
-
switch (argv[0].replace(/^.*\//, '')) {
|
|
596
|
-
case 'grep':
|
|
597
|
-
// grep never mutates its operands. Shell composition is rejected by the
|
|
598
|
-
// literal parser before this point.
|
|
599
|
-
return true;
|
|
600
|
-
case 'rg':
|
|
601
|
-
// ripgrep's --pre hooks may execute an arbitrary process.
|
|
602
|
-
return !argv.slice(1).some(argument => RG_PROCESS_SPAWNING_OPTION_RE.test(argument));
|
|
603
|
-
case 'find':
|
|
604
|
-
// Keep only the predicates/actions which cannot create, mutate, or
|
|
605
|
-
// execute files. Other find options remain fail-closed.
|
|
606
|
-
return !argv.slice(1).some(argument => FIND_MUTATING_PRIMARY_RE.test(argument));
|
|
607
|
-
case 'sed': {
|
|
608
|
-
// sed can write via -i or a `w` command. Permit only the bounded
|
|
609
|
-
// inspection form used for line-range reads: sed -n 'START[,END]p'.
|
|
610
|
-
const quiet = argv[1] === '-n' || argv[1] === '--quiet' || argv[1] === '--silent';
|
|
611
|
-
const program = quiet ? argv[2] : undefined;
|
|
612
|
-
return !!program
|
|
613
|
-
&& /^(?:\d+(?:,\d+)?|\$)?p$/.test(program)
|
|
614
|
-
// GNU sed accepts options after the script as well. Reject every
|
|
615
|
-
// later option so -i/-e/-f cannot be smuggled into this read form.
|
|
616
|
-
&& !argv.slice(3).some(argument => argument.startsWith('-'));
|
|
617
|
-
}
|
|
618
|
-
default:
|
|
619
|
-
return true;
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
function isUnverifiedLClassShellCommand(command, options) {
|
|
623
|
-
if (!shellCommandReferencesLClass(command, options))
|
|
624
|
-
return false;
|
|
625
|
-
return !isVerifiedLClassReadOnlyShellCommand(command);
|
|
626
|
-
}
|
|
627
714
|
function requestsFilesystemRoot(value) {
|
|
628
715
|
if (Array.isArray(value))
|
|
629
716
|
return value.some(requestsFilesystemRoot);
|
|
@@ -647,8 +734,11 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
647
734
|
const paths = [];
|
|
648
735
|
const grantPaths = [];
|
|
649
736
|
const projectRootGrantSubpaths = [];
|
|
650
|
-
if (['Read', '
|
|
651
|
-
|
|
737
|
+
if (['Read', 'Glob', 'Grep'].includes(toolName)) {
|
|
738
|
+
paths.push(...protectedReadToolPaths(toolName, input));
|
|
739
|
+
}
|
|
740
|
+
else if (['Write', 'Edit', 'NotebookEdit'].includes(toolName)) {
|
|
741
|
+
const filePath = input.file_path ?? input.notebook_path ?? input.path;
|
|
652
742
|
if (typeof filePath === 'string' && filePath)
|
|
653
743
|
paths.push(filePath);
|
|
654
744
|
}
|
|
@@ -661,9 +751,13 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
661
751
|
collectFilesystemGrantPaths(input.permissions, grantPaths, projectRootGrantSubpaths);
|
|
662
752
|
}
|
|
663
753
|
else if (toolName === 'Bash') {
|
|
664
|
-
const command = typeof input.command === 'string' ? input.command
|
|
665
|
-
|
|
666
|
-
|
|
754
|
+
const command = typeof input.command === 'string' ? input.command : '';
|
|
755
|
+
const protectedOperands = analyzeShellProtectedOperands(command, {
|
|
756
|
+
cwd: context?.projectPath,
|
|
757
|
+
root: context?.root,
|
|
758
|
+
});
|
|
759
|
+
if (protectedOperands.hClass) {
|
|
760
|
+
logger.warn(`[H-Class Protection] 🔒 Protected filesystem operand in shell command: tool=${toolName} ` +
|
|
667
761
|
`session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
668
762
|
return {
|
|
669
763
|
behavior: 'deny',
|
|
@@ -686,6 +780,15 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
686
780
|
// filesystem operation is checked again when it reaches Read/Write/Bash,
|
|
687
781
|
// FileChange, or PermissionGrant above.
|
|
688
782
|
const pathOptions = { cwd: context?.projectPath, root: context?.root };
|
|
783
|
+
if (['Read', 'Glob', 'Grep'].includes(toolName)
|
|
784
|
+
&& protectedReadToolMatchesHClass(toolName, input, pathOptions)) {
|
|
785
|
+
logger.warn(`[H-Class Protection] 🔒 Protected filesystem read scope blocked: tool=${toolName} ` +
|
|
786
|
+
`session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
787
|
+
return {
|
|
788
|
+
behavior: 'deny',
|
|
789
|
+
message: '🔒 文件读取范围包含受保护的 H 类配置/证书/快照路径,agent 不可直接操作',
|
|
790
|
+
};
|
|
791
|
+
}
|
|
689
792
|
if (requestsFilesystemRoot(input.permissions) || requestsFilesystemRoot(input.additionalPermissions)) {
|
|
690
793
|
logger.warn(`[H-Class Protection] 🔒 Permission grant covers filesystem root: tool=${toolName} ` +
|
|
691
794
|
`session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
@@ -771,8 +874,14 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
771
874
|
* operations may mutate, delete, move, replace, or grant write access to them.
|
|
772
875
|
*/
|
|
773
876
|
export function checkLClassWrite(toolName, input, context) {
|
|
774
|
-
if (['Read', 'Glob', 'Grep'].includes(toolName))
|
|
877
|
+
if (['Read', 'Glob', 'Grep'].includes(toolName)) {
|
|
878
|
+
const pathOptions = { cwd: context?.projectPath, root: context?.root };
|
|
879
|
+
const lClassRead = protectedReadToolMatchesLClass(toolName, input, pathOptions);
|
|
880
|
+
if (lClassRead && context?.permissionMode === 'readonly') {
|
|
881
|
+
return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径' };
|
|
882
|
+
}
|
|
775
883
|
return { behavior: 'allow' };
|
|
884
|
+
}
|
|
776
885
|
const paths = [];
|
|
777
886
|
const writeGrantPaths = [];
|
|
778
887
|
const projectRootWriteSubpaths = [];
|
|
@@ -794,14 +903,19 @@ export function checkLClassWrite(toolName, input, context) {
|
|
|
794
903
|
collectFilesystemWriteGrantPaths(input.permissions, writeGrantPaths, projectRootWriteSubpaths);
|
|
795
904
|
}
|
|
796
905
|
else if (toolName === 'Bash') {
|
|
797
|
-
const command = typeof input.command === 'string' ? input.command
|
|
906
|
+
const command = typeof input.command === 'string' ? input.command : '';
|
|
798
907
|
const pathOptions = { cwd: context?.projectPath, root: context?.root };
|
|
799
|
-
|
|
800
|
-
|
|
908
|
+
const protectedOperands = analyzeShellProtectedOperands(command, pathOptions);
|
|
909
|
+
const lClassDenied = protectedOperands.lClass
|
|
910
|
+
&& (context?.permissionMode === 'readonly' || protectedOperands.analysis.kind === 'unproven');
|
|
911
|
+
if (lClassDenied) {
|
|
912
|
+
logger.warn(`[L-Class Protection] L-class shell access denied: tool=${toolName} ` +
|
|
801
913
|
`session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
802
914
|
return {
|
|
803
915
|
behavior: 'deny',
|
|
804
|
-
message:
|
|
916
|
+
message: context?.permissionMode === 'readonly'
|
|
917
|
+
? '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径'
|
|
918
|
+
: '🔒 L-class 路径仅允许受限、可证明只读的命令;未知 Shell 语法失败关闭。修改必须通过受控的 EvolCore 命令',
|
|
805
919
|
};
|
|
806
920
|
}
|
|
807
921
|
collectFilesystemWriteGrantPaths(input.additionalPermissions, writeGrantPaths, projectRootWriteSubpaths);
|
|
@@ -957,6 +1071,29 @@ function prepareBoundedOutputInput(input, command, kind, context) {
|
|
|
957
1071
|
export function evaluateToolPreflight(toolName, input, context) {
|
|
958
1072
|
if (toolName === 'Bash') {
|
|
959
1073
|
const command = typeof input.command === 'string' ? input.command : '';
|
|
1074
|
+
if (context.sessionId && /^\s*(?:command\s+-v|which|type)\s+ec\s*$/i.test(command)) {
|
|
1075
|
+
return {
|
|
1076
|
+
behavior: 'deny',
|
|
1077
|
+
input,
|
|
1078
|
+
message: '🔒 EvolCore 托管会话不需要探测 `ec` 是否存在;请不要调用 `command -v`/`which`/`type`,也不要拼接其它命令。通信必须走当前 session 的 delegated sender。',
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
const argv = context.sessionId ? parseLiteralShellArgv(command) : null;
|
|
1082
|
+
const isOwnAgentMdPublish = !!argv
|
|
1083
|
+
&& argv.length === 5
|
|
1084
|
+
&& argv[0] === 'ec'
|
|
1085
|
+
&& argv[1] === 'aid'
|
|
1086
|
+
&& argv[2] === 'agentmd'
|
|
1087
|
+
&& argv[3] === 'put'
|
|
1088
|
+
&& !!context.selfAid
|
|
1089
|
+
&& argv[4].replace(/^@/, '') === context.selfAid.replace(/^@/, '');
|
|
1090
|
+
if (context.sessionId && /^\s*ec\s+aid(?:\s|$)/i.test(command) && !isOwnAgentMdPublish) {
|
|
1091
|
+
return {
|
|
1092
|
+
behavior: 'deny',
|
|
1093
|
+
input,
|
|
1094
|
+
message: '🔒 EvolCore 托管会话仅允许通过 `ec aid agentmd put <当前 self AID>` 发布自身名片;禁止通过 `ec aid` 自行发现、切换或操作其他身份。',
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
960
1097
|
const ecCommand = classifyEvolcoreShellCommand(command);
|
|
961
1098
|
if (ecCommand.kind === 'literal') {
|
|
962
1099
|
return { behavior: 'allow', input, reason: 'ec-command' };
|
|
@@ -982,7 +1119,7 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
982
1119
|
return {
|
|
983
1120
|
behavior: 'deny',
|
|
984
1121
|
input,
|
|
985
|
-
message: '🔒 EC
|
|
1122
|
+
message: '🔒 EC 命令调用被拒绝:一次 Bash/exec_command 只能执行一条完整的 `ec` 命令。请直接调用 `ec ...`,不要先用 `command -v`/`ec aid` 探测,也不要使用 `&&`、`||`、`;`、`|`、重定向、变量展开、子 shell、`sh -c` 或 `bash -lc` 拼接其它命令。',
|
|
986
1123
|
};
|
|
987
1124
|
}
|
|
988
1125
|
const boundedOutput = parseBoundedOutputShellCommand(command);
|
|
@@ -1020,6 +1157,7 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1020
1157
|
channel: context.channel,
|
|
1021
1158
|
peerId: context.userId,
|
|
1022
1159
|
role: context.role,
|
|
1160
|
+
permissionMode: context.permissionMode,
|
|
1023
1161
|
projectPath: context.projectPath,
|
|
1024
1162
|
workspacePath: context.workspacePath,
|
|
1025
1163
|
root: context.root,
|
|
@@ -293,10 +293,11 @@ export function getExistingLClassReadOnlyTargets(root = resolveRoot()) {
|
|
|
293
293
|
catch { }
|
|
294
294
|
return [...targets.values()];
|
|
295
295
|
}
|
|
296
|
-
export function buildClaudeProtectedFilesystem(root = resolveRoot()) {
|
|
296
|
+
export function buildClaudeProtectedFilesystem(root = resolveRoot(), options = {}) {
|
|
297
297
|
const hClassPaths = getExistingHClassMaskTargets(root).map(target => target.path);
|
|
298
298
|
const lClassPaths = getExistingLClassReadOnlyTargets(root).map(target => target.path);
|
|
299
299
|
const allPaths = [...new Set([...hClassPaths, ...lClassPaths])];
|
|
300
|
+
const denyRead = options.denyLClassRead ? allPaths : hClassPaths;
|
|
300
301
|
if (allPaths.length > CLAUDE_PROTECTED_TARGET_LIMIT) {
|
|
301
302
|
throw new Error(`[ClaudeSandbox] protected target limit exceeded: ${allPaths.length} > ${CLAUDE_PROTECTED_TARGET_LIMIT}`);
|
|
302
303
|
}
|
|
@@ -309,7 +310,7 @@ export function buildClaudeProtectedFilesystem(root = resolveRoot()) {
|
|
|
309
310
|
}
|
|
310
311
|
}
|
|
311
312
|
return {
|
|
312
|
-
denyRead: [...new Set(
|
|
313
|
+
denyRead: [...new Set(denyRead)],
|
|
313
314
|
denyWrite: allPaths,
|
|
314
315
|
};
|
|
315
316
|
}
|
|
@@ -321,16 +322,18 @@ export function buildCodexHClassFilesystemRules(root = resolveRoot()) {
|
|
|
321
322
|
...Object.fromEntries(getHClassSandboxPatterns(root).map(pattern => [pattern, 'deny'])),
|
|
322
323
|
};
|
|
323
324
|
}
|
|
324
|
-
export function buildCodexProtectedFilesystemRules(root = resolveRoot()) {
|
|
325
|
+
export function buildCodexProtectedFilesystemRules(root = resolveRoot(), options = {}) {
|
|
325
326
|
const lClassRules = {};
|
|
327
|
+
const lClassAccess = options.denyLClassRead ? 'deny' : 'read';
|
|
326
328
|
for (const target of getExistingLClassReadOnlyTargets(root)) {
|
|
327
329
|
if (target.kind === 'directory') {
|
|
328
|
-
if (path.basename(target.path) === 'triggers')
|
|
329
|
-
lClassRules[target.path] =
|
|
330
|
-
|
|
330
|
+
if (options.denyLClassRead || path.basename(target.path) === 'triggers') {
|
|
331
|
+
lClassRules[target.path] = lClassAccess;
|
|
332
|
+
}
|
|
333
|
+
lClassRules[path.join(target.path, '**')] = lClassAccess;
|
|
331
334
|
}
|
|
332
335
|
else {
|
|
333
|
-
lClassRules[target.path] =
|
|
336
|
+
lClassRules[target.path] = lClassAccess;
|
|
334
337
|
}
|
|
335
338
|
}
|
|
336
339
|
return {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export async function disposeAgentInstances(instances, onError) {
|
|
2
|
+
await Promise.allSettled(instances.map(async (instance) => {
|
|
3
|
+
try {
|
|
4
|
+
await instance.agent.dispose?.();
|
|
5
|
+
}
|
|
6
|
+
catch (error) {
|
|
7
|
+
onError?.(instance, error);
|
|
8
|
+
}
|
|
9
|
+
}));
|
|
10
|
+
}
|
|
11
|
+
export function createRunnerReloadTransaction(input) {
|
|
12
|
+
const prefix = `${input.aid}::`;
|
|
13
|
+
const previous = [...input.agentMap.entries()]
|
|
14
|
+
.filter(([key]) => key.startsWith(prefix))
|
|
15
|
+
.map(([key, agent]) => {
|
|
16
|
+
const split = key.lastIndexOf('::');
|
|
17
|
+
return {
|
|
18
|
+
evolagentName: key.slice(0, split),
|
|
19
|
+
baseagent: key.slice(split + 2),
|
|
20
|
+
agent,
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
let committed = false;
|
|
24
|
+
let stagedDisposed = false;
|
|
25
|
+
const disposeStaged = async () => {
|
|
26
|
+
if (stagedDisposed)
|
|
27
|
+
return;
|
|
28
|
+
stagedDisposed = true;
|
|
29
|
+
await disposeAgentInstances(input.staged, input.onDisposeError);
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
commit() {
|
|
33
|
+
if (committed)
|
|
34
|
+
return;
|
|
35
|
+
for (const instance of previous)
|
|
36
|
+
input.agentMap.delete(`${instance.evolagentName}::${instance.baseagent}`);
|
|
37
|
+
for (const instance of input.staged)
|
|
38
|
+
input.agentMap.set(`${instance.evolagentName}::${instance.baseagent}`, instance.agent);
|
|
39
|
+
committed = true;
|
|
40
|
+
},
|
|
41
|
+
async rollback() {
|
|
42
|
+
if (committed) {
|
|
43
|
+
for (const instance of input.staged)
|
|
44
|
+
input.agentMap.delete(`${instance.evolagentName}::${instance.baseagent}`);
|
|
45
|
+
for (const instance of previous)
|
|
46
|
+
input.agentMap.set(`${instance.evolagentName}::${instance.baseagent}`, instance.agent);
|
|
47
|
+
committed = false;
|
|
48
|
+
}
|
|
49
|
+
await disposeStaged();
|
|
50
|
+
},
|
|
51
|
+
async finalize() {
|
|
52
|
+
if (!committed)
|
|
53
|
+
return;
|
|
54
|
+
await disposeAgentInstances(previous, input.onDisposeError);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|