evolcore 0.0.12 → 0.0.14

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 (116) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/bin/codex-managed-hook.mjs +4 -1
  3. package/bin/ec-safe-output.js +26 -7
  4. package/bin/install-codex-managed-hooks.mjs +201 -0
  5. package/dist/agents/claude-runner.js +105 -11
  6. package/dist/agents/codex-app-server-client.js +176 -7
  7. package/dist/agents/codex-runner.js +248 -84
  8. package/dist/agents/ecagent-runner.js +21 -2
  9. package/dist/agents/gemini-runner.js +9 -4
  10. package/dist/aun/aid/control-aid.js +36 -13
  11. package/dist/aun/aid/store.js +13 -7
  12. package/dist/aun/group-fs-download.js +56 -0
  13. package/dist/aun/group-identity.js +1 -2
  14. package/dist/aun/msg/group.js +11 -33
  15. package/dist/aun/msg/index.js +1 -2
  16. package/dist/aun/msg/managed-operation.js +176 -55
  17. package/dist/aun/msg/upload.js +16 -1
  18. package/dist/aun/outbox.js +31 -2
  19. package/dist/aun/rpc/client.js +66 -0
  20. package/dist/aun/rpc/index.js +1 -2
  21. package/dist/aun/storage/{upload.js → client.js} +11 -2
  22. package/dist/aun/storage/index.js +1 -2
  23. package/dist/channels/aun.js +514 -150
  24. package/dist/channels/daemon.js +2 -0
  25. package/dist/cli/agent.js +9 -96
  26. package/dist/cli/aun-commands.js +1 -1
  27. package/dist/cli/cli-argv.js +10 -0
  28. package/dist/cli/config.js +9 -17
  29. package/dist/cli/ctl-command.js +1 -1
  30. package/dist/cli/daemon-commands.js +39 -24
  31. package/dist/cli/fs-command.js +65 -19
  32. package/dist/cli/init.js +77 -8
  33. package/dist/cli/queue-command.js +42 -0
  34. package/dist/cli/restart-monitor.js +3 -14
  35. package/dist/cli/task-context.js +172 -0
  36. package/dist/cli/trigger-command.js +7 -0
  37. package/dist/config/access-policy.js +1 -2
  38. package/dist/config/builtin-roles.js +7 -0
  39. package/dist/config/config-manager.js +27 -3
  40. package/dist/config/contact-book-store.js +7 -2
  41. package/dist/config/peer-role-resolver.js +6 -2
  42. package/dist/config/role-migration-startup.js +53 -0
  43. package/dist/config/role-store.js +1 -1
  44. package/dist/core/auth/agent-delegation.js +1 -1
  45. package/dist/core/auth/auth-gateway.js +30 -1
  46. package/dist/core/auth/authorization-audit.js +47 -0
  47. package/dist/core/auth/operation-authorizer.js +1 -0
  48. package/dist/core/auth/operation-catalog.js +10 -1
  49. package/dist/core/bootstrap-messages.js +9 -2
  50. package/dist/core/bootstrap-service.js +8 -1
  51. package/dist/core/command/command-handler.js +201 -51
  52. package/dist/core/command/menu-catalog.js +4 -0
  53. package/dist/core/command/menu-handler.js +85 -18
  54. package/dist/core/command/menu-protocol.js +23 -2
  55. package/dist/core/command/slash-handler.js +58 -10
  56. package/dist/core/event-catalog.js +8 -0
  57. package/dist/core/handoff/runtime.js +12 -1
  58. package/dist/core/handoff/store.js +5 -1
  59. package/dist/core/interaction-router.js +10 -0
  60. package/dist/core/message/im-renderer.js +34 -1
  61. package/dist/core/message/message-bridge.js +68 -13
  62. package/dist/core/message/message-log.js +2 -0
  63. package/dist/core/message/message-queue.js +267 -39
  64. package/dist/core/message/message-utils.js +81 -4
  65. package/dist/core/message/response-engine.js +242 -48
  66. package/dist/core/message/stream-debouncer.js +10 -2
  67. package/dist/core/permission/approval-gateway.js +1 -0
  68. package/dist/core/permission/ec-command-parser.js +649 -18
  69. package/dist/core/permission/index.js +1 -1
  70. package/dist/core/permission/mode.js +15 -0
  71. package/dist/core/permission/readonly-shell-query.js +148 -8
  72. package/dist/core/permission/sandbox-runtime.js +21 -0
  73. package/dist/core/permission/tool-policy.js +266 -56
  74. package/dist/core/protected-paths.js +107 -2
  75. package/dist/core/runtime-lock.js +101 -0
  76. package/dist/core/session/session-manager.js +21 -2
  77. package/dist/core/session/session-mapper.js +20 -1
  78. package/dist/eck/detect.js +18 -1
  79. package/dist/eck/group-rules-sync.js +15 -44
  80. package/dist/index.js +191 -89
  81. package/dist/ipc.js +16 -3
  82. package/dist/paths.js +3 -0
  83. package/dist/response-system/engines/v1/proactive-flow.js +92 -8
  84. package/dist/response-system/modes/single-session/index.js +3 -0
  85. package/dist/trigger/feedback.js +64 -11
  86. package/dist/trigger/history.js +42 -7
  87. package/dist/trigger/manager.js +21 -1
  88. package/dist/trigger/parser.js +14 -2
  89. package/dist/trigger/scheduler.js +1 -0
  90. package/dist/trigger/validation.js +67 -6
  91. package/dist/utils/ecweb-supervisor.js +332 -0
  92. package/dist/utils/error-utils.js +17 -0
  93. package/dist/utils/logger.js +37 -4
  94. package/kits/docs/evolcore/trigger.md +17 -1
  95. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  96. package/kits/schemas/role-config.schema.1.json +1 -0
  97. package/kits/templates/roles/admin.json +7 -0
  98. package/kits/templates/roles/member.json +8 -1
  99. package/kits/templates/roles/visitor.json +2 -0
  100. package/package.json +1 -1
  101. package/dist/aun/msg/payload-type.js +0 -27
  102. package/dist/aun/rpc/caller.js +0 -42
  103. package/dist/aun/rpc/connection.js +0 -25
  104. package/dist/aun/storage/manage.js +0 -10
  105. package/dist/config/access-policy-domain.js +0 -18
  106. package/dist/config/config-batch-get.js +0 -11
  107. package/dist/config/owner-policy.js +0 -4
  108. package/dist/config/role-config-v4-startup.js +0 -32
  109. package/dist/config/role-config-v5-startup.js +0 -27
  110. package/dist/core/auth/trigger-authorization.js +0 -15
  111. package/dist/core/interaction-registration.js +0 -10
  112. package/dist/core/permission/execution-sandbox.js +0 -16
  113. package/dist/core/relation/peer-key.js +0 -1
  114. package/dist/core/session/session-key.js +0 -24
  115. package/dist/eck/baseagent-caps.js +0 -18
  116. package/dist/eck/rules-loader.js +0 -28
@@ -10,10 +10,18 @@ export async function cmdQueue(args) {
10
10
 
11
11
  查询:
12
12
  --agent <aid> 必填,目标 agent(如 mybot.agentid.pub)
13
+ --after <time> 只显示 adapter 接收时间晚于该值的消息(epoch ms 或 ISO)
14
+ --before <time> 只显示 adapter 接收时间早于该值的消息(epoch ms 或 ISO)
13
15
  --showid 显示 messageId
14
16
  --full 显示完整消息内容(不截断)
15
17
  --format json 以 JSON 格式输出
16
18
 
19
+ JSON 时间字段:
20
+ receivedAt adapter 收到消息时间(epoch ms)
21
+ enqueuedAt 实际进入队列时间(epoch ms)
22
+ queueWaitMs 排队等待时长(ms)
23
+ processingStartedAt 开始处理时间(epoch ms,active)
24
+
17
25
  操作:
18
26
  --clear 清空该 agent 的所有待处理消息
19
27
  --cancel <messageId> 取消指定消息
@@ -21,6 +29,7 @@ export async function cmdQueue(args) {
21
29
 
22
30
  示例:
23
31
  ec queue --agent mybot.agentid.pub
32
+ ec queue --agent mybot.agentid.pub --after 2026-08-19T12:00:00+08:00
24
33
  ec queue --agent mybot.agentid.pub --showid
25
34
  ec queue --agent mybot.agentid.pub --clear
26
35
  ec queue --agent mybot.agentid.pub --cancel msg_d4e5f6
@@ -46,6 +55,12 @@ export async function cmdQueue(args) {
46
55
  process.exit(1);
47
56
  }
48
57
  const formatJson = formatValue === 'json';
58
+ const after = parseQueueTime(getArgValue(args, '--after'), '--after');
59
+ const before = parseQueueTime(getArgValue(args, '--before'), '--before');
60
+ if (after !== undefined && before !== undefined && after >= before) {
61
+ console.error('❌ --after 必须早于 --before');
62
+ process.exit(1);
63
+ }
49
64
  const clear = args.includes('--clear');
50
65
  const cancelIdx = args.indexOf('--cancel');
51
66
  const cancelCandidate = cancelIdx >= 0 && cancelIdx + 1 < args.length ? args[cancelIdx + 1] : undefined;
@@ -80,6 +95,8 @@ export async function cmdQueue(args) {
80
95
  action,
81
96
  messageId: cancelMsgId,
82
97
  sessionKey,
98
+ after,
99
+ before,
83
100
  sessionId: managed,
84
101
  delegationToken: process.env[AGENT_DELEGATION_TOKEN_ENV],
85
102
  } : {
@@ -88,6 +105,8 @@ export async function cmdQueue(args) {
88
105
  action,
89
106
  messageId: cancelMsgId,
90
107
  sessionKey,
108
+ after,
109
+ before,
91
110
  });
92
111
  if (!result) {
93
112
  console.error('错误: 无法连接 evolcore 服务');
@@ -118,6 +137,16 @@ export async function cmdQueue(args) {
118
137
  }
119
138
  console.log(renderQueueItemsCli(items, showId, full, agent));
120
139
  }
140
+ function parseQueueTime(value, flag) {
141
+ if (value === undefined)
142
+ return undefined;
143
+ const parsed = /^\d+$/.test(value) ? Number(value) : Date.parse(value);
144
+ if (!Number.isFinite(parsed)) {
145
+ console.error(`❌ ${flag} 必须是 epoch 毫秒或 ISO 时间: ${value}`);
146
+ process.exit(1);
147
+ }
148
+ return parsed;
149
+ }
121
150
  function renderQueueItemsCli(items, showId, full, aid) {
122
151
  const lines = [`${aid} — 队列 (${items.length} 条)`, ''];
123
152
  if (items.length === 0) {
@@ -151,6 +180,12 @@ function renderQueueItemsCli(items, showId, full, aid) {
151
180
  elapsed = sec >= 60 ? `${Math.floor(sec / 60)}m${sec % 60}s` : `${sec}s`;
152
181
  }
153
182
  parts.push(` ${elapsed.padEnd(elapsedColWidth)} `);
183
+ const wait = item.queueWaitMs == null ? '—' : formatDuration(item.queueWaitMs);
184
+ parts.push(` wait:${wait.padEnd(6)} `);
185
+ if (item.receivedAt != null)
186
+ parts.push(` recv:${formatQueueTime(item.receivedAt)} `);
187
+ if (item.enqueuedAt != null)
188
+ parts.push(` enq:${formatQueueTime(item.enqueuedAt)} `);
154
189
  // 内容列
155
190
  const content = full ? item.preview.replace(/\.\.\.$/, '') : item.preview;
156
191
  parts.push(`"${content}"`);
@@ -158,3 +193,10 @@ function renderQueueItemsCli(items, showId, full, aid) {
158
193
  }
159
194
  return lines.join('\n');
160
195
  }
196
+ function formatDuration(ms) {
197
+ const sec = Math.max(0, Math.round(ms / 1000));
198
+ return sec >= 60 ? `${Math.floor(sec / 60)}m${sec % 60}s` : `${sec}s`;
199
+ }
200
+ function formatQueueTime(timestamp) {
201
+ return new Date(timestamp).toISOString();
202
+ }
@@ -14,7 +14,6 @@ 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 { serviceProxyNeedsEcweb, startEcwebIfEnabled } from './daemon-commands.js';
18
17
  const execFileAsync = promisify(execFile);
19
18
  // 清理 Claude Code 环境变量,防止 SDK 认为是嵌套会话
20
19
  function cleanEnv() {
@@ -194,22 +193,12 @@ export async function cmdRestartMonitor() {
194
193
  await notifyDaemonOwners(p, `⚠️ ${WEB_PACKAGE_NAME} 升级失败,使用当前版本继续\n${ecwebUpgrade.error ?? ''}`.trim(), log);
195
194
  break;
196
195
  }
197
- // source=ecweb Service Proxy 只在 daemon 启动后的短窗口内发现实例。
198
- // restart-monitor 直接 spawn daemon,必须在此先启动独立的 ec-web,不能绕过
199
- // cmdStart ECWeb 生命周期逻辑。
200
- const ecwebStartedBeforeDaemon = serviceProxyNeedsEcweb(loadDaemonConfig())
201
- ? await startEcwebIfEnabled(p)
202
- : false;
203
- if (ecwebStartedBeforeDaemon)
204
- log('ECWeb started before daemon');
196
+ // ECWeb is owned by the daemon. Starting it here used to race with the
197
+ // daemon's own launcher and ECWeb's stale-instance cleanup, which could
198
+ // leave ServiceProxy pointed at a dead PID.
205
199
  // 启动并检测 ready signal
206
200
  let started = await spawnAndWaitReady(p, log, READY_TIMEOUT);
207
201
  if (started) {
208
- // 没有 Service Proxy 时可以等 daemon ready 后再启动;有代理时已在上方启动,
209
- // 避免重新拉起造成实例记录和反向隧道短暂失配。
210
- if (!ecwebStartedBeforeDaemon && await startEcwebIfEnabled(p)) {
211
- log('ECWeb started after daemon');
212
- }
213
202
  log('✓ Service restarted successfully');
214
203
  archiveSelfHealLog(p, log);
215
204
  // 发起会话内的“重启成功”通知由新进程自行发送,此处只负责失败/自愈告警。
@@ -1,5 +1,14 @@
1
1
  import { normalizeCausation } from '../core/causation/context.js';
2
+ import { RUNTIME_LOCK_DIR_ENV } from '../core/runtime-lock.js';
3
+ import crypto from 'node:crypto';
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
2
7
  export const TASK_RUNTIME_CONTEXT_ENV = 'EVOLCORE_TASK_RUNTIME_CONTEXT';
8
+ export const SESSION_RUNTIME_DIR_ENV = 'EVOLCORE_SESSION_RUNTIME_DIR';
9
+ export { RUNTIME_LOCK_DIR_ENV };
10
+ const generatedProcessManagedTempDirs = new Set();
11
+ let processTempCleanupRegistered = false;
3
12
  function isPlainObject(value) {
4
13
  return !!value && typeof value === 'object' && !Array.isArray(value);
5
14
  }
@@ -23,10 +32,167 @@ function normalizeTaskRuntimeContext(value) {
23
32
  peerType: optionalString(value.peerType),
24
33
  peerRole: optionalString(value.peerRole),
25
34
  threadId: optionalString(value.threadId),
35
+ sessionRuntimeDir: optionalAbsolutePath(value.sessionRuntimeDir),
36
+ runtimeLockDir: optionalAbsolutePath(value.runtimeLockDir),
26
37
  handoffIds: handoffIds?.length ? handoffIds : undefined,
27
38
  causation: normalizeCausation(value.causation),
28
39
  };
29
40
  }
41
+ function optionalAbsolutePath(value) {
42
+ if (typeof value !== 'string' || !path.isAbsolute(value))
43
+ return undefined;
44
+ return path.resolve(value);
45
+ }
46
+ function isPrivateDirectory(directory) {
47
+ try {
48
+ const resolved = path.resolve(directory);
49
+ const stat = fs.lstatSync(resolved);
50
+ if (!stat.isDirectory()
51
+ || stat.isSymbolicLink()
52
+ || (typeof process.getuid === 'function' && stat.uid !== process.getuid())
53
+ || (stat.mode & 0o077) !== 0)
54
+ return false;
55
+ // A private leaf below a symlinked ancestor can still escape the managed
56
+ // namespace. Walk the existing ancestor chain and reject such paths while
57
+ // allowing the normal public system temp parent (for generated roots).
58
+ let current = resolved;
59
+ while (true) {
60
+ const ancestor = fs.lstatSync(current);
61
+ if (ancestor.isSymbolicLink())
62
+ return false;
63
+ const parent = path.dirname(current);
64
+ if (parent === current)
65
+ break;
66
+ current = parent;
67
+ }
68
+ return true;
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ }
74
+ /**
75
+ * Ensure the daemon has a private process-wide temporary root.
76
+ *
77
+ * Service managers commonly omit TMPDIR. In that case create a private
78
+ * directory below the platform temp root and publish it for all managed
79
+ * runners. An explicitly supplied value is accepted only when it is already
80
+ * a private directory owned by this process. A shared system directory such
81
+ * as `/tmp`, a symlink, or a group/world-readable path is replaced with a
82
+ * fresh private root instead of becoming a capability for managed children.
83
+ */
84
+ export function ensureProcessManagedTempDir() {
85
+ const configured = process.env.TMPDIR?.trim();
86
+ if (configured) {
87
+ if (!path.isAbsolute(configured)) {
88
+ throw new Error('managed TMPDIR is unset or not absolute');
89
+ }
90
+ const resolved = path.resolve(configured);
91
+ if (isPrivateDirectory(resolved))
92
+ return resolved;
93
+ }
94
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'evolcore-managed-tmp-'));
95
+ try {
96
+ fs.chmodSync(directory, 0o700);
97
+ if (!isPrivateDirectory(directory)) {
98
+ throw new Error(`managed temp path is not a private directory: ${directory}`);
99
+ }
100
+ process.env.TMPDIR = directory;
101
+ generatedProcessManagedTempDirs.add(directory);
102
+ if (!processTempCleanupRegistered) {
103
+ processTempCleanupRegistered = true;
104
+ process.once('exit', () => {
105
+ for (const candidate of generatedProcessManagedTempDirs) {
106
+ if (!path.basename(candidate).startsWith('evolcore-managed-tmp-'))
107
+ continue;
108
+ try {
109
+ if (isPrivateDirectory(candidate))
110
+ fs.rmSync(candidate, { recursive: true, force: true });
111
+ }
112
+ catch {
113
+ // Best-effort cleanup; the directory is private and uniquely named.
114
+ }
115
+ }
116
+ });
117
+ }
118
+ return directory;
119
+ }
120
+ catch (error) {
121
+ try {
122
+ fs.rmSync(directory, { recursive: true, force: true });
123
+ }
124
+ catch { }
125
+ throw error;
126
+ }
127
+ }
128
+ function isRunnerOwnedSessionRuntimeDir(directory) {
129
+ const resolved = path.resolve(directory);
130
+ const base = path.basename(resolved);
131
+ const parent = path.dirname(resolved);
132
+ // Codex allocates a private, direct child named with a 24-character digest
133
+ // below its private evolcore-codex-* parent. The runner has already checked
134
+ // canonical containment before returning this path to the response engine.
135
+ const isCodexRuntime = /^[a-f0-9]{24}$/.test(base)
136
+ && /^evolcore-codex-(?:\d+|user)$/.test(path.basename(parent));
137
+ return isCodexRuntime && isPrivateDirectory(parent) && isPrivateDirectory(resolved);
138
+ }
139
+ /** Whether a task-provided runtime directory stays inside the process-managed
140
+ * TMPDIR and is safe to use for transient writes. */
141
+ export function isManagedSessionRuntimeDir(directory) {
142
+ const tmpDir = process.env.TMPDIR?.trim();
143
+ if (!directory || !path.isAbsolute(directory) || !tmpDir || !path.isAbsolute(tmpDir))
144
+ return false;
145
+ const root = path.resolve(tmpDir);
146
+ const resolved = path.resolve(directory);
147
+ const relative = path.relative(root, resolved);
148
+ if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
149
+ return false;
150
+ return isPrivateDirectory(root) && isPrivateDirectory(resolved);
151
+ }
152
+ /**
153
+ * Create a private runtime directory below the process-provided TMPDIR.
154
+ * There is deliberately no os.tmpdir() fallback: managed sessions must not
155
+ * silently escape the session-owned temporary workspace.
156
+ */
157
+ export function ensureSessionRuntimeDir(sessionId) {
158
+ const base = process.env.TMPDIR?.trim();
159
+ if (!base || !path.isAbsolute(base) || !sessionId)
160
+ return undefined;
161
+ const root = path.resolve(base);
162
+ const digest = crypto.createHash('sha256').update(sessionId).digest('hex').slice(0, 24);
163
+ const directory = path.join(root, `evolcore-runtime-${digest}`);
164
+ try {
165
+ fs.mkdirSync(root, { recursive: true, mode: 0o700 });
166
+ if (!isPrivateDirectory(root))
167
+ return undefined;
168
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
169
+ fs.mkdirSync(path.join(directory, 'locks'), { recursive: true, mode: 0o700 });
170
+ return isManagedSessionRuntimeDir(directory) ? directory : undefined;
171
+ }
172
+ catch {
173
+ return undefined;
174
+ }
175
+ }
176
+ /** Shared by tasks in one daemon runtime, while remaining outside each
177
+ * session's history/files area. The caller must still pass this path through
178
+ * the task environment before a sandboxed child can use it. */
179
+ export function ensureRuntimeLockDir(sessionRuntimeDir) {
180
+ if (!sessionRuntimeDir || !path.isAbsolute(sessionRuntimeDir))
181
+ return undefined;
182
+ if (!isManagedSessionRuntimeDir(sessionRuntimeDir) && !isRunnerOwnedSessionRuntimeDir(sessionRuntimeDir))
183
+ return undefined;
184
+ const parent = path.dirname(path.resolve(sessionRuntimeDir));
185
+ if (!isPrivateDirectory(parent))
186
+ return undefined;
187
+ const directory = path.join(parent, 'evolcore-locks');
188
+ try {
189
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
190
+ return isPrivateDirectory(directory) ? directory : undefined;
191
+ }
192
+ catch {
193
+ return undefined;
194
+ }
195
+ }
30
196
  export function runtimeRefMessageIdForMsgSend(args) {
31
197
  const runtime = args.runtime;
32
198
  if (!runtime)
@@ -76,5 +242,11 @@ export function buildTaskRuntimeEnv(ctx) {
76
242
  return {
77
243
  EVOLCORE_SESSION_ID: ctx.sessionId ?? '',
78
244
  [TASK_RUNTIME_CONTEXT_ENV]: JSON.stringify(clean),
245
+ // Every managed child receives the session directory as TMPDIR. This keeps
246
+ // ordinary temporary files and runtime-only fallbacks out of shared agent
247
+ // or process directories, including for non-Codex runners.
248
+ ...(clean.sessionRuntimeDir ? { TMPDIR: clean.sessionRuntimeDir } : {}),
249
+ ...(clean.sessionRuntimeDir ? { [SESSION_RUNTIME_DIR_ENV]: clean.sessionRuntimeDir } : {}),
250
+ ...(clean.runtimeLockDir ? { [RUNTIME_LOCK_DIR_ENV]: clean.runtimeLockDir } : {}),
79
251
  };
80
252
  }
@@ -113,6 +113,7 @@ Create 参数模式支持:
113
113
  --permission <readonly|auto|request|bypass>(省略则继承当前身份配置)
114
114
  --tz <时区> (仅 cron)
115
115
  --target-channel <channelKey> --target-channel-id <ID> (AUN 可简写为 aun)
116
+ --target-chat-type <private|group> (AUN 目标必填;channelId 同时作为群聊 ID)
116
117
  --target-session <main|thread> [--target-thread-id <threadId>]
117
118
  --trigger-thread <by-run|by-trigger> (兼容旧值 per-run)
118
119
  --name <名称>
@@ -263,6 +264,11 @@ async function createTrigger(args, json) {
263
264
  if (!parsed.targetChannel || !parsed.targetChannelId) {
264
265
  throw new Error('CLI flag 模式需要显式指定 --target-channel <channelKey> --target-channel-id <id>');
265
266
  }
267
+ const targetIsAun = parsed.targetChannel === 'aun' || parsed.targetChannel.startsWith('aun#');
268
+ if (targetIsAun && !parsed.targetChatType) {
269
+ throw new Error('AUN 目标必须显式指定 --target-chat-type private 或 group');
270
+ }
271
+ const targetChatType = parsed.targetChatType;
266
272
  const now = Date.now();
267
273
  if (parsed.targetSession === 'thread' && !parsed.targetThreadId) {
268
274
  throw new Error('--target-session thread 需要 --target-thread-id');
@@ -300,6 +306,7 @@ async function createTrigger(args, json) {
300
306
  channelKey: parsed.targetChannel,
301
307
  channelId: parsed.targetChannelId,
302
308
  session: parsed.targetSession,
309
+ ...(targetChatType ? { chatType: targetChatType } : {}),
303
310
  ...(parsed.targetSession === 'thread' ? { threadId: parsed.targetThreadId } : {}),
304
311
  },
305
312
  },
@@ -1,6 +1,5 @@
1
1
  import crypto from 'crypto';
2
- import { ConfigError, ConfigTarget, mutateConfig, read, validateConfig, } from './config-manager.js';
3
- import { validAgentOwnerIds, validateAccessPolicyDomain, } from './access-policy-domain.js';
2
+ import { ConfigError, ConfigTarget, mutateConfig, read, validateConfig, validAgentOwnerIds, validateAccessPolicyDomain, } from './config-manager.js';
4
3
  export class AccessPolicyError extends Error {
5
4
  code;
6
5
  data;
@@ -44,6 +44,13 @@ export function getManagementCommandPermissions(role) {
44
44
  'role.policy.write': { allow: true, dangerous: true, scopes: ['agent'], constraints: { requireAgentAdmin: true } },
45
45
  'ec.aid.show': { allow: true, dangerous: true, scopes: ['agent'], constraints: { targetCurrentAgentOnly: true, requireAgentAdmin: true } },
46
46
  'ec.aid.agentmd.put': { allow: true, dangerous: true, scopes: ['agent'], constraints: { targetCurrentAgentOnly: true, requireAgentAdmin: true } },
47
+ 'ec.fs.mkdir': { allow: true, permissionMode: 'auto', scopes: ['relation'], constraints: { groupOnly: true, currentRelationOnly: true, targetCurrentAgentOnly: true } },
48
+ 'ec.fs.upload': { allow: true, permissionMode: 'auto', scopes: ['relation'], constraints: { groupOnly: true, currentRelationOnly: true, targetCurrentAgentOnly: true } },
49
+ 'ec.fs.copy': { allow: true, permissionMode: 'auto', scopes: ['relation'], constraints: { groupOnly: true, currentRelationOnly: true, targetCurrentAgentOnly: true } },
50
+ 'ec.fs.move': { allow: true, permissionMode: 'auto', scopes: ['relation'], constraints: { groupOnly: true, currentRelationOnly: true, targetCurrentAgentOnly: true } },
51
+ 'ec.fs.remove': { allow: true, permissionMode: 'auto', scopes: ['relation'], constraints: { groupOnly: true, currentRelationOnly: true, targetCurrentAgentOnly: true } },
52
+ 'ec.fs.getfacl': { allow: true, scopes: ['relation'], constraints: { groupOnly: true, currentRelationOnly: true, targetCurrentAgentOnly: true } },
53
+ 'ec.fs.setfacl': { allow: true, permissionMode: 'auto', scopes: ['relation'], constraints: { groupOnly: true, currentRelationOnly: true, targetCurrentAgentOnly: true } },
47
54
  '*': { allow: true },
48
55
  // Reload is valid from an Agent's own channel and the daemon control plane.
49
56
  // The control-channel gate still requires a daemon owner for cross-Agent reloads.
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import fs from 'fs';
14
14
  import path from 'path';
15
+ import { ensureManagedLockParent, resolveManagedLockPath } from '../core/runtime-lock.js';
15
16
  import crypto from 'crypto';
16
17
  import { resolvePaths, agentConfig as agentConfigPath, agentContactConfig, agentRelationConfig, agentDir, agentRelationsDir, agentRolesIndex, agentRoleConfig, } from '../paths.js';
17
18
  import { atomicReadJson, atomicWriteJson } from '../utils/atomic-write.js';
@@ -28,7 +29,27 @@ import { clearRoleStoreCache, isRoleName } from './role-store.js';
28
29
  import { writeRoleDefinition, writeRoleRegistry } from './role-service.js';
29
30
  import { mutateContactBookTransactionSync } from './contact-book-store.js';
30
31
  import { DAEMON_SCHEMA_NAME } from '../product.js';
31
- import { validateAccessPolicyDomain } from './access-policy-domain.js';
32
+ export function shouldFailFastForMissingOwners(env = process.env) {
33
+ const value = (env.EVOLCORE_REQUIRE_OWNERS || '').trim().toLowerCase();
34
+ return value === '1' || value === 'true' || value === 'yes';
35
+ }
36
+ export function validAgentOwnerIds(agent) {
37
+ return [...new Set((agent?.owners ?? [])
38
+ .map(value => String(value || '').trim())
39
+ .filter(isValidAid))];
40
+ }
41
+ export function validateAccessPolicyDomain(selfAid, agent) {
42
+ const access = agent.access;
43
+ if (!access)
44
+ return undefined;
45
+ if (access.policyMode !== 'open' && validAgentOwnerIds(agent).length === 0) {
46
+ return {
47
+ code: 'restricted_mode_without_owner',
48
+ message: `Agent ${selfAid} cannot use ${access.policyMode} without an Owner`,
49
+ };
50
+ }
51
+ return undefined;
52
+ }
32
53
  export var ConfigTarget;
33
54
  (function (ConfigTarget) {
34
55
  ConfigTarget["Process"] = "process";
@@ -773,12 +794,15 @@ function sortConfigValue(value) {
773
794
  .map(([key, nested]) => [key, sortConfigValue(nested)]));
774
795
  }
775
796
  function acquireConfigMutationLock(file) {
776
- const lockDir = `${file}.mutation.lock`;
797
+ const lockDir = resolveManagedLockPath('config', file, `${file}.mutation.lock`);
777
798
  const ownerFile = path.join(lockDir, 'owner.json');
778
799
  const token = crypto.randomBytes(16).toString('hex');
779
800
  const candidate = `${lockDir}.candidate-${process.pid}-${token}`;
780
801
  const candidateOwner = path.join(candidate, 'owner.json');
781
- fs.mkdirSync(path.dirname(file), { recursive: true });
802
+ if (lockDir === `${file}.mutation.lock`)
803
+ fs.mkdirSync(path.dirname(file), { recursive: true });
804
+ else
805
+ ensureManagedLockParent(lockDir);
782
806
  fs.mkdirSync(candidate);
783
807
  fs.writeFileSync(candidateOwner, `${JSON.stringify({ pid: process.pid, token })}\n`, { mode: 0o600 });
784
808
  let acquired = false;
@@ -6,6 +6,7 @@ import { isExplicitGroupId } from '../aun/group-identity.js';
6
6
  import { agentConfig, agentContactConfig, agentDir, resolvePaths } from '../paths.js';
7
7
  import { atomicReadJson, atomicWrite, atomicWriteJson } from '../utils/atomic-write.js';
8
8
  import { fileCache } from '../core/daemon-file-cache.js';
9
+ import { ensureManagedLockParent, resolveManagedLockPath } from '../core/runtime-lock.js';
9
10
  import { ConfigTarget, notifyConfigWrite, validateConfig } from './config-manager.js';
10
11
  import { parseContactAlias } from './contact-alias.js';
11
12
  import { clearRoleStoreCache } from './role-store.js';
@@ -426,12 +427,16 @@ function readContactBookFromDisk(selfAid) {
426
427
  }
427
428
  }
428
429
  function acquireAgentLock(selfAid) {
429
- const lockDir = path.join(agentDir(selfAid), LOCK_NAME);
430
+ const legacyLockDir = path.join(agentDir(selfAid), LOCK_NAME);
431
+ const lockDir = resolveManagedLockPath('contact', selfAid, legacyLockDir);
430
432
  const ownerFile = path.join(lockDir, 'owner.json');
431
433
  const lockToken = crypto.randomBytes(16).toString('hex');
432
434
  const candidateDir = `${lockDir}.candidate-${process.pid}-${lockToken}`;
433
435
  const candidateOwnerFile = path.join(candidateDir, 'owner.json');
434
- fs.mkdirSync(path.dirname(lockDir), { recursive: true });
436
+ if (lockDir === legacyLockDir)
437
+ fs.mkdirSync(path.dirname(lockDir), { recursive: true });
438
+ else
439
+ ensureManagedLockParent(lockDir);
435
440
  fs.mkdirSync(candidateDir);
436
441
  fs.writeFileSync(candidateOwnerFile, `${JSON.stringify({
437
442
  pid: process.pid,
@@ -123,8 +123,12 @@ export function resolvePeerRoleDetail(ctx) {
123
123
  const winner = selectHighestRank(candidates, ctx.selfAid);
124
124
  if (winner)
125
125
  return resultFor(winner.role, winner.source, actor, ctx.selfAid);
126
- return roleRegistryExists(ctx.selfAid) && roleExists('visitor', ctx.selfAid)
127
- ? resultFor('visitor', 'builtin-fallback', actor, ctx.selfAid)
126
+ // Group members get the normal interactive member policy when no explicit
127
+ // group/actor/default assignment exists. Private conversations retain the
128
+ // read-only visitor fallback.
129
+ const fallbackRole = ctx.chatType === 'group' ? 'member' : 'visitor';
130
+ return roleRegistryExists(ctx.selfAid) && roleExists(fallbackRole, ctx.selfAid)
131
+ ? resultFor(fallbackRole, 'builtin-fallback', actor, ctx.selfAid)
128
132
  : resultFor(null, 'none', actor, ctx.selfAid);
129
133
  }
130
134
  export function roleToSessionIdentity(role) {
@@ -0,0 +1,53 @@
1
+ import path from 'path';
2
+ import { pathToFileURL } from 'url';
3
+ import { getPackageRoot, resolvePaths } from '../paths.js';
4
+ import { assertRoleConfigV4Ready, assertRoleConfigV5Ready, ConfigError } from './config-manager.js';
5
+ /** Apply the one-off v4 role config migration before daemon startup. */
6
+ export async function ensureRoleConfigV4OnStartup() {
7
+ try {
8
+ assertRoleConfigV4Ready();
9
+ return null;
10
+ }
11
+ catch (error) {
12
+ if (!(error instanceof ConfigError) || error.code !== 'ROLE_CONFIG_MIGRATION_REQUIRED')
13
+ throw error;
14
+ }
15
+ const home = resolvePaths().root;
16
+ const script = path.join(getPackageRoot(), 'kits', 'migrations', 'migrate-role-config-v4.mjs');
17
+ try {
18
+ const migration = await import(pathToFileURL(script).href);
19
+ const result = migration.runMigration({ home, apply: true });
20
+ assertRoleConfigV4Ready();
21
+ return result;
22
+ }
23
+ catch (error) {
24
+ const detail = error instanceof Error ? error.message : String(error);
25
+ throw new ConfigError('ROLE_CONFIG_MIGRATION_FAILED', `Automatic role config v4 migration failed: ${detail}\n` +
26
+ `Fix the reported config or run manually:\n` +
27
+ `node kits/migrations/migrate-role-config-v4.mjs --apply --home ${JSON.stringify(home)}`, { cause: error });
28
+ }
29
+ }
30
+ /** Apply the standalone v5 role registry migration before daemon startup. */
31
+ export async function ensureRoleConfigV5OnStartup() {
32
+ try {
33
+ assertRoleConfigV5Ready();
34
+ return null;
35
+ }
36
+ catch (error) {
37
+ if (!(error instanceof ConfigError) || error.code !== 'ROLE_CONFIG_V5_MIGRATION_REQUIRED')
38
+ throw error;
39
+ }
40
+ const home = resolvePaths().root;
41
+ const script = path.join(getPackageRoot(), 'kits', 'migrations', 'migrate-role-config-v5.mjs');
42
+ try {
43
+ const migration = await import(pathToFileURL(script).href);
44
+ const result = migration.runMigration({ home, apply: true });
45
+ assertRoleConfigV5Ready();
46
+ return result;
47
+ }
48
+ catch (error) {
49
+ const detail = error instanceof Error ? error.message : String(error);
50
+ throw new ConfigError('ROLE_CONFIG_V5_MIGRATION_FAILED', `Automatic standalone role migration failed: ${detail}\n` +
51
+ `node kits/migrations/migrate-role-config-v5.mjs --apply --home ${JSON.stringify(home)}`, { cause: error });
52
+ }
53
+ }
@@ -113,7 +113,7 @@ function readRegistry(selfAid, diagnostics) {
113
113
  code: 'INVALID_ROLE_ASSIGNMENT',
114
114
  roleId: selected,
115
115
  file: path.relative(resolvePaths().root, file),
116
- message: `defaultRoles.${scene} references invalid or unregistered role ${selected}; using visitor fallback`,
116
+ message: `defaultRoles.${scene} references invalid or unregistered role ${selected}; using ${scene === 'group' ? 'member' : 'visitor'} fallback`,
117
117
  });
118
118
  defaultRoles[scene] = null;
119
119
  }
@@ -2,7 +2,7 @@ import crypto from 'crypto';
2
2
  import { authorizeOperation, buildAuthSubject } from './auth-gateway.js';
3
3
  import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
4
4
  export const AGENT_DELEGATION_TOKEN_ENV = 'EVOLCORE_DELEGATION_TOKEN';
5
- export const AGENT_DELEGATION_COMMAND_TTL_MS = 60_000;
5
+ export const AGENT_DELEGATION_COMMAND_TTL_MS = 5 * 60_000;
6
6
  export class AgentDelegationRegistry {
7
7
  grantsByHash = new Map();
8
8
  activeHashBySession = new Map();
@@ -4,6 +4,22 @@ import { formatPeerKey } from '../relation/peer-identity.js';
4
4
  import { authorizeCommand, authorizeResolvedConfigCommand } from './operation-authorizer.js';
5
5
  import { auditCommandAuthorization } from './authorization-audit.js';
6
6
  import { hasTrustedPrincipal } from './authenticated-actor.js';
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
+ 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);
22
+ }
7
23
  export function buildAuthSubject(input) {
8
24
  const chatType = input.chatType === 'group' ? 'group' : 'private';
9
25
  const actorId = input.actorId;
@@ -27,6 +43,8 @@ export function buildAuthSubject(input) {
27
43
  return {
28
44
  selfAid: input.selfAid,
29
45
  actorId,
46
+ requestId: input.requestId,
47
+ sessionId: input.sessionId,
30
48
  channel: input.channel,
31
49
  channelType: input.channelType,
32
50
  channelId: input.channelId,
@@ -41,6 +59,8 @@ export function buildAuthSubject(input) {
41
59
  allowAccess: isDaemonOwner || (suppliedRole
42
60
  ? checkRoleAccess(suppliedRole, input.selfAid)
43
61
  : roleDetail.allowAccess && checkRoleAccess(role, input.selfAid)),
62
+ permissionMode: input.permissionMode
63
+ ?? resolveRuntimePermissionMode({ selfAid: input.selfAid, role }).effectiveValue,
44
64
  };
45
65
  }
46
66
  function isProcessOwner(input) {
@@ -144,6 +164,16 @@ function resolveRoleDetail(input) {
144
164
  function auditDecision(params, decision) {
145
165
  auditCommandAuthorization({
146
166
  ts: Date.now(),
167
+ ...params.auditMetadata,
168
+ correlationId: params.auditMetadata?.correlationId ?? params.auditMetadata?.requestId ?? params.subject.requestId,
169
+ requestId: params.auditMetadata?.requestId ?? params.subject.requestId,
170
+ sessionId: params.auditMetadata?.sessionId ?? params.subject.sessionId,
171
+ agentAid: params.auditMetadata?.agentAid ?? params.subject.selfAid,
172
+ permissionMode: params.auditMetadata?.permissionMode
173
+ ?? (decision.allow ? decision.permissionMode ?? params.subject.permissionMode : params.subject.permissionMode),
174
+ toolName: params.auditMetadata?.toolName,
175
+ policyCode: params.auditMetadata?.policyCode ?? (decision.allow ? undefined : decision.code),
176
+ decisionSource: params.auditMetadata?.decisionSource ?? (decision.allow ? 'command' : 'policy'),
147
177
  source: params.source,
148
178
  operation: params.intent.operation,
149
179
  scope: params.intent.scope,
@@ -161,6 +191,5 @@ function auditDecision(params, decision) {
161
191
  reason: decision.allow ? undefined : decision.reason,
162
192
  matchedRule: decision.matchedRule,
163
193
  argsSummary: params.intent.args,
164
- ...params.auditMetadata,
165
194
  });
166
195
  }