#!/usr/bin/env python3
"""Codex binary discovery, generated homes, configuration, and process environment."""

from __future__ import annotations

import contextlib
import copy
import hashlib
import json
import os
import re
import shutil
import subprocess
import tempfile
from collections.abc import Iterable, Mapping, Sequence
from pathlib import Path
from typing import Any

from mmo_app_server import APP_SERVER_LIFECYCLE_TIMEOUT_SECONDS
from mmo_guidance import (
    PROFILE_SKILL_NAME,
    PROFILE_SKILL_RELATIVE_PATH,
    agent_guidance_relative_path,
)
from mmo_profiles import (
    agent_mcp_tool_names,
    builtin_auth_link_mode,
    control_targets,
    load_settings,
)
from mmo_profiles import (
    mcp_children as _mcp_children,
)
from mmo_profiles import (
    native_children as _native_children,
)
from mmo_profiles import (
    reachable_native_agent_ids as _reachable_native_agent_ids,
)
from mmo_schema import extract_json_document
from mmo_state import (
    ACTIVE_SESSION_STATUSES,
    root_mcp_token,
    session_dir,
)
from mmo_tool_mcp import (
    codex_tool_mcp_server_config,
    tool_mcp_environment_names,
    tool_mcp_http_environment_names,
)
from mmo_util import (
    atomic_write_json,
    atomic_write_text,
    config_root,
    filtered_environment,
    install_root,
    is_within,
    parse_env_file,
    read_json,
    sha256_file,
    state_root,
    toml_dumps,
    valid_http_header_value,
)

CODEX_MODEL_CATALOG_CACHE_SCHEMA = 2
_BUNDLED_CODEX_CATALOGS: dict[str, dict[str, Any]] = {}

GENERIC_CODEX_MODEL_INSTRUCTIONS = """You are Codex, a coding agent operating in the user's workspace. Follow the system, developer, project, and role instructions provided for this session. Use the available tools to inspect, modify, and validate the workspace. Continue until the assigned task is complete, keep changes within scope, and report results accurately."""

_REASONING_DESCRIPTIONS = {
    "minimal": "Minimal reasoning for simple deterministic work",
    "low": "Light reasoning for straightforward work",
    "medium": "Balanced reasoning depth and latency",
    "high": "Greater reasoning depth for complex work",
    "xhigh": "Extra-high reasoning depth for difficult work",
    "max": "Maximum reasoning depth for the hardest work",
    "ultra": "Maximum reasoning with model-native delegation",
}


def _auth_link(home: Path, provider: Mapping[str, Any], settings: Mapping[str, Any]) -> list[str]:
    if provider["driver"] != "codex_builtin" or provider.get("auth") != "chatgpt":
        return []
    mode = builtin_auth_link_mode(provider, settings)
    if mode == "none":
        return []
    base_home = Path(str(settings.get("base_codex_home", "~/.codex"))).expanduser().resolve()
    linked: list[str] = []
    # Codex 0.149's .credentials.json is MCP OAuth state, not ChatGPT login
    # state. Propagating it would expose unrelated remote-MCP credentials.
    for name in ("auth.json",):
        source = base_home / name
        target = home / name
        if not source.exists():
            continue
        if mode == "shared":
            if target.is_symlink() and target.resolve() == source.resolve():
                pass
            else:
                if target.exists() or target.is_symlink():
                    target.unlink()
                target.symlink_to(source)
        elif mode == "copy":
            if target.is_symlink():
                target.unlink()
            shutil.copy2(source, target)
            os.chmod(target, 0o600)
        linked.append(name)
    return linked


def _codex_catalog_cache_root() -> Path:
    root = state_root() / "codex-model-catalogs"
    root.mkdir(parents=True, exist_ok=True, mode=0o700)
    return root


def _resolved_codex_binary() -> Path | None:
    configured = _codex_binary()
    candidate = shutil.which(configured)
    if candidate:
        return Path(candidate).resolve()
    path = Path(configured).expanduser()
    if path.is_file() and os.access(path, os.X_OK):
        return path.resolve()
    return None


def require_codex_binary() -> Path:
    """Resolve the configured Codex executable or reject session admission."""

    binary = _resolved_codex_binary()
    if binary is None:
        raise RuntimeError(f"Codex binary not found or not executable: {_codex_binary()}")
    return binary


def _codex_binary_fingerprint(binary: Path) -> str:
    stat_result = binary.stat()
    payload = json.dumps(
        {
            "path": str(binary),
            "size": stat_result.st_size,
            "mtime_ns": stat_result.st_mtime_ns,
            "schema": CODEX_MODEL_CATALOG_CACHE_SCHEMA,
        },
        sort_keys=True,
        separators=(",", ":"),
        allow_nan=False,
    ).encode("utf-8")
    return hashlib.sha256(payload).hexdigest()[:24]


def _validated_codex_catalog(document: Any) -> dict[str, Any] | None:
    if not isinstance(document, Mapping):
        return None
    rows = document.get("models")
    if not isinstance(rows, list):
        return None
    models: list[dict[str, Any]] = []
    seen: set[str] = set()
    for row in rows:
        if not isinstance(row, Mapping):
            continue
        slug = row.get("slug") or row.get("id")
        if not isinstance(slug, str) or not slug or slug in seen:
            continue
        item = copy.deepcopy(dict(row))
        item["slug"] = slug
        models.append(item)
        seen.add(slug)
    if not models:
        return None
    return {"models": models}


