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.
- package/CHANGELOG.md +32 -0
- package/bin/codex-managed-hook.mjs +16 -7
- package/bin/install-codex-managed-hooks.mjs +4 -2
- package/dist/agents/claude-runner.js +113 -24
- package/dist/agents/codex-app-server-client.js +6 -1
- package/dist/agents/codex-runner.js +21 -6
- package/dist/agents/ecagent-runner.js +39 -10
- package/dist/agents/gemini-runner.js +90 -19
- package/dist/aun/aid/store.js +36 -0
- package/dist/aun/msg/p2p.js +20 -8
- package/dist/channels/aun.js +159 -21
- package/dist/cli/agent-command.js +67 -6
- package/dist/cli/agent.js +26 -0
- package/dist/cli/command-log.js +23 -4
- package/dist/cli/daemon-commands.js +53 -12
- package/dist/cli/init.js +21 -5
- package/dist/cli/restart-monitor.js +13 -6
- package/dist/cli/watch-logs.js +2 -2
- package/dist/config/builtin-roles.js +5 -1
- package/dist/config/role-ranks.js +4 -0
- package/dist/core/audit/event-key.js +29 -0
- package/dist/core/audit/log-integrity.js +13 -3
- package/dist/core/auth/auth-gateway.js +14 -18
- package/dist/core/auth/authorization-audit.js +110 -3
- package/dist/core/auth/authorization-denial.js +17 -0
- package/dist/core/auth/operation-authorizer.js +143 -18
- package/dist/core/auth/operation-catalog.js +21 -5
- package/dist/core/bootstrap-messages.js +11 -6
- package/dist/core/bootstrap-service.js +26 -4
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/command/agent-control.js +25 -16
- package/dist/core/command/command-handler.js +50 -4
- package/dist/core/command/group-menu.js +1 -1
- package/dist/core/command/menu-catalog.js +32 -7
- package/dist/core/command/menu-handler.js +59 -23
- package/dist/core/command/menu-protocol.js +196 -0
- package/dist/core/command/slash-gate.js +14 -5
- package/dist/core/command/slash-handler.js +81 -99
- package/dist/core/event-catalog.js +18 -0
- package/dist/core/message/message-bridge.js +72 -9
- package/dist/core/message/pause-controller.js +53 -0
- package/dist/core/message/response-engine.js +97 -11
- package/dist/core/permission/sandbox-runtime.js +79 -13
- package/dist/core/permission/tool-policy.js +1 -1
- package/dist/index.js +357 -48
- package/dist/ipc.js +75 -4
- package/dist/utils/atomic-write.js +45 -11
- package/dist/utils/logger.js +27 -0
- package/dist/utils/windows-autostart.js +740 -83
- package/ecagent/dist/harness/agent-harness.d.ts +1 -1
- package/ecagent/dist/harness/agent-harness.js +6 -4
- package/kits/docs/evolcore/config.md +1 -1
- package/kits/docs/evolcore/group-rules.md +2 -1
- package/kits/docs/identity/ROLE_DETAIL.md +3 -1
- package/kits/eck_manifest.json +25 -16
- package/kits/rules/01-overview.md +5 -5
- package/kits/rules/03-identity.md +1 -1
- package/kits/rules/04-relation.md +4 -4
- package/kits/rules/05-venue.md +5 -5
- package/kits/templates/bootstrap-welcome.md +3 -1
- package/kits/templates/system-fragments/bootstrap.md +17 -9
- 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
|
-
|
|
568
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1375
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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)) {
|
package/dist/cli/watch-logs.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
/**
|
|
2
|
+
/** 去掉轮转后缀(按小时、按日及旧版带连字符日期)。入参可为文件名或绝对路径。 */
|
|
3
3
|
export function shortLogName(file) {
|
|
4
4
|
return path.basename(file, '.log')
|
|
5
|
-
.replace(/-\d{8}
|
|
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
|
-
|
|
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
|
|
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 ??
|
|
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
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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
|
+
}
|