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.
- package/CHANGELOG.md +22 -0
- package/bin/ec-safe-output.js +26 -7
- package/dist/agents/claude-runner.js +53 -7
- package/dist/agents/codex-app-server-client.js +53 -5
- package/dist/agents/codex-runner.js +104 -59
- package/dist/agents/ecagent-runner.js +4 -1
- package/dist/aun/aid/control-aid.js +36 -13
- package/dist/aun/aid/store.js +13 -7
- package/dist/aun/group-fs-download.js +56 -0
- package/dist/aun/group-identity.js +1 -2
- package/dist/aun/msg/group.js +11 -33
- package/dist/aun/msg/index.js +1 -2
- package/dist/aun/msg/managed-operation.js +116 -55
- package/dist/aun/msg/upload.js +16 -1
- package/dist/aun/outbox.js +31 -2
- package/dist/aun/rpc/client.js +66 -0
- package/dist/aun/rpc/index.js +1 -2
- package/dist/aun/storage/{upload.js → client.js} +11 -2
- package/dist/aun/storage/index.js +1 -2
- package/dist/channels/aun.js +371 -136
- package/dist/cli/agent.js +9 -96
- package/dist/cli/cli-argv.js +10 -0
- package/dist/cli/config.js +9 -17
- package/dist/cli/ctl-command.js +1 -1
- package/dist/cli/daemon-commands.js +39 -24
- package/dist/cli/fs-command.js +19 -10
- package/dist/cli/init.js +77 -8
- package/dist/cli/queue-command.js +42 -0
- package/dist/cli/restart-monitor.js +3 -14
- package/dist/cli/trigger-command.js +7 -0
- package/dist/config/access-policy.js +1 -2
- package/dist/config/builtin-roles.js +5 -0
- package/dist/config/config-manager.js +21 -1
- package/dist/config/peer-role-resolver.js +6 -2
- package/dist/config/role-migration-startup.js +53 -0
- package/dist/config/role-store.js +1 -1
- package/dist/core/auth/agent-delegation.js +1 -1
- package/dist/core/auth/auth-gateway.js +29 -1
- package/dist/core/auth/authorization-audit.js +15 -0
- package/dist/core/auth/operation-authorizer.js +1 -0
- package/dist/core/auth/operation-catalog.js +9 -0
- package/dist/core/bootstrap-messages.js +9 -2
- package/dist/core/bootstrap-service.js +1 -0
- package/dist/core/command/command-handler.js +198 -51
- package/dist/core/command/menu-catalog.js +4 -0
- package/dist/core/command/menu-handler.js +85 -18
- package/dist/core/command/menu-protocol.js +23 -2
- package/dist/core/command/slash-handler.js +58 -10
- package/dist/core/event-catalog.js +6 -0
- package/dist/core/handoff/runtime.js +12 -1
- package/dist/core/handoff/store.js +5 -1
- package/dist/core/interaction-router.js +10 -0
- package/dist/core/message/im-renderer.js +19 -0
- package/dist/core/message/message-bridge.js +63 -11
- package/dist/core/message/message-log.js +2 -0
- package/dist/core/message/message-queue.js +267 -39
- package/dist/core/message/message-utils.js +81 -4
- package/dist/core/message/response-engine.js +107 -41
- package/dist/core/message/stream-debouncer.js +10 -2
- package/dist/core/permission/approval-gateway.js +1 -0
- package/dist/core/permission/ec-command-parser.js +93 -14
- package/dist/core/permission/index.js +1 -1
- package/dist/core/permission/mode.js +15 -0
- package/dist/core/permission/readonly-shell-query.js +148 -8
- package/dist/core/permission/sandbox-runtime.js +21 -0
- package/dist/core/permission/tool-policy.js +249 -27
- package/dist/core/protected-paths.js +107 -2
- package/dist/core/session/session-manager.js +21 -2
- package/dist/core/session/session-mapper.js +20 -1
- package/dist/eck/detect.js +18 -1
- package/dist/eck/group-rules-sync.js +15 -44
- package/dist/index.js +161 -86
- package/dist/ipc.js +16 -3
- package/dist/paths.js +3 -0
- package/dist/trigger/feedback.js +64 -11
- package/dist/trigger/manager.js +21 -1
- package/dist/trigger/parser.js +14 -2
- package/dist/trigger/scheduler.js +1 -0
- package/dist/trigger/validation.js +67 -6
- package/dist/utils/ecweb-supervisor.js +332 -0
- package/dist/utils/error-utils.js +10 -0
- package/kits/docs/evolcore/trigger.md +17 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/templates/roles/admin.json +5 -0
- package/kits/templates/roles/member.json +7 -1
- package/kits/templates/roles/visitor.json +2 -0
- package/package.json +1 -1
- package/dist/aun/msg/payload-type.js +0 -27
- package/dist/aun/rpc/caller.js +0 -42
- package/dist/aun/rpc/connection.js +0 -25
- package/dist/aun/storage/manage.js +0 -10
- package/dist/config/access-policy-domain.js +0 -18
- package/dist/config/config-batch-get.js +0 -11
- package/dist/config/owner-policy.js +0 -4
- package/dist/config/role-config-v4-startup.js +0 -32
- package/dist/config/role-config-v5-startup.js +0 -27
- package/dist/core/auth/trigger-authorization.js +0 -15
- package/dist/core/interaction-registration.js +0 -10
- package/dist/core/permission/execution-sandbox.js +0 -16
- package/dist/core/relation/peer-key.js +0 -1
- package/dist/core/session/session-key.js +0 -24
- package/dist/eck/baseagent-caps.js +0 -18
- 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
|
|
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":
|
|
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.
|
|
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
|
-
}
|
package/dist/aun/rpc/caller.js
DELETED
|
@@ -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
|
-
}
|