memwal 0.1.9.dev1__tar.gz → 0.1.9.dev3__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 (29) hide show
  1. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/CHANGELOG.md +2 -0
  2. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/PKG-INFO +1 -1
  3. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/memwal/__init__.py +1 -1
  4. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/memwal/client.py +66 -2
  5. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/pyproject.toml +1 -1
  6. memwal-0.1.9.dev3/tests/test_auth_rejected_message.py +30 -0
  7. memwal-0.1.9.dev3/tests/test_normalize_server_url.py +96 -0
  8. memwal-0.1.9.dev1/tests/test_auth_rejected_message.py +0 -9
  9. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/.gitignore +0 -0
  10. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/README.md +0 -0
  11. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/examples/.env.example +0 -0
  12. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/examples/.gitignore +0 -0
  13. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/examples/async_remember_demo.py +0 -0
  14. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/examples/interactive_demo.py +0 -0
  15. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/examples/verify_credentials.py +0 -0
  16. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/memwal/compatibility.py +0 -0
  17. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/memwal/middleware.py +0 -0
  18. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/memwal/mock.py +0 -0
  19. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/memwal/types.py +0 -0
  20. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/memwal/utils.py +0 -0
  21. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/notebooks/walrus_memory_python_sdk.ipynb +0 -0
  22. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/run_tests.py +0 -0
  23. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/tests/__init__.py +0 -0
  24. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/tests/test_client.py +0 -0
  25. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/tests/test_env_presets.py +0 -0
  26. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/tests/test_integration.py +0 -0
  27. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/tests/test_middleware.py +0 -0
  28. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/tests/test_mock.py +0 -0
  29. {memwal-0.1.9.dev1 → memwal-0.1.9.dev3}/tests/test_signing.py +0 -0
@@ -4,8 +4,10 @@
4
4
 
5
5
  ### Fixed
6
6
 
7
+ - HTTP 503 with `x-auth-error: AUTH_UPSTREAM_UNAVAILABLE` is reported as a retryable credential-verification outage, not a sign-in failure. Other 503s keep the generic sanitized body.
7
8
  - `remember_bulk_async` rejects an empty `items` list before the request and raises when the relayer returns a `job_ids` length that does not match the batch.
8
9
  - restore `truncated` docs now match WALM-431 retryable semantics.
10
+ - Warn when `server_url` uses plaintext `http://` against a non-localhost host, matching the TypeScript SDK `normalizeServerUrl` guard. Localhost, `127.0.0.1`, `::1`, and `*.localhost` are exempt; invalid URLs are left for the HTTP client to surface. The warning logs only scheme, host, and port so URL userinfo is not written to logs.
9
11
 
10
12
  ## 0.1.8
11
13
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: memwal
3
- Version: 0.1.9.dev1
3
+ Version: 0.1.9.dev3
4
4
  Summary: Python SDK for Walrus Memory — Privacy-first AI memory with Ed25519 signing
5
5
  Project-URL: Homepage, https://memory.walrus.xyz
6
6
  Project-URL: Documentation, https://memory.walrus.xyz
@@ -122,4 +122,4 @@ __all__ = [
122
122
  "RecallManualResult",
123
123
  ]
124
124
 
125
- __version__ = "0.1.9.dev1"
125
+ __version__ = "0.1.9.dev3"
@@ -35,6 +35,7 @@ import time
35
35
  import uuid
36
36
  from datetime import datetime, timezone
37
37
  from typing import Any, Dict, List, Optional, Sequence, Tuple, TypeVar, Union
38
+ from urllib.parse import ParseResult, urlparse
38
39
 
39
40
  import httpx
40
41
  import nacl.signing
@@ -97,11 +98,62 @@ AUTH_REJECTED_MESSAGE = (
97
98
  "and dashboard credentials. Full troubleshooting: "
98
99
  "https://docs.wal.app/walrus-memory/troubleshooting/overview#401-auth_rejected-errors"
99
100
  )
101
+ AUTH_UPSTREAM_UNAVAILABLE = "AUTH_UPSTREAM_UNAVAILABLE"
102
+ UPSTREAM_UNAVAILABLE_MESSAGE = (
103
+ "Walrus Memory temporarily cannot verify credentials (upstream unavailable). "
104
+ "Retry; this is not a sign-in failure."
105
+ )
100
106
 
101
107
 
102
108
  logger = logging.getLogger("memwal")
103
109
 
104
110
 
