evolcore 0.0.20 → 0.0.22
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 +65 -0
- package/README.md +58 -9
- package/bin/codex-managed-hook.mjs +3 -0
- package/bin/install-codex-managed-hooks.mjs +3 -1
- package/dist/agents/baseagent.js +10 -6
- package/dist/agents/claude-runner.js +393 -108
- package/dist/agents/codex-app-server-client.js +41 -7
- package/dist/agents/codex-runner.js +1292 -220
- package/dist/agents/ecagent-runner.js +171 -61
- package/dist/agents/gemini-runner.js +130 -30
- package/dist/agents/request-identity.js +25 -0
- package/dist/agents/runner-types.js +19 -0
- package/dist/aun/aid/agentmd.js +59 -2
- package/dist/aun/aid/identity.js +4 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +72 -6
- package/dist/aun/msg/history.js +213 -36
- package/dist/aun/msg/managed-operation.js +58 -9
- package/dist/aun/msg/p2p.js +5 -0
- package/dist/aun/outbox.js +189 -80
- package/dist/aun/service-proxy.js +43 -25
- package/dist/channels/aun.js +618 -123
- package/dist/channels/daemon.js +6 -1
- package/dist/cli/agent-command.js +4 -3
- package/dist/cli/agent.js +66 -56
- package/dist/cli/aun-commands.js +177 -42
- package/dist/cli/command-log.js +10 -11
- package/dist/cli/contact.js +1 -0
- package/dist/cli/daemon-commands.js +98 -123
- package/dist/cli/init.js +27 -15
- package/dist/cli/task-context.js +50 -0
- package/dist/cli/trigger-command.js +14 -5
- package/dist/cli/watch-logs.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +19 -5
- package/dist/config/config-manager.js +167 -22
- package/dist/config/contact-book-store.js +25 -3
- package/dist/config/contact-operation-service.js +32 -1
- package/dist/config/contact-request-service.js +44 -0
- package/dist/config/daemon-services.js +186 -0
- package/dist/config/gateway-config.js +20 -9
- package/dist/config/role-service.js +54 -3
- package/dist/config/schema-migration.js +550 -0
- package/dist/config-store.js +151 -9
- package/dist/core/agent-application-service.js +279 -0
- package/dist/core/audit/log-integrity.js +102 -0
- package/dist/core/auth/agent-delegation.js +43 -1
- package/dist/core/auth/auth-gateway.js +41 -4
- package/dist/core/auth/authorization-audit.js +216 -8
- package/dist/core/auth/operation-authorizer.js +41 -1
- package/dist/core/auth/operation-catalog.js +9 -1
- package/dist/core/bootstrap-messages.js +8 -0
- package/dist/core/bootstrap-service.js +99 -27
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/command/agent-control.js +56 -16
- package/dist/core/command/command-handler.js +311 -44
- package/dist/core/command/connect-menu.js +3 -4
- package/dist/core/command/group-menu.js +5 -7
- package/dist/core/command/menu-handler.js +288 -80
- package/dist/core/command/menu-protocol.js +1 -1
- package/dist/core/command/role-menu.js +21 -11
- package/dist/core/command/slash-gate.js +85 -18
- package/dist/core/command/slash-handler.js +377 -36
- package/dist/core/data-migration.js +11 -1
- package/dist/core/event-catalog.js +37 -0
- package/dist/core/evolagent.js +4 -0
- package/dist/core/handoff/dispatcher.js +4 -0
- package/dist/core/handoff/runtime.js +33 -3
- package/dist/core/handoff/store.js +32 -9
- package/dist/core/inference/text-inference.js +7 -15
- package/dist/core/message/im-renderer.js +90 -87
- package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
- package/dist/core/message/message-bridge.js +184 -12
- package/dist/core/message/message-log.js +47 -7
- package/dist/core/message/message-queue.js +227 -16
- package/dist/core/message/message-utils.js +12 -5
- package/dist/core/message/response-engine.js +658 -109
- package/dist/core/message/send-receipt.js +1 -0
- package/dist/core/message/stream-debouncer.js +9 -2
- package/dist/core/model/model-catalog.js +23 -15
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/approval-gateway.js +180 -6
- package/dist/core/permission/ec-command-parser.js +627 -69
- package/dist/core/permission/mode.js +18 -3
- package/dist/core/{protected-paths.js → permission/protected-paths.js} +27 -14
- package/dist/core/permission/readonly-shell-query.js +263 -9
- package/dist/core/permission/sandbox-runtime.js +159 -1
- package/dist/core/permission/tool-error-code.js +12 -0
- package/dist/core/permission/tool-policy.js +618 -23
- package/dist/core/session/session-fs-store.js +154 -5
- package/dist/core/session/session-manager.js +329 -30
- package/dist/core/session/session-renew.js +37 -13
- package/dist/core/session/session-turn-coordinator.js +16 -5
- package/dist/eck/kit-renderer.js +1 -1
- package/dist/index.js +316 -50
- package/dist/ipc.js +459 -29
- package/dist/paths.js +82 -7
- package/dist/response-system/context-builder.js +1 -7
- package/dist/response-system/engines/v1/proactive-flow.js +7 -2
- package/dist/stats/price-resolver.js +4 -0
- package/dist/trigger/anomaly-store.js +1 -0
- package/dist/trigger/feedback.js +70 -7
- package/dist/trigger/history.js +79 -4
- package/dist/trigger/legacy-session-history.js +2 -2
- package/dist/trigger/parser.js +13 -3
- package/dist/trigger/scheduler.js +20 -3
- package/dist/trigger/validation.js +6 -1
- package/dist/utils/atomic-write.js +27 -0
- package/dist/utils/ecweb-utils.js +16 -2
- package/dist/utils/error-utils.js +4 -1
- package/dist/utils/logger.js +30 -6
- package/dist/utils/process-tree-stats.js +24 -4
- package/dist/utils/process-tree-worker.js +31 -0
- package/dist/utils/project-path.js +1 -2
- package/dist/utils/tool-summary.js +59 -0
- package/dist/utils/windows-shell-trust.js +201 -0
- package/kits/docs/INDEX.md +1 -1
- package/kits/docs/evolcore/INDEX.md +3 -3
- package/kits/docs/evolcore/agent-create.md +146 -0
- package/kits/docs/evolcore/agent.md +6 -0
- package/kits/docs/evolcore/contact.md +7 -1
- package/kits/docs/evolcore/group-collaboration.md +251 -0
- package/kits/docs/evolcore/group-rules.md +1 -19
- package/kits/docs/evolcore/group.md +3 -1
- package/kits/docs/evolcore/msg.md +16 -0
- package/kits/docs/evolcore/trigger.md +6 -3
- package/kits/docs/prompt-loading-architecture.md +6 -0
- package/kits/eck_message_manifest.json +6 -6
- package/kits/schemas/_meta.json +7 -4
- package/kits/schemas/agent-config.schema.11.json +13 -0
- package/kits/schemas/agent-config.schema.12.json +427 -0
- package/kits/schemas/daemon.schema.5.json +0 -1
- package/kits/schemas/daemon.schema.6.json +131 -0
- package/kits/schemas/defaults.schema.5.json +15 -3
- package/kits/schemas/migrations/README.md +3 -1
- package/kits/schemas/relation-config.schema.8.json +13 -0
- package/kits/schemas/role-config.schema.1.json +1 -2
- package/kits/schemas/single-session.schema.3.json +32 -0
- package/kits/templates/message-fragments/item.md +1 -1
- package/kits/templates/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +2 -1
- package/kits/templates/system-fragments/commands.md +2 -2
- package/package.json +6 -3
- package/skills/eclink/SKILL.md +2 -0
- package/dist/config/aun-gateway-config.js +0 -2
package/dist/config-store.js
CHANGED
|
@@ -23,15 +23,42 @@ import { checkAgentDir, isValidAid } from './aun/aid/validation.js';
|
|
|
23
23
|
import { normalizeAidDomain } from './aun/aid/domain.js';
|
|
24
24
|
import { isValidChannelName } from './core/channel-loader.js';
|
|
25
25
|
import { CONFIG_SCHEMA_VERSION } from './types.js';
|
|
26
|
-
import { ConfigTarget, assertNoLegacyAunGatewayConfig, read as cfgRead, stripLegacyAunGatewayConfig, write as cfgWrite, } from './config/config-manager.js';
|
|
26
|
+
import { ConfigTarget, assertNoLegacyAunGatewayConfig, read as cfgRead, normalizeProcessConfigCompat, stripLegacyAunGatewayConfig, write as cfgWrite, } from './config/config-manager.js';
|
|
27
27
|
import { expandVars, buildEnvResolver } from './config/merge.js';
|
|
28
28
|
import { isAgentLifecycle } from './config/lifecycle.js';
|
|
29
29
|
import { logger } from './utils/logger.js';
|
|
30
30
|
import { parseStableSemver } from './utils/stable-semver.js';
|
|
31
|
+
import { isSupportedBaseagent } from './agents/baseagent.js';
|
|
32
|
+
import { normalizeDaemonServices } from './config/daemon-services.js';
|
|
33
|
+
import { currentVersion } from './config/schema-registry.js';
|
|
34
|
+
const SERVICE_METADATA_SENSITIVE_KEY = /(?:^|[_-])(endpoint|url|uri|token|access[_-]?token|authorization|cookie|secret|password|private[_-]?key|key|cert|certificate)(?:$|[_-])/i;
|
|
35
|
+
function assertLocalServiceEndpoint(value, fieldPath) {
|
|
36
|
+
if (typeof value !== 'string' || value.trim() === '' || value.length > 2048) {
|
|
37
|
+
throw new Error(`${fieldPath} must be a URL string`);
|
|
38
|
+
}
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = new URL(value.trim());
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw new Error(`${fieldPath} must be a valid URL`);
|
|
45
|
+
}
|
|
46
|
+
if (!['http:', 'https:', 'ws:', 'wss:'].includes(parsed.protocol)) {
|
|
47
|
+
throw new Error(`${fieldPath} must use http, https, ws, or wss`);
|
|
48
|
+
}
|
|
49
|
+
const host = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, '');
|
|
50
|
+
if (host !== 'localhost' && !/^127(?:\.\d{1,3}){3}$/.test(host)) {
|
|
51
|
+
throw new Error(`${fieldPath} must point to localhost or 127.0.0.0/8`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
31
54
|
/** 读 {root}/daemon.json。文件不存在返回 {},不报错。 */
|
|
32
55
|
export function loadDaemonConfig() {
|
|
33
56
|
const raw = atomicReadJson(resolvePaths().daemonConfig);
|
|
34
|
-
|
|
57
|
+
const sanitized = stripLegacyAunGatewayConfig(raw ?? {}, resolvePaths().daemonConfig, message => logger.warn(message));
|
|
58
|
+
return validateDaemonConfig(normalizeProcessConfigCompat(normalizeDaemonServices(sanitized)), false);
|
|
59
|
+
}
|
|
60
|
+
export function isFullAccessEnabled(config = loadDaemonConfig()) {
|
|
61
|
+
return config.fullaccess?.enabled === true;
|
|
35
62
|
}
|
|
36
63
|
let eckSnapshotsConfigCache;
|
|
37
64
|
/** Parse the process-level snapshot gate once for the current daemon lifecycle. */
|
|
@@ -60,9 +87,22 @@ export function saveDaemonConfig(value) {
|
|
|
60
87
|
// daemon.json updates fail because of a persisted, unused legacy value.
|
|
61
88
|
const validateAidDomain = current === null
|
|
62
89
|
|| current.aun?.defaultAidDomain !== value.aun?.defaultAidDomain;
|
|
63
|
-
|
|
90
|
+
const normalized = normalizeProcessConfigCompat(normalizeDaemonServices(stripLegacyAunGatewayConfig(value, configPath, message => logger.warn(message))));
|
|
91
|
+
atomicWriteJson(configPath, validateDaemonConfig({ ...normalized, $schema_version: currentVersion('daemon') }, validateAidDomain));
|
|
64
92
|
}
|
|
65
93
|
function validateDaemonConfig(value, validateAidDomain) {
|
|
94
|
+
if (value.fullaccess !== undefined) {
|
|
95
|
+
if (!value.fullaccess || typeof value.fullaccess !== 'object' || Array.isArray(value.fullaccess)) {
|
|
96
|
+
throw new Error('daemon.json.fullaccess must be an object');
|
|
97
|
+
}
|
|
98
|
+
const unknownFullAccessFields = Object.keys(value.fullaccess).filter(key => key !== 'enabled');
|
|
99
|
+
if (unknownFullAccessFields.length > 0) {
|
|
100
|
+
throw new Error(`daemon.json.fullaccess contains unsupported fields: ${unknownFullAccessFields.join(', ')}`);
|
|
101
|
+
}
|
|
102
|
+
if (value.fullaccess.enabled !== undefined && typeof value.fullaccess.enabled !== 'boolean') {
|
|
103
|
+
throw new Error('daemon.json.fullaccess.enabled must be a boolean');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
66
106
|
const minEvolVersion = value.aun?.minEvolVersion;
|
|
67
107
|
if (minEvolVersion !== undefined && !parseStableSemver(minEvolVersion)) {
|
|
68
108
|
throw new Error('daemon.json.aun.minEvolVersion must use stable X.Y.Z format');
|
|
@@ -75,6 +115,80 @@ function validateDaemonConfig(value, validateAidDomain) {
|
|
|
75
115
|
if (defaultEncrypt !== undefined && typeof defaultEncrypt !== 'boolean') {
|
|
76
116
|
throw new Error('daemon.json.aun.defaultEncrypt must be a boolean');
|
|
77
117
|
}
|
|
118
|
+
if (value.services !== undefined && !Array.isArray(value.services)) {
|
|
119
|
+
throw new Error('daemon.json.services must be an array');
|
|
120
|
+
}
|
|
121
|
+
const serviceNames = new Set();
|
|
122
|
+
for (const [index, rawService] of (value.services ?? []).entries()) {
|
|
123
|
+
if (!rawService || typeof rawService !== 'object' || Array.isArray(rawService)) {
|
|
124
|
+
throw new Error(`daemon.json.services[${index}] must be an object`);
|
|
125
|
+
}
|
|
126
|
+
const service = rawService;
|
|
127
|
+
const unknownServiceFields = Object.keys(service).filter((key) => !['name', 'enabled', 'port', 'proxy'].includes(key));
|
|
128
|
+
if (unknownServiceFields.length > 0) {
|
|
129
|
+
throw new Error(`daemon.json.services[${index}] contains unsupported fields: ${unknownServiceFields.join(', ')}`);
|
|
130
|
+
}
|
|
131
|
+
if (typeof service.name !== 'string' || !/^[a-z0-9_-]+$/.test(service.name)) {
|
|
132
|
+
throw new Error(`daemon.json.services[${index}].name must match [a-z0-9_-]+`);
|
|
133
|
+
}
|
|
134
|
+
if (serviceNames.has(service.name)) {
|
|
135
|
+
throw new Error(`daemon.json.services contains duplicate name: ${service.name}`);
|
|
136
|
+
}
|
|
137
|
+
serviceNames.add(service.name);
|
|
138
|
+
if (typeof service.enabled !== 'boolean') {
|
|
139
|
+
throw new Error(`daemon.json.services[${service.name}].enabled must be a boolean`);
|
|
140
|
+
}
|
|
141
|
+
if (service.port !== undefined && (!Number.isInteger(service.port) || service.port < 1 || service.port > 65535)) {
|
|
142
|
+
throw new Error(`daemon.json.services[${service.name}].port must be an integer from 1 to 65535`);
|
|
143
|
+
}
|
|
144
|
+
if (service.proxy !== undefined) {
|
|
145
|
+
if (!service.proxy || typeof service.proxy !== 'object' || Array.isArray(service.proxy)) {
|
|
146
|
+
throw new Error(`daemon.json.services[${service.name}].proxy must be an object`);
|
|
147
|
+
}
|
|
148
|
+
const unknownProxyFields = Object.keys(service.proxy).filter((key) => !['enabled', 'source', 'endpoint', 'serviceType', 'visibility', 'metadata'].includes(key));
|
|
149
|
+
if (unknownProxyFields.length > 0) {
|
|
150
|
+
throw new Error(`daemon.json.services[${service.name}].proxy contains unsupported fields: ${unknownProxyFields.join(', ')}`);
|
|
151
|
+
}
|
|
152
|
+
if (service.proxy.enabled !== undefined && typeof service.proxy.enabled !== 'boolean') {
|
|
153
|
+
throw new Error(`daemon.json.services[${service.name}].proxy.enabled must be a boolean`);
|
|
154
|
+
}
|
|
155
|
+
if (service.proxy.source !== undefined && !['instance', 'static'].includes(service.proxy.source)) {
|
|
156
|
+
throw new Error(`daemon.json.services[${service.name}].proxy.source must be "instance" or "static"`);
|
|
157
|
+
}
|
|
158
|
+
if (service.proxy.endpoint !== undefined) {
|
|
159
|
+
assertLocalServiceEndpoint(service.proxy.endpoint, `daemon.json.services[${service.name}].proxy.endpoint`);
|
|
160
|
+
}
|
|
161
|
+
if (service.proxy.serviceType !== undefined && !['http', 'websocket', 'ssh'].includes(service.proxy.serviceType)) {
|
|
162
|
+
throw new Error(`daemon.json.services[${service.name}].proxy.serviceType must be http, websocket, or ssh`);
|
|
163
|
+
}
|
|
164
|
+
if (service.proxy.visibility !== undefined
|
|
165
|
+
&& !['public', 'private', 'aun-auth'].includes(service.proxy.visibility)) {
|
|
166
|
+
throw new Error(`daemon.json.services[${service.name}].proxy.visibility must be public, private, or aun-auth`);
|
|
167
|
+
}
|
|
168
|
+
if (service.proxy.metadata !== undefined) {
|
|
169
|
+
if (!service.proxy.metadata || typeof service.proxy.metadata !== 'object' || Array.isArray(service.proxy.metadata)) {
|
|
170
|
+
throw new Error(`daemon.json.services[${service.name}].proxy.metadata must be an object`);
|
|
171
|
+
}
|
|
172
|
+
for (const [key, metadataValue] of Object.entries(service.proxy.metadata)) {
|
|
173
|
+
if (!key || key.length > 256 || SERVICE_METADATA_SENSITIVE_KEY.test(key)
|
|
174
|
+
|| typeof metadataValue !== 'string' || metadataValue.length > 8192) {
|
|
175
|
+
throw new Error(`daemon.json.services[${service.name}].proxy.metadata must contain string key-value pairs`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (service.proxy.enabled === true) {
|
|
180
|
+
if (!['instance', 'static'].includes(service.proxy.source ?? '')) {
|
|
181
|
+
throw new Error(`daemon.json.services[${service.name}].proxy.source must be "instance" or "static"`);
|
|
182
|
+
}
|
|
183
|
+
if (service.proxy.source === 'static' && !service.proxy.endpoint) {
|
|
184
|
+
throw new Error(`daemon.json.services[${service.name}].proxy.endpoint is required when source is "static"`);
|
|
185
|
+
}
|
|
186
|
+
if (!['public', 'private', 'aun-auth'].includes(service.proxy.visibility ?? '')) {
|
|
187
|
+
throw new Error(`daemon.json.services[${service.name}].proxy.visibility must be public, private, or aun-auth`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
78
192
|
if (validateAidDomain && value.aun?.defaultAidDomain !== undefined) {
|
|
79
193
|
const defaultAidDomain = normalizeAidDomain(value.aun.defaultAidDomain, 'daemon.json.aun.defaultAidDomain');
|
|
80
194
|
return { ...value, aun: { ...value.aun, defaultAidDomain } };
|
|
@@ -176,13 +290,31 @@ function deepMergeObject(base, overlay) {
|
|
|
176
290
|
function isPlainObject(v) {
|
|
177
291
|
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
178
292
|
}
|
|
179
|
-
// ── 自动迁移(已删除)─────────────────────────────────────────────────
|
|
180
|
-
//
|
|
181
|
-
// 旧 data/daemon.json / agents/<name>.json → 新结构的一次性迁移已随配置体系 v2 退场
|
|
182
|
-
// (fresh init,不做兼容过渡,见 docs/config-system-design-v2.md §七)。
|
|
183
|
-
// 保留空壳仅为调用点签名兼容——startup 不再调用。
|
|
184
293
|
export function autoMigrateIfNeeded() {
|
|
185
|
-
|
|
294
|
+
const file = resolvePaths().daemonConfig;
|
|
295
|
+
const raw = atomicReadJson(file);
|
|
296
|
+
if (!raw)
|
|
297
|
+
return;
|
|
298
|
+
const needsLegacyMigration = raw.ecweb !== undefined || raw.serviceProxy !== undefined;
|
|
299
|
+
const daemonSchemaVersion = currentVersion('daemon');
|
|
300
|
+
const needsVersionBump = typeof raw.$schema_version !== 'number' || raw.$schema_version < daemonSchemaVersion;
|
|
301
|
+
// Never rewrite a configuration produced by a newer daemon. In particular,
|
|
302
|
+
// a future file may still carry compatibility fields; normalizing those
|
|
303
|
+
// fields and then calling saveDaemonConfig would otherwise force its schema
|
|
304
|
+
// version back to the current one and potentially discard future fields.
|
|
305
|
+
if (typeof raw.$schema_version === 'number' && raw.$schema_version > daemonSchemaVersion) {
|
|
306
|
+
if (needsLegacyMigration) {
|
|
307
|
+
logger.warn(`[migrate] daemon.json schema version ${raw.$schema_version} is newer than supported ${daemonSchemaVersion}; skip legacy service migration`);
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (!needsLegacyMigration && !needsVersionBump)
|
|
312
|
+
return;
|
|
313
|
+
const sanitized = stripLegacyAunGatewayConfig(raw, file, message => logger.warn(message));
|
|
314
|
+
saveDaemonConfig(normalizeDaemonServices(sanitized));
|
|
315
|
+
logger.info(needsLegacyMigration
|
|
316
|
+
? '[migrate] daemon.json ecweb/serviceProxy -> services[]'
|
|
317
|
+
: `[migrate] daemon.json schema version -> ${daemonSchemaVersion}`);
|
|
186
318
|
}
|
|
187
319
|
/**
|
|
188
320
|
* @deprecated Use ConfigManager.read(ConfigTarget.Agent, { self: aid }, { expand: true, cache: true }) instead.
|
|
@@ -304,6 +436,16 @@ export function validateAgentConfig(cfg) {
|
|
|
304
436
|
const errs = [];
|
|
305
437
|
if (!cfg.aid || !isValidAid(cfg.aid))
|
|
306
438
|
errs.push(`invalid aid: ${cfg.aid}`);
|
|
439
|
+
if (cfg.active_baseagent !== undefined && !isSupportedBaseagent(cfg.active_baseagent)) {
|
|
440
|
+
errs.push(`unsupported active_baseagent: ${cfg.active_baseagent} (choose claude/codex/gemini/ecagent)`);
|
|
441
|
+
}
|
|
442
|
+
if (cfg.baseagents && typeof cfg.baseagents === 'object') {
|
|
443
|
+
for (const name of Object.keys(cfg.baseagents)) {
|
|
444
|
+
if (!isSupportedBaseagent(name)) {
|
|
445
|
+
errs.push(`unsupported baseagent config: ${name} (choose claude/codex/gemini/ecagent)`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
307
449
|
if (cfg.lifecycle !== undefined && !isAgentLifecycle(cfg.lifecycle)) {
|
|
308
450
|
errs.push(`invalid lifecycle: ${String(cfg.lifecycle)}`);
|
|
309
451
|
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { agentMdPath, resolvePaths, } from '../paths.js';
|
|
4
|
+
import { loadAgent, saveAgent, } from '../config-store.js';
|
|
5
|
+
import { ConfigTarget, ensureFile as cfgEnsure, initConfigManager, read as cfgRead, write as cfgWrite, } from '../config/config-manager.js';
|
|
6
|
+
import { executeResolvedConfigCommand, } from '../config/config-operation-service.js';
|
|
7
|
+
import { resolveConfigCommand } from '../config/resolved-config-op.js';
|
|
8
|
+
import { AgentReloadBusyError } from './agent-reload-coordinator.js';
|
|
9
|
+
import { logger } from '../utils/logger.js';
|
|
10
|
+
function toPosix(value) {
|
|
11
|
+
if (!value)
|
|
12
|
+
return null;
|
|
13
|
+
return value.replace(/\\/g, '/');
|
|
14
|
+
}
|
|
15
|
+
function readAgentMdIdentity(aid) {
|
|
16
|
+
try {
|
|
17
|
+
const content = fs.readFileSync(agentMdPath(aid), 'utf8');
|
|
18
|
+
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1];
|
|
19
|
+
if (!frontmatter)
|
|
20
|
+
return { name: null, description: null };
|
|
21
|
+
const value = (key) => {
|
|
22
|
+
const match = frontmatter.match(new RegExp(`^${key}:\\s*["']?(.+?)["']?\\s*$`, 'm'));
|
|
23
|
+
return match?.[1]?.trim() || null;
|
|
24
|
+
};
|
|
25
|
+
return { name: value('name'), description: value('description') };
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return { name: null, description: null };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function errorFrom(error, fallback = 'agent operation failed') {
|
|
32
|
+
const message = error instanceof Error ? error.message : String(error || fallback);
|
|
33
|
+
const code = error instanceof AgentReloadBusyError
|
|
34
|
+
? error.code
|
|
35
|
+
: (error?.code || undefined);
|
|
36
|
+
const busyCount = error instanceof AgentReloadBusyError ? error.busyCount : undefined;
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
error: message,
|
|
40
|
+
...(code ? { code } : {}),
|
|
41
|
+
...(busyCount === undefined ? {} : { busyCount }),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Core-owned agent application facade. It contains no role resolution: callers
|
|
46
|
+
* must authorize the operation before invoking it. External CLI callers use
|
|
47
|
+
* the IPC adapter and therefore do not instantiate this class.
|
|
48
|
+
*/
|
|
49
|
+
export class AgentApplicationService {
|
|
50
|
+
deps;
|
|
51
|
+
constructor(deps) {
|
|
52
|
+
this.deps = deps;
|
|
53
|
+
}
|
|
54
|
+
list() {
|
|
55
|
+
return this.deps.registry.list();
|
|
56
|
+
}
|
|
57
|
+
/** Raw registry projection used by the evolagent.show IPC adapter. */
|
|
58
|
+
info(aid) {
|
|
59
|
+
// Keep the same race guard as the legacy IPC adapter: a list entry is not
|
|
60
|
+
// enough to claim that the live runtime handle still exists.
|
|
61
|
+
if (!this.deps.registry.get(aid))
|
|
62
|
+
return null;
|
|
63
|
+
return this.deps.registry.list().find(agent => agent.aid === aid) ?? null;
|
|
64
|
+
}
|
|
65
|
+
async show(aid) {
|
|
66
|
+
const info = this.info(aid);
|
|
67
|
+
const agent = this.deps.registry.get(aid);
|
|
68
|
+
if (!info || !agent)
|
|
69
|
+
return { ok: false, error: `Agent "${aid}" not found`, code: 'NOT_FOUND' };
|
|
70
|
+
const aidState = (this.deps.aunAids?.() ?? []).find(state => (state.aid === aid || state.agentName === aid));
|
|
71
|
+
const stats = (this.deps.aunAidStats?.() ?? []).find(snapshot => snapshot.aid === aid);
|
|
72
|
+
const connection = aidState
|
|
73
|
+
? {
|
|
74
|
+
status: aidState.status || 'unknown',
|
|
75
|
+
uptime_ms: (aidState.status === 'connected' && aidState.lastConnectedAt)
|
|
76
|
+
? Date.now() - aidState.lastConnectedAt
|
|
77
|
+
: null,
|
|
78
|
+
reconnect_count: aidState.reconnectCount ?? 0,
|
|
79
|
+
messages_received: stats?.messagesReceived ?? 0,
|
|
80
|
+
messages_sent: stats?.messagesSent ?? 0,
|
|
81
|
+
bytes_received: stats?.bytesReceived ?? 0,
|
|
82
|
+
bytes_sent: stats?.bytesSent ?? 0,
|
|
83
|
+
last_received_at: stats?.lastReceivedAt ? new Date(stats.lastReceivedAt).toISOString() : null,
|
|
84
|
+
last_sent_at: stats?.lastSentAt ? new Date(stats.lastSentAt).toISOString() : null,
|
|
85
|
+
unique_peer_count: stats?.uniquePeerCount ?? 0,
|
|
86
|
+
}
|
|
87
|
+
: null;
|
|
88
|
+
const config = agent.config;
|
|
89
|
+
const identity = readAgentMdIdentity(aid);
|
|
90
|
+
const paths = resolvePaths();
|
|
91
|
+
return {
|
|
92
|
+
ok: true,
|
|
93
|
+
aid,
|
|
94
|
+
status: info.status || 'stopped',
|
|
95
|
+
identity,
|
|
96
|
+
config: {
|
|
97
|
+
baseagent: info.baseagent || null,
|
|
98
|
+
model: info.model || null,
|
|
99
|
+
effort: info.effort || null,
|
|
100
|
+
chatmode: config?.chatmode ?? null,
|
|
101
|
+
channels: info.channels ?? [],
|
|
102
|
+
},
|
|
103
|
+
connection,
|
|
104
|
+
sessions: {
|
|
105
|
+
active: info.activeSessions ?? 0,
|
|
106
|
+
last_activity: info.lastActivity ? new Date(info.lastActivity).toISOString() : null,
|
|
107
|
+
},
|
|
108
|
+
paths: {
|
|
109
|
+
config: toPosix(path.join(paths.agentsDir, aid, 'config.json')),
|
|
110
|
+
agent_md: toPosix(agentMdPath(aid)),
|
|
111
|
+
project: toPosix(info.projectPath || null),
|
|
112
|
+
data: toPosix(path.join(paths.agentsDir, aid, 'data')),
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async create(options) {
|
|
117
|
+
return await this.deps.create(options);
|
|
118
|
+
}
|
|
119
|
+
async load(aid) {
|
|
120
|
+
try {
|
|
121
|
+
await this.deps.hotLoad(aid);
|
|
122
|
+
return { ok: true, aid };
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
return errorFrom(error);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async reload(aid, options = {}) {
|
|
129
|
+
try {
|
|
130
|
+
if (!aid) {
|
|
131
|
+
if (options.force) {
|
|
132
|
+
return { ok: false, code: 'INVALID_ARGS', error: '--force requires a target Agent AID' };
|
|
133
|
+
}
|
|
134
|
+
return { ok: true, results: await this.deps.resync() };
|
|
135
|
+
}
|
|
136
|
+
await this.deps.reload(aid, options);
|
|
137
|
+
return { ok: true };
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
return errorFrom(error);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async resync() {
|
|
144
|
+
try {
|
|
145
|
+
return { ok: true, results: await this.deps.resync() };
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
return errorFrom(error);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async start(aid) {
|
|
152
|
+
try {
|
|
153
|
+
if (!this.deps.registry.startAgent)
|
|
154
|
+
return { ok: false, error: 'startAgent unavailable', code: 'NOT_CONFIGURED' };
|
|
155
|
+
await this.deps.registry.startAgent(aid, this.deps.hooks);
|
|
156
|
+
return { ok: true, aid };
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
return errorFrom(error);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async stop(aid) {
|
|
163
|
+
try {
|
|
164
|
+
if (!this.deps.registry.stopAgent)
|
|
165
|
+
return { ok: false, error: 'stopAgent unavailable', code: 'NOT_CONFIGURED' };
|
|
166
|
+
await this.deps.registry.stopAgent(aid, this.deps.hooks);
|
|
167
|
+
return { ok: true, aid };
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
return errorFrom(error);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async setConfig(aid, key, rawValue) {
|
|
174
|
+
try {
|
|
175
|
+
initConfigManager();
|
|
176
|
+
const resolved = resolveConfigCommand(['config', 'set', key, rawValue, '--self', aid]);
|
|
177
|
+
if (!resolved.ok)
|
|
178
|
+
return { ok: false, code: resolved.code, error: resolved.reason };
|
|
179
|
+
const result = executeResolvedConfigCommand(resolved.command);
|
|
180
|
+
if (!result.ok)
|
|
181
|
+
return { ok: false, code: result.code, error: result.error };
|
|
182
|
+
if (result.subcommand !== 'set') {
|
|
183
|
+
return { ok: false, code: 'UNEXPECTED_RESULT', error: 'config set returned an unexpected result' };
|
|
184
|
+
}
|
|
185
|
+
const reloadResult = await this.reload(aid);
|
|
186
|
+
return {
|
|
187
|
+
ok: true,
|
|
188
|
+
aid,
|
|
189
|
+
key,
|
|
190
|
+
value: result.value,
|
|
191
|
+
reloaded: reloadResult.ok,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
return errorFrom(error);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async enable(aid, force = false) {
|
|
199
|
+
return await this.setEnabled(aid, true, force);
|
|
200
|
+
}
|
|
201
|
+
async disable(aid, force = false) {
|
|
202
|
+
return await this.setEnabled(aid, false, force);
|
|
203
|
+
}
|
|
204
|
+
async setEnabled(aid, enabled, force) {
|
|
205
|
+
let config;
|
|
206
|
+
try {
|
|
207
|
+
config = loadAgent(aid);
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
return errorFrom(error, 'failed to read agent config');
|
|
211
|
+
}
|
|
212
|
+
if (!config)
|
|
213
|
+
return { ok: false, error: `Agent "${aid}" not found`, code: 'NOT_FOUND' };
|
|
214
|
+
const previousEnabled = config.enabled;
|
|
215
|
+
try {
|
|
216
|
+
config.enabled = enabled;
|
|
217
|
+
saveAgent(config);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
return errorFrom(error, 'failed to save agent config');
|
|
221
|
+
}
|
|
222
|
+
const result = await this.reload(aid, { force });
|
|
223
|
+
if (!result.ok && result.code === 'AGENT_BUSY') {
|
|
224
|
+
if (previousEnabled === undefined)
|
|
225
|
+
delete config.enabled;
|
|
226
|
+
else
|
|
227
|
+
config.enabled = previousEnabled;
|
|
228
|
+
try {
|
|
229
|
+
saveAgent(config);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
logger.error(`[agent-service] failed to roll back ${aid} enabled state: ${error instanceof Error ? error.message : String(error)}`);
|
|
233
|
+
return errorFrom(error, 'failed to roll back agent config');
|
|
234
|
+
}
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
// Keep the historical CLI contract: a persisted toggle succeeds even when
|
|
238
|
+
// the daemon is unavailable or a best-effort hot reload fails.
|
|
239
|
+
return { ok: true, aid, enabled, reloaded: result.ok };
|
|
240
|
+
}
|
|
241
|
+
async delete(aid, purge = false) {
|
|
242
|
+
const paths = resolvePaths();
|
|
243
|
+
const agentDir = path.join(paths.agentsDir, aid);
|
|
244
|
+
const configPath = path.join(agentDir, 'config.json');
|
|
245
|
+
if (!fs.existsSync(configPath))
|
|
246
|
+
return { ok: false, error: `Agent "${aid}" not found`, code: 'NOT_FOUND' };
|
|
247
|
+
try {
|
|
248
|
+
if (purge) {
|
|
249
|
+
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
fs.unlinkSync(configPath);
|
|
253
|
+
const { removeCreateStatus } = await import('./message/create-status.js');
|
|
254
|
+
removeCreateStatus(agentDir);
|
|
255
|
+
}
|
|
256
|
+
let stopped = false;
|
|
257
|
+
try {
|
|
258
|
+
const result = await this.resync();
|
|
259
|
+
if (result.ok)
|
|
260
|
+
stopped = result.results.some(item => item.includes(aid));
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
logger.warn(`[agent-service] resync after delete ${aid} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
264
|
+
}
|
|
265
|
+
return { ok: true, aid, purged: purge, stopped };
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
return errorFrom(error);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/** Raw config read/write helpers reserved for Core-owned command handlers. */
|
|
272
|
+
readConfig(aid) {
|
|
273
|
+
return cfgRead(ConfigTarget.Agent, { self: aid });
|
|
274
|
+
}
|
|
275
|
+
writeConfig(aid, config) {
|
|
276
|
+
cfgEnsure(ConfigTarget.Agent, { self: aid });
|
|
277
|
+
cfgWrite(ConfigTarget.Agent, config, { self: aid });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -5,6 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
// this check also covers future rotation formats without changing the audit.
|
|
6
6
|
const STRUCTURED_LOG_RE = /^(?:daemon|events|channel-in|channel-out|messages|command-audit)(?:-[^.]+)?\.log$/;
|
|
7
7
|
const MIXED_TEXT_LOG_RE = /^daemon(?:-[^.]+)?\.log$/;
|
|
8
|
+
const LEGACY_JSONL_FILE = /\.jsonl$/i;
|
|
8
9
|
/** Validate and de-duplicate structured event streams by lifecycle identity. */
|
|
9
10
|
export function inspectStructuredLogs(logDir, options = {}) {
|
|
10
11
|
const issues = [];
|
|
@@ -157,3 +158,104 @@ function canonicalEventType(value) {
|
|
|
157
158
|
function hasContextValue(value) {
|
|
158
159
|
return value !== undefined && value !== null && value !== '' && value !== 'unknown';
|
|
159
160
|
}
|
|
161
|
+
function nonEmptyLogLines(content) {
|
|
162
|
+
return content.split(/\r?\n/).filter(line => line.length > 0);
|
|
163
|
+
}
|
|
164
|
+
/** Move legacy agents/<aid>/logs/*.jsonl into the deployment-level logs directory. */
|
|
165
|
+
export function migrateLegacyAgentJsonlLogs(agentsDir, logsDir) {
|
|
166
|
+
const result = {
|
|
167
|
+
filesRemoved: 0, linesMerged: 0, duplicateLinesSkipped: 0, directoriesRemoved: 0,
|
|
168
|
+
};
|
|
169
|
+
const sourcesByName = new Map();
|
|
170
|
+
let agents;
|
|
171
|
+
try {
|
|
172
|
+
agents = fs.readdirSync(agentsDir, { withFileTypes: true });
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
for (const agent of agents) {
|
|
178
|
+
if (!agent.isDirectory())
|
|
179
|
+
continue;
|
|
180
|
+
const logDir = path.join(agentsDir, agent.name, 'logs');
|
|
181
|
+
let entries;
|
|
182
|
+
try {
|
|
183
|
+
entries = fs.readdirSync(logDir, { withFileTypes: true });
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
for (const entry of entries) {
|
|
189
|
+
if (!entry.isFile() || !LEGACY_JSONL_FILE.test(entry.name))
|
|
190
|
+
continue;
|
|
191
|
+
const sources = sourcesByName.get(entry.name) ?? [];
|
|
192
|
+
sources.push({ filePath: path.join(logDir, entry.name), logDir });
|
|
193
|
+
sourcesByName.set(entry.name, sources);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
for (const [fileName, sources] of sourcesByName) {
|
|
197
|
+
const targetPath = path.join(logsDir, fileName);
|
|
198
|
+
let targetContent = '';
|
|
199
|
+
try {
|
|
200
|
+
if (fs.existsSync(targetPath))
|
|
201
|
+
targetContent = fs.readFileSync(targetPath, 'utf8');
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const targetLineCounts = new Map();
|
|
207
|
+
for (const line of nonEmptyLogLines(targetContent))
|
|
208
|
+
targetLineCounts.set(line, (targetLineCounts.get(line) ?? 0) + 1);
|
|
209
|
+
const readableSources = [];
|
|
210
|
+
const missingLines = [];
|
|
211
|
+
for (const source of sources) {
|
|
212
|
+
let sourceLines;
|
|
213
|
+
try {
|
|
214
|
+
sourceLines = nonEmptyLogLines(fs.readFileSync(source.filePath, 'utf8'));
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
readableSources.push(source);
|
|
220
|
+
const sourceLineCounts = new Map();
|
|
221
|
+
for (const line of sourceLines)
|
|
222
|
+
sourceLineCounts.set(line, (sourceLineCounts.get(line) ?? 0) + 1);
|
|
223
|
+
for (const [line, count] of sourceLineCounts) {
|
|
224
|
+
const alreadyPresent = targetLineCounts.get(line) ?? 0;
|
|
225
|
+
const missing = Math.max(0, count - alreadyPresent);
|
|
226
|
+
for (let index = 0; index < missing; index += 1)
|
|
227
|
+
missingLines.push(line);
|
|
228
|
+
if (missing > 0)
|
|
229
|
+
targetLineCounts.set(line, count);
|
|
230
|
+
result.duplicateLinesSkipped += count - missing;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
if (missingLines.length > 0) {
|
|
235
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
236
|
+
const separator = targetContent.length > 0 && !targetContent.endsWith('\n') ? '\n' : '';
|
|
237
|
+
fs.appendFileSync(targetPath, `${separator}${missingLines.join('\n')}\n`, 'utf8');
|
|
238
|
+
result.linesMerged += missingLines.length;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
for (const source of readableSources) {
|
|
245
|
+
try {
|
|
246
|
+
fs.unlinkSync(source.filePath);
|
|
247
|
+
result.filesRemoved += 1;
|
|
248
|
+
}
|
|
249
|
+
catch { }
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const legacyDirs = new Set([...sourcesByName.values()].flat().map(source => source.logDir));
|
|
253
|
+
for (const logDir of legacyDirs) {
|
|
254
|
+
try {
|
|
255
|
+
fs.rmdirSync(logDir);
|
|
256
|
+
result.directoriesRemoved += 1;
|
|
257
|
+
}
|
|
258
|
+
catch { }
|
|
259
|
+
}
|
|
260
|
+
return result;
|
|
261
|
+
}
|