evolcore 0.0.19 → 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 (136) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/README.md +60 -9
  3. package/bin/install-codex-managed-hooks.mjs +61 -0
  4. package/dist/agents/baseagent.js +10 -6
  5. package/dist/agents/claude-runner.js +383 -111
  6. package/dist/agents/codex-app-server-client.js +10 -2
  7. package/dist/agents/codex-runner.js +405 -137
  8. package/dist/agents/ecagent-runner.js +174 -63
  9. package/dist/agents/gemini-runner.js +130 -30
  10. package/dist/agents/request-identity.js +25 -0
  11. package/dist/agents/runner-types.js +19 -0
  12. package/dist/aun/aid/agentmd.js +66 -2
  13. package/dist/aun/aid/identity.js +4 -1
  14. package/dist/aun/aid/index.js +1 -1
  15. package/dist/aun/msg/group.js +86 -9
  16. package/dist/aun/msg/history.js +213 -36
  17. package/dist/aun/msg/managed-operation.js +58 -9
  18. package/dist/aun/msg/p2p.js +26 -11
  19. package/dist/aun/outbox.js +295 -68
  20. package/dist/aun/service-proxy.js +43 -25
  21. package/dist/channels/aun.js +1004 -273
  22. package/dist/channels/daemon.js +6 -1
  23. package/dist/cli/agent-command.js +4 -3
  24. package/dist/cli/agent.js +66 -56
  25. package/dist/cli/aun-commands.js +177 -42
  26. package/dist/cli/command-log.js +10 -11
  27. package/dist/cli/contact.js +1 -0
  28. package/dist/cli/daemon-commands.js +81 -121
  29. package/dist/cli/index.js +1 -0
  30. package/dist/cli/init.js +76 -24
  31. package/dist/cli/restart-monitor.js +3 -3
  32. package/dist/cli/task-context.js +46 -0
  33. package/dist/cli/trigger-command.js +1 -1
  34. package/dist/cli/watch-logs.js +10 -3
  35. package/dist/config/builtin-roles.js +1 -0
  36. package/dist/config/config-field-policy.js +16 -5
  37. package/dist/config/config-manager.js +226 -24
  38. package/dist/config/config-operation-service.js +1 -2
  39. package/dist/config/contact-operation-service.js +32 -1
  40. package/dist/config/contact-request-service.js +44 -0
  41. package/dist/config/daemon-services.js +186 -0
  42. package/dist/config/gateway-config.js +29 -16
  43. package/dist/config/lifecycle.js +16 -5
  44. package/dist/config/role-service.js +54 -3
  45. package/dist/config/schema-migration.js +550 -0
  46. package/dist/config-store.js +161 -12
  47. package/dist/core/agent-application-service.js +279 -0
  48. package/dist/core/audit/log-integrity.js +102 -0
  49. package/dist/core/auth/agent-delegation.js +31 -1
  50. package/dist/core/auth/auth-gateway.js +33 -4
  51. package/dist/core/auth/authorization-audit.js +155 -4
  52. package/dist/core/auth/operation-authorizer.js +41 -1
  53. package/dist/core/auth/operation-catalog.js +9 -1
  54. package/dist/core/bootstrap-messages.js +2 -2
  55. package/dist/core/bootstrap-service.js +27 -38
  56. package/dist/core/causation/aun-association.js +7 -4
  57. package/dist/core/channel-loader.js +0 -2
  58. package/dist/core/command/agent-control.js +56 -16
  59. package/dist/core/command/command-handler.js +290 -44
  60. package/dist/core/command/connect-menu.js +3 -4
  61. package/dist/core/command/group-menu.js +5 -7
  62. package/dist/core/command/menu-handler.js +279 -80
  63. package/dist/core/command/role-menu.js +21 -11
  64. package/dist/core/command/slash-gate.js +85 -18
  65. package/dist/core/command/slash-handler.js +350 -32
  66. package/dist/core/event-catalog.js +5 -0
  67. package/dist/core/evolagent.js +9 -4
  68. package/dist/core/handoff/dispatcher.js +4 -0
  69. package/dist/core/handoff/runtime.js +10 -0
  70. package/dist/core/handoff/store.js +32 -9
  71. package/dist/core/inference/text-inference.js +7 -15
  72. package/dist/core/message/im-renderer.js +83 -84
  73. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  74. package/dist/core/message/message-bridge.js +124 -10
  75. package/dist/core/message/message-log.js +14 -7
  76. package/dist/core/message/message-queue.js +206 -16
  77. package/dist/core/message/message-utils.js +12 -5
  78. package/dist/core/message/response-engine.js +495 -72
  79. package/dist/core/message/send-receipt.js +1 -0
  80. package/dist/core/message/stream-debouncer.js +9 -2
  81. package/dist/core/model/model-catalog.js +23 -15
  82. package/dist/core/model/model-diagnostics.js +28 -10
  83. package/dist/core/permission/approval-gateway.js +180 -6
  84. package/dist/core/permission/ec-command-parser.js +465 -56
  85. package/dist/core/permission/mode.js +18 -3
  86. package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
  87. package/dist/core/permission/readonly-shell-query.js +263 -9
  88. package/dist/core/permission/sandbox-runtime.js +205 -13
  89. package/dist/core/permission/tool-policy.js +673 -62
  90. package/dist/core/relation/peer-identity.js +18 -0
  91. package/dist/core/session/session-fs-store.js +154 -5
  92. package/dist/core/session/session-manager.js +299 -30
  93. package/dist/core/session/session-renew.js +19 -12
  94. package/dist/core/session/session-turn-coordinator.js +11 -4
  95. package/dist/eck/kit-renderer.js +18 -9
  96. package/dist/index.js +283 -65
  97. package/dist/ipc.js +374 -24
  98. package/dist/paths.js +64 -7
  99. package/dist/response-system/context-builder.js +1 -7
  100. package/dist/trigger/anomaly-store.js +1 -0
  101. package/dist/trigger/feedback.js +56 -5
  102. package/dist/trigger/history.js +79 -4
  103. package/dist/trigger/legacy-session-history.js +2 -2
  104. package/dist/trigger/parser.js +3 -2
  105. package/dist/trigger/validation.js +6 -1
  106. package/dist/utils/atomic-write.js +27 -0
  107. package/dist/utils/ecweb-utils.js +16 -2
  108. package/dist/utils/error-utils.js +4 -1
  109. package/dist/utils/logger.js +21 -2
  110. package/dist/utils/process-tree-stats.js +24 -4
  111. package/dist/utils/process-tree-worker.js +31 -0
  112. package/dist/utils/project-path.js +1 -2
  113. package/dist/utils/stats.js +52 -18
  114. package/dist/utils/welcome.js +2 -2
  115. package/kits/docs/INDEX.md +1 -1
  116. package/kits/docs/evolcore/INDEX.md +1 -1
  117. package/kits/docs/evolcore/contact.md +7 -1
  118. package/kits/docs/evolcore/msg.md +16 -0
  119. package/kits/rules/01-overview.md +1 -1
  120. package/kits/rules/03-identity.md +1 -1
  121. package/kits/rules/05-venue.md +1 -1
  122. package/kits/schemas/_meta.json +10 -5
  123. package/kits/schemas/agent-config.schema.10.json +2 -1
  124. package/kits/schemas/agent-config.schema.11.json +421 -0
  125. package/kits/schemas/daemon.schema.5.json +135 -0
  126. package/kits/schemas/daemon.schema.6.json +131 -0
  127. package/kits/schemas/defaults.schema.5.json +119 -0
  128. package/kits/schemas/migrations/README.md +3 -1
  129. package/kits/schemas/relation-config.schema.8.json +13 -0
  130. package/kits/schemas/role-config.schema.1.json +1 -2
  131. package/kits/schemas/single-session.schema.3.json +32 -0
  132. package/kits/templates/roles/admin.json +1 -0
  133. package/kits/templates/roles/member.json +1 -0
  134. package/kits/templates/roles/visitor.json +1 -0
  135. package/package.json +7 -3
  136. package/skills/eclink/SKILL.md +2 -0
