From 9d2b12ca71472a57c52e0d58aa37723a198354d5 Mon Sep 17 00:00:00 2001 From: ms-lead-reviewer Date: Thu, 23 Jul 2026 11:43:11 -0500 Subject: [PATCH] feat(tools/git): per-agent Gitea identity (git-credential-mosaic + get_gitea_token) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an opt-in per-agent Gitea identity so a fleet agent can push/commit/open PRs under its own token instead of the single shared account, giving cryptographic author-!=-reviewer separation (Gate-16). Resolution priority (both tools): MOSAIC_GIT_IDENTITY env > git config mosaic.gitIdentity (per-worktree, persists on disk) > git-supplied username (git-credential-mosaic only). If the resolved identity has a token file at ~/.config/mosaic/secrets/gitea-tokens/gitea-{usc,mosaicstack}-.token, that identity is used; otherwise both tools fall through to the existing shared-account path unchanged, so this is a no-op on any host without per-slot tokens configured. - tools/git/git-credential-mosaic: new git credential helper (get verb). - tools/git/detect-platform.sh: get_gitea_token() gains the same identity resolution, prepended ahead of the existing shared-token logic, so API tooling (pr-create.sh, issue-create.sh, ...) authors under the same identity as git push/fetch. - install.sh: explicit chmod +x for git-credential-mosaic (it ships without a .sh suffix, so the existing *.sh glob does not cover it); tools/** is already framework-owned so the file syncs automatically. - tools/git/README.md: documents the feature, the one-time `git config credential.helper` registration step (deliberately not auto-wired — see README for why), and the PowerShell-parity decision (detect-platform.ps1 authenticates via tea logins, not a raw-token function, so there is nothing to port there). - tools/git/test-git-credential-mosaic.sh, tools/git/test-gitea-token-identity.sh: new regression harnesses (red verified against the pre-patch code) covering identity-resolution priority, per-host token path selection, and shared-account fallback. Wired into package.json's test:framework-shell. Upstreams Mos host-local tooling-patch kit (2026-07-23), Patches 1+2. Closes #873 Co-Authored-By: Claude Opus 4.8 --- packages/mosaic/framework/install.sh | 4 + packages/mosaic/framework/tools/git/README.md | 61 +++++++ .../framework/tools/git/detect-platform.sh | 22 +++ .../framework/tools/git/git-credential-mosaic | 69 ++++++++ .../tools/git/test-git-credential-mosaic.sh | 161 ++++++++++++++++++ .../tools/git/test-gitea-token-identity.sh | 122 +++++++++++++ packages/mosaic/package.json | 2 +- 7 files changed, 440 insertions(+), 1 deletion(-) create mode 100644 packages/mosaic/framework/tools/git/git-credential-mosaic create mode 100644 packages/mosaic/framework/tools/git/test-git-credential-mosaic.sh create mode 100644 packages/mosaic/framework/tools/git/test-gitea-token-identity.sh diff --git a/packages/mosaic/framework/install.sh b/packages/mosaic/framework/install.sh index 612b0027..d0d8df5b 100755 --- a/packages/mosaic/framework/install.sh +++ b/packages/mosaic/framework/install.sh @@ -639,6 +639,10 @@ reconcile_framework_files # Ensure tool scripts are executable find "$TARGET_DIR/tools" -name "*.sh" -exec chmod +x {} + 2>/dev/null || true find "$TARGET_DIR/tools/_scripts" -type f -exec chmod +x {} + 2>/dev/null || true +# git-credential-mosaic (per-agent Gitea identity helper) ships without a .sh +# suffix — git resolves credential helpers by exact name/path, not extension — +# so the *.sh glob above does not cover it; chmod it explicitly. +[[ -f "$TARGET_DIR/tools/git/git-credential-mosaic" ]] && chmod +x "$TARGET_DIR/tools/git/git-credential-mosaic" 2>/dev/null || true ok "Framework synced to $TARGET_DIR" diff --git a/packages/mosaic/framework/tools/git/README.md b/packages/mosaic/framework/tools/git/README.md index c463de14..63451e65 100644 --- a/packages/mosaic/framework/tools/git/README.md +++ b/packages/mosaic/framework/tools/git/README.md @@ -7,3 +7,64 @@ These scripts provide host-aware GitHub and Gitea issue, pull-request, milestone A successful provider write command—or a wrapper message based only on that command's exit code—is **not** durable review provenance. Review comments count as durable provenance only after the wrapper reads the created provider record back and verifies that it belongs to the intended repository and pull request and contains the exact submitted body (or verifies the provider-returned record ID). `pr-review.sh` therefore fails closed when a Gitea comment cannot be written, its created comment ID cannot be identified, or provider read-back does not match. It reports comment success only after that read-back verification passes. + +## Per-agent Gitea identity (Gate-16 author≠reviewer) + +By default, git push/fetch (via `git-credential-mosaic`) and the API wrappers above (via +`detect-platform.sh`'s `get_gitea_token`) all authenticate as the single shared Gitea +account/token configured through `tools/_lib/credentials.sh`. That means every agent in a +fleet commits, pushes, and opens PRs under one identity — with no cryptographic +separation between an author and a reviewer. + +Both `git-credential-mosaic` and `get_gitea_token()` resolve an optional **per-agent +identity** before falling back to the shared account: + +1. `MOSAIC_GIT_IDENTITY` environment variable, or +2. `git config --get mosaic.gitIdentity` (set per-worktree; persists on disk across + non-persistent shells — `git config mosaic.gitIdentity `), or +3. (git-credential-mosaic only) the username git itself supplies for the credential + request. + +If the resolved identity has a token file at +`~/.config/mosaic/secrets/gitea-tokens/gitea-{usc,mosaicstack}-.token`, that +identity + token is used. **Nothing configured → nothing changes**: with no per-slot +token file present, both tools fall through to the existing shared-account path +unchanged, so this feature is a no-op on any host that hasn't provisioned per-slot +tokens. + +### Enabling it for a clone + +The framework installer syncs `git-credential-mosaic` to +`~/.config/mosaic/tools/git/git-credential-mosaic` (executable) on every install/update, +but does **not** register it as git's credential helper automatically. Registration is a +one-time, explicit step: + +```bash +# Per-repo (recommended — scopes the helper to this clone only): +git config credential.helper "$HOME/.config/mosaic/tools/git/git-credential-mosaic" + +# Per-worktree identity pin (Gate-16 separation): +git config mosaic.gitIdentity +``` + +This is deliberately **not** auto-registered on install/update: `credential.helper` is +global, order-sensitive git config (`~/.gitconfig`) that can already hold an +operator-chosen credential manager (keychain, `store`, `manager-core`, …) for +repositories unrelated to Mosaic. Silently inserting an entry on every framework +install/upgrade risks reordering or shadowing that operator-owned surface across the +whole host — the same operator-owned config the installer's manifest system is +otherwise careful never to touch. Because identity is already resolved per-worktree +(`mosaic.gitIdentity`), the correct granularity for registering the helper is per-clone +too, so a documented manual step is the right shape here, not a global auto-write. + +### PowerShell parity + +`detect-platform.ps1`'s Gitea wrappers authenticate through `tea` CLI logins +(`Get-GiteaLoginForHost`), not a raw-token `get_gitea_token`-equivalent function — there +is nothing to prepend the identity-resolution block to on the PowerShell side. A native +PowerShell git-credential helper is also unnecessary: `git-credential-mosaic` is invoked +by git's credential-helper protocol (stdin/stdout), which works identically under Git for +Windows' bundled `bash`/`sh` when configured via `credential.helper`, without a `.ps1` +counterpart. A `tea`-login-based per-agent identity for the PowerShell wrappers is a +separate, larger design (mapping identities to `tea login` profiles) and is out of scope +here. diff --git a/packages/mosaic/framework/tools/git/detect-platform.sh b/packages/mosaic/framework/tools/git/detect-platform.sh index 3111eb42..175c47c8 100755 --- a/packages/mosaic/framework/tools/git/detect-platform.sh +++ b/packages/mosaic/framework/tools/git/detect-platform.sh @@ -505,6 +505,28 @@ get_gitea_token() { script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" local cred_loader="$script_dir/../_lib/credentials.sh" + # 0. Per-agent identity (Gate-16 author≠reviewer). If MOSAIC_GIT_IDENTITY, or the + # per-worktree `git config mosaic.gitIdentity`, resolves to an agent that has a + # stored per-slot token for this host, act AS that agent so API tooling + # (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)" + if [[ -n "$_ident" ]]; then + local _idpfx="" + case "$host" in + git.uscllc.com) _idpfx=gitea-usc ;; + git.mosaicstack.dev) _idpfx=gitea-mosaicstack ;; + esac + if [[ -n "$_idpfx" ]]; then + local _idtok="$HOME/.config/mosaic/secrets/gitea-tokens/${_idpfx}-${_ident}.token" + if [[ -r "$_idtok" ]]; then + cat "$_idtok" + return 0 + fi + fi + fi + # 1. Mosaic credential loader (host → service mapping, run in subshell to avoid polluting env) if [[ -f "$cred_loader" ]]; then local token diff --git a/packages/mosaic/framework/tools/git/git-credential-mosaic b/packages/mosaic/framework/tools/git/git-credential-mosaic new file mode 100644 index 00000000..915fc7bc --- /dev/null +++ b/packages/mosaic/framework/tools/git/git-credential-mosaic @@ -0,0 +1,69 @@ +#!/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 +# 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. +ident="$MOSAIC_GIT_IDENTITY" +[ -z "$ident" ] && ident=$(git config --get mosaic.gitIdentity 2>/dev/null) +[ -z "$ident" ] && ident="$username_in" +if [ -n "$ident" ]; then + case "$host" in + git.uscllc.com) idpfx=gitea-usc;; + git.mosaicstack.dev) idpfx=gitea-mosaicstack;; + *) idpfx="";; + esac + if [ -n "$idpfx" ]; then + idtok="$HOME/.config/mosaic/secrets/gitea-tokens/${idpfx}-${ident}.token" + if [ -r "$idtok" ]; then + echo "username=${ident}" + echo "password=$(cat "$idtok")" + exit 0 + fi + fi +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. +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../_lib/credentials.sh +source "$script_dir/../_lib/credentials.sh" +load_credentials "$svc" >/dev/null 2>&1 || exit 0 +# 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" diff --git a/packages/mosaic/framework/tools/git/test-git-credential-mosaic.sh b/packages/mosaic/framework/tools/git/test-git-credential-mosaic.sh new file mode 100644 index 00000000..e84778b6 --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-git-credential-mosaic.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +# Regression harness for `git-credential-mosaic` — per-agent Gitea identity +# resolution (Gate-16 author≠reviewer separation). +# +# Covers: +# 1. Identity resolution priority: MOSAIC_GIT_IDENTITY env > git config +# mosaic.gitIdentity (per-worktree) > git-supplied username. +# 2. Correct per-slot token file path chosen per host +# (gitea-usc-.token vs gitea-mosaicstack-.token). +# 3. Per-slot token present -> emits that identity + token. +# 4. Per-slot token absent -> falls back to the shared account +# (backward-compat / no-op for hosts without per-slot tokens). +# 5. Unknown/unrelated host -> exits 0 with no output (passthrough). +# +# Uses stubbed token files under a fake HOME + a real (throwaway) git repo. +# NEVER reads real secrets or touches the real ~/.config/mosaic/secrets. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/git-credential-mosaic}" +FAKE_HOME="$WORK_DIR/home" +REPO_DIR="$WORK_DIR/repo" +# Mirror the real deployed layout (~/.config/mosaic/tools/{git,_lib}/) under the +# fake HOME: git-credential-mosaic resolves its credentials.sh sibling via a +# script-relative path (BASH_SOURCE), so the copy must live next to a stubbed +# _lib/credentials.sh, not the real one, to keep this test hermetic. +HELPER="$FAKE_HOME/.config/mosaic/tools/git/git-credential-mosaic" + +rm -rf "$WORK_DIR" +mkdir -p "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens" \ + "$FAKE_HOME/.config/mosaic/tools/git" \ + "$FAKE_HOME/.config/mosaic/tools/_lib" \ + "$REPO_DIR" + +cp "$SCRIPT_DIR/git-credential-mosaic" "$HELPER" +chmod +x "$HELPER" + +git -C "$REPO_DIR" init -q +git -C "$REPO_DIR" config user.email "test@example.invalid" +git -C "$REPO_DIR" config user.name "Test" + +# Fake shared-account credential loader — stands in for +# tools/_lib/credentials.sh's load_credentials(), scoped to this test only. +cat > "$FAKE_HOME/.config/mosaic/tools/_lib/credentials.sh" <<'SH' +load_credentials() { + case "$1" in + gitea-mosaicstack) GITEA_URL="https://git.mosaicstack.dev"; GITEA_TOKEN="shared-mosaicstack-token"; export GITEA_URL GITEA_TOKEN; return 0 ;; + gitea-usc) GITEA_URL="https://git.uscllc.com"; GITEA_TOKEN="shared-usc-token"; export GITEA_URL GITEA_TOKEN; return 0 ;; + *) return 1 ;; + esac +} +SH + +fail=0 +assert_eq() { + local desc="$1" expected="$2" actual="$3" + if [[ "$expected" != "$actual" ]]; then + echo "FAIL: $desc — expected '$expected', got '$actual'" >&2 + fail=1 + fi +} + +# Feed "host=\nusername=\n\n" on stdin (mirrors git's credential protocol) +# and run the helper with the fake HOME, inside REPO_DIR (so `git config +# mosaic.gitIdentity` resolves per-worktree), plus any extra env passed in $@. +run_helper() { + local host="$1" username_in="$2"; shift 2 + ( + cd "$REPO_DIR" + env -i HOME="$FAKE_HOME" PATH="$PATH" "$@" bash "$HELPER" get < shared fallback +# (backward-compat: unchanged behavior when nothing is configured). +# --------------------------------------------------------------------------- +git -C "$REPO_DIR" config --unset mosaic.gitIdentity 2>/dev/null || true +out=$(run_helper "git.mosaicstack.dev" "") +assert_eq "shared fallback: username" "username=git" "$(echo "$out" | grep '^username=')" +assert_eq "shared fallback: password" "password=shared-mosaicstack-token" "$(echo "$out" | grep '^password=')" + +# --------------------------------------------------------------------------- +# 2. git-supplied username resolves to an identity WITH a per-slot token -> +# that identity + token wins over the shared account. +# --------------------------------------------------------------------------- +echo -n "agentA-mosaicstack-token" > "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentA.token" +out=$(run_helper "git.mosaicstack.dev" "agentA") +assert_eq "username-resolved identity: username" "username=agentA" "$(echo "$out" | grep '^username=')" +assert_eq "username-resolved identity: password" "password=agentA-mosaicstack-token" "$(echo "$out" | grep '^password=')" + +# --------------------------------------------------------------------------- +# 3. git config mosaic.gitIdentity (per-worktree) beats git-supplied username. +# --------------------------------------------------------------------------- +echo -n "agentB-mosaicstack-token" > "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentB.token" +git -C "$REPO_DIR" config mosaic.gitIdentity agentB +out=$(run_helper "git.mosaicstack.dev" "agentA") +assert_eq "git-config beats username: username" "username=agentB" "$(echo "$out" | grep '^username=')" +assert_eq "git-config beats username: password" "password=agentB-mosaicstack-token" "$(echo "$out" | grep '^password=')" + +# --------------------------------------------------------------------------- +# 4. MOSAIC_GIT_IDENTITY env beats git config mosaic.gitIdentity. +# --------------------------------------------------------------------------- +echo -n "agentC-mosaicstack-token" > "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentC.token" +out=$(run_helper "git.mosaicstack.dev" "agentA" MOSAIC_GIT_IDENTITY=agentC) +assert_eq "env beats git-config: username" "username=agentC" "$(echo "$out" | grep '^username=')" +assert_eq "env beats git-config: password" "password=agentC-mosaicstack-token" "$(echo "$out" | grep '^password=')" +git -C "$REPO_DIR" config --unset mosaic.gitIdentity + +# --------------------------------------------------------------------------- +# 5. Identity resolves, but no matching per-slot token file -> falls back to +# the shared account (per-agent identity is opt-in, not a hard requirement). +# --------------------------------------------------------------------------- +out=$(run_helper "git.mosaicstack.dev" "no-such-agent") +assert_eq "no per-slot token: username" "username=git" "$(echo "$out" | grep '^username=')" +assert_eq "no per-slot token: password" "password=shared-mosaicstack-token" "$(echo "$out" | grep '^password=')" + +# --------------------------------------------------------------------------- +# 6. Correct per-slot token PATH is chosen per host: same agent id, different +# host prefix (gitea-usc- vs gitea-mosaicstack-). +# --------------------------------------------------------------------------- +echo -n "agentD-usc-token" > "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-usc-agentD.token" +out=$(run_helper "git.uscllc.com" "agentD") +assert_eq "host-scoped token path (usc): username" "username=agentD" "$(echo "$out" | grep '^username=')" +assert_eq "host-scoped token path (usc): password" "password=agentD-usc-token" "$(echo "$out" | grep '^password=')" +# agentD has NO mosaicstack token -> must fall back to shared mosaicstack, not +# leak the usc token across hosts. +out=$(run_helper "git.mosaicstack.dev" "agentD") +assert_eq "host-scoped token path (cross-host must not leak): username" "username=git" "$(echo "$out" | grep '^username=')" +assert_eq "host-scoped token path (cross-host must not leak): password" "password=shared-mosaicstack-token" "$(echo "$out" | grep '^password=')" + +# --------------------------------------------------------------------------- +# 7. Unrelated/unknown host -> exit 0, no output (passthrough for non-Gitea +# remotes, e.g. github.com via a different credential helper). +# --------------------------------------------------------------------------- +out=$(run_helper "github.com" "agentA") +assert_eq "unknown host: no output" "" "$out" + +# --------------------------------------------------------------------------- +# 8. Non-"get" verb (store/erase) -> exit 0, no output (git-credential +# protocol: this helper only implements get). +# --------------------------------------------------------------------------- +store_out=$(cd "$REPO_DIR" && env -i HOME="$FAKE_HOME" PATH="$PATH" bash "$HELPER" store < git config +# mosaic.gitIdentity (per-worktree). +# 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). +# +# 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. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/gitea-token-identity}" +FAKE_HOME="$WORK_DIR/home" +REPO_DIR="$WORK_DIR/repo" +CREDENTIALS_FILE="$FAKE_HOME/.config/mosaic/credentials.json" + +rm -rf "$WORK_DIR" +mkdir -p "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens" "$REPO_DIR" + +git -C "$REPO_DIR" init -q +git -C "$REPO_DIR" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git + +cat > "$CREDENTIALS_FILE" <<'JSON' +{ + "gitea": { + "mosaicstack": { + "url": "https://git.mosaicstack.dev", + "token": "shared-mosaicstack-token" + }, + "usc": { + "url": "https://git.uscllc.com", + "token": "shared-usc-token" + } + } +} +JSON + +fail=0 +assert_eq() { + local desc="$1" expected="$2" actual="$3" + if [[ "$expected" != "$actual" ]]; then + echo "FAIL: $desc — expected '$expected', got '$actual'" >&2 + fail=1 + fi +} + +# Runs get_gitea_token for $1=host inside REPO_DIR (per-worktree git config +# resolves there) with a fake HOME + the stub credentials.json, plus any +# extra env passed in $@. +call_get_gitea_token() { + local host="$1"; shift + ( + cd "$REPO_DIR" + # shellcheck disable=SC2016 # deliberately deferred: $DETECT_PLATFORM_SH is + # expanded by the INNER bash -c (via the exported env var below), not here. + env -i HOME="$FAKE_HOME" PATH="$PATH" MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \ + DETECT_PLATFORM_SH="$SCRIPT_DIR/detect-platform.sh" "$@" \ + bash -c 'source "$DETECT_PLATFORM_SH"; get_gitea_token "$1"' _ "$host" + ) +} + +# --------------------------------------------------------------------------- +# 1. No identity resolvable -> shared credential-loader token (unchanged). +# --------------------------------------------------------------------------- +git -C "$REPO_DIR" config --unset mosaic.gitIdentity 2>/dev/null || true +out=$(call_get_gitea_token "git.mosaicstack.dev") +assert_eq "shared fallback (no identity)" "shared-mosaicstack-token" "$out" + +# --------------------------------------------------------------------------- +# 2. git config mosaic.gitIdentity resolves to an agent WITH a per-slot +# token -> that token wins over the shared account. +# --------------------------------------------------------------------------- +echo -n "agentA-mosaicstack-token" > "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentA.token" +git -C "$REPO_DIR" config mosaic.gitIdentity agentA +out=$(call_get_gitea_token "git.mosaicstack.dev") +assert_eq "git-config identity token" "agentA-mosaicstack-token" "$out" + +# --------------------------------------------------------------------------- +# 3. MOSAIC_GIT_IDENTITY env beats git config mosaic.gitIdentity. +# --------------------------------------------------------------------------- +echo -n "agentB-mosaicstack-token" > "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentB.token" +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). +# --------------------------------------------------------------------------- +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" + +# --------------------------------------------------------------------------- +# 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. +# --------------------------------------------------------------------------- +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" +git -C "$REPO_DIR" config --unset mosaic.gitIdentity + +if [[ "$fail" -eq 0 ]]; then + echo "get_gitea_token identity resolution regression passed" +fi + +exit "$fail" diff --git a/packages/mosaic/package.json b/packages/mosaic/package.json index 7f6917ee..63687632 100644 --- a/packages/mosaic/package.json +++ b/packages/mosaic/package.json @@ -25,7 +25,7 @@ "lint": "eslint src", "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests && pnpm run test:framework-shell", - "test:framework-shell": "python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh" + "test:framework-shell": "python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh" }, "dependencies": { "@mosaicstack/brain": "workspace:*",