// 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 // — plugin emits these as its "output" // // Host → Plugin: // plugin.interrogate — host asks for the plugin's manifest // — 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..status — "pending" | "active" | "disconnected" // plugin..contributions — comma-separated contribution types // plugin..required — "true" | "false" // plugin..dependencies — comma-separated plugin names // plugin..subscriptions — comma-separated event names // plugin..emits — comma-separated event names // plugin..version — semver string // plugin..description — human-readable description // plugin..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) -> 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 = { "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 = { "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) -> 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) -> 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) -> 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) -> Result, NeuronError> { let events: [Map] = native_drain_events() let processed: Int = 0 for event in events { let event_name: String = event["event"] let payload: Map = 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", {}) }