Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions Clawbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ ClawBox adds no web tools on this harness. Reach the web with whatever your Herm
- `skill_install(id)` / `skill_uninstall(name)` — add or remove one. You can do this yourself; it is the Hermes answer to the app store.
- The owner sees the same catalogue in the **Hermes Skills** app on the desktop.

### 🧩 Plugins (they need a restart, and you can order it)

- A **plugin** is not a skill. A skill is read again on every turn; a plugin is scanned **once, when your process starts** — so a plugin installed or enabled while you are running does not reach you, and does not reach a new chat session either.
- After `hermes plugins install`, `enable`, `disable` or `remove`, call **`hermes_plugins_reload()`**. That is the whole step. Do not try `sudo systemctl restart …` — it is refused, and this tool is the supported way.
- Verifying a plugin by running `hermes chat -q` proves nothing about **this** chat: that is a separate, fresh process. Only the reload makes the plugin live here.
- The restart closes the owner's open chat window. Tell them to open a new chat; the box shows them a notice saying the same thing.

### 🧠 Your own model

- `ai_list_models()` / `ai_set_provider(...)` / `ai_set_model(...)` — the device default in `~/.hermes/config.yaml`.
Expand Down
28 changes: 28 additions & 0 deletions config/clawbox-sudoers
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,34 @@ clawbox ALL=(root) NOPASSWD: /usr/bin/systemctl disable clawbox-tunnel
clawbox ALL=(root) NOPASSWD: /usr/bin/systemctl restart hermes-gateway.service
clawbox ALL=(root) NOPASSWD: /usr/bin/systemctl restart hermes-gateway

# clawbox-hermes-dashboard DELIBERATELY HAS NO GRANT, and this note is here
# because the request for one keeps arriving with a real problem behind it.
#
# The problem is genuine: Hermes scans for plugins once per process, so a plugin
# installed after boot never reaches the chat that dashboard serves, and Hermes'
# own instruction is to restart. The assistant on a box hits this, reaches for
# `sudo systemctl restart clawbox-hermes-dashboard`, and is refused.
#
# The answer is not a grant, for two reasons.
#
# * IT IS NOT NEEDED. `bounceHermesDashboard()`
# (src/lib/hermes-dashboard-control.ts) already restarts this exact unit with
# no privilege at all: `hermes dashboard --stop` is upstream's own
# SIGTERM-grace-SIGKILL path over a process the clawbox user owns — the unit's
# own ExecStartPre runs the same command — and `Restart=always` brings it
# back. It then PROVES the restart: a new MainPID from systemd and :9119
# answering again. src/lib/hermes-plugin-reload.ts and
# /setup-api/hermes/plugins/reload are built on it and contain no sudo.
# * IT WOULD BE UNSAFE. `systemctl restart` STARTS a stopped unit, so this
# grant would let anything with clawbox-level access on an OPENCLAW box
# resurrect the Hermes dashboard that step_edition_foreign_teardown had just
# stopped and disabled — the state that had two harnesses long-polling one
# Telegram token and the box unable to receive a message for hours.
#
# install-foreign-edition-teardown.test.ts owns that invariant and
# install-sudoers-migration.test.ts asserts it from the installed file
# ("still grants nothing over a Hermes dashboard unit"). Both must stay.

