Files
will.anderson b77f537dc6 fix cross-repo path deps; remove el compiler from neuron-lang/
- 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/)
2026-04-29 03:27:39 -05:00

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", {})
}