Liking cljdoc? Tell your friends :D

vaelii.host.serve

Headless EDN-over-HTTP daemon: one JVM owns one KB and serves it to remote clients (vaelii.host.client). A thin reitit-ring + jetty layer over vaelii.core, the network dual of the in-process API.

Wire format is EDN. A sentence is a symbol s-expression — (dog Muffet), ?x, (genl dog animal) — which EDN round-trips losslessly; JSON would mangle the symbols. The body of every call is {:op <keyword> :args [...]}, and the reply is {:ok true :result …} or {:ok false :error "…"}. EDN is read with clojure.edn/read-string (never clojure.core/read-string), so an untrusted body cannot evaluate code — EDN has no reader-eval.

A refusal's :type is a plain keyword — :body-too-large, :not-edn, :cross-origin, :bad-host — and so is the :type an engine ex-info carries through. The protocol is what a client written against another build discriminates on, so it cannot be qualified by the namespace that happens to serve it: a ::-qualified keyword names this namespace, and a client matching on it would be matching on where the daemon's code lives.

The daemon is the single writer (docs/storage.md, the single-writer contract): it owns the one process allowed to mutate the store, so it serializes every op through one monitor. Concurrent client writes therefore apply one at a time and cannot interleave; reads pay the same lock, which is conservative but keeps the contract simple.

Only the allowlisted ops are reachable (ops). Each is a vaelii.core fn with the KB supplied by the daemon — the client sends only the op and the remaining args — so no client can reach an arbitrary var. Sentex records in a result are projected to plain maps before they hit the wire (the sentex-map contract), so the client reads them back without the impl record class.

The change feed is the one thing that is not a vaelii.core fn (feed-ops), and it is a table of its own for that reason: core/watch takes a callback, so what a remote caller holds open instead is a subscription with a cursor — :watch, :poll, :unwatch, :watchers, over the per-handler registry app builds (vaelii.host.subscribe, docs/feed.md). A :poll that waits runs outside the monitor; everything else about them is an ordinary EDN op.

One shared bearer token authenticates the caller. With VAELII_API_TOKEN set (guard/api-token), every request presents Authorization: Bearer <token> or is answered 401 with a WWW-Authenticate: Bearer challenge; GET /health is the one route that answers without it. One token for the process, not a session and not an identity — per-caller identity is a reverse proxy's job, and this is the check that has to exist below it. Binding anything but loopback requires a token (-main refuses to start otherwise); on the loopback default it is optional, and a daemon without one is drivable by every process on the machine.

A per-principal token table replaces the shared token when VAELII_TOKEN_TABLE names one (guard/token-table): each token carries a grant naming a principal and the contexts it writes, and a write op lands only there, recorded under that principal (bound-write). Reads stay open to every token in the table.

vaelii.host.guard covers what a token does not, and matters most on the open loopback daemon: POST /op requires Content-Type: application/edn, refuses a cross-origin Origin, and answers only to a Host naming the interface it was started on. Together those stop a page the operator happens to visit from driving the KB over loopback — which binding to loopback alone does not.

Headless EDN-over-HTTP daemon: one JVM owns one KB and serves it to remote clients
(`vaelii.host.client`).  A thin reitit-ring + jetty layer over `vaelii.core`, the
network dual of the in-process API.

**Wire format is EDN.**  A sentence is a symbol s-expression — `(dog Muffet)`, `?x`,
`(genl dog animal)` — which EDN round-trips losslessly; JSON would mangle the symbols.
The body of every call is `{:op <keyword> :args [...]}`, and the reply is
`{:ok true :result …}` or `{:ok false :error "…"}`.  EDN is read with
`clojure.edn/read-string` (never `clojure.core/read-string`), so an untrusted body
cannot evaluate code — EDN has no reader-eval.

