evolcore 0.0.21 → 0.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/AGENTS.md +62 -0
  2. package/CHANGELOG.md +53 -0
  3. package/README.md +2 -0
  4. package/bin/codex-managed-acl.ps1 +155 -0
  5. package/bin/codex-managed-hook.mjs +3 -0
  6. package/bin/install-codex-managed-hooks.mjs +376 -96
  7. package/dist/agents/baseagent-availability.js +48 -0
  8. package/dist/agents/baseagent.js +1 -1
  9. package/dist/agents/claude-runner.js +44 -226
  10. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +103 -17
  11. package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
  12. package/dist/agents/codex/catalog.js +70 -0
  13. package/dist/agents/codex/diagnostics.js +154 -0
  14. package/dist/agents/codex-runner.js +1292 -219
  15. package/dist/agents/ecagent-runner.js +10 -31
  16. package/dist/agents/gemini-runner.js +2 -6
  17. package/dist/aun/aid/store.js +2 -0
  18. package/dist/aun/menu-client.js +272 -0
  19. package/dist/aun/msg/group.js +65 -4
  20. package/dist/aun/msg/index.js +1 -1
  21. package/dist/aun/msg/managed-operation.js +51 -4
  22. package/dist/aun/outbox.js +7 -0
  23. package/dist/aun/rpc/client.js +8 -1
  24. package/dist/channels/aun.js +240 -36
  25. package/dist/channels/wechat.js +81 -10
  26. package/dist/cli/aun-commands.js +48 -4
  27. package/dist/cli/command-log.js +21 -2
  28. package/dist/cli/daemon-commands.js +169 -51
  29. package/dist/cli/index.js +20 -0
  30. package/dist/cli/init.js +2 -10
  31. package/dist/cli/menu-client.js +95 -0
  32. package/dist/cli/model.js +23 -8
  33. package/dist/cli/task-context.js +16 -2
  34. package/dist/cli/trigger-command.js +22 -5
  35. package/dist/config/config-field-policy.js +3 -0
  36. package/dist/config/config-manager.js +32 -5
  37. package/dist/config/contact-book-store.js +25 -3
  38. package/dist/config/schema-migration.js +14 -2
  39. package/dist/core/auth/agent-delegation.js +15 -0
  40. package/dist/core/auth/auth-gateway.js +8 -0
  41. package/dist/core/auth/authorization-audit.js +70 -6
  42. package/dist/core/auth/operation-catalog.js +4 -0
  43. package/dist/core/auth/remote-access-store.js +135 -0
  44. package/dist/core/bootstrap-messages.js +8 -0
  45. package/dist/core/bootstrap-service.js +93 -25
  46. package/dist/core/command/command-handler.js +62 -5
  47. package/dist/core/command/daemon-diagnostics.js +448 -0
  48. package/dist/core/command/menu-catalog.js +3 -0
  49. package/dist/core/command/menu-handler.js +150 -33
  50. package/dist/core/command/menu-protocol.js +120 -1
  51. package/dist/core/command/slash-gate.js +3 -3
  52. package/dist/core/command/slash-handler.js +83 -40
  53. package/dist/core/data-migration.js +11 -1
  54. package/dist/core/event-catalog.js +32 -0
  55. package/dist/core/handoff/runtime.js +23 -3
  56. package/dist/core/message/im-renderer.js +7 -3
  57. package/dist/core/message/message-bridge.js +60 -2
  58. package/dist/core/message/message-log.js +33 -0
  59. package/dist/core/message/message-queue.js +41 -15
  60. package/dist/core/message/response-engine.js +246 -114
  61. package/dist/core/model/catalog-service.js +308 -0
  62. package/dist/core/model/model-catalog.js +124 -334
  63. package/dist/core/model/model-diagnostics.js +4 -5
  64. package/dist/core/model/model-fallback.js +9 -16
  65. package/dist/core/model/model-permission.js +1 -1
  66. package/dist/core/permission/approval-gateway.js +16 -1
  67. package/dist/core/permission/ec-command-parser.js +272 -70
  68. package/dist/core/permission/protected-paths.js +74 -14
  69. package/dist/core/permission/sandbox-runtime.js +4 -2
  70. package/dist/core/permission/tool-error-code.js +12 -0
  71. package/dist/core/permission/tool-policy.js +290 -30
  72. package/dist/core/runtime-lock.js +13 -8
  73. package/dist/core/session/session-manager.js +30 -0
  74. package/dist/core/session/session-renew.js +18 -1
  75. package/dist/core/session/session-turn-coordinator.js +5 -1
  76. package/dist/index.js +104 -34
  77. package/dist/ipc.js +218 -96
  78. package/dist/paths.js +18 -0
  79. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  80. package/dist/stats/price-resolver.js +4 -0
  81. package/dist/trigger/feedback.js +14 -2
  82. package/dist/trigger/manager.js +3 -1
  83. package/dist/trigger/parser.js +86 -13
  84. package/dist/trigger/patch.js +104 -1
  85. package/dist/trigger/scheduler.js +20 -3
  86. package/dist/utils/ecweb-supervisor.js +11 -2
  87. package/dist/utils/ecweb-utils.js +8 -26
  88. package/dist/utils/error-utils.js +7 -0
  89. package/dist/utils/logger.js +9 -4
  90. package/dist/utils/npm-ops.js +47 -15
  91. package/dist/utils/tool-summary.js +59 -0
  92. package/dist/utils/windows-shell-trust.js +201 -0
  93. package/kits/docs/evolcore/INDEX.md +3 -2
  94. package/kits/docs/evolcore/agent-create.md +159 -0
  95. package/kits/docs/evolcore/agent.md +6 -0
  96. package/kits/docs/evolcore/group-collaboration.md +256 -0
  97. package/kits/docs/evolcore/group-rules.md +1 -19
  98. package/kits/docs/evolcore/group.md +3 -1
  99. package/kits/docs/evolcore/menu-client.md +34 -0
  100. package/kits/docs/evolcore/trigger.md +6 -3
  101. package/kits/docs/prompt-loading-architecture.md +6 -0
  102. package/kits/eck_message_manifest.json +6 -6
  103. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  104. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  105. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  106. package/kits/schemas/_meta.json +3 -2
  107. package/kits/schemas/agent-config.schema.12.json +427 -0
  108. package/kits/templates/message-fragments/item.md +1 -1
  109. package/kits/templates/roles/visitor.json +1 -0
  110. package/kits/templates/system-fragments/bootstrap.md +10 -2
  111. package/kits/templates/system-fragments/commands.md +2 -2
  112. package/package.json +11 -3
