The follow-up review found that the entry pointed at 6f464f72 while the engine
pin inside it still said a9641795, so an install fetched the old engine and lost
both of the fixes that round was meant to deliver. The pin is fixed in the
plugin itself now; this repins the catalog entry to that corrected commit so the
reviewed revision and the installed revision are the same thing again.
Also updated:
- `docs_url` and `image` now resolve at the new commit (both verified reachable).
- The description no longer implies "runs fully local" without qualification: it
notes that an embedding key already present in the environment is
auto-detected, and points at the README's egress section as the authoritative
list. That was the third finding.
Validation: `scripts/validate_plugin_catalog.py plugin-catalog/nexus.yaml` →
`OK: 1 file(s) valid`.
A second review pass over e420433 found five issues, one of them a real leak:
the endpoint matcher used a substring host test, so a lookalike domain could
receive a real provider key. Fixed in 616258b, along with the docstring that
promised a redirect the code never performed, a silent key rejection, an unused
import and a dependency-bound drift across three manifests.
Suite on the new pin: 2171 passed, 3 skipped.
Final revision: the mcp floor is corrected and the paid-station tests opt in
explicitly, so the audit passes on a clean machine. Audit run on this commit:
completed/success.
Carries the corrected mcp floor (>=2.0.0,<3.0.0). The previous pin resolved
mcp 1.x on CI, where `add_request_handler` does not exist, and the audit run
failed at collection. Reproduced and fixed; the CI job now passes in a clean
replica.
Points at 3be00e5, which carries the review fixes: opt-in cloud embedding,
opt-in query rewriting and paid stations, no cwd/.env load, key only paired
with its own endpoint, full egress documentation. The description no longer
claims a blanket 'runs fully local' without naming the opt-in paths.
Nexus Memory is a self-hosted MemoryProvider: Qdrant for storage, a
knowledge graph, and auto-detected embeddings (Voyage, OpenAI, Google,
Jina, Ollama, sentence-transformers). It prefetches before each turn and
writes back after it, and shares one collection with the Nexus MCP server
so Hermes and any MCP-connected agent read and write the same memories.
Fully local operation is supported — no cloud key is required with Ollama
or sentence-transformers.
The pinned tree is a dependency shim: its engine resolves from the same
repository at the pinned commit through tool.uv.sources, so the provider
arrives with its runtime rather than asking an index for a bare name.
Disclosure for review (admission rule 13):
- Network: Qdrant over HTTP (localhost by default), and HTTPS to the
configured embedding provider (Voyage/OpenAI/Google/Jina) or to a local
Ollama instance. No other hosts are contacted.
- Reads outside its own data: it checks ~/.hermes/scripts/*.sh and
~/Library/LaunchAgents/*.plist for a backup entry naming its backups
directory, only to decide whether to mention setting up off-site backup.
- Credentials: only its own config (the embedding provider key). It reads
no other tool's token file.
- Background: daemon threads for write-back, prefetch, backup and entity
extraction. No shell commands, no self-update (opt-in and off here).
Nothing tested mac_bundle_recover(), the start-of-run repair that finishes or
rolls back a Hermes.app swap a killed hand-off left half done.
posix.sh --self-test-swap-interrupt (body in swap-selftest.sh) runs the REAL
mac_swap on a fixture bundle tree in its own process group. mac_swap now
names its step boundaries (swap_checkpoint start/staged/aside/installed); the
self-test makes the swapper SIGSTOP itself at one of them and SIGKILLs the
whole group there, or resumes it and SIGKILLs it mid-ditto / mid-cleanup rm.
It checks the wreck has the expected shape, runs the REAL mac_bundle_recover,
and asserts the target is the complete old or complete new bundle (whole-tree
checksum, version marker included), no .old/.new/.hermes-update-* is left,
and a second recovery changes and logs nothing. Seven cells: before-stage,
mid-stage, staged, aside, installed, mid-cleanup, complete (control).
The seams are inert in a real run: SWAP_PAUSE_AT is set only inside the
self-test, and MAC_DITTO stays /usr/bin/ditto unless the self-test runs where
ditto is absent (Linux: cp -R).
test_desktop_update_mac_swap_interrupt.py drives the arm under
platforms("posix"): the Linux suite and the macOS lanes of tests-os.yml
(real /usr/bin/ditto, macOS /bin/bash 3.2).
Get-DefaultBrowserExe read only the legacy UserChoice ProgId, which Windows
11 25H2 Settings no longer updates, so the update hand-off fell back to the
WinForms card (or a stale browser) on machines whose real default is
Chrome/Edge. Read UserChoiceLatest\ProgId first, legacy UserChoice second.
Fixes#108051. Follows #108062 / #108064.
Co-authored-by: KoNit-K <konit.block@protonmail.com>
Co-authored-by: Konstantin Khlopkov <konstantin.khlopkov93@gmail.com>
Windows 11 25H2 Settings records the default browser under
UrlAssociations\https\UserChoiceLatest\ProgId and stops updating the legacy
UserChoice key, which UCPD.sys also write-protects. Reading only the legacy
ProgId made real-profile browsing refuse a correctly configured Edge/Chrome
default ("not a supported Chromium browser"), with WinError 2 when the key
exists without a value, or picked a stale browser when it still held one.
Resolve the effective ProgId via AssocQueryStringW(ASSOCSTR_PROGID) first,
then fall back to UserChoiceLatest\ProgId, then the legacy UserChoice value.
Classification (channels fail closed) is unchanged.
Salvages #106971 (Shell API lookup); the UserChoiceLatest fallback follows
#108064 and the reports on #107854.
Co-authored-by: Konstantin Khlopkov <konstantin.khlopkov93@gmail.com>
The self-improvement review fork refused every edit/patch/write_file/remove_file on a skill
without `created_by: "agent"`, so lessons about hand-written, URL-installed, foreground-created,
bundled, hub, external or pinned skills were dropped with only a WARNING line (77% of refused
review writes on the #134289 reporter's install). The prompts then steered the fork to a
different umbrella or a new near-duplicate skill.
Ownership now gates only delete/archive: the review may improve any skill it learns from (still
read-before-write, ledgered, and staged when skills.write_approval is on), while autonomous
archival stays limited to curator-managed, unpinned, non-bundled/hub/external skills. The review
prompts drop the protected-skill lists and say never to delete.
Fixes#134289
MemoryManager forwarded turns, transcripts and tool output to external
memory providers without the egress scrub, so a key echoed into tool
output was archived verbatim in the provider's store.
Every fan-out now goes through redact_for_egress (the chat/cron delivery
scrub; holds with security.redact_secrets off, fails closed): sync_all
(turn strings + messages), prefetch_all / queue_prefetch_all,
on_turn_start, on_session_end, on_pre_compress (both transcripts),
on_memory_write (content + metadata), on_delegation and provider
handle_tool_call args. Nested dicts/lists (tool-call arguments,
multimodal parts) are walked; data: URLs pass; the live transcript is
never mutated. A value masked anywhere in one hand-off is masked
everywhere in it, so a key the model repeats bare in prose is caught.
redact.py: the JSON-field rule now also matches inside JSON-encoded
strings (tool results wrap config dumps in escaped JSON), which is how
the reported opaque AQ.-prefixed key slipped past.
Co-authored-by: Teknium <teknium1@gmail.com>
The overlay binds its Escape handler in a passive effect keyed on the failed state, which can
flush after "Installation failed" is already painted. The test pressed Escape once right after
findByText, so on a loaded runner the key landed before the listener existed and the overlay
(correctly) stayed up for the whole waitFor. Proof: delaying the listener by 80 ms reproduces the
CI red on the old test and passes the new one.
auxiliary.voice_chat.reasoning_effort ships as "none", and it applies even when the slot is on
auto (the session's model answers the spoken turn with reasoning off; typed turns keep their
effort). Routes that cannot switch reasoning off get their weakest accepted level instead:
Responses ladders without "none" (gpt-6-astra, gpt-6.1-sol -> low), mandatory-thinking Claude,
and routes the catalog or an earlier 400 marked mandatory. A disable the voice model rejects at
runtime is remembered per route for the next voice turn, and the rejection flags never leak
between the voice route and the main one.
"Inherit" in the dashboard/Desktop writes an explicit "" so the slot default does not come back;
both resets restore the shipped default.
Voice replies had to come from the session's main model, so a heavy model made every spoken
answer slow (#83894). A new auxiliary slot, voice_chat, picks the model for voice turns:
- CLI/TUI voice mode, the Desktop voice conversation (prompt.submit voice_turn) and messaging
voice notes mark the turn; agent/voice_turn_route.py binds the route after the prompt, row and
preflight compaction were settled against the main model and undoes it before memory sync and
background review, so the next typed turn and the review run on the main model.
- A turn larger than the voice model's compression threshold stays on the main model with a
one-time notice: switching to voice never compacts the conversation.
- Usage lands in session_model_usage under task voice_chat; it never rewrites the session's
recorded model/route, so resume keeps the model the user picked.
- Slot appears in Desktop Settings -> Models, the dashboard, and `hermes model`; GPT-Live
delegations keep using the session model.
try_activate_fallback's in-place swap (client, pool, compressor, reasoning, extra_body) and
restore_primary_runtime's reinstall are now agent/route_binding.py::bind_route_entry and
::reinstall_primary_runtime / ::reinstall_runtime_snapshot, so a per-turn route can reuse the
same binder instead of a third copy. No behaviour change.