@@ -0,0 +1,186 @@
1
+ const SERVICE_TYPES = new Set(['http', 'websocket', 'ssh']);
2
+ const SERVICE_VISIBILITIES = new Set(['public', 'private', 'aun-auth']);
3
+ export const DEFAULT_ECWEB_PORT = 42705;
4
+ function isRecord(value) {
5
+ return !!value && typeof value === 'object' && !Array.isArray(value);
6
+ }
7
+ /**
8
+ * Convert the former `ecweb` + `serviceProxy` process configuration into the
9
+ * service-instance model. The function is intentionally idempotent and does
10
+ * not mutate its input.
11
+ */
12
+ export function normalizeDaemonServices(raw, warn) {
13
+ if (!isRecord(raw))
14
+ return raw;
15
+ const next = { ...raw };
16
+ const hasLegacyEcweb = Object.prototype.hasOwnProperty.call(raw, 'ecweb');
17
+ const hasLegacyProxy = Object.prototype.hasOwnProperty.call(raw, 'serviceProxy');
18
+ const hasCanonicalServices = Object.prototype.hasOwnProperty.call(raw, 'services');
19
+ const services = [];
20
+ if (hasCanonicalServices) {
21
+ if (!Array.isArray(raw.services)) {
22
+ throw new Error('daemon.json.services must be an array');
23
+ }
24
+ for (const [index, value] of raw.services.entries()) {
25
+ if (!isRecord(value)) {
26
+ throw new Error(`daemon.json.services[${index}] must be an object`);
27
+ }
28
+ if (typeof value.name !== 'string' || value.name.trim() === '') {
29
+ throw new Error(`daemon.json.services[${index}].name must be a non-empty string`);
30
+ }
31
+ // Preserve the canonical object for schema validation. In particular,
32
+ // do not trim/drop fields here, otherwise malformed new-format config
33
+ // could be silently rewritten into a different valid-looking shape.
34
+ services.push({ ...value });
35
+ }
36
+ }
37
+ if (hasLegacyEcweb && !isRecord(raw.ecweb)) {
38
+ throw new Error('daemon.json.ecweb must be an object before it can be migrated');
39
+ }
40
+ if (hasLegacyProxy && !isRecord(raw.serviceProxy)) {
41
+ throw new Error('daemon.json.serviceProxy must be an object before it can be migrated');
42
+ }
43
+ const legacyEcweb = hasLegacyEcweb ? raw.ecweb : undefined;
44
+ const legacyProxy = hasLegacyProxy ? raw.serviceProxy : undefined;
45
+ if (legacyEcweb?.enabled !== undefined && typeof legacyEcweb.enabled !== 'boolean') {
46
+ throw new Error('daemon.json.ecweb.enabled must be a boolean before it can be migrated');
47
+ }
48
+ if (legacyEcweb?.port !== undefined && typeof legacyEcweb.port !== 'number') {
49
+ throw new Error('daemon.json.ecweb.port must be a number before it can be migrated');
50
+ }
51
+ if (legacyProxy?.enabled !== undefined && typeof legacyProxy.enabled !== 'boolean') {
52
+ throw new Error('daemon.json.serviceProxy.enabled must be a boolean before it can be migrated');
53
+ }
54
+ const legacyProxyServices = [];
55
+ if (legacyProxy?.services !== undefined) {
56
+ if (!Array.isArray(legacyProxy.services)) {
57
+ throw new Error('daemon.json.serviceProxy.services must be an array before it can be migrated');
58
+ }
59
+ for (const [index, value] of legacyProxy.services.entries()) {
60
+ if (!isRecord(value)) {
61
+ throw new Error(`daemon.json.serviceProxy.services[${index}] cannot be represented by source="instance"`);
62
+ }
63
+ if (value.enabled !== undefined && typeof value.enabled !== 'boolean') {
64
+ throw new Error(`daemon.json.serviceProxy.services[${index}].enabled must be a boolean before it can be migrated`);
65
+ }
66
+ if (value.visibility !== undefined && typeof value.visibility !== 'string') {
67
+ throw new Error(`daemon.json.serviceProxy.services[${index}].visibility must be a string before it can be migrated`);
68
+ }
69
+ legacyProxyServices.push(value);
70
+ }
71
+ }
72
+ const legacyEcwebProxy = legacyProxyServices.find((value) => value.name === 'ecweb');
73
+ const ecwebIndex = services.findIndex((service) => service.name === 'ecweb');
74
+ // Convert the former flat serviceProxy entries into canonical services[].
75
+ // Keep every service rather than silently dropping non-ecweb entries.
76
+ for (const legacy of legacyProxyServices) {
77
+ const name = typeof legacy.name === 'string' ? legacy.name : '';
78
+ if (!name || name === 'ecweb')
79
+ continue;
80
+ if (services.some((service) => service.name === name))
81
+ continue;
82
+ const source = legacy.source === 'ecweb'
83
+ ? 'instance'
84
+ : legacy.source === 'instance'
85
+ ? 'instance'
86
+ : typeof legacy.endpoint === 'string' && legacy.endpoint.trim()
87
+ ? 'static'
88
+ : 'static';
89
+ const proxy = {
90
+ enabled: legacyProxy?.enabled === true && legacy.enabled !== false,
91
+ source,
92
+ ...(typeof legacy.endpoint === 'string' ? { endpoint: legacy.endpoint } : {}),
93
+ ...(typeof legacy.serviceType === 'string' && SERVICE_TYPES.has(legacy.serviceType)
94
+ ? { serviceType: legacy.serviceType } : {}),
95
+ ...(typeof legacy.visibility === 'string' ? { visibility: legacy.visibility } : {}),
96
+ ...(isRecord(legacy.metadata) ? { metadata: { ...legacy.metadata } } : {}),
97
+ };
98
+ services.push({ name, enabled: true, proxy });
99
+ }
100
+ if (legacyEcweb || legacyEcwebProxy) {
101
+ const service = ecwebIndex >= 0
102
+ ? services[ecwebIndex]
103
+ : {
104
+ name: 'ecweb',
105
+ // The old ECWeb lifecycle switch and Service Proxy switch were
106
+ // independent. Preserve that distinction during migration: a
107
+ // proxy-only config must not start managing the local process.
108
+ enabled: legacyEcweb?.enabled === true,
109
+ };
110
+ if (legacyEcweb) {
111
+ if (service.enabled === undefined && typeof legacyEcweb.enabled === 'boolean')
112
+ service.enabled = legacyEcweb.enabled;
113
+ if (service.port === undefined && typeof legacyEcweb.port === 'number')
114
+ service.port = legacyEcweb.port;
115
+ }
116
+ if (service.proxy !== undefined && !isRecord(service.proxy)) {
117
+ throw new Error('daemon.json.services[ecweb].proxy must be an object before legacy proxy settings can be migrated');
118
+ }
119
+ const proxy = { ...(service.proxy ?? {}) };
120
+ if (legacyEcwebProxy) {
121
+ if (proxy.enabled === undefined) {
122
+ proxy.enabled = legacyProxy?.enabled === true && legacyEcwebProxy.enabled !== false;
123
+ }
124
+ if (proxy.source === undefined) {
125
+ proxy.source = legacyEcwebProxy.source === 'static'
126
+ || (typeof legacyEcwebProxy.endpoint === 'string' && legacyEcwebProxy.endpoint.trim())
127
+ ? 'static'
128
+ : 'instance';
129
+ }
130
+ if (proxy.endpoint === undefined && typeof legacyEcwebProxy.endpoint === 'string')
131
+ proxy.endpoint = legacyEcwebProxy.endpoint;
132
+ if (proxy.serviceType === undefined && typeof legacyEcwebProxy.serviceType === 'string'
133
+ && SERVICE_TYPES.has(legacyEcwebProxy.serviceType)) {
134
+ proxy.serviceType = legacyEcwebProxy.serviceType;
135
+ }
136
+ if (proxy.visibility === undefined && typeof legacyEcwebProxy.visibility === 'string') {
137
+ proxy.visibility = legacyEcwebProxy.visibility;
138
+ }
139
+ if (proxy.metadata === undefined && isRecord(legacyEcwebProxy.metadata)) {
140
+ proxy.metadata = { ...legacyEcwebProxy.metadata };
141
+ }
142
+ if (proxy.enabled === true && proxy.visibility === undefined)
143
+ proxy.visibility = 'private';
144
+ }
145
+ if (Object.keys(proxy).length > 0)
146
+ service.proxy = proxy;
147
+ if (ecwebIndex < 0)
148
+ services.push(service);
149
+ }
150
+ if (hasCanonicalServices || services.length > 0)
151
+ next.services = services;
152
+ if (hasLegacyEcweb || hasLegacyProxy) {
153
+ warn?.('[config] daemon.json: ecweb/serviceProxy 已归一化为 services[]');
154
+ delete next.ecweb;
155
+ delete next.serviceProxy;
156
+ }
157
+ return next;
158
+ }
159
+ export function findService(services, name) {
160
+ return (services ?? []).find((service) => service.name === name);
161
+ }
162
+ export function ecwebService(services) {
163
+ return findService(services, 'ecweb');
164
+ }
165
+ export function upsertService(services, patch) {
166
+ const current = services ?? [];
167
+ const index = current.findIndex((service) => service.name === patch.name);
168
+ if (index < 0)
169
+ return [...current, patch];
170
+ const existing = current[index];
171
+ const replacement = {
172
+ ...existing,
173
+ ...patch,
174
+ ...(existing.proxy || patch.proxy
175
+ ? { proxy: { ...(existing.proxy ?? {}), ...(patch.proxy ?? {}) } }
176
+ : {}),
177
+ };
178
+ return current.map((service, serviceIndex) => serviceIndex === index ? replacement : service);
179
+ }
180
+ export function setEcwebEnabled(services, enabled, port = ecwebService(services)?.port ?? DEFAULT_ECWEB_PORT) {
181
+ return upsertService(services, {
182
+ name: 'ecweb',
183
+ enabled,
184
+ port,
185
+ });
186
+ }
@@ -17,10 +17,11 @@ import { resolvePaths } from '../paths.js';
17
17
  import { saveDefaultsSafe, saveAgent } from '../config-store.js';
