Releases: mordredagent/hermes-mordred
Release list
hermes-mordred 0.1.0a17
Alpha release of the privacy-preserving plugin suite for hermes-agent.
Changes
- Gate interactive network setup prompts and summaries by the selected privacy route (#118).
- Ignore editor state, GitHub Actions credential files, and local scratch notes (#120).
- Update repository URLs after the distribution and repository rename (#121, #130).
- Add the re-runnable, fail-closed
hermes-mordred setupfirst-run orchestrator (#122). - Correct extension setup guidance and document optional extension bundles (#123).
- Add secure Discord channel and thread-context resolution to the extension API (#124).
- Add installer
--with-extension, exact--version, help, and argument validation (#127). - Consolidate canonical documentation and add documentation-contract guards (#128).
- Bump the canonical package and compatibility shim to
0.1.0a17(#129).
Fixes
- Correct pairing dependencies, Hermes lifecycle, Firefox availability, protocol messages, and extension port guidance (#123).
- Prevent Discord resolution from bypassing Tor or an unverifiable VPN route, preserve dotenv precedence, load vault-managed tokens, and enforce the client deadline (#124, #126).
- Document file-vault recovery as macOS-only until Linux has a device-anchor store (#128).
- Preserve exact installer version pins when adding the messaging extra (#128).
- Preserve current setup, installer, package-rename, and extension behavior while removing superseded documentation (#128).
Distribution
- Canonical package:
hermes-mordred==0.1.0a17 - Compatibility shim:
mordred-hermes==0.1.0a17 - Both packages were verified on TestPyPI and PyPI before this prerelease was tagged.
hermes-mordred 0.1.0a16
This release moves the real PyPI distribution to hermes-mordred.
Existing mordred-hermes installs resolve through a metadata-only compatibility shim.
The Python import package remains mordred_hermes; the CLI remains
hermes-mordred; plugin IDs, native helper names, state paths, keys, and
other persistent compatibility identifiers do not change.
Changes
- Rename the public PyPI distribution to
hermes-mordredin0.1.0a16and retainmordred-hermesas a metadata-only compatibility shim.
Fixes
- Prevent old/new distribution file-ownership overlap during upgrades and fail safely when the canonical package cannot be installed.
Verification
- Canonical and compatibility artifacts passed TestPyPI and production PyPI fresh-environment checks.
- Six Hermes plugin entry points load from the canonical wheel.
- Removing only the compatibility shim leaves the canonical runtime and CLI intact.
v0.1.0a15
mordred-hermes 0.1.0a15
Packaging-hygiene release on top of 0.1.0a14. No runtime code changed:
src/ is identical to a14 apart from the version string.
The published a14 sdist still shipped six dev-era PoC harnesses; a15
excludes them.
a14 sdist: install.sh, keyvault_offline_digest.py,
poc_outbound_roundtrip.py, poc_pre_gateway_dispatch.py,
poc_reply_encryption_regression.py,
poc_reply_key_failclosed.py,
poc_thread_reply_encryption.py, poc_wrap_live_adapters.py
a15 sdist: install.sh, keyvault_offline_digest.py
Changes
- Exclude scripts/poc_*.py from the sdist so published artifacts carry
only the two scripts users need (#109) - Tighten CI config and scripts hygiene; translate remaining Japanese
comments in release.yml and docs/dev/CI.md to English (#109) - Bump the GitHub Actions group: checkout v7.0.1, setup-python v7.0.0,
upload-artifact v7.0.1, download-artifact v8.0.1, labeler v7.0.0,
cache v6.1.0, github-script v9.0.0 (#108) - Bump every pinned version surface to 0.1.0a15 (#110)
v0.1.0a14
mordred-hermes 0.1.0a14
Headline: scripts/install.sh — users install with one command instead of
assembling uv pip install --python ... by hand.
Supersedes 0.1.0a12 and 0.1.0a13, which reached main but were never
published to PyPI or tagged; these notes therefore cover everything since
v0.1.0a11.
Changes
- Add scripts/install.sh: resolves the Hermes-managed environment, gates on
the minimum Hermes version, selects the macOS/Linux extra, installs from
PyPI, and exposes hermes-mordred (#103) - Resolve the interpreter from the
hermeson PATH first, reading the
official launcher's exec line, so a second Hermes install cannot silently
receive the plugins (#103) - Write a launcher mirroring Hermes's PYTHONPATH/PYTHONHOME scrub instead of
a bare symlink (#103) - Use --upgrade-package so upgrading Mordred does not upgrade hermes-agent;
refuse non-virtualenv targets; run uv pip check before and after (#103) - Add tests/test_user_installer.py: 35 hermetic tests plus a real-uv
integration test (#103) - Rewrite the user docs around the installer; drop the $M idiom (#103)
- Simplify user/developer docs; add Extension and dev doc indexes; add local
Markdown link validation (#100) - Add Dependabot for the SHA-pinned actions; restructure agent guidance into
AGENTS.md (#101) - Parse state.json once per generation instead of per WebSocket frame (#94)
- Split keyvault/_secret_ops.py and llm_guard/enforce.py into cohesive
modules (#95, #96) - Harden extension protocol compatibility (#97)
Fixes
- install_launcher: set -e is suppressed inside command substitution, so a
failed mktemp ran on and reported failure after a clean install (#103) - version_at_least read a zero-padded minor as octal (#103)
- Reject interpreter paths with shell metacharacters (#103)
- ${HOME} under set -u aborted instead of printing guidance (#103)
- Correct the extension serve failure mode and the ~/.local/bin claim in the
docs (#103) - Rate-limit pre-auth needs-key notices (#93)
- state.json read racing a write no longer caches a pre-write snapshot (#94)
- guard_hermes_home: block ceremonies with interleaved flags (#101)
- Refuse empty-text inbound on mandatory-E2E platforms (#88)
- Bump gh-action-pypi-publish to v1.14.2 so Twine v7 accepts hatchling's
Metadata-Version 2.5 (#106)
0.1.0a11
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_setkeyed byslack:{team}:{cid}/discord:{guild}:{cid}
andsave_channel_keystores that verbatim, while a gateway event only
carries the platform's nativechat_id— so the exact-match lookup missed
every stored key and refused the command askey_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
InvalidEncryptedEnvelopereason 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 defaultreply_in_threadsession keying stampsthread_idwith a
synthetic root equal to a top-level message's ownts; Slack assigns that
ts only after the send, so the extension provably encrypted with
thread_root=Noneand every genuine top-level v3 command was refused as
authentication_failed. The command AAD now canonicalizes to
thread_root=Nonewhen the routed root IS the command's ownmessage_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,
degeneratemessage_idfail-closed, replay through the dispatch hook,
reply_in_thread=falseparity) -
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-levelENC:v3command 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.
0.1.0a10
Release 0.1.0a10 — a single follow-up fix to the audit-log encryption indicator shipped in 0.1.0a9 (#80, #81).
No breaking changes. The 0.1.0a9 notes still apply for the ENC:v3 gateway wire and the removal of keyvault enable-se --unattended.
Fixes
statusreportedaudit log plaintext (no audit wrapping key)for a legacy keyvault profile whose audit log is in fact MRAL-encrypted (#80). The 0.1.0a9 indicator decided solely on theAUDIT_KEY_FIELD/PENDING_AUDIT_KEY_FIELDmanifest records, but those are written only bywizard/_keyvault_init, so a keyvault created before 0.1.0a9's profile-scoped native key ids never carries either.privacy_check/audit.pytreats exactly that shape as a supported branch ("Pre-profile-scoping metadata selects the legacy global audit key"), resolves the legacy global key, and encrypts normally — so the new indicator contradicted the on-disk state on every pre-0.1.0a9 install. The verdict now has a legacy branch that reads the audit log's own MRAL header, because the manifest cannot prove either outcome there and claimingencryptedunconditionally would be a false positive (the legacy key has a real history of becoming unusable with a silent plaintext fallback). Committed, pending, and scoped verdicts are unchanged. The risk was operator action rather than data exposure: the line erred pessimistic, but someone trusting it could have run destructive keyvault ceremonies against a healthy encrypted setup.
Changes
- Version bumped to 0.1.0a10 across all pinned surfaces (#81).
0.1.0a9
Release 0.1.0a9 — security/privacy hardening sweep across all six plugins, aggregating #71, #74, #75, #76, #77.
Breaking changes
- The gateway
ENC:v3command/reply wire (#71): a v3-capable external Mordred Extension must be deployed together with this version. Legacy v1/v2 helpers remain for the WebSocket API/history paths only.keyvault enable-seno longer accepts--unattended(#76): the authorization policy is chosen at key generation, not helper install. UseMORDRED_SEKEY_UNATTENDED=1on init/recovery instead; scripts passing the old flag now exit 2.
Changes
- Audit/privacy (#71, #76): process-wide, encryption-aware, conflict-safe audit writer; shared
_audit_ioprimitives (O_NOFOLLOW/O_NONBLOCKopens, inode-stability checks, sidecarflock, no-follow gzip rotation); cross-process serialized whole-entry appends with truncate rollback;audit purge/decryptbind anO_NOFOLLOWdirectory descriptor andpurgerequires--yes. - Policy enforcement (#71, #76): strict provider/network policy enforced against the effective runtime request (repeated gateway calls, exact loopback local-LLM endpoints, verified Tor/provider transport); strict mode binds cloud routes to provider-owned endpoints including ambient SDK overrides (
ANTHROPIC_BASE_URL,OPENAI_BASE_URL, …); newauxiliary_guardcovers Hermes 0.19's auxiliary client resolvers that never emitpre_api_request; sibling-disable integrity checks registered from every runtime Mordred plugin. - Extension/gateway (#71, #74, #75, #76): context-bound gateway E2E v3 (direction/platform/channel/thread AAD, channel-key binding, persistent replay protection, single-token grammar, Slack
:lock:normalization, multiplex profile routing, Discord auto-thread canonicalization); hardened Host/Origin/token/WebAuthn lifecycle checks with stale pairing-generation revocation; WebAuthn split from pairing state with lazy extension submodules (pairing/help paths work without aiohttp); pairing commit ordered against CLI cancellation under one state lock; JSON-RPC responses bounded (2 MiB) and envelope-validated;wallet.jsonvalidated, locked, atomically written. - Keyvault (#76): profile-scoped native wrapping-key ids (two
HERMES_HOMEprofiles can no longer collide in the machine-global native store);keyvault_lockgains an out-of-tree lifecycle lock, fork-safe descriptors, and reentrancy;export_backupverifies the passphrase before opening any envelope;import_backuprequires a fresh destination and rolls back only what it created;.env/config.yamlreseal captures the live inode atomically; native helper installers stage/verify before atomic rename. - Network (#74, #75, #76): centralized provider resolution and network-setting validation across registration, hooks, and wizard; every network-path subprocess runs under an explicit timeout (
SubprocessRunneris aProtocol); Tor picks an adjacent free (SOCKS, ControlPort) pair; process-global Tor/VPN routing kept alive across Hermes turns and concurrent gateway sessions. - Runtime integrity (#74): new
MordredIntegrityRefused(BaseException)policy signal so Hermes'except Exceptionhook wrapper cannot swallow strict refusals (stderr-printed before raising, #75). - Release/CI (#71): pinned GitHub Actions, restored CI/release gates, artifact validation, declared direct runtime dependencies (incl. PySocks).
- Version (#77): bump to 0.1.0a9 across all pinned surfaces.
Fixes
- Strict mode refused every allow-listed cloud provider on its SDK-default endpoint (#76): absent
base_urlnow means "the vendor default" and is accepted when the provider owns one; tenant/region-scoped providers (azure-foundry,bedrock,vertex) still fail closed. - An interrupted
configuresilently forced strict mode forever (#76): a stranded policy-write transaction marker now surfaces instatus/policy showwith the remedy. - A stranded audit wrapping key left the audit log permanently plaintext (#76): provisioning now discards the untrusted key so the next attempt succeeds, and
statusreports audit-log encryption state. - Tor liveness (#74, #75): two-stage circuit-status probe — a
BUILTcircuit is healthy on its own; empty/in-progress replies resolve viaGETINFO network-liveness— so idle Tor sessions are no longer sticky-dropped and a running-but-circuit-less Tor is still detected; bootstrap reader now works on Windows (thread+queue instead of selectors-on-pipe). - WebAuthn (#74, #75): registration keys validated as DER EC P-256 before persistence with byte-for-byte preservation of the prior credential on rejection;
webauthnregistered in the lazy-import registry with pre-split patch seams mirrored. - Wallet (#71, #76): transactions canonicalized and validated before approval, then only the exact approved representation is signed;
signapproval requires a literal booleantrue(the JSON string"false"no longer crosses the approval boundary);fee_datafalls back on HTTP-level method rejection too. - Keyvault portability (#75):
fcntlguarded so the extension stack imports off POSIX (with POSIX re-raise);O_NOFOLLOWand parent-dir fsync made portable. - Misc (#76): BIP39 mnemonics canonicalized before validation;
mullvad status/lockdown-modeparsing requires exact tokens and never conflates query failure with OFF;policy=offrestores the pre-runtime proxy environment;workspace purgeno longer refuses on a stray.DS_Store; E2E replay capacity exhaustion reports recovery;provider_overridespreserved through configure/upgrade/OpenClaw migration (#71); broken pairing installs present as exit-2ExtensionGatewayUnavailableinstead of a raw traceback (#75).
v0.1.0a8
Changes
- extension page: ship the regenerated localhost app with restart recovery, cache-safe token delivery, and immediate correlated wallet RPC errors instead of timeouts (#69).
- extension provenance: regenerate the bundled page from Mordred-Extension main so the latest Discord/Teams i18n and protocol changes are included (#70).
- E2E operations: add focused reproduction scripts and restore the Slack E2E operator guide for outbound encryption regressions (#68).
- chore: bump all package, plugin-manifest, README, and setup surfaces to
0.1.0a8(#72).
Fixes
- Prevent Slack replies from falling through to cleartext by normalizing platform enums and aligning channel/thread fallback behavior (#68).
- Patch the actual live Slack/Discord adapter classes from
GatewayRunner.adapters, rather than import-path guesses that could wrap unused classes (#68). - Preserve Discord thread metadata during outbound encryption lookup so encrypted conversations cannot miss their key and delegate to plaintext send (#68).
- Fail closed when no valid reply key is known instead of falling back to an unreadable master key (#68).
- Mark token-bearing localhost HTML
no-store/nosniff/no-referrerand return correlated generic errors for wallet lookup failures (#69).
Install
pip install "mordred-hermes==0.1.0a8"v0.1.0a7
Changes
- wizard: fix
mordred_e2enot being enabled byconfigure— the 0.1.0a6 E2E plugin was silently inert for everyconfigureuser; addmordred_e2etoMORDRED_PLUGIN_NAMES+ regression tests (#63) - docs: update stale "5 plugins" wording repo-wide (CLAUDE.md, README, setup.md, SPEC.md, PLAN.md, release runbook) to reflect the 6 entry-point plugins shipped since 0.1.0a6 (#63, #66)
- extension: cap-relief refactors — extract wallet/sign helpers from
api.pyintoextension/wallet.py(810→675 lines), splitserve()'s event-loop lifecycle into_run_forever(48/50→14/50 statements), splitverify_webauthn_assertioninto fail-closed phase helpers (complexity 10→6, was at the hard cap) (#64) - extension: first automated test coverage for outbound E2E send wrappers (
tests/extension/test_extension_outbound.py, 7 tests) — Slack/Discord ciphertext round-trips, fail-closed no-key paths, plaintext passthrough, Discord parent-channel key lookup (#64) - deps: resync the provider-alias replica (
_provider_identity.py) anduv.locktohermes-agent0.19.0 (#65) - chore: version bump to 0.1.0a7 (#66)
Fixes
configurenever wrotemordred_e2eintoplugins.enabled(regression-by-omission from #60) (#63)- test isolation bug: outbound tests leaked
gateway*modules intosys.modules, silently defeating an unrelated test's missing-gateway simulation (#64) - CI red on
devpost-merge:test_replica_matches_hermes_sourcedrift againsthermes-agent0.19.0's 2 new provider aliases (#65)
Install
pip install "mordred-hermes==0.1.0a7"v0.1.0a6
PyPI: https://pypi.org/project/mordred-hermes/0.1.0a6/
Changes
- E2E transport now runs as a stock hermes-agent plugin — inbound decrypt via the
pre_gateway_dispatchhook, outbound reply-in-kind encryption via adaptersendwrappers; no hermes-agent core fork (#60) - Mandatory encryption on Slack/Discord: a message that cannot be decrypted (plaintext, or ciphertext with an unheld key) is never handed to the agent — the sender is asked to set up or obtain an encryption key and the message is skipped; non-enforced platforms pass through unchanged (#60)
- Key-exchange (🔑) control messages are dropped before dispatch so the agent never reacts to them (#60)
- Outbound fail-closed: a missing key in a known-encrypted thread posts a locked notice, never plaintext (#60)
- New
mordred_e2eentry-point plugin (mordred_hermes.extension.gateway_plugin) — entry-point discovery is now 6 plugins (#60) - Bump package version to 0.1.0a6 (#61)
Fixes
- (none)