#!/bin/bash # Read the harness registry and derive per-harness credentials from it. # # Source it — the whole point is exporting into the caller's environment, which a subshell # would lose: # # HARNESS_SCRIPTS_DIR=/workspace/scripts . /workspace/scripts/lib/harness-credentials.sh # harness_setup_credentials # # Three callers: `harbor-run`, which needs only this; `refresh-harness-auth`, which # re-derives and rewrites the auth files before an interactive launch; and # `setup-harnesses.sh`, which sources it and adds installs, config writing and launchers # on top. # # No -e here — this file is SOURCED, and shell options belong to the caller's shell (both # post-creates run with -e). An unguarded failure below therefore aborts container # creation, which is why every failure site is individually guarded rather than relying on # this line. set -uo pipefail _HARNESS_REGISTRY_DIR="${HARNESS_SCRIPTS_DIR:-/workspace/scripts}" # The registry is read with tomllib (stdlib from 3.11), and `python3` is not always new # enough — macOS ships 3.9, and a container may symlink an older managed interpreter. Pick # the first one that can actually import it rather than assuming. _raccoon_python() { local p for p in "${RACCOON_PYTHON:-}" python3 python3.13 python3.12 python3.11; do [ -n "$p" ] || continue command -v "$p" >/dev/null 2>&1 || continue if "$p" -c "import tomllib" >/dev/null 2>&1; then printf '%s' "$p" return 0 fi done return 1 } _harness_query() { local py py=$(_raccoon_python) || return 1 "$py" "$_HARNESS_REGISTRY_DIR/resolve_harness.py" "$@" } # Drop every whitespace character from a value read out of .env. A Windows-saved .env leaves a # \r on each value, which reaches the proxy as a 401; no key or base URL legitimately contains # whitespace anywhere, so deleting rather than trimming needs no cases. _harness_trim() { local out # Fall back to the raw value: a trim that cannot run must never turn a working key into an # empty one, which is what an unavailable `tr` would otherwise do to every caller. out="$(printf '%s' "$1" | tr -d '[:space:]' 2>/dev/null)" || out="$1" printf '%s' "${out:-$1}" } # The proxy root: the worker's ANTHROPIC_BASE_URL minus its provider path. _harness_proxy_root() { local base_url base_url="$(_harness_trim "${ANTHROPIC_BASE_URL:-}")" [ -n "$base_url" ] || return 1 base_url="${base_url%"${base_url##*[!/]}"}" # ".../llm_proxy/projects//anthropic" -> ".../llm_proxy/projects/", so each # harness's proxy_path composes onto the project route. Requires a path to strip: a base # URL that is a bare host with no path — a provider's own API root rather than the # proxy — would yield "https:/", handed to codex as a base URL and failing obscurely. case "${base_url#*://}" in */*) printf '%s' "${base_url%/*}" ;; *) return 2 ;; esac } harness_setup_credentials() { # `|| rc=$?` and not a bare assignment: this is sourced into a `set -e` shell (see the # note at the top), and a bare failing assignment would exit the caller's post-create # outright — silently, since the failure paths below are what do the explaining. local root rc=0 root="$(_harness_proxy_root)" || rc=$? if [ "$rc" -ne 0 ]; then if [ "$rc" -eq 2 ]; then echo "harness-setup: ANTHROPIC_BASE_URL (${ANTHROPIC_BASE_URL:-}) has no provider" >&2 echo "harness-setup: path, so it is not the proxy URL other harnesses derive their" >&2 echo "harness-setup: credentials from. claude will work; codex will not be" >&2 echo "harness-setup: authenticated. Use the base URL you were given." >&2 else echo "harness-setup: ANTHROPIC_BASE_URL unset — skipping credential derivation" >&2 fi return 0 fi ANTHROPIC_BASE_URL="$(_harness_trim "${ANTHROPIC_BASE_URL:-}")" export ANTHROPIC_BASE_URL local key key="$(_harness_trim "${ANTHROPIC_API_KEY:-}")" if [ -z "$key" ]; then echo "harness-setup: ANTHROPIC_API_KEY unset — skipping credential derivation" >&2 return 0 fi # harbor-run sources .env itself and passes ANTHROPIC_* through to the trial sandbox, so # cleaning only the derived per-harness copies would leave a claude trial carrying the CR. export ANTHROPIC_API_KEY="$key" 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 # ${!name} is an indirect expansion. Only set when empty: an explicit key wins. if [ -z "${!key_env:-}" ]; then export "$key_env=$key" fi if [ -n "$base_url_env" ] && [ -n "$proxy_path" ] && [ -z "${!base_url_env:-}" ]; then export "$base_url_env=$root/$proxy_path" fi echo "harness-setup: $id credentials ready ($key_env, ${base_url_env:-no base url})" >&2 done < <(_harness_query --authoring-credentials 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 # Last mile: an explicit OPENAI_API_KEY bypasses the derivation above, so trim here # too — this is the value that reaches the file the harness authenticates with. key="$(_harness_trim "${!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")" || { echo "harness-setup: WARNING $id auth dir not creatable — skipping $target" >&2 continue } # 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. # 0600 tmp + rename, never a redirect onto the target: a redirect truncates the live # file first, so a write dying mid-flight leaves codex an EMPTY auth.json. if ! RACCOON_AUTH_K="$key_env" RACCOON_AUTH_V="$key" RACCOON_AUTH_TARGET="$target" \ "$py" -c 'import json, os target = os.environ["RACCOON_AUTH_TARGET"] tmp = target + ".raccoon-tmp." + str(os.getpid()) try: with os.fdopen(os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600), "w") as fh: json.dump({os.environ["RACCOON_AUTH_K"]: os.environ["RACCOON_AUTH_V"]}, fh) fh.write("\n") os.replace(tmp, target) except OSError: try: os.unlink(tmp) except OSError: pass raise SystemExit(1) '; then echo "harness-setup: WARNING $id auth file NOT written — $target unwritable." >&2 echo "harness-setup: the key already on disk (if any) is left untouched." >&2 continue fi echo "harness-setup: $id auth -> $target" >&2 done < <(_harness_query --auth-files 2>/dev/null || true) } # Re-set just the root keys of a harness's config file (codex's `openai_base_url`), # leaving every other line — the explore surface's [hooks] table included — untouched. harness_refresh_config_keys() { local id config_path blob target py py=$(_raccoon_python) || return 0 # The surface only decides what a CREATE writes. An update takes the root keys off the # front of the same blob, so a surface's tables survive byte-for-byte either way. while IFS=$'\t' read -r id config_path blob; do [ -n "$config_path" ] && [ -n "$blob" ] || continue target=$(eval "printf '%s' \"$config_path\"") || continue mkdir -p "$(dirname "$target")" || continue if printf '%s' "$blob" | base64 -d | RACCOON_CONFIG_TARGET="$target" "$py" -c ' import os, re, sys, tomllib HEADER = "# Generated from harness-registry.toml — edits here are overwritten." target = os.environ["RACCOON_CONFIG_TARGET"] text = sys.stdin.read() # Empty counts as unresolved: writing an empty base URL would break a container whose # config is currently right, which is the one thing this must never do. if [m for m in re.finditer(r"\$\{(\w+)\}", text) if not os.environ.get(m.group(1))]: raise SystemExit(1) text = os.path.expandvars(text) wanted = [] for line in text.splitlines(): if line.lstrip().startswith("["): break m = re.match(r"\s*([A-Za-z0-9_-]+)\s*=", line) if m: wanted.append((m.group(1), line.rstrip())) if not wanted: raise SystemExit(0) mode = None if os.path.exists(target): try: with open(target, encoding="utf-8") as fh: lines = fh.read().splitlines() mode = os.stat(target).st_mode & 0o777 except OSError: raise SystemExit(1) # Everything from the first table header on belongs to a table. A key appended after # one is reparented into it, so both the search and the insert stay above the line. root_end = next((i for i, l in enumerate(lines) if l.lstrip().startswith("[")), len(lines)) changed = False for key, line in wanted: # The quoted spelling is the same key: replacing it beats adding a duplicate. pat = re.compile(r"\s*\"?" + re.escape(key) + r"\"?\s*=") at = next((i for i in range(root_end) if pat.match(lines[i])), None) if at is None: if root_end < len(lines) and lines[root_end].strip(): lines.insert(root_end, "") lines.insert(root_end, line) root_end += 1 changed = True elif lines[at] != line: lines[at] = line changed = True if not changed: raise SystemExit(0) out = "\n".join(lines).rstrip("\n") + "\n" else: # No file means container-create could not write one, so write what it would have: # on the explore surface that is the capture hooks too, not just the root keys. out = HEADER + "\n" + text try: doc = tomllib.loads(out) except tomllib.TOMLDecodeError: raise SystemExit(1) # Parsing is not enough: a line edit can land inside a multi-line value, which still # parses while leaving the key unset. Require every key to have reached the root. if doc != {**doc, **tomllib.loads("\n".join(line for _, line in wanted))}: raise SystemExit(1) # Pid-suffixed: two launches at once must not write the same scratch path. tmp = target + ".raccoon-tmp." + str(os.getpid()) try: with open(tmp, "w", encoding="utf-8") as fh: fh.write(out) if mode is not None: os.chmod(tmp, mode) os.replace(tmp, target) except OSError: try: os.unlink(tmp) except OSError: pass raise SystemExit(1) '; then echo "harness-setup: $id config keys refreshed -> $target" >&2 fi done < <(_harness_query --container-configs --surface "${RACCOON_SURFACE:-authoring}" 2>/dev/null || true) }