evolcore 0.0.17 → 0.0.19

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 (66) hide show
  1. package/CHANGELOG.md +51 -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 +43 -24
  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 +82 -14
  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/data-migration.js +10 -4
  40. package/dist/core/event-catalog.js +18 -0
  41. package/dist/core/message/message-bridge.js +66 -8
  42. package/dist/core/message/response-engine.js +147 -10
  43. package/dist/core/permission/ec-command-parser.js +148 -22
  44. package/dist/core/permission/sandbox-runtime.js +79 -13
  45. package/dist/core/permission/tool-policy.js +19 -7
  46. package/dist/index.js +357 -48
  47. package/dist/ipc.js +81 -5
  48. package/dist/paths.js +0 -3
  49. package/dist/utils/atomic-write.js +45 -11
  50. package/dist/utils/logger.js +27 -0
  51. package/dist/utils/windows-autostart.js +740 -83
  52. package/ecagent/dist/harness/agent-harness.d.ts +1 -1
  53. package/ecagent/dist/harness/agent-harness.js +6 -4
  54. package/kits/docs/evolcore/config.md +1 -1
  55. package/kits/docs/evolcore/group-rules.md +2 -1
  56. package/kits/docs/identity/ROLE_DETAIL.md +3 -1
  57. package/kits/docs/path-registry.md +1 -1
  58. package/kits/eck_manifest.json +25 -16
  59. package/kits/rules/01-overview.md +5 -5
  60. package/kits/rules/02-navigation.md +2 -2
  61. package/kits/rules/03-identity.md +1 -1
  62. package/kits/rules/04-relation.md +4 -4
  63. package/kits/rules/05-venue.md +5 -5
  64. package/kits/templates/bootstrap-welcome.md +3 -1
  65. package/kits/templates/system-fragments/bootstrap.md +17 -9
  66. 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
  }
@@ -201,6 +218,33 @@ function formatLocalTime(ms) {
201
218
  const d = new Date(ms);
202
219
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`;
203
220
  }
221
+ function formatRunningDuration(ms) {
222
+ const totalSeconds = Math.max(0, Math.floor(ms / 1000));
223
+ const seconds = totalSeconds % 60;
224
+ const totalMinutes = Math.floor(totalSeconds / 60);
225
+ if (totalMinutes === 0)
226
+ return `${seconds}秒`;
227
+ const minutes = totalMinutes % 60;
228
+ const totalHours = Math.floor(totalMinutes / 60);
229
+ if (totalHours === 0)
230
+ return `${minutes}分${seconds}秒`;
231
+ const hours = totalHours % 24;
232
+ const days = Math.floor(totalHours / 24);
233
+ if (days === 0)
234
+ return `${hours}小时${minutes}分${seconds}秒`;
235
+ return `${days}天${hours}小时${minutes}分${seconds}秒`;
236
+ }
237
+ function resolveRunningDuration(pid, uptime) {
238
+ if (typeof uptime === 'number' && Number.isFinite(uptime) && uptime >= 0) {
239
+ return formatRunningDuration(uptime);
240
+ }
241
+ const processStartedAt = getProcessStartTime(pid);
242
+ if (processStartedAt !== null) {
243
+ return formatRunningDuration(Date.now() - processStartedAt);
244
+ }
245
+ const instance = scanInstances().mains.find(entry => entry.alive && entry.record.pid === pid);
246
+ return instance ? formatRunningDuration(Date.now() - instance.record.startedAt) : '未知';
247
+ }
204
248
  function printStartupInfo(opts = {}) {
205
249
  const pkgRoot = getPackageRoot();
206
250
  const isNpmInstall = pkgRoot.includes('node_modules');
@@ -256,7 +300,7 @@ export async function cmdStart(opts = {}) {
256
300
  const existingDaemon = await probeDaemon(p.socket);
257
301
  if (existingDaemon) {
258
302
  console.log(` EvolCore is already running (PID: ${existingDaemon.pid}, IPC reachable)`);
259
- console.log(' 已在运行;部署/脚本可直接复用当前实例');
303
+ console.log(` 已在运行;已运行时长: ${resolveRunningDuration(existingDaemon.pid, existingDaemon.uptime)};部署/脚本可直接复用当前实例`);
260
304
  return;
261
305
  }
262
306
  ensureDataDirs();
@@ -356,7 +400,7 @@ export async function cmdStart(opts = {}) {
356
400
  console.log(` ${symbol} ${aid} — 最后活动 ${ago} (${info.event})`);
357
401
  }
358
402
  }
359
- console.log(' 已在运行;部署/脚本可继续轮询 IPC ready,或直接复用当前实例');
403
+ console.log(` 已在运行;已运行时长: ${formatRunningDuration(Date.now() - first.record.startedAt)};部署/脚本可继续轮询 IPC ready,或直接复用当前实例`);
360
404
  console.log(' 使用 ec restart 重启,或 ec stop 先停止');
361
405
  return;
362
406
  }
@@ -562,10 +606,11 @@ export async function cmdStart(opts = {}) {
562
606
  };
563
607
  setTimeout(checkReady, 1000);
564
608
  }
565
- async function stopPid(pid) {
609
+ async function stopPid(pid, gracefulRequested = false) {
566
610
  console.log(`🛑 Stopping EvolCore (PID: ${pid})...`);
567
- platform.killProcess(pid);
568
- if (await platform.waitForProcessExit(pid, 10_000)) {
611
+ if (!gracefulRequested)
612
+ platform.killProcess(pid);
613
+ if (await platform.waitForProcessExit(pid, gracefulRequested ? 30_000 : 10_000)) {
569
614
  console.log('✓ EvolCore stopped');
570
615
  return true;
571
616
  }
@@ -608,7 +653,10 @@ export async function cmdStop() {
608
653
  if (runtimeOrphans.length > 0) {
609
654
  console.log(`⚠ 检测到未登记的 EvolCore 进程,将一并停止: ${runtimeOrphans.map(orphan => orphan.pid).join(', ')}`);
610
655
  }
611
- const stopped = await Promise.all([...pids].map(pid => stopPid(pid)));
656
+ const gracefulPid = ping && pids.has(ping.pid)
657
+ ? await requestDaemonShutdown(p.socket, 3_000, 'ec stop', pids)
658
+ : null;
659
+ const stopped = await Promise.all([...pids].map(pid => stopPid(pid, pid === gracefulPid)));
612
660
  if (stopped.some(result => !result)) {
613
661
  process.exitCode = 1;
614
662
  return;
@@ -675,7 +723,12 @@ export async function cmdRestart(opts = {}) {
675
723
  if (aliveMains.length > 1) {
676
724
  console.log(`⚠ 检测到 ${aliveMains.length} 个 main 实例,将一并停止: ${aliveMains.map(m => m.record.pid).join(', ')}`);
677
725
  }
678
- const stopped = await Promise.all(aliveMains.map(m => stopPid(m.record.pid)));
726
+ const mainPids = new Set(aliveMains.map(entry => entry.record.pid));
727
+ const currentPing = await probeDaemon(socketPath);
728
+ const gracefulPid = currentPing && mainPids.has(currentPing.pid)
729
+ ? await requestDaemonShutdown(socketPath, 3_000, 'ec restart', mainPids)
730
+ : null;
731
+ const stopped = await Promise.all(aliveMains.map(m => stopPid(m.record.pid, m.record.pid === gracefulPid)));
679
732
  if (stopped.some(result => !result)) {
680
733
  console.error('❌ Restart aborted because an existing EvolCore process could not be stopped');
681
734
  process.exitCode = 1;
@@ -703,7 +756,12 @@ export async function cmdRestart(opts = {}) {
703
756
  const runtimeOrphans = findOrphanProcesses().filter(o => isRuntimeOrphan(o));
704
757
  if (runtimeOrphans.length > 0) {
705
758
  console.log(`⚠ 检测到未登记的 EvolCore 进程,将一并停止: ${runtimeOrphans.map(o => o.pid).join(', ')}`);
706
- const stopped = await Promise.all(runtimeOrphans.map(o => stopPid(o.pid)));
759
+ const orphanPids = new Set(runtimeOrphans.map(orphan => orphan.pid));
760
+ const currentPing = await probeDaemon(socketPath);
761
+ const gracefulPid = currentPing && orphanPids.has(currentPing.pid)
762
+ ? await requestDaemonShutdown(socketPath, 3_000, 'ec restart', orphanPids)
763
+ : null;
764
+ const stopped = await Promise.all(runtimeOrphans.map(o => stopPid(o.pid, o.pid === gracefulPid)));
707
765
  if (stopped.some(result => !result)) {
708
766
  console.error('❌ Restart aborted because an unregistered EvolCore process could not be stopped');
709
767
  process.exitCode = 1;
@@ -1359,7 +1417,7 @@ export function cmdLogs(args) {
1359
1417
  // ==================== Watch ====================
1360
1418
  let watchUseColor = false;
1361
1419
  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?)"/;
1420
+ 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
1421
  function parseWatchTs(s) {
1364
1422
  const m = s.match(/^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(Z)?$/);
1365
1423
  if (!m)
@@ -1371,10 +1429,21 @@ function parseWatchTs(s) {
1371
1429
  return new Date(+y, +mo - 1, +d, +h, +mi, +se, msNum).getTime();
1372
1430
  }
1373
1431
  function extractWatchTs(line) {
1374
- const m = line.match(WATCH_BRACKET_TS_RE) || line.match(WATCH_JSON_TS_RE);
1375
- if (!m)
1432
+ const bracket = line.match(WATCH_BRACKET_TS_RE);
1433
+ if (bracket) {
1434
+ const t = parseWatchTs(bracket[1]);
1435
+ return isNaN(t) ? null : t;
1436
+ }
1437
+ const json = line.match(WATCH_JSON_TS_RE);
1438
+ if (!json)
1376
1439
  return null;
1377
- const t = parseWatchTs(m[1]);
1440
+ if (json[2] !== undefined) {
1441
+ const numeric = Number(json[2]);
1442
+ if (!Number.isFinite(numeric) || numeric <= 0)
1443
+ return null;
1444
+ return numeric < 100_000_000_000 ? numeric * 1000 : numeric;
1445
+ }
1446
+ const t = parseWatchTs(json[1]);
1378
1447
  return isNaN(t) ? null : t;
1379
1448
  }
1380
1449
  function toLocalTimeStr(epoch) {
@@ -1791,8 +1860,7 @@ function cmdWatch(filterTypes) {
1791
1860
  const all = fs.readdirSync(p.logs).filter(f => f.endsWith('.log')).map(f => path.join(p.logs, f));
1792
1861
  return filterLogFiles(all, filterTypes);
1793
1862
  };
1794
- // Strip rotation suffix (e.g., "daemon-20260518-21" → "daemon")
1795
- const shortName = (f) => path.basename(f, '.log').replace(/-\d{8}-\d{2}$/, '');
1863
+ const shortName = shortLogNameLocal;
1796
1864
  // 计算最长文件名用于对齐
1797
1865
  let maxNameLen = 0;
1798
1866
  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,