236 lines
7.0 KiB
Bash
Executable File
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
|