1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
//! Account-selected clone credentials for the daemon's `inproject` base clone (#7166).
//!
//! Why: split out of `inproject.rs` (issue #7166 review follow-up) to keep that
//! file under the 500-SLOC production cap — the token-resolution,
//! identity-verification, and shadow-then-set env logic this module holds is a
//! self-contained concern with its own doc, its own tests, and only one call
//! site (`ensure_base_clone`) in the parent.
//! What: [`account_clone_env`] is the `pub(super)` entry point `ensure_base_clone`
//! calls; everything else here is private to this module (or `pub(super)` only
//! where the sibling `tests` module — a descendant of `inproject`, and so still
//! able to see anything `pub(super)` here exposes — needs direct access for
//! pure-value assertions).
//! Test: `inproject/tests.rs` — see each item's own doc for the exact test names.
/// The env overrides an account-selected clone applies to the `git` child (#7166).
///
/// Why: split out of [`ensure_base_clone`] so the shadow-then-set shape can
/// be asserted directly against a plain `std::process::Command`, without
/// spawning a real `git clone` — mirrors how [`crate::core::gh_identity::
/// GhEnv::apply_to`] is unit-tested at the pure-value layer.
/// What: `remove` — every [`crate::core::gh_identity::
/// GH_INHERITED_IDENTITY_ENV`] entry, so an exported `GH_TOKEN`/`GITHUB_TOKEN`
/// (or a `GH_CONFIG_DIR` naming a different identity) cannot outrank the
/// selection; `set` — whatever [`crate::core::gh_account::GhSpawnEnv::vars`]
/// resolved (`GH_CONFIG_DIR`[+`GH_USER`], or `GH_TOKEN`+`GH_USER`).
pub
/// Resolve the [`AccountCloneEnv`] for `account` (#7166, tightened after a
/// `code-critic` BLOCK on the initial version).
///
/// Why: the one place this module decides HOW an explicit account selection
/// becomes credentials for a raw `git clone` subprocess — as opposed to a
/// `gh` subcommand, which [`trusty_common::gh::GhCommand`] already handles.
/// `git clone` never goes through `gh`, so the token has to land where
/// `git`'s own already-configured credential resolution (this workspace's
/// convention: `credential.helper = !gh auth git-credential`) will find
/// it — the environment, never argv or the URL.
///
/// 🔴 The first version of this function called
/// [`crate::core::gh_account::gh_token_via_cli`] (`gh auth token -u
/// <account>`) directly and unconditionally. That selector is documented, in
/// this same crate, as NOT discriminating between logged-in accounts on a
/// keyring-backed `gh` install (macOS's default, and the exact `#7166`
/// scenario) — `-u bobmatnyc` and `-u bob-duetto` can return the identical
/// value, so the clone would silently authenticate as whichever account
/// happened to be globally active, not the one requested. Two changes close
/// that:
///
/// 1. A per-account `gh` config dir is ensured FIRST (#7166 owner ruling
/// "do B") via [`super::account_config_dir::ensure_account_config_dir_default`]
/// — built once from the operator's own logged-in `hosts.yml` on first
/// use of `account`, reused untouched afterward. Token resolution then
/// runs through [`crate::core::gh_account::resolve_gh_account_env_with`]
/// WITH that dir as `config_dir`, which always takes the config_dir arm
/// (`GH_CONFIG_DIR=<dir>`, no bare token) — the SAME selector that
/// genuinely discriminates between logged-in accounts on a keyring-backed
/// host, never the demoted `gh auth token -u` fallback. `gh auth switch`
/// is never called anywhere in this path: the isolation comes from a
/// SEPARATE config dir, not from mutating the shared one's active
/// pointer.
/// 2. The resolved identity is still VERIFIED before it is ever handed to
/// `git` — scoped to the SAME `GH_CONFIG_DIR`, via
/// [`crate::core::gh_account::api_login_scoped`] (reused rather than a
/// third implementation of "ask GitHub who a scoped credential belongs
/// to" — `gh_account_enforce` already has one for its own config_dir
/// verification). A mismatch (e.g. a stale, hand-edited account dir) is a
/// loud, named refusal.
///
/// What: `Err` on an unresolvable/failed identity, an unbuildable account
/// dir (already names the account and the `gh auth login` remedy — see
/// [`super::account_config_dir::ensure_account_config_dir`]'s doc), or a
/// login mismatch (names BOTH the requested and the actual login). `Ok`
/// returns the shadow-then-set pair described on [`AccountCloneEnv`].
/// Test: `account_clone_env_shadows_inherited_identity_and_sets_gh_token`,
/// `account_clone_env_propagates_a_resolver_failure_naming_the_account`,
/// `account_clone_env_refuses_a_token_minted_for_a_different_account`,
/// `account_clone_env_propagates_a_verification_failure`,
/// `account_clone_env_accepts_a_token_that_matches`,
/// `account_clone_env_uses_a_config_dir_and_verifies_it_scoped`,
/// `account_clone_env_refuses_a_config_dir_scoped_to_a_different_login`.
/// The account-dir bootstrap refusal itself (an unknown login, dir never
/// created) is covered at its own layer by
/// `super::account_config_dir::tests::ensure_account_config_dir_refuses_an_unknown_login`
/// — [`ensure_account_config_dir_default`](super::account_config_dir::ensure_account_config_dir_default)
/// reads the real `$HOME`, so this function's OWN propagation of that `Err`
/// (a plain `?`, right below) has no separate hermetic test of its own.
pub
/// [`account_clone_env`] with injectable identity-resolution and
/// -verification steps.
///
/// Why: this codebase's established seam for `gh` I/O that cannot run
/// hermetically in CI (see `gh_account::resolve_gh_account_env_with`, which
/// `resolve_token` mirrors) — lets the shadow-then-set shape, the
/// not-logged-in refusal, AND both verification mismatch refusals be
/// asserted with no real `gh` process, no PATH mutation, and no network.
/// `config_dir` stays a parameter (rather than always-`Some`, which is what
/// [`account_clone_env`] always passes today) so the pre-#7166-owner-ruling
/// `-u`-fallback shape stays directly testable too — a caller with no
/// buildable account dir would otherwise have no arm to fall back to.
/// What: delegates identity SELECTION to
/// [`crate::core::gh_account::resolve_gh_account_env_with`]; when the
/// resolved [`crate::core::gh_account::GhSpawnEnv::vars`] carries a
/// `GH_CONFIG_DIR` (the arm [`account_clone_env`] always takes now),
/// `verify_scoped_login` is called with `(dir, host)`; when it carries a bare
/// `GH_TOKEN` instead (the `-u`-fallback arm, reachable only when a caller
/// passes `config_dir: None`), `verify_login` is called with the token. Either
/// result is compared, case-insensitively (GitHub logins are
/// case-insensitive; matches
/// [`crate::core::gh_account::GhAccountStatus::canonical_logged_in_login`]'s
/// convention), against `account`. A `resolve_gh_account_env_with` `warning`
/// is logged, not swallowed.
/// Test: see [`account_clone_env`]'s `Test:` list — every case fires
/// through this function.
/// The full removal set for an account-selected clone's child env (#7166
/// review follow-up).
///
/// Why: [`crate::core::gh_identity::GH_INHERITED_IDENTITY_ENV`] deliberately
/// does NOT include `GH_HOST` — two existing consumers
/// ([`crate::core::gh_identity::resolve_gh_env`]'s `inherited_identity_to_clear`
/// and, transitively, `runtime::claude_code_gh_env`) have an existing test
/// (`binding_removes_the_inherited_identity_vars`) pinning an EXACT unset
/// list, so widening the shared const would be a silent behavior change for
/// both of them, not a scoped fix for this one. This clone path is
/// different: the clone URL's host is hardcoded to `github.com`
/// (`register_args::GITHUB_HOST`) — never read from config — so an ambient
/// `GH_HOST` here can only ever be a stale mismatch (e.g. a GHE host left
/// over from a previous invocation) that would misdirect `gh auth token -u`
/// / `gh api user` while `git clone` still targets `github.com`. Removing it
/// LOCALLY, for this one child env only, closes that without touching the
/// shared const or its consumers' behavior.
/// What: [`crate::core::gh_identity::GH_INHERITED_IDENTITY_ENV`] plus
/// `"GH_HOST"`.
/// Test: `account_clone_env_shadows_inherited_identity_and_sets_gh_token`.
/// Confirm a minted token actually belongs to the account it claims to,
/// before it is ever used to authenticate a clone (#7166 critic BLOCK).
///
/// Why: `gh auth token -u <account>` can return a DIFFERENT account's token
/// on a keyring-backed host (`gh_account.rs`'s own documented caveat) — this
/// is the check that turns "silently wrong identity" into a loud, named
/// refusal, by asking GitHub itself who the token belongs to.
/// What: runs `gh api user --jq .login`, bounded by
/// [`crate::core::gh_account::GH_ENFORCE_TIMEOUT`] (see
/// [`verify_token_login_with`]), with EVERY inherited identity var removed
/// and `token` set as `GH_TOKEN` — so the answer reflects `token` alone,
/// never an ambient credential — and returns the trimmed login on a
/// non-empty, zero-exit response.
/// Test: the `gh`-invoking closure itself (a real, deliberate network call)
/// has no pure branch left to unit test hermetically; the timeout wrapper it
/// delegates to is covered directly — see [`verify_token_login_with`].
/// [`verify_token_login`] with an explicit, injectable timeout and runner
/// (#7166 review follow-up HIGH).
///
/// Why: the FIRST version of this function called
/// `GhCommand::nonempty_stdout_blocking()` with no bound at all — a network
/// stall (or a hung `gh`) blocked the whole clone indefinitely, unlike every
/// other `gh`-subprocess call this crate makes for account selection
/// ([`crate::core::gh_account::gh_token_via_cli`], `gh_account_enforce`'s
/// `api_login_scoped`), which all run under
/// [`crate::core::gh_account::GH_ENFORCE_TIMEOUT`] via
/// [`crate::core::gh_account::run_bounded`]. Splitting the timeout wrapper
/// from the `gh`-invoking closure — the SAME seam
/// [`crate::core::gh_account::probe_gh_auth_with`] uses for its own
/// hermetically-tested timeout arm — lets the timeout branch be asserted
/// with a `std::thread::sleep`, no live `gh`, no network, no PATH mutation.
/// What: runs `run` on `run_bounded`'s detached thread; a stall past
/// `timeout` returns `Err` naming the timeout explicitly (distinct from a
/// `run` failure, which propagates verbatim) — [`account_clone_env_with`]
/// wraps whichever `Err` reaches it with the account name, so either shape
/// still names the account by the time it surfaces.
/// Test: `verify_token_login_with_times_out_and_names_the_bound`,
/// `verify_token_login_with_returns_the_inner_result_when_fast`.