Skip to content

Conversation

@rekmarks
Copy link
Member

@rekmarks rekmarks commented Jan 7, 2026

No description provided.

@claude
Copy link

claude bot commented Jan 8, 2026

PR Review: [stack] omnium-gatherum

This is a substantial PR that introduces the omnium-gatherum package with CapTP-based communication and a caplet management system. Overall, the architecture is well-designed with good adherence to POLA principles. Here are my findings:


🎯 Strengths

Architecture & Design

  • Excellent use of CapTP: The switch from custom RPC to @endo/captp is the right choice - leverages battle-tested Endo infrastructure for remote presence handling
  • POLA compliance: Good use of facets (makeFacet) and attenuated dependencies in controllers
  • Clean separation: Storage abstraction layer with StorageAdapter and NamespacedStorage types is well-designed
  • Type safety: Strong TypeScript types throughout, proper use of superstruct for runtime validation

Code Quality

  • Comprehensive test coverage: Good unit tests for all new modules (background-captp, kernel-captp, controllers, storage)
  • Integration tests: The CapTP integration test validates end-to-end E() communication
  • Documentation: TypeDoc comments are thorough and helpful

🐛 Issues & Concerns

1. Type Safety Issue in background-captp.ts (Minor)

// Line 56 in background-captp.ts
params: [captpMessage as unknown as Record<string, never>],

This double cast (as unknown as Record<string, never>) is a code smell. The issue is that CapTPMessage is Record<string, Json> but you're forcing it to Record<string, never>. This defeats type checking.

Recommendation: Either adjust the type definition or add a comment explaining why this unsafe cast is necessary.


2. Potential Memory Leak in CapletController (Moderate)

packages/omnium-gatherum/src/controllers/caplet/caplet-controller.ts:219-222

When installation fails after launching the subcluster, the subcluster is not terminated:

// Launch subcluster
const { subclusterId } = await launchSubcluster(clusterConfig);

// Store caplet data - if this fails, subcluster is orphaned
await Promise.all([
  storage.set(manifestKey(id), manifest),
  // ...
]);

Recommendation: Wrap in try-catch and terminate the subcluster if storage operations fail:

let subclusterId: string;
try {
  ({ subclusterId } = await launchSubcluster(clusterConfig));
  await Promise.all([/* storage ops */]);
} catch (error) {
  if (subclusterId) {
    await terminateSubcluster(subclusterId).catch(/* log error */);
  }
  throw error;
}

3. Error Handling in background.ts (Minor)

packages/extension/src/background.ts:97-99

offscreenStream.write(notification).catch((error) => {
  logger.error('Failed to send CapTP message:', error);
});

Errors are logged but not propagated. If CapTP message sending fails, the caller gets no indication. This could lead to silent failures.

Recommendation: Consider either:

  1. Propagating the error back through the CapTP abort mechanism
  2. Implementing a circuit breaker pattern after N consecutive failures
  3. At minimum, document why errors are swallowed

4. Missing Validation in chrome-storage.ts (Moderate)

packages/omnium-gatherum/src/controllers/storage/chrome-storage.ts:29

The keys() method calls storage.get(null) which retrieves ALL keys from chrome.storage.local. This could be expensive and include non-caplet data.

async keys(prefix?: string): Promise<string[]> {
  const all = await storage.get(null);  // Gets EVERYTHING
  const allKeys = Object.keys(all);
  // ...
}

Recommendation: Document performance implications or consider using chrome.storage.local.getBytesInUse() to warn if storage is large. Alternatively, maintain a metadata key that tracks all namespaced keys.


5. Race Condition in CapletController uninstall (Minor)

packages/omnium-gatherum/src/controllers/caplet/caplet-controller.ts:255-276

If terminateSubcluster fails, the caplet metadata is still deleted from storage, leaving an inconsistent state (running subcluster with no metadata).

Recommendation: Only delete storage after successful termination:

async uninstall(capletId: CapletId): Promise<void> {
  // ... get subclusterId ...
  
  // Terminate first
  await terminateSubcluster(subclusterId);
  
  // Only clean up storage if termination succeeded
  await Promise.all([/* delete operations */]);
}

6. Global E() Setup Timing (Security)

packages/extension/src/background.ts:18 and packages/omnium-gatherum/src/background.ts:25

defineGlobals() is called at module load time, which means globalThis.E is set before SES lockdown. However, the code comment suggests these globals should be available after lockdown for console access.

Recommendation: Verify that E is properly hardened by SES lockdown. If these definitions happen before import './endoify.js', they might not be frozen. Consider moving global definitions to the trusted prelude to ensure proper ordering.


7. Test Coverage Gap (Moderate)

While there are good unit tests, I notice:

  • No tests for error propagation in the full background → offscreen → kernel-worker path
  • No tests for concurrent caplet installations
  • No tests for storage corruption recovery (e.g., manifest exists but subclusterId missing)

Recommendation: Add integration tests covering:

  1. Network-like failures in the stream layer
  2. Concurrent operations on CapletController
  3. Storage corruption scenarios (already partially handled in code, but not tested)

8. Unused Import (Trivial)

packages/kernel-browser-runtime/src/background-captp.ts:2

import type { JsonRpcMessage, JsonRpcCall } from '@metamask/kernel-utils';

JsonRpcMessage is used but JsonRpcCall appears unused in the exports.


9. Type Inconsistency in KernelFacade

packages/kernel-browser-runtime/src/types.ts:8-15

KernelFacade uses Kernel['methodName'] references, which means if Kernel method signatures change, KernelFacade automatically inherits those changes. This is good for consistency but means the facade isn't actually providing any isolation.

Question: Is this intentional? If the facade should be a stable API boundary, consider explicitly defining signatures rather than referencing Kernel types.


📋 Recommendations

High Priority

  1. Fix the subcluster cleanup issue in CapletController (chore: Convert to monorepo #2)
  2. Add error propagation or circuit breaker for CapTP send failures (Publish packages to npm #3)
  3. Verify SES lockdown timing for global E() (chore: Update codeowners #6)

Medium Priority

  1. Document or optimize the keys() performance in chrome-storage (chore: Delete cruft #4)
  2. Fix uninstall race condition (docs: Update monorepo readme #5)
  3. Add integration tests for error paths (chore: Update repository PR template #7)

Low Priority

  1. Clean up type casts in background-captp (fix: Install Corepack as prereq in broken workflows #1)
  2. Remove unused imports (feat: Establish test extension and shims library #8)

🔒 Security Considerations

Good:

  • POLA principles applied consistently
  • Storage namespacing prevents key collisions
  • Proper use of harden() throughout

⚠️ Verify:

  • Ensure SES lockdown happens before global E() is accessible
  • Validate that CapTP doesn't leak capabilities through error messages
  • Check that chrome.storage.local permissions are properly declared in manifest

📊 Overall Assessment

Quality: ★★★★☆ (4/5)
Architecture: ★★★★★ (5/5)
Test Coverage: ★★★★☆ (4/5)
Security: ★★★★☆ (4/5)

This is high-quality work that establishes a solid foundation for the omnium-gatherum caplet system. The use of CapTP is the right architectural choice. The main concerns are around error handling edge cases and cleanup scenarios. With the recommended fixes, this would be production-ready.

Recommendation: ✅ Approve with requested changes


Great work on this PR! The architecture is sound and the code quality is high. Let me know if you'd like me to elaborate on any of these points.

rekmarks and others added 9 commits January 8, 2026 10:23
…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

This commit 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

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

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>
Implement Phase 1.2 of the omnium plan - Define Caplet Structure:

- Add modular controller architecture with POLA attenuation via makeFacet()
- Add storage abstraction layer (StorageAdapter, NamespacedStorage)
- Add Chrome storage adapter for platform storage
- Add CapletController for managing installed caplets
- Add Caplet types with superstruct validation
- Wire CapletController into background.ts and expose on globalThis.omnium.caplet
- Add comprehensive unit tests for all controller code
- Update PLAN.md to reflect implementation
Consolidate CapletControllerState from multiple top-level keys
(installed, manifests, subclusters, installedAt) into a single
`caplets: Record<CapletId, InstalledCaplet>` structure.

Changes:
- Add ControllerStorage abstraction using Immer for state management
- Controllers work with typed state object instead of storage keys
- Only modified top-level keys are persisted (via Immer patches)
- Remove state corruption checks (no longer possible with atomic storage)
- Fix makeFacet type - use string | symbol instead of keyof MethodGuard
- Update PLAN.md to reflect new storage architecture

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@socket-security
Copy link

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedimmer@​10.2.0991008393100

View full report

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.

2 participants