fix(gateway): clean stale PID file before O_EXCL write#1
Conversation
🚨 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 |
268 |
invalid-argument-type |
48 |
invalid-assignment |
40 |
unresolved-import |
38 |
unsupported-operator |
20 |
invalid-parameter-default |
11 |
invalid-type-form |
3 |
unused-type-ignore-comment |
3 |
invalid-return-type |
2 |
unknown-argument |
2 |
unresolved-reference |
2 |
not-subscriptable |
1 |
call-non-callable |
1 |
no-matching-overload |
1 |
First entries
agent/agent_init.py:114: [invalid-parameter-default] invalid-parameter-default: Default value of type `None` is not assignable to annotated parameter type `int`
agent/agent_init.py:1396: [invalid-argument-type] invalid-argument-type: Argument to function `is_local_endpoint` is incorrect: Expected `str`, found `(str & ~AlwaysFalsy) | (dict[str, str] & ~AlwaysFalsy) | (Any & ~AlwaysFalsy)`
run_agent.py:2796: [unresolved-attribute] unresolved-attribute: Object of type `Self@_apply_client_headers_for_base_url` has no attribute `_client_kwargs`
tools/delegate_tool.py:1140: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_delegate_depth` on type `AIAgent`
hermes_cli/auth.py:5355: [invalid-argument-type] invalid-argument-type: Argument to bound method `dict.get` is incorrect: Expected `str`, found `(str & ~Literal["spotify"] & ~Literal["nous"] & ~Literal["openai-codex"] & ~Literal["xai-oauth"] & ~Literal["qwen-oauth"] & ~Literal["google-gemini-cli"] & ~Literal["minimax-oauth"] & ~Literal["copilot-acp"]) | None`
optional-skills/research/darwinian-evolver/scripts/parrot_openrouter.py:20: [unresolved-import] unresolved-import: Cannot resolve imported module `openai`
run_agent.py:3028: [unresolved-attribute] unresolved-attribute: Object of type `Self@_fire_reasoning_delta` has no attribute `reasoning_callback`
tests/run_agent/test_steer.py:194: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_tool_worker_threads_lock` on type `AIAgent`
tests/tools/test_mcp_invalid_url.py:19: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
run_agent.py:2752: [unresolved-attribute] unresolved-attribute: Object of type `Self@_try_refresh_anthropic_client_credentials` has no attribute `provider`
agent/chat_completion_helpers.py:948: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to `def _fixed_temperature_for_model(model: str | None, base_url: str | None = None) -> object`
cli.py:8998: [invalid-assignment] invalid-assignment: Object of type `Unknown` is not assignable to attribute `reasoning_callback` on type `AIAgent & ~AlwaysFalsy`
run_agent.py:2802: [unresolved-attribute] unresolved-attribute: Object of type `Self@_swap_credential` has no attribute `api_mode`
run_agent.py:1557: [unresolved-attribute] unresolved-attribute: Object of type `Self@_save_session_log` has no attribute `tools`
tests/agent/test_async_utils.py:156: [invalid-argument-type] invalid-argument-type: Argument to function `safe_schedule_threadsafe` is incorrect: Expected `Coroutine[Any, Any, Any]`, found `Literal["not-a-coroutine"]`
run_agent.py:658: [unresolved-attribute] unresolved-attribute: Object of type `Self@_should_start_quiet_spinner` has no attribute `_print_fn`
tests/run_agent/test_plugin_context_engine_init.py:54: [unresolved-attribute] unresolved-attribute: Object of type `AIAgent` has no attribute `context_compressor`
agent/agent_init.py:1299: [invalid-argument-type] invalid-argument-type: Argument to `ContextCompressor.__init__` is incorrect: Expected `str`, found `None`
tests/run_agent/test_codex_xai_oauth_recovery.py:52: [unresolved-attribute] unresolved-attribute: Unresolved attribute `api_mode` on type `AIAgent`
run_agent.py:2621: [unresolved-attribute] unresolved-attribute: Object of type `Self@_try_refresh_codex_client_credentials` has no attribute `provider`
cli.py:7766: [unresolved-attribute] unresolved-attribute: Object of type `object` has no attribute `context_length`
tests/run_agent/test_tool_call_guardrail_runtime.py:163: [unresolved-attribute] unresolved-attribute: Unresolved attribute `tool_start_callback` on type `AIAgent`
agent/agent_init.py:113: [invalid-type-form] invalid-type-form: Function `callable` is not valid in a parameter annotation: Did you mean `collections.abc.Callable`?
run_agent.py:1891: [unresolved-attribute] unresolved-attribute: Object of type `Self@get_activity_summary` has no attribute `_api_call_count`
run_agent.py:715: [unresolved-attribute] unresolved-attribute: Object of type `Self@_emit_warning` has no attribute `status_callback`
... and 415 more
✅ Fixed issues (146):
| Rule | Count |
|---|---|
invalid-argument-type |
74 |
invalid-assignment |
26 |
unresolved-attribute |
25 |
unsupported-operator |
12 |
unresolved-reference |
3 |
unresolved-import |
2 |
not-subscriptable |
2 |
no-matching-overload |
1 |
invalid-return-type |
1 |
First entries
tests/run_agent/test_provider_attribution_headers.py:132: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> LiteralString, (key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> str]` cannot be called with key of type `Literal["X-OpenRouter-Cache-TTL"]` on object of type `str`
run_agent.py:1805: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `bound method dict[str, str].__getitem__(key: str, /) -> str` cannot be called with key of type `slice[None, Literal[8], None]` on object of type `dict[str, str]`
cli.py:9234: [invalid-argument-type] invalid-argument-type: Argument to function `estimate_usage_cost` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown, Unknown] | int | dict[Unknown | str, Unknown | str | dict[str, str]]`
tests/run_agent/test_provider_attribution_headers.py:46: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> LiteralString, (key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> str]` cannot be called with key of type `Literal["X-Title"]` on object of type `str`
run_agent.py:7886: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["response"]` and value of type `SimpleNamespace` on object of type `dict[str, None | list[Unknown]]`
run_agent.py:1659: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["timeout"]` and value of type `int | float` on object of type `dict[str, str | dict[str, str]]`
run_agent.py:15284: [invalid-argument-type] invalid-argument-type: Argument to function `len` is incorrect: Expected `Sized`, found `str | None`
run_agent.py:13296: [invalid-argument-type] invalid-argument-type: Argument to bound method `SessionDB.update_token_counts` is incorrect: Expected `str | None`, found `str | Unknown | dict[Unknown, Unknown] | int | dict[Unknown | str, Unknown | str | dict[str, str]]`
run_agent.py:15515: [invalid-argument-type] invalid-argument-type: Argument to function `len` is incorrect: Expected `Sized`, found `None | Unknown | str`
tests/cli/test_resume_display.py:648: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["resume_display"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 26 union elements`
run_agent.py:8135: [unresolved-attribute] unresolved-attribute: Attribute `append` is not defined on `None` in union `None | list[Unknown]`
run_agent.py:15034: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to `list[dict[str, Any]]`
run_agent.py:13257: [invalid-argument-type] invalid-argument-type: Argument to function `estimate_usage_cost` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown, Unknown] | int | dict[Unknown | str, Unknown | str | dict[str, str]]`
tests/run_agent/test_provider_attribution_headers.py:154: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> LiteralString, (key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> str]` cannot be called with key of type `Literal["HTTP-Referer"]` on object of type `str`
run_agent.py:8952: [invalid-assignment] invalid-assignment: Invalid assignment to data descriptor attribute `base_url` on type `Self@_restore_primary_runtime` with custom `__set__` method
gateway/platforms/yuanbao.py:103: [invalid-assignment] invalid-assignment: Object of type `Literal["0.0.0"]` is not assignable to `Literal["0.13.0"]`
run_agent.py:10688: [invalid-argument-type] invalid-argument-type: Argument to function `session_search` is incorrect: Expected `str`, found `Unknown | None`
run_agent.py:9718: [invalid-argument-type] invalid-argument-type: Argument to function `get_provider_profile` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown, Unknown] | int | dict[Unknown | str, Unknown | str | dict[str, str]]`
run_agent.py:9072: [invalid-argument-type] invalid-argument-type: Argument to function `get_provider_request_timeout` is incorrect: Expected `str | None`, found `Divergent | Unknown | str | ... omitted 3 union elements`
run_agent.py:4955: [invalid-argument-type] invalid-argument-type: Argument to function `save_trajectory` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown, Unknown] | int | dict[Unknown | str, Unknown | str | dict[str, str]]`
tests/cli/test_fast_command.py:480: [invalid-argument-type] invalid-argument-type: Argument to bound method `TestCase.assertIn` is incorrect: Expected `Iterable[Any] | Container[Any]`, found `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 26 union elements`
tests/tools/test_browser_lightpanda.py:242: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["engine"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 26 union elements`
tests/hermes_cli/test_destructive_slash_confirm_gate.py:32: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 26 union elements`
tests/tools/test_web_providers.py:226: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["search_backend"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 26 union elements`
run_agent.py:13230: [invalid-argument-type] invalid-argument-type: Argument to function `save_context_length` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown, Unknown] | int | dict[Unknown | str, Unknown | str | dict[str, str]]`
... and 121 more
Unchanged: 4168 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
…arch#26687) Accept delegation timeout/error statuses in the TUI subagent model, normalize unknown status strings defensively, and harden /agents overlay rendering/sorting so unknown statuses cannot crash glyph/color lookup. Add regression tests for live event normalization and disk snapshot replay.
…NousResearch#26195) * refactor(tui): thread cols through Md/StreamingMd/renderTable, update cache key * feat(tui): three-tier width calc + full-line string rendering in renderTable Replaces the old renderTable (L203-244) with: - Empty table guard - Ragged row normalization - Three-tier column width calculation (ideal → proportional shrink → hard scale) - Rounding remainder distribution - Full-line string rendering (one <Text> per row, not per cell) - wrap=truncate-end on all table lines - All cells rendered as plain text via stripInlineMarkup No wrapping or vertical fallback yet — those come in Phase 3 and 4. * feat(tui): wrapCell with grapheme-safe hard-break + multi-line row rendering Adds: - Intl.Segmenter-based grapheme splitting (fallback to [...word]) - wrapCell() for width-correct word wrapping on stripped text - Multi-line row rendering with LineEntry metadata (header/separator/body) - Post-render safety condition (maxLineWidth computed, vertical fallback in Task 4) - Non-wrapping path preserved for tables that fit at ideal widths * feat(tui): vertical key-value fallback with scaled threshold + safety check Wires: - Scaled row-height threshold (numCols<=3: 8, <=6: 5, else: 4) - Post-render safety check (maxLineWidth > available space) - Header-only edge case - Vertical format: bold headers, stripped cell text, clamped separator width - Iterates headers (not rows) for consistent key-value fields on ragged rows * test(tui): pass cols to Md in test helpers, add width-overflow assertions - renderAtWidth now passes cols={columns} to <Md> so width-aware code paths are exercised in tests - tableFuzz: every rendered line must fit within allocated width (stringWidth) - tableRepro: separator regex updated to match truncation ellipsis - stringWidth imported from @hermes/ink for CJK-correct assertions * fix(tui): address adversarial review — comment tier 3 budget overshoot, eliminate redundant wrapCell - Add comment on Tier 3 MIN_COL_WIDTH clamp exceeding budget (self-heals via safetyOverflow) - Track tallestBodyRow during allEntries build pass instead of re-wrapping every cell in a second traversal (eliminates O(cells) of redundant stripInlineMarkup+stringWidth) * fix(tui): pass cols to recursive fenced-markdown Md, fix test frame extraction - Thread cols into <Md> for fenced markdown blocks (L734) so nested tables use the width-aware renderer instead of max-content path - Fix renderAtWidth helpers to extract final Ink repaint frame instead of concatenating all intermediate frames (REPAINT_RE split) - Add fenced-markdown-table fixture to tableFuzz (exercises the nested path) * chore: remove repro test suites and tmux driver script These were scaffolding for development/reproduction — not needed in the PR.
remove pip installation method from docs
Ready column help and fallbacks now describe dependency-ready work; show a badge on unassigned ready cards and fix the stale unassigned tooltip. Align localized Ready help strings with the new semantics. Co-authored-by: Cursor <cursoragent@cursor.com>
…rm prompts (NousResearch#26414) When an approval / clarify / confirm overlay was active, the global input handler in useInputHandlers returned for every key that wasn't Ctrl+C, which silently disabled transcript scrolling. On long threads the context the prompt was asking about often lived above the visible viewport, and being unable to scroll while answering felt like the prompt had locked the UI. ApprovalPrompt also had no Esc handler at all, so the one obvious 'abort' key did nothing during a permission prompt and the user had to memorize Ctrl+C or hunt for the deny number. Fixes: - Extract shouldFallThroughForScroll(key) (pure, exported) covering wheel scrolls, PageUp/PageDown, and Shift+ArrowUp/Down. When a prompt overlay is up and the pressed key is a scroll input, skip the early return so it reaches the existing wheel/PageUp/Shift+arrow handlers below. Plain arrows still drive in-prompt selection — they don't fall through. - ApprovalPrompt now maps Esc to onChoice('deny'), parity with the global Ctrl+C cancellation path that already invokes cancelOverlayFromCtrlC() for approvals. The bottom-of-prompt hint now advertises 'Esc/Ctrl+C deny'. - Extract approvalAction(ch, key, sel) — pure key-dispatch helper for the approval prompt, exported so the regression matrix (Esc, numbers, Enter, arrows, edge clamping, precedence) is testable without mounting Ink. Tests: - useInputHandlers.test.ts: 6 cases covering shouldFallThroughForScroll positives (wheel/PageUp/PageDown/Shift+arrows) and negatives (plain arrows, bare shift, no scroll key). - approvalAction.test.ts: 8 cases covering Esc→deny, numeric mapping, Enter, ↑↓ within bounds, edge clamping, Esc-beats-others precedence, unrelated keystrokes.
…search#26726) The per-skill sidebar tree from PR NousResearch#26646 emitted category entries with only a label. Docusaurus derives translation keys from the label (sidebar.docs.category.<label>), and categories that exist in both Bundled and Optional (productivity, mcp, mlops, research, email, software-development, dogfood) collided on identical keys — failing i18n extraction and the Deploy Site build. Result: source had the sidebar fix but no per-skill page rendered with a sidebar in production. Add a 'key: skills-<source>-<category>' attribute to each generated category dict so Bundled vs Optional get distinct translation keys. Regenerated sidebars.ts via the script. Local docusaurus build passes.
…attempts on unsupported platforms (NousResearch#26718) Tirith ships no Windows binary, so on every Windows CLI startup users saw a scary 'tirith security scanner enabled but not available' banner they could not act on. The banner suggested degraded security; in reality pattern-matching guards still run and the message was pure noise. Fix: - New public is_platform_supported() helper in tools/tirith_security.py that returns False when _detect_target() doesn't resolve (Windows, any non-x86_64/aarch64 arch). - ensure_installed(), _resolve_tirith_path(), and check_command_security() short-circuit on unsupported platforms: cache _resolved_path = _INSTALL_FAILED with reason 'unsupported_platform', skip PATH probes, skip the background download thread, skip the disk failure marker, and return allow with an empty summary from check_command_security so the spawn loop never fires. - Explicit user-configured tirith_path is still honored everywhere (a user who built tirith themselves under WSL keeps that path). - CLI banner in cli.py gated on is_platform_supported() — fires only on platforms where tirith *should* work but isn't installed. - Docs note tirith's supported-platform list and point Windows users at WSL. Tests: tests/tools/test_tirith_security.py +8 tests covering Linux x86_64, Darwin arm64, Windows, and unknown-arch verdicts plus the silent ensure_installed / check_command_security / _resolve_tirith_path fast-paths and the explicit-path override. test_tirith_security.py 75 passed (8 new + 67 pre-existing) test_command_guards.py 19 passed
Mirrors the dependency-ready / assign-profile semantics used in other locales; Copilot review noted uk.ts was still on the old dispatcher-tick wording. Co-authored-by: Cursor <cursoragent@cursor.com>
generate_systemd_unit runs _build_service_path_dirs(); tests that mimic sudo (Path.home → /root) caused is_dir() to raise PermissionError for unprivileged users on /root/.hermes/..., failing CI. Treat inaccessible paths like missing. Co-authored-by: Cursor <cursoragent@cursor.com>
This reverts commit 965610f.
…shboard-kanban fix(dashboard): clarify Kanban Ready column semantics
Thin wrapper around Imbue's darwinian_evolver (AGPL-3.0, subprocess-only). Ships a working OpenRouter driver (parrot_openrouter.py), a snapshot inspector (show_snapshot.py), and a custom-problem template. SKILL.md has 58-char description, Pitfalls sourced from actually running the loop: non-viable seed trap, Azure content filter killing runs, loop.run() being a generator, nested-pickle snapshots, and aggressive default concurrency. Salvaged from NousResearch#12719 by @Bihruze — original PR shipped 12,289 LOC across 61 files (29 Python modules, FastAPI dashboard, VS Code extension, benchmark hub, marketplace, etc.) which was far beyond the scope of the underlying issue (NousResearch#336). This version stays at the ~700-LOC scope that issue actually asked for. Authorship of the original effort credited via AUTHOR_MAP entry and the SKILL.md author field. Verified end-to-end: seed 'Say {{ phrase }}' (score 0.000) evolved into 'Please repeat the following phrase exactly as it is, without any modifications or additional formatting: {{ phrase }}' (score 0.750) across 3 iterations on gpt-4o-mini via OpenRouter. Co-authored-by: Bihruze <98262967+Bihruze@users.noreply.github.com>
…NousResearch#11049) (NousResearch#26759) Plugins can now replace a built-in tool by passing override=True to ctx.register_tool(). Without it, the registry rejects any registration that would shadow an existing tool from a different toolset (unchanged default behavior). Unlocks the use case from NousResearch#11049: drop-in replacement of browser/web backends without forking core. Composes with the existing pre_tool_call hook for runtime interception of any implementation. The override is audit-logged at INFO so it surfaces in agent.log.
Document the three protocols already available for driving hermes-agent from external programs — ACP, the TUI gateway JSON-RPC, and the OpenAI-compatible API server — with a 'which one should I use' guide and a Pi-style RPC command mapping table. Sidebar entry under Developer Guide -> Architecture.
Zero-install localhost tunnels over SSH via Pinggy. Covers HTTP/HTTPS, TCP, TLS, access control (basic auth / bearer / IP whitelist), header manipulation (CORS, force-HTTPS), web debugger, Pro token mode, and four composite recipes (webhook receiver, MCP server exposure, local LLM endpoint share, dev-server quick-share with one-shot password). Closes NousResearch#361
…sor stops drifting (NousResearch#26717) * fix(tui): keep Ink displayCursor in sync with fast-echo writes so cursor stops drifting TextInput's fast-echo bypass writes characters directly to stdout to avoid waiting on a React re-render for each keystroke. The hardware cursor advances by text.length cells, but Ink's cached `displayCursor` (the basis for the next frame's relative cursor-move preamble in log-update) stayed unchanged. When ANY unrelated component re-rendered between the fast-echo write and the deferred composer setCur/setParent flush — status bar timer, streaming reasoning, etc. — the next frame's preamble emitted a relative cursor move from a stale parked position and the hardware cursor parked N cells offset from the actual caret. Visible symptom: extra whitespace between the just-typed character and the cursor block, intermittent, worse on long sessions during streaming. Alt-screen was immune because frames begin with absolute CSI H. This adds a small API in @hermes/ink: - `Ink.noteExternalCursorAdvance(dx, dy?)` — bumps displayCursor if set, otherwise seeds from frontFrame.cursor so the next preamble's relative move correctly cancels the external advance. No-op on alt-screen. - `CursorAdvanceContext` + `useCursorAdvance()` hook to expose it. TextInput then calls `noteCursorAdvance(text.length)` after the fast-echo `stdout.write(text)` append, and `noteCursorAdvance(-1)` after the fast-backspace `\b \b` sequence. Tests: 4 new vitest cases pin the API contract (bumps when set, seeds from frontFrame.cursor when null, alt-screen no-op, zero-delta no-op). All 751 ui-tui tests pass; tests/test_tui_gateway_server.py (177) pass. * fix(tui): also advance cursorDeclaration so fast-echo survives deferred React state Copilot review on PR NousResearch#26717 flagged a gap in the original fix: TextInput's fast-echo path defers the React `cur` state update by 16ms (perf optimization that batches re-renders during heavy typing). Inside that window, `useDeclaredCursor` still publishes a target computed from the PRE-keystroke `cur` — `cursorLayout(display, cur, columns)`. Advancing only `displayCursor` would let any unrelated re-render in that 16ms window run onRender's cursor-park branch with the stale declaration and visually undo the fast-echo's advance. The fix is symmetric: `noteExternalCursorAdvance` now bumps BOTH `displayCursor` (the log-update relative-move basis) AND, if non-null, `cursorDeclaration.relativeX/Y` (the target the cursor parks at after every frame). When React finally flushes `setCur`, `useDeclaredCursor` publishes a fresh declaration that supersedes our bumped one — exactly what we want. Adds two new vitest cases covering both halves: - active declaration advances in lock-step with displayCursor - null declaration stays null (no spurious bump) All 753 ui-tui tests pass; tests/test_tui_gateway_server.py (177) pass. Closes review threads: PRRT_kwDOPRF1G86ChKtD (textInput.tsx:1016 fast-echo append) PRRT_kwDOPRF1G86ChKtF (textInput.tsx:924 fast-backspace) PRRT_kwDOPRF1G86ChKtG (ink-cursor-advance.test.ts:57 missing coverage) * fix(tui): make fast-echo survive TextInput rerenders + alt-screen (Copilot round 2) Round 2 of PR NousResearch#26717 review. Three real holes Copilot flagged after the initial cursorDeclaration bump: 1. alt-screen early-return skipped BOTH halves of the notifier. But the default TUI wraps the composer in <AlternateScreen> — that IS the production path. CSI H resets log-update's relative-move basis, but the alt-screen park branch uses absolute CUP = `rect.x + decl.relativeX`, so a stale declaration there still parks the cursor at the pre-keystroke caret. Fix: skip ONLY the displayCursor half on alt-screen; still bump cursorDeclaration. 2. TextInput's own rerender could clobber the Ink-level bump. The fast- echo path defers setCur by 16ms; if a parent state change rerenders TextInput in that window, the layout effect inside useDeclaredCursor reads the stale React `cur` state and re-publishes a declaration at the OLD column. Fix: `cursorLayout(display, curRef.current, columns)` — read the always- up-to-date ref, not the deferred state. useMemo dropped (compute is cheap, single-line wrap-text in the common case). 3. Tests bypassed the production wiring. Added two structural tests: - `still advances cursorDeclaration on alt-screen` in the Ink-level suite, asserting displayCursor stays put but the declaration advances by the delta. - `textInputCursorSourceOfTruth.test.ts` pins three structural invariants: layout reads curRef.current, never the bare `cur` state, and the fast-echo stdout.write calls remain paired with noteCursorAdvance(±N). Source-grep invariants > flaky Ink mount tests for this kind of regression. 757/757 ui-tui tests pass (+3 over round 1). type-check clean. lint introduces zero new errors on touched files. tests/test_tui_gateway_server.py (177) pass. Closes review threads: PRRT_kwDOPRF1G86ChOG2 (ink.tsx alt-screen guard) PRRT_kwDOPRF1G86ChOG9 (textInput.tsx fast-backspace rerender window) PRRT_kwDOPRF1G86ChOHC (textInput.tsx fast-append rerender window) PRRT_kwDOPRF1G86ChOHJ (alt-screen test asserts wrong invariant) PRRT_kwDOPRF1G86ChOHP (missing integration-style coverage) * fix(tui): reject fast-backspace at soft-wrap boundary (Copilot round 3) PR NousResearch#26717 round 3. Copilot caught two real things: 1. `\b \b` cannot move the terminal cursor onto the previous visual row across a soft-wrap boundary. When the caret sits at visual column 0 of a wrapped row (e.g. value 'hello ' at width 6 → cursorLayout produces (line 1, col 0)), backspace would leave the physical cursor in place while the logical caret moves up to the end of the previous visual line. `noteCursorAdvance(-1)` would then feed Ink a wrong delta. Fix: `canFastBackspaceShape` now takes the composer width and rejects when `cursorLayout(value, cursor, columns).column === 0`. The fast path falls through to the normal Ink render, which correctly lays out the new caret position. The PR-description inconsistency about alt-screen is fixed in a separate gh pr edit. Adds 4 new tests in textInputFastEcho.test.ts pinning the rejection at exact-multiple wrap boundaries plus a positive control inside a wrapped line and a back-compat case where `columns` is omitted. 761/761 ui-tui tests pass. type-check / lint clean. 177/177 Python tests/test_tui_gateway_server.py pass. Closes review threads: PRRT_kwDOPRF1G86ChxE5 (textInput.tsx:933 wrap-boundary regression) * fix(tui): polish doc + tests after Copilot round 4 Three polish points Copilot raised: 1. canFastBackspaceShape doc comment overstated the legacy contract — said it conservatively rejects potential wrap boundaries when columns is omitted, but the implementation actually skips the wrap-boundary check entirely. Reworded to make the legacy behavior explicit and warn callers not to rely on protection they don't get. 2. ink-cursor-advance.test.ts rationale comment for the 'advances cursorDeclaration in lock-step' case still referenced the pre-fix `cursorLayout(display, cur, columns)` expression. Now accurately describes the current source of truth — `curRef.current` in textInput.tsx — and explains the window the bump is bridging. 3. Removed the three `__get*ForTest` accessors from Ink. The test file already cast the instance to inspect private state in the couple of tests that needed declaration mutation; the rest now use a small `peek(ink)` helper that does the same cast for reads. No test-only API surface ships in production. 761/761 ui-tui tests pass. type-check clean. lint introduces zero new errors on touched files. 177/177 tests/test_tui_gateway_server.py pass. Closes review threads: PRRT_kwDOPRF1G86Ch23W (canFastBackspaceShape doc accuracy) PRRT_kwDOPRF1G86Ch23f (stale test rationale) PRRT_kwDOPRF1G86Ch23p (test-only API surface in production) * fix(tui): tighten doc + add dy test coverage (Copilot round 5) Two polish points from round 5: 1. canFastBackspaceShape doc had two paragraphs that conflicted — the main 'Additionally rejects when the physical cursor sits at visual column 0' was stated unconditionally, then the columns-param paragraph qualified that it only happens when columns is passed. Reworked into clear 'When supplied / When omitted' branches with a concrete example value ('hello ' returns true without columns even though it would be unsafe at width 6). No more inconsistency. 2. Added a test asserting cursorDeclaration.relativeY advances when dy is non-zero. Existing tests exercised dy on displayCursor only. Newlines in fast-echoed text don't currently hit the bypass (canFastAppendShape rejects '\n'), but dy is part of the public notifier contract and must propagate symmetrically with dx so future callers get a fully-implemented contract. 762/762 ui-tui tests pass (+1). type-check / lint / build clean. Closes review threads: PRRT_kwDOPRF1G86Ch6Sz (doc inconsistency) PRRT_kwDOPRF1G86Ch6TE (missing dy coverage on declaration) * fix(tui): doc polish (Copilot round 6) Four small but valid points: 1. textInputCursorSourceOfTruth.test.ts used bare 'fs'/'path'/'url' imports; the rest of ui-tui consistently uses the 'node:' prefix (see src/__tests__/useSessionLifecycle.test.ts, src/lib/editor.test.ts). Switched to node:fs / node:path / node:url to match convention. 2. CursorAdvanceContext.ts type-level doc described only displayCursor. The notifier intentionally also mutates the active cursorDeclaration and that's the only part that matters on alt-screen. Reworked the doc into a two-part 'updates both' summary with the alt-screen asymmetry called out explicitly. 3. use-cursor-advance.ts hook doc had the same problem. Same fix — document both pieces of state, both screen modes. 4. App.tsx onCursorAdvance prop comment was incomplete. Same fix — describe both state updates and the screen-mode asymmetry. No behavior change. 762/762 ui-tui tests pass. type-check / lint / build clean. Closes review threads (auto-resolved on PR but valid critiques): PRRT_kwDOPRF1G86Ch926 (node: prefix on built-in imports) PRRT_kwDOPRF1G86Ch92_ (use-cursor-advance.ts doc) PRRT_kwDOPRF1G86Ch93H (CursorAdvanceContext.ts type doc) PRRT_kwDOPRF1G86Ch93J (App.tsx prop comment)
…text (NousResearch#26823) Adds _sanitize_tool_error() in model_tools and routes both error paths through it: registry.dispatch's try/except (the primary path for tool exceptions) and handle_function_call's outer except (defense in depth). Stripping targets structural framing tokens that the model itself can react to even though json.dumps already handles wire-layer escaping: XML role tags (tool_call, function_call, result, response, output, input, system, assistant, user), CDATA sections, and markdown code fences. Caps message body at 2000 chars and wraps with [TOOL_ERROR] prefix. Defense-in-depth: a tool exception carrying '<tool_call>...' won't break message framing (json escapes it), but the model still reads those tokens and they nudge it toward role-confusion framing. Ported from ironclaw#1639 (one piece of NousResearch#3838's three-feature scout). The truncated-tool-call (NousResearch#1632) and empty-response-recovery (NousResearch#1677, NousResearch#1720) pieces are skipped because main now implements both far more thoroughly (run_agent.py L8147/L12209/L13012 for truncation retry + length rewrite; L4500/L15090+ for empty-response scaffolding stripper, multi-stage nudge, fallback model activation).
…uth (NousResearch#26763) * feat(x_search): gated X (Twitter) search tool with OAuth-or-API-key auth Salvages tools/x_search_tool.py from the closed PR NousResearch#10786 (originally by @Jaaneek) and reworks its credential resolution so the tool registers when EITHER xAI credential path is available: * XAI_API_KEY (paid xAI API key) is set in ~/.hermes/.env or the env, OR * The user is signed in via xAI Grok OAuth — SuperGrok subscription — i.e. hermes auth add xai-oauth has been run Both paths route through xAI's built-in x_search Responses tool at https://api.x.ai/v1/responses. When both credentials exist OAuth wins, matching tools/xai_http.py's existing preference order (uses SuperGrok quota instead of paid API spend). The check_fn calls resolve_xai_http_credentials() which auto-refreshes the OAuth access token if it's within the refresh skew window, so a True return means the bearer is fetchable AND non-empty. Wiring - tools/x_search_tool.py — new tool, ~370 LOC. Schema gated by check_fn, bearer resolved per-call so revoked OAuth surfaces a clean tool_error rather than an HTTP 401. - toolsets.py — "x_search" toolset def. NOT added to _HERMES_CORE_TOOLS; users opt in via hermes tools. - hermes_cli/tools_config.py — CONFIGURABLE_TOOLSETS entry + TOOL_CATEGORIES block with two provider options (OAuth + API key) sharing the existing xai_grok post_setup hook for credential bootstrap. - hermes_cli/config.py — DEFAULT_CONFIG["x_search"] with model / timeout_seconds / retries. Additive nested key; no version bump. - tests/tools/test_x_search_tool.py — 13 tests covering HTTP shape, handle validation, citation extraction, 4xx/5xx/timeout handling, and the full credential-resolution matrix (OAuth-only, API-key-only, both-set, neither-set, resolver-raises, config overrides, registry registration). - website/docs/guides/xai-grok-oauth.md — adds X Search to the direct-to-xAI tools section with off-by-default note. - website/docs/user-guide/features/tools.md — new row in the tools table. Off by default — users enable via `hermes tools` → 🐦 X (Twitter) Search. Schema only appears to the model when xAI credentials are configured. Co-authored-by: Jaaneek <Jaaneek@users.noreply.github.com> * docs(x_search): add dedicated feature page + reference entries - website/docs/user-guide/features/x-search.md (new) — full feature walkthrough: authentication, enablement, configuration, parameters, returned fields, example, troubleshooting, see-also links. - website/docs/reference/tools-reference.md — new "x_search" toolset section with parameter docs and credential gating note. - website/docs/reference/toolsets-reference.md — new row in the toolset catalog table. - website/sidebars.ts — wires the new feature page under Media & Web, after web-search. --------- Co-authored-by: Jaaneek <Jaaneek@users.noreply.github.com>
…NousResearch#26824) Subagent delegation hardcoded api_mode='chat_completions' for any delegation.base_url that didn't match three specific hostnames (chatgpt.com, api.anthropic.com, api.kimi.com/coding), and never read delegation.api_mode from config. Azure AI Foundry's https://foundry.services.ai.azure.com/anthropic endpoint fell through and got chat_completions, causing 404s on every delegate_task call. The main agent already handles this correctly via the shared _detect_api_mode_for_url() helper (anything ending in /anthropic → anthropic_messages); delegation reimplemented its own narrower check. Reuse the shared detector and honor an explicit delegation.api_mode when set so users can also force the transport on non-standard endpoints the URL heuristic can't classify. Fixes NousResearch#10213. Co-authored-by: HiddenPuppy <HiddenPuppy@users.noreply.github.com>
…26825) Port from openai/codex#17667: MCP servers can now opt-in to parallel tool execution by setting supports_parallel_tool_calls: true in their config. This allows tools from the same server to run concurrently within a single tool-call batch, matching the behavior already available for built-in tools like web_search and read_file. Previously all MCP tools were forced sequential because they weren't in the _PARALLEL_SAFE_TOOLS set. Now _should_parallelize_tool_batch checks is_mcp_tool_parallel_safe() which looks up the server's config flag. Config example: mcp_servers: docs: command: "docs-server" supports_parallel_tool_calls: true Changes: - tools/mcp_tool.py: Track parallel-safe servers in _parallel_safe_servers set, populated during register_mcp_servers(). Add is_mcp_tool_parallel_safe() public API. - run_agent.py: Add _is_mcp_tool_parallel_safe() lazy-import wrapper. Update _should_parallelize_tool_batch() to check MCP tools against server config. - 11 new tests covering the feature end-to-end. - Updated MCP docs and config reference.
…earch#26829) Port three hardening patches from Claude Code 2.1.113's expanded deny rules to hermes' detect_dangerous_command() pattern list. 1. macOS /private/{etc,var,tmp,home} system paths /etc, /var, /tmp, /home are symlinks to /private/<name> on macOS. A write to /private/etc/sudoers works identically to /etc/sudoers but bypassed the plain /etc/ pattern check. Extracted a shared _SYSTEM_CONFIG_PATH fragment so /etc/ and the /private/ mirror stay in sync across redirect / tee / cp / mv / install / sed -i patterns. 2. killall -9 / -KILL / -SIGKILL / -s KILL / -r <regex> Parallel to the existing pkill -9 pattern. killall -9 against non-hermes processes was previously unprotected, and killall -r can sweep unrelated processes matching a regex. 3. find -execdir rm Same destructive effect as find -exec rm but ran in each match's directory. The previous pattern required a literal '-exec ' so -execdir slipped through. Guarded by 32 new test cases in 4 test classes: - TestMacOSPrivateSystemPaths (11 cases) - TestKillallKillSignals (9 cases) - TestFindExecdir (4 cases) - TestEtcPatternsUnaffectedByRefactor (6 regression guards on the existing /etc/ coverage after the _SYSTEM_CONFIG_PATH refactor) Inspiration: https://github.com/anthropics/claude-code/releases (Claude Code 2.1.113, April 17 2026 - "Enhanced deny rules" and "Dangerous path protection")
…rsions (NousResearch#26830) Closes NousResearch#10695. Picks up the still-vulnerable Python pins on current main: - aiohttp 3.13.3 -> 3.13.4 (messaging, slack, homeassistant, sms extras + lazy_deps platform.slack) — CVE-2026-34513 (DNS cache exhaustion), CVE-2026-34518 (cookie/proxy-auth leak on cross-origin redirect, relevant for the gateway since it handles OAuth tokens), CVE-2026-34519 (response reason injection), CVE-2026-34520 (null bytes in headers), CVE-2026-34525 (multiple Host headers). - anthropic 0.86.0 -> 0.87.0 (anthropic extra + lazy_deps provider.anthropic) — CVE-2026-34450 (memory tool files created mode 0o666), CVE-2026-34452 (path-traversal in async local-filesystem memory tool). Not directly exploitable since hermes-agent doesn't use the SDK's filesystem memory tool, but the SDK is bumped for hygiene. - cryptography pinned explicitly at 46.0.7 in core dependencies — CVE-2026-39892 (buffer overflow on non-contiguous buffers). Previously came in transitively via PyJWT[crypto]; the explicit floor keeps the WeCom/Weixin crypto paths from drifting below the fix. curl-cffi from the original issue is no longer in pyproject.toml or uv.lock, so no action needed there. uv.lock regenerated cleanly; only aiohttp / anthropic / cryptography moved. Credit: original issue + scoping by @shaun0927 (NousResearch#10695, NousResearch#10701). Floor analysis and packaging-surface audit by @gnanirahulnutakki (NousResearch#10784), adapted to current main's exact-pin style. Co-authored-by: shaun0927 <shaun0927@users.noreply.github.com> Co-authored-by: Gnani Rahul Nutakki <gnanirahulnutakki@users.noreply.github.com>
…arch#355) (NousResearch#26729) * feat(skills): add osint-investigation optional skill (closes NousResearch#355) Phase-1 public-records OSINT investigation framework adapted from ShinMegamiBoson/OpenPlanter (MIT). Lives in optional-skills/research/. Six data-source wiki entries (FEC, SEC EDGAR, USAspending, Senate LD, OFAC SDN, ICIJ Offshore Leaks), each following the 9-section template: summary, access, schema, coverage, cross-reference keys, data quality, acquisition, legal, references. Six stdlib-only acquisition scripts that emit normalized CSV, plus three analysis scripts: - entity_resolution.py — three-tier match (exact / fuzzy / token overlap) with explicit confidence per row - timing_analysis.py — permutation test for donation/contract timing correlation, joins through cross-links - build_findings.py — assembles structured findings.json with evidence chains pointing back to source rows Validation: full pipeline runs end-to-end on synthetic fixtures. Entity resolution found 24 cross-matches with 0 false positives on a 5-row / 4-row test set. Timing analysis on 5 donations clustered near 3 awards returned p=0.000, effect size 2.41 SD. Findings JSON correctly tags HIGH-severity timing pattern. All 9 scripts pass --help and py_compile. Docs site page auto-generated by website/scripts/generate-skill-docs.py; sidebar + catalog entries updated by the same generator. * fix(osint-investigation): live API fixes from end-to-end sweep Live-tested the skill on a real public-citizen query and found three bugs the synthetic E2E missed. All three are now fixed and re-verified. 1. FEC fetch hung on contributor name searches. The combination of two_year_transaction_period + sort=date + contributor_name puts the OpenFEC query plan on a slow path that the upstream gateway times out (25s+). Switched to min_date/max_date with no explicit sort. Renamed --candidate to --contributor (the original name was misleading: FEC searches by donor, not by candidate; --candidate is kept as a deprecated alias). Added --state filter for narrowing. 2. ICIJ Offshore Leaks reconcile endpoint returns 404. ICIJ removed the Open Refine reconciliation API. Rewrote fetch_icij_offshore.py to download the official bulk CSV ZIP (~70 MB, public, no auth) and search it locally. Cached under $HERMES_OSINT_CACHE/icij/ (default ~/.cache/hermes-osint/icij/) for 30 days, --force-refresh to refetch. Verified live: 'PUTIN' query returns 5 Panama Papers officer matches in 0.5s after first download. 3. SEC EDGAR silently returned 0 when the company-name resolver matched an individual Form 3/4/5 filer (insider trading disclosures). Now surfaces 'Resolved company X → CIK Y (Z)' on stderr, prints a filing-type histogram when the type filter wipes results, and explicitly warns when the matched CIK appears to be an individual filer rather than a corporate registrant. Bonus: _http.py was retrying 429 responses with exponential backoff plus honoring (often-missing) Retry-After headers, which compounded into multi-second hangs per page when the upstream key was over quota. Changed to fail-fast on 429 with a clear, actionable error showing the upstream's quota message. Verified: 0.3s fast-fail vs the previous 60s hang on DEMO_KEY rate-limit exhaustion. Updated SKILL.md, fec.md, and icij-offshore.md to match the new CLI flags and ICIJ bulk-cache flow. Regenerated the docusaurus page via website/scripts/generate-skill-docs.py. Live sweep results across all 6 sources for 'Dillon Rolnick, New York': - OFAC SDN: 0 matches ✓ (correctly not sanctioned) - USAspending: 0 matches ✓ (correctly not a federal contractor) - Senate LDA: 0 matches ✓ (correctly not a lobbying client) - SEC EDGAR: warns it resolved to 'Rolnick Michael' (CIK 0001845264) who is an individual Form 3 filer, not a corporate registrant - ICIJ: 0 matches ✓ (correctly not in any offshore leak) - FEC: rate-limited (DEMO_KEY); fails fast with clear quota message * feat(osint-investigation): expand to 12 sources covering identity, property, courts, archives, news Phase-2 expansion per Teknium feedback that the original 6-source skill (federal financial/regulatory only) wasn't a complete OSINT toolkit. Adds 6 more sources covering the major omissions a real investigation would reach for first. New sources (6 fetch scripts + 6 wiki entries): 1. NYC ACRIS — Real property records (deeds, mortgages, liens) via the city's Socrata API. Search by party name or property address. Joins Parties to Master to populate doc_type, dates, borough, and amount. Coverage: 5 NYC boroughs, ~70M party records, 1966-present. 2. OpenCorporates — Global corporate registry covering 130+ jurisdictions (~200M companies). Free API token at https://opencorporates.com/api_accounts/new raises the rate limit; HTML fallback works without one (limited fields). 3. CourtListener (Free Law Project) — federal + state court opinions (~10M back to colonial era) + PACER dockets via RECAP. Anonymous v4 search works; COURTLISTENER_TOKEN raises rate limits. 4. Wayback Machine CDX — historical web captures (~900B+). Used both for surveillance-of-record (when did this site change?) and as a content-recovery layer when other sources point to dead URLs. 5. Wikipedia + Wikidata — narrative bio + structured facts. Wikipedia OpenSearch for article matching, REST summary for extracts, Wikidata Action API (wbgetentities) for claims. Avoids the SPARQL Query Service which is aggressively rate-limited. 6. GDELT 2.0 DOC API — global news monitoring in 100+ languages, ~2015-present. Auto-retries with 6s backoff on the standard 1-req-per-5-sec throttle. Other changes in this commit: - SEC EDGAR no longer raises SystemExit when the company-name resolver finds no CIK; writes an empty CSV with header so the rest of a pipeline can keep moving and the warning is just on stderr. - _http.py User-Agent updated per Wikimedia policy: includes app name, version, and a 'set HERMES_OSINT_UA to identify yourself' instruction. - SKILL.md workflow now groups sources into two clusters (federal financial vs identity/property/courts/archives/news) with bash examples for each. 'When to use this skill' lists the broader set of investigation patterns the expanded sources unlock. Live sweep results on 'Dillon Rolnick, New York' across all 12 sources: ofac ✓ 0 (correctly clean) icij ✓ 0 (correctly not in any leak) usaspending ✓ 0 (correctly not a federal contractor) senate_lda ✓ 0 (correctly not a lobbying client) sec_edgar ✓ 0, warns: resolved to 'Rolnick Michael' (CIK 0001845264), individual Form 3 filer, NOT a corporate registrant fec — rate-limited (DEMO_KEY exhausted), fails fast with clear quota message nyc_acris ✓ 200 records named Rolnick across NYC; 48 records at 571 Hudson (the property the web identifies as his) opencorporates ✓ 0 (no API token configured; HTML fallback) courtlistener ✓ 0 for 'Dillon Rolnick'; 20 for 'Rolnick' generally; 5 for 'Microsoft' sanity check wayback ✓ 30 captures of nousresearch.com from 2011-present wikipedia ✓ 0 (correctly not notable enough); Bill Gates sanity returns full structured facts (occupation, employer, DOB, place of birth, country) gdelt ✓ 0 for 'Dillon Rolnick'; 5 for 'Nous Research' All 17 scripts compile clean and pass --help. Synthetic analysis pipeline regression still passes (entity_resolution 30 matches, timing p=0.000, findings 2). * feat(osint-investigation): remove FEC; DEMO_KEY rate-limits make it unreliable The FEC fetcher consistently failed the live sweep because the OpenFEC DEMO_KEY tier (40 calls/hour) exhausts on a single investigation, and the upstream returns slow-path query plans for unindexed contributor-name searches that the gateway times out. Without a real API key it's not usable; with one the user has to sign up at api.data.gov first. That's too much setup friction for a skill that should work out of the box. Removed: - scripts/fetch_fec.py - references/sources/fec.md Updated: - SKILL.md frontmatter description + tags - 'When NOT to use' now points users at https://www.fec.gov/data/ for federal donations - entity_resolution example switched from donor↔contractor to lobbying-client↔contractor (Senate LDA + USAspending pair) - timing_analysis example switched to lobbying-filings vs awards - 8 wiki entries had their 'FEC ↔ ...' cross-reference bullets removed 11 sources remain (5 federal financial + 6 identity/property/courts/ archives/news). All scripts compile, pass --help, and the synthetic analysis pipeline still passes on the new lobbying-shaped regression fixture (30 matches, p=0.000 on tight clustering, 2 findings).
Fixes NousResearch#26693 `hermes doctor` currently promotes invalid direct API keys into the final summary even when the matching OAuth path is already healthy. That makes the setup look more broken than it really is. This change keeps the failed API Connectivity row visible but stops treating it as a blocking summary issue when a healthy OAuth fallback already exists for the same provider family. Covered cases: - Gemini OAuth + invalid direct Gemini key - MiniMax OAuth + invalid direct MiniMax key Based on NousResearch#26704 by @worlldz.
…cceeds xAI's OAuth implementation at ``auth.x.ai`` validates the PKCE ``code_challenge`` at the **token** endpoint, not just at the authorize step. When Hermes sends the standards-compliant token POST with ``code_verifier`` alone — exactly what RFC 7636 §4.5 prescribes — xAI rejects the exchange with ``code_challenge is required`` and the user is stuck with no working OAuth login. The fix: * Extract the token POST into ``_xai_oauth_exchange_code_for_tokens`` so the wire format is unit-testable in isolation. * Send the original ``code_challenge`` and ``code_challenge_method`` in the form body alongside ``code_verifier``. Strict RFC-compliant servers ignore the extras at the token endpoint, and xAI's permissive implementation accepts the exchange. This is the standard "defensive echo" workaround used by every OAuth client that targets a server with this quirk. * Refuse to fire the POST when ``code_verifier`` is empty — leaking the authorization code to a server that can't redeem it is worse than failing locally with an actionable error. The new error code is ``xai_pkce_verifier_missing`` and the message points at this issue for context. * Surface the HTTP status code prominently in the 4xx error message (``xAI token exchange failed (HTTP 400). Response: …``) so users and maintainers can tell a 400 (bad request / PKCE problem) from a 403 (tier denied, see NousResearch#26847) at a glance instead of parsing the JSON body by eye. Closes NousResearch#26990
14 focused tests on the extracted helper ``_xai_oauth_exchange_code_for_tokens`` cover: Core contract: * ``code_verifier`` is on the wire (RFC 7636 §4.5). * ``code_challenge`` + ``code_challenge_method=S256`` are echoed (the NousResearch#26990 defense-in-depth that makes xAI's token endpoint stop rejecting valid exchanges). * ``grant_type=authorization_code``, ``code``, ``redirect_uri``, and ``client_id`` are all locked. * Content-Type is ``application/x-www-form-urlencoded`` (xAI rejects ``application/json`` on this endpoint). * The supplied ``token_endpoint`` URL is used verbatim — no hard-coded constant sneaks in via a future refactor. * ``timeout_seconds`` is forwarded; floored at 20s. Sanity guard: * Empty ``code_verifier`` raises ``xai_pkce_verifier_missing`` with a link to NousResearch#26990 — and NOTHING is sent. Leaking the auth code to a server that can't redeem it is the wrong failure mode. * Empty ``code_challenge`` omits only the defensive echo; the standards-compliant ``code_verifier`` request still goes out so RFC-compliant servers keep working. Error surfacing: * Non-200 responses include both ``HTTP <status>`` and the body verbatim — disambiguates 400 (PKCE / bad request) from 403 (tier denied, see NousResearch#26847). * Transport errors are wrapped as ``AuthError`` with the ``xai_token_exchange_failed`` code, so the surrounding ``format_auth_error`` UI mapping still fires. * Non-dict JSON payloads raise ``xai_token_exchange_invalid``. * 200 happy path returns the parsed payload dict verbatim. End-to-end wire-format guard: * A real ``httpx.Client`` with a stub transport captures the bytes on the wire and asserts every PKCE field round-trips through ``urlencode``. Catches a future refactor that swaps ``data=`` for ``json=`` (which xAI would silently reject).
… provider catalog
…ls for xAI compatibility
xAI's /responses endpoint rejects pattern and format JSON Schema keywords
in tool schemas with HTTP 400 'Invalid arguments passed to the model'.
The existing strip_pattern_and_format() only walked OpenAI-format tools
({'function': {'parameters': ...}}), missing Responses-format shapes
({'name': ..., 'parameters': ...}) used by codex_responses API mode.
This shows up most often with MCP-derived tools that carry validation
keywords (e.g. domain pattern regex in firecrawl, format: date-time)
through to the wire.
Extends the walk to handle both shapes. Auto-strip wiring is applied
separately in chat_completion_helpers (post-refactor location).
Closes NousResearch#27197
Port of the run_agent.py changes from NousResearch#27219 to current main: the _build_api_kwargs body was extracted into agent/chat_completion_helpers. build_api_kwargs, so wire the xAI tool-schema sanitization there (provider in {'xai', 'xai-oauth'} or base_url=api.x.ai). Logs a warning instead of silently swallowing exceptions, matching the contributor's review-followup fix. Co-authored-by: zccyman <zccyman@163.com>
…esearch#27572) * feat(kanban): orchestrator-driven auto-decomposition on triage Closes the core gap in the kanban system: dropping a one-liner into Triage now decomposes it into a graph of child tasks routed to specialist profiles by description, matching teknium's original vision ("main orchestrator splits/creates actual tasks, doles them out to each agent"). The build --------- - hermes_cli/profiles.py: new `description` + `description_auto` fields on ProfileInfo, persisted in <profile_dir>/profile.yaml. Helpers read_profile_meta / write_profile_meta. `create_profile` accepts optional description. - hermes_cli/profile_describer.py: new module — auto-generate a 1-2 sentence description from a profile's skills + model + name via the auxiliary LLM (`auxiliary.profile_describer`). - hermes_cli/main.py: new `hermes profile create --description ...` flag; new `hermes profile describe [name] [--text ... | --auto | --all --auto]` subcommand. - hermes_cli/kanban_db.py: new `decompose_triage_task` atomic helper — creates N child tasks, links the root as a child of every leaf (root waits for the whole graph), flips root `triage -> todo` with orchestrator assignee, records an audit comment + `decomposed` event in a single write_txn. - hermes_cli/kanban_decompose.py: new module — calls the auxiliary LLM (`auxiliary.kanban_decomposer`) with the profile roster + descriptions to produce a JSON task graph, then invokes the DB helper. Rewrites unknown assignees to the configured `kanban.default_assignee` (or the active default profile) so a task NEVER lands with assignee=None. Falls back to specify-style single-task promotion when the LLM returns `fanout: false`. - hermes_cli/kanban.py: new `hermes kanban decompose [task_id | --all]` CLI verb. - hermes_cli/config.py: new DEFAULT_CONFIG keys — kanban.orchestrator_profile, kanban.default_assignee, kanban.auto_decompose (default True), kanban.auto_decompose_per_tick (default 3), auxiliary.kanban_decomposer, auxiliary.profile_describer. - gateway/run.py: kanban dispatcher watcher now runs auto-decompose before each `_tick_once`, capped by `auto_decompose_per_tick` so a bulk-load of triage tasks doesn't burst-spend the aux LLM. - plugins/kanban/dashboard/plugin_api.py: new endpoints — GET /profiles (list roster + descriptions), PATCH /profiles/<name> (set description, user-authored), POST /profiles/<name>/describe-auto (LLM-generate), POST /tasks/<id>/decompose (run decomposer), GET/PUT /orchestration (orchestrator/default-assignee/auto-decompose pickers, with resolved fallbacks echoed back). - plugins/kanban/dashboard/dist/index.js: new OrchestrationPanel collapsible — dropdowns for orchestrator profile and default assignee, auto-decompose toggle, per-profile description editor with Save and Auto-generate buttons. New ⚗ Decompose button next to ✨ Specify on triage-column task drawers. Behavior -------- - A task in Triage gets fanned out into a small DAG of child tasks. Children with no internal parents flip to `ready` immediately (parallel dispatch). Children with sibling parents wait. The root stays alive as a parent of every child — when the whole graph finishes, it promotes to `ready` and the orchestrator profile wakes back up to judge completion (the "adds more tasks until done" part of the original vision). - `kanban.orchestrator_profile` unset -> falls back to the default profile (whichever `hermes` launches with no -p flag). - `kanban.default_assignee` unset -> same fallback. Tasks NEVER end up unassigned. - `kanban.auto_decompose=true` (default) runs the decomposer automatically on dispatcher ticks; manual `hermes kanban decompose` is always available. Tests ----- - tests/hermes_cli/test_kanban_decompose_db.py — 7 tests for the atomic DB helper (status transitions, dep graph, audit trail, validation errors). - tests/hermes_cli/test_kanban_decompose.py — 6 tests for the decomposer module (fanout, no-fanout fallback, unknown-assignee rewrite, malformed-JSON resilience, no-aux-client path). - tests/hermes_cli/test_profile_describer.py — 10 tests for profile.yaml r/w + the LLM auto-describer (yaml corrupt tolerance, user-vs-auto description protection, --overwrite, fallback parsing). E2E --- - CLI end-to-end: created profiles with descriptions, dropped a triage task, mocked the aux LLM with a 3-task graph -> verified all three children were created with the right assignees, the dependency edges matched the LLM's graph, root flipped to todo gated by every child, audit comment + `decomposed` event recorded. - Dashboard end-to-end: started the dashboard against an isolated HERMES_HOME, verified all four new endpoints via curl (profile listing, PATCH for description, PUT for orchestration settings, POST for decompose). Opened the UI in the browser, confirmed the OrchestrationPanel renders with all three pickers + the per-profile description editor, typed a description, clicked Save, verified ~/.hermes/profile.yaml was written. Clicked Decompose on the triage card and confirmed the inline error message surfaced as designed ("no auxiliary client configured"). * feat(kanban): surface decompose mode (Auto/Manual) as a one-click pill The auto/manual toggle already existed as kanban.auto_decompose (default true), but it was buried inside the collapsed Orchestration settings panel — users couldn't tell at a glance which mode they were in. This hoists it to a pill at the top of the kanban page so the state is always visible and one click flips it. UX - New "⚗ Decompose: AUTO|MANUAL" pill in the kanban header. Emerald styling when Auto is on (the default), muted/gray when Manual. - Pill is visible both in the collapsed AND expanded Orchestration settings views so context is preserved when the user opens the panel. - Tooltip explains both states + what clicking does. - Renamed the in-panel "Auto-decompose on triage / Enabled" checkbox to "Decompose mode / Auto (default) | Manual" for language parity with the pill. Behavior preserved - Default remains Auto (kanban.auto_decompose=true). - Manual mode restores pre-PR behavior: triage tasks stay in triage until the user clicks ⚗ Decompose on each card (or runs `hermes kanban decompose <id>`). Implementation - plugins/kanban/dashboard/dist/index.js: load /orchestration on mount (not just on expand) so the collapsed pill reflects real state. Render mode pill in both collapsed and expanded headers. Reuses the existing PUT /api/plugins/kanban/orchestration endpoint — no new backend, no new tests required. E2E verified - Pill renders as "⚗ Decompose: AUTO" on page load (default). - One click flips to "⚗ Decompose: MANUAL" with muted styling. - config.yaml on disk shows auto_decompose: false after the flip. - Second click round-trips back to Auto; config.yaml flips to true. * feat(kanban): rename mode pill to "Orchestration: Auto/Manual" Per Teknium feedback — "Decompose" was too implementation-specific. "Orchestration" is the user-facing concept (the whole pitch is the orchestrator profile routing work), and the pill is the front door to it. - Pill text: "Orchestration: Auto" / "Orchestration: Manual" (title case, no ⚗ prefix, no SHOUTY-CAPS for the mode value) - In-panel checkbox label: "Orchestration mode" (was "Decompose mode") - Tooltips updated to match - No behavior change * docs(kanban): document decompose, profile descriptions, orchestration mode Brings the docs site up to parity with the PR. English build verified locally (npx docusaurus build --locale en) — clean, no new broken links or anchors. Pre-existing broken-link warnings (rl-training, llms.txt, step-by-step-checklist, fallback-model) untouched. - website/docs/reference/cli-commands.md + `hermes kanban decompose` action row in the action table, with pointer to the Auto vs Manual orchestration section. - website/docs/reference/profile-commands.md + `--description "<text>"` flag on `hermes profile create`. + Full `hermes profile describe` section: read, --text, --auto, --overwrite, --all flags with examples. - website/docs/user-guide/features/kanban.md (the big one) + Triage column intro rewritten around the Auto-decompose default behavior, with pointer to the new Auto vs Manual section. + Status action row updated to mention both ⚗ Decompose and ✨ Specify on triage cards. + New "Auto vs Manual orchestration" section explaining the two modes, how to flip them (pill, config), how routing-by-description works, the no-None-assignee guarantee, plus a config knob table (auto_decompose, auto_decompose_per_tick, orchestrator_profile, default_assignee) and the two new auxiliary slots (kanban_decomposer, profile_describer). + REST surface table gains 6 new endpoint rows: /tasks/:id/decompose, /profiles (GET), /profiles/:name (PATCH), /profiles/:name/describe-auto, /orchestration (GET + PUT). - website/docs/user-guide/features/kanban-tutorial.md + Triage column blurb updated for Auto by default + Manual via the pill, with cross-link to the Auto vs Manual orchestration section. - website/docs/user-guide/profiles.md + Blank-profile flow now mentions --description and points to the kanban routing model for context. - website/docs/user-guide/configuration.md + `kanban_decomposer` and `profile_describer` added to the `hermes model -> Configure auxiliary models` menu listing.
…ousResearch#27623) Restructures the security section so the admin/user distinction is a first-class concept rather than buried under 'Slash Command Access Control'. The new section makes explicit that: - Slash commands are the first capability gated by the tier split today - Future gating (tools, model switching, etc.) will hang off the same admin/user distinction, so configuring it now is forward-compatible - Allowlists vs the admin/user split solve different problems and are contrasted up front Heading renamed: 'Slash Command Access Control' -> 'Admins vs Regular Users'. The platform-specific pages (telegram.md, discord.md) keep the old heading since slash gating IS the only thing they currently gate.
Quarantine Nous OAuth state when refresh fails with terminal invalid_grant/invalid_token errors. Clear local and shared refresh material across runtime, managed access-token, proxy, and credential-pool paths so Hermes stops retrying revoked refresh sessions.
…paque token on failure.
…ity-error fallback for explicit providers Closes NousResearch#26803 Root causes: 1. _is_payment_error() checked for billing keywords (credits, insufficient funds, billing, payment required) but missed daily token quota exhaustion phrases used by Bedrock, Vertex AI, and LiteLLM proxies — e.g. 'Too many tokens per day', 'quota exceeded', 'resource exhausted', 'daily limit'. These are functionally identical to credit exhaustion (provider cannot serve the request) but don't trigger fallback. 2. The call_llm() fallback chain was gated on resolved_provider == 'auto'. When a task resolves to a specific provider (e.g. 'custom' for a LiteLLM proxy, or 'openrouter'), capacity failures (payment/quota/connection) silently raise instead of trying alternatives. This is overly conservative: capacity errors mean the provider *cannot* serve the request regardless of user intent, so alternatives should always be tried. Fixes: - Add quota-related keywords to _is_payment_error(): quota_exceeded, too many tokens per day, daily limit, tokens per day, daily quota, resource exhausted (Vertex AI gRPC code). - Allow fallback for capacity errors (payment + connection) even when resolved_provider is not 'auto'. Rate-limit fallback stays gated on is_auto to honour explicit provider constraints for transient limits. - Apply both fixes to sync call_llm() and async acall_llm() paths. - Add 6 targeted tests for the new quota-error detection cases.
The two TestAuxiliaryClientPoisonedCacheEviction tests were written when explicit-provider users got no fallback at all on connection errors — they asserted ConnectionError propagated after eviction because the fallback gate blocked the auto chain. After the NousResearch#26803 fix in the previous commit, capacity errors (payment/quota/connection) now DO trigger fallback even on explicit providers. The tests still verify cache eviction (their actual contract) but now stub _try_payment_fallback so the fallback machinery does not attempt a real network call.
… net Layered fallback for auxiliary tasks (compression, vision, tts, web_extract, session_search, etc.): 1. Primary aux provider (existing) 2. User-configured auxiliary.<task>.fallback_chain (new) 3. Main agent provider + model (new — last-resort safety net) 4. Warn user + re-raise original error (new) For users on 'auto' (no explicit aux provider), the existing _try_payment_fallback auto-detection chain runs instead — its Step 1 already IS the main agent model, so they get the same behaviour without configuration. The configured fallback_chain config schema comes from NousResearch#26882 / @zccyman; the main-agent safety net + exhaustion warning were added on top. Closes NousResearch#26882. Builds on the capacity-error gate fix in the previous commit (NousResearch#26803 / @Bartok9).
7 new tests:
TestAuxiliaryFallbackLayering (3):
- configured_chain succeeds → main agent fallback NOT consulted
- chain returns nothing → main agent fallback runs and succeeds
- both exhausted → user-visible 'all fallbacks exhausted' warning
fires before the original error is re-raised
TestTryMainAgentModelFallback (4):
- returns (None, None, "") when main provider is 'auto'
- returns (None, None, "") when failed provider == main provider
(no point retrying the same backend)
- resolves the main provider's client when configured correctly
- skips when main provider is marked unhealthy
Adds a new 'Auxiliary Capacity-Error Fallback' section to website/docs/user-guide/features/fallback-providers.md covering: - The 4-step ladder (primary → fallback_chain → main agent → warn) - Which errors trigger fallback (402, 429 quota, connection) vs which respect explicit provider choice (transient 429 rate limits) - Optional fallback_chain config schema with vision + compression examples - Recognized quota-error phrases (Bedrock, Vertex AI, generic) Updates the bottom summary table — every auxiliary task now shows 'Layered (see above)' instead of 'Auto-detection chain' since explicit-provider users also get the main-agent safety net.
4d88964 to
c1a3596
Compare
🚨 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. |
Description
Fixes NousResearch#13511
When the gateway exits abnormally (SIGKILL, crash, OOM), the stale
gateway.pidfile remains on disk. On the next restart,write_pid_file()usesO_CREAT | O_EXCLfor atomic creation, which immediately fails withFileExistsError— causing the startup to report "PID file race lost to another gateway instance" even though the old process is dead.Fix
In
write_pid_file(), before theO_EXCLos.open()call, a stale PID pre-check is added:os.kill(pid, 0)to check if the process is aliveProcessLookupError) or we can't determine (PermissionError,ValueError,TypeError), remove the stale PID fileO_EXCLwrite proceeds normally andFileExistsErroris raised as beforeTesting
test_write_pid_file_cleans_stale_pid_from_dead_process— verifies a stale PID file from a non-existent process is cleaned before the atomic writetest_write_pid_file_preserves_pid_file_for_live_process— verifies that a PID file pointing to a live process is NOT cleaned, andFileExistsErroris correctly raised