From 2101c9b4468b22f57b2f02bb2c9da12067225819 Mon Sep 17 00:00:00 2001 From: fred Date: Fri, 11 Sep 2026 22:50:23 +0000 Subject: [PATCH] fix(git-tools): issue-comment.sh resolves API base without monolith GITEA_URL (#1502) --- .../framework/tools/git/issue-comment.sh | 11 +- .../tools/git/test-issue-comment-readback.sh | 100 +++++++++++++++++- 2 files changed, 108 insertions(+), 3 deletions(-) diff --git a/packages/mosaic/framework/tools/git/issue-comment.sh b/packages/mosaic/framework/tools/git/issue-comment.sh index 3f4b3d19..08fe963d 100755 --- a/packages/mosaic/framework/tools/git/issue-comment.sh +++ b/packages/mosaic/framework/tools/git/issue-comment.sh @@ -155,8 +155,15 @@ gitea_resolve_api_for_login() { } fi configured_url=$(get_gitea_url_for_host "$host") || { - echo "Error: Configured Gitea URL not found for comment read-back verification" >&2 - return 1 + # No monolith-configured Gitea URL for this host (#1450): seat-token-only + # hosts carry no gitea-mosaicstack/gitea-usc credentials.sh entry and no + # bare GITEA_URL, so get_gitea_url_for_host has nothing to match against. + # Synthesize the API base directly from the git remote's own host -- + # exactly the trust model issue-create.sh's REST fallback already uses + # successfully on these hosts. This is NOT a cross-host guess: $host came + # from get_remote_host() reading THIS repo's own origin remote, so the + # resolved base always matches the repo actually being acted on. + configured_url="https://${host}" } repo=$(get_gitea_repo_slug_for_url "$configured_url") || { echo "Error: Could not resolve Gitea owner/repository relative to configured URL" >&2 diff --git a/packages/mosaic/framework/tools/git/test-issue-comment-readback.sh b/packages/mosaic/framework/tools/git/test-issue-comment-readback.sh index 754dcc60..d3a97ef7 100755 --- a/packages/mosaic/framework/tools/git/test-issue-comment-readback.sh +++ b/packages/mosaic/framework/tools/git/test-issue-comment-readback.sh @@ -44,6 +44,14 @@ # clobber each other and every scratch file is removed on all exit paths. # 11. accepts the canonical -b/--body flag exactly like the -c/--comment alias # (R1, 2026-08-28): a full verified write via -b alone. +# 12. (#1450, 2026-09-11) on a SEAT-TOKEN-ONLY host — identity resolved purely +# via MOSAIC_GIT_IDENTITY's per-slot token file, no tea login involved, and +# no monolith credentials.json entry for this Gitea host (so +# get_gitea_url_for_host has nothing to match) — the wrapper still +# resolves the API base directly from the git remote's own host (no +# cross-host fallback/guessing) instead of failing closed with +# "Configured Gitea URL not found", and the POST + exact-ID read-back both +# run under that same seat identity, never the (absent) host default. set -euo pipefail @@ -129,6 +137,12 @@ OVERRIDE_TOKEN="override-token-placeholder" # repo host: host-bound selection must fail closed on the host mismatch. CROSS_HOST_LOGIN="foreign-host-reviewer" CROSS_HOST_TOKEN="cross-host-token-placeholder" +# A seat-token-only identity (#1450): resolved purely via MOSAIC_GIT_IDENTITY's +# per-slot token file under $HOME/.config/mosaic/secrets/gitea-tokens/ -- no tea +# login, no MOSAIC_CREDENTIALS_FILE entry for this host at all. +SEAT_IDENTITY="seat-only-agent" +SEAT_LOGIN="seat-only-actor" +SEAT_TOKEN="seat-token-placeholder" # tea config: the override login has its own token here (as tea itself stores # per-login tokens). The default login name ("mosaicstack") is deliberately NOT @@ -169,6 +183,19 @@ with open(sys.argv[1], "w", encoding="utf-8") as credentials: }, credentials) PY +# A monolith credentials file that EXISTS but carries no gitea.mosaicstack (or +# gitea.usc) entry -- the seat-token-only condition (#1450). Distinct from +# $CREDENTIALS_FILE above, which does carry a configured URL for the other +# cases in this suite. +EMPTY_CREDENTIALS_FILE="$WORK_DIR/credentials-empty.json" +printf '{}' > "$EMPTY_CREDENTIALS_FILE" + +# The seat identity's per-slot token file, exactly as a provisioned agent seat +# carries one: $HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-.token. +# get_gitea_token() resolves this BEFORE ever consulting MOSAIC_CREDENTIALS_FILE. +mkdir -p "$HOME_DIR/.config/mosaic/secrets/gitea-tokens" +printf '%s' "$SEAT_TOKEN" > "$HOME_DIR/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-${SEAT_IDENTITY}.token" + # tea stub: only ever answers the login list (used to resolve the default login # name). It must NEVER be asked to write a comment — the wrapper writes via REST. cat > "$BIN_DIR/tea" <<'SH' @@ -250,6 +277,7 @@ case "$auth_token" in "$ISSUE_COMMENT_DEFAULT_TOKEN") acting_identity="$ISSUE_COMMENT_ACTING_LOGIN" ;; "$ISSUE_COMMENT_OVERRIDE_TOKEN") acting_identity="$ISSUE_COMMENT_OVERRIDE_LOGIN" ;; "$ISSUE_COMMENT_CROSS_HOST_TOKEN") acting_identity="$ISSUE_COMMENT_CROSS_HOST_LOGIN" ;; + "$ISSUE_COMMENT_SEAT_TOKEN") acting_identity="$ISSUE_COMMENT_SEAT_LOGIN" ;; esac printf '%s %s %s\n' "$method" "$path" "${acting_identity:-}" >> "$ISSUE_COMMENT_AUTH_LOG" @@ -446,6 +474,53 @@ run_comment() { ISSUE_COMMENT_DEFAULT_TOKEN="$DEFAULT_TOKEN" \ ISSUE_COMMENT_OVERRIDE_TOKEN="$OVERRIDE_TOKEN" \ ISSUE_COMMENT_CROSS_HOST_TOKEN="$CROSS_HOST_TOKEN" \ + ISSUE_COMMENT_SEAT_LOGIN="$SEAT_LOGIN" \ + ISSUE_COMMENT_SEAT_TOKEN="$SEAT_TOKEN" \ + ISSUE_COMMENT_REPO_SLUG="$REPO_SLUG" \ + ISSUE_COMMENT_API_BASE="$API_BASE" \ + ISSUE_COMMENT_API_ROOT="$API_ROOT" \ + "$SCRIPT_DIR/issue-comment.sh" -i "$ISSUE_NUMBER" "${BODY_FLAG:--c}" "$BODY" "$@" + ) > "$OUTPUT_FILE" 2>&1 +} + +# Seat-token-only variant (#1450): no monolith credentials.json entry for this +# host at all (MOSAIC_CREDENTIALS_FILE points at an empty {}), and identity +# resolves purely via MOSAIC_GIT_IDENTITY's per-slot token file. Everything +# else is identical to run_comment() -- same sandboxing, same always-exported +# constants -- so a diff against run_comment() is exactly these two overrides. +run_comment_seat() { + local mode="$1" + shift + : > "$TEA_LOG" + : > "$CURL_LOG" + : > "$CURL_ARGV_LOG" + : > "$AUTH_LOG" + : > "$OUTPUT_FILE" + seed_state "$mode" + ( + cd "$REPO_DIR" + PATH="$BIN_DIR:$PATH" \ + TMPDIR="$TMP_SCRATCH" \ + HOME="$HOME_DIR" \ + XDG_CONFIG_HOME="$XDG_DIR" \ + MOSAIC_CREDENTIALS_FILE="$EMPTY_CREDENTIALS_FILE" \ + MOSAIC_GIT_IDENTITY="$SEAT_IDENTITY" \ + MOSAIC_BRAIN_HOME="" \ + ISSUE_COMMENT_TEA_LOG="$TEA_LOG" \ + ISSUE_COMMENT_CURL_LOG="$CURL_LOG" \ + ISSUE_COMMENT_CURL_ARGV_LOG="$CURL_ARGV_LOG" \ + ISSUE_COMMENT_AUTH_LOG="$AUTH_LOG" \ + ISSUE_COMMENT_STATE="$STATE_FILE" \ + ISSUE_COMMENT_TEST_MODE="$mode" \ + ISSUE_COMMENT_ACTING_LOGIN="$ACTING_LOGIN" \ + ISSUE_COMMENT_FOREIGN_LOGIN="$FOREIGN_LOGIN" \ + ISSUE_COMMENT_OVERRIDE_LOGIN="$OVERRIDE_LOGIN" \ + ISSUE_COMMENT_CROSS_HOST_LOGIN="$CROSS_HOST_LOGIN" \ + ISSUE_COMMENT_DEFAULT_TOKEN="$DEFAULT_TOKEN" \ + ISSUE_COMMENT_OVERRIDE_TOKEN="$OVERRIDE_TOKEN" \ + ISSUE_COMMENT_CROSS_HOST_TOKEN="$CROSS_HOST_TOKEN" \ + ISSUE_COMMENT_SEAT_LOGIN="$SEAT_LOGIN" \ + ISSUE_COMMENT_SEAT_TOKEN="$SEAT_TOKEN" \ ISSUE_COMMENT_REPO_SLUG="$REPO_SLUG" \ ISSUE_COMMENT_API_BASE="$API_BASE" \ ISSUE_COMMENT_API_ROOT="$API_ROOT" \ @@ -473,7 +548,7 @@ assert_no_temp_leak() { # expected path grep matches nothing, so no token value is ever printed. assert_token_not_in_argv() { local context="$1" - if grep -qF -e "$DEFAULT_TOKEN" -e "$OVERRIDE_TOKEN" -e "$CROSS_HOST_TOKEN" "$CURL_ARGV_LOG"; then + if grep -qF -e "$DEFAULT_TOKEN" -e "$OVERRIDE_TOKEN" -e "$CROSS_HOST_TOKEN" -e "$SEAT_TOKEN" "$CURL_ARGV_LOG"; then echo "FAIL: a Gitea bearer token leaked into curl argv ($context)" >&2 exit 1 fi @@ -650,4 +725,27 @@ assert_no_temp_leak "fresh-success-body-flag" assert_token_not_in_argv "fresh-success-body-flag" unset BODY_FLAG +# Case 12 (#1450, 2026-09-11): a SEAT-TOKEN-ONLY host -- no monolith +# credentials.json entry for this Gitea host at all (get_gitea_url_for_host has +# nothing to match), identity resolved purely via MOSAIC_GIT_IDENTITY's +# per-slot token file. The wrapper must still resolve the API base directly +# from the git remote's own host (no cross-host fallback/guessing -- proven by +# reusing this suite's existing $API_BASE/$API_ROOT constants unmodified) and +# run the POST, the /user lookup, and the exact-id read-back all under the +# seat identity, never a host-default identity that does not even exist here. +run_comment_seat fresh-success +grep -q 'Added and verified comment on Gitea issue #7 (comment ID 51)' "$OUTPUT_FILE" +grep -q "^POST $API_BASE/issues/7/comments$" "$CURL_LOG" +grep -q "^GET $API_BASE/issues/comments/51$" "$CURL_LOG" +grep -q "^POST $API_BASE/issues/7/comments $SEAT_LOGIN$" "$AUTH_LOG" +grep -q "^GET $API_ROOT/user $SEAT_LOGIN$" "$AUTH_LOG" +grep -q "^GET $API_BASE/issues/comments/51 $SEAT_LOGIN$" "$AUTH_LOG" +if grep -q " $ACTING_LOGIN\$" "$AUTH_LOG"; then + echo "FAIL: seat-token-only run was attributed to the (nonexistent) host-default identity" >&2 + cat "$AUTH_LOG" >&2 + exit 1 +fi +assert_no_temp_leak "seat-token-no-monolith" +assert_token_not_in_argv "seat-token-no-monolith" + echo "issue-comment.sh REST create + exact-id read-back regression passed"