VSKI v0.12.0
Plugins: supervised Python beside the server (experimental)
Section titled “Plugins: supervised Python beside the server (experimental)”Every real deployment grows a system around VSKI: an embeddings indexer, a record-to-graph sync, a crawler, a chat bot. Until now those lived as undocumented sidecars - cron entries, hand-rolled daemons, HTTP triggers pointing at localhost - with no shared lifecycle, no configuration story and no observability. v0.12.0 makes that pattern first-class: plugins are Python processes supervised by the server, configured and observed through the admin API and dashboard, running in the same container with zero network hops.
The design stance matters as much as the feature:
- Plugins are reusable source, not packages. Think shadcn components: you copy one into your distribution, verify its dependencies against the rest of your environment, tune its config schema, and build one image that just works. The server never installs or manages Python dependencies - the official images ship no Python at all, and distributions that want plugins bring their own locked interpreter (the wiki has the Dockerfile snippet).
- An instance is data. Plugin + database + JSON config + enabled flag. Changing an API key or cloning a bot for another chat group is a PATCH or a POST that restarts exactly one process - never the server, never the container.
- Not a sandbox. Plugin code ships inside your image and is trusted, exactly like workflow workers. There is no upload endpoint; code arrives via the filesystem only.
Plugins are experimental and disabled by default
(PLUGINS_ENABLED=true to switch on; when off, every plugin endpoint
answers 503 with setup instructions).
How it works
Section titled “How it works”A plugin is a directory under PLUGINS_DIR (default
DATA_DIR/plugins) containing a vski-plugin.json manifest (name,
runtime entry, config schema with secret fields, declared deps as
metadata) and a Python package. The supervisor spawns
$PLUGIN_PYTHON -m vski.plugin.runner inside it - PLUGIN_PYTHON must
have vski-sdk>=0.5.0 (and the plugin’s dependencies) installed by the
distribution.
- Control plane: protocol frames on the process’s stdin/stdout (initialize / ping / invoke / shutdown), logs on stderr. A warm invocation is a pipe write - no socket, no cold start.
- Health: pinged every 10s; an unresponsive process is killed and
restarted. Crashes restart with exponential backoff (1s doubling to
60s, reset after 5 minutes of uptime); the
restartscounter tracks crash restarts.kill -9recovery is covered by the e2e suite. - Credentials: each process start mints a fresh API key with the
pluginrole and deletes it when the process exits - key material exists only in server memory and the child environment. The key is a normal authenticated principal: collection and graph access rules apply to it. - Events: plugin handlers subscribe through the existing realtime websocket using a per-instance consumer group - at-least-once delivery with ACK-committed group offsets, so events that occur while a plugin is down are redelivered when it comes back (see the realtime section below).
- Replicas never supervise plugins (like webhooks and realtime); instance status stays readable through the synced registry.
All admin-only:
GET /api/plugins,POST /api/plugins/scan- catalog from manifestsGET/POST /api/plugin-instances(instances are unique per(dbName, name)),GET/PATCH/DELETE /api/plugin-instances/:name?db=- a config change bumps
configVersionand restarts only that instance’s process
- a config change bumps
POST .../:name/start|stop|restartPOST .../:name/invoke-{function, params, timeoutSeconds?}(default 30s, max 120s) over the warm pipeGET .../:name/logs?level=&limit=- captured stderr (newest first; kept 7 days in_plugin_logsin stats.db)
Collection triggers and HTTP cron jobs accept
plugin://<instance>/<function> as their URL: the webhook engine
dispatches it in-process with the same retry policy and the same
_webhook_logs pipeline - a zero-hop alternative to pointing a trigger
at localhost. Cron jobs resolve their own database for the instance
lookup.
Dashboard
Section titled “Dashboard”Settings → Plugins: catalog cards from the manifests, the instances
table with live status chips / restart counts / uptime, a config editor
generated from each manifest’s schema (saving restarts just that
process), an invoke console (function picker + params + result), a
captured-log tail, start/stop/restart actions, and a disabled-state
banner pointing at the env flags. The dashboard now depends on
@vski/sdk@^2.2.0.
Writing a plugin
Section titled “Writing a plugin”from vski.plugin import Plugin
app = Plugin()
@app.on_event("documents") # realtime consumer group, at-least-onceasync def on_document(ctx, event): ...
@app.function("reindex") # callable via /invoke and plugin:// triggersasync def reindex(ctx, params): ...
@app.cron("*/5 * * * *") # dependency-free 5-field cronasync def sweep(ctx): ...ctx carries a db-scoped client, the instance config and a log
whose output the server captures. Four working examples ship in
examples/plugins/: hello (every handler type, doubles as the e2e
fixture), note-bot (the wiki tutorial plugin), embeddings-indexer
(event-driven chunking/embedding via an OpenAI-compatible endpoint -
formalizes the Embeddings tutorial’s “next steps”) and graph-sync
(mirrors a collection into a light graph with properties.ref links -
formalizes the Graphs tutorial’s sync pattern).
Realtime: durable consumer groups and fixes
Section titled “Realtime: durable consumer groups and fixes”Work on plugin event delivery hardened the realtime gateway for everyone:
- API keys are now accepted on
/api/realtime(?auth=<key>, any enabled key) - service processes can subscribe like any client. - Consumer groups are durable. ACKs now advance a per-group
committed offset, and consumers joining later (or plugin processes
restarting) resume after it instead of tailing from “now” - events
that fired while nobody was connected are no longer skipped. Group
deliveries now carry the
ackIdfield both SDKs already expected (neither ever ACKed before, because the server never sent it). - Fixed: a fresh group consumer received one stale event from the head of the untrimmed event store (delivery position initialized from zero instead of the subscription position).
- Fixed: event frames carried the id of the first stored event with a matching action rather than their own, so ACKs could commit the wrong position.
- Event ids are zero-padded (
<millis>-<seq>), making the id ordering used throughout the gateway numerically correct.
Non-group subscriptions are unchanged. Delivery remains at-least-once for groups.
Supervisor hardening
Section titled “Supervisor hardening”Start/stop transitions are serialized per instance (a restart can no longer race the reconciler into double-spawning an orphaned process), shutdown waits are bounded, and no process is spawned once shutdown has begun.
- TypeScript/JS (
@vski/sdkv2.2.0): newclient.pluginsnamespace -list,scan,listInstances,getInstance,createInstance,updateInstance,deleteInstance,startInstance/stopInstance/restartInstance,invoke,logs- with full types (PluginCatalogItem,PluginInstance,PluginRuntimeInfo,PluginLogEntry). - Python (
vski-sdkv0.5.0): thevski.pluginmodule described above, a matchingclient.pluginsmanagement namespace, and three transport fixes: request paths with an embedded query string were silently dropped (httpx replaces a URL’s query when givenparams, even empty), websocket auth tokens are now URL-quoted (raw-base64 API keys containing+previously failed as spaces), and group subscriptions tracklastIdso a reconnecting consumer resumes after the last seen event.
Documentation & tests
Section titled “Documentation & tests”Wiki: Plugins reference
(architecture, manifest spec, env contract, API, security model,
distribution Dockerfile) and a hands-on
Plugins Tutorial
whose flows are covered end-to-end by the new suites:
48_plugins_test.ts (15 steps: catalog, lifecycle, warm invoke, event
delivery, captured logs, per-instance restart on config change,
plugin:// trigger delivery, clone-into-second-db, crash recovery,
admin gating, disabled-503) and test_48_plugins.py (instance
lifecycle, event delivery, plugin:// triggers, admin gating,
including downtime redelivery). The e2e harnesses now start their
servers with plugins enabled and the examples directory mounted.
Compatibility
Section titled “Compatibility”- No breaking API or storage changes. Plugins are off by default; the
plugins/plugin_instancestables (default.db) and_plugin_logs(stats.db) are created idempotently on boot. - Realtime group consumers start ACKing and resume from committed offsets - strictly fewer lost events; clients that manually ACKed are unaffected. The padded event-id format only matters within a single server process (the event store is in-memory).
- Official docker images are unchanged (no Python); plugin-enabled
distributions build
FROMthem per the wiki.