#!/usr/bin/env bash # auth.sh — pi credential checkpoint (M19, corrected per owner in #48): # per-provider auth report and mosaic-managed account listing. Never prints # credential material: provider names, credential types, permission bits, # and env var NAMES only. # # Ownership rule (#48): ~/.pi is READ-ONLY to the stack — the only # interaction is the existing read-only container mount of the default # credential. Mosaic-managed accounts live under the data root: # /auth/.json # # Usage: # scripts/auth.sh status report default harness credential + accounts # scripts/auth.sh accounts list mosaic-managed accounts (data root) # # Exit codes: 0 report produced · 2 credential file unparseable · # 3 default credential file missing for a read · 4 file/environment problem. set -uo pipefail cd "$(dirname "$0")/.." # shellcheck source=common.sh source scripts/common.sh load_config # fail closed: data root comes from the system config HARNESS_FILE="${PI_AUTH_FILE:-$HOME/.pi/agent/auth.json}" ACCOUNTS_DIR="$MOSAIC_DEV_DIR/auth" OP="${1:-}" # report_file FILE — providers + types for a pi auth.json; never values. report_file() { local file="$1" if [ ! -e "$file" ]; then echo " (not present)" return 3 fi if [ ! -f "$file" ] || [ -L "$file" ]; then echo "auth: credential file must be a regular, non-symbolic-link file: $file" >&2 return 4 fi if ! node -e ' const fs = require("fs"); const file = process.argv[1]; let doc; try { doc = JSON.parse(fs.readFileSync(file, "utf8")); } catch (e) { console.error("auth: credential file is not valid JSON: " + e.message); process.exit(2); } if (typeof doc !== "object" || doc === null || Array.isArray(doc)) { console.error("auth: credential file must be a JSON object keyed by provider"); process.exit(2); } const st = fs.statSync(file); console.log(" perms " + String(st.mode & 0o777).toString(8).padStart(3, "0")); const keys = Object.keys(doc).sort(); if (keys.length === 0) console.log(" providers: (none in file)"); for (const k of keys) { const e = doc[k]; let type = "unknown"; if (typeof e === "object" && e !== null && typeof e.type === "string" && /^[a-z0-9_-]+$/i.test(e.type)) type = e.type; console.log(" " + k + " type=" + type + " source=auth.json"); } ' "$file"; then return 2 fi return 0 } case "$OP" in status) echo "default harness credential (read-only to the stack; never written): $HARNESS_FILE" RC=0 report_file "$HARNESS_FILE" || RC=$? echo "resolution: auth.json entries take priority over environment (pi order: --api-key > auth.json > env > models.json)" ENV_NAMES="$(env | grep -oE '^[A-Z0-9_]+_API_KEY' | sort -u | paste -sd, -)" if [ -n "$ENV_NAMES" ]; then echo "env-side credential-like names set (informational; file entries take priority): $ENV_NAMES" else echo "env-side credential-like names set: (none)" fi echo "mosaic-managed accounts ($ACCOUNTS_DIR):" shopt -s nullglob FILES=("$ACCOUNTS_DIR"/*.json) shopt -u nullglob if [ "${#FILES[@]}" -eq 0 ]; then echo " (none)" else for f in "${FILES[@]}"; do name="$(basename "$f" .json)" perms="$(stat -c %a "$f" 2>/dev/null || echo '?')" marker="" [ "$f" = "$HARNESS_FILE" ] && marker=" <- active (PI_AUTH_FILE)" warn="" [ "$perms" != "600" ] && warn=" [not 0600 — agent --auth will refuse]" echo " $name (perms $perms)$marker$warn" done fi exit "$RC" ;; accounts) shopt -s nullglob FILES=("$ACCOUNTS_DIR"/*.json) shopt -u nullglob if [ "${#FILES[@]}" -eq 0 ]; then echo "accounts: (none) — mosaic-managed accounts are $ACCOUNTS_DIR/.json (perms 0600)" exit 0 fi echo "mosaic-managed accounts in $ACCOUNTS_DIR:" for f in "${FILES[@]}"; do name="$(basename "$f" .json)" perms="$(stat -c %a "$f" 2>/dev/null || echo '?')" marker="" [ "$f" = "$HARNESS_FILE" ] && marker=" <- active (PI_AUTH_FILE)" warn="" [ "$perms" != "600" ] && warn=" [not 0600 — agent --auth will refuse]" echo " $name (perms $perms)$marker$warn" done ;; *) echo "usage: scripts/auth.sh status | accounts" >&2 exit 4 ;; esac