# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0

"""`unsloth start` — launch a coding agent against a running Unsloth server."""

import atexit
import base64
import contextlib
import errno
import json
import os
import re
import shlex
import shutil
import signal
import subprocess
import sys
import tempfile
import threading
import time
import urllib.error
import urllib.request
from pathlib import Path
from typing import Literal, NamedTuple, NoReturn, Optional
from urllib.parse import urlencode, urlparse

import click
import typer
from typer.core import TyperCommand

from unsloth_cli._inference import (
    _USER_AGENT,
    _studio_token,
    ensure_studio_backend_path,
    find_studio_server,
    is_loopback_url,
    raise_for_deferred_error,
    require_completed_padded_body,
    urlopen_no_redirect,
    verify_studio_identity,
)

start_app = typer.Typer(
    help = "Start a coding agent against a running Unsloth server.",
    no_args_is_help = True,
    context_settings = {"help_option_names": ["-h", "--help"]},
)

_CODEX_PROFILE = "unsloth_api"
_CODEX_ENV_KEY = "UNSLOTH_STUDIO_AUTH_TOKEN"
_HERMES_ENV_KEY = "UNSLOTH_API_KEY"
_HERMES_PROVIDER = "unsloth"
# Skip the installer's interactive setup wizard: `unsloth start hermes` runs
# this hint unattended and then writes its own session-scoped Hermes config, so
# the wizard's global API-key/model prompts would block the launch and point the
# user at a different (global) provider than the one Unsloth just configured.
# Both installers expose a skip flag: `-SkipSetup` (PowerShell) and
# `--skip-setup` (POSIX; passed to the piped script via `bash -s --`). Pin both
# the fetched script and the repository checkout it performs to the same full
# commit so a later change to either upstream branch cannot silently replace
# code that Unsloth executes with the user's privileges.
_HERMES_INSTALL_COMMIT = "f1af945f6c576eccb126fa955edc9be258b33020"
_HERMES_INSTALL_BASE = (
    "https://raw.githubusercontent.com/NousResearch/hermes-agent/"
    f"{_HERMES_INSTALL_COMMIT}/scripts"
)
_HERMES_WINDOWS_INSTALL_HINT = (
    f"& ([scriptblock]::Create((irm {_HERMES_INSTALL_BASE}/install.ps1)))"
    f" -SkipSetup -Commit {_HERMES_INSTALL_COMMIT}"
)
_HERMES_POSIX_INSTALL_HINT = (
    f"curl -fsSL {_HERMES_INSTALL_BASE}/install.sh | bash -s --"
    f" --skip-setup --commit {_HERMES_INSTALL_COMMIT}"
)
# Hermes refuses to initialize when the model window is under 64,000 tokens; its
# error message points at the model.context_length / auxiliary.compression
# overrides in config.yaml. write_hermes_config claims this value for smaller
# windows and scales the compaction threshold back down to the real window.
_HERMES_MIN_CONTEXT = 65536
_PI_PROVIDER = "unsloth"
_SUBAGENT_NAME = "unsloth"
_SUBAGENT_DESCRIPTION = (
    "Local coding subagent powered by Unsloth for debugging, implementation, and codebase "
    "research. Use when the user asks to spawn an Unsloth or local agent."
)
_SUBAGENT_INSTRUCTIONS = (
    "You are a local coding subagent powered by Unsloth. Complete the assigned task directly, "
    "use the available tools when useful, verify your work, and return a concise result to the "
    "parent agent."
)
_SUBAGENT_PLAN_DESCRIPTION = (
    "Read-only local coding subagent powered by Unsloth for planning and codebase research. "
    "Use this local agent when Claude is in plan mode."
)
_SUBAGENT_PLAN_INSTRUCTIONS = (
    "You are a read-only local coding subagent powered by Unsloth. Investigate the assigned "
    "task with read-only tools, produce a concrete plan or answer, and return a concise result "
    "to the parent agent. Do not modify files."
)
_CLAUDE_SUBAGENT_MCP_MODULE = "unsloth_cli.claude_subagent_mcp"
_CLAUDE_SUBAGENT_TOOL = "mcp__plugin_unsloth-local-agent_unsloth__unsloth_agent"
_CLAUDE_SUBAGENT_PLAN_TOOL = "mcp__plugin_unsloth-local-agent_unsloth__unsloth_plan_agent"
_CODEX_SUBAGENT_MCP_MODULE = "unsloth_cli.codex_subagent_mcp"
_CODEX_SUBAGENT_MCP_SERVER = "unsloth_local_agent"
_CODEX_SUBAGENT_MCP_TOOL = "spawn_local_agent"
_CODEX_SUBAGENT_CONFIG_ENV = "UNSLOTH_CODEX_SUBAGENT_CONFIG"
_CODEX_PARENT_OVERLAY_MANIFEST = ".unsloth-parent-overlay.json"
_CODEX_EPHEMERAL_STALE_SECONDS = 24 * 60 * 60
_CODEX_EPHEMERAL_HEARTBEAT_SECONDS = 60
_CODEX_SUBAGENT_TOOL_DESCRIPTION = (
    f"{_SUBAGENT_DESCRIPTION} Use this tool instead of the built-in spawn_agent tool for those "
    "requests. Other subagent requests may use the built-in tools normally."
)
_CODEX_SUBAGENT_ROUTING_INSTRUCTIONS = (
    "When the user asks to spawn an Unsloth agent or local agent, you must call the "
    "spawn_local_agent MCP tool once with the complete task. Do not answer, simulate the "
    "result, call wait, or use a built-in subagent before calling the tool. Use built-in "
    "subagents for other delegation requests."
)
_PI_SUBAGENT_EXTENSION = Path(__file__).parent.parent / "pi_subagent.ts"
# OpenCode selects a model by "<providerID>/<modelID>". Use a dedicated id to avoid
# colliding with a user's providers; provider filters are set in the launch-time overlay.
_OPENCODE_PROVIDER = "unsloth-studio"
_PROVIDER_HEADER = f"[model_providers.{_CODEX_PROFILE}]"
_PASSTHROUGH = {"allow_extra_args": True, "ignore_unknown_options": True}


class _PassthroughCommand(TyperCommand):
    """Preserve the option separator when forwarding arguments to an agent."""

    def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
        raw_args = list(args)
        try:
            separator = raw_args.index("--")
        except ValueError:
            return super().parse_args(ctx, args)
        trailing_count = len(raw_args) - separator - 1
        remaining = super().parse_args(ctx, args)
        insert_at = max(0, len(remaining) - trailing_count)
        if insert_at >= len(remaining) or remaining[insert_at] != "--":
            remaining.insert(insert_at, "--")
            ctx.args = remaining
        return remaining


_CLAUDE_ENV_UNSET = ("ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN")
_CODEX_ENV_UNSET = ("OPENAI_API_KEY", "CODEX_API_KEY", "CODEX_ACCESS_TOKEN")

# Shared by every agent command; only the config/env/command differ.
# Help is grouped into rich panels so `--help` reads as Model / Server / Session
# instead of one long unaligned list.
_PANEL_MODEL = "Model"
_PANEL_SERVER = "Server"
_PANEL_SAMPLING = "Sampling"
_PANEL_SESSION = "Agent session"

_MODEL_OPTION = typer.Option(
    None,
    "--model",
    "-m",
    rich_help_panel = _PANEL_MODEL,
    help = "Model for the agent, or a bare `org/name(:variant)` positional. "
    "Defaults to the one loaded in Unsloth.",
)
_GGUF_VARIANT_OPTION = typer.Option(
    None,
    "--gguf-variant",
    rich_help_panel = _PANEL_MODEL,
    help = "GGUF quant variant to load (e.g. UD-Q4_K_XL). Defaults to UD-Q4_K_XL for "
    "unsloth/* GGUF repos, else Q4_K_M.",
)
_CONTEXT_OPTION = typer.Option(
    0,
    "--max-seq-length",
    "--context-length",
    rich_help_panel = _PANEL_MODEL,
    help = "Context length in tokens for the load (0 = model default).",
)
_LOAD_4BIT_OPTION = typer.Option(
    True,
    "--load-in-4bit/--no-load-in-4bit",
    rich_help_panel = _PANEL_MODEL,
    help = "Load hub models in 4-bit (ignored for GGUF).",
)
_TENSOR_PARALLEL_OPTION = typer.Option(
    False,
    "--tensor-parallel/--no-tensor-parallel",
    rich_help_panel = _PANEL_MODEL,
    help = "Split a GGUF across GPUs by tensor instead of by layer (multi-GPU only).",
)
_GPU_MEMORY_MODE_OPTION = typer.Option(
    None,
    "--gpu-memory-mode",
    rich_help_panel = _PANEL_MODEL,
    help = (
        "GPU memory strategy for GGUF models loaded by this command. Auto lets "
        "Unsloth manage placement. Manual with default layers and context delegates "
        "placement and sizing to llama.cpp --fit. Omit when attaching to preserve "
        "the running model's mode."
    ),
)

# Server knobs. Only used when `unsloth start` auto-starts the server (--serve);
# they have no effect when attaching to a server someone else already started.
_SERVE_OPTION = typer.Option(
    True,
    "--serve/--no-serve",
    rich_help_panel = _PANEL_SERVER,
    help = "If no Unsloth server is running, auto-start one for --model and keep it "
    "available after the agent exits. --no-serve errors out instead.",
)
_ENABLE_TOOLS_OPTION = typer.Option(
    False,
    "--enable-tools/--disable-tools",
    rich_help_panel = _PANEL_SERVER,
    help = "Server-side tools (web search, code execution) for the auto-started server. "
    "Default off so the agent's own tools are relayed unchanged.",
)
_TOOL_CALL_HEALING_OPTION = typer.Option(
    None,
    "--enable-tool-call-healing/--disable-tool-call-healing",
    rich_help_panel = _PANEL_SERVER,
    help = "Promote text-form tool calls from small GGUFs back into structured calls. On by "
    "default; when the flag is omitted an inherited UNSLOTH_DISABLE_TOOL_CALL_HEALING is kept.",
)
_TOOL_CALL_NUDGING_OPTION = typer.Option(
    None,
    "--enable-tool-call-nudging/--disable-tool-call-nudging",
    rich_help_panel = _PANEL_SERVER,
    help = "Retry once with a nudge when a non-streaming passthrough tool call can't be healed. "
    "On by default; when the flag is omitted an inherited UNSLOTH_TOOL_CALL_NUDGE is kept.",
)
_REASONING_OPTION = typer.Option(
    None,
    "--reasoning",
    rich_help_panel = _PANEL_SERVER,
    help = (
        "llama-server reasoning mode for an auto-started coding-agent server. "
        "Defaults to off so tool calls stay in the structured tool channel; use "
        "'auto' or 'on' to opt back into model reasoning."
    ),
)
# Sampling overrides pin a value on the auto-started server (winning over the client and the
# per-model recommendation). Default unset -> the model's recommended sampling is used.
_TEMPERATURE_OPTION = typer.Option(
    None,
    "--temperature",
    min = 0.0,
    max = 2.0,
    rich_help_panel = _PANEL_SAMPLING,
    help = "Pin the sampling temperature. Default: unset (per-model recommendation).",
)
_TOP_P_OPTION = typer.Option(
    None,
    "--top-p",
    min = 0.0,
    max = 1.0,
    rich_help_panel = _PANEL_SAMPLING,
    help = "Pin top-p (nucleus) sampling. Default: unset (per-model recommendation).",
)
_TOP_K_OPTION = typer.Option(
    None,
    "--top-k",
    min = -1,
    max = 100,
    rich_help_panel = _PANEL_SAMPLING,
    help = "Pin top-k sampling. Default: unset (per-model recommendation).",
)
_MIN_P_OPTION = typer.Option(
    None,
    "--min-p",
    min = 0.0,
    max = 1.0,
    rich_help_panel = _PANEL_SAMPLING,
    help = "Pin min-p sampling threshold. Default: unset (per-model recommendation).",
)
_REPETITION_PENALTY_OPTION = typer.Option(
    None,
    "--repetition-penalty",
    min = 1.0,
    max = 2.0,
    rich_help_panel = _PANEL_SAMPLING,
    help = "Pin the repetition penalty. Default: unset (per-model recommendation).",
)
_PRESENCE_PENALTY_OPTION = typer.Option(
    None,
    "--presence-penalty",
    min = 0.0,
    max = 2.0,
    rich_help_panel = _PANEL_SAMPLING,
    help = "Pin the presence penalty. Default: unset (per-model recommendation).",
)

# Agent-session knobs.
_KEY_OPTION = typer.Option(
    None,
    "--api-key",
    envvar = "UNSLOTH_API_KEY",
    rich_help_panel = _PANEL_SESSION,
    help = "Unsloth API key. For a local Unsloth it is minted automatically and "
    "remembered per server. For a remote server, pass one with --api-key "
    "(or UNSLOTH_API_KEY); it is remembered for next time.",
)
_LAUNCH_OPTION = typer.Option(
    True,
    "--launch/--no-launch",
    rich_help_panel = _PANEL_SESSION,
    help = "--no-launch prints the env and command instead (remote shells, WSL).",
)
# One normalized "run tools without prompting" switch. Each agent spells this
# differently and it's easy to forget which is which, so accept every spelling and
# route to the agent's own mechanism in _yolo_command_flags / the config writers.
_YOLO_OPTION = typer.Option(
    False,
    "--yolo",
    "--dangerously-skip-permissions",
    "--dangerously-bypass-approvals-and-sandbox",
    rich_help_panel = _PANEL_SESSION,
    help = "Auto-approve all tool actions for this session; routed to the agent's own "
    "flag/config. Any of the three spellings works for any agent.",
)
_PERSIST_OPTION = typer.Option(
    False,
    "--persist/--no-persist",
    rich_help_panel = _PANEL_SESSION,
    help = (
        "Keep this agent's Unsloth-managed session dir so you can resume it later. "
        "codex/openclaw/hermes/pi have their whole home relocated into an Unsloth dir "
        "that is a throwaway temp dir (wiped on exit) by default; with --persist it "
        "lives under the Unsloth agents dir and survives, so their own resume can reopen "
        "it. claude and opencode keep sessions in your own stores (~/.claude, "
        "~/.local/share/opencode), so they already resume regardless. To reopen a "
        "session, pass the agent's own resume command through, e.g. "
        "`unsloth start codex --persist resume` or `claude --resume <id>`; those flow to "
        "the agent unchanged."
    ),
)
_AS_SUBAGENT_OPTION = typer.Option(
    False,
    "--as-subagent",
    rich_help_panel = _PANEL_SESSION,
    help = "Keep the coding agent's current model and add Unsloth as a local subagent.",
)

# Per-agent CLI flag for "run tools without prompting". OpenCode (native --auto is
# command-scoped, handled below) and OpenClaw (config-only) are absent from this prefix map.
_YOLO_COMMAND_FLAGS = {
    "claude": ["--dangerously-skip-permissions"],
    "codex": ["--dangerously-bypass-approvals-and-sandbox"],
    "hermes": ["--yolo"],
    # Pi never prompts per tool call; its only approval gate is project trust, so -a
    # (trust project resources) is the closest "don't ask me" equivalent.
    "pi": ["--approve"],
}


def _yolo_command_flags(agent: str, yolo: bool) -> list:
    # .get so a config-based agent (or a typo) yields no flag instead of a KeyError.
    return _YOLO_COMMAND_FLAGS.get(agent, []) if yolo else []


# Subcommands that reject --auto (OpenCode exposes it only on the default TUI and `run`),
# so `opencode serve --auto` is never emitted. Includes console/generate, hidden from
# `opencode --help` but still registered. Unknown first positionals are TUI paths -> --auto.
_OPENCODE_NON_AUTO_SUBCOMMANDS = frozenset(
    "completion acp mcp attach debug providers auth agent upgrade uninstall serve web "
    "models stats export import github pr session plugin plug db console generate".split()
)
_OPENCODE_GLOBAL_BOOLEAN_OPTIONS = frozenset(
    "-h --help -v --version --print-logs --pure --mdns".split()
)
_OPENCODE_GLOBAL_VALUE_OPTIONS = frozenset(
    "--log-level --port --hostname --mdns-domain --cors".split()
)
_OPENCODE_NATIVE_AUTO_MIN_VERSION = (1, 17, 12)


def _opencode_supports_native_auto() -> bool:
    executable = _which_with_install_dirs("opencode")
    if executable is None:
        # No local binary: a --no-launch recipe may run elsewhere, and _run installs the
        # current release on launch -- either way assume native --auto is available.
        return True
    try:
        output = subprocess.check_output(
            [executable, "--version"],
            text = True,
            timeout = 10,
            stderr = subprocess.DEVNULL,
            env = _probe_env(),
        )
    except Exception:
        return False
    match = re.search(r"(\d+)\.(\d+)\.(\d+)", output)
    return bool(match) and tuple(int(part) for part in match.groups()) >= (
        _OPENCODE_NATIVE_AUTO_MIN_VERSION
    )


def _opencode_subcommand(args: list[str]) -> Optional[str]:
    """Return an explicit OpenCode subcommand after supported global options."""
    index = 0
    while index < len(args):
        arg = args[index]
        if arg == "--":
            return None
        if arg in _OPENCODE_GLOBAL_BOOLEAN_OPTIONS:
            index += 1
            continue
        if arg in _OPENCODE_GLOBAL_VALUE_OPTIONS:
            index += 2
            continue
        if any(arg.startswith(f"{option}=") for option in _OPENCODE_GLOBAL_VALUE_OPTIONS):
            index += 1
            continue
        # A non-global option (e.g. --session) is a TUI flag; stop before its value is
        # mistaken for a subcommand.
        if arg.startswith("-"):
            return None
        return arg
    return None


def _opencode_native_auto_args(args: list[str], yolo: bool) -> tuple[list[str], bool]:
    """Add OpenCode's native --auto when the selected command supports it."""
    routed = list(args)
    if not yolo:
        return routed, False
    if _opencode_subcommand(routed) in _OPENCODE_NON_AUTO_SUBCOMMANDS:
        return routed, False
    separator = routed.index("--") if "--" in routed else len(routed)
    # --mini's runMini TUI forces auto=false and never forwards --auto, so appending it is
    # useless; fall back to the config permission block so --yolo still auto-approves.
    if any(arg == "--mini" or arg.startswith("--mini=") for arg in routed[:separator]):
        return routed, False
    if "--auto" not in routed[:separator]:
        routed.insert(separator, "--auto")
    return routed, True


def _hermes_install_hint() -> str:
    return _HERMES_WINDOWS_INSTALL_HINT if os.name == "nt" else _HERMES_POSIX_INSTALL_HINT


def _npm_install_hint(package: str, *, ignore_scripts: bool = False) -> str:
    parts = ["npm", "install", "-g"]
    if os.name != "nt":
        # No home (bare container UID): fall back to npm's own prefix instead of failing.
        try:
            parts.extend(("--prefix", str(Path.home() / ".local")))
        except (RuntimeError, OSError):
            pass
    if ignore_scripts:
        parts.append("--ignore-scripts")
    parts.append(package)
    if os.name == "nt":
        return " ".join(_powershell_quote(part) for part in parts)
    return shlex.join(parts)


def _hermes_resume_oneshot_args(args: list[str]) -> list[str]:
    """Route resumed one-shot prompts through Hermes' session-aware chat command."""
    has_resume = any(
        arg in ("--resume", "-r", "--continue", "-c")
        or arg.startswith(("--resume=", "--continue="))
        or (len(arg) > 2 and arg.startswith(("-r", "-c")))
        for arg in args
    )
    if not has_resume:
        return args

    rewritten = list(args)
    for index, arg in enumerate(rewritten):
        if arg in ("-z", "--oneshot"):
            rewritten[index] = "-q"
        elif len(arg) > 2 and arg.startswith("-z"):
            # argparse accepts attached short-option values (`-zPROMPT` and
            # `-z=PROMPT`); preserve the value byte-for-byte when switching to -q.
            rewritten[index] = f"-q{arg[2:]}"
        elif arg.startswith("--oneshot="):
            rewritten[index] = f"--query={arg.partition('=')[2]}"
        else:
            continue
        if any(item == "--usage-file" or item.startswith("--usage-file=") for item in args):
            raise typer.BadParameter(
                "Hermes cannot resume a one-shot session with --usage-file; remove that option."
            )
        # `chat -Q -q` is the only mode that can resume a session, but it does not
        # inherit hermes' one-shot approval semantics: `-z` itself sets
        # HERMES_YOLO_MODE=1 and HERMES_ACCEPT_HOOKS=1 for the call (hermes_cli/
        # oneshot.py in _HERMES_INSTALL_COMMIT), because a one-shot has no user at
        # the terminal to answer a prompt. Re-add the equivalent flags so a resumed `-z` keeps behaving like a
        # plain `-z` instead of stalling on an approval nobody can answer. This is
        # parity with the flag the user already typed, not the --yolo option: a
        # resumed *interactive* session still prompts as usual.
        prefix = ["chat", "-Q"]
        if "--yolo" not in rewritten:
            prefix.append("--yolo")
        if "--accept-hooks" not in rewritten:
            prefix.append("--accept-hooks")
        rewritten = prefix + rewritten
        return rewritten
    return args


class LoadOptions(NamedTuple):
    """Model-load knobs forwarded to /api/inference/load when --model triggers a load."""

    gguf_variant: Optional[str] = None
    max_seq_length: int = 0
    load_in_4bit: bool = True
    tensor_parallel: bool = False
    gpu_memory_mode: Optional[Literal["auto", "manual"]] = None


class ServerOptions(NamedTuple):
    """Tool-call knobs forwarded to an auto-started `unsloth run` server."""

    enable_tools: bool = False
    tool_call_healing: Optional[bool] = None
    tool_call_nudging: Optional[bool] = None
    reasoning: Optional[Literal["on", "off", "auto"]] = None
    temperature: Optional[float] = None
    top_p: Optional[float] = None
    top_k: Optional[int] = None
    min_p: Optional[float] = None
    repetition_penalty: Optional[float] = None
    presence_penalty: Optional[float] = None


def _split_repo_variant(model: str) -> tuple:
    """Split ``org/name:QUANT`` into ``(repo, variant)`` -> ``("org/name", "QUANT")``.

    ``unsloth run`` and llama.cpp accept ``--model org/name:QUANT`` as shorthand for
    ``--model org/name --gguf-variant QUANT``. Mirror that here so a ``:variant`` suffix
    resolves against the already-loaded ``org/name`` (which /v1/models lists without the
    suffix) instead of trying to load a repo id containing ``:`` -- which Hugging Face
    rejects, and which would evict a model another session is using. Local paths, Windows
    drive letters, and ids without a ``:`` pass through unchanged.
    """
    s = (model or "").strip()
    if not s or s.startswith(("/", "./", "../", "~")) or s == ".":
        return s, None
    if len(s) >= 2 and s[1] == ":" and s[0].isalpha():  # Windows drive, e.g. C:\models\x
        return s, None
    if ":" not in s:
        return s, None
    repo, _, variant = s.rpartition(":")
    if not repo or not variant or "/" in variant:
        return s, None
    return repo, variant


def _looks_like_model(token: str) -> bool:
    """True for a bare `org/name(:variant)` hub id that is not a flag or a local path.

    Reuses `_is_hub_model_id`, so a relative dir like `owner/repo` that actually exists
    is left for the agent (e.g. OpenCode opens it as a project) instead of being taken
    as a model; a non-existent `org/name` is treated as a hub id.
    """
    if not token or token.startswith("-") or " " in token:
        return False
    repo, _ = _split_repo_variant(token)
    return _is_hub_model_id(repo)


def _consume_positional_model(model: Optional[str], args: list) -> tuple:
    """Route a leading `org/name` positional to --model when --model was not given.

    Only the FIRST token is considered so an option value like `--profile owner/repo`
    is never stolen, and only when --model is absent so an explicit --model always wins.
    Returns (model, remaining_args) with the consumed token removed from the passthrough.
    """
    args = list(args)
    if model or not args or not _looks_like_model(args[0]):
        return model, args
    return args[0], args[1:]


def _display_model_spec(model: str, variant: Optional[str]) -> str:
    """Return a user-facing model name that includes the selected GGUF variant."""
    repo, inline_variant = _split_repo_variant(model)
    selected_variant = variant or inline_variant
    return f"{repo}:{selected_variant}" if selected_variant else model