# Ollama, for Settings -> Local Models. Ollama is a system unit and it holds
# real RAM on an 8 GB box, so the owner has to be able to stop it and have it
# stay stopped; until this grant existed nothing in the UI could. `--now` is
Expand Down
58 changes: 58 additions & 0 deletions mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ chronically-failing tool takes *every* ClawBox tool offline for the agent.
| | OpenClaw | Hermes |
|---|---|---|
| Capability store | `app_search`, `app_install` | `skill_search`, `skill_info`, `skill_install`, `skill_list`, `skill_uninstall` |
| Plugin reload (`hermes_plugins_reload`) | **no** — no plugin system | yes |
| AI configuration | in Settings (gateway-owned) | `ai_list_models`, `ai_set_provider`, `ai_set_model` |
| Coding family (`bash`, file tools, web tools) | yes | **no** — Hermes ships its own, and a second unguarded shell doubles the attack surface for no gain |
| Coding agent (`coding_agent_run/status/stop`, `coding_secret_list`) | when the owner switched it on | when the owner switched it on |
Expand All @@ -102,6 +103,63 @@ chronically-failing tool takes *every* ClawBox tool offline for the agent.
| `clawbox_health` | Is the device API reachable and is our token accepted. Separates auth from connectivity. |
| `clawbox_context` | The device field guide, the webapp storage/styling rules, and whose screen the browser tools drive (`BROWSER_GUIDE` — the desktop's window while the owner's real-browser setting is on, an invisible one when it is off). The guide is one file, `Clawbox.md`, filtered before it is served: `<!-- edition:… -->` blocks follow the ACTIVE HARNESS (tool sets) and `<!-- ships:… -->` blocks follow the INSTALL (what the device has), so a `dual` box is told about both harnesses and a Hermes agent is never handed the OpenClaw toolbelt. |

### Hermes plugins (Hermes only)
`hermes_plugins_reload`

A PLUGIN is not a skill, and the difference is the whole reason this tool
exists. A skill is re-read per turn; a plugin is scanned **once, when the agent's
process starts** — `discover_plugins(force=True)` at start, and
`_ensure_plugins_discovered()` returning early ever after, with nothing reachable
over the dashboard socket passing its `force` flag. Hermes knows: every
`plugins install` ends with *"Restart the gateway for the plugin to take
effect."*

On this SKU the process serving chat is `clawbox-hermes-dashboard.service`, so
that instruction means "restart the dashboard" — and the assistant cannot,
because `sudo systemctl restart` is refused (agent shells run with
`no_new_privs`). Measured on the owner's box: a plugin installed at 12:59 into a
dashboard up since 10:52 was proven working in a fresh `hermes chat -q`, listed
as `enabled` by `hermes plugins list`, and invisible to every chat the owner
opened, new sessions included.

This tool is the supported way to ask, and **it is not a privilege**. It posts to
`/setup-api/hermes/plugins/reload`, whose restart is `bounceHermesDashboard()`:
`hermes dashboard --stop`, upstream's own SIGTERM path over a process the clawbox
user already owns, with the unit's `Restart=always` bringing it back and the
route waiting for a new main PID and for :9119 to answer before it reports
`ready`. **No sudoers grant is added or needed**, and one must not be: `systemctl
restart` also STARTS a stopped unit, which would let an OpenClaw box resurrect
the dashboard its foreign-edition teardown had just stopped and disabled
(`install-sudoers-migration.test.ts`, `install-foreign-edition-teardown.test.ts`).

The device does this by itself as well — a watcher in the web server bounces the
dashboard when `~/.hermes` really declares a different plugin set — so the tool
is for the deliberate case: call it once, right after `hermes plugins
install/enable/disable/remove`. The two share one baseline (`process-store.ts`),
so a reload asked for here does not earn a second bounce from the watcher eight
seconds later.

What the answer's `loaded` and `stale` are worth: `loaded` is HERMES' OWN
registry, read with `plugins.list` over the dashboard socket — `null` means the
process could not be asked and never "it loaded nothing", while `[]` means it
answered and has none of them on. `stale` says the box declares a plugin as
enabled that the running registry does not have; it is derived from that
registry rather than from a file's mtime, so an unrelated Settings save cannot
make it true.

Two answers that must not be collapsed. `restarted` without `serving_again` means
systemd owns the restart and it is on its way; that is not a failure and calling
again would stop a dashboard in the middle of coming back. And `loaded` is
`"could not be established on this device"` rather than an empty list whenever
the running agent could not be asked what it registered — a plugin whose only
registrations are tools logs them below the level that read can see, so an empty
list would have the assistant tell an owner their plugin is missing from a device
that is serving it.

**The owner's open chat window closes with the restart.** That is the feature, not
a fault: the box shows them a notice saying to open a new chat, and the tool's
answer repeats it so the assistant says the same thing.

