THE door from the sandbox back into Vis, and the only one.
The embedded interpreter calls out through ONE function it was handed, so everything a block can ask the host for arrives here as two strings: the name of a tool and a JSON envelope of its arguments. What crosses is DATA - a tool takes plain values and answers plain values, because the boundary carries text and nothing else. A live object (an interpreter handle, a stream, a file) crossed it and never will; a handle the model holds is a PYTHON object built over calls that come back through here.
One interpreter serves every session in this process, so the envelope names
the session that called (vis-python-runtime's install) and this registry
answers per session: shell bound for two workspaces is two different
functions under one name, and a session that never bound a name gets a
refusal that says so instead of a neighbour's tool.
The host is bound ONCE per process. Binding is idempotent because the
interpreter is: a second bind! would replace a live upcall stub while a
block sits inside it.
THE door from the sandbox back into Vis, and the only one. The embedded interpreter calls out through ONE function it was handed, so everything a block can ask the host for arrives here as two strings: the name of a tool and a JSON envelope of its arguments. What crosses is DATA - a tool takes plain values and answers plain values, because the boundary carries text and nothing else. A live object (an interpreter handle, a stream, a file) crossed it and never will; a handle the model holds is a PYTHON object built over calls that come back through here. One interpreter serves every session in this process, so the envelope names the session that called (`vis-python-runtime`'s `install`) and this registry answers per session: `shell` bound for two workspaces is two different functions under one name, and a session that never bound a name gets a refusal that says so instead of a neighbour's tool. The host is bound ONCE per process. Binding is idempotent because the interpreter is: a second `bind!` would replace a live upcall stub while a block sits inside it.
True while the host serves a call from a thread that the guest created. Such a call has no active extension call, so its doors can limit what it does.
True while the host serves a call from a thread that the guest created. Such a call has no active extension call, so its doors can limit what it does.
(bind!)Make dispatch the host THIS process's interpreter calls back into, if this
process has an interpreter to bind.
Idempotent: the interpreter holds one upcall stub, and rebinding it while a block is inside a call would swap the target under a live frame. Answers whether a host is bound now.
Two things this used to get wrong, both measured while loading a Python extension in a process that never started a sandbox. It marked itself done BEFORE the bind, so a bind that threw left the flag set and every later caller skipped it — the first extension in such a process failed to load and the rest only appeared to work. And it insisted on the interpreter existing at all: binding needs the cdylib resolved, which a process that only talks to the extension host has no reason to have fetched. A library that will not resolve is not an error here, it is a process with nothing to bind — the extension host binds its OWN interpreter in its own process, and the parent binds when it builds a sandbox.
Make [[dispatch]] the host THIS process's interpreter calls back into, if this process has an interpreter to bind. Idempotent: the interpreter holds one upcall stub, and rebinding it while a block is inside a call would swap the target under a live frame. Answers whether a host is bound now. Two things this used to get wrong, both measured while loading a Python extension in a process that never started a sandbox. It marked itself done BEFORE the bind, so a bind that threw left the flag set and every later caller skipped it — the first extension in such a process failed to load and the rest only appeared to work. And it insisted on the interpreter existing at all: binding needs the cdylib resolved, which a process that only talks to the extension host has no reason to have fetched. A library that will not resolve is not an error here, it is a process with nothing to bind — the extension host binds its OWN interpreter in its own process, and the parent binds when it builds a sandbox.
(call-args args kwargs)The argument vector a tool fn takes for Python args and kwargs.
Clojure tools consume options maps; Python callables recover the marked kwargs before invocation. User positional maps never carry this metadata.
The argument vector a tool fn takes for Python `args` and `kwargs`. Clojure tools consume options maps; Python callables recover the marked kwargs before invocation. User positional maps never carry this metadata.
(conveying session & body)Evaluate body, conveying the current dynamic bindings to session's host calls.
Evaluate `body`, conveying the current dynamic bindings to `session`'s host calls.
(conveying* session f)Call f, conveying THIS thread's dynamic bindings to session's host calls.
A context has no invocation identity on its host callbacks, so its complete driver calls must run one at a time. Waiting is interruptible; other contexts remain independent. Reentrant calls restore the outer driver's frame.
Call `f`, conveying THIS thread's dynamic bindings to `session`'s host calls. A context has no invocation identity on its host callbacks, so its complete driver calls must run one at a time. Waiting is interruptible; other contexts remain independent. Reentrant calls restore the outer driver's frame.
(dispatch caller tool payload)Serve one call from the sandbox: WHO called, payload in, reply JSON out.
caller is the INTERPRETER's answer — the namespace the call was made from —
and it is the only thing this authorizes against. The payload also carries a
session, because the guest's envelope always did, but that field is written
by the guest: a block that named a neighbour's session used to be served the
neighbour's tools, with the neighbour's roots (measured: a confined block read
a file its own policy had refused it one statement earlier). A mismatch is a
forgery attempt and is recorded as one.
THE host function itself, kept public so a test can measure the boundary
without an interpreter. It never throws: a failure is a reply the guest
raises as RuntimeError, because a host that throws here unwinds through the
interpreter's upcall stub.
Serve one call from the sandbox: WHO called, `payload` in, reply JSON out. `caller` is the INTERPRETER's answer — the namespace the call was made from — and it is the only thing this authorizes against. The payload also carries a session, because the guest's envelope always did, but that field is written by the guest: a block that named a neighbour's session used to be served the neighbour's tools, with the neighbour's roots (measured: a confined block read a file its own policy had refused it one statement earlier). A mismatch is a forgery attempt and is recorded as one. THE host function itself, kept public so a test can measure the boundary without an interpreter. It never throws: a failure is a reply the guest raises as `RuntimeError`, because a host that throws here unwinds through the interpreter's upcall stub.
(forget-session! session)Drop session's bindings, answering how many names went.
The interpreter's namespace is closed by whoever owns the session; this is the host half, and leaving it behind would keep every closure a finished session captured.
Drop `session`'s bindings, answering how many names went. The interpreter's namespace is closed by whoever owns the session; this is the host half, and leaving it behind would keep every closure a finished session captured.
(forget-tools! session names)Drop names from session's registry, answering nothing.
For a tool that exists only for the length of ONE call — a host callback an adapter hands INTO Python — the registry is what makes the name callable, so forgetting it is what takes the capability back.
Drop `names` from `session`'s registry, answering nothing. For a tool that exists only for the length of ONE call — a host callback an adapter hands INTO Python — the registry is what makes the name callable, so forgetting it is what takes the capability back.
(install-doors! session tools)(install-doors! session tools install-one)Install tools into session AND register them for every session.
A shim door is ONE host capability, not a per-session closure, and Python is
PROCESS state: attach and ls staple their entry points onto builtins, so
a second session can find names the FIRST session built — holding proxies that
still name it. The shared key is what keeps those doors answering after that
session is gone.
Install `tools` into `session` AND register them for every session. A shim door is ONE host capability, not a per-session closure, and Python is PROCESS state: `attach` and `ls` staple their entry points onto `builtins`, so a second session can find names the FIRST session built — holding proxies that still name it. The shared key is what keeps those doors answering after that session is gone.
(install-sync-tools! session tools)(install-sync-tools! session tools install-one)Bind tools as session's host tools that answer DIRECTLY, and answer the
names bound.
For Python the host runs, not the model: a sandbox tool hands back a thunk
the block runner settles, and trusted code - an extension calling
vis.shell(...) in the middle of a line - has no runner and no await.
The installer is a PARAMETER because trusted Python does not run where the
sandbox does: an extension session binds its names in the unconfined child
process, a sandbox session in this one, and the registry that answers them is
the same either way.
Bind `tools` as `session`'s host tools that answer DIRECTLY, and answer the names bound. For Python the host runs, not the model: a sandbox tool hands back a thunk the block runner settles, and trusted code - an extension calling `vis.shell(...)` in the middle of a line - has no runner and no `await`. The installer is a PARAMETER because trusted Python does not run where the sandbox does: an extension session binds its names in the unconfined child process, a sandbox session in this one, and the registry that answers them is the same either way.
(install-tools! session tools)(install-tools! session tools bind-one)Bind tools - {python-name fn} - as session's host tools and answer the
names bound.
Registration and installation are ONE step on purpose: a name the guest can call but the registry does not know is a refusal the model cannot act on.
bind-one says WHERE the guest half is bound: this process's interpreter by
default, or a session's own worker when the caller hands one in. The registry
half is the parent's either way — a worker calls back here.
Bind `tools` - `{python-name fn}` - as `session`'s host tools and answer the
names bound.
Registration and installation are ONE step on purpose: a name the guest can
call but the registry does not know is a refusal the model cannot act on.
`bind-one` says WHERE the guest half is bound: this process's interpreter by
default, or a session's own worker when the caller hands one in. The registry
half is the parent's either way — a worker calls back here.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 |