evolcore 0.0.19 → 0.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/README.md +60 -9
  3. package/bin/install-codex-managed-hooks.mjs +61 -0
  4. package/dist/agents/baseagent.js +10 -6
  5. package/dist/agents/claude-runner.js +383 -111
  6. package/dist/agents/codex-app-server-client.js +10 -2
  7. package/dist/agents/codex-runner.js +405 -137
  8. package/dist/agents/ecagent-runner.js +174 -63
  9. package/dist/agents/gemini-runner.js +130 -30
  10. package/dist/agents/request-identity.js +25 -0
  11. package/dist/agents/runner-types.js +19 -0
  12. package/dist/aun/aid/agentmd.js +66 -2
  13. package/dist/aun/aid/identity.js +4 -1
  14. package/dist/aun/aid/index.js +1 -1
  15. package/dist/aun/msg/group.js +86 -9
  16. package/dist/aun/msg/history.js +213 -36
  17. package/dist/aun/msg/managed-operation.js +58 -9
  18. package/dist/aun/msg/p2p.js +26 -11
  19. package/dist/aun/outbox.js +295 -68
  20. package/dist/aun/service-proxy.js +43 -25
  21. package/dist/channels/aun.js +1004 -273
  22. package/dist/channels/daemon.js +6 -1
  23. package/dist/cli/agent-command.js +4 -3
  24. package/dist/cli/agent.js +66 -56
  25. package/dist/cli/aun-commands.js +177 -42
  26. package/dist/cli/command-log.js +10 -11
  27. package/dist/cli/contact.js +1 -0
  28. package/dist/cli/daemon-commands.js +81 -121
  29. package/dist/cli/index.js +1 -0
  30. package/dist/cli/init.js +76 -24
  31. package/dist/cli/restart-monitor.js +3 -3
  32. package/dist/cli/task-context.js +46 -0
  33. package/dist/cli/trigger-command.js +1 -1
  34. package/dist/cli/watch-logs.js +10 -3
  35. package/dist/config/builtin-roles.js +1 -0
  36. package/dist/config/config-field-policy.js +16 -5
  37. package/dist/config/config-manager.js +226 -24
  38. package/dist/config/config-operation-service.js +1 -2
  39. package/dist/config/contact-operation-service.js +32 -1
  40. package/dist/config/contact-request-service.js +44 -0
  41. package/dist/config/daemon-services.js +186 -0
  42. package/dist/config/gateway-config.js +29 -16
  43. package/dist/config/lifecycle.js +16 -5
  44. package/dist/config/role-service.js +54 -3
  45. package/dist/config/schema-migration.js +550 -0
  46. package/dist/config-store.js +161 -12
  47. package/dist/core/agent-application-service.js +279 -0
  48. package/dist/core/audit/log-integrity.js +102 -0
  49. package/dist/core/auth/agent-delegation.js +31 -1
  50. package/dist/core/auth/auth-gateway.js +33 -4
  51. package/dist/core/auth/authorization-audit.js +155 -4
  52. package/dist/core/auth/operation-authorizer.js +41 -1
  53. package/dist/core/auth/operation-catalog.js +9 -1
  54. package/dist/core/bootstrap-messages.js +2 -2
  55. package/dist/core/bootstrap-service.js +27 -38
  56. package/dist/core/causation/aun-association.js +7 -4
  57. package/dist/core/channel-loader.js +0 -2
  58. package/dist/core/command/agent-control.js +56 -16
  59. package/dist/core/command/command-handler.js +290 -44
  60. package/dist/core/command/connect-menu.js +3 -4
  61. package/dist/core/command/group-menu.js +5 -7
  62. package/dist/core/command/menu-handler.js +279 -80
  63. package/dist/core/command/role-menu.js +21 -11
  64. package/dist/core/command/slash-gate.js +85 -18
  65. package/dist/core/command/slash-handler.js +350 -32
  66. package/dist/core/event-catalog.js +5 -0
  67. package/dist/core/evolagent.js +9 -4
  68. package/dist/core/handoff/dispatcher.js +4 -0
  69. package/dist/core/handoff/runtime.js +10 -0
  70. package/dist/core/handoff/store.js +32 -9
  71. package/dist/core/inference/text-inference.js +7 -15
  72. package/dist/core/message/im-renderer.js +83 -84
  73. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  74. package/dist/core/message/message-bridge.js +124 -10
  75. package/dist/core/message/message-log.js +14 -7
  76. package/dist/core/message/message-queue.js +206 -16
  77. package/dist/core/message/message-utils.js +12 -5
  78. package/dist/core/message/response-engine.js +495 -72
  79. package/dist/core/message/send-receipt.js +1 -0
  80. package/dist/core/message/stream-debouncer.js +9 -2
  81. package/dist/core/model/model-catalog.js +23 -15
  82. package/dist/core/model/model-diagnostics.js +28 -10
  83. package/dist/core/permission/approval-gateway.js +180 -6
  84. package/dist/core/permission/ec-command-parser.js +465 -56
  85. package/dist/core/permission/mode.js +18 -3
  86. package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
  87. package/dist/core/permission/readonly-shell-query.js +263 -9
  88. package/dist/core/permission/sandbox-runtime.js +205 -13
  89. package/dist/core/permission/tool-policy.js +673 -62
  90. package/dist/core/relation/peer-identity.js +18 -0
  91. package/dist/core/session/session-fs-store.js +154 -5
  92. package/dist/core/session/session-manager.js +299 -30
  93. package/dist/core/session/session-renew.js +19 -12
  94. package/dist/core/session/session-turn-coordinator.js +11 -4
  95. package/dist/eck/kit-renderer.js +18 -9
  96. package/dist/index.js +283 -65
  97. package/dist/ipc.js +374 -24
  98. package/dist/paths.js +64 -7
  99. package/dist/response-system/context-builder.js +1 -7
  100. package/dist/trigger/anomaly-store.js +1 -0
  101. package/dist/trigger/feedback.js +56 -5
  102. package/dist/trigger/history.js +79 -4
  103. package/dist/trigger/legacy-session-history.js +2 -2
  104. package/dist/trigger/parser.js +3 -2
  105. package/dist/trigger/validation.js +6 -1
  106. package/dist/utils/atomic-write.js +27 -0
  107. package/dist/utils/ecweb-utils.js +16 -2
  108. package/dist/utils/error-utils.js +4 -1
  109. package/dist/utils/logger.js +21 -2
  110. package/dist/utils/process-tree-stats.js +24 -4
  111. package/dist/utils/process-tree-worker.js +31 -0
  112. package/dist/utils/project-path.js +1 -2
  113. package/dist/utils/stats.js +52 -18
  114. package/dist/utils/welcome.js +2 -2
  115. package/kits/docs/INDEX.md +1 -1
  116. package/kits/docs/evolcore/INDEX.md +1 -1
  117. package/kits/docs/evolcore/contact.md +7 -1
  118. package/kits/docs/evolcore/msg.md +16 -0
  119. package/kits/rules/01-overview.md +1 -1
  120. package/kits/rules/03-identity.md +1 -1
  121. package/kits/rules/05-venue.md +1 -1
  122. package/kits/schemas/_meta.json +10 -5
  123. package/kits/schemas/agent-config.schema.10.json +2 -1
  124. package/kits/schemas/agent-config.schema.11.json +421 -0
  125. package/kits/schemas/daemon.schema.5.json +135 -0
  126. package/kits/schemas/daemon.schema.6.json +131 -0
  127. package/kits/schemas/defaults.schema.5.json +119 -0
  128. package/kits/schemas/migrations/README.md +3 -1
  129. package/kits/schemas/relation-config.schema.8.json +13 -0
  130. package/kits/schemas/role-config.schema.1.json +1 -2
  131. package/kits/schemas/single-session.schema.3.json +32 -0
  132. package/kits/templates/roles/admin.json +1 -0
  133. package/kits/templates/roles/member.json +1 -0
  134. package/kits/templates/roles/visitor.json +1 -0
  135. package/package.json +7 -3
  136. package/skills/eclink/SKILL.md +2 -0
