Delete the Python, Rust and C bindings, the Go example and the TypeScript seal and contracts exports, which nothing imports, in the release's one mesh-sdk change (ADR 0593 §12)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -1 +0,0 @@
|
||||
example
|
||||
@@ -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
|
||||
@@ -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 `<seat>.<verb>` 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`.
|
||||
-27
@@ -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 <stdio.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
#include "mesh_sdk.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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
|
||||
* `<seat>.<verb>` 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 <jansson.h>
|
||||
|
||||
#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
|
||||
+1
-1
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Generated
+2
-2
@@ -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"
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
0.1.17 90cca0bf1acfb386d66ccddb9f612703cf8f0205791b846982a524d0044aa0fb
|
||||
0.1.18 5cbc2d043e45ab47eadff55a61f8be7d50144cec3cf295333b5ea1b4c181cfdf
|
||||
|
||||
+2
-4
@@ -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"
|
||||
|
||||
@@ -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
|
||||
`<seat>.<verb>` 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.
|
||||
@@ -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"}),
|
||||
])
|
||||
@@ -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 `<seat>.<verb>` 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}"}})
|
||||
Binary file not shown.
@@ -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"
|
||||
@@ -1,2 +0,0 @@
|
||||
target/
|
||||
Cargo.lock
|
||||
@@ -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"
|
||||
@@ -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 `<seat>.<verb>` 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.
|
||||
@@ -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<String, Value>) -> Result<Value, String> {
|
||||
let who = a.get("who").and_then(Value::as_str).unwrap_or("world");
|
||||
Ok(json!({"greeting": format!("hello {}", who), "language": "rust"}))
|
||||
}
|
||||
|
||||
fn on(_: &Map<String, Value>) -> Result<Value, String> {
|
||||
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 },
|
||||
])
|
||||
}
|
||||
@@ -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
|
||||
//! `<seat>.<verb>` 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<String, Value>) -> Result<Value, String>,
|
||||
}
|
||||
|
||||
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<Value> = 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(())
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
Reference in New Issue
Block a user