evolcore 0.0.19 → 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.
Files changed (44) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/README.md +2 -0
  3. package/bin/install-codex-managed-hooks.mjs +61 -0
  4. package/dist/agents/claude-runner.js +4 -3
  5. package/dist/agents/codex-runner.js +3 -2
  6. package/dist/agents/ecagent-runner.js +3 -2
  7. package/dist/aun/aid/agentmd.js +7 -0
  8. package/dist/aun/msg/group.js +14 -3
  9. package/dist/aun/msg/p2p.js +21 -11
  10. package/dist/aun/outbox.js +144 -19
  11. package/dist/channels/aun.js +621 -211
  12. package/dist/cli/daemon-commands.js +12 -6
  13. package/dist/cli/index.js +1 -0
  14. package/dist/cli/init.js +55 -15
  15. package/dist/cli/restart-monitor.js +3 -3
  16. package/dist/config/aun-gateway-config.js +2 -0
  17. package/dist/config/config-manager.js +92 -8
  18. package/dist/config/config-operation-service.js +1 -2
  19. package/dist/config/gateway-config.js +9 -7
  20. package/dist/config/lifecycle.js +16 -5
  21. package/dist/config-store.js +13 -6
  22. package/dist/core/auth/authorization-audit.js +5 -2
  23. package/dist/core/bootstrap-messages.js +2 -2
  24. package/dist/core/bootstrap-service.js +21 -36
  25. package/dist/core/channel-loader.js +0 -2
  26. package/dist/core/evolagent.js +5 -4
  27. package/dist/core/message/response-engine.js +9 -4
  28. package/dist/core/permission/ec-command-parser.js +56 -3
  29. package/dist/core/permission/sandbox-runtime.js +46 -12
  30. package/dist/core/permission/tool-policy.js +98 -41
  31. package/dist/core/relation/peer-identity.js +18 -0
  32. package/dist/eck/kit-renderer.js +17 -8
  33. package/dist/index.js +30 -19
  34. package/dist/utils/stats.js +52 -18
  35. package/dist/utils/welcome.js +2 -2
  36. package/kits/rules/01-overview.md +1 -1
  37. package/kits/rules/03-identity.md +1 -1
  38. package/kits/rules/05-venue.md +1 -1
  39. package/kits/schemas/_meta.json +7 -4
  40. package/kits/schemas/agent-config.schema.10.json +2 -1
  41. package/kits/schemas/agent-config.schema.11.json +408 -0
  42. package/kits/schemas/daemon.schema.5.json +136 -0
  43. package/kits/schemas/defaults.schema.5.json +107 -0
  44. package/package.json +2 -1
@@ -5,7 +5,7 @@ import { kitsTemplatesDir, agentMdPath } from '../paths.js';
5
5
  import { resolveAgentDisplayName } from '../aun/aid/agentmd.js';
6
6
  import { logger } from '../utils/logger.js';
7
7
  import { loadAgent, saveAgent } from '../config-store.js';
8
- import { normalizeAgentLifecycle, withLifecycleForWrite } from '../config/lifecycle.js';
8
+ import { resolveAgentLifecycle, withLifecycleForWrite } from '../config/lifecycle.js';
9
9
  import { renderTemplate } from '../eck/manifest-engine.js';
10
10
  import { activeBaseagent } from './model/config-scope.js';
11
11
  import { buildEnvelope } from './message/message-utils.js';
@@ -123,12 +123,18 @@ export class BootstrapService {
123
123
  this.inFlight.delete(key);
124
124
  return false;
125
125
  }
