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.
Files changed (147) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +58 -9
  3. package/bin/codex-managed-hook.mjs +3 -0
  4. package/bin/install-codex-managed-hooks.mjs +3 -1
  5. package/dist/agents/baseagent.js +10 -6
  6. package/dist/agents/claude-runner.js +393 -108
  7. package/dist/agents/codex-app-server-client.js +41 -7
  8. package/dist/agents/codex-runner.js +1292 -220
  9. package/dist/agents/ecagent-runner.js +171 -61
  10. package/dist/agents/gemini-runner.js +130 -30
  11. package/dist/agents/request-identity.js +25 -0
  12. package/dist/agents/runner-types.js +19 -0
  13. package/dist/aun/aid/agentmd.js +59 -2
  14. package/dist/aun/aid/identity.js +4 -1
  15. package/dist/aun/aid/index.js +1 -1
  16. package/dist/aun/msg/group.js +72 -6
  17. package/dist/aun/msg/history.js +213 -36
  18. package/dist/aun/msg/managed-operation.js +58 -9
  19. package/dist/aun/msg/p2p.js +5 -0
  20. package/dist/aun/outbox.js +189 -80
  21. package/dist/aun/service-proxy.js +43 -25
  22. package/dist/channels/aun.js +618 -123
  23. package/dist/channels/daemon.js +6 -1
  24. package/dist/cli/agent-command.js +4 -3
  25. package/dist/cli/agent.js +66 -56
  26. package/dist/cli/aun-commands.js +177 -42
  27. package/dist/cli/command-log.js +10 -11
  28. package/dist/cli/contact.js +1 -0
  29. package/dist/cli/daemon-commands.js +98 -123
  30. package/dist/cli/init.js +27 -15
  31. package/dist/cli/task-context.js +50 -0
  32. package/dist/cli/trigger-command.js +14 -5
  33. package/dist/cli/watch-logs.js +10 -3
  34. package/dist/config/builtin-roles.js +1 -0
  35. package/dist/config/config-field-policy.js +19 -5
  36. package/dist/config/config-manager.js +167 -22
  37. package/dist/config/contact-book-store.js +25 -3
  38. package/dist/config/contact-operation-service.js +32 -1
  39. package/dist/config/contact-request-service.js +44 -0
  40. package/dist/config/daemon-services.js +186 -0
  41. package/dist/config/gateway-config.js +20 -9
  42. package/dist/config/role-service.js +54 -3
  43. package/dist/config/schema-migration.js +550 -0
  44. package/dist/config-store.js +151 -9
  45. package/dist/core/agent-application-service.js +279 -0
  46. package/dist/core/audit/log-integrity.js +102 -0
  47. package/dist/core/auth/agent-delegation.js +43 -1
  48. package/dist/core/auth/auth-gateway.js +41 -4
  49. package/dist/core/auth/authorization-audit.js +216 -8
  50. package/dist/core/auth/operation-authorizer.js +41 -1
  51. package/dist/core/auth/operation-catalog.js +9 -1
  52. package/dist/core/bootstrap-messages.js +8 -0
  53. package/dist/core/bootstrap-service.js +99 -27
  54. package/dist/core/causation/aun-association.js +7 -4
  55. package/dist/core/command/agent-control.js +56 -16
  56. package/dist/core/command/command-handler.js +311 -44
  57. package/dist/core/command/connect-menu.js +3 -4
  58. package/dist/core/command/group-menu.js +5 -7
  59. package/dist/core/command/menu-handler.js +288 -80
  60. package/dist/core/command/menu-protocol.js +1 -1
  61. package/dist/core/command/role-menu.js +21 -11
  62. package/dist/core/command/slash-gate.js +85 -18
  63. package/dist/core/command/slash-handler.js +377 -36
  64. package/dist/core/data-migration.js +11 -1
  65. package/dist/core/event-catalog.js +37 -0
  66. package/dist/core/evolagent.js +4 -0
  67. package/dist/core/handoff/dispatcher.js +4 -0
  68. package/dist/core/handoff/runtime.js +33 -3
  69. package/dist/core/handoff/store.js +32 -9
  70. package/dist/core/inference/text-inference.js +7 -15
  71. package/dist/core/message/im-renderer.js +90 -87
  72. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  73. package/dist/core/message/message-bridge.js +184 -12
  74. package/dist/core/message/message-log.js +47 -7
  75. package/dist/core/message/message-queue.js +227 -16
  76. package/dist/core/message/message-utils.js +12 -5
  77. package/dist/core/message/response-engine.js +658 -109
  78. package/dist/core/message/send-receipt.js +1 -0
  79. package/dist/core/message/stream-debouncer.js +9 -2
  80. package/dist/core/model/model-catalog.js +23 -15
  81. package/dist/core/model/model-diagnostics.js +28 -10
  82. package/dist/core/permission/approval-gateway.js +180 -6
  83. package/dist/core/permission/ec-command-parser.js +627 -69
  84. package/dist/core/permission/mode.js +18 -3
  85. package/dist/core/{protected-paths.js → permission/protected-paths.js} +27 -14
  86. package/dist/core/permission/readonly-shell-query.js +263 -9
  87. package/dist/core/permission/sandbox-runtime.js +159 -1
  88. package/dist/core/permission/tool-error-code.js +12 -0
  89. package/dist/core/permission/tool-policy.js +618 -23
  90. package/dist/core/session/session-fs-store.js +154 -5
  91. package/dist/core/session/session-manager.js +329 -30
  92. package/dist/core/session/session-renew.js +37 -13
  93. package/dist/core/session/session-turn-coordinator.js +16 -5
  94. package/dist/eck/kit-renderer.js +1 -1
  95. package/dist/index.js +316 -50
  96. package/dist/ipc.js +459 -29
  97. package/dist/paths.js +82 -7
  98. package/dist/response-system/context-builder.js +1 -7
  99. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  100. package/dist/stats/price-resolver.js +4 -0
  101. package/dist/trigger/anomaly-store.js +1 -0
  102. package/dist/trigger/feedback.js +70 -7
  103. package/dist/trigger/history.js +79 -4
  104. package/dist/trigger/legacy-session-history.js +2 -2
  105. package/dist/trigger/parser.js +13 -3
  106. package/dist/trigger/scheduler.js +20 -3
  107. package/dist/trigger/validation.js +6 -1
  108. package/dist/utils/atomic-write.js +27 -0
  109. package/dist/utils/ecweb-utils.js +16 -2
  110. package/dist/utils/error-utils.js +4 -1
  111. package/dist/utils/logger.js +30 -6
  112. package/dist/utils/process-tree-stats.js +24 -4
  113. package/dist/utils/process-tree-worker.js +31 -0
  114. package/dist/utils/project-path.js +1 -2
  115. package/dist/utils/tool-summary.js +59 -0
  116. package/dist/utils/windows-shell-trust.js +201 -0
  117. package/kits/docs/INDEX.md +1 -1
  118. package/kits/docs/evolcore/INDEX.md +3 -3
  119. package/kits/docs/evolcore/agent-create.md +146 -0
  120. package/kits/docs/evolcore/agent.md +6 -0
  121. package/kits/docs/evolcore/contact.md +7 -1
  122. package/kits/docs/evolcore/group-collaboration.md +251 -0
  123. package/kits/docs/evolcore/group-rules.md +1 -19
  124. package/kits/docs/evolcore/group.md +3 -1
  125. package/kits/docs/evolcore/msg.md +16 -0
  126. package/kits/docs/evolcore/trigger.md +6 -3
  127. package/kits/docs/prompt-loading-architecture.md +6 -0
  128. package/kits/eck_message_manifest.json +6 -6
  129. package/kits/schemas/_meta.json +7 -4
  130. package/kits/schemas/agent-config.schema.11.json +13 -0
  131. package/kits/schemas/agent-config.schema.12.json +427 -0
  132. package/kits/schemas/daemon.schema.5.json +0 -1
  133. package/kits/schemas/daemon.schema.6.json +131 -0
  134. package/kits/schemas/defaults.schema.5.json +15 -3
  135. package/kits/schemas/migrations/README.md +3 -1
  136. package/kits/schemas/relation-config.schema.8.json +13 -0
  137. package/kits/schemas/role-config.schema.1.json +1 -2
  138. package/kits/schemas/single-session.schema.3.json +32 -0
  139. package/kits/templates/message-fragments/item.md +1 -1
  140. package/kits/templates/roles/admin.json +1 -0
  141. package/kits/templates/roles/member.json +1 -0
  142. package/kits/templates/roles/visitor.json +1 -0
  143. package/kits/templates/system-fragments/bootstrap.md +2 -1
  144. package/kits/templates/system-fragments/commands.md +2 -2
  145. package/package.json +6 -3
  146. package/skills/eclink/SKILL.md +2 -0
  147. package/dist/config/aun-gateway-config.js +0 -2