@@ -32,6 +32,11 @@ function normalizeTaskRuntimeContext(value) {
32
32
  peerName: optionalString(value.peerName),
33
33
  peerType: optionalString(value.peerType),
34
34
  peerRole: optionalString(value.peerRole),
35
+ processRole: optionalProcessRole(value.processRole),
36
+ dataScope: optionalDataScope(value.dataScope),
37
+ authorizedBy: optionalString(value.authorizedBy),
38
+ executionSource: value.executionSource === 'fullaccess-command' || value.executionSource === 'trigger'
39
+ ? value.executionSource : undefined,
35
40
  threadId: optionalString(value.threadId),
36
41
  sessionRuntimeDir: optionalAbsolutePath(value.sessionRuntimeDir),
37
42
  runtimeLockDir: optionalAbsolutePath(value.runtimeLockDir),
@@ -39,6 +44,13 @@ function normalizeTaskRuntimeContext(value) {
39
44
  causation: normalizeCausation(value.causation),
40
45
  };
41
46
  }
47
+ function optionalProcessRole(value) {
48
+ return value === 'daemon-owner' || value === 'daemon-service' || value === 'fullaccess-run' || value === 'none'
49
+ ? value : undefined;
50
+ }
51
+ function optionalDataScope(value) {
52
+ return value === 'relation' || value === 'agent' || value === 'daemon' ? value : undefined;
53
+ }
42
54
  /**
43
55
  * Codex's shell carrier can leave one JSON-style escape layer in a message
44
56
  * argument (for example, the two characters `\\` and `n`). Decode only that
@@ -198,6 +210,40 @@ export function isManagedSessionRuntimeDir(directory, managedRoot) {
198
210
  return false;
199
211
  return isPrivateDirectory(root) && isPrivateDirectory(resolved);
200
212
  }
213
+ /**
214
+ * Resolve the session-owned temporary root for a managed child process.
215
+ *
216
+ * A direct runner call without task context is intentionally left alone for
217
+ * backwards-compatible tests and low-level integrations. Once the daemon has
218
+ * injected the task-context marker, however, falling back to the daemon's
219
+ * process-wide TMPDIR would break session isolation. Require the injected
220
+ * value to be present, identical to TMPDIR, and still owned by a trusted
221
+ * managed namespace before any runner-side helper writes a file.
222
+ */
223
+ export function getManagedTaskTempDir(runtimeEnv) {
224
+ if (typeof runtimeEnv?.[TASK_RUNTIME_CONTEXT_ENV] !== 'string')
225
+ return undefined;
226
+ const configured = runtimeEnv[SESSION_RUNTIME_DIR_ENV]?.trim();
227
+ const inherited = runtimeEnv.TMPDIR?.trim();
228
+ const context = parseTaskRuntimeContext(runtimeEnv[TASK_RUNTIME_CONTEXT_ENV]);
229
+ const contextDir = context?.sessionRuntimeDir;
230
+ const processRoot = process.env.TMPDIR?.trim();
231
+ const trusted = configured
232
+ && inherited
233
+ && path.isAbsolute(configured)
234
+ && path.isAbsolute(inherited)
235
+ && path.resolve(configured) === path.resolve(inherited)
236
+ // The process root is shared by all sessions. It may contain a session
237
+ // directory, but must never itself become the task's TMPDIR capability.
238
+ && (!processRoot || !path.isAbsolute(processRoot) || path.resolve(configured) !== path.resolve(processRoot))
239
+ && contextDir
240
+ && path.resolve(contextDir) === path.resolve(configured)
241
+ && (isManagedSessionRuntimeDir(configured) || isRunnerOwnedSessionRuntimeDir(configured));
242
+ if (!trusted) {
243
+ throw new Error('managed session TMPDIR is unavailable or untrusted');
244
+ }
245
+ return path.resolve(configured);
246
+ }
201
247
  /**
202
248
  * Create a private runtime directory below the process-provided TMPDIR.
203
249
  * There is deliberately no os.tmpdir() fallback: managed sessions must not
@@ -110,7 +110,7 @@ Create 参数模式支持:
110
110
  --baseagent <name> (仅 --exec trigger-session)
111
111
  --model <模型> --effort <low|medium|high|xhigh|max>
112
112
  --max-runs <次数> --max-duration <时长: 30s|15m|2h|1d>
113
- --permission <readonly|auto|request|bypass>(省略则继承当前身份配置)
113
+ --permission <readonly|auto|request|bypass|fullaccess>(省略则继承当前身份配置;fullaccess 仅 daemon-owner 且 feature flag 开启时可用)
114
114
  --tz <时区> (仅 cron)
115
115
  --target-channel <channelKey> --target-channel-id <ID> (AUN 可简写为 aun)
116
116
  --target-chat-type <private|group> (AUN 目标必填;channelId 同时作为群聊 ID)
@@ -1,15 +1,22 @@
1
1
  import path from 'path';
2
+ const WATCH_LOG_FILE = /\.(?:log|jsonl|out)(?:\..+)?$/;
3
+ /** Whether a file is a supported input for the interactive log watcher. */
4
+ export function isWatchLogFile(file) {
5
+ return WATCH_LOG_FILE.test(path.basename(file));
6
+ }
2
7
  /** 去掉轮转后缀(按小时、按日及旧版带连字符日期)。入参可为文件名或绝对路径。 */
