The runtime knows no language, so nothing ties it to Node.js. This ports its serve mode — the pinned bus connection and patient connect, following memberships, launching every served bundle over MCP on stdio with its own environment, a child's emit published as its module, each tool, the tools verb and seat verbs served where the mesh issued them, and the console on loopback — to one static binary. Same subjects, request and reply bodies, event headers and MCP answers. The TypeScript stays: it is still the runtime inside the per-module containers until WP4c. Tests run against a real bus and share the TypeScript fixtures.
126 lines
3.9 KiB
Go
126 lines
3.9 KiB
Go
package console
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/novox/mesh-tools/node-tools/internal/wire"
|
|
)
|
|
|
|
// bodyLimit is the most a request body may be.
|
|
const bodyLimit = 1 << 20
|
|
|
|
var loopback = map[string]bool{"127.0.0.1": true, "::1": true, "localhost": true, "[::1]": true}
|
|
|
|
// Listening is a console that listens: where, with the port the machine gave, and how to stop it.
|
|
type Listening struct {
|
|
Address string
|
|
Close func() error
|
|
}
|
|
|
|
// Serve listens on host:port, refused unless the host is loopback — said before binding, so a
|
|
// console that would open to a network is a startup failure (ADR 0152).
|
|
func Serve(s *Surface, listen string) (*Listening, error) {
|
|
at := strings.LastIndex(listen, ":")
|
|
if at < 0 {
|
|
return nil, fmt.Errorf("%q is not host:port", listen)
|
|
}
|
|
host, portText := listen[:at], listen[at+1:]
|
|
if !loopback[host] {
|
|
return nil, fmt.Errorf(`the console listens on loopback and nowhere else (novox/hq ADR 0152): %q is not this `+
|
|
"machine's own address — whoever is on the machine owns the mesh there, and nobody else may reach this", host)
|
|
}
|
|
port, err := strconv.Atoi(portText)
|
|
if err != nil || port < 0 || port > 65535 {
|
|
return nil, fmt.Errorf("%q is not a port", portText)
|
|
}
|
|
ln, err := net.Listen("tcp", net.JoinHostPort(strings.Trim(host, "[]"), portText))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
server := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { route(w, r, s) })}
|
|
go func() { _ = server.Serve(ln) }()
|
|
bound := ln.Addr().(*net.TCPAddr).Port
|
|
return &Listening{Address: host + ":" + strconv.Itoa(bound), Close: func() error { return server.Shutdown(context.Background()) }}, nil
|
|
}
|
|
|
|
func route(w http.ResponseWriter, r *http.Request, s *Surface) {
|
|
switch r.URL.Path {
|
|
case "/":
|
|
w.Header().Set("content-type", "text/plain; charset=utf-8")
|
|
_, _ = io.WriteString(w, "the mesh's console: MCP over HTTP at POST /mcp (novox/hq design 34)\n")
|
|
return
|
|
case "/mcp":
|
|
default:
|
|
writeJSON(w, 404, map[string]any{"error": "the console serves /mcp and nothing else"})
|
|
return
|
|
}
|
|
switch r.Method {
|
|
case http.MethodPost:
|
|
case http.MethodDelete:
|
|
w.WriteHeader(204) // no session to end
|
|
return
|
|
default:
|
|
w.Header().Set("allow", "POST, DELETE")
|
|
w.WriteHeader(405)
|
|
return
|
|
}
|
|
body, err := io.ReadAll(io.LimitReader(r.Body, bodyLimit+1))
|
|
if err != nil || len(body) > bodyLimit {
|
|
if err == nil {
|
|
err = fmt.Errorf("the request is larger than %d bytes", bodyLimit)
|
|
}
|
|
writeJSON(w, 413, map[string]any{"jsonrpc": "2.0", "id": nil, "error": map[string]any{"code": -32600, "message": err.Error()}})
|
|
return
|
|
}
|
|
trimmed := strings.TrimSpace(string(body))
|
|
if strings.HasPrefix(trimmed, "[") {
|
|
var batch []Request
|
|
if err := json.Unmarshal(body, &batch); err != nil {
|
|
writeJSON(w, 400, map[string]any{"jsonrpc": "2.0", "id": nil, "error": map[string]any{"code": -32700, "message": "the body is not JSON"}})
|
|
return
|
|
}
|
|
replies := []*Reply{}
|
|
for _, req := range batch {
|
|
if reply := s.Handle(req); reply != nil {
|
|
replies = append(replies, reply)
|
|
}
|
|
}
|
|
if len(replies) == 0 {
|
|
w.WriteHeader(202)
|
|
return
|
|
}
|
|
writeJSON(w, 200, replies)
|
|
return
|
|
}
|
|
var req Request
|
|
if err := json.Unmarshal(body, &req); err != nil {
|
|
writeJSON(w, 400, map[string]any{"jsonrpc": "2.0", "id": nil, "error": map[string]any{"code": -32700, "message": "the body is not JSON"}})
|
|
return
|
|
}
|
|
reply := s.Handle(req)
|
|
if reply == nil {
|
|
w.WriteHeader(202)
|
|
return
|
|
}
|
|
writeJSON(w, 200, reply)
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, body any) {
|
|
text, err := wire.Marshal(body)
|
|
if err != nil {
|
|
text, _ = json.Marshal(map[string]any{"error": errors.New("unencodable answer").Error()})
|
|
}
|
|
w.Header().Set("content-type", "application/json; charset=utf-8")
|
|
w.Header().Set("content-length", strconv.Itoa(len(text)))
|
|
w.WriteHeader(status)
|
|
_, _ = w.Write(text)
|
|
}
|