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
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
181 lines
6.6 KiB
Bash
Executable File
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 "$@"
|