Capability contract vocabulary: the declared, machine-readable description of what one MCP tool's commands are, what they accept, and how stable they are.
Contract: a manifest is DATA. It holds no handlers and never dereferences a :schema — schema refs are registry keys, resolved by hive-spi.schema.capability. Deps: hive-dsl + malli only.
Capability contract vocabulary: the declared, machine-readable description of what one MCP tool's commands are, what they accept, and how stable they are. Contract: a manifest is DATA. It holds no handlers and never dereferences a :schema — schema refs are registry keys, resolved by hive-spi.schema.capability. Deps: hive-dsl + malli only.
CLI-style subcommand dispatch for a consolidated tool.
make-handler turns a handler tree into a single fn dispatching on the
:command parameter, supporting n-depth paths ("status list") alongside
single-word commands.
CLI-style subcommand dispatch for a consolidated tool.
`make-handler` turns a handler tree into a single fn dispatching on the
:command parameter, supporting n-depth paths ("status list") alongside
single-word commands.The tool-result shape a CLI handler returns.
A host may enrich these — attaching recovery hints or pending agent
instructions — by passing its own builders to hive-addon.cli/make-handler.
The tool-result shape a CLI handler returns. A host may enrich these — attaching recovery hints or pending agent instructions — by passing its own builders to `hive-addon.cli/make-handler`.
Command-path parsing and handler-tree resolution.
A handler tree maps keyword segments either to a handler fn or to a nested tree. A nested tree may carry :_handler as the value used when a path stops there, or when no deeper segment matches.
Command-path parsing and handler-tree resolution. A handler tree maps keyword segments either to a handler fn or to a nested tree. A nested tree may carry :_handler as the value used when a path stops there, or when no deeper segment matches.
Soft resolution of HOST services an addon consumes.
An addon is published to maven; its host is not. A load-time
(:require [hive-mcp.x :as y]) therefore makes the published artifact
unloadable wherever the host is absent. This namespace is the seam that
replaces such a require: the host var is resolved THROUGH THE VAR at call
time, and a missing host degrades to a Result instead of a load failure.
Surface: resolve-var symbol -> var | nil, never throws available? symbol -> boolean soft symbol -> fn, the per-call seam defsoft def a soft fn at a call site api {k symbol} -> {k soft-fn}, a var-map of them
JVM only (.clj): requiring-resolve has no ClojureScript counterpart, and
the host boundary this covers is a JVM boundary.
Soft resolution of HOST services an addon consumes.
An addon is published to maven; its host is not. A load-time
`(:require [hive-mcp.x :as y])` therefore makes the published artifact
unloadable wherever the host is absent. This namespace is the seam that
replaces such a require: the host var is resolved THROUGH THE VAR at call
time, and a missing host degrades to a Result instead of a load failure.
Surface:
resolve-var symbol -> var | nil, never throws
available? symbol -> boolean
soft symbol -> fn, the per-call seam
defsoft def a soft fn at a call site
api {k symbol} -> {k soft-fn}, a var-map of them
JVM only (.clj): `requiring-resolve` has no ClojureScript counterpart, and
the host boundary this covers is a JVM boundary.Hot-reload for IAddon instances — the public surface a host consumes.
Reloading a namespace updates VARS. It does not update the addon INSTANCE the
host registered at mount time, nor the sibling instances that were injected
into its dependents' configs. Those objects were captured when they were
constructed and are frozen; a namespace reload leaves the system holding old
objects that call new code, which is the silent-corruption shape. What this
namespace does is reconstruct them from the one thing a reload cannot
invalidate: the MountSpec, which carries :addon/init-ns + :addon/init-fn
as data. Remount is therefore the ordinary mount pipeline re-run over the
affected slice, not a second registry.
Layout: hive-addon.hot.source — where an addon's source lives (:local/root vs jar) hive-addon.hot.cascade — pure: which addons a reload touches, in what order hive-addon.hot.strategy — OCP: how each addon is reloaded hive-addon.hot — this facade: wiring, triggering, reporting
hive-hot is a SOFT dependency, resolved through the var at call time and never
required. Without it every function here still works — reload-addon! falls
back to require :reload — and hot! degrades to a report saying
:hot/available? false instead of throwing. Consumers that want file-watching
add io.github.hive-agi/hive-hot themselves.
THE ONE HARD RULE: never reload hive-addon.protocol. Reloading a
protocol-defining namespace mints a new protocol var, and every live addon
instance — same class NAME, different class OBJECT — stops satisfying it, so
dispatch misses with an error that reads as if the method were never
implemented. no-reload is that rule as data; feed it to hive-hot's
:no-reload and the hazard cannot be tripped.
Hot-reload for IAddon instances — the public surface a host consumes. Reloading a namespace updates VARS. It does not update the addon INSTANCE the host registered at mount time, nor the sibling instances that were injected into its dependents' configs. Those objects were captured when they were constructed and are frozen; a namespace reload leaves the system holding old objects that call new code, which is the silent-corruption shape. What this namespace does is reconstruct them from the one thing a reload cannot invalidate: the MountSpec, which carries `:addon/init-ns` + `:addon/init-fn` as data. Remount is therefore the ordinary mount pipeline re-run over the affected slice, not a second registry. Layout: hive-addon.hot.source — where an addon's source lives (:local/root vs jar) hive-addon.hot.cascade — pure: which addons a reload touches, in what order hive-addon.hot.strategy — OCP: how each addon is reloaded hive-addon.hot — this facade: wiring, triggering, reporting hive-hot is a SOFT dependency, resolved through the var at call time and never required. Without it every function here still works — `reload-addon!` falls back to `require :reload` — and `hot!` degrades to a report saying :hot/available? false instead of throwing. Consumers that want file-watching add io.github.hive-agi/hive-hot themselves. THE ONE HARD RULE: never reload hive-addon.protocol. Reloading a protocol-defining namespace mints a new protocol var, and every live addon instance — same class NAME, different class OBJECT — stops satisfying it, so dispatch misses with an error that reads as if the method were never implemented. `no-reload` is that rule as data; feed it to hive-hot's :no-reload and the hazard cannot be tripped.
Which addons a reload actually touches — the pure Pipeline stratum of the hot-reload bridge. No IO, no var resolution, no host.
Reloading addon A is not enough. Every addon that received A's INSTANCE at
mount time (through the mounter's :mount/dependencies sibling injection)
still holds the pre-reload object; leaving them alone leaves the system half
old and half new, which is the same silent-corruption shape as a partial
namespace reload. So a reload seeds at the changed addons and closes forward
over the dependency graph.
The graph is NOT recomputed here: edges and solve come from
hive-addon.mount.solve, so the order a reload uses is the same order the
original mount used, produced by the same rule chain (and extended by the
same custom :rules).
Which addons a reload actually touches — the pure Pipeline stratum of the hot-reload bridge. No IO, no var resolution, no host. Reloading addon A is not enough. Every addon that received A's INSTANCE at mount time (through the mounter's `:mount/dependencies` sibling injection) still holds the pre-reload object; leaving them alone leaves the system half old and half new, which is the same silent-corruption shape as a partial namespace reload. So a reload seeds at the changed addons and closes forward over the dependency graph. The graph is NOT recomputed here: `edges` and `solve` come from hive-addon.mount.solve, so the order a reload uses is the same order the original mount used, produced by the same rule chain (and extended by the same custom `:rules`).
Inject an addon that was NOT on the classpath when the host booted.
inject! is the mount pipeline run over a slice that did not exist yet:
put the addon's paths on the live classpath, discover the manifests under
them (and ONLY under them — an addon already on the classpath that the
composer chose not to mount must not be resurrected by a scan), solve the
new specs against the peers already mounted, tear down the mounted
dependents that now have a new sibling to receive, mount the slice through
the ordinary IMountDriver, and register the new addons with hive-hot so
they reload like the rest.
Classpath extension is a JVM concern: the URL is added to the highest
DynamicClassLoader above the calling thread's context loader, which is the
loader every later require resolves through in an nREPL-hosted image. A
thread with no DynamicClassLoader in its chain cannot extend the classpath;
inject! then REFUSES with :hot/no-dynamic-classloader rather than mounting
code the next require would fail to find.
Maven dependencies of the injected addon are NOT resolved by default; pass
:resolve-deps? true to hand the project's deps.edn :deps to
clojure.repl.deps/add-libs first (needs a tools.deps basis in the image).
Inject an addon that was NOT on the classpath when the host booted. `inject!` is the mount pipeline run over a slice that did not exist yet: put the addon's paths on the live classpath, discover the manifests under them (and ONLY under them — an addon already on the classpath that the composer chose not to mount must not be resurrected by a scan), solve the new specs against the peers already mounted, tear down the mounted dependents that now have a new sibling to receive, mount the slice through the ordinary IMountDriver, and register the new addons with hive-hot so they reload like the rest. Classpath extension is a JVM concern: the URL is added to the highest DynamicClassLoader above the calling thread's context loader, which is the loader every later `require` resolves through in an nREPL-hosted image. A thread with no DynamicClassLoader in its chain cannot extend the classpath; inject! then REFUSES with :hot/no-dynamic-classloader rather than mounting code the next require would fail to find. Maven dependencies of the injected addon are NOT resolved by default; pass `:resolve-deps? true` to hand the project's deps.edn :deps to clojure.repl.deps/add-libs first (needs a tools.deps basis in the image).
JVM adapter: IMountDriver over hive-addon.mount.boundary.
boundary reaches the classpath, JarFile, URL and the context classloader, so it is the host-bound half of a remount and lives behind the port rather than inside the portable core.
JVM adapter: IMountDriver over hive-addon.mount.boundary. boundary reaches the classpath, JarFile, URL and the context classloader, so it is the host-bound half of a remount and lives behind the port rather than inside the portable core.
Ports through which the portable hot-reload core reaches host-specific work.
The core — schema, cascade, strategy — is portable .cljc and names only these protocols. One adapter namespace per host implements them; a per-host wiring namespace constructs the adapters and injects them into the reload context.
Adapters are constructed by FUNCTIONS, never held in a def or defonce: an
instance built at load time keeps the class object of whichever protocol var
was current then, and stops satisfying the protocol once this namespace is
reloaded.
Ports through which the portable hot-reload core reaches host-specific work. The core — schema, cascade, strategy — is portable .cljc and names only these protocols. One adapter namespace per host implements them; a per-host wiring namespace constructs the adapters and injects them into the reload context. Adapters are constructed by FUNCTIONS, never held in a `def` or `defonce`: an instance built at load time keeps the class object of whichever protocol var was current then, and stops satisfying the protocol once this namespace is reloaded.
Malli value objects for the IAddon hot-reload bridge.
Shapes are uncompiled malli DATA (house idiom: PascalCase defs) seeded into a
LOCAL composite registry that COMPOSES hive-addon.mount.schema's registry —
AddonId/MountResult/MountSpec are reused, never redefined. The registry is
NEVER installed as the malli global default; reach it via
schema/validate/explain/validate* or by passing {:registry registry}.
Hot shapes are registered under :hot/* keys (:hot/registration, :hot/report, :hot/remount-report, :hot/source, :hot/strategy-id).
Two invariants are carried as DATA rather than prose:
Malli value objects for the IAddon hot-reload bridge.
Shapes are uncompiled malli DATA (house idiom: PascalCase defs) seeded into a
LOCAL composite registry that COMPOSES hive-addon.mount.schema's registry —
AddonId/MountResult/MountSpec are reused, never redefined. The registry is
NEVER installed as the malli global default; reach it via
`schema`/`validate`/`explain`/`validate*` or by passing {:registry registry}.
Hot shapes are registered under :hot/* keys (:hot/registration, :hot/report,
:hot/remount-report, :hot/source, :hot/strategy-id).
Two invariants are carried as DATA rather than prose:
- RemountReport's :teardown/data-preserved? is [:= true] — the no-nuke
invariant inherited from TeardownReport.
- :hot/strategy-id is an open :keyword, never an enum. The strategy set is
extensible by any module (OCP); closing it here would be the defect.Where an addon's constructor namespace physically lives — the Collect stratum of the hot-reload bridge.
An addon wired as a :local/root dep (typically through an untracked
local.deps.edn override) puts its src on the classpath as a real DIRECTORY:
the bytes on disk can change, so clj-reload can watch and reload it. The same
addon consumed as an :mvn/version coordinate arrives inside a JAR, whose
bytes cannot change without a restart — hot-reload of it is not merely
unsupported, it is meaningless.
That distinction is the input to strategy selection, so it is resolved here as DATA (an AddonSource) rather than guessed at the reload site.
Pure apart from classpath lookup; never throws — an unresolvable namespace becomes {:hot/source-kind :absent}.
Where an addon's constructor namespace physically lives — the Collect stratum
of the hot-reload bridge.
An addon wired as a `:local/root` dep (typically through an untracked
local.deps.edn override) puts its `src` on the classpath as a real DIRECTORY:
the bytes on disk can change, so clj-reload can watch and reload it. The same
addon consumed as an `:mvn/version` coordinate arrives inside a JAR, whose
bytes cannot change without a restart — hot-reload of it is not merely
unsupported, it is meaningless.
That distinction is the input to strategy selection, so it is resolved here as
DATA (an AddonSource) rather than guessed at the reload site.
Pure apart from classpath lookup; never throws — an unresolvable namespace
becomes {:hot/source-kind :absent}.How an addon's new code is brought into a live system — the OCP seam of the hot-reload bridge.
The set of reload strategies is OPEN. Most addons want the default
(:remount: shut down, reconstruct from the manifest, re-initialize, cascade
to dependents), but an addon with particular needs — an open socket it must
drain, a native handle it cannot re-acquire, a stateless dispatcher that only
needs its vars refreshed — supplies its own. So strategies are a PROTOCOL with
a rule chain, never a case over a closed keyword set: closing this set would
be the defect, and adding a strategy must never mean editing this namespace.
Selection, in order:
:addon/reload-strategy <id> gets exactly that
strategy, looked up by id. A declared-but-unregistered id is an error, not
a silent fallback to the default — an addon that asked for special handling
and quietly got generic handling is worse than one that refused.-applies? answers true.
The built-in chain ends in a catch-all, so selection always succeeds.Registration is a var-held chain plus an install seam, mirroring the licence gate in hive-addon.mount.entitlement. Strategies are resolved from the var at CALL time, never captured at wiring time.
Portable: this namespace names no host API. The two effectful collaborators —
the namespace reloader and the mount driver — arrive through the reload
context (:hot/reload-ns!, :hot/mount-driver), so hive-hot stays a soft
dependency and the classpath-bound mount boundary stays behind
hive-addon.hot.port/IMountDriver.
How an addon's new code is brought into a live system — the OCP seam of the hot-reload bridge. The set of reload strategies is OPEN. Most addons want the default (`:remount`: shut down, reconstruct from the manifest, re-initialize, cascade to dependents), but an addon with particular needs — an open socket it must drain, a native handle it cannot re-acquire, a stateless dispatcher that only needs its vars refreshed — supplies its own. So strategies are a PROTOCOL with a rule chain, never a `case` over a closed keyword set: closing this set would be the defect, and adding a strategy must never mean editing this namespace. Selection, in order: 1. A spec that DECLARES `:addon/reload-strategy <id>` gets exactly that strategy, looked up by id. A declared-but-unregistered id is an error, not a silent fallback to the default — an addon that asked for special handling and quietly got generic handling is worse than one that refused. 2. Otherwise the first chain member whose `-applies?` answers true. The built-in chain ends in a catch-all, so selection always succeeds. Registration is a var-held chain plus an install seam, mirroring the licence gate in hive-addon.mount.entitlement. Strategies are resolved from the var at CALL time, never captured at wiring time. Portable: this namespace names no host API. The two effectful collaborators — the namespace reloader and the mount driver — arrive through the reload context (`:hot/reload-ns!`, `:hot/mount-driver`), so hive-hot stays a soft dependency and the classpath-bound mount boundary stays behind hive-addon.hot.port/IMountDriver.
Facade for the addon mounter — the public surface a host consumes.
Re-exports the pure solver (solve), the effectful boundary (mount!, dry-run, teardown!, discover-specs, parse-spec), the DIP config resolver + host constructor (resolve-config-default, atom-mount-host), and the schema validators/keys. mount-classpath! is the one-call composition root: discover-specs -> solve -> mount!. The IMountHost protocol is NOT re-exported (a protocol cannot be plain-def aliased) — implement it from its canonical home hive-addon.mount.port.
Rationale lives in hive memory (KG-linked), not here.
Facade for the addon mounter — the public surface a host consumes. Re-exports the pure solver (solve), the effectful boundary (mount!, dry-run, teardown!, discover-specs, parse-spec), the DIP config resolver + host constructor (resolve-config-default, atom-mount-host), and the schema validators/keys. mount-classpath! is the one-call composition root: discover-specs -> solve -> mount!. The IMountHost protocol is NOT re-exported (a protocol cannot be plain-def aliased) — implement it from its canonical home hive-addon.mount.port. Rationale lives in hive memory (KG-linked), not here.
Effectful boundary of the addon mounter — all IO and var resolution, injected through the IMountHost port and an optional config resolver.
Collect: discover-specs scans the classpath for META-INF/hive-addons/*.edn. Promote: parse-spec turns an EDN string into a validated MountSpec Result. Pipeline is hive-addon.mount.solve (pure, elsewhere). Boundary: mount!/dry-run/teardown! resolve constructors, inject already-mounted sibling instances into each dependent's config (DIP), and drive the host.
mount! GRACEFULLY DEGRADES: a spec that fails at any step is recorded in the MountReport and the loop CONTINUES; already-mounted addons are NEVER torn down on a mid-DAG failure. teardown! shuts down in reverse mount order and always reports :teardown/data-preserved? true (shutdown! never deletes data).
Effectful boundary of the addon mounter — all IO and var resolution, injected through the IMountHost port and an optional config resolver. Collect: discover-specs scans the classpath for META-INF/hive-addons/*.edn. Promote: parse-spec turns an EDN string into a validated MountSpec Result. Pipeline is hive-addon.mount.solve (pure, elsewhere). Boundary: mount!/dry-run/teardown! resolve constructors, inject already-mounted sibling instances into each dependent's config (DIP), and drive the host. mount! GRACEFULLY DEGRADES: a spec that fails at any step is recorded in the MountReport and the loop CONTINUES; already-mounted addons are NEVER torn down on a mid-DAG failure. teardown! shuts down in reverse mount order and always reports :teardown/data-preserved? true (shutdown! never deletes data).
Composition root: select discovered MountSpecs through declarative plug layers (build.edn / iaddon.edn), then order + mount them.
Joins the two pure engines by :addon/id — hive-addon.plug (SELECT, keyed by lib-sym) and hive-addon.mount (ORDER + EXECUTE, keyed by :addon/id). Per-addon plug :config overrides merge on top of the injected base config resolver. With no plug layers (or none carrying :iaddon/plugs) every discovered spec is kept — behaviourally identical to mount/mount-classpath!.
Composition root: select discovered MountSpecs through declarative plug layers (build.edn / iaddon.edn), then order + mount them. Joins the two pure engines by :addon/id — hive-addon.plug (SELECT, keyed by lib-sym) and hive-addon.mount (ORDER + EXECUTE, keyed by :addon/id). Per-addon plug :config overrides merge on top of the injected base config resolver. With no plug layers (or none carrying :iaddon/plugs) every discovered spec is kept — behaviourally identical to mount/mount-classpath!.
Whether a MountSpec is permitted to mount at all. Pure.
The mounter depends on this abstraction, never on a licence implementation: a gate is any ILicenseGate, or any (fn [spec] -> nil | reason-keyword). Returning nil permits; returning a keyword refuses and names the reason.
A spec whose :addon/trust-class is :proprietary is refused unless a gate is installed, so the default posture of an unconfigured host is closed.
Whether a MountSpec is permitted to mount at all. Pure. The mounter depends on this abstraction, never on a licence implementation: a gate is any ILicenseGate, or any (fn [spec] -> nil | reason-keyword). Returning nil permits; returning a keyword refuses and names the reason. A spec whose :addon/trust-class is :proprietary is refused unless a gate is installed, so the default posture of an unconfigured host is closed.
DIP seam for the addon mounter — the host registry abstraction.
IMountHost is the port through which the effectful boundary registers, initializes, shuts down, and looks up addon instances. hive-addon ships one in-memory implementation (atom-mount-host) for tests, dry-run, and non-MCP hosts; a real host (an MCP server) supplies its own. resolve-config-default is the identity-ish config resolver — a host may inject a richer one (e.g. hive-di-backed) at the boundary.
register!/shutdown! are no-nuke: a duplicate register! MUST NOT throw and shutdown! MUST NOT delete data.
DIP seam for the addon mounter — the host registry abstraction. IMountHost is the port through which the effectful boundary registers, initializes, shuts down, and looks up addon instances. hive-addon ships one in-memory implementation (atom-mount-host) for tests, dry-run, and non-MCP hosts; a real host (an MCP server) supplies its own. resolve-config-default is the identity-ish config resolver — a host may inject a richer one (e.g. hive-di-backed) at the boundary. register!/shutdown! are no-nuke: a duplicate register! MUST NOT throw and shutdown! MUST NOT delete data.
Malli value objects for the addon mounter.
Declarative mount manifests (MountSpec), the pure solver output (MountPlan),
and the effectful outcome reports (MountResult, MountReport, TeardownReport).
Shapes are uncompiled malli DATA (house idiom: PascalCase defs) seeded into a
LOCAL composite registry that COMPOSES hive-addon.schema's registry — the
AddonId/AddonType/CapabilitySet value objects are reused, never redefined.
The registry is NEVER installed as the malli global default; reach it via
schema/validate/explain/validate* or by passing {:registry registry}
yourself.
Mount shapes are registered under :mount/* keys (:mount/spec, :mount/plan, :mount/result, :mount/report, :mount/teardown-report).
TeardownReport carries the no-nuke invariant as data: :teardown/data-preserved? is [:= true], so a report can only validate when teardown preserved data.
Malli value objects for the addon mounter.
Declarative mount manifests (MountSpec), the pure solver output (MountPlan),
and the effectful outcome reports (MountResult, MountReport, TeardownReport).
Shapes are uncompiled malli DATA (house idiom: PascalCase defs) seeded into a
LOCAL composite registry that COMPOSES hive-addon.schema's registry — the
AddonId/AddonType/CapabilitySet value objects are reused, never redefined.
The registry is NEVER installed as the malli global default; reach it via
`schema`/`validate`/`explain`/`validate*` or by passing {:registry registry}
yourself.
Mount shapes are registered under :mount/* keys (:mount/spec, :mount/plan,
:mount/result, :mount/report, :mount/teardown-report).
TeardownReport carries the no-nuke invariant as data: :teardown/data-preserved?
is [:= true], so a report can only validate when teardown preserved data.Pure DAG solver for the addon mounter — spec set -> MountPlan. No IO, no var resolution.
Ordering constraints are a rule-chain: IDependencyRule turns the spec set
into directed [from-id to-id] edges (from mounts before to); edges folds the
chain into one edge set. Two built-in rules cover hard id deps
(:addon/dependencies) and looser capability deps (:addon/requires-capabilities
satisfied by any peer whose :addon/capabilities contains the cap).
solve is a pure deterministic fn of the spec SET: it Kahn topo-sorts with a
lowest-:addon/id lexicographic tie-break, so shuffled input yields an
identical :ordered. Cycles/missing-deps/unmet-capabilities are diagnosed as
data. Graceful by default (acyclic subset ordered, cycles reported); opt-in
:fail-closed-cycles true returns (r/err :mount/unsolvable ...).
PORTABLE STRATUM. This namespace is in the require closure of the hot-reload core and must load and BEHAVE IDENTICALLY on the JVM, cljw and cljrs. Three admission rules follow, each mechanically checkable and each derived from a measured divergence rather than from caution:
:clj does not select the
JVM: cljw presents :clj and cljrs presents :rust.for. On cljrs a second binding and :let are unbound-symbol errors,
and :when is SILENTLY IGNORED — in topo-sort that would emit a wrong
mount order while reporting success. mapcat/keep/reduce say the same
thing and are cleared on all three.:or destructuring default that is not SELF-EVALUATING. cljrs does not
evaluate the default, so a symbol default binds the symbol itself; rules
became the symbol default-rules, edges folded over a symbol into an
EMPTY edge set, and solve returned a lexicographic order reporting no
cycles. Literal defaults (false, 0, :kw) are unaffected, which is
exactly what hides the bug. Use (or x default) in the body.The three rules are enforced by hive-addon.mount.portable-test, which reads this stratum's source and fails on a violation, and the behaviour is pinned by the tri-runtime differential oracle in test/portable/oracle.cljc.
Pure DAG solver for the addon mounter — spec set -> MountPlan. No IO, no var resolution. Ordering constraints are a rule-chain: IDependencyRule turns the spec set into directed [from-id to-id] edges (from mounts before to); `edges` folds the chain into one edge set. Two built-in rules cover hard id deps (:addon/dependencies) and looser capability deps (:addon/requires-capabilities satisfied by any peer whose :addon/capabilities contains the cap). `solve` is a pure deterministic fn of the spec SET: it Kahn topo-sorts with a lowest-:addon/id lexicographic tie-break, so shuffled input yields an identical :ordered. Cycles/missing-deps/unmet-capabilities are diagnosed as data. Graceful by default (acyclic subset ordered, cycles reported); opt-in :fail-closed-cycles true returns (r/err :mount/unsolvable ...). PORTABLE STRATUM. This namespace is in the require closure of the hot-reload core and must load and BEHAVE IDENTICALLY on the JVM, cljw and cljrs. Three admission rules follow, each mechanically checkable and each derived from a measured divergence rather than from caution: - Zero reader conditionals and zero host interop. `:clj` does not select the JVM: cljw presents `:clj` and cljrs presents `:rust`. - No `for`. On cljrs a second binding and `:let` are unbound-symbol errors, and `:when` is SILENTLY IGNORED — in `topo-sort` that would emit a wrong mount order while reporting success. `mapcat`/`keep`/`reduce` say the same thing and are cleared on all three. - No `:or` destructuring default that is not SELF-EVALUATING. cljrs does not evaluate the default, so a symbol default binds the symbol itself; `rules` became the symbol `default-rules`, `edges` folded over a symbol into an EMPTY edge set, and solve returned a lexicographic order reporting no cycles. Literal defaults (`false`, `0`, `:kw`) are unaffected, which is exactly what hides the bug. Use `(or x default)` in the body. The three rules are enforced by hive-addon.mount.portable-test, which reads this stratum's source and fails on a violation, and the behaviour is pinned by the tri-runtime differential oracle in test/portable/oracle.cljc.
Facade for the OPAQUE addon seam: mount a compiled, source-free IAddon.
The problem this solves is commercial, not technical. A proprietary addon
cannot ship as a jar (every namespace name, every function name and the whole
call graph read straight out of it) and cannot ship as source at all. It
ships as ONE native binary with an embedded bytecode payload, built by
cljw build from the vendor's own code written against this library's
portable stratum, and the host mounts it through the generic proxy here.
Nothing in the mount path is special-cased for it. An opaque addon is an ORDINARY mount manifest with :addon/type :external and :addon/trust-class :proprietary, so the existing licence gate (hive-addon.mount.entitlement) governs it, and an unlicensed addon's constructor namespace is never loaded.
The vendor's side of the same wire is hive-addon.opaque.serve, and
entry-source writes the entry that calls it, so the kernel and the proxy
are framed by one library and cannot drift.
JVM-only (.clj): it reaches the subprocess transport and writes manifest files. The strata below it are .cljc, and the two the KERNEL loads (hive-addon.opaque.codec, hive-addon.opaque.serve) are malli-free.
Mounting works for any IAddon. Passing hive-native's opacity audit asks two more things of the vendor, both measured on a real cljw build, and neither guessable from the audit's output:
READ RECORD FIELDS AS BARE SYMBOLS inside defrecord method bodies, not
through keyword accessors. A field read as (:state this) escapes as a
keyword, so the mangler must KEEP the field name; read as state it is
renamed. This is the one change that moved a real kernel's accounting
from :unprovable to :accounted.
Note the trap: (:field this) is the idiom hive-addon's own portable
stratum REQUIRES, because cljrs does not bind record fields in method
bodies. That rule is about cljrs. A vendor kernel is compiled by cljw
alone, which binds them, so the portable rule does not apply to it and
following it out of habit costs the certification.
NAME THINGS THE AUDITOR CAN PROBE. Below hive-native's extraction floor (6 characters) a name cannot be searched for, so its absence from a report is not evidence it is gone, and the audit reports :unprovable rather than passing. A short name is not safer, it is unaudited.
What :opaque does NOT claim: it means nothing the tool can REMOVE survived. A tuned numeric constant still reaches the constant pool and is reported at :medium under :numeric-literal, a class no elision pass can strip. Code on a customer's CPU is recoverable in the end; opacity is a cost imposed on an attacker, never a lock.
Facade for the OPAQUE addon seam: mount a compiled, source-free IAddon. The problem this solves is commercial, not technical. A proprietary addon cannot ship as a jar (every namespace name, every function name and the whole call graph read straight out of it) and cannot ship as source at all. It ships as ONE native binary with an embedded bytecode payload, built by `cljw build` from the vendor's own code written against this library's portable stratum, and the host mounts it through the generic proxy here. Nothing in the mount path is special-cased for it. An opaque addon is an ORDINARY mount manifest with :addon/type :external and :addon/trust-class :proprietary, so the existing licence gate (hive-addon.mount.entitlement) governs it, and an unlicensed addon's constructor namespace is never loaded. The vendor's side of the same wire is hive-addon.opaque.serve, and `entry-source` writes the entry that calls it, so the kernel and the proxy are framed by one library and cannot drift. JVM-only (.clj): it reaches the subprocess transport and writes manifest files. The strata below it are .cljc, and the two the KERNEL loads (hive-addon.opaque.codec, hive-addon.opaque.serve) are malli-free. ## Writing a kernel that can certify :opaque Mounting works for any IAddon. Passing hive-native's opacity audit asks two more things of the vendor, both measured on a real cljw build, and neither guessable from the audit's output: 1. READ RECORD FIELDS AS BARE SYMBOLS inside defrecord method bodies, not through keyword accessors. A field read as `(:state this)` escapes as a keyword, so the mangler must KEEP the field name; read as `state` it is renamed. This is the one change that moved a real kernel's accounting from :unprovable to :accounted. Note the trap: `(:field this)` is the idiom hive-addon's own portable stratum REQUIRES, because cljrs does not bind record fields in method bodies. That rule is about cljrs. A vendor kernel is compiled by cljw alone, which binds them, so the portable rule does not apply to it and following it out of habit costs the certification. 2. NAME THINGS THE AUDITOR CAN PROBE. Below hive-native's extraction floor (6 characters) a name cannot be searched for, so its absence from a report is not evidence it is gone, and the audit reports :unprovable rather than passing. A short name is not safer, it is unaudited. What :opaque does NOT claim: it means nothing the tool can REMOVE survived. A tuned numeric constant still reaches the constant pool and is reported at :medium under :numeric-literal, a class no elision pass can strip. Code on a customer's CPU is recoverable in the end; opacity is a cost imposed on an attacker, never a lock.
The generic FOSS proxy: ONE IAddon record standing in for ANY opaque kernel.
This is the Single-Source Lever of the marketplace story. A proprietary addon ships as a compiled binary and nothing else; the record that mounts it is this one, MIT-licensed, generic, and identical for every vendor. That the proxy is open is not a leak: it holds no kernel logic at all, only framing and delegation. All of the IP is in the binary.
The kernel SELF-DESCRIBES, so this record learns the addon's tools, hooks, exclusions and schema extensions from the running kernel rather than from the manifest. A manifest that enumerated them would be a second copy of the truth and would rot the first time the vendor shipped a new build.
What the manifest still supplies is what must be known BEFORE the kernel runs: the addon id and its advertised capabilities. The mount pipeline reads both while deciding whether this addon is licensed and where it sits in the dependency order, which is strictly before anything has been started.
The generic FOSS proxy: ONE IAddon record standing in for ANY opaque kernel. This is the Single-Source Lever of the marketplace story. A proprietary addon ships as a compiled binary and nothing else; the record that mounts it is this one, MIT-licensed, generic, and identical for every vendor. That the proxy is open is not a leak: it holds no kernel logic at all, only framing and delegation. All of the IP is in the binary. The kernel SELF-DESCRIBES, so this record learns the addon's tools, hooks, exclusions and schema extensions from the running kernel rather than from the manifest. A manifest that enumerated them would be a second copy of the truth and would rot the first time the vendor shipped a new build. What the manifest still supplies is what must be known BEFORE the kernel runs: the addon id and its advertised capabilities. The mount pipeline reads both while deciding whether this addon is licensed and where it sits in the dependency order, which is strictly before anything has been started.
The OPAQUE wire codec: the one place the request/response bytes are decided, and the only opaque namespace BOTH sides load.
A proprietary addon ships as a compiled kernel in its own process; the host
mounts a generic proxy that speaks to it over one EDN line per request and
one per response. The proxy frames with request/encode and reads with
decode; the kernel reads with decode and frames with ok/error. One
namespace writes both sides, so proxy and kernel cannot drift.
MALLI-FREE ON PURPOSE. The kernel is compiled by cljw build into a native
binary, and malli does not load there; the schemas that describe these shapes
(hive-addon.opaque.schema) and the contracts over them
(hive-addon.opaque.contracts) are a HOST-side boundary layer. Nothing here
validates: validation is the host's job, on values this namespace produced.
Three-host portable stratum, so no reader conditionals, no for, no bare
record fields. See hive-addon.mount.portable-test.
The OPAQUE wire codec: the one place the request/response bytes are decided, and the only opaque namespace BOTH sides load. A proprietary addon ships as a compiled kernel in its own process; the host mounts a generic proxy that speaks to it over one EDN line per request and one per response. The proxy frames with `request`/`encode` and reads with `decode`; the kernel reads with `decode` and frames with `ok`/`error`. One namespace writes both sides, so proxy and kernel cannot drift. MALLI-FREE ON PURPOSE. The kernel is compiled by `cljw build` into a native binary, and malli does not load there; the schemas that describe these shapes (hive-addon.opaque.schema) and the contracts over them (hive-addon.opaque.contracts) are a HOST-side boundary layer. Nothing here validates: validation is the host's job, on values this namespace produced. Three-host portable stratum, so no reader conditionals, no `for`, no bare record fields. See hive-addon.mount.portable-test.
The m/=> contract spine for the opaque subsystem.
Every contract lives HERE rather than beside its function, for one reason
that decides the whole subsystem's shape: the two namespaces a compiled
kernel loads (hive-addon.opaque.codec and hive-addon.opaque.serve) must be
malli-free, because malli does not load under cljw build. A contract
written next to codec/encode would drag the schema runtime into the
binary. Stating them from the outside keeps the kernel lean and the host
fully checked.
HOST-ONLY, and loading it is OPTIONAL. Nothing requires this namespace at runtime; it is loaded by the suite and by a host that wants instrumentation. The contracts are read by hive-schemas to synthesize coverage from the same schemas rather than from hand-written examples.
Schemas are referenced as VARS, not as :opaque/* registry keywords, so a contract resolves without anyone having installed a registry.
The m/=> contract spine for the opaque subsystem. Every contract lives HERE rather than beside its function, for one reason that decides the whole subsystem's shape: the two namespaces a compiled kernel loads (hive-addon.opaque.codec and hive-addon.opaque.serve) must be malli-free, because malli does not load under `cljw build`. A contract written next to `codec/encode` would drag the schema runtime into the binary. Stating them from the outside keeps the kernel lean and the host fully checked. HOST-ONLY, and loading it is OPTIONAL. Nothing requires this namespace at runtime; it is loaded by the suite and by a host that wants instrumentation. The contracts are read by hive-schemas to synthesize coverage from the same schemas rather than from hand-written examples. Schemas are referenced as VARS, not as :opaque/* registry keywords, so a contract resolves without anyone having installed a registry.
No vars found in this namespace.
Malli value objects for the OPAQUE addon wire — the contract between a host proxying an IAddon and the out-of-process kernel that really implements it.
Shapes are uncompiled malli DATA (house idiom: PascalCase defs) seeded into a
LOCAL composite registry that COMPOSES hive-addon.schema's registry — the
AddonId/AddonType/CapabilitySet value objects are reused, never redefined.
The registry is NEVER installed as the malli global default; reach it via
schema/validate/explain/validate* or by passing {:registry registry}.
Opaque shapes are registered under :opaque/* keys (:opaque/wire, :opaque/request, :opaque/response, :opaque/describe, :opaque/spec).
The kernel side never loads this namespace: hive-addon.opaque.codec and hive-addon.opaque.serve are malli-free so a cljw-built binary carries no schema runtime. These schemas are the host-side single source the m/=> contracts (hive-addon.opaque.contracts) and the generated tests read.
Malli value objects for the OPAQUE addon wire — the contract between a host
proxying an IAddon and the out-of-process kernel that really implements it.
Shapes are uncompiled malli DATA (house idiom: PascalCase defs) seeded into a
LOCAL composite registry that COMPOSES hive-addon.schema's registry — the
AddonId/AddonType/CapabilitySet value objects are reused, never redefined.
The registry is NEVER installed as the malli global default; reach it via
`schema`/`validate`/`explain`/`validate*` or by passing {:registry registry}.
Opaque shapes are registered under :opaque/* keys (:opaque/wire,
:opaque/request, :opaque/response, :opaque/describe, :opaque/spec).
The kernel side never loads this namespace: hive-addon.opaque.codec and
hive-addon.opaque.serve are malli-free so a cljw-built binary carries no
schema runtime. These schemas are the host-side single source the m/=>
contracts (hive-addon.opaque.contracts) and the generated tests read.KERNEL side of the opaque wire: turn any IAddon into a line server.
A proprietary addon is written against this library's PORTABLE stratum and
compiled by cljw build into one native binary. That binary's whole job is
(serve! my-addon): read a request line, dispatch it to the addon through
IAddon, write a response line. The host mounts hive-addon.opaque.addon on the
other end, which speaks the same lines. Neither side knows anything about the
other beyond hive-addon.opaque.codec.
REQUIRES ONLY protocol + codec, both malli-free, so the compiled kernel carries no schema runtime and no validation code. The schemas describing these shapes live host-side (hive-addon.opaque.schema).
The kernel SELF-DESCRIBES. describe projects the addon's whole pure
surface, so a mount manifest never enumerates the kernel's tools and cannot
fall out of date with the binary it names.
A JVM+cljw claim, not a three-host one: this namespace dispatches through a protocol defined in ANOTHER namespace, which cljrs cannot yet do. Reader conditionals here must be TOTAL. See hive-addon.mount.portable-test.
KERNEL side of the opaque wire: turn any IAddon into a line server. A proprietary addon is written against this library's PORTABLE stratum and compiled by `cljw build` into one native binary. That binary's whole job is `(serve! my-addon)`: read a request line, dispatch it to the addon through IAddon, write a response line. The host mounts hive-addon.opaque.addon on the other end, which speaks the same lines. Neither side knows anything about the other beyond hive-addon.opaque.codec. REQUIRES ONLY protocol + codec, both malli-free, so the compiled kernel carries no schema runtime and no validation code. The schemas describing these shapes live host-side (hive-addon.opaque.schema). The kernel SELF-DESCRIBES. `describe` projects the addon's whole pure surface, so a mount manifest never enumerates the kernel's tools and cannot fall out of date with the binary it names. A JVM+cljw claim, not a three-host one: this namespace dispatches through a protocol defined in ANOTHER namespace, which cljrs cannot yet do. Reader conditionals here must be TOTAL. See hive-addon.mount.portable-test.
The transport PORT: a line-oriented request/response pipe to a running opaque kernel.
This is the DIP swap point of the opaque subsystem. hive-addon.opaque.addon is one generic proxy written against this abstraction; a subprocess over a native binary, an in-process function, and a future in-JVM wasm host each implement it, and adding one is a new record rather than a change anywhere above (OCP).
A transport is a PROTOCOL rather than a data profile on purpose. Transports differ by BEHAVIOUR (spawn, pipe, stop), and behaviour is what a protocol is for; the thing that differs only by CONSTANTS on this seam is the kernel spec, which is plain data.
Implementors are constructed by FUNCTIONS, never held in a def: an instance built at load time captures the protocol object current at that moment and stops satisfying the protocol the next time this namespace is reloaded.
The transport PORT: a line-oriented request/response pipe to a running opaque kernel. This is the DIP swap point of the opaque subsystem. hive-addon.opaque.addon is one generic proxy written against this abstraction; a subprocess over a native binary, an in-process function, and a future in-JVM wasm host each implement it, and adding one is a new record rather than a change anywhere above (OCP). A transport is a PROTOCOL rather than a data profile on purpose. Transports differ by BEHAVIOUR (spawn, pipe, stop), and behaviour is what a protocol is for; the thing that differs only by CONSTANTS on this seam is the kernel spec, which is plain data. Implementors are constructed by FUNCTIONS, never held in a def: an instance built at load time captures the protocol object current at that moment and stops satisfying the protocol the next time this namespace is reloaded.
In-process ITransport backed by a (fn [request-line] -> response-line).
No subprocess, but a FAITHFUL transport: it moves the same lines the subprocess moves, so a test that drives an addon through it is exercising the real wire rather than a stub of it. Two uses:
In-process ITransport backed by a (fn [request-line] -> response-line).
No subprocess, but a FAITHFUL transport: it moves the same lines the
subprocess moves, so a test that drives an addon through it is exercising the
real wire rather than a stub of it. Two uses:
- drive a kernel that is on this classpath, before it is compiled opaque,
so the same addon can be proven in-process and then shipped as a binary;
- inject a recording line function in a test, which is the only way to
assert the exact BYTES the proxy sends.Effectful ITransport: a long-lived OS process running the opaque kernel binary, spoken to over line-oriented EDN on its stdin and stdout.
This is the transport a marketplace artifact actually mounts through. The process is LONG-LIVED on purpose: a cljw binary costs roughly 36ms to start, which is invisible once per mount and ruinous once per tool call.
JVM-only (.clj, not .cljc). It is the one namespace in the opaque subsystem that names host classes, which is precisely why the seam above it is a protocol: nothing else has to care that this exists.
The kernel's stderr is INHERITED rather than piped. A piped stderr that nobody drains fills its buffer and deadlocks the kernel mid-call, and a proprietary binary's diagnostics belong in the host's log anyway.
Effectful ITransport: a long-lived OS process running the opaque kernel binary, spoken to over line-oriented EDN on its stdin and stdout. This is the transport a marketplace artifact actually mounts through. The process is LONG-LIVED on purpose: a cljw binary costs roughly 36ms to start, which is invisible once per mount and ruinous once per tool call. JVM-only (.clj, not .cljc). It is the one namespace in the opaque subsystem that names host classes, which is precisely why the seam above it is a protocol: nothing else has to care that this exists. The kernel's stderr is INHERITED rather than piped. A piped stderr that nobody drains fills its buffer and deadlocks the kernel mid-call, and a proprietary binary's diagnostics belong in the host's log anyway.
Resolve ordered iaddon.edn layers (lowest precedence first) into the selected plug set: merge → validate → lint → profile → drop-disabled → capability-select.
Resolve ordered iaddon.edn layers (lowest precedence first) into the selected plug set: merge → validate → lint → profile → drop-disabled → capability-select.
Fail-closed lint of a merged iaddon config — an extensible registry of pure rules.
Fail-closed lint of a merged iaddon config — an extensible registry of pure rules.
Fold iaddon config layers into one, with the trust security inversion applied to :iaddon/trust so higher-precedence layers can only tighten, never loosen.
Fold iaddon config layers into one, with the trust security inversion applied to :iaddon/trust so higher-precedence layers can only tighten, never loosen.
Malli schemas for iaddon.edn (:iaddon/* keys), in a local composite registry. Credential values never appear here — only reference chains.
Malli schemas for iaddon.edn (:iaddon/* keys), in a local composite registry. Credential values never appear here — only reference chains.
Plug sources as trust-aware value objects: a tools.deps coord classified into a family with its mutability/locality, behind the ISource protocol.
Plug sources as trust-aware value objects: a tools.deps coord classified into a family with its mutability/locality, behind the ISource protocol.
IAddon protocol — THE single source of truth for addon contracts.
A host loads addons that implement this protocol to gain capabilities (DIP): the host depends on this abstraction, never the concrete addons, and no addon compile-depends on the host. Any project — an MCP server or something unrelated — can host addons by consuming this leaf lib.
All addons implement this protocol. A host's registry operates on instances via this abstraction barrier.
Addon types:
Lifecycle: (initialize! addon config) → Start services, open connections (shutdown! addon) → Release resources, close connections (health addon) → Query current health status
IAddon protocol — THE single source of truth for addon contracts. A host loads addons that implement this protocol to gain capabilities (DIP): the host depends on this abstraction, never the concrete addons, and no addon compile-depends on the host. Any project — an MCP server or something unrelated — can host addons by consuming this leaf lib. All addons implement this protocol. A host's registry operates on instances via this abstraction barrier. Addon types: - :native — Built-in Clojure addons (same JVM) - :mcp-bridge — External MCP servers proxied via stdio/sse/http - :external — Non-MCP integrations (REST APIs, CLIs, etc.) Lifecycle: (initialize! addon config) → Start services, open connections (shutdown! addon) → Release resources, close connections (health addon) → Query current health status
Command contributions to a composite tool.
An addon contributes named subcommands to a host tool, so
analysis command="lint ..." reaches the contributing addon's handler.
Each contribution records the contributing addon so shutdown can retract
everything that addon added.
Shape: {tool-name {command-name {:handler fn :params {} :description str :addon addon-id}}}
Command contributions to a composite tool.
An addon contributes named subcommands to a host tool, so
`analysis command="lint ..."` reaches the contributing addon's handler.
Each contribution records the contributing addon so shutdown can retract
everything that addon added.
Shape: {tool-name {command-name {:handler fn :params {} :description str
:addon addon-id}}}Opaque capability registry.
An addon registers an implementation under a keyword key at startup; a consumer looks it up without knowing which addon provides it:
(register! :gs/struct-cmp my-cmp-fn)
(if-let [f (get-extension :gs/struct-cmp)] (f a b) fallback)
Registration is atomic and idempotent — re-registering a key replaces it.
Opaque capability registry.
An addon registers an implementation under a keyword key at startup; a
consumer looks it up without knowing which addon provides it:
(register! :gs/struct-cmp my-cmp-fn)
(if-let [f (get-extension :gs/struct-cmp)]
(f a b)
fallback)
Registration is atomic and idempotent — re-registering a key replaces it.Schema-property extensions contributed to a host tool.
An addon widens an existing tool's input schema by registering extra properties under that tool's name. Contributions merge, so several addons may extend the same tool.
Schema-property extensions contributed to a host tool. An addon widens an existing tool's input schema by registering extra properties under that tool's name. Contributions merge, so several addons may extend the same tool.
Dynamically registered tool definitions.
A tool definition is a map carrying at least a :name string and a :handler fn. Registration is keyed by :name, last write wins.
Dynamically registered tool definitions. A tool definition is a map carrying at least a :name string and a :handler fn. Registration is keyed by :name, last write wins.
Malli schema layer for the IAddon contract.
Every data shape that crosses the IAddon protocol boundary has a schema here, so a host can validate an addon's contract outputs and an addon can validate the config it receives. Stratified: the schemas sit as pure data BELOW the protocol — they read the protocol's own constants (single source, no drift) and the protocol never depends on the schemas.
Self-contained by design (DDD): the addon bounded context owns its schemas.
Deps are malli + hive-dsl only — NO hive-spi. Schemas are registered in a
LOCAL composite registry under :addon/* keys; reach them via schema,
validate, explain (which thread {:registry registry}) or by passing
{:registry registry} yourself. The registry is NEVER installed as the malli
global default (shared-JVM safety).
Errors bridge to hive-dsl Result: the validate*/validate-addon helpers
return (r/ok x) on success and (r/err :addon/... {:explanation ...}) on
failure, with a qualified-keyword error category.
Malli schema layer for the IAddon contract.
Every data shape that crosses the IAddon protocol boundary has a schema
here, so a host can validate an addon's contract outputs and an addon can
validate the config it receives. Stratified: the schemas sit as pure data
BELOW the protocol — they read the protocol's own constants (single source,
no drift) and the protocol never depends on the schemas.
Self-contained by design (DDD): the addon bounded context owns its schemas.
Deps are malli + hive-dsl only — NO hive-spi. Schemas are registered in a
LOCAL composite registry under :addon/* keys; reach them via `schema`,
`validate`, `explain` (which thread {:registry registry}) or by passing
{:registry registry} yourself. The registry is NEVER installed as the malli
global default (shared-JVM safety).
Errors bridge to hive-dsl Result: the `validate*`/`validate-addon` helpers
return (r/ok x) on success and (r/err :addon/... {:explanation ...}) on
failure, with a qualified-keyword error category.ITerminalAddon: the contract for addon-contributed terminal backends.
A companion protocol to IAddon. A concrete terminal backend (a vessel) implements BOTH on the same reify: IAddon carries lifecycle, this carries the terminal operations.
It lives here, beside IAddon, for the same reason IAddon does. A vessel is an addon like any other, so it must be able to compile against the contract alone. Defining it in a host would force every vessel to compile-depend on that host, which is precisely what this leaf lib exists to prevent.
Method signatures mirror a host's ling-strategy protocol exactly, same arities and argument semantics, so a host can dispatch to an addon backend through a thin adapter.
Lifecycle: the backend is started during IAddon/initialize! and torn down during IAddon/shutdown!.
ITerminalAddon: the contract for addon-contributed terminal backends. A companion protocol to IAddon. A concrete terminal backend (a vessel) implements BOTH on the same reify: IAddon carries lifecycle, this carries the terminal operations. It lives here, beside IAddon, for the same reason IAddon does. A vessel is an addon like any other, so it must be able to compile against the contract alone. Defining it in a host would force every vessel to compile-depend on that host, which is precisely what this leaf lib exists to prevent. Method signatures mirror a host's ling-strategy protocol exactly, same arities and argument semantics, so a host can dispatch to an addon backend through a thin adapter. Lifecycle: the backend is started during IAddon/initialize! and torn down during IAddon/shutdown!.
IVessel: the contract for a host environment that provides headed capabilities.
A vessel abstracts the headed environment (Emacs, tmux, VS Code, a web UI) behind a protocol, the way a window manager abstracts a screen. It supplies terminals, editors, delivery channels, REPLs, and it owns the mapping from an agent to the context that agent runs in.
It lives here, beside IAddon, for the reason IAddon does: a vessel ships as an addon, so it must compile against the contract alone. Defining it in a host would force every vessel to compile-depend on that host, which is what this leaf lib exists to prevent.
Vessels are a registry, not a singleton: several can be active at once (Emacs and tmux, Emacs and a web UI). The registry itself belongs to the host; only the contract is here.
IVessel and IAddon are implemented on SEPARATE objects. Both declare
capabilities, initialize! and shutdown!, so one reify cannot carry
both.
Reload-safety: defprotocol is not idempotent, so the declaration is
guarded: re-evaluating this namespace will not orphan existing
implementations.
IVessel: the contract for a host environment that provides headed capabilities. A vessel abstracts the headed environment (Emacs, tmux, VS Code, a web UI) behind a protocol, the way a window manager abstracts a screen. It supplies terminals, editors, delivery channels, REPLs, and it owns the mapping from an agent to the context that agent runs in. It lives here, beside IAddon, for the reason IAddon does: a vessel ships as an addon, so it must compile against the contract alone. Defining it in a host would force every vessel to compile-depend on that host, which is what this leaf lib exists to prevent. Vessels are a registry, not a singleton: several can be active at once (Emacs and tmux, Emacs and a web UI). The registry itself belongs to the host; only the contract is here. IVessel and IAddon are implemented on SEPARATE objects. Both declare `capabilities`, `initialize!` and `shutdown!`, so one reify cannot carry both. Reload-safety: `defprotocol` is not idempotent, so the declaration is guarded: re-evaluating this namespace will not orphan existing implementations.
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 |