#!/bin/bash
# Privileged seam for the unprivileged hal0-api / hermes provisioner: only ever
# writes the per-agent .env files that the D hardened-perms model pins root:root.
#
# Background (D hardened-perms): hal0-api can run as the unprivileged `hal0`
# system user, but the hermes provisioner still has to write two .env files
# that live in root:root directories:
#   * the outbound secrets vault /var/lib/hal0/secrets/agents/<agent>.env
#     (0600 root:root — pid1/root reads it as the unit's EnvironmentFile), and
#   * the non-secret driver env /etc/hal0/agents/<agent>.env (0644 root:root —
#     hal0 API/MCP URLs the hermes wrapper sources).
# An unprivileged provisioner cannot write into those root-owned dirs, so it
# delegates exactly those two writes here, just like hal0-slotctl delegates the
# slot-unit + systemctl ops.
#
# This script is the entire privileged surface for agent env writes. The agent
# regex below is a security requirement: it rejects path traversal (../), shell
# metacharacters, and anything that is not a literal agent name, so the grant in
# /etc/sudoers.d/hal0-agentenv can never be widened into arbitrary file writes.
# The target paths are constructed HERE from the validated name; the caller
# never supplies a path. File CONTENT arrives on stdin.
set -euo pipefail
cmd="${1:-}"; agent="${2:-}"
[[ "$agent" =~ ^[a-z0-9][a-z0-9-]{0,31}$ ]] || { echo "hal0-agentenv: bad agent name" >&2; exit 2; }
secrets="/var/lib/hal0/secrets/agents/${agent}.env"
driver="/etc/hal0/agents/${agent}.env"
case "$cmd" in
  merge-secrets)
    # stdin = KEY=VALUE update lines. Read-merge-write AS ROOT: the unprivileged
    # caller cannot read the 0600 vault to merge it, so the merge happens here.
    # Operator-added lines/comments and unrelated keys are preserved; matching
    # keys are replaced; new keys are appended. Atomic (tmp + os.replace),
    # 0600 root:root.
    install -d -m 0700 -o root -g root /var/lib/hal0/secrets /var/lib/hal0/secrets/agents
    # NB: the script is passed via `-c` (an argv), NOT a `<<heredoc` — a heredoc
    # would become python's stdin and shadow the caller's piped KEY=VALUE
    # updates, silently no-op'ing the merge. With `-c`, stdin stays the caller's.
    python3 -c '
import os, sys, tempfile
path = sys.argv[1]
updates = {}
for line in sys.stdin.read().splitlines():
    s = line.strip()
    if not s or s.startswith("#") or "=" not in s:
        continue
    k, v = s.split("=", 1)
    updates[k.strip()] = v
existing = []
if os.path.exists(path):
    with open(path, encoding="utf-8") as f:
        existing = f.read().splitlines()
seen, out = set(), []
for line in existing:
    s = line.strip()
    if not s or s.startswith("#"):
        out.append(line)
        continue
    k = s.split("=", 1)[0].strip()
    if k in updates:
        out.append(f"{k}={updates[k]}")
        seen.add(k)
    else:
        out.append(line)
for k, v in updates.items():
    if k not in seen:
        out.append(f"{k}={v}")
d = os.path.dirname(path)
fd, tmp = tempfile.mkstemp(dir=d)
try:
    with os.fdopen(fd, "w", encoding="utf-8") as f:
        f.write("\n".join(out) + "\n")
    os.chmod(tmp, 0o600)
    os.chown(tmp, 0, 0)
    os.replace(tmp, path)
except BaseException:
    try:
        os.unlink(tmp)
    except OSError:
        pass
    raise
' "$secrets"
    ;;
  write-driver-env)
    # stdin = full driver-env body (non-secret URLs). Atomic, 0644 root:root.
    install -d -m 0755 -o root -g root /etc/hal0/agents
    umask 022
    tmp="$(mktemp "${driver}.XXXXXX")"
    trap 'rm -f "$tmp"' EXIT
    cat > "$tmp"
    chown root:root "$tmp"
    chmod 0644 "$tmp"
    mv -f "$tmp" "$driver"
    trap - EXIT
    ;;
  *)
    echo "hal0-agentenv: bad cmd: ${cmd}" >&2
    exit 2
    ;;
esac
