From 025ff9fddc8ab3a7af972313f23015aa70112700 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Sat, 8 Feb 2025 19:43:12 -0800 Subject: [PATCH 1/5] remove unnecessary(?) pages --- src/SUMMARY.md | 33 +- src/apis/api_reference.md | 7 - src/apis/eth_provider.md | 239 --------- src/apis/frontend_development.md | 110 ---- src/apis/http_authentication.md | 137 ----- src/apis/http_client.md | 117 ----- src/apis/http_server.md | 205 -------- src/apis/kernel.md | 132 ----- src/apis/kinode_wit.md | 48 -- src/apis/kv.md | 204 -------- src/apis/net.md | 113 ---- src/apis/overview.md | 0 src/apis/sqlite.md | 221 -------- src/apis/terminal.md | 41 -- src/apis/timer.md | 24 - src/apis/vfs.md | 286 ---------- src/apis/websocket.md | 143 ----- src/audits-and-security.md | 14 - src/chess_app/chat.md | 252 --------- src/chess_app/chess_app.md | 4 - src/chess_app/chess_engine.md | 522 ------------------- src/chess_app/chess_home.png | Bin 925870 -> 0 bytes src/chess_app/frontend.md | 314 ----------- src/chess_app/putting_everything_together.md | 34 -- src/chess_app/setup.md | 11 - src/getting_started/design_philosophy.md | 53 -- src/getting_started/getting_started.md | 21 - src/getting_started/install.md | 159 ------ src/getting_started/kimap.md | 104 ---- src/getting_started/login.md | 193 ------- src/kit/boot-fake-node.md | 77 --- src/kit/boot-real-node.md | 47 -- src/kit/build-start-package.md | 106 ---- src/kit/build.md | 108 ---- src/kit/chain.md | 17 - src/kit/connect.md | 28 - src/kit/dev-ui.md | 22 - src/kit/inject-message.md | 40 -- src/kit/kit-dev-toolkit.md | 22 - src/kit/new.md | 39 -- src/kit/publish.md | 77 --- src/kit/remove-package.md | 31 -- src/kit/run-tests.md | 121 ----- src/kit/start-package.md | 11 - src/kit/view-api.md | 18 - src/my_first_app/build_and_deploy_an_app.md | 9 - src/process_stdlib/overview.md | 25 - 47 files changed, 3 insertions(+), 4536 deletions(-) delete mode 100644 src/apis/api_reference.md delete mode 100644 src/apis/eth_provider.md delete mode 100644 src/apis/frontend_development.md delete mode 100644 src/apis/http_authentication.md delete mode 100644 src/apis/http_client.md delete mode 100644 src/apis/http_server.md delete mode 100644 src/apis/kernel.md delete mode 100644 src/apis/kinode_wit.md delete mode 100644 src/apis/kv.md delete mode 100644 src/apis/net.md delete mode 100644 src/apis/overview.md delete mode 100644 src/apis/sqlite.md delete mode 100644 src/apis/terminal.md delete mode 100644 src/apis/timer.md delete mode 100644 src/apis/vfs.md delete mode 100644 src/apis/websocket.md delete mode 100644 src/audits-and-security.md delete mode 100644 src/chess_app/chat.md delete mode 100644 src/chess_app/chess_app.md delete mode 100644 src/chess_app/chess_engine.md delete mode 100644 src/chess_app/chess_home.png delete mode 100644 src/chess_app/frontend.md delete mode 100644 src/chess_app/putting_everything_together.md delete mode 100644 src/chess_app/setup.md delete mode 100644 src/getting_started/design_philosophy.md delete mode 100644 src/getting_started/getting_started.md delete mode 100644 src/getting_started/install.md delete mode 100644 src/getting_started/kimap.md delete mode 100644 src/getting_started/login.md delete mode 100644 src/kit/kit-dev-toolkit.md delete mode 100644 src/my_first_app/build_and_deploy_an_app.md delete mode 100644 src/process_stdlib/overview.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index faef8049..f1883264 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -1,12 +1,8 @@ # Summary -- [Getting Started](./getting_started/getting_started.md) +- Getting Started - [Quick Start](./getting_started/quick_start.md) - [Introduction](./getting_started/intro.md) - - [Kimap and KNS](./getting_started/kimap.md) - - [Design Philosophy](./getting_started/design_philosophy.md) - - [Installation](./getting_started/install.md) - - [Join the Network](./getting_started/login.md) - [System Components](./system/system_components.md) - [Processes](./system/processes_overview.md) - [Process Semantics](./system/process/processes.md) @@ -20,8 +16,7 @@ - [Files](./system/files.md) - [Databases](./system/databases.md) - [Terminal](./system/terminal.md) -- [Process Standard Library](./process_stdlib/overview.md) -- [Kit: Development Tool**kit**](./kit/kit-dev-toolkit.md) +- Kit: Development Tool**kit** - [Installation](./kit/install.md) - [`boot-fake-node`](./kit/boot-fake-node.md) - [`new`](./kit/new.md) @@ -38,18 +33,12 @@ - [`reset-cache`](./kit/reset-cache.md) - [`boot-real-node`](./kit/boot-real-node.md) - [`view-api`](./kit/view-api.md) -- [My First Kinode Application](./my_first_app/build_and_deploy_an_app.md) +- My First Kinode Application - [Environment Setup](./my_first_app/chapter_1.md) - [Sending and Responding to a Message](./my_first_app/chapter_2.md) - [Messaging with More Complex Data Types](./my_first_app/chapter_3.md) - [Frontend Time](./my_first_app/chapter_4.md) - [Sharing with the World](./my_first_app/chapter_5.md) -- [In-Depth Guide: Chess App](./chess_app/chess_app.md) - - [Environment Setup](./chess_app/setup.md) - - [Chess Engine](./chess_app/chess_engine.md) - - [Adding a Frontend](./chess_app/frontend.md) - - [Putting Everything Together](./chess_app/putting_everything_together.md) - - [Extension: Chat](./chess_app/chat.md) - [Cookbook (Handy Recipes)](./cookbook/cookbook.md) - [Saving State](./cookbook/save_state.md) - [Managing Child Processes](./cookbook/manage_child_processes.md) @@ -65,21 +54,5 @@ - [Talking to the Outside World](./cookbook/talking_to_the_outside_world.md) - [Exporting & Importing Package APIs](./cookbook/package_apis.md) - [Exporting Workers in Package APIs](./cookbook/package_apis_workers.md) -- [API Reference](./apis/api_reference.md) - - [ETH Provider API](./apis/eth_provider.md) - - [Frontend/UI Development](./apis/frontend_development.md) - - [HTTP API](./apis/http_authentication.md) - - [HTTP Client API](./apis/http_client.md) - - [HTTP Server API](./apis/http_server.md) - - [Kernel API](./apis/kernel.md) - - [`kinode.wit`](./apis/kinode_wit.md) - - [KV API](./apis/kv.md) - - [Net API](./apis/net.md) - - [SQLite API](./apis/sqlite.md) - - [Terminal API](./apis/terminal.md) - - [Timer API](./apis/timer.md) - - [VFS API](./apis/vfs.md) - - [WebSocket API](./apis/websocket.md) - [Hosted Nodes User Guide](./hosted-nodes.md) -- [Audits and Security](./audits-and-security.md) - [Glossary](./glossary.md) diff --git a/src/apis/api_reference.md b/src/apis/api_reference.md deleted file mode 100644 index 83557906..00000000 --- a/src/apis/api_reference.md +++ /dev/null @@ -1,7 +0,0 @@ -# APIs Overview - -The APIs documented in this section refer to Kinode runtime modules. -Specifically, they are the patterns of Requests and Responses that an app can use to interact with these modules. - -**Note: App developers usually should not use these APIs directly. -Most standard use-cases are better served by using functions in the [Process Standard Library](../process_stdlib/overview.md).** diff --git a/src/apis/eth_provider.md b/src/apis/eth_provider.md deleted file mode 100644 index a0ee3ee7..00000000 --- a/src/apis/eth_provider.md +++ /dev/null @@ -1,239 +0,0 @@ -# ETH Provider API - -**Note: Most processes will not use this API directly. Instead, they will use the `eth` portion of the[`process_lib`](../process_stdlib/overview.md) library, which papers over this API and provides a set of types and functions which are much easier to natively use. -This is mostly useful for re-implementing this module in a different client or performing niche actions unsupported by the library.** - -Processes can send two kinds of requests to `eth:distro:sys`: `EthAction` and `EthConfigAction`. -The former only requires the capability to message the process, while the latter requires the root capability issued by `eth:distro:sys`. -Most processes will only need to send `EthAction` requests. - -```rust -/// The Action and Request type that can be made to eth:distro:sys. Any process with messaging -/// capabilities can send this action to the eth provider. -/// -/// Will be serialized and deserialized using [`serde_json::to_vec`] and [`serde_json::from_slice`]. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum EthAction { - /// Subscribe to logs with a custom filter. ID is to be used to unsubscribe. - /// Logs come in as JSON value which can be parsed to [`alloy::rpc::types::eth::pubsub::SubscriptionResult`] - SubscribeLogs { - sub_id: u64, - chain_id: u64, - kind: SubscriptionKind, - params: serde_json::Value, - }, - /// Kill a SubscribeLogs subscription of a given ID, to stop getting updates. - UnsubscribeLogs(u64), - /// Raw request. Used by kinode_process_lib. - Request { - chain_id: u64, - method: String, - params: serde_json::Value, - }, -} - -/// Subscription kind. Pulled directly from alloy (https://github.com/alloy-rs/alloy). -/// Why? Because alloy is not yet 1.0 and the types in this interface must be stable. -/// If alloy SubscriptionKind changes, we can implement a transition function in runtime -/// for this type. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -#[serde(rename_all = "camelCase")] -pub enum SubscriptionKind { - /// New block headers subscription. - /// - /// Fires a notification each time a new header is appended to the chain, including chain - /// reorganizations. In case of a chain reorganization the subscription will emit all new - /// headers for the new chain. Therefore the subscription can emit multiple headers on the same - /// height. - NewHeads, - /// Logs subscription. - /// - /// Returns logs that are included in new imported blocks and match the given filter criteria. - /// In case of a chain reorganization previous sent logs that are on the old chain will be - /// resent with the removed property set to true. Logs from transactions that ended up in the - /// new chain are emitted. Therefore, a subscription can emit logs for the same transaction - /// multiple times. - Logs, - /// New Pending Transactions subscription. - /// - /// Returns the hash or full tx for all transactions that are added to the pending state and - /// are signed with a key that is available in the node. When a transaction that was - /// previously part of the canonical chain isn't part of the new canonical chain after a - /// reorganization its again emitted. - NewPendingTransactions, - /// Node syncing status subscription. - /// - /// Indicates when the node starts or stops synchronizing. The result can either be a boolean - /// indicating that the synchronization has started (true), finished (false) or an object with - /// various progress indicators. - Syncing, -} -``` - -The `Request` containing this action should always expect a response, since every action variant triggers one and relies on it to be useful. -The ETH provider will respond with the following type: - -```rust -/// The Response body type which a process will get from requesting -/// with an [`EthAction`] will be of this type, serialized and deserialized -/// using [`serde_json::to_vec`] and [`serde_json::from_slice`]. -/// -/// In the case of an [`EthAction::SubscribeLogs`] request, the response will indicate if -/// the subscription was successfully created or not. -#[derive(Debug, Serialize, Deserialize, Clone)] -pub enum EthResponse { - Ok, - Response(serde_json::Value), - Err(EthError), -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum EthError { - /// RPC provider returned an error. - /// Can be parsed to [`alloy::rpc::json_rpc::ErrorPayload`] - RpcError(serde_json::Value), - /// provider module cannot parse message - MalformedRequest, - /// No RPC provider for the chain - NoRpcForChain, - /// Subscription closed - SubscriptionClosed(u64), - /// Invalid method - InvalidMethod(String), - /// Invalid parameters - InvalidParams, - /// Permission denied - PermissionDenied, - /// RPC timed out - RpcTimeout, - /// RPC gave garbage back - RpcMalformedResponse, -} -``` - -The `EthAction::SubscribeLogs` request will receive a response of `EthResponse::Ok` if the subscription was successfully created, or `EthResponse::Err(EthError)` if it was not. -Then, after the subscription is successfully created, the process will receive *Requests* from `eth:distro:sys` containing subscription updates. -That request will look like this: - -```rust -/// Incoming `Request` containing subscription updates or errors that processes will receive. -/// Can deserialize all incoming requests from eth:distro:sys to this type. -/// -/// Will be serialized and deserialized using `serde_json::to_vec` and `serde_json::from_slice`. -pub type EthSubResult = Result; - -/// Incoming type for successful subscription updates. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct EthSub { - pub id: u64, - /// can be parsed to [`alloy::rpc::types::eth::pubsub::SubscriptionResult`] - pub result: serde_json::Value, -} - -/// If your subscription is closed unexpectedly, you will receive this. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct EthSubError { - pub id: u64, - pub error: String, -} -``` - -Again, for most processes, this is the entire API. -The `eth` portion of the `process_lib` library will handle the serialization and deserialization of these types and provide a set of functions and types that are much easier to use. - -### Config API - -If a process has the `root` capability from `eth:distro:sys`, it can send `EthConfigAction` requests. -These actions are used to adjust the underlying providers and relays used by the module, and its settings regarding acting as a relayer for other nodes (public/private/granular etc). - -The configuration of the ETH provider is persisted across two files named `.eth_providers` and `.eth_access_settings` in the node's home directory. `.eth_access_settings` is only created if the configuration is set past the default (private, empty allow/deny lists). - -```rust -/// The action type used for configuring eth:distro:sys. Only processes which have the "root" -/// capability from eth:distro:sys can successfully send this action. -#[derive(Debug, Serialize, Deserialize)] -pub enum EthConfigAction { - /// Add a new provider to the list of providers. - AddProvider(ProviderConfig), - /// Remove a provider from the list of providers. - /// The tuple is (chain_id, node_id/rpc_url). - RemoveProvider((u64, String)), - /// make our provider public - SetPublic, - /// make our provider not-public - SetPrivate, - /// add node to whitelist on a provider - AllowNode(String), - /// remove node from whitelist on a provider - UnallowNode(String), - /// add node to blacklist on a provider - DenyNode(String), - /// remove node from blacklist on a provider - UndenyNode(String), - /// Set the list of providers to a new list. - /// Replaces all existing saved provider configs. - SetProviders(SavedConfigs), - /// Get the list of current providers as a [`SavedConfigs`] object. - GetProviders, - /// Get the current access settings. - GetAccessSettings, - /// Get the state of calls and subscriptions. Used for debugging. - GetState, -} - -pub type SavedConfigs = HashSet; - -/// Provider config. Can currently be a node or a ws provider instance. -#[derive(Clone, Debug, Deserialize, Serialize, Hash, Eq, PartialEq)] -pub struct ProviderConfig { - pub chain_id: u64, - pub trusted: bool, - pub provider: NodeOrRpcUrl, -} - -#[derive(Clone, Debug, Deserialize, Serialize, Hash, Eq, PartialEq)] -pub enum NodeOrRpcUrl { - Node { - kns_update: crate::core::KnsUpdate, - use_as_provider: bool, // false for just-routers inside saved config - }, - RpcUrl(String), -} -``` - -`EthConfigAction` requests should always expect a response. The response body will look like this: -```rust -/// Response type from an [`EthConfigAction`] request. -#[derive(Debug, Serialize, Deserialize)] -pub enum EthConfigResponse { - Ok, - /// Response from a GetProviders request. - /// Note the [`crate::core::KnsUpdate`] will only have the correct `name` field. - /// The rest of the Update is not saved in this module. - Providers(SavedConfigs), - /// Response from a GetAccessSettings request. - AccessSettings(AccessSettings), - /// Permission denied due to missing capability - PermissionDenied, - /// Response from a GetState request - State { - active_subscriptions: HashMap>>, // None if local, Some(node_provider_name) if remote - outstanding_requests: HashSet, - }, -} - -/// Settings for our ETH provider -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct AccessSettings { - pub public: bool, // whether or not other nodes can access through us - pub allow: HashSet, // whitelist for access (only used if public == false) - pub deny: HashSet, // blacklist for access (always used) -} -``` - -A successful `GetProviders` request will receive a response of `EthConfigResponse::Providers(SavedConfigs)`, and a successful `GetAccessSettings` request will receive a response of `EthConfigResponse::AccessSettings(AccessSettings)`. -The other requests will receive a response of `EthConfigResponse::Ok` if they were successful, or `EthConfigResponse::PermissionDenied` if they were not. - -All of these types are serialized to a JSON string via `serde_json` and stored as bytes in the request/response body. -[The source code for this API can be found in the `eth` section of the Kinode runtime library.](https://github.com/kinode-dao/kinode/blob/main/lib/src/eth.rs) diff --git a/src/apis/frontend_development.md b/src/apis/frontend_development.md deleted file mode 100644 index 9110ca94..00000000 --- a/src/apis/frontend_development.md +++ /dev/null @@ -1,110 +0,0 @@ -# Frontend/UI Development - -Kinode can easily serve any webpage or web app developed with normal libraries and frameworks. - -There are some specific endpoints, JS libraries, and `process_lib` functions that are helpful for doing frontend development. - -There are also some important considerations and "gotchas" that can happen when trying to do frontend development. - -Kinode can serve a website or web app just like any HTTP webserver. -The preferred method is to upload your static assets on install by placing them in the `pkg` folder. -By convention, `kit` bundles these assets into a directory inside `pkg` called `ui`, but you can call it anything. -You **must** place your `index.html` in the top-level folder. -The structure should look like this: - -``` -my-package -└── pkg - └── ui (can have any name) - ├── assets (can have any name) - └── index.html -``` - -## /our & /our.js - -Every node has both `/our` and `/our.js` endpoints. -`/our` returns the node's ID as a string like `'my-node'`. -`/our.js` returns a JS script that sets `window.our = { node: 'my-node' }`. -By convention, you can then easily set `window.our.process` either in your UI code or from a process-specific endpoint. -The frontend would then have `window.our` set for use in your code. - -## Serving a Website - -The simplest way to serve a UI is using the `serve_ui` function from `process_lib`: - -``` -serve_ui(&our, "ui", true, false, vec!["/"]).unwrap(); -``` - -This will serve the `index.html` in the specified folder (here, `"ui"`) at the home path of your process. -If your process is called `my-process:my-package:template.os` and your Kinode is running locally on port 8080, -then the UI will be served at `http://localhost:8080/my-process:my-package:template.os`. - -`serve_ui` takes five arguments: our `&Address`, the name of the folder that contains your frontend, whether the UI requires authentication, whether the UI is local-only, and the path(s) on which to serve the UI (usually `["/"]`). - -## Development without kit - -The `kit` UI template uses the React framework compiled with Vite. -But you can use any UI framework as long as it generates an `index.html` and associated assets. -To make development easy, your setup should support a base URL and http proxying. - -### Base URL - -All processes on Kinode are namespaced by process name in the standard format of `process:package:publisher`. -So if your process is called `my-process:my-package:template.os`, then your process can only bind HTTP paths that start with `/my-process:my-package:template.os`. -Your UI should be developed and compiled with the base URL set to the appropriate process path. - -#### Vite - -In `vite.config.ts` (or `.js`) set `base` to your full process name, i.e. -``` -base: '/my-process:my-package:template.os' -``` - -#### Create React App - -In `package.json` set `homepage` to your full process name, i.e. -``` -homepage: '/my-process:my-package:template.os' -``` - -### Proxying HTTP Requests - -In UI development, it is very useful to proxy HTTP requests from the in-dev UI to your Kinode. -Below are some examples. - -#### Vite - -Follow the `server` entry in the [kit template](https://github.com/kinode-dao/kit/blob/master/src/new/templates/ui/chat/ui/vite.config.ts#L31-L47) in your own `vite.config.ts`. - -#### Create React App - -In `package.json` set `proxy` to your Kinode's URL, i.e. -``` -proxy: 'http://localhost:8080' -``` - -### Making HTTP Requests - -When making HTTP requests in your UI, make sure to prepend your base URL to the request. -For example, if your base URL is `/my-process:my-package:template.os`, then a `fetch` request to `/my-endpoint` would look like this: - -``` -fetch('/my-process:my-package:template.os/my-endpoint') -``` - -## Local Development and "gotchas" - -When developing a frontend locally, particularly with a framework like React, it is helpful to proxy HTTP requests through to your node. -The `vite.config.ts` provided in the `kit` template has code to handle this proxying. - -It is important to remember that the frontend will always have the process name as the first part of the HTTP path, -so all HTTP requests and file sources should start with the process name. -Many frontend JavaScript frameworks will handle this by default if you set the `base` or `baseUrl` properly. - -In development, websocket connections can be more annoying to proxy, so it is often easier to simply hardcode the URL if in development. -See your framework documentation for how to check if you are in dev or prod. -The `kit` template already handles this for you. - -Developing against a remote node is simple, you just have to change the proxy target in `vite.config.ts` to the URL of your node. -By default the template will target `http://localhost:8080`. diff --git a/src/apis/http_authentication.md b/src/apis/http_authentication.md deleted file mode 100644 index 1ec48650..00000000 --- a/src/apis/http_authentication.md +++ /dev/null @@ -1,137 +0,0 @@ -# HTTP API - -Incoming HTTP requests are handled by a Rust `warp` server in the core `http-server:distro:sys` process. -This process handles binding (registering) routes, simple JWT-based authentication, and serving a `/login` page if auth is missing. - -## Binding (Registering) HTTP Paths - -Any process that you build can bind (register) any number of HTTP paths with `http-server`. -Every path that you bind will be automatically prepended with the current process' ID. -For example, bind the route `/messages` within a process called `main:my-package:myname.os` like so: - -```rust -use kinode_process_lib::{http::bind_http_path}; - -bind_http_path("/messages", true, false).unwrap(); -``` - -Now, any HTTP requests to your node at `/main:my-package:myname.os/messages` will be routed to your process. - -The other two parameters to `bind_http_path` are `authenticated: bool` and `local_only: bool`. -`authenticated` means that `http-server` will check for an auth cookie (set at login/registration), and `local_only` means that `http-server` will only allow requests that come from `localhost`. - -Incoming HTTP requests will come via `http-server` and have both a `body` and a `lazy_load_blob`. -The `lazy_load_blob` is the HTTP request body itself, and the `body` is an `IncomingHttpRequest`: - -```rust -pub struct IncomingHttpRequest { - /// will parse to SocketAddr - pub source_socket_addr: Option, - /// will parse to http::Method - pub method: String, - /// will parse to url::Url - pub url: String, - /// the matching path that was bound - pub bound_path: String, - /// will parse to http::HeaderMap - pub headers: HashMap, - pub url_params: HashMap, - pub query_params: HashMap, -} -``` - -Note that `url` is the host and full path of the original HTTP request that came in. -`bound_path` is the matching path that was originally bound in `http-server`. - -## Handling HTTP Requests - -Usually, you will want to: -1) determine if an incoming request is a HTTP request. -2) figure out what kind of `IncomingHttpRequest` it is. -3) handle the request based on the path and method. - -Here is an example from the `kit` UI-enabled chat app template that handles both `POST` and `GET` requests to the `/messages` path: - -```rust -fn handle_http-server_request( - our: &Address, - message_archive: &mut MessageArchive, - source: &Address, - body: &[u8], - our_channel_id: &mut u32, -) -> anyhow::Result<()> { - let Ok(server_request) = serde_json::from_slice::(body) else { - // Fail silently if we can't parse the request - return Ok(()); - }; - - match server_request { - - // IMPORTANT BIT: - - HttpServerRequest::Http(IncomingHttpRequest { method, url, .. }) => { - // Check the path - if url.ends_with(&format!("{}{}", our.process.to_string(), "/messages")) { - // Match on the HTTP method - match method.as_str() { - // Get all messages - "GET" => { - let mut headers = HashMap::new(); - headers.insert("Content-Type".to_string(), "application/json".to_string()); - - send_response( - StatusCode::OK, - Some(headers), - serde_json::to_vec(&ChatResponse::History { - messages: message_archive.clone(), - }) - .unwrap(), - )?; - } - // Send a message - "POST" => { - print_to_terminal(0, "1"); - let Some(blob) = get_blob() else { - return Ok(()); - }; - print_to_terminal(0, "2"); - handle_chat_request( - our, - message_archive, - our_channel_id, - source, - &blob.bytes, - true, - )?; - - // Send an http response via the http server - send_response(StatusCode::CREATED, None, vec![])?; - } - _ => { - // Method not allowed - send_response(StatusCode::METHOD_NOT_ALLOWED, None, vec![])?; - } - } - } - } - - _ => {} - }; - - Ok(()) -} -``` - -`send_response` is a `process_lib` function that sends an HTTP response. The function signature is as follows: - -```rust -pub fn send_response( - status: StatusCode, - headers: Option>, - body: Vec, -) -> anyhow::Result<()> -``` - -## App-Specific Authentication - -COMING SOON diff --git a/src/apis/http_client.md b/src/apis/http_client.md deleted file mode 100644 index 8aa1582f..00000000 --- a/src/apis/http_client.md +++ /dev/null @@ -1,117 +0,0 @@ -# HTTP Client API - -See also: [docs.rs for HTTP Client part of `process_lib`](https://docs.rs/kinode_process_lib/latest/kinode_process_lib/http/index.html). - -**Note: Most processes will not use this API directly. Instead, they will use the [`process_lib`](../process_stdlib/overview.md) library, which papers over this API and provides a set of types and functions which are much easier to natively use. This is mostly useful for re-implementing this module in a different client or performing niche actions unsupported by the library.** - -The HTTP client is used for sending and receiving HTTP requests and responses. -It is also used for connecting to a websocket endpoint as a client. -From a process, you may send an `HttpClientAction` to the `http-client:distro:sys` process. -The action must be serialized to JSON and sent in the `body` of a request. -`HttpClientAction` is an `enum` type that includes both HTTP and websocket actions. - -```rust -/// Request type sent to the `http-client:distro:sys` service. -/// -/// Always serialized/deserialized as JSON. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum HttpClientAction { - Http(OutgoingHttpRequest), - WebSocketOpen { - url: String, - headers: HashMap, - channel_id: u32, - }, - WebSocketPush { - channel_id: u32, - message_type: WsMessageType, - }, - WebSocketClose { - channel_id: u32, - }, -} -``` - -The websocket actions, `WebSocketOpen`, `WebSocketPush`, and `WebSocketClose` all require a `channel_id`. -The `channel_id` is used to identify the connection, and must be unique for each connection from a given process. -Two or more connections can have the same `channel_id` if they are from different processes. -`OutgoingHttpRequest` is used to send an HTTP request. - -```rust -/// HTTP Request type that can be shared over Wasm boundary to apps. -/// This is the one you send to the `http-client:distro:sys` service. -/// -/// BODY is stored in the lazy_load_blob, as bytes -/// -/// TIMEOUT is stored in the message expect_response value -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct OutgoingHttpRequest { - /// must parse to [`http::Method`] - pub method: String, - /// must parse to [`http::Version`] - pub version: Option, - /// must parse to [`url::Url`] - pub url: String, - pub headers: HashMap, -} -``` - -All requests to the HTTP client will receive a response of `Result` serialized to JSON. -The process can await or ignore this response, although the desired information will be in the `HttpClientResponse` if the request was successful. -An HTTP request will have an `HttpResponse` defined in the [`http-server`](./http_server.md) module. -A websocket request (open, push, close) will simply respond with a `HttpClientResponse::WebSocketAck`. - -```rust -/// Response type received from the `http-client:distro:sys` service after -/// sending a successful [`HttpClientAction`] to it. -#[derive(Debug, Serialize, Deserialize)] -pub enum HttpClientResponse { - Http(HttpResponse), - WebSocketAck, -} -``` - -```rust -#[derive(Error, Debug, Serialize, Deserialize)] -pub enum HttpClientError { - // HTTP errors - #[error("http-client: request is not valid HttpClientRequest: {req}.")] - BadRequest { req: String }, - #[error("http-client: http method not supported: {method}.")] - BadMethod { method: String }, - #[error("http-client: url could not be parsed: {url}.")] - BadUrl { url: String }, - #[error("http-client: http version not supported: {version}.")] - BadVersion { version: String }, - #[error("http-client: failed to execute request {error}.")] - RequestFailed { error: String }, - - // WebSocket errors - #[error("http-client: failed to open connection {url}.")] - WsOpenFailed { url: String }, - #[error("http-client: failed to send message {req}.")] - WsPushFailed { req: String }, - #[error("http-client: failed to close connection {channel_id}.")] - WsCloseFailed { channel_id: u32 }, -} -``` - -The HTTP client can also receive external websocket messages over an active client connection. -These incoming websocket messages are processed and sent as `HttpClientRequest` to the process that originally opened the websocket. -The message itself is accessible with `get_blob()`. - -```rust -/// Request that comes from an open WebSocket client connection in the -/// `http-client:distro:sys` service. Be prepared to receive these after -/// using a [`HttpClientAction::WebSocketOpen`] to open a connection. -#[derive(Clone, Copy, Debug, Serialize, Deserialize)] -pub enum HttpClientRequest { - WebSocketPush { - channel_id: u32, - message_type: WsMessageType, - }, - WebSocketClose { - channel_id: u32, - }, -} -``` diff --git a/src/apis/http_server.md b/src/apis/http_server.md deleted file mode 100644 index 4b3402fb..00000000 --- a/src/apis/http_server.md +++ /dev/null @@ -1,205 +0,0 @@ -# HTTP Server API - -See also: [docs.rs for HTTP Server part of `process_lib`](https://docs.rs/kinode_process_lib/latest/kinode_process_lib/http/index.html). - -**Note: Most processes will not use this API directly. Instead, they will use the [`process_lib`](../process_stdlib/overview.md) library, which papers over this API and provides a set of types and functions which are much easier to natively use. This is mostly useful for re-implementing this module in a different client or performing niche actions unsupported by the library.** - -The HTTP server is used by sending and receiving requests and responses. -From a process, you may send an `HttpServerAction` to the `http-server:distro:sys` process. - -```rust -/// Request type sent to `http-server:distro:sys` in order to configure it. -/// -/// If a response is expected, all actions will return a Response -/// with the shape `Result<(), HttpServerActionError>` serialized to JSON. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum HttpServerAction { - /// Bind expects a lazy_load_blob if and only if `cache` is TRUE. The lazy_load_blob should - /// be the static file to serve at this path. - Bind { - path: String, - /// Set whether the HTTP request needs a valid login cookie, AKA, whether - /// the user needs to be logged in to access this path. - authenticated: bool, - /// Set whether requests can be fielded from anywhere, or only the loopback address. - local_only: bool, - /// Set whether to bind the lazy_load_blob statically to this path. That is, take the - /// lazy_load_blob bytes and serve them as the response to any request to this path. - cache: bool, - }, - /// SecureBind expects a lazy_load_blob if and only if `cache` is TRUE. The lazy_load_blob should - /// be the static file to serve at this path. - /// - /// SecureBind is the same as Bind, except that it forces requests to be made from - /// the unique subdomain of the process that bound the path. These requests are - /// *always* authenticated, and *never* local_only. The purpose of SecureBind is to - /// serve elements of an app frontend or API in an exclusive manner, such that other - /// apps installed on this node cannot access them. Since the subdomain is unique, it - /// will require the user to be logged in separately to the general domain authentication. - SecureBind { - path: String, - /// Set whether to bind the lazy_load_blob statically to this path. That is, take the - /// lazy_load_blob bytes and serve them as the response to any request to this path. - cache: bool, - }, - /// Unbind a previously-bound HTTP path - Unbind { path: String }, - /// Bind a path to receive incoming WebSocket connections. - /// Doesn't need a cache since does not serve assets. - WebSocketBind { - path: String, - authenticated: bool, - extension: bool, - }, - /// SecureBind is the same as Bind, except that it forces new connections to be made - /// from the unique subdomain of the process that bound the path. These are *always* - /// authenticated. Since the subdomain is unique, it will require the user to be - /// logged in separately to the general domain authentication. - WebSocketSecureBind { path: String, extension: bool }, - /// Unbind a previously-bound WebSocket path - WebSocketUnbind { path: String }, - /// Processes will RECEIVE this kind of request when a client connects to them. - /// If a process does not want this websocket open, they should issue a *request* - /// containing a [`HttpServerAction::WebSocketClose`] message and this channel ID. - WebSocketOpen { path: String, channel_id: u32 }, - /// When sent, expects a lazy_load_blob containing the WebSocket message bytes to send. - WebSocketPush { - channel_id: u32, - message_type: WsMessageType, - }, - /// 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, - }, - /// Sending will close a socket the process controls. - WebSocketClose(u32), -} - -/// The possible message types for [`HttpServerRequest::WebSocketPush`]. -/// Ping and Pong are limited to 125 bytes by the WebSockets protocol. -/// Text will be sent as a Text frame, with the lazy_load_blob bytes -/// being the UTF-8 encoding of the string. Binary will be sent as a -/// Binary frame containing the unmodified lazy_load_blob bytes. -#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] -pub enum WsMessageType { - Text, - Binary, - Ping, - Pong, - Close, -} -``` - -This struct must be serialized to JSON and placed in the `body` of a requests to `http-server:distro:sys`. -For actions that take additional data, such as `Bind` and `WebSocketPush`, it is placed in the `lazy_load_blob` of that request. - -After handling such a request, the HTTP server will always give a response of the shape `Result<(), HttpServerError>`, also serialized to JSON. This can be ignored, or awaited and handled. - -```rust -/// Part of the Response type issued by `http-server:distro:sys` -#[derive(Error, Debug, Serialize, Deserialize)] -pub enum HttpServerError { - #[error("request could not be parsed to HttpServerAction: {req}.")] - BadRequest { req: String }, - #[error("action expected blob")] - NoBlob, - #[error("path binding error: {error}")] - PathBindError { error: String }, - #[error("WebSocket error: {error}")] - WebSocketPushError { error: String }, -} -``` - -Certain actions will cause the HTTP server to send requests to the process in the future. -If a process uses `Bind` or `SecureBind`, that process will need to field future requests from the HTTP server. The server will handle incoming HTTP protocol messages to that path by sending an `HttpServerRequest` to the process which performed the binding, and will expect a response that it can then send to the client. - -**Note: Paths bound using the HTTP server are *always* prefixed by the ProcessId of the process that bound them.** - -**Note 2: If a process creates a static binding by setting `cache` to `true`, the HTTP server will serve whatever bytes were in the accompanying `lazy_load_blob` to all GET requests on that path.** - -If a process uses `WebSocketBind` or `WebSocketSecureBind`, future WebSocket connections to that path will be sent to the process, which is expected to issue a response that can then be sent to the client. - -Bindings can be removed using `Unbind` and `WebSocketUnbind` actions. -Note that the HTTP server module will persist bindings until the node itself is restarted (and no later), so unbinding paths is usually not necessary unless cleaning up an old static resource. - -The incoming request, whether the binding is for HTTP or WebSocket, will look like this: -```rust -/// HTTP Request received from the `http-server:distro:sys` service as a -/// result of either an HTTP or WebSocket binding, created via [`HttpServerAction`]. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum HttpServerRequest { - Http(IncomingHttpRequest), - /// Processes will receive this kind of request when a client connects to them. - /// If a process does not want this websocket open, they should issue a *request* - /// containing a [`HttpServerAction::WebSocketClose`] message and this channel ID. - WebSocketOpen { - path: String, - channel_id: u32, - }, - /// Processes can both SEND and RECEIVE this kind of request - /// (send as [`HttpServerAction::WebSocketPush`]). - /// When received, will contain the message bytes as lazy_load_blob. - WebSocketPush { - channel_id: u32, - message_type: WsMessageType, - }, - /// Receiving will indicate that the client closed the socket. Can be sent to close - /// from the server-side, as [`type@HttpServerAction::WebSocketClose`]. - WebSocketClose(u32), -} - -/// An HTTP request routed to a process as a result of a binding. -/// -/// BODY is stored in the lazy_load_blob, as bytes. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct IncomingHttpRequest { - /// will parse to SocketAddr - pub source_socket_addr: Option, - /// will parse to http::Method - pub method: String, - /// will parse to url::Url - pub url: String, - /// the matching path that was bound - pub bound_path: String, - /// will parse to http::HeaderMap - pub headers: HashMap, - pub url_params: HashMap, - pub query_params: HashMap, -} -``` - -Processes that use the HTTP server should expect to field this request type, serialized to JSON. -The process must issue a response with this structure in the body, serialized to JSON: - -```rust -/// HTTP Response type that can be shared over Wasm boundary to apps. -/// Respond to [`IncomingHttpRequest`] with this type. -/// -/// BODY is stored in the lazy_load_blob, as bytes -#[derive(Debug, Serialize, Deserialize)] -pub struct HttpResponse { - pub status: u16, - pub headers: HashMap, -} -``` - -This response is only required for HTTP requests. -`WebSocketOpen`, `WebSocketPush`, and `WebSocketClose` requests do not require a response. -If a process is meant to send data over an open WebSocket connection, it must issue a `HttpServerAction::WebSocketPush` request with the appropriate `channel_id`. -Find discussion of the `HttpServerAction::WebSocketExt*` requests in the [extensions document](../system/process/extensions.md). diff --git a/src/apis/kernel.md b/src/apis/kernel.md deleted file mode 100644 index d4855748..00000000 --- a/src/apis/kernel.md +++ /dev/null @@ -1,132 +0,0 @@ -# Kernel API - -Generally, userspace applications will not have the capability to message the kernel. -Those that can, such as the app store, have full control over starting and stopping all userspace processes. - -The kernel runtime task accepts one kind of `Request`: -```rust -/// IPC format for requests sent to kernel runtime module -#[derive(Debug, Serialize, Deserialize)] -pub enum KernelCommand { - /// RUNTIME ONLY: used to notify the kernel that booting is complete and - /// all processes have been loaded in from their persisted or bootstrapped state. - Booted, - /// Tell the kernel to install and prepare a new process for execution. - /// The process will not begin execution until the kernel receives a - /// `RunProcess` command with the same `id`. - /// - /// The process that sends this command will be given messaging capabilities - /// for the new process if `public` is false. - /// - /// All capabilities passed into initial_capabilities must be held by the source - /// of this message, or the kernel will discard them (silently for now). - InitializeProcess { - id: ProcessId, - wasm_bytes_handle: String, - wit_version: Option, - on_exit: OnExit, - initial_capabilities: HashSet, - public: bool, - }, - /// Create an arbitrary capability and grant it to a process. - GrantCapabilities { - target: ProcessId, - capabilities: Vec, - }, - /// Drop capabilities. Does nothing if process doesn't have these caps - DropCapabilities { - target: ProcessId, - capabilities: Vec, - }, - /// Tell the kernel to run a process that has already been installed. - /// TODO: in the future, this command could be extended to allow for - /// resource provision. - RunProcess(ProcessId), - /// Kill a running process immediately. This may result in the dropping / mishandling of messages! - KillProcess(ProcessId), - /// RUNTIME ONLY: notify the kernel that the runtime is shutting down and it - /// should gracefully stop and persist the running processes. - Shutdown, - /// Ask kernel to produce debugging information - Debug(KernelPrint), -} -``` - -All `KernelCommand`s are sent in the body field of a `Request`, serialized to JSON. -Only `InitializeProcess`, `RunProcess`, and `KillProcess` will give back a `Response`, also serialized to JSON text bytes using `serde_json`: - -```rust -#[derive(Debug, Serialize, Deserialize)] -pub enum KernelResponse { - InitializedProcess, - InitializeProcessError, - StartedProcess, - RunProcessError, - KilledProcess(ProcessId), - Debug(KernelPrintResponse), -} - -#[derive(Debug, Serialize, Deserialize)] -pub enum KernelPrintResponse { - ProcessMap(UserspaceProcessMap), - Process(Option), - HasCap(Option), -} - -pub type UserspaceProcessMap = HashMap; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct UserspacePersistedProcess { - pub wasm_bytes_handle: String, - pub wit_version: Option, - pub on_exit: OnExit, - pub capabilities: HashSet, - pub public: bool, -} -``` - -## `Booted` - -Purely for internal use within the kernel. -Sent by the kernel, to the kernel, to indicate that all persisted processes have been initialized and are ready to run. - -## `InitializeProcess` - -The first command used to start a new process. -Generally available to apps via the `spawn()` function in the WIT interface. -The `wasm_bytes_handle` is a pointer generated by the [filesystem](../system/files.md) API — it should be a valid `.wasm` file compiled using the [Kinode tooling](../kit/kit-dev-toolkit.md). -The `on_panic` field is an enum that specifies what to do if the process panics. -The `initial_capabilities` field is a set of capabilities that the process will have access to — note that the capabilities are signed by this kernel. -The `public` field specifies whether the process should be visible to other processes *without* needing to grant a messaging capability. - -`InitializeProcess` must be sent with a `lazy_load_blob`. -The blob must be the same .wasm file, in raw bytes, that the `wasm_bytes_handle` points to. - -This will *not* cause the process to begin running. -To do that, send a `RunProcess` command after a successful `InitializeProcess` command. - -## `GrantCapabilities` -This command directly inserts a list of capabilities into another process' state. -While you generally don't want to do this for security reasons, it helps you clean up the "handshake" process by which capabilities must be handed off between two processes before engaging in the business logic. -For instance, if you want a kernel module like `http-server` to be able to message a process back, you do this by directly inserting that `"messaging"` cap into `http-server`'s store. -Only the `app-store`, `terminal`, and `tester` make use of this. - -## `DropCapabilities` -This command removes a list of capabilities from another process' state. -Currently, no app makes use of this, as it is very powerful. - -## `RunProcess` - -Takes a process ID and tells kernel to call the `init` function. -The process must have first been initialized with a successful `InitializeProcess`. - -## `KillProcess` - -Takes a process ID and kills it. -This is a dangerous operation as messages queued for the process will be lost. -The process will be removed from the kernel's process table and will no longer be able to receive messages. - -## `Shutdown` - -Send to the kernel in order to gracefully shut down the system. -The runtime must perform this request before exiting in order to see that all processes are properly cleaned up. diff --git a/src/apis/kinode_wit.md b/src/apis/kinode_wit.md deleted file mode 100644 index 5111648b..00000000 --- a/src/apis/kinode_wit.md +++ /dev/null @@ -1,48 +0,0 @@ -# `kinode.wit` - -Throughout this book, readers will see references to [WIT](https://component-model.bytecodealliance.org/design/wit.html), the [WebAssembly Component Model](https://github.com/WebAssembly/component-model). -WIT, or Wasm Interface Type, is a language for describing the types and functions that are available to a WebAssembly module. -In conjunction with the Component Model itself, WIT allows for the creation of WebAssembly modules that can be used as components in a larger system. -This standard has been under development for many years, and while still under construction, it's the perfect tool for building an operating-system-like environment for Wasm apps. - -Kinode uses WIT to present a standard interface for Kinode processes. -This interface is a set of types and functions that are available to all processes. -It also contains functions (well, just a single function: `init()`) that processes must implement in order to compile and run on Kinode. -If one can generate WIT bindings in a language that compiles to Wasm, that language can be used to write Kinode processes. -So far, we've written Kinode processes in Rust, Javascript, Go, and Python. - -To see exactly how to use WIT to write Kinode processes, see the [My First App](../my_first_app/chapter_1.md) chapter or the [Chess Tutorial](../chess_app/chess_engine.md). - -To see `kinode.wit` for itself, see the [file in the GitHub repo](https://github.com/kinode-dao/kinode-wit/blob/master/kinode.wit). -Since this interface applies to all processes, it's one of the places in the OS where breaking changes are most likely to make an impact. -To that end, the version of the WIT file that a process uses must be compatible with the version of Kinode on which it runs. -Kinode intends to achieve perfect backwards compatibility upon first major release (1.0.0) of the OS and the WIT file. -After that point, since processes signal the version of the WIT file they use, subsequent updates can be made without breaking existing processes or needing to change the version they use. - -## Types - -[These 15 types](https://github.com/kinode-dao/kinode-wit/blob/758fac1fb144f89c2a486778c62cbea2fb5840ac/kinode.wit#L8-L106) make up the entirety of the shared type system between processes and the kernel. -Most types presented here are implemented in the [process standard library](../process_stdlib/overview.md) for ease of use. - -## Functions - -[These 16 functions](https://github.com/kinode-dao/kinode-wit/blob/758fac1fb144f89c2a486778c62cbea2fb5840ac/kinode.wit#L108-L190) are available to processes. -They are implemented in the kernel. -Again, the process standard library makes it such that these functions often don't need to be directly called in processes, but they are always available. -The functions are generally separated into 4 categories: system utilities, process management, capabilities management, and message I/O. -Future versions of the WIT file will certainly add more functions, but the categories themselves are highly unlikely to change. - -System utilities are functions like `print_to_terminal`, whose role is to provide a way for processes to interact with the runtime in an idiosyncratic way. - -Process management functions are used to adjust a processes' state in the kernel. -This includes its state-store and its on-exit behavior. -This category is also responsible for functions that give processes the ability to spawn and manage child processes. - -Capabilities management functions relate to the capabilities-based security system imposed by the kernel on processes. -Processes must acquire and manage capabilities in order to perform tasks external to themselves, such as messaging another process or writing to a file. -See the [capabilities overview](../system/process/capabilities.md) for more details. - -Lastly, message I/O functions are used to send and receive messages between processes. -Message-passing is the primary means by which processes communicate not only with themselves, but also with runtime modules which expose all kinds of I/O abilities. -For example, handling an HTTP request involves sending and receiving messages to and from the `http-server:disto:sys` runtime module. -Interacting with this module and others occurs through message I/O. diff --git a/src/apis/kv.md b/src/apis/kv.md deleted file mode 100644 index 186d60ca..00000000 --- a/src/apis/kv.md +++ /dev/null @@ -1,204 +0,0 @@ -### KV API - -Useful helper functions can be found in the [`kinode_process_lib`](../process_stdlib/overview.md). -More discussion of databases in Kinode can be found [here](../system/databases.md). - -#### Creating/Opening a database - -```rust -use kinode_process_lib::kv; - -let kv = kv::open(our.package_id(), "birthdays")?; - -// You can now pass this KV struct as a reference to other functions -``` - -#### Set - -```rust -let key = b"hello"; -let value= b"world"; - -let returnvalue = kv.set(&key, &value, None)?; -// The third argument None is for tx_id. -// You can group sets and deletes and commit them later. -``` - -#### Get - -```rust -let key = b"hello"; - -let returnvalue = kv.get(&key)?; -``` - -#### Delete - -```rust -let key = b"hello"; - -kv.delete(&key, None)?; -``` - -#### Transactions - -```rust -let tx_id = kv.begin_tx()?; - -let key = b"hello"; -let key2 = b"deleteme"; -let value= b"value"; - -kv.set(&key, &value, Some(tx_id))?; -kv.delete(&key, Some(tx_id))?; - -kv.commit_tx(tx_id)?; -``` - -### API - -```rust -/// Actions are sent to a specific key value database. `db` is the name, -/// `package_id` is the [`PackageId`] that created the database. Capabilities -/// are checked: you can access another process's database if it has given -/// you the read and/or write capability to do so. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct KvRequest { - pub package_id: PackageId, - pub db: String, - pub action: KvAction, -} - -/// IPC Action format representing operations that can be performed on the -/// key-value runtime module. These actions are included in a [`KvRequest`] -/// sent to the `kv:distro:sys` runtime module. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum KvAction { - /// Opens an existing key-value database or creates a new one if it doesn't exist. - /// Requires `package_id` in [`KvRequest`] to match the package ID of the sender. - /// The sender will own the database and can remove it with [`KvAction::RemoveDb`]. - /// - /// A successful open will respond with [`KvResponse::Ok`]. Any error will be - /// contained in the [`KvResponse::Err`] variant. - Open, - /// Permanently deletes the entire key-value database. - /// Requires `package_id` in [`KvRequest`] to match the package ID of the sender. - /// Only the owner can remove the database. - /// - /// A successful remove will respond with [`KvResponse::Ok`]. Any error will be - /// contained in the [`KvResponse::Err`] variant. - RemoveDb, - /// Sets a value for the specified key in the database. - /// - /// # Parameters - /// * `key` - The key as a byte vector - /// * `tx_id` - Optional transaction ID if this operation is part of a transaction - /// * blob: [`Vec`] - Byte vector to store for the key - /// - /// Using this action requires the sender to have the write capability - /// for the database. - /// - /// A successful set will respond with [`KvResponse::Ok`]. Any error will be - /// contained in the [`KvResponse::Err`] variant. - Set { key: Vec, tx_id: Option }, - /// Deletes a key-value pair from the database. - /// - /// # Parameters - /// * `key` - The key to delete as a byte vector - /// * `tx_id` - Optional transaction ID if this operation is part of a transaction - /// - /// Using this action requires the sender to have the write capability - /// for the database. - /// - /// A successful delete will respond with [`KvResponse::Ok`]. Any error will be - /// contained in the [`KvResponse::Err`] variant. - Delete { key: Vec, tx_id: Option }, - /// Retrieves the value associated with the specified key. - /// - /// # Parameters - /// * The key to look up as a byte vector - /// - /// Using this action requires the sender to have the read capability - /// for the database. - /// - /// A successful get will respond with [`KvResponse::Get`], where the response blob - /// contains the value associated with the key if any. Any error will be - /// contained in the [`KvResponse::Err`] variant. - Get(Vec), - /// Begins a new transaction for atomic operations. - /// - /// Sending this will prompt a [`KvResponse::BeginTx`] response with the - /// transaction ID. Any error will be contained in the [`KvResponse::Err`] variant. - BeginTx, - /// Commits all operations in the specified transaction. - /// - /// # Parameters - /// * `tx_id` - The ID of the transaction to commit - /// - /// A successful commit will respond with [`KvResponse::Ok`]. Any error will be - /// contained in the [`KvResponse::Err`] variant. - Commit { tx_id: u64 }, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum KvResponse { - /// Indicates successful completion of an operation. - /// Sent in response to actions Open, RemoveDb, Set, Delete, and Commit. - Ok, - /// Returns the transaction ID for a newly created transaction. - /// - /// # Fields - /// * `tx_id` - The ID of the newly created transaction - BeginTx { tx_id: u64 }, - /// Returns the value for the key that was retrieved from the database. - /// - /// # Parameters - /// * The retrieved key as a byte vector - /// * blob: [`Vec`] - Byte vector associated with the key - Get(Vec), - /// Indicates an error occurred during the operation. - Err(KvError), -} - -#[derive(Clone, Debug, Serialize, Deserialize, Error)] -pub enum KvError { - #[error("db [{0}, {1}] does not exist")] - NoDb(PackageId, String), - #[error("key not found")] - KeyNotFound, - #[error("no transaction {0} found")] - NoTx(u64), - #[error("no write capability for requested DB")] - NoWriteCap, - #[error("no read capability for requested DB")] - NoReadCap, - #[error("request to open or remove DB with mismatching package ID")] - MismatchingPackageId, - #[error("failed to generate capability for new DB")] - AddCapFailed, - #[error("kv got a malformed request that either failed to deserialize or was missing a required blob")] - MalformedRequest, - #[error("RocksDB internal error: {0}")] - RocksDBError(String), - #[error("IO error: {0}")] - IOError(String), -} - -/// The JSON parameters contained in all capabilities issued by `kv:distro:sys`. -/// -/// # Fields -/// * `kind` - The kind of capability, either [`KvCapabilityKind::Read`] or [`KvCapabilityKind::Write`] -/// * `db_key` - The database key, a tuple of the [`PackageId`] that created the database and the database name -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct KvCapabilityParams { - pub kind: KvCapabilityKind, - pub db_key: (PackageId, String), -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum KvCapabilityKind { - Read, - Write, -} -``` diff --git a/src/apis/net.md b/src/apis/net.md deleted file mode 100644 index df9cc941..00000000 --- a/src/apis/net.md +++ /dev/null @@ -1,113 +0,0 @@ -# Net API - -Most processes will not use this API directly. -Instead, processes will make use of the networking protocol simply by sending messages to processes running on other nodes. -This API is documented, rather, for those who wish to implement their own networking protocol. - -The networking API is implemented in the `net:distro:sys` process. - -For the specific networking protocol, see the [networking protocol](../system/networking_protocol.md) chapter. -This chapter is rather to describe the message-based API that the `net:distro:sys` process exposes. - -`Net`, like all processes and runtime modules, is architected around a main message-receiving loop. -The received `Request`s are handled in one of three ways: - -- If the `target.node` is "our domain", i.e. the domain name of the local node, and the `source.node` is also our domain, the message is parsed and treated as either a debugging command or one of the `NetActions` enum. - -- If the `target.node` is our domain, but the `source.node` is not, the message is either parsed as the `NetActions` enum, or if it fails to parse, is treated as a "hello" message and printed in the terminal, size permitting. This "hello" protocol simply attempts to display the `message.body` as a UTF-8 string and is mostly used for network debugging. - -- If the `source.node` is our domain, but the `target.node` is not, the message is sent to the target using the [networking protocol](../system/networking_protocol.md) implementation. - -Let's look at `NetActions`. Note that this message type can be received from remote or local processes. -Different implementations of the networking protocol may reject actions depending on whether they were instigated locally or remotely, and also discriminate on which remote node sent the action. -This is, for example, where a router would choose whether or not to perform routing for a specific node<>node connection. - -```rust -/// Must be parsed from message pack vector. -/// all Get actions must be sent from local process. used for debugging -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum NetAction { - /// Received from a router of ours when they have a new pending passthrough for us. - /// We should respond (if we desire) by using them to initialize a routed connection - /// with the NodeId given. - ConnectionRequest(NodeId), - /// can only receive from trusted source: requires net root cap - KnsUpdate(KnsUpdate), - /// can only receive from trusted source: requires net root cap - KnsBatchUpdate(Vec), - /// get a list of peers we are connected to - GetPeers, - /// get the [`Identity`] struct for a single peer - GetPeer(String), - /// get a user-readable diagnostics string containing networking inforamtion - GetDiagnostics, - /// sign the attached blob payload, sign with our node's networking key. - /// **only accepted from our own node** - /// **the source [`Address`] will always be prepended to the payload** - Sign, - /// given a message in blob payload, verify the message is signed by - /// the given source. if the signer is not in our representation of - /// the PKI, will not verify. - /// **the `from` [`Address`] will always be prepended to the payload** - Verify { from: Address, signature: Vec }, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Hash, Eq, PartialEq)] -pub struct KnsUpdate { - pub name: String, - pub public_key: String, - pub ips: Vec, - pub ports: BTreeMap, - pub routers: Vec, -} -``` - -This type must be parsed from a request body using MessagePack. -`ConnectionRequest` is sent by remote nodes as part of the WebSockets networking protocol in order to ask a router to connect them to a node that they can't connect to directly. -This is responded to with either an `Accepted` or `Rejected` variant of `NetResponses`. - -`KnsUpdate` and `KnsBatchUpdate` both are used as entry point by which the `net` module becomes aware of the Kinode PKI, or KNS. -In the current distro these are only accepted from the local node, and specifically the `kns-indexer` distro package. - -`GetPeers` is used to request a list of peers that the `net` module is connected to. It can only be received from the local node. - -`GetPeer` is used to request the `Identity` struct for a single peer. It can only be received from the local node. - -`GetName` is used to request the `NodeId` associated with a given namehash. It can only be received from the local node. - -`GetDiagnostics` is used to request a user-readable diagnostics string containing networking information. It can only be received from the local node. - -`Sign` is used to request that the attached blob payload be signed with our node's networking key. It can only be received from the local node. - -`Verify` is used to request that the attached blob payload be verified as being signed by the given source. It can only be received from the local node. - - -Finally, let's look at the type parsed from a `Response`. - -```rust -/// Must be parsed from message pack vector -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum NetResponse { - /// response to [`NetAction::ConnectionRequest`] - Accepted(NodeId), - /// response to [`NetAction::ConnectionRequest`] - Rejected(NodeId), - /// response to [`NetAction::GetPeers`] - Peers(Vec), - /// response to [`NetAction::GetPeer`] - Peer(Option), - /// response to [`NetAction::GetDiagnostics`]. a user-readable string. - Diagnostics(String), - /// response to [`NetAction::Sign`]. contains the signature in blob - Signed, - /// response to [`NetAction::Verify`]. boolean indicates whether - /// the signature was valid or not. note that if the signer node - /// cannot be found in our representation of PKI, this will return false, - /// because we cannot find the networking public key to verify with. - Verified(bool), -} -``` - -This type must be also be parsed using MessagePack, this time from responses received by `net`. - -In the future, `NetActions` and `NetResponses` may both expand to cover message types required for implementing networking protocols other than the WebSockets one. diff --git a/src/apis/overview.md b/src/apis/overview.md deleted file mode 100644 index e69de29b..00000000 diff --git a/src/apis/sqlite.md b/src/apis/sqlite.md deleted file mode 100644 index f4c4a9a0..00000000 --- a/src/apis/sqlite.md +++ /dev/null @@ -1,221 +0,0 @@ -### SQLite API - -Useful helper functions can be found in the [`kinode_process_lib`](../process_stdlib/overview.md). -More discussion of databases in Kinode can be found [here](../system/databases.md). - -#### Creating/Opening a database - -```rust -use kinode_process_lib::sqlite; - -let db = sqlite::open(our.package_id(), "users")?; -// You can now pass this SQLite struct as a reference to other functions -``` - -#### Write - -```rust -let statement = "INSERT INTO users (name) VALUES (?), (?), (?);".to_string(); -let params = vec![ -serde_json::Value::String("Bob".to_string()), -serde_json::Value::String("Charlie".to_string()), -serde_json::Value::String("Dave".to_string()), -]; - -sqlite.write(statement, params, None)?; -``` - -#### Read - -```rust -let query = "SELECT FROM users;".to_string(); -let rows = sqlite.read(query, vec![])?; -// rows: Vec> -println!("rows: {}", rows.len()); -for row in rows { - println!(row.get("name")); -} -``` - -#### Transactions - -```rust -let tx_id = sqlite.begin_tx()?; - -let statement = "INSERT INTO users (name) VALUES (?);".to_string(); -let params = vec![serde_json::Value::String("Eve".to_string())]; -let params2 = vec![serde_json::Value::String("Steve".to_string())]; - -sqlite.write(statement, params, Some(tx_id))?; -sqlite.write(statement, params2, Some(tx_id))?; - -sqlite.commit_tx(tx_id)?; -``` - -### API - -```rust -/// Actions are sent to a specific SQLite database. `db` is the name, -/// `package_id` is the [`PackageId`] that created the database. Capabilities -/// are checked: you can access another process's database if it has given -/// you the read and/or write capability to do so. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct SqliteRequest { - pub package_id: PackageId, - pub db: String, - pub action: SqliteAction, -} - -/// IPC Action format representing operations that can be performed on the -/// SQLite runtime module. These actions are included in a [`SqliteRequest`] -/// sent to the `sqlite:distro:sys` runtime module. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum SqliteAction { - /// Opens an existing key-value database or creates a new one if it doesn't exist. - /// Requires `package_id` in [`SqliteRequest`] to match the package ID of the sender. - /// The sender will own the database and can remove it with [`SqliteAction::RemoveDb`]. - /// - /// A successful open will respond with [`SqliteResponse::Ok`]. Any error will be - /// contained in the [`SqliteResponse::Err`] variant. - Open, - /// Permanently deletes the entire key-value database. - /// Requires `package_id` in [`SqliteRequest`] to match the package ID of the sender. - /// Only the owner can remove the database. - /// - /// A successful remove will respond with [`SqliteResponse::Ok`]. Any error will be - /// contained in the [`SqliteResponse::Err`] variant. - RemoveDb, - /// Executes a write statement (INSERT/UPDATE/DELETE) - /// - /// * `statement` - SQL statement to execute - /// * `tx_id` - Optional transaction ID - /// * blob: Vec - Parameters for the SQL statement, where SqlValue can be: - /// - null - /// - boolean - /// - i64 - /// - f64 - /// - String - /// - Vec (binary data) - /// - /// Using this action requires the sender to have the write capability - /// for the database. - /// - /// A successful write will respond with [`SqliteResponse::Ok`]. Any error will be - /// contained in the [`SqliteResponse::Err`] variant. - Write { - statement: String, - tx_id: Option, - }, - /// Executes a read query (SELECT) - /// - /// * blob: Vec - Parameters for the SQL query, where SqlValue can be: - /// - null - /// - boolean - /// - i64 - /// - f64 - /// - String - /// - Vec (binary data) - /// - /// Using this action requires the sender to have the read capability - /// for the database. - /// - /// A successful query will respond with [`SqliteResponse::Query`], where the - /// response blob contains the results of the query. Any error will be contained - /// in the [`SqliteResponse::Err`] variant. - Query(String), - /// Begins a new transaction for atomic operations. - /// - /// Sending this will prompt a [`SqliteResponse::BeginTx`] response with the - /// transaction ID. Any error will be contained in the [`SqliteResponse::Err`] variant. - BeginTx, - /// Commits all operations in the specified transaction. - /// - /// # Parameters - /// * `tx_id` - The ID of the transaction to commit - /// - /// A successful commit will respond with [`SqliteResponse::Ok`]. Any error will be - /// contained in the [`SqliteResponse::Err`] variant. - Commit { tx_id: u64 }, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum SqliteResponse { - /// Indicates successful completion of an operation. - /// Sent in response to actions Open, RemoveDb, Write, Query, BeginTx, and Commit. - Ok, - /// Returns the results of a query. - /// - /// * blob: Vec> - Array of rows, where each row contains SqlValue types: - /// - null - /// - boolean - /// - i64 - /// - f64 - /// - String - /// - Vec (binary data) - Read, - /// Returns the transaction ID for a newly created transaction. - /// - /// # Fields - /// * `tx_id` - The ID of the newly created transaction - BeginTx { tx_id: u64 }, - /// Indicates an error occurred during the operation. - Err(SqliteError), -} - -/// Used in blobs to represent array row values in SQLite. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub enum SqlValue { - Integer(i64), - Real(f64), - Text(String), - Blob(Vec), - Boolean(bool), - Null, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Error)] -pub enum SqliteError { - #[error("db [{0}, {1}] does not exist")] - NoDb(PackageId, String), - #[error("no transaction {0} found")] - NoTx(u64), - #[error("no write capability for requested DB")] - NoWriteCap, - #[error("no read capability for requested DB")] - NoReadCap, - #[error("request to open or remove DB with mismatching package ID")] - MismatchingPackageId, - #[error("failed to generate capability for new DB")] - AddCapFailed, - #[error("write statement started with non-existent write keyword")] - NotAWriteKeyword, - #[error("read query started with non-existent read keyword")] - NotAReadKeyword, - #[error("parameters blob in read/write was misshapen or contained invalid JSON objects")] - InvalidParameters, - #[error("sqlite got a malformed request that failed to deserialize")] - MalformedRequest, - #[error("rusqlite error: {0}")] - RusqliteError(String), - #[error("IO error: {0}")] - IOError(String), -} - -/// The JSON parameters contained in all capabilities issued by `sqlite:distro:sys`. -/// -/// # Fields -/// * `kind` - The kind of capability, either [`SqliteCapabilityKind::Read`] or [`SqliteCapabilityKind::Write`] -/// * `db_key` - The database key, a tuple of the [`PackageId`] that created the database and the database name -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct SqliteCapabilityParams { - pub kind: SqliteCapabilityKind, - pub db_key: (PackageId, String), -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum SqliteCapabilityKind { - Read, - Write, -} -``` diff --git a/src/apis/terminal.md b/src/apis/terminal.md deleted file mode 100644 index efb01986..00000000 --- a/src/apis/terminal.md +++ /dev/null @@ -1,41 +0,0 @@ -# Terminal API - -It is extremely rare for an app to have direct access to the terminal api. -Normally, the terminal will be used to call scripts, which will have access to the process in question. -For documentation on using, writing, publishing, and composing scripts, see the [terminal use documentation](../system/terminal.md), or for a quick start, the [script cookbook](../cookbook/writing_scripts.md). - -The Kinode terminal is broken up into two segments: a Wasm app, called `terminal:terminal:sys`, and a runtime module called `terminal:distro:sys`. -The Wasm app is the central area where terminal logic and authority live. -It parses `Requests` by attempting to read the `body` field as a UTF-8 string, then parsing that string into various commands to perform. -The runtime module exists in order to actually use this app from the terminal which is launched by starting Kinode. -It manages the raw input and presents an interface with features such as command history, text manipulation, and shortcuts. - -To "use" the terminal as an API, one simply needs the capability to message `terminal:terminal:sys`. -This is a powerful capability, equivalent to giving an application `root` authority over your node. -For this reason, users are unlikely to grant direct terminal access to most apps. - -If one does have the capability to send `Request`s to the terminal, they can execute commands like so: -``` -script-name:package-name:publisher-name -``` - -For example, the `hi` script, which pings another node's terminal with a message, can be called like so: -``` -hi:terminal:sys default-router-1.os what's up? -``` -In this case, the arguments are both `default-router-1.os` and the message `what's up?`. - -Some commonly used scripts have shorthand aliases because they are invoked so frequently. -For example, `hi:terminal:sys` can be shortened to just `hi` as in: -``` -hi default-router-1.os what's up? -``` - -The other most commonly used script is `m:terminal:sys`, or just `m` - which stands for `Message`. -`m` lets you send a request to any node or application like so: -``` -m some-node.os@proc:pkg:pub '{"foo":"bar"}' -``` - -Note that if your process has the ability to message the `terminal` app, then that process can call any script. -However, they will all have this standard calling convention of ` `. diff --git a/src/apis/timer.md b/src/apis/timer.md deleted file mode 100644 index 676eeb1c..00000000 --- a/src/apis/timer.md +++ /dev/null @@ -1,24 +0,0 @@ -# Timer API - -The Timer API allows processes to manage time-based operations within Kinode. -This API provides a simple yet powerful mechanism for scheduling actions to be executed after a specified delay. -The entire API is just the `TimerAction`: - -```rust -pub enum TimerAction { - Debug, - SetTimer(u64), -} -``` -This defines just two actions: `Debug` and `SetTimer` -## `Debug` -This action will print information about all active timers to the terminal. -## `SetTimer` -This lets you set a timer to pop after a set number of milliseconds, so e.g. `{"SetTimer": 1000}` would pop after one second. -The timer finishes by sending a `Response` once the timer has popped. -The response will have no information in the `body`. -To keep track of different timers, you can use two methods: -- `send_and_await_response` which will block your app while it is waiting - - use [`kinode_process_lib::timer::set_and_await_timer`](https://docs.rs/kinode_process_lib/0.0.0-reserved/kinode_process_lib/timer/fn.set_and_await_timer.html) for this -- use `context` to keep track of multiple timers without blocking - - use [`kinode_process_lib::timer::set_timer`](https://docs.rs/kinode_process_lib/0.0.0-reserved/kinode_process_lib/timer/fn.set_timer.html) to set the timer with optional context diff --git a/src/apis/vfs.md b/src/apis/vfs.md deleted file mode 100644 index dc8dfa99..00000000 --- a/src/apis/vfs.md +++ /dev/null @@ -1,286 +0,0 @@ -# VFS API - -Useful helper functions can be found in the [`kinode_process_lib`](https://github.com/kinode-dao/process_lib) - -The VFS API tries to map over the [`std::fs`](https://doc.rust-lang.org/std/fs/index.html) calls as directly as possible. - -Every request takes a path and a corresponding action. - -## Drives - -A drive is a directory within a package's VFS directory, e.g., `app-store:sys/pkg/` or `your_package:publisher.os/my_drive/`. -Drives are owned by packages. -Packages can share access to drives they own via [capabilities](../system/process/capabilities.md). -Each package is spawned with two drives: [`pkg/`](#pkg-drive) and [`tmp/`](#tmp-drive). -All processes in a package have caps to those drives. -Processes can also create additional drives. - -### `pkg/` drive - -The `pkg/` drive contains metadata about the package that Kinode requires to run that package, `.wasm` binaries, and optionally the API of the package and the UI. -When creating packages, the `pkg/` drive is populated by [`kit build`](../kit/build.md) and loaded into the Kinode using [`kit start-package`](../kit/start-package.md). - -### `tmp/` drive - -The `tmp/` drive can be written to directly by the owning package using standard filesystem functionality (i.e. `std::fs` in Rust) via WASI in addition to the Kinode VFS. - -### Imports - -```rust -use kinode_process_lib::vfs::{ - create_drive, open_file, open_dir, create_file, metadata, File, Directory, -}; -``` - -### Opening/Creating a Drive - -```rust -let drive_path: String = create_drive(our.package_id(), "drive_name")?; -// you can now prepend this path to any files/directories you're interacting with -let file = open_file(&format!("{}/hello.txt", &drive_path), true); -``` - -### Sharing a Drive Capability - -```rust -let vfs_read_cap = serde_json::json!({ - "kind": "read", - "drive": drive_path, -}).to_string(); - -let vfs_address = Address { - node: our.node.clone(), - process: ProcessId::from_str("vfs:distro:sys").unwrap(), -}; - -// get this capability from our store -let cap = get_capability(&vfs_address, &vfs_read_cap); - -// now if we have that Capability, we can attach it to a subsequent message. -if let Some(cap) = cap { - Request::new() - .capabilities(vec![cap]) - .body(b"hello".to_vec()) - .send()?; -} -``` - -```rust -// the receiving process can then save the capability to it's store, and open the drive. -save_capabilities(incoming_request.capabilities); -let dir = open_dir(&drive_path, false)?; -``` - -### Files - -#### Open a File - -```rust -/// Opens a file at path, if no file at path, creates one if boolean create is true. -let file_path = format!("{}/hello.txt", &drive_path); -let file = open_file(&file_path, true); -``` - -#### Create a File - -```rust -/// Creates a file at path, if file found at path, truncates it to 0. -let file_path = format!("{}/hello.txt", &drive_path); -let file = create_file(&file_path); -``` - -#### Read a File - -```rust -/// Reads the entire file, from start position. -/// Returns a vector of bytes. -let contents = file.read()?; -``` - -#### Write a File - -```rust -/// Write entire slice as the new file. -/// Truncates anything that existed at path before. -let buffer = b"Hello!"; -file.write(&buffer)?; -``` - -#### Write to File - -```rust -/// Write buffer to file at current position, overwriting any existing data. -let buffer = b"World!"; -file.write_all(&buffer)?; -``` - -#### Read at position - -```rust -/// Read into buffer from current cursor position -/// Returns the amount of bytes read. -let mut buffer = vec![0; 5]; -file.read_at(&buffer)?; -``` - -#### Set Length - -```rust -/// Set file length, if given size > underlying file, fills it with 0s. -file.set_len(42)?; -``` - -#### Seek to a position - -```rust -/// Seek file to position. -/// Returns the new position. -let position = SeekFrom::End(0); -file.seek(&position)?; -``` - -#### Sync - -```rust -/// Syncs path file buffers to disk. -file.sync_all()?; -``` - -#### Metadata - -```rust -/// Metadata of a path, returns file type and length. -let metadata = file.metadata()?; -``` - -### Directories - -#### Open a Directory - -```rust -/// Opens or creates a directory at path. -/// If trying to create an existing file, will just give you the path. -let dir_path = format!("{}/my_pics", &drive_path); -let dir = open_dir(&dir_path, true); -``` - -#### Read a Directory - -```rust -/// Iterates through children of directory, returning a vector of DirEntries. -/// DirEntries contain the path and file type of each child. -let entries = dir.read()?; -``` - -#### General path Metadata - -```rust -/// Metadata of a path, returns file type and length. -let some_path = format!("{}/test", &drive_path); -let metadata = metadata(&some_path)?; -``` - -### API - -```rust -/// IPC Request format for the vfs:distro:sys runtime module. -#[derive(Debug, Serialize, Deserialize)] -pub struct VfsRequest { - pub path: String, - pub action: VfsAction, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -pub enum VfsAction { - CreateDrive, - CreateDir, - CreateDirAll, - CreateFile, - OpenFile { create: bool }, - CloseFile, - Write, - WriteAll, - Append, - SyncAll, - Read, - ReadDir, - ReadToEnd, - ReadExact { length: u64 }, - ReadToString, - Seek(SeekFrom), - RemoveFile, - RemoveDir, - RemoveDirAll, - Rename { new_path: String }, - Metadata, - AddZip, - CopyFile { new_path: String }, - Len, - SetLen(u64), - Hash, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -pub enum SeekFrom { - Start(u64), - End(i64), - Current(i64), -} - -#[derive(Debug, Serialize, Deserialize)] -pub enum FileType { - File, - Directory, - Symlink, - Other, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct FileMetadata { - pub file_type: FileType, - pub len: u64, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct DirEntry { - pub path: String, - pub file_type: FileType, -} - -#[derive(Debug, Serialize, Deserialize)] -pub enum VfsResponse { - Ok, - Err(VfsError), - Read, - SeekFrom { new_offset: u64 }, - ReadDir(Vec), - ReadToString(String), - Metadata(FileMetadata), - Len(u64), - Hash([u8; 32]), -} - -#[derive(Error, Debug, Serialize, Deserialize)] -pub enum VfsError { - #[error("No capability for action {action} at path {path}")] - NoCap { action: String, path: String }, - #[error("Bytes blob required for {action} at path {path}")] - BadBytes { action: String, path: String }, - #[error("bad request error: {error}")] - BadRequest { error: String }, - #[error("error parsing path: {path}: {error}")] - ParseError { error: String, path: String }, - #[error("IO error: {error}, at path {path}")] - IOError { error: String, path: String }, - #[error("kernel capability channel error: {error}")] - CapChannelFail { error: String }, - #[error("Bad JSON blob: {error}")] - BadJson { error: String }, - #[error("File not found at path {path}")] - NotFound { path: String }, - #[error("Creating directory failed at path: {path}: {error}")] - CreateDirError { path: String, error: String }, - #[error("Other error: {error}")] - Other { error: String }, -} -``` diff --git a/src/apis/websocket.md b/src/apis/websocket.md deleted file mode 100644 index 75028eee..00000000 --- a/src/apis/websocket.md +++ /dev/null @@ -1,143 +0,0 @@ -# WebSocket API - -WebSocket connections are made with a Rust `warp` server in the core `http-server:distro:sys` process. -Each connection is assigned a `channel_id` that can be bound to a given process using a `WsRegister` message. -The process receives the `channel_id` for pushing data into the WebSocket, and any subsequent messages from that client will be forwarded to the bound process. - -## Opening a WebSocket Channel from a Client - -To open a WebSocket channel, connect to the main route on the node `/` and send a `WsRegister` message as either text or bytes. - -The simplest way to connect from a browser is to use the `@kinode/client-api` like so: - -```rs -const api = new KinodeEncryptorApi({ - nodeId: window.our.node, // this is set if the /our.js script is present in index.html - processId: "my-package:my-package:template.os", - onOpen: (_event, api) => { - console.log('Connected to Kinode') - // Send a message to the node via WebSocket - api.send({ data: 'Hello World' }) - }, -}) -``` - -`@kinode/client-api` is available here: [https://www.npmjs.com/package/@kinode/client-api](https://www.npmjs.com/package/@kinode/client-api) - -Simple JavaScript/JSON example: - -```rs -function getCookie(name) { - const cookies = document.cookie.split(';'); - for (let i = 0; i < cookies.length; i++) { - const cookie = cookies[i].trim(); - if (cookie.startsWith(name)) { - return cookie.substring(name.length + 1); - } - } -} - -const websocket = new WebSocket("http://localhost:8080/"); - -const message = JSON.stringify({ - "auth_token": getCookie(`kinode-auth_${nodeId}`), - "target_process": "my-package:my-package:template.os", - "encrypted": false, -}); - -websocket.send(message); -``` - -## Handling Incoming WebSocket Messages - -Incoming WebSocket messages will be enums of `HttpServerRequest` with type `WebSocketOpen`, `WebSocketPush`, or `WebSocketClose`. - -You will want to store the `channel_id` that comes in with `WebSocketOpen` so that you can push data to that WebSocket. -If you expect to have more than one client connected at a time, then you will most likely want to store the channel IDs in a Set (Rust `HashSet`). - -With a `WebSocketPush`, the incoming message will be on the `LazyLoadBlob`, accessible with `get_blob()`. - -`WebSocketClose` will have the `channel_id` of the closed channel, so that you can remove it from wherever you are storing it. - -A full example: - -```rs -fn handle_http-server_request( - our: &Address, - message_archive: &mut MessageArchive, - source: &Address, - body: &[u8], - channel_ids: &mut HashSet, -) -> anyhow::Result<()> { - let Ok(server_request) = serde_json::from_slice::(body) else { - // Fail silently if we can't parse the request - return Ok(()); - }; - - match server_request { - HttpServerRequest::WebSocketOpen { channel_id, .. } => { - // Set our channel_id to the newly opened channel - // Note: this code could be improved to support multiple channels - channel_ids.insert(channel_id); - } - HttpServerRequest::WebSocketPush { .. } => { - let Some(blob) = get_blob() else { - return Ok(()); - }; - - handle_chat_request( - our, - message_archive, - our_channel_id, - source, - &blob.bytes, - false, - )?; - } - HttpServerRequest::WebSocketClose(_channel_id) => { - channel_ids.remove(channel_id); - } - HttpServerRequest::Http(IncomingHttpRequest { method, url, bound_path, .. }) => { - // Handle incoming HTTP requests here - } - }; - - Ok(()) -} -``` - -## Pushing Data to a Client via WebSocket - -Pushing data to a connected WebSocket is very simple. Call the `send_ws_push` function from `process_lib`: - -```rs -pub fn send_ws_push( - node: String, - channel_id: u32, - message_type: WsMessageType, - blob: LazyLoadBlob, -) -> anyhow::Result<()> -``` - -`node` will usually be `our.node` (although you can also send a WS push to another node's `http-server`!), `channel_id` is the client you want to send to, `message_type` will be either `WsMessageType::Text` or `WsMessageType::Binary`, and `blob` will be a standard `LazyLoadBlob` with an optional `mime` field and required `bytes` field. - -If you would prefer to send the request without the helper function, this is that what `send_ws_push` looks like under the hood: - -```rs -Request::new() - .target(Address::new( - node, - ProcessId::from_str("http-server:distro:sys").unwrap(), - )) - .body( - serde_json::json!(HttpServerRequest::WebSocketPush { - channel_id, - message_type, - }) - .to_string() - .as_bytes() - .to_vec(), - ) - .blob(blob) - .send()?; -``` diff --git a/src/audits-and-security.md b/src/audits-and-security.md deleted file mode 100644 index 70d91862..00000000 --- a/src/audits-and-security.md +++ /dev/null @@ -1,14 +0,0 @@ -# Audits and Security - -The Kinode operating system runtime has been audited by [Enigma Dark](https://www.enigmadark.com/). -That report can be found [here](https://github.com/Enigma-Dark/security-review-reports/blob/main/2024-11-18_Architecture_Review_Report_Kinode.pdf). - -However, the audit was not comprehensive and focused on the robustness of the networking stack and the kernel. -Therefore, other parts of the runtime, such as the filesystem modules and the ETH RPC layer, remain unaudited. -Kinode OS remains a work in progress and will continue to be audited as it matures. - -### Smart Contracts - -Kinode OS uses a number of smart contracts to manage global state. -Audits below: -- [Kimap audit](https://cantina.xyz/portfolio/c2cbcbe7-727c-47cf-99f1-4e82ea8e5c77) by [Spearbit](https://spearbit.com/) diff --git a/src/chess_app/chat.md b/src/chess_app/chat.md deleted file mode 100644 index 472d45b5..00000000 --- a/src/chess_app/chat.md +++ /dev/null @@ -1,252 +0,0 @@ -# Extension 1: Chat - -So, at this point you've got a working chess game with a frontend. -There are a number of obvious improvements to the program to be made, as listed at the end of the [last chapter](./putting_everything_together.md). -The best way to understand those improvements is to start exploring other areas of the docs, such as the chapters on [capabilities-based security](../system/process/capabilities.md) and the [networking protocol](../system/networking_protocol.md), for error handling. - -This chapter will instead focus on how to *extend* an existing program with new functionality. -Chat is a basic feature for a chess program, but will touch the existing code in many places. -This will give you a good idea of how to extend your own programs. - -You need to alter at least 4 things about the program: -- The request-response types it can handle (i.e. the protocol itself) -- The incoming request handler for HTTP requests, to receive chats sent by `our` node -- The outgoing websocket update, to send received chats to the frontend -- The frontend, to display the chat - -Handling them in that order, first, look at the types used for request-response now: -```rust -#[derive(Debug, Serialize, Deserialize)] -enum ChessRequest { - NewGame { white: String, black: String }, - Move { game_id: String, move_str: String }, - Resign(String), -} - -#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] -enum ChessResponse { - NewGameAccepted, - NewGameRejected, - MoveAccepted, - MoveRejected, -} -``` - -These types need to be exhaustive, since incoming messages will be fed into a `match` statement that uses `ChessRequest` and `ChessResponse`. -For more complex apps, one could introduce a new type that serves as an umbrella over multiple "kinds" of message, but since a simple chat will only be a few extra entries into the existing types, it's unnecessary for this example. - -In order to add chat, the request type above will need a new variant, something like `Message(String)`. -It doesn't need a `from` field, since that's just the `source` of the message! - -A new response type will make the chat more robust, by acknowledging received messages. -Something like `MessageAck` will do, with no fields — since this will be sent in response to a `Message` request, the sender will know which message it's acknowledging. - -The new types will look like this: -```rust -#[derive(Debug, Serialize, Deserialize)] -enum ChessRequest { - NewGame { white: String, black: String }, - Move { game_id: String, move_str: String }, - Resign(String), - Message(String), -} - -#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] -enum ChessResponse { - NewGameAccepted, - NewGameRejected, - MoveAccepted, - MoveRejected, - MessageAck, -} -``` - -If you are modifying these types inside the finished chess app from this tutorial, your IDE should indicate that there are a few errors now: these new message types are not handled in their respective `match` statements. -Those errors, in `handle_chess_request` and `handle_local_request`, are where you'll need logic to handle messages other nodes send to this node, and messages this node sends to others, respectively. - -In `handle_chess_request`, the app receives requests from other nodes. -A reasonable way to handle incoming messages is to add them to a vector of messages that's saved for each active game. -The frontend could reflect this by adding a chat box next to each game, and displaying all messages sent over that game's duration. - -To do that, the `Game` struct must be altered to hold such a vector. - -```rust -struct Game { - pub id: String, // the node with whom we are playing - pub turns: u64, - pub board: String, - pub white: String, - pub black: String, - pub ended: bool, - /// messages stored in order as (sender, content) - pub messages: Vec<(String, String)>, -} -``` - -Then in the main switch statement in `handle_chess_request`: -```rust -... -ChessRequest::Message(content) => { - // Earlier in this code, we define game_id as the source node. - let Some(game) = state.games.get_mut(game_id) else { - return Err(anyhow::anyhow!("no game with {game_id}")); - }; - game.messages.push((game_id.to_string(), content.to_string())); - Ok(()) -} -... -``` - -In `handle_local_request`, the app sends requests to other nodes. -Note, however, that requests to message `our`self don't really make sense — what should really happen is that the chess frontend performs a PUT request, or sends a message over a websocket, and the chess backend process turns that into a message request to the other player. -So instead of handling `Message` requests in `handle_local_request`, the process should reject or ignore them: - -```rust -ChessRequest::Message(_) => { - Ok(()) -} -``` - -Instead, the chess backend will handle a new kind of PUT request in `handle_http_request`, such that the local frontend can be used to send messages in games being played. - -This is the current (super gross!!) code for handling PUT requests in `handle_http_request`: -```rust -// on PUT: make a move -"PUT" => { - let Some(blob) = get_blob() else { - return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]); - }; - let blob_json = serde_json::from_slice::(&blob.bytes)?; - let Some(game_id) = blob_json["id"].as_str() else { - return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]); - }; - let Some(game) = state.games.get_mut(game_id) else { - return http::send_response(http::StatusCode::NOT_FOUND, None, vec![]); - }; - if (game.turns % 2 == 0 && game.white != our.node) - || (game.turns % 2 == 1 && game.black != our.node) - { - return http::send_response(http::StatusCode::FORBIDDEN, None, vec![]); - } else if game.ended { - return http::send_response(http::StatusCode::CONFLICT, None, vec![]); - } - let Some(move_str) = blob_json["move"].as_str() else { - return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]); - }; - let mut board = Board::from_fen(&game.board).unwrap(); - if !board.apply_uci_move(move_str) { - // reader note: can surface illegal move to player or something here - return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]); - } - // send the move to the other player - // check if the game is over - // if so, update the records - let Ok(msg) = Request::new() - .target((game_id, our.process.clone())) - .body(serde_json::to_vec(&ChessRequest::Move { - game_id: game_id.to_string(), - move_str: move_str.to_string(), - })?) - .send_and_await_response(5)? - else { - return Err(anyhow::anyhow!( - "other player did not respond properly to our move" - )); - }; - if serde_json::from_slice::(msg.body())? != ChessResponse::MoveAccepted { - return Err(anyhow::anyhow!("other player rejected our move")); - } - // update the game - game.turns += 1; - if board.checkmate() || board.stalemate() { - game.ended = true; - } - game.board = board.fen(); - // update state and return to FE - let body = serde_json::to_vec(&game)?; - save_chess_state(&state); - // return the game - http::send_response( - http::StatusCode::OK, - Some(HashMap::from([( - String::from("Content-Type"), - String::from("application/json"), - )])), - body, - ) -} -``` - -Let's modify this to handle more than just making moves. -Note that there's an implicit JSON structure enforced by the code above, where PUT requests from your frontend look like this: - -```json -{ - "id": "game_id", - "move": "e2e4" -} -``` - -An easy way to allow messages is to match on whether the key `"move"` is present, and if not, look for the key `"message"`. -This could also easily be codified as a Rust type and deserialized. - -Now, instead of assuming `"move"` exists, let's add a branch that handles the `"message"` case. -This is a modification of the code above: -```rust -// on PUT: make a move OR send a message -"PUT" => { - // ... same as the previous snippet ... - let Some(move_str) = blob_json["move"].as_str() else { - let Some(message) = blob_json["message"].as_str() else { - return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]); - }; - // handle sending message to another player - let Ok(_ack) = Request::new() - .target((game_id, our.process.clone())) - .body(serde_json::to_vec(&ChessRequest::Message(message.to_string()))?) - .send_and_await_response(5)? - else { - // Reader Note: handle a failed message send! - return Err(anyhow::anyhow!( - "other player did not respond properly to our message" - )); - }; - game.messages.push((our.node.clone(), message.to_string())); - let body = serde_json::to_vec(&game)?; - save_chess_state(&state); - // return the game - return http::send_response( - http::StatusCode::OK, - Some(HashMap::from([( - String::from("Content-Type"), - String::from("application/json"), - )])), - body, - ); - }; - // - // ... the rest of the move-handling code, same as previous snippet ... - // -} -``` - -That's it. -A simple demonstration of how to extend the functionality of a given process. -There are a few key things to keep in mind when doing this, if you want to build stable, maintainable, upgradable applications: - -- By adding chat, you changed the format of the "chess protocol" implicitly declared by this program. -If a user is running the old code, their version won't know how to handle the new `Message` request type you added. -**Depending on the serialization/deserialization strategy used, this might even create incompatibilities with the other types of requests.** -This is a good reason to use a serialization strategy that allows for "unknown" fields, such as JSON. -If you're using a binary format, you'll need to be more careful about how you add new fields to existing types. - -- It's *okay* to break backwards compatibility with old versions of an app, but once a protocol is established, it's best to stick to it or start a new project. -Backwards compatibility can always be achieved by adding a version number to the request/response type(s) directly. -That's a simple way to know which version of the protocol is being used and handle it accordingly. - -- By adding a `messages` field to the `Game` struct, you changed the format of the state that gets persisted. -If a user was running the previous version of this process, and upgrades to this version, the old state will fail to properly deserialize. -If you are building an upgrade to an existing app, you should always test that the new version can appropriately handle old state. -If you have many versions, you might need to make sure that state types from *any* old version can be handled. -Again, inserting a version number that can be deserialized from persisted state is a useful strategy. -The best way to do this depends on the serialization strategy used. diff --git a/src/chess_app/chess_app.md b/src/chess_app/chess_app.md deleted file mode 100644 index d06a037e..00000000 --- a/src/chess_app/chess_app.md +++ /dev/null @@ -1,4 +0,0 @@ -# In-Depth Guide: Chess App - -This guide will walk you through building a very simple chess app on Kinode. -The final result will look like [this](https://github.com/kinode-dao/kinode/tree/main/kinode/packages/chess): chess is in the basic runtime distribution so you can try it yourself. diff --git a/src/chess_app/chess_engine.md b/src/chess_app/chess_engine.md deleted file mode 100644 index 22cf9e4d..00000000 --- a/src/chess_app/chess_engine.md +++ /dev/null @@ -1,522 +0,0 @@ -# Chess Engine - -Chess is a good example for a Kinode application walk-through because: -1. The basic game logic is already readily available. - There are dozens of high-quality chess libraries across many languages that can be imported into a Wasm app that runs on Kinode. - We'll be using [pleco](https://github.com/pleco-rs/Pleco). -2. It's a multiplayer game, showing Kinode's p2p communications and ability to serve frontends -3. It's fun! - -In `my-chess/Cargo.toml`, which should be in the `my-chess/` process directory inside the `my-chess/` package directory, add `pleco = "0.5"` to your dependencies. -In your `my-chess/src/lib.rs`, replace the existing code with: - -```rust -use pleco::Board; -use kinode_process_lib::{await_message, call_init, println, Address}; - -wit_bindgen::generate!({ - path: "target/wit", - world: "process-v0", -}); - -call_init!(init); -fn init(our: Address) { - println!("started"); - - let my-chess_board = Board::start_pos().fen(); - - println!("my-chess_board: {my-chess_board}"); - - loop { - // Call await_message() to receive any incoming messages. - await_message().map(|message| { - if !message.is_request() { continue }; - println!( - "{our}: got request from {}: {}", - message.source(), - String::from_utf8_lossy(message.body()) - ); - }); - } -} -``` - -Now, you have access to a chess board and can manipulate it easily. - -The [pleco docs](https://github.com/pleco-rs/Pleco#using-pleco-as-a-library) show everything you can do using the pleco library. -But this isn't very interesting by itself! -Chess is a multiplayer game. -To make your app multiplayer, start by creating a persisted state for the chess app and a `body` format for sending messages to other nodes. - -The first step to creating a multiplayer or otherwise networked project is adjusting your `manifest.json` to specify what [capabilities](../system/process/capabilities.md) your process will grant. - -Go to `my-chess/manifest.json` and make sure your chess process is public and gets network access: -```json -[ - { - "process_name": "my-chess", - "process_wasm_path": "/my-chess.wasm", - "on_exit": "Restart", - "request_networking": true, - "request_capabilities": [], - "grant_capabilities": [], - "public": true - } -] -``` - -Now, in `my-chess/src/lib.rs` add the following simple Request/Response interface and persistable game state: -```rust -use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; - -#[derive(Debug, Serialize, Deserialize)] -enum ChessRequest { - NewGame { white: String, black: String }, - Move { game_id: String, move_str: String }, - Resign(String), -} - -#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] -enum ChessResponse { - NewGameAccepted, - NewGameRejected, - MoveAccepted, - MoveRejected, -} - -/// -/// Our serializable state format. -/// -#[derive(Debug, Serialize, Deserialize)] -struct ChessState { - pub games: HashMap, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -struct Game { - /// the node with whom we are playing - pub id: String, - pub turns: u64, - /// a string representation of the board using FEN - pub board: String, - /// the white player's node id - pub white: String, - /// the black player's node id - pub black: String, - pub ended: bool, -} -``` - -Creating explicit `ChessRequest` and `ChessResponse` types is the easiest way to reliably communicate between two processes. -It makes message-passing very simple. -If you get a request, you can deserialize it to `ChessRequest` and ignore or throw an error if that fails. -If you get a response, you can do the same but with `ChessResponse`. -And every request and response that you send can be serialized in kind. -More advanced apps can take on different structures, but a top-level `enum` to serialize/deserialize and match on is usually a good idea. - -The `ChessState` `struct` shown above can also be persisted using the `set_state` and `get_state` commands exposed by Kinode's runtime. -Note that the `Game` `struct` here has `board` as a `String`. -This is because the `Board` type from pleco doesn't implement `Serialize` or `Deserialize`. -We'll have to convert it to a string using `fen()` before persisting it. -Then, you will convert it back to a `Board` with `Board::from_fen()` when you load it from state. - -The code below will contain a version of the `init()` function that creates an event loop and handles ChessRequests. -First, however, it's important to note that these types already bake in some assumptions about our "chess protocol". -Remember, requests can either expect a response, or be fired and forgotten. -Unless a response is expected, there's no way to know if a request was received or not. -In a game like chess, most actions have a logical response. -Otherwise, there's no way to easily alert the user that their counterparty has gone offline, or started to otherwise ignore our moves. -For the sake of the tutorial, there are three kinds of requests and only two expect a response. -In our code, the `NewGame` and `Move` requests will always await a response, blocking until they receive one (or the request times out). -`Resign`, however, will be fire-and-forget. -While a "real" game may prefer to wait for a response, it is important to let one player resign and thus clear their state *without* that resignation being "accepted" by a non-responsive player, so production-grade resignation logic is non-trivial. - -> An aside: when building consumer-grade peer-to-peer apps, you'll find that there are in fact very few "trivial" interaction patterns. -> Something as simple as resigning from a one-on-one game, which would be a single POST request in a client-frontend <> server-backend architecture, requires well-thought-out negotiations to ensure that both players can walk away with a clean state machine, regardless of whether the counterparty is cooperating. -> Adding more "players" to the mix makes this even more complex. -> To keep things clean, leverage the request/response pattern and the `context` field to store information about how to handle a given response, if you're not awaiting it in a blocking fashion. - -Below, you'll find the full code for the CLI version of the app. -You can build it and install it on a node using `kit`. -You can interact with it in the terminal, primitively, like so (assuming your first node is `fake.os` and second is `fake2.os`): -``` -m our@my-chess:my-chess:template.os '{"NewGame": {"white": "fake.os", "black": "fake2.os"}}' -m our@my-chess:my-chess:template.os '{"Move": {"game_id": "fake2.os", "move_str": "e2e4"}}' -``` -(If you want to make a more ergonomic CLI app, consider parsing `body` as a string, or better yet, writing [terminal scripts](../cookbook/writing_scripts.md) for various game actions.) - -As you read through the code, you might notice a problem with this app: there's no way to see your games! -A fun project would be to add a CLI command that shows you, in-terminal, the board for a given `game_id`. -But in the [next chapter](./frontend.md), we'll add a frontend to this app so you can see your games in a browser. - -`my-chess/Cargo.toml`: -```toml -[package] -name = "my-chess" -version = "0.1.0" -edition = "2021" - -[profile.release] -panic = "abort" -opt-level = "s" -lto = true - -[dependencies] -anyhow = "1.0" -bincode = "1.3.3" -kinode_process_lib = "0.9.0" -pleco = "0.5" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -wit-bindgen = "0.24.0" - -[lib] -crate-type = ["cdylib"] - -[package.metadata.component] -package = "kinode:process" -``` - -`my-chess/src/lib.rs`: -```rust -use kinode_process_lib::{ - await_message, call_init, get_typed_state, println, set_state, Address, Message, NodeId, - Request, Response, -}; -use pleco::Board; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -// Boilerplate: generate the Wasm bindings for a Kinode app -wit_bindgen::generate!({ - path: "target/wit", - world: "process-v0", -}); - -// -// Our "chess protocol" request/response format. We'll always serialize these -// to a byte vector and send them over `body`. -// - -#[derive(Debug, Serialize, Deserialize)] -enum ChessRequest { - NewGame { white: String, black: String }, - Move { game_id: String, move_str: String }, - Resign(String), -} - -#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] -enum ChessResponse { - NewGameAccepted, - NewGameRejected, - MoveAccepted, - MoveRejected, -} - -/// -/// Our serializable state format. -/// -#[derive(Debug, Serialize, Deserialize)] -struct ChessState { - pub games: HashMap, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -struct Game { - /// the node with whom we are playing - pub id: String, - pub turns: u64, - /// a string representation of the board using FEN - pub board: String, - /// the white player's node id - pub white: String, - /// the black player's node id - pub black: String, - pub ended: bool, -} - -/// Helper function to serialize and save the process state. -fn save_chess_state(state: &ChessState) { - set_state(&bincode::serialize(&state.games).unwrap()); -} - -/// Helper function to deserialize the process state. Note that we use a helper function -/// from process_lib to fetch a typed state, which will return None if the state does -/// not exist OR fails to deserialize. In either case, we'll make an empty new state. -fn load_chess_state() -> ChessState { - match get_typed_state(|bytes| bincode::deserialize::>(bytes)) { - Some(games) => ChessState { games }, - None => ChessState { - games: HashMap::new(), - }, - } -} - -call_init!(init); -fn init(our: Address) { - // A little printout to show in terminal that the process has started. - println!("started"); - - // Grab our state, then enter the main event loop. - let mut state: ChessState = load_chess_state(); - main_loop(&our, &mut state); -} - -fn main_loop(our: &Address, state: &mut ChessState) { - loop { - // Call await_message() to receive any incoming messages. - // If we get a network error, make a print and throw it away. - // In a high-quality consumer-grade app, we'd want to explicitly handle - // this and surface it to the user. - match await_message() { - Err(send_error) => { - println!("got network error: {send_error:?}"); - continue; - } - Ok(message) => { - if let Err(e) = handle_request(&our, &message, state) { - println!("error while handling request: {e:?}"); - } - } - } - } -} - -/// Handle chess protocol messages from ourself *or* other nodes. -fn handle_request(our: &Address, message: &Message, state: &mut ChessState) -> anyhow::Result<()> { - // Throw away responses. We never expect any responses *here*, because for every - // chess protocol request, we *await* its response in-place. This is appropriate - // for direct node<>node comms, less appropriate for other circumstances... - if !message.is_request() { - return Err(anyhow::anyhow!("message was response")); - } - // If the request is from another node, handle it as an incoming request. - // Note that we can enforce the ProcessId as well, but it shouldn't be a trusted - // piece of information, since another node can easily spoof any ProcessId on a request. - // It can still be useful simply as a protocol-level switch to handle different kinds of - // requests from the same node, with the knowledge that the remote node can finagle with - // which ProcessId a given message can be from. It's their code, after all. - if message.source().node != our.node { - // Deserialize the request `body` to our format, and throw it away if it - // doesn't fit. - let Ok(chess_request) = serde_json::from_slice::(message.body()) else { - return Err(anyhow::anyhow!("invalid chess request")); - }; - handle_chess_request(&message.source().node, state, &chess_request) - } - // ...and if the request is from ourselves, handle it as our own! - // Note that since this is a local request, we *can* trust the ProcessId. - else { - // Here, we accept messages *from any local process that can message this one*. - // Since the manifest.json specifies that this process is *public*, any local process - // can "play chess" for us. - // - // If you wanted to restrict this privilege, you could check for a specific process, - // package, and/or publisher here, *or* change the manifest to only grant messaging - // capabilities to specific processes. - let Ok(chess_request) = serde_json::from_slice::(message.body()) else { - return Err(anyhow::anyhow!("invalid chess request")); - }; - handle_local_request(our, state, &chess_request) - } -} - -/// handle chess protocol messages from other nodes -fn handle_chess_request( - source_node: &NodeId, - state: &mut ChessState, - action: &ChessRequest, -) -> anyhow::Result<()> { - println!("handling action from {source_node}: {action:?}"); - - // For simplicity's sake, we'll just use the node we're playing with as the game id. - // This limits us to one active game per partner. - let game_id = source_node; - - match action { - ChessRequest::NewGame { white, black } => { - // Make a new game with source.node - // This will replace any existing game with source.node! - if state.games.contains_key(game_id) { - println!("resetting game with {game_id} on their request!"); - } - let game = Game { - id: game_id.to_string(), - turns: 0, - board: Board::start_pos().fen(), - white: white.to_string(), - black: black.to_string(), - ended: false, - }; - // Use our helper function to persist state after every action. - // The simplest and most trivial way to keep state. You'll want to - // use a database or something in a real app, and consider performance - // when doing intensive data-based operations. - state.games.insert(game_id.to_string(), game); - save_chess_state(&state); - // Send a response to tell them we've accepted the game. - // Remember, the other player is waiting for this. - Response::new() - .body(serde_json::to_vec(&ChessResponse::NewGameAccepted)?) - .send()?; - Ok(()) - } - ChessRequest::Move { move_str, .. } => { - // note: ignore their game_id, just use their node ID so they can't spoof it - // Get the associated game and respond with an error if - // we don't have it in our state. - let Some(game) = state.games.get_mut(game_id) else { - // If we don't have a game with them, reject the move. - Response::new() - .body(serde_json::to_vec(&ChessResponse::MoveRejected)?) - .send()?; - return Ok(()); - }; - // Convert the saved board to one we can manipulate. - let mut board = Board::from_fen(&game.board).unwrap(); - if !board.apply_uci_move(move_str) { - // Reject invalid moves! - Response::new() - .body(serde_json::to_vec(&ChessResponse::MoveRejected)?) - .send()?; - return Ok(()); - } - game.turns += 1; - if board.checkmate() || board.stalemate() { - game.ended = true; - } - // Persist state. - game.board = board.fen(); - save_chess_state(&state); - // Send a response to tell them we've accepted the move. - Response::new() - .body(serde_json::to_vec(&ChessResponse::MoveAccepted)?) - .send()?; - Ok(()) - } - ChessRequest::Resign(_) => { - // They've resigned. The sender isn't waiting for a response to this, - // so we don't need to send one. - if let Some(game) = state.games.get_mut(game_id) { - game.ended = true; - save_chess_state(&state); - } - Ok(()) - } - } -} - -/// Handle actions we are performing. Here's where we'll send_and_await various requests. -/// -/// Each send_and_await here just uses a 5-second timeout. Note that this isn't waiting -/// for the other *human* player to respond, but for the other *process* to respond. -/// Carefully consider your timeout strategy -- sometimes it makes sense to automatically -/// retry, but other times you'll want to surface the error to the user. -fn handle_local_request( - our: &Address, - state: &mut ChessState, - action: &ChessRequest, -) -> anyhow::Result<()> { - match action { - ChessRequest::NewGame { white, black } => { - // Create a new game. We'll enforce that one of the two players is us. - if white != &our.node && black != &our.node { - return Err(anyhow::anyhow!("cannot start a game without us!")); - } - let game_id = if white == &our.node { black } else { white }; - // If we already have a game with this player, throw an error. - if let Some(game) = state.games.get(game_id) { - if !game.ended { - return Err(anyhow::anyhow!("already have a game with {game_id}")); - } - } - // Send the other player a NewGame request - // The request is exactly the same as what we got from terminal. - // We'll give them 5 seconds to respond... - let Ok(Message::Response { ref body, .. }) = - Request::to((game_id, our.process.clone())) - .body(serde_json::to_vec(&action)?) - .send_and_await_response(5)? - else { - return Err(anyhow::anyhow!( - "other player did not respond properly to new game request" - )); - }; - // If they accept, create a new game — otherwise, error out. - if serde_json::from_slice::(body)? != ChessResponse::NewGameAccepted { - return Err(anyhow::anyhow!("other player rejected new game request!")); - } - // New game with default board. - let game = Game { - id: game_id.to_string(), - turns: 0, - board: Board::start_pos().fen(), - white: white.to_string(), - black: black.to_string(), - ended: false, - }; - state.games.insert(game_id.to_string(), game); - save_chess_state(&state); - Ok(()) - } - ChessRequest::Move { game_id, move_str } => { - // Make a move. We'll enforce that it's our turn. The game_id is the - // person we're playing with. - let Some(game) = state.games.get_mut(game_id) else { - return Err(anyhow::anyhow!("no game with {game_id}")); - }; - if (game.turns % 2 == 0 && game.white != our.node) - || (game.turns % 2 == 1 && game.black != our.node) - { - return Err(anyhow::anyhow!("not our turn!")); - } else if game.ended { - return Err(anyhow::anyhow!("that game is over!")); - } - let mut board = Board::from_fen(&game.board).unwrap(); - if !board.apply_uci_move(move_str) { - return Err(anyhow::anyhow!("illegal move!")); - } - // Send the move to the other player, then check if the game is over. - // The request is exactly the same as what we got from terminal. - // We'll give them 5 seconds to respond... - let Ok(Message::Response { ref body, .. }) = - Request::to((game_id, our.process.clone())) - .body(serde_json::to_vec(&action)?) - .send_and_await_response(5)? - else { - return Err(anyhow::anyhow!( - "other player did not respond properly to our move" - )); - }; - if serde_json::from_slice::(body)? != ChessResponse::MoveAccepted { - return Err(anyhow::anyhow!("other player rejected our move")); - } - game.turns += 1; - if board.checkmate() || board.stalemate() { - game.ended = true; - } - game.board = board.fen(); - save_chess_state(&state); - Ok(()) - } - ChessRequest::Resign(ref with_who) => { - // Resign from a game with a given player. - let Some(game) = state.games.get_mut(with_who) else { - return Err(anyhow::anyhow!("no game with {with_who}")); - }; - // send the other player an end game request — no response expected - Request::to((with_who, our.process.clone())) - .body(serde_json::to_vec(&action)?) - .send()?; - game.ended = true; - save_chess_state(&state); - Ok(()) - } - } -} -``` - -That's it! You now have a fully peer-to-peer chess game that can be played (awkwardly) through your Kinode terminal. - -In the [next chapter](./frontend.md), we'll add a frontend to this app so you can play it more easily. \ No newline at end of file diff --git a/src/chess_app/chess_home.png b/src/chess_app/chess_home.png deleted file mode 100644 index 0305606b968b01bb4deb609aa9f09b576a3efef3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 925870 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