From 8d352aa7d5fcd38de1cbde6494fba24179f66f9f Mon Sep 17 00:00:00 2001 From: reachableceo Date: Thu, 13 Aug 2026 08:59:24 -0500 Subject: [PATCH] feat: add bw-run.sh credential sourcing layer [#442] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the --env-file ~/.creds/*.env pattern with Bitwarden-sourced credentials. Each agent identity stores its own API keys in a BW collection; bw-run.sh fetches them at runtime and passes to CLI containers via a temp env file (cleaned up on exit). Usage: bw-run.sh [args...] 💘 Generated with Crush Assisted-by: Crush:glm-5.2 --- scripts/bw-run.sh | 157 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100755 scripts/bw-run.sh diff --git a/scripts/bw-run.sh b/scripts/bw-run.sh new file mode 100755 index 0000000..63ce780 --- /dev/null +++ b/scripts/bw-run.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash +# bw-run.sh — run a CLI command with credentials sourced from Bitwarden. +# +# Replaces the --env-file ~/.creds/.env pattern. Each agent identity +# stores its own API keys/passwords in a Bitwarden collection. This script +# fetches the credential, writes it to a temp env file, runs the CLI, and +# cleans up — no credential material persists on disk. +# +# Usage: +# bw-run.sh [args...] +# +# Examples: +# bw-run.sh vp-techops redmine git.knownelement.com/reachableceo/redmine-cli:latest list --assigned-to-me -p 55 +# bw-run.sh vp-techops discourse git.knownelement.com/reachableceo/discourse-cli:latest ls -c vp-techops +# bw-run.sh reachableceo redmine git.knownelement.com/reachableceo/redmine-cli:latest whoami +# +# Prerequisites: +# - BW_CLIENTID and BW_CLIENTSECRET exported (or in ~/.config/bw/env) +# - Or BW_SESSION exported from a prior `bw unlock` +# +# Bitwarden item convention: +# Collection: (or "shared" for cross-agent creds) +# Item name: " " (e.g., "vp-techops Redmine") +# Fields: URL, USERNAME, PASSWORD, API_KEY, and custom fields as needed +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +# shellcheck disable=SC1091 +source "$HERE/lib/common.sh" + +############################################################################### + +print_usage() { + sed -n '2,26p' "$0" + exit 0 +} + +if [ $# -lt 3 ] || [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then + print_usage +fi + +AGENT="$1" +SERVICE="$2" +IMAGE="$3" +shift 3 + +ITEM_NAME="${AGENT} ${SERVICE}" + +############################################################################### +# Ensure Bitwarden session is active +############################################################################### + +ensure_bw_session() { + # If BW_SESSION is already set, trust it + if [ -n "${BW_SESSION:-}" ]; then + return 0 + fi + + # Try machine-to-machine auth via client credentials + if [ -n "${BW_CLIENTID:-}" ] && [ -n "${BW_CLIENTSECRET:-}" ]; then + log_info "Authenticating to Bitwarden via API key..." + if bw login --apikey >/dev/null 2>&1; then + export BW_SESSION + BW_SESSION=$(bw unlock --raw 2>/dev/null || true) + if [ -z "$BW_SESSION" ]; then + die "Bitwarden unlock failed after API-key login." + fi + return 0 + fi + fi + + # Try reading from the bw env file + local bw_env="${HOME}/.config/bw/env" + if [ -f "$bw_env" ]; then + # shellcheck disable=SC1090 + . "$bw_env" + if [ -n "${BW_CLIENTID:-}" ] && [ -n "${BW_CLIENTSECRET:-}" ]; then + log_info "Authenticating to Bitwarden via env file..." + bw login --apikey >/dev/null 2>&1 || true + export BW_SESSION + BW_SESSION=$(bw unlock --raw 2>/dev/null || true) + if [ -n "$BW_SESSION" ]; then + return 0 + fi + fi + fi + + die "No Bitwarden session. Set BW_SESSION or BW_CLIENTID/BW_CLIENTSECRET." +} + +############################################################################### + +ensure_bw_session + +log_info "Fetching credential: ${ITEM_NAME}" + +# Fetch the Bitwarden item and extract fields into env vars +RAW_JSON=$(bw get item "$ITEM_NAME" 2>/dev/null) || die "Bitwarden item not found: ${ITEM_NAME}" + +# Build env file content from the JSON response +ENV_CONTENT=$(echo "$RAW_JSON" | jq -r ' + def safename(f): f | gsub("[^A-Za-z0-9_]"; "_") | ascii_upcase; + .login as $login | + # Standard fields + ( + if $login.uris[0].uri then "URL=\($login.uris[0].uri)\n" else "" end + ) + + ( + if $login.username then "USERNAME=\($login.username)\n" else "" end + ) + + ( + if $login.password then "PASSWORD=\($login.password)\n" else "" end + ) + + # Map known field names to common env var patterns + ( + .fields[]? | + if .name == "API_KEY" or .name == "api_key" or .name == "apikey" then + "API_KEY=\(.value)\n" + elif .name == "token" or .name == "TOKEN" then + "TOKEN=\(.value)\n" + else + "\(safename(.name))=\(.value)\n" + end + ) + + # Also emit REDMINE_API_KEY / DISCOURSE_API_KEY etc. from the password field + # (many CLI tools expect the key in the password field) + "" +') || die "Failed to parse Bitwarden item JSON." + +if [ -z "$ENV_CONTENT" ]; then + die "No credential fields found in Bitwarden item: ${ITEM_NAME}" +fi + +# Also check if the password field IS the API key (common for CLI creds) +# and add a service-specific key env var based on the service name +SERVICE_UPPER=$(echo "$SERVICE" | tr '[:lower:]' '[:upper:]' | tr '-' '_') +API_KEY_VAR="${SERVICE_UPPER}_API_KEY" + +# If the service-specific key var isn't already in ENV_CONTENT, try adding it from password +if ! echo "$ENV_CONTENT" | grep -q "^${API_KEY_VAR}="; then + PASSWORD_VAL=$(echo "$RAW_JSON" | jq -r '.login.password // empty') + if [ -n "$PASSWORD_VAL" ]; then + ENV_CONTENT="${API_KEY_VAR}=${PASSWORD_VAL} +${ENV_CONTENT}" + fi +fi + +# Write to temp file, cleaned up on exit +ENVFILE=$(mktemp "/tmp/bw-${AGENT}-${SERVICE}-XXXXXX.env") +trap 'rm -f "$ENVFILE"' EXIT +chmod 600 "$ENVFILE" +printf '%s' "$ENV_CONTENT" > "$ENVFILE" + +log_ok "Credential sourced. Running: ${IMAGE} $*" + +# Run the CLI with BW-sourced credentials +docker run --rm --env-file "$ENVFILE" "$IMAGE" "$@"