evolcore 0.0.17 → 0.0.18

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 (62) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/bin/codex-managed-hook.mjs +16 -7
  3. package/bin/install-codex-managed-hooks.mjs +4 -2
  4. package/dist/agents/claude-runner.js +113 -24
  5. package/dist/agents/codex-app-server-client.js +6 -1
  6. package/dist/agents/codex-runner.js +21 -6
  7. package/dist/agents/ecagent-runner.js +39 -10
  8. package/dist/agents/gemini-runner.js +90 -19
  9. package/dist/aun/aid/store.js +36 -0
  10. package/dist/aun/msg/p2p.js +20 -8
  11. package/dist/channels/aun.js +159 -21
  12. package/dist/cli/agent-command.js +67 -6
  13. package/dist/cli/agent.js +26 -0
  14. package/dist/cli/command-log.js +23 -4
  15. package/dist/cli/daemon-commands.js +53 -12
  16. package/dist/cli/init.js +21 -5
  17. package/dist/cli/restart-monitor.js +13 -6
  18. package/dist/cli/watch-logs.js +2 -2
  19. package/dist/config/builtin-roles.js +5 -1
  20. package/dist/config/role-ranks.js +4 -0
  21. package/dist/core/audit/event-key.js +29 -0
  22. package/dist/core/audit/log-integrity.js +13 -3
  23. package/dist/core/auth/auth-gateway.js +14 -18
  24. package/dist/core/auth/authorization-audit.js +110 -3
  25. package/dist/core/auth/authorization-denial.js +17 -0
  26. package/dist/core/auth/operation-authorizer.js +143 -18
  27. package/dist/core/auth/operation-catalog.js +21 -5
  28. package/dist/core/bootstrap-messages.js +11 -6
  29. package/dist/core/bootstrap-service.js +26 -4
  30. package/dist/core/causation/aun-association.js +7 -4
  31. package/dist/core/command/agent-control.js +25 -16
  32. package/dist/core/command/command-handler.js +50 -4
  33. package/dist/core/command/group-menu.js +1 -1
  34. package/dist/core/command/menu-catalog.js +32 -7
  35. package/dist/core/command/menu-handler.js +59 -23
  36. package/dist/core/command/menu-protocol.js +196 -0
  37. package/dist/core/command/slash-gate.js +14 -5
  38. package/dist/core/command/slash-handler.js +81 -99
  39. package/dist/core/event-catalog.js +18 -0
  40. package/dist/core/message/message-bridge.js +72 -9
  41. package/dist/core/message/pause-controller.js +53 -0
  42. package/dist/core/message/response-engine.js +97 -11
  43. package/dist/core/permission/sandbox-runtime.js +79 -13
  44. package/dist/core/permission/tool-policy.js +1 -1
  45. package/dist/index.js +357 -48
  46. package/dist/ipc.js +75 -4
  47. package/dist/utils/atomic-write.js +45 -11
  48. package/dist/utils/logger.js +27 -0
  49. package/dist/utils/windows-autostart.js +740 -83
  50. package/ecagent/dist/harness/agent-harness.d.ts +1 -1
  51. package/ecagent/dist/harness/agent-harness.js +6 -4
  52. package/kits/docs/evolcore/config.md +1 -1
  53. package/kits/docs/evolcore/group-rules.md +2 -1
  54. package/kits/docs/identity/ROLE_DETAIL.md +3 -1
  55. package/kits/eck_manifest.json +25 -16
  56. package/kits/rules/01-overview.md +5 -5
  57. package/kits/rules/03-identity.md +1 -1
  58. package/kits/rules/04-relation.md +4 -4
  59. package/kits/rules/05-venue.md +5 -5
  60. package/kits/templates/bootstrap-welcome.md +3 -1
  61. package/kits/templates/system-fragments/bootstrap.md +17 -9
  62. package/package.json +1 -1
@@ -64,6 +64,23 @@ async function probeDaemon(socketPath, timeoutMs = 1000) {
64
64
  return null;
65
65
  return response;
66
66
  }
67
+ /** Ask the daemon to run its own graceful shutdown lifecycle. */
68
+ export async function requestDaemonShutdown(socketPath, timeoutMs = 3_000, reason = 'cli', expectedPids) {
69
+ const daemon = await probeDaemon(socketPath, Math.min(timeoutMs, 1_000));
70
+ if (!daemon || (expectedPids && !expectedPids.has(daemon.pid)))
71
+ return null;
72
+ const response = await ipcQuery(socketPath, {
73
+ type: 'shutdown',
74
+ reason,
75
+ expectedPid: daemon.pid,
76
+ }, timeoutMs);
77
+ if (response?.ok !== true || response.accepted !== true
78
+ || !Number.isInteger(response.pid) || response.pid <= 0
79
+ || response.pid !== daemon.pid) {
80
+ return null;
81
+ }
82
+ return response.pid;
83
+ }
67
84
  function hasVisibleDaemonPid(status, ping) {
68
85
  return status.mains.some(entry => entry.alive && entry.record.pid === ping.pid);
69
86
  }
@@ -562,10 +579,11 @@ export async function cmdStart(opts = {}) {
562
579
  };
563
580
  setTimeout(checkReady, 1000);
564
581
  }
565
- async function stopPid(pid) {
582
+ async function stopPid(pid, gracefulRequested = false) {
566
583
  console.log(`🛑 Stopping EvolCore (PID: ${pid})...`);
567
- platform.killProcess(pid);
568
- if (await platform.waitForProcessExit(pid, 10_000)) {
584
+ if (!gracefulRequested)
585
+ platform.killProcess(pid);
586
+ if (await platform.waitForProcessExit(pid, gracefulRequested ? 30_000 : 10_000)) {
569
587
  console.log('✓ EvolCore stopped');
570
588
  return true;
571
589
  }
@@ -608,7 +626,10 @@ export async function cmdStop() {
608
626
  if (runtimeOrphans.length > 0) {
609
627
  console.log(`⚠ 检测到未登记的 EvolCore 进程,将一并停止: ${runtimeOrphans.map(orphan => orphan.pid).join(', ')}`);
610
628
  }
611
- const stopped = await Promise.all([...pids].map(pid => stopPid(pid)));
629
+ const gracefulPid = ping && pids.has(ping.pid)
630
+ ? await requestDaemonShutdown(p.socket, 3_000, 'ec stop', pids)
631
+ : null;
632
+ const stopped = await Promise.all([...pids].map(pid => stopPid(pid, pid === gracefulPid)));
612
633
  if (stopped.some(result => !result)) {
613
634
  process.exitCode = 1;
614
635
  return;
@@ -675,7 +696,12 @@ export async function cmdRestart(opts = {}) {
675
696
  if (aliveMains.length > 1) {
676
697
  console.log(`⚠ 检测到 ${aliveMains.length} 个 main 实例,将一并停止: ${aliveMains.map(m => m.record.pid).join(', ')}`);
677
698
  }
678
- const stopped = await Promise.all(aliveMains.map(m => stopPid(m.record.pid)));
699
+ const mainPids = new Set(aliveMains.map(entry => entry.record.pid));
700
+ const currentPing = await probeDaemon(socketPath);
701
+ const gracefulPid = currentPing && mainPids.has(currentPing.pid)
702
+ ? await requestDaemonShutdown(socketPath, 3_000, 'ec restart', mainPids)
703
+ : null;
704
+ const stopped = await Promise.all(aliveMains.map(m => stopPid(m.record.pid, m.record.pid === gracefulPid)));
679
705
  if (stopped.some(result => !result)) {
680
706
  console.error('❌ Restart aborted because an existing EvolCore process could not be stopped');
681
707
  process.exitCode = 1;
@@ -703,7 +729,12 @@ export async function cmdRestart(opts = {}) {
703
729
  const runtimeOrphans = findOrphanProcesses().filter(o => isRuntimeOrphan(o));
704
730
  if (runtimeOrphans.length > 0) {
705
731
  console.log(`⚠ 检测到未登记的 EvolCore 进程,将一并停止: ${runtimeOrphans.map(o => o.pid).join(', ')}`);
706
- const stopped = await Promise.all(runtimeOrphans.map(o => stopPid(o.pid)));
732
+ const orphanPids = new Set(runtimeOrphans.map(orphan => orphan.pid));
733
+ const currentPing = await probeDaemon(socketPath);
734
+ const gracefulPid = currentPing && orphanPids.has(currentPing.pid)
735
+ ? await requestDaemonShutdown(socketPath, 3_000, 'ec restart', orphanPids)
736
+ : null;
737
+ const stopped = await Promise.all(runtimeOrphans.map(o => stopPid(o.pid, o.pid === gracefulPid)));
707
738
  if (stopped.some(result => !result)) {
708
739
  console.error('❌ Restart aborted because an unregistered EvolCore process could not be stopped');
709
740
  process.exitCode = 1;
@@ -1359,7 +1390,7 @@ export function cmdLogs(args) {
1359
1390
  // ==================== Watch ====================
1360
1391
  let watchUseColor = false;
1361
1392
  const WATCH_BRACKET_TS_RE = /^\[(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?)\]/;
1362
- const WATCH_JSON_TS_RE = /"ts"\s*:\s*"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?)"/;
1393
+ const WATCH_JSON_TS_RE = /"(?:ts|timestamp|startedAt|firedAt|finishedAt)"\s*:\s*(?:"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?)"|(\d+(?:\.\d+)?))/;
1363
1394
  function parseWatchTs(s) {
1364
1395
  const m = s.match(/^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(Z)?$/);
1365
1396
  if (!m)
@@ -1371,10 +1402,21 @@ function parseWatchTs(s) {
1371
1402
  return new Date(+y, +mo - 1, +d, +h, +mi, +se, msNum).getTime();
1372
1403
  }
1373
1404
  function extractWatchTs(line) {
1374
- const m = line.match(WATCH_BRACKET_TS_RE) || line.match(WATCH_JSON_TS_RE);
1375
- if (!m)
1405
+ const bracket = line.match(WATCH_BRACKET_TS_RE);
1406
+ if (bracket) {
1407
+ const t = parseWatchTs(bracket[1]);
1408
+ return isNaN(t) ? null : t;
1409
+ }
1410
+ const json = line.match(WATCH_JSON_TS_RE);
1411
+ if (!json)
1376
1412
  return null;
1377
- const t = parseWatchTs(m[1]);
1413
+ if (json[2] !== undefined) {
1414
+ const numeric = Number(json[2]);
1415
+ if (!Number.isFinite(numeric) || numeric <= 0)
1416
+ return null;
1417
+ return numeric < 100_000_000_000 ? numeric * 1000 : numeric;
1418
+ }
1419
+ const t = parseWatchTs(json[1]);
1378
1420
  return isNaN(t) ? null : t;
1379
1421
  }
1380
1422
  function toLocalTimeStr(epoch) {
@@ -1791,8 +1833,7 @@ function cmdWatch(filterTypes) {
1791
1833
  const all = fs.readdirSync(p.logs).filter(f => f.endsWith('.log')).map(f => path.join(p.logs, f));
1792
1834
  return filterLogFiles(all, filterTypes);
1793
1835
  };
1794
- // Strip rotation suffix (e.g., "daemon-20260518-21" → "daemon")
1795
- const shortName = (f) => path.basename(f, '.log').replace(/-\d{8}-\d{2}$/, '');
1836
+ const shortName = shortLogNameLocal;
1796
1837
  // 计算最长文件名用于对齐
1797
1838
  let maxNameLen = 0;
1798
1839
  const updateMaxName = () => {
package/dist/cli/init.js CHANGED
@@ -82,11 +82,11 @@ function ecagentUnavailableReason() {
82
82
  function buildDefaults(chosen, available, projectsDefaultPath) {
83
83
  const baseagents = {};
84
84
  for (const b of available) {
85
- // ecagent is bundled but opt-in; do not declare it merely because it is available.
86
- if (b === 'ecagent' && chosen !== 'ecagent')
87
- continue;
88
85
  baseagents[b] = {};
89
86
  }
87
+ // ecagent is bundled with EvolCore, so declare it even when the caller
88
+ // supplies a reduced availability list (for example, a test probe).
89
+ baseagents.ecagent ??= {};
90
90
  return {
91
91
  $schema_version: 1,
92
92
  active_baseagent: chosen,
@@ -125,9 +125,12 @@ function applyExplicitAutostart(enabled, runtimeRoot) {
125
125
  console.log(`❌ ${autostartPlatformLabel() || '登录自启'}配置失败: ${result.error || '未知错误'}`);
126
126
  }
127
127
  else {
128
+ const backend = enabled && result.backend ? `(方式:${result.backend})` : '';
128
129
  console.log(enabled
129
- ? `✓ 已启用 ${autostartPlatformLabel() || '登录'}自启`
130
+ ? `✓ 已启用 ${autostartPlatformLabel() || '登录'}自启${backend}`
130
131
  : `✓ 已禁用 ${autostartPlatformLabel() || '登录'}自启`);
132
+ if (result.warning)
133
+ console.log(`⚠ ${result.warning}`);
131
134
  }
132
135
  return result.ok;
133
136
  }
@@ -508,9 +511,12 @@ export async function initTail(options = {}) {
508
511
  console.log(` ❌ ${autostartPlatformLabel() || '登录自启'}配置失败: ${result.error || '未知错误'}`);
509
512
  return false;
510
513
  }
514
+ const backend = enabled && result.backend ? `(方式:${result.backend})` : '';
511
515
  console.log(enabled
512
- ? ` ✓ 已启用 ${autostartPlatformLabel() || '登录'}自启`
516
+ ? ` ✓ 已启用 ${autostartPlatformLabel() || '登录'}自启${backend}`
513
517
  : ` ✓ 已禁用 ${autostartPlatformLabel() || '登录'}自启`);
518
+ if (result.warning)
519
+ console.log(` ⚠ ${result.warning}`);
514
520
  return true;
515
521
  }
516
522
  async function handleOwnersPrompt(rl) {
@@ -827,11 +833,19 @@ export async function cmdInitNonInteractive(opts, dependencies = {}) {
827
833
  catch (e) {
828
834
  fail('IO_ERROR', `failed to write daemon.json: ${e?.message || e}`, EXIT_RUNTIME, format);
829
835
  }
836
+ let autostartBackend;
837
+ let autostartWarning;
830
838
  if (opts.autoStart !== undefined) {
831
839
  const autostart = configureAutostart(opts.autoStart, p.root);
832
840
  if (!autostart.ok) {
833
841
  fail('AUTOSTART_CONFIG_FAILED', autostart.error || 'failed to configure login autostart', EXIT_RUNTIME, format);
834
842
  }
843
+ if (opts.autoStart === true && autostart.backend) {
844
+ // Keep the selected Windows backend visible to automation callers as
845
+ // well as in data/autostart-state.json.
846
+ autostartBackend = autostart.backend;
847
+ }
848
+ autostartWarning = autostart.warning;
835
849
  }
836
850
  // ── 10. 输出 init.result ──
837
851
  const result = {
@@ -845,6 +859,8 @@ export async function cmdInitNonInteractive(opts, dependencies = {}) {
845
859
  projectsDefaultPath: projectsDefaultPath ?? null,
846
860
  defaultsPath: p.defaultsConfig,
847
861
  daemonConfigPath: p.daemonConfig,
862
+ ...(autostartBackend ? { autoStartBackend: autostartBackend } : {}),
863
+ ...(autostartWarning ? { autoStartWarning: autostartWarning } : {}),
848
864
  ...(forced ? { forced: true, previousOwners } : {}),
849
865
  };
850
866
  emitResult(result, format);
@@ -14,6 +14,7 @@ import { WEB_PACKAGE_NAME } from '../product.js';
14
14
  import { shouldSuppressRealRestart } from '../utils/restart-safety.js';
15
15
  import { rotateStdoutLog } from '../utils/log-writer.js';
16
16
  import { inspectDataMigrationRequirement } from '../core/data-migration.js';
17
+ import { requestDaemonShutdown } from './daemon-commands.js';
17
18
  const execFileAsync = promisify(execFile);
18
19
  // 清理 Claude Code 环境变量,防止 SDK 认为是嵌套会话
19
20
  function cleanEnv() {
@@ -105,11 +106,14 @@ export async function cmdRestartMonitor() {
105
106
  if (runtimeOrphans.length > 0) {
106
107
  const pids = runtimeOrphans.map(o => o.pid).join(', ');
107
108
  log(`Stopping unregistered EvolCore process(es): ${pids}`);
109
+ const orphanPids = new Set(runtimeOrphans.map(orphan => orphan.pid));
110
+ const ping = await requestDaemonShutdown(p.socket, 3_000, 'restart-monitor orphan cleanup', orphanPids);
108
111
  for (const orphan of runtimeOrphans) {
109
- platform.killProcess(orphan.pid, false);
112
+ if (orphan.pid !== ping)
113
+ platform.killProcess(orphan.pid, false);
110
114
  }
111
115
  const exited = await Promise.all(runtimeOrphans.map(async (orphan) => {
112
- if (await platform.waitForProcessExit(orphan.pid, 10_000))
116
+ if (await platform.waitForProcessExit(orphan.pid, orphan.pid === ping ? 30_000 : 10_000))
113
117
  return true;
114
118
  platform.killProcess(orphan.pid, true);
115
119
  return platform.waitForProcessExit(orphan.pid, 5_000);
@@ -130,12 +134,15 @@ export async function cmdRestartMonitor() {
130
134
  if (aliveMains.length > 0) {
131
135
  const oldPids = aliveMains.map(m => m.record.pid);
132
136
  log(`Monitoring ${oldPids.length} main process(es): ${oldPids.join(', ')}`);
133
- // 先并行 SIGTERM 通知所有活 main
137
+ const gracefulPid = await requestDaemonShutdown(p.socket, 3_000, 'restart-monitor', new Set(oldPids));
138
+ // 没有收到 IPC shutdown 确认的进程才走信号兜底
134
139
  for (const pid of oldPids) {
135
- try {
136
- platform.killProcess(pid, false);
140
+ if (pid !== gracefulPid) {
141
+ try {
142
+ platform.killProcess(pid, false);
143
+ }
144
+ catch { }
137
145
  }
138
- catch { }
139
146
  }
140
147
  const exited = await Promise.all(oldPids.map(async (oldPid) => {
141
148
  if (await platform.waitForProcessExit(oldPid, 30_000)) {
@@ -1,8 +1,8 @@
1
1
  import path from 'path';
2
- /** 去掉轮转后缀("daemon-20260518-21.log" → "daemon";"ts-sdk-2026-05-27.log" → "ts-sdk")。入参可为文件名或绝对路径。 */
2
+ /** 去掉轮转后缀(按小时、按日及旧版带连字符日期)。入参可为文件名或绝对路径。 */
3
3
  export function shortLogName(file) {
4
4
  return path.basename(file, '.log')
5
- .replace(/-\d{8}-\d{2}$/, '') // -YYYYMMDD-HH(按小时轮转)
5
+ .replace(/-\d{8}(?:-\d{2})?$/, '') // -YYYYMMDD[-HH](按日/小时轮转)
6
6
  .replace(/-\d{4}-\d{2}-\d{2}$/, ''); // -YYYY-MM-DD(按日轮转,如 ts-sdk)
7
7
  }
8
8
  /** 从 .log 文件名列表推导去重、字母序的类型列表。 */
@@ -3,11 +3,15 @@ import { expectedRoleRank } from './role-ranks.js';
3
3
  export { expectedRoleRank, ROLE_RANKS } from './role-ranks.js';
4
4
  export const BUILTIN_USER_ROLES = ['member', 'visitor'];
5
5
  export const MANAGEMENT_ROLES = ['owner', 'admin'];
6
+ export const DAEMON_OWNER_ROLE = 'daemon-owner';
6
7
  export function isManagementRole(role) {
7
8
  return role === 'owner' || role === 'admin';
8
9
  }
10
+ export function isDaemonOwnerRole(role) {
11
+ return role === DAEMON_OWNER_ROLE;
12
+ }
9
13
  export function isReservedRoleName(role) {
10
- return isManagementRole(role);
14
+ return isManagementRole(role) || isDaemonOwnerRole(role);
11
15
  }
12
16
  /** Built-in user policy data comes exclusively from kits/templates/roles. */
13
17
  export function getBuiltinRolesConfig() {
@@ -1,4 +1,6 @@
1
1
  export const ROLE_RANKS = Object.freeze({
2
+ /** Process-plane rank for display/audit only; it is never relation-assignable. */
3
+ 'daemon-owner': 1000,
2
4
  owner: 900,
3
5
  admin: 700,
4
6
  custom: 500,
@@ -6,6 +8,8 @@ export const ROLE_RANKS = Object.freeze({
6
8
  visitor: 100,
7
9
  });
8
10
  export function expectedRoleRank(role) {
11
+ if (role === 'daemon-owner')
12
+ return ROLE_RANKS['daemon-owner'];
9
13
  if (role === 'owner')
10
14
  return ROLE_RANKS.owner;
11
15
  if (role === 'admin')
@@ -0,0 +1,29 @@
1
+ import crypto from 'node:crypto';
2
+ function compactPart(value) {
3
+ const text = String(value ?? '').trim();
4
+ return text || undefined;
5
+ }
6
+ function stableDigest(parts) {
7
+ const material = parts.map(part => compactPart(part) ?? '-').join('\u0000');
8
+ return crypto.createHash('sha256').update(material).digest('hex').slice(0, 20);
9
+ }
10
+ /** Stable across source/mirror records for one tool-call lifecycle. */
11
+ export function buildToolLifecycleEventKey(input) {
12
+ return `tool:${stableDigest([
13
+ input.sessionId ?? 'unknown',
14
+ input.callId ?? input.correlationId ?? input.requestId ?? 'unknown',
15
+ ])}`;
16
+ }
17
+ /** Stable across repeated projections of one authorization decision. */
18
+ export function buildAuthorizationEventKey(input) {
19
+ const lifecycleId = input.callId ?? input.correlationId ?? input.requestId;
20
+ if (lifecycleId) {
21
+ return `${buildToolLifecycleEventKey({
22
+ sessionId: input.sessionId,
23
+ callId: input.callId,
24
+ correlationId: input.correlationId,
25
+ requestId: input.requestId,
26
+ })}:authorization:${stableDigest([input.operation])}`;
27
+ }
28
+ return `authorization:${stableDigest([input.sessionId ?? 'unknown', input.operation])}`;
29
+ }
@@ -77,6 +77,10 @@ export function inspectStructuredLogs(logDir, options = {}) {
77
77
  }
78
78
  function structuredRecordKey(value) {
79
79
  const nested = nestedRecord(value);
80
+ const eventKey = firstDefined(value, nested, ['eventKey', 'event_key']);
81
+ const eventPhase = firstDefined(value, nested, ['eventPhase', 'event_phase']);
82
+ if (eventKey)
83
+ return `${String(eventKey)}${eventPhase ? `:${String(eventPhase)}` : ''}`;
80
84
  const correlation = firstDefined(value, nested, [
81
85
  'correlationId', 'correlation_id', 'callId', 'call_id', 'toolUseId', 'tool_use_id',
82
86
  'requestId', 'request_id', 'msgId', 'operationId',
@@ -96,17 +100,23 @@ function isCorrelatableRecord(value) {
96
100
  }
97
101
  function stableHash(value) {
98
102
  const nested = nestedRecord(value);
103
+ const eventKey = firstDefined(value, nested, ['eventKey', 'event_key']);
99
104
  const canonical = {
105
+ eventKey,
106
+ eventPhase: firstDefined(value, nested, ['eventPhase', 'event_phase']),
100
107
  type: canonicalEventType(value),
101
108
  toolName: firstDefined(value, nested, ['toolName', 'tool', 'name']),
102
- callId: firstDefined(value, nested, ['callId', 'call_id', 'toolUseId', 'tool_use_id']),
103
- correlationId: firstDefined(value, nested, ['correlationId', 'correlation_id']),
104
- requestId: firstDefined(value, nested, ['requestId', 'request_id']),
109
+ callId: eventKey ? undefined : firstDefined(value, nested, ['callId', 'call_id', 'toolUseId', 'tool_use_id']),
110
+ correlationId: eventKey ? undefined : firstDefined(value, nested, ['correlationId', 'correlation_id']),
111
+ requestId: eventKey ? undefined : firstDefined(value, nested, ['requestId', 'request_id']),
105
112
  sessionId: firstDefined(value, nested, ['sessionId', 'session_id']),
106
113
  agentAid: firstDefined(value, nested, ['agentAid', 'agent_aid', 'selfAid']),
107
114
  permissionMode: firstDefined(value, nested, ['permissionMode', 'permission_mode']),
108
115
  isError: firstDefined(value, nested, ['isError', 'is_error']) ?? (value.ok === false || nested?.ok === false ? true : undefined),
109
116
  errorCode: firstDefined(value, nested, ['errorCode', 'error_code']),
117
+ decision: firstDefined(value, nested, ['decision']),
118
+ executed: firstDefined(value, nested, ['executed']),
119
+ executionState: firstDefined(value, nested, ['executionState', 'execution_state']),
110
120
  input: firstDefined(value, nested, ['input']),
111
121
  result: firstDefined(value, nested, ['result', 'content']),
112
122
  error: firstDefined(value, nested, ['error', 'errorMessage']),
@@ -5,20 +5,8 @@ import { authorizeCommand, authorizeResolvedConfigCommand } from './operation-au
5
5
  import { auditCommandAuthorization } from './authorization-audit.js';
6
6
  import { hasTrustedPrincipal } from './authenticated-actor.js';
7
7
  import { resolveRuntimePermissionMode } from '../role/runtime-policy.js';
8
- const CROSS_AGENT_READ_OPERATIONS = new Set([
9
- 'trigger.list',
10
- 'trigger.show',
11
- 'trigger.history',
12
- ]);
13
8
  export function isCrossAgentTriggerOperationAllowed(input) {
14
- if (input.control || input.taskAgentAid === input.targetAgentAid)
15
- return true;
16
- if (!input.taskAgentAid || !input.targetManagement)
17
- return false;
18
- return CROSS_AGENT_READ_OPERATIONS.has(input.operation);
19
- }
20
- export function isCrossAgentTriggerReadOperation(operation) {
21
- return CROSS_AGENT_READ_OPERATIONS.has(operation);
9
+ return input.control || input.daemonOwner === true || input.taskAgentAid === input.targetAgentAid;
22
10
  }
23
11
  export function buildAuthSubject(input) {
24
12
  const chatType = input.chatType === 'group' ? 'group' : 'private';
@@ -34,12 +22,14 @@ export function buildAuthSubject(input) {
34
22
  peerType: input.peerType,
35
23
  });
36
24
  const processOwners = input.processOwners ?? [];
37
- const isDaemonOwner = isProcessOwner({
25
+ const isDaemonService = input.trustedProcessRole === 'daemon-service';
26
+ const isDaemonOwner = !isDaemonService && isProcessOwner({
38
27
  actor: roleDetail.actor,
39
28
  processOwners,
40
29
  });
41
30
  const suppliedRole = input.identity?.role && input.identity.role !== 'none' ? input.identity.role : undefined;
42
- const role = suppliedRole ?? (isDaemonOwner ? 'owner' : roleDetail.effectiveRole ?? 'none');
31
+ const role = suppliedRole ?? roleDetail.effectiveRole ?? 'none';
32
+ const processRole = isDaemonService ? 'daemon-service' : isDaemonOwner ? 'daemon-owner' : 'none';
43
33
  return {
44
34
  selfAid: input.selfAid,
45
35
  actorId,
@@ -52,11 +42,13 @@ export function buildAuthSubject(input) {
52
42
  conversationId,
53
43
  peerKey: input.channelType && conversationId ? formatPeerKey(input.channelType, conversationId) : undefined,
54
44
  role,
55
- roleSource: isDaemonOwner && !roleDetail.effectiveRole ? 'fallback' : roleDetail.source,
45
+ relationRole: role,
46
+ processRole,
47
+ roleSource: roleDetail.source,
56
48
  identity: input.identity ?? roleToSessionIdentity(role === 'none' ? null : role),
57
49
  isDaemonOwner,
58
50
  fromControlChannel: !!input.fromControlChannel,
59
- allowAccess: isDaemonOwner || (suppliedRole
51
+ allowAccess: processRole !== 'none' || (suppliedRole
60
52
  ? checkRoleAccess(suppliedRole, input.selfAid)
61
53
  : roleDetail.allowAccess && checkRoleAccess(role, input.selfAid)),
62
54
  permissionMode: input.permissionMode
@@ -104,6 +96,7 @@ export function authorizeOperation(params) {
104
96
  selfAid: params.subject.selfAid,
105
97
  peerKey: params.subject.peerKey,
106
98
  role: params.subject.role,
99
+ processRole: params.subject.processRole,
107
100
  isDaemonOwner: params.subject.isDaemonOwner,
108
101
  fromControlChannel: params.subject.fromControlChannel,
109
102
  allowExplicitRelationTarget: params.allowExplicitRelationTarget,
@@ -183,10 +176,13 @@ function auditDecision(params, decision) {
183
176
  peerKey: params.subject.peerKey,
184
177
  channel: params.subject.channel,
185
178
  channelId: params.subject.channelId,
186
- role: params.subject.role,
179
+ role: decision.allow ? decision.role : params.subject.role,
180
+ processRole: params.subject.processRole,
187
181
  isDaemonOwner: params.subject.isDaemonOwner,
188
182
  fromControlChannel: params.subject.fromControlChannel,
189
183
  decision: decision.allow ? 'allow' : 'deny',
184
+ executed: false,
185
+ executionState: decision.allow ? 'authorized' : 'blocked',
190
186
  code: decision.allow ? undefined : decision.code,
191
187
  reason: decision.allow ? undefined : decision.reason,
192
188
  matchedRule: decision.matchedRule,
@@ -2,6 +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
6
  export function auditCommandAuthorization(event) {
6
7
  const shouldAudit = event.source === 'menu.cli' ||
7
8
  event.decision === 'deny' ||
@@ -23,11 +24,15 @@ export function auditCommandAuthorization(event) {
23
24
  export function auditToolPreflightDenial(input) {
24
25
  auditCommandAuthorization({
25
26
  ts: Date.now(),
27
+ callId: input.requestId,
28
+ correlationId: input.requestId,
26
29
  source: 'agent-tool',
27
30
  operation: 'tool.preflight.deny',
28
31
  scope: 'filesystem',
29
32
  dangerous: false,
30
33
  decision: 'deny',
34
+ executed: false,
35
+ executionState: 'blocked',
31
36
  decisionSource: 'policy',
32
37
  toolName: input.toolName,
33
38
  policyCode: input.policyCode,
@@ -55,11 +60,14 @@ export function auditToolPreflightDenial(input) {
55
60
  export function auditCodexApprovalDecision(input) {
56
61
  auditCommandAuthorization({
57
62
  ts: Date.now(),
63
+ callId: input.correlationId ?? input.requestId,
58
64
  source: 'agent-tool',
59
65
  operation: 'codex.approval',
60
66
  scope: input.toolName === 'PermissionGrant' ? 'agent' : 'filesystem',
61
67
  dangerous: input.toolName === 'PermissionGrant',
62
68
  decision: input.decision,
69
+ executed: false,
70
+ executionState: input.decision === 'allow' ? 'authorized' : 'blocked',
63
71
  decisionSource: input.decisionSource,
64
72
  requestId: input.requestId,
65
73
  correlationId: input.correlationId ?? input.requestId,
@@ -73,12 +81,58 @@ export function auditCodexApprovalDecision(input) {
73
81
  reason: input.reason,
74
82
  role: input.role ?? 'unknown',
75
83
  taskId: input.taskId,
76
- argsSummary: { method: input.method, executed: false },
84
+ argsSummary: { method: input.method },
85
+ });
86
+ }
87
+ /** Record a fail-closed infrastructure condition before a tool can execute. */
88
+ export function auditToolInfrastructureFailure(input) {
89
+ auditCommandAuthorization({
90
+ ts: Date.now(),
91
+ source: 'agent-tool',
92
+ operation: 'tool.execution.blocked',
93
+ scope: 'filesystem',
94
+ dangerous: false,
95
+ decision: 'deny',
96
+ decisionSource: 'infrastructure',
97
+ executed: false,
98
+ executionState: 'blocked',
99
+ callId: input.callId,
100
+ correlationId: input.callId,
101
+ requestId: input.callId,
102
+ sessionId: input.sessionId ?? 'unknown',
103
+ agentAid: input.agentAid ?? 'unknown',
104
+ permissionMode: input.permissionMode ?? 'unknown',
105
+ toolName: input.toolName,
106
+ policyCode: input.policyCode,
107
+ reason: input.reason,
108
+ role: input.role ?? 'unknown',
109
+ taskId: input.taskId,
110
+ argsSummary: input.occurrences && input.occurrences > 1
111
+ ? { occurrences: input.occurrences }
112
+ : undefined,
77
113
  });
78
114
  }
79
115
  function buildAuditRecord(event) {
116
+ const executed = event.executed ?? false;
117
+ const executionState = event.executionState
118
+ ?? (event.decision === 'deny'
119
+ ? 'blocked'
120
+ : executed
121
+ ? (event.exitCode === undefined || event.exitCode === 0 ? 'completed' : 'failed')
122
+ : 'authorized');
123
+ const lifecycleId = event.callId ?? event.correlationId ?? event.requestId;
80
124
  return {
81
125
  ts: event.ts,
126
+ eventKey: event.eventKey ?? (lifecycleId
127
+ ? buildAuthorizationEventKey({
128
+ sessionId: event.sessionId,
129
+ callId: event.callId,
130
+ correlationId: event.correlationId,
131
+ requestId: event.requestId,
132
+ operation: event.operation,
133
+ })
134
+ : undefined),
135
+ callId: event.callId,
82
136
  correlationId: event.correlationId ?? event.requestId,
83
137
  requestId: event.requestId,
84
138
  sessionId: event.sessionId,
@@ -96,23 +150,26 @@ function buildAuditRecord(event) {
96
150
  dangerous: event.dangerous,
97
151
  name: event.name,
98
152
  action: event.action,
99
- args: event.args?.argv ? { argv: [...event.args.argv] } : undefined,
153
+ args: event.args?.argv ? { argv: sanitizeAuditArgv(event.args.argv, event.decision) } : undefined,
100
154
  actorId: redactIdentifier(event.actorId),
101
155
  selfAid: redactIdentifier(event.selfAid),
102
156
  peerKey: redactIdentifier(event.peerKey),
103
157
  channel: event.channel,
104
158
  channelId: redactIdentifier(event.channelId),
105
159
  role: event.role,
160
+ processRole: event.processRole,
106
161
  isDaemonOwner: event.isDaemonOwner,
107
162
  fromControlChannel: event.fromControlChannel,
108
163
  taskId: event.taskId,
109
164
  messageId: event.messageId,
110
165
  decision: event.decision,
166
+ executed,
167
+ executionState,
111
168
  code: event.code,
112
169
  reason: event.reason,
113
170
  matchedRule: event.matchedRule,
114
171
  argvHash: event.argvHash,
115
- argsSummary: event.argsSummary,
172
+ argsSummary: summarizeAuthorizationArgs(event.operation, event.argsSummary),
116
173
  durationMs: event.durationMs,
117
174
  exitCode: event.exitCode,
118
175
  };
@@ -148,7 +205,9 @@ function logAuditEvent(record) {
148
205
  record.taskId ? `task=${record.taskId}` : null,
149
206
  record.messageId ? `message=${record.messageId}` : null,
150
207
  record.requestId ? `request=${record.requestId}` : null,
208
+ record.callId ? `call=${record.callId}` : null,
151
209
  record.correlationId ? `correlation=${record.correlationId}` : null,
210
+ record.eventKey ? `eventKey=${record.eventKey}` : null,
152
211
  record.sessionId ? `session=${record.sessionId}` : null,
153
212
  record.agentAid ? `agent=${record.agentAid}` : null,
154
213
  record.permissionMode ? `permissionMode=${record.permissionMode}` : null,
@@ -157,6 +216,8 @@ function logAuditEvent(record) {
157
216
  record.protectionClass ? `protectionClass=${record.protectionClass}` : null,
158
217
  record.matchedPath ? `matchedPath=${JSON.stringify(record.matchedPath)}` : null,
159
218
  record.decisionSource ? `decisionSource=${record.decisionSource}` : null,
219
+ `executed=${record.executed}`,
220
+ `executionState=${record.executionState}`,
160
221
  record.code ? `code=${record.code}` : null,
161
222
  record.matchedRule ? `rule=${record.matchedRule}` : null,
162
223
  record.dangerous ? 'dangerous=true' : null,
@@ -225,6 +286,52 @@ function writeRoleMutationAudit(payload) {
225
286
  }
226
287
  catch { }
227
288
  }
289
+ function summarizeAuthorizationArgs(operation, args) {
290
+ if (!args)
291
+ return undefined;
292
+ const summary = {};
293
+ for (const [key, value] of Object.entries(args)) {
294
+ if (value === undefined)
295
+ continue;
296
+ if (/^(?:self|peer|peerKey|target|targetId|session|sessionId|channelId)$/i.test(key)) {
297
+ summary[key] = typeof value === 'string' ? redactIdentifier(value) : '[redacted]';
298
+ continue;
299
+ }
300
+ if (/(?:value|token|secret|password|credential|content|payload|body|sql|query)/i.test(key)) {
301
+ summary[`${key}Type`] = Array.isArray(value) ? 'array' : typeof value;
302
+ summary[key] = '[redacted]';
303
+ continue;
304
+ }
305
+ if (typeof value === 'string') {
306
+ summary[key] = value.length <= 96 ? value : `${value.slice(0, 93)}...`;
307
+ }
308
+ else if (typeof value === 'number' || typeof value === 'boolean' || value === null) {
309
+ summary[key] = value;
310
+ }
311
+ else {
312
+ summary[key] = `[${Array.isArray(value) ? 'array' : 'object'}]`;
313
+ }
314
+ }
315
+ if (operation === 'ec.msg.history')
316
+ summary.kind = 'history';
317
+ return Object.keys(summary).length > 0 ? summary : undefined;
318
+ }
319
+ function sanitizeAuditArgv(argv, decision) {
320
+ if (decision === 'allow')
321
+ return [...argv];
322
+ const safe = [...argv];
323
+ if (safe[0] === 'config' && safe[1] === 'set' && safe.length > 3) {
324
+ safe[3] = '[redacted]';
325
+ }
326
+ for (let index = 0; index < safe.length - 1; index++) {
327
+ const flag = safe[index];
328
+ if (/^--(?:self|peer|session|channel|channel-id|token)$/i.test(flag)) {
329
+ safe[index + 1] = redactIdentifier(safe[index + 1]) ?? '[redacted]';
330
+ index++;
331
+ }
332
+ }
333
+ return safe;
334
+ }
228
335
  function protectionClassForPolicy(policyCode) {
229
336
  if (!policyCode)
230
337
  return undefined;
@@ -0,0 +1,17 @@
1
+ const NEXT_STEPS = {
2
+ DAEMON_OWNER_REQUIRED: 'Ask an AID listed in daemon.json.owners to run this operation from the daemon control channel or ECWeb.',
3
+ TARGET_SELF_ONLY: 'Target the current Agent, or ask a DaemonOwner to perform the cross-Agent operation.',
4
+ MANAGED_NON_INTERACTIVE_REQUIRED: 'Use the structured non-interactive Agent command (for example `ec agent new <aid> --non-interactive`) or ECWeb.',
5
+ PURGE_CONFIRMATION_REQUIRED: 'Run the purge from the daemon control plane and add --confirm-purge after verifying the target Agent.',
6
+ };
7
+ export function authorizationNextStep(reasonCode) {
8
+ return NEXT_STEPS[reasonCode];
9
+ }
10
+ export function authorizationDenialData(reasonCode) {
11
+ return { reasonCode, nextStep: authorizationNextStep(reasonCode) };
12
+ }
13
+ export function formatAuthorizationDenial(reason, reasonCode) {
14
+ if (!reasonCode)
15
+ return reason;
16
+ return `${reason}\nNext step: ${authorizationNextStep(reasonCode)}`;
17
+ }