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.
165 lines
9.3 KiB
Markdown
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.
|