3
8
  export function shortLogName(file) {
4
- return path.basename(file, '.log')
9
+ const baseName = path.basename(file);
10
+ const stem = baseName.replace(WATCH_LOG_FILE, '');
11
+ return stem
5
12
  .replace(/-\d{8}(?:-\d{2})?$/, '') // -YYYYMMDD[-HH](按日/小时轮转)
6
13
  .replace(/-\d{4}-\d{2}-\d{2}$/, ''); // -YYYY-MM-DD(按日轮转,如 ts-sdk)
7
14
  }
8
- /** .log 文件名列表推导去重、字母序的类型列表。 */
15
+ /** 从支持的日志文件名列表推导去重、字母序的类型列表。 */
9
16
  export function deriveLogTypes(files) {
10
17
  const set = new Set();
11
18
  for (const f of files) {
12
- if (!f.endsWith('.log'))
19
+ if (!isWatchLogFile(f))
13
20
  continue;
14
21
  set.add(shortLogName(f));
15
22
  }
@@ -32,6 +32,7 @@ export function getBuiltinRolesConfig() {
32
32
  export function getManagementCommandPermissions(role) {
33
33
  return {
34
34
  'contact.read': { allow: true, scopes: ['agent'] },
35
+ 'contact.add': { allow: true, scopes: ['relation'], constraints: { ownPeerOnly: true, privateOnly: true } },
35
36
  'contact.block': { allow: true, scopes: ['agent'], constraints: { requireAgentAdmin: true } },
36
37
  'connect.write': { allow: true, scopes: ['agent'], constraints: { requireAgentAdmin: true } },
37
38
  'connect.access.write': { allow: true, scopes: ['agent'], constraints: { requireAgentOwner: true } },
@@ -17,6 +17,8 @@ const BEHAVIOR_TOP_FIELDS = new Set([
17
17
  const BASEAGENT_BEHAVIOR_FIELDS = new Set([
18
18
  'model',
19
19
  'effort',
20
+ 'auxiliaryModel',
21
+ 'auxiliaryEffort',
20
22
  'reasoning',
21
23
  'agentProgressSummaries',
22
24
  'excludeDynamicSections',
@@ -25,7 +27,7 @@ const BASEAGENT_BEHAVIOR_FIELDS = new Set([
25
27
  'mode',
26
28
  'useVertex',
27
29
  ]);
28
- const SUPPORTED_BASEAGENTS = new Set(['claude', 'codex', 'gemini', 'ecagent', 'hermes']);
30
+ const SUPPORTED_BASEAGENTS = new Set(['claude', 'codex', 'gemini', 'ecagent']);
29
31
  const SENSITIVE_TOP_FIELDS = new Set([
30
32
  '$schema_version',
31
33
  'aid',
@@ -75,10 +77,17 @@ export function resolveConfigFieldRule(fieldPath) {
75
77
  const [, baseagent, leaf] = parts;
76
78
  if (!SUPPORTED_BASEAGENTS.has(baseagent))
77
79
  return { class: 'unknown' };
78
- const permissionKey = `baseagents.${baseagent}.${leaf === 'reasoning' ? 'effort' : leaf}`;
79
- if (leaf === 'model' && parts.length === 3)
80
+ const normalizedLeaf = leaf === 'reasoning'
81
+ ? 'effort'
82
+ : leaf === 'auxiliaryModel'
83
+ ? 'model'
84
+ : leaf === 'auxiliaryEffort'
85
+ ? 'effort'
86
+ : leaf;
87
+ const permissionKey = `baseagents.${baseagent}.${normalizedLeaf}`;
88
+ if ((leaf === 'model' || leaf === 'auxiliaryModel') && parts.length === 3)
80
89
  return scalar(permissionKey, 'string');
81
- if ((leaf === 'effort' || leaf === 'reasoning') && parts.length === 3)
90
+ if ((leaf === 'effort' || leaf === 'reasoning' || leaf === 'auxiliaryEffort') && parts.length === 3)
82
91
  return scalar(permissionKey, 'effort');
83
92
  if (baseagent === 'claude' && (leaf === 'agentProgressSummaries' || leaf === 'excludeDynamicSections') && parts.length === 3) {
84
93
  return scalar(permissionKey, 'boolean');
@@ -199,7 +208,9 @@ export function relationFieldWriteOperation(fieldPath) {
199
208
  return 'mentionmode.update';
200
209
  if (/^baseagents\.[^.]+\.model$/.test(field))
201
210
  return 'model.use';
202
- if (/^baseagents\.[^.]+\.(?:effort|reasoning)$/.test(field)) {
211
+ if (/^baseagents\.[^.]+\.auxiliaryModel$/.test(field))
212
+ return 'model.use';
213
+ if (/^baseagents\.[^.]+\.(?:effort|reasoning|auxiliaryEffort)$/.test(field)) {
203
214
  return 'model.effort';
204
215
  }
205
216
  return undefined;
@@ -29,6 +29,75 @@ import { clearRoleStoreCache, isRoleName } from './role-store.js';
29
29
  import { writeRoleDefinition, writeRoleRegistry } from './role-service.js';
30
30
  import { mutateContactBookTransactionSync } from './contact-book-store.js';
31
31
  import { DAEMON_SCHEMA_NAME } from '../product.js';
32
+ import { normalizeDaemonServices } from './daemon-services.js';
33
+ // AUN gateway discovery belongs to fastaun. Keep the retired gatewayUrl
34
+ // compatibility handling beside the rest of the configuration read/write
35
+ // policy so all config callers share one implementation.
36
+ const warnedLegacyGatewayFiles = new Set();
37
+ export function legacyAunGatewayConfigPaths(value) {
38
+ if (!value || typeof value !== 'object' || Array.isArray(value))
39
+ return [];
40
+ const record = value;
41
+ const paths = [];
42
+ if (record.aun && typeof record.aun === 'object' && !Array.isArray(record.aun)
43
+ && Object.prototype.hasOwnProperty.call(record.aun, 'gatewayUrl')) {
44
+ paths.push('/aun/gatewayUrl');
45
+ }
46
+ if (Array.isArray(record.channels)) {
47
+ record.channels.forEach((channel, index) => {
48
+ if (channel && typeof channel === 'object'
49
+ && channel.type === 'aun'
50
+ && Object.prototype.hasOwnProperty.call(channel, 'gatewayUrl')) {
51
+ paths.push(`/channels/${index}/gatewayUrl`);
52
+ }
53
+ });
54
+ }
55
+ return paths;
56
+ }
57
+ export function legacyAunGatewayConfigError(file, paths) {
58
+ return `${file}: AUN gatewayUrl at ${paths.join(', ')} ${paths.length === 1 ? 'is' : 'are'} no longer supported; `
59
+ + 'fastaun owns gateway discovery';
60
+ }
61
+ /** Reject newly submitted gateway overrides. Legacy persisted values are handled on read. */
62
+ export function assertNoLegacyAunGatewayConfig(value, file) {
63
+ const paths = legacyAunGatewayConfigPaths(value);
64
+ if (paths.length > 0)
65
+ throw new Error(legacyAunGatewayConfigError(file, paths));
66
+ }
67
+ /** Remove retired AUN gateway overrides without mutating the input. */
68
+ export function stripLegacyAunGatewayConfig(value, file, warn) {
69
+ if (!value || typeof value !== 'object' || Array.isArray(value))
70
+ return value;
71
+ const record = value;
72
+ const next = { ...record };
73
+ let changed = false;
74
+ if (record.aun && typeof record.aun === 'object' && !Array.isArray(record.aun)
75
+ && Object.prototype.hasOwnProperty.call(record.aun, 'gatewayUrl')) {
76
+ const { gatewayUrl: _legacyGatewayUrl, ...aun } = record.aun;
77
+ next.aun = aun;
78
+ changed = true;
79
+ }
80
+ if (Array.isArray(record.channels)) {
81
+ const channels = record.channels.map(channel => {
82
+ if (!channel || typeof channel !== 'object'
83
+ || channel.type !== 'aun'
84
+ || !Object.prototype.hasOwnProperty.call(channel, 'gatewayUrl'))
85
+ return channel;
86
+ const { gatewayUrl: _legacyGatewayUrl, ...rest } = channel;
87
+ changed = true;
88
+ return rest;
89
+ });
90
+ if (changed)
91
+ next.channels = channels;
92
+ }
93
+ if (!changed)
94
+ return value;
95
+ if (!warnedLegacyGatewayFiles.has(file)) {
96
+ warnedLegacyGatewayFiles.add(file);
97
+ warn(`[config] ${file}: AUN gatewayUrl is no longer supported and will be ignored; fastaun owns gateway discovery`);
98
+ }
99
+ return next;
100
+ }
32
101
  export function shouldFailFastForMissingOwners(env = process.env) {
33
102
  const value = (env.EVOLCORE_REQUIRE_OWNERS || '').trim().toLowerCase();
34
103
  return value === '1' || value === 'true' || value === 'yes';
@@ -114,6 +183,7 @@ export function assertRoleConfigV4Ready() {
114
183
  issues.push({ file, detail: `invalid JSON: ${error instanceof Error ? error.message : String(error)}` });
115
184
  return;
116
185
  }
186
+ value = stripLegacyAunGatewayConfig(value, file, message => configWarn(message));
117
187
  const actualVersion = value && typeof value === 'object'
118
188
  ? value.$schema_version
119
189
  : undefined;
@@ -207,7 +277,10 @@ export function assertRoleConfigV5Ready() {
207
277
  if (!fs.existsSync(agentFile))
208
278
  continue;
209
279
  const agent = readJson(agentFile);
210
- const agentVersion = agent?.$schema_version;
280
+ const sanitizedAgent = agent
281
+ ? stripLegacyAunGatewayConfig(agent, agentFile, message => configWarn(message))
282
+ : agent;
283
+ const agentVersion = sanitizedAgent?.$schema_version;
211
284
  let agentSchema = null;
212
285
  if (typeof agentVersion === 'number' && agentVersion >= 5) {
213
286
  try {
@@ -215,8 +288,8 @@ export function assertRoleConfigV5Ready() {
215
288
  }
216
289
  catch { }
217
290
  }
218
- if (!agent || !agentSchema || !agentSchema.validate(agent)) {
219
- if (agent)
291
+ if (!sanitizedAgent || !agentSchema || !agentSchema.validate(sanitizedAgent)) {
292
+ if (sanitizedAgent)
220
293
  issues.push({
221
294
  file: agentFile,
222
295
  detail: agentSchema
@@ -266,6 +339,7 @@ export function assertContactBookV2Ready() {
266
339
  issues.push({ file, detail: `invalid JSON: ${error instanceof Error ? error.message : String(error)}` });
267
340
  return;
268
341
  }
342
+ value = stripLegacyAunGatewayConfig(value, file, message => configWarn(message));
269
343
  const candidates = Array.isArray(schemas) ? schemas : [schemas];
270
344
  if (!candidates.some(schema => schema.validate(value))) {
271
345
  const detail = (candidates.at(-1)?.validate.errors ?? [])
@@ -519,13 +593,16 @@ export function read(target, sel, opts = {}) {
519
593
  }
520
594
  if (raw === null)
521
595
  return null;
596
+ raw = stripLegacyAunGatewayConfig(raw, file, message => configWarn(message));
522
597
  // schema 版本迁移(read 时若 $schema_version < current)
523
598
  const migrated = migrateIfNeeded(target, raw, file);
524
- const normalized = target === ConfigTarget.Agent
525
- ? normalizeAgentConfigForRead(migrated, sel?.self)
526
- : target === ConfigTarget.Relation
527
- ? foldLegacyProactiveBlock(migrated)
528
- : migrated;
599
+ const normalized = target === ConfigTarget.Process
600
+ ? normalizeProcessConfigCompat(normalizeDaemonServices(migrated))
601
+ : target === ConfigTarget.Agent
602
+ ? normalizeAgentConfigForRead(migrated, sel?.self)
603
+ : target === ConfigTarget.Relation
604
+ ? foldLegacyProactiveBlock(migrated)
605
+ : migrated;
529
606
  // Agent read compatibility may move the legacy top-level `agents` block to
530
607
  // `baseagents`, so canonicalize effort after that structural migration.
531
608
  const canonicalized = normalizeBaseagentEffortCompat(normalized);
@@ -606,12 +683,11 @@ function normalizeAgentConfigForWrite(value) {
606
683
  if (mutable.aid && !isValidAid(mutable.aid)) {
607
684
  throw new ConfigError('VALIDATION_ERROR', `Invalid aid "${mutable.aid}" (must be a valid multi-level domain like mybot.agentid.pub)`);
608
685
  }
609
- // 规范化 projects 字段:只保留 rootPath 和 defaultPath
686
+ // 规范化 projects 字段:只保留唯一公开项目路径 defaultPath
687
+ // defaults.projects.rootPath 已废弃,不能继续成为有效运行时参数。
610
688
  if (mutable.projects && typeof mutable.projects === 'object') {
611
689
  const projects = mutable.projects;
612
690
  const normalized = {};
613
- if (typeof projects.rootPath === 'string')
614
- normalized.rootPath = projects.rootPath;
615
691
  if (typeof projects.defaultPath === 'string')
616
692
  normalized.defaultPath = projects.defaultPath;
617
693
  if (Object.keys(normalized).length > 0) {
@@ -624,19 +700,58 @@ function normalizeAgentConfigForWrite(value) {
624
700
  return mutable;
625
701
  }
626
702
  function normalizeProcessConfigForWrite(value) {
627
- const configuredAidDomain = value.aun?.defaultAidDomain;
703
+ let normalized;
704
+ try {
705
+ normalized = normalizeProcessConfigCompat(normalizeDaemonServices(value));
706
+ }
707
+ catch (error) {
708
+ throw new ConfigError('DAEMON_SERVICES_MIGRATION_REQUIRED', error instanceof Error ? error.message : String(error));
709
+ }
710
+ const configuredAidDomain = normalized.aun?.defaultAidDomain;
628
711
  if (configuredAidDomain === undefined)
629
- return value;
712
+ return normalized;
630
713
  try {
631
714
  return {
632
- ...value,
633
- aun: { ...value.aun, defaultAidDomain: normalizeAidDomain(configuredAidDomain, 'aun.defaultAidDomain') },
715
+ ...normalized,
716
+ aun: { ...normalized.aun, defaultAidDomain: normalizeAidDomain(configuredAidDomain, 'aun.defaultAidDomain') },
634
717
  };
635
718
  }
636
719
  catch (error) {
637
720
  throw new ConfigError('VALIDATION_ERROR', error instanceof Error ? error.message : String(error));
638
721
  }
639
722
  }
723
+ /**
724
+ * Normalize renamed process-level settings while keeping old daemon.json files
725
+ * readable after an upgrade. New values take precedence when both keys exist.
726
+ */
727
+ export function normalizeProcessConfigCompat(value) {
728
+ if (!value || typeof value !== 'object' || Array.isArray(value))
729
+ return value;
730
+ const idleMonitor = value.idleMonitor;
731
+ const debug = value.debug;
732
+ let changed = false;
733
+ let normalized = value;
734
+ if (idleMonitor && typeof idleMonitor === 'object' && !Array.isArray(idleMonitor)) {
735
+ const nextIdleMonitor = { ...idleMonitor };
736
+ if (nextIdleMonitor.apiRetryTimeout === undefined && nextIdleMonitor.retryAttemptTimeout !== undefined) {
737
+ nextIdleMonitor.apiRetryTimeout = nextIdleMonitor.retryAttemptTimeout;
738
+ changed = true;
739
+ }
740
+ if (Object.prototype.hasOwnProperty.call(nextIdleMonitor, 'retryAttemptTimeout')) {
741
+ delete nextIdleMonitor.retryAttemptTimeout;
742
+ changed = true;
743
+ }
744
+ if (changed)
745
+ normalized = { ...normalized, idleMonitor: nextIdleMonitor };
746
+ }
747
+ if (debug && typeof debug === 'object' && !Array.isArray(debug)
748
+ && Object.prototype.hasOwnProperty.call(debug, 'flusherDiag')) {
749
+ const nextDebug = { ...debug };
750
+ delete nextDebug.flusherDiag;
751
+ normalized = { ...normalized, debug: nextDebug };
752
+ }
753
+ return normalized;
754
+ }
640
755
  function normalizeRelationConfigForWrite(value) {
641
756
  const mutable = normalizeBaseagentEffortCompat({ ...value });
642
757
  normalizeShowActivitiesCompat(mutable);
@@ -702,9 +817,12 @@ export function write(target, value, sel, opts = {}) {
702
817
  const schema = target === ConfigTarget.Contact && requestedVersion === 2
703
818
  ? loadSchema('contact-book', 2)
704
819
  : loadSchema(TARGET_SCHEMA[target]);
820
+ const retiredGatewayPaths = legacyAunGatewayConfigPaths(value);
821
+ if (retiredGatewayPaths.length > 0) {
822
+ throw new ConfigError('AUN_GATEWAY_URL_REMOVED', legacyAunGatewayConfigError(file, retiredGatewayPaths), { paths: retiredGatewayPaths });
823
+ }
705
824
  const withVer = ensureSchemaVersion(value, schema.version);
706
- const migrated = withVer;
707
- const canonicalized = normalizeBaseagentEffortCompat(migrated);
825
+ const canonicalized = normalizeBaseagentEffortCompat(withVer);
708
826
  // Agent config 写入规范化(aid 校验、projects 字段清理)
709
827
  const normalized = target === ConfigTarget.Agent
710
828
  ? normalizeAgentConfigForWrite(canonicalized)
@@ -908,7 +1026,11 @@ function ensureSchemaVersion(value, version) {
908
1026
  }
909
1027
  function validateOrThrow(schema, value, target) {
910
1028
  const ok = schema.validate(value);
911
- const scopeErrors = [...validateProcessOnlyFields(target, value), ...validateProcessAidDomain(target, value)];
1029
+ const scopeErrors = [
1030
+ ...validateProcessOnlyFields(target, value),
1031
+ ...validateProcessAidDomain(target, value),
1032
+ ...validateProcessServiceNames(target, value),
1033
+ ];
912
1034
  if (!ok || scopeErrors.length > 0) {
913
1035
  const errs = [
914
1036
  ...scopeErrors,
@@ -949,6 +1071,24 @@ function validateProcessAidDomain(target, value) {
949
1071
  return [`/aun/defaultAidDomain ${error instanceof Error ? error.message : String(error)}`];
950
1072
  }
951
1073
  }
1074
+ function validateProcessServiceNames(target, value) {
1075
+ if (target !== ConfigTarget.Process || !value || typeof value !== 'object')
1076
+ return [];
1077
+ const services = value.services;
1078
+ if (!Array.isArray(services))
1079
+ return [];
1080
+ const seen = new Set();
1081
+ const errors = [];
1082
+ services.forEach((service, index) => {
1083
+ if (!service || typeof service !== 'object' || typeof service.name !== 'string')
1084
+ return;
1085
+ if (seen.has(service.name))
1086
+ errors.push(`/services/${index}/name duplicates service name ${JSON.stringify(service.name)}`);
1087
+ else
1088
+ seen.add(service.name);
1089
+ });
1090
+ return errors;
1091
+ }
952
1092
  /**
953
1093
  * responseModeParams 桶专项校验。
954
1094
  *
@@ -999,8 +1139,28 @@ export function validateConfig(target, value) {
999
1139
  ? loadSchema('contact-book', 2)
1000
1140
  : loadSchema(TARGET_SCHEMA[target]);
1001
1141
  const withVer = ensureSchemaVersion(value, schema.version);
1002
- const ok = schema.validate(withVer);
1003
- const errors = [...validateProcessOnlyFields(target, withVer), ...validateProcessAidDomain(target, withVer)];
1142
+ let normalized = withVer;
1143
+ try {
1144
+ if (target === ConfigTarget.Process)
1145
+ normalized = normalizeProcessConfigCompat(normalizeDaemonServices(withVer));
1146
+ }
1147
+ catch (error) {
1148
+ return [error instanceof Error ? error.message : String(error)];
1149
+ }
1150
+ const ok = schema.validate(normalized);
1151
+ const errors = [
1152
+ ...validateProcessOnlyFields(target, normalized),
1153
+ ...validateProcessAidDomain(target, normalized),
1154
+ ...validateProcessServiceNames(target, normalized),
1155
+ ];
1156
+ if (target === ConfigTarget.Defaults || target === ConfigTarget.Agent || target === ConfigTarget.Relation) {
1157
+ try {
1158
+ validateResponseModeParams(normalized?.responseModeParams, target);
1159
+ }
1160
+ catch (error) {
1161
+ errors.push(error instanceof ConfigError ? error.message : String(error));
1162
+ }
1163
+ }
1004
1164
  if (ok && errors.length === 0)
1005
1165
  return [];
1006
1166
  return [
@@ -1014,10 +1174,22 @@ export function validateConfig(target, value) {
1014
1174
  ];
1015
1175
  }
1016
1176
  export function validateConfigFile(target, sel) {
1017
- const value = atomicReadJson(targetPath(target, sel));
1177
+ const file = targetPath(target, sel);
1178
+ const value = atomicReadJson(file);
1018
1179
  if (value === null)
1019
1180
  return { exists: false, errors: [] };
1020
- return { exists: true, errors: validateConfig(target, value) };
1181
+ // Persisted pre-removal files remain readable: validate the same sanitized
1182
+ // view used by runtime reads, while new writes are still rejected by write().
1183
+ let sanitized;
1184
+ try {
1185
+ sanitized = target === ConfigTarget.Process
1186
+ ? normalizeDaemonServices(stripLegacyAunGatewayConfig(value, file, message => configWarn(message)))
1187
+ : stripLegacyAunGatewayConfig(value, file, message => configWarn(message));
1188
+ }
1189
+ catch (error) {
1190
+ return { exists: true, errors: [error instanceof Error ? error.message : String(error)] };
1191
+ }
1192
+ return { exists: true, errors: validateConfig(target, sanitized) };
1021
1193
  }
1022
1194
  function fileCacheAvailable() {
1023
1195
  try {
@@ -1078,13 +1250,40 @@ export function resolveAgentConfig(sel, opts = {}) {
1078
1250
  const agentConfig = sel.self ? read(ConfigTarget.Agent, sel, opts) : null;
1079
1251
  const relationConfig = (sel.self && sel.peerKey) ? read(ConfigTarget.Relation, sel, opts) : null;
1080
1252
  const layers = [
1081
- stripStaticOwnerField(read(ConfigTarget.Defaults, undefined, opts)),
1253
+ stripStaticOwnerField(normalizeDefaultsBehaviorCompat(read(ConfigTarget.Defaults, undefined, opts))),
1082
1254
  agentConfig,
1083
1255
  stripRelationRoleData(stripStaticOwnerField(relationConfig)),
1084
1256
  ];
1085
1257
  const merged = mergeLayers(layers, fields);
1086
1258
  return merged;
1087
1259
  }
1260
+ /**
1261
+ * defaults.schema keeps the historical `config` object as the process-wide
1262
+ * response behavior compatibility surface. Make those values participate in
1263
+ * the normal agent/relation merge chain without leaking the compatibility
1264
+ * container itself into EffectiveAgentConfig.
1265
+ *
1266
+ * Explicit canonical fields (if a future schema adds them) always win. A
1267
+ * single default chat mode applies to the three peer categories because this
1268
+ * compatibility field predates the per-category `chatmode` dictionary.
1269
+ */
1270
+ export function normalizeDefaultsBehaviorCompat(defaults) {
1271
+ if (!defaults || !defaults.config || typeof defaults.config !== 'object' || Array.isArray(defaults.config))
1272
+ return defaults;
1273
+ const { config, ...rest } = defaults;
1274
+ const normalized = { ...rest };
1275
+ const chatMode = config.chatMode;
1276
+ if (!normalized.chatmode && (chatMode === 'interactive' || chatMode === 'proactive')) {
1277
+ normalized.chatmode = { private: chatMode, group: chatMode, nothuman: chatMode };
1278
+ }
1279
+ if (normalized.mentionMode === undefined && (config.mentionMode === 'disabled' || config.mentionMode === 'mention-only')) {
1280
+ normalized.mentionMode = config.mentionMode;
1281
+ }
1282
+ if (normalized.show_activities === undefined && ['all', 'text', 'none'].includes(config.show_activities)) {
1283
+ normalized.show_activities = config.show_activities;
1284
+ }
1285
+ return normalized;
1286
+ }
1088
1287
  function stripStaticOwnerField(config) {
1089
1288
  if (!config)
1090
1289
  return config;
@@ -1108,6 +1307,7 @@ function stripRelationRoleData(config) {
1108
1307
  */
1109
1308
  export function resolveEffective(sel, opts = {}) {
1110
1309
  const config = resolveAgentConfig(sel, opts);
1310
+ const observableDefault = loadSchema('agent-config').fields.get('observable')?.default;
1111
1311
  const effective = {
1112
1312
  $schema_version: config.$schema_version ?? currentVersion('agent-config'),
1113
1313
  aid: config.aid ?? sel.self ?? '',
@@ -1121,7 +1321,9 @@ export function resolveEffective(sel, opts = {}) {
1121
1321
  projects: config.projects,
1122
1322
  capabilities: config.capabilities,
1123
1323
  readonlySourceDiagnostics: config.readonlySourceDiagnostics,
1124
- observable: config.observable,
1324
+ // Schema defaults are not applied by AJV during reads; apply the current
1325
+ // factory default here so omitted observable fields are enabled at runtime.
1326
+ observable: config.observable ?? (typeof observableDefault === 'boolean' ? observableDefault : true),
1125
1327
  extra_backup: config.extra_backup,
1126
1328
  // Runtime configuration parameters
1127
1329
  active_baseagent: config.active_baseagent,
@@ -287,8 +287,7 @@ function relPath(absolute) {
287
287
  }
288
288
  /**
289
289
  * 各存储层都无值时的兜底:若 schema 为该字段声明了 `default`,展示出厂默认并标记
290
- * schemaDefault=true(仅影响 `ec config get` 的展示;运行时 resolveEffective 不受影响,
291
- * 未设值仍为 undefined,以保留协议层回退等既有语义)。
290
+ * schemaDefault=true。运行时 resolveEffective 也会应用 Agent schema observable 默认值。
292
291
  */
293
292
  function getWithSchemaDefault(op, absent, note) {
294
293
  const nt = note ? { note } : {};
@@ -4,7 +4,7 @@ import { isExplicitGroupId } from '../aun/group-identity.js';
4
4
  import { agentConfig } from '../paths.js';
5
5
  import { getContactSnapshot, resolveContactView } from './contact-book.js';
6
6
  import { ContactMutationError } from './contact-book-store.js';
7
- import { mutateContactWithOperation } from './contact-request-service.js';
7
+ import { addContact, mutateContactWithOperation } from './contact-request-service.js';
8
8
  export function resolveContactCommand(argv) {
9
9
  const args = argv[0] === 'contact' ? argv.slice(1) : [...argv];
10
10
  const selfAid = String(argValue(args, '--self') || '').trim();
@@ -39,6 +39,21 @@ export function resolveContactCommand(argv) {
39
39
  },
40
40
  };
41
41
  }
42
+ if (command === 'add') {
43
+ if (!isValidAid(subcommand || '') || isExplicitGroupId(subcommand || '')) {
44
+ return invalid('INVALID_AID', 'primaryId must be a valid individual AID');
45
+ }
46
+ return {
47
+ ok: true,
48
+ command: {
49
+ kind: 'add',
50
+ operationId: 'contact.add',
51
+ selfAid,
52
+ primaryId: subcommand,
53
+ canonicalArgv: canonicalArgv(argv),
54
+ },
55
+ };
56
+ }
42
57
  return invalid('UNKNOWN_COMMAND', `Unknown contact command: ${positional.join(' ')}`);
43
58
  }
44
59
  export async function executeResolvedContactCommand(command, actor = 'local-cli') {
@@ -81,6 +96,22 @@ export async function executeResolvedContactCommand(command, actor = 'local-cli'
81
96
  contactRevision: result.contactRevision,
82
97
  };
83
98
  }
99
+ if (command.kind === 'add') {
100
+ const result = await addContact({
101
+ selfAid: command.selfAid,
102
+ primaryId: command.primaryId,
103
+ actorId: actor,
104
+ });
105
+ return {
106
+ ok: true,
107
+ operation: 'add',
108
+ selfAid: command.selfAid,
109
+ primaryId: command.primaryId,
110
+ status: 'active',
111
+ changed: result.changed,
112
+ contactRevision: result.contactRevision,
113
+ };
114
+ }
84
115
  return { ok: false, code: 'CONTACT_OPERATION_FAILED', error: 'Unsupported contact operation' };
85
116
  }
86
117
  catch (error) {
@@ -38,6 +38,50 @@ export async function mutateContactWithOperation(input) {
38
38
  const { value: _value, ...mutationResult } = result;
39
39
  return mutationResult;
40
40
  }
41
+ /**
42
+ * Add a canonical AUN contact as active through the Contact Book transaction.
43
+ * Existing pending/declined entries are intentionally promoted here, while a
44
+ * blocked entry remains a hard stop and must be explicitly unblocked first.
45
+ */
46
+ export async function addContact(input) {
47
+ const primaryId = String(input.primaryId || '').trim();
48
+ if (!isValidAid(primaryId)) {
49
+ throw new ContactMutationError('INVALID_AID', `Invalid contact AID: ${primaryId}`);
50
+ }
51
+ const at = new Date(input.now ?? Date.now()).toISOString();
52
+ const logFile = agentContactOperations(input.selfAid);
53
+ const result = await mutateContactBookPrepared(input.selfAid, ({ contact, contactRevision }) => {
54
+ if (input.expectedContactRevision !== undefined && input.expectedContactRevision !== contactRevision) {
55
+ throw new ContactMutationError('CONFLICT', 'Contact book changed since it was read', {
56
+ expectedContactRevision: input.expectedContactRevision,
57
+ contactRevision,
58
+ });
59
+ }
60
+ if (contact.contacts[primaryId]?.status === 'blocked') {
61
+ throw new ContactMutationError('CONTACT_BLOCKED', `Contact is blocked: ${primaryId}`);
62
+ }
63
+ const mutations = [
64
+ { type: 'set-status', primaryId, status: 'active' },
65
+ ];
66
+ if (input.displayName !== undefined) {
67
+ mutations.push({ type: 'set-display-name', primaryId, displayName: input.displayName });
68
+ }
69
+ return {
70
+ mutation: mutations.length === 1 ? mutations[0] : { type: 'batch', mutations },
71
+ actor: input.actorId,
72
+ value: undefined,
73
+ beforeCommit: ({ contactRevision: nextRevision }) => operationLogChange(logFile, [operationRecord({
74
+ at,
75
+ op: 'manual-add',
76
+ primaryId,
77
+ actorId: input.actorId,
78
+ contactRevision: nextRevision,
79
+ })]),
80
+ };
81
+ });
82
+ const { value: _value, ...mutationResult } = result;
83
+ return mutationResult;
84
+ }
41
85
  export async function submitContactRequest(input) {
42
86
  const applicantAid = String(input.applicantAid || '').trim();
43
87
  const owners = listAgentOwners(input.selfAid);