package/dist/cli/model.js CHANGED
@@ -13,7 +13,7 @@
13
13
  import { isHelpFlag, wantsHelp, getArgValue } from './help.js';
14
14
  import { ModelScopeError, normalizePeer, determineScope, activeBaseagent, writeScope, clearScope, } from '../core/model/config-scope.js';
15
15
  import { resolveEffectiveFieldWithSource } from '../config/config-manager.js';
16
- import { getCatalog, getModelInfo } from '../core/model/model-catalog.js';
16
+ import { getCatalog, getModelInfo, catalogEfforts, resolveCatalogModel, assertCatalogSelectable } from '../core/model/model-catalog.js';
17
17
  import { constrainResolvedModelForRole, filterModelsForRole, validateModelSelectionForRole, } from '../core/model/model-permission.js';
18
18
  import { resolvePaths } from '../paths.js';
19
19
  import { ipcQuery } from '../ipc.js';
@@ -23,7 +23,7 @@ import { getFieldPermission } from '../config/roles.js';
23
23
  import { AGENT_DELEGATION_TOKEN_ENV } from '../core/auth/agent-delegation.js';
24
24
  import { formatModelCheck, runModelCheck } from '../core/model/model-diagnostics.js';
25
25
  import { validateRuntimeStringFieldOverride } from '../core/role/runtime-policy.js';
26
- const ALL_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max', 'auto'];
26
+ const ALL_EFFORTS = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'auto'];
27
27
  const SCOPE_LABEL = {
28
28
  global: '全局', agent: 'agent级', role: '角色级', relation: '关系级',
29
29
  };
