@xfxstudio/claworld 0.2.5 → 0.2.7

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 (29) hide show
  1. package/openclaw.plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/skills/claworld-help/SKILL.md +2 -2
  4. package/skills/claworld-join-and-chat/SKILL.md +18 -9
  5. package/src/lib/chat-request.js +19 -0
  6. package/src/lib/relay/kickoff-text.js +6 -1
  7. package/src/openclaw/installer/core.js +16 -2
  8. package/src/openclaw/plugin/claworld-channel-plugin.js +164 -12
  9. package/src/openclaw/plugin/config-schema.js +9 -1
  10. package/src/openclaw/plugin/register.js +151 -15
  11. package/src/openclaw/plugin/relay-client.js +502 -1
  12. package/src/openclaw/runtime/demo-session-bootstrap.js +1 -2
  13. package/src/openclaw/runtime/tool-contracts.js +40 -1
  14. package/src/openclaw/runtime/tool-inventory.js +3 -3
  15. package/src/openclaw/runtime/world-moderation-helper.js +9 -13
  16. package/src/product-shell/catalog/default-world-catalog.js +12 -258
  17. package/src/product-shell/contracts/world-manifest.js +0 -38
  18. package/src/product-shell/contracts/world-orchestration.js +0 -6
  19. package/src/product-shell/index.js +1 -5
  20. package/src/product-shell/membership/membership-service.js +24 -6
  21. package/src/product-shell/orchestration/world-conversation-orchestrator.js +0 -2
  22. package/src/product-shell/orchestration/world-conversation-text.js +0 -2
  23. package/src/product-shell/social/chat-request-routes.js +24 -1
  24. package/src/product-shell/social/chat-request-service.js +185 -15
  25. package/src/product-shell/worlds/world-admin-service.js +28 -120
  26. package/src/product-shell/worlds/world-authorization.js +20 -17
  27. package/src/product-shell/worlds/world-broadcast-service.js +2 -5
  28. package/src/product-shell/worlds/world-text.js +0 -2
  29. package/src/product-shell/results/result-service.js +0 -21
@@ -1,4 +1,5 @@
1
1
  import { normalizeAgentProfile, resolveAgentDisplayName, resolveAgentVisibility } from '../../lib/agent-profile.js';
2
+ import { normalizeChatRequestInput } from '../../lib/chat-request.js';
2
3
  import { createKickoffBrief, resolveStoredKickoffBrief } from '../../lib/relay/kickoff-text.js';
3
4
  import { WORLD_ACTIONS } from '../worlds/world-authorization.js';
4
5
  import { normalizeChatRequestApprovalPolicy } from '../contracts/chat-request-approval-policy.js';
@@ -198,6 +199,7 @@ function resolveAcceptNextAction(kickoff) {
198
199
  const kickoffStatus = kickoff?.status || 'skipped';
199
200
  if (kickoffStatus === 'established') return 'runtime_owns_live_conversation';
200
201
  if (kickoffStatus === 'sent') return 'wait_for_sender_opener_delivery';
202
+ if (kickoffStatus === 'kept_silent') return 'sender_kept_silent';
201
203
  if (kickoffStatus === 'failed') return 'backend_kickoff_failed';
202
204
  return 'chat_request_accepted_without_opening_message';
203
205
  }
@@ -280,6 +282,92 @@ function projectChatRequestOrigin(request = {}) {
280
282
  };
281
283
  }
282
284
 
