From 5d1a5f83daff806df5d64265355fcef52022c184 Mon Sep 17 00:00:00 2001 From: Jon Chery Date: Wed, 19 Aug 2026 22:23:46 +0000 Subject: [PATCH] =?UTF-8?q?feat(P01):=20core/mode=5Fresolver=20=E2=80=94?= =?UTF-8?q?=20client-mode=20resolution=20(REQ-327,=20D-226,=20cli-engineer?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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--- --- core/mode_resolver.py | 94 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 core/mode_resolver.py diff --git a/core/mode_resolver.py b/core/mode_resolver.py new file mode 100644 index 0000000..af863b0 --- /dev/null +++ b/core/mode_resolver.py @@ -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}") \ No newline at end of file