129 lines
3.5 KiB
Bash
Executable File
129 lines
3.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Lightweight scope gate before ACT. Exit 0=ok, 2=not ready, 1=usage/error.
|
|
# Usage:
|
|
# bash skills/scripts/case-guard.sh --case-root work/my-case
|
|
# bash skills/scripts/case-guard.sh --case-root work/my-case --force
|
|
set -euo pipefail
|
|
|
|
CASE_ROOT=""
|
|
FORCE=0
|
|
QUIET=0
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-CaseRoot|--case-root) CASE_ROOT="${2:-}"; shift 2 ;;
|
|
-Force|--force) FORCE=1; shift ;;
|
|
-Quiet|--quiet) QUIET=1; shift ;;
|
|
-h|--help) sed -n '2,6p' "$0"; exit 0 ;;
|
|
*) echo "Unknown arg: $1" >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
info() { [[ $QUIET -eq 1 ]] || echo "$*"; }
|
|
|
|
if [[ -z "$CASE_ROOT" ]]; then
|
|
echo "ERROR: --case-root required" >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! -d "$CASE_ROOT" ]]; then
|
|
echo "ERROR: CaseRoot missing: $CASE_ROOT" >&2
|
|
exit 1
|
|
fi
|
|
|
|
SCOPE_PATH="$CASE_ROOT/scope.md"
|
|
if [[ ! -f "$SCOPE_PATH" ]]; then
|
|
echo "ERROR: scope.md missing under $CASE_ROOT" >&2
|
|
exit 1
|
|
fi
|
|
|
|
SCOPE="$(cat "$SCOPE_PATH")"
|
|
ISSUES=()
|
|
|
|
# Read fields only from their contract sections. A status-like line in notes,
|
|
# evidence, or ops_refs must never satisfy the authorization gate.
|
|
section_field() {
|
|
local section="$1"
|
|
local field="$2"
|
|
awk -v wanted_section="$section" -v wanted_field="$field" '
|
|
/^##[[:space:]]+/ {
|
|
heading=$0
|
|
sub(/^##[[:space:]]+/, "", heading)
|
|
active=(tolower(heading)==tolower(wanted_section))
|
|
next
|
|
}
|
|
active {
|
|
line=$0
|
|
pattern="^[[:space:]]*-[[:space:]]*" wanted_field ":[[:space:]]*"
|
|
if (line ~ pattern) {
|
|
sub(pattern, "", line)
|
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", line)
|
|
print line
|
|
exit
|
|
}
|
|
}
|
|
' "$SCOPE_PATH"
|
|
}
|
|
|
|
auth_granted=0
|
|
if [[ "$(section_field auth status | tr '[:upper:]' '[:lower:]')" == "granted" ]]; then
|
|
auth_granted=1
|
|
fi
|
|
[[ $auth_granted -eq 1 ]] || ISSUES+=("auth.status is not granted")
|
|
|
|
net_mode="$(section_field network_profile mode | tr -d '\r' | awk '{print $1}')"
|
|
if [[ -z "$net_mode" ]]; then
|
|
ISSUES+=("network_profile.mode missing")
|
|
else
|
|
case "$net_mode" in
|
|
offline)
|
|
if ! printf '%s' "$SCOPE" | grep -Eiq 'sample|offline.?path|本地.?样本|\.apk\b|\.bin\b|\.exe\b'; then
|
|
ISSUES+=("network_profile.mode is offline without offline sample cue")
|
|
fi
|
|
;;
|
|
lab_only|authorized_target_only|unrestricted_lab) ;;
|
|
*) ISSUES+=("network_profile.mode is unsupported: $net_mode") ;;
|
|
esac
|
|
fi
|
|
|
|
has_asset=0
|
|
# crude: look for indented list items after assets: that are not []
|
|
if printf '%s\n' "$SCOPE" | awk '
|
|
BEGIN{inscope=0; inassets=0}
|
|
/^##[[:space:]]*in_scope/ {inscope=1; inassets=0; next}
|
|
/^##[[:space:]]/ {if(inscope){inscope=0; inassets=0}}
|
|
inscope && /-[[:space:]]*assets:/ {inassets=1; next}
|
|
inscope && inassets && /^[[:space:]]+-[[:space:]]+/ {
|
|
line=$0
|
|
sub(/^[[:space:]]+-[[:space:]]+/,"",line)
|
|
if (line !~ /^\[/ && length(line)>0) { found=1 }
|
|
}
|
|
END{ exit(found?0:1) }
|
|
'; then
|
|
has_asset=1
|
|
fi
|
|
if [[ $has_asset -eq 0 && "$net_mode" != "offline" ]]; then
|
|
ISSUES+=("in_scope.assets appears empty")
|
|
fi
|
|
|
|
ready=0
|
|
if [[ "$(section_field signoff ready_for_act | tr '[:upper:]' '[:lower:]')" == "true" ]]; then
|
|
ready=1
|
|
fi
|
|
[[ $ready -eq 1 ]] || ISSUES+=("ready_for_act is not true")
|
|
|
|
if [[ ${#ISSUES[@]} -eq 0 ]]; then
|
|
info "CASE-GUARD OK: $CASE_ROOT"
|
|
exit 0
|
|
fi
|
|
|
|
echo "CASE-GUARD NOT READY: $CASE_ROOT"
|
|
for i in "${ISSUES[@]}"; do echo " - $i"; done
|
|
|
|
if [[ $FORCE -eq 1 ]]; then
|
|
echo "CASE-GUARD: --force set; continuing with warnings only."
|
|
exit 0
|
|
fi
|
|
|
|
echo "Fix scope (or re-run case-init with --auth-granted --target-url ...) or pass --force."
|
|
exit 2
|