All four findings reproduced before fixing. rev-code-01 was right on each. B2 (blocker, mine). SEAT-IDENTITY provisioning step 4 said to symlink the framework store entry to the seat slot, while the same file says those bridges must not be recreated. The same bridge, told both ways, in one document. I rewrote the resolution and token-location sections when the deploy made them stale and did not carry the change into the numbered steps. Step 4 is gone and the file now says explicitly that no provisioning step links the store to the slot, so the omission cannot read as an oversight. S1 (mine). The guide claimed the helper "attempts a fleet notification" on refusal. The shipped helper does no such thing — its only reference to notification is a comment saying an alert built on the record is best-effort, and there is no send or wake call anywhere in the file. Now: it writes a durable record, the record is what exists, and nobody should wait for a notification that nothing sends. A guide that promises an alert is worse than one that promises nothing. S2. Estate-local content removed from files that ship to every estate: the ~/.mosaic/fleet/bin script paths (dead paths elsewhere) and the 2026-08-18 dates, which dated a specific host's migration rather than describing behavior. The bridge-removal passage now states the ORDERING that matters — remove bridges only after a seat-aware helper can reach the slot, never before — which is the part that transfers. B1. prettier reformatted all three files. Reproduced the pipeline 2515 failure locally before and confirmed clean after; the other three guides prettier flags are untouched by this branch (0 changes vs origin/next) and are pre-existing. Sanitization gate re-run and passing. Verified for the record, since I could not verify my own work: rev-code-01 confirmed the no-fallback claim TRUE against helper content on origin/next, and judged the evidence rules actionable on the grounds that each names an executable replacement.
6.2 KiB
Seat Identity & Credentials Guide
Every agent that touches a Mosaic-managed git host acts as a named seat with its own credential. This guide is how that works on a host, and what an agent must never do with it.
The mechanism below is the framework's. The specific paths, seats and stores are per-host: measure yours before trusting any of them.
The rule
One seat, one identity, one token file. A seat never borrows another seat's credential, never falls back to a shared owner account, and never carries a second copy of its own token. A second copy is drift, and drift surfaces as the stale copy returning 401 — which reads as a revoked token and sends whoever debugs it somewhere else entirely.
A credential refusal is correct behavior, not a bug to route around. If git refuses with a fail-closed diagnostic, the fix is to provision or correct your identity. Escalate; do not substitute.
How a credential is resolved
Find the helper the way git does, not with command -v. Git runs whatever
credential.helper names, and on a Mosaic host that is an absolute path — so a PATH lookup
answers a different question and the two disagree the moment the PATH copy is removed. It was
removed on hosts that have completed that migration.
git config --get-all credential.helper # every helper, in the order git tries them
Git tries each configured helper in turn until one supplies a credential. A fail-closed
helper supplies nothing, so a second helper configured behind it silently becomes the one that
answers. When you care which binary serves a credential, read the whole list.
Resolve all three forms git accepts — absolute path, !command, and a bare name looked up on
PATH — not just the one your host happens to use.
The helper resolves the identity in this order:
$MOSAIC_GIT_IDENTITYgit config --get mosaic.gitIdentity- the username git supplied on stdin
It maps the host to a store prefix — git.mosaicstack.dev to gitea-mosaicstack,
git.uscllc.com to gitea-usc. Any other host is declined quietly with rc=0, which is not an
error and raises no escalation.
Then it chooses one of two stores, and reads exactly one file:
brain_home = ${MOSAIC_BRAIN_HOME:-$HOME/.mosaic}
seat — when $brain_home/fleet/agents/<identity>/ EXISTS
$brain_home/fleet/agents/<identity>/secrets/<prefix>-<identity>.token
service — otherwise
~/.config/mosaic/secrets/gitea-tokens/<prefix>-<identity>.token
There is no precedence between the two and no fallback from one to the other. The existence of the seat directory decides it. A seat that has a directory and an empty slot fails closed; it does not reach the service store. That is the intended behavior — the alternative is an agent silently acting as somebody else.
If the file is unreadable the helper fails closed: it refuses and writes a durable record to
the escalation spool. It does not fall back to a shared account. The record is what exists — any
alerting built on top of it is a separate, best-effort concern and is not performed by the helper,
so do not wait for a notification that nothing sends. That fallback is what made
usc/uconnect#3084 unattributable, and it was removed deliberately.
Verify the helper you actually have:
h=$(git config --get credential.helper)
grep -c 'FAIL CLOSED' "$h" # expect >= 1
grep -c 'fleet/agents' "$h" # expect >= 1; 0 means it predates mosaicstack#1311
Where a seat's token lives
The seat slot is the only copy:
~/.mosaic/fleet/agents/<seat>/secrets/<prefix>-<seat>.token real file, mode 600
The framework store at ~/.config/mosaic/secrets/gitea-tokens/ holds tokens for service
identities only — identities with no seat directory. A seat's token does not belong there.
Before mosaicstack#1311 the deployed helper knew only the service store, and seats were bridged with a symlink from the store into the slot. Those bridges must be removed once a seat-aware helper is deployed, and must not be recreated. Remove them only after the helper can reach the slot without them; the reverse order takes every seat offline. A symlink is not how a system finds a credential; the helper resolving the right store is.
.principal and .scopes beside the token are grant records, not secrets. They are tracked. The
.token never is.
Provisioning a new seat
- Create
~/.mosaic/fleet/agents/<seat>/secrets/mode 700. - Write
.principal(the Gitea login) and.scopes(the granted scopes), mode 600. - The estate operator mints the token into the seat slot, mode 600. Agents do not mint their own, and do not ask another agent to mint one for them.
- Verify with an authenticated
GET /userand confirm the returned login is the seat, not the minting account. Record the date inENTITY.md. Never record the value.
There is no step that links the framework store to the slot. A seat-aware helper reads the slot directly; a store entry pointing at a slot is the bridge described below, and it is not part of provisioning.
Until step 3, the seat is unminted and its git writes fail closed. That is the designed state and is safe to launch in — the seat is told at launch so it does not discover it mid-task.
Acting as yourself
Name the identity on every invocation:
MOSAIC_GIT_IDENTITY=<seat> git push
git -c user.name=<seat> -c user.email=<seat>@mosaicstack.dev commit -m "..."
Never persist git config mosaic.gitIdentity inside a ~/src/stack worktree. Every worktree
of that clone shares one .git/config, so a persisted identity there silently rewrites the
identity of every other seat working in that clone. The per-invocation form has no exception.
Handling
- Never print a token value. Compare by SHA-256 digest, or write
<REDACTED>. - Never stage a
.token,secrets.json, orENTITY.md. Stage explicit paths and nevergit add -A—secrets/*.principalandsecrets/*.scopesare covered by no ignore rule. - Never place a token in an environment variable in an interactive session. A
declare -xdump has leaked the whole environment to a terminal before. - No real credential or operator data on a sandbox VM, ever.