Skip to content

0.1.0a11

Pre-release
Pre-release

Choose a tag to compare

@spiderings spiderings released this 01 Aug 08:04
· 65 commits to dev since this release
d053b13

Release 0.1.0a11.

An ENC:v3 fix release. Every fix here was found by verifying the browser
extension's new ENC:v3 support against a real install (Slack #my-hermes and
Discord #general) rather than by unit testing — on a real install, no v3
command could authenticate at all
before this.

Fixes

  • Resolve a v3 channel key stored under the extension's composite id, so
    existing installs work with no state migration. The extension pushes
    channel_key_set keyed by slack:{team}:{cid} / discord:{guild}:{cid}
    and save_channel_key stores that verbatim, while a gateway event only
    carries the platform's native chat_id — so the exact-match lookup missed
    every stored key and refused the command as key_not_bound_to_channel. The
    composite is accepted only when its last segment is exactly the channel
    id and its first segment is the event's platform, so the binding
    stays as tight as the exact match it replaces.
  • Log the fixed InvalidEncryptedEnvelope reason identifier on refusal (never
    message text, keys or plaintext) — every refusal previously logged the same
    sentence while needing completely different fixes, which made a live
    rejection undiagnosable without replaying the wire by hand.
  • Authenticate a top-level Slack command under its real context. The Slack
    adapter's default reply_in_thread session keying stamps thread_id with a
    synthetic root equal to a top-level message's own ts; Slack assigns that
    ts only after the send, so the extension provably encrypted with
    thread_root=None and every genuine top-level v3 command was refused as
    authentication_failed. The command AAD now canonicalizes to
    thread_root=None when the routed root IS the command's own message_id
    (mirror of the documented Discord auto-thread canonicalization), while reply
    routing keeps the synthetic thread. Canonicalizing requires two
    adapter-stamped markers to agree, so a captured top-level token still cannot
    be replayed into a genuine thread.

Changes

  • Bump the pinned version surfaces to 0.1.0a11.
  • Document the Slack synthetic-thread-root canonicalization in
    docs/dev/SLACK_E2E.md §2.

Test plan

  • uv run pytest — full suite green; 11 new tests in
    tests/extension/test_extension_e2e_v3.py (composite-id resolution,
    platform-mismatch and partial-suffix refusals, synthetic-root top-level
    acceptance, genuine-thread acceptance, top-level-token-in-thread
    refusal, thread-token-at-top-level refusal, non-Slack no-canonicalize,
    degenerate message_id fail-closed, replay through the dispatch hook,
    reply_in_thread=false parity)
  • ruff check / ruff format --check / mypy --strict src tools
  • Adversarial review (independent agent): verdict SOUND, no
    CRITICAL/HIGH/MEDIUM findings; its two hardening suggestions and five
    coverage gaps were all landed
  • Live gateway round trip on the production install (2026-08-01): a
    top-level ENC:v3 command from the real Slack client decrypted by the
    gateway, agent reply re-encrypted in-thread with the inbound kid; same
    round trip verified on Discord including the auto-thread path
  • TestPyPI dry run + fresh-venv discovery check (release workflow)

Pairs with mordredagent/mordred-extension#11 and #12 — the extension
side of the same wire format.