18
18
  import { resolveAnthropicConfig, resolveEcagentConfig, resolveOpenaiConfig } from '../agents/baseagent.js';
19
19
  import { buildModelRequestHeaders } from '../agents/request-identity.js';
20
+ import { appendV1RequestPath } from '../agents/request-identity.js';
20
21
  import { resolvePriceRow } from '../stats/billing.js';
21
22
  import { ipcQuery } from '../ipc.js';
22
23
  import { logger } from '../utils/logger.js';
23
- import { ConfigTarget, read as readConfig } from './config-manager.js';
24
+ import { ConfigTarget, read as readConfig, stripLegacyAunGatewayConfig } from './config-manager.js';
24
25
  /** 已知 baseagent 类型(网关可管理范围)。 */
25
26
  const GATEWAY_TYPES = ['claude', 'codex', 'gemini', 'ecagent'];
26
27
  const DISPLAY_NAMES = {
@@ -84,8 +85,9 @@ function readDefaultsRaw() {
84
85
  // loadDefaults 会展开 $ENV,掩码逻辑需看原始引用,故直接读盘
85
86
  try {
86
87
  const p = path.join(resolvePaths().agentsDir, 'defaults.json');
87
- if (fs.existsSync(p))
88
- return JSON.parse(fs.readFileSync(p, 'utf-8'));
88
+ if (fs.existsSync(p)) {
89
+ return stripLegacyAunGatewayConfig(JSON.parse(fs.readFileSync(p, 'utf-8')), p, message => logger.warn(message));
90
+ }
89
91
  }
90
92
  catch (e) {
91
93
  logger.warn(`[gateway] read defaults.json failed: ${e}`);
@@ -95,8 +97,9 @@ function readDefaultsRaw() {
95
97
  function readAgentRaw(aid) {
96
98
  try {
97
99
  const p = path.join(resolvePaths().agentsDir, aid, 'config.json');
98
- if (fs.existsSync(p))
99
- return JSON.parse(fs.readFileSync(p, 'utf-8'));
100
+ if (fs.existsSync(p)) {
101
+ return stripLegacyAunGatewayConfig(JSON.parse(fs.readFileSync(p, 'utf-8')), p, message => logger.warn(message));
102
+ }
100
103
  }
101
104
  catch (e) {
102
105
  logger.warn(`[gateway] read agents/${aid}/config.json failed: ${e}`);
@@ -539,6 +542,7 @@ function resolveGatewayCreds(scope, type) {
539
542
  baseUrl: r.baseUrl,
540
543
  apiKey: r.apiKey,
541
544
  headers: buildModelRequestHeaders({ baseagent: 'codex', baseUrl: r.baseUrl, agentAid: scope === 'defaults' ? undefined : scope, configuredHeaders: r.headers }),
545
+ queryParams: r.queryParams,
542
546
  };
543
547
  }
544
548
  else if (type === 'claude') {
@@ -547,6 +551,7 @@ function resolveGatewayCreds(scope, type) {
547
551
  baseUrl: r.baseUrl,
548
552
  apiKey: r.apiKey,
549
553
  headers: buildModelRequestHeaders({ baseagent: 'claude', baseUrl: r.baseUrl, agentAid: scope === 'defaults' ? undefined : scope, configuredHeaders: r.headers }),
554
+ queryParams: r.queryParams,
550
555
  };
551
556
  }
552
557
  else if (type === 'ecagent') {
@@ -555,6 +560,7 @@ function resolveGatewayCreds(scope, type) {
555
560
  baseUrl: r.baseUrl,
556
561
  apiKey: r.apiKey,
557
562
  headers: buildModelRequestHeaders({ baseagent: 'ecagent', baseUrl: r.baseUrl, agentAid: scope === 'defaults' ? undefined : scope, configuredHeaders: r.headers }),
563
+ queryParams: r.queryParams,
558
564
  };
559
565
  }
560
566
  return { error: 'gemini 暂不支持 HTTP 探测(CLI 后端)', code: 'NOT_SUPPORTED' };
@@ -572,20 +578,19 @@ export async function gatewayTest(args) {
572
578
  const creds = resolveGatewayCreds(scope, type);
573
579
  if ('error' in creds)
574
580
  return creds;
575
- const { baseUrl, apiKey, headers } = creds;
581
+ const { baseUrl, apiKey, headers, queryParams } = creds;
576
582
  if (!baseUrl)
577
583
  return { error: '未配置 baseUrl(或为官方占位地址)', code: 'INVALID_ARGS' };
578
584
  const start = Date.now();
585
+ let timer;
579
586
  try {
580
587
  const controller = new AbortController();
581
- const timer = setTimeout(() => controller.abort(), 5000);
582
- const base = baseUrl.replace(/\/+$/, '');
583
- const modelsUrl = base.endsWith('/v1') ? `${base}/models` : `${base}/v1/models`;
588
+ timer = setTimeout(() => controller.abort(), 5000);
589
+ const modelsUrl = appendV1RequestPath(baseUrl, 'models', queryParams);
584
590
  const resp = await fetch(modelsUrl, {
585
591
  headers: { ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), ...headers },
586
592
  signal: controller.signal,
587
593
  });
588
- clearTimeout(timer);
589
594
  const latency = Date.now() - start;
590
595
  if (!resp.ok)
591
596
  return { data: { ok: false, latency, error: `HTTP ${resp.status}` } };
@@ -598,6 +603,10 @@ export async function gatewayTest(args) {
598
603
  catch (e) {
599
604
  return { data: { ok: false, latency: Date.now() - start, error: e?.message || String(e) } };
600
605
  }
606
+ finally {
607
+ if (timer)
608
+ clearTimeout(timer);
609
+ }
601
610
  }
602
611
  /** 从 model-prices.jsonl 的 PriceRecord 提取展示用价格四元组。 */
603
612
  function priceRowToQuad(row) {
@@ -630,19 +639,19 @@ export async function gatewayModels(args) {
630
639
  const creds = resolveGatewayCreds(scope, type);
631
640
  if ('error' in creds)
632
641
  return creds;
633
- const { baseUrl, apiKey, headers } = creds;
642
+ const { baseUrl, apiKey, headers, queryParams } = creds;
634
643
  if (!baseUrl)
635
644
  return { error: '未配置 baseUrl(或为官方占位地址)', code: 'INVALID_ARGS' };
636
645
  let arr = [];
637
646
  let usdToCny = 7; // 默认汇率
647
+ let timer;
638
648
  try {
639
649
  const controller = new AbortController();
640
- const timer = setTimeout(() => controller.abort(), 8000);
641
- const resp = await fetch(`${baseUrl.replace(/\/+$/, '')}/v1/models`, {
650
+ timer = setTimeout(() => controller.abort(), 8000);
651
+ const resp = await fetch(appendV1RequestPath(baseUrl, 'models', queryParams), {
642
652
  headers: { ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), ...headers },
643
653
  signal: controller.signal,
644
654
  });
645
- clearTimeout(timer);
646
655
  if (!resp.ok)
647
656
  return { error: `HTTP ${resp.status}`, code: 'EXEC_FAILED' };
648
657
  const json = await resp.json().catch(() => null);
@@ -655,6 +664,10 @@ export async function gatewayModels(args) {
655
664
  catch (e) {
656
665
  return { error: `请求失败:${e?.message || e}`, code: 'EXEC_FAILED' };
657
666
  }
667
+ finally {
668
+ if (timer)
669
+ clearTimeout(timer);
670
+ }
658
671
  const evolcoreHome = resolvePaths().root;
659
672
  const now = Date.now();
660
673
  const models = arr.map((m) => {
@@ -780,7 +793,7 @@ export async function gatewaySyncEnv(args) {
780
793
  }
781
794
  if (hasChanges) {
782
795
  const defaultsPath = path.join(resolvePaths().agentsDir, 'defaults.json');
783
- fs.writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2), 'utf-8');
796
+ saveDefaultsSafe(stripLegacyAunGatewayConfig(defaults, defaultsPath, message => logger.warn(message)));
784
797
  synced.push('全局配置 (defaults.json)');
785
798
  }
786
799
  }
@@ -826,7 +839,7 @@ export async function gatewaySyncEnv(args) {
826
839
  }
827
840
  if (hasChanges) {
828
841
  const agentConfigPath = path.join(resolvePaths().agentsDir, aid, 'config.json');
829
- fs.writeFileSync(agentConfigPath, JSON.stringify(agentRaw, null, 2), 'utf-8');
842
+ saveAgent(stripLegacyAunGatewayConfig(agentRaw, agentConfigPath, message => logger.warn(message)));
830
843
  synced.push(`${aid} (config.json)`);
831
844
  }
832
845
  }
@@ -1,13 +1,24 @@
1
1
  import { CONFIG_SCHEMA_VERSION } from '../types.js';
2
2
  const VALID_LIFECYCLES = new Set(['created', 'bootstrapping', 'active']);
3
- export function normalizeAgentLifecycle(config) {
3
+ export function isAgentLifecycle(value) {
4
+ return typeof value === 'string' && VALID_LIFECYCLES.has(value);
5
+ }
6
+ /** Resolve the effective lifecycle. Null means an explicit invalid value. */
7
+ export function resolveAgentLifecycle(config) {
4
8
  const current = config.lifecycle;
5
- if (current && VALID_LIFECYCLES.has(current)) {
9
+ if (isAgentLifecycle(current))
10
+ return current;
11
+ if (current !== undefined)
12
+ return null;
13
+ return config.initialized === false ? 'created' : 'active';
14
+ }
15
+ export function normalizeAgentLifecycle(config) {
16
+ const lifecycle = resolveAgentLifecycle(config);
17
+ // Preserve explicit invalid values so validation and runtime gates can fail
18
+ // closed. Only an omitted lifecycle receives the compatibility default.
19
+ if (lifecycle === null || config.lifecycle === lifecycle) {
6
20
  return config;
7
21
  }
8
- const lifecycle = config.initialized === true ? 'active'
9
- : config.initialized === false ? 'created'
10
- : 'active';
11
22
  return { ...config, lifecycle };
12
23
  }
13
24
  export function withLifecycleForWrite(config, lifecycle) {
@@ -54,6 +54,18 @@ export function writeRoleDefinition(selfAid, roleId, value) {
54
54
  atomicWriteJson(file, normalized);
55
55
  clearRoleStoreCache(selfAid);
56
56
  }
57
+ /** Remove an agent-local override so the builtin role template is used again. */
58
+ export function resetRoleDefinition(selfAid, roleId) {
59
+ if (!isRoleName(roleId))
60
+ throw new RoleServiceError('INVALID_ROLE_ID', `Invalid role id: ${roleId}`);
61
+ const file = agentRoleConfig(selfAid, roleId);
62
+ const changed = fs.existsSync(file);
63
+ if (changed) {
64
+ fs.rmSync(file, { force: true });
65
+ clearRoleStoreCache(selfAid);
66
+ }
67
+ return changed;
68
+ }
57
69
  export function registerRole(selfAid, roleId, definition) {
58
70
  if (!isRoleName(roleId))
59
71
  throw new RoleServiceError('INVALID_ROLE_ID', `Invalid role id: ${roleId}`);
@@ -72,17 +84,56 @@ export function registerRole(selfAid, roleId, definition) {
72
84
  throw error;
73
85
  }
74
86
  }
75
- export function unregisterRole(selfAid, roleId) {
87
+ export function unregisterRole(selfAid, roleId, replacementRoleId) {
76
88
  if (roleId === 'visitor' || isManagementRole(roleId)) {
77
89
  throw new RoleServiceError('ROLE_PROTECTED', `Role ${roleId} cannot be unregistered`);
78
90
  }
79
91
  const registry = readRoleRegistry(selfAid);
80
92
  if (!registry.roles.includes(roleId))
81
93
  return;
94
+ const hasReplacement = replacementRoleId !== undefined;
95
+ const replacement = replacementRoleId === undefined ? undefined : (replacementRoleId || null);
96
+ if (replacement === roleId) {
97
+ throw new RoleServiceError('INVALID_REPLACEMENT_ROLE', 'replacementRoleId must differ from the role being deleted');
98
+ }
99
+ if (replacement !== undefined && replacement !== null) {
100
+ if (!isRoleName(replacement) || replacement === 'owner' || !registry.roles.includes(replacement)
101
+ || !hasValidRoleDefinition(selfAid, replacement)) {
102
+ throw new RoleServiceError('INVALID_REPLACEMENT_ROLE', `Invalid replacement role: ${replacement}`);
103
+ }
104
+ }
82
105
  const references = findRoleReferences(selfAid, roleId, registry);
83
- if (references.length)
106
+ if (references.length && !hasReplacement) {
84
107
  throw new RoleServiceError('ROLE_IN_USE', `Role ${roleId} is still referenced`, { references });
85
- writeRoleRegistry(selfAid, { ...registry, roles: registry.roles.filter(item => item !== roleId) });
108
+ }
109
+ const nextRegistry = {
110
+ ...registry,
111
+ roles: registry.roles.filter(item => item !== roleId),
112
+ defaultRoles: {
113
+ private: registry.defaultRoles.private === roleId ? (replacement ?? null) : registry.defaultRoles.private,
114
+ group: registry.defaultRoles.group === roleId ? (replacement ?? null) : registry.defaultRoles.group,
115
+ },
116
+ };
117
+ if (hasReplacement) {
118
+ const relations = agentRelationsDir(selfAid);
119
+ if (fs.existsSync(relations)) {
120
+ for (const entry of fs.readdirSync(relations, { withFileTypes: true })) {
121
+ if (!entry.isDirectory() || entry.name.startsWith('_'))
122
+ continue;
123
+ const file = agentRelationConfig(selfAid, entry.name);
124
+ const config = atomicReadJson(file);
125
+ if (!config || config.role !== roleId)
126
+ continue;
127
+ const next = { ...config };
128
+ if (replacement === null)
129
+ delete next.role;
130
+ else
131
+ next.role = replacement;
132
+ atomicWriteJson(file, next);
133
+ }
134
+ }
135
+ }
136
+ writeRoleRegistry(selfAid, nextRegistry);
86
137
  try {
87
138
  fs.rmSync(agentRoleConfig(selfAid, roleId), { force: true });
88
139
  }