#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" POLICY_CONFIG="$ROOT_DIR/config/copilot-policy-guard.config.json" BUDGET_CONFIG="$ROOT_DIR/config/model-budget-guard.config.json" AUTH_CONFIG="$ROOT_DIR/config/copilot-auth-watchdog.config.json" MODEL_PROFILES_CONFIG="$ROOT_DIR/config/model-profiles.config.json" MODEL_SCHEDULE_CONFIG="$ROOT_DIR/config/model-schedule.config.json" if ! command -v openclaw >/dev/null 2>&1; then echo "[configure-guardrails] openclaw CLI is required" >&2 exit 1 fi if ! command -v jq >/dev/null 2>&1; then echo "[configure-guardrails] jq is required" >&2 exit 1 fi if [[ ! -f "$MODEL_PROFILES_CONFIG" ]]; then cat > "$MODEL_PROFILES_CONFIG" <<'JSON' { "profiles": { "paid": { "description": "Work-hours higher-quality Copilot model", "primary": "", "fallbacks": [] }, "free": { "description": "Off-hours low-cost/free Copilot model stack", "primary": "", "fallbacks": [] } } } JSON fi if [[ ! -f "$MODEL_SCHEDULE_CONFIG" ]]; then cat > "$MODEL_SCHEDULE_CONFIG" <<'JSON' { "enabled": true, "dayProfile": "paid", "nightProfile": "free", "dayStartHour": 8, "nightStartHour": 18, "sessionKey": "agent:main:main", "switchScript": "./scripts/model_profile_switch.sh", "stateFile": "~/.openclaw/model-schedule-state.json" } JSON fi is_cheap_model() { local m m="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" [[ "$m" =~ (haiku|mini|flash|fast|nano|small|lite|economy) ]] } prompt_for_paid_model() { local default_model="$1" shift local -a candidates=("$@") if [[ ${#candidates[@]} -eq 0 ]]; then echo "$default_model" return 0 fi echo "" >&2 echo "[configure-guardrails] Choose paid profile primary model (non-free candidates):" >&2 for i in "${!candidates[@]}"; do local n=$((i + 1)) local marker="" if [[ "${candidates[$i]}" == "$default_model" ]]; then marker=" (default)" fi printf " %d) %s%s\n" "$n" "${candidates[$i]}" "$marker" >&2 done printf "Select number [default=%s]: " "$default_model" >&2 local choice="" if ! IFS= read -r choice; then echo "" >&2 echo "$default_model" return 0 fi if [[ -z "$choice" ]]; then echo "$default_model" return 0 fi if [[ "$choice" =~ ^[0-9]+$ ]]; then local idx=$((choice - 1)) if (( idx >= 0 && idx < ${#candidates[@]} )); then echo "${candidates[$idx]}" return 0 fi fi echo "[configure-guardrails] Invalid selection '$choice'; using default: $default_model" >&2 echo "$default_model" } copilot_models=() while IFS= read -r model; do [[ -z "$model" ]] && continue copilot_models+=("$model") done < <( openclaw models list 2>/dev/null \ | awk 'NR>1 {print $1}' \ | grep '^github-copilot/' \ | awk '!seen[$0]++' ) if [[ ${#copilot_models[@]} -eq 0 ]]; then cat >&2 <&2 if [[ -t 0 && -t 1 ]]; then interactive_picker="true" fi ;; esac if [[ "$interactive_picker" == "true" && ( ! -t 0 || ! -t 1 ) ]]; then echo "[configure-guardrails] PROMPT_FOR_PAID_MODEL enabled but no interactive TTY; falling back to auto selection." >&2 interactive_picker="false" fi primary_model="" for m in "${copilot_models[@]}"; do if is_cheap_model "$m"; then primary_model="$m" break fi done if [[ -z "$primary_model" ]]; then primary_model="${copilot_models[0]}" fi # Build free/low-cost fallback candidates (excluding primary) declare -a cheap_candidates=() for m in "${copilot_models[@]}"; do [[ "$m" == "$primary_model" ]] && continue if is_cheap_model "$m"; then cheap_candidates+=("$m") fi done # If none matched cheap pattern, pick up to 2 non-primary models as fallback if [[ ${#cheap_candidates[@]} -eq 0 ]]; then for m in "${copilot_models[@]}"; do [[ "$m" == "$primary_model" ]] && continue cheap_candidates+=("$m") [[ ${#cheap_candidates[@]} -ge 2 ]] && break done fi # Keep at most 2 fallbacks if [[ ${#cheap_candidates[@]} -gt 2 ]]; then cheap_candidates=("${cheap_candidates[@]:0:2}") fi # High models: anything non-cheap and non-primary declare -a high_models=() for m in "${copilot_models[@]}"; do [[ "$m" == "$primary_model" ]] && continue if ! is_cheap_model "$m"; then high_models+=("$m") fi done # Ensure at least one high model if multiple exist if [[ ${#high_models[@]} -eq 0 && ${#copilot_models[@]} -gt 1 ]]; then for m in "${copilot_models[@]}"; do [[ "$m" == "$primary_model" ]] && continue high_models+=("$m") break done fi paid_model="" if [[ ${#high_models[@]} -gt 0 ]]; then paid_model="${high_models[0]}" fi if [[ -z "$paid_model" ]]; then for m in "${copilot_models[@]}"; do [[ "$m" == "$primary_model" ]] && continue paid_model="$m" break done fi if [[ -z "$paid_model" ]]; then paid_model="$primary_model" fi if [[ "$interactive_picker" == "true" && ${#high_models[@]} -gt 0 ]]; then paid_model="$(prompt_for_paid_model "$paid_model" "${high_models[@]}")" echo "[configure-guardrails] selected paid model: $paid_model" fi # free profile = lowest-cost primary + lowest-cost fallbacks declare -a free_fallbacks=("${cheap_candidates[@]}") # paid profile = strongest available model + cheap fallback chain declare -a paid_fallbacks=() if [[ "$primary_model" != "$paid_model" ]]; then paid_fallbacks+=("$primary_model") fi for m in "${cheap_candidates[@]}"; do [[ "$m" == "$paid_model" ]] && continue skip="false" for cur in "${paid_fallbacks[@]}"; do if [[ "$cur" == "$m" ]]; then skip="true" break fi done [[ "$skip" == "true" ]] && continue paid_fallbacks+=("$m") done active_profile="paid" current_hour="$(date +%H)" current_hour=$((10#$current_hour)) if (( current_hour >= 18 || current_hour < 8 )); then active_profile="free" fi active_primary="$paid_model" active_fallbacks=("${paid_fallbacks[@]}") if [[ "$active_profile" == "free" ]]; then active_primary="$primary_model" active_fallbacks=("${free_fallbacks[@]}") fi fallbacks_json="$(printf '%s\n' "${active_fallbacks[@]}" | jq -R . | jq -s .)" high_json="$(printf '%s\n' "${high_models[@]}" | jq -R . | jq -s .)" free_fallbacks_json="$(printf '%s\n' "${free_fallbacks[@]}" | jq -R . | jq -s .)" paid_fallbacks_json="$(printf '%s\n' "${paid_fallbacks[@]}" | jq -R . | jq -s .)" # Update configs jq \ --arg primary "$active_primary" \ --argjson fallbacks "$fallbacks_json" \ '.enabled=true | .autoFix=true | .desiredPrimaryModel=$primary | .enforceFallbackAllowlist=true | .allowedFallbacks=$fallbacks | .providerPolicy["github-copilot"]=true | .providerPolicy["openai"]=false | .providerPolicy["anthropic"]=false | .providerPolicy["openrouter"]=false' \ "$POLICY_CONFIG" > "$POLICY_CONFIG.tmp" && mv "$POLICY_CONFIG.tmp" "$POLICY_CONFIG" jq \ --arg low "$primary_model" \ --argjson highs "$high_json" \ '.enabled=true | .lowModel=$low | .highModels=$highs | .warnAfterMinutes=2 | .revertAfterMinutes=45 | .minWarnIntervalMinutes=20' \ "$BUDGET_CONFIG" > "$BUDGET_CONFIG.tmp" && mv "$BUDGET_CONFIG.tmp" "$BUDGET_CONFIG" jq '.enabled=true | .minAlertIntervalMinutes=30 | .checkOpenClawModelStatus=true' \ "$AUTH_CONFIG" > "$AUTH_CONFIG.tmp" && mv "$AUTH_CONFIG.tmp" "$AUTH_CONFIG" jq \ --arg paid "$paid_model" \ --arg free "$primary_model" \ --argjson paid_fallbacks "$paid_fallbacks_json" \ --argjson free_fallbacks "$free_fallbacks_json" \ '.profiles.paid.description="Work-hours higher-quality Copilot model" | .profiles.paid.primary=$paid | .profiles.paid.fallbacks=$paid_fallbacks | .profiles.paid.providerPolicy["github-copilot"]=true | .profiles.paid.providerPolicy["openai"]=false | .profiles.paid.providerPolicy["anthropic"]=false | .profiles.paid.providerPolicy["openrouter"]=false | .profiles.free.description="Off-hours low-cost/free Copilot model stack" | .profiles.free.primary=$free | .profiles.free.fallbacks=$free_fallbacks | .profiles.free.providerPolicy["github-copilot"]=true | .profiles.free.providerPolicy["openai"]=false | .profiles.free.providerPolicy["anthropic"]=false | .profiles.free.providerPolicy["openrouter"]=false' \ "$MODEL_PROFILES_CONFIG" > "$MODEL_PROFILES_CONFIG.tmp" && mv "$MODEL_PROFILES_CONFIG.tmp" "$MODEL_PROFILES_CONFIG" jq \ '.enabled=true | .dayProfile="paid" | .nightProfile="free" | .dayStartHour=(.dayStartHour // 8) | .nightStartHour=(.nightStartHour // 18) | .sessionKey=(.sessionKey // "agent:main:main") | .switchScript=(.switchScript // "./scripts/model_profile_switch.sh") | .stateFile=(.stateFile // "~/.openclaw/model-schedule-state.json")' \ "$MODEL_SCHEDULE_CONFIG" > "$MODEL_SCHEDULE_CONFIG.tmp" && mv "$MODEL_SCHEDULE_CONFIG.tmp" "$MODEL_SCHEDULE_CONFIG" # Apply live policy/routing on target machine openclaw models set "$active_primary" >/dev/null openclaw models fallbacks clear >/dev/null for fb in "${active_fallbacks[@]}"; do openclaw models fallbacks add "$fb" >/dev/null done openclaw config set --json providers.github-copilot.enabled true >/dev/null openclaw config set --json providers.openai.enabled false >/dev/null openclaw config set --json providers.anthropic.enabled false >/dev/null openclaw config set --json providers.openrouter.enabled false >/dev/null echo "Configured Copilot guardrails defaults:" echo " active_profile_now: $active_profile" echo " active_primary_model: $active_primary" if [[ ${#active_fallbacks[@]} -gt 0 ]]; then echo " active_fallbacks: ${active_fallbacks[*]}" else echo " active_fallbacks: (none)" fi echo " paid_profile_primary: $paid_model" if [[ ${#paid_fallbacks[@]} -gt 0 ]]; then echo " paid_profile_fallbacks: ${paid_fallbacks[*]}" else echo " paid_profile_fallbacks: (none)" fi echo " free_profile_primary: $primary_model" if [[ ${#free_fallbacks[@]} -gt 0 ]]; then echo " free_profile_fallbacks: ${free_fallbacks[*]}" else echo " free_profile_fallbacks: (none)" fi echo "" echo "Applied live routing/provider policy and updated config files:" echo " $POLICY_CONFIG" echo " $BUDGET_CONFIG" echo " $AUTH_CONFIG" echo " $MODEL_PROFILES_CONFIG" echo " $MODEL_SCHEDULE_CONFIG"