def _subagent_model_id(
    base: str,
    key: str,
    entry: dict,
    requested_model: Optional[str],
    requested_variant: Optional[str],
) -> str:
    """Return an API model id that preserves the selected GGUF variant.

    Coding-agent model definitions outlive the initial load. If Unsloth later
    unloads the model, a bare repository id may resolve to a different cached
    quant. Include the explicit or currently loaded variant so an automatic
    reload selects the same weights.
    """
    model_id = str(entry["id"])
    _, inline_variant = _split_repo_variant(requested_model or "")
    variant = requested_variant or inline_variant
    if not variant:
        try:
            status = _http_json("GET", f"{base}/api/inference/status", key)
        except Exception:
            status = {}
            typer.echo(
                "Warning: could not verify the loaded GGUF variant; a later reload "
                "may pick a different cached quant. Pass :variant to pin it.",
                err = True,
            )
        if status.get("is_gguf"):
            variant = status.get("gguf_variant")
    if variant and _is_hub_model_id(model_id):
        return _display_model_spec(model_id, str(variant))
    if variant:
        # A path load is advertised as a bare basename with no ":variant" channel,
        # so the quant cannot be recorded and a later reload picks for itself.
        typer.echo(
            f"Warning: {model_id} loaded from a path, so the subagent config cannot "
            f"pin the {variant} quant; a reload may choose a different one. Load the "
            "model by repository id to pin it.",
            err = True,
        )
    return model_id


def _fail(message: str) -> NoReturn:
    typer.echo(message, err = True)
    raise typer.Exit(code = 1)


def _reject_as_subagent(agent: str, args: list) -> None:
    # Reject early, or the flag reaches the agent binary after Studio loaded the model.
    if any(arg == "--as-subagent" or arg.startswith("--as-subagent=") for arg in args):
        _fail(f"--as-subagent is not supported for {agent}.")


def _http_error_detail(exc: urllib.error.HTTPError) -> str:
    try:
        body = json.loads(exc.read().decode())
        return body.get("detail") or body["error"]["message"]
    except Exception:
        return str(exc)


def _http_json(
    method: str,
    url: str,
    token: str,
    payload = None,
    timeout = 30,
    error = None,
):
    """On HTTPError: raise if `error` is None, else fail with `error` plus the server's detail."""
    request = urllib.request.Request(
        url,
        data = None if payload is None else json.dumps(payload).encode(),
        headers = {
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
            "User-Agent": _USER_AGENT,
        },
        method = method,
    )
    try:
        # No redirects: a 3xx would leak this bearer token to an unvetted base.
        with urlopen_no_redirect(request, timeout = timeout) as response:
            body = json.loads(response.read().decode() or "{}")
        # A padded /load or /unload commits its 200 early, so a late failure arrives
        # in-band; raise it as the HTTPError handled below.
        return raise_for_deferred_error(url, body)
    except urllib.error.HTTPError as exc:
        if error is None:
            raise
        _fail(f"{error}: {_http_error_detail(exc)}")
    except (urllib.error.URLError, TimeoutError) as exc:
        if error is None:
            raise
        _fail(f"{error}: {getattr(exc, 'reason', None) or exc}")


# A server that WE auto-started (never one we merely found). Kept at module scope so
# failure paths and the atexit backstop can tear it down without threading a handle
# through all six agent commands. Only one agent runs per process, so one slot is enough.
_auto_served_server: Optional[subprocess.Popen] = None
# Model download + load can be slow; give the auto-started server room before giving up.
_SERVER_START_TIMEOUT_S = 900
_DOWNLOAD_POLL_INTERVAL_S = 1.0
_START_API_KEY_PREFIX = "UNSLOTH_START_API_KEY: "
_START_API_KEY_MARKER_ENV = "_UNSLOTH_START_API_KEY_MARKER"


def _format_download_bytes(value: int) -> str:
    value = max(0, int(value))
    for unit in ("B", "KiB", "MiB", "GiB", "TiB"):
        if value < 1024 or unit == "TiB":
            precision = 0 if unit in ("B", "KiB") else 1
            return f"{value:.{precision}f} {unit}"
        value /= 1024
    return "0 B"


def _format_download_eta(seconds: float) -> str:
    seconds = max(0, int(seconds))
    if seconds < 60:
        return f"{seconds}s"
    minutes, seconds = divmod(seconds, 60)
    if minutes < 60:
        return f"{minutes}m {seconds:02d}s"
    hours, minutes = divmod(minutes, 60)
    return f"{hours}h {minutes:02d}m"


class _DownloadProgressDisplay:
    """Render download progress without making redirected output noisy."""

    def __init__(self) -> None:
        self._samples: list[tuple[float, int]] = []
        self._shown = False
        self._last_bucket = -1
        self._last_line_length = 0
        self._last_expected = 0
        self._interactive = bool(getattr(sys.stdout, "isatty", lambda: False)())

    def update(self, progress: dict) -> None:
        downloaded = max(0, int(progress.get("downloaded_bytes") or 0))
        completed = max(0, int(progress.get("completed_bytes") or 0))
        expected = max(0, int(progress.get("expected_bytes") or 0))
        self._last_expected = max(self._last_expected, expected)
        fraction = float(progress.get("progress") or 0)
        if downloaded <= 0:
            return
        # A fully cached snapshot can report 99% with no incomplete bytes; that is
        # not a transfer, so don't show it as a download.
        if completed >= downloaded > 0:
            return

        now = time.monotonic()
        if self._samples and downloaded < self._samples[-1][1]:
            self._samples.clear()
        self._samples.append((now, downloaded))
        cutoff = now - 15.0
        while len(self._samples) > 2 and self._samples[0][0] < cutoff:
            self._samples.pop(0)

        rate = 0.0
        if len(self._samples) >= 2:
            elapsed = self._samples[-1][0] - self._samples[0][0]
            delta = self._samples[-1][1] - self._samples[0][1]
            if elapsed >= 1.0 and delta > 0:
                rate = delta / elapsed

        if expected > 0:
            # The endpoint caps at 99% while bytes remain in an incomplete file; trust it.
            fraction = min(1.0, max(0.0, fraction))
            percent = min(100, max(0, int(fraction * 100)))
            filled = min(24, int(fraction * 24))
            bar = "=" * filled + ">" + "." * max(0, 23 - filled) if filled < 24 else "=" * 24
            line = (
                f"Downloading model [{bar}] {percent:3d}% "
                f"{_format_download_bytes(downloaded)} / {_format_download_bytes(expected)}"
            )
            bucket = percent // 10
            if rate > 0:
                line += f" | {_format_download_bytes(rate)}/s"
                if downloaded < expected:
                    line += f" | ETA {_format_download_eta((expected - downloaded) / rate)}"
        else:
            line = f"Downloading model: {_format_download_bytes(downloaded)}"
            bucket = downloaded // (1024**3)
            if rate > 0:
                line += f" | {_format_download_bytes(rate)}/s"

        if self._interactive:
            padding = " " * max(0, self._last_line_length - len(line))
            typer.echo(f"\r{line}{padding}", nl = False)
            sys.stdout.flush()
            self._last_line_length = len(line)
        elif not self._shown or bucket > self._last_bucket:
            typer.echo(line)
            self._last_bucket = bucket
        self._shown = True

    def close(self) -> None:
        if self._interactive and self._shown:
            typer.echo()
        self._last_line_length = 0

    def complete(self) -> None:
        """Finish a displayed transfer after the model load confirms success."""
        if not self._shown:
            return
        downloaded = self._samples[-1][1] if self._samples else 0
        expected = max(downloaded, getattr(self, "_last_expected", 0))
        self.update(
            {
                "downloaded_bytes": expected,
                "expected_bytes": expected,
                "progress": 1.0,
            }
        )


def _normalized_variant(value: object) -> str:
    return re.sub(r"[^a-z0-9]", "", str(value or "").lower())


class _ModelDownloadProgress:
    """Best-effort polling of the model download endpoints."""

    def __init__(self, base: str, key: str, model: str, variant: Optional[str]) -> None:
        self._base = base
        self._key = key
        self._model = model
        self._variant = variant or ""
        self._expected_bytes = 0
        self._display = _DownloadProgressDisplay()
        self._configured = False
        self._disabled = not _is_hub_model_id(model)
        self._progress_prefix = "/api/hub"

    def _configure(self) -> None:
        self._configured = True
        if self._disabled:
            return
        # GGUF repos need the selected quant's size; the repo endpoint totals every
        # quant. Resolve the variant first, otherwise show bytes only.
        if self._variant or "gguf" in self._model.lower():
            try:
                params = urlencode({"repo_id": self._model})
                try:
                    info = _http_json(
                        "GET",
                        f"{self._base}/api/hub/gguf-variants?{params}",
                        self._key,
                        timeout = 10,
                    )
                except urllib.error.HTTPError as exc:
                    if exc.code != 404:
                        raise
                    self._progress_prefix = "/api/models"
                    info = _http_json(
                        "GET",
                        f"{self._base}/api/models/gguf-variants?{params}",
                        self._key,
                        timeout = 10,
                    )
                self._variant = self._variant or str(info.get("default_variant") or "")
                wanted = _normalized_variant(self._variant)
                for item in info.get("variants") or []:
                    quant = _normalized_variant(item.get("quant"))
                    filename = _normalized_variant(item.get("filename"))
                    if wanted and (wanted == quant or wanted in filename):
                        self._expected_bytes = int(
                            item.get("download_size_bytes") or item.get("size_bytes") or 0
                        )
                        break
            except Exception:
                # Older servers lack this endpoint; byte progress is still useful.
                pass

    def poll(self) -> None:
        if not self._configured:
            self._configure()
        if self._disabled:
            return
        try:
            if self._variant or "gguf" in self._model.lower():
                params = urlencode(
                    {
                        "repo_id": self._model,
                        "variant": self._variant,
                        "expected_bytes": self._expected_bytes,
                    }
                )
                url = f"{self._base}{self._progress_prefix}/gguf-download-progress?{params}"
            else:
                url = (
                    f"{self._base}{self._progress_prefix}/download-progress?"
                    f"{urlencode({'repo_id': self._model})}"
                )
            try:
                reading = _http_json("GET", url, self._key, timeout = 10)
            except urllib.error.HTTPError as exc:
                if exc.code != 404 or self._progress_prefix == "/api/models":
                    raise
                self._progress_prefix = "/api/models"
                self.poll()
                return
            self._display.update(reading)
        except Exception:
            # Progress is best-effort; never fail the load over a polling error.
            self._disabled = True

    def close(self) -> None:
        self._display.close()

    def complete(self) -> None:
        self._display.complete()


def _load_model_with_progress(
    base: str, key: str, model: str, load: LoadOptions, payload: dict
) -> dict:
    """Run the blocking load request while polling its download progress."""
    load_url = f"{base}/api/inference/load"
    result: list[tuple[bool, object]] = []
    done = threading.Event()

    def _load() -> None:
        try:
            value = _http_json(
                "POST",
                load_url,
                key,
                payload,
                timeout = 3600,
                error = "Model load failed",
            )
            result.append((True, value))
        except BaseException as exc:
            result.append((False, exc))
        finally:
            done.set()

    threading.Thread(target = _load, name = "unsloth-model-load", daemon = True).start()
    progress = _ModelDownloadProgress(base, key, model, load.gguf_variant)
    loading_announced = False
    try:
        while not done.wait(_DOWNLOAD_POLL_INTERVAL_S):
            if not loading_announced:
                typer.echo(f"Loading model: {_display_model_spec(model, load.gguf_variant)}")
                loading_announced = True
            progress.poll()
        ok, value = result[0]
        if not ok:
            assert isinstance(value, BaseException)
            # A pad-only or half-written body fails `_http_json`'s json.loads: report
            # the padded 200 that never completed, not a JSON error from the API.
            if isinstance(value, ValueError):
                require_completed_padded_body(load_url, None)
            raise value
        progress.complete()
        # `_http_json` decodes a blank body as `{}`, which would look like a completed load.
        return require_completed_padded_body(load_url, value)
    finally:
        progress.close()


def _studio_healthy(base: str, timeout: float = 3.0) -> bool:
    request = urllib.request.Request(f"{base}/api/health", headers = {"User-Agent": _USER_AGENT})
    try:
        with urllib.request.urlopen(request, timeout = timeout) as response:
            return json.loads(response.read(65536).decode() or "{}").get("status") == "healthy"
    except Exception:
        return False


def _log_tail(path: Path, lines: int = 20) -> str:
    try:
        return "\n".join(path.read_text(encoding = "utf-8", errors = "replace").splitlines()[-lines:])
    except OSError:
        return "(no server log)"


def _redacted_log_tail(path: Path, lines: int = 20) -> str:
    """Tail with minted keys removed; only for tails shown on the terminal."""
    return re.sub(r"sk-unsloth-\S+", "sk-unsloth-[redacted]", _log_tail(path, lines))


def _shutdown_server(server: Optional[subprocess.Popen]) -> None:
    # Idempotent teardown of a server WE started, plus its own children (llama-server,
    # cloudflared). A no-op once the process is already gone.
    if server is None or server.poll() is not None:
        return
    if os.name == "nt":
        # terminate()/kill() reach only the parent `unsloth run`; taskkill /T walks the
        # whole tree so the llama-server child doesn't keep the port and GPU (matches the
        # taskkill /T /F pattern already used in unsloth/dataprep/synthetic.py).
        try:
            subprocess.run(
                ["taskkill", "/PID", str(server.pid), "/T", "/F"],
                capture_output = True,
                timeout = 15,
                check = False,
            )
            server.wait(timeout = 5)
        except Exception:
            with contextlib.suppress(Exception):
                server.kill()
        return
    try:
        os.killpg(os.getpgid(server.pid), signal.SIGTERM)
    except OSError:
        server.terminate()
    try:
        server.wait(timeout = 15)
    except Exception:
        try:
            os.killpg(os.getpgid(server.pid), signal.SIGKILL)
        except OSError:
            server.kill()


def _shutdown_auto_served() -> None:
    global _auto_served_server
    server, _auto_served_server = _auto_served_server, None
    if server is not None and server.poll() is None:
        typer.echo("Stopping the auto-started Unsloth server…")
        _shutdown_server(server)


def _keep_auto_served() -> bool:
    """Release ownership so a successfully started server survives this CLI."""
    global _auto_served_server
    server, _auto_served_server = _auto_served_server, None
    atexit.unregister(_shutdown_auto_served)
    return server is not None and server.poll() is None


def _start_studio_server(
    base: str,
    model: str,
    load: LoadOptions,
    server: ServerOptions = ServerOptions(),
) -> subprocess.Popen:
    """Spawn `unsloth run` for `model`, wait until it is fully ready, and return it."""
    global _auto_served_server
    # Windows goes through this interpreter, not the launcher on PATH: shutil.which
    # resolves `unsloth` to the denied unsloth.exe, since PATHEXT puts .EXE ahead of
    # the .cmd shim (issue #8490). Without this, a user who reached the CLI through
    # unsloth.cmd would still fail here. sys.executable is the interpreter already
    # running this command, so the child inherits the same environment.
    if sys.platform == "win32":
        # Local import: unsloth_cli.commands.studio imports at package init after
        # this module, so a top-level import would be circular.
        from unsloth_cli.commands.studio import _managed_cli_argv
        launch_head = _managed_cli_argv(Path(sys.executable))
    else:
        launch_head = [shutil.which("unsloth") or "unsloth"]
    parsed = urlparse(base)
    # Tools default off = passthrough mode (relay the agent's own tools); --no-cloudflare =
    # loopback only, no tunnel. Mirrors .github/scripts/serve-unsloth-run.sh. Healing/nudging
    # travel via the child env below (version-agnostic) rather than new run flags that an
    # older re-exec'd run could mistake for llama-server args.
    command = [
        *launch_head,
        "run",
        "-H",
        parsed.hostname or "127.0.0.1",
        "-p",
        str(parsed.port or 8888),
        "--enable-tools" if server.enable_tools else "--disable-tools",
        "--no-cloudflare",
        "--model",
        model,
    ]
    if load.gguf_variant:
        command += ["--gguf-variant", load.gguf_variant]
    if load.max_seq_length:
        command += ["--context-length", str(load.max_seq_length)]
    if not load.load_in_4bit:
        command += ["--no-load-in-4bit"]
    if load.tensor_parallel:
        command += ["--tensor-parallel"]
    if load.gpu_memory_mode is not None:
        command += ["--gpu-memory-mode", load.gpu_memory_mode]

    log_path = Path(tempfile.gettempdir()) / f"unsloth-start-server-{os.getpid()}.log"
    typer.echo("Starting Unsloth server")
    typer.echo(f"Model: {_display_model_spec(model, load.gguf_variant)}")
    typer.echo(f"Server log: {log_path}")
    # 0600: the `unsloth run` banner in this log carries the minted sk-unsloth- key, and
    # the tempdir is world-traversable. Unlink first so a stale looser-mode file (pid
    # reuse) can't survive with its old permissions.
    log_path.unlink(missing_ok = True)
    log = os.fdopen(os.open(log_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600), "wb")
    # Own session/process group so a mid-session Ctrl+C (cancel a turn) doesn't reach the
    # server. It survives a successful agent session; torn down on startup/launch failure.
    child_env = os.environ.copy()
    # Current llama-server versions read this documented env equivalent of --reasoning.
    # Older managed versions ignore an unknown env variable instead of failing startup on
    # an unknown passthrough CLI flag. An omitted start option still defaults to off.
    child_env["LLAMA_ARG_REASONING"] = server.reasoning or "off"
    # Pass the marker via env so an older launcher ignores it instead of treating an
    # unknown CLI flag as a llama-server arg; new launchers preserve it across re-exec.
    child_env[_START_API_KEY_MARKER_ENV] = "1"
    # Convey healing/nudging through the env; `unsloth run` reads these when its own
    # flags are omitted, so this works even if run re-execs into an older Studio venv.
    # Only write when the operator set the flag explicitly; otherwise keep whatever they
    # already exported (child_env is a copy of os.environ), falling back to the start
    # defaults (healing on, nudging on) when nothing was inherited.
    if server.tool_call_healing is not None:
        child_env["UNSLOTH_DISABLE_TOOL_CALL_HEALING"] = "0" if server.tool_call_healing else "1"
    elif "UNSLOTH_DISABLE_TOOL_CALL_HEALING" not in child_env:
        child_env["UNSLOTH_DISABLE_TOOL_CALL_HEALING"] = "0"
    if server.tool_call_nudging is not None:
        child_env["UNSLOTH_TOOL_CALL_NUDGE"] = "1" if server.tool_call_nudging else "0"
    elif "UNSLOTH_TOOL_CALL_NUDGE" not in child_env:
        child_env["UNSLOTH_TOOL_CALL_NUDGE"] = "1"
    # Forward any sampling pin via the env; `unsloth run` reads UNSLOTH_SAMPLING_* and the
    # backend resolver applies it as a hard override. Only set fields the operator specified.
    for _sampling_env, _sampling_value in (
        ("UNSLOTH_SAMPLING_TEMPERATURE", server.temperature),
        ("UNSLOTH_SAMPLING_TOP_P", server.top_p),
        ("UNSLOTH_SAMPLING_TOP_K", server.top_k),
        ("UNSLOTH_SAMPLING_MIN_P", server.min_p),
        ("UNSLOTH_SAMPLING_REPETITION_PENALTY", server.repetition_penalty),
        ("UNSLOTH_SAMPLING_PRESENCE_PENALTY", server.presence_penalty),
    ):
        if _sampling_value is not None:
            child_env[_sampling_env] = str(_sampling_value)
    kwargs: dict = {
        "stdout": log,
        "stderr": subprocess.STDOUT,
        "stdin": subprocess.DEVNULL,
        "env": child_env,
    }
    if os.name == "nt":
        kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
    else:
        kwargs["start_new_session"] = True
    try:
        server = subprocess.Popen(command, **kwargs)
    finally:
        log.close()  # Popen dup'd the fd; drop the parent's copy
    _auto_served_server = server
    atexit.register(_shutdown_auto_served)

    deadline = time.monotonic() + _SERVER_START_TIMEOUT_S
    progress: Optional[_ModelDownloadProgress] = None
    early_key_seen = False
    try:
        while time.monotonic() < deadline:
            if server.poll() is not None:
                # The early key marker lands here before load finishes; redact it.
                tail = _redacted_log_tail(log_path)
                _shutdown_auto_served()
                _fail(f"The Unsloth server stopped before it was ready. Last log lines:\n{tail}")
            tail = _log_tail(log_path, lines = 400)
            if progress is None:
                marker = re.search(
                    rf"^{re.escape(_START_API_KEY_PREFIX)}(sk-unsloth-[^\s]+)$",
                    tail,
                    flags = re.MULTILINE,
                )
                if marker:
                    early_key_seen = True
                    progress = _ModelDownloadProgress(
                        base,
                        marker.group(1),
                        model,
                        load.gguf_variant,
                    )
            if progress is not None:
                progress.poll()
            # New children emit an early key marker, so wait for the final model banner;
            # older children only print the key after load, so fall back to that.
            ready_signal = "Model loaded:" in tail if early_key_seen else "sk-unsloth-" in tail
            if _studio_healthy(base) and ready_signal:
                if progress is not None:
                    progress.complete()
                    progress.close()
                    progress = None
                return server
            time.sleep(2.0)
    finally:
        if progress is not None:
            progress.close()
    _shutdown_auto_served()
    _fail(
        f"The Unsloth server didn't become ready within {_SERVER_START_TIMEOUT_S}s. See {log_path}."
    )


def _effective_base(base: str) -> str:
    # `unsloth run` binds to `parsed.port or 8888` and serves at the root, so normalize
    # UNSLOTH_STUDIO_URL to plain scheme://host:port. A portless http://127.0.0.1 would
    # otherwise launch on 8888 but poll port 80, and a path like /studio would poll
    # /studio/api/health (404) -- either way hitting the startup timeout. IPv6 literals
    # stay bracketed.
    parsed = urlparse(base)
    host = parsed.hostname or "127.0.0.1"
    if ":" in host:  # bare IPv6 literal (urlparse strips the brackets)
        host = f"[{host}]"
    return f"{parsed.scheme or 'http'}://{host}:{parsed.port or 8888}"


def _require_studio(
    model: Optional[str] = None,
    load: Optional[LoadOptions] = None,
    *,
    serve: bool = False,
    launch: bool = True,
    server_options: ServerOptions = ServerOptions(),
) -> tuple:
    """Return (base, server). server is a Popen only when WE auto-started it."""
    base = find_studio_server()
    if base is not None:
        # Attaching to a server someone else started: UNSLOTH_SAMPLING_* pins only reach the
        # server process when WE launch it (via _start_studio_server), so a sampling flag on the
        # attach path can't take effect. Warn instead of silently dropping it, so the operator is
        # not misled into thinking generation now uses the pinned value.
        _pinned = [
            _flag
            for _flag, _value in (
                ("--temperature", server_options.temperature),
                ("--top-p", server_options.top_p),
                ("--top-k", server_options.top_k),
                ("--min-p", server_options.min_p),
                ("--repetition-penalty", server_options.repetition_penalty),
                ("--presence-penalty", server_options.presence_penalty),
            )
            if _value is not None
        ]
        if _pinned:
            typer.echo(
                f"Warning: an Unsloth server is already running at {base}; sampling pins "
                f"({', '.join(_pinned)}) apply only when this command starts the server, so the "
                "running server keeps its current sampling. Stop it with `unsloth studio stop` "
                "and re-run to apply them.",
                err = True,
            )
        if server_options.reasoning is not None:
            typer.echo(
                f"Warning: an Unsloth server is already running at {base}; "
                f"--reasoning {server_options.reasoning} applies only when this command starts "
                "the server, so the running server keeps its current reasoning mode. Stop it "
                "with `unsloth studio stop` and re-run to apply the override.",
                err = True,
            )
        return base, None
    expected = os.environ.get("UNSLOTH_STUDIO_URL", "http://127.0.0.1:8888").rstrip("/")
    # Auto-start a local server only for an interactive launch with a model to serve, and
    # only for a plain-HTTP loopback target: never stand in for an explicit remote
    # UNSLOTH_STUDIO_URL, and never for an https:// one -- `unsloth run` serves plain
    # HTTP, so the health poll against https would spin until the startup timeout.
    if (
        serve
        and launch
        and model
        and is_loopback_url(expected)
        and urlparse(expected).scheme == "http"
    ):
        # Normalize to the port unsloth run actually binds, so the health poll and the
        # returned base hit the same server we launch (not a portless :80).
        expected = _effective_base(expected)
        load = load or LoadOptions()
        # Leave a bare GGUF repo's variant unset: the server's own quant preference already
        # picks the best available (UD-Q4_K_XL for Unsloth uploads, else Q4_K_M) and falls back
        # when that exact quant is missing, which forcing a fixed variant here would break.
        return expected, _start_studio_server(expected, model, load, server_options)
    model_hint = "" if model else " Pass --model to have it start one for you, or"
    _fail(
        f"No running Unsloth server found at {expected}.{model_hint} start one with "
        "`unsloth studio`, or point UNSLOTH_STUDIO_URL at a remote server."
    )


