@totalreclaw/totalreclaw 3.3.12-rc.9 → 3.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +33 -27
- package/api-client.ts +17 -9
- package/batch-gate.ts +42 -0
- package/billing-cache.ts +72 -23
- package/claims-helper.ts +2 -1
- package/config.ts +95 -13
- 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 +54 -24
- package/dist/claims-helper.js +2 -1
- package/dist/config.js +91 -13
- 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/fs-helpers.js +75 -379
- package/dist/import-adapters/gemini-adapter.js +29 -159
- package/dist/index.js +864 -2631
- package/dist/memory-runtime.js +459 -0
- package/dist/native-memory.js +123 -0
- package/dist/onboarding-cli.js +4 -8
- package/dist/pair-cli-relay.js +1 -8
- 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 +348 -290
- package/dist/tool-gating.js +39 -26
- package/dist/tools.js +367 -0
- package/dist/tr-cli-export-helper.js +3 -3
- package/dist/tr-cli.js +65 -156
- 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/fs-helpers.ts +93 -458
- package/import-adapters/gemini-adapter.ts +38 -183
- package/index.ts +912 -2917
- package/memory-runtime.ts +723 -0
- package/native-memory.ts +196 -0
- package/onboarding-cli.ts +3 -9
- package/openclaw.plugin.json +5 -17
- package/package.json +6 -3
- package/pair-cli-relay.ts +0 -9
- 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 +367 -307
- package/tool-gating.ts +39 -26
- package/tools.ts +499 -0
- package/tr-cli-export-helper.ts +3 -3
- package/tr-cli.ts +69 -182
- package/trajectory-poller.ts +162 -10
- package/vault-crypto.ts +705 -0
- package/auto-pair-on-load.ts +0 -308
- package/dist/auto-pair-on-load.js +0 -197
- package/dist/pair-pending-injection.js +0 -125
- package/pair-pending-injection.ts +0 -205
package/dist/fs-helpers.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import fs from 'node:fs';
|
|
30
30
|
import path from 'node:path';
|
|
31
|
+
import { envHomeDir } from './entry.js';
|
|
31
32
|
// ---------------------------------------------------------------------------
|
|
32
33
|
// MEMORY.md header ensure
|
|
33
34
|
// ---------------------------------------------------------------------------
|
|
@@ -82,8 +83,7 @@ export function ensureMemoryHeaderFile(workspace, header, markerSubstring = 'Tot
|
|
|
82
83
|
* covers the OpenClaw plugin sandbox case where the loaded module
|
|
83
84
|
* lives at `<pluginRoot>/dist/index.js` while `package.json` lives
|
|
84
85
|
* at `<pluginRoot>/package.json` (3.3.4-rc.1 fix — without this
|
|
85
|
-
* walk-up,
|
|
86
|
-
* all RC-gated logic that depends on the version string fails
|
|
86
|
+
* walk-up, version-dependent logic gets `version=unknown` and fails
|
|
87
87
|
* silently in production OpenClaw deployments).
|
|
88
88
|
* - Returns the `version` field, or `null` on any I/O / parse error.
|
|
89
89
|
*
|
|
@@ -471,157 +471,15 @@ export function wipePartialInstall(pluginRootDir) {
|
|
|
471
471
|
}
|
|
472
472
|
}
|
|
473
473
|
// ---------------------------------------------------------------------------
|
|
474
|
-
// Plugin load manifest (.loaded.json / .error.json) — 3.
|
|
474
|
+
// Plugin load manifest (.loaded.json / .error.json) — RETIRED Phase 3.4
|
|
475
475
|
// ---------------------------------------------------------------------------
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
* - `cat ~/.openclaw/extensions/totalreclaw/.loaded.json` shows
|
|
484
|
-
* `{loadedAt, tools, version}` after every successful gateway start.
|
|
485
|
-
* - `cat ~/.openclaw/extensions/totalreclaw/.error.json` shows
|
|
486
|
-
* `{loadedAt, error, stack}` if register() threw.
|
|
487
|
-
* - Both are overwritten on each register() call so the agent can rely
|
|
488
|
-
* on the timestamp matching the most recent gateway start.
|
|
489
|
-
*
|
|
490
|
-
* Why these MUST be synchronous writes (same constraint as
|
|
491
|
-
* `registerHttpRoute` per the comment in index.ts around the route
|
|
492
|
-
* registration site): the SDK loader treats register() returning as the
|
|
493
|
-
* signal to freeze the registries. An async `fs.promises.writeFile` would
|
|
494
|
-
* settle one microtask AFTER the loader has moved on, so the manifest
|
|
495
|
-
* could miss tools that registered late OR drop entirely if the gateway
|
|
496
|
-
* exits before the microtask runs. `writeFileSync` is the only safe choice.
|
|
497
|
-
*/
|
|
498
|
-
export const PLUGIN_LOADED_MANIFEST = '.loaded.json';
|
|
499
|
-
export const PLUGIN_ERROR_MANIFEST = '.error.json';
|
|
500
|
-
/**
|
|
501
|
-
* Resolve the plugin root dir from the loaded module's directory. The plugin
|
|
502
|
-
* is shipped with `dist/index.js` as the entry, so `import.meta.url` resolves
|
|
503
|
-
* to `<root>/dist/`. We walk up one level to put the manifests next to
|
|
504
|
-
* `package.json`. Defensive: if the caller already passed the root (no
|
|
505
|
-
* trailing `dist`), we still return a sensible path.
|
|
506
|
-
*/
|
|
507
|
-
function resolvePluginRootForManifest(pluginDir) {
|
|
508
|
-
const base = path.basename(pluginDir);
|
|
509
|
-
return base === 'dist' ? path.resolve(pluginDir, '..') : pluginDir;
|
|
510
|
-
}
|
|
511
|
-
/**
|
|
512
|
-
* Write the success manifest. SYNCHRONOUS — the SDK freezes the plugin
|
|
513
|
-
* registries the moment register() returns; `fs.promises.writeFile` would
|
|
514
|
-
* race that freeze and the manifest could miss late tool registrations or
|
|
515
|
-
* never land at all if the process exits before the microtask runs.
|
|
516
|
-
*
|
|
517
|
-
* Best-effort: returns `true` on success, `false` on any I/O error. Never
|
|
518
|
-
* throws — failing to write the manifest is a diagnostic loss, not a
|
|
519
|
-
* correctness loss, so we don't propagate.
|
|
520
|
-
*
|
|
521
|
-
* The manifest is written at mode 0644 (world-readable). It contains no
|
|
522
|
-
* secrets — only a timestamp, the (publicly known) tool names, and the
|
|
523
|
-
* plugin version. Cleared first so a stale `.error.json` from a previous
|
|
524
|
-
* failed boot doesn't survive a successful boot.
|
|
525
|
-
*/
|
|
526
|
-
/**
|
|
527
|
-
* Read the existing `.loaded.json` manifest for diagnostic surfaces
|
|
528
|
-
* (3.3.7-rc.1 — issue #216). Returns `null` if the manifest is
|
|
529
|
-
* missing, unreadable, or malformed. Best-effort: never throws.
|
|
530
|
-
*
|
|
531
|
-
* Scanner note: this helper lives in fs-helpers.ts (where all fs.*
|
|
532
|
-
* operations are consolidated) so the diagnostic slash command in
|
|
533
|
-
* `index.ts` doesn't have to introduce a fresh `readFileSync` call —
|
|
534
|
-
* the OpenClaw scanner whole-file rule disallows fs.read* next to the
|
|
535
|
-
* outbound-request trigger markers that index.ts already has in its
|
|
536
|
-
* on-chain submission code paths.
|
|
537
|
-
*/
|
|
538
|
-
export function readPluginLoadedManifest(pluginDir) {
|
|
539
|
-
try {
|
|
540
|
-
const root = resolvePluginRootForManifest(pluginDir);
|
|
541
|
-
const loadedPath = path.join(root, PLUGIN_LOADED_MANIFEST);
|
|
542
|
-
if (!fs.existsSync(loadedPath))
|
|
543
|
-
return null;
|
|
544
|
-
const raw = fs.readFileSync(loadedPath, 'utf-8');
|
|
545
|
-
const parsed = JSON.parse(raw);
|
|
546
|
-
if (typeof parsed.loadedAt !== 'number' || !Array.isArray(parsed.tools) || typeof parsed.version !== 'string') {
|
|
547
|
-
return null;
|
|
548
|
-
}
|
|
549
|
-
return parsed;
|
|
550
|
-
}
|
|
551
|
-
catch {
|
|
552
|
-
return null;
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
export function writePluginManifest(pluginDir, manifest) {
|
|
556
|
-
try {
|
|
557
|
-
const root = resolvePluginRootForManifest(pluginDir);
|
|
558
|
-
if (!fs.existsSync(root))
|
|
559
|
-
return false;
|
|
560
|
-
const loadedPath = path.join(root, PLUGIN_LOADED_MANIFEST);
|
|
561
|
-
const errorPath = path.join(root, PLUGIN_ERROR_MANIFEST);
|
|
562
|
-
// Best-effort error-file cleanup — a successful boot supersedes any
|
|
563
|
-
// prior failure marker. If the unlink fails (e.g. permission), the
|
|
564
|
-
// .loaded.json timestamp still tells the agent which is current.
|
|
565
|
-
try {
|
|
566
|
-
if (fs.existsSync(errorPath))
|
|
567
|
-
fs.unlinkSync(errorPath);
|
|
568
|
-
}
|
|
569
|
-
catch {
|
|
570
|
-
// Swallow — best-effort.
|
|
571
|
-
}
|
|
572
|
-
// 3.3.7-rc.1 (issue #216) — derive bootCount by reading the prior
|
|
573
|
-
// manifest. Lets the user grep `.loaded.json` after a container
|
|
574
|
-
// restart to verify register() actually ran. If the prior manifest
|
|
575
|
-
// is unreadable we start at 1.
|
|
576
|
-
let priorBootCount = 0;
|
|
577
|
-
try {
|
|
578
|
-
if (fs.existsSync(loadedPath)) {
|
|
579
|
-
const prior = JSON.parse(fs.readFileSync(loadedPath, 'utf-8'));
|
|
580
|
-
if (typeof prior.bootCount === 'number' && Number.isFinite(prior.bootCount)) {
|
|
581
|
-
priorBootCount = prior.bootCount;
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
catch {
|
|
586
|
-
// Swallow — if the prior manifest is corrupt we just start the counter fresh.
|
|
587
|
-
}
|
|
588
|
-
const enriched = {
|
|
589
|
-
...manifest,
|
|
590
|
-
bootCount: priorBootCount + 1,
|
|
591
|
-
bootAt: new Date(manifest.loadedAt).toISOString(),
|
|
592
|
-
pid: process.pid,
|
|
593
|
-
};
|
|
594
|
-
fs.writeFileSync(loadedPath, JSON.stringify(enriched, null, 2));
|
|
595
|
-
return true;
|
|
596
|
-
}
|
|
597
|
-
catch {
|
|
598
|
-
return false;
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
/**
|
|
602
|
-
* Write the error manifest. SYNCHRONOUS for the same reason as
|
|
603
|
-
* `writePluginManifest`. Called from the try/catch surrounding the
|
|
604
|
-
* register() body so the agent has a filesystem signal that the plugin
|
|
605
|
-
* registered AT LEAST attempted to load and failed in a specific way.
|
|
606
|
-
*
|
|
607
|
-
* Does NOT clear `.loaded.json` from a prior successful boot — keeping
|
|
608
|
-
* the older success marker around lets the agent see "last good boot was
|
|
609
|
-
* X, current boot failed at Y" without spelunking logs. The newer
|
|
610
|
-
* `.error.json` timestamp wins as "current state".
|
|
611
|
-
*/
|
|
612
|
-
export function writePluginError(pluginDir, error) {
|
|
613
|
-
try {
|
|
614
|
-
const root = resolvePluginRootForManifest(pluginDir);
|
|
615
|
-
if (!fs.existsSync(root))
|
|
616
|
-
return false;
|
|
617
|
-
const errorPath = path.join(root, PLUGIN_ERROR_MANIFEST);
|
|
618
|
-
fs.writeFileSync(errorPath, JSON.stringify(error, null, 2));
|
|
619
|
-
return true;
|
|
620
|
-
}
|
|
621
|
-
catch {
|
|
622
|
-
return false;
|
|
623
|
-
}
|
|
624
|
-
}
|
|
476
|
+
// The 3.3.2-rc.1 `.loaded.json` / `.error.json` manifest machinery (writer
|
|
477
|
+
// + reader + schema) was fully removed in Phase 3.4. Phase 3.1 already
|
|
478
|
+
// dropped the `writePluginManifest` call from register(); 3.4 retires the
|
|
479
|
+
// remaining reader (`readPluginLoadedManifest`) + the error-path writer
|
|
480
|
+
// (`writePluginError`) + the schema types now that no production path
|
|
481
|
+
// references them. The gateway log is the source of truth for register()
|
|
482
|
+
// failures; `/totalreclaw diag` reports pid + in-memory version only.
|
|
625
483
|
/**
|
|
626
484
|
* Drop the `.tr-partial-install` marker into `pluginRootDir`. Idempotent
|
|
627
485
|
* (overwrites any existing marker) and best-effort — returns `true` on
|
|
@@ -933,14 +791,11 @@ export function resolveOnboardingState(credentialsPath, statePath) {
|
|
|
933
791
|
* Auto-patch `~/.openclaw/openclaw.json` with the entries required by
|
|
934
792
|
* OpenClaw 2026.5.x for clean operation (issues #225 + #226 + verbosity):
|
|
935
793
|
*
|
|
936
|
-
*
|
|
937
|
-
*
|
|
938
|
-
*
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
* crash loop ("plugins.slots.memory: plugin not found: totalreclaw")
|
|
942
|
-
* that survives container restarts until `openclaw plugins install`
|
|
943
|
-
* repopulates the install record.
|
|
794
|
+
* NOTE (rc.20, #402): this helper no longer writes `plugins.slots.memory`
|
|
795
|
+
* (OpenClaw 2026.6.8 claims the memory slot natively on install/enable) nor
|
|
796
|
+
* `plugins.installs` (the former Fix #6 self-heal — its record reader
|
|
797
|
+
* discards the whole installs map if any entry fails schema validation).
|
|
798
|
+
* Pre-existing values of both are left untouched. The remaining fixes are:
|
|
944
799
|
*
|
|
945
800
|
* 2. `plugins.entries.totalreclaw.hooks.allowConversationAccess = true`
|
|
946
801
|
* Grant the plugin access to `agent_end` and `before_agent_start`
|
|
@@ -954,15 +809,11 @@ export function resolveOnboardingState(credentialsPath, statePath) {
|
|
|
954
809
|
* this to "off" on first run for a clean UX. Existing explicit values
|
|
955
810
|
* ("partial", "block", "progress") are preserved.
|
|
956
811
|
*
|
|
957
|
-
* 4
|
|
958
|
-
*
|
|
959
|
-
*
|
|
960
|
-
*
|
|
961
|
-
*
|
|
962
|
-
* `bundledDiscovery: "compat"` restores the looser behavior so allow-
|
|
963
|
-
* listed non-bundled plugins load. Without this fix, users with
|
|
964
|
-
* telegram or any model provider configured before TR install (which
|
|
965
|
-
* populates allow) get a silent plugin-skip on every gateway boot.
|
|
812
|
+
* Fix #4 (`plugins.bundledDiscovery="compat"`) and Fix #5 (appending
|
|
813
|
+
* "totalreclaw" to `plugins.allow`) were RETIRED together in rc.20 (#402):
|
|
814
|
+
* native install manages the allowlist, and TR growing it was the only
|
|
815
|
+
* thing that forced strict-allowlist mode (the sole reason the compat
|
|
816
|
+
* workaround existed). Pre-existing values of both keys are left untouched.
|
|
966
817
|
*
|
|
967
818
|
* Design constraints
|
|
968
819
|
* ------------------
|
|
@@ -985,10 +836,10 @@ export function resolveOnboardingState(credentialsPath, statePath) {
|
|
|
985
836
|
* Defaults to `<home>/.openclaw/openclaw.json`.
|
|
986
837
|
*/
|
|
987
838
|
export function patchOpenClawConfig(configPath,
|
|
988
|
-
//
|
|
989
|
-
//
|
|
839
|
+
// Retained on the signature (register() passes it) but no longer consumed:
|
|
840
|
+
// the Fix #6 install-record self-heal it fed was retired in rc.20 (#402).
|
|
990
841
|
pluginVersion) {
|
|
991
|
-
const home =
|
|
842
|
+
const home = envHomeDir();
|
|
992
843
|
const target = configPath ?? path.join(home, '.openclaw', 'openclaw.json');
|
|
993
844
|
// `'skipped'` when the config file is absent — this host may not be
|
|
994
845
|
// running OpenClaw, or may use a non-standard config location.
|
|
@@ -1003,109 +854,44 @@ pluginVersion) {
|
|
|
1003
854
|
cfg.plugins = {};
|
|
1004
855
|
}
|
|
1005
856
|
let mutated = false;
|
|
1006
|
-
// --- Fix #6 (
|
|
1007
|
-
//
|
|
1008
|
-
// OpenClaw 2026.5.6 has a config-rewrite-after-restart behaviour
|
|
1009
|
-
// observed on Pedro's pop-os QA host (2026-05-08): `openclaw plugins
|
|
1010
|
-
// install` writes the install record, gateway restart fires, but
|
|
1011
|
-
// after the restart something STRIPS `plugins.installs.totalreclaw` (and
|
|
1012
|
-
// sometimes `plugins.allow`, `plugins.entries.totalreclaw`,
|
|
1013
|
-
// `plugins.slots.memory`) from openclaw.json. The plugin's binary
|
|
1014
|
-
// remains in `~/.openclaw/npm/node_modules/@totalreclaw/totalreclaw/`,
|
|
1015
|
-
// but `openclaw plugins list` shows it as `disabled` because no
|
|
1016
|
-
// install record + no allow entry.
|
|
1017
|
-
//
|
|
1018
|
-
// Defensive self-heal: when this register() runs (which means the
|
|
1019
|
-
// plugin IS physically loaded by the gateway), if the install record
|
|
1020
|
-
// is missing or has no version, write a minimal record. This unlocks
|
|
1021
|
-
// Fix #1 (slot) and avoids the user-visible "plugin disabled"
|
|
1022
|
-
// condition without requiring `openclaw plugins install --force`.
|
|
857
|
+
// --- Fix #6 (installs self-heal) RETIRED in rc.20 (#402) ---
|
|
1023
858
|
//
|
|
1024
|
-
//
|
|
1025
|
-
// installedAt)
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
installedAt: new Date().toISOString(),
|
|
1041
|
-
};
|
|
1042
|
-
mutated = true;
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
// --- Fix #5 (3.3.12-rc.3): plugins.allow includes "totalreclaw" ---
|
|
859
|
+
// patchOpenClawConfig used to fabricate a `plugins.installs.totalreclaw`
|
|
860
|
+
// record (version/spec/source:"self-heal"/installedAt) whenever a version
|
|
861
|
+
// was passed and no record existed. Removed because it was inert at best
|
|
862
|
+
// and harmful at worst on OpenClaw 2026.6.8:
|
|
863
|
+
// (a) the record reader validates the WHOLE installs map against a
|
|
864
|
+
// schema where `source` must be a valid PluginInstallSource — the
|
|
865
|
+
// fabricated `source:"self-heal"` (and any source-less record) fails
|
|
866
|
+
// safeParse, discarding the ENTIRE installs map at load; and
|
|
867
|
+
// (b) the native installer never persists install records into
|
|
868
|
+
// openclaw.json (it strips them via withoutPluginInstallRecords and
|
|
869
|
+
// commits to the SQLite index, which always wins on merge).
|
|
870
|
+
// So the plugin no longer writes `plugins.installs` at all; a pre-existing
|
|
871
|
+
// record is left byte-identical. `pluginVersion` is retained on the
|
|
872
|
+
// signature (register() passes it) but is no longer consumed here.
|
|
873
|
+
void pluginVersion;
|
|
874
|
+
// --- Fix #5 (plugins.allow self-append) RETIRED in rc.20 (#402) ---
|
|
1046
875
|
//
|
|
1047
|
-
//
|
|
1048
|
-
//
|
|
1049
|
-
//
|
|
1050
|
-
//
|
|
1051
|
-
//
|
|
1052
|
-
//
|
|
1053
|
-
//
|
|
1054
|
-
//
|
|
876
|
+
// patchOpenClawConfig used to append "totalreclaw" to a non-empty
|
|
877
|
+
// `plugins.allow`. Removed as a pair with Fix #4 (below): OpenClaw's native
|
|
878
|
+
// `persistPluginInstall` already calls `addInstalledPluginToAllowlist`, and
|
|
879
|
+
// an empty/absent allowlist means allow-all (no write needed). TR
|
|
880
|
+
// unconditionally growing an allowlist was the very thing that flipped
|
|
881
|
+
// hosts into strict-allowlist enforcement — which then required the Fix #4
|
|
882
|
+
// `bundledDiscovery:"compat"` workaround. Dropping both lets native install
|
|
883
|
+
// own the allowlist; existing hosts keep whatever is already on disk.
|
|
884
|
+
// --- Fix #1 (memory-slot write) RETIRED in rc.20 (#402) ---
|
|
1055
885
|
//
|
|
1056
|
-
//
|
|
1057
|
-
//
|
|
1058
|
-
//
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
// --- Fix #1: plugins.slots.memory = "totalreclaw" (gated on install) ---
|
|
1066
|
-
//
|
|
1067
|
-
// DEFENSIVE GATE (3.3.9-rc.4 — 2026-05-05): only write the slot when
|
|
1068
|
-
// the plugin is genuinely INSTALLED (`plugins.installs.totalreclaw`
|
|
1069
|
-
// present with a `version`). Writing the slot unconditionally
|
|
1070
|
-
// produced a startup crash loop on Pedro's pop-os QA host on
|
|
1071
|
-
// 2026-05-05 — after a config reset, `plugins.installs.totalreclaw`
|
|
1072
|
-
// was missing but a previously-written `slots.memory = "totalreclaw"`
|
|
1073
|
-
// had survived. OpenClaw's startup validator refuses to start with
|
|
1074
|
-
//
|
|
1075
|
-
// Gateway failed to start: Error: Invalid config at openclaw.json.
|
|
1076
|
-
// plugins.slots.memory: plugin not found: totalreclaw
|
|
1077
|
-
// Run "openclaw doctor --fix" to repair, then retry.
|
|
1078
|
-
//
|
|
1079
|
-
// The container restart-loop drained ~13 attempts (12:10-12:23 UTC)
|
|
1080
|
-
// until `openclaw plugins install` was re-run and re-populated
|
|
1081
|
-
// `plugins.installs.totalreclaw`. With this gate, future installs
|
|
1082
|
-
// that wipe `plugins.installs` (config reset, `doctor --fix`,
|
|
1083
|
-
// migration tools) cannot regress into the same boot loop — slot is
|
|
1084
|
-
// only ever written when the install record exists, and the install
|
|
1085
|
-
// record is the install pipeline's authoritative signal that the
|
|
1086
|
-
// plugin is on disk and registered with the gateway.
|
|
1087
|
-
//
|
|
1088
|
-
// The hooks patch (Fix #2) and Telegram streaming patch (Fix #3) are
|
|
1089
|
-
// not gated this way — they write under `plugins.entries` and
|
|
1090
|
-
// `channels` which are inert without an install record, so they can
|
|
1091
|
-
// never trip the validator.
|
|
1092
|
-
const installsRoot = cfg.plugins.installs;
|
|
1093
|
-
const installEntry = typeof installsRoot === 'object' && installsRoot !== null
|
|
1094
|
-
? installsRoot.totalreclaw
|
|
1095
|
-
: undefined;
|
|
1096
|
-
const pluginIsInstalled = typeof installEntry === 'object'
|
|
1097
|
-
&& installEntry !== null
|
|
1098
|
-
&& typeof installEntry.version === 'string'
|
|
1099
|
-
&& installEntry.version.length > 0;
|
|
1100
|
-
if (pluginIsInstalled) {
|
|
1101
|
-
if (typeof cfg.plugins.slots !== 'object' || cfg.plugins.slots === null) {
|
|
1102
|
-
cfg.plugins.slots = {};
|
|
1103
|
-
}
|
|
1104
|
-
if (cfg.plugins.slots.memory !== 'totalreclaw') {
|
|
1105
|
-
cfg.plugins.slots.memory = 'totalreclaw';
|
|
1106
|
-
mutated = true;
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
886
|
+
// patchOpenClawConfig used to write `plugins.slots.memory = "totalreclaw"`
|
|
887
|
+
// (install-gated, to dodge a startup crash loop). OpenClaw 2026.6.8 now
|
|
888
|
+
// claims the memory slot NATIVELY during `plugins install`/`enable` (its
|
|
889
|
+
// persistPluginInstall "slot selection" phase), so the hand-written write
|
|
890
|
+
// was redundant on the good path and twice failed in production. The
|
|
891
|
+
// plugin no longer touches the memory slot — a pre-existing value is left
|
|
892
|
+
// byte-identical. Fix numbering below is preserved for continuity with the
|
|
893
|
+
// #225/#226 history. The Fix #6 install-record self-heal was retired
|
|
894
|
+
// above too (rc.20, #402).
|
|
1109
895
|
// --- Fix #2: plugins.entries.totalreclaw.hooks.allowConversationAccess = true ---
|
|
1110
896
|
if (typeof cfg.plugins.entries !== 'object' || cfg.plugins.entries === null) {
|
|
1111
897
|
cfg.plugins.entries = {};
|
|
@@ -1148,36 +934,15 @@ pluginVersion) {
|
|
|
1148
934
|
}
|
|
1149
935
|
}
|
|
1150
936
|
}
|
|
1151
|
-
// --- Fix #4
|
|
1152
|
-
//
|
|
1153
|
-
// OpenClaw 2026.5.x: when `plugins.allow` is populated (any non-empty
|
|
1154
|
-
// array), the gateway's plugin loader switches into strict-allowlist
|
|
1155
|
-
// mode. In strict mode, NON-BUNDLED plugins like totalreclaw are
|
|
1156
|
-
// silently rejected even when listed in `plugins.allow`, unless
|
|
1157
|
-
// `plugins.bundledDiscovery = "compat"` is explicitly set. Pedro's
|
|
1158
|
-
// 2026-05-07 QA on pop-os surfaced this — the gateway booted with only
|
|
1159
|
-
// the bundled providers (telegram, device-pair) and skipped totalreclaw
|
|
1160
|
-
// despite it being in the allow list. `openclaw doctor --fix` cures it
|
|
1161
|
-
// by setting `bundledDiscovery: "compat"`, but users shouldn't need
|
|
1162
|
-
// to run doctor manually for the plugin to load.
|
|
937
|
+
// --- Fix #4 (plugins.bundledDiscovery="compat") RETIRED in rc.20 (#402) ---
|
|
1163
938
|
//
|
|
1164
|
-
//
|
|
1165
|
-
// `plugins.
|
|
1166
|
-
//
|
|
1167
|
-
//
|
|
1168
|
-
//
|
|
1169
|
-
//
|
|
1170
|
-
//
|
|
1171
|
-
// the auto-discover code path. Real users with telegram + a model
|
|
1172
|
-
// provider configured before TR install have a populated allow list,
|
|
1173
|
-
// hitting the strict-mode path. The auto-QA harness in 3.3.11-rc.4
|
|
1174
|
-
// adds a populated-allow scenario to catch future regressions.
|
|
1175
|
-
if (Array.isArray(cfg.plugins.allow) && cfg.plugins.allow.length > 0) {
|
|
1176
|
-
if (cfg.plugins.bundledDiscovery === undefined) {
|
|
1177
|
-
cfg.plugins.bundledDiscovery = 'compat';
|
|
1178
|
-
mutated = true;
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
939
|
+
// patchOpenClawConfig used to set `plugins.bundledDiscovery = "compat"`
|
|
940
|
+
// whenever `plugins.allow` was populated, to keep non-bundled totalreclaw
|
|
941
|
+
// loadable under strict-allowlist mode. Retired as a pair with Fix #5: the
|
|
942
|
+
// ONLY reason a host was in strict-allowlist mode was that TR itself grew
|
|
943
|
+
// the allowlist (Fix #5). With Fix #5 gone, native install manages the
|
|
944
|
+
// allowlist and this compat workaround is unnecessary. An existing
|
|
945
|
+
// `bundledDiscovery` value on disk is left untouched.
|
|
1181
946
|
if (!mutated)
|
|
1182
947
|
return 'unchanged';
|
|
1183
948
|
// Write back with 2-space indent to match OpenClaw's own write style.
|
|
@@ -1227,81 +992,12 @@ export function checkCredentialsFileMode(credPath, logger) {
|
|
|
1227
992
|
}
|
|
1228
993
|
return 'ok';
|
|
1229
994
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
const parsed = JSON.parse(raw);
|
|
1240
|
-
if (parsed.v !== 1)
|
|
1241
|
-
return null;
|
|
1242
|
-
if (typeof parsed.url !== 'string' || !parsed.url)
|
|
1243
|
-
return null;
|
|
1244
|
-
if (typeof parsed.pin !== 'string' || !parsed.pin)
|
|
1245
|
-
return null;
|
|
1246
|
-
if (typeof parsed.sid !== 'string' || !parsed.sid)
|
|
1247
|
-
return null;
|
|
1248
|
-
if (typeof parsed.expires_at_ms !== 'number')
|
|
1249
|
-
return null;
|
|
1250
|
-
if (typeof parsed.created_at_ms !== 'number')
|
|
1251
|
-
return null;
|
|
1252
|
-
if (parsed.mode !== 'generate'
|
|
1253
|
-
&& parsed.mode !== 'import'
|
|
1254
|
-
&& parsed.mode !== 'either')
|
|
1255
|
-
return null;
|
|
1256
|
-
return parsed;
|
|
1257
|
-
}
|
|
1258
|
-
catch {
|
|
1259
|
-
return null;
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
/**
|
|
1263
|
-
* Write `.pair-pending.json` atomically-ish (single `writeFileSync`). Creates
|
|
1264
|
-
* the parent directory if missing. Uses mode `0o600` to match credentials.json
|
|
1265
|
-
* even though the contents are NOT secret (URL + PIN are surfaced to chat
|
|
1266
|
-
* anyway) — defensive default for the .totalreclaw/ dir.
|
|
1267
|
-
*
|
|
1268
|
-
* Returns `true` on success, `false` on any I/O error.
|
|
1269
|
-
*/
|
|
1270
|
-
export function writePairPendingFile(pendingPath, payload) {
|
|
1271
|
-
try {
|
|
1272
|
-
const dir = path.dirname(pendingPath);
|
|
1273
|
-
if (!fs.existsSync(dir))
|
|
1274
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
1275
|
-
fs.writeFileSync(pendingPath, JSON.stringify(payload), { mode: 0o600 });
|
|
1276
|
-
return true;
|
|
1277
|
-
}
|
|
1278
|
-
catch {
|
|
1279
|
-
return false;
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
/**
|
|
1283
|
-
* Delete the pair-pending sentinel. Used by:
|
|
1284
|
-
* - the pair-completion path (after credentials.json is finalized)
|
|
1285
|
-
* - the auto-pair flow when it detects an expired pending session
|
|
1286
|
-
*
|
|
1287
|
-
* Returns `true` if a file was deleted, `false` otherwise (no file or error).
|
|
1288
|
-
*/
|
|
1289
|
-
export function deletePairPendingFile(pendingPath) {
|
|
1290
|
-
try {
|
|
1291
|
-
if (!fs.existsSync(pendingPath))
|
|
1292
|
-
return false;
|
|
1293
|
-
fs.unlinkSync(pendingPath);
|
|
1294
|
-
return true;
|
|
1295
|
-
}
|
|
1296
|
-
catch {
|
|
1297
|
-
return false;
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
/**
|
|
1301
|
-
* Default `.pair-pending.json` path derived from a credentials.json path:
|
|
1302
|
-
* the sentinel lives in the same directory under the dot-prefixed name.
|
|
1303
|
-
* Callers MAY pass an explicit path (tests, custom layouts) instead.
|
|
1304
|
-
*/
|
|
1305
|
-
export function defaultPairPendingPath(credentialsPath) {
|
|
1306
|
-
return path.join(path.dirname(credentialsPath), '.pair-pending.json');
|
|
1307
|
-
}
|
|
995
|
+
// ---------------------------------------------------------------------------
|
|
996
|
+
// Pair-pending sentinel — RETIRED Phase 3.4
|
|
997
|
+
// ---------------------------------------------------------------------------
|
|
998
|
+
// The 3.3.13 `~/.totalreclaw/.pair-pending.json` sentinel + its utilities
|
|
999
|
+
// (loadPairPendingFile / writePairPendingFile / deletePairPendingFile /
|
|
1000
|
+
// defaultPairPendingPath / PairPendingFile) were removed in Phase 3.4.
|
|
1001
|
+
// Pairing is now user-initiated QR (`tr pair` CLI / the pair HTTP route)
|
|
1002
|
+
// per the native-integration design — the auto-pair-on-load state machine
|
|
1003
|
+
// that wrote + consumed this sentinel is gone.
|