evolcore 0.0.20 → 0.0.21

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 (123) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +58 -9
  3. package/dist/agents/baseagent.js +10 -6
  4. package/dist/agents/claude-runner.js +379 -108
  5. package/dist/agents/codex-app-server-client.js +10 -2
  6. package/dist/agents/codex-runner.js +402 -135
  7. package/dist/agents/ecagent-runner.js +171 -61
  8. package/dist/agents/gemini-runner.js +130 -30
  9. package/dist/agents/request-identity.js +25 -0
  10. package/dist/agents/runner-types.js +19 -0
  11. package/dist/aun/aid/agentmd.js +59 -2
  12. package/dist/aun/aid/identity.js +4 -1
  13. package/dist/aun/aid/index.js +1 -1
  14. package/dist/aun/msg/group.js +72 -6
  15. package/dist/aun/msg/history.js +213 -36
  16. package/dist/aun/msg/managed-operation.js +58 -9
  17. package/dist/aun/msg/p2p.js +5 -0
  18. package/dist/aun/outbox.js +182 -80
  19. package/dist/aun/service-proxy.js +43 -25
  20. package/dist/channels/aun.js +409 -88
  21. package/dist/channels/daemon.js +6 -1
  22. package/dist/cli/agent-command.js +4 -3
  23. package/dist/cli/agent.js +66 -56
  24. package/dist/cli/aun-commands.js +177 -42
  25. package/dist/cli/command-log.js +10 -11
  26. package/dist/cli/contact.js +1 -0
  27. package/dist/cli/daemon-commands.js +69 -115
  28. package/dist/cli/init.js +27 -15
  29. package/dist/cli/task-context.js +46 -0
  30. package/dist/cli/trigger-command.js +1 -1
  31. package/dist/cli/watch-logs.js +10 -3
  32. package/dist/config/builtin-roles.js +1 -0
  33. package/dist/config/config-field-policy.js +16 -5
  34. package/dist/config/config-manager.js +135 -17
  35. package/dist/config/contact-operation-service.js +32 -1
  36. package/dist/config/contact-request-service.js +44 -0
  37. package/dist/config/daemon-services.js +186 -0
  38. package/dist/config/gateway-config.js +20 -9
  39. package/dist/config/role-service.js +54 -3
  40. package/dist/config/schema-migration.js +550 -0
  41. package/dist/config-store.js +151 -9
  42. package/dist/core/agent-application-service.js +279 -0
  43. package/dist/core/audit/log-integrity.js +102 -0
  44. package/dist/core/auth/agent-delegation.js +31 -1
  45. package/dist/core/auth/auth-gateway.js +33 -4
  46. package/dist/core/auth/authorization-audit.js +150 -2
  47. package/dist/core/auth/operation-authorizer.js +41 -1
  48. package/dist/core/auth/operation-catalog.js +9 -1
  49. package/dist/core/bootstrap-service.js +6 -2
  50. package/dist/core/causation/aun-association.js +7 -4
  51. package/dist/core/command/agent-control.js +56 -16
  52. package/dist/core/command/command-handler.js +290 -44
  53. package/dist/core/command/connect-menu.js +3 -4
  54. package/dist/core/command/group-menu.js +5 -7
  55. package/dist/core/command/menu-handler.js +279 -80
  56. package/dist/core/command/role-menu.js +21 -11
  57. package/dist/core/command/slash-gate.js +85 -18
  58. package/dist/core/command/slash-handler.js +350 -32
  59. package/dist/core/event-catalog.js +5 -0
  60. package/dist/core/evolagent.js +4 -0
  61. package/dist/core/handoff/dispatcher.js +4 -0
  62. package/dist/core/handoff/runtime.js +10 -0
  63. package/dist/core/handoff/store.js +32 -9
  64. package/dist/core/inference/text-inference.js +7 -15
  65. package/dist/core/message/im-renderer.js +83 -84
  66. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  67. package/dist/core/message/message-bridge.js +124 -10
  68. package/dist/core/message/message-log.js +14 -7
  69. package/dist/core/message/message-queue.js +206 -16
  70. package/dist/core/message/message-utils.js +12 -5
  71. package/dist/core/message/response-engine.js +486 -68
  72. package/dist/core/message/send-receipt.js +1 -0
  73. package/dist/core/message/stream-debouncer.js +9 -2
  74. package/dist/core/model/model-catalog.js +23 -15
  75. package/dist/core/model/model-diagnostics.js +28 -10
  76. package/dist/core/permission/approval-gateway.js +180 -6
  77. package/dist/core/permission/ec-command-parser.js +410 -54
  78. package/dist/core/permission/mode.js +18 -3
  79. package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
  80. package/dist/core/permission/readonly-shell-query.js +263 -9
  81. package/dist/core/permission/sandbox-runtime.js +159 -1
  82. package/dist/core/permission/tool-policy.js +575 -21
  83. package/dist/core/session/session-fs-store.js +154 -5
  84. package/dist/core/session/session-manager.js +299 -30
  85. package/dist/core/session/session-renew.js +19 -12
  86. package/dist/core/session/session-turn-coordinator.js +11 -4
  87. package/dist/eck/kit-renderer.js +1 -1
  88. package/dist/index.js +253 -46
  89. package/dist/ipc.js +374 -24
  90. package/dist/paths.js +64 -7
  91. package/dist/response-system/context-builder.js +1 -7
  92. package/dist/trigger/anomaly-store.js +1 -0
  93. package/dist/trigger/feedback.js +56 -5
  94. package/dist/trigger/history.js +79 -4
  95. package/dist/trigger/legacy-session-history.js +2 -2
  96. package/dist/trigger/parser.js +3 -2
  97. package/dist/trigger/validation.js +6 -1
  98. package/dist/utils/atomic-write.js +27 -0
  99. package/dist/utils/ecweb-utils.js +16 -2
  100. package/dist/utils/error-utils.js +4 -1
  101. package/dist/utils/logger.js +21 -2
  102. package/dist/utils/process-tree-stats.js +24 -4
  103. package/dist/utils/process-tree-worker.js +31 -0
  104. package/dist/utils/project-path.js +1 -2
  105. package/kits/docs/INDEX.md +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -1
  107. package/kits/docs/evolcore/contact.md +7 -1
  108. package/kits/docs/evolcore/msg.md +16 -0
  109. package/kits/schemas/_meta.json +4 -2
  110. package/kits/schemas/agent-config.schema.11.json +13 -0
  111. package/kits/schemas/daemon.schema.5.json +0 -1
  112. package/kits/schemas/daemon.schema.6.json +131 -0
  113. package/kits/schemas/defaults.schema.5.json +15 -3
  114. package/kits/schemas/migrations/README.md +3 -1
  115. package/kits/schemas/relation-config.schema.8.json +13 -0
  116. package/kits/schemas/role-config.schema.1.json +1 -2
  117. package/kits/schemas/single-session.schema.3.json +32 -0
  118. package/kits/templates/roles/admin.json +1 -0
  119. package/kits/templates/roles/member.json +1 -0
  120. package/kits/templates/roles/visitor.json +1 -0
  121. package/package.json +6 -3
  122. package/skills/eclink/SKILL.md +2 -0
  123. package/dist/config/aun-gateway-config.js +0 -2
@@ -1,29 +1,16 @@
1
1
  import path from 'path';
2
2
  import { isValidAid } from '../aid/identity.js';
3
3
  import { isTransientProtocolMessage, messageLogPath } from '../../core/message/message-log.js';
4
- import { chatDirPath, readAllJsonlLines, readJsonFile, readLastJsonlLine } from '../../core/session/session-fs-store.js';
4
+ import { chatDirPath, readAllJsonlLines, readJsonFile, readLastJsonlLine, scanChatDirs, scanMetaFiles } from '../../core/session/session-fs-store.js';
5
+ import { tryParseChannelKey } from '../../core/channel-loader.js';
5
6
  import { resolvePaths } from '../../paths.js';
6
7
  const DEFAULT_LIMIT = 50;
7
8
  export const MAX_MSG_HISTORY_LIMIT = 500;
8
9
  const SESSION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/;
9
- const CHANNELS = new Set(['aun', 'feishu', 'wechat', 'dingtalk', 'qqbot', 'wecom']);
10
10
  export function msgHistory(args, options = {}) {
11
11
  if (!isValidAid(args.from)) {
12
12
  return error('INVALID_SELF_AID', `无效 self AID: ${args.from}`);
13
13
  }
14
- const channel = args.channel ?? 'aun';
15
- if (!CHANNELS.has(channel)) {
16
- return error('INVALID_CHANNEL', `不支持的 channel: ${String(args.channel)}`);
17
- }
18
- if (channel === 'aun' && !isValidAid(args.target)) {
19
- return error('INVALID_TARGET_AID', `无效 target AID: ${args.target}`);
20
- }
21
- if (channel !== 'aun' && !isSafeChannelPeerId(args.target)) {
22
- return error('INVALID_TARGET_ID', `无效 ${channel} 对端 ID`);
23
- }
24
- if (args.channelName !== undefined && !isSafeChannelName(args.channelName)) {
25
- return error('INVALID_CHANNEL_NAME', '无效 channel 实例名');
26
- }
27
14
  const limit = args.limit ?? DEFAULT_LIMIT;
28
15
  if (!Number.isInteger(limit) || limit < 1 || limit > MAX_MSG_HISTORY_LIMIT) {
29
16
  return error('INVALID_LIMIT', `limit 必须是 1-${MAX_MSG_HISTORY_LIMIT} 的整数`);
@@ -42,36 +29,167 @@ export function msgHistory(args, options = {}) {
42
29
  return error('INVALID_TIME_RANGE', 'after 必须早于 before');
43
30
  }
44
31
  const sessionsDir = options.sessionsDir ?? resolvePaths().sessionsDir;
45
- const chatDir = chatDirPath(sessionsDir, channel, args.target, args.from, channel === 'aun' && !args.channelName ? undefined : `${channel}#${args.from}#${args.channelName ?? 'main'}`);
46
- const resolvedSession = resolveSessionFilter(chatDir, args.from, args.target, channel, args.session ?? 'latest');
32
+ if (args.channelKey !== undefined || args.peerId !== undefined) {
33
+ return msgHistoryByChannelPeer(args, { sessionsDir, limit, direction });
34
+ }
35
+ return msgHistoryByLegacyRoute(args, { sessionsDir, limit, direction });
36
+ }
37
+ function msgHistoryByLegacyRoute(args, options) {
38
+ const channel = args.channel ?? 'aun';
39
+ const target = args.target;
40
+ if (!isSafeChannelType(channel)) {
41
+ return error('INVALID_CHANNEL', `无效 channel: ${String(args.channel)}`);
42
+ }
43
+ if (!target)
44
+ return error('INVALID_TARGET_ID', '缺少 target ID');
45
+ if (channel === 'aun' && !isValidAid(target)) {
46
+ return error('INVALID_TARGET_AID', `无效 target AID: ${target}`);
47
+ }
48
+ if (channel !== 'aun' && !isSafeChannelPeerId(target)) {
49
+ return error('INVALID_TARGET_ID', `无效 ${channel} 对端 ID`);
50
+ }
51
+ if (args.channelName !== undefined && !isSafeChannelName(args.channelName)) {
52
+ return error('INVALID_CHANNEL_NAME', '无效 channel 实例名');
53
+ }
54
+ const chatDir = chatDirPath(options.sessionsDir, channel, target, args.from, channel === 'aun' && !args.channelName ? undefined : `${channel}#${args.from}#${args.channelName ?? 'main'}`);
55
+ const resolvedSession = resolveLegacySessionFilter(chatDir, args.from, target, channel, args.session ?? 'latest');
47
56
  if (!resolvedSession.ok)
48
57
  return resolvedSession;
49
- let entries;
58
+ return readHistoryScopes({
59
+ args,
60
+ scopes: [{
61
+ chatDir,
62
+ ...(resolvedSession.sessionId ? { sessionIds: [resolvedSession.sessionId] } : {}),
63
+ }],
64
+ target,
65
+ limit: options.limit,
66
+ direction: options.direction,
67
+ });
68
+ }
69
+ function msgHistoryByChannelPeer(args, options) {
70
+ if (!args.channelKey || !args.peerId) {
71
+ return error('INVALID_CHANNEL_SELECTOR', '--channel-key 与 --peer-id 必须同时提供');
72
+ }
73
+ if (args.target !== undefined || args.channel !== undefined || args.channelName !== undefined) {
74
+ return error('INVALID_CHANNEL_SELECTOR', '使用 --channel-key/--peer-id 时不能同时指定 target、--channel 或 --channel-name');
75
+ }
76
+ if (!isSafeChannelPeerId(args.peerId)) {
77
+ return error('INVALID_PEER_ID', '无效 peer ID');
78
+ }
79
+ const channel = tryParseChannelKey(args.channelKey);
80
+ if (!channel || !isSafeChannelType(channel.type) || !isSafeChannelName(channel.name)) {
81
+ return error('INVALID_CHANNEL_KEY', 'channelKey 必须是完整且安全的 <type>#<selfAID>#<instance>');
82
+ }
83
+ if (channel.type === 'aun' || channel.type === 'daemon') {
84
+ return error('INVALID_CHANNEL_SELECTOR', `${channel.type} history 不支持 --channel-key/--peer-id 定位`);
85
+ }
86
+ if (channel.selfAID !== args.from) {
87
+ return error('SESSION_SCOPE_MISMATCH', 'channelKey 中的 self AID 必须与 from 一致');
88
+ }
89
+ const requested = args.session ?? 'latest';
90
+ if (requested !== 'latest' && requested !== 'all' && !SESSION_ID_RE.test(requested)) {
91
+ return error('INVALID_SESSION_ID', `无效 session ID: ${requested}`);
92
+ }
93
+ const scopesByChatDir = new Map();
94
+ let latest;
95
+ let requestedSessionWasOutOfScope = false;
96
+ for (const entry of scanChatDirs(options.sessionsDir)) {
97
+ if (entry.selfAID !== args.from
98
+ || entry.channelType !== channel.type
99
+ || entry.channelKey !== args.channelKey) {
100
+ continue;
101
+ }
102
+ const candidates = readPersistedSessions(entry.dirPath);
103
+ for (const session of candidates) {
104
+ if (!session.id || !SESSION_ID_RE.test(session.id))
105
+ continue;
106
+ const belongsToPeer = nonAunSessionBelongsToPeer(session, args.from, args.channelKey, channel.type, args.peerId);
107
+ if (requested === 'all') {
108
+ if (!belongsToPeer)
109
+ continue;
110
+ addScopeSession(scopesByChatDir, entry.dirPath, session.id);
111
+ continue;
112
+ }
113
+ if (requested === 'latest') {
114
+ if (!belongsToPeer)
115
+ continue;
116
+ if (!latest || compareSessionRecency(session, entry.dirPath, latest.session, latest.chatDir) > 0) {
117
+ latest = { chatDir: entry.dirPath, session };
118
+ }
119
+ continue;
120
+ }
121
+ if (session.id !== requested)
122
+ continue;
123
+ if (!belongsToPeer) {
124
+ requestedSessionWasOutOfScope = true;
125
+ continue;
126
+ }
127
+ addScopeSession(scopesByChatDir, entry.dirPath, session.id);
128
+ }
129
+ }
130
+ if (requested === 'latest') {
131
+ if (latest)
132
+ addScopeSession(scopesByChatDir, latest.chatDir, latest.session.id);
133
+ else
134
+ return error('LATEST_SESSION_NOT_FOUND', '该 channelKey/peerId 对暂无最新会话');
135
+ }
136
+ const scopes = [...scopesByChatDir.entries()]
137
+ .sort(([left], [right]) => left.localeCompare(right))
138
+ .map(([chatDir, sessionIds]) => ({ chatDir, sessionIds: [...sessionIds].sort() }));
139
+ if (requested !== 'latest' && requested !== 'all' && scopes.length === 0) {
140
+ if (requestedSessionWasOutOfScope) {
141
+ return error('SESSION_SCOPE_MISMATCH', '指定会话不属于该 channelKey/peerId 对');
142
+ }
143
+ return error('SESSION_NOT_FOUND', `在该 channelKey/peerId 对中找不到会话: ${requested}`);
144
+ }
145
+ return readHistoryScopes({
146
+ args,
147
+ scopes,
148
+ target: args.peerId,
149
+ channelKey: args.channelKey,
150
+ peerId: args.peerId,
151
+ limit: options.limit,
152
+ direction: options.direction,
153
+ });
154
+ }
155
+ function readHistoryScopes(input) {
156
+ const entries = [];
50
157
  try {
51
- entries = readAllJsonlLines(messageLogPath(chatDir));
158
+ input.scopes.forEach((scope, scopeIndex) => {
159
+ const sessionIds = scope.sessionIds ? new Set(scope.sessionIds) : undefined;
160
+ for (const entry of readAllJsonlLines(messageLogPath(scope.chatDir))) {
161
+ entries.push({ entry, sessionIds, scope: scopeIndex });
162
+ }
163
+ });
52
164
  }
53
165
  catch (cause) {
54
166
  return error('HISTORY_READ_FAILED', cause instanceof Error ? cause.message : String(cause));
55
167
  }
56
168
  const messages = entries
57
- .filter(entry => isMessageLogEntry(entry))
58
- .filter(entry => !isTransientProtocolMessage(entry))
59
- .filter(entry => resolvedSession.sessionId === undefined || entry.sessionId === resolvedSession.sessionId)
60
- .filter(entry => direction === 'all' || entry.dir === direction)
61
- .filter(entry => args.after === undefined || entry.ts > args.after)
62
- .filter(entry => args.before === undefined || entry.ts < args.before)
63
- .sort((left, right) => left.ts - right.ts)
64
- .slice(-limit);
169
+ .filter(({ entry }) => isMessageLogEntry(entry))
170
+ .filter(({ entry }) => !isTransientProtocolMessage(entry))
171
+ .filter(({ entry, sessionIds }) => sessionIds === undefined
172
+ || (typeof entry.sessionId === 'string' && sessionIds.has(entry.sessionId)))
173
+ .filter(({ entry }) => input.direction === 'all' || entry.dir === input.direction)
174
+ .filter(({ entry }) => input.args.after === undefined || entry.ts > input.args.after)
175
+ .filter(({ entry }) => input.args.before === undefined || entry.ts < input.args.before)
176
+ .sort((left, right) => left.entry.ts - right.entry.ts || left.scope - right.scope)
177
+ .slice(-input.limit)
178
+ .map(({ entry }) => entry);
179
+ const sessionIds = [...new Set(input.scopes.flatMap(scope => scope.sessionIds ?? []))];
65
180
  return {
66
181
  ok: true,
67
- self_aid: args.from,
68
- target_aid: args.target,
69
- session_id: resolvedSession.sessionId,
182
+ self_aid: input.args.from,
183
+ target_aid: input.target,
184
+ ...(input.channelKey ? { channel_key: input.channelKey } : {}),
185
+ ...(input.peerId ? { peer_id: input.peerId } : {}),
186
+ ...(sessionIds.length === 1 ? { session_id: sessionIds[0] } : {}),
187
+ ...(sessionIds.length > 1 ? { session_ids: sessionIds } : {}),
70
188
  count: messages.length,
71
189
  messages,
72
190
  };
73
191
  }
74
- function resolveSessionFilter(chatDir, selfAid, targetAid, channel, requested) {
192
+ function resolveLegacySessionFilter(chatDir, selfAid, targetId, channel, requested) {
75
193
  if (requested === 'all')
76
194
  return { ok: true };
77
195
  if (requested === 'latest') {
@@ -79,7 +197,7 @@ function resolveSessionFilter(chatDir, selfAid, targetAid, channel, requested) {
79
197
  if (!active?.id) {
80
198
  return error('LATEST_SESSION_NOT_FOUND', '该 AID 对暂无最新会话');
81
199
  }
82
- if (!sessionBelongsToPair(active, selfAid, targetAid, channel)) {
200
+ if (!legacySessionBelongsToRoute(active, selfAid, targetId, channel)) {
83
201
  return error('SESSION_SCOPE_MISMATCH', 'active.json 不属于指定 AID 对');
84
202
  }
85
203
  return { ok: true, sessionId: active.id };
@@ -89,7 +207,7 @@ function resolveSessionFilter(chatDir, selfAid, targetAid, channel, requested) {
89
207
  }
90
208
  const active = readJsonFile(path.join(chatDir, 'active.json'));
91
209
  if (active?.id === requested) {
92
- if (!sessionBelongsToPair(active, selfAid, targetAid, channel)) {
210
+ if (!legacySessionBelongsToRoute(active, selfAid, targetId, channel)) {
93
211
  return error('SESSION_SCOPE_MISMATCH', '指定会话不属于该 AID 对');
94
212
  }
95
213
  return { ok: true, sessionId: requested };
@@ -101,23 +219,82 @@ function resolveSessionFilter(chatDir, selfAid, targetAid, channel, requested) {
101
219
  const session = readLastJsonlLine(sessionPath);
102
220
  if (!session || session.id !== requested)
103
221
  continue;
104
- if (!sessionBelongsToPair(session, selfAid, targetAid, channel)) {
222
+ if (!legacySessionBelongsToRoute(session, selfAid, targetId, channel)) {
105
223
  return error('SESSION_SCOPE_MISMATCH', '指定会话不属于该 AID 对');
106
224
  }
107
225
  return { ok: true, sessionId: requested };
108
226
  }
109
227
  return error('SESSION_NOT_FOUND', `在该 AID 对中找不到会话: ${requested}`);
110
228
  }
111
- function sessionBelongsToPair(session, selfAid, targetAid, channel) {
229
+ function legacySessionBelongsToRoute(session, selfAid, targetId, channel) {
112
230
  if (session.channelType && session.channelType !== channel)
113
231
  return false;
114
- if (session.channelId && session.channelId !== targetAid)
232
+ if (session.channelId && session.channelId !== targetId)
115
233
  return false;
116
234
  const storedSelfAid = session.selfAID ?? session.selfId;
117
235
  if (storedSelfAid && storedSelfAid !== selfAid)
118
236
  return false;
119
237
  return true;
120
238
  }
239
+ function nonAunSessionBelongsToPeer(session, selfAid, channelKey, channelType, peerId) {
240
+ if (session.channelType !== channelType)
241
+ return false;
242
+ const storedSelfAid = session.selfAID ?? session.selfId;
243
+ if (storedSelfAid !== selfAid)
244
+ return false;
245
+ // The canonical chat directory is the authoritative channel-instance
246
+ // binding. Older metadata may retain a pre-migration `channel` key (for
247
+ // example `...#feishu` after the directory moved to `...#main`); only an
248
+ // explicitly persisted metadata.channelKey can contradict the directory.
249
+ if (session.metadata?.channelKey !== undefined && session.metadata.channelKey !== channelKey)
250
+ return false;
251
+ return session.metadata?.peerId === peerId;
252
+ }
253
+ /**
254
+ * `active.json` is only a cache. Session JSONL files are the authoritative
255
+ * history index, including archived and topic sessions, so selector reads
256
+ * inspect all three locations before deciding which message IDs are in scope.
257
+ */
258
+ function readPersistedSessions(chatDir) {
259
+ const byId = new Map();
260
+ const add = (session) => {
261
+ if (!session?.id || !SESSION_ID_RE.test(session.id))
262
+ return;
263
+ const current = byId.get(session.id);
264
+ if (!current || compareSessionRecency(session, chatDir, current, chatDir) > 0) {
265
+ byId.set(session.id, session);
266
+ }
267
+ };
268
+ add(readJsonFile(path.join(chatDir, 'active.json')));
269
+ for (const metadataDir of [chatDir, path.join(chatDir, '_threads')]) {
270
+ for (const fileName of scanMetaFiles(metadataDir)) {
271
+ add(readLastJsonlLine(path.join(metadataDir, fileName)));
272
+ }
273
+ }
274
+ return [...byId.values()];
275
+ }
276
+ function addScopeSession(scopes, chatDir, sessionId) {
277
+ const sessionIds = scopes.get(chatDir) ?? new Set();
278
+ sessionIds.add(sessionId);
279
+ scopes.set(chatDir, sessionIds);
280
+ }
281
+ function compareSessionRecency(left, leftChatDir, right, rightChatDir) {
282
+ const leftTime = Number.isFinite(left.updatedAt) ? left.updatedAt : left.createdAt ?? 0;
283
+ const rightTime = Number.isFinite(right.updatedAt) ? right.updatedAt : right.createdAt ?? 0;
284
+ if (leftTime !== rightTime)
285
+ return leftTime - rightTime;
286
+ const leftId = left.id ?? '';
287
+ const rightId = right.id ?? '';
288
+ if (leftId !== rightId)
289
+ return leftId.localeCompare(rightId);
290
+ return leftChatDir.localeCompare(rightChatDir);
291
+ }
292
+ function isSafeChannelType(value) {
293
+ return typeof value === 'string'
294
+ && value.trim().length > 0
295
+ && value.length <= 128
296
+ && !/[\u0000-\u001f\u007f#\/\\]/.test(value);
297
+ }
121
298
  function isSafeChannelPeerId(value) {
122
299
  return typeof value === 'string'
123
300
  && value.trim().length > 0
@@ -5,6 +5,7 @@ import { msgOnline } from './p2p.js';
5
5
  import { resolveRoot } from '../../paths.js';
6
6
  import { isExplicitGroupId } from '../group-identity.js';
7
7
  import { isValidAid } from '../aid/validation.js';
8
+ import { tryParseChannelKey } from '../../core/channel-loader.js';
8
9
  import { downloadGroupFsBytes } from '../group-fs-download.js';
9
10
  import { groupBan, groupBanlist, groupCreate, groupDissolve, groupInfo, groupInvite, groupJoin, groupKick, groupLeave, groupList, groupMembers, groupOnline, groupPull, groupAck, groupResume, groupRules, groupRulesFileGet, groupRulesFilePublish, groupRulesFileSet, groupSetRole, groupSuspend, groupTransferOwner, groupUnban, groupUpdate, groupUpdateRules, } from './group.js';
10
11
  /** Rules uploaded by a managed task are carried as bounded UTF-8 content. */
@@ -415,7 +416,7 @@ export class ManagedMsgOperationError extends Error {
415
416
  }
416
417
  }
417
418
  const HISTORY_VALUE_FLAGS = new Set([
418
- '--session', '--limit', '--before', '--after', '--direction', '--format',
419
+ '--session', '--limit', '--before', '--after', '--direction', '--format', '--channel-key', '--peer-id',
419
420
  ]);
420
421
  function failMsg(code, reason) {
421
422
  return { ok: false, code, reason };
@@ -431,7 +432,8 @@ function parseTime(value) {
431
432
  const timestamp = Date.parse(trimmed);
432
433
  return Number.isFinite(timestamp) ? timestamp : undefined;
433
434
  }
434
- /** Parse the daemon-owned, private-relation msg read grammar. */
435
+ /** Parse the daemon-owned msg read grammar. AUN keeps positional peer AIDs;
436
+ * non-AUN history is selected by its canonical channel key and native peer ID. */
435
437
  export function resolveManagedMsgOperation(argv) {
436
438
  const raw = argv[0] === 'msg' ? argv.slice(1) : argv;
437
439
  const kind = raw[0];
@@ -439,12 +441,12 @@ export function resolveManagedMsgOperation(argv) {
439
441
  return failMsg('NOT_ALLOWED', `unsupported managed msg command: ${kind || '<missing>'}`);
440
442
  }
441
443
  const from = String(raw[1] || '').replace(/^@/, '');
442
- const target = String(raw[2] || '').replace(/^@/, '');
443
444
  if (!isValidAid(from))
444
445
  return failMsg('INVALID_AID', `invalid from AID: ${from || '<missing>'}`);
445
- if (!isValidAid(target))
446
- return failMsg('INVALID_AID', `invalid target AID: ${target || '<missing>'}`);
447
446
  if (kind === 'online') {
447
+ const target = String(raw[2] || '').replace(/^@/, '');
448
+ if (!isValidAid(target))
449
+ return failMsg('INVALID_AID', `invalid target AID: ${target || '<missing>'}`);
448
450
  const allowed = new Set(['--format']);
449
451
  let format;
450
452
  for (let index = 3; index < raw.length; index++) {
@@ -470,9 +472,14 @@ export function resolveManagedMsgOperation(argv) {
470
472
  },
471
473
  };
472
474
  }
475
+ const positionals = [];
473
476
  const values = new Map();
474
- for (let index = 3; index < raw.length; index++) {
477
+ for (let index = 2; index < raw.length; index++) {
475
478
  const flag = raw[index];
479
+ if (!flag.startsWith('--')) {
480
+ positionals.push(flag);
481
+ continue;
482
+ }
476
483
  if (flag === '--aun-path' || flag === '--app' || flag === '--channel' || flag === '--channel-name') {
477
484
  return failMsg('NOT_ALLOWED', `${flag} is not allowed for managed msg history`);
478
485
  }
@@ -485,6 +492,29 @@ export function resolveManagedMsgOperation(argv) {
485
492
  return failMsg('INVALID_ARGUMENT', `${flag} requires a value`);
486
493
  values.set(flag, value);
487
494
  }
495
+ if (positionals.length > 1)
496
+ return failMsg('INVALID_ARGUMENT', 'managed msg history accepts at most one positional target');
497
+ const channelKey = values.get('--channel-key');
498
+ const peerId = values.get('--peer-id');
499
+ if ((channelKey === undefined) !== (peerId === undefined)) {
500
+ return failMsg('INVALID_ARGUMENT', '--channel-key and --peer-id must be provided together');
501
+ }
502
+ if (channelKey !== undefined) {
503
+ if (positionals.length !== 0) {
504
+ return failMsg('INVALID_ARGUMENT', '--channel-key/--peer-id cannot be combined with a positional target');
505
+ }
506
+ const parsedChannelKey = tryParseChannelKey(channelKey);
507
+ if (!parsedChannelKey || parsedChannelKey.selfAID !== from || parsedChannelKey.type === 'aun' || parsedChannelKey.type === 'daemon') {
508
+ return failMsg('INVALID_ARGUMENT', '--channel-key must be a non-AUN channel key belonging to from AID');
509
+ }
510
+ if (!peerId?.trim())
511
+ return failMsg('INVALID_ARGUMENT', '--peer-id must be non-empty');
512
+ }
513
+ else {
514
+ const target = String(positionals[0] || '').replace(/^@/, '');
515
+ if (!isValidAid(target))
516
+ return failMsg('INVALID_AID', `invalid target AID: ${target || '<missing>'}`);
517
+ }
488
518
  const format = values.get('--format');
489
519
  if (format !== undefined && format !== 'text' && format !== 'json') {
490
520
  return failMsg('INVALID_ARGUMENT', '--format only supports text|json');
@@ -509,14 +539,31 @@ export function resolveManagedMsgOperation(argv) {
509
539
  if (before !== undefined && after !== undefined && after >= before) {
510
540
  return failMsg('INVALID_ARGUMENT', '--after must be earlier than --before');
511
541
  }
542
+ const target = channelKey === undefined ? String(positionals[0] || '').replace(/^@/, '') : undefined;
543
+ const canonicalArgv = ['msg', kind, from];
544
+ if (target !== undefined)
545
+ canonicalArgv.push(target);
546
+ else
547
+ canonicalArgv.push('--channel-key', channelKey, '--peer-id', peerId);
548
+ if (values.has('--session'))
549
+ canonicalArgv.push('--session', values.get('--session'));
550
+ if (values.has('--limit'))
551
+ canonicalArgv.push('--limit', values.get('--limit'));
552
+ if (values.has('--before'))
553
+ canonicalArgv.push('--before', values.get('--before'));
554
+ if (values.has('--after'))
555
+ canonicalArgv.push('--after', values.get('--after'));
556
+ if (values.has('--direction'))
557
+ canonicalArgv.push('--direction', values.get('--direction'));
512
558
  return {
513
559
  ok: true,
514
560
  command: {
515
561
  kind,
516
562
  operationId: 'ec.msg.history',
517
- canonicalArgv: ['msg', kind, from, target],
563
+ canonicalArgv,
518
564
  from,
519
- target,
565
+ ...(target !== undefined ? { target } : {}),
566
+ ...(channelKey !== undefined ? { channelKey, peerId } : {}),
520
567
  session: values.get('--session'),
521
568
  limit,
522
569
  before,
@@ -531,7 +578,9 @@ export async function executeManagedMsgOperation(command) {
531
578
  ? msgHistory({
532
579
  from: command.from,
533
580
  target: command.target,
534
- channel: 'aun',
581
+ ...(command.channelKey
582
+ ? { channelKey: command.channelKey, peerId: command.peerId }
583
+ : { target: command.target, channel: 'aun' }),
535
584
  session: command.session,
536
585
  limit: command.limit,
537
586
  before: command.before,
@@ -214,6 +214,11 @@ async function appendMsgSendOutboundLog(args, result, opts) {
214
214
  }
215
215
  export async function msgSend(args) {
216
216
  const runtimeContext = await readBestTaskRuntimeContext();
217
+ // Contact admission is a daemon-owned policy for managed Agent tasks. A
218
+ // non-managed CLI caller may use any valid AUN sender identity and must not
219
+ // require an unrelated local Agent config (for example codex.agentid.cn).
220
+ // Managed sends are forced through tryDaemonMsgSend(), where the daemon
221
+ // checks the authoritative Contact Book before opening the AUN route.
217
222
  let conn;
218
223
  try {
219
224
  let payload;