Skip to content

test(gateway): harden config parity coverage#1

Closed
scorp323 wants to merge 7187 commits into
mainfrom
oracle/phase2-config-parity-hardening-20260703
Closed

test(gateway): harden config parity coverage#1
scorp323 wants to merge 7187 commits into
mainfrom
oracle/phase2-config-parity-hardening-20260703

Conversation

@scorp323

@scorp323 scorp323 commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds a gateway config ownership manifest and tests for config ownership/parity surfaces.
  • Hardens platform YAML/env, terminal bridge, model fallback, gateway precedence, agent gateway knobs, auxiliary/display config, plugin/env config, and API/webhook port-binding coverage.
  • Fixes the bounded source divergences exposed during the Phase 2 ladder in gateway/config.py / gateway/run.py.

Test plan

  • 105 passed, 9 warnings in 3.70s — targeted Phase 2 config-parity bundle on Hermes/M4.

Notes

  • Branch was pushed from Hermes/M4 after iPhone GitHub device-code OAuth approval.
  • package-lock.json and tests/gateway/test_session_hygiene.py were intentionally excluded and remain separate dirty local files on M4.
  • No runtime/gateway/cron/provider/model/vault/credential changes are part of this PR.

teknium1 and others added 30 commits June 20, 2026 10:46
Author email lacked a numeric-id prefix so the noreply auto-extraction
misses it; map it explicitly for PR NousResearch#43872 salvage.
…aining it

A nous inference_base_url that fails the host allowlist (e.g. a stale
stg-inference-api.nousresearch.com persisted before the allowlist
existed) was only replaced 'if refreshed_url:' — so when the validator
rejected the URL it left the poisoned value in place. The 'falling back
to default' warning fired but never took effect: every subsequent call,
including the auxiliary compression call, kept hitting the dead staging
endpoint and 401'd.

