@xfxstudio/claworld 0.1.0

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 (69) hide show
  1. package/README.md +60 -0
  2. package/bin/claworld.mjs +9 -0
  3. package/index.js +51 -0
  4. package/openclaw.plugin.json +470 -0
  5. package/package.json +76 -0
  6. package/setup-entry.js +6 -0
  7. package/src/lib/accepted-chat-kickoff.js +192 -0
  8. package/src/lib/agent-address.js +46 -0
  9. package/src/lib/agent-profile.js +69 -0
  10. package/src/lib/http-auth.js +151 -0
  11. package/src/lib/policy.js +118 -0
  12. package/src/lib/runtime-errors.js +149 -0
  13. package/src/lib/runtime-guidance.js +458 -0
  14. package/src/openclaw/index.js +53 -0
  15. package/src/openclaw/installer/cli.js +349 -0
  16. package/src/openclaw/installer/constants.js +6 -0
  17. package/src/openclaw/installer/core.js +1548 -0
  18. package/src/openclaw/installer/doctor.js +690 -0
  19. package/src/openclaw/installer/workspace-contract.js +403 -0
  20. package/src/openclaw/plugin/account-identity.js +66 -0
  21. package/src/openclaw/plugin/claworld-channel-plugin.js +3118 -0
  22. package/src/openclaw/plugin/config-schema.js +464 -0
  23. package/src/openclaw/plugin/lifecycle.js +114 -0
  24. package/src/openclaw/plugin/managed-config.js +648 -0
  25. package/src/openclaw/plugin/onboarding.js +291 -0
  26. package/src/openclaw/plugin/register.js +961 -0
  27. package/src/openclaw/plugin/relay-client.js +783 -0
  28. package/src/openclaw/plugin/runtime.js +12 -0
  29. package/src/openclaw/protocol/relay-event-protocol.js +31 -0
  30. package/src/openclaw/runtime/canonical-result-builder.js +116 -0
  31. package/src/openclaw/runtime/demo-session-bootstrap.js +37 -0
  32. package/src/openclaw/runtime/feedback-helper.js +145 -0
  33. package/src/openclaw/runtime/inbound-session-router.js +36 -0
  34. package/src/openclaw/runtime/outbound-session-bridge.js +17 -0
  35. package/src/openclaw/runtime/product-shell-helper.js +1712 -0
  36. package/src/openclaw/runtime/runtime-path.js +19 -0
  37. package/src/openclaw/runtime/system-message-orchestrator.js +1 -0
  38. package/src/openclaw/runtime/tool-contracts.js +714 -0
  39. package/src/openclaw/runtime/tool-inventory.js +92 -0
  40. package/src/openclaw/runtime/world-moderation-helper.js +415 -0
  41. package/src/openclaw/runtime/world-session-startup.js +1 -0
  42. package/src/product-shell/catalog/default-world-catalog.js +296 -0
  43. package/src/product-shell/contracts/candidate-feed.js +330 -0
  44. package/src/product-shell/contracts/chat-request-approval-policy.js +98 -0
  45. package/src/product-shell/contracts/world-manifest.js +435 -0
  46. package/src/product-shell/contracts/world-orchestration.js +1024 -0
  47. package/src/product-shell/feedback/feedback-contract.js +13 -0
  48. package/src/product-shell/feedback/feedback-routes.js +98 -0
  49. package/src/product-shell/feedback/feedback-service.js +254 -0
  50. package/src/product-shell/index.js +163 -0
  51. package/src/product-shell/matching/matchmaking-service.js +340 -0
  52. package/src/product-shell/membership/membership-service.js +277 -0
  53. package/src/product-shell/onboarding/onboarding-routes.js +37 -0
  54. package/src/product-shell/onboarding/onboarding-service.js +230 -0
  55. package/src/product-shell/orchestration/session-orchestrator.js +38 -0
  56. package/src/product-shell/results/result-service.js +15 -0
  57. package/src/product-shell/search/search-service.js +359 -0
  58. package/src/product-shell/social/chat-request-approval-policy.js +332 -0
  59. package/src/product-shell/social/chat-request-routes.js +108 -0
  60. package/src/product-shell/social/chat-request-service.js +632 -0
  61. package/src/product-shell/social/friend-routes.js +82 -0
  62. package/src/product-shell/social/friend-service.js +560 -0
  63. package/src/product-shell/social/social-routes.js +21 -0
  64. package/src/product-shell/social/social-service.js +140 -0
  65. package/src/product-shell/worlds/world-admin-service.js +705 -0
  66. package/src/product-shell/worlds/world-authorization.js +135 -0
  67. package/src/product-shell/worlds/world-broadcast-service.js +299 -0
  68. package/src/product-shell/worlds/world-routes.js +410 -0
  69. package/src/product-shell/worlds/world-service.js +89 -0
