-
v0.12.0 Stable
released this
2026-10-22 00:30:07 +00:00 | 4 commits to main since this releaseVSKI v0.12.0
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=trueto switch on; when off, every plugin endpoint
answers 503 with setup instructions).How it works
A plugin is a directory under
PLUGINS_DIR(default
DATA_DIR/plugins) containing avski-plugin.jsonmanifest (name,
runtime entry, config schema withsecretfields, declared deps as
metadata) and a Python package. The supervisor spawns
$PLUGIN_PYTHON -m vski.plugin.runnerinside it -PLUGIN_PYTHONmust
havevski-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); therestartscounter 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.
API
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_logspipeline - a zero-hop alternative to pointing a trigger
at localhost. Cron jobs resolve their own database for the instance
lookup.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
from vski.plugin import Plugin app = Plugin() @app.on_event("documents") # realtime consumer group, at-least-once async def on_document(ctx, event): ... @app.function("reindex") # callable via /invoke and plugin:// triggers async def reindex(ctx, params): ... @app.cron("*/5 * * * *") # dependency-free 5-field cron async def sweep(ctx): ...ctxcarries a db-scopedclient, the instanceconfigand alog
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") andgraph-sync
(mirrors a collection into a light graph withproperties.reflinks -
formalizes the Graphs tutorial's sync pattern).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 theackIdfield 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
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.SDKs
- TypeScript/JS (
@vski/sdkv2.2.0): newclient.plugins
namespace -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
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) andtest_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
- 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 buildFROMthem per the wiki.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Plugins are reusable source, not packages. Think shadcn components: