svos-dev restarted :8770 at 02:17 and both roster lines printed clean. Confirmed from this side rather than taken on their word: :8770 answers 200 on the new pid, an unauthenticated Bifrost dispatch gets 401, and Hermes reports 29 toolsets with svos_miranda the sole enabled=True row. Two patterns from their restart that generalise past this service. A dry-run boot against the still-held port: start the new process while the old one still owns the socket, and it proves every check above the bind before dying on EADDRINUSE. Zero downtime, no commitment, and it turns a one-way restart into a rehearsed one. Worth doing for any service whose startup validates before binding. And a trap: SIGTERM released the port but left the process alive for 35 seconds, needing SIGKILL. The port was free that entire time, so a script waiting on port availability would have started the replacement alongside a still-running old process. Kill by PID and wait on the PID, never on the port - a freed port is not evidence of a dead process, the same way an unreachable post office is an outage rather than an empty inbox.
9.3 KiB
svos_miranda Hermes plugin — validation pass (2026-09-15)
svos-dev asked infra-ops to run hermes plugins validate → doctor → compat
on /home/lkraven/development/svos/hermes_plugin/ and report before enabling.
Hermes Agent v0.21.1 (2026.9.7), local b88e6776, on nh3-dev.
The blocker (found, fixed by svos-dev at c964e64)
Three absolute intra-package imports — from hermes_plugin._vendored, .forward,
.jwt — pinned the package to its source directory name. The documented
install renames it to svos_miranda, and Hermes loads directory plugins under the
hermes_plugins.<dir> namespace; in neither case does a top-level hermes_plugin
exist. Fix: three relative imports.
⚠ The harness hid this from three different readers. My first validate passed
the import only because my cwd was the SVOS repo root. svos-dev's test suite
imports hermes_plugin.* from that same root, and their editable install resolves
the name from anywhere on the box — it only reproduced for them once sys.path was
stripped. Same class as feedback_filters_that_silently_narrow_the_window: the
instrument carried the result.
⚠ Two of the three commands CANNOT pass this plugin, ever
Neither is fixable from the plugin side. Both are now documented in its README.
validate— two independent causes. ItsRecordingContext.get_config(hermes_cli/plugin_validate.py:219-222) returns the default for every key, ignoringconfig.yamlentirely, sodispatch_keyis always"". And itsregister_toolreturnsNone, which the plugin's INV-P6 guard correctly reads as a name collision — so even with a key supplied it raises on the first tool.doctor— runsregister()under a tempHERMES_HOMEwith sockets blocked, so no config exists there either.
Tool-level gotchas worth remembering
- ⚠
hermes plugins doctorexits 0 even when it prints ERROR. Needs--ci. - ⚠
hermes plugins compat <nonexistent-path>prints ✓ and exits 0. A typo'd path reads as a pass. (The instrument itself is sound — verified with a throwaway plugin importing a real deprecated path, which it flagged with file:line, exit 1.) - ⚠
doctor's sandbox registry starts EMPTY — 0 entries, no built-ins. So doctor cannot detect tool-name collisions at all.validate's separate static "built-in tool collisions" check is what covers that. - The real
PluginContext.register_tool(hermes_cli/plugins.py:449-491) returns a truthyPluginRegistrationon success — confirmed against the live runtime.
Roster verified another way
Since neither command can supply config, a probe mirroring validate's context but
returning real settings and a truthy handle gave: 8 tools with
repo_read_enabled: true, 7 with false or omitted, names matching
plugin.yaml exactly, zero hooks/middleware/commands. All nine settings-validation
controls (quoted booleans, "90 s", zero/negative timeouts, empty/whitespace
strings) raise errors naming their own key.
A false finding I caught on myself
A probe registering read_file got back a PluginRegistration instead of the
expected refusal — which looked like the plugin's collision reading was wrong. It
was not: doctor's sandbox holds no built-ins, so nothing was claimed and my
positive case was not positive. Reported as untested rather than as a finding.
✅✅ FULLY LIVE 2026-09-15 02:17 — SVOS restarted, roster verified both ends
svos-dev restarted :8770 (pid 3931403; the pre-cutover process running since 09-09 is
gone) and both startup lines printed clean:
hermes roster required: platform_toolsets[api_server] = ['svos_miranda'] ;
agent.disabled_toolsets NOT required
hermes roster verified: ('svos_miranda',) -> [the eight]
Independently confirmed from this side, not taken on their word: :8770 → 200,
pid matches, an unauthenticated Bifrost dispatch → 401 (wall armed), and Hermes
reports 29 toolsets with svos_miranda the sole enabled=True.
⭐⭐ Two ops patterns from their restart — both generalise well past SVOS
1. Dry-run boot against the still-held port. They ran python -m server while the
OLD process still held :8770. It printed both roster lines and restored the thread,
then died on [Errno 98] address already in use. Every check above the bind proven,
zero downtime, before touching anything. It converts a one-way restart into a
rehearsed one and costs nothing. Adopt for any service whose startup does meaningful
validation before it binds.
2. ⚠⚠ SIGTERM released the port but did NOT end the process. It sat in shutdown for 35 seconds and needed SIGKILL — and the port was free that whole time. A script that waits on the port would have started the replacement alongside a still-live old process. ⭐ Kill by PID and wait on the PID, never on the port. Same family as an unreachable post office is an OUTAGE, not an empty inbox: a freed port is not evidence of a dead process.
✅ LIVE 2026-09-15 02:10 — gateway restarted, plugin registered
GET /v1/toolsets = 29 rows including svos_miranda. An api_server session
resolves to exactly 8 tools, write-klass absent. Operator's default session
verified intact at 46 tools after the restart (memory / read_file / write_file /
terminal / web_search / browser_exec all present) — the whole point of the ruling.
✅ RESOLVED — svos-dev fixed it at c9d2a96; the key is DELETED from config.
Their reading is better than mine and is the one to keep: _get_platform_tools
resolves platform_toolsets[<key>] FIRST and applies the global suppression
LAST, so subtracting 28 names from a one-element platform set is a no-op by
resolution order — not merely "adds no safety". That generalises to any future
platform; my measurement only established the single case.
⭐ And the endpoint already carried the answer: gateway/platforms/api_server.py:: _handle_toolsets computes each row's enabled as name in _get_platform_tools(config, "api_server"). Verified live — 29 rows, and svos_miranda is the ONLY row with
enabled=True. A check reading enabled rather than counting rows was always
correct. SVOS's build_miranda_roster now returns an empty disabled list
unconditionally and its startup line no longer names the key.
⚠ The 28-name list was removed, not commented — a paste-ready array behind a #
is what a future session uncomments. A short warning comment stands in its place.
⚠ agent.disabled_toolsets stays OFF permanently — operator: "i dont want the
tools disabled everywhere." So SVOS must stop verifying against the global
/v1/toolsets before it restarts: it will see 29 and refuse. Options put to
svos-dev: (1) verify the api_server surface instead — recommended; (3) relax to
"svos_miranda present AND write-klass five absent", which also survives any unrelated
plugin landing on this host. Option 2 (accept the fleet-wide cost) is ruled out.
ENABLED in config 2026-09-15 (operator-directed)
Installed to ~/.hermes/plugins/svos_miranda; plugins.enabled, the settings block
(dispatch key pulled from the vault, verified byte-equal), and
platform_toolsets.api_server: [svos_miranda] all set. Config backed up to
config.yaml.bak-20260915-svos-miranda-enable; diffed against it, only the intended
non-comment lines changed. hermes plugins list → svos_miranda enabled 0.1.0 user.
Gateway restarted 02:10:17 PDT — PID 3107822 → 3901622, confirmed by observing the change rather than assuming it.
⚠⚠ agent.disabled_toolsets is GLOBAL — it would have cost 26 tools fleet-wide
svos-dev's install instructions specify agent.disabled_toolsets = <the 28 rows minus svos_miranda>. That key is not scoped to api_server. It is a strict
end-of-pipeline subtraction applied to every session on every platform
(model_tools.py:216 "subtracted after enabling", applied :332; cli.py:2743 reads
the same key for the CLI).
Measured, not derived:
default session WITHOUT the line : 46 tools
default session WITH the line : 20 tools
lost 26: memory, read_file, write_file, patch, search_files, terminal,
process_manage, web_search, web_extract, browser_exec, execute_code,
computer_use, delegate_task, vision_analyze, video_analyze,
session_search, skills_*, todo_list, text_to_speech, image_generate, ha_*
⭐ And it is not needed for the security property. Measured:
enabled_toolsets=['svos_miranda'], disabled_toolsets=None resolves to exactly the
8 svos_miranda tools. platform_toolsets.api_server: [svos_miranda] already scopes
Miranda correctly on its own; the global subtraction adds no safety on top.
The only thing it buys is satisfying SVOS's startup roster check, which reads the
GLOBAL GET /v1/toolsets to verify a PER-PLATFORM property. Raised with svos-dev
with three options (verify against the api_server surface; accept the cost with
explicit operator sign-off; or relax the check to "svos_miranda present, write-klass
five absent"). Left commented out in the config with the measurement inline, so an
incidental Hermes restart cannot gut the operator's assistant.
⚠ Minor: stt appears in /v1/toolsets's 28 rows but resolving it logs
Unknown toolset: stt — an exact-set comparison pinned to that endpoint can fail for
reasons unrelated to the plugin.