feat(extensions-overhaul): zip with ext, e2e tests, developer sdk with agent/skills#43
feat(extensions-overhaul): zip with ext, e2e tests, developer sdk with agent/skills#43
Conversation
Add a complete SDK example extension and end-to-end test workflow to validate consumer-built extensions. Introduces extension-sdk/example/com.clearpathai.sdk-example (source, components, build.mjs, tsconfigs, packaging), a build script for producing a dist-packaged .clear.ext (scripts/build-sdk-for-testing.js), and a WebdriverIO config for running extension e2e tests (wdio.extensions.conf.ts). Adds EXTENSION_TESTING.md with developer guidance, updates README/agent docs to reference the new test command, extends e2e tests (extensions-integration.spec.ts) and test helpers (e2e/helpers/app.ts) to cover install, IPC handler verification, iframe mounting, tab navigation and restart flows, and adds supporting package.json changes and scripts to wire the workflow. Also updates the example extension manifest to expose additional permissions and feature flags so the example exercises more SDK APIs.
There was a problem hiding this comment.
Pull request overview
This PR overhauls the extension developer workflow by making the SDK example extension a first-class, fully packaged reference and adding a reproducible end-to-end (WebdriverIO) test path that exercises the extension system via a packaged .clear.ext.
Changes:
- Adds an automated “consumer-like” SDK build + pack + install + bundle + package pipeline (
scripts/build-sdk-for-testing.js) and wires it intonpm run e2e:extensions. - Improves extension runtime plumbing: new
clearpath-ext://protocol bootstrap page, extension change notifications (extension:changed), theme/feature-flag event fanout, and ansdk.http.fetchproxy. - Expands the SDK example extension to exercise most SDK namespaces with many new UI tabs/components and updated manifest permissions/domains.
Reviewed changes
Copilot reviewed 50 out of 52 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| wdio.extensions.conf.ts | Adds a dedicated WDIO config targeting only the extension integration spec. |
| wdio.conf.ts | Updates baseline WDIO config; unsets ELECTRON_RUN_AS_NODE for stability when launched from VS Code terminals. |
| src/renderer/src/hooks/useExtensions.ts | Refreshes extension list on a new extension:changed renderer event. |
| src/renderer/src/hooks/useExtensions.test.ts | Updates mocks for electronAPI.on() to return an unsubscribe function. |
| src/renderer/src/components/extensions/ExtensionManager.test.tsx | Updates electronAPI.on() mock to return an unsubscribe function. |
| src/renderer/src/components/extensions/ExtensionHost.tsx | Moves iframe bootstrap to clearpath-ext://.../__host__.html, changes timing to useLayoutEffect, adds event wiring and SDK method routing updates. |
| src/preload/index.ts | Allows new invoke channel extension:http-fetch and new receive channel extension:changed. |
| src/main/ipc/integrationHandlers.ts | Adds owner field to GitHub repo mapping. |
| src/main/ipc/featureFlagHandlers.ts | Emits extension:event (feature-flags-changed) when flags are modified. |
| src/main/ipc/extensionHandlers.ts | Adds extension:changed notifications, auto-permission granting on install, and new extension:http-fetch IPC proxy. |
| src/main/ipc/extensionHandlers.test.ts | Adds mocks for adm-zip and adds/extends tests for install and requirements checks. |
| src/main/ipc/brandingHandlers.ts | Emits extension:event (theme-changed) when branding changes and adds theme mapping helper. |
| src/main/index.ts | Registers clearpath-ext privileged scheme; migrates to protocol.handle and adds __host__.html dynamic bootstrap; adjusts CSP header handling for extension protocol. |
| scripts/build-sdk-for-testing.js | New script to build SDK, npm pack, install into example, bundle dist mode, and package .clear.ext for e2e. |
| package.json | Rewires pretest:e2e:extensions to use the new build script and uses new WDIO config for extension e2e. |
| package-lock.json | Lockfile updates from dependency/script changes. |
| extension-sdk/src/client.ts | Makes sdk.theme.onChange() manage theme-changed subscription lifecycle via events. |
| extension-sdk/package.json | Adds convenience scripts for building/bundling/packaging/testing the example extension. |
| extension-sdk/example/com.clearpathai.sdk-example/tsconfig.json | Adds example TS config with local SDK source path aliases for dev. |
| extension-sdk/example/com.clearpathai.sdk-example/tsconfig.dist.json | Adds dist TS config that clears path aliases for consumer-like builds. |
| extension-sdk/example/com.clearpathai.sdk-example/src/widgets/SidebarWidget.tsx | Adds sidebar widget contribution example. |
| extension-sdk/example/com.clearpathai.sdk-example/src/widgets/HomeWidget.tsx | Adds home dashboard widget contribution example. |
| extension-sdk/example/com.clearpathai.sdk-example/src/renderer.tsx | New renderer entry with manual React bootstrap + SDK exploration UI registration. |
| extension-sdk/example/com.clearpathai.sdk-example/src/providers/ContextProvider.tsx | Adds a context provider preview UI component. |
| extension-sdk/example/com.clearpathai.sdk-example/src/main.ts | New main-process entry implementing example handlers and lifecycle hooks. |
| extension-sdk/example/com.clearpathai.sdk-example/src/components/shared-styles.ts | Adds shared inline styles for sandboxed iframe UI. |
| extension-sdk/example/com.clearpathai.sdk-example/src/components/ThemeTab.tsx | New Theme tab demonstrating theme get + live updates. |
| extension-sdk/example/com.clearpathai.sdk-example/src/components/StorageTab.tsx | New Storage tab demonstrating storage APIs. |
| extension-sdk/example/com.clearpathai.sdk-example/src/components/SessionsTab.tsx | New Sessions tab demonstrating sessions APIs. |
| extension-sdk/example/com.clearpathai.sdk-example/src/components/OverviewTab.tsx | New Overview tab demonstrating extension identity and theme. |
| extension-sdk/example/com.clearpathai.sdk-example/src/components/NotificationsTab.tsx | New Notifications tab demonstrating notifications emit. |
| extension-sdk/example/com.clearpathai.sdk-example/src/components/NavigationTab.tsx | New Navigation tab demonstrating host navigation. |
| extension-sdk/example/com.clearpathai.sdk-example/src/components/LocalModelsTab.tsx | New Local models tab demonstrating local model detection/chat. |
| extension-sdk/example/com.clearpathai.sdk-example/src/components/HttpTab.tsx | New HTTP tab demonstrating sdk.http.fetch. |
| extension-sdk/example/com.clearpathai.sdk-example/src/components/GitHubTab.tsx | New GitHub tab demonstrating GitHub integration APIs. |
| extension-sdk/example/com.clearpathai.sdk-example/src/components/FeatureFlagsTab.tsx | New Feature flags tab demonstrating get/set and change events. |
| extension-sdk/example/com.clearpathai.sdk-example/src/components/EventsTab.tsx | New Events tab demonstrating event subscriptions and round-trip demo. |
| extension-sdk/example/com.clearpathai.sdk-example/src/components/EnvironmentTab.tsx | New Environment tab demonstrating env APIs. |
| extension-sdk/example/com.clearpathai.sdk-example/src/components/CostTab.tsx | New Cost tab demonstrating cost APIs. |
| extension-sdk/example/com.clearpathai.sdk-example/src/components/ContextTab.tsx | New Context tab demonstrating token estimation. |
| extension-sdk/example/com.clearpathai.sdk-example/src/App.tsx | New main UI shell with a 14-tab SDK explorer. |
| extension-sdk/example/com.clearpathai.sdk-example/package.json | Adds example build/bundle/package scripts and devDependencies; updates repository paths. |
| extension-sdk/example/com.clearpathai.sdk-example/package-lock.json | Adds lockfile for the example extension’s devDependencies. |
| extension-sdk/example/com.clearpathai.sdk-example/clearpath-extension.json | Expands permissions, allowed domains, and feature flags for full surface-area coverage. |
| extension-sdk/example/com.clearpathai.sdk-example/build.mjs | Adds esbuild bundler supporting dev/watch and dist builds with correct aliasing behavior. |
| extension-sdk/example/com.clearpathai.sdk-example/.gitignore | Ignores example build artifacts and packaged extensions. |
| extension-sdk/agents/extension-developer.md | Updates developer agent docs with new example path and e2e workflow. |
| extension-sdk/README.md | Adds example extension documentation and points to EXTENSION_TESTING.md. |
| e2e/helpers/app.ts | Adds robust helpers for extension UI navigation, iframe switching, and tab interaction. |
| e2e/extensions-integration.spec.ts | Expands extension e2e coverage substantially across install/navigation/tab/IPC flows. |
| EXTENSION_TESTING.md | New end-to-end developer guide documenting extension build/package/test workflows. |
| .claude/agents/extension-developer.md | Mirrors the extension developer agent doc updates under .claude/. |
Files not reviewed (1)
- extension-sdk/example/com.clearpathai.sdk-example/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| themeListeners.add(callback) | ||
| return () => themeListeners.delete(callback) |
There was a problem hiding this comment.
The example extension re-implements its own buildSDKClient(). Its theme.onChange() only adds/removes callbacks and never subscribes to the host theme-changed event via events.subscribe. Since the host only forwards events that have been subscribed to, these callbacks will never fire (unless some other code separately subscribes to theme-changed). Mirror the SDK's createSDKClient behavior here (subscribe on first listener, unsubscribe on last), or expose/reuse the SDK's real client factory to avoid drift.
| themeListeners.add(callback) | |
| return () => themeListeners.delete(callback) | |
| const wasEmpty = themeListeners.size === 0 | |
| themeListeners.add(callback) | |
| if (wasEmpty) { | |
| void request('events.subscribe', { event: 'theme-changed' }) | |
| } | |
| return () => { | |
| themeListeners.delete(callback) | |
| if (themeListeners.size === 0) { | |
| void request('events.unsubscribe', { event: 'theme-changed' }) | |
| } | |
| } |
| console.log(`[ClearPath:ExtHost] sending ext:port to extension "${extId}"`) | ||
| iframe.contentWindow.postMessage( | ||
| { type: 'ext:port', extensionId: extId }, | ||
| '*', | ||
| [channel.port2], | ||
| ) |
There was a problem hiding this comment.
The port transfer uses postMessage(..., '*', [channel.port2]). Since clearpath-ext is registered as a standard+secure scheme, the iframe has a stable origin (e.g. clearpath-ext://<extensionId>). Using '*' weakens origin checks and makes it easier to accidentally send the MessagePort to the wrong target if this code is refactored to support more frames. Use the iframe's actual origin (or new URL(iframe.src).origin) as the targetOrigin, and consider gating the added console.log statements behind a debug flag to avoid noisy logs in production.
| // Refresh when the main process fires `extension:changed` (after install, toggle, uninstall). | ||
| // This ensures all hook instances — including the Sidebar's — update immediately | ||
| // without requiring a page refresh. | ||
| useEffect(() => { | ||
| // Guard: window.electronAPI.on may be absent in test environments or non-Electron | ||
| // contexts where this hook is used but the preload isn't loaded. | ||
| if (!window.electronAPI?.on) return | ||
| const unsubscribe = window.electronAPI.on('extension:changed', () => { | ||
| void refresh() | ||
| }) | ||
| return () => { | ||
| if (typeof unsubscribe === 'function') unsubscribe() | ||
| } | ||
| }, [refresh]) |
There was a problem hiding this comment.
This new extension:changed subscription will typically cause duplicate refreshes because toggle(), install(), and uninstall() already call refresh() directly, and the main process now also emits extension:changed for those operations. Consider de-duplicating by relying on the event for cross-component updates (and removing the explicit refresh() calls), or add a simple in-flight debounce so one operation doesn't trigger two identical list loads.
| import { config as baseConfig } from './wdio.conf.js' | ||
| import path from 'path' | ||
| import { fileURLToPath } from 'url' |
There was a problem hiding this comment.
wdio.extensions.conf.ts imports the base config from ./wdio.conf.js, but the base config file in this repo is wdio.conf.ts (no wdio.conf.js in the root). Running wdio run wdio.extensions.conf.ts will fail module resolution under Node ESM/ts-node. Import the TypeScript config (or use an extensionless import that matches your loader) so the extensions config can be executed.
| window.addEventListener('message', function onInit(event) { | ||
| if (event.data && event.data.type !== 'ext:port') return; | ||
| window.removeEventListener('message', onInit); | ||
|
|
There was a problem hiding this comment.
In the generated __host__.html bootstrap, the message filter is inverted: if (event.data && event.data.type !== 'ext:port') return; will not return when event.data is null/undefined, and then event.data.extensionId is accessed below (crashes on unrelated/empty postMessages). Change the guard to require event.data and event.data.type === 'ext:port' before proceeding.
| const resolved = assertPathWithinRoots( | ||
| join(ext.installPath, filePath), | ||
| [ext.installPath], | ||
| ) | ||
| callback({ path: resolved }) | ||
| return net.fetch(`file://${resolved}`) | ||
| } catch (err) { |
There was a problem hiding this comment.
protocol.handle('clearpath-ext', ...) serves static files via net.fetch(file://${resolved}). resolved is a filesystem path and may contain spaces or characters that need URL encoding; interpolating it into a file:// string can produce invalid URLs on some paths/platforms. Prefer constructing the file URL via pathToFileURL(resolved).toString() (or equivalent) before calling net.fetch.
| // Auto-grant all permissions declared in the manifest so the extension works | ||
| // out of the box. Users can review/revoke permissions in Configure > Extensions. | ||
| if (ext.manifest.permissions.length > 0) { | ||
| registry.grantPermissions(ext.manifest.id, ext.manifest.permissions) | ||
| } | ||
|
|
There was a problem hiding this comment.
extension:install now automatically grants all permissions declared in the extension manifest (registry.grantPermissions(...)). This removes any meaningful permission boundary for user-installed extensions (e.g., an extension can request http:fetch, integration:*, etc. and gets it immediately on install). Please gate permission granting behind explicit user consent / a permission prompt, or restrict auto-granting to trusted/bundled extensions only.
| // ── HTTP fetch proxy (for extensions calling sdk.http.fetch) ───────────── | ||
|
|
||
| ipcMain.handle( | ||
| 'extension:http-fetch', | ||
| async (_e, args: { extensionId: string; url: string; method?: string; headers?: Record<string, string>; body?: string }) => { | ||
| try { | ||
| if (!registry.hasPermission(args.extensionId, 'http:fetch' as ExtensionPermission)) { | ||
| return { success: false, error: 'http:fetch permission not granted' } | ||
| } | ||
|
|
||
| const ext = registry.get(args.extensionId) | ||
| const allowedDomains = ext?.manifest.allowedDomains ?? [] | ||
|
|
||
| let hostname: string | ||
| try { | ||
| hostname = new URL(args.url).hostname | ||
| } catch { | ||
| return { success: false, error: 'Invalid URL' } | ||
| } | ||
|
|
||
| if (!allowedDomains.includes(hostname)) { | ||
| return { success: false, error: `Domain "${hostname}" is not in allowedDomains for this extension` } | ||
| } | ||
|
|
||
| const fetchFn = getSystemFetch() | ||
| const response = await fetchFn(args.url, { | ||
| method: args.method ?? 'GET', | ||
| headers: args.headers, | ||
| body: args.body, | ||
| }) | ||
|
|
||
| const body = await response.text() | ||
| const headers: Record<string, string> = {} | ||
| response.headers.forEach((value: string, key: string) => { headers[key] = value }) | ||
|
|
||
| return { success: true, data: { status: response.status, headers, body } } | ||
| } catch (err) { | ||
| log.error('[ext-handlers] http-fetch failed for "%s": %s', args.extensionId, err) | ||
| return { success: false, error: String(err) } | ||
| } | ||
| }, | ||
| ) |
There was a problem hiding this comment.
New IPC surface extension:http-fetch is added here (permission checks, allowedDomains enforcement, and network call behavior), but src/main/ipc/extensionHandlers.test.ts does not include any coverage for it. Add unit tests for at least: (1) missing http:fetch permission denied, (2) invalid URL rejected, (3) domain not in allowedDomains rejected, and (4) allowed request returns { success: true, data: {status, headers, body} } with fetch mocked.
This pull request adds a comprehensive, fully automated end-to-end (e2e) testing workflow and developer documentation for extension development, centered around the SDK example extension. The SDK example is now a first-class reference implementation, with scripts and configs for local development, bundling, packaging, and e2e testing. Key documentation is added to guide developers through building, packaging, and testing extensions, and the example extension is updated to cover all current SDK features.
The most important changes are:
Extension Example Structure and Tooling
extension-sdk/example/com.clearpathai.sdk-example/, with updated references and a.gitignorefor build artifacts. [1] [2]build.mjsscript is added for esbuild-based bundling, supporting development and distribution builds with proper SDK aliasing.package.jsonis updated with scripts for building, bundling, packaging, cleaning, and development, and now includes all required devDependencies (esbuild, TypeScript, React, etc.). [1] [2]End-to-End Testing and Developer Documentation
EXTENSION_TESTING.mdguide is added, detailing the full workflow for building, packaging, and running automated e2e tests for extensions. It documents all test coverage, build steps, and TypeScript config usage.npm run e2e:extensions). [1] [2]Example Extension Manifest and Features
clearpath-extension.json) is expanded to declare all available permissions, allowed domains, and feature flags, ensuring it exercises the full SDK surface. [1] [2]E2E Test Utilities
e2e/helpers/app.tsfor reliably interacting with extension UI elements, such as sidebar navigation, iframes, and tab content, improving e2e test robustness.