|
| 1 | +#!/bin/bash |
| 2 | +# Init script for the --init-script flag: installs the HumanLayer CLI |
| 3 | +# and launches its daemon. Runs as root inside the container after |
| 4 | +# startup; the daemon itself is dropped to the claude user, which is |
| 5 | +# what the agent CLI runs as. |
| 6 | +# |
| 7 | +# read -rs HUMANLAYER_LAUNCH_TOKEN && export HUMANLAYER_LAUNCH_TOKEN |
| 8 | +# claude-podman \ |
| 9 | +# --init-script examples/init-humanlayer.sh \ |
| 10 | +# --init-env HUMANLAYER_LAUNCH_TOKEN |
| 11 | +# |
| 12 | +# The token is read from the environment, never passed on the command |
| 13 | +# line: an argument would be visible to every user on the host via `ps`, |
| 14 | +# and would land in your shell history. |
| 15 | +set -euo pipefail |
| 16 | + |
| 17 | +if [ -z "${HUMANLAYER_LAUNCH_TOKEN:-}" ]; then |
| 18 | + echo "HUMANLAYER_LAUNCH_TOKEN is not set; pass it with --init-env" >&2 |
| 19 | + exit 1 |
| 20 | +fi |
| 21 | + |
| 22 | +# Pinned for reproducible installs. Bump deliberately; "latest" would |
| 23 | +# silently pull whatever npm serves at container start. |
| 24 | +HUMANLAYER_VERSION="0.31.0" |
| 25 | + |
| 26 | +# nodejs/npm to run the CLI, gcompat/libstdc++ because HumanLayer ships |
| 27 | +# glibc-linked binaries and the container is musl (Alpine) |
| 28 | +apk add --no-cache nodejs npm gcompat libstdc++ |
| 29 | + |
| 30 | +# Disable install scripts for every npm invocation in this container, |
| 31 | +# including any the agent runs later. Pre/post-install hooks are the |
| 32 | +# main way an npm package executes arbitrary code at install time. |
| 33 | +# Written to the global npmrc (root-owned, so the container user cannot |
| 34 | +# undo it), though note a user-level ~/.npmrc still overrides it. |
| 35 | +npm config set ignore-scripts true --location=global |
| 36 | + |
| 37 | +# Install as root into /usr/local: already on PATH, and the container |
| 38 | +# user cannot tamper with it. The flag is redundant given the config |
| 39 | +# above, but keeps this line correct on its own. |
| 40 | +npm install -g --ignore-scripts "@humanlayer/cli@${HUMANLAYER_VERSION}" |
| 41 | + |
| 42 | +humanlayer --version |
| 43 | + |
| 44 | +# The daemon runs as the agent does: uid 1000, the container's `claude` |
| 45 | +# user, which bin/claude maps to your host account via --userns=keep-id. |
| 46 | +# HumanLayer keeps its socket and session state under $HOME/.humanlayer, |
| 47 | +# so a root-owned daemon would put both where the CLI never looks — and |
| 48 | +# would give the approval broker root in the container for no reason. |
| 49 | +# Install as root, run as claude. |
| 50 | +DAEMON_USER=claude |
| 51 | +DAEMON_UID=$(id -u "$DAEMON_USER") |
| 52 | +if [ "$DAEMON_UID" != 1000 ]; then |
| 53 | + echo "expected $DAEMON_USER to be uid 1000, got $DAEMON_UID" >&2 |
| 54 | + exit 1 |
| 55 | +fi |
| 56 | + |
| 57 | +# `daemon launch` only runs in the foreground and has no detach flag, so |
| 58 | +# it must be backgrounded here — otherwise the init script never returns |
| 59 | +# and the agent CLI never starts. setsid detaches it from this exec |
| 60 | +# session; the process reparents to the container's init and lives as |
| 61 | +# long as the container does. There is no supervisor: if the daemon dies |
| 62 | +# mid-session it stays dead until you restart the container. |
| 63 | +# |
| 64 | +# The log may contain session details, so it belongs to the daemon user |
| 65 | +# and nobody else. |
| 66 | +LOG=/var/log/humanlayer-daemon.log |
| 67 | +touch "$LOG" |
| 68 | +chown "$DAEMON_USER" "$LOG" |
| 69 | +chmod 600 "$LOG" |
| 70 | + |
| 71 | +# su, not setpriv: Alpine's busybox setpriv has no --reuid. su also |
| 72 | +# gives the daemon claude's HOME, which is half the point of dropping to |
| 73 | +# it. Only HOME/SHELL/USER/LOGNAME are replaced, so the token still |
| 74 | +# reaches the inner shell through the environment instead of su's argv. |
| 75 | +setsid su "$DAEMON_USER" -c \ |
| 76 | + 'exec humanlayer daemon launch --launch-token "$HUMANLAYER_LAUNCH_TOKEN"' \ |
| 77 | + >>"$LOG" 2>&1 </dev/null & |
| 78 | +DAEMON_PID=$! |
| 79 | + |
| 80 | +# Give it a moment, then confirm it is actually up: without this a bad |
| 81 | +# token just lands in the log and the session starts daemon-less. |
| 82 | +sleep 3 |
| 83 | +if ! kill -0 "$DAEMON_PID" 2>/dev/null; then |
| 84 | + echo "humanlayer daemon exited immediately; last log lines:" >&2 |
| 85 | + tail -n 20 "$LOG" >&2 |
| 86 | + exit 1 |
| 87 | +fi |
| 88 | + |
| 89 | +echo "humanlayer daemon running (pid $DAEMON_PID), logging to $LOG" |
0 commit comments