Ctrl K
ComunidadGuías…

docker-entrypoint.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bash
# Robot Dev Team Project
# File: docker-entrypoint.sh
# Description: Container entrypoint configuring agent credentials and permissions.
# License: MIT
# SPDX-License-Identifier: MIT
# Copyright (c) 2025 MCKNLY LLC

set -euo pipefail

if [[ "${1:-}" != "--as-app" && "$(id -u)" -eq 0 ]]; then
  TARGET_UID=${LOCAL_UID:-10001}
  TARGET_GID=${LOCAL_GID:-10001}

  if getent group appuser >/dev/null 2>&1; then
    CURRENT_GID=$(getent group appuser | cut -d: -f3)
    if [[ "$CURRENT_GID" != "$TARGET_GID" ]]; then
      groupmod -o -g "$TARGET_GID" appuser
    fi
  else
    groupadd -o -g "$TARGET_GID" appuser
  fi

  if id appuser >/dev/null 2>&1; then
    CURRENT_UID=$(id -u appuser)
    if [[ "$CURRENT_UID" != "$TARGET_UID" ]]; then
      usermod -o -u "$TARGET_UID" -g "$TARGET_GID" appuser
    fi
  else
    useradd -m -u "$TARGET_UID" -g "$TARGET_GID" appuser
  fi

  mkdir -p /home/appuser
  # Recursive chown serves two purposes:
  #  1. Reconcile ownership after a UID change (LOCAL_UID at runtime can
  #     differ from the build-time useradd UID).
  #  2. Reset any root-owned dotfiles a previous `docker exec -u root`
  #     (or default-user exec) may have left in $HOME, e.g. ~/.config or
  #     ~/.gitconfig created by an interactive glab invocation. Without
  #     this, the next agent dispatch fails with "permission denied"
  #     when glab-usr tries to write its config (see glab-usr's
  #     self-defense check for the runtime side of this).
  # Errors on bind-mounted dirs (.claude, .gemini, .codex) are expected
  # and silenced; the underlying host filesystem owns those.
  chown -R appuser:appuser /home/appuser /work 2>/dev/null || true

  # Drop privileges and re-exec. setpriv replaces gosu, whose Debian build is a static Go
  # binary linked against an EOL Go 1.19.8 toolchain that Debian will not rebuild.
  # Three details are load-bearing, none of them stylistic:
  #  1. No --reset-env. setpriv would otherwise clear the environment before exec, and the
  #     *_AGENT_GITLAB_TOKEN loop below walks `env` -- it would find nothing and write no
  #     token files, failing every dispatch later rather than here. It would also drop the
  #     venv-first PATH the Dockerfile sets, so `python3 -m app.preflight` runs against the
  #     wrong interpreter. gosu inherited the environment too, so this is parity, not a
  #     shortcut.
  #  2. appuser is named, never resolved to a number. The groupmod/usermod calls above pass
  #     -o, which permits a non-unique id, so a LOCAL_UID colliding with an account already
  #     in the base image would make a uid lookup return the wrong record. --init-groups
  #     then runs initgroups(3) off that same name.
  #  3. --init-groups is mandatory: setpriv rejects --regid outright without one of
  #     --keep-groups/--clear-groups/--init-groups/--groups.
  # --inh-caps=-all clears the inheritable set. Scope the claim honestly: after a uid change
  # with no file capabilities and no ambient set the inheritable set is already empty, so this
  # states the intent rather than closing a hole. It is *not* a bar on regaining privilege --
  # that would be --no-new-privs (setuid/setgid bits, file capabilities) and --bounding-set=-all
  # (the ceiling itself), both deliberately left out because they change the contract every
  # agent subprocess and runtime installer inherits, and that is a separate decision.
  # A consequence worth knowing before deleting this flag: the smoke test's CapInh assertion
  # cannot catch its removal, because CapInh reads empty either way. That assertion witnesses
  # the post-drop capability state, not the presence of the flag, and nothing in CI does.
  exec setpriv --reuid appuser --regid appuser --init-groups --inh-caps=-all "$0" --as-app "$@"
fi

if [[ "${1:-}" == "--as-app" ]]; then
  shift
fi

# HOME arrives from the image (`ENV HOME=/home/appuser`), not from the privilege drop: gosu set
# it for the target user, setpriv deliberately leaves it alone. Root already carries the right
# value, so the fallback below never fires -- but that makes the image's ENV load-bearing. Drop
# it and every agent's credential dir silently resolves under /root.
export HOME=${HOME:-/home/appuser}

# The identity after the drop, stated once where operators and CI can both see it. Without this
# the property is unobservable from outside: there is no USER directive in the image, so a bare
# `docker exec <container> id -u` reports the exec's own root and says nothing about the served
# process. scripts/ci-smoke-image.sh asserts this line.
# stderr is dropped from the name lookup alone: under `docker run --user <uid>` with a uid that
# is not in /etc/passwd -- a mode SECURITY.md documents as supported -- `id -un` still prints the
# number, but warns on the way. The numbers are what matter here and they are read separately, so
# suppress the warning rather than let it into every operator's logs.
echo "[entrypoint] running as $(id -un 2>/dev/null) uid=$(id -u) gid=$(id -g) groups=$(id -G)"

write_token_file() {
  local token="$1"
  local path="$2"

  if [[ -n "$token" ]]; then
    local previous_umask
    previous_umask=$(umask)
    mkdir -p "$(dirname "$path")"
    umask 077
    printf '%s' "$token" > "$path"
    chmod 600 "$path"
    umask "$previous_umask"
  else
    rm -f "$path"
  fi
}

# Write token files for all agents that have *_AGENT_GITLAB_TOKEN set.
# Convention: env var FOO_AGENT_GITLAB_TOKEN -> token file ~/.<agent>/glab-token
# The agent directory name is the lowercase form with underscores replaced by hyphens.
while IFS='=' read -r var_name var_value; do
  case "$var_name" in
    *_AGENT_GITLAB_TOKEN)
      agent_prefix="${var_name%_AGENT_GITLAB_TOKEN}"
      agent_dir="$(printf '%s' "$agent_prefix" | tr '[:upper:]_' '[:lower:]-')"
      write_token_file "$var_value" "$HOME/.$agent_dir/glab-token"
      ;;
  esac
done < <(env)

echo "[entrypoint] Installing/updating agent CLIs..."

# Ensure user-local bin directory exists (used by the install scripts).
# The default agent CLIs use native installers; the previous npm cache and
# global-prefix plumbing was removed alongside the Node.js runtime when Gemini
# CLI was replaced by the Antigravity binary (`agy`). The one exception is the
# optional Pi harness: when a pi-* route is enabled, scripts/install-pi.sh
# bootstraps a pinned, user-local Node into ~/.local (only `node` is exposed on
# this shared PATH; Pi's npm/npx stay in the versioned Node dir).
mkdir -p "$HOME/.local/bin"
export PATH="$HOME/.local/bin:$PATH"

# Validate the agent configuration and derive which harnesses to install.
# The preflight prints one install-script path per line on stdout and fails
# (non-zero) when a route dispatches an agent that has no usable credentials
# -- a route that could never succeed. Failing here is deliberate: the
# alternative is failing later, mid-dispatch, after a webhook has fired.
# Must run *after* the token-file loop above, which the preflight reads.
# To add a new agent CLI, drop a scripts/install-<agent>.sh file into the
# repository with a `# provides: <binary>` line (see docs/ADDING_AN_AGENT.md).
SCRIPT_DIR="/work/scripts"
if ! preflight_output="$(python3 -m app.preflight --scripts-dir "$SCRIPT_DIR")"; then
  echo "[entrypoint] FATAL: agent configuration preflight failed; refusing to start" >&2
  exit 1
fi

# stdout is `key=value` lines: install=<script> and binary=<name>.
installers=()
binaries=()
while IFS= read -r line; do
  case "$line" in
    install=*) installers+=("${line#install=}") ;;
    binary=*) binaries+=("${line#binary=}") ;;
  esac
done <<< "$preflight_output"

# A download failure stays a warning: a flaky CDN is not a misconfiguration.
for installer in "${installers[@]}"; do
  echo "[entrypoint] Running $(basename "$installer")..."
  bash "$installer" || echo "[entrypoint] WARN: $(basename "$installer") exited with errors" >&2
done

# Goose reads its provider wiring (including a local model's base_url) from the
# host config dir, which is bind-mounted read-only. A base_url on loopback is
# correct on the host but points at the container from in here, so materialize a
# container-local copy with loopback redirected at the Docker host gateway. This
# reuses the preflight's gate for free: `binary=goose` is only emitted when a
# goose route is enabled *and* credentialed, so an operator who leaves the goose
# routes commented out pays nothing.
for binary in "${binaries[@]}"; do
  if [[ "$binary" == "goose" ]]; then
    python3 -m app.goose_config || {
      echo "[entrypoint] FATAL: could not materialize the Goose config" >&2
      exit 1
    }
    break
  fi
done

# ...but a harness that is still missing once the installs are done *is* a
# misconfiguration, and every dispatch to that agent would die at exec. This
# is the other half of the preflight invariant: credentials are checked before
# the installs, binaries after. Covers a failed download, a mis-declared
# `# provides:` line, and a BYOA binary that was never baked into the image.
missing=()
for binary in "${binaries[@]}"; do
  command -v "$binary" >/dev/null 2>&1 || missing+=("$binary")
done
if [[ ${#missing[@]} -gt 0 ]]; then
  echo "[entrypoint] FATAL: harness binaries not on PATH after install: ${missing[*]}" >&2
  echo "[entrypoint]        Routes dispatch these agents, so every run would fail at exec." >&2
  echo "[entrypoint]        Check the install script above for download errors, and that its" >&2
  echo "[entrypoint]        '# provides:' line names the binary the route actually runs." >&2
  exit 1
fi

echo "[entrypoint] Starting app: $*"
exec "$@"