Project-local Python extensions — trusted-context plug-ins.
Vis extensions are normally Clojure libraries baked into the binary at
build time. This namespace adds a second, fully dynamic authoring path:
drop a *.py file into
~/.vis/extensions/ (global — every project) <project>/.vis/extensions/ (project-local — this project only)
and it loads at startup (and on /reload) in BOTH the JVM and the
GraalVM native-image build — Python redefinition is pure Truffle
dynamism, no runtime class definition involved.
Each file is evaluated in its own TRUSTED GraalPy context. This is NOT
the model's sandbox: the model's context is untrusted, per-session and
deny-by-default; extension contexts are user-trusted (same trust level
as a Clojure extension on the classpath), process-wide, and get real
filesystem / network / environment access. The two share nothing — the
model can call an extension TOOL (through the host wrapper, envelope-
checked like any tool) but can never evaluate code in the extension's
context. Host capabilities are reachable ONLY through the bound vis
API (no arbitrary Java interop: allowAllAccess stays false and no
host classes are exposed).
Every context is built on env-python/shared-engine — the ONE
process-wide Engine that makes context creation safe even while a
sandbox eval is running (see the deadlock notes there). Calls into an
extension (tool, activation, prompt, slash, op hook) are serialized
with locking on its context, the same proven pattern as the printer
context.
The file's top-level vis.extension(...) call registers through the
ordinary register-extension! — from the registry's perspective a
Python extension is indistinguishable from a Clojure one (activation,
prompt assembly, slash dispatch, vis extensions list all just work).
A file that fails to load becomes a load-failure warning (surfaced via
vis doctor), never a crash.
Project-local Python extensions — trusted-context plug-ins. Vis extensions are normally Clojure libraries baked into the binary at build time. This namespace adds a second, fully dynamic authoring path: drop a `*.py` file into ~/.vis/extensions/ (global — every project) <project>/.vis/extensions/ (project-local — this project only) and it loads at startup (and on `/reload`) in BOTH the JVM and the GraalVM native-image build — Python redefinition is pure Truffle dynamism, no runtime class definition involved. Each file is evaluated in its own TRUSTED GraalPy context. This is NOT the model's sandbox: the model's context is untrusted, per-session and deny-by-default; extension contexts are user-trusted (same trust level as a Clojure extension on the classpath), process-wide, and get real filesystem / network / environment access. The two share nothing — the model can call an extension TOOL (through the host wrapper, envelope- checked like any tool) but can never evaluate code in the extension's context. Host capabilities are reachable ONLY through the bound `vis` API (no arbitrary Java interop: `allowAllAccess` stays false and no host classes are exposed). Every context is built on `env-python/shared-engine` — the ONE process-wide Engine that makes context creation safe even while a sandbox eval is running (see the deadlock notes there). Calls into an extension (tool, activation, prompt, slash, op hook) are serialized with `locking` on its context, the same proven pattern as the printer context. The file's top-level `vis.extension(...)` call registers through the ordinary `register-extension!` — from the registry's perspective a Python extension is indistinguishable from a Clojure one (activation, prompt assembly, slash dispatch, `vis extensions list` all just work). A file that fails to load becomes a load-failure warning (surfaced via `vis doctor`), never a crash.
(add-change-listener! listener-id f)Subscribe f to Python-extension set changes. f receives
{:extensions [<validated ext map> ...] :removed [<ext-name> ...]}
after every (re)load that changed anything: :extensions is the full
freshly-registered set, :removed the names that no longer exist.
Re-registering the same listener-id replaces the old listener.
Returns listener-id.
Subscribe `f` to Python-extension set changes. `f` receives
`{:extensions [<validated ext map> ...] :removed [<ext-name> ...]}`
after every (re)load that changed anything: `:extensions` is the full
freshly-registered set, `:removed` the names that no longer exist.
Re-registering the same `listener-id` replaces the old listener.
Returns `listener-id`.(load-failures)Load failures from the most recent Python-extension scan:
[{:file <path> :error <message>} ...].
Load failures from the most recent Python-extension scan:
`[{:file <path> :error <message>} ...]`.(load-python-extensions!)(load-python-extensions! {:keys [dirs]})Scan the Python extension dirs (default: ~/.vis/extensions and
<cwd>/.vis/extensions) and (re)load every *.py file. Idempotent:
when no file changed since the last scan this is a cheap no-op. On any
change the whole set is torn down and rebuilt (contexts are ~40ms warm
on the shared engine) — deterministic ordering, no partial states.
A file that fails to load is recorded in load-failures (and surfaced
by vis doctor) — it never crashes the host.
Returns {:loaded n :failed n :changed? bool}.
Scan the Python extension dirs (default: `~/.vis/extensions` and
`<cwd>/.vis/extensions`) and (re)load every `*.py` file. Idempotent:
when no file changed since the last scan this is a cheap no-op. On any
change the whole set is torn down and rebuilt (contexts are ~40ms warm
on the shared engine) — deterministic ordering, no partial states.
A file that fails to load is recorded in `load-failures` (and surfaced
by `vis doctor`) — it never crashes the host.
Returns `{:loaded n :failed n :changed? bool}`.(loaded-python-extensions)Snapshot of the currently loaded Python extensions:
{<canonical-path> {:sha ... :ext-name ...}} (context handle elided).
Snapshot of the currently loaded Python extensions:
`{<canonical-path> {:sha ... :ext-name ...}} ` (context handle elided).(reload-python-extensions!)(reload-python-extensions! opts)Force a full reload of every Python extension (even when no file
changed). Same return shape as load-python-extensions!. Live
sessions pick the new tool bindings up at the next turn boundary.
Force a full reload of every Python extension (even when no file changed). Same return shape as `load-python-extensions!`. Live sessions pick the new tool bindings up at the next turn boundary.
(remove-change-listener! listener-id)Remove a listener registered with add-change-listener!. Returns nil.
Remove a listener registered with [[add-change-listener!]]. Returns nil.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |