framework: port R4 credential-helper pair from brain 15f6979a (python entrypoint w/ alpine bash-path candidates + impl, byte-faithful; P0-SEC T125)

(cherry picked from commit 13e7398c2a)
This commit is contained in:
code-be-01
2026-08-30 16:51:40 -05:00
committed by marcie
parent f194ccb8a3
commit a47f7aec10
2 changed files with 536 additions and 212 deletions
@@ -1,220 +1,64 @@
#!/bin/bash #!/usr/bin/python3
# git-credential-mosaic — git credential helper. Resolves a Gitea token from the # git-credential-mosaic — production entrypoint (P0-SEC R4, rev-code-02 B1).
# Mosaic credential store at runtime so remote URLs never embed secrets.
# #
# Install (one-time, per clone or globally): # WHY THIS IS NOT BASH: three review rounds falsified every in-bash startup
# git config credential.helper "$HOME/.config/mosaic/tools/git/git-credential-mosaic" # guard. A non-interactive bash sources $BASH_ENV and imports exported
# functions BEFORE the first script line, so read(), unset(), exit(),
# declare(), printf() — every callable — can be shadows that fake the
# ancestry, defeat the scrub, or forge diagnostics (rev-code-02 probes 1 and
# 2, artifacts fc49e9d9 lineage). No in-language dispatch survives that.
# #
# Per-agent identity (Gate-16 author != reviewer separation): # This entrypoint is unshapable at the bash level: python does not read
# git config mosaic.gitIdentity <agent-id> # per-worktree, persists on disk # BASH_ENV and imports no bash functions, and the interpreter is pinned by
# # or: export MOSAIC_GIT_IDENTITY=<agent-id> # absolute shebang (no PATH resolution). It builds the child environment BY
# ALLOWLIST and execve's the bash implementation directly — the child bash
# starts with no BASH_ENV, no BASH_FUNC_*, no SHELLOPTS/BASHOPTS, and exactly
# the variables the credential protocol needs. stdin/stdout/stderr and argv
# pass through untouched.
# #
# ── WHY THIS FAILS CLOSED ────────────────────────────────────────────────────── # The implementation file (git-credential-mosaic.impl) refuses to run without
# This helper used to end by emitting the shared account's token for any request # the clean-mode marker, so it cannot be invoked directly as a shaped-entry
# it could not resolve to an identity. A seat with no identity, or with an # bypass of this wrapper.
# identity whose token was never provisioned, therefore received the most
# privileged credential configured on the host — silently, and indistinguishably
# from correct operation. Every record it then created (commit, push, PR, review)
# was attributed to that shared account, so author != reviewer separation was
# unenforceable and the true actor was unrecoverable after the fact.
#
# Under-provisioning must fail loudly, not impersonate. A refused git operation
# is recoverable in one command; a merged pull request attributed to the wrong
# principal is not.
#
# ── CONTRACT ───────────────────────────────────────────────────────────────────
# identity : MOSAIC_GIT_IDENTITY > git config mosaic.gitIdentity > the
# username git supplies on stdin
# store : chosen by what the identity IS, with no precedence and no
# cross-store fallback (see "Credential store selection" below)
# hit : emit username + password, exit 0
# miss : emit NOTHING, spool a durable escalation record, explain on
# stderr, exit 1 — git surfaces the failure and nothing is attributed
# unknown host : exit 0 with no output, no record (passthrough for non-Mosaic
# remotes handled by another helper)
#
# Backward compatibility is preserved for exactly one case: a host with no fleet
# and no identity requested still gets the shared account, because on such a host
# the shared account is the operator's own and there is no attribution to lose.
# A host that HAS a fleet has agents whose records must be distinguishable, so
# the shared fallback is refused there.
#
# A token is never written to stderr, to the escalation record, or to any log.
[ "$1" = "get" ] || exit 0 import os
import sys
host=""; username_in="" IMPL = os.path.join(os.path.dirname(os.path.realpath(__file__)), "git-credential-mosaic.impl")
while IFS= read -r line; do # Absolute-path candidates ONLY — never PATH resolution (an attacker-shaped
[ -z "$line" ] && break # PATH must not choose the interpreter). /usr/bin/bash is the fleet-host
case "$line" in # layout; /bin/bash is alpine and other FHS variants (found by the T125
host=*) host=${line#host=};; # gateway-image verification: the hardcoded /usr/bin/bash made every call
username=*) username_in=${line#username=};; # exit 127 inside node:22-alpine).
esac BASH_CANDIDATES = ("/usr/bin/bash", "/bin/bash")
done BASH = next((p for p in BASH_CANDIDATES if os.access(p, os.X_OK)), None)
# Recognized Gitea hosts carry the per-identity token scheme. Anything else is # Allowlist: everything else in the environment dies at this boundary. Adding
# declined quietly — another helper owns it, and refusing would break it. # a variable here is a security decision — it crosses into a shell that no
case "$host" in # longer has any startup shaping, but it also becomes the only context the
git.uscllc.com) idpfx=gitea-usc;; # implementation can see.
git.mosaicstack.dev) idpfx=gitea-mosaicstack;; KEEP = (
*) exit 0;; "HOME",
esac "PATH",
"LANG",
"MOSAIC_GIT_IDENTITY",
"MOSAIC_AGENT_NAME",
"MOSAIC_BRAIN_HOME",
"MOSAIC_CREDENTIAL_SPOOL",
"MOSAIC_CREDENTIAL_LINEAGE_FENCE",
)
ident="$MOSAIC_GIT_IDENTITY"; ident_src="MOSAIC_GIT_IDENTITY" env = {"_MOSAIC_HELPER_CLEAN": "1"}
if [ -z "$ident" ]; then for name in KEEP:
ident=$(git config --get mosaic.gitIdentity 2>/dev/null) value = os.environ.get(name)
ident_src="git config mosaic.gitIdentity" if value is not None:
fi env[name] = value
if [ -z "$ident" ]; then
ident="$username_in"
ident_src="the username git supplied"
fi
# ── Credential store selection ──────────────────────────────────────────────── argv = [BASH, IMPL] + sys.argv[1:]
# An identity is a SEAT or it is a SERVICE, and which one it is determines where if BASH is None:
# its credential lives. There is no precedence rule between the two stores and no sys.stderr.write("git-credential-mosaic: no executable bash at " + " or ".join(BASH_CANDIDATES) + "\n")
# fallback from one to the other: a seat whose slot is empty fails closed rather sys.exit(127)
# than reading a service credential that happens to share its name. try:
# os.execve(BASH, argv, env)
# seat — <brain>/fleet/agents/<ident>/ exists except OSError as exc:
# credential at <brain>/fleet/agents/<ident>/secrets/<idpfx>-<ident>.token sys.stderr.write(f"git-credential-mosaic: entrypoint exec failed: {exc}\n")
# service — it does not sys.exit(127)
# credential at ~/.config/mosaic/secrets/gitea-tokens/<idpfx>-<ident>.token
#
# One credential, one location. Two copies of one credential diverge, and the
# stale copy fails in a way that reads as a revoked token rather than as drift.
#
# Brain-home resolution mirrors packages/mosaic/src/fleet/brain-home.ts and
# tools/fleet/start-agent-session.sh: MOSAIC_BRAIN_HOME wins, else ~/.mosaic.
brain_home="${MOSAIC_BRAIN_HOME:-$HOME/.mosaic}"
svc_store="$HOME/.config/mosaic/secrets/gitea-tokens"
idtok=""; ident_kind=""
if [ -n "$ident" ]; then
if [ -d "$brain_home/fleet/agents/$ident" ]; then
ident_kind="seat"
idtok="$brain_home/fleet/agents/$ident/secrets/${idpfx}-${ident}.token"
else
ident_kind="service identity"
idtok="$svc_store/${idpfx}-${ident}.token"
fi
if [ -r "$idtok" ]; then
echo "username=${ident}"
echo "password=$(cat "$idtok")"
exit 0
fi
fi
# ── Shared-account fallback: ONLY on a host with no fleet and no identity ──────
# `fleet/agents` existing is the same signal brain-home.ts uses to decide a brain
# is active. Where there are seats, records must be attributable, so an
# unresolvable request is refused instead of borrowing the shared account.
fleet_present=0
[ -d "$brain_home/fleet/agents" ] && fleet_present=1
if [ -z "$ident" ] && [ "$fleet_present" -eq 0 ]; then
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../_lib/credentials.sh
source "$script_dir/../_lib/credentials.sh"
load_credentials "$idpfx" >/dev/null 2>&1 || exit 0
# GITEA_USER is not populated by load_credentials (it exports GITEA_URL and
# GITEA_TOKEN only). Gitea's git-over-HTTP auth authenticates from the token in
# the password field, not from the username string, so any non-empty
# placeholder works — deliberately NOT a real account name, since framework
# files stay operator-agnostic (tools/quality/scripts/verify-sanitized.sh).
echo "username=${GITEA_USER:-git}"
echo "password=$GITEA_TOKEN"
exit 0
fi
# ── FAIL CLOSED ───────────────────────────────────────────────────────────────
if [ -z "$ident" ]; then
reason="no-identity"
else
reason="no-token-for-identity"
fi
seat="${MOSAIC_AGENT_NAME:-unknown}"
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
# The escalation RECORD is durable and unconditional; any notification built on
# top of it is best-effort. Record and alert are deduplicated separately — a cap
# on the alert alone lets the spool grow without bound exactly while the operator
# is being told nothing, so the louder the failure the quieter it gets.
#
# A record field is arbitrary operator-supplied text: an identity comes from git
# config or the environment, and cwd is whatever directory git ran in. Either can
# contain a quote or a backslash, which would make the line unparseable JSON --
# and a spool that silently stops parsing is worse than no spool, because the
# operator only discovers it while reading the record that explains an outage.
json_escape() {
local s=$1
s=${s//\\/\\\\}
s=${s//\"/\\\"}
s=${s//$'\t'/\\t}
s=${s//$'\r'/\\r}
s=${s//$'\n'/\\n}
printf '%s' "$s"
}
spool="${MOSAIC_CREDENTIAL_SPOOL:-$HOME/.local/state/mosaic-credential-escalations}"
spool_record=""
if mkdir -p "$spool" 2>/dev/null; then
chmod 700 "$spool" 2>/dev/null
spoolfile="$spool/$(date -u +%Y%m%d).jsonl"
dedupe="$spool/.spooled-${seat}-${ident:-none}-${reason}-$(date -u +%Y%m%d%H%M)"
if [ ! -e "$dedupe" ]; then
: > "$dedupe" 2>/dev/null
printf '{"ts":"%s","reason":"%s","identity":"%s","identity_source":"%s","kind":"%s","seat":"%s","host":"%s","cwd":"%s"}\n' \
"$(json_escape "$ts")" "$(json_escape "$reason")" \
"$(json_escape "${ident:-<unset>}")" "$(json_escape "$ident_src")" \
"$(json_escape "${ident_kind:-none}")" "$(json_escape "$seat")" \
"$(json_escape "$host")" "$(json_escape "$PWD")" \
>> "$spoolfile" 2>/dev/null
chmod 600 "$spoolfile" 2>/dev/null
fi
# Name the record only if one is actually on disk. Printing the path
# unconditionally sends the operator to a file that does not exist on exactly
# the hosts where the spool could not be created.
[ -s "$spoolfile" ] && spool_record="$spoolfile"
find "$spool" -maxdepth 1 -name '.spooled-*' -mmin +120 -delete 2>/dev/null
fi
cat >&2 <<EOF
git-credential-mosaic: REFUSED (fail-closed).
host : ${host}
identity : ${ident:-<unset>}${ident:+ (from ${ident_src}; resolved as a ${ident_kind})}
reason : ${reason}
EOF
if [ -n "$ident" ]; then
cat >&2 <<EOF
expected : ${idtok}
EOF
fi
cat >&2 <<EOF
No per-identity credential resolved. This helper does NOT fall back to the shared
account: that fallback makes every record it creates attributable to one
principal, which is unrecoverable once a pull request has merged under it.
Fix (pick one):
export MOSAIC_GIT_IDENTITY=<agent-id> # process-scoped
git config mosaic.gitIdentity <agent-id> # per-repo/worktree, persists
Then provision that identity's credential at the path named above. An identity
with a directory under \${MOSAIC_BRAIN_HOME:-\$HOME/.mosaic}/fleet/agents/ is a
seat and is read ONLY from its own secrets/ slot; any other identity is read from
~/.config/mosaic/secrets/gitea-tokens/. There is no fallback between the two.
If this identity legitimately needs git access and has none, ask the orchestrator
to provision one.
EOF
if [ -n "$spool_record" ]; then
echo " record: ${spool_record}" >&2
else
echo " record: NOT WRITTEN — spool unavailable at ${spool}" >&2
fi
exit 1
@@ -0,0 +1,480 @@
#!/bin/bash
# git-credential-mosaic — git credential helper. Resolves a Gitea token 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"
#
# Per-agent identity (Gate-16 author != reviewer separation):
# git config mosaic.gitIdentity <agent-id> # per-worktree, persists on disk
# # or: export MOSAIC_GIT_IDENTITY=<agent-id>
#
# ── WHY THIS FAILS CLOSED ──────────────────────────────────────────────────────
# This helper used to end by emitting the shared account's token for any request
# it could not resolve to an identity. A seat with no identity, or with an
# identity whose token was never provisioned, therefore received the most
# privileged credential configured on the host — silently, and indistinguishably
# from correct operation. Every record it then created (commit, push, PR, review)
# was attributed to that shared account, so author != reviewer separation was
# unenforceable and the true actor was unrecoverable after the fact.
#
# Under-provisioning must fail loudly, not impersonate. A refused git operation
# is recoverable in one command; a merged pull request attributed to the wrong
# principal is not.
#
# ── CONTRACT ───────────────────────────────────────────────────────────────────
# identity : MOSAIC_GIT_IDENTITY > git config mosaic.gitIdentity > the
# username git supplies on stdin
# ownership: a FLEET SEAT caller may resolve ONLY its own identity, where
# the CALLER is established by process ANCESTRY, not by the
# current environment: every ancestor's /proc/<pid>/environ is
# frozen at exec, so a child can rewrite its own MOSAIC_AGENT_NAME
# but can never make an ancestor disagree with what the launcher
# gave it (P5-RM-006; the dual-variable override was measured by
# rev-code-02 F1). An anonymous caller (no lineage, no consensus)
# may resolve NOTHING on a fleet host — seat or service
# (rev-code-02 F2). Non-fleet hosts keep the documented legacy
# paths below.
# perms : a slot whose mode lets group or other read it (anything but
# ?00) is refused — a loose slot is provisioning drift, and
# serving from it silently widens every seat's exposure on a
# single-account host.
# store : chosen by what the identity IS, with no precedence and no
# cross-store fallback (see "Credential store selection" below)
# hit : emit username + password, exit 0
# miss : emit NOTHING, spool a durable escalation record, explain on
# stderr, exit 1 — git surfaces the failure and nothing is attributed
# unknown host : exit 0 with no output, no record (passthrough for non-Mosaic
# remotes handled by another helper)
#
# Backward compatibility is preserved for exactly one case: a host with no fleet
# and no identity requested still gets the shared account, because on such a host
# the shared account is the operator's own and there is no attribution to lose.
# A host that HAS a fleet has agents whose records must be distinguishable, so
# the shared fallback is refused there.
#
# A token is never written to stderr, to the escalation record, or to any log.
[ "$1" = "get" ] || exit 0
# ── The shared refusal path ──────────────────────────────────────────────────
# Every fail-closed exit funnels through refuse(): a durable escalation record
# (deduped, JSON-escaped), a stderr diagnostic naming host/identity/reason,
# caller-supplied guidance when the refusing site has specific advice, exit 1.
# Defined here because the ownership gate below must be able to reach it.
refuse() {
local guidance="${1:-}"
local seat ts spool spool_record spoolfile dedupe
seat="${MOSAIC_AGENT_NAME:-unknown}"
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
# A record field is arbitrary operator-supplied text: an identity comes from git
# config or the environment, and cwd is whatever directory git ran in. Either can
# contain a quote or a backslash, which would make the line unparseable JSON --
# and a spool that silently stops parsing is worse than no spool, because the
# operator only discovers it while reading the record that explains an outage.
json_escape() {
local s=$1
s=${s//\\/\\\\}
s=${s//\"/\\\"}
s=${s//$'\t'/\\t}
s=${s//$'\r'/\\r}
s=${s//$'\n'/\\n}
printf '%s' "$s"
}
spool="${MOSAIC_CREDENTIAL_SPOOL:-$HOME/.local/state/mosaic-credential-escalations}"
spool_record=""
if mkdir -p "$spool" 2>/dev/null; then
chmod 700 "$spool" 2>/dev/null
spoolfile="$spool/$(date -u +%Y%m%d).jsonl"
dedupe="$spool/.spooled-${seat}-${ident:-none}-${reason}-$(date -u +%Y%m%d%H%M)"
if [ ! -e "$dedupe" ]; then
: > "$dedupe" 2>/dev/null
printf '{"ts":"%s","reason":"%s","identity":"%s","identity_source":"%s","kind":"%s","seat":"%s","host":"%s","cwd":"%s"}\n' \
"$(json_escape "$ts")" "$(json_escape "$reason")" \
"$(json_escape "${ident:-<unset>}")" "$(json_escape "$ident_src")" \
"$(json_escape "${ident_kind:-none}")" "$(json_escape "$seat")" \
"$(json_escape "$host")" "$(json_escape "$PWD")" \
>> "$spoolfile" 2>/dev/null
chmod 600 "$spoolfile" 2>/dev/null
fi
# Name the record only if one is actually on disk. Printing the path
# unconditionally sends the operator to a file that does not exist on exactly
# the hosts where the spool could not be created.
[ -s "$spoolfile" ] && spool_record="$spoolfile"
find "$spool" -maxdepth 1 -name '.spooled-*' -mmin +120 -delete 2>/dev/null
fi
while IFS= builtin read -r _diag_line; do builtin printf '%s\n' "$_diag_line" >&2; done <<EOF
git-credential-mosaic: REFUSED (fail-closed).
host : ${host}
identity : ${ident:-<unset>}${ident:+ (from ${ident_src}; resolved as a ${ident_kind})}
reason : ${reason}
EOF
if [ -n "$ident" ]; then
while IFS= builtin read -r _diag_line; do builtin printf '%s\n' "$_diag_line" >&2; done <<EOF
expected : ${idtok}
EOF
fi
while IFS= builtin read -r _diag_line; do builtin printf '%s\n' "$_diag_line" >&2; done <<EOF
${guidance}
EOF
if [ -n "$spool_record" ]; then
echo " record: ${spool_record}" >&2
else
echo " record: NOT WRITTEN — spool unavailable at ${spool}" >&2
fi
exit 1
}
# ── Bash environment injection guard (rev-code-02 R3, B1) ───────────────────
# Non-interactive bash sources $BASH_ENV at startup and imports exported
# functions from BASH_FUNC_* environment entries; either can define a read()
# or printf() that shadows the builtin the ancestry walker and diagnostics
# rely on — measured live by the reviewer's fixture (BASH_ENV read() rewrote
# every ancestry entry). A legitimate fleet seat environment carries neither
# (verified: zero BASH_FUNC_* in seat envs), so their presence in a helper
# request is an injection attempt: scrub the shadows first (so even the
# refusal machinery cannot be subverted), then refuse fail-closed.
# Imported functions are detected by ENUMERATION, not env-var names: bash
# consumes BASH_FUNC_* variables while importing the functions, so the
# environment no longer shows them (measured). At this point the script has
# defined exactly one function of its own (refuse); anything else in the
# function table arrived from the caller's environment. BASH_ENV is checked
# directly (it remains visible after sourcing).
_injected=0
_inj_names=""
while IFS=' ' builtin read -r _decl _kind _fn; do
[ -n "$_fn" ] || continue
case "$_fn" in
refuse) ;;
*) _injected=1; _inj_names="$_inj_names $_fn";;
esac
done < <(declare -F)
_inj_vars="${!BASH_FUNC_@}"
if [ -n "$_inj_vars" ]; then
_injected=1
for _iv in $_inj_vars; do
case "$_iv" in
BASH_FUNC_*%%) _ifn="${_iv#BASH_FUNC_}"; _ifn="${_ifn%%%}";;
BASH_FUNC_*) _ifn="${_iv#BASH_FUNC_}";;
*) _ifn="";;
esac
[ -n "$_ifn" ] && { unset -f "$_ifn" 2>/dev/null; _inj_names="$_inj_names $_ifn"; }
done
fi
if [ "$_injected" = 1 ] || [ -n "${BASH_ENV:-}" ]; then
while IFS=' ' builtin read -r _decl _kind _fn; do
[ "$_fn" = refuse ] || unset -f "$_fn" 2>/dev/null
done < <(declare -F)
unset BASH_ENV 2>/dev/null
reason="bash-environment-injection-refused"
refuse "The helper's bash startup state was externally shaped: BASH_ENV is
set and/or exported BASH_FUNC_* functions are present in the request
environment. Non-interactive bash sources BASH_ENV and imports those
functions BEFORE any script line runs, so builtins this helper's security
decisions rely on could be shadowed. Nothing resolves from a shaped request
environment. If this surprised a legitimate workflow, the caller environment
must be cleaned (no BASH_ENV, no exported functions) before invoking git."
fi
host=""; username_in=""
while IFS= builtin read -r line; do
[ -z "$line" ] && break
case "$line" in
host=*) host=${line#host=};;
username=*) username_in=${line#username=};;
esac
done
# Recognized Gitea hosts carry the per-identity token scheme. Anything else is
# declined quietly — another helper owns it, and refusing would break it.
case "$host" in
git.uscllc.com) idpfx=gitea-usc;;
git.mosaicstack.dev) idpfx=gitea-mosaicstack;;
*) exit 0;;
esac
# ── Clean-entrypoint assert (P0-SEC R4) ─────────────────────────────────────
# This implementation only runs behind the python entrypoint
# (git-credential-mosaic), which execve's it with an allowlist environment:
# no BASH_ENV, no imported functions, nothing shapable at bash startup. A
# direct invocation without the marker is a bypass attempt on that boundary
# and refuses. Placed after refuse() and the host parse so the refusal path
# exists when it fires (an earlier placement died on 'refuse: command not
# found' — the failure mode is real, keep this after every definition it
# calls).
if [ "${_MOSAIC_HELPER_CLEAN:-}" != "1" ]; then
reason="direct-entrypoint-refused"
refuse "This implementation refuses to run outside the production
entrypoint. git-credential-mosaic (the python wrapper in this directory)
execve's it with a hand-built, unshapable environment; invoking the .impl
directly bypasses that boundary. Credential requests go through git, which
invokes the wrapper named in gitconfig."
fi
ident="$MOSAIC_GIT_IDENTITY"; ident_src="MOSAIC_GIT_IDENTITY"
if [ -z "$ident" ]; then
ident=$(git config --get mosaic.gitIdentity 2>/dev/null)
ident_src="git config mosaic.gitIdentity"
fi
if [ -z "$ident" ]; then
ident="$username_in"
ident_src="the username git supplied"
fi
# ── Credential store selection ────────────────────────────────────────────────
# An identity is a SEAT or it is a SERVICE, and which one it is determines where
# its credential lives. There is no precedence rule between the two stores and no
# fallback from one to the other: a seat whose slot is empty fails closed rather
# than reading a service credential that happens to share its name.
#
# seat — <brain>/fleet/agents/<ident>/ exists
# credential at <brain>/fleet/agents/<ident>/secrets/<idpfx>-<ident>.token
# service — it does not
# credential at ~/.config/mosaic/secrets/gitea-tokens/<idpfx>-<ident>.token
#
# One credential, one location. Two copies of one credential diverge, and the
# stale copy fails in a way that reads as a revoked token rather than as drift.
#
# Brain-home resolution mirrors packages/mosaic/src/fleet/brain-home.ts and
# tools/fleet/start-agent-session.sh: MOSAIC_BRAIN_HOME wins, else ~/.mosaic.
brain_home="${MOSAIC_BRAIN_HOME:-$HOME/.mosaic}"
svc_store="$HOME/.config/mosaic/secrets/gitea-tokens"
# ── Caller-identity ownership (P5-RM-006) ──────────────────────────────────────
# A credential request is honourable only when the CALLER owns the identity it
# asks for. On a fleet host every seat shares one unix account, so the
# launcher-established MOSAIC_AGENT_NAME is the only attribution signal the
# helper has. Two measured paths made the old contract unsafe:
#
# - a seat exporting MOSAIC_GIT_IDENTITY=<another-seat> resolved that seat's
# token through the normal precedence chain (T97 G2, jarvis V2 probe), and
# - an anonymous caller (no seat name) inherited the host gitconfig's
# username=jarvis line and resolved jarvis's slot (T94: five watcher units
# flapping on exactly this class).
#
# Ownership rules, fail-closed on fleet hosts only; a host with no fleet keeps
# the legacy contract unchanged:
# 1. a SEAT caller may resolve only its own identity;
# 2. an anonymous caller may not resolve any SEAT identity (service
# identities remain available to non-seat automation such as CI).
# [P5-RM-006r1 ancestry binding begin]
# ── Caller identity from exec-frozen ancestry (rev-code-02 F1/F2) ───────────
# Walk /proc self->root collecting MOSAIC_AGENT_NAME from each ancestor's
# frozen environ. Rules:
# - any DISAGREEMENT (an ancestor value != the current value, or ancestors
# disagreeing among themselves) is a rewrite -> spoof-refused, nothing
# resolves. A child can inject variables downward but cannot alter an
# ancestor's exec-frozen environ, so the launcher-established value always
# participates in the comparison.
# - consensus (all ancestors that carry the var agree with the current env,
# or with each other when the current env is empty) -> caller = that value.
# - no ancestor carries it -> the current claim is unlineaged: caller is
# anonymous regardless of what the environment says. A name with no
# lineage is a claim, not an identity.
# The walk stops at PID 1, at a missing /proc entry, or INCLUSIVE at an
# ancestor that carries MOSAIC_CREDENTIAL_LINEAGE_FENCE with an EMPTY agent
# name — the test-suite lineage root. A fence beside a non-empty name is
# IGNORED and the walk continues, so an attacker cannot fence off the true
# ancestry by planting the marker next to a victim name.
trusted_caller() {
# PATH-HARDENED (rev-code-02 R1 F1): every /proc read below uses ONLY bash
# builtins (read/case/parameter expansion). The first implementation piped
# through PATH-resolved tr/sed/head/grep, and a caller that prepends hostile
# utilities to PATH in the same invocation that overrides the identity
# variables could forge the ancestry itself. Builtins cannot be shadowed.
local pid ppid v entry line fence
local -a vals=()
pid=$$
while :; do
v=""
fence=0
if [ -r "/proc/$pid/environ" ]; then
# Read inside a captured subshell whose stderr is closed: opening
# /proc/<pid>/environ can fail with EACCES on ancestors that are
# readable-by-mode but not openable (session managers), and that open
# failure prints from the SHELL, immune to loop-level 2>/dev/null
# (measured). The subshell makes the skip silent; NUL separators are
# converted to newlines for the parent's builtin parse.
_env_text=$( { while IFS= builtin read -r -d '' _e; do builtin printf '%s\n' "$_e"; done < "/proc/$pid/environ"; } 2>/dev/null )
while IFS= builtin read -r entry; do
[ -n "$entry" ] || continue
case "$entry" in
MOSAIC_AGENT_NAME=*) v="${entry#MOSAIC_AGENT_NAME=}";;
MOSAIC_CREDENTIAL_LINEAGE_FENCE=*) fence=1;;
esac
done <<EOF_ENV
$_env_text
EOF_ENV
fi
if [ "$pid" != "$$" ]; then
[ -n "$v" ] && vals+=("$v")
if [ "$fence" = 1 ] && [ -z "$v" ]; then
break
fi
fi
ppid=""
if [ -r "/proc/$pid/status" ]; then
while IFS= builtin read -r line; do
case "$line" in
PPid:*) ppid="${line#PPid:}"; ppid="${ppid//[[:space:]]/}";;
esac
done < "/proc/$pid/status"
fi
case "$ppid" in ''|0|1) break;; esac
pid=$ppid
done
local self="${MOSAIC_AGENT_NAME:-}" i consensus=""
if [ "${#vals[@]}" -gt 0 ]; then
consensus="${vals[0]}"
for i in "${vals[@]}"; do
if [ "$i" != "$consensus" ]; then
printf 'SPOOF'
return
fi
done
if [ -n "$self" ] && [ "$self" != "$consensus" ]; then
printf 'SPOOF'
return
fi
fi
printf '%s' "$consensus"
}
if [ -d "$brain_home/fleet/agents" ]; then
caller="$(trusted_caller)"
if [ "$caller" = "SPOOF" ]; then
reason="caller-identity-spoof-refused"
refuse "The MOSAIC_AGENT_NAME lineage disagrees within this process tree:
an ancestor established by exec carries a different value than the request.
A child process can rewrite its own environment but never an ancestor's
frozen environ, so disagreement is a rewrite, not a race. Nothing resolves
under a rewritten caller identity. If this surprised a legitimate workflow,
run git from the seat's own session, not from a rewritten environment."
fi
if [ -n "$caller" ] && [ -d "$brain_home/fleet/agents/$caller" ]; then
if [ -n "$ident" ] && [ "$ident" != "$caller" ]; then
reason="cross-seat-identity-refused"
refuse "A seat may resolve only its own credential slot. Caller seat is
'$caller' (ancestry-established); the request names '$ident'. Overriding
MOSAIC_GIT_IDENTITY (or a git config / URL username) to another seat's name is
exactly the path this refusal exists to close. If '$ident' auth is genuinely
required, that seat runs the operation itself or the orchestrator provisions
an explicit grant."
fi
else
# Anonymous caller on a fleet host (no lineage, or the lineage root is not
# a seat): NOTHING resolves — seat slots (T94 jarvis@ class) or legacy
# service credentials (rev-code-02 F2: credentialed services are seats;
# the legacy store is vestigial and not anonymously reachable).
if [ -n "$ident" ]; then
ident_kind="${ident_kind:-}"
[ -d "$brain_home/fleet/agents/$ident" ] && ident_kind="seat" || ident_kind="service identity"
reason="anonymous-credential-refused"
refuse "This caller has no seat lineage on a fleet host and asked for
'$ident' (a ${ident_kind}). Anonymous callers resolve nothing on fleet hosts:
seat credentials must never serve an unattributable caller, and credentialed
services are seats with their own sessions (the legacy service store is
vestigial). Run from the owning seat's session."
fi
fi
fi
# [P5-RM-006r1 ancestry binding end]
idtok=""; ident_kind=""
if [ -n "$ident" ]; then
if [ -d "$brain_home/fleet/agents/$ident" ]; then
ident_kind="seat"
idtok="$brain_home/fleet/agents/$ident/secrets/${idpfx}-${ident}.token"
else
ident_kind="service identity"
idtok="$svc_store/${idpfx}-${ident}.token"
fi
if [ -r "$idtok" ]; then
# P5-RM-006 seat permissions: a SEAT slot readable by group or other is
# provisioning drift, and on a single-account fleet host it widens every
# seat's exposure at once. Refuse rather than serve from a loose slot; the
# record names the path so the provisioning fix is one chmod away.
# Scoped to seat slots: the framework service store is operator-managed
# and outside this work unit's permission surface.
if [ "${ident_kind:-}" = "seat" ]; then
# command -p resolves stat from the POSIX default PATH (system
# directories), never the caller's PATH (rev-code-02 R3 B2: a shadowed
# stat reported a 0644 slot as 600 and the helper served it). Output is
# shape-validated: anything that is not 3-4 octal digits refuses.
slot_mode="$(command -p stat -c '%a' "$idtok" 2>/dev/null || true)"
case "$slot_mode" in
[0-7][0-7][0-7]|[0-7][0-7][0-7][0-7]) ;;
*) slot_mode="unverifiable";;
esac
if [ "${slot_mode:1:2}" != "00" ]; then
reason="slot-permission-violation"
refuse "Slot $idtok has mode ${slot_mode:-unknown}; expected owner-only
(0600 or stricter). Tighten it: chmod 600 '$idtok'. This refusal is the seat
permissions half of P5-RM-006: a loose slot on a shared-account host is every
seat's exposure, so the helper declines to serve from it. Mode inspection uses
command -p (trusted PATH) and fails closed on unverifiable output."
fi
fi
echo "username=${ident}"
echo "password=$(<"$idtok")"
exit 0
fi
fi
# ── Shared-account fallback: ONLY on a host with no fleet and no identity ──────
# `fleet/agents` existing is the same signal brain-home.ts uses to decide a brain
# is active. Where there are seats, records must be attributable, so an
# unresolvable request is refused instead of borrowing the shared account.
fleet_present=0
[ -d "$brain_home/fleet/agents" ] && fleet_present=1
if [ -z "$ident" ] && [ "$fleet_present" -eq 0 ]; then
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../_lib/credentials.sh
source "$script_dir/../_lib/credentials.sh"
load_credentials "$idpfx" >/dev/null 2>&1 || exit 0
# GITEA_USER is not populated by load_credentials (it exports GITEA_URL and
# GITEA_TOKEN only). Gitea's git-over-HTTP auth authenticates from the token in
# the password field, not from the username string, so any non-empty
# placeholder works — deliberately NOT a real account name, since framework
# files stay operator-agnostic (tools/quality/scripts/verify-sanitized.sh).
echo "username=${GITEA_USER:-git}"
echo "password=$GITEA_TOKEN"
exit 0
fi
# ── FAIL CLOSED ───────────────────────────────────────────────────────────────
# The escalation RECORD is durable and unconditional; any notification built on
# top of it is best-effort (see refuse()). Record and alert are deduplicated
# separately — a cap on the alert alone lets the spool grow without bound
# exactly while the operator is being told nothing, so the louder the failure
# the quieter it gets.
if [ -z "$ident" ]; then
reason="no-identity"
else
reason="no-token-for-identity"
fi
refuse "No per-identity credential resolved. This helper does NOT fall back to the shared
account: that fallback makes every record it creates attributable to one
principal, which is unrecoverable once a pull request has merged under it.
Fix (pick one):
export MOSAIC_GIT_IDENTITY=<agent-id> # process-scoped
git config mosaic.gitIdentity <agent-id> # per-repo/worktree, persists
Then provision that identity's credential at the path named above. An identity
with a directory under \${MOSAIC_BRAIN_HOME:-\$HOME/.mosaic}/fleet/agents/ is a
seat and is read ONLY from its own secrets/ slot; any other identity is read from
~/.config/mosaic/secrets/gitea-tokens/. There is no fallback between the two.
If this identity legitimately needs git access and has none, ask the orchestrator
to provision one."