def _load_codex_bundled_catalog(binary: Path | None = None) -> dict[str, Any]:
    """Return the exact model metadata bundled with the active Codex binary.

    A configured ``model_catalog_json`` is the startup catalog for that Codex
    process.  Mixed built-in/custom processes therefore need the native rows
    copied from the active binary before MMO appends snapshot-specific external
    bindings.  The cache key is tied to the binary path, size, and mtime.
    """

    binary = binary or _resolved_codex_binary()
    if binary is None:
        raise RuntimeError(f"Codex binary not found: {_codex_binary()}")
    fingerprint = _codex_binary_fingerprint(binary)
    memory_cached = _BUNDLED_CODEX_CATALOGS.get(fingerprint)
    if memory_cached is not None:
        return copy.deepcopy(memory_cached)

    cache = _codex_catalog_cache_root() / f"{fingerprint}.json"
    with contextlib.suppress(OSError, ValueError, json.JSONDecodeError):
        cached = _validated_codex_catalog(read_json(cache))
        if cached is not None:
            _BUNDLED_CODEX_CATALOGS[fingerprint] = copy.deepcopy(cached)
            return copy.deepcopy(cached)

    with tempfile.TemporaryDirectory(prefix="query-", dir=_codex_catalog_cache_root()) as temporary:
        clean_home = Path(temporary) / "codex-home"
        clean_home.mkdir(mode=0o700)
        result = subprocess.run(
            [str(binary), "debug", "models", "--bundled"],
            env=filtered_environment(
                extra={
                    "CODEX_HOME": str(clean_home),
                    "GIT_TERMINAL_PROMPT": "0",
                    "NO_COLOR": "1",
                }
            ),
            text=True,
            capture_output=True,
            timeout=45,
            check=False,
        )
    document, parse_error = extract_json_document(result.stdout)
    catalog = _validated_codex_catalog(document)
    if result.returncode != 0 or catalog is None:
        detail = result.stderr.strip() or parse_error or "no model rows returned"
        raise RuntimeError(
            "unable to read Codex's bundled model catalog with "
            f"'{binary} debug models --bundled': {detail[-2000:]}"
        )
    atomic_write_json(cache, catalog, 0o600)
    _BUNDLED_CODEX_CATALOGS[fingerprint] = copy.deepcopy(catalog)
    return copy.deepcopy(catalog)


def _process_drivers(resolved: Mapping[str, Any], agent_ids: Sequence[str]) -> set[str]:
    return {
        str(
            resolved["routes"][resolved["models"][resolved["agents"][agent_id]["model"]]["route"]][
                "driver"
            ]
        )
        for agent_id in agent_ids
    }


def _process_needs_model_catalog(resolved: Mapping[str, Any], agent_ids: Sequence[str]) -> bool:
    return any(driver != "codex_builtin" for driver in _process_drivers(resolved, agent_ids))


def _profile_needs_bundled_codex_catalog(resolved: Mapping[str, Any]) -> bool:
    """Whether any generated Codex process mixes native and external models."""

    for agent_id in resolved["agents"]:
        relevant = [agent_id, *_reachable_native_agent_ids(resolved, agent_id)]
        drivers = _process_drivers(resolved, relevant)
        if "codex_builtin" in drivers and any(driver != "codex_builtin" for driver in drivers):
            return True
    return False


def bundled_codex_catalog_for_profile(
    resolved: Mapping[str, Any], binary: Path
) -> dict[str, Any] | None:
    """Load native model rows only for a generated mixed-provider Codex process."""

    if not _profile_needs_bundled_codex_catalog(resolved):
        return None
    return _load_codex_bundled_catalog(binary)


def _reasoning_presets(model: Mapping[str, Any]) -> list[dict[str, str]]:
    result: list[dict[str, str]] = []
    for effort in model.get("reasoning_levels", []):
        value = str(effort)
        if value == "none":
            continue
        result.append(
            {
                "effort": value,
                "description": _REASONING_DESCRIPTIONS.get(value, f"{value} reasoning effort"),
            }
        )
    return result


def _catalog_apply_patch_tool_type(model: Mapping[str, Any]) -> str | None:
    """Return a value accepted by the active Codex ModelInfo schema.

    Current Codex releases expose only the ``freeform`` catalog variant.  The
    generated Codex process always speaks Responses to either the provider or
    Switchyard.  A compatible endpoint may support ordinary function tools but
    reject Responses custom tools, so only models advertising that narrower
    capability receive apply_patch.  Function tools such as shell and MCP stay
    available independently.
    """

    return "freeform" if bool(model.get("supports_custom_tools", True)) else None