def _studio_auth_root() -> Path:
    from unsloth_cli.commands.studio import STUDIO_HOME
    return STUDIO_HOME / "auth"


def _key_cache_path() -> Path:
    return _studio_auth_root() / "agent_api_key.json"


def _read_cache(cache: Path) -> dict:
    try:
        data = json.loads(cache.read_text(encoding = "utf-8"))
    except Exception:
        return {}
    return data if isinstance(data, dict) else {}


def _server_buckets(servers: dict, base: str) -> dict:
    # Normalise a server's entry to {"saved": [...], "minted": [...]}, tolerating a
    # corrupt/legacy value (bare string/list -> treated as minted, behind the handshake).
    entry = servers.get(base) if isinstance(servers, dict) else None
    if isinstance(entry, list):
        return {"saved": [], "minted": [k for k in entry if isinstance(k, str)]}
    if not isinstance(entry, dict):
        return {"saved": [], "minted": []}

    def _strs(name: str) -> list:
        value = entry.get(name)
        return [k for k in value if isinstance(k, str)] if isinstance(value, list) else []

    return {"saved": _strs("saved"), "minted": _strs("minted")}


def _cached_keys(cache: Path, base: str, source: str) -> list:
    # Keys are scoped per server. `source` splits user-supplied --api-key keys
    # ("saved", trusted for that base) from auto-minted ones ("minted", replayed
    # only after the identity check). Legacy unscoped caches are ignored.
    return _server_buckets(_read_cache(cache).get("servers", {}), base)[source]


def _write_private_json(path: Path, data: dict) -> None:
    # O_CREAT with 0o600 so a file holding an API key is never world-readable,
    # even briefly (existing files keep whatever perms the user set).
    path.parent.mkdir(parents = True, exist_ok = True, mode = 0o700)
    fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
    with os.fdopen(fd, "w") as handle:
        handle.write(json.dumps(data, indent = 2) + "\n")


def _write_private_text(path: Path, text: str) -> None:
    path.parent.mkdir(parents = True, exist_ok = True, mode = 0o700)
    fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
    with os.fdopen(fd, "w", encoding = "utf-8") as handle:
        handle.write(text)


def _read_json_object(path: Path) -> Optional[dict]:
    # {} when missing, None when it can't be parsed as an object (so the caller
    # leaves a user-managed file untouched rather than clobbering it).
    if not path.exists():
        return {}
    try:
        data = json.loads(path.read_text(encoding = "utf-8"))
    except (ValueError, OSError):
        return None
    return data if isinstance(data, dict) else None


def _subdict(parent: dict, key: str) -> dict:
    child = parent.get(key)
    if not isinstance(child, dict):
        child = parent[key] = {}
    return child


def _remember_key(cache: Path, base: str, key: str, source: str) -> None:
    data = _read_cache(cache)
    servers = data.get("servers")
    if not isinstance(servers, dict):
        servers = data["servers"] = {}
    buckets = _server_buckets(servers, base)
    other = "minted" if source == "saved" else "saved"
    buckets[source] = ([key] + [k for k in buckets[source] if k != key])[:8]
    buckets[other] = [k for k in buckets[other] if k != key]  # a key has one provenance
    new_entry = {"saved": buckets["saved"], "minted": buckets["minted"]}
    if servers.get(base) == new_entry:
        return
    servers[base] = new_entry
    # Collapse legacy unscoped fields.
    data.pop("keys", None)
    data.pop("key", None)
    try:
        _write_private_json(cache, data)
    except OSError:
        pass  # worst case the next launch mints another key


def _key_accepted(base: str, key: str) -> bool:
    # Only a genuine auth rejection (401/403) means "this key is bad -- skip it and try
    # the next cached key or mint a fresh one". A 5xx or a network blip is a server-side
    # outage, not a bad key: fail with a clean message (never a traceback) instead of
    # silently discarding a working key and minting extras against a struggling server.
    try:
        _http_json("GET", f"{base}/v1/models", key)
        return True
    except urllib.error.HTTPError as exc:
        if exc.code in (401, 403):
            return False
        _fail(
            f"Unsloth server error while checking an API key ({exc.code}). "
            "The server may be starting up or unhealthy; try again shortly."
        )
    except (urllib.error.URLError, TimeoutError) as exc:
        _fail(
            "Couldn't reach the Unsloth server while checking an API key: "
            f"{getattr(exc, 'reason', None) or exc}"
        )


def _agent_api_key(
    base: str,
    explicit: Optional[str],
    *,
    auto_started: bool = False,
) -> str:
    cache = _key_cache_path()
    if explicit:
        if not auto_started or _key_accepted(base, explicit):
            _remember_key(cache, base, explicit, "saved")
            return explicit
        # The server was auto-started for this run, so an exported
        # UNSLOTH_API_KEY meant for some other server must not fail the
        # launch: the loopback mint path below is guaranteed to work.
        # (An explicit key that the fresh server accepts, e.g. one persisted
        # in this Unsloth home's auth db, is still honored above.)

    # Replay a key the user saved for *this exact* server first (scoped per base,
    # so it only goes back there -- including a remote/SSH-tunnelled Unsloth whose
    # secret the local handshake can't match). Skip ones the server rejects.
    for key in _cached_keys(cache, base, "saved"):
        if _key_accepted(base, key):
            _remember_key(cache, base, key, "saved")
            return key

    # Beyond here we auto-mint or replay an auto-minted key. find_studio_server()
    # trusts a base after only a health check, so both are limited to a loopback
    # server we can cryptographically confirm is ours.
    if not is_loopback_url(base):
        _fail(
            f"No saved API key for {base} and automatic minting only runs against "
            "a local Unsloth. Create an API key in Unsloth → Settings → API and "
            "pass it with --api-key (it is remembered per server), or set "
            "UNSLOTH_API_KEY."
        )
    if not verify_studio_identity(base):
        _fail(
            f"Couldn't verify that {base} is your Unsloth (it may be running as a "
            "different OS user, or another process took the port). Create an API "
            "key in Unsloth → Settings → API and pass it with --api-key, or set "
            "UNSLOTH_API_KEY."
        )

    # Identity verified: replay a previously auto-minted key, else mint a new one.
    for key in _cached_keys(cache, base, "minted"):
        if _key_accepted(base, key):
            _remember_key(cache, base, key, "minted")
            return key

    # Self-issue a JWT (signed with the local secret) and mint a key.
    token = _studio_token()
    if token is None:
        _fail(
            "Couldn't authenticate with the Unsloth server automatically. Create "
            "an API key in Unsloth → Settings → API and pass it with --api-key, "
            "or set UNSLOTH_API_KEY."
        )
    key = _http_json(
        "POST",
        f"{base}/api/auth/api-keys",
        token,
        {"name": "Coding agents (unsloth start)"},
        error = "Couldn't create an API key",
    )["key"]
    _remember_key(cache, base, key, "minted")
    return key


def _loaded_models(base: str, key: str) -> list:
    return _http_json("GET", f"{base}/v1/models", key, error = "Couldn't list models").get("data", [])


def _model_still_loaded(base: str, key: str, model_id: object) -> bool:
    try:
        models = _http_json("GET", f"{base}/v1/models", key, timeout = 5).get("data", [])
    except Exception:
        return False
    return any(m.get("id") == model_id and m.get("loaded") is not False for m in models)


_HF_REPO_ID_SEGMENT_RE = re.compile(r"^[A-Za-z0-9._-]+$")


def _is_hub_model_id(value: object) -> bool:
    if not isinstance(value, str):
        return False
    text = value.strip()
    if "\\" in text:
        return False
    if text.startswith(("/", "./", "../", "~")):
        return False
    if len(text) >= 2 and text[1] == ":" and text[0].isalpha():
        return False
    # A hub id is exactly "namespace/name" over a restricted charset. Anything with
    # extra path segments (e.g. a server-side relative path such as
    # models/Llama/Foo.gguf on a remote Unsloth) is not a hub id and must not be
    # casefold-matched against a differently cased path on a case-sensitive
    # filesystem. This is host independent, unlike the existence probe below which
    # cannot see a path that only exists on the server.
    parts = text.split("/")
    if len(parts) != 2:
        return False
    if any(part in ("", ".", "..") or not _HF_REPO_ID_SEGMENT_RE.match(part) for part in parts):
        return False
    try:
        if Path(os.path.expanduser(text)).exists():
            return False
    except OSError:
        return False
    return True


def _is_model_path(value: str) -> bool:
    """Mirrors core.inference.model_ids._looks_like_path: a repo id is exactly
    ``org/model``; anything else with a separator, drive, prefix or .gguf is a path.

    Deliberately not named _looks_like_path: that name is taken further down by the
    WSLENV classifier, which only matches absolute paths and would shadow this one.
    """
    if value.lower().endswith(".gguf"):
        return True
    if value.startswith(("/", "\\", "./", "../", ".\\", "..\\", "~")):
        return True
    if len(value) >= 2 and value[1] == ":":
        return True
    return value.count("/") >= 2 or "\\" in value


def _public_model_id(value: Optional[str]) -> Optional[str]:
    """The id Unsloth advertises for a model loaded by path.

    /v1/models never echoes a host path: it reports the file or directory name
    with any .gguf suffix stripped (core.inference.model_ids.public_model_id), so
    a path we asked to load has to be matched by that name too.
    """
    if not value or not _is_model_path(value):
        return None
    name = os.path.basename(value.replace("\\", "/").rstrip("/"))
    if name.lower().endswith(".gguf"):
        name = name[: -len(".gguf")]
    return name or None


def _model_id_matches(
    actual: object,
    requested: object,
    *,
    allow_casefold: bool = True,
) -> bool:
    if actual == requested:
        return True
    # Case-insensitive matching is only safe when the local existence probe in
    # _is_hub_model_id is authoritative, i.e. against a loopback Unsloth on this host.
    # Against a remote Unsloth a two-segment string is indistinguishable from a
    # server-side relative path (e.g. Models/Foo vs models/foo), so casefolding it
    # could attach to the wrong model on a case-sensitive server; defer to an exact
    # match there and let the load endpoint resolve the requested path.
    if not allow_casefold:
        return False
    if not (_is_hub_model_id(actual) and _is_hub_model_id(requested)):
        return False
    return str(actual).casefold() == str(requested).casefold()


def _resolve_model(
    base: str,
    key: str,
    requested: Optional[str],
    load: LoadOptions = LoadOptions(),
    preload_check = None,
) -> dict:
    models = _loaded_models(base, key)
    load_requested = False
    # Only casefold-match ids against a loopback Unsloth, where _is_hub_model_id's
    # local existence probe can actually reject a server-side path; see the note there.
    allow_casefold = is_loopback_url(base)
    # /v1/models reports the model id but not the active GGUF variant or runtime load
    # settings, so an id match alone can hide the wrong quant (Q8_0 serving while the
    # user asked for UD-Q4_K_XL). When the user passed any explicit load knob, defer to
    # /api/inference/load: the server's already-loaded dedup answers "already_loaded"
    # without reloading when the variant AND settings match, so a second session running
    # the same command still attaches without evicting the first.
    load_has_overrides = bool(
        load.gguf_variant
        or load.max_seq_length
        or not load.load_in_4bit
        or load.tensor_parallel
        or load.gpu_memory_mode is not None
    )
    # /v1/models also lists cached-but-unloaded catalog entries (loaded == False);
    # matching one would skip /api/inference/load and leave the agent pointed at a
    # model that is not resident, so only attach to an entry that is actually loaded.
    match = (
        None
        if requested and load_has_overrides
        else next(
            (
                m
                for m in models
                if _model_id_matches(m.get("id"), requested, allow_casefold = allow_casefold)
                and m.get("loaded") is not False
            ),
            None,
        )
    )
    if requested and match is None:
        load_requested = True
        # Only here is an evicting load certain: the gate must not reject a request the
        # resident model already satisfies (a path-loaded GGUF shown as a bare basename
        # can collide with a non-GGUF unsloth/<name>).
        active = next((m for m in models if m.get("loaded") is not False), None)
        if preload_check is not None:
            # An explicit knob forces match to None so the server's disk-free dedupe can
            # answer already_loaded; gating it would reject a second session for the model
            # already serving, whose file may have moved. Only the quant is checked below:
            # any other run knob changes the runtime intent, a real reload nothing dedupes.
            other_overrides = bool(
                load.max_seq_length
                or not load.load_in_4bit
                or load.tensor_parallel
                or load.gpu_memory_mode is not None
            )
            # /v1/models shows a path-loaded GGUF under its basename, so match that spelling
            # too, or a second session reruns the gate.
            wanted_ids = {requested, _public_model_id(requested)} - {None}
            resident_serves_request = not other_overrides and any(
                m.get("loaded") is not False
                and any(
                    _model_id_matches(m.get("id"), want, allow_casefold = allow_casefold)
                    for want in wanted_ids
                )
                for m in models
            )
            # /v1/models shows only the basename, so confirm a path request against the
            # identifier the server loaded -- else /new/foo.gguf reads as resident because
            # /old/foo.gguf is.
            if resident_serves_request and _is_model_path(requested):
                try:
                    status = _http_json("GET", f"{base}/api/inference/status", key)
                except Exception:
                    status = {}
                loaded_paths = {
                    str(status.get(field))
                    for field in ("model_identifier", "gguf_path", "model_path")
                    if status.get(field)
                }
                wanted_path = os.path.abspath(os.path.expanduser(requested))
                resident_serves_request = any(
                    os.path.abspath(os.path.expanduser(path)) == wanted_path
                    for path in loaded_paths
                )
            if resident_serves_request and load.gguf_variant:
                try:
                    status = _http_json("GET", f"{base}/api/inference/status", key)
                except Exception:
                    status = {}
                resident_variant = status.get("gguf_variant") if status.get("is_gguf") else None
                # Casefold, not _normalized_variant (it strips separators): a mistyped Q4KM
                # would skip the gate here yet still really reload on the server.
                resident_serves_request = (
                    bool(resident_variant)
                    and str(resident_variant).strip().lower()
                    == str(load.gguf_variant).strip().lower()
                )
            if not resident_serves_request:
                preload_check(base, key, requested, load.gguf_variant)
        active_id = active.get("id") if active else None
        announced_switch = False
        if active_id and not _model_id_matches(
            active_id,
            requested,
            allow_casefold = allow_casefold,
        ):
            typer.echo(f"Switching the Unsloth server from {active_id} to {requested}.")
            typer.echo("This unloads the current model for every attached session.")
            announced_switch = True
        elif active_id and load.gguf_variant:
            # Same repo id but an explicit quant still replaces the resident
            # weights; /v1/models has no variant, so ask the status endpoint.
            try:
                status = _http_json("GET", f"{base}/api/inference/status", key)
            except Exception:
                status = {}
            resident = status.get("gguf_variant") if status.get("is_gguf") else None
            if resident and _normalized_variant(resident) != _normalized_variant(load.gguf_variant):
                typer.echo(
                    f"Switching the Unsloth server from {active_id}:{resident} "
                    f"to {requested}:{load.gguf_variant}."
                )
                typer.echo("This unloads the current model for every attached session.")
                announced_switch = True
        # Mirror `unsloth run`'s load knobs; keep the default payload as just
        # model_path so a bare `--model` load is unchanged.
        payload = {"model_path": requested}
        if load.gguf_variant:
            payload["gguf_variant"] = load.gguf_variant
        if load.max_seq_length:
            payload["max_seq_length"] = load.max_seq_length
        if not load.load_in_4bit:
            payload["load_in_4bit"] = False
        if load.tensor_parallel:
            payload["tensor_parallel"] = True
        if load.gpu_memory_mode is not None:
            payload["gpu_memory_mode"] = load.gpu_memory_mode
            if load.gpu_memory_mode == "manual":
                payload["gpu_layers"] = -1
        try:
            loaded = _load_model_with_progress(base, key, requested, load, payload)
        except Exception:
            # The warning above promised an unload; if the server refused the
            # load before evicting anything, say so. Not BaseException: Ctrl+C
            # must stay immediate, without a probe or a survivor claim.
            if announced_switch and _model_still_loaded(base, key, active_id):
                typer.echo(f"Nothing was unloaded; {active_id} is still serving.", err = True)
            raise
        if loaded.get("status") == "already_loaded":
            typer.echo(f"Reusing loaded model: {_display_model_spec(requested, load.gguf_variant)}")
        # Unsloth registers the model under a canonical id (resolved identifier,
        # casing) that /v1/models echoes but which may differ from the path we
        # passed; match on the id the load reports so we don't silently fall
        # through to models[0] and connect to a different loaded model.
        wanted = {requested, _public_model_id(requested)} - {None}
        if isinstance(loaded, dict):
            wanted |= {loaded.get("model"), loaded.get("display_name")} - {None}
        models = _loaded_models(base, key)
        match = next(
            (
                m
                for m in models
                if m.get("loaded") is not False
                and any(
                    _model_id_matches(m.get("id"), w, allow_casefold = allow_casefold) for w in wanted
                )
            ),
            None,
        )
    if match is not None:
        if requested and not load_requested:
            typer.echo(f"Reusing loaded model: {_display_model_spec(requested, load.gguf_variant)}")
        return match
    if requested:
        # We asked Unsloth to load it and it didn't surface in /v1/models; don't
        # silently hand back an unrelated loaded model.
        _fail(
            f"Unsloth didn't report '{requested}' as loaded. Double-check the model "
            "id, or load it from the model dropdown in the UI."
        )
    if not models:
        _fail(
            "No model is loaded in Unsloth. Load one from the model dropdown in "
            "the UI, or pass --model <hf-id-or-path> to load it from here."
        )
    resident = next((m for m in models if m.get("loaded") is not False), None)
    if resident is None:
        _fail(
            "No model is currently resident in Unsloth. Pass --model <hf-id-or-path> "
            "to reload one, or load it from the model dropdown in the UI."
        )
    return resident


_HF_OFFLINE_TRUE_VALUES = frozenset({"1", "true", "yes", "on"})


def _hf_offline() -> bool:
    return any(
        os.environ.get(var, "").strip().lower() in _HF_OFFLINE_TRUE_VALUES
        for var in ("HF_HUB_OFFLINE", "TRANSFORMERS_OFFLINE")
    )


def _hub_gguf_files(repo: str) -> Optional[list]:
    if _hf_offline():
        return None
    endpoint = os.environ.get("HF_ENDPOINT", "https://huggingface.co").rstrip("/")
    try:
        request = urllib.request.Request(
            f"{endpoint}/api/models/{repo}",
            headers = {"User-Agent": _USER_AGENT},
        )
        with urllib.request.urlopen(request, timeout = 10) as response:
            info = json.loads(response.read().decode() or "{}")
    except Exception:
        return None
    siblings = info.get("siblings")
    if not isinstance(siblings, list) or not siblings:
        return None
    names = [s.get("rfilename") for s in siblings if isinstance(s, dict)]
    ggufs = [n for n in names if isinstance(n, str) and n.lower().endswith(".gguf")]
    return [n for n in ggufs if not _is_auxiliary_gguf(n)]


# Mirrors hub.utils.gguf._DRAFTER_KINDS / _DRAFTER_DIR_KINDS: dspark and dflash are the same
# DeepSeek V4 Flash drafter, but dflash/ is also a real family name, so only mtp/ and dspark/
# count as a companion folder.
_DRAFTER_KINDS = ("mtp", "dspark", "dflash")
_DRAFTER_DIR_KINDS = ("mtp", "dspark")


def _is_auxiliary_gguf(filename: str) -> bool:
    # Mirrors detect_gguf_model_remote (hub.utils.gguf.is_mtp_drafter_path): projectors,
    # separate-file drafters and big-endian builds are not loadable weights. Drafters match
    # by basename prefix or exact parent dir, never substring -- the kind names double as
    # family names, so Qwen3.6-...-DFlash-Q4_K_M.gguf IS the model. Only the root-level
    # trailing -be form is filtered; the fuller quant-aware check would over-reject here.
    p = filename.lower().replace("\\", "/")
    parts = [segment for segment in p.split("/") if segment]
    if not parts:
        return False
    name, parents = parts[-1], parts[:-1]
    if "mmproj" in p:
        return True
    if any(name.startswith(f"{kind}-") for kind in _DRAFTER_KINDS):
        return True
    if any(kind in parents for kind in _DRAFTER_DIR_KINDS):
        return True
    stem = name.rsplit(".", 1)[0]
    return not parents and stem.endswith(("-be", "_be"))


def _direct_gguf_is_companion(path: str) -> bool:
    """Whether the server refuses this .gguf path as a model in its own right.

    A strict subset of detect_gguf_model / gguf_variants._direct_gguf_loads: projector and
    drafter prefixes read off the basename, companion-only folders off the immediate
    parent -- the same context the server reads, so nothing loadable is refused here.
    Big-endian is left out on purpose: that check needs quant context the CLI can't mirror.
    """
    parts = [segment for segment in path.replace("\\", "/").split("/") if segment]
    if not parts:
        return False
    name = parts[-1].lower()
    if not name.endswith(".gguf"):
        return False
    # Root-independent refusals only: name prefixes read the basename alone, so they mean
    # the same under any model root. A drafter FOLDER does not.
    if "mmproj" in name:
        return True
    return any(name.startswith(f"{kind}-") for kind in _DRAFTER_KINDS)


def _path_syntax_is_native(path: str) -> bool:
    """Whether *path* is spelled the way this OS spells paths.

    A Windows path read from WSL, or a POSIX one read from Windows, parses into
    something this process cannot judge -- ``C:\\models\\m.gguf`` has parent
    ``.`` here -- so its absence locally says nothing about the server's disk.
    """
    windows_drive = len(path) >= 2 and path[1] == ":" and path[0].isalpha()
    if os.name == "nt":
        return True
    return not windows_drive and "\\" not in path


def _direct_gguf_companion_is_uncertain(path: str) -> bool:
    """Whether only the server can say if this path is a companion.

    detect_gguf_model reads drafter folders relative to the registered model root, so
    ``/models/MTP/foo-Q8_0.gguf`` is refused or loaded depending on where that root sits
    -- a question only the server can answer, since this process doesn't know its roots.
    """
    parts = [segment for segment in path.replace("\\", "/").split("/") if segment]
    return any(segment.lower() in _DRAFTER_DIR_KINDS for segment in parts[:-1])


# Mirrors model_config._extract_quant_label's pattern; change in lockstep.
_QUANT_LABEL_RE = re.compile(
    r"(UD-)?"
    r"(MXFP[0-9]+(?:_[A-Z0-9]+)*"
    r"|IQ[0-9]+_[A-Z]+(?:_[A-Z0-9]+)?"
    r"|TQ[0-9]+_[0-9]+"
    r"|Q[0-9]+_K_[A-Z]+"
    r"|Q[0-9]+_[0-9]+"
    r"|Q[0-9]+_K"
    r"|BF16|F16|F32)"
    r"(-[0-9]+(?:\.[0-9]+)?bpw)?",
    re.IGNORECASE,
)


def _direct_gguf_variant_labels(path: str) -> tuple:
    """The labels the server's direct-file resolver accepts for *path*.

    Mirrors _direct_gguf_for_variant: the quant label read from the basename
    first, the immediate parent only when the basename carries none, plus the
    shard-stripped stem itself. The basename wins the disagreement -- a
    Q8_0/foo-Q4_K_M.gguf answers Q4_K_M, so its parent must not vouch for
    Q8_0 here while the load resolves nothing and evicts.
    """
    norm = path.replace("\\", "/").rstrip("/")
    name = norm.rsplit("/", 1)[-1]
    stem = re.sub(r"-\d{3,}-of-\d{3,}$", "", name.rsplit(".", 1)[0])
    match = _QUANT_LABEL_RE.search(stem)
    if match is None and "/" in norm:
        match = _QUANT_LABEL_RE.search(norm.rsplit("/", 2)[-2])
    labels = [stem]
    if match is not None:
        label = f"{match.group(1) or ''}{match.group(2)}{match.group(3) or ''}"
        labels.append(label)
        # The resolver also accepts the hub-style bpw-stripped spelling.
        stripped = re.sub(r"-[0-9]+(?:\.[0-9]+)?bpw$", "", label, flags = re.IGNORECASE)
        if stripped != label:
            labels.append(stripped)
    else:
        # The extractor's own fallback: the last hyphen-separated segment.
        labels.append(stem.split("-")[-1])
    return tuple(labels)


# Mirrors hub.utils.gguf._BIG_ENDIAN_GGUF_FILENAME_RE; change in lockstep.
_BIG_ENDIAN_FILENAME_RE = re.compile(r"(^|[-_])be(?:[._-]|$)", re.IGNORECASE)


