Process boundaries for model Python and trusted Python extensions.
Each session's sandbox owns a confined runtime worker. Its trusted extension namespaces run in a separate worker, preserving their host APIs and native library support without sharing interpreter memory or host-call authority with model code. Registration outside a session uses the shared trusted worker.
Confinement, imports, native libraries and interpreter state are process-wide. A per-session extension worker keeps that state separate from other sessions as well as from the sandbox. Only the host chooses a worker's role.
The wire is ONE line of JSON per message over a unix socket, both ways. The
parent asks (install-runtime, install-tool, exec, run, run-block,
eval, confine, network, stdin, interrupt, close); the child asks
back with host, because the registry that knows what a name may call, the
persistence handle and the caller's dynamic binding frame all live in the
parent (python-host/dispatch). stdout is NOT the wire: Python that prints,
or a native library writing to fd 1, would corrupt it, so a child's own stdio
goes to a log file instead.
A message carrying op is a request, one without is its reply, so each side
numbers its own requests and no id can collide. Work has no timeout: a block
or extension tool may legitimately run for minutes. CONTROL is different:
an interrupt that cannot reach the child is bounded, because cancellation
must be able to retire that process instead of parking its caller forever.
A child that DIES is what the pump reports — every call waiting on it fails
at once with the child's log to read.
An interrupt reaches BOTH ends of a host call. The child's async exception
lands only when the guest next runs Python, and a guest parked in host
(a shell handle's wait, a long tool) runs none until the parent answers —
so interrupt! also fails every host call the worker has in flight, and the
tool thread serving it is interrupted. Measured before that: the unwind
watch expired, the worker was killed, and the next block silently started a
FRESH interpreter that had the runtime but none of the session's tools. A
retired key now refuses to restart until the session is rebuilt.
Process boundaries for model Python and trusted Python extensions. Each session's sandbox owns a confined runtime worker. Its trusted extension namespaces run in a separate worker, preserving their host APIs and native library support without sharing interpreter memory or host-call authority with model code. Registration outside a session uses the shared trusted worker. Confinement, imports, native libraries and interpreter state are process-wide. A per-session extension worker keeps that state separate from other sessions as well as from the sandbox. Only the host chooses a worker's role. The wire is ONE line of JSON per message over a unix socket, both ways. The parent asks (`install-runtime`, `install-tool`, `exec`, `run`, `run-block`, `eval`, `confine`, `network`, `stdin`, `interrupt`, `close`); the child asks back with `host`, because the registry that knows what a name may call, the persistence handle and the caller's dynamic binding frame all live in the parent (`python-host/dispatch`). stdout is NOT the wire: Python that prints, or a native library writing to fd 1, would corrupt it, so a child's own stdio goes to a log file instead. A message carrying `op` is a request, one without is its reply, so each side numbers its own requests and no id can collide. Work has no timeout: a block or extension tool may legitimately run for minutes. CONTROL is different: an interrupt that cannot reach the child is bounded, because cancellation must be able to retire that process instead of parking its caller forever. A child that DIES is what the pump reports — every call waiting on it fails at once with the child's log to read. An interrupt reaches BOTH ends of a host call. The child's async exception lands only when the guest next runs Python, and a guest parked in `host` (a shell handle's `wait`, a long tool) runs none until the parent answers — so `interrupt!` also fails every host call the worker has in flight, and the tool thread serving it is interrupted. Measured before that: the unwind watch expired, the worker was killed, and the next block silently started a FRESH interpreter that had the runtime but none of the session's tools. A retired key now refuses to restart until the session is rebuilt.
(configure! k policy-fn)Register the live session policy used whenever worker k starts or restarts.
Must happen before the first interpreter request; a missing policy fails closed.
A session being (re)built under a key that was retired is that key's new life.
Register the live session policy used whenever worker `k` starts or restarts. Must happen before the first interpreter request; a missing policy fails closed. A session being (re)built under a key that was retired is that key's new life.
(confine! k session read write refusal)Confine k's interpreter to read/write, or lift it with two empty lists.
The policy is that PROCESS's, which is why one worker per session is the whole
point: what used to be every session in the gateway is now this session.
Confine `k`'s interpreter to `read`/`write`, or lift it with two empty lists. The policy is that PROCESS's, which is why one worker per session is the whole point: what used to be every session in the gateway is now this session.
(extension-worker-key session)Host-owned worker identity for a session's trusted extensions, not its sandbox. The key is never accepted from Python or reconstructed from a wire payload.
Host-owned worker identity for a session's trusted extensions, not its sandbox. The key is never accepted from Python or reconstructed from a wire payload.
(forget-policy! k)Forget k after ordinary session disposal; stopping a wedged worker alone keeps
the policy so no late call can restart outside the boundary.
Forget `k` after ordinary session disposal; stopping a wedged worker alone keeps the policy so no late call can restart outside the boundary.
(guest-source-dir)Stage Vis-owned Python guest modules in the current home, restoring missing files.
Stage Vis-owned Python guest modules in the current home, restoring missing files.
(interrupt! k session)Interrupt whatever k's interpreter is running for session and answer
whether the child acknowledged it. A guest parked in a host call cannot take
the interrupt until that call answers, so every host call in flight is failed
here as well — whether or not the child answered in time.
Interrupt whatever `k`'s interpreter is running for `session` and answer whether the child acknowledged it. A guest parked in a host call cannot take the interrupt until that call answers, so every host call in flight is failed here as well — whether or not the child answered in time.
(pending-replies k)Snapshot the replies still owed by k, without starting a worker. These settle
only when the guest replies or exits, even if its host caller was cancelled.
Snapshot the replies still owed by `k`, without starting a worker. These settle only when the guest replies or exits, even if its host caller was cancelled.
(retire-worker! k reason)Stop k's worker after its control plane stopped answering and refuse to
start another under this key until the session is rebuilt or disposed.
Stop `k`'s worker after its control plane stopped answering and refuse to start another under this key until the session is rebuilt or disposed.
(retired? k)True when k was retired and not yet rebuilt or disposed.
True when `k` was retired and not yet rebuilt or disposed.
The worker for Python that belongs to no single session: extension files loading at startup, whose REGISTRATION is the gateway's and not a session's.
The worker for Python that belongs to no single session: extension files loading at startup, whose REGISTRATION is the gateway's and not a session's.
(stop!)Stop every worker. Idempotent; each process is a daemon of this one's lifetime, so an unclean exit leaves nothing behind.
Stop every worker. Idempotent; each process is a daemon of this one's lifetime, so an unclean exit leaves nothing behind.
(stop-worker! k)Stop the worker for k, if there is one. Idempotent. Closing the socket
releases every pending parent call; a child that does not leave promptly is
force-killed so retired sessions cannot accumulate processes.
Stop the worker for `k`, if there is one. Idempotent. Closing the socket releases every pending parent call; a child that does not leave promptly is force-killed so retired sessions cannot accumulate processes.
(worker-live? k)True when k already owns a live worker; never starts one.
True when `k` already owns a live worker; never starts one.
(worker-pids)PIDs of every live session or shared Python worker this process owns.
PIDs of every live session or shared Python worker this process owns.
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 |