def _generated_model_catalog_entry(
    *,
    model: Mapping[str, Any],
    binding_model: str,
    native_delegation: bool,
    visible: bool,
) -> dict[str, Any]:
    """Translate one resolved MMO model into Codex startup metadata.

    Codex selects context accounting, reasoning controls, patch-tool shape, and
    native-agent support from this row. Snapshot-specific generated bindings must therefore
    be exact catalog slugs rather than relying on unknown-model fallback metadata.

    The row targets the currently supported Codex ``model_messages`` structure.
    MMO generation 8 does not emit retired catalog fields as compatibility shims.
    """

    modalities = [
        str(item)
        for item in model.get("modalities", ["text"])
        if str(item) in {"text", "image", "audio"}
    ]
    if "text" not in modalities:
        modalities.insert(0, "text")
    reasoning = _reasoning_presets(model)
    supported = {item["effort"] for item in reasoning}
    configured_default = str(model.get("default_reasoning", "none"))
    default_reasoning = (
        configured_default
        if configured_default != "none" and configured_default in supported
        else None
    )
    context_window = int(model["context_window"])
    supports_reasoning = bool(model.get("supports_reasoning_summaries", False))
    patch_tool = _catalog_apply_patch_tool_type(model)
    instructions = GENERIC_CODEX_MODEL_INSTRUCTIONS
    entry: dict[str, Any] = {
        "slug": binding_model,
        "display_name": str(model.get("display_name") or model["upstream_id"]),
        "description": str(model.get("description") or "Codex MMO configured model"),
        "default_reasoning_level": default_reasoning,
        "supported_reasoning_levels": reasoning,
        "shell_type": ("shell_command" if model.get("tool_calling", True) else "disabled"),
        "visibility": "list" if visible else "hide",
        "supported_in_api": True,
        "priority": 90,
        "additional_speed_tiers": [],
        "service_tiers": [],
        "default_service_tier": None,
        "availability_nux": None,
        "upgrade": None,
        "model_messages": {
            "instructions_template": instructions,
            "instructions_variables": None,
            "approvals": None,
            "collaboration_modes": None,
            "auto_review": None,
            "permissions": None,
            "token_budget": None,
        },
        "include_skills_usage_instructions": True,
        "include_plugin_usage_instructions": False,
        "include_apps_usage_instructions": False,
        "supports_reasoning_summary_parameter": supports_reasoning,
        "default_reasoning_summary": "none",
        "support_verbosity": False,
        "default_verbosity": None,
        "apply_patch_tool_type": patch_tool,
        "web_search_tool_type": ("text_and_image" if "image" in modalities else "text"),
        "truncation_policy": {"mode": "bytes", "limit": 10_000},
        "supports_image_detail_original": "image" in modalities,
        "context_window": context_window,
        "max_context_window": context_window,
        "auto_compact_token_limit": None,
        "comp_hash": None,
        "effective_context_window_percent": 95,
        # Exact Codex 0.149 ModelInfo capability field.
        "supports_parallel_tool_calls": bool(model.get("parallel_tool_calls", False)),
        "experimental_supported_tools": [],
        "input_modalities": modalities,
        "supports_search_tool": False,
        "use_responses_lite": False,
        "auto_review_model_override": None,
        "model_specialty": None,
        "tool_mode": None,
    }
    if native_delegation:
        # V1 is the broadly compatible custom-agent surface. MMO separately
        # accounts for every delegated role in the profile snapshot and lineage.
        entry["multi_agent_version"] = "v1"
    else:
        entry["multi_agent_version"] = None
    return entry


def _write_process_model_catalog(
    *,
    home: Path,
    resolved: Mapping[str, Any],
    agent_ids: Sequence[str],
    bindings: Mapping[str, Mapping[str, Any]],
    bundled_catalog: Mapping[str, Any] | None,
) -> Path | None:
    if not _process_needs_model_catalog(resolved, agent_ids):
        return None

    rows: dict[str, dict[str, Any]] = {}
    if bundled_catalog is not None:
        for row in bundled_catalog.get("models", []):
            if isinstance(row, Mapping) and isinstance(row.get("slug"), str):
                rows[str(row["slug"])] = copy.deepcopy(dict(row))

    binding_drivers: dict[str, set[str]] = {}
    process_agent_id = agent_ids[0]
    grouped_agents: dict[str, list[str]] = {}
    for agent_id in agent_ids:
        agent = resolved["agents"][agent_id]
        model = resolved["models"][agent["model"]]
        provider = resolved["routes"][model["route"]]
        binding_model = str(bindings[agent_id]["model"])
        binding_drivers.setdefault(binding_model, set()).add(str(provider["driver"]))
        grouped_agents.setdefault(binding_model, []).append(agent_id)
    conflicts = {
        slug: sorted(drivers)
        for slug, drivers in binding_drivers.items()
        if "codex_builtin" in drivers and len(drivers) > 1
    }
    if conflicts:
        raise ValueError(
            "a native Codex process cannot safely use the same model slug through "
            "both the built-in and an external provider; use an MCP backend or a "
            f"Switchyard binding instead: {conflicts}"
        )

    for binding_model, members in grouped_agents.items():
        agent_id = members[0]
        agent = resolved["agents"][agent_id]
        model = resolved["models"][agent["model"]]
        provider = resolved["routes"][model["route"]]
        if provider["driver"] == "codex_builtin" and binding_model in rows:
            continue
        native_delegation = any(bool(_native_children(resolved, member)) for member in members)
        rows[binding_model] = _generated_model_catalog_entry(
            model=model,
            binding_model=binding_model,
            native_delegation=native_delegation,
            visible=process_agent_id in members,
        )

    catalog_path = home / "models.json"
    atomic_write_json(catalog_path, {"models": list(rows.values())}, 0o600)
    return catalog_path


def _codex_provider_config(
    snapshot: Mapping[str, Any],
    agent: Mapping[str, Any],
    gateway_base_url: str | None,
) -> tuple[str, str, dict[str, Any], list[str]]:
    resolved = snapshot["resolved"]
    model = resolved["models"][agent["model"]]
    provider = resolved["routes"][model["route"]]
    driver = provider["driver"]
    provider_tables: dict[str, Any] = {}
    command_flags: list[str] = []
    # Provider idleness is transport failure detection, not a task deadline.
    # Keep it well beyond the profile's warning-only stall interval so a slow
    # first token cannot erase an otherwise healthy durable goal.
    stall_window_ms = int(agent.get("stall_warning_seconds") or 1800) * 2000
    provider_idle_ms = int(provider.get("stream_idle_timeout_ms") or 0)
    stream_idle_timeout_ms = max(3_600_000, provider_idle_ms, stall_window_ms)
    if driver == "switchyard":
        if not gateway_base_url:
            raise RuntimeError("profile requires Switchyard but no gateway URL is available")
        provider_id = "mmo_switchyard"
        model_id = snapshot["manifest"]["route_ids"][agent["model"]]
        provider_tables[provider_id] = {
            "name": f"Codex MMO snapshot {snapshot['manifest']['snapshot_hash'][:12]}",
            "base_url": gateway_base_url,
            "wire_api": "responses",
            "requires_openai_auth": False,
            "request_max_retries": 1,
            "stream_max_retries": 1,
            "stream_idle_timeout_ms": stream_idle_timeout_ms,
        }
    elif driver == "codex_custom":
        provider_id = "mmo_" + re.sub(r"[^a-z0-9_]", "_", model["route"])
        model_id = model["upstream_id"]
        table: dict[str, Any] = {
            "name": provider["name"],
            "base_url": provider["base_url"],
            "wire_api": provider.get("wire_api", "responses"),
            "requires_openai_auth": False,
            "request_max_retries": provider["request_max_retries"],
            "stream_max_retries": provider["stream_max_retries"],
            "stream_idle_timeout_ms": stream_idle_timeout_ms,
        }
        credentials = list(provider.get("credential_envs", []))
        if credentials:
            table["env_key"] = credentials[0]
        if provider.get("http_headers"):
            table["http_headers"] = provider["http_headers"]
        if provider.get("env_http_headers"):
            table["env_http_headers"] = provider["env_http_headers"]
        provider_tables[provider_id] = table
    elif driver == "codex_builtin":
        provider_id = provider["provider_id"]
        model_id = model["upstream_id"]
    elif driver == "codex_oss":
        provider_id = provider["provider_id"]
        model_id = model["upstream_id"]
        command_flags = ["--oss", "--local-provider", provider_id]
    else:
        raise RuntimeError(f"unsupported provider driver: {driver}")
    return model_id, provider_id, provider_tables, command_flags


