How Extensions Are Manifested and Loaded in OpenWork: A Technical Deep Dive

OpenWork discovers extensions through a server-generated JSON manifest, dynamically imports each extension's ESM bundle at runtime, and registers their capabilities into a global openworkContext object that is gated by configurable desktop policies.

In the different-ai/openwork codebase, extensions are first-class plugins that augment the desktop client with new UI panels, commands, and provider integrations. Understanding exactly how extensions are manifested and loaded in OpenWork requires tracing the lifecycle from server-side manifest generation through client-side dynamic import and policy enforcement.

The Three-Phase Extension Lifecycle

OpenWork treats extension loading as a structured pipeline with distinct discovery, registration, and enforcement phases.

Phase 1: Manifest Discovery on the Server

At workspace startup, the server aggregates available extensions from three distinct sources: built-in extensions shipped with the application, locally installed plugins found under .opencode/plugins/, and marketplace extensions enabled for the organization. The core logic resides in apps/server/src/extensions-export.js, which exports the exportExtensions function. This utility scans the local plugin directory for .ts and .js files, extracts the manifest export from each, and merges these entries with the built-in catalog to produce a unified JSON manifest.

Phase 2: Loading and Runtime Registration

The desktop client fetches this manifest via a GET request to /workspace/:id/extensions/export. For each entry in the manifest, the client performs a dynamic import using import(entryPointUrl), where the URL may be a local file:// path or an HTTPS URL for marketplace extensions. The imported module must export a registerExtension function, which receives the global openworkContext defined in packages/types/src/openwork-context.ts. This context maintains an internal registry of type kind: "extensions" (among other kinds) and exposes methods like registerPanel() and registerCommand() that inject the extension's capabilities into the OpenWork runtime.

Phase 3: Policy Enforcement and UI Presentation

Before rendering extensions in the Extensions settings panel or command palette, the client consults desktop policies defined in packages/types/src/den/desktop-policies.ts. These policies include "extensions" (master toggle) and "localExtensions" (local installation permission). If a policy disables a specific class of extensions, the UI omits those cards and the runtime marks them as disabled, effectively sandboxing unauthorized functionality.

Server-Side Manifest Generation

The exportExtensions function in apps/server/src/extensions-export.js serves as the single source of truth for extension availability. It recursively scans the .opencode/plugins/ directory, validates discovered plugin files, and constructs a manifest array containing metadata such as id, name, description, and entry points. This manifest is cached and served to clients upon workspace initialization, ensuring that the client has a complete catalog before attempting any dynamic imports.

The OpenWork Context and Registration API

The openworkContext object acts as the dependency injection container for extensions. According to packages/types/src/openwork-context.ts, extensions register themselves by calling context methods that populate internal maps for panels, commands, and MCPs. The context also tracks diagnostic state via expectedBranch values such as "extensions-only", which indicates when the UI should limit functionality to extension-specific workflows. Registration is synchronous and atomic; once an extension calls registerExtension(), its capabilities are immediately available to the command palette and control pane without requiring an application restart.

Security and Policy Gating

Security is enforced at the policy layer defined in packages/types/src/den/desktop-policies.ts. Administrators can disable local extension management entirely using the policy "Allow users to install and manage extensions locally." or hide built-in extensions using "Allow users to see and use OpenWork's built-in extensions...". These boolean flags are evaluated in the UI layer before rendering extension cards, ensuring that policy violations never reach the execution phase. End-to-end tests in evals/specs/skills-local.slow.test.ts verify that policy-compliant extensions like OpenWork Browser are correctly loaded and listed, while restricted extensions remain hidden.

Implementing a Local Extension

Developers can create local extensions by placing TypeScript files in the .opencode/plugins/ directory. Each extension must export a manifest object and a registerExtension function.

// .opencode/plugins/hello-world-extension.ts
export const manifest = {
  id: "hello-world",
  name: "Hello World",
  description: "Shows a greeting panel",
  entry: "hello-world-extension.js",
};

export async function registerExtension(openworkContext) {
  // Register a new panel in the control pane
  openworkContext.registerPanel({
    id: "hello-world-panel",
    title: "Hello World",
    render: () => <div>Hello, OpenWork!</div>,
  });

  // Expose a command to open the panel
  openworkContext.registerCommand({
    id: "hello-world.show",
    title: "Show Hello World",
    action: () => openworkContext.openPanel("hello-world-panel"),
  });
}

When the desktop client initializes, the server's exportExtensions includes this plugin in the manifest. The client dynamically imports the entry point, invokes registerExtension with the global context, and the panel and command become instantly available. Flow tests in evals/flows/extensions-sidebar-and-header.flow.ts validate this end-to-end integration.

Summary

  • Manifest Discovery: The server aggregates extensions from built-in catalogs and .opencode/plugins/ via apps/server/src/extensions-export.js.
  • Dynamic Loading: The client fetches the manifest and performs import() on each extension's ESM bundle, registering capabilities into openworkContext.
  • Policy Enforcement: Desktop policies in packages/types/src/den/desktop-policies.ts gate visibility and execution, preventing unauthorized extensions from loading.
  • Hot Reload: Local extensions can be added to .opencode/plugins/ and loaded without restarting the application, triggering a manifest refresh and dynamic registration.

Frequently Asked Questions

What file format does OpenWork use for extension manifests?

OpenWork expects each extension to export a JavaScript object named manifest containing id, name, description, and entry properties. This manifest is consumed by the server's exportExtensions function and validated before being sent to the client as JSON.

Where does OpenWork look for locally installed extensions?

The server scans the .opencode/plugins/ directory within the user's workspace for files ending in .ts or .js. These paths are resolved and included in the extension manifest generated by apps/server/src/extensions-export.js.

How does OpenWork handle security for third-party extensions?

Security is enforced through desktop policies defined in packages/types/src/den/desktop-policies.ts. Administrators can disable local extension installation or hide specific extension categories, ensuring that only vetted marketplace extensions or built-in plugins are visible in the UI and executable by the runtime.

Can extensions be loaded without restarting the OpenWork desktop app?

Yes. When a user adds a new plugin to .opencode/plugins/, the server detects the change and updates the extension manifest. The client can then dynamically import the new module and call its registerExtension function against the live openworkContext, making the new functionality available immediately without an application restart.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →