Files
esh-pfi-infrastructure/persistent-memory.d/2026-09-15-svos-miranda-plugin-validation.md
T
vh 01f0014489 docs(memory): SVOS/Miranda fully live; bank two restart patterns from svos-dev
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.
2026-09-15 02:20:10 -07:00

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. Its RecordingContext.get_config (hermes_cli/plugin_validate.py:219-222) returns the default for every key, ignoring config.yaml entirely, so dispatch_key is always "". And its register_tool returns None, 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 — runs register() under a temp HERMES_HOME with sockets blocked, so no config exists there either.

Tool-level gotchas worth remembering

  • ⚠ hermes plugins doctor exits 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 truthy PluginRegistration on 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.