#!/bin/sh
# /usr/bin/intergen-privileged-runner — pkexec target for InterGen privileged tools.
#
# Invoked by intergen via pkexec when a tool the provenance gate
# classified PRIVILEGED_STATE_CHANGING (per RFC v0.1 §6) has been
# authorized for execution. Runs as root after PolicyKit authentication
# against org.intergenos.intergen.privileged-tool.
#
# Two-layer security:
#   1. PolicyKit (this runner is the exec.path target) — handles
#      AUTHENTICATION (user enters password; cached per session via
#      auth_admin_keep).
#   2. intergen.privileged_dispatch (Python entry point) — handles
#      AUTHORIZATION (validates tool name against allowlist + validates
#      arguments per-tool + executes the underlying command). Argument
#      validation lives in Python because the privileged-tool allowlist
#      and per-tool argument schemas live with the intergen tool registry
#      (intergen/tool_registry.py), and the runner must call back into
#      that registry to enforce the same contract intergen enforced
#      pre-pkexec.
#
# Argv contract (from the intergen caller via pkexec):
#   $1 — tool name (one of the PRIVILEGED_STATE_CHANGING allowlist;
#        validated by Python dispatcher against intergen.tool_registry)
#   $2 — JSON arguments object (validated against the tool's schema
#        by the Python dispatcher before execution)
#   $3 — AI-6 dispatch token (per-install-key HMAC binding the human
#        review-modal approval to this exact tool+args+uid; minted by the
#        user daemon on approval, verified root-side by the Python
#        dispatcher BEFORE execution).
#
# The runner is a thin shim — $1/$2 pass through to Python untouched
# (Python does the validation). $3 (the token) does NOT pass through on
# the Python dispatcher's argv: it is re-exported into the sanitized
# environment as INTERGEN_DISPATCH_TOKEN, because the dispatcher's argv is
# world-visible via /proc/<pid>/cmdline for the process lifetime, whereas
# the environment is owner+root-readable only. pkexec scrubs the caller's
# environment, so the runner's argv is the only channel that survives the
# privilege crossing — the runner performs the argv->env handoff. The
# runner's other job is to set up a clean root-context environment +
# propagate the calling user's identity for downstream tools that care
# about it (e.g. write_file's path-ownership semantics).

set -eu

# Refuse to run outside a pkexec context. A direct root invocation would
# bypass PolicyKit authentication entirely + would not have PKEXEC_UID
# set, breaking the calling-user identity propagation that the Python
# dispatcher relies on. This mirrors forge-gui-runner's defensive check
# at /usr/bin/forge-gui-runner:27-30.
PKEXEC_UID="${PKEXEC_UID:-}"
if [ -z "${PKEXEC_UID}" ]; then
    printf 'intergen-privileged-runner: PKEXEC_UID unset; refusing to run outside pkexec.\n' >&2
    exit 1
fi

# Argv strict equality: tool_name + args_json + dispatch_token, no extras.
# Widened from `-ne 2` to `-ne 3` for the AI-6 token (the 3rd arg) while
# keeping STRICT equality so a malicious caller still cannot smuggle extra
# positional arguments past this entry point. The Python dispatcher
# receives only $1 + $2 on argv by design (the token goes via env, below);
# any further argv would either be silently ignored or could be exploited
# via downstream argv-shaping. Reject early at the runner layer.
if [ "$#" -ne 3 ]; then
    printf 'intergen-privileged-runner: usage: intergen-privileged-runner <tool_name> <args_json> <dispatch_token> (exactly 3 args)\n' >&2
    exit 2
fi

TOOL_NAME="$1"
shift
ARGS_JSON="$1"
shift
TOKEN="$1"
shift

# Resolve the calling user's name (downstream tools may need
# /home/$USER context — e.g. write_file refuses to overwrite a
# file under another user's $HOME, which requires knowing the
# user). getent over $USER from /etc/passwd via uid since the
# pkexec environment may have stripped USER.
PKEXEC_USER=$(getent passwd "${PKEXEC_UID}" 2>/dev/null | cut -d: -f1)
if [ -z "${PKEXEC_USER}" ]; then
    printf 'intergen-privileged-runner: cannot resolve user for uid %s\n' "${PKEXEC_UID}" >&2
    exit 1
fi

# Sanitize environment. The LLM cannot inject env vars (provenance gate
# blocks ingress-influenced calls from reaching pkexec), but the
# intergen process itself runs as the user + carries user-session env.
# Clean PATH + locale + identity vars to a minimal known-safe set so
# privileged tool dispatch is deterministic regardless of how the
# user's shell was configured upstream. The Python dispatcher reads
# PKEXEC_UID + PKEXEC_USER directly; no other env propagation needed
# for the v1.0 minimum (manage_services + manage_packages + run_command
# + write_file all operate on system state without user-env input).
export PATH=/usr/sbin:/usr/bin:/sbin:/bin
export LANG=C.UTF-8
export LC_ALL=C.UTF-8
export HOME=/root
export PKEXEC_UID
export PKEXEC_USER

# AI-6 token handoff: re-export the token into the sanitized environment so
# the Python dispatcher reads it via INTERGEN_DISPATCH_TOKEN, NOT argv. This
# keeps the token off /proc/<pid>/cmdline (world-visible) — it lives only in
# the owner+root-readable environment. The dispatcher's argv stays exactly
# tool_name + args_json (2 positional), unchanged from the pre-token shape.
export INTERGEN_DISPATCH_TOKEN="${TOKEN}"

# Hand off to the Python dispatcher. Argv stays positional + clean (2 args;
# the token is in the env, above). python3 is guaranteed-present per
# packages/core/python (the intergen Python module ships with the same
# package set; if python3 disappears, intergen itself is broken upstream of
# this point and the issue surfaces before any tool dispatch).
exec /usr/bin/python3 -m intergen.privileged_dispatch "${TOOL_NAME}" "${ARGS_JSON}"
