@clawling/clawchat-plugin-openclaw 2026.7.2-1 → 2026.7.6-1
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/src/activation-greeting.js +34 -0
- package/dist/src/api-client.js +1 -0
- package/dist/src/config.js +23 -1
- package/dist/src/plugin-report.js +1 -0
- package/dist/src/runtime.js +92 -30
- package/package.json +1 -1
- package/src/activation-greeting.ts +35 -0
- package/src/api-client.ts +2 -0
- package/src/config.ts +28 -1
- package/src/plugin-report.ts +2 -0
- package/src/runtime.ts +95 -31
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
// Worded to make the greeting the single required action. Earlier wording
|
|
5
|
+
// ("ClawChat activation bootstrap: ... do both ...") led some agents to treat
|
|
6
|
+
// it as a setup task and write a `BOOTSTRAP.md` file instead of replying, so
|
|
7
|
+
// no greeting reached the user. Lead with the chat reply, forbid file/tool
|
|
8
|
+
// detours, and keep the profile update strictly optional and secondary.
|
|
9
|
+
const ACTIVATION_BOOTSTRAP_FALLBACK = [
|
|
10
|
+
"You are now connected to a ClawChat direct conversation with your user.",
|
|
11
|
+
"Reply now with one short, friendly greeting message in this conversation: introduce yourself and say you are connected and ready.",
|
|
12
|
+
"Send it as a normal chat reply. Do not write or create any files or notes, and do not call tools just to greet.",
|
|
13
|
+
"Only if you already have your own profile details (display name, bio, or avatar) may you also call `clawchat_update_account_profile` (use `clawchat_upload_avatar_image` first for a local avatar image); otherwise skip that and just greet.",
|
|
14
|
+
"Do not ask the user for profile information.",
|
|
15
|
+
].join("\n");
|
|
16
|
+
// Cross-plugin, user-editable override read lazily so edits apply on the next
|
|
17
|
+
// first-load without a restart. Any read failure falls back to the built-in text.
|
|
18
|
+
export function buildActivationBootstrapText(homeDir = os.homedir()) {
|
|
19
|
+
const greetingPath = path.join(homeDir, "clawchat", "greeting.md");
|
|
20
|
+
try {
|
|
21
|
+
const raw = fs.readFileSync(greetingPath);
|
|
22
|
+
const override = new TextDecoder("utf-8", { fatal: true }).decode(raw).trim();
|
|
23
|
+
if (override.length > 0) {
|
|
24
|
+
return override;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
const code = error.code;
|
|
29
|
+
if (code !== "ENOENT") {
|
|
30
|
+
console.warn(`clawchat.greeting failed to read override ${greetingPath}:`, error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return ACTIVATION_BOOTSTRAP_FALLBACK;
|
|
34
|
+
}
|
package/dist/src/api-client.js
CHANGED
package/dist/src/config.js
CHANGED
|
@@ -207,6 +207,28 @@ function decodeJwtStringClaim(token, claim) {
|
|
|
207
207
|
return "";
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Resolve the bot-account userId, validated against the token's `sub` claim.
|
|
212
|
+
*
|
|
213
|
+
* The WS session authenticates with the access token, so the backend binds the
|
|
214
|
+
* session to the token's `sub` — that IS the bot's user id for this session.
|
|
215
|
+
* Unlike ownerUserId (explicit config wins over the `oid` claim), a configured
|
|
216
|
+
* userId that disagrees with `sub` is always wrong for the session this token
|
|
217
|
+
* creates, and it silently breaks self-echo detection: with the OWNER's id
|
|
218
|
+
* here, every owner message is dropped as the bot's own echo (2026-07-02
|
|
219
|
+
* production incident — a provisioner wrote the owner id into the config). Token
|
|
220
|
+
* wins; the mismatch is logged. Also derives userId from `sub` when nothing is
|
|
221
|
+
* configured, mirroring the `oid` fallback above.
|
|
222
|
+
*/
|
|
223
|
+
function resolveUserId(token, configured) {
|
|
224
|
+
const tokenSub = decodeJwtStringClaim(token, "sub");
|
|
225
|
+
if (!tokenSub)
|
|
226
|
+
return configured;
|
|
227
|
+
if (configured && configured !== tokenSub) {
|
|
228
|
+
console.warn(`clawchat userId mismatch: configured ${configured} does not equal the access token's sub claim ${tokenSub}; using the token value. A misconfigured userId (e.g. the owner's id) breaks self-echo detection and mention gating — fix the channel userId / CLAWCHAT_USER_ID.`);
|
|
229
|
+
}
|
|
230
|
+
return tokenSub;
|
|
231
|
+
}
|
|
210
232
|
function readGroupMode(value) {
|
|
211
233
|
return value === "mention" ? "mention" : "all";
|
|
212
234
|
}
|
|
@@ -312,7 +334,7 @@ export function resolveOpenclawClawlingAccount(cfg, env = process.env) {
|
|
|
312
334
|
const mediaBaseUrl = readOptionalString(channel.mediaBaseUrl) || readEnvString(env, CLAWCHAT_MEDIA_BASE_URL_ENV);
|
|
313
335
|
const token = readOptionalString(channel.token) || readEnvString(env, CLAWCHAT_TOKEN_ENV);
|
|
314
336
|
const agentId = readOptionalString(channel.agentId) || readEnvString(env, CLAWCHAT_AGENT_ID_ENV);
|
|
315
|
-
const userId = readOptionalString(channel.userId) || readEnvString(env, CLAWCHAT_USER_ID_ENV);
|
|
337
|
+
const userId = resolveUserId(token, readOptionalString(channel.userId) || readEnvString(env, CLAWCHAT_USER_ID_ENV));
|
|
316
338
|
const ownerUserId = readOptionalString(channel.ownerUserId) ||
|
|
317
339
|
readEnvString(env, CLAWCHAT_OWNER_USER_ID_ENV) ||
|
|
318
340
|
// Fall back to the access token's `oid` (owner) claim. A provisioner may
|
|
@@ -53,6 +53,7 @@ export async function reportPluginVersionSafe(p) {
|
|
|
53
53
|
deviceId: p.deviceId,
|
|
54
54
|
platform: "openclaw",
|
|
55
55
|
pluginVersion: p.pluginVersion,
|
|
56
|
+
agentVersion: p.agentVersion,
|
|
56
57
|
runtimeName: "node",
|
|
57
58
|
runtimeVersion: process.version,
|
|
58
59
|
}, { authenticated: p.authenticated });
|
package/dist/src/runtime.js
CHANGED
|
@@ -4,6 +4,7 @@ import { hasControlCommand } from "openclaw/plugin-sdk/command-detection";
|
|
|
4
4
|
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
5
5
|
import { createOpenclawClawlingClient, resolveOpenclawClawlingDeviceId } from "./client.js";
|
|
6
6
|
import { createOpenclawClawlingApiClient } from "./api-client.js";
|
|
7
|
+
import { buildActivationBootstrapText } from "./activation-greeting.js";
|
|
7
8
|
import { reportPluginVersionSafe, resolvePluginVersion } from "./plugin-report.js";
|
|
8
9
|
import { ensureLivewareCli } from "./liveware-cli.js";
|
|
9
10
|
import { ClawlingApiError } from "./api-types.js";
|
|
@@ -309,20 +310,6 @@ function withClawChatSessionScope(cfg) {
|
|
|
309
310
|
},
|
|
310
311
|
};
|
|
311
312
|
}
|
|
312
|
-
function buildActivationBootstrapText() {
|
|
313
|
-
// Worded to make the greeting the single required action. Earlier wording
|
|
314
|
-
// ("ClawChat activation bootstrap: ... do both ...") led some agents to treat
|
|
315
|
-
// it as a setup task and write a `BOOTSTRAP.md` file instead of replying, so
|
|
316
|
-
// no greeting reached the user. Lead with the chat reply, forbid file/tool
|
|
317
|
-
// detours, and keep the profile update strictly optional and secondary.
|
|
318
|
-
return [
|
|
319
|
-
"You are now connected to a ClawChat direct conversation with your user.",
|
|
320
|
-
"Reply now with one short, friendly greeting message in this conversation: introduce yourself and say you are connected and ready.",
|
|
321
|
-
"Send it as a normal chat reply. Do not write or create any files or notes, and do not call tools just to greet.",
|
|
322
|
-
"Only if you already have your own profile details (display name, bio, or avatar) may you also call `clawchat_update_account_profile` (use `clawchat_upload_avatar_image` first for a local avatar image); otherwise skip that and just greet.",
|
|
323
|
-
"Do not ask the user for profile information.",
|
|
324
|
-
].join("\n");
|
|
325
|
-
}
|
|
326
313
|
function buildActivationBootstrapEnvelope(params) {
|
|
327
314
|
const text = buildActivationBootstrapText();
|
|
328
315
|
const now = Date.now();
|
|
@@ -574,6 +561,26 @@ function waitForActivationPoll(abortSignal, ms) {
|
|
|
574
561
|
async function waitForActivationCredentials(params) {
|
|
575
562
|
const { abortSignal, getStatus, setStatus, store, log } = params;
|
|
576
563
|
const accountId = params.account.accountId;
|
|
564
|
+
// SQLite-first: activation and token rotation persist to the activations
|
|
565
|
+
// row, so a complete row is the durable credential source of truth even
|
|
566
|
+
// when channel config/env also carries credentials. Config/env credentials
|
|
567
|
+
// only bootstrap the no-row case (the caller seeds a row from them).
|
|
568
|
+
if (store?.getActivationCredentials) {
|
|
569
|
+
const stored = store.getActivationCredentials({ platform: "openclaw", accountId });
|
|
570
|
+
if (stored && stored.accessToken !== params.rejectedActivationToken) {
|
|
571
|
+
log?.info?.(`[${accountId}] clawchat-plugin-openclaw loaded activation credentials from sqlite`);
|
|
572
|
+
return {
|
|
573
|
+
source: "sqlite",
|
|
574
|
+
account: {
|
|
575
|
+
...params.account,
|
|
576
|
+
configured: true,
|
|
577
|
+
token: stored.accessToken,
|
|
578
|
+
userId: stored.userId,
|
|
579
|
+
ownerUserId: stored.ownerUserId,
|
|
580
|
+
},
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
}
|
|
577
584
|
if (accountHasConnectCredentials(params.account)) {
|
|
578
585
|
return { account: params.account, source: "configured" };
|
|
579
586
|
}
|
|
@@ -647,12 +654,14 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
647
654
|
// so the backend links both to the same row. Imported from ./client.ts.
|
|
648
655
|
const reportDeviceId = resolveOpenclawClawlingDeviceId(account);
|
|
649
656
|
const pluginVersion = resolvePluginVersion();
|
|
657
|
+
const agentVersion = runtime.version ?? "";
|
|
650
658
|
void reportPluginVersionSafe({
|
|
651
659
|
baseUrl: account.baseUrl,
|
|
652
660
|
mediaBaseUrl: account.mediaBaseUrl,
|
|
653
661
|
token: "",
|
|
654
662
|
deviceId: reportDeviceId,
|
|
655
663
|
pluginVersion,
|
|
664
|
+
agentVersion,
|
|
656
665
|
authenticated: false,
|
|
657
666
|
log,
|
|
658
667
|
});
|
|
@@ -693,6 +702,26 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
693
702
|
if (!activationAccount)
|
|
694
703
|
return;
|
|
695
704
|
account = activationAccount.account;
|
|
705
|
+
// Config/env bootstrap seeding: converge on SQLite as the single durable
|
|
706
|
+
// credential store, so token rotation always has a row to update and the
|
|
707
|
+
// next startup resolves SQLite-first to the same credentials.
|
|
708
|
+
if (activationAccount.source === "configured" && store?.upsertActivation) {
|
|
709
|
+
try {
|
|
710
|
+
store.upsertActivation({
|
|
711
|
+
platform: "openclaw",
|
|
712
|
+
accountId,
|
|
713
|
+
userId: account.userId,
|
|
714
|
+
ownerUserId: account.ownerUserId,
|
|
715
|
+
accessToken: account.token,
|
|
716
|
+
refreshToken: readConfigRefreshToken(cfg),
|
|
717
|
+
loginMethod: "config-seed",
|
|
718
|
+
deviceId: CHANNEL_ID,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
catch (error) {
|
|
722
|
+
log?.error?.(`[${accountId}] clawchat-plugin-openclaw sqlite credential seed failed; continuing with config credentials: ${error instanceof Error ? error.message : String(error)}`);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
696
725
|
// Paired: link the report row via the authenticated endpoint, reusing the
|
|
697
726
|
// SAME frozen device_id so the backend upserts the existing unpaired row.
|
|
698
727
|
void reportPluginVersionSafe({
|
|
@@ -701,12 +730,15 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
701
730
|
token: account.token,
|
|
702
731
|
deviceId: reportDeviceId,
|
|
703
732
|
pluginVersion,
|
|
733
|
+
agentVersion,
|
|
704
734
|
authenticated: true,
|
|
705
735
|
log,
|
|
706
736
|
});
|
|
707
|
-
// §A.0 — fallback expiry source. Prefer the SQLite `activated_at`; null
|
|
708
|
-
//
|
|
709
|
-
//
|
|
737
|
+
// §A.0 — fallback expiry source. Prefer the SQLite `activated_at`; null for a
|
|
738
|
+
// configured start even though the seed block above may have just created a
|
|
739
|
+
// row — this cell only ever queries `store.getActivationCredentials` when
|
|
740
|
+
// `source === "sqlite"`, so a config-sourced account relies on the JWT `exp`
|
|
741
|
+
// alone until its next SQLite-first resolution.
|
|
710
742
|
let activatedAtMs = activationAccount.source === "sqlite" && store?.getActivationCredentials
|
|
711
743
|
? store.getActivationCredentials({ platform: "openclaw", accountId })?.activatedAt ?? null
|
|
712
744
|
: null;
|
|
@@ -872,16 +904,16 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
872
904
|
getAccessToken: () => account.token,
|
|
873
905
|
getRefreshToken: () => latestRefreshToken,
|
|
874
906
|
persistRotatedTokens: async (tokens) => {
|
|
875
|
-
// §0 — persist
|
|
876
|
-
//
|
|
877
|
-
//
|
|
878
|
-
//
|
|
879
|
-
//
|
|
880
|
-
// failure
|
|
881
|
-
//
|
|
882
|
-
//
|
|
883
|
-
//
|
|
884
|
-
//
|
|
907
|
+
// §0 — persist durably BEFORE the in-memory swap. SQLite is the
|
|
908
|
+
// startup credential source of truth: when a store is available its
|
|
909
|
+
// write must succeed or the refresh stays transient (no swap, keep
|
|
910
|
+
// current tokens, back off). A 0-row UPDATE (`false`) means the
|
|
911
|
+
// activations row is missing (e.g. cleared out-of-band) rather than a
|
|
912
|
+
// write failure — self-heal by re-seeding it from the in-memory
|
|
913
|
+
// identity with the ROTATED pair (mirrors Hermes's seed-on-rotate
|
|
914
|
+
// §C.2 semantics), then continue exactly as on success. `null` is a
|
|
915
|
+
// caught write exception and stays transient — that's a real failure,
|
|
916
|
+
// not a missing row.
|
|
885
917
|
if (store?.rotateActivationTokens) {
|
|
886
918
|
const rotateResult = store.rotateActivationTokens({
|
|
887
919
|
platform: "openclaw",
|
|
@@ -889,12 +921,42 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
889
921
|
accessToken: tokens.accessToken,
|
|
890
922
|
refreshToken: tokens.refreshToken,
|
|
891
923
|
});
|
|
892
|
-
if (rotateResult ===
|
|
924
|
+
if (rotateResult === false) {
|
|
925
|
+
if (!store.upsertActivation) {
|
|
926
|
+
throw new Error("clawchat-plugin-openclaw sqlite rotate activation tokens failed");
|
|
927
|
+
}
|
|
928
|
+
try {
|
|
929
|
+
store.upsertActivation({
|
|
930
|
+
platform: "openclaw",
|
|
931
|
+
accountId,
|
|
932
|
+
userId: account.userId,
|
|
933
|
+
ownerUserId: account.ownerUserId,
|
|
934
|
+
accessToken: tokens.accessToken,
|
|
935
|
+
refreshToken: tokens.refreshToken,
|
|
936
|
+
loginMethod: "config-seed",
|
|
937
|
+
deviceId: refreshDeviceId,
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
catch {
|
|
941
|
+
throw new Error("clawchat-plugin-openclaw sqlite rotate activation tokens failed");
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
else if (rotateResult === null) {
|
|
893
945
|
throw new Error("clawchat-plugin-openclaw sqlite rotate activation tokens failed");
|
|
894
946
|
}
|
|
947
|
+
// Channel config is a human-readable mirror once SQLite holds the
|
|
948
|
+
// pair — sync best-effort, never fail the refresh over it.
|
|
949
|
+
try {
|
|
950
|
+
await persistConfigTokens(tokens);
|
|
951
|
+
}
|
|
952
|
+
catch (error) {
|
|
953
|
+
log?.error?.(`[${accountId}] clawchat-plugin-openclaw config token mirror failed (sqlite already rotated): ${error instanceof Error ? error.message : String(error)}`);
|
|
954
|
+
}
|
|
955
|
+
return;
|
|
895
956
|
}
|
|
896
|
-
//
|
|
897
|
-
//
|
|
957
|
+
// No SQLite store available — channel config is the only durable
|
|
958
|
+
// store, so its write failure must reject and keep the refresh
|
|
959
|
+
// transient (pre-existing behavior for store-less test transports).
|
|
898
960
|
await persistConfigTokens(tokens);
|
|
899
961
|
},
|
|
900
962
|
swapInMemoryTokens: (tokens) => {
|
package/package.json
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
// Worded to make the greeting the single required action. Earlier wording
|
|
6
|
+
// ("ClawChat activation bootstrap: ... do both ...") led some agents to treat
|
|
7
|
+
// it as a setup task and write a `BOOTSTRAP.md` file instead of replying, so
|
|
8
|
+
// no greeting reached the user. Lead with the chat reply, forbid file/tool
|
|
9
|
+
// detours, and keep the profile update strictly optional and secondary.
|
|
10
|
+
const ACTIVATION_BOOTSTRAP_FALLBACK = [
|
|
11
|
+
"You are now connected to a ClawChat direct conversation with your user.",
|
|
12
|
+
"Reply now with one short, friendly greeting message in this conversation: introduce yourself and say you are connected and ready.",
|
|
13
|
+
"Send it as a normal chat reply. Do not write or create any files or notes, and do not call tools just to greet.",
|
|
14
|
+
"Only if you already have your own profile details (display name, bio, or avatar) may you also call `clawchat_update_account_profile` (use `clawchat_upload_avatar_image` first for a local avatar image); otherwise skip that and just greet.",
|
|
15
|
+
"Do not ask the user for profile information.",
|
|
16
|
+
].join("\n");
|
|
17
|
+
|
|
18
|
+
// Cross-plugin, user-editable override read lazily so edits apply on the next
|
|
19
|
+
// first-load without a restart. Any read failure falls back to the built-in text.
|
|
20
|
+
export function buildActivationBootstrapText(homeDir: string = os.homedir()): string {
|
|
21
|
+
const greetingPath = path.join(homeDir, "clawchat", "greeting.md");
|
|
22
|
+
try {
|
|
23
|
+
const raw = fs.readFileSync(greetingPath);
|
|
24
|
+
const override = new TextDecoder("utf-8", { fatal: true }).decode(raw).trim();
|
|
25
|
+
if (override.length > 0) {
|
|
26
|
+
return override;
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
30
|
+
if (code !== "ENOENT") {
|
|
31
|
+
console.warn(`clawchat.greeting failed to read override ${greetingPath}:`, error);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return ACTIVATION_BOOTSTRAP_FALLBACK;
|
|
35
|
+
}
|
package/src/api-client.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface PluginReportInput {
|
|
|
24
24
|
deviceId: string;
|
|
25
25
|
platform: string;
|
|
26
26
|
pluginVersion: string;
|
|
27
|
+
agentVersion: string;
|
|
27
28
|
runtimeName: string;
|
|
28
29
|
runtimeVersion: string;
|
|
29
30
|
}
|
|
@@ -33,6 +34,7 @@ export function buildPluginReportBody(input: PluginReportInput): Record<string,
|
|
|
33
34
|
device_id: input.deviceId,
|
|
34
35
|
platform: input.platform,
|
|
35
36
|
plugin_version: input.pluginVersion,
|
|
37
|
+
agent_version: input.agentVersion,
|
|
36
38
|
runtime_name: input.runtimeName,
|
|
37
39
|
runtime_version: input.runtimeVersion,
|
|
38
40
|
};
|
package/src/config.ts
CHANGED
|
@@ -332,6 +332,30 @@ function decodeJwtStringClaim(token: string, claim: string): string {
|
|
|
332
332
|
}
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Resolve the bot-account userId, validated against the token's `sub` claim.
|
|
337
|
+
*
|
|
338
|
+
* The WS session authenticates with the access token, so the backend binds the
|
|
339
|
+
* session to the token's `sub` — that IS the bot's user id for this session.
|
|
340
|
+
* Unlike ownerUserId (explicit config wins over the `oid` claim), a configured
|
|
341
|
+
* userId that disagrees with `sub` is always wrong for the session this token
|
|
342
|
+
* creates, and it silently breaks self-echo detection: with the OWNER's id
|
|
343
|
+
* here, every owner message is dropped as the bot's own echo (2026-07-02
|
|
344
|
+
* production incident — a provisioner wrote the owner id into the config). Token
|
|
345
|
+
* wins; the mismatch is logged. Also derives userId from `sub` when nothing is
|
|
346
|
+
* configured, mirroring the `oid` fallback above.
|
|
347
|
+
*/
|
|
348
|
+
function resolveUserId(token: string, configured: string): string {
|
|
349
|
+
const tokenSub = decodeJwtStringClaim(token, "sub");
|
|
350
|
+
if (!tokenSub) return configured;
|
|
351
|
+
if (configured && configured !== tokenSub) {
|
|
352
|
+
console.warn(
|
|
353
|
+
`clawchat userId mismatch: configured ${configured} does not equal the access token's sub claim ${tokenSub}; using the token value. A misconfigured userId (e.g. the owner's id) breaks self-echo detection and mention gating — fix the channel userId / CLAWCHAT_USER_ID.`,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
return tokenSub;
|
|
357
|
+
}
|
|
358
|
+
|
|
335
359
|
function readGroupMode(value: unknown): GroupMode {
|
|
336
360
|
return value === "mention" ? "mention" : "all";
|
|
337
361
|
}
|
|
@@ -465,7 +489,10 @@ export function resolveOpenclawClawlingAccount(
|
|
|
465
489
|
readOptionalString(channel.mediaBaseUrl) || readEnvString(env, CLAWCHAT_MEDIA_BASE_URL_ENV);
|
|
466
490
|
const token = readOptionalString(channel.token) || readEnvString(env, CLAWCHAT_TOKEN_ENV);
|
|
467
491
|
const agentId = readOptionalString(channel.agentId) || readEnvString(env, CLAWCHAT_AGENT_ID_ENV);
|
|
468
|
-
const userId =
|
|
492
|
+
const userId = resolveUserId(
|
|
493
|
+
token,
|
|
494
|
+
readOptionalString(channel.userId) || readEnvString(env, CLAWCHAT_USER_ID_ENV),
|
|
495
|
+
);
|
|
469
496
|
const ownerUserId =
|
|
470
497
|
readOptionalString(channel.ownerUserId) ||
|
|
471
498
|
readEnvString(env, CLAWCHAT_OWNER_USER_ID_ENV) ||
|
package/src/plugin-report.ts
CHANGED
|
@@ -46,6 +46,7 @@ export interface ReportParams {
|
|
|
46
46
|
token: string;
|
|
47
47
|
deviceId: string;
|
|
48
48
|
pluginVersion: string;
|
|
49
|
+
agentVersion: string;
|
|
49
50
|
authenticated: boolean;
|
|
50
51
|
log?: { debug?: (msg: string) => void };
|
|
51
52
|
}
|
|
@@ -66,6 +67,7 @@ export async function reportPluginVersionSafe(p: ReportParams): Promise<void> {
|
|
|
66
67
|
deviceId: p.deviceId,
|
|
67
68
|
platform: "openclaw",
|
|
68
69
|
pluginVersion: p.pluginVersion,
|
|
70
|
+
agentVersion: p.agentVersion,
|
|
69
71
|
runtimeName: "node",
|
|
70
72
|
runtimeVersion: process.version,
|
|
71
73
|
},
|
package/src/runtime.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
|
15
15
|
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
16
16
|
import { createOpenclawClawlingClient, resolveOpenclawClawlingDeviceId } from "./client.ts";
|
|
17
17
|
import { createOpenclawClawlingApiClient } from "./api-client.ts";
|
|
18
|
+
import { buildActivationBootstrapText } from "./activation-greeting.ts";
|
|
18
19
|
import { reportPluginVersionSafe, resolvePluginVersion } from "./plugin-report.ts";
|
|
19
20
|
import { ensureLivewareCli } from "./liveware-cli.ts";
|
|
20
21
|
import { ClawlingApiError } from "./api-types.ts";
|
|
@@ -101,6 +102,7 @@ type RuntimeConnectionStore = Pick<
|
|
|
101
102
|
Pick<
|
|
102
103
|
ClawChatStore,
|
|
103
104
|
| "getActivationCredentials"
|
|
105
|
+
| "upsertActivation"
|
|
104
106
|
| "rotateActivationTokens"
|
|
105
107
|
| "clearActivationCredentials"
|
|
106
108
|
| "insertMessage"
|
|
@@ -456,21 +458,6 @@ function withClawChatSessionScope(cfg: OpenClawConfig): OpenClawConfig {
|
|
|
456
458
|
};
|
|
457
459
|
}
|
|
458
460
|
|
|
459
|
-
function buildActivationBootstrapText(): string {
|
|
460
|
-
// Worded to make the greeting the single required action. Earlier wording
|
|
461
|
-
// ("ClawChat activation bootstrap: ... do both ...") led some agents to treat
|
|
462
|
-
// it as a setup task and write a `BOOTSTRAP.md` file instead of replying, so
|
|
463
|
-
// no greeting reached the user. Lead with the chat reply, forbid file/tool
|
|
464
|
-
// detours, and keep the profile update strictly optional and secondary.
|
|
465
|
-
return [
|
|
466
|
-
"You are now connected to a ClawChat direct conversation with your user.",
|
|
467
|
-
"Reply now with one short, friendly greeting message in this conversation: introduce yourself and say you are connected and ready.",
|
|
468
|
-
"Send it as a normal chat reply. Do not write or create any files or notes, and do not call tools just to greet.",
|
|
469
|
-
"Only if you already have your own profile details (display name, bio, or avatar) may you also call `clawchat_update_account_profile` (use `clawchat_upload_avatar_image` first for a local avatar image); otherwise skip that and just greet.",
|
|
470
|
-
"Do not ask the user for profile information.",
|
|
471
|
-
].join("\n");
|
|
472
|
-
}
|
|
473
|
-
|
|
474
461
|
function buildActivationBootstrapEnvelope(params: {
|
|
475
462
|
account: ResolvedOpenclawClawlingAccount;
|
|
476
463
|
conversationId: string;
|
|
@@ -872,6 +859,28 @@ async function waitForActivationCredentials(params: {
|
|
|
872
859
|
}): Promise<{ account: ResolvedOpenclawClawlingAccount; source: "configured" | "sqlite" } | null> {
|
|
873
860
|
const { abortSignal, getStatus, setStatus, store, log } = params;
|
|
874
861
|
const accountId = params.account.accountId;
|
|
862
|
+
// SQLite-first: activation and token rotation persist to the activations
|
|
863
|
+
// row, so a complete row is the durable credential source of truth even
|
|
864
|
+
// when channel config/env also carries credentials. Config/env credentials
|
|
865
|
+
// only bootstrap the no-row case (the caller seeds a row from them).
|
|
866
|
+
if (store?.getActivationCredentials) {
|
|
867
|
+
const stored = store.getActivationCredentials({ platform: "openclaw", accountId });
|
|
868
|
+
if (stored && stored.accessToken !== params.rejectedActivationToken) {
|
|
869
|
+
log?.info?.(
|
|
870
|
+
`[${accountId}] clawchat-plugin-openclaw loaded activation credentials from sqlite`,
|
|
871
|
+
);
|
|
872
|
+
return {
|
|
873
|
+
source: "sqlite",
|
|
874
|
+
account: {
|
|
875
|
+
...params.account,
|
|
876
|
+
configured: true,
|
|
877
|
+
token: stored.accessToken,
|
|
878
|
+
userId: stored.userId,
|
|
879
|
+
ownerUserId: stored.ownerUserId,
|
|
880
|
+
},
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
}
|
|
875
884
|
if (accountHasConnectCredentials(params.account)) {
|
|
876
885
|
return { account: params.account, source: "configured" };
|
|
877
886
|
}
|
|
@@ -957,12 +966,14 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
957
966
|
// so the backend links both to the same row. Imported from ./client.ts.
|
|
958
967
|
const reportDeviceId = resolveOpenclawClawlingDeviceId(account);
|
|
959
968
|
const pluginVersion = resolvePluginVersion();
|
|
969
|
+
const agentVersion = runtime.version ?? "";
|
|
960
970
|
void reportPluginVersionSafe({
|
|
961
971
|
baseUrl: account.baseUrl,
|
|
962
972
|
mediaBaseUrl: account.mediaBaseUrl,
|
|
963
973
|
token: "",
|
|
964
974
|
deviceId: reportDeviceId,
|
|
965
975
|
pluginVersion,
|
|
976
|
+
agentVersion,
|
|
966
977
|
authenticated: false,
|
|
967
978
|
log,
|
|
968
979
|
});
|
|
@@ -1002,6 +1013,27 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1002
1013
|
});
|
|
1003
1014
|
if (!activationAccount) return;
|
|
1004
1015
|
account = activationAccount.account;
|
|
1016
|
+
// Config/env bootstrap seeding: converge on SQLite as the single durable
|
|
1017
|
+
// credential store, so token rotation always has a row to update and the
|
|
1018
|
+
// next startup resolves SQLite-first to the same credentials.
|
|
1019
|
+
if (activationAccount.source === "configured" && store?.upsertActivation) {
|
|
1020
|
+
try {
|
|
1021
|
+
store.upsertActivation({
|
|
1022
|
+
platform: "openclaw",
|
|
1023
|
+
accountId,
|
|
1024
|
+
userId: account.userId,
|
|
1025
|
+
ownerUserId: account.ownerUserId,
|
|
1026
|
+
accessToken: account.token,
|
|
1027
|
+
refreshToken: readConfigRefreshToken(cfg),
|
|
1028
|
+
loginMethod: "config-seed",
|
|
1029
|
+
deviceId: CHANNEL_ID,
|
|
1030
|
+
});
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
log?.error?.(
|
|
1033
|
+
`[${accountId}] clawchat-plugin-openclaw sqlite credential seed failed; continuing with config credentials: ${error instanceof Error ? error.message : String(error)}`,
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1005
1037
|
// Paired: link the report row via the authenticated endpoint, reusing the
|
|
1006
1038
|
// SAME frozen device_id so the backend upserts the existing unpaired row.
|
|
1007
1039
|
void reportPluginVersionSafe({
|
|
@@ -1010,12 +1042,15 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1010
1042
|
token: account.token,
|
|
1011
1043
|
deviceId: reportDeviceId,
|
|
1012
1044
|
pluginVersion,
|
|
1045
|
+
agentVersion,
|
|
1013
1046
|
authenticated: true,
|
|
1014
1047
|
log,
|
|
1015
1048
|
});
|
|
1016
|
-
// §A.0 — fallback expiry source. Prefer the SQLite `activated_at`; null
|
|
1017
|
-
//
|
|
1018
|
-
//
|
|
1049
|
+
// §A.0 — fallback expiry source. Prefer the SQLite `activated_at`; null for a
|
|
1050
|
+
// configured start even though the seed block above may have just created a
|
|
1051
|
+
// row — this cell only ever queries `store.getActivationCredentials` when
|
|
1052
|
+
// `source === "sqlite"`, so a config-sourced account relies on the JWT `exp`
|
|
1053
|
+
// alone until its next SQLite-first resolution.
|
|
1019
1054
|
let activatedAtMs: number | null =
|
|
1020
1055
|
activationAccount.source === "sqlite" && store?.getActivationCredentials
|
|
1021
1056
|
? store.getActivationCredentials({ platform: "openclaw", accountId })?.activatedAt ?? null
|
|
@@ -1208,16 +1243,16 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1208
1243
|
getAccessToken: () => account.token,
|
|
1209
1244
|
getRefreshToken: () => latestRefreshToken,
|
|
1210
1245
|
persistRotatedTokens: async (tokens) => {
|
|
1211
|
-
// §0 — persist
|
|
1212
|
-
//
|
|
1213
|
-
//
|
|
1214
|
-
//
|
|
1215
|
-
//
|
|
1216
|
-
// failure
|
|
1217
|
-
//
|
|
1218
|
-
//
|
|
1219
|
-
//
|
|
1220
|
-
//
|
|
1246
|
+
// §0 — persist durably BEFORE the in-memory swap. SQLite is the
|
|
1247
|
+
// startup credential source of truth: when a store is available its
|
|
1248
|
+
// write must succeed or the refresh stays transient (no swap, keep
|
|
1249
|
+
// current tokens, back off). A 0-row UPDATE (`false`) means the
|
|
1250
|
+
// activations row is missing (e.g. cleared out-of-band) rather than a
|
|
1251
|
+
// write failure — self-heal by re-seeding it from the in-memory
|
|
1252
|
+
// identity with the ROTATED pair (mirrors Hermes's seed-on-rotate
|
|
1253
|
+
// §C.2 semantics), then continue exactly as on success. `null` is a
|
|
1254
|
+
// caught write exception and stays transient — that's a real failure,
|
|
1255
|
+
// not a missing row.
|
|
1221
1256
|
if (store?.rotateActivationTokens) {
|
|
1222
1257
|
const rotateResult = store.rotateActivationTokens({
|
|
1223
1258
|
platform: "openclaw",
|
|
@@ -1225,12 +1260,41 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1225
1260
|
accessToken: tokens.accessToken,
|
|
1226
1261
|
refreshToken: tokens.refreshToken,
|
|
1227
1262
|
});
|
|
1228
|
-
if (rotateResult ===
|
|
1263
|
+
if (rotateResult === false) {
|
|
1264
|
+
if (!store.upsertActivation) {
|
|
1265
|
+
throw new Error("clawchat-plugin-openclaw sqlite rotate activation tokens failed");
|
|
1266
|
+
}
|
|
1267
|
+
try {
|
|
1268
|
+
store.upsertActivation({
|
|
1269
|
+
platform: "openclaw",
|
|
1270
|
+
accountId,
|
|
1271
|
+
userId: account.userId,
|
|
1272
|
+
ownerUserId: account.ownerUserId,
|
|
1273
|
+
accessToken: tokens.accessToken,
|
|
1274
|
+
refreshToken: tokens.refreshToken,
|
|
1275
|
+
loginMethod: "config-seed",
|
|
1276
|
+
deviceId: refreshDeviceId,
|
|
1277
|
+
});
|
|
1278
|
+
} catch {
|
|
1279
|
+
throw new Error("clawchat-plugin-openclaw sqlite rotate activation tokens failed");
|
|
1280
|
+
}
|
|
1281
|
+
} else if (rotateResult === null) {
|
|
1229
1282
|
throw new Error("clawchat-plugin-openclaw sqlite rotate activation tokens failed");
|
|
1230
1283
|
}
|
|
1284
|
+
// Channel config is a human-readable mirror once SQLite holds the
|
|
1285
|
+
// pair — sync best-effort, never fail the refresh over it.
|
|
1286
|
+
try {
|
|
1287
|
+
await persistConfigTokens(tokens);
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
log?.error?.(
|
|
1290
|
+
`[${accountId}] clawchat-plugin-openclaw config token mirror failed (sqlite already rotated): ${error instanceof Error ? error.message : String(error)}`,
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
return;
|
|
1231
1294
|
}
|
|
1232
|
-
//
|
|
1233
|
-
//
|
|
1295
|
+
// No SQLite store available — channel config is the only durable
|
|
1296
|
+
// store, so its write failure must reject and keep the refresh
|
|
1297
|
+
// transient (pre-existing behavior for store-less test transports).
|
|
1234
1298
|
await persistConfigTokens(tokens);
|
|
1235
1299
|
},
|
|
1236
1300
|
swapInMemoryTokens: (tokens) => {
|