#!/usr/bin/env bash # jarvis entrypoint. Runs as root to: fix the /data bind-mount ownership, # mirror the Charlie repositories into the diagnostic workdir, lock that # checkout read-only, and render the sherlock config where the agent can read # it. It then drops to the unprivileged `jarvis` user to run the service. # # The service itself never runs as root; it holds CAP_SETUID/SETGID (granted # to the binary in the image) so it can drop the --yolo Copilot agent to a # separate `copilot` user that cannot write the read-only repos or the DB. # # The clone step is best-effort: if it fails, jarvis still starts and serves # alerts (the agent simply has less grounding). All configuration is via env. set -euo pipefail CONTEXT_DIR="${JARVIS_WORKDIR:-/context}" DB_PATH="${JARVIS_DB_PATH:-/data/jarvis.db}" DATA_DIR="$(dirname "$DB_PATH")" GITEA_URL="${GITEA_URL:-https://gitea.alexandru.macocian.me}" GITEA_ORGS="${GITEA_ORGS:-Charlie}" # space-separated org list to mirror GITEA_TOKEN="${GITEA_TOKEN:-}" # read-only token; optional for public repos CLONE_DEPTH="${JARVIS_CLONE_DEPTH:-1}" # shallow by default to stay light SERVICE_USER="${JARVIS_SERVICE_USER:-jarvis}" # runs the service, owns /data COPILOT_USER="${JARVIS_COPILOT_USER:-copilot}" # runs the sandboxed agent log() { printf '{"time":"%s","level":"INFO","msg":"entrypoint: %s"}\n' "$(date -u +%FT%TZ)" "$*" >&2; } warn() { printf '{"time":"%s","level":"WARN","msg":"entrypoint: %s"}\n' "$(date -u +%FT%TZ)" "$*" >&2; } # render_sherlock_config writes the sherlock config.toml from the environment. # Only non-secret targets go in the file; credentials stay in the environment # and are referenced by name (password_env / token_env). Services whose # prerequisites are missing are skipped so a partial local setup still runs. # The file is placed where the copilot agent user can read it. render_sherlock_config() { local cfg="${SHERLOCK_CONFIG:-/home/${COPILOT_USER}/.config/sherlock/config.toml}" local user="${JARVIS_LDAP_USER:-sys-jarvis}" mkdir -p "$(dirname "$cfg")" : > "$cfg" # Grafana + gssh: headless client-credentials against the sherlock-cli # Authentik app (same account, same app password). The JWT carries the # account's identity/claims; Grafana [auth.jwt] and the gssh gateway # validate it. if [[ -n "${SHERLOCK_CLIENT_ID:-}" && -n "${JARVIS_APP_PASSWORD:-}" ]]; then if [[ -n "${GRAFANA_URL:-}" ]]; then cat >> "$cfg" <> "$cfg" <> "$cfg" </dev/null 2>&1; then chown -R "$COPILOT_USER":"$COPILOT_USER" "/home/${COPILOT_USER}/.config" 2>/dev/null || true fi log "rendered sherlock config at ${cfg}" } auth_header=() if [[ -n "$GITEA_TOKEN" ]]; then auth_header=(-H "Authorization: token ${GITEA_TOKEN}") fi # clone_or_pull clone_or_pull() { local url="$1" dest="$2" # Inject the token into the URL so git can authenticate non-interactively. local auth_url="$url" if [[ -n "$GITEA_TOKEN" ]]; then auth_url="${url/https:\/\//https:\/\/oauth2:${GITEA_TOKEN}@}" fi if [[ -d "$dest/.git" ]]; then git -C "$dest" remote set-url origin "$auth_url" >/dev/null 2>&1 || true if git -C "$dest" pull --ff-only --quiet 2>/dev/null; then log "updated ${dest}" else warn "pull failed for ${dest} (keeping existing checkout)" fi else if git clone --quiet --depth "$CLONE_DEPTH" "$auth_url" "$dest" 2>/dev/null; then log "cloned ${dest}" else warn "clone failed for ${url}" fi fi # Never persist the token in the world-readable, locked-down checkout. [[ -d "$dest/.git" ]] && git -C "$dest" remote set-url origin "$url" >/dev/null 2>&1 || true } mirror_org() { local org="$1" page=1 while :; do local resp resp="$(curl -fsSL "${auth_header[@]}" \ "${GITEA_URL}/api/v1/orgs/${org}/repos?limit=50&page=${page}" 2>/dev/null || echo '[]')" local count count="$(printf '%s' "$resp" | jq 'length' 2>/dev/null || echo 0)" [[ "$count" -eq 0 ]] && break while IFS=$'\t' read -r name clone_url; do [[ -z "$name" ]] && continue clone_or_pull "$clone_url" "${CONTEXT_DIR}/${org}/${name}" done < <(printf '%s' "$resp" | jq -r '.[] | [.name, .clone_url] | @tsv') page=$((page + 1)) done } # lock_repos makes the mirrored checkout read-only and root-owned so the # --yolo agent (running as the copilot user) can read it for grounding but # cannot modify any working tree. root still refreshes it on the next start # via DAC override. lock_repos() { chown -R root:root "$CONTEXT_DIR" 2>/dev/null || true find "$CONTEXT_DIR" -type d -exec chmod 555 {} + 2>/dev/null || true find "$CONTEXT_DIR" -type f -exec chmod 444 {} + 2>/dev/null || true log "locked ${CONTEXT_DIR} read-only (root-owned) for the agent" } # --- root-only setup, then drop privileges to the service user --------------- if [ "$(id -u)" = "0" ]; then mkdir -p "$DATA_DIR" chown "$SERVICE_USER":"$SERVICE_USER" "$DATA_DIR" if [[ -n "$GITEA_ORGS" ]]; then mkdir -p "$CONTEXT_DIR" for org in $GITEA_ORGS; do log "mirroring org ${org} from ${GITEA_URL}" mirror_org "$org" || warn "mirroring org ${org} encountered errors" done lock_repos else log "GITEA_ORGS empty; skipping repo mirror" fi render_sherlock_config log "starting jarvis as ${SERVICE_USER} (agent sandboxed as ${COPILOT_USER})" exec gosu "$SERVICE_USER" jarvis "$@" fi # Fallback: already unprivileged (e.g. local `docker run --user`). Render the # config next to the current user and start without the root-only steps. render_sherlock_config || true log "starting jarvis (unprivileged; repo lockdown skipped)" exec jarvis "$@"