@openclaw/nextcloud-talk 2026.9.1-beta.1 → 2026.9.2

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.
package/dist/api.js CHANGED
@@ -1,2 +1,2 @@
1
- import { t as nextcloudTalkPlugin } from "./channel-tgvjpexV.js";
1
+ import { t as nextcloudTalkPlugin } from "./channel-B3OgVClw.js";
2
2
  export { nextcloudTalkPlugin };
@@ -7,41 +7,43 @@ import { createLoggedPairingApprovalNotifier, createPairingPrefixStripper } from
7
7
  import { createAllowlistProviderRouteAllowlistWarningCollector, createConditionalWarningCollector, resolveScopeRequireMention, resolveScopeToolsPolicy } from "openclaw/plugin-sdk/channel-policy";
8
8
  import { buildWebhookChannelStatusSummary, createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
9
9
  import { FormatCapabilityProfile, convertMarkdownTables, renderMarkdownWithMarkers, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
10
- import { createChannelApprovalAuth } from "openclaw/plugin-sdk/approval-auth-runtime";
11
- import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries, readNonEmptyStringPreservingWhitespace } from "openclaw/plugin-sdk/string-coerce-runtime";
12
10
  import { DEFAULT_ACCOUNT_ID, hasConfiguredAccountValue, normalizeAccountId, resolveAccountWithDefaultFallback } from "openclaw/plugin-sdk/account-core";
13
11
  import { isTruthyEnvValue } from "openclaw/plugin-sdk/runtime-env";
14
12
  import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
15
- import { buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString, resolveSecretInputString } from "openclaw/plugin-sdk/secret-input";
13
+ import { buildSecretInputSchema, hasConfiguredSecretInput, resolveSecretInputString } from "openclaw/plugin-sdk/secret-input";
14
+ import { isRecord, normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries, readNonEmptyStringPreservingWhitespace } from "openclaw/plugin-sdk/string-coerce-runtime";
15
+ import { createChannelApprovalAuth } from "openclaw/plugin-sdk/approval-auth-runtime";
16
16
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
17
17
  import { parseStrictNonNegativeInteger, parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
18
- import { readProviderJsonResponse, readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
18
+ import { readProviderJsonResponse, readProviderTextResponse } from "openclaw/plugin-sdk/provider-http";
19
19
  import { resolveChannelInboundRouteEnvelope } from "openclaw/plugin-sdk/channel-inbound";
20
20
  import { bindIngressLifecycleToReplyOptions, createAccountStatusSink, createChannelIngressMonitor, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, resolveChannelStreamingBlockEnabled, runPassiveAccountLifecycle } from "openclaw/plugin-sdk/channel-outbound";
21
- import { ssrfPolicyFromPrivateNetworkOptIn, ssrfPolicyFromPrivateNetworkOptIn as ssrfPolicyFromPrivateNetworkOptIn$1 } from "openclaw/plugin-sdk/ssrf-runtime";
21
+ import { SsrFBlockedError, ssrfPolicyFromPrivateNetworkOptIn, ssrfPolicyFromPrivateNetworkOptIn as ssrfPolicyFromPrivateNetworkOptIn$1 } from "openclaw/plugin-sdk/ssrf-runtime";
22
+ import { redactToolPayloadText } from "openclaw/plugin-sdk/logging-core";
22
23
  import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
23
24
  import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
24
25
  import { createHmac, randomBytes } from "node:crypto";
25
- import { fileExists, safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
26
- import { clearAccountEntryFields } from "openclaw/plugin-sdk/channel-plugin-common";
26
+ import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
27
27
  import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$2 } from "openclaw/plugin-sdk/account-id";
28
28
  import { DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, ReplyRuntimeConfigSchemaShape, ReplyToModeSchema, buildChannelConfigSchema, buildGroupEntrySchema, buildMultiAccountChannelSchema, requireOpenAllowFrom } from "openclaw/plugin-sdk/channel-config-schema";
29
29
  import { formatAllowFromLowercase, resolveAllowlistMatchByCandidates } from "openclaw/plugin-sdk/allow-from";
30
- import { adaptScopedAccountAccessor, createScopedChannelConfigAdapter, createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
30
+ import { adaptScopedAccountAccessor, clearAccountFieldsFromConfigSection, createScopedChannelConfigAdapter, createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
31
31
  import { requireChannelOpenAllowFrom, resolveLoggerBackedRuntime } from "openclaw/plugin-sdk/extension-shared";
32
32
  import { z } from "zod";
33
33
  import os from "node:os";
34
34
  import path from "node:path";
35
+ import { fileExists } from "openclaw/plugin-sdk/file-access-runtime";
35
36
  import { migratePersistentDedupeLegacyJsonFile, resolvePersistentDedupePluginStateNamespace } from "openclaw/plugin-sdk/persistent-dedupe";
36
37
  import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
37
38
  import { channelReadyPatch } from "openclaw/plugin-sdk/gateway-runtime";
38
39
  import { channelIngressRoutes, resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
40
+ import { safeParseJson, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
39
41
  import { buildChannelKeyCandidates, normalizeChannelSlug, resolveChannelEntryMatchWithFallback } from "openclaw/plugin-sdk/channel-targets";
40
42
  import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
41
- import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
42
43
  import { createServer } from "node:http";
43
44
  import { WEBHOOK_RATE_LIMIT_DEFAULTS, createAuthRateLimiter, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, resolveRequestClientIp } from "openclaw/plugin-sdk/webhook-ingress";
44
- import { isRecord } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
45
+ import { sendHttpRequestRejection } from "openclaw/plugin-sdk/webhook-request-guards";
46
+ import { isRecord as isRecord$1 } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
45
47
  import { jsonResult, readStringParam, resolveReactionMessageId } from "openclaw/plugin-sdk/channel-actions";
46
48
  import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, buildOutboundBaseSessionKey, normalizeAccountId as normalizeAccountId$1 } from "openclaw/plugin-sdk/routing";
47
49
  import { createChannelDmPolicy } from "openclaw/plugin-sdk/channel-dm-policy";
@@ -53,17 +55,19 @@ import { formatDocsLink as formatDocsLink$1 } from "openclaw/plugin-sdk/setup-to
53
55
  function resolveNextcloudTalkApiCredentialsResult(params) {
54
56
  const apiUser = params.apiUser?.trim();
55
57
  if (!apiUser) return { status: "missing" };
56
- const inlinePassword = normalizeResolvedSecretInputString({
58
+ const inlinePassword = resolveSecretInputString({
57
59
  value: params.apiPassword,
58
- path: "channels.nextcloud-talk.apiPassword"
60
+ path: "channels.nextcloud-talk.apiPassword",
61
+ mode: params.mode
59
62
  });
60
- if (inlinePassword) return {
63
+ if (inlinePassword.status === "available") return {
61
64
  status: "available",
62
65
  value: {
63
66
  apiUser,
64
- apiPassword: inlinePassword
67
+ apiPassword: inlinePassword.value
65
68
  }
66
69
  };
70
+ if (inlinePassword.status === "configured_unavailable") return { status: "configured_unavailable" };
67
71
  if (!params.apiPasswordFile?.trim()) return { status: "missing" };
68
72
  const result = tryReadSecretFileSync(params.apiPasswordFile, "Nextcloud Talk API password", { rejectHardlinks: false }, { configPath: params.configPath ?? "channels.nextcloud-talk.apiPasswordFile" });
69
73
  if (result.status === "available") return result.value ? {
@@ -180,15 +184,21 @@ function inspectNextcloudTalkAccount(params) {
180
184
  apiUser: account.config.apiUser,
181
185
  apiPassword: account.config.apiPassword,
182
186
  apiPasswordFile: account.config.apiPasswordFile,
183
- configPath: `channels.nextcloud-talk.accounts.${account.accountId}.apiPasswordFile`
187
+ configPath: `channels.nextcloud-talk.accounts.${account.accountId}.apiPasswordFile`,
188
+ mode: "inspect"
184
189
  });
185
- const credentialDiagnostics = [...account.credentialDiagnostics ?? [], ...apiCredentialResolution.status === "configured_unavailable" ? [apiCredentialResolution.diagnostic] : []];
190
+ const credentialDiagnostics = [...account.credentialDiagnostics ?? [], ...apiCredentialResolution.status === "configured_unavailable" && apiCredentialResolution.diagnostic ? [apiCredentialResolution.diagnostic] : []];
186
191
  return {
187
192
  ...account,
193
+ configured: isNextcloudTalkAccountConfigured(account),
194
+ mode: "webhook",
188
195
  apiCredentialStatus: apiCredentialResolution.status,
189
196
  ...credentialDiagnostics.length > 0 ? { credentialDiagnostics } : {}
190
197
  };
191
198
  }
199
+ function isNextcloudTalkAccountConfigured(account) {
200
+ return Boolean(account.tokenStatus !== "missing" && account.baseUrl?.trim());
201
+ }
192
202
  //#endregion
193
203
  //#region extensions/nextcloud-talk/src/approval-auth.ts
194
204
  function normalizeNextcloudTalkApproverId(value) {
@@ -210,6 +220,18 @@ async function releaseNextcloudTalkGuardedResponse(params) {
210
220
  if (!params.response.bodyUsed) await params.response.body?.cancel().catch(() => void 0);
211
221
  await params.release();
212
222
  }
223
+ async function readNextcloudTalkErrorBody(response, ...credentials) {
224
+ try {
225
+ let body = await readProviderTextResponse(response, "Nextcloud Talk error", {
226
+ maxBytes: 8192,
227
+ chunkTimeoutMs: 1e4
228
+ });
229
+ for (const credential of credentials) body = body.replaceAll(credential, "***");
230
+ return redactToolPayloadText(body);
231
+ } catch {
232
+ return "";
233
+ }
234
+ }
213
235
  //#endregion
214
236
  //#region extensions/nextcloud-talk/src/signature.ts
215
237
  const SIGNATURE_HEADER = "x-nextcloud-talk-signature";
@@ -222,7 +244,8 @@ const BACKEND_HEADER = "x-nextcloud-talk-backend";
222
244
  function verifyNextcloudTalkSignature(params) {
223
245
  const { signature, random, body, secret } = params;
224
246
  if (!signature || !random || !secret) return false;
225
- return safeEqualSecret(signature, createHmac("sha256", secret).update(random + body).digest("hex"));
247
+ const expected = createHmac("sha256", secret).update(random + body).digest("hex");
248
+ return safeEqualSecret(signature, expected);
226
249
  }
227
250
  /**
228
251
  * Extract webhook headers from an incoming request.
@@ -256,7 +279,6 @@ function generateNextcloudTalkSignature(params) {
256
279
  //#endregion
257
280
  //#region extensions/nextcloud-talk/src/bot-preflight.ts
258
281
  const BOT_FEATURE_RESPONSE = 2;
259
- const BOT_PREFLIGHT_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
260
282
  function normalizeUrlForMatch(value) {
261
283
  if (!value?.trim()) return "";
262
284
  try {
@@ -321,7 +343,7 @@ async function probeNextcloudTalkBotResponseFeature(params) {
321
343
  });
322
344
  try {
323
345
  if (!response.ok) {
324
- const body = await readResponseTextLimited(response, BOT_PREFLIGHT_ERROR_BODY_LIMIT_BYTES).catch(() => "");
346
+ const body = await readNextcloudTalkErrorBody(response, auth, credentials.apiPassword);
325
347
  return {
326
348
  ok: false,
327
349
  code: "api_error",
@@ -373,7 +395,16 @@ const nextcloudTalkConfigAdapter = createScopedChannelConfigAdapter({
373
395
  sectionKey: "nextcloud-talk",
374
396
  listAccountIds: listNextcloudTalkAccountIds,
375
397
  resolveAccount: adaptScopedAccountAccessor(resolveNextcloudTalkAccount),
376
- inspectAccount: adaptScopedAccountAccessor(inspectNextcloudTalkAccount),
398
+ inspectAccount: (cfg, accountId) => {
399
+ const account = inspectNextcloudTalkAccount({
400
+ cfg,
401
+ accountId
402
+ });
403
+ return {
404
+ ...account,
405
+ baseUrl: account.baseUrl ? "[set]" : "[missing]"
406
+ };
407
+ },
377
408
  defaultAccountId: resolveDefaultNextcloudTalkAccountId,
378
409
  clearBaseFields: [
379
410
  "botSecret",
@@ -405,7 +436,7 @@ const NextcloudTalkRoomSchema = buildGroupEntrySchema({ allowFrom: z.array(z.str
405
436
  const NextcloudTalkNetworkSchema = z.object({
406
437
  /** Dangerous opt-in for self-hosted Nextcloud Talk on trusted private/internal hosts. */
407
438
  dangerouslyAllowPrivateNetwork: z.boolean().optional() }).strict().optional();
408
- const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(z.object({
439
+ const NextcloudTalkAccountSchemaBase = z.object({
409
440
  name: z.string().optional(),
410
441
  enabled: z.boolean().optional(),
411
442
  configWrites: z.boolean().optional(),
@@ -429,7 +460,8 @@ const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(z.object({
429
460
  /** Network policy overrides for self-hosted Nextcloud Talk on trusted private/internal hosts. */
430
461
  network: NextcloudTalkNetworkSchema,
431
462
  ...ReplyRuntimeConfigSchemaShape
432
- }).strict(), {
463
+ }).strict();
464
+ const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(NextcloudTalkAccountSchemaBase, {
433
465
  optionalAccount: true,
434
466
  refine: (value, ctx) => {
435
467
  requireChannelOpenAllowFrom({
@@ -445,7 +477,7 @@ const NextcloudTalkConfigSchema = buildMultiAccountChannelSchema(z.object({
445
477
  //#region extensions/nextcloud-talk/src/replay-migration-contract.ts
446
478
  const NEXTCLOUD_TALK_PLUGIN_ID = "nextcloud-talk";
447
479
  const NEXTCLOUD_TALK_REPLAY_DEDUPE_NAMESPACE_PREFIX = "replay-dedupe";
448
- const NEXTCLOUD_TALK_REPLAY_DEDUPE_TTL_MS = 1440 * 60 * 1e3;
480
+ const NEXTCLOUD_TALK_REPLAY_DEDUPE_TTL_MS = 864e5;
449
481
  const NEXTCLOUD_TALK_REPLAY_DEDUPE_MAX_ENTRIES = 1e4;
450
482
  //#endregion
451
483
  //#region extensions/nextcloud-talk/src/doctor.ts
@@ -525,10 +557,12 @@ function normalizeNextcloudTalkAllowlist(values) {
525
557
  return (values ?? []).map((value) => normalizeNextcloudTalkAllowEntry(String(value))).filter(Boolean);
526
558
  }
527
559
  function resolveNextcloudTalkAllowlistMatch(params) {
560
+ const allowFrom = normalizeNextcloudTalkAllowlist(params.allowFrom);
561
+ const senderId = normalizeNextcloudTalkAllowEntry(params.senderId);
528
562
  return resolveAllowlistMatchByCandidates({
529
- allowList: normalizeNextcloudTalkAllowlist(params.allowFrom),
563
+ allowList: allowFrom,
530
564
  candidates: [{
531
- value: normalizeNextcloudTalkAllowEntry(params.senderId),
565
+ value: senderId,
532
566
  source: "id"
533
567
  }]
534
568
  });
@@ -596,17 +630,18 @@ function resolveNextcloudTalkGroupRequireMention(params) {
596
630
  }
597
631
  //#endregion
598
632
  //#region extensions/nextcloud-talk/src/room-info.ts
599
- const ROOM_CACHE_TTL_MS = 300 * 1e3;
600
- const ROOM_CACHE_ERROR_TTL_MS = 30 * 1e3;
633
+ const ROOM_CACHE_TTL_MS = 3e5;
634
+ const ROOM_CACHE_ERROR_TTL_MS = 3e4;
601
635
  const ROOM_CACHE_MAX_ENTRIES = 1e3;
602
636
  const NEXTCLOUD_TALK_ROOM_INFO_TIMEOUT_MS = 3e4;
603
637
  const roomCache = /* @__PURE__ */ new Map();
604
- function resolveRoomCacheKey(params) {
605
- return `${params.accountId}:${params.roomToken}`;
606
- }
607
- function cacheRoomInfo(key, value) {
608
- roomCache.set(key, value);
638
+ function cacheRoomInfo(key, kind, ttlMs) {
639
+ roomCache.set(key, {
640
+ kind,
641
+ expiresAt: Date.now() + ttlMs
642
+ });
609
643
  pruneMapToMaxSize(roomCache, ROOM_CACHE_MAX_ENTRIES);
644
+ return kind;
610
645
  }
611
646
  function resolveRoomKindFromType(type) {
612
647
  if (!type) return;
@@ -615,29 +650,28 @@ function resolveRoomKindFromType(type) {
615
650
  }
616
651
  async function resolveNextcloudTalkRoomKind(params) {
617
652
  const { account, roomToken, runtime } = params;
618
- const key = resolveRoomCacheKey({
619
- accountId: account.accountId,
620
- roomToken
621
- });
653
+ const key = `${account.accountId}:${roomToken}`;
622
654
  const cached = roomCache.get(key);
623
- if (cached) {
624
- const age = Date.now() - cached.fetchedAt;
625
- if (cached.kind && age < ROOM_CACHE_TTL_MS) return cached.kind;
626
- if (cached.error && age < ROOM_CACHE_ERROR_TTL_MS) return;
627
- }
655
+ if (cached && Date.now() < cached.expiresAt) return cached.kind;
656
+ roomCache.delete(key);
628
657
  const apiCredentials = resolveNextcloudTalkApiCredentials({
629
658
  apiUser: account.config.apiUser,
630
659
  apiPassword: account.config.apiPassword,
631
660
  apiPasswordFile: account.config.apiPasswordFile
632
661
  });
633
- if (!apiCredentials) return;
634
662
  const baseUrl = account.baseUrl?.trim();
635
- if (!baseUrl) return;
636
- const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v4/room/${roomToken}`;
663
+ if (!apiCredentials || !baseUrl) return;
664
+ const fallback = (error) => {
665
+ runtime?.error?.(`nextcloud-talk: room lookup error: ${String(error)}`);
666
+ return cacheRoomInfo(key, void 0, ROOM_CACHE_ERROR_TTL_MS);
667
+ };
668
+ const parsedBaseUrl = URL.parse(baseUrl);
669
+ if (!parsedBaseUrl || !["http:", "https:"].includes(parsedBaseUrl.protocol)) return fallback(/* @__PURE__ */ new Error("Nextcloud Talk room lookup requires an HTTP(S) base URL"));
637
670
  const auth = Buffer.from(`${apiCredentials.apiUser}:${apiCredentials.apiPassword}`, "utf-8").toString("base64");
671
+ let guarded;
638
672
  try {
639
- const { response, release } = await fetchWithSsrFGuard({
640
- url,
673
+ guarded = await fetchWithSsrFGuard({
674
+ url: `${baseUrl}/ocs/v2.php/apps/spreed/api/v4/room/${roomToken}`,
641
675
  init: {
642
676
  method: "GET",
643
677
  headers: {
@@ -650,34 +684,30 @@ async function resolveNextcloudTalkRoomKind(params) {
650
684
  policy: ssrfPolicyFromPrivateNetworkOptIn(account.config),
651
685
  timeoutMs: params.timeoutMs ?? NEXTCLOUD_TALK_ROOM_INFO_TIMEOUT_MS
652
686
  });
687
+ } catch (error) {
688
+ if (error instanceof SsrFBlockedError) return fallback(error);
689
+ throw error;
690
+ }
691
+ const { response, release } = guarded;
692
+ try {
693
+ if (!response.ok) {
694
+ const status = response.status;
695
+ if (status === 408 || status === 429 || status >= 500 && status <= 599) throw new Error(`Nextcloud Talk room lookup failed (${status})`);
696
+ runtime?.log?.(`nextcloud-talk: room lookup failed (${status}) token=${roomToken}`);
697
+ return cacheRoomInfo(key, void 0, ROOM_CACHE_ERROR_TTL_MS);
698
+ }
653
699
  try {
654
- if (!response.ok) {
655
- cacheRoomInfo(key, {
656
- fetchedAt: Date.now(),
657
- error: `status:${response.status}`
658
- });
659
- runtime?.log?.(`nextcloud-talk: room lookup failed (${response.status}) token=${roomToken}`);
660
- return;
661
- }
662
- const kind = resolveRoomKindFromType(parseStrictPositiveInteger((await readProviderJsonResponse(response, "Nextcloud Talk room info failed")).ocs?.data?.type));
663
- cacheRoomInfo(key, {
664
- fetchedAt: Date.now(),
665
- kind
666
- });
667
- return kind;
668
- } finally {
669
- await releaseNextcloudTalkGuardedResponse({
670
- response,
671
- release
672
- });
700
+ const payload = await readProviderJsonResponse(response, "Nextcloud Talk room info failed");
701
+ const kind = resolveRoomKindFromType(parseStrictPositiveInteger(payload.ocs?.data?.type));
702
+ return kind ? cacheRoomInfo(key, kind, ROOM_CACHE_TTL_MS) : void 0;
703
+ } catch (error) {
704
+ return fallback(error);
673
705
  }
674
- } catch (err) {
675
- cacheRoomInfo(key, {
676
- fetchedAt: Date.now(),
677
- error: formatErrorMessage(err)
706
+ } finally {
707
+ await releaseNextcloudTalkGuardedResponse({
708
+ response,
709
+ release
678
710
  });
679
- runtime?.error?.(`nextcloud-talk: room lookup error: ${String(err)}`);
680
- return;
681
711
  }
682
712
  }
683
713
  //#endregion
@@ -705,7 +735,6 @@ function looksLikeNextcloudTalkTargetId(raw) {
705
735
  }
706
736
  //#endregion
707
737
  //#region extensions/nextcloud-talk/src/send.ts
708
- const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_BYTES = 8 * 1024;
709
738
  const NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS = 200;
710
739
  const NEXTCLOUD_TALK_SEND_TIMEOUT_MS = 3e4;
711
740
  const NEXTCLOUD_TALK_FORMAT_PROFILE = FormatCapabilityProfile.define({
@@ -726,20 +755,12 @@ function renderNextcloudTalkMarkdown(markdown) {
726
755
  escapeText: (text) => text
727
756
  }, NEXTCLOUD_TALK_FORMAT_PROFILE);
728
757
  }
729
- /** Collapses whitespace and caps an error-body prefix to a short, log-safe snippet. */
758
+ /** Collapses and caps an already-redacted error body for display. */
730
759
  function collapseErrorSnippet(text) {
731
760
  const collapsed = text.replace(/\s+/g, " ").trim();
732
761
  if (collapsed.length > NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS) return `${truncateUtf16Safe(collapsed, NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_CHARS)}…`;
733
762
  return collapsed;
734
763
  }
735
- /** Reads a bounded, collapsed error-body snippet without buffering hostile responses. */
736
- async function readNextcloudTalkErrorSnippet(response) {
737
- try {
738
- return collapseErrorSnippet(await readResponseTextLimited(response, NEXTCLOUD_TALK_ERROR_SNIPPET_MAX_BYTES));
739
- } catch {
740
- return "";
741
- }
742
- }
743
764
  function resolveCredentials(explicit, account) {
744
765
  const baseUrl = explicit.baseUrl?.trim() ?? account.baseUrl;
745
766
  const secret = explicit.secret?.trim() ?? account.secret;
@@ -812,8 +833,9 @@ async function sendMessageNextcloudTalk(to, text, opts) {
812
833
  body: message,
813
834
  secret
814
835
  });
836
+ const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${roomToken}/message`;
815
837
  const { response, release } = await fetchWithSsrFGuard({
816
- url: `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${roomToken}/message`,
838
+ url,
817
839
  init: {
818
840
  method: "POST",
819
841
  headers: {
@@ -830,7 +852,7 @@ async function sendMessageNextcloudTalk(to, text, opts) {
830
852
  });
831
853
  try {
832
854
  if (!response.ok) {
833
- const errorBody = await readNextcloudTalkErrorSnippet(response);
855
+ const errorBody = collapseErrorSnippet(await readNextcloudTalkErrorBody(response, signature));
834
856
  const status = response.status;
835
857
  let errorMsg = `Nextcloud Talk send failed (${status})`;
836
858
  if (status === 400) errorMsg = `Nextcloud Talk: bad request - ${errorBody || "invalid message format"}`;
@@ -874,8 +896,9 @@ async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
874
896
  body: reaction,
875
897
  secret
876
898
  });
899
+ const url = `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${normalizedToken}/reaction/${messageId}`;
877
900
  const { response, release } = await fetchWithSsrFGuard({
878
- url: `${baseUrl}/ocs/v2.php/apps/spreed/api/v1/bot/${normalizedToken}/reaction/${messageId}`,
901
+ url,
879
902
  init: {
880
903
  method: "POST",
881
904
  headers: {
@@ -892,7 +915,7 @@ async function sendReactionNextcloudTalk(roomToken, messageId, reaction, opts) {
892
915
  });
893
916
  try {
894
917
  if (!response.ok) {
895
- const errorBody = await readNextcloudTalkErrorSnippet(response);
918
+ const errorBody = collapseErrorSnippet(await readNextcloudTalkErrorBody(response, signature));
896
919
  throw new Error(`Nextcloud Talk reaction failed: ${response.status} ${errorBody}`.trim());
897
920
  }
898
921
  return { ok: true };
@@ -963,7 +986,15 @@ async function handleNextcloudTalkInbound(params) {
963
986
  accountId: account.accountId
964
987
  });
965
988
  const rawBody = message.text?.trim() ?? "";
966
- if (!rawBody) return;
989
+ if (!rawBody) {
990
+ logInboundDrop({
991
+ log: (messageLocal) => runtime.log?.(messageLocal),
992
+ channel: CHANNEL_ID,
993
+ reason: `empty message body (mediaType=${message.mediaType})`,
994
+ target: message.senderId
995
+ });
996
+ return;
997
+ }
967
998
  const roomKind = await resolveNextcloudTalkRoomKind({
968
999
  account,
969
1000
  roomToken: message.roomToken,
@@ -984,7 +1015,10 @@ async function handleNextcloudTalkInbound(params) {
984
1015
  cfg: config,
985
1016
  surface: CHANNEL_ID
986
1017
  });
987
- const hasControlCommand = core.channel.text.hasControlCommand(rawBody, config);
1018
+ const structuredBody = rawBody.startsWith("{") ? safeParseJson(rawBody) : void 0;
1019
+ const structuredText = isRecord(structuredBody) && Object.hasOwn(structuredBody, "parameters") ? normalizeOptionalString(structuredBody.message) : void 0;
1020
+ const commandBody = structuredText?.startsWith("/") ? structuredText : rawBody;
1021
+ const hasControlCommand = core.channel.text.hasControlCommand(commandBody, config);
988
1022
  const shouldRequireMention = isGroup ? resolveNextcloudTalkGroupRequireMention({
989
1023
  cfg: config,
990
1024
  accountId: account.accountId,
@@ -1160,7 +1194,7 @@ async function handleNextcloudTalkInbound(params) {
1160
1194
  body,
1161
1195
  bodyForAgent: rawBody,
1162
1196
  rawBody,
1163
- commandBody: rawBody
1197
+ commandBody
1164
1198
  },
1165
1199
  access: {
1166
1200
  commands: { authorized: commandAuthorized },
@@ -1225,7 +1259,7 @@ function parseRawObject(rawEvent) {
1225
1259
  } catch (error) {
1226
1260
  throw new NextcloudTalkWebhookPayloadError("Nextcloud Talk webhook contains invalid JSON.", { cause: error });
1227
1261
  }
1228
- if (!isRecord(parsed)) throw new NextcloudTalkWebhookPayloadError("Nextcloud Talk webhook must be a JSON object.");
1262
+ if (!isRecord$1(parsed)) throw new NextcloudTalkWebhookPayloadError("Nextcloud Talk webhook must be a JSON object.");
1229
1263
  return parsed;
1230
1264
  }
1231
1265
  function requiredString(value, field) {
@@ -1235,10 +1269,10 @@ function requiredString(value, field) {
1235
1269
  function inspectNextcloudTalkWebhookEnvelope(rawEvent) {
1236
1270
  const envelope = parseRawObject(rawEvent);
1237
1271
  if (envelope.type !== "Create") return null;
1238
- const object = isRecord(envelope.object) ? envelope.object : null;
1272
+ const object = isRecord$1(envelope.object) ? envelope.object : null;
1239
1273
  if (object?.type !== void 0 && object.type !== "Note") return null;
1240
1274
  if (!object) throw new NextcloudTalkWebhookPayloadError("Nextcloud Talk webhook is missing object.");
1241
- const target = isRecord(envelope.target) ? envelope.target : null;
1275
+ const target = isRecord$1(envelope.target) ? envelope.target : null;
1242
1276
  return {
1243
1277
  eventId: requiredString(object.id, "object.id"),
1244
1278
  laneKey: `room:${requiredString(target?.id, "target.id")}`
@@ -1279,8 +1313,8 @@ async function migrateNextcloudTalkLegacyReplayState(params) {
1279
1313
  }
1280
1314
  //#endregion
1281
1315
  //#region extensions/nextcloud-talk/src/monitor.ts
1282
- const DEFAULT_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
1283
- const PREAUTH_WEBHOOK_MAX_BODY_BYTES = 64 * 1024;
1316
+ const DEFAULT_WEBHOOK_MAX_BODY_BYTES = 1048576;
1317
+ const PREAUTH_WEBHOOK_MAX_BODY_BYTES = 65536;
1284
1318
  const NEXTCLOUD_TALK_WEBHOOK_ACCEPTED_HEADER = "x-openclaw-delivery-accepted";
1285
1319
  const NEXTCLOUD_TALK_WEBHOOK_ACCEPTED_VALUE = "durable";
1286
1320
  const PREAUTH_WEBHOOK_BODY_TIMEOUT_MS = 5e3;
@@ -1336,9 +1370,14 @@ function verifyWebhookSignature(params) {
1336
1370
  function readNextcloudTalkWebhookBody(req, maxBodyBytes) {
1337
1371
  return readRequestBodyWithLimit(req, {
1338
1372
  maxBytes: Math.min(maxBodyBytes, PREAUTH_WEBHOOK_MAX_BODY_BYTES),
1339
- timeoutMs: PREAUTH_WEBHOOK_BODY_TIMEOUT_MS
1373
+ timeoutMs: PREAUTH_WEBHOOK_BODY_TIMEOUT_MS,
1374
+ destroyOnLimit: false
1340
1375
  });
1341
1376
  }
1377
+ async function rejectWebhookRequest(req, res, status, error) {
1378
+ if (res.headersSent) return;
1379
+ await sendHttpRequestRejection(req, res, status, JSON.stringify({ error }), "application/json");
1380
+ }
1342
1381
  function createNextcloudTalkWebhookServer(opts) {
1343
1382
  const { port, host, path, secret, onWebhook, onError, abortSignal } = opts;
1344
1383
  const maxBodyBytes = typeof opts.maxBodyBytes === "number" && Number.isFinite(opts.maxBodyBytes) && opts.maxBodyBytes > 0 ? Math.floor(opts.maxBodyBytes) : DEFAULT_WEBHOOK_MAX_BODY_BYTES;
@@ -1391,11 +1430,11 @@ function createNextcloudTalkWebhookServer(opts) {
1391
1430
  writeJsonResponse(res, 200);
1392
1431
  } catch (err) {
1393
1432
  if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) {
1394
- writeWebhookError(res, 413, WEBHOOK_ERRORS.payloadTooLarge);
1433
+ await rejectWebhookRequest(req, res, 413, WEBHOOK_ERRORS.payloadTooLarge);
1395
1434
  return;
1396
1435
  }
1397
1436
  if (isRequestBodyLimitError(err, "REQUEST_BODY_TIMEOUT")) {
1398
- writeWebhookError(res, 408, requestBodyErrorToText("REQUEST_BODY_TIMEOUT"));
1437
+ await rejectWebhookRequest(req, res, 408, requestBodyErrorToText("REQUEST_BODY_TIMEOUT"));
1399
1438
  return;
1400
1439
  }
1401
1440
  if (err instanceof NextcloudTalkWebhookPayloadError) {
@@ -1439,8 +1478,10 @@ function createNextcloudTalkWebhookServer(opts) {
1439
1478
  });
1440
1479
  });
1441
1480
  };
1442
- if (abortSignal) if (abortSignal.aborted) stop();
1443
- else abortSignal.addEventListener("abort", () => void stop(), { once: true });
1481
+ if (abortSignal) {
1482
+ if (abortSignal.aborted) stop();
1483
+ else abortSignal.addEventListener("abort", () => void stop(), { once: true });
1484
+ }
1444
1485
  return {
1445
1486
  server,
1446
1487
  start,
@@ -1450,6 +1491,12 @@ function createNextcloudTalkWebhookServer(opts) {
1450
1491
  //#endregion
1451
1492
  //#region extensions/nextcloud-talk/src/webhook-spool.ts
1452
1493
  const NEXTCLOUD_TALK_INGRESS_POLL_INTERVAL_MS = 500;
1494
+ function describeIgnoredWebhookEvent(rawEvent) {
1495
+ const envelope = parseRawObject(rawEvent);
1496
+ const type = typeof envelope.type === "string" ? envelope.type : "unknown";
1497
+ const object = isRecord$1(envelope.object) ? envelope.object : null;
1498
+ return `type=${type} objectType=${typeof object?.type === "string" ? object.type : "unknown"}`;
1499
+ }
1453
1500
  const NextcloudTalkWebhookPayloadSchema = z.object({
1454
1501
  type: z.enum([
1455
1502
  "Create",
@@ -1576,7 +1623,11 @@ function createNextcloudTalkWebhookSpool(options) {
1576
1623
  if (stopping) return Promise.reject(/* @__PURE__ */ new Error("Nextcloud Talk ingress stopped"));
1577
1624
  const receiveTask = (async () => {
1578
1625
  await startAfterMigration;
1579
- return (await monitor.admit(rawEvent)).kind === "ignored" ? "ignored" : "accepted";
1626
+ if ((await monitor.admit(rawEvent)).kind === "ignored") {
1627
+ options.runtime.log?.(`nextcloud-talk: ignored non-message webhook event (${describeIgnoredWebhookEvent(rawEvent)})`);
1628
+ return "ignored";
1629
+ }
1630
+ return "accepted";
1580
1631
  })();
1581
1632
  inFlightReceives.add(receiveTask);
1582
1633
  receiveTask.then(() => inFlightReceives.delete(receiveTask), () => inFlightReceives.delete(receiveTask));
@@ -1720,44 +1771,18 @@ const nextcloudTalkGatewayAdapter = {
1720
1771
  });
1721
1772
  },
1722
1773
  logoutAccount: async ({ accountId, cfg }) => {
1723
- const nextCfg = { ...cfg };
1724
- const nextSection = cfg.channels?.["nextcloud-talk"] ? { ...cfg.channels["nextcloud-talk"] } : void 0;
1725
- let cleared = false;
1726
- let changed = false;
1727
- if (nextSection) {
1728
- if (accountId === DEFAULT_ACCOUNT_ID$2 && nextSection.botSecret) {
1729
- delete nextSection.botSecret;
1730
- cleared = true;
1731
- changed = true;
1732
- }
1733
- const accountCleanup = clearAccountEntryFields({
1734
- accounts: nextSection.accounts,
1735
- accountId,
1736
- fields: ["botSecret"]
1737
- });
1738
- if (accountCleanup.changed) {
1739
- changed = true;
1740
- if (accountCleanup.cleared) cleared = true;
1741
- if (accountCleanup.nextAccounts) nextSection.accounts = accountCleanup.nextAccounts;
1742
- else delete nextSection.accounts;
1743
- }
1744
- }
1745
- if (changed) if (nextSection && Object.keys(nextSection).length > 0) nextCfg.channels = {
1746
- ...nextCfg.channels,
1747
- "nextcloud-talk": nextSection
1748
- };
1749
- else {
1750
- const nextChannels = { ...nextCfg.channels };
1751
- delete nextChannels["nextcloud-talk"];
1752
- if (Object.keys(nextChannels).length > 0) nextCfg.channels = nextChannels;
1753
- else delete nextCfg.channels;
1754
- }
1774
+ const { nextConfig, changed, cleared } = clearAccountFieldsFromConfigSection({
1775
+ cfg,
1776
+ sectionKey: "nextcloud-talk",
1777
+ accountId,
1778
+ fields: ["botSecret"]
1779
+ });
1755
1780
  const loggedOut = resolveNextcloudTalkAccount({
1756
- cfg: changed ? nextCfg : cfg,
1781
+ cfg: nextConfig,
1757
1782
  accountId
1758
1783
  }).secretSource === "none";
1759
1784
  if (changed) await getNextcloudTalkRuntime().config.replaceConfigFile({
1760
- nextConfig: nextCfg,
1785
+ nextConfig,
1761
1786
  afterWrite: { mode: "auto" }
1762
1787
  });
1763
1788
  return {
@@ -1939,6 +1964,49 @@ const nextcloudTalkDmPolicy = createChannelDmPolicy({
1939
1964
  applyPatch: ({ cfg, account, patch }) => setNextcloudTalkAccountConfig(cfg, account.accountId, patch),
1940
1965
  promptAllowFrom: promptNextcloudTalkAllowFromForAccount
1941
1966
  });
1967
+ const nextcloudTalkSetupAdapter = {
1968
+ singleAccountKeysToMove: ["rooms"],
1969
+ resolveAccountId: ({ accountId }) => normalizeAccountId$1(accountId),
1970
+ prepareAccountConfigInput: ({ input }) => {
1971
+ const setupInput = input;
1972
+ return {
1973
+ ...setupInput,
1974
+ baseUrl: setupInput.baseUrl ?? readNonEmptyStringPreservingWhitespace(setupInput.url),
1975
+ secret: setupInput.secret ?? readNonEmptyStringPreservingWhitespace(setupInput.token) ?? readNonEmptyStringPreservingWhitespace(setupInput.password),
1976
+ secretFile: setupInput.secretFile ?? readNonEmptyStringPreservingWhitespace(setupInput.tokenFile)
1977
+ };
1978
+ },
1979
+ applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
1980
+ cfg,
1981
+ channelKey: channel$1,
1982
+ accountId,
1983
+ name
1984
+ }),
1985
+ validateInput: createSetupInputPresenceValidator({
1986
+ defaultAccountOnlyEnvError: "NEXTCLOUD_TALK_BOT_SECRET can only be used for the default account.",
1987
+ validate: ({ input }) => {
1988
+ const setupInput = input;
1989
+ if (!setupInput.useEnv && !setupInput.secret && !setupInput.secretFile) return "Nextcloud Talk requires bot secret or --secret-file (or --use-env).";
1990
+ const normalizedBaseUrl = normalizeNextcloudTalkBaseUrl(setupInput.baseUrl);
1991
+ if (!normalizedBaseUrl) return "Nextcloud Talk requires --base-url.";
1992
+ const baseUrlError = validateNextcloudTalkBaseUrl(normalizedBaseUrl);
1993
+ if (baseUrlError) return baseUrlError;
1994
+ return null;
1995
+ }
1996
+ }),
1997
+ applyAccountConfig: ({ cfg, accountId, input }) => {
1998
+ const setupInput = input;
1999
+ return setNextcloudTalkAccountConfig(applyAccountNameToChannelSection({
2000
+ cfg,
2001
+ channelKey: channel$1,
2002
+ accountId,
2003
+ name: setupInput.name
2004
+ }), accountId, {
2005
+ baseUrl: normalizeNextcloudTalkBaseUrl(setupInput.baseUrl),
2006
+ ...setupInput.useEnv ? {} : setupInput.secretFile ? { botSecretFile: setupInput.secretFile } : setupInput.secret ? { botSecret: setupInput.secret } : {}
2007
+ }, setupInput.useEnv ? ["botSecret", "botSecretFile"] : void 0);
2008
+ }
2009
+ };
1942
2010
  const nextcloudTalkSetupContract = defineChannelSetupContract({
1943
2011
  fields: {
1944
2012
  baseUrl: {
@@ -2004,49 +2072,7 @@ const nextcloudTalkSetupContract = defineChannelSetupContract({
2004
2072
  envVars: ["NEXTCLOUD_TALK_BOT_SECRET"]
2005
2073
  }
2006
2074
  },
2007
- legacyAdapter: {
2008
- singleAccountKeysToMove: ["rooms"],
2009
- resolveAccountId: ({ accountId }) => normalizeAccountId$1(accountId),
2010
- prepareAccountConfigInput: ({ input }) => {
2011
- const setupInput = input;
2012
- return {
2013
- ...setupInput,
2014
- baseUrl: setupInput.baseUrl ?? readNonEmptyStringPreservingWhitespace(setupInput.url),
2015
- secret: setupInput.secret ?? readNonEmptyStringPreservingWhitespace(setupInput.token) ?? readNonEmptyStringPreservingWhitespace(setupInput.password),
2016
- secretFile: setupInput.secretFile ?? readNonEmptyStringPreservingWhitespace(setupInput.tokenFile)
2017
- };
2018
- },
2019
- applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
2020
- cfg,
2021
- channelKey: channel$1,
2022
- accountId,
2023
- name
2024
- }),
2025
- validateInput: createSetupInputPresenceValidator({
2026
- defaultAccountOnlyEnvError: "NEXTCLOUD_TALK_BOT_SECRET can only be used for the default account.",
2027
- validate: ({ input }) => {
2028
- const setupInput = input;
2029
- if (!setupInput.useEnv && !setupInput.secret && !setupInput.secretFile) return "Nextcloud Talk requires bot secret or --secret-file (or --use-env).";
2030
- const normalizedBaseUrl = normalizeNextcloudTalkBaseUrl(setupInput.baseUrl);
2031
- if (!normalizedBaseUrl) return "Nextcloud Talk requires --base-url.";
2032
- const baseUrlError = validateNextcloudTalkBaseUrl(normalizedBaseUrl);
2033
- if (baseUrlError) return baseUrlError;
2034
- return null;
2035
- }
2036
- }),
2037
- applyAccountConfig: ({ cfg, accountId, input }) => {
2038
- const setupInput = input;
2039
- return setNextcloudTalkAccountConfig(applyAccountNameToChannelSection({
2040
- cfg,
2041
- channelKey: channel$1,
2042
- accountId,
2043
- name: setupInput.name
2044
- }), accountId, {
2045
- baseUrl: normalizeNextcloudTalkBaseUrl(setupInput.baseUrl),
2046
- ...setupInput.useEnv ? {} : setupInput.secretFile ? { botSecretFile: setupInput.secretFile } : setupInput.secret ? { botSecret: setupInput.secret } : {}
2047
- }, setupInput.useEnv ? ["botSecret", "botSecretFile"] : void 0);
2048
- }
2049
- }
2075
+ legacyAdapter: nextcloudTalkSetupAdapter
2050
2076
  });
2051
2077
  //#endregion
2052
2078
  //#region extensions/nextcloud-talk/src/setup-surface.ts
@@ -2231,10 +2257,10 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
2231
2257
  configSchema: buildChannelConfigSchema(NextcloudTalkConfigSchema),
2232
2258
  config: {
2233
2259
  ...nextcloudTalkConfigAdapter,
2234
- isConfigured: (account) => Boolean(account.tokenStatus !== "missing" && account.baseUrl?.trim()),
2260
+ isConfigured: isNextcloudTalkAccountConfigured,
2235
2261
  describeAccount: (account) => describeWebhookAccountSnapshot({
2236
2262
  account,
2237
- configured: Boolean(account.tokenStatus !== "missing" && account.baseUrl?.trim()),
2263
+ configured: isNextcloudTalkAccountConfigured(account),
2238
2264
  extra: {
2239
2265
  secretSource: account.secretSource,
2240
2266
  tokenStatus: account.tokenStatus,
@@ -2290,7 +2316,7 @@ const nextcloudTalkPlugin = createChatChannelPlugin({
2290
2316
  accountId: account.accountId,
2291
2317
  name: account.name,
2292
2318
  enabled: account.enabled,
2293
- configured: Boolean(account.tokenStatus !== "missing" && account.baseUrl?.trim()),
2319
+ configured: isNextcloudTalkAccountConfigured(account),
2294
2320
  extra: {
2295
2321
  secretSource: account.secretSource,
2296
2322
  tokenStatus: account.tokenStatus,
@@ -1,2 +1,2 @@
1
- import { t as nextcloudTalkPlugin } from "./channel-tgvjpexV.js";
1
+ import { t as nextcloudTalkPlugin } from "./channel-B3OgVClw.js";
2
2
  export { nextcloudTalkPlugin };
@@ -5,7 +5,6 @@
5
5
  },
6
6
  "name": "Nextcloud Talk",
7
7
  "description": "OpenClaw Nextcloud Talk channel plugin for conversations.",
8
- "icon": "https://cdn.simpleicons.org/nextcloud",
9
8
  "activation": {
10
9
  "onStartup": false
11
10
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/nextcloud-talk",
3
- "version": "2026.9.1-beta.1",
3
+ "version": "2026.9.2",
4
4
  "description": "OpenClaw Nextcloud Talk channel plugin for conversations.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "type": "module",
10
10
  "peerDependencies": {
11
- "openclaw": ">=2026.9.1-beta.1"
11
+ "openclaw": ">=2026.9.2"
12
12
  },
13
13
  "peerDependenciesMeta": {
14
14
  "openclaw": {
@@ -129,10 +129,10 @@
129
129
  "minHostVersion": ">=2026.4.10"
130
130
  },
131
131
  "compat": {
132
- "pluginApi": ">=2026.9.1-beta.1"
132
+ "pluginApi": ">=2026.9.2"
133
133
  },
134
134
  "build": {
135
- "openclawVersion": "2026.9.1-beta.1"
135
+ "openclawVersion": "2026.9.2"
136
136
  },
137
137
  "release": {
138
138
  "publishToClawHub": true,