Reset to DEFAULT_NOUS_INFERENCE_URL when validation returns None at both
refresh sites in resolve_nous_runtime_credentials, so a poisoned
auth.json self-heals on the next refresh. The proxy adapter already did
this correctly; this brings the two auth.py sites in line.
…id (NousResearch#38763)

Context compression today rewrites the message list AND rotates the
session id — it ends the session, forks a parent_session_id child, and
renumbers the title (name -> name NousResearch#2). That moving identity key is the
root cause of a whole bug cluster: /goal lost (NousResearch#33618), pending response
lost at the split (NousResearch#14238), orphan sessions (NousResearch#33907), TUI sid desync
(NousResearch#36777), FTS search gaps + duplicate sidebar entries (NousResearch#45117), null
continuation cwd (NousResearch#42228), and title-rename dead-ends (NousResearch#48989). It also
forced a large defensive apparatus (compression lock, contextvar/env/
logging triple-sync, orphan finalization, gateway SessionEntry
re-propagation, tip projection) whose only job is surviving a
mid-conversation id change.

Add a compression.in_place config flag (default False during rollout).
When True, compaction rewrites the transcript and rebuilds the system
prompt but keeps the SAME session_id: no end_session, no child row, no
title renumber, no contextvar/logging re-sync, no memory/context-engine
session-switch. The conversation keeps one durable id for life, like
Claude Code / Codex. Compaction is lossy by design — the pre-compaction
transcript is summarized away, not archived.

The rotation path is unchanged when the flag is off (moved verbatim into
an else branch). Staged rollout: this PR ships the option behind a
default-off flag for live validation; a follow-up flips the default and
deletes the now-redundant rotation machinery, superseding the 14 open
band-aid PRs in this area.

- hermes_cli/config.py: add compression.in_place (default False), documented
- agent/agent_init.py: resolve the flag -> agent.compression_in_place
- agent/conversation_compression.py: branch compress_context() on the flag
- tests/run_agent/test_in_place_compaction.py: in-place invariants +
  rotation regression guard + config default

The pre-flush of current-turn messages (NousResearch#47202) runs in BOTH modes, so no
boundary data loss. Prompt-cache invariant preserved: the system-prompt
rebuild is the same single sanctioned invalidation that already happens
during compaction — no NEW invalidation. Message alternation preserved.
…dent end-to-end

Review (Codex + 3-agent parallel) found the first cut of in-place mode was
incomplete: it only updated the system prompt, so the persisted transcript
stayed 'full history + summary' and the next turn/resume reloaded the full
history and immediately re-compacted (a loop), and every downstream layer
that keyed off session-id rotation silently no-op'd. The session_id was
doing double duty as the 'compaction happened' signal. This wires the whole
path so removing rotation is actually complete:

Agent (agent/conversation_compression.py):
- In-place now DURABLY replaces the transcript: replace_messages(session_id,
  compressed) on the same row (the canonical store the gateway reloads from),
  not just update_system_prompt. Resume reloads the compacted set; no loop.
- Reset flush identity/cursor (_last_flushed_db_idx=0, _flushed_db_message_ids
  cleared) so next-turn appends diff against the compacted transcript.
- Expose a rotation-independent signal: agent._last_compaction_in_place, and
  in_place=True on the session:compress event.
- Fire the compaction-boundary hooks (context-engine on_session_start, memory
  manager on_session_switch, reason='compression') in BOTH modes — in-place
  passes the same id as parent so DAG/buffer state still checkpoints. Without
  this, memory/context plugins miss every in-place compaction.

Gateway auto-compress (gateway/run.py):
- Read agent._last_compaction_in_place; set history_offset=0 on rotation OR
  in-place (both return the compacted set, so slicing past the pre-compaction
  length would drop everything). Carry compacted_in_place in the result dict.
- No extra rewrite needed: the agent shares the gateway's SessionDB, so its
  replace_messages already updated the canonical store load_transcript reads.

Manual /compress (gateway/slash_commands.py):
- The throwaway /compress agent has no _session_db, so rewrite_transcript is
  the durable write. Previously gated behind 'if rotated:' which treated
  'id unchanged' as the NousResearch#44794 data-loss failure case and SKIPPED the rewrite
  — making /compress a silent no-op in in-place mode. Now rewrites on rotated
  OR in_place; the data-loss guard still fires only for the genuine
  no-rotation-AND-not-in-place failure.

Hygiene auto-compress already writes _compressed to the same id
unconditionally (its agent has no _session_db, can't rotate) — correct for
in-place, no change.

Tests (tests/run_agent/test_in_place_compaction.py):
- Assert the DURABLE transcript IS the compacted set after reload
  (get_messages_as_conversation == compacted), message_count==2, flush
  identity reset, and the rotation-independent signal set on in-place /
  unset on rotation. Rotation regression guard unchanged.

Verified: 64 tests green across in-place + rotation/persistence/boundary/
concurrent/failure-sync/command/cli suites; E2E both modes (durable replace,
gateway offset=0, rotation preserves old transcript); ruff clean. Still
default-off.
Parallel 3-reviewer cleanup of the in-place compaction code. Findings applied:

- perf: in-place mode no longer pre-flushes current-turn messages. The flush
  ran INSERTs that the immediately-following replace_messages(compressed)
  DELETE+reinsert discarded -- pure wasted writes per compaction. The
  current-turn tail survives via the compressor's compressed output
  (protect_last_n), not the flush. Verified no data loss; rotation still
  pre-flushes (its old session row is preserved, so the flush is real there).
- quality: hoist the two shared post-write steps (update_system_prompt +
  _last_flushed_db_idx = 0) below the if/else -- they ran in both branches
  against agent.session_id. Removes the easiest divergence bug.
- quality: compute the compaction-boundary locals (_old_sid, _is_boundary,
  _boundary_parent) ONCE instead of recomputing locals().get('old_session_id')
  and the "_old_sid or agent.session_id or ''" chain three times.
- quality: initialize compacted_in_place up front and assign
  agent._last_compaction_in_place directly, dropping the fragile
  locals().get('compacted_in_place') reflection.
- reuse: parse the in_place config flag with utils.is_truthy_value (the
  project's canonical truthy coerce) instead of a hand-rolled
  str().lower() in {...} (agent_init already imports from utils).

Dropped as false positives / out of scope: gateway getattr of agent internals
(established session_id pattern), dual result-dict carry (mirrors history_offset
etc.), stringly-typed "compression" (codebase-wide convention, no constant).

Behavior-preserving: 7 in-place tests (incl. 2 new flush-guard tests) + 26
rotation/boundary/persistence/command tests green; mutation check confirms the
durable-replace guard still binds (removing replace_messages fails the test);
ruff clean. Added test_in_place_skips_redundant_preflush /
test_rotation_still_preflushes to guard the perf change.
…e, not delete)

Teknium review: keeping one durable session id must NOT come at the cost of
destroying history. The prior in-place implementation used replace_messages,
which hard-DELETEs the pre-compaction turns (they also drop out of the FTS
index) — same id, but the original conversation is gone with no recovery path
and the summary becomes the only record. Rotation today is non-destructive
(the old session's full transcript survives under the old id); in-place must
match that durability contract, not weaken it.

Fix: compact in place by SOFT-ARCHIVING, reusing the existing messages.active
flag (the /undo soft-delete mechanic), instead of deleting:

- New SessionDB.archive_and_compact(session_id, compacted): in one atomic
  write, UPDATE messages SET active=0 on the live turns, then insert the
  compacted set as fresh active=1 rows. Nothing is deleted.
- The insert loop is extracted into a shared _insert_message_rows() helper so
  archive_and_compact and replace_messages don't duplicate the 60-line
  column/encoding block (extend-don't-duplicate).
- Agent in-place branch calls archive_and_compact instead of replace_messages.

Durability outcome (proven by test + E2E across repeated compactions):
- Live context load (get_messages_as_conversation / get_messages) filters
  active=1, so a resume reloads ONLY the compacted set — compaction still
  shrinks the live session.
- The pre-compaction turns stay on disk at active=0, recoverable via
  get_messages(include_inactive=True) / restore_rewound.
- They remain FTS-searchable: the messages_fts* triggers index on INSERT and
  remove on DELETE only — they do NOT key on active, and active=0 is a
  content-preserving UPDATE. session_search still finds them.
- Verified across TWO successive compactions: the 1st compaction's originals
  are still recoverable + searchable after the 2nd (answers the "no recovery
  path after the next compaction" concern directly).

message_count now reflects the LIVE (active/compacted) count, matching the
live load. replace_messages keeps its DELETE semantics (still correct for
/retry, /undo) and gains a docstring note pointing compaction at the
non-destructive method.

Tests: test_in_place_keeps_same_session_id strengthened to assert the 8
seeded originals survive at active=0 alongside the 2 compacted rows AND stay
FTS-searchable. Mutation check: swapping archive_and_compact back to a hard
DELETE fails the test, so the non-destructive contract is bound. 285
hermes_state + in-place tests green; rotation/persistence/compress-command/cli
suites green; ruff clean.
…ion_search

Follow-up to the soft-archive durability fix. Reusing the rewind/undo active=0
flag for compaction-archived turns inherited the wrong search semantics: undo
rows are intentionally HIDDEN from session_search (the user took them back), but
compaction-archived turns must stay DISCOVERABLE — that is the whole point of
Teknium's "searchable / recoverable" requirement. As built, search_messages
defaulted to WHERE active=1, so after in-place compaction the pre-compaction
turns were in the FTS index but filtered out of the default search. (The earlier
"searchable" claim only held for a raw FTS query / include_inactive=True, not
the actual session_search tool.)

Empirically confirmed the gap: search 'HMAC' returned 2 hits before compaction,
1 after (only the summary's mention) — the originals were hidden.

Fix — a `compacted` flag distinct from `active`, giving a 3-way state:
- active=1, compacted=0  → live context (normal)
- active=0, compacted=1  → compaction-archived: OUT of live context, IN search
- active=0, compacted=0  → rewind/undo: OUT of live context, OUT of search

Changes:
- messages.compacted INTEGER NOT NULL DEFAULT 0 added to SCHEMA_SQL. Declarative
  _reconcile_columns adds it on existing DBs — no version bump (plain column add).
- archive_and_compact: UPDATE … SET active=0, compacted=1 (was active=0 only).
- search_messages: default WHERE active=1 → (active=1 OR compacted=1), on BOTH
  the main FTS5 path and the trigram CJK path. include_inactive=True still
  returns everything. The short-CJK LIKE fallback already returns all rows
  (no active filter) — unchanged.
- Docstrings on archive_and_compact + search_messages document the 3-way state.

Verified: after compaction, session_search default finds the archived originals
(ids 1 & 4); rewind/undo rows stay hidden by default (recoverable via
include_inactive); live context still excludes both. 322 in-place + hermes_state
tests and 46 session_search tests green; ruff clean. Mutation check: reverting
the search WHERE to active-only fails the new searchable test.

(Surfaced by the question "is search semantic or only FTS?" — answer: session
search is FTS5 keyword/BM25 only, no embeddings over the transcript; semantic
retrieval lives in the optional memory-provider layer. Tracing that confirmed
the active-only filter gap above.)
Review nit (yoniebans): the config.py comment still said compaction is
'lossy: the pre-compaction transcript is discarded, matching Claude Code /
Codex' — leftover from the original destructive design. The shipped behavior
is soft-archive: lossy for the LIVE context (what the model reloads), but the
pre-compaction turns are kept on disk (active=0, compacted=1), searchable via
session_search and recoverable. Comment now says so. Comment-only; no behavior
change.
…ion event (NousResearch#49738)

Async-delegation completions (delegate_task(background=true)) and
background-process completions (terminal notify_on_complete) re-enter the
originating session as internal MessageEvents. When the session was busy,
_handle_active_session_busy_message treated them like a user TEXT message and
the default busy_input_mode='interrupt' aborted the active turn (and sent a
'Interrupting current task' ack) — the opposite of the design invariant that a
completion surfaces as a new turn only when idle.

Short-circuit internal events to return False so the base adapter queues them
silently (it already excludes internal events from debounce), cascading them as
the next turn after the current one finishes.
…idated return (NousResearch#49734)

* feat(delegation): single-task delegate_task always runs in the background

The model no longer decides whether a subagent runs in the background — a
single-task delegate_task from the top-level agent is now always dispatched
async, so the parent turn returns immediately and the subagent's result
re-enters the conversation when it finishes.

- run_agent._dispatch_delegate_task (the live model path) forces
  background=True for top-level single-task calls; the schema-level
  `background` param is ignored.
- A batch (tasks with >1 item) stays synchronous (fan-out can't go async).
- A delegation from an orchestrator subagent (depth > 0) stays synchronous —
  it needs its workers' results within its own turn.
- The function-level default is unchanged, so direct Python callers/tests keep
  the historical synchronous behavior.
- On async-pool capacity rejection, single-task now falls through to a
  synchronous run instead of erroring (the child stays attached for interrupt
  propagation; detach happens only on a successful dispatch).
- Schema `background` param marked deprecated/ignored; tool description
  updated to state the always-background single-task rule.

* feat(delegation): all delegate_task fan-out runs in the background

Extend the always-background behavior to the full fan-out. A batch is now
dispatched as N independent async subagents (one handle each), instead of
running synchronously. Single task and batch both return immediately; each
subagent's result re-enters the conversation as its own message when it
finishes.

- delegate_task: when background is set, loop over ALL built children and
  dispatch each via dispatch_async_delegation; return a combined handle block
  (count + per-task delegation_ids). Children the async pool rejects (at
  capacity) run synchronously inline and are reported alongside the dispatched
  handles, so nothing is silently dropped.
- run_agent._dispatch_delegate_task + registry handler: force background for
  any top-level model delegation (single OR batch); orchestrator subagents
  (depth > 0) still run synchronously since they need workers' results within
  their own turn.
- Removed the v1 'batch async not supported' rejection.
- Tool description updated: BOTH MODES RUN IN THE BACKGROUND.
- Tests updated to assert batch fan-out dispatches each task async (verified
  E2E: 3-task batch -> 3 independent completion-queue events).

* fix(delegation): background fan-out joins and returns one consolidated block

Correct the fan-out semantics: a backgrounded batch is dispatched as ONE
async unit (one handle, one async-pool slot), not N independent dispatches.
The unit runs all children in parallel, waits on every one, and emits a
SINGLE completion event carrying the consolidated per-task results. The chat
is never blocked; when all subagents finish, their full summaries re-enter
the conversation together as one message.

- async_delegation.dispatch_async_delegation_batch + _finalize_batch: a batch
  occupies one slot; its runner returns the combined {results:[...]} dict and
  one event with the full results list is pushed to the completion queue.
- delegate_tool: extract the sync execution+aggregation into
  _execute_and_aggregate(); background dispatches it via the batch unit and
  returns one handle; on pool-capacity rejection it runs the batch inline.
- process_registry._format_async_delegation: render a consolidated multi-task
  block (TASK i/N + per-task summary) when the event carries is_batch/results.
- Tests updated; E2E verified: 3-task batch -> immediate return -> one combined
  completion block with all three summaries.
… (401/403)

When the active provider returns a 401/403 that survives its per-provider
credential-refresh attempt (revoked OAuth, blocked/expired key, or an
account pinned to a dead/staging inference endpoint), the conversation
loop now escalates to the configured fallback chain instead of dead-ending.

Before: the generic failover dispatch fired only for {rate_limit, billing};
auth/auth_permanent fell through to 'switch providers manually' advice and
never called _try_activate_fallback(). A user whose primary credential was
broken kept thrashing on the same dead credential every turn — the main
agent appeared 'stuck in fallback mode' while never actually failing over.
This also affected auxiliary tasks (compression, vision, title-gen), since
auto-resolved aux follows the main provider.

After: a persistent auth failure with a configured fallback chain switches
to the next provider (mirroring the rate-limit/billing failover path),
guarded one-shot per attempt by TurnRetryState.auth_failover_attempted.
When no fallback is configured the behavior is unchanged — it falls through
to the existing terminal handling and provider-specific troubleshooting
guidance.

Tests: test_auth_provider_failover.py — 401/403 classify as auth, the
gating condition fires only with a chain present + guard unset, the guard
blocks repeats, and non-auth (500) errors do not trigger auth failover.
…graded session

When the auxiliary summary call fails with an authentication/permission
error (HTTP 401/403), context compression now ABORTS and preserves the
session unchanged instead of rotating into a child session with a
placeholder summary.

Before: a 401 (invalid/blocked key, or a token pointed at the wrong
inference host) fell through every transient-error check to 'return
None', and because compression.abort_on_summary_failure defaults False,
compress() took the static-fallback path and rotated the session anyway
(messages N->N). The user landed on a fresh-but-broken session that kept
failing the same way — paying for a full-context API call each turn with
no useful compression.

After: _generate_summary classifies 401/403 as a non-recoverable auth
failure (_last_summary_auth_failure) and compress() aborts on it
regardless of abort_on_summary_failure. A distinct auxiliary summary_model
that 401s still retries once on the main model first (its dedicated creds
may be the only broken thing); the abort only sticks when the main model
itself auth-fails or the fallback also auth-fails. The existing
_last_compress_aborted handling in conversation_compression.py already
skips rotation and emits a warning, so no session rotation occurs.

Tests: TestAuthFailureAborts — 401/403 flagging, compress() aborts despite
flag=False, non-auth failures keep the historical fallback path, and
aux-model auth failure recovers on main without aborting.
Adds a ConvertTo-LongPath helper to install.ps1 that expands a Windows 8.3
short path (e.g. C:\Users\FIRST~1.LAS) back to its long form via
Scripting.FileSystemObject. Paths without a "~<digit>" component are returned
unchanged (no COM round-trip), and any COM failure falls back to the input.

Adds an AST-loaded unit test that exercises the helper without executing the
installer body (pass-through, null/empty, and graceful fallback).
… don't abort

On a Windows profile whose folder name contains a space (e.g. "First Last"),
Windows can expose %TEMP%/%TMP% as an 8.3 short path
(C:\Users\FIRST~1.LAS\AppData\Local\Temp). PowerShell's FileSystem provider
mishandles the "~1.ext" component when the path reaches a provider cmdlet such
as `Tee-Object -FilePath`, throwing:

  An object at the specified path C:\Users\FIRST~1.LAS does not exist.

Every Node/Electron install+build stage streams its log to %TEMP% via
Tee-Object, so they all abort with that error (browser-tools npm, Playwright,
TUI npm, and the hard-failing desktop build), while the Python/uv stages --
which never write a side log to %TEMP% through a provider cmdlet -- succeed.

Normalize %TEMP%/%TMP% to their long form once, up front, so every downstream
cmdlet and child process sees a path the provider can resolve.

Fixes NousResearch#39308
…o strict providers

Per-message timestamp metadata injected by _apply_persist_user_message_override
leaks into the Chat Completions payload sent to the provider. Strict OpenAI-compatible
providers (e.g. Fireworks-backed endpoints like OpenCode Go 'glm-5.2', Mistral, Kimi)
reject this schema-foreign field with HTTP 400:

  Extra inputs are not permitted, field: 'messages[0].timestamp'

The ChatCompletionsTransport.convert_messages already strips known internal-only
fields (tool_name, _-prefixed scaffolding keys, codex_reasoning_items, etc.) — add
timestamp to that list.

Closes NousResearch#47868
Add a regression test for NousResearch#47868 asserting convert_messages strips the
internal per-message timestamp field, plus the identity-return path for
timestamp-free message lists. Map x7peeps for the release attribution gate.
In Docker the install tree (/opt/hermes) is read-only, so npm install for
the WhatsApp bridge fails with EACCES. Add resolve_whatsapp_bridge_dir() in
whatsapp_common.py: when the install dir is read-only, mirror the bridge
source into a writable HERMES_HOME location and use that. Both the
adapter and the 'hermes whatsapp' CLI resolve through the shared helper so
the install and runtime paths agree.

Fixes NousResearch#49561
Follow-up for salvaged NousResearch#49654: unit tests for resolve_whatsapp_bridge_dir()
(writable passthrough, read-only mirror, existing-mirror reuse) and the
AUTHOR_MAP entry for the contributor.
…aram (NousResearch#49854)

The kanban-worker skill taught kanban_complete with three full examples but
never mentioned the artifacts=[...] parameter added in NousResearch#27813 — so a worker
reading the skill had no way to learn it can ship a chart/PDF/image as a
native upload to the subscriber's chat.

Adds a 'Shipping deliverables' section covering absolute-path rules, the
inline-vs-file extension behavior, and the trap that the notifier reads the
top-level artifacts list (NOT metadata.*).
The dispatcher treated workspace_kind=worktree as metadata only and never
ran 'git worktree add', so every worktree task ran in the main repo checkout
instead of an isolated worktree — concurrent tasks silently shared one tree
and contaminated each other.

This materializes a real linked worktree at <repo>/.worktrees/<task_id> on
branch wt/<task_id> when resolve_workspace() handles a worktree task, treats a
repo-root workspace_path as shorthand for that location, persists the derived
workspace/branch back onto the task row, and — on rerun/redispatch — detects an
already-materialized linked worktree (via git-common-dir) and reuses it instead
of nesting a second .worktrees/<id> inside it.
Follow-up to the salvaged worktree-materialization fix. When a worktree
task has no explicit workspace_path, resolve the anchor from the board's
default_workdir (a git repo) and materialize <repo>/.worktrees/<id> per
task, instead of silently rooting under the dispatcher's CWD (whatever
directory launched the gateway, e.g. the Hermes checkout). If no
default_workdir is configured, raise with a clear message rather than
guessing from CWD.

Adds AUTHOR_MAP entry for the salvaged commit.
On Windows, hermes writes writer.bat (@echo off / hermes -p writer %*)
with CRLF endings instead of the POSIX writer shell script. The test
hardcoded the POSIX path and exact bytes, so it failed on Windows hosts.
Assert on stripped non-empty lines per platform, making it line-ending-
and OS-independent.
…ousResearch#49890)

doctor's npm audit hardcoded PROJECT_ROOT/scripts/whatsapp-bridge. In
read-only Docker installs the bridge deps live in the writable HERMES_HOME
mirror (NousResearch#49561), so node_modules was never found there and the bridge audit
silently skipped. Resolve the dir through the shared
resolve_whatsapp_bridge_dir() helper so doctor audits where deps actually
install. Falls back to the install-tree path if the helper is unavailable.
…ng them

When the agent is busy and the user sends multiple text follow-ups, the
interrupt-mode and steer-fallback path stored them via
merge_pending_message_event(merge_text=True), which newline-joins
consecutive TEXT messages into a SINGLE pending turn — collapsing two
separate user messages into one mashed-together turn and destroying the
message boundaries the user sees (NousResearch#43066 sub-bug 2).

Route that storage through _queue_or_replace_pending_event (the same FIFO
infrastructure used by busy queue-mode and /queue) so each follow-up gets
its own next-turn slot in arrival order, while still preserving
photo-burst / album merge semantics for media. Pure queue-mode already
used FIFO; this brings the interrupt/steer-fallback path in line.

The sibling defect in NousResearch#43066 (assistant messages lost after compaction)
was already fixed on main by the identity-tracking flush rewrite (NousResearch#46053)
plus the pre-rotation flush (NousResearch#47202), so this only addresses the
remaining busy-message-merge half.

Co-authored-by: KiruyaMomochi <65301509+KiruyaMomochi@users.noreply.github.com>
… call

The subagent-demotion busy-handler test asserted the internal
merge_pending_message_event call, which the FIFO refactor replaced with
_queue_or_replace_pending_event. Assert the behavioral outcome (the
follow-up lands in the pending slot for the next turn) instead — same
fix already applied to the two steer-fallback tests.
…s rotation

Three state-loss bugs at the compression rotation boundary, fixed together
because they all live in the same ~80-line rotation block:

- NousResearch#33618: a persistent /goal did not follow the rotation. load_goal does a
  flat per-session lookup with no lineage walk, so a goal silently died when
  compression minted a fresh child id. Added migrate_goal_to_session() and
  call it after the child session is created (move-not-copy: the parent row
  is archived as cleared so exactly one active goal row exists).

- NousResearch#33906/NousResearch#33907: if the child create_session raised (FK constraint,
  contended write), the outer handler only warned and let the agent continue
  on the NEW id — which has no row in state.db — producing an orphan session.
  Now the rotation rolls agent.session_id back to the still-indexed parent
  (reopening it) instead of stranding the conversation on a phantom id.

- NousResearch#27633: the compaction-boundary on_session_start notification omitted the
  platform kwarg, so context-engine plugins saw source=unknown for every
  message after the boundary. Forward platform (matching the initial
  session-start call in agent_init.py).

Co-authored-by: denisqq <21260182+denisqq@users.noreply.github.com>
Co-authored-by: zccyman <16263913+zccyman@users.noreply.github.com>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Closes NousResearch#48835

The bundled himalaya skill and its website docs documented command
syntax that does not match Himalaya CLI v1.2.0.

Verified against pimalaya/himalaya v1.2.0 source:
- message move: MessageMoveCommand declares target_folder BEFORE
  envelopes (src/email/message/command/move.rs) -> usage is
  '<TARGET> <ID>...', so 'move 42 "Archive"' is wrong; correct is
  'move "Archive" 42'.
- message copy: same ordering in copy.rs.
- attachment download: AttachmentDownloadCommand exposes the flag as
  '-d, --downloads-dir <PATH>' (src/email/message/attachment/command/
  download.rs), not '--dir'.

Fixed in all three surfaces that carried the wrong examples:
- skills/email/himalaya/SKILL.md
- website/docs/.../email-himalaya.md
- website/i18n/zh-Hans/.../email-himalaya.md
…tive PowerShell

The Chinese README still told Windows users to install WSL2 and run
the Linux installer. Hermes now ships a native PowerShell install
script, so replace the outdated WSL2-only note with the direct
PowerShell one-liner.

Fixes: documentation accuracy / Windows onboarding
annguyenNous and others added 21 commits June 21, 2026 19:02
logging.basicConfig() in TrajectoryCompressor.__init__ overrides the
root logger configuration every time the class is instantiated. Library
code should use logging.getLogger(__name__) and let the application
entry point configure the root logger.

Fixes inconsistent log formatting when the compressor is used alongside
other logging configuration in the gateway.
… main

Same library-code anti-pattern as the compressor fix: MiniSWERunner.__init__
called logging.basicConfig(), overriding the application's root logger config
every time a runner was instantiated. Moved the call into main() (the CLI
entry point) where it belongs; __init__ now only does getLogger(__name__).
Standalone verbose logging is preserved.
Remove the dashboard --insecure auth-bypass, add an MCP persistence guard +
IOC blocklist, and raise the API-server key entropy floor.

Driven by the June 2026 hermes-0day campaign (r/hermesagent, live 854.media
instance): scanners find exposed Hermes dashboards/API servers, drive the
root agent to plant a 'command: bash' MCP entry that appends an attacker SSH
key to authorized_keys, which cron + startup then re-execute every tick.

- dashboard: --insecure no longer disables the auth gate. should_require_auth
  returns True for every non-loopback bind; a public bind ALWAYS requires an
  auth provider (bundled password provider or OAuth). --insecure kept as a
  warned no-op for backward compat. Fail-closed error now points at the
  password provider, not at --insecure.
- mcp_security: validate_mcp_server_entry now also rejects shell payloads that
  write to OS persistence surfaces (authorized_keys/.ssh/pam.d/sudoers/cron/
  rc files) and hard-rejects a hermes-0day IOC blocklist (attacker SSH key +
  source IPs) anywhere in command/args/env. Runs at save AND spawn time.
- api_server: raise network-bind API_SERVER_KEY entropy floor 8->16 chars;
  warn when a network-accessible API server runs an unsandboxed local backend.
The s6 dashboard entrypoint and docker integration tests relied on
HERMES_DASHBOARD_INSECURE=1 to bring up a 0.0.0.0 dashboard with no auth
provider. With --insecure now a no-op (auth gate mandatory on non-loopback
binds), that path fails closed.

- s6 dashboard/run: drop --insecure derivation; warn that the env is a no-op
  and point operators at HERMES_DASHBOARD_BASIC_AUTH_* / OAuth.
- docker tests: supervision tests now register the bundled basic password
  provider (HERMES_DASHBOARD_BASIC_AUTH_USERNAME/_PASSWORD) so the gate has a
  provider and the dashboard binds. Rewrote the insecure-opt-out test to
  assert fail-closed (dashboard does NOT serve) instead of gate-bypass.
- docs (en + zh-Hans): HERMES_DASHBOARD_INSECURE documented as deprecated
  no-op; basic-auth is the zero-infra way to authenticate a containerized
  public dashboard.
Surface dangerous host/deployment posture at gateway startup so operators get
the 'you're exposed' signal the June 2026 MCP-config persistence campaign
victims never had. Warn-only — never blocks startup, never raises.

Checks (each independently fail-safe):
- Running as root (POSIX uid 0)
- SSH daemon with PasswordAuthentication enabled (incl. the 'yes' default)
- Running in a container with no persistent volume mount over HERMES_HOME
- Network-accessible API server with no API_SERVER_KEY

New module hermes_cli/security_audit_startup.py; invoked once per process from
start_gateway() right after setup_logging(). Cross-platform (root/SSH checks
no-op on Windows). Idea: @cthulhu.
…r grace

A daemon that ignores or stalls in its SIGTERM handler currently survives the
process-registry reap and leaks until reboot (observed as agent-browser
daemons accumulating to EMFILE on long-running gateways). _terminate_host_pid
now snapshots the tree, SIGTERMs it, waits a bounded grace window
(terminal.daemon_term_grace_seconds, default 2.0s, 0 disables), then SIGKILLs
any survivor. The recycled-PID identity guard still gates the whole path, so
escalation never reaches a stranger; Windows is unchanged (taskkill /F is
already a hard kill).

Config lives in config.yaml (terminal.daemon_term_grace_seconds), NOT an env
var, per the .env-secrets-only policy.

Implements the SIGKILL-escalation idea from @tkwong's NousResearch#15008, reworked onto the
current _terminate_host_pid tree-kill path (the original predated it) and
config-gated instead of env-var-gated.

Co-authored-by: Benjamin Wong <tkwong@inspiresynergy.com>
…cs survivors

Live testing against a real SIGTERM-ignoring process TREE (parent + children,
the agent-browser daemon + renderer shape) revealed psutil.wait_procs's
gone/alive partition mis-handles a parent/child tree: it reaps via
Process.wait() and could mark targets gone/alive inconsistently across the
tree, leaving survivors un-killed (flaky — sometimes the parent lived,
sometimes a child). Replace it with: sleep out the grace window, then
directly re-probe every captured target (_proc_alive, treating zombies as
dead) and SIGKILL any that's still running. Add a multi-child-tree regression
test. 6/6 escalation tests green across repeated runs; the real-tree E2E now
kills the full tree 6/6 runs.
…ousResearch#50497)

The welcome banner's 'Available Tools' merged in every toolset from the
global check_tool_availability() registry walk, regardless of whether it
was enabled for the current platform. On a Blank Slate CLI (file +
terminal only) that surfaced discord / feishu / kanban tools the agent
was never actually given — they are not in the agent's tool schema, but
the banner displayed them, making it look like they were exposed.

- Filter the unavailable-toolset merge to toolsets actually in
  enabled_toolsets (a toolset that's enabled but has unmet deps still
  legitimately shows as disabled/lazy).
- Gate the 'Available Skills' section on the skills toolset being
  enabled — when it's off, the agent can't load any skill, so show
  'Skills toolset disabled' instead of the on-disk catalog.

When enabled_toolsets is empty (older callers), behavior is unchanged.

Validation: blank-slate banner now shows only file + terminal and
'Skills toolset disabled'; a skills-enabled banner still lists the
catalog. Added regression tests; full banner suite green (15/15).
…providers (NousResearch#50492)

* feat(providers): remove google-gemini-cli + google-antigravity OAuth providers

Google now actively bans accounts for third-party tools that piggyback on
Gemini CLI / Antigravity / Code Assist OAuth, and because abuse prevention
sits at a backend layer the ban can extend to the entire Google account
(Gmail/Drive), with a second violation being permanent.
Ref: google-gemini/gemini-cli#20632

Removes both OAuth inference providers entirely (modules, provider profiles,
auth/runtime/config/models wiring, the /gquota Code Assist quota command,
the antigravity-cli optional skill, desktop + docs surface in en + zh-Hans).
The API-key 'gemini' provider (GOOGLE_API_KEY/GEMINI_API_KEY against
generativelanguage.googleapis.com) is unaffected and stays fully supported.

* fix(skills): keep the antigravity-cli skill — only the OAuth provider is removed

The antigravity-cli optional skill orchestrates the external `agy` binary as
a coding-agent tool via the terminal tool — it does NOT wrap Hermes inference
through the banned google-antigravity OAuth provider, so it carries none of
the account-ban risk that motivated removing that provider. Restore the skill,
its docs page, the sidebar entry, and the optional-skills catalog row. The
google-antigravity / google-gemini-cli inference providers stay fully removed.
gateway/platforms/telegram.py no longer exists (adapters moved to
plugins/platforms/<name>/adapter.py) and telegram no longer uses the
scoped-lock pattern. Point the token-lock canonical-pattern reference to
plugins/platforms/irc/adapter.py, which acquires the lock in connect()
and releases it in disconnect() — and is already cited as a canonical
example in ADDING_A_PLATFORM.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ns/platforms

Sibling-site follow-up to the AGENTS.md token-lock fix (NousResearch#50481). Platform
adapters migrated from gateway/platforms/<name>.py to
plugins/platforms/<name>/adapter.py; a handful (signal, weixin, bluebubbles,
qqbot, yuanbao, msgraph_webhook, webhook, api_server) still live in
gateway/platforms/.

- adding-platform-adapters.md: new-adapter creation path + reference-impl table
- gateway-internals.md: rewrite the adapter tree to reflect the actual split
- zh-Hans mirrors of both kept in parity
- scripts/release.py: add TutkuEroglu to AUTHOR_MAP (CI gate)
…usResearch#50534)

Plugins shelling out to bare `hermes` via the terminal tool hit
`command not found` (exit 127) when the gateway was launched without the
hermes install dir on PATH (systemd, service managers, cron, desktop
launchers) — even though `hermes` works in the user's own interactive
terminal, which sources the shell rc that exports that dir.

The terminal tool's subshell PATH was the agent process PATH plus a
static set of system dirs (_SANE_PATH); it never included wherever the
hermes console-script actually lives (~/.local/bin, the venv bin/Scripts,
pipx, nix). Resolve that dir once (which/argv0/sys.executable) and
prepend-if-missing it so bare `hermes` resolves regardless of launch
method.
NousResearch#50499)

* feat(cli): /reasoning full to show complete thinking, not 10-line clamp

The post-response Reasoning recap box hard-clamped long thinking to the
first 10 lines, so there was no way to see the full reasoning trace after
a turn (live streaming already shows it in full). Add display.reasoning_full
(default off) plus /reasoning full|clamp to toggle it at runtime; the clamp
truncation note now points at the command. Addresses repeated user requests
to show all thinking tokens.

* test(gateway): de-snapshot /reasoning help assertion

The test froze the exact args-hint literal '/reasoning [level|show|hide]',
which the new full/clamp args change to '[level|show|hide|full|clamp]'.
Convert to an invariant: assert /reasoning is in help and carries its core
args, not the exact hint string.

* feat(tui): /reasoning full|clamp parity in tui_gateway

The classic-CLI reasoning_full toggle had no TUI equivalent — typing
/reasoning full in the TUI fell through to parse_reasoning_effort and
errored. The TUI renders thinking as an expand/collapse section (no fixed
10-line recap), so map full -> sections.thinking=expanded (raw, uncapped
via thinkingPreview mode='full') and clamp -> collapsed, persisting
display.reasoning_full for cross-surface config consistency.
…h#50509)

* feat(cli): /prompt — compose your next prompt in $EDITOR

Adds /prompt (alias /compose): opens $VISUAL/$EDITOR on a temp markdown
file so you can hand-edit a multi-line prompt, then sends the saved buffer
as the next agent turn. Text after the command pre-seeds the buffer; an
empty save cancels. Reuses the one-shot _pending_agent_seed the interactive
loop already consumes (same mechanism as /blueprint), so no changes to the
input event loop or message pipeline. CLI-only.

* feat(tui): /prompt slash command opens $EDITOR (parity with CLI)

The TUI already opens $EDITOR via Ctrl+G (openEditor), but had no /prompt
slash command like the classic CLI. Wire openEditor into the slash handler
context and register /prompt (alias /compose) to call it; inline text after
the command is dropped into the composer first so it carries into the editor,
matching the CLI's /prompt <text>.
…ind (NousResearch#50551)

When `hermes dashboard --host 0.0.0.0` is run interactively with the auth
gate engaged but no DashboardAuthProvider configured, prompt to set up the
bundled username/password provider on the spot (or point at `hermes dashboard
register` for OAuth) instead of only emitting the fail-closed error.

- main.py: `_maybe_setup_dashboard_auth_interactively()` runs before
  start_server. No-ops on loopback binds, when a provider is already
  registered, or when stdin/stdout isn't a TTY (Docker/s6, CI, piped runs) so
  the fail-closed SystemExit stays the backstop for unattended deploys. On the
  password path it writes dashboard.basic_auth.{username,password_hash,secret}
  to config.yaml (scrypt hash, never plaintext), then force-rediscovers
  plugins so the basic provider registers before the gate check.
- web_server.py: fix the fail-closed hint — it told operators to set
  `dashboard_auth.basic.username` but the provider reads `dashboard.basic_auth`.
- docs: note the interactive setup under Fail-closed semantics.

No new env vars; reuses the existing dashboard.basic_auth config surface.
…arch#11)

Preserves M4 dirty worktree after GitHub source-parity recovery.\n\nIncludes gateway session-hygiene handoff/fresh-start support, local fallback context guard, stale vision payload pruning, Telegram button/session handling, and focused regression tests.\n\nEvidence: /Users/morpheusqilee/.hermes/reports/source-parity/m4-dirty-20260701T045105Z
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

🔎 Lint report: oracle/phase2-config-parity-hardening-20260703 vs origin/main

ruff

Total: 0 on HEAD, 1756 on base (✅ -1756)

🆕 New issues: none

✅ Fixed issues (1215):

Rule Count
F401 853
F841 130
E402 102
F541 38
E741 18
E401 17
F811 17
F821 15
E731 9
E701 6
E702 5
F601 4
F823 1
First entries
../../../../../tmp/lint-base/scripts/release.py:335: [F601] Dictionary key literal `"withapurpose37@gmail.com"` repeated
../../../../../tmp/lint-base/tests/hermes_cli/test_api_key_providers.py:9: [F401] `hermes_cli.auth.ProviderConfig` imported but unused
../../../../../tmp/lint-base/tests/gateway/test_matrix.py:3: [F401] `json` imported but unused
../../../../../tmp/lint-base/tests/gateway/test_webhook_dynamic_routes.py:4: [F401] `os` imported but unused
../../../../../tmp/lint-base/tests/honcho_plugin/test_session.py:1067: [F841] Local variable `result` is assigned to but never used
../../../../../tmp/lint-base/tests/agent/test_bedrock_integration.py:12: [F401] `os` imported but unused
../../../../../tmp/lint-base/tests/tools/test_tool_result_storage.py:8: [F401] `tools.budget_config.DEFAULT_TURN_BUDGET_CHARS` imported but unused
../../../../../tmp/lint-base/tests/hermes_cli/test_gateway_service.py:849: [F841] Local variable `root_group` is assigned to but never used
../../../../../tmp/lint-base/tests/gateway/test_slack.py:23: [F401] `gateway.platforms.base.SUPPORTED_DOCUMENT_TYPES` imported but unused
../../../../../tmp/lint-base/tests/agent/test_display.py:3: [F401] `os` imported but unused
../../../../../tmp/lint-base/tests/agent/test_rate_limit_tracker.py:192: [F401] `sys` imported but unused
../../../../../tmp/lint-base/tests/agent/test_prompt_caching.py:3: [F401] `copy` imported but unused
../../../../../tmp/lint-base/tests/tools/test_browser_cloud_fallback.py:7: [F401] `unittest.mock.patch` imported but unused
../../../../../tmp/lint-base/gateway/platforms/matrix.py:109: [E402] Module level import not at top of file
../../../../../tmp/lint-base/tests/acp/test_server.py:19: [F401] `acp.schema.LoadSessionResponse` imported but unused
../../../../../tmp/lint-base/tests/agent/transports/test_transport.py:8: [F401] `agent.transports.types.ToolCall` imported but unused
../../../../../tmp/lint-base/tests/gateway/test_sse_agent_cancel.py:10: [F401] `json` imported but unused
../../../../../tmp/lint-base/tests/run_agent/test_invalid_context_length_warning.py:3: [F401] `unittest.mock.call` imported but unused
../../../../../tmp/lint-base/tests/gateway/test_voice_command.py:2546: [F401] `gateway.platforms.base.MessageType` imported but unused
../../../../../tmp/lint-base/tests/gateway/test_clean_shutdown_marker.py:72: [F841] Local variable `entry` is assigned to but never used
../../../../../tmp/lint-base/tests/tools/test_transcription_tools.py:8: [F401] `os` imported but unused
../../../../../tmp/lint-base/tests/gateway/test_telegram_documents.py:20: [F401] `gateway.config.Platform` imported but unused
../../../../../tmp/lint-base/tests/run_agent/test_provider_parity.py:7: [F401] `json` imported but unused
../../../../../tmp/lint-base/tests/cli/test_cli_status_bar.py:339: [F401] `datetime.timedelta` imported but unused
../../../../../tmp/lint-base/tests/hermes_cli/test_tool_token_estimation.py:3: [F401] `unittest.mock.patch` imported but unused
... and 1190 more

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 11005 on HEAD, 5243 on base (🆕 +5762)

🆕 New issues (3636):

Rule Count
unresolved-attribute 1253
unresolved-import 1012
invalid-argument-type 674
invalid-assignment 220
unsupported-operator 124
not-subscriptable 105
invalid-method-override 87
invalid-parameter-default 28
invalid-return-type 26
unused-type-ignore-comment 24
no-matching-overload 22
call-non-callable 15
unresolved-reference 14
invalid-type-form 10
deprecated 7
+6 more rules
First entries
tests/hermes_cli/test_billing_scope_stepup.py:5: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
gateway/run.py:12435: [unresolved-attribute] unresolved-attribute: Attribute `bind_telegram_topic` is not defined on `None` in union `None | SessionDB`
agent/chat_completion_helpers.py:2790: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["error"]` and value of type `Exception` on object of type `dict[str, None | list[Unknown]]`
plugins/platforms/telegram/adapter.py:2222: [unresolved-attribute] unresolved-attribute: Attribute `VIDEO` is not defined on `None` in union `Unknown | None`
plugins/platforms/email/adapter.py:516: [invalid-argument-type] invalid-argument-type: Argument to bound method `IMAP4.uid` is incorrect: Expected `str`, found `None`
tests/hermes_cli/test_dashboard_auth_plugin_hook.py:8: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/tools/test_browser_lightpanda.py:481: [invalid-argument-type] invalid-argument-type: Argument to function `loads` is incorrect: Expected `str | bytes | bytearray`, found `str | dict[str, Any]`
tools/web_tools.py:255: [unresolved-import] unresolved-import: Cannot resolve imported module `ddgs`
tests/tools/test_tts_command_providers.py:141: [invalid-argument-type] invalid-argument-type: Argument to function `_is_command_provider_config` is incorrect: Expected `dict[str, Any]`, found `None`
tests/hermes_cli/test_opencode_go_flat_namespace.py:133: [invalid-argument-type] invalid-argument-type: Argument to function `switch_model` is incorrect: Expected `bool`, found `str | bool`
plugins/platforms/raft/adapter.py:476: [unresolved-attribute] unresolved-attribute: Attribute `Application` is not defined on `None` in union `Unknown | None`
run_agent.py:4170: [unresolved-attribute] unresolved-attribute: Object of type `Self@_rebuild_anthropic_client` has no attribute `provider`
tests/gateway/test_restart_resume_pending.py:1664: [invalid-assignment] invalid-assignment: Object of type `(session_key, source) -> tuple[object, None]` is not assignable to attribute `_claim_active_session_slot` of type `def _claim_active_session_slot(self, session_key: str, source: SessionSource) -> tuple[Any, str | None]`
tests/docker/test_docker_exec_privilege_drop.py:30: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
plugins/platforms/telegram/adapter.py:2567: [unresolved-import] unresolved-import: Cannot resolve imported module `telegram.error`
hermes_cli/main.py:10571: [invalid-argument-type] invalid-argument-type: Argument to function `normalize_profile_name` is incorrect: Expected `str`, found `Any | None`
agent/agent_init.py:693: [invalid-argument-type] invalid-argument-type: Argument to function `build_anthropic_client` is incorrect: Expected `int | float`, found `int | float | None`
tests/hermes_cli/test_codex_runtime_plugin_migration.py:96: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["command"]` and `dict[Unknown, Unknown] | None`
plugins/dashboard_auth/nous/__init__.py:80: [unresolved-import] unresolved-import: Cannot resolve imported module `httpx`
tests/plugins/test_google_meet_node.py:16: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/test_plugin_utils.py:42: [unresolved-attribute] unresolved-attribute: Object of type `() -> Unknown` has no attribute `reset`
agent/background_review.py:577: [unresolved-attribute] unresolved-attribute: Unresolved attribute `session_id` on type `AIAgent`
tests/tools/test_blueprints.py:12: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
gateway/restart.py:10: [invalid-argument-type] invalid-argument-type: Argument to constructor `float.__new__` is incorrect: Expected `str | Buffer | SupportsFloat | SupportsIndex`, found `Unknown | int | str | ... omitted 15 union elements`
tests/tools/test_file_tools.py:506: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> str, (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[str]]` cannot be called with key of type `Literal["old_string"]` on object of type `list[str]`
... and 3611 more

✅ Fixed issues (718):

Rule Count
unresolved-import 262
unresolved-attribute 170
invalid-argument-type 146
invalid-assignment 56
invalid-method-override 19
unsupported-operator 17
invalid-return-type 10
unresolved-reference 10
not-subscriptable 9
call-non-callable 8
invalid-parameter-default 7
no-matching-overload 3
deprecated 1
First entries
cli.py:7468: [invalid-assignment] invalid-assignment: Object of type `Unknown` is not assignable to attribute `_silence_duration` on type `AudioRecorder | TermuxAudioRecorder`
tests/hermes_cli/test_tools_config.py:542: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `int` in union `str | dict[str, str | list[Unknown] | bool | list[str]] | dict[str, str | list[Unknown]] | dict[str, str | list[dict[str, str]]] | int`
tests/hermes_cli/test_aux_config.py:46: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `bound method str.__getitem__(key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> str` cannot be called with key of type `Literal["session_search"]` on object of type `str`
tests/cli/test_cli_status_bar.py:138: [unresolved-import] unresolved-import: Cannot resolve imported module `prompt_toolkit.application`
gateway/platforms/dingtalk.py:851: [unresolved-attribute] unresolved-attribute: Attribute `TimeoutException` is not defined on `None` in union `Unknown | None`
gateway/platforms/telegram.py:753: [unresolved-attribute] unresolved-attribute: Attribute `COMMAND` is not defined on `None` in union `Unknown | None`
tests/gateway/test_config_cwd_bridge.py:14: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/tools/test_write_deny.py:4: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/run_agent/test_agent_loop_vllm.py:29: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/agent/transports/test_types.py:4: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tools/rl_training_tool.py:1131: [not-subscriptable] not-subscriptable: Cannot subscript object of type `bool` with no `__getitem__` method
gateway/platforms/telegram.py:761: [unresolved-attribute] unresolved-attribute: Attribute `AUDIO` is not defined on `None` in union `Unknown | None`
run_agent.py:5932: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_disable_streaming` on type `Self@_interruptible_streaming_api_call`
agent/google_oauth.py:235: [unresolved-attribute] unresolved-attribute: Module `msvcrt` has no member `LK_UNLCK`
tests/gateway/test_bluebubbles.py:269: [unresolved-import] unresolved-import: Cannot resolve imported module `httpx`
gateway/platforms/mattermost.py:733: [invalid-argument-type] invalid-argument-type: Argument is incorrect: Expected `list[str]`, found `(list[str] & ~AlwaysFalsy) | None`
tools/rl_training_tool.py:253: [unresolved-import] unresolved-import: Cannot resolve imported module `atroposlib.envs.base`
tests/agent/test_insights.py:152: [invalid-argument-type] invalid-argument-type: Argument to function `_has_known_pricing` is incorrect: Expected `str`, found `None`
gateway/platforms/wecom_callback.py:129: [unresolved-attribute] unresolved-attribute: Attribute `TCPSite` is not defined on `None` in union `Unknown | None`
environments/tool_call_parsers/longcat_parser.py:13: [unresolved-import] unresolved-import: Cannot resolve imported module `openai.types.chat.chat_completion_message_tool_call`
tests/run_agent/test_provider_attribution_headers.py:27: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `bound method str.__getitem__(key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> str` cannot be called with key of type `Literal["X-OpenRouter-Title"]` on object of type `str`
run_agent.py:1254: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["x-anthropic-beta"]` and value of type `str` on object of type `list[str | Unknown]`
gateway/platforms/discord.py:558: [unresolved-attribute] unresolved-attribute: Attribute `opus` is not defined on `None` in union `Unknown | None`
tools/rl_training_tool.py:229: [invalid-argument-type] invalid-argument-type: Argument to function `module_from_spec` is incorrect: Expected `ModuleSpec`, found `ModuleSpec | None`
tests/tools/test_browser_camofox.py:7: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
... and 693 more

Unchanged: 2149 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

⚠️ MCP catalog security review required

This PR changes the bundled MCP catalog or MCP catalog installer code. MCP entries can define local commands that users later install into mcp_servers, so this needs explicit maintainer review before merge.

A maintainer should verify:

  • any new/changed optional-mcps/**/manifest.yaml command and args are expected,
  • stdio transports do not use shell+egress/exfiltration payloads,
  • git install refs are pinned and bootstrap commands are minimal,
  • requested env vars/secrets match the upstream MCP's documented needs.

After review, add the mcp-catalog-reviewed label and re-run this check.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

🚨 CRITICAL Supply Chain Risk Detected

This 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 modified

These files can execute code during package installation or interpreter startup.

Files:

setup.py

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.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

⚠️ MCP catalog security review required

This PR changes the bundled MCP catalog or MCP catalog installer code. MCP entries can define local commands that users later install into mcp_servers, so this needs explicit maintainer review before merge.

A maintainer should verify:

  • any new/changed optional-mcps/**/manifest.yaml command and args are expected,
  • stdio transports do not use shell+egress/exfiltration payloads,
  • git install refs are pinned and bootstrap commands are minimal,
  • requested env vars/secrets match the upstream MCP's documented needs.

After review, add the mcp-catalog-reviewed label and re-run this check.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

🚨 CRITICAL Supply Chain Risk Detected

This 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 modified

These files can execute code during package installation or interpreter startup.

Files:

setup.py

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.

@scorp323

scorp323 commented Jul 3, 2026

Copy link
Copy Markdown
Owner Author

Superseded by clean upstream PR NousResearch#57620. This fork-local PR was based against stale fork main and included unrelated backlog, so closing to avoid misleading CI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.