def _direct_gguf_is_big_endian(path: str) -> bool:
    """Mirrors hub.utils.gguf.is_big_endian_gguf_path over the same one-parent
    context detect_gguf_model reads; change in lockstep. A quant-named parent
    exempts a bare -be basename (that file loads); a be marker at or after a
    basename quant does not.
    """
    norm = path.replace("\\", "/").rstrip("/")
    parts = [segment for segment in norm.split("/") if segment]
    name = parts[-1]
    stem = name.rsplit(".", 1)[0].lower()
    quant_stem = re.sub(r"-\d{3,}-of-\d{3,}$", "", stem)
    match = _QUANT_LABEL_RE.search(quant_stem)
    parent = parts[-2].lower() if len(parts) > 1 else ""
    if match is None and parent:
        match = _QUANT_LABEL_RE.search(parent)
    quant_key = (
        f"{match.group(1) or ''}{match.group(2)}{match.group(3) or ''}".lower()
        if match is not None
        else quant_stem.split("-")[-1]
    )
    quant_index = stem.find(quant_key) if quant_key else -1
    quant_in_parent_only = (
        bool(parent)
        and quant_index < 0
        and (
            (quant_key and quant_key in parent)
            or (not quant_key and _QUANT_LABEL_RE.search(parent))
        )
    )
    for be in _BIG_ENDIAN_FILENAME_RE.finditer(stem):
        if quant_index >= 0 and quant_index < be.start():
            return True
        tail = stem[be.end() :].lstrip("._-")
        if not tail or _QUANT_LABEL_RE.search(tail) is None:
            return not quant_in_parent_only
    return False


# Mirrors gguf_variants._DIRECT_SPLIT_RE / the load path's _GGUF_SPLIT_FILE_RE; change in
# lockstep. Five digits exactly: a shorter -001-of-002 name loads as an ordinary file.
_DIRECT_SPLIT_FILE_RE = re.compile(
    r"^(?P<stem>.+)-(?P<index>\d{5})-of-(?P<total>\d{5})$", re.IGNORECASE
)


def _direct_gguf_file_is_ready(path: str) -> bool:
    """Whether a CLI-visible direct .gguf file can actually serve a load.

    Mirrors the backend's completeness rules: zero bytes is an interrupted copy, a split
    needs every sibling index present and non-empty. Unknowable reports ready, so a path
    this process can't judge never blocks the load.
    """

    def _split_set_complete(candidate: Path) -> Optional[bool]:
        match = _DIRECT_SPLIT_FILE_RE.match(candidate.name.rsplit(".", 1)[0])
        if match is None:
            return None
        total = int(match.group("total"))
        if total < 2:
            return None
        sibling = re.compile(
            re.escape(match.group("stem"))
            + r"-(\d{"
            + str(len(match.group("index")))
            + r"})-of-"
            + re.escape(match.group("total"))
            + r"\.gguf$",
            re.IGNORECASE,
        )
        found = {
            int(m.group(1))
            for q in candidate.parent.iterdir()
            if (m := sibling.match(q.name)) and q.is_file() and q.stat().st_size > 0
        }
        return found >= set(range(1, total + 1))

    try:
        p = Path(os.path.expanduser(path))
        # A broken symlink is visible here, and the .gguf suffix alone still makes it a
        # load, which fails after teardown.
        if p.is_symlink() and not p.exists():
            return False
        if not p.is_file():
            return True
        if p.stat().st_size == 0:
            return False
        whole = _split_set_complete(p)
        if whole is not False:
            return True
        # Mirror _local_gguf_load_path: an incomplete nominal set still loads when the shard
        # is a symlink whose target sits with the full set.
        if p.is_symlink():
            target = p.resolve()
            return _split_set_complete(target) is not False
        return False
    except OSError:
        return True


def _answer_offers_variant(
    variants: list,
    variant: str,
    strict: bool = False,
) -> bool:
    """Whether a live variants answer can resolve *variant* to a file.

    Mirrors llama.cpp's resolution, case-insensitively: quant label first, then the
    whole-token filename fallback (as loose as it gets -- a separator-differing label
    resolves to nothing there either). A row missing both fields can't be disproven, so
    it vouches. ``strict`` drops the filename-token tier: the LOCAL resolver takes only
    exact labels, so a local answer must not vouch for a shorter token (Q4 inside
    model-Q4_K_M.gguf) the load would never resolve.
    """
    wanted = str(variant).strip().lower()
    if not wanted:
        return True
    token = re.compile(r"(?<![a-z0-9])" + re.escape(wanted) + r"(?![a-z0-9])")
    for row in variants:
        if not isinstance(row, dict):
            return True
        # A torn local row proves a load llama cannot serve, so its labels do not vouch in
        # strict mode; hub partials stay resumable and count.
        if strict and row.get("partial") is True:
            continue
        quant = row.get("quant")
        filename = row.get("filename")
        if not isinstance(quant, str) and not isinstance(filename, str):
            return True
        if isinstance(quant, str):
            label = quant.strip().lower()
            # The resolver also accepts the hub-style bpw-stripped spelling.
            if wanted in (label, re.sub(r"-[0-9]+(?:\.[0-9]+)?bpw$", "", label)):
                return True
        if isinstance(filename, str):
            # The local resolver also takes the exact shard-stripped stem: full relative
            # spelling only, never a nested file's bare basename.
            stem = re.sub(r"-\d{3,}-of-\d{3,}$", "", filename.rsplit(".", 1)[0]).lower()
            if wanted == stem:
                return True
            # Also any whole quant token of the BASENAME, which is how the listing's default
            # can name the file by its other label (F16-checkpoint-Q4_K_M -> Q4_K_M).
            if any(
                wanted
                in (
                    f"{m.group(1) or ''}{m.group(2)}".lower(),
                    # Keeps the bpw modifier the hub extractor drops, so both spellings match.
                    f"{m.group(1) or ''}{m.group(2)}{m.group(3) or ''}".lower(),
                )
                for m in _QUANT_LABEL_RE.finditer(stem.rsplit("/", 1)[-1])
            ):
                return True
        if strict:
            if not isinstance(quant, str):
                return True
            continue
        if isinstance(filename, str) and token.search(filename.lower()):
            return True
    return False


def _fail_codex_variant_missing(model_id: str, variant: str, variants: list) -> NoReturn:
    offered = [
        row.get("quant")
        for row in variants
        if isinstance(row, dict) and isinstance(row.get("quant"), str) and row.get("quant")
    ]
    message = f"{model_id} has no GGUF variant {variant}."
    if offered:
        message += " Available: " + ", ".join(dict.fromkeys(offered))
    _fail(message)


def _fail_codex_needs_gguf(model_id: str) -> NoReturn:
    message = f"Codex needs a GGUF model served by llama-server, but {model_id} is not one."
    guess = f"{model_id}-GGUF"
    if "gguf" not in model_id.lower() and _is_hub_model_id(guess) and _hub_gguf_files(guess):
        message += f" Try: unsloth start codex --model {guess}"
    _fail(message)


def _preflight_codex_gguf(
    model: Optional[str],
    *,
    serve: bool = True,
    launch: bool = True,
) -> None:
    # Hub-listing preflight for the auto-start path only: with a server running, identifiers
    # resolve against its cwd/cache/token, so _attach_gguf_check_for_codex asks the server
    # instead. Only a complete listing with no .gguf files rejects; unknown defers to the
    # post-connect check. Mirrors _require_studio's auto-start condition, so with no start
    # possible its "no running server" error comes first, without a hub probe.
    if not (serve and launch and model):
        return
    expected = os.environ.get("UNSLOTH_STUDIO_URL", "http://127.0.0.1:8888").rstrip("/")
    if not is_loopback_url(expected) or urlparse(expected).scheme != "http":
        return
    if find_studio_server() is not None:
        return
    repo, _ = _split_repo_variant(model)
    # A bare foo.gguf naming no local file is a shorthand, not a path: the load
    # canonicalizes it like any other owner-less name.
    if "/" not in repo and (not _is_model_path(repo) or repo.lower().endswith(".gguf")):
        # The server canonicalizes owner-less shorthands to unsloth/<name>.
        try:
            if Path(os.path.expanduser(repo)).exists():
                return
        except OSError:
            return
        repo = f"unsloth/{repo}"
    if not _is_hub_model_id(repo):
        return
    files = _hub_gguf_files(repo)
    if files is not None and not files:
        _fail_codex_needs_gguf(repo)


def _attach_gguf_check_for_codex(
    base: str,
    key: str,
    model: Optional[str],
    variant: Optional[str] = None,
) -> None:
    # Attach path: the server resolves the identifier with its own cwd, cache and token, so
    # ask it before the load evicts the resident model. A live empty list is definitive;
    # any error (an older server included) defers.
    if not model:
        return
    repo, inline_variant = _split_repo_variant(model)
    # `--model repo:QUANT` is split before the gate runs, so the caller passes the quant on.
    variant = variant or inline_variant
    # A .gguf filesystem path is GGUF by definition; only the hub-id shape (owner/name.gguf)
    # gets probed. A bare foo.gguf naming no local file takes the shorthand route, since the
    # load canonicalizes it to unsloth/foo.gguf.
    bare_missing_gguf = False
    if repo.lower().endswith(".gguf") and not _is_model_path(repo.rsplit(".", 1)[0]):
        try:
            bare_missing_gguf = not Path(os.path.expanduser(repo)).exists()
        except OSError:
            bare_missing_gguf = False
    if (
        repo.lower().endswith(".gguf")
        and not bare_missing_gguf
        and not (
            repo.count("/") == 1
            and not repo.startswith(("/", ".", "~"))
            and ":" not in repo
            and "\\" not in repo
        )
    ):
        # detect_gguf_model refuses a companion or big-endian build, so the load falls through
        # to transformers and unloads llama-server before failing. Settled by name: the probe's
        # empty answer defers whenever the path exists here, which on a loopback attach is
        # exactly when it does. The quant never redirects a direct file (from_identifier
        # consults it only for a DIRECTORY), though an explicit one still reaches the probe.
        refused = _direct_gguf_is_companion(repo) or _direct_gguf_is_big_endian(repo)
        if refused:
            _fail_codex_needs_gguf(repo)
        # A drafter folder further up is the server's call.
        uncertain = _direct_gguf_companion_is_uncertain(repo)
        # The variant does not exempt this: a direct file is loaded as itself, so a complete
        # sibling matching the quant is never substituted for it.
        if not refused and not uncertain:
            # Only when this process really shares the server's filesystem: the .gguf suffix
            # alone gets an incomplete file loaded, and llama-server finds the missing bytes
            # only after teardown. Loopback does not prove that -- 127.0.0.1 may be an SSH or
            # container forward where a server-valid path is simply absent here -- so confirm
            # this machine's Unsloth and otherwise defer to the probe.
            if is_loopback_url(base) and verify_studio_identity(base):
                # A .gguf-NAMED DIRECTORY is scanned, not loaded, so the suffix proves nothing.
                try:
                    is_gguf_dir = Path(os.path.expanduser(repo)).is_dir()
                except OSError:
                    is_gguf_dir = False
                if not is_gguf_dir:
                    # On loopback this reads the server's filesystem, so a name absent from a
                    # listable directory is absent for the load too, and the .gguf suffix
                    # still makes it a load that fails after teardown. An unreadable parent
                    # stays unknowable and defers.
                    try:
                        probe = Path(os.path.expanduser(repo))
                        missing = (
                            # Only a path this OS spells: a Windows path seen from WSL parses
                            # to nonsense (C:\... has parent '.') and the server may hold the
                            # real file, so it defers to the probe.
                            _path_syntax_is_native(repo)
                            and not probe.is_symlink()
                            and probe.parent.is_dir()
                            and not probe.exists()
                        )
                    except OSError:
                        missing = False
                    if missing:
                        _fail(f"{repo} does not exist. Check the path before pointing Codex at it.")
                    if not _direct_gguf_file_is_ready(repo):
                        _fail(
                            f"{repo} is incomplete (zero bytes or a split missing shards); "
                            "re-download or re-copy it before pointing Codex at it."
                        )
                    # Only a spelling this OS can judge is settled here: a Windows path read
                    # from WSL skipped the absence check above and reads as ready, so returning
                    # would vouch for a file nobody looked at. An explicit variant also goes to
                    # the probe, which judges the marked parent.
                    if not variant and _path_syntax_is_native(repo):
                        return
            # A remote server's filesystem is not ours to read, and the load takes the .gguf
            # suffix as authoritative, so ask the server instead. Its errors defer as always.
    # Mirrors the load path's shorthand precedence: the raw name first (it may be a directory
    # under the server's cwd), then the unsloth/<name> form the load falls back to only when
    # the raw name resolves to nothing. A live answer settles that exact id, so the canonical
    # form must not vouch for a raw name already answered, else a non-GGUF directory in the
    # server's cwd passes the gate and evicts. Only an error falls through.
    candidates = [repo]
    if "/" not in repo and (not _is_model_path(repo) or bare_missing_gguf):
        candidates.append(f"unsloth/{repo}")
    for candidate in candidates:
        try:
            info = _http_json(
                "GET", f"{base}/api/models/gguf-variants?{urlencode({'repo_id': candidate})}", key
            )
        except Exception:
            continue
        variants = info.get("variants") if isinstance(info, dict) else None
        if isinstance(variants, list):
            # A cleanable row is an empty leftover <quant>/ folder: it offers a delete, never
            # weights, so it cannot stand in for the load finding something.
            variants = [
                row for row in variants if not (isinstance(row, dict) and row.get("cleanable"))
            ]
        # The load uses a bare name only when it resolves locally, else it canonicalizes to
        # unsloth/<name>, so a raw answer the server calls non-local settles nothing. Without
        # the flag (older server) the raw answer is still the best evidence there is.
        if (
            "/" not in candidate
            and candidate != candidates[-1]
            and isinstance(info, dict)
            and "resolved_locally" in info
            and not info["resolved_locally"]
        ):
            continue
        # The server can answer the gate's real question with the load resolver itself, so it
        # settles the round -- even an EMPTY listing, since a root-blind lister can miss a file
        # detect_gguf_model still resolves. No filename grammar beats the code that loads.
        if isinstance(variants, list) and isinstance(info, dict):
            offered = info.get("loadable_variants")
            # No allow-list means a direct file, which loads as itself whatever the quant, so
            # the variantless verdict decides. Only a server omitting the field falls through.
            if variant and offered is None and isinstance(info.get("loadable"), bool):
                if not info["loadable"]:
                    _fail_codex_needs_gguf(candidate)
                return
            if variant and isinstance(offered, list):
                wanted_variant = str(variant).strip().lower()
                if not any(
                    isinstance(q, str) and q.strip().lower() == wanted_variant for q in offered
                ):
                    _fail_codex_variant_missing(candidate, variant, variants)
                return
            if not variant and isinstance(info.get("loadable"), bool):
                if not info["loadable"]:
                    _fail_codex_needs_gguf(candidate)
                return
        if isinstance(variants, list) and variants:
            # llama.cpp kills the resident model before resolving the quant, so a quant this
            # answer cannot serve is settled here. Local answers take exact labels only; the
            # looser filename-token tier is for hub answers.
            #
            # "Local" means the server says so (resolved_locally) or, predating that field,
            # path syntax / bare names, which resolve locally anyway. owner/name.gguf is
            # exempted like the direct-file branch (_is_model_path sees only the suffix),
            # since a remote answer wrongly judged local would face local-only rules.
            hub_shaped = (
                candidate.count("/") == 1
                and not candidate.startswith(("/", ".", "~"))
                and ":" not in candidate
                and "\\" not in candidate
            )
            local_answer = bool(info.get("resolved_locally")) or (
                not hub_shaped and (_is_model_path(candidate) or "/" not in candidate)
            )
            # A local answer of only torn rows has no resumable side: llama-server would get
            # the incomplete split only after teardown.
            if local_answer and all(
                isinstance(row, dict) and row.get("partial") is True for row in variants
            ):
                _fail(
                    f"{candidate} has only incomplete GGUF weights on the server; "
                    "finish or re-copy the download before pointing Codex at it."
                )
            # A variantless local load picks from the directory's top level, so rows living
            # only in quant subdirectories need the variant that resolves them -- else this
            # answer evicts for a load that finds nothing.
            if (
                local_answer
                and not variant
                and variants
                and all(
                    isinstance(row, dict)
                    and isinstance(row.get("filename"), str)
                    and "/" in row["filename"]
                    for row in variants
                )
            ):
                offered = ", ".join(
                    dict.fromkeys(
                        row["quant"]
                        for row in variants
                        if isinstance(row.get("quant"), str) and row["quant"]
                    )
                )
                _fail(
                    f"{candidate} keeps its GGUF weights in quant subdirectories, which a "
                    "variantless load cannot pick. Pass --gguf-variant"
                    + (f" (available: {offered})." if offered else ".")
                )
            if variant and not _answer_offers_variant(variants, variant, strict = local_answer):
                _fail_codex_variant_missing(candidate, variant, variants)
            return
        if isinstance(variants, list):
            # Explicit local syntax resolves locally on every server version, so its live empty
            # answer settles the load; deferring would let the same GGUF-less directory go down
            # the transformers path and evict. Only marker-less names keep deferring: older
            # servers read them as hub ids, and a local hit may be a server-side model from the
            # server's cwd -- unless it reports resolved_locally, having already resolved them.
            # A bare foo.gguf naming no local file is only the canonicalized spelling, so it
            # settles nothing until the canonical form has answered too.
            if bare_missing_gguf and candidate != candidates[-1]:
                continue
            if not _is_model_path(repo) and not info.get("resolved_locally"):
                try:
                    if Path(os.path.expanduser(repo)).exists():
                        return
                except OSError:
                    return
            _fail_codex_needs_gguf(candidate)


def _require_gguf_for_codex(base: str, key: str, model_id: str) -> None:
    # Codex always streams, and Unsloth only streams /v1/responses from llama-server.
    try:
        status = _http_json("GET", f"{base}/api/inference/status", key)
    except urllib.error.HTTPError as exc:
        if exc.code == 404:
            return  # older server without the endpoint; don't block the launch
        raise
    if status.get("is_gguf"):
        return
    _fail_codex_needs_gguf(model_id)


_DYNAMIC_SECTIONS_FLAG = "--exclude-dynamic-system-prompt-sections"


def _claude_settings_overlay(model_id: str) -> str:
    # Session-only `claude --settings` overlay (command-line tier, no ~/.claude write):
    # suppress the attribution header, keep every subagent on the served model (a user
    # CLAUDE_CODE_SUBAGENT_MODEL pin would otherwise route delegated work off the local
    # endpoint), and pin availableModels to the served model so a user allowlist can't
    # reject it. The pin must be non-empty; [] is ignored.
    return json.dumps(
        {
            "env": {
                "CLAUDE_CODE_ATTRIBUTION_HEADER": "0",
                "CLAUDE_CODE_SUBAGENT_MODEL": "inherit",
            },
            "availableModels": [model_id],
        }
    )


def _claude_version() -> Optional[tuple]:
    # None = no local `claude` (a --no-launch printout for another machine; assume a
    # current build). An unparseable version is treated as too old for the new flags.
    executable = _which_with_install_dirs("claude")
    if executable is None:
        return None
    try:
        result = subprocess.run(
            [executable, "--version"], capture_output = True, text = True, timeout = 10, env = _probe_env()
        )
        # Pull the X.Y.Z out of the output rather than assuming it is the first token.
        # claude prints it first today ("2.1.98 (Claude Code)"), but a format change
        # (e.g. "claude version 2.1.98") shouldn't silently drop the optimization flags;
        # no match falls through to "too old", same as an unparseable version.
        match = re.search(r"(\d+)\.(\d+)\.(\d+)", result.stdout)
        return tuple(int(part) for part in match.groups()) if match else (0,)
    except Exception:
        return (0,)


def _claude_flags(model_id: str) -> list:
    # KV-cache-preserving flags: move per-session context out of the system prompt and pass
    # the session overlay. claude < 2.1.98 rejects unknown flags; no local binary means a
    # printout for another machine, so assume a current build.
    version = _claude_version()
    if version is not None and version < (2, 1, 98):
        return []
    return [_DYNAMIC_SECTIONS_FLAG, "--settings", _claude_settings_overlay(model_id)]


def _claude_local_env(base: str, key: str, entry: dict) -> dict:
    """Build the local endpoint, cache, display, and compaction environment."""
    model_id = entry["id"]
    env = {
        "ANTHROPIC_BASE_URL": base,
        "ANTHROPIC_AUTH_TOKEN": key,
        "ANTHROPIC_MODEL": model_id,
        "CLAUDE_CODE_ATTRIBUTION_HEADER": "0",
        "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
        "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS": "1",
        "CLAUDE_CODE_NO_FLICKER": "1",
    }
    window = entry.get("context_length") or entry.get("max_context_length")
    if window:
        env["CLAUDE_CODE_AUTO_COMPACT_WINDOW"] = str(int(window))
        env["CLAUDE_AUTOCOMPACT_PCT_OVERRIDE"] = "90"
    return env


def _merge_codex_config(existing: str, base: str) -> str:
    chunks = re.split(r"(?m)^(?=\[)", existing)  # preamble, then one chunk per table
    if not re.search(r"(?m)^\s*oss_provider\s*=", chunks[0]):
        if chunks[0] and not chunks[0].endswith("\n"):
            chunks[0] += "\n"
        chunks[0] += f'oss_provider = "{_CODEX_PROFILE}"\n'
    # Drop the provider table and any stale [model_providers.unsloth_api.*] subtables.
    stale = (_PROVIDER_HEADER, _PROVIDER_HEADER[:-1] + ".")
    text = "".join(c for c in chunks if not c.startswith(stale))
    if not text.endswith("\n"):
        text += "\n"
    if not text.endswith("\n\n"):
        text += "\n"
    return text + (
        f"{_PROVIDER_HEADER}\n"
        'name = "Unsloth Studio"\n'
        f"base_url = {json.dumps(base + '/v1')}\n"
        f'env_key = "{_CODEX_ENV_KEY}"\n'
        'wire_api = "responses"\n'
        "requires_openai_auth = false\n"
    )


# Keep custom-model behavior aligned with Codex's own unknown-model fallback. This
# Apache-2.0 prompt is copied from openai/codex rust-v0.144.0 models-manager/prompt.md.
_CODEX_FALLBACK_PROMPT = Path(__file__).parent.parent / "codex_fallback_prompt.md"
_CODEX_MODEL_CATALOG_MIN_VERSION = (0, 110, 0)


def _codex_supports_model_catalog() -> bool:
    executable = _which_with_install_dirs("codex")
    if executable is None:
        # A --no-launch recipe may be copied to another machine; assume a current Codex.
        return True
    try:
        output = subprocess.check_output(
            [executable, "--version"],
            text = True,
            timeout = 10,
            stderr = subprocess.DEVNULL,
            env = _probe_env(),
        )
    except Exception:
        return False
    match = re.search(r"(\d+)\.(\d+)\.(\d+)", output)
    return bool(match) and tuple(int(part) for part in match.groups()) >= (
        _CODEX_MODEL_CATALOG_MIN_VERSION
    )


def _codex_model_catalog(model: dict) -> dict:
    """Return conservative metadata for an Unsloth model unknown to Codex's built-in catalog."""
    model_id = model["id"]
    window = model.get("context_length") or model.get("max_context_length")
    entry = {
        "slug": model_id,
        "display_name": model_id,
        "description": "Model served by Unsloth Studio",
        "supported_reasoning_levels": [],
        "shell_type": "default",
        "visibility": "none",
        "supported_in_api": True,
        "priority": 99,
        "availability_nux": None,
        "upgrade": None,
        "base_instructions": _CODEX_FALLBACK_PROMPT.read_text(encoding = "utf-8"),
        "supports_reasoning_summaries": False,
        "supports_reasoning_summary_parameter": False,
        "support_verbosity": False,
        "default_verbosity": None,
        "apply_patch_tool_type": None,
        "truncation_policy": {"mode": "bytes", "limit": 10_000},
        "supports_parallel_tool_calls": False,
        "experimental_supported_tools": [],
    }
    if window:
        entry["context_window"] = int(window)
        entry["max_context_window"] = int(window)
    return {"models": [entry]}


