@totalreclaw/totalreclaw 3.3.12-rc.2 → 3.3.12-rc.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/SKILL.md +34 -249
- package/api-client.ts +17 -9
- package/batch-gate.ts +42 -0
- package/billing-cache.ts +25 -20
- package/claims-helper.ts +7 -1
- package/config.ts +54 -12
- package/consolidation.ts +6 -13
- package/contradiction-sync.ts +19 -14
- package/credential-provider.ts +184 -0
- package/crypto.ts +27 -160
- package/dist/api-client.js +17 -9
- package/dist/batch-gate.js +40 -0
- package/dist/billing-cache.js +19 -21
- package/dist/claims-helper.js +7 -1
- package/dist/config.js +54 -12
- package/dist/consolidation.js +6 -15
- package/dist/contradiction-sync.js +15 -15
- package/dist/credential-provider.js +145 -0
- package/dist/crypto.js +17 -137
- package/dist/download-ux.js +11 -7
- package/dist/embedder-loader.js +266 -0
- package/dist/embedding.js +36 -3
- package/dist/entry.js +123 -0
- package/dist/extractor.js +134 -0
- package/dist/fs-helpers.js +116 -241
- package/dist/import-adapters/chatgpt-adapter.js +14 -0
- package/dist/import-adapters/claude-adapter.js +14 -0
- package/dist/import-adapters/gemini-adapter.js +43 -159
- package/dist/import-adapters/mcp-memory-adapter.js +14 -0
- package/dist/import-state-manager.js +100 -0
- package/dist/index.js +1130 -2520
- package/dist/llm-client.js +69 -1
- package/dist/memory-runtime.js +459 -0
- package/dist/native-memory.js +123 -0
- package/dist/onboarding-cli.js +3 -2
- package/dist/pair-cli.js +1 -1
- package/dist/pair-crypto.js +16 -358
- package/dist/pair-http.js +147 -4
- package/dist/relay.js +140 -0
- package/dist/reranker.js +13 -8
- package/dist/semantic-dedup.js +5 -7
- package/dist/skill-register.js +97 -0
- package/dist/subgraph-search.js +3 -1
- package/dist/subgraph-store.js +315 -282
- package/dist/tool-gating.js +39 -26
- package/dist/tools.js +367 -0
- package/dist/tr-cli-export-helper.js +103 -0
- package/dist/tr-cli.js +220 -127
- package/dist/trajectory-poller.js +155 -9
- package/dist/vault-crypto.js +551 -0
- package/download-ux.ts +12 -6
- package/embedder-loader.ts +293 -1
- package/embedding.ts +43 -3
- package/entry.ts +132 -0
- package/extractor.ts +167 -0
- package/fs-helpers.ts +166 -292
- package/import-adapters/chatgpt-adapter.ts +18 -0
- package/import-adapters/claude-adapter.ts +18 -0
- package/import-adapters/gemini-adapter.ts +56 -183
- package/import-adapters/mcp-memory-adapter.ts +18 -0
- package/import-adapters/types.ts +1 -1
- package/import-state-manager.ts +139 -0
- package/index.ts +1432 -3002
- package/llm-client.ts +74 -1
- package/memory-runtime.ts +723 -0
- package/native-memory.ts +196 -0
- package/onboarding-cli.ts +3 -2
- package/openclaw.plugin.json +5 -17
- package/package.json +7 -4
- package/pair-cli.ts +1 -1
- package/pair-crypto.ts +41 -483
- package/pair-http.ts +194 -5
- package/postinstall.mjs +138 -0
- package/relay.ts +172 -0
- package/reranker.ts +13 -8
- package/semantic-dedup.ts +5 -6
- package/skill-register.ts +146 -0
- package/skill.json +1 -1
- package/subgraph-search.ts +3 -1
- package/subgraph-store.ts +334 -299
- package/tool-gating.ts +39 -26
- package/tools.ts +499 -0
- package/tr-cli-export-helper.ts +138 -0
- package/tr-cli.ts +263 -133
- package/trajectory-poller.ts +162 -10
- package/vault-crypto.ts +705 -0
package/fs-helpers.ts
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
import fs from 'node:fs';
|
|
31
31
|
import path from 'node:path';
|
|
32
|
+
import { envHomeDir } from './entry.js';
|
|
32
33
|
|
|
33
34
|
// ---------------------------------------------------------------------------
|
|
34
35
|
// Types
|
|
@@ -132,8 +133,7 @@ export function ensureMemoryHeaderFile(
|
|
|
132
133
|
* covers the OpenClaw plugin sandbox case where the loaded module
|
|
133
134
|
* lives at `<pluginRoot>/dist/index.js` while `package.json` lives
|
|
134
135
|
* at `<pluginRoot>/package.json` (3.3.4-rc.1 fix — without this
|
|
135
|
-
* walk-up,
|
|
136
|
-
* all RC-gated logic that depends on the version string fails
|
|
136
|
+
* walk-up, version-dependent logic gets `version=unknown` and fails
|
|
137
137
|
* silently in production OpenClaw deployments).
|
|
138
138
|
* - Returns the `version` field, or `null` on any I/O / parse error.
|
|
139
139
|
*
|
|
@@ -544,208 +544,15 @@ export function wipePartialInstall(pluginRootDir: string): boolean {
|
|
|
544
544
|
}
|
|
545
545
|
|
|
546
546
|
// ---------------------------------------------------------------------------
|
|
547
|
-
// Plugin load manifest (.loaded.json / .error.json) — 3.
|
|
547
|
+
// Plugin load manifest (.loaded.json / .error.json) — RETIRED Phase 3.4
|
|
548
548
|
// ---------------------------------------------------------------------------
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
* Acceptance criteria (issue #186):
|
|
557
|
-
* - `cat ~/.openclaw/extensions/totalreclaw/.loaded.json` shows
|
|
558
|
-
* `{loadedAt, tools, version}` after every successful gateway start.
|
|
559
|
-
* - `cat ~/.openclaw/extensions/totalreclaw/.error.json` shows
|
|
560
|
-
* `{loadedAt, error, stack}` if register() threw.
|
|
561
|
-
* - Both are overwritten on each register() call so the agent can rely
|
|
562
|
-
* on the timestamp matching the most recent gateway start.
|
|
563
|
-
*
|
|
564
|
-
* Why these MUST be synchronous writes (same constraint as
|
|
565
|
-
* `registerHttpRoute` per the comment in index.ts around the route
|
|
566
|
-
* registration site): the SDK loader treats register() returning as the
|
|
567
|
-
* signal to freeze the registries. An async `fs.promises.writeFile` would
|
|
568
|
-
* settle one microtask AFTER the loader has moved on, so the manifest
|
|
569
|
-
* could miss tools that registered late OR drop entirely if the gateway
|
|
570
|
-
* exits before the microtask runs. `writeFileSync` is the only safe choice.
|
|
571
|
-
*/
|
|
572
|
-
export const PLUGIN_LOADED_MANIFEST = '.loaded.json';
|
|
573
|
-
export const PLUGIN_ERROR_MANIFEST = '.error.json';
|
|
574
|
-
|
|
575
|
-
/** Schema written to `.loaded.json` — see PLUGIN_LOADED_MANIFEST. */
|
|
576
|
-
export interface PluginLoadManifest {
|
|
577
|
-
/** Unix epoch milliseconds when register() finished. */
|
|
578
|
-
loadedAt: number;
|
|
579
|
-
/** Tool names passed to api.registerTool() during register(). */
|
|
580
|
-
tools: string[];
|
|
581
|
-
/** Plugin version string from package.json (or "unknown"). */
|
|
582
|
-
version: string;
|
|
583
|
-
/**
|
|
584
|
-
* 3.3.7-rc.1 (issue #216) — incremented every time register() completes.
|
|
585
|
-
* Container restart that successfully re-runs register() should bump this.
|
|
586
|
-
* If a user reports tools missing after-a-restart but bootCount is still N,
|
|
587
|
-
* the plugin's register() was NOT called on boot (separate root cause
|
|
588
|
-
* from "register ran but tools didn't bind to active session").
|
|
589
|
-
*/
|
|
590
|
-
bootCount?: number;
|
|
591
|
-
/** ISO timestamp of the most recent boot — easier to read than `loadedAt`. */
|
|
592
|
-
bootAt?: string;
|
|
593
|
-
/** PID of the gateway process that wrote this manifest. Lets the user
|
|
594
|
-
* verify the manifest is from the currently-running container vs a
|
|
595
|
-
* stale-mounted copy. */
|
|
596
|
-
pid?: number;
|
|
597
|
-
/**
|
|
598
|
-
* 3.3.8-rc.1 — true when registerTool() calls are no-op'd due to the
|
|
599
|
-
* OC 2026.5.2 issue #223 hybrid workaround. Tools in `tools[]` are
|
|
600
|
-
* exposed via the `tr` CLI binary instead of via the plugin API.
|
|
601
|
-
*/
|
|
602
|
-
hybridMode?: boolean;
|
|
603
|
-
/**
|
|
604
|
-
* 3.3.8-rc.1 — CLI commands that replace the tool registrations
|
|
605
|
-
* when hybridMode=true. Agent runs these from shell instead of using
|
|
606
|
-
* tool calls.
|
|
607
|
-
*/
|
|
608
|
-
hybridCliTools?: string[];
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
/** Schema written to `.error.json` when register() throws. */
|
|
612
|
-
export interface PluginLoadError {
|
|
613
|
-
loadedAt: number;
|
|
614
|
-
error: string;
|
|
615
|
-
stack?: string;
|
|
616
|
-
version?: string;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
/**
|
|
620
|
-
* Resolve the plugin root dir from the loaded module's directory. The plugin
|
|
621
|
-
* is shipped with `dist/index.js` as the entry, so `import.meta.url` resolves
|
|
622
|
-
* to `<root>/dist/`. We walk up one level to put the manifests next to
|
|
623
|
-
* `package.json`. Defensive: if the caller already passed the root (no
|
|
624
|
-
* trailing `dist`), we still return a sensible path.
|
|
625
|
-
*/
|
|
626
|
-
function resolvePluginRootForManifest(pluginDir: string): string {
|
|
627
|
-
const base = path.basename(pluginDir);
|
|
628
|
-
return base === 'dist' ? path.resolve(pluginDir, '..') : pluginDir;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
/**
|
|
632
|
-
* Write the success manifest. SYNCHRONOUS — the SDK freezes the plugin
|
|
633
|
-
* registries the moment register() returns; `fs.promises.writeFile` would
|
|
634
|
-
* race that freeze and the manifest could miss late tool registrations or
|
|
635
|
-
* never land at all if the process exits before the microtask runs.
|
|
636
|
-
*
|
|
637
|
-
* Best-effort: returns `true` on success, `false` on any I/O error. Never
|
|
638
|
-
* throws — failing to write the manifest is a diagnostic loss, not a
|
|
639
|
-
* correctness loss, so we don't propagate.
|
|
640
|
-
*
|
|
641
|
-
* The manifest is written at mode 0644 (world-readable). It contains no
|
|
642
|
-
* secrets — only a timestamp, the (publicly known) tool names, and the
|
|
643
|
-
* plugin version. Cleared first so a stale `.error.json` from a previous
|
|
644
|
-
* failed boot doesn't survive a successful boot.
|
|
645
|
-
*/
|
|
646
|
-
/**
|
|
647
|
-
* Read the existing `.loaded.json` manifest for diagnostic surfaces
|
|
648
|
-
* (3.3.7-rc.1 — issue #216). Returns `null` if the manifest is
|
|
649
|
-
* missing, unreadable, or malformed. Best-effort: never throws.
|
|
650
|
-
*
|
|
651
|
-
* Scanner note: this helper lives in fs-helpers.ts (where all fs.*
|
|
652
|
-
* operations are consolidated) so the diagnostic slash command in
|
|
653
|
-
* `index.ts` doesn't have to introduce a fresh `readFileSync` call —
|
|
654
|
-
* the OpenClaw scanner whole-file rule disallows fs.read* next to the
|
|
655
|
-
* outbound-request trigger markers that index.ts already has in its
|
|
656
|
-
* on-chain submission code paths.
|
|
657
|
-
*/
|
|
658
|
-
export function readPluginLoadedManifest(
|
|
659
|
-
pluginDir: string,
|
|
660
|
-
): PluginLoadManifest | null {
|
|
661
|
-
try {
|
|
662
|
-
const root = resolvePluginRootForManifest(pluginDir);
|
|
663
|
-
const loadedPath = path.join(root, PLUGIN_LOADED_MANIFEST);
|
|
664
|
-
if (!fs.existsSync(loadedPath)) return null;
|
|
665
|
-
const raw = fs.readFileSync(loadedPath, 'utf-8');
|
|
666
|
-
const parsed = JSON.parse(raw) as Partial<PluginLoadManifest>;
|
|
667
|
-
if (typeof parsed.loadedAt !== 'number' || !Array.isArray(parsed.tools) || typeof parsed.version !== 'string') {
|
|
668
|
-
return null;
|
|
669
|
-
}
|
|
670
|
-
return parsed as PluginLoadManifest;
|
|
671
|
-
} catch {
|
|
672
|
-
return null;
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
export function writePluginManifest(
|
|
677
|
-
pluginDir: string,
|
|
678
|
-
manifest: PluginLoadManifest,
|
|
679
|
-
): boolean {
|
|
680
|
-
try {
|
|
681
|
-
const root = resolvePluginRootForManifest(pluginDir);
|
|
682
|
-
if (!fs.existsSync(root)) return false;
|
|
683
|
-
const loadedPath = path.join(root, PLUGIN_LOADED_MANIFEST);
|
|
684
|
-
const errorPath = path.join(root, PLUGIN_ERROR_MANIFEST);
|
|
685
|
-
// Best-effort error-file cleanup — a successful boot supersedes any
|
|
686
|
-
// prior failure marker. If the unlink fails (e.g. permission), the
|
|
687
|
-
// .loaded.json timestamp still tells the agent which is current.
|
|
688
|
-
try {
|
|
689
|
-
if (fs.existsSync(errorPath)) fs.unlinkSync(errorPath);
|
|
690
|
-
} catch {
|
|
691
|
-
// Swallow — best-effort.
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
// 3.3.7-rc.1 (issue #216) — derive bootCount by reading the prior
|
|
695
|
-
// manifest. Lets the user grep `.loaded.json` after a container
|
|
696
|
-
// restart to verify register() actually ran. If the prior manifest
|
|
697
|
-
// is unreadable we start at 1.
|
|
698
|
-
let priorBootCount = 0;
|
|
699
|
-
try {
|
|
700
|
-
if (fs.existsSync(loadedPath)) {
|
|
701
|
-
const prior = JSON.parse(fs.readFileSync(loadedPath, 'utf-8')) as Partial<PluginLoadManifest>;
|
|
702
|
-
if (typeof prior.bootCount === 'number' && Number.isFinite(prior.bootCount)) {
|
|
703
|
-
priorBootCount = prior.bootCount;
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
} catch {
|
|
707
|
-
// Swallow — if the prior manifest is corrupt we just start the counter fresh.
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
const enriched: PluginLoadManifest = {
|
|
711
|
-
...manifest,
|
|
712
|
-
bootCount: priorBootCount + 1,
|
|
713
|
-
bootAt: new Date(manifest.loadedAt).toISOString(),
|
|
714
|
-
pid: process.pid,
|
|
715
|
-
};
|
|
716
|
-
|
|
717
|
-
fs.writeFileSync(loadedPath, JSON.stringify(enriched, null, 2));
|
|
718
|
-
return true;
|
|
719
|
-
} catch {
|
|
720
|
-
return false;
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
/**
|
|
725
|
-
* Write the error manifest. SYNCHRONOUS for the same reason as
|
|
726
|
-
* `writePluginManifest`. Called from the try/catch surrounding the
|
|
727
|
-
* register() body so the agent has a filesystem signal that the plugin
|
|
728
|
-
* registered AT LEAST attempted to load and failed in a specific way.
|
|
729
|
-
*
|
|
730
|
-
* Does NOT clear `.loaded.json` from a prior successful boot — keeping
|
|
731
|
-
* the older success marker around lets the agent see "last good boot was
|
|
732
|
-
* X, current boot failed at Y" without spelunking logs. The newer
|
|
733
|
-
* `.error.json` timestamp wins as "current state".
|
|
734
|
-
*/
|
|
735
|
-
export function writePluginError(
|
|
736
|
-
pluginDir: string,
|
|
737
|
-
error: PluginLoadError,
|
|
738
|
-
): boolean {
|
|
739
|
-
try {
|
|
740
|
-
const root = resolvePluginRootForManifest(pluginDir);
|
|
741
|
-
if (!fs.existsSync(root)) return false;
|
|
742
|
-
const errorPath = path.join(root, PLUGIN_ERROR_MANIFEST);
|
|
743
|
-
fs.writeFileSync(errorPath, JSON.stringify(error, null, 2));
|
|
744
|
-
return true;
|
|
745
|
-
} catch {
|
|
746
|
-
return false;
|
|
747
|
-
}
|
|
748
|
-
}
|
|
549
|
+
// The 3.3.2-rc.1 `.loaded.json` / `.error.json` manifest machinery (writer
|
|
550
|
+
// + reader + schema) was fully removed in Phase 3.4. Phase 3.1 already
|
|
551
|
+
// dropped the `writePluginManifest` call from register(); 3.4 retires the
|
|
552
|
+
// remaining reader (`readPluginLoadedManifest`) + the error-path writer
|
|
553
|
+
// (`writePluginError`) + the schema types now that no production path
|
|
554
|
+
// references them. The gateway log is the source of truth for register()
|
|
555
|
+
// failures; `/totalreclaw diag` reports pid + in-memory version only.
|
|
749
556
|
|
|
750
557
|
/**
|
|
751
558
|
* Drop the `.tr-partial-install` marker into `pluginRootDir`. Idempotent
|
|
@@ -1130,6 +937,24 @@ export function resolveOnboardingState(
|
|
|
1130
937
|
// ---------------------------------------------------------------------------
|
|
1131
938
|
// OpenClaw 2026.5.x config auto-patch (3.3.9-rc.2 — issues #225 + #226)
|
|
1132
939
|
// ---------------------------------------------------------------------------
|
|
940
|
+
//
|
|
941
|
+
// Phase 3.4 note (2026-06-22): this is the minimal idempotent slot-ensure
|
|
942
|
+
// that survives the config-patch state-machine retirement. The auto-pair-
|
|
943
|
+
// on-load + .pair-pending.json + SIGUSR1-for-pair dance was retired because
|
|
944
|
+
// pairing is now user-initiated QR. `patchOpenClawConfig` itself was NEVER
|
|
945
|
+
// part of the auto-pair state machine — it applies OpenClaw 2026.5.x
|
|
946
|
+
// compatibility keys (hook access, telegram streaming) and is idempotent
|
|
947
|
+
// (returns `'unchanged'` when all keys are already correct). Its only caller
|
|
948
|
+
// is register() in index.ts; the SIGUSR1 emitted on `'patched'` is the
|
|
949
|
+
// "restart-so-the-new-keys-take-effect-this-boot" signal, NOT a pair signal.
|
|
950
|
+
//
|
|
951
|
+
// Retired in rc.20 (#402): the memory-slot write (formerly Fix #1), the
|
|
952
|
+
// install-record self-heal (formerly Fix #6), and the plugins.allow self-append
|
|
953
|
+
// + bundledDiscovery="compat" pair (formerly Fix #5 + Fix #4). OpenClaw 2026.6.8
|
|
954
|
+
// claims the memory slot natively on `plugins install`/`enable`, manages the
|
|
955
|
+
// allowlist itself, and discards the whole installs map if any entry fails
|
|
956
|
+
// schema validation — so this helper no longer touches `plugins.slots.memory`,
|
|
957
|
+
// `plugins.installs`, `plugins.allow`, or `plugins.bundledDiscovery`.
|
|
1133
958
|
|
|
1134
959
|
/**
|
|
1135
960
|
* Outcome of `patchOpenClawConfig`.
|
|
@@ -1149,14 +974,11 @@ export type OpenClawConfigPatchResult = 'patched' | 'unchanged' | 'skipped' | 'e
|
|
|
1149
974
|
* Auto-patch `~/.openclaw/openclaw.json` with the entries required by
|
|
1150
975
|
* OpenClaw 2026.5.x for clean operation (issues #225 + #226 + verbosity):
|
|
1151
976
|
*
|
|
1152
|
-
*
|
|
1153
|
-
*
|
|
1154
|
-
*
|
|
1155
|
-
*
|
|
1156
|
-
*
|
|
1157
|
-
* crash loop ("plugins.slots.memory: plugin not found: totalreclaw")
|
|
1158
|
-
* that survives container restarts until `openclaw plugins install`
|
|
1159
|
-
* repopulates the install record.
|
|
977
|
+
* NOTE (rc.20, #402): this helper no longer writes `plugins.slots.memory`
|
|
978
|
+
* (OpenClaw 2026.6.8 claims the memory slot natively on install/enable) nor
|
|
979
|
+
* `plugins.installs` (the former Fix #6 self-heal — its record reader
|
|
980
|
+
* discards the whole installs map if any entry fails schema validation).
|
|
981
|
+
* Pre-existing values of both are left untouched. The remaining fixes are:
|
|
1160
982
|
*
|
|
1161
983
|
* 2. `plugins.entries.totalreclaw.hooks.allowConversationAccess = true`
|
|
1162
984
|
* Grant the plugin access to `agent_end` and `before_agent_start`
|
|
@@ -1170,15 +992,11 @@ export type OpenClawConfigPatchResult = 'patched' | 'unchanged' | 'skipped' | 'e
|
|
|
1170
992
|
* this to "off" on first run for a clean UX. Existing explicit values
|
|
1171
993
|
* ("partial", "block", "progress") are preserved.
|
|
1172
994
|
*
|
|
1173
|
-
* 4
|
|
1174
|
-
*
|
|
1175
|
-
*
|
|
1176
|
-
*
|
|
1177
|
-
*
|
|
1178
|
-
* `bundledDiscovery: "compat"` restores the looser behavior so allow-
|
|
1179
|
-
* listed non-bundled plugins load. Without this fix, users with
|
|
1180
|
-
* telegram or any model provider configured before TR install (which
|
|
1181
|
-
* populates allow) get a silent plugin-skip on every gateway boot.
|
|
995
|
+
* Fix #4 (`plugins.bundledDiscovery="compat"`) and Fix #5 (appending
|
|
996
|
+
* "totalreclaw" to `plugins.allow`) were RETIRED together in rc.20 (#402):
|
|
997
|
+
* native install manages the allowlist, and TR growing it was the only
|
|
998
|
+
* thing that forced strict-allowlist mode (the sole reason the compat
|
|
999
|
+
* workaround existed). Pre-existing values of both keys are left untouched.
|
|
1182
1000
|
*
|
|
1183
1001
|
* Design constraints
|
|
1184
1002
|
* ------------------
|
|
@@ -1202,8 +1020,11 @@ export type OpenClawConfigPatchResult = 'patched' | 'unchanged' | 'skipped' | 'e
|
|
|
1202
1020
|
*/
|
|
1203
1021
|
export function patchOpenClawConfig(
|
|
1204
1022
|
configPath?: string,
|
|
1023
|
+
// Retained on the signature (register() passes it) but no longer consumed:
|
|
1024
|
+
// the Fix #6 install-record self-heal it fed was retired in rc.20 (#402).
|
|
1025
|
+
pluginVersion?: string,
|
|
1205
1026
|
): OpenClawConfigPatchResult {
|
|
1206
|
-
const home =
|
|
1027
|
+
const home = envHomeDir();
|
|
1207
1028
|
const target = configPath ?? path.join(home, '.openclaw', 'openclaw.json');
|
|
1208
1029
|
|
|
1209
1030
|
// `'skipped'` when the config file is absent — this host may not be
|
|
@@ -1222,51 +1043,46 @@ export function patchOpenClawConfig(
|
|
|
1222
1043
|
|
|
1223
1044
|
let mutated = false;
|
|
1224
1045
|
|
|
1225
|
-
// --- Fix #
|
|
1226
|
-
//
|
|
1227
|
-
// DEFENSIVE GATE (3.3.9-rc.4 — 2026-05-05): only write the slot when
|
|
1228
|
-
// the plugin is genuinely INSTALLED (`plugins.installs.totalreclaw`
|
|
1229
|
-
// present with a `version`). Writing the slot unconditionally
|
|
1230
|
-
// produced a startup crash loop on Pedro's pop-os QA host on
|
|
1231
|
-
// 2026-05-05 — after a config reset, `plugins.installs.totalreclaw`
|
|
1232
|
-
// was missing but a previously-written `slots.memory = "totalreclaw"`
|
|
1233
|
-
// had survived. OpenClaw's startup validator refuses to start with
|
|
1046
|
+
// --- Fix #6 (installs self-heal) RETIRED in rc.20 (#402) ---
|
|
1234
1047
|
//
|
|
1235
|
-
//
|
|
1236
|
-
//
|
|
1237
|
-
//
|
|
1048
|
+
// patchOpenClawConfig used to fabricate a `plugins.installs.totalreclaw`
|
|
1049
|
+
// record (version/spec/source:"self-heal"/installedAt) whenever a version
|
|
1050
|
+
// was passed and no record existed. Removed because it was inert at best
|
|
1051
|
+
// and harmful at worst on OpenClaw 2026.6.8:
|
|
1052
|
+
// (a) the record reader validates the WHOLE installs map against a
|
|
1053
|
+
// schema where `source` must be a valid PluginInstallSource — the
|
|
1054
|
+
// fabricated `source:"self-heal"` (and any source-less record) fails
|
|
1055
|
+
// safeParse, discarding the ENTIRE installs map at load; and
|
|
1056
|
+
// (b) the native installer never persists install records into
|
|
1057
|
+
// openclaw.json (it strips them via withoutPluginInstallRecords and
|
|
1058
|
+
// commits to the SQLite index, which always wins on merge).
|
|
1059
|
+
// So the plugin no longer writes `plugins.installs` at all; a pre-existing
|
|
1060
|
+
// record is left byte-identical. `pluginVersion` is retained on the
|
|
1061
|
+
// signature (register() passes it) but is no longer consumed here.
|
|
1062
|
+
void pluginVersion;
|
|
1063
|
+
|
|
1064
|
+
// --- Fix #5 (plugins.allow self-append) RETIRED in rc.20 (#402) ---
|
|
1238
1065
|
//
|
|
1239
|
-
//
|
|
1240
|
-
//
|
|
1241
|
-
// `
|
|
1242
|
-
//
|
|
1243
|
-
//
|
|
1244
|
-
//
|
|
1245
|
-
//
|
|
1246
|
-
//
|
|
1066
|
+
// patchOpenClawConfig used to append "totalreclaw" to a non-empty
|
|
1067
|
+
// `plugins.allow`. Removed as a pair with Fix #4 (below): OpenClaw's native
|
|
1068
|
+
// `persistPluginInstall` already calls `addInstalledPluginToAllowlist`, and
|
|
1069
|
+
// an empty/absent allowlist means allow-all (no write needed). TR
|
|
1070
|
+
// unconditionally growing an allowlist was the very thing that flipped
|
|
1071
|
+
// hosts into strict-allowlist enforcement — which then required the Fix #4
|
|
1072
|
+
// `bundledDiscovery:"compat"` workaround. Dropping both lets native install
|
|
1073
|
+
// own the allowlist; existing hosts keep whatever is already on disk.
|
|
1074
|
+
|
|
1075
|
+
// --- Fix #1 (memory-slot write) RETIRED in rc.20 (#402) ---
|
|
1247
1076
|
//
|
|
1248
|
-
//
|
|
1249
|
-
//
|
|
1250
|
-
//
|
|
1251
|
-
//
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
&& installEntry !== null
|
|
1258
|
-
&& typeof installEntry.version === 'string'
|
|
1259
|
-
&& installEntry.version.length > 0;
|
|
1260
|
-
|
|
1261
|
-
if (pluginIsInstalled) {
|
|
1262
|
-
if (typeof cfg.plugins.slots !== 'object' || cfg.plugins.slots === null) {
|
|
1263
|
-
cfg.plugins.slots = {};
|
|
1264
|
-
}
|
|
1265
|
-
if (cfg.plugins.slots.memory !== 'totalreclaw') {
|
|
1266
|
-
cfg.plugins.slots.memory = 'totalreclaw';
|
|
1267
|
-
mutated = true;
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1077
|
+
// patchOpenClawConfig used to write `plugins.slots.memory = "totalreclaw"`
|
|
1078
|
+
// (install-gated, to dodge a startup crash loop). OpenClaw 2026.6.8 now
|
|
1079
|
+
// claims the memory slot NATIVELY during `plugins install`/`enable` (its
|
|
1080
|
+
// persistPluginInstall "slot selection" phase), so the hand-written write
|
|
1081
|
+
// was redundant on the good path and twice failed in production. The
|
|
1082
|
+
// plugin no longer touches the memory slot — a pre-existing value is left
|
|
1083
|
+
// byte-identical. Fix numbering below is preserved for continuity with the
|
|
1084
|
+
// #225/#226 history. The Fix #6 install-record self-heal was retired
|
|
1085
|
+
// above too (rc.20, #402).
|
|
1270
1086
|
|
|
1271
1087
|
// --- Fix #2: plugins.entries.totalreclaw.hooks.allowConversationAccess = true ---
|
|
1272
1088
|
if (typeof cfg.plugins.entries !== 'object' || cfg.plugins.entries === null) {
|
|
@@ -1311,36 +1127,15 @@ export function patchOpenClawConfig(
|
|
|
1311
1127
|
}
|
|
1312
1128
|
}
|
|
1313
1129
|
|
|
1314
|
-
// --- Fix #4
|
|
1130
|
+
// --- Fix #4 (plugins.bundledDiscovery="compat") RETIRED in rc.20 (#402) ---
|
|
1315
1131
|
//
|
|
1316
|
-
//
|
|
1317
|
-
//
|
|
1318
|
-
//
|
|
1319
|
-
//
|
|
1320
|
-
//
|
|
1321
|
-
//
|
|
1322
|
-
//
|
|
1323
|
-
// despite it being in the allow list. `openclaw doctor --fix` cures it
|
|
1324
|
-
// by setting `bundledDiscovery: "compat"`, but users shouldn't need
|
|
1325
|
-
// to run doctor manually for the plugin to load.
|
|
1326
|
-
//
|
|
1327
|
-
// Fix: when `plugins.allow` is a non-empty array AND
|
|
1328
|
-
// `plugins.bundledDiscovery` is unset, set it to "compat". If the user
|
|
1329
|
-
// explicitly chose "allowlist" (the stricter mode), preserve their
|
|
1330
|
-
// choice — only first-run defaults are touched.
|
|
1331
|
-
//
|
|
1332
|
-
// This bug was missed by the auto-QA harness because the harness ran
|
|
1333
|
-
// on a fresh canonical container with `plugins.allow=null`, hitting
|
|
1334
|
-
// the auto-discover code path. Real users with telegram + a model
|
|
1335
|
-
// provider configured before TR install have a populated allow list,
|
|
1336
|
-
// hitting the strict-mode path. The auto-QA harness in 3.3.11-rc.4
|
|
1337
|
-
// adds a populated-allow scenario to catch future regressions.
|
|
1338
|
-
if (Array.isArray(cfg.plugins.allow) && cfg.plugins.allow.length > 0) {
|
|
1339
|
-
if (cfg.plugins.bundledDiscovery === undefined) {
|
|
1340
|
-
cfg.plugins.bundledDiscovery = 'compat';
|
|
1341
|
-
mutated = true;
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1132
|
+
// patchOpenClawConfig used to set `plugins.bundledDiscovery = "compat"`
|
|
1133
|
+
// whenever `plugins.allow` was populated, to keep non-bundled totalreclaw
|
|
1134
|
+
// loadable under strict-allowlist mode. Retired as a pair with Fix #5: the
|
|
1135
|
+
// ONLY reason a host was in strict-allowlist mode was that TR itself grew
|
|
1136
|
+
// the allowlist (Fix #5). With Fix #5 gone, native install manages the
|
|
1137
|
+
// allowlist and this compat workaround is unnecessary. An existing
|
|
1138
|
+
// `bundledDiscovery` value on disk is left untouched.
|
|
1344
1139
|
|
|
1345
1140
|
if (!mutated) return 'unchanged';
|
|
1346
1141
|
|
|
@@ -1351,3 +1146,82 @@ export function patchOpenClawConfig(
|
|
|
1351
1146
|
return 'error';
|
|
1352
1147
|
}
|
|
1353
1148
|
}
|
|
1149
|
+
|
|
1150
|
+
// ---------------------------------------------------------------------------
|
|
1151
|
+
// Credentials file permission check (cred-1 security hardening)
|
|
1152
|
+
// ---------------------------------------------------------------------------
|
|
1153
|
+
|
|
1154
|
+
/** Subset of the OpenClaw logger used by the permission check. */
|
|
1155
|
+
export interface PermissionCheckLogger {
|
|
1156
|
+
warn(msg: string): void;
|
|
1157
|
+
error(msg: string): void;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* Result of `checkCredentialsFileMode`.
|
|
1162
|
+
*
|
|
1163
|
+
* - `'ok'` — file absent (not yet created) or permissions are exactly 0600.
|
|
1164
|
+
* - `'insecure'` — file present with mode broader than 0600; caller must refuse to continue.
|
|
1165
|
+
* - `'warned'` — file is 0600 but lives on a tmpfs / shared-volume path; logged.
|
|
1166
|
+
* - `'stat_error'` — `fs.statSync` failed for an unexpected reason; caller should warn + continue.
|
|
1167
|
+
*/
|
|
1168
|
+
export type CredentialsPermissionResult = 'ok' | 'insecure' | 'warned' | 'stat_error';
|
|
1169
|
+
|
|
1170
|
+
const TMPFS_PREFIXES = ['/tmp/', '/dev/shm/', '/run/', '/var/run/'];
|
|
1171
|
+
|
|
1172
|
+
export function checkCredentialsFileMode(
|
|
1173
|
+
credPath: string,
|
|
1174
|
+
logger: PermissionCheckLogger,
|
|
1175
|
+
): CredentialsPermissionResult {
|
|
1176
|
+
let stat: fs.Stats;
|
|
1177
|
+
try {
|
|
1178
|
+
stat = fs.statSync(credPath);
|
|
1179
|
+
} catch (err: unknown) {
|
|
1180
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
1181
|
+
if (code === 'ENOENT') return 'ok';
|
|
1182
|
+
logger.warn(`TotalReclaw: could not stat credentials file (${code ?? String(err)}); skipping permission check`);
|
|
1183
|
+
return 'stat_error';
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
const mode = stat.mode & 0o777;
|
|
1187
|
+
if (mode > 0o600) {
|
|
1188
|
+
const modeStr = '0' + mode.toString(8);
|
|
1189
|
+
logger.error(
|
|
1190
|
+
`TotalReclaw: STARTUP REFUSED — credentials file has insecure permissions (${modeStr}).\n` +
|
|
1191
|
+
` File: ${credPath}\n` +
|
|
1192
|
+
` Fix: chmod 600 "${credPath}"\n` +
|
|
1193
|
+
` Then restart your OpenClaw gateway.\n` +
|
|
1194
|
+
` Current mode ${modeStr} allows other OS users to read your recovery phrase.`,
|
|
1195
|
+
);
|
|
1196
|
+
return 'insecure';
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
try {
|
|
1200
|
+
const real = fs.realpathSync(credPath);
|
|
1201
|
+
for (const prefix of TMPFS_PREFIXES) {
|
|
1202
|
+
if (real.startsWith(prefix)) {
|
|
1203
|
+
logger.warn(
|
|
1204
|
+
`TotalReclaw: credentials file is on a volatile/shared path (${real}). ` +
|
|
1205
|
+
`It may be lost on reboot or be readable by other processes. ` +
|
|
1206
|
+
`Consider moving it to a persistent private directory and updating TOTALRECLAW_CREDENTIALS_PATH.`,
|
|
1207
|
+
);
|
|
1208
|
+
return 'warned';
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
} catch {
|
|
1212
|
+
// realpathSync can fail on unusual mounts — not actionable here
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
return 'ok';
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// ---------------------------------------------------------------------------
|
|
1219
|
+
// Pair-pending sentinel — RETIRED Phase 3.4
|
|
1220
|
+
// ---------------------------------------------------------------------------
|
|
1221
|
+
// The 3.3.13 `~/.totalreclaw/.pair-pending.json` sentinel + its utilities
|
|
1222
|
+
// (loadPairPendingFile / writePairPendingFile / deletePairPendingFile /
|
|
1223
|
+
// defaultPairPendingPath / PairPendingFile) were removed in Phase 3.4.
|
|
1224
|
+
// Pairing is now user-initiated QR (`tr pair` CLI / the pair HTTP route)
|
|
1225
|
+
// per the native-integration design — the auto-pair-on-load state machine
|
|
1226
|
+
// that wrote + consumed this sentinel is gone.
|
|
1227
|
+
|
|
@@ -59,6 +59,24 @@ export class ChatGPTAdapter extends BaseImportAdapter {
|
|
|
59
59
|
} else if (input.file_path) {
|
|
60
60
|
try {
|
|
61
61
|
const resolvedPath = input.file_path.replace(/^~/, os.homedir());
|
|
62
|
+
const fileStat = fs.statSync(resolvedPath);
|
|
63
|
+
const fileSizeMB = fileStat.size / (1024 * 1024);
|
|
64
|
+
if (fileSizeMB > 500) {
|
|
65
|
+
errors.push(
|
|
66
|
+
`File is too large to import: ${fileSizeMB.toFixed(1)}MB exceeds the 500MB cap. ` +
|
|
67
|
+
'Split the file into smaller chunks and import each separately.',
|
|
68
|
+
);
|
|
69
|
+
return { facts: [], chunks: [], totalMessages: 0, warnings, errors };
|
|
70
|
+
}
|
|
71
|
+
const freeMem = os.freemem();
|
|
72
|
+
if (freeMem < fileStat.size * 2) {
|
|
73
|
+
errors.push(
|
|
74
|
+
`Not enough free memory: ${(freeMem / (1024 * 1024)).toFixed(0)}MB available, ` +
|
|
75
|
+
`~${Math.ceil(fileStat.size * 2 / (1024 * 1024))}MB needed (2× file size). ` +
|
|
76
|
+
'Close other applications or split the file.',
|
|
77
|
+
);
|
|
78
|
+
return { facts: [], chunks: [], totalMessages: 0, warnings, errors };
|
|
79
|
+
}
|
|
62
80
|
content = fs.readFileSync(resolvedPath, 'utf-8');
|
|
63
81
|
} catch (e) {
|
|
64
82
|
errors.push(`Failed to read file: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
|
@@ -45,6 +45,24 @@ export class ClaudeAdapter extends BaseImportAdapter {
|
|
|
45
45
|
} else if (input.file_path) {
|
|
46
46
|
try {
|
|
47
47
|
const resolvedPath = input.file_path.replace(/^~/, os.homedir());
|
|
48
|
+
const fileStat = fs.statSync(resolvedPath);
|
|
49
|
+
const fileSizeMB = fileStat.size / (1024 * 1024);
|
|
50
|
+
if (fileSizeMB > 500) {
|
|
51
|
+
errors.push(
|
|
52
|
+
`File is too large to import: ${fileSizeMB.toFixed(1)}MB exceeds the 500MB cap. ` +
|
|
53
|
+
'Split the file into smaller chunks and import each separately.',
|
|
54
|
+
);
|
|
55
|
+
return { facts: [], chunks: [], totalMessages: 0, warnings, errors };
|
|
56
|
+
}
|
|
57
|
+
const freeMem = os.freemem();
|
|
58
|
+
if (freeMem < fileStat.size * 2) {
|
|
59
|
+
errors.push(
|
|
60
|
+
`Not enough free memory: ${(freeMem / (1024 * 1024)).toFixed(0)}MB available, ` +
|
|
61
|
+
`~${Math.ceil(fileStat.size * 2 / (1024 * 1024))}MB needed (2× file size). ` +
|
|
62
|
+
'Close other applications or split the file.',
|
|
63
|
+
);
|
|
64
|
+
return { facts: [], chunks: [], totalMessages: 0, warnings, errors };
|
|
65
|
+
}
|
|
48
66
|
content = fs.readFileSync(resolvedPath, 'utf-8');
|
|
49
67
|
} catch (e) {
|
|
50
68
|
errors.push(`Failed to read file: ${e instanceof Error ? e.message : 'Unknown error'}`);
|