@@ -298,7 +298,7 @@ async function cmdList(args, formatJson) {
298
298
  const cat = await getCatalog(sel.self, ba);
299
299
  const resolved = resolveModelConfig(effectiveSel, ba);
300
300
  const allowedModelIds = role
301
- ? filterModelsForRole(role, ba, cat.models.map(m => m.id), undefined, sel.self)
301
+ ? filterModelsForRole(role, ba, cat.models.map(m => m.id), model => resolveCatalogModel(cat, model), sel.self)
302
302
  : null;
303
303
  const models = allowedModelIds
304
304
  ? allowedModelIds.map(id => cat.models.find(m => m.id === id) ?? { id })
@@ -308,15 +308,21 @@ async function cmdList(args, formatJson) {
308
308
  effective: { model: resolved.model ?? null, source: resolved.source ?? null },
309
309
  role: role ?? null,
310
310
  catalogSource: cat.source,
311
+ freshness: cat.freshness,
312
+ revision: cat.revision,
313
+ fetchedAt: cat.fetchedAt,
314
+ diagnostics: cat.diagnostics,
311
315
  models,
312
316
  }, () => {
313
317
  const lines = [];
314
318
  lines.push(`当前生效: ${resolved.model ?? '(未设置,回落 SDK 默认)'}` +
315
319
  (resolved.source ? ` (来源: ${SCOPE_LABEL[resolved.source]})` : ''));
316
320
  lines.push('');
317
- const srcTag = cat.source === 'mock' ? ' [mock]'
318
- : cat.source === 'remote' ? ' [remote]'
319
- : '';
321
+ if (resolved.model && !cat.models.some(m => m.id === resolved.model))
322
+ lines.push('当前配置未列入目录');
323
+ lines.push(...cat.diagnostics.map(d => d.message));
324
+ const srcTag = cat.freshness === 'fallback' ? ` [${cat.source}/unverified]`
325
+ : cat.freshness === 'stale' ? ' [stale]' : '';
320
326
  lines.push(`可用模型 (${models.length})${srcTag}:`);
321
327
  for (const e of models) {
322
328
  const live = resolved.model === e.id ? '✓' : ' ';
@@ -430,6 +436,7 @@ async function cmdUse(args, formatJson) {
430
436
  const ba = activeBaseagent(sel.self);
431
437
  // 校验模型在 catalog 中
432
438
  const cat = await getCatalog(sel.self, ba);
439
+ assertCatalogSelectable(cat);
433
440
  if (!cat.models.some(m => m.id === modelId)) {
434
441
  fail(formatJson, 'UNKNOWN_MODEL', `模型不在目录中: ${modelId}(model list 查看可用模型)`);
435
442
  }
@@ -439,14 +446,15 @@ async function cmdUse(args, formatJson) {
439
446
  baseagent: ba,
440
447
  requestedModel: modelId,
441
448
  models: cat.models.map(m => m.id),
449
+ resolveModelId: model => resolveCatalogModel(cat, model),
442
450
  selfAid: sel.self,
443
451
  });
444
452
  if (!decision.ok)
445
453
  fail(formatJson, decision.code || 'MODEL_NOT_ALLOWED', decision.message || `model not allowed: ${modelId}`);
446
454
  }
447
455
  const effort = getArgValue(args, '--effort');
448
- if (effort !== undefined && !ALL_EFFORTS.includes(effort)) {
449
- fail(formatJson, 'INVALID_EFFORT', `无效推理强度: ${effort}(${ALL_EFFORTS.join('/')})`);
456
+ if (effort !== undefined && effort !== 'auto' && !catalogEfforts(cat, modelId, ba).includes(effort)) {
457
+ fail(formatJson, 'INVALID_EFFORT', `当前模型目录未声明支持推理强度: ${effort}`);
450
458
  }
451
459
  const effortVal = effort === 'auto' ? null : (effort ?? undefined);
452
460
  if (isManagedTargetedWriteEnv(args)) {
@@ -492,6 +500,13 @@ async function cmdEffort(args, formatJson) {
492
500
  const scope = determineWriteScope(sel, formatJson);
493
501
  const ba = activeBaseagent(sel.self);
494
502
  const val = level === 'auto' ? null : level;
503
+ if (val) {
504
+ const cat = await getCatalog(sel.self, ba);
505
+ assertCatalogSelectable(cat);
506
+ const model = resolveModelConfig(selectorWithRole(sel, explicitOrDerivedRole(args, sel)), ba).model;
507
+ if (!model || !catalogEfforts(cat, model, ba).includes(val))
508
+ fail(formatJson, 'INVALID_EFFORT', `当前模型目录未声明支持推理强度: ${val}`);
509
+ }
495
510
  if (isManagedTargetedWriteEnv(args)) {
496
511
  const effortKey = 'effort';
497
512
  preflightManagedFieldOverride(sel, explicitOrDerivedRole(args, sel), `baseagents.${ba}.${effortKey}`, val, formatJson);
@@ -6,6 +6,7 @@ import os from 'node:os';
6
6
  import path from 'node:path';
7
7
  export const TASK_RUNTIME_CONTEXT_ENV = 'EVOLCORE_TASK_RUNTIME_CONTEXT';
8
8
  export const SESSION_RUNTIME_DIR_ENV = 'EVOLCORE_SESSION_RUNTIME_DIR';
9
+ export const DAEMON_RUNTIME_EPOCH_ENV = 'EVOLCORE_DAEMON_RUNTIME_EPOCH';
9
10
  export { RUNTIME_LOCK_DIR_ENV };
10
11
  const generatedProcessManagedTempDirs = new Set();
11
12
  let processTempCleanupRegistered = false;
@@ -32,11 +33,13 @@ function normalizeTaskRuntimeContext(value) {
32
33
  peerName: optionalString(value.peerName),
33
34
  peerType: optionalString(value.peerType),
34
35
  peerRole: optionalString(value.peerRole),
36
+ permissionMode: optionalString(value.permissionMode),
35
37
  processRole: optionalProcessRole(value.processRole),
36
38
  dataScope: optionalDataScope(value.dataScope),
37
39
  authorizedBy: optionalString(value.authorizedBy),
38
40
  executionSource: value.executionSource === 'fullaccess-command' || value.executionSource === 'trigger'
39
41
  ? value.executionSource : undefined,
42
+ daemonRuntimeEpoch: optionalString(value.daemonRuntimeEpoch),
40
43
  threadId: optionalString(value.threadId),
41
44
  sessionRuntimeDir: optionalAbsolutePath(value.sessionRuntimeDir),
42
45
  runtimeLockDir: optionalAbsolutePath(value.runtimeLockDir),
@@ -250,8 +253,18 @@ export function getManagedTaskTempDir(runtimeEnv) {
250
253
  * silently escape the session-owned temporary workspace.
251
254
  */
252
255
  export function ensureSessionRuntimeDir(sessionId) {
253
- const base = process.env.TMPDIR?.trim();
254
- if (!base || !path.isAbsolute(base) || !sessionId)
256
+ if (!sessionId)
257
+ return undefined;
258
+ let base = process.env.TMPDIR?.trim();
259
+ try {
260
+ if (!base || !path.isAbsolute(base) || !isPrivateDirectory(path.resolve(base))) {
261
+ base = ensureProcessManagedTempDir();
262
+ }
263
+ }
264
+ catch {
265
+ return undefined;
266
+ }
267
+ if (!base || !path.isAbsolute(base))
255
268
  return undefined;
256
269
  const root = path.resolve(base);
257
270
  const digest = crypto.createHash('sha256').update(sessionId).digest('hex').slice(0, 24);
@@ -336,6 +349,7 @@ export function buildTaskRuntimeEnv(ctx) {
336
349
  const clean = normalizeTaskRuntimeContext(ctx);
337
350
  return {
338
351
  EVOLCORE_SESSION_ID: ctx.sessionId ?? '',
352
+ ...(ctx.daemonRuntimeEpoch ? { [DAEMON_RUNTIME_EPOCH_ENV]: ctx.daemonRuntimeEpoch } : {}),
339
353
  [TASK_RUNTIME_CONTEXT_ENV]: JSON.stringify(clean),
340
354
  // Every managed child receives the session directory as TMPDIR. This keeps
341
355
  // ordinary temporary files and runtime-only fallbacks out of shared agent
@@ -32,7 +32,7 @@ export async function cmdTrigger(args) {
32
32
  return;
33
33
  }
34
34
  if (process.env.EVOLCORE_SESSION_ID
35
- && (hasOption(rest, '--file') || hasOption(rest, '--prompt-file') || hasOption(rest, '--script-path'))) {
35
+ && (hasOption(rest, '--file') || hasOption(rest, '--prompt-file') || hasOption(rest, '--script-path') || hasOption(rest, '--script-file'))) {
36
36
  throw new Error('Managed tasks cannot read trigger definitions, prompts, or scripts from local files; use inline prompt content');
37
37
  }
38
38
  try {
@@ -121,12 +121,20 @@ Create 参数模式支持:
121
121
  通用输出参数:
122
122
  --json | --format json
123
123
 
124
- Update 仅支持常用字段:
124
+ Update 参数:
125
125
  --once | --delay | --at | --cron | --every | --event [--tz]
126
- --prompt <文本> | --prompt-file <路径> --name <名称>
126
+ --prompt <文本> | --prompt-file <路径> --script-file <路径> --name <名称>
127
+ --description <文本> (空字符串删除)
127
128
  --model <模型> --effort <强度> --permission <权限模式|inherit>(inherit 清除覆盖)
128
129
  --max-runs <次数> --max-duration <时长>
129
130
  --concurrency <forbid|replace|allow> --missed-policy <skip|run-once|run-all>
131
+ --feedback <origin|target|silent>
132
+ --target-channel <channelKey> --target-channel-id <ID> (必须同时指定)
133
+ --target-chat-type <private|group> --target-session <main|thread> [--target-thread-id <threadId>]
134
+ --trigger-thread <by-run|by-trigger> --baseagent <name> (仅 trigger_session 之间)
135
+ --on-error <fail|retry> --noop-sentinel <文本>
136
+ --retry-attempts <次数> --retry-backoff <毫秒>
137
+ --script-runtime <node|python|bash> --script-args <JSON> --script-timeout <时长>
130
138
 
131
139
  省略 --agent 时使用当前 agent AID;无会话上下文或跨身份操作时需要显式指定。
132
140
  任务会话中的跨身份操作仅允许目标 Agent 上具备 owner/admin 权限的调用者执行 list/show/history;创建、修改、启停、删除和运行仍只允许当前 Agent。`);
@@ -399,19 +407,28 @@ function shellQuoteArg(value) {
399
407
  }
400
408
  async function updateTrigger(args, json) {
401
409
  const agentAid = resolveAgentAid(args);
402
- const triggerId = positional(args, 0, ['--agent', '--format', '--if-revision', '--prompt-file']);
410
+ const triggerId = positional(args, 0, ['--agent', '--format', '--if-revision', '--prompt-file', '--script-file']);
403
411
  const usesPromptFile = hasOption(args, '--prompt-file');
412
+ const usesScriptFile = hasOption(args, '--script-file');
413
+ const scriptFilePath = usesScriptFile ? requireFlag(args, '--script-file') : undefined;
404
414
  const { parseTriggerUpdateArgv } = await import('../trigger/parser.js');
405
415
  const parsed = parseTriggerUpdateArgv(triggerId, withPromptFromFile(triggerUpdateArgs(args)), { allowLongPrompt: usesPromptFile });
406
416
  if (!parsed.ok)
407
417
  throw new Error(parsed.error);
418
+ const scriptFileRequested = parsed.value.scriptFile !== undefined;
419
+ const { scriptFile: _scriptFile, ...patchFields } = parsed.value;
420
+ const patch = scriptFileRequested ? { ...patchFields, scriptFile: true } : patchFields;
408
421
  const expectedRevision = flagValue(args, '--if-revision');
422
+ const scriptFileBase64 = scriptFilePath
423
+ ? fs.readFileSync(scriptFilePath).toString('base64')
424
+ : undefined;
409
425
  const res = await request({
410
426
  type: 'trigger.update',
411
427
  agentAid,
412
428
  triggerId,
413
- patch: parsed.value,
429
+ patch,
414
430
  ...(usesPromptFile ? { promptFile: true } : {}),
431
+ ...(scriptFileBase64 !== undefined ? { scriptFileBase64 } : {}),
415
432
  ...(expectedRevision ? { expectedRevision } : {}),
416
433
  }, 30_000);
417
434
  if (json)
@@ -2,6 +2,7 @@ const BEHAVIOR_TOP_FIELDS = new Set([
2
2
  'active_baseagent',
3
3
  'chatmode',
4
4
  'mentionMode',
5
+ 'groupSlashRequireMentions',
5
6
  'responseMode',
6
7
  'responseModeParams',
7
8
  'flush_delay',
@@ -110,6 +111,8 @@ export function resolveConfigFieldRule(fieldPath) {
110
111
  return scalar(field, 'nonnegative-number');
111
112
  if (field === 'mentionMode')
112
113
  return scalar(field, 'mention-mode');
114
+ if (field === 'groupSlashRequireMentions')
115
+ return scalar(field, 'boolean');
113
116
  if (field === 'show_activities')
114
117
  return scalar(field, 'show-activities');
115
118
  if (field === 'enable_rich_content')
@@ -933,7 +933,10 @@ function acquireConfigMutationLock(file) {
933
933
  return () => releaseConfigMutationLock(lockDir, ownerFile, token);
934
934
  }
935
935
  catch (error) {
936
- if (error?.code !== 'EEXIST' && error?.code !== 'ENOTEMPTY')
936
+ const lockMayAlreadyExist = error?.code === 'EEXIST'
937
+ || error?.code === 'ENOTEMPTY'
938
+ || (process.platform === 'win32' && error?.code === 'EPERM' && fs.existsSync(lockDir));
939
+ if (!lockMayAlreadyExist)
937
940
  throw error;
938
941
  if (reclaimStaleConfigMutationLock(lockDir, ownerFile))
939
942
  continue;
@@ -957,10 +960,10 @@ function reclaimStaleConfigMutationLock(lockDir, ownerFile) {
957
960
  pid = Number(JSON.parse(raw).pid);
958
961
  }
959
962
  catch {
960
- return false;
963
+ return reclaimCorruptConfigMutationLock(lockDir);
961
964
  }
962
965
  if (!Number.isInteger(pid) || pid <= 0)
963
- return false;
966
+ return reclaimCorruptConfigMutationLock(lockDir);
964
967
  try {
965
968
  process.kill(pid, 0);
966
969
  return false;
@@ -979,6 +982,24 @@ function reclaimStaleConfigMutationLock(lockDir, ownerFile) {
979
982
  }
980
983
  }
981
984
  }
985
+ function reclaimCorruptConfigMutationLock(lockDir) {
986
+ try {
987
+ const stat = fs.statSync(lockDir);
988
+ if (Date.now() - stat.mtimeMs < CONFIG_MUTATION_LOCK_WAIT_MS)
989
+ return false;
990
+ const reclaimed = `${lockDir}.reclaimed-corrupt-${crypto.randomBytes(8).toString('hex')}`;
991
+ fs.renameSync(lockDir, reclaimed);
992
+ fs.rmSync(reclaimed, { recursive: true, force: true });
993
+ return true;
994
+ }
995
+ catch (error) {
996
+ if (error?.code === 'ENOENT')
997
+ return true;
998
+ if (error?.code === 'EEXIST' || error?.code === 'ENOTEMPTY' || error?.code === 'EPERM')
999
+ return false;
1000
+ throw error;
1001
+ }
1002
+ }
982
1003
  function releaseConfigMutationLock(lockDir, ownerFile, token) {
983
1004
  try {
984
1005
  const owner = JSON.parse(fs.readFileSync(ownerFile, 'utf8'));
@@ -1289,9 +1310,13 @@ function stripStaticOwnerField(config) {
1289
1310
  return config;
1290
1311
  if (!Object.prototype.hasOwnProperty.call(config, 'owners')
1291
1312
  && !Object.prototype.hasOwnProperty.call(config, 'admins')
1292
- && !Object.prototype.hasOwnProperty.call(config, 'access'))
1313
+ && !Object.prototype.hasOwnProperty.call(config, 'access')
1314
+ && !Object.prototype.hasOwnProperty.call(config, 'groupSlashRequireMentions'))
1293
1315
  return config;
1294
- const { owners: _owners, admins: _admins, access: _access, ...rest } = config;
1316
+ // groupSlashRequireMentions is intentionally Agent-only. Relation/defaults
1317
+ // files allow forward-compatible unknown properties, so strip it before the
1318
+ // generic merge chain can treat a manually written value as an override.
1319
+ const { owners: _owners, admins: _admins, access: _access, groupSlashRequireMentions: _groupSlashRequireMentions, ...rest } = config;
1295
1320
  return rest;
1296
1321
  }
1297
1322
  function stripRelationRoleData(config) {
@@ -1330,6 +1355,7 @@ export function resolveEffective(sel, opts = {}) {
1330
1355
  baseagents: config.baseagents,
1331
1356
  chatmode: config.chatmode,
1332
1357
  mentionMode: config.mentionMode,
1358
+ groupSlashRequireMentions: config.groupSlashRequireMentions ?? true,
1333
1359
  model: config.model,
1334
1360
  responseMode: config.responseMode,
1335
1361
  responseModeParams: config.responseModeParams,
@@ -1361,6 +1387,7 @@ const EFFECTIVE_BEHAVIOR_FIELDS = [
1361
1387
  'baseagents',
1362
1388
  'chatmode',
1363
1389
  'mentionMode',
1390
+ 'groupSlashRequireMentions',
1364
1391
  'flush_delay',
1365
1392
  'debounce',
1366
1393
  'show_activities',
@@ -14,6 +14,7 @@ const LOCK_NAME = '.contact-book-mutation.lock';
14
14
  const JOURNAL_NAME = '.contact-book-mutation.json';
15
15
  const LOCK_WAIT_MS = 5_000;
16
16
  const LOCK_RETRY_MS = 20;
17
+ const CORRUPT_LOCK_STALE_MS = LOCK_WAIT_MS;
17
18
  export const EMPTY_CONTACT_REVISION = crypto.createHash('sha256').update('').digest('hex');
18
19
  export class ContactMutationError extends Error {
19
20
  code;
@@ -454,7 +455,10 @@ function acquireAgentLock(selfAid) {
454
455
  return () => releaseAgentLock(lockDir, ownerFile, lockToken);
455
456
  }
456
457
  catch (error) {
457
- if (error?.code !== 'EEXIST' && error?.code !== 'ENOTEMPTY')
458
+ const lockMayAlreadyExist = error?.code === 'EEXIST'
459
+ || error?.code === 'ENOTEMPTY'
460
+ || (process.platform === 'win32' && error?.code === 'EPERM' && fs.existsSync(lockDir));
461
+ if (!lockMayAlreadyExist)
458
462
  throw error;
459
463
  if (removeStaleLock(lockDir, ownerFile))
460
464
  continue;
@@ -478,10 +482,10 @@ function removeStaleLock(lockDir, ownerFile) {
478
482
  pid = Number(JSON.parse(rawOwner).pid);
479
483
  }
480
484
  catch {
481
- return false;
485
+ return reclaimCorruptLock(lockDir);
482
486
  }
483
487
  if (!Number.isInteger(pid) || pid <= 0)
484
- return false;
488
+ return reclaimCorruptLock(lockDir);
485
489
  try {
486
490
  process.kill(pid, 0);
487
491
  return false;
@@ -503,6 +507,24 @@ function removeStaleLock(lockDir, ownerFile) {
503
507
  }
504
508
  }
505
509
  }
510
+ function reclaimCorruptLock(lockDir) {
511
+ try {
512
+ const stat = fs.statSync(lockDir);
513
+ if (Date.now() - stat.mtimeMs < CORRUPT_LOCK_STALE_MS)
514
+ return false;
515
+ const staleClaim = `${lockDir}.reclaimed-corrupt-${crypto.randomBytes(8).toString('hex')}`;
516
+ fs.renameSync(lockDir, staleClaim);
517
+ fs.rmSync(staleClaim, { recursive: true, force: true });
518
+ return true;
519
+ }
520
+ catch (error) {
521
+ if (error?.code === 'ENOENT')
522
+ return true;
523
+ if (error?.code === 'EEXIST' || error?.code === 'ENOTEMPTY' || error?.code === 'EPERM')
524
+ return false;
525
+ throw error;
526
+ }
527
+ }
506
528
  function releaseAgentLock(lockDir, ownerFile, lockToken) {
507
529
  try {
508
530
  const owner = JSON.parse(fs.readFileSync(ownerFile, 'utf8'));
@@ -342,8 +342,20 @@ function processIsAlive(pid) {
342
342
  function assertDaemonStopped(home) {
343
343
  const candidates = [];
344
344
  const pidFile = path.join(home, 'daemon.pid');
345
- if (fs.existsSync(pidFile)) {
346
- candidates.push({ pid: Number(fs.readFileSync(pidFile, 'utf8').trim()), source: pidFile });
345
+ try {
346
+ // daemon.pid was retired in favor of the instance registry. It may still
347
+ // exist as a stale legacy file, but a Windows Codex sandbox can also leave
348
+ // a directory at this path when protecting a not-yet-created file. Never
349
+ // feed a directory (or symlink) to readFileSync: that turns harmless
350
+ // legacy residue into an EISDIR startup failure.
351
+ const stat = fs.lstatSync(pidFile);
352
+ if (stat.isFile()) {
353
+ candidates.push({ pid: Number(fs.readFileSync(pidFile, 'utf8').trim()), source: pidFile });
354
+ }
355
+ }
356
+ catch (error) {
357
+ if (!isRecord(error) || error.code !== 'ENOENT')
358
+ throw error;
347
359
  }
348
360
  const instanceDir = path.join(home, 'data', 'instance');
349
361
  if (fs.existsSync(instanceDir)) {
@@ -1,12 +1,17 @@
1
1
  import crypto from 'crypto';
2
2
  import { authorizeOperation, buildAuthSubject } from './auth-gateway.js';
3
3
  import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
4
+ import { logger } from '../../utils/logger.js';
4
5
  export const AGENT_DELEGATION_TOKEN_ENV = 'EVOLCORE_DELEGATION_TOKEN';
5
6
  export const AGENT_DELEGATION_COMMAND_TTL_MS = 5 * 60_000;
6
7
  export class AgentDelegationRegistry {
8
+ runtimeEpoch = crypto.randomBytes(16).toString('hex');
7
9
  grantsByHash = new Map();
8
10
  activeHashBySession = new Map();
9
11
  approvedCommands = new Map();
12
+ getRuntimeEpoch() {
13
+ return this.runtimeEpoch;
14
+ }
10
15
  issue(input) {
11
16
  if (input.executionIdentity && !isValidFullAccessExecutionIdentity(input.executionIdentity)) {
12
17
  throw new Error('invalid fullaccess task execution identity');
@@ -24,6 +29,7 @@ export class AgentDelegationRegistry {
24
29
  ok: false,
25
30
  code: 'DELEGATION_REQUIRED',
26
31
  reason: 'An active task delegation token is required',
32
+ diagnosticCause: 'missing_token',
27
33
  };
28
34
  }
29
35
  const tokenHash = hashDelegationToken(token);
@@ -38,10 +44,19 @@ export class AgentDelegationRegistry {
38
44
  : undefined;
39
45
  if (approved)
40
46
  return { ok: true, grant: approved };
47
+ const diagnosticCause = grant
48
+ ? grant.sessionId !== sessionId
49
+ ? 'session_mismatch'
50
+ : 'task_replaced'
51
+ : commandHash
52
+ ? 'approved_command_unavailable'
53
+ : 'unknown_token';
54
+ logger.warn(`[AgentDelegation] validate failed: cause=${diagnosticCause} session=${sessionId} grantSession=${grant?.sessionId ?? 'none'} taskId=${grant?.taskId ?? 'none'} hasCommandHash=${!!commandHash} tokenKnown=${!!grant} activeForSession=${this.activeHashBySession.get(sessionId) ?? 'none'} grantTokenHash=${grant?.tokenHash ?? 'none'}`);
41
55
  return {
42
56
  ok: false,
43
57
  code: 'INVALID_DELEGATION',
44
58
  reason: 'The task delegation token is invalid, revoked, or belongs to another session',
59
+ diagnosticCause,
45
60
  };
46
61
  }
47
62
  armApprovedCommand(input) {
@@ -192,6 +192,14 @@ function auditDecision(params, decision) {
192
192
  permissionMode: params.auditMetadata?.permissionMode
193
193
  ?? (decision.allow ? decision.permissionMode ?? params.subject.permissionMode : params.subject.permissionMode),
194
194
  toolName: params.auditMetadata?.toolName,
195
+ errorCode: params.auditMetadata?.errorCode
196
+ ?? (decision.allow
197
+ ? undefined
198
+ : params.auditMetadata?.decisionSource === 'approval'
199
+ ? 'USER_DENIED'
200
+ : params.auditMetadata?.decisionSource === 'infrastructure'
201
+ ? 'EXECUTION_FAILED'
202
+ : 'POLICY_DENIED'),
195
203
  policyCode: params.auditMetadata?.policyCode ?? (decision.allow ? undefined : decision.code),
196
204
  decisionSource: params.auditMetadata?.decisionSource ?? (decision.allow ? 'command' : 'policy'),
197
205
  source: params.source,
@@ -2,7 +2,7 @@ import { logger } from '../../utils/logger.js';
2
2
  import { LogWriter } from '../../utils/log-writer.js';
3
3
  import { resolvePaths } from '../../paths.js';
4
4
  import crypto from 'node:crypto';
5
- import { buildAuthorizationEventKey } from '../audit/event-key.js';
5
+ import { buildAuthorizationEventKey, buildToolLifecycleEventKey } from '../audit/event-key.js';
6
6
  import { normalizeExecutionPermissionMode } from '../permission/mode.js';
7
7
  export function auditCommandAuthorization(event) {
8
8
  const shouldAudit = event.source === 'menu.cli' ||
@@ -95,10 +95,20 @@ export function auditFullAccessEvent(input) {
95
95
  * denials otherwise only reach `logger.warn` and `TriggerExecutionAnomaly`.
96
96
  */
97
97
  export function auditToolPreflightDenial(input) {
98
+ const lifecycleId = input.callId ?? input.correlationId ?? input.requestId;
98
99
  auditCommandAuthorization({
99
100
  ts: Date.now(),
100
- callId: input.requestId,
101
- correlationId: input.requestId,
101
+ ...(lifecycleId ? {
102
+ eventKey: buildToolLifecycleEventKey({
103
+ sessionId: input.sessionId,
104
+ callId: input.callId,
105
+ correlationId: input.correlationId,
106
+ requestId: input.requestId,
107
+ }),
108
+ eventPhase: 'preflight',
109
+ } : {}),
110
+ callId: input.callId ?? input.requestId,
111
+ correlationId: input.correlationId ?? input.callId ?? input.requestId,
102
112
  source: 'agent-tool',
103
113
  operation: 'tool.preflight.deny',
104
114
  scope: 'filesystem',
@@ -107,6 +117,7 @@ export function auditToolPreflightDenial(input) {
107
117
  executed: false,
108
118
  executionState: 'blocked',
109
119
  decisionSource: 'policy',
120
+ errorCode: 'POLICY_DENIED',
110
121
  toolName: input.toolName,
111
122
  policyCode: input.policyCode,
112
123
  protectionClass: input.protectionClass ?? protectionClassForPolicy(input.policyCode),
@@ -135,9 +146,19 @@ export function auditToolPreflightDenial(input) {
135
146
  * decision source make policy, user, and infrastructure denials distinct.
136
147
  */
137
148
  export function auditCodexApprovalDecision(input) {
149
+ const lifecycleId = input.callId ?? input.correlationId ?? input.requestId;
138
150
  auditCommandAuthorization({
139
151
  ts: Date.now(),
140
- callId: input.correlationId ?? input.requestId,
152
+ ...(lifecycleId ? {
153
+ eventKey: buildToolLifecycleEventKey({
154
+ sessionId: input.sessionId,
155
+ callId: input.callId,
156
+ correlationId: input.correlationId,
157
+ requestId: input.requestId,
158
+ }),
159
+ eventPhase: 'approval',
160
+ } : {}),
161
+ callId: input.callId ?? input.correlationId ?? input.requestId,
141
162
  source: 'agent-tool',
142
163
  operation: 'codex.approval',
143
164
  scope: input.toolName === 'PermissionGrant' ? 'agent' : 'filesystem',
@@ -146,8 +167,11 @@ export function auditCodexApprovalDecision(input) {
146
167
  executed: false,
147
168
  executionState: input.decision === 'allow' ? 'authorized' : 'blocked',
148
169
  decisionSource: input.decisionSource,
170
+ errorCode: input.decision === 'deny'
171
+ ? input.decisionSource === 'approval' ? 'USER_DENIED' : input.decisionSource === 'policy' ? 'POLICY_DENIED' : infrastructureErrorCode(input.policyCode)
172
+ : undefined,
149
173
  requestId: input.requestId,
150
- correlationId: input.correlationId ?? input.requestId,
174
+ correlationId: input.correlationId ?? input.callId ?? input.requestId,
151
175
  sessionId: input.sessionId ?? 'unknown',
152
176
  agentAid: input.agentAid ?? 'unknown',
153
177
  agentName: input.agentName,
@@ -161,6 +185,9 @@ export function auditCodexApprovalDecision(input) {
161
185
  taskId: input.taskId,
162
186
  argsSummary: {
163
187
  method: input.method,
188
+ ...(input.summary ? { summary: input.summary } : {}),
189
+ ...(input.threadId ? { threadId: input.threadId } : {}),
190
+ ...(input.cwd ? { cwd: input.cwd } : {}),
164
191
  ...(input.parseFailure ? { parseFailure: input.parseFailure } : {}),
165
192
  },
166
193
  });
@@ -183,12 +210,18 @@ export function auditPermissionDecision(input) {
183
210
  toolName: input.toolName,
184
211
  policyCode: input.policyCode,
185
212
  decisionSource: input.decisionSource,
213
+ errorCode: input.decisionSource === 'approval'
214
+ ? 'USER_DENIED'
215
+ : input.decisionSource === 'policy'
216
+ ? 'POLICY_DENIED'
217
+ : infrastructureErrorCode(input.policyCode),
186
218
  source: 'agent-tool',
187
219
  operation: 'permission.runtime',
188
220
  scope: 'filesystem',
189
221
  dangerous: false,
190
222
  role: input.role ?? 'unknown',
191
223
  taskId: input.taskId,
224
+ argsSummary: input.summary ? { summary: input.summary } : undefined,
192
225
  decision: 'deny',
193
226
  executed: false,
194
227
  executionState: 'blocked',
@@ -205,6 +238,7 @@ export function auditToolInfrastructureFailure(input) {
205
238
  dangerous: false,
206
239
  decision: 'deny',
207
240
  decisionSource: 'infrastructure',
241
+ errorCode: infrastructureErrorCode(input.policyCode),
208
242
  executed: false,
209
243
  executionState: 'blocked',
210
244
  callId: input.callId,
@@ -224,8 +258,26 @@ export function auditToolInfrastructureFailure(input) {
224
258
  : undefined,
225
259
  });
226
260
  }
261
+ function infrastructureErrorCode(policyCode) {
262
+ const code = String(policyCode ?? '').toLowerCase();
263
+ if (/delegat|carrier|not.?armed/.test(code))
264
+ return 'DELEGATION_FAILED';
265
+ if (/capability|sandbox|unavailable|workspace_unknown/.test(code))
266
+ return 'CAPABILITY_UNAVAILABLE';
267
+ if (/argument|canonical|quote|shell|parse|invalid/.test(code))
268
+ return 'INVALID_ARGUMENT';
269
+ return 'EXECUTION_FAILED';
270
+ }
227
271
  function buildAuditRecord(event) {
228
272
  const executed = event.executed ?? false;
273
+ const decisionSource = event.decisionSource ?? (event.decision === 'deny' ? 'policy' : 'command');
274
+ const errorCode = event.errorCode ?? (event.decision === 'deny'
275
+ ? decisionSource === 'approval'
276
+ ? 'USER_DENIED'
277
+ : decisionSource === 'policy'
278
+ ? 'POLICY_DENIED'
279
+ : 'EXECUTION_FAILED'
280
+ : undefined);
229
281
  const executionState = event.executionState
230
282
  ?? (event.decision === 'deny'
231
283
  ? 'blocked'
@@ -244,6 +296,7 @@ function buildAuditRecord(event) {
244
296
  operation: event.operation,
245
297
  })
246
298
  : undefined),
299
+ eventPhase: event.eventPhase,
247
300
  callId: event.callId,
248
301
  correlationId: event.correlationId ?? event.requestId,
249
302
  requestId: event.requestId,
@@ -253,10 +306,15 @@ function buildAuditRecord(event) {
253
306
  permissionMode: normalizeAuditPermissionMode(event.permissionMode),
254
307
  toolName: event.toolName,
255
308
  approvalMethod: event.approvalMethod,
309
+ errorCode,
256
310
  policyCode: event.policyCode ?? event.code,
257
311
  protectionClass: event.protectionClass,
258
312
  matchedPath: event.matchedPath,
259
- decisionSource: event.decisionSource,
313
+ decisionSource,
314
+ ecwebRequestId: event.ecwebRequestId,
315
+ targetAgentAid: redactIdentifier(event.targetAgentAid),
316
+ endpoint: event.endpoint,
317
+ logicalToolName: event.logicalToolName,
260
318
  source: event.source,
261
319
  operation: event.operation,
262
320
  scope: event.scope,
@@ -343,11 +401,17 @@ function logAuditEvent(record) {
343
401
  record.callId ? `call=${record.callId}` : null,
344
402
  record.correlationId ? `correlation=${record.correlationId}` : null,
345
403
  record.eventKey ? `eventKey=${record.eventKey}` : null,
404
+ record.eventPhase ? `eventPhase=${record.eventPhase}` : null,
346
405
  record.sessionId ? `session=${record.sessionId}` : null,
347
406
  record.agentName ? `agentName=${JSON.stringify(record.agentName)}` : null,
348
407
  record.agentAid ? `agent=${record.agentAid}` : null,
349
408
  record.permissionMode ? `permissionMode=${record.permissionMode}` : null,
350
409
  record.toolName ? `tool=${record.toolName}` : null,
410
+ record.errorCode ? `errorCode=${record.errorCode}` : null,
411
+ record.ecwebRequestId ? `ecwebRequest=${record.ecwebRequestId}` : null,
412
+ record.targetAgentAid ? `targetAgent=${record.targetAgentAid}` : null,
413
+ record.endpoint ? `endpoint=${JSON.stringify(record.endpoint)}` : null,
414
+ record.logicalToolName ? `logicalTool=${record.logicalToolName}` : null,
351
415
  record.policyCode ? `policy=${record.policyCode}` : null,
352
416
  record.protectionClass ? `protectionClass=${record.protectionClass}` : null,
353
417
  record.matchedPath ? `matchedPath=${JSON.stringify(record.matchedPath)}` : null,
@@ -38,6 +38,10 @@ const GROUP_MANAGEMENT_OPERATIONS = [
38
38
  ['ec.group.join', 'write-agent', ['agent'], 'Request to join a group'],
39
39
  ['ec.group.leave', 'write-agent', ['agent'], 'Leave a group'],
40
40
  ['ec.group.invite', 'write-agent', ['agent'], 'Invite group members'],
41
+ ['ec.group.invites', 'read', ['agent'], 'List pending targeted group invitations'],
42
+ ['ec.group.accept-invite', 'write-agent', ['agent'], 'Accept a targeted group invitation'],
43
+ ['ec.group.reject-invite', 'write-agent', ['agent'], 'Reject a targeted group invitation'],
44
+ ['ec.group.revoke-invite', 'write-agent', ['agent'], 'Revoke a targeted group invitation'],
41
45
  ['ec.group.kick', 'write-agent', ['agent'], 'Remove a group member'],
42
46
  ['ec.group.role', 'write-agent', ['agent'], 'Change a group member role'],
43
47
  ['ec.group.owner', 'write-agent', ['agent'], 'Transfer group ownership'],