def write_codex_config(base: str, model: dict, home: Path) -> None:
    home.mkdir(parents = True, exist_ok = True)

    config = home / "config.toml"
    existing = config.read_text(encoding = "utf-8") if config.exists() else ""
    merged = _merge_codex_config(existing, base)
    if merged != existing:
        config.write_text(merged, encoding = "utf-8")
        typer.echo(f"Updated {config}")

    # oss_provider here too: codex --oss picks the provider from it, and the
    # profile layer must beat a user-set value (e.g. "ollama") in config.toml.
    profile_text = (
        f'oss_provider = "{_CODEX_PROFILE}"\n'
        f'model_provider = "{_CODEX_PROFILE}"\n'
        f"model = {json.dumps(model['id'])}\n"
    )
    if _codex_supports_model_catalog() and _CODEX_FALLBACK_PROMPT.is_file():
        catalog = home / "model-catalog.json"
        catalog_text = json.dumps(_codex_model_catalog(model), indent = 2) + "\n"
        if not catalog.exists() or catalog.read_text(encoding = "utf-8") != catalog_text:
            catalog.write_text(catalog_text, encoding = "utf-8")
            typer.echo(f"Updated {catalog}")
        # Resolve relative to the profile file. This also survives WSL launching a Windows
        # Codex binary, where a Linux absolute path inside TOML would not be usable.
        profile_text += f"model_catalog_json = {json.dumps(catalog.name)}\n"

    window = model.get("context_length") or model.get("max_context_length")
    if window:
        profile_text += f"model_context_window = {int(window)}\n"
    profile = home / f"{_CODEX_PROFILE}.config.toml"
    if not profile.exists() or profile.read_text(encoding = "utf-8") != profile_text:
        profile.write_text(profile_text, encoding = "utf-8")
        typer.echo(f"Updated {profile}")


def write_codex_subagent_bridge(
    base: str, key: str, model: dict, home: Path, *, yolo: bool
) -> Path:
    """Write private config for an explicit local Codex child launched through MCP."""
    child_home = home / "child"
    write_codex_config(base, model, child_home)
    path = home / "subagent.json"
    _write_private_json(
        path,
        {
            "api_key": key,
            "codex_home": str(child_home),
            "bypass_permissions": yolo,
        },
    )
    return path


def _wsl_windows_user_profile(executable: str) -> Path:
    """Return the Windows user profile as a path accessible from WSL."""
    profile = os.environ.get("USERPROFILE", "").strip()
    if not profile:
        try:
            profile = subprocess.check_output(
                ["cmd.exe", "/d", "/c", "echo %USERPROFILE%"],
                text = True,
                stderr = subprocess.DEVNULL,
                cwd = str(Path(executable).parent),
            ).strip()
        except (OSError, subprocess.CalledProcessError) as exc:
            _fail(f"Could not find the Windows user profile for Codex: {exc}")
    if not profile or profile == "%USERPROFILE%":
        _fail("Could not find the Windows user profile for Codex.")
    if profile.startswith("/"):
        return Path(profile)
    try:
        translated = subprocess.check_output(
            ["wslpath", "-u", profile],
            text = True,
            stderr = subprocess.DEVNULL,
        ).strip()
    except (OSError, subprocess.CalledProcessError) as exc:
        _fail(f"Could not translate Windows user profile {profile}: {exc}")
    if not translated:
        _fail(f"Could not translate Windows user profile {profile}.")
    return Path(translated)


def _codex_source_home(*, ignore_configured: bool = False) -> Path:
    configured = None if ignore_configured else os.environ.get("CODEX_HOME")
    if configured:
        if _wsl_windows_executable(["codex"]) and _looks_like_path(configured):
            if not configured.startswith("/"):
                try:
                    configured = subprocess.check_output(
                        ["wslpath", "-u", configured],
                        text = True,
                        stderr = subprocess.DEVNULL,
                    ).strip()
                except (OSError, subprocess.CalledProcessError) as exc:
                    _fail(f"Could not translate Windows CODEX_HOME {configured}: {exc}")
                if not configured:
                    _fail("Could not translate Windows CODEX_HOME.")
        return Path(configured).expanduser()
    executable = _wsl_windows_executable(["codex"])
    if executable:
        return _wsl_windows_user_profile(executable) / ".codex"
    return Path.home() / ".codex"


def _remove_overlay_entry(path: Path) -> None:
    is_junction = getattr(path, "is_junction", None)
    if is_junction and is_junction():
        path.rmdir()
    elif path.is_symlink() or path.is_file():
        path.unlink()
    elif path.is_dir():
        shutil.rmtree(path)
    elif path.exists():
        path.unlink()


def _create_directory_junction(source: Path, target: Path) -> bool:
    if os.name != "nt":
        return False
    try:
        result = subprocess.run(
            ["cmd.exe", "/d", "/c", "mklink", "/J", str(target), str(source)],
            capture_output = True,
            text = True,
            timeout = 30,
            check = False,
        )
    except (OSError, subprocess.SubprocessError):
        return False
    return result.returncode == 0


def write_codex_parent_overlay(overlay: Path) -> Path:
    """Add local-agent routing without replacing the cloud parent's configuration."""
    overlay.mkdir(parents = True, exist_ok = True, mode = 0o700)

    manifest_path = overlay / _CODEX_PARENT_OVERLAY_MANIFEST
    try:
        manifest = json.loads(manifest_path.read_text(encoding = "utf-8"))
    except (FileNotFoundError, OSError, json.JSONDecodeError):
        manifest = None
    source_home = _codex_source_home()
    overlay_key = str(overlay.resolve(strict = False))
    source_key = str(source_home.resolve(strict = False))
    if source_key == overlay_key:
        previous_source = manifest.get("source_home") if isinstance(manifest, dict) else None
        if isinstance(previous_source, str) and previous_source:
            candidate = Path(previous_source).expanduser()
            if str(candidate.resolve(strict = False)) != overlay_key:
                source_home = candidate
            else:
                source_home = _codex_source_home(ignore_configured = True)
        else:
            source_home = _codex_source_home(ignore_configured = True)
        source_key = str(source_home.resolve(strict = False))
    same_source = isinstance(manifest, dict) and manifest.get("source_home") == source_key
    if same_source:
        managed_entries = manifest.get("entries", [])
        if not isinstance(managed_entries, list):
            managed_entries = []
        for name in managed_entries:
            if isinstance(name, str) and name not in {"", ".", ".."} and Path(name).name == name:
                _remove_overlay_entry(overlay / name)
    else:
        # A reused overlay must never mix credentials, config, or plugins from two
        # different Codex homes. Legacy overlays have no manifest, so rebuild them once.
        for target in list(overlay.iterdir()):
            _remove_overlay_entry(target)

    # Keep the user's auth, config, plugins, agents, skills, rules, and session state visible.
    # Symlinks make this an overlay rather than a stale copy. If Windows denies them,
    # use directory junctions so large runtime state remains shared without a bulk copy.
    # Copy the configuration surfaces and sessions only if both link forms are unavailable.
    fallback_dirs = {"agents", "skills", "rules", "plugins", "marketplaces", "sessions"}
    entries = []
    if source_home.is_dir():
        for source in source_home.iterdir():
            if source.name in {
                "AGENTS.md",
                "AGENTS.override.md",
                _CODEX_PARENT_OVERLAY_MANIFEST,
            }:
                continue
            target = overlay / source.name
            _remove_overlay_entry(target)
            try:
                target.symlink_to(source, target_is_directory = source.is_dir())
                entries.append(source.name)
            except OSError:
                if source.is_file():
                    shutil.copy2(source, target)
                    entries.append(source.name)
                elif source.is_dir():
                    if _create_directory_junction(source, target):
                        entries.append(source.name)
                    elif source.name in fallback_dirs:
                        shutil.copytree(source, target)
                        entries.append(source.name)

    _write_private_json(
        manifest_path,
        {"source_home": source_key, "entries": sorted(entries)},
    )

    inherited = ""
    instruction_name = "AGENTS.md"
    for candidate in (source_home / "AGENTS.override.md", source_home / "AGENTS.md"):
        try:
            text = candidate.read_text(encoding = "utf-8")
        except FileNotFoundError:
            continue
        except OSError as exc:
            _fail(f"Could not preserve Codex instructions from {candidate}: {exc}")
        if text.strip():
            inherited = text.rstrip()
            instruction_name = candidate.name
            break

    other_name = "AGENTS.md" if instruction_name == "AGENTS.override.md" else "AGENTS.override.md"
    other = overlay / other_name
    if other.is_file() or other.is_symlink():
        other.unlink()
    routing = _CODEX_SUBAGENT_ROUTING_INSTRUCTIONS
    combined = f"{inherited}\n\n{routing}\n" if inherited else f"{routing}\n"
    _write_private_text(overlay / instruction_name, combined)
    return overlay


def _agent_config_path(path: Path, command: list) -> str:
    """Translate a generated config path when a Windows agent runs through WSL."""
    return _wsl_windows_path(path) if _wsl_windows_executable(command) else str(path)


def _opencode_subagent_inline_config(path: Path, permission: dict) -> dict:
    """Keep the local provider visible without hiding the parent's allowed providers."""
    inline: dict = {}
    inherited = os.environ.get("OPENCODE_CONFIG_CONTENT")
    if inherited:
        try:
            parsed = json.loads(inherited)
        except ValueError:
            _fail("OPENCODE_CONFIG_CONTENT is not valid JSON.")
        if not isinstance(parsed, dict):
            _fail("OPENCODE_CONFIG_CONTENT must contain a JSON object.")
        inline.update(parsed)

    def merge_provider_filters(effective_config: dict) -> None:
        enabled = effective_config.get("enabled_providers")
        if isinstance(enabled, list):
            inherited_enabled = inline.get("enabled_providers")
            if not isinstance(inherited_enabled, list):
                inherited_enabled = []
            providers = [
                provider
                for provider in [*inherited_enabled, *enabled]
                if provider != _OPENCODE_PROVIDER
            ]
            inline["enabled_providers"] = list(dict.fromkeys([*providers, _OPENCODE_PROVIDER]))
        disabled = effective_config.get("disabled_providers")
        if isinstance(disabled, list) and _OPENCODE_PROVIDER in disabled:
            inline["disabled_providers"] = [
                provider for provider in disabled if provider != _OPENCODE_PROVIDER
            ]

    # The inherited inline layer is already highest priority. Merge it even when
    # OpenCode is not installed yet, as in fresh-install and --no-launch flows.
    merge_provider_filters(inline)
    effective = inline

    executable = _which_with_install_dirs("opencode")
    if executable is None:
        typer.echo(
            f"Warning: OpenCode is not installed, so provider filters could not be checked. "
            f"The target configuration must allow '{_OPENCODE_PROVIDER}'.",
            err = True,
        )
    else:
        env = _probe_env(OPENCODE_CONFIG = _agent_config_path(path, ["opencode"]))
        try:
            resolved = subprocess.run(
                [executable, "debug", "config"],
                capture_output = True,
                text = True,
                timeout = 15,
                env = env,
            )
        except Exception as exc:
            _fail(f"Could not inspect OpenCode provider filters: {exc}")
        if resolved.returncode != 0:
            detail = resolved.stderr.strip() or resolved.stdout.strip()
            _fail(f"Could not inspect OpenCode provider filters: {detail or 'unknown error'}")
        try:
            effective = json.loads(resolved.stdout)
        except ValueError:
            _fail("Could not inspect OpenCode provider filters: invalid JSON response.")
        if not isinstance(effective, dict):
            _fail("Could not inspect OpenCode provider filters: expected a JSON object.")

        merge_provider_filters(effective)

    depth = effective.get("subagent_depth")
    inline["subagent_depth"] = (
        depth if isinstance(depth, int) and not isinstance(depth, bool) and depth > 0 else 1
    )
    if permission:
        inline["permission"] = permission
    return inline


def _b64_path(path: Path) -> str:
    """Path as base64, so it can cross a shell without being expanded."""
    return base64.b64encode(str(path).encode("utf-8")).decode("ascii")


_CLAUDE_PLAN_GATE_SCRIPT = '''\
"""Deny the editing agent while the parent session is in plan mode."""
import json, sys

try:
    mode = (json.load(sys.stdin) or {}).get("permission_mode")
except Exception:
    sys.exit(0)  # fail open: a hook error must never block the parent session
if mode == "plan":
    print(json.dumps({"hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "permissionDecision": "deny",
        "permissionDecisionReason": (
            "Plan mode is active. Call the read-only Unsloth plan agent "
            "(unsloth_plan_agent) instead of unsloth_agent."
        ),
    }}))
sys.exit(0)
'''


def write_claude_subagent_plugin(path: Path, server_env: dict) -> Path:
    """Write a session plugin that exposes the local Claude child through MCP."""
    plugin = path / "unsloth-local-agent"
    command = sys.executable
    args = ["-m", _CLAUDE_SUBAGENT_MCP_MODULE]
    mcp_env = dict(server_env)
    if _wsl_windows_executable(["claude"]):
        command = "wsl.exe"
        args = [
            "-d",
            os.environ["WSL_DISTRO_NAME"],
            "--",
            sys.executable,
            "-m",
            _CLAUDE_SUBAGENT_MCP_MODULE,
        ]
        mcp_env["WSLENV"] = _merge_wslenv(
            os.environ.get("WSLENV", ""),
            _wsl_bridge_names(server_env, ()),
        )
    _write_private_json(
        plugin / ".claude-plugin" / "plugin.json",
        {
            "name": "unsloth-local-agent",
            "version": "1.0.0",
            "description": _SUBAGENT_DESCRIPTION,
            "author": {"name": "Unsloth AI"},
        },
    )
    _write_private_json(
        plugin / ".mcp.json",
        {
            "mcpServers": {
                "unsloth": {
                    "type": "stdio",
                    "command": command,
                    "args": args,
                    "env": mcp_env,
                }
            }
        },
    )
    # Claude already refuses the editing tool in plan mode, since it advertises
    # readOnlyHint false. This PreToolUse hook replaces that dead end with a reason
    # naming the read-only tool to call instead. Skipped under the WSL bridge, where
    # the gate is a Linux path but the hook would run beside the Windows claude.
    gate = plugin / "hooks" / "plan_gate.py"
    if command == "wsl.exe":
        # A persisted plugin dir may still hold a gate from an earlier non-WSL run.
        for stale in (gate, plugin / "hooks" / "hooks.json"):
            stale.unlink(missing_ok = True)
    else:
        _write_private_text(gate, _CLAUDE_PLAN_GATE_SCRIPT)
        _write_private_json(
            plugin / "hooks" / "hooks.json",
            {
                "hooks": {
                    "PreToolUse": [
                        {
                            "matcher": _CLAUDE_SUBAGENT_TOOL,
                            "hooks": [
                                {
                                    "type": "command",
                                    # Run through runpy rather than handing the path to
                                    # the interpreter: a missing gate is then an
                                    # ordinary traceback (exit 1, fails open) instead
                                    # of exit 2, which Claude treats as a blocking
                                    # error and would deny the tool in every mode.
                                    # The path is base64'd because this string goes
                                    # through a shell: a temp root holding $(..) or a
                                    # backtick expands under sh, %VAR% under cmd, and
                                    # the gate then silently fails open. base64's
                                    # alphabet has no metacharacter in either.
                                    "command": (
                                        f'"{sys.executable}" -c '
                                        f'"import base64,runpy; runpy.run_path('
                                        f"base64.b64decode('{_b64_path(gate)}').decode())\""
                                    ),
                                    # A hook with no timeout stalls the parent for as
                                    # long as it hangs; measured unbounded past 400s.
                                    "timeout": 10,
                                }
                            ],
                        }
                    ]
                }
            },
        )
    skill = plugin / "skills" / "local-agent" / "SKILL.md"
    skill.parent.mkdir(parents = True, exist_ok = True, mode = 0o700)
    skill.write_text(
        "---\n"
        "description: Delegate a task to the local agent powered by Unsloth. Use when the "
        "user asks to spawn an Unsloth agent or local agent.\n"
        "---\n\n"
        "Call the Unsloth local agent tool once with the complete task. In plan mode, call "
        "the read-only Unsloth plan agent instead. Return its result to the user without "
        "claiming that the cloud parent completed the local work.\n",
        encoding = "utf-8",
    )
    return plugin


def _codex_subagent_flags(path: Path) -> list[str]:
    command = sys.executable
    package_root = str(Path(__file__).resolve().parents[2])
    bootstrap = (
        f"import sys;sys.path.insert(0,{json.dumps(package_root)});"
        f"from {_CODEX_SUBAGENT_MCP_MODULE} import main;main()"
    )
    args = ["-c", bootstrap, str(path)]
    if _wsl_windows_executable(["codex"]):
        command = "wsl.exe"
        args = [
            "-d",
            os.environ["WSL_DISTRO_NAME"],
            "--",
            sys.executable,
            "-c",
            bootstrap,
            str(path),
        ]
    server = (
        "{ "
        f"command = {json.dumps(command)}, "
        f"args = {json.dumps(args)}, "
        f"required = true, enabled_tools = [{json.dumps(_CODEX_SUBAGENT_MCP_TOOL)}], "
        'default_tools_approval_mode = "approve", '
        "startup_timeout_sec = 15, tool_timeout_sec = 3600 }"
    )
    return ["-c", f"mcp_servers.{_CODEX_SUBAGENT_MCP_SERVER}={server}"]


def _wsl_windows_executable(command: list) -> Optional[str]:
    if os.name == "nt" or not os.environ.get("WSL_DISTRO_NAME"):
        return None
    executable = shutil.which(command[0])
    if executable and executable.startswith("/mnt/"):
        return executable
    return None


def _wsl_windows_path(path: Path) -> str:
    try:
        translated = subprocess.check_output(["wslpath", "-w", str(path)], text = True).strip()
    except (OSError, subprocess.CalledProcessError) as exc:
        _fail(f"Could not translate WSL path {path}: {exc}")
    if not translated:
        _fail(f"Could not translate WSL path {path}")
    return translated


def _looks_like_path(value: str) -> bool:
    # A var only wants the WSLENV /p flag if its value is a filesystem path: an
    # absolute POSIX path (/...), a UNC path (\\...), or a drive-qualified Windows
    # path (C:...). Scalar knobs (e.g. a numeric context window) must pass through
    # untranslated, so they get no flag.
    return bool(value) and (value.startswith(("/", "\\")) or (len(value) >= 2 and value[1] == ":"))


def _wsl_bridge_names(env: dict, unset_env: tuple) -> tuple:
    # Build the WSLENV share list for a Windows shim reached from WSL. Path-valued
    # vars get /p so WSLENV translates them to the Windows path the /mnt shim can
    # actually open; a cleared var carries no value to translate.
    names = [name + ("/p" if _looks_like_path(value) else "") for name, value in env.items()]
    names.extend(unset_env)
    return tuple(dict.fromkeys(names))


def _merge_wslenv(current: str, names: tuple) -> str:
    # Index WSLENV entries by bare var name, preserving first-seen order. The vars we
    # bridge are applied last so our entry wins: a user's pre-existing unflagged "HOME"
    # is upgraded to "HOME/p" (rather than left as-is), since WSLENV ignores a duplicate
    # name and a bare entry would leave the path untranslated for a Windows shim.
    ordered = []
    by_name = {}
    for entry in (*current.split(":"), *names):
        if not entry:
            continue
        base = entry.split("/", 1)[0]
        if base not in by_name:
            ordered.append(base)
        by_name[base] = entry
    return ":".join(by_name[base] for base in ordered)


def _powershell_quote(arg: str) -> str:
    # PowerShell reads single-quoted strings literally (an embedded ' is doubled), so
    # JSON args such as `--settings {"env":...}` survive intact. list2cmdline's
    # backslash-escaped double quotes are cmd.exe syntax and PowerShell mis-parses them.
    if arg and re.fullmatch(r"[A-Za-z0-9_./:=+-]+", arg):
        return arg
    return "'" + arg.replace("'", "''") + "'"


def _print_env(
    env: dict,
    command: list,
    unset_env: tuple = (),
    wsl_env_bridge: tuple = (),
    cwd_env: tuple = (),
) -> None:
    if os.name == "nt":
        for name in unset_env:
            typer.echo(f"Remove-Item Env:{name} -ErrorAction SilentlyContinue")
        for name, value in env.items():
            # PowerShell: ` is the escape char, and $ triggers expansion inside "".
            escaped = value.replace("`", "``").replace('"', '`"').replace("$", "`$")
            typer.echo(f'$env:{name} = "{escaped}"')
        for name in cwd_env:
            typer.echo(f"$env:{name} = (Get-Location).Path")
        typer.echo(" ".join(_powershell_quote(arg) for arg in command))
        return
    for name in unset_env:
        typer.echo(f"export {name}=" if wsl_env_bridge else f"unset {name}")
    for name, value in env.items():
        typer.echo(f"export {name}={shlex.quote(value)}")
    for name in cwd_env:
        typer.echo(f'export {name}="$PWD"')
    if wsl_env_bridge:
        typer.echo(
            f"export WSLENV={shlex.quote(_merge_wslenv(os.environ.get('WSLENV', ''), wsl_env_bridge))}"
        )
    # The final line is a SELF-CONTAINED one-liner (inline env, VAR=... cmd) rather than a
    # bare command. People copy just the last line, and a bare `codex`/`claude` would then
    # run against their real ~/.codex or Anthropic credentials with zero isolation -- e.g.
    # inheriting a pre-existing damaged ~/.codex state DB and blaming the recipe. Inline
    # assignments scope every var (and empty-string the conflicting ones) to this single
    # invocation, so a partial copy behaves the same as pasting the whole block.
    inline = [f"{name}=" for name in unset_env]
    inline += [f"{name}={shlex.quote(value)}" for name, value in env.items()]
    inline += [f'{name}="$PWD"' for name in cwd_env]
    if wsl_env_bridge:
        inline.append(
            f"WSLENV={shlex.quote(_merge_wslenv(os.environ.get('WSLENV', ''), wsl_env_bridge))}"
        )
    typer.echo(" ".join((*inline, shlex.join(command))))


def _refresh_windows_path() -> None:
    # Merge Windows registry PATH hives after the current process PATH so a
    # freshly installed agent is visible without changing existing precedence.
    if os.name != "nt":
        return
    try:
        import winreg
    except Exception:
        return

    entries = []
    seen = set()

    def add_path(value: str) -> bool:
        added = False
        for entry in str(value).split(os.pathsep):
            entry = entry.strip()
            if not entry:
                continue
            key = os.path.normcase(entry).casefold()
            if key in seen:
                continue
            seen.add(key)
            entries.append(entry)
            added = True
        return added

    add_path(os.environ.get("PATH", ""))
    added_registry = False
    hives = (
        (winreg.HKEY_CURRENT_USER, "Environment"),
        (
            winreg.HKEY_LOCAL_MACHINE,
            r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment",
        ),
    )
    for root, sub in hives:
        try:
            with winreg.OpenKey(root, sub) as key:
                value, _ = winreg.QueryValueEx(key, "Path")
        except OSError:
            continue
        if value:
            added_registry = add_path(os.path.expandvars(str(value))) or added_registry
    if added_registry:
        os.environ["PATH"] = os.pathsep.join(entries)


def _managed_node_tools() -> Optional[tuple[Path, Path, bool]]:
    # Best-effort: any failure here means "no managed Node", never a broken launch.
    try:
        ensure_studio_backend_path()
        from utils.node_runtime import managed_node_binary, resolve_node_executable
        node = Path(managed_node_binary())
    except (ImportError, OSError, RuntimeError, TypeError, ValueError):
        return None
    npm = node.with_name("npm.cmd" if os.name == "nt" else "npm")
    try:
        usable = node.is_file() and npm.is_file()
        if os.name != "nt":
            usable = usable and os.access(node, os.X_OK) and os.access(npm, os.X_OK)
    except OSError:
        return None
    if not usable:
        return None
    try:
        resolved = resolve_node_executable()
        preferred = bool(resolved) and Path(resolved).resolve() == node.resolve()
    except (OSError, RuntimeError, TypeError, ValueError):
        preferred = False
    return node, npm, preferred


def _augment_path_with_install_dirs() -> None:
    # Add known install dirs to PATH so a freshly installed agent resolves without a new shell.
    # User dirs are appended (existing tools keep precedence); a preferred managed Node is
    # prepended so Node-backed shims use it. Only user dirs need a home, so a missing home must
    # not drop the managed Node, or a shim fails on `env node` under a bare container UID.
    try:
        home = Path.home()
    except (RuntimeError, OSError):
        home = None
    candidates = [home / ".local" / "bin"] if home is not None else []
    if os.name == "nt":
        appdata = os.environ.get("APPDATA")
        if appdata:
            candidates.append(Path(appdata) / "npm")
    managed_node = _managed_node_tools()
    if managed_node is not None and not managed_node[2]:
        candidates.append(managed_node[0].parent)
    current = os.environ.get("PATH")
    if current is None:
        # PATH unset: shutil.which() and exec*p* fall back to os.defpath (e.g. /bin:/usr/bin), so
        # keep that default instead of collapsing to just the install dirs (which would hide a
        # system-installed agent and strip the launched child's normal PATH). An explicitly empty
        # PATH is left as-is: like shutil.which, it means "search nothing", not os.defpath.
        current = os.defpath
    preferred_node = (
        str(managed_node[0].parent) if managed_node is not None and managed_node[2] else None
    )
    if preferred_node:
        preferred_key = os.path.normcase(preferred_node)
        current = os.pathsep.join(
            entry
            for entry in current.split(os.pathsep)
            if not entry or os.path.normcase(entry) != preferred_key
        )
    seen = {os.path.normcase(entry) for entry in current.split(os.pathsep) if entry}
    additions = [
        str(directory)
        for directory in candidates
        if directory.is_dir() and os.path.normcase(str(directory)) not in seen
    ]
    if preferred_node or additions:
        parts = [part for part in (preferred_node, current, *additions) if part]
        os.environ["PATH"] = os.pathsep.join(parts)


