molecule-ai-workspace-templ.../scripts/load-workspace-config.sh
Hongming Wang 43c2569faa fix(start.sh): read provider/model from /configs/config.yaml (Option B PR-4)
Closes the gap where CP user-data (PR-3, task #197) writes
runtime_config.{model,provider} into /configs/config.yaml but start.sh
only reads HERMES_DEFAULT_MODEL / HERMES_INFERENCE_PROVIDER env vars
that CP doesn't set. Result: every CP-provisioned hermes workspace
booted with the built-in `nousresearch/hermes-4-70b` default and
500'd at first prompt with "No LLM provider configured" — visible
in the 2026-04-30 hongmingwang tenant screenshots.

New scripts/load-workspace-config.sh, sourced by start.sh before the
existing DEFAULT_MODEL/PROVIDER derivation. Reads /configs/config.yaml
via python3 + PyYAML and exports HERMES_DEFAULT_MODEL +
HERMES_INFERENCE_PROVIDER if they're not already set.

Precedence (highest to lowest):
  1. HERMES_* env vars (operator override via workspace secrets)
  2. /configs/config.yaml runtime_config.{model,provider} (canvas UI)
  3. start.sh hard-coded fallback (nousresearch/hermes-4-70b)

Resilience:
  - Missing config.yaml → silent skip (dev containers)
  - Malformed YAML → silent skip (don't kill boot)
  - python3 missing → silent skip
  - PyYAML missing → silent skip
  - Empty/non-dict runtime_config → silent skip

Tests: scripts/test-load-workspace-config.sh — 11 cases covering all
silent-skip paths + happy paths + operator override + non-string scalar
coercion. Existing scripts/test-derive-provider.sh (12 cases) re-verified.

Wires shell tests into CI via a new shell-tests job — those tests
weren't running anywhere before, opportunistically closes that gap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 23:52:00 -07:00

119 lines
4.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# load-workspace-config.sh — bridge the workspace-level /configs/config.yaml
# (written by molecule-controlplane user-data per task #197) into the
# hermes-specific HERMES_DEFAULT_MODEL / HERMES_INFERENCE_PROVIDER env
# vars that start.sh + derive-provider.sh consume.
#
# Why this exists: PR-3 in the Option B series taught CP to write
# `runtime_config.model` and `runtime_config.provider` into
# /configs/config.yaml at provision time so the canvas Config tab can
# round-trip the operator's pick. start.sh used to only read the
# HERMES_* env vars, which CP doesn't set, so the config.yaml fields
# were silently ignored — every workspace booted with the built-in
# `nousresearch/hermes-4-70b` default and 500'd at first prompt with
# "No LLM provider configured" (visible in the 2026-04-30 hongmingwang
# tenant screenshots).
#
# Precedence (highest to lowest):
# 1. HERMES_DEFAULT_MODEL / HERMES_INFERENCE_PROVIDER env vars
# (operator override via workspace secrets — they win)
# 2. /configs/config.yaml runtime_config.{model,provider}
# (canvas Config tab — set via UI, written by CP user-data)
# 3. start.sh's hard-coded fallback (nousresearch/hermes-4-70b)
#
# Contract:
# Reads: /configs/config.yaml (or $MOLECULE_CONFIG_PATH/config.yaml)
# $HERMES_DEFAULT_MODEL, $HERMES_INFERENCE_PROVIDER
# Writes: HERMES_DEFAULT_MODEL (only if unset and config.yaml has it)
# HERMES_INFERENCE_PROVIDER (only if unset and config.yaml has it)
#
# Failure modes (silent — never blocks boot):
# - /configs/config.yaml doesn't exist → no-op
# - python3 not on PATH → no-op (start.sh's fallback still works)
# - PyYAML not importable → no-op
# - Malformed YAML → no-op
# - runtime_config absent or not a dict → no-op
#
# Resilience over completeness — same philosophy as the claude-code
# adapter's _load_providers fallback. A workspace with a missing or
# malformed config.yaml should still boot and fall through to the
# env-var/built-in defaults instead of dying at this step.
# Source-only safety: don't `set -e` here — this script is `.`-sourced
# by start.sh which already has its own set -euo pipefail. Errors here
# would otherwise kill the parent shell.
_lwc_config_path="${MOLECULE_CONFIG_PATH:-/configs}/config.yaml"
# Skip silently if the file isn't there. Workspaces booted before PR-3
# rolled out, or non-CP-provisioned dev containers, won't have it.
if [ ! -f "$_lwc_config_path" ]; then
unset _lwc_config_path
return 0 2>/dev/null || true
fi
# Skip if python3 is missing — start.sh's existing logic still works.
if ! command -v python3 >/dev/null 2>&1; then
unset _lwc_config_path
return 0 2>/dev/null || true
fi
# Single python invocation extracts both fields and prints `key=value`
# lines. Importing yaml inside try/except so a runtime missing PyYAML
# (shouldn't happen in production — molecule-ai-workspace-runtime
# brings it transitively — but defensive against dev images) yields
# zero output instead of an error. Any exception → empty output → the
# read loop below sets nothing and start.sh keeps its fallbacks.
_lwc_extracted=$(MOLECULE_CONFIG_FILE="$_lwc_config_path" python3 - <<'PYEOF' 2>/dev/null
import os, sys
try:
import yaml
except ImportError:
sys.exit(0)
try:
with open(os.environ["MOLECULE_CONFIG_FILE"]) as f:
data = yaml.safe_load(f) or {}
except Exception:
sys.exit(0)
rc = data.get("runtime_config") or {}
if not isinstance(rc, dict):
sys.exit(0)
# Print one key=value per line; empty values omitted so the bash side
# can use [ -n "$value" ] without ambiguity. Values are stringified so
# non-string YAML scalars (e.g. integers) don't break the read loop.
for env_name, yaml_key in (
("HERMES_DEFAULT_MODEL", "model"),
("HERMES_INFERENCE_PROVIDER", "provider"),
):
v = rc.get(yaml_key)
if v is None:
continue
s = str(v).strip()
if s:
print(f"{env_name}={s}")
PYEOF
)
# Apply the extracted values, but only when the corresponding env var
# isn't already set — operator override (env-var pre-set, e.g. via
# workspace secrets) must beat the YAML.
while IFS='=' read -r _lwc_key _lwc_value; do
[ -z "$_lwc_key" ] && continue
case "$_lwc_key" in
HERMES_DEFAULT_MODEL)
if [ -z "${HERMES_DEFAULT_MODEL:-}" ] && [ -n "$_lwc_value" ]; then
export HERMES_DEFAULT_MODEL="$_lwc_value"
echo "[load-workspace-config] HERMES_DEFAULT_MODEL=$_lwc_value (from $_lwc_config_path)" >&2
fi
;;
HERMES_INFERENCE_PROVIDER)
if [ -z "${HERMES_INFERENCE_PROVIDER:-}" ] && [ -n "$_lwc_value" ]; then
export HERMES_INFERENCE_PROVIDER="$_lwc_value"
echo "[load-workspace-config] HERMES_INFERENCE_PROVIDER=$_lwc_value (from $_lwc_config_path)" >&2
fi
;;
esac
done <<<"$_lwc_extracted"
unset _lwc_config_path _lwc_extracted _lwc_key _lwc_value