@totalreclaw/totalreclaw 3.3.1-rc.11 → 3.3.1-rc.13
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 +64 -0
- package/package.json +1 -1
- package/pair-crypto.ts +34 -24
- package/pair-page.ts +28 -17
- package/pair-remote-client.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,70 @@ 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.13] — 2026-04-24
|
|
8
|
+
|
|
9
|
+
Coordinated version bump with Python `2.3.1rc13`. No substantive
|
|
10
|
+
changes to the plugin's own TypeScript — the rc.13 fix lands on the
|
|
11
|
+
Hermes-side (`python/src/totalreclaw/hermes/pair_tool.py`) where the
|
|
12
|
+
asyncio lifecycle regression lived. We keep plugin + Python RC
|
|
13
|
+
numbers in lockstep so the release-pipeline tracker and
|
|
14
|
+
`qa-totalreclaw` skill carry both artifacts through QA as one
|
|
15
|
+
bundle.
|
|
16
|
+
|
|
17
|
+
See the corresponding entry in `python/CHANGELOG.md` for the full
|
|
18
|
+
design: the relay-pair WebSocket is now owned by a dedicated worker
|
|
19
|
+
thread (with its own event loop) so it survives the Hermes
|
|
20
|
+
tool-invocation loop teardown that destroyed the rc.10–rc.12 waiter
|
|
21
|
+
mid-recv and caused every pair attempt to 502.
|
|
22
|
+
|
|
23
|
+
The relay-served production pair page is also replaced with the
|
|
24
|
+
rc.13 wizard UX — a typeform-style 3-step flow (PIN → phrase → done)
|
|
25
|
+
mirroring the `docs/mockups/rc13-pair-wizard/` design. This lands in
|
|
26
|
+
the `totalreclaw-relay` repo PR, not here, but surfaces to every
|
|
27
|
+
OpenClaw user via the default relay pair flow.
|
|
28
|
+
|
|
29
|
+
### Plugin local-mode pair page
|
|
30
|
+
|
|
31
|
+
`skill/plugin/pair-page.ts` (the local-mode fallback served when a
|
|
32
|
+
user sets `TOTALRECLAW_PAIR_MODE=local`) retains its rc.10–rc.12 UX
|
|
33
|
+
shape. The wizard UX port for this file is deferred to rc.14 pending
|
|
34
|
+
a design decision on whether to share a single CSS+JS asset across
|
|
35
|
+
all three pair pages (relay / Python local / plugin local) or keep
|
|
36
|
+
them independently inlined. Local-mode is rarely exercised — the
|
|
37
|
+
plugin defaults to the relay flow via the Hermes Python sidecar and
|
|
38
|
+
only falls back here for air-gapped setups.
|
|
39
|
+
|
|
40
|
+
## [3.3.1-rc.12] — 2026-04-23
|
|
41
|
+
|
|
42
|
+
**Ship-stopper fix for rc.11.** The relay-served pair page's submit
|
|
43
|
+
button threw `NotSupportedError: Failed to execute 'importKey' on
|
|
44
|
+
'SubtleCrypto': Algorithm: Unrecognized name` when the user clicked
|
|
45
|
+
"Seal key and finish". Root cause: `ChaCha20-Poly1305` is NOT
|
|
46
|
+
implemented in the Web Crypto API of Chrome / Safari / Edge — the
|
|
47
|
+
spec exposes `AES-GCM` as the only AEAD. rc.10/rc.11 never worked
|
|
48
|
+
end-to-end for any user; every pair attempt failed silently and the
|
|
49
|
+
token expired without logging a failure — GH issue #79.
|
|
50
|
+
|
|
51
|
+
rc.12 swaps the cipher suite from ChaCha20-Poly1305 to AES-256-GCM on
|
|
52
|
+
both sides (browser + gateway). Wire shape unchanged — still 12-byte
|
|
53
|
+
nonce, 16-byte tag, sid-bound AAD, base64url encoding. HKDF info bumped
|
|
54
|
+
from `totalreclaw-pair-v1` to `totalreclaw-pair-v2` so rc.11 ciphertexts
|
|
55
|
+
cannot collide with rc.12 keys (fail-closed on any version skew).
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
- `skill/plugin/pair-crypto.ts`: `aeadDecrypt` / `aeadEncryptWithSessionKey`
|
|
59
|
+
switched from `chacha20-poly1305` to `aes-256-gcm`. `HKDF_INFO` bumped
|
|
60
|
+
to `totalreclaw-pair-v2`.
|
|
61
|
+
- `skill/plugin/pair-page.ts` (local-mode pair page): WebCrypto
|
|
62
|
+
`ChaCha20-Poly1305` calls swapped to `AES-GCM`. Capability probe
|
|
63
|
+
function renamed `chaChaSupported` → `aesGcmSupported`.
|
|
64
|
+
|
|
65
|
+
### Observability
|
|
66
|
+
- The relay's `pair-html.ts` (user-facing page) now reports phase-labelled
|
|
67
|
+
error messages so a network / encrypt / submit failure no longer masks
|
|
68
|
+
as a silent "stuck on acknowledge screen". Relay PR (fix/pair-aes-gcm-rc12)
|
|
69
|
+
is the canonical fix for the issue reported in #79.
|
|
70
|
+
|
|
7
71
|
## [3.3.1-rc.11] — 2026-04-23
|
|
8
72
|
|
|
9
73
|
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.
|
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.13",
|
|
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-crypto.ts
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pair-crypto — gateway-side cryptographic primitives for the v3.3.
|
|
3
|
-
*
|
|
2
|
+
* pair-crypto — gateway-side cryptographic primitives for the v3.3.x
|
|
3
|
+
* relay-brokered pair flow.
|
|
4
4
|
*
|
|
5
|
-
* Cipher suite (
|
|
5
|
+
* Cipher suite (design doc 3a-3b, cipher swap ratified 2026-04-23 / rc.12):
|
|
6
6
|
* - ECDH on x25519 for key agreement.
|
|
7
7
|
* - HKDF-SHA256 for symmetric-key derivation from the shared secret.
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
8
|
+
* - AES-256-GCM AEAD for the ciphertext payload, with the sid bound as
|
|
9
|
+
* associated data (AD = sid UTF-8 bytes, 12-byte nonce, 16-byte tag).
|
|
10
|
+
*
|
|
11
|
+
* rc.4..rc.11 used ChaCha20-Poly1305, but the Web Crypto API does NOT
|
|
12
|
+
* implement ChaCha20-Poly1305 in Chrome / Safari / Edge. The pair-page
|
|
13
|
+
* submit path silently threw `Algorithm: Unrecognized name` before
|
|
14
|
+
* reaching the network. rc.12 swaps the cipher suite to AES-256-GCM
|
|
15
|
+
* (universally supported in WebCrypto) and bumps HKDF_INFO to v2 so
|
|
16
|
+
* cross-version mis-pairs fail closed rather than garble.
|
|
10
17
|
*
|
|
11
18
|
* Every primitive is provided by the Node built-in `node:crypto` module
|
|
12
19
|
* on Node 18.19+ and above. NO third-party crypto dependency is added
|
|
13
|
-
* to the plugin for the gateway side. (The BROWSER side of the flow
|
|
14
|
-
*
|
|
15
|
-
* for older Safari — those ship as part of the served pairing page and
|
|
16
|
-
* do NOT affect the plugin's server-side dep tree.)
|
|
20
|
+
* to the plugin for the gateway side. (The BROWSER side of the flow uses
|
|
21
|
+
* WebCrypto's AES-GCM directly — no shim needed.)
|
|
17
22
|
*
|
|
18
23
|
* Scope and guarantees
|
|
19
24
|
* --------------------
|
|
@@ -33,11 +38,11 @@
|
|
|
33
38
|
*
|
|
34
39
|
* Interoperability with browser WebCrypto
|
|
35
40
|
* ---------------------------------------
|
|
36
|
-
* The WebCrypto x25519 + HKDF +
|
|
37
|
-
*
|
|
41
|
+
* The WebCrypto x25519 + HKDF + AES-GCM APIs are bit-for-bit compatible
|
|
42
|
+
* with Node's `crypto` as long as:
|
|
38
43
|
* - Raw 32-byte public/private keys are used (not DER/SPKI).
|
|
39
44
|
* - HKDF parameters are (hash=SHA-256, salt=sid bytes, info fixed
|
|
40
|
-
* ASCII string, length=32 bytes
|
|
45
|
+
* ASCII string "totalreclaw-pair-v2", length=32 bytes).
|
|
41
46
|
* - AEAD uses a 12-byte random nonce + 16-byte tag, AD = sid bytes.
|
|
42
47
|
* See tests for fixed test vectors.
|
|
43
48
|
*/
|
|
@@ -60,18 +65,23 @@ import {
|
|
|
60
65
|
|
|
61
66
|
/**
|
|
62
67
|
* HKDF "info" parameter — fixes the domain separation for this protocol.
|
|
63
|
-
* MUST match the browser-side constant in the pair-page bundle
|
|
64
|
-
*
|
|
68
|
+
* MUST match the browser-side constant in the pair-page bundle + the
|
|
69
|
+
* relay-served pair-html page.
|
|
70
|
+
*
|
|
71
|
+
* Versioned so we can roll to a new KDF or cipher suite without silently
|
|
72
|
+
* producing garbage with old ciphertexts. rc.12: bumped from v1 to v2
|
|
73
|
+
* after cipher-suite swap from ChaCha20-Poly1305 → AES-256-GCM (see
|
|
74
|
+
* module header comment for context).
|
|
65
75
|
*/
|
|
66
|
-
export const HKDF_INFO = 'totalreclaw-pair-
|
|
76
|
+
export const HKDF_INFO = 'totalreclaw-pair-v2';
|
|
67
77
|
|
|
68
|
-
/** HKDF output length — 32 bytes = 256-bit
|
|
78
|
+
/** HKDF output length — 32 bytes = 256-bit AES-256-GCM key. */
|
|
69
79
|
export const AEAD_KEY_BYTES = 32;
|
|
70
80
|
|
|
71
|
-
/**
|
|
81
|
+
/** AES-GCM nonce length — 12 bytes (SP 800-38D recommendation). */
|
|
72
82
|
export const AEAD_NONCE_BYTES = 12;
|
|
73
83
|
|
|
74
|
-
/**
|
|
84
|
+
/** AES-GCM auth tag length — 16 bytes (128 bits, standard). */
|
|
75
85
|
export const AEAD_TAG_BYTES = 16;
|
|
76
86
|
|
|
77
87
|
/** Raw x25519 public/private key length — 32 bytes per RFC 7748. */
|
|
@@ -101,7 +111,7 @@ export interface GatewayKeypair {
|
|
|
101
111
|
|
|
102
112
|
/** Fully-derived session keys — caller uses kEnc for AEAD ops. */
|
|
103
113
|
export interface SessionKeys {
|
|
104
|
-
/** 32-byte
|
|
114
|
+
/** 32-byte AES-256-GCM key. */
|
|
105
115
|
kEnc: Buffer;
|
|
106
116
|
}
|
|
107
117
|
|
|
@@ -323,9 +333,9 @@ export function deriveAeadKeyFromEcdh(opts: {
|
|
|
323
333
|
// ---------------------------------------------------------------------------
|
|
324
334
|
|
|
325
335
|
/**
|
|
326
|
-
* Decrypt
|
|
327
|
-
*
|
|
328
|
-
*
|
|
336
|
+
* Decrypt an AES-256-GCM AEAD ciphertext. Returns the plaintext on
|
|
337
|
+
* success; throws if the tag is invalid (which includes both tampering
|
|
338
|
+
* and wrong-key attempts).
|
|
329
339
|
*
|
|
330
340
|
* Ciphertext is expected in the combined form `plaintext || tag`, where
|
|
331
341
|
* tag is the trailing 16 bytes. The caller MUST supply the same
|
|
@@ -351,7 +361,7 @@ export function aeadDecrypt(opts: {
|
|
|
351
361
|
const ct = combined.subarray(0, combined.length - AEAD_TAG_BYTES);
|
|
352
362
|
const tag = combined.subarray(combined.length - AEAD_TAG_BYTES);
|
|
353
363
|
|
|
354
|
-
const decipher = createDecipheriv('
|
|
364
|
+
const decipher = createDecipheriv('aes-256-gcm', opts.kEnc, nonce, {
|
|
355
365
|
authTagLength: AEAD_TAG_BYTES,
|
|
356
366
|
});
|
|
357
367
|
decipher.setAAD(Buffer.from(opts.sid, 'utf-8'), { plaintextLength: ct.length });
|
|
@@ -409,7 +419,7 @@ export function aeadEncryptWithSessionKey(opts: {
|
|
|
409
419
|
}
|
|
410
420
|
|
|
411
421
|
const pt = Buffer.isBuffer(opts.plaintext) ? opts.plaintext : Buffer.from(opts.plaintext);
|
|
412
|
-
const cipher = createCipheriv('
|
|
422
|
+
const cipher = createCipheriv('aes-256-gcm', opts.kEnc, nonceBuf, {
|
|
413
423
|
authTagLength: AEAD_TAG_BYTES,
|
|
414
424
|
});
|
|
415
425
|
cipher.setAAD(Buffer.from(opts.sid, 'utf-8'), { plaintextLength: pt.length });
|
package/pair-page.ts
CHANGED
|
@@ -5,6 +5,17 @@
|
|
|
5
5
|
* the URL fragment (`#pk=...`), runs the client-side pairing flow
|
|
6
6
|
* ENTIRELY in the browser, and POSTs the encrypted payload back.
|
|
7
7
|
*
|
|
8
|
+
* rc.13 status: this OpenClaw-plugin local-mode page has NOT been
|
|
9
|
+
* ported to the wizard UX used by the relay (production) and the
|
|
10
|
+
* Python local-mode pages. Local-mode on the OpenClaw plugin is rarely
|
|
11
|
+
* exercised — the plugin defaults to a relay flow via the Hermes
|
|
12
|
+
* Python sidecar and only falls back here for air-gapped setups. The
|
|
13
|
+
* wizard UX port for this file is tracked for rc.14 alongside the
|
|
14
|
+
* design decision on whether to share a single CSS+JS asset across
|
|
15
|
+
* all three pair pages (relay / Python / plugin) or keep them
|
|
16
|
+
* independently inlined. For now, this file retains its rc.10–rc.12
|
|
17
|
+
* UX shape and the rc.12 AES-GCM cipher swap.
|
|
18
|
+
*
|
|
8
19
|
* Brand tokens imported from the v5b.html public site (colors, font
|
|
9
20
|
* stack). Typography falls back to system fonts for mobile parity —
|
|
10
21
|
* we don't ship Euclid Circular A bytes over the pairing HTTP surface.
|
|
@@ -364,7 +375,9 @@ button.secondary:hover:not(:disabled) { border-color: var(--border-accent); colo
|
|
|
364
375
|
// Client-observed time at page load (used to adjust for clock skew).
|
|
365
376
|
const CLIENT_EPOCH_AT_LOAD = Date.now();
|
|
366
377
|
|
|
367
|
-
|
|
378
|
+
// v2: cipher-suite swap in rc.12 (see pair-crypto.ts header). Keep
|
|
379
|
+
// this constant in lockstep with the gateway-side pair-crypto.ts.
|
|
380
|
+
const HKDF_INFO = "totalreclaw-pair-v2";
|
|
368
381
|
|
|
369
382
|
// ---------- Small utilities ----------
|
|
370
383
|
function $(sel, root) { return (root || document).querySelector(sel); }
|
|
@@ -473,10 +486,9 @@ button.secondary:hover:not(:disabled) { border-color: var(--border-accent); colo
|
|
|
473
486
|
}
|
|
474
487
|
|
|
475
488
|
// ---------- Crypto shims: prefer WebCrypto; fall back to JS path ----------
|
|
476
|
-
// WebCrypto's x25519 + HKDF +
|
|
477
|
-
//
|
|
478
|
-
// is self-contained
|
|
479
|
-
// for the MVP (tracked as Wave 3.1 polish follow-up).
|
|
489
|
+
// WebCrypto's x25519 + HKDF + AES-GCM availability is Safari 17+ and
|
|
490
|
+
// modern Chromium 133+ / Firefox 130+. If absent we render an error
|
|
491
|
+
// — the page is self-contained and we do not bundle any JS crypto shim.
|
|
480
492
|
async function ensureWebCryptoSupport() {
|
|
481
493
|
if (!window.crypto || !crypto.subtle) return false;
|
|
482
494
|
try {
|
|
@@ -505,29 +517,28 @@ button.secondary:hover:not(:disabled) { border-color: var(--border-accent); colo
|
|
|
505
517
|
);
|
|
506
518
|
return new Uint8Array(bits);
|
|
507
519
|
}
|
|
508
|
-
// AEAD:
|
|
509
|
-
//
|
|
510
|
-
|
|
511
|
-
async function aeadEncryptChaCha(keyBytes, nonce, sid, plaintext) {
|
|
520
|
+
// AEAD: AES-256-GCM (universal in WebCrypto). Cipher swap rationale
|
|
521
|
+
// lives in pair-crypto.ts header comment (rc.12 changelog entry).
|
|
522
|
+
async function aeadEncryptAesGcm(keyBytes, nonce, sid, plaintext) {
|
|
512
523
|
const key = await crypto.subtle.importKey(
|
|
513
524
|
"raw", keyBytes,
|
|
514
|
-
{ name: "
|
|
525
|
+
{ name: "AES-GCM" },
|
|
515
526
|
false, ["encrypt"],
|
|
516
527
|
);
|
|
517
528
|
const adBytes = new TextEncoder().encode(sid);
|
|
518
529
|
const ct = new Uint8Array(await crypto.subtle.encrypt(
|
|
519
|
-
{ name: "
|
|
530
|
+
{ name: "AES-GCM", iv: nonce, additionalData: adBytes, tagLength: 128 },
|
|
520
531
|
key, plaintext,
|
|
521
532
|
));
|
|
522
533
|
return ct;
|
|
523
534
|
}
|
|
524
535
|
|
|
525
|
-
async function
|
|
536
|
+
async function aesGcmSupported() {
|
|
526
537
|
try {
|
|
527
538
|
const k = new Uint8Array(32);
|
|
528
539
|
const n = new Uint8Array(12);
|
|
529
|
-
const key = await crypto.subtle.importKey("raw", k, { name: "
|
|
530
|
-
await crypto.subtle.encrypt({ name: "
|
|
540
|
+
const key = await crypto.subtle.importKey("raw", k, { name: "AES-GCM" }, false, ["encrypt"]);
|
|
541
|
+
await crypto.subtle.encrypt({ name: "AES-GCM", iv: n, additionalData: new Uint8Array(0), tagLength: 128 }, key, new Uint8Array(0));
|
|
531
542
|
return true;
|
|
532
543
|
} catch (e) { return false; }
|
|
533
544
|
}
|
|
@@ -742,8 +753,8 @@ button.secondary:hover:not(:disabled) { border-color: var(--border-accent); colo
|
|
|
742
753
|
render(renderError("Your browser does not support modern cryptographic APIs (X25519). Please update your browser and try again, or use a different device. Supported: Chrome 123+, Firefox 130+, Safari 17+."));
|
|
743
754
|
return;
|
|
744
755
|
}
|
|
745
|
-
if (!(await
|
|
746
|
-
render(renderError("Your browser does not support
|
|
756
|
+
if (!(await aesGcmSupported())) {
|
|
757
|
+
render(renderError("Your browser does not support AES-GCM. Update your browser or use a different device."));
|
|
747
758
|
return;
|
|
748
759
|
}
|
|
749
760
|
|
|
@@ -762,7 +773,7 @@ button.secondary:hover:not(:disabled) { border-color: var(--border-accent); colo
|
|
|
762
773
|
const nonce = new Uint8Array(12);
|
|
763
774
|
crypto.getRandomValues(nonce);
|
|
764
775
|
const ptBytes = new TextEncoder().encode(mnemonic);
|
|
765
|
-
const ct = await
|
|
776
|
+
const ct = await aeadEncryptAesGcm(kEnc, nonce, SID, ptBytes);
|
|
766
777
|
|
|
767
778
|
// 6. Zero sensitive buffers BEFORE sending. The JS GC will run
|
|
768
779
|
// whenever it runs; explicit zeroing is best-effort but honours
|
package/pair-remote-client.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Python implementation byte-for-byte so either side can open a session that
|
|
8
8
|
* the relay (`totalreclaw-relay`) + browser page (`pair-html.ts`) already
|
|
9
9
|
* understand. Crypto primitives come from the shared ``pair-crypto.ts``
|
|
10
|
-
* module — the same ECDH + HKDF +
|
|
10
|
+
* module — the same ECDH + HKDF + AES-256-GCM stack the loopback HTTP
|
|
11
11
|
* server uses.
|
|
12
12
|
*
|
|
13
13
|
* Flow (this file implements the gateway half):
|