Base64, portable.
Here rather than in koine.json because it is a host seam, not a pure
transform: the JVM has java.util.Base64 and cljgo has cljg.security, and
hand-rolling it in clojure.core would be slower and no more portable than
either.
Standard base64 with padding (RFC 4648 §4) — NOT the URL-safe alphabet.
That is the alphabet MCP image / blob content blocks use, which is what
this exists for.
Strings are treated as UTF-8 both ways. encode also accepts a byte array
(from koine.fs/read-bytes), which is the case that matters for binary
content; decode returns a string and decode-bytes the raw bytes.
Unblocked on cljgo by ADR 0110.
Base64, portable. Here rather than in `koine.json` because it is a host seam, not a pure transform: the JVM has `java.util.Base64` and cljgo has `cljg.security`, and hand-rolling it in `clojure.core` would be slower and no more portable than either. Standard base64 with padding (RFC 4648 §4) — NOT the URL-safe alphabet. That is the alphabet MCP `image` / `blob` content blocks use, which is what this exists for. Strings are treated as UTF-8 both ways. `encode` also accepts a byte array (from `koine.fs/read-bytes`), which is the case that matters for binary content; `decode` returns a string and `decode-bytes` the raw bytes. Unblocked on cljgo by ADR 0110.
Environment variables, portable.
Values read here are frequently secrets (API keys expanded into MCP headers). Nothing in this namespace logs, prints or caches a value.
Environment variables, portable. Values read here are frequently secrets (API keys expanded into MCP headers). Nothing in this namespace logs, prints or caches a value.
Filesystem, portable.
Note file-seq is a trap: it resolves on BOTH hosts but takes different
argument types — a java.io.File on the JVM, a string path on cljgo. A name
that resolves everywhere is not thereby portable, which is why directory
traversal lives behind this seam.
Filesystem, portable. Note `file-seq` is a trap: it resolves on BOTH hosts but takes different argument types — a java.io.File on the JVM, a string path on cljgo. A name that resolves everywhere is not thereby portable, which is why directory traversal lives behind this seam.
Outbound HTTP, portable. One request shape, one response shape, every host.
Outbound HTTP, portable. One request shape, one response shape, every host.
JSON for dual-host Clojure.
ENCODE is pure portable Clojure — deliberately NOT delegated to a host
library. Measured on 2026-07-27, Go's encoding/json and JVM
clojure.data.json disagree on 4 of 6 basic payloads (key order, HTML
escaping, float formatting, unicode escaping). Controlling those three
choices IS the whole encoder, so we own it and both hosts agree by
construction.
DECODE is also pure portable Clojure. Delegating it looked free (parsing has no formatting choices to disagree about) but with four hosts it would mean four parsers to keep in agreement, two of which are not even reachable.
JSON for dual-host Clojure. ENCODE is pure portable Clojure — deliberately NOT delegated to a host library. Measured on 2026-07-27, Go's `encoding/json` and JVM `clojure.data.json` disagree on 4 of 6 basic payloads (key order, HTML escaping, float formatting, unicode escaping). Controlling those three choices IS the whole encoder, so we own it and both hosts agree by construction. DECODE is also pure portable Clojure. Delegating it looked free (parsing has no formatting choices to disagree about) but with four hosts it would mean four parsers to keep in agreement, two of which are not even reachable.
Subprocesses, portable.
sh (run-to-completion) works on every host. spawn (a long-lived child
with piped stdin/stdout) is the one MCP stdio transports need, and is the
known gap on cljgo — see the cljgo work item in toolnexus ADR 0009.
Subprocesses, portable. `sh` (run-to-completion) works on every host. `spawn` (a long-lived child with piped stdin/stdout) is the one MCP stdio transports need, and is the known gap on cljgo — see the cljgo work item in toolnexus ADR 0009.
Routing, static files and reverse proxying — as pure handler combinators
over koine.server.
THE DESIGN CONSTRAINT: this file contains zero reader conditionals, on
purpose. Everything here is handler -> handler or data -> handler,
composed ABOVE the seam:
routing is map lookup (plain clojure.core)
static is koine.fs + slurp (already portable)
proxying is koine.http/request (already portable)
timing is koine.time/mono-ms (already portable)
So the namespace inherits four-host support for free and adds no host code.
If a change here seems to need a reader conditional, the design is wrong —
the branch belongs in the seam namespace (koine.server, koine.http,
koine.fs), not in a combinator. (There is a test asserting this file has
none; it greps the source.)
(def app
(route/routes->handler
{[:get "/health"] (fn [_] {:body "ok"})
"/assets/*" (route/static "public")
"/api/*" (route/proxy "http://127.0.0.1:9000")}
{:middleware [(fn [h] (route/wrap-log h println))]}))
(server/serve app {:port 8080})
Handlers speak exactly the koine.server request/response maps — request
{:method :path :headers :body}, response {:status :headers :body} — so
any handler here is a valid serve handler and any serve handler is a
valid route target. Nothing new to learn, nothing new to port.
NESTING. A wildcard match adds two keys to the request map:
:path-info the part of the path AFTER the matched prefix ("/a.css") :route-prefix the prefix that matched ("/assets/")
router, static and forward all read :path-info in preference to
:path, which is what makes them nest: a router inside a router matches on
the remainder, and static/forward mounted under /assets/* see
/a.css, not /assets/a.css. :path is never rewritten, so logging and
the handler's own view of the original URL stay intact.
KNOWN LIMITS, stated rather than faked:
koine.server's contract), so
static serves TEXT faithfully and binary files only as far as the
host's slurp decoding allows. Content types for binary extensions are
still emitted, because getting the header right costs nothing.koine.server hands over the path only, never the query string, so
forward forwards the path and drops any query.koine.fs/exists?/directory? are implemented for the JVM and cljgo
only. static therefore takes :exists?/:directory?/:read
overrides: the DEFAULTS are the koine.fs fns, and a host where those
are unimplemented can supply its own without this file growing a branch.
(The fix belongs in koine/fs.cljc, not here.)Routing, static files and reverse proxying — as pure handler combinators
over `koine.server`.
THE DESIGN CONSTRAINT: this file contains **zero reader conditionals**, on
purpose. Everything here is `handler -> handler` or `data -> handler`,
composed ABOVE the seam:
routing is map lookup (plain clojure.core)
static is `koine.fs` + `slurp` (already portable)
proxying is `koine.http/request` (already portable)
timing is `koine.time/mono-ms` (already portable)
So the namespace inherits four-host support for free and adds no host code.
If a change here seems to need a reader conditional, the design is wrong —
the branch belongs in the seam namespace (`koine.server`, `koine.http`,
`koine.fs`), not in a combinator. (There is a test asserting this file has
none; it greps the source.)
(def app
(route/routes->handler
{[:get "/health"] (fn [_] {:body "ok"})
"/assets/*" (route/static "public")
"/api/*" (route/proxy "http://127.0.0.1:9000")}
{:middleware [(fn [h] (route/wrap-log h println))]}))
(server/serve app {:port 8080})
Handlers speak exactly the `koine.server` request/response maps — request
`{:method :path :headers :body}`, response `{:status :headers :body}` — so
any handler here is a valid `serve` handler and any `serve` handler is a
valid route target. Nothing new to learn, nothing new to port.
NESTING. A wildcard match adds two keys to the request map:
:path-info the part of the path AFTER the matched prefix ("/a.css")
:route-prefix the prefix that matched ("/assets/")
`router`, `static` and `forward` all read `:path-info` in preference to
`:path`, which is what makes them nest: a router inside a router matches on
the remainder, and `static`/`forward` mounted under `/assets/*` see
`/a.css`, not `/assets/a.css`. `:path` is never rewritten, so logging and
the handler's own view of the original URL stay intact.
KNOWN LIMITS, stated rather than faked:
- Bodies are strings on every host (that is `koine.server`'s contract), so
`static` serves TEXT faithfully and binary files only as far as the
host's `slurp` decoding allows. Content types for binary extensions are
still emitted, because getting the header right costs nothing.
- `koine.server` hands over the path only, never the query string, so
`forward` forwards the path and drops any query.
- `koine.fs/exists?`/`directory?` are implemented for the JVM and cljgo
only. `static` therefore takes `:exists?`/`:directory?`/`:read`
overrides: the DEFAULTS are the `koine.fs` fns, and a host where those
are unimplemented can supply its own without this file growing a branch.
(The fix belongs in `koine/fs.cljc`, not here.)Inbound HTTP, portable. One path, one handler fn, every host.
Deliberately minimal. The use case is a JSON-RPC endpoint (toolnexus SPEC §7B/§7C): a request comes in on ONE path, a string body goes in, a string body comes out. There is no routing table, no middleware, no static files, no websockets, no TLS — every one of those multiplies the surface that has to agree across four hosts, and none of them is needed.
(def h (server/serve (fn [req] {:status 200 :body (:body req)})
{:port 0}))
(server/port h) ; => 54321
(server/stop! h) ; => nil, idempotent
handler : request-map -> response-map request {:method :post :path "/" :headers {"content-type" "…"} :body "…"} response {:status 200 :headers {…} :body "…"} ; :status defaults to 200, ; :headers to {}, :body to ""
Normalised across hosts: :method is always a lower-case keyword,
:headers keys are always lower-case strings with a single string value
(first value wins on repeats), :body is always a string — never nil.
Host support (measured 2026-07-27, see the porting notes at the bottom):
| host | serve | :port 0 | stop! | :host |
|---|---|---|---|---|
| jvm | yes | yes | yes | yes |
| cljgo | yes | yes | yes | no |
| glojure | yes | NO | yes | yes |
| let-go | yes | NO | NO | yes |
Where a host cannot do something it throws a named, actionable error at the point of use rather than pretending.
Inbound HTTP, portable. One path, one handler fn, every host.
Deliberately minimal. The use case is a JSON-RPC endpoint (toolnexus
SPEC §7B/§7C): a request comes in on ONE path, a string body goes in, a
string body comes out. There is no routing table, no middleware, no
static files, no websockets, no TLS — every one of those multiplies the
surface that has to agree across four hosts, and none of them is needed.
(def h (server/serve (fn [req] {:status 200 :body (:body req)})
{:port 0}))
(server/port h) ; => 54321
(server/stop! h) ; => nil, idempotent
handler : request-map -> response-map
request {:method :post :path "/" :headers {"content-type" "…"} :body "…"}
response {:status 200 :headers {…} :body "…"} ; :status defaults to 200,
; :headers to {}, :body to ""
Normalised across hosts: `:method` is always a lower-case keyword,
`:headers` keys are always lower-case strings with a single string value
(first value wins on repeats), `:body` is always a string — never nil.
Host support (measured 2026-07-27, see the porting notes at the bottom):
| host | serve | :port 0 | stop! | :host |
|---------|-------|---------|-------|-------|
| jvm | yes | yes | yes | yes |
| cljgo | yes | yes | yes | no |
| glojure | yes | NO | yes | yes |
| let-go | yes | NO | NO | yes |
Where a host cannot do something it throws a named, actionable error at
the point of use rather than pretending.Streaming HTTP responses — Server-Sent Events, portable.
koine.http/request buffers: it hands you the whole body once the server is
done. That is wrong for an LLM, where the point of stream mode is that
tokens surface as they are produced. This namespace is the incremental
counterpart, kept in its own file because the host seams are completely
different from the buffered ones (a buffered client and a streaming client
share a URL and nothing else).
The contract is INCREMENTAL DELIVERY, not "returns the same lines". A host that can only buffer does not get a slow-but-correct fallback here — it throws (rule 2). A fake stream passes every test and then fails in front of a user, which is strictly worse than an honest error.
Measured on 2026-07-27 (150 ms between server events, arrival times relative to the first byte):
| host | streams? | route |
|---|---|---|
| JVM | yes | HttpResponse$BodyHandlers/ofInputStream |
| let-go | yes | (http/request {… :as :stream}) + io/line-seq |
| Glojure | yes | net/http + chunked Body.Read into bytes.Buffer |
| cljgo | yes | (cljg.net.http/request {… :as :stream}) + cljg.stream/read-line |
The cljgo row was no until 2026-07-30 — cljg.net.http buffered through
io.ReadAll and exposed no reader. The :as :stream shim closed it.
Do NOT reach for BodyHandlers/ofLines on the JVM. It looks like the clean
route and it is not — see the comment on the :clj branch.
Streaming HTTP responses — Server-Sent Events, portable.
`koine.http/request` buffers: it hands you the whole body once the server is
done. That is wrong for an LLM, where the point of `stream` mode is that
tokens surface *as they are produced*. This namespace is the incremental
counterpart, kept in its own file because the host seams are completely
different from the buffered ones (a buffered client and a streaming client
share a URL and nothing else).
The contract is INCREMENTAL DELIVERY, not "returns the same lines". A host
that can only buffer does **not** get a slow-but-correct fallback here — it
throws (rule 2). A fake stream passes every test and then fails in front of a
user, which is strictly worse than an honest error.
Measured on 2026-07-27 (150 ms between server events, arrival times relative
to the first byte):
| host | streams? | route |
|---------|----------|---------------------------------------------------|
| JVM | yes | `HttpResponse$BodyHandlers/ofInputStream` |
| let-go | yes | `(http/request {… :as :stream})` + `io/line-seq` |
| Glojure | yes | `net/http` + chunked `Body.Read` into `bytes.Buffer` |
| cljgo | yes | `(cljg.net.http/request {… :as :stream})` + `cljg.stream/read-line` |
The cljgo row was `no` until 2026-07-30 — `cljg.net.http` buffered through
`io.ReadAll` and exposed no reader. The `:as :stream` shim closed it.
Do NOT reach for `BodyHandlers/ofLines` on the JVM. It looks like the clean
route and it is not — see the comment on the `:clj` branch.Wall-clock time, monotonic elapsed time and sleeping, portable.
toolnexus SPEC §8 stamps a ms duration on every llm/tool/run metric event
and its retry policy sleeps between attempts, so durations are load-bearing:
measure them with mono-ms/elapsed-ms (never with now-ms, which the
operating system can move backwards) and stamp events with now-ms.
Wall-clock time, monotonic elapsed time and sleeping, portable. toolnexus SPEC §8 stamps a `ms` duration on every llm/tool/run metric event and its retry policy sleeps between attempts, so durations are load-bearing: measure them with `mono-ms`/`elapsed-ms` (never with `now-ms`, which the operating system can move backwards) and stamp events with `now-ms`.
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 |