ops/staging/run-public-smoke.sh

58e6347eeb72 · 7 KB

#!/bin/sh
set -eu

script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
base_url=https://staging.openagents.com
mode=${1:-}

usage() {
  echo "usage: ops/staging/run-public-smoke.sh check" >&2
  echo "       ops/staging/run-public-smoke.sh --run CANDIDATE_DIRECTORY OUTPUT" >&2
  exit 64
}

for command_name in curl jq sha256sum; do
  if ! command -v "$command_name" >/dev/null 2>&1; then
    echo "$command_name is required to run the public staging smoke" >&2
    exit 1
  fi
done

if [ "$mode" = "check" ]; then
  [ "$#" -eq 1 ] || usage
  echo "Public staging smoke preflight passed (no network requests sent)."
  exit 0
fi

[ "$mode" = "--run" ] && [ "$#" -eq 3 ] || usage
candidate_dir=$2
output=$3
candidate_manifest="$candidate_dir/candidate-manifest.json"
candidate_checksum="$candidate_dir/candidate-manifest.sha256"

if [ -e "$output" ]; then
  echo "public smoke output already exists" >&2
  exit 1
fi

if [ ! -f "$candidate_manifest" ] || [ ! -f "$candidate_checksum" ]; then
  echo "candidate directory must contain the manifest and its checksum" >&2
  exit 1
fi

(cd "$candidate_dir" && sha256sum --check --strict candidate-manifest.sha256 >/dev/null)

jq -e --arg base_url "$base_url" '
  . as $manifest |
  .schema == "openagents.staging-candidate.v1" and
  (.git_sha | test("^[0-9a-f]{40}$")) and
  .branch == "main" and
  .target.environment == "staging" and
  (.target.project | test("stag"; "i")) and
  (.images.application.manifest_digest | test("^sha256:[0-9a-f]{64}$")) and
  (.images.application.reference | endswith("@" + $manifest.images.application.manifest_digest)) and
  $base_url == "https://staging.openagents.com"
' "$candidate_manifest" >/dev/null || {
  echo "candidate manifest does not satisfy the public staging smoke contract" >&2
  exit 1
}

git_sha=$(jq -r '.git_sha' "$candidate_manifest")
image_digest=$(jq -r '.images.application.manifest_digest' "$candidate_manifest")
started_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
output_parent=$(dirname -- "$output")

if [ ! -d "$output_parent" ]; then
  echo "public smoke output directory does not exist" >&2
  exit 1
fi

umask 077
smoke_root=$(mktemp -d /tmp/openagents-public-smoke.XXXXXX)
checks="$smoke_root/checks.jsonl"
: >"$checks"
overall=passed

cleanup() {
  find "$smoke_root" -depth -delete 2>/dev/null || true
}
trap cleanup EXIT INT TERM

