Liking cljdoc? Tell your friends :D

com.blockether.vis.internal.sandbox.egress-proxy

Gateway-hosted loopback EGRESS PROXY — the one door a jailed shell child may use to reach the network. Paired with the OS jail's net-off-except-loopback wall (process-jail), it turns vis.yml :network (allowed/denied domains + verb/path :rules) into REAL enforcement for shell children — curl, wget, a script, subprocess — not a cooperative env-var hint.

How the wall + door fit together:

  • The jail denies ALL sockets except TCP to 127.0.0.1:<this-port>, so a raw socket() / /dev/tcp in a script has nowhere to go — the kernel, not politeness, forces every byte through here.
  • http_proxy/https_proxy/ALL_PROXY point the child's HTTP clients at this port, so ordinary tools proxy voluntarily; the wall covers the rest.

What it enforces (no MITM — no new deps, no CA):

  • Plain HTTP (absolute-form proxy request): FULL host + method + path, because the request line is cleartext. GET-not-POST works here.
  • HTTPS (CONNECT host:443): HOST allow/deny always; FULL method + path too when a MITM capability is supplied and the policy asks for it (:mitm?) — the proxy terminates the child's TLS with an ephemeral per-host leaf cert (see internal.tls-mitm), reads the real verb/path, then re-encrypts to the real upstream (whose real cert it still validates). Without MITM it is a raw byte tunnel (verb opaque) — the documented CONNECT-only ceiling.

The policy is a plain VALUE (per session), read fresh per connection via policy-fn so /reload + config edits take effect with no restart. One request per upstream connection (we force Connection: close), so HTTP keep-alive can't smuggle a second, unfiltered verb onto an already-approved socket.

Gateway-hosted loopback EGRESS PROXY — the one door a jailed shell child may use
to reach the network. Paired with the OS jail's *net-off-except-loopback* wall
(`process-jail`), it turns vis.yml `:network` (allowed/denied domains + verb/path
`:rules`) into REAL enforcement for shell children — `curl`, `wget`, a script,
`subprocess` — not a cooperative env-var hint.

How the wall + door fit together:
  - The jail denies ALL sockets except TCP to `127.0.0.1:<this-port>`, so a raw
    `socket()` / `/dev/tcp` in a script has nowhere to go — the kernel, not
    politeness, forces every byte through here.
  - `http_proxy`/`https_proxy`/`ALL_PROXY` point the child's HTTP clients at this
    port, so ordinary tools proxy voluntarily; the wall covers the rest.

What it enforces (no MITM — no new deps, no CA):
  - Plain HTTP  (absolute-form proxy request): FULL host + method + path, because
    the request line is cleartext. GET-not-POST works here.
  - HTTPS (`CONNECT host:443`): HOST allow/deny always; FULL method + path too when
    a MITM capability is supplied and the policy asks for it (`:mitm?`) — the proxy
    terminates the child's TLS with an ephemeral per-host leaf cert (see
    `internal.tls-mitm`), reads the real verb/path, then re-encrypts to the real
    upstream (whose real cert it still validates). Without MITM it is a raw byte
    tunnel (verb opaque) — the documented CONNECT-only ceiling.

The policy is a plain VALUE (per session), read fresh per connection via `policy-fn`
so `/reload` + config edits take effect with no restart. One request per upstream
connection (we force `Connection: close`), so HTTP keep-alive can't smuggle a second,
unfiltered verb onto an already-approved socket.
raw docstring

com.blockether.vis.internal.sandbox.gateway

Gateway-lifecycle SANDBOX CAPABILITY: ONE shared loopback egress proxy and ONE ephemeral MITM CA for the WHOLE daemon, keyed PER SESSION.

Why shared, not per-session (turn 32): the gateway is multi-tenant — many clients/sessions hit one daemon. A per-session proxy+CA thrashes listeners and mints a fresh CA per session; worse, every child would carry a different trust root. Instead there is ONE listener and ONE CA (the "same certs" property — every child trusts the same root), plus a REGISTRY mapping a per-session TOKEN → that session's live policy fn.

How a connection is attributed to a session: the jailed child's proxy env carries its unguessable token in the proxy URL userinfo (http://<token>@127.0.0.1:<port>); curl/git/requests/… send it back as Proxy-Authorization: Basic base64(<token>:); the proxy hands the token to resolve-policy, which looks up the registry.

FAIL-CLOSED: a request whose token is missing or not registered to a LIVE session is DENIED (a :deny-all? sentinel policy) — the shared door never serves a policy it cannot attribute. The token is a random UUID, so one session cannot reach another's (broader) policy by guessing.

Lazy: the proxy listener and the CA keygen happen only on first ensure-proxy! / ensure-ca! — a gateway that never jails a shell child opens neither.

Gateway-lifecycle SANDBOX CAPABILITY: ONE shared loopback egress proxy and ONE
ephemeral MITM CA for the WHOLE daemon, keyed PER SESSION.

Why shared, not per-session (turn 32): the gateway is multi-tenant — many
clients/sessions hit one daemon. A per-session proxy+CA thrashes listeners and
mints a fresh CA per session; worse, every child would carry a different trust
root. Instead there is ONE listener and ONE CA (the "same certs" property —
every child trusts the same root), plus a REGISTRY mapping a per-session TOKEN →
that session's live policy fn.

How a connection is attributed to a session: the jailed child's proxy env carries
its unguessable token in the proxy URL userinfo (`http://<token>@127.0.0.1:<port>`);
curl/git/requests/… send it back as `Proxy-Authorization: Basic base64(<token>:)`;
the proxy hands the token to `resolve-policy`, which looks up the registry.

FAIL-CLOSED: a request whose token is missing or not registered to a LIVE session
is DENIED (a `:deny-all?` sentinel policy) — the shared door never serves a policy
it cannot attribute. The token is a random UUID, so one session cannot reach
another's (broader) policy by guessing.

Lazy: the proxy listener and the CA keygen happen only on first `ensure-proxy!` /
`ensure-ca!` — a gateway that never jails a shell child opens neither.
raw docstring

com.blockether.vis.internal.sandbox.jail

OS-level process CONTAINMENT — the 'jail' — that wraps the shell executors' argv so an allowed child is physically confined to the session workspace roots and, when network is off, cannot open a socket. This is a real containment boundary — not a cooperative name/argv check, which can be walked around since argv[0] is bash and the real binary hides inside the -lc string; the jail constrains what the child can DO once it runs, regardless of what a script inside it tries (curl, python -c, /dev/tcp — all hit the same wall).

POLICY, NOT GUARDS. The jail is driven by a declarative policy compiled from vis.yml + the LIVE session roots, not by hand-written guard functions. The policy is a plain VALUE passed per spawn (never a process-global singleton, so many concurrent sessions in one gateway never stomp each other). Its shape:

