@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 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
- api.registerHttpRoute!({ path: bundle.finishPath, handler: bundle.handlers.finish });
2749
- api.registerHttpRoute!({ path: bundle.startPath, handler: bundle.handlers.start });
2750
- api.registerHttpRoute!({ path: bundle.respondPath, handler: bundle.handlers.respond });
2751
- api.registerHttpRoute!({ path: bundle.statusPath, handler: bundle.handlers.status });
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.2",
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": [
@@ -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
- const dir = path.dirname(sessionsPath);
353
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
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);