#!/bin/bash # git-credential-mosaic — git credential helper — resolves Gitea tokens from # the Mosaic credential store at runtime so remote URLs never embed secrets. # # Install (one-time, per clone or globally): # git config credential.helper "$HOME/.config/mosaic/tools/git/git-credential-mosaic" # # or, fleet-wide: git config --global credential.helper "$HOME/.config/mosaic/tools/git/git-credential-mosaic" # # Per-agent Gate-16 identity (author != reviewer separation): # git config mosaic.gitIdentity # per-worktree, persists on disk # # or: export MOSAIC_GIT_IDENTITY= # # Resolution priority: MOSAIC_GIT_IDENTITY env > git config mosaic.gitIdentity # (per-worktree, survives across non-persistent shells) > git-supplied username # (credential.username / URL). When the resolved identity has a matching # per-agent token file, use it instead of the shared account. Backward # compatible: nothing resolvable -> shared token (unchanged behavior). [ "$1" = "get" ] || exit 0 host=""; username_in="" while IFS= read -r line; do [ -z "$line" ] && break case "$line" in host=*) host=${line#host=};; username=*) username_in=${line#username=};; esac done script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" trace_resolution() { [ "${MOSAIC_CREDENTIAL_TRACE:-}" = 1 ] || return 0 reason="$1" trace_identity="$2" trace_host="$3" source="$4" shared_path_entered=false [ "$resolution_path" = shared ] && shared_path_entered=true printf 'MOSAIC_CREDENTIAL_RESOLUTION outcome=ok reason=%s identity=%s host=%s resolution_path=%s shared_path_entered=%s source=%s\n' \ "$reason" "$trace_identity" "$trace_host" "$resolution_path" "$shared_path_entered" "$source" >&2 } resolution_path=unresolved # Per-agent identity resolution (Gate-16 author≠reviewer separation). # Priority: MOSAIC_GIT_IDENTITY env > git config mosaic.gitIdentity (per-worktree, # survives across non-persistent shells) > git-supplied username (credential.username # / URL). When the resolved identity has a matching per-agent token, use it instead of # the shared account. Backward-compatible: nothing resolvable → shared token. case "$host" in git.uscllc.com) idpfx=gitea-usc;; git.mosaicstack.dev) idpfx=gitea-mosaicstack;; *) exit 0;; esac ident="$MOSAIC_GIT_IDENTITY" [ -z "$ident" ] && ident=$(git config --get mosaic.gitIdentity 2>/dev/null) [ -z "$ident" ] && ident="$username_in" if [[ -n "$ident" && ! "$ident" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]]; then echo "quit=true" printf 'MOSAIC_CREDENTIAL_REFUSAL outcome=refused reason=invalid-identity identity= host=%s shared_path_entered=false source=git-credential-mosaic\n' "$host" >&2 exit 1 fi if [ -n "$idpfx" ] && [ -n "${MOSAIC_AGENT_NAME:-}" ] && [ -n "$ident" ] && [ "$ident" != "$MOSAIC_AGENT_NAME" ]; then echo "quit=true" printf 'MOSAIC_CREDENTIAL_REFUSAL outcome=refused reason=provider-identity-mismatch identity=%s fleet_identity=%s host=%s shared_path_entered=false source=git-credential-mosaic\n' \ "$ident" "$MOSAIC_AGENT_NAME" "$host" >&2 exit 1 fi if [ -n "$ident" ]; then if [ -n "$idpfx" ]; then idtok="$HOME/.config/mosaic/secrets/gitea-tokens/${idpfx}-${ident}.token" idcred="$HOME/.config/mosaic/secrets/gitea-tokens/${idpfx}-${ident}.credential.json" if [ -e "$idcred" ] || [ -L "$idcred" ]; then token=$(python3 "$script_dir/resolve-credential-envelope.py" \ "$HOME/.config/mosaic/secrets/gitea-tokens" "$idcred" "$ident" "${MOSAIC_CREDENTIAL_ESTATE:-}" "$host") || exit 1 resolution_path=identity trace_resolution credential-resolved "$ident" "$host" git-credential-mosaic echo "username=${ident}" echo "password=${token}" exit 0 fi if [ -e "$idtok" ] || [ -L "$idtok" ]; then token=$(python3 "$script_dir/resolve-legacy-token.py" \ "$HOME/.config/mosaic/secrets/gitea-tokens" "$idtok") || exit 1 resolution_path=identity trace_resolution credential-resolved "$ident" "$host" git-credential-mosaic echo "username=${ident}" echo "password=${token}" exit 0 fi if [ -n "${MOSAIC_AGENT_NAME:-}" ]; then echo "quit=true" printf 'MOSAIC_CREDENTIAL_REFUSAL outcome=refused reason=no-token-for-identity identity=%s host=%s shared_path_entered=false source=git-credential-mosaic path=%s\n' \ "$ident" "$host" "$idtok" >&2 exit 1 fi fi fi if [ -n "${MOSAIC_AGENT_NAME:-}" ] && [ -z "$ident" ]; then case "$host" in git.uscllc.com|git.mosaicstack.dev) echo "quit=true" printf 'MOSAIC_CREDENTIAL_REFUSAL outcome=refused reason=identity-required identity= host=%s shared_path_entered=false source=git-credential-mosaic\n' "$host" >&2 exit 1 ;; esac fi case "$host" in git.uscllc.com) svc=gitea-usc;; git.mosaicstack.dev) svc=gitea-mosaicstack;; *) exit 0;; esac # Script-relative (not $HOME-absolute) so this resolves correctly regardless # of where the framework installer places tools/ under $HOME — mirrors # detect-platform.sh's own cred_loader resolution in this same directory. # shellcheck source=../_lib/credentials.sh source "$script_dir/../_lib/credentials.sh" load_credentials "$svc" >/dev/null 2>&1 || exit 0 resolution_path=shared trace_resolution shared-credential-resolved '' "$host" credentials-loader # GITEA_USER is not populated by load_credentials (it only exports # GITEA_URL/GITEA_TOKEN for gitea-*), so this fallback is normally taken. Gitea's # git-over-HTTP auth authenticates from the token itself (the password field), # not from the username string, so any non-empty placeholder works here — this # is deliberately NOT a real account name (framework files must stay # operator-agnostic; see tools/quality/scripts/verify-sanitized.sh). echo "username=${GITEA_USER:-git}" echo "password=$GITEA_TOKEN"