diff --git a/roboco/llm/providers/grok.py b/roboco/llm/providers/grok.py index 689a6826..db203654 100644 --- a/roboco/llm/providers/grok.py +++ b/roboco/llm/providers/grok.py @@ -27,6 +27,7 @@ from __future__ import annotations import asyncio import dataclasses +import logging import os from pathlib import Path from typing import TYPE_CHECKING, Protocol @@ -37,6 +38,8 @@ from roboco.llm.providers.base import AgentProvider, ProviderError, SpawnResult if TYPE_CHECKING: from roboco.models.runtime import OrchestratorAgentConfig as AgentConfig +_log = logging.getLogger(__name__) + # The Grok agent image (own image, like every other agent role). Overridable for # tests / staged rollout. _DEFAULT_GROK_IMAGE = os.environ.get( @@ -179,6 +182,19 @@ class GrokCliProvider(AgentProvider): auth_dir = Path(GROK_AUTH_HOST_PATH) if (auth_dir / "auth.json").exists(): cmd.extend(["-v", f"{auth_dir}:{_GROK_AUTH_DIR_IN_CONTAINER}:ro"]) + else: + # The mount is the grok subscription credential — without it the + # container starts but the entrypoint ``--check`` backstop refuses + # to run (exit 78) and the agent is doomed. Fail loud at spawn time + # so the operator sees the missing credential immediately instead + # of diagnosing a later exit-78 from the container log markers. + _log.warning( + "grok host auth.json not found at %s — spawn will start the " + "container but it is doomed to exit 78 (no SuperGrok credential). " + "Run `grok login` on the host (or set ROBOCO_HOST_GROK_DIR to the " + "directory holding auth.json) before spawning Grok agents.", + auth_dir / "auth.json", + ) @staticmethod def _append_usage_mount(cmd: list[str], hosts: dict[str, str | None]) -> None: diff --git a/tests/unit/llm/test_providers.py b/tests/unit/llm/test_providers.py index 5f8bad9b..7a4c2e60 100644 --- a/tests/unit/llm/test_providers.py +++ b/tests/unit/llm/test_providers.py @@ -262,6 +262,26 @@ async def test_grok_spawn_omits_auth_mount_when_absent() -> None: assert not any("/home/agent/.grok-auth-ro" in c for c in cmd) +async def test_grok_spawn_warns_when_auth_absent( + caplog: pytest.LogCaptureFixture, +) -> None: + """A missing host auth.json must not be silent — the spawn is doomed to + exit 78, so the operator gets a spawn-time WARNING naming the missing file + and the remediation (``grok login`` on the host). Without it the container + silently started and only failed later at the entrypoint ``--check``. + """ + caplog.set_level("WARNING", logger="roboco.llm.providers.grok") + host = _FakeHost() + provider = GrokCliProvider(host) + with patch("asyncio.create_subprocess_exec", AsyncMock(return_value=_proc())): + await provider.spawn(_config()) + warnings = [r for r in caplog.records if r.levelname == "WARNING"] + assert warnings, "expected a spawn-time WARNING for the missing host auth.json" + msg = warnings[0].getMessage() + assert "auth.json" in msg + assert "grok login" in msg # names the remediation + + async def test_grok_spawn_prompt_is_injection_safe() -> None: host = _FakeHost() provider = GrokCliProvider(host)