evolcore 0.0.39 → 0.0.40

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 (102) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +31 -20
  3. package/bin/ec-safe-output.js +14 -2
  4. package/dist/agents/baseagent-availability.js +1 -0
  5. package/dist/agents/claude-runner.js +32 -46
  6. package/dist/agents/codex-runner.js +51 -27
  7. package/dist/agents/ecagent-runner.js +18 -7
  8. package/dist/agents/gemini-runner.js +1 -2
  9. package/dist/agents/runner-types.js +28 -6
  10. package/dist/aun/aid/client.js +1 -1
  11. package/dist/aun/aid/control-aid.js +23 -98
  12. package/dist/aun/aid/ownership-operation.js +1 -3
  13. package/dist/aun/group/invite-service.js +6 -0
  14. package/dist/aun/msg/p2p.js +8 -12
  15. package/dist/channels/aun.js +66 -48
  16. package/dist/channels/dingtalk.js +8 -2
  17. package/dist/channels/feishu.js +6 -1
  18. package/dist/channels/wecom.js +7 -3
  19. package/dist/cli/agent-create.js +23 -4
  20. package/dist/cli/aid-bind-command.js +4 -1
  21. package/dist/cli/daemon-commands.js +242 -17
  22. package/dist/cli/index.js +25 -13
  23. package/dist/cli/memory-command.js +33 -14
  24. package/dist/cli/restart-monitor.js +35 -13
  25. package/dist/cli/task-context.js +1 -0
  26. package/dist/config/config-field-policy.js +1 -0
  27. package/dist/contracts/ecweb-service-protocol.js +44 -0
  28. package/dist/core/agent-runtime.js +6 -0
  29. package/dist/core/auth/agent-delegation.js +7 -4
  30. package/dist/core/auth/auth-gateway.js +3 -1
  31. package/dist/core/auth/authorization-audit.js +5 -1
  32. package/dist/core/auth/debug-context.js +5 -1
  33. package/dist/core/auth/operation-catalog.js +16 -0
  34. package/dist/core/command/command-handler.js +7 -1
  35. package/dist/core/command/menu-catalog.js +48 -12
  36. package/dist/core/command/menu-handler.js +72 -4
  37. package/dist/core/command/menu-protocol.js +20 -1
  38. package/dist/core/command/slash-gate.js +17 -3
  39. package/dist/core/command/slash-handler.js +90 -7
  40. package/dist/core/event-catalog.js +2 -0
  41. package/dist/core/interaction-router.js +2 -1
  42. package/dist/core/ipc-registration.js +26 -7
  43. package/dist/core/memory/batch-meta.js +2 -0
  44. package/dist/core/memory/consolidation-context.js +42 -51
  45. package/dist/core/memory/consolidation-ops-records.js +39 -7
  46. package/dist/core/memory/consolidation-trigger.js +71 -64
  47. package/dist/core/memory/consolidation.js +226 -20
  48. package/dist/core/memory/memory-writer.js +160 -34
  49. package/dist/core/memory/relation-idle-monitor.js +287 -0
  50. package/dist/core/memory/relation-sweep.js +206 -0
  51. package/dist/core/message/aun-ipc-sender.js +5 -0
  52. package/dist/core/message/message-bridge.js +64 -55
  53. package/dist/core/message/message-log.js +17 -0
  54. package/dist/core/message/message-queue.js +6 -0
  55. package/dist/core/message/outbound.js +70 -0
  56. package/dist/core/message/peer-mode.js +7 -2
  57. package/dist/core/message/response-engine.js +341 -73
  58. package/dist/core/ownership/owner-resolver.js +384 -178
  59. package/dist/core/permission/ec-command-parser.js +19 -3
  60. package/dist/core/permission/tool-policy.js +7 -7
  61. package/dist/core/role/runtime-policy.js +64 -0
  62. package/dist/core/session/session-manager.js +25 -5
  63. package/dist/core/system-session/memory.js +3 -18
  64. package/dist/ecweb/basic-settings-service.js +709 -0
  65. package/dist/ecweb/providers-service.js +172 -0
  66. package/dist/index.js +92 -4
  67. package/dist/ipc.js +78 -11
  68. package/dist/stats/db.js +33 -0
  69. package/dist/stats/index.js +2 -2
  70. package/dist/stats/query.js +246 -0
  71. package/dist/stats/writer.js +41 -1
  72. package/dist/trigger/application-service.js +12 -0
  73. package/dist/trigger/authorization.js +2 -2
  74. package/dist/trigger/history.js +36 -0
  75. package/dist/trigger/scheduler.js +3 -0
  76. package/dist/utils/ecweb-supervisor.js +31 -10
  77. package/dist/utils/error-utils.js +8 -4
  78. package/dist/utils/evolcore-version.js +3 -2
  79. package/dist/utils/host-info.js +52 -0
  80. package/ecagent/dist/harness/agent-harness.js +4 -1
  81. package/kits/docs/evolcore/INDEX.md +1 -1
  82. package/kits/docs/evolcore/agent.md +1 -1
  83. package/kits/docs/evolcore/aid.md +29 -12
  84. package/kits/docs/evolcore/config.md +9 -7
  85. package/kits/docs/evolcore/memory-architecture.md +3 -4
  86. package/kits/docs/evolcore/memory-consolidation.md +28 -7
  87. package/kits/docs/evolcore/memory-tags.md +1 -1
  88. package/kits/docs/evolcore/memory.md +1 -1
  89. package/kits/docs/evolcore/menu-client.md +1 -1
  90. package/kits/docs/evolcore/menu-protocol.md +6 -0
  91. package/kits/docs/evolcore/msg.md +1 -1
  92. package/kits/eck_manifest.json +3 -3
  93. package/kits/rules/04-relation.md +3 -2
  94. package/kits/schemas/agent-config.schema.13.json +5 -1
  95. package/kits/templates/memory-consolidation/generalize.md +32 -4
  96. package/kits/templates/memory-consolidation/notes.md +2 -2
  97. package/kits/templates/memory-consolidation/promote.md +10 -2
  98. package/kits/templates/system-fragments/baseagent.md +5 -0
  99. package/kits/templates/system-fragments/memory.md +12 -3
  100. package/kits/templates/system-fragments/session.md +1 -1
  101. package/kits/{rules/07-memory-and-thread.md → templates/system-fragments/thread.md} +17 -80
  102. package/package.json +3 -3
@@ -0,0 +1,709 @@
1
+ import crypto from 'node:crypto';
2
+ import path from 'node:path';
3
+ import { ConfigTarget, mutateConfig, read, validateConfig, } from '../config/config-manager.js';
4
+ const BASEAGENTS = ['claude', 'codex', 'gemini', 'ecagent'];
5
+ const BASEAGENT_SET = new Set(BASEAGENTS);
6
+ const EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
7
+ const PROHIBITED_PATH_SEGMENTS = new Set(['__proto__', 'prototype', 'constructor']);
8
+ const HEADER_NAME = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/u;
9
+ const AID_DOMAIN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$/u;
10
+ const SERVICE_NAME = /^[a-z0-9_-]+$/u;
11
+ const SERVICE_SOURCES = new Set(['instance', 'static']);
12
+ const SERVICE_TYPES = new Set(['http', 'websocket', 'ssh']);
13
+ const SERVICE_VISIBILITIES = new Set(['public', 'private', 'aun-auth']);
14
+ const SENSITIVE_METADATA_KEY = /(?:^|[_-])(endpoint|url|uri|token|access[_-]?token|authorization|cookie|secret|password|private[_-]?key|key|cert|certificate)(?:$|[_-])/iu;
15
+ const DAEMON_EXACT_PATHS = new Set([
16
+ 'aun.defaultAidDomain',
17
+ 'aun.minEvolVersion',
18
+ 'aun.menuTokenRequired',
19
+ 'aun.defaultEncrypt',
20
+ 'services',
21
+ 'configSnapshots',
22
+ 'restartRecovery.enabled',
23
+ 'upgrade.autoOnStart',
24
+ 'upgrade.testPackages',
25
+ 'delegationDiagnostics.enabled',
26
+ 'idleMonitor.enabled',
27
+ 'idleMonitor.timeout',
28
+ 'idleMonitor.apiRetryTimeout',
29
+ 'idleMonitor.maxExecutionTime',
30
+ 'watch.logTypes',
31
+ 'debug.logLevel',
32
+ 'debug.aunTrace',
33
+ 'debug.aunSdkLog',
34
+ 'debug.upmsg',
35
+ 'debug.eckSnapshots',
36
+ ]);
37
+ const DEFAULTS_EXACT_PATHS = new Set([
38
+ 'active_baseagent',
39
+ 'Services',
40
+ 'projects.defaultPath',
41
+ 'responseMode',
42
+ 'responseModeParams.single-session.pre_tool_1stmsgchk',
43
+ 'responseModeParams.single-session.tool_use_reminder',
44
+ 'config.chatMode',
45
+ 'config.mentionMode',
46
+ 'config.show_activities',
47
+ 'session_renew.enabled',
48
+ 'session_renew.after_hours',
49
+ 'session_renew.fallback_action',
50
+ ]);
51
+ const BASEAGENT_COMMON_FIELDS = new Set([
52
+ 'provider',
53
+ 'providerServiceType',
54
+ 'model',
55
+ 'auxiliaryModel',
56
+ 'auxiliaryEffort',
57
+ 'fallback',
58
+ ]);
59
+ const BASEAGENT_FIELDS = {
60
+ claude: new Set([
61
+ ...BASEAGENT_COMMON_FIELDS,
62
+ 'effort',
63
+ 'pathToClaudeCodeExecutable',
64
+ 'useSettingSources',
65
+ 'agentProgressSummaries',
66
+ 'excludeDynamicSections',
67
+ ]),
68
+ codex: new Set([
69
+ ...BASEAGENT_COMMON_FIELDS,
70
+ 'effort',
71
+ 'enableRequestUserInput',
72
+ 'approvalsReviewer',
73
+ ]),
74
+ gemini: new Set([
75
+ ...BASEAGENT_COMMON_FIELDS,
76
+ 'cliPath',
77
+ 'mode',
78
+ 'useVertex',
79
+ 'project',
80
+ 'location',
81
+ ]),
82
+ ecagent: new Set([
83
+ ...BASEAGENT_COMMON_FIELDS,
84
+ 'effort',
85
+ 'timeoutMs',
86
+ 'maxRetries',
87
+ 'maxRetryDelayMs',
88
+ ]),
89
+ };
90
+ const BOOLEAN_PATHS = new Set([
91
+ 'upgrade.autoOnStart',
92
+ 'upgrade.testPackages',
93
+ 'aun.menuTokenRequired',
94
+ 'aun.defaultEncrypt',
95
+ 'configSnapshots',
96
+ 'restartRecovery.enabled',
97
+ 'delegationDiagnostics.enabled',
98
+ 'idleMonitor.enabled',
99
+ 'debug.aunTrace',
100
+ 'debug.aunSdkLog',
101
+ 'debug.upmsg',
102
+ 'debug.eckSnapshots',
103
+ 'responseModeParams.single-session.pre_tool_1stmsgchk',
104
+ 'responseModeParams.single-session.tool_use_reminder',
105
+ 'session_renew.enabled',
106
+ ]);
107
+ const BASEAGENT_BOOLEAN_FIELDS = new Set([
108
+ 'useSettingSources',
109
+ 'agentProgressSummaries',
110
+ 'excludeDynamicSections',
111
+ 'enableRequestUserInput',
112
+ 'useVertex',
113
+ ]);
114
+ const RESTART_DAEMON_PREFIXES = [
115
+ 'aun.',
116
+ 'services',
117
+ 'configSnapshots',
118
+ 'restartRecovery.',
119
+ 'idleMonitor.',
120
+ 'debug.',
121
+ ];
122
+ const RESTART_DEFAULTS_PREFIXES = [
123
+ 'active_baseagent',
124
+ 'baseagents.',
125
+ ];
126
+ function isRecord(value) {
127
+ return !!value && typeof value === 'object' && !Array.isArray(value);
128
+ }
129
+ function response(status, body) {
130
+ return { status, body };
131
+ }
132
+ function makeError(message, code, statusCode = 400) {
133
+ return Object.assign(new Error(message), { code, statusCode });
134
+ }
135
+ function stableValue(value) {
136
+ if (Array.isArray(value))
137
+ return value.map(stableValue);
138
+ if (!isRecord(value))
139
+ return value;
140
+ return Object.fromEntries(Object.entries(value)
141
+ .sort(([left], [right]) => left.localeCompare(right))
142
+ .map(([key, nested]) => [key, stableValue(nested)]));
143
+ }
144
+ function normalizeOwnerForRevision(value) {
145
+ if (!isRecord(value))
146
+ return value;
147
+ const normalized = { ...value };
148
+ const explicitOwner = typeof normalized.owner === 'string' ? normalized.owner.trim() : '';
149
+ const legacyOwner = Array.isArray(normalized.owners)
150
+ ? normalized.owners.find((item) => typeof item === 'string' && item.trim())?.trim() || ''
151
+ : '';
152
+ const owner = explicitOwner || legacyOwner;
153
+ delete normalized.owners;
154
+ if (owner)
155
+ normalized.owner = owner;
156
+ else
157
+ delete normalized.owner;
158
+ return normalized;
159
+ }
160
+ function settingsRevision(config) {
161
+ return crypto.createHash('sha256')
162
+ .update(JSON.stringify(stableValue(normalizeOwnerForRevision(config))))
163
+ .digest('hex');
164
+ }
165
+ function targetFor(file) {
166
+ return file === 'daemon' ? ConfigTarget.Process : ConfigTarget.Defaults;
167
+ }
168
+ function readSettingsFile(file) {
169
+ return read(targetFor(file), undefined, { cache: false, expand: false });
170
+ }
171
+ export function readBasicSettings() {
172
+ const rawDaemon = readSettingsFile('daemon');
173
+ const rawDefaults = readSettingsFile('defaults');
174
+ const daemon = rawDaemon ? clone(rawDaemon) : null;
175
+ if (daemon) {
176
+ delete daemon.owner;
177
+ delete daemon.owners;
178
+ }
179
+ return {
180
+ ok: true,
181
+ daemon,
182
+ defaults: rawDefaults,
183
+ revisions: {
184
+ daemon: settingsRevision(rawDaemon ?? { $schema_version: 8 }),
185
+ defaults: settingsRevision(rawDefaults ?? { $schema_version: 5 }),
186
+ },
187
+ };
188
+ }
189
+ function pathSegments(fieldPath) {
190
+ if (typeof fieldPath !== 'string' || !fieldPath || fieldPath.startsWith('.') || fieldPath.endsWith('.') || fieldPath.includes('..')) {
191
+ throw makeError(`Invalid settings path: ${typeof fieldPath === 'string' && fieldPath ? fieldPath : '(empty)'}`, 'INVALID_SETTINGS_PATH');
192
+ }
193
+ const segments = fieldPath.split('.');
194
+ if (segments.some(segment => !segment || PROHIBITED_PATH_SEGMENTS.has(segment))) {
195
+ throw makeError(`Unsafe settings path: ${fieldPath}`, 'UNSAFE_SETTINGS_PATH');
196
+ }
197
+ return segments;
198
+ }
199
+ function baseagentPath(fieldPath) {
200
+ const match = fieldPath.match(/^baseagents\.([a-z0-9_-]+)\.([A-Za-z][A-Za-z0-9]*)$/u);
201
+ return match ? { baseagent: match[1], field: match[2] } : null;
202
+ }
203
+ function baseagentBlockPath(fieldPath) {
204
+ const match = fieldPath.match(/^baseagents\.([a-z0-9_-]+)$/u);
205
+ return match?.[1] || null;
206
+ }
207
+ function isAllowedPath(file, fieldPath) {
208
+ try {
209
+ pathSegments(fieldPath);
210
+ }
211
+ catch {
212
+ return false;
213
+ }
214
+ if (file === 'daemon')
215
+ return DAEMON_EXACT_PATHS.has(fieldPath);
216
+ if (DEFAULTS_EXACT_PATHS.has(fieldPath))
217
+ return true;
218
+ const block = baseagentBlockPath(fieldPath);
219
+ if (block)
220
+ return BASEAGENT_SET.has(block);
221
+ const parsed = baseagentPath(fieldPath);
222
+ return !!parsed && BASEAGENT_SET.has(parsed.baseagent) && BASEAGENT_FIELDS[parsed.baseagent]?.has(parsed.field) === true;
223
+ }
224
+ function assertString(value, fieldPath, options = {}) {
225
+ if (typeof value !== 'string')
226
+ throw makeError(`${fieldPath} must be a string`, 'INVALID_SETTINGS_VALUE');
227
+ if (!options.allowEmpty && !value.trim())
228
+ throw makeError(`${fieldPath} cannot be empty`, 'INVALID_SETTINGS_VALUE');
229
+ if (value.length > (options.max ?? 4096))
230
+ throw makeError(`${fieldPath} is too long`, 'INVALID_SETTINGS_VALUE');
231
+ return value;
232
+ }
233
+ function assertBoolean(value, fieldPath) {
234
+ if (typeof value !== 'boolean')
235
+ throw makeError(`${fieldPath} must be a boolean`, 'INVALID_SETTINGS_VALUE');
236
+ }
237
+ function assertFiniteNumber(value, fieldPath, options = {}) {
238
+ if (typeof value !== 'number' || !Number.isFinite(value))
239
+ throw makeError(`${fieldPath} must be a finite number`, 'INVALID_SETTINGS_VALUE');
240
+ if (options.integer && !Number.isInteger(value))
241
+ throw makeError(`${fieldPath} must be an integer`, 'INVALID_SETTINGS_VALUE');
242
+ if (options.min !== undefined && value < options.min)
243
+ throw makeError(`${fieldPath} must be at least ${options.min}`, 'INVALID_SETTINGS_VALUE');
244
+ if (options.exclusiveMin !== undefined && value <= options.exclusiveMin)
245
+ throw makeError(`${fieldPath} must be greater than ${options.exclusiveMin}`, 'INVALID_SETTINGS_VALUE');
246
+ if (options.max !== undefined && value > options.max)
247
+ throw makeError(`${fieldPath} must be at most ${options.max}`, 'INVALID_SETTINGS_VALUE');
248
+ return value;
249
+ }
250
+ function assertStringMap(value, fieldPath, headerMap) {
251
+ if (!isRecord(value))
252
+ throw makeError(`${fieldPath} must be an object`, 'INVALID_SETTINGS_VALUE');
253
+ if (Object.keys(value).length > 100)
254
+ throw makeError(`${fieldPath} has too many entries`, 'INVALID_SETTINGS_VALUE');
255
+ for (const [key, nested] of Object.entries(value)) {
256
+ if (!key || key.length > 256 || PROHIBITED_PATH_SEGMENTS.has(key))
257
+ throw makeError(`${fieldPath} contains an invalid key`, 'INVALID_SETTINGS_VALUE');
258
+ if (headerMap && !HEADER_NAME.test(key))
259
+ throw makeError(`${fieldPath} contains an invalid HTTP header name: ${key}`, 'INVALID_SETTINGS_VALUE');
260
+ if (typeof nested !== 'string')
261
+ throw makeError(`${fieldPath}.${key} must be a string`, 'INVALID_SETTINGS_VALUE');
262
+ if (nested.length > 8192)
263
+ throw makeError(`${fieldPath}.${key} is too long`, 'INVALID_SETTINGS_VALUE');
264
+ if (headerMap && /[\0\r\n]/u.test(nested))
265
+ throw makeError(`${fieldPath}.${key} cannot contain NUL or line breaks`, 'INVALID_SETTINGS_VALUE');
266
+ }
267
+ }
268
+ function assertServiceEndpoint(value, fieldPath) {
269
+ const raw = assertString(value, fieldPath, { max: 2048 }).trim();
270
+ let parsed;
271
+ try {
272
+ parsed = new URL(raw);
273
+ }
274
+ catch {
275
+ throw makeError(`${fieldPath} must be a valid URL`, 'INVALID_SETTINGS_VALUE');
276
+ }
277
+ if (!['http:', 'https:', 'ws:', 'wss:'].includes(parsed.protocol))
278
+ throw makeError(`${fieldPath} must use http, https, ws, or wss`, 'INVALID_SETTINGS_VALUE');
279
+ const host = parsed.hostname.toLowerCase().replace(/^\[|\]$/gu, '');
280
+ if (host !== 'localhost' && !/^127(?:\.\d{1,3}){3}$/u.test(host))
281
+ throw makeError(`${fieldPath} must point to localhost or 127.0.0.0/8`, 'INVALID_SETTINGS_VALUE');
282
+ }
283
+ function assertServiceMetadata(value, fieldPath) {
284
+ assertStringMap(value, fieldPath, false);
285
+ for (const key of Object.keys(value)) {
286
+ if (SENSITIVE_METADATA_KEY.test(key))
287
+ throw makeError(`${fieldPath} cannot contain sensitive key: ${key}`, 'INVALID_SETTINGS_VALUE');
288
+ }
289
+ }
290
+ function assertServices(value) {
291
+ if (!Array.isArray(value))
292
+ throw makeError('services must be an array', 'INVALID_SETTINGS_VALUE');
293
+ if (value.length > 100)
294
+ throw makeError('services has too many entries', 'INVALID_SETTINGS_VALUE');
295
+ const names = new Set();
296
+ value.forEach((service, index) => {
297
+ if (!isRecord(service))
298
+ throw makeError(`services[${index}] must be an object`, 'INVALID_SETTINGS_VALUE');
299
+ const allowedKeys = new Set(['name', 'enabled', 'port', 'proxy']);
300
+ if (Object.keys(service).some(key => !allowedKeys.has(key)))
301
+ throw makeError(`services[${index}] contains an unsupported field`, 'INVALID_SETTINGS_VALUE');
302
+ const name = assertString(service.name, `services[${index}].name`, { max: 64 });
303
+ if (!SERVICE_NAME.test(name))
304
+ throw makeError(`services[${index}].name must contain only lowercase letters, digits, _ or -`, 'INVALID_SETTINGS_VALUE');
305
+ if (names.has(name))
306
+ throw makeError(`services contains duplicate name: ${name}`, 'INVALID_SETTINGS_VALUE');
307
+ names.add(name);
308
+ assertBoolean(service.enabled, `services[${index}].enabled`);
309
+ if (service.port !== undefined)
310
+ assertFiniteNumber(service.port, `services[${index}].port`, { integer: true, min: 1, max: 65535 });
311
+ if (service.proxy === undefined)
312
+ return;
313
+ if (!isRecord(service.proxy))
314
+ throw makeError(`services[${index}].proxy must be an object`, 'INVALID_SETTINGS_VALUE');
315
+ const allowedProxyKeys = new Set(['enabled', 'source', 'endpoint', 'serviceType', 'visibility', 'metadata']);
316
+ if (Object.keys(service.proxy).some(key => !allowedProxyKeys.has(key)))
317
+ throw makeError(`services[${index}].proxy contains an unsupported field`, 'INVALID_SETTINGS_VALUE');
318
+ if (service.proxy.enabled !== undefined)
319
+ assertBoolean(service.proxy.enabled, `services[${index}].proxy.enabled`);
320
+ if (service.proxy.source !== undefined && !SERVICE_SOURCES.has(String(service.proxy.source)))
321
+ throw makeError(`services[${index}].proxy.source must be instance or static`, 'INVALID_SETTINGS_VALUE');
322
+ if (service.proxy.endpoint !== undefined)
323
+ assertServiceEndpoint(service.proxy.endpoint, `services[${index}].proxy.endpoint`);
324
+ if (service.proxy.serviceType !== undefined && !SERVICE_TYPES.has(String(service.proxy.serviceType)))
325
+ throw makeError(`services[${index}].proxy.serviceType must be http, websocket, or ssh`, 'INVALID_SETTINGS_VALUE');
326
+ if (service.proxy.visibility !== undefined && !SERVICE_VISIBILITIES.has(String(service.proxy.visibility)))
327
+ throw makeError(`services[${index}].proxy.visibility is invalid`, 'INVALID_SETTINGS_VALUE');
328
+ if (service.proxy.metadata !== undefined)
329
+ assertServiceMetadata(service.proxy.metadata, `services[${index}].proxy.metadata`);
330
+ if (service.proxy.enabled === true && (!SERVICE_SOURCES.has(String(service.proxy.source)) || !service.proxy.visibility))
331
+ throw makeError(`services[${index}].proxy requires source and visibility when enabled`, 'INVALID_SETTINGS_VALUE');
332
+ if (service.proxy.enabled === true && service.proxy.source === 'static' && !service.proxy.endpoint)
333
+ throw makeError(`services[${index}].proxy.endpoint is required when source is static`, 'INVALID_SETTINGS_VALUE');
334
+ });
335
+ }
336
+ function assertStringArray(value, fieldPath) {
337
+ if (!Array.isArray(value))
338
+ throw makeError(`${fieldPath} must be an array`, 'INVALID_SETTINGS_VALUE');
339
+ if (value.length > 100)
340
+ throw makeError(`${fieldPath} has too many entries`, 'INVALID_SETTINGS_VALUE');
341
+ const strings = value.map(item => assertString(item, `${fieldPath}[]`, { max: 256 }).trim());
342
+ if (new Set(strings).size !== strings.length)
343
+ throw makeError(`${fieldPath} contains duplicate values`, 'INVALID_SETTINGS_VALUE');
344
+ }
345
+ function assertFallbackModels(value, fieldPath) {
346
+ if (!Array.isArray(value))
347
+ throw makeError(`${fieldPath} must be an array`, 'INVALID_SETTINGS_VALUE');
348
+ if (value.length > 3)
349
+ throw makeError(`${fieldPath} supports at most 3 models`, 'INVALID_SETTINGS_VALUE');
350
+ const models = new Set();
351
+ value.forEach((item, index) => {
352
+ if (!isRecord(item))
353
+ throw makeError(`${fieldPath}[${index}] must be an object`, 'INVALID_SETTINGS_VALUE');
354
+ if (Object.keys(item).some(key => !['provider', 'model', 'effort'].includes(key)))
355
+ throw makeError(`${fieldPath}[${index}] contains an unsupported field`, 'INVALID_SETTINGS_VALUE');
356
+ if (item.provider !== undefined)
357
+ validateBaseagentValue('', 'provider', `${fieldPath}[${index}].provider`, item.provider);
358
+ const model = assertString(item.model, `${fieldPath}[${index}].model`, { max: 512 }).trim();
359
+ const identity = `${String(item.provider || '').trim()}\0${model}`;
360
+ if (models.has(identity))
361
+ throw makeError(`${fieldPath} contains duplicate provider/model pairs`, 'INVALID_SETTINGS_VALUE');
362
+ models.add(identity);
363
+ if (item.effort !== undefined) {
364
+ const effort = assertString(item.effort, `${fieldPath}[${index}].effort`, { max: 16 });
365
+ if (!EFFORTS.has(effort))
366
+ throw makeError(`${fieldPath}[${index}].effort is invalid`, 'INVALID_SETTINGS_VALUE');
367
+ }
368
+ });
369
+ }
370
+ function assertHttpUrl(value, fieldPath) {
371
+ const raw = assertString(value, fieldPath, { max: 2048 }).trim();
372
+ let parsed;
373
+ try {
374
+ parsed = new URL(raw);
375
+ }
376
+ catch {
377
+ throw makeError(`${fieldPath} must be a valid URL`, 'INVALID_SETTINGS_VALUE');
378
+ }
379
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
380
+ throw makeError(`${fieldPath} must use http or https`, 'INVALID_SETTINGS_VALUE');
381
+ }
382
+ function validateDaemonValue(fieldPath, value) {
383
+ if (BOOLEAN_PATHS.has(fieldPath))
384
+ return assertBoolean(value, fieldPath);
385
+ if (fieldPath === 'services')
386
+ return assertServices(value);
387
+ if (fieldPath === 'watch.logTypes')
388
+ return assertStringArray(value, fieldPath);
389
+ if (fieldPath === 'aun.defaultAidDomain') {
390
+ const domain = assertString(value, fieldPath, { max: 253 }).trim();
391
+ if (!AID_DOMAIN.test(domain) || /^(?:\d{1,3}\.){3}\d{1,3}$/u.test(domain) || domain.toLowerCase().endsWith('.localhost'))
392
+ throw makeError(`${fieldPath} must be a valid multi-level domain`, 'INVALID_SETTINGS_VALUE');
393
+ return;
394
+ }
395
+ if (fieldPath === 'aun.minEvolVersion') {
396
+ const version = assertString(value, fieldPath, { max: 64 }).trim();
397
+ if (!/^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/u.test(version))
398
+ throw makeError(`${fieldPath} must be a semantic version such as 1.6.0`, 'INVALID_SETTINGS_VALUE');
399
+ return;
400
+ }
401
+ if (fieldPath === 'debug.logLevel') {
402
+ if (!['DEBUG', 'INFO', 'WARN', 'ERROR'].includes(String(value)))
403
+ throw makeError(`${fieldPath} is invalid`, 'INVALID_SETTINGS_VALUE');
404
+ return;
405
+ }
406
+ if (fieldPath === 'idleMonitor.timeout' || fieldPath === 'idleMonitor.apiRetryTimeout')
407
+ return void assertFiniteNumber(value, fieldPath, { exclusiveMin: 0 });
408
+ if (fieldPath === 'idleMonitor.maxExecutionTime')
409
+ return void assertFiniteNumber(value, fieldPath, { min: 0 });
410
+ throw makeError(`Unsupported daemon settings path: ${fieldPath}`, 'INVALID_SETTINGS_PATH');
411
+ }
412
+ function validateBaseagentValue(baseagent, field, fieldPath, value) {
413
+ if (field === 'provider' || field === 'providerServiceType') {
414
+ const provider = assertString(value, fieldPath, { max: 256 });
415
+ if (!/^[A-Za-z0-9_-]+$/u.test(provider) || PROHIBITED_PATH_SEGMENTS.has(provider))
416
+ throw makeError(`${fieldPath} is invalid`, 'INVALID_SETTINGS_VALUE');
417
+ return;
418
+ }
419
+ if (BASEAGENT_BOOLEAN_FIELDS.has(field))
420
+ return assertBoolean(value, fieldPath);
421
+ if (field === 'model' || field === 'auxiliaryModel' || field === 'apiKey' || field === 'pathToClaudeCodeExecutable' || field === 'cliPath' || field === 'project' || field === 'location') {
422
+ const stringValue = assertString(value, fieldPath, { max: field === 'apiKey' ? 8192 : 4096 });
423
+ if (field === 'apiKey' && ['<redacted>', '******'].includes(stringValue))
424
+ throw makeError(`${fieldPath} must contain a replacement secret, not the redacted placeholder`, 'INVALID_SETTINGS_VALUE');
425
+ return;
426
+ }
427
+ if (field === 'effort' || field === 'auxiliaryEffort') {
428
+ const effort = assertString(value, fieldPath, { max: 16 });
429
+ if (!EFFORTS.has(effort))
430
+ throw makeError(`${fieldPath} is invalid`, 'INVALID_SETTINGS_VALUE');
431
+ return;
432
+ }
433
+ if (field === 'baseUrl')
434
+ return assertHttpUrl(value, fieldPath);
435
+ if (field === 'headers')
436
+ return assertStringMap(value, fieldPath, true);
437
+ if (field === 'queryParams')
438
+ return assertStringMap(value, fieldPath, false);
439
+ if (field === 'fallback')
440
+ return assertFallbackModels(value, fieldPath);
441
+ if (field === 'approvalsReviewer') {
442
+ if (!['user', 'auto_review', 'guardian_subagent'].includes(String(value)))
443
+ throw makeError(`${fieldPath} is invalid`, 'INVALID_SETTINGS_VALUE');
444
+ return;
445
+ }
446
+ if (field === 'mode') {
447
+ if (baseagent !== 'gemini' || !['cli', 'sdk'].includes(String(value)))
448
+ throw makeError(`${fieldPath} is invalid`, 'INVALID_SETTINGS_VALUE');
449
+ return;
450
+ }
451
+ if (field === 'timeoutMs')
452
+ return void assertFiniteNumber(value, fieldPath, { integer: true, min: 1, max: 3_600_000 });
453
+ if (field === 'maxRetries')
454
+ return void assertFiniteNumber(value, fieldPath, { integer: true, min: 0, max: 100 });
455
+ if (field === 'maxRetryDelayMs')
456
+ return void assertFiniteNumber(value, fieldPath, { integer: true, min: 0, max: 3_600_000 });
457
+ throw makeError(`Unsupported Base Agent settings path: ${fieldPath}`, 'INVALID_SETTINGS_PATH');
458
+ }
459
+ function validateDefaultsValue(fieldPath, value) {
460
+ if (fieldPath === 'Services') {
461
+ if (!isRecord(value))
462
+ throw makeError('Services must be an object', 'INVALID_SETTINGS_VALUE');
463
+ for (const [serviceType, selection] of Object.entries(value)) {
464
+ if (!/^[A-Za-z0-9_-]+$/u.test(serviceType) || PROHIBITED_PATH_SEGMENTS.has(serviceType) || !isRecord(selection))
465
+ throw makeError('Invalid service selection', 'INVALID_SETTINGS_VALUE');
466
+ if (Object.keys(selection).some(field => !['provider', 'model', 'effort', 'fallback'].includes(field)))
467
+ throw makeError('Invalid service selection field', 'INVALID_SETTINGS_VALUE');
468
+ validateBaseagentValue('', 'provider', `Services.${serviceType}.provider`, selection.provider);
469
+ assertString(selection.model, `Services.${serviceType}.model`);
470
+ if (selection.effort !== undefined)
471
+ validateBaseagentValue('', 'effort', `Services.${serviceType}.effort`, selection.effort);
472
+ if (selection.fallback !== undefined)
473
+ assertFallbackModels(selection.fallback, `Services.${serviceType}.fallback`);
474
+ }
475
+ return;
476
+ }
477
+ if (BOOLEAN_PATHS.has(fieldPath))
478
+ return assertBoolean(value, fieldPath);
479
+ const parsed = baseagentPath(fieldPath);
480
+ if (parsed)
481
+ return validateBaseagentValue(parsed.baseagent, parsed.field, fieldPath, value);
482
+ if (baseagentBlockPath(fieldPath)) {
483
+ if (!isRecord(value) || Object.keys(value).length > 0)
484
+ throw makeError(`${fieldPath} must be an empty object`, 'INVALID_SETTINGS_VALUE');
485
+ return;
486
+ }
487
+ if (fieldPath === 'active_baseagent') {
488
+ if (!BASEAGENT_SET.has(String(value)))
489
+ throw makeError(`${fieldPath} is invalid`, 'INVALID_SETTINGS_VALUE');
490
+ return;
491
+ }
492
+ if (fieldPath === 'projects.defaultPath') {
493
+ const configuredPath = assertString(value, fieldPath, { max: 4096 }).trim();
494
+ if (!path.isAbsolute(configuredPath))
495
+ throw makeError(`${fieldPath} must be an absolute path`, 'INVALID_SETTINGS_VALUE');
496
+ return;
497
+ }
498
+ if (fieldPath === 'responseMode') {
499
+ if (value !== 'single-session')
500
+ throw makeError(`${fieldPath} must be single-session`, 'INVALID_SETTINGS_VALUE');
501
+ return;
502
+ }
503
+ if (fieldPath === 'config.chatMode') {
504
+ if (!['interactive', 'proactive'].includes(String(value)))
505
+ throw makeError(`${fieldPath} is invalid`, 'INVALID_SETTINGS_VALUE');
506
+ return;
507
+ }
508
+ if (fieldPath === 'config.mentionMode') {
509
+ if (!['disabled', 'mention-only', 'auto'].includes(String(value)))
510
+ throw makeError(`${fieldPath} is invalid`, 'INVALID_SETTINGS_VALUE');
511
+ return;
512
+ }
513
+ if (fieldPath === 'config.show_activities') {
514
+ if (!['all', 'text', 'none'].includes(String(value)))
515
+ throw makeError(`${fieldPath} is invalid`, 'INVALID_SETTINGS_VALUE');
516
+ return;
517
+ }
518
+ if (fieldPath === 'session_renew.after_hours')
519
+ return void assertFiniteNumber(value, fieldPath, { exclusiveMin: 0 });
520
+ if (fieldPath === 'session_renew.fallback_action') {
521
+ if (!['continue', 'new'].includes(String(value)))
522
+ throw makeError(`${fieldPath} is invalid`, 'INVALID_SETTINGS_VALUE');
523
+ return;
524
+ }
525
+ throw makeError(`Unsupported defaults settings path: ${fieldPath}`, 'INVALID_SETTINGS_PATH');
526
+ }
527
+ function validateChange(file, change) {
528
+ if (!isRecord(change) || typeof change.path !== 'string')
529
+ throw makeError('Each change must be an object', 'INVALID_SETTINGS_CHANGE');
530
+ if (file === 'daemon' && (change.path === 'owner' || change.path === 'owners')) {
531
+ throw makeError('Daemon Owner is managed by Evol App; use ownership binding instead', 'OWNER_MANAGED_BY_EVOL_APP', 403);
532
+ }
533
+ if (!isAllowedPath(file, change.path))
534
+ throw makeError(`Settings path is not editable: ${change.path}`, 'SETTINGS_PATH_NOT_EDITABLE');
535
+ if (change.op !== 'set' && change.op !== 'unset')
536
+ throw makeError(`Invalid operation for ${change.path}`, 'INVALID_SETTINGS_CHANGE');
537
+ if (change.op === 'unset') {
538
+ if (change.path === 'active_baseagent' || change.path === 'services')
539
+ throw makeError(`${change.path} cannot be unset`, 'INVALID_SETTINGS_CHANGE');
540
+ return;
541
+ }
542
+ if (!Object.prototype.hasOwnProperty.call(change, 'value'))
543
+ throw makeError(`Missing value for ${change.path}`, 'INVALID_SETTINGS_CHANGE');
544
+ if (file === 'daemon')
545
+ validateDaemonValue(change.path, change.value);
546
+ else
547
+ validateDefaultsValue(change.path, change.value);
548
+ }
549
+ function clone(value) {
550
+ return JSON.parse(JSON.stringify(value));
551
+ }
552
+ function applyChanges(config, file, changes) {
553
+ if (!Array.isArray(changes) || changes.length === 0)
554
+ throw makeError('changes must be a non-empty array', 'INVALID_SETTINGS_CHANGES');
555
+ if (changes.length > 200)
556
+ throw makeError('Too many settings changes', 'INVALID_SETTINGS_CHANGES');
557
+ const seen = new Set();
558
+ for (const change of changes) {
559
+ validateChange(file, change);
560
+ if (seen.has(change.path))
561
+ throw makeError(`Duplicate settings path: ${change.path}`, 'DUPLICATE_SETTINGS_PATH');
562
+ seen.add(change.path);
563
+ if (change.path === 'owner' || change.path === 'owners')
564
+ throw makeError('Daemon Owner is managed by Evol App; use ownership binding instead', 'OWNER_MANAGED_BY_EVOL_APP', 403);
565
+ const segments = pathSegments(change.path);
566
+ if (change.op === 'unset') {
567
+ let cursor = config;
568
+ for (const segment of segments.slice(0, -1)) {
569
+ if (!isRecord(cursor?.[segment])) {
570
+ cursor = undefined;
571
+ break;
572
+ }
573
+ cursor = cursor[segment];
574
+ }
575
+ if (cursor)
576
+ delete cursor[segments[segments.length - 1]];
577
+ continue;
578
+ }
579
+ let cursor = config;
580
+ for (const segment of segments.slice(0, -1)) {
581
+ if (!isRecord(cursor[segment]))
582
+ cursor[segment] = {};
583
+ cursor = cursor[segment];
584
+ }
585
+ cursor[segments[segments.length - 1]] = clone(change.value);
586
+ }
587
+ }
588
+ async function validateBaseagentAvailability(file, changes, currentConfig) {
589
+ if (file !== 'defaults')
590
+ return;
591
+ const { getBaseagentSnapshot } = await import('../agents/baseagent-availability.js');
592
+ const snapshot = await getBaseagentSnapshot({ probe: true });
593
+ const probes = Array.isArray(snapshot.baseagents) ? snapshot.baseagents : [];
594
+ const detected = new Set(probes.filter(item => item.detected === true).map(item => item.id));
595
+ const configured = new Set(isRecord(currentConfig.baseagents) ? Object.keys(currentConfig.baseagents) : []);
596
+ for (const change of changes) {
597
+ if (change.path === 'active_baseagent' && change.op === 'set' && !detected.has(String(change.value))) {
598
+ throw makeError(`active_baseagent is not available on this machine: ${String(change.value)}`, 'BASEAGENT_UNAVAILABLE');
599
+ }
600
+ const parsed = baseagentPath(change.path);
601
+ if (parsed && !detected.has(parsed.baseagent) && !configured.has(parsed.baseagent)) {
602
+ throw makeError(`Base Agent is not available on this machine: ${parsed.baseagent}`, 'BASEAGENT_UNAVAILABLE');
603
+ }
604
+ const block = baseagentBlockPath(change.path);
605
+ if (block && change.op === 'set' && !detected.has(block) && !configured.has(block)) {
606
+ throw makeError(`Base Agent is not available on this machine: ${block}`, 'BASEAGENT_UNAVAILABLE');
607
+ }
608
+ }
609
+ }
610
+ function restartRequiredForChanges(file, changes) {
611
+ const prefixes = file === 'daemon' ? RESTART_DAEMON_PREFIXES : RESTART_DEFAULTS_PREFIXES;
612
+ return changes.map(change => change.path)
613
+ .filter(fieldPath => prefixes.some(prefix => fieldPath === prefix || fieldPath.startsWith(prefix)));
614
+ }
615
+ export async function mutateBasicSettings(input) {
616
+ try {
617
+ if (!isRecord(input) || (input.file !== 'daemon' && input.file !== 'defaults')
618
+ || typeof input.revision !== 'string' || !/^[a-f0-9]{64}$/u.test(input.revision)
619
+ || !Array.isArray(input.changes)) {
620
+ throw makeError('Invalid Basic Settings mutation request', 'INVALID_REQUEST');
621
+ }
622
+ const file = input.file;
623
+ const changes = input.changes;
624
+ if (changes.length === 0)
625
+ throw makeError('changes must be a non-empty array', 'INVALID_SETTINGS_CHANGES');
626
+ if (changes.length > 200)
627
+ throw makeError('Too many settings changes', 'INVALID_SETTINGS_CHANGES');
628
+ for (const change of changes)
629
+ validateChange(file, change);
630
+ const currentConfig = readSettingsFile(file) ?? { $schema_version: file === 'daemon' ? 8 : 5 };
631
+ await validateBaseagentAvailability(file, changes, currentConfig);
632
+ const result = mutateConfig(targetFor(file), undefined, config => {
633
+ const latest = readSettingsFile(file) ?? { $schema_version: file === 'daemon' ? 8 : 5 };
634
+ if (settingsRevision(latest) !== input.revision) {
635
+ throw makeError('Configuration changed after it was loaded. Reload and try again.', 'BASIC_SETTINGS_REVISION_CONFLICT', 409);
636
+ }
637
+ applyChanges(config, file, changes);
638
+ const errors = validateConfig(targetFor(file), config);
639
+ if (errors.length > 0)
640
+ throw makeError(errors.join('; '), 'SCHEMA_INVALID');
641
+ });
642
+ const restartPaths = restartRequiredForChanges(file, changes);
643
+ return {
644
+ ok: true,
645
+ changed: result.changed,
646
+ restartRequired: result.changed && restartPaths.length > 0,
647
+ restartPaths: result.changed ? restartPaths : [],
648
+ };
649
+ }
650
+ catch (caught) {
651
+ return {
652
+ ok: false,
653
+ code: caught?.code || 'BASIC_SETTINGS_UPDATE_FAILED',
654
+ error: caught instanceof Error ? caught.message : String(caught),
655
+ statusCode: caught?.statusCode || 400,
656
+ };
657
+ }
658
+ }
659
+ function requestBody(value) {
660
+ if (!isRecord(value))
661
+ throw makeError('Basic Settings request must be a JSON object', 'INVALID_REQUEST');
662
+ return value;
663
+ }
664
+ export async function handleBasicSettingsService(input) {
665
+ try {
666
+ if (!isRecord(input) || typeof input.method !== 'string' || typeof input.pathname !== 'string'
667
+ || !input.pathname.startsWith('/api/basic-settings')) {
668
+ throw makeError('Invalid Basic Settings service request', 'INVALID_REQUEST');
669
+ }
670
+ const query = input.query ?? {};
671
+ if (!isRecord(query) || Object.values(query).some(value => typeof value !== 'string'))
672
+ throw makeError('Invalid Basic Settings query', 'INVALID_REQUEST');
673
+ const pathname = input.pathname.replace(/\/+$/u, '');
674
+ if (input.method === 'GET' && pathname === '/api/basic-settings') {
675
+ return response(200, { success: true, data: readBasicSettings() });
676
+ }
677
+ if (input.method === 'GET' && pathname === '/api/basic-settings/secret') {
678
+ const file = typeof query.file === 'string' ? query.file : 'defaults';
679
+ const fieldPath = typeof query.path === 'string' ? query.path : '';
680
+ if (file !== 'defaults' || !/^baseagents\.(claude|codex|gemini|ecagent)\.apiKey$/u.test(fieldPath)) {
681
+ throw makeError('Only Base Agent apiKey fields can be revealed', 'INVALID_SECRET_PATH');
682
+ }
683
+ const config = readSettingsFile('defaults');
684
+ const value = fieldPath.split('.').reduce((current, segment) => isRecord(current) ? current[segment] : undefined, config);
685
+ if (typeof value !== 'string' || !value.trim())
686
+ return response(404, { success: false, code: 'SECRET_NOT_CONFIGURED', error: 'API Key is not configured' });
687
+ return response(200, { success: true, data: { file, path: fieldPath, value } });
688
+ }
689
+ if (input.method === 'PATCH' && pathname === '/api/basic-settings') {
690
+ const result = await mutateBasicSettings(requestBody(input.body));
691
+ if (!result.ok)
692
+ return response(result.statusCode || 400, { success: false, code: result.code, error: result.error });
693
+ return response(200, {
694
+ success: true,
695
+ changed: result.changed,
696
+ restartRequired: result.restartRequired,
697
+ restartPaths: result.restartPaths || [],
698
+ });
699
+ }
700
+ return response(405, { success: false, error: 'Method not allowed' });
701
+ }
702
+ catch (caught) {
703
+ return response(caught?.statusCode || 400, {
704
+ success: false,
705
+ ...(caught?.code ? { code: caught.code } : {}),
706
+ error: caught instanceof Error ? caught.message : String(caught),
707
+ });
708
+ }
709
+ }