feat(P01): core/mode_resolver — client-mode resolution (REQ-327, D-226, cli-engineer)
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---
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
"""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}")
|
||||
Reference in New Issue
Block a user