@@ -0,0 +1,705 @@
1
+ import { WORLD_ACTIONS, WORLD_ROLES } from './world-authorization.js';
2
+
3
+ function normalizeText(value, fallback = null) {
4
+ if (value == null) return fallback;
5
+ const normalized = String(value).trim();
6
+ return normalized || fallback;
7
+ }
8
+
9
+ function normalizeBoolean(value, fallback = false) {
10
+ if (typeof value === 'boolean') return value;
11
+ return fallback;
12
+ }
13
+
14
+ function normalizeStringList(values = []) {
15
+ if (!Array.isArray(values)) return [];
16
+ return [...new Set(values.map((value) => normalizeText(value, null)).filter(Boolean))];
17
+ }
18
+
19
+ function normalizePositiveInteger(value, fallback = null) {
20
+ const parsed = Number(value);
21
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
22
+ return Math.max(1, Math.trunc(parsed));
23
+ }
24
+
25
+ function normalizeWorldEligibility(value, fallback = 'active') {
26
+ const normalized = normalizeText(value, fallback);
27
+ if (normalized === 'joined') return 'joined';
28
+ return 'active';
29
+ }
30
+
31
+ function normalizeBroadcastAudience(value, fallback = 'members') {
32
+ const normalized = normalizeText(value, fallback);
33
+ if (normalized === 'admins') return 'admins';
34
+ if (normalized === 'admins_and_owner') return 'admins_and_owner';
35
+ return 'members';
36
+ }
37
+
38
+ function normalizeBroadcastReplyPolicy(value, fallback = 'zero') {
39
+ const normalized = normalizeText(value, fallback);
40
+ if (normalized === 'at_most_one') return 'at_most_one';
41
+ return 'zero';
42
+ }
43
+
44
+ function slugify(value, fallback = 'world') {
45
+ const normalized = normalizeText(value, fallback)
46
+ .toLowerCase()
47
+ .replace(/[^a-z0-9]+/g, '-')
48
+ .replace(/^-+|-+$/g, '');
49
+ return normalized || fallback;
50
+ }
51
+
52
+ function createConfigurationError() {
53
+ const error = new Error('world_store_unavailable');
54
+ error.code = 'world_store_unavailable';
55
+ error.status = 500;
56
+ return error;
57
+ }
58
+
59
+ function createAgentNotFoundError(agentId) {
60
+ const error = new Error(`agent_not_found:${agentId}`);
61
+ error.code = 'agent_not_found';
62
+ error.status = 404;
63
+ return error;
64
+ }
65
+
66
+ function createWorldActionNotAllowedError({
67
+ worldId,
68
+ agentId,
69
+ action,
70
+ actorRole = WORLD_ROLES.NONE,
71
+ allowedRoles = [],
72
+ } = {}) {
73
+ const error = new Error(`world_action_not_allowed:${action}:${worldId}:${agentId}`);
74
+ error.code = 'world_action_not_allowed';
75
+ error.status = 403;
76
+ const messageByAction = {
77
+ [WORLD_ACTIONS.VIEW_MANAGEMENT]: 'agent does not have permission to access world management',
78
+ [WORLD_ACTIONS.MANAGE_WORLD]: 'agent does not have permission to manage this world',
79
+ [WORLD_ACTIONS.MANAGE_WORLD_ROLES]: 'only the world owner can manage world admins',
80
+ [WORLD_ACTIONS.CHANGE_ENABLED_STATE]: 'only the world owner can enable or disable this world',
81
+ };
82
+ error.responseBody = {
83
+ error: error.code,
84
+ message: messageByAction[action] || 'agent does not have permission to manage this world',
85
+ worldId,
86
+ agentId,
87
+ action,
88
+ actorRole,
89
+ allowedRoles,
90
+ };
91
+ return error;
92
+ }
93
+
94
+ function createInvalidWorldRequestError(fieldId, message = `${fieldId} is required`) {
95
+ const error = new Error(`invalid_world_request:${fieldId}`);
96
+ error.code = 'invalid_world_request';
97
+ error.status = 400;
98
+ error.responseBody = {
99
+ error: error.code,
100
+ message: 'world request is invalid',
101
+ fieldErrors: [
102
+ {
103
+ fieldId,
104
+ message,
105
+ },
106
+ ],
107
+ };
108
+ return error;
109
+ }
110
+
111
+ function normalizeAgentId(agentId) {
112
+ return normalizeText(agentId, null);
113
+ }
114
+
115
+ function normalizeWorldRole(worldRole, fallback = null) {
116
+ const normalized = normalizeText(worldRole, fallback);
117
+ return [WORLD_ROLES.OWNER, WORLD_ROLES.ADMIN, WORLD_ROLES.MEMBER].includes(normalized) ? normalized : fallback;
118
+ }
119
+
120
+ function normalizeFieldType(type) {
121
+ const normalized = normalizeText(type, 'string');
122
+ if (['string', 'string[]', 'number', 'boolean'].includes(normalized)) return normalized;
123
+ throw createInvalidWorldRequestError('entryProfileSchema.fields.type', `unsupported field type: ${normalized}`);
124
+ }
125
+
126
+ function normalizeEntryProfileField(field = {}, index = 0) {
127
+ const label = normalizeText(field.label, null);
128
+ const fieldId = normalizeText(field.fieldId, label ? slugify(label, `field-${index + 1}`) : `field_${index + 1}`);
129
+ if (!fieldId) {
130
+ throw createInvalidWorldRequestError('entryProfileSchema.fields.fieldId');
131
+ }
132
+
133
+ return {
134
+ fieldId,
135
+ label: label || fieldId,
136
+ type: normalizeFieldType(field.type),
137
+ required: field.required !== false,
138
+ searchable: field.searchable === true,
139
+ description: normalizeText(field.description, null),
140
+ examples: normalizeStringList(field.examples),
141
+ };
142
+ }
143
+
144
+ function normalizeEntryProfileSchema(input = {}) {
145
+ const rawFields = Array.isArray(input?.fields) ? input.fields : (Array.isArray(input) ? input : []);
146
+ if (rawFields.length === 0) {
147
+ throw createInvalidWorldRequestError('entryProfileSchema.fields', 'at least one profile field is required');
148
+ }
149
+
150
+ const fields = rawFields.map((field, index) => normalizeEntryProfileField(field, index));
151
+ const fieldIds = new Set();
152
+ for (const field of fields) {
153
+ if (fieldIds.has(field.fieldId)) {
154
+ throw createInvalidWorldRequestError('entryProfileSchema.fields.fieldId', `duplicate fieldId: ${field.fieldId}`);
155
+ }
156
+ fieldIds.add(field.fieldId);
157
+ }
158
+
159
+ const requiredFields = fields.filter((field) => field.required);
160
+ if (requiredFields.length === 0) {
161
+ throw createInvalidWorldRequestError('entryProfileSchema.fields.required', 'at least one required profile field is required');
162
+ }
163
+
164
+ const searchableFields = fields.filter((field) => field.searchable);
165
+ if (searchableFields.length === 0) {
166
+ throw createInvalidWorldRequestError('entryProfileSchema.fields.searchable', 'at least one searchable field is required');
167
+ }
168
+
169
+ return {
170
+ fields,
171
+ requiredFields,
172
+ optionalFields: fields.filter((field) => !field.required),
173
+ searchableFieldIds: searchableFields.map((field) => field.fieldId),
174
+ };
175
+ }
176
+
177
+ function normalizeAdminAgentIds(adminAgentIds, { creatorAgentId = null, store = null } = {}) {
178
+ if (adminAgentIds == null) return [];
179
+ if (!Array.isArray(adminAgentIds)) {
180
+ throw createInvalidWorldRequestError('adminAgentIds', 'adminAgentIds must be an array of agent ids');
181
+ }
182
+
183
+ const normalizedCreatorAgentId = normalizeAgentId(creatorAgentId);
184
+ const uniqueAgentIds = [];
185
+ const seen = new Set();
186
+
187
+ for (const rawAgentId of adminAgentIds) {
188
+ const agentId = normalizeAgentId(rawAgentId);
189
+ if (!agentId || seen.has(agentId) || agentId === normalizedCreatorAgentId) continue;
190
+ if (store && !store.getAgent(agentId)) {
191
+ throw createInvalidWorldRequestError('adminAgentIds', `unknown admin agent: ${agentId}`);
192
+ }
193
+ seen.add(agentId);
194
+ uniqueAgentIds.push(agentId);
195
+ }
196
+
197
+ return uniqueAgentIds;
198
+ }
199
+
200
+ function normalizeBroadcastConfig(broadcast, { existingBroadcast = null } = {}) {
201
+ if (broadcast !== undefined && (broadcast == null || typeof broadcast !== 'object' || Array.isArray(broadcast))) {
202
+ throw createInvalidWorldRequestError('broadcast', 'broadcast must be an object');
203
+ }
204
+
205
+ const source = broadcast === undefined ? existingBroadcast : broadcast;
206
+ const normalized = source && typeof source === 'object' && !Array.isArray(source)
207
+ ? source
208
+ : {};
209
+
210
+ return {
211
+ enabled: normalized.enabled === true,
212
+ audience: normalizeBroadcastAudience(
213
+ normalized.audience,
214
+ normalizeBroadcastAudience(existingBroadcast?.audience, 'members'),
215
+ ),
216
+ replyPolicy: normalizeBroadcastReplyPolicy(
217
+ normalized.replyPolicy,
218
+ normalizeBroadcastReplyPolicy(existingBroadcast?.replyPolicy, 'zero'),
219
+ ),
220
+ excludeSelf: normalized.excludeSelf === undefined
221
+ ? existingBroadcast?.excludeSelf !== false
222
+ : normalized.excludeSelf !== false,
223
+ };
224
+ }
225
+
226
+ function buildSearchSchema(entryProfileSchema) {
227
+ return {
228
+ mode: 'profile_overlap_search',
229
+ inputFieldIds: entryProfileSchema.searchableFieldIds,
230
+ summary: 'Search active online members by overlap on the world profile fields chosen by the world creator.',
231
+ onlineOnly: true,
232
+ defaultLimit: 10,
233
+ };
234
+ }
235
+
236
+ function buildMatchingStrategy(entryProfileSchema) {
237
+ return {
238
+ mode: 'profile_overlap',
239
+ cadence: 'on_demand',
240
+ strategySummary:
241
+ `Rank world members by overlap on ${entryProfileSchema.searchableFieldIds.join(', ')} before opening a conversation.`,
242
+ candidateSources: ['active_memberships'],
243
+ };
244
+ }
245
+
246
+ function resolveWorldMaxTurns(sessionTemplate, { existingSessionTemplate = null, required = false } = {}) {
247
+ const sessionTemplateProvided = sessionTemplate !== undefined;
248
+ if (sessionTemplateProvided) {
249
+ if (!sessionTemplate || typeof sessionTemplate !== 'object' || Array.isArray(sessionTemplate)) {
250
+ throw createInvalidWorldRequestError(
251
+ 'sessionTemplate.maxTurns',
252
+ 'sessionTemplate.maxTurns is required',
253
+ );
254
+ }
255
+
256
+ const explicitMaxTurns = normalizePositiveInteger(sessionTemplate.maxTurns, null);
257
+ if (explicitMaxTurns == null) {
258
+ throw createInvalidWorldRequestError(
259
+ 'sessionTemplate.maxTurns',
260
+ 'sessionTemplate.maxTurns must be a positive integer',
261
+ );
262
+ }
263
+ return explicitMaxTurns;
264
+ }
265
+
266
+ const existingMaxTurns = normalizePositiveInteger(existingSessionTemplate?.maxTurns, null);
267
+ if (existingMaxTurns != null) return existingMaxTurns;
268
+ if (required) throw createInvalidWorldRequestError('sessionTemplate.maxTurns');
269
+ return null;
270
+ }
271
+
272
+ function buildSessionTemplate(interactionRules, prohibitedRules, { maxTurns, existingSessionTemplate = null } = {}) {
273
+ return {
274
+ mode: normalizeText(existingSessionTemplate?.mode, 'a2a'),
275
+ maxTurns,
276
+ turnTimeoutMs: normalizePositiveInteger(existingSessionTemplate?.turnTimeoutMs, 60_000),
277
+ raiseHandPolicy: {
278
+ mode: normalizeText(existingSessionTemplate?.raiseHandPolicy?.mode, 'dual_raise_hand'),
279
+ summary: prohibitedRules || 'Both agents should stop once neither side has useful new questions.',
280
+ },
281
+ worldRules: {
282
+ openingText: interactionRules,
283
+ turnMessageRules: Array.isArray(existingSessionTemplate?.worldRules?.turnMessageRules)
284
+ ? existingSessionTemplate.worldRules.turnMessageRules
285
+ : [],
286
+ convergence: existingSessionTemplate?.worldRules?.convergence || {},
287
+ stateChangeMessages: existingSessionTemplate?.worldRules?.stateChangeMessages || {},
288
+ },
289
+ };
290
+ }
291
+
292
+ function buildResultContract(worldId, ratingRules) {
293
+ return {
294
+ schemaId: `${worldId}.result.v1`,
295
+ outputs: ['rating', 'recommendation', 'notes'],
296
+ successCriteria: [ratingRules || 'Each side gives an explicit 1 to 10 rating.'],
297
+ exampleSignals: {
298
+ intentSignals: [],
299
+ conversationSignals: [],
300
+ agentSignals: [],
301
+ },
302
+ };
303
+ }
304
+
305
+ function buildWorldRecord({
306
+ worldId,
307
+ creatorAgentId,
308
+ adminAgentIds = [],
309
+ eligibility = 'active',
310
+ broadcast = undefined,
311
+ displayName,
312
+ summary,
313
+ description,
314
+ interactionRules,
315
+ prohibitedRules,
316
+ ratingRules,
317
+ entryProfileSchema,
318
+ enabled = false,
319
+ status = null,
320
+ schemaVersion = 1,
321
+ existingMetrics = null,
322
+ sessionTemplate = undefined,
323
+ existingSessionTemplate = null,
324
+ existingBroadcast = null,
325
+ } = {}) {
326
+ const resolvedStatus = status || (enabled ? 'enabled' : 'draft');
327
+ const resolvedMaxTurns = resolveWorldMaxTurns(sessionTemplate, {
328
+ existingSessionTemplate,
329
+ required: existingSessionTemplate == null,
330
+ });
331
+ const resolvedEligibility = normalizeWorldEligibility(eligibility, 'active');
332
+ const resolvedBroadcast = normalizeBroadcastConfig(broadcast, { existingBroadcast });
333
+
334
+ return {
335
+ worldId,
336
+ slug: slugify(displayName, worldId),
337
+ displayName,
338
+ summary,
339
+ description,
340
+ category: 'ugc',
341
+ lifecycle: 'creator_managed',
342
+ tags: ['ugc', 'creator-managed'],
343
+ interactionRules,
344
+ prohibitedRules,
345
+ ratingRules,
346
+ roles: [],
347
+ joinSchema: {
348
+ requiredFields: entryProfileSchema.requiredFields,
349
+ optionalFields: entryProfileSchema.optionalFields,
350
+ hints: [],
351
+ },
352
+ searchSchema: buildSearchSchema(entryProfileSchema),
353
+ matching: buildMatchingStrategy(entryProfileSchema),
354
+ sessionTemplate: buildSessionTemplate(interactionRules, prohibitedRules, {
355
+ maxTurns: resolvedMaxTurns,
356
+ existingSessionTemplate,
357
+ }),
358
+ resultContract: buildResultContract(worldId, ratingRules),
359
+ meta: {
360
+ status: resolvedStatus === 'enabled' ? 'creator_enabled' : 'creator_draft',
361
+ persistence: 'store',
362
+ },
363
+ creatorAgentId,
364
+ adminAgentIds,
365
+ eligibility: resolvedEligibility,
366
+ broadcast: resolvedBroadcast,
367
+ status: resolvedStatus,
368
+ enabled,
369
+ schemaVersion,
370
+ metrics: existingMetrics || {
371
+ totalConversationCount: 0,
372
+ },
373
+ };
374
+ }
375
+
376
+ function projectEntryProfileSchema(world = {}) {
377
+ const requiredFields = Array.isArray(world.joinSchema?.requiredFields) ? world.joinSchema.requiredFields : [];
378
+ const optionalFields = Array.isArray(world.joinSchema?.optionalFields) ? world.joinSchema.optionalFields : [];
379
+ const searchableFieldIds = new Set(Array.isArray(world.searchSchema?.inputFieldIds) ? world.searchSchema.inputFieldIds : []);
380
+
381
+ return {
382
+ fields: [...requiredFields, ...optionalFields].map((field) => ({
383
+ fieldId: field.fieldId,
384
+ label: field.label,
385
+ type: field.type,
386
+ required: field.required === true,
387
+ searchable: searchableFieldIds.has(field.fieldId),
388
+ description: field.description || null,
389
+ examples: normalizeStringList(field.examples),
390
+ })),
391
+ };
392
+ }
393
+
394
+ function projectWorldStats(store, world = {}) {
395
+ const totalParticipants = store.countMemberships({ worldId: world.worldId });
396
+ const activeParticipants = store.countMemberships({ worldId: world.worldId, status: 'active' });
397
+ return {
398
+ totalParticipants,
399
+ activeParticipants,
400
+ totalConversationCount: Number(world.metrics?.totalConversationCount || 0),
401
+ };
402
+ }
403
+
404
+ function projectManagedWorldSummary(store, world = {}, { worldRole = null } = {}) {
405
+ return {
406
+ worldId: world.worldId,
407
+ displayName: world.displayName,
408
+ summary: world.summary,
409
+ enabled: world.enabled === true,
410
+ status: world.status || 'draft',
411
+ worldRole: normalizeWorldRole(worldRole, null),
412
+ createdAt: world.createdAt || null,
413
+ updatedAt: world.updatedAt || null,
414
+ stats: projectWorldStats(store, world),
415
+ };
416
+ }
417
+
418
+ function projectManagedWorld(store, world = {}, { worldRole = null } = {}) {
419
+ return {
420
+ worldId: world.worldId,
421
+ displayName: world.displayName,
422
+ summary: world.summary,
423
+ description: world.description,
424
+ enabled: world.enabled === true,
425
+ status: world.status || 'draft',
426
+ worldRole: normalizeWorldRole(worldRole, null),
427
+ schemaVersion: Number(world.schemaVersion || 1),
428
+ createdAt: world.createdAt || null,
429
+ updatedAt: world.updatedAt || null,
430
+ adminAgentIds: Array.isArray(world.adminAgentIds) ? world.adminAgentIds : [],
431
+ eligibility: normalizeWorldEligibility(world.eligibility, 'active'),
432
+ broadcast: normalizeBroadcastConfig(world.broadcast, {
433
+ existingBroadcast: world.broadcast || null,
434
+ }),
435
+ entryProfileSchema: projectEntryProfileSchema(world),
436
+ interactionRules: world.interactionRules || '',
437
+ prohibitedRules: world.prohibitedRules || '',
438
+ ratingRules: world.ratingRules || '',
439
+ sessionTemplate: world.sessionTemplate || null,
440
+ stats: projectWorldStats(store, world),
441
+ };
442
+ }
443
+
444
+ function buildWorldId(displayName, existingIds = []) {
445
+ const base = `ugc-${slugify(displayName, 'world')}`;
446
+ if (!existingIds.includes(base)) return base;
447
+
448
+ let counter = 2;
449
+ while (existingIds.includes(`${base}-${counter}`)) {
450
+ counter += 1;
451
+ }
452
+ return `${base}-${counter}`;
453
+ }
454
+
455
+ export function createWorldAdminService({ worldService, worldAuthorizationService, store = null } = {}) {
456
+ function assertStore() {
457
+ if (!store) throw createConfigurationError();
458
+ return store;
459
+ }
460
+
461
+ function assertActorAgent(agentId) {
462
+ const actorAgentId = normalizeAgentId(agentId);
463
+ if (!actorAgentId) throw createInvalidWorldRequestError('agentId', 'agentId is required');
464
+ const agent = assertStore().getAgent(actorAgentId);
465
+ if (!agent) throw createAgentNotFoundError(actorAgentId);
466
+ return actorAgentId;
467
+ }
468
+
469
+ async function markMembershipsStale(worldId) {
470
+ const membershipStore = assertStore();
471
+ const memberships = membershipStore.listMemberships({ worldId });
472
+ for (const membership of memberships) {
473
+ if (!['joined', 'active'].includes(membership.status)) continue;
474
+ await membershipStore.updateMembership(membership.membershipId, {
475
+ status: 'stale_profile',
476
+ });
477
+ }
478
+ }
479
+
480
+ function requireWorldAction({ worldId, actorAgentId, action } = {}) {
481
+ const authorization = worldAuthorizationService.evaluateWorldAction({
482
+ worldId,
483
+ actorAgentId,
484
+ action,
485
+ includeDisabled: true,
486
+ });
487
+ if (!authorization.allowed) {
488
+ throw createWorldActionNotAllowedError({
489
+ worldId: authorization.world.worldId,
490
+ agentId: actorAgentId,
491
+ action,
492
+ actorRole: authorization.worldRole,
493
+ allowedRoles: authorization.allowedRoles,
494
+ });
495
+ }
496
+ return authorization;
497
+ }
498
+
499
+ function hasManageWorldChanges(changes = null) {
500
+ if (!changes || typeof changes !== 'object' || Array.isArray(changes)) return false;
501
+ return Object.keys(changes).some((fieldId) => fieldId !== 'adminAgentIds');
502
+ }
503
+
504
+ return {
505
+ async createWorld({
506
+ ownerAgentId,
507
+ creatorAgentId,
508
+ adminAgentIds = [],
509
+ eligibility = 'active',
510
+ broadcast = undefined,
511
+ displayName,
512
+ summary,
513
+ description,
514
+ entryProfileSchema,
515
+ interactionRules,
516
+ prohibitedRules,
517
+ ratingRules,
518
+ sessionTemplate,
519
+ enabled = false,
520
+ } = {}) {
521
+ const storeBacked = assertStore();
522
+ const resolvedOwnerAgentId = assertActorAgent(ownerAgentId || creatorAgentId);
523
+ const resolvedDisplayName = normalizeText(displayName, null);
524
+ if (!resolvedDisplayName) throw createInvalidWorldRequestError('displayName');
525
+ const resolvedSummary = normalizeText(summary, null);
526
+ if (!resolvedSummary) throw createInvalidWorldRequestError('summary');
527
+ const resolvedDescription = normalizeText(description, resolvedSummary);
528
+ const resolvedInteractionRules = normalizeText(interactionRules, null);
529
+ if (!resolvedInteractionRules) throw createInvalidWorldRequestError('interactionRules');
530
+ const resolvedProhibitedRules = normalizeText(prohibitedRules, null);
531
+ if (!resolvedProhibitedRules) throw createInvalidWorldRequestError('prohibitedRules');
532
+ const resolvedRatingRules = normalizeText(ratingRules, null);
533
+ if (!resolvedRatingRules) throw createInvalidWorldRequestError('ratingRules');
534
+
535
+ const normalizedEntryProfileSchema = normalizeEntryProfileSchema(entryProfileSchema);
536
+ const normalizedAdminAgentIds = normalizeAdminAgentIds(adminAgentIds, {
537
+ creatorAgentId: resolvedOwnerAgentId,
538
+ store: storeBacked,
539
+ });
540
+ const worldId = buildWorldId(resolvedDisplayName, worldService.listWorldIds());
541
+ const worldRecord = buildWorldRecord({
542
+ worldId,
543
+ creatorAgentId: resolvedOwnerAgentId,
544
+ adminAgentIds: normalizedAdminAgentIds,
545
+ eligibility,
546
+ broadcast,
547
+ displayName: resolvedDisplayName,
548
+ summary: resolvedSummary,
549
+ description: resolvedDescription,
550
+ interactionRules: resolvedInteractionRules,
551
+ prohibitedRules: resolvedProhibitedRules,
552
+ ratingRules: resolvedRatingRules,
553
+ entryProfileSchema: normalizedEntryProfileSchema,
554
+ sessionTemplate,
555
+ enabled: normalizeBoolean(enabled, false),
556
+ });
557
+
558
+ const created = await storeBacked.createWorldConfig(worldRecord);
559
+ const normalizedWorld = worldService.requireWorld(created.worldId, { includeDisabled: true });
560
+
561
+ return projectManagedWorld(storeBacked, normalizedWorld, {
562
+ worldRole: WORLD_ROLES.OWNER,
563
+ });
564
+ },
565
+ listManagedWorlds({ actorAgentId, creatorAgentId, includeDisabled = true } = {}) {
566
+ const storeBacked = assertStore();
567
+ const resolvedActorAgentId = assertActorAgent(actorAgentId || creatorAgentId);
568
+ return worldAuthorizationService
569
+ .listManagedWorlds({ actorAgentId: resolvedActorAgentId, includeDisabled })
570
+ .map((context) => projectManagedWorldSummary(storeBacked, context.world, {
571
+ worldRole: context.worldRole,
572
+ }));
573
+ },
574
+ listOwnedWorlds(input = {}) {
575
+ return this.listManagedWorlds(input);
576
+ },
577
+ getManagedWorld({ actorAgentId, creatorAgentId, worldId } = {}) {
578
+ const storeBacked = assertStore();
579
+ const resolvedActorAgentId = assertActorAgent(actorAgentId || creatorAgentId);
580
+ const authorization = requireWorldAction({
581
+ worldId,
582
+ actorAgentId: resolvedActorAgentId,
583
+ action: WORLD_ACTIONS.VIEW_MANAGEMENT,
584
+ });
585
+ return projectManagedWorld(storeBacked, authorization.world, {
586
+ worldRole: authorization.worldRole,
587
+ });
588
+ },
589
+ async manageWorld({ actorAgentId, creatorAgentId, worldId, changes = null, enabled = null } = {}) {
590
+ const storeBacked = assertStore();
591
+ const resolvedActorAgentId = assertActorAgent(actorAgentId || creatorAgentId);
592
+ const hasChanges = changes && typeof changes === 'object' && !Array.isArray(changes);
593
+
594
+ let authorization = requireWorldAction({
595
+ worldId,
596
+ actorAgentId: resolvedActorAgentId,
597
+ action: WORLD_ACTIONS.VIEW_MANAGEMENT,
598
+ });
599
+ if (hasManageWorldChanges(changes)) {
600
+ authorization = requireWorldAction({
601
+ worldId,
602
+ actorAgentId: resolvedActorAgentId,
603
+ action: WORLD_ACTIONS.MANAGE_WORLD,
604
+ });
605
+ }
606
+ if (hasChanges && Object.prototype.hasOwnProperty.call(changes, 'adminAgentIds')) {
607
+ authorization = requireWorldAction({
608
+ worldId,
609
+ actorAgentId: resolvedActorAgentId,
610
+ action: WORLD_ACTIONS.MANAGE_WORLD_ROLES,
611
+ });
612
+ }
613
+ if (enabled != null) {
614
+ authorization = requireWorldAction({
615
+ worldId,
616
+ actorAgentId: resolvedActorAgentId,
617
+ action: WORLD_ACTIONS.CHANGE_ENABLED_STATE,
618
+ });
619
+ }
620
+
621
+ const existingWorld = authorization.world;
622
+ if (!hasChanges && enabled == null) {
623
+ return projectManagedWorld(storeBacked, existingWorld, {
624
+ worldRole: authorization.worldRole,
625
+ });
626
+ }
627
+
628
+ let nextSchemaVersion = Number(existingWorld.schemaVersion || 1);
629
+ let nextRecord = existingWorld;
630
+
631
+ if (hasChanges) {
632
+ const nextDisplayName = normalizeText(changes.displayName, existingWorld.displayName);
633
+ const nextSummary = normalizeText(changes.summary, existingWorld.summary);
634
+ const nextDescription = normalizeText(changes.description, existingWorld.description);
635
+ const nextInteractionRules = normalizeText(changes.interactionRules, existingWorld.interactionRules);
636
+ const nextProhibitedRules = normalizeText(changes.prohibitedRules, existingWorld.prohibitedRules);
637
+ const nextRatingRules = normalizeText(changes.ratingRules, existingWorld.ratingRules);
638
+ const nextSessionTemplate = Object.prototype.hasOwnProperty.call(changes, 'sessionTemplate')
639
+ ? changes.sessionTemplate
640
+ : undefined;
641
+ const nextAdminAgentIds = Object.prototype.hasOwnProperty.call(changes, 'adminAgentIds')
642
+ ? normalizeAdminAgentIds(changes.adminAgentIds, {
643
+ creatorAgentId: existingWorld.creatorAgentId,
644
+ store: storeBacked,
645
+ })
646
+ : (Array.isArray(existingWorld.adminAgentIds) ? existingWorld.adminAgentIds : []);
647
+ const nextEligibility = Object.prototype.hasOwnProperty.call(changes, 'eligibility')
648
+ ? normalizeWorldEligibility(changes.eligibility, existingWorld.eligibility || 'active')
649
+ : normalizeWorldEligibility(existingWorld.eligibility, 'active');
650
+ const nextBroadcast = Object.prototype.hasOwnProperty.call(changes, 'broadcast')
651
+ ? changes.broadcast
652
+ : undefined;
653
+ let entryProfileSchema = null;
654
+
655
+ if (Object.prototype.hasOwnProperty.call(changes, 'entryProfileSchema')) {
656
+ entryProfileSchema = normalizeEntryProfileSchema(changes.entryProfileSchema);
657
+ nextSchemaVersion += 1;
658
+ }
659
+
660
+ nextRecord = buildWorldRecord({
661
+ worldId: existingWorld.worldId,
662
+ creatorAgentId: existingWorld.creatorAgentId,
663
+ adminAgentIds: nextAdminAgentIds,
664
+ eligibility: nextEligibility,
665
+ broadcast: nextBroadcast,
666
+ displayName: nextDisplayName,
667
+ summary: nextSummary,
668
+ description: nextDescription,
669
+ interactionRules: nextInteractionRules,
670
+ prohibitedRules: nextProhibitedRules,
671
+ ratingRules: nextRatingRules,
672
+ entryProfileSchema: entryProfileSchema || normalizeEntryProfileSchema(projectEntryProfileSchema(existingWorld)),
673
+ sessionTemplate: nextSessionTemplate,
674
+ existingSessionTemplate: existingWorld.sessionTemplate || null,
675
+ existingBroadcast: existingWorld.broadcast || null,
676
+ enabled: enabled == null ? existingWorld.enabled === true : normalizeBoolean(enabled, false),
677
+ status: enabled == null
678
+ ? existingWorld.status
679
+ : (normalizeBoolean(enabled, false) ? 'enabled' : 'disabled'),
680
+ schemaVersion: nextSchemaVersion,
681
+ existingMetrics: existingWorld.metrics || null,
682
+ });
683
+ } else if (enabled != null) {
684
+ nextRecord = {
685
+ ...existingWorld,
686
+ enabled: normalizeBoolean(enabled, false),
687
+ status: normalizeBoolean(enabled, false) ? 'enabled' : 'disabled',
688
+ meta: {
689
+ ...(existingWorld.meta || {}),
690
+ status: normalizeBoolean(enabled, false) ? 'creator_enabled' : 'creator_disabled',
691
+ },
692
+ };
693
+ }
694
+
695
+ const updated = await storeBacked.updateWorldConfig(existingWorld.worldId, nextRecord);
696
+ if (nextSchemaVersion !== Number(existingWorld.schemaVersion || 1)) {
697
+ await markMembershipsStale(existingWorld.worldId);
698
+ }
699
+ const normalizedWorld = worldService.requireWorld(updated.worldId, { includeDisabled: true });
700
+ return projectManagedWorld(storeBacked, normalizedWorld, {
701
+ worldRole: authorization.worldRole,
702
+ });
703
+ },
704
+ };
705
+ }