{:roots-fn (fn [] [root-strings]) ; live session RW roots, re-read/spawn :net-enabled? <bool> ; whole shell-child network on/off :allow-read-write [<path> …] ; full read+write grant :deny-write [<path> …] ; protect within writable (deny wins) :allow-read [<path> …] ; additional read-only paths :deny-read [<path> …] ; protect a read region (deny wins) :deny-exec [<path> …] ; readable but never executable :keychain? <bool> ; the OS credential store is reachable :inbound-ports [<int> …] ; ports a child may ACCEPT on from ; other hosts (loopback is always open) :env-values {<NAME> <value>} ; RESOLVED project env (.env + ; environment:) with ONE call's own ; env delta merged on, per spawn :env-removals #{<NAME> …} ; names THAT call asked to UNSET :inherit-host-env? <bool>} ; jail.environment: inherit — the child ; also keeps the operator's ambient env

The filesystem model mirrors Anthropic's sandbox-runtime:

  • WRITE is allow-only: denied everywhere except the session roots + tmp + :allow-read-write; :deny-write wins.
  • READ is default-deny here (workspace-focused, stronger than srt's read-everywhere default): system code/config + RW paths + :allow-read are readable; :deny-read wins.

This namespace owns WHAT a session's child may do: it turns the session's configuration, live roots, proxy endpoint and call environment into one platform-neutral policy value and the complete child environment. HOW the operating system enforces that value belongs to com.blockether/vis-python-runtime (spawn-process! with :policy): the per-platform enforcement, the already-confined marker and the refusal on a host that cannot enforce all live there, beside the process launcher, so no enforcement text is assembled here.

OS-level process CONTAINMENT — the 'jail' — that wraps the shell executors'
argv so an allowed child is physically confined to the session workspace roots
and, when network is off, cannot open a socket. This is a real containment
boundary — not a cooperative name/argv check, which can be walked around since
argv[0] is `bash` and the real binary hides inside the `-lc` string; the jail
constrains what the child can DO once it runs, regardless of what a script
inside it tries (curl, python -c, /dev/tcp — all hit the same wall).

POLICY, NOT GUARDS. The jail is driven by a declarative *policy* compiled from
vis.yml + the LIVE session roots, not by hand-written guard functions. The
policy is a plain VALUE passed per spawn (never a process-global singleton, so
many concurrent sessions in one gateway never stomp each other). Its shape:

  {:roots-fn     (fn [] [root-strings])  ; live session RW roots, re-read/spawn
   :net-enabled? <bool>                  ; whole shell-child network on/off
   :allow-read-write [<path> …]           ; full read+write grant
   :deny-write       [<path> …]           ; protect within writable (deny wins)
   :allow-read       [<path> …]           ; additional read-only paths
   :deny-read        [<path> …]           ; protect a read region (deny wins)
   :deny-exec        [<path> …]           ; readable but never executable
   :keychain?        <bool>               ; the OS credential store is reachable
   :inbound-ports    [<int> …]            ; ports a child may ACCEPT on from
                                          ; other hosts (loopback is always open)
   :env-values       {<NAME> <value>}     ; RESOLVED project env (`.env` +
                                          ; `environment:`) with ONE call's own
                                          ; `env` delta merged on, per spawn
   :env-removals     #{<NAME> …}          ; names THAT call asked to UNSET
   :inherit-host-env? <bool>}             ; `jail.environment: inherit` — the child
                                          ; also keeps the operator's ambient env

The filesystem model mirrors Anthropic's sandbox-runtime:
  - WRITE is allow-only: denied everywhere except the session roots + tmp +
    `:allow-read-write`; `:deny-write` wins.
  - READ is default-deny here (workspace-focused, stronger than srt's
    read-everywhere default): system code/config + RW paths + `:allow-read`
    are readable; `:deny-read` wins.

This namespace owns WHAT a session's child may do: it turns the session's
configuration, live roots, proxy endpoint and call environment into one
platform-neutral policy value and the complete child environment. HOW the
operating system enforces that value belongs to `com.blockether/vis-python-runtime`
(`spawn-process!` with `:policy`): the per-platform enforcement, the
already-confined marker and the refusal on a host that cannot enforce all live
there, beside the process launcher, so no enforcement text is assembled here.
raw docstring

com.blockether.vis.internal.sandbox.policy

Canonical immutable security-policy snapshots and their model-facing view.

A snapshot is created once for a root environment, inherited unchanged by child environments, and replaced only by an explicit environment rebuild. Enforcement and context both derive from this value.

Canonical immutable security-policy snapshots and their model-facing view.

A snapshot is created once for a root environment, inherited unchanged by
child environments, and replaced only by an explicit environment rebuild.
Enforcement and context both derive from this value.
raw docstring

com.blockether.vis.internal.sandbox.tls-mitm

Ephemeral CA + per-host leaf minting for the egress proxy's TLS-terminating (MITM) tier — the piece that gives a jailed shell child GET-not-POST fidelity over HTTPS, matching what the interpreter method-guard already does pre-TLS.

Why it exists: a plain CONNECT proxy only sees CONNECT host:443 — the method and path live inside the TLS the proxy never opens, so HTTPS verb/path is opaque. To read them the proxy must TERMINATE the child's TLS: present the child a leaf cert for the requested host, decrypt, inspect method+path, then re-encrypt to the real upstream (whose real cert the proxy still validates).

Trust model:

  • The CA is EPHEMERAL and per-session — born in this JVM, never written to the host trust store. Its cert PEM is written to a temp file whose path is injected into the jailed child's trust env (CURL_CA_BUNDLE/SSL_CERT_FILE /REQUESTS_CA_BUNDLE/NODE_EXTRA_CA_CERTS/GIT_SSL_CAINFO). Only children inside the jail ever see or trust it.
  • Upstream (proxy -> real server) uses the SYSTEM trust store by default, so the real server's real certificate is still validated end to end.

No JCA provider is registered globally: bcpkix's Jca* builders use the default platform signer (SHA256withRSA via SunRsaSign), which keeps this native-image friendly and side-effect free.

Ephemeral CA + per-host leaf minting for the egress proxy's TLS-terminating
(MITM) tier — the piece that gives a jailed shell child GET-not-POST fidelity
over HTTPS, matching what the interpreter method-guard already does pre-TLS.

Why it exists: a plain CONNECT proxy only sees `CONNECT host:443` — the method
and path live inside the TLS the proxy never opens, so HTTPS verb/path is
opaque. To read them the proxy must TERMINATE the child's TLS: present the
child a leaf cert for the requested host, decrypt, inspect method+path, then
re-encrypt to the real upstream (whose real cert the proxy still validates).

Trust model:
  - The CA is EPHEMERAL and per-session — born in this JVM, never written to
    the host trust store. Its cert PEM is written to a temp file whose path is
    injected into the jailed child's trust env (`CURL_CA_BUNDLE`/`SSL_CERT_FILE`
    /`REQUESTS_CA_BUNDLE`/`NODE_EXTRA_CA_CERTS`/`GIT_SSL_CAINFO`). Only children
    inside the jail ever see or trust it.
  - Upstream (proxy -> real server) uses the SYSTEM trust store by default, so
    the real server's real certificate is still validated end to end.

No JCA provider is registered globally: bcpkix's Jca* builders use the default
platform signer (SHA256withRSA via SunRsaSign), which keeps this native-image
friendly and side-effect free.
raw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close