@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/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
- * - totalreclaw_setup -- initialize with recovery phrase (no gateway restart needed)
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 a `before_agent_start` hook that automatically injects
26
- * relevant memories into the agent's context.
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
- autoBootstrapCredentials,
119
- markFirstRunAnnouncementShown,
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
- * When non-null, the before_agent_start hook emits a one-time banner
414
- * announcing the freshly-generated (or recovered-from-corrupt) recovery
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.1.0 auto-setup: if no env-var mnemonic is present, call
433
- * `autoBootstrapCredentials` to either reuse credentials.json or mint a
434
- * fresh BIP-39 mnemonic. The explicit `totalreclaw_setup` tool stays
435
- * available for users who want to restore from an existing phrase, but
436
- * it is no longer required on first run.
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
- // ---- 3.1.0 auto-bootstrap ----
443
- //
444
- // When the env var isn't set, probe credentials.json. If it has a
445
- // usable mnemonic, reuse it; otherwise generate a fresh one and
446
- // persist it atomically. The user sees a one-time banner on their
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
- try {
451
- const outcome: BootstrapOutcome = autoBootstrapCredentials(CREDENTIALS_PATH, {
452
- generateMnemonic: () => {
453
- // Inline the scure/bip39 import so fs-helpers.ts never pulls
454
- // in the crypto surface (keeps its scanner-sim footprint small).
455
- const { generateMnemonic } = require('@scure/bip39');
456
- const { wordlist } = require('@scure/bip39/wordlists/english.js');
457
- return generateMnemonic(wordlist, 128);
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('TOTALRECLAW_RECOVERY_PHRASE not set — setup required (see SKILL.md Post-Install Setup)');
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: 'Setup TotalReclaw',
4166
+ label: 'TotalReclaw setup (deprecated — redirect to CLI)',
4054
4167
  description:
4055
- 'Initialize TotalReclaw with a recovery phrase. Derives encryption keys and registers with the server. ' +
4056
- 'Use this during first-time setup instead of setting environment variables no gateway restart needed.',
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: 'Optional 12-word BIP-39 recovery phrase. If not provided, generates a new one automatically. For returning users, pass their existing phrase.',
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
- try {
4069
- const providedPhrase = params.recovery_phrase?.trim() || '';
4070
-
4071
- // 3.1.0 idempotency: if the plugin is already fully set up AND
4072
- // the caller provides either no phrase or a matching one, return
4073
- // a no-op confirmation instead of a forced re-init (which
4074
- // triggers a stale-credentials delete + fresh register round
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: 'TotalReclaw setup complete! Encryption keys derived, server registration confirmed. ' +
4150
- 'You can now use totalreclaw_remember, totalreclaw_recall, and all other tools immediately no restart needed.\n\n' +
4151
- (wasGenerated ? `Recovery phrase: ${mnemonic}\n\n` : '') +
4152
- 'From now on, I will automatically remember important things from our conversations and recall relevant context at the start of each session.',
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
- } catch (err: unknown) {
4156
- const message = err instanceof Error ? err.message : String(err);
4157
- api.logger.error(`totalreclaw_setup failed: ${message}`);
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: [{ type: 'text', text: `Setup failed: ${humanizeError(message)}` }],
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
- // If setup is still needed after init, inject guided onboarding
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 First-Time Setup\n\n' +
4192
- 'TotalReclaw is an end-to-end encrypted memory vault. It remembers things across conversations — preferences, decisions, project context — all encrypted so no one (not even the server) can read your memories.\n\n' +
4193
- '**To get started, ask the user:** "Do you have an existing TotalReclaw recovery phrase you\'d like to restore, or should I generate a new one?"\n\n' +
4194
- '**If new user:** Run `npx @totalreclaw/mcp-server setup` to generate a cryptographically valid recovery phrase. Do NOT generate the phrase yourself — LLMs cannot compute BIP-39 checksums. The command will output the phrase. Then call the `totalreclaw_setup` tool with the phrase. Display the phrase prominently and warn:\n' +
4195
- '- "This is your ONLY way to access your memories. Write it down and store it securely."\n' +
4196
- '- "NEVER use a recovery phrase from an existing crypto wallet keep this separate from any wallet that holds funds."\n' +
4197
- '- "If you lose this phrase, your memories are gone forever there is no recovery."\n\n' +
4198
- '**If returning user:** Ask them to provide their 12-word phrase, then call `totalreclaw_setup` with that phrase.\n\n' +
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 {