Class: GDKBox::Provisioner
- Inherits:
-
Object
- Object
- GDKBox::Provisioner
- Defined in:
- lib/gdkbox/provisioner.rb
Overview
Runs the in-container setup steps: enabling SSH access and (optionally)
installing Claude Code. All steps are idempotent so they can be re-run
safely, for example after a gdkbox start.
Constant Summary collapse
- SSH_SETUP =
Installs and starts the OpenSSH server, then authorizes the gdkbox public key for the GDK user. Values are passed via the environment to dodge shell quoting.
The official GDK image ships an sshd_config that listens on a non-default port (2022) and runs GitLab's own
gitlab-sshdfor git-over-SSH, so we cannot assume a vanilla sshd on port 22. We drop in our own config (loaded via theIncludenear the top of sshd_config, so it overrides the image's laterPortline) pinning the listen port to the container port gdkbox publishes, ensure host keys exist, and restart sshd so it rebinds. This is idempotent and also re-run bygdkbox start. <<~'BASH' set -e if [ ! -x /usr/sbin/sshd ] && ! command -v sshd >/dev/null 2>&1; then apt-get update -qq DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openssh-server >/dev/null fi mkdir -p /run/sshd ssh-keygen -A >/dev/null 2>&1 || true mkdir -p /etc/ssh/sshd_config.d printf 'Port %s\nPasswordAuthentication no\n' "$GDKBOX_SSH_PORT" \ > /etc/ssh/sshd_config.d/00-gdkbox.conf # The stock ~/.bash_logout runs `clear_console -q`, which fails (no TTY) # under `docker exec ... bash -lc` and clobbers the command's exit # status with 1. We run agents through a login shell, so neutralize it # to keep `gdkbox dispatch` exit codes (and the rest) honest. : > "/home/$GDKBOX_USER/.bash_logout" 2>/dev/null || true # The GDK image manages its toolchain (ruby, node, bundle, ...) with mise # but only activates it in ~/.bashrc (interactive shells). `gdkbox # dispatch` and `gdkbox ssh <box> <cmd>` use non-interactive login shells, # which source ~/.profile, not ~/.bashrc. Put mise's shims on PATH there # so dispatched agents can actually run the GDK toolchain. Shims resolve # the correct tool version from the directory's mise/.tool-versions config. profile="/home/$GDKBOX_USER/.profile" touch "$profile" if ! grep -q 'gdkbox:mise-shims' "$profile" 2>/dev/null; then { echo '# gdkbox:mise-shims' echo 'if [ -d "$HOME/.local/share/mise/shims" ]; then PATH="$HOME/.local/share/mise/shims:$PATH"; fi' } >> "$profile" fi chown "$GDKBOX_USER:$GDKBOX_USER" "$profile" install -d -m 700 -o "$GDKBOX_USER" -g "$GDKBOX_USER" "/home/$GDKBOX_USER/.ssh" printf '%s\n' "$GDKBOX_PUBKEY" > "/home/$GDKBOX_USER/.ssh/authorized_keys" chown "$GDKBOX_USER:$GDKBOX_USER" "/home/$GDKBOX_USER/.ssh/authorized_keys" chmod 600 "/home/$GDKBOX_USER/.ssh/authorized_keys" /usr/sbin/sshd -t # Restart our sshd so it binds the configured port (gitlab-sshd and any # pre-existing sshd may be on other ports). Match only the OpenSSH daemon. pkill -x sshd >/dev/null 2>&1 || true sleep 1 /usr/sbin/sshd BASH
- AGENT_SETUP =
Installs an agent harness CLI (an npm package) for the GDK user.
The official GDK image manages Node with
mise, sonode/npmare not on PATH in a plain shell. We install into~/.local(which is on PATH for every shell) using mise's npm when a direct npm is unavailable, and symlink the mise-managed node so the agent'senv nodeshebang can find an interpreter. Bothnpmand the plain-npm case are handled so this works on the GDK image and on simpler images alike. The binary name and package come from the harness via the environment. <<~'BASH' set -e mkdir -p "$HOME/.local/bin" # Pin a concrete node: the agent CLIs are node scripts with `env node` # shebangs, and the mise *shim* for node errors outside a directory that # configures a node version. Symlinking the real mise install makes node # resolvable everywhere (and gets linked into /usr/local/bin afterwards). # Done before the early-exit so re-provisioning fixes the PATH too. if command -v mise >/dev/null 2>&1 && mise which node >/dev/null 2>&1; then ln -sf "$(mise which node)" "$HOME/.local/bin/node" fi if command -v "$GDKBOX_AGENT_BIN" >/dev/null 2>&1; then echo "$GDKBOX_AGENT_BIN already installed: $("$GDKBOX_AGENT_BIN" --version 2>/dev/null || echo unknown)" exit 0 fi if command -v npm >/dev/null 2>&1; then npm install -g --prefix "$HOME/.local" $GDKBOX_NPM_FLAGS "$GDKBOX_NPM_PKG" elif command -v mise >/dev/null 2>&1 && mise which npm >/dev/null 2>&1; then mise exec -- npm install -g --prefix "$HOME/.local" $GDKBOX_NPM_FLAGS "$GDKBOX_NPM_PKG" else echo "Neither npm nor mise-managed node found in box; cannot install $GDKBOX_AGENT_BIN" >&2 exit 1 fi "$HOME/.local/bin/$GDKBOX_AGENT_BIN" --version || true BASH
- AGENT_LINK =
Symlinks the user-installed agent binary (and the mise-managed
nodeit needs) into /usr/local/bin so they are on PATH for every shell, not just login shells. Without this,ssh box <agent>(a non-login shell) would not find the CLI even thoughgdkbox dispatch(a login shell) does. <<~'BASH' set -e home="/home/$GDKBOX_USER" if [ -e "$home/.local/bin/$GDKBOX_AGENT_BIN" ]; then ln -sf "$home/.local/bin/$GDKBOX_AGENT_BIN" "/usr/local/bin/$GDKBOX_AGENT_BIN" fi if [ -e "$home/.local/bin/node" ]; then ln -sf "$home/.local/bin/node" /usr/local/bin/node fi BASH
- API_KEY_SETUP =
Writes the harness's API key into the box so dispatched agents can authenticate without a human. The key is exported under the env var the harness's provider reads (e.g. ANTHROPIC_API_KEY, OPENAI_API_KEY). It is stored only inside the container, at a 0600 file owned by the GDK user, sourced explicitly by
gdkbox dispatchand wired into interactive shells. The key never touches host-side metadata. <<~'BASH' set -e home="/home/$GDKBOX_USER" install -d -m 700 -o "$GDKBOX_USER" -g "$GDKBOX_USER" "$home/.gdkbox" umask 077 printf "export %s='%s'\n" "$GDKBOX_API_KEY_ENV" "$GDKBOX_API_KEY" > "$home/.gdkbox/env" chown "$GDKBOX_USER:$GDKBOX_USER" "$home/.gdkbox/env" chmod 600 "$home/.gdkbox/env" line='[ -f "$HOME/.gdkbox/env" ] && . "$HOME/.gdkbox/env"' for f in "$home/.bashrc" "$home/.profile"; do touch "$f" grep -qF "$line" "$f" || printf '%s\n' "$line" >> "$f" chown "$GDKBOX_USER:$GDKBOX_USER" "$f" done BASH
- GIT_IDENTITY_SETUP =
Seeds the GDK user's global git identity so
git commitworks inside the box. Values arrive via the environment (dodging shell quoting) and blanks are skipped, so a partial host config seeds what it can. <<~'BASH' set -e [ -n "$GDKBOX_GIT_NAME" ] && git config --global user.name "$GDKBOX_GIT_NAME" [ -n "$GDKBOX_GIT_EMAIL" ] && git config --global user.email "$GDKBOX_GIT_EMAIL" true BASH
- GIT_SSH_SETUP =
Prepares the box's SSH client for real git traffic against a remote host (issue #6, hit when a gitlab_remote is configured):
StrictHostKeyChecking accept-new: a fresh box has an empty known_hosts, so the first fetch would die on host key verification.- Connection multiplexing: lazy fetches from the treeless clone open SSH connections in bursts; gitlab.com throttles that, which surfaces as a misleading "Permission denied (publickey)". One shared control connection absorbs the burst.
The stanza is tagged per host, so re-runs are no-ops and different remotes each get their own block.
<<~'BASH' set -e mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh" config="$HOME/.ssh/config" touch "$config" && chmod 600 "$config" if ! grep -qF "gdkbox:git-ssh $GDKBOX_GIT_HOST" "$config"; then cat >> "$config" <<EOF # gdkbox:git-ssh $GDKBOX_GIT_HOST Host $GDKBOX_GIT_HOST StrictHostKeyChecking accept-new ControlMaster auto ControlPath ~/.ssh/cm-%r@%h-%p ControlPersist 20m ServerAliveInterval 30 EOF fi BASH
- GITLAB_REMOTE_SETUP =
Points the GitLab checkout's origin at a different repository. The image hardcodes the community mirror (gitlab-community/gitlab-org/gitlab); team members typically want the canonical gitlab-org/gitlab. The mirror shares its history, so a set-url plus fetch is all it takes (the checkout is treeless, keeping the fetch cheap).
<<~'BASH' set -e git remote set-url origin "$GDKBOX_GITLAB_REMOTE" git fetch --quiet origin BASH
Instance Method Summary collapse
-
#initialize(docker:, config:) ⇒ Provisioner
constructor
A new instance of Provisioner.
-
#setup_agent(container_name, harness) ⇒ Object
Install the given harness (an npm-installable agent CLI) into the box.
- #setup_api_key(container_name, api_key, key_env) ⇒ Object
- #setup_git_identity(container_name, name:, email:) ⇒ Object
- #setup_git_ssh(container_name, host) ⇒ Object
- #setup_gitlab_remote(container_name, url) ⇒ Object
- #setup_ssh(container_name, public_key) ⇒ Object
Constructor Details
#initialize(docker:, config:) ⇒ Provisioner
Returns a new instance of Provisioner.
186 187 188 189 |
# File 'lib/gdkbox/provisioner.rb', line 186 def initialize(docker:, config:) @docker = docker @config = config end |
Instance Method Details
#setup_agent(container_name, harness) ⇒ Object
Install the given harness (an npm-installable agent CLI) into the box.
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
# File 'lib/gdkbox/provisioner.rb', line 204 def setup_agent(container_name, harness) unless harness.installable? raise Error, "gdkbox cannot auto-install the '#{harness.id}' harness yet." end @docker.exec( container_name, AGENT_SETUP, user: @config.ssh_user, env: { "GDKBOX_AGENT_BIN" => harness.bin, "GDKBOX_NPM_PKG" => harness.npm_package, "GDKBOX_NPM_FLAGS" => harness.npm_flags.to_s } ) @docker.exec( container_name, AGENT_LINK, user: "root", env: { "GDKBOX_USER" => @config.ssh_user, "GDKBOX_AGENT_BIN" => harness.bin } ) end |
#setup_api_key(container_name, api_key, key_env) ⇒ Object
251 252 253 254 255 256 257 258 259 260 261 |
# File 'lib/gdkbox/provisioner.rb', line 251 def setup_api_key(container_name, api_key, key_env) @docker.exec( container_name, API_KEY_SETUP, user: "root", env: { "GDKBOX_USER" => @config.ssh_user, "GDKBOX_API_KEY" => api_key, "GDKBOX_API_KEY_ENV" => key_env } ) end |
#setup_git_identity(container_name, name:, email:) ⇒ Object
223 224 225 226 227 228 229 230 231 232 |
# File 'lib/gdkbox/provisioner.rb', line 223 def setup_git_identity(container_name, name:, email:) @docker.exec( container_name, GIT_IDENTITY_SETUP, user: @config.ssh_user, env: { "GDKBOX_GIT_NAME" => name.to_s, "GDKBOX_GIT_EMAIL" => email.to_s } ) end |
#setup_git_ssh(container_name, host) ⇒ Object
234 235 236 237 238 239 240 |
# File 'lib/gdkbox/provisioner.rb', line 234 def setup_git_ssh(container_name, host) @docker.exec( container_name, GIT_SSH_SETUP, user: @config.ssh_user, env: { "GDKBOX_GIT_HOST" => host } ) end |
#setup_gitlab_remote(container_name, url) ⇒ Object
242 243 244 245 246 247 248 249 |
# File 'lib/gdkbox/provisioner.rb', line 242 def setup_gitlab_remote(container_name, url) @docker.exec( container_name, GITLAB_REMOTE_SETUP, user: @config.ssh_user, workdir: @config.gitlab_checkout_path, env: { "GDKBOX_GITLAB_REMOTE" => url } ) end |
#setup_ssh(container_name, public_key) ⇒ Object
191 192 193 194 195 196 197 198 199 200 201 |
# File 'lib/gdkbox/provisioner.rb', line 191 def setup_ssh(container_name, public_key) @docker.exec( container_name, SSH_SETUP, user: "root", env: { "GDKBOX_USER" => @config.ssh_user, "GDKBOX_PUBKEY" => public_key, "GDKBOX_SSH_PORT" => Config::SSH_CONTAINER_PORT.to_s } ) end |