111
+ def _server_url_for_log(parsed: ParseResult) -> str:
112
+ """Scheme/host/port only — never userinfo, path, query, or fragment."""
113
+
114
+ host = parsed.hostname or ""
115
+ if ":" in host:
116
+ host = f"[{host}]"
117
+ if parsed.port is not None:
118
+ return f"{parsed.scheme}://{host}:{parsed.port}"
119
+ return f"{parsed.scheme}://{host}"
120
+
121
+
122
+ def normalize_server_url(url: str) -> str:
123
+ """Strip a trailing slash and warn on plaintext HTTP to a remote host.
124
+
125
+ Ports the TypeScript ``normalizeServerUrl`` helper: localhost,
126
+ ``127.0.0.1``, ``::1``, and ``*.localhost`` are exempt. Invalid URLs
127
+ are returned trimmed so the HTTP client can surface the error later.
128
+
129
+ The warning logs only scheme/host/port so URL userinfo (HTTPX
130
+ credentials) and other sensitive components are not written to logs.
131
+ The returned URL is otherwise unchanged and still used for transport.
132
+ """
133
+
134
+ trimmed = url.rstrip("/")
135
+ try:
136
+ parsed = urlparse(trimmed)
137
+ host = (parsed.hostname or "").lower()
138
+ is_local = (
139
+ host == "localhost"
140
+ or host == "127.0.0.1"
141
+ or host == "::1"
142
+ or host.endswith(".localhost")
143
+ )
144
+ if parsed.scheme == "http" and host and not is_local:
145
+ logger.warning(
146
+ '[memwal] serverUrl "%s" uses plaintext HTTP on a non-localhost host. '
147
+ "Signed requests and any bearer material will be visible to the network. "
148
+ "Use https:// in production.",
149
+ _server_url_for_log(parsed),
150
+ )
151
+ except ValueError:
152
+ # invalid URL — let the HTTP call surface the error at request time
153
+ pass
154
+ return trimmed
155
+
156
+
105
157
  # ============================================================
106
158
  # Polling helpers (PR #121 parity with TS SDK)
107
159
  # ============================================================
@@ -229,7 +281,7 @@ class MemWal:
229
281
  self._private_key_hex = normalize_private_key(config.key)
230
282
  self._signing_key = build_signing_key(self._private_key_hex)
231
283
  self._account_id = config.account_id
232
- self._server_url = config.server_url.rstrip("/")
284
+ self._server_url = normalize_server_url(config.server_url)
233
285
  self._namespace = config.namespace
234
286
  self._client: Optional[httpx.AsyncClient] = None
235
287
  self._server_config: Optional[Dict[str, str]] = None
@@ -1285,6 +1337,8 @@ class MemWal:
1285
1337
  raise _HttpStatusError(
1286
1338
  status=response.status_code,
1287
1339
  body=err_text,
1340
+ auth_error=response.headers.get("x-auth-error"),
1341
+ retry_after=response.headers.get("retry-after"),
1288
1342
  )
1289
1343
 
1290
1344
  return response.json()
@@ -1328,15 +1382,25 @@ class _HttpStatusError(MemWalError):
1328
1382
  explicitly accepted).
1329
1383
  """
1330
1384
 
1331
- def __init__(self, status: int, body: str) -> None:
1385
+ def __init__(
1386
+ self,
1387
+ status: int,
1388
+ body: str,
1389
+ auth_error: str | None = None,
1390
+ retry_after: str | None = None,
1391
+ ) -> None:
1332
1392
  if status == 401:
1333
1393
  super().__init__(AUTH_REJECTED_MESSAGE)
1394
+ elif status == 503 and auth_error == AUTH_UPSTREAM_UNAVAILABLE:
1395
+ super().__init__(UPSTREAM_UNAVAILABLE_MESSAGE)
1334
1396
  else:
1335
1397
  super().__init__(
1336
1398
  f"Walrus Memory API error ({status}): {_redact_internal_urls(body)}"
1337
1399
  )
1338
1400
  self.status = status
1339
1401
  self.body = body
1402
+ self.auth_error = auth_error
1403
+ self.retry_after = retry_after
1340
1404
 
1341
1405
 
1342
1406
  class MemWalRememberJobNotFound(MemWalError):
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "memwal"
7
- version = "0.1.9.dev1"
7
+ version = "0.1.9.dev3"
8
8
  description = "Python SDK for Walrus Memory — Privacy-first AI memory with Ed25519 signing"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -0,0 +1,30 @@
1
+ """Tests for the AUTH_REJECTED_MESSAGE shown on a 401 from the relayer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from memwal.client import (
6
+ AUTH_REJECTED_MESSAGE,
7
+ AUTH_UPSTREAM_UNAVAILABLE,
8
+ UPSTREAM_UNAVAILABLE_MESSAGE,
9
+ _HttpStatusError,
10
+ )
11
+
12
+
13
+ def test_auth_rejected_message_points_to_troubleshooting_guide() -> None:
14
+ assert "docs.wal.app/walrus-memory/troubleshooting/overview" in AUTH_REJECTED_MESSAGE
15
+
16
+
17
+ def test_auth_503_is_retryable_not_a_credential_failure() -> None:
18
+ err = _HttpStatusError(
19
+ 503, "upstream unavailable", auth_error=AUTH_UPSTREAM_UNAVAILABLE, retry_after="5"
20
+ )
21
+ assert str(err) == UPSTREAM_UNAVAILABLE_MESSAGE
22
+ assert "sign-in" in str(err)
23
+ assert "401" not in str(err)
24
+ assert err.retry_after == "5"
25
+
26
+
27
+ def test_non_auth_503_keeps_generic_body() -> None:
28
+ err = _HttpStatusError(503, "Rate limiter temporarily unavailable")
29
+ assert "Rate limiter temporarily unavailable" in str(err)
30
+ assert "cannot verify credentials" not in str(err)
@@ -0,0 +1,96 @@
1
+ """Tests for plaintext HTTP server_url guarding (WALM-452 / #748).
2
+
3
+ No network: ``MemWal.create`` only stores config, and
4
+ ``normalize_server_url`` is a pure parse + log helper.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+
11
+ import pytest
12
+
13
+ from memwal.client import MemWal, normalize_server_url
14
+
15
+ _KEY = "ab" * 32
16
+ _ACCOUNT = "0xdummy"
17
+
18
+
19
+ def test_plaintext_remote_warns_and_strips(caplog: pytest.LogCaptureFixture) -> None:
20
+ caplog.set_level(logging.WARNING, logger="memwal")
21
+ assert (
22
+ normalize_server_url("http://relayer.example.com/")
23
+ == "http://relayer.example.com"
24
+ )
25
+ assert "plaintext" in caplog.text
26
+
27
+
28
+ def test_https_remote_does_not_warn(caplog: pytest.LogCaptureFixture) -> None:
29
+ caplog.set_level(logging.WARNING, logger="memwal")
30
+ assert (
31
+ normalize_server_url("https://relayer.memory.walrus.xyz")
32
+ == "https://relayer.memory.walrus.xyz"
33
+ )
34
+ assert caplog.text == ""
35
+
36
+
37
+ @pytest.mark.parametrize(
38
+ "url",
39
+ [
40
+ "http://localhost:8000",
41
+ "http://127.0.0.1:8000",
42
+ "http://foo.localhost",
43
+ "http://[::1]:8000",
44
+ ],
45
+ )
46
+ def test_plaintext_local_does_not_warn(
47
+ url: str, caplog: pytest.LogCaptureFixture
48
+ ) -> None:
49
+ caplog.set_level(logging.WARNING, logger="memwal")
50
+ assert normalize_server_url(url) == url
51
+ assert caplog.text == ""
52
+
53
+
54
+ def test_create_warns_on_plaintext_remote(caplog: pytest.LogCaptureFixture) -> None:
55
+ caplog.set_level(logging.WARNING, logger="memwal")
56
+ client = MemWal.create(
57
+ key=_KEY,
58
+ account_id=_ACCOUNT,
59
+ server_url="http://relayer.example.com/",
60
+ )
61
+ assert client._server_url == "http://relayer.example.com"
62
+ assert "plaintext" in caplog.text
63
+
64
+
65
+ def test_plaintext_remote_warning_omits_url_credentials(
66
+ caplog: pytest.LogCaptureFixture,
67
+ ) -> None:
68
+ """HTTPX userinfo must stay on the transport URL and out of the warning."""
69
+
70
+ caplog.set_level(logging.WARNING, logger="memwal")
71
+ url = "http://alice:example-secret@relayer.example.com/?token=super-secret"
72
+ assert (
73
+ normalize_server_url(url)
74
+ == "http://alice:example-secret@relayer.example.com/?token=super-secret"
75
+ )
76
+ assert "plaintext" in caplog.text
77
+ assert "http://relayer.example.com" in caplog.text
78
+ assert "alice" not in caplog.text
79
+ assert "example-secret" not in caplog.text
80
+ assert "super-secret" not in caplog.text
81
+ for rec in caplog.records:
82
+ assert "example-secret" not in rec.getMessage()
83
+ assert "example-secret" not in str(rec.args)
84
+ assert "super-secret" not in rec.getMessage()
85
+ assert "super-secret" not in str(rec.args)
86
+
87
+
88
+ def test_create_preserves_url_credentials_and_redacts_warning(
89
+ caplog: pytest.LogCaptureFixture,
90
+ ) -> None:
91
+ caplog.set_level(logging.WARNING, logger="memwal")
92
+ url = "http://alice:example-secret@relayer.example.com/"
93
+ client = MemWal.create(key=_KEY, account_id=_ACCOUNT, server_url=url)
94
+ assert client._server_url == "http://alice:example-secret@relayer.example.com"
95
+ assert "plaintext" in caplog.text
96
+ assert "example-secret" not in caplog.text
@@ -1,9 +0,0 @@
1
- """Tests for the AUTH_REJECTED_MESSAGE shown on a 401 from the relayer."""
2
-
3
- from __future__ import annotations
4
-
5
- from memwal.client import AUTH_REJECTED_MESSAGE
6
-
7
-
8
- def test_auth_rejected_message_points_to_troubleshooting_guide() -> None:
9
- assert "docs.wal.app/walrus-memory/troubleshooting/overview" in AUTH_REJECTED_MESSAGE
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes