No new inline comments this round, but two of the four suppressed ones were
real defects rather than restatements — both in code I added, both silently
wrong in the safe-looking direction.
**A symlinked secrets directory read as unmounted.** `isOnMountPoint` walks
to the nearest existing ancestor *lexically* and then string-prefixes it
against mountinfo, so `~/.mcp-inspector -> /data/inspector` with `/data`
mounted matched no mount point. The container then selected the session-only
store on a box that had explicitly arranged for persistence, and nothing said
so — the mount was there, the volume was there, the secrets evaporated on
exit. Resolved through symlinks now, tolerantly: a path that cannot be
resolved falls back to the lexical comparison, which is what this did before
and is correct whenever no symlink is involved. The `statSync` fallback branch
already followed links, so this also makes the two paths agree.
**The bulk read did not fix the case it was written for.** `getMany` batched
*one server's* fields, but both rehydration callers iterate servers — so a
catalog of 20 servers holding one secret each still paid 20 serialized scrypt
derivations. The same ~450ms stall the seam was introduced to remove, reached
one server at a time instead of one field at a time. Measuring the wrong unit
and then optimizing it is worse than not optimizing, because the number in the
commit message says it is fixed.
The seam is cross-server now — `getMany(requests: SecretBulkRequest[])` — and
both callers pass the whole catalog in one call. `FileSecretStore` decrypts
once for the entire rehydration; the keychain fallback still issues
independent parallel `get`s, which is what it wants. A test asserts one
derivation for four servers, next to the existing one asserting one for three
fields, so the property is pinned at both axes.
The other two suppressed comments are the cross-process guarantee — `mutate`'s
residual and the hand-off's TOCTOU delete — which remain decided against for
this PR and tracked in #2082.
Full `npm run ci` green.
Signed-off-by: cliffhall <cliff@futurescale.com>
The lock finding from this round is deliberately not addressed here — see
below.
- **My round-6 strict-read fix was inert in production.** `DeferredSecretStore`
is what `defaultSecretStore()` returns, and it did not forward `getStrict`
— so `secretStoreGetStrict` fell through to the tolerant `get` for every
real caller, and the strictness existed only in tests that injected a
concrete store. Forwarded now, along with `getMany`.
- **The same tolerant-read bug remained in both pre-existing migrations.**
I added `getStrict` for the new hand-off in round 6 and never applied it to
the mcp.json and client.json migrations, which have the identical
shape: decide from `get`, then write on `null`. A transient keychain read
failure therefore let an older plaintext value overwrite a newer keychain
one — and the disk copy was then stripped, so the newer value was gone. Both
use the strict read now; the throw is a `SecretStoreUnavailableError`, which
both call sites already handle as "abandon the migration, keep the file".
- **The permission check made a false statement on Windows.** `stat` returns
synthetic bits there, and interpreting them yields a confident falsehood
either way — "0666 — not owner-only" about a file the ACL may restrict, or
"owner-only" about one it does not. The ACL is the real answer and this does
not inspect it, so Windows now reports `unknown`.
- **Rehydration cost one full decryption per field.** `get` reads and decrypts
the entire file, and both rehydration paths ask field by field, serialized
behind the store's own queue. Measured on this machine, scrypt at N=16384 is
~23ms per derivation, so a 20-secret encrypted catalog spent **~450ms of
pure key derivation on every `GET /api/servers`** — a visible stall, not a
micro-optimization. Added an optional `getMany` seam: `FileSecretStore`
decrypts once per server, everything else falls back to the parallel
per-field `get` that already suited it. A test counts derivations directly:
1 for a three-field `getMany`, 3 for the three `get`s it replaced.
Also broadens a log message that still said "Keychain unavailable" for a
store that may not be a keychain.
Full `npm run ci` green — 5,762 tests under the coverage gate.
Signed-off-by: cliffhall <cliff@futurescale.com>
* refactor(web): update OAuth E2E tests to use new authentication method
- Replaced `runGuidedAuth` with `authenticate` in the InspectorClient OAuth end-to-end tests to support quick authentication with CIMD pre-registration.
- Updated comments to reflect the change in authentication method and its support for HTTP test metadata URLs.
- Ensured that the tests continue to validate the expected authorization URL after the change.
Additionally, imported `ensureCimdClientRegistration` in the OAuthManager to streamline client registration during the authentication process.
* Guided-auth removal, OAuth smoke testing, and TUI/CLI support
* Added CLI OAuth runner flags tests
* Added clarifying comment on port 0 binding
* README notes on TUI oauth callback port, prettier fixes for CLI tests
* CIMD validation uses inline like EMA IdP
* Fixed TUI test failures from merge
* test: bring 5 OAuth files over the 90 per-file coverage gate
The guided-auth-removal work added/touched several files that fell below
the repo's >=90 per-file coverage gate (npm run coverage). CI doesn't run
the gate (it's local-only), so validate + test:integration passed while
these gaps slipped through. Bring them all back over the line:
- core/auth/node/runner-oauth-callback.ts: test bad-URL/non-http-scheme
throws, no-port->80 default, and IPv6 hostname bracketing. Annotate the
provably-dead guards (empty host, out-of-range port, String(err)
fallback, pathname "/" fallback) -- all rejected by new URL() upstream --
with justified v8 ignore comments instead of lowering the gate.
- core/auth/cimd.ts: test the AS-doesn't-support-CIMD early return.
- core/react/useClientSettingsDraft.ts: test the functional-updater path,
the stale-onChange-after-close prev===null guard, and the
timer-fires-after-close value===null guard.
- ClientSettingsForm.tsx: test the CIMD checkbox toggle and CIMD URL clear.
- clients/tui/src/utils/oauthDisplay.ts: test static/dcr registration
kinds, the expired IdP session, and the configured-scope/empty-scope
branches of formatScopes.
npm run coverage now passes (exit 0) across web, cli, tui, launcher.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: cliffhall <cliff@futurescale.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>