Skip to content

Release 0.0.19a1 - #67

Closed
github-actions[bot] wants to merge 84 commits into
masterfrom
release-0.0.19a1
Closed

github-actions[bot] wants to merge 84 commits into
masterfrom
release-0.0.19a1

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

Human review requested! This supersedes any earlier open release proposal for this repository.

JarbasAl and others added 30 commits February 6, 2025 21:45
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* fix: stop reading the removed message_blacklist; use sha256 for self-signed cert

Supersedes the fork band-aid #12. message_blacklist was removed from the
client model (whitelist-only via allowed_types); drop the dead read rather
than defaulting it. Also signs the generated self-signed cert with sha256.

* feat: migrate to pyproject.toml, modernize CI, add tests

- Replace setup.py with pyproject.toml (dynamic version via version.py.__version__)
- Add hivemind-bus-client as runtime dep; test extras mirror websocket-protocol
- Replace old TigreGotico/gh-automations@master workflows with
  OpenVoiceOS/gh-automations@dev equivalents (build-tests, coverage,
  lint, pip-audit, license-check, release-preview, repo-health,
  publish-stable, release-workflow)
- Add __version__ computed string to version.py (required by setuptools dynamic)
- Add tests/: unit coverage for decode_auth, cert generation, run()
  lifecycle, version module; hivescope e2e smoke test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* tests: raise handler coverage to 100%

Add test_handlers.py covering get_client (cache hit/miss, invalid key,
password handshake, send/disconnect closures, blacklist defaults) and all
five HTTP handler classes (ConnectHandler, DisconnectHandler,
SendMessageHandler, GetMessagesHandler, GetBinMessagesHandler) including
auth-missing, not-connected, exception/500, and inner-loop break paths.
Total coverage: 100% (239/239 stmts), 50 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…ackstop (#15)

* fix: pin poorman-handshake>=2.0.0a1 + disable-able runtime password backstop

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: defensive import of runtime_password_min_bits (works against released hivemind-core)

* ci: re-trigger against published hivemind-core 4.7.0a1 (poorman 2.0)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: adapt handlers/tests to hivemind-core 4.7.0a1 (v3 + poorman 2.0)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
The HTTP NetworkProtocol read port = int(self.config.get("port", 5678)),
copying the WebSocket binding's default (hivemind_websocket_protocol also
defaults to 5678). Running both bindings together with no explicit port
config collided them on the same port.

HIVEMIND-TRANSPORT-1 §4 recommends port 5679 for the HTTP binding, and
the shipped hivemind_bus_client.http_client already assumes 5679 as its
default. The server-side default was the one out of step with both the
spec and the client.

Back-compat: this changes the default only. Deployments that set `port`
explicitly are unaffected. A deployment relying on the old 5678 default
for HTTP must set `port: 5678` explicitly or move to 5679 -- and if it
ran HTTP and WebSocket together, it was already broken by the collision.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
get_client() set client.skill_blacklist and client.intent_blacklist on the
connection. HiveMindClientConnection has no such fields — the dataclass
just accepted the ad-hoc attributes — and nothing ever read them.

