From b90aec2024dab71176ac976f52c9408a80d94845 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 18 Jun 2026 14:02:43 -0500 Subject: [PATCH] =?UTF-8?q?fix(framework/tools):=20wrapper=20hardening=20?= =?UTF-8?q?=E2=80=94=20TLS=20validation,=20cred-path=20fallback,=20no-CI?= =?UTF-8?q?=20fast-exit=20(#550)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F-03: validate TLS by default. New _mosaic_tls_opt helper in _lib/credentials.sh returns -k only for private-network IP literals (trusted LAN) or an explicit MOSAIC_INSECURE_TLS opt-in; generic mosaic_http/_post/_patch helpers now use `curl -sS $_tls` instead of `curl -sk`. Woodpecker scripts (_lib.sh, pipeline-status/list/trigger.sh) talk only to the two public/valid CI hosts, so `-sk` is changed to `-sS` (straight -k removal, no helper). F-02: credentials.sh resolves MOSAIC_CREDENTIALS_FILE via a fallback chain — env first, then ~/.config/mosaic/credentials.json, then the legacy ~/src/jarvis-brain/credentials.json retained as final fallback so the running fleet keeps working. F-06: pr-ci-wait.sh distinguishes a genuine no-CI condition (empty state AND no statuses) as a new `no-status` state and fast-exits 0 after 3 consecutive empty polls with a clear "no CI configured" message. Repos that DO have pipelines are unaffected — any pipeline signal resets the streak and pending still waits. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Kt2D8TsnDwhtzEAPijsNmR --- .../framework/tools/_lib/credentials.sh | 29 ++++++++++++++++--- .../mosaic/framework/tools/git/pr-ci-wait.sh | 25 ++++++++++++++++ .../mosaic/framework/tools/woodpecker/_lib.sh | 2 +- .../tools/woodpecker/pipeline-list.sh | 2 +- .../tools/woodpecker/pipeline-status.sh | 2 +- .../tools/woodpecker/pipeline-trigger.sh | 2 +- 6 files changed, 54 insertions(+), 8 deletions(-) diff --git a/packages/mosaic/framework/tools/_lib/credentials.sh b/packages/mosaic/framework/tools/_lib/credentials.sh index bbb942c..5a26689 100755 --- a/packages/mosaic/framework/tools/_lib/credentials.sh +++ b/packages/mosaic/framework/tools/_lib/credentials.sh @@ -16,7 +16,12 @@ # After loading, service-specific env vars are exported. # Run `load_credentials --help` for details. -MOSAIC_CREDENTIALS_FILE="${MOSAIC_CREDENTIALS_FILE:-$HOME/src/jarvis-brain/credentials.json}" +if [[ -z "${MOSAIC_CREDENTIALS_FILE:-}" ]]; then + for _cand in "$HOME/.config/mosaic/credentials.json" "$HOME/src/jarvis-brain/credentials.json"; do + if [[ -f "$_cand" ]]; then MOSAIC_CREDENTIALS_FILE="$_cand"; break; fi + done + : "${MOSAIC_CREDENTIALS_FILE:=$HOME/src/jarvis-brain/credentials.json}" +fi _mosaic_require_jq() { if ! command -v jq &>/dev/null; then @@ -34,6 +39,19 @@ _mosaic_read_cred() { jq -r "$jq_path // empty" "$MOSAIC_CREDENTIALS_FILE" } +# Decide curl TLS flag for a target URL: validate public hosts (MITM matters on +# WAN); allow self-signed only for private-network IP literals (trusted LAN) or an +# explicit $MOSAIC_INSECURE_TLS opt-in. Echoes "-k" or "" (empty). +_mosaic_tls_opt() { + local url="$1" host + [[ -n "${MOSAIC_INSECURE_TLS:-}" ]] && { echo "-k"; return; } + host=$(printf '%s' "$url" | sed -E 's#^[a-zA-Z]+://([^/:]+).*#\1#') + if [[ "$host" =~ ^(10\.|127\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.) ]]; then + echo "-k"; return + fi + echo "" +} + # Sync Woodpecker credentials to ~/.woodpecker/.env # Only writes when values differ to avoid unnecessary disk writes. _mosaic_sync_woodpecker_env() { @@ -261,7 +279,8 @@ mosaic_http() { local base_url="${4:-}" local response - response=$(curl -sk -w "\n%{http_code}" -X "$method" \ + local _tls; _tls=$(_mosaic_tls_opt "${base_url}${endpoint}") + response=$(curl -sS $_tls -w "\n%{http_code}" -X "$method" \ -H "$auth_header" \ -H "Content-Type: application/json" \ "${base_url}${endpoint}") @@ -279,7 +298,8 @@ mosaic_http_post() { local base_url="${4:-}" local response - response=$(curl -sk -w "\n%{http_code}" -X POST \ + local _tls; _tls=$(_mosaic_tls_opt "${base_url}${endpoint}") + response=$(curl -sS $_tls -w "\n%{http_code}" -X POST \ -H "$auth_header" \ -H "Content-Type: application/json" \ -d "$data" \ @@ -297,7 +317,8 @@ mosaic_http_patch() { local base_url="${4:-}" local response - response=$(curl -sk -w "\n%{http_code}" -X PATCH \ + local _tls; _tls=$(_mosaic_tls_opt "${base_url}${endpoint}") + response=$(curl -sS $_tls -w "\n%{http_code}" -X PATCH \ -H "$auth_header" \ -H "Content-Type: application/json" \ -d "$data" \ diff --git a/packages/mosaic/framework/tools/git/pr-ci-wait.sh b/packages/mosaic/framework/tools/git/pr-ci-wait.sh index 7b9e8b6..75d2b46 100755 --- a/packages/mosaic/framework/tools/git/pr-ci-wait.sh +++ b/packages/mosaic/framework/tools/git/pr-ci-wait.sh @@ -72,6 +72,11 @@ elif values and all(v == "success" for v in values): print("success") elif any(v in {"pending", "running", "queued", "waiting"} for v in values): print("pending") +elif not values and not state: + # No pipeline/status of any kind reported for this commit. Distinct from + # "unknown" (an ambiguous/unrecognized status that should keep polling): + # this signals a repo/commit that simply has no CI configured. + print("no-status") else: print("unknown") PY @@ -245,6 +250,13 @@ else exit 1 fi +# Count consecutive polls that find NO pipeline/status at all. A repo/commit with +# no CI configured (e.g. device-imaging class) would otherwise burn the full +# timeout in the pending/unknown branch. After NO_CI_MAX such polls, fast-exit 0 +# with a clear "no CI configured" message — distinct from a real failure. +NO_CI_STREAK=0 +NO_CI_MAX=3 + while true; do NOW_TS=$(date +%s) if (( NOW_TS > DEADLINE_TS )); then @@ -272,11 +284,24 @@ while true; do echo "Error: CI reported ${STATE} for PR #$PR_NUMBER." >&2 exit 1 ;; + no-status) + NO_CI_STREAK=$((NO_CI_STREAK + 1)) + if (( NO_CI_STREAK >= NO_CI_MAX )); then + echo "[INFO] no CI configured for this repo/commit (PR #$PR_NUMBER, ${NO_CI_STREAK} consecutive empty polls); treating as green." + exit 0 + fi + sleep "$INTERVAL_SEC" + ;; pending|unknown) + # A pipeline exists but hasn't reached a terminal state (or is + # transiently ambiguous) — keep waiting, and reset the no-CI streak + # since this commit is not in the "no CI at all" condition. + NO_CI_STREAK=0 sleep "$INTERVAL_SEC" ;; *) echo "[pr-ci-wait] Unrecognized state '${STATE}', continuing to poll..." + NO_CI_STREAK=0 sleep "$INTERVAL_SEC" ;; esac diff --git a/packages/mosaic/framework/tools/woodpecker/_lib.sh b/packages/mosaic/framework/tools/woodpecker/_lib.sh index bb9c0e6..899fa1a 100755 --- a/packages/mosaic/framework/tools/woodpecker/_lib.sh +++ b/packages/mosaic/framework/tools/woodpecker/_lib.sh @@ -12,7 +12,7 @@ wp_resolve_repo_id() { local full_name="$1" local response http_code body repo_id - response=$(curl -sk -w "\n%{http_code}" \ + response=$(curl -sS -w "\n%{http_code}" \ -H "Authorization: Bearer $WOODPECKER_TOKEN" \ "${WOODPECKER_URL}/api/repos/lookup/${full_name}") diff --git a/packages/mosaic/framework/tools/woodpecker/pipeline-list.sh b/packages/mosaic/framework/tools/woodpecker/pipeline-list.sh index d77ca37..60d9aa2 100755 --- a/packages/mosaic/framework/tools/woodpecker/pipeline-list.sh +++ b/packages/mosaic/framework/tools/woodpecker/pipeline-list.sh @@ -48,7 +48,7 @@ fi # Resolve owner/repo to numeric ID (Woodpecker v3 API) REPO_ID=$(wp_resolve_repo_id "$REPO") || exit 1 -response=$(curl -sk -w "\n%{http_code}" \ +response=$(curl -sS -w "\n%{http_code}" \ -H "Authorization: Bearer $WOODPECKER_TOKEN" \ "${WOODPECKER_URL}/api/repos/${REPO_ID}/pipelines?perPage=${LIMIT}") diff --git a/packages/mosaic/framework/tools/woodpecker/pipeline-status.sh b/packages/mosaic/framework/tools/woodpecker/pipeline-status.sh index 6fbd186..0996eb8 100755 --- a/packages/mosaic/framework/tools/woodpecker/pipeline-status.sh +++ b/packages/mosaic/framework/tools/woodpecker/pipeline-status.sh @@ -50,7 +50,7 @@ REPO_ID=$(wp_resolve_repo_id "$REPO") || exit 1 _wp_fetch() { local ep="$1" local resp http_code body - resp=$(curl -sk -w "\n%{http_code}" \ + resp=$(curl -sS -w "\n%{http_code}" \ -H "Authorization: Bearer $WOODPECKER_TOKEN" \ "$ep") http_code=$(echo "$resp" | tail -n1) diff --git a/packages/mosaic/framework/tools/woodpecker/pipeline-trigger.sh b/packages/mosaic/framework/tools/woodpecker/pipeline-trigger.sh index d8e497a..5d31fd2 100755 --- a/packages/mosaic/framework/tools/woodpecker/pipeline-trigger.sh +++ b/packages/mosaic/framework/tools/woodpecker/pipeline-trigger.sh @@ -46,7 +46,7 @@ REPO_ID=$(wp_resolve_repo_id "$REPO") || exit 1 echo "Triggering pipeline for $REPO on branch $BRANCH..." -response=$(curl -sk -w "\n%{http_code}" -X POST \ +response=$(curl -sS -w "\n%{http_code}" -X POST \ -H "Authorization: Bearer $WOODPECKER_TOKEN" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg b "$BRANCH" '{branch: $b}')" \