Extensions

Extensions supplement and compliment Kinode processes. Kinode processes have many features that make them good computational units, but they also have constraints. Extensions remove the constraints (e.g., not all libraries can be built to Wasm) while maintaining the advantages (e.g., the integration with the Kinode Request/Response system). The cost of extensions is that they are not as nicely bundled within the Kinode system: they must be run separately.

What is an Extension?

Extensions are WebSocket clients that connect to a paired Kinode process to extend library, language, or hardware support.

Kinode processes are Wasm components, which leads to advantages and disadvantages. The rest of the book (and in particular the processes chapter) discusses the advantages (e.g., integration with the Kinode Request/Response system and the capabilities security model). Two of the main disadvantages are:

  1. Only certain libraries and languages can be used.
  2. Hardware accelerators like GPUs are not easily accessible.

Extensions solve both of these issues, since an extension runs natively. Any language with any library supported by the bare metal host can be run as long as it can speak WebSockets.

Downsides of Extensions

Extensions enable use cases that pure processes lack. However, they come with a cost. Processes are contained and managed by your Kinode, but extensions are not. Extensions are independent servers that run alongside your Kinode. They do not yet have a Kinode-native distribution channel.

As such, extensions should only be used when absolutely necessary. Processes are more stable, maintainable, and easily upgraded. Only write an extension if there is no other choice.

How to Write an Extension?

An extension is composed of two parts: a Kinode package and the extension itself. They communicate with each other over a WebSocket connection that is managed by Kinode. Look at the Talking to the Outside World recipe for an example. The examples below show some more working extensions.

The WebSocket protocol

The process binds a WebSocket, so Kinode acts as the WebSocket server. The extension acts as a client, connecting to the WebSocket served by the Kinode process.

The process sends HttpServerAction::WebSocketExtPushOutgoing Requests to the http_server(look here and here) to communicate with the extension (see the enum defined at the bottom of this section).

Table 1: HttpServerAction::WebSocketExtPushOutgoing Inputs

Field NameDescription
channel_idGiven in a WebSocket message after a client connects.
message_typeThe WebSocketMessage type — recommended to be WsMessageType::Binary.
desired_reply_typeThe Kinode MessageType type that the extension should return — Request or Response.

The lazy_load_blob is the payload for the WebSocket message.

The http_server converts the Request into a HttpServerAction::WebSocketExtPushData, MessagePacks it, and sends it to the extension. Specifically, it attaches the Message's id, copies the desired_reply_type to the kinode_message_type field, and copies the lazy_load_blob to the blob field.

The extension replies with a MessagePacked HttpServerAction::WebSocketExtPushData. It should copy the id and kinode_message_type of the message it is serving into those same fields of the reply. The blob is the payload.

#![allow(unused)]
fn main() {
pub enum HttpServerAction {
    //...
    /// When sent, expects a `lazy_load_blob` containing the WebSocket message bytes to send.
    /// Modifies the `lazy_load_blob` by placing into `WebSocketExtPushData` with id taken from
    /// this `KernelMessage` and `kinode_message_type` set to `desired_reply_type`.
    WebSocketExtPushOutgoing {
        channel_id: u32,
        message_type: WsMessageType,
        desired_reply_type: MessageType,
    },
    /// For communicating with the ext.
    /// Kinode's http_server sends this to the ext after receiving `WebSocketExtPushOutgoing`.
    /// Upon receiving reply with this type from ext, http_server parses, setting:
    /// * id as given,
    /// * message type as given (Request or Response),
    /// * body as HttpServerRequest::WebSocketPush,
    /// * blob as given.
    WebSocketExtPushData {
        id: u64,
        kinode_message_type: MessageType,
        blob: Vec<u8>,
    },
    //...
}
}

The Package

The package is, minimally, a single process that serves as interface between Kinode and the extension. Each extension must come with a corresponding Kinode package.

Specifically, the interface process must:

  1. Bind an extension WebSocket: this will be used to communicate with the extension.
  2. Handle Kinode messages: e.g., Requests to be passed to the extension for processing.
  3. Handle WebSocket messages: these will come from the extension.

'Interface process' will be used interchangeably with 'package' throughout this page.

Bind an Extension WebSocket

The kinode_process_lib provides an easy way to bind an extension WebSocket:

kinode_process_lib::http::bind_ext_path("/")?;

which, for a process with process ID process:package:publisher.os, serves a WebSocket server for the extension to connect to at ws://localhost:8080/process:package:publisher.os. Passing a different endpoint like bind_ext_path("/foo") will append to the WebSocket endpoint like ws://localhost:8080/process:package:publisher.os/foo.

Handle Kinode Messages

Like any Kinode process, the interface process must handle Kinode messages. These are how other Kinode processes will make Requests that are served by the extension:

  1. Process A sends Request.
  2. Interface process receives Request, optionally does some logic, sends Request on to extension via WS.
  3. Extension does computation, replies on WS.
  4. Interface process receives Response, optionally does some logic, sends Response on to process A.

The WebSocket protocol section above discusses how to send messages to the extension over WebSockets. Briefly, a HttpServerAction::WebSocketExtPushOutgoing Request is sent to the http_server, with the payload in the lazy_load_blob.

It is recommended to use the following protocol:

  1. Use the WsMessageType::Binary WebSocket message type and use MessagePack to (de)serialize your messages. MessagePack is space-efficient and well supported by a variety of languages. Structs, dictionaries, arrays, etc. can be (de)serialized in this way. The extension must support MessagePack anyways, since the HttpServerAction::WebSocketExtPushData is (de)serialized using it.
  2. Set desired_reply_type to MessageType::Response type. Then the extension can indicate its reply is a Response, which will allow your Kinode process to properly route it back to the original requestor.
  3. If possible, the original requestor should serialize the lazy_load_blob, and the type of lazy_load_blob should be defined accordingly. Then, all the interface process needs to do is inherit the lazy_load_blob in its http_server Request. This increases efficiency since it avoids bringing those bytes across the Wasm boundry between the process and the runtime (see more discussion here).

Handle WebSocket Messages

At a minimum, the interface process must handle:

Table 2: HttpServerRequest Variants

HttpServerRequest variantDescription
WebSocketOpenSent when an extension connects. Provides the channel_id of the WebSocket connection, needed to message the extension: store this!
WebSocketCloseSent when the WebSocket closes. A good time to clean up the old channel_id, since it will no longer be used.
WebSocketPushUsed for sending payloads between interface and extension.

Although the extension will send a HttpServerAction::WebSocketExtPushData, the http_server converts that into a HttpServerRequest::WebSocketPush. The lazy_load_blob then contains the payload from the extension, which can either be processed in the interface or inherited and passed back to the original requestor process.

The Extension

The extension is, minimally, a WebSocket client that connects to the Kinode interface process. It can be written in any language and it is run natively on the host as a "side car" — a separate binary.

The extension should first connect to the interface process. The recommended pattern is to then iteratively accept and process messages from the WebSocket. Messages come in as MessagePack'd HttpServerAction::WebSocketExtPushData and must be replied to in the same format. The blob field is recommended to also be MessagePack'd. The id and kinode_message_type should be mirrored by the extension: what it receives in those fields should be copied in its reply.

Examples

Find some working examples of runtime extensions below:

Get Help: