@totalreclaw/totalreclaw 3.3.11-rc.1 → 3.3.11-rc.3
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/CHANGELOG.md +37 -0
- package/SKILL.md +49 -14
- package/dist/index.js +14 -3
- package/dist/pair-cli-relay.js +45 -1
- package/dist/tr-cli.js +1 -1
- package/index.ts +16 -3
- package/package.json +1 -1
- package/pair-cli-relay.ts +50 -1
- package/skill.json +1 -1
- package/tr-cli.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,43 @@ All notable changes to `@totalreclaw/totalreclaw` (the OpenClaw plugin) are docu
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [3.3.11-rc.3] — 2026-05-06
|
|
8
|
+
|
|
9
|
+
Two real-bug fixes flagged by Pedro's pop-os QA on rc.11-rc.1/rc.2: credentials.json shipped with only `{mnemonic}` (no userId, no salt), and the subgraph "Invalid character 'q' at position 0" Bytes-decode error.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **credentials.json now persists `userId` + `salt` at pair time.** Previously, `pair-cli-relay.ts::completePairing` wrote only `{mnemonic}` and deferred `/v1/register` + the `{userId, salt}` fields to the plugin's next `register()` load. When that load failed (Pedro's pop-os SIGUSR1 plugin-skip + various other transients), credentials stayed mnemonic-only and every subsequent operation re-attempted registration but never actually wrote the result. Fix: pair-cli-relay's completePairing now derives `keys = deriveKeys(mnemonic)`, computes `authKeyHash + saltHex`, calls `apiClient.register()` against the relay's `/v1/register` (converting `wss://…` → `https://…`), and writes the full `{mnemonic, salt(base64), userId, scope_address}` object to credentials.json before resolving the WS pair flow. USER_EXISTS in subgraph mode falls back to a deterministic userId derived from the auth-key hash. Pair flow is now self-contained: a successful pair leaves the user fully registered with complete credentials, regardless of subsequent plugin-load behavior.
|
|
14
|
+
|
|
15
|
+
- **Subgraph "Invalid character 'q' at position 0" eliminated.** Plugin's Smart Account derivation has a fallback: when `deriveSmartAccountAddress(mnemonic)` throws, it set `subgraphOwner = userId`. But the subgraph's `owner` field is typed `Bytes!` (0x-prefixed hex), and userIds (UUIDs from `/v1/register`) often start with non-hex chars like `q`/`r`/`y`, so every subsequent subgraph query returned `Failed to decode Bytes value: Invalid character 'q' at position 0`. Fix: never fall back to `userId` for the Bytes! field. Instead leave `subgraphOwner = null` and emit a clear error explaining that subgraph reads/writes will be skipped this session until SA derivation succeeds. This surfaces the underlying mnemonic issue (or whatever the SA-derivation failure was) rather than masking it as a downstream subgraph decoding error.
|
|
16
|
+
|
|
17
|
+
### Implementation notes
|
|
18
|
+
|
|
19
|
+
- `pair-cli-relay.ts` now imports `deriveKeys` + `computeAuthKeyHash` from `crypto.js` and `createApiClient` from `api-client.js`. The added `/v1/register` call is best-effort — registration failure logs a warn and continues with a mnemonic-only credentials.json (the plugin's `register()` retries on next boot). USER_EXISTS specifically derives the userId deterministically (`authKeyHash.slice(0, 32)`) so the credentials are still complete.
|
|
20
|
+
- `index.ts` SA-derivation fallback removed. `subgraphOwner` stays `null` on derivation failure. Existing call-sites already check for null/undefined before issuing subgraph queries; the change just stops poisoning those checks with a wrong-format string.
|
|
21
|
+
- 81/81 fs-helpers + 21/21 register-command-name + 37/37 skill-md + 21/21 tr-cli-json + 40/40 trajectory-poller. check-scanner: 129 files, 0 flags.
|
|
22
|
+
|
|
23
|
+
### Likely cures
|
|
24
|
+
|
|
25
|
+
The pop-os "plugin doesn't load after SIGUSR1" symptom from rc.11-rc.1/rc.2 was probably a downstream effect of these two bugs: incomplete credentials → SA derivation fails → subgraphOwner falls back to userId → first subgraph query throws → plugin's register() bails before reaching the trajectory-poller setup. With creds complete at pair time AND no garbage-fallback for the Bytes field, plugin's register() should run to completion and the poller should fire.
|
|
26
|
+
|
|
27
|
+
## [3.3.11-rc.2] — 2026-05-06
|
|
28
|
+
|
|
29
|
+
UX hardening on top of rc.1's trajectory-poller. Pedro's first install attempt on rc.1 surfaced two agent-prose violations the prior FORBIDDEN ACTIONS list didn't catch:
|
|
30
|
+
|
|
31
|
+
1. **Inter-line narration during install.** The agent emitted "Now let me verify the install:", "Now let me check if you already have credentials:", and "Need to pair. Let me kick that off:" between the four numbered user-visible lines. Each was edit churn the user shouldn't see — and the third leaked the internal word "pair", which the user doesn't know.
|
|
32
|
+
2. **"Stored on a relay server" architectural claim.** When asked "how does this work?", the agent replied that memories are stored on a relay server. WRONG. Memories are encrypted with a key derived from the recovery phrase, submitted on-chain via Account Abstraction (UserOps), persisted on a public blockchain (Base for free tier, Gnosis for paid), and indexed by The Graph. The relay only forwards encrypted bundles — it never sees plaintext, can't read memories, and could be replaced by any compatible relay without losing data.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- **SKILL.md "CRITICAL" section rewritten as "How TotalReclaw actually stores memories"** — leads with the on-chain decentralized architecture, not the relay. Forbidden vocabulary list expanded with centralized-custody phrasings ("stored on a relay server", "TotalReclaw's server", "single server owned by", "company server", etc.) alongside the existing local-only ban list.
|
|
37
|
+
- **New "User-facing vocabulary" translation table.** Maps internal jargon (pair, relay, WS, trapdoor, subgraph, bundler, UserOp, mnemonic file, `setsid -f`) to user-facing phrases ("set up an account", "your account", "your recovery phrase"). The word "pair" is now explicitly forbidden in user-visible prose — it's an internal CLI command, not a user concept.
|
|
38
|
+
- **New "Hard silence rules between numbered lines"** subsection with verbatim bad examples from this QA: "Now let me verify the install" / "Now let me check if you already have credentials" / "Need to pair. Let me kick that off" / "Standing by." / any "Let me X" prelude. The transcript the user sees is exactly the four numbered lines, nothing in between.
|
|
39
|
+
- **New "How does TotalReclaw work?" canonical answer section.** Provides the long-form explanation plus three short slices ("Where are memories stored?" / "Is the relay reading my data?" / "What if TotalReclaw shuts down?") so the agent has a paste-ready truthful answer for common user questions. Mirrored in `openclaw-setup-quickstart.md`.
|
|
40
|
+
- **Quickstart rules bumped from 5 to 7** with the same "no pair word" + "no centralized-custody phrasing" hard rules.
|
|
41
|
+
|
|
42
|
+
This RC is doc + SKILL.md only — same `tr-cli.js` and `trajectory-poller.js` binaries as rc.1. The trajectory poller verified end-to-end (40/40 unit tests + live container smoke) in rc.1 ships unchanged.
|
|
43
|
+
|
|
7
44
|
## [3.3.11-rc.1] — 2026-05-06
|
|
8
45
|
|
|
9
46
|
Auto-extraction restored without waiting on upstream OpenClaw. Pedro's pop-os QA on rc.10-rc.5 produced 0 extraction events across 2 h of Telegram chat — root cause confirmed in fresh canonical container (OpenClaw 2026.5.4): the `agent_end` hook is silently rejected for non-bundled plugins despite `plugins.entries.totalreclaw.hooks.allowConversationAccess=true` in config. Reproduces every gateway boot + every SIGUSR1 in-process restart. Plugin's `api.on('agent_end', handler)` call returns without error but the gateway never dispatches the event.
|
package/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: totalreclaw
|
|
3
3
|
description: "Install + set up TotalReclaw encrypted memory for OpenClaw, then use tr CLI for remember / recall. Trigger on 'install TotalReclaw', 'set up TotalReclaw', 'restore my recovery phrase', or any remember / recall request."
|
|
4
|
-
version: 3.3.11-rc.
|
|
4
|
+
version: 3.3.11-rc.3
|
|
5
5
|
author: TotalReclaw Team
|
|
6
6
|
license: MIT
|
|
7
7
|
homepage: https://totalreclaw.xyz
|
|
@@ -30,21 +30,32 @@ metadata:
|
|
|
30
30
|
>
|
|
31
31
|
> If the user pastes either URL: prefer the quickstart for execution. Fetch the full guide only if the user asks for context or troubleshooting beyond what the quickstart covers. This SKILL.md mirrors the quickstart's hard rules so you can act even if the URL is unreachable.
|
|
32
32
|
|
|
33
|
-
## CRITICAL:
|
|
33
|
+
## CRITICAL: How TotalReclaw actually stores memories (read before anything else)
|
|
34
34
|
|
|
35
|
-
**TotalReclaw is
|
|
35
|
+
**TotalReclaw is end-to-end encrypted memory stored across a DECENTRALIZED NETWORK — not on a single company server.** Memories are encrypted with keys derived from the user's 12-word recovery phrase, submitted as Account Abstraction UserOps, and persisted on a public blockchain (Base / Gnosis depending on tier). The Graph subgraph indexes them. The TotalReclaw relay only forwards encrypted bundles to the bundler and proxies subgraph reads — it never sees plaintext, can't read memories, and could be replaced by any compatible relay without losing data. No single company controls or can read user memories.
|
|
36
36
|
|
|
37
|
-
Forbidden vocabulary — these claims are
|
|
38
|
-
- "local"
|
|
39
|
-
- "
|
|
40
|
-
- "
|
|
41
|
-
- "on disk"
|
|
42
|
-
- "without an account"
|
|
43
|
-
- "local memory"
|
|
44
|
-
- "local storage"
|
|
45
|
-
- any phrasing suggesting memories can be stored without pairing a relay account
|
|
37
|
+
Forbidden vocabulary — these claims are WRONG (some imply local-only storage; some imply a single-company server):
|
|
38
|
+
- "local", "local-only", "stored locally", "on disk", "local memory", "local storage" — wrong direction (implies local-only). Memories require an account; they're encrypted and submitted on-chain.
|
|
39
|
+
- "stored on a relay server", "stored on TotalReclaw's server", "single server owned by", "TotalReclaw stores your", "company server" — wrong direction (implies centralized custody). The relay forwards ciphertext; storage is decentralized on-chain.
|
|
40
|
+
- "without an account" — wrong (an account is required to derive the encryption key).
|
|
46
41
|
|
|
47
|
-
If you
|
|
42
|
+
If you need to describe how it works, the canonical line is: **"Memories are encrypted with a key derived from your recovery phrase, submitted on-chain via Account Abstraction, and stored across a decentralized network. The relay only forwards encrypted bundles — it never sees plaintext. Only you can decrypt them, and only with your recovery phrase."**
|
|
43
|
+
|
|
44
|
+
## User-facing vocabulary (substitute internal jargon for plain words)
|
|
45
|
+
|
|
46
|
+
Internal terms that the user does NOT know — translate when you must speak:
|
|
47
|
+
|
|
48
|
+
| Internal term | Say to the user |
|
|
49
|
+
|---|---|
|
|
50
|
+
| pair / pairing / paired account | "set up an account" / "your TotalReclaw account" |
|
|
51
|
+
| relay / relay server | (don't mention; if forced, say "the encrypted-bundle forwarder") |
|
|
52
|
+
| WS / WebSocket / handshake | (don't mention) |
|
|
53
|
+
| trapdoor / blind index / dedup-key | (don't mention) |
|
|
54
|
+
| subgraph / bundler / UserOp / Account Abstraction | (don't mention; if forced, "submitted on-chain to a decentralized network") |
|
|
55
|
+
| credentials.json / mnemonic file | "your recovery phrase" |
|
|
56
|
+
| `tr pair --json` / `setsid -f` / shell exec | (don't mention; just do the work silently) |
|
|
57
|
+
|
|
58
|
+
The word "pair" is internal jargon — never use it in user-visible prose. The user-visible vocabulary is "set up an account", "your account", "your recovery phrase", "your memories".
|
|
48
59
|
|
|
49
60
|
## Top-level rule (read this before anything else)
|
|
50
61
|
|
|
@@ -94,6 +105,30 @@ Speak only these lines, in order. Substitute the bracketed placeholders with the
|
|
|
94
105
|
If existing credentials are detected (step "Check for existing credentials" below), emit ONLY this line and stop:
|
|
95
106
|
> ✓ TotalReclaw is already set up.
|
|
96
107
|
|
|
108
|
+
### Hard silence rules between numbered lines
|
|
109
|
+
|
|
110
|
+
Do NOT emit transitional / narrative lines between any of the four user-visible lines above. Specifically forbidden (verbatim examples from Pedro's 2026-05-06 QA):
|
|
111
|
+
|
|
112
|
+
- ❌ "Now let me verify the install:" → silent. Just run `tr status --json`. Emit line 2 when it returns.
|
|
113
|
+
- ❌ "Now let me check if you already have credentials:" → silent. Just run `test -s ~/.totalreclaw/credentials.json`.
|
|
114
|
+
- ❌ "Need to pair. Let me kick that off:" → silent. Just run the `setsid -f` block. Emit line 3 when URL+PIN returns. ALSO never use the word "pair" in user-facing text — see vocabulary table above.
|
|
115
|
+
- ❌ "Standing by." → silent. Acknowledgements between lines are noise.
|
|
116
|
+
- ❌ Any "Let me X" / "Now I'll Y" / "Just give me a second" prelude.
|
|
117
|
+
|
|
118
|
+
The transcript Pedro should see is exactly four messages — the four numbered lines, nothing in between.
|
|
119
|
+
|
|
120
|
+
## How does TotalReclaw work? (canonical answer for user questions)
|
|
121
|
+
|
|
122
|
+
When the user asks "how does this work?" / "where are my memories stored?" / "is this safe?" / similar — answer with this content. Do NOT say "stored on a relay server", "stored on TotalReclaw's server", or any phrasing that implies single-company custody. The actual architecture:
|
|
123
|
+
|
|
124
|
+
> **End-to-end encrypted, decentralized memory.** When you tell me to remember something, I encrypt it locally with a key derived from your 12-word recovery phrase, then submit it on-chain via Account Abstraction (a UserOperation). It lives on a public blockchain (Base Sepolia for free tier, Gnosis mainnet for paid). The Graph subgraph indexes it for fast retrieval. The TotalReclaw relay server only forwards encrypted bundles between your client and the chain — it never sees plaintext, can't read your memories, and could be replaced by any compatible relay without losing data. Your recovery phrase is the only thing that can decrypt the memories. Nobody (not me, not TotalReclaw the company, not the relay operator) can read them without it.
|
|
125
|
+
|
|
126
|
+
If the user asks shorter questions, reply with the relevant slice:
|
|
127
|
+
|
|
128
|
+
- **"Where are memories stored?"** → "Encrypted, on-chain (Base or Gnosis depending on your tier), indexed by The Graph. Not on any single company's server."
|
|
129
|
+
- **"Is the relay reading my data?"** → "No. The relay only forwards encrypted bundles. Plaintext never leaves your device. Only your recovery phrase can decrypt them."
|
|
130
|
+
- **"What if TotalReclaw shuts down?"** → "Your memories survive. They're on a public blockchain. Any compatible relay (or self-hosted relay) can read them with your recovery phrase."
|
|
131
|
+
|
|
97
132
|
## Setup flow (hybrid-primary)
|
|
98
133
|
|
|
99
134
|
1. **Install plugin first, skill second (silent — emit user-visible line 1 once at the start, then no prose until line 2).** Run via your shell tool, in this exact order:
|
|
@@ -135,7 +170,7 @@ TR_CLI="$(ls -d ~/.openclaw/npm/node_modules/@totalreclaw/totalreclaw/dist/tr-cl
|
|
|
135
170
|
|
|
136
171
|
# Status check (parse JSON to confirm install + onboarding state)
|
|
137
172
|
node "$TR_CLI" status --json
|
|
138
|
-
# Returns: {"version":"3.3.
|
|
173
|
+
# Returns: {"version":"3.3.11-rc.1","onboarded":false,"next_step":"pair","tool_count":17,"hybrid_mode":true}
|
|
139
174
|
|
|
140
175
|
# Pair / account setup — ALWAYS run via setsid -f to survive gateway restarts
|
|
141
176
|
PAIR_OUT="/tmp/tr-pair-out-$(date +%s).json"
|
package/dist/index.js
CHANGED
|
@@ -660,6 +660,13 @@ async function initialize(logger) {
|
|
|
660
660
|
logger.info(`Registered new user: ${userId}`);
|
|
661
661
|
}
|
|
662
662
|
// Derive Smart Account address for subgraph queries (on-chain owner identity).
|
|
663
|
+
// 3.3.11-rc.3: NEVER fall back to userId on derivation failure — the subgraph's
|
|
664
|
+
// `owner` field is typed `Bytes!` (0x-prefixed hex) and rejects userId UUIDs
|
|
665
|
+
// with `Failed to decode Bytes value: Invalid character 'q' at position 0`
|
|
666
|
+
// (because userIds often start with non-hex chars like q/r/y). When SA
|
|
667
|
+
// derivation fails the only safe path is to leave subgraphOwner unset and
|
|
668
|
+
// fail every subsequent on-chain operation with a clear "smart-account
|
|
669
|
+
// unavailable" error rather than spamming the subgraph with garbage Bytes.
|
|
663
670
|
if (isSubgraphMode()) {
|
|
664
671
|
try {
|
|
665
672
|
const config = getSubgraphConfig();
|
|
@@ -667,9 +674,13 @@ async function initialize(logger) {
|
|
|
667
674
|
logger.info(`Subgraph owner (Smart Account): ${subgraphOwner}`);
|
|
668
675
|
}
|
|
669
676
|
catch (err) {
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
677
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
678
|
+
logger.error(`Smart Account derivation failed: ${msg} — subgraph reads/writes will be skipped this session ` +
|
|
679
|
+
'(no Bytes-format owner available). Verify mnemonic in credentials.json.');
|
|
680
|
+
// Leave subgraphOwner undefined. Code paths that read it must guard
|
|
681
|
+
// against undefined and skip the subgraph round-trip rather than
|
|
682
|
+
// sending a malformed query.
|
|
683
|
+
subgraphOwner = null;
|
|
673
684
|
}
|
|
674
685
|
}
|
|
675
686
|
// One-time billing check for returning users (imported recovery phrase).
|
package/dist/pair-cli-relay.js
CHANGED
|
@@ -49,6 +49,8 @@ import { loadCredentialsJson, writeCredentialsJson, writeOnboardingState, } from
|
|
|
49
49
|
import { awaitPhraseUpload, openRemotePairSession, } from './pair-remote-client.js';
|
|
50
50
|
import { setRecoveryPhraseOverride } from './config.js';
|
|
51
51
|
import { encodePng, encodeUnicode } from './pair-qr.js';
|
|
52
|
+
import { deriveKeys, computeAuthKeyHash } from './crypto.js';
|
|
53
|
+
import { createApiClient } from './api-client.js';
|
|
52
54
|
/**
|
|
53
55
|
* Run the relay-mode pair CLI. Mirrors `runPairCli`'s exit-code semantics:
|
|
54
56
|
* - `completed` (status 0)
|
|
@@ -216,6 +218,45 @@ export async function runRelayPairCli(mode, opts) {
|
|
|
216
218
|
phraseValidator: (p) => validateMnemonic(p, wordlist),
|
|
217
219
|
completePairing: async ({ mnemonic }) => {
|
|
218
220
|
try {
|
|
221
|
+
// 3.3.11-rc.3: derive auth key + salt from mnemonic and pre-register
|
|
222
|
+
// with the relay HERE (not deferred to the plugin's next register()
|
|
223
|
+
// load). Pedro's 2026-05-06 QA found that pop-os was leaving
|
|
224
|
+
// credentials.json with only `{mnemonic}` because the plugin's
|
|
225
|
+
// post-pair register() either didn't run (post-SIGUSR1 plugin-skip
|
|
226
|
+
// bug) or partially failed before writing userId/salt back to disk.
|
|
227
|
+
// Doing it inline here means a successful pair is self-contained:
|
|
228
|
+
// browser uploads phrase → completePairing derives keys → register
|
|
229
|
+
// → write {userId, salt, mnemonic, scope_address}. Plugin's
|
|
230
|
+
// register() on next boot just authenticates with the existing
|
|
231
|
+
// credentials.
|
|
232
|
+
const keys = deriveKeys(mnemonic);
|
|
233
|
+
const authKeyHash = computeAuthKeyHash(keys.authKey);
|
|
234
|
+
const saltHex = keys.salt.toString('hex');
|
|
235
|
+
const saltB64 = keys.salt.toString('base64');
|
|
236
|
+
let registeredUserId;
|
|
237
|
+
try {
|
|
238
|
+
// wss://… → https://… for the REST register call. The relay
|
|
239
|
+
// serves both protocols on the same host.
|
|
240
|
+
const httpsBase = opts.relayBaseUrl.replace(/^ws/, 'http').replace(/\/+$/, '');
|
|
241
|
+
const apiClient = createApiClient(httpsBase);
|
|
242
|
+
const result = await apiClient.register(authKeyHash, saltHex);
|
|
243
|
+
registeredUserId = result.user_id;
|
|
244
|
+
opts.logger.info(`pair-cli (relay): registered user_id=${registeredUserId} (salt + auth-key persisted)`);
|
|
245
|
+
}
|
|
246
|
+
catch (regErr) {
|
|
247
|
+
const msg = regErr instanceof Error ? regErr.message : String(regErr);
|
|
248
|
+
// USER_EXISTS in subgraph mode → derive userId deterministically
|
|
249
|
+
// from auth-key hash so the credentials are still complete.
|
|
250
|
+
// Other errors → continue with mnemonic-only creds; the plugin's
|
|
251
|
+
// register() will retry on next load.
|
|
252
|
+
if (msg.includes('USER_EXISTS')) {
|
|
253
|
+
registeredUserId = authKeyHash.slice(0, 32);
|
|
254
|
+
opts.logger.info(`pair-cli (relay): USER_EXISTS — using derived userId=${registeredUserId}`);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
opts.logger.warn(`pair-cli (relay): /v1/register failed (best-effort, will retry on plugin load): ${msg}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
219
260
|
let scopeAddress;
|
|
220
261
|
try {
|
|
221
262
|
scopeAddress = await opts.deriveScopeAddress(mnemonic);
|
|
@@ -224,7 +265,9 @@ export async function runRelayPairCli(mode, opts) {
|
|
|
224
265
|
opts.logger.warn(`pair-cli (relay): scope_address derivation failed (will retry lazily): ${deriveErr instanceof Error ? deriveErr.message : String(deriveErr)}`);
|
|
225
266
|
}
|
|
226
267
|
const creds = loadCredentialsJson(opts.credentialsPath) ?? {};
|
|
227
|
-
const next = { ...creds, mnemonic };
|
|
268
|
+
const next = { ...creds, mnemonic, salt: saltB64 };
|
|
269
|
+
if (registeredUserId)
|
|
270
|
+
next.userId = registeredUserId;
|
|
228
271
|
if (scopeAddress)
|
|
229
272
|
next.scope_address = scopeAddress;
|
|
230
273
|
if (!writeCredentialsJson(opts.credentialsPath, next)) {
|
|
@@ -238,6 +281,7 @@ export async function runRelayPairCli(mode, opts) {
|
|
|
238
281
|
version: opts.pluginVersion,
|
|
239
282
|
});
|
|
240
283
|
opts.logger.info(`pair-cli (relay): session ${session.token.slice(0, 8)}… completed; credentials written` +
|
|
284
|
+
(registeredUserId ? ` (userId=${registeredUserId.slice(0, 8)}…)` : '') +
|
|
241
285
|
(scopeAddress ? ` (scope_address=${scopeAddress})` : ''));
|
|
242
286
|
return { state: 'active' };
|
|
243
287
|
}
|
package/dist/tr-cli.js
CHANGED
|
@@ -41,7 +41,7 @@ const STATE_PATH = CONFIG.onboardingStatePath;
|
|
|
41
41
|
// Auto-synced by skill/scripts/sync-version.mjs from skill/plugin/package.json::version.
|
|
42
42
|
// Do not edit by hand — running tests will catch drift but the publish workflow
|
|
43
43
|
// rewrites this constant at the start of every npm/ClawHub publish.
|
|
44
|
-
const PLUGIN_VERSION = '3.3.11-rc.
|
|
44
|
+
const PLUGIN_VERSION = '3.3.11-rc.3';
|
|
45
45
|
function die(msg, code = 1) {
|
|
46
46
|
process.stderr.write(`tr: ${msg}\n`);
|
|
47
47
|
process.exit(code);
|
package/index.ts
CHANGED
|
@@ -931,15 +931,28 @@ async function initialize(logger: OpenClawPluginApi['logger']): Promise<void> {
|
|
|
931
931
|
}
|
|
932
932
|
|
|
933
933
|
// Derive Smart Account address for subgraph queries (on-chain owner identity).
|
|
934
|
+
// 3.3.11-rc.3: NEVER fall back to userId on derivation failure — the subgraph's
|
|
935
|
+
// `owner` field is typed `Bytes!` (0x-prefixed hex) and rejects userId UUIDs
|
|
936
|
+
// with `Failed to decode Bytes value: Invalid character 'q' at position 0`
|
|
937
|
+
// (because userIds often start with non-hex chars like q/r/y). When SA
|
|
938
|
+
// derivation fails the only safe path is to leave subgraphOwner unset and
|
|
939
|
+
// fail every subsequent on-chain operation with a clear "smart-account
|
|
940
|
+
// unavailable" error rather than spamming the subgraph with garbage Bytes.
|
|
934
941
|
if (isSubgraphMode()) {
|
|
935
942
|
try {
|
|
936
943
|
const config = getSubgraphConfig();
|
|
937
944
|
subgraphOwner = await deriveSmartAccountAddress(config.mnemonic, config.chainId);
|
|
938
945
|
logger.info(`Subgraph owner (Smart Account): ${subgraphOwner}`);
|
|
939
946
|
} catch (err) {
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
947
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
948
|
+
logger.error(
|
|
949
|
+
`Smart Account derivation failed: ${msg} — subgraph reads/writes will be skipped this session ` +
|
|
950
|
+
'(no Bytes-format owner available). Verify mnemonic in credentials.json.',
|
|
951
|
+
);
|
|
952
|
+
// Leave subgraphOwner undefined. Code paths that read it must guard
|
|
953
|
+
// against undefined and skip the subgraph round-trip rather than
|
|
954
|
+
// sending a malformed query.
|
|
955
|
+
subgraphOwner = null;
|
|
943
956
|
}
|
|
944
957
|
}
|
|
945
958
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totalreclaw/totalreclaw",
|
|
3
|
-
"version": "3.3.11-rc.
|
|
3
|
+
"version": "3.3.11-rc.3",
|
|
4
4
|
"description": "End-to-end encrypted, agent-portable memory for OpenClaw and any LLM-agent runtime. XChaCha20-Poly1305 with protobuf v4 + on-chain Memory Taxonomy v1 (claim / preference / directive / commitment / episode / summary).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
package/pair-cli-relay.ts
CHANGED
|
@@ -58,6 +58,8 @@ import {
|
|
|
58
58
|
} from './pair-remote-client.js';
|
|
59
59
|
import { setRecoveryPhraseOverride } from './config.js';
|
|
60
60
|
import { encodePng, encodeUnicode } from './pair-qr.js';
|
|
61
|
+
import { deriveKeys, computeAuthKeyHash } from './crypto.js';
|
|
62
|
+
import { createApiClient } from './api-client.js';
|
|
61
63
|
import type {
|
|
62
64
|
PairCliIo,
|
|
63
65
|
PairCliJsonPayload,
|
|
@@ -272,6 +274,51 @@ export async function runRelayPairCli(
|
|
|
272
274
|
phraseValidator: (p: string) => validateMnemonic(p, wordlist),
|
|
273
275
|
completePairing: async ({ mnemonic }) => {
|
|
274
276
|
try {
|
|
277
|
+
// 3.3.11-rc.3: derive auth key + salt from mnemonic and pre-register
|
|
278
|
+
// with the relay HERE (not deferred to the plugin's next register()
|
|
279
|
+
// load). Pedro's 2026-05-06 QA found that pop-os was leaving
|
|
280
|
+
// credentials.json with only `{mnemonic}` because the plugin's
|
|
281
|
+
// post-pair register() either didn't run (post-SIGUSR1 plugin-skip
|
|
282
|
+
// bug) or partially failed before writing userId/salt back to disk.
|
|
283
|
+
// Doing it inline here means a successful pair is self-contained:
|
|
284
|
+
// browser uploads phrase → completePairing derives keys → register
|
|
285
|
+
// → write {userId, salt, mnemonic, scope_address}. Plugin's
|
|
286
|
+
// register() on next boot just authenticates with the existing
|
|
287
|
+
// credentials.
|
|
288
|
+
const keys = deriveKeys(mnemonic);
|
|
289
|
+
const authKeyHash = computeAuthKeyHash(keys.authKey);
|
|
290
|
+
const saltHex = keys.salt.toString('hex');
|
|
291
|
+
const saltB64 = keys.salt.toString('base64');
|
|
292
|
+
|
|
293
|
+
let registeredUserId: string | undefined;
|
|
294
|
+
try {
|
|
295
|
+
// wss://… → https://… for the REST register call. The relay
|
|
296
|
+
// serves both protocols on the same host.
|
|
297
|
+
const httpsBase = opts.relayBaseUrl.replace(/^ws/, 'http').replace(/\/+$/, '');
|
|
298
|
+
const apiClient = createApiClient(httpsBase);
|
|
299
|
+
const result = await apiClient.register(authKeyHash, saltHex);
|
|
300
|
+
registeredUserId = result.user_id;
|
|
301
|
+
opts.logger.info(
|
|
302
|
+
`pair-cli (relay): registered user_id=${registeredUserId} (salt + auth-key persisted)`,
|
|
303
|
+
);
|
|
304
|
+
} catch (regErr) {
|
|
305
|
+
const msg = regErr instanceof Error ? regErr.message : String(regErr);
|
|
306
|
+
// USER_EXISTS in subgraph mode → derive userId deterministically
|
|
307
|
+
// from auth-key hash so the credentials are still complete.
|
|
308
|
+
// Other errors → continue with mnemonic-only creds; the plugin's
|
|
309
|
+
// register() will retry on next load.
|
|
310
|
+
if (msg.includes('USER_EXISTS')) {
|
|
311
|
+
registeredUserId = authKeyHash.slice(0, 32);
|
|
312
|
+
opts.logger.info(
|
|
313
|
+
`pair-cli (relay): USER_EXISTS — using derived userId=${registeredUserId}`,
|
|
314
|
+
);
|
|
315
|
+
} else {
|
|
316
|
+
opts.logger.warn(
|
|
317
|
+
`pair-cli (relay): /v1/register failed (best-effort, will retry on plugin load): ${msg}`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
275
322
|
let scopeAddress: string | undefined;
|
|
276
323
|
try {
|
|
277
324
|
scopeAddress = await opts.deriveScopeAddress(mnemonic);
|
|
@@ -283,7 +330,8 @@ export async function runRelayPairCli(
|
|
|
283
330
|
);
|
|
284
331
|
}
|
|
285
332
|
const creds = loadCredentialsJson(opts.credentialsPath) ?? {};
|
|
286
|
-
const next: typeof creds = { ...creds, mnemonic };
|
|
333
|
+
const next: typeof creds = { ...creds, mnemonic, salt: saltB64 };
|
|
334
|
+
if (registeredUserId) next.userId = registeredUserId;
|
|
287
335
|
if (scopeAddress) next.scope_address = scopeAddress;
|
|
288
336
|
if (!writeCredentialsJson(opts.credentialsPath, next)) {
|
|
289
337
|
return { state: 'error', error: 'credentials_write_failed' };
|
|
@@ -297,6 +345,7 @@ export async function runRelayPairCli(
|
|
|
297
345
|
});
|
|
298
346
|
opts.logger.info(
|
|
299
347
|
`pair-cli (relay): session ${session.token.slice(0, 8)}… completed; credentials written` +
|
|
348
|
+
(registeredUserId ? ` (userId=${registeredUserId.slice(0, 8)}…)` : '') +
|
|
300
349
|
(scopeAddress ? ` (scope_address=${scopeAddress})` : ''),
|
|
301
350
|
);
|
|
302
351
|
return { state: 'active' };
|
package/skill.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "totalreclaw",
|
|
3
|
-
"version": "3.3.11-rc.
|
|
3
|
+
"version": "3.3.11-rc.3",
|
|
4
4
|
"description": "End-to-end encrypted memory for AI agents — portable, yours forever. XChaCha20-Poly1305 E2EE: server never sees plaintext.",
|
|
5
5
|
"author": "TotalReclaw Team",
|
|
6
6
|
"license": "MIT",
|
package/tr-cli.ts
CHANGED
|
@@ -52,7 +52,7 @@ const STATE_PATH = CONFIG.onboardingStatePath;
|
|
|
52
52
|
// Auto-synced by skill/scripts/sync-version.mjs from skill/plugin/package.json::version.
|
|
53
53
|
// Do not edit by hand — running tests will catch drift but the publish workflow
|
|
54
54
|
// rewrites this constant at the start of every npm/ClawHub publish.
|
|
55
|
-
const PLUGIN_VERSION = '3.3.11-rc.
|
|
55
|
+
const PLUGIN_VERSION = '3.3.11-rc.3';
|
|
56
56
|
|
|
57
57
|
function die(msg: string, code = 1): never {
|
|
58
58
|
process.stderr.write(`tr: ${msg}\n`);
|