Files
Alexandru MacocianandCopilot 80451af915
Charlie/project-charlie: Build image / config (push) Successful in 2s
Charlie/project-charlie: Deploy runners / config (push) Successful in 0s
Charlie/project-charlie: Deploy runners / deploy-morgott (push) Skipped
Charlie/project-charlie: Deploy runners / deploy-melina (push) Skipped
Charlie/project-charlie: Deploy stack / config (push) Successful in 1s
Charlie/project-charlie: Deploy stack / deploy (push) Skipped
Charlie/project-charlie: Build image / build (push) Successful in 5m57s
Run the agent as an unprivileged user against read-only repos
The entrypoint now mirrors and locks the repo checkout read-only
(root-owned) as root, renders the sherlock config into the copilot
user's home, then drops the service to the jarvis user. The jarvis
binary carries CAP_SETUID/SETGID so the non-root service can spawn the
agent as the separate copilot user.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5749c447-6ecd-46bd-946d-21b4d101d084
2026-07-30 18:51:25 +02:00

181 lines
6.6 KiB
Bash
Executable File

#!/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" <<EOF
[services.grafana]
issuer = "${SHERLOCK_ISSUER}"
client_id = "${SHERLOCK_CLIENT_ID}"
base_url = "${GRAFANA_URL}"
grant = "client_credentials"
username = "${user}"
password_env = "JARVIS_APP_PASSWORD"
EOF
log "sherlock: grafana service (client_credentials)"
fi
if [[ -n "${GSSH_URL:-}" ]]; then
cat >> "$cfg" <<EOF
[services.gssh]
issuer = "${SHERLOCK_ISSUER}"
client_id = "${SHERLOCK_CLIENT_ID}"
base_url = "${GSSH_URL}"
grant = "client_credentials"
username = "${user}"
password_env = "JARVIS_APP_PASSWORD"
EOF
log "sherlock: gssh service (client_credentials)"
fi
else
warn "SHERLOCK_CLIENT_ID/JARVIS_APP_PASSWORD unset; grafana+gssh MCPs disabled"
fi
# Gitea: static read-only PAT — Gitea's OAuth2 server has no machine grant.
if [[ -n "${GITEA_TOKEN:-}" ]]; then
cat >> "$cfg" <<EOF
[services.gitea]
base_url = "${GITEA_URL}"
token_env = "GITEA_TOKEN"
EOF
log "sherlock: gitea service (static token)"
else
warn "GITEA_TOKEN unset; gitea MCP disabled"
fi
chmod 600 "$cfg"
# The agent reads this as the copilot user, so hand it ownership.
if id "$COPILOT_USER" >/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_url> <dest>
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 "$@"