@@ -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,6 +17,7 @@ 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';
@@ -541,6 +542,7 @@ function resolveGatewayCreds(scope, type) {
541
542
  baseUrl: r.baseUrl,
542
543
  apiKey: r.apiKey,
543
544
  headers: buildModelRequestHeaders({ baseagent: 'codex', baseUrl: r.baseUrl, agentAid: scope === 'defaults' ? undefined : scope, configuredHeaders: r.headers }),
545
+ queryParams: r.queryParams,
544
546
  };
545
547
  }
546
548
  else if (type === 'claude') {
@@ -549,6 +551,7 @@ function resolveGatewayCreds(scope, type) {
549
551
  baseUrl: r.baseUrl,
550
552
  apiKey: r.apiKey,
551
553
  headers: buildModelRequestHeaders({ baseagent: 'claude', baseUrl: r.baseUrl, agentAid: scope === 'defaults' ? undefined : scope, configuredHeaders: r.headers }),
554
+ queryParams: r.queryParams,
552
555
  };
553
556
  }
554
557
  else if (type === 'ecagent') {
@@ -557,6 +560,7 @@ function resolveGatewayCreds(scope, type) {
557
560
  baseUrl: r.baseUrl,
558
561
  apiKey: r.apiKey,
559
562
  headers: buildModelRequestHeaders({ baseagent: 'ecagent', baseUrl: r.baseUrl, agentAid: scope === 'defaults' ? undefined : scope, configuredHeaders: r.headers }),
563
+ queryParams: r.queryParams,
560
564
  };
561
565
  }