def _probe_env(**extra: str) -> dict:
    """Environment for probes that RUN a resolved shim.

    _which_with_install_dirs restores PATH before returning, so a shim backed by Studio's
    managed Node would not find that node when executed.
    """
    original = os.environ.get("PATH")
    _augment_path_with_install_dirs()
    env = os.environ.copy()
    if original is None:
        os.environ.pop("PATH", None)
    else:
        os.environ["PATH"] = original
    env.update(extra)
    return env


def _which_with_install_dirs(name: str) -> Optional[str]:
    # shutil.which(name), but searching the known agent install dirs too, so a version probe
    # resolves the same binary _launch() will (it augments PATH before it runs). Without this an
    # agent present only in ~/.local/bin / %APPDATA%\npm is missed, wrongly assumed current, and
    # launched with flags an older build rejects. PATH is restored afterward: only _launch()
    # should persist the augmentation for the child process.
    original = os.environ.get("PATH")
    _augment_path_with_install_dirs()
    try:
        return shutil.which(name)
    finally:
        if original is None:
            os.environ.pop("PATH", None)
        else:
            os.environ["PATH"] = original


def _install_source(install_hint: str) -> Optional[str]:
    """The first http(s) URL an install hint fetches, or None (e.g. an npm install)."""
    match = re.search(r"https?://[^\s'\")]+", install_hint)
    return match.group(0) if match else None


def _pinned_raw_github_commit(source: str) -> Optional[str]:
    """Return the immutable full commit in a raw GitHub URL, if present."""
    match = re.match(
        r"^https://raw\.githubusercontent\.com/[^/]+/[^/]+/([0-9a-f]{40})/",
        source,
        flags = re.IGNORECASE,
    )
    return match.group(1).lower() if match else None


def _npm_executable() -> Optional[str]:
    managed_node = _managed_node_tools()
    if not (managed_node and managed_node[2]):
        executable = shutil.which("npm")
        if executable and not _wsl_windows_executable([executable]):
            return executable
        if executable:
            # WSL inherits the Windows PATH, so the rejected shim may shadow a native npm.
            for directory in os.get_exec_path():
                candidate = shutil.which("npm", path = directory)
                if candidate and not _wsl_windows_executable([candidate]):
                    return candidate

    return str(managed_node[1]) if managed_node is not None else None


def _install_command(install_hint: str) -> tuple[list[str], Optional[dict]]:
    if not re.match(r"^\s*npm(?:\s|$)", install_hint):
        if os.name == "nt":
            return (
                [
                    "powershell",
                    "-NoProfile",
                    "-ExecutionPolicy",
                    "Bypass",
                    "-Command",
                    install_hint,
                ],
                None,
            )
        return ["/bin/sh", "-c", install_hint], None

    npm = _npm_executable()
    if npm is None:
        _fail(
            "npm is required to install this agent, but no native system npm or usable "
            "Unsloth-managed Node installation was found. Install Node.js with npm, "
            "then re-run."
        )
    args = shlex.split(install_hint)
    env = dict(os.environ)
    # dirname, not Path().parent: Path picks its flavour from os.name, which the tests
    # override. Empty means npm is a bare name; prepending "" would put the cwd on PATH.
    npm_dir = os.path.dirname(npm)
    current_path = env.get("PATH", "")
    if npm_dir:
        env["PATH"] = os.pathsep.join([npm_dir, current_path]) if current_path else npm_dir
    if os.name == "nt":
        command = "& " + " ".join(_powershell_quote(arg) for arg in [npm, *args[1:]])
        return (
            [
                "powershell",
                "-NoProfile",
                "-ExecutionPolicy",
                "Bypass",
                "-Command",
                command,
            ],
            env,
        )
    return [npm, *args[1:]], env


def _install_agent(name: str, install_hint: str) -> Optional[str]:
    # Missing agent under --launch: offer to run its documented install command, then
    # re-resolve it on PATH. Consent-based (we never auto-run a remote install script
    # silently), and a non-interactive stdin cannot answer the prompt, so both the
    # no-TTY and declined cases return None and let the caller print the hint and exit.
    if not sys.stdin.isatty():
        return None
    typer.echo(f"`{name}` is not installed.")
    # Make the supply-chain risk explicit before the prompt: these are the vendors'
    # own installers (curl | bash, irm | iex, npm), run with the user's privileges,
    # and nothing checks a signature or hash on the fetched content. Naming the source
    # turns a blind "yes" into informed consent.
    source = _install_source(install_hint)
    if source:
        pinned_commit = _pinned_raw_github_commit(source)
        if pinned_commit:
            warning = (
                "Security warning: This will download and execute a third-party script "
                f"from {source} with your privileges. Unsloth pins this content to "
                f"immutable upstream commit {pinned_commit}, but does not independently "
                "verify or sandbox it. Continue only if you trust this source and commit."
            )
        else:
            warning = (
                "Security warning: This will download and execute an unverified third-party "
                f"script from {source} with your privileges. Unsloth does not pin or verify "
                "the downloaded content. Continue only if you trust this source."
            )
    else:
        warning = (
            f"This will RUN `{install_hint}` with your privileges; "
            "there is no signature or hash check."
        )
    typer.secho(warning, fg = "yellow", err = True)
    if not typer.confirm(f"Install `{name}` now with `{install_hint}`?", default = False):
        return None
    install_command, install_env = _install_command(install_hint)
    try:
        result = subprocess.run(install_command, env = install_env)
    except OSError as exc:
        _fail(
            f"Could not run the install command: {exc}. "
            f"Run it yourself, then re-run: {install_hint}"
        )
    if result.returncode != 0:
        message = f"Install command failed. Run it yourself, then re-run: {install_hint}"
        if os.name == "nt":
            # A hand-run retry can still hit the policy; point at the one-time per-user fix.
            message += (
                "\nIf it fails because running scripts is disabled (PSSecurityException), "
                "allow local scripts for your user, then retry:\n"
                "  Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned"
            )
        _fail(message)
    # Resolve the freshly installed agent without a shell restart: pull registry PATH
    # (Windows) plus well-known install dirs the installer may not have added to PATH.
    _refresh_windows_path()
    _augment_path_with_install_dirs()
    executable = shutil.which(name)
    if executable is None:
        _fail(
            f"`{name}` installed but isn't on PATH yet. Open a new shell (or add it to "
            f"PATH), then re-run. Install command: {install_hint}"
        )
    return executable


def _resolve_or_install_agent(name: str, install_hint: str, resolver) -> str:
    executable = resolver(name) or _install_agent(name, install_hint)
    if executable is None:
        _fail(f"`{name}` not found on PATH. Install it with: {install_hint}")
    return executable


def _require_agent_for_launch(name: str, install_hint: str, launch: bool) -> Optional[str]:
    if not launch:
        return None
    return _resolve_or_install_agent(name, install_hint, _which_with_install_dirs)


def _wsl_shim_env(
    command: list,
    env: dict,
    unset_env: tuple,
    cwd_env: tuple = (),
) -> tuple[dict, tuple]:
    if not _wsl_windows_executable(command):
        return env, ()
    wsl_env_bridge = _wsl_bridge_names(env, unset_env)
    if not wsl_env_bridge and not cwd_env:
        return env, ()
    # Bridge PWD via WSLENV (PWD/p) so the Windows shim finds its project root from the
    # live cwd, not a stale inherited Linux PWD. Don't freeze env["PWD"]: a --no-launch
    # recipe must translate the live PWD when run, not when generated; _launch overrides it.
    # cwd_env values likewise resolve at execution time and are always filesystem paths.
    return env, tuple(dict.fromkeys((*wsl_env_bridge, *(f"{name}/p" for name in cwd_env), "PWD/p")))


_NPM_CMD_SHIM_HEAD = (
    "@ECHO off\n"
    "GOTO start\n"
    ":find_dp0\n"
    "SET dp0=%~dp0\n"
    "EXIT /b\n"
    ":start\n"
    "SETLOCAL\n"
    "CALL :find_dp0\n"
)
_NPM_NODE_CMD_SHIM_PREFIX = (
    re.escape(_NPM_CMD_SHIM_HEAD)
    + r"(?P<environment>(?:@SET [^=\r\n]+=[^\r\n]+\n)*)"
    + re.escape(
        '\nIF EXIST "%dp0%\\node.exe" (\n'
        + '  SET "_prog=%dp0%\\node.exe"\n'
        + ") ELSE (\n"
        + '  SET "_prog=node"\n'
    )
)
_NPM_NODE_CMD_SHIM_SUFFIX = (
    r"(?P<node_args>[^\r\n]*?)[ \t]+" + r'"%dp0%\\(?P<target>[^"\r\n]+)"[ \t]+%\*'
)
_NPM_NODE_CMD_SHIMS = (
    re.compile(
        _NPM_NODE_CMD_SHIM_PREFIX
        + re.escape(
            "  SET PATHEXT=%PATHEXT:;.JS;=;%\n"
            + ")\n\n"
            + 'endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%"'
        )
        + _NPM_NODE_CMD_SHIM_SUFFIX,
        re.IGNORECASE,
    ),
    re.compile(
        _NPM_NODE_CMD_SHIM_PREFIX
        + re.escape(
            ")\n\n"
            + "endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "
            + "set PATHEXT=%PATHEXT:;.JS;=;% & "
            + '"%_prog%"'
        )
        + _NPM_NODE_CMD_SHIM_SUFFIX,
        re.IGNORECASE,
    ),
)
_NPM_NATIVE_CMD_SHIM = re.compile(
    re.escape(_NPM_CMD_SHIM_HEAD) + r'"%dp0%\\(?P<target>[^"\r\n]+)"[ \t]+%\*',
    re.IGNORECASE,
)
_NPM_NODE_SHEBANG = re.compile(
    r"^#!\s*(?:/usr/bin/env\s+(?:-S\s+)?((?:[^ \t=]+=[^ \t=]+\s+)*))?([^ \t]+)(.*)$"
)
_NPM_SHEBANG_DOLLAR = re.compile(r"\$\{?([^$@#?\- \t{}:]+)\}?")


def _npm_batch_environment(declarations: str) -> str:
    lines = []
    for declaration in declarations.split():
        name, separator, value = declaration.partition("=")
        name = name.strip()
        value = value.strip()
        if separator and name and value:
            value = _NPM_SHEBANG_DOLLAR.sub(lambda match: f"%{match.group(1)}%", value)
            lines.append(f"@SET {name}={value}\n")
    return "".join(lines)


def _windows_expand_environment(value: str, environment: dict) -> str:
    folded = {name.casefold(): item for name, item in environment.items()}
    return re.sub(
        r"%([^%\r\n]+)%",
        lambda match: folded.get(match.group(1).casefold(), ""),
        value,
    )


def _npm_node_shim_metadata(target: Path, match, environment: dict) -> Optional[tuple]:
    environment_block = match.group("environment") or ""
    node_args_text = (match.group("node_args") or "").strip()
    known_node_suffix = target.suffix.lower() in {".js", ".cjs", ".mjs"}
    if not environment_block and not node_args_text and known_node_suffix:
        return [], {}

    first_line = target.read_text(encoding = "utf-8").splitlines()[0]
    shebang = _NPM_NODE_SHEBANG.fullmatch(first_line)
    if shebang is None or Path(shebang.group(2)).name.casefold() not in {"node", "node.exe"}:
        return None
    declarations = shebang.group(1) or ""
    if _npm_batch_environment(declarations).casefold() != environment_block.casefold():
        return None
    if (shebang.group(3) or "").strip() != node_args_text:
        return None
    try:
        node_args = shlex.split(node_args_text) if node_args_text else []
    except ValueError:
        return None

    updates = {}
    expanded_environment = dict(environment)
    for line in environment_block.splitlines():
        name, value = line.removeprefix("@SET ").split("=", 1)
        expanded = _windows_expand_environment(value, expanded_environment)
        expanded_environment[name] = expanded
        updates[name] = expanded
    return node_args, updates


def _apply_windows_environment(environment: dict, updates: dict) -> None:
    for name, value in updates.items():
        existing = next((key for key in environment if key.casefold() == name.casefold()), None)
        if existing is not None and existing != name:
            del environment[existing]
        environment[name] = value


def _resolved_launch_command(
    executable: str,
    arguments: list,
    environment: Optional[dict] = None,
) -> list:
    """Return an argv that preserves arguments through standard Windows npm shims."""
    if os.name == "nt" and Path(executable).suffix.lower() in {".cmd", ".bat"}:
        # cmd.exe treats CR/LF inside `%*` as command separators, and Windows
        # PowerShell's native-command bridge also rewrites embedded quotes. Match
        # complete cmd-shim templates so custom wrappers keep their setup behavior.
        with contextlib.suppress(OSError, UnicodeError, IndexError):
            shim = Path(executable)
            contents = shim.read_text(encoding = "utf-8").replace("\r\n", "\n").strip()
            for pattern in _NPM_NODE_CMD_SHIMS:
                match = pattern.fullmatch(contents)
                if match is None:
                    continue
                relative = Path(*re.split(r"[\\/]+", match.group("target")))
                target = (shim.parent / relative).resolve()
                if not target.is_file() or not any(
                    part.casefold() == "node_modules" for part in target.parts
                ):
                    continue
                metadata = _npm_node_shim_metadata(target, match, environment or os.environ)
                if metadata is None:
                    continue
                node_args, environment_updates = metadata
                bundled_node = shim.parent / "node.exe"
                node = str(bundled_node) if bundled_node.is_file() else shutil.which("node.exe")
                if node:
                    if environment is not None:
                        _apply_windows_environment(environment, environment_updates)
                    return [node, *node_args, str(target), *arguments]

            match = _NPM_NATIVE_CMD_SHIM.fullmatch(contents)
            if match is not None:
                relative = Path(*re.split(r"[\\/]+", match.group("target")))
                target = (shim.parent / relative).resolve()
                if (
                    target.is_file()
                    and any(part.casefold() == "node_modules" for part in target.parts)
                    and target.suffix.lower() in {".exe", ".com"}
                ):
                    return [str(target), *arguments]
    return [executable, *arguments]


def _launch(
    command: list,
    env: dict,
    install_hint: str,
    unset_env: tuple = (),
    cwd_env: tuple = (),
) -> int:
    # Resolve well-known install dirs (e.g. ~/.local/bin) first, so an already-installed
    # agent not yet on PATH is found instead of prompting a needless reinstall.
    _augment_path_with_install_dirs()
    executable = _resolve_or_install_agent(command[0], install_hint, shutil.which)
    env, wsl_env_bridge = _wsl_shim_env(command, env, unset_env, cwd_env)
    env = {**env, **{name: os.getcwd() for name in cwd_env}}
    child_env = dict(os.environ)
    if wsl_env_bridge:
        # Override stale inherited PWD with the real cwd so the shim resolves the project root.
        env = {**env, "PWD": os.getcwd()}
        child_env["WSLENV"] = _merge_wslenv(child_env.get("WSLENV", ""), wsl_env_bridge)
        for name in unset_env:
            child_env[name] = ""
    else:
        for name in unset_env:
            child_env.pop(name, None)
    child_env.update(env)
    if os.name != "nt" and not wsl_env_bridge:
        # Keep POSIX child processes from seeing a stale inherited PWD when
        # subprocess cwd was changed by the caller. Some Node CLIs use PWD for
        # project-root discovery instead of process.cwd().
        child_env["PWD"] = os.getcwd()
    # Ctrl+C cancels a turn inside the agent; don't let it kill this wrapper. A no-op
    # handler, not SIG_IGN: exec preserves an ignored signal but resets a caught one.
    previous = signal.signal(signal.SIGINT, lambda *_: None)
    try:
        launch_command = _resolved_launch_command(executable, command[1:], child_env)
        code = subprocess.run(launch_command, env = child_env).returncode
    finally:
        signal.signal(signal.SIGINT, previous)
    # Negative returncode means killed by signal N; shells expect 128+N.
    return code if code >= 0 else 128 - code


def _connect(
    api_key: Optional[str],
    model: Optional[str],
    load: LoadOptions = LoadOptions(),
    *,
    serve: bool = False,
    launch: bool = True,
    server_options: ServerOptions = ServerOptions(),
    preload_check = None,
) -> tuple:
    # `--model org/name:QUANT` is shorthand for `--model org/name --gguf-variant QUANT`.
    # Split it before we match/serve so the attach path resolves against the already-loaded
    # `org/name` (listed without the suffix) instead of reloading a `:`-suffixed repo id --
    # which Unsloth rejects and which would evict a model another session is using.
    if model:
        repo, variant = _split_repo_variant(model)
        if variant:
            model = repo
            if not load.gguf_variant:
                load = load._replace(gguf_variant = variant)
    base, server = _require_studio(
        model, load, serve = serve, launch = launch, server_options = server_options
    )
    try:
        key = _agent_api_key(base, api_key, auto_started = server is not None)
        # A server we just started has exactly the requested model loaded, so resolve to
        # whatever it is serving instead of re-matching the raw --model string.
        # Only an attach can still trigger an evicting load, so _resolve_model gets the
        # pre-load check and runs it only when that load is imminent.
        entry = _resolve_model(
            base,
            key,
            None if server is not None else model,
            load,
            preload_check = None if server is not None else preload_check,
        )
    except BaseException:
        _shutdown_auto_served()
        raise
    return base, key, entry


def _run(
    base: str,
    entry: dict,
    env: dict,
    command: list,
    *,
    launch: bool,
    install_hint: str,
    unset_env: tuple = (),
    clear_screen: bool = False,
    cwd_env: tuple = (),
) -> None:
    # Some agents (Pi) render inline from wherever the cursor sits: their first
    # paint assumes a clean screen rather than clearing or entering the
    # alternate screen themselves. Hand them one so the session doesn't start
    # mid-scroll under our connection output. click.clear() is cross-platform
    # and a no-op when stdout is not a terminal (piped/CI), so transcripts and
    # --no-launch recipes stay intact.
    if launch and clear_screen:
        click.clear()
    typer.echo(f"Unsloth ready at {base} · model {entry['id']}")
    if not launch:
        env, wsl_env_bridge = _wsl_shim_env(command, env, unset_env, cwd_env)
        _print_env(
            env,
            command,
            unset_env = unset_env,
            wsl_env_bridge = wsl_env_bridge,
            cwd_env = cwd_env,
        )
        if _keep_auto_served():
            typer.echo(f"Unsloth Studio is still running at {base}.")
            typer.echo("Stop it with: unsloth studio stop")
        return
    try:
        code = _launch(
            command,
            env,
            install_hint = install_hint,
            unset_env = unset_env,
            cwd_env = cwd_env,
        )
    except BaseException:
        # Startup succeeded but the agent failed to launch; tear the server down
        # rather than orphan it.
        _shutdown_auto_served()
        raise
    auto_started = _auto_served_server is not None
    kept = _keep_auto_served()
    if auto_started and not kept:
        typer.echo(f"The auto-started Unsloth server at {base} stopped during the session.")
        raise typer.Exit(code = code)
    if code:
        # The server status below must not read as a successful agent session.
        typer.echo(f"The agent exited with code {code}.")
    if is_loopback_url(base):
        typer.echo(f"Unsloth Studio is still running at {base}.")
        typer.echo("Stop it with: unsloth studio stop")
    else:
        typer.echo(f"The remote Unsloth server is still running at {base}.")
    raise typer.Exit(code = code)


def _agents_config_root() -> Path:
    return _studio_auth_root() / "agents"


@contextlib.contextmanager
def _temporary_agent_config(prefix: str):
    # Nothing else prunes Studio's auth tree, so reuse the locked session helper: the next
    # launch reclaims homes left by a killed wrapper, and the lock spares live sessions.
    temp_root = _agents_config_root() / ".tmp"
    with contextlib.ExitStack() as stack:
        try:
            temp_root.mkdir(parents = True, exist_ok = True, mode = 0o700)
            path = stack.enter_context(_short_ephemeral_session(temp_root, prefix))
        except OSError:
            # Attaching to a remote or running Studio needs no local auth tree, so it may be
            # absent or unwritable. Fall back to the system temp dir, as before: no
            # reclamation there, but the OS prunes it.
            path = Path(tempfile.mkdtemp(prefix = prefix))
            stack.callback(shutil.rmtree, path, ignore_errors = True)
        yield path


# codex-subagent nests CODEX_HOME under <home>/parent, so it needs the short root too.
_CODEX_SHORT_HOME_AGENTS = ("codex", "codex-subagent")


def _ephemeral_session_parent(agent: str) -> Optional[Path]:
    """Return a non-system-temp parent when an agent needs one."""
    if os.name != "nt" or agent not in _CODEX_SHORT_HOME_AGENTS:
        return None
    # Codex creates a deeply nested curated-plugin checkout below CODEX_HOME.
    # A normal %TEMP%\unsloth-codex-* home can exceed legacy Windows path
    # limits during startup, and Codex also refuses to create its PATH helpers
    # below the system temp directory. Keep the throwaway home short but still
    # private to the current user; _session_config removes it on exit.
    root = Path.home() / ".unsloth" / ".tmp"
    root.mkdir(parents = True, exist_ok = True, mode = 0o700)
    return root


def _ephemeral_session_prefix(agent: str, parent: Optional[Path]) -> str:
    """Return the platform-specific prefix for an ephemeral agent home."""
    if agent in _CODEX_SHORT_HOME_AGENTS and parent is not None:
        return "u-codex-"
    return f"unsloth-{agent}-"


@contextlib.contextmanager
def _locked_file(path: Path, blocking: bool = True):
    """Yield whether an advisory lock was acquired for the first byte of path."""
    handle = path.open("a+b")
    acquired = False
    try:
        if os.name == "nt":
            import msvcrt

            handle.seek(0, os.SEEK_END)
            if handle.tell() == 0:
                handle.write(b"\0")
                handle.flush()
            handle.seek(0)
            while True:
                try:
                    msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, 1)
                    acquired = True
                    break
                except OSError as exc:
                    if exc.errno not in (errno.EACCES, errno.EAGAIN, errno.EDEADLK):
                        raise
                    if not blocking:
                        break
                    # LK_LOCK gives up after roughly ten seconds. Poll LK_NBLCK
                    # instead so a large stale plugin checkout cannot make a
                    # concurrent launch fail just because cleanup takes longer.
                    time.sleep(0.05)
        else:
            import fcntl
            mode = fcntl.LOCK_EX | (0 if blocking else fcntl.LOCK_NB)
            try:
                fcntl.flock(handle.fileno(), mode)
                acquired = True
            except BlockingIOError:
                acquired = False
        yield acquired
    finally:
        if acquired:
            if os.name == "nt":
                handle.seek(0)
                msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
            else:
                fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
        handle.close()


def _reclaim_stale_ephemeral_sessions(parent: Path, prefix: str) -> None:
    """Remove abandoned session homes while preserving locked live sessions."""
    for path in parent.glob(f"{prefix}*"):
        if not path.is_dir():
            continue
        active_lock = path / ".active.lock"
        try:
            modified = active_lock.stat().st_mtime if active_lock.exists() else path.stat().st_mtime
        except FileNotFoundError:
            continue
        # The wrapper owns the advisory lock, not the Codex child. If only the
        # wrapper is killed, its child may still be using CODEX_HOME; give that
        # process a full day to finish before treating the unlocked home as stale.
        if time.time() - modified < _CODEX_EPHEMERAL_STALE_SECONDS:
            continue
        try:
            with _locked_file(active_lock, blocking = False) as stale:
                pass
        except FileNotFoundError:
            # A normally exiting session may have removed itself after the glob.
            continue
        if stale:
            shutil.rmtree(path, ignore_errors = True)


def _refresh_ephemeral_session_marker(path: Path, stop: threading.Event) -> None:
    """Keep the stale grace period relative to wrapper death, not session start."""
    while not stop.wait(_CODEX_EPHEMERAL_HEARTBEAT_SECONDS):
        with contextlib.suppress(OSError):
            os.utime(path, None)


