GOAL.md's first gap: without a runner a standalone image is a read-only viewer, because launching `claude -p` needed a host-side process. The Claude Code CLI is a self-contained native binary, so the image can just carry it — and the *same* sidecar/sidecar.py the homelab runs on its host runs in-container against it. With RUNNER_IN_CONTAINER=1 (the standalone default) docker-entrypoint.sh starts the runner on the loopback, mints a SIDECAR_TOKEN if none was given, overrides SIDECAR_URL to point at it, and gives the CLI a writable $HOME on the data volume. The transcripts it writes there become a third live source (RUNNER_TRANSCRIPTS_DIR → SOURCE_DIRS), so an in-container session streams into the viewer like any other. Verified end to end: a standalone container (workspace + data, no homelab, no host sidecar) spawns a session, the CLI runs it, and the turn renders in the conversation list with its model tag. The CLI authenticates from ANTHROPIC_API_KEY or a Claude home mounted at RUNNER_HOME. The homelab is unchanged: it leaves the flag off and keeps its host sidecar, which is what lets a run use the host's own hooks, skills and credentials. standalone-smoke.sh now also asserts the CLI is on PATH and the runner is healthy — the packaging property that would otherwise regress silently. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
68 lines
3.4 KiB
Bash
68 lines
3.4 KiB
Bash
#!/bin/sh
|
|
# Container entrypoint: optionally start the *in-image runner*, then the backend.
|
|
#
|
|
# GOAL.md's first gap is "fold the host sidecar into the container": without a
|
|
# runner the image is a read-only viewer, because launching `claude -p` needs a
|
|
# process that can reach the CLI. The Claude Code CLI is a self-contained native
|
|
# binary, so the image can simply carry it — and the very same `sidecar/sidecar.py`
|
|
# that runs on the homelab host runs here, on the container's loopback.
|
|
#
|
|
# RUNNER_IN_CONTAINER=1 → start sidecar/sidecar.py on 127.0.0.1:$RUNNER_PORT
|
|
# and point the backend's SIDECAR_URL at it.
|
|
# unset (the homelab) → nothing changes: the backend proxies to the host
|
|
# sidecar over host.docker.internal, as before.
|
|
#
|
|
# The runner needs a writable HOME for the CLI's config/credentials, and the
|
|
# transcripts it writes there are picked up as a live source (RUNNER_TRANSCRIPTS_DIR
|
|
# → backend/main.py SOURCE_DIRS), so sessions spawned in-container stream into the
|
|
# viewer exactly like host-spawned ones.
|
|
#
|
|
# Auth for the CLI is whatever Claude Code itself accepts: ANTHROPIC_API_KEY (the
|
|
# key the token counter already uses), or an existing Claude home mounted at
|
|
# RUNNER_HOME (its OAuth credentials come along with it).
|
|
set -e
|
|
|
|
if [ "${RUNNER_IN_CONTAINER:-0}" = "1" ]; then
|
|
RUNNER_HOME="${RUNNER_HOME:-/data/home}"
|
|
RUNNER_PORT="${RUNNER_PORT:-8790}"
|
|
# Loopback-only, but sidecar.py refuses to run without a token — mint an
|
|
# ephemeral one when the operator didn't supply it, and hand it to both sides.
|
|
if [ -z "${SIDECAR_TOKEN:-}" ]; then
|
|
SIDECAR_TOKEN="$(python -c 'import secrets; print(secrets.token_hex(16))')"
|
|
fi
|
|
# The in-container runner is authoritative when it's on: the backend talks to
|
|
# it over loopback, not to any host sidecar.
|
|
SIDECAR_URL="http://127.0.0.1:${RUNNER_PORT}"
|
|
# Where the CLI writes its transcripts, and therefore what the backend watches.
|
|
RUNNER_TRANSCRIPTS_DIR="${RUNNER_TRANSCRIPTS_DIR:-${RUNNER_HOME}/.claude/projects}"
|
|
# Sessions run against the mounted repo by default.
|
|
SIDECAR_CWD="${SIDECAR_CWD:-${WORKSPACE:-/workspace}}"
|
|
SIDECAR_LOG_DIR="${SIDECAR_LOG_DIR:-/data/runner-logs}"
|
|
# Remote control pairs a run with the Claude app and needs an interactive
|
|
# login; a container that authenticates with an API key can't do that, so it's
|
|
# off unless the operator (with a mounted, logged-in Claude home) asks for it.
|
|
SIDECAR_REMOTE_CONTROL="${SIDECAR_REMOTE_CONTROL:-0}"
|
|
|
|
export SIDECAR_TOKEN SIDECAR_URL RUNNER_TRANSCRIPTS_DIR SIDECAR_CWD \
|
|
SIDECAR_LOG_DIR SIDECAR_REMOTE_CONTROL
|
|
|
|
mkdir -p "$RUNNER_TRANSCRIPTS_DIR" "$SIDECAR_LOG_DIR"
|
|
|
|
echo "[entrypoint] in-container runner: $(claude --version 2>/dev/null || echo 'claude MISSING')" \
|
|
"on 127.0.0.1:${RUNNER_PORT} (home=${RUNNER_HOME}, cwd=${SIDECAR_CWD})"
|
|
|
|
# Supervised: if the runner dies (an OOM, a bad CLI upgrade) it comes back
|
|
# rather than leaving the viewer silently unable to spawn. The backend's own
|
|
# process stays PID 1, so the container's health is still the API's health.
|
|
(
|
|
while true; do
|
|
HOME="$RUNNER_HOME" python -m uvicorn sidecar:app \
|
|
--app-dir /app/sidecar --host 127.0.0.1 --port "$RUNNER_PORT" \
|
|
|| echo "[entrypoint] runner exited ($?), restarting in 2s"
|
|
sleep 2
|
|
done
|
|
) &
|
|
fi
|
|
|
|
exec python -m uvicorn main:app --host 0.0.0.0 --port 8080
|