@totalreclaw/totalreclaw 3.3.12-rc.2 → 3.3.12-rc.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/SKILL.md +34 -249
- package/api-client.ts +17 -9
- package/batch-gate.ts +42 -0
- package/billing-cache.ts +25 -20
- package/claims-helper.ts +7 -1
- package/config.ts +54 -12
- package/consolidation.ts +6 -13
- package/contradiction-sync.ts +19 -14
- package/credential-provider.ts +184 -0
- package/crypto.ts +27 -160
- package/dist/api-client.js +17 -9
- package/dist/batch-gate.js +40 -0
- package/dist/billing-cache.js +19 -21
- package/dist/claims-helper.js +7 -1
- package/dist/config.js +54 -12
- package/dist/consolidation.js +6 -15
- package/dist/contradiction-sync.js +15 -15
- package/dist/credential-provider.js +145 -0
- package/dist/crypto.js +17 -137
- package/dist/download-ux.js +11 -7
- package/dist/embedder-loader.js +266 -0
- package/dist/embedding.js +36 -3
- package/dist/entry.js +123 -0
- package/dist/extractor.js +134 -0
- package/dist/fs-helpers.js +116 -241
- package/dist/import-adapters/chatgpt-adapter.js +14 -0
- package/dist/import-adapters/claude-adapter.js +14 -0
- package/dist/import-adapters/gemini-adapter.js +43 -159
- package/dist/import-adapters/mcp-memory-adapter.js +14 -0
- package/dist/import-state-manager.js +100 -0
- package/dist/index.js +1130 -2520
- package/dist/llm-client.js +69 -1
- package/dist/memory-runtime.js +459 -0
- package/dist/native-memory.js +123 -0
- package/dist/onboarding-cli.js +3 -2
- package/dist/pair-cli.js +1 -1
- package/dist/pair-crypto.js +16 -358
- package/dist/pair-http.js +147 -4
- package/dist/relay.js +140 -0
- package/dist/reranker.js +13 -8
- package/dist/semantic-dedup.js +5 -7
- package/dist/skill-register.js +97 -0
- package/dist/subgraph-search.js +3 -1
- package/dist/subgraph-store.js +315 -282
- package/dist/tool-gating.js +39 -26
- package/dist/tools.js +367 -0
- package/dist/tr-cli-export-helper.js +103 -0
- package/dist/tr-cli.js +220 -127
- package/dist/trajectory-poller.js +155 -9
- package/dist/vault-crypto.js +551 -0
- package/download-ux.ts +12 -6
- package/embedder-loader.ts +293 -1
- package/embedding.ts +43 -3
- package/entry.ts +132 -0
- package/extractor.ts +167 -0
- package/fs-helpers.ts +166 -292
- package/import-adapters/chatgpt-adapter.ts +18 -0
- package/import-adapters/claude-adapter.ts +18 -0
- package/import-adapters/gemini-adapter.ts +56 -183
- package/import-adapters/mcp-memory-adapter.ts +18 -0
- package/import-adapters/types.ts +1 -1
- package/import-state-manager.ts +139 -0
- package/index.ts +1432 -3002
- package/llm-client.ts +74 -1
- package/memory-runtime.ts +723 -0
- package/native-memory.ts +196 -0
- package/onboarding-cli.ts +3 -2
- package/openclaw.plugin.json +5 -17
- package/package.json +7 -4
- package/pair-cli.ts +1 -1
- package/pair-crypto.ts +41 -483
- package/pair-http.ts +194 -5
- package/postinstall.mjs +138 -0
- package/relay.ts +172 -0
- package/reranker.ts +13 -8
- package/semantic-dedup.ts +5 -6
- package/skill-register.ts +146 -0
- package/skill.json +1 -1
- package/subgraph-search.ts +3 -1
- package/subgraph-store.ts +334 -299
- package/tool-gating.ts +39 -26
- package/tools.ts +499 -0
- package/tr-cli-export-helper.ts +138 -0
- package/tr-cli.ts +263 -133
- package/trajectory-poller.ts +162 -10
- package/vault-crypto.ts +705 -0
package/config.ts
CHANGED
|
@@ -102,10 +102,10 @@ export function getSessionId(): string | null {
|
|
|
102
102
|
|
|
103
103
|
/**
|
|
104
104
|
* Runtime override for chain ID, set after the relay billing response is
|
|
105
|
-
* read.
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* the signature with
|
|
105
|
+
* read. After the ops-1 single-chain migration (2026-05), ALL tiers (free
|
|
106
|
+
* + Pro) are on Gnosis mainnet (chain 100). The default below is 100.
|
|
107
|
+
* The relay routes all writes to Gnosis, so UserOps MUST be signed against
|
|
108
|
+
* chain 100 — otherwise the bundler rejects the signature with AA24.
|
|
109
109
|
*
|
|
110
110
|
* See index.ts: after the billing lookup completes, call
|
|
111
111
|
* `setChainIdOverride(100)` for Pro users. Free users can leave the
|
|
@@ -148,6 +148,42 @@ export const CONFIG = {
|
|
|
148
148
|
serverUrl: (process.env.TOTALRECLAW_SERVER_URL || 'https://api.totalreclaw.xyz').replace(/\/+$/, ''),
|
|
149
149
|
selfHosted: process.env.TOTALRECLAW_SELF_HOSTED === 'true',
|
|
150
150
|
credentialsPath: process.env.TOTALRECLAW_CREDENTIALS_PATH || path.join(home, '.totalreclaw', 'credentials.json'),
|
|
151
|
+
// cred-3 stage 1 — credential-provider abstraction.
|
|
152
|
+
//
|
|
153
|
+
// `file` (default) preserves the legacy behavior: read/write
|
|
154
|
+
// `credentialsPath` directly via fs-helpers.
|
|
155
|
+
//
|
|
156
|
+
// `external` instructs the plugin to load credentials from an
|
|
157
|
+
// out-of-process source at boot. Two transports are supported (mutually
|
|
158
|
+
// exclusive — JSON wins if both set):
|
|
159
|
+
// 1. `externalCredentialsJson` — raw JSON string of CredentialsFile,
|
|
160
|
+
// injected at process start by the secret manager (Railway secrets,
|
|
161
|
+
// Docker `--env-file`, K8s envFrom). Most ergonomic for cloud
|
|
162
|
+
// deployments where the secret manager already exposes secrets as
|
|
163
|
+
// env vars.
|
|
164
|
+
// 2. `externalCredentialsPath` — filesystem path to a JSON file the
|
|
165
|
+
// secret manager mounts (Docker Compose `secrets:` block,
|
|
166
|
+
// K8s secret volumeMount, tmpfs populated by an ops wrapper that
|
|
167
|
+
// fetches from AWS Secrets Manager / Hetzner Vault before boot).
|
|
168
|
+
//
|
|
169
|
+
// `external` mode is read-only: write/clear are no-ops with a warning
|
|
170
|
+
// (the secret manager owns the source of truth — the plugin must not
|
|
171
|
+
// write back through this path).
|
|
172
|
+
//
|
|
173
|
+
// See `docs/guides/production-deployment.md` for the LUKS / dm-crypt
|
|
174
|
+
// pattern and managed-secret-store wiring examples.
|
|
175
|
+
credentialsProvider: (() => {
|
|
176
|
+
const v = (process.env.TOTALRECLAW_CREDENTIALS_PROVIDER ?? '').trim().toLowerCase();
|
|
177
|
+
return v === 'external' ? 'external' : 'file';
|
|
178
|
+
})() as 'file' | 'external',
|
|
179
|
+
externalCredentialsJson: (() => {
|
|
180
|
+
const v = (process.env.TOTALRECLAW_EXTERNAL_CREDENTIALS_JSON ?? '').trim();
|
|
181
|
+
return v.length > 0 ? v : null;
|
|
182
|
+
})() as string | null,
|
|
183
|
+
externalCredentialsPath: (() => {
|
|
184
|
+
const v = (process.env.TOTALRECLAW_EXTERNAL_CREDENTIALS_PATH ?? '').trim();
|
|
185
|
+
return v.length > 0 ? v : null;
|
|
186
|
+
})() as string | null,
|
|
151
187
|
// 3.2.0 onboarding state file — separate from credentials.json so it
|
|
152
188
|
// never contains secrets. Loaded on every plugin init + on every
|
|
153
189
|
// before_tool_call gate check.
|
|
@@ -185,19 +221,19 @@ export const CONFIG = {
|
|
|
185
221
|
: 'wss://api.totalreclaw.xyz')
|
|
186
222
|
).replace(/\/+$/, ''),
|
|
187
223
|
|
|
188
|
-
// Chain — chainId is no longer user-configurable.
|
|
189
|
-
//
|
|
190
|
-
// 100). The default here is used only before the first billing lookup
|
|
224
|
+
// Chain — chainId is no longer user-configurable. After the ops-1 single-
|
|
225
|
+
// chain migration, ALL tiers are on Gnosis (100). The default here is 100.
|
|
191
226
|
// completes. Self-hosted users can still point at a custom DataEdge via
|
|
192
227
|
// TOTALRECLAW_DATA_EDGE_ADDRESS / TOTALRECLAW_ENTRYPOINT_ADDRESS /
|
|
193
228
|
// TOTALRECLAW_RPC_URL (undocumented; internal knobs).
|
|
194
229
|
//
|
|
195
|
-
// Reads the runtime override set
|
|
196
|
-
// Falls back to
|
|
197
|
-
//
|
|
198
|
-
//
|
|
230
|
+
// Reads the runtime override set from the relay's authoritative chain_id
|
|
231
|
+
// (syncChainIdFromBilling). Falls back to 100 (Gnosis) pre-billing-lookup —
|
|
232
|
+
// after ops-1 both tiers are on Gnosis, so 84532 is never the default (#402).
|
|
233
|
+
// Must be a getter, not a literal — a literal would freeze UserOps to the
|
|
234
|
+
// wrong chainId and fail signature validation at the bundler.
|
|
199
235
|
get chainId(): number {
|
|
200
|
-
return _chainIdOverride ??
|
|
236
|
+
return _chainIdOverride ?? 100;
|
|
201
237
|
},
|
|
202
238
|
dataEdgeAddress: process.env.TOTALRECLAW_DATA_EDGE_ADDRESS || '',
|
|
203
239
|
entryPointAddress: process.env.TOTALRECLAW_ENTRYPOINT_ADDRESS || '',
|
|
@@ -224,6 +260,12 @@ export const CONFIG = {
|
|
|
224
260
|
// See: docs/specs/totalreclaw/client-consistency.md
|
|
225
261
|
cosineThreshold: parseFloat(process.env.TOTALRECLAW_COSINE_THRESHOLD ?? '0.15'),
|
|
226
262
|
extractInterval: parseInt(process.env.TOTALRECLAW_EXTRACT_INTERVAL ?? process.env.TOTALRECLAW_EXTRACT_EVERY_TURNS ?? '3', 10),
|
|
263
|
+
// Self-hosted fallback for max-facts-per-extraction. `undefined` when the
|
|
264
|
+
// env var is unset so getMaxFactsPerExtraction() can fall through to the
|
|
265
|
+
// billing cache then the built-in MAX_FACTS_PER_EXTRACTION constant.
|
|
266
|
+
maxFactsPerExtraction: process.env.TOTALRECLAW_MAX_FACTS_PER_EXTRACTION
|
|
267
|
+
? parseInt(process.env.TOTALRECLAW_MAX_FACTS_PER_EXTRACTION, 10)
|
|
268
|
+
: undefined,
|
|
227
269
|
relevanceThreshold: parseFloat(process.env.TOTALRECLAW_RELEVANCE_THRESHOLD ?? '0.3'),
|
|
228
270
|
semanticSkipThreshold: parseFloat(process.env.TOTALRECLAW_SEMANTIC_SKIP_THRESHOLD ?? '0.85'),
|
|
229
271
|
cacheTtlMs: parseInt(process.env.TOTALRECLAW_CACHE_TTL_MS ?? String(5 * 60 * 1000), 10),
|
package/consolidation.ts
CHANGED
|
@@ -18,11 +18,14 @@
|
|
|
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
|
|
|
24
26
|
import { createRequire } from 'node:module';
|
|
25
27
|
import { cosineSimilarity } from './reranker.js';
|
|
28
|
+
import { envNumber } from './entry.js';
|
|
26
29
|
|
|
27
30
|
// ---------------------------------------------------------------------------
|
|
28
31
|
// Lazy-load WASM core (mirrors claims-helper.ts / contradiction-sync.ts
|
|
@@ -47,12 +50,7 @@ function getWasm(): typeof import('@totalreclaw/core') {
|
|
|
47
50
|
* Must be a number in [0, 1]. Falls back to 0.85 if invalid or unset.
|
|
48
51
|
*/
|
|
49
52
|
export function getStoreDedupThreshold(): number {
|
|
50
|
-
|
|
51
|
-
if (envVal !== undefined) {
|
|
52
|
-
const parsed = parseFloat(envVal);
|
|
53
|
-
if (!isNaN(parsed) && parsed >= 0 && parsed <= 1) return parsed;
|
|
54
|
-
}
|
|
55
|
-
return 0.85;
|
|
53
|
+
return envNumber('TOTALRECLAW_STORE_DEDUP_THRESHOLD', 0.85, { min: 0, max: 1 });
|
|
56
54
|
}
|
|
57
55
|
|
|
58
56
|
/**
|
|
@@ -62,12 +60,7 @@ export function getStoreDedupThreshold(): number {
|
|
|
62
60
|
* Must be a number in [0, 1]. Falls back to 0.88 if invalid or unset.
|
|
63
61
|
*/
|
|
64
62
|
export function getConsolidationThreshold(): number {
|
|
65
|
-
|
|
66
|
-
if (envVal !== undefined) {
|
|
67
|
-
const parsed = parseFloat(envVal);
|
|
68
|
-
if (!isNaN(parsed) && parsed >= 0 && parsed <= 1) return parsed;
|
|
69
|
-
}
|
|
70
|
-
return 0.88;
|
|
63
|
+
return envNumber('TOTALRECLAW_CONSOLIDATION_THRESHOLD', 0.88, { min: 0, max: 1 });
|
|
71
64
|
}
|
|
72
65
|
|
|
73
66
|
/** Maximum candidates to compare against during store-time dedup. */
|
package/contradiction-sync.ts
CHANGED
|
@@ -19,6 +19,12 @@ import { createRequire } from 'node:module';
|
|
|
19
19
|
import fs from 'node:fs';
|
|
20
20
|
import os from 'node:os';
|
|
21
21
|
import path from 'node:path';
|
|
22
|
+
import {
|
|
23
|
+
envHomedir,
|
|
24
|
+
envNumber,
|
|
25
|
+
envString,
|
|
26
|
+
envStringLower,
|
|
27
|
+
} from './entry.js';
|
|
22
28
|
import {
|
|
23
29
|
computeEntityTrapdoor,
|
|
24
30
|
isDigestBlob,
|
|
@@ -145,9 +151,9 @@ export interface DecisionLogEntry {
|
|
|
145
151
|
|
|
146
152
|
/** Where feedback, decisions, and weights live. `~/.totalreclaw/` by default. */
|
|
147
153
|
function resolveStateDir(): string {
|
|
148
|
-
const override =
|
|
149
|
-
if (override
|
|
150
|
-
return path.join(
|
|
154
|
+
const override = envString('TOTALRECLAW_STATE_DIR');
|
|
155
|
+
if (override.length > 0) return override;
|
|
156
|
+
return path.join(envHomedir(), '.totalreclaw');
|
|
151
157
|
}
|
|
152
158
|
|
|
153
159
|
function ensureStateDir(): string {
|
|
@@ -508,7 +514,10 @@ function _resolveWithCandidatesCore(
|
|
|
508
514
|
weightsJson,
|
|
509
515
|
thresholdLower,
|
|
510
516
|
thresholdUpper,
|
|
511
|
-
|
|
517
|
+
// wasm-bindgen declares `now_unix` as i64 → JS BigInt. Node ≥26 rejects
|
|
518
|
+
// the implicit Number→BigInt coercion that older runtimes tolerated, so
|
|
519
|
+
// wrap explicitly (same as the legacy path below + loadWeightsFile).
|
|
520
|
+
BigInt(Math.floor(nowUnixSeconds)),
|
|
512
521
|
TIE_ZONE_SCORE_TOLERANCE,
|
|
513
522
|
);
|
|
514
523
|
} catch (err) {
|
|
@@ -738,7 +747,7 @@ export async function detectAndResolveContradictions(
|
|
|
738
747
|
} = input;
|
|
739
748
|
|
|
740
749
|
// Read env per-call so tests can toggle without module reload.
|
|
741
|
-
const raw = (
|
|
750
|
+
const raw = envStringLower('TOTALRECLAW_AUTO_RESOLVE_MODE');
|
|
742
751
|
const mode: AutoResolveMode =
|
|
743
752
|
raw === 'off' ? 'off' : raw === 'shadow' ? 'shadow' : 'active';
|
|
744
753
|
|
|
@@ -1004,15 +1013,11 @@ export const TUNING_LOOP_MIN_INTERVAL_SECONDS = 3600;
|
|
|
1004
1013
|
* empty, non-numeric, or negative.
|
|
1005
1014
|
*/
|
|
1006
1015
|
export function getTuningLoopMinIntervalSeconds(): number {
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
1013
|
-
return TUNING_LOOP_MIN_INTERVAL_SECONDS;
|
|
1014
|
-
}
|
|
1015
|
-
return parsed;
|
|
1016
|
+
// envNumber returns the fallback for unset/empty/non-finite/negative —
|
|
1017
|
+
// recovering the original 3-guard contract in one call.
|
|
1018
|
+
return envNumber('TOTALRECLAW_TUNING_MIN_INTERVAL_OVERRIDE_SECONDS', TUNING_LOOP_MIN_INTERVAL_SECONDS, {
|
|
1019
|
+
min: 0,
|
|
1020
|
+
});
|
|
1016
1021
|
}
|
|
1017
1022
|
|
|
1018
1023
|
/** A single row from feedback.jsonl — matches Rust `FeedbackEntry`. */
|
|
@@ -0,0 +1,184 @@
|
|
|
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
|
+
|
|
37
|
+
import fs from 'node:fs';
|
|
38
|
+
|
|
39
|
+
import { CONFIG } from './config.js';
|
|
40
|
+
import {
|
|
41
|
+
loadCredentialsJson,
|
|
42
|
+
writeCredentialsJson,
|
|
43
|
+
deleteCredentialsFile,
|
|
44
|
+
type CredentialsFile,
|
|
45
|
+
} from './fs-helpers.js';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Provider interface — three methods cover the lifecycle:
|
|
49
|
+
* - `load()` is called at boot and on every credential-dependent
|
|
50
|
+
* pre-tool hook. Returns null when the source has no usable creds.
|
|
51
|
+
* - `save()` persists a freshly-generated or updated `CredentialsFile`.
|
|
52
|
+
* Returns `false` for read-only providers (caller logs a warn).
|
|
53
|
+
* - `clear()` deletes the credentials (used by `forceReinitialization`).
|
|
54
|
+
* Returns `false` for read-only providers.
|
|
55
|
+
*
|
|
56
|
+
* `mode` is exposed so callers can branch UI / warnings — e.g. the
|
|
57
|
+
* first-run announcement is skipped in `external` mode because the
|
|
58
|
+
* mnemonic was authored elsewhere.
|
|
59
|
+
*/
|
|
60
|
+
export interface CredentialProvider {
|
|
61
|
+
readonly mode: 'file' | 'external';
|
|
62
|
+
load(): CredentialsFile | null;
|
|
63
|
+
save(creds: CredentialsFile): boolean;
|
|
64
|
+
clear(): boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Default provider — delegates to the existing fs-helpers functions.
|
|
69
|
+
* Behavior is identical to direct `loadCredentialsJson` / `writeCredentialsJson`
|
|
70
|
+
* / `deleteCredentialsFile` calls, so wiring a call site through this
|
|
71
|
+
* provider in `file` mode produces zero behavior delta.
|
|
72
|
+
*/
|
|
73
|
+
export class FileCredentialProvider implements CredentialProvider {
|
|
74
|
+
readonly mode = 'file' as const;
|
|
75
|
+
|
|
76
|
+
constructor(private readonly credentialsPath: string) {}
|
|
77
|
+
|
|
78
|
+
load(): CredentialsFile | null {
|
|
79
|
+
return loadCredentialsJson(this.credentialsPath);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
save(creds: CredentialsFile): boolean {
|
|
83
|
+
return writeCredentialsJson(this.credentialsPath, creds);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
clear(): boolean {
|
|
87
|
+
return deleteCredentialsFile(this.credentialsPath);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* External provider — reads credentials from a secret manager via one of
|
|
93
|
+
* two transports (env-injected JSON wins if both are set).
|
|
94
|
+
*
|
|
95
|
+
* Inline JSON (`TOTALRECLAW_EXTERNAL_CREDENTIALS_JSON`)
|
|
96
|
+
* The raw `CredentialsFile` JSON, injected as an env var at process
|
|
97
|
+
* start. Most ergonomic for managed platforms that expose secrets as
|
|
98
|
+
* env vars (Railway secrets, Heroku config vars, K8s `envFrom`).
|
|
99
|
+
*
|
|
100
|
+
* File mount (`TOTALRECLAW_EXTERNAL_CREDENTIALS_PATH`)
|
|
101
|
+
* A path to a JSON file the secret manager mounts. Pattern works for
|
|
102
|
+
* Docker Compose `secrets:` blocks, K8s secret `volumeMount`s, and
|
|
103
|
+
* tmpfs paths populated by an ops wrapper script that fetched the
|
|
104
|
+
* payload from a vault (AWS Secrets Manager, Hetzner Vault, etc.)
|
|
105
|
+
* before plugin start.
|
|
106
|
+
*
|
|
107
|
+
* Both flavors are read-only — `save()` and `clear()` return `false` and
|
|
108
|
+
* the caller logs a warn. The secret manager is the canonical source;
|
|
109
|
+
* writing back would create two competing truths.
|
|
110
|
+
*/
|
|
111
|
+
export class ExternalCredentialProvider implements CredentialProvider {
|
|
112
|
+
readonly mode = 'external' as const;
|
|
113
|
+
|
|
114
|
+
constructor(
|
|
115
|
+
private readonly options: {
|
|
116
|
+
readonly inlineJson: string | null;
|
|
117
|
+
readonly filePath: string | null;
|
|
118
|
+
},
|
|
119
|
+
) {}
|
|
120
|
+
|
|
121
|
+
load(): CredentialsFile | null {
|
|
122
|
+
const raw = this.readRaw();
|
|
123
|
+
if (raw === null) return null;
|
|
124
|
+
try {
|
|
125
|
+
return JSON.parse(raw) as CredentialsFile;
|
|
126
|
+
} catch {
|
|
127
|
+
// Parse error — return null so the bootstrap path can decide how to
|
|
128
|
+
// surface (typically: log + fall through to fresh-generate, same as
|
|
129
|
+
// a corrupt credentials.json on disk). We deliberately do not echo
|
|
130
|
+
// the payload here even on parse failure.
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
save(_creds: CredentialsFile): boolean {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
clear(): boolean {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private readRaw(): string | null {
|
|
144
|
+
if (this.options.inlineJson !== null) {
|
|
145
|
+
return this.options.inlineJson;
|
|
146
|
+
}
|
|
147
|
+
if (this.options.filePath !== null) {
|
|
148
|
+
try {
|
|
149
|
+
if (!fs.existsSync(this.options.filePath)) return null;
|
|
150
|
+
return fs.readFileSync(this.options.filePath, 'utf-8');
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Factory — pick the configured provider. Reads from `CONFIG` (single
|
|
161
|
+
* source of truth for env vars) so tests can override by mutating the
|
|
162
|
+
* config snapshot or by passing explicit deps to the constructors.
|
|
163
|
+
*
|
|
164
|
+
* In `external` mode with neither transport set, we still return an
|
|
165
|
+
* `ExternalCredentialProvider` so the caller observes the configured
|
|
166
|
+
* mode + sees `null` from `load()`. The caller is responsible for
|
|
167
|
+
* surfacing the misconfiguration; we do not silently fall back to
|
|
168
|
+
* `file` mode (that would mask a deploy-time mistake by reading a
|
|
169
|
+
* stale `credentials.json` left on disk).
|
|
170
|
+
*/
|
|
171
|
+
export function getCredentialProvider(
|
|
172
|
+
config: Pick<
|
|
173
|
+
typeof CONFIG,
|
|
174
|
+
'credentialsProvider' | 'credentialsPath' | 'externalCredentialsJson' | 'externalCredentialsPath'
|
|
175
|
+
> = CONFIG,
|
|
176
|
+
): CredentialProvider {
|
|
177
|
+
if (config.credentialsProvider === 'external') {
|
|
178
|
+
return new ExternalCredentialProvider({
|
|
179
|
+
inlineJson: config.externalCredentialsJson,
|
|
180
|
+
filePath: config.externalCredentialsPath,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return new FileCredentialProvider(config.credentialsPath);
|
|
184
|
+
}
|
package/crypto.ts
CHANGED
|
@@ -1,161 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (!_wasm) _wasm = requireWasm('@totalreclaw/core');
|
|
30
|
-
return _wasm;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
// BIP-39 Validation
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Check if the input looks like a BIP-39 mnemonic (12 or 24 words).
|
|
39
|
-
*
|
|
40
|
-
* Lenient: accepts phrases where all words look like valid BIP-39 words
|
|
41
|
-
* (allows invalid checksums, which LLMs sometimes generate).
|
|
42
|
-
*/
|
|
43
|
-
export function isBip39Mnemonic(input: string): boolean {
|
|
44
|
-
const words = input.trim().split(/\s+/);
|
|
45
|
-
return words.length === 12 || words.length === 24;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Re-export for backward compatibility
|
|
49
|
-
export const validateMnemonic = isBip39Mnemonic;
|
|
50
|
-
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
|
-
// Key Derivation
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Derive auth, encryption, and dedup keys from a recovery phrase.
|
|
57
|
-
*
|
|
58
|
-
* Delegates to the WASM module for BIP-39 seed derivation and HKDF
|
|
59
|
-
* key separation. Uses the lenient variant for phrases where all words
|
|
60
|
-
* are valid but the checksum fails.
|
|
61
|
-
*
|
|
62
|
-
* @param password - BIP-39 12/24-word mnemonic
|
|
63
|
-
* @param existingSalt - Ignored for BIP-39 path (salt is deterministic)
|
|
64
|
-
*/
|
|
65
|
-
export function deriveKeys(
|
|
66
|
-
password: string,
|
|
67
|
-
existingSalt?: Buffer,
|
|
68
|
-
): { authKey: Buffer; encryptionKey: Buffer; dedupKey: Buffer; salt: Buffer } {
|
|
69
|
-
const trimmed = password.trim();
|
|
70
|
-
|
|
71
|
-
// Try strict validation first, fall back to lenient
|
|
72
|
-
let result: { auth_key: string; encryption_key: string; dedup_key: string; salt: string };
|
|
73
|
-
try {
|
|
74
|
-
result = getWasm().deriveKeysFromMnemonic(trimmed);
|
|
75
|
-
} catch {
|
|
76
|
-
result = getWasm().deriveKeysFromMnemonicLenient(trimmed);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
authKey: Buffer.from(result.auth_key, 'hex'),
|
|
81
|
-
encryptionKey: Buffer.from(result.encryption_key, 'hex'),
|
|
82
|
-
dedupKey: Buffer.from(result.dedup_key, 'hex'),
|
|
83
|
-
salt: Buffer.from(result.salt, 'hex'),
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ---------------------------------------------------------------------------
|
|
88
|
-
// LSH Seed Derivation
|
|
89
|
-
// ---------------------------------------------------------------------------
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Derive a 32-byte seed for the LSH hasher.
|
|
93
|
-
*
|
|
94
|
-
* Delegates to the WASM module.
|
|
95
|
-
*/
|
|
96
|
-
export function deriveLshSeed(
|
|
97
|
-
password: string,
|
|
98
|
-
salt: Buffer,
|
|
99
|
-
): Uint8Array {
|
|
100
|
-
const seedHex = getWasm().deriveLshSeed(password.trim(), salt.toString('hex'));
|
|
101
|
-
return new Uint8Array(Buffer.from(seedHex, 'hex'));
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ---------------------------------------------------------------------------
|
|
105
|
-
// Auth Key Hash
|
|
106
|
-
// ---------------------------------------------------------------------------
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Compute the SHA-256 hash of the auth key.
|
|
110
|
-
*/
|
|
111
|
-
export function computeAuthKeyHash(authKey: Buffer): string {
|
|
112
|
-
return getWasm().computeAuthKeyHash(authKey.toString('hex'));
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// ---------------------------------------------------------------------------
|
|
116
|
-
// XChaCha20-Poly1305 Encrypt / Decrypt
|
|
117
|
-
// ---------------------------------------------------------------------------
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Encrypt a UTF-8 plaintext string with XChaCha20-Poly1305.
|
|
121
|
-
*
|
|
122
|
-
* Wire format (base64-encoded):
|
|
123
|
-
* [nonce: 24 bytes][tag: 16 bytes][ciphertext: variable]
|
|
124
|
-
*/
|
|
125
|
-
export function encrypt(plaintext: string, encryptionKey: Buffer): string {
|
|
126
|
-
return getWasm().encrypt(plaintext, encryptionKey.toString('hex'));
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Decrypt a base64-encoded XChaCha20-Poly1305 blob back to a UTF-8 string.
|
|
131
|
-
*/
|
|
132
|
-
export function decrypt(encryptedBase64: string, encryptionKey: Buffer): string {
|
|
133
|
-
return getWasm().decrypt(encryptedBase64, encryptionKey.toString('hex'));
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ---------------------------------------------------------------------------
|
|
137
|
-
// Blind Indices
|
|
138
|
-
// ---------------------------------------------------------------------------
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Generate blind indices (SHA-256 hashes of tokens) for a text string.
|
|
142
|
-
*
|
|
143
|
-
* Delegates to the WASM module which performs tokenization, stemming,
|
|
144
|
-
* and SHA-256 hashing.
|
|
145
|
-
*/
|
|
146
|
-
export function generateBlindIndices(text: string): string[] {
|
|
147
|
-
return getWasm().generateBlindIndices(text);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// ---------------------------------------------------------------------------
|
|
151
|
-
// Content Fingerprint (Dedup)
|
|
152
|
-
// ---------------------------------------------------------------------------
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Compute an HMAC-SHA256 content fingerprint for exact-duplicate detection.
|
|
156
|
-
*
|
|
157
|
-
* @returns 64-character hex string.
|
|
158
|
-
*/
|
|
159
|
-
export function generateContentFingerprint(plaintext: string, dedupKey: Buffer): string {
|
|
160
|
-
return getWasm().generateContentFingerprint(plaintext, dedupKey.toString('hex'));
|
|
161
|
-
}
|
|
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 {
|
|
19
|
+
isBip39Mnemonic,
|
|
20
|
+
validateMnemonic,
|
|
21
|
+
deriveKeys,
|
|
22
|
+
deriveLshSeed,
|
|
23
|
+
computeAuthKeyHash,
|
|
24
|
+
encrypt,
|
|
25
|
+
decrypt,
|
|
26
|
+
generateBlindIndices,
|
|
27
|
+
generateContentFingerprint,
|
|
28
|
+
} from './vault-crypto.js';
|
package/dist/api-client.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TotalReclaw Plugin - HTTP API Client
|
|
3
3
|
*
|
|
4
|
-
* Communicates with the TotalReclaw server over JSON/HTTP.
|
|
5
|
-
*
|
|
4
|
+
* Communicates with the TotalReclaw server over JSON/HTTP. All wire I/O
|
|
5
|
+
* goes through `relay.ts` (the plugin's single network site); this module
|
|
6
|
+
* owns request/response shape, status-checking, and error context.
|
|
6
7
|
*
|
|
7
8
|
* All authenticated endpoints expect:
|
|
8
9
|
* Authorization: Bearer <hex-encoded-auth-key>
|
|
@@ -15,6 +16,7 @@
|
|
|
15
16
|
* is set. See `relay-headers.ts` and internal#127.
|
|
16
17
|
*/
|
|
17
18
|
import { buildRelayHeaders } from './relay-headers.js';
|
|
19
|
+
import { relayFetch } from './relay.js';
|
|
18
20
|
// ---------------------------------------------------------------------------
|
|
19
21
|
// API Client Factory
|
|
20
22
|
// ---------------------------------------------------------------------------
|
|
@@ -60,7 +62,8 @@ export function createApiClient(serverUrl) {
|
|
|
60
62
|
* @returns `{ user_id }` on success.
|
|
61
63
|
*/
|
|
62
64
|
async register(authKeyHash, saltHex) {
|
|
63
|
-
const res = await
|
|
65
|
+
const res = await relayFetch({
|
|
66
|
+
url: `${baseUrl}/v1/register`,
|
|
64
67
|
method: 'POST',
|
|
65
68
|
headers: buildRelayHeaders({ 'Content-Type': 'application/json' }),
|
|
66
69
|
body: JSON.stringify({ auth_key_hash: authKeyHash, salt: saltHex }),
|
|
@@ -84,7 +87,8 @@ export function createApiClient(serverUrl) {
|
|
|
84
87
|
* @param authKeyHex Hex-encoded raw auth key (64 chars) for Bearer header.
|
|
85
88
|
*/
|
|
86
89
|
async store(userId, facts, authKeyHex) {
|
|
87
|
-
const res = await
|
|
90
|
+
const res = await relayFetch({
|
|
91
|
+
url: `${baseUrl}/v1/store`,
|
|
88
92
|
method: 'POST',
|
|
89
93
|
headers: buildRelayHeaders({
|
|
90
94
|
'Content-Type': 'application/json',
|
|
@@ -113,7 +117,8 @@ export function createApiClient(serverUrl) {
|
|
|
113
117
|
* @returns Array of encrypted search candidates.
|
|
114
118
|
*/
|
|
115
119
|
async search(userId, trapdoors, maxCandidates, authKeyHex) {
|
|
116
|
-
const res = await
|
|
120
|
+
const res = await relayFetch({
|
|
121
|
+
url: `${baseUrl}/v1/search`,
|
|
117
122
|
method: 'POST',
|
|
118
123
|
headers: buildRelayHeaders({
|
|
119
124
|
'Content-Type': 'application/json',
|
|
@@ -140,7 +145,8 @@ export function createApiClient(serverUrl) {
|
|
|
140
145
|
* @param authKeyHex Hex-encoded raw auth key for Bearer header.
|
|
141
146
|
*/
|
|
142
147
|
async deleteFact(factId, authKeyHex) {
|
|
143
|
-
const res = await
|
|
148
|
+
const res = await relayFetch({
|
|
149
|
+
url: `${baseUrl}/v1/facts/${encodeURIComponent(factId)}`,
|
|
144
150
|
method: 'DELETE',
|
|
145
151
|
headers: buildRelayHeaders({
|
|
146
152
|
Authorization: `Bearer ${authKeyHex}`,
|
|
@@ -161,7 +167,8 @@ export function createApiClient(serverUrl) {
|
|
|
161
167
|
* @returns The number of facts that were actually deleted.
|
|
162
168
|
*/
|
|
163
169
|
async batchDelete(factIds, authKeyHex) {
|
|
164
|
-
const res = await
|
|
170
|
+
const res = await relayFetch({
|
|
171
|
+
url: `${baseUrl}/v1/facts/batch-delete`,
|
|
165
172
|
method: 'POST',
|
|
166
173
|
headers: buildRelayHeaders({
|
|
167
174
|
'Content-Type': 'application/json',
|
|
@@ -189,7 +196,8 @@ export function createApiClient(serverUrl) {
|
|
|
189
196
|
const params = new URLSearchParams({ limit: String(limit) });
|
|
190
197
|
if (cursor)
|
|
191
198
|
params.set('cursor', cursor);
|
|
192
|
-
const res = await
|
|
199
|
+
const res = await relayFetch({
|
|
200
|
+
url: `${baseUrl}/v1/export?${params.toString()}`,
|
|
193
201
|
method: 'GET',
|
|
194
202
|
headers: buildRelayHeaders({
|
|
195
203
|
Authorization: `Bearer ${authKeyHex}`,
|
|
@@ -215,7 +223,7 @@ export function createApiClient(serverUrl) {
|
|
|
215
223
|
*/
|
|
216
224
|
async health() {
|
|
217
225
|
try {
|
|
218
|
-
const res = await
|
|
226
|
+
const res = await relayFetch({ url: `${baseUrl}/health`, method: 'GET' });
|
|
219
227
|
return res.status === 200;
|
|
220
228
|
}
|
|
221
229
|
catch {
|