credentials.sh: gitea loader assumes one principal per host, so every gitea-mosaicstack wrapper fails with a valid token #1175

Open
opened 2026-08-12 22:04:10 +00:00 by Mos · 0 comments
Contributor

Symptom

Every gitea-mosaicstack wrapper on this host fails with:

Error: Gitea token not found. Set GITEA_TOKEN or configure ~/.git-credentials.

ci-queue-wait.sh, pr-create.sh, pr-review.sh, issue-*.sh — the whole suite against
git.mosaicstack.dev is dead, which is worse than a wrapper being undiscoverable: gate 7
requires the wrapper before any raw provider call, so the fail-closed posture leaves the
agent with no legal path at all and a strong incentive to reach for curl anyway.

Cause

tools/_lib/credentials.sh resolves a flat key:

gitea-mosaicstack)
  export GITEA_TOKEN="${GITEA_TOKEN:-$(_mosaic_read_cred '.gitea.mosaicstack.token')}"

but a host with more than one principal on the same Gitea stores them named, with a
pointer:

gitea.mosaicstack.<principal>.token
gitea.mosaicstack.<principal>.username
gitea.mosaicstack.default = <principal>

.gitea.mosaicstack.token does not exist, so the loader exports an empty string and the
guard reports a missing token. The credential is present and valid — verified by hand:
both principals return HTTP 200 on /api/v1/user and push=true on the target repo.

This is not a stale or revoked credential, and diagnosing it as one wastes a rotation.

Why it is a framework bug and not a config bug

The same file already handles exactly this shape correctly for two other services —
authentik resolves .authentik.${ak_instance}.token, woodpecker resolves
.woodpecker.${wp_instance}.token with a documented legacy-flat fallback. Only the two
gitea-* branches assume a single principal per host. The multi-principal layout is the
one the fleet actually needs, because a seat must never borrow another seat's identity.

Proposed fix

In the gitea-* branches, resolve in this order:

  1. GITEA_TOKEN already in the environment — unchanged, still wins.
  2. MOSAIC_GITEA_PRINCIPAL if set — explicit beats implicit, and identity selection
    should be sayable out loud.
  3. .gitea.<host>.default.gitea.<host>.<that>.token.
  4. Flat .gitea.<host>.token — backward compatibility.
  5. Exactly one principal present → use it. More than one and no pointer → error listing
    the principal names
    , never pick one.

Step 5 matters on a host where the default login is an admin account: a loader that
silently picks a principal is making an authorisation decision on the agent's behalf.

Mirror username alongside token, since Gitea basic auth needs both and the wrappers
currently have no way to learn it.

Also worth flagging separately

On the host where this was found, gitea.mosaicstack.default points at a principal named
for the other Gitea instance. Both happen to authenticate, so nothing failed loudly —
but a host-matched default is the thing an operator expects, and a cross-host default is
the sort of quiet mismatch that surfaces later as an unexplained permission error. That
is an operator-file question, not a framework change.

Repro

Given a credentials file with named principals under gitea.mosaicstack and no flat
token key:

source ~/.config/mosaic/tools/_lib/credentials.sh
load_credentials gitea-mosaicstack   # → Error: gitea.mosaicstack.token not found
## Symptom Every `gitea-mosaicstack` wrapper on this host fails with: ``` Error: Gitea token not found. Set GITEA_TOKEN or configure ~/.git-credentials. ``` `ci-queue-wait.sh`, `pr-create.sh`, `pr-review.sh`, `issue-*.sh` — the whole suite against `git.mosaicstack.dev` is dead, which is worse than a wrapper being undiscoverable: gate 7 requires the wrapper before any raw provider call, so the fail-closed posture leaves the agent with no legal path at all and a strong incentive to reach for `curl` anyway. ## Cause `tools/_lib/credentials.sh` resolves a **flat** key: ```sh gitea-mosaicstack) export GITEA_TOKEN="${GITEA_TOKEN:-$(_mosaic_read_cred '.gitea.mosaicstack.token')}" ``` but a host with more than one principal on the same Gitea stores them **named**, with a pointer: ``` gitea.mosaicstack.<principal>.token gitea.mosaicstack.<principal>.username gitea.mosaicstack.default = <principal> ``` `.gitea.mosaicstack.token` does not exist, so the loader exports an empty string and the guard reports a missing token. The credential is present and valid — verified by hand: both principals return HTTP 200 on `/api/v1/user` and `push=true` on the target repo. This is not a stale or revoked credential, and diagnosing it as one wastes a rotation. ## Why it is a framework bug and not a config bug The same file already handles exactly this shape correctly for two other services — `authentik` resolves `.authentik.${ak_instance}.token`, `woodpecker` resolves `.woodpecker.${wp_instance}.token` with a documented legacy-flat fallback. Only the two `gitea-*` branches assume a single principal per host. The multi-principal layout is the one the fleet actually needs, because a seat must never borrow another seat's identity. ## Proposed fix In the `gitea-*` branches, resolve in this order: 1. `GITEA_TOKEN` already in the environment — unchanged, still wins. 2. `MOSAIC_GITEA_PRINCIPAL` if set — explicit beats implicit, and identity selection should be sayable out loud. 3. `.gitea.<host>.default` → `.gitea.<host>.<that>.token`. 4. Flat `.gitea.<host>.token` — backward compatibility. 5. Exactly one principal present → use it. More than one and no pointer → **error listing the principal names**, never pick one. Step 5 matters on a host where the default login is an admin account: a loader that silently picks a principal is making an authorisation decision on the agent's behalf. Mirror `username` alongside `token`, since Gitea basic auth needs both and the wrappers currently have no way to learn it. ## Also worth flagging separately On the host where this was found, `gitea.mosaicstack.default` points at a principal named for the *other* Gitea instance. Both happen to authenticate, so nothing failed loudly — but a host-matched default is the thing an operator expects, and a cross-host default is the sort of quiet mismatch that surfaces later as an unexplained permission error. That is an operator-file question, not a framework change. ## Repro Given a credentials file with named principals under `gitea.mosaicstack` and no flat `token` key: ```sh source ~/.config/mosaic/tools/_lib/credentials.sh load_credentials gitea-mosaicstack # → Error: gitea.mosaicstack.token not found ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#1175