562
566
  return { error: 'gemini 暂不支持 HTTP 探测(CLI 后端)', code: 'NOT_SUPPORTED' };
@@ -574,20 +578,19 @@ export async function gatewayTest(args) {
574
578
  const creds = resolveGatewayCreds(scope, type);
575
579
  if ('error' in creds)
576
580
  return creds;
577
- const { baseUrl, apiKey, headers } = creds;
581
+ const { baseUrl, apiKey, headers, queryParams } = creds;
578
582
  if (!baseUrl)
579
583
  return { error: '未配置 baseUrl(或为官方占位地址)', code: 'INVALID_ARGS' };
580
584
  const start = Date.now();
585
+ let timer;
581
586
  try {
582
587
  const controller = new AbortController();
583
- const timer = setTimeout(() => controller.abort(), 5000);
584
- const base = baseUrl.replace(/\/+$/, '');
585
- const modelsUrl = base.endsWith('/v1') ? `${base}/models` : `${base}/v1/models`;
588
+ timer = setTimeout(() => controller.abort(), 5000);
589
+ const modelsUrl = appendV1RequestPath(baseUrl, 'models', queryParams);
586
590
  const resp = await fetch(modelsUrl, {
587
591
  headers: { ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), ...headers },
588
592
  signal: controller.signal,
589
593
  });
590
- clearTimeout(timer);
591
594
  const latency = Date.now() - start;
592
595
  if (!resp.ok)
593
596
  return { data: { ok: false, latency, error: `HTTP ${resp.status}` } };
@@ -600,6 +603,10 @@ export async function gatewayTest(args) {
600
603
  catch (e) {
601
604
  return { data: { ok: false, latency: Date.now() - start, error: e?.message || String(e) } };
602
605
  }
606
+ finally {
607
+ if (timer)
608
+ clearTimeout(timer);
609
+ }
603
610
  }
604
611
  /** 从 model-prices.jsonl 的 PriceRecord 提取展示用价格四元组。 */
605
612
  function priceRowToQuad(row) {
@@ -632,19 +639,19 @@ export async function gatewayModels(args) {
632
639
  const creds = resolveGatewayCreds(scope, type);
633
640
  if ('error' in creds)
634
641
  return creds;
635
- const { baseUrl, apiKey, headers } = creds;
642
+ const { baseUrl, apiKey, headers, queryParams } = creds;
636
643
  if (!baseUrl)
637
644
  return { error: '未配置 baseUrl(或为官方占位地址)', code: 'INVALID_ARGS' };
638
645
  let arr = [];
639
646
  let usdToCny = 7; // 默认汇率
647
+ let timer;
640
648
  try {
641
649
  const controller = new AbortController();
642
- const timer = setTimeout(() => controller.abort(), 8000);
643
- const resp = await fetch(`${baseUrl.replace(/\/+$/, '')}/v1/models`, {
650
+ timer = setTimeout(() => controller.abort(), 8000);
651
+ const resp = await fetch(appendV1RequestPath(baseUrl, 'models', queryParams), {
644
652
  headers: { ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), ...headers },
645
653
  signal: controller.signal,
646
654
  });
647
- clearTimeout(timer);
648
655
  if (!resp.ok)
649
656
  return { error: `HTTP ${resp.status}`, code: 'EXEC_FAILED' };
650
657
  const json = await resp.json().catch(() => null);
@@ -657,6 +664,10 @@ export async function gatewayModels(args) {
657
664
  catch (e) {
658
665
  return { error: `请求失败:${e?.message || e}`, code: 'EXEC_FAILED' };
659
666
  }
667
+ finally {
668
+ if (timer)
669
+ clearTimeout(timer);
670
+ }
660
671
  const evolcoreHome = resolvePaths().root;
661
672
  const now = Date.now();
662
673
  const models = arr.map((m) => {
@@ -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
  }