#!/bin/sh
# GlAIpnir - AI Agents Sandbox - Manage a secure, isolated environment for
# running agents.
# Copyright (C) 2026  val4oss <val4oss@pm.me>
# 
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANYWARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
# 
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

# ================
# Global variables
# ----------------

# Return codes
SUCCESS=0
FAILURE=1

# Main variables
PRJ_ID="glaipnir"
SANDBOX_ID="ai-agents-sandbox"

# Path variables
DATA_D=/usr/share/glaipnir
SCRIPT_P="$(realpath "$0")"
IMG_D="${DATA_D}/image"
CONF_P_DEFAULT="${XDG_CONFIG_HOME:-${HOME}/.config}/${PRJ_ID}/${PRJ_ID}.conf"
CONF_P="${CONF_P_DEFAULT}"
CACHE_D_DEFAULT="${XDG_CACHE_HOME:-${HOME}/.cache}/${PRJ_ID}"
CACHE_D="${CACHE_D_DEFAULT}"
HOME_DATA_D_DEFAULT="${XDG_DATA_HOME:-${HOME}/.local/share}/${PRJ_ID}"
HOME_DATA_D="${HOME_DATA_D_DEFAULT}"
SANDBOX_D_DEFAULT="${CACHE_D_DEFAULT}/workspace"
SANDBOX_D="${PWD}"
BUILD_HOOK_ARG=""
RUN_HOOK_ARG=""

# Container variables
DEFAULT_IMG_REPO="registry.opensuse.org/home/vlefebvre/container-images/containers/opensuse"
IMG_NAME="${SANDBOX_ID}"
CTN_NAME="${SANDBOX_ID}"
IMG_TAG="1.1.0"
TRUSTED_AGENTS="copilot claude gemini opencode antigravity agy"
UNTRUSTED_AGENTS="hermes-agent"
UNTRUSTED_AGENTS_SENSITIVE_ACTIONS="build run"
AI_USER_NAME="aiuser"
AI_USER_UID=1000
AI_USER_GID=1000
AI_USER_WORKSPACE="/home/${AI_USER_NAME}/workspace"
PKGS=""
REPOS=""

# argument variables
AGENT=""
USE_MICROVM=1
VPN_PROTECTION=1
ACTION=""
DEBUG=0
BUILD_FULL=0
ALL=0
CLEAN_IMG=0
RESET_AGENT_CONF=0
FORCE=0
DNS_LIST=""
RO_MOUNTS=""
TOOLS_NEEDED="podman sed grep tar xargs"

# useful vars
MIN_LIBKRUN_VER="1.18.0"
# Set once the script restarted itself through 'sg', prevents a restart loop.
GLAIPNIR_SG="${GLAIPNIR_SG:-}"
# Arguments of the current run, used to restart after a group change.
CMD_LINE=""

# ========
# Includes
# --------

#!/bin/sh
# printer - Library for printing log messages.
# Copyright (C) 2026  val4oss <val4oss@pm.me>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
# Every message is written on the log channel, the file descriptor 3, opened
# once when this library is sourced. A command substitution only captures the
# standard output, so a log stays visible on the console even when it is
# emitted from inside a "$(...)". The standard output is therefore left free
# to carry the value returned by a function, and the standard error to carry
# the output of the external tools.

# ================
# Global variables
# ----------------

# Return codes, defined here too so that this library stays usable on its own
# whatever the order of the includes of the script sourcing it.
SUCCESS="${SUCCESS:-0}"
FAILURE="${FAILURE:-1}"

QUIET=0
VERBOSE=0

# Log channel destination, 'stderr' by default, so the messages reach the
# console when the script is interactive and the journal when it runs as a
# systemd unit. 'tty' forces the controlling terminal even when the standard
# output and the standard error are both redirected. 'file' appends to
# PRINTER_FILE.
PRINTER_SINK="${PRINTER_SINK:-stderr}"
PRINTER_FILE="${PRINTER_FILE:-}"
# Set by _printer_init(), 1 when the log channel accepts the ANSI colours.
PRINTER_COLOR=0
# Guard, the log channel is opened once even if this library is sourced twice.
PRINTER_READY="${PRINTER_READY:-0}"

# ==================
# Internal functions
# ------------------

###
# Open the log channel on the given target, keep stderr as a fallback
# ARGUMENTS:
#   1 - target : file or device to open the file descriptor 3 on
# OUTPUTS:
#   fd 3 : opened on the target, or on stderr when the target is not writable
###
_printer_open() {
    # The probe runs in a subshell, a failing 'exec' would abort the script.
    if (exec 3>>"$1") 2>/dev/null; then
        exec 3>>"$1"
    else
        exec 3>&2
        printf "[WARN] Cannot open the log channel '%s', using stderr.\n" \
            "$1" >&3
    fi
}

###
# Open the log channel once and detect whether it supports the colours
# OUTPUTS:
#   fd 3 : the log channel, opened according to PRINTER_SINK
###
_printer_init() {
    if [ "$PRINTER_READY" -eq 0 ]; then
        case "$PRINTER_SINK" in
            tty)  _printer_open "/dev/tty"      ;;
            file) _printer_open "$PRINTER_FILE" ;;
            *)    exec 3>&2                     ;;
        esac
        # No escape sequence in a log file nor in the journal.
        PRINTER_COLOR=0
        if [ -t 3 ]; then
            PRINTER_COLOR=1
        fi
        PRINTER_READY=1
    fi
}

###
# Print a tagged message on the log channel
# ARGUMENTS:
#   1 - tag  : level tag, right aligned on 6 characters with its brackets
#   2 - code : ANSI colour code of the level, used on a terminal only
#   3 - msg  : message to print
# OUTPUTS:
#   fd 3 : the formatted message
###
_print() {
    # Reopens the channel when printer_close() has been called before.
    _printer_init
    if [ "$PRINTER_COLOR" -eq 1 ]; then
        printf "%b%6s%b %s%b\n" "\033[0;${2}m" "[$1]" "\033[1;${2}m" "$3" \
            "\033[0m" >&3
    else
        printf "%6s %s\n" "[$1]" "$3" >&3
    fi
}

# ================
# Public functions
# ----------------

###
# Print a warning message
# ARGUMENTS:
#   1 - message to print
# OUTPUTS:
#   fd 3 : warning message, unless the quiet mode is on
###
print_warning() {
    if [ "$QUIET" -ne 1 ]; then
        _print "WARN" "33" "$1"
    fi
}

###
# Print a error message
# ARGUMENTS:
#   1 - message to print
# OUTPUTS:
#   fd 3 : error message, the quiet mode never hides it
###
print_error() {
    _print "ERR" "31" "$1"
}

###
# Print a info message
# ARGUMENTS:
#   1 - message to print
# OUTPUTS:
#   fd 3 : info message, unless the quiet mode is on
###
print_info() {
    if [ "$QUIET" -ne 1 ]; then
        _print "INFO" "32" "$1"
    fi
}

###
# Print a debug message
# ARGUMENTS:
#   1 - message to print
# OUTPUTS:
#   fd 3 : debug message, only when the verbose mode is on
###
print_debug() {
    if [ "$VERBOSE" -ne 0 ]; then
        _print "DEBG" "34" "$1"
    fi
}

###
# Release the log channel
# Useless before exiting, the kernel closes the descriptor anyway. Call it
# when this library is sourced by a long lived shell, or before spawning a
# child that must not inherit the channel. The next print_xxx() reopens it.
# GLOBALS:
#   write: PRINTER_READY
# OUTPUTS:
#   fd 3 : closed
# RETURNS:
#   SUCCESS
###
#printer_close() {
#    _ret="$SUCCESS"
#    exec 3>&-
#    PRINTER_READY=0
#    return "$_ret"
#}

# ===========
# Entry point
# -----------

_printer_init

# Source macOS-specific helpers (VPN enforcement, enforcer lifecycle).
# Provides: _macos_run_setup, _macos_run_teardown, _macos_remove_enforcer,
#           plus _ENFORCER_CONF and all _macos_* internals.
if [ "$(uname -s)" = "Darwin" ]; then
:# Skipping include macos-sandbox.sh
fi

# ==================
# Internal functions
# ------------------

# check if needed tools are available on the system
_check_tools_needed() {
    _ret="$SUCCESS"
    for dep in $TOOLS_NEEDED; do
        if ! command -v "$dep" > /dev/null 2>&1; then
            if [ ${missing_deps+x} ]; then
                missing_deps="$missing_deps $dep"
            else
                missing_deps="$dep"
            fi
        fi
    done
    if [ ${missing_deps+x} ]; then
        print_error "Some tools are missing on your system: $missing_deps"
        _ret="$FAILURE"
    fi
    return "$_ret"
}

# Get hooks according type of the argument, directory or single file.
# Id directory, it will return all scripts matching '[0-9][0-9]-*.sh'
_parse_hooks_f() {
    _hooks=""
    if [ -d "$1" ]; then
        for _h in "$1"/*.sh; do
            _h=$(echo "$_h" | sed -e 's|//|/|') # remove double slashes
            case "$(basename "$_h")" in
                [0-9][0-9]-*.sh) {
                    if [ -z "$_hooks" ]; then
                        _hooks="$_h"
                    else
                        _hooks="$_hooks $_h"
                    fi
                };;
            esac
        done
    elif [ -f "$1" ]; then
        _hooks="$1"
    fi
    echo "$_hooks"
}

# check if scripts exist and are valid (shellcheck if available).
_check_scripts() {
    _ret="$SUCCESS"
    _script="$1"
    if [ -n "$_script" ] && [ -f "$_script" ]; then
        if command -v shellcheck > /dev/null 2>&1; then
            if ! shellcheck -S warning "$_script" > /dev/null 2>&1; then
                print_error \
                    "Script '$_script' has shellcheck errors."
                _ret="$FAILURE"
            fi
        fi
    fi
    return "$_ret"
}

# print warning and prompt confirmation for untrusted agent
_check_untrusted_disclaimer() {
    print_warning "WARNING: The agent '$1' is considered UNTRUSTED"
    print_warning \
        "and does not comply with SUSE internal best practices."
    printf "Do you want to continue? [y/N]: "
    read -r _ans
    case "$_ans" in
        [Yy]* ) ;;
        * ) exit "$FAILURE";;
    esac
}

###
# check if agent is valid
# ARGUMENTS:
#   1 - agent: agent name
# RETURNS:
#   SUCCESS, FAILURE if directory cannot be created or is not valid
###
_valid_agent() {
    _va_rc="$FAILURE"
    _agent="$1"
    for _agt_v in $TRUSTED_AGENTS; do
        if [ "${_agent}" = "$_agt_v" ]; then
            _va_rc="$SUCCESS"
            break
        fi
    done
    if [ "${_va_rc}" = "${FAILURE}" ]; then
        for _agt_v in $UNTRUSTED_AGENTS; do
            if [ "$_agent" = "$_agt_v" ]; then
                for _act_v in $UNTRUSTED_AGENTS_SENSITIVE_ACTIONS; do
                    if [ "$ACTION" = "$_act_v" ]; then
                        _check_untrusted_disclaimer "$_agent"
                        break
                    fi
                done
                _va_rc="$SUCCESS"
                break
            fi
        done
    fi
    return "$_va_rc"
}

###
# Add an agent to the list of agents to run, after validating
# ARGUMENTS:
#   1 - agent: agent name
# RETURNS:
#   SUCCESS, FAILURE if not valid agent
###
_add_agent() {
    _aa_rc="${SUCCESS}"
    _agt="$1"
    if [ "$1" = "agy" ]; then
        _agt="antigravity"
    fi
    if ! _valid_agent "${_agt}"; then
        print_error "Unknown agent: '$1'. Valid agents:"
        print_error "  -Trusted: ${TRUSTED_AGENTS}"
        print_error "  -Untrusted: ${UNTRUSTED_AGENTS}"
        _aa_rc="${FAILURE}"
    else
        if [ "${AGENT}" = "" ]; then
            AGENT="$_agt"
        else
            BUILD_FULL=1
            AGENT="$_agt $AGENT"
            AGENT="$( printf "%s" "${AGENT}" | xargs -n1 |  sort -u | xargs)"
        fi 
    fi
    return "${_aa_rc}"
}

###
# parse configuration file if it exists
# RETURNS:
#   SUCCESS, FAILURE if conf not valid
###
_parse_conf() {
    _pc_rc="${SUCCESS}"
    if [ "$(basename "${CONF_P}")" = "${PRJ_ID}.conf" ] &&\
       [ ! -f "${CONF_P}" ]; then
        _c_dir="$(dirname "${CONF_P}")"
        _old_conf="${_c_dir}/${SANDBOX_ID}.conf"
        if [ -f "${_old_conf}" ]; then
            print_warning "Using legacy config $(basename "${_old_conf}"),"
            print_warning "Rename it to $(basename "${CONF_P}")."
            CONF_P="${_old_conf}"
        fi
    fi
    if [ -f "$CONF_P" ]; then
        _in_block=0
        _block_key=""
        while IFS= read -r _line; do
            # strip leading whitespace
            _line="${_line#"${_line%%[! ]*}"}"
            case "$_line" in
                "" | \#*) continue ;;
            esac
            if [ "$_in_block" -eq 1 ]; then
                case "$_line" in
                    *")"*) _in_block=0; _block_key="" ;;
                    *)
                        # strip quotes
                        _item=$(printf '%s' "$_line" \
                            | sed 's/^["'"'"']//;s/["'"'"']$//')
                        # Assigning to block keys
                        case "$_block_key" in
                            PACKAGES) PKGS="$PKGS $_item"   ;;
                            REPOS)    REPOS="$REPOS $_item" ;;
                            AGENTS)
                                _add_agent "$_item" || _pc_rc="${FAILURE}"
                                ;;
                            RO_MOUNTS)
                                if [ -n "$RO_MOUNTS" ]; then
                                    RO_MOUNTS="${RO_MOUNTS} ${_item}"
                                else
                                    RO_MOUNTS="${_item}"
                                fi
                                ;;
                        esac
                        ;;
                esac
            else
                _key="$(printf '%s' "$_line" | cut -d '=' -f 1)"
                _value="$(printf '%s' "$_line" | cut -d '=' -f 2-)"
                # strip quotes
                _value=$(printf '%s' "$_value" \
                    | sed 's/^["'"'"']//;s/["'"'"']$//')
                case "$_value" in
                    *"("*) _in_block=1; _block_key="$_key" ;;
                    *)
                        # Assigning to keys
                        case "$_key" in
                            AGENT)       
                                _add_agent "$_value" || _pc_rc="${FAILURE}"
                                ;;
                            USE_MICROVM)        USE_MICROVM="$_value" ;;
                            VPN_PROTECTION)     VPN_PROTECTION="$_value" ;;
                            WORKSPACE)          SANDBOX_D="$_value" ;;
                            CACHE)              CACHE_D="$_value" ;;
                            DATA_DIR|DATA_D)    HOME_DATA_D="$_value" ;;
                            IMG_TAG)            IMG_TAG="$_value" ;;
                            DNS)                DNS_LIST="$_value $DNS_LIST" ;;
                        esac
                esac
            fi
        done < "$CONF_P"
    fi
    return "${_pc_rc}"
}

# Add the current user in the group given in argument, after confirmation.
# On success the script restarts itself through 'sg' to apply the group
# without a re-login, so it only returns on failure or refusal.
_add_user_in_group() {
    _grp="$1"
    _usr="$(id -un)"
    printf "Add user '%s' in the '%s' group? [Y/n]: " "${_usr}" "${_grp}"
    read -r _ans || _ans="n"
    case "${_ans}" in
        [Nn]*) return "$FAILURE" ;;
    esac
    if ! sudo usermod -aG "${_grp}" "${_usr}"; then
        print_error "Failed to add '${_usr}' in the '${_grp}' group."
        return "$FAILURE"
    fi
    # Restart only once, 'sg' would loop if it does not apply the group.
    if [ -z "${GLAIPNIR_SG}" ] && command -v sg > /dev/null 2>&1; then
        print_info "Restarting with the '${_grp}' group applied..."
        exec sg "${_grp}" -c "GLAIPNIR_SG=1 sh ${SCRIPT_P} ${CMD_LINE}"
    fi
    print_warning "Log out and log back in to apply the '${_grp}' group."
    return "$FAILURE"
}

_check_microvm() {
    _ret="$SUCCESS"
    if [ ! -x "/usr/bin/krun" ]; then
        print_warning "krun not found at /usr/bin/krun."
        print_warning "     -> Install it via your package manager."
        _ret="$FAILURE"
    fi

    # for microvm libkrun needs to be > 1.18.0, that includes this fix:
    # https://github.com/containers/libkrun/commit/757b080b4c5f5934f8e5320a38b401aaec116764
    _libname="$(readlink "$(find /usr/lib*/ -name "libkrun.so.1" 2>/dev/null)")"
    if [ -n "$_libname" ]; then
        _ver="$(printf "%s" "$_libname" | sed -E 's/.*\.so\.//')"
        print_debug "Found libkrun version: $_ver"
        if [ -n "$_ver" ] &&\
            printf '%s\n' "$_ver" |\
            awk -F . '{ printf("%d%03d%03d\n", $1,$2,$3); }' |\
            awk -v req="$(printf '%s\n' "${MIN_LIBKRUN_VER}" |\
            awk -F . '{ printf("%d%03d%03d\n", $1,$2,$3); }')" \
                '{ if ($1 < req) exit 0; else exit 1; }'; then
            print_warning "libkrun version is too old (found: $_ver, required: > ${MIN_LIBKRUN_VER})."
            print_warning "     -> Please update libkrun to a version > ${MIN_LIBKRUN_VER}."
            _ret="$FAILURE"
        fi
    else
        print_debug "libkrun version not found"
    fi

    if [ ! -c /dev/kvm ]; then
        print_warning "/dev/kvm not found — KVM is not available on this host."
        print_warning "     -> Enable it by loading the kvm_amd or kvm_intel"
        print_warning "        kernel module."
        _ret="$FAILURE"
    elif ! id -Gn | tr ' ' '\n' | grep -qx kvm; then
        # Refusing to be added just falls back to the container mode.
        print_warning "User \"$(id -un)\" is not in the kvm group."
        if ! _add_user_in_group "kvm"; then
            print_warning "     -> Run 'sudo usermod -aG kvm $(id -un)'"
            print_warning "        (then relogin)."
            _ret="$FAILURE"
        fi
    fi

    # Nested-virtualisation check — warn only, does not abort
    if [ -f /sys/module/kvm_intel/parameters/nested ]; then
        if [ "$(cat /sys/module/kvm_intel/parameters/nested)" != "Y" ]; then
            print_warning "Nested virtualization is not enabled on this host."
            print_warning "This may cause issues when running microVMs inside \
another VM."
        fi
    elif [ -f /sys/module/kvm_amd/parameters/nested ]; then
        if [ "$(cat /sys/module/kvm_amd/parameters/nested)" != "1" ]; then
            print_warning "Nested virtualization is not enabled on this host."
            print_warning "This may cause issues when running microVMs inside \
another VM."
        fi
    fi

    return "$_ret"
}

