@totalreclaw/totalreclaw 3.3.0-rc.2 → 3.3.0-rc.4
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 +157 -0
- package/index.ts +16 -4
- package/package.json +1 -1
- package/pair-session-store.ts +32 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,163 @@ 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.0-rc.4] — 2026-04-20
|
|
8
|
+
|
|
9
|
+
Fourth release candidate for 3.3.0. Two independent ship-stopper fixes for
|
|
10
|
+
rc.3's QR-pairing flow, both surfaced by the auto-QA run against rc.3
|
|
11
|
+
artifacts (report: `docs/notes/QA-plugin-3.3.0-rc.3-20260420-1440.md` in
|
|
12
|
+
`totalreclaw-internal`, thread at `totalreclaw-internal#21`). No protocol /
|
|
13
|
+
on-chain changes vs 3.3.0. Bundled into a single RC because shipping them
|
|
14
|
+
separately would require two more QA loops for what are, individually,
|
|
15
|
+
one-line fixes.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **`skill/plugin/index.ts` — pair HTTP routes must use `auth: 'plugin'`, not
|
|
20
|
+
`'gateway'`** (lines 2750–2753, now 2760–2763 after added comment). rc.3
|
|
21
|
+
added `auth: 'gateway'` to the 4 `api.registerHttpRoute` calls, which the
|
|
22
|
+
SDK loader accepted as a legal value but whose runtime semantics are
|
|
23
|
+
"requires gateway bearer token" (see
|
|
24
|
+
`matchedPluginRoutesRequireGatewayAuth` at
|
|
25
|
+
`gateway-cli-CWpalJNJ.js:23186`). For the 4 pair routes — reached from a
|
|
26
|
+
phone/laptop browser with no bearer token — that means `/pair/*` is 401'd
|
|
27
|
+
at the plugin-auth stage before the handler ever runs. The second valid
|
|
28
|
+
literal, `auth: 'plugin'` (verified as the only other accepted value at
|
|
29
|
+
`loader-BkOjign1.js:662`), lets the plugin's handler run directly and
|
|
30
|
+
authenticate itself via the in-session sid + 6-digit secondaryCode +
|
|
31
|
+
single-use consumption + ECDH AEAD payload, which is the correct model
|
|
32
|
+
for QR-pair. QA observed `httpRouteCount: 0` in rc.3 via `plugins inspect`
|
|
33
|
+
and confirmed all 4 `/plugin/totalreclaw/pair/*` paths returned 404 / SPA
|
|
34
|
+
fallthrough. rc.4 switches all 4 to `auth: 'plugin'`.
|
|
35
|
+
|
|
36
|
+
**Before (rc.3):**
|
|
37
|
+
```ts
|
|
38
|
+
api.registerHttpRoute!({ path: bundle.finishPath, handler: bundle.handlers.finish, auth: 'gateway' });
|
|
39
|
+
api.registerHttpRoute!({ path: bundle.startPath, handler: bundle.handlers.start, auth: 'gateway' });
|
|
40
|
+
api.registerHttpRoute!({ path: bundle.respondPath, handler: bundle.handlers.respond, auth: 'gateway' });
|
|
41
|
+
api.registerHttpRoute!({ path: bundle.statusPath, handler: bundle.handlers.status, auth: 'gateway' });
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**After (rc.4):**
|
|
45
|
+
```ts
|
|
46
|
+
api.registerHttpRoute!({ path: bundle.finishPath, handler: bundle.handlers.finish, auth: 'plugin' });
|
|
47
|
+
api.registerHttpRoute!({ path: bundle.startPath, handler: bundle.handlers.start, auth: 'plugin' });
|
|
48
|
+
api.registerHttpRoute!({ path: bundle.respondPath, handler: bundle.handlers.respond, auth: 'plugin' });
|
|
49
|
+
api.registerHttpRoute!({ path: bundle.statusPath, handler: bundle.handlers.status, auth: 'plugin' });
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- **`skill/plugin/pair-session-store.ts::acquireSessionsFileLock` — mkdir
|
|
53
|
+
parent before `openSync(wx)`**. On a fresh install with no
|
|
54
|
+
`~/.totalreclaw/` directory, the lock's `openSync(path, 'wx')` returned
|
|
55
|
+
`ENOENT (No such file or directory)` and the retry loop misinterpreted
|
|
56
|
+
that as "lock already held", spinning at 50 ms intervals for the full
|
|
57
|
+
10 s `LOCK_WAIT_MS` before throwing `could not acquire lock`. The CLI
|
|
58
|
+
surfaced this as a hung `openclaw totalreclaw pair generate` with no QR,
|
|
59
|
+
URL, or secondary code ever rendered. `writePairSessionsFileSync`
|
|
60
|
+
already had a mkdir, but it was never reached because the lock never
|
|
61
|
+
acquired. rc.4 extracts a shared `ensureSessionsFileDir(sessionsPath)`
|
|
62
|
+
helper (mkdir `-p` with mode 0700) and calls it at the TOP of both
|
|
63
|
+
`acquireSessionsFileLock` AND `writePairSessionsFileSync` so the two
|
|
64
|
+
code paths can't drift. QA strace evidence in
|
|
65
|
+
`totalreclaw-internal#21`.
|
|
66
|
+
|
|
67
|
+
**Before (rc.3):**
|
|
68
|
+
```ts
|
|
69
|
+
async function acquireSessionsFileLock(sessionsPath) {
|
|
70
|
+
const lockPath = `${sessionsPath}.lock`;
|
|
71
|
+
// ...
|
|
72
|
+
while (true) {
|
|
73
|
+
try {
|
|
74
|
+
const fd = fs.openSync(lockPath, 'wx'); // ENOENT here on fresh install
|
|
75
|
+
// ...
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**After (rc.4):**
|
|
79
|
+
```ts
|
|
80
|
+
function ensureSessionsFileDir(sessionsPath) {
|
|
81
|
+
const dir = path.dirname(sessionsPath);
|
|
82
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function acquireSessionsFileLock(sessionsPath) {
|
|
86
|
+
ensureSessionsFileDir(sessionsPath); // NEW — guarantees parent dir
|
|
87
|
+
const lockPath = `${sessionsPath}.lock`;
|
|
88
|
+
// ...
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Added
|
|
92
|
+
|
|
93
|
+
- `skill/plugin/pair-session-store.test.ts` — two new blocks (§17, §18)
|
|
94
|
+
covering the fresh-install regression: `createPairSession` against a
|
|
95
|
+
path whose parent directory does NOT exist completes in < 2 s (was
|
|
96
|
+
10 s hang), materializes the missing dir with the correct mode, writes
|
|
97
|
+
the sessions file at 0600, and leaves no lock sentinel. Plus read-path
|
|
98
|
+
defensive tests: `getPairSession` / `listActivePairSessions` against
|
|
99
|
+
a missing dir return null / `[]` without throwing (previously would
|
|
100
|
+
have hit the same ENOENT hang).
|
|
101
|
+
- `skill/plugin/pair-http-route-registration.test.ts` — assertions
|
|
102
|
+
updated from `'gateway'` to `'plugin'`, plus a per-call regression
|
|
103
|
+
guard asserting `auth !== 'gateway'` so rc.3's value cannot sneak back
|
|
104
|
+
in. Test count: 23 → 27 assertions.
|
|
105
|
+
|
|
106
|
+
### Unchanged
|
|
107
|
+
|
|
108
|
+
No changes to: scanner-sim rules (still 0 flags), tarball contents (same
|
|
109
|
+
44 files; diff is content of 3 `.ts` files + `package.json` bump +
|
|
110
|
+
`CHANGELOG.md`), UX copy, terminology (`recovery phrase` throughout),
|
|
111
|
+
protobuf schema, Memory Taxonomy v1, on-chain contract surface, MCP
|
|
112
|
+
wiring, client integration, Hermes / NanoClaw / core (plugin-only RC).
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## [3.3.0-rc.3] — 2026-04-20
|
|
117
|
+
|
|
118
|
+
Third release candidate for 3.3.0. Sole change vs rc.2: adds the mandatory
|
|
119
|
+
`auth` field to the 4 `registerHttpRoute` calls that were silently dropped by
|
|
120
|
+
the OpenClaw 2026.4.2 loader. QR-pairing was end-to-end dead in rc.2 despite
|
|
121
|
+
the scanner and all other gates passing. See internal QA report at
|
|
122
|
+
`totalreclaw-internal#21`.
|
|
123
|
+
|
|
124
|
+
### Fixed
|
|
125
|
+
|
|
126
|
+
- `skill/plugin/index.ts` — added `auth: 'gateway'` to all 4
|
|
127
|
+
`api.registerHttpRoute!({...})` calls (lines 2750–2753). OpenClaw 2026.4.2
|
|
128
|
+
introduced a mandatory `auth` field; registrations without it are silently
|
|
129
|
+
dropped at load time. Affected routes: `/pair/finish`, `/pair/start`,
|
|
130
|
+
`/pair/respond`, `/pair/status`. The plugin's `logger.info('registered 4
|
|
131
|
+
QR-pairing HTTP routes')` still fired in rc.2, masking the failure — only
|
|
132
|
+
surfaced when `GET /plugin/totalreclaw/pair/finish` fell through to the SPA
|
|
133
|
+
and `POST /pair/respond` returned 404.
|
|
134
|
+
- `skill/plugin/index.ts` `PluginApi` interface — `registerHttpRoute` param
|
|
135
|
+
type updated to include `auth: 'gateway' | 'plugin'` so TypeScript enforces
|
|
136
|
+
the field going forward.
|
|
137
|
+
|
|
138
|
+
**Before:**
|
|
139
|
+
```ts
|
|
140
|
+
api.registerHttpRoute!({ path: bundle.finishPath, handler: bundle.handlers.finish });
|
|
141
|
+
api.registerHttpRoute!({ path: bundle.startPath, handler: bundle.handlers.start });
|
|
142
|
+
api.registerHttpRoute!({ path: bundle.respondPath, handler: bundle.handlers.respond });
|
|
143
|
+
api.registerHttpRoute!({ path: bundle.statusPath, handler: bundle.handlers.status });
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**After:**
|
|
147
|
+
```ts
|
|
148
|
+
api.registerHttpRoute!({ path: bundle.finishPath, handler: bundle.handlers.finish, auth: 'gateway' });
|
|
149
|
+
api.registerHttpRoute!({ path: bundle.startPath, handler: bundle.handlers.start, auth: 'gateway' });
|
|
150
|
+
api.registerHttpRoute!({ path: bundle.respondPath, handler: bundle.handlers.respond, auth: 'gateway' });
|
|
151
|
+
api.registerHttpRoute!({ path: bundle.statusPath, handler: bundle.handlers.status, auth: 'gateway' });
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Added
|
|
155
|
+
|
|
156
|
+
- `skill/plugin/pair-http-route-registration.test.ts` — new unit test (23
|
|
157
|
+
assertions) covering: 4 calls made, `auth` field present on every call,
|
|
158
|
+
`auth === 'gateway'`, paths contain `/pair/`, handlers are functions, all 4
|
|
159
|
+
endpoint segments covered (finish/start/respond/status), and no-throw when
|
|
160
|
+
`registerHttpRoute` is absent.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
7
164
|
## [3.3.0-rc.2] — 2026-04-20
|
|
8
165
|
|
|
9
166
|
Second release candidate for 3.3.0. Bundles the scanner false-positive
|
package/index.ts
CHANGED
|
@@ -214,6 +214,8 @@ interface OpenClawPluginApi {
|
|
|
214
214
|
registerHttpRoute?(params: {
|
|
215
215
|
path: string;
|
|
216
216
|
handler: (req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse) => Promise<void> | void;
|
|
217
|
+
/** OpenClaw 2026.4.2+ — required; loader silently drops the route if absent. */
|
|
218
|
+
auth: 'gateway' | 'plugin';
|
|
217
219
|
}): void;
|
|
218
220
|
}
|
|
219
221
|
|
|
@@ -2745,10 +2747,20 @@ const plugin = {
|
|
|
2745
2747
|
return { state: 'active' };
|
|
2746
2748
|
},
|
|
2747
2749
|
});
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2750
|
+
// auth: 'plugin' — the 4 pair routes are reached from the operator's
|
|
2751
|
+
// phone/laptop browser, which has no gateway bearer token. The plugin
|
|
2752
|
+
// authenticates each request itself via (a) the in-memory pair session
|
|
2753
|
+
// (sid + secondaryCode + single-use consumption), (b) ECDH + AEAD for
|
|
2754
|
+
// the encrypted mnemonic payload. See gateway-cli dist
|
|
2755
|
+
// `matchedPluginRoutesRequireGatewayAuth` / `enforcePluginRouteGatewayAuth`
|
|
2756
|
+
// — routes with `auth: 'gateway'` require a bearer token and 401 any
|
|
2757
|
+
// browser caller, which is the wrong semantic for QR-pair. rc.3
|
|
2758
|
+
// shipped `auth: 'gateway'` and the QA agent confirmed the routes
|
|
2759
|
+
// were unreachable from a browser (QA-plugin-3.3.0-rc.3 report).
|
|
2760
|
+
api.registerHttpRoute!({ path: bundle.finishPath, handler: bundle.handlers.finish, auth: 'plugin' });
|
|
2761
|
+
api.registerHttpRoute!({ path: bundle.startPath, handler: bundle.handlers.start, auth: 'plugin' });
|
|
2762
|
+
api.registerHttpRoute!({ path: bundle.respondPath, handler: bundle.handlers.respond, auth: 'plugin' });
|
|
2763
|
+
api.registerHttpRoute!({ path: bundle.statusPath, handler: bundle.handlers.status, auth: 'plugin' });
|
|
2752
2764
|
api.logger.info('TotalReclaw: registered 4 QR-pairing HTTP routes');
|
|
2753
2765
|
} catch (err) {
|
|
2754
2766
|
const msg = err instanceof Error ? err.message : String(err);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totalreclaw/totalreclaw",
|
|
3
|
-
"version": "3.3.0-rc.
|
|
3
|
+
"version": "3.3.0-rc.4",
|
|
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-session-store.ts
CHANGED
|
@@ -238,6 +238,25 @@ export const LOCK_WAIT_MS = 10_000;
|
|
|
238
238
|
/** Between-retry sleep. Short enough to feel responsive, long enough not to spin. */
|
|
239
239
|
export const LOCK_RETRY_MS = 50;
|
|
240
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Ensure the parent directory of `sessionsPath` exists, creating it
|
|
243
|
+
* (and any missing intermediates) with 0700 mode. This is called from
|
|
244
|
+
* BOTH the lock-acquisition and the write paths — if we only create
|
|
245
|
+
* the dir on write, a fresh install hits ENOENT on the lock's
|
|
246
|
+
* `openSync(path, 'wx')` and spins the retry loop until deadline (rc.3
|
|
247
|
+
* regression: QA-plugin-3.3.0-rc.3 report).
|
|
248
|
+
*
|
|
249
|
+
* Best-effort: a mkdir failure is re-thrown to the caller, which will
|
|
250
|
+
* surface it via the lock-acquisition error path (for lock) or the
|
|
251
|
+
* try/catch in `writePairSessionsFileSync` (for write). 0700 mode
|
|
252
|
+
* matches the privacy posture of the sessions file (0600) — if the
|
|
253
|
+
* user can read the directory they can already read the file.
|
|
254
|
+
*/
|
|
255
|
+
function ensureSessionsFileDir(sessionsPath: string): void {
|
|
256
|
+
const dir = path.dirname(sessionsPath);
|
|
257
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
258
|
+
}
|
|
259
|
+
|
|
241
260
|
/**
|
|
242
261
|
* Acquire an exclusive lock on the given sessions-file path by
|
|
243
262
|
* atomically creating `<path>.lock` with `wx` mode. Retries up to
|
|
@@ -251,6 +270,16 @@ export const LOCK_RETRY_MS = 50;
|
|
|
251
270
|
* and self-contained is safer than fighting the scanner.
|
|
252
271
|
*/
|
|
253
272
|
async function acquireSessionsFileLock(sessionsPath: string): Promise<() => void> {
|
|
273
|
+
// Guarantee the parent directory exists BEFORE the first openSync(wx).
|
|
274
|
+
// Without this, a fresh install where `~/.totalreclaw/` doesn't yet
|
|
275
|
+
// exist gets ENOENT on every attempt, tight-loops until deadline, and
|
|
276
|
+
// throws "could not acquire lock" — which the CLI surfaces as a hung
|
|
277
|
+
// pair command with no QR / code / URL ever rendered (rc.3 blocker;
|
|
278
|
+
// QA-plugin-3.3.0-rc.3 strace evidence in totalreclaw-internal#21).
|
|
279
|
+
// `writePairSessionsFileSync` already creates the dir, but that path
|
|
280
|
+
// is never reached because the lock never acquires.
|
|
281
|
+
ensureSessionsFileDir(sessionsPath);
|
|
282
|
+
|
|
254
283
|
const lockPath = `${sessionsPath}.lock`;
|
|
255
284
|
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
256
285
|
|
|
@@ -349,8 +378,9 @@ export function writePairSessionsFileSync(
|
|
|
349
378
|
file: PairSessionFile,
|
|
350
379
|
): boolean {
|
|
351
380
|
try {
|
|
352
|
-
|
|
353
|
-
|
|
381
|
+
// Same ensureSessionsFileDir used by acquireSessionsFileLock so the
|
|
382
|
+
// two paths can't drift.
|
|
383
|
+
ensureSessionsFileDir(sessionsPath);
|
|
354
384
|
const tmp = `${sessionsPath}.tmp-${process.pid}-${Date.now()}`;
|
|
355
385
|
fs.writeFileSync(tmp, JSON.stringify(file), { mode: 0o600 });
|
|
356
386
|
fs.renameSync(tmp, sessionsPath);
|