Skip to content

Conversation

@rekmarks
Copy link
Member

@rekmarks rekmarks commented Jan 14, 2026

Summary

  • Add modular controller architecture with POLA attenuation via makeFacet()
  • Add ControllerStorage abstraction with Immer-based state management and debounced persistence
  • Add Controller abstract base class
  • Add CapletController for managing installed caplets
  • Add storage adapters (ChromeStorage)

Test plan

  • Build passes
  • Unit tests for controller code
  • Manual browser testing

Part 2 of 4 in PR stack for omnium-III
Depends on: #751

🤖 Generated with Claude Code


Note

Introduces a controller system with persistent state and caplet lifecycle management, and wires it into the extension background.

  • Adds abstract Controller, ControllerStorage (Immer-based, debounced per-key persistence), and POLA makeFacet helper
  • Implements CapletController with install/uninstall/list/get/getByService, manifest validation (@metamask/superstruct, semver), and subcluster launch/terminate hooks
  • Exposes omnium.caplet API in background.ts using attenuated kernel methods and a Chrome storage adapter (makeChromeStorageAdapter)
  • Extends global.d.ts types and updates manifest.json permissions to include storage
  • Adds comprehensive unit tests for controllers, storage, facets, and caplet types; updates e2e test utilities
  • Updates dependencies (@endo/exo, @metamask/*, immer, semver, @types/semver)

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

@rekmarks rekmarks requested a review from a team as a code owner January 14, 2026 22:27
@socket-security
Copy link

socket-security bot commented Jan 14, 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

throw new Error('Failed to determine subclusterId after launch');
}

return { subclusterId: newSubcluster.id };
Copy link

Choose a reason for hiding this comment

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

Race condition in concurrent subcluster ID identification

Medium Severity

The launchSubcluster wrapper identifies newly created subclusters by comparing status snapshots before and after launch. When multiple caplets are installed concurrently, all callers may capture the same "before" snapshot, launch their subclusters, then each use find() on the "after" snapshot. Since find() returns the first match, concurrent installs could identify the same subcluster as "new", causing incorrect subclusterId associations and orphaned subclusters. On uninstall, this could terminate the wrong subcluster.

Fix in Cursor Fix in Web

subclusterId,
installedAt: Date.now(),
};
});
Copy link

Choose a reason for hiding this comment

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

TOCTOU race allows duplicate caplet installations

Medium Severity

The #install method checks if a caplet is already installed (line 204) before an await call to launchSubcluster (line 219), then updates state afterward (line 221). Two concurrent install calls for the same caplet ID can both pass the "already installed" check before either updates state. This results in two subclusters being created for the same caplet, with the second update() overwriting the first, orphaning one subcluster.

Fix in Cursor Fix in Web


if (!newSubcluster) {
throw new Error('Failed to determine subclusterId after launch');
}
Copy link

Choose a reason for hiding this comment

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

Missing cleanup orphans subcluster on identification failure

Medium Severity

After launchSubcluster succeeds (line 142), if the subsequent getStatus() call fails or returns unexpected results causing the "Failed to determine subclusterId" error (line 151), the already-created subcluster is left running with no reference to it. There's no try-catch with cleanup logic to terminate the orphaned subcluster on failure. This creates a resource leak where subclusters accumulate in the kernel with no way to clean them up.

Fix in Cursor Fix in Web

@rekmarks rekmarks marked this pull request as draft January 14, 2026 23:26
@rekmarks rekmarks force-pushed the rekm/controller-architecture branch from c98290e to 6736065 Compare January 14, 2026 23:57
rekmarks and others added 5 commits January 14, 2026 16:00
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>
- 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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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