285
+ function hasConversationScope(conversation = null) {
286
+ return Boolean(conversation && typeof conversation === 'object' && !Array.isArray(conversation) && Object.keys(conversation).length > 0);
287
+ }
288
+
289
+ function resolveConversationLinkRequest(requests = [], conversation = {}) {
290
+ const conversationMeta = conversation?.meta && typeof conversation.meta === 'object' && !Array.isArray(conversation.meta)
291
+ ? conversation.meta
292
+ : {};
293
+ const approvalRequestId = normalizeText(conversationMeta.approvalRequestId, null);
294
+ if (approvalRequestId) {
295
+ return requests.find((request) => normalizeText(request.chatRequestId || request.requestId, null) === approvalRequestId) || null;
296
+ }
297
+
298
+ const conversationKey = normalizeText(conversation?.conversationKey, null);
299
+ if (!conversationKey) return null;
300
+
301
+ return requests.find((request) => {
302
+ const kickoff = request?.kickoff && typeof request.kickoff === 'object' && !Array.isArray(request.kickoff)
303
+ ? request.kickoff
304
+ : {};
305
+ return normalizeText(kickoff.conversationKey, null) === conversationKey
306
+ || normalizeText(request?.conversation?.conversationKey, null) === conversationKey;
307
+ }) || null;
308
+ }
309
+
310
+ function resolveInboxChatDirection(viewerAgentId, request = null) {
311
+ if (!request) return null;
312
+ if (viewerAgentId === request.fromAgentId) return 'outbound';
313
+ if (viewerAgentId === request.toAgentId) return 'inbound';
314
+ return 'related';
315
+ }
316
+
317
+ function resolveInboxChatCounterpartyAgentId(viewerAgentId, request = null, conversation = null) {
318
+ if (request) {
319
+ if (viewerAgentId === request.fromAgentId) return request.toAgentId;
320
+ if (viewerAgentId === request.toAgentId) return request.fromAgentId;
321
+ }
322
+
323
+ const participantIds = Array.isArray(conversation?.participantIds) ? conversation.participantIds : [];
324
+ return participantIds.find((participantId) => participantId && participantId !== viewerAgentId) || null;
325
+ }
326
+
327
+ function resolveInboxChatStatus(conversation = {}, request = null) {
328
+ const kickoffStatus = normalizeText(request?.kickoff?.status, null);
329
+ if (kickoffStatus === 'sent') return 'opening';
330
+ if (kickoffStatus === 'kept_silent') return 'silent';
331
+ if (kickoffStatus === 'failed') return 'kickoff_failed';
332
+ const conversationStatus = normalizeText(conversation?.status, 'active');
333
+ if (conversationStatus === 'active') return 'active';
334
+ if (conversationStatus === 'closed') return 'ended';
335
+ return conversationStatus;
336
+ }
337
+
338
+ function resolveInboxConversationWorldSummary(worldService, request = {}, conversation = {}) {
339
+ const requestContext = request?.requestContext && typeof request.requestContext === 'object' && !Array.isArray(request.requestContext)
340
+ ? request.requestContext
341
+ : {};
342
+ const requestConversation = request?.conversation && typeof request.conversation === 'object' && !Array.isArray(request.conversation)
343
+ ? request.conversation
344
+ : {};
345
+ const conversationMeta = conversation?.meta && typeof conversation.meta === 'object' && !Array.isArray(conversation.meta)
346
+ ? conversation.meta
347
+ : {};
348
+ const worldMeta = conversationMeta.world && typeof conversationMeta.world === 'object' && !Array.isArray(conversationMeta.world)
349
+ ? conversationMeta.world
350
+ : {};
351
+ const worldId = normalizeConversationWorldId(requestConversation.worldId)
352
+ || normalizeConversationWorldId(requestContext.conversation?.worldId)
353
+ || normalizeConversationWorldId(worldMeta.worldId)
354
+ || normalizeConversationWorldId(conversationMeta.worldId);
355
+
356
+ if (!worldId) {
357
+ return {
358
+ mode: 'direct',
359
+ worldId: null,
360
+ world: null,
361
+ };
362
+ }
363
+
364
+ return {
365
+ mode: 'world',
366
+ worldId,
367
+ world: projectWorldSummary(worldService, worldId),
368
+ };
369
+ }
370
+
283
371
  export function createChatRequestService({
284
372
  store = null,
285
373
  relay = null,
@@ -349,8 +437,27 @@ export function createChatRequestService({
349
437
  };
350
438
  }
351
439
 
440
+ function projectChatInboxChat(conversation = {}, viewerAgentId = null, request = null) {
441
+ const direction = resolveInboxChatDirection(viewerAgentId, request);
442
+ const counterpartyAgentId = resolveInboxChatCounterpartyAgentId(viewerAgentId, request, conversation);
443
+
444
+ return {
445
+ chatRequestId: normalizeText(request?.chatRequestId || request?.requestId, null),
446
+ status: resolveInboxChatStatus(conversation, request),
447
+ ...(direction ? { direction } : {}),
448
+ createdAt: normalizeText(conversation.createdAt, normalizeText(request?.createdAt, null)),
449
+ updatedAt: normalizeText(conversation.updatedAt, null),
450
+ lastTurnAt: normalizeText(conversation.lastTurnAt, null),
451
+ conversationKey: normalizeText(conversation.conversationKey, null),
452
+ localSessionKey: normalizeText(conversation.sessionKey, null),
453
+ counterparty: projectAgent(assertStore(), counterpartyAgentId, presence),
454
+ conversation: resolveInboxConversationWorldSummary(worldService, request, conversation),
455
+ };
456
+ }
457
+
352
458
  return {
353
459
  projectChatRequest,
460
+ projectChatInboxChat,
354
461
  async syncApprovalPolicy({
355
462
  actorAgentId,
356
463
  credentialId = null,
@@ -388,6 +495,7 @@ export function createChatRequestService({
388
495
  openingMessage = null,
389
496
  openingPayload = null,
390
497
  worldId = null,
498
+ requestContext = null,
391
499
  origin = null,
392
500
  broadcast = null,
393
501
  source = 'chat_request',
@@ -398,29 +506,36 @@ export function createChatRequestService({
398
506
  throw createInvalidChatRequestError('chat_request_target_required', 'chat request target requires targetAgentId');
399
507
  }
400
508
  const targetAgent = requireAgent(normalizedTargetAgentId);
401
- const normalizedWorldId = normalizeConversationWorldId(worldId);
509
+ const normalizedSource = normalizeText(source, 'chat_request');
510
+ const normalizedRequestInput = normalizeChatRequestInput({
511
+ requestContext,
512
+ source: normalizedSource,
513
+ });
514
+ const normalizedWorldId = normalizeConversationWorldId(worldId)
515
+ || normalizeConversationWorldId(normalizedRequestInput.conversation?.worldId);
402
516
  if (!targetAgent?.address) {
403
517
  throw createTargetAddressNotFoundError(normalizedTargetAgentId);
404
518
  }
405
- const normalizedOpeningPayload = cloneJsonObject(openingPayload);
519
+ const normalizedOpeningPayload = cloneJsonObject(openingPayload) || cloneJsonObject(normalizedRequestInput.openingPayload);
406
520
  const normalizedKickoffBrief = createKickoffBrief({
407
521
  text: typeof kickoffBrief === 'object' && kickoffBrief !== null && !Array.isArray(kickoffBrief)
408
522
  ? kickoffBrief.text
409
- : openingMessage,
523
+ : normalizeText(openingMessage, normalizeText(normalizedRequestInput.openingMessage, null)),
410
524
  payload: typeof kickoffBrief === 'object' && kickoffBrief !== null && !Array.isArray(kickoffBrief)
411
525
  ? kickoffBrief.payload
412
526
  : normalizedOpeningPayload,
413
527
  source: typeof kickoffBrief === 'object' && kickoffBrief !== null && !Array.isArray(kickoffBrief)
414
528
  ? kickoffBrief.source
415
- : source === 'world_broadcast'
529
+ : normalizedSource === 'world_broadcast'
416
530
  ? 'world_broadcast_brief'
417
531
  : 'chat_request_brief',
418
532
  });
419
- const normalizedOrigin = normalizeChatRequestOrigin(origin);
420
- const normalizedBroadcast = normalizeChatRequestBroadcastMetadata(broadcast);
533
+ const normalizedOrigin = normalizeChatRequestOrigin(origin) || normalizeChatRequestOrigin(normalizedRequestInput.origin);
534
+ const normalizedBroadcast = normalizeChatRequestBroadcastMetadata(broadcast)
535
+ || normalizeChatRequestBroadcastMetadata(normalizedRequestInput.broadcast);
421
536
  if (normalizedWorldId) {
422
537
  worldService?.requireWorld?.(normalizedWorldId);
423
- if (normalizeText(source, 'chat_request') !== 'world_broadcast') {
538
+ if (normalizedSource !== 'world_broadcast') {
424
539
  const authorization = worldAuthorizationService.evaluateWorldAction({
425
540
  worldId: normalizedWorldId,
426
541
  actorAgentId: fromAgentId,
@@ -444,9 +559,10 @@ export function createChatRequestService({
444
559
  conversation: {
445
560
  ...(normalizedWorldId ? { worldId: normalizedWorldId } : {}),
446
561
  },
562
+ requestContext: cloneJsonObject(requestContext),
447
563
  origin: normalizedOrigin,
448
564
  broadcast: normalizedBroadcast,
449
- source: normalizeText(source, 'chat_request'),
565
+ source: normalizedSource,
450
566
  });
451
567
  if (result.status < 200 || result.status >= 300) {
452
568
  throw createRelayResponseError(result, 'chat_request_create_failed');
@@ -494,23 +610,55 @@ export function createChatRequestService({
494
610
  };
495
611
  },
496
612
 
497
- listChatRequests({ agentId, direction = null } = {}) {
613
+ listChatInbox({ agentId, direction = null } = {}) {
498
614
  requireAgent(agentId);
499
615
  const normalizedDirection = normalizeDirection(direction);
500
- let items = assertStore().listChatRequests ? assertStore().listChatRequests() : [];
501
- items = items.filter((request) => request.status === 'pending');
502
- items = items.filter((request) => request.fromAgentId === agentId || request.toAgentId === agentId);
616
+ let requests = assertStore().listChatRequests ? assertStore().listChatRequests() : [];
617
+ requests = requests.filter((request) => request.fromAgentId === agentId || request.toAgentId === agentId);
503
618
  if (normalizedDirection === 'inbound') {
504
- items = items.filter((request) => request.toAgentId === agentId);
619
+ requests = requests.filter((request) => request.toAgentId === agentId);
505
620
  } else if (normalizedDirection === 'outbound') {
506
- items = items.filter((request) => request.fromAgentId === agentId);
621
+ requests = requests.filter((request) => request.fromAgentId === agentId);
507
622
  }
508
623
 
624
+ const pendingRequests = sortByRecency(
625
+ requests.filter((request) => request.status === 'pending'),
626
+ 'createdAt',
627
+ ).map((request) => projectChatRequest(request, agentId));
628
+
629
+ const conversations = assertStore().listConversations
630
+ ? assertStore().listConversations({ participantId: agentId })
631
+ : [];
632
+ const chats = sortByRecency(
633
+ conversations
634
+ .map((conversation) => {
635
+ const linkedRequest = resolveConversationLinkRequest(requests, conversation);
636
+ if (!linkedRequest && !normalizeText(conversation?.meta?.approvalRequestId, null)) return null;
637
+ const projected = projectChatInboxChat(conversation, agentId, linkedRequest);
638
+ if (!projected) return null;
639
+ if (normalizedDirection && projected.direction !== normalizedDirection) return null;
640
+ return projected;
641
+ })
642
+ .filter(Boolean),
643
+ 'lastTurnAt',
644
+ 'updatedAt',
645
+ 'createdAt',
646
+ );
647
+
509
648
  return {
510
- items: sortByRecency(items, 'createdAt').map((request) => projectChatRequest(request, agentId)),
649
+ counts: {
650
+ pendingRequestCount: pendingRequests.length,
651
+ chatCount: chats.length,
652
+ },
653
+ pendingRequests,
654
+ chats,
511
655
  };
512
656
  },
513
657
 
658
+ listChatRequests({ agentId, direction = null } = {}) {
659
+ return this.listChatInbox({ agentId, direction });
660
+ },
661
+
514
662
  async acceptChatRequest(chatRequestId, { actorAgentId } = {}) {
515
663
  requireAgent(actorAgentId);
516
664
  const normalizedChatRequestId = normalizeText(chatRequestId, null);
@@ -534,5 +682,27 @@ export function createChatRequestService({
534
682
  nextAction: resolveAcceptNextAction(kickoff),
535
683
  };
536
684
  },
685
+
686
+ async rejectChatRequest(chatRequestId, { actorAgentId } = {}) {
687
+ requireAgent(actorAgentId);
688
+ const normalizedChatRequestId = normalizeText(chatRequestId, null);
689
+ if (!normalizedChatRequestId) {
690
+ throw createInvalidChatRequestError('chat_request_id_required', 'chatRequestId is required');
691
+ }
692
+
693
+ const result = await assertRelay('rejectChatRequest').rejectChatRequest(normalizedChatRequestId, {
694
+ actorAgentId,
695
+ });
696
+ if (result.status < 200 || result.status >= 300) {
697
+ throw createRelayResponseError(result, 'chat_request_reject_failed');
698
+ }
699
+
700
+ return {
701
+ status: 'rejected',
702
+ verdict: 'manual_reject',
703
+ chatRequest: projectChatRequest(result.body || {}, actorAgentId),
704
+ nextAction: 'request_rejected_by_peer',
705
+ };
706
+ },
537
707
  };
538
708
  }
@@ -12,36 +12,6 @@ function normalizeBoolean(value, fallback = false) {
12
12
  return fallback;
13
13
  }
14
14
 
15
- function normalizeStringList(values = []) {
16
- if (!Array.isArray(values)) return [];
17
- return [...new Set(values.map((value) => normalizeText(value, null)).filter(Boolean))];
18
- }
19
-
20
- function normalizePositiveInteger(value, fallback = null) {
21
- const parsed = Number(value);
22
- if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
23
- return Math.max(1, Math.trunc(parsed));
24
- }
25
-
26
- function normalizeWorldEligibility(value, fallback = 'active') {
27
- const normalized = normalizeText(value, fallback);
28
- if (normalized === 'joined') return 'joined';
29
- return 'active';
30
- }
31
-
32
- function normalizeBroadcastAudience(value, fallback = 'members') {
33
- const normalized = normalizeText(value, fallback);
34
- if (normalized === 'admins') return 'admins';
35
- if (normalized === 'admins_and_owner') return 'admins_and_owner';
36
- return 'members';
37
- }
38
-
39
- function normalizeBroadcastReplyPolicy(value, fallback = 'zero') {
40
- const normalized = normalizeText(value, fallback);
41
- if (normalized === 'at_most_one') return 'at_most_one';
42
- return 'zero';
43
- }
44
-
45
15
  function summarizeWorldContextText(worldContextText, fallback = null) {
46
16
  const normalized = normalizeText(worldContextText, null);
47
17
  if (!normalized) return fallback;
@@ -78,7 +48,7 @@ function createWorldActionNotAllowedError({
78
48
  worldId,
79
49
  agentId,
80
50
  action,
81
- actorRole = WORLD_ROLES.NONE,
51
+ actorRole = null,
82
52
  allowedRoles = [],
83
53
  } = {}) {
84
54
  const error = new Error(`world_action_not_allowed:${action}:${worldId}:${agentId}`);
@@ -87,7 +57,6 @@ function createWorldActionNotAllowedError({
87
57
  const messageByAction = {
88
58
  [WORLD_ACTIONS.VIEW_MANAGEMENT]: 'agent does not have permission to access world management',
89
59
  [WORLD_ACTIONS.MANAGE_WORLD]: 'agent does not have permission to manage this world',
90
- [WORLD_ACTIONS.MANAGE_WORLD_ROLES]: 'only the world owner can manage world admins',
91
60
  [WORLD_ACTIONS.CHANGE_ENABLED_STATE]: 'only the world owner can enable or disable this world',
92
61
  };
93
62
  error.responseBody = {
@@ -125,7 +94,7 @@ function normalizeAgentId(agentId) {
125
94
 
126
95
  function normalizeWorldRole(worldRole, fallback = null) {
127
96
  const normalized = normalizeText(worldRole, fallback);
128
- return [WORLD_ROLES.OWNER, WORLD_ROLES.ADMIN, WORLD_ROLES.MEMBER].includes(normalized) ? normalized : fallback;
97
+ return [WORLD_ROLES.OWNER, WORLD_ROLES.MEMBER].includes(normalized) ? normalized : fallback;
129
98
  }
130
99
 
131
100
  function buildDefaultEntryProfileField() {
@@ -175,60 +144,39 @@ function buildConversationTemplate(interactionRules, prohibitedRules, { existing
175
144
  };
176
145
  }
177
146
 
178
- function buildResultContract(worldId, ratingRules) {
179
- return {
180
- schemaId: `${worldId}.result.v1`,
181
- outputs: ['rating', 'recommendation', 'notes'],
182
- successCriteria: [ratingRules || 'Each side gives an explicit 1 to 10 rating.'],
183
- exampleSignals: {
184
- intentSignals: [],
185
- conversationSignals: [],
186
- agentSignals: [],
187
- },
188
- };
189
- }
190
-
191
147
  function buildWorldRecord({
192
148
  worldId,
193
149
  creatorAgentId,
194
150
  displayName,
195
151
  summary,
196
- description,
197
- interactionRules,
198
- prohibitedRules,
199
- ratingRules,
200
152
  worldContextText = null,
201
153
  enabled = false,
202
154
  status = null,
203
- schemaVersion = 1,
204
155
  existingMetrics = null,
205
- existingConversationTemplate = null,
206
156
  } = {}) {
207
157
  const resolvedStatus = status || (enabled ? 'enabled' : 'draft');
208
158
  const participantContextField = buildDefaultEntryProfileField();
159
+ const resolvedWorldContextText = buildWorldContextText({
160
+ worldId,
161
+ displayName,
162
+ summary,
163
+ worldContextText,
164
+ interactionRules: null,
165
+ prohibitedRules: null,
166
+ });
209
167
 
210
168
  return {
211
169
  worldId,
212
170
  slug: slugify(displayName, worldId),
213
171
  displayName,
214
172
  summary,
215
- description,
173
+ description: resolvedWorldContextText,
216
174
  category: 'ugc',
217
175
  lifecycle: 'creator_managed',
218
176
  tags: ['ugc', 'creator-managed'],
219
- interactionRules,
220
- prohibitedRules,
221
- ratingRules,
222
- worldContextText: buildWorldContextText({
223
- worldId,
224
- displayName,
225
- summary,
226
- worldContextText,
227
- interactionRules,
228
- prohibitedRules,
229
- ratingRules,
230
- }),
231
- roles: [],
177
+ interactionRules: null,
178
+ prohibitedRules: null,
179
+ worldContextText: resolvedWorldContextText,
232
180
  joinSchema: {
233
181
  requiredFields: [participantContextField],
234
182
  optionalFields: [],
@@ -236,16 +184,12 @@ function buildWorldRecord({
236
184
  },
237
185
  searchSchema: buildSearchSchema(),
238
186
  matching: buildMatchingStrategy(),
239
- conversationTemplate: buildConversationTemplate(interactionRules, prohibitedRules, {
240
- existingConversationTemplate,
241
- }),
242
- resultContract: buildResultContract(worldId, ratingRules),
187
+ conversationTemplate: buildConversationTemplate(null, null),
243
188
  meta: {
244
189
  status: resolvedStatus === 'enabled' ? 'creator_enabled' : 'creator_draft',
245
190
  persistence: 'store',
246
191
  },
247
192
  creatorAgentId,
248
- adminAgentIds: [],
249
193
  eligibility: 'active',
250
194
  broadcast: {
251
195
  enabled: false,
@@ -255,7 +199,7 @@ function buildWorldRecord({
255
199
  },
256
200
  status: resolvedStatus,
257
201
  enabled,
258
- schemaVersion,
202
+ schemaVersion: 1,
259
203
  metrics: existingMetrics || {
260
204
  totalConversationCount: 0,
261
205
  },
@@ -339,29 +283,18 @@ export function createWorldAdminService({ worldService, worldAuthorizationServic
339
283
  return actorAgentId;
340
284
  }
341
285
 
342
- async function markMembershipsStale(worldId) {
343
- const membershipStore = assertStore();
344
- const memberships = membershipStore.listMemberships({ worldId });
345
- for (const membership of memberships) {
346
- if (!['joined', 'active'].includes(membership.status)) continue;
347
- await membershipStore.updateMembership(membership.membershipId, {
348
- status: 'stale_profile',
349
- });
350
- }
351
- }
352
-
353
- function requireWorldAction({ worldId, actorAgentId, action } = {}) {
286
+ function requireWorldOwner({ worldId, actorAgentId } = {}) {
354
287
  const authorization = worldAuthorizationService.evaluateWorldAction({
355
288
  worldId,
356
289
  actorAgentId,
357
- action,
290
+ action: WORLD_ACTIONS.MANAGE_WORLD,
358
291
  includeDisabled: true,
359
292
  });
360
293
  if (!authorization.allowed) {
361
294
  throw createWorldActionNotAllowedError({
362
295
  worldId: authorization.world.worldId,
363
296
  agentId: actorAgentId,
364
- action,
297
+ action: WORLD_ACTIONS.MANAGE_WORLD,
365
298
  actorRole: authorization.worldRole,
366
299
  allowedRoles: authorization.allowedRoles,
367
300
  });
@@ -395,10 +328,6 @@ export function createWorldAdminService({ worldService, worldAuthorizationServic
395
328
  creatorAgentId: resolvedOwnerAgentId,
396
329
  displayName: resolvedDisplayName,
397
330
  summary: summarizeWorldContextText(resolvedWorldContextText, resolvedDisplayName),
398
- description: resolvedWorldContextText,
399
- interactionRules: null,
400
- prohibitedRules: null,
401
- ratingRules: null,
402
331
  worldContextText: resolvedWorldContextText,
403
332
  enabled: normalizeBoolean(enabled, false),
404
333
  });
@@ -413,8 +342,13 @@ export function createWorldAdminService({ worldService, worldAuthorizationServic
413
342
  listManagedWorlds({ actorAgentId, creatorAgentId, includeDisabled = true } = {}) {
414
343
  const storeBacked = assertStore();
415
344
  const resolvedActorAgentId = assertActorAgent(actorAgentId || creatorAgentId);
416
- return worldAuthorizationService
417
- .listManagedWorlds({ actorAgentId: resolvedActorAgentId, includeDisabled })
345
+ return worldService
346
+ .listOwnedWorlds({ creatorAgentId: resolvedActorAgentId, includeDisabled })
347
+ .map((world) => worldAuthorizationService.resolveWorldActorContext({
348
+ worldId: world.worldId,
349
+ actorAgentId: resolvedActorAgentId,
350
+ includeDisabled,
351
+ }))
418
352
  .map((context) => projectManagedWorldSummary(storeBacked, context.world, {
419
353
  worldRole: context.worldRole,
420
354
  }));
@@ -425,10 +359,9 @@ export function createWorldAdminService({ worldService, worldAuthorizationServic
425
359
  getManagedWorld({ actorAgentId, creatorAgentId, worldId } = {}) {
426
360
  const storeBacked = assertStore();
427
361
  const resolvedActorAgentId = assertActorAgent(actorAgentId || creatorAgentId);
428
- const authorization = requireWorldAction({
362
+ const authorization = requireWorldOwner({
429
363
  worldId,
430
364
  actorAgentId: resolvedActorAgentId,
431
- action: WORLD_ACTIONS.VIEW_MANAGEMENT,
432
365
  });
433
366
  return projectManagedWorld(storeBacked, authorization.world, {
434
367
  worldRole: authorization.worldRole,
@@ -439,25 +372,10 @@ export function createWorldAdminService({ worldService, worldAuthorizationServic
439
372
  const resolvedActorAgentId = assertActorAgent(actorAgentId || creatorAgentId);
440
373
  const hasChanges = changes && typeof changes === 'object' && !Array.isArray(changes);
441
374
 
442
- let authorization = requireWorldAction({
375
+ const authorization = requireWorldOwner({
443
376
  worldId,
444
377
  actorAgentId: resolvedActorAgentId,
445
- action: WORLD_ACTIONS.VIEW_MANAGEMENT,
446
378
  });
447
- if (hasManageWorldChanges(changes)) {
448
- authorization = requireWorldAction({
449
- worldId,
450
- actorAgentId: resolvedActorAgentId,
451
- action: WORLD_ACTIONS.MANAGE_WORLD,
452
- });
453
- }
454
- if (enabled != null) {
455
- authorization = requireWorldAction({
456
- worldId,
457
- actorAgentId: resolvedActorAgentId,
458
- action: WORLD_ACTIONS.CHANGE_ENABLED_STATE,
459
- });
460
- }
461
379
 
462
380
  const existingWorld = authorization.world;
463
381
  if (!hasChanges && enabled == null) {
@@ -466,7 +384,6 @@ export function createWorldAdminService({ worldService, worldAuthorizationServic
466
384
  });
467
385
  }
468
386
 
469
- let nextSchemaVersion = Number(existingWorld.schemaVersion || 1);
470
387
  let nextRecord = existingWorld;
471
388
 
472
389
  if (hasChanges) {
@@ -479,18 +396,12 @@ export function createWorldAdminService({ worldService, worldAuthorizationServic
479
396
  creatorAgentId: existingWorld.creatorAgentId,
480
397
  displayName: nextDisplayName,
481
398
  summary: summarizeWorldContextText(nextWorldContextText, nextDisplayName),
482
- description: nextWorldContextText,
483
- interactionRules: null,
484
- prohibitedRules: null,
485
- ratingRules: null,
486
399
  worldContextText: nextWorldContextText,
487
400
  enabled: enabled == null ? existingWorld.enabled === true : normalizeBoolean(enabled, false),
488
401
  status: enabled == null
489
402
  ? existingWorld.status
490
403
  : (normalizeBoolean(enabled, false) ? 'enabled' : 'disabled'),
491
- schemaVersion: nextSchemaVersion,
492
404
  existingMetrics: existingWorld.metrics || null,
493
- existingConversationTemplate: existingWorld.conversationTemplate || null,
494
405
  });
495
406
  } else if (enabled != null) {
496
407
  nextRecord = {
@@ -505,9 +416,6 @@ export function createWorldAdminService({ worldService, worldAuthorizationServic
505
416
  }
506
417
 
507
418
  const updated = await storeBacked.updateWorldConfig(existingWorld.worldId, nextRecord);
508
- if (nextSchemaVersion !== Number(existingWorld.schemaVersion || 1)) {
509
- await markMembershipsStale(existingWorld.worldId);
510
- }
511
419
  const normalizedWorld = worldService.requireWorld(updated.worldId, { includeDisabled: true });
512
420
  return projectManagedWorld(storeBacked, normalizedWorld, {
513
421
  worldRole: authorization.worldRole,
@@ -7,13 +7,11 @@ function normalizeText(value, fallback = null) {
7
7
  export const WORLD_ROLES = Object.freeze({
8
8
  OWNER: 'owner',
9
9
  MEMBER: 'member',
10
- NONE: 'none',
11
10
  });
12
11
 
13
12
  export const WORLD_ACTIONS = Object.freeze({
14
13
  VIEW_MANAGEMENT: 'view_world_management',
15
14
  MANAGE_WORLD: 'manage_world',
16
- MANAGE_WORLD_ROLES: 'manage_world_roles',
17
15
  CHANGE_ENABLED_STATE: 'change_world_enabled_state',
18
16
  BROADCAST: 'broadcast_world',
19
17
  SEARCH: 'search_world',
@@ -21,10 +19,22 @@ export const WORLD_ACTIONS = Object.freeze({
21
19
  CREATE_CHAT_REQUEST: 'create_world_chat_request',
22
20
  });
23
21
 
24
- function resolveWorldRole({ isOwner = false, isAdmin = false, isMember = false } = {}) {
22
+ function resolveWorldRole({ isOwner = false, isMember = false } = {}) {
25
23
  if (isOwner) return WORLD_ROLES.OWNER;
26
24
  if (isMember) return WORLD_ROLES.MEMBER;
27
- return WORLD_ROLES.NONE;
25
+ return null;
26
+ }
27
+
28
+ function resolveEffectiveRoles({ isOwner = false, isMember = false } = {}) {
29
+ const roles = [];
30
+ if (isOwner) {
31
+ roles.push(WORLD_ROLES.OWNER);
32
+ // World owners should retain member-scoped capabilities for worlds they are actively in.
33
+ if (isMember) roles.push(WORLD_ROLES.MEMBER);
34
+ return roles;
35
+ }
36
+ if (isMember) roles.push(WORLD_ROLES.MEMBER);
37
+ return roles;
28
38
  }
29
39
 
30
40
  function resolveActionRequirement(action) {
@@ -37,11 +47,6 @@ function resolveActionRequirement(action) {
37
47
  allowedRoles: [WORLD_ROLES.OWNER],
38
48
  reason: 'owner_role_required',
39
49
  };
40
- case WORLD_ACTIONS.MANAGE_WORLD_ROLES:
41
- return {
42
- allowedRoles: [WORLD_ROLES.OWNER],
43
- reason: 'owner_role_required',
44
- };
45
50
  case WORLD_ACTIONS.SEARCH:
46
51
  case WORLD_ACTIONS.VIEW_CANDIDATE_FEED:
47
52
  case WORLD_ACTIONS.CREATE_CHAT_REQUEST:
@@ -77,22 +82,20 @@ export function createWorldAuthorizationService({ worldService, membershipServic
77
82
  ? resolveMembership(world.worldId, normalizedActorAgentId, { includeDisabled })
78
83
  : null;
79
84
  const isOwner = normalizedActorAgentId != null && normalizedActorAgentId === world.creatorAgentId;
80
- const isAdmin = normalizedActorAgentId != null
81
- && Array.isArray(world.adminAgentIds)
82
- && world.adminAgentIds.includes(normalizedActorAgentId);
83
85
  const isMember = membership?.status === 'active';
84
- const worldRole = resolveWorldRole({ isOwner, isAdmin, isMember });
86
+ const worldRole = resolveWorldRole({ isOwner, isMember });
87
+ const effectiveRoles = resolveEffectiveRoles({ isOwner, isMember });
85
88
 
86
89
  return {
87
90
  world,
88
91
  actorAgentId: normalizedActorAgentId,
89
92
  worldRole,
90
- managementRole: isOwner ? WORLD_ROLES.OWNER : isAdmin ? WORLD_ROLES.ADMIN : null,
93
+ effectiveRoles,
94
+ managementRole: isOwner ? WORLD_ROLES.OWNER : null,
91
95
  membership,
92
96
  membershipStatus: membership?.status || null,
93
97
  roles: {
94
98
  owner: isOwner,
95
- admin: isAdmin,
96
99
  member: isMember,
97
100
  },
98
101
  };
@@ -111,7 +114,7 @@ export function createWorldAuthorizationService({ worldService, membershipServic
111
114
  includeDisabled,
112
115
  });
113
116
  const requirement = resolveActionRequirement(action);
114
- const allowed = requirement.allowedRoles.includes(actor.worldRole);
117
+ const allowed = actor.effectiveRoles.some((role) => requirement.allowedRoles.includes(role));
115
118
 
116
119
  return {
117
120
  ...actor,
@@ -125,7 +128,7 @@ export function createWorldAuthorizationService({ worldService, membershipServic
125
128
 
126
129
  listManagedWorlds({ actorAgentId, includeDisabled = true } = {}) {
127
130
  return worldService
128
- .listCustomWorlds({ includeDisabled })
131
+ .listOwnedWorlds({ creatorAgentId: actorAgentId, includeDisabled })
129
132
  .map((world) => resolveWorldActorContextForWorld(world, actorAgentId, { includeDisabled }))
130
133
  .filter((context) => context.managementRole != null);
131
134
  },