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.
- package/CHANGELOG.md +43 -1
- package/README.md +60 -9
- package/bin/install-codex-managed-hooks.mjs +61 -0
- package/dist/agents/baseagent.js +10 -6
- package/dist/agents/claude-runner.js +383 -111
- package/dist/agents/codex-app-server-client.js +10 -2
- package/dist/agents/codex-runner.js +405 -137
- package/dist/agents/ecagent-runner.js +174 -63
- package/dist/agents/gemini-runner.js +130 -30
- package/dist/agents/request-identity.js +25 -0
- package/dist/agents/runner-types.js +19 -0
- package/dist/aun/aid/agentmd.js +66 -2
- package/dist/aun/aid/identity.js +4 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +86 -9
- package/dist/aun/msg/history.js +213 -36
- package/dist/aun/msg/managed-operation.js +58 -9
- package/dist/aun/msg/p2p.js +26 -11
- package/dist/aun/outbox.js +295 -68
- package/dist/aun/service-proxy.js +43 -25
- package/dist/channels/aun.js +1004 -273
- package/dist/channels/daemon.js +6 -1
- package/dist/cli/agent-command.js +4 -3
- package/dist/cli/agent.js +66 -56
- package/dist/cli/aun-commands.js +177 -42
- package/dist/cli/command-log.js +10 -11
- package/dist/cli/contact.js +1 -0
- package/dist/cli/daemon-commands.js +81 -121
- package/dist/cli/index.js +1 -0
- package/dist/cli/init.js +76 -24
- package/dist/cli/restart-monitor.js +3 -3
- package/dist/cli/task-context.js +46 -0
- package/dist/cli/trigger-command.js +1 -1
- package/dist/cli/watch-logs.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +16 -5
- package/dist/config/config-manager.js +226 -24
- package/dist/config/config-operation-service.js +1 -2
- package/dist/config/contact-operation-service.js +32 -1
- package/dist/config/contact-request-service.js +44 -0
- package/dist/config/daemon-services.js +186 -0
- package/dist/config/gateway-config.js +29 -16
- package/dist/config/lifecycle.js +16 -5
- package/dist/config/role-service.js +54 -3
- package/dist/config/schema-migration.js +550 -0
- package/dist/config-store.js +161 -12
- package/dist/core/agent-application-service.js +279 -0
- package/dist/core/audit/log-integrity.js +102 -0
- package/dist/core/auth/agent-delegation.js +31 -1
- package/dist/core/auth/auth-gateway.js +33 -4
- package/dist/core/auth/authorization-audit.js +155 -4
- package/dist/core/auth/operation-authorizer.js +41 -1
- package/dist/core/auth/operation-catalog.js +9 -1
- package/dist/core/bootstrap-messages.js +2 -2
- package/dist/core/bootstrap-service.js +27 -38
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/channel-loader.js +0 -2
- package/dist/core/command/agent-control.js +56 -16
- package/dist/core/command/command-handler.js +290 -44
- package/dist/core/command/connect-menu.js +3 -4
- package/dist/core/command/group-menu.js +5 -7
- package/dist/core/command/menu-handler.js +279 -80
- package/dist/core/command/role-menu.js +21 -11
- package/dist/core/command/slash-gate.js +85 -18
- package/dist/core/command/slash-handler.js +350 -32
- package/dist/core/event-catalog.js +5 -0
- package/dist/core/evolagent.js +9 -4
- package/dist/core/handoff/dispatcher.js +4 -0
- package/dist/core/handoff/runtime.js +10 -0
- package/dist/core/handoff/store.js +32 -9
- package/dist/core/inference/text-inference.js +7 -15
- package/dist/core/message/im-renderer.js +83 -84
- package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
- package/dist/core/message/message-bridge.js +124 -10
- package/dist/core/message/message-log.js +14 -7
- package/dist/core/message/message-queue.js +206 -16
- package/dist/core/message/message-utils.js +12 -5
- package/dist/core/message/response-engine.js +495 -72
- package/dist/core/message/send-receipt.js +1 -0
- package/dist/core/message/stream-debouncer.js +9 -2
- package/dist/core/model/model-catalog.js +23 -15
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/approval-gateway.js +180 -6
- package/dist/core/permission/ec-command-parser.js +465 -56
- package/dist/core/permission/mode.js +18 -3
- package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
- package/dist/core/permission/readonly-shell-query.js +263 -9
- package/dist/core/permission/sandbox-runtime.js +205 -13
- package/dist/core/permission/tool-policy.js +673 -62
- package/dist/core/relation/peer-identity.js +18 -0
- package/dist/core/session/session-fs-store.js +154 -5
- package/dist/core/session/session-manager.js +299 -30
- package/dist/core/session/session-renew.js +19 -12
- package/dist/core/session/session-turn-coordinator.js +11 -4
- package/dist/eck/kit-renderer.js +18 -9
- package/dist/index.js +283 -65
- package/dist/ipc.js +374 -24
- package/dist/paths.js +64 -7
- package/dist/response-system/context-builder.js +1 -7
- package/dist/trigger/anomaly-store.js +1 -0
- package/dist/trigger/feedback.js +56 -5
- package/dist/trigger/history.js +79 -4
- package/dist/trigger/legacy-session-history.js +2 -2
- package/dist/trigger/parser.js +3 -2
- package/dist/trigger/validation.js +6 -1
- package/dist/utils/atomic-write.js +27 -0
- package/dist/utils/ecweb-utils.js +16 -2
- package/dist/utils/error-utils.js +4 -1
- package/dist/utils/logger.js +21 -2
- package/dist/utils/process-tree-stats.js +24 -4
- package/dist/utils/process-tree-worker.js +31 -0
- package/dist/utils/project-path.js +1 -2
- package/dist/utils/stats.js +52 -18
- package/dist/utils/welcome.js +2 -2
- package/kits/docs/INDEX.md +1 -1
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/contact.md +7 -1
- package/kits/docs/evolcore/msg.md +16 -0
- package/kits/rules/01-overview.md +1 -1
- package/kits/rules/03-identity.md +1 -1
- package/kits/rules/05-venue.md +1 -1
- package/kits/schemas/_meta.json +10 -5
- package/kits/schemas/agent-config.schema.10.json +2 -1
- package/kits/schemas/agent-config.schema.11.json +421 -0
- package/kits/schemas/daemon.schema.5.json +135 -0
- package/kits/schemas/daemon.schema.6.json +131 -0
- package/kits/schemas/defaults.schema.5.json +119 -0
- package/kits/schemas/migrations/README.md +3 -1
- package/kits/schemas/relation-config.schema.8.json +13 -0
- package/kits/schemas/role-config.schema.1.json +1 -2
- package/kits/schemas/single-session.schema.3.json +32 -0
- package/kits/templates/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/package.json +7 -3
- package/skills/eclink/SKILL.md +2 -0
package/dist/cli/task-context.js
CHANGED
|
@@ -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)
|
package/dist/cli/watch-logs.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
15
|
+
/** 从支持的日志文件名列表推导去重、字母序的类型列表。 */
|
|
9
16
|
export function deriveLogTypes(files) {
|
|
10
17
|
const set = new Set();
|
|
11
18
|
for (const f of files) {
|
|
12
|
-
if (!f
|
|
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'
|
|
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
|
|
79
|
-
|
|
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\.[^.]+\.
|
|
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
|
|
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 (!
|
|
219
|
-
if (
|
|
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.
|
|
525
|
-
?
|
|
526
|
-
: target === ConfigTarget.
|
|
527
|
-
?
|
|
528
|
-
:
|
|
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
|
|
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
|
-
|
|
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
|
|
712
|
+
return normalized;
|
|
630
713
|
try {
|
|
631
714
|
return {
|
|
632
|
-
...
|
|
633
|
-
aun: { ...
|
|
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
|
|
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 = [
|
|
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
|
-
|
|
1003
|
-
|
|
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
|
|
1177
|
+
const file = targetPath(target, sel);
|
|
1178
|
+
const value = atomicReadJson(file);
|
|
1018
1179
|
if (value === null)
|
|
1019
1180
|
return { exists: false, errors: [] };
|
|
1020
|
-
|
|
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
|
-
|
|
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
|
|
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);
|