Skip to main content
Version: 0.5.x

Introduction​

To improve the overall flexibility and extensibility of the framework, the core framework adopts a domain-driven design approach to model its core capabilities, organizing each domain capability's implementation and contracts in a decoupled manner. The apps/lina-core/pkg/plugin directory serves as the public Go contract boundary, exposing stable domain service interfaces to plugins. Source plugins consume the full capability.Services catalog through pluginhost.Services, while dynamic plugins consume the published hostServices capability subset through the pluginbridge.Services returned by pluginbridge.Default().

Component Structure​

PathResponsibility
capability/Aggregates stable host capabilities, including Services, plugin-scoped service bindings, and narrow interfaces for each domain. Source plugins use the full catalog directly; dynamic plugins use the published bridged subset.
pluginhost/Source plugin host namespace providing compile-time registration interfaces and runtime callback contracts.
pluginbridge/Dynamic plugin bridge namespace providing the pluginbridge.Default() / pluginbridge.New() runtime capability catalog, Wasm execution contracts, and hostServices encoding/decoding.

Domain Capability Overview​

MethodDomain DocumentationSPI ExtensionDescription
AI()AI CapabilitySupportedAggregates text, image, vector, audio, vision, document, safety, and video sub-capabilities
APIDoc()API Documentation-Parses route operation keys, localizes module labels and operation summaries
Auth()Auth Capability-Token issuance, tenant switching, impersonation tokens, and authorization permission view reading and checks
Users()Users Capability-User views, search, and visibility validation
BizCtx()Business Context-Reads current request user, tenant, impersonation, and platform bypass state
Cache()Cache Capability-Plugin-scoped runtime cache
Dict()Dict Capability-Dictionary type and value lifecycle management, label resolution, and cache refresh
Files()Files Capability-File views, upload, streaming open, and visibility validation
HostConfig()Host Config-Reads host configuration values; dynamic plugins must declare keys
I18n()i18n Capability-Source plugin runtime translation; not exposed as a dynamic host service
Jobs()Jobs Capability-Scheduled task lifecycle management
Manifest()Manifest Resources-Reads read-only resources under the current plugin's manifest/
Notifications()Notifications-Notification message lifecycle management
Org()Org CapabilitySupportedOptional org capability, department and position lifecycle management
Plugins()Plugin Governance-Aggregates plugin registry, plugin config, plugin state, and lifecycle sub-capabilities
Route()Dynamic Route-Reads current dynamic route metadata
Sessions()Sessions-Online session search, batch reading, and revocation
Storage()Files CapabilitySupportedPlugin-scoped object storage operations
Tenant()Tenant CapabilitySupportedOptional tenant capability, reads current tenant, visibility, switch validation, and source plugin tenant filtering
Lock()Lock Capability-Plugin-visible distributed lock acquisition, renewal, and release

Plugin-scoped capabilities are bound to the plugin identity by the host. For example, Plugins().Config() only reads the current plugin's own config.yaml, Manifest() only reads the current plugin's manifest/ resources, and AI() injects the source plugin ID into subsequent provider requests.

Capability Lifecycle​

Plugin capabilities are divided into two phases: declaration and runtime. The declaration phase is the static registration and discovery stage where the host uses declaration output to build governance state before business execution. The runtime phase is when plugin business logic executes and consumes the domain capability services provided by the host.

Declaration-Phase Capabilities​

Declaration-phase capabilities are the static registration output of plugins. Source plugins register through pluginhost.Declarations at compile time, while dynamic plugins declare through plugin.yaml manifests and pluginbridge.Declarations.

For detailed design and usage of declaration-phase capabilities, see Declaration Capabilities Overview.

Source Plugin Declaration Phase​

Source plugins register the following declarations through pluginhost.Declarations in init():

Declaration EntryDescription
ID()Returns a stable plugin identifier consistent with plugin.yaml
Assets()Binds the plugin's embedded filesystem, including manifests, frontend pages, SQL, and i18n resources
Lifecycle()Registers 16 lifecycle callbacks for install, upgrade, disable, uninstall, tenant disable, tenant delete, and install mode changes
Hooks()Subscribes to host extension point events, such as auth.login.succeeded, plugin.enabled, system.started, etc.
HTTP()Registers plugin HTTP route contribution callbacks, triggered uniformly at host startup
Jobs()Registers scheduled task contribution callbacks, triggered uniformly by the host scheduler
Providers()Declares domain capability provider factories, such as ProvideTenant, ProvideOrg, and ProvideAIText
Access()Registers menu filtering and permission filtering callbacks for runtime dynamic adjustment of workspace navigation and permissions

