evolcore 0.0.16 → 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 (65) hide show
  1. package/CHANGELOG.md +46 -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 +132 -14
  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/group.js +3 -1
  11. package/dist/aun/msg/p2p.js +23 -9
  12. package/dist/channels/aun.js +159 -21
  13. package/dist/cli/agent-command.js +67 -6
  14. package/dist/cli/agent.js +26 -0
  15. package/dist/cli/command-log.js +23 -4
  16. package/dist/cli/daemon-commands.js +53 -12
  17. package/dist/cli/init.js +21 -5
  18. package/dist/cli/restart-monitor.js +13 -6
  19. package/dist/cli/task-context.js +46 -1
  20. package/dist/cli/watch-logs.js +2 -2
  21. package/dist/config/builtin-roles.js +5 -1
  22. package/dist/config/role-ranks.js +4 -0
  23. package/dist/core/audit/event-key.js +29 -0
  24. package/dist/core/audit/log-integrity.js +13 -3
  25. package/dist/core/auth/auth-gateway.js +14 -18
  26. package/dist/core/auth/authorization-audit.js +110 -3
  27. package/dist/core/auth/authorization-denial.js +17 -0
  28. package/dist/core/auth/operation-authorizer.js +143 -18
  29. package/dist/core/auth/operation-catalog.js +21 -5
  30. package/dist/core/bootstrap-messages.js +11 -6
  31. package/dist/core/bootstrap-service.js +26 -4
  32. package/dist/core/causation/aun-association.js +7 -4
  33. package/dist/core/command/agent-control.js +25 -16
  34. package/dist/core/command/command-handler.js +50 -4
  35. package/dist/core/command/group-menu.js +1 -1
  36. package/dist/core/command/menu-catalog.js +32 -7
  37. package/dist/core/command/menu-handler.js +59 -23
  38. package/dist/core/command/menu-protocol.js +196 -0
  39. package/dist/core/command/slash-gate.js +14 -5
  40. package/dist/core/command/slash-handler.js +81 -99
  41. package/dist/core/event-catalog.js +18 -0
  42. package/dist/core/message/message-bridge.js +72 -9
  43. package/dist/core/message/pause-controller.js +53 -0
  44. package/dist/core/message/response-engine.js +98 -11
  45. package/dist/core/permission/sandbox-runtime.js +79 -13
  46. package/dist/core/permission/tool-policy.js +1 -1
  47. package/dist/index.js +357 -48
  48. package/dist/ipc.js +75 -4
  49. package/dist/utils/atomic-write.js +45 -11
  50. package/dist/utils/error-utils.js +38 -0
  51. package/dist/utils/logger.js +27 -0
  52. package/dist/utils/windows-autostart.js +740 -83
  53. package/ecagent/dist/harness/agent-harness.d.ts +1 -1
  54. package/ecagent/dist/harness/agent-harness.js +6 -4
  55. package/kits/docs/evolcore/config.md +1 -1
  56. package/kits/docs/evolcore/group-rules.md +2 -1
  57. package/kits/docs/identity/ROLE_DETAIL.md +3 -1
  58. package/kits/eck_manifest.json +25 -16
  59. package/kits/rules/01-overview.md +5 -5
  60. package/kits/rules/03-identity.md +1 -1
  61. package/kits/rules/04-relation.md +4 -4
  62. package/kits/rules/05-venue.md +5 -5
  63. package/kits/templates/bootstrap-welcome.md +3 -1
  64. package/kits/templates/system-fragments/bootstrap.md +17 -9
  65. 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)) {
@@ -23,6 +23,7 @@ function normalizeTaskRuntimeContext(value) {
23
23
  taskId: optionalString(value.taskId),
24
24
  sessionId: optionalString(value.sessionId),
25
25
  messageId: optionalString(value.messageId),
26
+ baseagent: optionalString(value.baseagent),
26
27
  channel: optionalString(value.channel),
27
28
  channelId: optionalString(value.channelId),
28
29
  chatType: optionalString(value.chatType),
@@ -38,6 +39,29 @@ function normalizeTaskRuntimeContext(value) {
38
39
  causation: normalizeCausation(value.causation),
39
40
  };
40
41
  }
42
+ /**
43
+ * Codex's shell carrier can leave one JSON-style escape layer in a message
44
+ * argument (for example, the two characters `\\` and `n`). Decode only that
45
+ * layer, and only for a task that is explicitly running the Codex backend.
46
+ * Actual line breaks and all non-Codex CLI input remain unchanged.
47
+ */
48
+ export function decodeCodexMessageText(text, runtime) {
49
+ if (runtime?.baseagent?.trim().toLowerCase() !== 'codex')
50
+ return text;
51
+ return text.replace(/\\([nrt])/g, (_match, escape) => {
52
+ if (escape === 'n')
53
+ return '\n';
54
+ if (escape === 'r')
55
+ return '\r';
56
+ return '\t';
57
+ });
58
+ }
59
+ /** Apply the Codex-only normalization to a text message payload. */
60
+ export function normalizeCodexTextPayload(payload, runtime) {
61
+ if (payload.type !== 'text' || typeof payload.text !== 'string')
62
+ return;
63
+ payload.text = decodeCodexMessageText(payload.text, runtime);
64
+ }
41
65
  function optionalAbsolutePath(value) {
42
66
  if (typeof value !== 'string' || !path.isAbsolute(value))
43
67
  return undefined;
@@ -75,6 +99,27 @@ function isPrivateDirectory(directory) {
75
99
  return false;
76
100
  }
77
101
  }
102
+ /**
103
+ * Resolve the platform temp root before creating a managed directory. macOS
104
+ * exposes its per-user temp directory through `/var`, which is a symlink to
105
+ * `/private/var`; walking that lexical path would otherwise reject a freshly
106
+ * created directory even though its real path is private. When an inherited
107
+ * TMPDIR was rejected, use a fixed system fallback instead of following that
108
+ * untrusted path back into the fallback.
109
+ */
110
+ function managedTempCreationRoot(configured) {
111
+ const candidate = configured
112
+ ? process.platform === 'win32'
113
+ ? path.join(process.env.SystemRoot || process.env.WINDIR || 'C:\\Windows', 'Temp')
114
+ : '/tmp'
115
+ : os.tmpdir();
116
+ try {
117
+ return fs.realpathSync.native(candidate);
118
+ }
119
+ catch {
120
+ return path.resolve(candidate);
121
+ }
122
+ }
78
123
  /**
79
124
  * Ensure the daemon has a private process-wide temporary root.
80
125
  *
@@ -95,7 +140,7 @@ export function ensureProcessManagedTempDir() {
95
140
  if (isPrivateDirectory(resolved))
96
141
  return resolved;
97
142
  }
98
- const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'evolcore-managed-tmp-'));
143
+ const directory = fs.mkdtempSync(path.join(managedTempCreationRoot(configured), 'evolcore-managed-tmp-'));
99
144
  try {
100
145
  fs.chmodSync(directory, 0o700);
101
146
  if (!isPrivateDirectory(directory)) {
@@ -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,