Skip to main content

Plugins Module

The Plugins Module (@ever-works/agent/plugins) is the core plugin infrastructure for the Ever Works platform. It handles plugin discovery, loading, lifecycle management, settings resolution, and enable/disable scoping. This module is @Global() and provides the foundation upon which all facades and pipeline operations depend.

Module Structure

packages/agent/src/plugins/
├── index.ts # Barrel exports
├── plugins.module.ts # PluginsModule (Global, dynamic)
├── plugins.constants.ts # Constants, state machine, events
├── interfaces/
│ └── plugins-module-options.interface.ts # Module configuration interfaces
├── entities/
│ ├── plugin.entity.ts # PluginEntity (admin-level)
│ ├── user-plugin.entity.ts # UserPluginEntity (user-level)
│ └── work-plugin.entity.ts # WorkPluginEntity (work-level)
├── repositories/
│ ├── plugin.repository.ts # PluginRepository
│ ├── user-plugin.repository.ts # UserPluginRepository
│ └── work-plugin.repository.ts # WorkPluginRepository
└── services/
├── plugin-registry.service.ts # In-memory registry with fast lookups
├── plugin-loader.service.ts # Discovery and loading from filesystem
├── plugin-lifecycle-manager.service.ts # State machine and lifecycle hooks
├── plugin-settings.service.ts # 4-level settings resolution
├── plugin-manifest-validator.service.ts # Manifest validation
├── plugin-version-checker.service.ts # Version compatibility checks
├── plugin-class-validator.service.ts # Plugin class interface validation
├── plugin-context-factory.service.ts # PluginContext creation
├── plugin-bootstrap.service.ts # Startup orchestration
├── custom-capability-registry.service.ts # Dynamic capability registration
├── plugin-operations.service.ts # High-level plugin operations
└── settings-schema-validator.service.ts # JSON Schema validation for settings

Architecture

Plugin State Machine

Plugins transition through well-defined states:

Valid state transitions (defined in plugins.constants.ts):

FromTo
unloadedloading
loadingloaded, error
loadedunloading
unloadingunloaded, error
errorloading, unloading

Key Components

PluginsModule (Dynamic Module)

A @Global() NestJS module with forRoot() and forRootAsync() static methods:

// Synchronous configuration
PluginsModule.forRoot({
pluginPaths: ['./plugins'],
builtInPlugins: [{ plugin: MyPlugin, manifest: myManifest }],
platformVersion: '1.0.0'
});

// Async configuration (factory pattern)
PluginsModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
pluginPaths: config.get('PLUGIN_PATHS')
}),
inject: [ConfigService]
});

PluginsModuleOptions (17 properties):

PropertyTypeDescription
pluginPathsstring[]Filesystem paths to scan for plugins
builtInPluginsPluginModule[]Programmatically registered plugins
platformVersionstringFor version compatibility checks
autoDiscoverbooleanEnable filesystem scanning
autoLoadbooleanAutomatically load discovered plugins
loadTimeoutnumberPlugin load timeout (ms)
enableWatcherbooleanWatch for plugin file changes
secretEncryptionKeystringKey for encrypting secret settings

PLUGIN_ENTITIES: The module exports [PluginEntity, UserPluginEntity, WorkPluginEntity] for TypeORM registration.

PluginRegistryService

An in-memory registry providing O(1) lookups by plugin ID and fast filtering by category or capability.

Data structures:

  • plugins: Map<string, RegisteredPlugin> -- Primary store
  • byCategory: Map<PluginCategory, Set<string>> -- Category index
  • byCapability: Map<string, Set<string>> -- Capability index

Key methods:

MethodDescription
register(plugin, manifest, options)Registers a plugin and builds indexes
unregister(pluginId)Removes a plugin and cleans up indexes
get(pluginId)Returns RegisteredPlugin or undefined
getByCategory(category)All plugins in a category
getByCapability(capability)All plugins with a capability
getDefaultForCapability(capability)First loaded plugin declaring itself as default
getReady()All plugins in loaded state
updateState(pluginId, newState, error?)Transitions state and records history
getDefaultForCapabilityScoped(cap, dirId?, userId?)Scoped default with DB lookups
getEnabledPluginsScoped(cap?, dirId?, userId?)All enabled plugins for scope
isPluginEnabledForScope(pluginId, dirId?, userId?)Checks enable status with scope resolution

RegisteredPlugin interface:

interface RegisteredPlugin {
plugin: IPlugin; // Plugin instance
manifest: PluginManifest; // Validated manifest
state: PluginState; // Current state
builtIn: boolean; // Built-in vs. discovered
installPath?: string; // Filesystem path
registeredAt: number; // Registration timestamp
loadedAt?: number; // Load completion timestamp
stateHistory: PluginStateTransition[]; // Full state transition log
error?: Error | string; // Last error
}

Enable Resolution Algorithm

The resolvePluginEnabled() function is the single source of truth for determining if a plugin is active:

PluginLoaderService

Discovers plugins on the filesystem and loads them into the registry.

Discovery flow:

  1. Scans configured pluginPaths works
  2. Reads package.json from each subwork
  3. Validates everworks.plugin manifest section
  4. Returns DiscoveredPlugin[]

Loading flow:

  1. Checks version compatibility
  2. Dynamic-imports the plugin module
  3. Finds the plugin class (default export, named export, or class scan)
  4. Validates the plugin class against the manifest
  5. Merges runtime manifest (from plugin.getManifest()) with package.json manifest
  6. Registers in the in-memory registry
  7. Persists to the database via PluginRepository

Dependency resolution: The loader performs a topological sort across all plugins (built-in + discovered) based on declared dependencies. Circular dependencies are detected and reported.

Default plugin paths (searched in order):

const DEFAULT_PLUGIN_PATHS = [
'./plugins',
'./node_modules/@ever-works/plugins',
'../plugins',
'../../packages/plugins',
'./packages/plugins'
];

PluginLifecycleManagerService

Manages plugin state transitions and lifecycle hooks.

MethodDescription
callOnLoad(pluginId)Calls plugin.onLoad(context) with a PluginContext
unload(pluginId)Calls plugin.onUnload(), unregisters custom capabilities, cleans up
shutdownAll()Gracefully unloads all plugins (used during module destruction)
isValidTransition(from, to)Validates state transitions against the state machine
getValidTransitions(from)Returns valid target states from a given state

PluginSettingsService

The 4-level settings resolution engine. See Facades Module for how facades consume resolved settings.

Resolution hierarchy (highest to lowest priority):

LevelSourceWhen Used
1Work settingsworkId provided, configMode !== 'admin-only'
2User settingsuserId provided, configMode !== 'admin-only'
3Admin settingsAlways checked
4Environment variablesx-envVar schema annotation maps to process.env
5Plugin defaultsdefault values from JSON Schema

Configuration modes:

ModeDescription
hybrid (default)Users and works can override admin settings
admin-onlyOnly admin-level settings are used; user/work settings are ignored

Security features:

  • x-envVar fields are never stored in the database; they are read exclusively from environment variables
  • x-secret fields are stored in separate secretSettings columns
  • Masked placeholder values (********) are stripped during updates to prevent accidental overwrite

Scope validation: Settings with x-scope: 'work' can only be updated at the work level. Settings with x-scope: 'user' cannot be updated at the global level.

Key methods:

MethodDescription
getResolvedSettings(pluginId, options)Full resolution with source tracking
getSettings(pluginId, options)Plain key-value settings (no source info)
updateAdminSettings(pluginId, settings)Update global settings
updateUserSettings(pluginId, userId, settings)Update user-level settings
updateWorkSettings(pluginId, dirId, settings)Update work-level settings
getSettingsSchema(pluginId)Raw JSON Schema
getSettingsSchemaForContext(pluginId, context)Schema filtered by scope context
validateSettings(pluginId, settings, options)Validate against schema and scope

Plugin Entities

PluginEntity (Admin Level)

ColumnTypeDescription
iduuid (PK)Auto-generated
pluginIdvarchar (unique)Plugin identifier
namevarcharDisplay name
versionvarcharInstalled version
categoryvarcharPlugin category
capabilitiesjsonArray of capability strings
manifestjsonFull manifest snapshot
statevarcharCurrent state
builtInbooleanWhether built-in
settingsjsonAdmin-level settings
secretSettingsjsonAdmin-level secrets (encrypted)
configurationModevarcharhybrid or admin-only

UserPluginEntity (User Level)

ColumnTypeDescription
iduuid (PK)Auto-generated
userIdvarcharLoose coupling (no FK)
pluginIdvarcharPlugin identifier
pluginEntityIduuid (FK)References PluginEntity
enabledbooleanUser-level enable toggle
autoEnableForWorksbooleanAuto-enable in new works
settingsjsonUser-level settings
secretSettingsjsonUser-level secrets
metadatajsonUser-specific metadata

WorkPluginEntity (Work Level)

ColumnTypeDescription
iduuid (PK)Auto-generated
workIdvarcharLoose coupling (no FK)
pluginIdvarcharPlugin identifier
pluginEntityIduuid (FK)References PluginEntity
enabledbooleanWork-level enable toggle
activeCapabilityvarchar (nullable)Marks this plugin as active for a capability
settingsjsonWork-level settings
secretSettingsjsonWork-level secrets
metadatajsonWork-specific metadata
priorityintOrdering priority

Plugin Events

Constants defined in plugins.constants.ts:

EventDescription
plugin.registeredPlugin added to registry
plugin.unregisteredPlugin removed from registry
plugin.loadedPlugin onLoad completed
plugin.unloadedPlugin onUnload completed
plugin.state_changedAny state transition
plugin.errorPlugin error occurred
plugin.settings_changedSettings updated at any scope

Setting Source Priority

const SETTING_SOURCE_PRIORITY: Record<SettingSource, number> = {
work: 1, // Highest
user: 2,
admin: 3,
env: 4,
default: 5 // Lowest
};

Usage

Checking Plugin Availability

import { PluginRegistryService } from '@ever-works/agent/plugins';

@Injectable()
export class MyService {
constructor(private readonly registry: PluginRegistryService) {}

isSearchAvailable(): boolean {
return this.registry.getByCapability('search').some((p) => p.state === 'loaded');
}

getAvailableAiProviders(): string[] {
return this.registry
.getByCapability('ai')
.filter((p) => p.state === 'loaded')
.map((p) => p.manifest.name);
}
}

Managing Plugin Settings

import { PluginSettingsService } from '@ever-works/agent/plugins';

@Injectable()
export class SettingsController {
constructor(private readonly settings: PluginSettingsService) {}

async updateUserApiKey(userId: string, pluginId: string, apiKey: string) {
await this.settings.updateUserSettings(
pluginId,
userId,
{
apiKey
},
{
secretKeys: ['apiKey']
}
);
}

async getWorkSettings(pluginId: string, workId: string, userId: string) {
return this.settings.getResolvedSettings(pluginId, {
workId,
userId,
includeSecrets: false
});
}
}