evolcore 0.0.20 → 0.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +58 -9
  3. package/dist/agents/baseagent.js +10 -6
  4. package/dist/agents/claude-runner.js +379 -108
  5. package/dist/agents/codex-app-server-client.js +10 -2
  6. package/dist/agents/codex-runner.js +402 -135
  7. package/dist/agents/ecagent-runner.js +171 -61
  8. package/dist/agents/gemini-runner.js +130 -30
  9. package/dist/agents/request-identity.js +25 -0
  10. package/dist/agents/runner-types.js +19 -0
  11. package/dist/aun/aid/agentmd.js +59 -2
  12. package/dist/aun/aid/identity.js +4 -1
  13. package/dist/aun/aid/index.js +1 -1
  14. package/dist/aun/msg/group.js +72 -6
  15. package/dist/aun/msg/history.js +213 -36
  16. package/dist/aun/msg/managed-operation.js +58 -9
  17. package/dist/aun/msg/p2p.js +5 -0
  18. package/dist/aun/outbox.js +182 -80
  19. package/dist/aun/service-proxy.js +43 -25
  20. package/dist/channels/aun.js +409 -88
  21. package/dist/channels/daemon.js +6 -1
  22. package/dist/cli/agent-command.js +4 -3
  23. package/dist/cli/agent.js +66 -56
  24. package/dist/cli/aun-commands.js +177 -42
  25. package/dist/cli/command-log.js +10 -11
  26. package/dist/cli/contact.js +1 -0
  27. package/dist/cli/daemon-commands.js +69 -115
  28. package/dist/cli/init.js +27 -15
  29. package/dist/cli/task-context.js +46 -0
  30. package/dist/cli/trigger-command.js +1 -1
  31. package/dist/cli/watch-logs.js +10 -3
  32. package/dist/config/builtin-roles.js +1 -0
  33. package/dist/config/config-field-policy.js +16 -5
  34. package/dist/config/config-manager.js +135 -17
  35. package/dist/config/contact-operation-service.js +32 -1
  36. package/dist/config/contact-request-service.js +44 -0
  37. package/dist/config/daemon-services.js +186 -0
  38. package/dist/config/gateway-config.js +20 -9
  39. package/dist/config/role-service.js +54 -3
  40. package/dist/config/schema-migration.js +550 -0
  41. package/dist/config-store.js +151 -9
  42. package/dist/core/agent-application-service.js +279 -0
  43. package/dist/core/audit/log-integrity.js +102 -0
  44. package/dist/core/auth/agent-delegation.js +31 -1
  45. package/dist/core/auth/auth-gateway.js +33 -4
  46. package/dist/core/auth/authorization-audit.js +150 -2
  47. package/dist/core/auth/operation-authorizer.js +41 -1
  48. package/dist/core/auth/operation-catalog.js +9 -1
  49. package/dist/core/bootstrap-service.js +6 -2
  50. package/dist/core/causation/aun-association.js +7 -4
  51. package/dist/core/command/agent-control.js +56 -16
  52. package/dist/core/command/command-handler.js +290 -44
  53. package/dist/core/command/connect-menu.js +3 -4
  54. package/dist/core/command/group-menu.js +5 -7
  55. package/dist/core/command/menu-handler.js +279 -80
  56. package/dist/core/command/role-menu.js +21 -11
  57. package/dist/core/command/slash-gate.js +85 -18
  58. package/dist/core/command/slash-handler.js +350 -32
  59. package/dist/core/event-catalog.js +5 -0
  60. package/dist/core/evolagent.js +4 -0
  61. package/dist/core/handoff/dispatcher.js +4 -0
  62. package/dist/core/handoff/runtime.js +10 -0
  63. package/dist/core/handoff/store.js +32 -9
  64. package/dist/core/inference/text-inference.js +7 -15
  65. package/dist/core/message/im-renderer.js +83 -84
  66. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  67. package/dist/core/message/message-bridge.js +124 -10
  68. package/dist/core/message/message-log.js +14 -7
  69. package/dist/core/message/message-queue.js +206 -16
  70. package/dist/core/message/message-utils.js +12 -5
  71. package/dist/core/message/response-engine.js +486 -68
  72. package/dist/core/message/send-receipt.js +1 -0
  73. package/dist/core/message/stream-debouncer.js +9 -2
  74. package/dist/core/model/model-catalog.js +23 -15
  75. package/dist/core/model/model-diagnostics.js +28 -10
  76. package/dist/core/permission/approval-gateway.js +180 -6
  77. package/dist/core/permission/ec-command-parser.js +410 -54
  78. package/dist/core/permission/mode.js +18 -3
  79. package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
  80. package/dist/core/permission/readonly-shell-query.js +263 -9
  81. package/dist/core/permission/sandbox-runtime.js +159 -1
  82. package/dist/core/permission/tool-policy.js +575 -21
  83. package/dist/core/session/session-fs-store.js +154 -5
  84. package/dist/core/session/session-manager.js +299 -30
  85. package/dist/core/session/session-renew.js +19 -12
  86. package/dist/core/session/session-turn-coordinator.js +11 -4
  87. package/dist/eck/kit-renderer.js +1 -1
  88. package/dist/index.js +253 -46
  89. package/dist/ipc.js +374 -24
  90. package/dist/paths.js +64 -7
  91. package/dist/response-system/context-builder.js +1 -7
  92. package/dist/trigger/anomaly-store.js +1 -0
  93. package/dist/trigger/feedback.js +56 -5
  94. package/dist/trigger/history.js +79 -4
  95. package/dist/trigger/legacy-session-history.js +2 -2
  96. package/dist/trigger/parser.js +3 -2
  97. package/dist/trigger/validation.js +6 -1
  98. package/dist/utils/atomic-write.js +27 -0
  99. package/dist/utils/ecweb-utils.js +16 -2
  100. package/dist/utils/error-utils.js +4 -1
  101. package/dist/utils/logger.js +21 -2
  102. package/dist/utils/process-tree-stats.js +24 -4
  103. package/dist/utils/process-tree-worker.js +31 -0
  104. package/dist/utils/project-path.js +1 -2
  105. package/kits/docs/INDEX.md +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -1
  107. package/kits/docs/evolcore/contact.md +7 -1
  108. package/kits/docs/evolcore/msg.md +16 -0
  109. package/kits/schemas/_meta.json +4 -2
  110. package/kits/schemas/agent-config.schema.11.json +13 -0
  111. package/kits/schemas/daemon.schema.5.json +0 -1
  112. package/kits/schemas/daemon.schema.6.json +131 -0
  113. package/kits/schemas/defaults.schema.5.json +15 -3
  114. package/kits/schemas/migrations/README.md +3 -1
  115. package/kits/schemas/relation-config.schema.8.json +13 -0
  116. package/kits/schemas/role-config.schema.1.json +1 -2
  117. package/kits/schemas/single-session.schema.3.json +32 -0
  118. package/kits/templates/roles/admin.json +1 -0
  119. package/kits/templates/roles/member.json +1 -0
  120. package/kits/templates/roles/visitor.json +1 -0
  121. package/package.json +6 -3
  122. package/skills/eclink/SKILL.md +2 -0
  123. package/dist/config/aun-gateway-config.js +0 -2
@@ -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
- return validateDaemonConfig(stripLegacyAunGatewayConfig(raw ?? {}, resolvePaths().daemonConfig, message => logger.warn(message)), false);
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
- atomicWriteJson(configPath, validateDaemonConfig(stripLegacyAunGatewayConfig(value, configPath, message => logger.warn(message)), validateAidDomain));
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
- /* no-op: legacy migration removed (config-system v2 fresh init) */
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
+ }
@@ -8,6 +8,9 @@ export class AgentDelegationRegistry {
8
8
  activeHashBySession = new Map();
9
9
  approvedCommands = new Map();
10
10
  issue(input) {
11
+ if (input.executionIdentity && !isValidFullAccessExecutionIdentity(input.executionIdentity)) {
12
+ throw new Error('invalid fullaccess task execution identity');
13
+ }
11
14
  this.revokeSession(input.sessionId);
12
15
  const token = crypto.randomBytes(32).toString('base64url');
13
16
  const tokenHash = hashDelegationToken(token);
@@ -134,7 +137,7 @@ export function authorizeDelegatedAunMsgSend(registry, input) {
134
137
  if (!validation.ok)
135
138
  return validation;
136
139
  const grant = validation.grant;
137
- if (grant.selfAid !== input.aid) {
140
+ if (grant.selfAid !== input.aid && !hasTrustedFullAccessDelegation(grant)) {
138
141
  return { ok: false, code: 'INVALID_DELEGATION', reason: 'Delegation self agent does not match sender' };
139
142
  }
140
143
  if (grant.messageId && grant.messageId !== input.messageId) {
@@ -157,6 +160,7 @@ export function authorizeDelegatedAunMsgSend(registry, input) {
157
160
  conversationId: channelId,
158
161
  processOwners: [],
159
162
  fromControlChannel: false,
163
+ ...trustedExecutionIdentityInput(grant),
160
164
  });
161
165
  const subject = { ...builtSubject, peerKey: grant.peerKey };
162
166
  const decision = authorizeOperation({
@@ -180,6 +184,32 @@ export function authorizeDelegatedAunMsgSend(registry, input) {
180
184
  }
181
185
  return { ok: true, grant };
182
186
  }
187
+ /** Build the only AuthGateway input shape accepted for a task-held fullaccess claim. */
188
+ export function trustedExecutionIdentityInput(grant) {
189
+ return grant.executionIdentity && isValidFullAccessExecutionIdentity(grant.executionIdentity)
190
+ ? {
191
+ trustedProcessRole: 'fullaccess-run',
192
+ trustedFullAccessAuthorization: grant.executionIdentity,
193
+ }
194
+ : {};
195
+ }
196
+ export function hasTrustedFullAccessDelegation(grant) {
197
+ return !!grant.executionIdentity && isValidFullAccessExecutionIdentity(grant.executionIdentity);
198
+ }
199
+ function isValidFullAccessExecutionIdentity(identity) {
200
+ if (identity.permissionMode !== 'fullaccess'
201
+ || identity.processRole !== 'fullaccess-run'
202
+ || identity.dataScope !== 'daemon')
203
+ return false;
204
+ if (identity.source === 'fullaccess-command') {
205
+ return typeof identity.authorizedBy === 'string' && identity.authorizedBy.length > 0;
206
+ }
207
+ return identity.source === 'trigger'
208
+ && typeof identity.authorizedBy === 'string' && identity.authorizedBy.length > 0
209
+ && typeof identity.triggerId === 'string' && identity.triggerId.length > 0
210
+ && typeof identity.runId === 'string' && identity.runId.length > 0
211
+ && typeof identity.attemptId === 'string' && identity.attemptId.length > 0;
212
+ }
183
213
  function hashDelegationToken(token) {
184
214
  return crypto.createHash('sha256').update(token).digest('hex');
185
215
  }