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.
131 lines
4.3 KiB
Go
131 lines
4.3 KiB
Go
// node-tools — the node's tool runtime, in Go (novox/hq ADR 0175, ADR 0193; to-be 38 WP4d).
|
|
//
|
|
// One process per machine: it connects to the bus on the node's credential, launches every assigned
|
|
// module's tools bundle and serves its tools and its seats' verbs; as the node-tools module — or
|
|
// wherever MESH_CONSOLE_LISTEN says — it is also the console, MCP over HTTP on loopback.
|
|
//
|
|
// MESH_BROKER_FILE the credential the mesh sealed to this machine for the runtime
|
|
// MESH_TOOL_MODULES <module>=<entrypoint>,… the bundles to launch
|
|
// MESH_TOOL_ENV {"<module>": {"<word>": "<value>"}} what each is given (ADR 0192)
|
|
// MESH_OPERATOR_ACCOUNT whose machine this is, and MESH_OPERATOR_HOME their home
|
|
// MESH_CONSOLE_LISTEN where the console listens, overriding 127.0.0.1:4270
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"math/rand/v2"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-tools/node-tools/internal/bus"
|
|
"github.com/novox/mesh-tools/node-tools/internal/console"
|
|
"github.com/novox/mesh-tools/node-tools/internal/runtime"
|
|
)
|
|
|
|
// runtimeModule is the module that is the node's tool runtime; on its credential, serving is also
|
|
// the console.
|
|
const runtimeModule = "node-tools"
|
|
|
|
// consoleListen is where the console listens when nothing says otherwise.
|
|
const consoleListen = "127.0.0.1:4270"
|
|
|
|
func main() {
|
|
log.SetFlags(0)
|
|
if len(os.Args) > 1 && os.Args[1] != "serve" {
|
|
fmt.Fprintf(os.Stderr, "node-tools: %q is not a mode; this runtime serves (novox/hq ADR 0193)\n", os.Args[1])
|
|
os.Exit(2)
|
|
}
|
|
cred := credential()
|
|
conn := connectPatiently(cred)
|
|
|
|
served, err := runtime.ServedModulesFrom(os.Getenv(runtime.ToolModules), cred.Module)
|
|
if err != nil {
|
|
log.Fatalf("node-tools: %v", err)
|
|
}
|
|
envs, err := runtime.TakeToolEnvs()
|
|
if err != nil {
|
|
log.Fatalf("node-tools: %v", err)
|
|
}
|
|
stop, err := runtime.Run(conn, served, envs, log.Printf)
|
|
if err != nil {
|
|
log.Fatalf("node-tools: %v", err)
|
|
}
|
|
|
|
listen := os.Getenv("MESH_CONSOLE_LISTEN")
|
|
if listen == "" && cred.Module == runtimeModule {
|
|
listen = consoleListen
|
|
}
|
|
var up *console.Listening
|
|
if listen != "" {
|
|
node := cred.Node
|
|
if node == "" {
|
|
node = "?"
|
|
}
|
|
who := node + "." + cred.Module
|
|
up, err = console.Serve(console.NewSurface(conn, who), listen)
|
|
if err != nil {
|
|
log.Fatalf("node-tools: %v", err)
|
|
}
|
|
log.Printf("mesh console listening on http://%s/mcp as %s", up.Address, who)
|
|
}
|
|
|
|
signals := make(chan os.Signal, 1)
|
|
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
|
|
<-signals
|
|
stop()
|
|
if up != nil {
|
|
_ = up.Close()
|
|
}
|
|
conn.Close()
|
|
}
|
|
|
|
// credential reads the credential the mesh delivered; a missing or unreadable one is a fault of
|
|
// configuration, said and final.
|
|
func credential() bus.Credential {
|
|
file := os.Getenv("MESH_BROKER_FILE")
|
|
if file == "" {
|
|
if url := os.Getenv("MESH_BROKER_URL"); url != "" {
|
|
return bus.Credential{URL: url, Module: runtimeModule}
|
|
}
|
|
fmt.Fprintln(os.Stderr, "node-tools: set MESH_BROKER_FILE (a sealed credential) or MESH_BROKER_URL — there is no broker to reach")
|
|
os.Exit(1)
|
|
}
|
|
raw, err := os.ReadFile(file)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "node-tools: cannot read the broker credential at %s: %v\n", file, err)
|
|
os.Exit(1)
|
|
}
|
|
var cred bus.Credential
|
|
if err := json.Unmarshal(raw, &cred); err != nil {
|
|
fmt.Fprintf(os.Stderr, "node-tools: cannot read the broker credential at %s: %v\n", file, err)
|
|
os.Exit(1)
|
|
}
|
|
if cred.URL == "" {
|
|
fmt.Fprintf(os.Stderr, "node-tools: %s carries no url — it is not a broker credential\n", file)
|
|
os.Exit(1)
|
|
}
|
|
return cred
|
|
}
|
|
|
|
// connectPatiently retries while the bus is merely not reachable yet — the normal case at startup —
|
|
// and gives up at once on what waiting cannot fix (issue 058).
|
|
func connectPatiently(cred bus.Credential) *bus.Conn {
|
|
for delay := 2 * time.Second; ; delay = min(delay*2, 30*time.Second) {
|
|
conn, err := bus.Connect(cred)
|
|
if err == nil {
|
|
return conn
|
|
}
|
|
if fatal := bus.Fatal(err); fatal != "" {
|
|
fmt.Fprintf(os.Stderr, "node-tools: %s — waiting will not fix this; giving up\n", fatal)
|
|
os.Exit(1)
|
|
}
|
|
wait := delay + time.Duration(rand.IntN(1000))*time.Millisecond
|
|
fmt.Fprintf(os.Stderr, "node-tools: the broker is not reachable yet (%v); retrying in %ds\n", err, int(wait.Round(time.Second)/time.Second))
|
|
time.Sleep(wait)
|
|
}
|
|
}
|