Skip to content

Commit a305e78

Browse files
author
Christer Barreholm
committed
feat: example init script to run humanlayer daemon
1 parent e1bdeb2 commit a305e78

1 file changed

Lines changed: 89 additions & 0 deletions

File tree

examples/init-humanlayer.sh

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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

Comments
 (0)