"""Credential-safe adapter over the official Ark CLI executable.""" from __future__ import annotations import base64 import json import os import re import shutil import subprocess import threading import time from dataclasses import dataclass from pathlib import Path from typing import Any, Callable, Mapping, Sequence from urllib.parse import parse_qs, urlencode, urlsplit from openai4s.host.bash import redact_shell_text _MAX_OUTPUT_BYTES = 1_000_000 _DEFAULT_TIMEOUT_S = 30.0 _SAFE_ENV_NAMES = frozenset( { "APPDATA", "BROWSER", "COMSPEC", "DBUS_SESSION_BUS_ADDRESS", "DISPLAY", "HOME", "LANG", "LC_ALL", "LOCALAPPDATA", "LOGNAME", "PATH", "PATHEXT", "SHELL", "SYSTEMDRIVE", "SYSTEMROOT", "TEMP", "TMP", "TMPDIR", "USER", "USERPROFILE", "WAYLAND_DISPLAY", "WINDIR", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_RUNTIME_DIR", # Proxy variables are a deliberate trade-off: a proxy URL can embed # credentials, but without them the arkcli child cannot reach the # control plane at all on proxied networks. The shared kernel # allowlist keeps excluding them. ``_child_env`` matches on # ``key.upper()``, so the lowercase forms are covered too. "ALL_PROXY", "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", } ) _DEVICE_CODE = re.compile(r"^[A-Za-z0-9+/=_&-]{8,8192}$") _PROFILE_NAME = re.compile(r"^[A-Za-z0-9._-]{1,160}$") _MODEL_ID = re.compile(r"^[A-Za-z0-9._:-]{1,256}$") class ArkCliError(RuntimeError): """A controlled Ark CLI failure safe to map to an API error code.""" def __init__(self, code: str, message: str) -> None: super().__init__(message) self.code = code self.message = message @dataclass(frozen=True, slots=True) class CommandResult: returncode: int stdout: str stderr: str timed_out: bool = False cancelled: bool = False CommandRunner = Callable[[Sequence[str], float, threading.Event | None], CommandResult] def _child_env(source: Mapping[str, str] | None = None) -> dict[str, str]: """Give Ark CLI desktop context without inheriting daemon credentials.""" values = source if source is not None else os.environ return { key: str(value) for key, value in values.items() if key.upper() in _SAFE_ENV_NAMES and value } _ARKCLI_SKILLS = { "auth": "arkcli-auth", "plans": "arkcli-plans", "usage": "arkcli-usage", "profile": "arkcli-profile", "api": "arkcli-api-explorer", } def _command_env(argv: Sequence[str]) -> dict[str, str]: child_env = _child_env() # The domain sits at argv[1] when arkcli runs directly and at argv[2] # when a resolved batch shim prepends the node interpreter and script. skill_name = "arkcli-auth" for part in argv[1:3]: matched = _ARKCLI_SKILLS.get(str(part).lower()) if matched: skill_name = matched break child_env.update( { "ARKCLI_CALLER_TYPE": "ai_agent", "ARKCLI_CALLER_NAME": "openai4s", "ARKCLI_SKILL_NAME": skill_name, } ) if os.name != "nt" and str(argv[0]).lower().endswith(".exe"): # A Windows arkcli.exe reached through WSL interop receives only the # Linux variables WSLENV names, and the allowlist never copies the # daemon's own WSLENV. Name exactly the caller identity and the # already-allowlisted proxy settings a Linux arkcli would see. `/w` is # "only when running Win32 from WSL"; `/u` is the opposite direction, # and `/p` would rewrite a proxy URL as a path. child_env["WSLENV"] = ":".join( f"{key}/w" for key in child_env if key.startswith("ARKCLI_") or key.upper().endswith("_PROXY") ) return child_env def _run_bounded( argv: Sequence[str], timeout_s: float, cancel_event: threading.Event | None = None, ) -> CommandResult: """Run one fixed argv with bounded stdout/stderr and cancellable waiting.""" creationflags = 0 if os.name == "nt": creationflags = int(getattr(subprocess, "CREATE_NO_WINDOW", 0)) process = subprocess.Popen( list(argv), stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=_command_env(argv), shell=False, creationflags=creationflags, ) stdout = bytearray() stderr = bytearray() def drain(pipe: Any, target: bytearray) -> None: try: while True: chunk = pipe.read(16_384) if not chunk: return remaining = _MAX_OUTPUT_BYTES - len(target) if remaining > 0: target.extend(chunk[:remaining]) finally: try: pipe.close() except Exception: # noqa: BLE001 - process cleanup is best effort pass readers = [ threading.Thread(target=drain, args=(process.stdout, stdout), daemon=True), threading.Thread(target=drain, args=(process.stderr, stderr), daemon=True), ] for reader in readers: reader.start() deadline = time.monotonic() + max(0.1, float(timeout_s)) timed_out = False cancelled = False while process.poll() is None: if cancel_event is not None and cancel_event.is_set(): cancelled = True process.kill() break if time.monotonic() >= deadline: timed_out = True process.kill() break time.sleep(0.05) try: returncode = process.wait(timeout=2.0) except subprocess.TimeoutExpired: process.kill() returncode = process.wait() for reader in readers: reader.join(timeout=2.0) return CommandResult( returncode=returncode, stdout=bytes(stdout).decode("utf-8", errors="replace"), stderr=bytes(stderr).decode("utf-8", errors="replace"), timed_out=timed_out, cancelled=cancelled, ) _SHIM_SCRIPT = re.compile( r"%(?:~dp0|dp0%)[\\/]([^\"\r\n<>|*?%]+?\.(?:js|cjs|mjs))", re.IGNORECASE, ) def _resolve_batch_shim( path: str, *, which: Callable[[str], str | None] = shutil.which ) -> list[str] | None: """Bypass an npm ``.cmd`` shim (the BatBadBut class of argv corruption). CreateProcess hands a batch file to cmd.exe, whose argument re-parsing does not match ``list2cmdline`` quoting, so JSON ``--params`` arguments get mangled. When the shim's Node entry script can be located, running ``node