#!/usr/bin/bash
# SPDX-License-Identifier: AGPL-3.0-only
# /usr/local/libexec/ai-tools/ai-tools-admin
# Host administration for the ai-tools sandbox. A root helper (run via sudo), not an ai-tools CLI verb: it edits host
# config (the OPERATORS list, the ai-ops group, the sandbox account's linger; the loaded optional SELinux policy groups)
# while the ai-tools CLI is unprivileged and refuses to run as root.
#
#   ```bash
#   atr admin operators                          # list (the zero-argument default)
#   atr admin operators add [user]               # default: $SUDO_USER
#   atr admin operators remove <user>
#   atr admin selinux groups                     # show core + optional group state
#   atr admin selinux groups enable <name>...    # load prebuilt (stable) groups
#   atr admin selinux groups disable <name>      # unload one
#   atr admin system bootstrap                   # provision the sandbox account's toolchain
#   atr admin system bootstrap --scope full      # ... and every enabled integration
#   atr admin system bootstrap --agents agent-codex # ... enabling the named agent, unattended
#   atr admin system entrypoints relabel         # verify + relabel the agent entrypoints
#   atr admin system post-upgrade                # reconcile the .rpmnew files upgrades leave
#   atr admin system post-upgrade --check        # findings as a record stream; silent, exit 0 when clean
#   atr admin system layout                      # every installed path, with the drift each carries
#   atr admin system layout reconcile            # restore each drifted path to its row
#   atr admin status                             # the host's health, read as root
#   atr admin assets                             # the asset sets, and what each enabled agent links
#   atr admin assets enable --set <name>         # enable a set in every enabled agent
#   atr admin stop                               # stop every agent session, through the stop helper
#   atr admin dotnet bootstrap                   # a domain a provider package contributes
#   ```
#
# The spelling is the project's command grammar (cli-grammar.rule.md): a bare-word command, a plural collection,
# the verb after the noun, `list` as the zero-argument default, and a singular domain (`selinux`, `system`) where one is
# needed. `--` introduces an option and never a command, which here is `--help`/`-h` and `--version`. The command
# surface is data in help.lib.sh, which renders the help and reads a command line as a help request ahead of the root
# check: `-h` or `--help` anywhere, or `help` where a command word goes, prints the help of the path typed and does not
# run the command, and a bare run prints the whole help.
#
# An operator is a login user (a human or a rootless service account) that drives the sandbox through the shared
# ai-tools account. `add` is accumulating and idempotent: it seeds the user's config in `~/.config/ai-tools` (asking
# before it creates a missing `.config`), appends the name to OPERATORS in /etc/ai-tools/operator.conf, adds the user
# to the ai-ops group (the sudoers grant and the launch wrapper gate on membership), ensures the sandbox account's
# linger, and offers to wire the PATH ordering line. The seed comes first so that an account enrolled here holds
# the project registry every later command resolves it through; a first enrolment that cannot write one is refused
# with the host unchanged. `remove` reverses the host-side membership (drops the name from OPERATORS and ai-ops),
# leaving the user's own registry and config. `list` prints the current operators.
#
# `selinux groups` toggles the optional policy groups, all off by default. It loads the COMPILED ai_tools_<group>.pp
# that ai-tools-selinux (or a checkout's install-selinux.sh build) staged under AI_TOOLS_SELINUX_GROUPS__PACKAGE_DIR
# via semodule -- no source tree or selinux-policy-devel needed on the host. The group set, descriptions, and per-group
# stability are single-sourced from selinux-groups.lib.sh, shared with selinux/install-selinux.sh (the source-tree
# authoring tool that instead COMPILES a group; this operator helper only loads a shipped one). Only STABLE groups are
# on the shipped set; `groups enable` of an EXPERIMENTAL (unaudited) group is refused with a pointer to the source
# compile-and-verify workflow (install-selinux.sh + the avc bring-up loop), since this tool will not load an unaudited
# module. `groups disable` works for any loaded group, stable or not.
#
# `system bootstrap` provisions the sandbox account and the Node toolchain the enabled agents run on, through the root
# helper ai-tools-bootstrap. It is the first command an administrator runs on a new host, and the one that installs
# software over the network, which is why it is a command rather than an RPM scriptlet: a scriptlet must succeed offline
# and inside a build chroot. Idempotent -- an existing account, nvm install or Node version is reused -- so it is also
# the re-run after enabling an agent in operator.conf. No agent is enabled until an operator names one:
# with AI_TOOLS_AGENTS absent the helper asks which ONE installed agent to enable and writes that line, and `--agents`
# is the unattended form of the same choice, passed through to the helper, which checks the names against the installed
# manifests before writing. The bare form does the minimal provision; `--scope full` then runs each ENABLED
# integration's own `bootstrap` through the admin-commands seam, so a host is provisioned end to end in one command
# without base naming an integration.
#
# Beyond those, the command set is EXTENSIBLE rather than enumerated: this tool ships in ai-tools-base, which is
# installed before anyone knows which provider packages a host will add, so a provider contributes a domain of its own
# as an executable fragment at /usr/local/lib/ai-tools/admin-commands.d/<name> and this tool discovers it. The basename
# is the domain token -- the same name the provider takes in agents.d/integrations.d and in operator.conf. Dispatch is
# an exec, not a source, so a fragment keeps its own `set -euo pipefail`, root guard and logging. A fragment is honored
# only while it and its directory pass the provider trust predicate, and one claiming a name base owns is refused rather
# than merged; both refusals are reported (providers.rule.md, the admin-commands.d seam). INSTALLATION, not enablement,
# decides whether a command exists -- what a command reports still names the enablement state,
# and `system bootstrap --scope full` reads the enabled set instead.
#
# `system entrypoints relabel` reconciles each enabled agent's entrypoint after a toolchain change, through the root
# helper ai-tools-relabel-agent: it verifies the binary against the checksum its vendor signed and records the result
# in that agent's pin, then restores ai_tools_exec_t so the domain transition fires and ai-tools-run stops fail-closing
# on the launch. It clears AI_TOOLS_ENTRYPOINT_PIN_REUSE before the exec, so this route always re-fetches the vendor's
# signed manifest: the unattended callers (ai-tools-relabel.service, the agent package's %post) may answer
# from an unchanged pin, and an administrator asking for a reconcile is asking for the fetch.
#
# `status` reports this host's health as root: the same resource `atr status` reports to an operator, completed
# with the readings that vantage point prints as `?` -- among them the sandbox account's own `systemd --user units`,
# whose bus root reaches over the machine transport; the entrypoint pin, in a state directory a non-operator has no
# traverse bit on; and the SELinux type each agent path carries right now, inside a 0750 toolchain. Every verdict comes
# from the same services.lib.sh registry both reports and the launch wrapper's pre-launch warning read, so the two
# commands differ in what each is allowed to see and agree on what they both see.
#
# `system post-upgrade` reconciles the `<file>.rpmnew` copies an upgrade leaves beside the %config(noreplace) files this
# stack owns. rpm keeps what the host edited and parks the new version alongside it; choosing between the two is
# a judgement about the operator's own configuration, so it happens here, when the operator asks, and never
# in a scriptlet. Each file gets the treatment its content deserves -- merge, report, or show only, per the post-upgrade
# registry -- and every treatment shows what it would change, confirms, backs the file up before writing, and names each
# path it touched. The copy itself is never removed: each block names the one to delete, once the operator has merged
# what they want from it. The from-source installer reaches the same end through its own keep-or-reset prompts and dated
# .bak/.shipped sidecars; this is the RPM-side equivalent. The same command migrates each enrolled operator's registry
# an earlier release wrote (_pu_registry_migrate), and under `--check` writes a `registry-unmigrated` finding per legacy
# file and `registry-invalid` for one format 1.0 does not carry.
#
# Its domain rules are cli.rule.md (what each command does) and cli-grammar.rule.md (how it is spelled).

set -euo pipefail

readonly SANDBOX_USER="ai-tools"
readonly SANDBOX_GROUP="ai-tools"
readonly OPERATORS_GROUP="ai-ops"
readonly OPERATOR_CONF="/etc/ai-tools/operator.conf"
readonly OPERATOR_LIB="/usr/local/lib/ai-tools/operator.lib.sh"
readonly SELINUX_GROUPS_LIB="/usr/local/lib/ai-tools/selinux-groups.lib.sh"
readonly CONF_LIB="/usr/local/lib/ai-tools/conf.lib.sh"
readonly SETTINGS_MERGE_LIB="/usr/local/lib/ai-tools/settings-merge.lib.sh"
readonly PROVIDERS_LIB="/usr/local/lib/ai-tools/providers.lib.sh"
readonly PATH_ORDER_LIB="/usr/local/lib/ai-tools/path-order.lib.sh"
readonly RECORDS_TSV_LIB="/usr/local/lib/ai-tools/records-tsv.lib.sh"
readonly ASSETS_LIB="/usr/local/lib/ai-tools/assets.lib.sh"
readonly LAYOUT_LIB="/usr/local/lib/ai-tools/layout.lib.sh"
readonly HELP_LIB="/usr/local/lib/ai-tools/help.lib.sh"
# Where a provider package drops the command fragment carrying its own domain. The environment override is a root-only
# test hook (tests.rule.md, ref-section-b4y9), so tests/unit/admin-commands.sh drives the dispatch against a fixture
# tree. `--help` lists the domains through it ahead of the root check, so a non-root caller can point the LISTING
# at another root-owned directory; the dispatch still needs root and each fragment still needs root ownership,
# so the reach does not add a command. Unset in production.
readonly ADMIN_COMMANDS_DIR="${AI_TOOLS_ADMIN_COMMANDS_DIR:-/usr/local/lib/ai-tools/admin-commands.d}"
# What an administrator does about a contributed command that is not root's alone. Stated once and shared by every
# message that reports one, so the dispatch, the full-scope bootstrap and the help give one answer. It is deliberately
# NOT "chmod it": a packaged command installs root-owned and unwritable by anyone else, so a file in that state is
# either one installed by hand or a change somebody made to this host -- and a mode fixed in place would re-bless
# content that whoever could write the file may already have rewritten.
readonly ADMIN_COMMANDS_TAMPER_REMEDY="a packaged command installs root-owned and unwritable by anyone else, so reinstall the package owning it -- 'rpm -qf <path>' names it -- and look into how the file came to be writable; a command installed by hand is yours to correct"
# Root-only helpers, 750 root:root, reached directly rather than through sudo: this tool already refuses a non-root
# caller. Both ship in ai-tools-integration-nodejs, which the metapackage pulls in weakly, so each command checks
# for its helper and names that package when it is absent. The ai-tools-relabel.path watcher, ai-tools-bootstrap
# and the agent package's %post run the relabel helper as root themselves.
readonly RELABEL_ENTRYPOINT_BIN="/usr/local/libexec/ai-tools/ai-tools-relabel-agent"
readonly BOOTSTRAP_BIN="/usr/local/libexec/ai-tools/ai-tools-bootstrap"
# The session-stop helper `stop` execs, the one `atr stop` reaches through ai-tools; root reaches it directly.
readonly STOP_BIN="/usr/local/libexec/ai-tools/ai-tools-stop"

# Substituted at deploy time (install.sh install_subst from packaging/VERSION; the RPM from %{version}), and left
# as the literal token in the checkout -- which `--version` reports as `dev`, the same value and the same fallback
# the CLI uses.
AI_TOOLS_VERSION="0.29.0-1.el9"
[[ "${AI_TOOLS_VERSION}" == @*@ ]] && AI_TOOLS_VERSION="dev"
readonly AI_TOOLS_VERSION

# A leading message code (msg.lib.sh states the form) is printed on its own line ahead of the message, the shape
# tests/lib/harness.sh's assert_msg reads. Matched inline, since these helpers report before the library is loaded.
die() {
    local code=""
    if [[ "${1-}" =~ ^MSG-[A-Z][0-9][A-Z][0-9]$ ]]; then code="$1"; shift; printf '%s\n' "${code}" >&2; fi
    printf 'ai-tools-admin: error: %s\n' "$*" >&2; exit 1
}
log()  { printf 'ai-tools-admin: %s\n' "$*"; }
# warn: a refusal that narrows what this tool will do -- a contributed command skipped, an integration that would not
# provision. stderr, so the domain list on stdout stays data-only.
warn() {
    local code=""
    if [[ "${1-}" =~ ^MSG-[A-Z][0-9][A-Z][0-9]$ ]]; then code="$1"; shift; printf '%s\n' "${code}" >&2; fi
    printf 'ai-tools-admin: warning: %s\n' "$*" >&2
}
# notice: a consequence of what this tool wrote, at a lower severity than warn -- the run is complete and the host is
# as asked for. stderr, like warn, so a verb's record stream on stdout stays data-only.
notice() {
    local code=""
    if [[ "${1-}" =~ ^MSG-[A-Z][0-9][A-Z][0-9]$ ]]; then code="$1"; shift; printf '%s\n' "${code}" >&2; fi
    printf 'ai-tools-admin: notice: %s\n' "$*" >&2
}

# reject <message>: the command line was rejected. Exit 2 separates a command nobody can type correctly
# from an operation that ran and failed (`die`, exit 1), which is the split ai-tools-admin(8) documents and the one
# ai-tools(1) already uses.
reject() {
    local code=""
    if [[ "${1-}" =~ ^MSG-[A-Z][0-9][A-Z][0-9]$ ]]; then code="$1"; shift; printf '%s\n' "${code}" >&2; fi
    printf 'ai-tools-admin: %s\n' "$*" >&2
    printf "try 'atr admin --help'\n" >&2
    exit 2
}

# reject_unknown_command <code> <message>: the refusal for a command path no arm accepts, at reject's exit 2: the code
# line and the one message line, which names the help command for the deepest path the help holds. Each arm writes its
# own message, so each keeps its own code.
reject_unknown_command() {
    printf '%s\n' "$1" >&2
    printf 'ai-tools-admin: %s\n' "$2" >&2
    exit 2
}

# die_unsourced <lib>: one situation for every library this tool requires, the library named as its argument -- called
# after the `source` line so each keeps its own shellcheck source directive.
die_unsourced() { die MSG-V6F4 "cannot source ${1}"; }

# coded_refusal <code> <text>: a refusal carried as a VALUE -- the code on the first line, the text on the second.
# admin_command_check's branches define their codes through it, so each of its situations keeps a code of its own while
# the two sites that emit the value need not know which branch produced it.
coded_refusal() { printf '%s\n' "$1" "$2"; }
# emit_coded <emitter> <value> [prefix]: hand a coded_refusal value to warn or die as the two arguments they read,
# with <prefix> naming who the refusal is about ahead of the text.
emit_coded() { "$1" "${2%%$'\n'*}" "${3-}${2#*$'\n'}"; }

# ── contributed command domains ──────────────────────────────────────────────────────────────
# The seam that lets a provider package add a domain to this tool. Base cannot enumerate the integrations it ships
# without, so the domain list is DISCOVERED -- and every way that discovery can fail leaves the command surface smaller,
# never wider.

# admin_commands_trusted: the SET-WIDE gate, applied before any contributed command is run.
#
# Per-file skipping already keeps a mis-permissioned fragment from being executed, and this refuses its neighbours
# as well. The reason is what the failure means rather than what it reaches: only root may write this directory,
# so a fragment inside it that group or other may write is a file the sandbox account can rewrite between one run
# and the next, sitting where root looks for commands. Running the entries that still pass would leave that file
# in place and the administrator with no reason to act. Base's own commands are untouched by this, so the host stays
# administrable while the finding is dealt with (ADMIN_COMMANDS_TAMPER_REMEDY).
admin_commands_trusted() {
    (( AI_TOOLS_HELP__ADMIN_COMMANDS_TAMPERED == 0 ))
}

# admin_command_check <domain>: succeed when the fragment carrying <domain> conforms to the interface this tool
# dispatches, publishing its declared verbs in ADMIN_COMMAND_VERBS; otherwise set _admin_command_reason to what is
# wrong, as a coded_refusal value. A reason rather than a message, so the two callers can act differently on it --
# an administrator's own command stops, while `system bootstrap --scope full` names it and carries on to the next
# integration. Each branch defines its own message code, since neither emit site knows which of them it holds.
#
# Where the trust checks decide WHO wrote the file, this decides whether the file is a command of this seam at all. It
# is a conformance contract, not a security boundary: what stops a file the agent wrote is the trust predicate,
# and what this stops is a file that was never meant to be run this way. It is declarative and static -- a fragment is
# read, never executed, to find out what it is, so a report is built by reading alone, without forking or running
# the fragment.
#
# A conforming fragment is a script (`#!`) carrying three declarations in its first 20 lines. The whole block is
# the interface: a third-party integration writes it once, and every check here reads it rather than running anything.
#
#   # ai-tools-admin-command: <domain>              the domain it is installed as
#   # ai-tools-admin-api-min-version: <maj>.<min>   the least this tool must implement for it to run
#   # ai-tools-admin-verbs: <verb> ...              the top-level verbs it answers (this project's
#                                                   list grammar: commas and whitespace separate)
#
# Each earns its place:
#   * the COMMAND line makes the fragment self-identifying, so a root-owned executable that merely
#     ends up in this directory -- a stray tool, an editor's backup, one provider's command copied
#     under another provider's name -- does not claim to be a command here and is not run as one.
#   * the API-MIN-VERSION line is version skew made visible, and it is a FLOOR rather than a
#     stamp of what the fragment was written on. A fragment ships in a package that upgrades
#     independently of base, and a third party does not re-declare it for every release of this
#     project, so what it states is what it NEEDS: `1.0` means "any ai-tools-admin implementing 1.0
#     or later can run me", and it keeps being true as this tool moves forward.
#   * the VERBS line is a capability list base READS rather than probes. `system bootstrap --scope
#     full` asks whether a provider has a `bootstrap` before running anything, so an integration
#     that contributes other commands is reported as having no provisioning to do rather than as
#     having failed. It is NOT argument validation -- what a verb accepts is the fragment's own
#     dispatch to answer, and a base that second-guessed it would drift out of agreement with it.
#
# There is no date and no "written against" version in the block: the package that installs the fragment carries both,
# and a hand-maintained copy of either would drift from it.
#
# ADMIN_COMMAND_API is what this tool implements, and it is the only version base holds. A fragment runs when its
# declared floor is one this tool satisfies -- the same comparison Apache httpd makes between a module's Module Magic
# Number and its own (AP_MODULE_MAGIC_AT_LEAST), and the same shape as every plugin host that takes a floor from its
# plugins (Chrome's minimum_chrome_version, Jenkins' jenkins.version baseline, a VS Code extension's engines.vscode):
#
#   the MAJOR must match      a different major is a different contract, and neither side can guess
#                             at the other's, so the refusal names both versions
#   the MINOR must be at      a minor revision only ADDS, so a fragment that needs 1.0 runs on 1.7;
#   least the declared one    one that needs 1.7 does not run here until base catches up
#
# That leaves one number to maintain and puts retirement in the major digit, where a reader already expects it, rather
# than in a second constant saying the same thing. It is also what lets a third party declare a floor once and leave it
# alone: everything valid at 1.0 keeps dispatching however far the minor advances.
#
# How the version moves, since that is the question every later change to this seam asks:
#   * base learns to CALL something new -- a `reset` or `update` verb invoked the way `system
#     bootstrap --scope full` invokes `bootstrap` -- is ADDITIVE. A fragment that does not declare
#     the verb is skipped, exactly as one without `bootstrap` is, so this takes a MINOR bump.
#   * base changes what it REQUIRES -- a new mandatory declaration, a different invocation -- is
#     still a MINOR bump while base keeps honouring the old shape (the new key optional, the old
#     invocation still answered). It is a MAJOR bump only when base drops that compatibility path,
#     which is the single change that refuses a working third-party command.
#   * a provider adding a verb TO ITSELF moves neither digit. `dotnet reset` is typed directly and
#     asks base for no behaviour, so the interface grows only when base starts invoking something.
readonly ADMIN_COMMAND_API="1.0"
_admin_command_reason=""
ADMIN_COMMAND_VERBS=()
admin_command_check() {
    local domain="$1"
    local fragment="${ADMIN_COMMANDS_DIR}/${domain}"
    local head_bytes header declared_floor declared_major declared_minor declared_verbs verb
    local base_major="${ADMIN_COMMAND_API%%.*}" base_minor="${ADMIN_COMMAND_API##*.}"
    _admin_command_reason=""
    ADMIN_COMMAND_VERBS=()
    if [[ ! -x "${fragment}" ]]; then
        _admin_command_reason="$(coded_refusal MSG-F4Z5 "not executable: ${fragment} -- reinstall the package that ships it")"
        return 1
    fi
    head_bytes="$(head -c 2 "${fragment}" 2>/dev/null || true)"
    if [[ "${head_bytes}" != '#!' ]]; then
        _admin_command_reason="$(coded_refusal MSG-D9F7 "not a script: ${fragment} -- a contributed command is an interpreted file")"
        return 1
    fi
    # Captured before matching, never piped into `grep`/`head`: an early-exiting reader leaves the writer to die
    # of SIGPIPE, which pipefail reports as a failed probe (see ai_tools_selinux_groups__is_loaded). Every read works
    # on this one string.
    header="$(head -n 20 "${fragment}" 2>/dev/null || true)"
    if ! grep -qxF -- "# ai-tools-admin-command: ${domain}" <<<"${header}"; then
        _admin_command_reason="$(coded_refusal MSG-H5F8 "no domain declaration: ${fragment} does not declare '# ai-tools-admin-command: ${domain}' in its first 20 lines -- reinstall the package that ships it")"
        return 1
    fi

    declared_floor="$(_admin_command_field "${header}" api-min-version)"
    if [[ -z "${declared_floor}" ]]; then
        _admin_command_reason="$(coded_refusal MSG-U6U6 "no interface floor: ${fragment} declares no '# ai-tools-admin-api-min-version: <major>.<minor>' -- reinstall the package that ships it")"
        return 1
    fi
    if [[ ! "${declared_floor}" =~ ^([0-9]+)\.([0-9]+)$ ]]; then
        _admin_command_reason="$(coded_refusal MSG-B5K9 "a malformed interface floor: ${fragment} declares $(printf '%q' "${declared_floor}"), which is not <major>.<minor>")"
        return 1
    fi
    declared_major="${BASH_REMATCH[1]}"; declared_minor="${BASH_REMATCH[2]}"
    if (( declared_major != base_major )); then
        _admin_command_reason="$(coded_refusal MSG-W3T9 "an incompatible contract: ${fragment} needs contributed-command interface ${declared_floor}, and this ai-tools-admin implements ${ADMIN_COMMAND_API} -- a different major is a different contract, so upgrade whichever of the two is behind")"
        return 1
    fi
    if (( declared_minor > base_minor )); then
        _admin_command_reason="$(coded_refusal MSG-M3D7 "an interface newer than this one: ${fragment} needs contributed-command interface ${declared_floor}, and this ai-tools-admin implements ${ADMIN_COMMAND_API} -- upgrade ai-tools-base")"
        return 1
    fi

    declared_verbs="$(_admin_command_field "${header}" verbs)"
    # One list grammar across this project: commas and whitespace both separate (conf.lib.sh).
    ai_tools_conf__split ADMIN_COMMAND_VERBS "${declared_verbs}"
    if [[ "${#ADMIN_COMMAND_VERBS[@]}" -eq 0 ]]; then
        _admin_command_reason="$(coded_refusal MSG-V5Q3 "no verb list: ${fragment} declares no '# ai-tools-admin-verbs: <verb> ...' -- a command that answers nothing is not one")"
        return 1
    fi
    for verb in "${ADMIN_COMMAND_VERBS[@]}"; do
        [[ "${verb}" =~ ^[a-z][a-z0-9-]*$ ]] && continue
        _admin_command_reason="$(coded_refusal MSG-E3G7 "a verb outside the command charset: ${fragment} declares $(printf '%q' "${verb}") among its verbs, and a verb is a bare lower-case word")"
        return 1
    done
    return 0
}

# _admin_command_field <header> <key>: the value of one `# ai-tools-admin-<key>: ` line, empty when absent. First
# occurrence wins, so a later line cannot quietly override the declaration a reader saw first. Works on the captured
# header string, so no read reaches the file again.
_admin_command_field() {
    local values
    values="$(sed -n "s/^# ai-tools-admin-$2:[[:space:]]*\\(.*[^[:space:]]\\)[[:space:]]*\$/\\1/p" <<<"$1")"
    printf '%s' "${values%%$'\n'*}"
}

# admin_command_has_verb <verb>: succeed when the verb list published by the last admin_command_check holds <verb>. Read
# after that call, never instead of it.
admin_command_has_verb() {
    local wanted="$1" verb
    for verb in "${ADMIN_COMMAND_VERBS[@]}"; do
        [[ "${verb}" == "${wanted}" ]] && return 0
    done
    return 1
}

# contributed_dispatch <name> [args...]: exec the fragment carrying <name>, with the remaining arguments. An exec rather
# than a source: the fragment keeps its own `set -euo pipefail`, its own root guard and its own logging, and cannot
# collide with this tool's function names.
#
# Membership of ADMIN_DOMAINS is the gate, so <name> is never interpolated into a path before it has matched
# a discovered domain. Reached only from the top-level default arm, which is what makes a base name unreachable here
# twice over: its own arm matched first, and admin_domains refuses a fragment claiming one.
contributed_dispatch() {
    local domain="$1"; shift
    ai_tools_help__read_admin_domains "${ADMIN_COMMANDS_DIR}"
    local known found=no help_command
    for known in "${AI_TOOLS_HELP__ADMIN_DOMAINS[@]}"; do
        [[ "${known}" == "${domain}" ]] && { found=yes; break; }
    done
    if [[ "${found}" != yes ]]; then
        help_command="$(ai_tools_help__format_help_command ai-tools-admin "${domain}")"
        reject_unknown_command MSG-N2A5 "unknown command: ${domain} -- run '${help_command}'"
    fi
    admin_commands_trusted \
        || die MSG-A3P2 "refusing every contributed command while ${ADMIN_COMMANDS_DIR} holds a file that is not root's alone (named above) -- ${ADMIN_COMMANDS_TAMPER_REMEDY}"
    admin_command_check "${domain}" || emit_coded die "${_admin_command_reason}"
    exec "${ADMIN_COMMANDS_DIR}/${domain}" "$@"
}

# The shared config grammar and sidecar handling that `system post-upgrade` drives, and the trust predicate every
# contributed command is vetted with. Required, not optional: a reconcile that silently skipped its merge would leave
# a shipped hook uninvoked while reporting success, and a dispatch that could not tell a trusted fragment from a planted
# one would exec whatever it found. Loaded BEFORE the other libraries, unlike them, because `--help` lists this host's
# contributed domains and that list is drawn through this predicate.
# shellcheck source=SCRIPTDIR/../../lib/ai-tools/conf.lib.sh
. "${CONF_LIB}" || die_unsourced "${CONF_LIB}"
# The hook-declaration merge `system post-upgrade` applies to a kept settings.json. Required for the same reason.
# shellcheck source=SCRIPTDIR/../../lib/ai-tools/settings-merge.lib.sh
. "${SETTINGS_MERGE_LIB}" || die_unsourced "${SETTINGS_MERGE_LIB}"
# The record stream `system post-upgrade --check` writes and the exit status every report here ends with. Required:
# a check that could not write its stream would exit 0 over a host it did not read. Sources records-base.lib.sh itself.
# shellcheck source=SCRIPTDIR/../../lib/ai-tools/records-tsv.lib.sh
. "${RECORDS_TSV_LIB}" || die_unsourced "${RECORDS_TSV_LIB}"

# Provider resolver: the manifest key behind each domain's summary line, and the enabled-integration list
# `system bootstrap --scope full` iterates. Optional at load and gated at each use -- without it every installed
# fragment still dispatches (its own presence is what decides that), a domain is listed without its summary,
# and full-scope bootstrap REFUSES rather than guessing which integrations this host runs.
# shellcheck source=SCRIPTDIR/../../lib/ai-tools/providers.lib.sh
source "${PROVIDERS_LIB}" 2>/dev/null || true

# Where an operator's shell finds an agent launcher, and the guard line that settles it. Required, like the operator
# lib: enrolment asks about that line and then writes it, so a missing library is a broken install rather than
# a question this tool can ask a different way. It reads the enabled agents through providers.lib.sh, so a host
# that could not load that resolver gets the `unknown` verdict and is asked with the stake unnamed.
# shellcheck source=SCRIPTDIR/../../lib/ai-tools/path-order.lib.sh
. "${PATH_ORDER_LIB}" || die_unsourced "${PATH_ORDER_LIB}"

# The command surface as data, the help rendered from it, the reading of a command line as a help request,
# and the contributed-domain discovery the dispatch and the help both read. Required: the dispatch cannot tell
# a contributed domain from an unknown command without it. Sources conf.lib.sh itself, so it loads after it.
# shellcheck source=SCRIPTDIR/../../lib/ai-tools/help.lib.sh
. "${HELP_LIB}" || die_unsourced "${HELP_LIB}"

# Executed, this administers a host and needs root. Sourced -- by tests/unit/admin-operator-add.sh, which drives one
# function with sudo stubbed -- it does not assert anything about the host and only defines, stopping at the matching
# guard that precedes the dispatch. Everything between the two is definitions, so the executed path still refuses
# a non-root caller before any action.
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    # A help request and `--version` read no host state and leave the host as it is, so they answer any caller here,
    # ahead of the root check: an operator meeting the tool gets the command surface rather than a refusal naming sudo
    # without saying what to run under it. ai_tools_help__resolve_request states what a help request is; a bare run is
    # one. A help request on an unknown path continues with the path as typed, so the arm that refuses it names it
    # under its own code, and one on a contributed domain continues to that domain, which answers its own help.
    help_status=0
    help_target="$(ai_tools_help__resolve_request ai-tools-admin "${ADMIN_COMMANDS_DIR}" -- "$@")" || help_status=$?
    case "${help_status}" in
        0)
            if [[ "${help_target}" == - ]]; then
                ai_tools_help__render ai-tools-admin "${ADMIN_COMMANDS_DIR}"
            else
                ai_tools_help__render_hint ai-tools-admin "${help_target}"
            fi
            exit 0 ;;
        2)  IFS=' ' read -ra help_words <<<"${help_target}"; set -- "${help_words[@]}" ;;
        3)  # The domain first, then its help flag ahead of every other word and of any `--`, so the fragment reads
            # the line as a help request whichever form asked: `help DOMAIN ...`, `--help DOMAIN ...`, `DOMAIN ... -h`.
            help_words=("${help_target}" --help)
            help_seen_domain=0 help_seen_delimiter=0 help_first=1
            for help_word in "$@"; do
                if (( help_seen_delimiter == 0 )); then
                    if [[ "${help_word}" == -- ]]; then help_seen_delimiter=1
                    elif (( help_first == 1 )) && [[ "${help_word}" == help ]]; then help_first=0; continue
                    elif [[ "${help_word}" == -h || "${help_word}" == --help ]]; then help_first=0; continue
                    elif (( help_seen_domain == 0 )) && [[ "${help_word}" == "${help_target}" ]]; then
                        help_seen_domain=1; help_first=0; continue
                    fi
                fi
                help_first=0
                help_words+=("${help_word}")
            done
            set -- "${help_words[@]}"
            unset help_word help_seen_domain help_seen_delimiter help_first ;;
    esac
    unset help_status help_target help_words
    if [[ "${1-}" == --version ]]; then printf 'ai-tools-admin %s\n' "${AI_TOOLS_VERSION}"; exit 0; fi
    [[ "${EUID}" -eq 0 ]] || die MSG-D7D4 "run as root (sudo)"
fi

# shellcheck source=SCRIPTDIR/../../lib/ai-tools/operator.lib.sh
. "${OPERATOR_LIB}" || die_unsourced "${OPERATOR_LIB}"

# Optional SELinux policy-group registry + predicates, shared with install-selinux.sh.
# shellcheck source=SCRIPTDIR/../../lib/ai-tools/selinux-groups.lib.sh
. "${SELINUX_GROUPS_LIB}" || die_unsourced "${SELINUX_GROUPS_LIB}"

# Shared yes/no prompt (ai_tools_msg__confirm; see msg.lib.sh). REQUIRED like the operator lib: a valid install ships
# it, so there is no fallback. Include-guarded, so a re-source is a no-op.
# shellcheck source=SCRIPTDIR/../../lib/ai-tools/msg.lib.sh
source /usr/local/lib/ai-tools/msg.lib.sh || die_unsourced /usr/local/lib/ai-tools/msg.lib.sh
# Fixed 80-column frame for any box this tool renders, aligned with the CLI's.
export AI_TOOLS_MSG_FULLWIDTH=1

# write_operators <name>...: set the OPERATORS list in operator.conf (root:root 644). Edits ONLY the OPERATORS line
# in an existing file, through the shared writer (ai_tools_conf__set_list, conf.lib.sh -- the grammar's owner,
# so the line replaced is the one every reader of the file matches), preserving every other setting the operator
# maintains there; seeds a minimal file when absent. 644: world-readable (the agent hooks and the root helpers both read
# it; it is free of secrets) and root-write-only, so the agent cannot rewrite the identity root hands files back to.
write_operators() {
    install -d -o root -g root -m 755 /etc/ai-tools
    if [[ ! -f "${OPERATOR_CONF}" ]]; then
        local tmp; tmp="$(mktemp)"
        printf '%s\n' "# ai-tools host configuration -- full reference: man 5 ai-tools-operator.conf" > "${tmp}"
        install -o root -g root -m 644 "${tmp}" "${OPERATOR_CONF}"
        rm -f "${tmp}"
    fi
    ai_tools_conf__set_list "${OPERATOR_CONF}" OPERATORS "$@" \
        || die MSG-N4H9 "could not write OPERATORS into ${OPERATOR_CONF} -- the enrolment is incomplete; check the file and re-run"
}

# in_list <name>: succeed when <name> is already in AI_TOOLS_OPERATOR__OPERATORS.
in_list() {
    local n; for n in "${AI_TOOLS_OPERATOR__OPERATORS[@]:-}"; do [[ "${n}" == "$1" ]] && return 0; done
    return 1
}

# seed_config_file <user> <group> <path> <seed-function>: place <path> from what <seed-function> prints, owned
# by the operator at 600. An existing file holds that operator's own edits and is left as it stands, so enrolment is
# idempotent. Returns non-zero when the file is not there afterwards, so a failed write reaches the caller
# as the refusal it makes rather than as a step that logged `seeding` and stopped.
seed_config_file() {
    local user="$1" group="$2" file="$3" seed="$4" tmp status=0
    [[ -f "${file}" ]] && return 0
    log "seeding ${file}"
    tmp="$(mktemp)"
    "${seed}" > "${tmp}" && install -o "${user}" -g "${group}" -m 600 "${tmp}" "${file}" || status=$?
    rm -f "${tmp}"
    (( status == 0 )) || warn MSG-P5C5 "could not seed ${file}"
    return "${status}"
}

# ensure_config_home <user> <group> <home>: make sure `<home>/.config` exists, since `~/.config/ai-tools` is seeded
# inside it and an account that has never logged in has no config home yet.
#
# An existing one is left as it stands: other applications keep their config there, and its mode is not this command's
# to decide. A missing one is created with `mkdir`, so the HOST's umask decides the mode -- the compliance answer
# for a directory this command only has to traverse, and a permissive umask leaves the access boundary where it already
# is, on the `700` `~/.config/ai-tools` inside it. Creating it asks first, default YES: the write lands in an account's
# home, and a run with no terminal takes the yes, which is what seeds an operator during an unattended install.
# `restorecon` gives it `config_home_t`, which root creating it inside the home does not.
#
# Returns non-zero when `.config` is absent afterwards, so the caller refuses the enrolment rather than reporting a seed
# that did not happen.
ensure_config_home() {
    local user="$1" group="$2" cfg_home="$3/.config"
    [[ -d "${cfg_home}" ]] && return 0
    if [[ -e "${cfg_home}" ]]; then
        warn MSG-Z6V4 "not a directory: ${cfg_home} -- ${user}'s ai-tools config cannot be seeded inside it"
        return 1
    fi
    log "${user} has no ${cfg_home}, which is where its ai-tools config directory goes"
    if ! ai_tools_msg__confirm "Create ${cfg_home}, owned by ${user}?" y; then
        warn MSG-B6P3 "declined: ${cfg_home} was not created, so ${user}'s ai-tools config cannot be seeded"
        return 1
    fi
    if ! mkdir "${cfg_home}"; then
        warn MSG-S9G3 "could not create ${cfg_home} as ${user}:${group}"
        return 1
    fi
    # A directory root owns is one the operator cannot write, so an ownership this could not set takes the empty
    # directory with it and the enrolment refuses on a home it did not change.
    if ! chown "${user}:${group}" "${cfg_home}"; then
        rmdir "${cfg_home}" 2>/dev/null || true
        warn MSG-U8E9 "could not give ${cfg_home} to ${user}:${group}; removed the directory again"
        return 1
    fi
    if command -v restorecon >/dev/null 2>&1; then restorecon "${cfg_home}" 2>/dev/null || true; fi
    log "created ${cfg_home} $(stat -c '%a %U:%G' "${cfg_home}" 2>/dev/null)"
}

# seed_operator_config <user>: create the config an operator keeps in ~/.config/ai-tools --
# `~/.config/ai-tools/project-registry` (the launch gate) and `~/.config/ai-tools/secret-patterns` (which basenames are
# credentials) -- at 600 inside a 700 directory, so the sandbox account, neither owner nor group member and unable
# to enter the directory, reads neither while the root helpers read both on the operator's behalf. This is the one place
# either file is created for an operator, and both headers come from conf.lib.sh, so an account enrolled on a packaged
# host gets what a from-source install writes.
#
# Each way it can come up short is reported where it happens and returns non-zero, so `op_add` decides what an enrolment
# missing its `~/.config/ai-tools/project-registry` becomes. The registry is the file ai_tools_conf__resolve_registry
# picks: a `~/.config/ai-tools/allowed-projects` an earlier release wrote stays the account's registry until
# `system post-upgrade` migrates it, so `~/.config/ai-tools/project-registry` is not seeded beside it, and the line
# names the command.
seed_operator_config() {
    local user="$1" home group cfg status=0
    home="$(getent passwd "${user}" | cut -d: -f6)"
    group="$(id -gn "${user}")"
    if [[ -z "${home}" || ! -d "${home}" ]]; then
        warn MSG-R7U6 "no home directory on this host for ${user}; skipping the config seed"
        warn "    create the home, then re-run: atr admin operators add ${user}"
        return 1
    fi
    ensure_config_home "${user}" "${group}" "${home}" || return 1
    cfg="${home}/.config/ai-tools"
    if [[ ! -d "${cfg}" ]] && ! install -d -o "${user}" -g "${group}" -m 700 "${cfg}"; then
        warn MSG-N2J8 "could not create ${cfg} at 700 ${user}:${group}"
        return 1
    fi
    local registry_state registry
    IFS=$'\t' read -r registry_state registry < <(ai_tools_conf__resolve_registry "${cfg}")
    if [[ "${registry_state}" == legacy ]]; then
        warn MSG-A2P2 "the registry ${registry} is the allowed-projects an earlier release wrote, so ~/.config/ai-tools/project-registry is not seeded beside it -- migrate it with: atr admin system post-upgrade"
    else
        seed_config_file "${user}" "${group}" "${registry}" ai_tools_conf__get_allowlist_seed || status=1
    fi
    seed_config_file "${user}" "${group}" "${cfg}/secret-patterns"   ai_tools_conf__get_secret_patterns_seed || status=1
    return "${status}"
}

# label_operator_config <user>: give that operator's ~/.config/ai-tools the ai_tools_conf_t SELinux type,
# through relabel.lib.sh. There is one rule per operator and enrolment is where this host learns an account is one,
# so the rule is registered here; what the type buys is in .claude/rules/confinement.rule.md. A re-run re-asserts it,
# which repairs an account whose rule a concurrent semanage transaction refused.
#
# BEST-EFFORT, and the two ways it declines are different facts. On a host with no SELinux layer there is no such type
# to apply -- the intended DAC-only deployment -- so the run stays silent. A host that has one and could not apply it is
# WARNED with the command that repairs it, and the enrolment still stands: the two facts that make an operator are
# already written, so a refusal here would leave an account in OPERATORS and in the group with no way to finish.
label_operator_config() {
    local user="$1" home cfg status=0
    home="$(getent passwd "${user}" 2>/dev/null | cut -d: -f6)"
    cfg="${home}/.config/ai-tools"
    [[ -n "${home}" && -d "${cfg}" ]] || return 0
    # shellcheck source=SCRIPTDIR/../../lib/ai-tools/relabel.lib.sh
    if ! source "${RELABEL_LIB}" 2>/dev/null \
            || ! declare -F ai_tools_relabel__label_operator_conf >/dev/null 2>&1; then
        warn MSG-H5N2 "cannot label ${cfg}: ${RELABEL_LIB} did not load -- reinstall ai-tools-base"
        return 0
    fi
    # Taken in this shell, not a subshell: the lock is an open descriptor (see relabel.lib.sh). It serializes this write
    # against the other helpers that write the same policy store.
    ai_tools_relabel__lock
    if [[ -n "${AI_TOOLS_RELABEL__LOCK_NOTE}" ]]; then
        warn MSG-G2C6 "proceeding without the relabel lock: ${AI_TOOLS_RELABEL__LOCK_NOTE}"
    fi
    ai_tools_relabel__label_operator_conf "${cfg}" || status=$?
    case "${status}" in
        0) log "labelled ${cfg} ${AI_TOOLS_RELABEL__OPERATOR_CONF_TYPE}" ;;
        2) : ;;   # no SELinux layer on this host -- nothing to label, and not a fault
        *) warn MSG-N3T7 "could not label ${cfg} ${AI_TOOLS_RELABEL__OPERATOR_CONF_TYPE}${AI_TOOLS_RELABEL__FCONTEXT_ERROR:+ -- ${AI_TOOLS_RELABEL__FCONTEXT_ERROR}}"
           warn "    until it carries that type the ownership handback no-ops for ${user}'s projects"
           warn "    repair: atr admin operators add ${user}" ;;
    esac
    return 0
}

# wire_init_file <file> <user> <group> [login-chain] : add the guard line to one bash init file, creating it owned
# by the account when it is absent. Idempotent -- a file already naming the fragment is left as it is. `login-chain`
# seeds a created file with the `. ~/.bashrc` block EL's skel carries, which the caller passes for ~/.bash_profile
# alone: bash reads that file by itself at login, so one holding only the guard line would leave a login shell without
# the account's own .bashrc, its nvm init among it. Top-level so tests/unit/admin-operator-add.sh drives it against its
# own fixture files, apart from the prompt in wire_dedup.
wire_init_file() {
    local f="$1" user="$2" group="$3" seed="${4-}"
    if [[ ! -e "${f}" ]]; then
        install -o "${user}" -g "${group}" -m 644 /dev/null "${f}" || return 1
        [[ "${seed}" == login-chain ]] && printf '%s\n' \
            "# Created by ai-tools-admin: read this account's .bashrc at login." \
            'if [ -f ~/.bashrc ]; then' '    . ~/.bashrc' 'fi' >> "${f}"
    fi
    if [[ "$(ai_tools_path_order__read_guard_state "${f}")" == yes ]]; then
        log "PATH ordering already wired in ${f}"; return 0
    fi
    grep -qF 'NVM_DIR' "${f}" \
        || log "note: NVM_DIR not found in ${f} -- the line still works, but it is meant to follow your nvm init"
    printf '\n# Added by ai-tools-admin: rank /usr/local/bin (the ai-tools wrappers) first on PATH.\n# Keep this AFTER anything that prepends to PATH, the nvm init included.\n%s\n' \
        "${AI_TOOLS_PATH_ORDER__GUARD}" >> "${f}"
    log "wired PATH ordering into ${f}"
}

# path_order_explain <user> <state> <launcher> <winner> The framed block that precedes the question, written
# for the state the account is in. Its whole job is to put the stake in front of the decision: what the answer settles
# is whether typing an agent's name reaches the sandbox at all, so the block names the binary that wins today
# and what running it would mean. A question that reads as shell-config housekeeping collects a no from the operator it
# protects. The renderer is ai_tools_msg__block, so the paths inside it stay copy-pasteable.
path_order_explain() {
    local user="$1" state="$2" launcher="$3" winner="$4"
    case "${state}" in
    shadowed)
        ai_tools_msg__block "PATH ordering: /usr/local/bin has to win" \
            "Typing ${launcher} in ${user}'s shell starts:" \
            "" \
            "    ${winner}" \
            "" \
            "That is an UNCONFINED agent: it runs as ${user}, with ${user}'s credentials and home, outside the project registry, the SELinux confinement and the ownership handback. The sandbox is reached only through the wrapper in ${AI_TOOLS_PATH_ORDER__WRAPPER_DIR}, so that directory has to come first on the PATH." \
            "" \
            "ai-tools ships one line that deduplicates the PATH and ranks the root-owned directories ahead of the nvm shims. It is appended after the nvm init, changes nothing else about the shell, and is undone by deleting it." ;;
    clear)
        ai_tools_msg__block "PATH ordering" \
            "Typing ${launcher} in ${user}'s shell reaches ${AI_TOOLS_PATH_ORDER__WRAPPER_DIR}/${launcher} -- the sandbox wrapper -- so the ordering is right as it stands." \
            "" \
            "The line below is what keeps it right. Without it, an agent installed under ${user}'s own nvm, an nvm init, or anything else that prepends to PATH puts an unconfined agent ahead of the wrapper, and nothing says so: the agent simply starts outside the sandbox." ;;
    *)
        ai_tools_msg__block "PATH ordering" \
            "This host cannot read where ${user}'s shell finds an agent launcher, so it cannot say whether typing one reaches the sandbox wrapper in ${AI_TOOLS_PATH_ORDER__WRAPPER_DIR} or an agent installed somewhere else on that PATH -- which would run unconfined, as ${user}." \
            "" \
            "The line below settles it either way: it deduplicates the PATH and ranks the root-owned directories first." ;;
    esac
}

# path_order_decline <user> <state> <launcher> <winner> <bashrc> <bashprof> What is said when the line is not written --
# declined, or a run with no terminal. On a SHADOWED account this is the last place the consequence can be stated, so it
# is a warning naming the binary that wins rather than a line reporting the step as skipped; on an account that already
# reaches the wrapper it is a note, since that account's sessions are sandboxed today. Either way it prints the line,
# to be added by hand.
path_order_decline() {
    local user="$1" state="$2" launcher="$3" winner="$4" bashrc="$5" bashprof="$6"
    if [[ "${state}" == shadowed ]]; then
        warn MSG-W7J6 "typing ${launcher} in ${user}'s shell runs ${winner}, not ${AI_TOOLS_PATH_ORDER__WRAPPER_DIR}/${launcher} -- that agent runs unconfined, as ${user}"
        warn "    add this line after the nvm init in ${bashrc} and ${bashprof}, or re-run: atr admin operators add ${user}"
        warn "    ${AI_TOOLS_PATH_ORDER__GUARD}"
        return 0
    fi
    log "PATH ordering not wired; add this line after the nvm init in ${bashrc} and ${bashprof}:"
    log "  ${AI_TOOLS_PATH_ORDER__GUARD}"
}

# wire_dedup <user> Settle where ${user}'s shell finds an agent launcher. It reads the account's ordering first
# (path-order.lib.sh), states what that reading means, and only then offers to source the PATH fragment from ~/.bashrc
# and ~/.bash_profile after their nvm init. That order carries the meaning: the question decides whether typing
# an agent's name reaches the sandbox wrapper or an agent of the same name elsewhere on the PATH, which runs unconfined
# as the operator -- so it is asked with the account's own state named, and declining it on a shadowed account warns
# instead of passing in silence.
#
# This wiring is the fragment's only delivery: it lives in the ai-tools lib dir rather than /etc/profile.d,
# so an unwired account keeps its stock PATH. The two files it names are what bash reads; an account that logs
# in through another shell is told where its own ordering stands. It edits the operator's home, so it asks first
# and never rewrites one in a run with no terminal, the `[[ -t 0 && -e /dev/tty ]]` branch deciding which.
# ai_tools_path_order__repoint is the one exception, and it follows a file this package moved rather than making
# a choice of its own.
wire_dedup() {
    local user="$1" home group login_shell bashrc bashprof
    local state launcher winner pair
    home="$(getent passwd "${user}" | cut -d: -f6)"
    login_shell="$(getent passwd "${user}" | cut -d: -f7)"
    group="$(id -gn "${user}")"
    [[ -n "${home}" && -d "${home}" ]] || return 0
    bashrc="${home}/.bashrc"; bashprof="${home}/.bash_profile"

    # A host upgraded from a release that shipped the fragment under its former name carries a guard line naming a file
    # this package moved, and that line succeeds without applying the ordering. Repoint before reading, so the state
    # this command reports and asks about is the repaired one.
    local repointed
    while IFS= read -r repointed; do
        [[ -n "${repointed}" ]] && log "repointed the PATH ordering line in ${repointed} to ${AI_TOOLS_PATH_ORDER__FRAGMENT}"
    done < <(ai_tools_path_order__repoint_user "${user}")

    ai_tools_path_order__read_user "${user}" || true
    state="${AI_TOOLS_PATH_ORDER__STATE:-unknown}"
    winner="${AI_TOOLS_PATH_ORDER__SHADOW:-}"
    # The launcher a message names is the shadowed one where there is one, and otherwise the first this host enables --
    # so the message is about a command the operator types.
    launcher=""
    for pair in "${AI_TOOLS_PATH_ORDER__WINNERS[@]+"${AI_TOOLS_PATH_ORDER__WINNERS[@]}"}"; do
        [[ -z "${launcher}" ]] && launcher="${pair%%=*}"
        [[ -n "${winner}" && "${pair#*=}" == "${winner}" ]] && { launcher="${pair%%=*}"; break; }
    done
    launcher="${launcher:-the agent}"

    # The two files it names govern bash. Another login shell reads its own, so the operator hears which ordering their
    # sessions actually get, at the moment the wiring is offered.
    case "${login_shell}" in
        */bash|'') ;;
        *) log "note: ${user}'s login shell is ${login_shell}, which reads its own init files rather than ${bashrc} or ${bashprof}"
           log "      rank ${AI_TOOLS_PATH_ORDER__WRAPPER_DIR} ahead of the nvm shims there too, so that typing ${launcher} reaches the ai-tools wrapper in that shell" ;;
    esac

    # Already wired and still shadowed: appending the line again edits the file and leaves the ordering as it was,
    # because what stands between this account and the wrapper is a later line that prepends to PATH. Report
    # that diagnosis instead of asking a question whose yes does not change the ordering.
    if [[ "${state}" == shadowed && "${AI_TOOLS_PATH_ORDER__WIRED:-no}" == yes ]]; then
        warn MSG-M2N9 "the PATH ordering line is already in ${user}'s shell init, and typing ${launcher} still runs ${winner}"
        warn "    something after that line prepends to PATH -- move it to the END of ${bashrc}, after the nvm init and anything else that touches PATH"
        return 0
    fi
    if [[ "${state}" == wired ]]; then
        log "PATH ordering: ${bashrc} already sources ${AI_TOOLS_PATH_ORDER__FRAGMENT}, and ${launcher} reaches the wrapper"
        return 0
    fi

    path_order_explain "${user}" "${state}" "${launcher}" "${winner}"
    if [[ -t 0 && -e /dev/tty ]]; then
        if ai_tools_msg__confirm \
            "Add the PATH ordering line to ${bashrc} and ${bashprof}?" y; then
            wire_init_file "${bashrc}"   "${user}" "${group}"
            wire_init_file "${bashprof}" "${user}" "${group}" login-chain
        else
            path_order_decline "${user}" "${state}" "${launcher}" "${winner}" "${bashrc}" "${bashprof}"
        fi
    else
        log "non-interactive: not editing shell init"
        path_order_decline "${user}" "${state}" "${launcher}" "${winner}" "${bashrc}" "${bashprof}"
    fi
}

# report_operator_role <user> -- say which of the two operator shapes this enrolment produced, where the decision is
# being made rather than where it first fails.
#
# This command writes both facts that make an operator (ai-ops membership, a name in OPERATORS) and CANNOT write
# the third thing a claim needs: a general sudo grant, which the host's own sudoers decides. Group membership cannot
# imply a sudoers rule, so the only way to know is to ask sudo -- and running as root, it can ask on the enrolled
# account's behalf without a password. The probe names ai-tools-lockdown because the secret gate is a claim's first
# sudo, so it answers the question the administrator actually has: can this account claim a project?
#
# An account without the grant is a supported shape, not a misconfiguration, so this reports and never refuses: it names
# the `--for` command that claims on the account's behalf.
#
# A non-zero answer is a refusal only while sudo is answering at all -- for a command no rule matches, `sudo -l` exits
# non-zero with EMPTY output, so there is no message separating that from a sudo which failed for its own reasons (an
# unreachable sudoers backend, a host that refuses `-l`). It is separated by a second probe, the same way the CLI's
# sudo_grant_missing does it: listing the account's whole rule set, which succeeds for anyone this command has just
# enrolled, since the %ai-ops rules apply to the membership written moments earlier (sudo reads the group database, not
# a cached credential). Only a first probe refused while the second answers is read as "no grant"; anything else is
# reported as undetermined, because a wrong verdict here is acted on immediately.
report_operator_role() {
    local user="$1"
    local claim_helper="/usr/local/libexec/ai-tools/ai-tools-lockdown"
    if ! command -v sudo >/dev/null 2>&1; then
        log "${user}: no sudo on this host, so no project can be claimed by anyone -- ${user} can still launch agent sessions"
        return 0
    fi
    if LC_ALL=C sudo -l -U "${user}" "${claim_helper}" >/dev/null 2>&1; then
        log "${user} holds a general sudo grant: it can claim projects as well as launch sessions"
        return 0
    fi
    if ! LC_ALL=C sudo -l -U "${user}" >/dev/null 2>&1; then
        log "${user}: sudo did not answer, so whether ${user} holds a general sudo grant is undetermined -- check with: sudo -l -U ${user}"
        return 0
    fi
    log "${user} holds no general sudo grant: it can launch agent sessions and read the reports, and cannot claim a project"
    log "${user}: claim on its behalf from an operator that holds one -- atr projects claim --for ${user} <path>"
}

op_add() {
    local user="${1:-${SUDO_USER:-}}"
    [[ -n "${user}" ]] || reject MSG-H2P9 "operators add: name a user, or run it through sudo so SUDO_USER is set"
    [[ "${user}" != "${SANDBOX_USER}" ]] || die MSG-C9C4 "an operator must not be the sandbox account ${SANDBOX_USER}"
    [[ "${user}" != "root" ]]            || die MSG-H3M6 "an operator must be a normal login user, not root"
    # The name is held to the portable set before `id` reads it, so a word `id` would take as an option (`--version`) is
    # refused here and not run.
    ai_tools_conf__is_portable_name_valid "${user}" || reject MSG-J2S8 "operators add: not an account name: ${user}"
    id "${user}" &>/dev/null || die MSG-U8T8 "no such user: ${user}"

    ai_tools_operator__load_operators || true   # tolerate an unenrolled host (empty list)
    local enrolled=0
    if in_list "${user}"; then
        enrolled=1
        log "${user} is already an operator; reconciling group, config, and sandbox linger"
    fi

    # The config is seeded BEFORE either fact that makes an operator, so an enrolment either completes or leaves
    # the host as it found it. An account in `OPERATORS` and in `ai-ops` whose `~/.config/ai-tools/project-registry`
    # does not exist is refused at every launch, and does not resolve an owner in any root helper, while the enrolment
    # reports success -- so the refusal belongs here, ahead of the first write, where a re-run then enrols the account
    # in full.
    #
    # A RECONCILING re-run carries on instead: the record already stands, this run did not write it, and withdrawing
    # an operator is `operators remove`'s decision. It repairs what it can, reports what it could not, and exits
    # non-zero at the close.
    local seeded=0
    seed_operator_config "${user}" || seeded=1
    if (( seeded && ! enrolled )); then
        die MSG-T4X9 "not enrolled: ${user}'s config in ~/.config/ai-tools could not be seeded, and without ~/.config/ai-tools/project-registry it cannot start a session or claim a project -- clear the reason above and re-run"
    fi

    if (( ! enrolled )); then
        local newlist=()
        [[ "${#AI_TOOLS_OPERATOR__OPERATORS[@]}" -gt 0 ]] && newlist=( "${AI_TOOLS_OPERATOR__OPERATORS[@]}" )
        newlist+=( "${user}" )
        write_operators "${newlist[@]}"
        log "added ${user} to OPERATORS"
    fi

    # ai-ops membership: the sudoers grant and the launch wrapper gate on it. The sandbox account is never a member (it
    # must not be able to drive itself as an operator). Add -- and log -- only when the user is not already a member,
    # so a reconciling re-run does not report a change it did not make.
    if id -nG "${user}" 2>/dev/null | tr ' ' '\n' | grep -qx "${OPERATORS_GROUP}"; then
        log "${user} is already in group ${OPERATORS_GROUP}"
    else
        usermod -aG "${OPERATORS_GROUP}" "${user}" || die MSG-C4A8 "failed to add ${user} to ${OPERATORS_GROUP}"
        log "added ${user} to group ${OPERATORS_GROUP}"
    fi

    label_operator_config "${user}"

    # The sandbox account needs a `systemd --user instance` without an interactive login: its nvm-update timer and each
    # ai-tools-run session unit run there, and it has no login shell, so only linger keeps that instance alive.
    # An operator runs claude from its own active login, so it does not need linger here; enabling operator linger
    # for other reasons is host policy.
    log "enabling linger for ${SANDBOX_USER}"
    loginctl enable-linger "${SANDBOX_USER}"  2>/dev/null \
        || warn MSG-Q4K4 "could not enable linger for ${SANDBOX_USER}"

    wire_dedup "${user}"
    log "operator ${user} added"
    report_operator_role "${user}"
    # ai-ops membership applies to NEW login sessions; an already-open shell keeps the credential set it had at login,
    # and the launch wrapper gates on that live set. Name the activation step so the operator's first claude launch does
    # not hit the stale-session refusal.
    log "${user}: start a new login session (or run 'newgrp ${OPERATORS_GROUP}') before launching claude -- ${OPERATORS_GROUP} membership does not apply to already-open shells"
    # Only a reconciling re-run reaches this: a first enrolment already refused. Said last because the reason was
    # reported several steps and one prompt back, and non-zero so an unattended caller reads it without parsing
    # the output.
    if (( seeded )); then
        warn MSG-P9K2 "config not seeded for ${user}, which stays enrolled -- without ~/.config/ai-tools/project-registry it cannot start a session or claim a project"
        warn "    re-run once the reason above is cleared: atr admin operators add ${user}"
        exit 1
    fi
}

op_remove() {
    local user="${1:-}"
    [[ -n "${user}" ]] || reject MSG-S3E9 "operators remove: name the user to withdraw"
    ai_tools_operator__load_operators || true
    if ! in_list "${user}"; then
        log "${user} is not an operator; nothing to remove"
        return 0
    fi
    local kept=() n
    for n in "${AI_TOOLS_OPERATOR__OPERATORS[@]}"; do [[ "${n}" == "${user}" ]] || kept+=("${n}"); done
    write_operators "${kept[@]}"
    log "removed ${user} from OPERATORS"
    # Drop ai-ops membership; leave the account's own allowlist and secret patterns (their data).
    gpasswd -d "${user}" "${OPERATORS_GROUP}" >/dev/null 2>&1 \
        || warn MSG-J9F9 "could not remove ${user} from ${OPERATORS_GROUP}"
    log "removed ${user} from group ${OPERATORS_GROUP}"
}

op_list() {
    if ai_tools_operator__load_operators; then
        printf '%s\n' "${AI_TOOLS_OPERATOR__OPERATORS[@]}"
    else
        log "no operators configured"
    fi
}

# ── selinux groups: optional policy-group management ─────────────────────────────────
# These load/unload the COMPILED ai_tools_<group>.pp staged in the package directory; the group set and text come
# from selinux-groups.lib.sh. Distinct from selinux/install-selinux.sh, which compiles a group from source in a repo
# checkout -- this runs on any installed host.

# require_selinux: guard shared by every selinux command. Returns 1 (caller exits 0 -- no policy to manage) when SELinux
# is disabled; dies when semodule is absent (a real gap).
require_selinux() {
    if [[ "$(getenforce 2>/dev/null)" == "Disabled" ]]; then
        log "SELinux is disabled on this host -- no policy groups to manage"
        return 1
    fi
    command -v semodule >/dev/null 2>&1 || die MSG-A8Z7 "semodule not found -- install policycoreutils"
    return 0
}

# _sel_require_known_group <name>: refuse a name the registry does not carry, showing what this host does know. One
# situation for both verbs -- a name that is not a group is the same refusal whether it was to be loaded or unloaded,
# so the two arms share it rather than each writing it.
_sel_require_known_group() {
    ai_tools_selinux_groups__is_valid "$1" && return 0
    log "unknown group '$1'. Available groups:"; _selinux_usage_groups
    die MSG-D7Y7 "no such policy group: $1"
}

# _selinux_usage_groups: list the known groups (name + description) to stderr.
_selinux_usage_groups() {
    local entry
    for entry in "${AI_TOOLS_SELINUX_GROUPS__REGISTRY[@]}"; do
        printf '    %-10s %s\n' \
            "$(ai_tools_selinux_groups__get_name "${entry}")" \
            "$(ai_tools_selinux_groups__get_description "${entry}")" >&2
    done
}

# sel_enable <name>...: load each named group in turn. Several names are accepted because a toolchain declares the set
# it needs (selinux_groups in its manifest, ai-tools-providers(5)) and the status nudge prints that set as one command;
# each name is validated before any is loaded, so a typo refuses the whole command rather than loading half of it.
sel_enable() {
    [[ $# -ge 1 ]] || reject MSG-H7J4 "selinux groups enable: name the group(s) to load"
    require_selinux || return 0
    local name
    for name in "$@"; do
        [[ -n "${name}" && "${name}" != -* ]] || reject MSG-Q3Z5 "selinux groups enable: '${name}' is not a group name"
        _sel_require_known_group "${name}"
    done
    for name in "$@"; do _sel_enable_one "${name}"; done
}

_sel_enable_one() {
    local name="$1"
    if ai_tools_selinux_groups__is_loaded "${name}"; then
        log "group '${name}' is already loaded -- nothing to do"
        return 0
    fi
    # Experimental groups are unaudited drafts and are NOT on the shipped set. This tool loads only shipped, stable
    # modules; an experimental group must be compiled and verified against a real workload from a source checkout first
    # (install-selinux.sh does both), because it widens the sandbox domain's access beyond the repo-only core. Point
    # the operator there rather than loading an unaudited module.
    if ai_tools_selinux_groups__is_experimental "${name}"; then
        ai_tools_msg__warn MSG-C4F5 \
            "The '${name}' SELinux policy group is an EXPERIMENTAL, unaudited draft. It is not shipped prebuilt and cannot be enabled from here -- it widens the sandbox domain's access beyond the repo-only core and must be compiled and verified against a real workload from a source checkout first."
        log "compile, audit under permissive, and load it from a repo checkout:"
        log "    sudo selinux/install-selinux.sh enable-group ${name}"
        log "    (then re-run the bring-up loop in selinux/avc/ before relying on it)"
        log "docs: selinux/README.md, \"Optional policy groups\""
        die "'${name}' is experimental -- verify and enable it from source (see above)"
    fi
    local pp="${AI_TOOLS_SELINUX_GROUPS__PACKAGE_DIR}/ai_tools_${name}.pp"
    [[ -f "${pp}" ]] || die MSG-S5P4 "compiled module ${pp} not found -- reinstall ai-tools-selinux, or from a checkout: sudo selinux/install-selinux.sh build"
    # A former module still loaded from before this group was renamed or split out of it goes in the same transaction,
    # together with every OTHER current group that former module's rules became, so the host never holds both rule sets,
    # never loses a capability the old module carried, and a failed load leaves the old module in place.
    local former sibling
    local -a swap_args=( -i "${pp}" )
    if former="$(ai_tools_selinux_groups__get_former_module "${name}" 2>/dev/null)" \
            && ai_tools_selinux_groups__is_module_loaded "${former}"; then
        while IFS= read -r sibling; do
            [[ -n "${sibling}" && "${sibling}" != "${name}" ]] || continue
            [[ -f "${AI_TOOLS_SELINUX_GROUPS__PACKAGE_DIR}/ai_tools_${sibling}.pp" ]] || continue
            swap_args+=( -i "${AI_TOOLS_SELINUX_GROUPS__PACKAGE_DIR}/ai_tools_${sibling}.pp" )
        done < <(ai_tools_selinux_groups__list_groups_from_former_module "${former}")
        log "replacing the loaded '${former}' module with the group(s) its rules became"
        semodule -r "${former}" "${swap_args[@]}" || die MSG-M8X2 "semodule failed to replace ${former}"
    else
        log "loading group: ai_tools_${name}"
        semodule "${swap_args[@]}" || die MSG-W7T4 "semodule failed to load ${pp}"
    fi
    _restore_group_static_labels
    log "group '${name}' enabled"
    log "re-run the SELinux bring-up loop (selinux/avc/) to catch any new denials from the"
    log "expanded surface before relying on it under enforcing"
}

sel_disable() {
    local name="${1:-}"
    [[ $# -le 1 ]] || reject MSG-W8A7 "selinux groups disable: one group name at a time"
    [[ -n "${name}" && "${name}" != -* ]] || reject MSG-P2T5 "selinux groups disable: name the group to unload"
    require_selinux || return 0
    _sel_require_known_group "${name}"
    if ai_tools_selinux_groups__is_loaded "${name}"; then
        semodule -r "ai_tools_${name}" || die MSG-Q6F4 "semodule failed to remove ai_tools_${name}"
        _restore_group_static_labels
        log "group '${name}' disabled"
    else
        log "group '${name}' is not loaded -- nothing to do"
    fi
}

# _restore_group_static_labels: after a group is loaded or unloaded, restore the labels its static file contexts decide.
# The one tree a group's .fc names is the sandbox-clone area (the dotnet group maps a clone's build output
# to ai_tools_project_build_t), so that is what is restored; a claimed project's build rule is a per-project local rule
# written at claim time and is not the group's.
#
# A clone is a tree the sandbox account can write, so the walk runs through ai-tools-relabel-tree, the descriptor-pinned
# walker, which labels the entry it holds open and so walks whatever process of that account is live (cli.rule.md,
# ref-section-e6c6), at the canonical path realpath resolves, since the walker refuses a root whose path is not the one
# it reads back. The group change stands either way: a walk that left hardlinked paths at their type is a log line
# naming them, and a path realpath cannot resolve or any other non-zero exit is a warning naming the walk to re-run.
_restore_group_static_labels() {
    local clones=/var/opt/ai-tools/sandbox-projects dir lines rc=0 kept
    [[ -d "${clones}" ]] || return 0
    if ! dir="$(realpath -e -- "${clones}" 2>/dev/null)"; then
        warn MSG-S8K7 "could not restore labels under ${clones} -- realpath cannot resolve it; once it resolves, run: sudo /usr/local/libexec/ai-tools/ai-tools-relabel-tree -- ${clones}"
        return 0
    fi
    lines="$(/usr/local/libexec/ai-tools/ai-tools-relabel-tree -- "${dir}" 2>&1 >/dev/null)" || rc=$?
    kept="$(grep -v '^summary ' <<<"${lines}" | tr '\n' ';' | sed 's/;$//')"
    case "${rc}" in
        0) ;;
        4) log "the walk over ${dir} left these hardlinked entries at their type: ${kept:-the walker printed no detail}" ;;
        *) warn MSG-C9W9 "could not restore labels under ${dir} -- ${kept:-ai-tools-relabel-tree exited ${rc}}; run: sudo /usr/local/libexec/ai-tools/ai-tools-relabel-tree -- ${dir}" ;;
    esac
    return 0
}

# sel_list is a read-only REPORT, not operational output, so it renders as a plain section (like the CLI's
# `providers`/`projects list`) instead of `log`'s per-line `ai-tools-admin:` prefix. The core module uses the same
# bracketed [LOADED]/[disabled] state column as the group rows for one legend.
sel_list() {
    require_selinux || return 0
    local core_state='[disabled]' modules
    # Captured, not piped into `grep -q`: an early-exiting reader makes semodule die of SIGPIPE and pipefail then
    # reports the probe failed -- see ai_tools_selinux_groups__is_loaded.
    modules="$(semodule -l 2>/dev/null || true)"
    grep -qx 'ai_tools' <<<"${modules}" && core_state='[LOADED]  '
    printf '\nSELinux policy groups\n\n'
    printf '  %s core module (ai_tools) -- the confinement domain; DAC-only when disabled\n\n' "${core_state}"
    printf '  optional groups (all default: disabled)\n'
    local entry name desc stability state
    for entry in "${AI_TOOLS_SELINUX_GROUPS__REGISTRY[@]}"; do
        name="$(ai_tools_selinux_groups__get_name "${entry}")"
        desc="$(ai_tools_selinux_groups__get_description "${entry}")"
        stability="$(ai_tools_selinux_groups__get_stability "${entry}")"
        if ai_tools_selinux_groups__is_loaded "${name}"; then state='[LOADED]  '; else state='[disabled]'; fi
        printf '    %s %-9s %-15s %s\n' "${state}" "${name}" "(${stability})" "${desc}"
    done
    printf '\n  toggle       : atr admin selinux groups enable <name> | disable <name>\n'
    printf '  experimental : not shipped prebuilt -- enable from a source checkout with\n'
    printf '                 sudo selinux/install-selinux.sh enable-group <name>\n'
}

# ── system bootstrap: provision the sandbox account and its toolchain ────────────────────────
# Execs the provisioning helper, which creates the ai-tools account and its /opt/ai-tools home if they are absent,
# installs nvm and Node as that account, then installs each enabled agent's npm package and points its launcher
# at the result. The helper keeps its own name, path and output; what this command changes is how an administrator
# reaches it.
#
# The provisioning logic stays a separate file rather than moving in here: it is long, runs unattended
# from the container selftest, and reaches the network, none of which this dispatcher
# does.
#
# Scope defaults to the MINIMUM that works, and a bare run takes that default: the toolchain and the enabled agents,
# which is what a first host needs. `--scope full` also reaches every enabled integration, through each one's own
# contributed `bootstrap` -- which is why full scope needed the admin-commands seam before it could exist. It is spelled
# as a switch rather than a positional word because every other verb here takes a resource identifier in that slot.
system_bootstrap() {
    local scope=minimal
    local -a helper_args=()
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --scope)
                [[ $# -ge 2 ]] || reject MSG-V5J3 "system bootstrap: --scope takes a value (minimal|full)"
                scope="$2"; shift 2 ;;
            # The unattended form of the agent choice the helper otherwise asks for; the helper checks each name
            # against the installed manifests before it writes anything, so the value passes through as typed.
            --agents)
                [[ $# -ge 2 ]] || reject MSG-X8K6 "system bootstrap: --agents takes a value (NAME[,NAME...])"
                helper_args+=(--agents "$2"); shift 2 ;;
            *)  reject MSG-H5Z4 "system bootstrap: unknown argument '$1' (--scope minimal|full, --agents NAME[,NAME...])" ;;
        esac
    done
    case "${scope}" in
        minimal|full) ;;
        *) reject MSG-A8G5 "system bootstrap: unknown scope '${scope}' (minimal|full)" ;;
    esac
    [[ -x "${BOOTSTRAP_BIN}" ]] \
        || die MSG-D9W6 "the provisioning helper is not installed: ${BOOTSTRAP_BIN} -- install ai-tools-integration-nodejs"
    # Minimal scope has no step after the helper, so it hands the process over rather than wrapping it: the helper's
    # exit status is this command's, unmediated.
    [[ "${scope}" == full ]] || exec "${BOOTSTRAP_BIN}" "${helper_args[@]+"${helper_args[@]}"}"
    "${BOOTSTRAP_BIN}" "${helper_args[@]+"${helper_args[@]}"}" || die MSG-Y6F3 "the toolchain bootstrap failed -- no integration was reached"
    bootstrap_integrations
}

# bootstrap_integrations: run the `bootstrap` of each ENABLED integration that contributes one. Enablement, not
# installation, is what "everything this host runs" means, so this reads the enabled set where the dispatch reads
# the installed one.
#
# Every integration is attempted and each failure named before the command exits non-zero: an administrator provisioning
# a host wants every outcome, not the first one that went wrong. An enabled integration that does not contribute
# a command fragment is reported and skipped -- base cannot assume any particular package is installed.
bootstrap_integrations() {
    declare -F ai_tools_providers__list_enabled_integrations >/dev/null 2>&1 \
        || die MSG-Z4D6 "the provider resolver is unavailable, so the enabled integrations cannot be resolved -- the toolchain itself is provisioned; re-run without --scope to confirm"
    local -a enabled=()
    mapfile -t enabled < <(ai_tools_providers__list_enabled_integrations)
    if [[ "${#enabled[@]}" -eq 0 ]]; then
        log "no integration is enabled in ${OPERATOR_CONF} -- nothing further to provision"
        return 0
    fi
    ai_tools_help__read_admin_domains "${ADMIN_COMMANDS_DIR}"
    admin_commands_trusted \
        || die MSG-D9U7 "refusing every contributed command while ${ADMIN_COMMANDS_DIR} holds a file that is not root's alone (named above); the toolchain itself is provisioned -- ${ADMIN_COMMANDS_TAMPER_REMEDY}"
    local integration known known_domain failed=0
    for integration in "${enabled[@]}"; do
        known=no
        for known_domain in "${AI_TOOLS_HELP__ADMIN_DOMAINS[@]}"; do
            [[ "${known_domain}" == "${integration}" ]] && { known=yes; break; }
        done
        if [[ "${known}" != yes ]]; then
            log "${integration}: contributes no command -- nothing to provision"
            continue
        fi
        # The same gates an administrator's own `ai-tools-admin <domain> bootstrap` passes, so a fragment reached
        # from here is vetted exactly as one that is typed.
        if ! admin_command_check "${integration}"; then
            emit_coded warn "${_admin_command_reason}" "${integration}: "
            failed=1
            continue
        fi
        # Read from the declaration rather than learned by running it: an integration whose commands are something other
        # than provisioning has no work here, and running its fragment to find that out would report a rejected command
        # line as a failed provision.
        if ! admin_command_has_verb bootstrap; then
            log "${integration}: declares no bootstrap verb -- nothing to provision"
            continue
        fi
        log "provisioning the ${integration} integration"
        if ! "${ADMIN_COMMANDS_DIR}/${integration}" bootstrap; then
            warn MSG-J2Y8 "provisioning failed -- ${integration}: its bootstrap failed, with the cause above and in its own log"
            failed=1
        fi
    done
    (( failed == 0 )) || die MSG-V8V7 "one or more integrations did not provision"
}

# ── system entrypoints relabel: reconcile each enabled agent's entrypoint ─────────────────────
# Two steps in the helper, in this order: VERIFY the binary against the checksum its vendor signed and record it
# in that agent's pin, which ai-tools-run compares the binary against at launch; then RELABEL it to ai_tools_exec_t,
# which an nvm-update leaves as bin_t so the domain transition stops firing. Takes no path -- the helper resolves
# the entrypoints from the agent manifests.
entrypoints_relabel() {
    [[ $# -eq 0 ]] || reject MSG-A4J6 "system entrypoints relabel: takes no arguments"
    [[ -x "${RELABEL_ENTRYPOINT_BIN}" ]] \
        || die MSG-F5R8 "the entrypoint relabel helper is not installed: ${RELABEL_ENTRYPOINT_BIN} -- install ai-tools-integration-nodejs"
    log "reconciling the agent entrypoints (verify, then relabel)"
    # Cleared, not merely left unset: the guarantee that this command re-fetches the vendor's signed manifest holds
    # however it was invoked, rather than resting on sudo scrubbing the environment (updater.rule.md). The unattended
    # callers set it themselves.
    unset AI_TOOLS_ENTRYPOINT_PIN_REUSE
    exec "${RELABEL_ENTRYPOINT_BIN}"
}

# ── system post-upgrade: reconcile the .rpmnew files an upgrade leaves ─────────────────────── rpm keeps
# an operator-modified %config(noreplace) file and parks the package's copy beside it as <file>.rpmnew. Choosing
# between the two is a judgement call about the operator's own configuration, so no scriptlet makes it: this is
# the explicit, interactive command that does, and it is what the install output points at. Every treatment confirms
# first, backs the file up before writing, and names each path it touched. A .rpmnew survives the run, whichever
# treatment it got: each one leaves part of the reconciliation to the operator, and the copy is the baseline that edit
# is made from, so each block closes by naming the file to remove once the merge is done.
#
# The treatment follows the file's CONTENT, rather than one generic merge covering all three:
#   json    hook DECLARATIONS merge additively -- they are control plane, and a declaration the
#           file lacks means a shipped hook installs but no event invokes it. The permission arrays
#           are the host's and stay exactly as written (claude-settings.rule.md).
#   keyval  reported, never rewritten. An absent key already means its default, so a stale file
#           costs knowledge rather than behaviour, and its layout is the operator's own prose. Its
#           merge opens <copy>-merge, the package copy with the file's own values carried in.
#   review  shown only. A tool does not merge the sudo grant.
#   show    a file the registry does not name: named with the command that compares it, never printed, since
#           a kept config of another package may hold a credential (endpoints/typesafe.conf holds an API key).
readonly -a POSTUPGRADE_FILES=(
    "/opt/ai-tools/.claude/settings.json|json|Claude Code settings"
    "/etc/ai-tools/operator.conf|keyval|host options"
    "/etc/sudoers.d/ai-tools|review|sudoers grant"
)
# The directories the packages of this stack ship a kept config file into. POSTUPGRADE_FILES is base's and cannot name
# what an agent or integration package ships, so a .rpmnew under one of these that the registry does not name is found
# here and reported: a KEY=value `*.conf` with the keyval treatment, anything else with `show`.
readonly -a POSTUPGRADE_DIRS=(/etc/ai-tools /etc/codex /opt/ai-tools/.claude /opt/ai-tools/.codex)

# AI_TOOLS_POSTUPGRADE_ROOT prefixes every path in that registry, so the test suite drives this command against fixtures
# in its own /tmp testdir instead of the live host's control plane. It is a root-only test hook (tests.rule.md,
# ref-section-b4y9), unset in production, where the registry paths are absolute.

# _pu_diff <deployed> <rpmnew> [<label> <label>]: show what the package would change, indented, each side named by its
# label where the files compared are temporary copies. Colourized through colordiff when the host has it AND stdout is
# a terminal: colordiff is an EPEL package on RHEL, so it is used where present and never depended on, and the terminal
# test keeps escape sequences out of a redirected run, the way every other message this project prints degrades
# when piped. diff(1) is optional too -- without it the report continues and only the difference itself is missing.
_pu_diff() {
    local differ=diff
    local -a labels=()
    command -v diff >/dev/null 2>&1 \
        || { warn MSG-Q4S8 "diff is not installed, so the difference is not shown here -- install diffutils"; return 0; }
    [[ -t 1 ]] && command -v colordiff >/dev/null 2>&1 && differ=colordiff
    [[ -n "${3-}" ]] && labels=(--label "$3" --label "$4")
    "${differ}" -u "${labels[@]}" "$1" "$2" 2>/dev/null | sed 's/^/    /' || true
}

# _pu_say <level> <text>: one report line about the file being reconciled, prefixed by that file's name (_PU_NAME)
# so a line names what it is about. The level says whether the operator has anything to do: `ok` is a green check,
# `info` plain, `act` yellow -- something to carry over or decide -- and `err` red. Colour only on a terminal, like
# the CLI's own report, so a captured run stays plain text.
if [[ -t 1 ]]; then
    readonly _PU_GRN=$'\033[32m' _PU_YEL=$'\033[33m' _PU_RED=$'\033[31m' _PU_DIM=$'\033[2m' _PU_RST=$'\033[0m'
else
    readonly _PU_GRN='' _PU_YEL='' _PU_RED='' _PU_DIM='' _PU_RST=''
fi
_PU_NAME=""
# The lines that ask the operator to act, counted so the closing line can say whether anything needs review,
# and the errors among them, which colour that line red rather than yellow.
_PU_ATTENTION=0
_PU_ERRORS=0
_pu_say() {
    local level="$1" text="$2"
    [[ "${level}" == act || "${level}" == err ]] && _PU_ATTENTION=$(( _PU_ATTENTION + 1 ))
    [[ "${level}" == err ]] && _PU_ERRORS=$(( _PU_ERRORS + 1 ))
    case "${level}" in
        ok)  printf '  %s: %s✓%s %s\n' "${_PU_NAME}" "${_PU_GRN}" "${_PU_RST}" "${text}" ;;
        act) printf '  %s:   %s%s%s\n' "${_PU_NAME}" "${_PU_YEL}" "${text}" "${_PU_RST}" ;;
        err) printf '  %s:   %s%s%s\n' "${_PU_NAME}" "${_PU_RED}" "${text}" "${_PU_RST}" ;;
        *)   printf '  %s:   %s\n' "${_PU_NAME}" "${text}" ;;
    esac
}

# _pu_leave <rpmnew> [merged]: close a file's block by naming what is left to do with the copy. This function prints,
# and is the only thing any treatment does about the .rpmnew, so the copy survives every run. Each treatment leaves part
# of the reconciliation to the operator -- the permission rules here, the whole edit for a KEY=value file, the adoption
# of a sudo grant -- and the copy is the only record of what the package shipped, so deleting it would take away
# the baseline that edit is made from. It is the operator's file to remove, once the merge they wanted is in place.
# `merged` says nothing is left to carry over, so the removal is all that remains and its command is printed.
_pu_leave() {
    local rpmnew="$1"
    if [[ "${2:-}" == merged ]]; then
        _pu_say ok "nothing is left to carry over -- remove the copy when you are ready:"
        _pu_say info "  sudo rm ${rpmnew}"
    else
        if [[ -f "${rpmnew}-merge" ]]; then
            _pu_say act "carry over what you want by hand, then remove ${rpmnew} and ${rpmnew}-merge"
        else
            _pu_say act "carry over what you want by hand, then remove ${rpmnew}"
        fi
    fi
}

# _pu_installed_day: the day this command's own file was installed, as YYYYMMDD. A copy dated before it is
# from an earlier installation. Compared by day because rpm gives every file of one package its build time, and files
# built in one run can differ by seconds.
_pu_installed_day() { date -r "${BASH_SOURCE[0]}" +%Y%m%d 2>/dev/null; }

# _pu_provenance <copy>: the one line under a file's headline, naming the copy, which route left it (the package's
# .rpmnew, or the installer's .shipped from a from-source install) and its date. A copy dated before this installation
# -- a from-source install after an RPM one, or a copy left from an earlier upgrade -- is an earlier version's template,
# which is said beside it.
_pu_provenance() {
    local copy="$1" dated day installed route="package copy"
    [[ "${copy}" == *.shipped ]] && route="installer copy"
    dated="$(date -r "${copy}" +%Y-%m-%d 2>/dev/null)" || dated="an unknown date"
    day="${dated//-/}"
    installed="$(_pu_installed_day)" || installed=""
    if [[ -n "${installed}" && "${day}" =~ ^[0-9]{8}$ && "${day}" < "${installed}" ]]; then
        printf '%s: %s, dated %s -- older than this installation, so an earlier version'"'"'s template' \
            "${route}" "${copy}" "${dated}"
    else
        printf '%s: %s, dated %s' "${route}" "${copy}" "${dated}"
    fi
}

# _pu_json <deployed> <rpmnew>: merge the hook declarations the deployed file does not carry. The addition list comes
# from running the merge on a THROWAWAY COPY first, so what the operator confirms is the exact set the real merge adds
# rather than a promise of one, and a merge that would fail says so before the real file is touched.
_pu_json() {
    local deployed="$1" rpmnew="$2" scratch status=0
    ai_tools_settings_merge__require_jq \
        || { warn MSG-A5Z7 "jq is missing, so this file's JSON cannot be read -- merge it by hand"
             _PU_ATTENTION=$(( _PU_ATTENTION + 1 )); return 0; }

    scratch="$(mktemp -d)" || return 0
    cp -p "${deployed}" "${scratch}/probe" 2>/dev/null || { rm -rf "${scratch}"; return 0; }
    ai_tools_settings_merge__merge_hook_declarations "${scratch}/probe" "${rpmnew}" || status=$?
    rm -rf "${scratch}"

    case "${status}" in
    1)  _pu_say ok "hook declarations are already current"
        _pu_settings_rest "${deployed}" "${rpmnew}"
        return 0 ;;
    2)  warn MSG-Q4F6 "cannot merge the hook declarations: ${ai_tools_settings_merge__reason}"
        _pu_say err "unchanged -- copy the \"hooks\" block from ${rpmnew} by hand"
        return 0 ;;
    esac

    local line
    if (( ${#ai_tools_settings_merge__added[@]} > 0 )); then
        _pu_say act "hook declarations this version adds:"
        for line in "${ai_tools_settings_merge__added[@]}"; do _pu_say act "  + ${line}"; done
    fi
    if (( ${#ai_tools_settings_merge__removed[@]} > 0 )); then
        _pu_say act "hook declarations declared twice, of which the merge keeps the first:"
        for line in "${ai_tools_settings_merge__removed[@]}"; do _pu_say act "  - ${line}"; done
    fi
    _pu_say info "the permission rules stay as written"
    ai_tools_msg__confirm "  Merge these into ${deployed}?" y || { _pu_say act "skipped -- ${deployed} unchanged"; return 0; }

    status=0
    ai_tools_settings_merge__merge_hook_declarations "${deployed}" "${rpmnew}" || status=$?
    if (( status >= 2 )); then
        warn MSG-X9F8 "the merge failed: ${ai_tools_settings_merge__reason} -- ${deployed} is unchanged"
        _PU_ATTENTION=$(( _PU_ATTENTION + 1 ))
        _PU_ERRORS=$(( _PU_ERRORS + 1 ))
        return 0
    fi
    _pu_say ok "merged -- the previous file is saved as ${ai_tools_settings_merge__backup}"

    # Close against what is left. Once the permission rules match too, the .rpmnew has no difference left to report,
    # so the operator is told the removal is all that remains.
    if command -v diff >/dev/null 2>&1 && diff -q "${deployed}" "${rpmnew}" >/dev/null 2>&1; then
        _pu_say ok "now matches the shipped file exactly"
        _pu_leave "${rpmnew}" merged
    else
        _pu_settings_rest "${deployed}" "${rpmnew}"
    fi
}

# _pu_settings_rest <deployed> <rpmnew>: report what a settings file differs in once its hook declarations are current.
# The permission rule lists are compared as sets (ai_tools_settings_merge__find_permission_gaps) and every other setting
# with its keys sorted (ai_tools_settings_merge__read_settings_rest), so a list's order, a key's place in the object,
# and how hooks are grouped are not reported. A rule the copy carries and the file does not is named for the operator
# to add; a rule only the file carries is the host's and is listed without being counted; with neither left, the copy
# does not carry anything the file lacks.
_pu_settings_rest() {
    local deployed="$1" rpmnew="$2" gaps kind list rule scratch pending=0
    local -a missing=() extra=()
    if ! gaps="$(ai_tools_settings_merge__find_permission_gaps "${deployed}" "${rpmnew}")"; then
        _pu_say act "the permission rules were not compared (jq is missing or a file is not valid JSON):"
        _pu_diff "${deployed}" "${rpmnew}"
        _pu_leave "${rpmnew}"
        return 0
    fi
    while IFS=$'\t' read -r kind list rule; do
        case "${kind}" in
            missing) missing+=("${list}: ${rule}") ;;
            extra)   extra+=("${list}: ${rule}") ;;
        esac
    done <<< "${gaps}"
    if (( ${#missing[@]} > 0 )); then
        _pu_say act "rules this version ships that the file does not carry -- add them unless you removed them on purpose:"
        for rule in "${missing[@]}"; do _pu_say act "  ${rule}"; done
        pending=1
    fi
    if (( ${#extra[@]} > 0 )); then
        _pu_say info "rules the file carries that the package copy does not, kept as yours:"
        for rule in "${extra[@]}"; do _pu_say info "  ${rule}"; done
    fi
    scratch="$(mktemp -d)" || scratch=""
    if [[ -n "${scratch}" ]] && ai_tools_settings_merge__read_settings_rest "${deployed}" > "${scratch}/file" \
            && ai_tools_settings_merge__read_settings_rest "${rpmnew}" > "${scratch}/copy" \
            && ! cmp -s "${scratch}/file" "${scratch}/copy"; then
        _pu_say act "other settings differ (key order ignored):"
        _pu_diff "${scratch}/file" "${scratch}/copy" "${deployed}" "${rpmnew}"
        pending=1
    fi
    [[ -n "${scratch}" ]] && rm -rf "${scratch}"
    if (( pending )); then
        _pu_say info "merge into the left (live file); new changes are on the right:"
        printf '      %s\n' "$(_pu_merge_command "${deployed}" "${rpmnew}")"
        _pu_leave "${rpmnew}"
    else
        _pu_say ok "the rest matches the package copy, apart from order and layout"
        _pu_leave "${rpmnew}" merged
    fi
}

# _pu_prose <file>: the file's comment prose as one word stream -- comment lines with their `#` removed, the commented
# defaults (`#KEY=value`, `# KEY=value`) left out as settings, and whitespace collapsed -- so a comment that was only
# re-wrapped reads as unchanged and a reworded one does not.
_pu_prose() {
    sed -n '/^[[:space:]]*#/{/^[[:space:]]*#[[:space:]]\{0,1\}[A-Za-z_][A-Za-z0-9_]*=/d;s/^[[:space:]]*#//;p;}' "$1" \
        2>/dev/null | tr -s '[:space:]' ' '
}

# _pu_set_keys <array-name> <file>: each key <file> assigns, once, in file order. A commented default is not a key
# the file sets.
_pu_set_keys() {
    local -n _pu_set_out="$1"
    local line key
    _pu_set_out=()
    while IFS= read -r line || [[ -n "${line}" ]]; do
        line="${line#"${line%%[![:space:]]*}"}"
        [[ -z "${line}" || "${line}" == '#'* || "${line}" != *=* ]] && continue
        key="${line%%=*}"; key="${key%"${key##*[![:space:]]}"}"
        [[ "${key}" =~ ^[A-Za-z_][A-Za-z0-9_]*$ && " ${_pu_set_out[*]} " != *" ${key} "* ]] && _pu_set_out+=("${key}")
    done < "$2"
}

# _pu_own_keys <array-name> <deployed> <rpmnew>: the keys the deployed file sets whose value the copy does not set
# the same way -- the operator's own settings, which is what most of such a difference is. Names only: a kept config may
# hold a credential, so no value is printed or kept beyond the comparison.
_pu_own_keys() {
    local -n _pu_own_out="$1"
    local deployed="$2" rpmnew="$3" key live shipped
    local -a set_keys=()
    _pu_own_out=()
    _pu_set_keys set_keys "${deployed}"
    for key in "${set_keys[@]}"; do
        live="$(ai_tools_conf__print_value "${deployed}" "${key}")" || true
        if shipped="$(ai_tools_conf__print_value "${rpmnew}" "${key}")" && [[ "${shipped}" == "${live}" ]]; then
            continue
        fi
        _pu_own_out+=("${key}")
    done
}

# _pu_merge_copy <out-var> <deployed> <rpmnew>: write <rpmnew>-merge, the package copy with this host's settings carried
# in, and set <out-var> to its path. Each key the live file sets replaces the copy's first line for that key, set
# or commented, with the live file's own line, and a later assignment of the key in the copy is dropped. The merge then
# shows the options and prose the version changed, and not each value the host chose, which differs from the template
# by design and would otherwise be a difference to review and leave. A key the copy does not mention is not carried:
# the live line then stands alone on the left, which is a difference worth seeing. The copy sits beside the package copy
# it is made from -- an .rpmnew, or the .shipped a from-source install leaves -- and exists only while a merge is
# pending: each run removes the ones it did not rewrite (_pu_prune_merge_copies). It is 0600 root whatever the live
# file's mode, since it carries the live values and a kept KEY=value file may hold a credential, and its name does not
# end in .rpmnew or .shipped, so no scan here reads it as a package copy. The comparison with the live file ignores
# trailing newlines, so a file without a final one is not offered a merge for it. Returns 0 with the copy written
# and recorded in _PU_MERGE_WRITTEN, 2 with no copy when it would equal the live file -- the package copy then differs
# only in the host's values -- and 1 with <out-var> empty when it could not be written.
_pu_merge_copy() {
    local -n _pu_merge_out="$1"
    local deployed="$2" rpmnew="$3" line key trimmed scratch
    local -a set_keys=()
    local -A live_line=() carried_keys=()
    _pu_merge_out=""
    _pu_set_keys set_keys "${deployed}"
    while IFS= read -r line || [[ -n "${line}" ]]; do
        trimmed="${line#"${line%%[![:space:]]*}"}"
        [[ -z "${trimmed}" || "${trimmed}" == '#'* || "${trimmed}" != *=* ]] && continue
        key="${trimmed%%=*}"; key="${key%"${key##*[![:space:]]}"}"
        [[ " ${set_keys[*]} " == *" ${key} "* ]] && live_line["${key}"]="${line}"
    done < "${deployed}"
    # mktemp creates the file 0600 and owned by this root process, before a line of the live file is written to it.
    scratch="$(mktemp "${rpmnew%/*}/.${rpmnew##*/}-merge.XXXXXX" 2>/dev/null)" || return 1
    while IFS= read -r line || [[ -n "${line}" ]]; do
        if [[ "${line}" =~ ^[[:space:]]*#[[:space:]]?([A-Za-z_][A-Za-z0-9_]*)= ]]; then
            key="${BASH_REMATCH[1]}"
            if [[ -n "${live_line[${key}]+set}" && -z "${carried_keys[${key}]+set}" ]]; then
                printf '%s\n' "${live_line[${key}]}"; carried_keys["${key}"]=1; continue
            fi
        elif [[ "${line}" =~ ^[[:space:]]*([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*= ]]; then
            key="${BASH_REMATCH[1]}"
            if [[ -n "${live_line[${key}]+set}" ]]; then
                [[ -n "${carried_keys[${key}]+set}" ]] && continue
                printf '%s\n' "${live_line[${key}]}"; carried_keys["${key}"]=1; continue
            fi
        fi
        printf '%s\n' "${line}"
    done < "${rpmnew}" > "${scratch}" || { rm -f "${scratch}"; return 1; }
    if [[ "$(< "${deployed}")" == "$(< "${scratch}")" ]]; then
        rm -f "${scratch}"
        return 2
    fi
    mv -f "${scratch}" "${rpmnew}-merge" 2>/dev/null || { rm -f "${scratch}"; return 1; }
    _PU_MERGE_WRITTEN+=("${rpmnew}-merge")
    _pu_merge_out="${rpmnew}-merge"
}

# The merge copies this run wrote, so the closing prune keeps them.
_PU_MERGE_WRITTEN=()

# _pu_prune_merge_copies <root>: remove each root-owned <copy>-merge under the scanned locations that this run did not
# write, naming it. A merge copy carries the live file's values, so it exists only while the report asks for a merge:
# once the operator has merged, removed the package copy, or a .shipped copy stays behind as recovery material, the next
# run removes it. A same-named file of another owner -- one the sandbox account placed in its own config directory -- is
# not this command's to remove and is left as it is. Removal unlinks the name and does not follow it.
_pu_prune_merge_copies() {
    local root="$1" dir copy written
    local -a copies=()
    for dir in "${POSTUPGRADE_DIRS[@]}"; do
        [[ -d "${root}${dir}" ]] || continue
        mapfile -t -O "${#copies[@]}" copies < <(find "${root}${dir}" -maxdepth 3 -type f -uid 0 \
            \( -name '*.rpmnew-merge' -o -name '*.shipped-merge' \) 2>/dev/null | sort -u)
    done
    for copy in "${copies[@]+"${copies[@]}"}"; do
        for written in "${_PU_MERGE_WRITTEN[@]+"${_PU_MERGE_WRITTEN[@]}"}"; do
            [[ "${copy}" == "${written}" ]] && continue 2
        done
        rm -f "${copy}" && printf '%s• removed %s -- no merge is pending for it%s\n' "${_PU_DIM}" "${copy}" "${_PU_RST}"
    done
}

# _pu_keyval <deployed> <rpmnew>: report and never write. A KEY=value config is mostly prose -- commented option blocks
# whose layout is the operator's -- and merging prose would need a convention an operator has to learn before they can
# predict it. Name the options the new version documents that this file does not mention, the operator's own settings,
# and whether the comments changed, and leave the edit to them. The difference is named as a command and not printed:
# a kept KEY=value file may hold a credential. The removal is offered only when every option is mentioned and the prose
# is the same, since otherwise the copy still holds something the live file lacks.
_pu_keyval() {
    local deployed="$1" rpmnew="$2" key page
    local -a new_keys=() own_keys=()
    local carried=0
    if ai_tools_conf__find_new_keys new_keys "${deployed}" "${rpmnew}"; then
        carried=1
        _pu_say act "options this version documents that the file does not mention:"
        for key in "${new_keys[@]}"; do _pu_say act "  ${key}"; done
        _pu_say info "each is optional, and an unmentioned key keeps its default -- copy the blocks you want"
    else
        _pu_say ok "every option this version documents is mentioned"
    fi
    _pu_own_keys own_keys "${deployed}" "${rpmnew}"
    (( ${#own_keys[@]} == 0 )) || _pu_say ok "set on this host, and kept as set: ${own_keys[*]}"

    page="ai-tools-${deployed##*/}"
    if [[ "$(_pu_prose "${deployed}")" != "$(_pu_prose "${rpmnew}")" ]]; then
        carried=1
        if [[ -r "/usr/local/share/man/man5/${page}.5" ]]; then
            _pu_say act "the comments differ from this version's -- the current wording is in ${page}(5)"
        else
            _pu_say act "the comments differ from this version's"
        fi
    fi
    local merge_copy merge_status=0
    _pu_merge_copy merge_copy "${deployed}" "${rpmnew}" || merge_status=$?
    case "${merge_status}" in
        0)  _pu_say info "merge into the left (live file); the right is this version's copy with the values set here carried in, so each difference is one the version made:"
            printf '      %s\n' "$(_pu_merge_command "${deployed}" "${merge_copy}")" ;;
        2)  _pu_say ok "the copy differs from the file only in the values set here" ;;
        *)  _pu_say info "merge into the left (live file); the right is this version's copy as shipped, so keep the left side of each key set here:"
            printf '      %s\n' "$(_pu_merge_command "${deployed}" "${rpmnew}")" ;;
    esac
    if (( carried )); then _pu_leave "${rpmnew}"; else _pu_leave "${rpmnew}" merged; fi
}

# _pu_review <deployed> <rpmnew>: show and stop. This file is the sudo grant itself.
_pu_review() {
    local deployed="$1" rpmnew="$2"
    _PU_ATTENTION=$(( _PU_ATTENTION + 1 ))
    ai_tools_msg__warn MSG-H8A2 \
        "This file defines the sudo grant that lets an operator launch the sandbox. It is shown, never merged: check any change yourself with visudo -c before adopting it."
    _pu_diff "${deployed}" "${rpmnew}"
    _pu_say info "adopt the packaged version with:  sudo visudo -c -f ${rpmnew} && sudo cp ${rpmnew} ${deployed}"
    _pu_leave "${rpmnew}"
}

# _pu_show <deployed> <rpmnew>: a file this command has no treatment for. Named, never printed or merged.
_pu_show() {
    local deployed="$1" rpmnew="$2"
    _pu_say act "this command does not merge this file -- merge into the left (live file); new changes are on the right:"
    printf '      %s\n' "$(_pu_merge_command "${deployed}" "${rpmnew}")"
    _pu_leave "${rpmnew}"
}

# _pu_clock_lines <array-name> <entry>...: fill the named array with "<when>  <path>" for every file or copy
# among the entries dated after the system clock (ai_tools_conf__find_paths_ahead_of_clock), and fail when there is one.
# The copy a file is compared with is the newest by date, so under a clock that is behind the comparison picks the wrong
# copy and the report would send the operator to merge from it: both runs ask this first and, with a finding, name
# the clock as the first thing to correct and make no comparison.
_pu_clock_lines() {
    local -n _pu_clock_out="$1"
    shift
    local entry file kind label copy when path
    local -a paths=()
    _pu_clock_out=()
    for entry in "$@"; do
        IFS='|' read -r file kind label copy <<< "${entry}"
        paths+=("${file}" "${copy}")
    done
    (( ${#paths[@]} > 0 )) || return 0
    while IFS=$'\t' read -r when path; do
        [[ -n "${path}" ]] && _pu_clock_out+=("${when}  ${path}")
    done < <(ai_tools_conf__find_paths_ahead_of_clock "${paths[@]}" || true)
    (( ${#_pu_clock_out[@]} == 0 ))
}

# _pu_entries <root>: one "<file>|<kind>|<label>|<copy>" line per file with a baseline copy waiting -- the registry
# first, then each one found under POSTUPGRADE_DIRS that the registry does not name. The copy is the newest beside
# the file of the package's .rpmnew and the installer's .shipped (ai_tools_conf__find_latest_copy), so a host
# whose install routes alternated is compared with the baseline that reached it last, whichever route brought it.
_pu_entries() {
    local root="$1" entry file kind label dir copy fd pid
    local -a named=()
    for entry in "${POSTUPGRADE_FILES[@]}"; do
        IFS='|' read -r file kind label <<< "${entry}"
        named+=("${root}${file}")
        [[ -f "${root}${file}" ]] && copy="$(ai_tools_conf__find_latest_copy "${root}${file}")" \
            && printf '%s|%s|%s|%s\n' "${root}${file}" "${kind}" "${label}" "${copy}"
    done
    for dir in "${POSTUPGRADE_DIRS[@]}"; do
        [[ -d "${root}${dir}" ]] || continue
        exec {fd}< <(find "${root}${dir}" -maxdepth 3 -type f \( -name '*.rpmnew' -o -name '*.shipped' \) 2>/dev/null | sort); pid=$!
        while IFS= read -r -u "${fd}" copy; do
            case "${copy}" in
                *.rpmnew)  file="${copy%.rpmnew}" ;;
                *.shipped) file="$(sed -E 's/\.[0-9]{8}(-[0-9]+)?\.shipped$//' <<< "${copy}")"
                           [[ "${file}" != "${copy}" ]] || continue ;;
            esac
            [[ -f "${file}" && " ${named[*]} " != *" ${file} "* ]] || continue
            named+=("${file}")
            copy="$(ai_tools_conf__find_latest_copy "${file}")" || continue
            if [[ "${file}" == *.conf ]]; then kind=keyval; else kind=show; fi
            printf '%s|%s|%s|%s\n' "${file}" "${kind}" "${file##*/}" "${copy}"
        done
        exec {fd}<&-
        # A directory the walk could not read is a copy this collector may have missed, so the collector fails rather
        # than reporting the files it did find as the whole set.
        wait "${pid}" || return 1
    done
    return 0
}

# _pu_ask_gaps <root>: name each ask entry the kept settings.json lacks for an installed command, with the line to add.
# Checked whether or not a .rpmnew is waiting: rpm parks a copy only on the upgrade that changed the shipped file,
# and the operator may have removed it since, while the entry stays missing. Reported and not written, since
# the permission rules are the host's.
_pu_ask_gaps() {
    local settings="$1/opt/ai-tools/.claude/settings.json" gaps line
    local -a entries=() fix=()
    [[ -f "${settings}" ]] || return 0
    if ! gaps="$(ai_tools_settings_merge__find_ask_gaps "${settings}" "$1")"; then
        warn MSG-B2E6 "the ask entries in ${settings} were not checked -- jq is missing or the file is not valid JSON"
        _PU_ATTENTION=$(( _PU_ATTENTION + 1 ))
        return 0
    fi
    [[ -n "${gaps}" ]] || return 0
    mapfile -t entries <<< "${gaps}"
    mapfile -t fix < <(ai_tools_settings_merge__format_ask_fix "${settings}" "${entries[@]}")
    _PU_NAME="${settings##*/}"
    ai_tools_msg__headline "${_PU_NAME} -- commands that run without asking" 1 "${settings}"
    _pu_say act "each of these sends data off the host, and the file does not ask before it runs:"
    for line in "${entries[@]}"; do _pu_say act "  ${line}"; done
    if (( ${#fix[@]} > 0 )); then
        _pu_say info "to have it ask, ${fix[0]}"
        # Printed bare, not through _pu_say, so the snippet copies out of the terminal without a prefix on each line.
        # Green as a diff's added lines are: every line of it is text to add.
        printf "      ${_PU_GRN}%s${_PU_RST}\n" "${fix[@]:1}"
    fi
    _pu_say info "this command does not edit the file, since the permission rules are yours -- re-run it to confirm"
}

# _pu_managed_pairs <root>: "<live>\t<reference>" per managed file an installed agent's manifest declares
# (ai_tools_providers__list_agent_managed_files), both paths under <root>. The manifests are read under <root> as well,
# so the unit suite drives the check against fixture manifests. Returns 1, printing nothing, when the provider library
# did not load.
_pu_managed_pairs() {
    local root="$1" agent live reference
    declare -F ai_tools_providers__list_agent_managed_files >/dev/null 2>&1 || return 1
    # shellcheck disable=SC2034  # read by the provider functions this one calls, which see it through dynamic scope
    local AI_TOOLS_AGENTS_DIR="${root}${AI_TOOLS_AGENTS_DIR}"
    while IFS=$'\t' read -r agent _ _; do
        [[ -n "${agent}" ]] || continue
        while IFS=$'\t' read -r live reference; do
            printf '%s\t%s\n' "${root}${live}" "${root}${reference}"
        done < <(ai_tools_providers__list_agent_managed_files "${agent}" 2>/dev/null)
    done < <(ai_tools_providers__list_installed_agents 2>/dev/null)
}

# _pu_merge_command <file> <copy>: the command that merges a file with its package copy side by side, the file
# on the left. sudoedit reads each as root into a temporary copy the invoking user owns, runs the merge tool
# as that user on both, and writes a copy back only when it changed, so the tool never runs as root; an edit made
# to the right-hand pane lands in the package copy, which is disposable. meld where it is installed, vimdiff otherwise.
_pu_merge_command() {
    local tool=vimdiff
    command -v meld >/dev/null 2>&1 && tool=meld
    printf 'SUDO_EDITOR=%s sudoedit %s %s' "${tool}" "$1" "$2"
}

# _pu_key_gaps <root>: name each key an agent's managed file does not set that its shipped copy does, with the command
# that merges the file with that copy. Checked whether or not a .rpmnew is waiting, for the reason _pu_ask_gaps states,
# and not written: the file is the host's, and a key arrives with the comment that explains it, which a key-by-key edit
# would not carry over.
_pu_key_gaps() {
    local root="$1" live reference keys key
    local -a missing=()
    while IFS=$'\t' read -r live reference; do
        [[ -f "${live}" && -f "${reference}" ]] || continue
        cmp -s "${live}" "${reference}" && continue
        keys="$(ai_tools_providers__find_managed_file_missing_keys "${live}" "${reference}")" || continue
        [[ -n "${keys}" ]] || continue
        mapfile -t missing <<< "${keys}"
        _PU_NAME="${live##*/}"
        ai_tools_msg__headline "${_PU_NAME} -- keys this release ships that the file does not set" 1 "${live}"
        for key in "${missing[@]}"; do _pu_say act "  ${key}"; done
        # sudoedit writes back an edited right-hand pane too, so the merge runs against a .rpmnew -- the one rpm left,
        # or one recreated here -- and the pristine copy the status reports compare with stays untouched.
        _pu_say info "merge into the left (live file); new changes are on the right:"
        # Printed bare, so each command copies out of the terminal whole.
        [[ -f "${live}.rpmnew" ]] || printf '      sudo cp %s %s\n' "${reference}" "${live}.rpmnew"
        printf '      %s\n' "$(_pu_merge_command "${live}" "${live}.rpmnew")"
        _pu_say info "then remove ${live}.rpmnew -- this command does not edit the file, since its contents are yours"
    done < <(_pu_managed_pairs "${root}")
}

# _pu_orphan_report <root>: a block per package copy whose file is gone, naming the two ways to settle it.
_pu_orphan_report() {
    local path
    while IFS= read -r path; do
        _PU_NAME="${path##*/}"
        ai_tools_msg__headline "${_PU_NAME} -- a package copy without its file" 1 "${path}"
        _pu_say act "the file this copy belongs to is gone -- restore it from the copy, or remove the copy:"
        printf '      sudo cp -p %s %s\n' "${path}" "${path%.rpmnew}"
        printf '      sudo rm %s\n' "${path}"
    done < <(_pu_orphans "$1")
}

# _pu_asset_report <root>: one block for the shared skills, subagents and orientation text whose live copy or agent link
# is not the one provisioning places. A missing asset or link needs the operator; an outdated or overridden one is named
# as their choice.
_pu_asset_report() {
    local state path detail
    local -a lines=()
    mapfile -t lines < <(_pu_assets "$1")
    (( ${#lines[@]} > 0 )) || return 0
    _PU_NAME="assets"
    ai_tools_msg__headline "shared skills, subagents and orientation" 1 "$1/opt/ai-tools"
    for path in "${lines[@]}"; do
        IFS=$'\t' read -r state _ path detail <<< "${path}"
        case "${state}" in
            asset-missing|asset-unlinked|error) _pu_say act "${path} -- ${detail}" ;;
            *)                                  _pu_say info "${path} -- ${detail}" ;;
        esac
    done
    if printf '%s\n' "${lines[@]}" | grep -qE '^(asset-missing|asset-unlinked)'; then
        _pu_say info "atr admin system bootstrap seeds and links them again"
    fi
}

# _pu_sort_copies: read sidecar paths on stdin and print them grouped by file and in the order they were made --
# by date, then by the day's number, an unnumbered copy (the name an earlier release gave a day's first) counting as 1.
# A name that carries a date but not in that shape (a copy made by hand) is kept, after the day's numbered copies.
_pu_sort_copies() {
    awk 'BEGIN { OFS = "\t" }
        match($0, /\.[0-9]{8}(-[0-9]+)?\.(bak|shipped|retired)$/) {
            tail = substr($0, RSTART + 1); base = substr($0, 1, RSTART - 1)
            day = substr(tail, 1, 8); n = 1
            if (substr(tail, 9, 1) == "-") { n = substr(tail, 10); sub(/\..*/, "", n) }
            print base, day, n, $0; next
        }
        match($0, /\.[0-9]{8}/) { print substr($0, 1, RSTART - 1), substr($0, RSTART + 1, 8), 9999, $0; next }
        { print $0, "99999999", 9999, $0 }' \
        | sort -t $'\t' -k1,1 -k2,2 -k3,3n | cut -f4
}

# _pu_sidecars <root> [<reference>...]: list the dated copies _pu_copies names, so an operator learns they exist,
# leaving out each <reference> -- a .shipped _pu_report compared a file with, which has a block of its own. Listed,
# never removed: a .bak is the only copy that restores host tuning a merge got wrong. One dated before this command's
# own file is from an earlier installation, which is said beside it.
_pu_sidecars() {
    local root="$1" path stamp installed
    shift
    local -a copies=() references=("$@")
    installed="$(_pu_installed_day)" || installed=""
    mapfile -t copies < <(_pu_copies "${root}")
    (( ${#copies[@]} > 0 )) || return 0
    ai_tools_msg__headline "earlier copies kept beside the config files" 1 \
        "a .bak is what a file held before a merge replaced it, a .shipped a baseline an earlier from-source install left," \
        "a .retired a managed file or a withdrawn skill set aside"
    for path in "${copies[@]}"; do
        [[ " ${references[*]+"${references[*]}"} " != *" ${path} "* ]] || continue
        stamp="$(sed -nE 's/.*\.([0-9]{8})(-[0-9]+)?\.(bak|shipped|retired)$/\1/p' <<< "${path}")"
        if [[ -n "${installed}" && -n "${stamp}" && "${stamp}" < "${installed}" ]]; then
            printf '  %s  %s(before this installation)%s\n' "${path}" "${_PU_DIM}" "${_PU_RST}"
        else
            printf '  %s\n' "${path}"
        fi
    done
    printf '  each is yours to keep or remove -- this command reads only the newest copy beside a file, above\n'
}

# postupgrade [--check [--all] [--format tsv]]: reconcile the copies, or with `--check` write what needs attention
# as a record stream without asking or writing. Returns 0 when no file is left to act
# on, AI_TOOLS_RECORDS_BASE__EXIT_FINDINGS while one is, and 1 when a merge the run was asked to make did not happen
# (the two `err` lines _pu_json counts), so a caller reads the outcome from the exit status alone; under `--check`
# the status is the report state's, which adds AI_TOOLS_RECORDS_BASE__EXIT_UNREADABLE for a source the check could not
# read (ai-tools-records(5), EXIT STATUS).
postupgrade() {
    local check=0 all=0 format=tsv format_given=0 refusal=""
    while [[ $# -gt 0 && -z "${refusal}" ]]; do
        case "$1" in
            --check)  check=1 ;;
            --all)    all=1 ;;
            --format) if [[ $# -ge 2 ]]; then format="$2"; format_given=1; shift
                      else refusal="--format takes a value (tsv)"; fi ;;
            *)        refusal="unknown argument '$1' (--check [--all] [--format tsv])" ;;
        esac
        shift
    done
    if [[ -z "${refusal}" ]] && (( ! check && (all || format_given) )); then
        refusal="--all and --format apply to --check alone"
    elif [[ -z "${refusal}" && "${format}" != tsv ]]; then
        refusal="unknown --format '${format}' (tsv)"
    fi
    [[ -z "${refusal}" ]] || reject MSG-S9M6 "system post-upgrade: ${refusal}"
    if (( check )); then
        _PU_ALL="${all}"
        _pu_run_check "${AI_TOOLS_POSTUPGRADE_ROOT:-}"
        ai_tools_records_base__get_exit_status || return $?
        return 0
    fi
    _pu_report
    (( _PU_ERRORS == 0 )) || return 1
    (( _PU_ATTENTION == 0 )) || return "${AI_TOOLS_RECORDS_BASE__EXIT_FINDINGS}"
    return 0
}

# ── `--check`: the findings as a record stream ───────────────────────────────────────────────
# One record per finding, in the stream ai-tools-records(5) states -- no colour, no heading, and no output at all
# on a host with no finding -- so a cron job mails only a host that needs attention and a monitor reads the header
# and the rows. Each finding is one situation, so it carries one message code, and _pu_write_finding is the one table
# tying a finding to its code and its severity: `attention` makes the run exit AI_TOOLS_RECORDS_BASE__EXIT_FINDINGS,
# `unreadable` (the `error` finding, a check that could not run) AI_TOOLS_RECORDS_BASE__EXIT_UNREADABLE, and `info` is
# written under `--all` alone and leaves the exit as it is. The caller names the subject-type: `file`
# for a configuration finding, and for an asset finding the type _pu_asset_line read at its path, and the item is
# the components the collector's own granularity gives -- the event and the command of a hook, the list and the rule
# of a permission rule, a key, and for an `error` one token naming the check -- so two findings on one file keep two
# ids. No row carries an operator: every subject is under /etc or /opt/ai-tools, host-wide.
#
# The collectors feed the check through `< <(...)`, and _pu_run_check saves each one's PID on the statement that opens
# it and waits for it once its output is read (records.rule.md), so a collector that exits non-zero is an `error` row
# on the root it walked rather than a shorter set read as complete. They keep the line and `|` framing the interactive
# report reads them with, which holds for a path the packages install and does not for a name the sandbox account plants
# under an agent's config directory with a line feed in it: such a name splits into two rows and removes none, so it
# adds findings to a run and does not make one read as clean.
_PU_ALL=0
_PU_CODE=""
_PU_SEVERITY=""

# _pu_attention / _pu_unreadable / _pu_info <code> <finding>: the code and severity a finding token carries,
# into _PU_CODE and _PU_SEVERITY -- one word per severity, and the code first, so the message index reads each table
# line as the definition it is and the messages page derives the severity from the word.
_pu_attention()  { _PU_CODE="$1"; _PU_SEVERITY=attention; }
_pu_unreadable() { _PU_CODE="$1"; _PU_SEVERITY=unreadable; }
_pu_info()       { _PU_CODE="$1"; _PU_SEVERITY=info; }

# _pu_write_finding <finding> <subject-type> <subject> <detail> [<component>...]: write one record, or none
# for an `info` finding without `--all`. ai-tools-admin(8) lists what each finding means. A token this table does not
# carry, and an empty item component, are defects of this file: each is written as an `error` row, so the run reads
# AI_TOOLS_RECORDS_BASE__EXIT_UNREADABLE rather than silently short.
_pu_write_finding() {
    local finding="$1" subject_type="$2" subject="$3" detail="$4" item
    shift 4
    case "${finding}" in
        hook-missing)       _pu_attention MSG-F2G7 "hook-missing" ;;
        hook-repeated)      _pu_attention MSG-E8S8 "hook-repeated" ;;
        ask-missing)        _pu_attention MSG-E9V5 "ask-missing" ;;
        rule-missing)       _pu_attention MSG-Z8U4 "rule-missing" ;;
        key-missing)        _pu_attention MSG-K5H2 "key-missing" ;;
        option-unmentioned) _pu_attention MSG-N3U8 "option-unmentioned" ;;
        rpmnew-differs)     _pu_attention MSG-P4Q4 "rpmnew-differs" ;;
        rpmnew-review)      _pu_attention MSG-Y3P3 "rpmnew-review" ;;
        rpmnew-orphan)      _pu_attention MSG-K8D2 "rpmnew-orphan" ;;
        asset-missing)      _pu_attention MSG-X6H5 "asset-missing" ;;
        asset-unlinked)     _pu_attention MSG-N9S4 "asset-unlinked" ;;
        list-unmigrated)    _pu_attention MSG-P5K4 "list-unmigrated" ;;
        list-unmigratable)  _pu_attention MSG-S3D8 "list-unmigratable" ;;
        registry-unmigrated) _pu_attention MSG-C2H5 "registry-unmigrated" ;;
        registry-invalid)   _pu_attention MSG-G6H4 "registry-invalid" ;;
        error)              _pu_unreadable MSG-Y3J5 "error" ;;
        rpmnew-residual)    _pu_info MSG-J3X7 "rpmnew-residual" ;;
        copy-kept)          _pu_info MSG-W8F8 "copy-kept" ;;
        asset-outdated)     _pu_info MSG-R6B2 "asset-outdated" ;;
        asset-overridden)   _pu_info MSG-W3M8 "asset-overridden" ;;
        *)  _pu_write_finding error "${subject_type}" "${subject}" "no code for the finding '${finding}'" finding-table
            return 0 ;;
    esac
    if ! ai_tools_records_tsv__frame_item_components item "$@"; then
        _pu_write_finding error "${subject_type}" "${subject}" "an empty item component for ${finding}" finding-table
        return 0
    fi
    [[ "${_PU_SEVERITY}" != info ]] || (( _PU_ALL )) || return 0
    ai_tools_records_tsv__write_record "" "${_PU_CODE}" "${_PU_SEVERITY}" "${finding}" "${subject_type}" "" "${item}" \
        "${subject}" "${detail}"
}

# _pu_hook_finding <finding> <file> <line>: a hook declaration the merge names as "<event>: <command>", written
# with the event and the command as its item components. A line without that shape is an `error` row, not a guess.
_pu_hook_finding() {
    if [[ "$3" != *": "* ]]; then
        _pu_write_finding error file "$2" "a hook declaration the merge named could not be read: $3" hook-merge
        return 0
    fi
    _pu_write_finding "$1" file "$2" "$3" "${3%%: *}" "${3#*: }"
}

# _pu_wait_collector <pid> <subject-type> <subject> <source>: wait for a collector opened with `< <(...)`, and write
# an `error` row naming it when it exited non-zero. Called in the report's own shell, after the collector's output is
# read, with the PID saved on the statement that opened it.
_pu_wait_collector() {
    local status=0
    wait "$1" || status=$?
    (( status == 0 )) || _pu_write_finding error "$2" "$3" "the ${4} collector exited ${status}" "$4"
}

# _pu_run_check <root>: every finding, through _pu_write_finding, without asking or writing. Each reads the same
# predicate the report does. The caller reads the outcome off the report state, ai_tools_records_base__get_exit_status.
_pu_run_check() {
    local root="$1" file kind label scratch status key line state stype path detail live reference fd pid
    local -a new_keys=() entries=() files_with_baseline=()
    ai_tools_records_base__begin_report
    _pu_kind_findings "${root}"
    _pu_registry_findings "${root}"
    exec {fd}< <(_pu_entries "${root}"); pid=$!
    mapfile -t -u "${fd}" files_with_baseline
    exec {fd}<&-
    _pu_wait_collector "${pid}" directory "${root:-/}" entries
    # A file or copy dated after the clock: the comparison would pick the wrong copy, so each is an `error` row --
    # a check that could not run -- and the run does not compare a file.
    local -a clock_lines=()
    if ! _pu_clock_lines clock_lines "${files_with_baseline[@]+"${files_with_baseline[@]}"}"; then
        for line in "${clock_lines[@]}"; do
            _pu_write_finding error file "${line#*  }" "dated ${line%%  *}, after the system clock -- set the clock before acting on this report" clock
        done
        files_with_baseline=()
    fi
    local entry
    for entry in "${files_with_baseline[@]+"${files_with_baseline[@]}"}"; do
        IFS='|' read -r file kind label copy <<< "${entry}"
        if cmp -s "${file}" "${copy}"; then _pu_write_finding rpmnew-residual file "${copy}" ""; continue; fi
        case "${kind}" in
        json)
            if ! ai_tools_settings_merge__require_jq 2>/dev/null; then
                _pu_write_finding error file "${file}" "jq is not installed" jq
                continue
            fi
            scratch="$(mktemp -d)" || { _pu_write_finding error file "${file}" "no temporary directory" mktemp; continue; }
            status=0
            cp -p "${file}" "${scratch}/probe" && ai_tools_settings_merge__merge_hook_declarations "${scratch}/probe" \
                "${copy}" >/dev/null 2>&1 || status=$?
            rm -rf "${scratch}"
            case "${status}" in
            0)  for line in "${ai_tools_settings_merge__added[@]}"; do _pu_hook_finding hook-missing "${file}" "${line}"; done
                for line in "${ai_tools_settings_merge__removed[@]}"; do
                    _pu_hook_finding hook-repeated "${file}" "${line}"
                done
                _pu_settings_findings "${file}" "${copy}" ;;
            1)  _pu_settings_findings "${file}" "${copy}" ;;
            *)  _pu_write_finding error file "${file}" "${ai_tools_settings_merge__reason:-merge probe failed}" hook-merge ;;
            esac ;;
        keyval)
            if ai_tools_conf__find_new_keys new_keys "${file}" "${copy}"; then
                for key in "${new_keys[@]}"; do _pu_write_finding option-unmentioned file "${file}" "${key}" "${key}"; done
            fi
            [[ "$(_pu_prose "${file}")" == "$(_pu_prose "${copy}")" ]] \
                || _pu_write_finding rpmnew-differs file "${file}" "comments" ;;
        review)
            _pu_write_finding rpmnew-review file "${file}" "sudoers grant" ;;
        *)
            _pu_write_finding rpmnew-differs file "${file}" "" ;;
        esac
    done

    exec {fd}< <(_pu_orphans "${root}"); pid=$!
    while IFS= read -r -u "${fd}" path; do
        _pu_write_finding rpmnew-orphan file "${path}" "the file it belongs to is gone"
    done
    exec {fd}<&-
    _pu_wait_collector "${pid}" directory "${root:-/}" orphans

    file="${root}/opt/ai-tools/.claude/settings.json"
    if [[ -f "${file}" ]]; then
        if line="$(ai_tools_settings_merge__find_ask_gaps "${file}" "${root}" 2>/dev/null)"; then
            [[ -n "${line}" ]] && mapfile -t entries <<< "${line}"
            for line in "${entries[@]}"; do _pu_write_finding ask-missing file "${file}" "${line}" "${line}"; done
        else
            _pu_write_finding error file "${file}" "ask entries not checked: jq is missing or the file is not valid JSON" \
                ask-entries
        fi
    fi

    if declare -F ai_tools_providers__find_managed_file_missing_keys >/dev/null 2>&1; then
        exec {fd}< <(_pu_managed_pairs "${root}"); pid=$!
        while IFS=$'\t' read -r -u "${fd}" live reference; do
            [[ -f "${live}" && -f "${reference}" ]] || continue
            cmp -s "${live}" "${reference}" && continue
            if line="$(ai_tools_providers__find_managed_file_missing_keys "${live}" "${reference}")"; then
                entries=()
                [[ -n "${line}" ]] && mapfile -t entries <<< "${line}"
                for line in "${entries[@]}"; do _pu_write_finding key-missing file "${live}" "${line}" "${line}"; done
            else
                _pu_write_finding error file "${live}" "keys not checked: the file or its shipped copy is not readable" \
                    managed-keys
            fi
        done
        exec {fd}<&-
        _pu_wait_collector "${pid}" directory "${root}/etc" managed-pairs
    else
        _pu_write_finding error directory "${root}/etc" "managed-file keys not checked: ${PROVIDERS_LIB} did not load" \
            managed-keys
    fi

    exec {fd}< <(_pu_assets "${root}"); pid=$!
    while IFS=$'\t' read -r -u "${fd}" state stype path detail; do
        if [[ "${state}" == error ]]; then
            _pu_write_finding error "${stype}" "${path}" "${detail}" shared-assets
        else
            _pu_write_finding "${state}" "${stype}" "${path}" "${detail}"
        fi
    done
    exec {fd}<&-
    _pu_wait_collector "${pid}" directory "${root}/opt/ai-tools" assets

    exec {fd}< <(_pu_copies "${root}"); pid=$!
    while IFS= read -r -u "${fd}" path; do _pu_write_finding copy-kept file "${path}" ""; done
    exec {fd}<&-
    _pu_wait_collector "${pid}" directory "${root:-/}" copies
}

# _pu_settings_findings <file> <copy>: the findings _pu_settings_rest reports for a settings file and its baseline copy
# -- a rule the copy carries that the file does not, and any other setting that differs -- through the same two readers.
_pu_settings_findings() {
    local file="$1" copy="$2" gaps kind list rule scratch
    if ! gaps="$(ai_tools_settings_merge__find_permission_gaps "${file}" "${copy}")"; then
        _pu_write_finding error file "${file}" "permission rules not compared: jq is missing or a file is not valid JSON" \
            permission-rules
        return 0
    fi
    while IFS=$'\t' read -r kind list rule; do
        [[ "${kind}" == missing ]] && _pu_write_finding rule-missing file "${file}" "${list}: ${rule}" "${list}" "${rule}"
    done <<< "${gaps}"
    scratch="$(mktemp -d)" || return 0
    if ai_tools_settings_merge__read_settings_rest "${file}" > "${scratch}/file" \
            && ai_tools_settings_merge__read_settings_rest "${copy}" > "${scratch}/copy" \
            && ! cmp -s "${scratch}/file" "${scratch}/copy"; then
        _pu_write_finding rpmnew-differs file "${file}" "settings"
    fi
    rm -rf "${scratch}"
}

# _pu_kind_findings <root>: the provider list items operator.conf holds in an earlier release's bare form, read
# through the plan the rewrite follows (ai_tools_providers__plan_kind_migration, providers.lib.sh): list-unmigrated
# for a key the run would rewrite, list-unmigratable for an item no run can map, one row per item. A detection with no
# plan to read -- providers.lib.sh did not load -- is a check that could not run.
_pu_kind_findings() {
    local file="$1/etc/ai-tools/operator.conf" verdict key old new fd pid
    [[ -f "${file}" ]] || return 0
    if ! declare -F ai_tools_providers__plan_kind_migration >/dev/null 2>&1; then
        declare -F ai_tools_conf__find_unmigrated_items >/dev/null 2>&1 && [[ -n "$(ai_tools_conf__find_unmigrated_items "${file}")" ]] \
            && _pu_write_finding error file "${file}" "provider names not checked: ${PROVIDERS_LIB} did not load" \
                provider-names
        return 0
    fi
    exec {fd}< <(ai_tools_providers__plan_kind_migration "${file}"); pid=$!
    while IFS=$'\t' read -r -u "${fd}" verdict key old new; do
        case "${verdict}" in
            migrate) _pu_write_finding list-unmigrated file "${file}" "${key}: [${old// /, }] -> [${new// /, }]" "${key}" ;;
            blocked) _pu_write_finding list-unmigratable file "${file}" "${key}: ${old}" "${key}" "${old}" ;;
        esac
    done
    exec {fd}<&-
    _pu_wait_collector "${pid}" file "${file}" kind-plan
}

# _pu_registry_list <root>: `<operator>\t<uid>\t<state>\t<path>` per enrolled operator with a home on this host,
# the registry ai_tools_conf__resolve_registry picks in that operator's ~/.config/ai-tools under <root> -- OPERATORS
# read from <root>'s operator.conf (the AI_TOOLS_OPERATOR_CONF hook's file with no root), and each home from the account
# database, so the suite prefixes both.
_pu_registry_list() {
    local root="$1" user home uid state path conf="$1/etc/ai-tools/operator.conf"
    local -a operators=()
    [[ -n "${root}" ]] || conf="${AI_TOOLS_OPERATOR_CONF:-${OPERATOR_CONF}}"
    ai_tools_conf__read_list operators "${conf}" OPERATORS 2>/dev/null || return 0
    for user in "${operators[@]}"; do
        home="$(getent passwd "${user}" 2>/dev/null | cut -d: -f6)" || home=""
        uid="$(id -u "${user}" 2>/dev/null)" || continue
        [[ -n "${home}" ]] || continue
        IFS=$'\t' read -r state path < <(ai_tools_conf__resolve_registry "${root}${home}/.config/ai-tools")
        printf '%s\t%s\t%s\t%s\n' "${user}" "${uid}" "${state}" "${path}"
    done
}

# _pu_read_registry_refusal <registry-file>: print, on one line, why the launch gate refuses <registry-file> -- the
# library's refusal and its code, read through ai_tools_conf__check_registry as root, which takes the link rule from the
# file's own owner -- and return the check's status. Prints nothing and returns 0 for a file the gate reads.
_pu_read_registry_refusal() {
    local out line rc=0 code="" text=""
    out="$(ai_tools_conf__check_registry "$1" 2>&1 >/dev/null)" || rc=$?
    (( rc )) || return 0
    while IFS= read -r line; do
        if [[ "${line}" =~ ^MSG-[A-Z][0-9][A-Z][0-9]$ ]]; then code="${line}"; elif [[ -n "${line}" ]]; then text="${line#conf: }"; fi
    done <<< "${out}"
    printf '%s%s' "${text:-$1 cannot be read}" "${code:+ (${code})}"
    return "${rc}"
}

# _pu_registry_findings <root>: registry-unmigrated for each enrolled operator whose registry is
# the `~/.config/ai-tools/allowed-projects` an earlier release wrote and the legacy grammar reads, and registry-invalid
# for one no reader accepts, legacy or current, with the library's line. The source check is the migration's own (one
# lstat, the operator's file at 600), made here without the write. Each file is then read as the launch gate reads it,
# under the grammar the loader detects, so a line that grammar refuses, and an exclusion through a link the sandbox
# account can change, are registry-invalid with the library's line. A legacy file whose exclusion crosses such a link is
# registry-unmigrated as well, since the migration copies the line and the remedy is the same edit in either file.
_pu_registry_findings() {
    local root="$1" user uid state path kind owner mode reason refusal rc fd pid
    exec {fd}< <(_pu_registry_list "${root}"); pid=$!
    while IFS=$'\t' read -r -u "${fd}" user uid state path; do
        case "${state}" in
            legacy)
                reason=""
                IFS=: read -r kind owner mode < <(stat -c '%F:%u:%a' -- "${path}" 2>/dev/null) || kind=""
                case "${kind}" in
                    'regular file'|'regular empty file') ;;
                    '') reason="${path} cannot be read" ;;
                    *)  reason="${path} is a ${kind}, not a regular file" ;;
                esac
                [[ -n "${reason}" || "${owner}" == "${uid}" ]] || reason="${path} is held by uid ${owner}, not by ${user}"
                [[ -n "${reason}" || "${mode}" == 600 ]] || reason="${path} is mode ${mode}, not 600"
                rc=0; [[ -n "${reason}" ]] || refusal="$(_pu_read_registry_refusal "${path}")" || rc=$?
                (( rc == 0 || rc == 2 )) || reason="${refusal}"
                if [[ -n "${reason}" ]]; then
                    _pu_write_finding registry-invalid file "${path}" "${reason}" "${user}"
                else
                    (( rc == 0 )) || _pu_write_finding registry-invalid file "${path}" "${refusal}" "${user}"
                    _pu_write_finding registry-unmigrated file "${path}" "${user}: migrates to ${path%/*}/project-registry" "${user}"
                fi ;;
            current)
                rc=0; refusal="$(_pu_read_registry_refusal "${path}")" || rc=$?
                case "${rc}" in
                    0) ;;
                    2|3) _pu_write_finding registry-invalid file "${path}" "${refusal}" "${user}" ;;
                    *) _pu_write_finding registry-invalid file "${path}" "${path} cannot be read" "${user}" ;;
                esac ;;
        esac
    done
    exec {fd}<&-
    _pu_wait_collector "${pid}" file "${root}/etc/ai-tools/operator.conf" registries
}

# _pu_orphans <root>: each package copy whose file is gone, one path per line. rpm parks a copy only beside a file it
# kept, so one of these is a file removed afterwards, and neither the report nor a merge otherwise reaches it. Exits
# non-zero when a directory walk failed, since a copy it did not see is then not known to be absent.
_pu_orphans() {
    local root="$1" entry file kind label dir fd pid
    {
        for entry in "${POSTUPGRADE_FILES[@]}"; do
            IFS='|' read -r file kind label <<< "${entry}"
            [[ -f "${root}${file}.rpmnew" && ! -e "${root}${file}" ]] && printf '%s\n' "${root}${file}.rpmnew"
        done
        for dir in "${POSTUPGRADE_DIRS[@]}"; do
            [[ -d "${root}${dir}" ]] || continue
            exec {fd}< <(find "${root}${dir}" -maxdepth 3 -type f -name '*.rpmnew' 2>/dev/null); pid=$!
            while IFS= read -r -u "${fd}" file; do
                [[ -e "${file%.rpmnew}" ]] || printf '%s\n' "${file}"
            done
            exec {fd}<&-
            wait "${pid}" || exit 1
        done
        exit 0
    } | sort -u
}

# _pu_copies <root>: the dated copies this stack keeps as recovery material, in the order they were made -- a .bak
# beside a config file a merge replaced, a .shipped baseline left when a merge could not run, a .retired managed file
# an agent package replaced, and a withdrawn skill or subagent under /opt/ai-tools/retired. None is rpm's, and none is
# read by any command. Exits non-zero when a directory walk failed, for the reason _pu_orphans states.
_pu_copies() {
    local root="$1" dir
    {
        for dir in "${POSTUPGRADE_DIRS[@]}" /etc/sudoers.d; do
            [[ -d "${root}${dir}" ]] || continue
            find "${root}${dir}" -maxdepth 3 -type f \
                \( -name '*.[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]*.bak' \
                   -o -name '*.[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]*.shipped' \
                   -o -name '*.[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]*.retired' \) 2>/dev/null || exit 1
        done
        if [[ -d "${root}/opt/ai-tools/retired" ]]; then
            find "${root}/opt/ai-tools/retired" -mindepth 1 -maxdepth 1 -name '*.retired' 2>/dev/null || exit 1
        fi
        exit 0
    } | _pu_sort_copies
}

# _pu_asset_line <finding> <expected-type> <path> <detail>: one _pu_assets line about <path>, its subject-type read
# under the policy records.rule.md states (ref-section-u5t7), <expected-type> being the type expected at <path>.
_pu_asset_line() {
    local subject_type read_status=0
    subject_type="$(ai_tools_records_base__read_subject_type "$3")" || read_status=$?
    (( read_status == 0 )) || subject_type="$2"
    printf '%s\t%s\t%s\t%s\n' "$1" "${subject_type}" "$3" "$4"
    (( read_status == 0 || read_status == 2 )) && return 0
    printf 'error\t%s\t%s\t%s\n' "$2" "$3" \
        "the type of the entry here could not be read (it is not a symbolic link, a directory or a regular file, or the read failed), so the $1 line about it carries the type expected here, $2"
}

# _pu_assets <root>: `<finding> TAB <subject-type> TAB <path> TAB <detail>` for each shipped skill, subagent
# and orientation text whose live copy or agent link is not the one provisioning places -- the seeding
# that `system bootstrap` and base's scriptlet run. Each line's subject-type is what is at its path (_pu_asset_line);
# where no entry is there, the kind's type for a live copy -- `directory` for a skill, `file` otherwise -- and `link`
# for an agent's link, the types provisioning places. A live asset that is absent or an empty directory is missing:
# a session is not offered it. A real file where an agent's link belongs, or a live asset without the managed marker, is
# the operator's override and is left to them. A symlink at the live name is the assets resolver's view link and is not
# compared. Checked only where the pristine assets are installed, under <root>.
_pu_assets() {
    local root="$1" src_root="$1/usr/share/ai-tools" live_root="$1/opt/ai-tools"
    local kind glob src name marker dst dst_marker cur new field agent dir link target kind_type
    [[ -d "${src_root}" ]] || return 0
    # shellcheck source=SCRIPTDIR/../../lib/ai-tools/control-plane.lib.sh
    source /usr/local/lib/ai-tools/control-plane.lib.sh 2>/dev/null || true
    # shellcheck source=SCRIPTDIR/../../lib/ai-tools/managed-assets.lib.sh
    source /usr/local/lib/ai-tools/managed-assets.lib.sh 2>/dev/null || true
    # The sanitizer is required: a link's target is a string the sandbox account can write, since an agent's config
    # directory is group-writable, and this report prints it on root's terminal.
    # shellcheck source=SCRIPTDIR/../../lib/ai-tools/log.lib.sh
    source /usr/local/lib/ai-tools/log.lib.sh 2>/dev/null || true
    if ! declare -F ai_tools_managed_assets__is_managed >/dev/null 2>&1 \
            || ! declare -F ai_tools_control_plane__list_agent_asset_dirs >/dev/null 2>&1 \
            || ! declare -F ai_tools_log__sanitize >/dev/null 2>&1 \
            || ! declare -F ai_tools_records_base__read_subject_type >/dev/null 2>&1; then
        printf 'error\tdirectory\t%s\t%s\n' "${live_root}" "shared assets not checked: the asset libraries did not load"
        return 0
    fi
    for kind in "${AI_TOOLS_MANAGED_ASSETS__KINDS[@]}"; do
        case "${kind}" in
            skills)      glob="${src_root}/skills/ai-tools-*/";      field=skills_dir;    kind_type="directory" ;;
            subagents)   glob="${src_root}/subagents/ai-tools-*.md"; field=subagents_dir; kind_type="file" ;;
            orientation) glob="${src_root}/orientation/AGENTS.md";  field="";            kind_type="file" ;;
            *)           continue ;;
        esac
        for src in ${glob}; do
            [[ -e "${src}" ]] || continue
            name="$(basename "${src}")"
            ai_tools_managed_assets__is_retired "${kind}" "${name}" && continue
            if [[ -d "${src}" ]]; then marker="${src%/}/SKILL.md"; else marker="${src}"; fi
            ai_tools_managed_assets__is_managed "${marker}" || continue
            dst="${live_root}/${kind}/${name}"
            # A symlink at the live name is the assets resolver's view link, not a seeded copy: its marker and version
            # are not compared, and its per-agent links are reconcile's.
            [[ -L "${dst}" ]] && continue
            if [[ -d "${src}" ]]; then dst_marker="${dst}/SKILL.md"; else dst_marker="${dst}"; fi
            if [[ ! -e "${dst}" ]] || { [[ -d "${dst}" ]] && [[ -z "$(find "${dst}" -mindepth 1 -print -quit)" ]]; }; then
                _pu_asset_line asset-missing "${kind_type}" "${dst}" "not seeded -- sessions are not offered it"
                continue
            fi
            if ! ai_tools_managed_assets__is_managed "${dst_marker}"; then
                _pu_asset_line asset-overridden "${kind_type}" "${dst}" "not ai-tools-managed, so provisioning leaves it"
                continue
            fi
            new="$(ai_tools_managed_assets__read_asset_version "${marker}")"
            cur="$(ai_tools_managed_assets__read_asset_version "${dst_marker}")"
            [[ -n "${new}" && -n "${cur}" && "${new}" -gt "${cur}" ]] \
                && _pu_asset_line asset-outdated "${kind_type}" "${dst}" "v${cur} live, v${new} shipped"
            while IFS=$'\t' read -r agent dir; do
                [[ -n "${agent}" ]] || continue
                if [[ -n "${field}" ]]; then link="${root}${dir}/${name}"; else link="${root}${dir}"; fi
                [[ -d "${link%/*}" ]] || continue
                if [[ -L "${link}" ]]; then
                    target="$(readlink "${link}")"
                    [[ "${target}" == "${dst}" || "${target}" == "${CP_HOME}/${kind}/${name}" ]] \
                        || _pu_asset_line asset-unlinked link "${link}" \
                               "${agent}: points at $(ai_tools_log__sanitize "${target}")"
                elif [[ -e "${link}" ]]; then
                    _pu_asset_line asset-overridden link "${link}" \
                        "${agent}: a file of its own in place of the link"
                else
                    _pu_asset_line asset-unlinked link "${link}" "${agent}: no link"
                fi
            done < <(if [[ -n "${field}" ]]; then ai_tools_control_plane__list_agent_asset_dirs "${field}"
                     else ai_tools_control_plane__list_agent_memory_targets; fi)
        done
    done
    return 0
}

# _pu_kind_noun <KEY>: what an item of a kind-prefixed list key names, for a report line.
_pu_kind_noun() {
    case "$1" in
        AI_TOOLS_AGENTS)       printf 'agent' ;;
        AI_TOOLS_INTEGRATIONS) printf 'integration' ;;
        *)                     printf 'filter set' ;;
    esac
}

# _pu_kind_migrate <root>: rewrite the provider list items operator.conf holds in an earlier release's bare form
# (ai_tools_providers__migrate_kinds, providers.lib.sh), whether or not a .rpmnew waits, and report each key in a block
# of its own. It runs on every run, the unattended one included: the rewrite changes spelling alone, and until it lands
# every session start is refused. A key holding a name no installed manifest or rule set matches is left as written
# and named, since only the operator knows what it meant.
#
# A rewritten AI_TOOLS_AGENTS enables agents that the unmigrated line left enabled nowhere, so no relabel covered them
# while it stood: an install run in that window restorecon'd the toolchain with no entrypoint rule to apply last,
# which leaves an entrypoint hardlinked to its platform package on that package's type. The reconciliation
# `system entrypoints relabel` runs (ai-tools-relabel-agent) therefore follows the rewrite, answering from an unchanged
# pin as the other unattended callers do. It is skipped under AI_TOOLS_POSTUPGRADE_ROOT, since the suite does not change
# the host's SELinux policy to test a helper.
_pu_kind_migrate() {
    local file="$1/etc/ai-tools/operator.conf" line verdict key old new opened=0 agents_rewritten=0
    [[ -f "${file}" ]] || return 0
    if ! declare -F ai_tools_providers__migrate_kinds >/dev/null 2>&1; then
        declare -F ai_tools_conf__find_unmigrated_items >/dev/null 2>&1 && [[ -n "$(ai_tools_conf__find_unmigrated_items "${file}")" ]] \
            || return 0
        _PU_NAME="${file##*/}"
        ai_tools_msg__headline "${_PU_NAME} -- provider names" 1 "${file}"
        _pu_say err "provider names were not rewritten: ${PROVIDERS_LIB} did not load -- reinstall ai-tools-base"
        return 0
    fi
    while IFS= read -r line; do
        IFS=$'\t' read -r verdict key old new <<< "${line}"
        if (( ! opened )); then
            opened=1
            _PU_NAME="${file##*/}"
            ai_tools_msg__headline "${_PU_NAME} -- provider names" 1 "${file}" \
                "each provider list item names its kind: agent-<name>, integration-<name>, filter-<name>"
        fi
        case "${verdict}" in
            backup)    _pu_say info "the file as it was is saved as ${key}" ;;
            rewritten) _pu_say ok "${key}: [${old// /, }] -> [${new// /, }]"
                       [[ "${key}" == AI_TOOLS_AGENTS ]] && agents_rewritten=1 ;;
            blocked)   _pu_say act "${key} holds ${old}, which names no installed $(_pu_kind_noun "${key}") -- the line is left as written and enables nothing; edit it by hand" ;;
            failed)    _pu_say err "${key} was not rewritten: ${new} -- the line is left as written" ;;
        esac
    done < <(ai_tools_providers__migrate_kinds "${file}")
    (( agents_rewritten )) && [[ -z "${AI_TOOLS_POSTUPGRADE_ROOT:-}" ]] || return 0
    if [[ ! -x "${RELABEL_ENTRYPOINT_BIN}" ]]; then
        _pu_say info "the entrypoints were not reconciled: ${RELABEL_ENTRYPOINT_BIN} is not installed"
    elif AI_TOOLS_ENTRYPOINT_PIN_REUSE=1 "${RELABEL_ENTRYPOINT_BIN}"; then
        _pu_say ok "the enabled agents' entrypoints are reconciled (verified and labelled)"
    else
        _pu_say err "the entrypoint reconciliation reported a problem above -- re-run it: atr admin system entrypoints relabel"
    fi
}

# _pu_registry_migrate <root>: migrate every enrolled operator's `~/.config/ai-tools/allowed-projects`
# to `~/.config/ai-tools/project-registry` (the plan and the write are ai_tools_conf__plan_registry_migration
# and ai_tools_conf__migrate_registry, conf.lib.sh), one block per operator, on every run, the unattended one included.
# What the migration keeps, drops and names is conf.lib.sh's (NOTE-Q2D8); this block prints the plan row by row first,
# names a registry no reader accepts, legacy or current, and leaves it as written, migrates a legacy file the gate
# refuses only for an exclusion through a link and names the line after its rows, and writes as root
# through the operator's own 0700 directory, the new file taking the source's owner and mode.
_pu_registry_migrate() {
    local root="$1" user uid state path verdict number raw hint sidecar rc scratch notes registry
    while IFS=$'\t' read -r user uid state path; do
        case "${state}" in
            current)
                ai_tools_conf__check_registry "${path}" 2>/dev/null && continue
                _PU_NAME="registry"
                ai_tools_msg__headline "project registry -- ${user}" 1 "${path}"
                { ai_tools_conf__check_registry "${path}" 2>&1 || true; } | sed 's/^/    /'
                _pu_say err "no reader accepts this registry, so no session starts under it -- fix the line named above"
                continue ;;
            legacy) ;;
            *) continue ;;
        esac
        _PU_NAME="registry"
        ai_tools_msg__headline "project registry -- ${user}" 1 "${path}" \
            "becomes ${path%/*}/project-registry, format 1.0: the marker, then each entry with its sign, in canonical order"
        notes=0
        while IFS=$'\t' read -r verdict number raw hint; do
            case "${verdict}" in
                marker) _pu_say info "new first line: ${raw}" ;;
                keep)   _pu_say info "line ${number}: ${raw}" ;;
                drop)   _pu_say info "line ${number}: ${raw} -- a comment line, dropped; format 1.0 carries a note at the end of an entry" ;;
                refuse) _pu_say err "line ${number}: ${raw} -- ${hint}" ;;
                note)   _pu_say act "line ${number}: ${raw} -- ${hint}"; notes=$(( notes + 1 )) ;;
            esac
        done < <(ai_tools_conf__plan_registry_migration "${path}" 2>/dev/null)
        # Each `!` keeps its sign: one no project encloses excludes its path in either format, and only the operator
        # knows whether it was a disabled project, so the block closes by counting them once.
        if (( notes > 0 )); then
            _pu_say act "${notes} '!' line(s) above read as excluded paths in format 1.0, so no session starts there, as before -- to keep one as a disabled project that atr projects enable restores, change its '!' to '%'"
        fi
        # Called in this shell, not a capture, so the refusal it leaves in AI_TOOLS_CONF__MIGRATION_REFUSAL is read
        # here.
        rc=0; scratch="$(mktemp)" || { _pu_say err "not migrated: a scratch file cannot be created"; continue; }
        ai_tools_conf__migrate_registry "${path}" "${path%/*}/project-registry" "${uid}" > "${scratch}" 2>/dev/null || rc=$?
        sidecar="$(cat "${scratch}")"; rm -f "${scratch}"
        case "${rc}" in
            0) _pu_say ok "written ${path%/*}/project-registry; the file as it was is saved as ${sidecar}" ;;
            2|3) _pu_say err "not migrated: ${AI_TOOLS_CONF__MIGRATION_REFUSAL} -- fix it, then re-run atr admin system post-upgrade" ;;
            *) _pu_say err "not migrated: ${AI_TOOLS_CONF__MIGRATION_REFUSAL} -- ${path} stays the registry; re-run atr admin system post-upgrade" ;;
        esac
        # The new file keeps every entry, so an exclusion through a link the gate refuses refuses it as it did the old
        # one, and the same edit clears it in either.
        registry="${path}"; (( rc == 0 )) && registry="${path%/*}/project-registry"
        if ! ai_tools_conf__check_registry "${registry}" 2>/dev/null; then
            { ai_tools_conf__check_registry "${registry}" 2>&1 || true; } | sed 's/^/    /'
            _pu_say err "no reader accepts ${registry}, so no session starts under it -- fix the line named above"
        fi
    done < <(_pu_registry_list "${root}")
}

# _pu_report: the reconciliation itself, one block per file with a copy waiting, then the closing lines.
_pu_report() {
    local file kind label title found=0 attention_before copy
    local root="${AI_TOOLS_POSTUPGRADE_ROOT:-}"
    # Files whose copy still differs after their treatment, which is when a side-by-side comparison has something
    # to show, and copies byte-identical to their file, which have nothing to show and are only named for removal.
    local -a to_compare=() identical=()

    _pu_kind_migrate "${root}"
    _pu_registry_migrate "${root}"
    local -a references=() entries=() clock_lines=()
    local entry line
    mapfile -t entries < <(_pu_entries "${root}")
    # A file or copy dated after the clock says the clock is behind, and the copy this run would compare a file with is
    # the newest by date: the block names the clock as the first thing to correct, and this run does not compare a file.
    if ! _pu_clock_lines clock_lines "${entries[@]+"${entries[@]}"}"; then
        _PU_NAME="clock"
        ai_tools_msg__headline "the system clock is behind a file this command orders by date" 1 "${clock_lines[@]}"
        warn MSG-S5S2 "the system clock reads $(date '+%Y-%m-%d %H:%M:%S'), earlier than the file(s) above -- the copy a file is compared with is the newest by date, so set the clock first (timedatectl set-time, or chronyc makestep once a time source is reachable), then re-run this command; no file was compared"
        _PU_ATTENTION=$(( _PU_ATTENTION + 1 ))
        found=1
        entries=()
    fi
    for entry in "${entries[@]+"${entries[@]}"}"; do
        IFS='|' read -r file kind label copy <<< "${entry}"
        found=1
        references+=("${copy}")
        # A copy byte-identical to the file does not add an option or a line of prose to it, whatever its format, so it
        # does not get a block of its own: it is listed for removal under the closing line.
        if cmp -s "${file}" "${copy}"; then
            identical+=("${copy}")
            continue
        fi
        _PU_NAME="${file##*/}"
        if [[ "${label}" == "${_PU_NAME}" ]]; then title="${_PU_NAME}"; else title="${_PU_NAME} -- ${label}"; fi
        ai_tools_msg__headline "${title}" 1 "${file}" "$(_pu_provenance "${copy}")"
        attention_before="${_PU_ATTENTION}"
        case "${kind}" in
            json)   _pu_json   "${file}" "${copy}" ;;
            keyval) _pu_keyval "${file}" "${copy}" ;;
            review) _pu_review "${file}" "${copy}" ;;
            show)   _pu_show   "${file}" "${copy}" ;;
        esac
        (( _PU_ATTENTION > attention_before )) && ! cmp -s "${file}" "${copy}" && to_compare+=("${file}")
    done

    _pu_orphan_report "${root}"
    _pu_ask_gaps "${root}"
    _pu_key_gaps "${root}"
    _pu_asset_report "${root}"
    _pu_sidecars "${root}" "${references[@]+"${references[@]}"}"
    _pu_prune_merge_copies "${root}"
    printf '\n'
    # The closing line takes the colour of the worst line above it: red for an error, yellow for anything else to act
    # on. A copy identical to its file is not something to act on, so it does not colour the line; it is named
    # as reconciled rather than as "nothing to do", since the copies are listed for removal under it.
    if (( _PU_ERRORS > 0 )); then
        printf '%sPost-upgrade done -- review the warnings and errors above%s\n' "${_PU_RED}" "${_PU_RST}"
    elif (( _PU_ATTENTION > 0 )); then
        printf '%sPost-upgrade done -- review the warnings above%s\n' "${_PU_YEL}" "${_PU_RST}"
    elif (( found == 0 )); then
        printf 'Post-upgrade done -- no package or installer copy is waiting, so every config file this stack owns is reconciled\n'
    elif (( ${#identical[@]} > 0 )); then
        printf 'Post-upgrade done -- every config file is reconciled, and its baseline copy is identical to it\n'
    else
        printf 'Post-upgrade done -- nothing needs your attention\n'
    fi
    # The comparison command is printed on a line of its own, indented, so it copies whole. It is offered only while
    # a file still differs from its copy in a way the report asked the operator to act on.
    if (( ${#to_compare[@]} > 0 )); then
        printf '\n%sCompare a file with its baseline copy side by side, and carry over what you want:%s\n\n' \
            "${_PU_DIM}" "${_PU_RST}"
        printf '  %s%s%s\n\n' "${_PU_DIM}" "$(_pu_merge_command '<file>' '<copy>')" "${_PU_RST}"
        command -v meld >/dev/null 2>&1 \
            || printf '%s• meld is not installed; for a side-by-side view in a desktop session: sudo dnf install meld%s\n' \
                "${_PU_DIM}" "${_PU_RST}"
    fi
    if (( ${#identical[@]} > 0 )); then
        printf '%s• baseline copies identical to their files, to remove when you are ready:%s\n' "${_PU_DIM}" "${_PU_RST}"
        for copy in "${identical[@]}"; do printf '    %ssudo rm %s%s\n' "${_PU_DIM}" "${copy}" "${_PU_RST}"; done
    fi
    printf '%s• system post-upgrade is idempotent -- re-run it at any time%s\n' "${_PU_DIM}" "${_PU_RST}"
    if (( _PU_ATTENTION > 0 )); then
        printf '%s• Happy merging!%s\n' "${_PU_DIM}" "${_PU_RST}"
    else
        printf "%s• All settings merged. You're good to go!%s\n" "${_PU_DIM}" "${_PU_RST}"
    fi
}

# ── status ───────────────────────────────────────────────────────────────────────────────────
# `atr status` read from root's vantage -- one resource reported twice, not a second report. Which readings root adds
# and why the two agree is .claude/rules/cli.rule.md; what this file contributes is the rendering, in the plain
# bracket-token idiom the rest of this tool uses. The verdicts come from services.lib.sh and relabel.lib.sh, which emit
# data and leave every consumer to format it.
readonly SERVICES_LIB="/usr/local/lib/ai-tools/services.lib.sh"
readonly RELABEL_LIB="/usr/local/lib/ai-tools/relabel.lib.sh"
readonly ENTRYPOINT_VERIFY_LIB="/usr/local/lib/ai-tools/entrypoint-verify.lib.sh"
readonly TOOLCHAIN_LIB="/usr/local/lib/ai-tools/toolchain.lib.sh"
# The cache cleanup state and its per-group verdicts, read by status_caches alone, which loads it.
readonly CACHE_CLEANUP_LIB="/usr/local/lib/ai-tools/cache-cleanup.lib.sh"
readonly CONFINEMENT_LIB="/usr/local/lib/ai-tools/confinement.lib.sh"
# AI_TOOLS_LAUNCHER_DIR is the hook the CLI and relabel.lib.sh read for the same directory,
# so tests/unit/admin-status.sh drives this report against fixture links. It is a root-only test hook here
# (tests.rule.md, ref-section-b4y9), and it moves a report: no access decision here reads it.
readonly LAUNCHER_LINK_DIR="${AI_TOOLS_LAUNCHER_DIR:-/opt/ai-tools/bin}"

# st <state> <text>: one report line, state in a bracket token so a scan down the left column finds what needs
# attention. The vocabulary is the CLI's -- OK, DOWN, FAILED, STALE, SKIPPED, n/a, ? -- because an administrator reads
# both reports about one host and a second set of words for the same states would read as a second set of facts.
st()      { printf '    %-13s %s\n' "[$1]" "$2"; }
detail()  { printf '                  %s\n' "$*"; }
heading() { printf '\n  %s\n\n' "$*"; }

# The two counts status() folds into its exit (ai-tools-records(5)): STATUS_PROBLEMS, a fault a section read,
# and STATUS_UNREADABLE, a reading a section could not make because a library base ships did not load. A `?` line counts
# toward neither (cli.rule.md, ref-section-r8u5).
STATUS_PROBLEMS=0
STATUS_UNREADABLE=0

# status_services: every unit in the shared registry, with its consequence and remedy where one needs attention. Renders
# to stdout and counts the units needing attention in STATUS_PROBLEMS.
status_services() {
    heading "Services"
    local rec unit scope stamp mode state age when exit_code reason remedy uid
    while IFS= read -r rec; do
        unit="$(ai_tools_services__get_field "${rec}" 1)"
        scope="$(ai_tools_services__get_field "${rec}" 2)"
        stamp="$(ai_tools_services__get_field "${rec}" 7)"
        mode="$(ai_tools_services__get_field "${rec}" 8)"
        state="$(ai_tools_services__read_record_state "${rec}")"
        age=""; when=""
        if [[ -n "${stamp}" ]]; then
            age="$(ai_tools_services__format_age "$(ai_tools_services__read_stamp_age "${stamp}")")"
            [[ -n "${age}" ]] && when="last run ${age}"
        fi
        case "${state}" in
            active)  st OK "${unit}${when:+  ${when}}" ;;
            down)    st DOWN "${unit}" ;;
            skipped) reason="$(ai_tools_services__read_stamp_field "${stamp}" REASON)"
                     st SKIPPED "${unit}  ${when:-last run at an unknown time}${reason:+, ${reason}} -- nothing was changed" ;;
            failed)  if [[ -n "${stamp}" ]]; then
                         exit_code="$(ai_tools_services__read_stamp_field "${stamp}" EXIT_CODE)"
                         st FAILED "${unit}  ${when:-last run at an unknown time}, exit ${exit_code:-?}"
                     else
                         exit_code="$(ai_tools_services__read_unit_property "${unit}" ExecMainStatus "${scope}")"
                         st FAILED "${unit}  its last run exited ${exit_code:-non-zero}"
                     fi ;;
            stale)   st STALE "${unit}  ${when:-last run long ago}" ;;
            absent)  st "n/a" "${unit}  not installed" ;;
            # The `?` readings, told apart by checks that completed, and why the oneshot line does not name a command:
            # cli.rule.md (ref-section-r8u5).
            *)       if [[ "${scope}" == system ]] && ! command -v systemctl >/dev/null 2>&1; then
                         st "?" "${unit}  systemctl is unavailable here"
                     elif [[ "${scope}" == system ]] && declare -F ai_tools_services__is_oneshot_unrun >/dev/null 2>&1 \
                             && ai_tools_services__is_oneshot_unrun "${unit}"; then
                         st "?" "${unit}  has not run since boot -- a oneshot's state is its last run's, known after its first"
                     elif [[ "${scope}" == system ]]; then
                         st "?" "${unit}  its state could not be read -- systemctl status ${unit} shows it"
                     elif declare -F ai_tools_services__is_stamp_unwritten >/dev/null 2>&1 \
                             && ai_tools_services__is_stamp_unwritten "${stamp}"; then
                         st "?" "${unit}  no run recorded yet -- its first scheduled run has not happened"
                     else
                         st "?" "${unit}  neither its manager nor a last-run stamp could be read"
                     fi ;;
        esac
        ai_tools_services__is_attention_needed "${state}" || continue
        STATUS_PROBLEMS=$(( STATUS_PROBLEMS + 1 ))
        detail "$(ai_tools_services__get_field "${rec}" 5)"
        # A sandbox-user unit's commands name the sandbox ACCOUNT, and services.lib.sh is deployed with no
        # ai-tools substitution, so they are composed here -- the same split the CLI makes, and the reason
        # the registry's remedy field is empty for those units.
        if [[ "${scope}" != system ]]; then
            detail "sudo systemctl --user -M ${SANDBOX_USER}@.host status ${unit}"
            uid="$(id -u "${SANDBOX_USER}" 2>/dev/null || true)"
            [[ -n "${uid}" ]] \
                && detail "sudo journalctl _SYSTEMD_USER_UNIT=${unit} _UID=${uid} -n 50 --no-pager"
            detail "sudo systemctl --user -M ${SANDBOX_USER}@.host restart ${unit}"
        fi
        remedy="$(ai_tools_services__read_remedy "${rec}")"
        [[ -n "${remedy}" ]] && detail "${remedy}"
    done < <(ai_tools_services__list_records)
    return 0
}

# status_operator_conf [operator-conf]: whether the sandbox account can read operator.conf. Root reads any file,
# so the reading is made as that account, through runuser over the root-owned /usr/bin/test, which does not execute
# a file the account can write. A file that account cannot read leaves nvm-update and ai-tools-run
# on the default-enabled providers with the declared set unknown (ai_tools_providers__read_gate), so it is counted.
# A control run of /usr/bin/true first separates "cannot read" from a runuser that did not run, which reads `?` and is
# not counted. Prints no line for a readable file or a host without the file or the account. <operator-conf> is
# a parameter so a unit test drives it over a fixture.
status_operator_conf() {
    local operator_conf="${1:-${OPERATOR_CONF}}" meta
    [[ -e "${operator_conf}" ]] || return 0
    id -u "${SANDBOX_USER}" >/dev/null 2>&1 || return 0
    if ! runuser -u "${SANDBOX_USER}" -- /usr/bin/true >/dev/null 2>&1; then
        st "?" "cannot test whether ${SANDBOX_USER} reads ${operator_conf} -- runuser did not run as that account"
        return 0
    fi
    runuser -u "${SANDBOX_USER}" -- /usr/bin/test -r "${operator_conf}" >/dev/null 2>&1 && return 0
    STATUS_PROBLEMS=$(( STATUS_PROBLEMS + 1 ))
    meta="$(stat -c '%F, owner=%U:%G, mode=%a' -- "${operator_conf}" 2>/dev/null)" || meta="could not be stat-ed"
    st "NOT READABLE" "${SANDBOX_USER} cannot read ${operator_conf} (${meta}) -- sessions and the updater read the default-enabled providers alone, and each updater run fails"
    detail "atr admin system layout reconcile"
    return 0
}

# status_registries: every enrolled operator's project registry, read as root through each 0700 config directory and
# as the launch gate reads it (ai_tools_conf__check_registry): one no reader accepts or that cannot be read,
# and a `~/.config/ai-tools/allowed-projects` an earlier release wrote, each count, with what clears it,
# since the projects verbs refuse over both and a refused file stops every launch under it. An operator with no registry
# is named and not counted, since enrolment seeds one.
status_registries() {
    local user uid state path rc
    heading "Project registries"
    while IFS=$'\t' read -r user uid state path; do
        if [[ "${state}" == none ]]; then
            st "n/a" "${user}: no registry at ${path}"
            detail "atr admin operators add ${user}"
            continue
        fi
        rc=0; ai_tools_conf__check_registry "${path}" 2>/dev/null || rc=$?
        if (( rc != 0 )); then
            STATUS_PROBLEMS=$(( STATUS_PROBLEMS + 1 ))
            if (( rc == 1 )); then
                st "REFUSED" "${user}: ${path} cannot be read, so no session starts under it"
            else
                st "REFUSED" "${user}: ${path} holds a line no reader accepts, so no session starts under it"
            fi
            { ai_tools_conf__check_registry "${path}" 2>&1 || true; } \
                | while IFS= read -r line; do detail "${line}"; done
        elif [[ "${state}" == legacy ]]; then
            STATUS_PROBLEMS=$(( STATUS_PROBLEMS + 1 ))
            st "NOT MIGRATED" "${user}: ${path} is the allowed-projects an earlier release wrote; the projects verbs refuse until it is migrated"
            detail "atr admin system post-upgrade"
        else
            st OK "${user}: ${path} (format $(ai_tools_conf__read_registry_format "${path}"))"
        fi
    done < <(_pu_registry_list "")
    return 0
}

# status_provisioning: per enabled agent, whether its stable launcher link exists -- bootstrap's last artifact
# for that agent, and the link the CLI's bootstrap gate reads, so the two reports answer alike. The link is tested
# per launcher, since the launcher directory also holds the base package's own files and is never empty on an installed
# host. Counted as the CLI counts it (cli.rule.md): an unprovisioned agent and an empty enabled set are reported and not
# counted, since an unfinished install is what the section exists to say; a resolver that did not load is a reading this
# section could not make, so the report exits 5.
status_provisioning() {
    heading "Provisioning"
    status_operator_conf "${AI_TOOLS_OPERATOR_CONF:-${OPERATOR_CONF}}"
    if ! declare -F ai_tools_providers__list_enabled_agents >/dev/null 2>&1; then
        st UNREADABLE "cannot read the enabled agents -- ${PROVIDERS_LIB} did not load; reinstall ai-tools-base"
        STATUS_UNREADABLE=$(( STATUS_UNREADABLE + 1 ))
        return 0
    fi
    local agent launcher reason
    local -a agents=()
    mapfile -t agents < <(ai_tools_providers__list_enabled_agents 2>/dev/null)
    if (( ${#agents[@]} == 0 )); then
        reason=""
        declare -F ai_tools_providers__evaluate_empty_agents >/dev/null 2>&1 \
            && IFS=$'\t' read -r _ reason <<< "$(ai_tools_providers__evaluate_empty_agents)"
        st "n/a" "no agent enabled${reason:+ -- ${reason}}"
        return 0
    fi
    for agent in "${agents[@]}"; do
        IFS=$'\t' read -r agent _ launcher <<< "${agent}"
        if [[ -n "${launcher}" && -L "${LAUNCHER_LINK_DIR}/${launcher}" ]]; then
            st OK "${agent} provisioned (${launcher})"
        else
            st MISSING "${agent} has no launcher in ${LAUNCHER_LINK_DIR} -- it is not provisioned"
            detail "atr admin system bootstrap"
        fi
    done
    return 0
}

# status_managed_files: per enabled agent, each managed file its manifest names (managed_files, ai-tools-providers(5))
# whose live copy is not the shipped one -- the same reading `atr status` makes, rendered in this tool's table.
# The package never overwrites such a file, so this line is where an operator learns it differs. An edited file is
# a supported state and is not counted; a missing one is, since the package is then broken. Prints no line for a file
# matching the shipped copy. Without the resolver status_provisioning has already said so; a resolver that loaded
# without its managed-files reader or the comparison it runs per file is a reading this section could not make.
status_managed_files() {
    declare -F ai_tools_providers__list_enabled_agents >/dev/null 2>&1 || return 0
    if ! declare -F ai_tools_providers__list_agent_managed_files >/dev/null 2>&1 \
            || ! declare -F ai_tools_providers__evaluate_managed_file >/dev/null 2>&1; then
        st UNREADABLE "managed files: ${PROVIDERS_LIB} did not load its managed-files reader; reinstall ai-tools-base"
        STATUS_UNREADABLE=$(( STATUS_UNREADABLE + 1 ))
        return 0
    fi
    local agent live reference state
    while IFS=$'\t' read -r agent _ _; do
        [[ -n "${agent}" ]] || continue
        while IFS=$'\t' read -r live reference; do
            [[ -n "${live}" ]] || continue
            state="$(ai_tools_providers__evaluate_managed_file "${live}" "${reference}")"
            case "${state}" in
                shipped) ;;
                edited)  st edited "${agent}  ${live} differs from the shipped copy"
                         detail "${agent} reads the live file alone: a key this release adds is not in it, and what it declares is the host's"
                         detail "shipped copy: ${reference}" ;;
                missing) st MISSING "${agent}  ${live} is missing -- reinstall the ${agent} package"
                         STATUS_PROBLEMS=$(( STATUS_PROBLEMS + 1 )) ;;
                *)       st "?" "${agent}  ${live} cannot be compared with the shipped copy ${reference}" ;;
            esac
        done < <(ai_tools_providers__list_agent_managed_files "${agent}")
    done < <(ai_tools_providers__list_enabled_agents 2>/dev/null)
    return 0
}

# status_entrypoints: per enabled agent, the two halves of the entrypoint reconciliation -- the pin
# ai_tools_entrypoint_verify__write_pin leaves, and the type its paths carry now. Reported together because they fail
# independently: verification can succeed while labelling does not, leaving a green pin written by the very run
# whose labelling failed.
#
# Only two states here count toward the exit status, and both stop the next launch: an unpinned entrypoint
# where AI_TOOLS_REQUIRE_ENTRYPOINT_VERIFY is set, and a mislabelled path. Unpinned without that setting is a supported
# state -- an air-gapped host, a release published without a manifest -- so it is reported and left uncounted.
status_entrypoints() {
    heading "Entrypoints"
    # The provider, entrypoint, relabel and service libraries answer this section between them, and a partial load must
    # report that rather than reach an undefined function -- which under `set -e` would abort the whole report
    # over the section it could not give. Each ships with base, so a missing one is a reading this section could not
    # make, not a `?`. Every reader an agent's line calls is checked here, the ones entrypoint_live_verdict
    # and entrypoint_stale_mark call included: a reader missing past this point would leave the pin's own fields
    # to render as VERIFIED or UNCHANGED for a binary the report did not hash, and a missing verification-policy reader
    # would render an unpinned agent as not blocked on a host that requires verification. The live labels have a reader
    # and a guard of their own, so status_labels runs on this branch too.
    if ! declare -F ai_tools_providers__list_enabled_agents      >/dev/null 2>&1 \
            || ! declare -F ai_tools_providers__read_agent_manifest_field  >/dev/null 2>&1 \
            || ! declare -F ai_tools_entrypoint_verify__get_pin_path   >/dev/null 2>&1 \
            || ! declare -F ai_tools_entrypoint_verify__get_stale_path >/dev/null 2>&1 \
            || ! declare -F ai_tools_entrypoint_verify__read_pin_kind  >/dev/null 2>&1 \
            || ! declare -F ai_tools_entrypoint_verify__check          >/dev/null 2>&1 \
            || ! declare -F ai_tools_entrypoint_verify__is_required    >/dev/null 2>&1 \
            || ! declare -F ai_tools_relabel__resolve_agent_entrypoint_path >/dev/null 2>&1 \
            || ! declare -F ai_tools_services__read_stamp_field   >/dev/null 2>&1 \
            || ! declare -F ai_tools_services__read_stamp_age     >/dev/null 2>&1 \
            || ! declare -F ai_tools_services__format_age       >/dev/null 2>&1; then
        st UNREADABLE "the provider, entrypoint, relabel or service libraries did not load -- no entrypoint was read; reinstall ai-tools-base"
        STATUS_UNREADABLE=$(( STATUS_UNREADABLE + 1 ))
        status_labels
        return 0
    fi

    local agent pin version age live strict=no seen=0
    ai_tools_entrypoint_verify__is_required && strict=yes
    while IFS=$'\t' read -r agent _ _; do
        [[ -n "${agent}" ]] || continue
        seen=1
        # Verification compares the binary against the checksum in the vendor's signed release manifest,
        # which an agent's package names in release_manifest_url. An agent whose package omits that key is reported
        # as such rather than as perpetually unverified.
        pin="$(ai_tools_entrypoint_verify__get_pin_path "${agent}" 2>/dev/null || true)"
        if [[ -z "$(ai_tools_providers__read_agent_manifest_field "${agent}" release_manifest_url 2>/dev/null || true)" \
              && ! -e "${pin}" ]]; then
            st "n/a" "${agent}  its package declares no signed release manifest, and root has recorded no pin yet"
            continue
        fi
        version="$(ai_tools_services__read_stamp_field "${pin}" VERSION)"
        # The reading this vantage adds: root can traverse the toolchain, so it hashes the entrypoint and compares it
        # against the pin instead of reporting what the last reconciliation recorded. A binary that changed since is
        # therefore named here even where no reconciliation has run over it yet -- the pin's own fields cannot say
        # so, and the operator's report can only read them.
        live="$(entrypoint_live_verdict "${agent}")"
        if [[ "${live}" == mismatch ]]; then
            st MISMATCH "${agent}  the installed entrypoint does not match its pin${version:+ (${version})} -- its sessions refuse to start"
            detail "atr admin system entrypoints relabel   (re-reads the entrypoint and prints how to replace it)"
            STATUS_PROBLEMS=$(( STATUS_PROBLEMS + 1 ))
        elif entrypoint_stale_mark "${agent}"; then
            STATUS_PROBLEMS=$(( STATUS_PROBLEMS + 1 ))
        elif [[ -n "${version}" && "$(ai_tools_entrypoint_verify__read_pin_kind "${agent}" 2>/dev/null || true)" == observed ]]; then
            # Recorded as installed: a change to the binary refuses at every setting, and no vendor signature stands
            # behind the value. UNCHANGED is what the comparison proves; it does not say the binary was sound when root
            # first recorded it. It is a pin, so it satisfies the strictness switch and is not a problem.
            age="$(ai_tools_services__format_age "$(ai_tools_services__read_stamp_age "${pin}" VERIFIED)")"
            st UNCHANGED "${agent}  ${version}${age:+, ${age}}, as installed -- its vendor publishes no signed manifest"
        elif [[ -n "${version}" ]]; then
            age="$(ai_tools_services__format_age "$(ai_tools_services__read_stamp_age "${pin}" VERIFIED)")"
            st VERIFIED "${agent}  ${version}${age:+, ${age}}"
        elif [[ -e "${pin}" ]]; then
            st unverified "${agent}  its pin is present and carries no version this reader accepts"
            detail "atr admin system entrypoints relabel   (rewrites the pin)"
        elif [[ "${strict}" == yes ]]; then
            st UNVERIFIED "${agent}  this host requires verification, so its sessions will not launch"
            detail "atr admin system entrypoints relabel   (fetches the vendor's signed manifest)"
            STATUS_PROBLEMS=$(( STATUS_PROBLEMS + 1 ))
        else
            st unverified "${agent}  no pin -- launches are not blocked"
        fi
    done < <(ai_tools_providers__list_enabled_agents 2>/dev/null)
    [[ "${seen}" -eq 1 ]] || st "n/a" "no agent is enabled in ${OPERATOR_CONF}"
    status_labels
}

# status_agent_versions: one Version line per enabled agent, the version its installed package declares, read as data --
# ai_tools_entrypoint_verify__read_installed_version over the package.json around the entrypoint the stable launcher
# resolves to. Root traverses the toolchain the operator cannot, and reads it rather than running the agent's
# `--version`, which would execute a file the sandbox account can write (updater.rule.md, "Root runs none of the
# toolchain"). An entrypoint outside its package reads as unknown, with the path, which is itself the finding. A missing
# enabled-agent resolver does not print a line, since Provisioning and Entrypoints report it; a missing relabel resolver
# or installed-version reader, each shipped with base, prints one line under MSG-M9D4 and counts in STATUS_UNREADABLE.
status_agent_versions() {
    declare -F ai_tools_providers__list_enabled_agents                >/dev/null 2>&1 || return 0
    if ! declare -F ai_tools_relabel__resolve_agent_entrypoint_path >/dev/null 2>&1 \
            || ! declare -F ai_tools_entrypoint_verify__read_installed_version >/dev/null 2>&1; then
        warn MSG-M9D4 "the relabel or entrypoint library (${RELABEL_LIB}, ${ENTRYPOINT_VERIFY_LIB}) did not load its readers -- no agent version was read; reinstall ai-tools-base"
        STATUS_UNREADABLE=$(( STATUS_UNREADABLE + 1 ))
        return 0
    fi
    local agent entrypoint version
    while IFS=$'\t' read -r agent _ _; do
        [[ -n "${agent}" ]] || continue
        entrypoint="$(ai_tools_relabel__resolve_agent_entrypoint_path "${agent}" 2>/dev/null || true)"
        if [[ -z "${entrypoint}" ]]; then
            printf '    %-13s %s\n' "${agent}" "not provisioned (no launcher link resolves)"
            continue
        fi
        version="$(ai_tools_entrypoint_verify__read_installed_version "${entrypoint}")"
        printf '    %-13s %s\n' "${agent}" \
            "${version:-unknown (the launcher resolves to ${entrypoint}, outside its package)}"
    done < <(ai_tools_providers__list_enabled_agents 2>/dev/null)
    return 0
}

# entrypoint_live_verdict <agent>: print what the agent's installed entrypoint hashes to against its pin RIGHT NOW --
# `ok`, `mismatch`, `unpinned`, `unreadable` -- or an empty string where the reading cannot be made. The third root-only
# reading of this report, beside the live SELinux type: the toolchain is 0750 and sandbox-owned, so no operator-side
# command can hash that file, and a pin records only what the last reconciliation found. Read-only, and it does not
# reach the network: it is the comparison the launch shim makes on every launch. status_entrypoints checks both readers
# it calls before the first agent.
entrypoint_live_verdict() {
    local agent="$1" entrypoint
    entrypoint="$(ai_tools_relabel__resolve_agent_entrypoint_path "${agent}" 2>/dev/null || true)"
    [[ -n "${entrypoint}" ]] || return 0
    ai_tools_entrypoint_verify__check "${agent}" "${entrypoint}" 2>/dev/null || true
}

# entrypoint_stale_mark <agent>: report, and return 0, when the last reconciliation REFUSED to re-record this agent's
# pin. The refusal leaves the pin standing -- that staleness is what makes the next launch refuse -- so without this
# the pin's own fields render as a verification that succeeded. Returns non-zero when there is no mark, which is
# the ordinary state. status_entrypoints checks the stale-path reader before the first agent.
entrypoint_stale_mark() {
    local agent="$1" record reason version age
    record="$(ai_tools_entrypoint_verify__get_stale_path "${agent}" 2>/dev/null || true)"
    [[ -n "${record}" && -e "${record}" ]] || return 1
    [[ "$(ai_tools_services__read_stamp_field "${record}" STATE)" == stale ]] || return 1
    reason="$(ai_tools_services__read_stamp_field "${record}" REASON)"
    version="$(ai_tools_services__read_stamp_field "${record}" VERSION)"
    age="$(ai_tools_services__format_age "$(ai_tools_services__read_stamp_age "${record}" DETECTED)")"
    st "PIN STALE" "${agent}  a reconciliation refused to re-record this pin (${reason:-refused}${version:+, ${version}})${age:+, ${age}} -- its sessions refuse to start"
    detail "atr admin system entrypoints relabel   (re-reads the entrypoint and prints how to replace it)"
    return 0
}

# status_labels: render ai_tools_relabel__list_agent_labels -- the live SELinux type of each enabled agent's own paths,
# which that function's header specifies. It closes status_entrypoints rather than opening a section of its own, so each
# agent's pin and its labels read together.
status_labels() {
    # Base ships this library beside this tool, so a missing report means a broken or half-upgraded install rather than
    # an optional piece -- a reading that could not be made, counted as such, since that is what it is from the reader's
    # side.
    if ! declare -F ai_tools_relabel__list_agent_labels >/dev/null 2>&1; then
        st UNREADABLE "live labels: ${RELABEL_LIB} did not load its report -- reinstall ai-tools-base"
        STATUS_UNREADABLE=$(( STATUS_UNREADABLE + 1 ))
        return 0
    fi
    local report verdict agent what path actual wanted rc=0
    report="$(ai_tools_relabel__list_agent_labels)" || rc=$?
    if [[ "${rc}" -eq 2 ]]; then
        st "n/a" "SELinux confinement is inactive here -- there is no agent label to carry"
        return 0
    fi
    if [[ "${rc}" -eq 3 ]]; then
        st UNREADABLE "live labels: a reader the label report calls did not load -- no agent label was read; reinstall ai-tools-base"
        STATUS_UNREADABLE=$(( STATUS_UNREADABLE + 1 ))
        return 0
    fi
    [[ -n "${report}" ]] || return 0
    while read -r verdict agent what path actual wanted; do
        case "${verdict}" in
            ok)   st labelled "${agent}  its ${what} is ${actual} -- ${path}" ;;
            bad)  st "NOT LABELLED" "${agent}  its ${what} is '${actual}', NOT ${wanted} -- ${path}"
                  detail "its next session refuses to launch rather than run unconfined"
                  detail "atr admin system entrypoints relabel"
                  STATUS_PROBLEMS=$(( STATUS_PROBLEMS + 1 )) ;;
            none) st "n/a" "${agent}  its ${what} is not installed -- nothing to label" ;;
        esac
    done <<< "${report}"
    return 0
}

# status_unit_search_path: the sandbox account's systemd unit search path -- the chain's drift
# (ai_tools_control_plane__find_unit_search_path_drift), any unexpected entry on it
# (ai_tools_control_plane__find_unexpected_unit_search_path_entries), and the `Persistent=` timer stamp. A root-vantage
# reading: the chain has no world bits. It reads and does not write. <home> is a parameter so a unit test drives it
# over a fixture chain.
status_unit_search_path() {
    local home="${1:-${CP_HOME:-/opt/ai-tools}}"
    heading "Sandbox unit search path"
    if ! declare -F ai_tools_control_plane__find_unit_search_path_drift >/dev/null 2>&1 \
            || ! declare -F ai_tools_control_plane__find_unexpected_unit_search_path_entries >/dev/null 2>&1; then
        st UNREADABLE "the control-plane library did not load its unit search path readers -- reinstall ai-tools-base"
        STATUS_UNREADABLE=$(( STATUS_UNREADABLE + 1 ))
        return 0
    fi
    local line path got wanted drift_out drift_rc=0 open=0
    drift_out="$(ai_tools_control_plane__find_unit_search_path_drift "${home}" "${SANDBOX_GROUP}")" || drift_rc=$?
    case "${drift_rc}" in
        0)  while IFS= read -r line; do
                [[ -n "${line}" ]] || continue
                path="${line%% *}"; got="${line#* }"; wanted="${got#* root:}"; got="${got%% root:*}"
                # An absent directory is a host the provisioning run has not reached, which Provisioning reports; one
                # that exists at another owner or mode is the state this layout removes, so it counts.
                if [[ "${got}" == absent ]]; then
                    st "n/a" "${path}  not created yet -- the provisioning run makes it root:${wanted}"
                    continue
                fi
                open=1
                st DRIFTED "${path}  is ${got}, not root:${wanted}"
                STATUS_PROBLEMS=$(( STATUS_PROBLEMS + 1 ))
            done <<< "${drift_out}"
            if (( open )); then
                detail "${SANDBOX_USER}'s --user manager runs unconfined, so a unit left on this path would run with none of the properties a session unit sets"
                detail "atr admin system layout reconcile"
            fi ;;
        1)  st OK "root owns every directory down to ${home}/${CP_UNIT_SEARCH_PATH_CHAIN[-1]}" ;;
        *)  st UNREADABLE "the unit search path under ${home} could not be read -- reinstall ai-tools-base"
            STATUS_UNREADABLE=$(( STATUS_UNREADABLE + 1 )) ;;
    esac
    while IFS= read -r line; do
        [[ -n "${line}" ]] || continue
        st UNEXPECTED "${line}  is on the unit search path and nothing ai-tools ships writes it -- inspect it, then remove it"
        STATUS_PROBLEMS=$(( STATUS_PROBLEMS + 1 ))
    done < <(ai_tools_control_plane__find_unexpected_unit_search_path_entries "${home}")
    status_update_timer_stamp "${home}"
    return 0
}

# status_caches [home] [state-file]: per cache group, what its last cleanup did and when, read from the state file
# through cache-cleanup.lib.sh, whose verdicts and remedies atr status renders too, and beside it the size the cache
# holds now, which only root reads in the 0750 tree (`du -sxP`, as data, bounded to ten seconds). A group that failed,
# or that holds links its producer does not write, is counted in STATUS_PROBLEMS; an overdue one is shown and not
# counted, and an empty file reads `?` and is not counted. <home> and <state-file> are parameters so a unit test drives
# the section over fixtures; the report passes AI_TOOLS_CACHE_CLEANUP_STATE, a root-only override.
status_caches() {
    local home="${1:-${CP_HOME:-/opt/ai-tools}}"
    local state="${2:-${AI_TOOLS_CACHE_CLEANUP_STATE:-/var/opt/ai-tools/state/cache-cleanup.status}}"
    local name relative verdict result bytes links reason checked_age cleaned_age token text size size_text
    heading "Caches"
    # shellcheck source=SCRIPTDIR/../../lib/ai-tools/cache-cleanup.lib.sh
    source "${CACHE_CLEANUP_LIB}" 2>/dev/null || true
    if ! declare -F ai_tools_cache_cleanup__list_group_statuses >/dev/null 2>&1; then
        st UNREADABLE "the cache cleanup library (${CACHE_CLEANUP_LIB}) did not load -- reinstall ai-tools-base"
        STATUS_UNREADABLE=$(( STATUS_UNREADABLE + 1 ))
        return 0
    fi
    if ai_tools_services__is_stamp_unwritten "${state}"; then
        st "?" "no cleanup recorded yet -- the updater's first run and the first session launch write ${state}"
        return 0
    fi
    while IFS=$'\t' read -r name relative verdict result bytes links reason checked_age cleaned_age; do
        [[ -n "${name}" ]] || continue
        [[ "${checked_age}" == - ]] && checked_age=""
        [[ "${cleaned_age}" == - ]] && cleaned_age=""
        text="${relative}  "
        [[ "${verdict}" == links ]] && text+="${links} symbolic link(s) its producer does not write, "
        text+="$(ai_tools_cache_cleanup__format_result "${result}" "${bytes}" "${reason}")"
        [[ -n "${checked_age}" ]] && text+=", $(ai_tools_services__format_age "${checked_age}")"
        if [[ "${result}" != removed && "${result}" != absent && -n "${cleaned_age}" ]]; then
            text+=" -- last cleaned $(ai_tools_services__format_age "${cleaned_age}")"
        fi
        size="$(timeout 10 du -sxP --bytes -- "${home}/${relative}" 2>/dev/null | cut -f1 || true)"
        size_text=""
        if [[ "${size}" =~ ^[0-9]+$ ]]; then
            size_text="; holds $(ai_tools_cache_cleanup__format_bytes "${size}") now"
        elif [[ ! -e "${home}/${relative}" ]]; then
            size_text="; absent now"
        fi
        text+="${size_text}"
        case "${verdict}" in
            unknown) st "?" "${relative}  no cleanup recorded yet${size_text}"; continue ;;
            failed)  token=FAILED ;;
            links)   token=LINKS ;;
            overdue) token=OVERDUE ;;
            *)       case "${result}" in
                         removed|absent) token=OK ;;
                         # The token is the result here, so the text keeps its reason alone.
                         *)              token="${result^^}"; text="${text/"  ${result} ("/"  ("}" ;;
                     esac ;;
        esac
        st "${token}" "${text}"
        case "${verdict}" in
            failed|links) detail "$(ai_tools_cache_cleanup__get_status_remedy "${verdict}" "${reason}")"
                          STATUS_PROBLEMS=$(( STATUS_PROBLEMS + 1 )) ;;
            overdue)      detail "$(ai_tools_cache_cleanup__get_status_remedy "${verdict}" "${reason}")" ;;
        esac
    done < <(ai_tools_cache_cleanup__list_group_statuses "${state}")
    return 0
}

# status_update_timer_stamp: the `Persistent=` stamp nvm-update.timer keeps (ai_tools_services__evaluate_timer_stamp).
# Whether the timer is overdue is the Services section's reading.
status_update_timer_stamp() {
    local home="${1:-${CP_HOME:-/opt/ai-tools}}"
    if ! declare -F ai_tools_services__evaluate_timer_stamp >/dev/null 2>&1 \
            || ! declare -F ai_tools_services__parse_timespan_seconds >/dev/null 2>&1; then
        st UNREADABLE "the service library did not load its timer-stamp readers -- reinstall ai-tools-base"
        STATUS_UNREADABLE=$(( STATUS_UNREADABLE + 1 ))
        return 0
    fi
    local stamp="${home}/${CP_TIMER_STAMP_DIR}/${CP_UPDATE_TIMER_STAMP}"
    local state=ok mtime now skew="" accuracy delay allowance verdict
    if [[ -L "${stamp}" || ( -e "${stamp}" && ! -f "${stamp}" ) ]]; then
        state=unreadable
    elif [[ ! -e "${stamp}" ]]; then
        state=absent
    elif ! mtime="$(stat -c %Y -- "${stamp}" 2>/dev/null)" || [[ ! "${mtime}" =~ ^[0-9]+$ ]]; then
        state=unreadable
    else
        now="$(date -u +%s 2>/dev/null || true)"
        if [[ "${now}" =~ ^[0-9]+$ ]]; then
            skew=0
            [[ "${mtime}" -gt "${now}" ]] && skew=$(( mtime - now ))
        fi
    fi
    # An unreadable property leaves its share of the allowance at 0, which only makes the future test stricter.
    accuracy="$(ai_tools_services__parse_timespan_seconds "$(ai_tools_services__read_unit_property nvm-update.timer AccuracyUSec sandbox-user)")"
    delay="$(ai_tools_services__parse_timespan_seconds "$(ai_tools_services__read_unit_property nvm-update.timer RandomizedDelayUSec sandbox-user)")"
    allowance=$(( ${accuracy:-0} + ${delay:-0} ))
    verdict="$(ai_tools_services__evaluate_timer_stamp "${state}" "${skew}" "${allowance}")"
    case "${verdict}" in
        ok)     st OK "the update timer's Persistent= stamp" ;;
        absent) st "n/a" "no Persistent= stamp yet -- the next start of that manager runs a catch-up update" ;;
        future) st FUTURE "its Persistent= stamp is dated $(( ${skew:-0} / 60 )) min ahead of now, past the timer's own ${allowance}s tolerance"
                detail "systemd reads that stamp at timer start, so a missed window gets no catch-up run while it stands"
                detail "check this host's clock, then: sudo systemctl --user -M ${SANDBOX_USER}@.host restart nvm-update.timer"
                STATUS_PROBLEMS=$(( STATUS_PROBLEMS + 1 )) ;;
        *)      st "?" "its Persistent= stamp could not be read at ${stamp}" ;;
    esac
    return 0
}

