#!/bin/bash # Install the harnesses a worker can author with, from scripts/harness-registry.toml. # # Source it, then call unpiped — it exports credentials, which a subshell would lose: # # . /workspace/scripts/setup-harnesses.sh # harness_setup_all # # Registry reading and credential derivation live in lib/harness-credentials.sh, sourced # below, because `harbor-run` needs those and nothing else here. # # No -e here — but this file is SOURCED, and shell options belong to the caller's shell: # both post-creates run with -e, so that is what is in force. An unguarded failure below # therefore aborts container creation, which is why every failure site is individually # guarded (`|| true`, `if !`) rather than relying on this line. set -uo pipefail _HARNESS_REGISTRY_DIR="${HARNESS_SCRIPTS_DIR:-/workspace/scripts}" if [ ! -f "$_HARNESS_REGISTRY_DIR/lib/harness-credentials.sh" ]; then echo "harness-setup: FATAL — $_HARNESS_REGISTRY_DIR/lib/harness-credentials.sh is" >&2 echo "harness-setup: missing, so nothing here can read the registry. Every step below" >&2 echo "harness-setup: would report a missing interpreter instead of this." >&2 return 1 2>/dev/null || exit 1 fi # shellcheck disable=SC1091 . "$_HARNESS_REGISTRY_DIR/lib/harness-credentials.sh" # Every setup step reads the registry through _harness_query, and each call suppresses # stderr so one bad row can't abort the container. That means a BROKEN interpreter turns # the whole of setup into a silent no-op: no credentials, no CLIs, no config, no # launchers, and no error anywhere. Check it once, loudly, before any of that. harness_preflight() { local err py found=yes py=$(_raccoon_python) || { py=python3; found=no; } if ! err=$("$py" "$_HARNESS_REGISTRY_DIR/resolve_harness.py" --list 2>&1 >/dev/null); then echo "harness-setup: FATAL — cannot read the harness registry, so no agent CLI" >&2 echo "harness-setup: would be installed. Nothing below will run." >&2 echo "harness-setup: interpreter: $(command -v "$py" || echo MISSING) ($("$py" -V 2>&1))" >&2 if [ "$found" = no ]; then echo "harness-setup: no python3.11+ with tomllib found; set RACCOON_PYTHON to override" >&2 fi echo "harness-setup: registry: $_HARNESS_REGISTRY_DIR/harness-registry.toml" >&2 printf 'harness-setup: %s\n' "$err" >&2 return 1 fi } # claude installs into $HOME/.local/bin, which is not on PATH during post-create. case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) export PATH="$HOME/.local/bin:$PATH" ;; esac # --- installs ---------------------------------------------------------------- harness_install_clis() { local id cli install while IFS=$'\t' read -r id cli install; do [ -n "$install" ] || continue if command -v "$cli" >/dev/null 2>&1; then echo "harness-setup: $cli already installed — skipping" >&2 continue fi echo "harness-setup: installing $id ($cli)" >&2 # Reported as unavailable below rather than fatal. if ! bash -c "$install" >&2; then echo "harness-setup: WARNING $id failed to install — $cli will be unavailable" >&2 fi done < <(_harness_query --authoring-installs 2>/dev/null || true) } # Report which CLIs are usable. Non-zero when NONE are: one harness missing is survivable # (a worker uses the other), but zero means the container cannot author anything at all, # and that must stop setup rather than read as a couple of warnings. harness_report() { local id cli install ready=0 missing=0 while IFS=$'\t' read -r id cli install; do [ -n "$cli" ] || continue if command -v "$cli" >/dev/null 2>&1; then echo " $cli — ready" >&2 ready=$((ready + 1)) else echo " $cli — NOT AVAILABLE (install failed; see above)" >&2 missing=$((missing + 1)) fi done < <(_harness_query --authoring-installs 2>/dev/null || true) # A CLI on PATH with no key is worse than a missing one: it starts, then fails at the # first request with the harness's own auth error, which says nothing about setup. local id key_env base_url_env proxy_path while IFS=$'\t' read -r id key_env base_url_env proxy_path; do [ -n "$key_env" ] || continue if [ -z "${!key_env:-}" ]; then echo " $id — installed but NO CREDENTIALS: $key_env is unset." >&2 echo " Derived from ANTHROPIC_BASE_URL + ANTHROPIC_API_KEY; set both in .env." >&2 fi done < <(_harness_query --authoring-credentials 2>/dev/null || true) if [ "$ready" -eq 0 ]; then echo "harness-setup: FATAL — no agent CLI installed ($missing attempted)." >&2 echo "harness-setup: This container cannot author a task. Check the install" >&2 echo "harness-setup: output above: the CLIs download over the network, so a" >&2 echo "harness-setup: proxy, DNS or upstream change breaks every one at once." >&2 return 1 fi [ "$missing" -gt 0 ] && echo "harness-setup: $missing harness(es) unavailable; $ready usable" >&2 return 0 } # --- Explore launchers ------------------------------------------------------- # One `raccoon-explore-` per harness, aliased to its `cli`. harness_install_launchers() { local bin="$HOME/.local/bin" mkdir -p "$bin" # Read at launcher run time so the note stays a file, not a baked-in copy. local note_src="${HARNESS_TOOLSET_NOTE:-/workspace/scripts/toolset_note.md}" local agent_cli_dir="${AGENT_CLI_DIR:-/opt/agent-cli}" local id cli launch while IFS=$'\t' read -r id cli launch; do [ -n "$cli" ] && [ -n "$launch" ] || continue cat > "$bin/raccoon-explore-$cli" <&2 done < <(_harness_query --explore-launchers 2>/dev/null || true) } # Alias lines for ~/.bashrc. harness_alias_lines() { local id cli launch while IFS=$'\t' read -r id cli launch; do [ -n "$cli" ] && [ -n "$launch" ] || continue echo "alias $cli=\"raccoon-explore-$cli\"" done < <(_harness_query --explore-launchers 2>/dev/null || true) } # Write each harness's config file from the registry, replacing whatever was there. # # The file is OWNED, not merged: TOML has no way to return to the document root after a # table header, so appending or prepending around foreign content silently reparents # root-level keys into whichever table happens to precede them. Owning it also means a # registry change actually reaches a container that was already set up. harness_write_configs() { local id config_path blob target tmp while IFS=$'\t' read -r id config_path blob; do [ -n "$config_path" ] && [ -n "$blob" ] || continue # Guarded: a bare failing assignment exits the caller's `set -e` post-create with # no explanation. A path this cannot expand is one harness's problem, not the # container's. target=$(eval "printf '%s' \"$config_path\"") || { echo "harness-setup: WARNING $id config_path could not be expanded — skipping" >&2 continue } mkdir -p "$(dirname "$target")" tmp="$target.raccoon-tmp" # Expansion is strict: an unset var would otherwise be written through as the # literal ${VAR}, which surfaces much later as an unparseable value. if ! { echo "# Generated from harness-registry.toml — edits here are overwritten." printf '%s' "$blob" | base64 -d | python3 -c ' import os, re, sys text = sys.stdin.read() missing = sorted( {m.group(1) for m in re.finditer(r"\$\{(\w+)\}", text) if m.group(1) not in os.environ} ) if missing: sys.stderr.write("unset: " + ", ".join(missing) + "\n") raise SystemExit(1) sys.stdout.write(os.path.expandvars(text)) ' } > "$tmp"; then rm -f "$tmp" echo "harness-setup: WARNING $id config NOT written — a value it needs is unset." >&2 echo "harness-setup: run harness_setup_credentials first (harness_setup_all does)." >&2 continue fi mv "$tmp" "$target" echo "harness-setup: $id config -> $target" >&2 done < <(_harness_query --container-configs --surface "${RACCOON_SURFACE:-authoring}" 2>/dev/null || true) } # Link every available skill into each harness's skills_dir, for harnesses that declare one. # Both container layouts are covered: the explore container holds the snapshot skill under # plugins/, the authoring container holds the authoring skills under .claude/skills. Whichever # directories exist here are the ones this container has. harness_install_skills() { local sources="${RACCOON_SKILL_SOURCE_DIRS:-/workspace/plugins/create-snapshot/skills /workspace/.claude/skills}" local id dir target src skill name installed while IFS=$'\t' read -r id dir; do [ -n "$dir" ] || continue target=$(eval "printf '%s' \"$dir\"") || { echo "harness-setup: WARNING $id skills_dir could not be expanded — skipping" >&2 continue } mkdir -p "$target" installed=0 for src in $sources; do [ -d "$src" ] || continue for skill in "$src"/*/; do [ -f "$skill/SKILL.md" ] || continue name=$(basename "$skill") ln -sfn "${skill%/}" "$target/$name" installed=$((installed + 1)) done done echo "harness-setup: $id skills -> $target ($installed linked)" >&2 done < <(_harness_query --skills-dirs 2>/dev/null || true) } # Write the auth file for harnesses that read credentials from disk rather than $ENV. harness_write_auth() { local id auth_path key_env target key py py=$(_raccoon_python) || { echo "harness-setup: no python3.11+ with tomllib — skipping auth files" >&2 return 0 } while IFS=$'\t' read -r id auth_path key_env; do [ -n "$auth_path" ] && [ -n "$key_env" ] || continue key="${!key_env:-}" if [ -z "$key" ]; then echo "harness-setup: $key_env unset — skipping $id auth file" >&2 continue fi target=$(eval "printf '%s' \"$auth_path\"") || { echo "harness-setup: WARNING $id auth_path could not be expanded — skipping" >&2 continue } mkdir -p "$(dirname "$target")" # json.dumps, not printf: a key containing a quote or backslash would otherwise # produce a file the CLI cannot parse, and the failure would surface as an auth # error rather than a malformed file. RACCOON_AUTH_K="$key_env" RACCOON_AUTH_V="$key" "$py" -c 'import json, os, sys json.dump({os.environ["RACCOON_AUTH_K"]: os.environ["RACCOON_AUTH_V"]}, sys.stdout) sys.stdout.write("\n")' > "$target" chmod 600 "$target" echo "harness-setup: $id auth -> $target" >&2 done < <(_harness_query --auth-files 2>/dev/null || true) } # The lines that explain a setup failure are printed as it happens, and the devcontainer # CLI's own stack trace lands on top of them. Close with a banner so the worker has # something to look for, and something to send us. _harness_fatal_banner() { echo "" >&2 echo " ============================================================" >&2 echo " HARNESS SETUP FAILED — this container has no agent CLI." >&2 echo "" >&2 echo " The harness-setup: lines above say why. Anything the" >&2 echo " devcontainer prints after this is a consequence, not the" >&2 echo " cause; send us the harness-setup: lines." >&2 echo " ============================================================" >&2 echo "" >&2 } harness_setup_all() { harness_preflight || { _harness_fatal_banner; return 1; } harness_setup_credentials harness_write_auth harness_install_clis harness_write_configs harness_install_skills # Launchers are NOT installed here. They are an Explore concern (that container aliases # `claude`/`codex` to them), and it passes its own AGENT_CLI_DIR — installing them here # too wrote every launcher twice, the first time with the wrong editor path, and left an # unused one in the authoring container. echo "harness-setup: authoring harnesses" >&2 harness_report || { _harness_fatal_banner; return 1; } }