evolcore 0.0.18 → 0.0.20
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 +39 -0
- package/README.md +2 -0
- package/bin/install-codex-managed-hooks.mjs +61 -0
- package/dist/agents/claude-runner.js +4 -3
- package/dist/agents/codex-runner.js +25 -20
- package/dist/agents/ecagent-runner.js +3 -2
- package/dist/aun/aid/agentmd.js +7 -0
- package/dist/aun/msg/group.js +14 -3
- package/dist/aun/msg/p2p.js +21 -11
- package/dist/aun/outbox.js +144 -19
- package/dist/channels/aun.js +621 -211
- package/dist/cli/daemon-commands.js +41 -8
- package/dist/cli/index.js +1 -0
- package/dist/cli/init.js +55 -15
- package/dist/cli/restart-monitor.js +3 -3
- package/dist/config/aun-gateway-config.js +2 -0
- package/dist/config/config-manager.js +92 -8
- package/dist/config/config-operation-service.js +1 -2
- package/dist/config/gateway-config.js +9 -7
- package/dist/config/lifecycle.js +16 -5
- package/dist/config-store.js +13 -6
- package/dist/core/auth/authorization-audit.js +5 -2
- package/dist/core/bootstrap-messages.js +2 -2
- package/dist/core/bootstrap-service.js +21 -36
- package/dist/core/channel-loader.js +0 -2
- package/dist/core/data-migration.js +10 -4
- package/dist/core/evolagent.js +5 -4
- package/dist/core/message/message-bridge.js +6 -11
- package/dist/core/message/response-engine.js +62 -6
- package/dist/core/permission/ec-command-parser.js +203 -24
- package/dist/core/permission/sandbox-runtime.js +46 -12
- package/dist/core/permission/tool-policy.js +116 -47
- package/dist/core/relation/peer-identity.js +18 -0
- package/dist/eck/kit-renderer.js +17 -8
- package/dist/index.js +30 -19
- package/dist/ipc.js +6 -1
- package/dist/paths.js +0 -3
- package/dist/utils/stats.js +52 -18
- package/dist/utils/welcome.js +2 -2
- package/kits/docs/path-registry.md +1 -1
- package/kits/rules/01-overview.md +1 -1
- package/kits/rules/02-navigation.md +2 -2
- package/kits/rules/03-identity.md +1 -1
- package/kits/rules/05-venue.md +1 -1
- package/kits/schemas/_meta.json +7 -4
- package/kits/schemas/agent-config.schema.10.json +2 -1
- package/kits/schemas/agent-config.schema.11.json +408 -0
- package/kits/schemas/daemon.schema.5.json +136 -0
- package/kits/schemas/defaults.schema.5.json +107 -0
- package/package.json +2 -1
- package/dist/core/message/pause-controller.js +0 -53
|
@@ -285,6 +285,48 @@ function appendLClassReadOnlyBinds(args, root) {
|
|
|
285
285
|
args.push('--ro-bind', target.path, target.path);
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
|
+
export function ensureCodexManagedMountpoint(systemConfigDirectory = '/etc/codex', effectiveUid = typeof process.getuid === 'function' ? process.getuid() : undefined) {
|
|
289
|
+
let systemConfigStat;
|
|
290
|
+
try {
|
|
291
|
+
systemConfigStat = fs.lstatSync(systemConfigDirectory);
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
if (error.code !== 'ENOENT') {
|
|
295
|
+
throw new Error(`Codex managed requirements mountpoint is unavailable: ${systemConfigDirectory} `
|
|
296
|
+
+ `could not be inspected (${error instanceof Error ? error.message : String(error)})`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (!systemConfigStat && process.platform === 'linux' && effectiveUid === 0) {
|
|
300
|
+
let created = false;
|
|
301
|
+
try {
|
|
302
|
+
fs.mkdirSync(systemConfigDirectory, { mode: 0o755 });
|
|
303
|
+
created = true;
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
if (error.code !== 'EEXIST') {
|
|
307
|
+
throw new Error(`Codex managed requirements mountpoint is unavailable: ${systemConfigDirectory} `
|
|
308
|
+
+ `could not be created by the root daemon (${error instanceof Error ? error.message : String(error)})`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (created)
|
|
312
|
+
fs.chmodSync(systemConfigDirectory, 0o755);
|
|
313
|
+
try {
|
|
314
|
+
systemConfigStat = fs.lstatSync(systemConfigDirectory);
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
throw new Error(`Codex managed requirements mountpoint is unavailable: ${systemConfigDirectory} `
|
|
318
|
+
+ `could not be verified after creation (${error instanceof Error ? error.message : String(error)})`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (!systemConfigStat) {
|
|
322
|
+
throw new Error(`Codex managed requirements mountpoint is unavailable: ${systemConfigDirectory} `
|
|
323
|
+
+ 'must be created as a real directory during installation; run: sudo install -d -m 0755 /etc/codex');
|
|
324
|
+
}
|
|
325
|
+
if (!systemConfigStat.isDirectory() || systemConfigStat.isSymbolicLink()) {
|
|
326
|
+
throw new Error(`Codex managed requirements mountpoint is unsafe: ${systemConfigDirectory} `
|
|
327
|
+
+ 'must be a real directory');
|
|
328
|
+
}
|
|
329
|
+
}
|
|
288
330
|
export function buildHClassGuardCommand(executable, executableArgs, root = resolveRoot(), options = {}) {
|
|
289
331
|
const bubblewrapPath = resolveBubblewrapPath();
|
|
290
332
|
if (!bubblewrapPath)
|
|
@@ -306,18 +348,10 @@ export function buildHClassGuardCommand(executable, executableArgs, root = resol
|
|
|
306
348
|
}
|
|
307
349
|
const managedDirectory = path.dirname(requirements);
|
|
308
350
|
const systemConfigDirectory = '/etc/codex';
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
catch {
|
|
314
|
-
throw new Error(`Codex managed requirements mountpoint is unavailable: ${systemConfigDirectory} `
|
|
315
|
-
+ 'must be created as a real directory during installation');
|
|
316
|
-
}
|
|
317
|
-
if (!systemConfigStat.isDirectory() || systemConfigStat.isSymbolicLink()) {
|
|
318
|
-
throw new Error(`Codex managed requirements mountpoint is unsafe: ${systemConfigDirectory} `
|
|
319
|
-
+ 'must be a real directory');
|
|
320
|
-
}
|
|
351
|
+
// The npm lifecycle and public installer provision this path. Root-owned
|
|
352
|
+
// legacy daemons may repair only the missing-directory case; unsafe path
|
|
353
|
+
// types and unprivileged processes remain fail-closed.
|
|
354
|
+
ensureCodexManagedMountpoint(systemConfigDirectory);
|
|
321
355
|
// Keep the copied hook immutable even when a session uses bypass mode.
|
|
322
356
|
args.push('--ro-bind', managedDirectory, managedDirectory);
|
|
323
357
|
// The guard starts from a writable root so owner-bypass workspaces keep
|
|
@@ -4,7 +4,7 @@ import { randomUUID } from 'crypto';
|
|
|
4
4
|
import { logger } from '../../utils/logger.js';
|
|
5
5
|
import { resolveRoot } from '../../paths.js';
|
|
6
6
|
import { containsHClassReference, containsLClassReference, checkProtectedPathAccess, getExistingHClassMaskTargets, hClassGrantIncludesProtectedPath, isHClassPath, isSameOrDescendant, isLClassPath, lClassGrantIncludesProtectedPath, resolveProtectedCandidate, resolveProtectedCandidateWithoutFinalSymlink, } from '../protected-paths.js';
|
|
7
|
-
import { classifyEvolcoreShellCommand, parseCodexToolCommandArgv, parseBoundedOutputShellCommand, parseLiteralShellArgv,
|
|
7
|
+
import { classifyEvolcoreShellCommand, parseCodexToolCommandArgv, parseBoundedOutputShellCommand, parseLiteralShellArgv, resolveCodexShellCarrierArgv, } from './ec-command-parser.js';
|
|
8
8
|
import { analyzeReadonlyShellQuery, } from './readonly-shell-query.js';
|
|
9
9
|
/** Resolve the session-owned temporary root supplied by the execution host. */
|
|
10
10
|
export function resolveManagedTempDir(env = process.env) {
|
|
@@ -425,7 +425,7 @@ function shellOperandMatchesClass(operand, className, options, accessKind = oper
|
|
|
425
425
|
const workspace = options.cwd ? resolveProtectedCandidate(options.cwd) : undefined;
|
|
426
426
|
if (isSameOrDescendant(candidate, managedTemp)
|
|
427
427
|
&& (!workspace || !isSameOrDescendant(candidate, workspace))) {
|
|
428
|
-
return
|
|
428
|
+
return undefined;
|
|
429
429
|
}
|
|
430
430
|
}
|
|
431
431
|
if (className === 'h' && accessKind === 'metadata' && operand.access === 'exact') {
|
|
@@ -439,27 +439,31 @@ function shellOperandMatchesClass(operand, className, options, accessKind = oper
|
|
|
439
439
|
followFinalSymlink: false,
|
|
440
440
|
});
|
|
441
441
|
if (options.allowProtectedMetadata === false) {
|
|
442
|
-
return metadata.pathClass === 'h-file' || metadata.pathClass === 'h-directory'
|
|
442
|
+
return metadata.pathClass === 'h-file' || metadata.pathClass === 'h-directory'
|
|
443
|
+
? resolveProtectedCandidate(operand.value, options.cwd)
|
|
444
|
+
: undefined;
|
|
443
445
|
}
|
|
444
446
|
// The no-follow decision is authoritative for metadata. In particular,
|
|
445
447
|
// an ordinary symlink whose target is H-class must remain inspectable as a
|
|
446
448
|
// link; falling through to isHClassPath() would follow it and reject the
|
|
447
449
|
// otherwise safe lstat/stat operation.
|
|
448
450
|
if (metadata.pathClass !== 'unknown' && metadata.allowed) {
|
|
449
|
-
return
|
|
451
|
+
return undefined;
|
|
450
452
|
}
|
|
451
453
|
}
|
|
452
454
|
const reference = className === 'h' ? containsHClassReference(operand.value) : containsLClassReference(operand.value);
|
|
453
455
|
if (reference)
|
|
454
|
-
return
|
|
456
|
+
return resolveProtectedCandidate(operand.value, options.cwd);
|
|
455
457
|
if (operand.access === 'recursive') {
|
|
456
|
-
|
|
458
|
+
const matched = className === 'h'
|
|
457
459
|
? hClassReadScopeIncludesProtectedPath(operand.value, options)
|
|
458
460
|
: lClassGrantIncludesProtectedPath(operand.value, options);
|
|
461
|
+
return matched ? resolveProtectedCandidate(operand.value, options.cwd) : undefined;
|
|
459
462
|
}
|
|
460
|
-
|
|
463
|
+
const matched = className === 'h'
|
|
461
464
|
? isHClassPath(operand.value, options)
|
|
462
465
|
: isLClassPath(operand.value, options);
|
|
466
|
+
return matched ? resolveProtectedCandidate(operand.value, options.cwd) : undefined;
|
|
463
467
|
}
|
|
464
468
|
/**
|
|
465
469
|
* A read-only recursive query only needs to cover H-class targets that exist
|
|
@@ -489,16 +493,41 @@ function analyzeShellProtectedOperands(command, options) {
|
|
|
489
493
|
};
|
|
490
494
|
}
|
|
491
495
|
const operands = queryPathOperands(analysis);
|
|
492
|
-
|
|
493
|
-
|
|
496
|
+
// Only parsed path operands can establish a lock-file diagnostic. Searching
|
|
497
|
+
// the raw command would mistake a grep/rg pattern or report text for an
|
|
498
|
+
// actual protected filesystem target.
|
|
499
|
+
const lockDiagnostic = operands.some(({ operand }) => containsLockPathReference(operand.value))
|
|
500
|
+
|| hasFindLockPattern(analysis);
|
|
501
|
+
const hClassPath = operands.map(({ operand, accessKind }) => shellOperandMatchesClass(operand, 'h', options, accessKind)).find(Boolean);
|
|
502
|
+
const lClassPath = operands.map(({ operand, accessKind }) => shellOperandMatchesClass(operand, 'l', options, accessKind)).find(Boolean);
|
|
494
503
|
return {
|
|
495
504
|
analysis,
|
|
496
|
-
hClass: lockDiagnostic ||
|
|
497
|
-
lClass:
|
|
498
|
-
pathHClass,
|
|
505
|
+
hClass: lockDiagnostic || !!hClassPath,
|
|
506
|
+
lClass: !!lClassPath,
|
|
507
|
+
pathHClass: !!hClassPath,
|
|
499
508
|
lockDiagnostic,
|
|
509
|
+
hClassPath,
|
|
510
|
+
lClassPath,
|
|
500
511
|
};
|
|
501
512
|
}
|
|
513
|
+
/** Detect a find pattern such as `-name '*.lock'` from parsed argv only. */
|
|
514
|
+
function hasFindLockPattern(analysis) {
|
|
515
|
+
if (analysis.kind !== 'proven-readonly')
|
|
516
|
+
return false;
|
|
517
|
+
return analysis.ir.pipelines.some(pipeline => pipeline.some(command => {
|
|
518
|
+
if (command.executable !== 'find')
|
|
519
|
+
return false;
|
|
520
|
+
for (let index = 0; index < command.argv.length - 1; index++) {
|
|
521
|
+
if (!['-name', '-iname', '-path', '-ipath', '-wholename'].includes(command.argv[index]))
|
|
522
|
+
continue;
|
|
523
|
+
if (/(?:^|[\\/])[^\\/]*\.lock(?:$|[\\/])/i.test(command.argv[index + 1]))
|
|
524
|
+
return true;
|
|
525
|
+
if (/^EVOLCORE_RUNTIME_LOCK_DIR$/i.test(command.argv[index + 1]))
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
return false;
|
|
529
|
+
}));
|
|
530
|
+
}
|
|
502
531
|
function protectedReadToolPaths(toolName, input) {
|
|
503
532
|
if (toolName === 'Read') {
|
|
504
533
|
return typeof input.file_path === 'string' ? [input.file_path] : [];
|
|
@@ -513,33 +542,43 @@ function protectedReadToolPaths(toolName, input) {
|
|
|
513
542
|
}
|
|
514
543
|
function protectedReadToolMatchesLClass(toolName, input, options) {
|
|
515
544
|
const paths = protectedReadToolPaths(toolName, input);
|
|
516
|
-
|
|
517
|
-
|
|
545
|
+
const direct = paths.find(filePath => containsLClassReference(filePath) || isLClassPath(filePath, options));
|
|
546
|
+
if (direct) {
|
|
547
|
+
return resolveProtectedCandidate(direct, options.cwd);
|
|
518
548
|
}
|
|
519
549
|
if (toolName === 'Grep') {
|
|
520
550
|
const scope = typeof input.path === 'string' && input.path ? input.path : '.';
|
|
521
|
-
return lClassGrantIncludesProtectedPath(scope, options)
|
|
551
|
+
return lClassGrantIncludesProtectedPath(scope, options)
|
|
552
|
+
? resolveProtectedCandidate(scope, options.cwd)
|
|
553
|
+
: undefined;
|
|
522
554
|
}
|
|
523
555
|
if (toolName === 'Glob') {
|
|
524
556
|
const scope = globReadScope(input, options);
|
|
525
|
-
return lClassGrantIncludesProtectedPath(scope, { root: options.root })
|
|
557
|
+
return lClassGrantIncludesProtectedPath(scope, { root: options.root })
|
|
558
|
+
? resolveProtectedCandidate(scope, options.cwd)
|
|
559
|
+
: undefined;
|
|
526
560
|
}
|
|
527
|
-
return
|
|
561
|
+
return undefined;
|
|
528
562
|
}
|
|
529
563
|
function protectedReadToolMatchesHClass(toolName, input, options) {
|
|
530
564
|
const paths = protectedReadToolPaths(toolName, input);
|
|
531
|
-
|
|
532
|
-
|
|
565
|
+
const direct = paths.find(filePath => containsHClassReference(filePath) || isHClassPath(filePath, options));
|
|
566
|
+
if (direct) {
|
|
567
|
+
return resolveProtectedCandidate(direct, options.cwd);
|
|
533
568
|
}
|
|
534
569
|
if (toolName === 'Grep') {
|
|
535
570
|
const scope = typeof input.path === 'string' && input.path ? input.path : '.';
|
|
536
|
-
return hClassReadScopeIncludesProtectedPath(scope, options)
|
|
571
|
+
return hClassReadScopeIncludesProtectedPath(scope, options)
|
|
572
|
+
? resolveProtectedCandidate(scope, options.cwd)
|
|
573
|
+
: undefined;
|
|
537
574
|
}
|
|
538
575
|
if (toolName === 'Glob') {
|
|
539
576
|
const scope = globReadScope(input, options);
|
|
540
|
-
return hClassReadScopeIncludesProtectedPath(scope, options)
|
|
577
|
+
return hClassReadScopeIncludesProtectedPath(scope, options)
|
|
578
|
+
? resolveProtectedCandidate(scope, options.cwd)
|
|
579
|
+
: undefined;
|
|
541
580
|
}
|
|
542
|
-
return
|
|
581
|
+
return undefined;
|
|
543
582
|
}
|
|
544
583
|
function globLiteralPrefix(pattern) {
|
|
545
584
|
const normalized = pattern.replace(/\\/g, '/');
|
|
@@ -646,14 +685,17 @@ export function checkReadonly(toolName, input, projectPath, context) {
|
|
|
646
685
|
if (!protectedReadToolStaysInWorkspace(toolName, input, projectPath)) {
|
|
647
686
|
return { behavior: 'deny', message: '🔒 只读模式:文件读取范围不能越出当前项目目录', policyCode: 'readonly_workspace_escape' };
|
|
648
687
|
}
|
|
649
|
-
|
|
650
|
-
|
|
688
|
+
const directHPath = paths.find(filePath => containsHClassReference(filePath) || isHClassPath(filePath, pathOptions));
|
|
689
|
+
if (directHPath) {
|
|
690
|
+
return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径', policyCode: 'readonly_h_class_read', matchedPath: resolveProtectedCandidate(directHPath, projectPath) };
|
|
651
691
|
}
|
|
652
|
-
|
|
653
|
-
|
|
692
|
+
const hPath = protectedReadToolMatchesHClass(toolName, input, pathOptions);
|
|
693
|
+
if (hPath) {
|
|
694
|
+
return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径', policyCode: 'readonly_h_class_read', matchedPath: hPath };
|
|
654
695
|
}
|
|
655
|
-
|
|
656
|
-
|
|
696
|
+
const lPath = protectedReadToolMatchesLClass(toolName, input, pathOptions);
|
|
697
|
+
if (lPath) {
|
|
698
|
+
return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径', policyCode: 'readonly_l_class_read', matchedPath: lPath };
|
|
657
699
|
}
|
|
658
700
|
return { behavior: 'allow' };
|
|
659
701
|
}
|
|
@@ -691,11 +733,15 @@ export function checkReadonly(toolName, input, projectPath, context) {
|
|
|
691
733
|
}
|
|
692
734
|
if (protectedOperands.hClass) {
|
|
693
735
|
logger.warn(`[ReadonlyCheck] 🔒 H-class path blocked in readonly shell: cmd="${cmd}" session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
694
|
-
|
|
736
|
+
if (protectedOperands.analysis.kind === 'proven-readonly') {
|
|
737
|
+
return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径', policyCode: 'readonly_h_class_read', matchedPath: protectedOperands.hClassPath };
|
|
738
|
+
}
|
|
695
739
|
}
|
|
696
740
|
if (protectedOperands.lClass) {
|
|
697
741
|
logger.warn(`[ReadonlyCheck] 🔒 L-class path blocked in readonly shell: cmd="${cmd}" session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
698
|
-
|
|
742
|
+
if (protectedOperands.analysis.kind === 'proven-readonly') {
|
|
743
|
+
return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径', policyCode: 'readonly_l_class_read', matchedPath: protectedOperands.lClassPath };
|
|
744
|
+
}
|
|
699
745
|
}
|
|
700
746
|
if (protectedOperands.analysis.kind === 'proven-readonly') {
|
|
701
747
|
return { behavior: 'deny', message: '🔒 只读模式:Shell 查询范围不能越出当前项目目录', policyCode: 'readonly_workspace_escape' };
|
|
@@ -967,6 +1013,7 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
967
1013
|
return {
|
|
968
1014
|
behavior: 'deny',
|
|
969
1015
|
message: '🔒 临时文件必须位于当前会话的 $TMPDIR 内',
|
|
1016
|
+
matchedPath: tempCheck.paths[0],
|
|
970
1017
|
};
|
|
971
1018
|
}
|
|
972
1019
|
// A managed temp write is intentionally allowed to contain names such as
|
|
@@ -992,6 +1039,7 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
992
1039
|
return {
|
|
993
1040
|
behavior: 'deny',
|
|
994
1041
|
message: '🔒 Shell 命令涉及受保护的 H 类配置/证书/快照路径,agent 不可直接操作',
|
|
1042
|
+
matchedPath: protectedOperands.hClassPath,
|
|
995
1043
|
};
|
|
996
1044
|
}
|
|
997
1045
|
collectFilesystemGrantPaths(input.additionalPermissions, grantPaths, projectRootGrantSubpaths);
|
|
@@ -1010,14 +1058,17 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
1010
1058
|
// filesystem operation is checked again when it reaches Read/Write/Bash,
|
|
1011
1059
|
// FileChange, or PermissionGrant above.
|
|
1012
1060
|
const pathOptions = { cwd: context?.projectPath, root: context?.root };
|
|
1013
|
-
if (['Read', 'Glob', 'Grep'].includes(toolName)
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
`
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1061
|
+
if (['Read', 'Glob', 'Grep'].includes(toolName)) {
|
|
1062
|
+
const matchedReadPath = protectedReadToolMatchesHClass(toolName, input, pathOptions);
|
|
1063
|
+
if (matchedReadPath) {
|
|
1064
|
+
logger.warn(`[H-Class Protection] 🔒 Protected filesystem read scope blocked: tool=${toolName} ` +
|
|
1065
|
+
`session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
1066
|
+
return {
|
|
1067
|
+
behavior: 'deny',
|
|
1068
|
+
message: '🔒 文件读取范围包含受保护的 H 类配置/证书/快照路径,agent 不可直接操作',
|
|
1069
|
+
matchedPath: matchedReadPath,
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1021
1072
|
}
|
|
1022
1073
|
if (requestsFilesystemRoot(input.permissions) || requestsFilesystemRoot(input.additionalPermissions)) {
|
|
1023
1074
|
logger.warn(`[H-Class Protection] 🔒 Permission grant covers filesystem root: tool=${toolName} ` +
|
|
@@ -1036,6 +1087,7 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
1036
1087
|
return {
|
|
1037
1088
|
behavior: 'deny',
|
|
1038
1089
|
message: '🔒 请求的文件系统权限范围包含受保护的 H 类路径,已拒绝授权',
|
|
1090
|
+
matchedPath: resolveProtectedCandidate(grantPath, pathOptions.cwd),
|
|
1039
1091
|
};
|
|
1040
1092
|
}
|
|
1041
1093
|
}
|
|
@@ -1059,6 +1111,7 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
1059
1111
|
return {
|
|
1060
1112
|
behavior: 'deny',
|
|
1061
1113
|
message: '🔒 请求的 project_roots 权限范围包含受保护的 H 类路径,已拒绝授权',
|
|
1114
|
+
matchedPath: resolveProtectedCandidate(candidate, permissionWorkspace),
|
|
1062
1115
|
};
|
|
1063
1116
|
}
|
|
1064
1117
|
}
|
|
@@ -1093,7 +1146,8 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
1093
1146
|
behavior: 'deny',
|
|
1094
1147
|
message: `🔒 此文件受保护,agent 不可直接写入\n\n` +
|
|
1095
1148
|
`文件:${filePath}\n` +
|
|
1096
|
-
`类型:配置/快照/证书等系统关键文件${suggestion}
|
|
1149
|
+
`类型:配置/快照/证书等系统关键文件${suggestion}`,
|
|
1150
|
+
matchedPath: resolveProtectedCandidate(filePath, pathOptions.cwd),
|
|
1097
1151
|
};
|
|
1098
1152
|
}
|
|
1099
1153
|
}
|
|
@@ -1108,7 +1162,7 @@ export function checkLClassWrite(toolName, input, context) {
|
|
|
1108
1162
|
const pathOptions = { cwd: context?.projectPath, root: context?.root };
|
|
1109
1163
|
const lClassRead = protectedReadToolMatchesLClass(toolName, input, pathOptions);
|
|
1110
1164
|
if (lClassRead && context?.permissionMode === 'readonly') {
|
|
1111
|
-
return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径' };
|
|
1165
|
+
return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径', matchedPath: lClassRead };
|
|
1112
1166
|
}
|
|
1113
1167
|
return { behavior: 'allow' };
|
|
1114
1168
|
}
|
|
@@ -1149,6 +1203,7 @@ export function checkLClassWrite(toolName, input, context) {
|
|
|
1149
1203
|
message: context?.permissionMode === 'readonly'
|
|
1150
1204
|
? '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径'
|
|
1151
1205
|
: '🔒 L-class 路径允许读取,但当前 Bash 复合命令无法证明为只读,已拒绝执行。请改用受控的 EvolCore 读取命令或白名单只读命令',
|
|
1206
|
+
matchedPath: protectedOperands.lClassPath,
|
|
1152
1207
|
};
|
|
1153
1208
|
}
|
|
1154
1209
|
collectFilesystemWriteGrantPaths(input.additionalPermissions, writeGrantPaths, projectRootWriteSubpaths);
|
|
@@ -1167,6 +1222,7 @@ export function checkLClassWrite(toolName, input, context) {
|
|
|
1167
1222
|
return {
|
|
1168
1223
|
behavior: 'deny',
|
|
1169
1224
|
message: '🔒 请求的文件系统写权限包含 L-class 只读路径,已拒绝授权',
|
|
1225
|
+
matchedPath: resolveProtectedCandidate(grantPath, pathOptions.cwd),
|
|
1170
1226
|
};
|
|
1171
1227
|
}
|
|
1172
1228
|
}
|
|
@@ -1188,6 +1244,7 @@ export function checkLClassWrite(toolName, input, context) {
|
|
|
1188
1244
|
return {
|
|
1189
1245
|
behavior: 'deny',
|
|
1190
1246
|
message: `🔒 L-class 路径允许读取但禁止 Agent 直接修改、删除或移动\n\n文件:${filePath}\n\n💡 请使用对应的 EvolCore 受控命令修改`,
|
|
1247
|
+
matchedPath: resolveProtectedCandidate(filePath, pathOptions.cwd),
|
|
1191
1248
|
};
|
|
1192
1249
|
}
|
|
1193
1250
|
return { behavior: 'allow' };
|
|
@@ -1352,7 +1409,8 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1352
1409
|
? input.commandArgv
|
|
1353
1410
|
: undefined;
|
|
1354
1411
|
const outerArgv = explicitCommandArgv ?? parseLiteralShellArgv(command) ?? undefined;
|
|
1355
|
-
const
|
|
1412
|
+
const shellCarrier = outerArgv ? resolveCodexShellCarrierArgv(outerArgv) : undefined;
|
|
1413
|
+
const wrappedCommand = shellCarrier?.command;
|
|
1356
1414
|
const policyCommand = wrappedCommand ?? command;
|
|
1357
1415
|
if (context.sessionId && /^\s*(?:command\s+-v|which|type)\s+ec\s*$/i.test(policyCommand)) {
|
|
1358
1416
|
return {
|
|
@@ -1397,7 +1455,9 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1397
1455
|
policyCode: 'managed_ec_aid_scope_forbidden',
|
|
1398
1456
|
};
|
|
1399
1457
|
}
|
|
1400
|
-
const ecCommand = classifyEvolcoreShellCommand(policyCommand
|
|
1458
|
+
const ecCommand = classifyEvolcoreShellCommand(policyCommand, shellCarrier
|
|
1459
|
+
? { dialect: shellCarrier.dialect }
|
|
1460
|
+
: {});
|
|
1401
1461
|
if (ecCommand.kind === 'literal') {
|
|
1402
1462
|
return { behavior: 'allow', input, reason: 'ec-command' };
|
|
1403
1463
|
}
|
|
@@ -1406,25 +1466,34 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1406
1466
|
}
|
|
1407
1467
|
if (ecCommand.kind === 'composite') {
|
|
1408
1468
|
if (ecCommand.issue === 'unsafe-expansion') {
|
|
1469
|
+
const message = shellCarrier?.dialect === 'powershell'
|
|
1470
|
+
? '🔒 EC 双引号正文包含未转义的 PowerShell 展开;纯文字请优先使用单引号,或用 PowerShell 反引号转义 $ 和正文内的双引号。PowerShell 传给 ec 时会还原为原文字面量'
|
|
1471
|
+
: '🔒 EC 双引号正文包含未转义的 Shell 展开;请将反引号写成 \\`,将 $()、${}、$变量中的 $ 写成 \\$。Shell 传给 ec 时会还原为原文字面量';
|
|
1409
1472
|
return {
|
|
1410
1473
|
behavior: 'deny',
|
|
1411
1474
|
input,
|
|
1412
|
-
message
|
|
1475
|
+
message,
|
|
1413
1476
|
policyCode: 'ec_shell_unsafe_expansion',
|
|
1414
1477
|
};
|
|
1415
1478
|
}
|
|
1416
1479
|
if (ecCommand.issue === 'invalid-quote') {
|
|
1480
|
+
const message = shellCarrier?.dialect === 'powershell'
|
|
1481
|
+
? '🔒 EC 命令引号未闭合或存在错误嵌套;PowerShell 正文内的双引号请用反引号转义,或改用单引号包裹纯文字'
|
|
1482
|
+
: '🔒 EC 命令引号未闭合或存在错误嵌套;正文中的双引号请写成 \\"';
|
|
1417
1483
|
return {
|
|
1418
1484
|
behavior: 'deny',
|
|
1419
1485
|
input,
|
|
1420
|
-
message
|
|
1486
|
+
message,
|
|
1421
1487
|
policyCode: 'ec_shell_invalid_quote',
|
|
1422
1488
|
};
|
|
1423
1489
|
}
|
|
1490
|
+
const message = shellCarrier?.dialect === 'powershell'
|
|
1491
|
+
? '🔒 EC 命令调用被拒绝:一次 PowerShell/exec_command 只能执行一条完整的 `ec` 命令。请直接调用 `ec ...`,不要拼接探测命令,也不要使用 `&&`、`||`、`;`、`|`、重定向、变量展开或子表达式追加其它命令。'
|
|
1492
|
+
: '🔒 EC 命令调用被拒绝:一次 Bash/exec_command 只能执行一条完整的 `ec` 命令。请直接调用 `ec ...`,不要先用 `command -v`/`ec aid` 探测,也不要使用 `&&`、`||`、`;`、`|`、重定向、变量展开、子 shell、`sh -c` 或 `bash -lc` 拼接其它命令。';
|
|
1424
1493
|
return {
|
|
1425
1494
|
behavior: 'deny',
|
|
1426
1495
|
input,
|
|
1427
|
-
message
|
|
1496
|
+
message,
|
|
1428
1497
|
policyCode: 'ec_shell_composite_command',
|
|
1429
1498
|
};
|
|
1430
1499
|
}
|
|
@@ -1460,7 +1529,7 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1460
1529
|
allowProtectedMetadata: context.allowProtectedMetadata,
|
|
1461
1530
|
});
|
|
1462
1531
|
if (hClass.behavior === 'deny') {
|
|
1463
|
-
return { behavior: 'deny', input: checkedInput, message: hClass.message, policyCode: 'h_class_protection' };
|
|
1532
|
+
return { behavior: 'deny', input: checkedInput, message: hClass.message, policyCode: 'h_class_protection', matchedPath: hClass.matchedPath };
|
|
1464
1533
|
}
|
|
1465
1534
|
const lClass = checkLClassWrite(toolName, checkedInput, {
|
|
1466
1535
|
sessionId: context.sessionId,
|
|
@@ -1474,7 +1543,7 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1474
1543
|
managedTempDir: context.managedTempDir,
|
|
1475
1544
|
});
|
|
1476
1545
|
if (lClass.behavior === 'deny') {
|
|
1477
|
-
return { behavior: 'deny', input: checkedInput, message: lClass.message, policyCode: 'l_class_protection' };
|
|
1546
|
+
return { behavior: 'deny', input: checkedInput, message: lClass.message, policyCode: 'l_class_protection', matchedPath: lClass.matchedPath };
|
|
1478
1547
|
}
|
|
1479
1548
|
const dangerous = checkDangerousCommand(toolName, checkedInput);
|
|
1480
1549
|
if (dangerous.isDangerous
|
|
@@ -151,6 +151,24 @@ export class PeerIdentityCache {
|
|
|
151
151
|
* @param forceRefresh 强制刷新(忽略缓存时效)
|
|
152
152
|
*/
|
|
153
153
|
static async resolve(channelType, peerId, agentDir, store, forceRefresh = false) {
|
|
154
|
+
// A malformed transport envelope must never turn into a relation key such
|
|
155
|
+
// as `aun#` or trigger an agent.md lookup for an empty AID. Keep the
|
|
156
|
+
// fallback in-memory so callers can safely render an unknown peer without
|
|
157
|
+
// creating a bogus relation directory.
|
|
158
|
+
if (typeof peerId !== 'string' || !peerId.trim()) {
|
|
159
|
+
logger.debug(`[PeerIdentityCache] Ignored empty peer id: channel=${channelType}`);
|
|
160
|
+
return {
|
|
161
|
+
aid: '',
|
|
162
|
+
type: 'unknown',
|
|
163
|
+
isAgent: true,
|
|
164
|
+
agentMdHash: '',
|
|
165
|
+
agentMdUpdatedAt: 0,
|
|
166
|
+
verifiedAt: 0,
|
|
167
|
+
lastCheckedAt: Date.now(),
|
|
168
|
+
source: 'unknown',
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
peerId = peerId.trim();
|
|
154
172
|
// 1. 缓存检查
|
|
155
173
|
if (!forceRefresh && !this.needsRefresh(channelType, peerId, agentDir)) {
|
|
156
174
|
const cached = this.get(channelType, peerId, agentDir);
|
package/dist/eck/kit-renderer.js
CHANGED
|
@@ -4,6 +4,7 @@ import { isEckSnapshotsEnabled } from '../config-store.js';
|
|
|
4
4
|
import { eckDebugDir } from '../paths.js';
|
|
5
5
|
import { logger } from '../utils/logger.js';
|
|
6
6
|
import { clearRoleStoreCache } from '../config/role-store.js';
|
|
7
|
+
import { resolveAgentLifecycle } from '../config/lifecycle.js';
|
|
7
8
|
import { loadManifest, loadManifestMeta, invalidateManifestCache, evaluateWhen, renderTemplate, renderLoopSection, resolvePathWithDiag, loadSectionFiles, loadChildTemplate, buildPathMappings, shortenPath, } from './manifest-engine.js';
|
|
8
9
|
const DEFAULT_MANIFEST_FILE = 'eck_manifest.json';
|
|
9
10
|
// ── Caches ──
|
|
@@ -58,7 +59,15 @@ export function renderKitSections(ctx, manifestFile = DEFAULT_MANIFEST_FILE) {
|
|
|
58
59
|
const meta = loadManifestMeta(manifestFile);
|
|
59
60
|
const fileParts = [];
|
|
60
61
|
const fragmentParts = [];
|
|
61
|
-
|
|
62
|
+
// Legacy callers may omit lifecycle; preserve the same active default used
|
|
63
|
+
// by agent configuration normalization while leaving explicit invalid
|
|
64
|
+
// values untouched for fail-closed manifest conditions.
|
|
65
|
+
const resolvedLifecycle = resolveAgentLifecycle({ lifecycle: ctx.vars.lifecycle });
|
|
66
|
+
const vars = resolvedLifecycle === null
|
|
67
|
+
? ctx.vars
|
|
68
|
+
: { ...ctx.vars, lifecycle: resolvedLifecycle };
|
|
69
|
+
const renderCtx = { ...ctx, vars };
|
|
70
|
+
const pathMappings = buildPathMappings(vars);
|
|
62
71
|
const sessionCache = getSessionCache(ctx.sessionId);
|
|
63
72
|
const diagnostics = [];
|
|
64
73
|
// 总闸计数(跨所有段累计)
|
|
@@ -78,7 +87,7 @@ export function renderKitSections(ctx, manifestFile = DEFAULT_MANIFEST_FILE) {
|
|
|
78
87
|
if (capReached) {
|
|
79
88
|
diag.skippedByTotalCap = true;
|
|
80
89
|
// 仅当该段本会命中时才计入"未加载"集合(enabled 且 when 通过)
|
|
81
|
-
if (section.enabled !== false && evaluateWhen(section.when,
|
|
90
|
+
if (section.enabled !== false && evaluateWhen(section.when, vars)) {
|
|
82
91
|
diag.whenPassed = true;
|
|
83
92
|
skippedByCap.push(section.id);
|
|
84
93
|
}
|
|
@@ -90,21 +99,21 @@ export function renderKitSections(ctx, manifestFile = DEFAULT_MANIFEST_FILE) {
|
|
|
90
99
|
diagnostics.push(diag);
|
|
91
100
|
continue;
|
|
92
101
|
}
|
|
93
|
-
diag.whenPassed = evaluateWhen(section.when,
|
|
102
|
+
diag.whenPassed = evaluateWhen(section.when, vars);
|
|
94
103
|
if (!diag.whenPassed) {
|
|
95
104
|
diag.resolveStatus = 'skipped-when';
|
|
96
105
|
diagnostics.push(diag);
|
|
97
106
|
continue;
|
|
98
107
|
}
|
|
99
108
|
if (rawPath) {
|
|
100
|
-
const r = resolvePathWithDiag(rawPath,
|
|
109
|
+
const r = resolvePathWithDiag(rawPath, vars);
|
|
101
110
|
diag.resolvedPath = r.resolved;
|
|
102
111
|
diag.resolveStatus = r.status;
|
|
103
112
|
if (r.unresolvedTokens.length > 0)
|
|
104
113
|
diag.unresolvedTokens = r.unresolvedTokens;
|
|
105
114
|
}
|
|
106
115
|
const overflowOut = {};
|
|
107
|
-
const files = loadSectionFiles(section,
|
|
116
|
+
const files = loadSectionFiles(section, vars, sessionCache, overflowOut);
|
|
108
117
|
diag.fileCount = files.length;
|
|
109
118
|
if (overflowOut.value)
|
|
110
119
|
diag.dirOverflow = overflowOut.value;
|
|
@@ -124,7 +133,7 @@ export function renderKitSections(ctx, manifestFile = DEFAULT_MANIFEST_FILE) {
|
|
|
124
133
|
capReached = true;
|
|
125
134
|
break;
|
|
126
135
|
}
|
|
127
|
-
const content = renderSectionContent(section, rawContent,
|
|
136
|
+
const content = renderSectionContent(section, rawContent, vars);
|
|
128
137
|
if (!content.trim()) {
|
|
129
138
|
diag.emptyContent = true;
|
|
130
139
|
continue;
|
|
@@ -151,7 +160,7 @@ export function renderKitSections(ctx, manifestFile = DEFAULT_MANIFEST_FILE) {
|
|
|
151
160
|
}
|
|
152
161
|
diag.used = anyUsed;
|
|
153
162
|
// 本段触发总闸但一个文件都没用上 → 也算"未加载"
|
|
154
|
-
if (capReached && !anyUsed && evaluateWhen(section.when,
|
|
163
|
+
if (capReached && !anyUsed && evaluateWhen(section.when, vars)) {
|
|
155
164
|
diag.skippedByTotalCap = true;
|
|
156
165
|
diag.whenPassed = true;
|
|
157
166
|
skippedByCap.push(section.id);
|
|
@@ -169,7 +178,7 @@ export function renderKitSections(ctx, manifestFile = DEFAULT_MANIFEST_FILE) {
|
|
|
169
178
|
? `<system-reminder>\nEvolCore Context Kit documents are shown below.\n\n${body}\n\nIMPORTANT: Use this context when it affects the current interaction.\n</system-reminder>`
|
|
170
179
|
: '';
|
|
171
180
|
const fragmentsOutput = fragmentParts.length > 0 ? fragmentParts.join('\n\n') : '';
|
|
172
|
-
writeDebugFiles(
|
|
181
|
+
writeDebugFiles(renderCtx, output, fragmentsOutput, diagnostics);
|
|
173
182
|
return output;
|
|
174
183
|
}
|
|
175
184
|
export function cleanEckDebug() {
|