Feat: rendez#103
Conversation
AI-Generated Change:
- Model: claude-sonnet-4-6
- Intent: enable bus client to periodically retrieve INTERCOM messages from
one or more rendezvous servers without requiring a simultaneous connection
- Impact:
- New __init__ kwargs: rendezvous_urls (List[str], default None),
rendezvous_poll_interval (float, default 60.0 seconds)
- Background daemon thread (_rendezvous_poller) starts automatically when
rendezvous_urls is provided; stops on on_close()
- _poll_rendezvous(): signs proof-of-ownership, POSTs to /retrieve,
feeds each returned HiveMessage into _handle_hive_protocol()
- Errors (HTTP 4xx/5xx, connection refused) are logged and swallowed —
polling continues on next interval
- _stop_rendezvous_polling() guards against missing attrs for legacy
__new__-constructed instances (no regression in existing tests)
- 12 new unit tests covering thread lifecycle, injection, and error paths
- Verified via: uv run pytest test/ -q (289 passed, 4 skipped)
AI-Generated Change:
- Model: claude-sonnet-4-6
- Intent: bring all docs in sync with the rendezvous_urls polling feature added
in the previous commit
- Impact:
- docs/index.md: added Rendezvous Polling to Key Features list
- docs/api.md: added HiveMessageBusClient constructor parameters table;
added Rendezvous Polling subsection with _start/_stop/_poll methods;
updated RENDEZVOUS HiveMessageType description from "Reserved" to accurate
- docs/client_api.md: added §1 Rendezvous Polling subsection with usage example
- docs/message_types.md: added RENDEZVOUS row to HiveMessageType table;
added RENDEZVOUS section explaining HTTP plugin, polling flow, and privacy
- docs/examples.md: added two examples — receiving via polling, depositing
a message from the sender side
- Verified via: manual review
…vous polling AI-Generated Change: - Model: claude-sonnet-4-6 - Intent: resolve four cryptographic soundness issues Fix 1 & 2 — Server-bound ownership proof in rendezvous polling (client.py): _fetch_rendezvous_server_pubkey() added — fetches and caches GET /pubkey. _poll_rendezvous() passes server_pubkey to sign_ownership() so proof is bound to the specific server (prevents cross-server replay within 60s window). Fix 3 — hybrid_decrypt signature verification (encryption.py): New optional param verify_key: when provided AND envelope has "signature", verifies PSS-SHA256 over ciphertext before decryption. Raises ValueError on failure or if signature is absent when verify_key is supplied. handle_intercom() in protocol.py now passes sender's pubkey (from HiveMapper) as verify_key when the envelope carries a signature and the sender is known. Fix 4 — Recipient binding via AES-GCM AAD (encryption.py): hybrid_encrypt() gains optional recipient_pubkey: SHA-256 fingerprint of recipient's PEM used as GCM AAD; stored as "recipient_fingerprint" in envelope. hybrid_decrypt() gains optional expected_recipient: verifies AAD before returning plaintext — GCM auth fails if envelope was intended for a different recipient. emit_intercom() passes recipient_pubkey=pubkey so all sent INTERCOM messages carry the binding. handle_intercom() passes expected_recipient=own_pubkey when recipient_fingerprint is present. Backward compatibility: all new params default to None/off; unsigned or unbound legacy envelopes continue to decrypt without error. Tests: - test_encryption.py: 8 new tests (Fix 3: 4, Fix 4: 4) - test_rendezvous_polling.py: _make_client updated with server pubkey cache 296 passed, 4 skipped. - Verified via: uv run pytest test/ -q (296 passed, 4 skipped)
📝 WalkthroughWalkthroughThis pull request introduces asynchronous rendezvous polling to HiveMessageBusClient, enabling clients to retrieve messages from Changes
Sequence DiagramsequenceDiagram
participant Client as HiveMessageBusClient
participant BgThread as Polling Thread
participant Server as Rendezvous Server
participant Handler as _handle_hive_protocol()
Note over Client,Handler: Initialization
Client->>Client: __init__(rendezvous_urls=...)
Client->>BgThread: _start_rendezvous_polling()
activate BgThread
Note over BgThread,Server: Polling Loop
loop Every rendezvous_poll_interval seconds
BgThread->>Server: GET /pubkey (fetch RSA public key)
Server-->>BgThread: pubkey (cached)
BgThread->>BgThread: Create ownership signature<br/>(pubkey + timestamp)
BgThread->>Server: POST /retrieve { signature, ... }
Server-->>BgThread: [serialized HiveMessage, ...]
Note over BgThread: For each message
BgThread->>BgThread: Deserialize HiveMessage
BgThread->>Handler: _handle_hive_protocol(message)
Handler->>Handler: Route to subscribers
end
Note over Client,Handler: Shutdown
Client->>BgThread: _stop_rendezvous_polling()<br/>(on_close event)
deactivate BgThread
BgThread->>BgThread: Exit polling loop
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~90 minutes Possibly Related PRs
Suggested Labels
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The bots have finished their work. Take a look! 🤖I've aggregated the results of the automated checks for this PR below. 📋 Repo HealthChecking the pulse of the repository's maintenance. 💓 Latest Version: ✅ 🏷️ Release PreviewEnsuring our release notes are clear and concise. ✍️ Current:
✅ PR title follows conventional commit format. 🚀 Release Channel Compatibility Predicted next version:
🔒 Security (pip-audit)Locking the doors and checking the windows... 🔒 ✅ No known vulnerabilities found (61 packages scanned). 🔍 LintThe results are fresh out of the pipeline. 🏗️ ❌ ruff: issues found — see job log 📊 CoverageCharting the progress of our testing efforts. 📉 ❌ 56.8% total coverage Files below 80% coverage (6 files)
Full report: download the 🔨 Build TestsChecking the alignment of our build components. 📏
❌ 3.10: Install OK, tests failed An automated high-five for your latest changes! 🖐️ |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
test/unittests/test_encryption.py (1)
196-211: Consider extracting tempfile key loading to a helper to reduce duplication.The pattern of writing PEM to tempfile, calling
load_RSA_key, then unlinking is repeated ~10 times. A fixture or helper would improve maintainability.♻️ Optional: Extract helper fixture
`@pytest.fixture` def load_key_from_pem(): """Helper to load RSA key from PEM string via tempfile.""" import tempfile, os from poorman_handshake.asymmetric.utils import load_RSA_key def _load(pem: str): with tempfile.NamedTemporaryFile(delete=False, suffix=".pem", mode="w") as f: f.write(pem) tmp = f.name try: return load_RSA_key(tmp) finally: os.unlink(tmp) return _loadThen use:
sender_priv = load_key_from_pem(sender_priv_pem)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/unittests/test_encryption.py` around lines 196 - 211, The test repeats writing PEM strings to temp files, calling load_RSA_key, and unlinking; extract that logic into a helper/fixture (e.g., load_key_from_pem) that takes a PEM string, creates the NamedTemporaryFile, writes the PEM, calls load_RSA_key(path), unlinks the temp in a finally, and returns the loaded key; then replace repeated blocks (where sender_priv = load_RSA_key(tmp) and recipient_priv = load_RSA_key(tmp2), etc.) with calls to the helper (e.g., sender_priv = load_key_from_pem(sender_priv_pem), recipient_priv = load_key_from_pem(recipient_priv_pem)).hivemind_bus_client/client.py (1)
282-289: Polling loop has correct stop semantics but initial poll is delayed.The
wait(timeout=...)call means the first poll happens only afterrendezvous_poll_intervalseconds. If immediate polling on startup is desired, consider polling once before entering the loop.♻️ Optional: Poll immediately on startup
def _rendezvous_poll_loop(self) -> None: """Background loop: poll every configured rendezvous URL at the configured interval.""" + # Initial poll immediately on startup + for url in self._rendezvous_urls: + try: + self._poll_rendezvous(url.rstrip("/")) + except Exception: + LOG.exception("Rendezvous poll error for %s", url) while not self._rendezvous_stop_event.wait(timeout=self._rendezvous_poll_interval): for url in self._rendezvous_urls:🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@hivemind_bus_client/client.py` around lines 282 - 289, The loop in _rendezvous_poll_loop currently waits before the first poll so the initial rendezvous is delayed; call _poll_rendezvous(url.rstrip("/")) once for each url in self._rendezvous_urls (wrapping each call in the same try/except that logs via LOG.exception) before entering the existing while not self._rendezvous_stop_event.wait(timeout=self._rendezvous_poll_interval) loop so you perform an immediate startup poll while preserving stop semantics and existing error handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/examples.md`:
- Around line 273-275: The example calls hybrid_encrypt(recipient_pubkey,
HiveMessage(...).serialize(), sign_key=private_key) but does not pass the
recipient_pubkey as the named parameter required to include
recipient_fingerprint; update the call to supply recipient_pubkey (e.g.,
recipient_pubkey=recipient_pubkey) so hybrid_encrypt produces an envelope bound
to the recipient; keep the rest (HiveMessage, HiveMessageType.BUS,
HiveMessageType.INTERCOM, payload=envelope, sign_key=private_key) unchanged.
In `@docs/index.md`:
- Line 13: The markdown contains a literal "\n- " sequence in the bullet list
(around the "Binary Support" and "Rendezvous Polling" items) so "Rendezvous
Polling" is not on its own line; edit the docs index content where the list is
defined and replace the escaped newline sequence with an actual newline so each
bullet (e.g., the "Binary Support" and "Rendezvous Polling" items) is on its own
line, ensuring standard Markdown list formatting.
In `@hivemind_bus_client/client.py`:
- Line 324: The import of sign_ownership from hivemind_rendezvous.auth is
unguarded and the package isn't listed as a dependency; either add
hivemind_rendezvous to the project dependencies (pyproject.toml/requirements) or
make the import optional by wrapping "from hivemind_rendezvous.auth import
sign_ownership" in a try/except ImportError that sets sign_ownership = None (or
a no-op) and update any callers in this module to check for None and raise a
clear, actionable error or skip signing when unavailable; locate the import and
its usages (symbol: sign_ownership) and implement the conditional handling to
gracefully degrade or fail with an instructive message.
In `@test/unittests/test_rendezvous_polling.py`:
- Around line 102-104: The tests fail because patching
"hivemind_rendezvous.auth.sign_ownership" tries to import a module that may not
be installed; update each use of patch for _PATCH_SIGN (and any `@patch`
decorators targeting hivemind_rendezvous.auth.sign_ownership) to pass
create=True (i.e., patch(_PATCH_SIGN, ..., create=True)), or alternatively add a
pytest fixture that injects a dummy hivemind_rendezvous module with an
auth.sign_ownership attribute so that calls to patch() succeed without raising
ModuleNotFoundError.
---
Nitpick comments:
In `@hivemind_bus_client/client.py`:
- Around line 282-289: The loop in _rendezvous_poll_loop currently waits before
the first poll so the initial rendezvous is delayed; call
_poll_rendezvous(url.rstrip("/")) once for each url in self._rendezvous_urls
(wrapping each call in the same try/except that logs via LOG.exception) before
entering the existing while not
self._rendezvous_stop_event.wait(timeout=self._rendezvous_poll_interval) loop so
you perform an immediate startup poll while preserving stop semantics and
existing error handling.
In `@test/unittests/test_encryption.py`:
- Around line 196-211: The test repeats writing PEM strings to temp files,
calling load_RSA_key, and unlinking; extract that logic into a helper/fixture
(e.g., load_key_from_pem) that takes a PEM string, creates the
NamedTemporaryFile, writes the PEM, calls load_RSA_key(path), unlinks the temp
in a finally, and returns the loaded key; then replace repeated blocks (where
sender_priv = load_RSA_key(tmp) and recipient_priv = load_RSA_key(tmp2), etc.)
with calls to the helper (e.g., sender_priv =
load_key_from_pem(sender_priv_pem), recipient_priv =
load_key_from_pem(recipient_priv_pem)).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a0618109-f2d9-4047-8c4c-78f8ce157dc6
📒 Files selected for processing (10)
docs/api.mddocs/client_api.mddocs/examples.mddocs/index.mddocs/message_types.mdhivemind_bus_client/client.pyhivemind_bus_client/encryption.pyhivemind_bus_client/protocol.pytest/unittests/test_encryption.pytest/unittests/test_rendezvous_polling.py
| envelope = hybrid_encrypt(recipient_pubkey, HiveMessage(HiveMessageType.BUS, inner).serialize(), | ||
| sign_key=private_key) | ||
| msg = HiveMessage(HiveMessageType.INTERCOM, payload=envelope) |
There was a problem hiding this comment.
Add recipient_pubkey to enable recipient binding in the example.
The example encrypts for recipient_pubkey but doesn't pass it as the recipient_pubkey parameter, so the envelope won't include recipient_fingerprint for binding. This should match the new API.
📝 Proposed fix
envelope = hybrid_encrypt(recipient_pubkey, HiveMessage(HiveMessageType.BUS, inner).serialize(),
- sign_key=private_key)
+ sign_key=private_key,
+ recipient_pubkey=recipient_pubkey)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| envelope = hybrid_encrypt(recipient_pubkey, HiveMessage(HiveMessageType.BUS, inner).serialize(), | |
| sign_key=private_key) | |
| msg = HiveMessage(HiveMessageType.INTERCOM, payload=envelope) | |
| envelope = hybrid_encrypt(recipient_pubkey, HiveMessage(HiveMessageType.BUS, inner).serialize(), | |
| sign_key=private_key, | |
| recipient_pubkey=recipient_pubkey) | |
| msg = HiveMessage(HiveMessageType.INTERCOM, payload=envelope) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/examples.md` around lines 273 - 275, The example calls
hybrid_encrypt(recipient_pubkey, HiveMessage(...).serialize(),
sign_key=private_key) but does not pass the recipient_pubkey as the named
parameter required to include recipient_fingerprint; update the call to supply
recipient_pubkey (e.g., recipient_pubkey=recipient_pubkey) so hybrid_encrypt
produces an envelope bound to the recipient; keep the rest (HiveMessage,
HiveMessageType.BUS, HiveMessageType.INTERCOM, payload=envelope,
sign_key=private_key) unchanged.
| - **CASCADE Aggregation**: Collects responses from all nodes with timeout and early resolution via `HiveMapper`. | ||
| - **PING Discovery**: Flood-based topology mapping with public key and locale announcement. | ||
| - **Binary Support**: Optimized handling for binary payloads such as TTS audio and file transfers. | ||
| - **Binary Support**: Optimized handling for binary payloads such as TTS audio and file transfers.\n- **Rendezvous Polling**: Optional background thread that periodically retrieves INTERCOM messages from one or more `hivemind-rendezvous` servers, enabling async communication with nodes from non-simultaneously-connected hives. |
There was a problem hiding this comment.
Fix inline \n — should be an actual line break.
The literal \n- string appears in the markdown source. This should be a proper newline to render the "Rendezvous Polling" feature as a separate bullet point.
📝 Proposed fix
-- **Binary Support**: Optimized handling for binary payloads such as TTS audio and file transfers.\n- **Rendezvous Polling**: Optional background thread that periodically retrieves INTERCOM messages from one or more `hivemind-rendezvous` servers, enabling async communication with nodes from non-simultaneously-connected hives.
+- **Binary Support**: Optimized handling for binary payloads such as TTS audio and file transfers.
+- **Rendezvous Polling**: Optional background thread that periodically retrieves INTERCOM messages from one or more `hivemind-rendezvous` servers, enabling async communication with nodes from non-simultaneously-connected hives.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - **Binary Support**: Optimized handling for binary payloads such as TTS audio and file transfers.\n- **Rendezvous Polling**: Optional background thread that periodically retrieves INTERCOM messages from one or more `hivemind-rendezvous` servers, enabling async communication with nodes from non-simultaneously-connected hives. | |
| - **Binary Support**: Optimized handling for binary payloads such as TTS audio and file transfers. | |
| - **Rendezvous Polling**: Optional background thread that periodically retrieves INTERCOM messages from one or more `hivemind-rendezvous` servers, enabling async communication with nodes from non-simultaneously-connected hives. |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/index.md` at line 13, The markdown contains a literal "\n- " sequence in
the bullet list (around the "Binary Support" and "Rendezvous Polling" items) so
"Rendezvous Polling" is not on its own line; edit the docs index content where
the list is defined and replace the escaped newline sequence with an actual
newline so each bullet (e.g., the "Binary Support" and "Rendezvous Polling"
items) is on its own line, ensuring standard Markdown list formatting.
| Args: | ||
| base_url: Rendezvous server base URL (trailing slash already stripped). | ||
| """ | ||
| from hivemind_rendezvous.auth import sign_ownership |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if hivemind_rendezvous is declared as a dependency
echo "=== Checking setup.py ==="
cat setup.py 2>/dev/null | grep -i rendezvous || echo "Not found in setup.py"
echo "=== Checking pyproject.toml ==="
cat pyproject.toml 2>/dev/null | grep -i rendezvous || echo "Not found in pyproject.toml"
echo "=== Checking requirements*.txt ==="
cat requirements*.txt 2>/dev/null | grep -i rendezvous || echo "Not found in requirements"Repository: JarbasHiveMind/hivemind-websocket-client
Length of output: 252
🏁 Script executed:
# Check what dependencies are actually declared
echo "=== Checking setup.py dependencies ==="
head -100 setup.py 2>/dev/null || echo "setup.py not found or too small"
echo -e "\n=== Checking pyproject.toml ==="
cat pyproject.toml 2>/dev/null || echo "pyproject.toml not found"
echo -e "\n=== Checking for any requirements files ==="
ls -la requirements*.txt 2>/dev/null || echo "No requirements files found"Repository: JarbasHiveMind/hivemind-websocket-client
Length of output: 4183
🏁 Script executed:
# View the code context around line 324 in hivemind_bus_client/client.py
echo "=== Code context around line 324 ==="
sed -n '310,340p' hivemind_bus_client/client.py 2>/dev/null || echo "File or lines not found"Repository: JarbasHiveMind/hivemind-websocket-client
Length of output: 1447
🏁 Script executed:
# Check requirements.txt content
cat requirements.txt
# Get more context around line 324 to check for error handling
echo -e "\n=== Broader context (lines 300-360) ==="
sed -n '300,360p' hivemind_bus_client/client.pyRepository: JarbasHiveMind/hivemind-websocket-client
Length of output: 2977
Add hivemind_rendezvous to dependencies or implement graceful error handling.
The import from hivemind_rendezvous.auth import sign_ownership at line 324 is missing error handling. The package is not listed in pyproject.toml or requirements.txt. Either add hivemind_rendezvous to project dependencies, or wrap the import in a try-except block to gracefully degrade if the package is unavailable (since the method uses a lazy import pattern suggesting optional functionality).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@hivemind_bus_client/client.py` at line 324, The import of sign_ownership from
hivemind_rendezvous.auth is unguarded and the package isn't listed as a
dependency; either add hivemind_rendezvous to the project dependencies
(pyproject.toml/requirements) or make the import optional by wrapping "from
hivemind_rendezvous.auth import sign_ownership" in a try/except ImportError that
sets sign_ownership = None (or a no-op) and update any callers in this module to
check for None and raise a clear, actionable error or skip signing when
unavailable; locate the import and its usages (symbol: sign_ownership) and
implement the conditional handling to gracefully degrade or fail with an
instructive message.
| _PATCH_URLOPEN = "hivemind_bus_client.client.urllib.request.urlopen" | ||
| _PATCH_LOAD = "hivemind_bus_client.client.load_RSA_key" | ||
| _PATCH_SIGN = "hivemind_rendezvous.auth.sign_ownership" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check how sign_ownership is imported in client.py to determine correct patch target
# Find the import statement for sign_ownership
rg -n "sign_ownership" --type py -C 3Repository: JarbasHiveMind/hivemind-websocket-client
Length of output: 2175
🏁 Script executed:
#!/bin/bash
# Check the test file structure and how the patches are used
cat -n test/unittests/test_rendezvous_polling.py | head -180Repository: JarbasHiveMind/hivemind-websocket-client
Length of output: 8007
🏁 Script executed:
#!/bin/bash
# Check if hivemind_rendezvous is listed as a dependency
find . -name "setup.py" -o -name "setup.cfg" -o -name "pyproject.toml" -o -name "requirements*.txt" | head -10Repository: JarbasHiveMind/hivemind-websocket-client
Length of output: 130
🏁 Script executed:
#!/bin/bash
# Check dependencies in setup files
cat setup.py 2>/dev/null || cat setup.cfg 2>/dev/null || cat pyproject.toml 2>/dev/nullRepository: JarbasHiveMind/hivemind-websocket-client
Length of output: 2677
🏁 Script executed:
#!/bin/bash
# Check requirements.txt to see if hivemind_rendezvous is listed
cat requirements.txtRepository: JarbasHiveMind/hivemind-websocket-client
Length of output: 264
🏁 Script executed:
#!/bin/bash
# Check if there are test-specific requirements
cat requirements-test.txt 2>/dev/null || echo "No test requirements file found"Repository: JarbasHiveMind/hivemind-websocket-client
Length of output: 115
🏁 Script executed:
#!/bin/bash
# Check pyproject.toml for optional dependencies
cat pyproject.tomlRepository: JarbasHiveMind/hivemind-websocket-client
Length of output: 1407
Use create=True in the patch() calls or add a mock fixture for hivemind_rendezvous to avoid ModuleNotFoundError when the module is not installed.
Since hivemind_rendezvous is not listed as a dependency, the tests fail when patch("hivemind_rendezvous.auth.sign_ownership") tries to import the non-existent module. The simplest fix is to pass create=True to each patch() call targeting this path, or set up a module mock in a pytest fixture.
Patch approach (minimal change)
Replace all with patch(_PATCH_SIGN, ...) calls with with patch(_PATCH_SIGN, ..., create=True):, or add create=True to the @patch(_PATCH_SIGN) decorator if used as a decorator elsewhere.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@test/unittests/test_rendezvous_polling.py` around lines 102 - 104, The tests
fail because patching "hivemind_rendezvous.auth.sign_ownership" tries to import
a module that may not be installed; update each use of patch for _PATCH_SIGN
(and any `@patch` decorators targeting hivemind_rendezvous.auth.sign_ownership) to
pass create=True (i.e., patch(_PATCH_SIGN, ..., create=True)), or alternatively
add a pytest fixture that injects a dummy hivemind_rendezvous module with an
auth.sign_ownership attribute so that calls to patch() succeed without raising
ModuleNotFoundError.
Summary by CodeRabbit
Release Notes
New Features
Documentation
Tests