Registers superagents-lab/hermes-search1api (v0.1.0) — five tools backed by
the Search1API API: search1api_search, search1api_news, search1api_crawl,
search1api_sitemap, search1api_trending. Vendored official Python SDK; only
third-party dependency is httpx (already a Hermes core dependency).
hermes plugins validate (main @ 6005aa1): all checks pass, security scan
safe; declared tools match registrations.
Submitted by the plugin repo owner (superagents-lab).
The pinned tree now carries an Agent Plugins v1 plugin.json at the repo
root, so 'hermes plugins validate' passes and the catalog installer
finds its manifest. Security scan clean (no cautions).
New pin: 347ce44a1e71173285b2943a7a97bbdece8327b2
Split out from drkpxl/drkpxl-skills so the entry matches the product:
one skill, one repo, name on the tin. The new repo contains only the
morning-briefing skill. Entry name, repo URL, SHA pin, description,
and category all updated.
Community skills collection by the repo owner (drkpxl). Anchor skill:
morning-briefing — a personal daily newspaper that gathers weather,
calendar, AQI, curated 36-hour news (Reddit RSS + X + web search with
per-story QR codes), and newsletter digests, renders a single 8.5x11
newsprint page, and prints it every morning.
Also bundles: Tiny Air (US AQI via MCP), Bro, Copywriting, Delegate to
pi, Prototype, Research, Requesting Code Review.
SKILL.md follows the HARDLINE authoring standards (validated: 54-char
description, modern section order, native tool references). SHA pinned
at d81a9b5ee427a378839828d4a8eab3925468b7ce.
The Windows CRT reports isatty()==True for every character device, the NUL
device included, so `hermes gateway start < NUL` (stdin=DEVNULL) still took
the TTY branch: prompt_yes_no() hit EOF, defaulted to Yes and registered a
Scheduled Task — the silent install #113977 describes. Confirm isatty with
GetConsoleMode(STD_INPUT_HANDLE); a handle no console accepts is treated
like a pipe. Pipes and HERMES_NONINTERACTIVE keep their behaviour.
`start()` asked "Install it now so the gateway starts on login?" with default Yes, then called
`install()` which asked two more default-Yes questions; on a redirected stdin (`< /dev/null`,
scripts, agent tool calls) all three answered themselves and a bare `start` wrote a Startup-folder
login item, spawned the gateway, and start() spawned it a second time (#113977).
- The persistence question is answered only explicitly: `HERMES_GATEWAY_INSTALL_START_ON_LOGIN`
(which start() never consulted before) or a prompt on a real TTY. Without a TTY, or under
`HERMES_NONINTERACTIVE=1`, the answer is No: the gateway starts, nothing persistent is written,
and the explicit `hermes gateway install` command is printed.
- Declining on a TTY also starts the gateway — the command is `start`; only persistence was declined.
- A Yes hands `start_now=True, start_on_login=True` to install(), which spawns and reports (including
the UAC hand-off), so start() no longer double-spawns and no longer prints the false
"Gateway install did not complete in this process" after an install that was skipped by intent.
- `install --no-start-on-login` without `--start-now` now hints `hermes gateway run` instead of the
`hermes gateway start` that used to offer the very auto-start just declined.
Supersedes #113981 (@KoNit-K), which flipped the first prompt's non-TTY default but left the
env override, the hint text and the false warning in place.
build_converse_kwargs gated cachePoint markers on the raw model id, so a profile ARN wrapping
Claude matched nothing in _CACHE_POINT_PATTERNS and silently lost Bedrock prompt caching.
_model_supports_prompt_cache now resolves the profile through the per-process-cached
_resolve_inference_profile_model_id first; the request still targets the profile ARN.
Part of #114476
The cherry-picked #114482 never resolved a profile in production: the only
caller, agent/model_metadata.py::_resolve_bedrock_context_length, invokes
get_bedrock_context_length(model, probe=False) with no region, and the
resolver was gated on `region`; and it called
get_inference_profile(inferenceProfileId=...) where botocore requires
`inferenceProfileIdentifier`, so even with a region the call raised
ParamValidationError, was swallowed, and the 128k default applied with only
the new warning. Live against a botocore Stubber: 128000 before, 1000000 after.
- resolve in the ARN's own region (field 4), then the passed region, then
the standard AWS chain; the runtime region / base_url may differ
- inferenceProfileIdentifier is the only request parameter GetInferenceProfile has
- match only `application-inference-profile/`: system-defined
`inference-profile/us.anthropic...` ARNs embed the model id and need no call
- no nested-profile recursion: GetInferenceProfile lists foundation-model ARNs
and the wrapped ARN itself satisfies the static-table substring match
- cache both outcomes per process (runs on every context-length resolution),
cleared by reset_client_cache()
- tests trimmed to two invariants on the production call shape; restore the
TestBedrockContextProbe class header the cherry-pick clobbered
- docs: bedrock:GetInferenceProfile IAM permission and the fallback WARNING
Co-authored-by: Yagna Vudathu <yagnavudathu@gmail.com>
An application-inference-profile ARN names no model, so the context probe
error text and the static substring table both miss and the 128k default
silently applies: a profile wrapping a 1M-context Sonnet compacts at ~96k.
Resolve the wrapped foundation-model id via GetInferenceProfile (reusing
the control-plane client; an application profile's id IS its ARN, so the
call works on both API shapes), then let the existing probe/table paths
run on that id. Resolution failures (missing bedrock:GetInferenceProfile,
no creds) keep today's behaviour but now log a WARNING naming the
explicit model.context_length escape hatch instead of staying silent.
Fixes#114476
Gate review: `--keep` pruning ran after the summary regardless of `errors`,
so a timer hitting the same unreadable file every run would exit 1 each time
and still rotate every complete `hermes-backup-*.zip` out after N runs,
leaving only incomplete archives. Prune only after a complete backup; the
test pins a pre-existing good archive surviving an incomplete run with
`--keep 1`. The summary no longer hard-codes the caller's exit code.
`hermes backup` recorded per-file failures, printed `Backup incomplete: <path>`
and still returned shell status 0, so a cron job or systemd timer would publish
"successful" archives missing state.db indefinitely.
`run_backup()` now returns whether the archive is complete and `cmd_backup()`
maps False to exit status 1. The zip is kept so the operator can still restore
the rest; hard failures keep their SystemExit(1)/(2). `--quick` is unchanged.
Slim redo of #101096 on current main (the branch predates the run_backup /
_run_backup_locked split and the backup lock); same policy, same exit codes.
Supersedes #68866 (@jbryce) which proposed the policy first.
A provider whose catalog default has no path (api.anthropic.com) has no
sibling OpenAI-compatible path to infer an override's protocol from, so the
declared transport stays; the docstring says so and a test case pins it.
Round-2 gate folds: utils.base_url_path sits next to base_url_hostname on
the same scheme-tolerant parser, so a scheme-less override (api.minimax.io)
keeps host and path checks agreeing; a default with no path is an explicit
"nothing to compare" case; the dead `or "chat_completions"` after
determine_api_mode is gone; the test fixture seeds the catalog defaults
unconditionally so a warm models.dev cache cannot change what the contract
compares against, and the stale #53054 test comment states the new contract.
Gate findings folded: the accepted path is derived from the provider's
catalog default (/anthropic for MiniMax, /plan/anthropic for Tencent) instead
of a hardcoded /anthropic prefix, so /anthropic-compat no longer passes and a
non-/anthropic default provider's own endpoint does; an empty base_url keeps
the declared transport like an unknown default; the catalog read is
allow_network=False (determine_api_mode just warmed it); _base_url_path is
shared with URL detection. Tests pin the catalog defaults they compare
against so the contract holds offline.
33a2f29a6 made the runtime fallback honour the provider's declared transport
when URL detection has no opinion. For minimax/minimax-cn that is
anthropic_messages, which is right on api.minimax.io/anthropic but wrong for a
model.base_url override at an OpenAI-compatible relay (LiteLLM, token proxy,
corporate egress) or at the provider's own /v1 path: every request is built
Messages-shaped with x-api-key and 401s, or silently reroutes to a fallback
provider.
The fallback now keeps anthropic_messages only when base_url shares the
catalog default's host and is the bare host or a path under /anthropic;
everywhere else it lands on chat_completions. Other declared transports are
untouched (openai-api on a custom proxy still keeps codex_responses, as
33a2f29a6 intended), and URL detection still runs first.
Salvage of PR 76859 by webtecnica, rebuilt on the refactored module with the
path check teknium asked for in review.
Co-authored-by: webtecnica <webtecnica@gmail.com>
The salvaged guard covered only the save path. Validate, activate and delete completions
from a profile-A instance could still set state, fire onConfigSaved / onMainModelChanged
and toast after SettingsView remounted the panel under profile B — the identical stale-
completion race, three more doors. The same `mounted` ref now gates all four handlers
and `refresh()`.
Test: the salvaged vitest file now partially mocks `@/hermes` (the profile-scope note
subscribes to `setApiRequestProfile` at import time) and stubs `ActiveProfileNote`.
Closes#69451 (renderer `profileScoped()` forwarding, backend `_config_profile_scope`
and the profile-keyed remount landed on main earlier; this closes the last atom).
Salvage of #69452.
`hermes dashboard` from a named profile re-execs as `-p default dashboard --open-profile P`,
but `--open-profile` only reached the one URL `_maybe_open_browser()` opened. A `/chat?resume=<id>`
deep link without `?profile=` initialised `ProfileProvider` with the empty (launch) scope, so the
embedded chat ran in the default profile — no MCP servers, wrong model/skills — while the switcher
showed P. No error, no indication.
The server now records `initial_profile` on `app.state` and injects it into the SPA bootstrap as
`window.__HERMES_INITIAL_PROFILE__` (escaped for the script context); the Vite dev proxy forwards
it. `ProfileProvider` uses it only when the URL carries no `profile` param: an explicit
`?profile=` (including an explicit empty one) still wins, and the sticky-active-profile alignment
no longer replaces a launch-preselected scope.
Closes#73085. Salvage of #73260 (cherry-pick of 99e341cdef0 resolved onto the split
web_server_dashboard.py; start_server assertion dropped as a change-detector).
Desktop app-global remote mode and the Ink TUI send `config.set` with `session_id` and no
`profile` param. `@_profile_scoped` only bound `params["profile"]`, so a write from a focused
worker session round-tripped `_load_cfg_raw()`/`_save_cfg()` against the LAUNCH profile's
config.yaml while the worker profile kept its old value (and reverted on restart).
The decorator now falls back to the named session's `profile_home` when `profile` is absent,
binding the full runtime scope (home + secrets + terminal) the same way an explicit profile
does. `config.set cwd` from a profile-bound session no longer publishes `TERMINAL_CWD` into
the shared process env (that variable belongs to the launch profile).
Closes#85669. Supersedes #85676 (its diff predates the methods_* split).
Co-authored-by: worlldz <cryptoworlldz@gmail.com>
A `hermes -p B` child built from a process that loaded profile A's env (a gateway, the
dashboard, the post-update fleet restart) started with A's `DISCORD_ALLOWED_CHANNELS`,
`TELEGRAM_GROUP_ALLOWED_CHATS`, `GATEWAY_ALLOW_ALL_USERS`... and enforced them as its
own: gates are not credentials (no secret scrub sees them), a unit-file `Environment=`
or operator export is in no dotenv (no name-list strip sees them), and B's own `.env`
rarely defines the key (its dotenv load never overwrites the inherited value). Observed
as profile B's gateway rejecting every message in B's own channel after a per-profile
restart issued from A (#113270).
- `local_env_policy.is_profile_gate_env` / `strip_profile_gate_env`: gates matched by
shape (`_ALLOWED_`, `_ALLOW_ALL_`, `_ALLOW_FROM`, `_ALLOW_BOTS`, `_IGNORED_CHANNELS`,
...), never `HERMES_*`, so a gate added to any adapter is covered without a second edit.
- `strip_launch_profile_env` drops them on its existing routed-home branch: the seam
`served_profile_child_env`, the kanban dispatcher, cron workers and the dashboard
action env already funnel through (the dashboard site now calls it for its target).
Same-home children keep an operator export.
- `update_restart_recovery._child_environment(profile)`: the one site that bypassed
every helper (bare `os.environ.copy()` relaunching EVERY profile) strips gates when
the profile is not the one the updater runs as; the module stays stdlib-only at
import time.
Live: fresh-process `hermes_cli.update_restart_recovery --stdin` with three gates in
the updater env — base hands all three to profile b's relaunch, fixed hands none and
keeps them for the launch profile.
Refs #113270; supersedes #113308 (@yashraj4, static key list + always-strip; this keeps
same-profile children intact and covers the per-adapter gate set).
Gate review: tcp_site.py grew its own `_WILDCARD_HOSTS` that already diverged
from shared_ingress.py (dropped "*" and ""). One `is_wildcard_host` in
shared_ingress, used by both the TIME_WAIT rebind guard and
`listener_base_url`.
The positive case (bind over a lingering server-side TIME_WAIT socket) lives
once, on the api_server adapter, since both adapters share start_tcp_site; the
webhook file keeps the negative case (a live listener still wins). That test
hung: the blocker never closed the probe connection server-side, so
`wait_closed()` waited forever.
On macOS, an exclusive bind (SO_REUSEADDR disabled) refuses a port that is
still held only by a server-side TIME_WAIT socket for 2*MSL (~30s) after a
previous connection close — the gateway's own shutdown, or any
``Connection: close`` request. A ``/restart`` issued within that window
failed to bind, even though nobody was actually listening.
For an explicit host, a refused connect probe proves nobody is listening:
the kernel still rejects an exact duplicate bind even with SO_REUSEADDR, and
a foreign wildcard listener would answer the probe. So one retry with
SO_REUSEADDR is safe in that case. A wildcard host keeps the strict
exclusive path, since a foreign listener on a non-loopback interface can't
be probed this way. A live listener still wins either way.
The bind, probe, and retry logic is shared by webhook.py and api_server.py
through the new gateway/platforms/tcp_site.py, since both adapters had the
same exposure — api_server additionally marked a spurious TIME_WAIT bind
failure as a non-retryable port conflict.
(cherry picked from commit 289fd06148c447829f1e38b66ff78b9e4bebdeb4)
_send_file hit the identical stale -2 after its own tokenless re-send and still
raised the generic "sendmessage error"; it now raises the shared
_session_not_ready_error. The genuine rate-limit cooldown message carries
ret/errcode/errmsg so operators can tell a real -2 frequency limit from the
session case. One parametrised test (stored token / no token) asserts the send
fails once, classify_send_error does not call it rate_limited, and the breaker
stays closed; troubleshooting row in the Weixin docs.
iLink answers a bot-initiated send with ret=-2 errmsg="prepare failed" when the
peer's session is not ready (no inbound message yet, or the context token is
gone). That is deterministic: treating it as a frequency limit spent the retry
budget on an error that never clears and opened the 30 s rate-limit breaker for
the whole account, blocking every later send (#80125, #112709).
After the tokenless re-send (46ab37ce35) has been tried — or when there is no
token to drop — a stale-session -2 now fails fast with a descriptive
"session not ready" error and never reaches the rate-limit path. The text
avoids the "rate limit" substring classify_send_error keys on.
Salvage of #80152 (fail fast + keep the breaker closed); #80156 arrived five
minutes later with the same change.
Co-authored-by: RelaxJonh <92573950+RelaxJonh@users.noreply.github.com>
Groups: policy, group-JID allowlist, and that participants are still authorised by
the gateway sender allowlist or pairing (`open` alone admits nobody without one);
`require_mention` defaults to false; WHATSAPP_GROUP_POLICY / WHATSAPP_GROUP_ALLOWED_USERS
rows in the environment reference. The alt-id node tests collapse to one (the
participantAlt case duplicated the first-contact case; the "still resolves via mapping
files" case only re-asserted matchesAllowedUser).
The bridge now gates groups by WHATSAPP_GROUP_POLICY / WHATSAPP_GROUP_ALLOWED_USERS
(#73465), but only the DM policy and DM allowlist were exported from the adapter's
resolved config — a YAML group_allow_from never reached the bridge and, under a
multiplexed gateway, the launch process's WHATSAPP_GROUP_* values did. _bridge_env
exports both like it already did for the DM pair, so unlisted-group traffic is cut
before media download instead of after the POST to Python.
whatsapp_common._select_allowlist generalises the DM precedence reader (config key
presence wins, an explicit empty list stays authoritative, then the first truthy env
carrier); the adapter's group list and the Cloud sibling's group list use it instead
of hand-rolled `or` chains, so `group_allow_from: []` means the same everywhere.
Tests: bridge env carries group policy + group allowlist from the secondary profile's
YAML (scoped precedence file); env fallback test trimmed to the two invariants (red on
the merge base); bare test adapters seed _group_policy/_group_allow_from, which
_bridge_env now reads.
The Node bridge receives WHATSAPP_GROUP_ALLOWED_USERS via
_BRIDGE_PASSTHROUGH_ENV and the YAML bridge maps config.yaml
whatsapp.group_allow_from onto the same env carrier, but
WhatsAppAdapter.__init__ seeded _group_allow_from from config.extra
alone — an install that set the group allowlist only through the
documented env var silently ran group gating with an empty allowlist:
with group_policy=allowlist every group chat was rejected at intake
(the second failure mode triaged on #72529, the half with 'no PR yet').
Mirror the DM path's precedence: config keys win by key presence
(explicit empty list stays authoritative), then the profile-scoped
WHATSAPP_GROUP_ALLOW_FROM / WHATSAPP_GROUP_ALLOWED_USERS env CSV —
multiplex-safe through _wenv, like every other WHATSAPP_* read.
Tests pin the full precedence matrix: env-only seeding, the
allow_from-style alias, config-wins-over-env, the legacy groupAllowFrom
key, empty-by-default, the scoped .env read under multiplex, and the
group-policy gating outcome for an env-seeded allowlist.
Use the configured group policy and group-JID allowlist at Node bridge intake instead of applying the DM sender allowlist to group participants.
Co-authored-by: Martin Gontovnikas <m@gon.to>
The hand-rolled CREATE/INSERT/DROP/RENAME duplicated SessionSchemaMixin._rebuild_table,
which already runs inside a caller-owned transaction and has its own atomicity test.
The stranded {table}_new drop stays: older builds crashed with that scratch name.
_execute_write probes the DB generation before running the callback and raises
StateDbReplacedError (a RuntimeError, not sqlite3.Error); _read_ctx never does, so
the heal must not let it escape a read path that promises the empty value.
Atomicity probe now denies the copy into the fresh table: a non-transactional
rebuild leaves an empty v3 table the retry skips (verified red with executescript).
Denying every DROP TABLE on the topic tables now trips on the new
'DROP TABLE IF EXISTS {table}_new' before anything is written, so the probe
passed even with an executescript rebuild. Deny the RENAME of the scratch
table instead: the legacy table is already dropped at that point, and only a
transactional rebuild still has its rows on retry (verified red with
executescript swapped back in).