Compare commits

..
Author SHA1 Message Date
fred c703cc50eb git-credential-mosaic: escape the escalation record, and stop naming a record that was never written
ci/woodpecker/pr/ci Pipeline was successful
Both defects found in review by rev-code-01 on #1311.

F3 — the JSONL record interpolated every field with a bare %s. An identity comes
from git config or the environment and a cwd is whatever directory git ran in, so
either can contain a quote or a backslash. One such refusal turned the day's spool
into unparseable JSONL, and the operator would only discover it while reading the
record that explains an outage. Fields are now JSON-escaped.

F2 — the diagnostic printed "record: <spool>/<date>.jsonl" unconditionally, but
the record is only written inside the branch where mkdir -p succeeded. When the
spool cannot be created the helper named a file that does not exist, on exactly
the hosts where the escalation was lost. It now reports the real path or says
NOT WRITTEN.

Also: prettier on README.md, which was the format-step failure on pipeline 2508.
It reflowed only the two tables this branch added.

Tests: cases 14 and 15 cover both. Verified discriminating — against the previous
helper with these same tests, case 14 fails with the unparseable record printed
and case 15 fails on both assertions; against this one both pass.

The first draft of case 14 used `ls "$spool"/*.jsonl | head -1`, which under
`set -o pipefail` exits 2 on a missed glob and killed the suite with zero output
— the same silent-nonzero failure rev-code-01 hit from a partial tools/ extraction
and the reason this file exists. Replaced with a glob loop and a comment.
2026-08-18 17:04:55 -05:00
fred 3d2b712355 git credentials: fail closed, and read a seat's token from its own slot
ci/woodpecker/pr/ci Pipeline failed
Two changes to one rule: a credential is resolved from exactly one place,
and an identity that cannot be resolved is refused rather than substituted.

FAIL CLOSED. Both readers ended in an unconditional fall-through to the
shared Gitea account whenever an identity did not resolve. Every seat in a
fleet therefore pushed, opened PRs and filed reviews under one account, and
a record made that way cannot be traced to the agent that made it
afterwards. The fallback now applies only where there is no attribution to
lose: a host with no fleet. Where seats exist, an unresolvable request emits
nothing, exits nonzero, explains itself on stderr, and — in the git helper —
appends a record naming the identity, host, reason and cwd, and no token
value, to ${MOSAIC_CREDENTIAL_SPOOL:-~/.local/state/mosaic-credential-escalations}.

A host runs a fleet when <brain>/fleet/agents exists, which is the signal
packages/mosaic/src/fleet/brain-home.ts already uses to decide a brain is
active, resolved the same way (MOSAIC_BRAIN_HOME, else ~/.mosaic). This is
what keeps the change a no-op for an operator who has not provisioned
per-slot tokens: no fleet directory, shared account, unchanged. It is also
why there is no environment variable to restore the old behavior — one would
reintroduce the substitution being removed.

STORE SELECTION. Both readers hardcoded ~/.config/mosaic/secrets/gitea-tokens,
so a seat's own secrets/ slot was invisible to the framework: a seat could
hold a valid credential and still be served the shared account. The store is
now chosen by what the identity is. An identity with a directory under
<brain>/fleet/agents/ is a seat and is read only from
<brain>/fleet/agents/<id>/secrets/; any other identity is a service identity
and is read from the framework store. There is no precedence between them
and no fallback from one to the other, so a seat with an empty slot is
refused even when a same-named token sits in the framework store. Two copies
of one credential are drift rather than redundancy, and drift surfaces as
the stale copy returning 401, which reads as a revoked token and sends
whoever debugs it somewhere else.

detect-platform.sh is in scope alongside git-credential-mosaic because they
are the two readers of these tokens. Patching only the git helper would make
"one credential, one location" true for push and fetch and false for
pr-create.sh, issue-create.sh and pr-review.sh, which is the harder failure
to notice.

TESTS. The three assertions that pinned the shared-account fall-through are
now fail-closed assertions, and a refusal is checked four independent ways:
nonzero exit, empty stdout, a stderr diagnostic naming identity and host,
and no shared token value anywhere in the output. The exit code alone would
pass against a helper that emitted the credential and then failed. Added:
seat-slot resolution, the no-cross-store-fallback case with a control
proving the framework-store file it declines to read is readable, no-identity
on a fleet host, the fleet gate firing on the default ~/.mosaic and not only
on an injected MOSAIC_BRAIN_HOME, and a cross-host leak check. Both suites
were run against the pre-change code as a control and fail there on exactly
the shared-token emission.

