165 lines
6.0 KiB
Python
165 lines
6.0 KiB
Python
"""Goose readiness + config reporting for ``omnigent setup``.
|
|
|
|
Unlike :mod:`omnigent.onboarding.cursor_auth`, Omnigent manages **no** Goose
|
|
credentials: Goose owns its own auth via ``goose configure`` (keyring or
|
|
``~/.config/goose/config.yaml``). This module is a thin, read-only reporter —
|
|
it confirms the ``goose`` binary is installed and surfaces the configured
|
|
provider/model so setup can show Goose as ready (and which model it will drive)
|
|
without ever touching Goose's secrets.
|
|
|
|
Detection prefers Goose's **own** config resolution via ``goose info -v`` (which
|
|
prints a ``goose Configuration:`` block with ``GOOSE_PROVIDER`` / ``GOOSE_MODEL``
|
|
once a provider is configured, and omits it when not). That is authoritative
|
|
across platforms and config formats — ``goose configure`` may store provider/key
|
|
in the keyring and a config file whose exact path/shape differs by OS, so reading
|
|
``goose info -v`` is more reliable than hand-parsing ``config.yaml`` (the latter
|
|
remains a best-effort fallback when the binary can't be run).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
from omnigent.onboarding.harness_install import GOOSE_KEY, harness_cli_installed
|
|
|
|
#: Override for the goose binary path (mirrors omnigent.goose_native).
|
|
_GOOSE_PATH_ENV = "OMNIGENT_GOOSE_PATH"
|
|
#: ``goose info -v`` is local-only (no network), so a short timeout is ample.
|
|
_INFO_TIMEOUT_S = 10.0
|
|
|
|
|
|
def goose_cli_installed() -> bool:
|
|
"""Return whether the ``goose`` binary is on ``PATH``."""
|
|
return harness_cli_installed(GOOSE_KEY)
|
|
|
|
|
|
def _goose_binary() -> str | None:
|
|
"""Resolve the goose executable (``OMNIGENT_GOOSE_PATH`` override, else PATH)."""
|
|
override = os.environ.get(_GOOSE_PATH_ENV, "").strip()
|
|
if override:
|
|
return override
|
|
return shutil.which("goose")
|
|
|
|
|
|
def goose_info_config() -> tuple[str | None, str | None]:
|
|
"""Return ``(provider, model)`` from ``goose info -v``, or ``(None, None)``.
|
|
|
|
Parses the ``goose Configuration:`` section Goose prints for its *own*
|
|
resolved config (env overrides + config file + defaults). Returns
|
|
``(None, None)`` when the binary is absent, the command fails/times out, or
|
|
no provider is configured (Goose omits the ``GOOSE_PROVIDER`` line then), so
|
|
a caller can cleanly distinguish "configured" from "not".
|
|
"""
|
|
binary = _goose_binary()
|
|
if binary is None:
|
|
return None, None
|
|
try:
|
|
proc = subprocess.run(
|
|
[binary, "info", "-v"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=_INFO_TIMEOUT_S,
|
|
)
|
|
except (OSError, subprocess.SubprocessError):
|
|
return None, None
|
|
if proc.returncode != 0:
|
|
return None, None
|
|
provider: str | None = None
|
|
model: str | None = None
|
|
in_config = False
|
|
for raw in proc.stdout.splitlines():
|
|
stripped = raw.strip()
|
|
if stripped.lower().startswith("goose configuration"):
|
|
in_config = True
|
|
continue
|
|
if not in_config:
|
|
continue
|
|
if stripped.startswith("GOOSE_PROVIDER:"):
|
|
provider = stripped.split(":", 1)[1].strip() or None
|
|
elif stripped.startswith("GOOSE_MODEL:"):
|
|
model = stripped.split(":", 1)[1].strip() or None
|
|
return provider, model
|
|
|
|
|
|
def goose_config_path() -> Path:
|
|
"""Return Goose's config file path for this process's HOME.
|
|
|
|
Honors ``XDG_CONFIG_HOME``; defaults to ``~/.config/goose/config.yaml``.
|
|
"""
|
|
xdg = os.environ.get("XDG_CONFIG_HOME", "").strip()
|
|
base = Path(xdg) if xdg else Path.home() / ".config"
|
|
return base / "goose" / "config.yaml"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class GooseConfigSummary:
|
|
"""What setup needs to know about the local Goose configuration.
|
|
|
|
:param installed: ``goose`` binary present on ``PATH``.
|
|
:param provider: Configured ``GOOSE_PROVIDER`` (env override wins over the
|
|
config file), or ``None`` if neither is set.
|
|
:param model: Configured ``GOOSE_MODEL`` (env override wins), or ``None``.
|
|
"""
|
|
|
|
installed: bool
|
|
provider: str | None
|
|
model: str | None
|
|
|
|
@property
|
|
def ready(self) -> bool:
|
|
"""Launchable when the binary is present (Goose resolves its own auth)."""
|
|
return self.installed
|
|
|
|
|
|
def _config_value(key: str) -> str | None:
|
|
"""Read *key* from the Goose config file (top-level scalar), or ``None``.
|
|
|
|
Deliberately a minimal, dependency-light scan: Goose stores ``GOOSE_PROVIDER``
|
|
/ ``GOOSE_MODEL`` as top-level YAML scalars. A parse failure or missing file
|
|
returns ``None`` (best-effort reporting, never raises).
|
|
"""
|
|
path = goose_config_path()
|
|
try:
|
|
import yaml
|
|
|
|
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
except (OSError, ValueError):
|
|
return None
|
|
if not isinstance(data, dict):
|
|
return None
|
|
value = data.get(key)
|
|
return value if isinstance(value, str) and value else None
|
|
|
|
|
|
def goose_config_summary() -> GooseConfigSummary:
|
|
"""Summarize the local Goose configuration for setup display.
|
|
|
|
Resolution order, most authoritative first:
|
|
|
|
1. ``GOOSE_PROVIDER`` / ``GOOSE_MODEL`` env (an explicit per-shell override),
|
|
2. ``goose info -v`` — Goose's own resolved config (handles the keyring /
|
|
config-file path + format across platforms; see :func:`goose_info_config`),
|
|
3. a best-effort top-level scan of ``config.yaml`` (fallback when the binary
|
|
can't be run, e.g. in unit tests).
|
|
|
|
Using ``goose info -v`` is why a provider set via ``goose configure`` is now
|
|
detected even when it isn't a plain top-level ``GOOSE_PROVIDER`` scalar in the
|
|
file we'd guess at.
|
|
"""
|
|
info_provider, info_model = goose_info_config()
|
|
provider = (
|
|
os.environ.get("GOOSE_PROVIDER", "").strip()
|
|
or info_provider
|
|
or _config_value("GOOSE_PROVIDER")
|
|
)
|
|
model = os.environ.get("GOOSE_MODEL", "").strip() or info_model or _config_value("GOOSE_MODEL")
|
|
return GooseConfigSummary(
|
|
installed=goose_cli_installed(),
|
|
provider=provider,
|
|
model=model,
|
|
)
|