# Detect the default public-facing interface, excluding VPN/tunnel interfaces.
# Returns the first default-route interface not matching tun|wg|vpn|tap|ppp.
_detect_public_iface() {
    _ret="$SUCCESS"
    _iface=""
    _vpn_if="tun|wg|vpn|tap|ppp|openvpn|docker0|br-"

    if command -v ip > /dev/null 2>&1; then
        _iface="$(ip route show default \
            | awk '{print $5}' \
            | grep -Ev "${_vpn_if}" \
            | head -1)"
    elif command -v route > /dev/null 2>&1; then
        _iface="$(route -n get default 2>/dev/null \
            | awk '/interface:/{print $2; exit}' \
            | grep -Ev "${_vpn_if}" \
            | head -1)"
    fi

    if [ -n "$_iface" ]; then
        printf "%s\n" "$_iface"
    else
        _ret="$FAILURE"
    fi

    return "$_ret"
}

###
# Check if podman image exists
# ARGUMENTS:
#   1 - img: Name or ID of the container to check
# RETURNS:
#   SUCCESS, FAILURE if image doesn't exists
###
_podman_img_exists() {
    _img="${1}"
    [ -n "${_img}" ] || return "${FAILURE}"
    podman image exists "${_img}"
}

# Get the repository of IMG_NAME
_podman_img_repo() {
    podman images "${IMG_NAME}" --format '{{.Repository}}'
}

###
# Get the unique IDs of the local image given in argument
# ARGUMENTS:
#   1 - img: Name of the image, without any tag
#   2 - tag: Tag to look for, all the tags when omitted
###
_podman_img_ids() {
    _img="${1}"
    _tag="${2:-*}"
    [ -n "${_img}" ] || return "${SUCCESS}"
    podman images \
        --format "{{.Repository}} {{.ID}}" \
        --filter "reference=${_img}:${_tag}" \
        --filter "dangling=false" |\
    while IFS=" " read -r _repo _id; do
        # Keep only the local repository, not a remote one sharing its name
        case "${_repo}" in
            "${_img}"|"localhost/${_img}") printf "%s\n" "${_id}" ;;
        esac
    done | sort -u
}

# Get the list of available images for the project
_podman_list_img() {
    _list=""
    _list=$(
        podman images \
            --sort created \
            --format "{{.Repository}}:{{.Tag}}" \
            --filter "reference=${IMG_NAME}*"\
            --filter "dangling=false" |\
        while IFS= read -r line; do
            printf "%s" "$line "
        done
    )
    printf "%s" "$_list"
}

###
# Check if podman container exists
# ARGUMENTS:
#   1 - ctn: Name or ID of the container to check
# RETURNS:
#   SUCCESS, FAILURE if container doesn't exists
###
_podman_ctn_exists() {
    _ctn="${1}"
    [ -n "${_ctn}" ] || return "${FAILURE}"
    podman container exists "${_ctn}"
}

# Get the list of available containers
_podman_list_ctn() {
    _list=""
    _list=$(
        podman ps -a \
            --format "{{.Names}}" \
            --filter "name=${CTN_NAME}*" |\
        while IFS= read -r line; do
            printf "%s" "$line "
        done
    )
    printf "%s" "$_list"
}

###
# Forced removing podman container
# ARGUMENTS:
#   1 - ctn: Name or ID of the container to remove
# OUTPUTS:
#   fd 3: Log messages
# RETURNS:
#   SUCCESS, FAILURE if container removal fails
###
_podman_rm_ctn() {
    _ctn="$1"
    _ctn_rc="${SUCCESS}"
    [ -n "${_ctn}" ] || return "${FAILURE}"

    if _podman_ctn_exists "${_ctn}"; then
        if ! podman rm -f "${_ctn}" > /dev/null 2>&1; then
            print_error "Failed to remove ${_ctn}"
            _ctn_rc="${FAILURE}"
        fi
    fi

    return "${_ctn_rc}"
}

###
# Safely remove a container image by first stopping and removing any
# containers created from it.
# ARGUMENTS:
#   1 - img: Name or ID of the image to remove
# OUTPUTS:
#   fd 3: Log messages
# RETURNS:
#   SUCCESS, FAILURE if container or image removal fails
###
_podman_rm_img() {
    _img="$1"
    _img_rc="${SUCCESS}"
    [ -n "${_img}" ] || return "${FAILURE}"

    _ctns=$(podman ps -a --filter "ancestor=${_img}" --format "{{.ID}}")
    if [ -n "$_ctns" ]; then
        for _ctn in $_ctns; do
            print_info "Removing container ${_ctn} using image ${_img}..."
            if ! _podman_rm_ctn "${_ctn}"; then _img_rc="$FAILURE"; fi
        done
    fi

    if [ "${_img_rc}" = "${SUCCESS}" ]; then
        print_info "Removing image ${_img}"
       if ! podman image rm "${_img}" > /dev/null 2>&1; then
            print_error "Failed to remove image ${_img}"
            _img_rc="${FAILURE}"
       fi
    fi

    return "${_img_rc}"
}

###
# Verify that the directory exists and is a allowed paths to not share
# sensitives path
# ARGUMENTS:
#   1 - dir: Path to the dir to verify
# RETURNS:
#   SUCCESS, FAILURE if directory cannot be created or is not valid
###
_verify_mount_point_d() {
    _ret="$SUCCESS"
    _path="$(realpath "$1" 2>/dev/null || echo "$1")"

    if [ -z "$1" ]; then
        print_warning "Mount point is not set."
        _ret="$FAILURE"
    fi

    if [ "${_ret}" != "${FAILURE}" ] && [ -e "${_path}" ] &&\
       [ ! -d "${_path}" ]; then
        print_warning "Mount point '${_path}' not a directory."
        _ret="$FAILURE"
    fi

    if [ "${_ret}" != "${FAILURE}" ]; then
        # TODO: Other list to allows ?
        _restricted_dirs="${HOME}"
        _allowed_paths="${_restricted_dirs} /tmp /var/tmp /opt"
        _ret="${FAILURE}"
       for _apath in ${_allowed_paths}; do
           case "${_path}" in
               "${_apath}"*) _ret="${SUCCESS}"; break ;;
           esac
       done
       if [ "${_ret}" = "${FAILURE}" ]; then
           print_warning "Mounting from ${_path} is forbidden"
       else
           for _rdir in ${_restricted_dirs}; do
               if [ "${_path}" = "${_rdir}" ]; then
                   print_warning "Mounting from ${_rdir} is forbidden"
                   _ret="${FAILURE}"
                   break
               fi
           done
       fi
    fi

    if [ "${_ret}" != "${FAILURE}" ] && [ ! -e "${_path}" ]; then
        print_debug "Mount point '${_path}' not found. Creating it..."
        if mkdir -p "${_path}"; then
            _ret="$SUCCESS"
        else
            print_warning "Failed to create it"
        fi
    fi
 
    return "$_ret"
}

