@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/billing-cache.js
CHANGED
|
@@ -11,8 +11,12 @@
|
|
|
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`)
|
|
17
|
+
* - keeps the DataEdge-address override in sync with the relay's
|
|
18
|
+
* authoritative `data_edge_address` (staging vs prod contract; consumed
|
|
19
|
+
* verbatim — see `syncDataEdgeAddressFromBilling`, #460)
|
|
16
20
|
* - does NOT import anything that performs outbound I/O
|
|
17
21
|
*
|
|
18
22
|
* Do NOT add any outbound-request call to this file — a single match for
|
|
@@ -22,7 +26,9 @@
|
|
|
22
26
|
*/
|
|
23
27
|
import fs from 'node:fs';
|
|
24
28
|
import path from 'node:path';
|
|
25
|
-
import { CONFIG, setChainIdOverride } from './config.js';
|
|
29
|
+
import { CONFIG, setChainIdOverride, setDataEdgeAddressOverride } from './config.js';
|
|
30
|
+
/** A plausible EVM address — anything else from billing is ignored (#460). */
|
|
31
|
+
const DATA_EDGE_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
26
32
|
// ---------------------------------------------------------------------------
|
|
27
33
|
// Constants
|
|
28
34
|
// ---------------------------------------------------------------------------
|
|
@@ -33,25 +39,45 @@ export const BILLING_CACHE_TTL = 2 * 60 * 60 * 1000; // 2 hours
|
|
|
33
39
|
// Chain-id sync
|
|
34
40
|
// ---------------------------------------------------------------------------
|
|
35
41
|
/**
|
|
36
|
-
* Apply the
|
|
42
|
+
* Apply the relay's authoritative `chain_id` to the runtime chain override.
|
|
37
43
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
44
|
+
* After ops-1 (2026-06-05) both tiers run on Gnosis (chain 100) and the relay
|
|
45
|
+
* returns an authoritative `chain_id` in `/v1/billing/status`. The client MUST
|
|
46
|
+
* consume that verbatim — the old tier→chain derivation (Free ⇒ 84532 Base
|
|
47
|
+
* Sepolia) was retired two-tier logic that mis-signed FREE-tier UserOps
|
|
48
|
+
* against the wrong chain and queried a Base-Sepolia RPC for a Gnosis-deployed
|
|
49
|
+
* sender, producing deterministic AA10 (#402).
|
|
42
50
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
51
|
+
* A missing / non-finite `chain_id` (older relay, partial payload) defaults to
|
|
52
|
+
* 100 — never 84532. Called from `readBillingCache` and `writeBillingCache` so
|
|
53
|
+
* every cache read or write keeps the override in sync. Idempotent.
|
|
46
54
|
*/
|
|
47
|
-
export function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
export function syncChainIdFromBilling(chainId) {
|
|
56
|
+
setChainIdOverride(typeof chainId === 'number' && Number.isFinite(chainId) ? chainId : 100);
|
|
57
|
+
}
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// DataEdge-address sync
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
/**
|
|
62
|
+
* Apply the relay's authoritative `data_edge_address` to the runtime DataEdge
|
|
63
|
+
* override. Mirrors `syncChainIdFromBilling`.
|
|
64
|
+
*
|
|
65
|
+
* The relay routes each environment to its own DataEdge (staging is on-chain
|
|
66
|
+
* isolated). If the client ignores this and uses the WASM-baked default (the
|
|
67
|
+
* PROD DataEdge), writes against the staging relay mine on the prod contract
|
|
68
|
+
* while reads come from the staging subgraph → empty recall + phantom
|
|
69
|
+
* "stored=N" success (#460).
|
|
70
|
+
*
|
|
71
|
+
* A missing / malformed `data_edge_address` (older relay, partial payload,
|
|
72
|
+
* junk) clears the override (`null`) so resolution falls through to the WASM
|
|
73
|
+
* default — never a stale value. Only a plausible address
|
|
74
|
+
* (`0x` + 40 hex) is honored. Called from `readBillingCache` and
|
|
75
|
+
* `writeBillingCache` so every cache read or write keeps the override in sync.
|
|
76
|
+
* Idempotent. The explicit env override (`TOTALRECLAW_DATA_EDGE_ADDRESS`)
|
|
77
|
+
* still wins — it is the first term in `getSubgraphConfig`.
|
|
78
|
+
*/
|
|
79
|
+
export function syncDataEdgeAddressFromBilling(address) {
|
|
80
|
+
setDataEdgeAddressOverride(typeof address === 'string' && DATA_EDGE_ADDRESS_RE.test(address) ? address : null);
|
|
55
81
|
}
|
|
56
82
|
// ---------------------------------------------------------------------------
|
|
57
83
|
// Read / write
|
|
@@ -71,8 +97,10 @@ export function readBillingCache() {
|
|
|
71
97
|
const raw = JSON.parse(fs.readFileSync(BILLING_CACHE_PATH, 'utf-8'));
|
|
72
98
|
if (!raw.checked_at || Date.now() - raw.checked_at > BILLING_CACHE_TTL)
|
|
73
99
|
return null;
|
|
74
|
-
// Keep chain
|
|
75
|
-
|
|
100
|
+
// Keep chain + DataEdge overrides in sync with the persisted authoritative
|
|
101
|
+
// values across process restarts.
|
|
102
|
+
syncChainIdFromBilling(raw.chain_id);
|
|
103
|
+
syncDataEdgeAddressFromBilling(raw.data_edge_address);
|
|
76
104
|
return raw;
|
|
77
105
|
}
|
|
78
106
|
catch {
|
|
@@ -94,7 +122,9 @@ export function writeBillingCache(cache) {
|
|
|
94
122
|
catch {
|
|
95
123
|
// Best-effort — don't block on cache write failure.
|
|
96
124
|
}
|
|
97
|
-
// Sync chain
|
|
98
|
-
//
|
|
99
|
-
|
|
125
|
+
// Sync chain + DataEdge overrides AFTER the write so in-process UserOp
|
|
126
|
+
// signing + subgraph reads pick up the correct chain and contract
|
|
127
|
+
// immediately, even if the disk write failed.
|
|
128
|
+
syncChainIdFromBilling(cache.chain_id);
|
|
129
|
+
syncDataEdgeAddressFromBilling(cache.data_edge_address);
|
|
100
130
|
}
|
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);
|
|
@@ -411,7 +412,7 @@ export function resolveDigestMode() {
|
|
|
411
412
|
* @internal Not public config — emergency kill-switch only.
|
|
412
413
|
*/
|
|
413
414
|
export function resolveAutoResolveMode() {
|
|
414
|
-
const raw = (
|
|
415
|
+
const raw = envStringLower('TOTALRECLAW_AUTO_RESOLVE_MODE');
|
|
415
416
|
if (raw === 'off')
|
|
416
417
|
return 'off';
|
|
417
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
|
|
@@ -107,6 +107,35 @@ export function setChainIdOverride(chainId) {
|
|
|
107
107
|
export function __resetChainIdOverrideForTests() {
|
|
108
108
|
_chainIdOverride = null;
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Runtime override for the DataEdge contract address, set after the relay
|
|
112
|
+
* billing response is read. The relay returns an authoritative
|
|
113
|
+
* `data_edge_address` in `/v1/billing/status` (staging routes to the isolated
|
|
114
|
+
* staging DataEdge, production to the prod DataEdge). Chain-aware clients MUST
|
|
115
|
+
* consume it verbatim — otherwise writes mine on the WASM-baked prod DataEdge
|
|
116
|
+
* while reads hit the relay's subgraph, so recall silently returns empty
|
|
117
|
+
* against staging (#460). Mirrors `_chainIdOverride`.
|
|
118
|
+
*
|
|
119
|
+
* `null` means "no billing override" → fall through to the WASM default. The
|
|
120
|
+
* explicit operator env override (`TOTALRECLAW_DATA_EDGE_ADDRESS`, exposed as
|
|
121
|
+
* `CONFIG.dataEdgeAddress`) always wins over this.
|
|
122
|
+
*/
|
|
123
|
+
let _dataEdgeAddressOverride = null;
|
|
124
|
+
export function setDataEdgeAddressOverride(address) {
|
|
125
|
+
_dataEdgeAddressOverride = address;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Read the billing-sourced DataEdge override (or `''` when unset). Used by
|
|
129
|
+
* `getSubgraphConfig` as the middle term of the env → billing → WASM-default
|
|
130
|
+
* resolution order (#460).
|
|
131
|
+
*/
|
|
132
|
+
export function getDataEdgeAddressOverride() {
|
|
133
|
+
return _dataEdgeAddressOverride ?? '';
|
|
134
|
+
}
|
|
135
|
+
/** Reset the DataEdge override — used by tests. */
|
|
136
|
+
export function __resetDataEdgeAddressOverrideForTests() {
|
|
137
|
+
_dataEdgeAddressOverride = null;
|
|
138
|
+
}
|
|
110
139
|
export const CONFIG = {
|
|
111
140
|
// Core — recoveryPhrase reads from override first, then env var.
|
|
112
141
|
// Use getRecoveryPhrase() for dynamic access; this property is for
|
|
@@ -133,6 +162,42 @@ export const CONFIG = {
|
|
|
133
162
|
serverUrl: (process.env.TOTALRECLAW_SERVER_URL || 'https://api.totalreclaw.xyz').replace(/\/+$/, ''),
|
|
134
163
|
selfHosted: process.env.TOTALRECLAW_SELF_HOSTED === 'true',
|
|
135
164
|
credentialsPath: process.env.TOTALRECLAW_CREDENTIALS_PATH || path.join(home, '.totalreclaw', 'credentials.json'),
|
|
165
|
+
// cred-3 stage 1 — credential-provider abstraction.
|
|
166
|
+
//
|
|
167
|
+
// `file` (default) preserves the legacy behavior: read/write
|
|
168
|
+
// `credentialsPath` directly via fs-helpers.
|
|
169
|
+
//
|
|
170
|
+
// `external` instructs the plugin to load credentials from an
|
|
171
|
+
// out-of-process source at boot. Two transports are supported (mutually
|
|
172
|
+
// exclusive — JSON wins if both set):
|
|
173
|
+
// 1. `externalCredentialsJson` — raw JSON string of CredentialsFile,
|
|
174
|
+
// injected at process start by the secret manager (Railway secrets,
|
|
175
|
+
// Docker `--env-file`, K8s envFrom). Most ergonomic for cloud
|
|
176
|
+
// deployments where the secret manager already exposes secrets as
|
|
177
|
+
// env vars.
|
|
178
|
+
// 2. `externalCredentialsPath` — filesystem path to a JSON file the
|
|
179
|
+
// secret manager mounts (Docker Compose `secrets:` block,
|
|
180
|
+
// K8s secret volumeMount, tmpfs populated by an ops wrapper that
|
|
181
|
+
// fetches from AWS Secrets Manager / Hetzner Vault before boot).
|
|
182
|
+
//
|
|
183
|
+
// `external` mode is read-only: write/clear are no-ops with a warning
|
|
184
|
+
// (the secret manager owns the source of truth — the plugin must not
|
|
185
|
+
// write back through this path).
|
|
186
|
+
//
|
|
187
|
+
// See `docs/guides/production-deployment.md` for the LUKS / dm-crypt
|
|
188
|
+
// pattern and managed-secret-store wiring examples.
|
|
189
|
+
credentialsProvider: (() => {
|
|
190
|
+
const v = (process.env.TOTALRECLAW_CREDENTIALS_PROVIDER ?? '').trim().toLowerCase();
|
|
191
|
+
return v === 'external' ? 'external' : 'file';
|
|
192
|
+
})(),
|
|
193
|
+
externalCredentialsJson: (() => {
|
|
194
|
+
const v = (process.env.TOTALRECLAW_EXTERNAL_CREDENTIALS_JSON ?? '').trim();
|
|
195
|
+
return v.length > 0 ? v : null;
|
|
196
|
+
})(),
|
|
197
|
+
externalCredentialsPath: (() => {
|
|
198
|
+
const v = (process.env.TOTALRECLAW_EXTERNAL_CREDENTIALS_PATH ?? '').trim();
|
|
199
|
+
return v.length > 0 ? v : null;
|
|
200
|
+
})(),
|
|
136
201
|
// 3.2.0 onboarding state file — separate from credentials.json so it
|
|
137
202
|
// never contains secrets. Loaded on every plugin init + on every
|
|
138
203
|
// before_tool_call gate check.
|
|
@@ -166,21 +231,28 @@ export const CONFIG = {
|
|
|
166
231
|
|| (process.env.TOTALRECLAW_SERVER_URL
|
|
167
232
|
? process.env.TOTALRECLAW_SERVER_URL.replace(/^https?:\/\//, 'wss://').replace(/^http:/, 'ws:')
|
|
168
233
|
: '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
|
|
234
|
+
// Chain — chainId is no longer user-configurable. After the ops-1 single-
|
|
235
|
+
// chain migration, ALL tiers are on Gnosis (100). The default here is 100.
|
|
172
236
|
// completes. Self-hosted users can still point at a custom DataEdge via
|
|
173
237
|
// TOTALRECLAW_DATA_EDGE_ADDRESS / TOTALRECLAW_ENTRYPOINT_ADDRESS /
|
|
174
238
|
// TOTALRECLAW_RPC_URL (undocumented; internal knobs).
|
|
175
239
|
//
|
|
176
|
-
// Reads the runtime override set
|
|
177
|
-
// Falls back to
|
|
178
|
-
//
|
|
179
|
-
//
|
|
240
|
+
// Reads the runtime override set from the relay's authoritative chain_id
|
|
241
|
+
// (syncChainIdFromBilling). Falls back to 100 (Gnosis) pre-billing-lookup —
|
|
242
|
+
// after ops-1 both tiers are on Gnosis, so 84532 is never the default (#402).
|
|
243
|
+
// Must be a getter, not a literal — a literal would freeze UserOps to the
|
|
244
|
+
// wrong chainId and fail signature validation at the bundler.
|
|
180
245
|
get chainId() {
|
|
181
|
-
return _chainIdOverride ??
|
|
246
|
+
return _chainIdOverride ?? 100;
|
|
247
|
+
},
|
|
248
|
+
// Explicit operator env override for the DataEdge contract. Stays env-only:
|
|
249
|
+
// it is the FIRST term of `getSubgraphConfig`'s env → billing → WASM-default
|
|
250
|
+
// resolution order, so it wins over the relay's authoritative
|
|
251
|
+
// `data_edge_address` (see `getDataEdgeAddressOverride` / #460). Getter, not
|
|
252
|
+
// a literal, so it reflects the live env rather than freezing at module load.
|
|
253
|
+
get dataEdgeAddress() {
|
|
254
|
+
return process.env.TOTALRECLAW_DATA_EDGE_ADDRESS || '';
|
|
182
255
|
},
|
|
183
|
-
dataEdgeAddress: process.env.TOTALRECLAW_DATA_EDGE_ADDRESS || '',
|
|
184
256
|
entryPointAddress: process.env.TOTALRECLAW_ENTRYPOINT_ADDRESS || '',
|
|
185
257
|
rpcUrl: process.env.TOTALRECLAW_RPC_URL || '',
|
|
186
258
|
// 3.3.3-rc.1 (issue #187 — ONNX decouple): kill switch for the
|
|
@@ -202,6 +274,12 @@ export const CONFIG = {
|
|
|
202
274
|
// See: docs/specs/totalreclaw/client-consistency.md
|
|
203
275
|
cosineThreshold: parseFloat(process.env.TOTALRECLAW_COSINE_THRESHOLD ?? '0.15'),
|
|
204
276
|
extractInterval: parseInt(process.env.TOTALRECLAW_EXTRACT_INTERVAL ?? process.env.TOTALRECLAW_EXTRACT_EVERY_TURNS ?? '3', 10),
|
|
277
|
+
// Self-hosted fallback for max-facts-per-extraction. `undefined` when the
|
|
278
|
+
// env var is unset so getMaxFactsPerExtraction() can fall through to the
|
|
279
|
+
// billing cache then the built-in MAX_FACTS_PER_EXTRACTION constant.
|
|
280
|
+
maxFactsPerExtraction: process.env.TOTALRECLAW_MAX_FACTS_PER_EXTRACTION
|
|
281
|
+
? parseInt(process.env.TOTALRECLAW_MAX_FACTS_PER_EXTRACTION, 10)
|
|
282
|
+
: undefined,
|
|
205
283
|
relevanceThreshold: parseFloat(process.env.TOTALRECLAW_RELEVANCE_THRESHOLD ?? '0.3'),
|
|
206
284
|
semanticSkipThreshold: parseFloat(process.env.TOTALRECLAW_SEMANTIC_SKIP_THRESHOLD ?? '0.85'),
|
|
207
285
|
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
|
+
}
|