@totalreclaw/totalreclaw 3.3.1-rc.10 → 3.3.1-rc.12
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 +77 -1
- package/SKILL.md +15 -3
- package/config.ts +15 -0
- package/index.ts +151 -166
- package/package.json +8 -4
- package/pair-crypto.ts +34 -24
- package/pair-page.ts +17 -17
- package/pair-qr.ts +152 -0
- package/pair-remote-client.ts +540 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,9 +4,85 @@ 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.1-rc.12] — 2026-04-23
|
|
8
|
+
|
|
9
|
+
**Ship-stopper fix for rc.11.** The relay-served pair page's submit
|
|
10
|
+
button threw `NotSupportedError: Failed to execute 'importKey' on
|
|
11
|
+
'SubtleCrypto': Algorithm: Unrecognized name` when the user clicked
|
|
12
|
+
"Seal key and finish". Root cause: `ChaCha20-Poly1305` is NOT
|
|
13
|
+
implemented in the Web Crypto API of Chrome / Safari / Edge — the
|
|
14
|
+
spec exposes `AES-GCM` as the only AEAD. rc.10/rc.11 never worked
|
|
15
|
+
end-to-end for any user; every pair attempt failed silently and the
|
|
16
|
+
token expired without logging a failure — GH issue #79.
|
|
17
|
+
|
|
18
|
+
rc.12 swaps the cipher suite from ChaCha20-Poly1305 to AES-256-GCM on
|
|
19
|
+
both sides (browser + gateway). Wire shape unchanged — still 12-byte
|
|
20
|
+
nonce, 16-byte tag, sid-bound AAD, base64url encoding. HKDF info bumped
|
|
21
|
+
from `totalreclaw-pair-v1` to `totalreclaw-pair-v2` so rc.11 ciphertexts
|
|
22
|
+
cannot collide with rc.12 keys (fail-closed on any version skew).
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- `skill/plugin/pair-crypto.ts`: `aeadDecrypt` / `aeadEncryptWithSessionKey`
|
|
26
|
+
switched from `chacha20-poly1305` to `aes-256-gcm`. `HKDF_INFO` bumped
|
|
27
|
+
to `totalreclaw-pair-v2`.
|
|
28
|
+
- `skill/plugin/pair-page.ts` (local-mode pair page): WebCrypto
|
|
29
|
+
`ChaCha20-Poly1305` calls swapped to `AES-GCM`. Capability probe
|
|
30
|
+
function renamed `chaChaSupported` → `aesGcmSupported`.
|
|
31
|
+
|
|
32
|
+
### Observability
|
|
33
|
+
- The relay's `pair-html.ts` (user-facing page) now reports phase-labelled
|
|
34
|
+
error messages so a network / encrypt / submit failure no longer masks
|
|
35
|
+
as a silent "stuck on acknowledge screen". Relay PR (fix/pair-aes-gcm-rc12)
|
|
36
|
+
is the canonical fix for the issue reported in #79.
|
|
37
|
+
|
|
38
|
+
## [3.3.1-rc.11] — 2026-04-23
|
|
39
|
+
|
|
40
|
+
OpenClaw-side universal pair reachability — the plugin's `totalreclaw_pair` tool now routes through the relay WebSocket by default, mirroring the Python `2.3.1rc10` pivot on the Hermes side. The URL returned to the user is `https://api-staging.totalreclaw.xyz/pair/p/<token>#pk=<gateway_pubkey>` instead of the previous `http://<gateway-host>:<port>/plugin/totalreclaw/pair/finish?sid=<sid>#pk=…`. Managed hosts, Docker-in-cloud setups, phone-scan-QR flows, and split-network operators can now complete pairing without the browser needing loopback or LAN access to the gateway.
|
|
41
|
+
|
|
42
|
+
Paired with Hermes Python `2.3.1rc11` — both clients now reach for the relay by default, and `TOTALRECLAW_PAIR_MODE=local` on either side restores the rc.4–rc.10 loopback flow for air-gapped / self-hosted deployments.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- **`skill/plugin/pair-remote-client.ts`** — new. TypeScript mirror of `python/src/totalreclaw/pair/remote_client.py` (rc.10 Hermes):
|
|
47
|
+
- `openRemotePairSession({ relayBaseUrl?, pin?, clientId?, mode? })` — generates an ephemeral x25519 keypair via the existing `pair-crypto.ts` module, opens a WebSocket to `/pair/session/open`, sends `{type:"open", gateway_pubkey, pin, client_id, mode}`, and returns a `RemotePairSession` handle containing the user-facing URL (with `#pk=` fragment), PIN, token, expiry, and the live WebSocket.
|
|
48
|
+
- `awaitPhraseUpload(session, { completePairing, phraseValidator?, timeoutMs? })` — blocks on the kept-open WebSocket until the relay pushes `{type:"forward", client_pubkey, nonce, ciphertext}`. Decrypts locally via `decryptPairingPayload` using the gateway's private key (same ECDH + HKDF + ChaCha20-Poly1305 primitives as rc.10's loopback flow — byte-compatible with Python's `pair.crypto`). Runs the caller-supplied `completePairing` handler and sends `{type:"ack"}` back on success or `{type:"nack", error}` on validator / decrypt / completion failure.
|
|
49
|
+
- `pairViaRelay(...)` — one-shot convenience wrapper for tests and simple callers.
|
|
50
|
+
- **`ws` runtime dep** (`^8.18.3`) + **`@types/ws`** — pure-JS WebSocket client. Transitive already via `@totalreclaw/core`; rc.11 promotes it to a direct dep so the plugin's own import graph is explicit.
|
|
51
|
+
- **`TOTALRECLAW_PAIR_MODE`** env (plugin side) — mirrors the Python env. Unset or any non-`local` value routes through the relay; `local` preserves the rc.4–rc.10 loopback HTTP server served by `pair-http.ts` (`/plugin/totalreclaw/pair/{finish,start,respond,status}`).
|
|
52
|
+
- **`TOTALRECLAW_PAIR_RELAY_URL`** env (plugin side) — self-hosters can point at their own relay. Defaults to `wss://api-staging.totalreclaw.xyz`.
|
|
53
|
+
- **`skill/plugin/pair-remote-client.test.ts`** — 20 assertions across 5 scenarios: happy-path round-trip, invalid-phrase nack, relay open error, decrypt failure, https-to-wss scheme conversion. Runs against a local `ws` server stub — no network dependency.
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
|
|
57
|
+
- **`totalreclaw_pair` tool** now branches on `CONFIG.pairMode`. In relay mode it returns the URL + PIN immediately and schedules a background task that blocks on the WebSocket until the browser completes (or the TTL lapses). Credentials-write happens in that background task via the same `loadCredentialsJson` / `writeCredentialsJson` / `setRecoveryPhraseOverride` / `writeOnboardingState` side-effect chain that the loopback `pair-http.respond` handler uses — so the onboarding-state flip remains identical. Tool payload shape unchanged (`{url, pin, expires_at_ms, qr_ascii, qr_png_b64, qr_unicode, mode}`) except for a new `transport: 'relay' | 'local'` field that tooling (QA harness, telemetry) can use to confirm which path served a given URL.
|
|
58
|
+
|
|
59
|
+
### Phrase-safety invariants (preserved)
|
|
60
|
+
|
|
61
|
+
- Relay is blind: the gateway's ephemeral x25519 private key never leaves the plugin host. The relay forwards opaque ciphertext; it cannot derive the symmetric key.
|
|
62
|
+
- PIN is out-of-band: the user reads the PIN from agent chat and types it into the browser. The relay stores the PIN in memory only; logs carry no PIN, no ciphertext, no pubkey, no phrase.
|
|
63
|
+
- Session state is in-memory on the relay with a 5-minute TTL. Redis deferred to Phase 2 per the design blueprint.
|
|
64
|
+
- Backwards-compat: `TOTALRECLAW_PAIR_MODE=local` preserves every bit of the rc.4–rc.10 flow — same loopback HTTP server, same session store, same browser page, same decrypt handler.
|
|
65
|
+
|
|
66
|
+
### Mechanism / byte-compat
|
|
67
|
+
|
|
68
|
+
The crypto is a literal TypeScript binding against the same `pair-crypto.ts` module `pair-http.ts` already imports. No new cipher suite, no new wire format — only the transport (WebSocket to relay + relay-served HTML page) differs from the loopback path. A ciphertext produced by the relay-served `pair-html.ts` page decrypts under the same gateway private key using the same `decryptPairingPayload(...)` call path. This is deliberate: `pair-crypto.ts` is the byte-compat anchor shared with Python's `pair.crypto`, and rc.11 extends that anchor to the relay wire.
|
|
69
|
+
|
|
7
70
|
## [3.3.1-rc.10] — 2026-04-23
|
|
8
71
|
|
|
9
|
-
Coordinated version bump with Hermes Python `2.3.1rc10`. rc.10 ships the relay-brokered pair flow — see `python/CHANGELOG.md` (the `2.3.1rc10` entry) for the full design.
|
|
72
|
+
Coordinated version bump with Hermes Python `2.3.1rc10`. rc.10 ships the relay-brokered pair flow — see `python/CHANGELOG.md` (the `2.3.1rc10` entry) for the full design. The `totalreclaw_pair` pair URL on the OpenClaw plugin side still uses the gateway-loopback HTTP server (the OpenClaw plugin runs in-process alongside a browser on the same host for most deployments, so the loopback URL actually reaches the user). The relay-brokered path is currently Hermes-side only — the OpenClaw plugin can pick it up in a later RC if the same universal-reachability problem starts biting OpenClaw users.
|
|
73
|
+
|
|
74
|
+
Bundled into rc.10: the previously-parked rc.5 QR display layer from PR #76 (`pair-qr.ts` + `pair-qr.test.ts`, tool-payload `qr_png_b64` + `qr_unicode` fields, `totalreclaw_setup` / `totalreclaw_onboarding_start` stub removal). All rebased onto main via the chore/rc.10-qr-rebase-pr76 branch.
|
|
75
|
+
|
|
76
|
+
### Added (rebased from PR #76)
|
|
77
|
+
|
|
78
|
+
- **`skill/plugin/pair-qr.ts`** — new. QR encoder module wrapping `qrcode` (PNG) + `qrcode-terminal` (Unicode block). Same contract as the Python side (`totalreclaw.pair.qr`).
|
|
79
|
+
- **`totalreclaw_pair` tool payload** — the `details` block now carries `qr_png_b64` (base64 PNG for image transports) and `qr_unicode` (terminal block-char string) alongside the existing `qr_ascii`. URL + PIN unchanged.
|
|
80
|
+
- **SKILL.md "Rendering the QR on your transport" section** — per-transport agent rendering guidance (Telegram attachment, terminal inline, web chat `<img>` embed).
|
|
81
|
+
- **`qrcode` + `@types/qrcode`** runtime deps.
|
|
82
|
+
|
|
83
|
+
### Removed (rc.5 phrase-safety carve-out closure, rebased)
|
|
84
|
+
|
|
85
|
+
- **`totalreclaw_setup` + `totalreclaw_onboarding_start`** agent tools — both were neutered pointer stubs in rc.4; rc.5 auto-QA flagged them as future-regression surface and their mere presence signalled to agents that "phrase handling happens here". Deleted outright in rc.5, preserved through rc.10. `skill/plugin/phrase-safety-registry.test.ts` now asserts neither name is registered.
|
|
10
86
|
|
|
11
87
|
Version bump reason: rc cadence keeps Python + plugin aligned so the release-pipeline tracker carries them through QA as one artifact set.
|
|
12
88
|
|
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 totalreclaw_remember / totalreclaw_recall. Trigger on 'install TotalReclaw', 'set up TotalReclaw', 'restore my recovery phrase', or any remember / recall request."
|
|
4
|
-
version: 3.3.1-rc.
|
|
4
|
+
version: 3.3.1-rc.11
|
|
5
5
|
author: TotalReclaw Team
|
|
6
6
|
license: MIT
|
|
7
7
|
homepage: https://totalreclaw.xyz
|
|
@@ -34,7 +34,7 @@ If the user says "install totalreclaw" (or any variant) AND no `totalreclaw_*` t
|
|
|
34
34
|
2. Tell the user: *"Plugin installed. Restarting the gateway so the tools bind."* Then ask the user to restart the gateway: `openclaw gateway restart` (native) or `docker restart tr-openclaw` (Docker). The agent-facing shell CANNOT self-restart the gateway it's running inside of — this step has to be a user action. Wait for the user to confirm the restart is done.
|
|
35
35
|
3. After restart: verify `totalreclaw_pair` is now in your toolset. If not, tell the user the restart didn't take effect and to retry the restart command. Do not loop the install.
|
|
36
36
|
4. Check `~/.totalreclaw/credentials.json` (via `test -s ~/.totalreclaw/credentials.json`). If it exists, confirm *"TotalReclaw is already set up."* and stop.
|
|
37
|
-
5. Call `totalreclaw_pair`. Returns `{url, pin, qr_ascii, expires_at_ms}
|
|
37
|
+
5. Call `totalreclaw_pair`. Returns `{url, pin, qr_ascii, qr_png_b64, qr_unicode, expires_at_ms}` (see "Rendering the QR on your transport" below for the rc.5+ additions).
|
|
38
38
|
6. Relay verbatim: *"Open <url> in your browser. Enter your 12-word recovery phrase (or let the browser generate one). Confirm PIN <pin>. I'll wait for you to say done."*
|
|
39
39
|
7. After user says done: verify `~/.totalreclaw/credentials.json`. If missing, call `totalreclaw_pair` again (PIN likely expired) and resend.
|
|
40
40
|
8. Confirm: *"TotalReclaw set up ✓. Ask me to remember or recall anything — try `totalreclaw_remember` or ask me to remember something."*
|
|
@@ -47,11 +47,23 @@ Used when the user pastes a verbose prompt describing skill install + plugin ins
|
|
|
47
47
|
|
|
48
48
|
1. `openclaw plugins install @totalreclaw/totalreclaw` (stable). For RC: `@totalreclaw/totalreclaw@rc`.
|
|
49
49
|
2. Restart gateway so plugin routes + tools bind. Docker: `docker restart tr-openclaw`. Native: `openclaw gateway restart`. Wait ~5s; confirm the gateway is back up. (If you are running inside the gateway, ask the user to run the restart command and wait for confirmation.)
|
|
50
|
-
3. Call `totalreclaw_pair`. Returns `{url, pin, qr_ascii, expires_at_ms}
|
|
50
|
+
3. Call `totalreclaw_pair`. Returns `{url, pin, qr_ascii, qr_png_b64, qr_unicode, expires_at_ms}` (see "Rendering the QR on your transport" below for the rc.5+ additions).
|
|
51
51
|
4. Relay to user verbatim: *"Open <url> in your browser. Enter your 12-word recovery phrase (or let the browser generate one). Confirm PIN <pin>. I'll wait for you to say done."*
|
|
52
52
|
5. After user says done: verify `~/.totalreclaw/credentials.json` exists and is non-empty. If missing, call `totalreclaw_pair` again (PIN likely expired) and resend.
|
|
53
53
|
6. Confirm: *"TotalReclaw is set up. Ask me to remember or recall anything."*
|
|
54
54
|
|
|
55
|
+
## Rendering the QR on your transport (rc.5+)
|
|
56
|
+
|
|
57
|
+
When you call `totalreclaw_pair` and receive the payload, tell the user: *"Open the URL below or scan this QR code from your phone. PIN: <pin>."*
|
|
58
|
+
|
|
59
|
+
Then render the QR based on your chat transport:
|
|
60
|
+
|
|
61
|
+
- **Transport supports image attachments** (Telegram, Slack, web chat, any transport with file/image upload): decode `qr_png_b64` from base64 to bytes, save to a tempfile with a `.png` suffix, and attach the tempfile as an image in your reply. Also show the URL + PIN as text so users who prefer clicking can still do so.
|
|
62
|
+
- **Terminal-only transport** (OpenClaw native CLI chat, bare SSH shell): emit the `qr_unicode` string inline in your reply. It renders as block characters that display correctly in most terminal fonts; the user scans from their phone.
|
|
63
|
+
- **Web chat with inline-image support**: embed as `<img src="data:image/png;base64,<qr_png_b64>" alt="Pair QR">` if the chat client renders HTML; otherwise fall back to attachment.
|
|
64
|
+
|
|
65
|
+
The QR encodes ONLY the pair URL. The 6-digit PIN is a separate out-of-band confirmation — it is deliberately NOT baked into the QR (dual-channel design: scan + type). Never log, print, or re-emit the PIN outside of the user-facing reply. The PIN is a shared secret between you and the user; it must NOT end up in any other tool call, any log file, or any memory store.
|
|
66
|
+
|
|
55
67
|
## Phrase safety (HARD — never break)
|
|
56
68
|
|
|
57
69
|
NEVER echo / generate / ask the user to paste a recovery phrase in chat. NEVER invoke `openclaw totalreclaw onboard`, `totalreclaw setup`, or any phrase-touching CLI via your shell tool (stdout enters LLM context). The ONLY agent-facilitated setup path is `totalreclaw_pair`. If the user pastes a phrase anyway: tell them it is compromised, call `totalreclaw_pair` with `mode=generate` for a fresh wallet.
|
package/config.ts
CHANGED
|
@@ -105,6 +105,21 @@ export const CONFIG = {
|
|
|
105
105
|
// for 15-min TTL windows; 0600 mode.
|
|
106
106
|
pairSessionsPath: process.env.TOTALRECLAW_PAIR_SESSIONS_PATH || path.join(home, '.totalreclaw', 'pair-sessions.json'),
|
|
107
107
|
|
|
108
|
+
// 3.3.1-rc.11 — pair-flow transport selector. Mirrors the Python-side
|
|
109
|
+
// `TOTALRECLAW_PAIR_MODE` env (rc.10). `'relay'` (default) routes
|
|
110
|
+
// `totalreclaw_pair` through the universal-reachability WebSocket relay at
|
|
111
|
+
// `TOTALRECLAW_PAIR_RELAY_URL`. `'local'` preserves the rc.4–rc.10 loopback
|
|
112
|
+
// HTTP flow (the plugin serves `/plugin/totalreclaw/pair/*` via
|
|
113
|
+
// `pair-http.ts`). Air-gapped / self-hosted users can pin `'local'` here.
|
|
114
|
+
pairMode: (() => {
|
|
115
|
+
const v = (process.env.TOTALRECLAW_PAIR_MODE ?? '').trim().toLowerCase();
|
|
116
|
+
return v === 'local' ? 'local' : 'relay';
|
|
117
|
+
})() as 'relay' | 'local',
|
|
118
|
+
// 3.3.1-rc.11 — relay base URL for the WebSocket-brokered pair flow.
|
|
119
|
+
// `wss://` preferred; `https://` is rewritten in the remote-client.
|
|
120
|
+
pairRelayUrl: (process.env.TOTALRECLAW_PAIR_RELAY_URL
|
|
121
|
+
|| 'wss://api-staging.totalreclaw.xyz').replace(/\/+$/, ''),
|
|
122
|
+
|
|
108
123
|
// Chain — chainId is no longer user-configurable. It is auto-detected from
|
|
109
124
|
// the relay billing response (free = Base Sepolia / 84532, Pro = Gnosis /
|
|
110
125
|
// 100). The default here is used only before the first billing lookup
|
package/index.ts
CHANGED
|
@@ -4891,162 +4891,36 @@ const plugin = {
|
|
|
4891
4891
|
);
|
|
4892
4892
|
|
|
4893
4893
|
// ---------------------------------------------------------------
|
|
4894
|
-
//
|
|
4894
|
+
// Tools: totalreclaw_setup + totalreclaw_onboarding_start —
|
|
4895
|
+
// REMOVED in 3.3.1-rc.5 (phrase-safety carve-out closure).
|
|
4895
4896
|
// ---------------------------------------------------------------
|
|
4896
4897
|
//
|
|
4897
|
-
//
|
|
4898
|
-
//
|
|
4899
|
-
//
|
|
4900
|
-
//
|
|
4901
|
-
//
|
|
4898
|
+
// rc.4 left these two registrations in place as *neutered* stubs —
|
|
4899
|
+
// ``totalreclaw_setup`` rejected any ``recovery_phrase`` argument
|
|
4900
|
+
// and returned a CLI-pointer message; ``totalreclaw_onboarding_start``
|
|
4901
|
+
// was already pointer-only. Neither path could leak a phrase in
|
|
4902
|
+
// rc.4, but the rc.4 auto-QA (2026-04-22) flagged them as future-
|
|
4903
|
+
// regression surface: any future patch that re-enables phrase
|
|
4904
|
+
// acceptance (e.g. a flag-driven "power-user" path) would silently
|
|
4905
|
+
// re-open the leak, and their mere presence in the tool registry
|
|
4906
|
+
// keeps signalling to agents that "phrase handling happens here".
|
|
4902
4907
|
//
|
|
4903
|
-
//
|
|
4904
|
-
//
|
|
4905
|
-
//
|
|
4906
|
-
//
|
|
4907
|
-
//
|
|
4908
|
+
// Per ``project_phrase_safety_rule.md`` the ONLY approved agent-
|
|
4909
|
+
// facilitated setup surface is ``totalreclaw_pair`` (browser-side
|
|
4910
|
+
// crypto keeps the phrase out of the LLM round-trip by construction).
|
|
4911
|
+
// rc.5 deletes both registrations outright. The underlying CLI
|
|
4912
|
+
// wizard (``openclaw totalreclaw onboard``) is unchanged — users
|
|
4913
|
+
// run it in their own terminal, outside any agent shell.
|
|
4908
4914
|
//
|
|
4909
|
-
//
|
|
4910
|
-
//
|
|
4911
|
-
//
|
|
4912
|
-
// CLI wizard.
|
|
4913
|
-
api.registerTool(
|
|
4914
|
-
{
|
|
4915
|
-
name: 'totalreclaw_setup',
|
|
4916
|
-
label: 'TotalReclaw setup (deprecated — redirect to CLI)',
|
|
4917
|
-
description:
|
|
4918
|
-
'DEPRECATED in 3.2.0. This tool no longer accepts recovery phrases or performs ' +
|
|
4919
|
-
'setup. It returns a pointer to `openclaw totalreclaw onboard` — the secure CLI ' +
|
|
4920
|
-
'wizard that runs on the user\'s terminal so the phrase never touches the LLM ' +
|
|
4921
|
-
'provider. Prefer calling `totalreclaw_onboarding_start` for the same pointer.',
|
|
4922
|
-
parameters: {
|
|
4923
|
-
type: 'object',
|
|
4924
|
-
properties: {
|
|
4925
|
-
recovery_phrase: {
|
|
4926
|
-
type: 'string',
|
|
4927
|
-
description:
|
|
4928
|
-
'Legacy parameter — IGNORED in 3.2.0. If provided, the tool returns a ' +
|
|
4929
|
-
'security warning explaining that phrases must never be pasted through ' +
|
|
4930
|
-
'chat. Use the `openclaw totalreclaw onboard` CLI wizard to import an ' +
|
|
4931
|
-
'existing phrase safely.',
|
|
4932
|
-
},
|
|
4933
|
-
},
|
|
4934
|
-
additionalProperties: false,
|
|
4935
|
-
},
|
|
4936
|
-
async execute(_toolCallId: string, params: { recovery_phrase?: string }) {
|
|
4937
|
-
// Phrase-passing is a security boundary violation in 3.2.0. Reject
|
|
4938
|
-
// with a message that explains WHY — the LLM might try again with
|
|
4939
|
-
// a different shape otherwise.
|
|
4940
|
-
if (typeof params?.recovery_phrase === 'string' && params.recovery_phrase.trim().length > 0) {
|
|
4941
|
-
api.logger.warn(
|
|
4942
|
-
'totalreclaw_setup: rejected phrase-passing call (3.2.0 deprecation).',
|
|
4943
|
-
);
|
|
4944
|
-
return {
|
|
4945
|
-
content: [{
|
|
4946
|
-
type: 'text',
|
|
4947
|
-
text:
|
|
4948
|
-
'For security, TotalReclaw no longer accepts a recovery phrase through ' +
|
|
4949
|
-
'chat. Pasting a phrase into this tool would ship it to the LLM provider, ' +
|
|
4950
|
-
'which defeats the whole point of end-to-end encryption.\n\n' +
|
|
4951
|
-
'Ask the user to open a terminal on their machine and run:\n\n' +
|
|
4952
|
-
' openclaw totalreclaw onboard\n\n' +
|
|
4953
|
-
'The wizard imports an existing phrase via a hidden stdin prompt that ' +
|
|
4954
|
-
'never touches the LLM, the transcript, or the network.',
|
|
4955
|
-
}],
|
|
4956
|
-
};
|
|
4957
|
-
}
|
|
4958
|
-
|
|
4959
|
-
// No-arg call against an already-active state: confirm + move on.
|
|
4960
|
-
const state = resolveOnboardingState(CREDENTIALS_PATH, CONFIG.onboardingStatePath);
|
|
4961
|
-
if (state.onboardingState === 'active') {
|
|
4962
|
-
return {
|
|
4963
|
-
content: [{
|
|
4964
|
-
type: 'text',
|
|
4965
|
-
text:
|
|
4966
|
-
'TotalReclaw is already set up and active on this machine. Memory tools ' +
|
|
4967
|
-
'are unblocked — you can call `totalreclaw_remember` and `totalreclaw_recall` ' +
|
|
4968
|
-
'directly. If the user wants to rotate phrases, have them delete ' +
|
|
4969
|
-
'`~/.totalreclaw/credentials.json` and run `openclaw totalreclaw onboard` again.',
|
|
4970
|
-
}],
|
|
4971
|
-
};
|
|
4972
|
-
}
|
|
4973
|
-
|
|
4974
|
-
// Fresh state, no phrase: redirect to the CLI wizard.
|
|
4975
|
-
return {
|
|
4976
|
-
content: [{
|
|
4977
|
-
type: 'text',
|
|
4978
|
-
text:
|
|
4979
|
-
'TotalReclaw setup must run on the user\'s local terminal so the recovery ' +
|
|
4980
|
-
'phrase never touches the LLM. Ask the user to open a terminal and run:\n\n' +
|
|
4981
|
-
' openclaw totalreclaw onboard\n\n' +
|
|
4982
|
-
'The wizard walks through generate-new-phrase or import-existing-phrase. ' +
|
|
4983
|
-
'After it completes, memory tools become available automatically. See the ' +
|
|
4984
|
-
'`totalreclaw_onboarding_start` tool for the same pointer in a more ' +
|
|
4985
|
-
'discoverable shape.',
|
|
4986
|
-
}],
|
|
4987
|
-
};
|
|
4988
|
-
},
|
|
4989
|
-
},
|
|
4990
|
-
{ name: 'totalreclaw_setup' },
|
|
4991
|
-
);
|
|
4992
|
-
|
|
4993
|
-
// ---------------------------------------------------------------
|
|
4994
|
-
// Tool: totalreclaw_onboarding_start (3.2.0 pointer-only tool)
|
|
4995
|
-
// ---------------------------------------------------------------
|
|
4915
|
+
// Audit assertion: ``phrase-safety-registry.test.ts`` asserts
|
|
4916
|
+
// neither name is present in the ``api.registerTool`` call list.
|
|
4917
|
+
// Re-adding either fails CI.
|
|
4996
4918
|
//
|
|
4997
|
-
//
|
|
4998
|
-
//
|
|
4999
|
-
//
|
|
5000
|
-
//
|
|
5001
|
-
|
|
5002
|
-
{
|
|
5003
|
-
name: 'totalreclaw_onboarding_start',
|
|
5004
|
-
label: 'TotalReclaw — start onboarding',
|
|
5005
|
-
description:
|
|
5006
|
-
'Call this when the user wants to set up TotalReclaw memory or asks about ' +
|
|
5007
|
-
'enabling memory features. This tool does NOT generate, display, or accept ' +
|
|
5008
|
-
'a recovery phrase — it returns a short pointer that tells the user to run ' +
|
|
5009
|
-
'the onboarding wizard in their local terminal. All phrase handling happens ' +
|
|
5010
|
-
'outside the LLM. If TotalReclaw is already active, the tool returns a ' +
|
|
5011
|
-
'confirmation.',
|
|
5012
|
-
parameters: {
|
|
5013
|
-
type: 'object',
|
|
5014
|
-
properties: {},
|
|
5015
|
-
additionalProperties: false,
|
|
5016
|
-
},
|
|
5017
|
-
async execute() {
|
|
5018
|
-
const state = resolveOnboardingState(CREDENTIALS_PATH, CONFIG.onboardingStatePath);
|
|
5019
|
-
if (state.onboardingState === 'active') {
|
|
5020
|
-
return {
|
|
5021
|
-
content: [{
|
|
5022
|
-
type: 'text',
|
|
5023
|
-
text:
|
|
5024
|
-
'TotalReclaw is already set up on this machine. Your encryption keys are ' +
|
|
5025
|
-
'ready — `totalreclaw_remember`, `totalreclaw_recall`, and the other memory ' +
|
|
5026
|
-
'tools are unblocked. Run `openclaw totalreclaw status` in a terminal for ' +
|
|
5027
|
-
'more detail.',
|
|
5028
|
-
}],
|
|
5029
|
-
};
|
|
5030
|
-
}
|
|
5031
|
-
return {
|
|
5032
|
-
content: [{
|
|
5033
|
-
type: 'text',
|
|
5034
|
-
text:
|
|
5035
|
-
'TotalReclaw onboarding requires a local terminal so your recovery phrase ' +
|
|
5036
|
-
'never touches the LLM provider. On the same machine as your OpenClaw ' +
|
|
5037
|
-
'gateway, open a terminal and run:\n\n' +
|
|
5038
|
-
' openclaw totalreclaw onboard\n\n' +
|
|
5039
|
-
'The wizard will ask whether you want to generate a new phrase or import an ' +
|
|
5040
|
-
'existing TotalReclaw phrase. Both paths display/accept the phrase only on ' +
|
|
5041
|
-
'your terminal — nothing crosses the network. After the wizard completes, ' +
|
|
5042
|
-
'come back here and I\'ll be able to use `totalreclaw_remember` and ' +
|
|
5043
|
-
'`totalreclaw_recall`.',
|
|
5044
|
-
}],
|
|
5045
|
-
};
|
|
5046
|
-
},
|
|
5047
|
-
},
|
|
5048
|
-
{ name: 'totalreclaw_onboarding_start' },
|
|
5049
|
-
);
|
|
4919
|
+
// Historical tombstone (so LLM-assisted contributors don't re-add
|
|
4920
|
+
// the former shape from training-data memory): rc.4 registered two
|
|
4921
|
+
// tools by the names "totalreclaw_setup" and
|
|
4922
|
+
// "totalreclaw_onboarding_start" as pointer-only stubs. Both were
|
|
4923
|
+
// deleted in rc.5. Do not re-introduce.
|
|
5050
4924
|
|
|
5051
4925
|
// ---------------------------------------------------------------
|
|
5052
4926
|
// Tool: totalreclaw_onboard — REMOVED in 3.3.1-rc.4 (phrase-safety).
|
|
@@ -5122,18 +4996,99 @@ const plugin = {
|
|
|
5122
4996
|
const rawMode = params?.mode;
|
|
5123
4997
|
const mode: 'generate' | 'import' =
|
|
5124
4998
|
rawMode === 'import' ? 'import' : 'generate';
|
|
4999
|
+
const pairMode = CONFIG.pairMode;
|
|
5125
5000
|
try {
|
|
5126
|
-
|
|
5127
|
-
|
|
5001
|
+
// 3.3.1-rc.11 — relay-brokered pair by default (universal reachability).
|
|
5002
|
+
// `TOTALRECLAW_PAIR_MODE=local` preserves the rc.4–rc.10 loopback flow
|
|
5003
|
+
// for air-gapped / self-hosted setups. Both paths return the same
|
|
5004
|
+
// tool payload (`{url, pin, expires_at_ms, qr_*, mode, instructions}`);
|
|
5005
|
+
// only the URL origin differs.
|
|
5006
|
+
let url: string;
|
|
5007
|
+
let pin: string;
|
|
5008
|
+
let sidOrToken: string;
|
|
5009
|
+
let expiresAtMs: number;
|
|
5010
|
+
let localSession: import('./pair-session-store.js').PairSession | undefined;
|
|
5011
|
+
|
|
5012
|
+
if (pairMode === 'relay') {
|
|
5013
|
+
const { openRemotePairSession, awaitPhraseUpload } = await import(
|
|
5014
|
+
'./pair-remote-client.js'
|
|
5015
|
+
);
|
|
5016
|
+
const remoteSession = await openRemotePairSession({
|
|
5017
|
+
relayBaseUrl: CONFIG.pairRelayUrl,
|
|
5018
|
+
mode: mode === 'generate' ? 'generate' : 'import',
|
|
5019
|
+
});
|
|
5020
|
+
url = remoteSession.url;
|
|
5021
|
+
pin = remoteSession.pin;
|
|
5022
|
+
sidOrToken = remoteSession.token;
|
|
5023
|
+
// Relay sends ISO-8601; convert to ms for tool payload parity.
|
|
5024
|
+
const parsed = Date.parse(remoteSession.expiresAt);
|
|
5025
|
+
expiresAtMs = Number.isFinite(parsed)
|
|
5026
|
+
? parsed
|
|
5027
|
+
: Date.now() + 5 * 60_000;
|
|
5028
|
+
// Background task — writes credentials.json + flips state when
|
|
5029
|
+
// the browser completes the flow. Tool handler returns
|
|
5030
|
+
// immediately so the agent can tell the user the URL + PIN.
|
|
5031
|
+
void (async () => {
|
|
5032
|
+
try {
|
|
5033
|
+
await awaitPhraseUpload(remoteSession, {
|
|
5034
|
+
phraseValidator: (p: string) =>
|
|
5035
|
+
validateMnemonic(p, wordlist),
|
|
5036
|
+
completePairing: async ({ mnemonic }) => {
|
|
5037
|
+
try {
|
|
5038
|
+
const creds =
|
|
5039
|
+
loadCredentialsJson(CREDENTIALS_PATH) ?? {};
|
|
5040
|
+
const next = { ...creds, mnemonic };
|
|
5041
|
+
if (!writeCredentialsJson(CREDENTIALS_PATH, next)) {
|
|
5042
|
+
return { state: 'error', error: 'credentials_write_failed' };
|
|
5043
|
+
}
|
|
5044
|
+
setRecoveryPhraseOverride(mnemonic);
|
|
5045
|
+
writeOnboardingState(CONFIG.onboardingStatePath, {
|
|
5046
|
+
onboardingState: 'active',
|
|
5047
|
+
createdBy: mode === 'generate' ? 'generate' : 'import',
|
|
5048
|
+
credentialsCreatedAt: new Date().toISOString(),
|
|
5049
|
+
version: '3.3.1-rc.11',
|
|
5050
|
+
});
|
|
5051
|
+
api.logger.info(
|
|
5052
|
+
`totalreclaw_pair(relay): session ${remoteSession.token.slice(0, 8)}… completed; credentials written`,
|
|
5053
|
+
);
|
|
5054
|
+
return { state: 'active' };
|
|
5055
|
+
} catch (err: unknown) {
|
|
5056
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5057
|
+
api.logger.error(
|
|
5058
|
+
`totalreclaw_pair(relay): completePairing failed: ${msg}`,
|
|
5059
|
+
);
|
|
5060
|
+
return { state: 'error', error: msg };
|
|
5061
|
+
}
|
|
5062
|
+
},
|
|
5063
|
+
});
|
|
5064
|
+
} catch (bgErr: unknown) {
|
|
5065
|
+
// Expected on TTL expiry / user-aborts — log at warn, not error.
|
|
5066
|
+
const bgMsg = bgErr instanceof Error ? bgErr.message : String(bgErr);
|
|
5067
|
+
api.logger.warn(
|
|
5068
|
+
`totalreclaw_pair(relay): background task ended for token=${remoteSession.token.slice(0, 8)}…: ${bgMsg}`,
|
|
5069
|
+
);
|
|
5070
|
+
}
|
|
5071
|
+
})();
|
|
5072
|
+
} else {
|
|
5073
|
+
// Local loopback path (rc.10 behaviour).
|
|
5074
|
+
const { createPairSession } = await import('./pair-session-store.js');
|
|
5075
|
+
const { generateGatewayKeypair } = await import('./pair-crypto.js');
|
|
5076
|
+
const kp = generateGatewayKeypair();
|
|
5077
|
+
const session = await createPairSession(CONFIG.pairSessionsPath, {
|
|
5078
|
+
mode,
|
|
5079
|
+
operatorContext: { channel: 'agent' },
|
|
5080
|
+
rngPrivateKey: () => Buffer.from(kp.skB64, 'base64url'),
|
|
5081
|
+
rngPublicKey: () => Buffer.from(kp.pkB64, 'base64url'),
|
|
5082
|
+
});
|
|
5083
|
+
url = buildPairingUrl(api, session);
|
|
5084
|
+
pin = session.secondaryCode;
|
|
5085
|
+
sidOrToken = session.sid;
|
|
5086
|
+
expiresAtMs = session.expiresAtMs;
|
|
5087
|
+
localSession = session;
|
|
5088
|
+
}
|
|
5089
|
+
|
|
5090
|
+
// QR renderers — same for both modes; input is the URL string.
|
|
5128
5091
|
const { defaultRenderQr } = await import('./pair-cli.js');
|
|
5129
|
-
const kp = generateGatewayKeypair();
|
|
5130
|
-
const session = await createPairSession(CONFIG.pairSessionsPath, {
|
|
5131
|
-
mode,
|
|
5132
|
-
operatorContext: { channel: 'agent' },
|
|
5133
|
-
rngPrivateKey: () => Buffer.from(kp.skB64, 'base64url'),
|
|
5134
|
-
rngPublicKey: () => Buffer.from(kp.pkB64, 'base64url'),
|
|
5135
|
-
});
|
|
5136
|
-
const url = buildPairingUrl(api, session);
|
|
5137
5092
|
const qrAscii = await new Promise<string>((resolve) => {
|
|
5138
5093
|
let settled = false;
|
|
5139
5094
|
const t = setTimeout(() => {
|
|
@@ -5156,16 +5111,40 @@ const plugin = {
|
|
|
5156
5111
|
resolve('');
|
|
5157
5112
|
}
|
|
5158
5113
|
});
|
|
5114
|
+
|
|
5115
|
+
// 3.3.1-rc.5 — PNG + Unicode QR for multi-transport rendering.
|
|
5116
|
+
let qrPngB64 = '';
|
|
5117
|
+
let qrUnicode = '';
|
|
5118
|
+
try {
|
|
5119
|
+
const { encodePng, encodeUnicode } = await import('./pair-qr.js');
|
|
5120
|
+
const [pngBuf, uni] = await Promise.all([
|
|
5121
|
+
encodePng(url),
|
|
5122
|
+
encodeUnicode(url),
|
|
5123
|
+
]);
|
|
5124
|
+
qrPngB64 = pngBuf.toString('base64');
|
|
5125
|
+
qrUnicode = uni;
|
|
5126
|
+
} catch (qrErr: unknown) {
|
|
5127
|
+
api.logger.warn(
|
|
5128
|
+
`totalreclaw_pair: QR encode failed (non-fatal): ${
|
|
5129
|
+
qrErr instanceof Error ? qrErr.message : String(qrErr)
|
|
5130
|
+
}`,
|
|
5131
|
+
);
|
|
5132
|
+
}
|
|
5133
|
+
|
|
5159
5134
|
api.logger.info(
|
|
5160
|
-
`totalreclaw_pair: session ${
|
|
5135
|
+
`totalreclaw_pair: session ${sidOrToken.slice(0, 8)}… mode=${mode} transport=${pairMode} url=${url} qr_png=${qrPngB64.length} qr_unicode=${qrUnicode.length}`,
|
|
5161
5136
|
);
|
|
5137
|
+
// Voidly reference localSession so TS does not flag the unused
|
|
5138
|
+
// local-branch binding. Future rc.12 diagnostics can expose
|
|
5139
|
+
// `session.mode` / `session.status` separately.
|
|
5140
|
+
void localSession;
|
|
5162
5141
|
return {
|
|
5163
5142
|
content: [{
|
|
5164
5143
|
type: 'text',
|
|
5165
5144
|
text:
|
|
5166
5145
|
`Pairing session started.\n\n` +
|
|
5167
5146
|
`URL: ${url}\n\n` +
|
|
5168
|
-
`PIN (type this into the browser): ${
|
|
5147
|
+
`PIN (type this into the browser): ${pin}\n\n` +
|
|
5169
5148
|
(qrAscii ? `QR code:\n\n${qrAscii}\n\n` : '') +
|
|
5170
5149
|
`Instructions for the user:\n` +
|
|
5171
5150
|
`1. Open the URL above on their phone or another browser (scan the QR or copy-paste).\n` +
|
|
@@ -5179,12 +5158,18 @@ const plugin = {
|
|
|
5179
5158
|
`This session expires in ~5 minutes. Run this tool again if you need a fresh URL.`,
|
|
5180
5159
|
}],
|
|
5181
5160
|
details: {
|
|
5182
|
-
sid:
|
|
5161
|
+
sid: sidOrToken,
|
|
5183
5162
|
url,
|
|
5184
|
-
pin
|
|
5163
|
+
pin,
|
|
5185
5164
|
mode,
|
|
5186
|
-
expires_at_ms:
|
|
5165
|
+
expires_at_ms: expiresAtMs,
|
|
5187
5166
|
qr_ascii: qrAscii,
|
|
5167
|
+
qr_png_b64: qrPngB64,
|
|
5168
|
+
qr_unicode: qrUnicode,
|
|
5169
|
+
// rc.11 — surface the transport so downstream tooling (QA
|
|
5170
|
+
// harness asserters, telemetry) can confirm which path
|
|
5171
|
+
// served the URL. Either 'relay' or 'local'.
|
|
5172
|
+
transport: pairMode,
|
|
5188
5173
|
},
|
|
5189
5174
|
};
|
|
5190
5175
|
} catch (err: unknown) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totalreclaw/totalreclaw",
|
|
3
|
-
"version": "3.3.1-rc.
|
|
3
|
+
"version": "3.3.1-rc.12",
|
|
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": [
|
|
@@ -31,11 +31,15 @@
|
|
|
31
31
|
"author": "TotalReclaw Team",
|
|
32
32
|
"license": "MIT",
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"@huggingface/transformers": "^4.0.1",
|
|
34
35
|
"@totalreclaw/client": "^1.2.0",
|
|
35
36
|
"@totalreclaw/core": "^2.1.1",
|
|
36
|
-
"@
|
|
37
|
+
"@types/qrcode": "^1.5.6",
|
|
38
|
+
"@types/ws": "^8.5.12",
|
|
37
39
|
"onnxruntime-node": "^1.24.0",
|
|
38
|
-
"qrcode
|
|
40
|
+
"qrcode": "^1.5.4",
|
|
41
|
+
"qrcode-terminal": "^0.12.0",
|
|
42
|
+
"ws": "^8.18.3"
|
|
39
43
|
},
|
|
40
44
|
"files": [
|
|
41
45
|
"*.ts",
|
|
@@ -50,7 +54,7 @@
|
|
|
50
54
|
"skill.json"
|
|
51
55
|
],
|
|
52
56
|
"scripts": {
|
|
53
|
-
"test": "npx tsx manifest-shape.test.ts && npx tsx config-schema.test.ts && npx tsx llm-profile-reader.test.ts && npx tsx llm-client.test.ts && npx tsx llm-client-retry.test.ts && npx tsx gateway-url.test.ts && npx tsx retype-setscope.test.ts && npx tsx tool-gating.test.ts && npx tsx onboarding-noninteractive.test.ts && npx tsx pair-cli-json.test.ts && npx tsx qa-bug-report.test.ts && npx tsx nonce-serialization.test.ts && npx tsx phrase-safety-registry.test.ts",
|
|
57
|
+
"test": "npx tsx manifest-shape.test.ts && npx tsx config-schema.test.ts && npx tsx llm-profile-reader.test.ts && npx tsx llm-client.test.ts && npx tsx llm-client-retry.test.ts && npx tsx gateway-url.test.ts && npx tsx retype-setscope.test.ts && npx tsx tool-gating.test.ts && npx tsx onboarding-noninteractive.test.ts && npx tsx pair-cli-json.test.ts && npx tsx pair-qr.test.ts && npx tsx pair-remote-client.test.ts && npx tsx qa-bug-report.test.ts && npx tsx nonce-serialization.test.ts && npx tsx phrase-safety-registry.test.ts",
|
|
54
58
|
"check-scanner": "node ../scripts/check-scanner.mjs",
|
|
55
59
|
"prepublishOnly": "node ../scripts/check-scanner.mjs"
|
|
56
60
|
},
|