Skip to content

CmdPal JS/TS Extensions - Phase 2: JSON-RPC transport + manifest - #49323

Open
michaeljolley wants to merge 12 commits into
dev/mjolley/phase-1-cmdpal-ts-sdkfrom
dev/mjolley/phase-2-cmdpal-jsonrpc-transport
Open

CmdPal JS/TS Extensions - Phase 2: JSON-RPC transport + manifest#49323
michaeljolley wants to merge 12 commits into
dev/mjolley/phase-1-cmdpal-ts-sdkfrom
dev/mjolley/phase-2-cmdpal-jsonrpc-transport

Conversation

@michaeljolley

@michaeljolley michaeljolley commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Warning

This PR is one in a series of PRs focused on adding TypeScript/JavaScript extension support to Command Palette.

This PR should not be merged until PR #49324 is merged into it.

Note

To test the final result, run the branch associated with PR #49364.

Summary

Phase 2 of the effort to add JavaScript/TypeScript extensions to Command Palette (CmdPal). It adds the low-level C# host primitives only: the JSON-RPC transport and the extension manifest parser. This is additive library code under Microsoft.CmdPal.UI.ViewModels. Nothing is wired into DI, the runner, or the UI yet; that comes in a later phase.

Tracking issue: #48707

Stacking

This PR stacks on Phase 1 (the TypeScript SDK, PR #49321) and targets dev/mjolley/phase-1-cmdpal-ts-sdk, not main. Please keep it as a draft until Phase 1 is reviewed and merged. Phase 3 (adapters/proxies) will stack on top of this branch.

What is included

JSON-RPC 2.0 message model (Services/JsonRpc/)

  • Request, response, notification, and error models using System.Text.Json source-generated contexts (AOT friendly).
  • Error object with the standard code constants (parse error, invalid request, method not found, invalid params, internal error), plus a JsonRpcException.

JsonRpcConnection transport (Services/JsonRpc/JsonRpcConnection.cs)

  • LSP-style Content-Length framing over a pair of byte streams (typically a child process stdout/stdin), so it is unit-testable with in-memory pipes.
  • Byte-accurate UTF-8 framing; async read loop that handles partial and coalesced reads.
  • SendRequestAsync with id correlation and a 10 second request timeout; a generic overload that deserializes the result via a JsonTypeInfo<T>.
  • Fire-and-forget SendNotificationAsync.
  • Dispatch of inbound requests and notifications to registered handlers; unknown inbound methods return a method-not-found error.
  • Writes serialized behind a lock; stderr is pumped and logged out-of-band (never part of the protocol).
  • Disposal cancels pending requests and stops the read loop cleanly. Protocol errors are surfaced via exceptions/events and logged rather than swallowed.

JSExtensionManifest parser (Models/JSExtensionManifest.cs)

  • Parses a package.json: top-level npm fields (name, version, description, main, engines.node) plus the cmdpal section (displayName, icon, publisher, capabilities, debug, debugPort, main override).
  • TryParse / TryParseFile return success plus a failure reason. Recognized as a CmdPal extension iff it has a cmdpal object AND a non-empty name AND either cmdpal.main or top-level main resolves to an existing file.

Tests

Added MSTest coverage in Microsoft.CmdPal.UI.ViewModels.UnitTests using in-memory pipes (no real Node process):

  • Framing encode/decode round-trips including multi-byte UTF-8 (byte-accurate Content-Length on both the write and inbound decode paths) and partial/coalesced reads.
  • Request/response correlation, concurrent correlation, timeout, error-to-exception, and null-result-to-default.
  • Notification dispatch and inbound request dispatch (including unknown method to method-not-found).
  • Disposal cancels pending requests.
  • Manifest parse of valid/invalid package.json covering each validation rule.

All 24 new tests pass; the full CmdPal solution filter build is clean (exit code 0) with no new analyzer or StyleCop warnings.

Adds low-level C# host primitives for JavaScript/TypeScript CmdPal extensions:

- JSON-RPC 2.0 message model (request/response/notification/error) using System.Text.Json source-generated contexts.

- JsonRpcConnection: an LSP-style Content-Length framed transport over stdio streams with byte-accurate framing, an async read loop handling partial and coalesced reads, request/response correlation with a 10 second timeout, fire-and-forget notifications, inbound request/notification dispatch, serialized writes, out-of-band stderr logging, and disposal that cancels pending requests.

- JSExtensionManifest: parses package.json (npm fields plus the cmdpal section) and validates it as a CmdPal extension via a TryParse/TryParseFile API that returns success plus a failure reason.

This is additive library code only; nothing is wired into DI or the UI yet (that is a later phase).

Unit tests cover framing round-trips including multi-byte UTF-8 and partial/coalesced reads, request correlation and timeout, notification dispatch, and each manifest validation rule using in-memory pipes.

Refs #48707

Co-authored-by: Copilot App <[email protected]>

Copilot-Session: 2a035fc1-177d-4e3f-8c8d-88614dd3bb52
…k' into dev/mjolley/phase-2-cmdpal-jsonrpc-transport
… points

- JsonRpcConnection: reject an inbound Content-Length that exceeds a 32 MB maximum before allocating the body buffer, so a malformed extension cannot force a multi-gigabyte allocation. The oversized header surfaces as an InvalidDataException via the read loop and Error event.

- JSExtensionManifest: require the entry point to be a relative path that stays within the extension directory. Absolute/rooted paths and '..' traversal that escapes the directory are now rejected with clear reasons, matching the spec's relative-path requirement.

Adds unit tests for the oversized Content-Length rejection and for absolute and directory-escaping entry points.

Co-authored-by: Copilot App <[email protected]>

Copilot-Session: 2a035fc1-177d-4e3f-8c8d-88614dd3bb52
@michaeljolley
michaeljolley marked this pull request as ready for review July 21, 2026 17:05
@michaeljolley michaeljolley self-assigned this Jul 21, 2026
michaeljolley and others added 8 commits July 22, 2026 13:06
…o phase 2

Co-authored-by: Copilot App <[email protected]>
Copilot-Session: 762976e0-2453-40ca-8034-978a04a19d1e
Address six review findings in the Command Palette JS extension
transport and manifest layers:

- Partial-write framing: acquire the write lock cancellably, then emit
  the header, body, and flush under a non-cancellable token so a frame
  is never half-written; any write failure moves the connection to a
  terminal closed state so it is never reused.
- Reader-exit terminal state: flip an atomic connection state when the
  read loop exits and re-check it after a pending request is registered,
  so sends after EOF fail fast instead of waiting for the timeout.
- Bounded stderr: replace unbounded ReadLineAsync with a new
  BoundedStderrReader that truncates oversized lines, rate limits per
  window, and caps total forwarded volume.
- Decoupled notifications: enqueue inbound notifications onto a bounded
  channel drained by a dedicated consumer task, so slow or reentrant
  handlers never block frame reading or response correlation. Full queue
  drops the oldest entry with a rate limited warning.
- Disposal safety: enter the terminal state first, complete the queue,
  drain in-flight writers, await the read loop, error pump, and
  notification consumer, and dispose the write lock last so an active
  writer never triggers ObjectDisposedException.
- Manifest hardening: ignore unknown fields, surface missing or
  malformed values through the parse result, restrict entry points to
  .js, .mjs, and .cjs, and reject reparse-point (junction or symlink)
  containment escapes. Expose a normalized NameKey for a future
  discovery-level uniqueness check.

Add unit tests covering each area.

Co-authored-by: Copilot App <[email protected]>
Copilot-Session: 3f0875f9-177f-40db-888c-248a044d2bb9
Propagates the stale-test and golden-fixture corrections from
dev/mjolley/phase-1-cmdpal-ts-sdk so the SDK typecheck stays green at
this layer.

Co-authored-by: Copilot App <[email protected]>
Copilot-Session: 311db8aa-f8db-47bb-943f-5447037946c7
… 2, phase 2)

