At the 320px column minimum the full ISO timestamp ate most of line 1. Use an
HH:MM:SS (UTC) form in the embedded HistoryEntry/NetworkEntry headers only; the
full-size layouts keep the full ISO string.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In the pinned monitoring column (embedded), the single-line entry headers wrap
badly. Add compact two-line variants gated on `embedded`:
- HistoryEntry: line 1 = time + client/server badge + elapsed + status; line 2 =
method (and target, truncated) on the left with the controls on the right,
where Replay becomes an icon (new ReplayButton element) next to Pin/Expand.
- NetworkEntry: line 1 = time + HTTP method + category + elapsed + status; line 2
= the URL in a horizontal ScrollArea (new Text `nowrap` variant) with the
expand toggle on the right.
The full-size (non-embedded) layouts are unchanged. `embedded` is threaded from
the History/Network panels into the entries.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the Client/Server direction badge to the right of the timestamp so the
monospace timestamp is leftmost, matching the NetworkEntry header layout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Fix the resize handle disappearing: the animation wrapper is a Mantine Group
(default align: center), and the full-height handle has no intrinsic height, so
it collapsed to 0. Set align: stretch on the wrapper.
- Closing the column now leaves the primary on the screen currently selected in
the header (commits the active tab first), instead of swapping in the column's
screen.
- Keep the primary screen's standard ScreenStage transition; only the column
(handle + column) slides open/closed from the right with a fade.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add a search box to MonitoringControls (right of the tabs). It's a single
shared search kept across column tab switches, filtering whichever screen is
shown. Embedded panels apply this text but still ignore the sidebar-only
filters (levels/categories/directions/method), which have no control there.
- Close button now uses the "collapse sidebar" icon (TbLayoutSidebarRightCollapse),
the mirror of the pin button's expand icon.
- Closing the column carries its selection to the primary view: the screen the
user was watching becomes the active header tab (rather than snapping back to
the pre-pin selection).
- Animate the column open/close: the handle + column slide in from the right and
fade (reversing on close) via a Mantine Transition.
- HistoryListPanel: drop the redundant "History (N)" header when there are no
pinned requests (lone unpinned section renders headerless).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ResizeHandle: clear the `resizing-col` body class on unmount so a drag
interrupted by the handle unmounting (narrow past the breakpoint / disconnect)
can't leave page-wide user-select:none / col-resize applied.
- Embedded monitor column now shows the full unfiltered stream instead of
silently mirroring the full-size screen's live filter (the column has no
filter control to explain an empty/partial list).
- MonitoringControls: give the SegmentedControl an aria-label ("Monitoring
screen") so the tab group has an accessible name.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Render PinColumnButton as a subtle size-sm Button with a size-20 icon, the same
style as the panel's ListToggle, instead of a default ActionIcon.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On the History and Network panels, place the ListToggle (expand/collapse)
after Export and before the pin-as-column button. (Logs has no such toggle.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Place the "pin as column" icon after Clear/Export (rightmost) on the Logs,
History, and Network panel toolbars instead of at the front.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the ability to "pin" the Logging, History, and Network screens into a
second column on the right of the InspectorView, so a primary screen and a
monitor screen are visible at once (e.g. watch Logs while calling a Tool).
- A "pin as column" button on the Logs/History/Network panel toolbars opens
the column (shown only when wide + connected).
- The column hosts a MonitoringControls tab row (available monitor tabs +
close) over the selected embedded screen. Pinning removes the monitor group
from the header tab bar; closing (x) returns it.
- The divider between the primary area and the column is draggable (pointer +
keyboard); width is clamped and persisted.
- The split collapses to one column below ~1040px (the ServerListScreen
single-card breakpoint) and while disconnected, without discarding the pin
preference — it reopens when wide + connected again.
State (pinned/tab/width) is view-local and persisted via the existing
useLocalStorage pattern, so no new props thread through App and ViewHeader is
unchanged (it just receives a filtered tab list). Screens/panels gain an
`embedded` mode (fills the parent height, drops the 340px filter sidebar) via a
shared EmbeddableScrollArea element.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`tsc -b` (run in CI's validate step, but not by vitest locally) rejected
`ok.mock.calls[0][1].body` — under the project's strict indexing the
untyped mock's call tuple is `[]`, so `[0]` is possibly-undefined and
`[1]` is out of range. Capture the request body inside a `vi.fn<typeof
fetch>` implementation instead of indexing into `mock.calls`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the Node-free guard beyond oauth-persist.ts to every module
reachable from the browser's RemoteOAuthStorage (storage-remote,
oauth-storage, store, oauth-persist, store-serialize), so a forbidden
store-io/node:/atomically import added one hop away is caught too — not
just a direct import in oauth-persist.ts. Addresses the non-blocking
hardening note on Claude review of #1612.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The refactor routed the browser's RemoteOAuthStorage through
`oauth-persist.ts`, which value-imported the Node-only `store-io.js`
(`atomically` -> `stubborn-fs` -> `node:process.getuid`, plus
`node:fs/promises`). That dragged Node built-ins into the browser bundle
and the web app crashed to a blank page at runtime — a regression from
v2/main, where browser OAuth storage used a Zustand JSON adapter with no
Node imports. Unit/integration tests run in node/happy-dom and
`smoke:web` only checks the served HTML, so none of them execute the app
in a real browser and the break slipped through.
Fix, following the existing `store-id.ts` precedent:
- Split the pure JSON (de)serializers into Node-free
`core/storage/store-serialize.ts`; `store-io.ts` re-exports them so
existing importers are unaffected.
- Move the Node-only `createFileOAuthPersistBackend` into
`core/auth/node/oauth-persist-file.ts`; update `storage-node.ts` and
the storage adapters test to import from there.
- `oauth-persist.ts` now imports only the Node-free serializers, so the
browser bundle no longer pulls in `node:fs`/`atomically`.
Tests:
- oauth-persist-browser-safe.test.ts: source-level guard that
`oauth-persist.ts` never imports store-io/node:/atomically.
- Direct unit tests for the isomorphic remote + session backends
(restoring oauth-persist.ts coverage after the file backend moved out).
- store.test.ts: OAuthMemoryStore unit coverage incl. replace() fallbacks.
- storage.test.ts: clearAllOAuthClientState null-snapshot (no file) path.
Verified end-to-end in a real browser (Playwright): DCR OAuth flow
completes, tokens persist via the remote store, and after a full page
reload the server reconnects with no re-authentication.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document the contract the remote read() path leans on — the server's
missing-file response is `c.json({}, 200)`, and the backend now passes
that empty object straight to parseOAuthPersistBlob. Addresses the
optional note on Claude review of #1612.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review follow-ups on the RemoteOAuthStorage web migration:
- useEmaIdpLoginState.logout(): add a .catch to the now-async
clearEmaIdpSession call so a storage failure no longer surfaces as an
unhandled rejection; leave loginState unchanged on failure (the session
was not actually cleared). Add a test asserting no unhandled rejection.
- createOAuthStore: attach a no-op catch to the eager persistLoadPromise
so a hydration read that fails before any load() awaits it does not
become an unhandled rejection; real awaiters still observe the rejection
via the shared reference.
- storage-remote.test: replace `as unknown as typeof fetch` double-casts
with the typed vi.fn<typeof fetch>() pattern the same PR established.
- remoteOAuthStorage: correct the doc comment to describe the cache-of-one
accurately instead of implying a keyed map.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Serialize persist() in OAuthStorageBase so overlapping remote POSTs cannot
reorder and clobber state. Regenerate launcher package-lock.json to drop
stale zustand entries. Let parseOAuthPersistBlob accept parsed objects
from res.json() without a redundant JSON round-trip.
- Add a dedicated `inspectorTabs.test.ts` (was relying only on incidental
coverage from App.test.tsx) — covers the tab-id constants and both branches
of the `isInspectorTabId` type guard; 100% per-file.
- AGENTS.md: extend the `core/auth/` structure-tree entry to describe the new
mid-session recovery modules (challenge parsing, scope union, shared OAuth UX
copy, force-reauthorization) and the node loopback callback flow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DZyeWManYTvcXxjcrhxSQc
- App.tsx: replace the inline `style={{}}` on the re-auth banner wrapper with
Mantine style props (pos/top/bg) plus a `.reauth-banner-bar` class for the
z-index/box-shadow that have no Mantine shorthand.
- App.tsx: extract the ~100-line inline `onAuthorize` and the inline `onCancel`
step-up handlers into named `handleStepUpAuthorize`/`handleStepUpCancel`.
- ReAuthBanner: move the flat `styles={{ root }}` (background/border) into a
`reauth` Alert theme variant; replace the raw `<span>` with `<Text>`.
- StepUpAuthModal: extract the inline `onClick` wrapper into `handleAuthorize`.
No behavior change; validate + integration green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DZyeWManYTvcXxjcrhxSQc
The mid-session auth work passed `validate`/CI but failed the local-only
`npm run test:coverage` per-file gate (≥90% lines/statements/functions/
branches) on 12 files. Add targeted unit/integration tests so every changed
file clears the gate; annotate provably-dead defensive branches with justified
`/* v8 ignore */` comments rather than relaxing the gate.
Files brought to ≥90 on all four dimensions:
- core/auth: mcpAuth, oauthUx, utils, challenge, node/runner-interactive-oauth
- core/mcp: oauthManager, inspectorClient, node/authChallengeFetch,
remote/remoteClientTransport, remote/node/{remote-session,server}
- clients/web/src/utils: oauthResume, browserTabVisibility
`npm run test:coverage` now passes (3575 tests, 0 threshold failures);
`npm run validate` green across all four clients.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DZyeWManYTvcXxjcrhxSQc
Clear the loopback callback timeout in runner-interactive-oauth finally
so already_authorized and error paths do not hang the CLI/TUI. Check EMA
step-up satisfaction against minted token scope before persisting the
requested union, matching the standard OAuth path.
Co-authored-by: Cursor <cursoragent@cursor.com>
Address round-1 review feedback on fetchTracking redaction:
- Cross-reference REDACTED_VALUE / REDACTED_HEADER_VALUE so a reader
understands why two identical "[REDACTED]" constants coexist.
- Document that redactBody deliberately scopes to form-encoded + JSON
(multipart/binary bodies pass through; OAuth never uses multipart).
- Add a tracker-path test asserting a nested sensitive key in a JSON
request body is redacted in the recorded copy while the live outbound
body stays byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
Broaden the web vitest `coverage.include` from the single-file
`src/lib/sandbox-csp.{ts,tsx}` entry to `src/lib/**/*.{ts,tsx}` so the
per-file >=90 four-dimension gate applies to every library module under
`src/lib`, not just sandbox-csp.
- Add unit tests for the previously-ungated `environmentFactory.ts`
(createWebEnvironment) and `types/customHeaders.ts` value helpers,
bringing both to 100% on all four dimensions.
- Annotate downloadFile.ts's provably-dead `?? ""` fallback (String.split
always returns a non-empty array so .pop() is never undefined) with a
justified `v8 ignore`, closing its last uncovered branch (92.3 -> 100).
All five src/lib files now report 100/100/100/100.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
Extends the header redaction from #1585. Before a fetch entry reaches any
sink, mask known-sensitive credentials in the request/response bodies and
the URL query string, in addition to headers:
- redactBody(): form-urlencoded + JSON bodies, recursive over nested
objects/arrays, best-effort (never throws), values-only.
- redactUrlQuery(): masks sensitive query params, preserves path,
non-sensitive params, and any fragment.
- Sensitive fields: client_secret, code, refresh_token, access_token,
id_token, code_verifier, client_assertion, assertion, password, token
(case-insensitive), reusing the [REDACTED] sentinel.
Applied to the request success + error entries and the async response-body
callback. Live outbound requests are byte-identical — only the logged copy
is redacted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
The "cancels a pending debounce timer when the input is re-focused" test
passed trivially after the delay:null change: synchronous typing meant the
assertion ran at t≈0, before the 300ms debounce could ever fire, so it
passed whether or not handleFocus's clearTimeout worked.
Rewrote it with fake timers so the debounce window elapses deterministically
(advance the clock by 400ms directly). The interaction is driven with
fireEvent rather than userEvent because userEvent's async internals deadlock
under vitest fake timers with the Mantine/happy-dom stack. Verified
load-bearing: removing the clearTimeout in handleFocus makes it fail
(expected 1 to be 0), and it passes once restored.
Also tightened the sibling-context comment (with delay:null the sibling is
reliably "es"); the tolerant /^es?$/ regex is left as-is.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
Root-cause and deterministically fix the recurring 5s-timeout flakes that
surfaced under v8-instrumented, concurrent load. All fixes are test-only; no
product source changed. No test was skipped or disabled, and the per-file
coverage gate (>=90 on all four dims) still holds.
Common root cause across the modal/form/screen suites: userEvent.setup()
schedules a real setTimeout between keystrokes. Under CPU contention those
yields balloon, so multi-field interactions blew past the 5s per-test
timeout. Fix: userEvent.setup({ delay: null }) removes the per-keystroke
real-timer dependence (typing dispatches synchronously), making the suites
load-independent.
Per file:
- ServerConfigModal / ServerImportConfigModal / ServerImportJsonModal /
ResourcesScreen / InspectorView: userEvent.setup({ delay: null }).
- PromptArgumentsForm (completions): replaced wall-clock sleeps sized just
past the 300ms completion debounce (await setTimeout(400)) — which race
the debounce timer under load — with awaited conditions (findBy/waitFor)
on the real rendered outcome, plus delay:null. Negative assertions now
lean on the component's synchronous state/timer teardown instead of a
timed window, so they are deterministic regardless of machine speed.
- inspectorClient integration "tracks stderr logs": the child's stderr is
piped out-of-band from the tool's JSON-RPC response, so reading the log
synchronously after callTool raced the stderr chunk. Wrapped the assertion
in vi.waitFor so it polls until the line lands.
Verified: web validate + 6x test:coverage (4 sequential + 2 concurrent
under load) + standalone test:integration, all green (229 files / 3237
tests per coverage run; 799 integration tests), zero intermittent failures.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
Round-2 @claude review of #1592 caught a seventh member of the clear
family that the first pass missed: clearClientInformation still wrote to
the store synchronously, carrying the same pre-hydration clobber risk the
clearAfterHydration helper was written for (a clear before hydration lands
persists a near-empty blob, overwriting the whole on-disk oauth.json).
Wrap its mutation the same way and add a clear-before-hydration test
proving a sibling token (and thus every other server's blob) survives.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
Address round-1 @claude review of #1592:
- OAuthStorageBase.clear* no longer mutate the pre-hydration (empty) store —
a clear issued before hydration is deferred until it lands, so it merges
onto the real persisted state instead of (a) being resurrected by the late
rehydrate merge or (b) persisting a near-empty blob that clobbers every
other server's on-disk credential. Already-hydrated clears stay synchronous.
- store.setServerState migrates a pre-normalization raw-key blob onto the
canonical key on first partial write (was: shadowed it with a fresh
canonical entry, orphaning the raw blob's other fields). clearServerState
now also drops the raw-key orphan.
- Document the keepalive 64KB combined-body ceiling on the remote-storage
write, the getRemoteOAuthStorage memo key intentionally omitting fetchFn,
and the real ordering invariant behind the sync getScope.
- Tests for the clear-before-hydration and raw-key-migration paths.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
Address round-1 @claude review of #1588 (no behavior change to the
enforced policy):
- approveCspSources / buildSandboxCspPolicy: document that
resourceDomains intentionally feeds script-src/style-src per the
McpUiResourceCsp contract, and that approve screens injection-safety
only (not breadth — a bare `*` is accepted; safe under the
opaque-origin sandbox).
- CSP_KEYS: replace the `satisfies` (proves listed keys valid) with an
exhaustiveCspKeys() helper that ALSO fails to compile if the upstream
ext-apps type gains a domain key CSP_KEYS omits, so a requested
restriction can never be silently dropped.
- SAFE_CSP_SOURCE: accept case-insensitive schemes (URL schemes are
case-insensitive); previously over-rejected `HTTPS://…`. Adds tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
- Soften the OAuth-callback-rejected toast: the shape guard is
defense-in-depth, not full CSRF prevention (PKCE remains primary), so the
message no longer overstates ("rejecting to prevent a cross-site request" ->
"did not originate from this session").
- Document the intentional asymmetry between a present-but-malformed `state`
(rejected) and a wholly absent `state` (accepted, matched via
OAUTH_PENDING_SERVER_KEY) — rejecting the null case would mask real provider
error redirects that omit `state`.
- Make the "valid state is not rejected" test assert the specific downstream
"could not be matched" toast (seeding OAUTH_PENDING_SERVER_KEY) instead of an
indirect "some toast fired" check.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX