TLS-1, TLS-2, TLS-3: the runner declares its own secret ledgers, refuses a bundle tool named as a ledger repair, and states the capabilities of its calls in its PING (issue 1966)

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 11:40:03 +01:00
parent 84ede95c4a
commit 8b1ab467a9
8 changed files with 153 additions and 6 deletions
+5
View File
@@ -43,6 +43,11 @@ var Patience = 5 * time.Second
// to the laptop, and the hundred-byte PING answers queued behind it.
const Digest = "digest"
// Capabilities is the service metadata key a runtime states what it can do under, a comma-separated list of capabilities
// (runtime.CallCapabilities): its machine's node-engine asks the runtime's PING and takes them into the node's record, so a
// module needing one is placed only where the runtime states it.
const Capabilities = "capabilities"
// Shortened is the service metadata key a runtime sets when its announcement was cut to fit the bus.
const Shortened = "shortened"
@@ -39,3 +39,22 @@ func TestTheManifestNamesTheFirstFamiliesOneByOne(t *testing.T) {
t.Errorf("the tool runner consumes %v: a wait is no consumer (ADR 0367 §2)", m.Consumes)
}
}
// issue 1966 TLS-1: the tool runner declares its own secret `ledgers`, each module's ledger credential, in the directory it
// keeps its broker credential in, which is where the runner reads it (runtime.LedgersFileOf).
func TestTheManifestDeclaresTheRunnersOwnSecretLedgers(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
var m struct {
OwnSecrets map[string]string `json:"own-secrets"`
}
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
want := map[string]string{"broker": "${dir:mesh-state}/broker", "ledgers": "${dir:mesh-state}/ledgers"}
if !reflect.DeepEqual(m.OwnSecrets, want) {
t.Errorf("own secrets %v, want %v", m.OwnSecrets, want)
}
}
+27 -4
View File
@@ -10,6 +10,7 @@ import (
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/micro"
"github.com/novox/mesh-tools/node-tools/internal/announce"
"github.com/novox/mesh-tools/node-tools/internal/bus"
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
)
@@ -100,10 +101,6 @@ func TestTheRuntimeAnnouncesWhatItServesInTheServicesProtocol(t *testing.T) {
if msg, err := nc.Request("$SRV.PING", nil, 2*time.Second); err != nil || !strings.Contains(string(msg.Data), micro.PingResponseType) {
t.Errorf("ping: %v %v", msg, err)
}
if _, err := nc.Request("$SRV.INFO.somebody-else", nil, 300*time.Millisecond); err == nil {
t.Error("answered a request for another service")
}
// alpha is re-issued a plain subject: the announcement says so, and says it is interchangeable.
mesh.Issue(t, mt.MembershipOf("alpha", "anchor", true, nil))
var after string
@@ -120,3 +117,29 @@ func TestTheRuntimeAnnouncesWhatItServesInTheServicesProtocol(t *testing.T) {
}
_ = bus.Served{}
}
// issue 1966 TLS-3 (ENG-2): the runner states the capabilities of the calls it answers in its PING, on its own instance, which
// is what its machine's node-engine asks and takes into the node's record; no bundle is needed for it.
func TestTheRunnerStatesTheCapabilitiesOfItsCallsInItsPing(t *testing.T) {
mesh := mt.New(t)
mesh.Issue(t, mt.MembershipOf("alpha", "anchor", false, nil))
nodeTools := connect(t, "node-tools", "anchor")
stop, err := Run(nodeTools, nil, nil, (&mt.Logs{}).Logf)
if err != nil {
t.Fatal(err)
}
defer stop()
nc, err := nats.Connect(mt.URL(t))
if err != nil {
t.Fatal(err)
}
defer nc.Close()
msg, err := nc.Request("$SRV.PING.node-tools.anchor", nil, 2*time.Second)
if err != nil {
t.Fatalf("ping of the instance: %v", err)
}
var ping micro.Ping
if err := json.Unmarshal(msg.Data, &ping); err != nil || ping.Metadata[announce.Capabilities] != "bus-state-calls,ledger-calls" {
t.Errorf("the runner states %q (%v), want its bus-state and ledger calls", ping.Metadata[announce.Capabilities], err)
}
}
+28
View File
@@ -3,10 +3,38 @@ package runtime
import (
"encoding/json"
"fmt"
"sort"
"git.novox.be/novox/mesh-sdk/go/buscalls"
)
// The capabilities the tool runner states of the calls it answers (novox/hq ADR 0595 §9, ADR 0597; design 25 § 12.4): each is
// a family of the methods a bundle asks it, named so a manifest's `capabilities` can ask for it, and stated in its announcement
// (announce.Capabilities) for its machine's node-engine to take into the node's record. A module whose manifest asks for one is
// held off a machine whose runner does not state it.
const (
CapBusStateCalls = "bus-state-calls"
CapLedgerCalls = "ledger-calls"
)
// callFamilies are the methods each capability stands for, the ones BusState and BusStateWatch answer.
var callFamilies = map[string][]string{
CapBusStateCalls: {buscalls.ObjectsPut, buscalls.ObjectsGet, buscalls.ObjectsList, buscalls.ObjectsDelete, buscalls.ObjectsInfo,
buscalls.ObjectsWatch, buscalls.StreamRead, buscalls.StreamFill, buscalls.WorkQueueSize, buscalls.WorkQueueDead},
CapLedgerCalls: {buscalls.LedgerApply, buscalls.LedgerGet, buscalls.LedgerHistory, buscalls.LedgerWatch, buscalls.LedgerQuery,
buscalls.LedgerRebuild, buscalls.LedgerReindex, buscalls.LedgerSnapshot, buscalls.LedgerLag},
}
// CallCapabilities is every capability of the calls this runner answers, sorted.
func CallCapabilities() []string {
out := make([]string, 0, len(callFamilies))
for c := range callFamilies {
out = append(out, c)
}
sort.Strings(out)
return out
}
// BusState answers a bundle's calls on its module's bus state in the new form (mesh-sdk buscalls; novox/hq ADR 0595
// §9, design 25 § 12.5; issue 1966): each as the module whose bundle asks, never a module the request names, so a
// bundle reaches its own module's bindings alone.
+15
View File
@@ -213,6 +213,21 @@ const (
RepairLag = "ledger_lag"
)
// repairClash is why a bundle's tools of a module cannot be served beside the runner's own ledger repairs: a tool named as
// one of them, which the runner serves for every module owning a ledger, would be taken for it or hide it. It names the
// tool and the repair it clashes with; empty when none does.
func repairClash(module string, tools []launch.Tool) string {
for _, t := range tools {
for _, repair := range []string{RepairRebuild, RepairReindex, RepairSnapshot, RepairLag} {
if t.Name == repair {
return fmt.Sprintf("%s's bundle serves a tool named %s, which is also the ledger repair the runner serves for %s "+
"(novox/hq ADR 0597 §10): rename the bundle's tool", module, t.Name, module)
}
}
}
return ""
}
// The effects a tool carries (novox/hq ADR 0387 §1).
const (
EffectReads = "reads"
@@ -15,6 +15,7 @@ import (
"github.com/nats-io/nats.go/jetstream"
"github.com/novox/mesh-tools/node-tools/internal/bus"
"github.com/novox/mesh-tools/node-tools/internal/launch"
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
)
@@ -179,3 +180,45 @@ func TestAnApplyIsStampedWithTheCallerTheRuntimeProved(t *testing.T) {
t.Fatal("a call that ended was still taken as in flight")
}
}
// issue 1966 TLS-2: a bundle's tool named as one of the runner's ledger repairs is refused at the load, naming the tool and the
// repair; any other name is served.
func TestABundleToolNamedAsALedgerRepairIsRefused(t *testing.T) {
for _, c := range []struct{ name, tool, want string }{
{"rebuild", RepairRebuild, "m's bundle serves a tool named ledger_rebuild, which is also the ledger repair the runner serves for m"},
{"reindex", RepairReindex, "ledger_reindex"},
{"snapshot", RepairSnapshot, "ledger_snapshot"},
{"lag", RepairLag, "ledger_lag"},
{"another name", "ledger_status", ""},
} {
t.Run(c.name, func(t *testing.T) {
got := repairClash("m", []launch.Tool{{Name: "first"}, {Name: c.tool}})
if (c.want == "") != (got == "") || !strings.Contains(got, c.want) {
t.Errorf("clash %q, want one containing %q", got, c.want)
}
})
}
}
// issue 1966 TLS-3: every method of a capability the runner states is a call it answers, and the two states its calls under
// no other name; none answers "the runtime answers no ...".
func TestEveryCallOfAStatedCapabilityIsAnswered(t *testing.T) {
m, _, _ := aLedger(t)
if got := strings.Join(CallCapabilities(), ","); got != "bus-state-calls,ledger-calls" {
t.Fatalf("the runner states %s", got)
}
for capability, methods := range callFamilies {
for _, method := range methods {
t.Run(capability+" "+method, func(t *testing.T) {
_, err := m.BusState(method, json.RawMessage(`"not a request"`))
if err != nil && strings.Contains(err.Error(), "the runtime answers no") {
if stop, werr := m.BusStateWatch(method, json.RawMessage(`"not a request"`), func(json.RawMessage) error { return nil }); werr != nil && strings.Contains(werr.Error(), "the runtime answers no") {
t.Errorf("%s is stated under %s and answered by neither BusState nor BusStateWatch", method, capability)
} else if stop != nil {
stop()
}
}
})
}
}
}
+14 -1
View File
@@ -308,7 +308,7 @@ func Run(conn *bus.Conn, served []Served, envs map[string]map[string]string, log
announced, err := announce.Serve(conn, announce.Service{
Name: conn.Module(), ID: instanceOf(conn),
Description: "the mesh's tool runtime on " + node + ": every assigned module's tools and the seats they hold",
Metadata: map[string]string{"node": node},
Metadata: map[string]string{"node": node, announce.Capabilities: strings.Join(CallCapabilities(), ",")},
}, func() []announce.Endpoint { return endpointsOf(conn, r.own(), r.registrations(), r.modules) })
if err != nil {
logf("[mesh-tools] cannot announce what it serves: %v", err)
@@ -396,6 +396,19 @@ func (r *runner) load(s Served) *loadedModule {
fail(err.Error())
continue
}
// A tool named as one of the runner's ledger repairs would be taken for it, or hide it: refused at the load, naming both
// (ADR 0597 §10).
clashed := ""
for _, reg := range child.Registrations {
if clashed == "" && r.isServed[reg.Module] {
clashed = repairClash(reg.Module, reg.Tools)
}
}
if clashed != "" {
child.Stop()
fail(clashed)
continue
}
l.children = append(l.children, child.Stop)
for _, reg := range child.Registrations {
got := registration{module: reg.Module, owner: module, tools: reg.Tools}
+2 -1
View File
@@ -29,7 +29,8 @@
}
},
"own-secrets": {
"broker": "${dir:mesh-state}/broker"
"broker": "${dir:mesh-state}/broker",
"ledgers": "${dir:mesh-state}/ledgers"
},
"listens": [
{