memwal 0.1.0.dev0__tar.gz → 0.1.0.dev2__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.
- {memwal-0.1.0.dev0 → memwal-0.1.0.dev2}/PKG-INFO +29 -3
- {memwal-0.1.0.dev0 → memwal-0.1.0.dev2}/README.md +28 -2
- {memwal-0.1.0.dev0 → memwal-0.1.0.dev2}/examples/async_remember_demo.py +2 -0
- {memwal-0.1.0.dev0 → memwal-0.1.0.dev2}/memwal/__init__.py +3 -1
- {memwal-0.1.0.dev0 → memwal-0.1.0.dev2}/memwal/client.py +18 -8
- {memwal-0.1.0.dev0 → memwal-0.1.0.dev2}/memwal/middleware.py +14 -2
- {memwal-0.1.0.dev0 → memwal-0.1.0.dev2}/memwal/types.py +36 -2
- {memwal-0.1.0.dev0 → memwal-0.1.0.dev2}/pyproject.toml +1 -1
- {memwal-0.1.0.dev0 → memwal-0.1.0.dev2}/tests/test_client.py +9 -2
- memwal-0.1.0.dev2/tests/test_env_presets.py +71 -0
- {memwal-0.1.0.dev0 → memwal-0.1.0.dev2}/tests/test_integration.py +13 -1
- {memwal-0.1.0.dev0 → memwal-0.1.0.dev2}/tests/test_middleware.py +4 -1
- {memwal-0.1.0.dev0 → memwal-0.1.0.dev2}/tests/test_signing.py +12 -17
- {memwal-0.1.0.dev0 → memwal-0.1.0.dev2}/.gitignore +0 -0
- {memwal-0.1.0.dev0 → memwal-0.1.0.dev2}/examples/.env.example +0 -0
- {memwal-0.1.0.dev0 → memwal-0.1.0.dev2}/examples/.gitignore +0 -0
- {memwal-0.1.0.dev0 → memwal-0.1.0.dev2}/examples/interactive_demo.py +0 -0
- {memwal-0.1.0.dev0 → memwal-0.1.0.dev2}/memwal/utils.py +0 -0
- {memwal-0.1.0.dev0 → memwal-0.1.0.dev2}/run_tests.py +0 -0
- {memwal-0.1.0.dev0 → memwal-0.1.0.dev2}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memwal
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev2
|
|
4
4
|
Summary: Python SDK for MemWal — Privacy-first AI memory with Ed25519 signing
|
|
5
5
|
Project-URL: Homepage, https://memwal.ai
|
|
6
6
|
Project-URL: Documentation, https://docs.memwal.ai
|
|
@@ -130,6 +130,32 @@ async with MemWal.create(
|
|
|
130
130
|
await memwal.remember("I prefer dark mode")
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
+
## Environment Presets
|
|
134
|
+
|
|
135
|
+
Instead of hardcoding a relayer URL, pass `env` to target a hosted relayer.
|
|
136
|
+
Same shorthand as the TypeScript SDK and MCP package.
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from memwal import MemWal
|
|
140
|
+
|
|
141
|
+
memwal = MemWal.create(
|
|
142
|
+
key=os.environ["MEMWAL_KEY"],
|
|
143
|
+
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
144
|
+
env="prod", # prod | dev | staging | local
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
| `env` | Relayer URL |
|
|
149
|
+
|-------|-------------|
|
|
150
|
+
| `prod` | `https://relayer.memwal.ai` |
|
|
151
|
+
| `dev` | `https://relayer.dev.memwal.ai` |
|
|
152
|
+
| `staging` | `https://relayer.staging.memwal.ai` |
|
|
153
|
+
| `local` | `http://127.0.0.1:8000` |
|
|
154
|
+
|
|
155
|
+
Precedence: an explicit non-default **`server_url` wins over `env`**, which wins
|
|
156
|
+
over the default. An unknown preset raises `ValueError`. `env` is also accepted
|
|
157
|
+
by `MemWalSync.create`, `with_memwal_langchain`, and `with_memwal_openai`.
|
|
158
|
+
|
|
133
159
|
## AI Middleware
|
|
134
160
|
|
|
135
161
|
### LangChain
|
|
@@ -201,10 +227,10 @@ Create a new async client.
|
|
|
201
227
|
Every request is signed with Ed25519:
|
|
202
228
|
|
|
203
229
|
```
|
|
204
|
-
message = f"{timestamp}.{method}.{path}.{sha256(body)}
|
|
230
|
+
message = f"{timestamp}.{method}.{path}.{sha256(body)}"
|
|
205
231
|
```
|
|
206
232
|
|
|
207
|
-
Headers sent: `x-public-key`, `x-signature`, `x-timestamp`, `x-
|
|
233
|
+
Headers sent: `x-public-key`, `x-signature`, `x-timestamp`, `x-delegate-key`, `x-account-id`.
|
|
208
234
|
|
|
209
235
|
## License
|
|
210
236
|
|
|
@@ -91,6 +91,32 @@ async with MemWal.create(
|
|
|
91
91
|
await memwal.remember("I prefer dark mode")
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
## Environment Presets
|
|
95
|
+
|
|
96
|
+
Instead of hardcoding a relayer URL, pass `env` to target a hosted relayer.
|
|
97
|
+
Same shorthand as the TypeScript SDK and MCP package.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from memwal import MemWal
|
|
101
|
+
|
|
102
|
+
memwal = MemWal.create(
|
|
103
|
+
key=os.environ["MEMWAL_KEY"],
|
|
104
|
+
account_id=os.environ["MEMWAL_ACCOUNT_ID"],
|
|
105
|
+
env="prod", # prod | dev | staging | local
|
|
106
|
+
)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
| `env` | Relayer URL |
|
|
110
|
+
|-------|-------------|
|
|
111
|
+
| `prod` | `https://relayer.memwal.ai` |
|
|
112
|
+
| `dev` | `https://relayer.dev.memwal.ai` |
|
|
113
|
+
| `staging` | `https://relayer.staging.memwal.ai` |
|
|
114
|
+
| `local` | `http://127.0.0.1:8000` |
|
|
115
|
+
|
|
116
|
+
Precedence: an explicit non-default **`server_url` wins over `env`**, which wins
|
|
117
|
+
over the default. An unknown preset raises `ValueError`. `env` is also accepted
|
|
118
|
+
by `MemWalSync.create`, `with_memwal_langchain`, and `with_memwal_openai`.
|
|
119
|
+
|
|
94
120
|
## AI Middleware
|
|
95
121
|
|
|
96
122
|
### LangChain
|
|
@@ -162,10 +188,10 @@ Create a new async client.
|
|
|
162
188
|
Every request is signed with Ed25519:
|
|
163
189
|
|
|
164
190
|
```
|
|
165
|
-
message = f"{timestamp}.{method}.{path}.{sha256(body)}
|
|
191
|
+
message = f"{timestamp}.{method}.{path}.{sha256(body)}"
|
|
166
192
|
```
|
|
167
193
|
|
|
168
|
-
Headers sent: `x-public-key`, `x-signature`, `x-timestamp`, `x-
|
|
194
|
+
Headers sent: `x-public-key`, `x-signature`, `x-timestamp`, `x-delegate-key`, `x-account-id`.
|
|
169
195
|
|
|
170
196
|
## License
|
|
171
197
|
|
|
@@ -25,6 +25,7 @@ import os
|
|
|
25
25
|
import sys
|
|
26
26
|
import time
|
|
27
27
|
from pathlib import Path
|
|
28
|
+
from typing import Optional
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
def _load_env() -> None:
|
|
@@ -49,6 +50,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
49
50
|
|
|
50
51
|
from memwal import ( # noqa: E402
|
|
51
52
|
MemWal,
|
|
53
|
+
MemWalRememberJobFailed,
|
|
52
54
|
RememberBulkItem,
|
|
53
55
|
RememberBulkOptions,
|
|
54
56
|
)
|
|
@@ -33,6 +33,7 @@ from .client import (
|
|
|
33
33
|
from .middleware import with_memwal_langchain, with_memwal_openai
|
|
34
34
|
from .utils import delegate_key_to_sui_address, delegate_key_to_public_key
|
|
35
35
|
from .types import (
|
|
36
|
+
ENV_PRESETS,
|
|
36
37
|
AnalyzedFact,
|
|
37
38
|
AnalyzeResult,
|
|
38
39
|
AnalyzeWaitResult,
|
|
@@ -81,6 +82,7 @@ __all__ = [
|
|
|
81
82
|
"withMemWal",
|
|
82
83
|
# Types
|
|
83
84
|
"MemWalConfig",
|
|
85
|
+
"ENV_PRESETS",
|
|
84
86
|
"AskMemory",
|
|
85
87
|
"AskResult",
|
|
86
88
|
"RememberResult",
|
|
@@ -108,4 +110,4 @@ __all__ = [
|
|
|
108
110
|
"RecallManualResult",
|
|
109
111
|
]
|
|
110
112
|
|
|
111
|
-
__version__ = "0.1.0.
|
|
113
|
+
__version__ = "0.1.0.dev2"
|
|
@@ -29,7 +29,7 @@ import asyncio
|
|
|
29
29
|
import json
|
|
30
30
|
import random
|
|
31
31
|
import time
|
|
32
|
-
from typing import Any, Dict, List, Optional, Sequence, TypeVar
|
|
32
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple, TypeVar
|
|
33
33
|
|
|
34
34
|
import httpx
|
|
35
35
|
|
|
@@ -55,6 +55,7 @@ from .types import (
|
|
|
55
55
|
RememberBulkResult,
|
|
56
56
|
RememberBulkStatusItem,
|
|
57
57
|
RememberBulkStatusResult,
|
|
58
|
+
RememberJobStatus,
|
|
58
59
|
RememberManualOptions,
|
|
59
60
|
RememberManualResult,
|
|
60
61
|
RememberResult,
|
|
@@ -128,16 +129,21 @@ class MemWal:
|
|
|
128
129
|
cls,
|
|
129
130
|
key: str,
|
|
130
131
|
account_id: str,
|
|
131
|
-
server_url: str = "
|
|
132
|
+
server_url: str = "http://localhost:8000",
|
|
132
133
|
namespace: str = "default",
|
|
134
|
+
env: Optional[str] = None,
|
|
133
135
|
) -> "MemWal":
|
|
134
136
|
"""Create a new MemWal client instance.
|
|
135
137
|
|
|
136
138
|
Args:
|
|
137
139
|
key: Ed25519 private key hex string (the delegate key).
|
|
138
140
|
account_id: MemWalAccount object ID on Sui.
|
|
139
|
-
server_url: Server URL (default: ``
|
|
141
|
+
server_url: Server URL (default: ``http://localhost:8000``).
|
|
140
142
|
namespace: Default namespace for memory isolation (default: ``"default"``).
|
|
143
|
+
env: Optional relayer preset — ``"prod"``, ``"dev"``, ``"staging"``,
|
|
144
|
+
or ``"local"``. Resolves ``server_url`` to the matching hosted
|
|
145
|
+
relayer unless an explicit non-default ``server_url`` is given.
|
|
146
|
+
Precedence: explicit ``server_url`` > ``env`` > default.
|
|
141
147
|
|
|
142
148
|
Returns:
|
|
143
149
|
A configured :class:`MemWal` instance.
|
|
@@ -147,6 +153,7 @@ class MemWal:
|
|
|
147
153
|
account_id=account_id,
|
|
148
154
|
server_url=server_url,
|
|
149
155
|
namespace=namespace,
|
|
156
|
+
env=env,
|
|
150
157
|
)
|
|
151
158
|
return cls(config)
|
|
152
159
|
|
|
@@ -708,14 +715,12 @@ class MemWal:
|
|
|
708
715
|
) -> Dict[str, Any]:
|
|
709
716
|
"""Make a signed request to the server.
|
|
710
717
|
|
|
711
|
-
Signature format:
|
|
712
|
-
``{timestamp}.{method}.{path}.{body_sha256}.{nonce}.{account_id}``
|
|
718
|
+
Signature format: ``{timestamp}.{method}.{path}.{body_sha256}``
|
|
713
719
|
|
|
714
720
|
Headers sent:
|
|
715
721
|
- ``x-public-key``: Ed25519 public key hex
|
|
716
722
|
- ``x-signature``: Ed25519 signature hex
|
|
717
723
|
- ``x-timestamp``: Unix seconds string
|
|
718
|
-
- ``x-nonce``: UUID v4 replay-protection nonce
|
|
719
724
|
- ``x-delegate-key``: Private key hex
|
|
720
725
|
- ``x-account-id``: MemWalAccount object ID
|
|
721
726
|
- ``Content-Type``: application/json
|
|
@@ -842,22 +847,27 @@ class MemWalSync:
|
|
|
842
847
|
cls,
|
|
843
848
|
key: str,
|
|
844
849
|
account_id: str,
|
|
845
|
-
server_url: str = "
|
|
850
|
+
server_url: str = "http://localhost:8000",
|
|
846
851
|
namespace: str = "default",
|
|
852
|
+
env: Optional[str] = None,
|
|
847
853
|
) -> "MemWalSync":
|
|
848
854
|
"""Create a synchronous MemWal client.
|
|
849
855
|
|
|
850
|
-
Same parameters as :meth:`MemWal.create
|
|
856
|
+
Same parameters as :meth:`MemWal.create` (including the ``env``
|
|
857
|
+
relayer preset).
|
|
851
858
|
"""
|
|
852
859
|
inner = MemWal.create(
|
|
853
860
|
key=key,
|
|
854
861
|
account_id=account_id,
|
|
855
862
|
server_url=server_url,
|
|
856
863
|
namespace=namespace,
|
|
864
|
+
env=env,
|
|
857
865
|
)
|
|
858
866
|
return cls(inner)
|
|
859
867
|
|
|
860
868
|
def _run(self, coro: Any) -> Any:
|
|
869
|
+
import asyncio
|
|
870
|
+
|
|
861
871
|
try:
|
|
862
872
|
loop = asyncio.get_running_loop()
|
|
863
873
|
except RuntimeError:
|
|
@@ -137,12 +137,13 @@ def with_memwal_langchain(
|
|
|
137
137
|
llm: "BaseChatModel",
|
|
138
138
|
key: str,
|
|
139
139
|
account_id: str,
|
|
140
|
-
server_url: str = "
|
|
140
|
+
server_url: str = "http://localhost:8000",
|
|
141
141
|
namespace: str = "default",
|
|
142
142
|
max_memories: int = 5,
|
|
143
143
|
auto_save: bool = True,
|
|
144
144
|
min_relevance: float = 0.3,
|
|
145
145
|
debug: bool = False,
|
|
146
|
+
env: Optional[str] = None,
|
|
146
147
|
) -> "BaseChatModel":
|
|
147
148
|
"""Wrap a LangChain ``BaseChatModel`` with MemWal memory management.
|
|
148
149
|
|
|
@@ -163,6 +164,8 @@ def with_memwal_langchain(
|
|
|
163
164
|
auto_save: Auto-save new facts from conversation.
|
|
164
165
|
min_relevance: Minimum similarity score (0-1) to include a memory.
|
|
165
166
|
debug: Enable debug logging.
|
|
167
|
+
env: Optional relayer preset (``"prod"`` / ``"dev"`` / ``"staging"`` /
|
|
168
|
+
``"local"``). Same precedence as :meth:`MemWal.create`.
|
|
166
169
|
|
|
167
170
|
Returns:
|
|
168
171
|
A wrapped ``BaseChatModel`` that automatically uses MemWal memory.
|
|
@@ -181,6 +184,7 @@ def with_memwal_langchain(
|
|
|
181
184
|
account_id=account_id,
|
|
182
185
|
server_url=server_url,
|
|
183
186
|
namespace=namespace,
|
|
187
|
+
env=env,
|
|
184
188
|
)
|
|
185
189
|
|
|
186
190
|
log = logger.debug if not debug else logger.warning
|
|
@@ -254,6 +258,8 @@ def with_memwal_langchain(
|
|
|
254
258
|
messages: List[List[BaseMessage]], *args: Any, **kwargs: Any
|
|
255
259
|
) -> ChatResult:
|
|
256
260
|
# For sync generate, we inject memories synchronously via asyncio.run
|
|
261
|
+
import asyncio
|
|
262
|
+
|
|
257
263
|
enriched = []
|
|
258
264
|
for msg_list in messages:
|
|
259
265
|
try:
|
|
@@ -290,12 +296,13 @@ def with_memwal_openai(
|
|
|
290
296
|
client: Any,
|
|
291
297
|
key: str,
|
|
292
298
|
account_id: str,
|
|
293
|
-
server_url: str = "
|
|
299
|
+
server_url: str = "http://localhost:8000",
|
|
294
300
|
namespace: str = "default",
|
|
295
301
|
max_memories: int = 5,
|
|
296
302
|
auto_save: bool = True,
|
|
297
303
|
min_relevance: float = 0.3,
|
|
298
304
|
debug: bool = False,
|
|
305
|
+
env: Optional[str] = None,
|
|
299
306
|
) -> Any:
|
|
300
307
|
"""Wrap an OpenAI client with MemWal memory management.
|
|
301
308
|
|
|
@@ -318,6 +325,8 @@ def with_memwal_openai(
|
|
|
318
325
|
auto_save: Auto-save new facts from conversation.
|
|
319
326
|
min_relevance: Minimum similarity score (0-1) to include a memory.
|
|
320
327
|
debug: Enable debug logging.
|
|
328
|
+
env: Optional relayer preset (``"prod"`` / ``"dev"`` / ``"staging"`` /
|
|
329
|
+
``"local"``). Same precedence as :meth:`MemWal.create`.
|
|
321
330
|
|
|
322
331
|
Returns:
|
|
323
332
|
The same client, with ``chat.completions.create`` wrapped to use MemWal.
|
|
@@ -327,6 +336,7 @@ def with_memwal_openai(
|
|
|
327
336
|
account_id=account_id,
|
|
328
337
|
server_url=server_url,
|
|
329
338
|
namespace=namespace,
|
|
339
|
+
env=env,
|
|
330
340
|
)
|
|
331
341
|
|
|
332
342
|
log = logger.debug if not debug else logger.warning
|
|
@@ -408,6 +418,8 @@ def _wrap_sync_openai(
|
|
|
408
418
|
original_create = client.chat.completions.create
|
|
409
419
|
|
|
410
420
|
def patched_create(*args: Any, **kwargs: Any) -> Any:
|
|
421
|
+
import asyncio
|
|
422
|
+
|
|
411
423
|
messages = kwargs.get("messages") or (args[0] if args else None)
|
|
412
424
|
if messages is None:
|
|
413
425
|
return original_create(*args, **kwargs)
|
|
@@ -15,6 +15,21 @@ from typing import List, Optional
|
|
|
15
15
|
# Config
|
|
16
16
|
# ============================================================
|
|
17
17
|
|
|
18
|
+
#: Default ``server_url`` when neither an explicit URL nor an ``env`` preset
|
|
19
|
+
#: is supplied. Kept as a module constant so ``__post_init__`` can tell an
|
|
20
|
+
#: untouched default apart from an explicitly-passed custom URL.
|
|
21
|
+
DEFAULT_SERVER_URL = "http://localhost:8000"
|
|
22
|
+
|
|
23
|
+
#: Named relayer environments. Mirrors the TypeScript SDK / MCP package
|
|
24
|
+
#: ``--prod`` / ``--dev`` / ``--staging`` / ``--local`` presets so the same
|
|
25
|
+
#: shorthand works across every MemWal client.
|
|
26
|
+
ENV_PRESETS = {
|
|
27
|
+
"prod": "https://relayer.memwal.ai",
|
|
28
|
+
"dev": "https://relayer.dev.memwal.ai",
|
|
29
|
+
"staging": "https://relayer.staging.memwal.ai",
|
|
30
|
+
"local": "http://127.0.0.1:8000",
|
|
31
|
+
}
|
|
32
|
+
|
|
18
33
|
|
|
19
34
|
@dataclass
|
|
20
35
|
class MemWalConfig:
|
|
@@ -23,14 +38,33 @@ class MemWalConfig:
|
|
|
23
38
|
Attributes:
|
|
24
39
|
key: Ed25519 private key (hex string). This is the delegate key from app.memwal.com.
|
|
25
40
|
account_id: MemWalAccount object ID on Sui.
|
|
26
|
-
server_url: Server URL (default:
|
|
41
|
+
server_url: Server URL (default: http://localhost:8000). An explicit
|
|
42
|
+
non-default value always wins over ``env``.
|
|
27
43
|
namespace: Default namespace for memory isolation (default: "default").
|
|
44
|
+
env: Optional relayer preset — one of ``"prod"``, ``"dev"``,
|
|
45
|
+
``"staging"``, ``"local"``. Resolves ``server_url`` to the matching
|
|
46
|
+
hosted relayer when ``server_url`` is left at its default.
|
|
47
|
+
Precedence: explicit ``server_url`` > ``env`` > default.
|
|
28
48
|
"""
|
|
29
49
|
|
|
30
50
|
key: str
|
|
31
51
|
account_id: str
|
|
32
|
-
server_url: str =
|
|
52
|
+
server_url: str = DEFAULT_SERVER_URL
|
|
33
53
|
namespace: str = "default"
|
|
54
|
+
env: Optional[str] = None
|
|
55
|
+
|
|
56
|
+
def __post_init__(self) -> None:
|
|
57
|
+
if self.env is not None:
|
|
58
|
+
preset = ENV_PRESETS.get(self.env)
|
|
59
|
+
if preset is None:
|
|
60
|
+
valid = ", ".join(sorted(ENV_PRESETS))
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"Unknown env preset {self.env!r}. Valid presets: {valid}"
|
|
63
|
+
)
|
|
64
|
+
# Explicit, non-default server_url takes precedence over the
|
|
65
|
+
# preset; only fill from the preset when server_url is untouched.
|
|
66
|
+
if self.server_url == DEFAULT_SERVER_URL:
|
|
67
|
+
self.server_url = preset
|
|
34
68
|
|
|
35
69
|
|
|
36
70
|
# ============================================================
|
|
@@ -16,7 +16,7 @@ import respx
|
|
|
16
16
|
|
|
17
17
|
from memwal.client import MemWal, MemWalError
|
|
18
18
|
from memwal.types import RecallManualOptions, RememberManualOptions
|
|
19
|
-
from memwal.utils import bytes_to_hex, sha256_hex
|
|
19
|
+
from memwal.utils import build_signature_message, bytes_to_hex, sha256_hex
|
|
20
20
|
|
|
21
21
|
# ============================================================
|
|
22
22
|
# Fixtures
|
|
@@ -122,7 +122,14 @@ class TestRemember:
|
|
|
122
122
|
timestamp = headers["x-timestamp"]
|
|
123
123
|
body_hash = sha256_hex(body_str)
|
|
124
124
|
nonce = headers["x-nonce"]
|
|
125
|
-
message =
|
|
125
|
+
message = build_signature_message(
|
|
126
|
+
timestamp=timestamp,
|
|
127
|
+
method="POST",
|
|
128
|
+
path="/api/remember",
|
|
129
|
+
body_sha256=body_hash,
|
|
130
|
+
nonce=nonce,
|
|
131
|
+
account_id=headers["x-account-id"],
|
|
132
|
+
)
|
|
126
133
|
|
|
127
134
|
# Verify signature
|
|
128
135
|
verify_key = nacl.signing.VerifyKey(bytes.fromhex(headers["x-public-key"]))
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Tests for the relayer environment presets (prod/dev/staging/local).
|
|
2
|
+
|
|
3
|
+
Pure config resolution — no network. Mirrors the precedence rule documented
|
|
4
|
+
in the README: explicit non-default ``server_url`` > ``env`` > default.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from memwal import ENV_PRESETS, MemWal, MemWalConfig, MemWalSync
|
|
10
|
+
from memwal.types import DEFAULT_SERVER_URL
|
|
11
|
+
|
|
12
|
+
# A throwaway but structurally valid 32-byte Ed25519 seed (64 hex chars).
|
|
13
|
+
KEY = "11" * 32
|
|
14
|
+
ACCOUNT = "0x" + "ab" * 32
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.mark.parametrize(
|
|
18
|
+
"env,expected",
|
|
19
|
+
[
|
|
20
|
+
("prod", "https://relayer.memwal.ai"),
|
|
21
|
+
("dev", "https://relayer.dev.memwal.ai"),
|
|
22
|
+
("staging", "https://relayer.staging.memwal.ai"),
|
|
23
|
+
("local", "http://127.0.0.1:8000"),
|
|
24
|
+
],
|
|
25
|
+
)
|
|
26
|
+
def test_env_preset_resolves(env, expected):
|
|
27
|
+
cfg = MemWalConfig(key=KEY, account_id=ACCOUNT, env=env)
|
|
28
|
+
assert cfg.server_url == expected
|
|
29
|
+
assert ENV_PRESETS[env] == expected
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_explicit_server_url_overrides_env():
|
|
33
|
+
cfg = MemWalConfig(
|
|
34
|
+
key=KEY,
|
|
35
|
+
account_id=ACCOUNT,
|
|
36
|
+
server_url="https://my.custom.relayer",
|
|
37
|
+
env="prod",
|
|
38
|
+
)
|
|
39
|
+
assert cfg.server_url == "https://my.custom.relayer"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_no_env_keeps_default():
|
|
43
|
+
cfg = MemWalConfig(key=KEY, account_id=ACCOUNT)
|
|
44
|
+
assert cfg.server_url == DEFAULT_SERVER_URL
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_unknown_env_raises():
|
|
48
|
+
with pytest.raises(ValueError, match="Unknown env preset"):
|
|
49
|
+
MemWalConfig(key=KEY, account_id=ACCOUNT, env="prdo")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_create_threads_env_through_to_client():
|
|
53
|
+
client = MemWal.create(key=KEY, account_id=ACCOUNT, env="staging")
|
|
54
|
+
assert client._server_url == "https://relayer.staging.memwal.ai"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_sync_create_threads_env_through():
|
|
58
|
+
client = MemWalSync.create(key=KEY, account_id=ACCOUNT, env="dev")
|
|
59
|
+
assert client._inner._server_url == "https://relayer.dev.memwal.ai"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_explicit_default_url_with_env_still_takes_preset():
|
|
63
|
+
# Passing the default URL explicitly is indistinguishable from not
|
|
64
|
+
# passing it — documented edge: the preset still applies.
|
|
65
|
+
cfg = MemWalConfig(
|
|
66
|
+
key=KEY,
|
|
67
|
+
account_id=ACCOUNT,
|
|
68
|
+
server_url=DEFAULT_SERVER_URL,
|
|
69
|
+
env="prod",
|
|
70
|
+
)
|
|
71
|
+
assert cfg.server_url == "https://relayer.memwal.ai"
|
|
@@ -40,12 +40,14 @@ import hashlib
|
|
|
40
40
|
import json
|
|
41
41
|
import os
|
|
42
42
|
import time
|
|
43
|
+
import uuid
|
|
43
44
|
|
|
44
45
|
import httpx
|
|
45
46
|
import nacl.signing
|
|
46
47
|
import pytest
|
|
47
48
|
|
|
48
49
|
from memwal.client import MemWal, MemWalError, MemWalSync
|
|
50
|
+
from memwal.utils import build_signature_message, bytes_to_hex
|
|
49
51
|
|
|
50
52
|
# ── Config ───────────────────────────────────────────────────────────────────
|
|
51
53
|
|
|
@@ -77,7 +79,15 @@ def _raw_signed_request(
|
|
|
77
79
|
body_bytes = json.dumps(body, separators=(",", ":")).encode()
|
|
78
80
|
body_hash = hashlib.sha256(body_bytes).hexdigest()
|
|
79
81
|
timestamp = timestamp_override or str(int(time.time()))
|
|
80
|
-
|
|
82
|
+
nonce = str(uuid.uuid4())
|
|
83
|
+
message = build_signature_message(
|
|
84
|
+
timestamp=timestamp,
|
|
85
|
+
method=method.upper(),
|
|
86
|
+
path=path,
|
|
87
|
+
body_sha256=body_hash,
|
|
88
|
+
nonce=nonce,
|
|
89
|
+
account_id=ACCOUNT_ID or "0x0",
|
|
90
|
+
)
|
|
81
91
|
signed = signing_key.sign(message.encode())
|
|
82
92
|
signature_hex = signed.signature.hex()
|
|
83
93
|
pub_key_hex = pub_key_override or signing_key.verify_key.encode().hex()
|
|
@@ -92,6 +102,8 @@ def _raw_signed_request(
|
|
|
92
102
|
"x-public-key": pub_key_hex,
|
|
93
103
|
"x-signature": signature_hex,
|
|
94
104
|
"x-timestamp": timestamp,
|
|
105
|
+
"x-nonce": nonce,
|
|
106
|
+
"x-account-id": ACCOUNT_ID or "0x0",
|
|
95
107
|
},
|
|
96
108
|
)
|
|
97
109
|
|
|
@@ -16,10 +16,13 @@ with ``respx`` and all LLM responses are mocked with ``unittest.mock``.
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
18
|
import asyncio
|
|
19
|
-
|
|
19
|
+
import json
|
|
20
|
+
from typing import Any, List
|
|
21
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
20
22
|
|
|
21
23
|
import httpx
|
|
22
24
|
import nacl.signing
|
|
25
|
+
import pytest
|
|
23
26
|
import respx
|
|
24
27
|
|
|
25
28
|
from memwal.middleware import (
|
|
@@ -131,16 +131,16 @@ class TestBuildSignatureMessage:
|
|
|
131
131
|
"""Tests for build_signature_message -- the exact format the server expects."""
|
|
132
132
|
|
|
133
133
|
def test_format_matches_spec(self) -> None:
|
|
134
|
-
"""Signature message
|
|
134
|
+
"""Signature message MUST match the canonical 6-part server format."""
|
|
135
135
|
result = build_signature_message(
|
|
136
136
|
timestamp="1700000000",
|
|
137
137
|
method="POST",
|
|
138
138
|
path="/api/remember",
|
|
139
139
|
body_sha256="abc123",
|
|
140
|
-
nonce="
|
|
141
|
-
account_id="
|
|
140
|
+
nonce="550e8400-e29b-41d4-a716-446655440000",
|
|
141
|
+
account_id="0xabc123",
|
|
142
142
|
)
|
|
143
|
-
assert result == "1700000000.POST./api/remember.abc123.
|
|
143
|
+
assert result == "1700000000.POST./api/remember.abc123.550e8400-e29b-41d4-a716-446655440000.0xabc123"
|
|
144
144
|
|
|
145
145
|
def test_get_method(self) -> None:
|
|
146
146
|
result = build_signature_message(
|
|
@@ -148,8 +148,8 @@ class TestBuildSignatureMessage:
|
|
|
148
148
|
method="GET",
|
|
149
149
|
path="/health",
|
|
150
150
|
body_sha256="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
151
|
-
nonce="
|
|
152
|
-
account_id="
|
|
151
|
+
nonce="550e8400-e29b-41d4-a716-446655440000",
|
|
152
|
+
account_id="0xabc123",
|
|
153
153
|
)
|
|
154
154
|
parts = result.split(".")
|
|
155
155
|
assert parts[0] == "1700000001"
|
|
@@ -168,15 +168,10 @@ class TestBuildSignatureMessage:
|
|
|
168
168
|
path = "/api/remember"
|
|
169
169
|
body = json.dumps({"text": "hello", "namespace": "default"}, separators=(",", ":"))
|
|
170
170
|
body_hash = sha256_hex(body)
|
|
171
|
+
nonce = "550e8400-e29b-41d4-a716-446655440000"
|
|
172
|
+
account_id = "0xabc123"
|
|
171
173
|
|
|
172
|
-
message = build_signature_message(
|
|
173
|
-
timestamp,
|
|
174
|
-
method,
|
|
175
|
-
path,
|
|
176
|
-
body_hash,
|
|
177
|
-
nonce="nonce-3",
|
|
178
|
-
account_id="0xabc",
|
|
179
|
-
)
|
|
174
|
+
message = build_signature_message(timestamp, method, path, body_hash, nonce=nonce, account_id=account_id)
|
|
180
175
|
sig_hex, pub_hex = sign_message(message, signing_key)
|
|
181
176
|
|
|
182
177
|
# Verify (as the server would)
|
|
@@ -194,11 +189,11 @@ class TestBuildSignatureMessage:
|
|
|
194
189
|
"POST",
|
|
195
190
|
"/api/remember",
|
|
196
191
|
body_hash,
|
|
197
|
-
nonce="
|
|
198
|
-
account_id="
|
|
192
|
+
nonce="550e8400-e29b-41d4-a716-446655440000",
|
|
193
|
+
account_id="0xabc123",
|
|
199
194
|
)
|
|
200
195
|
|
|
201
|
-
# Extract the hash from the message
|
|
196
|
+
# Extract the hash from the canonical 6-part message
|
|
202
197
|
extracted_hash = message.split(".")[3]
|
|
203
198
|
assert extracted_hash == body_hash
|
|
204
199
|
assert extracted_hash == hashlib.sha256(body_str.encode("utf-8")).hexdigest()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|