**A refusal's `:type` is a plain keyword** — `:body-too-large`, `:not-edn`,
`:cross-origin`, `:bad-host` — and so is the `:type` an engine `ex-info` carries
through.  The protocol is what a client written against another build discriminates
on, so it cannot be qualified by the namespace that happens to serve it: a
`::`-qualified keyword names *this* namespace, and a client matching on it would be
matching on where the daemon's code lives.

**The daemon is the single writer** (docs/storage.md, the single-writer contract): it
owns the one process allowed to mutate the store, so it serializes every op through
one monitor.  Concurrent client writes therefore apply one at a time and cannot
interleave; reads pay the same lock, which is conservative but keeps the contract
simple.

**Only the allowlisted ops are reachable** (`ops`).  Each is a `vaelii.core` fn with
the KB supplied by the daemon — the client sends only the op and the remaining args —
so no client can reach an arbitrary var.  Sentex records in a result are projected to
plain maps before they hit the wire (the `sentex`-map contract), so the client reads
them back without the `impl` record class.

**The change feed is the one thing that is not a `vaelii.core` fn** (`feed-ops`), and
it is a table of its own for that reason: `core/watch` takes a callback, so what a
remote caller holds open instead is a subscription with a **cursor** — `:watch`,
`:poll`, `:unwatch`, `:watchers`, over the per-handler registry `app` builds
(`vaelii.host.subscribe`, docs/feed.md).  A `:poll` that waits runs **outside** the
monitor; everything else about them is an ordinary EDN op.

**One shared bearer token authenticates the caller.**  With `VAELII_API_TOKEN` set
(`guard/api-token`), every request presents `Authorization: Bearer <token>` or is
answered 401 with a `WWW-Authenticate: Bearer` challenge; `GET /health` is the one
route that answers without it.  One token for the process, not a session and not an
identity — per-caller identity is a reverse proxy's job, and this is the check that
has to exist below it.  Binding anything but loopback **requires** a token (`-main`
refuses to start otherwise); on the loopback default it is optional, and a daemon
without one is drivable by every process on the machine.

**A per-principal token table replaces the shared token** when `VAELII_TOKEN_TABLE`
names one (`guard/token-table`): each token carries a grant naming a principal and the
contexts it writes, and a write op lands only there, recorded under that principal
(`bound-write`).  Reads stay open to every token in the table.

`vaelii.host.guard` covers what a token does not, and matters most on the open
loopback daemon: `POST /op` requires `Content-Type: application/edn`, refuses a
cross-origin `Origin`, and answers only to a `Host` naming the interface it was
started on.  Together those stop a page the operator happens to visit from driving
the KB over loopback — which binding to loopback alone does not.
raw docstring

*served-kbs*clj

nil, or a fn from a string a request sends to the KB this server serves under that name, or nil when it serves none. The browser binds it to its catalog around handle-op (vaelii.browser.web/op-post); the daemon serves one KB and leaves it nil.

nil, or a fn from a string a request sends to the KB this server serves under that
name, or nil when it serves none.  The browser binds it to its catalog around
`handle-op` (`vaelii.browser.web/op-post`); the daemon serves one KB and leaves it nil.
sourceraw docstring

-mainclj

(-main & args)

Run the daemon in the foreground. Args: [port [dir]] [--listen ADDR] [--starter], in any order. dir opens the store there under the backend its files were written by, or a new durable :disk-log store when it holds none (dir-backend), recovered on open; with no dir the KB is in-memory. --starter loads the shipped starter schema (vaelii.host.starter/load-into) before the port is bound.

lein run -m vaelii.host.serve 4200 /var/lib/vaelii lein run -m vaelii.host.serve 4200 --starter ; in memory, with the schema lein run -m vaelii.host.serve 4200 /var/lib/vaelii --listen 0.0.0.0 ; opt-in

It binds loopback unless --listen says otherwise (loopback); what it binds and what it requires are one decision (auth-posture):

  • --listen names a non-loopback address ⇒ VAELII_API_TOKEN is required; without one it is a stderr line and exit 2, a code of its own.
  • Loopback, the default, ⇒ the token is used when set, and its absence is a startup warning.

Naming an address also drops the Host allowlist (guard/allowed-hosts); set VAELII_ALLOWED_HOSTS to keep the check (host-posture warns at startup). A --listen with no address, an unknown flag, or a stray argument is one line and exit 1.

Run the daemon in the foreground.  Args: `[port [dir]] [--listen ADDR] [--starter]`, in any
order.  `dir` opens the store there under the backend its files were written by, or a new
durable `:disk-log` store when it holds none (`dir-backend`), recovered on open; with no
`dir` the KB is in-memory.  `--starter` loads the shipped starter schema
(`vaelii.host.starter/load-into`) before the port is bound.

  lein run -m vaelii.host.serve 4200 /var/lib/vaelii
  lein run -m vaelii.host.serve 4200 --starter                          ; in memory, with the schema
  lein run -m vaelii.host.serve 4200 /var/lib/vaelii --listen 0.0.0.0   ; opt-in

It binds **loopback** unless `--listen` says otherwise (`loopback`); what it binds and what
it requires are one decision (`auth-posture`):

- `--listen` names a **non-loopback** address ⇒ `VAELII_API_TOKEN` is **required**;
  without one it is a stderr line and exit **2**, a code of its own.
- **Loopback**, the default, ⇒ the token is used when set, and its absence is a startup
  warning.

Naming an address also drops the `Host` allowlist (`guard/allowed-hosts`); set
`VAELII_ALLOWED_HOSTS` to keep the check (`host-posture` warns at startup).  A `--listen`
with no address, an unknown flag, or a stray argument is one line and exit 1.
sourceraw docstring

appclj

(app kb)
(app kb opts)

The ring handler for a KB: pure request -> response, so it is tested without a socket. One monitor per handler serializes the ops (the single-writer contract).

:host names the interface this handler is served on, which fixes the Host values it answers to (guard/allowed-hosts). :token is the shared bearer token every request must present; omitted, it is VAELII_API_TOKEN (guard/api-token), and an explicit nil serves open. :token-table is the per-principal token table, a map or the path of an EDN file (guard/check-token-table); omitted with :token omitted too, it is VAELII_TOKEN_TABLE's (guard/token-table); omitted beside a :token, or an explicit nil, it is none. Set, it replaces :token: a write op lands only in the contexts that token's grant names (bound-write). A table that does not check is refused (:bad-token-table).

The ring handler for a KB: pure `request -> response`, so it is tested without a socket.
One monitor per handler serializes the ops (the single-writer contract).

`:host` names the interface this handler is served on, which fixes the `Host` values it
answers to (`guard/allowed-hosts`).  `:token` is the shared bearer token every request
must present; omitted, it is `VAELII_API_TOKEN` (`guard/api-token`), and an explicit nil
serves **open**.  `:token-table` is the per-principal token table, a map or the path of
an EDN file (`guard/check-token-table`); omitted with `:token` omitted too, it is
`VAELII_TOKEN_TABLE`'s (`guard/token-table`); omitted beside a `:token`, or an explicit
nil, it is none.  Set, it replaces `:token`: a write op lands only in the contexts that
token's grant names (`bound-write`).  A table that does not check is refused
(`:bad-token-table`).
sourceraw docstring

feed-opsclj

The change-feed operations, keyed by op keyword: (fn [ctx args]) over a ctx of {:kb :registry :monitor}, plus an optional :admit (handle-op's, which :watch calls inside the monitor before it registers anything). Each says its own relationship to the write monitor. :watch and :unwatch take it: they are instantaneous, and taking it makes the boundary exact (every settle that finished before a :watch returned is outside the feed, every one that starts after is inside). :poll must not, since a parked long poll would block every writer. :watchers does not either: it reads the registry and expires what has expired, and should not queue behind a bulk load.

The change-feed operations, keyed by op keyword: `(fn [ctx args])` over a `ctx` of
`{:kb :registry :monitor}`, plus an optional `:admit` (`handle-op`'s, which `:watch` calls
inside the monitor before it registers anything).  Each says its own relationship to the
write monitor.  `:watch` and `:unwatch` take it: they are instantaneous, and taking it
makes the boundary exact (every settle that finished before a `:watch` returned is outside
the feed, every one that starts after is inside).  `:poll` **must not**, since a parked
long poll would block every writer.  `:watchers` does not either: it reads the registry and
expires what has expired, and should not queue behind a bulk load.
sourceraw docstring

handle-opclj

(handle-op kb registry monitor req)
(handle-op kb registry monitor req admit)
(handle-op kb registry monitor req admit host)

Run one {:op :args} request under the write lock and answer with EDN. The op runs on a thread of its own (off-worker), so stopping the server interrupts no KB write. Public because vaelii.browser.web answers POST /op by calling this with its active KB, its own subscription registry and its own write monitor (docs/operations.md).

Two guards run before the body is read: guard/edn-body? forces a CORS preflight this daemon cannot answer, and guard/same-origin? refuses a browser that sent someone else's origin (vaelii.host.guard). Two tables are looked up in order: the vaelii.core allowlist (ops, under the monitor), then the change-feed ops (feed-ops), which decide about the monitor themselves. A write op (write-ops) is held inside the monitor to the grant the request's token carries (guard/wrap-token-table) by bound-write; a request with no grant may not name the provenance :principal.

admit is nil, or a no-argument fn called inside the monitor before an engine op and before :watch registers: nil lets the op run, {:status :type :error} refuses it. host is nil, or {:ops :refuse} from a process serving this protocol beside something else: :ops maps op keywords to fns of the op's args that the host answers itself, run outside the monitor before both tables; :refuse is a no-argument fn asked before any other known op, nil to let it run or {:status :ok false :type :error} to refuse it.

Run one `{:op :args}` request under the write lock and answer with EDN.  The op runs on a
thread of its own (`off-worker`), so stopping the server interrupts no KB write.  Public
because `vaelii.browser.web` answers `POST /op` by calling this with its active KB, its
own subscription registry and its own write monitor (docs/operations.md).

Two guards run before the body is read: `guard/edn-body?` forces a CORS preflight this
daemon cannot answer, and `guard/same-origin?` refuses a browser that sent someone else's
origin (`vaelii.host.guard`).  Two tables are looked up in order: the `vaelii.core`
allowlist (`ops`, under the monitor), then the change-feed ops (`feed-ops`), which decide
about the monitor themselves.  A write op (`write-ops`) is held inside the monitor to the
grant the request's token carries (`guard/wrap-token-table`) by `bound-write`; a request
with no grant may not name the provenance `:principal`.

`admit` is nil, or a no-argument fn called **inside** the monitor before an engine op and
before `:watch` registers: nil lets the op run, `{:status :type :error}` refuses it.
`host` is nil, or `{:ops :refuse}` from a process serving this protocol beside something
else: `:ops` maps op keywords to fns of the op's args that the host answers itself, run
outside the monitor before both tables; `:refuse` is a no-argument fn asked before any
other known op, nil to let it run or `{:status :ok false :type :error}` to refuse it.
sourceraw docstring

http-threadsclj

How many worker threads the daemon's HTTP server runs.

Stated rather than defaulted, because it is one half of a pair: a parked long poll holds one of these for the length of its wait, so subscribe/max-parked has to stay well under it or the feature that exists for liveness becomes the thing that stalls the daemon. Left implicit the two numbers were 50 and 64, the wrong way round, and nothing said so — 55 parked polls took /health from 62 ms to 26 s. serve_test asserts the relationship.

How many worker threads the daemon's HTTP server runs.

Stated rather than defaulted, because it is one half of a pair: a parked long poll
holds one of these for the length of its wait, so `subscribe/max-parked` has to stay
well under it or the feature that exists for liveness becomes the thing that stalls
the daemon.  Left implicit the two numbers were 50 and 64, the wrong way round, and
nothing said so — 55 parked polls took `/health` from 62 ms to 26 s.  `serve_test`
asserts the relationship.
sourceraw docstring

kbless-opsclj

The ops whose vaelii.core fn takes no KB (op* above). Held as data rather than left implicit in the closures, because a generator reading this table has to know whether an op's first vaelii.core parameter is the KB the daemon supplies or an argument the caller sends: vaelii.host.client's wrappers (arity for arity) read it here, and so does any caller that builds a call schema from ops.

The ops whose `vaelii.core` fn takes no KB (`op*` above).  Held as data rather than
left implicit in the closures, because a generator reading this table has to know
whether an op's first `vaelii.core` parameter is the KB the daemon supplies or an
argument the caller sends: `vaelii.host.client`'s wrappers (arity for arity) read it
here, and so does any caller that builds a call schema from `ops`.
sourceraw docstring

op-namesclj

Every op keyword this daemon answers, sorted — the vaelii.core allowlist and the daemon's own together. What an :unknown-op refusal hands back, so a caller discovering the surface sees one list rather than the larger half of two.

Every op keyword this daemon answers, sorted — the `vaelii.core` allowlist and the
daemon's own together.  What an `:unknown-op` refusal hands back, so a caller
discovering the surface sees one list rather than the larger half of two.
sourceraw docstring

opsclj

The reachable operations, keyed by op keyword: reads, writes and introspection, the working set a remote caller needs. A vaelii.core fn that takes a KB is either a row here or a row of docs/operations.md's table of fns that are not ops, whose rule decides which; serve_parity_test fails on a fn in neither. Every entry is wrapped by bounded and by write-bounded, each a no-op for an op its table does not name.

The reachable operations, keyed by op keyword: reads, writes and introspection, the
working set a remote caller needs.  A `vaelii.core` fn that takes a KB is either a row here
or a row of docs/operations.md's table of fns that are not ops, whose rule decides which;
`serve_parity_test` fails on a fn in neither.  Every entry is wrapped by `bounded` and by
`write-bounded`, each a no-op for an op its table does not name.
sourceraw docstring

portclj

(port server)

The actual TCP port a started Server is listening on — the ephemeral one when it was started with :port 0, read off its first connector.

The actual TCP port a started `Server` is listening on — the ephemeral one when it
was started with `:port 0`, read off its first connector.
sourceraw docstring

search-boundsclj

Which of the two ceilings apply to which op, and under which key the op reads it. A read that expands rules runs on the daemon's single write monitor, so every other request queues behind it. The clock ceiling, :max-ms (config/max-query-ms), bounds every entry point that takes a clock. :query, :query?, :query-status and :argue take no clock (query-opt-keys has no :max-ms), so the depth ceiling, :max-depth (config/max-query-depth), bounds them. An anytime budget map is :max-ms and takes no default of its own.

The table holds the search bounds of reads, and no write's bound. A read with no dial of its own (:sentexes-matching) is not on it; the four backward-search entry points (core/ask-opt-keys, core/prove-opt-keys) are held to the clock ceiling. Eight writes name a bound this table does not hold: :assert, :try-assert, :assert-many, :assert-rule and :forward-chain read :max-depth, :max-term-depth and :max-derivations, and :edit, :edit-with-consequences and :preview read them off each batch entry's opts. All three bound the chaining fixpoint (chain/default-chain-opts) on the same monitor. write-bounds holds :max-ms to the write ceiling for all but :forward-chain and :preview (no clock). docs/operations.md.

Which of the two ceilings apply to which op, and under which key the op reads it.  A read
that expands rules runs on the daemon's **single write monitor**, so every other request
queues behind it.  The clock ceiling, `:max-ms` (`config/max-query-ms`), bounds every
entry point that takes a clock.  `:query`, `:query?`, `:query-status` and `:argue` take
no clock (`query-opt-keys` has no `:max-ms`), so the depth ceiling, `:max-depth`
(`config/max-query-depth`), bounds them.  An anytime budget map is `:max-ms` and takes
no default of its own.

**The table holds the search bounds of reads, and no write's bound.**  A read with no dial
of its own (`:sentexes-matching`) is not on it; the four backward-search entry points
(`core/ask-opt-keys`, `core/prove-opt-keys`) are held to the clock ceiling.  **Eight writes
name a bound this table does not hold**: `:assert`, `:try-assert`, `:assert-many`,
`:assert-rule` and `:forward-chain` read `:max-depth`, `:max-term-depth` and
`:max-derivations`, and `:edit`, `:edit-with-consequences` and `:preview` read them off
each batch entry's opts.  All three bound the chaining fixpoint
(`chain/default-chain-opts`) on the same monitor.  `write-bounds` holds `:max-ms` to the
write ceiling for all but `:forward-chain` and `:preview` (no clock).  docs/operations.md.
sourceraw docstring

startclj

(start kb {:keys [port host] :or {port 4200 host loopback} :as opts})

Start the daemon over kb and return the running jetty Server (:join? false, so the caller controls its lifetime). :port 0 binds an ephemeral port; read it with port. :host defaults to loopback; pass an address ("0.0.0.0") to bind publicly (see loopback). :token and :token-table are app's, each forwarded only when the key is there. An address with neither a token nor a table is refused (guard/require-token!), and so is a table that does not check (:bad-token-table), both before anything binds. .stop runs stop-ops: an op not yet in the monitor answers 503 :stopping, and the stop returns once the op in it finishes and the daemon's feed listeners are off kb.

Start the daemon over `kb` and return the running jetty `Server` (`:join? false`, so the
caller controls its lifetime).  `:port 0` binds an ephemeral port; read it with `port`.
`:host` defaults to loopback; pass an address (`"0.0.0.0"`) to bind publicly (see
`loopback`).  `:token` and `:token-table` are `app`'s, each forwarded only when the key is
there.  An address with neither a token nor a table is refused (`guard/require-token!`),
and so is a table that does not check (`:bad-token-table`), both before anything binds.
`.stop` runs `stop-ops`: an op not yet in the monitor answers 503 `:stopping`, and the
stop returns once the op in it finishes and the daemon's feed listeners are off `kb`.
sourceraw docstring

write-boundsclj

The chaining writes the daemon holds to config/max-write-ms, and where each one's option map sits: :option-arity is the argument count that ends in the map, and :fill the vaelii.core defaults between the last argument a caller may send and the map.

A chaining write runs on the daemon's single write monitor, so an operator may hold it to a clock as the reads search-bounds names are: a request may name a smaller :max-ms and is refused (:over-ceiling) for a larger one, and an absent one is the ceiling. A write the clock ends is rolled back (docs/inference.md, "Stopping a run").

The chaining writes the daemon holds to `config/max-write-ms`, and where each one's
option map sits: `:option-arity` is the argument count that ends in the map, and `:fill`
the `vaelii.core` defaults between the last argument a caller may send and the map.

A chaining write runs on the daemon's single write monitor, so an operator may hold it
to a clock as the reads `search-bounds` names are: a request may name a smaller
`:max-ms` and is refused (`:over-ceiling`) for a larger one, and an absent one is the
ceiling.  A write the clock ends is rolled back (docs/inference.md, "Stopping a run").
sourceraw docstring

write-opsclj

The ops that store, remove or annotate a sentex a request names, and :export, which writes the whole KB to the server's host: a token-table grant bounds them (bound-write). The other ops read, store nothing (:check, :check-edit, :preview), or store what the request does not name: :forward-chain what the rules derive, :abduce its hypotheses in a scratch context (docs/operations.md, "The token table").

The ops that store, remove or annotate a sentex a request names, and `:export`, which
writes the whole KB to the server's host: a token-table grant bounds them
(`bound-write`).  The other ops read, store nothing (`:check`, `:check-edit`,
`:preview`), or store what the request does not name: `:forward-chain` what the rules
derive, `:abduce` its hypotheses in a scratch context (docs/operations.md, "The token
table").
sourceraw 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