Commit Graph

2728 Commits

Author SHA1 Message Date
kshitijk4poor
343500b354 refactor(web): drop the gate's leftovers and keep the tier rules tested
Review and simplify pass on the guest fast-search restore.

- tools/tool_backend_helpers.py: _account_info was split out so two gates
  could share one account read. Only managed_nous_tools_enabled is left, so
  fold it back into its pre-salvage shape. Keep the branched force_fresh
  call, because tests stub the reader with zero-arg lambdas.
- plugins/web/perplexity/provider.py: the managed= parameter on
  _managed_gateway only fed the removed refusal helper. search() now calls
  resolve_free_search_gateway() directly once it has the route answer. The
  module docstring now names guests.
- hermes_cli/nous_account.py: the _error_info docstring described the
  removed gate. The tier carried on a failed lookup still picks the "needs
  a Nous account" copy over billing or re-login guidance.
- Tests: the case-insensitive tier match and the tier on error snapshots
  lost their only coverage when the gate tests went, so cover them at
  their consumer, format_nous_portal_entitlement_message. The fast-search
  premise now asserts that the guest case really reads as the anonymous
  tier. Remove the dead exp_offset parameter.
- Docs: the Perplexity section said there is no anonymous tier, which is
  true for direct keys only. It now names the managed guest route.
2026-10-04 00:10:54 +05:30
kshitijk4poor
96ac3d349f fix(web): restore free fast search for anonymous guest users
The free managed Perplexity fast-search route exists for anonymous guest
Portal users. The salvage of #130121 (cee3000c24, via #130660) added a
registered-account gate, fast_search_entitled(), that excluded the
anonymous tier. That defeated the original change: guests fell back to
the keyless ring and never sent a gateway request.

This removes the gate, so resolve_free_search_gateway() decides on
identity alone again, as rewbs wrote it. The guest-disabled, expiry
refresh and dead-guest rules still apply through the token reader. Every
other vendor, and the billed managed-Firecrawl fallback, keep the paid
entitlement gate.

Kept from the salvage: the case-insensitive is_anonymous_tier, the
account tier carried on error snapshots, and the single route walk per
search dispatch. Tests: the anonymous identity is back in the autodetect
and rescue cases. The tests that pinned the guest exclusion are removed.
2026-10-04 00:10:54 +05:30
teknium1
33eedf29c9 docs(plugins): pin plugins.isolation in the managed scope for shared deployments
plugins.isolation is profile config, so a profile (or its agent) can set it
back to in_process. The managed scope already beats profile config and makes
`hermes config set` refuse, and the isolation reader goes through that
overlay; document it as the operator lock and pin the contract with a test
(red when isolation_mode() reads the raw profile file).
2026-10-03 10:15:11 -07:00
teknium1
ad5c8e336e fix: plugin host review findings (async ctx, caller session, restarts, per-profile dashboard)
Independent review of the plugin host found eight defects; each is fixed here and
the reproducible ones are covered by two new invariant tests (red on the previous
head, green now).

- Async plugin code: `await ctx.<facade>.<async method>` works (the facade probe
  reports coroutine methods and the host returns a coroutine function), and ctx
  calls made from coroutines keep the caller's session. The wire origin is now a
  ContextVar carried onto host event-loop tasks, and the host's fallback context
  starts empty instead of copying whichever caller created the host.
- Memory / context-engine / cron providers survive a host crash: proxies record
  the host generation, and a category proxy reloads its plugin into the new host
  on next use (bounded by the same restart budget). A proxy from an earlier host
  can no longer address a recycled object id.
- Hosted provider objects no longer leak: the host imports a category plugin's
  module once and drops objects whose Hermes proxies were collected.
- Every public attribute of a hosted object is read live (class-level defaults
  included), and attributes Hermes assigns reach the plugin's object unless they
  hold a live handle.
- A user memory provider's config_schema.py runs in the plugin host.
- Dashboard plugin APIs resolve the plugin in the requesting profile (its copy,
  its plugins.enabled, its host) and keep repeated headers such as Set-Cookie.
- platform_actions coroutines run on the gateway loop their adapters use.
- Each incoming request gets its own thread (no pool exhaustion under nested
  callbacks); a late close from a replaced host no longer triggers a reload; the
  model-provider extraction cache is keyed by full path.
