@qlop/plugin-sdk
v0.2.1
Published
`@qlop/plugin-sdk` is the supported frontend contract for Qlop plugins.
Readme
@qlop/plugin-sdk
@qlop/plugin-sdk is the supported frontend contract for Qlop plugins.
Plugins compile against this package. They should not import app-private modules from apps/desktop.
External plugin repositories should install the published package rather than relying on monorepo
workspace linking.
pnpm add @qlop/[email protected] reactKeep the plugin dependency version exact, and keep manifest.json plugin_sdk_version equal to
that same exact SDK version. The host currently treats SDK compatibility as an exact match.
@qlop/ui-kit is not part of the external plugin contract. Reusable plugin-facing UI primitives
should come from this SDK, not from app-internal UI kit imports.
This README documents the current 0.2.x panel-based plugin model. It is intentionally strict.
Some rules described here are architectural requirements that the host will enforce more directly
over time. Plugin authors should treat them as binding now rather than waiting for stricter runtime
checks later.
What The SDK Owns
- plugin manifest types
- panel frontend module types
- host auth command helpers
- shared panel shell primitives
What The SDK Does Not Own
- direct access to app-private frontend modules
- direct access to raw Tauri internals
- arbitrary filesystem, shell, or OS automation
- workflow/catalogue source-of-truth ownership
- custom authentication storage
Qlop plugins are not mini-desktop apps with unrestricted host access. A plugin is a bounded, host-loaded extension surface used to support org workflows inside the Qlop runtime.
Runtime Model
In the current SDK version, plugins are panel-first.
That means:
- a plugin ships one compiled frontend ESM bundle
- that bundle exports a
panelsmap - the host loads the bundle
- the host selects the requested panel component
- the host renders that panel inside a Qlop-managed panel window
The host owns:
- plugin discovery
- manifest validation
- compatibility checks
- auth gating
- panel window creation
- theme resolution
- runtime import of the plugin bundle
The plugin owns:
- panel UI
- plugin-specific frontend behavior
- optional backend routes and integration logic
Plugins should assume that every panel runs inside a host-controlled shell, not in a fully independent app runtime.
React Runtime Contract
Qlop owns the React runtime for plugin panels.
That means:
- the host installs a singleton React instance on
globalThis.__QLOP_REACT__ - plugin bundles must bind to that singleton through SDK shim modules
- plugin bundles must not assume raw
reactimports will resolve at runtime - plugin bundles should not ship their own independent React copy for panel rendering
When bundling a plugin, alias the React entrypoints to the SDK shims:
pnpm exec esbuild frontend/index.ts \
--bundle \
--format=esm \
--platform=browser \
--target=es2020 \
--jsx=automatic \
--alias:react=@qlop/plugin-sdk/react \
--alias:react/jsx-runtime=@qlop/plugin-sdk/jsx-runtime \
--alias:react/jsx-dev-runtime=@qlop/plugin-sdk/jsx-dev-runtime \
--outfile=dist/index.jsIf your bundler supports aliases through configuration files instead of CLI flags, configure those same three mappings there.
Plugin Purpose
Plugins exist to support Qlop workflows, not replace Qlop's workflow authority.
A plugin may:
- provide a specialized panel for a workflow step
- query org systems through its backend
- present authenticated views or tools needed to complete a workflow
- contribute theme tokens for its own panel presentation
Preferred theme token names follow the host's shadcn-style roles, for example:
background,foregroundcard,card-foregroundprimary,primary-foregroundsecondary,secondary-foregroundmuted,muted-foregrounddestructive,destructive-foregroundborder,input,ring
A plugin must not assume it owns:
- the canonical meaning of a workflow item
- the global search/index model
- app-wide navigation policy
- raw credential storage
- unrestricted machine automation
Catalogue items and workflow records remain the host's authoritative execution inputs. Plugins are attached capability surfaces around those workflows.
Auth Model
Qlop uses external auth providers. Qlop does not define its own user model.
In phase 1:
- auth connections are defined by the app, not by plugins
- panels declare
auth_strategy - the host shows the auth UI before panel render
- the plugin does not build its own login form
- Active Directory / LDAP is the first implemented provider
AD / LDAP Contract
AD connections are app-owned and connection-oriented. They require:
idstrategy: "ad"displayNamedomainserverUrl- optional
baseDn
Supported serverUrl forms:
ldaps://host:636ldap://host:389
If ldap:// is used, Qlop attempts StartTLS by default.
Qlop binds against LDAP before it marks the session active. Multiple AD sessions may stay active for the current app runtime. Connection metadata, last-used username, and a plugin-scoped last-used connection preference may be persisted by the app for reuse.
Manifest Contract
manifest.json is the canonical contract between a plugin and the Qlop host.
Required fields:
idauthordisplay_nameversionmin_qlop_versionplugin_sdk_versionfrontend.entrypanelspermissions
Optional fields:
backend.entrytheme_pathsettingsactions
The manifest is read and validated before the frontend bundle is imported. A plugin should assume that invalid manifests, invalid version ranges, or missing runtime files can cause the host to mark the plugin unavailable without executing any plugin code.
Panel definitions
A panel definition identifies a stable UI surface within the plugin.
panel_id must be stable across plugin releases unless the panel is intentionally replaced.
Changing display names is fine. Reusing a panel_id for a different panel meaning is not.
permissions
permissions is part of the supported manifest contract in the current SDK version, even where host
enforcement is still evolving.
Plugin authors must treat permissions as a declaration of intended capability domains. Do not use
it as decorative metadata.
The architectural intent is:
- plugins declare what host capabilities they expect
- the host validates that declaration
- the host exposes only approved capability surfaces to plugin code
- undeclared capabilities should be considered unavailable
Supported permission values in 0.2.x are:
host.backend.requesthost.external.openhost.auth.readhost.auth.writehost.actions.executehost.remote_api.execute
Permissions are enforced by the host when plugin code uses the matching helper families. Keep them conservative and explicit.
Good permission declarations are:
- stable
- human-reviewable
- auditable
- narrow enough to explain why the plugin needs them
Bad permission declarations are:
- catch-all names with unclear meaning
- permissions added “just in case”
- permissions that do not correspond to real plugin behavior
Backend entry
If a plugin declares backend.entry, backend behavior should remain plugin-owned and domain-specific.
The backend is the right place for:
- LDAP or directory queries
- org API calls
- data normalization for a plugin panel
- workflow-specific external system access
The backend is not the right place to recreate host responsibilities such as plugin discovery, window management, catalogue ownership, or host auth policy.
Plugin settings
Plugins may optionally declare a host-managed settings block:
settings.schema_id- optional
settings.display_name - optional
settings.description - optional
settings.defaults
The host owns persistence. Plugins may read or update only their own settings through the SDK helper surface. Settings changes may cause the host to reload plugin runtime state.
Settings fields are manifest-declared form definitions. Current supported field types are:
texttextareanumberbooleanselectstring_listobject_list
Plugin actions
Plugins may declare custom workflow actions in manifest.json. A plugin action is metadata plus a
backend route. Catalogue workflows invoke it with the built-in plugin_action type.
Rules:
actions[].action_idmust be unique inside the pluginactions[].display_namemust be non-emptyactions[].paramsuses the same schema as plugin settingsactions[].execute.methodmust bePOSTactions[].execute.pathmust point to a plugin backend route- declaring any action requires
backend.entry - declaring any action requires
host.actions.execute
Manifest example:
{
"permissions": ["host.backend.request", "host.actions.execute"],
"backend": { "entry": "backend/routes.py" },
"actions": [
{
"action_id": "post-demo-event",
"display_name": "Post Demo Event",
"description": "Send a workflow event to the example plugin backend.",
"params": {
"schema_id": "example-plugin.post-demo-event.v1",
"fields": [
{ "key": "message", "label": "Message", "type": "text", "required": true },
{
"key": "severity",
"label": "Severity",
"type": "select",
"options": [
{ "value": "info", "label": "Info" },
{ "value": "warning", "label": "Warning" }
]
}
]
},
"execute": { "method": "POST", "path": "/actions/post-demo-event" }
}
]
}Catalogue usage:
{
"type": "plugin_action",
"condition": "always",
"on_error": "stop",
"params": {
"plugin_id": "example-plugin",
"action_id": "post-demo-event",
"values": {
"message": "{{prompt.note}}",
"severity": "info"
}
}
}Manifest Example
{
"id": "example-ad-plugin",
"author": "example-org",
"display_name": "Example AD Plugin",
"version": "1.0.0",
"min_qlop_version": "0.1.0",
"plugin_sdk_version": "0.2.0",
"frontend": {
"entry": "dist/index.js"
},
"panels": [
{
"panel_id": "example-ad-panel",
"display_name": "Example AD Panel",
"auth_strategy": "ad"
}
],
"permissions": [
"host.auth.read",
"host.auth.write"
]
}Frontend Module Example
import { definePluginFrontendModule } from '@qlop/plugin-sdk'
import ExampleAdPanel from './ExampleAdPanel'
export default definePluginFrontendModule({
panels: {
'example-ad-panel': ExampleAdPanel
}
})The exported panels map is the plugin's render contract with the host.
Rules:
- panel keys must match manifest
panel_idvalues - every exported panel must be intentionally declared in the manifest
- plugin code must not expect the host to discover components by filename or convention
- the default export must remain a plain registration object, not a side-effectful bootstrap routine
Optional Typed Helpers
The SDK exports a few no-runtime-cost helpers to keep plugin code explicit:
definePlugin(...)definePluginFrontendModule(...)definePanel(...)PanelShell
These helpers mainly improve authoring clarity and type inference.
Host Auth Helpers
The SDK also exports host auth commands:
import {
clearActiveAuthSession,
getActiveAuthSession,
listActiveAuthSessions,
getPanelAuthState,
listAuthConnections,
loginToAuthConnection
} from '@qlop/plugin-sdk'These talk to the Qlop host. They do not bypass host-managed auth.
Use them when a panel needs to inspect current auth status or trigger explicit sign-in flows later.
These helpers are examples of the intended plugin boundary:
- the plugin asks the host for auth state
- the host decides how auth is resolved and persisted
- the plugin does not reach around the host to manage raw credentials itself
For plugin-owned backend routes, use the host-mediated backend helper rather than direct localhost fetches from the panel:
import { requestPluginBackend } from '@qlop/plugin-sdk'
const response = await requestPluginBackend<MyResponse>('my-plugin', {
method: 'GET',
path: '/my-route',
query: { id: '123' }
})This keeps mounted plugin routes behind the same authenticated sidecar boundary.
For organisation-brokered external API calls, use the host-mediated organisation API helper.
This requires host.remote_api.execute, and only works when the local Qlop sidecar is configured
with an organisation service:
import { requestOrganisationApi } from '@qlop/plugin-sdk'
const response = await requestOrganisationApi<{ ticketId: string }>({
externalServiceId: 'service-desk',
operation: 'create-ticket',
params: {
title: 'Printer issue',
priority: 'normal'
}
})Plugins request named operations. They do not send raw API secrets, arbitrary URLs, or arbitrary headers. The organisation service validates params against the operation schema before calling the external API.
For simple external links that should be opened by the host shell, use the SDK opener helper.
Only https, http, and mailto URLs are supported:
import { openExternalUrl } from '@qlop/plugin-sdk'
await openExternalUrl('https://www.rundeck.com')Panel Author Responsibilities
Every panel should be written with the following assumptions:
- the host may delay panel render until auth is complete
- the host may refuse to load the plugin before render if compatibility checks fail
- the host owns the surrounding window and shell
- the host may reload or invalidate plugin modules across app sessions
- the panel should fail locally without destabilizing the rest of the app
Panels should keep their frontend responsibilities focused on:
- rendering workflow-specific UI
- collecting user input
- presenting data returned by the plugin backend or host contract
- invoking supported host helper functions through the SDK
Panels should not:
- import from
apps/desktop - assume direct filesystem access
- assume direct shell execution
- construct raw local file URLs to plugin assets outside the declared bundle contract
- treat the panel window as an independent application root
- mutate host-global state except through supported contracts
- call backend routes for a different plugin id
- rely on undeclared permissions continuing to work
Authoring Standard
Prefer function components for panel implementations.
Use PanelShell for new panels:
import { PanelShell } from '@qlop/plugin-sdk'
export default function ExamplePanel() {
return (
<PanelShell panelId="example-panel" title="Example Panel">
<p className="text-sm text-muted-foreground">Example plugin panel scaffold.</p>
</PanelShell>
)
}BasePanel remains available for compatibility, but it is now legacy authoring surface for new
plugins.
Workflow-Safe Plugin Design
Qlop is workflow-oriented. Plugin design should reinforce that.
A good plugin panel:
- makes an approved workflow easier to complete
- keeps system-specific knowledge inside the plugin
- keeps workflow identity and authority in the host/catalogue layer
- allows operators to replace or update the workflow target without rewriting the panel contract
A weak plugin panel:
- hardcodes org workflow meaning entirely inside the plugin
- assumes it is the sole source of truth for where users should go next
- bypasses host-managed metadata, auth, or policy
In practice, this means the plugin should usually answer:
- how to present or fetch workflow-specific data
- how to talk to one external system
It should not try to answer:
- what the org's canonical workflow is
- whether the user is allowed to run that workflow at all
- how the host should index or rank that workflow globally
Plugin-Owned Runtime Resources
In the current model, runtime resource ownership is panel-scoped.
That means:
- panel windows are plugin-owned runtime surfaces
- plugin-specific backend activity is part of plugin runtime ownership
- any future process tracking should apply to plugin/panel-owned runtime work only
General catalogue actions such as opening a file, opening a folder, opening a URL, or copying text do not become plugin-owned runtime resources just because a workflow item triggered them.
Workflow items may also declare a host-managed request_auth action before later sensitive actions
such as run_exe or run_command.
Rules:
request_authis workflow-chain scoped, not plugin-panel scopedrequest_auth: { strategy: "local" }keeps later sensitive actions on the current OS userrequest_auth: { strategy: "ad" }requests a chain-local Active Directory credential for later sensitive actionsrequest_authdoes not change the host's global active auth session for panels
AD-Backed Panel Responsibilities
For AD-backed panels specifically:
- declare
auth_strategy: "ad"inmanifest.json - assume the host may block panel render until auth is complete
- keep LDAP query logic in the plugin backend, not in the frontend panel
Do not:
- store passwords in plugin state
- build a separate login form for the same auth profile
- assume direct access to raw saved credentials
Compatibility Discipline
Plugin authors should treat compatibility fields as real contracts:
- bump
versionwhen plugin behavior changes - keep
panel_idstable unless intentionally replacing a panel - update
min_qlop_versiononly when the plugin truly requires newer host behavior - update
plugin_sdk_versiononly when the plugin is rebuilt against a newer SDK contract
Do not rely on accidental compatibility with older hosts.
Failure Expectations
A plugin must be safe to reject.
The host may refuse to load or render a plugin because of:
- invalid manifest shape
- missing frontend bundle
- incompatible host version
- incompatible SDK version
- missing auth configuration
- missing requested panel export
Plugin code should be written so these failures are diagnosable and contained. The rest of Qlop must remain stable if one plugin fails.
Example
See:
