@totalreclaw/totalreclaw 3.3.12-rc.13 → 3.3.12-rc.15
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/SKILL.md +31 -27
- package/config.ts +7 -8
- package/dist/config.js +7 -8
- package/dist/index.js +25 -1
- package/dist/pair-http.js +147 -4
- package/dist/tr-cli.js +1 -1
- package/index.ts +24 -1
- package/package.json +2 -2
- package/pair-http.ts +194 -5
- package/skill.json +1 -1
- package/tr-cli.ts +1 -1
package/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: totalreclaw
|
|
3
|
-
description: "End-to-end encrypted, decentralized memory for OpenClaw.
|
|
4
|
-
version: 3.3.12-rc.
|
|
3
|
+
description: "End-to-end encrypted, decentralized memory for OpenClaw. A native kind:memory provider — recall is automatic via memory_search/memory_get, and facts are captured in the background. Trigger on 'install TotalReclaw', 'set up TotalReclaw', 'restore my recovery phrase', any recall request ('what do you remember about me', 'what's my X'), AND any explicit remember request ('remember X', 'save X')."
|
|
4
|
+
version: 3.3.12-rc.15
|
|
5
5
|
author: TotalReclaw Team
|
|
6
6
|
license: MIT
|
|
7
7
|
homepage: https://totalreclaw.xyz
|
|
@@ -22,19 +22,17 @@ metadata:
|
|
|
22
22
|
- persistent-context
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
-
# TotalReclaw — OpenClaw
|
|
25
|
+
# TotalReclaw — OpenClaw native memory provider
|
|
26
26
|
|
|
27
|
-
End-to-end encrypted memory stored across a decentralized network (not on a single company server).
|
|
27
|
+
End-to-end encrypted memory stored across a decentralized network (not on a single company server). TotalReclaw is OpenClaw's **native `kind:"memory"` provider**: recall runs through the conventional `memory_search` / `memory_get` tools (the same surface `active-memory` uses), and **facts are captured automatically in the background** — you do not call a tool to remember each fact. This skill tells you the install + account-setup flow and when to drive recall explicitly.
|
|
28
28
|
|
|
29
|
-
##
|
|
29
|
+
## How memory works here (read first)
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
- **Recall is native.** `memory_search` and `memory_get` are the memory tools. OpenClaw's `active-memory` sub-agent routes recall through them and the plugin injects guidance telling you when to search. When the user asks about themselves ("what's my X?", "do I X?", "what do you remember?"), call `memory_search`. If 0 results, say so plainly — don't guess from session history.
|
|
32
|
+
- **Capture is automatic.** The plugin extracts facts from the conversation in the background (trajectory poller). You do **not** call a tool on every preference/fact the user states — that happens for you. Never write user facts to `MEMORY.md`, `USER.md`, or any local file; those are agent-persona config, user memories live encrypted on-chain.
|
|
33
|
+
- **Explicit remember only on request.** If the user explicitly says "remember X" / "save X" / "note X" / "don't forget X", use the CLI: `tr remember --json "X"`. One call per distinct fact.
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
When the user asks about themselves ("what's my X?", "do I X?", "what do you remember?"), call `totalreclaw_recall` FIRST. If 0 results, say so plainly — don't guess from session history.
|
|
36
|
-
|
|
37
|
-
Do not describe storage as `"local"`, `"local-only"`, `"stored locally"`, `"on disk"`, `"local memory"`, `"local storage"`, or `"without an account"` — memories are decentralized on-chain (not on any single company server) and an account is required to derive the encryption key.
|
|
35
|
+
Do not describe storage as `"local"`, `"local-only"`, `"stored locally"`, `"on disk"`, or `"without an account"` — memories are decentralized on-chain (not on any single company server) and an account is required to derive the encryption key.
|
|
38
36
|
|
|
39
37
|
## Phrase safety (HARD)
|
|
40
38
|
|
|
@@ -43,34 +41,40 @@ The 12-word recovery phrase MUST NEVER enter chat or the agent's LLM context. Th
|
|
|
43
41
|
## Install
|
|
44
42
|
|
|
45
43
|
```
|
|
46
|
-
openclaw plugins install @totalreclaw/totalreclaw
|
|
44
|
+
openclaw plugins install @totalreclaw/totalreclaw
|
|
47
45
|
openclaw skills install totalreclaw
|
|
48
46
|
```
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
## Setup (plugin-driven)
|
|
48
|
+
Published on npm + ClawHub (`p-diogo`, GitHub-verified). For a release candidate, pin with `@rc`; stable resolves from `latest`. The plugin self-applies any gateway config it needs on load and auto-reloads (see Restart below) — no manual gateway restart.
|
|
53
49
|
|
|
54
|
-
|
|
50
|
+
## Setup (pair — user-initiated QR)
|
|
55
51
|
|
|
56
|
-
|
|
52
|
+
Pairing is a deliberate, user-initiated QR flow (the plugin does **not** auto-pair on load). When credentials don't exist:
|
|
57
53
|
|
|
58
|
-
|
|
54
|
+
1. Run `tr pair --json` → returns `{"url":"https://…/pair/p/<id>#pk=…","pin":"123456","expires_at_ms":…}`.
|
|
55
|
+
2. Surface the **url** and **pin** to the user verbatim (read from the JSON; never invent values): "Open `<url>` in your browser, enter PIN `<pin>`, and generate or paste your 12-word recovery phrase. Reply done once it's sealed."
|
|
56
|
+
3. The user completes in the browser (phrase is generated/imported browser-side, encrypted, posted to the relay — never touches this chat). On completion `~/.totalreclaw/credentials.json` is written.
|
|
57
|
+
4. Confirm: `✓ TotalReclaw set up.` (If credentials already exist when first asked: `✓ TotalReclaw is already set up.`)
|
|
59
58
|
|
|
60
|
-
|
|
59
|
+
Pair is UNCONDITIONAL when no credentials — the user installing/requesting setup IS the consent; do not add a second consent gate. `tr --help` lists every subcommand.
|
|
61
60
|
|
|
62
61
|
```bash
|
|
63
62
|
TR_CLI="$(ls -d ~/.openclaw/npm/node_modules/@totalreclaw/totalreclaw/dist/tr-cli.js ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js 2>/dev/null | head -1)"
|
|
64
|
-
node "$TR_CLI"
|
|
65
|
-
node "$TR_CLI"
|
|
66
|
-
node "$TR_CLI" recall --json "where does the user work" --limit 5 # {"results":[{"text":"...","score":0.8}]}
|
|
67
|
-
node "$TR_CLI" pair --json # {"url":"...","pin":"123456","expires_at_ms":...} — plugin's auto-pair is preferred; only use this for re-pair
|
|
63
|
+
node "$TR_CLI" pair --json # start/re-pair
|
|
64
|
+
node "$TR_CLI" status --json # {"version":"…","paired":true,…}
|
|
68
65
|
```
|
|
69
66
|
|
|
70
|
-
|
|
67
|
+
## Restart (agent-driven, never user-manual)
|
|
68
|
+
|
|
69
|
+
The plugin self-reloads: after install it writes the gateway config it needs and signals `SIGUSR1` to its own PID so the gateway picks it up without a manual restart. **If `memory_search`/`memory_get` are not bound** (tools didn't take effect on first load), issue the literal slash command `/totalreclaw-restart` **autonomously** — announce it in one line, never ask the user to authorize it, never stall, never `kill` / `docker restart` / prompt the user to restart. This mirrors the autonomous-restart pattern other runtimes use; the user should never perform a manual restart.
|
|
70
|
+
|
|
71
|
+
## Tools + CLI surface
|
|
72
|
+
|
|
73
|
+
**Recall (native memory contract — agent-facing):** `memory_search`, `memory_get`.
|
|
71
74
|
|
|
72
|
-
|
|
75
|
+
**Explicit capture + curation (CLI — `tr`, i.e. `node "$TR_CLI" …`):**
|
|
76
|
+
`tr remember` (explicit write) · `tr pin` / `tr unpin` · `tr retype` · `tr set_scope` · `tr status` · `tr export` · `tr pair`. Import + plan upgrade run via the gateway subcommand: `openclaw totalreclaw import from <source> --file <path> [--json]`, `openclaw totalreclaw upgrade [--json]`, `openclaw totalreclaw import status|abort`.
|
|
73
77
|
|
|
74
|
-
|
|
78
|
+
The legacy `totalreclaw_*` agent tools and the `tr recall` CLI are retired — recall is `memory_search`, explicit capture is `tr remember`. If a stale guide references them, follow this SKILL instead.
|
|
75
79
|
|
|
76
|
-
Full
|
|
80
|
+
Full guide: <https://github.com/p-diogo/totalreclaw/blob/main/docs/guides/openclaw-setup.md>
|
package/config.ts
CHANGED
|
@@ -102,10 +102,10 @@ export function getSessionId(): string | null {
|
|
|
102
102
|
|
|
103
103
|
/**
|
|
104
104
|
* Runtime override for chain ID, set after the relay billing response is
|
|
105
|
-
* read.
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* the signature with
|
|
105
|
+
* read. After the ops-1 single-chain migration (2026-05), ALL tiers (free
|
|
106
|
+
* + Pro) are on Gnosis mainnet (chain 100). The default below is 100.
|
|
107
|
+
* The relay routes all writes to Gnosis, so UserOps MUST be signed against
|
|
108
|
+
* chain 100 — otherwise the bundler rejects the signature with AA24.
|
|
109
109
|
*
|
|
110
110
|
* See index.ts: after the billing lookup completes, call
|
|
111
111
|
* `setChainIdOverride(100)` for Pro users. Free users can leave the
|
|
@@ -221,9 +221,8 @@ export const CONFIG = {
|
|
|
221
221
|
: 'wss://api.totalreclaw.xyz')
|
|
222
222
|
).replace(/\/+$/, ''),
|
|
223
223
|
|
|
224
|
-
// Chain — chainId is no longer user-configurable.
|
|
225
|
-
//
|
|
226
|
-
// 100). The default here is used only before the first billing lookup
|
|
224
|
+
// Chain — chainId is no longer user-configurable. After the ops-1 single-
|
|
225
|
+
// chain migration, ALL tiers are on Gnosis (100). The default here is 100.
|
|
227
226
|
// completes. Self-hosted users can still point at a custom DataEdge via
|
|
228
227
|
// TOTALRECLAW_DATA_EDGE_ADDRESS / TOTALRECLAW_ENTRYPOINT_ADDRESS /
|
|
229
228
|
// TOTALRECLAW_RPC_URL (undocumented; internal knobs).
|
|
@@ -233,7 +232,7 @@ export const CONFIG = {
|
|
|
233
232
|
// not a literal — a literal would freeze all Pro-tier UserOps to the
|
|
234
233
|
// wrong chainId and AA23 at the bundler.
|
|
235
234
|
get chainId(): number {
|
|
236
|
-
return _chainIdOverride ??
|
|
235
|
+
return _chainIdOverride ?? 100;
|
|
237
236
|
},
|
|
238
237
|
dataEdgeAddress: process.env.TOTALRECLAW_DATA_EDGE_ADDRESS || '',
|
|
239
238
|
entryPointAddress: process.env.TOTALRECLAW_ENTRYPOINT_ADDRESS || '',
|
package/dist/config.js
CHANGED
|
@@ -90,10 +90,10 @@ export function getSessionId() {
|
|
|
90
90
|
}
|
|
91
91
|
/**
|
|
92
92
|
* Runtime override for chain ID, set after the relay billing response is
|
|
93
|
-
* read.
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* the signature with
|
|
93
|
+
* read. After the ops-1 single-chain migration (2026-05), ALL tiers (free
|
|
94
|
+
* + Pro) are on Gnosis mainnet (chain 100). The default below is 100.
|
|
95
|
+
* The relay routes all writes to Gnosis, so UserOps MUST be signed against
|
|
96
|
+
* chain 100 — otherwise the bundler rejects the signature with AA24.
|
|
97
97
|
*
|
|
98
98
|
* See index.ts: after the billing lookup completes, call
|
|
99
99
|
* `setChainIdOverride(100)` for Pro users. Free users can leave the
|
|
@@ -202,9 +202,8 @@ export const CONFIG = {
|
|
|
202
202
|
|| (process.env.TOTALRECLAW_SERVER_URL
|
|
203
203
|
? process.env.TOTALRECLAW_SERVER_URL.replace(/^https?:\/\//, 'wss://').replace(/^http:/, 'ws:')
|
|
204
204
|
: 'wss://api.totalreclaw.xyz')).replace(/\/+$/, ''),
|
|
205
|
-
// Chain — chainId is no longer user-configurable.
|
|
206
|
-
//
|
|
207
|
-
// 100). The default here is used only before the first billing lookup
|
|
205
|
+
// Chain — chainId is no longer user-configurable. After the ops-1 single-
|
|
206
|
+
// chain migration, ALL tiers are on Gnosis (100). The default here is 100.
|
|
208
207
|
// completes. Self-hosted users can still point at a custom DataEdge via
|
|
209
208
|
// TOTALRECLAW_DATA_EDGE_ADDRESS / TOTALRECLAW_ENTRYPOINT_ADDRESS /
|
|
210
209
|
// TOTALRECLAW_RPC_URL (undocumented; internal knobs).
|
|
@@ -214,7 +213,7 @@ export const CONFIG = {
|
|
|
214
213
|
// not a literal — a literal would freeze all Pro-tier UserOps to the
|
|
215
214
|
// wrong chainId and AA23 at the bundler.
|
|
216
215
|
get chainId() {
|
|
217
|
-
return _chainIdOverride ??
|
|
216
|
+
return _chainIdOverride ?? 100;
|
|
218
217
|
},
|
|
219
218
|
dataEdgeAddress: process.env.TOTALRECLAW_DATA_EDGE_ADDRESS || '',
|
|
220
219
|
entryPointAddress: process.env.TOTALRECLAW_ENTRYPOINT_ADDRESS || '',
|
package/dist/index.js
CHANGED
|
@@ -3524,6 +3524,17 @@ const plugin = {
|
|
|
3524
3524
|
sessionsPath: CONFIG.pairSessionsPath,
|
|
3525
3525
|
apiBase: '/plugin/totalreclaw/pair',
|
|
3526
3526
|
logger: api.logger,
|
|
3527
|
+
// 3.3.14 — wire the relay URL so buildPairRoutes exposes the
|
|
3528
|
+
// in-process `/pair/init` route. The gateway process opens the
|
|
3529
|
+
// relay WebSocket directly (via openRemotePairSession from
|
|
3530
|
+
// pair-remote-client.ts), eliminating the 30s-subprocess-kill
|
|
3531
|
+
// 502 that the CLI path (tr pair) hit when OpenClaw's shell
|
|
3532
|
+
// tool killed the subprocess mid-pair. relayBaseUrl is sourced
|
|
3533
|
+
// from CONFIG.pairRelayUrl (config.ts reads it from the env
|
|
3534
|
+
// once, centrally) — never read from the environment inside
|
|
3535
|
+
// pair-http.ts (scanner-surface rule).
|
|
3536
|
+
relayBaseUrl: CONFIG.pairRelayUrl,
|
|
3537
|
+
initPairMode: 'either',
|
|
3527
3538
|
validateMnemonic: (p) => validateMnemonic(p, wordlist),
|
|
3528
3539
|
completePairing: async ({ mnemonic }) => {
|
|
3529
3540
|
// Write credentials.json + flip state to 'active' via
|
|
@@ -3584,7 +3595,20 @@ const plugin = {
|
|
|
3584
3595
|
api.registerHttpRoute({ path: bundle.startPath, handler: bundle.handlers.start, auth: 'plugin' });
|
|
3585
3596
|
api.registerHttpRoute({ path: bundle.respondPath, handler: bundle.handlers.respond, auth: 'plugin' });
|
|
3586
3597
|
api.registerHttpRoute({ path: bundle.statusPath, handler: bundle.handlers.status, auth: 'plugin' });
|
|
3587
|
-
|
|
3598
|
+
// 3.3.14 — in-process pair trigger. The bundle exposes initPath +
|
|
3599
|
+
// handlers.init ONLY when relayBaseUrl is wired (always true here,
|
|
3600
|
+
// since CONFIG.pairRelayUrl has a built-in default). Registered
|
|
3601
|
+
// with auth: 'plugin' (same as the other pair routes) so the
|
|
3602
|
+
// agent's localhost curl reaches it without a gateway bearer
|
|
3603
|
+
// token. The route opens the relay WS in the gateway process →
|
|
3604
|
+
// survives shell-tool timeouts, retries, SIGUSR1 reloads.
|
|
3605
|
+
if (bundle.initPath && bundle.handlers.init) {
|
|
3606
|
+
api.registerHttpRoute({ path: bundle.initPath, handler: bundle.handlers.init, auth: 'plugin' });
|
|
3607
|
+
api.logger.info('TotalReclaw: registered 5 QR-pairing HTTP routes synchronously (incl. in-process /pair/init)');
|
|
3608
|
+
}
|
|
3609
|
+
else {
|
|
3610
|
+
api.logger.info('TotalReclaw: registered 4 QR-pairing HTTP routes synchronously (in-process /pair/init not wired — no relay URL)');
|
|
3611
|
+
}
|
|
3588
3612
|
}
|
|
3589
3613
|
else {
|
|
3590
3614
|
api.logger.warn('api.registerHttpRoute is unavailable on this OpenClaw version — /totalreclaw pair will not work. ' +
|
package/dist/pair-http.js
CHANGED
|
@@ -2,7 +2,23 @@
|
|
|
2
2
|
* pair-http — gateway-side HTTP route handlers for the v3.3.0 QR-pairing
|
|
3
3
|
* flow. Registered via `api.registerHttpRoute` from `index.ts`.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Five endpoints (all under /plugin/totalreclaw/pair/):
|
|
6
|
+
*
|
|
7
|
+
* GET /plugin/totalreclaw/pair/init
|
|
8
|
+
* → IN-PROCESS pair trigger (3.3.14). Opens the relay WebSocket
|
|
9
|
+
* directly in the gateway process via `openRemotePairSession`
|
|
10
|
+
* (from pair-remote-client.ts), returns `{url, pin, sid,
|
|
11
|
+
* expires_at_ms}` immediately so the agent can surface URL+PIN
|
|
12
|
+
* to the user, and starts a BACKGROUND `awaitPhraseUpload` that
|
|
13
|
+
* blocks on the WS for the browser's encrypted phrase, decrypts
|
|
14
|
+
* locally, and invokes the injected `completePairing` callback
|
|
15
|
+
* (writes credentials.json + flips onboarding state). The WS
|
|
16
|
+
* lives in the gateway process — immune to the shell-tool's
|
|
17
|
+
* 30s subprocess timeout that killed the `tr pair` CLI path
|
|
18
|
+
* (relay returned 502 on /pair/respond when the subprocess
|
|
19
|
+
* died). This is the primary agent-facilitated pair path; the
|
|
20
|
+
* CLI `tr pair --json` remains as a fallback for non-agent
|
|
21
|
+
* scenarios.
|
|
6
22
|
*
|
|
7
23
|
* GET /plugin/totalreclaw/pair/finish?sid=<sid>
|
|
8
24
|
* → returns the browser pairing page (HTML + inline JS + CSS).
|
|
@@ -37,6 +53,12 @@
|
|
|
37
53
|
* - NO environment-variable reads. All config values flow in via
|
|
38
54
|
* `PairHttpConfig`; callers read from `CONFIG` in `config.ts`.
|
|
39
55
|
*
|
|
56
|
+
* Adding `openRemotePairSession` (which dials an outbound WebSocket to
|
|
57
|
+
* the relay) keeps this file env=N, net=Y → the env-harvesting rule
|
|
58
|
+
* requires BOTH an env read AND a request trigger in the same file;
|
|
59
|
+
* the relay base URL arrives via `PairHttpConfig.relayBaseUrl` (caller-
|
|
60
|
+
* injected), never read from the environment here.
|
|
61
|
+
*
|
|
40
62
|
* Logging: NEVER logs the secondary code, the mnemonic, the gateway
|
|
41
63
|
* private key, or raw request bodies. Session ids and status
|
|
42
64
|
* transitions are logged at info/warn levels for diagnostics.
|
|
@@ -44,9 +66,11 @@
|
|
|
44
66
|
import { consumePairSession, getPairSession, registerFailedSecondaryCode, rejectPairSession, transitionPairSession, MAX_SECONDARY_CODE_ATTEMPTS, } from './pair-session-store.js';
|
|
45
67
|
import { compareSecondaryCodesCT, decryptPairingPayload } from './pair-crypto.js';
|
|
46
68
|
import { renderPairPage } from './pair-page.js';
|
|
69
|
+
import { awaitPhraseUpload, openRemotePairSession, } from './pair-remote-client.js';
|
|
47
70
|
/**
|
|
48
|
-
* Build the
|
|
49
|
-
* `api.registerHttpRoute({ path, handler })`.
|
|
71
|
+
* Build the route handlers. The caller registers each with
|
|
72
|
+
* `api.registerHttpRoute({ path, handler })`. When `cfg.relayBaseUrl` is
|
|
73
|
+
* set, the bundle also carries the in-process `/pair/init` handler.
|
|
50
74
|
*/
|
|
51
75
|
export function buildPairRoutes(cfg) {
|
|
52
76
|
const apiBase = cfg.apiBase.replace(/\/+$/, '');
|
|
@@ -283,7 +307,117 @@ export function buildPairRoutes(cfg) {
|
|
|
283
307
|
mode: session.mode,
|
|
284
308
|
});
|
|
285
309
|
}
|
|
286
|
-
|
|
310
|
+
// ---------------------------------------------------------------
|
|
311
|
+
// 3.3.14 — In-process pair trigger (the 30s-subprocess-kill 502 fix)
|
|
312
|
+
// ---------------------------------------------------------------
|
|
313
|
+
//
|
|
314
|
+
// Defined unconditionally so the handler identity is stable for the
|
|
315
|
+
// lifetime of the bundle; it short-circuits with 503 when no relay
|
|
316
|
+
// URL was wired. Only attached to the returned bundle + registered
|
|
317
|
+
// as a route when `cfg.relayBaseUrl` is set (see the return below).
|
|
318
|
+
async function handleInit(req, res) {
|
|
319
|
+
if (!methodAllowed(req, ['GET'])) {
|
|
320
|
+
sendJson(res, 405, { error: 'method_not_allowed' });
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const relay = cfg.relayBaseUrl;
|
|
324
|
+
if (!relay) {
|
|
325
|
+
// Caller did not wire a relay URL — the in-process route is inert.
|
|
326
|
+
sendJson(res, 503, { error: 'init_not_configured' });
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
// 1. Open the relay WebSocket IN-PROCESS (the gateway process owns the
|
|
330
|
+
// socket). This is the fix: the WS is no longer held by a CLI
|
|
331
|
+
// subprocess, so OpenClaw's 30s shell-tool timeout cannot kill it
|
|
332
|
+
// and the relay never sees a mid-pair disconnect → no more 502 on
|
|
333
|
+
// /pair/respond.
|
|
334
|
+
let session;
|
|
335
|
+
try {
|
|
336
|
+
const openOpts = {
|
|
337
|
+
relayBaseUrl: relay,
|
|
338
|
+
mode: cfg.initPairMode ?? 'either',
|
|
339
|
+
};
|
|
340
|
+
if (cfg.initWebSocketImpl) {
|
|
341
|
+
openOpts.webSocketImpl = cfg.initWebSocketImpl;
|
|
342
|
+
}
|
|
343
|
+
session = await openRemotePairSession(openOpts);
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
347
|
+
cfg.logger.warn(`pair-http /init: relay session open failed: ${msg}`);
|
|
348
|
+
sendJson(res, 502, { error: 'relay_open_failed', detail: msg });
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const parsedExpiresMs = Date.parse(session.expiresAt);
|
|
352
|
+
const expiresAtMs = Number.isFinite(parsedExpiresMs)
|
|
353
|
+
? parsedExpiresMs
|
|
354
|
+
: now() + 5 * 60_000;
|
|
355
|
+
// 2. Respond IMMEDIATELY with the URL + PIN + sid (relay token) +
|
|
356
|
+
// expiry. The agent reads this and surfaces the URL+PIN to the
|
|
357
|
+
// user. The relay token here plays the `sid` role for agent-side
|
|
358
|
+
// correlation (parity with the CLI JSON payload shape).
|
|
359
|
+
sendJson(res, 200, {
|
|
360
|
+
v: 1,
|
|
361
|
+
sid: session.token,
|
|
362
|
+
url: session.url,
|
|
363
|
+
pin: session.pin,
|
|
364
|
+
mode: session.mode,
|
|
365
|
+
expires_at_ms: expiresAtMs,
|
|
366
|
+
});
|
|
367
|
+
// 3. Start the background wait IN THE GATEWAY PROCESS. We do NOT
|
|
368
|
+
// await this from the request handler — the HTTP response has
|
|
369
|
+
// already been sent. The promise resolves when the browser
|
|
370
|
+
// uploads the encrypted phrase (the relay pushes a `forward`
|
|
371
|
+
// frame), the gateway decrypts locally, and `completePairing`
|
|
372
|
+
// writes credentials.json + flips onboarding state. Errors are
|
|
373
|
+
// logged but never reach the HTTP response (it's already gone).
|
|
374
|
+
//
|
|
375
|
+
// The injected `completePairing` callback receives the mnemonic
|
|
376
|
+
// + a session-shaped object. We adapt the relay session to the
|
|
377
|
+
// `PairSession`-like shape the existing handler signature
|
|
378
|
+
// expects; only the fields completePairing actually reads are
|
|
379
|
+
// populated, the rest default. The mnemonic is the load-bearing
|
|
380
|
+
// field (it writes credentials.json); the session shape carries
|
|
381
|
+
// sid/mode for log correlation.
|
|
382
|
+
void (async () => {
|
|
383
|
+
try {
|
|
384
|
+
const result = await awaitPhraseUpload(session, {
|
|
385
|
+
phraseValidator: validate,
|
|
386
|
+
timeoutMs: cfg.initAwaitTimeoutMs,
|
|
387
|
+
completePairing: async ({ mnemonic }) => {
|
|
388
|
+
// Adapt the relay session to the PairSession-like shape the
|
|
389
|
+
// existing CompletePairingHandler signature expects. Only
|
|
390
|
+
// sid + mode are load-bearing for log correlation; the
|
|
391
|
+
// crypto fields are unused (decryption already happened).
|
|
392
|
+
const sessionLike = {
|
|
393
|
+
sid: session.token,
|
|
394
|
+
mode: session.mode === 'generate' ? 'generate' : 'import',
|
|
395
|
+
};
|
|
396
|
+
return cfg.completePairing({ mnemonic, session: sessionLike });
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
if (result.state === 'active') {
|
|
400
|
+
cfg.logger.info(`pair-http /init: session ${redactSid(session.token)} completed in-process; onboarding active`);
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
cfg.logger.warn(`pair-http /init: session ${redactSid(session.token)} completion non-active: ${result.error ?? 'unknown'}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
408
|
+
// Timeouts are expected when the user closes the browser without
|
|
409
|
+
// completing; log at info so a benign expire doesn't look like an
|
|
410
|
+
// error in the gateway log.
|
|
411
|
+
if (msg.includes('timeout') || msg.includes('closed')) {
|
|
412
|
+
cfg.logger.info(`pair-http /init: session ${redactSid(session.token)} expired/closed before completion`);
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
cfg.logger.warn(`pair-http /init: session ${redactSid(session.token)} failed: ${msg}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
})();
|
|
419
|
+
}
|
|
420
|
+
const bundle = {
|
|
287
421
|
finishPath: `${apiBase}/finish`,
|
|
288
422
|
startPath: `${apiBase}/start`,
|
|
289
423
|
respondPath: `${apiBase}/respond`,
|
|
@@ -295,6 +429,15 @@ export function buildPairRoutes(cfg) {
|
|
|
295
429
|
status: handleStatus,
|
|
296
430
|
},
|
|
297
431
|
};
|
|
432
|
+
// Only surface the in-process /init route when a relay URL is wired.
|
|
433
|
+
// Older callers that construct the bundle without `relayBaseUrl` get
|
|
434
|
+
// the original four-route shape (back-compat for tests + any external
|
|
435
|
+
// consumers of buildPairRoutes).
|
|
436
|
+
if (cfg.relayBaseUrl) {
|
|
437
|
+
bundle.initPath = `${apiBase}/init`;
|
|
438
|
+
bundle.handlers.init = handleInit;
|
|
439
|
+
}
|
|
440
|
+
return bundle;
|
|
298
441
|
}
|
|
299
442
|
// ---------------------------------------------------------------------------
|
|
300
443
|
// Internals: body reading, response helpers, validation
|
package/dist/tr-cli.js
CHANGED
|
@@ -51,7 +51,7 @@ const STATE_PATH = CONFIG.onboardingStatePath;
|
|
|
51
51
|
// Auto-synced by skill/scripts/sync-version.mjs from skill/plugin/package.json::version.
|
|
52
52
|
// Do not edit by hand — running tests will catch drift but the publish workflow
|
|
53
53
|
// rewrites this constant at the start of every npm/ClawHub publish.
|
|
54
|
-
const PLUGIN_VERSION = '3.3.12-rc.
|
|
54
|
+
const PLUGIN_VERSION = '3.3.12-rc.15';
|
|
55
55
|
function die(msg, code = 1) {
|
|
56
56
|
process.stderr.write(`tr: ${msg}\n`);
|
|
57
57
|
process.exit(code);
|
package/index.ts
CHANGED
|
@@ -4192,6 +4192,17 @@ const plugin = {
|
|
|
4192
4192
|
sessionsPath: CONFIG.pairSessionsPath,
|
|
4193
4193
|
apiBase: '/plugin/totalreclaw/pair',
|
|
4194
4194
|
logger: api.logger,
|
|
4195
|
+
// 3.3.14 — wire the relay URL so buildPairRoutes exposes the
|
|
4196
|
+
// in-process `/pair/init` route. The gateway process opens the
|
|
4197
|
+
// relay WebSocket directly (via openRemotePairSession from
|
|
4198
|
+
// pair-remote-client.ts), eliminating the 30s-subprocess-kill
|
|
4199
|
+
// 502 that the CLI path (tr pair) hit when OpenClaw's shell
|
|
4200
|
+
// tool killed the subprocess mid-pair. relayBaseUrl is sourced
|
|
4201
|
+
// from CONFIG.pairRelayUrl (config.ts reads it from the env
|
|
4202
|
+
// once, centrally) — never read from the environment inside
|
|
4203
|
+
// pair-http.ts (scanner-surface rule).
|
|
4204
|
+
relayBaseUrl: CONFIG.pairRelayUrl,
|
|
4205
|
+
initPairMode: 'either',
|
|
4195
4206
|
validateMnemonic: (p) => validateMnemonic(p, wordlist),
|
|
4196
4207
|
completePairing: async ({ mnemonic }) => {
|
|
4197
4208
|
// Write credentials.json + flip state to 'active' via
|
|
@@ -4252,7 +4263,19 @@ const plugin = {
|
|
|
4252
4263
|
api.registerHttpRoute!({ path: bundle.startPath, handler: bundle.handlers.start, auth: 'plugin' });
|
|
4253
4264
|
api.registerHttpRoute!({ path: bundle.respondPath, handler: bundle.handlers.respond, auth: 'plugin' });
|
|
4254
4265
|
api.registerHttpRoute!({ path: bundle.statusPath, handler: bundle.handlers.status, auth: 'plugin' });
|
|
4255
|
-
|
|
4266
|
+
// 3.3.14 — in-process pair trigger. The bundle exposes initPath +
|
|
4267
|
+
// handlers.init ONLY when relayBaseUrl is wired (always true here,
|
|
4268
|
+
// since CONFIG.pairRelayUrl has a built-in default). Registered
|
|
4269
|
+
// with auth: 'plugin' (same as the other pair routes) so the
|
|
4270
|
+
// agent's localhost curl reaches it without a gateway bearer
|
|
4271
|
+
// token. The route opens the relay WS in the gateway process →
|
|
4272
|
+
// survives shell-tool timeouts, retries, SIGUSR1 reloads.
|
|
4273
|
+
if (bundle.initPath && bundle.handlers.init) {
|
|
4274
|
+
api.registerHttpRoute!({ path: bundle.initPath, handler: bundle.handlers.init, auth: 'plugin' });
|
|
4275
|
+
api.logger.info('TotalReclaw: registered 5 QR-pairing HTTP routes synchronously (incl. in-process /pair/init)');
|
|
4276
|
+
} else {
|
|
4277
|
+
api.logger.info('TotalReclaw: registered 4 QR-pairing HTTP routes synchronously (in-process /pair/init not wired — no relay URL)');
|
|
4278
|
+
}
|
|
4256
4279
|
} else {
|
|
4257
4280
|
api.logger.warn(
|
|
4258
4281
|
'api.registerHttpRoute is unavailable on this OpenClaw version — /totalreclaw pair will not work. ' +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totalreclaw/totalreclaw",
|
|
3
|
-
"version": "3.3.12-rc.
|
|
3
|
+
"version": "3.3.12-rc.15",
|
|
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": [
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"scripts": {
|
|
69
69
|
"build": "rm -rf dist && tsc -p tsconfig.json --noCheck",
|
|
70
70
|
"verify-tarball": "node ../scripts/verify-tarball.mjs",
|
|
71
|
-
"test": "npx tsx batch-gate.test.ts && npx tsx manifest-shape.test.ts && npx tsx config-schema.test.ts && npx tsx config.test.ts && npx tsx relay-headers.test.ts && npx tsx scope-address-visible.test.ts && npx tsx llm-profile-reader.test.ts && npx tsx llm-client.test.ts && npx tsx llm-client-retry.test.ts && npx tsx llm-client-json-mode.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 && npx tsx test_issue_92_onnx_download_ux.test.ts && npx tsx onboard-pair-only.test.ts && npx tsx import-state.test.ts && npx tsx import-time-smoke.test.ts && npx tsx install-staging-cleanup.test.ts && npx tsx partial-install-detection.test.ts && npx tsx install-reload-idempotency.test.ts && npx tsx json-stdout-cleanliness.test.ts && npx tsx url-binding.test.ts && npx tsx fs-helpers.test.ts && npx tsx pair-cli-default-mode.test.ts && npx tsx embedding-fallback-tag.test.ts && npx tsx staging-banner-gate.test.ts && npx tsx restart-auth.test.ts && npx tsx inbound-user-tracker.test.ts && npx tsx register-command-name.test.ts && npx tsx skill-md-
|
|
71
|
+
"test": "npx tsx batch-gate.test.ts && npx tsx manifest-shape.test.ts && npx tsx config-schema.test.ts && npx tsx config.test.ts && npx tsx relay-headers.test.ts && npx tsx scope-address-visible.test.ts && npx tsx llm-profile-reader.test.ts && npx tsx llm-client.test.ts && npx tsx llm-client-retry.test.ts && npx tsx llm-client-json-mode.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 pair-http.test.ts && npx tsx pair-http-route-registration.test.ts && npx tsx pair-http-init.test.ts && npx tsx qa-bug-report.test.ts && npx tsx nonce-serialization.test.ts && npx tsx phrase-safety-registry.test.ts && npx tsx test_issue_92_onnx_download_ux.test.ts && npx tsx onboard-pair-only.test.ts && npx tsx import-state.test.ts && npx tsx import-time-smoke.test.ts && npx tsx install-staging-cleanup.test.ts && npx tsx partial-install-detection.test.ts && npx tsx install-reload-idempotency.test.ts && npx tsx json-stdout-cleanliness.test.ts && npx tsx url-binding.test.ts && npx tsx fs-helpers.test.ts && npx tsx pair-cli-default-mode.test.ts && npx tsx embedding-fallback-tag.test.ts && npx tsx staging-banner-gate.test.ts && npx tsx restart-auth.test.ts && npx tsx inbound-user-tracker.test.ts && npx tsx register-command-name.test.ts && npx tsx skill-md-native.test.ts &&npx tsx tr-cli-json-output.test.ts && npx tsx import-upgrade-cli.test.ts && npx tsx postinstall-validate.test.ts && npx tsx credential-provider.test.ts && npx tsx memory-runtime.test.ts && npx tsx tools.test.ts && npx tsx register-native.test.ts && npx tsx relay.test.ts && npx tsx vault-crypto.test.ts && npx tsx entry-env.test.ts && npx tsx trajectory-poller.test.ts",
|
|
72
72
|
"smoke:dist": "npx tsx dist-esm-smoke.test.ts",
|
|
73
73
|
"check-scanner": "node ../scripts/check-scanner.mjs",
|
|
74
74
|
"check-version-drift": "node ../scripts/check-version-drift.mjs",
|
package/pair-http.ts
CHANGED
|
@@ -2,7 +2,23 @@
|
|
|
2
2
|
* pair-http — gateway-side HTTP route handlers for the v3.3.0 QR-pairing
|
|
3
3
|
* flow. Registered via `api.registerHttpRoute` from `index.ts`.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Five endpoints (all under /plugin/totalreclaw/pair/):
|
|
6
|
+
*
|
|
7
|
+
* GET /plugin/totalreclaw/pair/init
|
|
8
|
+
* → IN-PROCESS pair trigger (3.3.14). Opens the relay WebSocket
|
|
9
|
+
* directly in the gateway process via `openRemotePairSession`
|
|
10
|
+
* (from pair-remote-client.ts), returns `{url, pin, sid,
|
|
11
|
+
* expires_at_ms}` immediately so the agent can surface URL+PIN
|
|
12
|
+
* to the user, and starts a BACKGROUND `awaitPhraseUpload` that
|
|
13
|
+
* blocks on the WS for the browser's encrypted phrase, decrypts
|
|
14
|
+
* locally, and invokes the injected `completePairing` callback
|
|
15
|
+
* (writes credentials.json + flips onboarding state). The WS
|
|
16
|
+
* lives in the gateway process — immune to the shell-tool's
|
|
17
|
+
* 30s subprocess timeout that killed the `tr pair` CLI path
|
|
18
|
+
* (relay returned 502 on /pair/respond when the subprocess
|
|
19
|
+
* died). This is the primary agent-facilitated pair path; the
|
|
20
|
+
* CLI `tr pair --json` remains as a fallback for non-agent
|
|
21
|
+
* scenarios.
|
|
6
22
|
*
|
|
7
23
|
* GET /plugin/totalreclaw/pair/finish?sid=<sid>
|
|
8
24
|
* → returns the browser pairing page (HTML + inline JS + CSS).
|
|
@@ -37,6 +53,12 @@
|
|
|
37
53
|
* - NO environment-variable reads. All config values flow in via
|
|
38
54
|
* `PairHttpConfig`; callers read from `CONFIG` in `config.ts`.
|
|
39
55
|
*
|
|
56
|
+
* Adding `openRemotePairSession` (which dials an outbound WebSocket to
|
|
57
|
+
* the relay) keeps this file env=N, net=Y → the env-harvesting rule
|
|
58
|
+
* requires BOTH an env read AND a request trigger in the same file;
|
|
59
|
+
* the relay base URL arrives via `PairHttpConfig.relayBaseUrl` (caller-
|
|
60
|
+
* injected), never read from the environment here.
|
|
61
|
+
*
|
|
40
62
|
* Logging: NEVER logs the secondary code, the mnemonic, the gateway
|
|
41
63
|
* private key, or raw request bodies. Session ids and status
|
|
42
64
|
* transitions are logged at info/warn levels for diagnostics.
|
|
@@ -56,6 +78,11 @@ import {
|
|
|
56
78
|
} from './pair-session-store.js';
|
|
57
79
|
import { compareSecondaryCodesCT, decryptPairingPayload } from './pair-crypto.js';
|
|
58
80
|
import { renderPairPage } from './pair-page.js';
|
|
81
|
+
import {
|
|
82
|
+
awaitPhraseUpload,
|
|
83
|
+
openRemotePairSession,
|
|
84
|
+
type RemotePairSession,
|
|
85
|
+
} from './pair-remote-client.js';
|
|
59
86
|
|
|
60
87
|
// ---------------------------------------------------------------------------
|
|
61
88
|
// Types
|
|
@@ -99,7 +126,7 @@ export type CompletePairingHandler = (inputs: {
|
|
|
99
126
|
export interface PairHttpConfig {
|
|
100
127
|
/** Absolute path to pair-sessions.json. */
|
|
101
128
|
sessionsPath: string;
|
|
102
|
-
/** Pathname prefix the
|
|
129
|
+
/** Pathname prefix the routes live under. */
|
|
103
130
|
apiBase: string;
|
|
104
131
|
/** Writes credentials + flips state. Injected from index.ts. */
|
|
105
132
|
completePairing: CompletePairingHandler;
|
|
@@ -111,6 +138,33 @@ export interface PairHttpConfig {
|
|
|
111
138
|
maxBodyBytes?: number;
|
|
112
139
|
/** If set, override BIP-39 validator for tests. Default does a word-count + wordlist check. */
|
|
113
140
|
validateMnemonic?: (phrase: string) => boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Relay base URL for the in-process `/pair/init` route (3.3.14). When
|
|
143
|
+
* set, `buildPairRoutes` exposes an `init` handler that opens the relay
|
|
144
|
+
* WebSocket directly in the gateway process (via `openRemotePairSession`).
|
|
145
|
+
* When omitted, the `init` handler is omitted from the bundle — older
|
|
146
|
+
* callers that haven't been updated still get the original 4 routes.
|
|
147
|
+
* Caller sources this from `CONFIG.pairRelayUrl`; NEVER read from the
|
|
148
|
+
* environment here (scanner-surface rule).
|
|
149
|
+
*/
|
|
150
|
+
relayBaseUrl?: string;
|
|
151
|
+
/**
|
|
152
|
+
* Pair mode advertised in the relay open-frame for `/pair/init`.
|
|
153
|
+
* Defaults to 'either' (the relay will render both generate + import
|
|
154
|
+
* panels). Callers can pin 'generate' or 'import'.
|
|
155
|
+
*/
|
|
156
|
+
initPairMode?: 'generate' | 'import' | 'either';
|
|
157
|
+
/**
|
|
158
|
+
* Override the WebSocket constructor used by `openRemotePairSession`
|
|
159
|
+
* for the `/pair/init` route. Tests inject a stub; production leaves
|
|
160
|
+
* this unset so the real `ws` client is used.
|
|
161
|
+
*/
|
|
162
|
+
initWebSocketImpl?: typeof import('ws').WebSocket;
|
|
163
|
+
/**
|
|
164
|
+
* Override the forward-frame await timeout (ms) for the in-process
|
|
165
|
+
* background await. Defaults to the 5-minute relay TTL.
|
|
166
|
+
*/
|
|
167
|
+
initAwaitTimeoutMs?: number;
|
|
114
168
|
}
|
|
115
169
|
|
|
116
170
|
/**
|
|
@@ -133,23 +187,32 @@ interface PairRespondBody {
|
|
|
133
187
|
* Shape returned so the plugin wiring can invoke each handler directly.
|
|
134
188
|
* Callers normally pass each one to `api.registerHttpRoute` — but we
|
|
135
189
|
* also expose them in an object for tests.
|
|
190
|
+
*
|
|
191
|
+
* `initPath` / `handlers.init` are present ONLY when `PairHttpConfig`
|
|
192
|
+
* supplied a `relayBaseUrl` (3.3.14 in-process pair route). Older
|
|
193
|
+
* callers that omit it get back the original four-route bundle.
|
|
136
194
|
*/
|
|
137
195
|
export interface PairRouteBundle {
|
|
138
196
|
finishPath: string;
|
|
139
197
|
startPath: string;
|
|
140
198
|
respondPath: string;
|
|
141
199
|
statusPath: string;
|
|
200
|
+
/** Present only when `cfg.relayBaseUrl` is set (in-process pair route). */
|
|
201
|
+
initPath?: string;
|
|
142
202
|
handlers: {
|
|
143
203
|
finish: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
144
204
|
start: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
145
205
|
respond: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
146
206
|
status: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
207
|
+
/** Present only when `cfg.relayBaseUrl` is set (in-process pair route). */
|
|
208
|
+
init?: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
147
209
|
};
|
|
148
210
|
}
|
|
149
211
|
|
|
150
212
|
/**
|
|
151
|
-
* Build the
|
|
152
|
-
* `api.registerHttpRoute({ path, handler })`.
|
|
213
|
+
* Build the route handlers. The caller registers each with
|
|
214
|
+
* `api.registerHttpRoute({ path, handler })`. When `cfg.relayBaseUrl` is
|
|
215
|
+
* set, the bundle also carries the in-process `/pair/init` handler.
|
|
153
216
|
*/
|
|
154
217
|
export function buildPairRoutes(cfg: PairHttpConfig): PairRouteBundle {
|
|
155
218
|
const apiBase = cfg.apiBase.replace(/\/+$/, '');
|
|
@@ -402,7 +465,124 @@ export function buildPairRoutes(cfg: PairHttpConfig): PairRouteBundle {
|
|
|
402
465
|
});
|
|
403
466
|
}
|
|
404
467
|
|
|
405
|
-
|
|
468
|
+
// ---------------------------------------------------------------
|
|
469
|
+
// 3.3.14 — In-process pair trigger (the 30s-subprocess-kill 502 fix)
|
|
470
|
+
// ---------------------------------------------------------------
|
|
471
|
+
//
|
|
472
|
+
// Defined unconditionally so the handler identity is stable for the
|
|
473
|
+
// lifetime of the bundle; it short-circuits with 503 when no relay
|
|
474
|
+
// URL was wired. Only attached to the returned bundle + registered
|
|
475
|
+
// as a route when `cfg.relayBaseUrl` is set (see the return below).
|
|
476
|
+
async function handleInit(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
477
|
+
if (!methodAllowed(req, ['GET'])) {
|
|
478
|
+
sendJson(res, 405, { error: 'method_not_allowed' });
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const relay = cfg.relayBaseUrl;
|
|
482
|
+
if (!relay) {
|
|
483
|
+
// Caller did not wire a relay URL — the in-process route is inert.
|
|
484
|
+
sendJson(res, 503, { error: 'init_not_configured' });
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// 1. Open the relay WebSocket IN-PROCESS (the gateway process owns the
|
|
489
|
+
// socket). This is the fix: the WS is no longer held by a CLI
|
|
490
|
+
// subprocess, so OpenClaw's 30s shell-tool timeout cannot kill it
|
|
491
|
+
// and the relay never sees a mid-pair disconnect → no more 502 on
|
|
492
|
+
// /pair/respond.
|
|
493
|
+
let session: RemotePairSession;
|
|
494
|
+
try {
|
|
495
|
+
const openOpts: Parameters<typeof openRemotePairSession>[0] = {
|
|
496
|
+
relayBaseUrl: relay,
|
|
497
|
+
mode: cfg.initPairMode ?? 'either',
|
|
498
|
+
};
|
|
499
|
+
if (cfg.initWebSocketImpl) {
|
|
500
|
+
openOpts.webSocketImpl = cfg.initWebSocketImpl;
|
|
501
|
+
}
|
|
502
|
+
session = await openRemotePairSession(openOpts);
|
|
503
|
+
} catch (err) {
|
|
504
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
505
|
+
cfg.logger.warn(`pair-http /init: relay session open failed: ${msg}`);
|
|
506
|
+
sendJson(res, 502, { error: 'relay_open_failed', detail: msg });
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const parsedExpiresMs = Date.parse(session.expiresAt);
|
|
511
|
+
const expiresAtMs = Number.isFinite(parsedExpiresMs)
|
|
512
|
+
? parsedExpiresMs
|
|
513
|
+
: now() + 5 * 60_000;
|
|
514
|
+
|
|
515
|
+
// 2. Respond IMMEDIATELY with the URL + PIN + sid (relay token) +
|
|
516
|
+
// expiry. The agent reads this and surfaces the URL+PIN to the
|
|
517
|
+
// user. The relay token here plays the `sid` role for agent-side
|
|
518
|
+
// correlation (parity with the CLI JSON payload shape).
|
|
519
|
+
sendJson(res, 200, {
|
|
520
|
+
v: 1,
|
|
521
|
+
sid: session.token,
|
|
522
|
+
url: session.url,
|
|
523
|
+
pin: session.pin,
|
|
524
|
+
mode: session.mode,
|
|
525
|
+
expires_at_ms: expiresAtMs,
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// 3. Start the background wait IN THE GATEWAY PROCESS. We do NOT
|
|
529
|
+
// await this from the request handler — the HTTP response has
|
|
530
|
+
// already been sent. The promise resolves when the browser
|
|
531
|
+
// uploads the encrypted phrase (the relay pushes a `forward`
|
|
532
|
+
// frame), the gateway decrypts locally, and `completePairing`
|
|
533
|
+
// writes credentials.json + flips onboarding state. Errors are
|
|
534
|
+
// logged but never reach the HTTP response (it's already gone).
|
|
535
|
+
//
|
|
536
|
+
// The injected `completePairing` callback receives the mnemonic
|
|
537
|
+
// + a session-shaped object. We adapt the relay session to the
|
|
538
|
+
// `PairSession`-like shape the existing handler signature
|
|
539
|
+
// expects; only the fields completePairing actually reads are
|
|
540
|
+
// populated, the rest default. The mnemonic is the load-bearing
|
|
541
|
+
// field (it writes credentials.json); the session shape carries
|
|
542
|
+
// sid/mode for log correlation.
|
|
543
|
+
void (async (): Promise<void> => {
|
|
544
|
+
try {
|
|
545
|
+
const result = await awaitPhraseUpload(session, {
|
|
546
|
+
phraseValidator: validate,
|
|
547
|
+
timeoutMs: cfg.initAwaitTimeoutMs,
|
|
548
|
+
completePairing: async ({ mnemonic }) => {
|
|
549
|
+
// Adapt the relay session to the PairSession-like shape the
|
|
550
|
+
// existing CompletePairingHandler signature expects. Only
|
|
551
|
+
// sid + mode are load-bearing for log correlation; the
|
|
552
|
+
// crypto fields are unused (decryption already happened).
|
|
553
|
+
const sessionLike = {
|
|
554
|
+
sid: session.token,
|
|
555
|
+
mode: session.mode === 'generate' ? 'generate' : 'import',
|
|
556
|
+
} as PairSession;
|
|
557
|
+
return cfg.completePairing({ mnemonic, session: sessionLike });
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
if (result.state === 'active') {
|
|
561
|
+
cfg.logger.info(
|
|
562
|
+
`pair-http /init: session ${redactSid(session.token)} completed in-process; onboarding active`,
|
|
563
|
+
);
|
|
564
|
+
} else {
|
|
565
|
+
cfg.logger.warn(
|
|
566
|
+
`pair-http /init: session ${redactSid(session.token)} completion non-active: ${result.error ?? 'unknown'}`,
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
} catch (err) {
|
|
570
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
571
|
+
// Timeouts are expected when the user closes the browser without
|
|
572
|
+
// completing; log at info so a benign expire doesn't look like an
|
|
573
|
+
// error in the gateway log.
|
|
574
|
+
if (msg.includes('timeout') || msg.includes('closed')) {
|
|
575
|
+
cfg.logger.info(
|
|
576
|
+
`pair-http /init: session ${redactSid(session.token)} expired/closed before completion`,
|
|
577
|
+
);
|
|
578
|
+
} else {
|
|
579
|
+
cfg.logger.warn(`pair-http /init: session ${redactSid(session.token)} failed: ${msg}`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
})();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const bundle: PairRouteBundle = {
|
|
406
586
|
finishPath: `${apiBase}/finish`,
|
|
407
587
|
startPath: `${apiBase}/start`,
|
|
408
588
|
respondPath: `${apiBase}/respond`,
|
|
@@ -414,6 +594,15 @@ export function buildPairRoutes(cfg: PairHttpConfig): PairRouteBundle {
|
|
|
414
594
|
status: handleStatus,
|
|
415
595
|
},
|
|
416
596
|
};
|
|
597
|
+
// Only surface the in-process /init route when a relay URL is wired.
|
|
598
|
+
// Older callers that construct the bundle without `relayBaseUrl` get
|
|
599
|
+
// the original four-route shape (back-compat for tests + any external
|
|
600
|
+
// consumers of buildPairRoutes).
|
|
601
|
+
if (cfg.relayBaseUrl) {
|
|
602
|
+
bundle.initPath = `${apiBase}/init`;
|
|
603
|
+
bundle.handlers.init = handleInit;
|
|
604
|
+
}
|
|
605
|
+
return bundle;
|
|
417
606
|
}
|
|
418
607
|
|
|
419
608
|
// ---------------------------------------------------------------------------
|
package/skill.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "totalreclaw",
|
|
3
|
-
"version": "3.3.12-rc.
|
|
3
|
+
"version": "3.3.12-rc.15",
|
|
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
|
@@ -68,7 +68,7 @@ const STATE_PATH = CONFIG.onboardingStatePath;
|
|
|
68
68
|
// Auto-synced by skill/scripts/sync-version.mjs from skill/plugin/package.json::version.
|
|
69
69
|
// Do not edit by hand — running tests will catch drift but the publish workflow
|
|
70
70
|
// rewrites this constant at the start of every npm/ClawHub publish.
|
|
71
|
-
const PLUGIN_VERSION = '3.3.12-rc.
|
|
71
|
+
const PLUGIN_VERSION = '3.3.12-rc.15';
|
|
72
72
|
|
|
73
73
|
function die(msg: string, code = 1): never {
|
|
74
74
|
process.stderr.write(`tr: ${msg}\n`);
|