totalreclaw 0.1.0__tar.gz

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 (42) hide show
  1. totalreclaw-0.1.0/PKG-INFO +135 -0
  2. totalreclaw-0.1.0/README.md +111 -0
  3. totalreclaw-0.1.0/pyproject.toml +32 -0
  4. totalreclaw-0.1.0/setup.cfg +4 -0
  5. totalreclaw-0.1.0/src/totalreclaw/__init__.py +5 -0
  6. totalreclaw-0.1.0/src/totalreclaw/client.py +242 -0
  7. totalreclaw-0.1.0/src/totalreclaw/crypto.py +122 -0
  8. totalreclaw-0.1.0/src/totalreclaw/embedding.py +119 -0
  9. totalreclaw-0.1.0/src/totalreclaw/hermes/__init__.py +75 -0
  10. totalreclaw-0.1.0/src/totalreclaw/hermes/debrief.py +172 -0
  11. totalreclaw-0.1.0/src/totalreclaw/hermes/extractor.py +212 -0
  12. totalreclaw-0.1.0/src/totalreclaw/hermes/hooks.py +307 -0
  13. totalreclaw-0.1.0/src/totalreclaw/hermes/llm_client.py +139 -0
  14. totalreclaw-0.1.0/src/totalreclaw/hermes/schemas.py +97 -0
  15. totalreclaw-0.1.0/src/totalreclaw/hermes/state.py +204 -0
  16. totalreclaw-0.1.0/src/totalreclaw/hermes/tools.py +139 -0
  17. totalreclaw-0.1.0/src/totalreclaw/lsh.py +65 -0
  18. totalreclaw-0.1.0/src/totalreclaw/operations.py +405 -0
  19. totalreclaw-0.1.0/src/totalreclaw/protobuf.py +150 -0
  20. totalreclaw-0.1.0/src/totalreclaw/relay.py +167 -0
  21. totalreclaw-0.1.0/src/totalreclaw/reranker.py +482 -0
  22. totalreclaw-0.1.0/src/totalreclaw/userop.py +491 -0
  23. totalreclaw-0.1.0/src/totalreclaw.egg-info/PKG-INFO +135 -0
  24. totalreclaw-0.1.0/src/totalreclaw.egg-info/SOURCES.txt +40 -0
  25. totalreclaw-0.1.0/src/totalreclaw.egg-info/dependency_links.txt +1 -0
  26. totalreclaw-0.1.0/src/totalreclaw.egg-info/entry_points.txt +2 -0
  27. totalreclaw-0.1.0/src/totalreclaw.egg-info/requires.txt +17 -0
  28. totalreclaw-0.1.0/src/totalreclaw.egg-info/top_level.txt +1 -0
  29. totalreclaw-0.1.0/tests/test_client.py +94 -0
  30. totalreclaw-0.1.0/tests/test_cross_client.py +80 -0
  31. totalreclaw-0.1.0/tests/test_crypto.py +127 -0
  32. totalreclaw-0.1.0/tests/test_debrief.py +129 -0
  33. totalreclaw-0.1.0/tests/test_embedding.py +55 -0
  34. totalreclaw-0.1.0/tests/test_extractor.py +515 -0
  35. totalreclaw-0.1.0/tests/test_hermes_plugin.py +497 -0
  36. totalreclaw-0.1.0/tests/test_lsh.py +70 -0
  37. totalreclaw-0.1.0/tests/test_operations.py +225 -0
  38. totalreclaw-0.1.0/tests/test_protobuf.py +477 -0
  39. totalreclaw-0.1.0/tests/test_relay.py +62 -0
  40. totalreclaw-0.1.0/tests/test_reranker.py +558 -0
  41. totalreclaw-0.1.0/tests/test_staging_integration.py +87 -0
  42. totalreclaw-0.1.0/tests/test_userop.py +202 -0
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: totalreclaw
3
+ Version: 0.1.0
4
+ Summary: End-to-end encrypted memory for AI agents — Python client
5
+ Author: TotalReclaw Team
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: totalreclaw-core>=0.1.0
10
+ Requires-Dist: httpx>=0.27
11
+ Requires-Dist: onnxruntime>=1.17
12
+ Requires-Dist: numpy>=1.26
13
+ Requires-Dist: PyStemmer>=2.2
14
+ Requires-Dist: transformers>=4.40
15
+ Requires-Dist: huggingface-hub>=0.23
16
+ Requires-Dist: eth-account>=0.13
17
+ Provides-Extra: hermes
18
+ Requires-Dist: hermes-agent>=0.5.0; extra == "hermes"
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=8.0; extra == "dev"
21
+ Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
22
+ Requires-Dist: pytest-xdist>=3.5; extra == "dev"
23
+ Requires-Dist: mnemonic>=0.21; extra == "dev"
24
+
25
+ # totalreclaw
26
+
27
+ End-to-end encrypted memory for AI agents -- the "password manager for AI memory."
28
+
29
+ Store, search, and recall memories across any AI agent with zero-knowledge encryption. Your data is encrypted on-device before it leaves -- the server never sees plaintext.
30
+
31
+ ## Features
32
+
33
+ - **End-to-end encrypted** -- AES-256-GCM encryption, HKDF key derivation from BIP-39 mnemonic
34
+ - **Portable** -- Same recovery phrase works across Hermes, OpenClaw, Claude Desktop, IronClaw
35
+ - **Local embeddings** -- Qwen3-Embedding-0.6B runs on-device (no API calls)
36
+ - **Hybrid search** -- BM25 + cosine similarity + RRF reranking
37
+ - **LSH bucketing** -- Locality-sensitive hashing for encrypted search
38
+ - **On-chain storage** -- Managed service stores on Gnosis/Base Sepolia via ERC-4337
39
+
40
+ ## Quick Start
41
+
42
+ ```bash
43
+ pip install totalreclaw
44
+ ```
45
+
46
+ ```python
47
+ import asyncio
48
+ from totalreclaw import TotalReclaw
49
+
50
+ async def main():
51
+ client = TotalReclaw(mnemonic="your twelve word recovery phrase here")
52
+
53
+ # Store a memory
54
+ fact_id = await client.remember("Pedro prefers dark mode for all editors")
55
+
56
+ # Search memories
57
+ results = await client.recall("What does Pedro prefer?")
58
+ for r in results:
59
+ print(f" [{r.rrf_score:.3f}] {r.text}")
60
+
61
+ # Delete a memory
62
+ await client.forget(fact_id)
63
+
64
+ # Export all memories
65
+ facts = await client.export_all()
66
+
67
+ # Check billing
68
+ status = await client.status()
69
+ print(f"Tier: {status.tier}, Used: {status.free_writes_used}/{status.free_writes_limit}")
70
+
71
+ await client.close()
72
+
73
+ asyncio.run(main())
74
+ ```
75
+
76
+ ## With Embeddings (Recommended)
77
+
78
+ For semantic search, install with embedding support:
79
+
80
+ ```bash
81
+ pip install totalreclaw
82
+ ```
83
+
84
+ ```python
85
+ from totalreclaw import TotalReclaw
86
+ from totalreclaw.embedding import get_embedding
87
+
88
+ client = TotalReclaw(mnemonic="...")
89
+
90
+ # Store with embedding for semantic search
91
+ text = "Pedro prefers dark mode"
92
+ embedding = get_embedding(text)
93
+ await client.remember(text, embedding=embedding)
94
+
95
+ # Search with embedding
96
+ query = "What are Pedro's UI preferences?"
97
+ query_emb = get_embedding(query)
98
+ results = await client.recall(query, query_embedding=query_emb)
99
+ ```
100
+
101
+ The embedding model (~600 MB) downloads automatically on first use.
102
+
103
+ ## Hermes Agent Plugin
104
+
105
+ ```bash
106
+ pip install totalreclaw[hermes]
107
+ ```
108
+
109
+ The plugin registers automatically with Hermes Agent v0.5.0+. See the [Hermes setup guide](https://github.com/p-diogo/totalreclaw/blob/main/docs/guides/hermes-setup.md).
110
+
111
+ ## Architecture
112
+
113
+ ```
114
+ Plaintext → AES-256-GCM encrypt → Blind indices (SHA-256) → LSH buckets → On-chain via relay
115
+
116
+ Query → Blind trapdoors → GraphQL search → Decrypt candidates → BM25+Cosine+RRF rerank → Top 8
117
+ ```
118
+
119
+ All encryption happens client-side. The relay server and on-chain storage never see plaintext.
120
+
121
+ ## Cross-Language Parity
122
+
123
+ This Python client produces byte-for-byte identical outputs to the TypeScript implementation (`@totalreclaw/mcp-server`):
124
+
125
+ - Key derivation (HKDF-SHA256)
126
+ - AES-256-GCM wire format (iv || tag || ciphertext)
127
+ - Blind indices (SHA-256 + Porter stemming)
128
+ - Content fingerprints (HMAC-SHA256)
129
+ - LSH bucket hashes (32-bit x 20 tables)
130
+
131
+ Memories stored by the Python client can be recalled by the MCP server, and vice versa.
132
+
133
+ ## License
134
+
135
+ MIT
@@ -0,0 +1,111 @@
1
+ # totalreclaw
2
+
3
+ End-to-end encrypted memory for AI agents -- the "password manager for AI memory."
4
+
5
+ Store, search, and recall memories across any AI agent with zero-knowledge encryption. Your data is encrypted on-device before it leaves -- the server never sees plaintext.
6
+
7
+ ## Features
8
+
9
+ - **End-to-end encrypted** -- AES-256-GCM encryption, HKDF key derivation from BIP-39 mnemonic
10
+ - **Portable** -- Same recovery phrase works across Hermes, OpenClaw, Claude Desktop, IronClaw
11
+ - **Local embeddings** -- Qwen3-Embedding-0.6B runs on-device (no API calls)
12
+ - **Hybrid search** -- BM25 + cosine similarity + RRF reranking
13
+ - **LSH bucketing** -- Locality-sensitive hashing for encrypted search
14
+ - **On-chain storage** -- Managed service stores on Gnosis/Base Sepolia via ERC-4337
15
+
16
+ ## Quick Start
17
+
18
+ ```bash
19
+ pip install totalreclaw
20
+ ```
21
+
22
+ ```python
23
+ import asyncio
24
+ from totalreclaw import TotalReclaw
25
+
26
+ async def main():
27
+ client = TotalReclaw(mnemonic="your twelve word recovery phrase here")
28
+
29
+ # Store a memory
30
+ fact_id = await client.remember("Pedro prefers dark mode for all editors")
31
+
32
+ # Search memories
33
+ results = await client.recall("What does Pedro prefer?")
34
+ for r in results:
35
+ print(f" [{r.rrf_score:.3f}] {r.text}")
36
+
37
+ # Delete a memory
38
+ await client.forget(fact_id)
39
+
40
+ # Export all memories
41
+ facts = await client.export_all()
42
+
43
+ # Check billing
44
+ status = await client.status()
45
+ print(f"Tier: {status.tier}, Used: {status.free_writes_used}/{status.free_writes_limit}")
46
+
47
+ await client.close()
48
+
49
+ asyncio.run(main())
50
+ ```
51
+
52
+ ## With Embeddings (Recommended)
53
+
54
+ For semantic search, install with embedding support:
55
+
56
+ ```bash
57
+ pip install totalreclaw
58
+ ```
59
+
60
+ ```python
61
+ from totalreclaw import TotalReclaw
62
+ from totalreclaw.embedding import get_embedding
63
+
64
+ client = TotalReclaw(mnemonic="...")
65
+
66
+ # Store with embedding for semantic search
67
+ text = "Pedro prefers dark mode"
68
+ embedding = get_embedding(text)
69
+ await client.remember(text, embedding=embedding)
70
+
71
+ # Search with embedding
72
+ query = "What are Pedro's UI preferences?"
73
+ query_emb = get_embedding(query)
74
+ results = await client.recall(query, query_embedding=query_emb)
75
+ ```
76
+
77
+ The embedding model (~600 MB) downloads automatically on first use.
78
+
79
+ ## Hermes Agent Plugin
80
+
81
+ ```bash
82
+ pip install totalreclaw[hermes]
83
+ ```
84
+
85
+ The plugin registers automatically with Hermes Agent v0.5.0+. See the [Hermes setup guide](https://github.com/p-diogo/totalreclaw/blob/main/docs/guides/hermes-setup.md).
86
+
87
+ ## Architecture
88
+
89
+ ```
90
+ Plaintext → AES-256-GCM encrypt → Blind indices (SHA-256) → LSH buckets → On-chain via relay
91
+
92
+ Query → Blind trapdoors → GraphQL search → Decrypt candidates → BM25+Cosine+RRF rerank → Top 8
93
+ ```
94
+
95
+ All encryption happens client-side. The relay server and on-chain storage never see plaintext.
96
+
97
+ ## Cross-Language Parity
98
+
99
+ This Python client produces byte-for-byte identical outputs to the TypeScript implementation (`@totalreclaw/mcp-server`):
100
+
101
+ - Key derivation (HKDF-SHA256)
102
+ - AES-256-GCM wire format (iv || tag || ciphertext)
103
+ - Blind indices (SHA-256 + Porter stemming)
104
+ - Content fingerprints (HMAC-SHA256)
105
+ - LSH bucket hashes (32-bit x 20 tables)
106
+
107
+ Memories stored by the Python client can be recalled by the MCP server, and vice versa.
108
+
109
+ ## License
110
+
111
+ MIT
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "totalreclaw"
7
+ version = "0.1.0"
8
+ description = "End-to-end encrypted memory for AI agents — Python client"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.11"
12
+ authors = [{ name = "TotalReclaw Team" }]
13
+ dependencies = [
14
+ "totalreclaw-core>=0.1.0",
15
+ "httpx>=0.27",
16
+ "onnxruntime>=1.17",
17
+ "numpy>=1.26",
18
+ "PyStemmer>=2.2",
19
+ "transformers>=4.40",
20
+ "huggingface-hub>=0.23",
21
+ "eth-account>=0.13",
22
+ ]
23
+
24
+ [project.optional-dependencies]
25
+ hermes = ["hermes-agent>=0.5.0"]
26
+ dev = ["pytest>=8.0", "pytest-asyncio>=0.24", "pytest-xdist>=3.5", "mnemonic>=0.21"]
27
+
28
+ [project.entry-points."hermes_agent.plugins"]
29
+ totalreclaw = "totalreclaw.hermes"
30
+
31
+ [tool.pytest.ini_options]
32
+ asyncio_mode = "auto"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """TotalReclaw — End-to-end encrypted memory for AI agents."""
2
+
3
+ from .client import TotalReclaw
4
+
5
+ __all__ = ["TotalReclaw"]
@@ -0,0 +1,242 @@
1
+ """
2
+ TotalReclaw Client -- High-level API.
3
+
4
+ Usage:
5
+ from totalreclaw import TotalReclaw
6
+
7
+ client = TotalReclaw(mnemonic="abandon abandon ...")
8
+ await client.remember("Pedro prefers dark mode")
9
+ results = await client.recall("What does Pedro prefer?")
10
+ await client.forget(results[0].id)
11
+ facts = await client.export_all()
12
+ status = await client.status()
13
+ await client.close()
14
+ """
15
+ from __future__ import annotations
16
+ from typing import Optional
17
+
18
+ from .crypto import (
19
+ derive_keys_from_mnemonic,
20
+ derive_lsh_seed,
21
+ compute_auth_key_hash,
22
+ DerivedKeys,
23
+ )
24
+ from .lsh import LSHHasher
25
+ from .relay import RelayClient, BillingStatus, DEFAULT_RELAY_URL
26
+ from .reranker import RerankerResult
27
+ from .operations import store_fact, search_facts, forget_fact, export_facts
28
+
29
+ # Smart Account address derivation constants
30
+ # These match the CREATE2 deterministic address generation used by
31
+ # SimpleSmartAccount in the permissionless library
32
+ SIMPLE_ACCOUNT_FACTORY = "0x91E60e0613810449d098b0b5Ec8b51A0FE8c8985"
33
+ ENTRYPOINT_V07 = "0x0000000071727De22E5E9d8BAf0edAc6f37da032"
34
+
35
+
36
+ def _get_eoa_account(mnemonic: str):
37
+ """Derive the EOA (externally-owned account) from a BIP-39 mnemonic.
38
+
39
+ Returns the full ``LocalAccount`` so callers can access both address and
40
+ private key.
41
+ """
42
+ from eth_account import Account
43
+ Account.enable_unaudited_hdwallet_features()
44
+ return Account.from_mnemonic(mnemonic.strip(), account_path="m/44'/60'/0'/0/0")
45
+
46
+
47
+ def _get_eoa_address(mnemonic: str) -> str:
48
+ """Derive the EOA address from a BIP-39 mnemonic."""
49
+ return _get_eoa_account(mnemonic).address
50
+
51
+
52
+ async def _derive_smart_account_address(mnemonic: str, rpc_url: str = "https://sepolia.base.org") -> str:
53
+ """Derive the CREATE2 Smart Account address by querying the factory contract.
54
+
55
+ Calls SimpleAccountFactory.getAddress(owner, 0) via eth_call on a public RPC.
56
+ This matches the `toSimpleSmartAccount` logic in permissionless/viem.
57
+
58
+ Factory: 0x91E60e0613810449d098b0b5Ec8b51A0FE8c8985 (v0.7)
59
+ Method: getAddress(address owner, uint256 salt) view returns (address)
60
+ """
61
+ import httpx
62
+
63
+ eoa_address = _get_eoa_address(mnemonic)
64
+
65
+ # ABI-encode: getAddress(address,uint256)
66
+ # keccak256("getAddress(address,uint256)")[:4] = 0x8cb84e18
67
+ selector = "8cb84e18"
68
+
69
+ # Pad owner address to 32 bytes and salt (0) to 32 bytes
70
+ owner_padded = eoa_address.lower().replace("0x", "").zfill(64)
71
+ salt_padded = "0" * 64
72
+
73
+ calldata = f"0x{selector}{owner_padded}{salt_padded}"
74
+
75
+ async with httpx.AsyncClient(timeout=15.0) as client:
76
+ resp = await client.post(
77
+ rpc_url,
78
+ json={
79
+ "jsonrpc": "2.0",
80
+ "method": "eth_call",
81
+ "params": [
82
+ {"to": SIMPLE_ACCOUNT_FACTORY, "data": calldata},
83
+ "latest",
84
+ ],
85
+ "id": 1,
86
+ },
87
+ )
88
+ result = resp.json().get("result", "")
89
+ if not result or result == "0x" or len(result) < 66:
90
+ # Fallback to EOA if RPC fails
91
+ return eoa_address.lower()
92
+ # Result is ABI-encoded address (32 bytes, last 20 bytes are the address)
93
+ address = "0x" + result[-40:]
94
+ return address.lower()
95
+
96
+
97
+ class TotalReclaw:
98
+ """High-level TotalReclaw client with remember/recall/forget/export/status.
99
+
100
+ The wallet_address is the CREATE2 Smart Account address. If not provided,
101
+ call `await client.resolve_address()` before using remember/recall/forget/export
102
+ to derive it via an RPC call to the SimpleAccountFactory contract.
103
+ """
104
+
105
+ def __init__(
106
+ self,
107
+ mnemonic: str,
108
+ relay_url: str = DEFAULT_RELAY_URL,
109
+ wallet_address: Optional[str] = None,
110
+ is_test: bool = False,
111
+ ):
112
+ self._mnemonic = mnemonic.strip()
113
+ self._keys = derive_keys_from_mnemonic(self._mnemonic)
114
+ self._lsh_seed = derive_lsh_seed(self._mnemonic, self._keys.salt)
115
+ self._lsh_hasher: Optional[LSHHasher] = None
116
+ self._auth_key_hex = self._keys.auth_key.hex()
117
+ self._relay_url = relay_url
118
+
119
+ # Derive EOA account (address + private key) for UserOp signing
120
+ eoa_acct = _get_eoa_account(self._mnemonic)
121
+ self._eoa_address: str = eoa_acct.address
122
+ self._eoa_private_key: bytes = bytes(eoa_acct.key)
123
+
124
+ # Use provided address, or fall back to EOA (resolve_address fixes it later)
125
+ self._wallet_address = (wallet_address or self._eoa_address).lower()
126
+ self._address_resolved = wallet_address is not None
127
+ self._relay = RelayClient(
128
+ relay_url=relay_url,
129
+ auth_key_hex=self._auth_key_hex,
130
+ wallet_address=self._wallet_address,
131
+ is_test=is_test,
132
+ )
133
+ self._registered = False
134
+
135
+ async def resolve_address(self) -> str:
136
+ """Resolve the CREATE2 Smart Account address via RPC.
137
+
138
+ Must be called before remember/recall/forget/export if wallet_address
139
+ was not provided at construction time.
140
+ """
141
+ if self._address_resolved:
142
+ return self._wallet_address
143
+
144
+ self._wallet_address = await _derive_smart_account_address(self._mnemonic)
145
+ self._address_resolved = True
146
+ # Update relay client with resolved address
147
+ self._relay._wallet_address = self._wallet_address
148
+ return self._wallet_address
149
+
150
+ async def _ensure_address(self) -> None:
151
+ """Lazily resolve the Smart Account address if not yet done."""
152
+ if not self._address_resolved:
153
+ await self.resolve_address()
154
+
155
+ def _get_lsh_hasher(self, dims: int = 1024) -> LSHHasher:
156
+ if self._lsh_hasher is None:
157
+ self._lsh_hasher = LSHHasher(self._lsh_seed, dims)
158
+ return self._lsh_hasher
159
+
160
+ @property
161
+ def wallet_address(self) -> str:
162
+ return self._wallet_address
163
+
164
+ @property
165
+ def keys(self) -> DerivedKeys:
166
+ return self._keys
167
+
168
+ async def register(self) -> str:
169
+ """Register with the relay. Returns user_id."""
170
+ auth_hash = compute_auth_key_hash(self._keys.auth_key)
171
+ user_id = await self._relay.register(auth_hash, self._keys.salt.hex())
172
+ self._registered = True
173
+ return user_id
174
+
175
+ async def remember(
176
+ self,
177
+ text: str,
178
+ embedding: Optional[list[float]] = None,
179
+ importance: float = 0.5,
180
+ source: str = "python-client",
181
+ ) -> str:
182
+ """Store a fact. Returns the fact ID."""
183
+ await self._ensure_address()
184
+ lsh = self._get_lsh_hasher() if embedding else None
185
+ return await store_fact(
186
+ text=text,
187
+ keys=self._keys,
188
+ owner=self._wallet_address,
189
+ relay=self._relay,
190
+ lsh_hasher=lsh,
191
+ embedding=embedding,
192
+ importance=importance,
193
+ source=source,
194
+ eoa_private_key=self._eoa_private_key,
195
+ eoa_address=self._eoa_address,
196
+ sender=self._wallet_address,
197
+ )
198
+
199
+ async def recall(
200
+ self,
201
+ query: str,
202
+ query_embedding: Optional[list[float]] = None,
203
+ max_candidates: int = 250,
204
+ top_k: int = 8,
205
+ ) -> list[RerankerResult]:
206
+ """Search for facts matching a query. Returns ranked results."""
207
+ await self._ensure_address()
208
+ return await search_facts(
209
+ query=query,
210
+ keys=self._keys,
211
+ owner=self._wallet_address,
212
+ relay=self._relay,
213
+ query_embedding=query_embedding,
214
+ max_candidates=max_candidates,
215
+ top_k=top_k,
216
+ )
217
+
218
+ async def forget(self, fact_id: str) -> bool:
219
+ """Soft-delete a fact by writing a tombstone."""
220
+ await self._ensure_address()
221
+ return await forget_fact(
222
+ fact_id,
223
+ self._wallet_address,
224
+ self._relay,
225
+ eoa_private_key=self._eoa_private_key,
226
+ eoa_address=self._eoa_address,
227
+ sender=self._wallet_address,
228
+ )
229
+
230
+ async def export_all(self) -> list[dict]:
231
+ """Export all active facts, decrypted."""
232
+ await self._ensure_address()
233
+ return await export_facts(self._keys, self._wallet_address, self._relay)
234
+
235
+ async def status(self) -> BillingStatus:
236
+ """Get billing status."""
237
+ await self._ensure_address()
238
+ return await self._relay.get_billing_status()
239
+
240
+ async def close(self):
241
+ """Close the HTTP client."""
242
+ await self._relay.close()
@@ -0,0 +1,122 @@
1
+ """
2
+ TotalReclaw E2EE crypto primitives.
3
+
4
+ Delegates to the totalreclaw_core Rust/PyO3 module for all crypto operations.
5
+ Maintains the same Python API for backward compatibility.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import base64
10
+ import struct
11
+ from dataclasses import dataclass
12
+
13
+ import totalreclaw_core
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Key Derivation
17
+ # ---------------------------------------------------------------------------
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class DerivedKeys:
22
+ salt: bytes
23
+ auth_key: bytes
24
+ encryption_key: bytes
25
+ dedup_key: bytes
26
+
27
+
28
+ def derive_keys_from_mnemonic(mnemonic: str) -> DerivedKeys:
29
+ """Derive all crypto keys from a BIP-39 mnemonic.
30
+
31
+ Delegates to totalreclaw_core (Rust/PyO3).
32
+ """
33
+ result = totalreclaw_core.derive_keys_from_mnemonic(mnemonic)
34
+ return DerivedKeys(
35
+ salt=result["salt"],
36
+ auth_key=result["auth_key"],
37
+ encryption_key=result["encryption_key"],
38
+ dedup_key=result["dedup_key"],
39
+ )
40
+
41
+
42
+ def compute_auth_key_hash(auth_key: bytes) -> str:
43
+ """SHA-256(authKey) as hex string."""
44
+ return totalreclaw_core.compute_auth_key_hash(auth_key)
45
+
46
+
47
+ def derive_lsh_seed(mnemonic: str, salt: bytes) -> bytes:
48
+ """Derive the 32-byte LSH seed."""
49
+ return totalreclaw_core.derive_lsh_seed(mnemonic, salt)
50
+
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # AES-256-GCM Encryption / Decryption
54
+ # ---------------------------------------------------------------------------
55
+
56
+
57
+ def encrypt(plaintext: str, encryption_key: bytes) -> str:
58
+ """Encrypt with AES-256-GCM.
59
+
60
+ Wire format: iv(12) || tag(16) || ciphertext -> base64.
61
+ """
62
+ return totalreclaw_core.encrypt(plaintext, encryption_key)
63
+
64
+
65
+ def decrypt(encrypted_base64: str, encryption_key: bytes) -> str:
66
+ """Decrypt AES-256-GCM.
67
+
68
+ Expects wire format: iv(12) || tag(16) || ciphertext.
69
+ """
70
+ return totalreclaw_core.decrypt(encrypted_base64, encryption_key)
71
+
72
+
73
+ # ---------------------------------------------------------------------------
74
+ # Blind Indices + Stemming
75
+ # ---------------------------------------------------------------------------
76
+
77
+
78
+ def generate_blind_indices(text: str) -> list[str]:
79
+ """Generate blind indices (SHA-256 hashes of tokens).
80
+
81
+ Tokenization rules (matches canonical TypeScript/Rust implementation):
82
+ 1. Lowercase
83
+ 2. Remove punctuation (keep Unicode letters, numbers, whitespace)
84
+ 3. Split on whitespace
85
+ 4. Filter tokens shorter than 2 characters
86
+
87
+ Each surviving token is SHA-256 hashed and returned as a hex string.
88
+ Stemmed variants are prefixed with "stem:" before hashing.
89
+ The returned array is deduplicated.
90
+ """
91
+ return totalreclaw_core.generate_blind_indices(text)
92
+
93
+
94
+ # ---------------------------------------------------------------------------
95
+ # Content Fingerprint (Dedup)
96
+ # ---------------------------------------------------------------------------
97
+
98
+
99
+ def generate_content_fingerprint(plaintext: str, dedup_key: bytes) -> str:
100
+ """HMAC-SHA256 content fingerprint. Returns 64-char hex string."""
101
+ return totalreclaw_core.generate_content_fingerprint(plaintext, dedup_key)
102
+
103
+
104
+ # ---------------------------------------------------------------------------
105
+ # Embedding Encryption
106
+ # ---------------------------------------------------------------------------
107
+
108
+
109
+ def encrypt_embedding(embedding: list[float], encryption_key: bytes) -> str:
110
+ """Encrypt embedding: pack as LE float32 array -> base64 -> AES encrypt."""
111
+ buf = struct.pack(f"<{len(embedding)}f", *embedding)
112
+ return encrypt(base64.b64encode(buf).decode("ascii"), encryption_key)
113
+
114
+
115
+ def decrypt_embedding(
116
+ encrypted_embedding: str, encryption_key: bytes
117
+ ) -> list[float]:
118
+ """Decrypt embedding back to float array."""
119
+ decrypted_base64 = decrypt(encrypted_embedding, encryption_key)
120
+ buf = base64.b64decode(decrypted_base64)
121
+ count = len(buf) // 4
122
+ return list(struct.unpack(f"<{count}f", buf))