@xfxstudio/claworld 0.2.9 → 0.2.10-beta.1

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 (49) hide show
  1. package/README.md +1 -1
  2. package/openclaw.plugin.json +7 -63
  3. package/package.json +6 -2
  4. package/skills/claworld-help/SKILL.md +5 -1
  5. package/skills/claworld-join-and-chat/SKILL.md +21 -1
  6. package/skills/claworld-manage-worlds/SKILL.md +81 -10
  7. package/src/lib/agent-profile.js +8 -3
  8. package/src/lib/chat-request.js +0 -1
  9. package/src/lib/policy.js +2 -6
  10. package/src/lib/public-identity.js +175 -0
  11. package/src/lib/relay/kickoff-text.js +1 -0
  12. package/src/openclaw/installer/cli.js +48 -4
  13. package/src/openclaw/installer/constants.js +1 -0
  14. package/src/openclaw/installer/core.js +247 -71
  15. package/src/openclaw/installer/doctor.js +31 -17
  16. package/src/openclaw/plugin/account-identity.js +1 -2
  17. package/src/openclaw/plugin/claworld-channel-plugin.js +453 -263
  18. package/src/openclaw/plugin/config-schema.js +9 -23
  19. package/src/openclaw/plugin/managed-config.js +294 -84
  20. package/src/openclaw/plugin/onboarding.js +37 -45
  21. package/src/openclaw/plugin/register.js +124 -13
  22. package/src/openclaw/plugin/relay-client.js +233 -17
  23. package/src/openclaw/runtime/backend-error-context.js +91 -0
  24. package/src/openclaw/runtime/feedback-helper.js +1 -2
  25. package/src/openclaw/runtime/product-shell-helper.js +43 -9
  26. package/src/openclaw/runtime/tool-contracts.js +26 -3
  27. package/src/openclaw/runtime/tool-inventory.js +7 -0
  28. package/src/openclaw/runtime/world-moderation-helper.js +3 -19
  29. package/src/product-shell/contracts/candidate-feed.js +7 -0
  30. package/src/product-shell/contracts/world-manifest.js +0 -1
  31. package/src/product-shell/contracts/world-orchestration.js +10 -1
  32. package/src/product-shell/conversation-feedback/conversation-feedback-service.js +261 -0
  33. package/src/product-shell/feedback/feedback-routes.js +0 -1
  34. package/src/product-shell/feedback/feedback-service.js +4 -9
  35. package/src/product-shell/index.js +40 -7
  36. package/src/product-shell/matching/matchmaking-service.js +22 -1
  37. package/src/product-shell/membership/membership-service.js +5 -1
  38. package/src/product-shell/onboarding/onboarding-service.js +16 -26
  39. package/src/product-shell/profile/public-identity-routes.js +60 -0
  40. package/src/product-shell/profile/public-identity-service.js +190 -0
  41. package/src/product-shell/search/search-service.js +9 -2
  42. package/src/product-shell/social/chat-request-service.js +22 -7
  43. package/src/product-shell/social/friend-routes.js +1 -1
  44. package/src/product-shell/social/friend-service.js +16 -19
  45. package/src/product-shell/social/social-routes.js +2 -2
  46. package/src/product-shell/social/social-service.js +31 -35
  47. package/src/product-shell/worlds/world-admin-service.js +31 -10
  48. package/src/product-shell/worlds/world-broadcast-service.js +2 -2
  49. package/src/lib/agent-address.js +0 -46
@@ -45,12 +45,15 @@ import {
45
45
  resolveWorldSelection,
46
46
  resolveWorldSelectionFlow,
47
47
  } from '../runtime/product-shell-helper.js';
48
+ import { extractBackendErrorContext } from '../runtime/backend-error-context.js';
48
49
  import { getClaworldRuntime } from './runtime.js';
49
50
  import {
50
51
  createRuntimeBoundaryError,
51
52
  normalizeRuntimeBoundaryError,
52
53
  serializeRuntimeBoundaryError,
53
54
  } from '../../lib/runtime-errors.js';
55
+ import { PUBLIC_IDENTITY_STATUS } from '../../lib/public-identity.js';
56
+ import { v4 as uuidv4 } from 'uuid';
54
57
 
55
58
  function normalizeRelayHttpBaseUrl(serverUrl) {
56
59
  const parsed = new URL(serverUrl);
@@ -67,108 +70,16 @@ function normalizePluginOptionalText(value) {
67
70
  return normalized || null;
68
71
  }
69
72
 
70
- function inferRelayDomain(runtimeConfig = {}) {
71
- const defaultToAddress = String(runtimeConfig.relay?.defaultToAddress || '').trim();
72
- const atIndex = defaultToAddress.indexOf('@');
73
- if (atIndex > 0 && atIndex < defaultToAddress.length - 1) {
74
- return defaultToAddress.slice(atIndex + 1).trim().toLowerCase();
75
- }
76
- return 'relay.local';
77
- }
78
-
79
- const RELAY_LOCAL_AGENT_CODE_PATTERN = /^[A-Za-z0-9._:+~-]+$/;
80
- const RELAY_CANONICAL_AGENT_CODE_PATTERN = /^[A-Za-z0-9._:+~-]+@[A-Za-z0-9._:+~-]+$/;
81
-
82
- function normalizeRelayDomainName(value, fallback = null) {
83
- const normalized = normalizeClaworldText(value, fallback)?.toLowerCase() || null;
84
- if (!normalized) return fallback;
85
- if (!/^[a-z0-9._:+~-]+$/.test(normalized)) return fallback;
86
- return normalized;
87
- }
88
-
89
- function normalizeRelayLocalAgentCode(value, fallback = null) {
90
- const normalized = normalizeClaworldText(value, fallback)?.toLowerCase() || null;
91
- if (!normalized) return fallback;
92
- if (!RELAY_LOCAL_AGENT_CODE_PATTERN.test(normalized)) return fallback;
93
- return normalized;
94
- }
95
-
96
- function parseCanonicalRelayAgentCode(value) {
97
- const normalized = normalizeClaworldText(value, null)?.toLowerCase() || null;
98
- if (!normalized || !RELAY_CANONICAL_AGENT_CODE_PATTERN.test(normalized)) return null;
99
- const atIndex = normalized.indexOf('@');
100
- if (atIndex <= 0 || atIndex >= normalized.length - 1) return null;
101
- const relayLocalCode = normalizeRelayLocalAgentCode(normalized.slice(0, atIndex), null);
102
- const domain = normalizeRelayDomainName(normalized.slice(atIndex + 1), null);
103
- if (!relayLocalCode || !domain) return null;
104
- return {
105
- agentCode: `${relayLocalCode}@${domain}`,
106
- relayLocalCode,
107
- domain,
108
- };
109
- }
110
-
111
- function buildCanonicalRelayAgentCode(relayLocalCode, runtimeConfig = {}, domainOverride = null) {
112
- const normalizedRelayLocalCode = normalizeRelayLocalAgentCode(relayLocalCode, null);
113
- const domain = normalizeRelayDomainName(domainOverride, null)
114
- || normalizeRelayDomainName(inferRelayDomain(runtimeConfig), null);
115
- if (!normalizedRelayLocalCode || !domain) return null;
116
- return `${normalizedRelayLocalCode}@${domain}`;
117
- }
118
-
119
- function normalizeCanonicalRelayAgentCode(value, runtimeConfig = {}) {
120
- const parsed = parseCanonicalRelayAgentCode(value);
121
- if (parsed) return parsed.agentCode;
122
- const relayLocalCode = normalizeRelayLocalAgentCode(value, null);
123
- if (!relayLocalCode) return null;
124
- return buildCanonicalRelayAgentCode(relayLocalCode, runtimeConfig);
73
+ function resolveClientMessageId(value = null) {
74
+ return normalizePluginOptionalText(value) || `cmsg_${uuidv4()}`;
125
75
  }
126
76
 
127
- function normalizeRelayIdentityInput({ runtimeConfig = {}, agentId = null, agentCode = null, address = null } = {}) {
128
- const normalizedAgentId = normalizeClaworldText(agentId, null);
129
- const normalizedAddress = normalizeClaworldText(resolveRelayAddress(address || agentCode || '', runtimeConfig), null)?.toLowerCase() || null;
130
- const parsedCanonicalAgentCode = parseCanonicalRelayAgentCode(agentCode)
131
- || parseCanonicalRelayAgentCode(normalizedAddress);
132
- const relayLocalCode = normalizeRelayLocalAgentCode(agentCode, null)
133
- || parsedCanonicalAgentCode?.relayLocalCode
134
- || null;
135
- const domain = parsedCanonicalAgentCode?.domain
136
- || normalizeRelayDomainName(normalizedAddress?.split('@')[1], null)
137
- || normalizeRelayDomainName(inferRelayDomain(runtimeConfig), null);
138
- const normalizedAgentCode = parsedCanonicalAgentCode?.agentCode
139
- || (relayLocalCode ? buildCanonicalRelayAgentCode(relayLocalCode, runtimeConfig, domain) : null)
140
- || null;
141
-
142
- return {
143
- agentId: normalizedAgentId,
144
- agentCode: normalizedAgentCode,
145
- relayLocalCode,
146
- address: normalizedAddress || normalizedAgentCode || null,
147
- domain,
148
- };
149
- }
150
-
151
- function buildRelayAgentSummary(item = {}, runtimeConfig = {}) {
77
+ function buildRelayAgentSummary(item = {}) {
152
78
  const normalizedAgentId = normalizeClaworldText(item?.agentId, null);
153
- const relayLocalCode = normalizeRelayLocalAgentCode(item?.agentCode, null);
154
- const address = normalizeClaworldText(item?.address, null)?.toLowerCase()
155
- || buildCanonicalRelayAgentCode(relayLocalCode, runtimeConfig, item?.domain)
156
- || null;
157
- const parsedCanonicalAgentCode = parseCanonicalRelayAgentCode(address);
158
- const domain = normalizeRelayDomainName(item?.domain, null)
159
- || parsedCanonicalAgentCode?.domain
160
- || normalizeRelayDomainName(inferRelayDomain(runtimeConfig), null);
161
- const canonicalAgentCode = parsedCanonicalAgentCode?.agentCode
162
- || buildCanonicalRelayAgentCode(relayLocalCode, runtimeConfig, domain)
163
- || null;
164
-
165
79
  return {
166
80
  agentId: normalizedAgentId,
167
- agentCode: canonicalAgentCode,
168
- relayLocalCode: relayLocalCode || parsedCanonicalAgentCode?.relayLocalCode || null,
169
- domain,
170
- address: address || canonicalAgentCode || null,
171
81
  displayName: normalizeClaworldText(item?.displayName, null),
82
+ publicIdentity: item?.publicIdentity && typeof item.publicIdentity === 'object' ? item.publicIdentity : null,
172
83
  discoverable: typeof item?.discoverable === 'boolean' ? item.discoverable : null,
173
84
  contactable: typeof item?.contactable === 'boolean' ? item.contactable : null,
174
85
  online: typeof item?.online === 'boolean' ? item.online : null,
@@ -179,19 +90,10 @@ function normalizeClaworldTarget(raw) {
179
90
  if (typeof raw !== 'string') return undefined;
180
91
  let value = raw.trim();
181
92
  if (!value) return undefined;
182
- value = value.replace(/^claworld:/i, '').replace(/^user:/i, '').replace(/^@/, '').trim();
93
+ value = value.replace(/^claworld:/i, '').replace(/^user:/i, '').trim();
183
94
  return value || undefined;
184
95
  }
185
96
 
186
- function resolveRelayAddress(to, runtimeConfig = {}) {
187
- const normalized = normalizeClaworldTarget(typeof to === 'string' ? to : '');
188
- const fallback = normalizeClaworldTarget(runtimeConfig.relay?.defaultToAddress || '');
189
- const candidate = normalized || fallback;
190
- if (!candidate) return '';
191
- if (candidate.includes('@')) return candidate.toLowerCase();
192
- return `${candidate.toLowerCase()}@${inferRelayDomain(runtimeConfig)}`;
193
- }
194
-
195
97
  function normalizeClaworldText(value, fallback = null) {
196
98
  if (value == null) return fallback;
197
99
  const normalized = String(value).trim();
@@ -215,6 +117,45 @@ function shouldAuthorizeBridgedCommand({ runtimeConfig = {}, incomingText }) {
215
117
  return typeof incomingText === 'string' && incomingText.trim().startsWith('/');
216
118
  }
217
119
 
120
+ function normalizeUntrustedContextLines(value) {
121
+ if (Array.isArray(value)) {
122
+ return value
123
+ .map((entry) => resolveNormalizedText(entry, null))
124
+ .filter(Boolean);
125
+ }
126
+ const singleLine = resolveNormalizedText(value, null);
127
+ return singleLine ? [singleLine] : [];
128
+ }
129
+
130
+ function mergeUntrustedContextLines(...groups) {
131
+ const merged = [];
132
+ const seen = new Set();
133
+ for (const group of groups) {
134
+ for (const line of normalizeUntrustedContextLines(group)) {
135
+ if (seen.has(line)) continue;
136
+ seen.add(line);
137
+ merged.push(line);
138
+ }
139
+ }
140
+ return merged;
141
+ }
142
+
143
+ function parseBridgeTimestampMs(value) {
144
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
145
+ if (typeof value !== 'string') return null;
146
+ const normalized = value.trim();
147
+ if (!normalized) return null;
148
+ const parsed = Date.parse(normalized);
149
+ return Number.isFinite(parsed) ? parsed : null;
150
+ }
151
+
152
+ function resolveBridgeDeliveryTimestampMs({ delivery = {}, metadata = {} } = {}) {
153
+ return parseBridgeTimestampMs(delivery?.createdAt)
154
+ || parseBridgeTimestampMs(delivery?.turnCreatedAt)
155
+ || parseBridgeTimestampMs(metadata?.createdAt)
156
+ || Date.now();
157
+ }
158
+
218
159
  const CLAWORLD_RELAY_OPERATIONAL_NOTICE_PATTERNS = [
219
160
  /^🧭\s*New session:\s+\S+/i,
220
161
  /^🧹\s*Auto-compaction complete(?:\s*\(count \d+\))?\.$/i,
@@ -355,10 +296,11 @@ function buildClaworldDirectoryEntries(config = {}, accountId = null) {
355
296
  const account = inspectClaworldChannelAccount(config, id);
356
297
  if (!account?.enabled || !account?.configured) continue;
357
298
  const normalizedId = String(account.accountId || id || '').trim();
358
- if (!normalizedId) continue;
299
+ const boundAgentId = normalizeClaworldText(account.relay?.agentId, null);
300
+ if (!normalizedId || !boundAgentId) continue;
359
301
  if (currentAccountId && normalizedId === currentAccountId) continue;
360
302
  entries.push({
361
- id: resolveRelayAddress(normalizedId, account),
303
+ id: boundAgentId,
362
304
  name: normalizedId,
363
305
  handle: normalizedId,
364
306
  rank: 100,
@@ -366,11 +308,10 @@ function buildClaworldDirectoryEntries(config = {}, accountId = null) {
366
308
  }
367
309
 
368
310
  const current = inspectClaworldChannelAccount(config, currentAccountId || null);
369
- const defaultTarget = resolveRelayAddress(current?.relay?.defaultToAddress || '', current || {});
370
- if (defaultTarget) {
371
- const localPart = defaultTarget.split('@')[0] || defaultTarget;
372
- if (!entries.some((entry) => entry.id === defaultTarget)) {
373
- entries.push({ id: defaultTarget, name: localPart, handle: localPart, rank: 50 });
311
+ const defaultTargetAgentId = normalizeClaworldText(current?.relay?.defaultTargetAgentId, null);
312
+ if (defaultTargetAgentId) {
313
+ if (!entries.some((entry) => entry.id === defaultTargetAgentId)) {
314
+ entries.push({ id: defaultTargetAgentId, name: defaultTargetAgentId, handle: defaultTargetAgentId, rank: 50 });
374
315
  }
375
316
  }
376
317
 
@@ -381,8 +322,8 @@ async function deliverRelayMessage({ runtimeConfig, to, text, fetchImpl, logger,
381
322
  const fromAgentId = runtimeConfig.relay?.agentId;
382
323
  if (!fromAgentId) throw new Error('claworld relay.agentId is required for outbound send');
383
324
 
384
- const toAddress = resolveRelayAddress(to, runtimeConfig);
385
- if (!toAddress) throw new Error('claworld outbound target is required');
325
+ const targetAgentId = normalizeClaworldText(to, null);
326
+ if (!targetAgentId) throw new Error('claworld outbound targetAgentId is required');
386
327
 
387
328
  const normalizedText = normalizeClaworldText(text, null);
388
329
  const payload = messagePayload && typeof messagePayload === 'object'
@@ -397,6 +338,9 @@ async function deliverRelayMessage({ runtimeConfig, to, text, fetchImpl, logger,
397
338
  }
398
339
  payload.source = normalizeClaworldText(payload.source, 'openclaw-claworld');
399
340
  payload.accountId = normalizeClaworldText(payload.accountId, runtimeConfig.accountId);
341
+ const clientMessageId = resolveClientMessageId(
342
+ outboundContext.clientMessageId || outboundContext.metadata?.clientMessageId || null
343
+ );
400
344
 
401
345
  const baseUrl = normalizeRelayHttpBaseUrl(runtimeConfig.serverUrl);
402
346
  const result = await fetchJson(fetchImpl, `${baseUrl}/v1/messages`, {
@@ -408,7 +352,8 @@ async function deliverRelayMessage({ runtimeConfig, to, text, fetchImpl, logger,
408
352
  },
409
353
  body: JSON.stringify({
410
354
  fromAgentId,
411
- toAddress,
355
+ targetAgentId,
356
+ clientMessageId,
412
357
  payload,
413
358
  conversation: {
414
359
  conversationKey: outboundContext.conversationKey || outboundContext.metadata?.conversationKey || null,
@@ -433,7 +378,7 @@ async function deliverRelayMessage({ runtimeConfig, to, text, fetchImpl, logger,
433
378
  context: {
434
379
  accountId: runtimeConfig.accountId || null,
435
380
  fromAgentId,
436
- toAddress,
381
+ targetAgentId,
437
382
  status: result.status,
438
383
  },
439
384
  });
@@ -442,13 +387,14 @@ async function deliverRelayMessage({ runtimeConfig, to, text, fetchImpl, logger,
442
387
  return {
443
388
  channel: 'claworld',
444
389
  messageId: result.body?.turn?.turnId || `turn_${Date.now()}`,
445
- chatId: toAddress,
390
+ chatId: targetAgentId,
446
391
  timestamp: Date.now(),
447
392
  meta: {
393
+ clientMessageId,
448
394
  sessionKey: result.body?.delivery?.sessionKey || outboundContext.sessionKey || outboundContext.SessionKey || null,
449
395
  turnId: result.body?.turn?.turnId || null,
450
396
  conversationKey: result.body?.conversationKey || null,
451
- toAddress,
397
+ targetAgentId,
452
398
  },
453
399
  };
454
400
  }
@@ -481,8 +427,7 @@ function createRelayRouteError({
481
427
  context: {
482
428
  accountId: runtimeConfig.accountId || null,
483
429
  httpStatus: result?.status || null,
484
- backendCode: result?.body?.error || null,
485
- backendMessage: result?.body?.message || null,
430
+ ...extractBackendErrorContext(result?.body),
486
431
  ...context,
487
432
  },
488
433
  });
@@ -491,7 +436,7 @@ function createRelayRouteError({
491
436
  async function createFriendRequest({
492
437
  runtimeConfig,
493
438
  fromAgentId,
494
- toAddress,
439
+ targetAgentId,
495
440
  message = null,
496
441
  metadata = {},
497
442
  fetchImpl,
@@ -506,7 +451,7 @@ async function createFriendRequest({
506
451
  },
507
452
  body: JSON.stringify({
508
453
  fromAgentId,
509
- toAddress,
454
+ targetAgentId,
510
455
  message: normalizeClaworldText(message, null),
511
456
  metadata: metadata && typeof metadata === 'object' && !Array.isArray(metadata) ? metadata : {},
512
457
  }),
@@ -517,7 +462,7 @@ async function createFriendRequest({
517
462
  runtimeConfig,
518
463
  code: 'friend_request_failed',
519
464
  publicMessage: 'failed to create friend request',
520
- context: { fromAgentId, toAddress },
465
+ context: { fromAgentId, targetAgentId },
521
466
  });
522
467
  }
523
468
  return result.body || {};
@@ -833,16 +778,6 @@ function isNonRecoverableBootstrapHoldError(error) {
833
778
  }
834
779
 
835
780
  function buildBootstrapHoldMessage(error, runtimeConfig = {}) {
836
- if (error?.code === 'agent_code_conflict') {
837
- const requestedAgentCode = normalizeClaworldText(
838
- error?.context?.requestedAgentCode,
839
- normalizeRuntimeRegistration(runtimeConfig).agentCode || null,
840
- );
841
- return requestedAgentCode
842
- ? `Claworld setup blocked: relay agent code ${requestedAgentCode} already exists. Add the bound appToken for this account or choose a different registration.agentCode.`
843
- : 'Claworld setup blocked: relay agent code already exists. Add the bound appToken for this account or choose a different registration.agentCode.';
844
- }
845
-
846
781
  const publicMessage = normalizeClaworldText(error?.publicMessage, null);
847
782
  const fallbackMessage = normalizeClaworldText(error?.message, 'relay binding bootstrap failed');
848
783
  return `Claworld setup blocked: ${publicMessage || fallbackMessage}`;
@@ -970,6 +905,175 @@ async function fetchJson(fetchImpl, url, init = {}) {
970
905
  }
971
906
  }
972
907
 
908
+ async function fetchPublicIdentity({
909
+ runtimeConfig,
910
+ agentId = null,
911
+ fetchImpl,
912
+ }) {
913
+ if (!resolveRuntimeAppToken(runtimeConfig)) {
914
+ const recommendedDisplayName = normalizeClaworldText(
915
+ runtimeConfig?.name,
916
+ normalizeClaworldText(runtimeConfig?.registration?.displayName, null),
917
+ );
918
+ return {
919
+ status: 'pending',
920
+ agentId: normalizeClaworldText(agentId, null),
921
+ ready: false,
922
+ publicIdentity: {
923
+ status: PUBLIC_IDENTITY_STATUS.PENDING,
924
+ displayName: null,
925
+ code: null,
926
+ displayIdentity: null,
927
+ confirmedAt: null,
928
+ updatedAt: null,
929
+ },
930
+ recommendedDisplayName,
931
+ nextAction: 'set_public_identity',
932
+ requiredAction: 'set_public_identity',
933
+ nextTool: 'claworld_update_public_identity',
934
+ missingFields: [
935
+ {
936
+ fieldId: 'displayName',
937
+ label: 'Public Name',
938
+ description: 'A public display name used in Claworld identity surfaces.',
939
+ },
940
+ {
941
+ fieldId: 'code',
942
+ label: 'Public Code',
943
+ description: 'A system-generated unique suffix used in the public identity.',
944
+ },
945
+ ],
946
+ feedbackSummary: {
947
+ totalLikesReceived: 0,
948
+ totalDislikesReceived: 0,
949
+ totalLikesGiven: 0,
950
+ totalDislikesGiven: 0,
951
+ },
952
+ };
953
+ }
954
+
955
+ const baseUrl = normalizeRelayHttpBaseUrl(runtimeConfig.serverUrl);
956
+ const path = buildRelayJsonPath('/v1/profile/public-identity', {
957
+ agentId,
958
+ });
959
+ const result = await fetchJson(fetchImpl, `${baseUrl}${path}`, {
960
+ method: 'GET',
961
+ headers: {
962
+ ...(runtimeConfig.apiKey ? { 'x-api-key': runtimeConfig.apiKey } : {}),
963
+ ...buildRuntimeAuthHeaders(runtimeConfig),
964
+ },
965
+ });
966
+ if (!result.ok) {
967
+ createRelayRouteError({
968
+ result,
969
+ runtimeConfig,
970
+ code: 'public_identity_fetch_failed',
971
+ publicMessage: 'failed to read public identity status',
972
+ context: { agentId: normalizeClaworldText(agentId, null) },
973
+ });
974
+ }
975
+ return result.body || {};
976
+ }
977
+
978
+ async function updatePublicIdentity({
979
+ runtimeConfig,
980
+ agentId = null,
981
+ displayName = null,
982
+ fetchImpl,
983
+ }) {
984
+ const normalizedDisplayName = normalizeClaworldText(displayName, null);
985
+ if (!normalizedDisplayName) {
986
+ throw createRuntimeBoundaryError({
987
+ code: 'tool_input_invalid',
988
+ category: 'input',
989
+ status: 400,
990
+ message: 'claworld public identity update requires displayName',
991
+ publicMessage: 'claworld public identity update requires displayName',
992
+ recoverable: true,
993
+ context: { field: 'displayName' },
994
+ });
995
+ }
996
+ let resolvedRuntimeConfig = applyRuntimeIdentity(runtimeConfig);
997
+ let resolvedAgentId = normalizeClaworldText(agentId, normalizeClaworldText(resolvedRuntimeConfig?.relay?.agentId, null));
998
+
999
+ if (!resolveRuntimeAppToken(resolvedRuntimeConfig)) {
1000
+ const baseUrl = normalizeRelayHttpBaseUrl(resolvedRuntimeConfig.serverUrl);
1001
+ const activationResult = await fetchJson(fetchImpl, `${baseUrl}/v1/onboarding/activate`, {
1002
+ method: 'POST',
1003
+ headers: {
1004
+ 'content-type': 'application/json',
1005
+ ...(resolvedRuntimeConfig.apiKey ? { 'x-api-key': resolvedRuntimeConfig.apiKey } : {}),
1006
+ ...buildRuntimeAuthHeaders(resolvedRuntimeConfig),
1007
+ },
1008
+ body: JSON.stringify({
1009
+ displayName: normalizedDisplayName,
1010
+ }),
1011
+ });
1012
+ if (!activationResult.ok) {
1013
+ createRelayRouteError({
1014
+ result: activationResult,
1015
+ runtimeConfig: resolvedRuntimeConfig,
1016
+ code: 'claworld_activation_failed',
1017
+ publicMessage: 'failed to activate Claworld account',
1018
+ });
1019
+ }
1020
+ const activatedToken = normalizeClaworldText(activationResult.body?.appToken, null);
1021
+ const activatedAgentId = normalizeClaworldText(activationResult.body?.agentId, null);
1022
+ if (!activatedToken || !activatedAgentId) {
1023
+ throw createRuntimeBoundaryError({
1024
+ code: 'claworld_activation_failed',
1025
+ category: 'runtime',
1026
+ status: 502,
1027
+ message: 'claworld activation did not return appToken and agentId',
1028
+ publicMessage: 'failed to activate Claworld account',
1029
+ recoverable: true,
1030
+ });
1031
+ }
1032
+ resolvedRuntimeConfig = applyRuntimeIdentity(resolvedRuntimeConfig, {
1033
+ appToken: activatedToken,
1034
+ relay: {
1035
+ ...(resolvedRuntimeConfig?.relay && typeof resolvedRuntimeConfig.relay === 'object' ? resolvedRuntimeConfig.relay : {}),
1036
+ agentId: activatedAgentId,
1037
+ },
1038
+ });
1039
+ resolvedAgentId = activatedAgentId;
1040
+ }
1041
+ const baseUrl = normalizeRelayHttpBaseUrl(runtimeConfig.serverUrl);
1042
+ const result = await fetchJson(fetchImpl, `${baseUrl}/v1/profile/public-identity`, {
1043
+ method: 'PUT',
1044
+ headers: {
1045
+ 'content-type': 'application/json',
1046
+ ...(resolvedRuntimeConfig.apiKey ? { 'x-api-key': resolvedRuntimeConfig.apiKey } : {}),
1047
+ ...buildRuntimeAuthHeaders(resolvedRuntimeConfig),
1048
+ },
1049
+ body: JSON.stringify({
1050
+ ...(resolvedAgentId ? { agentId: resolvedAgentId } : {}),
1051
+ displayName: normalizedDisplayName,
1052
+ }),
1053
+ });
1054
+ if (!result.ok) {
1055
+ createRelayRouteError({
1056
+ result,
1057
+ runtimeConfig: resolvedRuntimeConfig,
1058
+ code: 'public_identity_update_failed',
1059
+ publicMessage: 'failed to update public identity',
1060
+ context: {
1061
+ agentId: resolvedAgentId,
1062
+ },
1063
+ });
1064
+ }
1065
+ return {
1066
+ ...(result.body || {}),
1067
+ runtimeActivation: !resolveRuntimeAppToken(runtimeConfig)
1068
+ ? {
1069
+ status: 'activated',
1070
+ agentId: resolvedAgentId,
1071
+ }
1072
+ : null,
1073
+ runtimeConfig: resolvedRuntimeConfig,
1074
+ };
1075
+ }
1076
+
973
1077
  async function registerRelayBinding({ runtimeConfig, fetchImpl, logger }) {
974
1078
  if (typeof fetchImpl !== 'function') {
975
1079
  throw new Error('fetch is unavailable for relay registration');
@@ -985,7 +1089,6 @@ async function registerRelayBinding({ runtimeConfig, fetchImpl, logger }) {
985
1089
  ...(runtimeConfig.apiKey ? { 'x-api-key': runtimeConfig.apiKey } : {}),
986
1090
  }),
987
1091
  body: JSON.stringify({
988
- ...(registration.enabled ? { agentCode: registration.agentCode } : {}),
989
1092
  ...(registration.displayName ? { displayName: registration.displayName } : {}),
990
1093
  }),
991
1094
  });
@@ -1007,11 +1110,7 @@ async function registerRelayBinding({ runtimeConfig, fetchImpl, logger }) {
1007
1110
  : registerResult.status >= 500
1008
1111
  ? 'transport'
1009
1112
  : 'config';
1010
- const publicMessage = errorCode === 'agent_code_conflict'
1011
- ? 'relay agent code already exists; reuse its appToken or choose a different registration.agentCode'
1012
- : errorCode === 'agent_code_mismatch'
1013
- ? 'configured relay agent code does not match the provided appToken'
1014
- : 'relay registration failed';
1113
+ const publicMessage = 'relay registration failed';
1015
1114
 
1016
1115
  throw createRuntimeBoundaryError({
1017
1116
  code: errorCode,
@@ -1022,14 +1121,7 @@ async function registerRelayBinding({ runtimeConfig, fetchImpl, logger }) {
1022
1121
  recoverable: registerResult.status >= 500,
1023
1122
  context: {
1024
1123
  accountId: runtimeConfig.accountId || null,
1025
- requestedAgentCode: normalizeClaworldText(
1026
- registerResult.body?.requestedAgentCode,
1027
- registration.agentCode || null,
1028
- ),
1029
- authenticatedAgentCode: normalizeClaworldText(registerResult.body?.authenticatedAgentCode, null),
1030
- authenticatedAddress: normalizeClaworldText(registerResult.body?.authenticatedAddress, null),
1031
1124
  conflictingAgentId: normalizeClaworldText(registerResult.body?.agent?.agentId, null),
1032
- conflictingAddress: normalizeClaworldText(registerResult.body?.agent?.address, null),
1033
1125
  appTokenConfigured: Boolean(resolveRuntimeAppToken(runtimeConfig)),
1034
1126
  },
1035
1127
  });
@@ -1085,67 +1177,30 @@ async function resolveRelayAgentSummary({
1085
1177
  fetchImpl,
1086
1178
  logger,
1087
1179
  agentId = null,
1088
- agentCode = null,
1089
- address = null,
1090
1180
  }) {
1091
- const requestedIdentity = normalizeRelayIdentityInput({
1092
- runtimeConfig,
1093
- agentId,
1094
- agentCode,
1095
- address,
1096
- });
1181
+ const normalizedAgentId = normalizeClaworldText(agentId, null);
1097
1182
 
1098
1183
  try {
1099
1184
  const items = await fetchRelayAgents({ runtimeConfig, fetchImpl, logger });
1100
1185
  const match = items
1101
- .map((item) => buildRelayAgentSummary(item, runtimeConfig))
1102
- .find((item) => {
1103
- if (requestedIdentity.agentId && item.agentId === requestedIdentity.agentId) return true;
1104
- if (requestedIdentity.agentCode && item.agentCode === requestedIdentity.agentCode) return true;
1105
- if (requestedIdentity.address && item.address === requestedIdentity.address) return true;
1106
- if (requestedIdentity.relayLocalCode && item.relayLocalCode === requestedIdentity.relayLocalCode) return true;
1107
- return false;
1108
- }) || null;
1186
+ .map((item) => buildRelayAgentSummary(item))
1187
+ .find((item) => normalizedAgentId && item.agentId === normalizedAgentId) || null;
1109
1188
 
1110
1189
  if (match) {
1111
1190
  return {
1112
1191
  ...match,
1113
1192
  resolved: true,
1114
- resolutionSource: requestedIdentity.agentId && match.agentId === requestedIdentity.agentId
1115
- ? 'agentId'
1116
- : requestedIdentity.agentCode && match.agentCode === requestedIdentity.agentCode
1117
- ? 'agentCode'
1118
- : requestedIdentity.address && match.address === requestedIdentity.address
1119
- ? 'address'
1120
- : 'relayLocalCode',
1193
+ resolutionSource: 'agentId',
1121
1194
  };
1122
1195
  }
1123
1196
  } catch {
1124
1197
  // Fallback below keeps pairing/send tools usable even when lookup fails.
1125
1198
  }
1126
1199
 
1127
- const canInferFallbackAgentCode = Boolean(
1128
- requestedIdentity.agentCode
1129
- || requestedIdentity.address
1130
- || requestedIdentity.relayLocalCode,
1131
- );
1132
- const fallbackAgentCode = requestedIdentity.agentCode
1133
- || (canInferFallbackAgentCode ? buildCanonicalRelayAgentCode(runtimeConfig.localAgent?.agentCode, runtimeConfig) : null)
1134
- || (canInferFallbackAgentCode ? buildCanonicalRelayAgentCode(runtimeConfig.accountId, runtimeConfig) : null);
1135
- const parsedFallbackAgentCode = parseCanonicalRelayAgentCode(fallbackAgentCode);
1136
-
1137
1200
  return {
1138
- agentId: requestedIdentity.agentId,
1139
- agentCode: fallbackAgentCode,
1140
- relayLocalCode: requestedIdentity.relayLocalCode
1141
- || parsedFallbackAgentCode?.relayLocalCode
1142
- || normalizeRelayLocalAgentCode(runtimeConfig.localAgent?.agentCode, null)
1143
- || null,
1144
- domain: requestedIdentity.domain
1145
- || parsedFallbackAgentCode?.domain
1146
- || normalizeRelayDomainName(inferRelayDomain(runtimeConfig), null),
1147
- address: requestedIdentity.address || fallbackAgentCode || null,
1148
- displayName: normalizeClaworldText(runtimeConfig.localAgent?.displayName, null),
1201
+ agentId: normalizedAgentId,
1202
+ displayName: normalizeClaworldText(runtimeConfig.registration?.displayName, normalizeClaworldText(runtimeConfig.localAgent?.displayName, null)),
1203
+ publicIdentity: null,
1149
1204
  discoverable: null,
1150
1205
  contactable: null,
1151
1206
  online: null,
@@ -1158,10 +1213,6 @@ async function ensureAgentPairing({ runtimeConfig, fetchImpl, logger }) {
1158
1213
  const binding = await ensureRelayBinding({ runtimeConfig, fetchImpl, logger });
1159
1214
  const pairedRuntimeConfig = binding.runtimeConfig;
1160
1215
  const relayAgentId = normalizeClaworldText(pairedRuntimeConfig.relay?.agentId, null);
1161
- const relayAgentCode = normalizeRelayLocalAgentCode(
1162
- pairedRuntimeConfig.localAgent?.agentCode,
1163
- normalizeRelayLocalAgentCode(pairedRuntimeConfig.accountId, null),
1164
- );
1165
1216
 
1166
1217
  if (!relayAgentId) {
1167
1218
  return {
@@ -1174,7 +1225,6 @@ async function ensureAgentPairing({ runtimeConfig, fetchImpl, logger }) {
1174
1225
  fetchImpl,
1175
1226
  logger,
1176
1227
  agentId: relayAgentId,
1177
- agentCode: relayAgentCode,
1178
1228
  }),
1179
1229
  };
1180
1230
  }
@@ -1184,14 +1234,13 @@ async function ensureAgentPairing({ runtimeConfig, fetchImpl, logger }) {
1184
1234
  reason: null,
1185
1235
  bindingSource: binding.bindingSource,
1186
1236
  runtimeConfig: pairedRuntimeConfig,
1187
- relayAgent: await resolveRelayAgentSummary({
1188
- runtimeConfig: pairedRuntimeConfig,
1189
- fetchImpl,
1190
- logger,
1191
- agentId: relayAgentId,
1192
- agentCode: relayAgentCode,
1193
- }),
1194
- };
1237
+ relayAgent: await resolveRelayAgentSummary({
1238
+ runtimeConfig: pairedRuntimeConfig,
1239
+ fetchImpl,
1240
+ logger,
1241
+ agentId: relayAgentId,
1242
+ }),
1243
+ };
1195
1244
  }
1196
1245
 
1197
1246
  async function fetchPostSetupWorldDirectory({ cfg, accountId, runtimeConfig, limit = null, sort = null, page = null, fetchImpl, logger }) {
@@ -1248,7 +1297,7 @@ async function ensureRelayBinding({ runtimeConfig, fetchImpl, logger }) {
1248
1297
  serverUrl: normalizedRuntimeConfig.serverUrl,
1249
1298
  appTokenConfigured: Boolean(appToken),
1250
1299
  registrationEnabled: registration.enabled,
1251
- agentCode: registration.agentCode || null,
1300
+ displayName: registration.displayName || null,
1252
1301
  hasRelayAgentId: Boolean(normalizedRuntimeConfig.relay?.agentId),
1253
1302
  });
1254
1303
 
@@ -1273,10 +1322,12 @@ function buildDeliveryInboundEnvelope({
1273
1322
  incomingText,
1274
1323
  contextText = null,
1275
1324
  commandText = null,
1325
+ timestamp = null,
1276
1326
  deliveryId,
1277
1327
  sessionKey,
1278
1328
  worldId = null,
1279
1329
  conversationKey = null,
1330
+ untrustedContext = [],
1280
1331
  }) {
1281
1332
  const envelopeOptions = runtime?.channel?.reply?.resolveEnvelopeFormatOptions
1282
1333
  ? runtime.channel.reply.resolveEnvelopeFormatOptions(currentCfg)
@@ -1286,37 +1337,43 @@ function buildDeliveryInboundEnvelope({
1286
1337
  String(incomingText || '').trim(),
1287
1338
  ].filter(Boolean).join('\n\n');
1288
1339
  const remoteLabel = String(remoteIdentity || 'unknown-peer').trim() || 'unknown-peer';
1289
- const systemLines = [
1340
+ const rawBody = String(incomingText || '').trim();
1341
+ const normalizedCommandText = String(commandText || '').trim();
1342
+ const commandBody = normalizedCommandText || rawBody;
1343
+ const bodyForAgent = bodyText || rawBody;
1344
+ const contextLines = mergeUntrustedContextLines([
1290
1345
  `[claworld peer ${remoteLabel}]`,
1291
1346
  ...(worldId ? [`[claworld world ${worldId}]`] : []),
1292
1347
  ...(conversationKey ? [`[claworld conversation ${conversationKey}]`] : []),
1293
1348
  `[claworld session ${sessionKey}]`,
1294
1349
  `[claworld delivery ${deliveryId}]`,
1295
- ];
1296
- const rawBody = `${bodyText}\n\n${systemLines.join('\n')}`;
1297
- const normalizedCommandText = String(commandText || '').trim();
1298
- const commandBody = normalizedCommandText || rawBody;
1350
+ ], untrustedContext);
1351
+ const envelopeTimestamp = Number.isFinite(timestamp) ? new Date(timestamp) : new Date();
1299
1352
 
1300
1353
  if (runtime?.channel?.reply?.formatAgentEnvelope) {
1301
1354
  return {
1302
1355
  Body: runtime.channel.reply.formatAgentEnvelope({
1303
1356
  channel: 'Claworld',
1304
1357
  from: remoteLabel,
1305
- timestamp: new Date(),
1358
+ timestamp: envelopeTimestamp,
1306
1359
  envelope: envelopeOptions,
1307
- body: rawBody,
1360
+ body: bodyForAgent,
1308
1361
  }),
1309
1362
  RawBody: rawBody,
1310
1363
  CommandBody: commandBody,
1311
- ContextLines: systemLines,
1364
+ BodyForAgent: bodyForAgent,
1365
+ BodyForCommands: commandBody,
1366
+ UntrustedContext: contextLines,
1312
1367
  };
1313
1368
  }
1314
1369
 
1315
1370
  return {
1316
- Body: `${remoteLabel}: ${rawBody}`,
1371
+ Body: `${remoteLabel}: ${bodyForAgent}`,
1317
1372
  RawBody: rawBody,
1318
1373
  CommandBody: commandBody,
1319
- ContextLines: systemLines,
1374
+ BodyForAgent: bodyForAgent,
1375
+ BodyForCommands: commandBody,
1376
+ UntrustedContext: contextLines,
1320
1377
  };
1321
1378
  }
1322
1379
 
@@ -1678,15 +1735,14 @@ async function maybeBridgeRuntimeDelivery({
1678
1735
  : {};
1679
1736
  const deliveryId = resolveNormalizedText(delivery.deliveryId, null);
1680
1737
  const sessionKey = resolveNormalizedText(delivery.sessionKey, null);
1681
- const incomingText = resolveNormalizedText(payload.text, null);
1682
- const contextText = null;
1738
+ const contextText = resolveNormalizedText(payload.contextText, null);
1739
+ const incomingText = resolveNormalizedText(
1740
+ payload.commandText,
1741
+ contextText ? null : resolveNormalizedText(payload.text, null),
1742
+ );
1743
+ const commandText = resolveNormalizedText(payload.commandText, incomingText);
1683
1744
  const fromAgentId = resolveNormalizedText(metadata.fromAgentId, null);
1684
- const remoteAddress = resolveNormalizedText(metadata.fromAddress, null)
1685
- || resolveRelayAddress(fromAgentId || '', runtimeConfig)
1686
- || fromAgentId
1687
- || 'unknown-peer';
1688
- const remoteRouteIdentity = resolveRelayAddress(fromAgentId || '', runtimeConfig)
1689
- || remoteAddress;
1745
+ const remoteIdentity = fromAgentId || 'unknown-peer';
1690
1746
 
1691
1747
  if (
1692
1748
  !runtime?.channel?.reply?.finalizeInboundContext
@@ -1699,11 +1755,12 @@ async function maybeBridgeRuntimeDelivery({
1699
1755
  });
1700
1756
  return { skipped: true, reason: 'missing_runtime_bridge_hooks' };
1701
1757
  }
1702
- if (!deliveryId || !sessionKey || !incomingText) {
1758
+ if (!deliveryId || !sessionKey || (!incomingText && !contextText)) {
1703
1759
  logger.warn?.(`[claworld:${runtimeAccountId}] skipping delivery bridge: missing delivery payload`, {
1704
1760
  deliveryId,
1705
1761
  sessionKey,
1706
1762
  hasIncomingText: Boolean(incomingText),
1763
+ hasContextText: Boolean(contextText),
1707
1764
  });
1708
1765
  return { skipped: true, reason: 'missing_delivery_payload' };
1709
1766
  }
@@ -1716,48 +1773,51 @@ async function maybeBridgeRuntimeDelivery({
1716
1773
  const worldId = resolveDeliveryWorldId(delivery);
1717
1774
  const commandAuthorized = shouldAuthorizeBridgedCommand({
1718
1775
  runtimeConfig,
1719
- incomingText,
1776
+ incomingText: commandText || incomingText,
1720
1777
  });
1721
- const { Body, RawBody, CommandBody, ContextLines } = buildDeliveryInboundEnvelope({
1778
+ const inboundTimestamp = resolveBridgeDeliveryTimestampMs({ delivery, metadata });
1779
+ const { Body, RawBody, CommandBody, BodyForAgent, BodyForCommands, UntrustedContext } = buildDeliveryInboundEnvelope({
1722
1780
  runtime,
1723
1781
  currentCfg,
1724
- remoteIdentity: remoteAddress,
1782
+ remoteIdentity,
1725
1783
  incomingText,
1726
1784
  contextText,
1727
- commandText: incomingText,
1785
+ commandText,
1786
+ timestamp: inboundTimestamp,
1728
1787
  deliveryId,
1729
1788
  sessionKey,
1730
1789
  worldId,
1731
1790
  conversationKey: metadata.conversationKey || null,
1791
+ untrustedContext: payload.untrustedContext,
1732
1792
  });
1733
- const localAddress = resolveRelayAddress(runtimeConfig.accountId || '', runtimeConfig);
1793
+ const localIdentity = normalizeClaworldText(runtimeConfig.relay?.agentId, runtimeConfig.accountId);
1734
1794
  const inboundCtx = runtime.channel.reply.finalizeInboundContext({
1735
1795
  Body,
1736
1796
  RawBody,
1737
1797
  CommandBody,
1738
- BodyForAgent: RawBody,
1739
- BodyForCommands: CommandBody,
1740
- From: `claworld:${remoteRouteIdentity}`,
1741
- To: localAddress ? `claworld:${localAddress}` : `claworld:${runtimeConfig.accountId}`,
1798
+ BodyForAgent,
1799
+ BodyForCommands,
1800
+ From: `claworld:${remoteIdentity}`,
1801
+ To: `claworld:${localIdentity}`,
1742
1802
  SessionKey: sessionKey,
1743
1803
  AccountId: runtimeConfig.accountId,
1744
1804
  OriginatingChannel: 'claworld',
1745
- OriginatingFrom: remoteRouteIdentity,
1746
- OriginatingTo: remoteRouteIdentity,
1805
+ OriginatingFrom: remoteIdentity,
1806
+ OriginatingTo: remoteIdentity,
1747
1807
  ChatType: 'direct',
1748
- SenderName: remoteAddress,
1749
- SenderId: remoteRouteIdentity,
1808
+ SenderName: remoteIdentity,
1809
+ SenderId: remoteIdentity,
1750
1810
  MessageId: deliveryId,
1751
1811
  Provider: 'claworld',
1752
1812
  Surface: 'claworld',
1753
- ConversationLabel: remoteAddress,
1754
- Timestamp: Date.now(),
1813
+ ConversationLabel: remoteIdentity,
1814
+ Timestamp: inboundTimestamp,
1755
1815
  MessageSid: deliveryId,
1756
1816
  WasMentioned: false,
1757
1817
  CommandAuthorized: commandAuthorized,
1758
1818
  RelayDeliveryId: deliveryId,
1759
1819
  RelayFromAgentId: fromAgentId,
1760
- UntrustedContext: ContextLines,
1820
+ UntrustedContext,
1761
1821
  });
1762
1822
  const localAgentId = resolveBoundLocalAgentId({
1763
1823
  cfg: currentCfg,
@@ -1788,7 +1848,7 @@ async function maybeBridgeRuntimeDelivery({
1788
1848
  deliveryId,
1789
1849
  sessionKey,
1790
1850
  localAgentId,
1791
- remoteIdentity: remoteAddress,
1851
+ remoteIdentity,
1792
1852
  routeStatus: routed?.status || null,
1793
1853
  bodyPreview: String(Body || '').slice(0, 240),
1794
1854
  rawBodyPreview: String(RawBody || '').slice(0, 240),
@@ -1796,6 +1856,23 @@ async function maybeBridgeRuntimeDelivery({
1796
1856
  commandAuthorized,
1797
1857
  });
1798
1858
 
1859
+ try {
1860
+ if (typeof relayClient?.sendAcceptedAndWaitForAck === 'function') {
1861
+ await relayClient.sendAcceptedAndWaitForAck({
1862
+ deliveryId,
1863
+ sessionKey,
1864
+ source: 'runtime_dispatch',
1865
+ });
1866
+ }
1867
+ } catch (error) {
1868
+ logger.warn?.(`[claworld:${runtimeAccountId}] delivery acceptance acknowledgement failed`, {
1869
+ deliveryId,
1870
+ sessionKey,
1871
+ localAgentId,
1872
+ error: error?.message || String(error),
1873
+ });
1874
+ }
1875
+
1799
1876
  let {
1800
1877
  dispatchResult,
1801
1878
  replied,
@@ -1910,6 +1987,18 @@ export function createClaworldChannelPlugin({
1910
1987
  });
1911
1988
  }
1912
1989
 
1990
+ function rememberAccountBinding({ runtimeConfig, accountId = null, bindingSource = 'binding_cache', relayAgent = null }) {
1991
+ const normalizedRuntimeConfig = applyRuntimeIdentity(runtimeConfig);
1992
+ const accountKey = resolveAccountBindingKey(normalizedRuntimeConfig, accountId || null);
1993
+ accountBindingStates.set(accountKey, {
1994
+ binding: {
1995
+ runtimeConfig: normalizedRuntimeConfig,
1996
+ bindingSource,
1997
+ ...(relayAgent ? { relayAgent } : {}),
1998
+ },
1999
+ });
2000
+ }
2001
+
1913
2002
  async function ensureAccountRelayBinding({ runtimeConfig, accountId = null }) {
1914
2003
  const normalizedRuntimeConfig = applyRuntimeIdentity(runtimeConfig);
1915
2004
  const accountKey = resolveAccountBindingKey(normalizedRuntimeConfig, accountId || null);
@@ -1957,7 +2046,7 @@ export function createClaworldChannelPlugin({
1957
2046
  return promise;
1958
2047
  }
1959
2048
 
1960
- async function persistRuntimeAppToken({ runtime, accountId, appToken }) {
2049
+ async function persistRuntimeAppToken({ runtime, accountId, appToken, relayAgentId = null }) {
1961
2050
  if (!runtime?.config?.loadConfig || !runtime?.config?.writeConfigFile) {
1962
2051
  return { skipped: true, reason: 'missing_runtime_config_io' };
1963
2052
  }
@@ -1980,43 +2069,67 @@ export function createClaworldChannelPlugin({
1980
2069
  ? accounts[accountId]
1981
2070
  : {};
1982
2071
 
1983
- if (normalizeClaworldText(account.appToken, null) === appToken) {
2072
+ const normalizedRelayAgentId = normalizeClaworldText(relayAgentId, normalizeClaworldText(account?.relay?.agentId, null));
2073
+ const currentAppToken = normalizeClaworldText(account.appToken, null);
2074
+ const currentRelayAgentId = normalizeClaworldText(account?.relay?.agentId, null);
2075
+ if (currentAppToken === appToken && currentRelayAgentId === normalizedRelayAgentId) {
1984
2076
  return { skipped: true, reason: 'already_persisted' };
1985
2077
  }
1986
2078
 
1987
2079
  accounts[accountId] = {
1988
2080
  ...account,
1989
2081
  appToken,
2082
+ relay: {
2083
+ ...(account?.relay && typeof account.relay === 'object' && !Array.isArray(account.relay) ? account.relay : {}),
2084
+ ...(normalizedRelayAgentId ? { agentId: normalizedRelayAgentId } : {}),
2085
+ },
1990
2086
  };
2087
+ delete accounts[accountId].registration;
1991
2088
  claworldRoot.accounts = accounts;
1992
2089
  nextCfg.channels.claworld = claworldRoot;
1993
2090
  await runtime.config.writeConfigFile(nextCfg);
1994
2091
  return { skipped: false, ok: true };
1995
2092
  }
1996
2093
 
1997
- async function resolveBoundRuntimeContext(context = {}) {
2094
+ function resolveConfiguredRuntimeContext(context = {}) {
1998
2095
  const cfg = context.cfg || {};
1999
2096
  const accountId = context.accountId || null;
2000
2097
  const runtimeContext = accountRuntimeContexts.get(accountId || 'default') || null;
2001
- let runtimeConfig = runtimeContext?.runtimeConfig || context.runtimeConfig || resolveClaworldRuntimeConfig(cfg, accountId);
2098
+ const runtimeConfig = runtimeContext?.runtimeConfig || context.runtimeConfig || resolveClaworldRuntimeConfig(cfg, accountId);
2099
+ return {
2100
+ ...context,
2101
+ cfg: runtimeContext?.cfg || cfg,
2102
+ accountId: runtimeConfig.accountId || accountId || null,
2103
+ runtimeConfig,
2104
+ agentId: context.agentId || runtimeConfig.relay?.agentId || null,
2105
+ bindingSource: runtimeContext?.deferredFailure ? 'runtime_context_deferred' : 'runtime_context',
2106
+ };
2107
+ }
2108
+
2109
+ async function resolveBoundRuntimeContext(context = {}) {
2110
+ const configuredContext = resolveConfiguredRuntimeContext(context);
2111
+ const cfg = configuredContext.cfg || {};
2112
+ const accountId = configuredContext.accountId || null;
2113
+ let runtimeConfig = configuredContext.runtimeConfig;
2114
+ const runtimeContext = accountRuntimeContexts.get(accountId || 'default') || null;
2002
2115
  if (runtimeContext?.runtimeConfig && !runtimeContext?.deferredFailure) {
2003
2116
  return {
2004
- ...context,
2117
+ ...configuredContext,
2005
2118
  cfg: runtimeContext.cfg || cfg,
2006
2119
  accountId: runtimeConfig.accountId || accountId || null,
2007
2120
  runtimeConfig,
2008
- agentId: context.agentId || runtimeConfig.relay?.agentId || null,
2121
+ agentId: configuredContext.agentId || runtimeConfig.relay?.agentId || null,
2009
2122
  bindingSource: 'runtime_context',
2010
2123
  };
2011
2124
  }
2012
2125
  const binding = await ensureAccountRelayBinding({ runtimeConfig, accountId });
2013
2126
  runtimeConfig = binding.runtimeConfig;
2014
2127
  return {
2015
- ...context,
2128
+ ...configuredContext,
2016
2129
  cfg,
2017
2130
  accountId: runtimeConfig.accountId || accountId || null,
2018
2131
  runtimeConfig,
2019
- agentId: context.agentId || runtimeConfig.relay?.agentId || null,
2132
+ agentId: configuredContext.agentId || runtimeConfig.relay?.agentId || null,
2020
2133
  bindingSource: binding.bindingSource,
2021
2134
  };
2022
2135
  }
@@ -2137,6 +2250,7 @@ export function createClaworldChannelPlugin({
2137
2250
  runtime: pluginRuntime,
2138
2251
  accountId: runtimeConfig.accountId,
2139
2252
  appToken: resolveRuntimeAppToken(runtimeConfig),
2253
+ relayAgentId: runtimeConfig.relay?.agentId || null,
2140
2254
  });
2141
2255
  if (!persisted.skipped) {
2142
2256
  logger.info?.(`[claworld:${runtimeAccountId}] persisted runtime appToken`, {
@@ -2324,6 +2438,77 @@ export function createClaworldChannelPlugin({
2324
2438
  };
2325
2439
  }
2326
2440
 
2441
+ async function getRuntimePublicIdentity(context = {}) {
2442
+ const resolvedContext = resolveConfiguredRuntimeContext(context);
2443
+ return fetchPublicIdentity({
2444
+ runtimeConfig: resolvedContext.runtimeConfig,
2445
+ agentId: resolvedContext.agentId || null,
2446
+ fetchImpl,
2447
+ });
2448
+ }
2449
+
2450
+ async function updateRuntimePublicIdentity(context = {}) {
2451
+ const resolvedContext = resolveConfiguredRuntimeContext(context);
2452
+ const updateResult = await updatePublicIdentity({
2453
+ runtimeConfig: resolvedContext.runtimeConfig,
2454
+ agentId: resolvedContext.agentId || null,
2455
+ displayName: context.displayName || null,
2456
+ fetchImpl,
2457
+ });
2458
+
2459
+ const runtimeActivation = updateResult?.runtimeActivation && typeof updateResult.runtimeActivation === 'object'
2460
+ ? updateResult.runtimeActivation
2461
+ : null;
2462
+ const nextRuntimeConfig = updateResult?.runtimeConfig && typeof updateResult.runtimeConfig === 'object'
2463
+ ? updateResult.runtimeConfig
2464
+ : resolvedContext.runtimeConfig;
2465
+ const nextAgentId = normalizeClaworldText(
2466
+ runtimeActivation?.agentId,
2467
+ normalizeClaworldText(resolvedContext.agentId, normalizeClaworldText(nextRuntimeConfig?.relay?.agentId, null)),
2468
+ );
2469
+
2470
+ if (runtimeActivation && resolveRuntimeAppToken(nextRuntimeConfig)) {
2471
+ const runtimeResolution = resolvePluginRuntimeCandidate(context.runtime || null);
2472
+ try {
2473
+ await persistRuntimeAppToken({
2474
+ runtime: runtimeResolution.runtime,
2475
+ accountId: resolvedContext.accountId || nextRuntimeConfig.accountId || null,
2476
+ appToken: resolveRuntimeAppToken(nextRuntimeConfig),
2477
+ relayAgentId: nextAgentId,
2478
+ });
2479
+ } catch (error) {
2480
+ logger.warn?.('[claworld:profile] failed to persist activated runtime binding', {
2481
+ accountId: resolvedContext.accountId || nextRuntimeConfig.accountId || null,
2482
+ error: error?.message || String(error),
2483
+ });
2484
+ }
2485
+
2486
+ rememberAccountBinding({
2487
+ runtimeConfig: nextRuntimeConfig,
2488
+ accountId: resolvedContext.accountId || nextRuntimeConfig.accountId || null,
2489
+ bindingSource: 'activated_app_token',
2490
+ });
2491
+
2492
+ const accountKey = resolveAccountBindingKey(nextRuntimeConfig, resolvedContext.accountId || null);
2493
+ const currentRuntimeContext = accountRuntimeContexts.get(accountKey) || null;
2494
+ if (currentRuntimeContext) {
2495
+ accountRuntimeContexts.set(accountKey, {
2496
+ ...currentRuntimeContext,
2497
+ runtimeConfig: nextRuntimeConfig,
2498
+ deferredFailure: null,
2499
+ deferredErrorMessage: null,
2500
+ });
2501
+ }
2502
+ }
2503
+
2504
+ const payload = updateResult && typeof updateResult === 'object' && !Array.isArray(updateResult)
2505
+ ? { ...updateResult }
2506
+ : {};
2507
+ delete payload.runtimeActivation;
2508
+ delete payload.runtimeConfig;
2509
+ return payload;
2510
+ }
2511
+
2327
2512
  return {
2328
2513
  id: 'claworld',
2329
2514
  meta: {
@@ -2349,9 +2534,9 @@ export function createClaworldChannelPlugin({
2349
2534
  },
2350
2535
  agentPrompt: {
2351
2536
  messageToolHints: () => [
2352
- '- Claworld targets are canonical identity handles like `alice@robin`.',
2537
+ '- Claworld message targets are canonical `agentId` values such as `agt_xxx`.',
2353
2538
  '- Omit `target` to keep replying inside the current A2A session when the runtime already inferred the peer.',
2354
- '- Use explicit targets like `user:alice@robin` when you want to open a new relay session to another remote OpenClaw agent.',
2539
+ '- Resolve public identity like `displayName#code` to `agentId` before opening a new relay session to another agent.',
2355
2540
  ],
2356
2541
  },
2357
2542
  reload: { configPrefixes: ['channels.claworld'] },
@@ -2383,19 +2568,18 @@ export function createClaworldChannelPlugin({
2383
2568
  looksLikeId: (raw, normalized) => {
2384
2569
  const value = String(normalized || raw || '').trim();
2385
2570
  if (!value) return false;
2386
- if (value.includes('@')) return RELAY_CANONICAL_AGENT_CODE_PATTERN.test(value);
2387
- return /^[a-z0-9._:+~-]+$/i.test(value) || /^agt_[a-z0-9_-]+$/i.test(value);
2571
+ return /^agt_[a-z0-9_-]+$/i.test(value);
2388
2572
  },
2389
- hint: '<agent@relay-domain|agentId>',
2573
+ hint: '<agentId>',
2390
2574
  },
2391
2575
  },
2392
2576
  directory: {
2393
2577
  self: async ({ cfg, accountId } = {}) => {
2394
2578
  const account = inspectClaworldChannelAccount(cfg || {}, accountId || null);
2395
- const address = resolveRelayAddress(account?.accountId || '', account || {});
2396
- if (!account?.configured || !address) return null;
2579
+ const agentId = normalizeClaworldText(account?.relay?.agentId, null);
2580
+ if (!account?.configured || !agentId) return null;
2397
2581
  return {
2398
- id: address,
2582
+ id: agentId,
2399
2583
  name: account.accountId,
2400
2584
  handle: account.accountId,
2401
2585
  };
@@ -2478,8 +2662,6 @@ export function createClaworldChannelPlugin({
2478
2662
  fetchImpl,
2479
2663
  logger,
2480
2664
  agentId: context.agentId || context.targetAgentId || null,
2481
- agentCode: context.agentCode || context.targetAgentCode || null,
2482
- address: context.address || null,
2483
2665
  }),
2484
2666
  },
2485
2667
  social: {
@@ -2488,7 +2670,7 @@ export function createClaworldChannelPlugin({
2488
2670
  return createFriendRequest({
2489
2671
  runtimeConfig: resolvedContext.runtimeConfig,
2490
2672
  fromAgentId: resolvedContext.agentId || null,
2491
- toAddress: context.toAddress || null,
2673
+ targetAgentId: context.targetAgentId || null,
2492
2674
  message: context.message || null,
2493
2675
  metadata: context.metadata || {},
2494
2676
  fetchImpl,
@@ -2578,6 +2760,10 @@ export function createClaworldChannelPlugin({
2578
2760
  });
2579
2761
  },
2580
2762
  },
2763
+ profile: {
2764
+ getPublicIdentity: getRuntimePublicIdentity,
2765
+ updatePublicIdentity: updateRuntimePublicIdentity,
2766
+ },
2581
2767
  postSetup: {
2582
2768
  fetchWorldDirectory: async (context = {}) => {
2583
2769
  const resolvedContext = await resolveBoundRuntimeContext(context);
@@ -2686,7 +2872,7 @@ export function createClaworldChannelPlugin({
2686
2872
  agentId: resolvedContext.agentId || null,
2687
2873
  displayName: context.displayName || null,
2688
2874
  worldContextText: context.worldContextText || null,
2689
- enabled: context.enabled === true,
2875
+ enabled: typeof context.enabled === 'boolean' ? context.enabled : true,
2690
2876
  fetchImpl,
2691
2877
  logger,
2692
2878
  });
@@ -2728,6 +2914,10 @@ export function createClaworldChannelPlugin({
2728
2914
  results,
2729
2915
  demo,
2730
2916
  productShell: {
2917
+ profile: {
2918
+ getPublicIdentity: getRuntimePublicIdentity,
2919
+ updatePublicIdentity: updateRuntimePublicIdentity,
2920
+ },
2731
2921
  fetchWorldDirectory: async (context = {}) => {
2732
2922
  const resolvedContext = await resolveBoundRuntimeContext(context);
2733
2923
  return fetchPostSetupWorldDirectory({
@@ -2868,14 +3058,14 @@ export function createClaworldChannelPlugin({
2868
3058
  moderation: {
2869
3059
  createWorld: async (context = {}) => {
2870
3060
  const resolvedContext = await resolveBoundRuntimeContext(context);
2871
- return createModeratedWorld({
3061
+ return createModeratedWorld({
2872
3062
  cfg: resolvedContext.cfg || {},
2873
3063
  accountId: resolvedContext.accountId || null,
2874
3064
  runtimeConfig: resolvedContext.runtimeConfig || null,
2875
3065
  agentId: resolvedContext.agentId || null,
2876
3066
  displayName: context.displayName || null,
2877
3067
  worldContextText: context.worldContextText || null,
2878
- enabled: context.enabled === true,
3068
+ enabled: typeof context.enabled === 'boolean' ? context.enabled : true,
2879
3069
  fetchImpl,
2880
3070
  logger,
2881
3071
  });