def _tool_mcp_server_ids(resolved: Mapping[str, Any], agent_ids: Sequence[str]) -> list[str]:
    return sorted(
        {
            server_id
            for agent_id in agent_ids
            for server_id in resolved["agents"][agent_id].get("tool_mcp_servers", {})
        }
    )


def _tool_mcp_server_configs(
    resolved: Mapping[str, Any],
    agent_id: str,
    server_ids: Sequence[str],
) -> dict[str, dict[str, Any]]:
    definitions = resolved.get("tool_mcp_servers", {})
    grants = resolved["agents"][agent_id].get("tool_mcp_servers", {})
    return {
        server_id: codex_tool_mcp_server_config(
            definitions[server_id],
            grants.get(server_id),
        )
        for server_id in server_ids
    }


def app_server_lifecycle_timeout(resolved: Mapping[str, Any], agent_id: str) -> float:
    """Cover every enabled Tool MCP startup plus bounded protocol overhead."""

    grants = resolved["agents"][agent_id].get("tool_mcp_servers", {})
    definitions = resolved.get("tool_mcp_servers", {})
    configured = [
        float(definitions[server_id].get("startup_timeout_sec") or 0) for server_id in grants
    ]
    return max(APP_SERVER_LIFECYCLE_TIMEOUT_SECONDS, max(configured, default=0.0) + 30.0)


def _mesh_tool_timeout(resolved: Mapping[str, Any], agent_id: str) -> float:
    """Keep the outer MCP call alive through every reachable control target."""

    agent = resolved["agents"][agent_id]
    target_ids = {
        agent_id,
        *_mcp_children(resolved, agent_id),
        *control_targets(agent),
    }
    return max(app_server_lifecycle_timeout(resolved, target) for target in target_ids) + 30.0


def _binding_bundle(
    snapshot: Mapping[str, Any],
    agent_ids: Sequence[str],
    gateway_base_url: str | None,
    *,
    process_agent_id: str,
) -> tuple[dict[str, dict[str, Any]], dict[str, Any], list[str]]:
    bindings: dict[str, dict[str, Any]] = {}
    providers: dict[str, Any] = {}
    process_flags: list[str] = []
    for agent_id in agent_ids:
        agent = snapshot["resolved"]["agents"][agent_id]
        model_id, provider_id, tables, flags = _codex_provider_config(
            snapshot, agent, gateway_base_url
        )
        if flags and agent_id != process_agent_id:
            raise ValueError(
                f"native agent {agent_id} uses Codex OSS mode, which cannot be selected "
                "inside a mixed-provider parent process; bind it through Switchyard instead"
            )
        if agent_id == process_agent_id:
            process_flags = flags
        for key, value in tables.items():
            existing = providers.get(key)
            if existing is not None and existing != value:
                raise RuntimeError(f"conflicting generated Codex provider table: {key}")
            providers[key] = value
        bindings[agent_id] = {
            "model": model_id,
            "model_provider": provider_id,
        }
    return bindings, providers, process_flags


def _agent_mcp_server_config(
    *,
    session_id: str,
    caller_agent: str,
    tool_names: Iterable[str],
    native_token: str | None = None,
    job_scoped: bool = False,
    enabled: bool = True,
    tool_timeout_sec: float,
) -> dict[str, Any]:
    tools = sorted(set(tool_names))
    enabled = bool(enabled and tools)
    env = {
        "MMO_INSTALL_ROOT": str(install_root()),
        "MMO_ROOT_SESSION_ID": session_id,
        "MMO_CALLER_AGENT": caller_agent,
    }
    if native_token is not None:
        env["MMO_CALLER_NATIVE"] = "1"
        env["MMO_NATIVE_CALLER_TOKEN"] = native_token
    forwarded_env = ["MMO_CALLER_TOKEN", "MMO_RUN_ID"]
    if job_scoped:
        forwarded_env.append("MMO_CALLER_JOB_ID")
    server: dict[str, Any] = {
        "command": str(install_root() / "libexec" / "mmo_mcp.py"),
        "args": [],
        "required": bool(enabled),
        "enabled": bool(enabled),
        "startup_timeout_sec": 10,
        # This covers the largest compiled lifecycle timeout reachable through
        # this caller's grants, plus protocol overhead. The MCP client must not
        # abandon a valid app-server control round trip first.
        "tool_timeout_sec": tool_timeout_sec,
        "default_tools_approval_mode": "approve",
        # Forward the process-scoped caller capability without serializing its
        # plaintext into generated config.toml.
        "env_vars": forwarded_env,
        "env": env,
    }
    if enabled:
        server["enabled_tools"] = tools
        server["tools"] = {name: {"approval_mode": "approve"} for name in tools}
    return server


