evolcore 0.0.35 → 0.0.36

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 (224) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +6 -0
  3. package/bin/install-codex-managed-hooks.mjs +20 -3
  4. package/dist/agents/baseagent-availability.js +1 -1
  5. package/dist/agents/baseagent.js +88 -0
  6. package/dist/agents/claude-runner.js +59 -12
  7. package/dist/agents/codex/app-server-client.js +18 -1
  8. package/dist/agents/codex-runner.js +262 -21
  9. package/dist/agents/ecagent-runner.js +47 -11
  10. package/dist/agents/gemini-runner.js +36 -12
  11. package/dist/aun/aid/client.js +4 -3
  12. package/dist/aun/aid/control-aid.js +270 -8
  13. package/dist/aun/aid/identity.js +6 -3
  14. package/dist/aun/aid/ownership-operation.js +58 -0
  15. package/dist/aun/aid/store.js +1 -0
  16. package/dist/aun/group/fs-download.js +67 -2
  17. package/dist/aun/group/history.js +20 -5
  18. package/dist/aun/group/operations.js +8 -2
  19. package/dist/aun/menu/client.js +39 -2
  20. package/dist/aun/menu/transport.js +1 -1
  21. package/dist/aun/msg/history.js +43 -4
  22. package/dist/aun/msg/managed-operation.js +30 -53
  23. package/dist/aun/msg/p2p.js +51 -15
  24. package/dist/aun/outbox.js +4 -0
  25. package/dist/channels/aun.js +260 -73
  26. package/dist/channels/daemon.js +6 -4
  27. package/dist/channels/feishu.js +3 -0
  28. package/dist/cli/agent-command.js +161 -223
  29. package/dist/cli/agent-create.js +190 -223
  30. package/dist/cli/agent.js +3 -1
  31. package/dist/cli/aid-bind-command.js +53 -0
  32. package/dist/cli/aun-commands.js +384 -91
  33. package/dist/cli/bench.js +1 -1
  34. package/dist/cli/cli-argv.js +74 -0
  35. package/dist/cli/daemon-commands.js +105 -74
  36. package/dist/cli/diag.js +1 -1
  37. package/dist/cli/fs-command.js +18 -19
  38. package/dist/cli/getmeta-command.js +159 -6
  39. package/dist/cli/index.js +6 -12
  40. package/dist/cli/init-channel.js +8 -55
  41. package/dist/cli/init.js +125 -218
  42. package/dist/cli/queue-command.js +11 -8
  43. package/dist/cli/restart-monitor.js +134 -48
  44. package/dist/cli/session-command.js +1 -1
  45. package/dist/cli/stats.js +7 -5
  46. package/dist/cli/thread-command.js +103 -30
  47. package/dist/cli/trigger-command.js +1 -1
  48. package/dist/cli/watch-msg-diag.js +1 -0
  49. package/dist/config/access-policy.js +2 -2
  50. package/dist/config/boot-log.js +14 -11
  51. package/dist/config/config-field-policy.js +9 -1
  52. package/dist/config/config-manager.js +176 -90
  53. package/dist/config/config-operation-service.js +7 -14
  54. package/dist/config/contact-book-store.js +142 -26
  55. package/dist/config/contact-book.js +4 -2
  56. package/dist/config/lifecycle.js +1 -1
  57. package/dist/config/owner-config.js +96 -0
  58. package/dist/config/peer-role-resolver.js +12 -18
  59. package/dist/config/providers-config.js +382 -0
  60. package/dist/config/providers-import.js +74 -0
  61. package/dist/config/providers-management.js +328 -0
  62. package/dist/config/providers.js +22 -0
  63. package/dist/config/resolved-config-op.js +17 -14
  64. package/dist/config/snapshot.js +28 -16
  65. package/dist/config-store.js +18 -14
  66. package/dist/core/agent-ipc-registration.js +21 -63
  67. package/dist/core/agent-runtime.js +16 -5
  68. package/dist/core/auth/agent-delegation.js +199 -25
  69. package/dist/core/auth/auth-gateway.js +44 -2
  70. package/dist/core/auth/authorization-denial.js +1 -1
  71. package/dist/core/auth/debug-context.js +2 -1
  72. package/dist/core/auth/delegation-authorization.js +2 -2
  73. package/dist/core/auth/delegation-diagnostics.js +4 -1
  74. package/dist/core/auth/operation-authorizer.js +10 -1
  75. package/dist/core/auth/operation-catalog.js +19 -1
  76. package/dist/core/bootstrap-service.js +43 -20
  77. package/dist/core/channel-loader.js +0 -1
  78. package/dist/core/channel-runtime.js +30 -4
  79. package/dist/core/command/agent-control.js +196 -37
  80. package/dist/core/command/cli-intent-parser.js +60 -4
  81. package/dist/core/command/command-handler.js +212 -60
  82. package/dist/core/command/connect-menu.js +1 -0
  83. package/dist/core/command/diag-service.js +5 -4
  84. package/dist/core/command/file-search.js +368 -0
  85. package/dist/core/command/menu-catalog.js +2 -1
  86. package/dist/core/command/menu-handler.js +292 -107
  87. package/dist/core/command/menu-protocol.js +23 -0
  88. package/dist/core/command/role-menu.js +7 -19
  89. package/dist/core/command/skill-slash.js +130 -25
  90. package/dist/core/command/slash-handler.js +101 -53
  91. package/dist/core/command/topic-slash.js +16 -5
  92. package/dist/core/daemon-bootstrap.js +7 -1
  93. package/dist/core/daemon-control.js +19 -1
  94. package/dist/core/daemon-lifecycle.js +2 -1
  95. package/dist/core/event-catalog.js +2 -0
  96. package/dist/core/evolagent-registry.js +13 -2
  97. package/dist/core/evolagent.js +3 -6
  98. package/dist/core/handoff/ipc-registration.js +2 -2
  99. package/dist/core/handoff/runtime.js +40 -6
  100. package/dist/core/handoff/store.js +1 -0
  101. package/dist/core/inference/text-inference.js +95 -6
  102. package/dist/core/ipc-registration.js +43 -24
  103. package/dist/core/memory/consolidation-context.js +4 -1
  104. package/dist/core/memory/consolidation.js +25 -14
  105. package/dist/core/message/aun-ipc-sender.js +296 -88
  106. package/dist/core/message/aun-request-registry.js +226 -0
  107. package/dist/core/message/create-status.js +33 -4
  108. package/dist/core/message/message-bridge.js +62 -13
  109. package/dist/core/message/message-log.js +0 -1
  110. package/dist/core/message/message-queue.js +4 -1
  111. package/dist/core/message/message-utils.js +8 -1
  112. package/dist/core/message/peer-mode.js +17 -9
  113. package/dist/core/message/response-engine.js +266 -124
  114. package/dist/core/message/retry-scheduler.js +11 -8
  115. package/dist/core/model/catalog-service.js +60 -18
  116. package/dist/core/model/model-catalog.js +112 -9
  117. package/dist/core/model/model-fallback.js +94 -26
  118. package/dist/core/ownership/owner-resolver.js +687 -0
  119. package/dist/core/permission/approval-gateway.js +196 -62
  120. package/dist/core/permission/ec-command-parser.js +138 -25
  121. package/dist/core/permission/filesystem-approval-scope.js +141 -0
  122. package/dist/core/permission/protected-paths.js +14 -1
  123. package/dist/core/permission/sandbox-runtime.js +171 -18
  124. package/dist/core/permission/tool-policy.js +13 -2
  125. package/dist/core/role/runtime-policy.js +3 -4
  126. package/dist/core/session/session-idle-monitor.js +10 -8
  127. package/dist/core/session/session-manager.js +72 -45
  128. package/dist/core/session/session-mapper.js +18 -7
  129. package/dist/core/session/session-renew.js +5 -4
  130. package/dist/core/session/session-title.js +2 -1
  131. package/dist/core/session/session-turn-coordinator.js +4 -2
  132. package/dist/core/skill/index-generator.js +2 -16
  133. package/dist/core/task/task-recovery-service.js +15 -2
  134. package/dist/core/thread/command-parser.js +28 -13
  135. package/dist/core/thread/index.js +1 -0
  136. package/dist/core/thread/ipc-operation.js +11 -8
  137. package/dist/core/thread/thread-operation.js +79 -44
  138. package/dist/core/thread/thread-route.js +54 -6
  139. package/dist/core/thread/thread-service.js +236 -45
  140. package/dist/core/thread/topic-id.js +248 -0
  141. package/dist/eck/kit-renderer.js +1 -1
  142. package/dist/eck/message-renderer.js +12 -2
  143. package/dist/index.js +50 -27
  144. package/dist/ipc.js +59 -6
  145. package/dist/paths.js +1 -0
  146. package/dist/product.js +1 -0
  147. package/dist/response-system/engines/v1/proactive-flow.js +10 -3
  148. package/dist/stats/db.js +8 -0
  149. package/dist/stats/writer.js +26 -4
  150. package/dist/trigger/authorization.js +48 -3
  151. package/dist/trigger/feedback.js +28 -10
  152. package/dist/trigger/ipc-service.js +86 -10
  153. package/dist/trigger/local-files.js +55 -23
  154. package/dist/trigger/scheduler.js +57 -11
  155. package/dist/trigger/system-definitions.js +1 -0
  156. package/dist/trigger/validation.js +44 -15
  157. package/dist/utils/aid-bind.js +17 -297
  158. package/dist/utils/avatar-upload.js +16 -4
  159. package/dist/utils/cross-platform.js +109 -34
  160. package/dist/utils/ecweb-supervisor.js +1 -1
  161. package/dist/utils/ecweb-utils.js +8 -1
  162. package/dist/utils/error-utils.js +44 -6
  163. package/dist/utils/instance-registry.js +12 -2
  164. package/dist/utils/npm-ops.js +102 -27
  165. package/dist/utils/package-version-info.js +72 -27
  166. package/dist/utils/restart-safety.js +50 -0
  167. package/dist/utils/upgrade-jobs.js +304 -0
  168. package/dist/utils/windows-autostart.js +17 -10
  169. package/kits/docs/INDEX.md +5 -0
  170. package/kits/docs/aun/ABILITY_CALLING.md +239 -0
  171. package/kits/docs/evolcore/INDEX.md +14 -3
  172. package/kits/docs/evolcore/agent-create.md +55 -141
  173. package/kits/docs/evolcore/agent.md +38 -16
  174. package/kits/docs/evolcore/aid.md +34 -0
  175. package/kits/docs/evolcore/config.md +20 -1
  176. package/kits/docs/evolcore/event.md +2 -2
  177. package/kits/docs/evolcore/fs.md +18 -2
  178. package/kits/docs/evolcore/group-collaboration.md +13 -246
  179. package/kits/docs/evolcore/group.md +2 -1
  180. package/kits/docs/evolcore/memory-consolidation-batches.md +1 -1
  181. package/kits/docs/evolcore/menu-protocol.md +65 -1
  182. package/kits/docs/evolcore/msg.md +64 -2
  183. package/kits/docs/evolcore/skill.md +86 -0
  184. package/kits/docs/evolcore/stats.md +6 -0
  185. package/kits/docs/evolcore/task.md +2 -0
  186. package/kits/docs/evolcore/thread-chain.md +145 -51
  187. package/kits/docs/evolcore/trigger.md +48 -3
  188. package/kits/eck_manifest.json +21 -0
  189. package/kits/eck_message_manifest.json +4 -4
  190. package/kits/migrations/migrate-role-config-v5.mjs +20 -7
  191. package/kits/rules/01-overview.md +2 -1
  192. package/kits/rules/03-identity.md +3 -1
  193. package/kits/rules/04-relation.md +1 -1
  194. package/kits/rules/07-memory-and-thread.md +1 -1
  195. package/kits/schemas/_meta.json +16 -1
  196. package/kits/schemas/agent-config.schema.13.json +44 -2
  197. package/kits/schemas/contact-book.schema.3.json +5 -0
  198. package/kits/schemas/daemon.schema.6.json +3 -1
  199. package/kits/schemas/daemon.schema.8.json +329 -0
  200. package/kits/schemas/defaults.schema.5.json +39 -3
  201. package/kits/schemas/providers.schema.1.json +76 -0
  202. package/kits/schemas/relation-config.schema.9.json +5 -1
  203. package/kits/skills/.switches.json +28 -0
  204. package/kits/skills/aid-ability/SKILL.md +214 -0
  205. package/kits/skills/ec-agent-create/SKILL.md +162 -0
  206. package/kits/skills/ec-group/SKILL.md +2 -1
  207. package/kits/skills/ec-group-collaboration/SKILL.md +228 -0
  208. package/kits/skills/ec-identity/SKILL.md +143 -0
  209. package/kits/skills/ec-skill/scripts/cli.mjs +36 -3
  210. package/kits/skills/ec-skill/scripts/skill.mjs +202 -15
  211. package/kits/skills/ec-trigger/SKILL.md +167 -0
  212. package/kits/skills/ec-trigger/references/examples.md +153 -0
  213. package/kits/skills/thread-ops/SKILL.md +108 -420
  214. package/kits/templates/message-fragments/thread-context.md +1 -2
  215. package/kits/templates/message-fragments/trigger-thread-check.md +1 -1
  216. package/kits/templates/roles/member.json +1 -1
  217. package/kits/templates/roles/visitor.json +1 -0
  218. package/kits/templates/system-fragments/bootstrap.md +1 -0
  219. package/kits/templates/system-fragments/channel.md +2 -1
  220. package/kits/templates/system-fragments/commands.md +5 -4
  221. package/kits/templates/system-fragments/identity.md +2 -0
  222. package/kits/templates/system-fragments/session.md +1 -1
  223. package/kits/templates/system-fragments/venue.md +1 -1
  224. package/package.json +4 -3
