mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-30 19:09:28 +00:00
Second, deeper pass over tools/gateway/hermes_cli plus first pass over the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker, dashboard, conformance, monitoring, secret_sources, hermes_state, providers). Same rubric as wave 1 (AGENTS.md test policy); security, alternation/caching invariants, issue-number regressions, and E2E kept. Real test-quality fixes found and rooted out along the way: - tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls (DEFAULT_CONFIG smart-approval leaked in) — pinned approval mode=manual via autouse fixture: 17.4s → 0.4s. - test_model_switch_custom_providers.py / test_user_providers_model_switch.py silently probed live provider catalogs (~2s/test) — stubbed cached_provider_model_ids/provider_model_ids/fetch_api_models. - test_telegram_noise_filter.py: 15-platform copy-paste matrix over shared gateway.run logic → 3 representative platforms (55s → 3.9s). - test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on MagicMock agents — interrupt.side_effect now clears _running_agents (22s → 1.0s). - test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x (24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait 5s → 0.5s. - test_telegram_init_deadline.py: loop-block margin restored to 1.0s with rationale comment — the watchdog-dump assertion needs the loop blocked well past deadline+grace under parallel load (flaked once in the 40-worker verification run at a 0.2s margin). Verification: full hermetic suite via scripts/run_tests.sh — 2,438 files, 21,718 tests passed, 0 failed, 293.9s wall. Suite totals vs original baseline: 46,820 → 19,757 test functions (−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
555 lines
20 KiB
Python
555 lines
20 KiB
Python
"""Tests for hermes_cli.tools_config platform tool persistence."""
|
|
|
|
import logging
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from hermes_cli.nous_account import NousPortalAccountInfo
|
|
from hermes_cli.tools_config import (
|
|
_DEFAULT_OFF_TOOLSETS,
|
|
_apply_toolset_change,
|
|
_checklist_toolset_keys,
|
|
_configure_provider,
|
|
_reconfigure_provider,
|
|
_get_platform_tools,
|
|
_platform_toolset_summary,
|
|
_reconfigure_tool,
|
|
_run_post_setup,
|
|
_save_platform_tools,
|
|
_toolset_has_keys,
|
|
_toolset_needs_configuration_prompt,
|
|
CONFIGURABLE_TOOLSETS,
|
|
TOOL_CATEGORIES,
|
|
gui_toolset_label,
|
|
_visible_providers,
|
|
provider_readiness_status,
|
|
tools_command,
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_all_invalid_platform_toolsets_logs_runtime_warning(caplog):
|
|
"""#38798: an explicit platform config whose toolset names are all invalid
|
|
(e.g. 'hermes' instead of 'hermes-cli') must warn at resolve time so an
|
|
already-corrupted config is caught at runtime, not just during migration."""
|
|
import hermes_cli.tools_config as _tc
|
|
# The runtime warning fires once per platform per process; clear the guard
|
|
# so this test is deterministic regardless of prior resolutions.
|
|
_tc._warned_invalid_platform_toolsets.discard("cli")
|
|
config = {"platform_toolsets": {"cli": ["hermes"]}}
|
|
|
|
with caplog.at_level(logging.WARNING, logger="hermes_cli.tools_config"):
|
|
_get_platform_tools(config, "cli")
|
|
|
|
warnings = [r.getMessage() for r in caplog.records if r.levelno >= logging.WARNING]
|
|
assert any("#38798" in m and "hermes" in m for m in warnings), warnings
|
|
|
|
|
|
def test_valid_platform_toolsets_no_runtime_warning(caplog):
|
|
"""A correctly-configured platform must not emit the #38798 warning."""
|
|
config = {"platform_toolsets": {"cli": ["hermes-cli"]}}
|
|
|
|
with caplog.at_level(logging.WARNING, logger="hermes_cli.tools_config"):
|
|
_get_platform_tools(config, "cli")
|
|
|
|
assert not any("#38798" in r.getMessage() for r in caplog.records)
|
|
|
|
|
|
def test_partially_valid_platform_toolsets_no_runtime_warning(caplog):
|
|
"""When at least one configured toolset is valid, tools still resolve, so
|
|
the runtime zero-tools warning must not fire (the migration-time check still
|
|
flags the individual bad name)."""
|
|
config = {"platform_toolsets": {"cli": ["hermes-cli", "bogus"]}}
|
|
|
|
with caplog.at_level(logging.WARNING, logger="hermes_cli.tools_config"):
|
|
_get_platform_tools(config, "cli")
|
|
|
|
assert not any("#38798" in r.getMessage() for r in caplog.records)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_platform_tools_homeassistant_toolset_enabled_for_cron_when_hass_token_set(monkeypatch):
|
|
"""HA toolset is runtime-gated by check_fn (requires HASS_TOKEN).
|
|
|
|
When HASS_TOKEN is set, the user has explicitly opted in — _DEFAULT_OFF_TOOLSETS
|
|
shouldn't also strip HA from platforms (like cron) that run through
|
|
_get_platform_tools without an explicit saved toolset list.
|
|
|
|
Regression guard for Norbert's HA cron breakage after #14798 made cron
|
|
honor per-platform tool config.
|
|
"""
|
|
monkeypatch.setenv("HASS_TOKEN", "fake-test-token")
|
|
|
|
cron_enabled = _get_platform_tools({}, "cron")
|
|
assert "homeassistant" in cron_enabled
|
|
# moa must stay off — the original goal of #14798
|
|
assert "moa" not in cron_enabled
|
|
|
|
cli_enabled = _get_platform_tools({}, "cli")
|
|
assert "homeassistant" in cli_enabled
|
|
|
|
|
|
# ─── #35527: platform-restricted default-off toolsets (discord/discord_admin)
|
|
# are stripped by _DEFAULT_OFF_TOOLSETS even when the user explicitly opts in
|
|
# via the platform's native composite. The composite ``hermes-discord``
|
|
# contains both ``discord`` and ``discord_admin`` tools, so configuring it is
|
|
# an explicit opt-in that should survive the default-off strip. ───────────────
|
|
|
|
|
|
def test_discord_toolsets_do_not_leak_to_other_platforms():
|
|
"""Layer 4 (guard): discord/discord_admin are platform-restricted — they
|
|
must never appear on a non-discord platform even when that platform is
|
|
explicitly configured."""
|
|
config = {"platform_toolsets": {"telegram": ["hermes-telegram", "discord"]}}
|
|
enabled = _get_platform_tools(config, "telegram")
|
|
assert "discord" not in enabled
|
|
assert "discord_admin" not in enabled
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_toolset_has_keys_for_vision_accepts_codex_auth(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
(tmp_path / "auth.json").write_text(
|
|
'{"active_provider":"openai-codex","providers":{"openai-codex":{"tokens":{"access_token": "codex-...oken","refresh_token": "codex-...oken"}}}}'
|
|
)
|
|
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
|
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
|
|
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
|
|
|
monkeypatch.setattr(
|
|
"agent.auxiliary_client.resolve_vision_provider_client",
|
|
lambda: ("openai-codex", object(), "gpt-4.1"),
|
|
)
|
|
|
|
assert _toolset_has_keys("vision") is True
|
|
|
|
|
|
def test_save_platform_tools_preserves_mcp_server_names():
|
|
"""Ensure MCP server names are preserved when saving platform tools.
|
|
|
|
Regression test for https://github.com/NousResearch/hermes-agent/issues/1247
|
|
"""
|
|
config = {
|
|
"platform_toolsets": {
|
|
"cli": ["web", "terminal", "time", "github", "custom-mcp-server"]
|
|
}
|
|
}
|
|
|
|
new_selection = {"web", "browser"}
|
|
|
|
with patch("hermes_cli.tools_config.save_config"):
|
|
_save_platform_tools(config, "cli", new_selection)
|
|
|
|
saved_toolsets = config["platform_toolsets"]["cli"]
|
|
|
|
assert "time" in saved_toolsets
|
|
assert "github" in saved_toolsets
|
|
assert "custom-mcp-server" in saved_toolsets
|
|
assert "web" in saved_toolsets
|
|
assert "browser" in saved_toolsets
|
|
assert "terminal" not in saved_toolsets
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_first_install_nous_auto_configures_video_gen(monkeypatch):
|
|
"""When a Nous subscriber checks video_gen in the toolset checklist,
|
|
apply_nous_managed_defaults must write video_gen.provider and
|
|
video_gen.use_gateway so the FAL plugin can route through the gateway
|
|
at runtime. Regression test for the bug where video_gen was marked as
|
|
auto-configured but no config was actually written."""
|
|
monkeypatch.setattr("hermes_cli.nous_subscription.managed_nous_tools_enabled", lambda: True)
|
|
config = {
|
|
"model": {"provider": "nous"},
|
|
"platform_toolsets": {"cli": []},
|
|
}
|
|
for env_var in (
|
|
"VOICE_TOOLS_OPENAI_KEY",
|
|
"OPENAI_API_KEY",
|
|
"ELEVENLABS_API_KEY",
|
|
"FIRECRAWL_API_KEY",
|
|
"FIRECRAWL_API_URL",
|
|
"TAVILY_API_KEY",
|
|
"PARALLEL_API_KEY",
|
|
"BROWSERBASE_API_KEY",
|
|
"BROWSERBASE_PROJECT_ID",
|
|
"BROWSER_USE_API_KEY",
|
|
"FAL_KEY",
|
|
):
|
|
monkeypatch.delenv(env_var, raising=False)
|
|
|
|
monkeypatch.setattr(
|
|
"hermes_cli.tools_config._prompt_toolset_checklist",
|
|
lambda *args, **kwargs: {"video_gen"},
|
|
)
|
|
monkeypatch.setattr("hermes_cli.tools_config.save_config", lambda config: None)
|
|
monkeypatch.setattr(
|
|
"hermes_cli.tools_config._get_enabled_platforms",
|
|
lambda: ["cli"],
|
|
)
|
|
monkeypatch.setattr(
|
|
"hermes_cli.nous_subscription.get_nous_portal_account_info",
|
|
lambda *args, **kwargs: NousPortalAccountInfo(
|
|
logged_in=True,
|
|
source="jwt",
|
|
fresh=False,
|
|
paid_service_access=True,
|
|
),
|
|
)
|
|
|
|
configured = []
|
|
monkeypatch.setattr(
|
|
"hermes_cli.tools_config._configure_toolset",
|
|
lambda ts_key, config: configured.append(ts_key),
|
|
)
|
|
|
|
tools_command(first_install=True, config=config)
|
|
|
|
assert config["video_gen"]["provider"] == "fal"
|
|
assert config["video_gen"]["use_gateway"] is True
|
|
# video_gen should NOT appear in the manual configure list — it's auto-configured
|
|
assert "video_gen" not in configured
|
|
|
|
# ── Platform / toolset consistency ────────────────────────────────────────────
|
|
|
|
|
|
class TestPlatformToolsetConsistency:
|
|
"""Every platform in tools_config.PLATFORMS must have a matching toolset."""
|
|
|
|
def test_all_platforms_have_toolset_definitions(self):
|
|
"""Each platform's default_toolset must exist in TOOLSETS."""
|
|
from hermes_cli.tools_config import PLATFORMS
|
|
from toolsets import TOOLSETS
|
|
|
|
for platform, meta in PLATFORMS.items():
|
|
ts_name = meta["default_toolset"]
|
|
assert ts_name in TOOLSETS, (
|
|
f"Platform {platform!r} references toolset {ts_name!r} "
|
|
f"which is not defined in toolsets.py"
|
|
)
|
|
|
|
def test_gateway_toolset_includes_all_messaging_platforms(self):
|
|
"""hermes-gateway includes list should cover all messaging platforms."""
|
|
from hermes_cli.tools_config import PLATFORMS
|
|
from toolsets import TOOLSETS
|
|
|
|
gateway_includes = set(TOOLSETS["hermes-gateway"]["includes"])
|
|
# Exclude non-messaging platforms from the check
|
|
non_messaging = {"cli", "api_server", "cron"}
|
|
for platform, meta in PLATFORMS.items():
|
|
if platform in non_messaging:
|
|
continue
|
|
ts_name = meta["default_toolset"]
|
|
assert ts_name in gateway_includes, (
|
|
f"Platform {platform!r} toolset {ts_name!r} missing from "
|
|
f"hermes-gateway includes"
|
|
)
|
|
|
|
def test_skills_config_covers_tools_config_platforms(self):
|
|
"""skills_config.PLATFORMS should have entries for all gateway platforms."""
|
|
from hermes_cli.tools_config import PLATFORMS as TOOLS_PLATFORMS
|
|
from hermes_cli.skills_config import PLATFORMS as SKILLS_PLATFORMS
|
|
|
|
non_messaging = {"api_server"}
|
|
for platform in TOOLS_PLATFORMS:
|
|
if platform in non_messaging:
|
|
continue
|
|
assert platform in SKILLS_PLATFORMS, (
|
|
f"Platform {platform!r} in tools_config but missing from "
|
|
f"skills_config PLATFORMS"
|
|
)
|
|
|
|
|
|
def test_numeric_mcp_server_name_does_not_crash_sorted():
|
|
"""YAML parses bare numeric keys (e.g. ``12306:``) as int.
|
|
|
|
_get_platform_tools must normalise them to str so that sorted()
|
|
on the returned set never raises TypeError on mixed int/str.
|
|
|
|
Regression test for https://github.com/NousResearch/hermes-agent/issues/6901
|
|
"""
|
|
config = {
|
|
"platform_toolsets": {"cli": ["web", 12306]},
|
|
"mcp_servers": {
|
|
12306: {"url": "https://example.com/mcp"},
|
|
"normal-server": {"url": "https://example.com/mcp2"},
|
|
},
|
|
}
|
|
|
|
enabled = _get_platform_tools(config, "cli")
|
|
|
|
# All names must be str — no int leaking through
|
|
assert all(isinstance(name, str) for name in enabled), (
|
|
f"Non-string toolset names found: {enabled}"
|
|
)
|
|
assert "12306" in enabled
|
|
|
|
# sorted() must not raise TypeError
|
|
sorted(enabled)
|
|
|
|
|
|
# ─── Imagegen Backend Picker Wiring ────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestImagegenBackendRegistry:
|
|
"""IMAGEGEN_BACKENDS tags drive the model picker flow in tools_config."""
|
|
|
|
def test_fal_backend_registered(self):
|
|
from hermes_cli.tools_config import IMAGEGEN_BACKENDS
|
|
assert "fal" in IMAGEGEN_BACKENDS
|
|
|
|
def test_fal_catalog_loads_lazily(self):
|
|
"""catalog_fn should defer import to avoid import cycles."""
|
|
from hermes_cli.tools_config import IMAGEGEN_BACKENDS
|
|
catalog, default = IMAGEGEN_BACKENDS["fal"]["catalog_fn"]()
|
|
assert default == "fal-ai/flux-2/klein/9b"
|
|
assert "fal-ai/flux-2/klein/9b" in catalog
|
|
assert "fal-ai/flux-2-pro" in catalog
|
|
|
|
def test_image_gen_providers_tagged_with_fal_backend(self):
|
|
"""Both Nous Subscription and FAL.ai providers must carry the
|
|
imagegen_backend tag so _configure_provider fires the picker."""
|
|
from hermes_cli.tools_config import TOOL_CATEGORIES
|
|
providers = TOOL_CATEGORIES["image_gen"]["providers"]
|
|
for p in providers:
|
|
assert p.get("imagegen_backend") == "fal", (
|
|
f"{p['name']} missing imagegen_backend tag"
|
|
)
|
|
|
|
|
|
class TestImagegenModelPicker:
|
|
"""_configure_imagegen_model writes selection to config and respects
|
|
curses fallback semantics (returns default when stdin isn't a TTY)."""
|
|
|
|
def test_picker_writes_chosen_model_to_config(self):
|
|
from hermes_cli.tools_config import _configure_imagegen_model
|
|
config = {}
|
|
# Force _prompt_choice to pick index 1 (second-in-ordered-list).
|
|
with patch("hermes_cli.tools_config._prompt_choice", return_value=1):
|
|
_configure_imagegen_model("fal", config)
|
|
# ordered[0] == current (default klein), ordered[1] == first non-default
|
|
assert config["image_gen"]["model"] != "fal-ai/flux-2/klein/9b"
|
|
assert config["image_gen"]["model"].startswith("fal-ai/")
|
|
|
|
def test_picker_with_gpt_image_does_not_prompt_quality(self):
|
|
"""GPT-Image quality is pinned to medium in the tool's defaults —
|
|
no follow-up prompt, no config write for quality_setting."""
|
|
from hermes_cli.tools_config import (
|
|
_configure_imagegen_model,
|
|
IMAGEGEN_BACKENDS,
|
|
)
|
|
catalog, default_model = IMAGEGEN_BACKENDS["fal"]["catalog_fn"]()
|
|
model_ids = list(catalog.keys())
|
|
ordered = [default_model] + [m for m in model_ids if m != default_model]
|
|
gpt_idx = ordered.index("fal-ai/gpt-image-1.5")
|
|
|
|
# Only ONE picker call is expected (for model) — not two (model + quality).
|
|
call_count = {"n": 0}
|
|
def fake_prompt(*a, **kw):
|
|
call_count["n"] += 1
|
|
return gpt_idx
|
|
|
|
config = {}
|
|
with patch("hermes_cli.tools_config._prompt_choice", side_effect=fake_prompt):
|
|
_configure_imagegen_model("fal", config)
|
|
|
|
assert call_count["n"] == 1, (
|
|
f"Expected 1 picker call (model only), got {call_count['n']}"
|
|
)
|
|
assert config["image_gen"]["model"] == "fal-ai/gpt-image-1.5"
|
|
assert "quality_setting" not in config["image_gen"]
|
|
|
|
|
|
def test_picker_repairs_corrupt_config_section(self):
|
|
"""When image_gen is a non-dict (user-edit YAML), the picker should
|
|
replace it with a fresh dict rather than crash."""
|
|
from hermes_cli.tools_config import _configure_imagegen_model
|
|
config = {"image_gen": "some-garbage-string"}
|
|
with patch("hermes_cli.tools_config._prompt_choice", return_value=0):
|
|
_configure_imagegen_model("fal", config)
|
|
assert isinstance(config["image_gen"], dict)
|
|
assert config["image_gen"]["model"] == "fal-ai/flux-2/klein/9b"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_effective_configurable_toolsets_dedupes_bundled_plugins():
|
|
"""Bundled plugins (plugins/spotify) share their toolset key with the
|
|
built-in CONFIGURABLE_TOOLSETS entry. The effective list must not list
|
|
them twice — otherwise `hermes tools` → "reconfigure existing" shows
|
|
the same toolset two rows in a row.
|
|
"""
|
|
from hermes_cli.tools_config import _get_effective_configurable_toolsets
|
|
|
|
all_ts = _get_effective_configurable_toolsets()
|
|
keys = [ts_key for ts_key, _, _ in all_ts]
|
|
assert len(keys) == len(set(keys)), (
|
|
f"duplicate toolset keys in effective list: "
|
|
f"{[k for k in keys if keys.count(k) > 1]}"
|
|
)
|
|
# Spotify specifically — the bug that motivated the dedupe.
|
|
spotify_rows = [t for t in all_ts if t[0] == "spotify"]
|
|
assert len(spotify_rows) == 1, spotify_rows
|
|
# Built-in label wins over the plugin label.
|
|
assert spotify_rows[0][1] == "🎵 Spotify"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Inline Nous Portal login gate on managed-provider selection
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Checklist diff scope: non-configurable toolsets (kanban) must not be
|
|
# reported as added/removed by `hermes tools` ──────────────────────────
|
|
|
|
|
|
|
|
|
|
def test_kanban_not_reported_as_removed_in_diff():
|
|
"""Reproduces the false-signal bug: `hermes tools` printed ``- kanban``
|
|
when saving a platform that resolves kanban as enabled, even though the
|
|
checklist never offered kanban as a toggle.
|
|
|
|
The printed diff must be scoped to ``_checklist_toolset_keys`` so a tool
|
|
the user could not deselect is never reported as removed. The persisted
|
|
config still keeps kanban (verified separately by _save_platform_tools).
|
|
"""
|
|
config = {"platform_toolsets": {"telegram": ["kanban", "web", "terminal"]}}
|
|
current = _get_platform_tools(config, "telegram", include_default_mcp_servers=False)
|
|
assert "kanban" in current # resolved as enabled at read time
|
|
|
|
# The checklist can only return configurable keys it was shown; kanban
|
|
# is never one of them.
|
|
universe = _checklist_toolset_keys("telegram")
|
|
new_enabled = {t for t in current if t != "kanban"}
|
|
|
|
# Unscoped (old, buggy) diff would surface kanban.
|
|
assert (current - new_enabled) == {"kanban"}
|
|
# Scoped (fixed) diff drops it.
|
|
assert ((current - new_enabled) & universe) == set()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_vision_picker_custom_endpoint(tmp_path, monkeypatch):
|
|
"""Custom endpoint writes base_url+model to config and the key to env."""
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
import hermes_cli.tools_config as tc
|
|
from hermes_cli.config import load_config
|
|
|
|
seq = iter([2]) # Custom OpenAI-compatible endpoint
|
|
prompts = iter(["https://my.endpoint/v1", "sk-secret", "my-vision-model"])
|
|
with patch.object(tc, "_prompt_choice", side_effect=lambda *a, **k: next(seq)), \
|
|
patch.object(tc, "_prompt", side_effect=lambda *a, **k: next(prompts)), \
|
|
patch.object(tc, "save_env_value") as save_env, \
|
|
patch.object(tc, "_toolset_has_keys", return_value=False):
|
|
tc._configure_vision_backend()
|
|
|
|
v = load_config().get("auxiliary", {}).get("vision", {})
|
|
assert v.get("base_url") == "https://my.endpoint/v1"
|
|
assert v.get("model") == "my-vision-model"
|
|
# provider pinned to "custom" so the resolver routes through base_url.
|
|
assert v.get("provider") == "custom"
|
|
save_env.assert_called_once_with("OPENAI_API_KEY", "sk-secret")
|
|
|
|
|
|
|
|
|
|
# ─── provider_readiness_status ────────────────────────────────────────────────
|
|
#
|
|
# Server-side truth for the GUI "Ready" pill (issue: Capabilities tab showed
|
|
# Ready for every zero-env-var provider row, including logged-out Nous
|
|
# Subscription rows and never-installed KittenTTS/Piper).
|
|
|
|
|
|
def _fake_features(*, logged_in: bool, paid: bool = True):
|
|
account = (
|
|
NousPortalAccountInfo(
|
|
logged_in=True, source="jwt", fresh=False, paid_service_access=paid
|
|
)
|
|
if logged_in
|
|
else NousPortalAccountInfo(
|
|
logged_in=False, source="none", fresh=False, paid_service_access=None
|
|
)
|
|
)
|
|
return SimpleNamespace(nous_auth_present=logged_in, account_info=account)
|
|
|
|
|
|
|
|
|
|
# ── Windows console-flash guard for post-setup subprocess spawns ──────────────
|
|
#
|
|
# The desktop GUI runs post-setup hooks through a detached, console-less
|
|
# `hermes tools post-setup <key>` child. On Windows each console child (npm,
|
|
# npx, pip, powershell) spawned without CREATE_NO_WINDOW materializes a brand
|
|
# new console window — the "terminal flash" reported on the Capabilities
|
|
# browser-setup journey. `_post_setup_no_window_flags` is the single wrapper
|
|
# every hook spawn passes as `creationflags`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Post-setup readiness predicates for the browser rows ─────────────────────
|
|
#
|
|
# The GUI's "Run setup" idempotence rides on provider_readiness_status
|
|
# reporting ready/needs_setup honestly. agent_browser (local browser) must
|
|
# track the FULL local install (CLI + Chromium), the cloud-provider hook
|
|
# ("browserbase") only the CLI, and camofox its npm package.
|
|
|
|
|