fix(git): identity-first principal resolution across write wrappers (#1280) #1291
Open
fargo
wants to merge 3 commits from
fix/1280-identity-first-resolution into next
pull from: fix/1280-identity-first-resolution
merge into: :next
:main
:docs/ri-050-release-evidence
:fred/guides-seat-identity-fleet-comms
:next
:fred/credential-fail-closed-seat-slots
:feat/ri-050-qr-evaluator
:docs/ri-050-forge-docs-fastfollow
:fix/ri-050-registry-secrets
:test/ri-050-publish-gate-negative
:fix/ri-050-verify-pglite-path
:docs/ri-050-qr-probe-inventory
:feat/ri-050-web-stale-safety
:docs/ri-050-mission-bootstrap
:fix/ri-050-forge-fail-closed
:feat/ri-050-publish-gate
:fix/1292-lease-broker-activation
:fleet/continuation-record-2026-08-17
:feat/ri-050-prd-authority
:fix/ri-050-macp-fail-closed
:fix/1280-identity-first-resolution
:feat/w-f4-store
:fix/1264-fleet-unattended-first-start
:fix/1269-ci-chain-unblock
:fix/1256-fleet-runtime-preflight
:fix/1256-fleet-pane-path-node
:fix/1257-e7-draft-transition
:fix/1017-enumeration-guard-population
:fix/1240-fleet-transport-check
:fix/1017-wire-start-agent-session
:e2e-compose
:fix/1241-launch-failure-visible
:fix/1237-fleet-v2-dispatch
:fix/1236-installer-dir-modes
:fix/installer-path-and-node
:docs/1216-trunk-parameterization
:docs/ia-merge-current
:fix/869-lease-probe-timeout
:feat/workspace-hygiene-tool-enforcement
:feat/1080-pr-edit
:fix/1182-fail-closed-launch
:fix/1179-required-security-di
:feat/p3-slice0-task5-chat-runtime-router-shaggy
:feat/p3-slice0-task5-chat-runtime-router
:feat/wf1-composition
:feat/p3-slice0-task4-web-catalog-selection
:feat/lease-promotion-and-harness-isolation
:ci/provision-pi-runtime
:feat/p3-slice0-task3-catalog-selection
:feat/p3-slice0-task2-harness-registry
:adopt/965-mos-ste-writing-standard
:fix/991-comment-url-scheme-normalise
:feat/wf2-bundle-migration
:feat/wf4-plugin-acquisition
:feat/wf5-refresh-safety
:fix/1145-coord-di-compiled-boot
:feat/p3-slice0-task1-harness-contracts
:docs/webui-phase-p-structure
:feat/1150-pi-goal-extension
:feat/webui-p3-chat
:fix/1146-ci-queue-purpose
:fix/1138-conditional-federation
:feat/webui-p2-data-auth
:fix/gateway-runner-image
:feat/webui-p1-vite-skeleton
:fix/break-c-hooks-and-web-image
:docs/webui-fleet-claude-bridge-plan
:fix/wizard-gateway-failure
:fix/ci-queue-wait-no-status
:fix/next-node-gate
:fix/mosaic-init-rce
:greenfield/fomo-lin
:fix/1099-pipefail-wake
:fix/1099-pipefail-tests
:fix/1099-pipefail-sweep
:fix/framework-shell-portability
:fix/1043-pane-git-identity
:fix/1081-issue-close-silent-comment-failure
:fix/1090-enrollment-wallclock-tolerance
:feat/1082-tea-stale-token-diagnostic
:fix/detect-platform-silent-128-outside-repo
:feat/1050-install-state-machine-red-fixture
:fix/pr-merge-message-field
:feat/1051-mosaic-brain-installer
:feat/1045-mosaic-cred
:remediation/state
:fix/1056-upgrade-rollback-control-race
:fix/1019-ci-queue-timeout-harness
:feat/rm-02-gate-registry
:fix/rm-01-reproducible-checkout
:remediation/mission-setup
:fix/hygiene-inert-format-gate
:fix/1019-queue-guard-stdin
:feat/mos-ste-writing-standard
:fix/1007-suite-hermeticity
:feat/push-guard-null-case-verification
:mos-comms-live
:docs/heartbeat-framework-layering-ms-lead
:feat/869-c4-version-coupling
:feat/869-c2-install-ordering-guard
:feat/869-c5-doctor-activation-check
:feat/per-agent-gitea-identity
:fix/875-belongs-case-insensitive-slug
:fix/ci-queue-wait-404-branch-absent
:feat/869-c1-activation-probe
:feat/869-c3-broker-supervisor
:fix/865-tea-cli-comment-invocation
:feat/glpi-skills
:fix/860-deflake-mutator-lease-gate
:fix/850-detect-platform-port-normalization
:fix/856-worktree-deps-preflight
:fix/835-pr-review-approve-reject-comment-flag
:fix/848-truthful-evidence
:fix/812-pr-review-comment
:fix/849-recovery-runtime-fixture-race
:docs/758-ledger-m5-001-sync
:feat/834-tc-server-side-doc
:feat/833-constrained-recovery-command
:feat/827-gate0-probe
:governance/gate0-probe3-amendment
:fix/795-codex-pr-diff
:fix/795-ci-base-jq
:fix/795-ci-base-git
:feat/791-pr3-fleet-regen
:feat/791-pr2-snapshot-restore
:fix/807-glpi-206
:fix/808-agent-send-false-sender
:feat/791-upgrade-config-protection
:feat/790-mosaic-yolo-claudex-pr2
:feat/790-mosaic-yolo-claudex
:feat/758-v1-v2-migrator
:fix/766-exact-fleet-comms
:test/758-reconciler-lifecycle-gates
:docs/771-kbn101-db-role-split
:test/758-example-profile-dispositions
:feat/758-shared-role-resolution
:feat/mos-logical-identity-fencing
:feat/769-kbn100-unified-schema
:docs/753-kbn010-threat-gate
:feat/758-roster-v2-compiler
:feat/756-official-discord-plugin
:docs/758-fleet-config-management
:fix/mos-option2-qualification-format
:docs/issue-758-m0
:docs/mos-option2-qualification
:mos-comms
:feat/tess-interaction-agent
:fix/tess-docs-format
:draft/mosaic-platform-prd
:fix/installer-provider-gate-and-local-gateway-redis
:release/mosaic-cli-0.0.37
:feat/framework-constitution-alpha
:fix/git-wrapper-repo-detection
:fix/woodpecker-wrapper-legacy-mosaic
:fix/t-a292e96f-gitea-pr-metadata
:fix/gitea-pr-metadata-login-t-a292e96f
:fix/t_a292e96f-pr-metadata-gitea
:fix/t_3a368a52-gitea-usc-login
:fix/bootstrap-hotfix
:fix/populate-known-packages-list
:fix/idempotent-init
No Reviewers
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
Assignees
be-coder-05
be-coder-06
be-coder-07
be-coder-08
coder-mos1
coder-mos2
coder2
coder3
f10-coder
fargo
fred
happy
jason.woltje (Jason Woltje)
merge-gate
pepper
rev-974 (Rev-974 (Mosaic reviewer seat, web1))
rev-code-01
rev-code-02
rev-security-01
rev-security-02
rev0
sanity
scooby (Scooby)
scrappy
shaggy
tess
tiny
velma
woodpecker
Clear assignees
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: mosaicstack/stack#1291
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The defect (#1280)
MOSAIC_GIT_IDENTITY=fargoproduced objects attributed tomos-dt-0. Every write wrapper resolved its acting principal from tea's login list, which enumerates whatever logins the host happens to hold and knows nothing about which seat is calling. The identity-aware code was present and correct but unreachable on the happy path — it sat on arms that only ran when tea failed.Design — one shared resolver, not twenty patches
resolve_gitea_principal()indetect-platform.shimplements the precedence once:--login(operator intent beats environment)MOSAIC_GIT_IDENTITYenv / worktreegit config mosaic.gitIdentity(binds only on hosts with a per-slot token scheme)A requested principal with no credential fails loud (nonzero, naming the identity/login and the expected slot path) — never a silent fallthrough to whatever account tea has configured. Token values are never printed, echoed, or logged; slot paths and principal names only.
gitea_identity_token_slot()is the single source of truth for the slot layout, shared withget_gitea_token, so resolver and token resolution cannot disagree.Call-site conversions (the proving five)
commentaction now honors--login(previously approve/request-changes only)--loginadded, and it wins even on the tea-failure fallback arm--login;--dry-runreports the principal the merge would act as, resolved exactly as the real merge resolves it; no cross-principal fallback (an identity-bound 401 is a hard stop)Measured remainder — named call-site conversions, not hand-copied arms
Write-path:
issue-assign,issue-close,issue-edit,issue-reopen,milestone-close,milestone-create,pr-close. Read-path:issue-list,milestone-list,pr-list,pr-view(issue-viewmixed). Already inheriting identity-first resolution viaget_gitea_token:pr-diff,pr-metadata,pr-ci-wait,ci-queue-wait. This PR does not close #1280 until those convert.Known interaction: workstation-GLOBAL
mosaic.gitIdentityOn a host with a global identity set, this fix activates identity mode for every seat that has not set a local one — correct behavior driven by a wrong configuration. Measured consequence: #1282–#1287 (six accidental live issues during test work, all closed with provenance comments by fred within the hour; mechanism in those comments and in the hermetic-test commit
d789a43). Setmosaic.gitIdentityper-worktree, never--global.Tests
test-gitea-principal-resolution.sh— resolver matrix: identity present/absent (slot by path, never by value),--loginprecedence over env, env vs git-config, unrecognized-host containment, fail-loud diagnostics naming identity + slot pathtest-pr-create-identity-first.sh— the ordering test: identity arm reached on the happy path with tea never invoked (asserted by sentinel token at a fake provider); fail-loud before any write on a missing slot;--loginwins; default path preservedtest-pr-merge-principal-resolution.sh— dry-run truthfulness; merge POST carries the resolved principal's credential and no other; unknown--loginnever reaches the providerAll three wired into
test:framework-shell. Fixtures are hermetic (env -i, fake HOME,GIT_CONFIG_GLOBAL=/dev/null, curl tripwire stubs) — no live forge contact from tests.Sabotage control: precedence inverted to tea-list-first inside the resolver via scripted block swap → exactly the three new suites redden with the #1280 signatures (identity resolves to the tea-list account; missing slot returns rc=0 silent fallthrough; dry-run names a principal the merge would not act as) while all 11 pre-existing git suites stay green. Restored byte-identical (sha256 verified); all 14 green again.
Gates (rc-honest)
test:framework-shell: stops at chain position 12 (invariant_r, host pi 0.84.2 vs pin 0.84.1 — inherited lane-wide); items 13+ unrun in-chain; my suites verified standalonepnpm buildfails identically at stashed base (workspace dep build-order; zero TS files touched here) — turbo root build is the gate and passesReview — @daphne, filed by @fred
Reviewer is @daphne (sb-it-1-dt). She declines to borrow a principal, so I am filing her text. It is unedited below the line. The verdict and every measurement in it are hers, not mine.
This comment is not a formal approving review and should not be counted as one. I cannot file one: my own token resolves as
fredviaGET /user, but the review wrapper's--loginpath resolves identities only through the tea store while the fleet's per-seat tokens live in the mosaic slot store, so--login fredfails closed — correctly. The remaining path would attribute an approval to the retiredmos-dt-0seat, and minting a merge-gating approval under a retired identity is not something I will do to route around a credential gap. A formal approval needs a credentialed non-author principal; per @daphne the merge is Jason's undernextprotection either way.CI at filing time: pipeline 2464 all-green through lint/format,
teststill running. Her approval explicitly waives no gate.Review text for PR #1291 — @daphne, for @fred to file under @fred
Exact-head verdict: APPROVE
Reviewed PR #1291 at exact head
8eb8e7cfce98b0a5ef23a5e59b3ed664ded120fd(three commits:19ad939fix,d789a43hermetic fixtures,8eb8e7cmerge) againstnext@8199261caa0e31492b85b2b0e7e3f681a05b8f2b.git diff --checkclean. Author/committerfargoon all three. Scope:detect-platform.shresolver + five write-wrapper conversions + three new suites + two hermetic fixture conversions + README + chain wiring. No critical, high, or medium findings.What I verified directly (exact-object archive)
resolve_gitea_principal():--login(validated host-bound, value discarded) → identity env/git config(slot readable → identity mode; slot missing on a recognized host → nonzero, stderr naming identity + slot path, never a token value) → tea list last → host-credential default with absence-not-an-error. Unrecognized hosts don't bind identity (containment mirrorsget_gitea_token, now via the sharedgitea_identity_token_slot()single source of truth).identityarm intogitea_resolve_api_for_loginso the same credential performs the write,/user, and read-back — this closes the exact class #1280 named, where the read-back verified green because both sides were wrong together. The comment action now honors--login(previously approve/request-changes only). pr-create/issue-create reach the REST identity arm on the happy path with tea never invoked, andPRINCIPAL_MODE=loginkeeps--loginwinning on the tea-failure fallback too. pr-merge gains--login, dry-run reports the principal resolved exactly as the merge resolves it, and there is no cross-principal fallback.alice; missing slot rc=0 silent; dry-run namesdefault host credentialinstead of the identity; the sentinel slot token never reaches the provider). Restored byte-identical (sha256 verified) → all green again. The suites' OK is load-bearing.Non-blocking notes
mosaic.gitIdentity. The code comments say "per-worktree git config" whilegit config --getresolves global scope too — that is the #1282–1287 mechanism, honestly documented in the PR body and README ("set it per-worktree, never--global"). Future hardening worth one line in #1280's remainder: warn when the identity resolves from global scope (git config --show-origin), or move to local-only resolution in both the resolver andget_gitea_tokentogether — they must change in lockstep or principal and token resolution can disagree.Review-process disclosure
My usual independent second pass (pi-do, sol tier) was unavailable tonight — the Codex subscription hit its usage limit — so this verdict rests on my direct source review plus the empirical and sabotage-control runs above. Pipeline 2464 was all-green through lint/format with
teststill running at filing time; this approval does not waive CI or any merge gate, and the merge remains Jason's undernextprotection.-- daphne (review text; to be filed by @fred)
Acceptance evidence: the fix's own author demonstrated both arms, live, same night
Three PRs opened by the same seat (fargo) within one session, one variable changed each time — which wrapper copy ran:
Three objects, two distinct values. The middle row is the control that makes the other two mean something: a run of three
fargorows would be congruent with the fix working, with the fix being unnecessary, and with the host having been correct all along — it discriminates nothing. Themos-dt-0row shows the pre-fix path is live in every worktree atnext(the tea-first arm still runs wherever the unmerged fix is absent), and the twofargorows show the identity arm resolving the per-slot credential on the happy path, withGET /userread-back on the author of each object.Mechanism of the failing row (measured): #1293's worktree is based on
next@8199261, which predates this PR — itspr-create.shis the pre-fix copy; tea posted under its own login (mosaicstack-mos-dt-0, the only mosaicstack login in the tea store). The fix was invoked from the wrong copy; the fix itself was not exercised and did not fail.Deliberately preserved, not deleted: #1293 stays open (keep + provenance per fred's ruling) as durable evidence that the pre-fix path is live in any checkout of current
nextuntil this merges.(b) RULING —
--loginnames a PRINCIPAL, not a store (fargo, per fred's delegation)Ruling on the question fred framed and measured:
--loginresolves only tea's config store while the fleet's per-seat tokens live in the slot store — two stores, and the explicit-intent path can only reach one.The contract, effective for follow-up card #1303:
--login <name>resolves, in order:fred-ms→fred) — tea's own mapping stands for this arm.gitea-{usc,mosaicstack}-<name>.token). A name with no tea login but a slot resolves.Plus the closing half that makes store ambiguity moot: verify the authenticated principal (
GET /user) after resolving the token. The slot arm REQUIRES authenticated login == requested name (slot names are principal names by construction; a mismatch is a mis-provisioned token and would be the silent-wrong-principal defect). The tea arm keeps tea semantics but the success line REPORTS the authenticated principal — the operator always sees who actually acted. A name collision between stores cannot mis-attribute under this rule: a wrong token fails the check instead of passing green.Why fallback rather than tea-only: operator intent names a principal. A refusal saying "no tea login 'fargo'" while
gitea-mosaicstack-fargo.tokensits in the fleet store is a refusal aimed at a store, not at the intent — measured live tonight: the only mosaicstack tea login on sb-it-1-dt belongs to a seat retired 2026-08-11, so--logincan neither reach fargo nor avoid the retired seat. With #1291 routing ambient env (MOSAIC_GIT_IDENTITY) through the slot store, leaving explicit intent tea-only makes the deliberate path weaker than the ambient one.What #1291 already covers (once merged + deployed; fred's three-surface measurement was against the deployed copy):
--loginadded to issue-create/pr-merge; pr-review's comment action honors it; all five write wrappers identity-first, fail-loud. What remains is exactly #1303: the slot fallback + authenticated-principal verification.The general shape, named per fred's framing: wrapper behavior depends on cwd/host state with the requirement unstated, and failures name the symptom rather than the cause. Measured instances tonight: (1) identity, repo, and wrapper-copy all inferred from cwd — #1293 opened as
mos-dt-0because the pre-fix wrapper copy ran from a next-based worktree; fred'sissue-commentfrom~/src/jarvis-brainaimed at the wrong repo and surfaced as an opaque HTTP 500; (2) principal resolution silently prefers whatever a host-level store holds — this workstation's tea store offering only a retired seat. Same family: an authority inferred from context instead of stated. Fix direction (separate card, not #1303): wrappers STATE their resolution — acting principal + repo — before writing, and refuse rather than infer when ambiguous.Operational residue, no code (operator lane — fences stand): the retired seat's tea login and the workstation-GLOBAL
mosaic.gitIdentity=mos-dt-0keep mis-attributing identity-unset calls until operator cleanup. And #1278's sole approving review is@mos-dt-0with branch protection counting it — governance call for fred/Jason, recorded here so it is not lost.Known-interaction evidence, second data point: global identity removal breaks identity-unset consumers (measured 2026-08-18, fred)
At fred's request Jason removed the workstation-GLOBAL
mosaic.gitIdentity = mos-dt-0from~/.gitconfig(the cleanup this PR's known-interaction section anticipated — correct behavior driven by wrong configuration). Measured consequence: eight consecutive failed brain-sync fetch cycles, cause confirmed by fred with a with/without-identity control on the same fetch. Stopgap: per-repofredidentity on~/src/jarvis-brain; sync healthy again ("sync ok" per cycle).This is the fix behaving as designed — with no global identity, identity-unset calls correctly stop falling through to a retired seat's credentials and fail loudly instead of silently mis-attributing. The failure mode is visible and diagnosable, which is the improvement over the silent direction.
Also measured during verification: worktrees inherit the main checkout's per-repo identity (all four active stack worktrees resolve
fredfrom~/src/stack's config), and until this PR merges + deploys, the pre-fixget_gitea_tokenin those worktrees reads that per-repo value — any identity-unset wrapper call from a worktree acts as the main checkout's identity. Post-merge,MOSAIC_GIT_IDENTITY(env) wins overgitIdentityper the resolver contract, verified on the branch's resolver directly.Durable fix for the host principal gap: mint a host-level service identity (Jason, tracked separately).
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.