@clawling/clawchat-plugin-openclaw 2026.9.11-1 → 2026.9.13-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/src/runtime.ts CHANGED
@@ -19,7 +19,12 @@ import { createOpenclawClawlingApiClient } from "./api-client.ts";
19
19
  import { buildActivationBootstrapText } from "./activation-greeting.ts";
20
20
  import { reportPluginVersionSafe, resolvePluginVersion } from "./plugin-report.ts";
21
21
  import path from "node:path";
22
- import { ensureLivewareCli, livewareCliHomeDir, resolveLivewarePath } from "./liveware-cli.ts";
22
+ import {
23
+ ensureLivewareCli,
24
+ livewareCliHomeDir,
25
+ livewareSampleRootDir,
26
+ resolveLivewarePath,
27
+ } from "./liveware-cli.ts";
23
28
  import {
24
29
  LivewareSampleSupervisor,
25
30
  type LivewareSampleStoreLike,
@@ -38,10 +43,14 @@ import {
38
43
  effectiveGroupCommandMode,
39
44
  effectiveGroupMode,
40
45
  hasOpenclawClawlingConnectCredentials,
46
+ normalizeOpenclawClawlingAccountId,
47
+ readOpenclawClawlingAccountSection,
41
48
  resolveOpenclawClawlingAccount,
42
49
  type ResolvedOpenclawClawlingAccount,
43
50
  CLAWCHAT_REFRESH_TOKEN_ENV,
44
51
  } from "./config.ts";
52
+ import { patchOpenclawClawlingAccountSection } from "./account-config-writes.ts";
53
+ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
45
54
  import { isValidChatId, type ClawlingChatClient } from "./ws-client.ts";
46
55
  import { dispatchOpenclawClawlingInbound, type IngestTurnParams } from "./inbound.ts";
47
56
  import {
@@ -234,21 +243,24 @@ export function classifyHelloFailReason(
234
243
  return "generic";
235
244
  }
236
245
 
237
- /** Read `channels.<CHANNEL_ID>.refreshToken` from a live config, or null. */
238
246
  /**
239
- * The refresh token paired with the configured access token. Channel config
240
- * `refreshToken` wins. `CLAWCHAT_REFRESH_TOKEN` (declared in the manifest's
241
- * `channelEnvVars`) is honoured ONLY when the channel config carries no `token`
242
- * of its own — i.e. when the access token itself came from `CLAWCHAT_TOKEN` —
243
- * so an env-provisioned pair can rotate, while a login-written config token is
244
- * never paired with a stray refresh token left in the environment.
247
+ * The refresh token paired with the account's access token. The account
248
+ * section's `refreshToken` wins. `CLAWCHAT_REFRESH_TOKEN` is process-wide, so
249
+ * it is honoured ONLY for the default account and ONLY when that section
250
+ * carries no `token` of its own (i.e. the access token itself came from
251
+ * `CLAWCHAT_TOKEN`) — an env-provisioned pair can rotate, while a login-written
252
+ * token is never paired with a stray refresh token left in the environment.
245
253
  */
246
- function readConfigRefreshToken(cfg: OpenClawConfig, env: NodeJS.ProcessEnv = process.env): string | null {
247
- const channels = (cfg as { channels?: Record<string, unknown> }).channels;
248
- const channel = isRecord(channels) ? channels[CHANNEL_ID] : undefined;
249
- const refreshToken = isRecord(channel) ? channel.refreshToken : undefined;
254
+ function readConfigRefreshToken(
255
+ cfg: OpenClawConfig,
256
+ accountId: string,
257
+ env: NodeJS.ProcessEnv = process.env,
258
+ ): string | null {
259
+ const section = readOpenclawClawlingAccountSection(cfg, accountId);
260
+ const refreshToken = section.refreshToken;
250
261
  if (typeof refreshToken === "string" && refreshToken.trim()) return refreshToken.trim();
251
- const configToken = isRecord(channel) ? channel.token : undefined;
262
+ if (normalizeOpenclawClawlingAccountId(accountId) !== DEFAULT_ACCOUNT_ID) return null;
263
+ const configToken = section.token;
252
264
  if (typeof configToken === "string" && configToken.trim()) return null;
253
265
  const envRefreshToken = env[CLAWCHAT_REFRESH_TOKEN_ENV];
254
266
  return typeof envRefreshToken === "string" && envRefreshToken.trim() ? envRefreshToken.trim() : null;
@@ -697,7 +709,7 @@ export interface AutoActivateFromEnvParams {
697
709
  /** Test seam — login implementation. */
698
710
  runLogin?: (params: LoginParams) => Promise<void>;
699
711
  /** Test seam — account resolver applied to the (mutated) cfg after login. */
700
- resolveAccount?: (cfg: OpenClawConfig) => ResolvedOpenclawClawlingAccount;
712
+ resolveAccount?: (cfg: OpenClawConfig, accountId?: string | null) => ResolvedOpenclawClawlingAccount;
701
713
  }
702
714
 
703
715
  /**
@@ -714,6 +726,14 @@ export async function maybeAutoActivateFromEnv(
714
726
  ): Promise<ResolvedOpenclawClawlingAccount> {
715
727
  const code = (params.connectCode ?? "").trim();
716
728
  if (!code) return params.account;
729
+ // The env var is process-wide: it can only ever describe ONE identity, and
730
+ // that is the default account. A named account never redeems it.
731
+ if (normalizeOpenclawClawlingAccountId(params.account.accountId) !== DEFAULT_ACCOUNT_ID) {
732
+ params.log?.info?.(
733
+ `[${params.account.accountId}] clawchat-plugin-openclaw ${CONNECT_CODE_ENV} present but ignored: env activation is for the default account only`,
734
+ );
735
+ return params.account;
736
+ }
717
737
  if (accountHasConnectCredentials(params.account)) return params.account;
718
738
  const accountId = params.account.accountId;
719
739
  const stored = params.store?.getActivationCredentials?.({ platform: "openclaw", accountId });
@@ -750,7 +770,7 @@ export async function maybeAutoActivateFromEnv(
750
770
  );
751
771
  return params.account;
752
772
  }
753
- const next = resolveAccount(params.cfg);
773
+ const next = resolveAccount(params.cfg, accountId);
754
774
  params.log?.info?.(
755
775
  `[${accountId}] clawchat-plugin-openclaw ${CONNECT_CODE_ENV} activation succeeded hasToken=${Boolean(next.token)}`,
756
776
  );
@@ -1098,7 +1118,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
1098
1118
  userId: account.userId,
1099
1119
  ownerUserId: account.ownerUserId,
1100
1120
  accessToken: account.token,
1101
- refreshToken: readConfigRefreshToken(cfg),
1121
+ refreshToken: readConfigRefreshToken(cfg, accountId),
1102
1122
  loginMethod: "config-seed",
1103
1123
  deviceId: CHANNEL_ID,
1104
1124
  });
@@ -1186,13 +1206,16 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
1186
1206
  // `RuntimeConnectionStore` only `Pick`s a subset of its methods, so widen
1187
1207
  // through the structural `LivewareSampleStoreLike` contract (Task 8/4).
1188
1208
  store: store as unknown as LivewareSampleStoreLike,
1189
- sampleRoot: path.join(stateDirForSample ?? "", "clawchat", "liveware-sample"),
1209
+ // Per account: two supervisors in one process otherwise install, swap and
1210
+ // delete inside the SAME `app/` tree. `default` keeps the legacy path.
1211
+ sampleRoot: livewareSampleRootDir(stateDirForSample ?? "", accountId),
1190
1212
  resolveToken: () => account.token ?? "",
1191
1213
  resolveAgentUserId: () => account.userId ?? null,
1192
1214
  resolveLivewarePath: () => resolveLivewarePath(stateDirForSample),
1193
- // Per-instance HOME for the liveware CLI: its credentials otherwise live
1194
- // in the host-global `$HOME/.clawling`, which a second profile clobbers.
1195
- cliHome: livewareCliHomeDir(stateDirForSample),
1215
+ // Per-instance AND per-account HOME for the liveware CLI: its credentials
1216
+ // otherwise live in the host-global `$HOME/.clawling`, which a second
1217
+ // profile — or a second account in this very process — clobbers.
1218
+ cliHome: livewareCliHomeDir(stateDirForSample, accountId),
1196
1219
  cliReady: livewareCliReady,
1197
1220
  listApps: async () => {
1198
1221
  const client = getConversationApiClient();
@@ -1246,7 +1269,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
1246
1269
  : undefined;
1247
1270
  };
1248
1271
 
1249
- // §0/§C.1 — write the channel-config `token`/`refreshToken` keys. `tokens=null`
1272
+ // §0/§C.1 — write the account slot's `token`/`refreshToken` keys. `tokens=null`
1250
1273
  // blanks them (auto-logout); otherwise persists the rotated pair.
1251
1274
  const persistConfigTokens = async (
1252
1275
  tokens: { accessToken: string; refreshToken: string } | null,
@@ -1261,20 +1284,13 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
1261
1284
  await mutateConfigFile({
1262
1285
  afterWrite: { mode: "none", reason: "clawchat-plugin-openclaw token refresh" },
1263
1286
  mutate(draft) {
1264
- const channels = ((draft as { channels?: Record<string, unknown> }).channels ?? {}) as Record<
1265
- string,
1266
- unknown
1267
- >;
1268
- const existing = (channels[CHANNEL_ID] ?? {}) as Record<string, unknown>;
1269
- const nextSection: Record<string, unknown> = {
1270
- ...existing,
1271
- token: tokens ? tokens.accessToken : "",
1272
- refreshToken: tokens ? tokens.refreshToken : "",
1273
- };
1274
- Object.assign(draft, {
1275
- ...draft,
1276
- channels: { ...channels, [CHANNEL_ID]: nextSection },
1277
- });
1287
+ Object.assign(
1288
+ draft,
1289
+ patchOpenclawClawlingAccountSection(draft, accountId, {
1290
+ token: tokens ? tokens.accessToken : "",
1291
+ refreshToken: tokens ? tokens.refreshToken : "",
1292
+ }),
1293
+ );
1278
1294
  },
1279
1295
  });
1280
1296
  };
@@ -1375,7 +1391,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
1375
1391
  (activationAccount.source === "sqlite" && store?.getActivationCredentials
1376
1392
  ? store.getActivationCredentials({ platform: "openclaw", accountId })?.refreshToken
1377
1393
  : null) ??
1378
- readConfigRefreshToken(cfg);
1394
+ readConfigRefreshToken(cfg, accountId);
1379
1395
 
1380
1396
  const refreshManager = new RefreshManager({
1381
1397
  baseUrl: account.baseUrl,