@totalreclaw/totalreclaw 3.1.0 → 3.2.0
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/config.ts +4 -0
- package/fs-helpers.ts +146 -0
- package/index.ts +352 -212
- package/onboarding-cli.ts +546 -0
- package/package.json +10 -3
- package/tool-gating.ts +69 -0
package/index.ts
CHANGED
|
@@ -20,10 +20,27 @@
|
|
|
20
20
|
* - totalreclaw_import_from -- import memories from other tools (Mem0, MCP Memory, etc.)
|
|
21
21
|
* - totalreclaw_upgrade -- create Stripe checkout for Pro upgrade
|
|
22
22
|
* - totalreclaw_migrate -- migrate testnet memories to mainnet after Pro upgrade
|
|
23
|
-
* -
|
|
23
|
+
* - totalreclaw_onboarding_start -- non-secret pointer to the CLI wizard (3.2.0)
|
|
24
|
+
* - totalreclaw_setup -- DEPRECATED in 3.2.0; redirects to the CLI wizard
|
|
24
25
|
*
|
|
25
|
-
* Also registers
|
|
26
|
-
*
|
|
26
|
+
* Also registers:
|
|
27
|
+
* - `before_agent_start` hook that automatically injects relevant memories
|
|
28
|
+
* into the agent's context (and a non-secret onboarding hint when the
|
|
29
|
+
* user has not completed the CLI setup yet).
|
|
30
|
+
* - `before_tool_call` hook that gates every memory tool until onboarding
|
|
31
|
+
* state is `active` (3.2.0).
|
|
32
|
+
* - `registerCli` subcommand `openclaw totalreclaw onboard` — the ONLY
|
|
33
|
+
* surface that generates or accepts a recovery phrase. Lives entirely on
|
|
34
|
+
* the user's terminal; the phrase never enters an LLM request or a
|
|
35
|
+
* session transcript.
|
|
36
|
+
* - `registerCommand` slash command `/totalreclaw {onboard,status}` — a
|
|
37
|
+
* non-secret pointer that directs the user to the CLI wizard.
|
|
38
|
+
*
|
|
39
|
+
* Security: in 3.2.0, the recovery phrase NEVER appears in tool responses,
|
|
40
|
+
* `prependContext` blocks, slash-command replies, or any other surface that
|
|
41
|
+
* is sent to the LLM provider or persisted to the session transcript. See
|
|
42
|
+
* `docs/plans/2026-04-20-plugin-320-secure-onboarding.md` in the internal
|
|
43
|
+
* repo for the threat-model analysis and per-surface classification.
|
|
27
44
|
*
|
|
28
45
|
* All data is encrypted client-side with XChaCha20-Poly1305. The server never
|
|
29
46
|
* sees plaintext.
|
|
@@ -115,10 +132,10 @@ import {
|
|
|
115
132
|
deleteCredentialsFile,
|
|
116
133
|
isRunningInDocker,
|
|
117
134
|
deleteFileIfExists,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
type BootstrapOutcome,
|
|
135
|
+
resolveOnboardingState,
|
|
136
|
+
type OnboardingState,
|
|
121
137
|
} from './fs-helpers.js';
|
|
138
|
+
import { decideToolGate, isGatedToolName } from './tool-gating.js';
|
|
122
139
|
import crypto from 'node:crypto';
|
|
123
140
|
|
|
124
141
|
// ---------------------------------------------------------------------------
|
|
@@ -155,6 +172,38 @@ interface OpenClawPluginApi {
|
|
|
155
172
|
registerTool(tool: unknown, opts?: { name?: string; names?: string[] }): void;
|
|
156
173
|
registerService(service: { id: string; start(): void; stop?(): void }): void;
|
|
157
174
|
on(hookName: string, handler: (...args: unknown[]) => unknown, opts?: { priority?: number }): void;
|
|
175
|
+
/**
|
|
176
|
+
* 3.2.0 — register a top-level `openclaw <cmd>` subcommand. The handler
|
|
177
|
+
* receives a commander `Command` to attach subcommands to. Output goes
|
|
178
|
+
* straight to the user's TTY; nothing touches the LLM or the transcript.
|
|
179
|
+
* We deliberately type `program` as `unknown` at this boundary because
|
|
180
|
+
* we don't import the SDK's full types; the runtime shape is commander's
|
|
181
|
+
* `Command` which we cast at the call site.
|
|
182
|
+
*/
|
|
183
|
+
registerCli?(
|
|
184
|
+
registrar: (ctx: { program: unknown; config?: unknown; workspaceDir?: string; logger?: unknown }) => void | Promise<void>,
|
|
185
|
+
opts?: { commands?: string[] },
|
|
186
|
+
): void;
|
|
187
|
+
/**
|
|
188
|
+
* 3.2.0 — register a slash command (e.g. `/totalreclaw`). The handler
|
|
189
|
+
* runs before the agent; its reply is delivered via the channel adapter.
|
|
190
|
+
* Reply text IS appended to the session transcript (see gateway-cli
|
|
191
|
+
* L9300-9312), so we only emit non-secret pointers.
|
|
192
|
+
*/
|
|
193
|
+
registerCommand?(command: {
|
|
194
|
+
name: string;
|
|
195
|
+
description: string;
|
|
196
|
+
acceptsArgs?: boolean;
|
|
197
|
+
requireAuth?: boolean;
|
|
198
|
+
handler: (ctx: {
|
|
199
|
+
senderId?: string;
|
|
200
|
+
channel?: string;
|
|
201
|
+
args?: string;
|
|
202
|
+
commandBody?: string;
|
|
203
|
+
isAuthorizedSender?: boolean;
|
|
204
|
+
config?: unknown;
|
|
205
|
+
}) => { text: string } | Promise<{ text: string }>;
|
|
206
|
+
}): void;
|
|
158
207
|
}
|
|
159
208
|
|
|
160
209
|
// ---------------------------------------------------------------------------
|
|
@@ -410,84 +459,44 @@ let needsSetup = false;
|
|
|
410
459
|
let firstRunAfterInit = true;
|
|
411
460
|
|
|
412
461
|
/**
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
* phrase. Populated by `autoBootstrapCredentials` inside `initialize()`;
|
|
416
|
-
* consumed + cleared by the hook, which then calls
|
|
417
|
-
* `markFirstRunAnnouncementShown(CREDENTIALS_PATH)` to persist the
|
|
418
|
-
* acknowledgement to disk so a process restart does not re-announce.
|
|
419
|
-
*/
|
|
420
|
-
let pendingFirstRunAnnouncement: {
|
|
421
|
-
mnemonic: string;
|
|
422
|
-
/** 'fresh_generated' | 'recovered_from_corrupt' */
|
|
423
|
-
reason: 'fresh_generated' | 'recovered_from_corrupt';
|
|
424
|
-
/** Set when `reason === 'recovered_from_corrupt'`. */
|
|
425
|
-
backupPath?: string;
|
|
426
|
-
} | null = null;
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Derive keys from the recovery phrase, load or create credentials, and
|
|
430
|
-
* register with the server if this is the first run.
|
|
462
|
+
* Derive keys from the recovery phrase, load credentials, and register with
|
|
463
|
+
* the server if this is the first run.
|
|
431
464
|
*
|
|
432
|
-
* 3.
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
465
|
+
* 3.2.0: this function is read-only with respect to the mnemonic. It pulls
|
|
466
|
+
* the phrase from either the env var override or an existing
|
|
467
|
+
* `credentials.json` written by the onboarding wizard. It never generates a
|
|
468
|
+
* fresh phrase — that only happens inside the CLI wizard where the phrase
|
|
469
|
+
* can be surfaced to the user on a non-LLM TTY. If no usable phrase is
|
|
470
|
+
* available here, `needsSetup` is flipped and the `before_tool_call` gate
|
|
471
|
+
* directs the caller to `openclaw totalreclaw onboard`.
|
|
437
472
|
*/
|
|
438
473
|
async function initialize(logger: OpenClawPluginApi['logger']): Promise<void> {
|
|
439
474
|
const serverUrl = CONFIG.serverUrl || 'https://api.totalreclaw.xyz';
|
|
440
475
|
let masterPassword = CONFIG.recoveryPhrase;
|
|
441
476
|
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
// next agent turn revealing the phrase (see `pendingFirstRunAnnouncement`
|
|
448
|
-
// + the before_agent_start handler).
|
|
477
|
+
// 3.2.0: if the env var is unset, probe credentials.json for a
|
|
478
|
+
// pre-existing mnemonic (written either by the CLI wizard on this machine
|
|
479
|
+
// or ported in from another client). We do NOT generate a phrase here —
|
|
480
|
+
// generation is the wizard's job so the user sees the phrase on a TTY
|
|
481
|
+
// and never through the LLM.
|
|
449
482
|
if (!masterPassword) {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
});
|
|
460
|
-
masterPassword = outcome.mnemonic;
|
|
461
|
-
setRecoveryPhraseOverride(outcome.mnemonic);
|
|
462
|
-
if (outcome.status === 'fresh_generated') {
|
|
463
|
-
logger.info('Auto-setup: generated a fresh recovery phrase + wrote credentials.json');
|
|
464
|
-
} else if (outcome.status === 'recovered_from_corrupt') {
|
|
465
|
-
logger.warn(
|
|
466
|
-
`Auto-setup: credentials.json was unusable; renamed to ${outcome.backupPath ?? '<rename failed>'} ` +
|
|
467
|
-
`and generated a new recovery phrase. The old file is preserved in case you can recover ` +
|
|
468
|
-
`the prior mnemonic from it.`,
|
|
469
|
-
);
|
|
470
|
-
} else {
|
|
471
|
-
logger.info('Auto-setup: reusing existing credentials.json');
|
|
472
|
-
}
|
|
473
|
-
if (outcome.announcementPending) {
|
|
474
|
-
pendingFirstRunAnnouncement = {
|
|
475
|
-
mnemonic: outcome.mnemonic,
|
|
476
|
-
reason: outcome.status === 'recovered_from_corrupt' ? 'recovered_from_corrupt' : 'fresh_generated',
|
|
477
|
-
backupPath: outcome.backupPath,
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
} catch (err) {
|
|
481
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
482
|
-
logger.warn(`Auto-setup failed (${msg}); falling back to "setup required" flow`);
|
|
483
|
-
needsSetup = true;
|
|
484
|
-
return;
|
|
483
|
+
const existing = loadCredentialsJson(CREDENTIALS_PATH);
|
|
484
|
+
const candidate =
|
|
485
|
+
(typeof existing?.mnemonic === 'string' && existing.mnemonic.trim()) ||
|
|
486
|
+
(typeof existing?.recovery_phrase === 'string' && existing.recovery_phrase.trim()) ||
|
|
487
|
+
'';
|
|
488
|
+
if (candidate) {
|
|
489
|
+
masterPassword = candidate;
|
|
490
|
+
setRecoveryPhraseOverride(candidate);
|
|
491
|
+
logger.info('Loaded recovery phrase from credentials.json');
|
|
485
492
|
}
|
|
486
493
|
}
|
|
487
494
|
|
|
488
495
|
if (!masterPassword) {
|
|
489
496
|
needsSetup = true;
|
|
490
|
-
logger.info(
|
|
497
|
+
logger.info(
|
|
498
|
+
'TotalReclaw: no recovery phrase available — run `openclaw totalreclaw onboard` in a terminal to set up',
|
|
499
|
+
);
|
|
491
500
|
return;
|
|
492
501
|
}
|
|
493
502
|
|
|
@@ -2526,6 +2535,94 @@ const plugin = {
|
|
|
2526
2535
|
},
|
|
2527
2536
|
});
|
|
2528
2537
|
|
|
2538
|
+
// ---------------------------------------------------------------
|
|
2539
|
+
// 3.2.0 — CLI wizard registration (leak-free onboarding surface)
|
|
2540
|
+
// ---------------------------------------------------------------
|
|
2541
|
+
//
|
|
2542
|
+
// `api.registerCli` attaches a top-level `openclaw totalreclaw ...`
|
|
2543
|
+
// subcommand chain. The wizard runs entirely on the user's TTY —
|
|
2544
|
+
// stdout/stdin — and NEVER routes any of its I/O through the LLM
|
|
2545
|
+
// provider or the session transcript. This is the ONLY surface in
|
|
2546
|
+
// 3.2.0 where a recovery phrase is generated or accepted.
|
|
2547
|
+
//
|
|
2548
|
+
// The dynamic import keeps the @scure/bip39 + readline/promises
|
|
2549
|
+
// surface out of the `register()` hot path — only pulled in when the
|
|
2550
|
+
// CLI subcommand actually fires.
|
|
2551
|
+
if (typeof api.registerCli === 'function') {
|
|
2552
|
+
api.registerCli(
|
|
2553
|
+
async ({ program }) => {
|
|
2554
|
+
const { registerOnboardingCli } = await import('./onboarding-cli.js');
|
|
2555
|
+
registerOnboardingCli(program as import('commander').Command, {
|
|
2556
|
+
credentialsPath: CREDENTIALS_PATH,
|
|
2557
|
+
statePath: CONFIG.onboardingStatePath,
|
|
2558
|
+
logger: api.logger,
|
|
2559
|
+
});
|
|
2560
|
+
},
|
|
2561
|
+
{ commands: ['totalreclaw'] },
|
|
2562
|
+
);
|
|
2563
|
+
} else {
|
|
2564
|
+
api.logger.warn(
|
|
2565
|
+
'api.registerCli is unavailable on this OpenClaw version — `openclaw totalreclaw onboard` will not work. ' +
|
|
2566
|
+
'Users can still set TOTALRECLAW_RECOVERY_PHRASE manually.',
|
|
2567
|
+
);
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
// ---------------------------------------------------------------
|
|
2571
|
+
// 3.2.0 — slash command `/totalreclaw {onboard,status}` (in-chat bridge)
|
|
2572
|
+
// ---------------------------------------------------------------
|
|
2573
|
+
//
|
|
2574
|
+
// `api.registerCommand` replies bypass the LLM for the current turn BUT
|
|
2575
|
+
// are appended to the session transcript, so the LLM sees the reply on
|
|
2576
|
+
// the NEXT turn. That is fine here because every reply is a non-secret
|
|
2577
|
+
// pointer — it directs the user to the CLI wizard and explicitly
|
|
2578
|
+
// explains why the phrase cannot appear in chat.
|
|
2579
|
+
if (typeof api.registerCommand === 'function') {
|
|
2580
|
+
api.registerCommand({
|
|
2581
|
+
name: 'totalreclaw',
|
|
2582
|
+
description: 'TotalReclaw onboarding + status (non-secret pointer to the terminal wizard)',
|
|
2583
|
+
acceptsArgs: true,
|
|
2584
|
+
requireAuth: false,
|
|
2585
|
+
handler: async (ctx) => {
|
|
2586
|
+
const sub = (ctx.args || '').trim().split(/\s+/)[0]?.toLowerCase() || 'help';
|
|
2587
|
+
if (sub === 'onboard' || sub === 'setup' || sub === 'init') {
|
|
2588
|
+
return {
|
|
2589
|
+
text:
|
|
2590
|
+
'To set up TotalReclaw, open a terminal on this machine and run:\n\n' +
|
|
2591
|
+
' openclaw totalreclaw onboard\n\n' +
|
|
2592
|
+
'Why a terminal? Your recovery phrase must never pass through the ' +
|
|
2593
|
+
'LLM provider. This chat message is visible to the LLM, so we do ' +
|
|
2594
|
+
'not show the phrase here. The wizard runs entirely on your local ' +
|
|
2595
|
+
'machine — the phrase is generated, displayed, and stored on disk ' +
|
|
2596
|
+
'(mode 0600) without ever touching the network.',
|
|
2597
|
+
};
|
|
2598
|
+
}
|
|
2599
|
+
if (sub === 'status') {
|
|
2600
|
+
// Non-secret summary — never shows the mnemonic.
|
|
2601
|
+
let stateLabel: string;
|
|
2602
|
+
try {
|
|
2603
|
+
const state = resolveOnboardingState(CREDENTIALS_PATH, CONFIG.onboardingStatePath);
|
|
2604
|
+
stateLabel = state.onboardingState;
|
|
2605
|
+
} catch {
|
|
2606
|
+
stateLabel = 'unknown';
|
|
2607
|
+
}
|
|
2608
|
+
return {
|
|
2609
|
+
text:
|
|
2610
|
+
`TotalReclaw onboarding state: ${stateLabel}.\n` +
|
|
2611
|
+
(stateLabel === 'active'
|
|
2612
|
+
? 'Memory tools are active on this machine.'
|
|
2613
|
+
: 'Memory tools are gated. Run `openclaw totalreclaw onboard` in a terminal to complete setup.'),
|
|
2614
|
+
};
|
|
2615
|
+
}
|
|
2616
|
+
return {
|
|
2617
|
+
text:
|
|
2618
|
+
'TotalReclaw slash commands:\n' +
|
|
2619
|
+
' /totalreclaw onboard — how to set up TotalReclaw securely\n' +
|
|
2620
|
+
' /totalreclaw status — current onboarding state',
|
|
2621
|
+
};
|
|
2622
|
+
},
|
|
2623
|
+
});
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2529
2626
|
// ---------------------------------------------------------------
|
|
2530
2627
|
// Tool: totalreclaw_remember
|
|
2531
2628
|
// ---------------------------------------------------------------
|
|
@@ -4044,126 +4141,204 @@ const plugin = {
|
|
|
4044
4141
|
);
|
|
4045
4142
|
|
|
4046
4143
|
// ---------------------------------------------------------------
|
|
4047
|
-
// Tool: totalreclaw_setup
|
|
4144
|
+
// Tool: totalreclaw_setup (DEPRECATED in 3.2.0)
|
|
4048
4145
|
// ---------------------------------------------------------------
|
|
4049
|
-
|
|
4146
|
+
//
|
|
4147
|
+
// Pre-3.2.0 behaviour: auto-generate a mnemonic + return it in the tool
|
|
4148
|
+
// response body so the LLM surfaces it to the user. That path shipped
|
|
4149
|
+
// the recovery phrase to the LLM provider's logs — incompatible with
|
|
4150
|
+
// TotalReclaw's "server cannot read your memories" pitch. 3.2.0
|
|
4151
|
+
// replaces it with a pointer-only stub.
|
|
4152
|
+
//
|
|
4153
|
+
// Kept registered (rather than deleted) for back-compat — LLMs that
|
|
4154
|
+
// learned the old tool name from training data won't silently succeed
|
|
4155
|
+
// if the user asks them to set up memory. They'll call this tool,
|
|
4156
|
+
// receive a pointer to `openclaw totalreclaw onboard`, and the flow
|
|
4157
|
+
// continues on the user's TTY.
|
|
4158
|
+
//
|
|
4159
|
+
// The `recovery_phrase` param is kept in the schema so existing tool
|
|
4160
|
+
// calls parse — but ANY phrase the caller passes is rejected, and the
|
|
4161
|
+
// tool NEVER writes credentials.json. All real setup happens in the
|
|
4162
|
+
// CLI wizard.
|
|
4050
4163
|
api.registerTool(
|
|
4051
4164
|
{
|
|
4052
4165
|
name: 'totalreclaw_setup',
|
|
4053
|
-
label: '
|
|
4166
|
+
label: 'TotalReclaw setup (deprecated — redirect to CLI)',
|
|
4054
4167
|
description:
|
|
4055
|
-
'
|
|
4056
|
-
'
|
|
4168
|
+
'DEPRECATED in 3.2.0. This tool no longer accepts recovery phrases or performs ' +
|
|
4169
|
+
'setup. It returns a pointer to `openclaw totalreclaw onboard` — the secure CLI ' +
|
|
4170
|
+
'wizard that runs on the user\'s terminal so the phrase never touches the LLM ' +
|
|
4171
|
+
'provider. Prefer calling `totalreclaw_onboarding_start` for the same pointer.',
|
|
4057
4172
|
parameters: {
|
|
4058
4173
|
type: 'object',
|
|
4059
4174
|
properties: {
|
|
4060
4175
|
recovery_phrase: {
|
|
4061
4176
|
type: 'string',
|
|
4062
|
-
description:
|
|
4177
|
+
description:
|
|
4178
|
+
'Legacy parameter — IGNORED in 3.2.0. If provided, the tool returns a ' +
|
|
4179
|
+
'security warning explaining that phrases must never be pasted through ' +
|
|
4180
|
+
'chat. Use the `openclaw totalreclaw onboard` CLI wizard to import an ' +
|
|
4181
|
+
'existing phrase safely.',
|
|
4063
4182
|
},
|
|
4064
4183
|
},
|
|
4065
4184
|
additionalProperties: false,
|
|
4066
4185
|
},
|
|
4067
4186
|
async execute(_toolCallId: string, params: { recovery_phrase?: string }) {
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
// trip). Prior versions always rebuilt everything.
|
|
4076
|
-
if (!needsSetup) {
|
|
4077
|
-
const currentCreds = loadCredentialsJson(CREDENTIALS_PATH);
|
|
4078
|
-
const currentMnemonic = typeof currentCreds?.mnemonic === 'string' ? currentCreds.mnemonic.trim() : '';
|
|
4079
|
-
if (!providedPhrase || (currentMnemonic && currentMnemonic === providedPhrase)) {
|
|
4080
|
-
return {
|
|
4081
|
-
content: [{
|
|
4082
|
-
type: 'text',
|
|
4083
|
-
text: 'TotalReclaw is already set up. Your existing recovery phrase is kept — no changes made.\n\n' +
|
|
4084
|
-
'If you want to rotate wallets or switch to a different phrase, first delete ~/.totalreclaw/credentials.json, then call this tool with the new phrase.',
|
|
4085
|
-
}],
|
|
4086
|
-
};
|
|
4087
|
-
}
|
|
4088
|
-
}
|
|
4089
|
-
|
|
4090
|
-
let mnemonic = providedPhrase;
|
|
4091
|
-
|
|
4092
|
-
// Auto-generate if not provided
|
|
4093
|
-
if (!mnemonic) {
|
|
4094
|
-
const { generateMnemonic } = await import('@scure/bip39');
|
|
4095
|
-
const { wordlist } = await import('@scure/bip39/wordlists/english');
|
|
4096
|
-
mnemonic = generateMnemonic(wordlist, 128);
|
|
4097
|
-
api.logger.info('totalreclaw_setup: generated new BIP-39 mnemonic');
|
|
4098
|
-
}
|
|
4099
|
-
|
|
4100
|
-
// Guard: refuse to overwrite existing credentials with a DIFFERENT phrase
|
|
4101
|
-
// (prevents data loss when background sessions_spawn workers call setup).
|
|
4102
|
-
// Allow re-init with the SAME phrase (handles agent exec → setup flow).
|
|
4103
|
-
// loadCredentialsJson returns null for missing/corrupt files — we proceed
|
|
4104
|
-
// with setup in both cases, matching the prior try/catch semantics.
|
|
4105
|
-
const existingCreds = loadCredentialsJson(CREDENTIALS_PATH);
|
|
4106
|
-
if (existingCreds && existingCreds.mnemonic && existingCreds.userId && existingCreds.mnemonic !== mnemonic) {
|
|
4107
|
-
api.logger.info('totalreclaw_setup: credentials exist with different mnemonic, refusing to overwrite');
|
|
4108
|
-
return {
|
|
4109
|
-
content: [{
|
|
4110
|
-
type: 'text',
|
|
4111
|
-
text: 'TotalReclaw is already set up with an existing recovery phrase. Your encrypted memories are tied to that phrase.\n\n' +
|
|
4112
|
-
'If you intentionally want to start fresh with a NEW phrase (this will make existing memories inaccessible), ' +
|
|
4113
|
-
'delete ~/.totalreclaw/credentials.json first, then call this tool again.',
|
|
4114
|
-
}],
|
|
4115
|
-
};
|
|
4116
|
-
}
|
|
4117
|
-
|
|
4118
|
-
// Basic validation: must be 12 words
|
|
4119
|
-
const words = mnemonic.split(/\s+/);
|
|
4120
|
-
if (words.length !== 12) {
|
|
4121
|
-
return {
|
|
4122
|
-
content: [{
|
|
4123
|
-
type: 'text',
|
|
4124
|
-
text: `Error: Recovery phrase must be exactly 12 words (got ${words.length}). Use \`npx @totalreclaw/mcp-server setup\` to generate a valid BIP-39 mnemonic.`,
|
|
4125
|
-
}],
|
|
4126
|
-
};
|
|
4127
|
-
}
|
|
4128
|
-
|
|
4129
|
-
api.logger.info('totalreclaw_setup: initializing with provided recovery phrase');
|
|
4130
|
-
|
|
4131
|
-
// Force re-initialization with the new mnemonic.
|
|
4132
|
-
// This derives keys, registers with the server, saves credentials,
|
|
4133
|
-
// and sets up LSH/auth — all without a gateway restart.
|
|
4134
|
-
await forceReinitialization(mnemonic, api.logger);
|
|
4135
|
-
|
|
4136
|
-
if (needsSetup) {
|
|
4137
|
-
return {
|
|
4138
|
-
content: [{
|
|
4139
|
-
type: 'text',
|
|
4140
|
-
text: 'Setup failed — could not initialize with the provided recovery phrase. Check the logs for details.',
|
|
4141
|
-
}],
|
|
4142
|
-
};
|
|
4143
|
-
}
|
|
4144
|
-
|
|
4145
|
-
const wasGenerated = !params.recovery_phrase?.trim();
|
|
4187
|
+
// Phrase-passing is a security boundary violation in 3.2.0. Reject
|
|
4188
|
+
// with a message that explains WHY — the LLM might try again with
|
|
4189
|
+
// a different shape otherwise.
|
|
4190
|
+
if (typeof params?.recovery_phrase === 'string' && params.recovery_phrase.trim().length > 0) {
|
|
4191
|
+
api.logger.warn(
|
|
4192
|
+
'totalreclaw_setup: rejected phrase-passing call (3.2.0 deprecation).',
|
|
4193
|
+
);
|
|
4146
4194
|
return {
|
|
4147
4195
|
content: [{
|
|
4148
4196
|
type: 'text',
|
|
4149
|
-
text:
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4197
|
+
text:
|
|
4198
|
+
'For security, TotalReclaw no longer accepts a recovery phrase through ' +
|
|
4199
|
+
'chat. Pasting a phrase into this tool would ship it to the LLM provider, ' +
|
|
4200
|
+
'which defeats the whole point of end-to-end encryption.\n\n' +
|
|
4201
|
+
'Ask the user to open a terminal on their machine and run:\n\n' +
|
|
4202
|
+
' openclaw totalreclaw onboard\n\n' +
|
|
4203
|
+
'The wizard imports an existing phrase via a hidden stdin prompt that ' +
|
|
4204
|
+
'never touches the LLM, the transcript, or the network.',
|
|
4153
4205
|
}],
|
|
4154
4206
|
};
|
|
4155
|
-
}
|
|
4156
|
-
|
|
4157
|
-
|
|
4207
|
+
}
|
|
4208
|
+
|
|
4209
|
+
// No-arg call against an already-active state: confirm + move on.
|
|
4210
|
+
const state = resolveOnboardingState(CREDENTIALS_PATH, CONFIG.onboardingStatePath);
|
|
4211
|
+
if (state.onboardingState === 'active') {
|
|
4158
4212
|
return {
|
|
4159
|
-
content: [{
|
|
4213
|
+
content: [{
|
|
4214
|
+
type: 'text',
|
|
4215
|
+
text:
|
|
4216
|
+
'TotalReclaw is already set up and active on this machine. Memory tools ' +
|
|
4217
|
+
'are unblocked — you can call `totalreclaw_remember` and `totalreclaw_recall` ' +
|
|
4218
|
+
'directly. If the user wants to rotate phrases, have them delete ' +
|
|
4219
|
+
'`~/.totalreclaw/credentials.json` and run `openclaw totalreclaw onboard` again.',
|
|
4220
|
+
}],
|
|
4160
4221
|
};
|
|
4161
4222
|
}
|
|
4223
|
+
|
|
4224
|
+
// Fresh state, no phrase: redirect to the CLI wizard.
|
|
4225
|
+
return {
|
|
4226
|
+
content: [{
|
|
4227
|
+
type: 'text',
|
|
4228
|
+
text:
|
|
4229
|
+
'TotalReclaw setup must run on the user\'s local terminal so the recovery ' +
|
|
4230
|
+
'phrase never touches the LLM. Ask the user to open a terminal and run:\n\n' +
|
|
4231
|
+
' openclaw totalreclaw onboard\n\n' +
|
|
4232
|
+
'The wizard walks through generate-new-phrase or import-existing-phrase. ' +
|
|
4233
|
+
'After it completes, memory tools become available automatically. See the ' +
|
|
4234
|
+
'`totalreclaw_onboarding_start` tool for the same pointer in a more ' +
|
|
4235
|
+
'discoverable shape.',
|
|
4236
|
+
}],
|
|
4237
|
+
};
|
|
4162
4238
|
},
|
|
4163
4239
|
},
|
|
4164
4240
|
{ name: 'totalreclaw_setup' },
|
|
4165
4241
|
);
|
|
4166
4242
|
|
|
4243
|
+
// ---------------------------------------------------------------
|
|
4244
|
+
// Tool: totalreclaw_onboarding_start (3.2.0 pointer-only tool)
|
|
4245
|
+
// ---------------------------------------------------------------
|
|
4246
|
+
//
|
|
4247
|
+
// When the user asks the LLM to set up TotalReclaw, this tool directs
|
|
4248
|
+
// them to the CLI wizard. The response body is a non-secret pointer —
|
|
4249
|
+
// it NEVER contains a recovery phrase — so it can safely flow through
|
|
4250
|
+
// the LLM provider and the transcript.
|
|
4251
|
+
api.registerTool(
|
|
4252
|
+
{
|
|
4253
|
+
name: 'totalreclaw_onboarding_start',
|
|
4254
|
+
label: 'TotalReclaw — start onboarding',
|
|
4255
|
+
description:
|
|
4256
|
+
'Call this when the user wants to set up TotalReclaw memory or asks about ' +
|
|
4257
|
+
'enabling memory features. This tool does NOT generate, display, or accept ' +
|
|
4258
|
+
'a recovery phrase — it returns a short pointer that tells the user to run ' +
|
|
4259
|
+
'the onboarding wizard in their local terminal. All phrase handling happens ' +
|
|
4260
|
+
'outside the LLM. If TotalReclaw is already active, the tool returns a ' +
|
|
4261
|
+
'confirmation.',
|
|
4262
|
+
parameters: {
|
|
4263
|
+
type: 'object',
|
|
4264
|
+
properties: {},
|
|
4265
|
+
additionalProperties: false,
|
|
4266
|
+
},
|
|
4267
|
+
async execute() {
|
|
4268
|
+
const state = resolveOnboardingState(CREDENTIALS_PATH, CONFIG.onboardingStatePath);
|
|
4269
|
+
if (state.onboardingState === 'active') {
|
|
4270
|
+
return {
|
|
4271
|
+
content: [{
|
|
4272
|
+
type: 'text',
|
|
4273
|
+
text:
|
|
4274
|
+
'TotalReclaw is already set up on this machine. Your encryption keys are ' +
|
|
4275
|
+
'ready — `totalreclaw_remember`, `totalreclaw_recall`, and the other memory ' +
|
|
4276
|
+
'tools are unblocked. Run `openclaw totalreclaw status` in a terminal for ' +
|
|
4277
|
+
'more detail.',
|
|
4278
|
+
}],
|
|
4279
|
+
};
|
|
4280
|
+
}
|
|
4281
|
+
return {
|
|
4282
|
+
content: [{
|
|
4283
|
+
type: 'text',
|
|
4284
|
+
text:
|
|
4285
|
+
'TotalReclaw onboarding requires a local terminal so your recovery phrase ' +
|
|
4286
|
+
'never touches the LLM provider. On the same machine as your OpenClaw ' +
|
|
4287
|
+
'gateway, open a terminal and run:\n\n' +
|
|
4288
|
+
' openclaw totalreclaw onboard\n\n' +
|
|
4289
|
+
'The wizard will ask whether you want to generate a new phrase or import an ' +
|
|
4290
|
+
'existing TotalReclaw phrase. Both paths display/accept the phrase only on ' +
|
|
4291
|
+
'your terminal — nothing crosses the network. After the wizard completes, ' +
|
|
4292
|
+
'come back here and I\'ll be able to use `totalreclaw_remember` and ' +
|
|
4293
|
+
'`totalreclaw_recall`.',
|
|
4294
|
+
}],
|
|
4295
|
+
};
|
|
4296
|
+
},
|
|
4297
|
+
},
|
|
4298
|
+
{ name: 'totalreclaw_onboarding_start' },
|
|
4299
|
+
);
|
|
4300
|
+
|
|
4301
|
+
// ---------------------------------------------------------------
|
|
4302
|
+
// Hook: before_tool_call (3.2.0 memory-tool gate)
|
|
4303
|
+
// ---------------------------------------------------------------
|
|
4304
|
+
//
|
|
4305
|
+
// Blocks every memory tool until onboarding state is `active`. The
|
|
4306
|
+
// `blockReason` string is LLM-visible but carries no secret — it's a
|
|
4307
|
+
// pointer to the CLI wizard.
|
|
4308
|
+
//
|
|
4309
|
+
// Non-gated tools: totalreclaw_upgrade, totalreclaw_migrate,
|
|
4310
|
+
// totalreclaw_onboarding_start, totalreclaw_setup (deprecated).
|
|
4311
|
+
// Billing tools work pre-onboarding because they help the user reach a
|
|
4312
|
+
// Pro tier before they have memories to store; setup-adjacent tools
|
|
4313
|
+
// return their own routing messages.
|
|
4314
|
+
//
|
|
4315
|
+
// Decision logic lives in `tool-gating.ts` so it's unit-testable
|
|
4316
|
+
// without a full plugin host.
|
|
4317
|
+
api.on(
|
|
4318
|
+
'before_tool_call',
|
|
4319
|
+
async (event: unknown) => {
|
|
4320
|
+
const evt = event as { toolName?: string } | undefined;
|
|
4321
|
+
const toolName = evt?.toolName;
|
|
4322
|
+
if (!toolName || !isGatedToolName(toolName)) {
|
|
4323
|
+
return undefined;
|
|
4324
|
+
}
|
|
4325
|
+
let state: OnboardingState | null = null;
|
|
4326
|
+
try {
|
|
4327
|
+
state = resolveOnboardingState(CREDENTIALS_PATH, CONFIG.onboardingStatePath);
|
|
4328
|
+
} catch (err) {
|
|
4329
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4330
|
+
api.logger.warn(`before_tool_call: state resolution failed: ${msg}`);
|
|
4331
|
+
return undefined; // Fail-open: if we can't read state, let the tool run and surface its own error.
|
|
4332
|
+
}
|
|
4333
|
+
const decision = decideToolGate(toolName, state);
|
|
4334
|
+
if (decision.block) {
|
|
4335
|
+
return { block: true, blockReason: decision.blockReason };
|
|
4336
|
+
}
|
|
4337
|
+
return undefined;
|
|
4338
|
+
},
|
|
4339
|
+
{ priority: 5 },
|
|
4340
|
+
);
|
|
4341
|
+
|
|
4167
4342
|
// ---------------------------------------------------------------
|
|
4168
4343
|
// Hook: before_agent_start
|
|
4169
4344
|
// ---------------------------------------------------------------
|
|
@@ -4184,53 +4359,25 @@ const plugin = {
|
|
|
4184
4359
|
|
|
4185
4360
|
await ensureInitialized(api.logger);
|
|
4186
4361
|
|
|
4187
|
-
//
|
|
4362
|
+
// 3.2.0 onboarding pending: emit a non-secret guidance banner so
|
|
4363
|
+
// the LLM knows how to respond when the user asks about setup.
|
|
4364
|
+
// This contains ZERO secret material — the phrase never enters an
|
|
4365
|
+
// LLM request. The CLI wizard (`openclaw totalreclaw onboard`) is
|
|
4366
|
+
// the only surface that generates / reveals the recovery phrase.
|
|
4188
4367
|
if (needsSetup) {
|
|
4189
4368
|
return {
|
|
4190
4369
|
prependContext:
|
|
4191
|
-
'## TotalReclaw
|
|
4192
|
-
'TotalReclaw
|
|
4193
|
-
'
|
|
4194
|
-
'
|
|
4195
|
-
'
|
|
4196
|
-
'
|
|
4197
|
-
'
|
|
4198
|
-
'
|
|
4199
|
-
'**IMPORTANT:** Use the `totalreclaw_setup` tool to complete setup. This initializes encryption keys immediately — no gateway restart needed. The user can continue chatting right away.\n\n' +
|
|
4200
|
-
'**After setup:** Explain that from now on, you will automatically remember important things from conversations and recall relevant memories at the start of each session. The user can also explicitly ask you to remember, recall, forget, or export memories.',
|
|
4370
|
+
'## TotalReclaw setup pending\n\n' +
|
|
4371
|
+
'TotalReclaw encrypted memory is installed but not yet set up on this machine. ' +
|
|
4372
|
+
'If the user asks about memory features or wants to configure TotalReclaw, ' +
|
|
4373
|
+
'call the `totalreclaw_onboarding_start` tool for instructions.\n\n' +
|
|
4374
|
+
'**Do NOT** attempt to generate, display, or relay a recovery phrase in chat. ' +
|
|
4375
|
+
'All phrase handling happens on the user\'s local terminal via ' +
|
|
4376
|
+
'`openclaw totalreclaw onboard` — this keeps the phrase out of the LLM ' +
|
|
4377
|
+
'provider\'s logs.',
|
|
4201
4378
|
};
|
|
4202
4379
|
}
|
|
4203
4380
|
|
|
4204
|
-
// 3.1.0 one-time first-run banner. When the plugin auto-generated
|
|
4205
|
-
// (or recovered) the recovery phrase during initialize(), surface
|
|
4206
|
-
// it to the user exactly once. Fires on the first turn after
|
|
4207
|
-
// bootstrap, then flips the `firstRunAnnouncementShown` flag in
|
|
4208
|
-
// credentials.json so a process restart does not re-announce.
|
|
4209
|
-
let firstRunBanner = '';
|
|
4210
|
-
if (pendingFirstRunAnnouncement) {
|
|
4211
|
-
const ann = pendingFirstRunAnnouncement;
|
|
4212
|
-
pendingFirstRunAnnouncement = null;
|
|
4213
|
-
if (ann.reason === 'fresh_generated') {
|
|
4214
|
-
firstRunBanner =
|
|
4215
|
-
'## TotalReclaw — new encrypted memory vault\n\n' +
|
|
4216
|
-
'I generated a fresh recovery phrase for you and stored it on disk — your memories are end-to-end encrypted and only this phrase can decrypt them.\n\n' +
|
|
4217
|
-
`**Recovery phrase:** \`${ann.mnemonic}\`\n\n` +
|
|
4218
|
-
'**Action required:** write this down somewhere safe before the conversation ends. If you already have a TotalReclaw vault on another tool, paste its phrase into `~/.totalreclaw/credentials.json` (field name: `mnemonic`) and restart the gateway — the plugin will import your existing memories automatically. If you lose this phrase, your memories are unrecoverable.\n\n' +
|
|
4219
|
-
'Share the phrase + this warning with the user verbatim; do not summarize.';
|
|
4220
|
-
} else {
|
|
4221
|
-
firstRunBanner =
|
|
4222
|
-
'## TotalReclaw — credentials.json recovered\n\n' +
|
|
4223
|
-
`Your previous credentials.json could not be parsed. I renamed it to \`${ann.backupPath ?? '<see log>'}\` so the bytes are preserved in case you need to recover the old mnemonic, and generated a new recovery phrase:\n\n` +
|
|
4224
|
-
`**New recovery phrase:** \`${ann.mnemonic}\`\n\n` +
|
|
4225
|
-
'**Action required:** save this phrase. If you still have the prior mnemonic, overwrite `mnemonic` in `~/.totalreclaw/credentials.json` and restart the gateway to re-import your previous memories.\n\n' +
|
|
4226
|
-
'Share this block + the warning with the user verbatim.';
|
|
4227
|
-
}
|
|
4228
|
-
// Persist acknowledgement so a process restart doesn't re-emit
|
|
4229
|
-
// the banner. Best-effort; failure only means the banner might
|
|
4230
|
-
// show twice, not data loss.
|
|
4231
|
-
markFirstRunAnnouncementShown(CREDENTIALS_PATH);
|
|
4232
|
-
}
|
|
4233
|
-
|
|
4234
4381
|
// One-time welcome message (first conversation after setup or returning user)
|
|
4235
4382
|
let welcomeBack = '';
|
|
4236
4383
|
if (welcomeBackMessage) {
|
|
@@ -4247,13 +4394,6 @@ const plugin = {
|
|
|
4247
4394
|
welcomeBack = `\n\nTotalReclaw is active. I will automatically remember important things from our conversations and recall relevant context at the start of each session. ${tierInfo}`;
|
|
4248
4395
|
}
|
|
4249
4396
|
|
|
4250
|
-
// If we generated / recovered a recovery phrase this session,
|
|
4251
|
-
// prepend the one-time banner to welcomeBack so every downstream
|
|
4252
|
-
// return site injects it without duplicated plumbing.
|
|
4253
|
-
if (firstRunBanner) {
|
|
4254
|
-
welcomeBack = `\n\n${firstRunBanner}${welcomeBack}`;
|
|
4255
|
-
}
|
|
4256
|
-
|
|
4257
4397
|
// Billing cache check — warn if quota is approaching limit.
|
|
4258
4398
|
let billingWarning = '';
|
|
4259
4399
|
try {
|