Using Plugins
CLI Loading
Use --plugin-dir to load executable plugins from a directory:
scriptling --plugin-dir ./plugins script.py
scriptling --plugin-dir ./plugins --plugin-dir ./more-plugins -c 'import plugin.hello; print(plugin.hello.greet("Ada"))'The flag can be repeated. Scriptling scans executable files directly inside each directory. Subdirectories are ignored.
Configuration options:
| Source | Key |
|---|---|
| CLI | --plugin-dir ./plugins |
| Environment | SCRIPTLING_PLUGIN_DIR=./plugins |
| Config file | plugins.dirs = ["./plugins"] |
Plugin loading is eager. Startup failures are reported as warnings. A loaded plugin that fails while a script is running produces an execution error.
Importing Plugin Libraries
A plugin declares a short name, for example hello, and Scriptling exposes it as plugin.hello:
import plugin.hello
print(plugin.hello.greet("Ada"))Inspecting Plugins
Use the built-in scriptling.plugin library for metadata and direct calls:
import scriptling.plugin
for meta in scriptling.plugin.list():
print(meta["name"])
meta = scriptling.plugin.describe("plugin.hello")
print(meta["functions"])
result = scriptling.plugin.call_function("plugin.hello", "greet", "Ada")Remote Objects
Plugin classes create remote objects inside the plugin process. The Scriptling object is a proxy:
import plugin.hello
import scriptling.plugin
cfg = plugin.hello.Config("Ada")
print(cfg.get("name"))
scriptling.plugin.release(cfg)Prefer explicit release() for deterministic cleanup. Embedded Go applications can call plugin.ReleaseWithContext(ctx, obj) when release should follow a request context. The contextless plugin.Release(obj) and GC finalizer fallback use plugin.DefaultReleaseTimeout. All class instances with __del__ get a GC finalizer installed automatically: both in-process and plugin objects: as a best-effort fallback.
Loading JSON-RPC Peers at Runtime
The --plugin-dir flag loads plugins eagerly at startup. For ad-hoc loading
from inside a script, use scriptling.plugin.load and unload. These can
spawn executable peers over stdio or connect to HTTP(S) JSON-RPC endpoints.
They are always available: even without --plugin-dir: in run, server, and
--json-rpc modes.
load takes the library name first and the executable path or HTTP endpoint
second. With the default scriptling=False, the loaded client is helper-only
and is driven through call_function. With scriptling=True, Scriptling
performs the plugin handshake and registers an importable plugin.* proxy
library.
import scriptling.plugin
# Spawn an executable and register it as "plugin.mine". Returns the normalised
# name. Identity is by absolute path: calling load() again with the same
# name+path is a no-op.
name = scriptling.plugin.load("mine", "/opt/myext/bin")
# Call functions on it via call_function. The short name also works:
# call_function("mine", "do_thing", "hello").
result = scriptling.plugin.call_function(name, "do_thing", "hello")
# Close the process and free the name for re-use.
scriptling.plugin.unload(name)HTTP endpoints use the same call surface:
import scriptling.plugin
name = scriptling.plugin.load(
"remote",
"http://127.0.0.1:8000/json-rpc",
headers={"Authorization": "Bearer token"},
)
result = scriptling.plugin.call_function(name, "do_thing", {"value": "hello"})Batch calls
Use batch_call to send several function calls to the same peer in one
JSON-RPC batch frame. The result list matches the input order.
import scriptling.plugin
name = scriptling.plugin.load("myrpc", "scriptling",
args=["--json-rpc", "./setup.py"])
results = scriptling.plugin.batch_call(name, [
{"name": "ping"},
{"name": "add", "args": [20, 22]},
{"name": "search", "kwargs": {"query": "hello"}},
])Each call is a dictionary with name, optional args, and optional kwargs.
For raw JSON-RPC peers, name is sent directly as the JSON-RPC method. For
scriptling=True peers, each item is sent as a typed plugin function.call.
Callback arguments are not supported in batch_call.
Passing command-line arguments
Use the args keyword to pass command-line arguments to the executable: for
example when loading scriptling itself in --json-rpc mode:
import scriptling.plugin
name = scriptling.plugin.load("myrpc", "scriptling",
args=["--json-rpc", "./setup.py"])
result = scriptling.plugin.call_function(name, "echo", "hello")args is ignored for HTTP(S) endpoints. For self-signed HTTPS endpoints, pass
insecure_skip_tls=True:
name = scriptling.plugin.load("devrpc", "https://127.0.0.1:8443/json-rpc",
insecure_skip_tls=True,
headers={"Authorization": "Bearer token"})The scriptling flag
The flag controls whether the full Scriptling plugin protocol is used:
| Mode | Behaviour |
|---|---|
scriptling=False (default) |
No handshake. call_function sends the function name directly as the JSON-RPC method; describe() / list() report transport: "json" but no version or schema. |
scriptling=True |
Performs the plugin protocol handshake, uses function.call, registers an importable plugin.* proxy library, and fills describe() / list() from the peer. |
# With handshake - import plugin.widgets works and describe() shows metadata.
name = scriptling.plugin.load("widgets", "/opt/widgets/widget", scriptling=True)
scriptling.plugin.call_function(name, "build", "chair")
import plugin.widgets
plugin.widgets.build("desk")HTTP(S) peers can also receive headers on every JSON-RPC request, including the handshake, calls, and batches:
name = scriptling.plugin.load(
"widgets",
"https://plugins.example.test/json-rpc",
scriptling=True,
headers={"Authorization": "Bearer token"},
)HTTP plugin transport is request/response only. It supports calls, objects,
generated plugin.* proxies, and batches, but the server cannot initiate
callbacks back to the client. Use stdio plugins when host callbacks or
plugin.Logger(ctx) are required.
Identity, collisions, and unload
load()is idempotent on path/URL + name: calling it twice with the same absolute path or URL and the same name returns the same client (thescriptling,args,insecure_skip_tls, andheadersoptions are ignored on the second call).- Loading an already-loaded path or URL under a different name raises an error.
- Loading a new path or URL under a name already in use raises an error. The
name must not collide with any existing plugin library: including ones
discovered via
--plugin-dir. unload(name)sends a best-effort shutdown, closes the process, removes the client, and removes any dynamicplugin.*proxy registered for that peer. The same name+path can beload()-ed again afterwards.- All loaded executables appear in
scriptling.plugin.list()alongside any plugins discovered via--plugin-dir.