60 KiB
DBX plugin platform
DBX plugins are optional, versioned .dbxp packages. They can add native backend behavior, sandboxed workbench UI, saved connection types, and filesystem providers without increasing the base DBX installation size. Declarative extension metadata consumed at build time lives here as well.
New plugin developers can read the official Chinese documentation or English documentation. The source files are also available as Chinese MDX and English MDX; the lower-level Chinese CLI walkthrough remains available in GETTING_STARTED.zh-CN.md.
Install the precompiled development CLI without cloning or compiling DBX:
npm install --global @dbx-app/plugin-cli
Marketplace listing pull requests go to t8y2/dbx-store. Plugin host, SDK, CLI, schema, documentation, and official-example changes go to t8y2/dbx. Ordinary plugin source stays in the plugin author's own repository.
The platform contract is manifest v1 + Host API 1.x + sidecar protocol v1. The source code for a plugin may live in this repository or in a separate repository; DBX installs only the built package.
Repository layout
manifest.schema.json— editor/CI schema for manifest v1.marketplace.schema.json— editor/CI schema for marketplace catalog v1.sdk/rust/dbx-plugin-sdk— Rust sidecar SDK.sdk/go/dbx-plugin-sdk— Go sidecar SDK.sdk/cli— Rust source for thedbx-plugin create/packageCLI published as@dbx-app/plugin-cli.sdk/packager— deterministic.dbxppackager plus repository-side Ed25519 signing.sdk/templates/github/plugin-release.yml— caller template for multi-platform unsigned release candidates.examples/hello-workbench— complete connection provider + native sidecar + sandboxed workbench example.RELEASING.md— source ownership, release assets, official-store submission, and review workflow.SIGNING.md— repository-signing trust model, key rotation, and future author-attestation boundary.connection-types/— build-time registry for every DBX connection target, including databases, data services, message queues, and service registries.dialects/— SQL dialect descriptors, type catalogs, DDL templates, and metadata rules.mappings/— cross-dialect type mapping rules.jdbc/— legacy JDBC sidecar retained during migration to manifest v1; connects through vendor JDBC drivers and custom JDBC URLs.
Create a frontend-only universal plugin or a complete Rust/Go plugin project:
dbx-plugin create my-ui-plugin --template frontend
dbx-plugin create my-rust-plugin --template rust
dbx-plugin create my-go-plugin --template go
All generated projects include sandbox workbench UI, localized manifest metadata, .dbxp packaging configuration, and a GitHub Release workflow. Rust and Go templates additionally include a native sidecar and multi-platform build matrix; frontend-only projects publish one universal package.
Source, package, and installation
These are separate artifacts:
- Source repository — plugin authors build and test here or in another repository.
.dbxpartifacts — plugin CI publishes unsigned review candidates; the repository operator signs approved candidates and publishes the installable assets.- Installed plugin — DBX verifies and extracts the package under its plugin store.
Optional plugins are not bundled into the DBX base package. Installing SSH, SFTP, OpenDAL, or another future plugin increases only the local plugin store size.
Marketplace and repository model
The Plugin Center separates three concerns:
- Marketplace aggregates enabled repositories, searches and filters catalog metadata, selects the current platform artifact, and installs or updates it.
- Installed manages the local plugin lifecycle and opens connection providers, workbenches, and filesystem providers.
- Settings manages local
.dbxpinstallation, custom repositories, development-only unsigned packages, and advanced repository trust.
Repository configuration is persisted under the plugin store in .repositories.json. DBX always exposes one managed dbx-official repository backed by the official catalog:
https://raw.githubusercontent.com/t8y2/dbx-store/main/catalog/index.json
The official URL is part of the DBX client contract and cannot be edited in Plugin Center settings. Official repository signing public keys are built into DBX; release builds may append rotation keys with DBX_PLUGIN_MARKETPLACE_TRUSTED_KEYS_JSON. Custom repositories use user-managed repository keys. The official repository accepts only DBX-managed keys, so adding a custom key cannot impersonate an official package.
Catalog v1 is defined by marketplace.schema.json and includes repository metadata, localized plugin metadata, searchable tags, permissions, versions, release notes, and target artifacts. Each artifact declares signingKeyId. DBX first selects the exact current target and then falls back to universal. Artifact URLs may be relative to the catalog URL. DBX enforces a 4 MiB catalog limit and a 512 MiB package limit, resolves only HTTP(S) URLs, limits redirects, isolates repository failures, verifies the catalog-declared size and SHA-256, then requires the package Manifest ID, version, publisher, permissions, and Ed25519 key ID to match the reviewed catalog before activation.
Run the live official-store smoke test with:
cargo run -p dbx-core --no-default-features --example plugin_marketplace_smoke
cargo run -p dbx-core --no-default-features --example plugin_marketplace_smoke -- <plugin-id> [version]
Without a plugin ID the smoke test verifies catalog availability and parsing. With a listed plugin ID it additionally downloads, verifies, installs, and reports the trusted signing key.
Review and signatures have different jobs
- Human review decides whether a plugin is allowed into a curated catalog and whether
verifiedmay be shown. - Catalog SHA-256 proves that the downloaded release asset is the exact artifact selected by the reviewed catalog.
- Ed25519 repository signing proves that the installed package is exactly the artifact approved and published by that repository.
- Runtime permissions and host boundaries limit what the plugin UI can request after installation.
Review alone cannot protect an already approved download URL from later replacement. A signature alone does not mean the plugin is safe or reviewed; it only authenticates the signer and bytes.
Recommended repository ownership
Keep the host, SDK, schemas, packager, and minimal examples in t8y2/dbx. The official catalog and review metadata live in the separate t8y2/dbx-store repository:
t8y2/dbx-store
├── plugins/ # reviewed metadata submissions
├── publishers/ # publisher identity and review records
├── revoked.json # revoked versions or signing keys
├── catalog/index.json # generated catalog v1
└── .github/workflows/ # schema, review, hash, and publish gates
Plugin source code may remain in independent author repositories. Author CI builds one unsigned .dbxp candidate per target and publishes it to GitHub Releases, a CDN, or object storage. The reusable release workflow emits release-candidates.json with Manifest identity plus verified candidate metadata. After review, the repository signing workflow signs the accepted bytes and publishes final artifacts and metadata; the catalog Git repository should not accumulate binary packages.
The same protocol supports future commercial deployment without changing package format: a public official repository, user-added third-party repositories, managed enterprise repositories with organization policy, and fully offline catalog/package mirrors. Credentials for private repositories must enter a secret-store-backed request layer rather than .repositories.json.
Package layout
example-1.0.0-darwin-arm64.dbxp
├── manifest.json
├── checksums.json
├── signature.json # required except explicit development installs
├── bin/
│ └── darwin-arm64/
│ └── example-plugin
├── assets/
│ ├── plugin.svg
│ └── connection.svg
└── ui/
├── index.html
└── assets/...
.dbxp is a ZIP container with stricter rules:
- every file except
checksums.jsonandsignature.jsonmust be covered exactly once by SHA-256 checksums; - absolute paths, parent traversal, duplicate entries, symlinks, oversized entries, and decompression bombs are rejected;
- installed versions are immutable;
- activation records select the current version and support rollback;
- install, rollback, uninstall, and trust-store mutation share a filesystem lock;
- marketplace and normal local installs require a signature from a trusted Ed25519 repository key;
- unsigned packages require the explicit development-install toggle.
Manifest v1
Add the schema to a manifest for editor validation:
{
"$schema": "../../manifest.schema.json",
"manifest_version": 1,
"id": "vendor.example",
"name": "Example",
"icon": "assets/plugin.svg",
"version": "1.0.0",
"source": "https://github.com/example/dbx-plugin",
"homepage": "https://example.com/dbx-plugin",
"engines": {
"dbx": ">=0.5.68",
"host_api": "^1.0"
}
}
The runtime also validates semantic versions, engine ranges, IDs, contribution references, field types and bindings, entrypoint containment, current-platform binaries, and required backend/UI entrypoints. The JSON schema improves authoring but is not the security boundary.
Entrypoints
{
"entrypoints": {
"backend": {
"executable": "bin/darwin-arm64/example"
},
"ui": {
"root": "ui",
"entry": "ui/index.html"
}
}
}
Use stdio-jsonl for ordinary request/event traffic. Use stdio-framed when PTY, SFTP, file transfer, or another feature needs binary channels. The workbench bridge transfers plugin-UI binary payloads as transferable ArrayBuffers in both directions (8 MiB per message; chunk larger transfers), so UI-side binary traffic no longer pays a base64 round trip.
Contributions
connection-provider
A provider owns validation and connect/disconnect lifecycle for a saved non-SQL connection. DBX stores these as:
ConnectionConfig
├── db_type: "plugin"
├── plugin_id
├── plugin_connection_provider
├── plugin_connection_type
├── common fields: name / host / port / username / password / database
├── external_config: provider-defined non-secret values
├── connection_secrets: provider-defined secrets
└── transport_layers: SSH jump / SOCKS / HTTP proxy
The provider-defined type, such as ssh, stays in plugin_connection_type; it does not extend DBX's exhaustive database enum.
Plugin authors may declare package-relative display metadata:
{
"name": "Example plugin",
"icon": "assets/plugin.svg",
"contributions": [
{
"type": "connection-provider",
"id": "vendor.example.connection",
"label": "Example connection",
"icon": "assets/connection.svg"
}
]
}
DBX resolves connection display metadata in this order: provider label / icon, plugin name / icon, then provider ID and the built-in generic plugin icon. Declared icon files must remain inside the plugin package and use SVG, PNG, JPEG, GIF, WebP, or ICO. SVG is rendered through an image URL rather than injected into the DBX document.
Field bindings:
| Binding | Storage |
|---|---|
name, host, port, username, password, database |
matching common ConnectionConfig field |
config |
external_config[field.key] |
secret |
connection_secrets[field.key], persisted outside config_json |
Password fields default to secret when binding is omitted. DBX validates required values and value types before calling the plugin. The plugin receives the hydrated connection only in its backend lifecycle request; the workbench UI receives a connection ID and non-secret navigation context.
Absent optional fields stay absent: when DBX hands the manifest to its own UI it omits description, placeholder, default, and binding for fields that do not declare them, and "default": null means "no default" exactly like omitting the key. Treat a missing value as unset — never as an empty string, and never as the literal text null, which is not a storable plugin value.
Well-known field keys: a config-bound field keyed connect_timeout_secs declares the plugin's own connect/handshake timeout and is the single source of truth for it. On save, DBX mirrors its resolved value (the declared default, or the value a user entered in the connection form) into the typed ConnectionConfig.connect_timeout_secs — the dialog's generic global/per-connection timeout radios do not apply to providers declaring this field. The host's connection/test and connection/connect RPC deadline follows the same resolved value (stored external_config first, then the declared default), so the deadline never fires before the plugin's own timeout; providers that do not declare the field keep the generic typed-timeout behavior. Declare it when your transport needs more than the generic built-in 10s default (e.g. SSH handshakes on slow links).
Local file fields
A text, password, or textarea field may declare picker when the user should choose a local file (private keys, keystores, credential files):
{
"key": "private_key_path",
"label": "Private key path",
"type": "text",
"binding": "config",
"picker": { "kind": "file", "accept": [".pem", ".key", ".ppk"], "content_field": "private_key" }
}
- Desktop hosts open a native picker and store the chosen absolute path in the declaring field. The plugin backend runs on the same machine, so it can read the file itself.
- Browser hosts cannot resolve a path on the user's machine, so the same action becomes an upload: DBX reads the selected file and stores its content in
content_field(which must be a declaredtext/password/textareasibling), and clears the declaring field. A picker withoutcontent_fieldis therefore desktop-only and stays hidden in the browser. - Switching source clears the other one: choosing a path removes the uploaded content and vice versa. This matters for fields that are alternatives — a plugin that prefers
private_keycontent overprivate_key_pathmust not keep serving a stale upload after the user re-picked a path. kindisfile(default use case) ordirectory(desktop-only: a browser cannot hand a folder to the plugin).acceptlists up to 16 filters as extensions (.pem) or MIME types (text/plain) and is passed to the native dialog and the browser file input unchanged. Uploads are capped at 1 MiB.
picker is additive; hosts older than the release that ships it reject the manifest, so keep engines.dbx at or above that release when the form relies on it.
Conditional fields
A field may declare visible_when and required_when. A leaf clause matches when the referenced sibling field holds a non-empty value listed in one_of; listed values may be strings, numbers, or booleans and are compared by canonical string form, so false and "false" both match a boolean false. Clauses compose with all_of, any_of, and not:
{
"key": "sudo_command",
"label": "Sudo command",
"type": "text",
"visible_when": {
"all_of": [
{ "field": "sudo_source", "one_of": ["custom"] },
{ "field": "read_only", "one_of": [false] }
]
},
"required_when": {
"all_of": [
{ "field": "sudo_source", "one_of": ["custom"] },
{ "not": { "field": "read_only", "one_of": [true] } }
]
}
}
all_of/any_ofmust contain at least one nested condition, nesting is limited to 8 levels and 64 nodes, and every referenced field must be a sibling declared by the same provider.- Conditions cascade: while the field a clause reads is itself hidden, the clause does not count. A hidden container's stored default therefore cannot surface a grandchild field, and a hidden operand of
notkeeps the field dormant instead of lighting it up. - DBX evaluates the same conditions for the dialog and for save/test/connect validation, so a manifest can never produce a form DBX itself rejects.
- Composite conditions were added after the single-clause contract; keep
engines.dbxat or above the DBX release that ships them if the form relies on them.
Lifecycle methods receive:
{
"provider": { "id": "vendor.example.connection", "databaseType": "example" },
"connection": { "id": "...", "db_type": "plugin", "...": "..." },
"runtime": { "host": "127.0.0.1", "port": 49152 }
}
runtime.host and runtime.port are the final endpoint after DBX transport layers. A protocol plugin must connect to this endpoint instead of rebuilding DBX tunnels itself.
Transport proxy route for multi-endpoint targets
A static tunnel forwards exactly one remote endpoint. Protocols whose server advertises additional endpoints a client must dial (Kafka advertised.listeners, cluster discovery, etc.) cannot be served that way: the bootstrap endpoint connects, but every advertised broker is unreachable. Such providers declare proxy_route on the connection-provider contribution:
{
"type": "connection-provider",
"id": "vendor.kafka.connection",
"database_type": "kafka",
"proxy_route": true
}
When transport layers are configured, DBX then delivers a SOCKS5 route instead of a static forward:
{
"provider": { "...": "..." },
"connection": { "...": "..." },
"runtime": {
"host": "",
"port": 0,
"proxy": { "type": "socks5", "host": "127.0.0.1", "port": 49153, "username": "", "password": "" }
}
}
- With SSH as the final transport layer the route is the hop's dynamic SOCKS5 endpoint (
ssh -D); with a SOCKS5 proxy layer the route is that proxy, tunneled through any preceding layers.username/passwordare omitted when empty. runtime.host/runtime.portstay at the connection's logical endpoint, which the plugin should keep using as its seed/metadata source while dialing every endpoint through the SOCKS5 route. Credentials ride the same encrypted lifecycle channel as connection secrets and must never be logged by the plugin.- Without the flag, transport layers keep the static-tunnel behavior, which requires the connection to resolve a single remote endpoint (providers should declare
host/portbindings, as the SSH and LDAP plugins do); DBX rejects plugin connections that would tunnel to an empty endpoint instead of timing out silently.
Connection dialog actions
Connection providers may add ordered custom actions before DBX-owned lifecycle buttons:
{
"actions": [
{
"id": "discover",
"label": "Discover endpoint",
"variant": "outline",
"requires_valid_form": false
}
]
}
actionsdeclares only custom button metadata. DBX invokesconnection/actionwith the action ID; plugins cannot choose arbitrary RPC method names.test,save, andsave-and-connectare host-owned actions. DBX adds them from provider capabilities and dialog mode, validates the form, persists secrets through its secret store, and invokes the fixed lifecycle methods.- A custom action may run with an incomplete form only when
requires_valid_formisfalse; DBX still validates declared field types, secret keys, and transport configuration. whenacceptsalways,create, oredit.variantacceptsdefault,outline,secondary,destructive, orghost.timeout_msis limited to 1-120000 ms.close_on_successcontrols whether the connection dialog closes after a successful custom action.
connection/action receives the normal provider lifecycle payload plus action: { id }. It may return a message and updates for declared fields:
{
"success": true,
"message": "Endpoint discovered",
"fieldValues": {
"host": "db.internal",
"port": 5432
}
}
fieldValues may contain only fields declared by that provider and must match their declared types. null clears a field. The plugin cannot write arbitrary ConnectionConfig keys or bypass DBX-owned save, secret persistence, transport, and connection lifecycle logic.
workbench
A workbench opens in a normal persistent DBX tab. The iframe is loaded with sandbox="allow-scripts", a restrictive CSP, no Tauri object, no parent DOM access, and no direct network access. The host injects window.dbxPlugin:
ready/context/locale—localeis the current DBX locale such asenorzh-CNtheme—{ appearance: "light" | "dark", tokens }with the resolved DBX design tokens; theme changes are pushed live through env updates, and the SDK applies them to the plugin document rootonContext(listener)— context changes are pushed live; the iframe is not reloaded, so plugin UI state survives navigationinvoke(method, params, options)notify(method, params)sendBinary(channel, data)— requireshost.binaryreadAsset(path)/readAssetUrl(path)openWorkbench(contributionId, context)— requireshost.workbenchexecuteCommand(commandId, context?)— executes one of this plugin's own declared commands through the same path a menu placement takes:enablementis re-checked,presentation: "panel"commands dock in the bottom panel andpresentation: "tab"(default) commands open a workbench tab, with §4.1 singleton reuse.contextmerges over the command-declared context (reserved identity fields stay host-owned) and scopesinstance_key{{path}}placeholders — declareinstance_key: "logs:{{connectionId}}"to get one panel instance per connection; a template that cannot resolve falls back to its literal. Resolves{ error }for expected outcomes (unknown command, enablement-blocked); requireshost.workbenchopenFilesystem(providerId, context)— requireshost.filesystemgetPlanCapabilities(connectionId)/explainPlan(request)— reads an estimated execution plan for one connection; requireshost.plans:read, see Estimated execution plansgetTableMetadata({ connectionId, database?, schema?, table })— reads narrow schema metadata for one table on an already-open connection; requireshost.schema:read, see Table Schema MetadataqueryData({ connectionId, database?, schema?, sql, maxRows?, timeoutMs? })— runs one read-only SQL statement on a connection the user granted to the plugin; requireshost.data:read, see Read-only data queriesstorage—storage.get(key)/storage.set(key, value)/storage.delete(key)persist small JSON state per plugin inplugin-data/<id>; requireshost.storage; values cap at 256 KiB and the whole store at 1 MiB, bulk data belongs in the sidecar'sDBX_PLUGIN_DATA_DIRai.openConversation({ title, prompt, context, send?, mode? })— opens a plugin conversation in the built-in DBX AI panel; requireshost.ai;modedefaults toaskfor snapshot compatibility, whilemode: "agent"uses live tools on the context's open plugin connection when available.titlecaps at 200 characters,promptat 32000, andcontextat 2 MiB, andsenddefaults tofalseso passtrueto start the analysis immediately. The host copies the snapshot into the conversation as history data and never hands model output or model configuration back to the plugin through this conversation APIai.listProviders()/ai.discoverModels(configId)— requirehost.aiandcapabilities.aiModelDiscovery. List API providers including those without configured models, and fetch models using credentials retained by the host. Manual model IDs are accepted by text generation without changing global defaults.ai.listModels()— requireshost.aiandcapabilities.aiCompletion. Returns only{ configId, name, model, isDefault }for configured API models. No endpoints, keys, headers, or CLI environment are exposed. CLI agents are excluded from text completion.ai.generateText({ configId, model, prompt })— requireshost.aiandcapabilities.aiCompletion; prompts are nonempty and limited to 100000 characters. The desktop host asks for confirmation naming the plugin and selected model before each send, resolves credentials itself, and returns plain text (up to 16000 characters). No tools or file writes are enabled. One generation per workbench may run at a time. Provider failures return a generic error to avoid leaking secrets. Old hosts may omit these methods/capability.ai.setRecommendations({ context, items })/ai.clearRecommendations()— updates the context-aware quick-question chips shown by the global DBX AI panel; requireshost.ai. Each item is{ id, label, prompt, order? }, supports{{path.to.value}}placeholders resolved against the supplied context, and is capped at five visible items. Recommendations belong to the current workbench instance and are not persisted into conversation history. Useai.openConversation()when a user clicks a recommendation; DBX sends the selected prompt through the same snapshot safety boundary.capabilities—{ downloadFile, planApi, schemaMetadataApi, dataApi, storage, ai, aiModelDiscovery, aiCompletion, aiRecommendations }advertised in the init message; a missing orfalseentry means that Host API group is unavailable on this host, so gate the matching call on it instead of probing with a requestonEvent(listener)— events are forwarded only withhost.eventsonBinary(listener)— binary frames are forwarded only withhost.binary; listeners receive{ channel, data: Uint8Array }
The sandbox default is no network access at all. A plugin may declare per-origin permissions such as host.network:https://api.vendor.com (https only, no path, at most 8 origins, duplicates rejected); declared origins — and only those — are added to the sandbox connect-src, so reviewers can see exactly which services the plugin UI may call.
All backend calls are rebound to the owning plugin ID by the host. A plugin UI cannot invoke another plugin.
Official UI kit and theming
Every sandbox document ships with a small official component kit built on the DBX design tokens, so plugin UI follows light/dark mode and custom palettes automatically:
<button class="dbx-btn dbx-btn--primary">Connect</button>
<input class="dbx-input" placeholder="Endpoint" />
<span class="dbx-badge">Ready</span>
Available classes: dbx-card, dbx-section-title, dbx-btn (--primary / --danger / --ghost), dbx-label, dbx-input, dbx-select, dbx-textarea, dbx-hint, dbx-row (label + field grid), dbx-table, dbx-badge, dbx-link. Custom plugin CSS can use the same var(--color-*) tokens; document.documentElement.dataset.dbxTheme reflects the current appearance.
Plugin-authored names, descriptions, contribution labels, form-field text, and select-option labels can be localized through manifest.json > localizations. DBX selects the exact current locale first, then its base language, and finally falls back to the manifest's default text:
{
"localizations": {
"zh-CN": {
"name": "示例插件",
"contributions": {
"vendor.example.connection": {
"label": "示例连接",
"fields": {
"host": { "label": "主机", "placeholder": "请输入主机" },
"mode": { "options": { "readonly": "只读" } }
}
}
}
}
}
}
result-view
A result view contributes a plugin-rendered visualization for query results. DBX shows one toolbar button per installed view next to the result grid; clicking it opens a plugin tab that renders the plugin's UI entrypoint with the current result as context:
{
"type": "result-view",
"id": "vendor.example.graph",
"label": "Graph"
}
A result view declares display metadata only: it carries no UI of its own and never names a workbench. The opened contribution id reaches the plugin UI in the init payload (dbx-plugin-init detail contributionId), so a plugin that declares several result views selects the matching one inside its single UI entrypoint.
The context.result snapshot is bounded — { columns, rows (<= 500), truncated } plus sql, connectionId, and database. Plugins that need more rows can re-run a read-only statement with queryData (requires host.data:read and the user's consent for that connection). Requires a UI entrypoint.
context-menu
A context-menu entry is rendered natively by DBX (no sandbox iframe, native theme and keyboard behavior) in the declared menu surface. v1 supports the saved-connection and table menus in the Sidebar Tree. Object Browser integration is not part of the first table contribution surface.
{
"type": "context-menu",
"id": "vendor.example.inspect",
"label": "Inspect endpoint",
"menu": "connection"
}
For a table-scoped action, declare menu: "table":
{
"type": "context-menu",
"id": "example.inspect-table",
"label": "Inspect table",
"menu": "table"
}
A context-menu item can instead declare a host-handled Workbench action:
{
"type": "context-menu",
"id": "vendor.example.generate",
"label": "Generate test data",
"menu": "table",
"action": {
"type": "open-workbench",
"workbench": "vendor.example.main"
}
}
The workbench reference must identify a workbench contribution in the same plugin manifest. This declarative action is resolved by the host and does not invoke the plugin backend.
To provide state-dependent items and one level of native submenus, set "dynamic": true on a context-menu contribution. This requires a backend entrypoint. On each right-click, DBX calls contextMenu/resolve/<id> with the same non-secret { connection } or { table } envelope used by legacy actions, plus the current locale; connection menus also include ownerPluginId when the connection belongs to a plugin. A plugin can return an empty menu for connections it does not own. Return { "items": [...] }; returning an empty array hides the contribution. For example:
{
"items": [
{
"label": "Tunnels",
"children": [
{ "label": "Manage port forwards", "action": { "type": "open-workbench", "workbench": "ssh.tunnels", "presentation": "dialog" } },
{ "label": "Start saved tunnels", "action": { "type": "invoke", "id": "start-all", "reopenConnectionOnMissing": true } }
]
}
]
}
Each item accepts label, optional visible, enabled, checked, and either action or children. Children may not contain another submenu. An invoke action calls the declared contribution's existing contextMenu/<id> backend method with the original envelope plus itemId; the host never executes an arbitrary method supplied by the resolver. For connection items, reopenConnectionOnMissing: true retries once after DBX restores that plugin connection when the backend reports Connection is not active; omit it for actions that do not need an open connection. An open-workbench action must target a workbench declared by the same plugin. Dynamic menu items may add "presentation": "dialog" to that action to show the workbench in a modal over DBX; without it the workbench opens in a tab. The host limits response size and resolution time; invalid or failed responses contribute no items. Existing static context-menu contributions continue to work unchanged.
For legacy entries without action, clicking a connection item dispatches contextMenu/<id> with the existing non-secret connection summary ({ id, dbType, name, database }) under connection. Clicking a table item uses the same backend method and dispatches:
{
"table": {
"connectionId": "connection-id",
"database": "example",
"schema": "public",
"table": "users"
}
}
database and schema are optional and are omitted when the selected database does not expose those scopes. The table context contains object identity only; it never contains credentials, connection strings, or raw connection configuration.
For a declarative open-workbench action, DBX passes the current connection summary as Workbench context for menu: "connection", and the stable TableContext object above directly as Workbench context for menu: "table" (without the backend table envelope). Connection context includes only id, dbType, name, and database; the host may also provide the standard connectionId for tab association. Neither path includes host, port, username, password, a connection string, or raw connection configuration. Reopening the same Workbench refreshes it with the latest invocation context.
Declarative launch options (options_action)
A dock command contribution's open-workbench action may declare options_action: "<sidecar method>" (context-menu entries cannot: their action form has no such field, and unknown fields are rejected at manifest validation). Before offering launch targets, the host invokes that method with the current UI locale so labels arrive localized:
{ "locale": "zh-CN" }
The sidecar answers:
{
"entries": [
{ "label": "Local terminal", "description": "Default shell: /bin/zsh", "context": {} },
{ "label": "zsh", "description": "/bin/zsh", "context": { "shell": "zsh" } }
]
}
contextis opaque plugin payload: it is merged into the host-authored Workbench context of the panel opened for that entry, and the host never interprets it.- A command declaring
options_actionowns the picker: the host's generic replay item hides whileoptions_actionis declared and does not come back if the sidecar fetch fails or returns nothing — the picker then simply has no launch entries. - Plugins ignore the
localefield safely if they do not localize.
A backend entrypoint is required only for legacy context-menu entries without a declarative action. Their { "message": "..." } result continues to surface as a toast.
filesystem-provider
A filesystem provider declares URI schemes, an optional icon, root_uri, and capabilities (read, write, delete, rename, mkdir). It is the reusable boundary for OpenDAL-like storage integrations: DBX owns the generic file-browser tab, while the plugin owns authentication, remote API calls, and provider-specific state. The provider icon is used for the saved connection in the sidebar and for its DBX tab; it falls back to the plugin-level icon when omitted.
A connection provider can set filesystem_provider instead of workbench. Opening that saved connection connects the plugin lifecycle and opens the DBX host file manager. A provider may declare both: DBX opens the custom workbench by default, and the sandboxed UI can call openFilesystem(providerId, context) with host.filesystem permission.
{
"type": "filesystem-provider",
"id": "vendor.storage.files",
"label": "Object storage",
"icon": "assets/filesystem.svg",
"schemes": ["s3"],
"root_uri": "s3://bucket/",
"capabilities": ["read"]
}
Host API 1.x defines these backend methods:
filesystem/listreceivesproviderId, optionalconnectionId,uri, optional paginationcursor, and a boundedlimit. It returns{ entries, nextCursor? }.filesystem/readreceivesproviderId, optionalconnectionId,uri, and boundedmaxBytes. It returns{ dataBase64, contentType?, truncated, etag? }for preview-sized reads.filesystem/writereceivesproviderId, optionalconnectionId,uri,dataBase64,create,overwrite, and optional optimistic-concurrencyetag.filesystem/createDirectoryreceivesproviderId, optionalconnectionId, anduri.filesystem/deletereceivesproviderId, optionalconnectionId,uri, andrecursive.filesystem/renamereceivesproviderId, optionalconnectionId,sourceUri,targetUri, andoverwrite.
Every entry has name, canonical uri, kind (file, directory, symlink, or other), and optional size, modifiedAt, and contentType. DBX validates schemes, response sizes, base64, cursors, and entry metadata before the frontend sees a result.
Mutation methods return { success, message?, entry? } and are rejected unless the provider declares the matching capability. Inline read/write payloads are capped at 4 MiB. The built-in file manager currently owns directory navigation, pagination, and bounded file preview. Large upload/download and PTY/SFTP streams use stdio-framed binary channels with plugin-defined transfer methods, chunk acknowledgements, cancellation, and progress events; they must not be encoded as one large JSON value.
mcp
A backend that implements the optional MCP tool bridge (mcp/tools + mcp/call, see "Tools for the built-in AI assistant" below) does not put its tools in front of anyone by itself: the built-in AI agent only sees a plugin after the user enabled it in the Plugin Center, and the external dbx MCP server (dbx-mcp) only sees plugins that opt in with external_tools: true. This optional contribution declares and tunes those surfaces:
{
"type": "mcp",
"id": "vendor.ssh.mcp",
"description": "Terminal and file tools over SSH connections.",
"external_tools": true
}
Only id is required; ai_tools defaults to true (set it to false to keep the tools off the AI surface even when the user opts in) and external_tools defaults to false (set it to true to expose the tools on the external server). At most one mcp contribution is allowed per manifest, and it requires a backend entrypoint. Declaring it makes the manifest unreadable to hosts that predate the contribution type, so plugins that must install on older hosts should stay undeclared. The runtime gates stay in force on every call regardless: open connections for the AI surface, the global MCP policy plus per-connection allowlist for the external surface, per-call approval for anything not annotations.readOnlyHint: true, and the Plugin Center switch (which always wins) to turn a plugin's AI tools off entirely.
Table Schema Metadata
A plugin can read narrow, read-only schema metadata for one table without owning a driver, connection pool, credential, or SQL string. It reuses the canonical PluginTableContext identity used by table contributions:
{
"permissions": ["host.schema:read"]
}
if (window.dbxPlugin.capabilities.schemaMetadataApi) {
const metadata = await window.dbxPlugin.getTableMetadata({
connectionId,
database,
schema,
table: "users"
});
renderColumns(metadata.columns);
}
The result is deliberately narrower than DBX's internal ColumnInfo:
{
"columns": [
{
"name": "id",
"dataType": "integer",
"nullable": false,
"precision": 32,
"default": "nextval('users_id_seq'::regclass)"
}
],
"fieldCapabilities": {
"length": "supported",
"precision": "supported",
"scale": "supported",
"default": "supported"
}
}
columns exposes only name, dataType, nullable, and optional length, precision, scale, and default. Comments, keys/indexes, credentials, connection strings, driver objects, and arbitrary SQL results never cross the boundary. Optional values remain omitted or null; the host never turns missing metadata into 0 or an empty string. fieldCapabilities reports supported, unsupported, or unknown for each structured optional field. unknown means DBX lacks reliable provider provenance and must not be treated as support.
database and schema are optional and omitted when unavailable. connectionId and table are non-empty, trimmed identity values capped at 256 characters. The host must already hold the matching connection/session: a saved-but-disconnected connection is rejected with Connection is not open, and a database without an open matching session is rejected instead of creating a pool or switching connections. The permission grants no arbitrary SQL and no write access.
This is Host API 1.3. A plugin that requires it declares:
{
"engines": { "host_api": "^1.3" }
}
The schemaMetadataApi capability is runtime detection for older hosts; a plugin should gate the call on it rather than probing the request. Without host.schema:read, the bridge rejects the call before the backend adapter runs.
Read-only data queries
A plugin can run one read-only SQL statement on a DBX connection the user granted to it. It never receives a driver, pool, credential, or connection string:
{
"engines": { "host_api": "^1.4" },
"permissions": ["host.data:read"]
}
if (window.dbxPlugin.capabilities.dataApi) {
const { columns, rows, truncated } = await window.dbxPlugin.queryData({
connectionId,
database,
sql: "SELECT status, count(*) AS total FROM orders GROUP BY status",
maxRows: 200
});
}
The result is { dbType, columns: [{ name, dataType? }], rows, truncated, elapsedMs }. The host owns every decision:
- Consent per (plugin, connection). The first query for a connection shows a host dialog naming both. An allow is persisted in
app_settings.plugin_data_grantsand listed under the plugin in Plugin Center → Installed, where it can be revoked; a denial is remembered for the workbench session. Uninstalling a plugin drops its grants. A revoked grant fails withPLUGIN_DATA_ACCESS_NOT_GRANTED: …and the next query asks again. - One read-only statement. The statement must be the only one in the request and must be rated read-only by the shared SQL risk classifier used for MCP read-only access and the AI agent. Writes, DDL, locking reads, and session database switches (
USE …) are rejected. - Open SQL connections only. A saved-but-closed connection is rejected with
Connection is not open; non-SQL connections are not served. - Bounds.
maxRowsdefaults to 500 (max 5000), serialized rows are capped at 8 MiB, andtimeoutMsis clamped to the connection timeout and 60 s.
The backend re-checks the manifest permission and the grant on every call (crates/dbx-core/src/query/plugin_data.rs); the bridge only adds the consent prompt and a per-session cache. Hosts without a consent surface deny instead of granting.
Estimated execution plans
A plugin can read the estimated execution plan of a query without touching a database driver, a credential, or a connection string. The host builds the EXPLAIN statement with its own build_explain_sql, applies the same read-only safety gate DBX uses for its own plan view, and executes it on the connection the plugin names. The plugin receives only the raw plan.
{
"permissions": ["host.plans:read"]
}
| Request | Purpose |
|---|---|
host.getPlanCapabilities({ connectionId }) |
Reports what the host and this connection can plan. It only reads the stored connection config; it never connects or probes the server. The connection must already be open. |
host.explainPlan({ connectionId, database?, schema?, sql, mode, timeoutMs? }) |
Returns the estimated plan for sql. The connection must already be open. |
host.getPlanCapabilities returns:
{
"dbType": "postgres",
"dbVersion": "15.19",
"supports": { "estimatedPlan": true },
"limits": { "maxTimeoutMs": 60000, "maxPlanBytes": 4194304 }
}
supports.estimatedPlanisfalsewhen this connection's dialect has no estimated plan path in DBX; disable the feature instead of callingexplainPlan.limitsare the host's ceilings.maxTimeoutMsalready accounts for the connection's own query timeout, and a requestedtimeoutMsis clamped to it. A connection configured with no query timeout still gets the host ceiling.dbVersionis present only when DBX already learned the product version for this connection. The host never probes the server on the plugin's behalf.
host.explainPlan returns:
{
"dbType": "postgres",
"dbVersion": "15.19",
"format": "json",
"rawPlan": [{ "Plan": { "Node Type": "Seq Scan" } }],
"truncated": false,
"warnings": []
}
formatisjson,xml(SQL ServerShowPlanXML), ortext.rawPlanis a parsed JSON document forjsonand the plan text otherwise, so the plugin can parse and normalize it itself.warningscarriesplan_not_jsonwhen the server answered with something that is not JSON (the payload is then reported astextrather than pretending it is JSON),plan_truncatedwhen the host cut the plan to respectmaxPlanBytes, andplan_rows_truncatedwhen the driver stopped collecting plan rows.truncatedistruewhenever the host cut the plan for either reason.
The sandboxed UI can call this through window.dbxPlugin directly:
if (window.dbxPlugin.capabilities.planApi) {
const capabilities = await window.dbxPlugin.getPlanCapabilities(connectionId);
if (capabilities.supports.estimatedPlan) {
const plan = await window.dbxPlugin.explainPlan({ connectionId, database, sql, mode: "estimated", timeoutMs: 15000 });
}
}
Boundaries:
- Estimated plans only.
modemust be"estimated"; any other value is rejected. Actual plans (EXPLAIN ANALYZE,SET STATISTICS XML) execute the statement and are not part of this API. - The plugin never supplies SQL to execute. Only the source
sqlis accepted and the host builds theEXPLAINstatement itself, so a plugin cannot pass anEXPLAINstatement, a driver command, or an execution mode. - Read-only targets only. The same gate DBX uses for its own plan view rejects multi-statement input, DDL, DML, and dangerous keywords. Oracle is the one dialect where DBX also plans DML, because
EXPLAIN PLAN FORdoes not execute it. - No credentials. The response carries the plan and metadata only; a password, credential, connection string, or driver internals never cross this boundary, and the plan is not a user result set.
- The connection must already be open. DBX does not connect on a plugin's behalf. A saved connection that is currently disconnected is rejected by both
host.getPlanCapabilitiesandhost.explainPlanwithConnection is not open; only a connection DBX already holds open can be planned. host.plans:readis read-only. It does not permit normal SQL execution, writes, DDL, or actual plans, and it is the only permission this API reads.- Bounded.
limits.maxPlanBytescaps the plan payload,timeoutMsis clamped by the host, and a plan that cannot be cut safely fails with an explicit error instead of returning a partial document.
Gate on capability rather than probing: read window.dbxPlugin.capabilities.planApi from the init message (an older host omits it), then confirm per-connection support with host.getPlanCapabilities before calling host.explainPlan.
The plan API is Host API 1.2, so a plugin that cannot work without it declares the floor in its manifest:
{
"engines": { "host_api": "^1.2" }
}
The manifest range is a compatibility floor and capabilities.planApi is the runtime check; keep both.
Host API methods a plugin may call
Plugins normally answer requests, but Host API 1.1 adds one method a plugin backend may call back into DBX. Plugin-initiated requests use string ids ("prompt-1"), while DBX-owned requests and their responses keep numeric ids, so one stream carries both directions and older hosts that only understand numeric ids ignore the new frames instead of failing.
host/requestUserInputasks the user a question through the DBX UI and returns the answer. It is the channel for anything the host cannot answer on the user's behalf: a bastion's keyboard-interactive MFA code, a one-time approval, a host-key confirmation, or a choice between accounts.
{
"jsonrpc": "2.0",
"id": "prompt-1",
"method": "host/requestUserInput",
"params": {
"prompt": "Verification code (6 digits)",
"title": "JumpServer login",
"echo": false,
"default": "000000",
"options": [{ "value": "jinpy", "label": "jinpy (admin)" }],
"timeoutSecs": 300
}
}
promptis required (≤2000 characters).title(≤200),default(≤1000), andoptions(≤8 entries, unique values, ≤200 characters each) are optional;echodefaults tofalse, so the dialog masks input unless the plugin says otherwise.timeoutSecsis clamped to 5-600 and defaults to 300.- The result is
{ "action": "submit", "value": "123456" },{ "action": "cancel" }, or{ "action": "timeout" }. Onlysubmitcarries a value; treatcancelandtimeoutas "no answer" and fail closed — never fall back to a guess. - The prompt is delivered through the same blocking dialog the host uses for its own host-key and keyboard-interactive prompts, so it also appears for
connection/testandconnection/connect. While a prompt is open DBX pauses the request deadline of the call that is waiting on it, so a user typing a code is never mistaken for a connect timeout. - Errors come back as JSON-RPC errors:
-32001means no user interface is attached (headless/MCP runs, or the desktop dialog is not mounted),-32602means the params are invalid,-32601means the host does not implement the method. A plugin must degrade gracefully on all three instead of blocking forever. - DBX answers only with what the user typed. It never auto-fills, caches, or logs the value, and it allows at most four open prompts per plugin session.
- Capability gating:
plugin/initializeadvertiseshost.hostApiVersion(1.1.0or later) andhost.features(containinghost.requestUserInputwhen available). Only call the method when it is advertised; an older host reports1.0.0and drops the frame. - The Rust SDK (
dbx-plugin-sdk) wraps this:dbx_plugin_sdk::host_client(),HostClient::supports("host/requestUserInput"), andHostClient::request_user_input(&UserInputPrompt::secret("Verification code")).
Tools for the built-in AI assistant
A backend can expose tools to the built-in DBX AI agent through the MCP-shaped sidecar methods mcp/tools and mcp/call — the same methods DBX's MCP bridge uses:
{"jsonrpc":"2.0","id":7,"method":"mcp/tools","params":{"connectionId":"<open connection id>"}}
{"jsonrpc":"2.0","id":8,"method":"mcp/call","params":{"tool":"orders_lag","arguments":{"group":"billing"},"lifecycle":{"provider":{},"connection":{},"runtime":{}}}}
mcp/tools returns { "tools": [{ "name", "description", "inputSchema", "annotations"? }] }; mcp/call returns an MCP CallToolResult ({ "content": [{ "type": "text", "text": "…" }], "isError": false }). lifecycle is the open connection's connection/connect payload, with secrets resolved by the host and the runtime endpoint after transport layers.
Host rules (crates/dbx-core/src/ai/plugin_tools.rs):
- Tools are opt-in: a plugin contributes AI tools only after the user enabled it in Plugin Center, and a manifest
mcpcontribution withai_tools: falsekeeps it off the surface even when opted in. The Plugin Center switch turns a plugin off for good. Tools appear only in Agent mode with API model providers, and only for plugin connections that are currently open. - The host binds the connection:
connectionId/connectionNameare removed from the model-facing schema, and the boundconnectionIdis injected into the forwarded arguments when the plugin schema declares it. With several open connections the model chooses through an addeddbx_connectionargument. - A tool runs without asking only when its entry sets
annotations.readOnlyHint: true. Every other call pauses the run behind an inline approval that shows the exact forwarded arguments; unanswered approvals are denied after five minutes (crates/dbx-core/src/ai/tool_approval.rs). The hint is trusted because the plugin's native backend is already trusted code; the approval protects against model mistakes and prompt injection, not against a hostile plugin. - Names are exposed as
<prefix>__<tool>(io.dbx.ssh→ssh__…). Schemas are reduced to a provider-portable subset (type,description,properties,required,items, stringenum, numeric/length/item bounds); argument names must match[A-Za-z_][A-Za-z0-9_]{0,63}. - Discovery times out after 8 s per connection, calls after 120 s, and results are compacted before reaching the model.
Backend protocol
The backend is a persistent child process with stdin/stdout reserved for the DBX protocol. Diagnostics must go to stderr.
Initialization
DBX starts every sidecar with plugin/initialize:
{
"host": {
"dbxVersion": "0.5.68",
"hostApiVersion": "1.0.0",
"protocolVersions": [1]
},
"plugin": {
"id": "vendor.example",
"version": "1.0.0"
},
"permissions": ["host.events"]
}
The plugin returns:
{
"protocolVersion": 1,
"capabilities": ["connections", "events"],
"plugin": {
"id": "vendor.example",
"version": "1.0.0"
}
}
The host rejects a protocol or backend identity mismatch before exposing the session. This catches a package that points at the wrong executable even when the process otherwise speaks the protocol.
JSON messages
Requests and responses follow JSON-RPC 2.0. Plugin events are JSON-RPC notifications emitted by the sidecar. The host supports concurrent in-flight requests, per-request timeouts, strict JSON-RPC validation for manifest v1, crash propagation, status reporting, bounded event buffers, and automatic child termination after handshake, protocol, or output failure. The Rust SDK dispatches work through a bounded configurable worker pool.
stdio-jsonl writes one JSON value per line. A single JSON message is limited to 8 MiB.
Framed messages
stdio-framed uses a 5-byte header:
kind: u8 | payload_length: u32 big-endian | payload
- kind
0: UTF-8 JSON payload; - kind
1:channel_length: u16 big-endian | channel UTF-8 | binary bytes.
Binary payloads are limited to 64 MiB per frame. Channel names are validated. Use application-level chunking, offsets, acknowledgements, and cancellation for large transfers.
Activation and process lifecycle
sequenceDiagram
participant UI as DBX UI
participant Host as PluginHost
participant Sidecar as Plugin sidecar
UI->>Host: invoke / test / connect / open workbench
Host->>Host: resolve active compatible version
Host->>Sidecar: spawn once on demand
Host->>Sidecar: plugin/initialize
Sidecar-->>Host: protocol + capabilities
Host->>Sidecar: provider or workbench RPC request
Sidecar-->>Host: result + events/binary frames
Host-->>UI: typed result + plugin-scoped events
Sidecars are shared per plugin process, not spawned per tab. Plugins own their internal session registries. DBX tears down plugin-owned connection pools before replace, rollback, or uninstall. Uninstall is blocked while saved connections still reference the plugin.
Security model
- Package authenticity: checksums + trusted Ed25519 repository keys.
- UI isolation: sandboxed iframe, restrictive CSP, bounded bridge payloads, safe asset paths, plugin identity binding.
- Secret persistence: plugin secrets are removed from connection JSON and stored through DBX's secret-store path. Ordinary cloud-sync snapshots always contain redacted placeholders. Secrets enter sync data only inside the encrypted payload when the user has configured a sync passphrase; without one, plugin secrets remain local and are not synchronized.
- Native backend trust: a native sidecar runs with the current OS user's privileges. A signature identifies the repository that approved and published the package; it is not an OS sandbox or proof that the author is harmless. Install only plugins whose backend code you trust.
- Permission declarations: privileged host bridge operations require declared permissions. Plugin UI network egress is fully blocked except for explicitly declared
host.network:origins. Native process filesystem/network access cannot currently be completely mediated by DBX.host.plans:readgrants reading host-generated estimated execution plans only; it never grants SQL execution, writes, DDL, or actual plans.host.schema:readgrants only narrow metadata for a table on an already-open host connection; it never grants arbitrary SQL, writes, or reconnects.host.storageconfines the workbench UI to a small JSON store inside its ownplugin-data/<id>directory; it grants no other filesystem reach.host.ailets a workbench open a built-in AI conversation seeded with a snapshot the plugin supplies and publish context-aware recommendation chips; the conversation API returns no model output or configuration. The optional text completion API returns sanitized model choices and generated text only after host confirmation; it never exposes credentials or SQL execution.host.data:readgrants single read-only statements only on connections the user consented to, per plugin and connection, revocable in Plugin Center; it never grants writes, DDL, locking reads, database switches, or reconnects. - AI tool exposure: plugin MCP tools reach the built-in AI agent only after the user enabled the plugin in Plugin Center, only on open connections and — unless declared read-only — only after a per-call approval; visibility is revocable per plugin (a manifest
ai_tools: falsealso opts out). The externaldbxMCP server exposes a plugin's tools only when its manifest declaresexternal_tools: true, and additionally applies the global MCP connection allowlist.
Custom repository public keys can be added or removed in Plugin Center. Obtain them through a channel independent from the downloaded package.
Build and release
The complete author and official-store flow is documented in RELEASING.md. Plugin authors keep source code in their own repository and publish unsigned candidates. DBX Store reviews and signs approved candidates, publishes installable artifacts, and records only catalog metadata in Git.
For a Rust backend:
cargo build --release --manifest-path path/to/backend/Cargo.toml
Stage manifest.json, declared assets, the current-target binary, and optional UI assets, then package:
cargo run --release \
--manifest-path plugins/sdk/packager/Cargo.toml \
-- path/to/stage path/to/vendor.example-1.0.0-darwin-arm64.dbxp \
--artifact-metadata path/to/vendor.example-1.0.0-darwin-arm64.artifact.json \
--target darwin-arm64
Repository operators sign an already-built candidate after review:
DBX_PLUGIN_SIGNING_KEY="..." \
cargo run --release \
--manifest-path plugins/sdk/packager/Cargo.toml \
-- sign path/to/vendor.example-1.0.0-darwin-arm64.unsigned.dbxp path/to/vendor.example-1.0.0-darwin-arm64.dbxp \
--key-id vendor-release \
--artifact-metadata path/to/vendor.example-1.0.0-darwin-arm64.artifact.json \
--target darwin-arm64
Plugin authors do not receive the official repository private key. Never commit a repository signing seed. Publish the corresponding 32-byte public key separately and rotate it with a new signingKeyId.
For native plugins, copy sdk/templates/github/plugin-release.yml into the plugin repository and pin the reusable workflow to a released DBX plugin SDK tag or commit. Frontend-only plugins may replace the build matrix with one universal build.
Compatibility policy
- Increment
manifest_versiononly for manifest shape changes that an older DBX cannot interpret. - Increment the sidecar protocol version only for wire-level incompatibilities.
- Use
engines.host_apifor host API compatibility andengines.dbxfor product-version constraints. - Additive contribution fields should remain optional within the same host API major version.
- Saved plugin connections must remain readable across plugin upgrades; migrate provider-owned
external_configexplicitly in the plugin backend when needed.
Validation
Core checks:
cargo test -p dbx-core --no-default-features plugins::
cargo check -p dbx-web --no-default-features
SDK and example checks:
cargo test --manifest-path plugins/sdk/rust/dbx-plugin-sdk/Cargo.toml
cargo check --manifest-path plugins/sdk/packager/Cargo.toml
node plugins/examples/hello-workbench/package.mjs
node plugins/examples/hello-workbench/smoke.mjs