#!/usr/bin/env bash # # CB-596 step 1: measure which credentials a live member actually holds. # # The claim under test is that a member's herdr pane starts a LOGIN shell, that shell sources # ${SHARED_ENV}/tools/secrets.sh, and so a member inherits every name that file exports — while # CB-592 blocks exactly one of them (GITEA_ACCESS_TOKEN). That is an inference from the code, not a # measurement, and issue #82 says plainly: do not build a fix on the inference. This is the # measurement. # # WHY THIS IS A SCRIPT AND NOT A COMMAND SOMEONE TYPES # # Enumerating credential names inside a member is exactly the action that should need the operator's # explicit approval, and the command classifier refuses it. That refusal is correct. This script is # the seam: it is one auditable file the operator can read once, top to bottom, and then run — rather # than approving an ad-hoc shell pipeline whose behaviour they have to take on trust. # # WHAT IT WILL NOT DO # # * It never prints a credential value, and never any prefix or suffix of one. Not one character. # Issue #82's criterion 1 asked for a 6-character prefix; this prints a truncated SHA-256 instead. # A prefix of a short secret is most of the secret, and it would end up pasted into a ticket. The # hash answers every question the prefix was for — is it set, is it the same value as over there, # is it the CB-592 sentinel — and answers none of the ones it should not. # * It never writes anywhere, never contacts the network, and never touches secrets.sh, which is # the operator's file. # # HOW TO RUN IT # # 1. As the operator, in a member's pane (a spawned worker's terminal): # bash scripts/probe-member-credentials.sh # 2. For the comparison row, in your OWN shell — a lead, not a member: # bash scripts/probe-member-credentials.sh --allow-outside-member # # The two outputs side by side are the finding: any name whose hash matches between them is a # credential the member holds in full. # set -uo pipefail # The names ${SHARED_ENV}/tools/secrets.sh exports, recorded on 2026-08-16 (issue #82). Names only — # this list contains no values and never should. If secrets.sh gains a name, this list goes stale and # the probe silently stops asking about it; that staleness is itself part of what #82's criterion 4 # has to solve, so it is called out in the summary rather than hidden. NAMES=( AI_GATEWAY_TOKEN BESZEL_ADMIN_EMAIL BESZEL_ADMIN_PASSWORD BESZEL_HUB_URL BESZEL_KEY BESZEL_UNIVERSAL_TOKEN BRAIN_MCP_TOKEN CF_ACCOUNT_ID CF_API_TOKEN CF_USER_TOKEN CONFLUENCE_API_TOKEN CONFLUENCE_USERNAME CONTEXT7_TOKEN GITEA_HOST GITLAB_OAUTH_CLIENT_SECRET GITLAB_PERSONAL_ACCESS_TOKEN GRAFANA_ADMIN_PASSWORD GRAFANA_ADMIN_USER HASS_TOKEN HW_PASSWORD HW_USER LTMS_API_KEY MEMORY_MCP_TOKEN METRICS_PUSH_TOKEN OPENCODE_AUTOMODE_MODEL TELEGRAM_BOT_TOKEN TELEGRAM_CHAT_ID TS_API_KEY TS_AUTHKEY WORKER_GITEA_TOKEN GITEA_ACCESS_TOKEN ) allow_outside=0 for arg in "$@"; do case "$arg" in --allow-outside-member) allow_outside=1 ;; -h|--help) sed -n '2,40p' "$0"; exit 0 ;; *) echo "unknown argument: $arg" >&2; exit 2 ;; esac done if [ "${BRIDGED_MEMBER:-}" != "1" ] && [ "$allow_outside" -eq 0 ]; then cat >&2 <<'EOF' refusing to run: BRIDGED_MEMBER is not 1, so this is not a member's shell. The finding this probe exists for is what a MEMBER holds. Run it in a spawned worker's pane. If you meant to take the comparison reading from your own shell, pass --allow-outside-member and the output will be labelled as such. EOF exit 1 fi # Prefer sha256sum (Linux), fall back to shasum (macOS). If neither exists, report presence and # length only — degraded, but never a value. hasher="" if command -v sha256sum >/dev/null 2>&1; then hasher="sha256sum" elif command -v shasum >/dev/null 2>&1; then hasher="shasum -a 256" fi digest() { # value -> first 12 hex chars of its sha256, or "-" when no hasher is available [ -z "$hasher" ] && { printf '%s' "-"; return; } printf '%s' "$1" | $hasher | cut -c1-12 } if [ "${BRIDGED_MEMBER:-}" = "1" ]; then where="MEMBER (BRIDGED_MEMBER=1)" else where="NOT a member — comparison reading only" fi echo "CB-596 credential probe" echo "reading from : $where" echo "shell : ${SHELL:-unknown}" echo "hash : ${hasher:-none available — lengths only}" # Only printed so the two readings can be told apart when they are pasted side by side. echo "host : $(hostname 2>/dev/null || echo unknown)" echo printf '%-30s %-7s %6s %s\n' "NAME" "STATE" "LEN" "SHA256-12" printf '%-30s %-7s %6s %s\n' "------------------------------" "-------" "------" "------------" set_count=0 for name in "${NAMES[@]}"; do value="${!name:-}" if [ -z "$value" ]; then printf '%-30s %-7s %6s %s\n' "$name" "unset" "-" "-" else set_count=$((set_count + 1)) printf '%-30s %-7s %6s %s\n' "$name" "SET" "${#value}" "$(digest "$value")" fi done echo echo "$set_count of ${#NAMES[@]} names are set in this shell." echo cat <<'EOF' How to read this: * Take the MEMBER reading and the comparison reading, and line them up. A name whose SHA256-12 matches on both sides is a credential the member holds in full. That is the finding. * GITEA_ACCESS_TOKEN is the control. CB-592 replaces it with a blocked sentinel, so its hash should DIFFER between the two readings. If it matches, CB-592 is not working and that is the most urgent thing on this page. * AI_GATEWAY_TOKEN matching is expected and correct, not a leak: bridged.yaml names it in `tokenEnv:` for the local and gx profiles, so a member reaching the gateway is by design. * A name that is set here but is NOT in the list above will not appear at all. The list was recorded on 2026-08-16 and does not update itself. Anything added to secrets.sh since then is invisible to this probe — which is the same gap issue #82 criterion 4 asks to close properly. EOF