@xfxstudio/claworld 0.2.6 → 0.2.8

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.6",
11
+ "version": "0.2.8",
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.6",
3
+ "version": "0.2.8",
4
4
  "description": "Claworld channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -316,6 +316,20 @@ export function parseJsonDocument(text, fallback = null) {
316
316
  return fallback;
317
317
  }
318
318
 
319
+ function parseCommandJsonOutput(result = {}, fallback = null) {
320
+ const stdout = String(result?.stdout || '').trim();
321
+ const stderr = String(result?.stderr || '').trim();
322
+ const combined = [stdout, stderr].filter(Boolean).join('\n');
323
+ const reversed = [stderr, stdout].filter(Boolean).join('\n');
324
+ return (
325
+ parseJsonDocument(stdout, null)
326
+ || parseJsonDocument(stderr, null)
327
+ || parseJsonDocument(combined, null)
328
+ || parseJsonDocument(reversed, null)
329
+ || fallback
330
+ );
331
+ }
332
+
319
333
  function parseLegacyChannelTokenStatus(tokenValue = '') {
320
334
  const normalized = normalizeText(tokenValue, '').toLowerCase();
321
335
  if (!normalized || normalized === 'missing' || normalized === 'none' || normalized === 'unset') {
@@ -1089,7 +1103,7 @@ export async function readGatewayStatus({
1089
1103
  env: buildOpenclawCommandEnv({ configPath, stateDir, env }),
1090
1104
  dryRun,
1091
1105
  });
1092
- const payload = parseJsonDocument(`${result.stdout || ''}\n${result.stderr || ''}`, null);
1106
+ const payload = parseCommandJsonOutput(result, null);
1093
1107
  if (!payload) {
1094
1108
  throw createInstallerError(
1095
1109
  'invalid_gateway_status',
@@ -1118,7 +1132,7 @@ export async function readChannelStatus({
1118
1132
  dryRun,
1119
1133
  });
1120
1134
  const output = `${result.stdout || ''}\n${result.stderr || ''}`;
1121
- const payload = parseJsonDocument(output, null) || parseLegacyChannelStatus(output);
1135
+ const payload = parseCommandJsonOutput(result, null) || parseLegacyChannelStatus(output);
1122
1136
  if (!payload) {
1123
1137
  throw createInstallerError(
1124
1138
  'invalid_channel_status',
@@ -6,8 +6,11 @@ import {
6
6
  DEFAULT_CLAWORLD_AGENT_ID,
7
7
  DEFAULT_CLAWORLD_SERVER_URL,
8
8
  expandUserPath,
9
+ getEffectiveAgentSandboxMode,
10
+ MIN_MANAGED_SESSION_VISIBILITY,
9
11
  normalizeText,
10
12
  resolveClaworldManagedRuntimeOptions,
13
+ sandboxModeNeedsSessionToolsVisibility,
11
14
  } from '../plugin/managed-config.js';
12
15
  import {
13
16
  CLAWORLD_DOCTOR_COMMAND,
@@ -87,6 +90,18 @@ function findChannelAccount(channelStatus, accountId) {
87
90
  return entries.find((entry) => entry?.accountId === accountId) || null;
88
91
  }
89
92
 
93
+ const SESSION_VISIBILITY_RANK = Object.freeze({
94
+ self: 0,
95
+ tree: 1,
96
+ agent: 2,
97
+ all: 3,
98
+ });
99
+
100
+ function resolveConfiguredSessionVisibility(config = {}) {
101
+ const visibility = normalizeText(config?.tools?.sessions?.visibility, null);
102
+ return Object.prototype.hasOwnProperty.call(SESSION_VISIBILITY_RANK, visibility) ? visibility : null;
103
+ }
104
+
90
105
  function normalizeGatewayBaseUrl(gatewayStatus) {
91
106
  const probeUrl = normalizeText(gatewayStatus?.gateway?.probeUrl, null);
92
107
  if (probeUrl) {
@@ -455,6 +470,54 @@ export async function runClaworldDoctor({
455
470
  details: { issues: configuredAccount.issues },
456
471
  }));
457
472
 
473
+ const configuredSessionVisibility = resolveConfiguredSessionVisibility(config);
474
+ const sessionVisibilityHealthy = (
475
+ configuredSessionVisibility != null
476
+ && SESSION_VISIBILITY_RANK[configuredSessionVisibility] >= SESSION_VISIBILITY_RANK[MIN_MANAGED_SESSION_VISIBILITY]
477
+ );
478
+ checks.push(createCheck({
479
+ id: 'session-tools-visibility',
480
+ category: 'Managed config',
481
+ label: 'Session tool visibility',
482
+ status: sessionVisibilityHealthy ? 'pass' : 'warn',
483
+ summary: sessionVisibilityHealthy
484
+ ? `tools.sessions.visibility is \`${configuredSessionVisibility}\`, which satisfies the managed same-agent minimum \`${MIN_MANAGED_SESSION_VISIBILITY}\`.`
485
+ : `tools.sessions.visibility is \`${configuredSessionVisibility || 'missing'}\`, but the managed same-agent follow-up path needs at least \`${MIN_MANAGED_SESSION_VISIBILITY}\`.`,
486
+ action: sessionVisibilityHealthy
487
+ ? null
488
+ : `Rerun \`${CLAWORLD_INSTALLER_COMMAND}\` to raise session visibility for managed same-agent follow-up routing.`,
489
+ details: {
490
+ actualVisibility: configuredSessionVisibility,
491
+ minimumVisibility: MIN_MANAGED_SESSION_VISIBILITY,
492
+ },
493
+ }));
494
+
495
+ const effectiveSandboxMode = getEffectiveAgentSandboxMode(config, resolvedAgentId);
496
+ if (sandboxModeNeedsSessionToolsVisibility(effectiveSandboxMode)) {
497
+ const sandboxSessionToolsVisibility = normalizeText(
498
+ config?.agents?.defaults?.sandbox?.sessionToolsVisibility,
499
+ null,
500
+ );
501
+ const sandboxVisibilityHealthy = sandboxSessionToolsVisibility === 'all';
502
+ checks.push(createCheck({
503
+ id: 'sandbox-session-tools-visibility',
504
+ category: 'Managed config',
505
+ label: 'Sandbox session tool visibility',
506
+ status: sandboxVisibilityHealthy ? 'pass' : 'warn',
507
+ summary: sandboxVisibilityHealthy
508
+ ? `Effective sandbox mode is \`${effectiveSandboxMode}\` and agents.defaults.sandbox.sessionToolsVisibility is correctly set to \`all\`.`
509
+ : `Effective sandbox mode is \`${effectiveSandboxMode}\`, so agents.defaults.sandbox.sessionToolsVisibility should be \`all\` (currently \`${sandboxSessionToolsVisibility || 'missing'}\`).`,
510
+ action: sandboxVisibilityHealthy
511
+ ? null
512
+ : `Rerun \`${CLAWORLD_INSTALLER_COMMAND}\` to widen sandbox session-tool visibility for managed follow-up routing.`,
513
+ details: {
514
+ effectiveSandboxMode,
515
+ sessionToolsVisibility: sandboxSessionToolsVisibility,
516
+ requiredSessionToolsVisibility: 'all',
517
+ },
518
+ }));
519
+ }
520
+
458
521
  checks.push(createCheck({
459
522
  id: 'app-token',
460
523
  category: 'Credentials and runtime',
@@ -10,6 +10,7 @@ import {
10
10
  defaultClaworldAccountId,
11
11
  inspectClaworldChannelAccount,
12
12
  listClaworldAccountIds,
13
+ projectClaworldStatusAccount,
13
14
  resolveClaworldChannelAccount,
14
15
  resolveClaworldRuntimeConfig,
15
16
  validateClaworldChannelConfig,
@@ -61,6 +62,11 @@ function normalizeRelayHttpBaseUrl(serverUrl) {
61
62
  return parsed.toString().replace(/\/$/, '');
62
63
  }
63
64
 
65
+ function normalizePluginOptionalText(value) {
66
+ const normalized = String(value ?? '').trim();
67
+ return normalized || null;
68
+ }
69
+
64
70
  function inferRelayDomain(runtimeConfig = {}) {
65
71
  const defaultToAddress = String(runtimeConfig.relay?.defaultToAddress || '').trim();
66
72
  const atIndex = defaultToAddress.indexOf('@');
@@ -710,6 +716,34 @@ async function acceptChatRequest({
710
716
  return result.body || {};
711
717
  }
712
718
 
719
+ async function rejectChatRequest({
720
+ runtimeConfig,
721
+ actorAgentId,
722
+ chatRequestId,
723
+ fetchImpl,
724
+ }) {
725
+ const baseUrl = normalizeRelayHttpBaseUrl(runtimeConfig.serverUrl);
726
+ const result = await fetchJson(fetchImpl, `${baseUrl}/v1/chat-requests/${encodeURIComponent(chatRequestId)}/reject`, {
727
+ method: 'POST',
728
+ headers: {
729
+ 'content-type': 'application/json',
730
+ ...(runtimeConfig.apiKey ? { 'x-api-key': runtimeConfig.apiKey } : {}),
731
+ ...buildRuntimeAuthHeaders(runtimeConfig),
732
+ },
733
+ body: JSON.stringify({ actorAgentId }),
734
+ });
735
+ if (!result.ok) {
736
+ createRelayRouteError({
737
+ result,
738
+ runtimeConfig,
739
+ code: 'chat_request_reject_failed',
740
+ publicMessage: 'failed to reject chat request',
741
+ context: { actorAgentId, chatRequestId },
742
+ });
743
+ }
744
+ return result.body || {};
745
+ }
746
+
713
747
  async function syncChatRequestApprovalPolicy({
714
748
  runtimeConfig,
715
749
  fetchImpl,
@@ -1301,7 +1335,12 @@ function createDeliveryReplyDispatcher({
1301
1335
  : undefined;
1302
1336
 
1303
1337
  let replied = false;
1338
+ let keptSilent = false;
1304
1339
  let suppressed = false;
1340
+ let replyTransport = null;
1341
+ let replyFallbackUsed = false;
1342
+ let keptSilentTransport = null;
1343
+ let keptSilentFallbackUsed = false;
1305
1344
  const finalTexts = [];
1306
1345
  const blockTexts = [];
1307
1346
  let partialContinuationText = '';
@@ -1383,16 +1422,52 @@ function createDeliveryReplyDispatcher({
1383
1422
  suppressed = true;
1384
1423
  return false;
1385
1424
  }
1386
- relayClient.sendReply({
1387
- deliveryId,
1388
- sessionKey,
1389
- replyText: normalized,
1390
- source: 'openclaw-autochain',
1391
- });
1425
+ if (typeof relayClient.sendReplyAndWaitForAck === 'function') {
1426
+ const replyResult = await relayClient.sendReplyAndWaitForAck({
1427
+ deliveryId,
1428
+ sessionKey,
1429
+ replyText: normalized,
1430
+ source: 'openclaw-autochain',
1431
+ });
1432
+ replyTransport = replyResult?.transport || 'websocket';
1433
+ replyFallbackUsed = replyResult?.fallbackUsed === true;
1434
+ } else {
1435
+ relayClient.sendReply({
1436
+ deliveryId,
1437
+ sessionKey,
1438
+ replyText: normalized,
1439
+ source: 'openclaw-autochain',
1440
+ });
1441
+ replyTransport = 'websocket-fire-and-forget';
1442
+ replyFallbackUsed = false;
1443
+ }
1392
1444
  replied = true;
1393
1445
  return true;
1394
1446
  };
1395
1447
 
1448
+ const flushKeptSilent = async (reason = null) => {
1449
+ if (replied || keptSilent || suppressed) return false;
1450
+ if (allowReply === false) {
1451
+ suppressed = true;
1452
+ return false;
1453
+ }
1454
+ if (typeof relayClient.sendKeepSilentAndWaitForAck === 'function') {
1455
+ const silentResult = await relayClient.sendKeepSilentAndWaitForAck({
1456
+ deliveryId,
1457
+ sessionKey,
1458
+ reason: normalizePluginOptionalText(reason) || 'no_renderable_reply',
1459
+ source: 'openclaw-autochain',
1460
+ });
1461
+ keptSilentTransport = silentResult?.transport || 'websocket';
1462
+ keptSilentFallbackUsed = silentResult?.fallbackUsed === true;
1463
+ } else {
1464
+ keptSilentTransport = 'unsupported';
1465
+ keptSilentFallbackUsed = false;
1466
+ }
1467
+ keptSilent = true;
1468
+ return true;
1469
+ };
1470
+
1396
1471
  const dispatchApi = runtime.channel.reply.createReplyDispatcherWithTyping({
1397
1472
  responsePrefix: prefixContext.responsePrefix,
1398
1473
  responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
@@ -1421,6 +1496,7 @@ function createDeliveryReplyDispatcher({
1421
1496
  });
1422
1497
 
1423
1498
  const markDispatchIdle = async () => {
1499
+ await dispatchApi.dispatcher.waitForIdle?.();
1424
1500
  if (!replied && !suppressed) {
1425
1501
  const continuation = buildRelayContinuationText({
1426
1502
  finalTexts,
@@ -1435,6 +1511,8 @@ function createDeliveryReplyDispatcher({
1435
1511
  : null;
1436
1512
  if (continuation.text) {
1437
1513
  await flushReply(continuation.text);
1514
+ } else {
1515
+ await flushKeptSilent(continuation.source);
1438
1516
  }
1439
1517
  }
1440
1518
  await dispatchApi.markDispatchIdle?.();
@@ -1472,6 +1550,7 @@ function createDeliveryReplyDispatcher({
1472
1550
  },
1473
1551
  markDispatchIdle,
1474
1552
  didReply: () => replied,
1553
+ didKeepSilent: () => keptSilent,
1475
1554
  getRuntimeOutputSummary: () => ({
1476
1555
  counts: { ...runtimeOutputSummary.counts },
1477
1556
  previews: {
@@ -1484,6 +1563,10 @@ function createDeliveryReplyDispatcher({
1484
1563
  },
1485
1564
  relayContinuationSource: runtimeOutputSummary.relayContinuationSource,
1486
1565
  relayContinuationPreview: runtimeOutputSummary.relayContinuationPreview,
1566
+ replyTransport,
1567
+ replyFallbackUsed,
1568
+ keptSilentTransport,
1569
+ keptSilentFallbackUsed,
1487
1570
  }),
1488
1571
  };
1489
1572
  }
@@ -1500,7 +1583,14 @@ async function runDeliveryReplyDispatch({
1500
1583
  runtimeAccountId,
1501
1584
  inboundCtx,
1502
1585
  } = {}) {
1503
- const { dispatcher, replyOptions, markDispatchIdle, didReply, getRuntimeOutputSummary } = createDeliveryReplyDispatcher({
1586
+ const {
1587
+ dispatcher,
1588
+ replyOptions,
1589
+ markDispatchIdle,
1590
+ didReply,
1591
+ didKeepSilent,
1592
+ getRuntimeOutputSummary,
1593
+ } = createDeliveryReplyDispatcher({
1504
1594
  runtime,
1505
1595
  currentCfg,
1506
1596
  relayClient,
@@ -1523,6 +1613,7 @@ async function runDeliveryReplyDispatch({
1523
1613
  return {
1524
1614
  dispatchResult,
1525
1615
  replied: didReply(),
1616
+ keptSilent: didKeepSilent(),
1526
1617
  runtimeOutputSummary: getRuntimeOutputSummary(),
1527
1618
  };
1528
1619
  }
@@ -1704,6 +1795,7 @@ async function maybeBridgeRuntimeDelivery({
1704
1795
  let {
1705
1796
  dispatchResult,
1706
1797
  replied,
1798
+ keptSilent,
1707
1799
  runtimeOutputSummary,
1708
1800
  } = await runDeliveryReplyDispatch({
1709
1801
  runtime,
@@ -1747,6 +1839,7 @@ async function maybeBridgeRuntimeDelivery({
1747
1839
  ({
1748
1840
  dispatchResult,
1749
1841
  replied,
1842
+ keptSilent,
1750
1843
  runtimeOutputSummary,
1751
1844
  } = await runDeliveryReplyDispatch({
1752
1845
  runtime,
@@ -1762,19 +1855,21 @@ async function maybeBridgeRuntimeDelivery({
1762
1855
  }));
1763
1856
  }
1764
1857
 
1765
- logger.info?.(`[claworld:${runtimeAccountId}] delivery bridge completed`, {
1766
- deliveryId,
1767
- sessionKey,
1768
- queuedFinal: Boolean(dispatchResult?.queuedFinal),
1769
- replied,
1770
- routeStatus: routed?.status || null,
1771
- runtimeOutputSummary,
1772
- });
1858
+ logger.info?.(`[claworld:${runtimeAccountId}] delivery bridge completed`, {
1859
+ deliveryId,
1860
+ sessionKey,
1861
+ queuedFinal: Boolean(dispatchResult?.queuedFinal),
1862
+ replied,
1863
+ keptSilent,
1864
+ routeStatus: routed?.status || null,
1865
+ runtimeOutputSummary,
1866
+ });
1773
1867
 
1774
1868
  return {
1775
1869
  skipped: false,
1776
1870
  ok: true,
1777
1871
  replied,
1872
+ keptSilent,
1778
1873
  queuedFinal: Boolean(dispatchResult?.queuedFinal),
1779
1874
  sessionKey,
1780
1875
  routeStatus: routed?.status || null,
@@ -1794,6 +1889,69 @@ export function createClaworldChannelPlugin({
1794
1889
  const relayClients = new Map();
1795
1890
  const lifecycles = new Map();
1796
1891
  const accountRuntimeContexts = new Map();
1892
+ const accountBindingStates = new Map();
1893
+
1894
+ function resolveAccountBindingKey(runtimeConfig = {}, fallbackAccountId = 'default') {
1895
+ return String(runtimeConfig?.accountId || fallbackAccountId || 'default');
1896
+ }
1897
+
1898
+ function mergeBoundRuntimeConfig(currentRuntimeConfig = {}, boundRuntimeConfig = {}) {
1899
+ return applyRuntimeIdentity({
1900
+ ...currentRuntimeConfig,
1901
+ ...boundRuntimeConfig,
1902
+ relay: {
1903
+ ...(currentRuntimeConfig?.relay && typeof currentRuntimeConfig.relay === 'object' ? currentRuntimeConfig.relay : {}),
1904
+ ...(boundRuntimeConfig?.relay && typeof boundRuntimeConfig.relay === 'object' ? boundRuntimeConfig.relay : {}),
1905
+ },
1906
+ });
1907
+ }
1908
+
1909
+ async function ensureAccountRelayBinding({ runtimeConfig, accountId = null }) {
1910
+ const normalizedRuntimeConfig = applyRuntimeIdentity(runtimeConfig);
1911
+ const accountKey = resolveAccountBindingKey(normalizedRuntimeConfig, accountId || null);
1912
+ const cachedState = accountBindingStates.get(accountKey) || null;
1913
+ const cachedBinding = cachedState?.binding || null;
1914
+
1915
+ if (
1916
+ cachedBinding
1917
+ && cachedBinding.runtimeConfig?.serverUrl
1918
+ && cachedBinding.runtimeConfig.serverUrl === normalizedRuntimeConfig.serverUrl
1919
+ ) {
1920
+ return {
1921
+ ...cachedBinding,
1922
+ runtimeConfig: mergeBoundRuntimeConfig(normalizedRuntimeConfig, cachedBinding.runtimeConfig),
1923
+ bindingSource: cachedBinding.bindingSource === 'configured_app_token'
1924
+ ? 'configured_app_token'
1925
+ : 'binding_cache',
1926
+ };
1927
+ }
1928
+
1929
+ if (cachedState?.promise) {
1930
+ return cachedState.promise;
1931
+ }
1932
+
1933
+ const promise = ensureRelayBinding({
1934
+ runtimeConfig: normalizedRuntimeConfig,
1935
+ fetchImpl,
1936
+ logger,
1937
+ }).then((binding) => {
1938
+ const resolvedBinding = {
1939
+ ...binding,
1940
+ runtimeConfig: mergeBoundRuntimeConfig(normalizedRuntimeConfig, binding.runtimeConfig),
1941
+ };
1942
+ accountBindingStates.set(accountKey, { binding: resolvedBinding });
1943
+ return resolvedBinding;
1944
+ }).catch((error) => {
1945
+ const latest = accountBindingStates.get(accountKey) || null;
1946
+ if (latest?.promise === promise) {
1947
+ accountBindingStates.delete(accountKey);
1948
+ }
1949
+ throw error;
1950
+ });
1951
+
1952
+ accountBindingStates.set(accountKey, { promise });
1953
+ return promise;
1954
+ }
1797
1955
 
1798
1956
  async function persistRuntimeAppToken({ runtime, accountId, appToken }) {
1799
1957
  if (!runtime?.config?.loadConfig || !runtime?.config?.writeConfigFile) {
@@ -1847,7 +2005,7 @@ export function createClaworldChannelPlugin({
1847
2005
  bindingSource: 'runtime_context',
1848
2006
  };
1849
2007
  }
1850
- const binding = await ensureRelayBinding({ runtimeConfig, fetchImpl, logger });
2008
+ const binding = await ensureAccountRelayBinding({ runtimeConfig, accountId });
1851
2009
  runtimeConfig = binding.runtimeConfig;
1852
2010
  return {
1853
2011
  ...context,
@@ -1896,7 +2054,7 @@ export function createClaworldChannelPlugin({
1896
2054
 
1897
2055
  let binding;
1898
2056
  try {
1899
- binding = await ensureRelayBinding({ runtimeConfig, fetchImpl, logger });
2057
+ binding = await ensureAccountRelayBinding({ runtimeConfig, accountId: runtimeAccountId });
1900
2058
  } catch (error) {
1901
2059
  const normalized = normalizeRuntimeBoundaryError(error, {
1902
2060
  code: 'claworld_relay_binding_failed',
@@ -2059,6 +2217,7 @@ export function createClaworldChannelPlugin({
2059
2217
  relayClients.delete(accountKey);
2060
2218
  }
2061
2219
  accountRuntimeContexts.delete(accountKey);
2220
+ accountBindingStates.delete(accountKey);
2062
2221
  },
2063
2222
  });
2064
2223
 
@@ -2200,7 +2359,8 @@ export function createClaworldChannelPlugin({
2200
2359
  validate: validateClaworldChannelConfig,
2201
2360
  listAccountIds: (cfg) => listClaworldAccountIds(cfg),
2202
2361
  defaultAccountId: (cfg) => defaultClaworldAccountId(cfg),
2203
- inspectAccount: (cfg, accountId) => inspectClaworldChannelAccount(cfg, accountId),
2362
+ inspectAccount: (cfg, accountId) =>
2363
+ projectClaworldStatusAccount(inspectClaworldChannelAccount(cfg, accountId)),
2204
2364
  resolveAccount: (cfg, accountId) => resolveClaworldChannelAccount(cfg, accountId),
2205
2365
  resolveRuntimeConfig: (cfg, accountId) => resolveClaworldRuntimeConfig(cfg, accountId),
2206
2366
  isConfigured: (account, cfg) => {
@@ -2387,6 +2547,15 @@ export function createClaworldChannelPlugin({
2387
2547
  fetchImpl,
2388
2548
  });
2389
2549
  },
2550
+ rejectChatRequest: async (context = {}) => {
2551
+ const resolvedContext = await resolveBoundRuntimeContext(context);
2552
+ return rejectChatRequest({
2553
+ runtimeConfig: resolvedContext.runtimeConfig,
2554
+ actorAgentId: resolvedContext.agentId || null,
2555
+ chatRequestId: context.chatRequestId || null,
2556
+ fetchImpl,
2557
+ });
2558
+ },
2390
2559
  },
2391
2560
  postSetup: {
2392
2561
  fetchWorldDirectory: async (context = {}) => {
@@ -2524,6 +2693,7 @@ export function createClaworldChannelPlugin({
2524
2693
  mode: context.mode || 'get',
2525
2694
  changes: context.changes || null,
2526
2695
  enabled: Object.prototype.hasOwnProperty.call(context, 'enabled') ? context.enabled : null,
2696
+ status: context.status || null,
2527
2697
  fetchImpl,
2528
2698
  logger,
2529
2699
  });
@@ -2712,6 +2882,7 @@ export function createClaworldChannelPlugin({
2712
2882
  mode: context.mode || 'get',
2713
2883
  changes: context.changes || null,
2714
2884
  enabled: Object.prototype.hasOwnProperty.call(context, 'enabled') ? context.enabled : null,
2885
+ status: context.status || null,
2715
2886
  fetchImpl,
2716
2887
  logger,
2717
2888
  });
@@ -417,6 +417,14 @@ export function inspectClaworldChannelAccount(config = {}, accountId = null) {
417
417
  };
418
418
  }
419
419
 
420
+ export function projectClaworldStatusAccount(inspection = {}) {
421
+ // Keep the steady-state credential nested under relay/runtimeConfig so
422
+ // generic OpenClaw status does not misclassify Claworld as a bot+app token
423
+ // channel.
424
+ const { appToken: _appToken, ...statusAccount } = inspection || {};
425
+ return statusAccount;
426
+ }
427
+
420
428
  export function resolveClaworldRuntimeConfig(config = {}, accountId = null) {
421
429
  const result = validateClaworldChannelConfig(config, accountId);
422
430
  if (!result.ok) {
@@ -435,11 +443,8 @@ export function resolveClaworldRuntimeConfig(config = {}, accountId = null) {
435
443
  export function resolveClaworldChannelAccount(config = {}, accountId = null) {
436
444
  const runtimeConfig = resolveClaworldRuntimeConfig(config, accountId);
437
445
  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;
441
446
  return {
442
- ...statusAccount,
447
+ ...projectClaworldStatusAccount(inspection),
443
448
  runtimeReady: true,
444
449
  resolvedFrom: accountId ? 'requested_account' : 'default_account',
445
450
  runtimeConfig,
@@ -23,6 +23,8 @@ export const DEFAULT_CLAWORLD_APPROVAL_MODE = DEFAULT_CHAT_REQUEST_APPROVAL_POLI
23
23
  export const DEFAULT_CLAWORLD_SESSION_TARGET = 'mainagent';
24
24
  export const DEFAULT_CLAWORLD_FALLBACK_TARGET = 'mainagent';
25
25
  export const CLAWORLD_PLUGIN_TOOL_ALLOW_ENTRY = 'claworld';
26
+ export const MIN_MANAGED_SESSION_VISIBILITY = 'agent';
27
+ export const REQUIRED_SANDBOX_SESSION_TOOLS_VISIBILITY = 'all';
26
28
 
27
29
  export const TOOL_PROFILES = CLAWORLD_TOOL_PROFILES;
28
30
 
@@ -128,6 +130,109 @@ function findManagedAccountEntry(config = {}, accountId) {
128
130
  return {};
129
131
  }
130
132
 
133
+ const SESSION_VISIBILITY_RANK = Object.freeze({
134
+ self: 0,
135
+ tree: 1,
136
+ agent: 2,
137
+ all: 3,
138
+ });
139
+
140
+ const SANDBOX_SESSION_TOOLS_VISIBILITY_RANK = Object.freeze({
141
+ spawned: 0,
142
+ all: 1,
143
+ });
144
+
145
+ function normalizeSessionVisibility(value, fallback = null) {
146
+ const normalized = normalizeText(value, fallback);
147
+ return Object.prototype.hasOwnProperty.call(SESSION_VISIBILITY_RANK, normalized)
148
+ ? normalized
149
+ : fallback;
150
+ }
151
+
152
+ function normalizeSandboxSessionToolsVisibility(value, fallback = null) {
153
+ const normalized = normalizeText(value, fallback);
154
+ return Object.prototype.hasOwnProperty.call(SANDBOX_SESSION_TOOLS_VISIBILITY_RANK, normalized)
155
+ ? normalized
156
+ : fallback;
157
+ }
158
+
159
+ function compareRankedSetting(value, target, rankMap) {
160
+ const nextValue = normalizeText(value, null);
161
+ const nextTarget = normalizeText(target, null);
162
+ const valueRank = Object.prototype.hasOwnProperty.call(rankMap, nextValue) ? rankMap[nextValue] : null;
163
+ const targetRank = Object.prototype.hasOwnProperty.call(rankMap, nextTarget) ? rankMap[nextTarget] : null;
164
+ if (valueRank == null && targetRank == null) return 0;
165
+ if (valueRank == null) return -1;
166
+ if (targetRank == null) return 1;
167
+ return valueRank - targetRank;
168
+ }
169
+
170
+ export function getEffectiveAgentSandboxMode(config = {}, agentId = DEFAULT_CLAWORLD_AGENT_ID) {
171
+ const normalizedAgentId = normalizeText(agentId, DEFAULT_CLAWORLD_AGENT_ID);
172
+ const agentEntry = findAgentEntry(config, normalizedAgentId);
173
+ const agentSandboxMode = normalizeText(agentEntry?.sandbox?.mode, null);
174
+ if (agentSandboxMode) return agentSandboxMode;
175
+ return normalizeText(config?.agents?.defaults?.sandbox?.mode, 'off');
176
+ }
177
+
178
+ export function sandboxModeNeedsSessionToolsVisibility(mode) {
179
+ return mode === 'all' || mode === 'non-main';
180
+ }
181
+
182
+ function ensureManagedSessionRoutingVisibility(config = {}, {
183
+ agentId = DEFAULT_CLAWORLD_AGENT_ID,
184
+ summary = [],
185
+ } = {}) {
186
+ config.tools = ensureObject(config.tools);
187
+ const existingSessionTools = ensureObject(config.tools.sessions);
188
+ const existingVisibility = normalizeSessionVisibility(existingSessionTools.visibility, null);
189
+ if (compareRankedSetting(existingVisibility, MIN_MANAGED_SESSION_VISIBILITY, SESSION_VISIBILITY_RANK) < 0) {
190
+ config.tools.sessions = {
191
+ ...existingSessionTools,
192
+ visibility: MIN_MANAGED_SESSION_VISIBILITY,
193
+ };
194
+ summary.push(
195
+ existingVisibility
196
+ ? `tools.sessions.visibility raised from ${existingVisibility} to ${MIN_MANAGED_SESSION_VISIBILITY}`
197
+ : `tools.sessions.visibility set to ${MIN_MANAGED_SESSION_VISIBILITY}`,
198
+ );
199
+ } else if (Object.keys(existingSessionTools).length > 0) {
200
+ config.tools.sessions = existingSessionTools;
201
+ }
202
+
203
+ const effectiveSandboxMode = getEffectiveAgentSandboxMode(config, agentId);
204
+ if (!sandboxModeNeedsSessionToolsVisibility(effectiveSandboxMode)) {
205
+ return;
206
+ }
207
+
208
+ config.agents = ensureObject(config.agents);
209
+ config.agents.defaults = ensureObject(config.agents.defaults);
210
+ const existingSandbox = ensureObject(config.agents.defaults.sandbox);
211
+ const existingSessionToolsVisibility = normalizeSandboxSessionToolsVisibility(
212
+ existingSandbox.sessionToolsVisibility,
213
+ null,
214
+ );
215
+ if (
216
+ compareRankedSetting(
217
+ existingSessionToolsVisibility,
218
+ REQUIRED_SANDBOX_SESSION_TOOLS_VISIBILITY,
219
+ SANDBOX_SESSION_TOOLS_VISIBILITY_RANK,
220
+ ) < 0
221
+ ) {
222
+ config.agents.defaults.sandbox = {
223
+ ...existingSandbox,
224
+ sessionToolsVisibility: REQUIRED_SANDBOX_SESSION_TOOLS_VISIBILITY,
225
+ };
226
+ summary.push(
227
+ existingSessionToolsVisibility
228
+ ? `agents.defaults.sandbox.sessionToolsVisibility raised from ${existingSessionToolsVisibility} to ${REQUIRED_SANDBOX_SESSION_TOOLS_VISIBILITY}`
229
+ : `agents.defaults.sandbox.sessionToolsVisibility set to ${REQUIRED_SANDBOX_SESSION_TOOLS_VISIBILITY}`,
230
+ );
231
+ } else if (Object.keys(existingSandbox).length > 0) {
232
+ config.agents.defaults.sandbox = existingSandbox;
233
+ }
234
+ }
235
+
131
236
  function inferExistingAgentId(config = {}, accountId = DEFAULT_CLAWORLD_ACCOUNT_ID) {
132
237
  const bindings = Array.isArray(config?.bindings) ? config.bindings : [];
133
238
  const bindingMatch = bindings
@@ -716,6 +821,11 @@ export function applyClaworldManagedRuntimeConfig(inputConfig = {}, options = {}
716
821
  : `reconciled claworld binding for ${options.accountId}`,
717
822
  );
718
823
 
824
+ ensureManagedSessionRoutingVisibility(config, {
825
+ agentId: options.agentId,
826
+ summary,
827
+ });
828
+
719
829
  return {
720
830
  config,
721
831
  summary,