fix(framework): detect-platform get_gitea_token fail-loud on absent per-slot token (Patch 2b)
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
The per-agent identity resolution added to get_gitea_token() (Patch 2 / #873) resolves an explicit identity (MOSAIC_GIT_IDENTITY env, or git config mosaic.gitIdentity) and returns that identity's per-slot Gitea token when present. Gap: when the identity resolves but its per-slot token file is ABSENT for a recognized Gitea host, the function fell through to the shared/default credential-loader token instead of failing. API tooling (pr-create.sh, issue-create.sh, pr-review.sh) then silently posted the PR/issue/review as the WRONG agent — e.g. a reviewer seat's Gate-16 review getting attributed to the shared/default identity — corrupting author≠reviewer separation while also masking the missing-token misconfiguration. This surfaced most often on the tea-stale API-fallback path, which is exactly when tooling leans on get_gitea_token. Fix: in the step-0 explicit-identity branch, when the per-slot token for that identity is absent on a recognized Gitea host, print a stderr diagnostic naming the identity, its source (env vs git config), the host, and the expected token path, then return 1 instead of falling through. All three callers already `return 1` on a nonzero get_gitea_token, so fail-loud propagates with zero caller edits. Scope (deliberate, minimal blast radius): explicit-identity-only. Plain `git config user.name` is not an identity trigger — only MOSAIC_GIT_IDENTITY / git config mosaic.gitIdentity count, so ordinary shared/human repo usage is unaffected. Recognized-Gitea-hosts-only: unrecognized hosts have no per-slot token scheme, so identity-set + unknown-host still falls through unchanged (no fail-loud). The MOSAIC_STRICT_IDENTITY opt-in discussed as a possible future extension (gating the full `... > git username` chain) is deliberately NOT part of this patch — deferred per spec as a later, separate proposal. Red-first: stashed the source fix, ran the extended test-gitea-token-identity.sh — 16 assertions failed exactly as expected (shared token leaked, no fail-loud diagnostic). Restored the fix — all green, full test:framework-shell chain passes. Backward compat verified: the no-identity-requested case still returns the shared token unchanged. Extends test-gitea-token-identity.sh: the no-per-slot-token case (both identity sources: env and git config) on a recognized host now asserts nonzero return + empty stdout + stderr diagnostic naming identity/source/host/expected path, instead of asserting a shared-token fallback. Adds a same-identity cross-host case (token exists for one host, absent for another → fail-loud on the host lacking it, no cross-host token leak) and a scope-containment case (identity set + unrecognized host → existing fall-through behavior unchanged, fail-loud diagnostic does not fire). Gates: shellcheck clean on both changed files, sanitization gate (verify-sanitized.sh) passes, full test:framework-shell chain green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0158NZqN2n2ymKFeJAZ4GUCb
This commit is contained in:
@@ -10,10 +10,19 @@
|
||||
# 2. Correct per-slot token file path chosen per host
|
||||
# (gitea-usc-<id>.token vs gitea-mosaicstack-<id>.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
|
||||
|
||||
Reference in New Issue
Block a user