@contextlib.contextmanager
def _short_ephemeral_session(parent: Path, prefix: str = "u-codex-"):
    """Create a session home whose lock makes crash cleanup concurrency-safe."""
    path = None
    active_lock = contextlib.ExitStack()
    heartbeat_stop = None
    heartbeat = None
    try:
        with _locked_file(parent / ".cleanup.lock") as cleanup_lock:
            if not cleanup_lock:  # The blocking acquisition should always succeed.
                raise RuntimeError(f"Could not lock ephemeral session root: {parent}")
            _reclaim_stale_ephemeral_sessions(parent, prefix)
            path = Path(tempfile.mkdtemp(prefix = prefix, dir = parent))
            locked = active_lock.enter_context(_locked_file(path / ".active.lock"))
            if not locked:
                raise RuntimeError(f"Could not lock ephemeral session home: {path}")
            heartbeat_stop = threading.Event()
            heartbeat = threading.Thread(
                target = _refresh_ephemeral_session_marker,
                args = (path / ".active.lock", heartbeat_stop),
                name = "unsloth-agent-home-heartbeat",
                daemon = True,
            )
            heartbeat.start()
        yield path
    finally:
        if heartbeat_stop is not None:
            heartbeat_stop.set()
        if heartbeat is not None:
            heartbeat.join(timeout = 1)
        try:
            with _locked_file(parent / ".cleanup.lock") as cleanup_lock:
                if not cleanup_lock:  # The blocking acquisition should always succeed.
                    raise RuntimeError(f"Could not lock ephemeral session root: {parent}")
                # Release the live marker only after deletion is serialized with
                # startup scavenging, so no scanner can race this rmtree.
                active_lock.close()
                if path is not None:
                    shutil.rmtree(path, ignore_errors = True)
        finally:
            active_lock.close()


@contextlib.contextmanager
def _session_config(
    agent: str,
    launch: bool,
    persist: bool = False,
):
    """Yield a private directory for an agent's session config (never the user's own).

    launch (default): an ephemeral temp dir removed after the agent process exits, so
    nothing persists. no-launch: a stable Unsloth-owned dir (the printed recipe is run
    later on this machine), reused across runs. persist (from --persist): use that same
    stable dir even for a launch, so the agent's session survives the exit and can be
    resumed next time. Either way the user's real ~/.<agent> config is left untouched.
    """
    if launch and not persist:
        # Windows codex keeps #7519's short home (MAX_PATH); everyone else uses Studio's root.
        parent = _ephemeral_session_parent(agent)
        prefix = _ephemeral_session_prefix(agent, parent)
        if parent is not None:
            with _short_ephemeral_session(parent, prefix) as path:
                yield path
        else:
            with _temporary_agent_config(prefix) as path:
                yield path
    else:
        # Never wipe this dir: a previously printed recipe may still be running
        # an agent whose sessions/state live here, and every config writer
        # merges idempotently into an existing home anyway. Writers must also
        # reset any state a previous run's flags left behind (--yolo especially),
        # since files here outlive the invocation that wrote them.
        path = _agents_config_root() / agent
        path.mkdir(parents = True, exist_ok = True, mode = 0o700)
        yield path


def write_openclaw_config(
    base: str,
    key: str,
    model: dict,
    path: Path,
    yolo: bool = False,
    workspace_path: Optional[str] = None,
) -> None:
    config = _read_json_object(path)
    if config is None:
        typer.echo(
            f"Warning: couldn't parse {path} — add an 'unsloth' provider there "
            "yourself, or move the file aside and re-run.",
            err = True,
        )
        return
    before = json.dumps(config, sort_keys = True)
    # Unsloth is a generic OpenAI-compatible /v1 endpoint (the vLLM/LM Studio path).
    provider_model = {"id": model["id"], "name": model["id"]}
    window = model.get("context_length") or model.get("max_context_length")
    if window:
        provider_model["contextWindow"] = int(window)
    models = _subdict(config, "models")
    models.setdefault("mode", "merge")
    _subdict(models, "providers")["unsloth"] = {
        "baseUrl": f"{base}/v1",
        "apiKey": key,
        "api": "openai-completions",
        "models": [provider_model],
    }
    # Pin a default model, else OpenClaw drops into its setup agent ("no models available").
    agents = _subdict(config, "agents")
    defaults = _subdict(agents, "defaults")
    _subdict(defaults, "model")["primary"] = f"unsloth/{model['id']}"
    # `unsloth start openclaw` is a coding-agent entry point, so the selected
    # project may already contain its own AGENTS.md and git metadata. Do not seed
    # OpenClaw's personal-assistant bootstrap files or initialize a repository in it.
    defaults["skipBootstrap"] = True
    # OPENCLAW_STATE_DIR does not relocate the workspace. Callers normally pin it to
    # the directory where `unsloth start openclaw` was invoked so OpenClaw edits the
    # same project as every other coding agent. Keep the managed fallback for direct
    # config-writer callers that do not provide an explicit workspace.
    if workspace_path is None:
        workspace = path.parent / "workspace"
        workspace.mkdir(parents = True, exist_ok = True, mode = 0o700)
        workspace_path = str(workspace)
    defaults["workspace"] = workspace_path
    # Per-agent paths override agents.defaults.workspace and OPENCLAW_STATE_DIR. This
    # config is itself an isolated Unsloth copy, so remove stale explicit paths and let
    # OpenClaw resolve every listed agent beneath the managed defaults/state directory.
    agent_list = agents.get("list")
    if isinstance(agent_list, list):
        for agent_config in agent_list:
            if isinstance(agent_config, dict):
                agent_config.pop("workspace", None)
                agent_config.pop("agentDir", None)
    # Unauthenticated loopback gateway: without auth.mode=none the client won't open
    # the websocket. The daemon must still be started separately (`openclaw gateway`).
    gateway = _subdict(config, "gateway")
    gateway.setdefault("mode", "local")
    _subdict(gateway, "auth").setdefault("mode", "none")
    if yolo:
        # OpenClaw has no --yolo flag, and it gates tool execution on BOTH the
        # tools.exec config AND a host-local approvals file (the stricter wins), so
        # setting only the config still lets the agent prompt/deny. Set both, mirroring
        # `openclaw exec-policy preset yolo`.
        exec_policy = _subdict(_subdict(config, "tools"), "exec")
        exec_policy["host"] = "gateway"
        exec_policy["security"] = "full"
        exec_policy["ask"] = "off"
        # Approvals file in OPENCLAW_STATE_DIR (== this config's dir). ask=off means
        # nothing is ever prompted, so the runtime socket block is unnecessary here.
        approvals = path.parent / "exec-approvals.json"
        _write_private_json(
            approvals,
            {"version": 1, "defaults": {"security": "full", "ask": "off", "askFallback": "full"}},
        )
        typer.echo(f"Updated {approvals}")
    else:
        # The no-launch config dir is reused across runs, so a previous --yolo run may
        # have left auto-approval state behind. OpenClaw treats an omitted exec policy as
        # security=full, ask=off on the gateway host, so deleting the keys would keep
        # auto-approval on: a non-yolo run must WRITE a prompting policy. Only a
        # permissive/yolo policy is replaced; a stricter one set by hand survives.
        tools = config.get("tools")
        exec_policy = tools.get("exec") if isinstance(tools, dict) else None
        exec_policy = exec_policy if isinstance(exec_policy, dict) else {}
        # Match ONLY the exact fingerprint --yolo writes (host=gateway, security=full,
        # ask=off, all explicit, no mode); anything else is left untouched. host=auto or an
        # omitted host resolves to security=deny under an active sandbox, so treating those
        # as the permissive gateway default would broaden a fresh sandboxed config from
        # deny to allowlist. host=node and host=sandbox are user-set (--yolo only writes
        # gateway). tools.exec.mode is OpenClaw's normalized knob (it cannot be combined
        # with security/ask, and OpenClaw never rewrites our security/ask write into it),
        # so a mode is always a deliberate user policy; never clobber it.
        permissive = (
            "mode" not in exec_policy
            and exec_policy.get("host") == "gateway"
            and exec_policy.get("security") == "full"
            and exec_policy.get("ask") == "off"
        )
        if permissive:
            exec_policy = _subdict(_subdict(config, "tools"), "exec")
            exec_policy.pop("host", None)  # routing only; defaults to the gateway host
            exec_policy["security"] = "allowlist"  # only allowlisted commands skip approval
            exec_policy["ask"] = "on-miss"  # prompt on every non-allowlisted command
        # Drop the yolo defaults from the host approvals file (a stricter default set by
        # the user or OpenClaw is kept). With a prompting tools.exec the stricter of the
        # two layers wins, so an omitted approvals default still prompts.
        approvals = path.parent / "exec-approvals.json"
        if approvals.exists():
            state = _read_json_object(approvals)
            if state is not None:
                defaults = state.get("defaults")
                # Strip the defaults only when they are exactly the yolo fingerprint; a
                # user-managed mixed policy that merely shares a field (e.g. askFallback=full,
                # whose omitted default is deny) must be kept intact.
                yolo_defaults = (("security", "full"), ("ask", "off"), ("askFallback", "full"))
                is_yolo = isinstance(defaults, dict) and all(
                    defaults.get(k) == v for k, v in yolo_defaults
                )
                if is_yolo:
                    for k, _ in yolo_defaults:
                        del defaults[k]
                    if not defaults:
                        del state["defaults"]
                    if set(state) <= {"version"}:
                        # Nothing left but our own yolo payload: remove it.
                        approvals.unlink()
                        typer.echo(f"Removed {approvals}")
                    else:
                        # Keep approvals OpenClaw itself recorded; only the yolo defaults go.
                        _write_private_json(approvals, state)
                        typer.echo(f"Updated {approvals}")
    if json.dumps(config, sort_keys = True) != before:
        _write_private_json(path, config)
        typer.echo(f"Updated {path}")


def write_opencode_config(
    base: str,
    key: str,
    model: dict,
    path: Path,
    yolo: bool = False,
    as_subagent: bool = False,
) -> dict:
    config = _read_json_object(path)
    if config is None:
        typer.echo(
            f"Warning: couldn't parse {path} — add an '{_OPENCODE_PROVIDER}' provider "
            "there yourself, or move the file aside and re-run.",
            err = True,
        )
        return {}
    before = json.dumps(config, sort_keys = True)
    config.setdefault("$schema", "https://opencode.ai/config.json")
    # Keep the provider definition in this private session file. The launch path
    # adjusts effective provider filters in the higher-priority inline overlay.
    model_entry = {"name": model["id"]}
    window = model.get("context_length") or model.get("max_context_length")
    if window:
        window = int(window)
        # A custom-provider model with no limit defaults to context 0, which silently
        # disables OpenCode's auto-compaction; declare the real window (and a sane
        # output cap) so it compacts instead of overflowing the server.
        model_entry["limit"] = {"context": window, "output": min(window // 4, 8192)}
    _subdict(config, "provider")[_OPENCODE_PROVIDER] = {
        "npm": "@ai-sdk/openai-compatible",
        "name": "Unsloth Studio",
        "options": {"baseURL": f"{base}/v1", "apiKey": key},
        "models": {model["id"]: model_entry},
    }
    # Normal mode pins this as the session model. Subagent mode leaves the user's
    # main/small models alone and exposes the local model to @unsloth and /models.
    opencode_model = f"{_OPENCODE_PROVIDER}/{model['id']}"
    if as_subagent:
        for field in ("model", "small_model"):
            if str(config.get(field) or "").startswith(f"{_OPENCODE_PROVIDER}/"):
                config.pop(field, None)
        managed_compaction = {"auto": True, "reserved": max(1, window // 10)} if window else None
        if managed_compaction and config.get("compaction") == managed_compaction:
            config.pop("compaction", None)
        _subdict(config, "agent")[_SUBAGENT_NAME] = {
            "description": _SUBAGENT_DESCRIPTION,
            "mode": "subagent",
            "model": opencode_model,
            "prompt": _SUBAGENT_INSTRUCTIONS,
        }
    else:
        config["model"] = opencode_model
        agents = config.get("agent")
        if isinstance(agents, dict):
            agents.pop(_SUBAGENT_NAME, None)
            if not agents:
                config.pop("agent", None)
    if window and not as_subagent:
        # Compact with ~10% headroom (near 90% full). The fixed 20k-token default
        # buffer over-compacts, or never settles, on a small local context.
        compaction = _subdict(config, "compaction")
        compaction["auto"] = True
        compaction["reserved"] = max(1, window // 10)
    tools = ("edit", "bash", "webfetch", *(("task",) if as_subagent else ()))
    if yolo:
        # Fallback for commands without native --auto and for the append-safe bare
        # --no-launch command (subcommand unknown yet). Rides inline (OPENCODE_CONFIG_CONTENT)
        # so it wins over a project config. TUI and `run` launches use --auto and call here
        # with yolo=False, letting OpenCode preserve explicit deny rules.
        session_permission = {t: "allow" for t in tools}
        session_permission["external_directory"] = {"*": "allow"}
        config["permission"] = dict(session_permission)
    else:
        # Undo only what --yolo wrote: our yolo sets an explicit per-tool "allow" for these
        # three tools, so flip exactly those explicit allows back to "ask". A "deny"/"ask",
        # a granular object, a string, or a "*" catch-all is the user's own rule and is left
        # untouched. We do NOT carry a permission inline for a non-yolo session: since
        # OPENCODE_CONFIG_CONTENT outranks the project opencode.json we cannot read, any
        # value forced there would override the user's project rules (weakening a project
        # deny, or auto-approving through a granular object's permissive default). Clearing
        # our own persisted yolo state is the fix; the project's own permissions are honored.
        session_permission: dict = {}
        permission = config.get("permission")
        if isinstance(permission, dict):
            for tool in tools:
                if permission.get(tool) == "allow":
                    permission[tool] = "ask"
            if permission.get("external_directory") == {"*": "allow"}:
                permission["external_directory"] = {"*": "ask"}
    if json.dumps(config, sort_keys = True) != before:
        _write_private_json(path, config)
        typer.echo(f"Updated {path}")
    return session_permission


def write_hermes_config(base: str, model: dict, path: Path) -> None:
    import yaml

    config: dict = {}
    if path.exists():
        try:
            loaded = yaml.safe_load(path.read_text(encoding = "utf-8"))
        except (yaml.YAMLError, OSError):
            typer.echo(
                f"Warning: couldn't parse {path} — configure the custom endpoint "
                "there yourself, or move the file aside and re-run.",
                err = True,
            )
            return
        if isinstance(loaded, dict):
            config = loaded
        elif loaded is not None:
            # Non-empty, non-mapping YAML is a user-managed file; leave it.
            typer.echo(
                f"Warning: couldn't parse {path} — configure the custom endpoint "
                "there yourself, or move the file aside and re-run.",
                err = True,
            )
            return
    # Hermes only reads the key for a *named* custom provider (a bare
    # `provider: custom` ignores it), so register it under providers.*.
    _subdict(config, "model").update(
        provider = f"custom:{_HERMES_PROVIDER}",
        default = model["id"],
        api_mode = "openai",
    )
    window = model.get("context_length") or model.get("max_context_length")
    if window:
        window = int(window)
        # Hermes auto-detects context from GET /v1/models, but OpenAI's schema has no
        # context field, so it can fall back to a 256k default that overflows a small
        # local model. Pin the real window (top-level model.context_length is the
        # highest-priority override) and compact at 90% of it (Hermes defaults to 50%).
        if window >= _HERMES_MIN_CONTEXT:
            _subdict(config, "model")["context_length"] = window
            _subdict(config, "compression").update(enabled = True, threshold = 0.9)
        else:
            # Below Hermes' 64,000-token floor it refuses to initialize, so claim
            # the floor and shrink the threshold so compaction still fires at 90%
            # of the REAL window (the threshold is a fraction of the claimed
            # context_length). The auxiliary override keeps the same floor check
            # from rejecting the compression model mid-session.
            _subdict(config, "model")["context_length"] = _HERMES_MIN_CONTEXT
            threshold = round(0.9 * window / _HERMES_MIN_CONTEXT, 4)
            _subdict(config, "compression").update(enabled = True, threshold = threshold)
            auxiliary = _subdict(_subdict(config, "auxiliary"), "compression")
            auxiliary["context_length"] = _HERMES_MIN_CONTEXT
    _subdict(config, "providers")[_HERMES_PROVIDER] = {
        "base_url": f"{base}/v1",
        "api_mode": "openai",
        "key_env": _HERMES_ENV_KEY,
    }
    text = yaml.safe_dump(config, sort_keys = False)
    if not path.exists() or path.read_text(encoding = "utf-8") != text:
        path.parent.mkdir(parents = True, exist_ok = True)
        path.write_text(text, encoding = "utf-8")
        typer.echo(f"Updated {path}")


def write_pi_config(base: str, key: str, model: dict, path: Path) -> None:
    config = _read_json_object(path)
    if config is None:
        typer.echo(
            f"Warning: couldn't parse {path} — add an 'unsloth' provider there "
            "yourself, or move the file aside and re-run.",
            err = True,
        )
        return
    before = json.dumps(config, sort_keys = True)
    # Pi reads custom providers from ~/.pi/agent/models.json (HOME-relocated for the
    # session). Unsloth is a generic OpenAI-compatible /v1 endpoint, and the key lives
    # in the config rather than the env (matching openclaw/opencode).
    provider_model = {"id": model["id"]}
    window = model.get("context_length") or model.get("max_context_length")
    if window:
        window = int(window)
        # An unspecified model defaults to contextWindow 128000 / maxTokens 16384,
        # far larger than a small Unsloth context, so Pi compacts too late and overflows
        # the server. Pin the real window and a sane output cap (mirrors OpenCode).
        provider_model["contextWindow"] = window
        provider_model["maxTokens"] = min(window // 4, 8192)
    _subdict(config, "providers")[_PI_PROVIDER] = {
        "api": "openai-completions",
        "baseUrl": f"{base}/v1",
        "apiKey": key,
        "models": [provider_model],
    }
    if json.dumps(config, sort_keys = True) != before:
        _write_private_json(path, config)
        typer.echo(f"Updated {path}")


def write_pi_subagent_config(
    base: str,
    key: str,
    model: dict,
    path: Path,
    approve: bool = False,
) -> None:
    """Write private bootstrap data for the bundled Pi extension."""
    window = model.get("context_length") or model.get("max_context_length")
    window = int(window) if window else 32768
    _write_private_json(
        path,
        {
            "baseUrl": f"{base}/v1",
            "apiKey": key,
            "model": model["id"],
            "contextWindow": window,
            "maxTokens": min(window // 4, 8192),
            "approve": approve,
        },
    )


@start_app.command("claude", cls = _PassthroughCommand, context_settings = _PASSTHROUGH)
def claude(
    ctx: typer.Context,
    model: Optional[str] = _MODEL_OPTION,
    api_key: Optional[str] = _KEY_OPTION,
    launch: bool = _LAUNCH_OPTION,
    gguf_variant: Optional[str] = _GGUF_VARIANT_OPTION,
    max_seq_length: int = _CONTEXT_OPTION,
    load_in_4bit: bool = _LOAD_4BIT_OPTION,
    tensor_parallel: bool = _TENSOR_PARALLEL_OPTION,
    gpu_memory_mode: Optional[Literal["auto", "manual"]] = _GPU_MEMORY_MODE_OPTION,
    enable_tools: bool = _ENABLE_TOOLS_OPTION,
    tool_call_healing: Optional[bool] = _TOOL_CALL_HEALING_OPTION,
    tool_call_nudging: Optional[bool] = _TOOL_CALL_NUDGING_OPTION,
    reasoning: Optional[Literal["on", "off", "auto"]] = _REASONING_OPTION,
    temperature: Optional[float] = _TEMPERATURE_OPTION,
    top_p: Optional[float] = _TOP_P_OPTION,
    top_k: Optional[int] = _TOP_K_OPTION,
    min_p: Optional[float] = _MIN_P_OPTION,
    repetition_penalty: Optional[float] = _REPETITION_PENALTY_OPTION,
    presence_penalty: Optional[float] = _PRESENCE_PENALTY_OPTION,
    serve: bool = _SERVE_OPTION,
    yolo: bool = _YOLO_OPTION,
    persist: bool = _PERSIST_OPTION,
    as_subagent: bool = _AS_SUBAGENT_OPTION,
):
    """Point Claude Code at the running Unsloth server and start it."""
    # Route a leading `org/name` positional to --model; forward the rest to the agent.
    model, ctx.args[:] = _consume_positional_model(model, ctx.args)
    install_hint = (
        "irm https://claude.ai/install.ps1 | iex"
        if os.name == "nt"
        else "curl -fsSL https://claude.ai/install.sh | bash"
    )
    _require_agent_for_launch("claude", install_hint, launch)
    base, key, entry = _connect(
        api_key,
        model,
        LoadOptions(gguf_variant, max_seq_length, load_in_4bit, tensor_parallel, gpu_memory_mode),
        serve = serve,
        launch = launch,
        server_options = ServerOptions(
            enable_tools = enable_tools,
            tool_call_healing = tool_call_healing,
            tool_call_nudging = tool_call_nudging,
            reasoning = reasoning,
            temperature = temperature,
            top_p = top_p,
            top_k = top_k,
            min_p = min_p,
            repetition_penalty = repetition_penalty,
            presence_penalty = presence_penalty,
        ),
    )
    model_id = entry["id"]
    if as_subagent:
        subagent_id = _subagent_model_id(base, key, entry, model, gguf_variant)
        subagent_model = {**entry, "id": subagent_id}
        window = subagent_model.get("context_length") or subagent_model.get("max_context_length")
        server_env = {
            "UNSLOTH_CLAUDE_SUBAGENT_BASE_URL": base,
            "UNSLOTH_CLAUDE_SUBAGENT_API_KEY": key,
            "UNSLOTH_CLAUDE_SUBAGENT_MODEL": subagent_id,
            "UNSLOTH_CLAUDE_SUBAGENT_BYPASS_PERMISSIONS": "1" if yolo else "0",
        }
        if window:
            server_env["UNSLOTH_CLAUDE_SUBAGENT_CONTEXT_WINDOW"] = str(int(window))
        with _session_config("claude-subagent", launch, persist = persist) as config:
            plugin = write_claude_subagent_plugin(config, server_env)
            command = [
                "claude",
                "--plugin-dir",
                _agent_config_path(plugin, ["claude"]),
                # Before ctx.args: a forwarded `--` would turn later flags positional.
                "--allowedTools",
                f"{_CLAUDE_SUBAGENT_TOOL},{_CLAUDE_SUBAGENT_PLAN_TOOL}",
                *_yolo_command_flags("claude", yolo),
                *ctx.args,
            ]
            typer.echo(
                "Unsloth is available as a local agent. "
                "Ask Claude to spawn an Unsloth or local agent."
            )
            _run(
                base,
                subagent_model,
                {},
                command,
                launch = launch,
                install_hint = install_hint,
            )
        return

    env = _claude_local_env(base, key, entry)
    # Claude Code auto-compacts against its native context window. The local env
    # above supplies the loaded model's real window and a 90% threshold instead.
    # --yolo (or its aliases) maps to Claude's own --dangerously-skip-permissions.
    # IS_SANDBOX is left unset on purpose: Claude refuses bypass mode as root unless a
    # sandbox is detected, and we don't want to falsely claim one on the user's host.
    # claude keeps its history in ~/.claude/projects, which --settings/env never
    # relocate, so a session already survives exit; resume it with `claude --continue`
    # or `--resume <id>` passed through.
    command = [
        "claude",
        "--model",
        model_id,
        *_claude_flags(model_id),
        *_yolo_command_flags("claude", yolo),
        *ctx.args,
    ]
    _run(
        base,
        entry,
        env,
        command,
        launch = launch,
        install_hint = install_hint,
        unset_env = _CLAUDE_ENV_UNSET,
    )


@start_app.command("codex", cls = _PassthroughCommand, context_settings = _PASSTHROUGH)
def codex(
    ctx: typer.Context,
    model: Optional[str] = _MODEL_OPTION,
    api_key: Optional[str] = _KEY_OPTION,
    launch: bool = _LAUNCH_OPTION,
    gguf_variant: Optional[str] = _GGUF_VARIANT_OPTION,
    max_seq_length: int = _CONTEXT_OPTION,
    load_in_4bit: bool = _LOAD_4BIT_OPTION,
    tensor_parallel: bool = _TENSOR_PARALLEL_OPTION,
    gpu_memory_mode: Optional[Literal["auto", "manual"]] = _GPU_MEMORY_MODE_OPTION,
    enable_tools: bool = _ENABLE_TOOLS_OPTION,
    tool_call_healing: Optional[bool] = _TOOL_CALL_HEALING_OPTION,
    tool_call_nudging: Optional[bool] = _TOOL_CALL_NUDGING_OPTION,
    reasoning: Optional[Literal["on", "off", "auto"]] = _REASONING_OPTION,
    temperature: Optional[float] = _TEMPERATURE_OPTION,
    top_p: Optional[float] = _TOP_P_OPTION,
    top_k: Optional[int] = _TOP_K_OPTION,
    min_p: Optional[float] = _MIN_P_OPTION,
    repetition_penalty: Optional[float] = _REPETITION_PENALTY_OPTION,
    presence_penalty: Optional[float] = _PRESENCE_PENALTY_OPTION,
    serve: bool = _SERVE_OPTION,
    yolo: bool = _YOLO_OPTION,
    persist: bool = _PERSIST_OPTION,
    as_subagent: bool = _AS_SUBAGENT_OPTION,
):
    """Point OpenAI Codex at the running Unsloth server and start it."""
    # Route a leading `org/name` positional to --model; forward the rest to the agent.
    model, ctx.args[:] = _consume_positional_model(model, ctx.args)
    install_hint = _npm_install_hint("@openai/codex")
    _require_agent_for_launch("codex", install_hint, launch)
    _preflight_codex_gguf(model, serve = serve, launch = launch)
    base, key, entry = _connect(
        api_key,
        model,
        LoadOptions(gguf_variant, max_seq_length, load_in_4bit, tensor_parallel, gpu_memory_mode),
        serve = serve,
        launch = launch,
        preload_check = _attach_gguf_check_for_codex,
        server_options = ServerOptions(
            enable_tools = enable_tools,
            tool_call_healing = tool_call_healing,
            tool_call_nudging = tool_call_nudging,
            reasoning = reasoning,
            temperature = temperature,
            top_p = top_p,
            top_k = top_k,
            min_p = min_p,
            repetition_penalty = repetition_penalty,
            presence_penalty = presence_penalty,
        ),
    )
    # This preflight runs after _connect may have auto-started a server but before _run
    # takes over its lifecycle, so tear the server down here if it rejects the model
    # (e.g. a transformers-backend model) rather than leaving it on the atexit backstop.
    try:
        _require_gguf_for_codex(base, key, entry["id"])
    except BaseException:
        _shutdown_auto_served()
        raise
    if as_subagent:
        subagent_id = _subagent_model_id(base, key, entry, model, gguf_variant)
        subagent_model = {**entry, "id": subagent_id}
        with _session_config("codex-subagent", launch, persist = persist) as home:
            bridge_config = write_codex_subagent_bridge(
                base,
                key,
                subagent_model,
                home,
                yolo = yolo,
            )
            parent_home = write_codex_parent_overlay(home / "parent")
            command = [
                "codex",
                *_codex_subagent_flags(bridge_config),
                *_yolo_command_flags("codex", yolo),
                *ctx.args,
            ]
            typer.echo(
                "Unsloth is available as a local agent. "
                "Ask Codex to spawn an Unsloth or local agent."
            )
            _run(
                base,
                subagent_model,
                {"CODEX_HOME": str(parent_home)},
                command,
                launch = launch,
                install_hint = install_hint,
            )
        return
    command = [
        "codex",
        "--oss",
        "--profile",
        _CODEX_PROFILE,
        *_yolo_command_flags("codex", yolo),
        *ctx.args,
    ]
    with _session_config("codex", launch, persist = persist) as home:
        write_codex_config(base, entry, home)
        env = {_CODEX_ENV_KEY: key, "CODEX_HOME": str(home)}
        _run(base, entry, env, command, launch = launch, install_hint = install_hint)


@start_app.command("openclaw", cls = _PassthroughCommand, context_settings = _PASSTHROUGH)
def openclaw(
    ctx: typer.Context,
    model: Optional[str] = _MODEL_OPTION,
    api_key: Optional[str] = _KEY_OPTION,
    launch: bool = _LAUNCH_OPTION,
    gguf_variant: Optional[str] = _GGUF_VARIANT_OPTION,
    max_seq_length: int = _CONTEXT_OPTION,
    load_in_4bit: bool = _LOAD_4BIT_OPTION,
    tensor_parallel: bool = _TENSOR_PARALLEL_OPTION,
    gpu_memory_mode: Optional[Literal["auto", "manual"]] = _GPU_MEMORY_MODE_OPTION,
    enable_tools: bool = _ENABLE_TOOLS_OPTION,
    tool_call_healing: Optional[bool] = _TOOL_CALL_HEALING_OPTION,
    tool_call_nudging: Optional[bool] = _TOOL_CALL_NUDGING_OPTION,
    reasoning: Optional[Literal["on", "off", "auto"]] = _REASONING_OPTION,
    temperature: Optional[float] = _TEMPERATURE_OPTION,
    top_p: Optional[float] = _TOP_P_OPTION,
    top_k: Optional[int] = _TOP_K_OPTION,
    min_p: Optional[float] = _MIN_P_OPTION,
    repetition_penalty: Optional[float] = _REPETITION_PENALTY_OPTION,
    presence_penalty: Optional[float] = _PRESENCE_PENALTY_OPTION,
    serve: bool = _SERVE_OPTION,
    yolo: bool = _YOLO_OPTION,
    persist: bool = _PERSIST_OPTION,
):
    """Point OpenClaw at the running Unsloth server and start it."""
    # Route a leading `org/name` positional to --model; forward the rest to the agent.
    model, ctx.args[:] = _consume_positional_model(model, ctx.args)
    _reject_as_subagent("openclaw", ctx.args)
    install_hint = (
        "iwr -useb https://openclaw.ai/install.ps1 | iex"
        if os.name == "nt"
        else "curl -fsSL https://openclaw.ai/install.sh | bash"
    )
    _require_agent_for_launch("openclaw", install_hint, launch)
    base, key, entry = _connect(
        api_key,
        model,
        LoadOptions(gguf_variant, max_seq_length, load_in_4bit, tensor_parallel, gpu_memory_mode),
        serve = serve,
        launch = launch,
        server_options = ServerOptions(
            enable_tools = enable_tools,
            tool_call_healing = tool_call_healing,
            tool_call_nudging = tool_call_nudging,
            reasoning = reasoning,
            temperature = temperature,
            top_p = top_p,
            top_k = top_k,
            min_p = min_p,
            repetition_penalty = repetition_penalty,
            presence_penalty = presence_penalty,
        ),
    )
    openclaw_args = list(ctx.args)
    # Default a bare `unsloth start openclaw` to the local TUI. Anything the caller
    # passes through is forwarded verbatim so OpenClaw parses it under its own grammar
    # (openclaw [global-flags] <command> [options]): an explicit subcommand, a global
    # flag that must precede the command such as --profile/--dev, or a tui option. We
    # cannot reinterpret those safely because a leading "--flag value" is ambiguous
    # between a global (`--profile test`) and a tui option (`--message hi`); prepending
    # `tui --local` would break the global form, so only the empty case is defaulted.
    if not openclaw_args:
        openclaw_args = ["tui", "--local"]
    command = ["openclaw", *openclaw_args]
    with _session_config("openclaw", launch, persist = persist) as cfg:
        config_path = cfg / "openclaw.json"
        # key lives in the config, not the env; --yolo writes the exec policy here too.
        # Resolve the project only when the recipe executes: a --no-launch command may be
        # generated in one directory, saved, and intentionally run later from another.
        write_openclaw_config(
            base,
            key,
            entry,
            config_path,
            yolo = yolo,
            workspace_path = "${OPENCLAW_WORKSPACE_DIR}",
        )
        # Scope both config and state so OpenClaw never touches the user's ~/.openclaw.
        env = {"OPENCLAW_CONFIG_PATH": str(config_path), "OPENCLAW_STATE_DIR": str(cfg)}
        _run(
            base,
            entry,
            env,
            command,
            launch = launch,
            install_hint = install_hint,
            cwd_env = ("OPENCLAW_WORKSPACE_DIR",),
        )


@start_app.command("opencode", cls = _PassthroughCommand, context_settings = _PASSTHROUGH)
def opencode(
    ctx: typer.Context,
    model: Optional[str] = _MODEL_OPTION,
    api_key: Optional[str] = _KEY_OPTION,
    launch: bool = _LAUNCH_OPTION,
    gguf_variant: Optional[str] = _GGUF_VARIANT_OPTION,
    max_seq_length: int = _CONTEXT_OPTION,
    load_in_4bit: bool = _LOAD_4BIT_OPTION,
    tensor_parallel: bool = _TENSOR_PARALLEL_OPTION,
    gpu_memory_mode: Optional[Literal["auto", "manual"]] = _GPU_MEMORY_MODE_OPTION,
    enable_tools: bool = _ENABLE_TOOLS_OPTION,
    tool_call_healing: Optional[bool] = _TOOL_CALL_HEALING_OPTION,
    tool_call_nudging: Optional[bool] = _TOOL_CALL_NUDGING_OPTION,
    reasoning: Optional[Literal["on", "off", "auto"]] = _REASONING_OPTION,
    temperature: Optional[float] = _TEMPERATURE_OPTION,
    top_p: Optional[float] = _TOP_P_OPTION,
    top_k: Optional[int] = _TOP_K_OPTION,
    min_p: Optional[float] = _MIN_P_OPTION,
    repetition_penalty: Optional[float] = _REPETITION_PENALTY_OPTION,
    presence_penalty: Optional[float] = _PRESENCE_PENALTY_OPTION,
    serve: bool = _SERVE_OPTION,
    yolo: bool = _YOLO_OPTION,
    persist: bool = _PERSIST_OPTION,
    as_subagent: bool = _AS_SUBAGENT_OPTION,
):
    """Point OpenCode at the running Unsloth server and start it."""
    # Route a leading `org/name` positional to --model; forward the rest to the agent.
    model, ctx.args[:] = _consume_positional_model(model, ctx.args)
    install_hint = _npm_install_hint("opencode-ai")
    _require_agent_for_launch("opencode", install_hint, launch)
    base, key, entry = _connect(
        api_key,
        model,
        LoadOptions(gguf_variant, max_seq_length, load_in_4bit, tensor_parallel, gpu_memory_mode),
        serve = serve,
        launch = launch,
        server_options = ServerOptions(
            enable_tools = enable_tools,
            tool_call_healing = tool_call_healing,
            tool_call_nudging = tool_call_nudging,
            reasoning = reasoning,
            temperature = temperature,
            top_p = top_p,
            top_k = top_k,
            min_p = min_p,
            repetition_penalty = repetition_penalty,
            presence_penalty = presence_penalty,
        ),
    )
    if as_subagent:
        subagent_id = _subagent_model_id(base, key, entry, model, gguf_variant)
        subagent_model = {**entry, "id": subagent_id}
        # Stay append-safe for a bare no-launch recipe: a later `run <prompt>` would make
        # `opencode --auto run ...` parse as the TUI, so keep yolo in the inline fallback.
        route_native_auto = yolo and _opencode_supports_native_auto() and (launch or bool(ctx.args))
        opencode_args, native_auto = _opencode_native_auto_args(list(ctx.args), route_native_auto)
        command = ["opencode", *opencode_args]
        with _session_config("opencode-subagent", launch, persist = persist) as cfg:
            config_path = cfg / "opencode.json"
            session_permission = write_opencode_config(
                base,
                key,
                subagent_model,
                config_path,
                yolo = yolo and not native_auto,
                as_subagent = True,
            )
            env = {"OPENCODE_CONFIG": str(config_path)}
            inline_config = _opencode_subagent_inline_config(config_path, session_permission)
            # A project opencode.json outranks the session file and could field-merge its
            # own agent.unsloth over ours. Pin ours in the inline overlay so it wins.
            inline_config.setdefault("agent", {})[_SUBAGENT_NAME] = {
                "description": _SUBAGENT_DESCRIPTION,
                "mode": "subagent",
                "model": f"{_OPENCODE_PROVIDER}/{subagent_model['id']}",
                "prompt": _SUBAGENT_INSTRUCTIONS,
            }
            env["OPENCODE_CONFIG_CONTENT"] = json.dumps(inline_config)
            typer.echo("Unsloth is available as @unsloth and in /models.")
            _run(
                base,
                subagent_model,
                env,
                command,
                launch = launch,
                install_hint = install_hint,
            )
        return
    opencode_model = f"{_OPENCODE_PROVIDER}/{entry['id']}"
    # The inline OPENCODE_CONFIG_CONTENT below pins the model in the highest-priority
    # layer, so the session model is forced without a --model flag. Only add --model for
    # an interactive bare launch (a convenience so the TUI opens on our model). It is
    # omitted for passthrough (inserting it before a subcommand can be misparsed) and for
    # --no-launch, where the printed command is consumed by drivers that append a
    # subcommand such as `run <prompt>`; a leading --model would land before that
    # subcommand and break it. Those paths rely on the inline pin instead.
    native_auto = False
    route_native_auto = yolo and _opencode_supports_native_auto()
    if ctx.args:
        opencode_args, native_auto = _opencode_native_auto_args(list(ctx.args), route_native_auto)
        command = ["opencode", *opencode_args]
    elif launch:
        opencode_args, native_auto = _opencode_native_auto_args(
            ["--model", opencode_model],
            route_native_auto,
        )
        command = ["opencode", *opencode_args]
    else:
        # Append-safe base: `opencode --auto run ...` parses as the TUI with a project
        # "run", not the run subcommand. Command unknown here, so keep the config fallback.
        command = ["opencode"]
    # opencode keeps sessions in ~/.local/share/opencode (never relocated), so resume
    # already survives exit; reopen the last one by passing `opencode --continue` through.
    with _session_config("opencode", launch, persist = persist) as cfg:
        config_path = cfg / "opencode.json"
        # OPENCODE_CONFIG is an overlay (loaded between the user's global and project
        # configs), so this adds the Unsloth provider/model for the session without
        # changing the user's default model. Key lives in the config, not the env.
        session_permission = write_opencode_config(
            base,
            key,
            entry,
            config_path,
            yolo = yolo and not native_auto,
        )
        # A project's own opencode.json outranks OPENCODE_CONFIG, so the session model pin
        # would silently lose to a repo config. Carry it in OPENCODE_CONFIG_CONTENT, which
        # outranks project config; the API key stays in the private file, never the env.
        # Only the config fallback carries a permission. Native --auto omits it (auto-approve
        # asks, keep explicit denies); a non-yolo session omits it too, honoring project rules.
        # opencode filters every provider (a config-defined custom one included) through
        # its enabled_providers allowlist and disabled_providers denylist, and a model pin
        # does not bypass that gate -- a filtered provider resolves to ModelNotFoundError.
        # To guarantee the session model loads without reading or modifying the user's real
        # config, scope THIS session to our provider alone: allowlist _OPENCODE_PROVIDER and
        # clear the denylist. These arrays are replaced (not merged) by higher layers, so
        # setting them in the highest-priority inline overlay neutralizes any user allowlist
        # or denylist for the launch. It is session-only: it lives in OPENCODE_CONFIG_CONTENT
        # for this invocation and never touches the user's config files, so their normal
        # `opencode` is unchanged; only this session is limited to the Unsloth provider.
        # small_model is opencode's separate model for lightweight tasks; pin it to the
        # session model too, or a user/project small_model on another (now filtered)
        # provider would resolve a not-found error mid-session. The session serves one
        # model, so the session model is the only valid target here anyway.
        inline_config: dict = {
            "model": opencode_model,
            "small_model": opencode_model,
            "enabled_providers": [_OPENCODE_PROVIDER],
            "disabled_providers": [],
        }
        if session_permission:
            inline_config["permission"] = session_permission
        env = {
            "OPENCODE_CONFIG": str(config_path),
            "OPENCODE_CONFIG_CONTENT": json.dumps(inline_config),
        }
        _run(base, entry, env, command, launch = launch, install_hint = install_hint)


@start_app.command("hermes", cls = _PassthroughCommand, context_settings = _PASSTHROUGH)
def hermes(
    ctx: typer.Context,
    model: Optional[str] = _MODEL_OPTION,
    api_key: Optional[str] = _KEY_OPTION,
    launch: bool = _LAUNCH_OPTION,
    gguf_variant: Optional[str] = _GGUF_VARIANT_OPTION,
    max_seq_length: int = _CONTEXT_OPTION,
    load_in_4bit: bool = _LOAD_4BIT_OPTION,
    tensor_parallel: bool = _TENSOR_PARALLEL_OPTION,
    gpu_memory_mode: Optional[Literal["auto", "manual"]] = _GPU_MEMORY_MODE_OPTION,
    enable_tools: bool = _ENABLE_TOOLS_OPTION,
    tool_call_healing: Optional[bool] = _TOOL_CALL_HEALING_OPTION,
    tool_call_nudging: Optional[bool] = _TOOL_CALL_NUDGING_OPTION,
    reasoning: Optional[Literal["on", "off", "auto"]] = _REASONING_OPTION,
    temperature: Optional[float] = _TEMPERATURE_OPTION,
    top_p: Optional[float] = _TOP_P_OPTION,
    top_k: Optional[int] = _TOP_K_OPTION,
    min_p: Optional[float] = _MIN_P_OPTION,
    repetition_penalty: Optional[float] = _REPETITION_PENALTY_OPTION,
    presence_penalty: Optional[float] = _PRESENCE_PENALTY_OPTION,
    serve: bool = _SERVE_OPTION,
    yolo: bool = _YOLO_OPTION,
    persist: bool = _PERSIST_OPTION,
):
    """Point Hermes (Nous Research) at the running Unsloth server and start it."""
    # Route a leading `org/name` positional to --model; forward the rest to the agent.
    model, ctx.args[:] = _consume_positional_model(model, ctx.args)
    _reject_as_subagent("hermes", ctx.args)
    native_args = [*_yolo_command_flags("hermes", yolo), *ctx.args]
    command = ["hermes", *_hermes_resume_oneshot_args(native_args)]
    install_hint = _hermes_install_hint()
    _require_agent_for_launch("hermes", install_hint, launch)
    base, key, entry = _connect(
        api_key,
        model,
        LoadOptions(gguf_variant, max_seq_length, load_in_4bit, tensor_parallel, gpu_memory_mode),
        serve = serve,
        launch = launch,
        server_options = ServerOptions(
            enable_tools = enable_tools,
            tool_call_healing = tool_call_healing,
            tool_call_nudging = tool_call_nudging,
            reasoning = reasoning,
            temperature = temperature,
            top_p = top_p,
            top_k = top_k,
            min_p = min_p,
            repetition_penalty = repetition_penalty,
            presence_penalty = presence_penalty,
        ),
    )
    with _session_config("hermes", launch, persist = persist) as home:
        # HERMES_HOME relocates hermes' whole home dir (config.yaml, sessions, state)
        # like CODEX_HOME, so the user's ~/.hermes is left untouched for the session.
        write_hermes_config(base, entry, home / "config.yaml")
        env = {_HERMES_ENV_KEY: key, "HERMES_HOME": str(home)}
        _run(base, entry, env, command, launch = launch, install_hint = install_hint)


@start_app.command("pi", cls = _PassthroughCommand, context_settings = _PASSTHROUGH)
def pi(
    ctx: typer.Context,
    model: Optional[str] = _MODEL_OPTION,
    api_key: Optional[str] = _KEY_OPTION,
    launch: bool = _LAUNCH_OPTION,
    gguf_variant: Optional[str] = _GGUF_VARIANT_OPTION,
    max_seq_length: int = _CONTEXT_OPTION,
    load_in_4bit: bool = _LOAD_4BIT_OPTION,
    tensor_parallel: bool = _TENSOR_PARALLEL_OPTION,
    gpu_memory_mode: Optional[Literal["auto", "manual"]] = _GPU_MEMORY_MODE_OPTION,
    enable_tools: bool = _ENABLE_TOOLS_OPTION,
    tool_call_healing: Optional[bool] = _TOOL_CALL_HEALING_OPTION,
    tool_call_nudging: Optional[bool] = _TOOL_CALL_NUDGING_OPTION,
    reasoning: Optional[Literal["on", "off", "auto"]] = _REASONING_OPTION,
    temperature: Optional[float] = _TEMPERATURE_OPTION,
    top_p: Optional[float] = _TOP_P_OPTION,
    top_k: Optional[int] = _TOP_K_OPTION,
    min_p: Optional[float] = _MIN_P_OPTION,
    repetition_penalty: Optional[float] = _REPETITION_PENALTY_OPTION,
    presence_penalty: Optional[float] = _PRESENCE_PENALTY_OPTION,
    serve: bool = _SERVE_OPTION,
    yolo: bool = _YOLO_OPTION,
    persist: bool = _PERSIST_OPTION,
    as_subagent: bool = _AS_SUBAGENT_OPTION,
):
    """Point Pi (coding agent) at the running Unsloth server and start it."""
    # Route a leading `org/name` positional to --model; forward the rest to the agent.
    model, ctx.args[:] = _consume_positional_model(model, ctx.args)
    install_hint = _npm_install_hint(
        "@earendil-works/pi-coding-agent",
        ignore_scripts = True,
    )
    if as_subagent and not _PI_SUBAGENT_EXTENSION.is_file():
        _fail(f"Missing Pi subagent extension: {_PI_SUBAGENT_EXTENSION}")
    _require_agent_for_launch("pi", install_hint, launch)
    base, key, entry = _connect(
        api_key,
        model,
        LoadOptions(gguf_variant, max_seq_length, load_in_4bit, tensor_parallel, gpu_memory_mode),
        serve = serve,
        launch = launch,
        server_options = ServerOptions(
            enable_tools = enable_tools,
            tool_call_healing = tool_call_healing,
            tool_call_nudging = tool_call_nudging,
            reasoning = reasoning,
            temperature = temperature,
            top_p = top_p,
            top_k = top_k,
            min_p = min_p,
            repetition_penalty = repetition_penalty,
            presence_penalty = presence_penalty,
        ),
    )
    if as_subagent:
        subagent_id = _subagent_model_id(base, key, entry, model, gguf_variant)
        subagent_model = {**entry, "id": subagent_id}
        extension = _agent_config_path(_PI_SUBAGENT_EXTENSION, ["pi"])
        with _session_config("pi-subagent", launch, persist = persist) as config:
            config_path = config / "subagent.json"
            write_pi_subagent_config(
                base,
                key,
                subagent_model,
                config_path,
                approve = yolo,
            )
            command = [
                "pi",
                "--extension",
                extension,
                *_yolo_command_flags("pi", yolo),
                *ctx.args,
            ]
            typer.echo(
                "Unsloth is available as a local agent and in /model. "
                "Ask Pi to spawn an Unsloth or local agent."
            )
            _run(
                base,
                subagent_model,
                {"UNSLOTH_PI_SUBAGENT_CONFIG": str(config_path)},
                command,
                launch = launch,
                install_hint = install_hint,
                clear_screen = True,
            )
        return
    # Pi defaults to the google provider, so pin our provider/model on the command
    # line; the custom OpenAI-compatible endpoint itself is only configurable via
    # ~/.pi/agent/models.json.
    command = [
        "pi",
        "--provider",
        _PI_PROVIDER,
        "--model",
        entry["id"],
        *_yolo_command_flags("pi", yolo),
        *ctx.args,
    ]
    # --ignore-scripts matches Pi's documented install recipe (its README notes Pi needs
    # no install scripts), so accepting the prompt skips dependency lifecycle scripts.
    with _session_config("pi", launch, persist = persist) as home:
        # Pi resolves its config dir from PI_CODING_AGENT_DIR first (getAgentDir() prefers
        # it over $HOME/.pi/agent), so pin it at the session dir: an inherited
        # PI_CODING_AGENT_DIR in the user's shell would otherwise send Pi to their real
        # config and skip our provider/key. HOME is relocated too so any other ~/.pi paths
        # stay in the session. The key rides in the config rather than the env.
        pi_agent_dir = home / ".pi" / "agent"
        write_pi_config(base, key, entry, pi_agent_dir / "models.json")
        env = {"HOME": str(home), "PI_CODING_AGENT_DIR": str(pi_agent_dir)}
        if os.name == "nt" or os.environ.get("WSL_DISTRO_NAME"):
            # Node resolves ~/.pi via USERPROFILE (then HOMEDRIVE + HOMEPATH) on Windows,
            # not HOME. Set them whenever Pi may run as a Windows process: native Windows,
            # or a /mnt Windows shim launched from WSL (the WSLENV bridge then translates
            # the path). Otherwise the Windows process falls back to the user's real
            # %USERPROFILE%\.pi. splitdrive yields no drive off a POSIX path, so
            # HOMEDRIVE/HOMEPATH stay unset there.
            env["USERPROFILE"] = str(home)
            drive, tail = os.path.splitdrive(str(home))
            if drive:
                env["HOMEDRIVE"], env["HOMEPATH"] = drive, tail
        # Pi paints inline from the current cursor position (no alternate screen,
        # no clear on first render), so give it the clean screen it assumes.
        _run(
            base,
            entry,
            env,
            command,
            launch = launch,
            install_hint = install_hint,
            clear_screen = True,
        )