126
- const config = normalizeAgentLifecycle(loadedConfig);
127
- if (config.lifecycle === 'active') {
126
+ const lifecycle = resolveAgentLifecycle(loadedConfig);
127
+ if (lifecycle === 'active') {
128
128
  this.inFlight.delete(key);
129
129
  return false;
130
130
  }
131
- const starting = config.lifecycle === 'created';
131
+ if (lifecycle !== 'created' && lifecycle !== 'bootstrapping') {
132
+ this.inFlight.delete(key);
133
+ logger.warn(`[Bootstrap] Refusing invalid lifecycle for ${aid}: ${String(loadedConfig.lifecycle)}`);
134
+ return false;
135
+ }
136
+ const config = { ...loadedConfig, lifecycle };
137
+ const starting = lifecycle === 'created';
132
138
  const channelType = ctx.channelType || this.channelTypeFromKey(ctx.channelKey);
133
139
  const configuredRecipient = this.resolveConfiguredRecipient(config, ctx.channelKey, channelType);
134
140
  const recipientId = ctx.recipientId || configuredRecipient;
@@ -140,35 +146,14 @@ export class BootstrapService {
140
146
  this.inFlight.delete(key);
141
147
  return false;
142
148
  }
143
- // The bootstrap prompt is deliberately a private message to the configured
144
- // owner. An inbound owner message may have arrived from a group, in which
145
- // case ctx.channelId is the group AID and must not be paired with the
146
- // private delivery route below (that would call message.send(to=group)).
147
- // A configured AUN recipient may itself be a group AID. Ask the channel's
148
- // authoritative group endpoint before choosing the transport; never infer
149
- // group/private semantics from the AID string or from the inbound venue.
150
- let isGroupRecipient = false;
151
- try {
152
- isGroupRecipient = channelType === 'aun'
153
- ? (await ctx.adapter.isGroup?.(recipientId)) === true
154
- : false;
155
- }
156
- catch (error) {
157
- // Group/private routing is security-sensitive. If the authoritative
158
- // probe fails, do not guess a route, and release the single-flight key
159
- // so a later connection or retry can recover.
160
- this.inFlight.delete(key);
161
- logger.warn(`[Bootstrap] Failed to resolve recipient type for ${aid}: ${error instanceof Error ? error.message : String(error)}`);
162
- return false;
163
- }
164
- const delivery = isGroupRecipient === true
165
- ? { chatType: 'group', groupId: recipientId }
166
- : { chatType: 'private' };
167
- const channelId = delivery.chatType === 'group'
168
- ? delivery.groupId
169
- : channelType === 'aun'
170
- ? recipientId
171
- : ctx.channelId || this.defaultChannelIdForConnection(channelType, recipientId);
149
+ // Bootstrap is initiated by connection/owner binding, not by a reply to
150
+ // an existing group message. `owners[]` contains personal Owner AIDs, so
151
+ // the first prompt must always use the private message route. Inbound
152
+ // group context must never change this initial delivery target.
153
+ const delivery = { chatType: 'private' };
154
+ const channelId = channelType === 'aun'
155
+ ? recipientId
156
+ : ctx.channelId || this.defaultChannelIdForConnection(channelType, recipientId);
172
157
  if (!channelId) {
173
158
  this.inFlight.delete(key);
174
159
  return false;
@@ -234,14 +219,14 @@ export class BootstrapService {
234
219
  const loadedConfig = loadAgent(aid) || agent?.config;
235
220
  if (!loadedConfig)
236
221
  return { ok: false, error: `Agent "${aid}" not found` };
237
- const lifecycle = normalizeAgentLifecycle(loadedConfig).lifecycle;
222
+ const lifecycle = resolveAgentLifecycle(loadedConfig);
238
223
  if (lifecycle === 'active')
239
224
  return { ok: true, aid, transitioned: false };
240
225
  if (lifecycle !== 'bootstrapping') {
241
226
  return {
242
227
  ok: false,
243
228
  code: 'INVALID_LIFECYCLE',
244
- error: `Agent "${aid}" is ${lifecycle}; only a bootstrapping agent can become ready`,
229
+ error: `Agent "${aid}" is ${lifecycle ?? `invalid (${String(loadedConfig.lifecycle)})`}; only a bootstrapping agent can become ready`,
245
230
  };
246
231
  }
247
232
  this.setLifecycle(agent, aid, 'active');
@@ -253,7 +238,7 @@ export class BootstrapService {
253
238
  const agent = this.agentRegistry.get(aid);
254
239
  const loadedConfig = loadAgent(aid) || agent?.config;
255
240
  return loadedConfig
256
- ? normalizeAgentLifecycle(loadedConfig).lifecycle
241
+ ? resolveAgentLifecycle(loadedConfig)
257
242
  : null;
258
243
  }
259
244
  setLifecycle(agent, aid, lifecycle) {
@@ -65,7 +65,6 @@ export class ChannelLoader {
65
65
  owner: agent.getOwner(aunEffName),
66
66
  enabled: true,
67
67
  keystorePath: agent.config.aun?.keystorePath,
68
- gatewayUrl: agent.config.aun?.gatewayUrl,
69
68
  };
70
69
  const configInsts = [aunInst];
71
70
  for (const inst of agent.config.channels) {
@@ -255,7 +254,6 @@ export function buildReloadHooks(deps) {
255
254
  aid,
256
255
  enabled: true,
257
256
  keystorePath: agent.config?.aun?.keystorePath,
258
- gatewayUrl: agent.config?.aun?.gatewayUrl,
259
257
  }
260
258
  : (() => {
261
259
  const agentChannels = agent.config?.channels ?? [];
@@ -243,16 +243,17 @@ export class EvolAgent {
243
243
  b.mentionMode = value;
244
244
  });
245
245
  }
246
- /** 读取观察者模式开关(默认 false)。 */
246
+ /** 读取观察者模式开关(默认 true;显式 false 可关闭)。 */
247
247
  getObservable() {
248
- return this.merged.observable === true;
248
+ return this.merged.observable ?? true;
249
249
  }
250
250
  /** 设置观察者模式开关:开启后入站/出站消息各转发一份给 owners[]。 */
251
251
  setObservable(value) {
252
+ // The default is enabled, so false must remain an explicit persisted override.
252
253
  if (value)
253
- this.rawAgent.observable = true;
254
- else
255
254
  delete this.rawAgent.observable;
255
+ else
256
+ this.rawAgent.observable = false;
256
257
  this.merged.observable = value;
257
258
  this.persist();
258
259
  }
@@ -33,6 +33,7 @@ import { eligibleFallbackModels } from '../model/model-fallback.js';
33
33
  import { RetryScheduler } from './retry-scheduler.js';
34
34
  import { constrainRuntimePermissionMode, resolveRuntimeStringField } from '../role/runtime-policy.js';
35
35
  import { resolveEffective, resolveEffectiveFieldWithSource } from '../../config/config-manager.js';
36
+ import { resolveAgentLifecycle } from '../../config/lifecycle.js';
36
37
  import { authorizationConfigRevision, checkRoleAccess, getFirstStaticAgentOwner, listStaticAgentAdmins, listStaticAgentOwners, resolvePeerRoleDetail, roleToSessionIdentity, } from '../../config/peer-role-resolver.js';
37
38
  import { insertUsageEvent, insertContextBreakdown, insertModelCalls } from '../../stats/writer.js';
38
39
  import { normalizeUsage } from '../../stats/normalizer.js';
@@ -1606,7 +1607,9 @@ export class ResponseEngine {
1606
1607
  const owningAgentForTask = this.agentRegistry?.resolveByChannel(channelKey)
1607
1608
  ?? (taskAgentAid ? this.agentRegistry?.get(taskAgentAid) : null);
1608
1609
  const runnerSelfAid = owningAgentForTask?.aid || session.selfAID || message.selfAID;
1609
- const lifecycle = owningAgentForTask?.config?.lifecycle;
1610
+ const lifecycle = owningAgentForTask
1611
+ ? resolveAgentLifecycle(owningAgentForTask.config)
1612
+ : undefined;
1610
1613
  const isActive = lifecycle === 'active';
1611
1614
  const isBootstrapping = lifecycle === 'bootstrapping';
1612
1615
  // Per-method agent name for stats bucketing (agent.name or '<unknown>')
@@ -1617,10 +1620,12 @@ export class ResponseEngine {
1617
1620
  return;
1618
1621
  }
1619
1622
  // Agent-owned turns are runnable only in the two explicit execution
1620
- // states. An untransitioned `created` agent or a malformed/missing
1621
- // lifecycle must not fall through to a base runner's default prompt.
1623
+ // states. An untransitioned `created` agent or malformed lifecycle must
1624
+ // not fall through to a base runner's default prompt. Missing lifecycle
1625
+ // is normalized to active for legacy configurations.
1622
1626
  if (owningAgentForTask && !isActive && !isBootstrapping) {
1623
- const blockedLifecycle = lifecycle ?? 'missing';
1627
+ const blockedLifecycle = lifecycle
1628
+ ?? `invalid (${String(owningAgentForTask.config.lifecycle)})`;
1624
1629
  logger.error(`[ResponseEngine] Agent lifecycle is not runnable: agent=${owningAgentForTask.aid} lifecycle=${blockedLifecycle}`);
1625
1630
  this.publishTriggerExecutionFailure(message, `agent_lifecycle_not_runnable:${blockedLifecycle}`);
1626
1631
  return;
@@ -17,7 +17,57 @@ const POWERSHELL_UNQUOTED_META = new Set([
17
17
  ';', '&', '|', '<', '>', '$', '(', ')', '*', '?', '[', ']', '{', '}', '#', '@',
18
18
  ]);
19
19
  const AMBIGUOUS_FILE_PATH_RE = /[\0\r\n$`*?\[\]{}<>|;&]/;
20
- const EVOLCORE_COMMAND_POSITION_RE = /(?:^|[;&|()`])\s*ec(?=\s|[;&|()<>`]|$)/i;
20
+ // This is intentionally limited to the command boundary. It is not a search
21
+ // for `ec` anywhere in the input: quoted arguments, regexps and log text must
22
+ // never establish an EC invocation.
23
+ function hasEcCommandPrefix(command) {
24
+ let quote = null;
25
+ let escaped = false;
26
+ let boundary = true;
27
+ for (let index = 0; index < command.length; index++) {
28
+ const char = command[index];
29
+ if (escaped) {
30
+ escaped = false;
31
+ boundary = false;
32
+ continue;
33
+ }
34
+ if (quote === 'single') {
35
+ if (char === "'")
36
+ quote = null;
37
+ continue;
38
+ }
39
+ if (quote === 'double') {
40
+ if (char === '\\') {
41
+ escaped = true;
42
+ continue;
43
+ }
44
+ if (char === '"')
45
+ quote = null;
46
+ continue;
47
+ }
48
+ if (char === "'") {
49
+ quote = 'single';
50
+ boundary = false;
51
+ continue;
52
+ }
53
+ if (char === '"') {
54
+ quote = 'double';
55
+ boundary = false;
56
+ continue;
57
+ }
58
+ if (/[;&|`()<>]/.test(char)) {
59
+ boundary = true;
60
+ continue;
61
+ }
62
+ if (boundary && /\s/.test(char))
63
+ continue;
64
+ if (boundary && command.slice(index, index + 2).toLowerCase() === 'ec'
65
+ && (index + 2 === command.length || /\s|[;&|`()<>]/.test(command[index + 2])))
66
+ return true;
67
+ boundary = false;
68
+ }
69
+ return false;
70
+ }
21
71
  function trimShellSpacing(value) {
22
72
  return value.replace(/^[ \t]+|[ \t]+$/g, '');
23
73
  }
@@ -1059,7 +1109,7 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
1059
1109
  });
1060
1110
  if (nested.ok && nested.argv[0] === 'ec')
1061
1111
  return { kind: 'literal' };
1062
- if (!nested.ok && EVOLCORE_COMMAND_POSITION_RE.test(carrier.command)) {
1112
+ if (!nested.ok && hasEcCommandPrefix(carrier.command)) {
1063
1113
  return { kind: 'composite', issue: nested.issue };
1064
1114
  }
1065
1115
  }
@@ -1071,7 +1121,10 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
1071
1121
  if (boundedOutput && boundedOutput.argv[0] === 'ec') {
1072
1122
  return { kind: 'bounded-output', command: boundedOutput };
1073
1123
  }
1074
- return EVOLCORE_COMMAND_POSITION_RE.test(command)
1124
+ // Only a command-boundary prefix is enough to preserve a useful syntax
1125
+ // error for a malformed direct EC invocation. Never scan for EC tokens in
1126
+ // arbitrary raw text after parsing fails.
1127
+ return hasEcCommandPrefix(command)
1075
1128
  ? { kind: 'composite', issue: literal.issue }
1076
1129
  : { kind: 'none' };
1077
1130
  }
@@ -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
- let systemConfigStat;
310
- try {
311
- systemConfigStat = fs.lstatSync(systemConfigDirectory);
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
@@ -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 false;
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 false;
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 true;
456
+ return resolveProtectedCandidate(operand.value, options.cwd);
455
457
  if (operand.access === 'recursive') {
456
- return className === 'h'
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
- return className === 'h'
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
- const lockDiagnostic = /(?:^|[\/\s'"`])(?:[^\s'"`/]*\.lock|EVOLCORE_RUNTIME_LOCK_DIR)(?=$|[\/\s'"`=])/i.test(command);
493
- const pathHClass = operands.some(({ operand, accessKind }) => shellOperandMatchesClass(operand, 'h', options, accessKind));
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 || pathHClass,
497
- lClass: operands.some(({ operand, accessKind }) => shellOperandMatchesClass(operand, 'l', options, accessKind)),
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
- if (paths.some(filePath => containsLClassReference(filePath) || isLClassPath(filePath, options))) {
517
- return true;
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 false;
561
+ return undefined;
528
562
  }
529
563
  function protectedReadToolMatchesHClass(toolName, input, options) {
530
564
  const paths = protectedReadToolPaths(toolName, input);
531
- if (paths.some(filePath => containsHClassReference(filePath) || isHClassPath(filePath, options))) {
532
- return true;
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 false;
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
- if (paths.some(filePath => containsHClassReference(filePath) || isHClassPath(filePath, pathOptions))) {
650
- return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径', policyCode: 'readonly_h_class_read' };
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
- if (protectedReadToolMatchesHClass(toolName, input, pathOptions)) {
653
- return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径', policyCode: 'readonly_h_class_read' };
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
- if (protectedReadToolMatchesLClass(toolName, input, pathOptions)) {
656
- return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径', policyCode: 'readonly_l_class_read' };
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
- return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径', policyCode: 'readonly_h_class_read' };
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
- return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径', policyCode: 'readonly_l_class_read' };
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
- && protectedReadToolMatchesHClass(toolName, input, pathOptions)) {
1015
- logger.warn(`[H-Class Protection] 🔒 Protected filesystem read scope blocked: tool=${toolName} ` +
1016
- `session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
1017
- return {
1018
- behavior: 'deny',
1019
- message: '🔒 文件读取范围包含受保护的 H 类配置/证书/快照路径,agent 不可直接操作',
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' };
@@ -1472,7 +1529,7 @@ export function evaluateToolPreflight(toolName, input, context) {
1472
1529
  allowProtectedMetadata: context.allowProtectedMetadata,
1473
1530
  });
1474
1531
  if (hClass.behavior === 'deny') {
1475
- 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 };
1476
1533
  }
1477
1534
  const lClass = checkLClassWrite(toolName, checkedInput, {
1478
1535
  sessionId: context.sessionId,
@@ -1486,7 +1543,7 @@ export function evaluateToolPreflight(toolName, input, context) {
1486
1543
  managedTempDir: context.managedTempDir,
1487
1544
  });
1488
1545
  if (lClass.behavior === 'deny') {
1489
- 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 };
1490
1547
  }
1491
1548
  const dangerous = checkDangerousCommand(toolName, checkedInput);
1492
1549
  if (dangerous.isDangerous