2026-10-03 10:15:11 -07:00
teknium1
17a7a9cb6b docs(plugins): plugin host isolation mode and its invariants 2026-10-03 10:15:11 -07:00
teknium1
5fa55d1678 feat(plugins): run third-party plugins in a per-profile plugin host (plugins.isolation: host)
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.
2026-10-03 10:15:11 -07:00
kshitijk4poor
db45b44ab7 refactor(email): derive preflight fields from the registry, share payload unwrap
The HEADER.FIELDS list hand-copied the _AUTOMATED_HEADERS keys, so a key
added there would silently miss the preflight and the sender's full body
would be fetched before being dropped. Build it from the registry (the
IMAP command is byte-identical today). The preflight also repeated the
RFC822 payload unwrap and had three identical warn/consume/continue
branches: one _imap_payload helper now serves both unwraps and the
rejections collapse to one branch (gate reuse + quality Low, convergent).
2026-10-03 20:54:04 +05:30
kshitijk4poor
e9a385c603 refactor(email): require the pre-fetch authorization gate
preauthorize=None kept an unsafe path on the shipped adapter: any caller
that left the gate out still RFC822-fetched unauthorized mail. The only
production caller (_check_inbox) always passes it; the None default served
only direct test calls (gate quality Medium + 2c Low, convergent). Make it
required, drop the guard, and have those tests pass an allow-all gate
(their IMAP mocks answer the BODY.PEEK preflight where needed).
2026-10-03 20:54:04 +05:30
kshitijk4poor
7e6762b6fd refactor(email): schedule the pre-fetch gate via safe_schedule_threadsafe
The gate hand-rolled run_coroutine_threadsafe(...).result(). On a closed
loop that raised and leaked a never-awaited coroutine (gate reuse Low
finding). The shared helper closes the coroutine and returns None, which
the gate now treats as a rejection, so it still fails closed.
2026-10-03 20:54:04 +05:30
kshitijk4poor
4fb207491e refactor(email): annotate _message_metadata with email_lib.message.Message
Every sibling signature in the adapter spells the type through the
existing email_lib import; the bare Message import served one annotation
(gate quality Low finding).
2026-10-03 20:54:04 +05:30
FroRaut
81706c8b30 fix(email): authorize before fetching MIME bodies
The adapter fetched the full RFC822 payload (and decoded attachments) for
every unseen UID before deciding whether the sender was allowed, and
_verify_sender_authentication searched every Authentication-Results field
for one whose authserv-id matched (or was a subdomain of) the pin. A
forged lower field or an attacker-chosen child.<authserv_id> therefore
authenticated a spoofed From:.

- Only the topmost Authentication-Results field is authoritative; a set
  authserv_id must match it exactly (RFC 8601 CFWS comments allowed via
  _ar_clauses). Main's _NO_AUTH_RESULTS_REASON/_UNTRUSTED_AUTHSERV_REASON
  constants are kept so _DROP_HINTS still resolves.
- _check_inbox fetches a bounded BODY.PEEK[HEADER.FIELDS ...] preflight
  and runs _sender_accepted on the event loop before requesting RFC822;
  rejected UIDs are marked \Seen without downloading the body.

Salvaged from #129772 and rebuilt on main: the PR skipped the preflight
under open access via self._open_access(), which main removed in
c2f595d6af (open access now also authenticates From:), so the gate always
runs here. Poll-loop test mocks now authorize their unauthenticated test
senders instead of forcing open access.
2026-10-03 20:54:04 +05:30
kshitijk4poor
c03cca2763 refactor(telegram): parse max_concurrent_updates with coerce_port
Gate BR3 r1 reuse (Low): build_update_processor hand-rolled the int-or-
default parse that gateway.platforms._shared.coerce_port already does.
Neither caught OverflowError, so `max_concurrent_updates: .inf` in YAML
aborted connect(). coerce_port now treats non-finite values as unparseable
(as gateway.config._coerce_num already does), which also hardens its other
port/int callers; the telegram setting warns and falls back to 32.
2026-10-03 20:50:15 +05:30
kshitijk4poor
03395f9de7 refactor(telegram): build PerChatUpdateProcessor on SimpleUpdateProcessor
Gate BR3 r1 quality (Low): PTB's SimpleUpdateProcessor already provides the
no-op initialize/shutdown the class copied, and config.extra is always a
dict, so the `(extra or {})` guard was dead. The wiring test only compared
PTB's concurrent_updates property with itself (true for any processor);
assert the PerChatUpdateProcessor type and DEFAULT_MAX_CONCURRENT_UPDATES
instead - now red when a plain SimpleUpdateProcessor(32) is wired in.
2026-10-03 20:50:15 +05:30
kshitijk4poor
ff90457267 fix(telegram): keep per-chat chain intact when a waiting update is cancelled
PerChatUpdateProcessor awaited the predecessor's tail future directly, so
cancelling a waiting same-chat update also cancelled that future (gate
BR3 r1 2c + quality, Medium). The running predecessor then raised
InvalidStateError from done.set_result() after its handler succeeded, and
the next waiter started while the predecessor still ran (probe order:
start A, start C). Shield the await and, for a cancelled waiter, release
its turn only once its predecessor finishes.
2026-10-03 20:50:15 +05:30
kshitijk4poor
a6d7e08fd1 fix(telegram): keep same-chat updates in order under concurrent dispatch
Plain PTB concurrency (concurrent_updates=32) unblocks other chats while one
chat's handler is slow, but also lets that chat's NEXT update dispatch and
deliver before the first one finishes (download/vision/callback work happens
inline, before text batching can merge them).

PerChatUpdateProcessor (update_admission.py, not the 7k-line adapter) chains
each update behind the previous update of the same chat while keeping the
bounded cross-chat pool. The builder keeps the instance, so the connect-retry
rebuild in _initialize_app_with_retries reuses it. An invalid
max_concurrent_updates now logs a warning instead of silently falling back
(as #74652 did), and the dispatch-stall watchdog comment no longer claims PTB
dispatches sequentially.

Co-authored-by: fortona-space <299084105+fortona-space@users.noreply.github.com>
2026-10-03 20:50:15 +05:30
Yuan Li
5907983b96 fix(telegram): enable concurrent update processing (PTB default serializes all updates)
(cherry picked from commit 443c8b585ae1672e30c66e605b62ba174b3a9162)
2026-10-03 20:50:15 +05:30
kshitijk4poor
e2501d72ef refactor(telegram): raise plain _PollingStallError for ingress dispatch stall
Gate finding (quality, Low): _IngressDispatchStallError had no consumer -
every branch checks isinstance(error, _PollingStallError) - so the subclass
and its docstring were dead weight. One stall was also announced three times
(the "healthy but deaf" pre-log, the handoff warning, the fatal error line);
the sibling _check_polling_stall deliberately skips a pre-log. Drop the
subclass, drop the pre-log and carry the "healthy but deaf" counts in the
error text instead, and read the polling generation once.

Tests assert on the error handed to the stubbed handoff rather than caplog.
2026-10-03 18:40:38 +05:30
kshitijk4poor
1809f4fec3 fix(telegram): size ingress dispatch stall budget past a bounded slow handler
Gate finding (2c, Low): PTB dispatches updates sequentially (no
concurrent_updates) and counts a dispatch when the handler starts, so one
slow-but-bounded handler (sticker vision_analyze at 120s+, an awaited
slash-confirm callback) plus a queued update looked like "no dispatch
progress" after two 90s heartbeats and triggered a fatal rebuild that drops
already-acknowledged updates.

Raise _INGRESS_DISPATCH_STALL_HEARTBEATS to 4 (360s) so the window exceeds
_POLLING_ERROR_TASK_STUCK_TIMEOUT (300s) with margin. Deliberately not gated
on _inflight_update_ids: that would hide a handler that is itself wedged.
Tests drive heartbeats by count; counts updated, semantics unchanged, plus a
270s-no-escalation assertion.
2026-10-03 18:40:38 +05:30
kshitijk4poor
1a51cedc0b fix(telegram): name the actual stall in the polling-stall fatal message
The confirmed-stall fatal branch hardcoded "getUpdates made no progress",
which is wrong for _IngressDispatchStallError: there getUpdates IS
progressing and PTB's dispatcher is the wedged part. Operators reading
gateway_state.json / logs were pointed at the transport. Interpolate the
(redacted) error text instead; the existing polling-stall errors already
say "getUpdates made no progress ...", so their message is unchanged in
substance.
2026-10-03 18:40:38 +05:30
kshitijk4poor
3d3fdbd170 refactor(telegram): drop no-loop fallback from ingress dispatch stall watchdog
_check_ingress_dispatch_stall is only called from the async polling
heartbeat, where a running loop always exists; the try/except RuntimeError
and the debounce reset that made its deferred retry work existed only for a
sync unit-test call. Match the sibling _check_polling_stall (no guard) and
delete the no-loop test. Without the fallback the counter stays at the
threshold until dispatch progresses, so a stall is escalated once.
2026-10-03 18:40:38 +05:30
JerryLiu369
96efe44c1c fix(telegram): unblock ingress stall recovery retry after no-loop fallback
(cherry picked from commit 589c8c06be30b92450eee3023200480d0d43cbb1)
2026-10-03 18:40:38 +05:30
JerryLiu369
3671e521f2 fix(telegram): escalate ingress dispatch stall to supervisor rebuild
The ingress dispatch watchdog logged 'healthy but deaf' but never drove
recovery, leaving the gateway reporting connected while PTB's dispatcher
stopped draining its queue (#130407 §1).

Hand the stalled adapter to the supervisor for a rebuild via a new
_IngressDispatchStallError (_PollingStallError subclass, so the ladder
skips the in-place Updater restart that keeps the wedged dispatcher and
goes retryable-fatal). _schedule_polling_recovery also marks the adapter
degraded so gateway_state.json stops saying connected. Guarded by the
recovery-in-flight check like the polling stall watchdog; no-loop direct
calls keep the warning as the diagnostic.

Tests: new test_telegram_ingress_dispatch_recovery.py (8 tests: debounce,
scheduling, degraded+fatal handoff, in-flight/webhook/teardown guards,
re-arm, no-loop fallback); existing delivery-gap counting tests stub
recovery to isolate the log contract.

(cherry picked from commit 7767ed38e4f4508b67fc4e2bf3dc726095243732)
2026-10-03 18:40:38 +05:30
kshitijk4poor
eba3414727 refactor(discord): derive the resolved-ID count and accessor from the set
Quality-gate Lows (BS03 r1): resolved_count always equalled
len(self._username_resolved_ids) (each name is discarded from to_resolve on
its first match, so it adds exactly one ID), and resolved_allowlist_user_ids
filtered isdigit()/"*" out of a set that only ever holds str(member.id) and is
always initialised in __init__ and reset by the resolver. Drop the counter, the
filter, the getattr fallback (no test builds the adapter without the resolver
and then calls the accessor) and the stale "Numeric only" docstring line.
Behaviour unchanged (probe-BS03-1 output identical before and after).
2026-10-03 16:37:12 +05:30
kshitijk4poor
d13d92cc82 fix(discord): reset username-resolved IDs before the empty-allowlist guard
_resolve_allowed_usernames reset _username_resolved_ids only after the
early return for an empty allowlist / missing client, so a reconnect
after the allowlist was emptied kept the previous connect's resolved IDs
in the set the gateway authz union reads. Reset first so the set always
reflects the current connect.
2026-10-03 16:37:12 +05:30
EloquentBrush0x
462389ea5e fix(discord): a numeric allowlist entry removed after connect stops authorizing
resolved_allowlist_user_ids() returned every numeric ID in the adapter's
connect-time _allowed_user_ids, and the gateway authz layer unions that set
into the reloaded DISCORD_ALLOWED_USERS. So an ID removed from the allowlist
after connect (hermes pairing revoke from the CLI or dashboard, or a hand
edit) kept authorizing until the gateway restarted. #130920's in-place purge
only reaches adapters in the same process, and those commands run outside it.

Track the IDs _resolve_allowed_usernames actually resolved from username
entries and return only those: they are what the per-turn .env reload loses
(a0a63a1bc2); numeric entries are already read live from the env.

(cherry picked from commit 90451505545745dbb27ecad28f2c431ad5afd301)
2026-10-03 16:37:12 +05:30
EloquentBrush0x
675bad762a fix(discord): resolve allowlisted usernames by username only, not display name or nickname
_resolve_allowed_usernames resolved a DISCORD_ALLOWED_USERS name through member.name,
member.display_name or member.global_name, and the first match took the slot. Any member of a
shared server could set a nickname or display name to an allowlisted username, get the
resolved ID (DMs, tools, approval buttons) and lock the real user out.

Match the unique account username only, as #125034 did for SimpleX display names. An entry
that matches only a display name stays unresolved and gets a connect-time hint. Tests cover
a copied nickname and a copied display name seen before the real user, and the hint; the
Discord docs now say display names and nicknames are never matched.

(cherry picked from commit 252f0bccab39afe94f0f71254e47a8f785ec1fb0)
2026-10-03 16:37:12 +05:30
kshitijk4poor
0089fca37d fix(slack): gated slash commands get a bare ack, not "Running /x"
The channel gate ran inside _handle_slash_command, after the bolt listener had
already acked with an ephemeral "Running /x". In an ignored or non-allowed
channel the invoker was promised a run that never came, and an ignored channel
("never touched") still got a bot reply. Move both checks into
_slash_channel_gated() and consult it before the ack: gated -> bare ack().
_handle_slash_command keeps calling the same predicate for direct callers.
The listener becomes the _handle_hermes_command method so the existing
parametrized gate test drives the real ack path (BN11 gate r1 2c Low finding).
2026-10-03 16:36:48 +05:30
kshitijk4poor
46dd3161bb refactor(slack): classify the 1:1 DM once in _handle_slash_command
The allowed_channels guard recomputed str(channel_id).startswith("D") inline and
the next line computed it again as is_dm. Hoist is_dm above the gates and reuse it.
Same behaviour, one DM classification (BN11 gate r1 quality/reuse Low finding).
2026-10-03 16:36:48 +05:30
John Paul Soliva
b376591778 fix(slack): slash commands obey allowed_channels and ignored_channels, like messages
The message path drops a message from an ignored channel (_prefilter_inbound)
and, when allowed_channels is set, from any non-1:1 conversation outside it
(_channel_gate_allows). _handle_slash_command applied neither, so a slash
command ran in a channel the operator had excluded. Apply both gates by
channel id alone, before any lookup, so a failed conversations.info can never
drop a control command.

Rebuilt from 2b7cde506c (#128767). Fixes #130358.
2026-10-03 16:36:48 +05:30
kshitijk4poor
05fdbf6317 refactor(matrix): import markdown once in the standalone sender, drop the None sentinel
The standalone payload builder imported markdown inside the per-chunk loop
under suppress(ImportError), which read as though the import could fail per
chunk. Try it once before the loop and branch on the result.

_standalone_send seeded `result = None` and returned it after the loop,
suggesting a possible None return. truncate_message always yields at least one
chunk, so the loop always runs; return the failing result directly and the
last result otherwise. No behaviour change.
2026-10-03 16:31:35 +05:30
Froraut
b015e59fb9 fix(matrix): address review — budget edit mentions and move plugin tests
An edit carries m.mentions twice: once inside m.new_content and once at the
top level. When a budget-sized edit names many distinct Matrix IDs, that
fixed part alone could pass the content budget. Shortening the fallback body
then had no room to work with, and the edit still went out at about 54 KB,
which an encrypted room would reject with M_TOO_LARGE.

When the edit is still over budget with an empty fallback, it now drops the
top-level mentions copy. That copy only drives the edit's own notifications,
and m.new_content keeps the full list.

The event-size tests move to tests/plugins/platforms/matrix/test_event_size.py,
mirroring the plugin source tree, and gain a mention-heavy text. The
tests/gateway/test_matrix_message_length.py file is back to main apart from the
env-override value that the new ceiling requires.

(cherry picked from commit 4b06bcb7dbffb732bef6e877cb66dea2fbff8c73)
2026-10-03 16:31:35 +05:30
Froraut
d584836ffa fix(matrix): keep every outbound event under the homeserver's byte cap
The homeserver rejects any event whose canonical JSON is over 65,536 bytes
(M_TOO_LARGE), but Matrix chunked outbound text on characters, 16,000 by
default with a 65,535 ceiling. Each message carries its text twice (body plus
formatted_body HTML), and an edit carries it four times (the "* " fallback
plus m.new_content).

On the default config this caused three failures:

- A formatted chunk of CJK text came to about 95 KB and was rejected, so
  _send_with_retry fell back to a plain 3,500-char truncation.
- A streamed edit near the limit came to about 80 KB even for English, so
  edits failed mid-stream.
- The standalone sender (cron without a running gateway) posted each
  16,000-char chunk as one event, and hit the same failure on non-Latin text.

The fix measures Matrix text in UTF-8 bytes, the unit of the cap:

- The adapter's message_len_fn now returns UTF-8 bytes, so send() and the
  stream consumer's split and progress-edit budgets count the same unit.
- A chunk gets a third of a 45,000-byte content budget, which leaves room for
  the event envelope and E2EE's 4/3 base64 inflation. That makes 15,000 the
  default and the ceiling for max_message_length, so English chunks stay
  about the same size.
- _build_text_message_content drops the HTML when escape-heavy text would
  push the content past the budget.
- An edit that would still be too large keeps its full m.new_content and
  shortens only the top-level fallback, which edit-aware clients never
  render.
- The standalone sender re-chunks on UTF-8 bytes and applies the same HTML
  guard.

(cherry picked from commit cab00c94175955a68ecd4d38c5748d63148ca6be)
2026-10-03 16:31:35 +05:30
kshitijk4poor
8f54cbdce5 refactor(email): compute the allowlist match once and drop a stale reason comment 2026-10-03 16:30:02 +05:30
kshitijk4poor
c4343445c4 fix(email): warn when a listed sender's mail fails authentication
r2 review: moving explicit failing verdicts to debug hid a listed contact whose
own domain breaks DKIM or whose forwarder breaks alignment. Those drops warn
again, with the verdict and no opt-out hint; forged stranger mail under open
access stays at debug. The two operator hints now live in one reason->hint
table behind a single warning call.
2026-10-03 16:30:02 +05:30
kshitijk4poor
a4dadad7cd fix(email): warn when a mis-pinned authserv_id drops an admitted sender
The previous commit moved every non-missing-header auth failure to debug,
which made a mis-pinned authserv_id (the only stamp comes from another
authserv-id) drop allowlisted mail silently at default log levels. That is
an operator misconfiguration, not routine forgery, so it warns again, with
a hint pointing at platforms.email.authserv_id instead of the opt-out.
2026-10-03 16:30:02 +05:30
kshitijk4poor
4c982e9ca4 fix(email): warn with the opt-out hint only when Authentication-Results is missing
Under open access every stranger is granted, so each forged or DMARC-failing
message logged a WARNING telling the operator to disable sender
authentication. Keep that warning (and hint) for the one verdict that points
at a server that never stamps the header; log every other unauthenticated
drop at debug, including an explicit failing verdict for an allowlisted
address. The reason string is lifted to _NO_AUTH_RESULTS_REASON so the
parser and the check share it.
2026-10-03 16:30:02 +05:30
dborosev
c2f595d6af fix: authenticate senders in public email mode 2026-10-03 16:30:02 +05:30
Brooklyn Nicholson
c82abc309a fix(telegram): widen fence anchors to keep mid-line and indented blocks
The anchored fence regex from the upstream commits (kept here via
cherry-pick) fixed #85381 but required the opening ``` to sit at a
line start with <=3 spaces of indent, silently downgrading two shapes
that main renders as <pre> into escaped literal prose:

- mid-line-opened fences ("Here is the code: ```python")
- fences indented by 4+ spaces (list/blockquote-nested code)

Widen the anchors while keeping the inline-triple fix:

- the opening line may now carry any leading whitespace or lead-in
  prose, as long as the opening ``` is the first triple-backtick run
  on the line and the rest of the line is backtick-free (so an inline
  pair like ```x``` can never end a line and open a fence);
- the closing fence may sit at any indent, not just 0-3 spaces.

The tempered prefix cannot skip past an earlier triple-backtick run,
so an inline pair followed later by a real fence no longer swallows
it (both main and the anchored regex mangled that mix in different
ways). The scar conformance vector reverts to main's committed value:
the mid-line-opened fence it encodes is protected again.
2026-10-02 22:25:39 -05:00
PRATHAMESH75
3480de0402 fix(telegram): match CRLF-terminated closing fences
The anchored fenced-code regex ended the closing fence with [ \t]*$, so a
close line terminated by CRLF (\r\n) — Windows-authored content — failed to
match: the \r sits between the fence and the $ position. That left the whole
block unprotected, escaping its backticks into literal ` runs and dropping
the code formatting. Allow an optional trailing \r before the line end.

Adds a CRLF-fence regression (fails on the old anchor).
2026-10-02 22:25:39 -05:00
PRATHAMESH75
5f7b1e2d65 fix(telegram): anchor fenced-code regex so inline triple backticks aren't mangled
The MarkdownV2 fenced-code protection regex
r'(```(?:[^\n]*\n)?[\s\S]*?```)' made the newline after the opening
fence optional, so it also matched *inline* triple-backtick spans on a
single line (e.g. "the syntax is ```x``` inline"). _protect_fenced
then treated the inline span as a pre block, escaping backticks into an
unbalanced entity; Telegram rejected the whole message with 'can't find
end of pre entity' and Hermes fell back to plain text, dropping all rich
formatting.

Anchor the pattern to line starts (0-3 spaces indent) so only standalone
fences match, and rewrite _protect_fenced to use explicit capture groups
(opening line / body / closing fence) instead of naive [:-3] slicing,
which also fixes a doubled closing fence when the close line had trailing
whitespace.

Fixes #85381
2026-10-02 22:25:39 -05:00
Benjamin Brumbaugh
d5cda9135c fix(disk-cleanup): fail closed on unresolved track paths 2026-10-02 21:21:36 -05:00
Benjamin Brumbaugh
cc2dd7016e test: harden disk cleanup path safety 2026-10-02 21:21:36 -05:00
Benjamin Brumbaugh
0ba4992c52 fix(disk-cleanup): accept macOS temp aliases 2026-10-02 21:21:36 -05:00
Brooklyn Nicholson
a510d6c64a fix(web/firecrawl): accept the server timeout in the keyless scrape client
#131868 passes timeout=60_000 to the Firecrawl client's scrape() so the
server-side deadline matches our 60 s wait. The SDK accepts it, but the
keyless REST client's scrape() took only url/formats, so every extract
raised TypeError for keyless users with the keyless ring disabled or the
tier pinned to paid. Accept the kwarg and forward it in the v2 payload,
which takes the same field.
2026-10-02 21:12:48 -05:00
Tranquil-Flow
eaecc99c7e fix(discord): upsert recreated slash commands without a delete-first step (#104399)
A 429 between the recreated path's delete and upsert strands the command
deleted until a fully-clean later sync; on reconnect-heavy deployments
that never happens, so commands like /model silently vanish from the
slash picker. Discord's create endpoint overwrites same-name commands,
so the lone upsert recreates while keeping the command available.
2026-10-02 20:31:33 -05:00
liuhao1024
e0daebeb6b fix(web/firecrawl): pass timeout to scrape API to match asyncio deadline
The Firecrawl scrape() call was missing the `timeout` parameter, so the
server-side used its 30s default.  The client-side asyncio.wait_for
expects 60s, causing SCRAPE_TIMEOUT on slow pages before the Python
deadline expires.

Fix: pass timeout=60000 (ms) to scrape() so both deadlines align.

Fixes #43272
2026-10-02 20:24:48 -05:00
worlldz
2f78707a79 fix(web): fail over keyless auth rejections 2026-10-02 18:58:06 -05:00
teknium1
87be434bff chore(memory): remove the bundled supermemory provider (moved to the plugin catalog)
plugin-catalog/supermemory.yaml already points at supermemoryai/hermes-supermemory,
the same provider maintained by Supermemory. The catalog entry stays dormant while a
bundled copy exists (bundled providers win in find_provider_dir), so deleting the
in-tree copy is what lets hermes_cli/memory_provider_migration.py install it for
every home whose memory.provider is "supermemory".

retaindb, byterover, holographic, mem0, openviking and honcho stay bundled.
Prose that lists the bundled set (CONTRIBUTING en/es, plugins/AGENTS.md, the
memory.provider default comment, `hermes memory --help`) now names supermemory as
a catalog provider. The SUPERMEMORY_* env hints and redaction stay: the catalog
copy still reads them.
2026-10-02 05:10:06 -07:00
teknium1
7e53b3ef82 chore(memory): remove bundled honcho provider; install from the plugin catalog 2026-10-02 04:39:28 -07:00
teknium1
d802c47238 docs: one complete plugin catalog submission guide, synced with the README rules
Submitters only saw a stale 5-point summary on the catalog page (it even listed
a 'Released' requirement nothing enforces), while reviewers applied ten README
rules plus criteria written nowhere public (credentials, approvals, disclosure,
compatibility metadata, forks of bundled plugins).

- plugin-catalog/README.md: rules 11-15 write down those review criteria; the
  rule list sits between admission-rules markers.
- New developer-guide page 'Submitting to the plugin catalog': before you
  submit, the PR flow, the full rule block (verbatim mirror), pin updates,
  delisting vs removal, asking for a hook.
- The /plugins 'Submit' links and the user-guide section point at it.
- tests/website/test_catalog_rules_mirror.py fails when the two rule blocks
  differ; classify_changes makes a README-only edit run it.
- AGENTS.md, plugins/AGENTS.md and CONTRIBUTING.md reference both files.
2026-10-01 21:58:11 -07:00