fix(security): enable guard_agent_created by default - #2
Conversation
…llowlist The Nous Portal proxy adapter forwards minted ``agent_key`` bearer tokens to whatever ``base_url`` ``resolve_nous_runtime_credentials()`` returns, which is read directly from the refresh / agent-key-mint response and persisted to ``~/.hermes/auth.json``. With no validation beyond a trailing-slash strip, a poisoned URL (Portal-side MITM, or local write to auth.json) gets forwarded the legitimate bearer on every subsequent proxy request — exfiltrating the user's inference budget and opening a response-injection channel back into the IDE / chat client. Add ``_validate_nous_inference_url_from_network()`` in ``hermes_cli.auth``: an https + host-allowlist check that returns None for anything outside ``inference-api.nousresearch.com``, so callers fall back to the documented default rather than ship the bearer to an attacker. This commit wires the validator into the proxy adapter at ``nous_portal.py``. A follow-up commit wires it into the four refresh / mint sites in ``auth.py`` so the poisoned URL never lands in auth.json in the first place. The env-var override path (``NOUS_INFERENCE_BASE_URL``) bypasses validation by design — that's the documented staging/dev escape hatch and the env source is already trusted (the user set it themselves). Co-authored-by: memosr <mehmet.sr35@gmail.com>
…e sites @memosr's PR NousResearch#27612 put the inference_base_url allowlist check only at the Nous proxy adapter forward boundary. The poisoned URL, however, lands in ``auth.json`` upstream of that — at five refresh / agent-key-mint payload read sites inside ``resolve_nous_runtime_credentials`` and ``_extend_state_from_refresh``. Without gating those sites, a single MITM on a refresh response persists the attacker's URL across restarts, even if the proxy adapter's defense-in-depth check would later catch it on the way out. Replace ``_optional_base_url`` with ``_validate_nous_inference_url_from_network`` at all five Portal-network reads: - hermes_cli/auth.py L4840 (refresh-only access-token path) - hermes_cli/auth.py L4876 (mint payload path) - hermes_cli/auth.py L5154 (terminal-runtime access-token refresh) - hermes_cli/auth.py L5262 (cross-process serialized refresh) - hermes_cli/auth.py L5317 (terminal-runtime mint payload) The state-read path at L5025 (``state.get("inference_base_url")``) is deliberately NOT gated — pre-existing state in ``auth.json`` is either already validated (it came from one of the five network sites above) or set by a trusted local actor (manual edit, ``_setup_nous_auth`` test fixture, ``hermes login nous`` against a staging endpoint via the documented ``NOUS_INFERENCE_BASE_URL`` env override). Direct write_file / patch tampering with auth.json is independently blocked by PR NousResearch#14157. Adds tests/hermes_cli/test_nous_inference_url_validation.py covering: - validator https + host + edge-case rules (12 cases) - all 5 network call sites grep contracts (no _optional_base_url regression possible without test failure) - proxy adapter defense-in-depth check still present - env override path NOT gated (documented dev/staging behaviour) 18 new tests, all 119 Nous-auth tests green.
…ams parity) First migration of an existing built-in platform adapter to the plugin system established by IRC / Teams / LINE / Google Chat. Closes NousResearch#24325; advances the umbrella refactor in NousResearch#3823. Matches Teams' shape exactly — adapter under ``plugins/platforms/discord/`` with the standard ``__init__.py`` / ``adapter.py`` / ``plugin.yaml`` shell, ``register(ctx)`` entry point, **no back-compat shim** at the old import path, and full parity for the four hooks Teams uses plus the ``apply_yaml_config_fn`` hook that landed in NousResearch#25443 (the Discord plugin is the first consumer of that hook): * ``standalone_sender_fn`` — out-of-process cron delivery via REST API * ``setup_fn`` — interactive ``hermes setup gateway`` wizard * ``apply_yaml_config_fn`` — translate ``config.yaml`` ``discord:`` keys into ``DISCORD_*`` env vars (replaces the hardcoded block in ``gateway/config.py``) * ``is_connected`` — declares connection state from ``DISCORD_BOT_TOKEN`` * ``check_fn`` — lazy-installs ``discord.py`` on demand * plus ``allowed_users_env``, ``allow_all_env``, ``cron_deliver_env_var``, ``max_message_length``, ``emoji``, ``required_env``, ``install_hint`` * ``gateway/platforms/discord.py`` (5,101 LOC) → ``plugins/platforms/discord/adapter.py`` (git rename, R090). * New ``plugins/platforms/discord/{__init__.py, plugin.yaml}`` with ``requires_env`` / ``optional_env`` declarations. * Append ``register(ctx)`` block + new hook implementations (``_standalone_send``, ``interactive_setup``, ``_apply_yaml_config``, ``_clean_discord_user_ids``, ``_is_connected``, ``_build_adapter``, plus helpers ``_DISCORD_CHANNEL_TYPE_PROBE_CACHE`` etc.) to the adapter. * Replace the ``Platform.DISCORD elif`` branch in ``GatewayRunner._create_adapter()`` (−9 LOC) with a generic post-creation hook (+6 LOC) in the registry path: any plugin adapter that declares a ``gateway_runner`` attribute now gets it auto-injected. Webhook's built-in branch is unchanged (it doesn't go through the registry path). * Move ``_send_discord`` (190 LOC) and helpers (``_DISCORD_CHANNEL_TYPE_PROBE_CACHE``, ``_remember_channel_is_forum``, ``_probe_is_forum_cached``, ``_derive_forum_thread_name``) from ``tools/send_message_tool.py`` into the plugin as ``_standalone_send``. * Wire via ``standalone_sender_fn=_standalone_send`` (Teams pattern; same gap fixed in NousResearch#21804 for other plugin platforms). * Replace the Discord ``elif`` in ``tools/send_message_tool.py`` ``_send_to_platform`` with a 10-line registry-hook dispatch. * Drop the ``DiscordAdapter`` import and the ``Platform.DISCORD: DiscordAdapter.MAX_MESSAGE_LENGTH`` ``_MAX_LENGTHS`` entry — the registry's ``max_message_length=2000`` covers it. * Move ``_setup_discord`` and ``_clean_discord_user_ids`` (68 LOC) from ``hermes_cli/setup.py`` into the plugin as ``interactive_setup``. * Wire via ``setup_fn=interactive_setup``. CLI helpers (``prompt``, ``print_info``, etc.) are lazy-imported so the plugin's module-load surface stays minimal. * Remove ``"discord": _s._setup_discord`` from ``hermes_cli/gateway.py::_builtin_setup_fn``. * Remove the entire 32-line ``_PLATFORMS["discord"]`` static dict entry — Discord's setup metadata is now discovered dynamically via ``_all_platforms()`` from the registry entry. * Move the 59-line ``discord_cfg`` YAML→env bridge from ``gateway/config.py::load_gateway_config()`` into the plugin as ``_apply_yaml_config``. Covers ``require_mention``, ``thread_require_mention``, ``free_response_channels``, ``auto_thread``, ``reactions``, ``ignored_channels``, ``allowed_channels``, ``no_thread_channels``, ``allow_mentions.{everyone,roles,users, replied_user}``, and ``reply_to_mode`` (including the YAML 1.1 ``off``-as-False coercion and the ``extra.reply_to_mode`` fallback). * Wire via ``apply_yaml_config_fn=_apply_yaml_config``. * The hook runs BEFORE ``_apply_env_overrides`` and after the generic shared-key loop, exactly as documented in ``website/docs/developer-guide/adding-platform-adapters.md``. * Behavior is preserved exactly — every assignment still uses ``not os.getenv(...)`` guards so env vars take precedence over YAML. All 78 references to the old import path are rewritten — no back-compat shim: * 51 ``from gateway.platforms.discord import X`` → ``from plugins.platforms.discord.adapter import X`` * 5 ``import gateway.platforms.discord as discord_platform`` → ``import plugins.platforms.discord.adapter as discord_platform`` * 1 ``from gateway.platforms import discord as discord_mod`` → ``from plugins.platforms.discord import adapter as discord_mod`` * 21 ``mock.patch("gateway.platforms.discord.X")`` strings → ``mock.patch("plugins.platforms.discord.adapter.X")`` * 1 docstring reference in ``hermes_cli/commands.py`` * 1 import in ``tools/send_message_tool.py`` (now removed entirely) The import-safety test in ``tests/gateway/test_discord_imports.py`` is updated to purge the new canonical module name from ``sys.modules``. **38 files changed, +621 / −473** — net positive due to the YAML hook implementation (89 new LOC in the plugin trading for 59 deleted in core), but every line moved has a clear plugin home now. The git rename is detected at R090 because the adapter gained ~340 LOC of moved-in hook implementations (``_standalone_send`` + ``interactive_setup`` + ``_apply_yaml_config`` + helpers). * All 568 Discord-specific tests pass across 25 ``test_discord_*.py`` files plus voice/send/text-batching/reload-skills/stream-consumer/ integration tests. * All 147 tests in the YAML-touching subset (``test_discord_reply_mode``, ``test_discord_free_response``, ``test_discord_allowed_channels``, ``test_discord_allowed_mentions``, ``test_discord_channel_controls``, ``test_discord_reactions``, ``test_discord_thread_persistence``, ``test_runtime_footer``) pass — this is the strongest signal that the YAML→env hook behaves identically to the legacy block. * Broader gateway/cron/integration sweep (1297 tests) introduces zero new failures vs ``main``. Pre-existing failures in ``tests/gateway/test_tts_media_routing.py`` and ``tests/e2e/test_platform_commands.py`` reproduce identically on the unchanged ``main`` revision. * Plugin discovery sanity check confirms Discord registers alongside the other four platform plugins: Registered platforms: ['discord', 'google_chat', 'irc', 'line', 'teams'] These Discord-shaped tendrils in core were **deliberately not moved** — they are generic platform-registry concerns affecting every platform, not Discord-specific: * ``gateway/config.py:1205`` ``DISCORD_BOT_TOKEN → config.token`` env enablement — same shape Telegram has. The existing ``env_enablement_fn`` registry hook only seeds ``extra``, not ``.token``, so it can't replace this without an adapter refactor to read from ``extra["bot_token"]``. * ``gateway/run.py`` voice-mode hooks (``self.adapters.get(Platform.DISCORD)`` for ``start_voice_mode``/``stop_voice_mode``), role-based auth, ``DISCORD_ALLOW_BOTS`` branch in ``_is_user_authorized``, ``_UPDATE_ALLOWED_PLATFORMS`` frozenset, and the per-platform allowlist maps — generic platform-registry concerns. * ``Platform.DISCORD`` enum literal — stable identifier used as dict keys throughout the codebase; removing it is a separate refactor with no real benefit. * ``tools/discord_tool.py`` and ``tools/environments/local.py`` — first-class agent tools and env-passthrough config, neither is the gateway adapter. Each of these is worth its own scoping issue when the time comes.
…ed=False, not silently fall back to check_fn Two bugs surfaced by PR NousResearch#24356 migrating Discord into the registry: 1. plugins/platforms/discord/adapter.py::_is_connected — read DISCORD_BOT_TOKEN via hermes_cli.gateway.get_env_value (the abstraction tests patch) instead of os.getenv directly. The legacy non-registry path used get_env_value; bypassing it broke test_setup_openclaw_migration which patches gateway_mod.get_env_value to simulate a hermetic env. 2. hermes_cli/gateway.py::_platform_status — when entry.is_connected is defined and returns False, return 'not configured' immediately. Don't fall back to entry.check_fn(), which would let 'SDK is installed' override 'no token configured' and incorrectly report the platform as ready. The fallback to check_fn is the right behaviour only when is_connected is None (not registered). Fixes 5 test failures observed on CI for PR NousResearch#24356: - tests/hermes_cli/test_setup.py::test_setup_gateway_skips_service_install_when_systemctl_missing - tests/hermes_cli/test_setup.py::test_setup_gateway_in_container_shows_docker_guidance - tests/hermes_cli/test_setup_irc.py::TestIRCGatewaySetupFreshInstall::test_setup_gateway_irc_counts_as_messaging_platform - tests/hermes_cli/test_setup_openclaw_migration.py::TestGetSectionConfigSummary::test_gateway_returns_none_without_tokens - tests/hermes_cli/test_setup_openclaw_migration.py::TestSetupWizardSkipsConfiguredSections::test_sections_skipped_when_migration_imported_settings Same _platform_status bug exists for sibling plugin platforms (teams, google_chat) whose check_fn returns true on SDK install alone; their tests just never exercised the registry path before. The bug only became test-visible when Discord migrated into the registry. Validation: 11,167 tests across tests/gateway/ + tests/cron/ + tests/tools/test_send_message_tool.py + tests/hermes_cli/ pass with zero failures.
…s on stale-branch PRs The workflow diffs base.sha..head.sha (two-dot), which compares the tip-of-main tree directly against the PR tip. When files land on main after a PR branched off, they appear in the diff even though the PR never touched them — triggering false-positive findings. Example: PR NousResearch#30609 was flagged for hermes_cli/setup.py, a file added to main by an unrelated commit after the PR branched. Switch to three-dot diff (base.sha...head.sha), which diffs from the merge base to the PR tip — only changes introduced by this PR are included. Applied to all four diff commands in both jobs (scan and dep-bounds).
…usResearch#30619) * fix(minimax-oauth): refresh short-lived access tokens per request MiniMax OAuth issues ~15-minute access tokens. The Anthropic SDK caches api_key as a static string at client construction, so a session that resolves credentials once at startup keeps sending the same bearer until MiniMax returns 401 mid-session. Swap the static string for a callable token provider, reusing the existing Entra-ID bearer-hook infrastructure in build_anthropic_client. The callable re-reads auth.json on each invocation and calls _refresh_minimax_oauth_state, which is a no-op when the token still has more than 60s of life left and refreshes proactively otherwise. Refreshes persist to auth.json so other processes (gateway, cron) see them immediately. The wire-up lives at the agent-init / model-switch boundary rather than in resolve_runtime_provider, so aux client paths that hand the api_key string to OpenAI(api_key=...) are unaffected. * docs: add infographic for minimax-oauth token refresh
run_tests_parallel.py:
- --slice I/N flag (also HERMES_TEST_SLICE env var) runs only the
I-th slice of N, distributing files across slices by cached
duration using LPT (Longest Processing Time first) greedy
algorithm so each slice gets roughly equal wall time
- Duration cache (test_durations.json): maps relative file paths to
last-observed subprocess wall time. _save_durations merges with
existing cache so entries from other slices are preserved.
- Per-file subprocess timing in progress output + end-of-run
distribution summary (percentiles, top-10 slowest, <1s/<2s counts)
- Unknown files default to 2.0s estimate (~P50), spread evenly by LPT
.github/workflows/tests.yml:
- Matrix strategy: slice [1, 2, 3, 4] with fail-fast: false
- Each slice restores duration cache from main (stable key, no SHA),
runs its portion, uploads per-slice durations as artifacts
- save-durations job (main only, if: always()) downloads all 4
artifacts, merges into single cache entry for future PRs
- Timeout reduced from 60min to 30min per slice (~1/4 the work)
Cache design:
- Stable key (test-durations) not keyed by commit SHA — durations
are about files, not commits, and SHA-keyed caches miss on every
new commit and on PR merge commits
- actions/cache scoping: main's cache is visible to all PRs targeting
main; feature branches without a cache still work (default 2.0s)
- No dotfile prefix (upload-artifact v7 skips hidden files)
- test_browser_secret_exfil: mock _run_browser_command instead of launching real Chrome (secret check is pre-launch, browser is irrelevant to the assertion) - test_web_server: add time.sleep(0.05) after pub.send_text() to yield the event loop before receive_text(). TestClient's sync mode can race the broadcast handler otherwise, hanging the test.
Benchmarked 4/5/6/7/8 slices with LPT duration-balanced distribution: - 4 slices: 4.8m wall, 135s spread - 5 slices: 3.4m wall, 46s spread - 6 slices: 3.3m wall, 26s spread ← optimal - 7 slices: 3.9m wall, 109s spread - 8 slices: 3.7m wall, 96s spread 6 slices is the sweet spot: lowest wall time, tightest spread. 7+ gets slower due to per-slice startup overhead dominating. Also removes benchmark branch markers from save-durations condition.
Removes the global `uppercase` + `font-mondwest` from the App.tsx root
that forced every page to opt-out, replaces stacked-alpha text colors
with semantic tokens for WCAG-AA contrast across all 7 themes, and
applies the new `text-display` utility from @nous-research/ui@0.16.0
on intentional brand chrome (page titles, sidebar headings, segmented
filters) only. Bumps every sub-12px arbitrary text size to text-xs.
Also widens the dashboard plugin routes (/api/dashboard/agent-plugins/
{name:path}/...) so category-namespaced plugins like observability/
langfuse and image_gen/openai can be enable/disabled from the dashboard
— previously the FE encodeURIComponent-ed the slash and the backend
{name} route rejected it. _validate_plugin_name still blocks .. and
backslash, and strips leading/trailing slash.
Touches sessions/env/keys page chrome and adds two new i18n keys
(`overview`, `showMore`/`showLess`) across all 18 locales.
Squashes 19 commits from PR NousResearch#28832.
Co-authored-by: Hermes <noreply@nousresearch.com>
Follow-up to PR NousResearch#28832 — the dashboard plugin routes now accept slashed names like `observability/langfuse` and `image_gen/openai`, but `_sanitize_plugin_name` still rejected forward slash and so dashboard update + remove on those plugins fell through to '404 not found' even though they exist on disk. Adds an opt-in `allow_subdir=True` flag that: - Permits internal forward slashes (category-namespaced plugin keys emitted by `_discover_all_plugins`). - Strips leading and trailing slashes. - Still rejects `..` and backslash, and still asserts the resolved target lives inside `plugins_dir`. Opted in at the two read-paths that operate on installed plugins: `_require_installed_plugin` (CLI update/remove) and `_user_installed_plugin_dir` (dashboard update/remove). The install path keeps the default (`allow_subdir=False`) because freshly-cloned plugins always land top-level under `~/.hermes/plugins/<name>/`. Adds 6 targeted unit tests covering the new flag's allow/reject matrix.
…ding, hash file paths - skills_hub: validate that uninstall_skill's install_path resolves inside SKILLS_DIR before calling shutil.rmtree, preventing recursive deletion of arbitrary directories via poisoned lock.json entries - skills_hub: include file paths (not just contents) in bundle_content_hash so swapping filenames between files changes the hash, strengthening update-detection integrity - pairing: wrap list_pending() in self._lock so _cleanup_expired() file writes don't race with concurrent generate_code()/approve_code() calls Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
… bundle_content_hash) PR NousResearch#6656 added rel_path + \x00 prefixing to ``bundle_content_hash`` so a filename swap between two files in a bundle changes the digest. But it only patched the in-memory side — ``content_hash`` in ``tools/skills_guard.py`` (the on-disk equivalent) still hashed file contents only. These two functions need to stay symmetric: ``check_for_skill_updates`` compares the disk hash of an installed skill against the bundle hash of the upstream copy. With the asymmetric fix, every clean install showed as drifted because the digests no longer matched (2 existing tests in ``test_skills_hub.py`` started failing as soon as the contributor's change landed). Apply the same ``rel_path + \x00 + content`` shape to the disk-side function. Both functions now produce the same digest for the same skill content laid out two ways. Documented the symmetry invariant in the docstring so a future change to either function knows to touch both. Also adds tests/tools/test_pr_6656_regressions.py with 10 regression tests covering all three fixes salvaged in PR NousResearch#6656: - uninstall_skill path traversal (4 cases: parent segments, absolute paths, symlink escape, legitimate skill) - bundle_content_hash filename swap detection (4 cases: in-memory swap, identity, disk-side swap, bundle↔disk symmetry) - list_pending lock contract (2 cases: source-grep contract, smoke) Also fixes AUTHOR_MAP entry for @aaronlab — their commit email (1115117931@qq.com) maps to "aaronagent" which isn't a real GitHub login, so changelog @mentions would 404.
…ousResearch#17656) `get_read_block_error` previously only denied reads inside `${HERMES_HOME}/skills/.hub`, which left `auth.json` (provider OAuth state + plaintext API keys) and `.anthropic_oauth.json` (Anthropic PKCE tokens) directly readable by the agent. A prompt-injection reaching `read_file` could exfiltrate active provider credentials in plaintext. Mode-0600 file permissions only protect against *other Unix users* — the agent runs as the file's owner, so `read_file` is unaffected. Extend the existing deny list with the three credential paths identified in NousResearch#17656 (`auth.json`, `auth.lock`, `.anthropic_oauth.json`). The check uses the same `Path.resolve()` pattern as `skills/.hub`, so symlink/path-traversal indirection is caught too. The agent doesn't need to read these directly — `auxiliary_client` and `credential_pool` consume them through process env / OAuth flows that bypass `read_file`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
read_file_tool resolves relative paths against TERMINAL_CWD (or the task's live terminal cwd), but the prior call passed the original unresolved string to get_read_block_error. That function's own resolve() is anchored at the Python process cwd, so when a task's TERMINAL_CWD pointed at HERMES_HOME and the agent issued read_file on the relative path "auth.json", the credential-store denylist was never reached and the file was read normally. Pass the already-resolved absolute path string at the file_tools call site, document the contract on get_read_block_error, and add a read_file_tool-level regression test that pins the relative-path case under TERMINAL_CWD == HERMES_HOME. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ts, root Extends @briandevans's PR NousResearch#17659 from {auth.json, auth.lock, .anthropic_oauth.json} to also cover: - HERMES_HOME/.env (provider API keys) - HERMES_HOME/webhook_subscriptions.json (per-route HMAC secrets) - HERMES_HOME/mcp-tokens/ (OAuth token directory; dir + everything inside) …AND iterates over both _hermes_home_path() AND _hermes_root_path() so profile-mode runs (HERMES_HOME = <root>/profiles/<name>) also block <root>/{auth.json, .env, mcp-tokens/, ...}. Same widening shape as the write-deny side already does (NousResearch#15981, NousResearch#14157). Explicitly NOT a security boundary. Per the personal-assistant trust model, the terminal tool runs as the same OS user and can `cat auth.json` directly. This read-deny exists as defense-in-depth: - Models that respect tool denials empirically tend to stop rather than reach for the shell. - The denial surfaces an audit trail when something tries to read credentials — easier to spot in logs than a generic `cat`. Docstring + error message both flag this as defense-in-depth so future contributors don't mistake it for a real security boundary and don't re-decline reports that propose the same fix shape. Absorbs the .env and mcp-tokens/ coverage from @tomqiaozc's parallel PR NousResearch#8055 (closed-as-duplicate, credited). Co-authored-by: Tom Qiao <zqiao@microsoft.com>
…Research#29156) GHSA-5qr3-c538-wm9j — half one of the bypass chain. ``_discover_dashboard_plugins`` opted into the untrusted ``./.hermes/ plugins/`` source via ``if os.environ.get("HERMES_ENABLE_PROJECT_ PLUGINS"):`` — which is True for any non-empty string. ``=0``, ``=false``, ``=no``, ``=off`` all return non-empty strings and so *enabled* the project source even though every operator (and the agent loader, ``hermes_cli/plugins.py`` line 815) reads those values as "disabled". An attacker who can land a manifest under the CWD's ``.hermes/plugins/`` directory — a malicious cloned repo, a worktree checked out from a forked PR, a CI runner workspace — was therefore guaranteed to get their manifest discovered the moment the user ran ``hermes dashboard`` from that directory, regardless of whether the user thought they had project plugins disabled. Switch to the shared ``utils.env_var_enabled`` helper used by the agent loader so the gate accepts the documented truthy set (``1`` / ``true`` / ``yes`` / ``on``, case-insensitive) and treats everything else — including ``0`` / ``false`` / ``no`` — as off. Half two (path-traversal + project-source ``api`` import) lands in the next commit. Together they break the RCE chain at two distinct choke points so a future regression in either one alone can't re-open the advisory.
…upt (NousResearch#29507) Layer-2 defense for the FD-recycling race: even with ``force_close_tcp_sockets`` reduced to shutdown-only, the followup ``client.close()`` in ``_close_openai_client`` still walks the httpx pool and closes sockets — and if called from a stranger thread (the interrupt-check loop, the stale-call detector) it has the same FD-recycling exposure that wrote a TLS record on top of ``kanban.db``. Stamp the request_client_holder with the owning thread's ident at ``_set_request_client`` time. In ``_close_request_client_once``: * Owning thread (the worker's ``finally``) → pop + ``client.close()`` via ``_close_request_openai_client``, exactly as before. * Stranger thread → ``_abort_request_openai_client`` (new): only ``shutdown(SHUT_RDWR)`` the pool sockets and log a deferred-close marker. The holder stays populated so the worker's eventual ``finally`` performs the real close from its own thread context, where the FD release races nothing. Applied symmetrically to both the non-streaming ``interruptible_api_call`` and the streaming variant — both routinely get hit by stranger-thread interrupts. The log field ``tcp_force_closed=N`` keeps its existing shape; the new abort path adds ``deferred_close=stranger_thread`` so production triage can distinguish the two close kinds.
…NousResearch#29507) Ten regressions across both prongs of the NousResearch#29507 fix, organised so each test names exactly which way the bug could come back: Prong 1 — ``force_close_tcp_sockets``: * ``shutdown_only_no_close`` is the smoking-gun assertion. If a future refactor adds back ``sock.close()`` to this helper, the FD-recycling race that wrote TLS bytes on top of ``kanban.db`` is back, and this trips. * ``uses_shut_rdwr`` pins that both halves are shut down (a half-close wouldn't unblock a worker stuck in ``recv``). * ``swallows_oserror_on_shutdown`` covers the already-shutdown case. * ``handles_multiple_pool_entries`` walks all pool connections. Prong 2 — thread-aware ``_close_request_client_once``: * ``stranger_thread_aborts_only_no_close`` simulates the asyncio_0 → Thread-1616 interrupt path: stranger drives abort, holder stays populated for the worker's eventual finally. * ``owner_thread_pops_and_full_close`` is the worker-thread path: pops + full close. * ``stranger_then_owner_close_sequence_runs_full_close_exactly_once`` replays the reporter's exact timeline at object level: abort runs once, full close runs once, holder ends empty. Agent surface: * ``_abort_request_openai_client_does_not_call_client_close`` pins that the new entrypoint shuts sockets and emits the ``deferred_close=stranger_thread`` marker but never calls ``client.close()``. * ``_abort_request_openai_client_null_client_is_noop`` defensive. End-to-end: * ``fd_recycle_window_closed_by_shutdown_only`` reproduces the race at object level — runs the abort path from a stranger thread and asserts that no ``close()`` ever fires, so the kernel can never recycle the FD under the owner's still-active reference.
…ousResearch#30852) `_deliver_kanban_artifacts` routes candidates through `BasePlatformAdapter.filter_local_delivery_paths` (added in 41d2c75), which rejects paths outside `MEDIA_DELIVERY_SAFE_ROOTS`. The two artifact-delivery tests create fixtures under `tmp_path`, which lives outside the cache roots — so under CI's hermetic HOME the filter silently dropped both fake files and the assertions on `images_uploaded` / `documents_uploaded` failed. Fix: monkeypatch `HERMES_MEDIA_ALLOW_DIRS=str(tmp_path)` in both tests so the safety filter accepts the fixtures. Production behaviour unchanged; test-side fix only. CI fail repro on origin/main: test (6) shard, both test_notifier_uploads_artifacts_on_completion and test_notifier_artifact_delivery_skips_missing_files.
…n skills_guard - _determine_verdict() returned 'caution' for medium/low-only findings, causing community skills with harmless patterns (e.g. path traversal notation, unpinned pip install) to be incorrectly blocked. Now returns 'safe' when only medium/low severity findings are present. - should_allow_install() allowed --force to override 'dangerous' verdict, contradicting documented behavior that --force does NOT override dangerous scan results. Added explicit check to prevent force-installing skills with dangerous verdict.
Follow-up to @sprmn24's verdict-logic fix. The previous block-message ended in 'Use --force to override' regardless of verdict — but as of the --force fix above, dangerous community/trusted skills can't be overridden by --force at all. The misleading hint sends users in a loop. Replace it with a specific message that tells them what the documented behavior actually is. Adds two regression tests covering the dangerous-verdict message shape and one that pins the existing --force hint for non-dangerous blocks.
…ousResearch#30860) * feat(portal): one-shot setup, status CLI, and Nous-included markers Four small Portal-aware surfaces that drive subscription value without adding friction for non-Portal users. - hermes setup --portal: one-shot Nous OAuth + provider switch + Tool Gateway opt-in. Shareable as a single command from docs/social. - hermes portal {status,open,tools}: small surface over Portal auth + Tool Gateway routing. Defaults to 'status' when no subcommand. - Tool picker (hermes tools): when the user is logged into Nous, mark Nous-managed provider rows with a star and 'Included with your Nous subscription'. Suppressed when not authed — non-subscribers see the picker unchanged. - BYOK setup hint: a single dim line 'Available through Nous Portal subscription.' appears when the user is being prompted for a paid API key (Firecrawl, FAL, ElevenLabs, Browserbase, etc.) AND the category has a Nous-managed sibling AND the user is not already authed to Nous. Suppressed in all other cases. Tested live end-to-end in an isolated HERMES_HOME with a simulated authed and unauthed user. Targeted suite (tests/hermes_cli/ test_tools_config.py + test_setup.py) passes 97/97. * fix: add portal to _BUILTIN_SUBCOMMANDS so plugin discovery fast-path skips it
PR 2362cc4 ("fix(gateway): enforce env variable template expansion on runtime config loaders") refactored `_load_service_tier` to read config via the new `_load_gateway_runtime_config` wrapper instead of opening `_hermes_home/config.yaml` directly. The `test_run_agent_passes_priority_processing_to_gateway_agent` test still only stubbed `_load_gateway_config` (the inner loader), so the runtime wrapper saw an empty config and `_load_service_tier` returned None, breaking the test: FAILED tests/gateway/test_fast_command.py::test_run_agent_passes_priority_processing_to_gateway_agent - AssertionError: assert None == 'priority' Fix: also stub `_load_gateway_runtime_config` to return the expected `agent.service_tier=fast` config, so the test once again drives the priority routing path it was written to verify. Confirmed reproducing on current main before the patch and passing after.
…ousResearch#30864) Closes NousResearch#30045. Based on @qike-ms's PR NousResearch#30141. Telegram status callbacks (lifecycle, compression, context-pressure) used to append a fresh bubble on every emit. Now adapter tracks {(chat_id, status_key) -> message_id}; first call sends, subsequent calls edit. Failed edits drop the cache entry and fall through to a fresh send. - gateway/platforms/telegram.py: send_or_update_status() (+34 LOC) - gateway/run.py: route _status_callback_sync through it when the adapter supports it; plain adapter.send() otherwise (+15 LOC) - 5 tests covering first send / edit-in-place / edit-failure fallback / distinct key & chat isolation
…-facing pages (NousResearch#30869) PR NousResearch#30860 added a one-shot Portal setup command and a small portal CLI surface. Update the docs so the new commands are discoverable without upgrading the tone of existing Portal mentions. - getting-started/quickstart.md: small tip near Choose a Provider pointing at 'hermes setup --portal' as the easiest fresh-install path. - user-guide/features/tool-gateway.md: lead the Get-Started section with 'hermes setup --portal' for fresh installs, keep 'hermes model' for already-configured users, and add 'hermes portal status / tools' to the activity-check commands. - user-guide/features/{web-search,image-generation,tts,browser}.md: the existing 'Nous Subscribers' tip blocks now name the one-shot command for new installs, keeping the existing 'hermes tools' path for users who only want to swap a single backend. - reference/cli-commands.md: register 'hermes portal' in the top-level command table, add a 'hermes portal' section with subcommands, and add '--portal' to the 'hermes setup' options table. Tone: each page already had a Portal mention. This PR keeps the per-page count to one and uses concrete CLI commands rather than promotional copy. Tool Gateway page is the one exception (the whole doc is about Portal).
… page describes (NousResearch#30874) Follow-up to NousResearch#30869. Adds Portal mentions on user-facing pages that naturally call for an LLM + tool credentials but didn't previously acknowledge Portal as a one-stop option. - getting-started/installation.md: tip after the 'after install' block pointing at 'hermes setup --portal' for users who want everything wired at once instead of piecewise via 'hermes model' + 'hermes tools'. - user-guide/configuring-models.md: small tip near the top — the page is literally about provider/model choice and previously had zero Portal mention. - user-guide/features/voice-mode.md: Prerequisites need both an LLM and TTS — a Portal subscription is the single setup that covers both. - user-guide/features/batch-processing.md: highlights Portal as a predictable-cost option for parallel agent runs that hit many APIs. - user-guide/features/api-server.md: backend needs models + tools; one Portal sub gives a fully-equipped OpenAI-compatible endpoint. - user-guide/windows-native.md: early-beta users on Windows benefit most from skipping per-tool Windows-key-juggling. - integrations/providers.md: updates the existing Tool Gateway tip and the Nous Portal section to mention the new commands. - user-guide/features/fallback-providers.md: Nous row in the provider table now lists 'hermes setup --portal' as the fresh-install path. Tone discipline: one Portal mention per page, concrete CLI commands (no marketing copy), always solving a problem the page itself sets up.
…tlement classifier (NousResearch#29344) ``_is_entitlement_failure`` over-matched on xAI 403s. xAI returns the same permission-denied ``code`` text for two distinct conditions: 1. Unsubscribed account ("active Grok subscription. Manage at https://grok.com" in the ``error`` field). 2. Stale OAuth access token ("OAuth2 access token could not be validated. [WKE=unauthenticated:bad-credentials]" in the ``error`` field). The classifier's "does not have permission + grok" substring heuristic treated both identically, so the credential-pool refresh path was short-circuited for case (2) — long-running TUI sessions stuck on a stale OAuth token surfaced a non-retryable client error and the user had to exit + reopen the TUI to recover (the startup-resolve path bypasses the classifier entirely, which is why bridge adapters with proactive refresh cadences didn't see this in practice). This patch adopts the reporter's recommended fix (option 1, tightest): honor xAI's explicit ``[WKE=unauthenticated:...]`` suffix and the ``OAuth2 access token could not be validated`` phrasing as authoritative "this is auth, not entitlement" signals. When either appears anywhere in the body's text fields, the classifier returns False eagerly — *before* the entitlement keyword checks run — so the refresh-on-401 path takes over and the existing loop-protection still guards against runaway refresh storms if the refresh itself fails. Two small adjustments fall out of this: * The haystack now also covers ``code`` and ``error`` keys directly, not just the ``message``/``reason`` shape ``_extract_api_error_context`` produces. Real runtime paths use the normalised shape, but the test suite and any future call sites that pass raw bodies get the same treatment. Backwards compatible: missing keys default to empty strings, the haystack still skips when everything is blank. * Both disambiguator checks fire BEFORE the entitlement keyword checks. If a future xAI body somehow lands with both an entitlement message AND the WKE suffix, the WKE suffix wins (correct — auth is recoverable; entitlement is not, and a refreshed token will surface the entitlement message on the next request anyway). Existing tests (``test_is_entitlement_failure_matches_real_xai_bodies``, ``test_is_entitlement_failure_false_for_unrelated_auth_errors``, ``test_recover_with_credential_pool_skips_refresh_on_entitlement_403``, ``test_recover_with_credential_pool_still_refreshes_genuine_auth_failure``) continue to pass unchanged — the unsubscribed-account path, the generic auth-error path, and the refresh-on-401 path are all left intact.
…uator (NousResearch#29344) Eleven new tests pinning the NousResearch#29344 fix. Layout mirrors the existing "Fix D" entitlement section so the bad-credentials disambiguator sits alongside the entitlement-block tests it complements. Classifier-level coverage: * ``test_is_entitlement_failure_false_for_bad_credentials_wke_suffix`` — verbatim shape from the reporter's wire capture (``{code: 'caller does not have permission', error: 'OAuth2 access token could not be validated. [WKE=unauthenticated:bad-credentials]'}``) ↦ classifier must return False so the refresh path runs. * ``test_is_entitlement_failure_false_for_wke_suffix_in_normalized_shape`` — same body after ``_extract_api_error_context`` has rewritten it to ``{reason, message}``. The disambiguator must fire in BOTH shapes; without this guard the production call site at ``_recover_with_credential_pool`` (which goes through the normalised extractor) would still misclassify. * ``test_is_entitlement_failure_false_for_any_wke_unauthenticated_variant`` — parametrised forward-compat: ``bad-credentials``, ``expired-token``, ``revoked``, ``some-future-reason``. xAI documents the prefix as stable, the suffix after the colon as a reason code that can grow; every variant under ``unauthenticated:`` must route to refresh. * ``test_is_entitlement_failure_false_via_oauth2_validation_phrase_alone`` — belt-and-braces guard: if a future API revision drops the WKE suffix but keeps "OAuth2 access token could not be validated", we still classify correctly. * ``test_is_entitlement_failure_wke_signal_overrides_entitlement_keywords`` — defensive: if a body ever carries BOTH the WKE suffix and entitlement language, the WKE signal wins. Auth is recoverable; entitlement isn't, and a refreshed token will resurface the entitlement message on the next request. * ``test_is_entitlement_failure_case_insensitive_wke_match`` — pins that the classifier lowercases the haystack so a future xAI build that uppercases the prefix doesn't reintroduce the bug. Recovery-path coverage (end-to-end through ``_recover_with_credential_pool``): * ``test_recover_with_credential_pool_refreshes_on_xai_bad_credentials_403`` — the headline test the reporter requested: a bad-credentials 403 with the exact wire body must call ``try_refresh_current()`` exactly once and ``_swap_credential`` once. Pre-fix this returned ``(False, _)`` because the entitlement classifier over-matched and short-circuited the refresh path. * ``test_recover_with_credential_pool_still_blocks_real_entitlement`` — companion regression guard for NousResearch#26847: a pure unsubscribed- account body (no WKE suffix, no OAuth2-validation phrase) must still surface as entitlement and skip refresh. The new disambiguator must not weaken the original loop-protection it was added to preserve. The scaffolding reuses ``_make_codex_agent``, ``_FakePool``, and the existing ``MagicMock`` patterns from the surrounding tests so the new section reads as a natural extension of "Fix D" rather than a separate test file.
…ousResearch#29344) _recover_with_credential_pool had a second classification site that blanket- treated any 403 against xai-oauth as entitlement (defense-in-depth for NousResearch#26847). That override defeated the new _is_entitlement_failure disambiguator from the parent commit — bad-credentials 403s still short-circuited the refresh path. Apply the same WKE-unauthenticated / OAuth2-validation-phrase guard at the override site so xAI's authoritative 'this is auth, not entitlement' signal wins there too. The NousResearch#26847 catch-all still triggers for genuine entitlement bodies that don't carry the disambiguator. Closes the end-to-end gap exposed by test_recover_with_credential_pool_refreshes_on_xai_bad_credentials_403.
…esearch#26045) (NousResearch#30877) Reproduction (production, 2026-05-14): two concurrent sessions on the same agent. Session A patches MEMORY.md directly via the patch tool, appending ~8KB of structured content (Vendor Master, Standing Orders, Pin Board) — none of it through the memory tool, so no § delimiters. Session B starts later with stale in-memory state (1 entry, ~331 chars). Session B calls memory(action=replace) on its one known entry. The tool's _read_file parses A's content as a single 8KB 'entry' (no § splits), then replace truncates that entry to B's new 333-byte content. ~8KB of structured content silently destroyed. The atomic-rename write path is fine in isolation. The bug is the implicit contract: the tool assumes MEMORY.md is exclusively a §-delimited list of small entries it wrote, but the v0.13 install runbook itself uses 'cat >> MEMORY.md' for onboarding, the patch tool edits the file directly, and operators do too. Fix: a drift guard in MemoryStore._detect_external_drift that fires on either signal: 1. Re-parse + re-serialize doesn't produce identical bytes (catches oddly-encoded delimiters / partial writes). 2. Any single parsed entry exceeds the store's whole-file char limit. The tool budgets the ENTIRE store against that limit (2200 chars for memory, 1375 for user), so no tool-written entry can legitimately be larger. An entry bigger than the store limit means an external writer dropped free-form content into what the tool will treat as one entry. When drift fires, _reload_target writes a .bak.<ts> snapshot of the on-disk file, then add/replace/remove refuse to flush. The original file stays untouched. The error dict surfaces the .bak path AND a remediation string ('integrate missing entries via memory(add=...) one at a time, then rewrite the file clean') so the model can act on it without escalating to the operator. Tests: - test_replace_refuses_on_drift, test_add_refuses_on_drift, test_remove_refuses_on_drift — all three mutators refuse - test_clean_file_does_not_trigger_drift — false-positive check - test_error_message_points_at_remediation — error string shape - test_drift_guard_also_protects_user_target — USER.md too - test_drift_backup_filename_is_unique_per_invocation — bak.<ts> naming pin 144 memory tests passing (was 137; +7). Fixes NousResearch#26045
…ousResearch#24912) (NousResearch#30879) User incident (Slack, 2026-05-13): user walked away mid-conversation, agent requested approval to run `rm -rf .git`, the prompt timed out after the gateway_timeout (default 300s), and the agent removed the .git folder on its own. Corroborated by an independent report from a Telegram user. The underlying code path was correct — `check_all_command_guards` returns `approved=False` with a BLOCKED message on both timeout and explicit deny, and `terminal_tool` surfaces that as `status=blocked` to the agent. The bug is at the model-interface layer: the message "BLOCKED: Command timed out. Do NOT retry this command." reads to some models as "try a different command achieving the same outcome." This commit changes only the model-facing message + the structured return shape: - Timeout message now explicitly names the three evasion paths the agent must avoid: retry, rephrase, AND achieve the same outcome via a different command. Ends with "Silence is not consent." - Explicit deny gets the same shape minus the silence-is-not-consent line (it WAS an explicit deny, not silence). - New structured fields on the return dict: `outcome` ("timeout" or "denied") and `user_consent` (always False on this branch) so plugins, hooks, and audit pipelines don't have to string-parse the message to distinguish the two cases. The mechanism that should already have prevented the original incident — timeout treated as deny, BLOCKED result, post hook fires with `choice="timeout"` — is unchanged. This commit hardens only the agent's reading of the result. Tests: - test_timeout_returns_approved_false_with_no_consent — pins the return shape on the Slack-shaped notify_cb-registered path - test_timeout_message_is_emphatic_against_retry_and_rephrase — pins the exact phrases the message must contain - test_explicit_deny_carries_same_no_consent_shape — same contract on explicit /deny - test_timeout_emits_post_hook_with_timeout_outcome — pins the post_approval_response hook payload so audit plugins can act 329 approval tests passing (4 new + 325 existing). Fixes NousResearch#24912
…/Messaging reference (NousResearch#30941) A small, self-contained section under 'Skip the API-key collection — Nous Portal' explaining what Portal gives you (300+ models + Tool Gateway), the one-shot install command, and how to inspect routing. No buzzwords, no comparison tables, no overselling. Positioned right after 'Getting Started' so it lands where someone scanning the README has just seen the install steps and is deciding their next move. Skippable by anyone who already knows their provider. The line 'You can still bring your own keys per-tool whenever you want' is the deliberate honesty rail — Portal is an option, not a funnel. Existing per-provider language elsewhere in the README is unchanged. Mirrored to README.zh-CN.md to keep the two READMEs in sync.
…indings Path.resolve() before any I/O and confine backup writes to the resolved parent directory. Adds explicit parent-equality assertions so static analyzers see the containment guarantee, and walks WAL/SHM sidecars through the same resolved-parent path so accidental .. segments are collapsed before shutil.copy2. Functionally equivalent to the original PR; preserves the corrupt bytes to <db>.corrupt.<ts>.bak in the same directory, still raises KanbanDbCorruptError from connect(). E2E with Stefan's exact hex header + malformed pages still passes. 163/163 kanban tests still pass.
…etion (NousResearch#30949) First scratch workspace creation on an install now emits a one-shot warning log + a 'tip_scratch_workspace' event on the task. Sentinel file at ~/.hermes/kanban/.scratch_tip_shown silences subsequent creations across the whole install. Behavior unchanged — scratch is still ephemeral by design. This just makes the design visible to new users (reported in user community: 'progress files vanished, no warning anywhere'). Docs (en + ko) updated to spell out 'Deleted when the task completes' on the scratch bullet and 'Preserved on completion' on worktree/dir.
* fix(tui): commit composer input bursts immediately Salvage the WSL/terminal multi-character input burst fix with focused regression coverage so delayed pseudo-paste buffers cannot reorder later edits. * fix(tui): keep newline input bursts on paste path Preserve paste handling for multi-character chunks with newlines while keeping repeated printable key bursts on the immediate composer path. * refactor(tui): share composer frame batch interval Use one frame-sized batching constant for parent updates, local renders, and input burst flushes.
* fix(tui): log parent gateway lifecycle exits Add parent-side breadcrumbs for TUI gateway shutdown and transport exits so future backend EOF/SIGTERM reports identify the parent action that caused them. * chore(tui): retrigger lifecycle logging checks Retry transient GitHub checkout failures on the lifecycle logging PR.
…31055) * fix(tui): ignore late thinking deltas after completion Prevent stale reasoning events from repainting the TUI status after a turn has already completed and the UI is idle. * test(tui): restore timers after thinking delta assertion Keep fake timer cleanup in a finally block so assertion failures cannot leak timer mode into later tests.
Changes the default of skills.guard_agent_created from False to True, so agent-created skills are scanned for malicious content before being loaded into the system prompt. Previously off by default with the rationale that the agent can use terminal() directly - but skills loaded via system prompt can inject prompt manipulation without ever calling terminal().
🚨 CRITICAL Supply Chain Risk DetectedThis PR contains a pattern that has been used in real supply chain attacks. A maintainer must review the flagged code carefully before merging. 🚨 CRITICAL: Install-hook file added or modifiedThese files can execute code during package installation or interpreter startup. Files: Scanner only fires on high-signal indicators: .pth files, base64+exec/eval combos, subprocess with encoded commands, or install-hook files. Low-signal warnings were removed intentionally — if you're seeing this comment, the finding is worth inspecting. |
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-attribute |
57 |
unresolved-import |
23 |
invalid-argument-type |
11 |
invalid-assignment |
8 |
unresolved-global |
5 |
unresolved-reference |
5 |
invalid-method-override |
3 |
unsupported-operator |
1 |
invalid-parameter-default |
1 |
unused-type-ignore-comment |
1 |
not-subscriptable |
1 |
First entries
plugins/platforms/discord/adapter.py:5004: [unresolved-global] unresolved-global: Invalid global declaration of `ExecApprovalView`: `ExecApprovalView` has no declarations or bindings in the global scope
plugins/platforms/discord/adapter.py:926: [invalid-argument-type] invalid-argument-type: Argument to function `atomic_json_write` is incorrect: Expected `int`, found `None`
gateway/run.py:16807: [invalid-argument-type] invalid-argument-type: Argument to bound method `AIAgent.run_conversation` is incorrect: Expected `str | None`, found `list[dict[str, Any]] | str`
plugins/platforms/discord/adapter.py:660: [unresolved-attribute] unresolved-attribute: Attribute `default` is not defined on `<special-form 'typing.Any'>` in union `Unknown | <special-form 'typing.Any'>`
plugins/platforms/discord/adapter.py:6131: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `None` in union `Unknown | None | dict[Unknown, Unknown]`
plugins/platforms/discord/adapter.py:696: [unresolved-attribute] unresolved-attribute: Attribute `Bot` is not defined on `None` in union `Unknown | None`
plugins/platforms/discord/adapter.py:3214: [unresolved-attribute] unresolved-attribute: Module `discord` has no member `Permissions`
plugins/platforms/discord/adapter.py:1981: [unresolved-attribute] unresolved-attribute: Module `discord` has no member `PCMVolumeTransformer`
tests/gateway/test_telegram_status_update.py:17: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
plugins/platforms/discord/adapter.py:4207: [invalid-argument-type] invalid-argument-type: Argument to constructor `int.__new__` is incorrect: Expected `str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc`, found `Any | None | str`
hermes_cli/oneshot.py:321: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `dict[str, Any]`, found `(list[dict[str, Any]] & ~AlwaysFalsy) | None`
tests/gateway/test_telegram_status_update.py:40: [unresolved-attribute] unresolved-attribute: Unresolved attribute `ChatType` on type `ModuleType`
tests/hermes_cli/test_nous_inference_url_validation.py:97: [invalid-argument-type] invalid-argument-type: Argument to function `_validate_nous_inference_url_from_network` is incorrect: Expected `str | None`, found `Literal[12345]`
tests/plugins/model_providers/test_opencode_go_profile.py:5: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
plugins/platforms/discord/adapter.py:4063: [unresolved-reference] unresolved-reference: Name `ExecApprovalView` used when not defined
tests/gateway/test_telegram_status_update.py:52: [unresolved-attribute] unresolved-attribute: Unresolved attribute `filters` on type `ModuleType`
plugins/platforms/discord/adapter.py:5004: [unresolved-global] unresolved-global: Invalid global declaration of `SlashConfirmView`: `SlashConfirmView` has no declarations or bindings in the global scope
tests/gateway/test_discord_clarify_buttons.py:30: [unresolved-import] unresolved-import: Module `plugins.platforms.discord.adapter` has no member `ClarifyChoiceView`
plugins/platforms/discord/adapter.py:5563: [unresolved-attribute] unresolved-attribute: Module `discord` has no member `ButtonStyle`
plugins/platforms/discord/adapter.py:3355: [unresolved-attribute] unresolved-attribute: Module `discord` has no member `app_commands`
plugins/platforms/discord/adapter.py:4215: [unresolved-reference] unresolved-reference: Name `UpdatePromptView` used when not defined
tests/gateway/test_discord_component_auth.py:25: [unresolved-import] unresolved-import: Module `plugins.platforms.discord.adapter` has no member `UpdatePromptView`
tests/gateway/test_discord_component_auth.py:23: [unresolved-import] unresolved-import: Module `plugins.platforms.discord.adapter` has no member `ModelPickerView`
plugins/platforms/discord/adapter.py:2729: [unresolved-attribute] unresolved-attribute: Module `discord` has no member `http`
tests/gateway/test_telegram_status_update.py:29: [unresolved-attribute] unresolved-attribute: Unresolved attribute `InlineKeyboardButton` on type `ModuleType`
... and 91 more
✅ Fixed issues (112):
| Rule | Count |
|---|---|
unresolved-attribute |
46 |
invalid-argument-type |
26 |
unresolved-import |
13 |
invalid-assignment |
11 |
unresolved-global |
5 |
unresolved-reference |
5 |
invalid-method-override |
3 |
unsupported-operator |
1 |
unused-type-ignore-comment |
1 |
invalid-parameter-default |
1 |
First entries
gateway/platforms/discord.py:4538: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `None | str` and `ThreadParticipationTracker`
cli.py:7897: [invalid-argument-type] invalid-argument-type: Argument to function `build_welcome_banner` is incorrect: Expected `int`, found `None | Unknown`
cli.py:14178: [unresolved-attribute] unresolved-attribute: Object of type `AIAgent & ~AlwaysFalsy` has no attribute `session_id`
gateway/platforms/discord.py:625: [unresolved-attribute] unresolved-attribute: Attribute `opus` is not defined on `None` in union `Unknown | None`
gateway/platforms/discord.py:976: [invalid-argument-type] invalid-argument-type: Argument expression after ** must be a mapping type: Found `Unknown | None | dict[Unknown, Unknown]`
gateway/platforms/discord.py:5002: [unresolved-global] unresolved-global: Invalid global declaration of `ClarifyChoiceView`: `ClarifyChoiceView` has no declarations or bindings in the global scope
tests/gateway/test_discord_component_auth.py:24: [unresolved-import] unresolved-import: Module `gateway.platforms.discord` has no member `SlashConfirmView`
cli.py:9369: [invalid-argument-type] invalid-argument-type: Argument to bound method `AIAgent._flush_messages_to_session_db` is incorrect: Expected `list[dict[Unknown, Unknown]]`, found `None`
cli.py:8352: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `dict[str, Any]`, found `Unknown | None`
gateway/platforms/discord.py:5506: [unresolved-attribute] unresolved-attribute: Attribute `Embed` is not defined on `None` in union `Unknown | None`
cli.py:9101: [invalid-assignment] invalid-assignment: Object of type `bool` is not assignable to attribute `verbose_logging` on type `AIAgent & ~AlwaysFalsy`
cli.py:6510: [invalid-assignment] invalid-assignment: Object of type `TodoStore` is not assignable to attribute `_todo_store` on type `AIAgent & <Protocol with members '_todo_store'> & ~AlwaysFalsy`
gateway/platforms/discord.py:4099: [unresolved-reference] unresolved-reference: Name `SlashConfirmView` used when not defined
cli.py:14489: [invalid-assignment] invalid-assignment: Object of type `Literal[True]` is not assignable to attribute `quiet_mode` on type `AIAgent | None`
tests/gateway/test_discord_model_picker.py:14: [unresolved-import] unresolved-import: Module `gateway.platforms.discord` has no member `ModelPickerView`
gateway/platforms/discord.py:926: [invalid-argument-type] invalid-argument-type: Argument to function `atomic_json_write` is incorrect: Expected `int`, found `None`
cli.py:6502: [invalid-assignment] invalid-assignment: Object of type `str` is not assignable to attribute `session_id` on type `AIAgent & ~AlwaysFalsy`
gateway/platforms/discord.py:401: [unresolved-attribute] unresolved-attribute: Object of type `object` has no attribute `decode`
gateway/platforms/discord.py:3353: [unresolved-attribute] unresolved-attribute: Attribute `app_commands` is not defined on `None` in union `Unknown | None`
gateway/platforms/discord.py:660: [unresolved-attribute] unresolved-attribute: Attribute `default` is not defined on `<special-form 'typing.Any'>` in union `Unknown | <special-form 'typing.Any'>`
gateway/platforms/discord.py:3751: [unresolved-attribute] unresolved-attribute: Attribute `Object` is not defined on `None` in union `Unknown | None`
cli.py:6340: [invalid-assignment] invalid-assignment: Object of type `str & ~AlwaysFalsy` is not assignable to attribute `session_id` on type `AIAgent & ~AlwaysFalsy`
cli.py:14497: [invalid-argument-type] invalid-argument-type: Argument to bound method `AIAgent.run_conversation` is incorrect: Expected `str`, found `Any | list[dict[str, Any]] | str`
tests/gateway/test_discord_clarify_buttons.py:30: [unresolved-import] unresolved-import: Module `gateway.platforms.discord` has no member `ClarifyChoiceView`
gateway/platforms/discord.py:3804: [unresolved-attribute] unresolved-attribute: Attribute `Forbidden` is not defined on `None` in union `Unknown | None`
... and 87 more
Unchanged: 4689 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
Changes
skills.guard_agent_createddefault fromFalsetoTrue. Agent-created skills loaded into system prompt without scanning can include prompt injection payloads affecting agent behavior without callingterminal().