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

165 lines
9.3 KiB
Markdown

# 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.