OpenClaw-Setup/openclaw-setup-copilot/scripts/copilot_policy_guard.sh

236 lines
7.0 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
CONFIG_PATH="${POLICY_GUARD_CONFIG:-$ROOT_DIR/config/copilot-policy-guard.config.json}"
if ! command -v jq >/dev/null 2>&1; then
echo "[copilot-policy-guard] jq is required" >&2
exit 1
fi
if ! command -v openclaw >/dev/null 2>&1; then
echo "[copilot-policy-guard] openclaw CLI is required" >&2
exit 1
fi
expand_tilde() {
local p="$1"
if [[ "$p" == "~" ]]; then
echo "$HOME"
elif [[ "$p" == "~/"* ]]; then
echo "$HOME/${p#~/}"
else
echo "$p"
fi
}
now_ms() {
python3 - <<'PY'
import time
print(int(time.time()*1000))
PY
}
send_notice() {
local text="$1"
local last_channel="$2"
local last_to="$3"
if [[ -z "$last_channel" || -z "$last_to" ]]; then
echo "[copilot-policy-guard] notice skipped (missing channel/target): $text" >&2
return 0
fi
local target="$last_to"
if [[ "$target" == *:* ]]; then
target="${target#*:}"
fi
openclaw message send \
--channel "$last_channel" \
--target "$target" \
--message "$text" \
>/dev/null 2>&1 || true
}
if [[ ! -f "$CONFIG_PATH" ]]; then
echo "[copilot-policy-guard] missing config: $CONFIG_PATH" >&2
exit 1
fi
enabled="$(jq -r '.enabled // true' "$CONFIG_PATH")"
if [[ "$enabled" != "true" ]]; then
exit 0
fi
auto_fix="$(jq -r '.autoFix // true' "$CONFIG_PATH")"
session_key="$(jq -r '.sessionKey // "agent:main:main"' "$CONFIG_PATH")"
alert_interval_min="$(jq -r '.minAlertIntervalMinutes // 20' "$CONFIG_PATH")"
state_file_raw="$(jq -r '.stateFile // "~/.openclaw/copilot-policy-guard-state.json"' "$CONFIG_PATH")"
state_file="$(expand_tilde "$state_file_raw")"
required_primary_prefix="$(jq -r '.requiredPrimaryPrefix // "github-copilot/"' "$CONFIG_PATH")"
required_fallback_prefix="$(jq -r '.requiredFallbackPrefix // "github-copilot/"' "$CONFIG_PATH")"
desired_primary_model="$(jq -r '.desiredPrimaryModel // empty' "$CONFIG_PATH")"
enforce_allowlist="$(jq -r '.enforceFallbackAllowlist // false' "$CONFIG_PATH")"
state_dir="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}"
sessions_file="$state_dir/agents/main/sessions/sessions.json"
last_channel=""
last_to=""
if [[ -r "$sessions_file" ]]; then
session_json="$(jq -c --arg key "$session_key" '.[$key] // {}' "$sessions_file" 2>/dev/null || true)"
if [[ -n "$session_json" && "$session_json" != "{}" ]]; then
last_channel="$(jq -r '.lastChannel // empty' <<<"$session_json")"
last_to="$(jq -r '.lastTo // empty' <<<"$session_json")"
fi
fi
declare -a issues=()
declare -a fixes=()
models_json="$(openclaw models status --json 2>/dev/null || true)"
if [[ -z "$models_json" ]]; then
issues+=("openclaw models status --json failed")
else
default_model="$(jq -r '.defaultModel // empty' <<<"$models_json")"
if [[ -n "$default_model" && "$default_model" != ${required_primary_prefix}* ]]; then
issues+=("primary model is not Copilot: $default_model")
if [[ "$auto_fix" == "true" && -n "$desired_primary_model" ]]; then
if openclaw models set "$desired_primary_model" >/dev/null 2>&1; then
fixes+=("set primary model to $desired_primary_model")
else
issues+=("failed to set primary model to $desired_primary_model")
fi
fi
fi
fallbacks=()
while IFS= read -r fb; do
[[ -z "$fb" ]] && continue
fallbacks+=("$fb")
done < <(jq -r '.fallbacks[]? // empty' <<<"$models_json")
bad_prefix=0
bad_allowlist=0
for fb in "${fallbacks[@]}"; do
if [[ "$fb" != ${required_fallback_prefix}* ]]; then
bad_prefix=1
issues+=("fallback is not Copilot: $fb")
fi
done
if [[ "$enforce_allowlist" == "true" ]]; then
allowed_fallbacks=()
while IFS= read -r af; do
[[ -z "$af" ]] && continue
allowed_fallbacks+=("$af")
done < <(jq -r '.allowedFallbacks[]? // empty' "$CONFIG_PATH")
if [[ ${#allowed_fallbacks[@]} -gt 0 ]]; then
for fb in "${fallbacks[@]}"; do
found=0
for af in "${allowed_fallbacks[@]}"; do
if [[ "$fb" == "$af" ]]; then
found=1
break
fi
done
if [[ $found -eq 0 ]]; then
bad_allowlist=1
issues+=("fallback not in allowlist: $fb")
fi
done
fi
fi
if [[ "$auto_fix" == "true" && ( $bad_prefix -eq 1 || $bad_allowlist -eq 1 ) ]]; then
desired_fallbacks=()
if [[ "$enforce_allowlist" == "true" ]]; then
while IFS= read -r af; do
[[ -n "$af" ]] && desired_fallbacks+=("$af")
done < <(jq -r '.allowedFallbacks[]? // empty' "$CONFIG_PATH")
fi
if [[ ${#desired_fallbacks[@]} -eq 0 ]]; then
for fb in "${fallbacks[@]}"; do
if [[ "$fb" == ${required_fallback_prefix}* ]]; then
desired_fallbacks+=("$fb")
fi
done
fi
if openclaw models fallbacks clear >/dev/null 2>&1; then
fixes+=("cleared fallback list")
for fb in "${desired_fallbacks[@]}"; do
if openclaw models fallbacks add "$fb" >/dev/null 2>&1; then
fixes+=("added fallback $fb")
else
issues+=("failed to add fallback $fb")
fi
done
else
issues+=("failed to clear fallback list")
fi
fi
fi
while IFS='=' read -r provider desired; do
[[ -z "$provider" ]] && continue
desired_bool="false"
if [[ "$desired" == "true" ]]; then
desired_bool="true"
fi
current="$(openclaw config get "providers.${provider}.enabled" 2>/dev/null || true)"
current_trim="$(printf '%s' "$current" | tr -d '[:space:]')"
if [[ "$current_trim" != "$desired_bool" ]]; then
issues+=("provider policy drift: providers.${provider}.enabled expected ${desired_bool}, got ${current_trim:-unset}")
if [[ "$auto_fix" == "true" ]]; then
if openclaw config set --json "providers.${provider}.enabled" "$desired_bool" >/dev/null 2>&1; then
fixes+=("set providers.${provider}.enabled=${desired_bool}")
else
issues+=("failed to set providers.${provider}.enabled=${desired_bool}")
fi
fi
fi
done < <(jq -r '.providerPolicy // {} | to_entries[] | "\(.key)=\(.value)"' "$CONFIG_PATH")
if [[ ${#issues[@]} -eq 0 && ${#fixes[@]} -eq 0 ]]; then
exit 0
fi
mkdir -p "$(dirname "$state_file")"
if [[ ! -f "$state_file" ]]; then
printf '{}\n' > "$state_file"
fi
state_json="$(cat "$state_file")"
last_alert_at="$(jq -r --arg key "$session_key" '.[$key].lastAlertAt // 0' <<<"$state_json")"
if [[ ! "$last_alert_at" =~ ^[0-9]+$ ]]; then
last_alert_at=0
fi
now="$(now_ms)"
interval_ms=$((alert_interval_min * 60 * 1000))
should_alert="false"
if (( now - last_alert_at >= interval_ms )); then
should_alert="true"
fi
summary=""
if [[ ${#fixes[@]} -gt 0 ]]; then
summary="Copilot policy guard auto-fixed: ${fixes[*]}"
else
summary="Copilot policy drift detected: ${issues[*]}"
fi
echo "[copilot-policy-guard] $summary"
if [[ "$should_alert" == "true" ]]; then
send_notice "$summary" "$last_channel" "$last_to"
state_json="$(jq --arg key "$session_key" --argjson now "$now" '.[$key].lastAlertAt=$now' <<<"$state_json")"
printf '%s\n' "$state_json" > "$state_file"
fi