Standard production layout: the OKR app (was nested under AINative_OKR_CASAN5/) is now
the repository root. No more wrapper directory.
- Promote AINative_OKR_CASAN5/* -> repo root (backend/ frontend/ packages/ apps/
.specify/ docs/ infra/ nginx/ scripts/ + configs). Merge tool dirs: .gitea (kept the
active deploy ci.yml, added harness-ci.yml + runbooks), .claude (agents/commands +
launch.json), .github moved up.
- Remove redundant: 00_SUBMISSION_PACKAGE, scattered root notes (FPT_CASAN_Full.md,
tu-tuong-casan.md, casan-tu-sinh..., casan_harness_assessment.md, source-review...,
README_CASAN5_REFINED.md), casan-next-plans/ and optimize-docs/ (competition/planning
artifacts — roadmap + design history preserved in git log / commit messages).
- Update all references to the old layout:
- .gitea/workflows/{ci,harness-ci}.yml, .github/workflows/{ci,deploy}.yml:
working-directory .; drop AINative_OKR_CASAN5/ prefix; .specify/{tests,scripts}
-> packages/casan-harness/... (.specify/logs state kept)
- .claude/launch.json, .gitea/*-runbook.md: path prefixes
- CLAUDE.md, README.md: docs/input -> apps/okr/domain/input
- policy-bundle.yaml: 8 policy paths -> packages/casan-harness/...; manifest re-signed
- secrets-scan.sh: fixture excludes -> new package/domain paths.
Full gate from the new root: PASS=64 FAIL=0 SKIP=3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
147 lines
4.6 KiB
Bash
147 lines
4.6 KiB
Bash
#!/usr/bin/env bash
|
|
set -uo pipefail
|
|
|
|
# CASAN H6 — live alert dispatch (D1).
|
|
# Pushes AgentOps alerts to a real HTTP webhook (Slack/Teams/PagerDuty-style
|
|
# endpoint) instead of only appending to a local log file. Undelivered alerts
|
|
# are queued to a dead-letter file so no alert is silently lost.
|
|
#
|
|
# Usage:
|
|
# alert-dispatch.sh <alert-json-file> dispatch one alert (JSON object)
|
|
# alert-dispatch.sh --flush-deadletter retry alerts that failed delivery
|
|
#
|
|
# Env:
|
|
# CASAN_ALERT_WEBHOOK webhook URL (required to dispatch)
|
|
# CASAN_ALERT_STRICT=1 delivery failure => exit 1 (fail-loud); default warn
|
|
# CASAN_ALERT_DEDUP_WINDOW_S suppress same service/step/type within N s (default 300)
|
|
# CASAN_AGENTOPS_DIR state dir override (default .specify/agentops)
|
|
#
|
|
# Greppable outputs:
|
|
# ALERT_DISPATCHED | ALERT_DEDUP_SUPPRESSED | ALERT_DELIVERY_FAILED |
|
|
# ALERT_DEADLETTER_FLUSHED | ALERT_WEBHOOK_UNSET
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
source "$SCRIPT_DIR/casan-paths.sh"
|
|
PROJECT_ROOT="$CASAN_APP_ROOT"
|
|
AGENTOPS_DIR="${CASAN_AGENTOPS_DIR:-$CASAN_HARNESS_ROOT/agentops}"
|
|
STATE="$AGENTOPS_DIR/alert-dispatch-state.jsonl"
|
|
DEADLETTER="$AGENTOPS_DIR/alert-deadletter.jsonl"
|
|
WEBHOOK="${CASAN_ALERT_WEBHOOK:-}"
|
|
STRICT="${CASAN_ALERT_STRICT:-0}"
|
|
DEDUP_S="${CASAN_ALERT_DEDUP_WINDOW_S:-300}"
|
|
mkdir -p "$AGENTOPS_DIR"
|
|
|
|
post_payload() { # <json-payload> — 0 = delivered
|
|
curl -sS -m 5 --retry 2 --retry-delay 1 \
|
|
-H 'Content-Type: application/json' \
|
|
-d "$1" "$WEBHOOK" >/dev/null 2>&1
|
|
}
|
|
|
|
if [[ "${1:-}" == "--flush-deadletter" ]]; then
|
|
if [[ -z "$WEBHOOK" ]]; then
|
|
echo "ALERT_WEBHOOK_UNSET cannot flush dead-letter queue" >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! -s "$DEADLETTER" ]]; then
|
|
echo "ALERT_DEADLETTER_FLUSHED redelivered=0 remaining=0"
|
|
exit 0
|
|
fi
|
|
TMP="$DEADLETTER.tmp"
|
|
: > "$TMP"
|
|
sent=0; kept=0
|
|
while IFS= read -r line; do
|
|
[[ -z "$line" ]] && continue
|
|
if post_payload "$line"; then
|
|
sent=$((sent + 1))
|
|
else
|
|
printf '%s\n' "$line" >> "$TMP"
|
|
kept=$((kept + 1))
|
|
fi
|
|
done < "$DEADLETTER"
|
|
mv "$TMP" "$DEADLETTER"
|
|
echo "ALERT_DEADLETTER_FLUSHED redelivered=$sent remaining=$kept"
|
|
[[ "$kept" -eq 0 ]] || exit 1
|
|
exit 0
|
|
fi
|
|
|
|
ALERT_FILE="${1:-}"
|
|
if [[ -z "$ALERT_FILE" || ! -f "$ALERT_FILE" ]]; then
|
|
echo "Usage: alert-dispatch.sh <alert-json-file> | --flush-deadletter" >&2
|
|
exit 64
|
|
fi
|
|
|
|
if [[ -z "$WEBHOOK" ]]; then
|
|
echo "ALERT_WEBHOOK_UNSET alert not dispatched (set CASAN_ALERT_WEBHOOK)" >&2
|
|
[[ "$STRICT" == "1" ]] && exit 1
|
|
exit 0
|
|
fi
|
|
|
|
# Normalize the alert, decide severity, and apply the dedup window.
|
|
DECISION="$(python - "$ALERT_FILE" "$STATE" "$DEDUP_S" <<'PY'
|
|
import json, sys, time
|
|
|
|
alert_path, state_path, window = sys.argv[1], sys.argv[2], int(sys.argv[3])
|
|
alert = json.load(open(alert_path, encoding="utf-8"))
|
|
body = alert.get("body", {}) if isinstance(alert.get("body"), dict) else {}
|
|
resource = alert.get("resource", {}) if isinstance(alert.get("resource"), dict) else {}
|
|
atype = body.get("alert.type") or alert.get("alert_type") or "unknown"
|
|
step = body.get("step.name") or alert.get("step") or "unknown"
|
|
service = resource.get("service.name") or alert.get("agent") or "unknown"
|
|
critical = {"execution-failed", "circuit-open", "cost-spike", "token-overuse", "audit-gap"}
|
|
severity = "CRITICAL" if atype in critical else "WARN"
|
|
key = f"{service}/{step}/{atype}"
|
|
now = int(time.time())
|
|
last = None
|
|
try:
|
|
with open(state_path, encoding="utf-8") as fh:
|
|
for line in fh:
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
rec = json.loads(line)
|
|
if rec.get("key") == key:
|
|
last = rec.get("ts")
|
|
except OSError:
|
|
pass
|
|
if last is not None and now - last < window:
|
|
print("SUPPRESS " + key)
|
|
raise SystemExit(0)
|
|
payload = json.dumps({
|
|
"source": "casan-agentops",
|
|
"severity": severity,
|
|
"alert_type": atype,
|
|
"step": step,
|
|
"service": service,
|
|
"dedup_key": key,
|
|
"alert": alert,
|
|
})
|
|
with open(state_path, "a", encoding="utf-8") as fh:
|
|
fh.write(json.dumps({"key": key, "ts": now}) + "\n")
|
|
print("SEND " + payload)
|
|
PY
|
|
)"
|
|
|
|
case "$DECISION" in
|
|
SUPPRESS*)
|
|
echo "ALERT_DEDUP_SUPPRESSED key=${DECISION#SUPPRESS } window_s=$DEDUP_S"
|
|
exit 0
|
|
;;
|
|
SEND*)
|
|
PAYLOAD="${DECISION#SEND }"
|
|
;;
|
|
*)
|
|
echo "ALERT_DISPATCH_ERROR unparsable alert file: $ALERT_FILE" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
if post_payload "$PAYLOAD"; then
|
|
echo "ALERT_DISPATCHED webhook=$WEBHOOK"
|
|
exit 0
|
|
fi
|
|
|
|
printf '%s\n' "$PAYLOAD" >> "$DEADLETTER"
|
|
echo "ALERT_DELIVERY_FAILED queued=dead-letter webhook=$WEBHOOK" >&2
|
|
[[ "$STRICT" == "1" ]] && exit 1
|
|
exit 0
|