fix(#4717): consult the per-install runtime marker at both identity seams (#4861)

* test(#4717): add failing-first coverage for the two runtime-identity marker seams

* fix(#4717): consult the per-install runtime marker at both identity seams

resolveReportedRuntime (agent_runtime) and loadConfigResolved
(config.runtime) both ignored the per-install .gsd-runtime marker that
resolveRuntime and the model-resolver gate already read. On a
multi-runtime machine (e.g. a globally exported CODEX_HOME), host sniffing
misreported every Claude Code session as codex, and a shared
defaults.json stamped by the first non-Claude install leaked its runtime
to every other one.

Seam 1: the reported-runtime ladder becomes explicit > install marker >
host detection > claude. Seam 2: loadConfigResolved fills an empty
config.runtime from GSD_RUNTIME then the marker, copy-on-write (the
builtin-defaults branch returns a shared object). Explicit runtimes and
marker-less trees are unchanged.

* fix(#4717): a marker-detected runtime opts into its tier map (decision a)

* fix(#4717): stamped-defaults leg, marker fail-safe, docs, review fold-ins

* chore(#4717): backfill changeset PR number (4861)

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-09-18 13:50:01 -04:00
committed by GitHub
parent 58c7bbb16a
commit 9a41a95212
10 changed files with 343 additions and 33 deletions

View File

@@ -603,13 +603,19 @@ function resolveModelInternal(cwd: string, agentType: string): string {
// #2517 `runtime:"codex"` + resolve_model_ids:"omit" -> the codex tier
// model. "Explicit non-Claude opt-in wins" — the operator naming a
// runtime in the project config outranks an omit.
// #2297 GSD_RUNTIME/marker = codex + a GLOBAL (defaults-poisoned) omit
// -> "" (acceptance #4). A merely DETECTED runtime does not
// constitute that opt-in, so the omit still wins.
// #4717 (user-sanctioned decision a — supersedes #2297 acceptance #4):
// a DETECTED runtime now constitutes that opt-in too. The identity
// fill in config-loader materializes GSD_RUNTIME / the per-install
// marker into `config.runtime` when it is empty, so a genuinely
// installed non-Claude runtime resolves its own tier map instead of
// the omit's "". The shared "omit" remains a Claude protection:
// marker/env = claude still ignores it (native aliases), and
// garbage runtime values still fail safe to "" (guard below).
//
// So the opt-in signal is specifically the `runtime` KEY, not the resolved
// runtime: step 3 reads the active runtime's tier map, but only outranks the
// omit gate when the operator wrote that key. Both contracts hold unchanged.
// So the opt-in signal is the `runtime` KEY — written by the operator or
// materialized by the #4717 fill: step 3 reads the active runtime's tier
// map, but only outranks the omit gate when that key canonicalizes to a
// recognised non-Claude runtime.
const omitApplies = config['resolve_model_ids'] === 'omit'
&& (projectExplicitlySetsOmit(cwd) || !RUNTIMES_WITH_NATIVE_ALIASES.has(activeRuntime));
// CANONICALIZED, not the raw field. Comparing the raw value against the literal