5d1a5f83da
Priority: --mode flag → NOVA_CLIENT_MODE env → credential type → TTY. No silent fallbacks: every return carries a non-empty selection_reason. - resolve_mode(flag, env_var, credential_type, stdin_isatty) -> (mode, reason) - resolve_mode_from_env() reads --mode from sys.argv (best-effort scan, no full argparse), NOVA_CLIENT_MODE, ~/.nova/credentials.json active credential type, and sys.stdin.isatty() (D-226: stdin, NOT stdout). - INV-13: invalid env values logged + ignored, fall through. - INV-14: developer_pat/nova_oidc_token + TTY → interactive; + no-TTY → agent. ---ci--- project: acdl phase: 1 milestone: v1.28 status: execute persona: cli-engineer ---/ci---
94 lines
3.0 KiB
Python
94 lines
3.0 KiB
Python
"""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}") |