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.

@socket-security
Copy link

socket-security bot commented Jan 8, 2026

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

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updated@​types/​semver@​7.7.0 ⏵ 7.7.1100 +110075 +181100
Addedimmer@​10.2.0991008393100

View full report

rekmarks and others added 24 commits January 13, 2026 14:48
…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 <[email protected]>
…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 <[email protected]>
      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 <[email protected]>
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 <[email protected]>
- Add abstract Controller class with state management via ControllerStorage
- Convert CapletController to extend Controller base class
- Use makeFacet() pattern for returning hardened exo methods
- Add base-controller tests (12 tests)
- Add semver deep import type declaration
- Add storage permission to manifest.json

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
- Convert ControllerStorage from factory to class with static make() method
- Implement synchronous update() with debounced fire-and-forget persistence
- Fix critical debounce bug: accumulate modified keys across debounce window
- Implement bounded latency (timer not reset, max delay = one debounce interval)
- Add immediate writes when idle > debounceMs for better responsiveness
- Add clear() and clearState() methods to reset storage to defaults
- Remove old namespaced-storage implementation
- Refactor all tests to use actual ControllerStorage with mock adapters
- Add shared makeMockStorageAdapter() utility in test/utils.ts
- Update controllers to create their own storage from adapters

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Remove strict reverse DNS format requirement for CapletId to allow more flexibility during early development. Now accepts any non-empty ASCII string without whitespace, removing restrictions on hyphens, underscores, uppercase, and segment count.

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Implements Phase 1a of the caplet system, establishing the foundational
architecture for caplet vats with a working echo-caplet example. This
validates the caplet vat contract and installation lifecycle before
tackling service injection complexity.

Changes:
- Add comprehensive caplet vat contract documentation
- Create echo-caplet.js demonstrating buildRootObject pattern
- Add bundle build script using @endo/bundle-source
- Implement caplet integration tests (8 new tests, all passing)
- Create test fixtures for caplet manifests
- Refactor makeMockStorageAdapter to support shared storage
- Add plan in .claude/plans for follow-up work

Key achievements:
- Caplet vat contract fully documented with examples
- Echo-caplet bundles successfully (696KB)
- Install/uninstall lifecycle tested and working
- Service lookup by name validated
- State persistence across controller restarts verified
- 100% code coverage for CapletController maintained

Deferred to future work (Phase 1b):
- Kref capture mechanism
- Service parameter injection
- Consumer caplet implementation
- Two-caplet communication

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Implements Phase 1b functionality to store caplet root kernel references (krefs) and expose them via omnium.caplet.getCapletRoot().

This enables: omnium.caplet.install(manifest), omnium.caplet.getCapletRoot(capletId), and E(presence).method() for calling vat methods from background console.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add explicit type annotation for kernelP and use spread operator for optional rootKref field.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Remove KrefWrapper type from kernel-browser-runtime types
- Make rootKref a required string field in LaunchResult (not optional)
- Make rootKref required in InstalledCaplet and omnium LaunchResult
- Add assertions in kernel-facade for capData, subclusterId, and rootKref
- Remove isKrefWrapper function (inline check kept in makeKrefTables)
- Update tests to use simplified types and improved mocks

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add tests for validation errors in kernel-facade launchSubcluster:
- Throws when kernel returns no capData
- Throws when capData body has no subclusterId
- Throws when capData slots is empty (no root kref)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add omnium.manifests.echo so users can install caplets from the console:
  await omnium.caplet.install(omnium.manifests.echo)

Changes:
- Create src/manifests.ts with echo caplet manifest using chrome.runtime.getURL
- Add echo-caplet.bundle to vite static copy targets
- Expose manifests in background.ts via omnium.manifests
- Update global.d.ts with manifests type and missing getCapletRoot

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add omnium.loadCaplet(id) to dynamically fetch caplet manifest and bundle
- Fix vatPowers.logger missing in browser vats (iframe.ts)
- Fix SubclusterLaunchResult to return bootstrapRootKref directly
  instead of trying to extract it from bootstrap() return slots

The bootstrapRootKref is the kref of the vat root object, which is
already known when the vat launches. Previously we incorrectly tried
to get it from the slots of the bootstrap() method return value.

Next step: Wire up CapTP marshalling so E(root).echo() works with
the caplet root presence.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Set globalThis.kernel in the extension and omnium to the kernel itself. Remove
ping and getKernel methods from background console interface. The kernel
exposes ping().
…ects

Implement slot translation pattern to enable E() (eventual sends) on vat
objects from the extension background. This creates presences from kernel
krefs that forward method calls to kernel.queueMessage() via the existing
CapTP connection.

Key changes:
- Add background-kref.ts with makeBackgroundKref() factory
- Add node-endoify.js to kernel-shims for Node.js environments
- Update kernel-facade to convert kref strings to standins
- Fix launch-subcluster RPC result to use null for JSON compatibility
- Integrate resolveKref/krefOf into omnium background

The new approach uses @endo/marshal with smallcaps format (matching the
kernel) rather than trying to hook into CapTP internal marshalling, which
uses incompatible capdata format.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…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 <[email protected]>
…helpers

- Remove packages/nodejs/src/env/endoify.ts re-export, use @metamask/kernel-shims/node-endoify directly
- Update vitest configs to use kernel-shims for setup files
- Remove inline endoify imports from test files (now handled by vitest setup)
- Fix test helpers to handle SubclusterLaunchResult return type from launchSubcluster()
- Add kernel-shims dependency to kernel-test and nodejs-test-workers packages
- Set coverage thresholds to 0 temporarily

Co-Authored-By: Claude Opus 4.5 <[email protected]>
rekmarks and others added 2 commits January 13, 2026 15:20
…e configs

- Fix accidentally broken nodejs vat worker (which broke all tests relying
  on it)
- Rename node-endoify.js to endoify-node.js for consistency
- Update package.json export from ./node-endoify to ./endoify-node
- Update all vitest configs to use the new export path
- Update depcheckrc.yml ignore pattern

Co-Authored-By: Claude Opus 4.5 <[email protected]>
rekmarks and others added 4 commits January 13, 2026 20:38
- Import and initialize makeBackgroundKref to enable E() calls on vat objects
- Expose captp.resolveKref and captp.krefOf on globalThis for console access
- Refactor startDefaultSubcluster to return the bootstrap vat rootKref
- Add greetBootstrapVat function that automatically calls hello() on the
  bootstrap vat after subcluster launch on startup
- Update global.d.ts with captp type declaration for IDE support

Co-Authored-By: Claude <[email protected]>
- Rename background-kref.ts to kref-presence.ts
- Rename makeBackgroundKref to makePresenceManager
- Rename BackgroundKref type to PresenceManager
- Rename BackgroundKrefOptions to PresenceManagerOptions
- Update all imports and references across affected packages
- Update JSDoc comments to reflect new naming
- All tests pass for kernel-browser-runtime, extension, omnium-gatherum

Co-Authored-By: Claude <[email protected]>
…nvertKrefsToStandins

- Move convertKrefsToStandins from kernel-facade.ts to kref-presence.ts for better organization
- Export convertKrefsToStandins for use by kernel-facade
- Add comprehensive unit tests for convertKrefsToStandins (20 tests covering kref conversion, arrays, objects, primitives)
- Add unit tests for makePresenceManager (3 tests for kref resolution and memoization)
- Add integration test in kernel-facade.test.ts verifying kref conversion in queueMessage

Co-Authored-By: Claude <[email protected]>
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