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.
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.
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.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.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 |