Beside every TypeScript entrypoint the builder writes an executable launcher; the runtime is told it (hq ADR 0193)

The runtime knows no language: the build makes each served entrypoint executable. For a TypeScript
bundle that is <entry>.serve.mjs, which imports the entrypoint and serves what it registered over
MCP on stdio through the bundle's own SDK. The build records its launchers on the bundle, and the
composer names the launcher where a build wrote one and the entrypoint where it did not, so bundles
built before this keep serving until they are rebuilt.
This commit is contained in:
jochen
2026-10-03 21:07:07 +02:00
parent f966693583
commit 17dba2a34c
6 changed files with 136 additions and 2 deletions
+49 -1
View File
@@ -587,6 +587,12 @@ func one(ctx context.Context, run Runner, publish Publisher,
if err != nil {
return catalogue.Built{}, fmt.Errorf("%s: compiling %s failed: %w", module, a.Name, err)
}
// **Every entrypoint the runtime may serve is executable** (novox/hq ADR 0193). The runtime
// knows no language; for one that runs through an interpreter the build writes the launcher.
launchers, err := writeLaunchers(compiled, chain, a)
if err != nil {
return catalogue.Built{}, fmt.Errorf("%s: writing %s's launchers failed: %w", module, a.Name, err)
}
say("bundle", "compiled, packing")
body, err := pack(compiled)
if err != nil {
@@ -598,7 +604,7 @@ func one(ctx context.Context, run Runner, publish Publisher,
if err != nil {
return catalogue.Built{}, err
}
return catalogue.Built{Name: a.Name, Kind: a.Kind, Reference: where, Digest: digest}, nil
return catalogue.Built{Name: a.Name, Kind: a.Kind, Reference: where, Digest: digest, Launchers: launchers}, nil
case catalogue.ArtifactPackage:
// Built and published on a public base, to the mesh's package registry, by version
@@ -1173,3 +1179,45 @@ func binaryName(a catalogue.Artifact) string {
}
return a.Name
}
// launcherSuffix is what a TypeScript entrypoint's launcher is called beside it: index.js is
// started as index.serve.mjs (novox/hq ADR 0193). An ES module by its own extension, whatever the
// bundle's package.json says.
const launcherSuffix = ".serve.mjs"
// writeLaunchers writes, beside every entrypoint of a TypeScript bundle, an executable that
// imports the entrypoint and serves what it registered over MCP on stdio — through the bundle's
// own copy of the SDK, so registering and serving meet in one registry (novox/hq ADR 0193). Its
// answer is each entrypoint's launcher, by entrypoint, relative to the bundle's root; nothing for
// a language whose build is already executable.
func writeLaunchers(root string, chain Toolchain, a catalogue.Artifact) (map[string]string, error) {
if chain.Language != "typescript" {
return nil, nil
}
out := map[string]string{}
for _, entry := range a.Entrypoints {
if !strings.HasSuffix(entry, ".js") {
continue
}
launcher := strings.TrimSuffix(entry, ".js") + launcherSuffix
body := "#!/usr/bin/env node\n" +
"// Written by the mesh's builder (novox/hq ADR 0193): serve what " + entry + " registers,\n" +
"// over MCP on stdio, as the module the node's runtime names in MESH_SERVED_MODULE.\n" +
"import { serveRegisteredOverStdio } from \"@novox/mesh-sdk/stdio\";\n" +
"await import(\"./" + filepath.Base(entry) + "\");\n" +
"await serveRegisteredOverStdio();\n"
path := filepath.Join(root, filepath.FromSlash(launcher))
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return nil, err
}
if err := os.WriteFile(path, []byte(body), 0o755); err != nil {
return nil, err
}
// WriteFile honours the umask; the mode a machine unpacks is the one packed, so it is set.
if err := os.Chmod(path, 0o755); err != nil {
return nil, err
}
out[entry] = launcher
}
return out, nil
}
+49
View File
@@ -0,0 +1,49 @@
package builder
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/novox/mesh-controller/internal/catalogue"
)
// novox/hq ADR 0193: every entrypoint the runtime may serve is executable, and the runtime knows no
// language — so a TypeScript bundle carries a launcher beside each entrypoint.
func TestATypeScriptBundleCarriesAnExecutableLauncherBesideEachEntrypoint(t *testing.T) {
root := t.TempDir()
chain, err := ToolchainFor("typescript")
if err != nil {
t.Fatal(err)
}
got, err := writeLaunchers(root, chain, catalogue.Artifact{Name: "tools", Kind: catalogue.ArtifactBundle,
Language: "typescript", Entrypoints: []string{"tools/index.js", "index.js"}})
if err != nil {
t.Fatal(err)
}
if got["tools/index.js"] != "tools/index.serve.mjs" || got["index.js"] != "index.serve.mjs" {
t.Fatalf("launchers: %v", got)
}
path := filepath.Join(root, "tools", "index.serve.mjs")
info, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
if info.Mode().Perm() != 0o755 {
t.Errorf("the launcher is %v, not executable 0755", info.Mode().Perm())
}
body, _ := os.ReadFile(path)
for _, want := range []string{"#!/usr/bin/env node\n", `from "@novox/mesh-sdk/stdio"`, `await import("./index.js")`, "serveRegisteredOverStdio()"} {
if !strings.Contains(string(body), want) {
t.Errorf("the launcher lacks %q:\n%s", want, body)
}
}
// A compiled language's build is executable already: no launcher.
goChain, _ := ToolchainFor("go")
none, err := writeLaunchers(t.TempDir(), goChain, catalogue.Artifact{Name: "tools", Kind: catalogue.ArtifactBundle, Language: "go"})
if err != nil || len(none) != 0 {
t.Errorf("a Go bundle was given launchers: %v %v", none, err)
}
}
+4 -1
View File
@@ -28,6 +28,9 @@ type Built struct {
Reference string
// Digest is "sha256:<hex>", for an archive. An image reference already ends in one.
Digest string
// Launchers are, for a bundle in an interpreted language, the executable the build wrote beside
// each entrypoint, by entrypoint (novox/hq ADR 0193): what the node's runtime starts to serve it.
Launchers map[string]string
}
// Resolve fills a manifest's resources in from what was built.
@@ -92,7 +95,7 @@ func (m Manifest) Resolve(built []Built) (Manifest, error) {
out.Bundles = append(out.Bundles, Bundle{
Name: a.Name, Source: Recorded(made.Reference), Digest: made.Digest,
Language: a.Language, Entrypoints: append([]string(nil), a.Entrypoints...),
Loads: loads, Env: copyWords(a.Env),
Loads: loads, Env: copyWords(a.Env), Launchers: copyWords(made.Launchers),
})
}
sort.Slice(out.Bundles, func(i, j int) bool { return out.Bundles[i].Name < out.Bundles[j].Name })
+3
View File
@@ -604,6 +604,9 @@ type Bundle struct {
Loads []string `json:"loads,omitempty"`
// Env is what the artifact said it is given (ADR 0192), as written; composed per machine.
Env map[string]string `json:"env,omitempty"`
// Launchers are the executables the build wrote beside its entrypoints, by entrypoint (novox/hq
// ADR 0193). A bundle built before them has none, and is served as it was built.
Launchers map[string]string `json:"launchers,omitempty"`
}
// Build says how to produce this module's artifacts from its source.
+5
View File
@@ -169,6 +169,11 @@ func (r Resolution) runtimeProcess(with Rendering) (map[string]any, error) {
continue
}
for _, load := range b.Loads {
// What the runtime starts: the launcher the build wrote beside the entrypoint, where
// it wrote one (ADR 0193); the entrypoint itself for a build from before them.
if launcher, has := b.Launchers[load]; has {
load = launcher
}
served = append(served, m.Module+"="+BundlePath(m.Module, b.Name)+"/"+load)
}
restartOn = append(restartOn, m.Module+"."+BundleID(b.Name))
+26
View File
@@ -336,3 +336,29 @@ func TestABundlesWordsAreRefusedWhenTheyAreNotPathsOrConstants(t *testing.T) {
}
}
}
// novox/hq ADR 0193: the runtime is told the launcher a build wrote, and the entrypoint itself for a
// build from before launchers — so the move needs no flag day.
func TestTheRuntimeStartsTheLauncherWhereTheBuildWroteOne(t *testing.T) {
with := Rendering{ArtifactStore: "anchor.internal:5101",
Needed: map[string]map[string]string{RuntimeModule: {"broker": "sealed-credential"}}}
launched := Manifest{Module: "dash", Version: "1", Tools: []string{"status"},
Build: &Build{Artifacts: []Artifact{{Name: "tools", Kind: ArtifactBundle, Language: "typescript",
Entrypoints: []string{"tools/index.js"}}}}}
launched, err := launched.Resolve([]Built{{Name: "tools", Kind: ArtifactBundle,
Reference: ArtifactStoreScheme + "dash/tools/blobs/" + bundleDigest, Digest: bundleDigest,
Launchers: map[string]string{"tools/index.js": "tools/index.serve.mjs"}}})
if err != nil {
t.Fatal(err)
}
older := aToolsModule(t, "nftables", "tools/index.js")
out, err := Resolution{Node: "anchor", Account: "ops", Modules: []Manifest{launched, older, theRuntime(t)}}.Declaration(with)
if err != nil {
t.Fatal(err)
}
env := fileNamed(out, RuntimeModule+"."+RuntimeProcessID())["env"].(map[string]string)
want := "dash=" + BundleRoot + "/dash/tools/tools/index.serve.mjs,nftables=" + BundleRoot + "/nftables/tools/tools/index.js"
if env[RuntimeToolModules] != want {
t.Errorf("the runtime is told %q, want %q", env[RuntimeToolModules], want)
}
}