Skip to content

Conversation

@rekmarks
Copy link
Member

@rekmarks rekmarks commented Jan 14, 2026

Answers @kumavis's challenge of "What're you afraid of CapTP or something?" by replacing our kernel JSON-RPC API with E() on a facet of the kernel. This makes it easy to expose as much of the kernel API as we want via eventual send, and allows us to benefit from pipelining internally. In addition, it facilitates the removal of the command stream and the related RPC API logic. Finally, a number of rationalizations are applied to the extension and omnium.

This PR is part of a stack followed by: #752, #753, and #754


Note

Introduces CapTP-based communication and removes the JSON-RPC command stream.

  • Add CapTP utilities in kernel-browser-runtime: makeBackgroundCapTP, makeKernelCapTP, makeKernelFacade, and JSON-RPC captp notification helpers; export new KernelFacade and CapTPMessage types
  • Update kernel-worker to use CapTP for background↔kernel messaging; initialize kernel without a command stream; route internal RPC only for panel/internal comms
  • Refactor @metamask/ocap-kernel to drop command stream handling and kernel RPC entrypoints; Kernel.make now accepts only platform services and the database; adjust stop() accordingly
  • Migrate extension and omnium backgrounds/offscreens to CapTP over ChromeRuntimeDuplexStream, use global E and kernel/omnium helpers; remove env/dev-console and background trusted prelude files; add TS globals
  • Add unit and integration tests for CapTP (background-captp, kernel-side CapTP, and E() end-to-end); introduce vitest.integration.config.ts and test:integration scripts; CI gains an "Integration Tests" job
  • Update deps (add @endo/captp, @endo/eventual-send), tsconfigs, build constants, and minor coverage thresholds

Written by Cursor Bugbot for commit cbf22fd. This will update automatically on new commits. Configure here.

rekmarks and others added 4 commits January 14, 2026 11:21
…tion

Implements userspace E() infrastructure using @endo/captp to enable
the background script to use E() naturally with kernel objects.

Key changes:
- Add CapTP setup on kernel side (kernel-browser-runtime):
  - kernel-facade.ts: Creates kernel facade exo with makeDefaultExo
  - kernel-captp.ts: Sets up CapTP endpoint with kernel facade as bootstrap
  - message-router.ts: Routes messages between kernel RPC and CapTP

- Add CapTP setup on background side (omnium-gatherum):
  - background-captp.ts: Sets up CapTP endpoint to connect to kernel
  - types.ts: TypeScript types for the kernel facade

- Update message streams to use JsonRpcMessage for bidirectional support
- CapTP messages wrapped in JSON-RPC notifications: { method: 'captp', params: [msg] }
- Make E globally available in background via defineGlobals()
- Expose omnium.getKernel() for obtaining kernel remote presence

Usage:
  const kernel = await omnium.getKernel();
  const status = await E(kernel).getStatus();

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…cture

Completes the migration from JSON-RPC to CapTP for background ↔ kernel
communication and harmonizes the extension and omnium-gatherum packages.

Remove the Kernel internal RPC infrastructure entirely:
- Remove commandStream parameter from Kernel constructor and make() method
- Remove #commandStream and #rpcService private fields
- Remove #handleCommandMessage method and stream draining logic
- Delete packages/ocap-kernel/src/rpc/kernel/ directory (contained only ping handler)
- Update all Kernel.make() call sites across packages

The Kernel no longer accepts or processes JSON-RPC commands directly. All external
communication now flows through CapTP via the KernelFacade.

Move background CapTP infrastructure from omnium-gatherum to kernel-browser-runtime:
- Move background-captp.ts to packages/kernel-browser-runtime/src/
- Export from kernel-browser-runtime index: makeBackgroundCapTP, isCapTPNotification,
  getCapTPMessage, makeCapTPNotification, and related types
- Delete packages/omnium-gatherum/src/captp/ directory
- Delete packages/kernel-browser-runtime/src/kernel-worker/captp/message-router.ts
  (no longer needed since all communication uses CapTP)

Both omnium-gatherum and extension now import CapTP utilities from kernel-browser-runtime.

Update extension to use CapTP/E() instead of RpcClient:
- Replace RpcClient with makeBackgroundCapTP in background.ts
- Add getKernel() method to globalThis.kernel for E() usage
- Update ping() to use E(kernel).ping() instead of rpcClient.call()
- Remove @metamask/kernel-rpc-methods and @MetaMask/ocap-kernel dependencies

