memwal 0.1.0.dev3__tar.gz → 0.1.1.dev0__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.dev3 → memwal-0.1.1.dev0}/PKG-INFO +3 -3
- {memwal-0.1.0.dev3 → memwal-0.1.1.dev0}/README.md +2 -2
- {memwal-0.1.0.dev3 → memwal-0.1.1.dev0}/memwal/__init__.py +3 -1
- {memwal-0.1.0.dev3 → memwal-0.1.1.dev0}/memwal/client.py +75 -2
- memwal-0.1.1.dev0/memwal/compatibility.py +72 -0
- {memwal-0.1.0.dev3 → memwal-0.1.1.dev0}/memwal/types.py +8 -1
- {memwal-0.1.0.dev3 → memwal-0.1.1.dev0}/memwal/utils.py +1 -1
- {memwal-0.1.0.dev3 → memwal-0.1.1.dev0}/pyproject.toml +1 -1
- {memwal-0.1.0.dev3 → memwal-0.1.1.dev0}/tests/test_client.py +78 -1
- {memwal-0.1.0.dev3 → memwal-0.1.1.dev0}/tests/test_integration.py +3 -1
- {memwal-0.1.0.dev3 → memwal-0.1.1.dev0}/tests/test_middleware.py +22 -0
- {memwal-0.1.0.dev3 → memwal-0.1.1.dev0}/.gitignore +0 -0
- {memwal-0.1.0.dev3 → memwal-0.1.1.dev0}/examples/.env.example +0 -0
- {memwal-0.1.0.dev3 → memwal-0.1.1.dev0}/examples/.gitignore +0 -0
- {memwal-0.1.0.dev3 → memwal-0.1.1.dev0}/examples/async_remember_demo.py +0 -0
- {memwal-0.1.0.dev3 → memwal-0.1.1.dev0}/examples/interactive_demo.py +0 -0
- {memwal-0.1.0.dev3 → memwal-0.1.1.dev0}/memwal/middleware.py +0 -0
- {memwal-0.1.0.dev3 → memwal-0.1.1.dev0}/run_tests.py +0 -0
- {memwal-0.1.0.dev3 → memwal-0.1.1.dev0}/tests/__init__.py +0 -0
- {memwal-0.1.0.dev3 → memwal-0.1.1.dev0}/tests/test_env_presets.py +0 -0
- {memwal-0.1.0.dev3 → memwal-0.1.1.dev0}/tests/test_signing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memwal
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1.dev0
|
|
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
|
|
@@ -227,10 +227,10 @@ Create a new async client.
|
|
|
227
227
|
Every request is signed with Ed25519:
|
|
228
228
|
|
|
229
229
|
```
|
|
230
|
-
message = f"{timestamp}.{method}.{
|
|
230
|
+
message = f"{timestamp}.{method}.{path_and_query}.{body_sha256}.{nonce}.{account_id}"
|
|
231
231
|
```
|
|
232
232
|
|
|
233
|
-
|
|
233
|
+
Signed requests send `x-public-key`, `x-signature`, `x-timestamp`, `x-nonce`, and `x-account-id`. Relayer-mode requests also send `x-seal-session`; manual-mode requests omit decrypt credentials.
|
|
234
234
|
|
|
235
235
|
## License
|
|
236
236
|
|
|
@@ -188,10 +188,10 @@ Create a new async client.
|
|
|
188
188
|
Every request is signed with Ed25519:
|
|
189
189
|
|
|
190
190
|
```
|
|
191
|
-
message = f"{timestamp}.{method}.{
|
|
191
|
+
message = f"{timestamp}.{method}.{path_and_query}.{body_sha256}.{nonce}.{account_id}"
|
|
192
192
|
```
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
Signed requests send `x-public-key`, `x-signature`, `x-timestamp`, `x-nonce`, and `x-account-id`. Relayer-mode requests also send `x-seal-session`; manual-mode requests omit decrypt credentials.
|
|
195
195
|
|
|
196
196
|
## License
|
|
197
197
|
|
|
@@ -24,6 +24,7 @@ Quick start::
|
|
|
24
24
|
|
|
25
25
|
from .client import (
|
|
26
26
|
MemWal,
|
|
27
|
+
MemWalCompatibilityError,
|
|
27
28
|
MemWalError,
|
|
28
29
|
MemWalRememberJobFailed,
|
|
29
30
|
MemWalRememberJobNotFound,
|
|
@@ -70,6 +71,7 @@ __all__ = [
|
|
|
70
71
|
"MemWal",
|
|
71
72
|
"MemWalSync",
|
|
72
73
|
"MemWalError",
|
|
74
|
+
"MemWalCompatibilityError",
|
|
73
75
|
"MemWalRememberJobFailed",
|
|
74
76
|
"MemWalRememberJobNotFound",
|
|
75
77
|
"MemWalRememberJobTimeout",
|
|
@@ -110,4 +112,4 @@ __all__ = [
|
|
|
110
112
|
"RecallManualResult",
|
|
111
113
|
]
|
|
112
114
|
|
|
113
|
-
__version__ = "0.1.
|
|
115
|
+
__version__ = "0.1.1.dev0"
|
|
@@ -35,6 +35,7 @@ from typing import Any, Dict, List, Optional, Sequence, Tuple, TypeVar
|
|
|
35
35
|
import httpx
|
|
36
36
|
import nacl.signing
|
|
37
37
|
|
|
38
|
+
from .compatibility import compatibility_error
|
|
38
39
|
from .types import (
|
|
39
40
|
AnalyzedFact,
|
|
40
41
|
AnalyzeResult,
|
|
@@ -134,6 +135,8 @@ class MemWal:
|
|
|
134
135
|
self._server_config: Optional[Dict[str, str]] = None
|
|
135
136
|
self._session_cache: Optional[Tuple[str, int]] = None
|
|
136
137
|
self._session_build_task: Optional[asyncio.Task[str]] = None
|
|
138
|
+
self._relayer_version_metadata: Optional[Dict[str, Any]] = None
|
|
139
|
+
self._compatibility_lock: Optional[asyncio.Lock] = None
|
|
137
140
|
|
|
138
141
|
@classmethod
|
|
139
142
|
def create(
|
|
@@ -652,7 +655,22 @@ class MemWal:
|
|
|
652
655
|
if response.status_code != 200:
|
|
653
656
|
raise MemWalError(f"Health check failed: {response.status_code}")
|
|
654
657
|
data = response.json()
|
|
655
|
-
return HealthResult(
|
|
658
|
+
return HealthResult(
|
|
659
|
+
status=data["status"],
|
|
660
|
+
version=data["version"],
|
|
661
|
+
relayer_version=data.get("relayerVersion"),
|
|
662
|
+
api_version=data.get("apiVersion"),
|
|
663
|
+
min_supported_sdk=data.get("minSupportedSdk"),
|
|
664
|
+
feature_flags=data.get("featureFlags"),
|
|
665
|
+
deprecations=data.get("deprecations"),
|
|
666
|
+
build=data.get("build"),
|
|
667
|
+
mode=data.get("mode"),
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
async def compatibility(self) -> Dict[str, Any]:
|
|
671
|
+
"""Fetch and validate the relayer compatibility contract."""
|
|
672
|
+
|
|
673
|
+
return await self._ensure_compatible_relayer()
|
|
656
674
|
|
|
657
675
|
# ============================================================
|
|
658
676
|
# Manual API (user handles SEAL + embedding + Walrus)
|
|
@@ -727,6 +745,42 @@ class MemWal:
|
|
|
727
745
|
# Internal: Signed HTTP Requests
|
|
728
746
|
# ============================================================
|
|
729
747
|
|
|
748
|
+
async def _ensure_compatible_relayer(self) -> Dict[str, Any]:
|
|
749
|
+
if self._relayer_version_metadata is not None:
|
|
750
|
+
return self._relayer_version_metadata
|
|
751
|
+
|
|
752
|
+
if self._compatibility_lock is None:
|
|
753
|
+
self._compatibility_lock = asyncio.Lock()
|
|
754
|
+
|
|
755
|
+
async with self._compatibility_lock:
|
|
756
|
+
if self._relayer_version_metadata is not None:
|
|
757
|
+
return self._relayer_version_metadata
|
|
758
|
+
|
|
759
|
+
version_response = await self._http.get(f"{self._server_url}/version")
|
|
760
|
+
if version_response.status_code == 200:
|
|
761
|
+
metadata = version_response.json()
|
|
762
|
+
elif version_response.status_code in (404, 405):
|
|
763
|
+
health_response = await self._http.get(f"{self._server_url}/health")
|
|
764
|
+
if health_response.status_code != 200:
|
|
765
|
+
raise MemWalError(
|
|
766
|
+
"MemWal compatibility check failed: "
|
|
767
|
+
f"GET /version returned {version_response.status_code}, "
|
|
768
|
+
f"and GET /health returned {health_response.status_code}"
|
|
769
|
+
)
|
|
770
|
+
metadata = health_response.json()
|
|
771
|
+
else:
|
|
772
|
+
raise MemWalError(
|
|
773
|
+
"MemWal compatibility check failed: "
|
|
774
|
+
f"GET /version returned {version_response.status_code}"
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
error = compatibility_error(metadata, self._server_url)
|
|
778
|
+
if error is not None:
|
|
779
|
+
raise MemWalCompatibilityError(error)
|
|
780
|
+
|
|
781
|
+
self._relayer_version_metadata = metadata
|
|
782
|
+
return metadata
|
|
783
|
+
|
|
730
784
|
async def _fetch_server_config(self) -> Dict[str, str]:
|
|
731
785
|
if self._server_config is not None:
|
|
732
786
|
return self._server_config
|
|
@@ -842,7 +896,7 @@ class MemWal:
|
|
|
842
896
|
"""Make a signed request to the server.
|
|
843
897
|
|
|
844
898
|
Signature format:
|
|
845
|
-
``{timestamp}.{method}.{
|
|
899
|
+
``{timestamp}.{method}.{path_and_query}.{body_sha256}.{nonce}.{account_id}``
|
|
846
900
|
|
|
847
901
|
For ``GET`` requests the canonical body string is the empty string,
|
|
848
902
|
and no HTTP request body is sent. This keeps the signed payload hash
|
|
@@ -853,12 +907,15 @@ class MemWal:
|
|
|
853
907
|
- ``x-public-key``: Ed25519 public key hex
|
|
854
908
|
- ``x-signature``: Ed25519 signature hex
|
|
855
909
|
- ``x-timestamp``: Unix seconds string
|
|
910
|
+
- ``x-nonce``: UUID v4 replay-protection nonce
|
|
856
911
|
- ``x-seal-session``: Base64-encoded exported session envelope
|
|
857
912
|
- ``x-account-id``: MemWalAccount object ID
|
|
858
913
|
- ``Content-Type``: application/json
|
|
859
914
|
"""
|
|
860
915
|
import uuid
|
|
861
916
|
|
|
917
|
+
await self._ensure_compatible_relayer()
|
|
918
|
+
|
|
862
919
|
method_upper = method.upper()
|
|
863
920
|
timestamp = str(int(time.time()))
|
|
864
921
|
body_str = "" if method_upper == "GET" else json.dumps(body, separators=(",", ":"))
|
|
@@ -899,6 +956,12 @@ class MemWal:
|
|
|
899
956
|
|
|
900
957
|
if response.status_code not in accepted_statuses:
|
|
901
958
|
err_text = response.text
|
|
959
|
+
if response.status_code == 426:
|
|
960
|
+
raise MemWalCompatibilityError(
|
|
961
|
+
"MemWal relayer rejected this SDK as unsupported "
|
|
962
|
+
f"(HTTP 426 Upgrade Required). Relayer response: "
|
|
963
|
+
f"{err_text[:300] or 'upgrade required'}"
|
|
964
|
+
)
|
|
902
965
|
raise _HttpStatusError(
|
|
903
966
|
status=response.status_code,
|
|
904
967
|
body=err_text,
|
|
@@ -913,6 +976,12 @@ class MemWalError(Exception):
|
|
|
913
976
|
pass
|
|
914
977
|
|
|
915
978
|
|
|
979
|
+
class MemWalCompatibilityError(MemWalError):
|
|
980
|
+
"""Raised when the SDK and relayer API contract are incompatible."""
|
|
981
|
+
|
|
982
|
+
pass
|
|
983
|
+
|
|
984
|
+
|
|
916
985
|
class _HttpStatusError(MemWalError):
|
|
917
986
|
"""Internal: raised when an HTTP response status is not in ``accepted_statuses``.
|
|
918
987
|
|
|
@@ -1135,6 +1204,10 @@ class MemWalSync:
|
|
|
1135
1204
|
"""Synchronous version of :meth:`MemWal.health`."""
|
|
1136
1205
|
return self._run(self._inner.health())
|
|
1137
1206
|
|
|
1207
|
+
def compatibility(self) -> Dict[str, Any]:
|
|
1208
|
+
"""Synchronous version of :meth:`MemWal.compatibility`."""
|
|
1209
|
+
return self._run(self._inner.compatibility())
|
|
1210
|
+
|
|
1138
1211
|
def remember_manual(self, opts: RememberManualOptions) -> RememberManualResult:
|
|
1139
1212
|
"""Synchronous version of :meth:`MemWal.remember_manual`."""
|
|
1140
1213
|
return self._run(self._inner.remember_manual(opts))
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Relayer/API compatibility checks for the Python SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any, Dict, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
MEMWAL_PYTHON_COMPATIBILITY_VERSION = "0.1.0"
|
|
9
|
+
SUPPORTED_RELAYER_API_MAJOR = 1
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def compatibility_error(metadata: Dict[str, Any], server_url: str) -> Optional[str]:
|
|
13
|
+
"""Return an actionable error string when relayer metadata is unsupported."""
|
|
14
|
+
|
|
15
|
+
api_version = metadata.get("apiVersion")
|
|
16
|
+
relayer_version = metadata.get("relayerVersion")
|
|
17
|
+
min_supported = metadata.get("minSupportedSdk")
|
|
18
|
+
|
|
19
|
+
if not api_version or not relayer_version or not isinstance(min_supported, dict):
|
|
20
|
+
return (
|
|
21
|
+
f"MemWal relayer at {server_url} does not expose compatibility metadata. "
|
|
22
|
+
"Upgrade the relayer to a version that serves GET /version, or use an older SDK."
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
api_major = _semver_major(str(api_version))
|
|
26
|
+
if api_major is None:
|
|
27
|
+
return f'MemWal relayer at {server_url} returned invalid apiVersion "{api_version}".'
|
|
28
|
+
|
|
29
|
+
if api_major != SUPPORTED_RELAYER_API_MAJOR:
|
|
30
|
+
return (
|
|
31
|
+
"This MemWal Python SDK supports relayer API "
|
|
32
|
+
f"{SUPPORTED_RELAYER_API_MAJOR}.x, but {server_url} reports apiVersion "
|
|
33
|
+
f"{api_version}. Upgrade or downgrade the SDK/relayer pair."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
min_python = min_supported.get("python")
|
|
37
|
+
if not isinstance(min_python, str):
|
|
38
|
+
return f"MemWal relayer at {server_url} did not report minSupportedSdk.python."
|
|
39
|
+
if _parse_semver(min_python) is None:
|
|
40
|
+
return (
|
|
41
|
+
f'MemWal relayer at {server_url} returned invalid '
|
|
42
|
+
f'minSupportedSdk.python "{min_python}".'
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if _compare_semver(MEMWAL_PYTHON_COMPATIBILITY_VERSION, min_python) < 0:
|
|
46
|
+
return (
|
|
47
|
+
f"MemWal relayer at {server_url} requires Python SDK >= {min_python}, "
|
|
48
|
+
f"but this package supports the {MEMWAL_PYTHON_COMPATIBILITY_VERSION} "
|
|
49
|
+
"compatibility baseline. Upgrade memwal or use an older compatible relayer."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _semver_major(version: str) -> Optional[int]:
|
|
56
|
+
parsed = _parse_semver(version)
|
|
57
|
+
return parsed[0] if parsed else None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _compare_semver(left: str, right: str) -> int:
|
|
61
|
+
left_parts = _parse_semver(left)
|
|
62
|
+
right_parts = _parse_semver(right)
|
|
63
|
+
if left_parts is None or right_parts is None:
|
|
64
|
+
raise ValueError(f"invalid semver comparison: {left} vs {right}")
|
|
65
|
+
return (left_parts > right_parts) - (left_parts < right_parts)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _parse_semver(version: str) -> Optional[Tuple[int, int, int]]:
|
|
69
|
+
match = re.match(r"^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$", version.strip())
|
|
70
|
+
if match is None:
|
|
71
|
+
return None
|
|
72
|
+
return int(match.group(1)), int(match.group(2)), int(match.group(3))
|
|
@@ -9,7 +9,7 @@ the MemWal Rust server (TEE).
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
from dataclasses import dataclass
|
|
12
|
-
from typing import List, Optional
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
13
|
|
|
14
14
|
# ============================================================
|
|
15
15
|
# Config
|
|
@@ -136,6 +136,13 @@ class HealthResult:
|
|
|
136
136
|
|
|
137
137
|
status: str
|
|
138
138
|
version: str
|
|
139
|
+
relayer_version: Optional[str] = None
|
|
140
|
+
api_version: Optional[str] = None
|
|
141
|
+
min_supported_sdk: Optional[Dict[str, str]] = None
|
|
142
|
+
feature_flags: Optional[Dict[str, bool]] = None
|
|
143
|
+
deprecations: Optional[List[Dict[str, Any]]] = None
|
|
144
|
+
build: Optional[Dict[str, Any]] = None
|
|
145
|
+
mode: Optional[str] = None
|
|
139
146
|
|
|
140
147
|
|
|
141
148
|
@dataclass
|
|
@@ -105,7 +105,7 @@ def build_signature_message(
|
|
|
105
105
|
|
|
106
106
|
Current format (matches Rust server ``services/server/src/auth.rs``)::
|
|
107
107
|
|
|
108
|
-
"{timestamp}.{method}.{
|
|
108
|
+
"{timestamp}.{method}.{path_and_query}.{body_sha256}.{nonce}.{account_id}"
|
|
109
109
|
|
|
110
110
|
The trailing ``nonce`` was added in MED-1 (replay protection); the
|
|
111
111
|
``account_id`` was added in LOW-23 so an intermediary can't swap the
|
|
@@ -16,7 +16,7 @@ import nacl.signing
|
|
|
16
16
|
import pytest
|
|
17
17
|
import respx
|
|
18
18
|
|
|
19
|
-
from memwal.client import MemWal, MemWalError
|
|
19
|
+
from memwal.client import MemWal, MemWalCompatibilityError, MemWalError
|
|
20
20
|
from memwal.types import RecallManualOptions, RememberManualOptions
|
|
21
21
|
from memwal.utils import build_signature_message, bytes_to_hex, sha256_hex
|
|
22
22
|
|
|
@@ -35,7 +35,35 @@ _TEST_PACKAGE_ID = "0x" + "11" * 32
|
|
|
35
35
|
_TEST_SUI_RPC = "http://localhost:9001"
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
def _version_payload(
|
|
39
|
+
api_version: str = "1.0.0",
|
|
40
|
+
min_python: str = "0.1.0",
|
|
41
|
+
) -> dict[str, Any]:
|
|
42
|
+
return {
|
|
43
|
+
"relayerVersion": "0.1.0",
|
|
44
|
+
"apiVersion": api_version,
|
|
45
|
+
"minSupportedSdk": {
|
|
46
|
+
"typescript": "0.0.4",
|
|
47
|
+
"python": min_python,
|
|
48
|
+
"mcp": "0.0.1",
|
|
49
|
+
},
|
|
50
|
+
"featureFlags": {"runtime.versionEndpoint": True},
|
|
51
|
+
"deprecations": [],
|
|
52
|
+
"build": {},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _mock_version(
|
|
57
|
+
api_version: str = "1.0.0",
|
|
58
|
+
min_python: str = "0.1.0",
|
|
59
|
+
) -> None:
|
|
60
|
+
respx.get(f"{_TEST_SERVER}/version").mock(
|
|
61
|
+
return_value=httpx.Response(200, json=_version_payload(api_version, min_python))
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
38
65
|
def mock_seal_session_prereqs() -> None:
|
|
66
|
+
_mock_version()
|
|
39
67
|
respx.get(f"{_TEST_SERVER}/config").mock(
|
|
40
68
|
return_value=httpx.Response(
|
|
41
69
|
200,
|
|
@@ -409,10 +437,58 @@ class TestHealth:
|
|
|
409
437
|
assert result.status == "ok"
|
|
410
438
|
assert result.version == "0.1.0"
|
|
411
439
|
|
|
440
|
+
@respx.mock
|
|
441
|
+
async def test_compatibility(self, memwal_client: MemWal) -> None:
|
|
442
|
+
_mock_version()
|
|
443
|
+
|
|
444
|
+
metadata = await memwal_client.compatibility()
|
|
445
|
+
|
|
446
|
+
assert metadata["apiVersion"] == "1.0.0"
|
|
447
|
+
assert metadata["minSupportedSdk"]["python"] == "0.1.0"
|
|
448
|
+
|
|
449
|
+
@respx.mock
|
|
450
|
+
async def test_compatibility_rejects_unsupported_relayer(
|
|
451
|
+
self, memwal_client: MemWal
|
|
452
|
+
) -> None:
|
|
453
|
+
_mock_version(api_version="2.0.0")
|
|
454
|
+
|
|
455
|
+
with pytest.raises(MemWalCompatibilityError, match="supports relayer API 1.x"):
|
|
456
|
+
await memwal_client.compatibility()
|
|
457
|
+
|
|
458
|
+
@respx.mock
|
|
459
|
+
async def test_compatibility_rejects_old_sdk(self, memwal_client: MemWal) -> None:
|
|
460
|
+
_mock_version(min_python="9.0.0")
|
|
461
|
+
|
|
462
|
+
with pytest.raises(MemWalCompatibilityError, match="requires Python SDK >= 9.0.0"):
|
|
463
|
+
await memwal_client.recall("test")
|
|
464
|
+
|
|
465
|
+
@respx.mock
|
|
466
|
+
async def test_compatibility_rejects_missing_python_min(
|
|
467
|
+
self, memwal_client: MemWal
|
|
468
|
+
) -> None:
|
|
469
|
+
payload = _version_payload()
|
|
470
|
+
del payload["minSupportedSdk"]["python"]
|
|
471
|
+
respx.get(f"{_TEST_SERVER}/version").mock(
|
|
472
|
+
return_value=httpx.Response(200, json=payload)
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
with pytest.raises(MemWalCompatibilityError, match="minSupportedSdk.python"):
|
|
476
|
+
await memwal_client.compatibility()
|
|
477
|
+
|
|
478
|
+
@respx.mock
|
|
479
|
+
async def test_compatibility_rejects_invalid_python_min(
|
|
480
|
+
self, memwal_client: MemWal
|
|
481
|
+
) -> None:
|
|
482
|
+
_mock_version(min_python="latest")
|
|
483
|
+
|
|
484
|
+
with pytest.raises(MemWalCompatibilityError, match="invalid minSupportedSdk.python"):
|
|
485
|
+
await memwal_client.compatibility()
|
|
486
|
+
|
|
412
487
|
|
|
413
488
|
class TestManualAPI:
|
|
414
489
|
@respx.mock
|
|
415
490
|
async def test_remember_manual(self, memwal_client: MemWal) -> None:
|
|
491
|
+
_mock_version()
|
|
416
492
|
route = respx.post(f"{_TEST_SERVER}/api/remember/manual").mock(
|
|
417
493
|
return_value=httpx.Response(
|
|
418
494
|
200,
|
|
@@ -441,6 +517,7 @@ class TestManualAPI:
|
|
|
441
517
|
|
|
442
518
|
@respx.mock
|
|
443
519
|
async def test_recall_manual(self, memwal_client: MemWal) -> None:
|
|
520
|
+
_mock_version()
|
|
444
521
|
route = respx.post(f"{_TEST_SERVER}/api/recall/manual").mock(
|
|
445
522
|
return_value=httpx.Response(
|
|
446
523
|
200,
|
|
@@ -47,7 +47,7 @@ import httpx
|
|
|
47
47
|
import nacl.signing
|
|
48
48
|
import pytest
|
|
49
49
|
|
|
50
|
-
from memwal.client import MemWal, MemWalError, MemWalSync
|
|
50
|
+
from memwal.client import MemWal, MemWalCompatibilityError, MemWalError, MemWalSync
|
|
51
51
|
from memwal.utils import build_signature_message, bytes_to_hex
|
|
52
52
|
|
|
53
53
|
# ── Config ───────────────────────────────────────────────────────────────────
|
|
@@ -173,6 +173,8 @@ class TestAuthRejection:
|
|
|
173
173
|
mw = MemWalSync.create(key=unregistered_key, account_id="0x0", server_url=SERVER_URL)
|
|
174
174
|
with pytest.raises(MemWalError) as exc_info:
|
|
175
175
|
mw.remember("hello")
|
|
176
|
+
if isinstance(exc_info.value, MemWalCompatibilityError):
|
|
177
|
+
pytest.skip("live relayer does not expose compatibility metadata yet")
|
|
176
178
|
err = str(exc_info.value)
|
|
177
179
|
assert "401" in err or "403" in err, f"Expected 401/403 in: {err}"
|
|
178
180
|
|
|
@@ -51,10 +51,31 @@ _SERVER = "http://localhost:8000"
|
|
|
51
51
|
|
|
52
52
|
_RECALL_URL = f"{_SERVER}/api/recall"
|
|
53
53
|
_ANALYZE_URL = f"{_SERVER}/api/analyze"
|
|
54
|
+
_VERSION_URL = f"{_SERVER}/version"
|
|
54
55
|
_SUI_RPC_URL = "http://localhost:9001"
|
|
55
56
|
_PACKAGE_ID = "0x" + "11" * 32
|
|
56
57
|
|
|
57
58
|
|
|
59
|
+
def _mock_version() -> None:
|
|
60
|
+
respx.get(_VERSION_URL).mock(
|
|
61
|
+
return_value=httpx.Response(
|
|
62
|
+
200,
|
|
63
|
+
json={
|
|
64
|
+
"relayerVersion": "0.1.0",
|
|
65
|
+
"apiVersion": "1.0.0",
|
|
66
|
+
"minSupportedSdk": {
|
|
67
|
+
"typescript": "0.0.4",
|
|
68
|
+
"python": "0.1.0",
|
|
69
|
+
"mcp": "0.0.1",
|
|
70
|
+
},
|
|
71
|
+
"featureFlags": {"runtime.versionEndpoint": True},
|
|
72
|
+
"deprecations": [],
|
|
73
|
+
"build": {},
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
58
79
|
def _mock_seal_session_prereqs() -> None:
|
|
59
80
|
"""Register mocks for the SEAL session prerequisites.
|
|
60
81
|
|
|
@@ -66,6 +87,7 @@ def _mock_seal_session_prereqs() -> None:
|
|
|
66
87
|
|
|
67
88
|
Mirrors the helper of the same name in ``test_client.py``.
|
|
68
89
|
"""
|
|
90
|
+
_mock_version()
|
|
69
91
|
respx.get(f"{_SERVER}/config").mock(
|
|
70
92
|
return_value=httpx.Response(
|
|
71
93
|
200,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|