### Hermes skills (Hermes only)
`skill_search` · `skill_info` · `skill_install` · `skill_list` · `skill_uninstall`

Expand Down
6 changes: 6 additions & 0 deletions mcp/clawbox-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import {
import { registerImprovementTools } from "./tools/improvement";
import { registerMediaTools } from "./tools/media";
import { registerOrientationTools } from "./tools/orientation";
import { registerHermesPluginTools } from "./tools/hermes-plugins";
import { registerSkillTools } from "./tools/skills";
import { registerMemoryTools } from "./tools/memory";
import { registerSystemTools } from "./tools/system";
Expand Down Expand Up @@ -195,6 +196,11 @@ export async function buildServer(
// (see `watchEmailReadability` in main()). Nothing else here is re-asked.
registerOrientationTools(reg, ctx);
registerSkillTools(reg);
// Hermes-only, and the registrar drops it on OpenClaw. It is next to the skill
// family because the two are how a Hermes box gains abilities — but a skill is
// re-read per turn and a PLUGIN is scanned once per process, which is the whole
// reason this tool has to exist.
registerHermesPluginTools(reg);
registerMemoryTools(reg);
registerAiTools(reg, ctx);
registerSystemTools(reg, ctx);
Expand Down
146 changes: 146 additions & 0 deletions mcp/tools/hermes-plugins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Making a Hermes plugin the assistant just installed actually reach the chat.
//
// THE FAILURE THIS TOOL REMOVES, from the owner's own transcript (2026-09-18).
// The assistant installed and enabled the `superpowers` plugin, verified it
// worked by running `hermes chat -q` — a FRESH process — and reported success.
// The chat the owner was looking at is served by a long-lived process that had
// started hours earlier, and Hermes scans for plugins exactly once per process.
// So the plugin was installed, provably working, and invisible to every chat the
// owner opened, including new sessions.
//
// The assistant then did the only sensible thing and tried `sudo systemctl
// restart clawbox-hermes-dashboard`. It was refused — agent shells run with
// `no_new_privs`, and no such grant exists or should — and it stopped there.
//
// This tool is what it should have reached for instead. What it asks for is not
// privileged: the route behind it stops a process the clawbox user already owns
// and lets systemd's `Restart=always` bring it back.

import { apiPost, type ApiOptions } from "../lib/api";
import { type ErrorRule } from "../lib/errors";
import { json, type Registrar } from "../lib/register";

/** What POST /setup-api/hermes/plugins/reload answers. */
interface ReloadBody {
restarted?: unknown;
inFlight?: unknown;
ready?: unknown;
plugins?: unknown;
loaded?: unknown;
stale?: unknown;
detail?: unknown;
}

/**
* The restart takes seconds, not milliseconds: a stop, systemd's `RestartSec=5`,
* two `ExecStartPre` steps and the process binding its socket. This waits out
* the route's whole worst case plus the round trip rather than abandoning a
* restart that is working — timing out here is a FALSE FAILURE on the one call
* whose purpose is to say whether the plugin is live.
*
* THE ARITHMETIC, spelled out because it was wrong once and drifted:
*
* - the bounce waits `DASHBOARD_RESPAWN_WAIT_MS` (45 s) for a new main pid and
* the socket, and may spend one more `systemctl show` (5 s) deciding whether
* anything is still coming — 50 s (`src/lib/hermes-dashboard-control.ts`);
* - the post-bounce read is capped at `POST_BOUNCE_READ_BUDGET_MS` (10 s)
* rather than the ~33 s its three RPC budgets allow
* (`src/lib/hermes-plugin-reload.ts`);
* - plus the owner notice and the round trip.
*
* 60 s used to sit under that sum, so a slow box answered the agent "the restart
* was started but did not report back in time" over a dashboard that was already
* serving the plugin. 90 s clears it with room, and the note below is why
* waiting is the right trade in the first place.
*/
const RELOAD_TIMEOUT_MS = 90_000;

const RELOAD_RULES: ErrorRule[] = [
{
status: 404,
code: "NOT_SUPPORTED_HERE",
message: "This ClawBox does not run the Hermes agent, so it has no plugins to reload.",
next: "Do not retry. Report that this device has no Hermes plugins.",
},
{
status: 502,
code: "ENDPOINT_DOWN",
message: "The Hermes agent could not be restarted, so the plugin change is not active yet.",
next: "Tell the owner the plugin change was saved but could not be applied to the running agent, and that a reboot from Settings will apply it.",
},
];

/**
* Timing out is NOT a reason to call this again. The restart is already under
* way on the box, and a second call would stop a dashboard that is in the middle
* of coming back — turning one bounce into two outages for the owner.
*/
const RELOAD_TIMEOUT_NOTE = {
message: "The restart was started but did not report back in time.",
next: "Do not call this again. Wait about a minute, then use clawbox_health or simply tell the owner to open a new chat.",
};

export function registerHermesPluginTools(reg: Registrar): void {
reg.tool(
"hermes_plugins_reload",
"Restart this device's Hermes agent so it loads plugins installed or enabled since it started. "
+ "Hermes only scans for plugins when its process starts, so a plugin you add with "
+ "`hermes plugins install`, `enable`, `disable` or `remove` does NOT reach the chat until this "
+ "is called — not even in a new chat session. Call it once, right after any of those commands. "
+ "Never try `sudo systemctl restart` for this; it is refused, and this tool is the supported way. "
+ "The owner's open chat window closes when the agent restarts, so tell them to open a new chat. "
+ "Answers which plugins the device now declares and, where it can be established, which ones the "
+ "restarted agent actually loaded.",
{},
// HERMES ONLY. On OpenClaw there is no dashboard and no plugin system, and a
// tool that 404s for ever there trips the per-server circuit breaker that
// takes every ClawBox tool offline for the agent.
{ editions: ["hermes"], destructive: true, profile: "core" },
async () => {
const options: ApiOptions = {
timeoutMs: RELOAD_TIMEOUT_MS,
rules: RELOAD_RULES,
onTimeout: RELOAD_TIMEOUT_NOTE,
};
const body = await apiPost<ReloadBody>("/setup-api/hermes/plugins/reload", {}, options);
const plugins = Array.isArray(body.plugins) ? body.plugins.map(String) : [];
// `loaded` is null when the running agent could not be asked what it
// registered, and that is NOT "it loaded nothing" — passing an empty list
// on would have the assistant tell the owner their plugin is missing from a
// device that is serving it. The null is carried through as an explicit
// "could not be established".
const loaded = Array.isArray(body.loaded) ? body.loaded.map(String) : null;
// On most devices `loaded` cannot be read at all — the dashboard publishes
// no plugin-registration lines — so THIS is the fact that proves the
// restart took: the process now serving chat is no longer behind the files.
const stale = typeof body.stale === "boolean" ? body.stale : null;
// Another caller — the owner pressing the card's own reload — already owns
// this restart. Reported as a distinct fact rather than as a failure, and
// with the same instruction the timeout note carries: the restart IS
// happening, and calling again would stop a dashboard mid-recovery.
if (body.inFlight === true) {
return json({
restarted: false,
restart_already_under_way: true,
plugins,
tell_the_owner: "The assistant is already restarting — open a new chat in a moment to use the plugin.",
next: "Do not call this again.",
});
}
return json({
restarted: body.restarted === true,
// `restarted` without `ready` means systemd owns the restart and it is on
// its way back. That is not a failure and must not be retried.
serving_again: body.ready === true,
plugins,
...(loaded ? { loaded } : { loaded: "could not be established on this device" }),
...(stale === null
? {}
: stale
? { warning: "the agent is still behind the files — the plugin is NOT loaded yet" }
: { up_to_date: "the agent now serving chat has read the current plugin set" }),
tell_the_owner: "Open a new chat to use the plugin — the previous chat window closed with the restart.",
});
},
);
}
Loading
Loading