From 3c7890f17fac4abba30edc1ba85a5e6e953c8c98 Mon Sep 17 00:00:00 2001 From: "jason.woltje" Date: Sat, 25 Jul 2026 20:51:19 +0000 Subject: [PATCH] fix(framework): detect-platform get_gitea_token fail-loud on absent per-slot token (Patch 2b) (#890) Co-authored-by: jason.woltje Co-committed-by: jason.woltje --- .../framework/tools/git/detect-platform.sh | 15 ++- .../tools/git/test-gitea-token-identity.sh | 119 ++++++++++++++++-- 2 files changed, 121 insertions(+), 13 deletions(-) diff --git a/packages/mosaic/framework/tools/git/detect-platform.sh b/packages/mosaic/framework/tools/git/detect-platform.sh index e12f9d40..97c6e5b3 100755 --- a/packages/mosaic/framework/tools/git/detect-platform.sh +++ b/packages/mosaic/framework/tools/git/detect-platform.sh @@ -511,7 +511,11 @@ get_gitea_token() { # (pr-create, issue-create, …) authors under the right identity — matching the # git credential helper. Backward-compatible: nothing resolvable → shared logic below. local _ident="${MOSAIC_GIT_IDENTITY:-}" - [[ -z "$_ident" ]] && _ident="$(git config --get mosaic.gitIdentity 2>/dev/null || true)" + local _ident_src="MOSAIC_GIT_IDENTITY" + if [[ -z "$_ident" ]]; then + _ident="$(git config --get mosaic.gitIdentity 2>/dev/null || true)" + _ident_src="git config mosaic.gitIdentity" + fi if [[ -n "$_ident" ]]; then local _idpfx="" case "$host" in @@ -524,6 +528,15 @@ get_gitea_token() { cat "$_idtok" return 0 fi + # FAIL LOUD: an explicit git identity was requested for a recognized Gitea host, + # but no per-slot token exists for THAT identity. Refuse to fall through to the + # shared/default credential loader below — silently borrowing another slot's token + # would post PRs/issues/reviews under the WRONG agent (e.g. rev2's review attributed + # to coder3), corrupting Gate-16 author≠reviewer separation. Hard-stop instead so the + # caller aborts loudly rather than acting as the wrong identity. + echo "Error: git identity '$_ident' requested (via $_ident_src) for host '$host', but no per-slot token at $_idtok." >&2 + echo " Refusing to borrow another slot's token. Provision the per-slot token, or unset the identity to use shared credentials." >&2 + return 1 fi fi diff --git a/packages/mosaic/framework/tools/git/test-gitea-token-identity.sh b/packages/mosaic/framework/tools/git/test-gitea-token-identity.sh index a9875d94..d56e7bee 100755 --- a/packages/mosaic/framework/tools/git/test-gitea-token-identity.sh +++ b/packages/mosaic/framework/tools/git/test-gitea-token-identity.sh @@ -10,10 +10,19 @@ # 2. Correct per-slot token file path chosen per host # (gitea-usc-.token vs gitea-mosaicstack-.token). # 3. Per-slot token present -> that token is returned (agent-authored calls). -# 4. Per-slot token absent -> falls back to the shared credential-loader -# token (backward-compat / no-op for hosts without per-slot tokens). -# 5. Unrelated host with no shared credentials configured -> failure -# (unchanged, existing behavior). +# 4. No identity requested -> shared credential-loader token (backward +# compat, unchanged). +# 5. Patch 2b — explicit identity + recognized Gitea host + ABSENT per-slot +# token for that identity -> FAIL LOUD (nonzero return, empty stdout, a +# stderr diagnostic naming identity/source/host/expected path). Must NOT +# fall through to the shared/default token (Gate-16 author≠reviewer +# integrity — never silently borrow another slot's credentials). Covered +# for both identity sources (git config, MOSAIC_GIT_IDENTITY env) and +# for a same-identity cross-host case (token exists for one host, not +# the other). +# 6. Scope containment: identity requested + an UNRECOGNIZED Gitea host (no +# per-slot token scheme) -> Patch 2b does not apply; existing +# fall-through behavior is unchanged. # # Uses a stubbed credentials.json + stubbed per-slot token files under a fake # HOME. NEVER reads real secrets or touches the real ~/.config/mosaic/secrets. @@ -95,24 +104,110 @@ out=$(call_get_gitea_token "git.mosaicstack.dev" MOSAIC_GIT_IDENTITY=agentB) assert_eq "env beats git-config identity token" "agentB-mosaicstack-token" "$out" # --------------------------------------------------------------------------- -# 4. Identity resolves but has no per-slot token for THIS host -> falls back -# to the shared token (per-agent identity is opt-in per host). +# 4. FAIL LOUD (Patch 2b): an identity is explicitly requested (via git config +# mosaic.gitIdentity, and separately via MOSAIC_GIT_IDENTITY env) for a +# RECOGNIZED Gitea host, but no per-slot token exists for THAT identity. +# Must NOT fall through to the shared/default token — silently borrowing +# another slot's credentials would post PRs/issues/reviews as the WRONG +# agent (Gate-16 author≠reviewer integrity break). Expect: nonzero return, +# EMPTY stdout (no token — shared or otherwise — leaked), and a stderr +# diagnostic naming the identity, its source, the host, and the expected +# per-slot token path. # --------------------------------------------------------------------------- +assert_failloud() { + local desc="$1" host="$2" ident="$3" expected_tok_path="$4"; shift 4 + local stderr_file="$WORK_DIR/stderr.tmp" + : > "$stderr_file" + set +e + local stdout + stdout=$(call_get_gitea_token "$host" "$@" 2>"$stderr_file") + local rc=$? + set -e + local stderr + stderr=$(cat "$stderr_file") + if [[ "$rc" -eq 0 ]]; then + echo "FAIL: $desc — expected nonzero return, got 0 (stdout='$stdout')" >&2 + fail=1 + fi + if [[ -n "$stdout" ]]; then + echo "FAIL: $desc — expected empty stdout (no token leaked), got '$stdout'" >&2 + fail=1 + fi + if [[ "$stderr" != *"$ident"* ]]; then + echo "FAIL: $desc — stderr does not name the requested identity '$ident':" >&2 + echo "$stderr" >&2 + fail=1 + fi + if [[ "$stderr" != *"$host"* ]]; then + echo "FAIL: $desc — stderr does not name the host '$host':" >&2 + echo "$stderr" >&2 + fail=1 + fi + if [[ "$stderr" != *"$expected_tok_path"* ]]; then + echo "FAIL: $desc — stderr does not name the expected per-slot token path '$expected_tok_path':" >&2 + echo "$stderr" >&2 + fail=1 + fi + if [[ "$stderr" == *"shared"*"token"* ]]; then + echo "FAIL: $desc — stderr unexpectedly mentions a shared token value:" >&2 + echo "$stderr" >&2 + fail=1 + fi +} + +# 4a. git config mosaic.gitIdentity source, recognized host (mosaicstack), +# shared token IS present but must not be borrowed. git -C "$REPO_DIR" config mosaic.gitIdentity no-such-agent -out=$(call_get_gitea_token "git.mosaicstack.dev") -assert_eq "no per-slot token falls back to shared" "shared-mosaicstack-token" "$out" +assert_failloud "fail-loud via git-config identity (recognized host)" \ + "git.mosaicstack.dev" "no-such-agent" \ + "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-no-such-agent.token" +git -C "$REPO_DIR" config --unset mosaic.gitIdentity + +# 4b. MOSAIC_GIT_IDENTITY env source (takes priority over git config), same +# recognized-host / absent-token scenario -> also fails loud. +assert_failloud "fail-loud via MOSAIC_GIT_IDENTITY env (recognized host)" \ + "git.mosaicstack.dev" "no-such-agent-env" \ + "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-no-such-agent-env.token" \ + MOSAIC_GIT_IDENTITY=no-such-agent-env # --------------------------------------------------------------------------- # 5. Correct per-slot token PATH per host: same agent id, only a usc token -# exists -> usc host returns it, mosaicstack host must NOT leak it and -# instead falls back to the shared mosaicstack token. +# exists. usc host returns it (happy path, unchanged). mosaicstack host +# has NO per-slot token for this identity -> Patch 2b fail-loud applies +# there too (must NOT fall back to the shared mosaicstack token, and must +# NOT leak the agent's usc token either). # --------------------------------------------------------------------------- echo -n "agentD-usc-token" > "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-usc-agentD.token" git -C "$REPO_DIR" config mosaic.gitIdentity agentD out=$(call_get_gitea_token "git.uscllc.com") assert_eq "host-scoped token path (usc)" "agentD-usc-token" "$out" -out=$(call_get_gitea_token "git.mosaicstack.dev") -assert_eq "host-scoped token path (no cross-host leak)" "shared-mosaicstack-token" "$out" +assert_failloud "fail-loud on cross-host absence (no fallback, no cross-host leak)" \ + "git.mosaicstack.dev" "agentD" \ + "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentD.token" +git -C "$REPO_DIR" config --unset mosaic.gitIdentity + +# --------------------------------------------------------------------------- +# 6. Scope containment: identity explicitly requested, but the host is NOT a +# recognized Gitea host (no per-slot token scheme at all) -> Patch 2b does +# NOT apply; existing fall-through behavior is unchanged (ends in the +# pre-existing generic failure since no shared credentials match either, +# NOT the fail-loud diagnostic path). +# --------------------------------------------------------------------------- +git -C "$REPO_DIR" config mosaic.gitIdentity no-such-agent +set +e +out=$(call_get_gitea_token "github.com" 2>"$WORK_DIR/stderr-scope.tmp") +rc=$? +set -e +err=$(cat "$WORK_DIR/stderr-scope.tmp") +if [[ "$rc" -eq 0 ]]; then + echo "FAIL: unrecognized host + identity — expected nonzero (no credentials configured), got 0" >&2 + fail=1 +fi +if [[ "$err" == *"no per-slot token at"* ]]; then + echo "FAIL: unrecognized host + identity — fail-loud diagnostic must not fire for a host with no per-slot scheme:" >&2 + echo "$err" >&2 + fail=1 +fi git -C "$REPO_DIR" config --unset mosaic.gitIdentity if [[ "$fail" -eq 0 ]]; then