def _base_codex_config(
    *,
    model: Mapping[str, Any],
    provider: Mapping[str, Any],
    agent: Mapping[str, Any],
    binding: Mapping[str, Any],
    providers: Mapping[str, Any],
) -> dict[str, Any]:
    config: dict[str, Any] = {
        "model": binding["model"],
        "model_provider": binding["model_provider"],
        "project_doc_max_bytes": 65536,
        "approval_policy": agent.get("approval_policy", "never"),
        "sandbox_mode": agent["permissions"],
        "web_search": agent.get("web_search", "disabled"),
        "check_for_update_on_startup": False,
        "model_providers": dict(providers),
        "features": {
            "shell_snapshot": True,
            "unified_exec": True,
        },
        "memories": {
            "generate_memories": False,
            "use_memories": False,
            "disable_on_external_context": True,
        },
        # App-server threads are the durable execution identity. Persist every
        # turn so a detached/restarted host can resume or fork exact context.
        "history": {"persistence": "save-all"},
        "shell_environment_policy": {
            "inherit": "core",
            "ignore_default_excludes": False,
        },
        "sandbox_workspace_write": {
            "network_access": bool(agent.get("network_access", False)),
            "exclude_slash_tmp": False,
            "exclude_tmpdir_env_var": False,
        },
    }
    # Built-in model metadata belongs to the active Codex catalog. Pinning a
    # static project baseline here can silently override newer account/client
    # limits and capabilities. External generated bindings still require explicit values.
    if provider["driver"] != "codex_builtin":
        config["model_context_window"] = int(model["context_window"])
    if agent.get("reasoning") != "none":
        config["model_reasoning_effort"] = agent["reasoning"]
    if agent.get("plan_reasoning") not in (None, "none"):
        config["plan_mode_reasoning_effort"] = agent["plan_reasoning"]
    return config


def _snapshot_guidance_text(snapshot: Mapping[str, Any], relative: str) -> str:
    directory = Path(str(snapshot["directory"]))
    target = directory / relative
    if target.is_symlink() or not target.is_file() or not is_within(target.resolve(), directory):
        raise RuntimeError(f"compiled guidance is missing or unsafe: {relative}")
    text = target.read_text(encoding="utf-8")
    expected_hash = snapshot["manifest"].get("payload_files", {}).get(relative)
    actual_hash = hashlib.sha256(text.encode("utf-8")).hexdigest()
    if not isinstance(expected_hash, str) or actual_hash != expected_hash:
        raise RuntimeError(f"compiled guidance failed integrity validation: {relative}")
    return text


def _skill_config(skill_path: Path, *, enabled: bool) -> dict[str, Any]:
    return {"config": [{"path": str(skill_path), "enabled": enabled}]}


def _native_agent_file(
    snapshot: Mapping[str, Any],
    agent_id: str,
    *,
    binding: Mapping[str, Any],
    session_id: str,
    native_token: str | None,
    tool_mcp_server_ids: Sequence[str],
    job_scoped: bool,
    skill_path: Path | None,
) -> dict[str, Any]:
    resolved = snapshot["resolved"]
    agent = resolved["agents"][agent_id]
    model = resolved["models"][agent["model"]]
    provider = resolved["routes"][model["route"]]
    config: dict[str, Any] = {
        "name": agent["native_name"],
        "description": agent.get("description") or f"Codex MMO role {agent_id}",
        "developer_instructions": _snapshot_guidance_text(
            snapshot, agent_guidance_relative_path(agent_id)
        ),
        "model": binding["model"],
        "model_provider": binding["model_provider"],
        "approval_policy": agent.get("approval_policy", "never"),
        "sandbox_mode": agent["permissions"],
        "web_search": agent.get("web_search", "disabled"),
        "sandbox_workspace_write": {
            "network_access": bool(agent.get("network_access", False)),
            "exclude_slash_tmp": False,
            "exclude_tmpdir_env_var": False,
        },
        "agents": {
            "enabled": bool(
                resolved["coordination"].get("native_nested_delegation", False)
                and _native_children(resolved, agent_id)
            ),
            "max_concurrent_threads_per_session": int(
                resolved["coordination"]["native_max_concurrent_threads"]
            ),
            "interrupt_message": bool(
                resolved["coordination"].get("native_interrupt_message", True)
            ),
        },
    }
    if provider["driver"] != "codex_builtin":
        config["model_context_window"] = int(model["context_window"])
        # Summary capability for external route rows is carried by the generated
        # ModelInfo row. Codex 0.149 has no ConfigToml field for that capability.
    if agent.get("reasoning") != "none":
        config["model_reasoning_effort"] = agent["reasoning"]
    if agent.get("plan_reasoning") not in (None, "none"):
        config["plan_mode_reasoning_effort"] = agent["plan_reasoning"]
    coordinates_mcp = bool(agent["can_spawn"] or agent.get("controls"))
    if skill_path is not None:
        config["skills"] = _skill_config(skill_path, enabled=coordinates_mcp)
    elif coordinates_mcp:
        raise RuntimeError(f"coordination-capable native agent {agent_id} has no compiled skill")
    mcp_servers = _tool_mcp_server_configs(
        resolved,
        agent_id,
        tool_mcp_server_ids,
    )
    mcp_enabled = bool(_mcp_children(resolved, agent_id) or agent.get("controls"))
    # Override an inherited root MCP identity. A generated per-role token lets
    # the supervisor recognize a native caller without trusting model-provided
    # arguments.
    if snapshot["resolved"]["coordination"]["orchestration"] == "hybrid":
        mesh_tools = agent_mcp_tool_names(
            resolved["agents"],
            root_agent=str(resolved["profile"]["root"]),
            agent_id=agent_id,
        )
        mcp_servers["mmo_mesh"] = _agent_mcp_server_config(
            session_id=session_id,
            caller_agent=agent_id,
            tool_names=mesh_tools,
            native_token=native_token if mcp_enabled else None,
            job_scoped=bool(job_scoped and mcp_enabled),
            enabled=mcp_enabled,
            tool_timeout_sec=_mesh_tool_timeout(resolved, agent_id),
        )
    if mcp_servers:
        config["mcp_servers"] = mcp_servers
    return config


def _render_codex_config(
    snapshot: Mapping[str, Any],
    agent_id: str,
    *,
    home: Path,
    gateway_base_url: str | None,
    session_id: str,
    native_tokens: Mapping[str, str],
    bundled_catalog: Mapping[str, Any] | None,
    availability: Mapping[str, Mapping[str, Any]],
    skill_path: Path | None,
    disable_native_delegation: bool = False,
) -> tuple[str, list[str], dict[str, str]]:
    resolved = snapshot["resolved"]
    agent = resolved["agents"][agent_id]
    model = resolved["models"][agent["model"]]
    provider = resolved["routes"][model["route"]]
    native_ids = [
        child_id
        for child_id in _reachable_native_agent_ids(resolved, agent_id)
        if availability.get(resolved["agents"][child_id]["route"], {}).get("available")
    ]
    bindings, providers, command_flags = _binding_bundle(
        snapshot,
        [agent_id, *native_ids],
        gateway_base_url,
        process_agent_id=agent_id,
    )
    config = _base_codex_config(
        model=model,
        provider=provider,
        agent=agent,
        binding=bindings[agent_id],
        providers=providers,
    )
    if agent["can_spawn"] or agent.get("controls"):
        if skill_path is None:
            raise RuntimeError(f"coordination-capable agent {agent_id} has no compiled skill")
        config["skills"] = _skill_config(skill_path, enabled=True)
    relevant_ids = [agent_id, *native_ids]
    tool_mcp_server_ids = _tool_mcp_server_ids(resolved, relevant_ids)
    model_catalog = _write_process_model_catalog(
        home=home,
        resolved=resolved,
        agent_ids=relevant_ids,
        bindings=bindings,
        bundled_catalog=bundled_catalog,
    )
    if model_catalog is not None:
        config["model_catalog_json"] = str(model_catalog)
    if any(
        resolved["routes"][resolved["models"][resolved["agents"][item]["model"]]["route"]]["driver"]
        == "codex_builtin"
        for item in relevant_ids
    ):
        # Generated homes receive file-backed auth by link or explicit copy.
        # Codex keyring entries are keyed to canonical CODEX_HOME, so `auto`
        # would look in a different namespace and could migrate a shared file
        # into session-local keyring state during token refresh.
        config["cli_auth_credentials_store"] = "file"

    native_children = [] if disable_native_delegation else _native_children(resolved, agent_id)
    native_files: dict[str, str] = {}
    if native_ids:
        agents_table: dict[str, Any] = {
            "enabled": bool(native_children),
            "max_concurrent_threads_per_session": int(
                resolved["coordination"]["native_max_concurrent_threads"]
            ),
            "interrupt_message": bool(
                resolved["coordination"].get("native_interrupt_message", True)
            ),
        }
        agents_directory = home / "agents"
        agents_directory.mkdir(parents=True, exist_ok=True, mode=0o700)
        for child_id in native_ids:
            child = resolved["agents"][child_id]
            path = agents_directory / f"{child['native_name']}.toml"
            native_files[child_id] = str(path)
            agents_table[child["native_name"]] = {
                "description": child.get("description") or f"Codex MMO role {child_id}",
                "config_file": str(path),
            }
            child_config = _native_agent_file(
                snapshot,
                child_id,
                binding=bindings[child_id],
                session_id=session_id,
                native_token=native_tokens.get(child_id),
                tool_mcp_server_ids=tool_mcp_server_ids,
                job_scoped=bool(
                    agent_id != resolved["profile"]["root"] and "mcp" in agent.get("backends", [])
                ),
                skill_path=skill_path,
            )
            atomic_write_text(path, toml_dumps(child_config), 0o600)
        config["agents"] = agents_table
        config["features"]["multi_agent"] = bool(native_children)
    else:
        config["agents"] = {"enabled": False}
        config["features"]["multi_agent"] = False

    mcp_servers = _tool_mcp_server_configs(
        resolved,
        agent_id,
        tool_mcp_server_ids,
    )
    management_enabled = bool(_mcp_children(resolved, agent_id) or agent.get("controls"))
    control_enabled = bool(agent.get("controls"))
    is_mcp_worker = bool(
        agent_id != resolved["profile"]["root"] and "mcp" in agent.get("backends", [])
    )
    if management_enabled or control_enabled:
        mesh_tools = agent_mcp_tool_names(
            resolved["agents"],
            root_agent=str(resolved["profile"]["root"]),
            agent_id=agent_id,
        )
        mcp_servers["mmo_mesh"] = _agent_mcp_server_config(
            session_id=session_id,
            caller_agent=agent_id,
            tool_names=mesh_tools,
            job_scoped=is_mcp_worker,
            enabled=True,
            tool_timeout_sec=_mesh_tool_timeout(resolved, agent_id),
        )
    if mcp_servers:
        config["mcp_servers"] = mcp_servers
    return toml_dumps(config), command_flags, native_files


def materialize_agent_home(
    session_directory: Path,
    snapshot: Mapping[str, Any],
    agent_id: str,
    gateway_base_url: str | None,
    *,
    session_id: str,
    native_tokens: Mapping[str, str],
    bundled_catalog: Mapping[str, Any] | None,
    availability: Mapping[str, Mapping[str, Any]],
    disable_native_delegation: bool = False,
) -> dict[str, Any]:
    home = session_directory / "codex-home" / agent_id
    home.mkdir(parents=True, exist_ok=True, mode=0o700)
    resolved = snapshot["resolved"]
    agent = resolved["agents"][agent_id]
    skill_path: Path | None = None
    if agent["can_spawn"] or agent.get("controls"):
        skill_directory = home / "skills" / PROFILE_SKILL_NAME
        skill_directory.mkdir(parents=True, exist_ok=True, mode=0o700)
        skill_path = skill_directory / "SKILL.md"
        atomic_write_text(
            skill_path,
            _snapshot_guidance_text(snapshot, PROFILE_SKILL_RELATIVE_PATH),
            0o600,
        )
    config_text, command_flags, native_files = _render_codex_config(
        snapshot,
        agent_id,
        home=home,
        gateway_base_url=gateway_base_url,
        session_id=session_id,
        native_tokens=native_tokens,
        bundled_catalog=bundled_catalog,
        availability=availability,
        skill_path=skill_path,
        disable_native_delegation=disable_native_delegation,
    )
    atomic_write_text(home / "config.toml", config_text, 0o600)
    atomic_write_text(
        home / "AGENTS.md",
        _snapshot_guidance_text(snapshot, agent_guidance_relative_path(agent_id)),
        0o600,
    )
    relevant_agent_ids = [agent_id, *_reachable_native_agent_ids(resolved, agent_id)]
    linked_auth: list[str] = []
    for relevant_id in relevant_agent_ids:
        relevant = resolved["agents"][relevant_id]
        relevant_model = resolved["models"][relevant["model"]]
        relevant_provider = resolved["routes"][relevant_model["route"]]
        if relevant_provider["driver"] == "codex_builtin":
            linked_auth = _auth_link(home, relevant_provider, load_settings())
            break
    direct_credential_groups: list[list[str]] = []
    direct_header_envs: list[str] = []
    seen_groups: set[tuple[str, ...]] = set()
    seen_header_envs: set[str] = set()
    for relevant_id in relevant_agent_ids:
        relevant = resolved["agents"][relevant_id]
        relevant_model = resolved["models"][relevant["model"]]
        relevant_provider = resolved["routes"][relevant_model["route"]]
        if relevant_provider["driver"] != "codex_custom":
            continue
        group = tuple(relevant_provider.get("credential_envs", []))
        if group and group not in seen_groups:
            seen_groups.add(group)
            direct_credential_groups.append(list(group))
        for env_name in relevant_provider.get("env_http_headers", {}).values():
            if env_name not in seen_header_envs:
                seen_header_envs.add(env_name)
                direct_header_envs.append(env_name)
    tool_mcp_envs = sorted(
        {
            env_name
            for server_id in _tool_mcp_server_ids(resolved, relevant_agent_ids)
            for env_name in tool_mcp_environment_names(
                resolved.get("tool_mcp_servers", {})[server_id]
            )
        }
    )
    tool_mcp_http_envs = sorted(
        {
            env_name
            for server_id in _tool_mcp_server_ids(resolved, relevant_agent_ids)
            for env_name in tool_mcp_http_environment_names(
                resolved.get("tool_mcp_servers", {})[server_id]
            )
        }
    )
    model = resolved["models"][agent["model"]]
    provider = resolved["routes"][model["route"]]
    model_catalog_path = home / "models.json"
    return {
        "home": str(home),
        "command_flags": command_flags,
        "linked_auth": linked_auth,
        "model": model["upstream_id"],
        "route": model["route"],
        "driver": provider["driver"],
        "credential_envs": list(provider.get("credential_envs", [])),
        "direct_credential_groups": direct_credential_groups,
        "direct_header_envs": direct_header_envs,
        "tool_mcp_envs": tool_mcp_envs,
        "tool_mcp_http_envs": tool_mcp_http_envs,
        "native_agent_files": native_files,
        "orchestration_skill": str(skill_path) if skill_path is not None else None,
        "orchestration_skill_sha256": sha256_file(skill_path) if skill_path is not None else None,
        "model_catalog_json": str(model_catalog_path) if model_catalog_path.is_file() else None,
        "model_catalog_sha256": sha256_file(model_catalog_path)
        if model_catalog_path.is_file()
        else None,
    }


def _persistent_agent_home(
    directory: Path,
    session: Mapping[str, Any],
    agent_id: str,
) -> Path:
    codex_home_root = directory / "codex-home"
    expected_raw = codex_home_root / agent_id
    if codex_home_root.is_symlink() or expected_raw.is_symlink():
        raise RuntimeError(f"persistent Codex home cannot be a symlink for {agent_id}")
    expected = expected_raw.resolve()
    recorded = Path(str(session["homes"][agent_id]["home"])).expanduser().resolve()
    if (
        recorded != expected
        or not expected.is_dir()
        or not is_within(expected, directory.resolve())
    ):
        raise RuntimeError(f"persistent Codex home is missing or invalid for {agent_id}")
    return expected


def refresh_session_homes(
    session: Mapping[str, Any],
    snapshot: Mapping[str, Any],
    *,
    gateway_base_url: str | None,
    availability: Mapping[str, Mapping[str, Any]],
    native_tokens: Mapping[str, str],
) -> dict[str, Any]:
    directory = session_dir(str(session["session_id"]))
    homes: dict[str, Any] = {}
    resolved = snapshot["resolved"]
    for agent_id in resolved["agents"]:
        expected_home = _persistent_agent_home(directory, session, agent_id)
        relevant = [agent_id, *_reachable_native_agent_ids(resolved, agent_id)]
        drivers = _process_drivers(resolved, relevant)
        bundled_catalog: Mapping[str, Any] | None = None
        catalog_path = expected_home / "models.json"
        if _process_needs_model_catalog(resolved, relevant):
            expected_catalog_hash = session["homes"][agent_id].get("model_catalog_sha256")
            if (
                not isinstance(expected_catalog_hash, str)
                or not re.fullmatch(r"[0-9a-f]{64}", expected_catalog_hash)
                or not catalog_path.is_file()
                or catalog_path.is_symlink()
                or sha256_file(catalog_path) != expected_catalog_hash
            ):
                raise RuntimeError(
                    f"pinned generated model catalog failed integrity validation for {agent_id}"
                )
        if "codex_builtin" in drivers and any(item != "codex_builtin" for item in drivers):
            with contextlib.suppress(OSError, ValueError, json.JSONDecodeError):
                bundled_catalog = _validated_codex_catalog(read_json(catalog_path))
            if bundled_catalog is None:
                raise RuntimeError(
                    f"pinned generated model catalog is missing or invalid for {agent_id}"
                )
        homes[agent_id] = materialize_agent_home(
            directory,
            snapshot,
            agent_id,
            gateway_base_url,
            session_id=str(session["session_id"]),
            native_tokens=native_tokens,
            bundled_catalog=bundled_catalog,
            availability=availability,
            disable_native_delegation=bool(session.get("tainted")),
        )
    return homes


def _credential_environment_for_agent(
    session: Mapping[str, Any], agent_id: str
) -> tuple[dict[str, str], list[str]]:
    groups = session["homes"][agent_id].get("direct_credential_groups", [])
    header_envs = session["homes"][agent_id].get("direct_header_envs", [])
    tool_mcp_envs = session["homes"][agent_id].get("tool_mcp_envs", [])
    tool_mcp_http_envs = set(session["homes"][agent_id].get("tool_mcp_http_envs", []))
    if not groups and not header_envs and not tool_mcp_envs:
        return {}, []
    values = parse_env_file(config_root() / "credentials.env")
    values.update({key: value for key, value in os.environ.items() if value})
    supplied: dict[str, str] = {}
    allowed: list[str] = []
    for raw_group in groups:
        credentials = [str(name) for name in raw_group]
        source = next((name for name in credentials if values.get(name)), None)
        if source is None:
            raise RuntimeError("missing direct-provider credential: " + "/".join(credentials))
        target = credentials[0]
        supplied[target] = values[source]
        allowed.append(target)
    for raw_name in header_envs:
        env_name = str(raw_name)
        value = values.get(env_name)
        # Codex treats a missing or blank env-backed header as optional and
        # omits it. Preserve that contract while ensuring a configured secret
        # is not removed by filtered_environment().
        if value is None or not value.strip():
            continue
        if not valid_http_header_value(value):
            raise RuntimeError(
                f"direct-provider header environment variable {env_name} "
                "contains a prohibited control character"
            )
        supplied[env_name] = value
        allowed.append(env_name)
    for raw_name in tool_mcp_envs:
        env_name = str(raw_name)
        value = values.get(env_name)
        if value is None:
            continue
        if env_name in tool_mcp_http_envs:
            if not value.strip():
                continue
            if not valid_http_header_value(value):
                raise RuntimeError(
                    f"tool MCP HTTP environment variable {env_name} "
                    "contains a prohibited control character"
                )
        elif "\x00" in value:
            raise RuntimeError(f"tool MCP environment variable {env_name} contains NUL")
        supplied[env_name] = value
        allowed.append(env_name)
    return supplied, allowed


def session_environment(
    session: Mapping[str, Any],
    agent_id: str,
    *,
    caller_job_id: str | None = None,
    caller_token: str | None = None,
    interactive: bool = False,
) -> dict[str, str]:
    run_id = session.get("current_run_id")
    if not isinstance(run_id, str) or session.get("status") not in ACTIVE_SESSION_STATUSES:
        raise RuntimeError("session has no active execution run")
    credential_values, allow_sensitive = _credential_environment_for_agent(session, agent_id)
    extra = {
        **credential_values,
        "CODEX_HOME": session["homes"][agent_id]["home"],
        "MMO_INSTALL_ROOT": str(install_root()),
        "MMO_CONFIG_ROOT": str(config_root()),
        "MMO_STATE_ROOT": str(state_root()),
        "MMO_ROOT_SESSION_ID": session["session_id"],
        "MMO_RUN_ID": run_id,
        "MMO_PROFILE_SNAPSHOT": session["snapshot_hash"],
        "MMO_CALLER_AGENT": agent_id,
        "MMO_ALLOWED_ROOT": session["allowed_root"],
        "MMO_SESSION_DIR": str(session_dir(session["session_id"])),
        "GIT_TERMINAL_PROMPT": "0",
    }
    # Keep worker/event logs deterministic, but never disable the foreground
    # Codex TUI's color capabilities.  An explicit user NO_COLOR setting is
    # still inherited and respected by filtered_environment().
    if not interactive:
        extra["NO_COLOR"] = "1"
    if caller_job_id:
        extra["MMO_CALLER_JOB_ID"] = caller_job_id
        if not caller_token:
            raise RuntimeError("worker MCP caller capability token is missing")
    elif caller_token is None:
        caller_token = root_mcp_token(str(session["session_id"]))
    if caller_token:
        extra["MMO_CALLER_TOKEN"] = caller_token
    return filtered_environment(allow_sensitive=allow_sensitive, extra=extra)


def _codex_binary() -> str:
    settings = load_settings()
    configured = str(settings.get("codex_bin", "codex"))
    return os.environ.get("MMO_CODEX_BIN") or configured
