khive-runtime 0.11.0

Composable Service API: entity/note CRUD, graph traversal, hybrid search, curation.
Documentation
# Pack Runtime Integration Points

`pack.rs` defines the `PackRuntime` trait — the hooks a pack implements to integrate with a
`KhiveRuntime` (embedders, entity-type validators, note-mutation notifications) — and the
`VerbRegistry` dispatch surface that routes a verb call through gate, audit, and namespace
resolution before reaching a pack handler. Each section below is the extended technical contract
for one hook or dispatch-path function; the in-source doc-comment on each item carries only the
concise standalone summary plus a pointer here.

## KindHook::validate_proposal_entity

Approved `AddEntity` changesets resolve the entity owner's hook from the applying
worker's actual `VerbRegistry`, then call this validation-only method before
preparing domain SQL. Its immutable `EntityDraft` has a canonical kind. The method
is synchronous, receives no runtime handle, and defaults to accepting the draft.
Implementations must not write storage or change the approved content.

Workspace overrides it with the same pure integer `properties.schema_version`
predicate used by shared create and entity update. Neither `prepare_create` nor
`after_create` runs on this proposal path. Other kinds retain their default
admission. A validation error follows the existing pre-commit failed-apply audit
and projection handling; an accepted review is not itself evidence of a
successful domain apply. Multi-step Compound proposals remain refused.

Approved `AddNote` proposals are validated by the sibling hook below, not by this one:
`validate_proposal_entity` "does not apply to AddNote" (per its own doc comment) because notes
have their own analogous seam. ADR-021's prior memory exception on the `AddNote` route is
withdrawn as of ADR-017's 2026-09-22 amendment; see `validate_proposal_note` immediately below.

## KindHook::validate_proposal_note

Analogous to `validate_proposal_entity` but for the `AddNote` route (ADR-017's 2026-09-22
amendment). The kg pack's proposal route resolves the note kind's owning hook through the
registry and calls this validation-only method at two points against the same immutable
changeset: once when `propose` accepts a new `AddNote` changeset, before any proposal row
exists, and again when an approved proposal is applied, using the canonical kind resolved by
`canonical_note_kind`. The method is synchronous,
receives no runtime handle, and defaults to accepting the draft. Implementations must not write
storage or normalize the draft.