Blacklist enforcement moved to OVOSAgentPolicy (HiveMind-core#85), which
resolves the DB row through client.resolve_user(db) and reads
user.skill_blacklist there. These assignments are vestigial from before
that change, and read as though the transport applies an ACL when it
does not.

The two tests asserting the attributes were pinning the dead behaviour.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: sync the client database on api-key miss, not on every request

get_client() called db.sync() before every api-key lookup. HTTP is
request-oriented, so an unknown key drove a full database sync per
request rather than per connection: an unauthenticated caller could
sync the database as fast as it could issue GETs.

Look the key up first and sync only on a miss, debounced across
handlers, then look again. Ported from the websocket protocol, where
the same fix landed as part of keeping open() off the hot path.

Also drop undelivered_bin on disconnect. do_disconnect() popped
undelivered and clients but not undelivered_bin, and since it is a
defaultdict every poll for binary data recreated the entry — the queue
and whatever it still held survived for the life of the process.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: allow first HTTP client db sync

* fix: add shared HTTP session state

* fix: reuse server redis config for HTTP sessions

* fix: keep HTTP Redis session keys in db prefix

* fix: debounce sentinel must not treat early monotonic time as recent

A fresh ClientDatabaseSync used 0.0 as the never-synced sentinel and
compared it against time.monotonic(); on hosts with low uptime the very
first sync landed inside the debounce window and was skipped. Track the
last sync as Optional[float] instead. Also call db.sync() directly —
every ClientDatabase provides it — instead of feature-detecting it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: give the _Protocol double the identity_rsa_key core now reads

HiveMindClientConnection builds its per-connection HandShake from
hm_protocol.identity_rsa_key instead of re-importing the PEM per
connection, so the double raised AttributeError the moment a client was
constructed.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Gaëtan Trellu <gaetan.trellu@gmail.com>
Reconciled with the replica-aware session state from fix/db-sync-on-miss:
ClientRegistry is the memory session backend (connections plus pending
text/binary queues) and the local connection cache when redis owns the
cross-replica state. Also drops the dead skill/intent blacklist
assignments in get_client — HiveMindClientConnection declares neither
and enforcement lives in the agent policy layer.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
JarbasAl and others added 27 commits September 1, 2026 22:09
* fix: tolerate HiveMind-core 5.x dropping the legacy handshake flags

HiveMind-core 5.x removed handshake_enabled and require_crypto: v3 Noise
is the sole transport crypto there, always negotiated, so there is no
"handshake disabled" mode and no separate pre-shared crypto requirement.

Reading them directly raises AttributeError, and because it happens
inside ConnectHandler.post every /connect answers 500:

  Connection failed: 'HiveMindListenerProtocol' object has no attribute
  'handshake_enabled'

That makes 0.0.11a1 unusable with core 5.x. Any client reaching its hub
over the HTTP protocol simply cannot connect -- we hit it with a preview
bridge while moving a public hub to 5.1.0a2.

The defaults describe 5.x, so the guard no longer fires there while 4.x
behaviour is unchanged: handshake_enabled defaults True (the handshake is
always available) and require_crypto defaults False.

The existing tests could not catch this -- they build the protocol with
MagicMock, which invents any attribute asked of it. The new test uses a
double that raises AttributeError for exactly those two names, and fails
against the previous code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: patch legacy handshake flags with create=True for core 5.x

The existing test_no_crypto_key_handshake_disabled_require_crypto test
patched HiveMindListenerProtocol.handshake_enabled/require_crypto, which
core 5.x removed, so patch.object raised AttributeError. create=True
patches them onto the class to simulate a 4.x hub, keeping the
guard-fires path covered. See HiveMind-core#315.

---------

Co-authored-by: Gaëtan Trellu <gaetan.trellu@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The core's handle_client_disconnected ends by calling client.disconnect(),
which re-entered this same do_disconnect and recursed until RecursionError
on every disconnect. do_disconnect now pops the registry entry first and
only calls the core's callback when the entry was still present, so the
second, core-triggered call is a no-op.

A protocol v3 session carries every post-handshake message as an
encrypted binary Noise transport frame, and the HTTP listener handed
everything to the core as text: the first message after the handshake
was rejected as a non-Noise message on a v3 session, the connection was
dropped, and nothing was ever deliverable over HTTP. HTTP has no binary
opcode, so the client base64-encodes each frame and marks it binary=1;
the listener decodes it with validation back to the bytes the core's
decode requires, answers a chunk of a multi-frame message with
"buffered", and keeps the legacy text path untouched when the flag is
absent. A frame that cannot be decoded is a 400, not a 500.

A client that lost its session has to be able to tell "reconnect" from
"delivered" by the status alone, so "not connected" is a 409, as is a
binary frame arriving on a connection that has no Noise session (a
restart on the memory backend, a dropped session): the core would
otherwise parse ciphertext as a bitstring and the client would see a
500. The core is told when a connection it dropped goes away, since
nothing closes over HTTP, and /disconnect releases the cached connection
so a later /connect runs the handshake offer again. Request bodies are
capped at one megabyte, which no frame needs. The decrypted message is
logged only by type at info, with the envelope at debug: a BUS message
carries the user's words. The client model in HiveMind-core 5.x has no
crypto_key, so the row is read without requiring one.

Tests run two real Noise transports through the real handler and a real
client connection, single-frame and multi-frame, plus the refusals, the
flag spellings, the logging, and both disconnect paths. The API and
operations documents describe the parameter, the new statuses, the body
cap, and why a v3 session is bound to one replica.

Addresses #49

Co-authored-by: JarbasAi <jarbasai@mailfence.com>
Co-authored-by: Gaëtan Trellu <gaetan.trellu@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
… handshake gate (#52)

The client row and the listener protocol no longer carry crypto_key,
handshake_enabled or require_crypto: the Noise handshake is mandatory
on every connection (HIVEMIND-CRYPTO-1 §3). Reading them through
getattr kept a gate that can never fire; the plugin now pins the core
floor that defines the model it reads.

Co-authored-by: JarbasAi <jarbasai@mailfence.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…nnot collide (#56)

The self-signed private key was written at mode 644, so any account on
the host could read it and impersonate the listener. It is now created
with mode 600 through os.open rather than chmod-ed afterwards, because a
key that is world-readable for an instant is world-readable.

The serial was random.randint(0, 2000). Every host builds the same
issuer name, so a certificate here is identified by its serial alone and
a range of two thousand collides; a store keyed on issuer and serial
cannot hold both. It now comes from x509.random_serial_number().

Generation moves from pyOpenSSL's deprecated X509 API to cryptography's
CertificateBuilder, which is what surfaced the other two. That removes
twelve deprecation warnings and the pyOpenSSL dependency, and both files
are written through context managers instead of being left to the
garbage collector.

Generation is skipped when both files already exist, so an existing
deployment keeps the certificate it has until the pair is deleted.

Co-authored-by: JarbasAi <jarbasai@mailfence.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Bare `hivescope` dependency resolves to the newest stable release
(0.1.0), which predates the alphas the stack actually runs on, so
the suite fails at collection/runtime for reasons unrelated to this
repository's code. Floor it at the alpha that matches current core.

Co-authored-by: JarbasAi <jarbasai@mailfence.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…he wheel (#59)

This repository carries BOTH triggers that put the repository root on
`sys.path`: `tests/__init__.py` and a `conftest.py` at the root. The root
holds a second copy of `hivemind_http_protocol`, and that copy wins the
import, so `build_tests` installs a wheel it never tests and cannot detect a
packaging defect. T-6298 measured that deleting the marker alone does NOT fix
this repository, because the root conftest re-adds the root by itself.

Both go. `pytest_plugins` cannot move into `tests/conftest.py`, because pytest
refuses that key in a non-top-level conftest, so a new `tests/conftest.py`
carries a `pytest_configure` hook that calls
`config.pluginmanager.import_plugin("hivescope.pytest_fixtures")` behind the
same `try/except ImportError`. The registration and the old conditional
contract are both kept, with no file at the root.

Deleting `tests/__init__.py` breaks one sibling import that spells the `tests`
package, so it loses the prefix: `from tests.test_handlers import ...` becomes
`from test_handlers import ...`. With `tests/` no longer a package, pytest
puts `tests/` itself on `sys.path`, so the unprefixed form resolves.

Measured on dev 84c68d7, with the wheel built by `uv build` and installed
non-editable with the `test` extra, which is what `build_tests` passes. The
instrument prepends `raise ImportError(...)` to the WORKING TREE copy of
`hivemind_http_protocol/__init__.py`, so a green run proves the wheel was
imported. Path `tests`:

    before, clean          107 passed
    before, tree poisoned  9 errors during collection
    after, clean           107 passed
    after, tree poisoned   107 passed

Co-authored-by: JarbasAi <jarbasai@mailfence.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
RetentionQueue.drain copied the deque and then cleared it. A frame put
from another thread between the two calls was lost. RetentionStore.sweep
and queue_for also iterated and mutated the shared dict without a lock,
which raised KeyError when a sweep removed a queue between the membership
check and the lookup.

Each queue now holds a lock around put, drain and len. The store holds
a re-entrant lock around sweep, queue_for, pop, clear and the
missing-key path. Public behaviour is unchanged.

Fail-before: the new delivery test fails 9 of 10 runs on dev (delivers
5000 to 15000 of 20000 frames); the churn test fails 10 of 10 (KeyError).
Both pass 10 of 10 after the fix.

Co-authored-by: JarbasAi <jarbasai@mailfence.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
enqueue_message looked up the client's outbox with queue_for() and then
appended to it after the store lock was released. A TTL sweep on another
thread could remove that outbox between the two steps, and the frame went
into a queue nobody reads. Under a Noise v3 session one lost frame
desynchronises the transport nonce and the client drops the session.

RetentionStore.put() now finds the outbox and appends under one hold of the
store lock, and ClientRegistry.enqueue() routes text and binary frames
through it.

Fail-before: the new test asserts that the frame is delivered or that the
sweep's drop log counts it ("dropped 1 "). On the unfixed code the frame is
neither delivered nor counted (1 failed). With the fix it passes, and the
full suite passes (110).

Co-authored-by: JarbasAi <jarbasai@mailfence.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
RetentionQueue.put() refreshed the idle timestamp, so a client that stopped
polling while frames kept arriving was never swept. Only the per-client
frame cap bounded it. The README and docs/architecture.md define
undelivered_ttl as the time a client may go without polling.

put() no longer touches the timestamp; drain() still does. The test that
asserted the old behaviour (a client that only receives survives the
sweep) is replaced by one asserting the documented behaviour.

The test for poll activity puts a frame between polls and asserts the
last drain returns it, so removing the reset in drain() fails it. The docs
say to keep undelivered_ttl at least ten times the poll interval.

Fail-before: the new test fails on the unfixed code (assert 'deaf' not in
store), passes with the fix, and the full suite passes (109).

Co-authored-by: JarbasAi <jarbasai@mailfence.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: fc2cd161-f93a-4890-8a22-09abae8c418c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot closed this Oct 21, 2026
@github-actions
github-actions Bot deleted the release-0.0.19a1 branch October 21, 2026 10:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants