Commit Graph
8 Commits
Author SHA1 Message Date
wizardchen d46fa25e41 feat(plugin): dynamic form options, OAuth connections and resource limits
Plugin config forms can now ask the plugin while they are filled in:

- x-options {name, dependsOn, search} loads a field's choices from
  /v1/options/{name}. POST /api/v1/plugins/:id/options merges the form's
  values with the stored secrets (system, tenant or instance scope) so a
  redacted token still works. Go and Python SDKs, OpenAPI and conformance
  cover the endpoint.
- x-oauth turns a string field into an account connection. WeKnora runs
  the authorization code flow (PKCE, client credentials from the plugin's
  system config), keeps the tokens sealed in plugin_oauth_connections and
  the field holds "oauth:<id>". The invoker swaps references for fresh
  access tokens at call time, refreshing near expiry. Callback:
  /api/v1/plugin-oauth/callback, one-time states in Redis or memory.
- SchemaForm gains an options widget (dependsOn reload, remote search)
  and an OAuth connect widget; connector, web search, workspace and
  platform config forms provide the plugin behind them.

Host plugins are capped per runtime.resources on Linux: RLIMIT_DATA for
memory (WEKNORA_PLUGIN_MEMORY_DEFAULT when undeclared) and, with a
delegated cgroup v2 (WEKNORA_PLUGIN_CGROUP), memory.max and cpu.max.
2026-09-26 11:51:35 +08:00
wizardchen e095ee7444 feat(plugin): event delivery and webhooks for plugins
Events:
- A plugin lists what it wants in permissions.events: knowledge.ingested,
  knowledge.failed, knowledge.deleted or chat.answered. The admin reviews
  the list at install; a plugin that only listens needs no other
  contribution.
- A publish enqueues one plugin:event task per subscribed plugin the
  workspace has on (maintenance queue, Redis or Lite).
- The task calls the plugin's new /v1/events at least once. Retryable
  failures retry up to 10 times, keeping the event ID; other failures drop
  the event.
- The plugin host refuses a process that subscribes without serving
  events.

Where events come from:
- A document reaches completed or failed through a dozen paths but a few
  repository writes, so a knowledge repository wrapper raises
  knowledge.ingested and knowledge.failed on the transition only:
  UpdateKnowledge, UpdateKnowledgeForTransfer, UpdateKnowledgeColumns,
  FinalizeSubtask, CompleteProcessingWithoutSubtasks.
- knowledge.deleted comes from the shared delete path.
- chat.answered comes from where RAG and agent answers are persisted. The
  stop button's early completion, which has no question, is skipped.

Webhooks:
- contributes.webhooks gives each workspace a secret URL per webhook:
  /api/v1/plugin-callbacks/{plugin}/{webhook}/{tenant}.{mac}. The MAC is
  keyed from SYSTEM_AES_KEY/JWT_SECRET.
- WeKnora answers 404 unless the URL is genuine, the webhook exists and the
  workspace has the plugin on. Calls are rate-limited per URL, bodies are
  capped at 1 MB and cookies are stripped.
- The call is relayed to the plugin's /v1/webhooks/{id} with that
  workspace's envelope; the plugin verifies the sender.
- Admins get the URLs from GET /plugins/:id/webhooks, shown with copy
  buttons in the plugin center's configure drawer. Plugins get them in
  context.webhooks when APP_EXTERNAL_URL is set.

SDKs:
- Go: OnEvent and Webhook.
- Python: @plugin.on_event and @plugin.webhook.
- Conformance, OpenAPI and READMEs are updated.

Also: examples/plugins/activity (Python) keeps a workspace activity feed
from events and a secret-checked note webhook, and shows it on a toolbox
page.
2026-09-26 11:21:55 +08:00
wizardchen d628fdf38f feat(plugin): standalone plugin hosts with Redis discovery
`WeKnora plugin-host` runs host plugins outside the app processes. It needs
no Kubernetes: it shares the app's database, object storage, Redis and
SYSTEM_AES_KEY.

Plugin host:
- Loads the installed host plugins of the kinds it runs
  (WEKNORA_PLUGIN_HOST_KINDS). It never migrates the database.
- Every 5s it announces itself and its plugins in Redis
  (weknora:plugin-host:<node>, TTL 15s).
- Serves its plugins at /p/{id}/{version}/... through a gateway. Calls must
  be HMAC-signed with a key derived from SYSTEM_AES_KEY (or JWT_SECRET);
  streams are relayed as they come.
- On shutdown it withdraws first, then lets calls in flight finish.
- Its node reports appear in the plugin's node list as `plugin-host:...`.

App nodes:
- Run the kinds WEKNORA_PLUGIN_EMBEDDED_KINDS names (default: every kind the
  machine can run; "none" hands all to plugin hosts).
- Call other host plugins on the least busy plugin host that runs the
  active version.
- A kind with no plugin host configured fails the load with the reason.
  With hosts configured, a missing one is a retryable "unavailable".
- Local plugins always get the loopback Host API address.
  WEKNORA_PLUGIN_HOST_API_URL is for plugins elsewhere, which also skip
  their egress proxy for that host.

Also:
- The reconciler takes runtime and manifest filters and a node role.
- The SDK client gains Raw for relaying.
- Deployment: a compose `plugin-host` profile, a Helm `pluginHost` Deployment
  (pods advertise their IP, app pods get the embedded kinds and Host API
  URL), .env.example entries.
- A plugins page in the docs site.
2026-09-26 10:19:25 +08:00
wizardchen 5f1245351f feat(plugin): Python SDK and python host plugins
pluginsdk/python is the extension protocol for Python 3.9+, standard
library only. It has the same shape as the Go SDK:

- web search, parsers, connectors (NDJSON streaming with checkpoints) and
  a config check;
- the host handshake over a unix socket with the host's token, or remote
  mode with HMAC-signed requests;
- errors mapped to protocol codes;
- a Host API client (kv);
- graceful drain on SIGTERM.

The Go conformance suite now also runs the Python SDK's test plugin in both
modes, and plugin-sdk.yml runs the Python tests on 3.9 and 3.13.

The embedded host runs runtime.kind python:

- WeKnora's python3 (or WEKNORA_PLUGIN_PYTHON) runs the entry, with
  vendor/ and the package root on PYTHONPATH.
- No bytecode is written into the shared package.
- On Windows, where Python has no unix sockets, the plugin listens on a
  loopback port.
- Installing checks the entry and the interpreter.

examples/plugins/notebooks is a Jupyter notebook parser written with it:
code, text outputs and charts, with a workspace setting for outputs.
2026-09-26 00:36:33 +08:00
wizardchen 39ffd07597 feat(plugin): remote runtime
A plugin can now run as a service the administrator deploys elsewhere and
registers by URL (runtime.type: remote). Installing asks for the URL,
issues a 32-byte signing secret (stored sealed, shown once) and keeps both
across upgrades. The admin API and the plugin detail drawer change the URL
and rotate the secret.

- internal/plugin/remote: an Activator that checks the service's health
  and manifest against the package, signs every call with the secret,
  health-checks it and reports outages to the reconciler; after an outage
  it checks the manifest again.
- Calls route to the host or remote runtime by where the plugin loaded.
  Remote plugins get Host API access only when WEKNORA_PLUGIN_HOST_API_URL
  names an address they can reach.
- The reconciler reloads a plugin when its URL or secret changes, and now
  retries a failed activation with backoff (30s doubling to 10m) instead of
  leaving it failed until someone changes it. That also covers host
  plugins that failed to start. A plugin that failed is unloaded when
  disabled.
- SavePlugin updates the new columns; a test keeps the update column list
  in step with the table.
- Migrations: plugins.remote_url, plugins.remote_secret.
2026-09-26 00:23:45 +08:00
wizardchen 5fa64e200a feat(plugin): Host API with plugin tokens and a key-value store
Plugins can call back into WeKnora. Each call to a plugin that asked for
Host API scopes (permissions.hostApi, accepted at install) carries the
Host API URL and a five-minute token bound to the plugin, its version,
the calling tenant and those scopes. Tokens are HS256 JWTs with their own
audience, keyed from the cluster secrets (SYSTEM_AES_KEY, else
JWT_SECRET), and are checked by /api/v1/plugin-host/* itself, outside
user and API-key auth.

The first scope is kv: a key-value store partitioned by plugin and tenant
(plugin_kv, migrations 000117 / sqlite 000036), so plugins keep state
without a database of their own. Get, put with an optional TTL, delete and
paged prefix listing; keys up to 256 bytes, JSON values up to 64 KB,
10,000 keys per plugin and tenant; expired entries read as missing and are
swept every ten minutes.

The SDK exposes it as call.Host().KVGet / KVPut / KVDelete / KVList. Host
plugins reach this node on loopback, which their environment now excludes
from the egress proxy.
2026-09-25 23:40:21 +08:00
wizardchen 6a4778b4a1 fix(plugin): mark a host plugin ready before Activate returns
The supervisor goroutine set the client and ready state, so a call made
right after Activate could still see the plugin as starting (CI caught it
on a busy runner). The first launch now publishes both synchronously.
2026-09-25 23:26:34 +08:00
wizardchen 7dc17a6d92 feat(plugin): run code plugins in an embedded plugin host
Host-runtime plugins (runtime: { type: host, kind: binary }) now install and
run. Each node's host starts the package's binary for its OS and arch as a
child process and only lets calls through once the process printed its
handshake, answers health checks, and reports the installed plugin ID,
version and protocol and every code contribution plugin.yaml declares. A
crash or three failed health checks restarts it with backoff; the node
reports the plugin degraded meanwhile. An upgrade starts the new process
before stopping the old one. Linux children die with WeKnora.

Isolation: the child gets a private unix socket and a random token, an
environment built from scratch so WeKnora's own secrets never reach plugin
code, and HTTP(S)_PROXY pointing at a per-plugin egress proxy that forwards
only to the hosts in permissions.egress, through the SSRF-safe dialer.

Manifests of code plugins must declare apiVersion weknora.plugin/v1, and
egress entries must be host names or *.domain wildcards. Web search and data
source connectors are open to third-party code plugins. The installer
accepts host packages that carry a binary for the server's OS and arch.
2026-09-25 22:33:48 +08:00