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.
(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.
(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.
Call `f`, conveying THIS thread's dynamic bindings to `session`'s host calls.
(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 |