evolcore 0.0.12 → 0.0.13

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 (104) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/ec-safe-output.js +26 -7
  3. package/dist/agents/claude-runner.js +53 -7
  4. package/dist/agents/codex-app-server-client.js +53 -5
  5. package/dist/agents/codex-runner.js +104 -59
  6. package/dist/agents/ecagent-runner.js +4 -1
  7. package/dist/aun/aid/control-aid.js +36 -13
  8. package/dist/aun/aid/store.js +13 -7
  9. package/dist/aun/group-fs-download.js +56 -0
  10. package/dist/aun/group-identity.js +1 -2
  11. package/dist/aun/msg/group.js +11 -33
  12. package/dist/aun/msg/index.js +1 -2
  13. package/dist/aun/msg/managed-operation.js +116 -55
  14. package/dist/aun/msg/upload.js +16 -1
  15. package/dist/aun/outbox.js +31 -2
  16. package/dist/aun/rpc/client.js +66 -0
  17. package/dist/aun/rpc/index.js +1 -2
  18. package/dist/aun/storage/{upload.js → client.js} +11 -2
  19. package/dist/aun/storage/index.js +1 -2
  20. package/dist/channels/aun.js +371 -136
  21. package/dist/cli/agent.js +9 -96
  22. package/dist/cli/cli-argv.js +10 -0
  23. package/dist/cli/config.js +9 -17
  24. package/dist/cli/ctl-command.js +1 -1
  25. package/dist/cli/daemon-commands.js +39 -24
  26. package/dist/cli/fs-command.js +19 -10
  27. package/dist/cli/init.js +77 -8
  28. package/dist/cli/queue-command.js +42 -0
  29. package/dist/cli/restart-monitor.js +3 -14
  30. package/dist/cli/trigger-command.js +7 -0
  31. package/dist/config/access-policy.js +1 -2
  32. package/dist/config/builtin-roles.js +5 -0
  33. package/dist/config/config-manager.js +21 -1
  34. package/dist/config/peer-role-resolver.js +6 -2
  35. package/dist/config/role-migration-startup.js +53 -0
  36. package/dist/config/role-store.js +1 -1
  37. package/dist/core/auth/agent-delegation.js +1 -1
  38. package/dist/core/auth/auth-gateway.js +29 -1
  39. package/dist/core/auth/authorization-audit.js +15 -0
  40. package/dist/core/auth/operation-authorizer.js +1 -0
  41. package/dist/core/auth/operation-catalog.js +9 -0
  42. package/dist/core/bootstrap-messages.js +9 -2
  43. package/dist/core/bootstrap-service.js +1 -0
  44. package/dist/core/command/command-handler.js +198 -51
  45. package/dist/core/command/menu-catalog.js +4 -0
  46. package/dist/core/command/menu-handler.js +85 -18
  47. package/dist/core/command/menu-protocol.js +23 -2
  48. package/dist/core/command/slash-handler.js +58 -10
  49. package/dist/core/event-catalog.js +6 -0
  50. package/dist/core/handoff/runtime.js +12 -1
  51. package/dist/core/handoff/store.js +5 -1
  52. package/dist/core/interaction-router.js +10 -0
  53. package/dist/core/message/im-renderer.js +19 -0
  54. package/dist/core/message/message-bridge.js +63 -11
  55. package/dist/core/message/message-log.js +2 -0
  56. package/dist/core/message/message-queue.js +267 -39
  57. package/dist/core/message/message-utils.js +81 -4
  58. package/dist/core/message/response-engine.js +107 -41
  59. package/dist/core/message/stream-debouncer.js +10 -2
  60. package/dist/core/permission/approval-gateway.js +1 -0
  61. package/dist/core/permission/ec-command-parser.js +93 -14
  62. package/dist/core/permission/index.js +1 -1
  63. package/dist/core/permission/mode.js +15 -0
  64. package/dist/core/permission/readonly-shell-query.js +148 -8
  65. package/dist/core/permission/sandbox-runtime.js +21 -0
  66. package/dist/core/permission/tool-policy.js +249 -27
  67. package/dist/core/protected-paths.js +107 -2
  68. package/dist/core/session/session-manager.js +21 -2
  69. package/dist/core/session/session-mapper.js +20 -1
  70. package/dist/eck/detect.js +18 -1
  71. package/dist/eck/group-rules-sync.js +15 -44
  72. package/dist/index.js +161 -86
  73. package/dist/ipc.js +16 -3
  74. package/dist/paths.js +3 -0
  75. package/dist/trigger/feedback.js +64 -11
  76. package/dist/trigger/manager.js +21 -1
  77. package/dist/trigger/parser.js +14 -2
  78. package/dist/trigger/scheduler.js +1 -0
  79. package/dist/trigger/validation.js +67 -6
  80. package/dist/utils/ecweb-supervisor.js +332 -0
  81. package/dist/utils/error-utils.js +10 -0
  82. package/kits/docs/evolcore/trigger.md +17 -1
  83. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  84. package/kits/schemas/role-config.schema.1.json +1 -0
  85. package/kits/templates/roles/admin.json +5 -0
  86. package/kits/templates/roles/member.json +7 -1
  87. package/kits/templates/roles/visitor.json +2 -0
  88. package/package.json +1 -1
  89. package/dist/aun/msg/payload-type.js +0 -27
  90. package/dist/aun/rpc/caller.js +0 -42
  91. package/dist/aun/rpc/connection.js +0 -25
  92. package/dist/aun/storage/manage.js +0 -10
  93. package/dist/config/access-policy-domain.js +0 -18
  94. package/dist/config/config-batch-get.js +0 -11
  95. package/dist/config/owner-policy.js +0 -4
  96. package/dist/config/role-config-v4-startup.js +0 -32
  97. package/dist/config/role-config-v5-startup.js +0 -27
  98. package/dist/core/auth/trigger-authorization.js +0 -15
  99. package/dist/core/interaction-registration.js +0 -10
  100. package/dist/core/permission/execution-sandbox.js +0 -16
  101. package/dist/core/relation/peer-key.js +0 -1
  102. package/dist/core/session/session-key.js +0 -24
  103. package/dist/eck/baseagent-caps.js +0 -18
  104. package/dist/eck/rules-loader.js +0 -28
@@ -0,0 +1,332 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { spawn } from 'child_process';
4
+ import { resolvePaths } from '../paths.js';
5
+ import { WEB_PACKAGE_NAME } from '../product.js';
6
+ import { resolveGlobalPkg } from './npm-ops.js';
7
+ import { ecwebVersionRequirementError, resolveEcwebLaunchCommand } from './ecweb-utils.js';
8
+ import * as platform from './cross-platform.js';
9
+ import { getProcessStartTime } from './process-introspect.js';
10
+ const INSTANCE_FILE_RE = /^(?:ecweb|watch-web)-\d+\.json$/;
11
+ function sleep(ms) {
12
+ return new Promise(resolve => setTimeout(resolve, ms));
13
+ }
14
+ function isAlive(record) {
15
+ if (!Number.isInteger(record.pid) || record.pid <= 0)
16
+ return false;
17
+ // ECWeb versions before the supervisor used a different Linux boot-time
18
+ // calculation, so their `startedAt` can be off by seconds or minutes even
19
+ // while the PID and HTTP listener are healthy. The instance file is already
20
+ // scoped to this EVOLCORE_HOME; use liveness here and let the HTTP probe be
21
+ // the ownership/health check. PID/start-time validation remains in the
22
+ // explicit kill path below.
23
+ return platform.isProcessRunning(record.pid);
24
+ }
25
+ function readEntries() {
26
+ const dir = resolvePaths().instanceDir;
27
+ let files;
28
+ try {
29
+ files = fs.readdirSync(dir);
30
+ }
31
+ catch {
32
+ return [];
33
+ }
34
+ const entries = [];
35
+ for (const file of files) {
36
+ if (!INSTANCE_FILE_RE.test(file))
37
+ continue;
38
+ const filePath = path.join(dir, file);
39
+ try {
40
+ const value = JSON.parse(fs.readFileSync(filePath, 'utf8'));
41
+ if (Number.isInteger(value.pid) && value.pid > 0) {
42
+ entries.push({
43
+ filePath,
44
+ record: {
45
+ pid: value.pid,
46
+ port: Number.isInteger(value.port) ? value.port : 42705,
47
+ ...(typeof value.startedAt === 'number' ? { startedAt: value.startedAt } : {}),
48
+ },
49
+ });
50
+ }
51
+ }
52
+ catch {
53
+ // A partially-written instance file is ignored until the next check.
54
+ }
55
+ }
56
+ return entries;
57
+ }
58
+ function removeEntry(entry) {
59
+ try {
60
+ fs.unlinkSync(entry.filePath);
61
+ }
62
+ catch { }
63
+ }
64
+ function removeEntriesForPid(pid) {
65
+ for (const entry of readEntries()) {
66
+ if (entry.record.pid === pid)
67
+ removeEntry(entry);
68
+ }
69
+ }
70
+ async function probe(port) {
71
+ try {
72
+ const response = await fetch(`http://127.0.0.1:${port}/`, {
73
+ signal: AbortSignal.timeout(1500),
74
+ });
75
+ return response.ok;
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
81
+ /**
82
+ * Owns the ECWeb process for the lifetime of the daemon.
83
+ *
84
+ * The CLI may have started ECWeb moments earlier (ec start/restart/watch web).
85
+ * We first adopt a healthy registered instance and only spawn after a few
86
+ * seconds of failed discovery. This closes the race between the CLI launcher,
87
+ * ECWeb's own stale-instance cleanup, and daemon startup.
88
+ */
89
+ export function createEcwebSupervisor(options) {
90
+ const failureThreshold = Math.max(1, options.failureThreshold ?? 3);
91
+ const checkIntervalMs = Math.max(1000, options.checkIntervalMs ?? 5000);
92
+ let active = null;
93
+ let child = null;
94
+ let logFd = null;
95
+ let timer = null;
96
+ let failureCount = 0;
97
+ let startInFlight = null;
98
+ let stopping = false;
99
+ const logFile = path.join(resolvePaths().logs, 'ecweb-daemon.log');
100
+ const setActiveFromEntry = (entry, owned) => {
101
+ active = {
102
+ pid: entry.record.pid,
103
+ port: entry.record.port,
104
+ // Capture the current PID start time rather than trusting the record.
105
+ // Older ECWeb builds could write a clock-skewed `startedAt` value.
106
+ startedAt: getProcessStartTime(entry.record.pid)
107
+ ?? (typeof entry.record.startedAt === 'number' ? entry.record.startedAt : null),
108
+ owned,
109
+ };
110
+ };
111
+ const findHealthyInstance = async () => {
112
+ for (const entry of readEntries()) {
113
+ if (!isAlive(entry.record)) {
114
+ removeEntry(entry);
115
+ continue;
116
+ }
117
+ if (await probe(entry.record.port))
118
+ return entry;
119
+ }
120
+ return null;
121
+ };
122
+ const waitForHealthyInstance = async (timeoutMs) => {
123
+ const deadline = Date.now() + timeoutMs;
124
+ do {
125
+ const found = await findHealthyInstance();
126
+ if (found)
127
+ return found;
128
+ if (Date.now() >= deadline)
129
+ break;
130
+ await sleep(250);
131
+ } while (!stopping);
132
+ return null;
133
+ };
134
+ const closeLogStream = () => {
135
+ if (logFd === null)
136
+ return;
137
+ try {
138
+ fs.closeSync(logFd);
139
+ }
140
+ catch { }
141
+ logFd = null;
142
+ };
143
+ const killActive = async () => {
144
+ const current = active;
145
+ if (!current) {
146
+ // During the short window between spawn() and ECWeb writing its
147
+ // instance record there is no `active` entry yet, but the daemon still
148
+ // owns the child and must not leave it behind on shutdown.
149
+ const pendingPid = child?.pid;
150
+ if (pendingPid && platform.isProcessRunning(pendingPid)) {
151
+ try {
152
+ platform.killProcess(pendingPid, true);
153
+ }
154
+ catch { }
155
+ await platform.waitForProcessExit(pendingPid, 5000);
156
+ }
157
+ return;
158
+ }
159
+ const actual = getProcessStartTime(current.pid);
160
+ // Never kill a reused PID. `startedAt` was captured from the live process
161
+ // when it was adopted/spawned, so this remains safe even for legacy records
162
+ // whose on-disk timestamp used a different clock.
163
+ const sameProcess = current.startedAt !== null && actual !== null
164
+ ? current.startedAt === actual
165
+ : false;
166
+ if (sameProcess && platform.isProcessRunning(current.pid)) {
167
+ try {
168
+ platform.killProcess(current.pid, true);
169
+ }
170
+ catch { }
171
+ await platform.waitForProcessExit(current.pid, 5000);
172
+ }
173
+ removeEntriesForPid(current.pid);
174
+ active = null;
175
+ };
176
+ const spawnEcweb = async () => {
177
+ const root = resolvePaths().root;
178
+ const localEntry = path.join(root, 'ecweb', 'dist', 'index.js');
179
+ const globalPkg = resolveGlobalPkg(WEB_PACKAGE_NAME);
180
+ let launch = resolveEcwebLaunchCommand(['--home', root, '--port', String(options.port)], {
181
+ installedPkg: globalPkg,
182
+ });
183
+ let cwd = root;
184
+ let version = globalPkg?.version;
185
+ if (!launch && fs.existsSync(localEntry)) {
186
+ launch = {
187
+ command: process.execPath,
188
+ args: [localEntry, '--home', root, '--port', String(options.port)],
189
+ entry: localEntry,
190
+ source: 'package-bin',
191
+ };
192
+ cwd = path.dirname(path.dirname(localEntry));
193
+ try {
194
+ version = JSON.parse(fs.readFileSync(path.join(root, 'ecweb', 'package.json'), 'utf8')).version;
195
+ }
196
+ catch { }
197
+ }
198
+ if (!launch) {
199
+ options.logger.warn(`ECWeb 启动入口不存在,请安装 ${WEB_PACKAGE_NAME}@latest`);
200
+ return;
201
+ }
202
+ const versionError = ecwebVersionRequirementError(version);
203
+ if (versionError) {
204
+ options.logger.error(`ECWeb 未启动: ${versionError}`);
205
+ return;
206
+ }
207
+ fs.mkdirSync(resolvePaths().logs, { recursive: true });
208
+ fs.appendFileSync(logFile, `\n# daemon supervisor start ${new Date().toISOString()}\n`);
209
+ // `spawn()` accepts inherited file descriptors, but not a WriteStream
210
+ // whose fd has not been opened yet. Open once and pass the numeric fd to
211
+ // both stdout and stderr; the parent closes its copy after spawning.
212
+ logFd = fs.openSync(logFile, 'a');
213
+ const childLogFd = logFd;
214
+ let spawned;
215
+ try {
216
+ spawned = spawn(launch.command, launch.args, {
217
+ cwd,
218
+ detached: true,
219
+ stdio: ['ignore', childLogFd, childLogFd],
220
+ windowsHide: platform.isWindows,
221
+ env: { ...process.env, EVOLCORE_HOME: root },
222
+ });
223
+ }
224
+ catch (error) {
225
+ closeLogStream();
226
+ options.logger.error(`ECWeb 子进程创建失败: ${error?.message || error}`);
227
+ return;
228
+ }
229
+ closeLogStream();
230
+ child = spawned;
231
+ spawned.once('error', (error) => {
232
+ options.logger.error(`ECWeb 子进程创建失败: ${error.message}`);
233
+ });
234
+ spawned.once('exit', (code, signal) => {
235
+ options.logger.warn(`ECWeb 进程退出 (PID: ${spawned.pid ?? 'unknown'}, code=${code ?? 'null'}, signal=${signal ?? 'null'})`);
236
+ if (child === spawned)
237
+ child = null;
238
+ if (!stopping) {
239
+ active = null;
240
+ failureCount = failureThreshold;
241
+ }
242
+ closeLogStream();
243
+ });
244
+ spawned.unref();
245
+ if (!spawned.pid) {
246
+ closeLogStream();
247
+ options.logger.warn('ECWeb 启动失败:未取得子进程 PID');
248
+ return;
249
+ }
250
+ options.logger.info(`✓ ECWeb supervisor 已启动 (PID: ${spawned.pid}),日志: ${logFile}`);
251
+ // ECWeb writes its own instance record after binding. Do not fabricate a
252
+ // record here; the watchdog will adopt the real record on its next tick.
253
+ // Returning immediately keeps daemon startup independent of HTTP bind
254
+ // latency while ServiceProxy continues to wait for the instance file.
255
+ };
256
+ const ensureRunning = async () => {
257
+ if (stopping || startInFlight)
258
+ return;
259
+ const healthy = await findHealthyInstance();
260
+ if (healthy) {
261
+ const changed = !active || active.pid !== healthy.record.pid || active.port !== healthy.record.port;
262
+ setActiveFromEntry(healthy, active?.pid === healthy.record.pid ? active.owned : false);
263
+ failureCount = 0;
264
+ if (changed)
265
+ options.logger.info(`✓ ECWeb supervisor 接管现有实例 (PID: ${healthy.record.pid}, 端口: ${healthy.record.port})`);
266
+ return;
267
+ }
268
+ failureCount += 1;
269
+ if (failureCount < failureThreshold)
270
+ return;
271
+ // A manual `ec watch web` may be between killing the old process and
272
+ // registering the replacement. Re-scan immediately before spawning.
273
+ const replacement = await waitForHealthyInstance(1500);
274
+ if (replacement) {
275
+ setActiveFromEntry(replacement, false);
276
+ failureCount = 0;
277
+ return;
278
+ }
279
+ startInFlight = (async () => {
280
+ await killActive();
281
+ await spawnEcweb();
282
+ failureCount = 0;
283
+ })().finally(() => { startInFlight = null; });
284
+ await startInFlight;
285
+ };
286
+ return {
287
+ async start() {
288
+ if (stopping)
289
+ return;
290
+ // CLI/restart-monitor writes the instance record before the daemon is
291
+ // ready. Give that process enough time to bind before launching another.
292
+ const existing = await waitForHealthyInstance(1_500);
293
+ if (existing) {
294
+ setActiveFromEntry(existing, false);
295
+ options.logger.info(`✓ ECWeb supervisor 接管现有实例 (PID: ${existing.record.pid}, 端口: ${existing.record.port})`);
296
+ }
297
+ else {
298
+ failureCount = failureThreshold;
299
+ await ensureRunning();
300
+ }
301
+ timer = setInterval(() => {
302
+ void ensureRunning().catch((error) => {
303
+ options.logger.error(`ECWeb watchdog 检查失败: ${error?.message || error}`);
304
+ });
305
+ }, checkIntervalMs);
306
+ timer.unref?.();
307
+ },
308
+ async stop() {
309
+ if (stopping)
310
+ return;
311
+ stopping = true;
312
+ if (timer)
313
+ clearInterval(timer);
314
+ timer = null;
315
+ if (startInFlight) {
316
+ try {
317
+ await startInFlight;
318
+ }
319
+ catch { }
320
+ }
321
+ await killActive();
322
+ if (child) {
323
+ try {
324
+ child.removeAllListeners();
325
+ }
326
+ catch { }
327
+ child = null;
328
+ }
329
+ closeLogStream();
330
+ },
331
+ };
332
+ }
@@ -2,6 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { getPackageRoot, resolvePaths } from '../paths.js';
4
4
  import { logger } from './logger.js';
5
+ import { isSandboxInitializationFailure } from '../core/permission/sandbox-runtime.js';
5
6
  export var ErrorType;
6
7
  (function (ErrorType) {
7
8
  ErrorType["SDK_TIMEOUT"] = "sdk_timeout";
@@ -11,6 +12,7 @@ export var ErrorType;
11
12
  ErrorType["STREAM_ERROR"] = "stream_error";
12
13
  ErrorType["CONTEXT_TOO_LONG"] = "context_too_long";
13
14
  ErrorType["MODEL_UNAVAILABLE"] = "model_unavailable";
15
+ ErrorType["SANDBOX_INITIALIZATION_FAILED"] = "sandbox_initialization_failed";
14
16
  ErrorType["UNKNOWN"] = "unknown";
15
17
  })(ErrorType || (ErrorType = {}));
16
18
  /**
@@ -269,6 +271,9 @@ export function classifyError(error) {
269
271
  || /api error: 404\b/.test(msg)) {
270
272
  return ErrorType.MODEL_UNAVAILABLE;
271
273
  }
274
+ if (isSandboxInitializationFailure(error)) {
275
+ return ErrorType.SANDBOX_INITIALIZATION_FAILED;
276
+ }
272
277
  if (msg.includes('401') || msg.includes('authentication_error') || msg.includes('unauthorized')) {
273
278
  return ErrorType.AUTH_ERROR;
274
279
  }
@@ -328,6 +333,8 @@ export function getErrorMessage(error, terminalReason, includeEmoji = true) {
328
333
  return `${prefix}工具执行失败,请检查操作或重试`;
329
334
  case 'permission_denied':
330
335
  return `${prefix}权限被拒绝,操作已取消`;
336
+ case 'sandbox_initialization_failed':
337
+ return `${warnPrefix}只读执行沙箱启动失败(sandbox_initialization_failed),当前任务未执行;请检查 user namespace/seccomp 配置后重试`;
331
338
  case 'aborted_streaming':
332
339
  case 'aborted_tools':
333
340
  return `${prefix}任务已中断`;
@@ -345,6 +352,9 @@ export function getErrorMessage(error, terminalReason, includeEmoji = true) {
345
352
  if (msg.includes('CONTEXT_COMPACT_FAILED') || isContextTooLongText(msg)) {
346
353
  return `${warnPrefix}上下文过长,自动压缩失败,请手动输入 /compact 重试`;
347
354
  }
355
+ if (isSandboxInitializationFailure(error)) {
356
+ return `${warnPrefix}只读执行沙箱启动失败(sandbox_initialization_failed),当前任务未执行;请检查 user namespace/seccomp 配置后重试`;
357
+ }
348
358
  if (msg.includes('401') || msg.includes('authentication_error') || msg.toLowerCase().includes('unauthorized')) {
349
359
  return `${errPrefix}API Key 无效,请检查密钥配置。使用 /status 查看当前配置`;
350
360
  }
@@ -132,11 +132,24 @@ Trigger V3 第二阶段使用 `execution.type` 区分执行位置:
132
132
  ```bash
133
133
  --target-channel <channelKey>
134
134
  --target-channel-id <channelId>
135
+ --target-chat-type <private|group>
135
136
  --target-session main
136
137
  --target-session thread --target-thread-id <threadId>
137
138
  ```
138
139
 
139
- AUN 渠道由 agent AID 隐式创建,不需要写入 agent 配置的 `channels[]`。CLI 和 Trigger V3.1 definition 均可将 `aun` 用作 `channelKey` 简写;保存前会按 `agentAid` 展开为 `aun#<agentAid>#main`。其他渠道仍需指定完整的实例 channel key。
140
+ AUN 渠道由 agent AID 隐式创建,不需要写入 agent 配置的 `channels[]`。CLI 和 Trigger V3.1 definition 均可将 `aun` 用作 `channelKey` 简写;保存前会按 `agentAid` 展开为 `aun#<agentAid>#main`。AUN 目标必须明确设置 `chatType`;`channelId` 在私聊时是对端 ID,在群聊时就是群 ID,不再额外保存 `groupId`。读取缺少 `chatType` 的旧 AUN Trigger definition 时,迁移逻辑会仅在该次迁移中按 `private` 补齐并回写;新建和编辑仍要求显式指定。其他渠道仍需指定完整的实例 channel key。
141
+
142
+ ```json
143
+ {
144
+ "strategy": "target",
145
+ "target": {
146
+ "channelKey": "aun",
147
+ "channelId": "peer.agentid.pub",
148
+ "chatType": "private",
149
+ "session": "main"
150
+ }
151
+ }
152
+ ```
140
153
 
141
154
  Trigger V3.1 feedback 是单策略:
142
155
 
@@ -307,6 +320,7 @@ ec trigger create --exec script --file ./trigger-dir --enable
307
320
  "target": {
308
321
  "channelKey": "<channelKey>",
309
322
  "channelId": "<channelId>",
323
+ "chatType": "private",
310
324
  "session": "main"
311
325
  }
312
326
  },
@@ -388,6 +402,7 @@ ec trigger create --exec script --file ./trigger-dir --enable
388
402
  "target": {
389
403
  "channelKey": "<channelKey>",
390
404
  "channelId": "<channelId>",
405
+ "chatType": "private",
391
406
  "session": "main"
392
407
  },
393
408
  "onReply": {
@@ -466,6 +481,7 @@ flag 模式不能配置过滤条件。需要根据事件 payload 过滤时,写
466
481
  "target": {
467
482
  "channelKey": "<channelKey>",
468
483
  "channelId": "<channelId>",
484
+ "chatType": "private",
469
485
  "session": "main"
470
486
  }
471
487
  },
@@ -49,8 +49,7 @@ function isExplicitGroupId(value) {
49
49
  return (id.startsWith('group.') && id.includes('/'))
50
50
  || /^\d+\.[a-z0-9.-]+$/i.test(id)
51
51
  || /^grp_[a-z0-9_-]+$/i.test(id)
52
- || /^g-[a-z0-9_-]+(?:[.@][a-z0-9.-]+)?$/i.test(id)
53
- || /(?:^|\.)groupid\.pub$/i.test(id);
52
+ || /^g-[a-z0-9_-]+(?:[.@][a-z0-9.-]+)?$/i.test(id);
54
53
  }
55
54
 
56
55
  function readJson(file) {
@@ -132,6 +132,7 @@
132
132
  "properties": {
133
133
  "allow": { "type": "boolean", "description": "是否允许该角色执行匹配的 operation。" },
134
134
  "dangerous": { "type": "boolean", "default": false, "description": "是否将该授权规则标记为危险操作规则;危险操作还会应用额外安全约束。" },
135
+ "permissionMode": { "type": "string", "enum": ["readonly", "auto", "request", "bypass"], "description": "临时 operation 级权限模式;后续迁移到作用域模式规则。" },
135
136
  "scopes": {
136
137
  "type": "array",
137
138
  "items": { "type": "string", "enum": ["relation", "role", "agent", "process", "filesystem", "control", "raw-cli"], "description": "该命令权限适用的一个配置或执行作用域。", "x-enumDescriptions": { "relation": "当前 Agent 与对端的关系配置域", "role": "独立角色注册表或角色策略配置域", "agent": "单个 Agent 的配置与生命周期域", "process": "EvolCore 进程级配置与运行域", "filesystem": "受控文件系统操作域", "control": "控制面命令和管理通道域", "raw-cli": "未经结构化 operation 封装的原始 CLI 执行域" } },
@@ -63,6 +63,11 @@
63
63
  "scopes": ["agent"],
64
64
  "constraints": { "targetCurrentAgentOnly": true, "requireAgentAdmin": true }
65
65
  },
66
+ "ec.fs.mkdir": { "allow": true, "permissionMode": "auto", "scopes": ["relation"], "constraints": { "groupOnly": true, "currentRelationOnly": true, "targetCurrentAgentOnly": true } },
67
+ "ec.fs.upload": { "allow": true, "permissionMode": "auto", "scopes": ["relation"], "constraints": { "groupOnly": true, "currentRelationOnly": true, "targetCurrentAgentOnly": true } },
68
+ "ec.fs.copy": { "allow": true, "permissionMode": "auto", "scopes": ["relation"], "constraints": { "groupOnly": true, "currentRelationOnly": true, "targetCurrentAgentOnly": true } },
69
+ "ec.fs.move": { "allow": true, "permissionMode": "auto", "scopes": ["relation"], "constraints": { "groupOnly": true, "currentRelationOnly": true, "targetCurrentAgentOnly": true } },
70
+ "ec.fs.remove": { "allow": true, "permissionMode": "auto", "scopes": ["relation"], "constraints": { "groupOnly": true, "currentRelationOnly": true, "targetCurrentAgentOnly": true } },
66
71
  "*": { "allow": true },
67
72
  "agent.reload": {
68
73
  "allow": true,
@@ -24,7 +24,7 @@
24
24
  "storage.*": { "allow": false },
25
25
  "aid.*": { "allow": false },
26
26
  "gateway.*": { "allow": false },
27
- "group.list": { "allow": false },
27
+ "group.list": { "allow": true, "scopes": ["agent"], "constraints": { "targetCurrentAgentOnly": true } },
28
28
  "agent.baseagent.current": { "allow": true, "scopes": ["relation"], "constraints": { "ownPeerOnly": true, "targetCurrentAgentOnly": true } },
29
29
  "agent.baseagent.list": { "allow": true, "scopes": ["agent"], "constraints": { "targetCurrentAgentOnly": true } },
30
30
  "agent.*": { "allow": false },
@@ -66,6 +66,11 @@
66
66
  "ec.fs.cat": { "allow": true, "scopes": ["relation"], "constraints": { "groupOnly": true, "currentRelationOnly": true, "targetCurrentAgentOnly": true } },
67
67
  "ec.fs.find": { "allow": true, "scopes": ["relation"], "constraints": { "groupOnly": true, "currentRelationOnly": true, "targetCurrentAgentOnly": true } },
68
68
  "ec.fs.df": { "allow": true, "scopes": ["relation"], "constraints": { "groupOnly": true, "currentRelationOnly": true, "targetCurrentAgentOnly": true } },
69
+ "ec.fs.mkdir": { "allow": true, "permissionMode": "auto", "scopes": ["relation"], "constraints": { "groupOnly": true, "currentRelationOnly": true, "targetCurrentAgentOnly": true } },
70
+ "ec.fs.upload": { "allow": true, "permissionMode": "auto", "scopes": ["relation"], "constraints": { "groupOnly": true, "currentRelationOnly": true, "targetCurrentAgentOnly": true } },
71
+ "ec.fs.copy": { "allow": true, "permissionMode": "auto", "scopes": ["relation"], "constraints": { "groupOnly": true, "currentRelationOnly": true, "targetCurrentAgentOnly": true } },
72
+ "ec.fs.move": { "allow": true, "permissionMode": "auto", "scopes": ["relation"], "constraints": { "groupOnly": true, "currentRelationOnly": true, "targetCurrentAgentOnly": true } },
73
+ "ec.fs.remove": { "allow": true, "permissionMode": "auto", "scopes": ["relation"], "constraints": { "groupOnly": true, "currentRelationOnly": true, "targetCurrentAgentOnly": true } },
69
74
  "ec.handoff.status": { "allow": true, "scopes": ["relation"], "constraints": { "currentRelationOnly": true, "targetCurrentAgentOnly": true } },
70
75
  "ec.handoff.list": { "allow": true, "scopes": ["relation"], "constraints": { "currentRelationOnly": true, "targetCurrentAgentOnly": true } },
71
76
  "ec.handoff.trace": { "allow": true, "scopes": ["relation"], "constraints": { "currentRelationOnly": true, "targetCurrentAgentOnly": true } },
@@ -73,6 +78,7 @@
73
78
  "ec.queue.*": { "allow": false },
74
79
  "ec.group.send": { "allow": true, "constraints": { "groupOnly": true, "currentRelationOnly": true } },
75
80
  "ec.group.file": { "allow": true, "constraints": { "groupOnly": true, "currentRelationOnly": true } },
81
+ "ec.group.list": { "allow": true, "scopes": ["agent"], "constraints": { "targetCurrentAgentOnly": true } },
76
82
  "ec.group.info": { "allow": true, "scopes": ["relation"], "constraints": { "groupOnly": true, "currentRelationOnly": true, "targetCurrentAgentOnly": true } },
77
83
  "ec.group.pull": { "allow": true, "scopes": ["relation"], "constraints": { "groupOnly": true, "currentRelationOnly": true, "targetCurrentAgentOnly": true } },
78
84
  "ec.group.members": { "allow": true, "scopes": ["relation"], "constraints": { "groupOnly": true, "currentRelationOnly": true, "targetCurrentAgentOnly": true } },
@@ -36,6 +36,7 @@
36
36
  "group.mentionmode.update": { "allow": false },
37
37
  "group.rulespolicy.current": { "allow": true, "scopes": ["relation"], "constraints": { "groupOnly": true, "currentRelationOnly": true, "targetCurrentAgentOnly": true } },
38
38
  "group.rulespolicy.update": { "allow": false },
39
+ "group.list": { "allow": true, "scopes": ["agent"], "constraints": { "targetCurrentAgentOnly": true } },
39
40
  "trigger.list": { "allow": true, "scopes": ["relation"], "constraints": { "ownPeerOnly": true, "targetCurrentAgentOnly": true } },
40
41
  "trigger.show": { "allow": true, "scopes": ["relation"], "constraints": { "ownPeerOnly": true, "targetCurrentAgentOnly": true } },
41
42
  "trigger.history": { "allow": true, "scopes": ["relation"], "constraints": { "ownPeerOnly": true, "targetCurrentAgentOnly": true } },
@@ -69,6 +70,7 @@
69
70
  "storage.*": { "allow": false },
70
71
  "ec.group.send": { "allow": true, "scopes": ["relation"], "constraints": { "groupOnly": true, "currentRelationOnly": true } },
71
72
  "ec.group.file": { "allow": false },
73
+ "ec.group.list": { "allow": true, "scopes": ["agent"], "constraints": { "targetCurrentAgentOnly": true } },
72
74
  "ec.group.info": { "allow": true, "scopes": ["relation"], "constraints": { "groupOnly": true, "currentRelationOnly": true, "targetCurrentAgentOnly": true } },
73
75
  "ec.group.pull": { "allow": false },
74
76
  "ec.group.members": { "allow": false },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolcore",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "AI Agent gateway connecting Claude, Codex, Gemini, and the bundled ecagent runner to messaging channels with multi-project session management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,27 +0,0 @@
1
- import path from 'path';
2
- const EXT_TYPE_MAP = {
3
- // image
4
- '.png': 'image', '.jpg': 'image', '.jpeg': 'image',
5
- '.gif': 'image', '.webp': 'image', '.svg': 'image',
6
- '.bmp': 'image', '.heic': 'image', '.heif': 'image',
7
- // video
8
- '.mp4': 'video', '.mov': 'video', '.webm': 'video',
9
- '.avi': 'video', '.mkv': 'video', '.m4v': 'video',
10
- // voice
11
- '.opus': 'voice', '.mp3': 'voice', '.aac': 'voice',
12
- '.m4a': 'voice', '.wav': 'voice', '.flac': 'voice', '.ogg': 'voice',
13
- };
14
- /**
15
- * 按扩展名推断 payload.type。
16
- * 未识别的扩展名归类为 'file'。
17
- */
18
- export function inferPayloadType(filename) {
19
- const ext = path.extname(filename).toLowerCase();
20
- return EXT_TYPE_MAP[ext] ?? 'file';
21
- }
22
- /**
23
- * 校验显式传入的 --as 值。
24
- */
25
- export function isValidPayloadType(value) {
26
- return value === 'image' || value === 'video' || value === 'voice' || value === 'file';
27
- }
@@ -1,42 +0,0 @@
1
- import { createShortConnection } from './connection.js';
2
- export async function rpcCall(aid, method, params, opts) {
3
- const conn = await createShortConnection(aid, opts);
4
- try {
5
- const result = await conn.call(method, params);
6
- return { ok: true, result };
7
- }
8
- catch (e) {
9
- if (e.code !== undefined && e.message !== undefined) {
10
- return { ok: false, error: { code: e.code, message: e.message, data: e.data } };
11
- }
12
- return { ok: false, error: { code: -1, message: String(e.message || e) } };
13
- }
14
- finally {
15
- await conn.close();
16
- }
17
- }
18
- export async function rpcBatch(aid, calls, opts) {
19
- const conn = await createShortConnection(aid, opts);
20
- const results = [];
21
- try {
22
- for (const { method, params } of calls) {
23
- try {
24
- const result = await conn.call(method, params);
25
- results.push({ ok: true, result });
26
- }
27
- catch (e) {
28
- if (e.code !== undefined && e.message !== undefined) {
29
- results.push({ ok: false, error: { code: e.code, message: e.message, data: e.data } });
30
- }
31
- else {
32
- results.push({ ok: false, error: { code: -1, message: String(e.message || e) } });
33
- }
34
- break; // stop on first failure
35
- }
36
- }
37
- }
38
- finally {
39
- await conn.close();
40
- }
41
- return results;
42
- }
@@ -1,25 +0,0 @@
1
- import { getAidStore, loadClient, SLOT } from '../aid/store.js';
2
- export async function createShortConnection(aid, opts) {
3
- const store = await getAidStore({ slotId: opts?.slotId ?? SLOT.cli, aunPath: opts?.aunPath });
4
- try {
5
- const client = await loadClient(store, aid);
6
- await client.connect({ connection_kind: 'short', short_ttl_ms: 30000, auto_reconnect: false });
7
- return {
8
- async call(method, params) {
9
- return client.call(method, params);
10
- },
11
- async close() {
12
- try {
13
- await client.close();
14
- }
15
- finally {
16
- store.close();
17
- }
18
- },
19
- };
20
- }
21
- catch (e) {
22
- store.close();
23
- throw e;
24
- }
25
- }
@@ -1,10 +0,0 @@
1
- import { rpcCall } from '../rpc/caller.js';
2
- export async function storageLs(aid, prefix, opts) {
3
- return rpcCall(aid, 'storage.list_objects', { prefix: prefix || '' }, { aunPath: opts?.aunPath });
4
- }
5
- export async function storageRm(aid, remotePath, opts) {
6
- return rpcCall(aid, 'storage.delete_object', { object_key: remotePath }, { aunPath: opts?.aunPath });
7
- }
8
- export async function storageQuota(aid, opts) {
9
- return rpcCall(aid, 'storage.get_quota', {}, { aunPath: opts?.aunPath });
10
- }
@@ -1,18 +0,0 @@
1
- import { isValidAid } from '../aun/aid/validation.js';
2
- export function validAgentOwnerIds(agent) {
3
- return [...new Set((agent?.owners ?? [])
4
- .map(value => String(value || '').trim())
5
- .filter(isValidAid))];
6
- }
7
- export function validateAccessPolicyDomain(selfAid, agent) {
8
- const access = agent.access;
9
- if (!access)
10
- return undefined;
11
- if (access.policyMode !== 'open' && validAgentOwnerIds(agent).length === 0) {
12
- return {
13
- code: 'restricted_mode_without_owner',
14
- message: `Agent ${selfAid} cannot use ${access.policyMode} without an Owner`,
15
- };
16
- }
17
- return undefined;
18
- }
@@ -1,11 +0,0 @@
1
- import { normalizeCliArgv } from '../cli/cli-argv.js';
2
- export function splitConfigBatchGetArgv(inputArgv) {
3
- const argv = normalizeCliArgv(inputArgv);
4
- if (argv[0] !== 'config' || argv[1] !== 'get')
5
- return null;
6
- const fields = [];
7
- let i = 2;
8
- for (; i < argv.length && !argv[i].startsWith('-'); i++)
9
- fields.push(argv[i]);
10
- return { fields, rest: argv.slice(i) };
11
- }