main
157 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a49d196b5c |
Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts: # hermes_cli/env_loader.py # hermes_cli/urllib_security.py # tools/terminal_scope.py |
||
|
|
f16a54bf7c |
fix(relay): treat the "relay" placeholder as an unresolved ack lane
Review caught the fallback never firing. `_event_from_wire` maps an absent OR unknown wire platform to `Platform.RELAY`, so an unresolved lane arrives as the truthy string "relay", never as "". Testing only for "" made the chat-lane and descriptor fallbacks dead code: a Telegram-primary gateway whose connector did not stamp the platform still got ✅ and still failed. Check each candidate in turn — event platform, the chat's inbound lane, then the descriptor's primary — skipping "" and "relay", since any of them can hold the placeholder. Reading the lane straight from `_platform_by_chat` rather than `_chat_platform` matters too: that helper folds in the descriptor itself, so a cached "relay" would shadow a real primary platform. The replaced test asserted on `platform=None`, a state the wire cannot produce, which is why it passed while the path was broken. The new ones build events through `_event_from_wire` for absent, explicit-"relay" and unknown platforms, cover chat-lane precedence over a different primary, and keep ✅ for a genuinely relay-primary gateway. |
||
|
|
ab24fb0581 |
fix(relay): use a reaction Telegram actually allows for the turn ack
The ack lifecycle reacted 👀 on start and swapped it for ✅/❌ on completion, hardcoded for every platform the relay adapter fronts. Telegram accepts only the 73 emoji listed on `ReactionTypeEmoji`. 👀 is in that set, ✅ and ❌ are not, so on Telegram the opening ack landed and every completion ack was rejected by the Bot API. `_react` is best-effort by design, so it failed silently on every single turn — matching the `turn_ack_reaction_lifecycle` probe finding. Pick the emoji per lane instead: 👍/👎 on Telegram (both in its set), ✅/❌ everywhere else, since Slack/Discord/Matrix/Signal take arbitrary emoji and ✅ reads better there. The lane comes from the inbound event that the ack follows, so no cache is needed; `_chat_platform` and the descriptor's primary platform are the fallbacks. `Platform` is a plain `Enum`, so `str(member)` is "Platform.TELEGRAM", not "telegram" — the lookup reads `.value` first. A naive `str()` makes the whole fix a silent no-op, so the tests use the real enum and a mutation check covers it. |
||
|
|
e1576d06a6 |
Merge remote-tracking branch 'origin/main' into ethie/pm-clean
Resolved toward the branch: PM provisions uv/python (main's install.ps1 uv-shim salvage + its test and workflow steps dropped), the shim re-exec stays retired, package.json carries no electron-builder block (afterExtract identity stamp wired into electron-builder.config.cjs instead; after-pack.mjs keeps signing only), Desktop workspace-deps helpers stay retired. Main's scratch-dir bootstrap (export_scratch_tmp_env) is taken and re-run after profile resolution. |
||
|
|
e09cd0d2bc |
fix(relay): echo the routed profile on every outbound frame and follow_up
The connector stamps `profile` on inbound and passthrough_forward frames but the gateway never sent it back, so the connector had nothing to stamp on the NEXT interaction of a routed chat. `_capture_scope` now remembers the routed profile per chat, `_with_scope` echoes it as `metadata.profile` on chat-addressed frames, and `send_follow_up` derives it from the `agent:<profile>:` key namespace. A single-profile gateway emits no key — frames stay byte-identical. Contract §4 documents the round-trip. |
||
|
|
a6ae6ace51 |
Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts: # .github/workflows/js-tests.yml # agent/model_metadata.py # apps/desktop/electron/main.ts # apps/desktop/scripts/bundle-electron-main.mjs # apps/desktop/src/app/settings/about-settings.tsx # apps/desktop/src/app/settings/gateway-settings.test.tsx # apps/desktop/src/app/settings/gateway-settings.tsx # apps/desktop/src/app/updates-overlay.tsx # gateway/shutdown_flush.py # hermes_bootstrap.py # hermes_cli/local_runtime/binaries.py # hermes_cli/main.py # hermes_cli/managed_uv.py # hermes_cli/update_cmd.py # hermes_cli/update_cmd_deps.py # hermes_cli/update_cmd_fleet.py # hermes_cli/update_cmd_maint.py # hermes_cli/update_receipt.py # hermes_cli/update_serve_obligations.py # hermes_constants.py # tests/hermes_cli/test_doctor.py # tests/hermes_cli/test_managed_uv.py # tests/hermes_cli/test_pending_supervisor_recovery.py # tests/hermes_cli/test_startup_fast_guards.py # tests/hermes_cli/test_update_desktop_stale_warning.py # tests/hermes_cli/test_update_fleet_restart_pending.py # tests/hermes_state/test_hermes_state.py # tests/tools/test_tirith_security.py # tools/bot_relay.py # tools/checkpoint_manager.py # tools/write_approval.py # website/docs/getting-started/updating.md # website/docs/reference/environment-variables.md |
||
|
|
3ffee76761 |
fix(gateway): race IPv6/IPv4 on every cold-start WebSocket dial
The bootstrap racer covers sync connects only. The gateway's WebSocket dials (relay connector, Yuanbao, Buzz) go through ``websockets.connect`` → ``loop.create_connection``, whose ``happy_eyeballs_delay`` defaults to ``None``: a serial walk that burns the full connect timeout on every blackholed AAAA record before IPv4 answers — the same stall class #114265 reports, one layer up. ``websockets`` forwards unknown kwargs to ``loop.create_connection``, so each call site passes ``happy_eyeballs_delay=0.25`` (the RFC 8305 delay anyio and the sync racer already use). One invariant test per call site captures the kwargs at a mocked ``websockets.connect``. |
||
|
|
70addd3522 |
fix(gateway): keep default-off byte parity; scope only the policy reads
Four places changed behaviour for users who never touched the setting: - `_interim_send` was stamped on every `warn` status and media-failure notice, and the Slack/relay egress doors learned to skip stream sealing for it. Main's status sends carry no interim mark at all, so the gap is class-wide (every status kind), and fixing it for warnings alone is an undeclared streaming-contract change. Reverted here; the whole-class fix belongs in its own PR against gateway/AGENTS.md rule 3. - The entire post-handler delivery (unwrap, TTS, final text, attachments, delivery-ledger writes) ran inside `_media_delivery_scope`. Under multiplex that binds the routed home, so delivery obligations landed in the routed profile's state.db while boot-time `_claim_pending_obligations` still reads the launch home. Only the policy reads (`diagnostic_wake_muted`, `warning_text`) bind the routed scope now; delivery stays where main ran it. - The turn-crash notice is rebuilt the same way: scope around the policy read, send outside. - The "delivery failed after multiple attempts" notice is unconditional again: the requested result itself was lost and this line is its only signal, so it is not a diagnostic. Tests that asserted the reverted behaviours are removed; the reviewer-round test file is renamed for what it covers. |
||
|
|
cd3de040ab |
feat(notifications): opt-in suppression of user-channel warning notifications
Squash of the 54 commits on victor-kyriazakos:feat/user-channel-warning-suppression (PR #112302, head f45c640e55) so the contributor's authorship survives a rebase-merge; the commits interleave with a cron delivery-ledger rework that the salvage removes in follow-up commits, so per-commit cherry-picks were not practical. Adds display.suppress_warning_notifications (global + per-platform, default false): one resolver (gateway/warning_notifications.py), BasePlatformAdapter.emit_warning / emit_media_warning / warning_text, a notification_category classification carried through wakes, queues and persistence, and render/present boundaries for CLI/TUI. |
||
|
|
b4a294fff9 |
Merge origin/main; keep PM as plugin dependency owner
Reconcile plugin declarations and validation through PM's atomic generation publication; preserve external runtimes, target markers, and conflict refusal. Keep one source-update completion owner and port upstream lifecycle changes to the PM desktop/runtime paths. |
||
|
|
23036e20a6 |
fix(ux): plain-language, actionable user-facing messages (core)
Squashed integration of the user-facing message audit for this surface set. Full per-finding receipts: /tmp/ux-audit/lanes/*-receipt.md (campaign artifacts). |
||
|
|
612d542281 |
Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts: # .gitignore # Dockerfile # agent/onboarding.py # apps/desktop/electron/main.ts # apps/desktop/electron/pool-stop.ts # apps/desktop/src/components/model-picker.test.tsx # apps/desktop/src/store/updates.ts # apps/desktop/vite.config.ts # datagen-config-examples/run_browser_tasks.sh # docs/rca-ssl-cacert-post-git-pull.md # gateway/run.py # hermes_cli/backup.py # hermes_cli/credential_lifecycle.py # hermes_cli/dashboard_procs.py # hermes_cli/doctor_state.py # hermes_cli/env_loader.py # hermes_cli/gateway_windows.py # hermes_cli/local_runtime/endpoint.py # hermes_cli/psutil_android.py # hermes_cli/update_cmd.py # hermes_cli/update_cmd_windows.py # hermes_cli/web_routers/local_models.py # hermes_cli/web_server_config.py # hermes_cli/web_server_cron.py # plugins/memory/hindsight/__init__.py # plugins/memory/holographic/__init__.py # plugins/memory/honcho/cli.py # plugins/memory/mem0/__init__.py # plugins/platforms/google_chat/oauth.py # plugins/platforms/photon/adapter.py # scripts/ci/list_os_marked_tests.py # scripts/run_tests.sh # tests/agent/test_compression_stall_fallback.py # tests/agent/test_create_openai_client_ssl_verify.py # tests/gateway/test_google_chat_oauth_dependencies.py # tests/hermes_cli/conftest.py # tests/hermes_cli/test_cli_init.py # tests/hermes_cli/test_gateway_migrate_multiplex.py # tests/hermes_cli/test_psutil_android_extract.py # tests/hermes_cli/test_relaunch.py # tests/hermes_cli/test_update_check.py # tests/hermes_cli/test_update_handoff_desktop_rebuild.py # tests/hermes_cli/test_worktree_gc.py # tests/scripts/desktop_update/test_desktop_update_windows_python_handoff.py # tests/scripts/desktop_update/test_desktop_update_windows_retry_policy.py # tests/scripts/desktop_update/test_desktop_update_windows_timestamp.py # tests/scripts/install/test_install_autostash_conflict_recovery.py # tests/scripts/install/test_install_clone_throttle_fallback.py # tests/scripts/install/test_install_commit_pin_rollback.py # tests/scripts/install/test_install_diverged_update.py # tests/scripts/install/test_install_lockfile_churn.py # tests/scripts/install/test_install_macos_launcher.py # tests/scripts/install/test_install_no_initial_commit.py # tests/scripts/install/test_install_ps1_ascii_only.py # tests/scripts/install/test_install_ps1_browser_install.py # tests/scripts/install/test_install_ps1_managed_node_swap.py # tests/scripts/install/test_install_ps1_native_stderr_eap.py # tests/scripts/install/test_install_ps1_node_path_for_npm.py # tests/scripts/install/test_install_ps1_python_fallback_venv.py # tests/scripts/install/test_install_ps1_resolver_strictmode.py # tests/scripts/install/test_install_ps1_uv_install_fallback.py # tests/scripts/install/test_install_ps1_uv_powershell_host.py # tests/scripts/install/test_install_ps1_venv_process_tree.py # tests/scripts/install/test_install_ps1_venv_recreate_safety.py # tests/scripts/install/test_install_ps1_venv_rename_abort.py # tests/scripts/install/test_install_ps1_venv_transaction_boundary.py # tests/scripts/install/test_install_ps1_web_server_syntax_probe.py # tests/scripts/install/test_install_scripts_computer_use.py # tests/scripts/install/test_install_sh_acp_launcher.py # tests/scripts/install/test_install_sh_bootstrap_marker.py # tests/scripts/install/test_install_sh_browser_install.py # tests/scripts/install/test_install_sh_install_method_stamp.py # tests/scripts/install/test_install_sh_node_deps_failure.py # tests/scripts/install/test_install_sh_node_deps_workspaces.py # tests/scripts/install/test_install_sh_node_global_prefix.py # tests/scripts/install/test_install_sh_node_npm_check.py # tests/scripts/install/test_install_sh_node_prerelease.py # tests/scripts/install/test_install_sh_node_probe.py # tests/scripts/install/test_install_sh_node_tarball_without_xz.py # tests/scripts/install/test_install_sh_pythonpath_sanitization.py # tests/scripts/install/test_install_sh_reuse_supported_python.py # tests/scripts/install/test_install_sh_root_fhs_uv_python_path.py # tests/scripts/install/test_install_sh_setup_wizard_tty_probe.py # tests/scripts/install/test_install_sh_symlink_stomp.py # tests/scripts/install/test_install_sh_termux_network_prereqs.py # tests/scripts/install/test_install_sh_termux_python_bounds.py # tests/scripts/install/test_install_sh_uv_lock_config.py # tests/scripts/install/test_install_unmerged_index.py # tests/scripts/test_run_tests_parallel.py # tests/test_managed_runtime_resolution.py # tests/test_project_metadata.py # tests/tools/test_browser_use_cli.py # tests/tools/test_tts_pythonpath_fallback.py # tests/tui_gateway/test_hosted_room_driver_runtime.py # tests/tui_gateway/test_tui_gateway_server.py # tools/lazy_deps.py # tools/voice_mode.py # uv.lock # website/docs/developer-guide/macos-bundle-updates.md # website/docs/developer-guide/pm-audit-status.md # website/docs/developer-guide/shared-bundle-builds.md # website/docs/developer-guide/source-update-completion.md # website/docs/developer-guide/stable-releases.md |
||
|
|
a4e2a82a6d | fix(gateway): preflight task-card destination before transport fallback | ||
|
|
0b40f5a790 |
docs: fold the root docs/ tree into the Docusaurus site and delete it
docs/ was not the documentation site; it was a grab bag of long-form design notes, wire contracts and observability guides that landed with feature PRs because their authors needed somewhere to put them. Root AGENTS.md already says long-form dev docs live in website/docs/developer-guide/; this moves the 14 living documents there (or to the matching user-guide section) so they are published, searchable and linked from the sidebar instead of being found by grep only. Developer guide: micro-compaction, gateway-session-lifecycle (was session-lifecycle), state-db-recovery, multiplexing-gateway, chronos-managed-cron-contract, relay-connector-contract, observer-hooks (was observability/README), gateway-monitoring (observability/monitoring), relay-shared-metrics, middleware, streaming-tts, billing-lifecycle. User guide: egress/network-isolation (was security/network-egress- isolation), features/kanban-multi-gateway (was kanban/multi-gateway). Each page got title/description frontmatter and a sidebar entry; repo- relative links became site links or GitHub blob URLs; two MDX brace hazards escaped. Every in-tree pointer (module docstrings, config comments, the relay conformance test's Path, the monitoring-doc test, gateway-internals, cron-internals, kanban docs, .dockerignore, AGENTS.md) now names the new location. `docusaurus build` passes with no unresolved links on the moved pages. |
||
|
|
ad305bead5 |
refactor(gateway): send_exec_approval is a base template method; 9 adapters only render buttons
Nine surfaces (feishu, teams, slack, telegram, whatsapp_cloud, qqbot, matrix, discord, relay) each re-derived the approval choice set — [Allow Once]; session + always unless smart-denied; [Deny] — and four of them (discord, slack, teams, whatsapp_cloud) never adopted base._format_exec_approval, so header/reason/smart-deny wording and truncation budgets drifted per adapter. Three separate commits had to touch 5–9 adapters for one semantic fix. BasePlatformAdapter.send_exec_approval now builds an ExecApprovalPrompt (shared text via _format_exec_approval, shared `(label, choice, style)` rows via _exec_approval_actions) and hands it to the `_send_exec_approval_prompt` hook. Each adapter keeps only its widget mapping (~10–20 LOC); platform wording stays via the existing `_EA_*` class attrs, and a new `_exec_approval_cmd_budget` hook lets Slack/Discord budget the command against their hard message caps (3000-char section / 2000-char message) instead of computing it inline. `_EA_REASON_BUDGET` covers Slack's 500 / Discord's 300 reason caps. The runner used to detect button support by `hasattr(type(adapter), "send_exec_approval")`; that is now true for every adapter, so `_renders_exec_approval_buttons` asks `supports_exec_approval_buttons()` (hook overridden?) and keeps the duck-typed check for non-BasePlatformAdapter classes. Visible text changes (button semantics unchanged everywhere): - Discord: the smart-deny line now follows the reason (was inside the header before the fence); the truncation marker is "..." not "\n... [truncated]". - Slack: smart-deny line follows the reason instead of the header. - Teams: unchanged (same 2000-char preview, same smart-deny block). - WhatsApp Cloud: identical text; body still capped at 1024. - QQBot/relay: unchanged. |
||
|
|
b3bfc3afe5 |
Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts: # apps/desktop/electron/backend-connection-state.test.ts # apps/desktop/electron/backend-connection-state.ts # apps/desktop/electron/backend-exit.test.ts # apps/desktop/electron/main.ts # apps/desktop/electron/pool-spawn-coordinator.test.ts # apps/desktop/electron/pool-stop.ts # apps/desktop/electron/preload.ts # apps/desktop/src/app/settings/about-settings.tsx # apps/desktop/src/app/updates-overlay.tsx # apps/desktop/src/global.d.ts # apps/desktop/src/store/notifications.ts # apps/desktop/src/store/updates.ts # gateway/config_loader.py # hermes_cli/banner.py # plugins/platforms/dingtalk/adapter.py # tests/hermes_cli/test_plugins_cmd.py # tests/test_live_system_guard.py # tui_gateway/server.py # website/docs/user-guide/desktop.md |
||
|
|
05d705dd69 |
Merge pull request #105839 from victor-kyriazakos/fix/relay-explicit-disable
fix(relay): honor explicit profile disable before relay startup |
||
|
|
6f5dda1aee |
fix(relay): read the opt-out through the loader's own YAML reader
relay_explicitly_disabled() went through hermes_cli.config.read_raw_config,
which turns a malformed config.yaml into {} and then applies the managed
overlay. load_gateway_config() raises on the same file and falls back to
env + gateway.json WITHOUT the managed layer. With a managed
relay.enabled: false and an injected URL the two readers disagreed: the
loader kept relay enabled and suppressed native Slack, while startup
refused to register relay — a profile with no route at all.
Extract the loader's file selection into config_loader.read_yaml_layers()
and have both consumers use it; the predicate mirrors the loader's
fallback (no YAML layer) on a parse error. Regression test fails on the
previous predicate.
|
||
|
|
337d69856f |
fix(relay): legacy gateway.json opt-out is advisory, like every platform
The loader stamps _enabled_explicit only from YAML blocks; a gateway.json `enabled: false` is re-enabled by env presence for slack, telegram and everyone else. Tagging the marker for platforms.relay alone in the legacy reader made relay the one platform whose gateway.json verdict beat the env — a per-platform pin nobody would find later. If legacy opt-outs should become authoritative, that is one rule for all platforms in its own PR. relay_explicitly_disabled() now reads the marker the loader itself sets instead of re-deriving the verdict from 'enabled' in block, so the standalone predicate and load_gateway_config() cannot disagree. |
||
|
|
5746b5bae6 |
fix(relay): veto the disable at the activation boundary only
Every relay object (adapter, media client, WS transport) is constructed inside the registry factory that register_relay_adapter() installs, and that call plus self_provision_relay()/send_relay_policy() all resolve through relay_url(). So the opt-out has three reachable boundaries: relay_url(), relay_fronted_platforms() (env-sourced, standalone cron) and the force=/url= bypass in register_relay_adapter(). The guards on RelayAdapter.connect, _get_media_client, RelayMediaClient.enabled/download and every WS dial/redial were unreachable unless config flipped on a RUNNING gateway — which the docs in this PR say is unsupported. Half-honoring that flip is worse than not honoring it: inbound kept flowing over the open socket while media downloads silently returned None and the next redial terminated. Drop those five guards, the _RelayDisabled terminal exception they needed, and the legacy-JSON lru_cache that existed only to make a full config merge affordable on the per-message path. The activation-time predicate runs a handful of times per boot; the shared raw-config and managed-scope readers already cache on mtime. |
||
|
|
f97a4102dd |
fix(relay): accept a provision response that issues no secret (lockstep with gateway-gateway#223) (#104767)
* fix(relay): accept a provision response that issues no secret Lockstep with gateway-gateway #223 (finding F-004). The connector used to return the stored per-gateway secret from POST /relay/provision on EVERY replay, to anyone reaching the endpoint with the right gatewayId. It now returns credential material only to a caller that proves recorded ownership or possession, and answers secretIssued:false otherwise with secret/deliveryKey ABSENT. This gateway treats that as a hard failure, so #223 cannot merge until this ships. 1. A secret-less response is a VALUE, not an error. _post_provision() raised on any response without a secret. Two legitimate responses hit that raise once #223 lands: - every platform after the first in a multi-platform boot. All platforms share ONE gatewayId, so the first POST mints the secret and the rest are replays of an unchanged binding. self_provision_relay() catches RuntimeError per platform and continues, so a Telegram+Discord gateway would front Telegram and leave Discord unfronted with a warning. - a re-provision by a caller that legitimately no longer holds the secret. Refusing to hand it back IS F-004; /relay/rotate is the way back. Only a malformed body raises now. No secret AND no secretIssued key is still the old unexpected-response case (a pre-F-004 connector), so that keeps failing loudly -- which is what makes this deployable in either order. 2. Only a response that CARRIES credentials may write them. A separate bug that change 1 exposes rather than causes. The guard tested the same env var it wrote: once a withheld response reaches it, it writes an empty secret -- which then reads as "still unset" on the next pass, masking itself, while GATEWAY_RELAY_ID and _DELIVERY_KEY have ALREADY been stamped from a response carrying no credentials. Guard on the secret being present. Tests: 5 invariants in tests/gateway/relay/test_provision_secret_optional.py. Proven red on base -- reverting gateway/relay/__init__.py to origin/main turns 2 of the 5 red, one per fix, then green with the fix restored. My first draft of the multi-platform test passed on base because it had the FIRST platform issue the secret, which satisfies the old guard so it never re-enters and the bug never fires; it proved nothing until the ordering was fixed and a direct write-rule test added. tests/gateway/relay: 303 passed, 0 failed. The 6 failures in the wider tests/gateway run (systemd_notify, wecom_callback, shutdown_forensics, scale_to_zero) reproduce identically on clean origin/main -- pre-existing. * fix(relay): fail closed on the F-004 discriminator; a fully-withheld boot is not a provision Two review findings on the parent commit, each with a red-on-parent test. 1. `_post_provision()` accepted ANY present `secretIssued` value when `secret` was absent (`true`, `"false"`, `0`, …) and settled them as a successful provision. The only credential-less shape the connector emits is literal JSON `false`, so match the discriminator by value: `is not False` raises. 2. `self_provision_relay()` returned True and logged the INFO "self-provisioned" line when every platform answered `secretIssued:false` — no credential in hand, WS upgrade about to be refused, nothing in the boot log to say why. Now: WARNING naming `/relay/rotate` / pin `GATEWAY_RELAY_SECRET`, return False. tests/gateway/relay/: 308 passed, 0 failed. |
||
|
|
c15e1e94c1 | fix(relay): log terminal config opt-out on websocket dial | ||
|
|
7d2b3b767d |
merge: integrate upstream/main into ethie/pm-clean
Merge upstream
|
||
|
|
f2de102872 |
fix(relay): preserve legacy opt-out without standalone bootstrap
Carry legacy relay enabled intent through the canonical env sweep without changing other platforms. Read opt-out config through the cached non-bootstrap reader and existing managed overlay. Reproduce both Salt findings before fixes, cover native startup/delivery and isolated cold imports, and mutation-check the regressions. |
||
|
|
9c576fc69d |
fix(relay): honor explicit profile disable before automatic relay activity
Relay bootstrap treated endpoint availability as activation, bypassing the platform configuration's explicit opt-out. An injected relay URL could provision credentials from a native-only profile and suppress its native connectors even when relay was disabled. Reuse the gateway's platform merge and boolean normalization for the explicit-disable decision. Guard registration overrides, discovery, media, and all dial/retry entrypoints; preserve URL-only legacy activation and explicit operator enrollment. Skip relay-exclusive native suppression for an opted-out relay. Add real-config startup and standalone regression coverage, including inherited credentials, managed config, config precedence, native delivery, and fresh-token retry. Document restart semantics without claiming live socket teardown. |
||
|
|
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
|
||
|
|
fef0e16fe1 |
fix(relay): re-dial once with a fresh token before treating a 4401 as revocation (#102602)
* fix(relay): re-dial once with a fresh token before treating a 4401 as revocation A 4401 close after a successful handshake was read unconditionally as the connector having revoked this gateway's per-gateway secret (opt-out), so the transport latched auth_revoked, the adapter went relay_disabled, and all messaging stopped until a manual restart. But the connector sends the same plain 4401 'unauthorized' for an EXPIRED upgrade token (make_upgrade_token TTL is 300s). Scale-to-zero makes that routine: the instance is suspended while a re-dial is in flight, the token was minted before the freeze, the dial completes on resume with a token past its TTL, the connector refuses it with 4401, and the gateway misreads an expired token as a revoked credential. Production incident 2026-09-02 - the connector DB secret was never revoked. Fix, in gateway/relay/ws_transport.py: - The first post-handshake 4401 is provisional. The reader schedules ONE immediate re-dial (_redial_with_fresh_token) that bypasses the reconnect backoff; _dial_and_start mints a fresh token on every call. The backoff supervisor design is untouched. - Only a 4401 against that fresh token (either refused at the upgrade or closed after a descriptor on that connection, tracked by dial generation) latches auth_revoked. Terminal behaviour is otherwise unchanged. - If the fresh dial fails for a non-auth reason, hand off to the normal backoff supervisor. - Read the Close frame reason (_close_reason_of, sibling of _close_code_of). A 4401 whose reason is exactly 'expired' never latches revocation and takes the normal reconnect path - forward-compatible hook for the connector change landing separately. - disconnect() cancels the one-shot retry task alongside the supervisor. Tests (tests/gateway/relay/test_ws_transport.py, real websockets server): - 4401 once, next dial accepted -> reconnected, auth_revoked False, exactly 2 dials. - 4401 on every dial -> auth_revoked True after exactly one retry (2 dials), no supervisor, no further dials. - 4401 reason 'expired' (once and repeated) -> never latched, reconnects via the normal supervisor. - Existing 7d-B tests keep passing (4401 before any handshake stays retryable; the revoking-every-dial stub still latches). * fix(relay): tolerate a partially built transport in disconnect() teardown Three teardown tests construct WebSocketRelayTransport via object.__new__ without __init__, so the new _auth_retry attribute was absent and disconnect() raised AttributeError. Read it with getattr like the other optional teardown handles. * fix(relay): one live dialer — the fresh-token retry is a flag on the next dial, not a second dialer Review (round 1) found a race: a reader that dies with a provisional 4401 while the backoff supervisor is already mid-dial started _redial_with_fresh_token as a SECOND concurrent dialer; both installed sockets/readers and the supervisor could overwrite the accepted retry socket (3 dials, wrong socket). Now a provisional 4401 sets _auth_retry_pending; _dial_and_start consumes it and stamps _auth_retry_generation on whichever dialer performs the next dial. The reader arms a dialer only when none is live (_dialer_running), and an upgrade-time 4401 on that generation latches revocation from either dialer (_latch_if_fresh_token_refused). The reader also iterates its captured ws handle, not self._ws. Regression test reproduces the race with a deterministic fake connect; it fails with the _dialer_running guard removed. * fix(relay): a dial whose reader died mid-hello is a failed dial; the retry marker survives network failures Review round 2: - BLOCKER: with one-live-dialer, a reader that dies while its own dialer is still inside _dial_and_start (hello in flight) arms nothing — that is the dialer's job — but the dialer then returned 'connected', leaving no socket, no reader, no dialer. _dial_and_start now raises ConnectionError when the reader it installed has already finished, so both dialers take their normal failure path (retry -> supervisor; supervisor -> backoff). - MAJOR: _auth_retry_pending was consumed before websockets.connect, so a connect-time network failure un-marked the retry and the NEXT dial's real fresh-token 4401 read as another first strike (revocation never latched). The marker is now consumed only when the token reaches an auth outcome: upgrade accepted (stamp the generation) or upgrade 4401'd (judge it). Two regression tests, each mutation-checked red against its own guard. |
||
|
|
ab2f4602de |
refactor: MessageEvent to gateway/platforms/event.py; ElicitationHandler takes a call_context thunk
Breaks the two import cycles that forced Protocol stand-ins in the F821 sweep, so the two sites now name the real types. gateway/platforms/event.py (new leaf): MessageType, ProcessingOutcome, MessageEvent moved out of base.py verbatim. Their only dependency is gateway.session.SessionSource; base.py imported helpers.py at module level, so helpers could not name MessageEvent. Now TextBatchAggregator is typed by the real MessageEvent. 249 importers repointed (`from gateway.platforms.base import` -> `.event`, preserving each import's layout); gateway.platforms.__init__ re-exports from .event. The three revert-scheduled PLUGIN-COMPAT pointers that named these symbols (gateway.slash_commands → MessageType, dingtalk → MessageType, photon → ProcessingOutcome) and their COMPAT_MANIFEST rows now target gateway.platforms.event. Docs updated: ADDING_A_PLATFORM.md, adding-platform-adapters.md (en + zh-Hans). tools/mcp_tool_sampling.py: ElicitationHandler no longer holds a back-reference to its MCPServerTask (mcp_tool imports sampling, so the task type cannot be named there). It only ever read owner._pending_call_context, so it takes `call_context: Callable[[], Context | None]` and MCPServerTask passes `lambda: self._pending_call_context`. The consent call is one `functools.partial`, run directly or inside the captured Context. ty on the 11 touched production files vs origin/main: 0 new diagnostics, 14 resolved. (The one `source: SessionSource = None` diagnostic moves with the class; typing it Optional exposes ~60 unguarded call sites — separate follow-up.) Tests: tests/gateway + tests/plugins + tests/tools + touched files, 18,235 passed; the 31 failures reproduce identically on origin/main (macOS /private/tmp, systemd socket, long-path fixtures, live-service tests). |
||
|
|
ce49cdbc59 |
merge: reconcile upstream main with pm audit closeout
Merge upstream
|
||
|
|
a6e10e693f |
fix(gateway): stop dropping the first message after an Azure agent sleeps (#99736)
* feat(gateway): let NAS broker the scale-to-zero suspend where the guest has no lever * fix(gateway): require the going_idle ack and outlast the broker before suspending * fix(gateway): release the redial hold on every path that abandons the suspend * fix(gateway): hold the re-dial only for the brokered lever, and require it * fix(gateway): make the re-dial hold contract real and fence the in-guest suspend too * refactor(gateway): share the watcher-iteration and descriptor helpers across the sleep tests * refactor(gateway): move the sleep tests' repeated setup into their fixtures * refactor(gateway): one docstring line per sleep test, and parametrise the abort and lever paths * fix(gateway): fence the freeze gap, cool down aborts, and stop clobbering a shutdown drain * fix(gateway): slice the Fly freeze fence on the wall clock and widen it for larger machines |
||
|
|
e8fcb007b9 |
Merge remote-tracking branch 'upstream/main' into ethie/pm-clean
# Conflicts: # AGENTS.md # acp_adapter/edit_approval.py # acp_adapter/server.py # agent/agent_init.py # agent/anthropic_adapter.py # agent/anthropic_credentials.py # agent/auxiliary_client.py # agent/azure_identity_adapter.py # agent/bedrock_adapter.py # agent/browser_registry.py # agent/chat_completion_helpers.py # agent/coding_context.py # agent/context_references.py # agent/conversation_loop.py # agent/copilot_acp_client.py # agent/credits_tracker.py # agent/curator.py # agent/curator_backup.py # agent/deadline.py # agent/display.py # agent/errors.py # agent/estop.py # agent/i18n.py # agent/image_gen_registry.py # agent/image_routing.py # agent/learning_graph.py # agent/learning_mutations.py # agent/lsp/servers.py # agent/model_metadata.py # agent/models_dev.py # agent/monitoring/gateway_health_export.py # agent/monitoring/otlp_exporter.py # agent/pet/store.py # agent/process_bootstrap.py # agent/prompt_builder.py # agent/proxy_sources/iron_proxy.py # agent/secret_sources/_cache.py # agent/secret_sources/bitwarden.py # agent/secret_sources/registry.py # agent/shell_hooks.py # agent/skill_bundles.py # agent/skill_commands.py # agent/skill_utils.py # agent/ssl_guard.py # agent/ssl_verify.py # agent/system_prompt.py # agent/terminal_env_registry.py # agent/trace_upload.py # agent/transcription_registry.py # agent/tts_registry.py # agent/verify/environment.py # agent/vertex_adapter.py # agent/video_gen_registry.py # agent/web_search_registry.py # cli.py # cron/jobs.py # cron/scheduler.py # gateway/agent_cache_pressure.py # gateway/cgroup_cleanup.py # gateway/channel_directory.py # gateway/config.py # gateway/control_socket.py # gateway/dead_targets.py # gateway/drain_control.py # gateway/hooks.py # gateway/kanban_watchers.py # gateway/lifecycle_ledger.py # gateway/mirror.py # gateway/pairing.py # gateway/platform_registry.py # gateway/platforms/helpers.py # gateway/platforms/weixin.py # gateway/readiness.py # gateway/restart_loop_guard.py # gateway/rich_sent_store.py # gateway/run.py # gateway/session.py # gateway/shutdown_flush.py # gateway/shutdown_forensics.py # gateway/slash_commands.py # gateway/status.py # gateway/sticker_cache.py # gateway/whatsapp_identity.py # hermes_bootstrap.py # hermes_cli/_early_recovery.py # hermes_cli/_install_repair.py # hermes_cli/_startup_fast.py # hermes_cli/_subprocess_compat.py # hermes_cli/agent_plugins.py # hermes_cli/auth.py # hermes_cli/backup.py # hermes_cli/banner.py # hermes_cli/browser_connect.py # hermes_cli/build_info.py # hermes_cli/cli_agent_setup_mixin.py # hermes_cli/cli_commands_mixin.py # hermes_cli/codex_models.py # hermes_cli/config.py # hermes_cli/config_defaults.py # hermes_cli/config_migrations.py # hermes_cli/container_boot.py # hermes_cli/dashboard_auth/registry.py # hermes_cli/debug.py # hermes_cli/dep_ensure.py # hermes_cli/doctor.py # hermes_cli/doctor_live.py # hermes_cli/dump.py # hermes_cli/env_loader.py # hermes_cli/foreign_sessions.py # hermes_cli/gateway.py # hermes_cli/gateway_windows.py # hermes_cli/gui_uninstall.py # hermes_cli/image_provenance.py # hermes_cli/install_identity.py # hermes_cli/kanban.py # hermes_cli/kanban_db.py # hermes_cli/linux_desktop_entry.py # hermes_cli/local_runtime/binaries.py # hermes_cli/local_runtime/endpoint.py # hermes_cli/local_runtime/growth.py # hermes_cli/local_runtime/supervisor.py # hermes_cli/logs.py # hermes_cli/macos_tcc_anchor.py # hermes_cli/main.py # hermes_cli/memory_setup.py # hermes_cli/model_catalog.py # hermes_cli/models.py # hermes_cli/nous_subscription.py # hermes_cli/npm_engine.py # hermes_cli/plugin_index.py # hermes_cli/plugins.py # hermes_cli/plugins_cmd.py # hermes_cli/profile_distribution.py # hermes_cli/profiles.py # hermes_cli/prompt_size.py # hermes_cli/psutil_android.py # hermes_cli/runtime_repair.py # hermes_cli/security_advisories.py # hermes_cli/security_audit.py # hermes_cli/security_audit_startup.py # hermes_cli/service_manager.py # hermes_cli/session_export_md.py # hermes_cli/setup.py # hermes_cli/skills_hub.py # hermes_cli/slack_cli.py # hermes_cli/status.py # hermes_cli/subcommands/gateway.py # hermes_cli/subcommands/uninstall.py # hermes_cli/tools_config.py # hermes_cli/uninstall.py # hermes_cli/update_cmd.py # hermes_cli/update_contract.py # hermes_cli/update_inventory.py # hermes_cli/update_lock.py # hermes_cli/update_receipt.py # hermes_cli/urllib_security.py # hermes_cli/web_routers/local_models.py # hermes_cli/web_routers/profiles.py # hermes_cli/web_routers/skills.py # hermes_cli/web_server.py # hermes_constants.py # hermes_state.py # plugins/disk-cleanup/__init__.py # plugins/disk-cleanup/disk_cleanup.py # plugins/google_meet/node/registry.py # plugins/google_meet/node/server.py # plugins/google_meet/process_manager.py # plugins/google_meet/realtime/openai_client.py # plugins/hermes-achievements/dashboard/plugin_api.py # plugins/memory/hindsight/__init__.py # plugins/memory/honcho/__init__.py # plugins/memory/honcho/cli.py # plugins/memory/honcho/client.py # plugins/memory/honcho/oauth.py # plugins/memory/honcho/session.py # plugins/memory/mem0/__init__.py # plugins/memory/mem0/_setup.py # plugins/memory/openviking/__init__.py # plugins/memory/retaindb/__init__.py # plugins/memory/supermemory/__init__.py # plugins/platforms/a2a/protocol.py # plugins/platforms/dingtalk/adapter.py # plugins/platforms/discord/adapter.py # plugins/platforms/feishu/adapter.py # plugins/platforms/google_chat/adapter.py # plugins/platforms/matrix/adapter.py # plugins/platforms/photon/adapter.py # plugins/platforms/photon/auth.py # plugins/platforms/photon/cli.py # plugins/platforms/slack/adapter.py # plugins/platforms/teams/adapter.py # plugins/platforms/telegram/adapter.py # plugins/platforms/wecom/callback_adapter.py # plugins/platforms/whatsapp/adapter.py # plugins/teams_pipeline/store.py # plugins/video_gen/fal/__init__.py # plugins/web/ddgs/provider.py # plugins/web/exa/provider.py # plugins/web/firecrawl/provider.py # plugins/web/parallel/provider.py # tests/agent/test_ssl_ca_guard.py # tests/hermes_cli/test_certifi_repair.py # tests/hermes_cli/test_cmd_update.py # tests/hermes_cli/test_cmd_update_apt.py # tests/hermes_cli/test_dashboard_unified_launch.py # tests/hermes_cli/test_dep_ensure.py # tests/hermes_cli/test_doctor.py # tests/hermes_cli/test_doctor_live.py # tests/hermes_cli/test_gui_command.py # tests/hermes_cli/test_kanban_boards.py # tests/hermes_cli/test_kanban_db.py # tests/hermes_cli/test_lazy_refresh_venv_repair.py # tests/hermes_cli/test_memory_setup_provider_arg.py # tests/hermes_cli/test_nous_subscription.py # tests/hermes_cli/test_pip_install_detection.py # tests/hermes_cli/test_profile_export_credentials.py # tests/hermes_cli/test_psutil_android_extract.py # tests/hermes_cli/test_status.py # tests/hermes_cli/test_tui_npm_install.py # tests/hermes_cli/test_update_fleet_restart_pending.py # tests/hermes_cli/test_update_head_moved_gate.py # tests/hermes_cli/test_update_interrupted_recovery.py # tests/hermes_cli/test_web_server.py # tests/hermes_cli/test_web_ui_build.py # tests/test_hermes_logging.py # tests/test_managed_runtime_resolution.py # tests/tools/test_browser_chromium_autoinstall.py # tests/tools/test_browser_chromium_check.py # tests/tools/test_browser_homebrew_paths.py # tests/tools/test_browser_lightpanda.py # tests/tools/test_browser_npx_warmup.py # tests/tools/test_browser_open_timeout.py # tests/tools/test_browser_orphan_reaper.py # tests/tools/test_browser_real_profile.py # tests/tools/test_browser_suspect_recycle.py # tests/tools/test_find_shell.py # tests/tools/test_local_env_blocklist.py # tests/tools/test_macos_protected_search.py # tests/tui_gateway/test_compute_host.py # tools/approval.py # tools/blueprints.py # tools/bot_mode_dm.py # tools/bot_mode_probe.py # tools/bot_relay.py # tools/browser_tool.py # tools/browser_use_cli.py # tools/checkpoint_manager.py # tools/code_execution_tool.py # tools/code_kernel.py # tools/computer_use/cua_backend.py # tools/cronjob_tools.py # tools/discord_tool.py # tools/environments/base.py # tools/environments/daytona.py # tools/environments/local.py # tools/environments/modal.py # tools/environments/vercel_sandbox.py # tools/fal_common.py # tools/file_operations.py # tools/lazy_deps.py # tools/mcp_tool.py # tools/neutts_synth.py # tools/process_registry.py # tools/read_extract.py # tools/registry.py # tools/skill_ledger.py # tools/skill_linter.py # tools/skill_manager_tool.py # tools/skill_usage.py # tools/skills_ast_audit.py # tools/skills_guard.py # tools/skills_hub.py # tools/skills_sync.py # tools/skills_sync_client.py # tools/skills_tool.py # tools/terminal_scope.py # tools/terminal_tool.py # tools/tirith_security.py # tools/transcription_tools.py # tools/tts_tool.py # tools/vision_tools.py # tools/voice_mode.py # tools/wake_word.py # tools/web_result_cache.py # tools/website_policy.py # tools/working_diff.py # tools/write_approval.py # tui_gateway/entry.py # tui_gateway/methods_tools.py # tui_gateway/server.py |
||
|
|
642579db60 |
Merge remote-tracking branch 'upstream/main' into ethie/pm-clean
# Conflicts: # .github/actions/detect-changes/action.yml # .github/workflows/ci.yaml # .github/workflows/tests-os.yml # agent/prompt_builder.py # agent/ssl_verify.py # agent/subdirectory_hints.py # apps/desktop/electron/main.ts # apps/desktop/electron/preload.ts # apps/desktop/src/app/settings/about-settings.tsx # apps/desktop/src/global.d.ts # apps/desktop/src/i18n/ar.ts # apps/desktop/src/store/updates.ts # cron/suggestions.py # gateway/channel_directory.py # hermes_cli/config.py # hermes_cli/doctor.py # hermes_cli/linux_desktop_entry.py # hermes_cli/main.py # hermes_cli/web_routers/profiles.py # hermes_constants.py # plugins/platforms/photon/adapter.py # scripts/ci/classify_changes.py # scripts/install.ps1 # tests/agent/test_relay_runtime_plugins.py # tests/ci/test_classify_changes.py # tests/hermes_cli/test_gui_command.py # tests/hermes_cli/test_linux_desktop_entry.py # tests/hermes_cli/test_update_fleet_restart_pending.py # tests/state/test_fts_runtime_rebuild.py # tests/tools/test_lazy_deps.py # tests/tools/test_macos_protected_search.py # tools/browser_tool.py # tools/file_operations.py # tools/lazy_deps.py # tools/mcp_tool.py # tools/working_diff.py # uv.lock |
||
|
|
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. |
||
|
|
deb9f3c1bd | simplify(compat): hermes_cli/commands — drop the last PEP 562 lazy re-export hook (25 names), repoint 10 callers + 11 test files to commands_platforms/commands_completion | ||
|
|
7a33369e81 |
simplify(compat): interrupt — drop _ThreadAwareEventProxy/_interrupt_event legacy alias, repoint 2 test files
No runtime consumer read the proxy (terminal_tool/environments call is_interrupted()/set_interrupt() directly); its only users were tests patching tools.interrupt._interrupt_event, which had no effect on the code under test. tools/terminal_tool.py's own re-export of the name is owned by another worker. |
||
|
|
c93ace77c2 | simplify(compat): config/runtime_provider/plugins/commands/secrets_cli/kanban — drop 96 re-exports (incl. PEP 562 facades) + 3 aliases (get_pre_tool_call_directive/_block_message, get_telegram_handler_factories), repoint 56 callers + 50 test files | ||
|
|
2031c819fe |
simplify(compat): gateway — re-land 92d0bd0d73 (reverted by stale-index commit b818085298)
Re-applies the gateway compat removal byte-for-byte; see
|
||
|
|
b818085298 | simplify(compat): doctor/status — drop 13 re-exports + the doctor_* globals() facade (97 names), repoint 6 callers / 13 tests | ||
|
|
92d0bd0d73 |
simplify(compat): gateway — drop 30 re-exports/aliases + 2 shim modules, re-remove 3 shim-only names, repoint 24 callers + 34 test files
Per COMPAT_REMOVAL.md (internal import paths are not a stable API): Re-exports removed - gateway/run.py: atomic_json_write, load_dotenv, resolve_delivery_transport, TurnRunner, merge_pending_message_event, _arm_loop_floor_timer, start_loop_liveness_watchdog, DEFAULT_GATEWAY_POST_INTERRUPT_GRACE_TIMEOUT, _UNSET (9) — run_* mixins and tests now import from the defining module (gateway.delivery / gateway.run_turn_runner / gateway.platforms.base / gateway.shutdown_watchdog / gateway.restart / utils). - gateway/session.py: SessionResetPolicy, normalize_whatsapp_identifier, TranscriptReadError, auto_continue_freshness_window (+ "_now & co." noqa facade) — gateway/__init__ takes SessionResetPolicy from .config; callers take TranscriptReadError from gateway.session_transcript. - gateway/kanban_watchers.py: _wake_scope_id + "tests import via origin" noqa facade; tests import from kanban_watchers_common / _notifier. - gateway/slash_commands.py: _model_switch_skew_guard, HISTORY_UNREADABLE. - gateway/stream_consumer.py: escape_code_fences_for_display. - gateway/platforms/api_server.py: "re-exported" RunIdempotencyStore comment; tui_gateway + tests import gateway.platforms.api_server_run_idempotency. - gateway/platforms/__init__.py: PEP 562 __getattr__/__dir__ lazy QQAdapter / YuanbaoAdapter facade (no in-tree importer). - gateway/startup_watchdog.py: whole re-export shim module deleted; the three in-tree callers import hermes_startup_watchdog directly. Aliases removed - gateway/platforms/signal.py: SignalAdapter._markdown_to_signal. - gateway/shutdown_forensics.py: _parse_systemd_duration_to_us. - gateway/platforms/yuanbao.py: OutboundManager.start_slow_notifier / cancel_slow_notifier / get_chat_lock / _chat_locks / CHAT_DICT_MAX_SIZE delegates; module-level get_active_adapter / send_yuanbao_direct; MarkdownProcessor has_unclosed_fence / ends_with_table_row / split_at_paragraph_boundary static pass-throughs (chunk_markdown_text stays — it carries yuanbao's chunking policy). tools/send_message_senders + tools/yuanbao_tools call YuanbaoAdapter.get_active() / sender.send_direct(). Shim-only names re-removed (earlier review-fix round |
||
|
|
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
|
||
|
|
96c104c903 |
review-fix(public-api): restore from_platform_entry, SessionManager.remove_session/cleanup, is_relay_media_url, SessionTurnLeaseRegistry.__len__ + tests
BASE exposed these public names; the simplify refactor dropped them (and deleted/removed their
tests) although plugins/connectors import them. Restore each with BASE's signature and body
(download() again routes its auth decision through is_relay_media_url), and restore the covering
tests ported to the new layout, plus DB-only/task-cwd coverage for remove_session/cleanup, the
zero->4096 chunking normalization for from_platform_entry, and len() on empty/populated registries.
A/B vs
|
||
|
|
8e42ea87ec | refactor(relay): compact package-level docstrings (keep rules/WHYs) | ||
|
|
454ae0fbf4 | refactor(relay/adapter): one draft-frame builder for open/seal frames | ||
|
|
52d77da2c3 | refactor(relay): table-driven idp key resolution and platform-block lookup in relay config readers | ||
|
|
155ee47a81 | refactor(relay/adapter): one op-gated best-effort egress helper (_gated_op) for media/prompt/react/thread ops; shared typing frame | ||
|
|
bf8f95d33a | refactor(relay/adapter): single format-hint stamper, shared Slack egress prep, prompt label tables as data | ||
|
|
6fb2921c54 | refactor(relay): AST-neutral line packing (hug/fold/pack) across relay package | ||
|
|
55cc148069 | refactor(relay/adapter): compact docstrings/comments (keep WHYs), fold cache-capture loops, reuse _apply_descriptor in __init__ | ||
|
|
3635aa6a32 | refactor(relay/adapter): unify task-card frame emission, SendResult projection and prompt-unavailable tail; collapse defensive ladders | ||
|
|
beed360a16 | refactor(relay): compact ws_transport docstrings/comments, drop always-constant frame_type param |