@@ -0,0 +1,328 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { posix } from 'node:path';
3
+ import { resolvePaths } from '../paths.js';
4
+ import { ConfigTarget, mutateConfig } from './config-manager.js';
5
+ import { readProvidersFile, validateProvidersFile } from './providers.js';
6
+ import { providerEnvironmentVariable, providerNameFromBaseUrlIfStatic, resolveProviderService, resolveProviderServiceForBaseagent } from './providers-config.js';
7
+ export const PROVIDER_SECRET_MASK = '<redacted>';
8
+ function revisionValue(value) {
9
+ if (Array.isArray(value))
10
+ return value.map(revisionValue);
11
+ if (!value || typeof value !== 'object')
12
+ return value;
13
+ return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right))
14
+ .map(([key, entry]) => [key, revisionValue(entry)]));
15
+ }
16
+ export function providersRevision(value) {
17
+ return createHash('sha256').update(JSON.stringify(revisionValue({ providers: value.providers, ...(value.nextId !== undefined ? { nextId: value.nextId } : {}) }))).digest('hex');
18
+ }
19
+ export function readProvidersSnapshot(file = resolvePaths().providersConfig) {
20
+ return providersSnapshot(readProvidersFile(file));
21
+ }
22
+ function providersSnapshot(value) {
23
+ const providers = structuredClone(value.providers);
24
+ for (const provider of Object.values(providers)) {
25
+ if (provider.common.apiKey && !providerEnvironmentVariable(provider.common.apiKey))
26
+ provider.common.apiKey = PROVIDER_SECRET_MASK;
27
+ for (const field of ['headers', 'queryParams']) {
28
+ const values = provider.common[field];
29
+ if (values)
30
+ for (const key of Object.keys(values))
31
+ values[key] = PROVIDER_SECRET_MASK;
32
+ }
33
+ }
34
+ return {
35
+ ...(value.$schema_version !== undefined ? { $schema_version: value.$schema_version } : {}),
36
+ ...(value.nextId !== undefined ? { nextId: value.nextId } : {}),
37
+ providers,
38
+ revision: providersRevision(value),
39
+ };
40
+ }
41
+ function retainSecrets(value, previous) {
42
+ if (!value || typeof value !== 'object' || Array.isArray(value))
43
+ return value;
44
+ const provider = structuredClone(value);
45
+ if (!provider.common || typeof provider.common !== 'object' || Array.isArray(provider.common))
46
+ return provider;
47
+ const common = provider.common;
48
+ if (common.apiKey === PROVIDER_SECRET_MASK) {
49
+ if (!previous?.common.apiKey)
50
+ throw new Error('A replacement API key is required');
51
+ common.apiKey = previous.common.apiKey;
52
+ }
53
+ for (const field of ['headers', 'queryParams']) {
54
+ if (!common[field] || typeof common[field] !== 'object' || Array.isArray(common[field]))
55
+ continue;
56
+ for (const key of Object.keys(common[field])) {
57
+ if (common[field][key] !== PROVIDER_SECRET_MASK)
58
+ continue;
59
+ const existing = previous?.common[field]?.[key];
60
+ if (existing === undefined)
61
+ throw new Error(`A replacement ${field} value is required`);
62
+ common[field][key] = existing;
63
+ }
64
+ }
65
+ return provider;
66
+ }
67
+ export function resolveProviderDraftService(input, file = resolvePaths().providersConfig) {
68
+ if (!/^[A-Za-z0-9_-]+$/u.test(input.providerId) || ['__proto__', 'constructor', 'prototype'].includes(input.providerId)) {
69
+ throw new Error('Invalid provider identifier');
70
+ }
71
+ const current = readProvidersFile(file);
72
+ const previous = Object.hasOwn(current.providers, input.providerId) ? current.providers[input.providerId] : undefined;
73
+ const provider = resolveDraftProvider(input.value, previous, input.imports);
74
+ if (input.baseagent) {
75
+ return resolveProviderServiceForBaseagent(input.providerId, input.baseagent, input.serviceType, {
76
+ providers: { [input.providerId]: provider },
77
+ });
78
+ }
79
+ return resolveProviderService(input.providerId, input.serviceType, { providers: { [input.providerId]: provider } });
80
+ }
81
+ export async function updateProvider(input, file = resolvePaths().providersConfig) {
82
+ if (!/^[A-Za-z0-9_-]+$/u.test(input.providerId) || ['__proto__', 'constructor', 'prototype'].includes(input.providerId)) {
83
+ throw new Error('Invalid provider identifier');
84
+ }
85
+ const result = mutateConfig(ConfigTarget.Providers, { providerFile: file }, config => {
86
+ const current = config;
87
+ if (providersRevision(current) !== input.revision) {
88
+ throw Object.assign(new Error('Providers changed after loading; reload before saving'), { statusCode: 409 });
89
+ }
90
+ if (input.remove) {
91
+ current.nextId = nextProviderSequence(current);
92
+ delete current.providers[input.providerId];
93
+ }
94
+ else {
95
+ const value = retainSecrets(input.value, current.providers[input.providerId]);
96
+ const normalized = validateProvidersFile({ providers: { [input.providerId]: value } });
97
+ current.providers[input.providerId] = normalized.providers[input.providerId];
98
+ }
99
+ return current;
100
+ });
101
+ return providersSnapshot(result.value);
102
+ }
103
+ function rejectMaskedImportSecrets(providers) {
104
+ for (const [providerId, provider] of Object.entries(providers)) {
105
+ if (provider.common.apiKey === PROVIDER_SECRET_MASK) {
106
+ throw new Error(`Provider ${providerId} has a redacted API key; import a file with the original secret`);
107
+ }
108
+ for (const field of ['headers', 'queryParams']) {
109
+ for (const [key, value] of Object.entries(provider.common[field] ?? {})) {
110
+ if (value === PROVIDER_SECRET_MASK) {
111
+ throw new Error(`Provider ${providerId} ${field}.${key} is redacted; import a file with the original secret`);
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+ export function parseProviderImport(text) {
118
+ if (typeof text !== 'string' || !text.trim())
119
+ throw new Error('Paste one Provider configuration');
120
+ if (Buffer.byteLength(text, 'utf8') > 1024 * 1024) {
121
+ throw Object.assign(new Error('Provider configuration exceeds 1 MiB'), { statusCode: 413 });
122
+ }
123
+ let source = text.replace(/^\uFEFF/u, '').trim();
124
+ const fenced = source.match(/^```(?:json)?\s*\n([\s\S]*?)\n```$/iu);
125
+ if (fenced)
126
+ source = fenced[1];
127
+ let parsed;
128
+ try {
129
+ parsed = JSON.parse(source);
130
+ }
131
+ catch {
132
+ throw new Error('Provider configuration must be valid JSON');
133
+ }
134
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
135
+ throw new Error('Provider configuration must be an object');
136
+ const value = parsed;
137
+ const isProviderMap = Object.values(value).every(provider => provider && typeof provider === 'object'
138
+ && !Array.isArray(provider) && Object.hasOwn(provider, 'common'));
139
+ const isSingleProvider = !isProviderMap && ['name', 'common', 'services'].some(field => Object.hasOwn(value, field));
140
+ if (!Object.hasOwn(value, 'providers') && isSingleProvider && !Object.hasOwn(value, 'common')) {
141
+ throw new Error('Provider configuration is missing "common"; check the field spelling');
142
+ }
143
+ const normalized = validateProvidersFile(Object.hasOwn(value, 'providers') ? value
144
+ : { providers: isSingleProvider ? { imported: value } : value });
145
+ const providers = Object.values(normalized.providers);
146
+ if (providers.length !== 1)
147
+ throw new Error('Import exactly one Provider at a time');
148
+ rejectMaskedImportSecrets(normalized.providers);
149
+ const provider = providers[0];
150
+ provider.name ||= providerNameFromBaseUrlIfStatic(provider.common.baseUrl);
151
+ return provider;
152
+ }
153
+ export function previewProviderImport(text) {
154
+ return previewProviderValue(parseProviderImport(text));
155
+ }
156
+ export function previewProviderValue(value) {
157
+ const normalized = validateProvidersFile({ providers: { preview: value } });
158
+ return { provider: providersSnapshot(normalized).providers.preview };
159
+ }
160
+ function nextProviderSequence(config) {
161
+ let nextId = config.nextId ?? 1;
162
+ for (const providerId of Object.keys(config.providers)) {
163
+ const match = /^provider-(\d+)$/u.exec(providerId);
164
+ if (match)
165
+ nextId = Math.max(nextId, Number(match[1]) + 1);
166
+ }
167
+ if (!Number.isSafeInteger(nextId) || nextId >= Number.MAX_SAFE_INTEGER)
168
+ throw new Error('Provider ID sequence is exhausted');
169
+ return nextId;
170
+ }
171
+ export async function createProvider(input, file = resolvePaths().providersConfig) {
172
+ if (!/^[a-f0-9]{64}$/u.test(input.revision))
173
+ throw new Error('A valid providers revision is required');
174
+ const normalized = validateProvidersFile({ providers: { imported: input.value } });
175
+ rejectMaskedImportSecrets(normalized.providers);
176
+ const provider = normalized.providers.imported;
177
+ provider.name ||= providerNameFromBaseUrlIfStatic(provider.common.baseUrl);
178
+ const result = mutateConfig(ConfigTarget.Providers, { providerFile: file }, config => {
179
+ const current = config;
180
+ if (providersRevision(current) !== input.revision) {
181
+ throw Object.assign(new Error('Providers changed after loading; reload before saving'), { statusCode: 409 });
182
+ }
183
+ const sequence = nextProviderSequence(current);
184
+ const providerId = `provider-${sequence}`;
185
+ current.providers[providerId] = provider;
186
+ current.nextId = sequence + 1;
187
+ return { current, providerId };
188
+ });
189
+ return { ...providersSnapshot(result.value.current), providerId: result.value.providerId };
190
+ }
191
+ function providerHostname(baseUrl) {
192
+ if (providerEnvironmentVariable(baseUrl))
193
+ return undefined;
194
+ return new URL(baseUrl).hostname.toLowerCase().replace(/\.$/u, '');
195
+ }
196
+ function mergeImportedProvider(existing, imported) {
197
+ const services = structuredClone(existing.services || {});
198
+ const existingVariable = providerEnvironmentVariable(existing.common.baseUrl);
199
+ const importedVariable = providerEnvironmentVariable(imported.common.baseUrl);
200
+ if (existingVariable || importedVariable) {
201
+ if (existingVariable !== importedVariable) {
202
+ throw new Error('Cannot merge Providers with different environment-variable base URLs');
203
+ }
204
+ for (const [serviceType, service] of Object.entries(imported.services || {})) {
205
+ if (!Object.hasOwn(services, serviceType))
206
+ services[serviceType] = structuredClone(service);
207
+ }
208
+ return {
209
+ ...existing,
210
+ common: {
211
+ ...existing.common,
212
+ ...(!existing.common.apiKey?.trim() && imported.common.apiKey ? { apiKey: imported.common.apiKey } : {}),
213
+ },
214
+ services,
215
+ };
216
+ }
217
+ const baseUrl = new URL(existing.common.baseUrl);
218
+ for (const [serviceType, service] of Object.entries(imported.services || {})) {
219
+ if (Object.hasOwn(services, serviceType))
220
+ continue;
221
+ const endpoint = new URL(resolveProviderService('imported', serviceType, { providers: { imported } }).baseUrl);
222
+ if (baseUrl.protocol !== endpoint.protocol || baseUrl.port !== endpoint.port || providerHostname(baseUrl.href) !== providerHostname(endpoint.href)) {
223
+ throw new Error('Cannot merge this domain: the source uses a different protocol or port');
224
+ }
225
+ const relativePath = posix.relative(baseUrl.pathname.replace(/\/+$/u, '') || '/', endpoint.pathname.replace(/\/+$/u, '') || '/');
226
+ services[serviceType] = { ...service, path: relativePath ? `./${relativePath}` : '' };
227
+ }
228
+ return {
229
+ ...existing,
230
+ common: {
231
+ ...existing.common,
232
+ ...(!existing.common.apiKey?.trim() && imported.common.apiKey ? { apiKey: imported.common.apiKey } : {}),
233
+ },
234
+ services,
235
+ };
236
+ }
237
+ function resolveDraftProvider(value, previous, imports = []) {
238
+ if (!Array.isArray(imports))
239
+ throw new Error('Provider imports must be an array');
240
+ let secretSource = previous;
241
+ for (const item of imports) {
242
+ const imported = validateProvidersFile({ providers: { imported: item } }).providers.imported;
243
+ rejectMaskedImportSecrets({ imported });
244
+ const secretSourceHost = secretSource ? providerHostname(secretSource.common.baseUrl) : undefined;
245
+ const importedHost = providerHostname(imported.common.baseUrl);
246
+ if (secretSource && secretSourceHost && importedHost && secretSourceHost !== importedHost) {
247
+ throw new Error('Imported Provider domain does not match the target Provider');
248
+ }
249
+ secretSource = secretSource ? mergeImportedProvider(secretSource, imported) : imported;
250
+ }
251
+ return validateProvidersFile({ providers: { draft: retainSecrets(value, secretSource) } }).providers.draft;
252
+ }
253
+ export async function saveProviderChanges(input, file = resolvePaths().providersConfig) {
254
+ if (!Array.isArray(input.changes) || !input.changes.length)
255
+ throw new Error('Provider changes are required');
256
+ const identifiers = new Set();
257
+ for (const change of input.changes) {
258
+ if (!change || typeof change.providerId !== 'string' || !/^[A-Za-z0-9_-]+$/u.test(change.providerId)
259
+ || ['__proto__', 'constructor', 'prototype'].includes(change.providerId) || identifiers.has(change.providerId)) {
260
+ throw new Error('Invalid or duplicate provider identifier');
261
+ }
262
+ if ((change.create !== undefined && typeof change.create !== 'boolean')
263
+ || (change.remove !== undefined && typeof change.remove !== 'boolean')
264
+ || (change.imports !== undefined && !Array.isArray(change.imports))
265
+ || (change.remove && (change.create || change.value !== undefined || change.imports?.length))) {
266
+ throw new Error('Invalid provider change');
267
+ }
268
+ identifiers.add(change.providerId);
269
+ }
270
+ const result = mutateConfig(ConfigTarget.Providers, { providerFile: file }, config => {
271
+ const current = config;
272
+ if (providersRevision(current) !== input.revision) {
273
+ throw Object.assign(new Error('Providers changed after loading; reload before saving'), { statusCode: 409 });
274
+ }
275
+ const providerIds = {};
276
+ const originalIds = new Set(Object.keys(current.providers));
277
+ if (input.changes.some(change => change.remove))
278
+ current.nextId = nextProviderSequence(current);
279
+ for (const change of input.changes) {
280
+ if (change.create ? originalIds.has(change.providerId) : !originalIds.has(change.providerId)) {
281
+ throw new Error(change.create ? 'Provider already exists' : 'Provider not found');
282
+ }
283
+ if (change.remove) {
284
+ delete current.providers[change.providerId];
285
+ continue;
286
+ }
287
+ const previous = change.create ? undefined : current.providers[change.providerId];
288
+ const provider = resolveDraftProvider(change.value, previous, change.imports);
289
+ provider.name ||= providerNameFromBaseUrlIfStatic(provider.common.baseUrl);
290
+ const providerId = change.create ? `provider-${nextProviderSequence(current)}` : change.providerId;
291
+ if (change.create)
292
+ current.nextId = Number(providerId.slice('provider-'.length)) + 1;
293
+ current.providers[providerId] = provider;
294
+ providerIds[change.providerId] = providerId;
295
+ }
296
+ return { current, providerIds };
297
+ });
298
+ return { ...providersSnapshot(result.value.current), providerIds: result.value.providerIds };
299
+ }
300
+ export async function importOrMergeBaseagentProvider(input, file = resolvePaths().providersConfig) {
301
+ if (!/^[a-f0-9]{64}$/u.test(input.revision))
302
+ throw new Error('A valid providers revision is required');
303
+ const normalized = validateProvidersFile({ providers: { imported: input.value } });
304
+ rejectMaskedImportSecrets(normalized.providers);
305
+ const imported = normalized.providers.imported;
306
+ imported.name ||= providerNameFromBaseUrlIfStatic(imported.common.baseUrl);
307
+ const result = mutateConfig(ConfigTarget.Providers, { providerFile: file }, config => {
308
+ const current = config;
309
+ if (providersRevision(current) !== input.revision) {
310
+ throw Object.assign(new Error('Providers changed after loading; reload before saving'), { statusCode: 409 });
311
+ }
312
+ const importedHost = providerHostname(imported.common.baseUrl);
313
+ const match = importedHost
314
+ ? Object.entries(current.providers).find(([, provider]) => providerHostname(provider.common.baseUrl) === importedHost)
315
+ : undefined;
316
+ if (match) {
317
+ const [providerId, existing] = match;
318
+ current.providers[providerId] = validateProvidersFile({ providers: { [providerId]: mergeImportedProvider(existing, imported) } }).providers[providerId];
319
+ return { current, providerId, merged: true };
320
+ }
321
+ const sequence = nextProviderSequence(current);
322
+ const providerId = `provider-${sequence}`;
323
+ current.providers[providerId] = imported;
324
+ current.nextId = sequence + 1;
325
+ return { current, providerId, merged: false };
326
+ });
327
+ return { ...providersSnapshot(result.value.current), providerId: result.value.providerId, merged: result.value.merged };
328
+ }
@@ -0,0 +1,22 @@
1
+ import { resolvePaths } from '../paths.js';
2
+ import { ConfigTarget, read, write } from './config-manager.js';
3
+ import { assertProviderSelectionWrite as validateSelectionWrite, resolveProviderModelOverrides as resolveModelOverrides, resolveProviderService as resolveService, resolveProviderServiceForBaseagent as resolveServiceForBaseagent, resolveServiceSelection as resolveSelection, } from './providers-config.js';
4
+ export { providerIsConfigured, providerServiceTypeForBaseagent, providerServiceTypesForBaseagent, providerServiceTypeForPurpose, validateProvidersFile, } from './providers-config.js';
5
+ export function readProvidersFile(file = resolvePaths().providersConfig) {
6
+ return read(ConfigTarget.Providers, { providerFile: file }) ?? { providers: {} };
7
+ }
8
+ export function writeProvidersFile(value, file = resolvePaths().providersConfig) {
9
+ write(ConfigTarget.Providers, value, { providerFile: file });
10
+ }
11
+ export function resolveProviderService(providerId, serviceType, file = resolvePaths().providersConfig) {
12
+ return resolveService(providerId, serviceType, readProvidersFile(file));
13
+ }
14
+ export function resolveProviderServiceForBaseagent(providerId, baseagent, preferredServiceType, file = resolvePaths().providersConfig) {
15
+ return resolveServiceForBaseagent(providerId, baseagent, preferredServiceType, readProvidersFile(file));
16
+ }
17
+ export function resolveServiceSelection(selections, serviceType) {
18
+ return resolveSelection(selections, serviceType, resolveProviderService);
19
+ }
20
+ export function assertProviderSelectionWrite(next, previous = {}, inherited = {}) {
21
+ validateSelectionWrite(next, previous, inherited, (providerId, serviceType) => resolveModelOverrides(providerId, serviceType, readProvidersFile()));
22
+ }
@@ -26,7 +26,7 @@ export function resolveConfigCommand(inputArgv, options = {}) {
26
26
  return { ok: false, code: 'MISSING_ARG', reason: 'A config subcommand is required' };
27
27
  }
28
28
  if (options.defaultRelation?.self && options.defaultRelation.peerKey
29
- && isScopedConfigSubcommand(subcommand)
29
+ && (isScopedConfigSubcommand(subcommand) || subcommand === 'get' || subcommand === 'set' || subcommand === 'unset')
30
30
  && !hasExplicitConfigSelector(argv)) {
31
31
  argv = [...argv, '--self', options.defaultRelation.self, '--peer', options.defaultRelation.peerKey];
32
32
  }
@@ -118,14 +118,14 @@ function resolveFieldCommand(argv, subcommand, options = {}) {
118
118
  reason: 'Agent role policies are managed through the role policy editor',
119
119
  };
120
120
  }
121
- const canContainFallbackModels = field === 'baseagents'
121
+ const canContainFallback = field === 'baseagents'
122
122
  || /^baseagents\.[^.]+$/.test(field)
123
- || /^baseagents\.[^.]+\.fallbackModels(?:\.|$)/.test(field);
124
- if (subcommand !== 'get' && canContainFallbackModels) {
123
+ || /^baseagents\.[^.]+\.fallback(?:\.|$)/.test(field);
124
+ if (subcommand !== 'get' && canContainFallback) {
125
125
  return {
126
126
  ok: false,
127
127
  code: 'DEFAULTS_FILE_ONLY',
128
- reason: 'fallbackModels is configured only by editing agents/defaults.json',
128
+ reason: 'Baseagent fallback is configured only by editing agents/defaults.json',
129
129
  };
130
130
  }
131
131
  const fieldRule = resolveConfigFieldRule(field, selector.scope);
@@ -176,7 +176,13 @@ function resolveFieldCommand(argv, subcommand, options = {}) {
176
176
  const rawValue = subcommand === 'set' ? business.values[1] : undefined;
177
177
  let value;
178
178
  if (subcommand === 'set') {
179
- if (field === 'owners') {
179
+ if (field === 'owner') {
180
+ const owner = rawValue.trim();
181
+ if (!isValidAid(owner))
182
+ return { ok: false, code: 'INVALID_CONFIG_VALUE', reason: `Invalid owner AID: ${JSON.stringify(rawValue)}` };
183
+ value = owner;
184
+ }
185
+ else if (field === 'owners') {
180
186
  const parsedOwners = parseOwnersValue(rawValue);
181
187
  if (!parsedOwners.ok)
182
188
  return parsedOwners;
@@ -556,14 +562,11 @@ function parseOwnersValue(raw) {
556
562
  if (!Array.isArray(values) || values.length === 0) {
557
563
  return { ok: false, code: 'INVALID_CONFIG_VALUE', reason: 'owners must contain at least one AID' };
558
564
  }
559
- const owners = [];
560
- for (const value of values) {
561
- if (typeof value !== 'string' || !value.trim() || !isValidAid(value.trim())) {
562
- return { ok: false, code: 'INVALID_CONFIG_VALUE', reason: `Invalid owner AID: ${JSON.stringify(value)}` };
563
- }
564
- const owner = value.trim();
565
- if (!owners.includes(owner))
566
- owners.push(owner);
565
+ const owners = [...new Set(values
566
+ .filter((value) => typeof value === 'string' && !!value.trim())
567
+ .map(value => value.trim()))];
568
+ if (owners.length === 0 || owners.some(owner => !isValidAid(owner))) {
569
+ return { ok: false, code: 'INVALID_CONFIG_VALUE', reason: `Invalid owner AID: ${JSON.stringify(owners)}` };
567
570
  }
568
571
  return { ok: true, value: owners };
569
572
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * snapshot —— 配置快照与回滚。
3
3
  *
4
- * v3 设计快照范围:daemon.json + agents/defaults.json + 每 agent 的 config.json
5
- * + 每关系的 config.json + extra_backup 声明文件。**不含 .env**(凭证不进版本库)。
4
+ * v3 设计快照范围:daemon.json + providers.json + agents/defaults.json + 每 agent 的 config.json
5
+ * + 每关系的 config.json + extra_backup 声明文件。**不含 .env**。
6
6
  *
7
7
  * 版本号:全量百位递增(v100/v200…),增量嵌套其下(v101…v199,每全量 ≤99 个增量)。
8
8
  * 目录:{root}/backups/config/v100/{meta.json, snapshot/...} + v101/{meta.json, delta/...}
@@ -16,7 +16,7 @@ import fs from 'fs';
16
16
  import path from 'path';
17
17
  import { resolvePaths } from '../paths.js';
18
18
  import { logger } from '../utils/logger.js';
19
- import { ConfigTarget, read } from './config-manager.js';
19
+ import { ConfigTarget, notifyConfigWrite, read } from './config-manager.js';
20
20
  const RETAIN_FULL = 10;
21
21
  const RETAIN_DELTA = 20;
22
22
  /** Process-level master switch. Missing config remains enabled for compatibility. */
@@ -56,6 +56,7 @@ export function collectConfigFiles(root) {
56
56
  const pushIf = (abs) => { if (fs.existsSync(abs))
57
57
  out.push(rel(abs)); };
58
58
  pushIf(path.join(root, 'daemon.json'));
59
+ pushIf(path.join(root, 'providers.json'));
59
60
  const agentsRoot = path.join(root, 'agents');
60
61
  pushIf(path.join(agentsRoot, 'defaults.json'));
61
62
  if (fs.existsSync(agentsRoot)) {
@@ -342,9 +343,7 @@ function createFull(root, work, trigger, description) {
342
343
  const verDir = path.join(backupsDir(), version);
343
344
  const snapDir = path.join(verDir, 'snapshot');
344
345
  for (const [rel, content] of work) {
345
- const dest = path.join(snapDir, rel);
346
- fs.mkdirSync(path.dirname(dest), { recursive: true });
347
- fs.writeFileSync(dest, content, 'utf-8');
346
+ writeSnapshotConfigFile(snapDir, rel, content);
348
347
  }
349
348
  const meta = {
350
349
  version, type: 'full', createdAt: nowIso(), trigger, description,
@@ -388,9 +387,25 @@ function createDelta(root, work, baseFull, baseMap, trigger, description) {
388
387
  return { created: true, version, type: 'delta' };
389
388
  }
390
389
  function writeDeltaFile(filesDir, rel, content) {
391
- const dest = path.join(filesDir, rel);
392
- fs.mkdirSync(path.dirname(dest), { recursive: true });
393
- fs.writeFileSync(dest, content, 'utf-8');
390
+ writeSnapshotConfigFile(filesDir, rel, content);
391
+ }
392
+ export function writeSnapshotConfigFile(root, rel, content) {
393
+ const dest = path.join(root, rel);
394
+ const protectedProvider = rel === 'providers.json';
395
+ fs.mkdirSync(path.dirname(dest), { recursive: true, ...(protectedProvider ? { mode: 0o700 } : {}) });
396
+ fs.writeFileSync(dest, content, { encoding: 'utf8', ...(protectedProvider ? { mode: 0o600 } : {}) });
397
+ if (protectedProvider && process.platform !== 'win32')
398
+ fs.chmodSync(dest, 0o600);
399
+ }
400
+ export function removeSnapshotConfigFile(root, rel) {
401
+ const dest = path.join(root, rel);
402
+ const files = rel === 'providers.json' ? [dest, `${dest}_`, `${dest}__`] : [dest];
403
+ for (const file of files) {
404
+ try {
405
+ fs.unlinkSync(file);
406
+ }
407
+ catch { }
408
+ }
394
409
  }
395
410
  function nowIso() {
396
411
  // Date 在 workflow 脚本里被禁;此处是运行时模块,正常可用。
@@ -422,20 +437,17 @@ export function restore(version) {
422
437
  // 删除工作目录中目标版本没有的配置文件
423
438
  for (const rel of current.keys()) {
424
439
  if (!target.has(rel)) {
425
- try {
426
- fs.unlinkSync(path.join(root, rel));
427
- }
428
- catch { /* best effort */ }
440
+ removeSnapshotConfigFile(root, rel);
429
441
  }
430
442
  }
431
443
  // 写入目标版本所有文件
432
444
  let applied = 0;
433
445
  for (const [rel, content] of target) {
434
- const dest = path.join(root, rel);
435
- fs.mkdirSync(path.dirname(dest), { recursive: true });
436
- fs.writeFileSync(dest, content, 'utf-8');
446
+ writeSnapshotConfigFile(root, rel, content);
437
447
  applied++;
438
448
  }
449
+ if (current.has('providers.json') || target.has('providers.json'))
450
+ notifyConfigWrite(ConfigTarget.Providers);
439
451
  writeCurrent({ full: loc.full, delta: loc.delta });
440
452
  writeWVersion({ full: loc.full, delta: loc.delta });
441
453
  logger.info(`[snapshot] restored ${version} (${applied} files)`);
@@ -32,6 +32,7 @@ import { parseStableSemver } from './utils/stable-semver.js';
32
32
  import { isSupportedBaseagent } from './agents/baseagent.js';
33
33
  import { normalizeDaemonServices } from './config/daemon-services.js';
34
34
  import { currentVersion } from './config/schema-registry.js';
35
+ import { assertOwnerConfigRecord } from './config/owner-config.js';
35
36
  const SERVICE_METADATA_SENSITIVE_KEY = /(?:^|[_-])(endpoint|url|uri|token|access[_-]?token|authorization|cookie|secret|password|private[_-]?key|key|cert|certificate)(?:$|[_-])/i;
36
37
  function assertLocalServiceEndpoint(value, fieldPath) {
37
38
  if (typeof value !== 'string' || value.trim() === '' || value.length > 2048) {
@@ -56,7 +57,8 @@ function assertLocalServiceEndpoint(value, fieldPath) {
56
57
  export function loadDaemonConfig() {
57
58
  const raw = atomicReadJson(resolvePaths().daemonConfig);
58
59
  const sanitized = stripLegacyAunGatewayConfig(raw ?? {}, resolvePaths().daemonConfig, message => logger.warn(message));
59
- return validateDaemonConfig(normalizeProcessConfigCompat(normalizeDaemonServices(sanitized)), false);
60
+ const normalized = validateDaemonConfig(normalizeProcessConfigCompat(normalizeDaemonServices(sanitized)), false);
61
+ return normalized;
60
62
  }
61
63
  export function isRestartRecoveryEnabled(config = loadDaemonConfig()) {
62
64
  return config.restartRecovery?.enabled === true;
@@ -111,6 +113,15 @@ export function saveDaemonConfig(value) {
111
113
  catch { /* best-effort */ }
112
114
  }
113
115
  function validateDaemonConfig(value, validateAidDomain) {
116
+ assertOwnerConfigRecord(value, 'daemon.json', isValidAid);
117
+ if (value.upgrade !== undefined) {
118
+ if (!value.upgrade || typeof value.upgrade !== 'object' || Array.isArray(value.upgrade))
119
+ throw new Error('daemon.json.upgrade must be an object');
120
+ for (const [field, enabled] of Object.entries(value.upgrade)) {
121
+ if (!['autoOnStart', 'testPackages'].includes(field) || typeof enabled !== 'boolean')
122
+ throw new Error(`daemon.json.upgrade.${field} must be a supported boolean setting`);
123
+ }
124
+ }
114
125
  if (value.restartRecovery !== undefined) {
115
126
  if (!value.restartRecovery || typeof value.restartRecovery !== 'object' || Array.isArray(value.restartRecovery)) {
116
127
  throw new Error('daemon.json.restartRecovery must be an object');
@@ -498,24 +509,17 @@ export function validateAgentConfig(cfg) {
498
509
  errs.push('channels must be an array');
499
510
  return errs;
500
511
  }
512
+ if (cfg.owner !== undefined && (typeof cfg.owner !== 'string' || !isValidAid(cfg.owner))) {
513
+ errs.push(`invalid owner: ${cfg.owner}`);
514
+ }
501
515
  if (cfg.owners !== undefined) {
502
- if (!Array.isArray(cfg.owners)) {
516
+ if (!Array.isArray(cfg.owners))
503
517
  errs.push('owners must be an array');
504
- }
505
- else {
506
- const seenOwners = new Set();
518
+ else
507
519
  cfg.owners.forEach((owner, i) => {
508
- if (typeof owner !== 'string' || !isValidAid(owner)) {
520
+ if (typeof owner !== 'string' || !isValidAid(owner))
509
521
  errs.push(`owners[${i}] invalid aid: ${owner}`);
510
- return;
511
- }
512
- if (seenOwners.has(owner)) {
513
- errs.push(`owners[${i}] duplicate aid: ${owner}`);
514
- return;
515
- }
516
- seenOwners.add(owner);
517
522
  });
518
- }
519
523
  }
520
524
  let aunCount = 0;
521
525
  const seenNamesByType = new Map();