The memory pack registers this hook for `kind="memory"` (ADR-021's 2026-09-22 amendment),
refusing unconditionally with the canonical invalid-input shape (`details.reason =
"kind_admission_refused"`, `details.kind = "memory"`, `details.route = "proposal_add_note"`,
message naming `memory.remember`), the same refusal `prepare_create` already gives shared
`create`, `stream.batch`, and standalone `stream.append`. Running the check at both `propose`
and apply means a proposal approved before this hook existed still fails at apply, visibly,
through the existing failed-apply audit and projection handling, writing no note, edge, FTS
row, or provenance record and leaving the draft and its review history unchanged. Other note
kinds keep their default admission, and this seam never narrows what shared `create` already
accepts, only what the proposal route accepts.

## brain_consumer_kinds

Packs that request brain profile resolution declare their exact wire-level consumer values in
`Pack::BRAIN_CONSUMER_KINDS` and return the same slice from
`PackRuntime::brain_consumer_kinds`. `VerbRegistry::all_brain_consumer_kinds` composes the loaded
declarations, deduplicating shared uses such as `recall` while preserving first-seen order.

`brain.bind` validates specific values against that aggregate. The `"*"` wildcard is always legal
at the binding boundary but is registry-owned and must not appear in a pack declaration. An enum
variant or ADR mention alone does not make a kind bindable: a loaded pack must declare that it
actually consumes the kind.

## register_embedders

`PackRuntime::register_embedders` is called by the transport during pack initialisation, before
the first verb dispatch, so that `KhiveRuntime::embedder(name)` resolves provider names declared
here. Implement it to contribute non-lattice embedding backends:

```ignore
fn register_embedders(&self, runtime: &KhiveRuntime) {
    runtime.register_embedder(MyCustomProvider::new());
}
```

The default no-op preserves backwards compatibility — packs that only use built-in lattice models
do not need to override this method.

## register_entity_type_validator

`PackRuntime::register_entity_type_validator` is called by the transport during pack
initialisation, after the registry is built and before the first verb dispatch, so that
`create_many` and `create_entity` reject unregistered `entity_type` values at the runtime layer
in addition to the handler layer. Packs that own `EntityTypeRegistry` vocabularies (e.g.
`KgPack`) should override this to install their registry's `resolve` function. The default no-op
leaves the runtime validator absent (skip-when-None), which is the correct behaviour for bare
runtimes without packs.

This single-argument hook is intentionally left unchanged (not widened) so an out-of-tree pack
that already overrides it keeps compiling even if it declares no entity types. A pack that needs
the boot-time composed pack vocabulary should override `register_entity_type_validator_with_types`
instead — `call_register_entity_type_validators` calls that hook, not this one.

`register_entity_type_validator_with_types` receives the boot-time composed set of every loaded
pack's `ENTITY_TYPES` (`VerbRegistry::all_entity_types`) — the same aggregate every pack in the
loaded set receives, mirroring how `EDGE_RULES` are aggregated once and consulted by every pack.
It defaults to calling `register_entity_type_validator` with just the runtime, so a pack that
overrides only the older, simpler hook — or overrides neither — keeps compiling and behaving
exactly as before. `call_register_entity_type_validators` calls this hook, not the older one, so
a pack that wants the composed vocabulary must override this one. Packs that own
`EntityTypeRegistry` vocabularies should override this hook to compose
`EntityTypeRegistry::with_extra(pack_entity_types)` and install its `resolve` function.

## register_note_mutation_hook

Called by the transport during pack initialisation, after the registry is built and before the
first verb dispatch — same timing as `register_entity_type_validator`. Packs that cache derived
state keyed by note content (e.g. `khive-pack-memory`'s warm ANN index) should override this to
install a hook via `KhiveRuntime::install_note_mutation_hook`, so `update_note`/`delete_note`
notify them even when the mutation arrived through a different pack's verb that has no
dependency on the reacting pack (e.g. KG's `update`/`delete` on a `kind="memory"` note). The
default no-op leaves the runtime hook absent (skip-when-None), which is the correct behaviour for
packs that don't cache note-derived state and for bare runtimes without packs.

## register_note_search_ann_provider

Called at the same transport boot sites as `register_note_mutation_hook`. The runtime
owns `NoteSearchAnnProvider` and one optional installation slot; the memory pack
installs its graph-backed provider only when both the logical backend identity
and the actual opened backend instance match the runtime receiving the hook.
Two independently opened databases can both be named `main`; that alone does
not authorize sharing an ANN graph. A routed runtime on another backend leaves the
slot empty and uses the exact note-vector search. The provider is responsible
for its own durable `note_search` consumer registration, same-snapshot fresh-tail
merge, and namespace/liveness post-filtering before it returns candidates.
Its read-side runtime clone drops callback slots, so the installed provider
does not retain its own installation through an Arc cycle.

## registered_embedding_model_names

Used by ADR-103 Amendment 1's `model_count` computation at the dispatch audit-row emission seam
(`VerbRegistry::dispatch_with_identity`) for the two embedding-bearing verb families whose model
fan-out is not a per-dispatch constant: singleton `create` and `memory.remember` without an
explicit `embedding_model` override. Defaults to empty — only the packs that own those verbs (kg,
memory) need to override this by forwarding to their internal `KhiveRuntime`.

## dispatch_as

For embedding hosts (gateways, servers, or other processes that embed this runtime as a library)
that authenticate a principal through their own channel — not through the request DSL — and then
need that principal to be the effective actor for one dispatch. `verified_actor` is a typed
Rust-side argument: it can only be supplied by code holding a `VerbRegistry` handle. `dispatch_as`
never reads `params["actor"]` to derive the effective actor; individual verbs may still accept an
`actor` field for their own documented business semantics, unrelated to the acting principal.

`verified_actor` is a `VerifiedActor`, whose constructor rejects blank identifiers. This keeps an
authentication-integration failure (an empty subject from the host's own auth channel) from
silently downgrading to the anonymous/local actor — the failure surfaces at `VerifiedActor::new`
instead of being laundered into a valid dispatch.

Every pack handler that reads "who is calling" (for example, a proposal review's `reviewer`
field) resolves it from the `NamespaceToken` the dispatch boundary mints, so `verified_actor`
becomes exactly the principal those handlers observe.

## dispatch_with_identity

`identity = None` behaves exactly like `VerbRegistry::dispatch`. `identity = Some(id)` uses
`id.namespace` / `id.actor_id` / `id.visible_namespaces` in place of `self.default_namespace` /
`self.actor_id` / `self.visible_namespaces` for this call's namespace resolution, gate request,
and token minting — the registry's own fields are never mutated, so concurrent calls with
different (or no) identity are independent. This is what lets one warm registry correctly serve
requests from many attribution identities over the same shared backend (same db, same warm ANN
indexes) instead of rejecting or silently dispatching under its own baked identity (ADR-096 Fork
1).

`RequestIdentity.process_ref` is a non-authoritative request-context rider. The dispatcher copies
it to `NamespaceToken::process_ref()` for pack handlers but never consults it for gate decisions,
namespace visibility, or actor resolution. An explicit `None` on a daemon-origin identity remains
absent; it never falls back to the warm daemon's own environment. Identity-less local dispatch
resolves `KHIVE_PROCESS_REF` in the current process at dispatch time.

## build_audit_storage_event

Shared by the immediate-append path (all verbs, denied calls, bulk `links`) and the deferred
singleton-`link` fallback so both audit shapes are produced by one code path.

`resource` is the ADR-103 `resource` payload object. ADR-103 Decision (a) stamps the closed
`work_class` enum on every event, so every call site passes `Some`:
`crate::cost_unit::resource_payload` (`{"work_class": ..., "cost_unit": ...}`) for a
successfully-resolved dispatch, or `crate::cost_unit::base_resource_payload` (`{"work_class":
...}`, no `cost_unit` key) for denied calls, errored dispatches, and the no-pack-owns-this-verb
case. `None` is reserved for a caller with no `work_class` to stamp at all (none exist today); it
must never be used to omit `cost_unit` alone.

## persist_git_digest_receipt

Successful `git.digest` is the one strict exception to best-effort audit
persistence. Both `dispatch_with_identity` and
`dispatch_intercepted_with_identity` allocate one schema-v2 audit event,
insert its UUID into the handler result as `receipt_id`, attach the complete
result at `payload.result`, set `target_id=project_id`, and append before
returning. The normal flattened `AuditEvent` fields and ADR-103 `resource`
object remain present, including `resource.request_id` when the transport
supplied one. That id correlates the containing request as a group: every
operation in one batch or chain shares it. `receipt_id` remains the unique
key for one successful digest result. The handler policy is `AlwaysVerbose`,
so the default MCP presentation returns the exact stored `payload.result`
without shortening UUIDs or dropping empty fields.

No configured event store, malformed `project_id`, or append failure replaces the handler success with
`RuntimeError::Internal("git_digest_receipt_persist_failed: ...")`. The
message tells the caller that writes may already have committed and reveals
no storage/source/command detail. When receipt construction rejects malformed
handler output while a store and gate audit are available, the borrowed audit
is still appended once through the generic schema-v1 Error path; it is not
silently consumed. A receipt append failure is not retried as a generic append
against the same failing store. Handler errors and every other verb retain the
ordinary best-effort audit path.

A gate infrastructure error takes precedence over the receipt path: it writes the ordinary
best-effort `gate_unavailable` Error audit and returns `RuntimeError::GateUnavailable` before the
handler or intercepted operation can run.

## LinkAuditSuccessV2

Schema v2 audit payload for a successful singleton `link` call. Additive over the v1 `AuditEvent`
shape: every v1 field is preserved via `#[serde(flatten)]`, and the edge identity/relation/weight
the caller created or resolved are added at the top level.

Audit schema v2 is a verb-discriminated additive family: the flattened `verb`
selects its enrichment. Singleton `link` carries the edge fields above;
successful `git.digest` carries `result` (the complete receipted report) plus
the ordinary `resource` object. Consumers must retain the common v1 envelope
and branch on `verb` before interpreting enrichment fields.

## link_audit_success_from_result

Extracts the edge fields needed to enrich a successful singleton `link` audit row from the
handler's returned JSON. Returns `None` (rather than a `Result`) on any missing/malformed field —
the caller treats that as "cannot enrich" and falls back to the v1 audit shape instead of failing
the already-succeeded `link` call.

## resolve_explicit_namespace

This is the single chokepoint both `VerbRegistry::dispatch` (single-backend and JSON-form
ingress) and the multi-backend coordinator intercept (`dispatch_via_coordinator_inner` in
`khive-mcp`) call into, so no ingress path can bypass the fail-closed rule by routing around
`dispatch`.