###
# Verify that workspace directory exists or can be created
# RETURNS:
#   SUCCESS, FAILURE if directory cannot be created or is not valid
###
_verify_workspace_d() {
    _ret="$SUCCESS"
    SANDBOX_D="$(echo "${SANDBOX_D}" | sed "s|~|${HOME}|g")"
    _verify_mount_point_d "$SANDBOX_D" || {
        _ret="$SUCCESS"
        # Default could be overriding.
        if [ "$SANDBOX_D_DEFAULT" = "$HOME" ]; then
            print_warning "Default sandbox workspace points to HOME. Consider"
            print_warning "the use of '--workspace' to choose the workspace"
            print_warning "volume to mount."
            SANDBOX_D="${CACHE_D}/workspace"
        else
            SANDBOX_D="$SANDBOX_D_DEFAULT"
        fi
        print_warning \
            "Falling back to default workspace: '$SANDBOX_D'."
        _verify_mount_point_d "${SANDBOX_D}" || _ret="${FAILURE}"
    }
    return "$_ret"
}

###
# Verify that cache directory exists or can be created
# RETURNS:
#   SUCCESS, FAILURE if directory cannot be created or is not valid
###
_verify_cache_d() {
    CACHE_D="$(echo "${CACHE_D}" | sed "s|~|${HOME}|g")"
    _verify_mount_point_d "$CACHE_D" || {
        print_warning "Falling back to default cache: '$CACHE_D_DEFAULT'."
        CACHE_D="$CACHE_D_DEFAULT"
    }
}

###
# Verify that home data directory exists or can be created
# RETURNS:
#   SUCCESS, FAILURE if directory cannot be created or is not valid
###
_verify_home_data_dir() {
    HOME_DATA_D="$(echo "${HOME_DATA_D}" | sed "s|~|${HOME}|g")"
    _verify_mount_point_d "${HOME_DATA_D}" || {
        print_warning \
            "Falling back to default data dir: '${HOME_DATA_D_DEFAULT}'."
        HOME_DATA_D="${HOME_DATA_D_DEFAULT}"
    }
}

###
# Copy an agent's skel files from ${IMG_D}/agents/<agent> into a mount dir,
# preserving their parent directory layout.
# ARGUMENTS:
#   1 - agent: Name of the agent to copy
#   2 - dst: Destination directory to copy into.
# RETURNS:
#   SUCCESS, FAILURE if not copying
###
_copy_agent_files() {
    _src="${IMG_D}/agents/$1"
    _dst="$2"
    [ -d "$_src" ] || return "$SUCCESS"
    mkdir -p "$_dst"
    cp -rn "$_src/." "$_dst/"
    return "$SUCCESS"
}

###
# Copy host files into a into a mount dir hold by glaipnir
# if the dst already exists but the src is newer, warn the user to reset the
# agent configuration in the debug log.
# ARGUMENTS:
#   1 - src: File or directory to copy from, in the host HOME
#   2 - dst: File or directory to copy to, in the glaipnir managed mount dir
# OUTPUTS:
#   fd 3: log messages
# RETURNS:
#   SUCCESS, also when the host holds nothing to copy
###
_copy_host_files() {
    _src="$1"
    _dst="$2"
    [ -e "$_src" ] || return "$SUCCESS"
    if [ -e "${_dst}" ]; then
        _src_last_modified="$(stat -c %Y "${_src}" 2>/dev/null)"
        _dst_created="$(stat -c %W "${_dst}" 2>/dev/null)"
        if [ -n "${_src_last_modified}" ] && [ -n "${_dst_created}" ]; then
            if [ "${_src_last_modified}" -gt "${_dst_created}" ]; then
                print_debug "'${_src} is newer than '${_dst}', to update the"
                print_debug "sandbox configuration use '--reset-agent-config'"
                print_debug "option."
            fi
        fi
        return "${SUCCESS}"
    fi
    print_debug "Copying the host '$_src' into '$_dst'"
    _cp_rc="$SUCCESS"
    if [ -d "$_src" ]; then
        mkdir -p "$_dst"
        cp -rn "$_src/." "$_dst/" 2>/dev/null || _cp_rc="$FAILURE"
    else
        cp -n "$_src" "$_dst" 2>/dev/null || _cp_rc="$FAILURE"
    fi
    [ "$_cp_rc" = "$SUCCESS" ] ||
        print_warning "glaipnir did not copy some files of '$_src'."
    return "$SUCCESS"
}

###
# Remove the agents mount dir. The next bind fills it again from the host.
# RETURNS:
#   SUCCESS, FAILURE when glaipnir cannot remove the directory
###
_reset_agent_mounts() {
    [ -n "${CACHE_D}" ] || return "$FAILURE"
    _reset_d="${CACHE_D}/agents-mount"
    [ -d "$_reset_d" ] || return "$SUCCESS"
    print_info "Removing the agents configuration in '$_reset_d'..."
    rm -rf "$_reset_d" || {
        print_error "Failed to remove '$_reset_d'."
        return "$FAILURE"
    }
    return "$SUCCESS"
}

###
# Bind and fill agent mounts for the auth, config, skills..., if applicable
# OUTPUTS:
#   fd 3: log messages
# RETURNS:
#   SUCCESS, FAILURE if the mount point is not valid
###
_bind_agent_mounts() {
    _mount_d="${CACHE_D}/agents-mount"
    _verify_mount_point_d "$_mount_d" || {
        print_error "${_mount_d} is not a valid mount point."
        return "$FAILURE"
    }
    _home="/home/aiuser"
    _mounts=""
    _gemini_mounted=0
    _gcloud_mounted=0
    _host_cfg_d="${XDG_CONFIG_HOME:-${HOME}/.config}"

    # ~/.agents for all agents
    mkdir -p "${_mount_d}/.agents"
    _copy_host_files "${HOME}/.agents" "${_mount_d}/.agents"
    _mounts="$_mounts --volume $_mount_d/.agents:$_home/.agents:z"

    # copilot
    case " $AGENT " in *" copilot "*)
        mkdir -p "${_mount_d}/.config/gh" "${_mount_d}/.copilot"
        _copy_host_files "${_host_cfg_d}/gh" "${_mount_d}/.config/gh"
        _copy_host_files "${HOME}/.copilot" "${_mount_d}/.copilot"
        _mounts="$_mounts --volume $_mount_d/.config/gh:$_home/.config/gh:z"
        _mounts="$_mounts --volume $_mount_d/.copilot:$_home/.copilot:z"
    ;; esac

    # gemini + antigravity share ~/.gemini/
    case " $AGENT " in *" gemini "*|*" antigravity "*)
        if [ "$_gemini_mounted" = "0" ]; then
            mkdir -p "$_mount_d/.gemini"
            _copy_host_files "${HOME}/.gemini" "$_mount_d/.gemini"
            _mounts="$_mounts --volume $_mount_d/.gemini:$_home/.gemini:z"
            _gemini_mounted=1
        fi
    ;; esac

    # claude
    case " $AGENT " in *" claude "*)
        mkdir -p "$_mount_d/.claude"
        _copy_host_files "${HOME}/.claude" "$_mount_d/.claude"
        _copy_host_files "${HOME}/.claude.json" "$_mount_d/.claude.json"
        # must exist as file before mount
        touch "$_mount_d/.claude.json"
        _mounts="$_mounts --volume $_mount_d/.claude:$_home/.claude:z"
        _mounts="$_mounts --volume $_mount_d/.claude.json:$_home/.claude.json:z"
    ;; esac

    # claude (Vertex) + opencode share ~/.config/gcloud/
    # Never copy the ADC, cloud identity, from the host.
    # case " $AGENT " in *" claude "*|*" opencode "*)
    case " $AGENT " in *" claude "*|*" opencode "*)
        if [ "$_gcloud_mounted" = "0" ]; then
            mkdir -p "$_mount_d/.config/gcloud"
            _mounts="$_mounts \
--volume $_mount_d/.config/gcloud:$_home/.config/gcloud:z"
            _gcloud_mounted=1
        fi
    ;; esac

    # opencode
    case " $AGENT " in *" opencode "*)
        mkdir -p "$_mount_d/.config/opencode"
        _copy_host_files "${_host_cfg_d}/opencode" "$_mount_d/.config/opencode"
        _mounts="$_mounts \
--volume $_mount_d/.config/opencode:$_home/.config/opencode:z"
    ;; esac

    # hermes-agent
    case " $AGENT " in *" hermes-agent "*)
        mkdir -p "$_mount_d/.hermes"
        _copy_host_files "${HOME}/.hermes" "$_mount_d/.hermes"
        _mounts="$_mounts --volume $_mount_d/.hermes:$_home/.hermes:z"
    ;; esac

    # Seed each mounted config dir with the agent's skel files from the image.
    # Most agents map to ".<agent>", except antigravity (shares .gemini) and
    # hermes-agent (.hermes).
    for _agt in $AGENT; do
        case "$_agt" in
            antigravity)  _copy_agent_files "$_agt" "$_mount_d/.gemini" ;;
            hermes-agent) _copy_agent_files "$_agt" "$_mount_d/.hermes" ;;
            *)            _copy_agent_files "$_agt" "$_mount_d/.$_agt" ;;
        esac
    done

    printf '%s' "$_mounts"
}

###
# Bind read-only mounts requested through --ro-mount / RO_MOUNTS.
# Entries are 'src' or 'src:dst' defaults to
# /home/aiuser/<basename(src)>.;
# OUTPUTS:
#   fd 3: log messages
#   fd 1: list of --volume options to pass to podman, or empty if none
# RETURNS:
#   SUCCESS, FAILURE if a source is missing or destinations collide
###
_bind_ro_mounts() {
    [ -n "$RO_MOUNTS" ] || return "$SUCCESS"
    _brm_rc="$SUCCESS"
    _brm_mounts=""
    _brm_dsts="/home/${AI_USER_NAME} /tmp"

    for _brm_entry in $RO_MOUNTS; do
        _brm_src="$(printf '%s' "$_brm_entry" | cut -d ':' -f 1)"
        _brm_dst="$(printf '%s' "$_brm_entry" | cut -d ':' -f 2)"
        if [ "$_brm_dst" = "$_brm_entry" ]; then
            _brm_dst="/home/${AI_USER_NAME}/$(basename "$_brm_src")"
        else
            _brm_dst="$(realpath "$_brm_dst" 2>/dev/null || echo "$_brm_dst")"
        fi

        if ! _verify_mount_point_d "$_brm_src"; then
            print_warning \
                "ro mount source '$_brm_src' is not a valid directory."
            _brm_rc="$FAILURE"
            continue
        fi
        case " $_brm_dsts " in
            *" $_brm_dst "*)
                print_error "ro mounts collide on destination '$_brm_dst'."
                print_error \
                    "Use 'src:dst' to give each mount a distinct destination."
                _brm_rc="$FAILURE"
                continue
                ;;
        esac
        _brm_dsts="$_brm_dsts $_brm_dst"
        _brm_mounts="$_brm_mounts --volume $_brm_src:$_brm_dst:ro,z"
    done

    printf '%s' "$_brm_mounts"
    return "$_brm_rc"
}

# ================
# Action functions
# ----------------

# Print usage information
usage() {
    _str="Usage: ${PRJ_ID} [-q|-v|-h] <actions> <agent> [options]
  -q, --quiet   Suppress all output except errors
  -v, --verbose Enable verbose output
  -vv           Enable verbose and debug podman outputs
  --version     Show version information and exit
  -h, --help    Show this help message and exit

Actions:
  run           Run the sandbox with the specified agent
  build         Build the container image for the specified agent
                By default it bases from the registry: ${DEFAULT_IMG_REPO}
  clean         Remove generated container for the specified agent
  clean-cache   Remove glaipnir cache
  clean-data    Remove glaipnir saved data directory
  save-data     Archive agent config cache to XDG_DATA_HOME
  restore-data  Restore last data archive from XDG_DATA_HOME to cache
  status        Show the current status, built images, running containers...

Agents:
  (trusted)     ${TRUSTED_AGENTS}
  (untrusted)   ${UNTRUSTED_AGENTS}
                if no agent is specified, all trusted agents will be built
                (only for build action)

Options:
  --no-microvm Run the sandbox without microVM isolation (not recommended)
  --cache      Defined path for caching agent's data
  --conf       Defined conf file path for building the image. See Notes.
  --build-hook Defined hook(s) path for building the image. See Notes.
  --run-hook   Defined hook(s) path for running the image. See Notes.
  --dns        Defined DNS to use when using microvm. Can be set multiple times,
               default are 1.1.1.1 and 8.8.8.8
  --workspace, -w <dir>
               For 'run' action, Specify a custom workspace directory
               (default: ${SANDBOX_D_DEFAULT}) to mount in the sandbox at
               /home/aiuser/workspace.
  --ro-mount <src>[:<dst>]
               For 'run' action, bind-mount a host path read-only into the
               sandbox. Can be set multiple times. Defaults 'dst' to
               /home/aiuser/<basename(src)> when omitted.
  --no-vpn-protection
               For 'run' action, disable the microVM protection against VPN
               leaks. This is not recommended.
  --full       Build fully the container image instead of refering to the one
               from registry ${DEFAULT_IMG_REPO}
  --all, -a    For 'clean' action, Remove all containers and images
  --image      For 'clean' action, Remove built images of an agent
  --reset-agent-config
               For 'run' action, remove the agents configuration cached in
               ${CACHE_D_DEFAULT}/agents-mount before the start. glaipnir
               fills it again from the configuration of your host HOME.
  --data-dir <dir>
               Specify custom directory for saved data archives
               (default: ${HOME_DATA_D_DEFAULT})
  --force      Force overwriting existing cache data on restore

Notes:
  - The sandbox is designed to run in a secure, isolated environment.
  - On each 'run', glaipnir copies the agents configuration of your host HOME
    (~/.claude, ~/.gemini, ~/.config/gh...) into the sandbox one. A user
    already authenticated keeps the credentials and the history. glaipnir only
    reads your host, and it never overwrites a file of the sandbox.
  - Untrusted agents may not comply with best practices and could pose security
    risks.
  - To enable microVM isolation, ensure that KVM is available and the user is in
    the kvm group.
  - To configure the sandbox, you can 
    1. create a configuration file at ${CONF_P} with the following format:
    AGENT=<agent_name>
    USE_MICROVM=1
    WORKSPACE=<workspace_directory>
    IMG_TAG=<image_tag>
    PACKAGES=(
        <package1>
        <package2>
        ...
    )
    READONLY_MOUNTS=(
        <src1>
        <src2>:<dst2>
        ...
    )
    2. Create hooks to customize the image build. Gives the path to a script or
       to a folder containing '[0-9][0-9]-xxx.sh' scripts. Will be run as root.
    3. Create hooks to customize the container. Gives the path to a script or
       to a folder containing '[0-9][0-9]-xxx.sh' scripts. Will be run as
       userai.
"
    printf "%s\n" "$_str"
}

# Print version information
print_version() {
    printf "%s version: %s\n" "${PRJ_ID}" "$IMG_TAG"
}

