@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
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot-time chain-gate predicate for client-side `executeBatch` UserOp
|
|
3
|
+
* submission. Spec #281 §9 Phase 1 (item imp-16).
|
|
4
|
+
*
|
|
5
|
+
* The gate batches via `executeBatch` on Gnosis (chain 100). After ops-1
|
|
6
|
+
* (2026-06-05) BOTH tiers run on Gnosis, so both now batch — the chain==100
|
|
7
|
+
* check is retained as a safety guard (a non-100 chain, which shouldn't occur
|
|
8
|
+
* after ops-1, falls back to single-fact). The legacy Free ⇒ Base Sepolia
|
|
9
|
+
* (84532) single-fact path is gone with the retired two-tier routing (#402).
|
|
10
|
+
* The `TOTALRECLAW_GNOSIS_BATCH_ENABLED` env var is a hard kill-switch —
|
|
11
|
+
* setting it to `false` disables batching regardless of chain, so ops can
|
|
12
|
+
* revert to single-fact submission without a client redeploy. Behaviour of
|
|
13
|
+
* `shouldBatchOnChain` is unchanged by #402.
|
|
14
|
+
*
|
|
15
|
+
* Read at boot only (module-load time). Per-write reads would re-parse the
|
|
16
|
+
* env on every submission — too expensive for the auto-extraction hot path
|
|
17
|
+
* and pointless because the env doesn't change mid-process.
|
|
18
|
+
*
|
|
19
|
+
* Sibling work-leaves wire `shouldBatchOnChain` into `submitFactBatchOnChain`
|
|
20
|
+
* (TS) and `agent/lifecycle.py` (Python mirror in `batch_gate.py`); this
|
|
21
|
+
* module ships the primitive only.
|
|
22
|
+
*
|
|
23
|
+
* Env read is centralized in entry.ts (env-reading seam, Task 1.3 of the
|
|
24
|
+
* OpenClaw native integration plan, 2026-06-21).
|
|
25
|
+
*/
|
|
26
|
+
import { isGnosisBatchEnabledAtBoot } from './entry.js';
|
|
27
|
+
const GNOSIS_CHAIN_ID = 100;
|
|
28
|
+
export function shouldBatchOnChain(chainId) {
|
|
29
|
+
if (!isGnosisBatchEnabledAtBoot())
|
|
30
|
+
return false;
|
|
31
|
+
return chainId === GNOSIS_CHAIN_ID;
|
|
32
|
+
}
|
|
33
|
+
export const __testing = {
|
|
34
|
+
readGateForTests(env, chainId) {
|
|
35
|
+
const enabled = (env.TOTALRECLAW_GNOSIS_BATCH_ENABLED ?? '').toLowerCase() !== 'false';
|
|
36
|
+
if (!enabled)
|
|
37
|
+
return false;
|
|
38
|
+
return chainId === GNOSIS_CHAIN_ID;
|
|
39
|
+
},
|
|
40
|
+
};
|
package/dist/billing-cache.js
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* This module:
|
|
12
12
|
* - reads/writes `~/.totalreclaw/billing-cache.json` (path from CONFIG)
|
|
13
13
|
* - exports `BillingCache`, `BILLING_CACHE_PATH`, `BILLING_CACHE_TTL`
|
|
14
|
-
* - keeps the chain-id override in sync with the
|
|
15
|
-
*
|
|
14
|
+
* - keeps the chain-id override in sync with the relay's authoritative
|
|
15
|
+
* `chain_id` (after ops-1 both tiers are on Gnosis 100; the client consumes
|
|
16
|
+
* the relay value verbatim — see `syncChainIdFromBilling`)
|
|
16
17
|
* - does NOT import anything that performs outbound I/O
|
|
17
18
|
*
|
|
18
19
|
* Do NOT add any outbound-request call to this file — a single match for
|
|
@@ -33,25 +34,21 @@ export const BILLING_CACHE_TTL = 2 * 60 * 60 * 1000; // 2 hours
|
|
|
33
34
|
// Chain-id sync
|
|
34
35
|
// ---------------------------------------------------------------------------
|
|
35
36
|
/**
|
|
36
|
-
* Apply the
|
|
37
|
+
* Apply the relay's authoritative `chain_id` to the runtime chain override.
|
|
37
38
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* After ops-1 (2026-06-05) both tiers run on Gnosis (chain 100) and the relay
|
|
40
|
+
* returns an authoritative `chain_id` in `/v1/billing/status`. The client MUST
|
|
41
|
+
* consume that verbatim — the old tier→chain derivation (Free ⇒ 84532 Base
|
|
42
|
+
* Sepolia) was retired two-tier logic that mis-signed FREE-tier UserOps
|
|
43
|
+
* against the wrong chain and queried a Base-Sepolia RPC for a Gnosis-deployed
|
|
44
|
+
* sender, producing deterministic AA10 (#402).
|
|
42
45
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
+
* A missing / non-finite `chain_id` (older relay, partial payload) defaults to
|
|
47
|
+
* 100 — never 84532. Called from `readBillingCache` and `writeBillingCache` so
|
|
48
|
+
* every cache read or write keeps the override in sync. Idempotent.
|
|
46
49
|
*/
|
|
47
|
-
export function
|
|
48
|
-
|
|
49
|
-
setChainIdOverride(100);
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
// Free or unknown → reset to the default free-tier chain.
|
|
53
|
-
setChainIdOverride(84532);
|
|
54
|
-
}
|
|
50
|
+
export function syncChainIdFromBilling(chainId) {
|
|
51
|
+
setChainIdOverride(typeof chainId === 'number' && Number.isFinite(chainId) ? chainId : 100);
|
|
55
52
|
}
|
|
56
53
|
// ---------------------------------------------------------------------------
|
|
57
54
|
// Read / write
|
|
@@ -71,8 +68,9 @@ export function readBillingCache() {
|
|
|
71
68
|
const raw = JSON.parse(fs.readFileSync(BILLING_CACHE_PATH, 'utf-8'));
|
|
72
69
|
if (!raw.checked_at || Date.now() - raw.checked_at > BILLING_CACHE_TTL)
|
|
73
70
|
return null;
|
|
74
|
-
// Keep chain override in sync with persisted
|
|
75
|
-
|
|
71
|
+
// Keep chain override in sync with the persisted authoritative chain_id
|
|
72
|
+
// across process restarts.
|
|
73
|
+
syncChainIdFromBilling(raw.chain_id);
|
|
76
74
|
return raw;
|
|
77
75
|
}
|
|
78
76
|
catch {
|
|
@@ -96,5 +94,5 @@ export function writeBillingCache(cache) {
|
|
|
96
94
|
}
|
|
97
95
|
// Sync chain override AFTER the write so in-process UserOp signing picks
|
|
98
96
|
// up the correct chain immediately, even if the disk write failed.
|
|
99
|
-
|
|
97
|
+
syncChainIdFromBilling(cache.chain_id);
|
|
100
98
|
}
|
package/dist/claims-helper.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import crypto from 'node:crypto';
|
|
12
12
|
import { createRequire } from 'node:module';
|
|
13
13
|
import { isValidMemoryType, isValidMemoryTypeV1, V0_TO_V1_TYPE, VALID_MEMORY_SCOPES, VALID_MEMORY_SOURCES, VALID_MEMORY_VOLATILITIES, VALID_MEMORY_TYPES_V1, } from './extractor.js';
|
|
14
|
+
import { envStringLower } from './entry.js';
|
|
14
15
|
// Lazy-load WASM. We use createRequire so this module loads cleanly under
|
|
15
16
|
// both the OpenClaw runtime (CJS-ish tsx) and bare Node ESM (used by tests).
|
|
16
17
|
const requireWasm = createRequire(import.meta.url);
|
|
@@ -181,6 +182,11 @@ export function buildCanonicalClaimV1(input) {
|
|
|
181
182
|
if (typeof input.embeddingModelId === 'string' && input.embeddingModelId.length > 0) {
|
|
182
183
|
canonical.embedding_model_id = input.embeddingModelId;
|
|
183
184
|
}
|
|
185
|
+
// am-1: Crystal-shaped debrief structured metadata. Re-attached after
|
|
186
|
+
// validation using the same plugin-extra pattern as volatility.
|
|
187
|
+
if (fact.crystalMetadata) {
|
|
188
|
+
canonical.metadata = fact.crystalMetadata;
|
|
189
|
+
}
|
|
184
190
|
return JSON.stringify(canonical);
|
|
185
191
|
}
|
|
186
192
|
/**
|
|
@@ -406,7 +412,7 @@ export function resolveDigestMode() {
|
|
|
406
412
|
* @internal Not public config — emergency kill-switch only.
|
|
407
413
|
*/
|
|
408
414
|
export function resolveAutoResolveMode() {
|
|
409
|
-
const raw = (
|
|
415
|
+
const raw = envStringLower('TOTALRECLAW_AUTO_RESOLVE_MODE');
|
|
410
416
|
if (raw === 'off')
|
|
411
417
|
return 'off';
|
|
412
418
|
if (raw === 'shadow')
|
package/dist/config.js
CHANGED
|
@@ -90,10 +90,10 @@ export function getSessionId() {
|
|
|
90
90
|
}
|
|
91
91
|
/**
|
|
92
92
|
* Runtime override for chain ID, set after the relay billing response is
|
|
93
|
-
* read.
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* the signature with
|
|
93
|
+
* read. After the ops-1 single-chain migration (2026-05), ALL tiers (free
|
|
94
|
+
* + Pro) are on Gnosis mainnet (chain 100). The default below is 100.
|
|
95
|
+
* The relay routes all writes to Gnosis, so UserOps MUST be signed against
|
|
96
|
+
* chain 100 — otherwise the bundler rejects the signature with AA24.
|
|
97
97
|
*
|
|
98
98
|
* See index.ts: after the billing lookup completes, call
|
|
99
99
|
* `setChainIdOverride(100)` for Pro users. Free users can leave the
|
|
@@ -133,6 +133,42 @@ export const CONFIG = {
|
|
|
133
133
|
serverUrl: (process.env.TOTALRECLAW_SERVER_URL || 'https://api.totalreclaw.xyz').replace(/\/+$/, ''),
|
|
134
134
|
selfHosted: process.env.TOTALRECLAW_SELF_HOSTED === 'true',
|
|
135
135
|
credentialsPath: process.env.TOTALRECLAW_CREDENTIALS_PATH || path.join(home, '.totalreclaw', 'credentials.json'),
|
|
136
|
+
// cred-3 stage 1 — credential-provider abstraction.
|
|
137
|
+
//
|
|
138
|
+
// `file` (default) preserves the legacy behavior: read/write
|
|
139
|
+
// `credentialsPath` directly via fs-helpers.
|
|
140
|
+
//
|
|
141
|
+
// `external` instructs the plugin to load credentials from an
|
|
142
|
+
// out-of-process source at boot. Two transports are supported (mutually
|
|
143
|
+
// exclusive — JSON wins if both set):
|
|
144
|
+
// 1. `externalCredentialsJson` — raw JSON string of CredentialsFile,
|
|
145
|
+
// injected at process start by the secret manager (Railway secrets,
|
|
146
|
+
// Docker `--env-file`, K8s envFrom). Most ergonomic for cloud
|
|
147
|
+
// deployments where the secret manager already exposes secrets as
|
|
148
|
+
// env vars.
|
|
149
|
+
// 2. `externalCredentialsPath` — filesystem path to a JSON file the
|
|
150
|
+
// secret manager mounts (Docker Compose `secrets:` block,
|
|
151
|
+
// K8s secret volumeMount, tmpfs populated by an ops wrapper that
|
|
152
|
+
// fetches from AWS Secrets Manager / Hetzner Vault before boot).
|
|
153
|
+
//
|
|
154
|
+
// `external` mode is read-only: write/clear are no-ops with a warning
|
|
155
|
+
// (the secret manager owns the source of truth — the plugin must not
|
|
156
|
+
// write back through this path).
|
|
157
|
+
//
|
|
158
|
+
// See `docs/guides/production-deployment.md` for the LUKS / dm-crypt
|
|
159
|
+
// pattern and managed-secret-store wiring examples.
|
|
160
|
+
credentialsProvider: (() => {
|
|
161
|
+
const v = (process.env.TOTALRECLAW_CREDENTIALS_PROVIDER ?? '').trim().toLowerCase();
|
|
162
|
+
return v === 'external' ? 'external' : 'file';
|
|
163
|
+
})(),
|
|
164
|
+
externalCredentialsJson: (() => {
|
|
165
|
+
const v = (process.env.TOTALRECLAW_EXTERNAL_CREDENTIALS_JSON ?? '').trim();
|
|
166
|
+
return v.length > 0 ? v : null;
|
|
167
|
+
})(),
|
|
168
|
+
externalCredentialsPath: (() => {
|
|
169
|
+
const v = (process.env.TOTALRECLAW_EXTERNAL_CREDENTIALS_PATH ?? '').trim();
|
|
170
|
+
return v.length > 0 ? v : null;
|
|
171
|
+
})(),
|
|
136
172
|
// 3.2.0 onboarding state file — separate from credentials.json so it
|
|
137
173
|
// never contains secrets. Loaded on every plugin init + on every
|
|
138
174
|
// before_tool_call gate check.
|
|
@@ -166,19 +202,19 @@ export const CONFIG = {
|
|
|
166
202
|
|| (process.env.TOTALRECLAW_SERVER_URL
|
|
167
203
|
? process.env.TOTALRECLAW_SERVER_URL.replace(/^https?:\/\//, 'wss://').replace(/^http:/, 'ws:')
|
|
168
204
|
: 'wss://api.totalreclaw.xyz')).replace(/\/+$/, ''),
|
|
169
|
-
// Chain — chainId is no longer user-configurable.
|
|
170
|
-
//
|
|
171
|
-
// 100). The default here is used only before the first billing lookup
|
|
205
|
+
// Chain — chainId is no longer user-configurable. After the ops-1 single-
|
|
206
|
+
// chain migration, ALL tiers are on Gnosis (100). The default here is 100.
|
|
172
207
|
// completes. Self-hosted users can still point at a custom DataEdge via
|
|
173
208
|
// TOTALRECLAW_DATA_EDGE_ADDRESS / TOTALRECLAW_ENTRYPOINT_ADDRESS /
|
|
174
209
|
// TOTALRECLAW_RPC_URL (undocumented; internal knobs).
|
|
175
210
|
//
|
|
176
|
-
// Reads the runtime override set
|
|
177
|
-
// Falls back to
|
|
178
|
-
//
|
|
179
|
-
//
|
|
211
|
+
// Reads the runtime override set from the relay's authoritative chain_id
|
|
212
|
+
// (syncChainIdFromBilling). Falls back to 100 (Gnosis) pre-billing-lookup —
|
|
213
|
+
// after ops-1 both tiers are on Gnosis, so 84532 is never the default (#402).
|
|
214
|
+
// Must be a getter, not a literal — a literal would freeze UserOps to the
|
|
215
|
+
// wrong chainId and fail signature validation at the bundler.
|
|
180
216
|
get chainId() {
|
|
181
|
-
return _chainIdOverride ??
|
|
217
|
+
return _chainIdOverride ?? 100;
|
|
182
218
|
},
|
|
183
219
|
dataEdgeAddress: process.env.TOTALRECLAW_DATA_EDGE_ADDRESS || '',
|
|
184
220
|
entryPointAddress: process.env.TOTALRECLAW_ENTRYPOINT_ADDRESS || '',
|
|
@@ -202,6 +238,12 @@ export const CONFIG = {
|
|
|
202
238
|
// See: docs/specs/totalreclaw/client-consistency.md
|
|
203
239
|
cosineThreshold: parseFloat(process.env.TOTALRECLAW_COSINE_THRESHOLD ?? '0.15'),
|
|
204
240
|
extractInterval: parseInt(process.env.TOTALRECLAW_EXTRACT_INTERVAL ?? process.env.TOTALRECLAW_EXTRACT_EVERY_TURNS ?? '3', 10),
|
|
241
|
+
// Self-hosted fallback for max-facts-per-extraction. `undefined` when the
|
|
242
|
+
// env var is unset so getMaxFactsPerExtraction() can fall through to the
|
|
243
|
+
// billing cache then the built-in MAX_FACTS_PER_EXTRACTION constant.
|
|
244
|
+
maxFactsPerExtraction: process.env.TOTALRECLAW_MAX_FACTS_PER_EXTRACTION
|
|
245
|
+
? parseInt(process.env.TOTALRECLAW_MAX_FACTS_PER_EXTRACTION, 10)
|
|
246
|
+
: undefined,
|
|
205
247
|
relevanceThreshold: parseFloat(process.env.TOTALRECLAW_RELEVANCE_THRESHOLD ?? '0.3'),
|
|
206
248
|
semanticSkipThreshold: parseFloat(process.env.TOTALRECLAW_SEMANTIC_SKIP_THRESHOLD ?? '0.85'),
|
|
207
249
|
cacheTtlMs: parseInt(process.env.TOTALRECLAW_CACHE_TTL_MS ?? String(5 * 60 * 1000), 10),
|
package/dist/consolidation.js
CHANGED
|
@@ -18,10 +18,13 @@
|
|
|
18
18
|
* and `clusterFacts` WASM functions when available, falling back to local
|
|
19
19
|
* implementations that use WASM-backed `cosineSimilarity`.
|
|
20
20
|
*
|
|
21
|
-
* Threshold helpers remain local
|
|
21
|
+
* Threshold helpers remain local; their env reads are centralized in
|
|
22
|
+
* entry.ts (env-reading seam, Task 1.3 of the OpenClaw native
|
|
23
|
+
* integration plan, 2026-06-21).
|
|
22
24
|
*/
|
|
23
25
|
import { createRequire } from 'node:module';
|
|
24
26
|
import { cosineSimilarity } from './reranker.js';
|
|
27
|
+
import { envNumber } from './entry.js';
|
|
25
28
|
// ---------------------------------------------------------------------------
|
|
26
29
|
// Lazy-load WASM core (mirrors claims-helper.ts / contradiction-sync.ts
|
|
27
30
|
// pattern — plays nicely under both the OpenClaw runtime (CJS-ish tsx) and
|
|
@@ -44,13 +47,7 @@ function getWasm() {
|
|
|
44
47
|
* Must be a number in [0, 1]. Falls back to 0.85 if invalid or unset.
|
|
45
48
|
*/
|
|
46
49
|
export function getStoreDedupThreshold() {
|
|
47
|
-
|
|
48
|
-
if (envVal !== undefined) {
|
|
49
|
-
const parsed = parseFloat(envVal);
|
|
50
|
-
if (!isNaN(parsed) && parsed >= 0 && parsed <= 1)
|
|
51
|
-
return parsed;
|
|
52
|
-
}
|
|
53
|
-
return 0.85;
|
|
50
|
+
return envNumber('TOTALRECLAW_STORE_DEDUP_THRESHOLD', 0.85, { min: 0, max: 1 });
|
|
54
51
|
}
|
|
55
52
|
/**
|
|
56
53
|
* Get the cosine similarity threshold for bulk consolidation clustering.
|
|
@@ -59,13 +56,7 @@ export function getStoreDedupThreshold() {
|
|
|
59
56
|
* Must be a number in [0, 1]. Falls back to 0.88 if invalid or unset.
|
|
60
57
|
*/
|
|
61
58
|
export function getConsolidationThreshold() {
|
|
62
|
-
|
|
63
|
-
if (envVal !== undefined) {
|
|
64
|
-
const parsed = parseFloat(envVal);
|
|
65
|
-
if (!isNaN(parsed) && parsed >= 0 && parsed <= 1)
|
|
66
|
-
return parsed;
|
|
67
|
-
}
|
|
68
|
-
return 0.88;
|
|
59
|
+
return envNumber('TOTALRECLAW_CONSOLIDATION_THRESHOLD', 0.88, { min: 0, max: 1 });
|
|
69
60
|
}
|
|
70
61
|
/** Maximum candidates to compare against during store-time dedup. */
|
|
71
62
|
export const STORE_DEDUP_MAX_CANDIDATES = 200;
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { createRequire } from 'node:module';
|
|
18
18
|
import fs from 'node:fs';
|
|
19
|
-
import os from 'node:os';
|
|
20
19
|
import path from 'node:path';
|
|
20
|
+
import { envHomedir, envNumber, envString, envStringLower, } from './entry.js';
|
|
21
21
|
import { computeEntityTrapdoor, isDigestBlob, } from './claims-helper.js';
|
|
22
22
|
const requireWasm = createRequire(import.meta.url);
|
|
23
23
|
let _wasm = null;
|
|
@@ -31,10 +31,10 @@ function getWasm() {
|
|
|
31
31
|
// ---------------------------------------------------------------------------
|
|
32
32
|
/** Where feedback, decisions, and weights live. `~/.totalreclaw/` by default. */
|
|
33
33
|
function resolveStateDir() {
|
|
34
|
-
const override =
|
|
35
|
-
if (override
|
|
34
|
+
const override = envString('TOTALRECLAW_STATE_DIR');
|
|
35
|
+
if (override.length > 0)
|
|
36
36
|
return override;
|
|
37
|
-
return path.join(
|
|
37
|
+
return path.join(envHomedir(), '.totalreclaw');
|
|
38
38
|
}
|
|
39
39
|
function ensureStateDir() {
|
|
40
40
|
const dir = resolveStateDir();
|
|
@@ -292,7 +292,11 @@ function _resolveWithCandidatesCore(core, input, items, byId) {
|
|
|
292
292
|
const { newClaim, newClaimId, newEmbedding, weightsJson, thresholdLower, thresholdUpper, nowUnixSeconds, logger } = input;
|
|
293
293
|
let actionsJson;
|
|
294
294
|
try {
|
|
295
|
-
actionsJson = core.resolveWithCandidates(JSON.stringify(newClaim), newClaimId, JSON.stringify(newEmbedding), JSON.stringify(items), weightsJson, thresholdLower, thresholdUpper,
|
|
295
|
+
actionsJson = core.resolveWithCandidates(JSON.stringify(newClaim), newClaimId, JSON.stringify(newEmbedding), JSON.stringify(items), weightsJson, thresholdLower, thresholdUpper,
|
|
296
|
+
// wasm-bindgen declares `now_unix` as i64 → JS BigInt. Node ≥26 rejects
|
|
297
|
+
// the implicit Number→BigInt coercion that older runtimes tolerated, so
|
|
298
|
+
// wrap explicitly (same as the legacy path below + loadWeightsFile).
|
|
299
|
+
BigInt(Math.floor(nowUnixSeconds)), TIE_ZONE_SCORE_TOLERANCE);
|
|
296
300
|
}
|
|
297
301
|
catch (err) {
|
|
298
302
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -453,7 +457,7 @@ function _resolveWithLegacyPipeline(core, input, items, byId) {
|
|
|
453
457
|
export async function detectAndResolveContradictions(input) {
|
|
454
458
|
const { newClaim, newClaimId, newEmbedding, subgraphOwner, authKeyHex, encryptionKey, deps, logger, } = input;
|
|
455
459
|
// Read env per-call so tests can toggle without module reload.
|
|
456
|
-
const raw = (
|
|
460
|
+
const raw = envStringLower('TOTALRECLAW_AUTO_RESOLVE_MODE');
|
|
457
461
|
const mode = raw === 'off' ? 'off' : raw === 'shadow' ? 'shadow' : 'active';
|
|
458
462
|
if (mode === 'off')
|
|
459
463
|
return [];
|
|
@@ -687,15 +691,11 @@ export const TUNING_LOOP_MIN_INTERVAL_SECONDS = 3600;
|
|
|
687
691
|
* empty, non-numeric, or negative.
|
|
688
692
|
*/
|
|
689
693
|
export function getTuningLoopMinIntervalSeconds() {
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
696
|
-
return TUNING_LOOP_MIN_INTERVAL_SECONDS;
|
|
697
|
-
}
|
|
698
|
-
return parsed;
|
|
694
|
+
// envNumber returns the fallback for unset/empty/non-finite/negative —
|
|
695
|
+
// recovering the original 3-guard contract in one call.
|
|
696
|
+
return envNumber('TOTALRECLAW_TUNING_MIN_INTERVAL_OVERRIDE_SECONDS', TUNING_LOOP_MIN_INTERVAL_SECONDS, {
|
|
697
|
+
min: 0,
|
|
698
|
+
});
|
|
699
699
|
}
|
|
700
700
|
/**
|
|
701
701
|
* Walk `decisions.jsonl` in reverse and find the most recent `supersede_existing`
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* credential-provider — credential source abstraction (cred-3 stage 1).
|
|
3
|
+
*
|
|
4
|
+
* The plugin needs to load its `CredentialsFile` (mnemonic + userId + salt +
|
|
5
|
+
* scope_address) from one of two sources without per-deployment forks:
|
|
6
|
+
*
|
|
7
|
+
* 1. `file` — read/write `~/.totalreclaw/credentials.json` directly
|
|
8
|
+
* (legacy behavior; default).
|
|
9
|
+
* 2. `external` — read from a secret manager that exposes the JSON as
|
|
10
|
+
* either an env var (Railway secrets, Docker `--env-file`, K8s
|
|
11
|
+
* `envFrom`) OR a mounted file path (Docker Compose `secrets:`,
|
|
12
|
+
* K8s secret volumeMount, tmpfs populated by an ops wrapper that
|
|
13
|
+
* pulls from a managed vault before plugin start).
|
|
14
|
+
*
|
|
15
|
+
* Stage 1 introduces the abstraction + integration test. Stage 2 routes
|
|
16
|
+
* Hermes Python through the same surface; stage 3 routes MCP and ships a
|
|
17
|
+
* concrete vault adapter. Plugin call sites are not yet rewired to go
|
|
18
|
+
* through the provider — that's a follow-up so behavior under the default
|
|
19
|
+
* `file` mode is byte-identical to today.
|
|
20
|
+
*
|
|
21
|
+
* Scanner constraints (see skill/scripts/check-scanner.mjs):
|
|
22
|
+
* - This file MUST avoid any subprocess module import. Both transport
|
|
23
|
+
* flavors here use `node:fs` only.
|
|
24
|
+
* - This file MUST avoid network-word substrings to keep the
|
|
25
|
+
* exfiltration rule quiet. All env reads are centralized in
|
|
26
|
+
* `config.ts`; we accept resolved values here.
|
|
27
|
+
*
|
|
28
|
+
* Phrase-safety:
|
|
29
|
+
* - The mnemonic is never logged from this file. Errors reference only
|
|
30
|
+
* transport names + sizes, never the raw payload.
|
|
31
|
+
* - File writes preserve mode `0o600` via the existing fs-helpers
|
|
32
|
+
* `writeCredentialsJson`.
|
|
33
|
+
* - `external` mode is read-only by design — the secret manager owns
|
|
34
|
+
* the source of truth; writing back would split it.
|
|
35
|
+
*/
|
|
36
|
+
import fs from 'node:fs';
|
|
37
|
+
import { CONFIG } from './config.js';
|
|
38
|
+
import { loadCredentialsJson, writeCredentialsJson, deleteCredentialsFile, } from './fs-helpers.js';
|
|
39
|
+
/**
|
|
40
|
+
* Default provider — delegates to the existing fs-helpers functions.
|
|
41
|
+
* Behavior is identical to direct `loadCredentialsJson` / `writeCredentialsJson`
|
|
42
|
+
* / `deleteCredentialsFile` calls, so wiring a call site through this
|
|
43
|
+
* provider in `file` mode produces zero behavior delta.
|
|
44
|
+
*/
|
|
45
|
+
export class FileCredentialProvider {
|
|
46
|
+
credentialsPath;
|
|
47
|
+
mode = 'file';
|
|
48
|
+
constructor(credentialsPath) {
|
|
49
|
+
this.credentialsPath = credentialsPath;
|
|
50
|
+
}
|
|
51
|
+
load() {
|
|
52
|
+
return loadCredentialsJson(this.credentialsPath);
|
|
53
|
+
}
|
|
54
|
+
save(creds) {
|
|
55
|
+
return writeCredentialsJson(this.credentialsPath, creds);
|
|
56
|
+
}
|
|
57
|
+
clear() {
|
|
58
|
+
return deleteCredentialsFile(this.credentialsPath);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* External provider — reads credentials from a secret manager via one of
|
|
63
|
+
* two transports (env-injected JSON wins if both are set).
|
|
64
|
+
*
|
|
65
|
+
* Inline JSON (`TOTALRECLAW_EXTERNAL_CREDENTIALS_JSON`)
|
|
66
|
+
* The raw `CredentialsFile` JSON, injected as an env var at process
|
|
67
|
+
* start. Most ergonomic for managed platforms that expose secrets as
|
|
68
|
+
* env vars (Railway secrets, Heroku config vars, K8s `envFrom`).
|
|
69
|
+
*
|
|
70
|
+
* File mount (`TOTALRECLAW_EXTERNAL_CREDENTIALS_PATH`)
|
|
71
|
+
* A path to a JSON file the secret manager mounts. Pattern works for
|
|
72
|
+
* Docker Compose `secrets:` blocks, K8s secret `volumeMount`s, and
|
|
73
|
+
* tmpfs paths populated by an ops wrapper script that fetched the
|
|
74
|
+
* payload from a vault (AWS Secrets Manager, Hetzner Vault, etc.)
|
|
75
|
+
* before plugin start.
|
|
76
|
+
*
|
|
77
|
+
* Both flavors are read-only — `save()` and `clear()` return `false` and
|
|
78
|
+
* the caller logs a warn. The secret manager is the canonical source;
|
|
79
|
+
* writing back would create two competing truths.
|
|
80
|
+
*/
|
|
81
|
+
export class ExternalCredentialProvider {
|
|
82
|
+
options;
|
|
83
|
+
mode = 'external';
|
|
84
|
+
constructor(options) {
|
|
85
|
+
this.options = options;
|
|
86
|
+
}
|
|
87
|
+
load() {
|
|
88
|
+
const raw = this.readRaw();
|
|
89
|
+
if (raw === null)
|
|
90
|
+
return null;
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(raw);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Parse error — return null so the bootstrap path can decide how to
|
|
96
|
+
// surface (typically: log + fall through to fresh-generate, same as
|
|
97
|
+
// a corrupt credentials.json on disk). We deliberately do not echo
|
|
98
|
+
// the payload here even on parse failure.
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
save(_creds) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
clear() {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
readRaw() {
|
|
109
|
+
if (this.options.inlineJson !== null) {
|
|
110
|
+
return this.options.inlineJson;
|
|
111
|
+
}
|
|
112
|
+
if (this.options.filePath !== null) {
|
|
113
|
+
try {
|
|
114
|
+
if (!fs.existsSync(this.options.filePath))
|
|
115
|
+
return null;
|
|
116
|
+
return fs.readFileSync(this.options.filePath, 'utf-8');
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Factory — pick the configured provider. Reads from `CONFIG` (single
|
|
127
|
+
* source of truth for env vars) so tests can override by mutating the
|
|
128
|
+
* config snapshot or by passing explicit deps to the constructors.
|
|
129
|
+
*
|
|
130
|
+
* In `external` mode with neither transport set, we still return an
|
|
131
|
+
* `ExternalCredentialProvider` so the caller observes the configured
|
|
132
|
+
* mode + sees `null` from `load()`. The caller is responsible for
|
|
133
|
+
* surfacing the misconfiguration; we do not silently fall back to
|
|
134
|
+
* `file` mode (that would mask a deploy-time mistake by reading a
|
|
135
|
+
* stale `credentials.json` left on disk).
|
|
136
|
+
*/
|
|
137
|
+
export function getCredentialProvider(config = CONFIG) {
|
|
138
|
+
if (config.credentialsProvider === 'external') {
|
|
139
|
+
return new ExternalCredentialProvider({
|
|
140
|
+
inlineJson: config.externalCredentialsJson,
|
|
141
|
+
filePath: config.externalCredentialsPath,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return new FileCredentialProvider(config.credentialsPath);
|
|
145
|
+
}
|
package/dist/crypto.js
CHANGED
|
@@ -1,138 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// the CJS `require` global is undefined at runtime. Prior to the rc.21 fix
|
|
20
|
-
// this file called bare `require('@totalreclaw/core')` and every consumer
|
|
21
|
-
// died with `require is not defined`. Matches the pattern already used by
|
|
22
|
-
// claims-helper / consolidation / contradiction-sync / digest-sync / pin /
|
|
23
|
-
// retype-setscope. See issue #124.
|
|
24
|
-
import { createRequire } from 'node:module';
|
|
25
|
-
const requireWasm = createRequire(import.meta.url);
|
|
26
|
-
let _wasm = null;
|
|
27
|
-
function getWasm() {
|
|
28
|
-
if (!_wasm)
|
|
29
|
-
_wasm = requireWasm('@totalreclaw/core');
|
|
30
|
-
return _wasm;
|
|
31
|
-
}
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
// BIP-39 Validation
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
/**
|
|
36
|
-
* Check if the input looks like a BIP-39 mnemonic (12 or 24 words).
|
|
37
|
-
*
|
|
38
|
-
* Lenient: accepts phrases where all words look like valid BIP-39 words
|
|
39
|
-
* (allows invalid checksums, which LLMs sometimes generate).
|
|
40
|
-
*/
|
|
41
|
-
export function isBip39Mnemonic(input) {
|
|
42
|
-
const words = input.trim().split(/\s+/);
|
|
43
|
-
return words.length === 12 || words.length === 24;
|
|
44
|
-
}
|
|
45
|
-
// Re-export for backward compatibility
|
|
46
|
-
export const validateMnemonic = isBip39Mnemonic;
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
// Key Derivation
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
/**
|
|
51
|
-
* Derive auth, encryption, and dedup keys from a recovery phrase.
|
|
52
|
-
*
|
|
53
|
-
* Delegates to the WASM module for BIP-39 seed derivation and HKDF
|
|
54
|
-
* key separation. Uses the lenient variant for phrases where all words
|
|
55
|
-
* are valid but the checksum fails.
|
|
56
|
-
*
|
|
57
|
-
* @param password - BIP-39 12/24-word mnemonic
|
|
58
|
-
* @param existingSalt - Ignored for BIP-39 path (salt is deterministic)
|
|
59
|
-
*/
|
|
60
|
-
export function deriveKeys(password, existingSalt) {
|
|
61
|
-
const trimmed = password.trim();
|
|
62
|
-
// Try strict validation first, fall back to lenient
|
|
63
|
-
let result;
|
|
64
|
-
try {
|
|
65
|
-
result = getWasm().deriveKeysFromMnemonic(trimmed);
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
result = getWasm().deriveKeysFromMnemonicLenient(trimmed);
|
|
69
|
-
}
|
|
70
|
-
return {
|
|
71
|
-
authKey: Buffer.from(result.auth_key, 'hex'),
|
|
72
|
-
encryptionKey: Buffer.from(result.encryption_key, 'hex'),
|
|
73
|
-
dedupKey: Buffer.from(result.dedup_key, 'hex'),
|
|
74
|
-
salt: Buffer.from(result.salt, 'hex'),
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
// ---------------------------------------------------------------------------
|
|
78
|
-
// LSH Seed Derivation
|
|
79
|
-
// ---------------------------------------------------------------------------
|
|
80
|
-
/**
|
|
81
|
-
* Derive a 32-byte seed for the LSH hasher.
|
|
82
|
-
*
|
|
83
|
-
* Delegates to the WASM module.
|
|
84
|
-
*/
|
|
85
|
-
export function deriveLshSeed(password, salt) {
|
|
86
|
-
const seedHex = getWasm().deriveLshSeed(password.trim(), salt.toString('hex'));
|
|
87
|
-
return new Uint8Array(Buffer.from(seedHex, 'hex'));
|
|
88
|
-
}
|
|
89
|
-
// ---------------------------------------------------------------------------
|
|
90
|
-
// Auth Key Hash
|
|
91
|
-
// ---------------------------------------------------------------------------
|
|
92
|
-
/**
|
|
93
|
-
* Compute the SHA-256 hash of the auth key.
|
|
94
|
-
*/
|
|
95
|
-
export function computeAuthKeyHash(authKey) {
|
|
96
|
-
return getWasm().computeAuthKeyHash(authKey.toString('hex'));
|
|
97
|
-
}
|
|
98
|
-
// ---------------------------------------------------------------------------
|
|
99
|
-
// XChaCha20-Poly1305 Encrypt / Decrypt
|
|
100
|
-
// ---------------------------------------------------------------------------
|
|
101
|
-
/**
|
|
102
|
-
* Encrypt a UTF-8 plaintext string with XChaCha20-Poly1305.
|
|
103
|
-
*
|
|
104
|
-
* Wire format (base64-encoded):
|
|
105
|
-
* [nonce: 24 bytes][tag: 16 bytes][ciphertext: variable]
|
|
106
|
-
*/
|
|
107
|
-
export function encrypt(plaintext, encryptionKey) {
|
|
108
|
-
return getWasm().encrypt(plaintext, encryptionKey.toString('hex'));
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Decrypt a base64-encoded XChaCha20-Poly1305 blob back to a UTF-8 string.
|
|
112
|
-
*/
|
|
113
|
-
export function decrypt(encryptedBase64, encryptionKey) {
|
|
114
|
-
return getWasm().decrypt(encryptedBase64, encryptionKey.toString('hex'));
|
|
115
|
-
}
|
|
116
|
-
// ---------------------------------------------------------------------------
|
|
117
|
-
// Blind Indices
|
|
118
|
-
// ---------------------------------------------------------------------------
|
|
119
|
-
/**
|
|
120
|
-
* Generate blind indices (SHA-256 hashes of tokens) for a text string.
|
|
121
|
-
*
|
|
122
|
-
* Delegates to the WASM module which performs tokenization, stemming,
|
|
123
|
-
* and SHA-256 hashing.
|
|
124
|
-
*/
|
|
125
|
-
export function generateBlindIndices(text) {
|
|
126
|
-
return getWasm().generateBlindIndices(text);
|
|
127
|
-
}
|
|
128
|
-
// ---------------------------------------------------------------------------
|
|
129
|
-
// Content Fingerprint (Dedup)
|
|
130
|
-
// ---------------------------------------------------------------------------
|
|
131
|
-
/**
|
|
132
|
-
* Compute an HMAC-SHA256 content fingerprint for exact-duplicate detection.
|
|
133
|
-
*
|
|
134
|
-
* @returns 64-character hex string.
|
|
135
|
-
*/
|
|
136
|
-
export function generateContentFingerprint(plaintext, dedupKey) {
|
|
137
|
-
return getWasm().generateContentFingerprint(plaintext, dedupKey.toString('hex'));
|
|
138
|
-
}
|
|
2
|
+
* crypto.ts — legacy re-export shim.
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 (Task 1.1) of the OpenClaw native integration
|
|
5
|
+
* (docs/plans/2026-06-21-openclaw-native-integration-plan.md, 2026-06-21):
|
|
6
|
+
* the pure-compute vault primitives (XChaCha20-Poly1305 encrypt/decrypt,
|
|
7
|
+
* BIP-39 key derivation, blind indices, content fingerprints, LSH seed,
|
|
8
|
+
* auth-key hash) have moved to `vault-crypto.ts`. This file remains as a
|
|
9
|
+
* thin re-export so existing importers (`index.ts`, `tr-cli.ts`,
|
|
10
|
+
* `tr-cli-export-helper.ts`, `pair-cli-relay.ts`) do not break in this
|
|
11
|
+
* pass — a big-bang rewrite of the 331KB monolith's import graph is out
|
|
12
|
+
* of scope for the scanner-clean split.
|
|
13
|
+
*
|
|
14
|
+
* Nothing here reads the environment or hits the network; all key
|
|
15
|
+
* material and nonces are passed in by callers. See vault-crypto.ts for
|
|
16
|
+
* the implementation.
|
|
17
|
+
*/
|
|
18
|
+
export { isBip39Mnemonic, validateMnemonic, deriveKeys, deriveLshSeed, computeAuthKeyHash, encrypt, decrypt, generateBlindIndices, generateContentFingerprint, } from './vault-crypto.js';
|