@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.
Files changed (79) hide show
  1. package/SKILL.md +33 -27
  2. package/api-client.ts +17 -9
  3. package/batch-gate.ts +42 -0
  4. package/billing-cache.ts +72 -23
  5. package/claims-helper.ts +2 -1
  6. package/config.ts +95 -13
  7. package/consolidation.ts +6 -13
  8. package/contradiction-sync.ts +19 -14
  9. package/credential-provider.ts +184 -0
  10. package/crypto.ts +27 -160
  11. package/dist/api-client.js +17 -9
  12. package/dist/batch-gate.js +40 -0
  13. package/dist/billing-cache.js +54 -24
  14. package/dist/claims-helper.js +2 -1
  15. package/dist/config.js +91 -13
  16. package/dist/consolidation.js +6 -15
  17. package/dist/contradiction-sync.js +15 -15
  18. package/dist/credential-provider.js +145 -0
  19. package/dist/crypto.js +17 -137
  20. package/dist/download-ux.js +11 -7
  21. package/dist/embedder-loader.js +266 -0
  22. package/dist/embedding.js +36 -3
  23. package/dist/entry.js +123 -0
  24. package/dist/fs-helpers.js +75 -379
  25. package/dist/import-adapters/gemini-adapter.js +29 -159
  26. package/dist/index.js +864 -2631
  27. package/dist/memory-runtime.js +459 -0
  28. package/dist/native-memory.js +123 -0
  29. package/dist/onboarding-cli.js +4 -8
  30. package/dist/pair-cli-relay.js +1 -8
  31. package/dist/pair-cli.js +1 -1
  32. package/dist/pair-crypto.js +16 -358
  33. package/dist/pair-http.js +147 -4
  34. package/dist/relay.js +140 -0
  35. package/dist/reranker.js +13 -8
  36. package/dist/semantic-dedup.js +5 -7
  37. package/dist/skill-register.js +97 -0
  38. package/dist/subgraph-search.js +3 -1
  39. package/dist/subgraph-store.js +348 -290
  40. package/dist/tool-gating.js +39 -26
  41. package/dist/tools.js +367 -0
  42. package/dist/tr-cli-export-helper.js +3 -3
  43. package/dist/tr-cli.js +65 -156
  44. package/dist/trajectory-poller.js +155 -9
  45. package/dist/vault-crypto.js +551 -0
  46. package/download-ux.ts +12 -6
  47. package/embedder-loader.ts +293 -1
  48. package/embedding.ts +43 -3
  49. package/entry.ts +132 -0
  50. package/fs-helpers.ts +93 -458
  51. package/import-adapters/gemini-adapter.ts +38 -183
  52. package/index.ts +912 -2917
  53. package/memory-runtime.ts +723 -0
  54. package/native-memory.ts +196 -0
  55. package/onboarding-cli.ts +3 -9
  56. package/openclaw.plugin.json +5 -17
  57. package/package.json +6 -3
  58. package/pair-cli-relay.ts +0 -9
  59. package/pair-cli.ts +1 -1
  60. package/pair-crypto.ts +41 -483
  61. package/pair-http.ts +194 -5
  62. package/postinstall.mjs +138 -0
  63. package/relay.ts +172 -0
  64. package/reranker.ts +13 -8
  65. package/semantic-dedup.ts +5 -6
  66. package/skill-register.ts +146 -0
  67. package/skill.json +1 -1
  68. package/subgraph-search.ts +3 -1
  69. package/subgraph-store.ts +367 -307
  70. package/tool-gating.ts +39 -26
  71. package/tools.ts +499 -0
  72. package/tr-cli-export-helper.ts +3 -3
  73. package/tr-cli.ts +69 -182
  74. package/trajectory-poller.ts +162 -10
  75. package/vault-crypto.ts +705 -0
  76. package/auto-pair-on-load.ts +0 -308
  77. package/dist/auto-pair-on-load.js +0 -197
  78. package/dist/pair-pending-injection.js +0 -125
  79. package/pair-pending-injection.ts +0 -205
@@ -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 = process.env.TOTALRECLAW_STATE_DIR;
149
- if (override && override.length > 0) return override;
150
- return path.join(os.homedir(), '.totalreclaw');
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
- Math.floor(nowUnixSeconds),
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 = (process.env.TOTALRECLAW_AUTO_RESOLVE_MODE ?? '').trim().toLowerCase();
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
- const raw = process.env.TOTALRECLAW_TUNING_MIN_INTERVAL_OVERRIDE_SECONDS;
1008
- if (raw === undefined || raw === null || raw === '') {
1009
- return TUNING_LOOP_MIN_INTERVAL_SECONDS;
1010
- }
1011
- const parsed = Number(raw);
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
- * TotalReclaw Plugin - Crypto Operations (WASM-backed)
3
- *
4
- * Thin re-exports over `@totalreclaw/core` WASM module. Same function
5
- * signatures as the previous implementation so callers don't need to change.
6
- *
7
- * The WASM module handles BIP-39 key derivation, XChaCha20-Poly1305 encrypt/
8
- * decrypt, SHA-256 blind indices, HMAC-SHA256 content fingerprints,
9
- * and LSH seed derivation.
10
- *
11
- * Key derivation chain (BIP-39 handled by WASM):
12
- * mnemonic -> BIP-39 PBKDF2 -> 512-bit seed
13
- * -> HKDF-SHA256(seed, salt, "totalreclaw-auth-key-v1", 32) -> authKey
14
- * -> HKDF-SHA256(seed, salt, "totalreclaw-encryption-key-v1", 32) -> encryptionKey
15
- * -> HKDF-SHA256(seed, salt, "openmemory-dedup-v1", 32) -> dedupKey
16
- */
17
-
18
- // Lazy-load WASM. Uses createRequire so this module loads cleanly under bare
19
- // Node ESM — the shipped `dist/index.js` declares `"type":"module"`, where
20
- // the CJS `require` global is undefined at runtime. Prior to the rc.21 fix
21
- // this file called bare `require('@totalreclaw/core')` and every consumer
22
- // died with `require is not defined`. Matches the pattern already used by
23
- // claims-helper / consolidation / contradiction-sync / digest-sync / pin /
24
- // retype-setscope. See issue #124.
25
- import { createRequire } from 'node:module';
26
- const requireWasm = createRequire(import.meta.url);
27
- let _wasm: typeof import('@totalreclaw/core') | null = null;
28
- function getWasm() {
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';
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * TotalReclaw Plugin - HTTP API Client
3
3
  *
4
- * Communicates with the TotalReclaw server over JSON/HTTP. Uses Node.js
5
- * built-in `fetch` (available since Node 18).
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 fetch(`${baseUrl}/v1/register`, {
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 fetch(`${baseUrl}/v1/store`, {
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 fetch(`${baseUrl}/v1/search`, {
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 fetch(`${baseUrl}/v1/facts/${encodeURIComponent(factId)}`, {
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 fetch(`${baseUrl}/v1/facts/batch-delete`, {
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 fetch(`${baseUrl}/v1/export?${params.toString()}`, {
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 fetch(`${baseUrl}/health`, { method: 'GET' });
226
+ const res = await relayFetch({ url: `${baseUrl}/health`, method: 'GET' });
219
227
  return res.status === 200;
220
228
  }
221
229
  catch {
@@ -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
+ };