shellcheck is not installed on the authoring host, so the rewritten helper
is unlinted locally and CI is the first lint of it.
2026-08-18 16:19:43 -05:00
fargo 245e0c427d feat(quality-rails): typed evaluator absorbs QC-19/QC-20; verify-release wiring (RI-3-002, #1275) (#1308)
ci/woodpecker/push/publish Pipeline failed
2026-08-18 17:54:47 +00:00
jarvisandfargo ff45f7b5d0 docs(ri-050): forge fail-closed docs + TASKS status catch-up (#1275) (#1299)
ci/woodpecker/push/publish Pipeline failed
Co-authored-by: Jarvis <[email protected]>
2026-08-18 16:00:36 +00:00
jarvisandfargo 64350892e7 docs(ri-050): RI-3-001 complete quality-rails probe inventory (#1275) (#1302)
ci/woodpecker/push/publish Pipeline was canceled
Co-authored-by: jarvis <[email protected]>
2026-08-18 15:59:34 +00:00
jason.woltjeandjarvis 6e9df3c640 fix(doctor): greenfield brain lock-in note (fred's #1301 follow-up, #1288) (#1307)
ci/woodpecker/push/publish Pipeline failed
Co-authored-by: Jason Woltje <[email protected]>
2026-08-18 07:57:37 +00:00
7 changed files with 656 additions and 217 deletions
+3 -3
View File
@@ -23,10 +23,10 @@
| RI-2-001 | done | RI-N2 (Forge): remove stub-executor false success; `--simulate` typed `simulated` results that satisfy nothing; literal-`true` gates and echo-review replaced with real gates or typed waiting-for-authority | #1275 | pi-glm-5.3 | mosaicstack/stack | fix/ri-050-forge-fail-closed | RI-0-001 | 20K | Independent review APPROVED 2026-08-17 (Gitea review 172 on PR #1278, head 99b8f6ea; reviewing seat fargo — recorded under shared host principal mos-dt-0, provenance correction posted by fred; wrapper gap filed by fred). Executed at head: forge tests 116/116, lint green, typecheck green after building macp dist (minimal-install artifact, not a defect), workspace typecheck 45/45, no external type consumers of the changed interfaces. CI red = known lane-wide fleet-test failure only, carries no information about this change (fred, log-content analysis, pipelines 2456-2458). Non-blocking finding: README L141-143 + skills/mosaic-forge/SKILL.md document bare forge run/resume, which now fails closed — fast-follow docs touch. Merge queued behind #1270. UPDATE 2026-08-18: #1270 merged; CI GREEN at head 4917df1f via serialized retry (pipeline 2477) - root cause of prior reds was CI-agent contention (web SPA timeouts under concurrent pipelines), superseding the fleet-test-failure theory. |
| RI-2-002 | done | RI-N2 (MACP): gate runner fails closed on empty commands, stub executors, and unimplemented CI-provider gates unless explicit simulate; typed capability failures | #1275 | pi-glm-5.3 | mosaicstack/stack | fix/ri-050-macp-fail-closed | RI-0-001 | 15K | PR #1293 (head 2097379e): CI green (pipeline 2465), independent review APPROVED (Gitea review 173, jarvis seat, 2026-08-17) - macp 109/109 verified at head. Merge queued behind #1276/#1277/#1278. |
| RI-3-001 | done | RI-N4: complete probe inventory mapping every TS and shell quality-rail check to one canonical check with disposition (preserve/strengthen/retire, each named) | #1275 | pi-glm-5.3 | mosaicstack/stack | docs/ri-050-qr-probe-inventory | RI-0-001 | 12K | PR #1302 (head e06a47fac591): CI green (2484), independent review APPROVED (Gitea review 187, fargo seat, 2026-08-18) — 54 rows / 21 canonical checks / dispositions 43-2-9-0 verified by row-count and code spot-checks. Merged by fargo at pinned head. |
| RI-3-002 | done | RI-N4: TS evaluator absorbs effective shell probes; typed results (passed/failed/blocked/error/not-applicable) with versioned digested check definitions; shell commands become thin adapters; contract/parity/negative-control tests | #1275 | pi-glm-5.3 | mosaicstack/stack | feat/ri-050-qr-evaluator | RI-3-001 | 30K | | PR #1308 (head 68279d61): CI green (2506), independent review APPROVED (Gitea review 188, fred, seven mutations incl. vacuous-pass + stage-removal). Merged by fargo at pinned head → next @ 245e0c4. Follow-up #1309 (digest wording). |
| RI-4-001 | done | RI-N3: one PRD application service — `mission --plan` persists mission↔PRD linkage (ids/versions/selected requirements); `mosaic prdy` routes through the service or becomes a named import/export adapter; Markdown is a labeled generated view; explicit conflict-aware import | #1275 | pi-glm-5.3 | mosaicstack/stack | feat/ri-050-prd-authority | RI-0-001 | 35K | PR #1294 (head 8d258e1d): CI green (pipeline 2466), independent review APPROVED (Gitea review 174, jarvis seat, 2026-08-17) - prdy 20/20 + command specs 9/9 at head. Merge queued behind #1276/#1277/#1278. | PR #1294 (head 8d258e1d): CI green (2466), review 174. Merged 2026-08-18 overnight wave → next @ d92de53. |
| RI-3-002 | not-started | RI-N4: TS evaluator absorbs effective shell probes; typed results (passed/failed/blocked/error/not-applicable) with versioned digested check definitions; shell commands become thin adapters; contract/parity/negative-control tests | #1275 | pi-glm-5.3 | mosaicstack/stack | feat/ri-050-qr-evaluator | RI-3-001 | 30K | |
| RI-4-001 | in-progress | RI-N3: one PRD application service — `mission --plan` persists mission↔PRD linkage (ids/versions/selected requirements); `mosaic prdy` routes through the service or becomes a named import/export adapter; Markdown is a labeled generated view; explicit conflict-aware import | #1275 | pi-glm-5.3 | mosaicstack/stack | feat/ri-050-prd-authority | RI-0-001 | 35K | PR #1294 (head 8d258e1d): CI green (pipeline 2466), independent review APPROVED (Gitea review 174, jarvis seat, 2026-08-17) - prdy 20/20 + command specs 9/9 at head. Merge queued behind #1276/#1277/#1278. |
| RI-5-001 | done | RI-N5: typed freshness states (current/stale/partial/unknown/unavailable); no failed-fetch-renders-empty; stale derived verdicts → unknown; mutations disabled when stale; failure-matrix tests | #1275 | pi-glm-5.3 | mosaicstack/stack | feat/ri-050-web-stale-safety | RI-0-001 | 25K | |
| RI-V-001 | in-progress | Final verification + release evidence: all cards verified merged, negative controls demonstrated, real `next` publish run green on exact commit, evidence pack recorded | #1275 | pi-glm-5.3 | mosaicstack/stack | docs/ri-050-release-evidence | RI-1-002, RI-2-001, RI-2-002, RI-3-002, RI-4-001, RI-5-001 | 10K | | Evidence pack live on branch docs/ri-050-release-evidence @ 340b95e — all five requirements evidenced; final exhibit (fully-green push) gated on registry item; then PR + topher review. |
| RI-V-001 | not-started | Final verification + release evidence: all cards verified merged, negative controls demonstrated, real `next` publish run green on exact commit, evidence pack recorded | #1275 | pi-glm-5.3 | mosaicstack/stack | docs/ri-050-release-evidence | RI-1-002, RI-2-001, RI-2-002, RI-3-002, RI-4-001, RI-5-001 | 10K | |
## Dispatch waves (max 2 parallel workers)
@@ -1,96 +0,0 @@
# RI-050 Release Evidence Pack (alpha 0.0.50 release-integrity floor)
> Status: **DRAFT — near complete**. All five normative requirements (RI-N1..N5) merged to `next` behind the live gate. Remaining for closure: one fully-green push pipeline (blocked only on the registry-credential item below) and final verification. Last updated 2026-08-18 by fargo (day-takeover orchestrator).
> Card: RI-V-001. All sections marked ⏳ pending their card's merge. Normative source:
> `docs/PRD.md` § Release Integrity Workstream (#1275).
## RI-N1 — Canonical terminal verification + exact-commit publish gate
| exhibit | evidence | where |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------- |
| Gate live, fail-closed | Push pipeline **2486**: `verify` ran at exact commit, FAILED on a real latent defect (gateway cross-user-isolation cleanup on the no-DB path), and `build` / `publish-npm` / `build-gateway` were all **skipped**. First push in repo history that did not publish ungated (prior ungated publishes beside failing builds: 2439, 2462, 2482). | Woodpecker repo 47 pipeline 2486 |
| Gate-caught defect fixed | PR **#1304** (afterAll honors `dbAvailable`; both paths verified: dead-port 28 skipped + file passes; live-5433 28 passed). Review 180 (fred). | PR #1304 |
| First gated green npm publish | Push pipeline **2488** (post-#1304): `verify` GREEN → `build` GREEN → `publish-next-npm` GREEN, all publish effects behind the gate. | Woodpecker pipeline 2488 |
| Negative controls | PR **#1305**: structural DAG tests (S1 missing edge, S2 renamed effect incl. command-based npm/kaniko detection, S3 detach, S4 failure:ignore/success override, S5 when-filter, S6 HEAD-mover between verify and publish with legitimate-recheckout positive control, S7 removal) + subset-stage composition control in verify-release.test.mjs. Mutation-verified by the dispatching seat in both directions (true bypass → S1 assertion fires; non-bypass edit → correctly green). Scripts tests 20/20, CI 2490 green. | PR #1305 |
| ✅ Canonical command | `scripts/verify-release.mjs` (stage table pinned to ci.yml by checked-in test). Merged with #1277; now also invokes the RI-N4 evaluator via its `quality-rails` stage (#1308). | `scripts/verify-release.mjs` |
## RI-N2 — Forge + MACP fail-closed (typed explicit simulation)
- ✅ Forge: PR **#1278** merged (head 4917df1f; CI 2477; review 184 fred at pinned head — prior review 172 dismissed by rebase, correctly re-taken).
- ✅ MACP: PR **#1293** merged (head 2097379e; CI 2465; review 173).
- ✅ Post-merge behavior docs: PR **#1299** merged (head 8a405b14; CI 2497; review 186 fargo at pinned head — legitimate independent seat; merged 2026-08-18 with --expect-head pin, content-verified on next @ ff45f7b).
## RI-N3 — PRD authority
- ✅ PR **#1294** merged (head 8d258e1d; CI 2466; review 174).
## RI-N4 — Quality-rails evaluator
- ✅ Probe inventory: PR **#1302** merged (head e06a47fac59; CI 2484; review 187 fargo at pinned head; 54 rows / 21 canonical checks / dispositions 43-2-9-0 row-count-verified; merged 2026-08-18, content-verified on next @ 6435089).
- ✅ TS evaluator absorbs shell probes: PR **#1308** merged (head 68279d61; CI 2506; review 188 fred at pinned head — seven targeted mutations, seven detections, incl. the vacuous-pass hole M1 and stage-removal M7). Evaluator: typed fail-closed verdicts, digested versioned definitions, per-subject sets; QC-19 absorbed (verbatim-list parity oracle), QC-20 as thin adapter (verify.sh unmodified); verify-release `quality-rails` stage wired (RI-N1 consumes the evaluator). Worker-produced, independently verified by the dispatching seat (quality-rails 40/40 incl. sabotage control 6-failed/34-passed restored sha-verified; root build 25/25; typecheck 45/45).
## RI-N5 — Consequence-aware stale UI
- ✅ PR **#1300** merged (head a337d787; CI 2481; review 179). Web suite 199 → 281 tests (failure matrix + negative controls), independently re-run by the dispatching seat before merge.
## Known-open infrastructure item (not a card)
Gateway/ci-base **image** pushes fail on registry credentials: Woodpecker repo
secrets `REGISTRY_USERNAME`/`REGISTRY_PASSWORD` are rejected by the Gitea
container registry (explicit `UNAUTHORIZED` at `/v2/token`; pipeline 2494 after
PR #1306 corrected the secret references — previously masked as an ambiguous
push-permission error since at least 2439). Requires a package-scoped token
(Jason). The npm publish path is green and gated; this item tracks image pushes
only and predates the RI-050 floor.
**Update 2026-08-18 (fargo):** Jason set new secret values ~17:25Z; pipeline
**2507** (the #1308 merge push, first after the update, 18:0xZ) still fails
`build-gateway` with the identical `UNAUTHORIZED`. Read-only isolation (no
secrets read, no CI retries): the registry endpoint and auth mechanism are
HEALTHY — a valid Gitea token via basic-auth mints a JWT at `/v2/token` (200),
bad credentials 401 cleanly. Therefore the failure is isolated to the secret
VALUES, not the endpoint or pipeline. Most likely shape error (labeled guess):
the registry authenticates username + **API token with package scope**, not
username + login password; if REGISTRY_PASSWORD holds a login password rather
than a minted token value, `/v2/token` 401s exactly as observed. npm publishes
remained green in 2507; every publish step except the image push is gated and
green.
## Process record (audit trail)
- Merges executed under the jarvis principal (topher seat; identity provisioning
pending) via the Gitea API replicating `pr-merge.sh` semantics (head-pin +
squash + keep branch): `pr-merge.sh` hard-codes `main`-only targets and cannot
express this repo's `next` trunk — wrapper gap captured to OpenBrain
(id 9db7a95a) and to the framework queue.
- Reviews tonight: 175/178 (zane's #1298, both heads, by topher); 176/177/179/
180/181/182 (fred) — cross-review rule (producer ≠ reviewer) held on every
merge: producers were pi workers / zane; reviewers were the other seat.
- CI contention note: concurrent PR pipelines on the single CI agent can time
out the web SPA suite (measured 2470/2472 vs serialized 2475/2476/2477);
serialize retries when the queue is busy.
## Process record — 2026-08-18 day takeover (fargo)
- Takeover directive: Jason (via jarvis router + both seats' handoff documents,
relayed verbatim over comms). First-move conflict between the two handoffs
(zane: doctor PR first; topher: review-queue first) resolved on dependency
grounds per jarvis's read — topher's order won; zane's finding-2 doctor PR
(upgraded by fred's measurement) remains queued, nothing depends on it.
- Reviews 186 (#1299) + 187 (#1302): fargo, at pinned heads, as the legitimate
independent seat (topher dispatched both producers; cross-review rule held).
Both merged with --expect-head pinning via the REPO-COPY pr-merge.sh
(allows next; the installed copy still lags — zane's route, not the raw-API
break-glass), each preceded by ci-queue-wait -B next -R mosaicstack/stack.
CI green at both heads (2497, 2484). Merges content-verified on the shipping
ref (TASKS anchors at ff45f7b / 6435089).
- RI-3-002: one pi worker (zai/glm-5.3:high), independently verified by the
dispatching seat before push; PR #1308 reviewed by fred (188, seven
mutations incl. vacuous-pass and stage-removal) and merged head-pinned at
68279d61 → next @ 245e0c4.
- Registry-credential isolation measurement (above) performed read-only; no
secret values read, no retry-pushes against CI.
- One reviewer-scope disclosure (fred, review 188): fred's approval explicitly
did NOT re-run root build/typecheck/mosaic-vitest — those remain the
dispatching seat's numbers. The changed-package suites, verify-release
suite, and seven mutations were fred's own.
+49 -7
View File
@@ -43,7 +43,7 @@ fleet commits, pushes, and opens PRs under one identity — with no cryptographi
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:
identity**:
1. `MOSAIC_GIT_IDENTITY` environment variable, or
2. `git config --get mosaic.gitIdentity` (set per-worktree; persists on disk across
@@ -51,12 +51,54 @@ identity** before falling back to the shared account:
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}-<agent-id>.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.
### Which store a credential is read from
The store is chosen by what the identity **is**, not by which file happens to exist first:
| The identity | Its credential is read from |
| ------------------------------------------------------------------ | -------------------------------------------------------------------------- |
| has a directory at `<brain>/fleet/agents/<id>/` — it is a **seat** | `<brain>/fleet/agents/<id>/secrets/gitea-{usc,mosaicstack}-<id>.token` |
| does not — it is a **service identity** | `~/.config/mosaic/secrets/gitea-tokens/gitea-{usc,mosaicstack}-<id>.token` |
`<brain>` is `MOSAIC_BRAIN_HOME` if set, else `~/.mosaic` — the same resolution
`packages/mosaic/src/fleet/brain-home.ts` performs.
**There is no precedence between the two stores and no fallback from one to the other.**
A seat whose slot is empty is refused even when a same-named token sits in the framework
store. One credential lives in exactly one location: a second copy is drift rather than
redundancy, and the way drift surfaces is a stale copy returning 401, which reads as a
revoked token and sends whoever debugs it to the wrong place.
### What happens when nothing resolves
| identity resolves | token in its store | host runs a fleet | result |
| ----------------- | ------------------ | ----------------- | ------------------------- |
| yes | yes | — | that identity + token |
| yes | no | — | **fail closed** |
| no | — | yes | **fail closed** |
| no | — | no | shared account, unchanged |
A host "runs a fleet" when `<brain>/fleet/agents` exists — the same signal `brain-home.ts`
uses to decide a brain is active.
Failing closed means: nothing is emitted, the exit status is nonzero, a stderr diagnostic
names the identity, its source, the store it resolved to and the path that was expected,
and `git-credential-mosaic` additionally appends a record (identity, host, reason, cwd —
never a token value) to `${MOSAIC_CREDENTIAL_SPOOL:-~/.local/state/mosaic-credential-escalations}`.
The git operation fails; nothing is attributed to anyone.
The shared-account fallback that used to cover these two cases is why a PR could be
authored, commented and merged under an account whose owner did not open it — every seat
shared one identity, so the record could not be traced back afterwards. An
under-provisioned agent is refused rather than handed the most privileged account
available.
**On a host with no fleet, nothing changes**: no `fleet/agents` directory means the shared
account still answers, so this is a no-op for an operator who has not provisioned per-slot
tokens. On a host that does run a fleet, a human doing manual git work needs an identity
of their own — `MOSAIC_GIT_IDENTITY=<id>` with a provisioned slot. There is deliberately no
environment variable that restores the fallback; one would reintroduce exactly the
substitution this removes.
### Enabling it for a clone
@@ -507,37 +507,69 @@ get_gitea_token() {
# 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
# stored credential 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.
# git credential helper, which this block deliberately mirrors.
local _ident="${MOSAIC_GIT_IDENTITY:-}"
local _ident_src="MOSAIC_GIT_IDENTITY"
if [[ -z "$_ident" ]]; then
_ident="$(git config --get mosaic.gitIdentity 2>/dev/null || true)"
_ident_src="git config mosaic.gitIdentity"
fi
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
# FAIL LOUD: an explicit git identity was requested for a recognized Gitea host,
# but no per-slot token exists for THAT identity. Refuse to fall through to the
# shared/default credential loader below — silently borrowing another slot's token
# would post PRs/issues/reviews under the WRONG agent (e.g. rev2's review attributed
# to coder3), corrupting Gate-16 author≠reviewer separation. Hard-stop instead so the
# caller aborts loudly rather than acting as the wrong identity.
echo "Error: git identity '$_ident' requested (via $_ident_src) for host '$host', but no per-slot token at $_idtok." >&2
echo " Refusing to borrow another slot's token. Provision the per-slot token, or unset the identity to use shared credentials." >&2
return 1
# Recognized Gitea hosts are the ones carrying the per-identity token scheme.
local _idpfx=""
case "$host" in
git.uscllc.com) _idpfx=gitea-usc ;;
git.mosaicstack.dev) _idpfx=gitea-mosaicstack ;;
esac
# Brain-home resolution mirrors packages/mosaic/src/fleet/brain-home.ts and
# tools/fleet/start-agent-session.sh: MOSAIC_BRAIN_HOME wins, else ~/.mosaic.
local _brain_home="${MOSAIC_BRAIN_HOME:-$HOME/.mosaic}"
if [[ -n "$_ident" && -n "$_idpfx" ]]; then
# Credential store selection: an identity is a SEAT or a SERVICE, and which
# one it is decides where its credential lives. No precedence between the
# two stores and no fallback from one to the other — a seat with an empty
# slot fails loud rather than reading a service credential of the same name.
# One credential, one location: two copies diverge, and the stale copy fails
# in a way that reads as a revoked token rather than as drift.
local _idtok _ident_kind
if [[ -d "$_brain_home/fleet/agents/$_ident" ]]; then
_ident_kind="seat"
_idtok="$_brain_home/fleet/agents/$_ident/secrets/${_idpfx}-${_ident}.token"
else
_ident_kind="service identity"
_idtok="$HOME/.config/mosaic/secrets/gitea-tokens/${_idpfx}-${_ident}.token"
fi
if [[ -r "$_idtok" ]]; then
cat "$_idtok"
return 0
fi
# FAIL LOUD: an explicit git identity was requested for a recognized Gitea host,
# but no credential exists for THAT identity. Refuse to fall through to the
# shared/default credential loader below — silently borrowing another identity's
# token would post PRs/issues/reviews under the WRONG agent (e.g. rev2's review
# attributed to coder3), corrupting Gate-16 author≠reviewer separation. Hard-stop
# instead so the caller aborts loudly rather than acting as the wrong identity.
echo "Error: git identity '$_ident' requested (via $_ident_src) for host '$host', but no per-slot token at $_idtok." >&2
echo " Resolved as a ${_ident_kind}; there is no fallback between the seat and service stores." >&2
echo " Refusing to borrow another slot's token. Provision the credential at that path, or unset the identity to use shared credentials." >&2
return 1
fi
# FAIL LOUD: no identity resolved, on a host that HAS a fleet. Where seats exist,
# every record must name the agent that made it, so an unattributable request is
# refused rather than handed the shared account. `fleet/agents` existing is the
# same signal brain-home.ts uses to decide a brain is active. A host with no fleet
# keeps the shared path below unchanged: there the shared account is the operator's
# own and there is no attribution to lose.
if [[ -z "$_ident" && -n "$_idpfx" && -d "$_brain_home/fleet/agents" ]]; then
echo "Error: no git identity resolved for host '$host', but this host runs a fleet ($_brain_home/fleet/agents)." >&2
echo " Refusing to fall back to the shared account: records it creates cannot be attributed to the agent that made them." >&2
echo " Set MOSAIC_GIT_IDENTITY=<agent-id> or 'git config mosaic.gitIdentity <agent-id>'." >&2
return 1
fi
# 1. Mosaic credential loader (host → service mapping, run in subshell to avoid polluting env)
@@ -1,21 +1,48 @@
#!/bin/bash
# git-credential-mosaic — git credential helper — resolves Gitea tokens from
# the Mosaic credential store at runtime so remote URLs never embed secrets.
# git-credential-mosaic — git credential helper. Resolves a Gitea token 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):
# Per-agent identity (Gate-16 author != reviewer separation):
# git config mosaic.gitIdentity <agent-id> # per-worktree, persists on disk
# # or: export MOSAIC_GIT_IDENTITY=<agent-id>
#
# 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).
# ── WHY THIS FAILS CLOSED ──────────────────────────────────────────────────────
# This helper used to end by emitting the shared account's token for any request
# it could not resolve to an identity. A seat with no identity, or with an
# identity whose token was never provisioned, therefore received the most
# privileged credential configured on the host — silently, and indistinguishably
# from correct operation. Every record it then created (commit, push, PR, review)
# was attributed to that shared account, so author != reviewer separation was
# unenforceable and the true actor was unrecoverable after the fact.
#
# Under-provisioning must fail loudly, not impersonate. A refused git operation
# is recoverable in one command; a merged pull request attributed to the wrong
# principal is not.
#
# ── CONTRACT ───────────────────────────────────────────────────────────────────
# identity : MOSAIC_GIT_IDENTITY > git config mosaic.gitIdentity > the
# username git supplies on stdin
# store : chosen by what the identity IS, with no precedence and no
# cross-store fallback (see "Credential store selection" below)
# hit : emit username + password, exit 0
# miss : emit NOTHING, spool a durable escalation record, explain on
# stderr, exit 1 — git surfaces the failure and nothing is attributed
# unknown host : exit 0 with no output, no record (passthrough for non-Mosaic
# remotes handled by another helper)
#
# Backward compatibility is preserved for exactly one case: a host with no fleet
# and no identity requested still gets the shared account, because on such a host
# the shared account is the operator's own and there is no attribution to lose.
# A host that HAS a fleet has agents whose records must be distinguishable, so
# the shared fallback is refused there.
#
# A token is never written to stderr, to the escalation record, or to any log.
[ "$1" = "get" ] || exit 0
host=""; username_in=""
while IFS= read -r line; do
[ -z "$line" ] && break
@@ -24,46 +51,170 @@ while IFS= read -r line; do
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
# Recognized Gitea hosts carry the per-identity token scheme. Anything else is
# declined quietly — another helper owns it, and refusing would break it.
case "$host" in
git.uscllc.com) svc=gitea-usc;;
git.mosaicstack.dev) svc=gitea-mosaicstack;;
git.uscllc.com) idpfx=gitea-usc;;
git.mosaicstack.dev) idpfx=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"
ident="$MOSAIC_GIT_IDENTITY"; ident_src="MOSAIC_GIT_IDENTITY"
if [ -z "$ident" ]; then
ident=$(git config --get mosaic.gitIdentity 2>/dev/null)
ident_src="git config mosaic.gitIdentity"
fi
if [ -z "$ident" ]; then
ident="$username_in"
ident_src="the username git supplied"
fi
# ── Credential store selection ────────────────────────────────────────────────
# An identity is a SEAT or it is a SERVICE, and which one it is determines where
# its credential lives. There is no precedence rule between the two stores and no
# fallback from one to the other: a seat whose slot is empty fails closed rather
# than reading a service credential that happens to share its name.
#
# seat — <brain>/fleet/agents/<ident>/ exists
# credential at <brain>/fleet/agents/<ident>/secrets/<idpfx>-<ident>.token
# service — it does not
# credential at ~/.config/mosaic/secrets/gitea-tokens/<idpfx>-<ident>.token
#
# One credential, one location. Two copies of one credential diverge, and the
# stale copy fails in a way that reads as a revoked token rather than as drift.
#
# Brain-home resolution mirrors packages/mosaic/src/fleet/brain-home.ts and
# tools/fleet/start-agent-session.sh: MOSAIC_BRAIN_HOME wins, else ~/.mosaic.
brain_home="${MOSAIC_BRAIN_HOME:-$HOME/.mosaic}"
svc_store="$HOME/.config/mosaic/secrets/gitea-tokens"
idtok=""; ident_kind=""
if [ -n "$ident" ]; then
if [ -d "$brain_home/fleet/agents/$ident" ]; then
ident_kind="seat"
idtok="$brain_home/fleet/agents/$ident/secrets/${idpfx}-${ident}.token"
else
ident_kind="service identity"
idtok="$svc_store/${idpfx}-${ident}.token"
fi
if [ -r "$idtok" ]; then
echo "username=${ident}"
echo "password=$(cat "$idtok")"
exit 0
fi
fi
# ── Shared-account fallback: ONLY on a host with no fleet and no identity ──────
# `fleet/agents` existing is the same signal brain-home.ts uses to decide a brain
# is active. Where there are seats, records must be attributable, so an
# unresolvable request is refused instead of borrowing the shared account.
fleet_present=0
[ -d "$brain_home/fleet/agents" ] && fleet_present=1
if [ -z "$ident" ] && [ "$fleet_present" -eq 0 ]; then
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../_lib/credentials.sh
source "$script_dir/../_lib/credentials.sh"
load_credentials "$idpfx" >/dev/null 2>&1 || exit 0
# GITEA_USER is not populated by load_credentials (it exports GITEA_URL and
# GITEA_TOKEN only). Gitea's git-over-HTTP auth authenticates from the token in
# the password field, not from the username string, so any non-empty
# placeholder works — deliberately NOT a real account name, since framework
# files stay operator-agnostic (tools/quality/scripts/verify-sanitized.sh).
echo "username=${GITEA_USER:-git}"
echo "password=$GITEA_TOKEN"
exit 0
fi
# ── FAIL CLOSED ───────────────────────────────────────────────────────────────
if [ -z "$ident" ]; then
reason="no-identity"
else
reason="no-token-for-identity"
fi
seat="${MOSAIC_AGENT_NAME:-unknown}"
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
# The escalation RECORD is durable and unconditional; any notification built on
# top of it is best-effort. Record and alert are deduplicated separately — a cap
# on the alert alone lets the spool grow without bound exactly while the operator
# is being told nothing, so the louder the failure the quieter it gets.
#
# A record field is arbitrary operator-supplied text: an identity comes from git
# config or the environment, and cwd is whatever directory git ran in. Either can
# contain a quote or a backslash, which would make the line unparseable JSON --
# and a spool that silently stops parsing is worse than no spool, because the
# operator only discovers it while reading the record that explains an outage.
json_escape() {
local s=$1
s=${s//\\/\\\\}
s=${s//\"/\\\"}
s=${s//$'\t'/\\t}
s=${s//$'\r'/\\r}
s=${s//$'\n'/\\n}
printf '%s' "$s"
}
spool="${MOSAIC_CREDENTIAL_SPOOL:-$HOME/.local/state/mosaic-credential-escalations}"
spool_record=""
if mkdir -p "$spool" 2>/dev/null; then
chmod 700 "$spool" 2>/dev/null
spoolfile="$spool/$(date -u +%Y%m%d).jsonl"
dedupe="$spool/.spooled-${seat}-${ident:-none}-${reason}-$(date -u +%Y%m%d%H%M)"
if [ ! -e "$dedupe" ]; then
: > "$dedupe" 2>/dev/null
printf '{"ts":"%s","reason":"%s","identity":"%s","identity_source":"%s","kind":"%s","seat":"%s","host":"%s","cwd":"%s"}\n' \
"$(json_escape "$ts")" "$(json_escape "$reason")" \
"$(json_escape "${ident:-<unset>}")" "$(json_escape "$ident_src")" \
"$(json_escape "${ident_kind:-none}")" "$(json_escape "$seat")" \
"$(json_escape "$host")" "$(json_escape "$PWD")" \
>> "$spoolfile" 2>/dev/null
chmod 600 "$spoolfile" 2>/dev/null
fi
# Name the record only if one is actually on disk. Printing the path
# unconditionally sends the operator to a file that does not exist on exactly
# the hosts where the spool could not be created.
[ -s "$spoolfile" ] && spool_record="$spoolfile"
find "$spool" -maxdepth 1 -name '.spooled-*' -mmin +120 -delete 2>/dev/null
fi
cat >&2 <<EOF
git-credential-mosaic: REFUSED (fail-closed).
host : ${host}
identity : ${ident:-<unset>}${ident:+ (from ${ident_src}; resolved as a ${ident_kind})}
reason : ${reason}
EOF
if [ -n "$ident" ]; then
cat >&2 <<EOF
expected : ${idtok}
EOF
fi
cat >&2 <<EOF
No per-identity credential resolved. This helper does NOT fall back to the shared
account: that fallback makes every record it creates attributable to one
principal, which is unrecoverable once a pull request has merged under it.
Fix (pick one):
export MOSAIC_GIT_IDENTITY=<agent-id> # process-scoped
git config mosaic.gitIdentity <agent-id> # per-repo/worktree, persists
Then provision that identity's credential at the path named above. An identity
with a directory under \${MOSAIC_BRAIN_HOME:-\$HOME/.mosaic}/fleet/agents/ is a
seat and is read ONLY from its own secrets/ slot; any other identity is read from
~/.config/mosaic/secrets/gitea-tokens/. There is no fallback between the two.
If this identity legitimately needs git access and has none, ask the orchestrator
to provision one.
EOF
if [ -n "$spool_record" ]; then
echo " record: ${spool_record}" >&2
else
echo " record: NOT WRITTEN — spool unavailable at ${spool}" >&2
fi
exit 1
@@ -1,16 +1,26 @@
#!/usr/bin/env bash
# Regression harness for `git-credential-mosaic` — per-agent Gitea identity
# resolution (Gate-16 author≠reviewer separation).
# resolution (Gate-16 author≠reviewer separation) and fail-closed refusal.
#
# 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
# 2. Correct token file path chosen per host
# (gitea-usc-<id>.token vs gitea-mosaicstack-<id>.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).
# 3. Credential store selection: an identity with a directory under
# <brain>/fleet/agents/ is a SEAT and is read ONLY from its own secrets/
# slot; any other identity is a SERVICE and is read from the framework
# store. No precedence between them and NO fallback from one to the other.
# 4. Fail-closed: an identity that resolves but has no credential is REFUSED —
# no output, nonzero exit, a stderr diagnostic, and a durable spool record.
# The shared account is never emitted in its place.
# 5. Fail-closed: no identity resolvable on a host that runs a fleet is also
# REFUSED, because records made there must name the agent that made them.
# 6. Backward compatibility, the one surviving fallback: no identity AND no
# fleet -> shared account, unchanged. On such a host the shared account is
# the operator's own and there is no attribution to lose.
# 7. Unknown/unrelated host -> exits 0 with no output (passthrough).
# 8. Non-"get" verb -> exits 0 with no output.
#
# Uses stubbed token files under a fake HOME + a real (throwaway) git repo.
# NEVER reads real secrets or touches the real ~/.config/mosaic/secrets.
@@ -21,6 +31,9 @@ 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"
BRAIN_DIR="$WORK_DIR/brain"
SPOOL_DIR="$WORK_DIR/spool"
SVC_STORE="$FAKE_HOME/.config/mosaic/secrets/gitea-tokens"
# 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
@@ -28,10 +41,10 @@ REPO_DIR="$WORK_DIR/repo"
HELPER="$FAKE_HOME/.config/mosaic/tools/git/git-credential-mosaic"
rm -rf "$WORK_DIR"
mkdir -p "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens" \
mkdir -p "$SVC_STORE" \
"$FAKE_HOME/.config/mosaic/tools/git" \
"$FAKE_HOME/.config/mosaic/tools/_lib" \
"$REPO_DIR"
"$REPO_DIR" "$BRAIN_DIR"
cp "$SCRIPT_DIR/git-credential-mosaic" "$HELPER"
chmod +x "$HELPER"
@@ -68,7 +81,8 @@ run_helper() {
local host="$1" username_in="$2"; shift 2
(
cd "$REPO_DIR"
env -i HOME="$FAKE_HOME" PATH="$PATH" "$@" bash "$HELPER" get <<EOF
env -i HOME="$FAKE_HOME" PATH="$PATH" MOSAIC_CREDENTIAL_SPOOL="$SPOOL_DIR" "$@" \
bash "$HELPER" get <<EOF
host=$host
username=$username_in
@@ -76,20 +90,61 @@ EOF
)
}
# A refusal must be observable in four independent ways: nonzero exit, EMPTY
# stdout, a stderr diagnostic naming the identity and host, and — the assertion
# that actually catches a regression to the old behavior — NO shared token value
# anywhere in the output. Checking only the exit code would pass against a helper
# that emitted the shared credential and then exited 1.
assert_fail_closed() {
local desc="$1" host="$2" username_in="$3" want_in_stderr="$4"; shift 4
local stderr_file="$WORK_DIR/stderr.tmp"
: > "$stderr_file"
set +e
local stdout
stdout=$(run_helper "$host" "$username_in" "$@" 2>"$stderr_file")
local rc=$?
set -e
local stderr
stderr=$(cat "$stderr_file")
if [[ "$rc" -eq 0 ]]; then
echo "FAIL: $desc — expected nonzero exit, got 0 (stdout='$stdout')" >&2
fail=1
fi
if [[ -n "$stdout" ]]; then
echo "FAIL: $desc — expected empty stdout (nothing emitted), got '$stdout'" >&2
fail=1
fi
if [[ "$stdout$stderr" == *"shared-mosaicstack-token"* || "$stdout$stderr" == *"shared-usc-token"* ]]; then
echo "FAIL: $desc — a SHARED token value appeared in the output. The shared-account fallback must be gone:" >&2
echo "$stdout$stderr" >&2
fail=1
fi
if [[ -n "$want_in_stderr" && "$stderr" != *"$want_in_stderr"* ]]; then
echo "FAIL: $desc — stderr does not contain '$want_in_stderr':" >&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
}
# ---------------------------------------------------------------------------
# 1. No identity resolvable anywhere, no per-slot token -> shared fallback
# (backward-compat: unchanged behavior when nothing is configured).
# 1. Backward compatibility: nothing resolvable, and NO fleet on this host ->
# shared account, unchanged. This is the only surviving fallback.
# ---------------------------------------------------------------------------
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=')"
assert_eq "no identity + no fleet: username" "username=git" "$(echo "$out" | grep '^username=')"
assert_eq "no identity + no fleet: 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.
# 2. git-supplied username resolves to a SERVICE identity WITH a token in the
# framework store -> that identity + token wins over the shared account.
# ---------------------------------------------------------------------------
echo -n "agentA-mosaicstack-token" > "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentA.token"
echo -n "agentA-mosaicstack-token" > "$SVC_STORE/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=')"
@@ -97,7 +152,7 @@ assert_eq "username-resolved identity: password" "password=agentA-mosaicstack-to
# ---------------------------------------------------------------------------
# 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"
echo -n "agentB-mosaicstack-token" > "$SVC_STORE/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=')"
@@ -106,54 +161,210 @@ assert_eq "git-config beats username: password" "password=agentB-mosaicstack-tok
# ---------------------------------------------------------------------------
# 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"
echo -n "agentC-mosaicstack-token" > "$SVC_STORE/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).
# 5. Correct token PATH is chosen per host: same agent id, different host
# prefix (gitea-usc- vs gitea-mosaicstack-).
# ---------------------------------------------------------------------------
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"
echo -n "agentD-usc-token" > "$SVC_STORE/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).
# 6. FAIL CLOSED — identity resolves, no credential for it on this host. Must
# NOT borrow the shared account, and must NOT leak the same agent's token
# for a DIFFERENT host (agentD holds a usc token and no mosaicstack one).
# ---------------------------------------------------------------------------
assert_fail_closed "cross-host absence refuses (no shared fallback, no cross-host leak)" \
"git.mosaicstack.dev" "agentD" "gitea-mosaicstack-agentD.token"
# The agent's own usc token must not appear either.
: > "$WORK_DIR/stderr.tmp"
set +e
leak_out=$(run_helper "git.mosaicstack.dev" "agentD" 2>"$WORK_DIR/stderr.tmp")
set -e
if [[ "$leak_out$(cat "$WORK_DIR/stderr.tmp")" == *"agentD-usc-token"* ]]; then
echo "FAIL: cross-host leak — the usc token value appeared on a mosaicstack request" >&2
fail=1
fi
assert_fail_closed "unknown identity refuses (shared account never substituted)" \
"git.mosaicstack.dev" "no-such-agent" "no-token-for-identity"
# ---------------------------------------------------------------------------
# 7. A refusal leaves a durable spool record, and that record contains no token.
# The stderr diagnostic is transient; the record is what an operator reads
# afterwards, so it must exist independently of anyone watching the terminal.
# ---------------------------------------------------------------------------
spool_file=$(find "$SPOOL_DIR" -maxdepth 1 -name '*.jsonl' | head -n 1)
if [[ -z "$spool_file" ]]; then
echo "FAIL: fail-closed left no spool record under $SPOOL_DIR" >&2
fail=1
else
spool_body=$(cat "$spool_file")
assert_eq "spool record names the refused identity" "1" \
"$(grep -c '"identity":"no-such-agent"' "$spool_file" | head -n 1)"
if [[ "$spool_body" == *"shared-"*"-token"* || "$spool_body" == *"agentD-usc-token"* ]]; then
echo "FAIL: spool record contains a token value:" >&2
echo "$spool_body" >&2
fail=1
fi
fi
# ---------------------------------------------------------------------------
# 8. SEAT identity: an id with a directory under <brain>/fleet/agents/ is read
# from its OWN secrets/ slot, not from the framework store.
# ---------------------------------------------------------------------------
mkdir -p "$BRAIN_DIR/fleet/agents/seatE/secrets"
echo -n "seatE-slot-token" > "$BRAIN_DIR/fleet/agents/seatE/secrets/gitea-mosaicstack-seatE.token"
out=$(run_helper "git.mosaicstack.dev" "seatE" MOSAIC_BRAIN_HOME="$BRAIN_DIR")
assert_eq "seat reads its own slot: username" "username=seatE" "$(echo "$out" | grep '^username=')"
assert_eq "seat reads its own slot: password" "password=seatE-slot-token" "$(echo "$out" | grep '^password=')"
# ---------------------------------------------------------------------------
# 9. NO CROSS-STORE FALLBACK — the assertion this whole store-selection design
# exists for. seatF is a seat (it has a directory) with an EMPTY slot, while
# a framework-store token of the identical name is present and readable.
# The helper must refuse rather than read it: one credential, one location,
# and a seat that reads a same-named service credential is exactly the
# silent-substitution failure the fail-closed rule removes.
# ---------------------------------------------------------------------------
mkdir -p "$BRAIN_DIR/fleet/agents/seatF/secrets"
echo -n "seatF-SERVICE-STORE-token" > "$SVC_STORE/gitea-mosaicstack-seatF.token"
assert_fail_closed "seat with empty slot does NOT fall back to the framework store" \
"git.mosaicstack.dev" "seatF" "fleet/agents/seatF/secrets" MOSAIC_BRAIN_HOME="$BRAIN_DIR"
: > "$WORK_DIR/stderr.tmp"
set +e
xstore_out=$(run_helper "git.mosaicstack.dev" "seatF" MOSAIC_BRAIN_HOME="$BRAIN_DIR" 2>"$WORK_DIR/stderr.tmp")
set -e
if [[ "$xstore_out$(cat "$WORK_DIR/stderr.tmp")" == *"seatF-SERVICE-STORE-token"* ]]; then
echo "FAIL: cross-store fallback — a seat read the framework store's same-named token" >&2
fail=1
fi
# Control: that framework-store token IS readable, so the refusal above is the
# store rule firing and not an unreadable file. A non-seat identity pointed at
# the same file gets it.
out=$(run_helper "git.mosaicstack.dev" "seatF" MOSAIC_BRAIN_HOME="$WORK_DIR/no-such-brain")
assert_eq "control — same file IS readable for a non-seat identity" \
"password=seatF-SERVICE-STORE-token" "$(echo "$out" | grep '^password=')"
# ---------------------------------------------------------------------------
# 10. FAIL CLOSED — no identity resolvable, but this host runs a fleet. Where
# seats exist, an unattributable request is refused instead of receiving
# the shared account. Contrast with case 1, which is the same request on a
# host with no fleet and still returns the shared account.
# ---------------------------------------------------------------------------
git -C "$REPO_DIR" config --unset mosaic.gitIdentity 2>/dev/null || true
assert_fail_closed "no identity on a fleet host refuses" \
"git.mosaicstack.dev" "" "no-identity" MOSAIC_BRAIN_HOME="$BRAIN_DIR"
# ---------------------------------------------------------------------------
# 11. The brain home defaults to ~/.mosaic when MOSAIC_BRAIN_HOME is unset —
# the fleet gate must fire on the default path too, not only on an
# explicitly injected one. Case 1 ran before this directory existed; the
# same call now refuses, which also proves case 1 was measuring the
# no-fleet branch rather than passing for an unrelated reason.
# ---------------------------------------------------------------------------
mkdir -p "$FAKE_HOME/.mosaic/fleet/agents"
assert_fail_closed "fleet gate fires on the default ~/.mosaic brain home" \
"git.mosaicstack.dev" "" "no-identity"
# ---------------------------------------------------------------------------
# 12. Unrelated/unknown host -> exit 0, no output (passthrough for non-Gitea
# remotes, e.g. github.com via a different credential helper). A fleet host
# must not refuse a host this helper does not own.
# ---------------------------------------------------------------------------
out=$(run_helper "github.com" "agentA")
assert_eq "unknown host: no output" "" "$out"
out=$(run_helper "github.com" "" MOSAIC_BRAIN_HOME="$BRAIN_DIR")
assert_eq "unknown host on a fleet host: still passthrough, not a refusal" "" "$out"
# ---------------------------------------------------------------------------
# 8. Non-"get" verb (store/erase) -> exit 0, no output (git-credential
# protocol: this helper only implements get).
# 13. 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 <<EOF
host=git.mosaicstack.dev
username=agentA
username=no-such-agent
password=whatever
EOF
)
assert_eq "store verb: no output" "" "$store_out"
# ---------------------------------------------------------------------------
# 14. The escalation record is machine-readable even when a field carries a
# quote or a backslash. A cwd is arbitrary operator text; an unescaped one
# silently turns the spool into unparseable JSONL, and the operator only
# finds out while reading the record that explains an outage.
# ---------------------------------------------------------------------------
hostile_dir="$WORK_DIR/we\"ird\\dir"
mkdir -p "$hostile_dir"
hostile_spool="$WORK_DIR/spool-hostile"
(
cd "$hostile_dir"
env -i HOME="$FAKE_HOME" PATH="$PATH" MOSAIC_CREDENTIAL_SPOOL="$hostile_spool" \
MOSAIC_GIT_IDENTITY=no-such-agent \
bash "$HELPER" get <<EOF >/dev/null 2>&1
host=git.mosaicstack.dev
username=no-such-agent
EOF
) || true
# Deliberately not `ls ... | head -1`: under `set -o pipefail` a missed glob
# makes ls exit 2, the pipeline inherits it, and `set -e` kills this suite with
# zero output — the same silent-nonzero failure this file exists to catch.
record_file=""
for candidate in "$hostile_spool"/*.jsonl; do
if [[ -e "$candidate" ]]; then
record_file="$candidate"
break
fi
done
if [[ -z "$record_file" ]]; then
echo "FAIL: hostile cwd — no escalation record was written at all" >&2
fail=1
elif ! python3 -c 'import json,sys
for line in open(sys.argv[1]):
line = line.strip()
if line:
json.loads(line)' "$record_file" 2>/dev/null; then
echo "FAIL: hostile cwd — escalation record is not parseable JSONL:" >&2
cat "$record_file" >&2
fail=1
fi
# ---------------------------------------------------------------------------
# 15. When the spool cannot be created, the diagnostic must NOT name a record
# path. Naming a file that was never written sends the operator to an
# empty path on exactly the hosts where the escalation was lost.
# ---------------------------------------------------------------------------
unwritable_spool="/proc/mosaic-credential-spool-cannot-exist"
nospool_err=$(
cd "$REPO_DIR"
env -i HOME="$FAKE_HOME" PATH="$PATH" MOSAIC_CREDENTIAL_SPOOL="$unwritable_spool" \
MOSAIC_GIT_IDENTITY=no-such-agent \
bash "$HELPER" get <<EOF 2>&1 >/dev/null
host=git.mosaicstack.dev
username=no-such-agent
EOF
) || true
if [[ "$nospool_err" == *"record: $unwritable_spool/"* ]]; then
echo "FAIL: unwritable spool — diagnostic names a record file that was never written" >&2
fail=1
fi
if [[ "$nospool_err" != *"NOT WRITTEN"* ]]; then
echo "FAIL: unwritable spool — diagnostic does not say the record was not written" >&2
echo "$nospool_err" >&2
fail=1
fi
if [[ "$fail" -eq 0 ]]; then
echo "git-credential-mosaic identity resolution regression passed"
fi
@@ -23,6 +23,17 @@
# 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.
# 7. Credential store selection: an identity with a directory under
# <brain>/fleet/agents/ is a SEAT and is read ONLY from its own secrets/
# slot; any other identity is a SERVICE and is read from the framework
# store. No precedence between them and NO fallback from one to the
# other — a seat with an empty slot is REFUSED even when a same-named
# token sits in the framework store.
# 8. Fail loud when NO identity resolves on a host that runs a fleet: where
# seats exist, an unattributable API call is refused rather than made
# under the shared account. On a host with no fleet the same call still
# returns the shared token (case 1), which is what keeps this change a
# no-op for non-fleet operators of the framework.
#
# 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.
@@ -210,6 +221,94 @@ if [[ "$err" == *"no per-slot token at"* ]]; then
fi
git -C "$REPO_DIR" config --unset mosaic.gitIdentity
# ---------------------------------------------------------------------------
# 7. SEAT identity: an id with a directory under <brain>/fleet/agents/ is read
# from its OWN secrets/ slot, not from the framework store. The brain home
# is resolved exactly as packages/mosaic/src/fleet/brain-home.ts does it:
# MOSAIC_BRAIN_HOME, else ~/.mosaic.
# ---------------------------------------------------------------------------
BRAIN_DIR="$WORK_DIR/brain"
mkdir -p "$BRAIN_DIR/fleet/agents/seatE/secrets"
echo -n "seatE-slot-token" > "$BRAIN_DIR/fleet/agents/seatE/secrets/gitea-mosaicstack-seatE.token"
out=$(call_get_gitea_token "git.mosaicstack.dev" MOSAIC_GIT_IDENTITY=seatE MOSAIC_BRAIN_HOME="$BRAIN_DIR")
assert_eq "seat reads its own slot" "seatE-slot-token" "$out"
# ---------------------------------------------------------------------------
# 8. NO CROSS-STORE FALLBACK. seatF is a seat (it has a directory) with an
# EMPTY slot, while a framework-store token of the identical name is
# present and readable. It must be REFUSED rather than served that token:
# one credential, one location. A seat that silently reads a same-named
# service credential is the same substitution failure as the shared-account
# fallback, one store further down.
# ---------------------------------------------------------------------------
mkdir -p "$BRAIN_DIR/fleet/agents/seatF/secrets"
echo -n "seatF-SERVICE-STORE-token" > "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-seatF.token"
assert_failloud "seat with empty slot does NOT fall back to the framework store" \
"git.mosaicstack.dev" "seatF" \
"$BRAIN_DIR/fleet/agents/seatF/secrets/gitea-mosaicstack-seatF.token" \
MOSAIC_GIT_IDENTITY=seatF MOSAIC_BRAIN_HOME="$BRAIN_DIR"
# assert_failloud only screens stderr for the word "shared"; this store's token
# is not named that, so check for its value explicitly.
set +e
xstore_out=$(call_get_gitea_token "git.mosaicstack.dev" MOSAIC_GIT_IDENTITY=seatF MOSAIC_BRAIN_HOME="$BRAIN_DIR" 2>"$WORK_DIR/stderr-xstore.tmp")
set -e
if [[ "$xstore_out$(cat "$WORK_DIR/stderr-xstore.tmp")" == *"seatF-SERVICE-STORE-token"* ]]; then
echo "FAIL: cross-store fallback — a seat was served the framework store's same-named token" >&2
fail=1
fi
# Control: that framework-store token IS readable, so the refusal above is the
# store rule firing and not an unreadable file. The same id, resolved against a
# brain home where it is not a seat, gets it.
out=$(call_get_gitea_token "git.mosaicstack.dev" MOSAIC_GIT_IDENTITY=seatF MOSAIC_BRAIN_HOME="$WORK_DIR/no-such-brain")
assert_eq "control — same file IS readable for a non-seat identity" "seatF-SERVICE-STORE-token" "$out"
# ---------------------------------------------------------------------------
# 9. FAIL LOUD — no identity resolvable, but this host runs a fleet. Contrast
# with case 1: the identical call on a host with no fleet still returns the
# shared token.
# ---------------------------------------------------------------------------
git -C "$REPO_DIR" config --unset mosaic.gitIdentity 2>/dev/null || true
set +e
noid_out=$(call_get_gitea_token "git.mosaicstack.dev" MOSAIC_BRAIN_HOME="$BRAIN_DIR" 2>"$WORK_DIR/stderr-noid.tmp")
noid_rc=$?
set -e
noid_err=$(cat "$WORK_DIR/stderr-noid.tmp")
if [[ "$noid_rc" -eq 0 ]]; then
echo "FAIL: no identity on a fleet host — expected nonzero return, got 0 (stdout='$noid_out')" >&2
fail=1
fi
if [[ -n "$noid_out" ]]; then
echo "FAIL: no identity on a fleet host — expected empty stdout, got '$noid_out'" >&2
fail=1
fi
if [[ "$noid_out" == *"shared-mosaicstack-token"* || "$noid_err" == *"shared-mosaicstack-token"* ]]; then
echo "FAIL: no identity on a fleet host — the shared token was served anyway:" >&2
echo "$noid_out$noid_err" >&2
fail=1
fi
if [[ "$noid_err" != *"MOSAIC_GIT_IDENTITY"* ]]; then
echo "FAIL: no identity on a fleet host — stderr does not say how to set an identity:" >&2
echo "$noid_err" >&2
fail=1
fi
# ---------------------------------------------------------------------------
# 10. The brain home defaults to ~/.mosaic when MOSAIC_BRAIN_HOME is unset —
# the fleet gate must fire on the default path too, not only on an
# explicitly injected one. Case 1 ran before this directory existed; the
# same call now refuses, which also proves case 1 was measuring the
# no-fleet branch rather than passing for an unrelated reason.
# ---------------------------------------------------------------------------
mkdir -p "$FAKE_HOME/.mosaic/fleet/agents"
set +e
dflt_out=$(call_get_gitea_token "git.mosaicstack.dev" 2>"$WORK_DIR/stderr-dflt.tmp")
dflt_rc=$?
set -e
if [[ "$dflt_rc" -eq 0 || -n "$dflt_out" ]]; then
echo "FAIL: fleet gate did not fire on the default ~/.mosaic brain home (rc=$dflt_rc stdout='$dflt_out')" >&2
fail=1
fi
if [[ "$fail" -eq 0 ]]; then
echo "get_gitea_token identity resolution regression passed"
fi