# status_selinux_attestation: render the rows ai_tools_confinement__list_attestation_report prints (confinement.lib.sh):
# the per-domain mode of ai_tools_t and the Booleans that widen it, read and classified by the launch shim's own
# functions, so this report, atr status and the launch cannot disagree. A finding counts only where the report's verdict
# row says AI_TOOLS_REQUIRE_SELINUX is set, since that is when it refuses a launch, and on a host without SELinux
# confinement by its own configuration (ai_tools_confinement__evaluate_dac_only_state) only where that key makes every
# launch warn; elsewhere it is reported as the posture it is. <operator-conf> is a parameter so a unit test drives
# the counting rule over a fixture.
status_selinux_attestation() {
    local operator_conf="${1:-${OPERATOR_CONF}}"
    heading "SELinux status"
    # shellcheck source=SCRIPTDIR/../../lib/ai-tools/confinement.lib.sh
    source "${CONFINEMENT_LIB}" 2>/dev/null || true
    if ! declare -F ai_tools_confinement__list_attestation_report >/dev/null 2>&1 \
            || ! declare -F ai_tools_confinement__evaluate_dac_only_state >/dev/null 2>&1; then
        st UNREADABLE "${CONFINEMENT_LIB} did not load its attestation readers -- reinstall ai-tools-base"
        STATUS_UNREADABLE=$(( STATUS_UNREADABLE + 1 ))
        return 0
    fi
    local selinux_mode module_present policy_shipped dac_only_state=""
    selinux_mode="$(getenforce 2>/dev/null || true)"
    if [[ -z "${selinux_mode}" ]]; then
        st "n/a" "SELinux is not readable here (getenforce) -- there is no domain to attest"
        return 0
    fi
    module_present="$(ai_tools_confinement__read_module_present "${AI_TOOLS_CONFINEMENT__MODULE_PROBE_PATH}" 2>/dev/null || true)"
    policy_shipped="$(ai_tools_confinement__read_policy_shipped "${AI_TOOLS_CONFINEMENT__CORE_MODULE_FILE}")"
    dac_only_state="$(ai_tools_confinement__evaluate_dac_only_state "${selinux_mode}" "${module_present}" "${policy_shipped}")" \
        || true
    if [[ -n "${dac_only_state}" ]]; then
        case "${dac_only_state}" in
            disabled) st "n/a" "SELinux is disabled on this host -- there is no domain to attest" ;;
            *)        st "n/a" "the ai_tools policy is not installed (SELinux ${selinux_mode}) -- there is no domain to attest" ;;
        esac
        if ai_tools_confinement__is_selinux_required "${operator_conf}"; then
            detail "AI_TOOLS_REQUIRE_SELINUX is set, so every launch runs DAC-only and warns that the requirement is not met"
            detail "declare this host DAC-only:  set AI_TOOLS_REQUIRE_SELINUX=no in ${operator_conf}"
            detail "or install the policy:       sudo dnf install ai-tools-selinux"
            STATUS_PROBLEMS=$(( STATUS_PROBLEMS + 1 ))
        else
            detail "AI_TOOLS_REQUIRE_SELINUX is not set, so a launch runs DAC-only without a warning"
        fi
        return 0
    fi
    local -a row
    local origin_note effect_note enforcement_note="not enforced"
    ai_tools_confinement__is_selinux_required "${operator_conf}" && enforcement_note="launch blocked"
    while IFS=$'\t' read -r -a row; do
        case "${row[0]}" in
            domain)   # <yes|no|unread> <remedy|->
                case "${row[1]}" in
                    no)  st enforcing "ai_tools_t is enforced as a domain" ;;
                    yes) st PERMISSIVE "ai_tools_t is a permissive domain -- its denials are logged and not enforced"
                         detail "${row[2]}" ;;
                    *)   st "?" "whether ai_tools_t is a permissive domain could not be read" ;;
                esac ;;
            boolean)  # <name> <classification> <state> <required> <origin> <opening> <grants> <remedy|->
                origin_note=""
                [[ "${row[5]}" == operator.conf ]] && origin_note=", operator.conf"
                [[ "${row[5]}" == built-in ]] && origin_note=", built in"
                effect_note=""
                [[ "${row[6]}" == on || "${row[6]}" == off ]] && effect_note="when ${row[6]}, allows ${row[7]}"
                case "${row[2]}" in
                    matches) st "${row[3]}" "${row[1]}  required: ${row[4]}${origin_note}" ;;
                    differs) st "${row[3]^^}" "${row[1]}  required: ${row[4]}${origin_note}, ${enforcement_note}${effect_note:+ -- ${effect_note}}"
                             detail "${row[8]}" ;;
                    open)    st "${row[3]}" "${row[1]}  ${effect_note}" ;;
                    closed)  st "${row[3]}" "${row[1]}${effect_note:+  ${effect_note/, allows/, would allow}}" ;;
                    malformed) st MALFORMED "${row[1]} in ${operator_conf} has ${row[7]} -- every launch refuses until it is fixed" ;;
                    *)       st "?" "${row[1]}  could not be read" ;;
                esac ;;
            verdict)  # <ok|permissive|boolean|unknown> <required: yes|no>
                [[ "${row[1]}" == ok ]] && continue
                if [[ "${row[2]}" == yes ]]; then
                    detail "AI_TOOLS_REQUIRE_SELINUX is set, so every launch refuses while this stands"
                    STATUS_PROBLEMS=$(( STATUS_PROBLEMS + 1 ))
                else
                    detail "AI_TOOLS_REQUIRE_SELINUX is no, so launches are not refused for this"
                fi ;;
        esac
    done < <(ai_tools_confinement__list_attestation_report "${operator_conf}")
    return 0
}

# status_assets: the Assets section, read from the plan alone (assets.lib.sh), without the lock and without a write: one
# [ATTENTION] line per enabled asset that is not linked, per untrusted or foreign entry the plan met, and per agent
# whose own entry stands at an enabled asset's name, each counted in STATUS_PROBLEMS; one [UNREADABLE] line per input
# the plan could not read, counted in STATUS_UNREADABLE; the withdrawn-successor notice (status_assets_successors);
# an OK line per linked asset that declares a capability; and an OK summary of how many entries are enabled and how many
# the plan resolved to linked, naming `assets list` as the reading that names the link changes a reconcile would make,
# which this section does not print, so a missing link reads here as resolved. The entry and row lines come in the order
# the record stream takes (ai_tools_assets__sort_report_rows). A library that did not load is a reading this section
# could not make, and so is an order that could not be computed, whose lines then come in the order the plan recorded
# them (ai_tools_assets__get_report_row_references). The plan and the order run with stderr sent to /dev/null, so their
# warnings do not print between the section's lines: an order that failed is the section's own [UNREADABLE] line,
# and an order the library fell back from changes the lines' order alone. Every string from a set or an agent's
# directory passes the sanitizer. The section closes with status_assets_summary over the counts it added.
status_assets() {
    local problems_before="${STATUS_PROBLEMS}" unreadable_before="${STATUS_UNREADABLE}"
    heading "Assets"
    status_assets_rows
    status_assets_summary $(( STATUS_PROBLEMS - problems_before )) $(( STATUS_UNREADABLE - unreadable_before ))
    return 0
}

# status_assets_rows: the Assets section's lines, as status_assets states them.
status_assets_rows() {
    # shellcheck source=SCRIPTDIR/../../lib/ai-tools/assets.lib.sh
    source "${ASSETS_LIB}" 2>/dev/null || true
    if ! declare -F ai_tools_assets__plan >/dev/null 2>&1 || ! declare -F ai_tools_assets__sort_report_rows >/dev/null 2>&1 \
            || ! declare -F ai_tools_assets__get_report_row_references >/dev/null 2>&1; then
        st UNREADABLE "the assets library (${ASSETS_LIB}) did not load -- reinstall ai-tools-base"
        STATUS_UNREADABLE=$(( STATUS_UNREADABLE + 1 ))
        return 0
    fi
    ai_tools_assets__plan 2>/dev/null
    local entry reference linked=0
    local -a references=()
    case "${AI_TOOLS_ASSETS__LIST_STATE}" in
        untrusted) st ATTENTION "enable-list-untrusted  ${AI_TOOLS_OPERATOR_CONF} -- $(ai_tools_log__sanitize "${AI_TOOLS_ASSETS__LIST_DETAIL}")"
                   detail "no asset is linked while it stands; restore the file to root:root 0644"
                   STATUS_PROBLEMS=$(( STATUS_PROBLEMS + 1 )) ;;
        invalid)   st ATTENTION "id-malformed  AI_TOOLS_ASSETS -- $(ai_tools_log__sanitize "${AI_TOOLS_ASSETS__LIST_DETAIL}")"
                   STATUS_PROBLEMS=$(( STATUS_PROBLEMS + 1 )) ;;
        unread)    st UNREADABLE "error  AI_TOOLS_ASSETS -- $(ai_tools_log__sanitize "${AI_TOOLS_ASSETS__LIST_DETAIL}")"
                   STATUS_UNREADABLE=$(( STATUS_UNREADABLE + 1 )) ;;
        unset)     status_assets_successors ;;
    esac
    local order_status=0
    ai_tools_assets__sort_report_rows references 2>/dev/null || order_status=$?
    if (( order_status != 0 )); then
        st UNREADABLE "error  the order of the Assets lines could not be computed (status ${order_status}), so they are printed in the order the plan recorded them"
        STATUS_UNREADABLE=$(( STATUS_UNREADABLE + 1 ))
        ai_tools_assets__get_report_row_references references
    fi
    for reference in "${references[@]}"; do
        case "${reference%%$'\t'*}" in
            entry) status_assets_report_entry "${AI_TOOLS_ASSETS__ENTRIES[${reference#*$'\t'}]}" ;;
            row)   status_assets_report_row "${reference#*$'\t'}" ;;
        esac
    done
    for entry in "${AI_TOOLS_ASSETS__ENTRIES[@]}"; do
        [[ "${AI_TOOLS_ASSETS__STATE[${entry}]}" == linked ]] && linked=$(( linked + 1 ))
    done
    st OK "${#AI_TOOLS_ASSETS__ENTRIES[@]} asset(s) enabled in AI_TOOLS_ASSETS, ${linked} resolved to linked; atr admin assets list names the link changes a reconcile would make"
    return 0
}

# status_assets_report_entry <entry>: the line for one enable-list entry: an OK line for a linked asset that declares
# a capability, none for another linked one, and an [ATTENTION] or [UNREADABLE] line with its reason otherwise.
status_assets_report_entry() {
    local entry="$1" reason="${AI_TOOLS_ASSETS__DETAIL[$1]:-}"
    if [[ "${AI_TOOLS_ASSETS__STATE[${entry}]}" == linked ]]; then
        [[ -z "${AI_TOOLS_ASSETS__CAPS[${entry}]:-}" ]] \
            || st OK "$(ai_tools_log__sanitize "${entry}")  linked, requires ${AI_TOOLS_ASSETS__CAPS[${entry}]}"
        return 0
    fi
    if [[ "${AI_TOOLS_ASSETS__STATE[${entry}]}" == error ]]; then
        st UNREADABLE "$(ai_tools_log__sanitize "${entry}")  ${AI_TOOLS_ASSETS__STATE[${entry}]}"
        STATUS_UNREADABLE=$(( STATUS_UNREADABLE + 1 ))
    else
        st ATTENTION "$(ai_tools_log__sanitize "${entry}")  ${AI_TOOLS_ASSETS__STATE[${entry}]}"
        STATUS_PROBLEMS=$(( STATUS_PROBLEMS + 1 ))
    fi
    detail "$(ai_tools_log__sanitize "${reason:0:200}")"
}

# status_assets_report_row <index>: the line for one row the plan recorded: [ATTENTION] or [UNREADABLE] with its detail,
# none for an info row.
status_assets_report_row() {
    local index="$1" reason="${AI_TOOLS_ASSETS__ROW_DETAIL[$1]}"
    case "${AI_TOOLS_ASSETS__ROW_SEVERITY[index]}" in
        attention)  st ATTENTION "${AI_TOOLS_ASSETS__ROW_FINDING[index]}  $(ai_tools_log__sanitize "${AI_TOOLS_ASSETS__ROW_SUBJECT[index]}")"
                    STATUS_PROBLEMS=$(( STATUS_PROBLEMS + 1 )) ;;
        unreadable) st UNREADABLE "${AI_TOOLS_ASSETS__ROW_FINDING[index]}  $(ai_tools_log__sanitize "${AI_TOOLS_ASSETS__ROW_SUBJECT[index]}")"
                    STATUS_UNREADABLE=$(( STATUS_UNREADABLE + 1 )) ;;
        *)          return 0 ;;
    esac
    detail "$(ai_tools_log__sanitize "${reason:0:200}")"
}

# status_assets_successors: the notice for a host whose upgrade withdrew assets a set succeeds while AI_TOOLS_ASSETS is
# absent from operator.conf, the one input that records the operator's decision. One [ATTENTION] successor-unenabled
# line per successor set, naming the withdrawn entries (ai_tools_managed_assets__list_withdrawn_successors)
# and the commands that end it: the set's package install where the set is not on the host
# (ai_tools_assets__is_set_installed), its enable, and AI_TOOLS_ASSETS=[] to keep it off. A retired directory that could
# not be listed, and a seeder library that did not load, are each one [UNREADABLE] line. A host without a retired copy
# of a withdrawn entry with a successor, a fresh install among them, does not print a line. Runs as root without
# the lock.
status_assets_successors() {
    local listing entry successor successor_set install
    local -a sets=()
    local -A names=()
    if ! declare -F ai_tools_managed_assets__list_withdrawn_successors >/dev/null 2>&1; then
        st UNREADABLE "error  the seeder library (/usr/local/lib/ai-tools/managed-assets.lib.sh) did not load -- reinstall ai-tools-base; a withdrawn asset's successor is not checked"
        STATUS_UNREADABLE=$(( STATUS_UNREADABLE + 1 ))
        return 0
    fi
    if ! listing="$(ai_tools_managed_assets__list_withdrawn_successors "${AI_TOOLS_ASSETS_HOME}" 2>/dev/null)"; then
        st UNREADABLE "error  ${AI_TOOLS_ASSETS_HOME}/retired -- the directory could not be listed, so a withdrawn asset's successor is not checked"
        STATUS_UNREADABLE=$(( STATUS_UNREADABLE + 1 ))
        return 0
    fi
    while IFS=$'\t' read -r entry successor; do
        [[ -n "${entry}" ]] || continue
        successor_set="${successor%%/*}"
        [[ -n "${names[${successor_set}]+x}" ]] || sets+=( "${successor_set}" )
        names["${successor_set}"]+="${names[${successor_set}]:+, }${entry#*/}"
    done <<< "${listing}"
    for successor_set in "${sets[@]}"; do
        install=""
        ai_tools_assets__is_set_installed "${successor_set}" || install="sudo dnf install ai-tools-assets-sets-${successor_set} && "
        st ATTENTION "successor-unenabled  ${successor_set} -- ${names[${successor_set}]} withdrawn from ai-tools-base; their successors in the set ${successor_set} are not enabled and AI_TOOLS_ASSETS is not set; copies are under ${AI_TOOLS_ASSETS_HOME}/retired"
        detail "run: ${install}atr admin assets enable --set ${successor_set}, or set AI_TOOLS_ASSETS=[] in ${AI_TOOLS_OPERATOR_CONF} to keep them off"
        STATUS_PROBLEMS=$(( STATUS_PROBLEMS + 1 ))
    done
}

# status_assets_summary <finding-count> <unreadable-count> : the Assets section's closing line, printed when either
# count is one or more: the non-zero counts and the exit status the section sets, 5 where a reading is unavailable and 4
# otherwise. The counts are the ones the section added to STATUS_PROBLEMS and STATUS_UNREADABLE, so the line
# and the exit agree. Prose for a person, not a record: a monitor reads the exit status.
status_assets_summary() {
    local finding_count="$1" unreadable_count="$2" findings="" readings=""
    (( finding_count > 0 || unreadable_count > 0 )) || return 0
    if (( finding_count == 1 )); then findings="1 finding needs attention"
    elif (( finding_count > 1 )); then findings="${finding_count} findings need attention"
    fi
    if (( unreadable_count == 1 )); then readings="1 reading is unavailable"
    elif (( unreadable_count > 1 )); then readings="${unreadable_count} readings are unavailable"
    fi
    printf '    Summary: %s%s%s in this section (exit %s)\n' "${findings}" "${findings:+${readings:+ and }}" "${readings}" \
        "$( (( unreadable_count > 0 )) && printf 5 || printf 4 )"
}

# status_node_version: the Version section's Node line, from the same verdict the CLI renders
# (ai_tools_toolchain__evaluate_node_versions, toolchain.lib.sh): the active version read off the enabled agents' stable
# launcher links, and the version the updater's last run recorded shown beside it only where the two differ. Root could
# read the toolchain itself; the link is read instead so the two reports have one source and one answer. A host
# with neither a link nor a stamp gets no Node line, and Provisioning says why. toolchain.lib.sh ships with base, so its
# link readers missing is a reading this section could not make: reported under MSG-B5R6 and counted
# in STATUS_UNREADABLE, with the stamp's version printed as the last update run's, since no link was read.
status_node_version() {
    local rec stamp_node="" verdict kind version stamp_seen
    if declare -F ai_tools_services__read_stamp_field >/dev/null 2>&1; then
        while IFS= read -r rec; do
            stamp_node="$(ai_tools_services__read_stamp_field "$(ai_tools_services__get_field "${rec}" 7)" NODE)"
            [[ -n "${stamp_node}" && "${stamp_node}" != unknown ]] && break
            stamp_node=""
        done < <(ai_tools_services__list_records)
    fi
    # shellcheck source=SCRIPTDIR/../../lib/ai-tools/toolchain.lib.sh
    source "${TOOLCHAIN_LIB}" 2>/dev/null || true
    if declare -F ai_tools_toolchain__list_agent_link_node_versions >/dev/null 2>&1 \
            && declare -F ai_tools_toolchain__evaluate_node_versions >/dev/null 2>&1; then
        verdict="$(ai_tools_toolchain__list_agent_link_node_versions "${LAUNCHER_LINK_DIR}" 2>/dev/null \
                       | ai_tools_toolchain__evaluate_node_versions "${stamp_node}")"
    else
        warn MSG-B5R6 "the toolchain library (${TOOLCHAIN_LIB}) did not load its link readers -- the active Node version was not read; reinstall ai-tools-base"
        STATUS_UNREADABLE=$(( STATUS_UNREADABLE + 1 ))
        [[ -n "${stamp_node}" ]] \
            && printf '    %-13s %s (the version the last update run recorded -- no launcher link was read)\n' "node" "${stamp_node}"
        return 0
    fi
    IFS=$'\t' read -r kind version stamp_seen <<<"${verdict}"
    case "${kind}" in
        active) printf '    %-13s %s%s\n' "node" "${version}" \
                    "${stamp_seen:+ (active; the last update run saw ${stamp_seen})}" ;;
        split)  printf '    %-13s %s (the enabled agents'"'"' launchers name different Node versions -- an update may be in progress)\n' \
                    "node" "${version}" ;;
        stamp)  printf '    %-13s %s (as of the last toolchain update -- no launcher link names one)\n' "node" "${version}" ;;
    esac
    return 0
}

# status: the host report. The exit is the report state's (ai-tools-records(5)): 4 when a section read a fault, 5
# when a section could not make a reading it promises, so it is usable from a monitor or a cron check without parsing
# this output -- the same contract `atr status` offers, and the reason `?` and `n/a` are never counted: a reading this
# vantage point cannot make must not make a healthy host alarm every night.
status() {
    [[ $# -eq 0 ]] || reject MSG-T6S6 "status: takes no arguments"
    STATUS_PROBLEMS=0
    STATUS_UNREADABLE=0
    ai_tools_records_base__begin_report
    # Ahead of the library load, so a report that cannot be given still says what was asked for: the refusal then reads
    # as this command failing rather than as an unattributed error.
    printf '\nai-tools host status\n'

    # Loaded here rather than beside the other libraries: each is read by one command, and relabel.lib.sh pulls
    # in the provider and control-plane libraries behind it. `operators add` loads that one the same way, inside
    # the command that needs it. Each is best-effort and its section reports what it could not read. The service
    # registry is the one whose absence is reported here, since the Services section cannot open without it: a reading
    # that could not be made, so the run exits 5 and the other sections still print what they read.
    # shellcheck source=SCRIPTDIR/../../lib/ai-tools/services.lib.sh
    source "${SERVICES_LIB}" 2>/dev/null || true
    # shellcheck source=SCRIPTDIR/../../lib/ai-tools/relabel.lib.sh
    source "${RELABEL_LIB}" 2>/dev/null || true
    # shellcheck source=SCRIPTDIR/../../lib/ai-tools/entrypoint-verify.lib.sh
    source "${ENTRYPOINT_VERIFY_LIB}" 2>/dev/null || true
    local services_readable=yes
    if ! declare -F ai_tools_services__list_records   >/dev/null 2>&1 \
            || ! declare -F ai_tools_services__read_record_state >/dev/null 2>&1 \
            || ! declare -F ai_tools_services__format_age  >/dev/null 2>&1; then
        warn MSG-V6N9 "the service registry (${SERVICES_LIB}) is unavailable -- reinstall ai-tools-base"
        STATUS_UNREADABLE=$(( STATUS_UNREADABLE + 1 ))
        services_readable=no
    fi
    # Root reaching the sandbox account's own manager is what this report adds over the operator's.
    declare -F ai_tools_services__set_sandbox_account >/dev/null 2>&1 && ai_tools_services__set_sandbox_account "${SANDBOX_USER}"

    heading "Version"
    printf '    %-13s %s\n' "ai-tools" "${AI_TOOLS_VERSION}"
    status_node_version
    status_agent_versions

    status_provisioning
    status_registries
    status_managed_files
    status_assets

    if [[ "${services_readable}" == yes ]]; then
        status_services
    else
        heading "Services"
        st UNREADABLE "the service registry did not load, so no unit could be read"
    fi
    status_caches "${CP_HOME:-/opt/ai-tools}" \
        "${AI_TOOLS_CACHE_CLEANUP_STATE:-/var/opt/ai-tools/state/cache-cleanup.status}"
    status_entrypoints
    status_unit_search_path "${CP_HOME:-/opt/ai-tools}"
    status_selinux_attestation "${OPERATOR_CONF}"

    # Pointers, not duplication: the reports that own the detail this one deliberately does not, then the commands
    # that read and drive the two managed units this report judges by their last run. The updater's unit lives
    # in the sandbox account's own manager, which root reaches over the machine transport and whose journal is selected
    # by unit and uid, since `journalctl --user-unit` as root reads root's own units (cli.rule.md).
    heading "More"
    printf '    %s\n' \
        "atr status          the same host, read from an operator's vantage" \
        "atr providers       installed agents and integrations, and which are enabled" \
        "atr admin selinux groups   the core module and the optional groups" \
        "atr admin --help           every command this host has"
    local sandbox_uid
    sandbox_uid="$(id -u "${SANDBOX_USER}" 2>/dev/null || printf '<uid of %s>' "${SANDBOX_USER}")"
    printf '\n    %s\n' "The relabel watcher and the toolchain updater, read and run as root:"
    printf '      %-78s %s\n' \
        "sudo systemctl status ai-tools-relabel.path ai-tools-relabel.service" "the watcher, and the result of its last run" \
        "sudo journalctl -u ai-tools-relabel.service -n 50" "what that run printed" \
        "sudo systemctl start ai-tools-relabel.service" "run the relabel now; a FAILED above clears with a run that succeeds" \
        "sudo systemctl --user -M ${SANDBOX_USER}@.host status nvm-update.timer nvm-update.service" "the updater's schedule and its last run" \
        "sudo journalctl _SYSTEMD_USER_UNIT=nvm-update.service _UID=${sandbox_uid} -n 50" "what the updater printed" \
        "sudo systemctl --user -M ${SANDBOX_USER}@.host start nvm-update.service" "run the update now" \
        "sudo systemctl --user -M ${SANDBOX_USER}@.host daemon-reload" "reload that manager after a unit file changed on disk"
    printf '\n'

    (( STATUS_PROBLEMS == 0 )) || ai_tools_records_base__accumulate_severity attention
    (( STATUS_UNREADABLE == 0 )) || ai_tools_records_base__accumulate_severity unreadable
    ai_tools_records_base__get_exit_status || return $?
    return 0
}

# ── assets ───────────────────────────────────────────────────────────────────────────────────────────────────────────
# The `assets` domain: AI_TOOLS_ASSETS in operator.conf names the assets every enabled agent loads, and assets.lib.sh
# resolves each to `linked` or a reason token and keeps the view and the agents' links current (shipped-assets.rule.md).
# The lock a writing verb holds (ai_tools_managed_assets__lock), the one record stream every verb prints and its exit
# are shipped-assets.rule.md's (ref-section-j4a7). Each attention row is logged under its code to journald
# and to the root-only assets.log.

# assets_load: source assets.lib.sh, or exit 5 under its code: a verb whose resolver did not load does not read or write
# any input.
assets_load() {
    # shellcheck source=SCRIPTDIR/../../lib/ai-tools/assets.lib.sh
    source "${ASSETS_LIB}" 2>/dev/null || true
    if ! declare -F ai_tools_assets__reconcile >/dev/null 2>&1 || ! declare -F ai_tools_assets__build_enable_snapshot >/dev/null 2>&1 \
            || ! declare -F ai_tools_assets__record_report_row >/dev/null 2>&1 || ! declare -F ai_tools_assets__report_state >/dev/null 2>&1; then
        warn MSG-P8M5 "the assets library (${ASSETS_LIB}) did not load, so no asset was read or changed -- reinstall ai-tools-base"
        exit 5
    fi
    AI_TOOLS_LOG_TAG=ai-tools-assets
    AI_TOOLS_LOG_FILE=assets.log
}

# _assets_attention_row <code> <situation> <finding> <subject> <identifier> <detail> and _assets_info_row (the same
# at info): record one row of a verb's own, subject-type `file`, its item the identifier, for the report the verb's
# reconcile writes (ai_tools_assets__record_report_row), which logs an attention row under its code. The situation is
# the code's own wording, for the index; the row carries the finding and the detail.
_assets_attention_row() {
    ai_tools_assets__record_report_row "$1" attention "$3" file "$4" "$5" "" "$6"
}
_assets_info_row() {
    ai_tools_assets__record_report_row "$1" info "$3" file "$4" "$5" "" "$6"
}

# assets_lock_or_exit: take the assets lock for the rest of the verb, or exit 1 with no input read and no link changed;
# ai_tools_managed_assets__lock has written the refusal under MSG-M8T9.
assets_lock_or_exit() {
    ai_tools_managed_assets__lock || exit 1
}

# assets_read_list <array-name>: the entries of AI_TOOLS_ASSETS in the file the verbs write, after the checks the write
# makes: the file, where it exists, passes ai_tools_conf__is_trusted -- a file the predicate refuses is not one this
# tool repairs by writing to it -- the file is read whole, and the key, where present, reads as a list, since an invalid
# one would be lost on the rewrite. An absent key reads as the empty list. Each refusal exits 1 before any write: a file
# not read whole would otherwise read as an empty list, and the write that follows would drop every entry the file
# holds.
assets_read_list() {
    local -n _assets_read_list_out="$1"
    local conf="${AI_TOOLS_OPERATOR_CONF}" status=0
    _assets_read_list_out=()
    [[ -e "${conf}" || -L "${conf}" ]] || return 0
    ai_tools_conf__is_trusted "${conf}" \
        || die MSG-K6K3 "assets: ${conf} is not root's alone ($(ai_tools_conf__read_untrusted_reason "${conf}")), so it is not written -- restore it to root:root 0644 and run the command again"
    ai_tools_conf__read_list _assets_read_list_out "${conf}" AI_TOOLS_ASSETS 2>/dev/null || status=$?
    case "${status}" in
        0) ;;
        1) return 0 ;;
        *) die MSG-C7N4 "assets: ${conf} could not be read whole (reader status ${status}), so AI_TOOLS_ASSETS is not written and the file is unchanged -- check that it is readable and run the command again" ;;
    esac
    [[ "${ai_tools_conf__list_invalid:-0}" == 0 ]] \
        || die MSG-B4Z5 "assets: AI_TOOLS_ASSETS in ${conf} is not a valid list, so it is not rewritten -- write it as [<set>/<kind>/<name>, ...] and run the command again"
}

# assets_write_list <entry>...: write AI_TOOLS_ASSETS as exactly these entries through ai_tools_conf__set_list,
# after a dated .bak of the file where it exists. Exits 1 after the rows the verb recorded
# (ai_tools_assets__write_recorded_rows), without the reconcile, when the backup fails, the file as it was,
# or when the writer does not confirm the write: ai_tools_conf__set_list returns 1 alike for a file it could not write
# and for one it replaced and could not read back, so the refusal names the backup and the reconcile that brings
# the links in line with what the file holds, and does not say the file is unchanged. A file carrying neither a live
# line nor the key's commented default -- one written before the key -- takes the line at its end, and the write says
# so, since the slot under its header reaches that file through `system post-upgrade`.
assets_write_list() {
    local conf="${AI_TOOLS_OPERATOR_CONF}" backup="" rc=0
    if [[ -e "${conf}" ]] && ! backup="$(ai_tools_conf__write_backup "${conf}")"; then
        ai_tools_assets__write_recorded_rows
        die MSG-V3M3 "assets: ${conf} could not be backed up, so AI_TOOLS_ASSETS was not written"
    fi
    ai_tools_conf__set_list "${conf}" AI_TOOLS_ASSETS "$@" || rc=$?
    if (( rc != 0 )); then
        ai_tools_assets__write_recorded_rows
        die MSG-Q8E5 "assets: the write of AI_TOOLS_ASSETS to ${conf} was not confirmed (writer status ${rc}), so the file holds the list it held or the one this call wrote${backup:+; the file as it was is ${backup}} -- read the line, then run atr admin assets reconcile to bring the links in line with it"
    fi
    ai_tools_log__info "assets: AI_TOOLS_ASSETS in ${conf} lists ${#} entries${backup:+; backup ${backup}}"
    [[ "${AI_TOOLS_CONF__WRITE_PLACEMENT:-}" != appended ]] \
        || notice MSG-T3X4 "assets: AI_TOOLS_ASSETS was added at the end of ${conf}, which predates the key -- atr admin system post-upgrade carries it under its header from the shipped template"
}

# assets_sort_identifiers <array-name>: sort the identifiers in the named array in place, by kind, then set, then name
# (assets_is_identifier_ordered_before). An insertion sort over the array itself, without a pipe or a process,
# so the array keeps every identifier it held.
assets_sort_identifiers() {
    local -n _assets_sort_identifiers_ids="$1"
    local index position identifier
    for (( index = 1; index < ${#_assets_sort_identifiers_ids[@]}; index++ )); do
        identifier="${_assets_sort_identifiers_ids[index]}"
        position="${index}"
        while (( position > 0 )) && assets_is_identifier_ordered_before "${identifier}" "${_assets_sort_identifiers_ids[position - 1]}"; do
            _assets_sort_identifiers_ids[position]="${_assets_sort_identifiers_ids[position - 1]}"
            position=$(( position - 1 ))
        done
        _assets_sort_identifiers_ids[position]="${identifier}"
    done
}

# assets_is_identifier_ordered_before <a> <b>: succeed when identifier <a> sorts ahead of <b>: its kind, then its set,
# then its name, each compared byte by byte under LC_ALL=C, so `acme/skills/x` sorts ahead of `acme-two/skills/x`
# although `-` sorts ahead of `/` in a byte order over the whole identifier.
assets_is_identifier_ordered_before() {
    local LC_ALL=C a_rest="${1#*/}" b_rest="${2#*/}"
    if [[ "${a_rest%%/*}" != "${b_rest%%/*}" ]]; then
        [[ "${a_rest%%/*}" < "${b_rest%%/*}" ]]
    elif [[ "${1%%/*}" != "${2%%/*}" ]]; then
        [[ "${1%%/*}" < "${2%%/*}" ]]
    else
        [[ "${a_rest#*/}" < "${b_rest#*/}" ]]
    fi
}

# assets_assert_list_edit <list-name> <read-name> <added-name> <removed-name>: exit 1 under MSG-S4M7, after the rows
# the verb recorded and before the backup, unless the list about to be written holds exactly the entries read, less
# every occurrence of one removed, plus those added -- each as often as that makes it. operator.conf is then as it was:
# a verb writes only the edit it reports.
assets_assert_list_edit() {
    local -n _assets_assert_list_edit_list="$1" _assets_assert_list_edit_read="$2" _assets_assert_list_edit_added="$3" \
        _assets_assert_list_edit_removed="$4"
    local identifier
    local -A removed_identifiers=() expected_counts=()
    for identifier in "${_assets_assert_list_edit_removed[@]}"; do removed_identifiers["${identifier}"]=1; done
    for identifier in "${_assets_assert_list_edit_read[@]}" "${_assets_assert_list_edit_added[@]}"; do
        [[ -z "${removed_identifiers[${identifier}]+x}" ]] || continue
        expected_counts["${identifier}"]=$(( ${expected_counts[${identifier}]:-0} + 1 ))
    done
    for identifier in "${_assets_assert_list_edit_list[@]}"; do
        expected_counts["${identifier}"]=$(( ${expected_counts[${identifier}]:-0} - 1 ))
    done
    for identifier in "${!expected_counts[@]}"; do
        [[ "${expected_counts[${identifier}]}" == 0 ]] && continue
        ai_tools_assets__write_recorded_rows
        die MSG-S4M7 "assets: the list to write does not hold exactly the entries read with this call's change (an entry that differs: $(ai_tools_log__sanitize "${identifier}")), so AI_TOOLS_ASSETS was not written and ${AI_TOOLS_OPERATOR_CONF} is unchanged -- this is a defect in ai-tools-admin; report it"
    done
}

# assets_record_enabled_rows <identifier>...: one `enabled` row per identifier an enable added to AI_TOOLS_ASSETS,
# the list edit an `assets disable` reports as `disabled`.
assets_record_enabled_rows() {
    local identifier
    for identifier in "$@"; do
        _assets_info_row MSG-N3M8 "assets enable: an identifier added to AI_TOOLS_ASSETS" enabled "${AI_TOOLS_OPERATOR_CONF}" \
            "${identifier}" "added to AI_TOOLS_ASSETS; each placed row names a link made for it"
    done
}

# assets_finish <reconcile-status>: exit as the records page states for a command that changes the host: 1 when a write
# did not take, else the record stream's own fold. A row the verb recorded that no report wrote -- a reconcile
# that could not take the lock -- is written first.
assets_finish() {
    ai_tools_assets__write_recorded_rows
    (( $1 == 0 )) || exit 1
    ai_tools_records_base__get_exit_status || exit $?
    exit 0
}

# assets_reconcile_now: run the view transaction for the sandbox group, and return its status.
assets_reconcile_now() {
    local status=0
    ai_tools_assets__reconcile "${SANDBOX_GROUP}" || status=$?
    return "${status}"
}

# assets_enable <identifier>... | --set <set>: check every identifier before writing -- the grammar and a registry kind,
# then a binding for its set, shipped or the operator's -- refusing the whole call on the first that fails; then add
# each new one to AI_TOOLS_ASSETS once, write the line sorted (assets_sort_identifiers), and reconcile. A call
# whose every identifier is already listed leaves the file as it is. An identifier whose set is not installed is
# accepted where the set is bound, and the reconcile reports it set-absent.
assets_enable() {
    local identifier status=0 entry
    local -a listed=() entries=() added=() removed=()
    (( $# > 0 )) || reject MSG-N5U6 "assets enable takes one or more <set>/<kind>/<name>, or --set <set>"
    case "$1" in
        --set) shift; assets_enable_set "$@"; return ;;
    esac
    assets_load
    ai_tools_records_base__begin_report
    assets_lock_or_exit
    for identifier in "$@"; do
        status=0
        ai_tools_assets__parse_id "${identifier}" || status=$?
        (( status == 0 )) \
            || die MSG-E9D6 "assets enable: '$(ai_tools_log__sanitize "${identifier}")' is not an asset identifier -- ${AI_TOOLS_ASSETS__ID_DETAIL}; AI_TOOLS_ASSETS is unchanged"
        ai_tools_assets__is_binding_present "${AI_TOOLS_ASSETS__ID_SET}" \
            || die MSG-W6D7 "assets enable: no binding names the set ${AI_TOOLS_ASSETS__ID_SET} under ${AI_TOOLS_ASSETS_BINDINGS_DIR} or ${AI_TOOLS_ASSETS_OPERATOR_BINDINGS_DIR}, so ${identifier} is accepted only once a binding pins its signer; AI_TOOLS_ASSETS is unchanged"
    done
    assets_read_list listed
    for identifier in "$@"; do
        for entry in "${listed[@]}" "${added[@]}"; do [[ "${entry}" == "${identifier}" ]] && continue 2; done
        added+=( "${identifier}" )
    done
    if (( ${#added[@]} > 0 )); then
        entries=( "${listed[@]}" "${added[@]}" )
        assets_sort_identifiers entries
        assets_assert_list_edit entries listed added removed
        assets_write_list "${entries[@]}"
        assets_record_enabled_rows "${added[@]}"
    fi
    status=0
    assets_reconcile_now || status=$?
    assets_finish "${status}"
}

# assets_enable_set <set>: snapshot the set as it stands (ai_tools_assets__build_enable_snapshot, whose doc states
# which set states refuse); a refused set exits with that state's token. Each asset that passes the asset-scope rules is
# written as its own identifier, and each that does not is reported under MSG-J4E9 and left out. A later release
# of the set does not enable a new asset until that asset is enabled by name.
assets_enable_set() {
    local set="${1:-}" identifier index entry status=0
    local -a listed=() entries=() added=() removed=()
    (( $# == 1 )) || reject MSG-Z7D4 "assets enable --set takes exactly one set name"
    assets_load
    ai_tools_records_base__begin_report
    assets_lock_or_exit
    ai_tools_assets_verify__is_valid_set_name "${set}" \
        || die MSG-R3M6 "assets enable --set: '$(ai_tools_log__sanitize "${set}")' is not a set name -- 1-64 characters of a-z, 0-9 and single hyphens; AI_TOOLS_ASSETS is unchanged"
    ai_tools_assets__build_enable_snapshot "${set}"
    [[ "${AI_TOOLS_ASSETS__SNAPSHOT_STATE}" == ok ]] \
        || die MSG-P9Z3 "assets enable --set: the set ${set} is ${AI_TOOLS_ASSETS__SNAPSHOT_STATE} -- $(ai_tools_log__sanitize "${AI_TOOLS_ASSETS__SNAPSHOT_DETAIL:0:300}"); AI_TOOLS_ASSETS is unchanged"
    assets_read_list listed
    for (( index = 0; index < ${#AI_TOOLS_ASSETS__SNAPSHOT_IDS[@]}; index++ )); do
        identifier="${AI_TOOLS_ASSETS__SNAPSHOT_IDS[index]}"
        case "${AI_TOOLS_ASSETS__SNAPSHOT_TOKENS[index]}" in
            ok|capability-unsupported|integration-off) ;;
            *)  _assets_attention_row MSG-J4E9 "assets enable --set: an asset of the set that fails the asset-scope rules, left out of AI_TOOLS_ASSETS" \
                    "${AI_TOOLS_ASSETS__SNAPSHOT_TOKENS[index]}" "${AI_TOOLS_OPERATOR_CONF}" "${identifier}" \
                    "not written to AI_TOOLS_ASSETS: the asset fails a rule base enforces"
                continue ;;
        esac
        for entry in "${listed[@]}" "${added[@]}"; do [[ "${entry}" == "${identifier}" ]] && continue 2; done
        added+=( "${identifier}" )
    done
    if (( ${#added[@]} > 0 )); then
        entries=( "${listed[@]}" "${added[@]}" )
        assets_sort_identifiers entries
        assets_assert_list_edit entries listed added removed
        assets_write_list "${entries[@]}"
        assets_record_enabled_rows "${added[@]}"
    fi
    assets_reconcile_now || status=$?
    assets_finish "${status}"
}

# assets_disable <identifier>...: remove each named entry from AI_TOOLS_ASSETS -- one the list does not hold is reported
# under MSG-Q5T6 and does not refuse the others -- write the kept entries sorted (assets_sort_identifiers)
# with the backup, and reconcile, which removes the links. An identifier named more than once counts once, in the order
# first named, so it takes one `disabled` or `not-enabled` row. A call naming only identifiers the list does not hold
# leaves the file as it is.
assets_disable() {
    local identifier entry status=0 found
    local -a entries=() kept=() removed=() absent=() added=()
    (( $# > 0 )) || reject MSG-H7A8 "assets disable takes one or more <set>/<kind>/<name>"
    assets_load
    ai_tools_records_base__begin_report
    assets_lock_or_exit
    assets_read_list entries
    for identifier in "$@"; do
        for entry in "${removed[@]}" "${absent[@]}"; do [[ "${entry}" == "${identifier}" ]] && continue 2; done
        found=0
        for entry in "${entries[@]}"; do [[ "${entry}" == "${identifier}" ]] && found=1; done
        if (( found )); then
            removed+=( "${identifier}" )
        else
            absent+=( "${identifier}" )
            _assets_attention_row MSG-Q5T6 "assets disable: an identifier AI_TOOLS_ASSETS does not hold" not-enabled \
                "${AI_TOOLS_OPERATOR_CONF}" "${identifier}" "AI_TOOLS_ASSETS does not list it, so it was not disabled"
        fi
    done
    for entry in "${entries[@]}"; do
        for identifier in "${removed[@]}"; do [[ "${entry}" == "${identifier}" ]] && continue 2; done
        kept+=( "${entry}" )
    done
    if (( ${#removed[@]} > 0 )); then
        assets_sort_identifiers kept
        assets_assert_list_edit kept entries added removed
        assets_write_list "${kept[@]}"
        for identifier in "${removed[@]}"; do
            _assets_info_row MSG-Q2C6 "assets disable: an identifier removed from AI_TOOLS_ASSETS" disabled \
                "${AI_TOOLS_OPERATOR_CONF}" "${identifier}" "removed from AI_TOOLS_ASSETS; a session already running keeps what it loaded"
        done
    fi
    assets_reconcile_now || status=$?
    assets_finish "${status}"
}

# assets_list: `assets list`, and a bare `assets`: every enabled asset's state and each link a reconcile would place
# or remove, as a record stream (ai_tools_assets__report_state). Reads without the assets lock (shipped-assets.rule.md,
# ref-section-e5k5) and does not write, so it does not exit 1.
assets_list() {
    (( $# == 0 )) || reject MSG-F3D9 "assets list takes no arguments"
    assets_load
    ai_tools_records_base__begin_report
    ai_tools_assets__report_state
    ai_tools_records_base__get_exit_status || exit $?
    exit 0
}

# assets_reconcile_verb: `assets reconcile`, the view transaction alone.
assets_reconcile_verb() {
    local status=0
    (( $# == 0 )) || reject MSG-M7S5 "assets reconcile takes no arguments"
    assets_load
    ai_tools_records_base__begin_report
    assets_lock_or_exit
    assets_reconcile_now || status=$?
    assets_finish "${status}"
}

# assets_dispatch: the domain's verbs; a bare `assets` runs `list`, the one that reads.
assets_dispatch() {
    local verb="${1:-list}"; [[ $# -eq 0 ]] || shift
    case "${verb}" in
        list)      assets_list "$@" ;;
        enable)    assets_enable "$@" ;;
        disable)   assets_disable "$@" ;;
        reconcile) assets_reconcile_verb "$@" ;;
        *)         local help_command
                   help_command="$(ai_tools_help__format_help_command ai-tools-admin "assets ${verb}")"
                   reject_unknown_command MSG-D4S7 "unknown command: assets ${verb} -- run '${help_command}'" ;;
    esac
}

# ── dispatch ─────────────────────────────────────────────────────────────────────────────────
# One arm per name in the grammar's two shapes: `<collection> [verb]`, where the absent verb is `list`,
# and `<domain> <collection|verb>`. A bare collection lists, and so does a domain whose `list` reads without a write
# (`assets`); any other bare domain prints its own commands, since it has no `list` of its own.

operators_dispatch() {
    local verb="${1:-list}"; [[ $# -eq 0 ]] || shift
    case "${verb}" in
        list)   op_list   "$@" ;;
        add)    op_add    "$@" ;;
        remove) op_remove "$@" ;;
        *)      local help_command
                help_command="$(ai_tools_help__format_help_command ai-tools-admin "operators ${verb}")"
                reject_unknown_command MSG-H6J5 "unknown command: operators ${verb} -- run '${help_command}'" ;;
    esac
}

selinux_groups_dispatch() {
    local verb="${1:-list}"; [[ $# -eq 0 ]] || shift
    case "${verb}" in
        list)    sel_list ;;
        enable)  sel_enable  "$@" ;;
        disable) sel_disable "$@" ;;
        *)       local help_command
                 help_command="$(ai_tools_help__format_help_command ai-tools-admin "selinux groups ${verb}")"
                 reject_unknown_command MSG-H3F8 "unknown command: selinux groups ${verb} -- run '${help_command}'" ;;
    esac
}

selinux_dispatch() {
    [[ $# -ge 1 ]] || reject MSG-D6K9 "selinux owns one collection: 'selinux groups [list|enable|disable]'"
    local resource="$1"; shift
    case "${resource}" in
        groups) selinux_groups_dispatch "$@" ;;
        *)      local help_command
                help_command="$(ai_tools_help__format_help_command ai-tools-admin "selinux ${resource}")"
                reject_unknown_command MSG-V9Z4 "unknown command: selinux ${resource} -- run '${help_command}'" ;;
    esac
}

# ── system layout ────────────────────────────────────────────────────────────────────────────
# The installed layout as one table (layout.lib.sh): `system layout` prints every entry that applies on this host
# with what it carries now and names each field that drifted, `--check` writes the record stream ai-tools-records(5)
# states, and `system layout reconcile` brings each entry to its row. This command owns the rendering and the option
# grammar; what a row is, how each field is read and what the reconcile repairs are the library's (layout.rule.md).
# Every read and write is root's: the table reports what root sees, and the gate that refuses a session is the row's own
# reader in the process making the decision.

# layout_load: source the library and require every function this command calls. An absent reader is a refusal naming
# the library and the recovery, never an empty table.
layout_load() {
    # shellcheck source=SCRIPTDIR/../../lib/ai-tools/layout.lib.sh
    source "${LAYOUT_LIB}" 2>/dev/null || true
    declare -F ai_tools_layout__begin_pass ai_tools_layout__read_entry_lines ai_tools_layout__read_entry_state \
            ai_tools_layout__parse_stream_line ai_tools_layout__write_check_records ai_tools_layout__reconcile >/dev/null 2>&1 \
        || die MSG-C6D2 "the layout library did not load or does not define the readers this command calls: ${LAYOUT_LIB} -- reinstall ai-tools-base (sudo dnf reinstall ai-tools-base, or sudo ./install.sh install from a checkout)"
}

# layout_require_table: refuse while the table did not validate, naming the declaration refused. `--check` does not call
# it: the library writes that refusal as the stream's one error row.
layout_require_table() {
    [[ -z "${AI_TOOLS_LAYOUT__DECLARATION_ERROR}" ]] \
        || die MSG-R8U8 "the layout table did not validate: ${AI_TOOLS_LAYOUT__DECLARATION_ERROR} -- reinstall ai-tools-base"
}

# layout_reject_option <text>: the one refusal for a command line this domain does not parse, at reject's exit 2.
layout_reject_option() { reject MSG-R7Y4 "system layout: ${1}"; }

# layout_format_named_acl_entries <text>: the named and default entries of an ACL text, comma-joined, or `-`
# where the text holds the base entries alone; the base entries are the mode column's.
layout_format_named_acl_entries() {
    local acl_line named_entries=""
    while IFS= read -r acl_line; do
        case "${acl_line}" in user::*|group::*|other::*|mask::*|"") continue ;; esac
        named_entries+="${named_entries:+,}${acl_line}"
    done <<< "$1"
    printf '%s' "${named_entries:--}"
}

# layout_format_attribute_cell <observed> <expected> <drifted>: one attribute column, the read value with the declared
# one in brackets where the field drifted.
layout_format_attribute_cell() {
    if [[ "$3" == 1 ]]; then printf '%s [%s]' "${1:--}" "${2:--}"; else printf '%s' "${1:--}"; fi
}

# layout_write_table: the human table, one line per entry -- the drift column first, so a drifted line is found by eye,
# then the path (an operator's row with the operator in front), the type, the owner, the mode, the named ACL entries
# and the SELinux type, every attribute as read now -- then the counts and, while a finding remains, the remedy.
# The exit is the report state's: a finding counts toward 4, an unreadable field or an error toward 5.
layout_write_table() {
    local layout_line finding_line type_cell drift_cell finding_token finding_field operator_prefix label_expected
    local entry_count=0 clean_count=0 drifted_count=0 unreadable_count=0 error_count=0
    local -a layout_lines=() layout_fields=() layout_finding=()
    local -A layout_state=() drift_fields=()
    local -a layout_findings=()
    ai_tools_records_base__begin_report
    ai_tools_layout__begin_pass 0
    ai_tools_layout__read_entry_lines layout_lines || :
    printf '%-18s %s\n' "drift" "path  type owner mode acl selinux-type"
    for layout_line in "${layout_lines[@]}"; do
        if [[ "${layout_line}" == error$'\t'* ]]; then
            ai_tools_layout__parse_stream_line "${layout_line}" layout_fields \
                || layout_fields=( error host layout.lib.sh entries "a line of the entry set did not decode" )
            printf '%-18s %s (%s): %s\n' "error" "${layout_fields[2]:-?}" "${layout_fields[3]:-?}" "${layout_fields[4]:-}"
            error_count=$(( error_count + 1 )); ai_tools_records_base__accumulate_severity unreadable
            continue
        fi
        layout_state=(); layout_findings=(); drift_fields=(); drift_cell=""
        if ! ai_tools_layout__read_entry_state "${layout_line}" layout_state layout_findings; then
            printf '%-18s %s\n' "error" "an entry of the set did not decode"
            error_count=$(( error_count + 1 )); ai_tools_records_base__accumulate_severity unreadable
            continue
        fi
        entry_count=$(( entry_count + 1 ))
        for finding_line in "${layout_findings[@]}"; do
            ai_tools_layout__parse_stream_line "${finding_line}" layout_finding || layout_finding=( error entry )
            finding_token="${layout_finding[0]}" finding_field="${layout_finding[1]:-}"
            case "${finding_token}" in
                *-drift)          drift_fields["${finding_field}"]=1; drift_cell+="${drift_cell:+,}${finding_field}"
                                  ai_tools_records_base__accumulate_severity attention ;;
                unreadable|error) drift_cell+="${drift_cell:+,}${finding_token}"
                                  unreadable_count=$(( unreadable_count + 1 ))
                                  ai_tools_records_base__accumulate_severity unreadable ;;
                *)                drift_cell+="${drift_cell:+,}${finding_token}"
                                  ai_tools_records_base__accumulate_severity attention ;;
            esac
        done
        if (( ${#layout_findings[@]} == 0 )); then
            clean_count=$(( clean_count + 1 )); drift_cell=ok
        else
            drifted_count=$(( drifted_count + 1 ))
        fi
        case "${layout_state[type]:-}" in
            file|directory|link|absent) type_cell="${layout_state[type]}" ;;
            *)                          type_cell="?" ;;
        esac
        operator_prefix="${layout_state[operator]:+${layout_state[operator]}: }"
        label_expected="${layout_state[expected_label]:-}"
        printf '%-18s %s%s  %s %s %s %s %s\n' "${drift_cell}" "${operator_prefix}" "${layout_state[path]}" "${type_cell}" \
            "$(layout_format_attribute_cell "${layout_state[owner]:-}" "${layout_state[expected_owner]}" "${drift_fields[owner]:-0}")" \
            "$(layout_format_attribute_cell "${layout_state[mode]:-}" "${layout_state[expected_mode]}" "${drift_fields[mode]:-0}")" \
            "$(layout_format_attribute_cell "$(layout_format_named_acl_entries "${layout_state[acl]:-}")" \
                "$(layout_format_named_acl_entries "${layout_state[expected_acl]}")" "${drift_fields[acl]:-0}")" \
            "$(layout_format_attribute_cell "${layout_state[label]:-}" "${label_expected:-${layout_state[label]:-}}" \
                "${drift_fields[label]:-0}")"
    done
    printf '\n%s entries: %s at their row, %s with a finding, %s unreadable, %s error line(s)\n' \
        "${entry_count}" "${clean_count}" "${drifted_count}" "${unreadable_count}" "${error_count}"
    (( drifted_count + error_count == 0 )) \
        || printf 'restore each entry to its row:  atr admin system layout reconcile\n'
    ai_tools_records_base__get_exit_status || return $?
    return 0
}

# layout_list [--check [--all] [--format tsv]]: the table, or with `--check` the record stream of ai-tools-records(5)
# in the report's own shell, read-only in both forms. A record the shared writer could not write ends the stream
# with the warning the exit 5 reports.
layout_list() {
    local check=0 all=0 stream_format=tsv stream_format_given=0 refusal=""
    while [[ $# -gt 0 && -z "${refusal}" ]]; do
        case "$1" in
            --check)  check=1 ;;
            --all)    all=1 ;;
            --format) if [[ $# -ge 2 ]]; then stream_format="$2"; stream_format_given=1; shift
                      else refusal="--format takes a value (tsv)"; fi ;;
            *)        refusal="unknown argument '$1' (--check [--all] [--format tsv])" ;;
        esac
        shift
    done
    if [[ -z "${refusal}" ]] && (( ! check && (all || stream_format_given) )); then
        refusal="--all and --format apply to --check alone"
    elif [[ -z "${refusal}" && "${stream_format}" != tsv ]]; then
        refusal="unknown --format '${stream_format}' (tsv)"
    fi
    [[ -z "${refusal}" ]] || layout_reject_option "${refusal}"
    layout_load
    if (( check )); then
        ai_tools_records_base__begin_report
        if ! ai_tools_layout__write_check_records "${all}"; then
            warn MSG-Y2F7 "a record could not be written, so the stream is incomplete"
            return "${AI_TOOLS_RECORDS_BASE__EXIT_UNREADABLE}"
        fi
        ai_tools_records_base__get_exit_status || return $?
        return 0
    fi
    layout_require_table
    layout_write_table
}

# layout_reconcile [--without-rpm]: bring every entry to its row through the library, each of its lines printed in this
# command's voice as it arrives, then the counts. The library runs as a collector (records.rule.md): its PID is saved
# on the statement that opens it, and its status, the outcome ai-tools-admin(8) states, is the command's.
#
# The run is bounded by AI_TOOLS_LAYOUT_RUN_BOUND seconds (600 by default; a root-only test hook, tests.rule.md,
# whose malformed value takes the default). The lock bounds how long a run waits for another; the deadline bounds
# the run holding it, which an open blocked on a FIFO at a row's path would otherwise hold until the host is restarted.
# Past it the collector and its direct children take SIGTERM, then SIGKILL a second later, which ends the lock's
# holders, and the command reports MSG-H8U2 at exit 1: the run did not complete its table, so it is a failure and not
# a cap.
layout_reconcile() {
    local without_rpm=0 refusal="" collector_fd collector_pid reconcile_status=0 layout_line line_tag entry_path
    local finding_field changed_count=0 refused_count=0 error_count=0
    local run_bound="${AI_TOOLS_LAYOUT_RUN_BOUND:-600}" deadline remaining read_status timed_out=0
    local -a layout_fields=() collector_children=()
    [[ "${run_bound}" =~ ^[1-9][0-9]{0,5}$ ]] || run_bound=600
    while [[ $# -gt 0 && -z "${refusal}" ]]; do
        case "$1" in
            --without-rpm) without_rpm=1 ;;
            *)             refusal="unknown argument '$1' (--without-rpm)" ;;
        esac
        shift
    done
    [[ -z "${refusal}" ]] || layout_reject_option "reconcile: ${refusal}"
    layout_load
    layout_require_table
    deadline=$(( SECONDS + run_bound ))
    exec {collector_fd}< <(ai_tools_layout__reconcile "${without_rpm}"); collector_pid=$!
    while :; do
        remaining=$(( deadline - SECONDS ))
        (( remaining > 0 )) || { timed_out=1; break; }
        read_status=0
        IFS= read -r -t "${remaining}" -u "${collector_fd}" layout_line || read_status=$?
        (( read_status <= 128 )) || { timed_out=1; break; }
        (( read_status == 0 )) || break
        if ! ai_tools_layout__parse_stream_line "${layout_line}" layout_fields || (( ${#layout_fields[@]} < 4 )); then
            printf 'ai-tools-admin: error: a line of the reconcile did not decode\n' >&2
            error_count=$(( error_count + 1 )); continue
        fi
        line_tag="${layout_fields[0]}" entry_path="${layout_fields[1]}" finding_field="${layout_fields[2]}"
        case "${line_tag}" in
            changed) changed_count=$(( changed_count + 1 ))
                     log "changed ${entry_path} ${finding_field}: ${layout_fields[3]//$'\n'/,} -> ${layout_fields[4]//$'\n'/,}" ;;
            refused) refused_count=$(( refused_count + 1 ))
                     printf 'ai-tools-admin: refused %s %s: %s\n' "${entry_path}" "${finding_field}" "${layout_fields[3]}" >&2 ;;
            error)   error_count=$(( error_count + 1 ))
                     case "${finding_field}" in
                         lock) warn MSG-J7S6 "the reconcile did not run: ${layout_fields[3]}" ;;
                         *)    printf 'ai-tools-admin: error %s %s: %s\n' "${entry_path}" "${finding_field}" "${layout_fields[3]}" >&2 ;;
                     esac ;;
            *)       error_count=$(( error_count + 1 ))
                     printf 'ai-tools-admin: error: a line of the reconcile carries no tag\n' >&2 ;;
        esac
    done
    if (( timed_out )); then
        mapfile -t collector_children < <(pgrep -P "${collector_pid}" 2>/dev/null || true)
        kill -TERM "${collector_pid}" "${collector_children[@]}" 2>/dev/null || :
        sleep 1
        kill -KILL "${collector_pid}" "${collector_children[@]}" 2>/dev/null || :
    fi
    exec {collector_fd}<&-
    wait "${collector_pid}" || reconcile_status=$?
    if (( timed_out )); then
        warn MSG-H8U2 "the reconcile did not finish within ${run_bound}s and was stopped; a row may be left short of its state -- see: atr admin system layout"
        error_count=$(( error_count + 1 )); reconcile_status=1
    fi
    log "layout: ${changed_count} changed, ${refused_count} refused, ${error_count} error(s)"
    (( refused_count == 0 )) \
        || warn MSG-W5G4 "a state the reconcile does not repair remains; each refused line above names its remedy (man ai-tools-admin)"
    # 2 is the library refusing a caller that is not root or a table that did not validate, each answered before this
    # ran; it is reported as a failure rather than read as a rejected command line.
    if (( reconcile_status == 2 )); then
        printf 'ai-tools-admin: error: the layout library refused the reconcile\n' >&2
        reconcile_status=1
    fi
    return "${reconcile_status}"
}

system_layout_dispatch() {
    # `list` is the zero-argument default, and an option after `layout` is the list's. A bare `system layout` has no
    # word to shift, and a failed `shift` ends the command silently under `set -e`.
    local verb="${1:-list}"
    case "${verb}" in
        list)      [[ $# -eq 0 ]] || shift; layout_list "$@" ;;
        reconcile) shift; layout_reconcile "$@" ;;
        --*)       layout_list "$@" ;;
        *)         local help_command
                   help_command="$(ai_tools_help__format_help_command ai-tools-admin "system layout ${verb}")"
                   # The domain's one refusal code, defined at layout_reject_option and printed here as the token, since
                   # a second definition of it would be a second message under one code.
                   printf "MSG-R7Y4\nai-tools-admin: unknown command: system layout %s -- run '%s'\n" \
                       "${verb}" "${help_command}" >&2
                   exit 2 ;;
    esac
}

system_entrypoints_dispatch() {
    # No `list` yet, so a bare `system entrypoints` names its verb rather than running one: relabel re-fetches a signed
    # manifest and rewrites a pin, which is not a reading a default may take.
    [[ $# -ge 1 ]] || reject MSG-U8G3 "system entrypoints takes a verb: 'system entrypoints relabel'"
    local verb="$1"; shift
    case "${verb}" in
        relabel) entrypoints_relabel "$@" ;;
        *)       local help_command
                 help_command="$(ai_tools_help__format_help_command ai-tools-admin "system entrypoints ${verb}")"
                 reject_unknown_command MSG-T7N9 "unknown command: system entrypoints ${verb} -- run '${help_command}'" ;;
    esac
}

system_dispatch() {
    [[ $# -ge 1 ]] || reject MSG-C7S7 "system takes a resource or a verb: 'system bootstrap', 'system entrypoints relabel', 'system layout', 'system post-upgrade'"
    local name="$1"; shift
    case "${name}" in
        bootstrap)    system_bootstrap "$@" ;;
        entrypoints)  system_entrypoints_dispatch "$@" ;;
        layout)       system_layout_dispatch "$@" ;;
        post-upgrade) postupgrade "$@" ;;
        *)            local help_command
                      help_command="$(ai_tools_help__format_help_command ai-tools-admin "system ${name}")"
                      reject_unknown_command MSG-Q2X5 "unknown command: system ${name} -- run '${help_command}'" ;;
    esac
}

# Sourced rather than executed (see the note at the root check): stop here with every function defined and no command
# dispatched, so the caller's arguments are not read as a command.
[[ "${BASH_SOURCE[0]}" == "${0}" ]] || return 0

# A help request, a bare run and `--version` are answered ahead of the root check, so a command reaches this point.
case "$1" in
    operators) shift; operators_dispatch "$@" ;;
    selinux)   shift; selinux_dispatch   "$@" ;;
    system)    shift; system_dispatch    "$@" ;;
    status)    shift; status             "$@" ;;
    assets)    shift; assets_dispatch    "$@" ;;
    # The stop helper decides what it accepts and refuses; ai-tools(1) states the command under stop.
    stop)      shift; exec "${STOP_BIN}" "$@" ;;
    # Anything else is either a domain a provider package contributed or an unknown command, and only the discovered set
    # tells the two apart. Base names are matched first, so a fragment cannot shadow one however it is named.
    *) contributed_dispatch "$@" ;;
esac
