"""Nova client-mode resolver (P1, REQ-327, D-226). Priority: --mode flag → NOVA_CLIENT_MODE env → credential type → TTY. No silent fallbacks: every return carries a non-empty selection_reason. INV-13: invalid env values are ignored + warned, then fall through. INV-14: credential_type developer_pat/nova_oidc_token + TTY → interactive; + no-TTY → agent. TTY check is sys.stdin.isatty() (D-226). """ from __future__ import annotations import json import logging import os import sys from pathlib import Path from typing import Optional, Tuple log = logging.getLogger("nova.mode_resolver") _VALID_MODES = ("agent", "interactive") _CRED_MODE_TYPES = ("developer_pat", "nova_oidc_token") def resolve_mode( flag: Optional[str] = None, env_var: Optional[str] = None, credential_type: Optional[str] = None, stdin_isatty: bool = False, ) -> Tuple[str, str]: """Return (mode, selection_reason) honoring D-226 priority.""" if flag is not None and flag in _VALID_MODES: return flag, "flag" if env_var is not None and env_var != "": if env_var in _VALID_MODES: return env_var, "env" log.warning( "NOVA_CLIENT_MODE=%r invalid (expected one of %s); ignoring", env_var, _VALID_MODES, ) if credential_type in _CRED_MODE_TYPES: mode = "interactive" if stdin_isatty else "agent" return mode, f"credential:{credential_type}" mode = "interactive" if stdin_isatty else "agent" return mode, "tty" def _read_credential_type(path: Path) -> Optional[str]: """Read the active credential's type from ~/.nova/credentials.json.""" try: data = json.loads(path.read_text()) except (OSError, json.JSONDecodeError): return None active_jti = data.get("active_credential_jti") for cred in data.get("credentials", []) or []: if cred.get("jti") == active_jti: return cred.get("type") return None def resolve_mode_from_env(credential_type: Optional[str] = None) -> Tuple[str, str]: """Resolve mode using sys.argv, NOVA_CLIENT_MODE, credentials, and TTY. Best-effort --mode scan of sys.argv (no full argparse); env var; ~/.nova/credentials.json active credential type; sys.stdin.isatty(). """ flag: Optional[str] = None argv = sys.argv[1:] for i, tok in enumerate(argv): if tok == "--mode" and i + 1 < len(argv): flag = argv[i + 1] break if tok.startswith("--mode="): flag = tok.split("=", 1)[1] break env_var = os.environ.get("NOVA_CLIENT_MODE") if env_var is not None and env_var == "": env_var = "" if credential_type is None: cred_path = Path.home() / ".nova" / "credentials.json" credential_type = _read_credential_type(cred_path) return resolve_mode( flag=flag, env_var=env_var, credential_type=credential_type, stdin_isatty=sys.stdin.isatty(), ) if __name__ == "__main__": mode, reason = resolve_mode_from_env() print(f"mode={mode} reason={reason}")