# callback for build action
build() {
    _ret="$SUCCESS"

    # Check scripts before continuing
    # TODO: Get the hooks from vendordir `/usr/etc/${PRJ_ID}/*`
    # TODO: Get the hooks from admin config `/etc/${PRJ_ID}/*`
    [ -n "${BUILD_HOOK_ARG}" ] && \
        _build_hook="$(_parse_hooks_f "${BUILD_HOOK_ARG}")"
    for _h in $_build_hook; do
        if ! _check_scripts "$_h"; then
            print_warning "malformated hook script: $_h"
            _ret="$FAILURE"
        fi
    done
    [ "${_ret}" = "$FAILURE" ] && return "${_ret}"

    # Populate the build dir
    _build_d="${CACHE_D}/build"
    [ -d "${_build_d}" ] || mkdir -p "${_build_d}"
    cp -r "${IMG_D}/." "${_build_d}/"

    # Copy user hooks into the build context
    [ -n "$_build_hook" ] && {
        print_debug "Copying build hook(s): $_build_hook"
        for _h in $_build_hook; do
            cp "$_h" "${_build_d}/hooks/build/"
        done
    }

    if [ $BUILD_FULL -eq 1 ]; then
        _container_f="${_build_d}/Containerfile"
    else
        _container_f="${_build_d}/Containerfile.agent"
        sed -i "s|%%AGENT%%|$AGENT|g" "${_container_f}"
    fi
    print_info "Building container image ${IMG_NAME}:${IMG_TAG} ..."
    # Store every image previously built, whatever its version tag
    _prev_img_ids="$(_podman_img_ids "${IMG_NAME}")"
    set --
    [ ${DEBUG} -eq 1 ] && set -- --log-level=debug
    set -- "$@" build --no-cache --rm \
        --pull=always \
        --build-arg "AGENT=\"${AGENT}\"" \
        --build-arg "IMG_TAG=${IMG_TAG}" \
        --build-arg "PKGS=\"${PKGS}\"" \
        --build-arg "REPOS=\"${REPOS}\"" \
        --tag "${IMG_NAME}:${IMG_TAG}" \
        --tag "${IMG_NAME}:latest" \
        --file "${_container_f}" \
        "${_build_d}"
    _args=$*
    _cmd="podman ${_args}"
    [ ${VERBOSE} -eq 0 ] && _cmd="${_cmd} > /dev/null 2>&1"

    print_debug "$_cmd"
    if ! eval "$_cmd"; then
        print_error "Image build failed."
        _ret="$FAILURE"
    else
        print_info "Image built successfully."
        _new_img_id="$(_podman_img_ids "${IMG_NAME}" "${IMG_TAG}")"
        for _prev_img_id in ${_prev_img_ids}; do
            [ "${_prev_img_id}" = "${_new_img_id}" ] && continue
            _podman_rm_img "${_prev_img_id}" || true
        done
    fi

    # clean up temporary hook files from build context
    [ -d "${_build_d}" ] && rm -r "${_build_d}"
    return "$_ret"
}

# callback for run action
run() {
    _ret="$SUCCESS"

    _podman_cmd="podman"
    [ ${DEBUG} -eq 1 ] && _podman_cmd="podman --log-level=debug"

    [ "$(uname -s)" = "Darwin" ] && _macos_adjust_microvm
    if [ "$USE_MICROVM" -eq 1 ]; then
        if ! _check_microvm; then
            print_warning "MicroVM isolation is not available."
            print_warning "    -> Running without it for agent '${AGENT}' \
(not recommended)..."
            USE_MICROVM=0
        else
            print_info "Running sandbox with microVM isolation for agent \
'${AGENT}'..."
        fi
    else
        print_warning "Running sandbox without microVM isolation for agent \
'${AGENT}' (not recommended)..."
    fi
    
    if [ "$USE_MICROVM" = "1" ]; then
        TOOLS_NEEDED="$TOOLS_NEEDED krun"
        CTN_NAME="${CTN_NAME}-microvm"
    else
        # passt/pasta and ip run inside the Podman Machine's Linux VM on
        # macOS, not on the host, so only check for them on Linux.
        if [ "$(uname -s)" != "Darwin" ]; then
            TOOLS_NEEDED="$TOOLS_NEEDED passt ip"
        fi
    fi

    if [ -n "$GOOGLE_CLOUD_PROJECT$VERTEX_LOCATION" ] && \
       _podman_ctn_exists "$CTN_NAME"; then
        print_warning "Vertex env vars are applied only when creating a new container."
        print_warning "     -> Use 'clean ${AGENT}' then 'run ${AGENT}' to apply updates."
    fi

    if ! _check_tools_needed; then
        print_error "Required tools for the selected isolation are missing."
        print_error "Please install them and try again."
        return "$FAILURE"
    fi

    _network_arg="--network pasta"
    _dns_args=""
    if [ "${VPN_PROTECTION}" = "0" ]; then
        print_warning "VPN leak protection is disabled."
        print_warning "    -> The sandbox will have unrestricted egress."
    elif [ "$(uname -s)" = "Darwin" ]; then
        # On macOS, pasta:outbound_addr cannot bind at the host
        # level because containers run inside Podman Machine (Linux
        # VM) and all traffic is proxied through gvproxy on the macOS
        # host. VM-layer nftables enforcement is handled by
        # macos-vpn-enforcer.sh (started above via launchctl).
        if ! _macos_run_setup; then
            return "$FAILURE"
        fi
    else
        _iface="$(_detect_public_iface)" || true
        if [ -z "$_iface" ]; then
            print_error "Could not detect a non-VPN interface."
            print_error "    -> Aborting to avoid unrestricted egress."
            return "$FAILURE"
        else
            print_info "Binding outbound to interface: ${_iface}"
            _network_arg="--network \"pasta:--outbound-if4,${_iface}\""
            [ "$DNS_LIST" = "" ] && DNS_LIST="1.1.1.1 8.8.8.8"
            for _dns in $DNS_LIST; do
                _dns_args="$_dns_args --dns \"$_dns\""
            done
        fi
        [ "$DNS_LIST" = "" ] && DNS_LIST="1.1.1.1 8.8.8.8"
    fi
    for _dns in $DNS_LIST; do
        _dns_args="$_dns_args --dns \"$_dns\""
    done

    _ctn_state=""
    if _podman_ctn_exists "$CTN_NAME"; then
        _ctn_state=$(podman inspect "$CTN_NAME" --format '{{.State.Status}}')
    fi
    if [ "${RESET_AGENT_CONF}" -eq 1 ]; then
        if [ "$_ctn_state" = "running" ]; then
            print_warning "Container '$CTN_NAME' runs, glaipnir cannot reset"
            print_warning "    -> its agents configuration. Exit it, then"
            print_warning "    -> try again."
        else
            _reset_agent_mounts || return "$FAILURE"
        fi
    fi
    _agent_mounts="$(_bind_agent_mounts)" || return "$FAILURE"
    _ro_mounts="$(_bind_ro_mounts)" || return "$FAILURE"

    # Resume a stopped container
    if [ -n "$_ctn_state" ]; then
        case "$_ctn_state" in
            running) {
                _runtime=$(
                    podman inspect "$CTN_NAME" --format '{{.OCIRuntime}}'
                )
                if [ -n "$_runtime" ] && [ "$_runtime" = "krun" ]; then
                    _nbr="$(podman ps -a --format '{{.Names}}' |\
                            grep -c "$CTN_NAME")"
                    CTN_NAME="$CTN_NAME-$_nbr"
                    print_info "A container already run with '$_runtime'."
                    print_info "Creating a new one with suffixe $CTN_NAME."
                else
                    print_info "Attaching to running container..."
                    if ! ${_podman_cmd} exec -it "$CTN_NAME" bash; then
                        _ret="$FAILURE"
                    fi
                    [ "$(uname -s)" = "Darwin" ] \
                        && [ "${VPN_PROTECTION}" = "1" ] \
                        && _macos_run_teardown
                    return "$_ret"
                fi
            };;
            initialized|created|configured|exited) {
                print_info "Starting container from '${_ctn_state}'..."
                if ! ${_podman_cmd} start -ai "$CTN_NAME"; then
                    _ret="$FAILURE"
                fi
                [ "$(uname -s)" = "Darwin" ] \
                    && [ "${VPN_PROTECTION}" = 1 ] \
                    && _macos_run_teardown
                return "$_ret"
            };;
            *)       {
                print_error "Container '$CTN_NAME' is in state '${_ctn_state}'"
                print_error "    -> Cannot attach or resume."
                print_error "    -> Please use 'clean' before 'run'."
                return "$FAILURE"
            };;
        esac
    fi

    _verify_workspace_d || return "$FAILURE"

    # Check if image is built locally
    _default_repo="${DEFAULT_IMG_REPO}/${IMG_NAME}"
    if ! _podman_img_exists "${IMG_NAME}" ||\
       [ "$(_podman_img_repo)" = "${_default_repo}" ]; then
        print_info "Pulling image: ${_default_repo}..."
        if ! podman pull -q "${_default_repo}" > /dev/null 2>&1; then
            print_warning "No Image built and cannot pull ${_default_repo}"
            if ! build; then
                print_error "Failed to build image ${IMG_NAME}"
                return "${FAILURE}"
            fi
        fi
    fi

    # Setup run hooks
    # TODO: Get the hooks from vendordir `/usr/etc/${PRJ_ID}/*`
    # TODO: Get the hooks from admin config `/etc/${PRJ_ID}/*`
    _hooks_mount_d="${CACHE_D}/run-hooks"
    [ -d "${_hooks_mount_d}" ] && rm -r "${_hooks_mount_d}"
    mkdir -p "${_hooks_mount_d}"
    [ -n "${RUN_HOOK_ARG}" ] && \
        _run_hook="$(_parse_hooks_f "${RUN_HOOK_ARG}")"
    for _h in $_run_hook; do
        if ! _check_scripts "$_h"; then
            print_warning "malformated hook script: $_h"
            _ret="$FAILURE"
        fi
    done
    [ "${_ret}" = "$FAILURE" ] && return "$_ret"
    [ -n "$_run_hook" ] && {
        print_debug "Copying run hook(s): $_run_hook"
        for _h in $_run_hook; do
            cp "$_h" "${_hooks_mount_d}/"
        done
    }

    # Determine the destination workspace path in sandbox
    _ai_workspace_d="$(realpath "${SANDBOX_D}")"
    if echo "${_ai_workspace_d}" | grep -q "${HOME}"; then
        _ai_workspace_d="$(
            echo "${_ai_workspace_d}" | sed "s|^${HOME}/||;s|^${HOME}$||"
        )"
        if [ -n "${_ai_workspace_d}" ]; then
            AI_USER_WORKSPACE="/home/${AI_USER_NAME}/${_ai_workspace_d}"
        fi
    fi

    set --
    [ "$USE_MICROVM" = "1" ] && set -- --runtime krun --rm
    set -- "$@" \
        --name "$CTN_NAME" \
        "${_agent_mounts}" \
        "${_ro_mounts}" \
        "${_network_arg}" \
        "${_dns_args}" \
        --volume "${_hooks_mount_d}:/usr/local/bin/${SANDBOX_ID}-run-hooks/:z" \
        --volume "${SANDBOX_D}:${AI_USER_WORKSPACE}:z" \
        --tmpfs "/tmp:rw,nosuid,nodev,noexec,size=1g" \
        --cap-drop ALL \
        --security-opt no-new-privileges \
        --userns keep-id \
        --hostname ai-sandbox \
        --pids-limit 1024 \
        --env "AI_USER=${AI_USER_NAME}" \
        --env "AI_UID=${AI_USER_UID}" \
        --env "AI_GID=${AI_USER_GID}" \
        --env "AI_WORKSPACE=${AI_USER_WORKSPACE}" \
        --env "AI_SANDBOX_VERSION=${IMG_TAG}"

    # opencode unpacks its OpenTUI native library into the Bun temporary
    # directory and loads it with dlopen(), which needs an executable
    # mapping: /tmp stays noexec, so point Bun at a dedicated tmpfs.
    # Upstream: https://github.com/sst/opencode/issues/5175
    case " $AGENT " in *" opencode "*)
        set -- "$@" \
            --tmpfs "/run/agent-tmp:rw,nosuid,nodev,mode=1777,size=512m" \
            --env "BUN_TMPDIR=/run/agent-tmp"
    ;; esac

    # Forward cloud/relay settings needed by Vertex-backed OpenCode sessions.
    if [ -n "$GOOGLE_CLOUD_PROJECT" ]; then
        set -- "$@" --env "GOOGLE_CLOUD_PROJECT=$GOOGLE_CLOUD_PROJECT"
    fi
    if [ -n "$VERTEX_LOCATION" ]; then
        set -- "$@" --env "VERTEX_LOCATION=$VERTEX_LOCATION"
    fi

    if [ "$USE_MICROVM" = "1" ]; then
        # Available on crun > 1.27, below /.krun_config.json in the image is
        # required
        set -- "$@" --annotation krun.ram_mib=8192 --annotation krun.cpus=4
    fi

    _args=$*
    print_info "Starting isolated container..."
    _cmd="${_podman_cmd} run -it $_args ${IMG_NAME}:latest"
    print_debug "$_cmd"
    if ! eval "$_cmd"; then
        print_error "Failed to start container ${CTN_NAME}."
        _ret="$FAILURE"
    fi
    [ "$(uname -s)" = "Darwin" ] \
        && [ "${VPN_PROTECTION}" -eq 1 ] \
        && _macos_run_teardown
    return "$_ret"
}

