@xfxstudio/claworld 0.2.4 → 0.2.6

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.
@@ -8,7 +8,7 @@
8
8
  ],
9
9
  "name": "Claworld Persona Relay",
10
10
  "description": "Claworld relay world channel plugin for OpenClaw.",
11
- "version": "0.2.4",
11
+ "version": "0.2.6",
12
12
  "configSchema": {
13
13
  "type": "object",
14
14
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfxstudio/claworld",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Claworld channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -18,6 +18,19 @@ import {
18
18
  runClaworldInstallerUpdate,
19
19
  } from './core.js';
20
20
 
21
+ function resolveCliHomeDir(env = process.env) {
22
+ const envHomeDir = String(
23
+ env.HOME
24
+ || env.USERPROFILE
25
+ || (
26
+ env.HOMEDRIVE && env.HOMEPATH
27
+ ? path.join(env.HOMEDRIVE, env.HOMEPATH)
28
+ : ''
29
+ ),
30
+ ).trim();
31
+ return envHomeDir || os.homedir();
32
+ }
33
+
21
34
  function printHelp() {
22
35
  console.log(`Usage: ${CLAWORLD_INSTALLER_BIN_NAME} <command> [options]
23
36
 
@@ -79,7 +92,7 @@ function nextValue(argv, index) {
79
92
  }
80
93
 
81
94
  export function parseInstallerCliArgs(argv = process.argv.slice(2), env = process.env) {
82
- const homeDir = os.homedir();
95
+ const homeDir = resolveCliHomeDir(env);
83
96
  const options = {
84
97
  command: null,
85
98
  json: false,
@@ -36,6 +36,15 @@ export const DEFAULT_VERIFICATION_DELAY_MS = 1_000;
36
36
  export const seedManagedWorkspace = seedManagedWorkspaceContract;
37
37
  const TRACKED_PLUGIN_UPDATEABLE_SOURCES = new Set(['npm', 'marketplace']);
38
38
 
39
+ function resolveRequireGatewayRunning(env = process.env) {
40
+ const normalized = normalizeText(env?.CLAWORLD_INSTALLER_REQUIRE_GATEWAY_RUNNING, null);
41
+ if (!normalized) return true;
42
+ const lowered = normalized.toLowerCase();
43
+ if (['0', 'false', 'no', 'off'].includes(lowered)) return false;
44
+ if (['1', 'true', 'yes', 'on'].includes(lowered)) return true;
45
+ return true;
46
+ }
47
+
39
48
  function normalizeComparablePath(value) {
40
49
  if (!value) return null;
41
50
  return path.resolve(String(value));
@@ -1224,6 +1233,7 @@ export async function verifyClaworldInstall({
1224
1233
  dryRun = false,
1225
1234
  attempts = DEFAULT_VERIFICATION_ATTEMPTS,
1226
1235
  delayMs = DEFAULT_VERIFICATION_DELAY_MS,
1236
+ requireGatewayRunning = resolveRequireGatewayRunning(env),
1227
1237
  } = {}) {
1228
1238
  let lastResult = null;
1229
1239
 
@@ -1283,7 +1293,7 @@ export async function verifyClaworldInstall({
1283
1293
  const bindingReady = Boolean(bindingLine);
1284
1294
 
1285
1295
  lastResult = {
1286
- ok: pluginReady && gatewayRunning && channelReady && bindingReady,
1296
+ ok: pluginReady && channelReady && bindingReady && (!requireGatewayRunning || gatewayRunning),
1287
1297
  attempt,
1288
1298
  plugin,
1289
1299
  gatewayStatus,
@@ -1293,6 +1303,7 @@ export async function verifyClaworldInstall({
1293
1303
  gatewayRunning,
1294
1304
  channelReady,
1295
1305
  bindingReady,
1306
+ requireGatewayRunning,
1296
1307
  };
1297
1308
 
1298
1309
  if (lastResult.ok) {
@@ -1488,6 +1488,45 @@ function createDeliveryReplyDispatcher({
1488
1488
  };
1489
1489
  }
1490
1490
 
1491
+ async function runDeliveryReplyDispatch({
1492
+ runtime,
1493
+ currentCfg,
1494
+ relayClient,
1495
+ deliveryId,
1496
+ sessionKey,
1497
+ localAgentId,
1498
+ allowReply,
1499
+ logger,
1500
+ runtimeAccountId,
1501
+ inboundCtx,
1502
+ } = {}) {
1503
+ const { dispatcher, replyOptions, markDispatchIdle, didReply, getRuntimeOutputSummary } = createDeliveryReplyDispatcher({
1504
+ runtime,
1505
+ currentCfg,
1506
+ relayClient,
1507
+ deliveryId,
1508
+ sessionKey,
1509
+ localAgentId,
1510
+ allowReply,
1511
+ logger,
1512
+ runtimeAccountId,
1513
+ });
1514
+
1515
+ const dispatchResult = await runtime.channel.reply.dispatchReplyFromConfig({
1516
+ ctx: inboundCtx,
1517
+ cfg: currentCfg,
1518
+ dispatcher,
1519
+ replyOptions,
1520
+ });
1521
+ await markDispatchIdle();
1522
+
1523
+ return {
1524
+ dispatchResult,
1525
+ replied: didReply(),
1526
+ runtimeOutputSummary: getRuntimeOutputSummary(),
1527
+ };
1528
+ }
1529
+
1491
1530
  function resolveBoundLocalAgentId({ cfg = {}, runtimeConfig = {}, relayClient } = {}) {
1492
1531
  const accountId = resolveNormalizedText(runtimeConfig.accountId, null);
1493
1532
  const bindings = Array.isArray(cfg?.bindings) ? cfg.bindings : [];
@@ -1662,7 +1701,11 @@ async function maybeBridgeRuntimeDelivery({
1662
1701
  commandAuthorized,
1663
1702
  });
1664
1703
 
1665
- const { dispatcher, replyOptions, markDispatchIdle, didReply, getRuntimeOutputSummary } = createDeliveryReplyDispatcher({
1704
+ let {
1705
+ dispatchResult,
1706
+ replied,
1707
+ runtimeOutputSummary,
1708
+ } = await runDeliveryReplyDispatch({
1666
1709
  runtime,
1667
1710
  currentCfg,
1668
1711
  relayClient,
@@ -1672,16 +1715,52 @@ async function maybeBridgeRuntimeDelivery({
1672
1715
  allowReply: metadata.allowReply !== false,
1673
1716
  logger,
1674
1717
  runtimeAccountId,
1718
+ inboundCtx,
1675
1719
  });
1676
- const dispatchResult = await runtime.channel.reply.dispatchReplyFromConfig({
1677
- ctx: inboundCtx,
1678
- cfg: currentCfg,
1679
- dispatcher,
1680
- replyOptions,
1681
- });
1682
- await markDispatchIdle();
1683
- const replied = didReply();
1684
- const runtimeOutputSummary = getRuntimeOutputSummary();
1720
+
1721
+ const shouldRetryKickoffDispatch = (
1722
+ metadata.deliveryType === 'kickoff'
1723
+ && metadata.allowReply !== false
1724
+ && replied !== true
1725
+ && runtimeOutputSummary.counts.final > 0
1726
+ && runtimeOutputSummary.counts.operationalNotice > 0
1727
+ && runtimeOutputSummary.counts.final === runtimeOutputSummary.counts.operationalNotice
1728
+ && runtimeOutputSummary.counts.block === 0
1729
+ && runtimeOutputSummary.counts.tool === 0
1730
+ && runtimeOutputSummary.counts.partial === 0
1731
+ && runtimeOutputSummary.counts.reasoning === 0
1732
+ && runtimeOutputSummary.counts.toolStart === 0
1733
+ && runtimeOutputSummary.counts.assistantMessageStart === 0
1734
+ && runtimeOutputSummary.counts.reasoningEnd === 0
1735
+ && runtimeOutputSummary.counts.compactionStart === 0
1736
+ && runtimeOutputSummary.counts.compactionEnd === 0
1737
+ );
1738
+
1739
+ if (shouldRetryKickoffDispatch) {
1740
+ logger.warn?.(`[claworld:${runtimeAccountId}] kickoff delivery produced only operational notices; retrying dispatch once`, {
1741
+ deliveryId,
1742
+ sessionKey,
1743
+ localAgentId,
1744
+ runtimeOutputSummary,
1745
+ });
1746
+
1747
+ ({
1748
+ dispatchResult,
1749
+ replied,
1750
+ runtimeOutputSummary,
1751
+ } = await runDeliveryReplyDispatch({
1752
+ runtime,
1753
+ currentCfg,
1754
+ relayClient,
1755
+ deliveryId,
1756
+ sessionKey,
1757
+ localAgentId,
1758
+ allowReply: metadata.allowReply !== false,
1759
+ logger,
1760
+ runtimeAccountId,
1761
+ inboundCtx,
1762
+ }));
1763
+ }
1685
1764
 
1686
1765
  logger.info?.(`[claworld:${runtimeAccountId}] delivery bridge completed`, {
1687
1766
  deliveryId,
@@ -435,8 +435,11 @@ export function resolveClaworldRuntimeConfig(config = {}, accountId = null) {
435
435
  export function resolveClaworldChannelAccount(config = {}, accountId = null) {
436
436
  const runtimeConfig = resolveClaworldRuntimeConfig(config, accountId);
437
437
  const inspection = inspectClaworldChannelAccount(config, accountId);
438
+ // Keep the steady-state credential nested under relay/runtimeConfig so generic
439
+ // OpenClaw status does not misclassify Claworld as a bot+app token channel.
440
+ const { appToken: _appToken, ...statusAccount } = inspection;
438
441
  return {
439
- ...inspection,
442
+ ...statusAccount,
440
443
  runtimeReady: true,
441
444
  resolvedFrom: accountId ? 'requested_account' : 'default_account',
442
445
  runtimeConfig,
@@ -3,6 +3,8 @@ import {
3
3
  projectToolChatRequestListResponse,
4
4
  projectToolChatRequestMutationResponse,
5
5
  projectToolCreateWorldResponse,
6
+ projectToolManagedWorldResponse,
7
+ projectToolOwnedWorldsResponse,
6
8
  projectToolWorldList,
7
9
  projectToolFeedbackSubmissionResponse,
8
10
  projectToolJoinWorldResponse,
@@ -502,6 +504,120 @@ function buildRegisteredTools(api, plugin) {
502
504
  return buildToolResult(projectToolCreateWorldResponse(payload, { accountId: context.accountId }));
503
505
  },
504
506
  },
507
+ {
508
+ name: 'claworld_list_owned_worlds',
509
+ label: 'Claworld List Owned Worlds',
510
+ description: 'Owner-focused governance tool. List the worlds owned by the current account before choosing one to manage.',
511
+ metadata: buildToolMetadata({
512
+ category: 'world_management',
513
+ usageNotes: [
514
+ 'Use when the user wants to inspect or pick from worlds they own.',
515
+ 'This tool only returns owned worlds, not general directory worlds.',
516
+ 'Pair this with claworld_manage_world for owner-only worldContext updates.',
517
+ ],
518
+ examples: [
519
+ {
520
+ title: 'List owned worlds',
521
+ input: {
522
+ accountId: 'claworld',
523
+ },
524
+ outcome: 'Returns owner-managed worlds for the current account.',
525
+ },
526
+ ],
527
+ }),
528
+ parameters: objectParam({
529
+ description: 'Minimal payload for listing worlds owned by the current account.',
530
+ required: ['accountId'],
531
+ properties: {
532
+ accountId: accountIdProperty,
533
+ includeDisabled: {
534
+ type: 'boolean',
535
+ description: 'Whether to include disabled or draft owned worlds.',
536
+ },
537
+ },
538
+ examples: [
539
+ {
540
+ accountId: 'claworld',
541
+ },
542
+ ],
543
+ }),
544
+ async execute(_toolCallId, params = {}) {
545
+ const context = await resolveToolContext(api, plugin, params);
546
+ const payload = await plugin.runtime.productShell.moderation.listOwnedWorlds({
547
+ ...context,
548
+ includeDisabled: params.includeDisabled !== false,
549
+ });
550
+ return buildToolResult(projectToolOwnedWorldsResponse(payload, { accountId: context.accountId }));
551
+ },
552
+ },
553
+ {
554
+ name: 'claworld_manage_world',
555
+ label: 'Claworld Manage World',
556
+ description: 'Owner-only world management tool. Update the canonical worldContextText for one owner-managed world, with optional displayName/enabled changes.',
557
+ metadata: buildToolMetadata({
558
+ category: 'world_management',
559
+ usageNotes: [
560
+ 'Use only when the current agent owns the target world.',
561
+ 'This is the minimal management tool on the current public surface.',
562
+ 'Prefer updating worldContextText directly instead of editing legacy world schema fields.',
563
+ ],
564
+ examples: [
565
+ {
566
+ title: 'Update one owned world context',
567
+ input: {
568
+ accountId: 'claworld',
569
+ worldId: 'ugc-weekend-debate-club',
570
+ worldContextText: '世界:Weekend Debate Club [ugc-weekend-debate-club]\n简介:A creator-managed world for short structured debates.\n互动规则:Debate one topic at a time and stay concise.\n结果要求:Leave one clear 1 to 10 rating.',
571
+ },
572
+ outcome: 'Returns the updated managed-world projection when the current agent is the owner.',
573
+ },
574
+ ],
575
+ }),
576
+ parameters: objectParam({
577
+ description: 'Minimal owner-only world management payload.',
578
+ required: ['accountId', 'worldId', 'worldContextText'],
579
+ properties: {
580
+ accountId: accountIdProperty,
581
+ worldId: worldIdProperty,
582
+ worldContextText: stringParam({
583
+ description: 'Replacement canonical world context text for the owned world.',
584
+ minLength: 1,
585
+ examples: ['世界:Weekend Debate Club [ugc-weekend-debate-club]\n简介:A creator-managed world for short structured debates.\n互动规则:Debate one topic at a time and stay concise.\n结果要求:Leave one clear 1 to 10 rating.'],
586
+ }),
587
+ displayName: stringParam({
588
+ description: 'Optional new display name for the owned world.',
589
+ minLength: 1,
590
+ examples: ['Weekend Debate Club'],
591
+ }),
592
+ enabled: {
593
+ type: 'boolean',
594
+ description: 'Optional enabled flag for the owned world.',
595
+ },
596
+ },
597
+ examples: [
598
+ {
599
+ accountId: 'claworld',
600
+ worldId: 'ugc-weekend-debate-club',
601
+ worldContextText: '世界:Weekend Debate Club [ugc-weekend-debate-club]\n简介:A creator-managed world for short structured debates.\n互动规则:Debate one topic at a time and stay concise.\n结果要求:Leave one clear 1 to 10 rating.',
602
+ },
603
+ ],
604
+ }),
605
+ async execute(_toolCallId, params = {}) {
606
+ const context = await resolveToolContext(api, plugin, params);
607
+ const changes = {
608
+ worldContextText: params.worldContextText,
609
+ ...(params.displayName ? { displayName: params.displayName } : {}),
610
+ };
611
+ const payload = await plugin.runtime.productShell.moderation.manageWorld({
612
+ ...context,
613
+ worldId: params.worldId,
614
+ mode: 'patch',
615
+ changes,
616
+ ...(Object.prototype.hasOwnProperty.call(params, 'enabled') ? { enabled: params.enabled === true } : {}),
617
+ });
618
+ return buildToolResult(projectToolManagedWorldResponse(payload, { accountId: context.accountId }));
619
+ },
620
+ },
505
621
  {
506
622
  name: 'claworld_request_chat',
507
623
  label: 'Claworld Request Chat',
@@ -76,7 +76,7 @@ function projectParticipantContextField(field = null) {
76
76
 
77
77
  function projectWorldRole(worldRole, fallback = null) {
78
78
  const normalized = normalizeText(worldRole, fallback);
79
- return ['owner', 'admin', 'member'].includes(normalized) ? normalized : fallback;
79
+ return ['owner', 'member'].includes(normalized) ? normalized : fallback;
80
80
  }
81
81
 
82
82
  function projectWorldStats(stats = null) {
@@ -22,6 +22,8 @@ export const CLAWORLD_WORLD_TOOL_NAMES = Object.freeze([
22
22
 
23
23
  export const CLAWORLD_WORLD_ADMIN_PUBLIC_TOOL_NAMES = Object.freeze([
24
24
  'claworld_create_world',
25
+ 'claworld_list_owned_worlds',
26
+ 'claworld_manage_world',
25
27
  ]);
26
28
 
27
29
  export const CLAWORLD_COMPATIBILITY_TOOL_NAMES = Object.freeze([
@@ -35,8 +37,6 @@ export const CLAWORLD_RETIRED_PUBLIC_TOOL_NAMES = Object.freeze([
35
37
  'claworld_accept_friend_request',
36
38
  'claworld_reject_friend_request',
37
39
  'claworld_broadcast_world',
38
- 'claworld_list_owned_worlds',
39
- 'claworld_manage_world',
40
40
  'claworld_resolve_agent',
41
41
  ]);
42
42
 
@@ -76,7 +76,7 @@ function normalizeWorldStats(stats = null) {
76
76
 
77
77
  function normalizeWorldRole(worldRole, fallback = null) {
78
78
  const normalized = normalizeText(worldRole, fallback);
79
- return ['owner', 'admin', 'member'].includes(normalized) ? normalized : fallback;
79
+ return ['owner', 'member'].includes(normalized) ? normalized : fallback;
80
80
  }
81
81
 
82
82
  function normalizeManagedWorld(payload = {}) {
@@ -2,283 +2,37 @@ export const DEFAULT_WORLD_MANIFESTS = Object.freeze([
2
2
  {
3
3
  worldId: 'dating-demo-world',
4
4
  displayName: 'Dating Demo World',
5
- summary: 'Mutual-interest matching world for proving the A2A conversation loop before human handoff.',
5
+ summary: 'A lightweight social world for proving world-scoped join, review, and chat request flow.',
6
6
  description:
7
- 'A lightweight social world for people who want their agents to screen for mutual fit before deciding whether a human handoff is worthwhile.',
7
+ 'A lightweight social world where agents compare fit briefly before deciding whether to continue beyond agent chat.',
8
8
  category: 'social',
9
9
  lifecycle: 'prototype',
10
10
  tags: ['dating', 'matching', 'a2a'],
11
- interactionRules:
12
- 'Both agents should clarify fit quickly, stay respectful, avoid over-sharing, and stop once both sides can decide whether to continue beyond agent chat.',
13
- prohibitedRules:
14
- 'Do not pressure, harass, manipulate, or ask for unsafe personal details. Do not continue pushing once the other side clearly signals discomfort or disinterest.',
15
- ratingRules:
16
- 'When the interaction ends, each agent should rate the other side from 1 to 10 based on mutual fit, conversational quality, and respect for the world rules.',
17
- roles: [
18
- {
19
- roleId: 'seeker',
20
- label: 'Seeker',
21
- objective: 'Find a compatible person and decide whether to raise hand.',
22
- promptSummary: 'Represent your human owner faithfully and optimize for respectful fit.',
23
- },
24
- {
25
- roleId: 'candidate',
26
- label: 'Candidate',
27
- objective: 'Assess alignment and decide whether to continue beyond agent-only chat.',
28
- promptSummary: 'Share enough context to evaluate fit while staying concise and safe.',
29
- },
30
- ],
31
- joinSchema: {
32
- requiredFields: [
33
- {
34
- fieldId: 'headline',
35
- label: 'Headline',
36
- description: 'One-line self introduction shown during initial discovery.',
37
- examples: ['Shanghai-based product lead who likes trail running'],
38
- },
39
- {
40
- fieldId: 'intent',
41
- label: 'Intent',
42
- description: 'What kind of connection the user is open to.',
43
- examples: ['serious relationship', 'new friends first'],
44
- },
45
- {
46
- fieldId: 'location',
47
- label: 'Location',
48
- description: 'Current city or region used for basic filtering.',
49
- examples: ['Shanghai'],
50
- },
51
- ],
52
- optionalFields: [
53
- {
54
- fieldId: 'interests',
55
- label: 'Interests',
56
- type: 'string[]',
57
- description: 'Interests or hobbies used for match prompts.',
58
- examples: ['running', 'indie films', 'cats'],
59
- },
60
- {
61
- fieldId: 'conversationStyle',
62
- label: 'Conversation Style',
63
- description: 'Tone preference for the agent during A2A chat.',
64
- examples: ['playful but direct'],
65
- },
66
- ],
67
- hints: [
68
- 'Keep profile fields concrete so both matching and prompt injection stay stable.',
69
- 'World-level rules should decide when two agents have effectively reached a human handoff threshold.',
70
- ],
71
- },
72
- searchSchema: {
73
- mode: 'profile_overlap_search',
74
- inputFieldIds: ['intent', 'location', 'interests'],
75
- summary:
76
- 'Compatibility-only manual search over active online members by intent, location, and shared interests. Candidate feed review is the canonical path before request_chat.',
77
- hints: [
78
- 'Search defaults to the viewer membership profile when no explicit query is provided.',
79
- 'Only online members with an active world membership are returned.',
80
- ],
81
- },
82
- matching: {
83
- mode: 'scored_push',
84
- cadence: 'periodic',
85
- strategySummary:
86
- 'Score active online memberships by intent, location, and interests, deliver candidate summaries first, and route live contact through request_chat after review.',
87
- candidateSources: ['active_memberships_online'],
88
- },
89
- conversationTemplate: {
90
- mode: 'a2a',
91
- worldRules: {
92
- openingText: 'You are in the dating demo world. Clarify fit quickly and stop once both sides can decide whether to continue.',
93
- turnMessageRules: [{ id: 'nudge-2', atTurn: 2, templateRef: 'world.turn.nudge' }],
94
- convergence: {
95
- whenRemainingTurnsLTE: 1,
96
- text: 'Focus on unresolved blockers and whether both sides have enough information to decide next steps.',
97
- },
98
- },
99
- },
100
- resultContract: {
101
- schemaId: 'dating-demo-world.result.v1',
102
- outputs: ['match_score', 'recommendation', 'risks', 'evidence'],
103
- successCriteria: ['both agents can summarize fit', 'next step is explicit'],
104
- exampleSignals: {
105
- intentSignals: [{ id: 'intent-1', type: 'intent_match', score: 0.8, summary: 'Stated intent aligns.' }],
106
- conversationSignals: [
107
- { id: 'conv-1', type: 'next_step_ready', score: 0.7, summary: 'One side is ready to stop and move forward.' },
108
- { id: 'conv-2', type: 'next_step_ready', score: 0.7, summary: 'The other side is also ready to stop.' },
109
- ],
110
- agentSignals: [{ id: 'agent-1', type: 'safety', risk: 0.1, summary: 'No major risk surfaced.' }],
111
- },
112
- },
11
+ worldContextText:
12
+ '世界:Dating Demo World [dating-demo-world]\n简介:A lightweight social world for proving world-scoped join, review, and chat request flow.\n互动规则:Clarify fit quickly, stay respectful, and stop once next steps are clear.\n禁止事项:Do not pressure, harass, manipulate, or ask for unsafe personal details.\n结果要求:Rate the interaction from 1 to 10 based on fit, clarity, and respect.',
113
13
  },
114
14
  {
115
15
  worldId: 'skill-handoff-world',
116
16
  displayName: 'Skill Handoff World',
117
- summary: 'Supply-demand world for small scoped skill requests before full project delivery and escrow are introduced.',
17
+ summary: 'A small scoped skill-handoff world for proving service-fit review before direct contact.',
118
18
  description:
119
- 'A service marketplace world where agents help their owners quickly screen for delivery fit before moving to a direct human conversation.',
19
+ 'A service marketplace world where agents quickly screen delivery fit before moving to direct human contact.',
120
20
  category: 'marketplace',
121
21
  lifecycle: 'prototype',
122
22
  tags: ['skills', 'services', 'matching'],
123
- interactionRules:
124
- 'Agents should clarify scope, constraints, expected deliverables, and whether a direct handoff is justified. Keep the exchange concrete and decision-oriented.',
125
- prohibitedRules:
126
- 'Do not misrepresent capabilities, hide obvious delivery blockers, or pressure the other side into a commitment without clear scope alignment.',
127
- ratingRules:
128
- 'At the end of the exchange, each agent should rate the other side from 1 to 10 based on scope clarity, responsiveness, and confidence in a productive handoff.',
129
- roles: [
130
- {
131
- roleId: 'buyer',
132
- label: 'Buyer',
133
- objective: 'Describe a task crisply enough for rapid screening.',
134
- },
135
- {
136
- roleId: 'seller',
137
- label: 'Seller',
138
- objective: 'Assess fit, delivery scope, and whether to continue to a human handoff.',
139
- },
140
- ],
141
- joinSchema: {
142
- requiredFields: [
143
- {
144
- fieldId: 'headline',
145
- label: 'Headline',
146
- description: 'What the user can buy or sell in one line.',
147
- },
148
- {
149
- fieldId: 'capabilities',
150
- label: 'Capabilities',
151
- type: 'string[]',
152
- description: 'Skills or request categories used during matching.',
153
- examples: ['typescript', 'prompt design', 'landing page'],
154
- },
155
- ],
156
- optionalFields: [
157
- {
158
- fieldId: 'budgetBand',
159
- label: 'Budget Band',
160
- description: 'Optional budget or rate band for screening.',
161
- examples: ['200-500 USD', '50 USD/hour'],
162
- },
163
- ],
164
- hints: ['Use this world to prove supply-demand matching before implementing transaction settlement.'],
165
- },
166
- searchSchema: {
167
- mode: 'capability_overlap_search',
168
- inputFieldIds: ['capabilities', 'budgetBand'],
169
- summary:
170
- 'Compatibility-only manual search over active online members by capability overlap and optional budget fit. Candidate feed review is the canonical path before request_chat.',
171
- },
172
- matching: {
173
- mode: 'intent_filter',
174
- cadence: 'on_demand',
175
- strategySummary:
176
- 'Filter active online world members by capability overlap, deliver candidate summaries first, and let members request_chat before negotiating fit in a short session.',
177
- candidateSources: ['active_memberships_online'],
178
- },
179
- conversationTemplate: {
180
- mode: 'a2a',
181
- worldRules: {
182
- openingText: 'Clarify scope, constraints, and whether a human handoff is justified.',
183
- },
184
- },
185
- resultContract: {
186
- schemaId: 'skill-handoff-world.result.v1',
187
- outputs: ['recommendation', 'risks', 'evidence'],
188
- successCriteria: ['scope is summarized', 'handoff recommendation is explicit'],
189
- exampleSignals: {
190
- intentSignals: [{ id: 'intent-1', type: 'intent_match', score: 0.65, summary: 'Capabilities appear relevant.' }],
191
- conversationSignals: [{ id: 'conv-1', type: 'human_handoff_ready', score: 0.6, summary: 'Seller is ready for human handoff.' }],
192
- agentSignals: [{ id: 'agent-1', type: 'scope_risk', risk: 0.2, summary: 'A few requirements remain vague.' }],
193
- },
194
- },
23
+ worldContextText:
24
+ '世界:Skill Handoff World [skill-handoff-world]\n简介:A service marketplace world where agents screen delivery fit before moving to direct human contact.\n互动规则:Clarify scope, constraints, and whether a handoff is justified.\n禁止事项:Do not misrepresent capabilities or hide delivery blockers.\n结果要求:Rate the exchange from 1 to 10 based on scope clarity and handoff confidence.',
195
25
  },
196
26
  {
197
27
  worldId: 'job-match-world',
198
28
  displayName: 'Job Match World',
199
- summary: 'Recruiter-candidate world optimized for profile completeness, matching, and concise A2A screening.',
29
+ summary: 'A hiring-fit world optimized for profile completeness, matching, and concise A2A screening.',
200
30
  description:
201
- 'A recruiting world for agent-assisted screening where candidates and recruiters validate fit before escalating to direct human contact.',
31
+ 'A recruiting world for agent-assisted screening where both sides validate fit before escalating to direct human contact.',
202
32
  category: 'recruiting',
203
33
  lifecycle: 'prototype',
204
34
  tags: ['jobs', 'recruiting', 'screening'],
205
- interactionRules:
206
- 'Agents should focus on role fit, experience relevance, hiring constraints, and whether a direct next step is justified. Keep the conversation concise and factual.',
207
- prohibitedRules:
208
- 'Do not fabricate experience, compensation expectations, or hiring authority. Do not request sensitive personal data unrelated to the role fit conversation.',
209
- ratingRules:
210
- 'When the interaction ends, each agent should rate the other side from 1 to 10 based on role fit, signal quality, and likelihood that a human follow-up is worthwhile.',
211
- roles: [
212
- {
213
- roleId: 'candidate',
214
- label: 'Candidate',
215
- objective: 'Surface fit and blockers quickly.',
216
- },
217
- {
218
- roleId: 'recruiter',
219
- label: 'Recruiter',
220
- objective: 'Determine whether to escalate to human contact.',
221
- },
222
- ],
223
- joinSchema: {
224
- requiredFields: [
225
- {
226
- fieldId: 'headline',
227
- label: 'Headline',
228
- description: 'Current role or target role.',
229
- },
230
- {
231
- fieldId: 'experienceSummary',
232
- label: 'Experience Summary',
233
- description: 'Condensed summary of experience relevant to matching.',
234
- },
235
- {
236
- fieldId: 'targetRole',
237
- label: 'Target Role',
238
- description: 'Role or hiring need that anchors matching.',
239
- },
240
- ],
241
- optionalFields: [
242
- {
243
- fieldId: 'location',
244
- label: 'Location',
245
- },
246
- {
247
- fieldId: 'workMode',
248
- label: 'Work Mode',
249
- description: 'Onsite, hybrid, or remote.',
250
- },
251
- ],
252
- hints: ['This world should eventually integrate search/browse, but the current shell only defines the contract.'],
253
- },
254
- searchSchema: {
255
- mode: 'profile_overlap_search',
256
- inputFieldIds: ['targetRole', 'location', 'workMode'],
257
- summary:
258
- 'Compatibility-only manual search over active online members by role fit, location, and work mode. Candidate feed review is the canonical path before request_chat.',
259
- },
260
- matching: {
261
- mode: 'profile_overlap',
262
- cadence: 'periodic',
263
- strategySummary:
264
- 'Use active online memberships plus target role, experience summary, and location/work mode as the first-pass scoring surface, deliver candidate summaries, and route contact through request_chat after review.',
265
- candidateSources: ['active_memberships_online'],
266
- },
267
- conversationTemplate: {
268
- mode: 'a2a',
269
- worldRules: {
270
- openingText: 'Focus on role fit, constraints, and whether both sides should move to a human conversation.',
271
- },
272
- },
273
- resultContract: {
274
- schemaId: 'job-match-world.result.v1',
275
- outputs: ['match_score', 'recommendation', 'risks', 'evidence'],
276
- successCriteria: ['role fit is explicit', 'human next step is explicit'],
277
- exampleSignals: {
278
- intentSignals: [{ id: 'intent-1', type: 'role_fit', score: 0.7, summary: 'Role expectations line up.' }],
279
- conversationSignals: [{ id: 'conv-1', type: 'next_step_ready', score: 0.55, summary: 'Both sides are ready to stop and proceed.' }],
280
- agentSignals: [{ id: 'agent-1', type: 'timeline_risk', risk: 0.15, summary: 'Availability still needs confirmation.' }],
281
- },
282
- },
35
+ worldContextText:
36
+ '世界:Job Match World [job-match-world]\n简介:A recruiting world for agent-assisted screening before direct human contact.\n互动规则:Focus on fit, constraints, and whether a next step is justified.\n禁止事项:Do not fabricate experience, compensation, or hiring authority.\n结果要求:Rate the interaction from 1 to 10 based on fit, clarity, and follow-up likelihood.',
283
37
  },
284
38
  ]);
@@ -77,16 +77,6 @@ function normalizeField(field = {}, index = 0, { required = false } = {}) {
77
77
  };
78
78
  }
79
79
 
80
- function normalizeRole(role = {}, index = 0) {
81
- const roleId = normalizeText(role.roleId || role.id, `role_${index + 1}`);
82
- return {
83
- roleId,
84
- label: normalizeText(role.label, roleId),
85
- objective: normalizeText(role.objective, null),
86
- promptSummary: normalizeText(role.promptSummary, null),
87
- };
88
- }
89
-
90
80
  function normalizeExampleSignals(exampleSignals = {}) {
91
81
  const normalizeGroup = (key) => (Array.isArray(exampleSignals[key]) ? exampleSignals[key] : []);
92
82
  return {
@@ -207,7 +197,6 @@ export function normalizeWorldManifest(manifest = {}, index = 0) {
207
197
  ratingRules: normalizedRatingRules,
208
198
  }),
209
199
  creatorAgentId: normalizeText(manifest.creatorAgentId, null),
210
- adminAgentIds: normalizeStringList(manifest.adminAgentIds),
211
200
  eligibility: normalizeWorldEligibility(manifest.eligibility, 'active'),
212
201
  broadcast: normalizeBroadcastConfig(manifest.broadcast, {
213
202
  enabled: manifest.broadcast?.enabled === true,
@@ -224,9 +213,6 @@ export function normalizeWorldManifest(manifest = {}, index = 0) {
224
213
  : 0,
225
214
  }
226
215
  : { totalConversationCount: 0 },
227
- roles: Array.isArray(manifest.roles)
228
- ? manifest.roles.map((role, roleIndex) => normalizeRole(role, roleIndex))
229
- : [],
230
216
  joinSchema,
231
217
  searchSchema,
232
218
  matching: {
@@ -174,7 +174,6 @@ function normalizeWorldDetail(payload = {}) {
174
174
  interactionRules: normalizeText(payload.interactionRules, null),
175
175
  prohibitedRules: normalizeText(payload.prohibitedRules, null),
176
176
  ratingRules: normalizeText(payload.ratingRules, null),
177
- adminAgentIds: normalizeStringList(payload.adminAgentIds),
178
177
  eligibility: normalizeText(payload.eligibility, 'active'),
179
178
  broadcast: normalizeBroadcastConfig(payload.broadcast),
180
179
  requiredFields,
@@ -237,7 +236,6 @@ function normalizeWorldDetail(payload = {}) {
237
236
  interactionRules: normalizeText(payload.interactionRules || world.interactionRules, null),
238
237
  prohibitedRules: normalizeText(payload.prohibitedRules || world.prohibitedRules, null),
239
238
  ratingRules: normalizeText(payload.ratingRules || world.ratingRules, null),
240
- adminAgentIds: normalizeStringList(payload.adminAgentIds || world.adminAgentIds),
241
239
  eligibility: normalizeText(payload.eligibility || world.eligibility, 'active'),
242
240
  broadcast: normalizeBroadcastConfig(payload.broadcast || world.broadcast),
243
241
  requiredFields,
@@ -132,6 +132,7 @@ export function createClaworldProductShell({
132
132
  'GET /v1/chat-requests',
133
133
  'PUT /v1/chat-requests/approval-policy',
134
134
  'POST /v1/chat-requests/:chatRequestId/accept',
135
+ 'POST /v1/chat-requests/:chatRequestId/reject',
135
136
  'GET /v1/worlds',
136
137
  'GET /v1/worlds/:worldId',
137
138
  'POST /v1/worlds',
@@ -81,9 +81,13 @@ export function createMembershipService({ worldService, store = null } = {}) {
81
81
  }
82
82
 
83
83
  return {
84
- evaluateJoin({ worldId, participantContextText = null } = {}) {
84
+ evaluateJoin({ worldId, participantContextText = null, profile = null, profileSnapshot = null } = {}) {
85
85
  const world = worldService.requireWorld(worldId);
86
- const normalizedParticipantContextText = normalizeText(participantContextText, null);
86
+ const normalizedParticipantContextText = resolveNormalizedParticipantContextText({
87
+ world,
88
+ participantContextText,
89
+ profileSnapshot: profileSnapshot || profile,
90
+ });
87
91
  const accepted = Boolean(normalizedParticipantContextText);
88
92
 
89
93
  return {
@@ -132,7 +136,7 @@ export function createMembershipService({ worldService, store = null } = {}) {
132
136
  };
133
137
  },
134
138
 
135
- async createMembership({ worldId, agentId, participantContextText } = {}) {
139
+ async createMembership({ worldId, agentId, participantContextText, profile = null, profileSnapshot = null } = {}) {
136
140
  const world = worldService.requireWorld(worldId);
137
141
  const membershipStore = assertStore();
138
142
  const normalizedAgentId = normalizeAgentId(agentId);
@@ -145,6 +149,7 @@ export function createMembershipService({ worldService, store = null } = {}) {
145
149
  world,
146
150
  agent,
147
151
  participantContextText,
152
+ profileSnapshot: profileSnapshot || profile,
148
153
  });
149
154
  if (!normalizedParticipantContextText) {
150
155
  throw createInvalidJoinRequestError(
@@ -166,7 +171,7 @@ export function createMembershipService({ worldService, store = null } = {}) {
166
171
  worldId,
167
172
  agentId: normalizedAgentId,
168
173
  status: 'joined',
169
- profileSnapshot: normalizeProfileSnapshot(null, normalizedParticipantContextText),
174
+ profileSnapshot: normalizeProfileSnapshot(profileSnapshot || profile, normalizedParticipantContextText),
170
175
  participantContextText: normalizedParticipantContextText,
171
176
  });
172
177
 
@@ -177,6 +182,8 @@ export function createMembershipService({ worldService, store = null } = {}) {
177
182
  worldId,
178
183
  agentId,
179
184
  participantContextText,
185
+ profile = null,
186
+ profileSnapshot = null,
180
187
  } = {}) {
181
188
  const world = worldService.requireWorld(worldId);
182
189
  const membershipStore = assertStore();
@@ -191,6 +198,7 @@ export function createMembershipService({ worldService, store = null } = {}) {
191
198
  world,
192
199
  agent,
193
200
  participantContextText,
201
+ profileSnapshot: profileSnapshot || profile,
194
202
  });
195
203
  if (!normalizedParticipantContextText) {
196
204
  throw createInvalidJoinRequestError(
@@ -207,14 +215,24 @@ export function createMembershipService({ worldService, store = null } = {}) {
207
215
  const membership = existingMembership
208
216
  ? await membershipStore.updateMembership(existingMembership.membershipId, {
209
217
  status: 'active',
210
- profileSnapshot: normalizeProfileSnapshot(existingMembership.profileSnapshot, normalizedParticipantContextText),
218
+ profileSnapshot: normalizeProfileSnapshot(
219
+ {
220
+ ...(existingMembership.profileSnapshot && typeof existingMembership.profileSnapshot === 'object'
221
+ ? existingMembership.profileSnapshot
222
+ : {}),
223
+ ...((profileSnapshot || profile) && typeof (profileSnapshot || profile) === 'object' && !Array.isArray(profileSnapshot || profile)
224
+ ? (profileSnapshot || profile)
225
+ : {}),
226
+ },
227
+ normalizedParticipantContextText,
228
+ ),
211
229
  participantContextText: normalizedParticipantContextText,
212
230
  })
213
231
  : await membershipStore.createMembership({
214
232
  worldId: world.worldId,
215
233
  agentId: normalizedAgentId,
216
234
  status: 'active',
217
- profileSnapshot: normalizeProfileSnapshot(null, normalizedParticipantContextText),
235
+ profileSnapshot: normalizeProfileSnapshot(profileSnapshot || profile, normalizedParticipantContextText),
218
236
  participantContextText: normalizedParticipantContextText,
219
237
  });
220
238
 
@@ -104,4 +104,24 @@ export function registerChatRequestRoutes(app, { chatRequestService, store }) {
104
104
  sendChatRequestError(res, error);
105
105
  }
106
106
  });
107
+
108
+ app.post('/v1/chat-requests/:chatRequestId/reject', async (req, res) => {
109
+ const authAgent = resolveAuthenticatedAgentId({
110
+ store,
111
+ req,
112
+ providedAgentId: req.body?.actorAgentId,
113
+ fieldName: 'actorAgentId',
114
+ });
115
+ if (!authAgent.ok) return res.status(authAgent.status).json(authAgent.body);
116
+ if (!authAgent.agentId) return sendMissingAgentIdentity(res);
117
+
118
+ try {
119
+ const result = await chatRequestService.rejectChatRequest(req.params.chatRequestId, {
120
+ actorAgentId: authAgent.agentId,
121
+ });
122
+ res.json(result);
123
+ } catch (error) {
124
+ sendChatRequestError(res, error);
125
+ }
126
+ });
107
127
  }
@@ -534,5 +534,27 @@ export function createChatRequestService({
534
534
  nextAction: resolveAcceptNextAction(kickoff),
535
535
  };
536
536
  },
537
+
538
+ async rejectChatRequest(chatRequestId, { actorAgentId } = {}) {
539
+ requireAgent(actorAgentId);
540
+ const normalizedChatRequestId = normalizeText(chatRequestId, null);
541
+ if (!normalizedChatRequestId) {
542
+ throw createInvalidChatRequestError('chat_request_id_required', 'chatRequestId is required');
543
+ }
544
+
545
+ const result = await assertRelay('rejectChatRequest').rejectChatRequest(normalizedChatRequestId, {
546
+ actorAgentId,
547
+ });
548
+ if (result.status < 200 || result.status >= 300) {
549
+ throw createRelayResponseError(result, 'chat_request_reject_failed');
550
+ }
551
+
552
+ return {
553
+ status: 'rejected',
554
+ verdict: 'manual_reject',
555
+ chatRequest: projectChatRequest(result.body || {}, actorAgentId),
556
+ nextAction: 'request_rejected_by_peer',
557
+ };
558
+ },
537
559
  };
538
560
  }
@@ -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() {
@@ -193,42 +162,36 @@ function buildWorldRecord({
193
162
  creatorAgentId,
194
163
  displayName,
195
164
  summary,
196
- description,
197
- interactionRules,
198
- prohibitedRules,
199
- ratingRules,
200
165
  worldContextText = null,
201
166
  enabled = false,
202
167
  status = null,
203
- schemaVersion = 1,
204
168
  existingMetrics = null,
205
- existingConversationTemplate = null,
206
169
  } = {}) {
207
170
  const resolvedStatus = status || (enabled ? 'enabled' : 'draft');
208
171
  const participantContextField = buildDefaultEntryProfileField();
172
+ const resolvedWorldContextText = buildWorldContextText({
173
+ worldId,
174
+ displayName,
175
+ summary,
176
+ worldContextText,
177
+ interactionRules: null,
178
+ prohibitedRules: null,
179
+ ratingRules: null,
180
+ });
209
181
 
210
182
  return {
211
183
  worldId,
212
184
  slug: slugify(displayName, worldId),
213
185
  displayName,
214
186
  summary,
215
- description,
187
+ description: resolvedWorldContextText,
216
188
  category: 'ugc',
217
189
  lifecycle: 'creator_managed',
218
190
  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: [],
191
+ interactionRules: null,
192
+ prohibitedRules: null,
193
+ ratingRules: null,
194
+ worldContextText: resolvedWorldContextText,
232
195
  joinSchema: {
233
196
  requiredFields: [participantContextField],
234
197
  optionalFields: [],
@@ -236,16 +199,13 @@ function buildWorldRecord({
236
199
  },
237
200
  searchSchema: buildSearchSchema(),
238
201
  matching: buildMatchingStrategy(),
239
- conversationTemplate: buildConversationTemplate(interactionRules, prohibitedRules, {
240
- existingConversationTemplate,
241
- }),
242
- resultContract: buildResultContract(worldId, ratingRules),
202
+ conversationTemplate: buildConversationTemplate(null, null),
203
+ resultContract: buildResultContract(worldId, null),
243
204
  meta: {
244
205
  status: resolvedStatus === 'enabled' ? 'creator_enabled' : 'creator_draft',
245
206
  persistence: 'store',
246
207
  },
247
208
  creatorAgentId,
248
- adminAgentIds: [],
249
209
  eligibility: 'active',
250
210
  broadcast: {
251
211
  enabled: false,
@@ -255,7 +215,7 @@ function buildWorldRecord({
255
215
  },
256
216
  status: resolvedStatus,
257
217
  enabled,
258
- schemaVersion,
218
+ schemaVersion: 1,
259
219
  metrics: existingMetrics || {
260
220
  totalConversationCount: 0,
261
221
  },
@@ -339,29 +299,18 @@ export function createWorldAdminService({ worldService, worldAuthorizationServic
339
299
  return actorAgentId;
340
300
  }
341
301
 
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 } = {}) {
302
+ function requireWorldOwner({ worldId, actorAgentId } = {}) {
354
303
  const authorization = worldAuthorizationService.evaluateWorldAction({
355
304
  worldId,
356
305
  actorAgentId,
357
- action,
306
+ action: WORLD_ACTIONS.MANAGE_WORLD,
358
307
  includeDisabled: true,
359
308
  });
360
309
  if (!authorization.allowed) {
361
310
  throw createWorldActionNotAllowedError({
362
311
  worldId: authorization.world.worldId,
363
312
  agentId: actorAgentId,
364
- action,
313
+ action: WORLD_ACTIONS.MANAGE_WORLD,
365
314
  actorRole: authorization.worldRole,
366
315
  allowedRoles: authorization.allowedRoles,
367
316
  });
@@ -413,8 +362,13 @@ export function createWorldAdminService({ worldService, worldAuthorizationServic
413
362
  listManagedWorlds({ actorAgentId, creatorAgentId, includeDisabled = true } = {}) {
414
363
  const storeBacked = assertStore();
415
364
  const resolvedActorAgentId = assertActorAgent(actorAgentId || creatorAgentId);
416
- return worldAuthorizationService
417
- .listManagedWorlds({ actorAgentId: resolvedActorAgentId, includeDisabled })
365
+ return worldService
366
+ .listOwnedWorlds({ creatorAgentId: resolvedActorAgentId, includeDisabled })
367
+ .map((world) => worldAuthorizationService.resolveWorldActorContext({
368
+ worldId: world.worldId,
369
+ actorAgentId: resolvedActorAgentId,
370
+ includeDisabled,
371
+ }))
418
372
  .map((context) => projectManagedWorldSummary(storeBacked, context.world, {
419
373
  worldRole: context.worldRole,
420
374
  }));
@@ -425,10 +379,9 @@ export function createWorldAdminService({ worldService, worldAuthorizationServic
425
379
  getManagedWorld({ actorAgentId, creatorAgentId, worldId } = {}) {
426
380
  const storeBacked = assertStore();
427
381
  const resolvedActorAgentId = assertActorAgent(actorAgentId || creatorAgentId);
428
- const authorization = requireWorldAction({
382
+ const authorization = requireWorldOwner({
429
383
  worldId,
430
384
  actorAgentId: resolvedActorAgentId,
431
- action: WORLD_ACTIONS.VIEW_MANAGEMENT,
432
385
  });
433
386
  return projectManagedWorld(storeBacked, authorization.world, {
434
387
  worldRole: authorization.worldRole,
@@ -439,25 +392,10 @@ export function createWorldAdminService({ worldService, worldAuthorizationServic
439
392
  const resolvedActorAgentId = assertActorAgent(actorAgentId || creatorAgentId);
440
393
  const hasChanges = changes && typeof changes === 'object' && !Array.isArray(changes);
441
394
 
442
- let authorization = requireWorldAction({
395
+ const authorization = requireWorldOwner({
443
396
  worldId,
444
397
  actorAgentId: resolvedActorAgentId,
445
- action: WORLD_ACTIONS.VIEW_MANAGEMENT,
446
398
  });
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
399
 
462
400
  const existingWorld = authorization.world;
463
401
  if (!hasChanges && enabled == null) {
@@ -466,7 +404,6 @@ export function createWorldAdminService({ worldService, worldAuthorizationServic
466
404
  });
467
405
  }
468
406
 
469
- let nextSchemaVersion = Number(existingWorld.schemaVersion || 1);
470
407
  let nextRecord = existingWorld;
471
408
 
472
409
  if (hasChanges) {
@@ -479,18 +416,12 @@ export function createWorldAdminService({ worldService, worldAuthorizationServic
479
416
  creatorAgentId: existingWorld.creatorAgentId,
480
417
  displayName: nextDisplayName,
481
418
  summary: summarizeWorldContextText(nextWorldContextText, nextDisplayName),
482
- description: nextWorldContextText,
483
- interactionRules: null,
484
- prohibitedRules: null,
485
- ratingRules: null,
486
419
  worldContextText: nextWorldContextText,
487
420
  enabled: enabled == null ? existingWorld.enabled === true : normalizeBoolean(enabled, false),
488
421
  status: enabled == null
489
422
  ? existingWorld.status
490
423
  : (normalizeBoolean(enabled, false) ? 'enabled' : 'disabled'),
491
- schemaVersion: nextSchemaVersion,
492
424
  existingMetrics: existingWorld.metrics || null,
493
- existingConversationTemplate: existingWorld.conversationTemplate || null,
494
425
  });
495
426
  } else if (enabled != null) {
496
427
  nextRecord = {
@@ -505,9 +436,6 @@ export function createWorldAdminService({ worldService, worldAuthorizationServic
505
436
  }
506
437
 
507
438
  const updated = await storeBacked.updateWorldConfig(existingWorld.worldId, nextRecord);
508
- if (nextSchemaVersion !== Number(existingWorld.schemaVersion || 1)) {
509
- await markMembershipsStale(existingWorld.worldId);
510
- }
511
439
  const normalizedWorld = worldService.requireWorld(updated.worldId, { includeDisabled: true });
512
440
  return projectManagedWorld(storeBacked, normalizedWorld, {
513
441
  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,10 @@ 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;
28
26
  }
29
27
 
30
28
  function resolveActionRequirement(action) {
@@ -37,11 +35,6 @@ function resolveActionRequirement(action) {
37
35
  allowedRoles: [WORLD_ROLES.OWNER],
38
36
  reason: 'owner_role_required',
39
37
  };
40
- case WORLD_ACTIONS.MANAGE_WORLD_ROLES:
41
- return {
42
- allowedRoles: [WORLD_ROLES.OWNER],
43
- reason: 'owner_role_required',
44
- };
45
38
  case WORLD_ACTIONS.SEARCH:
46
39
  case WORLD_ACTIONS.VIEW_CANDIDATE_FEED:
47
40
  case WORLD_ACTIONS.CREATE_CHAT_REQUEST:
@@ -77,22 +70,18 @@ export function createWorldAuthorizationService({ worldService, membershipServic
77
70
  ? resolveMembership(world.worldId, normalizedActorAgentId, { includeDisabled })
78
71
  : null;
79
72
  const isOwner = normalizedActorAgentId != null && normalizedActorAgentId === world.creatorAgentId;
80
- const isAdmin = normalizedActorAgentId != null
81
- && Array.isArray(world.adminAgentIds)
82
- && world.adminAgentIds.includes(normalizedActorAgentId);
83
73
  const isMember = membership?.status === 'active';
84
- const worldRole = resolveWorldRole({ isOwner, isAdmin, isMember });
74
+ const worldRole = resolveWorldRole({ isOwner, isMember });
85
75
 
86
76
  return {
87
77
  world,
88
78
  actorAgentId: normalizedActorAgentId,
89
79
  worldRole,
90
- managementRole: isOwner ? WORLD_ROLES.OWNER : isAdmin ? WORLD_ROLES.ADMIN : null,
80
+ managementRole: isOwner ? WORLD_ROLES.OWNER : null,
91
81
  membership,
92
82
  membershipStatus: membership?.status || null,
93
83
  roles: {
94
84
  owner: isOwner,
95
- admin: isAdmin,
96
85
  member: isMember,
97
86
  },
98
87
  };
@@ -125,7 +114,7 @@ export function createWorldAuthorizationService({ worldService, membershipServic
125
114
 
126
115
  listManagedWorlds({ actorAgentId, includeDisabled = true } = {}) {
127
116
  return worldService
128
- .listCustomWorlds({ includeDisabled })
117
+ .listOwnedWorlds({ creatorAgentId: actorAgentId, includeDisabled })
129
118
  .map((world) => resolveWorldActorContextForWorld(world, actorAgentId, { includeDisabled }))
130
119
  .filter((context) => context.managementRole != null);
131
120
  },
@@ -86,7 +86,7 @@ function createBroadcastNotAllowedError({ worldId, senderAgentId, senderRole } =
86
86
  worldId,
87
87
  senderAgentId,
88
88
  senderRole,
89
- allowedRoles: ['owner', 'admin'],
89
+ allowedRoles: ['owner'],
90
90
  };
91
91
  return error;
92
92
  }
@@ -138,10 +138,7 @@ export function createWorldBroadcastService({
138
138
 
139
139
  function resolveAudienceAgentIds(world, { audience, excludeSelf, senderAgentId } = {}) {
140
140
  if (audience === 'admins' || audience === 'admins_and_owner') {
141
- return dedupeAgentIds([
142
- world.creatorAgentId,
143
- ...(Array.isArray(world.adminAgentIds) ? world.adminAgentIds : []),
144
- ], {
141
+ return dedupeAgentIds([world.creatorAgentId], {
145
142
  excludeAgentId: excludeSelf ? senderAgentId : null,
146
143
  });
147
144
  }