Skip to content
5 changes: 5 additions & 0 deletions packages/utils/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
- The independent-evidence rule for the trusted account home (#4766) is now pinned by discriminating regression proof for the shape its macOS repro cannot reach: a Linux identity whose uid has no local `/etc/passwd` entry (NSS/LDAP/SSSD-backed accounts, minimal or distroless containers), reported as #4773. New subprocess tests run the resolver under an unprivileged user namespace whose mapped uid is verified absent from the local passwd database (chosen from common subordinate-style candidates or the invoking user's `/etc/subuid` range), so the runtime `userInfo().homedir` echo of a checkout-declared home variable cannot silently regress back into the trusted set; they fail on the pre-rule behavior, hosts without the capability skip with a loud warning naming the lost coverage, and the compat shapes (passwd-backed uid, absent platform home variable, unambiguous operator home, dynamic declaration) are asserted unchanged on every platform, using the platform-authoritative key (`USERPROFILE` on Windows). `agent-dir-trust.test.ts`'s account-home expectation now reads the passwd database directly instead of a parent-side `os.userInfo().homedir`, which follows an isolated `HOME` and made the assertion fail under a pristine `HOME` (#4773).

### Fixed
- NSS account-home cache entries are now scoped by effective uid and account identity, so setuid or container identity transitions cannot reuse another user's trusted home or state; failed lookups remain fail-closed and call-time HOME/XDG refresh is unchanged.
- The authoritative home for user-scope state is resolved at call time again, instead of being snapshotted when `dirs.ts` loads. The provenance hardening had anchored `getTrustedHomeDir()` (and everything derived from it: config root, default agent dir, plugins dir) to an import-time value, so any home established or changed after module load silently lost every user-scope location -- user-scope skills under `~/.gjc/agent/skills` and user-scope MCP servers under `~/.gjc/agent/mcp.json` stopped being discovered. The provenance rule is unchanged: a home the project dotenv could have planted is still rejected in favor of the OS account database, and a bare filesystem root is still refused (#4761).
- An ambiguous home is no longer refused when the account database corroborates it. Independence was tested by string inequality (`accountHome !== runtimeHome`), so an operator whose `HOME` legitimately matches their account entry was locked out of their own user state as soon as any checkout declared `HOME` dynamically -- the account lookup was treated as an echo precisely when it agreed. Independence is now a property of the *source*: an NSS answer is environment-independent evidence whichever path it names, while an `os.userInfo()` fallback (which Bun derives from `$HOME`) is never evidence. A planted home is still rejected whenever the account database contradicts it, and still fails closed when no environment-independent lookup is available (#4761).
- The runtime home is validated before it can be trusted. `resolveTrustedHome()` accepted whatever `os.homedir()` returned, so a relative home (Bun returns `HOME` verbatim) anchored the config root, agent dir and plugins dir beneath the current working directory. It is now held to the same absolute, non-root standard as the account home, and an unusable value falls through to the account lookup instead of being honored (#4761).
- The Linux account home is read through NSS (`getent passwd <uid>`) instead of parsing `/etc/passwd` directly. LDAP- and SSSD-backed accounts have no local passwd entry, so the file read missed them entirely and fell through to `os.userInfo().homedir`, which Bun derives from `$HOME` -- exactly the untrusted value the account lookup exists to avoid. `getent` is the NSS front end, so local and directory-backed accounts both resolve; the probe runs with a fixed `PATH`/`LC_ALL` and no inherited environment. macOS and Windows keep the portable `os.userInfo()` path (#4761).
- A project-declared `HOME` could again select the trusted home on macOS, so credentials were read from a checkout-controlled home directory. When the project dotenv declares the platform-authoritative home variable, the resolver falls back to `accountHomeFromSystem()`; that helper reads `os.userInfo().homedir`, which Bun resolves from `$HOME` on macOS (unlike Node, which reads the passwd database). The rejected value therefore came back as its own justification and `~/.env` under the hostile home was parsed for credentials. The account home is now accepted as independent evidence only when it differs from the runtime home -- true for the Linux `/etc/passwd` lookup, false for the macOS `$HOME` echo -- and an ambiguous home with no independent evidence resolves to the filesystem root sentinel, which marks user state unavailable and keeps credential resolution fail-closed.
- Windows environment names are case-insensitive, but every project-dotenv provenance lookup was keyed on the exact case parsed from the file. A `.env` line `userprofile=...`, `gjc_coding_agent_dir=...`, or a lowercase provider key was therefore invisible to the trusted-home, agent-directory and credential guards while `process.env`/`Bun.env` still resolved it -- the declaration was live but unguarded. Project snapshot keys and their lookups are now folded through `canonicalEnvKey()`, which upper-cases on Windows only; POSIX names stay case-sensitive, pinned by a test that fails if the fold is applied unconditionally. The `win32` branch itself is proven by `packages/utils/test/env-provenance.windows.test.ts`, which runs on the required windows-latest lane: a lowercase `userprofile`, a mixed-case `Gjc_Coding_Agent_Dir`, and a lowercase provider key declared by the project are all rejected, while a genuinely inherited uppercase credential still resolves.
- `postmortem.test.ts` no longer writes its crash fixtures into the developer's real crash store. Every scenario there deliberately crashes a subprocess, and the fatal handler resolved `getCrashLogPath()` with no override, so each run injected a dozen `fixture: ...` signatures into `~/.gjc/agent/gjc-crash.log` -- visible in `gjc crash list`, offered up by `gjc crash report`, eligible for the opt-in upstream relay, and competing for the log's fixed byte cap against a genuine crash the developer might need to file. The spawned fixtures now run under a temp `GJC_CODING_AGENT_DIR`, and the tests assert the fixture store received the records and the real store was untouched.
Expand Down
Loading
Loading