Fixes three round-2 phase-2 findings in the Command Palette JSON-RPC transport:

- Non-cancellable writes: frame emission (WriteAsync/FlushAsync) no longer runs
  under CancellationToken.None. It is now bounded by a dedicated write timeout and
  linked to disposal, so a child that stops draining stdin can no longer hang a
  write (or the write lock) forever. On timeout or disposal the write is abandoned,
  the connection enters its terminal closed state (raising Disconnected so the owner
  tears the child down), and the caller fails fast with a transport error. Caller
  cancellation still never corrupts a live, reusable connection.
- Malformed-body logging: the parse-failure log path now truncates the offending
  payload to a small cap with a marker instead of logging the full body up to the
  32 MB frame cap.
- Inbound request fan-out: inbound requests are drained by a fixed pool of workers
  through a bounded queue, replacing the untracked fire-and-forget tasks. This caps
  concurrency, applies backpressure to the peer when saturated, and lets disposal
  await the in-flight handlers cleanly.

Adds unit tests covering the write timeout and disposal teardown of a stuck write,
truncated logging of an oversized malformed body, and bounded inbound concurrency
with clean disposal. Updates the disposal-during-write test to the new contract.

Co-authored-by: Copilot App <[email protected]>
Copilot-Session: 311db8aa-f8db-47bb-943f-5447037946c7
Bring the phase-1 TypeScript SDK round-2 fixes down onto phase-2. Clean merge (ts-sdk vs C# transport, no overlap).

Co-authored-by: Copilot App <[email protected]>
Copilot-Session: 311db8aa-f8db-47bb-943f-5447037946c7
… flood, and slow dispose

Round 3 review remediation for the Phase 2 JSON-RPC transport (PR #49323).

r3-p2-01 (reader-admission deadlock): the read loop no longer awaits bounded
work-queue admission. Dispatch is now synchronous: response and error frames are
correlated and completed immediately on the reader, while requests and
notifications are admitted with non-blocking TryWrite. An over-budget request is
rejected with a JSON-RPC server-busy error and notifications are dropped, so the
reader always stays free to route responses that in-flight handlers await.

r3-p2-02 (count-only bounds): the notification and request queues now enforce
aggregate byte budgets in addition to their item counts, so frames up to the
32 MiB cap can no longer retain tens of gigabytes. Over-budget requests are
rejected and notifications dropped rather than admitted.

r3-p2-03 (protocol-error log flood): protocol-error logging is routed through a
new bounded RateLimitedProtocolLog that emits at most a fixed number of entries
per window and reports a suppressed-count summary, so a peer that streams
malformed frames cannot flood the log. The limiter holds only fixed counters.

r3-p2-04 (slow dispose): disposal now drains the write lock and every background
task under a single aggregate Stopwatch deadline instead of a separate per-task
timeout, bounding total shutdown time regardless of worker count.

Adds MSTest coverage for each behavior: an in-flight handler completing while the
request queue is saturated, byte-budget rejection of requests and dropping of
notifications, rate-limited protocol logging, and bounded disposal with many
blocked workers.

Co-authored-by: Copilot App <[email protected]>
Copilot-Session: 311db8aa-f8db-47bb-943f-5447037946c7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Product-Command Palette Refers to the Command Palette utility

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant