From b56841596e805328aec273a54c1c17048bb2ed73 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 2 Nov 2026 18:21:30 +0100 Subject: [PATCH] =?UTF-8?q?Delete=20the=20Python,=20Rust=20and=20C=20bindi?= =?UTF-8?q?ngs,=20the=20Go=20example=20and=20the=20TypeScript=20seal=20and?= =?UTF-8?q?=20contracts=20exports,=20which=20nothing=20imports,=20in=20the?= =?UTF-8?q?=20release's=20one=20mesh-sdk=20change=20(ADR=200593=20=C2=A712?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- c/.gitignore | 1 - c/Makefile | 8 -- c/README.md | 9 -- c/example.c | 27 ------ c/mesh_sdk.c | 83 ---------------- c/mesh_sdk.h | 29 ------ go/README.md | 2 +- go/example/main.go | 29 ------ package-lock.json | 4 +- package-version.sum | 2 +- package.json | 6 +- python/README.md | 8 -- python/example.py | 13 --- python/mesh_sdk/__init__.py | 77 --------------- .../__pycache__/__init__.cpython-314.pyc | Bin 5871 -> 0 bytes python/pyproject.toml | 9 -- rust/.gitignore | 2 - rust/Cargo.toml | 8 -- rust/README.md | 8 -- rust/examples/greet.rs | 19 ---- rust/src/lib.rs | 80 ---------------- src/index.ts | 1 - src/seal/box.ts | 90 ------------------ src/seal/index.ts | 29 ------ test/seal.test.ts | 71 -------------- 25 files changed, 6 insertions(+), 609 deletions(-) delete mode 100644 c/.gitignore delete mode 100644 c/Makefile delete mode 100644 c/README.md delete mode 100644 c/example.c delete mode 100644 c/mesh_sdk.c delete mode 100644 c/mesh_sdk.h delete mode 100644 go/example/main.go delete mode 100644 python/README.md delete mode 100755 python/example.py delete mode 100644 python/mesh_sdk/__init__.py delete mode 100644 python/mesh_sdk/__pycache__/__init__.cpython-314.pyc delete mode 100644 python/pyproject.toml delete mode 100644 rust/.gitignore delete mode 100644 rust/Cargo.toml delete mode 100644 rust/README.md delete mode 100644 rust/examples/greet.rs delete mode 100644 rust/src/lib.rs delete mode 100644 src/seal/box.ts delete mode 100644 src/seal/index.ts delete mode 100644 test/seal.test.ts diff --git a/c/.gitignore b/c/.gitignore deleted file mode 100644 index 33a9488..0000000 --- a/c/.gitignore +++ /dev/null @@ -1 +0,0 @@ -example diff --git a/c/Makefile b/c/Makefile deleted file mode 100644 index acd63f1..0000000 --- a/c/Makefile +++ /dev/null @@ -1,8 +0,0 @@ -CFLAGS ?= -O2 -Wall -Wextra -JANSSON := $(shell pkg-config --cflags --libs jansson) - -example: example.c mesh_sdk.c mesh_sdk.h - $(CC) $(CFLAGS) -o $@ example.c mesh_sdk.c $(JANSSON) - -clean: - rm -f example diff --git a/c/README.md b/c/README.md deleted file mode 100644 index bad0829..0000000 --- a/c/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# mesh-sdk for C - -The skeleton (novox/hq ADR 0188): `mesh_serve(name, tools, count)` speaks MCP over stdio to the -node's tool runtime. A `mesh_tool` is a name, a description, an argument schema and a function -returning JSON. A tool named `.` is the seat's implementation. JSON is jansson's, the -one dependency. - -What it holds, under ADR 0039's test: the stdio loop and the tool type. Nothing else — no bus, no -client, nothing of any module's. `make` and pipe a `tools/list` request into `./example`. diff --git a/c/example.c b/c/example.c deleted file mode 100644 index 49a5773..0000000 --- a/c/example.c +++ /dev/null @@ -1,27 +0,0 @@ -/* A tools bundle in C: one tool of its own and one seat verb, served over stdio. */ -#include "mesh_sdk.h" -#include - -static json_t *greet(json_t *args, char **error) { - (void)error; - const char *who = json_string_value(json_object_get(args, "who")); - char greeting[128]; - snprintf(greeting, sizeof greeting, "hello %s", who ? who : "world"); - return json_pack("{s:s,s:s}", "greeting", greeting, "language", "c"); -} - -static json_t *on(json_t *args, char **error) { - (void)args; (void)error; - return json_pack("{s:b,s:s}", "on", 1, "language", "c"); -} - -int main(void) { - json_t *who = json_pack("{s:{s:s,s:s}}", "who", "type", "string", "description", "whom"); - const mesh_tool tools[] = { - {"greet", "say hello", who, greet}, - {"node-lamp.on", "the seat's verb", NULL, on}, - }; - int rc = mesh_serve("c-example", tools, 2); - json_decref(who); - return rc; -} diff --git a/c/mesh_sdk.c b/c/mesh_sdk.c deleted file mode 100644 index 7f3e000..0000000 --- a/c/mesh_sdk.c +++ /dev/null @@ -1,83 +0,0 @@ -#include "mesh_sdk.h" -#include -#include -#include - -static void say(json_t *m) { - json_object_set_new(m, "jsonrpc", json_string("2.0")); - char *text = json_dumps(m, JSON_COMPACT); - fprintf(stdout, "%s\n", text); - fflush(stdout); - free(text); - json_decref(m); -} - -static json_t *as_schema(json_t *given) { - if (!given || !json_is_object(given) || json_object_size(given) == 0) - return json_pack("{s:s,s:{}}", "type", "object", "properties"); - if (json_object_get(given, "type") || json_object_get(given, "properties")) - return json_incref(given); - return json_pack("{s:s,s:O}", "type", "object", "properties", given); -} - -int mesh_serve(const char *name, const mesh_tool *tools, size_t count) { - char *line = NULL; - size_t cap = 0; - ssize_t got; - while ((got = getline(&line, &cap, stdin)) >= 0) { - json_error_t err; - json_t *req = json_loads(line, 0, &err); - if (!req) continue; - json_t *id = json_object_get(req, "id"); - const char *method = json_string_value(json_object_get(req, "method")); - json_t *params = json_object_get(req, "params"); - if (!method) { json_decref(req); continue; } - if (strcmp(method, "initialize") == 0) { - say(json_pack("{s:O,s:{s:s,s:{s:{}},s:{s:s,s:s}}}", "id", id ? id : json_null(), "result", - "protocolVersion", MESH_SDK_PROTOCOL, "capabilities", "tools", - "serverInfo", "name", name, "version", "1")); - } else if (strcmp(method, "notifications/initialized") == 0) { - } else if (strcmp(method, "ping") == 0) { - if (id) say(json_pack("{s:O,s:{}}", "id", id, "result")); - } else if (strcmp(method, "tools/list") == 0) { - json_t *listed = json_array(); - for (size_t i = 0; i < count; i++) - json_array_append_new(listed, json_pack("{s:s,s:s,s:o}", "name", tools[i].name, - "description", tools[i].description, "inputSchema", as_schema(tools[i].input))); - say(json_pack("{s:O,s:{s:o}}", "id", id ? id : json_null(), "result", "tools", listed)); - } else if (strcmp(method, "tools/call") == 0) { - const char *wanted = params ? json_string_value(json_object_get(params, "name")) : NULL; - const mesh_tool *tool = NULL; - for (size_t i = 0; wanted && i < count; i++) - if (strcmp(tools[i].name, wanted) == 0) tool = &tools[i]; - if (!tool) { - char message[256]; - snprintf(message, sizeof message, "%s has no tool %s", name, wanted ? wanted : ""); - say(json_pack("{s:O,s:{s:i,s:s}}", "id", id ? id : json_null(), "error", "code", -32602, "message", message)); - } else { - json_t *args = params ? json_object_get(params, "arguments") : NULL; - json_t *empty = args ? NULL : json_object(); - char *error = NULL; - json_t *result = tool->run(args ? args : empty, &error); - if (empty) json_decref(empty); - if (result) { - char *text = json_dumps(result, JSON_COMPACT); - say(json_pack("{s:O,s:{s:[{s:s,s:s}]}}", "id", id ? id : json_null(), "result", "content", "type", "text", "text", text)); - free(text); - json_decref(result); - } else { - say(json_pack("{s:O,s:{s:[{s:s,s:s}],s:b}}", "id", id ? id : json_null(), "result", "content", - "type", "text", "text", error ? error : "the tool failed", "isError", 1)); - free(error); - } - } - } else if (id) { - char message[256]; - snprintf(message, sizeof message, "no %s", method); - say(json_pack("{s:O,s:{s:i,s:s}}", "id", id, "error", "code", -32601, "message", message)); - } - json_decref(req); - } - free(line); - return ferror(stdin) ? -1 : 0; -} diff --git a/c/mesh_sdk.h b/c/mesh_sdk.h deleted file mode 100644 index 47f92ef..0000000 --- a/c/mesh_sdk.h +++ /dev/null @@ -1,29 +0,0 @@ -/* The Novox Mesh SDK for C — the skeleton (novox/hq ADR 0188). - * - * A tools bundle is a process the node's runtime launches and speaks MCP over stdio to: - * `initialize`, `tools/list` once, `tools/call` per call, newline-framed JSON-RPC on stdin and - * stdout. This is the loop, so a bundle writes its tools and nothing else. A tool named - * `.` is the module's implementation of that seat's verb; any other name is the - * module's own. JSON is jansson's. - * - * No transport here: the bus, the subjects and the memberships are the runtime's (ADR 0039, 0175). - * stdout is the protocol; log to stderr. */ -#ifndef MESH_SDK_H -#define MESH_SDK_H -#include - -#define MESH_SDK_PROTOCOL "2025-03-26" - -/* One tool. `run` returns a new JSON value (the answer) or NULL with *error set to a message the - * caller frees. `input` is the schema of the arguments, or NULL for a tool that takes nothing. */ -typedef struct { - const char *name; - const char *description; - json_t *input; - json_t *(*run)(json_t *args, char **error); -} mesh_tool; - -/* Serve these tools on stdin/stdout until stdin closes. Returns 0, or -1 on an I/O fault. */ -int mesh_serve(const char *name, const mesh_tool *tools, size_t count); - -#endif diff --git a/go/README.md b/go/README.md index 58dcfa4..f554fca 100644 --- a/go/README.md +++ b/go/README.md @@ -16,6 +16,6 @@ at the same version as the TypeScript package. What it holds, under ADR 0039's test: the stdio loop and the tool type, and `cli`, the local protocol `mesh-cli` speaks to the node-engine on its machine (novox/hq ADR 0272): the socket's path and one request and one -answer; and `seal`, the mesh's sealed box (novox/hq ADR 0206: X25519, HKDF-SHA256, AES-256-GCM, `{v, eph, iv, tag, ct}`), byte for byte the box the licence manager made before it lived here and the TypeScript `@novox/mesh-sdk/seal` makes. Nothing else — no bus, no client, nothing of any module's. `go build ./example` and pipe a `tools/list` request into it. +answer; and `seal`, the mesh's sealed box (novox/hq ADR 0206: X25519, HKDF-SHA256, AES-256-GCM, `{v, eph, iv, tag, ct}`), byte for byte the box the licence manager made before it lived here. Nothing else — no bus, no client, nothing of any module's. From release 4a of design 47 (novox/hq ADR 0453 clause 3, ADR 0446 clause 4) it also holds the composition of a machine's declaration, over a plain input and with nothing behind it: `declaration` and `validate`, the node-engine's parser and its validator, copied from mesh-host; `catalogue`, the manifests, their resolution and rendering, and `overlay`, the private network's generator, copied from mesh-controller; `compose`, the controller's planFor, renderingFor and declarationWith as functions of an `Input` (the composition never allocates, makes, keeps or writes: what a send would do it names as foreseen or returns); `sdkversion`, which mesh-sdk version a program was built with; and, in `delivery`, the layouts of the keys the builder, the checker and the indexer write (the source fingerprint's new form, the builds, the verdicts and kept verdicts, the manifests by digest). None of them holds a bus, a store or a vault client, which `go list -deps` of `compose` shows. diff --git a/go/example/main.go b/go/example/main.go deleted file mode 100644 index 283854e..0000000 --- a/go/example/main.go +++ /dev/null @@ -1,29 +0,0 @@ -// A tools bundle in Go: one tool of its own and one seat verb, served over stdio. -package main - -import ( - "fmt" - "os" - - stdio "git.novox.be/novox/mesh-sdk/go" -) - -func main() { - err := stdio.Serve("go-example", []stdio.Tool{ - {Name: "greet", Description: "say hello", Input: map[string]any{"who": map[string]any{"type": "string", "description": "whom"}}, - Run: func(a map[string]any) (any, error) { - who, _ := a["who"].(string) - if who == "" { - who = "world" - } - return map[string]any{"greeting": "hello " + who, "language": "go"}, nil - }}, - {Name: "node-lamp.on", Description: "the seat's verb", Run: func(map[string]any) (any, error) { - return map[string]any{"on": true, "language": "go"}, nil - }}, - }) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} diff --git a/package-lock.json b/package-lock.json index acf4da6..4110505 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@novox/mesh-sdk", - "version": "0.1.17", + "version": "0.1.18", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@novox/mesh-sdk", - "version": "0.1.17", + "version": "0.1.18", "devDependencies": { "@types/node": "^22.0.0", "typescript": "^5.6.0" diff --git a/package-version.sum b/package-version.sum index 6925adf..c41176c 100644 --- a/package-version.sum +++ b/package-version.sum @@ -1 +1 @@ -0.1.17 90cca0bf1acfb386d66ccddb9f612703cf8f0205791b846982a524d0044aa0fb +0.1.18 5cbc2d043e45ab47eadff55a61f8be7d50144cec3cf295333b5ea1b4c181cfdf diff --git a/package.json b/package.json index cd0af54..8822ad8 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,10 @@ { "name": "@novox/mesh-sdk", - "version": "0.1.17", + "version": "0.1.18", "description": "The stable spine a Novox Mesh module's own code builds against.", "type": "module", "exports": { ".": "./dist/index.js", - "./contracts": "./dist/contracts/index.js", "./provider-loop": "./dist/provider-loop/index.js", "./provisioner": "./dist/provider-loop/index.js", "./tools": "./dist/tools/index.js", @@ -13,8 +12,7 @@ "./events": "./dist/events/index.js", "./primitives": "./dist/primitives/index.js", "./stdio": "./dist/stdio/index.js", - "./state": "./dist/state/index.js", - "./seal": "./dist/seal/index.js" + "./state": "./dist/state/index.js" }, "files": [ "dist" diff --git a/python/README.md b/python/README.md deleted file mode 100644 index 8bb2fac..0000000 --- a/python/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# mesh-sdk for Python - -The skeleton (novox/hq ADR 0188): `mesh_sdk.serve(name, tools)` speaks MCP over stdio to the node's -tool runtime. A `Tool` is a name, a description, an argument schema and a function. A tool named -`.` is the seat's implementation. No dependencies beyond the standard library. - -What it holds, under ADR 0039's test: the stdio loop and the tool type. Nothing else — no bus, no -client, nothing of any module's. Run `./example.py` and type a `tools/list` request to see it answer. diff --git a/python/example.py b/python/example.py deleted file mode 100755 index ab483c3..0000000 --- a/python/example.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python3 -# A tools bundle in Python: one tool of its own and one seat verb, served over stdio. -import os -import sys - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from mesh_sdk import Tool, serve # noqa: E402 - -serve("python-example", [ - Tool("greet", "say hello", lambda a: {"greeting": f"hello {a.get('who', 'world')}", "language": "python"}, - {"who": {"type": "string", "description": "whom"}}), - Tool("node-lamp.on", "the seat's verb", lambda a: {"on": True, "language": "python"}), -]) diff --git a/python/mesh_sdk/__init__.py b/python/mesh_sdk/__init__.py deleted file mode 100644 index ec44341..0000000 --- a/python/mesh_sdk/__init__.py +++ /dev/null @@ -1,77 +0,0 @@ -"""The Novox Mesh SDK for Python — the skeleton (novox/hq ADR 0188). - -A tools bundle is a process the node's runtime launches and speaks MCP over stdio to: `initialize`, -`tools/list` once, `tools/call` per call, newline-framed JSON-RPC on stdin and stdout. This module is -the loop, so a bundle writes its tools and nothing else. A tool named `.` is the -module's implementation of that seat's verb; any other name is the module's own. - -No transport here: the bus, the subjects and the memberships are the runtime's (ADR 0039, 0175). -stdout is the protocol; log to stderr. -""" -import json -import sys -from dataclasses import dataclass, field -from typing import Any, Callable, Dict, List - -PROTOCOL = "2025-03-26" - - -@dataclass -class Tool: - name: str - description: str - run: Callable[[Dict[str, Any]], Any] - # The JSON schema of the arguments, or the bare property map the catalogue's modules write. - input: Dict[str, Any] = field(default_factory=dict) - - -def as_schema(given: Dict[str, Any]) -> Dict[str, Any]: - if not given: - return {"type": "object", "properties": {}} - if given.get("type") == "object" or "properties" in given: - return given - return {"type": "object", "properties": given} - - -def serve(name: str, tools: List[Tool]) -> None: - """Serve these tools to the runtime over stdio until stdin closes.""" - by_name = {t.name: t for t in tools} - out = sys.stdout - - def say(message: Dict[str, Any]) -> None: - out.write(json.dumps(message) + "\n") - out.flush() - - for line in sys.stdin: - line = line.strip() - if not line: - continue - try: - request = json.loads(line) - except ValueError: - continue - rid = request.get("id") - method = request.get("method") - params = request.get("params") or {} - if method == "initialize": - say({"jsonrpc": "2.0", "id": rid, "result": {"protocolVersion": PROTOCOL, "capabilities": {"tools": {}}, "serverInfo": {"name": name, "version": "1"}}}) - elif method == "notifications/initialized": - pass - elif method == "ping": - if rid is not None: - say({"jsonrpc": "2.0", "id": rid, "result": {}}) - elif method == "tools/list": - say({"jsonrpc": "2.0", "id": rid, "result": {"tools": [ - {"name": t.name, "description": t.description, "inputSchema": as_schema(t.input)} for t in tools]}}) - elif method == "tools/call": - tool = by_name.get(str(params.get("name", ""))) - if tool is None: - say({"jsonrpc": "2.0", "id": rid, "error": {"code": -32602, "message": f"{name} has no tool {params.get('name')}"}}) - continue - try: - result = tool.run(params.get("arguments") or {}) - say({"jsonrpc": "2.0", "id": rid, "result": {"content": [{"type": "text", "text": json.dumps(result)}]}}) - except Exception as err: # the tool failed: a tool error, not a protocol one - say({"jsonrpc": "2.0", "id": rid, "result": {"content": [{"type": "text", "text": str(err)}], "isError": True}}) - elif rid is not None: - say({"jsonrpc": "2.0", "id": rid, "error": {"code": -32601, "message": f"no {method}"}}) diff --git a/python/mesh_sdk/__pycache__/__init__.cpython-314.pyc b/python/mesh_sdk/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index 0956f1f7e9fea1c818dac7f6dc731f5957d51843..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5871 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 BHr@aL diff --git a/python/pyproject.toml b/python/pyproject.toml deleted file mode 100644 index a815345..0000000 --- a/python/pyproject.toml +++ /dev/null @@ -1,9 +0,0 @@ -[project] -name = "novox-mesh-sdk" -version = "0.1.0" -description = "The Novox Mesh SDK for Python: a tools bundle over stdio (novox/hq ADR 0188)" -requires-python = ">=3.9" - -[build-system] -requires = ["setuptools"] -build-backend = "setuptools.build_meta" diff --git a/rust/.gitignore b/rust/.gitignore deleted file mode 100644 index 2c96eb1..0000000 --- a/rust/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -target/ -Cargo.lock diff --git a/rust/Cargo.toml b/rust/Cargo.toml deleted file mode 100644 index 1f13ffe..0000000 --- a/rust/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "mesh_sdk" -version = "0.1.0" -edition = "2021" -description = "The Novox Mesh SDK for Rust: a tools bundle over stdio (novox/hq ADR 0188)" - -[dependencies] -serde_json = "1" diff --git a/rust/README.md b/rust/README.md deleted file mode 100644 index 7e82995..0000000 --- a/rust/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# mesh-sdk for Rust - -The skeleton (novox/hq ADR 0188): `mesh_sdk::serve(name, &tools)` speaks MCP over stdio to the -node's tool runtime. A `Tool` is a name, a description, an argument schema and a function. A tool -named `.` is the seat's implementation. One dependency, `serde_json`. - -What it holds, under ADR 0039's test: the stdio loop and the tool type. Nothing else — no bus, no -client, nothing of any module's. `cargo build --example greet` and pipe a `tools/list` request in. diff --git a/rust/examples/greet.rs b/rust/examples/greet.rs deleted file mode 100644 index bc8d1a6..0000000 --- a/rust/examples/greet.rs +++ /dev/null @@ -1,19 +0,0 @@ -// A tools bundle in Rust: one tool of its own and one seat verb, served over stdio. -use mesh_sdk::{serve, Tool}; -use serde_json::{json, Map, Value}; - -fn greet(a: &Map) -> Result { - let who = a.get("who").and_then(Value::as_str).unwrap_or("world"); - Ok(json!({"greeting": format!("hello {}", who), "language": "rust"})) -} - -fn on(_: &Map) -> Result { - Ok(json!({"on": true, "language": "rust"})) -} - -fn main() -> std::io::Result<()> { - serve("rust-example", &[ - Tool { name: "greet", description: "say hello", input: json!({"who": {"type": "string", "description": "whom"}}), run: greet }, - Tool { name: "node-lamp.on", description: "the seat's verb", input: json!({}), run: on }, - ]) -} diff --git a/rust/src/lib.rs b/rust/src/lib.rs deleted file mode 100644 index f97b1b1..0000000 --- a/rust/src/lib.rs +++ /dev/null @@ -1,80 +0,0 @@ -//! The Novox Mesh SDK for Rust — the skeleton (novox/hq ADR 0188). -//! -//! A tools bundle is a process the node's runtime launches and speaks MCP over stdio to: -//! `initialize`, `tools/list` once, `tools/call` per call, newline-framed JSON-RPC on stdin and -//! stdout. This crate is the loop, so a bundle writes its tools and nothing else. A tool named -//! `.` is the module's implementation of that seat's verb; any other name is the -//! module's own. -//! -//! No transport here: the bus, the subjects and the memberships are the runtime's (ADR 0039, -//! 0175). stdout is the protocol; log to stderr. -use serde_json::{json, Map, Value}; -use std::io::{self, BufRead, Write}; - -/// The MCP version this speaks; the runtime says the same. -pub const PROTOCOL: &str = "2025-03-26"; - -/// One tool a bundle serves. `input` is the JSON schema of the arguments, or the bare property -/// map; `run` answers with any JSON or an error message. -pub struct Tool { - pub name: &'static str, - pub description: &'static str, - pub input: Value, - pub run: fn(&Map) -> Result, -} - -fn as_schema(given: &Value) -> Value { - match given.as_object() { - None => json!({"type": "object", "properties": {}}), - Some(m) if m.is_empty() => json!({"type": "object", "properties": {}}), - Some(m) if m.contains_key("type") || m.contains_key("properties") => given.clone(), - Some(_) => json!({"type": "object", "properties": given}), - } -} - -/// Serve these tools to the runtime over stdio until stdin closes. -pub fn serve(name: &str, tools: &[Tool]) -> io::Result<()> { - let stdin = io::stdin(); - let mut out = io::stdout(); - let mut say = |m: Value| -> io::Result<()> { - let mut m = m; - m["jsonrpc"] = json!("2.0"); - writeln!(out, "{}", m)?; - out.flush() - }; - for line in stdin.lock().lines() { - let line = line?; - let req: Value = match serde_json::from_str(line.trim()) { - Ok(v) => v, - Err(_) => continue, - }; - let id = req.get("id").cloned().unwrap_or(Value::Null); - let method = req.get("method").and_then(Value::as_str).unwrap_or(""); - let params = req.get("params").cloned().unwrap_or(json!({})); - match method { - "initialize" => say(json!({"id": id, "result": {"protocolVersion": PROTOCOL, - "capabilities": {"tools": {}}, "serverInfo": {"name": name, "version": "1"}}}))?, - "notifications/initialized" => {} - "ping" => { if !id.is_null() { say(json!({"id": id, "result": {}}))? } } - "tools/list" => { - let listed: Vec = tools.iter().map(|t| json!({ - "name": t.name, "description": t.description, "inputSchema": as_schema(&t.input)})).collect(); - say(json!({"id": id, "result": {"tools": listed}}))? - } - "tools/call" => { - let wanted = params.get("name").and_then(Value::as_str).unwrap_or(""); - let empty = Map::new(); - let args = params.get("arguments").and_then(Value::as_object).unwrap_or(&empty); - match tools.iter().find(|t| t.name == wanted) { - None => say(json!({"id": id, "error": {"code": -32602, "message": format!("{} has no tool {}", name, wanted)}}))?, - Some(t) => match (t.run)(args) { - Ok(v) => say(json!({"id": id, "result": {"content": [{"type": "text", "text": v.to_string()}]}}))?, - Err(e) => say(json!({"id": id, "result": {"content": [{"type": "text", "text": e}], "isError": true}}))?, - }, - } - } - _ => { if !id.is_null() { say(json!({"id": id, "error": {"code": -32601, "message": format!("no {}", method)}}))? } } - } - } - Ok(()) -} diff --git a/src/index.ts b/src/index.ts index 57175b8..0f28891 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,4 +13,3 @@ export * as messaging from "./messaging/index.js"; export * as events from "./events/index.js"; export * as primitives from "./primitives/index.js"; export * as state from "./state/index.js"; -export * as seal from "./seal/index.js"; diff --git a/src/seal/box.ts b/src/seal/box.ts deleted file mode 100644 index 30ea274..0000000 --- a/src/seal/box.ts +++ /dev/null @@ -1,90 +0,0 @@ -// The sealed box itself (novox/hq ADR 0183, ADR 0206), with its two sources of randomness as arguments so -// the tests can prove it byte for byte against the Go package's vectors. Modules import `./index.js`. - -import { - createCipheriv, - createDecipheriv, - createPrivateKey, - createPublicKey, - diffieHellman, - hkdfSync, - type KeyObject, -} from "node:crypto"; - -/** A value sealed to one recipient, as it travels in JSON; every field but `v` is standard base64. */ -export interface SealedBox { - readonly v: number; - /** The one-time public key, SPKI DER. */ - readonly eph: string; - readonly iv: string; - readonly tag: string; - readonly ct: string; -} - -/** The only box version `open` opens. */ -export const VERSION = 1; - -const INFO = "novox-mesh sealed box v1"; -const IV_BYTES = 12; -const TAG_BYTES = 16; - -function x25519(key: KeyObject, what: string): KeyObject { - if (key.asymmetricKeyType !== "x25519") throw new Error(`not an X25519 ${what} key`); - return key; -} - -export function publicFromPem(pem: string): KeyObject { - return x25519(createPublicKey({ key: pem, format: "pem" }), "public"); -} - -export function privateFromPem(pem: string): KeyObject { - return x25519(createPrivateKey({ key: pem, format: "pem" }), "private"); -} - -/** The recipient's raw 32-byte public key: the last 32 bytes of its SPKI DER. */ -function raw(pub: KeyObject): Buffer { - const der = pub.export({ format: "der", type: "spki" }); - return der.subarray(der.length - 32); -} - -function boxKey(secret: Buffer, ephDer: Buffer, recipientRaw: Buffer): Buffer { - return Buffer.from(hkdfSync("sha256", secret, Buffer.concat([ephDer, recipientRaw]), INFO, 32)); -} - -/** `seal` with the one-time private key and the iv given. */ -export function sealWith(plaintext: string, recipientPem: string, eph: KeyObject, iv: Buffer): SealedBox { - const recipient = publicFromPem(recipientPem); - x25519(eph, "one-time"); - if (iv.length !== IV_BYTES) throw new Error(`an iv of ${iv.length} bytes, not ${IV_BYTES}`); - const secret = diffieHellman({ privateKey: eph, publicKey: recipient }); - const ephDer = createPublicKey(eph).export({ format: "der", type: "spki" }); - const cipher = createCipheriv("aes-256-gcm", boxKey(secret, ephDer, raw(recipient)), iv); - const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]); - return { - v: VERSION, - eph: ephDer.toString("base64"), - iv: iv.toString("base64"), - tag: cipher.getAuthTag().toString("base64"), - ct: ct.toString("base64"), - }; -} - -/** Open a box with the recipient's PEM private key; throws for a box to another key, changed, or of another version. */ -export function open(box: SealedBox, privatePem: string): string { - if (box.v !== VERSION) throw new Error(`a sealed box of version ${box.v}, not ${VERSION}`); - const priv = privateFromPem(privatePem); - const ephDer = Buffer.from(box.eph, "base64"); - const eph = x25519(createPublicKey({ key: ephDer, format: "der", type: "spki" }), "one-time"); - const iv = Buffer.from(box.iv, "base64"); - const tag = Buffer.from(box.tag, "base64"); - // node accepts a shorter tag than 16 bytes; the box never has one, so a short one is refused here. - if (iv.length !== IV_BYTES || tag.length !== TAG_BYTES) throw new Error("the box does not open with this key"); - const secret = diffieHellman({ privateKey: priv, publicKey: eph }); - const decipher = createDecipheriv("aes-256-gcm", boxKey(secret, ephDer, raw(createPublicKey(priv))), iv); - decipher.setAuthTag(tag); - try { - return Buffer.concat([decipher.update(Buffer.from(box.ct, "base64")), decipher.final()]).toString("utf8"); - } catch { - throw new Error("the box does not open with this key"); - } -} diff --git a/src/seal/index.ts b/src/seal/index.ts deleted file mode 100644 index d3b1751..0000000 --- a/src/seal/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Sealing to one recipient (novox/hq ADR 0183, ADR 0206): X25519 for the agreement, HKDF-SHA256 for the -// key, AES-256-GCM for the box. The TypeScript twin of the Go package `go/seal`, byte for byte: the same -// fixtures prove both. A box is `{ v: 1, eph, iv, tag, ct }`; keys travel as PEM (SPKI public, PKCS #8 -// private). - -import { generateKeyPairSync, randomBytes } from "node:crypto"; -import { sealWith, open, VERSION, type SealedBox } from "./box.js"; - -export { open, VERSION, type SealedBox }; - -/** A recipient's keypair as the two PEM strings it is kept and sent as. */ -export interface KeyPair { - readonly publicKey: string; - readonly privateKey: string; -} - -/** A fresh X25519 keypair, PEM-encoded. */ -export function generateKeyPair(): KeyPair { - const { publicKey, privateKey } = generateKeyPairSync("x25519"); - return { - publicKey: publicKey.export({ format: "pem", type: "spki" }) as string, - privateKey: privateKey.export({ format: "pem", type: "pkcs8" }) as string, - }; -} - -/** Seal plaintext to the recipient's PEM public key, with a fresh one-time key and iv. */ -export function seal(plaintext: string, recipientPem: string): SealedBox { - return sealWith(plaintext, recipientPem, generateKeyPairSync("x25519").privateKey, randomBytes(12)); -} diff --git a/test/seal.test.ts b/test/seal.test.ts deleted file mode 100644 index 9eb840b..0000000 --- a/test/seal.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; -import { createPrivateKey } from "node:crypto"; - -import { seal, open, generateKeyPair, type SealedBox } from "../dist/seal/index.js"; -import { sealWith } from "../dist/seal/box.js"; - -// The fixtures the Go package proves itself against: boxes the licence manager's own seal.go made, and -// the one the agent module's TypeScript made. One format, three implementations. -const fixtures = new URL("../go/seal/testdata/", import.meta.url); -const lm = JSON.parse(readFileSync(new URL("sealed-by-licence-manager.json", fixtures), "utf8")) as { - recipient: { publicKey: string; privateKey: string }; - sealed: { plaintext: string; box: SealedBox }[]; - vectors: { plaintext: string; ephPrivate: string; iv: string; box: SealedBox }[]; -}; - -// PKCS #8 for an X25519 private key is this prefix and the 32 raw bytes. -const pkcs8 = (raw: Buffer) => - createPrivateKey({ key: Buffer.concat([Buffer.from("302e020100300506032b656e04220420", "hex"), raw]), format: "der", type: "pkcs8" }); - -test("seal: every box the licence manager sealed opens to its plaintext", () => { - assert.ok(lm.sealed.length > 0); - for (const s of lm.sealed) assert.equal(open(s.box, lm.recipient.privateKey), s.plaintext); -}); - -test("seal: given the same one-time key and iv, the box is byte for byte the licence manager's", () => { - assert.ok(lm.vectors.length > 0); - for (const v of lm.vectors) { - const box = sealWith(v.plaintext, lm.recipient.publicKey, pkcs8(Buffer.from(v.ephPrivate, "base64")), Buffer.from(v.iv, "base64")); - assert.deepEqual(box, v.box); - assert.equal(open(box, lm.recipient.privateKey), v.plaintext); - } -}); - -test("seal: the agent module's TypeScript box opens", () => { - const f = JSON.parse(readFileSync(new URL("sealed-by-typescript.json", fixtures), "utf8")); - assert.equal(open(f.box, f.privateKey), f.plaintext); -}); - -test("seal: a box opens for its recipient only, and two boxes share no one-time key or iv", () => { - const a = generateKeyPair(); - const b = generateKeyPair(); - const box = seal("a token", a.publicKey); - assert.equal(open(box, a.privateKey), "a token"); - assert.throws(() => open(box, b.privateKey)); - const again = seal("a token", a.publicKey); - assert.notEqual(again.eph, box.eph); - assert.notEqual(again.iv, box.iv); -}); - -test("seal: a box changed in any field, or of another version, does not open", () => { - const good = lm.sealed[1].box; - const flip = (s: string) => { - const b = Buffer.from(s, "base64"); - b[b.length - 1] ^= 1; - return b.toString("base64"); - }; - for (const box of [ - { ...good, ct: flip(good.ct) }, - { ...good, tag: flip(good.tag) }, - { ...good, iv: flip(good.iv) }, - { ...good, eph: flip(good.eph) }, - { ...good, v: 2 }, - ]) assert.throws(() => open(box as SealedBox, lm.recipient.privateKey)); -}); - -test("seal: a key that is not X25519 PEM is refused", () => { - assert.throws(() => seal("x", "not a key")); - assert.throws(() => open(lm.sealed[0].box, lm.recipient.publicKey)); -});