record_check() {
  case_id=$1
  path=$2
  expected_kind=$3
  body="$smoke_root/body"
  headers="$smoke_root/headers"
  curl_error="$smoke_root/curl-error"
  : >"$body"
  : >"$headers"
  : >"$curl_error"

  request_ok=true
  if status_code=$(curl -q -sS \
      --proto '=https' \
      --tlsv1.2 \
      --connect-timeout 10 \
      --max-time 30 \
      --output "$body" \
      --dump-header "$headers" \
      --write-out '%{http_code}' \
      "$base_url$path" 2>"$curl_error"); then
    :
  else
    request_ok=false
    status_code=000
  fi

  content_type=$(grep -i '^content-type:' "$headers" 2>/dev/null | tail -n 1 | cut -d ':' -f 2- | tr -d '\r' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
  body_size=$(wc -c <"$body" | tr -d ' ')
  body_sha256=$(sha256sum "$body" | cut -d ' ' -f 1)
  headers_sha256=$(sha256sum "$headers" | cut -d ' ' -f 1)
  semantic_ok=false

  if [ "$request_ok" = true ] && [ "$status_code" = 200 ]; then
    case "$expected_kind" in
      json)
        if printf '%s' "$content_type" | grep -Eiq '^application/json([[:space:]]*;|$)' &&
           jq -e --arg git_sha "$git_sha" '
             .status == "ok" and .revision == $git_sha
           ' "$body" >/dev/null 2>&1; then
          semantic_ok=true
        fi
        ;;
      html)
        if printf '%s' "$content_type" | grep -Eiq '^text/html([[:space:]]*;|$)' &&
           [ "$body_size" -gt 0 ]; then
          semantic_ok=true
        fi
        ;;
      binary)
        if [ "$body_size" -gt 0 ]; then
          semantic_ok=true
        fi
        ;;
    esac
  fi

  csp_present=false
  permissions_policy_present=false
  nonce_bound=false
  if [ "$path" = "/" ]; then
    if grep -Eiq '^content-security-policy:' "$headers" &&
       grep -Eiq "^content-security-policy:.*default-src 'self'.*script-src 'self' 'nonce-[A-Za-z0-9_-]+'" "$headers"; then
      csp_present=true
    fi
    if grep -Eiq '^permissions-policy:[[:space:]]*microphone=\(self\)' "$headers"; then
      permissions_policy_present=true
    fi

    header_nonce=$(sed -n "s/^content-security-policy:.*script-src 'self' 'nonce-\([A-Za-z0-9_-]*\)'.*/\1/ip" "$headers" | tail -n 1)
    if [ -n "$header_nonce" ] && grep -Fq "nonce=\"$header_nonce\"" "$body"; then
      nonce_bound=true
    fi

    if [ "$csp_present" != true ] || [ "$permissions_policy_present" != true ] || [ "$nonce_bound" != true ]; then
      semantic_ok=false
    fi
  fi

  if [ "$semantic_ok" != true ]; then
    overall=failed
  fi

  jq -cn \
    --arg case_id "$case_id" \
    --arg path "$path" \
    --arg expected_kind "$expected_kind" \
    --arg status_code "$status_code" \
    --arg content_type "$content_type" \
    --argjson body_size "$body_size" \
    --arg body_sha256 "$body_sha256" \
    --arg headers_sha256 "$headers_sha256" \
    --argjson request_ok "$request_ok" \
    --argjson semantic_ok "$semantic_ok" \
    --argjson csp_present "$csp_present" \
    --argjson permissions_policy_present "$permissions_policy_present" \
    --argjson nonce_bound "$nonce_bound" '
    {
      case_id: $case_id,
      path: $path,
      expected_kind: $expected_kind,
      status_code: ($status_code | tonumber),
      content_type: $content_type,
      body_bytes: $body_size,
      body_sha256: $body_sha256,
      response_headers_sha256: $headers_sha256,
      request_ok: $request_ok,
      semantic_ok: $semantic_ok,
      browser_policy: (if $path == "/" then {
        csp_present: $csp_present,
        permissions_policy_present: $permissions_policy_present,
        response_nonce_bound_to_theme_bootstrap: $nonce_bound
      } else null end)
    }
  ' >>"$checks"
}

record_check public-001 /health json
record_check public-001 /status json
record_check public-001 /api/status json
record_check public-001 /favicon.ico binary
record_check public-002 / html
record_check public-002 /leaderboard html
record_check public-002 /changelog html
record_check public-002 /docs html
record_check public-002 /components html

completed_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
candidate_manifest_sha256=$(sha256sum "$candidate_manifest" | cut -d ' ' -f 1)
temporary_output="$smoke_root/public-smoke.json"

jq -s \
  --arg git_sha "$git_sha" \
  --arg image_digest "$image_digest" \
  --arg candidate_manifest_sha256 "$candidate_manifest_sha256" \
  --arg base_url "$base_url" \
  --arg started_at "$started_at" \
  --arg completed_at "$completed_at" \
  --arg outcome "$overall" '
  {
    schema: "openagents.staging-public-smoke.v1",
    target: {environment: "staging", base_url: $base_url},
    candidate: {
      git_sha: $git_sha,
      application_manifest_digest: $image_digest,
      candidate_manifest_sha256: $candidate_manifest_sha256
    },
    started_at: $started_at,
    completed_at: $completed_at,
    outcome: $outcome,
    content_retained: false,
    checks: .
  }
' "$checks" >"$temporary_output"

"$script_dir/scan-evidence.sh" "$temporary_output" >/dev/null
mv "$temporary_output" "$output"
chmod 600 "$output"

if [ "$overall" != passed ]; then
  echo "Public staging smoke failed; review the content-free receipt: $output" >&2
  exit 1
fi

echo "Public staging smoke passed: $output"