The chmod 644 on pidfiles was injected mid-command between daemon(8)
and its arguments (--host, --port, etc.), causing the shell to pass
'chmod' as a subcommand argument to 'hermes dashboard', which
instantly crashed. Move it after the complete daemon invocation.
This caused the 502 Bad Gateway on ai.clawdie.si after restart —
dashboard process never started because it rejected 'chmod' as an
invalid dashboard subcommand.
These declared platforms: [linux, macos, windows] and omitted freebsd, so
skill_matches_platform() rejected them on FreeBSD (same class as PR #5's
hermes-agent fix). codex ships as a FreeBSD pkg (it's in our host baseline);
claude-code and opencode are Node/Go tools that run on FreeBSD's node24/go.
Coding-agent skills are core for an agent host, so this is the highest-value
slice of the broader "skills omit freebsd" gap (~20 skills). The rest
(research/mlops/python CLI skills) is a follow-up; genuinely OS-specific ones
(apple/*, GPU-only mlops) correctly stay as-is.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`hermes status` reported the gateway as "manual process" on FreeBSD because
get_gateway_runtime_snapshot() only had systemd/launchd branches and fell
through. Add a proper FreeBSD rc.d path so status reports real state:
- is_freebsd() helper (sys.platform.startswith("freebsd")).
- _freebsd_rcd_service_path() + _probe_freebsd_service_running() — probes the
hermes_daemon rc.d service via `service hermes_daemon onestatus` (reports
running regardless of the rcvar enable flag; works without root; fail-safe on
timeout/missing binary), mirroring the launchd probe.
- snapshot branch: manager="rc.d (hermes_daemon)", service_installed (rc.d file
present), service_running (onestatus), service_scope="rc.d".
This is the deeper fix behind the status.py fallback message (PR #6). Not changed:
get_managed_gateway_pids() restart-drain path (systemd/launchd only) — gateway
liveness already works via find_gateway_pids(); rc.d restart-drain is a separate
follow-up.
py_compile clean; wiring + branch-ordering verified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`hermes status` showed "Manager: (not supported on this platform)" on FreeBSD
because the gateway-status fallback only knew systemd/launchd (the primary
get_gateway_runtime_snapshot() path raises on FreeBSD → this except branch).
Add a freebsd branch reporting the rc.d service, matching the linux/darwin
branch style.
Cosmetic-only (the displayed manager line). A deeper fix — teaching
get_gateway_runtime_snapshot()/gateway.py to actually probe the rc.d service
(running/installed) so status reports real state — is a separate, larger change.
py_compile clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two changes needed:
- skills/.../hermes-agent/SKILL.md: add freebsd to platforms list
- agent/skill_utils.py: add freebsd to PLATFORM_MAP so
skill_matches_platform() recognizes sys.platform='freebsd*'
Without PLATFORM_MAP entry, the YAML platform declaration alone is not
enough — the runtime check compares against sys.platform prefixes.
_build_gateway_ws_url() and _build_sidecar_url() used
the raw bound host (0.0.0.0) to construct ws:// URLs for
the PTY child. 0.0.0.0 is listen-only, not diallable —
the TUI gateway couldn't connect, showing 'gateway exited'
in the Chat tab.
Normalise 0.0.0.0 / :: to 127.0.0.1 before constructing
the WebSocket URL so the PTY child can connect back.
- hermes_dashboard.in: matches live install at
/usr/local/etc/rc.d/hermes_dashboard, was untracked
- .gitignore: add ui-tui/node.core (Node crash dump)
- Restore package-lock.json to committed state
Telegram Web doesn't support Bot API 10.1 sendRichMessage, so
gateway startup/shutdown/restart notifications rendered as
'This message is not supported on the web version of Telegram.'
Root cause: TelegramAdapter.send() uses sendRichMessage for
every non-empty message. Lifecycle notifications are plain text
and gain nothing from rich rendering.
Fix:
- telegram.py: add 'skip_rich' metadata flag to _should_attempt_rich()
- run.py: set skip_rich=True on all 4 lifecycle notification paths
(shutdown→active chats, shutdown→home channel, startup→home,
restart→active chat)
Lifecycle notifications now use plain sendMessage. All other
messages continue using sendRichMessage for rich rendering.
Also: sync colibri_bridge.in template from live OSA deployment.
#!/bin/bash does not resolve on FreeBSD — bash lives at /usr/local/bin/bash,
not /bin. The documented first-install command `./setup-hermes.sh` would fail
with 'bad interpreter: No such file or directory'. Use env-based lookup so the
interpreter resolves wherever bash is on PATH (FreeBSD, Linux, macOS).
Covers setup-hermes.sh (the FreeBSD first-validation entrypoint) plus the two
scripts/ bash helpers. install-freebsd.sh is already #!/bin/sh.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Align the FreeBSD/desktop installer with the agreed Python 3.12 floor
(see layered-soul/docs/TOOLCHAIN.md). uv fetches a standalone 3.12, so this
is independent of FreeBSD's pkg default flavor. Lands the writable-repo
version of the patch stranded on osa's read-only hermes-freebsd mirror.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The README claimed "six targeted changes" and listed tools/voice_mode.py as
patched, but commit a9e4caa3f touched only three code files (setup.py,
uninstall.py, install-freebsd.sh) plus this README. voice_mode.py was never
modified — voice works on FreeBSD via pkg-installed ffplay without code
changes, as the same README already states. Drop the phantom row and fix the
count.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- setup.py: FreeBSD platform detection, pkg package manager, rc.d service
- uninstall.py: /usr/local/bin symlink path on FreeBSD
- voice_mode.py: aplay/ffplay audio on FreeBSD
- install-freebsd.sh: native POSIX sh installer for FreeBSD 14/15
- README-FreeBSD.md: documentation
Built from MIT-licensed upstream NousResearch/hermes-agent.
No LGPL code — clean-room implementation guided by platform behavior.
Clipboard (xclip) and voice (ffplay) work via pkg-installed tools.
* fix(desktop): jump-to-approval pill for off-screen approvals
A blocked approval's only response surface is the inline Run/Reject bar on
the pending tool row. When that row is scrolled out of view the session looks
stalled with no visible action. Surface a composer-anchored "Approval needed"
pill only when an approval is pending AND its inline bar is scrolled away;
clicking scrolls the bar back into view. Preserves the deliberate inline (not
modal) approval design — the pill never duplicates the approve/reject controls.
The inline bar mirrors its own viewport visibility via IntersectionObserver
(tracks scroll/resize/layout) and registers a scroll-into-view handler the pill
fires, mirroring the existing thread-scroll jump-button bridge.
Supersedes #45828.
* fix(desktop): morph jump-to-bottom into approval prompt; drop scroll bridge
Collapse the separate "jump to approval" pill into the existing
scroll-to-bottom control: when scrolled away from the bottom while an approval
is pending, it relabels to "Approval needed". A parked approval's inline
Run/Reject bar is always the bottom-most content, so the existing
scroll-to-bottom action lands the user right on it — one control, no collision.
This also fixes the layout corruption from the first cut: the pill called
native el.scrollIntoView(), which scrolls every scrollable ancestor including
the overflow:hidden chat shell containers. Those have no scrollbar to scroll
back and don't remount on session switch, so the composer stayed shoved and
the breakage persisted across sessions. Reusing requestScrollToBottom() (the
use-stick-to-bottom path) only touches the one designated scroll container.
Removes the now-unused approval-scroll store + IntersectionObserver wiring.
Three tests covering: a stale .bak poisoning a failed update's move/restore, an orphaned .bak misread as a user deletion, and a partially written dest blocking restore-on-failure. All three fail on current main without the fix.
Refs #44942
Recover an orphaned .bak before classification (interrupted updates no longer read as user deletions), clear a stale .bak before shutil.move (replace, not nest), and clear a partial dest before restore so restore-on-failure actually runs.
Fixes#44942
tools/approval.py already denies tee/redirection writes to every
_SENSITIVE_WRITE_TARGET (~/.ssh/*, ~/.netrc/.pgpass/.npmrc/.pypirc, shell
rc files, ~/.hermes/config.yaml/.env) via the DANGEROUS_PATTERNS tee/`>`
rules, but cp/mv/install were only paired for _SYSTEM_CONFIG_PATH (/etc) and
the project-relative env/config target. So `cp evil ~/.ssh/authorized_keys`
(SSH-key implant / persistence), `cp creds ~/.netrc`, and `cp evil ~/.bashrc`
(login-time command injection) auto-approved while the equivalent tee/`>`
forms were denied — an unpaired write deny is theater (same rationale as
#14639 / commit 4e9d886d, which paired the terminal side for
~/.hermes/config.yaml writes but did not touch these cp/mv/install verbs on
the broader sensitive set).
Add one (cp|mv|install) DANGEROUS_PATTERNS entry reusing the existing
_SENSITIVE_WRITE_TARGET fragment, anchored via _COMMAND_TAIL so it fires on
the destination (last arg) only: reading OUT of a sensitive path
(`cp ~/.ssh/config /tmp/x`) stays auto-approved. Description differs from the
system-config cp entry so the two keep distinct approval keys (no silent
cross-approval). Additive — does not subsume the /etc or project-config rules.
Adds TestSensitiveCopyMovePattern: 5 positive cases (ssh authorized_keys,
ssh private key via mv, netrc via install, bashrc, ~/.hermes/config.yaml) +
2 negative guards (copy FROM ssh, unrelated copy). The ssh/netrc/bashrc
positives fail on main and pass on this branch; the negatives stay green
both ways.
Carry forward focused follow-ups from PR #45741: treat PTB's raw Bot API 10.1 response shapes safely, recognize real missing-endpoint errors, preserve link preview settings on rich sends, and lock the rich limit to Telegram's character-based cap.
Large paste and Ctrl+A → Delete froze the composer for seconds — both routed
through Chromium's contenteditable editing pipeline (~O(n²) on multiline DOM).
- insertPlainTextAtCaret: Range + text/<br> fragment (paste path)
- deleteSelectionInEditor: range.deleteContents for non-collapsed Backspace/Delete
- Shared composerSelectionRange helper; both flush via flushEditorToDraft
Profiled live (47 KB / 122 paragraphs): paste 4474 ms → 13 ms; select-delete
1304 ms → 4 ms. Collapsed-caret deletes still native.
* fix(desktop): accept slash command on space at command stage
Pressing space on a no-arg slash command (e.g. /hermes-agent) fell
through to the arg-completion stage and dead-ended on "No matches"
instead of inserting the directive. Space now mirrors Tab/Enter while
the command name is still being typed: no-arg commands commit the chip,
arg-taking commands expand to their options step.
* fix(desktop): suppress arg popover for no-arg slash commands
Committing a no-arg command (`/hermes-agent `) re-detected the chip+space
as an arg query and re-opened the popover on "No matches". The arg-stage
menu now only opens when the command actually takes args.
* fix(desktop): polish slash arg completion (space/tab/click + typed args)
Unify Enter/Tab/Space accept of the highlighted item at both the command
and arg stages: no-arg commands commit a chip, arg commands expand to
options, and an arg option commits the full `/cmd arg` chip. A fully-typed
arg (which the backend completer drops from suggestions) now commits on
Space/Tab via the verbatim text instead of dead-ending, and the "No
matches" empty state is suppressed past a command's name. Space stays
slash-only so @ mentions keep a literal space.
The salvaged fix's two regression tests mock adapter.handle_message, so
they only assert the pre-claimed sentinel is set/cleaned around a stub —
they never drive the real dispatch chain. Add a full-path test that
exercises _schedule_resume_pending_sessions -> _guarded_handle_message ->
adapter.handle_message -> _process_message_background -> _handle_message
and asserts the resumed session's agent runs EXACTLY ONCE: not zero (the
pre-claim must not self-bounce the resume into a queued no-op) and not
twice (the duplicate-agent bug #45456 the fix targets). Also assert no
leaked sentinel and no orphaned pending event after the drain settles.
Tighten the _guarded_handle_message docstring: on current main the real
sentinel is taken over inside _handle_message (not _process_message_background),
and note the `is _AGENT_PENDING_SENTINEL` guard only releases the slot we
ourselves placed, never one a live run owns.
When the gateway restarts and auto-resumes an interrupted session, an
inbound message arriving in the window between `asyncio.create_task()`
and the task's first await could spin up a second AIAgent for the same
session. Both agents would then process messages concurrently,
producing interleaved duplicate responses (#45456).
Fix: set `_AGENT_PENDING_SENTINEL` in `_running_agents` immediately
after the "already running" check, before creating the task. This
closes the race window — any inbound message sees the slot as occupied
and queues behind the auto-resume.
A `_guarded_handle_message` wrapper ensures the pre-claimed sentinel is
always released, even if `handle_message` raises before reaching
`_process_message_background` (whose `finally` block handles normal
cleanup).
(cherry picked from commit 85150c976bcd067d96900dbf85a4616bb4851e1c)