Harmonize extension trusted prelude setup with omnium:
- Delete extension separate dev-console.js and background-trusted-prelude.js
- Add global.d.ts with TypeScript declarations for E and kernel globals
- Both packages now use the same pattern: defineGlobals() call at module top

Remove unused dependencies flagged by depcheck:
- kernel-browser-runtime: Remove @endo/promise-kit
- extension: Remove @MetaMask/ocap-kernel, @metamask/utils
- kernel-test: Remove @metamask/streams, @metamask/utils
- nodejs: Remove @metamask/utils
- omnium-gatherum: Remove @endo/captp, @endo/marshal, @metamask/kernel-rpc-methods,
  @MetaMask/ocap-kernel, @metamask/utils

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
      Add comprehensive tests for the CapTP infrastructure:
      - background-captp.test.ts: Tests for utility functions and makeBackgroundCapTP
      - kernel-facade.test.ts: Tests for facade delegation to kernel methods
      - kernel-captp.test.ts: Tests for makeKernelCapTP factory
      - captp.integration.test.ts: Full round-trip E() tests with real endoify

      Configure vitest with inline projects to use different setupFiles:
      - Unit tests use mock-endoify for isolated testing
      - Integration tests use real endoify for CapTP/E() functionality

      🤖 Generated with [Claude Code](https://claude.com/claude-code)

      Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
rekmarks and others added 4 commits January 14, 2026 15:29
…ntegration

Split the vitest configuration into two separate files to fix issues
with tests running from the repo root:
- vitest.config.ts: Unit tests with mock-endoify
- vitest.integration.config.ts: Integration tests with node-endoify

Add test:integration script to run integration tests separately.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
node-version: ${{ matrix.node-version }}
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: yarn build
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general I hate that we have to build before anything except the e2e tests but we already live in sin so this is fine for now. I'm of a mind to start caching builds in CI after this PR.

@rekmarks
Copy link
Member Author

@claude review

@claude

This comment was marked as resolved.

Comment on lines +6 to +7
import { delay, isJsonRpcMessage } from '@metamask/kernel-utils';
import type { JsonRpcMessage } from '@metamask/kernel-utils';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are swapping out JSON-RPC for CapTP as our IPC layer, what's going on here? The changes to this file substitute JsonRpcMessage for both JsonRpcCall and JsonRpcResponse, but it's still JsonRpcSomething. One of the things I find very pleasing about this PR is the net code reduction from getting rid of all the JSON-RPC boilerplate, but this particular change right here puzzles me.

Copy link
Member Author

@rekmarks rekmarks Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, we get rid of the RPC Client / Server stuff and the kernel's command stream, but the captp wire messages have a JSON-RPC wrapper. This buys us a little bit of flexibility in case we ever want to pass other kinds of data over these streams. I think I can just remove the wrapper without side effects if you feel strongly about it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may actually have found another message type we want to pass over this stream so I think it's best to leave it.

/**
* A CapTP message that can be sent over the wire.
*/
export type CapTPMessage = Record<string, Json>;
Copy link
Contributor

@FUDCo FUDCo Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I notice this type is also defined the same way with the same name and exported in packages/kernel-browser-runtime/src/background-captp.ts. That feels a little sketchy to me.

Copy link
Contributor

@FUDCo FUDCo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking very good. I can feel my blood pressure dropping.

Holding off on clicking Approve right now because I'm unsure what automatic mayhem doing that might trigger. I think we might need to coordinate the order of landing with my latest PR #744 to minimize merge nightmares.

@rekmarks rekmarks enabled auto-merge January 20, 2026 19:25
Copy link
Contributor

@FUDCo FUDCo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woot!

@rekmarks rekmarks added this pull request to the merge queue Jan 20, 2026
Merged via the queue into main with commit 399c457 Jan 20, 2026
28 checks passed
@rekmarks rekmarks deleted the rekm/captp-infrastructure branch January 20, 2026 19:31
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

*/
async function startDefaultSubcluster(): Promise<void> {
const kernelStream = await connectToKernel({ label: 'background', logger });
const rpcClient = new RpcClient(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing stream cleanup on initialization failure causes duplicate listeners

Medium Severity

When main() fails after creating offscreenStream but before completing (e.g., during ping()), the stream and its chrome.runtime.onMessage listener are never cleaned up. When start() triggers a retry, a new stream is created with another listener. Both listeners remain active, causing messages to be delivered to both the old orphaned backgroundCapTP and the new one. This leads to duplicate message processing and potential state corruption in the CapTP protocol.

Additional Locations (1)

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants