7a99c4d13c7a779f6552d0fa0c901c4def4bc511
162 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7a99c4d13c |
fix(send): the 'not configured' error lists the home and sources it consulted
The error now names the resolved home's `.env` (and whether the platform's token key is defined there), `config.yaml` (block absent / `enabled: false` / no token) and the environment variable(s) checked, so a Windows or profile home user can fix the file this process actually read. When a gateway started from the same home already has the platform connected, the message says its token lives only in that process's environment and which key to add to `.env`. Docstrings in `gateway/channel_directory.py` and `send_cmd._load_hermes_env` stop naming `~/.hermes`; the pipe-script-output guide documents the message. |
||
|
|
59e40220e2 |
fix(send): derive hermes send path hints from the resolved Hermes home
`hermes send --help`, the `--list` empty-state hint and the "Platform 'x' is not configured" error hardcoded `~/.hermes/...`, which does not exist on a Windows install rooted at `%LOCALAPPDATA%\hermes` or under a profile home. Build the strings from `get_hermes_home()` instead. Salvaged from #114297 (the `_load_hermes_env` secret-scope rewrite and its reload-based tests are dropped; see the PR body). |
||
|
|
0f76689076 |
fix(tools): scrub lone surrogates from the outbound send body
`hermes send MESSAGE` hands surrogateescape-decoded argv straight to send_message_tool; a lone surrogate then crashes the UTF-8 marshal inside platform SDK request bodies (feishu/lark) and the message is silently lost after the retries. Every send_message caller (model tool call, `hermes send`, dashboard console) enters _handle_send, so scrub there — before media extraction, the session mirror and the platform sender see the text. Model output delivered by the gateway/cron is already scrubbed by finalize_turn / the gateway reply boundary. Fixes #113799 |
||
|
|
a9838c2100 |
fix(multiplex): tool and memory-provider env reads stay inside the routed profile
Under gateway.multiplex_profiles, os.environ holds the DEFAULT profile's .env; a
secondary profile's values exist only in the per-turn secret scope. Every reader
below still read os.environ/os.getenv at call time, so a secondary profile's turn
silently used the default profile's value.
Credentials (F6): FIRECRAWL_API_KEY (read_file hosted OCR), OPENVIKING_API_KEY,
mem0-OSS OPENAI_API_KEY, MODAL_TOKEN_ID/SECRET and BROWSER_USE_API_KEY presence
gates, and the xAI video plugin's os.getenv("XAI_API_KEY") fallback AFTER the
scoped resolver had already missed — the exact fallback-after-miss shape
gateway/AGENTS.md forbids. Deleted, not re-scoped: the resolver is the scope.
Identity / tenant (F7): MEM0_USER_ID/AGENT_ID/HOST/MODE, SUPERMEMORY_CONTAINER_TAG,
RETAINDB_PROJECT, OPENVIKING_ACCOUNT/USER/AGENT (and the whole layered() env
read), HINDSIGHT_BANK_ID/MODE/retain shaping, HERMES_HONCHO_HOST. A raw read
put a secondary profile's memories into the default profile's account/bank/
project/tenant and recalled them back into the default's turns. Each now uses
get_secret with the provider's own per-profile default on a miss.
Endpoints (F8): OPENAI_BASE_URL (aux custom runtime + direct-alias expansion),
XAI_BASE_URL/HERMES_XAI_BASE_URL (aux OAuth), NOUS_INFERENCE_BASE_URL (#65941,
both the aux builder and hermes_cli.auth_nous._nous_inference_env_override),
GATEWAY_PROXY_URL (same UnscopedSecretError-only fallback shape as
GATEWAY_PROXY_KEY three lines below), FIRECRAWL_API_URL, BROWSERBASE_BASE_URL,
SUPERMEMORY/RETAINDB/HONCHO/HINDSIGHT URLs. The keys beside them were already
scoped, so a secondary's key was sent to the default profile's proxy or host.
Targets / display (F11): WEIXIN_HOME_CHANNEL (message posted into the default's
chat), HERMES_LANGUAGE, and agent/i18n's process-wide lru_cache of
display.language — now keyed by HERMES_HOME.
Outbound webhooks: hooks.outbound[].secret_env resolved from os.environ while
the gateway registers each profile's targets inside that profile's scope, so a
secondary's deliveries were signed with the default's secret or left unsigned.
Agent-cache eviction: _spawn_release_thread started a bare threading.Thread, so
commit_memory_session -> provider on_session_end ran with an EMPTY context. The
thread now runs copy_context() and, for the unscoped housekeeping sweep, enters
the owning profile's _profile_runtime_scope resolved from the session key
(agent:<profile>:...). The pressure batch does the same per key.
session_search (#82903): agent/inline_tool_executors.py::_session_search
forwarded every schema argument except `profile`, so a gateway agent could
never select a named profile's store. Forwarded; the ownership-scoping design
in #87779/#87847 is a separate design call and is not attempted here.
Live repro (/tmp/mux_audit/fix-tool-memory-reads/repro.py): 28 FAIL on
origin/main -> 0 FAIL with this change; 10 new invariant tests red on base.
Fixes #82903
Fixes #65941
Fixes #99121
Addresses #87779
Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
Co-authored-by: Michael Versluis (Berry) <michael@wve.nl>
|
||
|
|
866332bfb5 |
fix(relay): authorize send_message targets and surface egress declines (P5) (#99220)
* fix(relay): authorize send_message targets and surface egress declines
P5 of the relay egress-authorization workstream. The relay path
authenticated the SENDER but never authorized the DESTINATION, and the
gateway compounded it from both ends.
(a) send_message could silently name an arbitrary relay target. Its
`target` parameter is free-form ('platform:chat_id'), so a model could
name ANY chat id and the gateway would emit an outbound frame for it.
gateway/relay/egress.py adds an attestation floor: a relay-routed
destination must have a provenance this gateway can show -- the
operator's home channel, the channel directory, or its own gateway
session origins. Anything else is refused HERE, with a visible tool
error naming the target, before a frame is written. Non-relay platforms
and platforms served by a live native adapter in this process are
untouched (same precedence resolve_delivery_transport applies).
(b) Connector declines were swallowed into apparent successes. The
connector's egress floor answers an unauthorized destination with a
DEFINITE failure whose text is deliberately uniform (F-005). Several
relay lanes degrade a *transport drop* by design and were degrading an
*authorization refusal* the same way:
- _send_media returned None, sending the caller into
BasePlatformAdapter's text fallback -- a DIFFERENT op re-addressed at
the very chat the connector had just refused.
- _send_prompt returned None, so exec-approval / slash-confirm /
clarify reported "relay prompt op unavailable" (a wrong reason) and
ran their numbered-text fallbacks into the refused chat.
- task_card_stop discarded the error entirely.
- typing / delete / react / thread ops degraded silently at debug.
is_egress_decline() classifies THAT a decline happened (never why --
the uniform text is not parsed for reasons) and requires a definite,
non-ambiguous failure, so a lost-ack retry is still a transport
outcome. Lanes with an error-carrying contract now report the decline
verbatim; cosmetic bool/None lanes still degrade but log it at WARNING.
Advisory progress drops that legitimately degrade are unchanged: the
task_card send lane, the draft ambiguous/except branches, and every
transport-exception path keep their existing fail-open behaviour.
Tests: 21 mutations of the production source, all KILLED.
* fix(relay): authorize the RESOLVED target; declines must not fall back
Review round 1 (independently confirmed by a second reviewer) found three
blockers. Two are fixed here; the third (B-2, Telegram @username) is a policy
decision left open deliberately.
B-1 — THE FIX CAUSED THE OUTAGE IT PREVENTED (tools/send_message_tool.py)
The P5(a) guard ran ABOVE Slack user->DM resolution, so it authorized the
internal pseudo-id `_parse_target_ref` emits (`user_name:ben`, `user:U...`).
Provenances only ever hold RESOLVED conversation ids, so a fully attested DM
was compared as a handle against a set of `D...` ids and refused:
base slack:@ben SENT head(before) slack:@ben REFUSED
Every Slack DM by handle was broken. Moved the guard below resolution; it now
authorizes the destination that is actually sent to, and the refusal names the
resolved id. Position is load-bearing, so it is commented as such and pinned:
reverting the move turns exactly the four new cases red.
B-3 — A DECLINE IS NOT A LANE FAILURE (gateway/run.py)
`_approval_send_outcome` had only sent/failed/ambiguous, so a connector
decline collapsed into `failed` — which is the cue to run the plain-text
fallback into the chat the connector had just refused. The adapter fix in the
previous commit improved the error STRING while user-visible behaviour stayed
identical to base; the commit message overstated it. Fixed properly:
- new `declined` verdict, recognised via the shared `is_egress_decline`
contract (not string sniffing at the call site)
- exec-approval returns without the text fallback
- slash-confirm suppresses the text reply AND clears the registration, so a
card that never rendered cannot capture the user's next message
`send_clarify` was already correct (returns early inside the adapter).
MUTATIONS (production source; both directions)
classifier never returns 'declined' -> KILLED (4 cases)
ALL failures classified as 'declined' -> KILLED (2 cases)
guard moved back above Slack resolution -> KILLED (4 cases)
decline CODE changed (review M05) -> KILLED
marker match made case-sensitive (M10) -> KILLED
M05 was a tautology: the test asserted the imported constant against itself,
so changing the constant could not fail it. The wire contract is now pinned as
a literal, because the connector stamps that exact string and a one-sided
change is a silent cross-repo break.
REGRESSION CHECK: the 12 failures + 1 collection error in this test selection
are PRE-EXISTING cross-test contamination — the identical set fails at
|
||
|
|
d63e380324 |
compat(plugins): warn once per name when a plugin resolves an old import path; lint step restored in CI
Every PLUGIN-COMPAT __getattr__ now calls hermes_cli.plugin_compat.warn_once(facade, name, target) before
resolving, emitting a HermesPluginCompatWarning (FutureWarning) once per process per name: old path, new
path, removal target. Importing a facade for its live API stays silent; only resolving a moved name warns.
COMPAT_MANIFEST.md documents the warning and how to silence it during migration.
Verified the runtime never routes through a pointer: every entry point (run_agent, cli, hermes_cli.main,
gateway.run, tui_gateway.server, web_server, model_tools + tool discovery, hermes_state, cron.scheduler,
browser_tool, mcp_tool, kanban, auth) imports clean and `hermes doctor` runs end to end with the warning
promoted to an error.
Also restores the check_compat_pointers CI step to .github/workflows/lint.yml, which
|
||
|
|
2776813df3 |
compat(plugins): temporary import-path shims for external plugins — ONE commit, revert on schedule
The Sep 2026 decomposition (PR #102117) makes internal import paths a non-API: names now live in the focused modules that define them. This commit is the ONLY thing keeping the old paths alive, so external plugins have time to update. It is deliberately a single, unsquashed commit: git revert <this sha> removes every shim, stub and manifest at once on the announced date. Nothing in-tree may depend on these pointers: scripts/check_compat_pointers.py (wired into lint.yml) fails CI if it does. What it adds (see COMPAT_MANIFEST.md, compat_manifest.json): - 332 facade modules get one delimited `PLUGIN-COMPAT` block appended at the end of the file - 1,172 moved names resolved lazily via a module `__getattr__` (PEP 562) — never a top-level import, so no import cycles; facades that already had `__getattr__` get a chained one - 592 third-party/stdlib names the old modules used to expose, with their original import statements - 266 public definitions that had been deleted as unused, restored byte-for-byte from the pre-decomposition tree (+40 private helpers and 16 imports pulled in only because a restored definition needs them) - 3 deleted modules recreated as re-export stubs (gateway/startup_watchdog, hermes_cli/observability/ relay_runtime, tools/environments/modal_utils) - private names (`_x`) get no pointer: they were never API (3,792 skipped) Verified: all 335 touched modules import under a fresh HERMES_HOME and every manifest name resolves; the lint reports zero in-tree uses; ruff clean; targeted suites unchanged. |
||
|
|
707161e77b | simplify(compat): file_tools/send_message_tool/cronjob_tools/process_registry — drop 34 re-exports, repoint 5 callers + 27 test files | ||
|
|
e83816a4d1 |
review-fix(comments): restore lost #NNNN rationale comments across non-test source (mechanical sweep, condensed, code unchanged)
For each issue anchor present in BASE
|
||
|
|
691e2f7d9f | refactor(tools): send_message wave-2 cut — table-driven telegram media, shared platform-module guard, flattened slack/target resolution, compact docs | ||
|
|
19a479bc06 | refactor(tools): tidy signal retry logging and platform config guard | ||
|
|
dc79b0a11a | refactor(tools): compact send_message routing call sites | ||
|
|
0a9bc0f3e6 | refactor(tools): share _NO_DELIVERABLE, tighten telegram format helper | ||
|
|
4228d0f20a | refactor(tools): compact send_message literals and telegram media warnings | ||
|
|
51d44c5e32 | refactor(tools): fold small conditionals in send_message helpers | ||
|
|
c9d52c05b2 | refactor(tools): unify adapter media-method selection across matrix and live-adapter sends | ||
|
|
6260446b15 | refactor(tools): tighten send_message target/home/mirror helpers | ||
|
|
6b226dbce9 | refactor(tools): compact signal RPC builder and blank squeeze | ||
|
|
3106c534c2 | refactor(tools): extract telegram format + gateway-loop dispatch helpers, compact media descriptors | ||
|
|
37ae28f187 | refactor(tools): compact send_message docstrings and standalone fallback | ||
|
|
b99a74a3c7 | refactor(tools): share live-adapter lookup, prune unused re-exports, hug brackets in send_message | ||
|
|
79f0c28840 | refactor(tools): dedupe send_message helpers (success payloads, target resolve, signal batch, slack forms) | ||
|
|
6723628de9 | refactor(tools/messaging): split send_message into senders/targets tables; dedupe discord/bot_mode/relay helpers; extract feishu_lark shared plumbing; compact graph client/auth | ||
|
|
d4cec15b47 |
refactor(tools): first-wave simplification of tools/ (file ops split, lazy_deps, code_exec, approval, browser, delegate, mcp, skills, terminal, voice, media)
Behavior-neutral structural pass over tools/*: god-file extractions into sibling modules (file_operations_common/lint/search, file_tools_paths/ read_tracking/write, code_execution_env/rpc, tool_search_catalog/names/ validation, tts_command_provider, ...), duplicate helper unification, if/elif -> dispatch tables, dead-code removal, docstring compaction. Tool schemas (get_tool_definitions) verified byte-identical to base. |
||
|
|
fcd34e57a2 | fix(buzz): complete media-only delivery reporting | ||
|
|
9c25704257 | fix(buzz): verify live media delivery receipts | ||
|
|
37c943997b | fix(buzz): support media in standalone sends | ||
|
|
1885a40ad3 | fix(buzz): preserve literal mentions and exact UUID targets | ||
|
|
42dc0dea70 |
fix(send_message): cross-loop dispatch to live WeCom adapter
When send_message is invoked from the agent's worker thread (a different event loop than the gateway's), awaiting the WeCom adapter directly can hang because the adapter enqueues onto the gateway loop. Dispatch via run_coroutine_threadsafe onto the gateway loop when the caller loop differs, with caller-cancellation shielded so an already-enqueued send is not cancelled mid-flight (which would otherwise cause a false-failure retry -> duplicate). Recognizes WeCom native chat IDs as explicit send targets and whitelists WeCom for media delivery. Part of the async queue design this branch introduces. |
||
|
|
48f69e51d3 |
fix(signal): chunk long standalone sends and cover both delivery paths (salvage #57929 + #67279)
Follow-up to lkz-de's adapter chunking commit: long Signal messages no longer truncate on ANY delivery path. - tools/send_message_tool.py: register Signal's 8000-char limit in _MAX_LENGTHS (imported from the adapter module so the two paths can't drift) so hermes send / cron standalone / MCP sends split via the shared truncate_message() pass instead of signal-cli rejecting them. Standalone-path idea credited to @5L-hermes01 (#67279). - tests: regression test proving standalone Signal sends chunk at the adapter limit with no truncation footer (fails on pre-fix main). - docs: Long Messages section on the Signal page (en + zh-Hans). Both fixes verified by sabotage A/B (tests fail with the respective half reverted to origin/main) and a real-import E2E: 27k-char message with emoji + cross-boundary bold + code blocks -> 4 chunks, all styles in-range UTF-16, lossless reassembly. |
||
|
|
ed4f50de51 |
fix(send_message): hand unresolved cron and react targets to the adapter again
Restores pass-through behavior for cron delivery and react/unreact that was lost when |
||
|
|
d409f67485 |
feat(platforms): add typed plugin send paths
Route plugin target parsing, validation, and host-driven delivery through PlatformEntry across CLI and cron while preserving the host-only send_message policy. |
||
|
|
d3ebe14b03 | fix(send_message): constrain opaque plugin fallback | ||
|
|
a37192546e | feat(gateway,send_message): plugin platform target parsing via PlatformEntry.parse_target_ref_fn and verbatim fallback (#67941 #33547) | ||
|
|
274214d3c9 | fix(send_message): avoid shared schema mutation and support sync enricher handlers | ||
|
|
482682db78 | send_message: plugin enricher registry for custom platforms | ||
|
|
e8c5cb5710 |
fix(qqbot): scope the authz, startup-validation, and direct-send QQ reads
Review follow-up: the adapter-level resolver alone left three paths reading per-profile QQ_* values from raw os.getenv, so a secondary multiplex profile's scoped opt-in or credentials were ignored (or the primary's environ values leaked in): - gateway/authz_mixin.py: route the per-platform allow-all flag and the per-platform/group allowlist + allow-bots reads through the scope-aware gateway.config._getenv. Deployment-global GATEWAY_* reads intentionally stay on os.getenv. This makes the same fix effective for every own-policy platform, not just QQ; unscoped behavior is byte-identical to os.getenv. - gateway/run.py (_own_policy_open_startup_violation): resolve the per-platform dm/group policy and allow-all opt-in via _getenv; the secondary-profile caller already runs inside _profile_runtime_scope. - tools/send_message_tool.py (_send_qqbot): the QQ_APP_ID / QQ_CLIENT_SECRET fallbacks now honor the active profile scope. Tests: tests/gateway/test_qqbot_scope_paths.py covers all three paths end-to-end (scope wins, no environ inheritance for non-opted profiles, single-profile environ fallback unchanged); the STT suite now asserts QQ_STT_BASE_URL and QQ_STT_MODEL scoping alongside the API key. All five scoped-behavior tests fail on the previous commit and pass here. |
||
|
|
5b751dc0ad |
chore: remove unused imports and dead locals (ruff F401/F841 sweep)
Cleans F401 unused imports and F841 dead local assignments across root *.py, agent/, hermes_cli/, tools/, gateway/, cron/, tui_gateway/ (tests/, plugins/, skills/ excluded). Intentionally KEPT (false positives / test-patch surfaces): - agent/transports/__init__.py package re-exports - cli.py browser_connect re-exports (DEFAULT_BROWSER_CDP_URL area, used by tests/cli/test_cli_browser_connect.py) - hermes_cli/main.py _prompt_auth_credentials_choice / _model_flow_bedrock_api_key (accessed via main_mod attr in tests) - gateway/run.py aliased replay_cleanup + whatsapp_identity re-exports and _PORT_BINDING_PLATFORM_VALUES (test-referenced) - hermes_cli/web_server.py get_running_pid (tests monkeypatch it) and _OAUTH_TOKEN_URL availability probe - hermes_cli/config.py get_process_hermes_home re-export (noqa'd F811 chain) and yaml availability-probe import - hermes_cli/nous_subscription.py managed_nous_tools_enabled (tests patch hermes_cli.nous_subscription.managed_nous_tools_enabled) - try/except ImportError availability probes (env_loader, tts_tool, mcp_tool, web_server anthropic OAuth block) - tools/web_tools.py noqa F401 re-exports - hermes_cli/setup_whatsapp_cloud.py:263 'proceed' skipped: possible missing-guard bug, flagged for separate review - unused function parameters (signature changes out of scope) Side-effect RHS calls preserved where only the binding was dead (e.g. web_server proc = _spawn_hermes_action -> bare call). |
||
|
|
1a7f73b8ea |
refactor: migrate hand-rolled error envelopes to shared tool_error()
Replace json.dumps({"error": ...}) boilerplate with the documented
tools/registry.py tool_error() helper across 13 files.
Migrated: 59 sites (58 code sites + 1 docstring example in
path_security.py), incl. multi-key envelopes passed via kwargs
(available_actions, path/already_read, pattern/already_searched,
parameters/hint, needs_reauth/server, error_type/tool/result_type).
Also removed 2 now-redundant local tool_error imports in mcp_tool.py
in favor of a module-level import.
Skipped (not byte/shape-compatible with tool_error):
- {"success": false, "error": ...} envelopes (browser_tool,
browser_camofox, browser_dialog_tool, web_tools, tts_tool,
skills_tool, image_generation_tool, project_tools, memory_tool,
cronjob_tools, x_search_tool, xai_video_tools) — leading keys
differ; key order would change.
- terminal_tool/code_execution_tool envelopes carrying output/
exit_code/status leading keys.
- tool_search.py:912-area multi-key success paths (non-error).
- mcp_tool.py MCPSampling._error — returns MCP-spec ErrorData
object, not a JSON string; incompatible.
- send_message_tool._error — returns a dict (not str) and applies
secret redaction; return type must be preserved.
Behavior note: sites that previously omitted ensure_ascii=False now
emit raw UTF-8 (tool_error's canonical behavior) — JSON-equivalent.
Tests: 23 targeted files (tool_search, discord, file_tools/read
guards/operations, registry, clarify, homeassistant, code_execution,
send_message, delegate, terminal, mcp, model_tools, sanitize_tool_error,
retaindb plugin) — all pass. ruff clean.
|
||
|
|
f041c95b7f |
fix(send_message): pass photon DM chat GUIDs through as explicit targets
'photon:any;-;+1555...' targets matched no parser pattern, so _handle_send bounced them off the channel directory and failed resolution even though the adapter accepts the GUID verbatim (the react handler already passed them through). Recognize the DM chat GUID shape (mirrors the adapter's _DM_CHAT_GUID_RE) in _parse_target_ref for photon only. |
||
|
|
4ae27548d6 | fix(media): recognize m2a audio attachments | ||
|
|
0ec1b9f7fa |
fix(gateway,tools): add missing .3gp and .webm to video extension sets
MEDIA_DELIVERY_EXTS in gateway/platforms/base.py omitted .3gp, causing MEDIA: tags with .3gp files to leak as plain text instead of being extracted for native video delivery. _VIDEO_EXTS in tools/send_message_tool.py and _MIGRATION_VIDEO_EXTS in the Feishu adapter omitted .webm, causing .webm files to be classified as documents instead of video on Telegram and other platforms. Both extensions are already present in every gateway-side _VIDEO_EXTS definition (run.py, kanban_watchers.py, weixin.py, base.py local). Closes #71621, Closes #71603 |
||
|
|
d9fe008db8 |
fix(slack): prefer live send adapter and try multi-workspace tokens individually
Two related Slack delivery fixes for send_message text sends: - Route Slack text delivery through _send_via_adapter so the live in-process gateway adapter (multi-workspace aware, channel→client mapping, adapter-side gates) is preferred, with the plugin's _standalone_send as the out-of-process fallback — matching how the media path already behaves. - _standalone_send: SLACK_BOT_TOKEN can be a comma-separated list in multi-workspace installs and slack_tokens.json carries OAuth per-workspace tokens; the standalone Web-API path used to send the literal comma-joined string, which Slack rejects as invalid_auth. Try each token individually, retrying on token-scoped errors (invalid_auth / not_in_channel / channel_not_found …) and stopping on terminal ones. User-DM resolution (U…/W… targets) also tries each token. Adapted from #47547 by @replygirl — the original patched the legacy tools/send_message_tool.py::_send_slack helper, which moved to the Slack plugin's _standalone_send in #41112. Salvaged from #47547 |
||
|
|
57f8ba3b19 | fix(slack): open DMs for user send targets | ||
|
|
5f4c952ab0 |
fix(slack): support MEDIA attachments in send_message
Slack could already deliver files in-channel via the gateway, but send_message omitted MEDIA for Slack and told the model it was unsupported — causing agents to inconsistently refuse PDF sends. Wire Slack through files_upload_v2 in the standalone sender. |
||
|
|
4f726ed467 | fix(slack): preserve media in standalone cron delivery | ||
|
|
6160a80253 |
fix(gateway/platforms): migrate all Weixin fallbacks to get_secret() for consistent profile-scoped resolution
Per egilewski's security review, WEIXIN_BASE_URL and WEIXIN_CDN_BASE_URL were still resolved from process-global environment variables, leaving mixed-scope bypasses in multiplex mode. Changed files: - gateway/platforms/weixin.py: Added get_secret import, replaced os.getenv() with get_secret() for WEIXIN_ACCOUNT_ID, WEIXIN_TOKEN, WEIXIN_BASE_URL, WEIXIN_CDN_BASE_URL in WeixinAdapter.__init__() and send_weixin_direct() - tools/send_message_tool.py: Added get_secret import, replaced os.getenv() with get_secret() for all WEIXIN_* fallbacks in _handle_send() All runtime Weixin send paths now resolve both credentials and endpoint configuration from the same profile-scoped source. |
||
|
|
d73a6f5ac2 | fix(telegram): include duration in standalone sends | ||
|
|
709da844b5 |
feat(gateway): attach MEDIA: caption to the media bubble on standalone sends
hermes send "MEDIA:/x.png This Caption" now arrives as one native captioned bubble instead of a separate text message followed by an uncaptioned bubble. Root cause: the standalone senders (hermes send / cron / send_message tool) stripped the MEDIA: tag, sent the remaining text as its own message, and called the media send with no caption -- even though hermes send's help advertises the captioned form and the bridges/adapters already support a caption. Signal already captioned correctly. - tools/send_message_tool.py: new _media_caption_split() chokepoint decides caption-vs-separate-body (single captionable non-voice file within the platform's message-length cap). Wired into the Telegram, WhatsApp and Discord dispatch paths. - Telegram/WhatsApp/Discord: when the single captioned file is missing, the caption text is delivered as a plain message so it is never silently lost. - Telegram caption send gets a MarkdownV2->plain parse fallback. - Tests: _media_caption_split unit tests + per-platform caption tests (ride, multi-file fallback, voice exclusion, over-limit fallback, missing-file text fallback); updated the 3 tests that asserted the old text-then-media split. Closes the gap reported against #58911 (the MEDIA_CAPTION directive PR); credit to @ferreiraesilva for surfacing the caption behavior. |
||
|
|
55e3ee1ab8 |
fix: remove dead f-string prefixes via ruff F541 (216 sites) (#52336)
ruff check --fix --select F541 . on current main. Pure prefix removals; adjacent-string concatenations keep the f only on interpolating fragments. No string content or live placeholder altered. |