b77f537dc6
- neuron-runtime, neuron-store, neuron-migrate: fix path deps (products/engram/ → foundation/engram/engrams/, products/el/ → foundation/el/engrams/) - neuron-rs workspace: fix axon-events/axon-protocol paths (../../../platform/ → ../../platform/, paths were off by one level) - neuron-lang/compiler/ removed (el self-hosting compiler now lives in foundation/el/el-compiler/)
403 lines
15 KiB
EmacsLisp
403 lines
15 KiB
EmacsLisp
// plugins/host.el — Plugin host for the Neuron daemon.
|
|
//
|
|
// Plugins communicate with the host exclusively through the event bus.
|
|
// A plugin's identity IS its contributions — no separate type field.
|
|
// The marketplace queries contributions to surface clean category lanes.
|
|
//
|
|
// Plugin interaction model (both directions are events):
|
|
//
|
|
// Plugin → Host:
|
|
// plugin.announce — plugin is alive, wants to register
|
|
// plugin.manifest — response to plugin.interrogate, declares capabilities
|
|
// <any event> — plugin emits these as its "output"
|
|
//
|
|
// Host → Plugin:
|
|
// plugin.interrogate — host asks for the plugin's manifest
|
|
// <subscribed events> — host forwards events the plugin declared interest in
|
|
//
|
|
// Manifest format (plugin.manifest payload):
|
|
// {
|
|
// "plugin": "voice",
|
|
// "version": "1.0.0",
|
|
// "description": "Voice input and output for Neuron",
|
|
// "contributions": ["connector", "notification_channel"],
|
|
// "required": false,
|
|
// "dependencies": ["audio-hardware"],
|
|
// "subscriptions": ["agent.turn_complete"],
|
|
// "emits": ["voice.speaking", "voice.idle"],
|
|
// }
|
|
//
|
|
// Contribution types (contributions IS the type — one list, no separate field):
|
|
// connector — bridges to an external system
|
|
// interceptor — operates on the event pipeline
|
|
// imprint — shapes the cognitive layer directly
|
|
// knowledge — adds domain knowledge to the graph
|
|
// process — adds CCR workflow definitions
|
|
// tool — adds agent-callable tools (registered in daemon)
|
|
// command — adds CLI commands
|
|
// behavior — adds behavioral patterns
|
|
// safety_rule — adds safety constraints
|
|
// hardware — adds hardware device support
|
|
// sync_handler — adds sync protocol support
|
|
// notification_channel — adds a notification delivery channel
|
|
// ui — adds UI panels or widgets
|
|
|
|
from types import { NeuronError }
|
|
|
|
// ── Plugin registry entry ─────────────────────────────────────────────────────
|
|
//
|
|
// State keys:
|
|
// plugin.<name>.status — "pending" | "active" | "disconnected"
|
|
// plugin.<name>.contributions — comma-separated contribution types
|
|
// plugin.<name>.required — "true" | "false"
|
|
// plugin.<name>.dependencies — comma-separated plugin names
|
|
// plugin.<name>.subscriptions — comma-separated event names
|
|
// plugin.<name>.emits — comma-separated event names
|
|
// plugin.<name>.version — semver string
|
|
// plugin.<name>.description — human-readable description
|
|
// plugin.<name>.endpoint — delivery address (URL or IPC path)
|
|
// plugins.registered — comma-separated list of known plugin names
|
|
|
|
// ── Registry helpers ──────────────────────────────────────────────────────────
|
|
|
|
// plugin_names — list all registered plugin names.
|
|
fn plugin_names() -> [String] {
|
|
let raw: String = state_get("plugins.registered")
|
|
if str_eq(raw, "") {
|
|
return []
|
|
}
|
|
str_split(raw, ",")
|
|
}
|
|
|
|
// plugin_register — add a plugin name to the registry.
|
|
fn plugin_register(name: String) -> Void {
|
|
let existing: String = state_get("plugins.registered")
|
|
if str_eq(existing, "") {
|
|
state_set("plugins.registered", name)
|
|
} else {
|
|
let names: [String] = str_split(existing, ",")
|
|
let found: Bool = list_contains(names, name)
|
|
if !found {
|
|
state_set("plugins.registered", existing + "," + name)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn plugin_set_status(name: String, status: String) -> Void {
|
|
state_set("plugin." + name + ".status", status)
|
|
}
|
|
|
|
fn plugin_get_status(name: String) -> String {
|
|
state_get("plugin." + name + ".status")
|
|
}
|
|
|
|
fn plugin_set_endpoint(name: String, endpoint: String) -> Void {
|
|
state_set("plugin." + name + ".endpoint", endpoint)
|
|
}
|
|
|
|
fn plugin_get_endpoint(name: String) -> String {
|
|
state_get("plugin." + name + ".endpoint")
|
|
}
|
|
|
|
fn plugin_set_subscriptions(name: String, events: [String]) -> Void {
|
|
state_set("plugin." + name + ".subscriptions", str_join(events, ","))
|
|
}
|
|
|
|
fn plugin_get_subscriptions(name: String) -> [String] {
|
|
let raw: String = state_get("plugin." + name + ".subscriptions")
|
|
if str_eq(raw, "") {
|
|
return []
|
|
}
|
|
str_split(raw, ",")
|
|
}
|
|
|
|
fn plugin_set_emits(name: String, events: [String]) -> Void {
|
|
state_set("plugin." + name + ".emits", str_join(events, ","))
|
|
}
|
|
|
|
// plugin_set_contributions — store the contribution types for a plugin.
|
|
fn plugin_set_contributions(name: String, contributions: [String]) -> Void {
|
|
state_set("plugin." + name + ".contributions", str_join(contributions, ","))
|
|
}
|
|
|
|
// plugin_get_contributions — list of contribution types this plugin declares.
|
|
fn plugin_get_contributions(name: String) -> [String] {
|
|
let raw: String = state_get("plugin." + name + ".contributions")
|
|
if str_eq(raw, "") {
|
|
return []
|
|
}
|
|
str_split(raw, ",")
|
|
}
|
|
|
|
// plugin_set_required — whether this plugin must be loaded at startup.
|
|
fn plugin_set_required(name: String, required: Bool) -> Void {
|
|
if required {
|
|
state_set("plugin." + name + ".required", "true")
|
|
} else {
|
|
state_set("plugin." + name + ".required", "false")
|
|
}
|
|
}
|
|
|
|
// plugin_is_required — true if this plugin declared itself required.
|
|
fn plugin_is_required(name: String) -> Bool {
|
|
let raw: String = state_get("plugin." + name + ".required")
|
|
str_eq(raw, "true")
|
|
}
|
|
|
|
// plugin_set_dependencies — record declared plugin dependencies.
|
|
fn plugin_set_dependencies(name: String, deps: [String]) -> Void {
|
|
state_set("plugin." + name + ".dependencies", str_join(deps, ","))
|
|
}
|
|
|
|
// plugin_get_dependencies — list of plugin names this plugin depends on.
|
|
fn plugin_get_dependencies(name: String) -> [String] {
|
|
let raw: String = state_get("plugin." + name + ".dependencies")
|
|
if str_eq(raw, "") {
|
|
return []
|
|
}
|
|
str_split(raw, ",")
|
|
}
|
|
|
|
// ── Contribution queries ───────────────────────────────────────────────────────
|
|
|
|
// plugins_by_contribution — return active plugin names that declare a given
|
|
// contribution type. Used by the marketplace to surface clean category lanes.
|
|
fn plugins_by_contribution(contribution: String) -> [String] {
|
|
let names: [String] = plugin_names()
|
|
let result: [String] = []
|
|
for name in names {
|
|
let status: String = plugin_get_status(name)
|
|
if str_eq(status, "active") {
|
|
let contribs: [String] = plugin_get_contributions(name)
|
|
if list_contains(contribs, contribution) {
|
|
list_append(result, name)
|
|
}
|
|
}
|
|
}
|
|
result
|
|
}
|
|
|
|
// plugin_has_contribution — true if a specific plugin declares a contribution.
|
|
fn plugin_has_contribution(name: String, contribution: String) -> Bool {
|
|
let contribs: [String] = plugin_get_contributions(name)
|
|
list_contains(contribs, contribution)
|
|
}
|
|
|
|
// required_plugins — return all plugins that declared required: true.
|
|
// Called at startup to verify all required plugins have announced.
|
|
fn required_plugins() -> [String] {
|
|
let names: [String] = plugin_names()
|
|
let result: [String] = []
|
|
for name in names {
|
|
if plugin_is_required(name) {
|
|
list_append(result, name)
|
|
}
|
|
}
|
|
result
|
|
}
|
|
|
|
// ── Routing helpers ───────────────────────────────────────────────────────────
|
|
|
|
// plugins_subscribed_to — return names of active plugins subscribed to an event.
|
|
fn plugins_subscribed_to(event_name: String) -> [String] {
|
|
let names: [String] = plugin_names()
|
|
let result: [String] = []
|
|
for name in names {
|
|
let subs: [String] = plugin_get_subscriptions(name)
|
|
let active: String = plugin_get_status(name)
|
|
if str_eq(active, "active") {
|
|
if list_contains(subs, event_name) {
|
|
list_append(result, name)
|
|
}
|
|
}
|
|
}
|
|
result
|
|
}
|
|
|
|
// deliver_event — forward an event to a plugin's endpoint.
|
|
fn deliver_event(plugin_name: String, event_name: String, payload: Map<String, Any>) -> Void {
|
|
let endpoint: String = plugin_get_endpoint(plugin_name)
|
|
if str_eq(endpoint, "") {
|
|
println("[plugin-host] " + plugin_name + ": no endpoint, skipping delivery of " + event_name)
|
|
return
|
|
}
|
|
let envelope: Map<String, Any> = {
|
|
"event": event_name,
|
|
"payload": payload,
|
|
}
|
|
let resp = native_http_post({
|
|
"url": endpoint + "/event",
|
|
"headers": {"Content-Type": "application/json"},
|
|
"body": envelope,
|
|
})
|
|
if !resp.ok {
|
|
println("[plugin-host] delivery failed: " + plugin_name + " event=" + event_name)
|
|
}
|
|
}
|
|
|
|
// ── Interrogation ─────────────────────────────────────────────────────────────
|
|
|
|
fn host_interrogate(plugin_name: String, endpoint: String) -> Void {
|
|
println("[plugin-host] interrogating plugin: " + plugin_name)
|
|
native_emit("plugin.interrogate", {"plugin": plugin_name, "endpoint": endpoint})
|
|
let req_body: Map<String, Any> = {
|
|
"event": "plugin.interrogate",
|
|
"payload": {"plugin": plugin_name},
|
|
}
|
|
let resp = native_http_post({
|
|
"url": endpoint + "/event",
|
|
"headers": {"Content-Type": "application/json"},
|
|
"body": req_body,
|
|
})
|
|
if !resp.ok {
|
|
println("[plugin-host] interrogate delivery failed: " + plugin_name)
|
|
}
|
|
}
|
|
|
|
// ── Event processing ──────────────────────────────────────────────────────────
|
|
|
|
fn host_handle_announce(payload: Map<String, Any>) -> Void {
|
|
let name: String = payload["plugin"]
|
|
let endpoint: String = payload["endpoint"]
|
|
|
|
if str_eq(name, "") {
|
|
println("[plugin-host] announce missing plugin name, ignoring")
|
|
return
|
|
}
|
|
|
|
println("[plugin-host] plugin announced: " + name + " at " + endpoint)
|
|
|
|
plugin_register(name)
|
|
plugin_set_status(name, "pending")
|
|
plugin_set_endpoint(name, endpoint)
|
|
|
|
native_emit("plugin.status", {
|
|
"plugin": name,
|
|
"status": "pending",
|
|
"reason": "announced",
|
|
})
|
|
|
|
host_interrogate(name, endpoint)
|
|
}
|
|
|
|
// host_handle_manifest — process a plugin.manifest event.
|
|
//
|
|
// Stores all declared capabilities. The contributions list IS the plugin's
|
|
// type — no separate type field. required and dependencies drive startup
|
|
// loading order.
|
|
fn host_handle_manifest(payload: Map<String, Any>) -> Void {
|
|
let name: String = payload["plugin"]
|
|
let contributions: [String] = payload["contributions"]
|
|
let subscriptions: [String] = payload["subscriptions"]
|
|
let emits: [String] = payload["emits"]
|
|
let required: Bool = payload["required"]
|
|
let dependencies: [String] = payload["dependencies"]
|
|
|
|
if str_eq(name, "") {
|
|
println("[plugin-host] manifest missing plugin name, ignoring")
|
|
return
|
|
}
|
|
|
|
let current_status: String = plugin_get_status(name)
|
|
if str_eq(current_status, "") {
|
|
plugin_register(name)
|
|
}
|
|
|
|
plugin_set_contributions(name, contributions)
|
|
plugin_set_subscriptions(name, subscriptions)
|
|
plugin_set_emits(name, emits)
|
|
plugin_set_required(name, required)
|
|
plugin_set_dependencies(name, dependencies)
|
|
plugin_set_status(name, "active")
|
|
|
|
println("[plugin-host] plugin active: " + name
|
|
+ " contributions=" + str_join(contributions, ",")
|
|
+ " subs=" + str_join(subscriptions, ","))
|
|
|
|
native_emit("plugin.status", {
|
|
"plugin": name,
|
|
"status": "active",
|
|
"contributions": str_join(contributions, ","),
|
|
"subscriptions": str_join(subscriptions, ","),
|
|
"emits": str_join(emits, ","),
|
|
"required": required,
|
|
})
|
|
}
|
|
|
|
// ── Event routing ─────────────────────────────────────────────────────────────
|
|
|
|
fn host_route_event(event_name: String, payload: Map<String, Any>) -> Void {
|
|
let recipients: [String] = plugins_subscribed_to(event_name)
|
|
for plugin_name in recipients {
|
|
deliver_event(plugin_name, event_name, payload)
|
|
}
|
|
}
|
|
|
|
// ── Host tick ─────────────────────────────────────────────────────────────────
|
|
|
|
@manager
|
|
fn host_tick(params: Map<String, Any>) -> Result<Map<String, Any>, NeuronError> {
|
|
let events: [Map<String, Any>] = native_drain_events()
|
|
let processed: Int = 0
|
|
|
|
for event in events {
|
|
let event_name: String = event["event"]
|
|
let payload: Map<String, Any> = event["payload"]
|
|
|
|
if str_eq(event_name, "plugin.announce") {
|
|
host_handle_announce(payload)
|
|
} else {
|
|
if str_eq(event_name, "plugin.manifest") {
|
|
host_handle_manifest(payload)
|
|
} else {
|
|
host_route_event(event_name, payload)
|
|
}
|
|
}
|
|
let processed = processed + 1
|
|
}
|
|
|
|
Ok({"status": "ok", "processed": int_to_str(processed)})
|
|
}
|
|
|
|
// ── Status introspection ──────────────────────────────────────────────────────
|
|
|
|
// host_status — JSON snapshot of the plugin registry.
|
|
fn host_status() -> String {
|
|
let names: [String] = plugin_names()
|
|
let parts: String = "{"
|
|
let first: Bool = true
|
|
|
|
for name in names {
|
|
let status: String = plugin_get_status(name)
|
|
let endpoint: String = plugin_get_endpoint(name)
|
|
let contribs: String = state_get("plugin." + name + ".contributions")
|
|
let subs: String = state_get("plugin." + name + ".subscriptions")
|
|
let emits_raw: String = state_get("plugin." + name + ".emits")
|
|
let required: String = state_get("plugin." + name + ".required")
|
|
|
|
let entry: String = "\"" + name + "\":{\"status\":\"" + status
|
|
+ "\",\"contributions\":\"" + contribs
|
|
+ "\",\"endpoint\":\"" + endpoint
|
|
+ "\",\"subscriptions\":\"" + subs
|
|
+ "\",\"emits\":\"" + emits_raw
|
|
+ "\",\"required\":\"" + required + "\"}"
|
|
|
|
if first {
|
|
let parts = parts + entry
|
|
let first = false
|
|
} else {
|
|
let parts = parts + "," + entry
|
|
}
|
|
}
|
|
|
|
parts + "}"
|
|
}
|
|
|
|
// ── Startup ───────────────────────────────────────────────────────────────────
|
|
|
|
fn host_on_startup() -> Void {
|
|
state_set("plugins.registered", "")
|
|
println("[plugin-host] ready — waiting for plugin.announce events")
|
|
native_emit("plugin.host_ready", {})
|
|
}
|