- nix dev shell: pnpm-workspace.yaml (overrides, catalog, allowBuilds)
joins the package.json stamp, so a settings-only change relocks.
- desktop-pane-fixture: resolve vitest/config from apps/desktop; pnpm
links it only there.
- short-session hang repro: name the lockfile in the target metadata, so
an npm-era target fails as a lockfile mismatch rather than a bare hash.
- test_mirrors: the pnpm mirror test prefetches from the registry and
needs allow_network, like its siblings.
READMEs, contributor guides, AGENTS.md files, skills and the website (with
the zh-Hans translations) give pnpm commands for the workspaces; the
website itself is still an npm project.
pnpm 12.15.0 (native executable) installs every JS workspace from one
pnpm-lock.yaml, imported from package-lock.json with the same resolved
versions. website/, the WhatsApp bridge and the photon sidecar stay separate
npm projects.
Configuration moves from .npmrc and the root manifest into
pnpm-workspace.yaml: the 14-day minimumReleaseAge and its excludes,
overrides, allowBuilds (the old allowScripts set, with strictDepBuilds
failing any unlisted build script), engineStrict, sideEffectsCache: false
(in-place rebuilds must not write into the store) and
verifyDepsBeforeRun: false (a `pnpm run` never installs).
The pnpm version is pinned in pm/lock.json and in root devEngines.
pmOnFail: ignore keeps that pin out of pnpm-lock.yaml: a leading pin
document makes osv-scanner read zero packages. scripts/build/node-deps.mjs
enforces the exact version instead, and every lock reader refuses a
multi-document lock. A root preinstall refuses `npm install`; devEngines
onFail is warn so npx (MCP servers, the tsc linter) keeps working inside
the checkout.
User-machine installs run --frozen-lockfile --trust-lockfile (release age is
checked when locking, in CI, not per user). node-deps removes npm-era
node_modules trees before the first pnpm install. Nix builds the store with
fetchPnpmDeps and a hash file that CI checks.
`hermes -z` has run through the gateway (`_run_oneshot_from_args` ->
`launch_gateway_chat`) at every dispatch site for a while, but the old
in-process runner stayed behind: `hermes_cli.oneshot.run_oneshot` /
`_run_agent` built a bare AIAgent outside the session authority, and
`hermes_cli.main._run_and_exit_oneshot` (with its
`_cleanup_oneshot_runtime` cleanup table) was its only caller. Nothing
outside tests reached any of it.
Delete that path. `hermes_cli.main._exit_after_oneshot` stays exactly as
it is: `--cpu-profile` names it as the one-shot hard-exit path and it
writes the profile before `os._exit`; only its docstring loses the
pointer to the removed cleanup helper. `hermes_cli/oneshot.py` keeps
only what the gateway path and gateway CLI turns still import: the
exit-code mapping, the `--usage-file` ledger with its auxiliary-usage
delta, and `--toolsets` normalization. The process-receipt eval probe
now calls `process_registry.wait_for_pending_completions` directly.
Tests that only exercised the deleted runner are removed; the exit-code
and usage-file tests now call the kept helpers directly; the cost-guard
test patches the gateway launcher instead of the removed function. A new
test pins that `hermes -z` dispatches to `launch_gateway_chat`.
Refs #145171 (EP-11a)
- The MCP OAuth renderer probe bundled its entry with esbuild resolving from
the checkout root, so its react, react-dom and react-query imports still
relied on hoisting. Resolve them from apps/desktop, which declares them
(verified: from the root they fail to resolve in an isolated pnpm tree).
- The Windows MSIX commit fixture linked node_modules before `git add apps`,
so the commit could pick up the dependency link. Link the install trees
after the commit, which needs only the fixture's own files.
Scripts outside the declaring workspace found these through the hoisted root node_modules:
mock-server and connector-rehearsal (whose desktop-local Electron path did not exist under npm),
ui-tui's visual harness, the eval harnesses, and the tests/install/e2e-assets drivers, which import
tests-js's @playwright/test and zod. They now resolve from apps/desktop or tests-js; the typed
e2e-assets files go through a tests-js re-export module so their types resolve the same way.
test_cron_timeout_closes_the_descendant_snapshot_fork_window read each
descendant's status the instant _run_job_script returned. SIGKILL is
asynchronous: the killed process stays in state R until the kernel
finishes tearing it down, and freeing its memory waits for a CPU on a
loaded runner. CI caught the pre-existing setsid child there
({gone, running, zombie}).
The kill was delivered. A scratch dump at the check showed the "running"
child in state R with SIGKILL pending (ShdPnd 0x100) and PF_EXITING set,
and it exited 86-233 ms later. kill_process_tree is correct.
The probe now waits up to 10 s for every recorded descendant to be gone,
a zombie or dead (the transient X state while its parent reaps it). The
children now sleep 60 s instead of 15, so a descendant the kill missed
is still alive when the wait ends and the probe still fails.
Repro: give the first child 1.5 GiB of touched memory, so its exit takes
~100-200 ms. Base fails 12/12 with CI's shape; the fixed probe fails
0/12. The fixed probe still fails when the fork-window stop+rescan is
reverted, and when snapshotted descendants are never signalled.
browser_exec is the only core browser tool (plus browser_cdp, browser_dialog,
browser_vault, all CDP). Deleted: browser_navigate/snapshot/click/type/
scroll/back/press/get_images/vision/console and every module that served
them (browser_tool_session/_lifecycle/_cloud/_cdp/_real_profile/
_lightpanda_fallback/_eval_policy/_origin/_snapshot/_vision) plus
browser_camofox*.py (Camofox ships as the catalog plugin 'camofox').
tools/browser_tool.py and tools/browser_tool_install.py shrink to the
frozen old-updater stub warm_agent_browser_npx_cache.
toolsets: _HERMES_CORE_TOOLS / browser toolset list only the CDP tools.
model_tools: browser_tools legacy alias and the browser_navigate /
browser_type schema rewriters removed. Cloud providers no longer request an
install post_setup hook (nothing to install).
`hermes claw` (hermes_cli/claw.py) and the openclaw-migration optional skill
moved into the agent-importers plugin (hermes-official-plugins #140/#141), which
keeps `hermes claw migrate|cleanup` as the older spelling of
`hermes import-agent openclaw`. Core drops them with their tests (ported to the
plugin) and the eval leg that loaded the skill's script.
Setup: hermes_cli/setup_agent_import.py replaces setup_migration's OpenClaw
offer. On a first-time `hermes setup` it checks ~/.openclaw (or ~/.clawdbot /
~/.moltbot), ~/.claude, ~/.codex and ~/.cursor; when any is found it asks
"Import your setup from <found>?" (default yes), shows a checklist when more
than one was found, installs agent-importers through the normal catalog install
(or enables it when present), then runs `hermes import-agent <agent>` for each
pick in a child process, which previews and asks first. OpenClaw gets
--migrate-secrets, as core's step did. Nothing found, a no, or
HERMES_NONINTERACTIVE: no prompt, nothing installed. The skip-configured-section
offer now follows any import that changed config.yaml or .env.
LEFT_CORE: `claw` joins the agent-importers row as old_cli, so `hermes claw`
without the plugin names the install. No automatic install for it: an OpenClaw
migration is one-shot and leaves no state that needs the plugin later.
Docs: import guide, OpenClaw guide, cli-commands, quickstart, bundled
cli-reference; locale tips and README pointers.
Symptom: in a classic `hermes chat` session on the shared gateway, /cron
add|edit|pause|resume|run|remove still ran main's handler in the CLI
process, which called the cronjob tool there and wrote the profile's
cron/jobs.json from the client. Under strace, every write to jobs.json
came from the `chat --cli` process and none from the gateway.
Root cause: the one-gateway runtime (#106742) made the gateway the
owner of the profile's runtime state and of the ticker that fires cron
jobs. It left CLICommandsMixin._cron_* in the client, and the
gateway's slash.exec refused /cron for every local session except TUI
reads (2efe028116). That left the cron store with two writers.
Change:
- Move main's /cron handler (overview, list and every write) into
hermes_cli/cli_cron_lines.py as line builders (cron_command_lines),
with no other behaviour change. The CLI prints them when it has no
gateway session. The unused CLICommandsMixin._cron_list goes; its
test and eval read cron_list_lines directly.
- On the gateway, /cron is an owner command
(gateway/session_cron_commands.py). Bare /cron and `list` are reads
for every local session; that includes Desktop- and ACP-created
sessions, which main refused with unsupported_command. Every other
form is an admission-fenced control. An unclosed quote is refused as
invalid_params before pre_command fires. `/cron list -a` keeps the
list-all alias that the TUI read path accepted.
- A /cron write runs only when it arrives on a connection that is not
attached to the session: the classic CLI's own command socket
(GatewayConnection.call_until), which waits with no deadline. A
connection attached to the session keeps main's unsupported_command.
That is how Ink, Desktop and ACP send /cron: on their main
connection, which the gateway answers one request at a time and
which waits with a deadline, while a /cron run lasts as long as its
job. The gate is on the requesting connection, not on the surface
that created the session, so a classic CLI that resumed a TUI-made
session still writes (as on main), and Ink on a CLI-made session is
still refused.
- The classic CLI sends /cron to slash.exec
(gateway_setting_commands.GATEWAY_SETTINGS) with no deadline, on a
socket of its own (gateway_plugin_commands.call_unbounded, now shared
with plugin commands; its Ctrl+C handle is renamed
_gateway_command_wait). Ctrl+C prints the stopped line and
propagates.
- Regenerate the parity table. test_pre_command_in_gateway pinned the
old refusal of every classic CLI /cron; it now pins that the accepted
/cron forms fire pre_command once and a refused one (an unclosed
quote) fires nothing.
Why: the design contract allows one writer per owned store, and
clients only read and submit. The gateway already serves this
profile's cron store to its ticker and to Desktop (cron.manage). Now
the classic CLI's writes go through the same process.
Refs #134889 (T1)
Refs #139158
375 commits from main (0e427d788d). 24 files conflicted; the resolutions that carry a decision:
- /initiate-setup: main removed the command (25bf722063). Its gateway rewrite, CLI handler and the
Desktop kickoff go with main; this branch's command-layer admission of it is dropped too
(gateway/session_commands.py _PROMPT_COMMANDS, its slash.exec test row, the parity doc row), and
the Desktop slash dispatcher loses the `hidden` option that only the kickoff used.
- Code this branch moved out of its old file, carrying main's edits to the moved copy:
first-contact notes (gateway/run_turn.py -> run_turn_prepare.py: no message/setup_command),
the TUI agent factory (tui_gateway/server.py -> agent_factory.py: onboarding toolsets at build,
setup_beat/set_theme lifecycle cards, no cli platform extras on a coding selection), the serve
lifespan (web_server.py -> web_server_app.py standalone_lifespan: first-install probe before the
state.db reconcile, onboarding due answer after the setup bootstrap), and the compression child
publish (the shared _publish_compression_child_on_conn reads the parent's model_config).
- agent/auxiliary_client.py: both import changes (_route_key from main; this branch's move of
_without_structured_output_format).
- Desktop: main's onboarding run (decideOnboarding, OnboardingEnd, card-waiting Send) replaces the
kickoff; this branch's prepared-submission slash path, request_id on session.create and
PreparedImageRecovery stay. Pool-limits imports are dropped (this branch removed the pool).
createBackendSessionForSend takes main's (preview, overrides) signature.
- ci.yaml detect job: main's `ref: inputs.commit` plus this branch's sparse checkout.
- package.json / lock: main's vitest 4.1.11 plus this branch's ws 8.21.1.
- Generated: gateway-contract.openrpc.json regenerated; parity doc regenerated.
set_theme (desktop_ui, deferred like apply_layout was) sets the accent,
light/dark mode and layout preset, or with `picker` shows an accent or
layout card and waits up to 300 s. apply_layout emitted a layout.apply
event and returned success whether or not the renderer applied anything;
set_theme asks the renderer through the theme.apply server request and
returns its answer, so an unknown preset or a chat that is not on screen
comes back as an error with the available ids.
theme.pick waits for a person; the backend tells a dismissed card
({cancelled}) from an expired one ({timed_out}). The tool description
lists the Simple and Advanced preset ids from one constant, which fixes
the old description that named neither sidebar preset. The layout.apply
event and its renderer handler are deleted with the tool.
Conflicts (2 files, 1 hunk each); every main-side change is kept, ported to
where the branch moved the code:
hermes_cli/kanban_db.py (build_worker_context helpers _ctx_cap .. _ctx_comments)
main: f5a6651231c/d56e357556f coerce foreign/text timestamps on read; in
_ctx_role_history the "Recent work" stamp became
_ctx_stamp(_to_epoch(row['ended_at']) or 0, now) instead of int().
branch: moved the whole _ctx_* helper block into hermes_cli/kanban_db_context.py
(kanban_db.py imports it back), deleting it from kanban_db.py.
merged: kept the branch's deletion in kanban_db.py and applied main's change in
kanban_db_context.py::_ctx_role_history as
_ctx_stamp(_kb._to_epoch(row['ended_at']) or 0, now). Main's other
timestamp coercions in kanban_db.py (Task/Run/Comment/Attachment/Event
from_row, _TASK_EPOCH_COLUMNS, release_stale_claims,
_extend_live_stale_claim, task_age) auto-merged unchanged.
hermes_cli/web_server.py (_lifespan)
main: d7830c6f42 reworded one comment in the free-tier bootstrap block
("the ONE place its identity is created" -> "the one boot-time creator
of its identity"; guests are now also created on first connector use).
branch: moved _lifespan's body into hermes_cli/web_server_app.py::app_lifespan.
merged: kept the branch's relocation and applied main's comment rewording in
web_server_app.py.
Merge-induced health fix (no conflict marker; the combination tripped the ratchet):
apps/desktop/src/app/session/hooks/use-cwd-actions.ts::changeSessionCwd
main: 63ed3caa4ba/dbc538547fc grew the function (statusbar cwd change for the
focused chat / tile runtime ids) to CC 20.
branch: added the session_busy refusal mapping (sidebar.projects.moveBusy) in
its catch, +1 branch -> CC 21 > target 20 (scripts/check health blocking).
merged: both behaviours kept; the busy-error mapping moved verbatim into a
module-level cwdChangeError(err) helper. Health back to 0 blocking.
Check: every line main added between 1fd1827726 and 3b63f7b0a4 (317 files) is
present in the merged tree.
The stock prompt surface regrew ~3.4k tokens after the August diet, almost
all from features added straight into the never-deferred core. This takes
back the biggest pieces:
- browser_vault: the five eager browser_vault_* tools (list, fill, unlock,
save_login, enter_code) become one browser_vault tool with an action enum,
deferred behind tool_search by default. It still rides with the browser
(an empty vault keeps save_login reachable), so discovery holds; only a
catalog line is paid per request. Results and errors name the new actions;
the input-tool password note points at it through tool_call.
- terminal: schema re-dieted (1,136 -> 651 Qwen tokens). Description and
param docs no longer repeat each other; heartbeat / persist_on_release keep
their rules in one line each; process(...) references name process_manage.
- tool_search / tool_call: the keyword-query rule lives once, in the queries
param; description and connector hint trimmed.
- system prompt: the "Async handoff" block is removed. Its rule ships in the
delegate_task description and in the background dispatch result the model
reads at the moment it matters, and the polling loop it targeted is guarded
structurally (#124724).
Stock CLI surface (prompt-surface render, chars/4): 18,612 -> 16,524.
Fresh install, Qwen3 tokenizer: 14,962 -> 13,120 (Sep 7 post-diet: 11,595).
Live A/B harness: evals/prefix_rediet/.
evals/shared_gateway/README.md said the "unintegrated bootstrap is expected to fail"
normal_boot.py; this PR integrates it and tests/gateway/test_normal_runtime_boot.py
asserts state == 'ready' from the real control socket. The README now says a pass is
expected and a failure is a regression.
The four worker wave notes (worker-persistence-inventory.md and the -substrate,
-lifecycle-wave22, -constructor-context-wave20 notes; ~2,000 lines) were pinned to
an old snapshot, cited private worktrees and .hermes/plans paths, called worker
switching "still blocked", and nothing links to them: removed from the shipped tree.
Docs only; no test.
Review: ahrazzle B2-28
Docs follow code (round-13 ruling for stale docs):
- cli.md said classic `hermes chat` refuses bare --continue and --resume latest
(exit 2). gateway_chat.wants_latest/_resume_latest resolve both through the owner
and _UNSUPPORTED holds neither; the developer guide already documents them. The
refused list also omitted --compact, which _UNSUPPORTED refuses. Examples and the
info box now match _UNSUPPORTED.
- The busy_input_mode `queue` row said the classic CLI queues in-process.
GatewayChatView.submit sends every line to prompt.submit, never reads
busy_input_mode and has no steer/redirect path: classic chat always queues at the
gateway; interrupt/steer are TUI/Desktop behaviours.
Docs only; no test.
Review: ahrazzle B2-23
Review: ahrazzle B2-24
No conflicts. Brings in main's reasoning-replay series, holographic memory leaving core
(run_profile_reconcile now imports it through import_provider_module), and the update
main-channel fix.
Eight files the branch adds ended in one or more blank lines, so
`git diff --check $(git merge-base HEAD origin/main) HEAD` reported 8 "new blank line at EOF"
errors. Whitespace-only: each file now ends with exactly one newline; no other line changes.
Check (8 errors on r11-base, 0 here): git diff --check c57331677d HEAD
Review: andrexibiza 24
Review: andrexibiza 5479692359-P2.10
Review: JoaoMarcos44 diffcheck
(cherry picked from commit 6b34c53b7041b037431405282a6fb2d76f657893)
Anthropic answers a probe that asks about "earlier reasoning" with a 200
content_filter refusal and no usage block, which left cross-turn cells
without a baseline. Both refusal shapes now rerun the cell with neutral
wording, and opaque cells pick the newest turn-1 capture per position so a
partial rerun does not mix captures.
Newer OpenAI/Gemini models emit no reasoning on a trivial turn 1, so step 2
had nothing to replay; turn 1 now poses a small arithmetic task and escalates
effort until a reasoning carrier and tool call come back. Opaque cells are
compared only within one turn-1 capture (run_id), xAI's safety-layer 403 on
"earlier reasoning" falls back to neutral probe wording per cell, xAI
cost_in_usd_ticks is read as billed cost, and keyless local servers cost $0.
A 200 only proves a provider accepted a replayed reasoning field; the gateway or
chat template may still drop it. This stdlib-only harness measures whether each
carrier (reasoning_content / reasoning / reasoning_details / Responses reasoning
items / Gemini thought parts and signatures) reaches the model's prompt: the
prompt-token delta against a byte-identical baseline, plus canary recall, for
in-loop and cross-turn positions, and real turn-1 opaque carriers replayed
verbatim vs stripped. Portal calls are keyed by lane (gen- = OpenRouter) and
can be resampled until a direct-lane result exists. Results stay out of tree.
Main made the strict ruff config the default (`ruff check .`, blocking), and
the branch's code carried 278 findings under it (main itself is clean). Fixed:
- PLW1510 (86): `check=False` on each subprocess.run, preserving behaviour,
as main's own sweep (0006a5e229) did.
- ASYNC220 (9, test fixtures only): `noqa` with the reason, main's convention
for a Popen that returns at once and whose handle the test drives.
- ruff's safe autofixes plus the narrow unsafe ones (UP006/UP037/RUF100/RUF013/
PLR0402/RUF059 underscore renames/UP031/UP041/UP017/FURB167/PIE807/RUF007/
RUF015/RUF022/FURB162), and by hand: four %-formats to f-strings, three
unused unpacked names, one fixture loop over `.items()`.
No behaviour change. `ruff check .` passes; scripts/check passes; the 412 test
files mirroring every edited file pass (3 tests/pm files that fail only under
that batch's uv lookup pass solo, before and after this commit).
Conflicts: tests/hermes_cli/test_finite_chat_delegation.py and
tests/tools/test_completed_process_results.py, where main's lint sweep (0006a5e229) added
check=False to subprocess.run calls this branch had rewritten. Kept the branch's test bodies
and carried check=False onto their subprocess.run calls.
17 conflicts, all from main's lint sweep (ruff UP007/UP017/UP022/UP028/UP031/UP037/UP041,
FURB188/FURB192); no other main commit touches a conflicted file. Resolution: the branch side
of each file plus main's exact autofix (ruff 0.15.10, main's ruff.strict.toml, --fix
--unsafe-fixes). ruff.strict.toml takes main's copy. The pool-retirement live fixture stays
deleted with the pooled backend this branch retires.
8 conflicts, all from main's lint sweep (#135281 "Harder linter fixes" + the RUF series:
implicit-Optional, unused-unpacked-variable, next(...) for [...][0]). No other main commit touches
a conflicted file. Resolution: the branch side of each file plus main's exact autofix
(ruff 0.15.10, main's ruff.strict.toml, --fix --unsafe-fixes) on it. ruff.strict.toml takes main's
copy (the newly enabled rules). tests/cron/test_bot_chat_delivery_child_env.py stays deleted
(1cc9cb9199 retired its lane).
45 conflicts, all from #135177 (lefthook + `ruff check --fix` over 2,366 files). No other main commit
touches a conflicted file. Resolution: the branch side of each file, then main's exact autofix
(ruff 0.15.10, `--config ./ruff.strict.toml --fix`) on it — rewrites only (typing generics, noqa
drops, import forms), no import removed. tests/cron/test_bot_chat_cli_home.py stays deleted
(1cc9cb9199 retired its lane).
Desktop refused any group-writable profile home (mode & 0o022) because Node cannot prove the
group private (no NSS/xattrs). The gateway's own policy (home_mode_unsafe) admits group-write
for the owner's private group with no ACL — the umask-002 user-private-group default. The
refusal was classified as a stale gateway, so Desktop re-ran `gateway ensure`, whose home
maintenance chmodded the operator's directory back to 0700 to get through.
Now Node refuses only world-write; a group-writable home is handed to the installed runtime's
Python ticket client (the same helper Windows uses), which applies home_mode_unsafe before
minting. A shared group or a named-user ACL comes back as unsafe_control_permissions and is
reported as "run chmod g-w <home>", never as a stale gateway. Socket, pointer and fallback dir
stay owner-only.
Live (real Electron + gateway, 0775 home set after first-dial maintenance): before, the second
dial logged "forgot stale canonical endpoint ... re-ensuring" and the home ended 0700; after, it
mints with the home still 0775.
Code-health ratchet on PR #106742 (BLE001/S110/HX006), no behaviour change:
- narrow per-row JSON skips: hosted_room_attachments.reconcile_room_events
(ValueError/TypeError/AttributeError/RecursionError), hermes_state_gateway
routing-entry lookups (ValueError/TypeError/RecursionError)
- approval_context: only the gateway import is optional (ImportError);
finite_turn_required() is a ContextVar read that cannot fail
- deliberate boundaries keep their behaviour and now log the traceback:
profile park (exc_info), session-store init (module logger, same
"gateway.run" name, still WARNING), a2a profile dispatch (local log;
outbound reply stays redacted), readiness probe and session_search
profile open (debug), Windows control-pipe thread hand-off (debug; start()
re-raises), eval harness top level (logger.exception)
- managed_worker.main: traceback goes to the worker's own stderr (the owner
spawns it with stderr=DEVNULL) via a non-propagating handler, never the
channel or profile log files; the owner still gets only
managed_worker_failed
- evals/shared_gateway: timeout=30 on git rev-parse/diff calls
(cherry picked from commit dfc56cdd5ab875bd14b921de71be4a5bc8afc57f)
12 conflicted files, resolved in three lanes:
- gateway/run.py: take the branch split; main's tirith removal and spool-replay
ordering ported into run_runtime_init.py / run_bootstrap.py.
- config migrations: main keeps v50 (tirith); the branch's bot-chat timeout removal
becomes v51 (_config_version 51, template + test updated).
- hermes_state_usage.py: main's voice_chat task route threaded through the branch's
owner transaction; worker.persist usage allowlist accepts task.
- tui_gateway/server.py: branch side; main's edits applied where the code now lives
(agent_factory, session_registry). Desktop sessions get the catalog toolset on the
authority path; daemon-run finite CLI turns are not watched turns; lazy session info
reports nous/welcome on the free tier.
- Desktop/Ink: one branch-child spinner fix (main's route-token check); main's
uninstall i18n keys kept, pool keys stay removed; voice_turn forwarded and accepted
by the authority submit (surface_v1.voice_turn).
- chat_ws / PTY keepalive test: main's stricter test kept.
_get_due_jobs_locked repairs store-side problems mid-scan (half-paused records
self-disable, completed one-shots are swept, records are normalized) and then
persists them with a bare save_jobs(...) before returning the due list. Any
store-write failure there — ENOSPC on a full disk, a read-only mount, a
permissions problem — raised out of get_due_jobs(), so cron_tick() aborted and
NO job on that profile ran until a write succeeded. Live on a full /opt/data:
"Cron tick error for profile at /opt/data: [Errno 28] No space left on device"
from that exact call site, repeating every tick, with every job then reporting
"missed its scheduled time" and re-anchoring.
The repairs are already applied in memory — that is what the scan keys off — so
the persist is a side effect the next tick retries. Contain the store write and
keep dispatching; a non-store error still propagates.
(cherry picked from commit 76676fb0084f06890f76280f50387325ad2d59c7)
tirith ran `tirith check` on every terminal command, default-on with fail-open, as a
pinned PM binary auto-downloaded at startup. Measured against the shipped 0.4.2 binary
it caught 14/16 attack commands but flagged 12/24 everyday agent commands
(`echo $((6*7))`, `"$VAR/x.sh"`, `gh api | python3`, LAN curls, `.dev` hosts,
`pip install` via analysis_incomplete), and in cron/-q deny mode each of those is a
hard block. It only ever saw terminal strings (execute_code, writes and the browser
bypass it), fail-open meant no protection wherever the binary could not install
(Windows, Termux, x64 musl), and it produced a steady issue/PR stream (fail_open
semantics four times, install loops filling disks, a SIGSEGV hang, session-approval
scoping, third-party promo text on approval cards).
Removed: tools/tirith_security.py, the tirith branches of the command guard (the
is_tirith tuple, session-only "always" downgrade, import-error/fail-open paths,
unattended tirith pass), the CLI and gateway startup installers, the PM package and
lock pins, the nix runtime dep, security.tirith_* defaults, the CLI warning string
and three tirith tips (gaps refilled from the tail so translations survive), plus
the gateway's "no automated assessor" warning that only existed to point at tirith.
Config v50 drops security.tirith_* and enables nothing in its place. Hermes ships
no tirith plugin; if tirith upstream publishes one, it goes through normal catalog
admission under their name.
plugins.isolation: host imports every non-bundled plugin in a host process started
on demand per profile with only that profile's environment, and hands plugins a
RemotePluginContext; Hermes registers proxies for tools, hooks, commands, skills and
provider objects (subclasses of the ABC it checks). Re-entrant: handlers may call
ctx back into Hermes while Hermes waits on them. Every user-code import path routes
to the host (general, memory/context-engine/cron, model-provider profiles via a
cached credential-free extraction, dashboard APIs via an ASGI bridge) or refuses.
One table names what cannot cross (live gateway objects); the static audit shown
by `hermes plugins validate/show` reads it. A host that dies is restarted with its
plugins (bounded). Default stays in_process: nothing changes unless opted in.
Why: shared-pool deployments must never run one tenant's plugin Python in an
interpreter holding another tenant's data, without plugins becoming tenant-aware.
The worker isolated HERMES_HOME only. The longrange_backup_pipeline task
("schedule it nightly at 2am") let agents run the operator's real crontab
and write into the real ~/.hermes: a past battery left three nightly cron
jobs and two scripts behind on the host, pointing into deleted run dirs.
sandbox.isolate_host turns on the product's own TERMINAL_HOME_MODE=profile
contract, points the worker's HOME at the same profile home (in-process
~ expansion for write_file), puts a file-backed crontab shim first on PATH
and unsets the systemd user bus. README quickstart drops its /tmp paths.