Dynamic Plugin Declaration Phase​

Dynamic plugins express declarations through plugin.yaml manifests and build-time contracts:

Declaration SourceDescription
plugin.yamlDeclares plugin identity, version, dependencies, menus, permissions, multi-tenant strategy, public static assets, and hostServices authorization requests
Routes()Declares route group bindings, specifying API prefixes and route packages
Jobs()Registers scheduled task contracts through host-service calls
WASM custom sectionsEmbeds metadata in .wasm artifacts such as ABI version, runtime type, codecs, and export function names
protocol.BridgeSpecDefines bridge ABI contracts, including version number, runtime type, codec, and alloc/execute export names

Runtime-Phase Capabilities​

Runtime-phase capabilities are the services available during plugin business logic execution. Source plugins and dynamic plugins share the host domain capability model, but with different public entry points.

Source Plugin Runtime​

Source plugins access runtime capabilities through pluginhost.Services. This interface embeds capability.Services, directly exposing all domain capability methods.

Dynamic Plugin Runtime​

Dynamic plugins access published runtime capabilities through pluginbridge.Default(). All calls are transmitted via WASI host call and dispatched by the host after validation against the hostServices authorization snapshot. Dynamic plugins access a subset of capabilities from the dynamic service catalog and have three exclusive capabilities:

Capability EntryDescription
Published domain capabilitiesSuch as AI, Auth, Cache, Storage, accessed via host-call bridging; I18n() is not published as a dynamic host service
Runtime()Exclusive: log writing, plugin state read/write, time retrieval, UUID generation, node identity reading
Network()Exclusive: governed outbound HTTP requests, requiring authorized target addresses declared in plugin.yaml
RecordStore()Exclusive: ORM-like wrapper for the data service, only accessing declared plugin-owned tables

Capability Differences Between Source and Dynamic Plugins​

Source plugins access the full domain capability set through capability.Services, including both read and write operations (e.g., Jobs().Run(), Sessions().Revoke(), Notifications().Send()). These write operations are executed after governance validation covering state, target, tenant, and audit checks.

The dynamic plugin pluginbridge.Services interface only exposes the capability subset published as hostServices. Dynamic plugins can only invoke specific methods that have been declared in plugin.yaml, authorized by the host, and registered with the WASM host-service dispatcher. Nearly all domain capabilities now expose both read and write interfaces to dynamic plugins — for example, the sessions dynamic service includes the sessions.revoke force-revoke command, the jobs dynamic service includes write operations like jobs.run and jobs.status.set, and services like dict, files, hostconfig, org, and users also provide full lifecycle management methods such as Create, Update, and Delete. Plugin authors should refer to the dynamic service catalog table below for the specific method list of each service.

SPI Architecture​

Some domain capabilities are optional framework capabilities — they are not built into the main framework but are implemented by provider plugins and injected into the host runtime. Current capabilities using the SPI pattern include AI, Org, and Tenant, with official provider plugins linapro-ai-core, linapro-org-core, and linapro-tenant-core respectively.

Architecture Design​

The SPI (Service Provider Interface) pattern's core idea is separating capability contracts from capability implementations. The host defines the public interface for a domain capability (the SPI contract), and provider plugins implement the actual business logic. The host uses deferred construction to avoid hard dependencies on optional plugins during startup — the provider is only instantiated when the capability is first consumed.

Design PointDescription
Deferred constructionThe host only constructs provider instances when the capability is first consumed, avoiding hard dependencies on optional plugins during startup
Graceful degradationWhen no provider is available, the capability returns an empty result or unavailable state, not nil or an error
Source injectionThe host injects request context, plugin identity, and auxiliary capabilities into the provider via ProviderEnv
Enable state isolationProvider state is independent of business entry visibility — a plugin's business entry may be invisible to the current tenant but still available as a platform capability provider

SPI Service Registration​

Source plugins declare SPI factories through pluginhost.Declarations.Providers(). Each factory is a constructor function that receives a ProviderEnv parameter and returns a provider instance:

ProviderEnv is the runtime context injected by the host into the provider, typically containing:

Injection ItemDescription
Plugin identityThe current provider plugin's ID, for auditing and isolation
Request contextCurrent request's tenant, user, and other business context
Auxiliary capabilitiesHost capabilities needed by the provider implementation, such as TenantFilter, user views, etc.

SPI Provider Status Check​

The host uses Plugins().State().IsProviderEnabled() to determine if a provider is available. This check has different semantics from IsEnabled:

Check MethodSemanticsUse Case
IsEnabledWhether the plugin's business entry is visible to the current tenantMenu filtering, route visibility, permission filtering
IsProviderEnabledWhether the plugin is platform-enabled and can serve as a framework capability providerPre-check before AI, Org, Tenant capability calls

The provider check ensures that even if a business entry is disabled at the tenant level, platform-level capabilities can still serve normally.

Plugin Implementing SPI Providers​

Implementing an SPI provider as a source plugin involves two steps: registration and implementation. Using the Org capability as an example:

Registering the SPI factory:

The provider plugin registers a factory function through the Providers() declaration entry in init():

func init() {
plugin := pluginhost.NewDeclarations("my-author-my-org-provider")
if err := plugin.Providers().ProvideOrg(func(ctx context.Context, env orgspi.ProviderEnv) (orgspi.Provider, error) {
return &myOrgProvider{env: env}, nil
}); err != nil {
panic(err)
}

if err := pluginhost.RegisterSourcePlugin(plugin); err != nil {
panic(err)
}
}

Implementing the SPI contract:

The provider must implement the Provider interface defined in the capability domain package. Using orgcap.Provider as an example, the provider must implement complete org capabilities including department views, position views, etc.:

type myOrgProvider struct {
env orgcap.ProviderEnv
}

func (p *myOrgProvider) ListUserDeptAssignments(ctx context.Context, userIDs []string) ([]DeptAssignment, error) {
// Query the provider's own org data
// Can access host-injected auxiliary capabilities via p.env
}

func (p *myOrgProvider) GetUserDeptInfo(ctx context.Context, userID string) (*DeptInfo, error) {
// Implement department info query
}

The Tenant capability additionally provides the tenantcap.Resolver interface, responsible for resolving tenant identity from HTTP requests, composable as a chain of responsibility based on request headers, domains, paths, tokens, or other strategies.

Dynamic Plugins and SPI​

Dynamic plugins cannot directly register SPI factories because providers need to implement Go interfaces and run within the host process. Dynamic plugins interact with SPI capabilities as follows:

Interaction MethodDescription
Consume SPI capabilitiesInvoke published SPI capability methods through hostServices declarations, e.g., service: ai, service: org, service: tenant
Check SPI provider statusUse the plugins.provider_enabled.check dynamic method to determine provider availability
Status queryUse capability.available and capability.status dynamic methods to query capability availability and active providers

Dynamic plugins declare consumption of SPI capabilities in plugin.yaml:

hostServices:
- service: ai
methods:
- text.generate
- service: org
methods:
- users.dept_name.get
- service: tenant
methods:
- tenants.current

Dynamic hostServices​

Dynamic plugins cannot directly access host implementation packages. They declare the host services they need to call through hostServices in plugin.yaml. A typical plugin.yaml hostServices declaration looks like this:

hostServices:
- service: runtime
methods:
- log.write
- state.get
- state.set
- service: storage
methods:
- put
- get
- list
resources:
paths:
- exports/
- service: data
methods:
- list
- get
- create
resources:
tables:
- plugin_demo_reports
- service: network
methods:
- request
resources:
- url: https://api.example.com/v1/*
- service: hostconfig
methods: [get]
resources:
keys:
- workspace.basePath
- service: manifest
methods: [get]
resources:
paths:
- profile.yaml
- service: ai
methods:
- text.generate

Resource Declaration Forms​

Resource TypeDeclaration FieldServices
noneNo resources declaredruntime, apidoc, auth, ai, users, bizctx, dict, files, jobs, notifications, plugins, route, sessions, org, tenant
pathresources.pathsstorage, manifest
tableresources.tablesdata
keyresources.keyshostconfig
resourceresources[].url or resources[].ref with service-specific attributesnetwork, cache, lock, notifications (only messages.send)

Production validation requires data service tables to belong to the plugin's own namespace. Dynamic plugins must not declare host core tables like sys_*, nor should they use host table names as targets for plugin data capabilities.

Dynamic Service Catalog​

ServiceDomain DocumentationResource TypeMethods
aiAI Capabilitynonetext.generate, text.method_status.get, ai.methods.status.batch_get, image.generate, image.edit, embedding.create, audio.transcribe, audio.synthesize, vision.analyze, document.analyze, document.cite, safety.moderate, video.generate, video.edit, video.extend, video.operation.get, video.operation.cancel
apidocAPI Documentationnoneroute_text.resolve, route_texts.resolve, route_title_operation_keys.find
authAuth Capabilitynonetoken.tenant.select, token.tenant.switch, token.impersonation_token.issue, token.impersonation_token.revoke, authz.permissions.batch_get, authz.permissions.batch_has, authz.permissions.has, authz.users.platform_admin.check, authz.role_permissions.replace
bizctxBusiness Contextnonecurrent.get
cacheCache Capabilityresourceget, get_many, set, set_many, delete, delete_many, incr, expire
dataRecord Storetablelist, get, batch_get, create, update, delete, transaction
dictDict Capabilitynonedict.refresh, dict.type.get, dict.type.batch_get, dict.type.list, dict.type.visible.ensure, dict.type.keys.visible.ensure, dict.type.create, dict.type.update, dict.type.delete, dict.value.get, dict.value.batch_get, dict.value.labels.resolve, dict.value.list, dict.value.visible.ensure, dict.value.values.visible.ensure, dict.value.create, dict.value.update, dict.value.delete, dict.value.by_type.delete
filesFiles Capabilitynonefiles.batch_get, files.list, files.visible.ensure, files.upload, files.create_from_storage, files.metadata.update, files.delete, files.delete_many
hostconfigHost Configkeyget, sys_config.get, sys_config.value.set, sys_config.reset
jobsJobs Capabilitynonejobs.batch_get, jobs.list, jobs.visible.ensure, jobs.create, jobs.update, jobs.delete, jobs.run, jobs.status.set, jobs.register
lockLock Capabilityresourceacquire, renew, release
manifestManifest Resourcespathget, get_many, list
networkNetwork Capabilityresourcerequest
notificationsNotificationsRead no resource; messages.send uses resources[].refmessages.batch_get, messages.list, messages.by_source.batch_get, messages.visible.ensure, messages.send, messages.delete, messages.by_source.delete, messages.mark_read, messages.mark_unread
orgOrg Capabilitynonecapability.available, capability.status, org.assignment.user_profiles.batch_get, org.department.tree.list, org.department.batch_get, org.department.list, org.department.create, org.department.update, org.department.delete, org.post.batch_get, org.post.options.list, org.post.create, org.post.update, org.post.delete, org.department.visible.ensure_many, org.post.visible.ensure_many, org.assignment.by_user.replace, org.assignment.by_user.cleanup
pluginsPlugin Governancenoneplugins.current.get, plugins.batch_get, plugins.registry.list, plugins.tenant.list, config.get, plugins.state.enabled.check, plugins.state.provider_enabled.check, plugins.state.enabled_authoritative.check, plugins.lifecycle.tenant_plugin_disable.ensure, plugins.lifecycle.tenant_plugin_disabled.notify, plugins.lifecycle.tenant_delete.ensure, plugins.lifecycle.tenant_deleted.notify
routeDynamic Routenonemetadata.get
runtimeDynamic Runtimenonelog.write, state.get, state.get_many, state.set, state.set_many, state.delete, state.delete_many, info.now, info.uuid, info.node
sessionsSessions Capabilitynonesessions.current.get, sessions.list, sessions.batch_get, sessions.users.online.batch_get, sessions.visible.ensure, sessions.revoke, sessions.revoke_many
storageFiles Capabilitypathput, put.init, put.chunk, put.commit, put.abort, get, delete, delete.batch, list, list.cursor, stat, stat.batch
tenantTenant Capabilitynonecapability.available, capability.status, tenant.context.current, tenant.context.info, tenant.context.platform_bypass, tenant.directory.batch_get, tenant.directory.list, tenant.membership.validate, tenant.membership.list_by_user, tenant.directory.visible.ensure_many, tenant.plugins.enabled.set, tenant.plugins.defaults.provision, tenant.filter.context
usersUsers Capabilitynoneusers.current.get, users.batch_get, users.resolve.batch, users.list, users.visible.ensure, users.create, users.update, users.delete, users.status.set, users.password.reset, users.assignment.roles.replace

Dynamic Plugin Exclusive Capabilities​

Runtime(), Network(), and RecordStore() are dynamic plugin exclusive capabilities on the pluginbridge.Default() catalog. They are not part of capability.Services because source plugins already run within the host process and can use native host equivalents.

CapabilityPublic EntryDescription
Runtime()pluginbridge.Default().Runtime()Dynamic plugins use the WASI host-service client to write logs, read/write state, read time, generate UUIDs, and read node identity; source plugins use native host logging and runtime context directly
Network()pluginbridge.Default().Network()Dynamic plugins access governed outbound HTTP through host-service authorization; source plugins use native host HTTP client or injected domain services
RecordStore()pluginbridge.Default().RecordStore()Dynamic plugins use the pluginbridge-side facade wrapping the data host-service protocol and typed query plans; source plugins use their own DAO or provider seams