Caesium / libsodium-backed implementation of hive-system.protocols/ICrypto.
Uses parameter-object map signatures (:crypto/* keys) like the rest of
the crypto layer. Loads caesium lazily via requiring-resolve so absence
of native libsodium only fails at construction (->caesium-crypto).
Algorithms supported: :xchacha20-poly1305 — caesium.crypto.aead/xchacha20poly1305-ietf-{encrypt,decrypt}
Caesium / libsodium-backed implementation of `hive-system.protocols/ICrypto`.
Uses parameter-object map signatures (`:crypto/*` keys) like the rest of
the crypto layer. Loads caesium lazily via `requiring-resolve` so absence
of native libsodium only fails at construction (`->caesium-crypto`).
Algorithms supported:
:xchacha20-poly1305 — caesium.crypto.aead/xchacha20poly1305-ietf-{encrypt,decrypt}Plain-fn DSL over hive-system.protocols/ICrypto. Parameter-object style:
every fn takes a single map keyed under :crypto/*.
Callers depend on this ns + ICrypto, never on a concrete adapter (Tink / caesium / ...). Java interop stays inside the adapters.
Surface (key prefix :crypto/*):
(aead-seal! {:crypto/adapter c :crypto/key k :crypto/plaintext pt :crypto/aad aad}) => Result<{:crypto/ciphertext ^bytes :crypto/iv ^bytes}>
(aead-open! {:crypto/adapter c :crypto/key k :crypto/ciphertext ct :crypto/iv nonce :crypto/aad aad}) => Result<{:crypto/plaintext ^bytes}>
(hpke-seal! {:crypto/adapter c :crypto/pubkey pub :crypto/plaintext pt :crypto/aad aad}) => Result<{:crypto/ciphertext ^bytes}>
(hpke-open! {:crypto/adapter c :crypto/keypair kp :crypto/ciphertext ct :crypto/aad aad}) => Result<{:crypto/plaintext ^bytes}>
(sha256 {:crypto/adapter c :crypto/data ^bytes}) => Result<{:crypto/hash ^bytes :crypto/algorithm :sha256}>
(random-key) -> 32-byte ^bytes (XChaCha20 key) (random-nonce) -> 24-byte ^bytes (XChaCha20 nonce) (random-x25519-keypair) -> {:public 32B :private 32B}
Default :crypto/algorithm keys are filled in by each fn (caller may
override via the same key in the input map).
Plain-fn DSL over `hive-system.protocols/ICrypto`. Parameter-object style:
every fn takes a single map keyed under `:crypto/*`.
Callers depend on this ns + ICrypto, never on a concrete adapter
(Tink / caesium / ...). Java interop stays inside the adapters.
Surface (key prefix `:crypto/*`):
(aead-seal! {:crypto/adapter c :crypto/key k :crypto/plaintext pt :crypto/aad aad})
=> Result<{:crypto/ciphertext ^bytes :crypto/iv ^bytes}>
(aead-open! {:crypto/adapter c :crypto/key k :crypto/ciphertext ct
:crypto/iv nonce :crypto/aad aad})
=> Result<{:crypto/plaintext ^bytes}>
(hpke-seal! {:crypto/adapter c :crypto/pubkey pub
:crypto/plaintext pt :crypto/aad aad})
=> Result<{:crypto/ciphertext ^bytes}>
(hpke-open! {:crypto/adapter c :crypto/keypair kp
:crypto/ciphertext ct :crypto/aad aad})
=> Result<{:crypto/plaintext ^bytes}>
(sha256 {:crypto/adapter c :crypto/data ^bytes})
=> Result<{:crypto/hash ^bytes :crypto/algorithm :sha256}>
(random-key) -> 32-byte ^bytes (XChaCha20 key)
(random-nonce) -> 24-byte ^bytes (XChaCha20 nonce)
(random-x25519-keypair) -> {:public 32B :private 32B}
Default `:crypto/algorithm` keys are filled in by each fn (caller may
override via the same key in the input map).Ed25519 over PKCS#8 / X.509 DER encodings, via the JDK provider (Java 15+).
The Tink path in hive-system.crypto.tink consumes RAW 32-byte key
material. Keyrings and key files in this ecosystem store the JDK encodings
instead — PKCS#8 for a private key, X.509 for a public one — which is what
KeyFactory consumes. Both encodings wrap the same 32 bytes and yield
byte-identical RFC 8032 signatures, so the two paths are interchangeable
over one key.
Selected with :crypto/key-encoding :der; :raw, the default, keeps the
Tink path. Signing takes the PKCS#8 private key as :crypto/key — the
public half is derived from it, so no keypair need be assembled.
Ed25519 over PKCS#8 / X.509 DER encodings, via the JDK provider (Java 15+). The Tink path in `hive-system.crypto.tink` consumes RAW 32-byte key material. Keyrings and key files in this ecosystem store the JDK encodings instead — PKCS#8 for a private key, X.509 for a public one — which is what KeyFactory consumes. Both encodings wrap the same 32 bytes and yield byte-identical RFC 8032 signatures, so the two paths are interchangeable over one key. Selected with `:crypto/key-encoding :der`; `:raw`, the default, keeps the Tink path. Signing takes the PKCS#8 private key as `:crypto/key` — the public half is derived from it, so no keypair need be assembled.
Pure-Java HKDF-SHA256 (RFC 5869). Shared by ICrypto adapters so that
crypto-derive-key returns byte-identical bytes regardless of which
adapter resolves the call.
Composed from javax.crypto.Mac HMAC-SHA256 — no native dependency.
Pure-Java HKDF-SHA256 (RFC 5869). Shared by ICrypto adapters so that `crypto-derive-key` returns byte-identical bytes regardless of which adapter resolves the call. Composed from `javax.crypto.Mac` HMAC-SHA256 — no native dependency.
Macro sugar for AEAD ceremonies built on hive-system.crypto.core.
Provided as an opt-in convenience layer on top of the fn DSL — required only when call sites benefit from compact nested-let composition.
Use:
(let-aead [crypto (tink/->tink-crypto) key (core/random-key) {:keys [ciphertext iv]} (core/aead-seal! crypto key pt aad)] (core/aead-open! crypto key ciphertext iv aad))
let-aead short-circuits on the first :error Result — same semantics as
hive-dsl.result/let-ok — but accepts plain (non-Result) values for
adapter / key construction lines.
Macro sugar for AEAD ceremonies built on `hive-system.crypto.core`.
Provided as an opt-in convenience layer on top of the fn DSL — required
only when call sites benefit from compact nested-let composition.
Use:
(let-aead [crypto (tink/->tink-crypto)
key (core/random-key)
{:keys [ciphertext iv]} (core/aead-seal! crypto key pt aad)]
(core/aead-open! crypto key ciphertext iv aad))
`let-aead` short-circuits on the first :error Result — same semantics as
`hive-dsl.result/let-ok` — but accepts plain (non-Result) values for
adapter / key construction lines.An hive-spi.crypto.ports/ISigner backed by this system's ICrypto.
The port speaks base64 text — PKCS#8 private, X.509 public, base64
signature — while ICrypto speaks bytes. This namespace is that translation
and nothing else: it decodes, delegates with :crypto/key-encoding :der,
and re-encodes.
Installing it makes hive-license sign through whichever ICrypto a host has already built, instead of hive-license's own JDK adapter. Signatures are byte-identical either way, so licences issued before and after an install verify against the same public key.
An `hive-spi.crypto.ports/ISigner` backed by this system's ICrypto. The port speaks base64 text — PKCS#8 private, X.509 public, base64 signature — while ICrypto speaks bytes. This namespace is that translation and nothing else: it decodes, delegates with `:crypto/key-encoding :der`, and re-encodes. Installing it makes hive-license sign through whichever ICrypto a host has already built, instead of hive-license's own JDK adapter. Signatures are byte-identical either way, so licences issued before and after an install verify against the same public key.
Tink-backed implementation of hive-system.protocols/ICrypto.
See hive-system.protocols/ICrypto for the parameter-object contract —
every op receives a single map keyed under :crypto/*.
Algorithms supported: :xchacha20-poly1305 — symmetric AEAD (subtle.XChaCha20Poly1305) :hpke-x25519 — RFC 9180 single-shot HPKE (DHKEM-X25519 + HKDF-SHA256 + ChaCha20-Poly1305) :sha256 — message digest
Ed25519 keys default to RAW 32-byte material. Pass
:crypto/key-encoding :der to work in the JDK encodings instead — PKCS#8
private under :crypto/key, X.509 public under :crypto/pubkey — which is
what a keyring stores. Both encodings wrap the same 32 bytes and produce
byte-identical signatures. See hive-system.crypto.ed25519-der.
Tink-backed implementation of `hive-system.protocols/ICrypto`.
See `hive-system.protocols/ICrypto` for the parameter-object contract —
every op receives a single map keyed under `:crypto/*`.
Algorithms supported:
:xchacha20-poly1305 — symmetric AEAD (subtle.XChaCha20Poly1305)
:hpke-x25519 — RFC 9180 single-shot HPKE
(DHKEM-X25519 + HKDF-SHA256 + ChaCha20-Poly1305)
:sha256 — message digest
Ed25519 keys default to RAW 32-byte material. Pass
`:crypto/key-encoding :der` to work in the JDK encodings instead — PKCS#8
private under `:crypto/key`, X.509 public under `:crypto/pubkey` — which is
what a keyring stores. Both encodings wrap the same 32 bytes and produce
byte-identical signatures. See `hive-system.crypto.ed25519-der`.DependencyEnsurer — composes IShell + IDistroDetector + IPackageInstaller
to deliver the user-level ensure! verb.
The record itself is pure-data + dispatch; every side effect goes
through one of the three injected ports. Tests exercise the policy
matrix (:auto / :ask / :throw) by feeding fakes.
DependencyEnsurer — composes IShell + IDistroDetector + IPackageInstaller to deliver the user-level `ensure!` verb. The record itself is pure-data + dispatch; every side effect goes through one of the three injected ports. Tests exercise the policy matrix (`:auto` / `:ask` / `:throw`) by feeding fakes.
Protocols for the dependency-ensurer subsystem.
Tools that shell out (ssh, sshpass, pass, gpg, clojure, …)
currently assume the binary is on PATH. When missing, calls explode
with a confusing IOException: Cannot run program … deep in
babashka.process. This namespace introduces the user-level verb
"make sure I can call X" — composing narrower ports
(IShell from hive-system.protocols, plus IDistroDetector and
IPackageInstaller defined here) so each impl stays ISP-narrow.
IDistroDetector — "which Linux/BSD/macOS family am I on?"
One axis of variance: distro identity. Pure-ish (reads
/etc/os-release, uname, etc.).IPackageInstaller — "install package X via the system pkg mgr".
One axis of variance: package manager (apt / dnf / pacman / brew …).
All side effects.IDependencyEnsurer — the user-level facade that composes the
above three to deliver a single ensure! verb.New distros plug in via new IDistroDetector impls; new package
managers via new IPackageInstaller impls. IDependencyEnsurer
itself stays distro/manager-agnostic.
Protocols for the dependency-ensurer subsystem. ## Why this exists Tools that shell out (`ssh`, `sshpass`, `pass`, `gpg`, `clojure`, …) currently assume the binary is on PATH. When missing, calls explode with a confusing `IOException: Cannot run program …` deep in babashka.process. This namespace introduces the user-level verb "make sure I can call X" — composing narrower ports (`IShell` from `hive-system.protocols`, plus `IDistroDetector` and `IPackageInstaller` defined here) so each impl stays ISP-narrow. ## ISP boundary - `IDistroDetector` — "which Linux/BSD/macOS family am I on?" One axis of variance: distro identity. Pure-ish (reads `/etc/os-release`, `uname`, etc.). - `IPackageInstaller` — "install package X via the system pkg mgr". One axis of variance: package manager (apt / dnf / pacman / brew …). All side effects. - `IDependencyEnsurer` — the user-level facade that composes the above three to deliver a single `ensure!` verb. New distros plug in via new `IDistroDetector` impls; new package managers via new `IPackageInstaller` impls. `IDependencyEnsurer` itself stays distro/manager-agnostic.
Standard local-host adapters for IDistroDetector and IPackageInstaller,
plus a default-ensurer factory that wires the whole thing up using
the existing hive-system.shell.core/make-shell adapter.
Adapters here read /etc/os-release and shell out to apt-get / dnf /
pacman / brew via the injected IShell. They never auto-install during
construction; only install! runs effects, and only when explicitly
invoked by IDependencyEnsurer/ensure! under an :auto or accepted
:ask policy.
Tests use fake IShell + fake distro/installer impls — see
test/hive_system/deps/core_test.clj.
Standard local-host adapters for IDistroDetector and IPackageInstaller, plus a `default-ensurer` factory that wires the whole thing up using the existing `hive-system.shell.core/make-shell` adapter. Adapters here read `/etc/os-release` and shell out to apt-get / dnf / pacman / brew via the injected IShell. They never auto-install during construction; only `install!` runs effects, and only when explicitly invoked by `IDependencyEnsurer/ensure!` under an `:auto` or accepted `:ask` policy. Tests use *fake* IShell + fake distro/installer impls — see `test/hive_system/deps/core_test.clj`.
Reusable interceptors for system operations.
All interceptors follow the re-frame context model: {:coeffects {:event [...] :db ...} :effects {...}}
Reusable interceptors for system operations.
- timing: Measures wall-clock duration, injects into effects
- journal: Records operation to IJournal after execution
- tool-check: Validates binary exists before shell exec
- result-lift: Converts raw handler return into Result-typed effect
All interceptors follow the re-frame context model:
{:coeffects {:event [...] :db ...} :effects {...}}Event-driven system operations.
Registers event handlers for shell execution, tool checking, and process lifecycle. Each handler returns a Result in the effects map, which the journal interceptor persists.
Init: (init!) ;; default — no journal (init! {:journal-fn record!}) ;; with IJournal callback
Dispatch: (dispatch [:sys/exec "ls -la"]) (dispatch [:sys/exec ["rg" "pattern" "src/"]]) (dispatch [:sys/exec-ok "make test"]) (dispatch [:sys/which "rg"]) (dispatch [:sys/require-tool :ripgrep]) (dispatch [:sys/require-tools [:ripgrep :fd :jq]])
All handlers return {:result (ok ...) or (err ...)} in effects. The timing interceptor adds :_duration-ms. The journal interceptor persists to IJournal if configured.
Event-driven system operations.
Registers event handlers for shell execution, tool checking,
and process lifecycle. Each handler returns a Result in the
effects map, which the journal interceptor persists.
Init:
(init!) ;; default — no journal
(init! {:journal-fn record!}) ;; with IJournal callback
Dispatch:
(dispatch [:sys/exec "ls -la"])
(dispatch [:sys/exec ["rg" "pattern" "src/"]])
(dispatch [:sys/exec-ok "make test"])
(dispatch [:sys/which "rg"])
(dispatch [:sys/require-tool :ripgrep])
(dispatch [:sys/require-tools [:ripgrep :fd :jq]])
All handlers return {:result (ok ...) or (err ...)} in effects.
The timing interceptor adds :_duration-ms.
The journal interceptor persists to IJournal if configured.IPathQuery implementation backed by babashka.fs. All operations return hive-dsl Results for railway composition.
Convenience API (stateless, default instance): (exists? path) → Result<boolean> (directory? path) → Result<boolean> (absolute-dir? path) → Result<boolean> (resolve-first-dir [...]) → Result<string> (find-files path exts) → Result<vec<string>> (expand-path path exts) → vec<string> (rescue-wrapped)
DIP: consumers depend on IPathQuery protocol, not this impl. Swap FsPathQuery for a test double via make-path-query or protocol.
IPathQuery implementation backed by babashka.fs. All operations return hive-dsl Results for railway composition. Convenience API (stateless, default instance): (exists? path) → Result<boolean> (directory? path) → Result<boolean> (absolute-dir? path) → Result<boolean> (resolve-first-dir [...]) → Result<string> (find-files path exts) → Result<vec<string>> (expand-path path exts) → vec<string> (rescue-wrapped) DIP: consumers depend on IPathQuery protocol, not this impl. Swap FsPathQuery for a test double via make-path-query or protocol.
IFilesystem implementation backed by babashka.fs + java.nio. All operations return hive-dsl Results for railway composition.
Convenience API (stateless, default instance): (mkdirs! path) → Result<{:path}> (tmpdir! prefix) → Result<{:path}> (atomic-write! path content) → Result<{:path :bytes}> (lock! path timeout-ms) → Result<{:lock}> (watch! path patterns handler) → Result<{:watcher}>
DIP: consumers depend on the IFilesystem protocol, not this impl. Swap FsFilesystem for a test double via make-filesystem or the protocol.
IFilesystem implementation backed by babashka.fs + java.nio.
All operations return hive-dsl Results for railway composition.
Convenience API (stateless, default instance):
(mkdirs! path) → Result<{:path}>
(tmpdir! prefix) → Result<{:path}>
(atomic-write! path content) → Result<{:path :bytes}>
(lock! path timeout-ms) → Result<{:lock}>
(watch! path patterns handler) → Result<{:watcher}>
DIP: consumers depend on the IFilesystem protocol, not this impl.
Swap FsFilesystem for a test double via make-filesystem or the protocol.OllamaGpuExecutor — IGpuExecutor + IGpuStatus over the ollama HTTP API.
First concretion of hive-system.protocols.gpu. Mirrors hive-system.secrets/* placement: protocol in hive-system.protocols.*, concrete adapter in a sibling sub-namespace.
Capabilities: :embed — POST /api/embed :generate — POST /api/generate (single-shot, no streaming for now)
What we deliberately don't do here:
ollama pull <model> already happened.Construct via (->ollama-executor opts) — never the raw record ctor.
OllamaGpuExecutor — IGpuExecutor + IGpuStatus over the ollama HTTP API.
First concretion of hive-system.protocols.gpu. Mirrors hive-system.secrets/*
placement: protocol in hive-system.protocols.*, concrete adapter in a
sibling sub-namespace.
Capabilities:
:embed — POST /api/embed
:generate — POST /api/generate (single-shot, no streaming for now)
What we deliberately don't do here:
- Pool / retry classification — that's the caller's job via
hive-weave.gpu/gpu-fork-join + hive-dsl rescue helpers.
- Streaming / SSE — generate returns once, no incremental delta.
- Model autoload — assumes `ollama pull <model>` already happened.
Construct via (->ollama-executor opts) — never the raw record ctor.nvidia-smi probe satisfying IGpuStatus.
Authoritative source for VRAM observability. The OllamaGpuExecutor's gpu-status returns -1 for VRAM fields because ollama doesn't expose them reliably; this probe shells out to nvidia-smi for the truth.
Uses hive-system.protocols/IShell so a mock shell can drive tests without launching real subprocesses (DIP). Default ctor wires the stateless hive-system.shell.core/exec! convenience.
Output schema (ok branch): {:vram-free-mb long :vram-total-mb long :inflight 0 ; probe doesn't track in-flight calls :queued 0 ; idem :backend-id :nvidia-smi}
Err branch tags: :gpu/no-driver — nvidia-smi missing on PATH or returned non-zero :gpu/probe-parse — nvidia-smi output couldn't be parsed :gpu/transport-failed — exec layer raised
nvidia-smi probe satisfying IGpuStatus.
Authoritative source for VRAM observability. The OllamaGpuExecutor's
gpu-status returns -1 for VRAM fields because ollama doesn't expose them
reliably; this probe shells out to nvidia-smi for the truth.
Uses hive-system.protocols/IShell so a mock shell can drive tests
without launching real subprocesses (DIP). Default ctor wires the
stateless hive-system.shell.core/exec! convenience.
Output schema (ok branch):
{:vram-free-mb long
:vram-total-mb long
:inflight 0 ; probe doesn't track in-flight calls
:queued 0 ; idem
:backend-id :nvidia-smi}
Err branch tags:
:gpu/no-driver — nvidia-smi missing on PATH or returned non-zero
:gpu/probe-parse — nvidia-smi output couldn't be parsed
:gpu/transport-failed — exec layer raisedBOUNDARY: write a regex as data, get a Pattern the existing subsystem already knows how to run.
(api/->expr :java [:cat :start [:+ :digit] :end]) ;; => {:ok "^\d+$"}
(api/register! #:pattern{:id :audit/probe} [:cat "ready" [:? "?"]]) (pattern/match? :audit/probe "ready?")
A Construct projects INTO the existing Pattern value object rather than
replacing it: :pattern/expr is filled from the construct, :pattern/construct
records what it was built from, and everything downstream — the registry's
validation, IPatternEngine, match?, scan — is untouched. A pattern
written as a literal string keeps working exactly as before.
register! goes through pattern.registry/register-pattern! so a construct
that projects to an invalid Pattern is refused at the same gate as every
other pattern.
BOUNDARY: write a regex as data, get a Pattern the existing subsystem
already knows how to run.
(api/->expr :java [:cat :start [:+ :digit] :end])
;; => {:ok "^\\d+$"}
(api/register! #:pattern{:id :audit/probe} [:cat "ready" [:? "?"]])
(pattern/match? :audit/probe "ready?")
A Construct projects INTO the existing `Pattern` value object rather than
replacing it: `:pattern/expr` is filled from the construct, `:pattern/construct`
records what it was built from, and everything downstream — the registry's
validation, `IPatternEngine`, `match?`, `scan` — is untouched. A pattern
written as a literal string keeps working exactly as before.
`register!` goes through `pattern.registry/register-pattern!` so a construct
that projects to an invalid Pattern is refused at the same gate as every
other pattern.PROMOTE layer: lift authored regal syntax into a validated Construct, project it back, and say what a Construct requires of its target.
Everything here is pure and total — a malformed form yields a Result describing why, never an exception and never a silent repair.
(normalize [:cat "a" [:alt "b" "c"]]) => {:ok {…}} (->regal normalized) => [:cat "a" [:alt "b" "c"]] (required-capabilities normalized) => #{}
required-capabilities is what makes dialect support a set-difference
decidable BEFORE emission rather than a try/catch around a compile.
PROMOTE layer: lift authored regal syntax into a validated Construct, project
it back, and say what a Construct requires of its target.
Everything here is pure and total — a malformed form yields a Result
describing why, never an exception and never a silent repair.
(normalize [:cat "a" [:alt "b" "c"]]) => {:ok {…}}
(->regal normalized) => [:cat "a" [:alt "b" "c"]]
(required-capabilities normalized) => #{}
`required-capabilities` is what makes dialect support a set-difference
decidable BEFORE emission rather than a `try`/`catch` around a compile.The DIP swap point: a regex target language as data, plus the one gate every emitter passes through.
For a Construct a dialect cannot express, emit returns
(r/err :construct/unsupported …). It NEVER emits an approximation and never
drops a node. A :lookahead quietly discarded for RE2 produces a DIFFERENT
LANGUAGE that still compiles and still returns hits — the worst outcome
available, because nothing reports it.
emit is a plain function that checks capabilities and THEN calls
-render. An implementation supplies rendering only; it cannot skip,
weaken or forget the check, because it never sees it. One definition, N
implementations.
The dialect profile is an ARGUMENT throughout — resolved from the registry at call time, never captured at wiring time.
The DIP swap point: a regex target language as data, plus the one gate every emitter passes through. ## The substitutability contract For a Construct a dialect cannot express, `emit` returns `(r/err :construct/unsupported …)`. It NEVER emits an approximation and never drops a node. A `:lookahead` quietly discarded for RE2 produces a DIFFERENT LANGUAGE that still compiles and still returns hits — the worst outcome available, because nothing reports it. ## Why the gate is here and not in the emitter `emit` is a plain function that checks capabilities and THEN calls `-render`. An implementation supplies rendering only; it cannot skip, weaken or forget the check, because it never sees it. One definition, N implementations. The dialect profile is an ARGUMENT throughout — resolved from the registry at call time, never captured at wiring time.
POSIX ERE — the dialect grep -E speaks, rendered without regal.
regal has no ERE flavor, and its :java/:ecma output is not ERE: a nested
alternation comes out as (?:a|b), which POSIX ERE has no syntax for. So
this is the one dialect that renders from the Construct directly. It is
small because ERE is small — literals, classes, alternation, greedy
quantifiers, anchors, and one grouping syntax.
rg rejects what it cannot do: an unsupported pattern exits 2 with a
message. GNU grep 3.11 does not. Measured:
grep -E '\d+' matches a literal d — no warning, exit 0
grep -E '(?:a|b)c' matches abc — a warning, exit 0
grep -E 'ab*?' greedy — silently not lazy
grep -E '(?=x)' never matches — a warning, exit 1
Every one of those is a DIFFERENT LANGUAGE that still runs and still returns hits. For rg the capability gate converts a stderr parse into data; for grep it converts a silently wrong answer into a named refusal, which is the stronger reason the gate exists at all.
:posix-ere therefore provides: nothingIts capability set is empty. \d must be written [:class [\0 \9]],
which renders [0-9]; a tab must be a literal tab inside a :literal, not
:tab; a :capture is refused because ERE's only group is a capturing one,
so any structural group this emitter needs would shift the very index
:match/groups promises.
Everything the gate lets through is rendered exactly, or not at all.
POSIX ERE — the dialect `grep -E` speaks, rendered without regal. ## Why this emitter is hand-written regal has no ERE flavor, and its `:java`/`:ecma` output is not ERE: a nested alternation comes out as `(?:a|b)`, which POSIX ERE has no syntax for. So this is the one dialect that renders from the Construct directly. It is small because ERE is small — literals, classes, alternation, greedy quantifiers, anchors, and one grouping syntax. ## Why the gate matters more here than anywhere else `rg` rejects what it cannot do: an unsupported pattern exits 2 with a message. GNU grep 3.11 does not. Measured: grep -E '\d+' matches a literal `d` — no warning, exit 0 grep -E '(?:a|b)c' matches `abc` — a warning, exit 0 grep -E 'ab*?' greedy — silently not lazy grep -E '(?=x)' never matches — a warning, exit 1 Every one of those is a DIFFERENT LANGUAGE that still runs and still returns hits. For rg the capability gate converts a stderr parse into data; for grep it converts a silently wrong answer into a named refusal, which is the stronger reason the gate exists at all. ## What `:posix-ere` therefore provides: nothing Its capability set is empty. `\d` must be written `[:class [\0 \9]]`, which renders `[0-9]`; a tab must be a literal tab inside a `:literal`, not `:tab`; a `:capture` is refused because ERE's only group is a capturing one, so any structural group this emitter needs would shift the very index `:match/groups` promises. Everything the gate lets through is rendered exactly, or not at all.
COLLECT layer: an anti-corruption layer over lambdaisland/regal, and the three dialect profiles it can render.
Regal is the emitter we buy rather than build — it owns escaping, precedence and grouping, and it is the only code here that knows what a regex looks like. Its vocabulary does not cross into the domain: it throws, we return a Result; it names flavors, we name dialects.
Measured on 0.1.175: regal does NOT gate on flavor. [:atomic "ab"] renders
as (?>ab) under :ecma, where JavaScript has no atomic groups, and an
unknown flavor is accepted silently. The capability gate in
construct.dialect is therefore load-bearing, not ceremony.
:java every capability flavor :java9 :ecma no atomic groups flavor :ecma :re2 no lookaround, no atomic flavor :ecma (rg, sd, RE2, Rust regex)
:posix-ere is deliberately absent: ERE has no non-capturing group, so
regal's (?:…) output is not ERE even after a capability gate. It needs a
real second emitter, which this design declines to build.
COLLECT layer: an anti-corruption layer over lambdaisland/regal, and the three dialect profiles it can render. Regal is the emitter we buy rather than build — it owns escaping, precedence and grouping, and it is the only code here that knows what a regex looks like. Its vocabulary does not cross into the domain: it throws, we return a Result; it names flavors, we name dialects. ## What regal does not do, and this layer must Measured on 0.1.175: regal does NOT gate on flavor. `[:atomic "ab"]` renders as `(?>ab)` under `:ecma`, where JavaScript has no atomic groups, and an unknown flavor is accepted silently. The capability gate in `construct.dialect` is therefore load-bearing, not ceremony. ## Dialects shipped :java every capability flavor :java9 :ecma no atomic groups flavor :ecma :re2 no lookaround, no atomic flavor :ecma (rg, sd, RE2, Rust regex) `:posix-ere` is deliberately absent: ERE has no non-capturing group, so regal's `(?:…)` output is not ERE even after a capability gate. It needs a real second emitter, which this design declines to build.
Malli value objects for regex CONSTRUCTS — a regex expression reified as data.
A construct is AUTHORED in lambdaisland/regal's vector syntax — [:cat "a" [:alt "b" "c"]] — and NORMALIZED into the recursive map value object this
namespace defines. The authored form is what callers write; the normalized
form is what carries contracts, generators and tests. construct.core
promotes one into the other and projects back.
The split is forced: malli refuses a :ref reachable inside a sequence
schema (::m/potentially-recursive-seqex), and regal's head-plus-variadic
vector IS a sequence schema.
A Capability names a regex feature whose availability differs across target
languages. A Dialect declares the set it PROVIDES; a construct's required
set is computed from its shape. Support is a set-difference, decidable before
emission — never a try/catch around a compile.
These schemas are the single source: they drive the m/=> contracts and the
synthesized property/mutation tests.
Malli value objects for regex CONSTRUCTS — a regex expression reified as data. ## Two forms, one language A construct is AUTHORED in lambdaisland/regal's vector syntax — `[:cat "a" [:alt "b" "c"]]` — and NORMALIZED into the recursive map value object this namespace defines. The authored form is what callers write; the normalized form is what carries contracts, generators and tests. `construct.core` promotes one into the other and projects back. The split is forced: malli refuses a `:ref` reachable inside a sequence schema (`::m/potentially-recursive-seqex`), and regal's head-plus-variadic vector IS a sequence schema. ## Capabilities A `Capability` names a regex feature whose availability differs across target languages. A `Dialect` declares the set it PROVIDES; a construct's required set is computed from its shape. Support is a set-difference, decidable before emission — never a `try`/`catch` around a compile. These schemas are the single source: they drive the `m/=>` contracts and the synthesized property/mutation tests.
Facade for the pattern subsystem: name what you are looking for, get an answer, never mention an engine.
(pattern/register-pattern! #:pattern{:id :audit/probe :expr "ready\?"}) (pattern/match? :audit/probe source) (pattern/scan :audit/probe source)
The regex engine registers itself on load, so :regex is available without
ceremony; *default-engine* selects what an engine-less Pattern uses.
Re-exports are DELEGATING fns, never def-aliases — a def alias of a protocol method freezes its method cache.
Facade for the pattern subsystem: name what you are looking for, get an
answer, never mention an engine.
(pattern/register-pattern! #:pattern{:id :audit/probe :expr "ready\\?"})
(pattern/match? :audit/probe source)
(pattern/scan :audit/probe source)
The regex engine registers itself on load, so `:regex` is available without
ceremony; `*default-engine*` selects what an engine-less Pattern uses.
Re-exports are DELEGATING fns, never def-aliases — a def alias of a protocol
method freezes its method cache.Protocol for the pattern subsystem.
IPatternEngine is the DIP seam between consumers (audits, linters, log
scanners) and concrete matching engines (java.util.regex today; a structural
s-expression matcher, a glob matcher, or a tree-sitter query engine next).
Consumers depend on this protocol and on a Pattern value. They never name an engine, so swapping or adding one changes no call site.
An engine MUST:
Result from every operation — never throw for a bad pattern.:pattern/unsupported-flag) rather than
ignore it: a silently dropped flag is a wrong answer.:match/end exclusive,
with (subs subject start end) equal to :match/text.engine-id keyword.Protocol for the pattern subsystem. ## ISP boundary `IPatternEngine` is the DIP seam between consumers (audits, linters, log scanners) and concrete matching engines (java.util.regex today; a structural s-expression matcher, a glob matcher, or a tree-sitter query engine next). Consumers depend on this protocol and on a Pattern value. They never name an engine, so swapping or adding one changes no call site. ## Engine contract An engine MUST: - Return `Result` from every operation — never throw for a bad pattern. - Reject a flag it cannot honour (`:pattern/unsupported-flag`) rather than ignore it: a silently dropped flag is a wrong answer. - Report matches in subject order, non-overlapping, `:match/end` exclusive, with `(subs subject start end)` equal to `:match/text`. - Use a stable, unique `engine-id` keyword.
java.util.regex adapter for IPatternEngine.
The default engine. Pure promoters (flags->bitmask, ->matches) carry the
logic; the record is a thin Result-returning shell over them.
java.util.regex adapter for IPatternEngine. The default engine. Pure promoters (`flags->bitmask`, `->matches`) carry the logic; the record is a thin Result-returning shell over them.
Engine and named-pattern registries — the DIP swap points.
Two open sets, two registries:
engines id -> IPatternEngine ;; how to match patterns id -> Pattern ;; what to look for
A consumer names a pattern (:audit/readiness-probe) instead of carrying a
literal expression, so one definition serves every call site and a fix lands
in one place. resolve-pattern is PURE — the index is an argument, which is
what makes it testable and what lets a caller run against a private index.
Engine and named-pattern registries — the DIP swap points. Two open sets, two registries: engines id -> IPatternEngine ;; how to match patterns id -> Pattern ;; what to look for A consumer names a pattern (`:audit/readiness-probe`) instead of carrying a literal expression, so one definition serves every call site and a fix lands in one place. `resolve-pattern` is PURE — the index is an argument, which is what makes it testable and what lets a caller run against a private index.
Malli value objects for the pattern subsystem.
A Pattern is a portable, ENGINE-AGNOSTIC description of what to look for: an expression plus flags, optionally named by an id. A Match is one located occurrence — text plus its half-open [start end) offsets in the subject.
These schemas are the single source: they drive the m/=> contracts and the
synthesized property/mutation tests.
Malli value objects for the pattern subsystem. A Pattern is a portable, ENGINE-AGNOSTIC description of what to look for: an expression plus flags, optionally named by an id. A Match is one located occurrence — text plus its half-open [start end) offsets in the subject. These schemas are the single source: they drive the `m/=>` contracts and the synthesized property/mutation tests.
IProcess implementation via java.lang.ProcessBuilder / ProcessHandle. Long-lived process lifecycle: spawn returns a live handle; wait/signal/pipe act on it. All operations return hive-dsl Result.
Stream draining and process-tree teardown are shared with the IShell implementation — see hive-system.process.streams / .tree.
No hive-weave here on purpose: this namespace has to stay loadable under Babashka, and hive-weave.pool is not.
IProcess implementation via java.lang.ProcessBuilder / ProcessHandle. Long-lived process lifecycle: spawn returns a live handle; wait/signal/pipe act on it. All operations return hive-dsl Result. Stream draining and process-tree teardown are shared with the IShell implementation — see hive-system.process.streams / .tree. No hive-weave here on purpose: this namespace has to stay loadable under Babashka, and hive-weave.pool is not.
Process liveness detection — single source of truth for is this pid alive?
semantics across the hive ecosystem.
Use this instead of inlining kill -0, ProcessHandle/of, or backend-
specific tricks. Liveness check is a system-level concern and belongs in
hive-system (per the IProcess family of protocols), not in callers.
The LivenessSignal ADT is the closed sum of valid outcomes:
:liveness/alive — pid is owned by a live OS process :liveness/dead — pid was once valid but the process is gone (ESRCH) :liveness/unknown — pid is nil/non-integer, or the check itself raised transiently. Callers MUST NOT zombify on :unknown (degrade-soft semantics — protect against false positives during boot races, container migrations, and short-lived OS hiccups).
Process liveness detection — single source of truth for `is this pid alive?`
semantics across the hive ecosystem.
Use this instead of inlining `kill -0`, `ProcessHandle/of`, or backend-
specific tricks. Liveness check is a system-level concern and belongs in
hive-system (per the IProcess family of protocols), not in callers.
The `LivenessSignal` ADT is the closed sum of valid outcomes:
:liveness/alive — pid is owned by a live OS process
:liveness/dead — pid was once valid but the process is gone (ESRCH)
:liveness/unknown — pid is nil/non-integer, or the check itself raised
transiently. Callers MUST NOT zombify on :unknown
(degrade-soft semantics — protect against false
positives during boot races, container migrations,
and short-lived OS hiccups).Reading a child process's output without blocking on its descendants.
EOF on a process stream arrives when the LAST holder of the write end closes it, which is not when the command exits: a backgrounded grandchild inherits the pipe and holds it open. Every read here is therefore abandonable — it runs on a daemon thread and is collected with a BOUNDED deref, so a detached descendant cannot extend its caller's deadline.
Leaf namespace: java.io and clojure.java.io only. No hive-weave, so it loads under Babashka.
Reading a child process's output without blocking on its descendants. EOF on a process stream arrives when the LAST holder of the write end closes it, which is not when the command exits: a backgrounded grandchild inherits the pipe and holds it open. Every read here is therefore abandonable — it runs on a daemon thread and is collected with a BOUNDED deref, so a detached descendant cannot extend its caller's deadline. Leaf namespace: java.io and clojure.java.io only. No hive-weave, so it loads under Babashka.
Destroying a process together with everything it spawned.
Killing only the direct child leaves grandchildren alive, holding the pipes and whatever resource the kill was meant to reclaim — a timeout that reaps just the shell reclaims nothing.
Leaf namespace: java.lang only. No hive-weave, so it loads under Babashka.
Destroying a process together with everything it spawned. Killing only the direct child leaves grandchildren alive, holding the pipes and whatever resource the kill was meant to reclaim — a timeout that reaps just the shell reclaims nothing. Leaf namespace: java.lang only. No hive-weave, so it loads under Babashka.
IWorker implementation — a warm request/response worker over a long-lived process (hive-system.process IProcess). Requests/responses are newline- delimited EDN; a monotonic :id correlates each response. Calls are single- flight via a hive-weave gate (1 permit); reads are timeout-bounded.
IWorker implementation — a warm request/response worker over a long-lived process (hive-system.process IProcess). Requests/responses are newline- delimited EDN; a monotonic :id correlates each response. Calls are single- flight via a hive-weave gate (1 permit); reads are timeout-bounded.
Core protocols for hive-system.
Layer 1: IJournal — temporal execution fabric (Datahike/Proximum) Layer 2: System operation protocols (IProcess, INetwork, IFilesystem, etc.)
Every system operation returns Result (ok/err) from hive-dsl. Every Result is journaled with bitemporal semantics.
SOLID-D: All consumers depend on these protocols, never concrete implementations. SOLID-O: New operation types = new protocol implementations, zero changes to journal.
Core protocols for hive-system. Layer 1: IJournal — temporal execution fabric (Datahike/Proximum) Layer 2: System operation protocols (IProcess, INetwork, IFilesystem, etc.) Every system operation returns Result (ok/err) from hive-dsl. Every Result is journaled with bitemporal semantics. SOLID-D: All consumers depend on these protocols, never concrete implementations. SOLID-O: New operation types = new protocol implementations, zero changes to journal.
GPU executor protocols — DIP boundary for GPU-bound work.
Two protocols, ISP-split: IGpuExecutor — does work (embed, generate, rerank) IGpuStatus — introspects backend (free VRAM, inflight, queued)
Closed ADTs cross the boundary: GpuRequest — what the caller wants done + declared :gpu/vram-mb budget GpuResponse — output payload + executor-id + duration-ms
Layering rationale: hive-mcp consumers (embedders, future rerankers, future local-LLM compression) depend on these protocols, never on a concrete backend. hive-weave.gpu admits requests against a VRAM budget BEFORE dispatching through gpu-execute!. New backends (ollama, llama.cpp, vLLM) extend the protocols; admission control + consumers stay closed (OCP).
CPPB mapping: Promote — GpuRequest is the ADT shape callers build up Pipeline — hive-weave.gpu/gpu-fork-join admits + dispatches Boundary — concrete IGpuExecutor impl performs IO
SOLID: SRP — IGpuExecutor does work; IGpuStatus introspects; admission lives in hive-weave OCP — new backends extend; consumers + admission untouched LSP — every executor returns the same Result shape (see schemas below) ISP — split work vs introspection so a probe-only sidecar (nvidia-smi) can satisfy IGpuStatus without claiming work-execution capability DIP — every consumer depends on these protocols, never on a record
GPU executor protocols — DIP boundary for GPU-bound work.
Two protocols, ISP-split:
IGpuExecutor — does work (embed, generate, rerank)
IGpuStatus — introspects backend (free VRAM, inflight, queued)
Closed ADTs cross the boundary:
GpuRequest — what the caller wants done + declared :gpu/vram-mb budget
GpuResponse — output payload + executor-id + duration-ms
Layering rationale:
hive-mcp consumers (embedders, future rerankers, future local-LLM
compression) depend on these protocols, never on a concrete backend.
hive-weave.gpu admits requests against a VRAM budget BEFORE dispatching
through gpu-execute!. New backends (ollama, llama.cpp, vLLM) extend the
protocols; admission control + consumers stay closed (OCP).
CPPB mapping:
Promote — GpuRequest is the ADT shape callers build up
Pipeline — hive-weave.gpu/gpu-fork-join admits + dispatches
Boundary — concrete IGpuExecutor impl performs IO
SOLID:
SRP — IGpuExecutor does work; IGpuStatus introspects; admission lives in hive-weave
OCP — new backends extend; consumers + admission untouched
LSP — every executor returns the same Result shape (see schemas below)
ISP — split work vs introspection so a probe-only sidecar (nvidia-smi)
can satisfy IGpuStatus without claiming work-execution capability
DIP — every consumer depends on these protocols, never on a recordRedaction registry + walker for safely surfacing structured data (compiled plans, event payloads, env maps, log lines) to an AI assistant or any debug surface.
A rule is a map:
{:select <selector> ; path-vector OR predicate fn (key,val)→bool :replace <fn> ; (fn [val] -> replacement) :id <keyword> ; assigned by register-rule! :doc <string?>}
Rules are stored in a global atom registry. redact walks input via
clojure.walk/postwalk, applying matching rules and auto-replacing
any Tainted it encounters with its print-method form.
[:env "SSHPASS"] matches the value at that path
inside any nested map.(fn [k v] ...) invoked for every map entry; truthy
return → replace the value.Registered at namespace load time:
:env/sshpass → [:env "SSHPASS"] → "<redacted>" :env/ssh-auth-sock → [:env "SSH_AUTH_SOCK"] → "<redacted>" :env/ssh-askpass → [:env "SSH_ASKPASS"] → "<redacted>" :env/ssh-private-key → [:env "SSH_PRIVATE_KEY"] → "<redacted>"
Any Tainted encountered during the walk is replaced with its
pr-str form (the redacted print-method output) — no rule needed.
redact never invokes untaint and never reads :value directly.
It is safe to compose into a logger appender.
Redaction registry + walker for safely surfacing structured data
(compiled plans, event payloads, env maps, log lines) to an AI
assistant or any debug surface.
## Model
A *rule* is a map:
{:select <selector> ; path-vector OR predicate fn (key,val)→bool
:replace <fn> ; (fn [val] -> replacement)
:id <keyword> ; assigned by register-rule!
:doc <string?>}
Rules are stored in a global atom registry. `redact` walks input via
`clojure.walk/postwalk`, applying matching rules and auto-replacing
any `Tainted` it encounters with its print-method form.
## Selector forms
- Vector path — `[:env "SSHPASS"]` matches the value at that path
inside any nested map.
- Predicate fn — `(fn [k v] ...)` invoked for every map entry; truthy
return → replace the value.
## Default rules
Registered at namespace load time:
:env/sshpass → [:env "SSHPASS"] → "<redacted>"
:env/ssh-auth-sock → [:env "SSH_AUTH_SOCK"] → "<redacted>"
:env/ssh-askpass → [:env "SSH_ASKPASS"] → "<redacted>"
:env/ssh-private-key → [:env "SSH_PRIVATE_KEY"] → "<redacted>"
## Tainted handling
Any `Tainted` encountered during the walk is replaced with its
`pr-str` form (the redacted print-method output) — no rule needed.
## Privacy contract
`redact` never invokes `untaint` and never reads `:value` directly.
It is safe to compose into a logger appender.Structural redactor for ssh argv vectors.
Preserve the shape of the argv (every flag in its place, every
option preserved, count unchanged) while replacing Tainted slots with
stable hash tokens like <host:#a3f4> / <id:#7c2>.
The redactor is structurally aware: it understands the standard
ssh(1) flag syntax and knows which slots can hold a hostname /
identity / port. But it is intentionally CONSERVATIVE — it ONLY
touches slots whose contents are Tainted. Plain strings pass
through unchanged.
A hostname is whatever the user happens to call their VPS. We don't
want to pattern-match hostnames (false positives, false negatives).
Instead, the credential resolver / plan compiler decides what is
sensitive by wrapping in Tainted. The redactor's job is to take
that signal and present it as a token rather than a <redacted ...>
blob — preserving structural fidelity for debugging.
Single-arg flags (the next argv slot is the value):
-l <user> → identity -i <keyfile> → identity (keyfile path) -p <port> → port -o <key=val> → option (passed through; we don't parse it) -F, -E, -S, -W, -L, -R, -D, -J, -Q, -e, -m, -c, -O, -B (each of the above takes a value in the following slot)
Boolean flags (no following value): -1 -2 -4 -6 -A -a -C -f -G -g -K -k -M -N -n -q -s -T -t -V -v -X -x -Y -y
Anything that doesn't look like a flag and appears after we've consumed flags is treated as the destination (host, or user@host).
Plain strings: pass through verbatim.
Tainted slots: replaced with their <src:#hash> token form. If a
Tainted carries :source :argv-host, you get
<host:#xxxx>; for :argv-id you get <id:#xxxx>;
the source label IS the token prefix.
Repeated Tainted with the same underlying value within a single argv produces the SAME token (because the hash is value-stable within a JVM). Operators can spot "this argv references the same host twice" at a glance.
The function does NOT call untaint and does NOT lengthen or shorten the argv. Output count == input count.
Structural redactor for ssh argv vectors.
## Goal
Preserve the *shape* of the argv (every flag in its place, every
option preserved, count unchanged) while replacing Tainted slots with
stable hash tokens like `<host:#a3f4>` / `<id:#7c2>`.
The redactor is structurally aware: it understands the standard
ssh(1) flag syntax and knows which slots can hold a hostname /
identity / port. But it is intentionally CONSERVATIVE — it ONLY
touches slots whose contents are `Tainted`. Plain strings pass
through unchanged.
## Why conservative
A hostname is whatever the user happens to call their VPS. We don't
want to pattern-match hostnames (false positives, false negatives).
Instead, the credential resolver / plan compiler decides what is
sensitive by wrapping in `Tainted`. The redactor's job is to take
that signal and present it as a token rather than a `<redacted ...>`
blob — preserving structural fidelity for debugging.
## Recognized ssh flag idioms
Single-arg flags (the next argv slot is the value):
-l <user> → identity
-i <keyfile> → identity (keyfile path)
-p <port> → port
-o <key=val> → option (passed through; we don't parse it)
-F, -E, -S, -W, -L, -R, -D, -J, -Q, -e, -m, -c, -O, -B
(each of the above takes a value in the following slot)
Boolean flags (no following value): -1 -2 -4 -6 -A -a -C -f -G -g
-K -k -M -N -n -q -s -T -t -V -v -X -x -Y -y
Anything that doesn't look like a flag and appears after we've
consumed flags is treated as the destination (host, or user@host).
## Behavior
Plain strings: pass through verbatim.
Tainted slots: replaced with their `<src:#hash>` token form. If a
Tainted carries `:source` :argv-host, you get
`<host:#xxxx>`; for :argv-id you get `<id:#xxxx>`;
the source label IS the token prefix.
Repeated Tainted with the same underlying value within a single
argv produces the SAME token (because the hash is value-stable
within a JVM). Operators can spot "this argv references the same
host twice" at a glance.
The function does NOT call untaint and does NOT lengthen or shorten
the argv. Output count == input count.Tainted-value wrapper for sensitive material that must NOT reach an AI assistant or any debug surface — but whose presence and identity should still be observable via stable per-run hash tokens.
Where hive-system.secrets.core/Secret is the storage primitive for
credentials ("don't print, don't compare, don't serialize"), Tainted
is a diagnostic primitive: it wraps any value (hostnames, ports,
identities, env values, argv slots) so we can correlate the same
underlying value across many log lines with a short hash token like
<host:#a3f4> — without ever printing the value itself.
Tainted is a defrecord with three fields:
:value — the wrapped sensitive value :source — non-sensitive label (e.g. "pass:vps/r1/ip", :argv-host) :hash — 4-hex-char stable token derived from HMAC-SHA256(salt, value)
<host:#a3f4> across log lines.print-method ALWAYS emits <redacted src=... h=#xxxx> and NEVER
the underlying value. toString mirrors that.
untaint — the ONE blessed unwrap. Every call site is a leak risk.
This is the only safe extraction point and MUST only be invoked at
trust boundaries (process spawn, network write, file write to a
trusted local sink). Never call it within reach of a log appender,
event subscriber, or anything whose output may be surfaced to the
assistant.Secret apply.Tainted-value wrapper for sensitive material that must NOT reach an AI
assistant or any debug surface — but whose *presence and identity*
should still be observable via stable per-run hash tokens.
## Purpose
Where `hive-system.secrets.core/Secret` is the storage primitive for
credentials ("don't print, don't compare, don't serialize"), `Tainted`
is a *diagnostic* primitive: it wraps any value (hostnames, ports,
identities, env values, argv slots) so we can correlate the same
underlying value across many log lines with a short hash token like
`<host:#a3f4>` — without ever printing the value itself.
## Type
`Tainted` is a defrecord with three fields:
:value — the wrapped sensitive value
:source — non-sensitive label (e.g. "pass:vps/r1/ip", :argv-host)
:hash — 4-hex-char stable token derived from HMAC-SHA256(salt, value)
## Hash semantics
- Hash is computed once at construction and cached.
- Same value within a single JVM run produces the SAME hash token,
so operators can correlate `<host:#a3f4>` across log lines.
- Salt is generated per JVM at namespace-load time via SecureRandom
and held in a private atom — cross-JVM-restart unlinkability is by
design (the salt cannot be reconstructed from prior runs).
- Hash space is 16^4 = 65,536 — collision probability is acceptable
within a single run's working set of distinct sensitive values.
## Print safety
`print-method` ALWAYS emits `<redacted src=... h=#xxxx>` and NEVER
the underlying value. `toString` mirrors that.
## Audit surface
- `untaint` — the ONE blessed unwrap. Every call site is a leak risk.
This is the only safe extraction point and MUST only be invoked at
trust boundaries (process spawn, network write, file write to a
trusted local sink). Never call it within reach of a log appender,
event subscriber, or anything whose output may be surfaced to the
assistant.
## Non-goals
- Encryption / secure memory wiping. JVM strings can't be reliably
zeroed; the same caveats as `Secret` apply.
- Cross-process correlation. By design — see Hash semantics above.Opaque secret container for credentials, keys, and other sensitive values.
Carry sensitive values through the system without leaking them via
logs, REPL output, exception traces, pretty-printers, or accidental
stdout. Unwrapping requires an explicit expose call — auditable,
greppable, intentional.
Secret is a deftype (not defrecord) — it is intentionally NOT a map.
Keyword lookup ((:value s)), get, and map destructuring all return
nil. The underlying field is reachable only via Java interop
((.value s)), which is greppable and easy to ban via lint.
expose — the ONE blessed unwrap. Every call site is a leak risk.(.value <Secret>) — interop escape hatch; ban in lint.Registered for print-method, print-dup, and clojure.pprint/simple-dispatch.
toString overrides Object — covers exception messages, log lines.
Hash is constant (0); equality is constant-time. Never serializable.
Opaque secret container for credentials, keys, and other sensitive values. ## Purpose Carry sensitive values through the system without leaking them via logs, REPL output, exception traces, pretty-printers, or accidental stdout. Unwrapping requires an explicit `expose` call — auditable, greppable, intentional. ## Type `Secret` is a deftype (not defrecord) — it is intentionally NOT a map. Keyword lookup (`(:value s)`), `get`, and map destructuring all return nil. The underlying field is reachable only via Java interop (`(.value s)`), which is greppable and easy to ban via lint. ## Audit surface - `expose` — the ONE blessed unwrap. Every call site is a leak risk. - `(.value <Secret>)` — interop escape hatch; ban in lint. ## Print safety Registered for `print-method`, `print-dup`, and `clojure.pprint/simple-dispatch`. `toString` overrides Object — covers exception messages, log lines. Hash is constant (0); equality is constant-time. Never serializable. ## Non-goals (v1) - Secure memory wiping. JVM strings cannot be reliably zeroed; this would be theater for SSH-pass / API-token use cases. Use char[] wrappers if you need defense against heap dumps. - Encryption at rest. This wraps live values in memory, not storage.
GNU pass (password-store) ISecretBackend implementation.
Resolves entries from the user's pass store into opaque Secret
values. Never logs the resolved content. Stderr is captured but
sanitized in error returns.
->PassBackend — defrecord constructor (preferred via DIP)pass-show — convenience: full output as one Secretpass-show-line — convenience: first non-empty line onlypass-available? — convenience: Result<bool> backend health:pass/not-found — entry does not exist:pass/empty — entry exists but resolved to blank:pass/exec-fail — pass binary failed (binary missing, gpg lock, etc.):pass/timeout — pass invocation exceeded deadlineErrors include the lookup :key (NOT the resolved value) for diagnostics.
GNU pass (password-store) ISecretBackend implementation. Resolves entries from the user's `pass` store into opaque Secret values. Never logs the resolved content. Stderr is captured but sanitized in error returns. ## API - `->PassBackend` — defrecord constructor (preferred via DIP) - `pass-show` — convenience: full output as one Secret - `pass-show-line` — convenience: first non-empty line only - `pass-available?` — convenience: Result<bool> backend health ## Backend errors - `:pass/not-found` — entry does not exist - `:pass/empty` — entry exists but resolved to blank - `:pass/exec-fail` — `pass` binary failed (binary missing, gpg lock, etc.) - `:pass/timeout` — pass invocation exceeded deadline Errors include the lookup `:key` (NOT the resolved value) for diagnostics.
Protocols for the secrets subsystem.
ISecretBackend is the DIP seam between consumers (probe, hive-mcp,
etc.) and concrete secret stores (GNU pass, Bitwarden CLI, 1Password,
Vault, age-encrypted files, etc.).
New backends implement this protocol; consumers depend only on the protocol. No call site needs to change to swap or add backends.
A backend MUST:
Result<Secret> from fetch — never raw values, never
plaintext bound in a map outside the Secret type.:line-only? and :timeout-ms opts (may ignore others).available? cheaply (no network round-trip if avoidable).backend-id keyword (e.g. :pass,
:bw, :op, :vault, :age).Protocols for the secrets subsystem. ## ISP boundary `ISecretBackend` is the DIP seam between consumers (probe, hive-mcp, etc.) and concrete secret stores (GNU pass, Bitwarden CLI, 1Password, Vault, age-encrypted files, etc.). New backends implement this protocol; consumers depend only on the protocol. No call site needs to change to swap or add backends. ## Backend contract A backend MUST: - Return `Result<Secret>` from `fetch` — never raw values, never plaintext bound in a map outside the Secret type. - Surface `:line-only?` and `:timeout-ms` opts (may ignore others). - Report `available?` cheaply (no network round-trip if avoidable). - Use a stable, namespaced `backend-id` keyword (e.g. `:pass`, `:bw`, `:op`, `:vault`, `:age`).
Backend registry + uniform resolution for ISecretBackend implementations.
Consumers depend on this ns + protocols.clj. They never need to know
which concrete backend (pass, bw, op, vault, age) is in use.
At process boot, register the backends you need: (registry/register! (pass/make-pass-backend)) (registry/register! (bw/make-bw-backend {:session ...}))
At call sites, resolve via a SecretRef map: (registry/fetch {:backend :pass :key "vps/r1/root"}) (registry/fetch {:backend :pass :key "vps/r1/ip" :opts {:line-only? true}})
Returns Result<Secret>.
{:backend <keyword> ;; backend-id (e.g. :pass, :bw) :key <string> ;; opaque key passed to the backend :opts <map?>} ;; optional per-fetch opts
validate-ref checks structure WITHOUT touching the backend — pure,
safe to run during config loading.
Backend registry + uniform resolution for ISecretBackend implementations.
Consumers depend on this ns + protocols.clj. They never need to know
which concrete backend (`pass`, `bw`, `op`, `vault`, `age`) is in use.
## Workflow
1. At process boot, register the backends you need:
(registry/register! (pass/make-pass-backend))
(registry/register! (bw/make-bw-backend {:session ...}))
2. At call sites, resolve via a SecretRef map:
(registry/fetch {:backend :pass :key "vps/r1/root"})
(registry/fetch {:backend :pass :key "vps/r1/ip" :opts {:line-only? true}})
Returns Result<Secret>.
## SecretRef shape
{:backend <keyword> ;; backend-id (e.g. :pass, :bw)
:key <string> ;; opaque key passed to the backend
:opts <map?>} ;; optional per-fetch opts
## Validation
`validate-ref` checks structure WITHOUT touching the backend — pure,
safe to run during config loading.Binary identity: which names on PATH denote a program.
(bin :rg) => "rg" (names :fd) => ["fd" "fdfind"] (locate :fd) => (ok {:path "/usr/bin/fdfind" :bin "fdfind"})
A binary identity answers exactly one question: what is this program called
where it is installed. It says nothing about how to install it (that is
shell.tools, which owns provisioning) and nothing about how a pattern
enters its argv (that is shell.search, which owns grammar). Both of those
registries reference an id here rather than restating the names.
An id is registered under :binary/id AND under every :binary/aliases
member, all pointing at the same map, so a lookup is a plain get and the
value always names its own canonical id. shell.tools has keyed ripgrep as
:ripgrep since before shell.search keyed it :rg; both resolve, and
neither registry has to know what the other calls it.
Collect (registry reads) / Promote (pure derivation over a Binary VALUE) /
Boundary (locate, the only form here that touches PATH). The probe is a
port, so a caller — a test above all — supplies its own adapter instead of
redefining somebody's var.
Binary identity: which names on PATH denote a program.
(bin :rg) => "rg"
(names :fd) => ["fd" "fdfind"]
(locate :fd) => (ok {:path "/usr/bin/fdfind" :bin "fdfind"})
## What belongs here, and what does not
A binary identity answers exactly one question: what is this program called
where it is installed. It says nothing about how to install it (that is
`shell.tools`, which owns provisioning) and nothing about how a pattern
enters its argv (that is `shell.search`, which owns grammar). Both of those
registries reference an id here rather than restating the names.
## Aliases
An id is registered under `:binary/id` AND under every `:binary/aliases`
member, all pointing at the same map, so a lookup is a plain `get` and the
value always names its own canonical id. `shell.tools` has keyed ripgrep as
`:ripgrep` since before `shell.search` keyed it `:rg`; both resolve, and
neither registry has to know what the other calls it.
## Layout
Collect (registry reads) / Promote (pure derivation over a Binary VALUE) /
Boundary (`locate`, the only form here that touches PATH). The probe is a
port, so a caller — a test above all — supplies its own adapter instead of
redefining somebody's var.IShell implementation via ProcessBuilder. All operations return hive-dsl Results.
Stream draining and process-tree teardown are shared with the IProcess implementation — see hive-system.process.streams / .tree. Both faced the same hazard (a descendant holding an inherited pipe), and one copy of the answer is the point.
IShell implementation via ProcessBuilder. All operations return hive-dsl Results. Stream draining and process-tree teardown are shared with the IProcess implementation — see hive-system.process.streams / .tree. Both faced the same hazard (a descendant holding an inherited pipe), and one copy of the answer is the point.
Detect available package managers and system capabilities. Delegates to babashka.fs for path resolution.
Detect available package managers and system capabilities. Delegates to babashka.fs for path resolution.
Pure POSIX shell-quoting helpers.
Two functions:
shell-quote — single-quote a string for safe embedding in a
sh -c command, escaping embedded single-quotes and quoting the
empty string as '' so it survives tokenization.join-as-cmd — join an argv into one shell-command string by
quoting each element with shell-quote.Re-quoting an already-quoted string nests correctly — each sh -c
peel removes exactly one quoting layer.
Pure POSIX shell-quoting helpers.
Two functions:
- `shell-quote` — single-quote a string for safe embedding in a
`sh -c` command, escaping embedded single-quotes and quoting the
empty string as `''` so it survives tokenization.
- `join-as-cmd` — join an argv into one shell-command string by
quoting each element with `shell-quote`.
Re-quoting an already-quoted string nests correctly — each `sh -c`
peel removes exactly one quoting layer.Building an argv for a pattern-taking CLI from a Construct.
Callers used to hand-assemble a pattern string and hand it to rg. A
lookahead in that string reaches rg, which rejects it at runtime with
regex parse error: look-around, including look-ahead and look-behind, is not supported
— a failure the caller can only discover by spawning a process and then parsing English out of stderr. The dialect gate already knows the answer from the shape of the construct, so the refusal belongs here, before the fork, naming the missing capability as data.
With grep the argument is stronger still. GNU grep does not reject what it
cannot do: grep -E '\d+' matches a literal d, exit 0, no warning. There
the gate is not saving the caller a stderr parse, it is saving them a wrong
answer they had no way to notice.
A tool is a Tool map: which dialect it speaks, what it needs in order to
speak it, and how a pattern enters its argv. Adding ugrep is a register!
call, not an edit to argv — the open set gets a registry, not a case.
An argv is a claim about a program's parser, and only that program can
settle it. rg, sd and grep are driven for real by the suite, and the
separator rule argv applies is what those runs established.
fd is measured too, as of fd 9.0.0: sd's positional shape holds for it,
including the negative control — strip the -- and fd reads -dash-[0-9]+
as --max-depth ash-[0-9]+ and exits 2. What that run also established is
that the argv was not the whole claim. Debian installs the binary as
fdfind, so the shape was right, the program was present, and the command
was still unrunnable. Hence executable and spawn-argv: argv states the
grammar, and resolving argv[0] against PATH is a separate question with its
own refusal.
A PATH grep is frequently ugrep or another superset, which would accept
constructs POSIX ERE cannot express. That only ever widens what runs, never
narrows it, so emitting strict ERE stays correct whichever binary answers.
Building an argv for a pattern-taking CLI from a Construct. ## What this is for Callers used to hand-assemble a pattern string and hand it to `rg`. A lookahead in that string reaches rg, which rejects it at runtime with regex parse error: look-around, including look-ahead and look-behind, is not supported — a failure the caller can only discover by spawning a process and then parsing English out of stderr. The dialect gate already knows the answer from the shape of the construct, so the refusal belongs here, before the fork, naming the missing capability as data. With `grep` the argument is stronger still. GNU grep does not reject what it cannot do: `grep -E '\d+'` matches a literal `d`, exit 0, no warning. There the gate is not saving the caller a stderr parse, it is saving them a wrong answer they had no way to notice. ## Tools are data, not code A tool is a `Tool` map: which dialect it speaks, what it needs in order to speak it, and how a pattern enters its argv. Adding `ugrep` is a `register!` call, not an edit to `argv` — the open set gets a registry, not a `case`. ## What is measured An argv is a claim about a program's parser, and only that program can settle it. `rg`, `sd` and `grep` are driven for real by the suite, and the separator rule `argv` applies is what those runs established. `fd` is measured too, as of fd 9.0.0: sd's positional shape holds for it, including the negative control — strip the `--` and fd reads `-dash-[0-9]+` as `--max-depth ash-[0-9]+` and exits 2. What that run also established is that the argv was not the whole claim. Debian installs the binary as `fdfind`, so the shape was right, the program was present, and the command was still unrunnable. Hence `executable` and `spawn-argv`: `argv` states the grammar, and resolving argv[0] against PATH is a separate question with its own refusal. A PATH `grep` is frequently ugrep or another superset, which would accept constructs POSIX ERE cannot express. That only ever widens what runs, never narrows it, so emitting strict ERE stays correct whichever binary answers.
Tool provisioning: descriptions and install hints per package manager.
(require-tool :ripgrep) => (ok {:path "/usr/bin/rg"}) or (err :tool/missing {:hints [...]})
What a tool is CALLED is not stated here — it is one fact, owned by
shell.binary, which shell.search reads too. This namespace answers
"is it here, and how do I install it"; that one answers "what is it
called".
Tool provisioning: descriptions and install hints per package manager.
(require-tool :ripgrep) => (ok {:path "/usr/bin/rg"})
or (err :tool/missing {:hints [...]})
What a tool is CALLED is not stated here — it is one fact, owned by
`shell.binary`, which `shell.search` reads too. This namespace answers
"is it here, and how do I install it"; that one answers "what is it
called".IJournal implementation backed by Datahike.
Stores operation executions as Datalog entities with bitemporal attributes:
Schema is installed on first connection. Idempotent.
IJournal implementation backed by Datahike. Stores operation executions as Datalog entities with bitemporal attributes: - :op/tx-time — when the operation was recorded (Datahike transaction time) - :op/valid-time — when the operation actually occurred (wall clock) - :op/type — keyword (:process/spawn, :network/connect, :shell/exec, etc.) - :op/input — EDN-serialized input args - :op/output — EDN-serialized Result (ok value or err) - :op/duration-ms — wall-clock duration - :op/success? — boolean (derived from Result) - :op/caller — agent-id or context string - :op/session — session identifier - :op/causal-prev — reference to prior operation (causal chain) Schema is installed on first connection. Idempotent.
No vars found in this namespace.
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 |