# callback for clean action
clean() {
    _ret="$SUCCESS"
    if [ "$(uname -s)" != "Darwin" ] && [ "$USE_MICROVM" = 1 ]; then
        CTN_NAME="${CTN_NAME}-microvm"
    fi

    # Clean container(s)
    if [ $ALL -eq 1 ]; then
        _containers=$(_podman_list_ctn)
        for _ctn in ${_containers}; do
            if _podman_rm_ctn "$_ctn"; then
                print_info "Container ${_ctn} removed."
            else
                print_error "Failed to remove container '$_ctn'."
                _ret="$FAILURE"
            fi
        done
    else
        if _podman_ctn_exists "$CTN_NAME"; then
            _ctns="$(podman ps -a --format '{{.Names}}' |\
                grep -E "$CTN_NAME-[0-9]+")"
            if [ -n "$_ctns" ]; then
                for _ctn in $_ctns; do
                    print_info "Stopping and removing container '$_ctn'..."
                    if _podman_rm_ctn "$_ctn"; then
                        print_info "Container removed."
                    else
                        print_error "Failed to remove container '$_ctn'."
                        _ret="$FAILURE"
                    fi
                done
            fi
            print_info "Stopping and removing container '$CTN_NAME'..."
            if _podman_rm_ctn "$CTN_NAME"; then
                print_info "Container removed."
            else
                print_error "Failed to remove container '$CTN_NAME'."
                _ret="$FAILURE"
            fi
        fi
    fi

    if [ $ALL -eq 1 ]; then
        # Keep care of old volumes
        # ---
        _home_volume="$CTN_NAME-home"
        if podman volume exists "$_home_volume"; then
            if ! podman volume rm -f "$_home_volume"; then
                print_error "Failed to remove volume '$_home_volume'."
                _ret="$FAILURE"
            fi
        fi
        # ---
        if [ "$(uname -s)" = "Darwin" ]; then
            print_info "Removing macOS VPN enforcer artifacts..."
            if ! _macos_remove_enforcer; then
                print_error "Failed to remove VPN enforcer artifacts."
                _ret="$FAILURE"
            fi
        fi
    fi

    # Clean images, if all has been given all images will be removed. If an
    # agent has been specified, the listing will show only related agent image.
    if [ ${CLEAN_IMG} -eq 1 ] || [ ${ALL} -eq 1 ]; then
        _images=$(_podman_list_img)
        # Protect from "all" not given, IMG_NAME whould list all.
        if [ ${ALL} -eq 0 ]; then
            _images=$(echo "${_images}" | tr ' ' '\n' | grep "${IMG_NAME}:")
        fi
        for _img in ${_images}; do
            _podman_rm_img "${_img}" || _ret="${FAILURE}"
        done
        [ "${_ret}" = "${SUCCESS}" ] &&\
            print_info "All images related to ${IMG_NAME} has been removed."
    fi
    return "${_ret}"
}

###
# clean_cache action callback - Removes the cache directory
# OUTPUTS:
#   fd 3: Log messages
# RETURNS:
#   SUCCESS, FAILURE if container doesn't exists
###
clean_cache() {
    _cc_rc="${SUCCESS}"
    print_info "Removing cache dir ${CACHE_D}..."
    rm -rf "${CACHE_D}" || {
        print_error "Faield to clean cache dir."
        _cc_rc="${FAILURE}"
    }

    [ "${_cc_rc}" != "${SUCCESS}" ] || {
        print_info "podman volumes and Cache has been cleaned."
    }

}

###
# clean_data action callback - Removes the data directory
# OUTPUTS:
#   fd 3: Log messages
# RETURNS:
#   SUCCESS, FAILURE if removing data dir fails
###
clean_data() {
    _cd_rc="${SUCCESS}"
    _verify_home_data_dir
    print_info "Removing data dir ${HOME_DATA_D}..."
    rm -rf "${HOME_DATA_D}" || {
        print_error "Failed to clean data dir."
        _cd_rc="${FAILURE}"
    }

    [ "${_cd_rc}" != "${SUCCESS}" ] || {
        print_info "Data directory has been cleaned."
    }
    return "${_cd_rc}"
}

###
# save_data action callback - Archives agents-mount cache to data dir
# OUTPUTS:
#   fd 3: Log messages
# RETURNS:
#   SUCCESS, FAILURE if saving fails
###
save_data() {
    _sd_rc="${SUCCESS}"
    _src_d="${CACHE_D}/agents-mount"
    if [ ! -d "${_src_d}" ]; then
        print_error "No agents configuration found in '${_src_d}' to save."
        return "${FAILURE}"
    fi

    _verify_home_data_dir
    if [ ! -d "${HOME_DATA_D}" ]; then
        mkdir -p "${HOME_DATA_D}" || {
            print_error "Failed to create data directory '${HOME_DATA_D}'."
            return "${FAILURE}"
        }
    fi

    _archive_name="${PRJ_ID}-data-$(date +%Y%m%d_%H%M%S).tar.gz"
    _archive_path="${HOME_DATA_D}/${_archive_name}"

    print_info "Saving data from '${_src_d}' to '${_archive_path}'..."
    if tar -czf "${_archive_path}" -C "${CACHE_D}" "agents-mount"; then
        print_info "Data archive successfully saved: ${_archive_path}"
    else
        print_error "Failed to create archive '${_archive_path}'."
        _sd_rc="${FAILURE}"
    fi
    return "${_sd_rc}"
}

###
# restore_data action callback - Restores latest archive to cache
# OUTPUTS:
#   fd 3: Log messages
# RETURNS:
#   SUCCESS, FAILURE if restore fails
###
restore_data() {
    _rd_rc="${SUCCESS}"
    _verify_home_data_dir
    if [ ! -d "${HOME_DATA_D}" ]; then
        print_error "Data directory '${HOME_DATA_D}' does not exist."
        return "${FAILURE}"
    fi

    _last_archive=""
    for _f in "${HOME_DATA_D}/${PRJ_ID}-data-"*.tar.gz; do
        [ -f "${_f}" ] || continue
        _last_archive="${_f}"
    done

    if [ -z "${_last_archive}" ]; then
        print_error "No saved data archive found in '${HOME_DATA_D}'."
        return "${FAILURE}"
    fi

    _target_d="${CACHE_D}/agents-mount"
    if [ -e "${_target_d}" ]; then
        if [ "${FORCE}" -eq 0 ]; then
            print_warning "Cache data already exists in '${_target_d}'."
            print_warning "Avoided restoring. Use '--force' to override."
            return "${SUCCESS}"
        fi
        print_info "Force flag set: removing existing '${_target_d}'..."
        rm -rf "${_target_d}" || {
            print_error "Failed to remove '${_target_d}'."
            return "${FAILURE}"
        }
    fi

    [ -d "${CACHE_D}" ] || mkdir -p "${CACHE_D}" || {
        print_error "Failed to create cache directory '${CACHE_D}'."
        return "${FAILURE}"
    }

    print_info "Restoring data from '${_last_archive}' to '${CACHE_D}'..."
    if tar -xzf "${_last_archive}" -C "${CACHE_D}"; then
        print_info "Data successfully restored to '${CACHE_D}'."
    else
        print_error "Failed to restore data from '${_last_archive}'."
        _rd_rc="${FAILURE}"
    fi
    return "${_rd_rc}"
}

# callback for status action
status() {
    printf "Images :\n"
    _images=$(_podman_list_img)
    if [ "${_images}" = "" ]; then
        printf "  None\n"
    else
        for _img in ${_images}; do
            printf "  - %s\n" "${_img}"
        done
    fi
    printf "Containers :\n"
    _containers=$(_podman_list_ctn)
    if [ "${_containers}" = "" ]; then
        printf "  None\n"
    else
        for _ctn in ${_containers}; do
            _status="$(podman inspect "${_ctn}" --format "{{.State.Status}}")"
            _ctn_img="$(podman inspect "${_ctn}" --format "{{.ImageName}}")"
            printf "  - %s [%s]\n" "${_ctn}" "${_status}"
            printf "      with %s\n" "${_ctn_img}"
        done
    fi
    exit "${SUCCESS}"
}

# ===========
# Entry point
# -----------

# Keep the arguments to be able to restart after a group change.
CMD_LINE="$*"

# Get arguments
if [ $# -lt 1 ]; then
    print_error "Missing command"
    usage; exit 1
fi

if [ -f "${CONF_P}" ]; then
    if ! _parse_conf; then
        print_error "Failed to parse configuration file: $CONF_P"
        exit "${FAILURE}"
    fi
fi

while [ $# -gt 0 ]; do
    case "$1" in
        help|--help|-h)          usage;                     exit 0  ;;
        verbose|--verbose|-v)    VERBOSE=1;                 shift 1 ;;
        -vv)                     VERBOSE=1; DEBUG=1;        shift 1 ;;
        quiet|--quiet|-q)        QUIET=1;                   shift 1 ;;
        version|--version)       print_version;             exit 0  ;;
        run|build|clean|status)  ACTION="$1";               shift 1 ;;
        clean-cache)             ACTION="clean_cache";      shift 1 ;;
        clean-data)              ACTION="clean_data";       shift 1 ;;
        save-data)               ACTION="save_data";        shift 1 ;;
        restore-data)            ACTION="restore_data";     shift 1 ;;
        --no-microvm|no-microvm) USE_MICROVM=0;             shift 1 ;;
        --no-vpn-protection|no-vpn-protection)
                                 VPN_PROTECTION=0;          shift 1 ;;
        --conf)
            CONF_P="$2"
            if ! _parse_conf; then
                print_error "Failed to parse configuration file: $CONF_P"
                exit "${FAILURE}"
            fi
            shift 2
            ;;
        --cache)                 CACHE_D="$2";              shift 2 ;;
        --build-hook)            BUILD_HOOK_ARG="$2";       shift 2 ;;
        --run-hook)              RUN_HOOK_ARG="$2";         shift 2 ;;
        --dns)                   DNS_LIST="$2 $DNS_LIST";   shift 2 ;;
        full|--full)             BUILD_FULL=1;              shift 1 ;;
        --reset-agent-config)    RESET_AGENT_CONF=1;        shift 1 ;;
        all|--all|-a)            ALL=1;                     shift 1 ;;
        --image)
            if [ "$ACTION" =  "clean" ]; then
                CLEAN_IMG=1;
            fi
            shift 1
            ;;
        --workspace|-w)
            if [ -z "$2" ]; then
                print_error "Error: $1 requires an argument."
                exit 1
            fi
            SANDBOX_D="$2"
            shift 2
            ;;
        --ro-mount)
            if [ -z "$2" ]; then
                print_error "Error: $1 requires an argument."
                exit 1
            fi
            if [ -n "$RO_MOUNTS" ]; then
                RO_MOUNTS="${RO_MOUNTS} $2"
            else
                RO_MOUNTS="$2"
            fi
            shift 2
            ;;
        --data-dir)
            if [ -z "$2" ]; then
                print_error "Error: $1 requires an argument."
                exit 1
            fi
            HOME_DATA_D="$2"
            shift 2
            ;;
        --force)                 FORCE=1;                   shift 1 ;;
        -*)
            print_error "Unknown option: $1"
            usage
            exit 1
            ;;
        *)
            _add_agent "$1" || exit 1
            shift 1
            ;;
    esac
done

_check_tools_needed || {
    print_error "Please install the missing tools and try again. Aborting."
    exit "${FAILURE}"
}

if [ "$CACHE_D" != "${CACHE_D_DEFAULT}" ]; then
    _verify_cache_d
    SANDBOX_D_DEFAULT="${CACHE_D}/workspace"
else
    # Previous CACHE DIR was with "ai-agents-sandbox" name
    _old_conf_d="${HOME}/.cache/${SANDBOX_ID}"
    if [ -d "${_old_conf_d}" ] && [ ! -d "${CACHE_D_DEFAULT}" ]; then
        mv "${_old_conf_d}" "${CACHE_D_DEFAULT}"
    fi
fi

# Per-workspace configuration file.
if [ "${SANDBOX_D}" != "${SANDBOX_D_DEFAULT}" ] &&\
   [ "${CONF_P}" = "${CONF_P_DEFAULT}" ]; then
    _conf_p="${SANDBOX_D}/.${PRJ_ID}.conf"
    if [ -f "${_conf_p}" ]; then
        print_info "Using workspace configuration file: ${_conf_p}"
        CONF_P="${_conf_p}"
        if ! _parse_conf; then
            print_error "Failed to parse configuration file: $CONF_P"
            exit "${FAILURE}"
        fi
    fi
fi

if [ "$AGENT" != "" ]; then
    _agents="$(printf "%s" "${AGENT}" | tr ' ' '-')"
    IMG_NAME="${IMG_NAME}-${_agents}"
    CTN_NAME="${CTN_NAME}-${_agents}"
else
    # IF build is invoked, it should act like full argument.
    # Todo: Create the all image to export the full build for all agents
    BUILD_FULL=1
    AGENT="$TRUSTED_AGENTS"
fi

if ! eval "$ACTION"; then
    print_error "[✗] Action '$ACTION' failed."
    exit "${FAILURE}"
elif [ -z "${ACTION}" ]; then
    print_warning "No action specified. Nothing to do."
    usage
else
    print_info "[✓] Done."
fi
