package link import ( "context" "encoding/json" "errors" "fmt" "sort" "strings" "time" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/asks" "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/micro" ) // The node seat node-engine, served on the engine's own link (novox/hq ADR 0563 §1). // // Each verb is answered on this node's subject alone, `mesh.seat.node-engine.tool..`, and on the same // address naming its caller, `mesh.seat.node-engine.call...`, whose last token the bus lets each user // publish with its own name alone (novox/hq issue 365): the caller is that token, and a call on the subject naming none is // from an unproven caller, recorded so. The body is the call's arguments; the answer is {result, error, node}, as every // tool's (mesh-tools node-tools). One service, `node-engine`, answers discovery with each verb's seat, tool and node. // EngineSeat is the seat. const EngineSeat = catalogue.EngineSeat // EngineToolSubject is where a verb of this node's seat is asked naming no caller. func EngineToolSubject(verb, node string) string { return "mesh.seat." + EngineSeat + ".tool." + verb + "." + node } // EngineCallPattern is where a verb of this node's seat is asked naming its caller, any caller. func EngineCallPattern(verb, node string) string { return "mesh.seat." + EngineSeat + ".call." + verb + "." + node + ".*" } // EngineEventSubject is where the seat says one of its events about this node. func EngineEventSubject(event, node string) string { return "mesh.seat." + EngineSeat + ".event." + event + "." + node } // EngineCall is one call of the seat: its verb, its caller as the bus proved it, and its arguments. type EngineCall struct { Verb string Caller string Args map[string]any } // EngineFunc answers one call: its result, or why it is refused. type EngineFunc func(EngineCall) (any, error) // CallerOfSubject is the caller a call's subject names for this node's verb: the last token of a caller-named subject, // each `~` a dot, or stdio.Unproven on the subject that names none or on any other. func CallerOfSubject(subject, verb, node string) string { token, ok := strings.CutPrefix(subject, "mesh.seat."+EngineSeat+".call."+verb+"."+node+".") if !ok || token == "" || strings.ContainsAny(token, ".*> ") { return stdio.Unproven } return strings.ReplaceAll(token, "~", ".") } // MaxEngineCall bounds a call's arguments: a path, an access and a few words. const MaxEngineCall = 16 << 10 // AnswerEngineCall is the envelope one call is answered with: the verb's result, or its refusal in words. func AnswerEngineCall(handle EngineFunc, verb, node, subject string, body []byte) []byte { envelope := map[string]any{"node": node} say := func() []byte { raw, err := json.Marshal(envelope) if err != nil { raw, _ = json.Marshal(map[string]any{"node": node, "error": "the answer could not be written: " + err.Error()}) } return raw } args := map[string]any{} switch { case handle == nil: envelope["error"] = "this engine serves no verb of " + EngineSeat return say() case len(body) > MaxEngineCall: envelope["error"] = fmt.Sprintf("the call is %d bytes, more than a call of %s takes", len(body), EngineSeat) return say() case len(strings.TrimSpace(string(body))) > 0: if err := json.Unmarshal(body, &args); err != nil { envelope["error"] = "the call's arguments are not an object: " + err.Error() return say() } } result, err := handle(EngineCall{Verb: verb, Caller: CallerOfSubject(subject, verb, node), Args: args}) if err != nil { envelope["error"] = err.Error() } else { envelope["result"] = result } return say() } // EngineSeats is a link that serves this node's seat. Optional, as Asked is. type EngineSeats interface { ServeEngineSeat(EngineFunc) } // ServeEngineSeat answers every verb of the seat on this node's subjects, as the service node-engine, which says each // verb's seat, tool and node to discovery (novox/hq ADR 0197). A bus whose user list is older than the grant refuses the // subscriptions, said by the client; a caller then hears nobody answer. func (l *natsLink) ServeEngineSeat(handle EngineFunc) { if handle == nil || l.conn == nil { return } svc, err := micro.AddService(l.conn, micro.Config{Name: EngineSeat, Version: "1.0.0", Description: "the node seat node-engine of " + l.node + " (novox/hq ADR 0563)", QueueGroupDisabled: true, Metadata: map[string]string{"seat": EngineSeat, "scope": "node", "node": l.node}}) if err != nil { return } apart := make(chan struct{}, apartAtOnce) for _, v := range catalogue.EngineVerbs() { verb := v.Name answer := EngineAnswer(handle, verb, l.node, apart) meta := EngineEndpointMetadata(v, l.node) _ = svc.AddEndpoint(verb, answer, micro.WithEndpointSubject(EngineToolSubject(verb, l.node)), micro.WithEndpointMetadata(meta)) _ = svc.AddEndpoint(verb+"-called", answer, micro.WithEndpointSubject(EngineCallPattern(verb, l.node)), micro.WithEndpointMetadata(meta)) } l.engine = svc } // answeredApart are the verbs answered off the subscription that delivered them: each endpoint's subscription hands its // asks over one at a time, so a second ask of a verb that takes long waited for the first, and its bound counted from // when the first ended (issue 1126, review of 2026-11-03: two checks asking compose-candidate of one node at once, the // second answered at about 100 s, past the bus's one-minute reply window). Each is answered in a goroutine of its own, // its bound counted from its arrival. var answeredApart = map[string]bool{ActComposeCandidate: true} // apartAtOnce bounds the asks answered apart at once on this node; one more is refused in words at once. const apartAtOnce = 4 // EngineAnswer is the handler of one verb: answered on the subscription's goroutine, or, for a verb answered apart, in // its own while fewer than apart's capacity run, and refused at once past it. func EngineAnswer(handle EngineFunc, verb, node string, apart chan struct{}) micro.HandlerFunc { return func(r micro.Request) { if !answeredApart[verb] { _ = r.Respond(AnswerEngineCall(handle, verb, node, r.Subject(), r.Data())) return } select { case apart <- struct{}{}: default: raw, _ := json.Marshal(map[string]any{"node": node, "error": fmt.Sprintf( "this node answers %d asks of %s at once now: ask again when one is answered", cap(apart), verb)}) _ = r.Respond(raw) return } subject, data := r.Subject(), r.Data() go func() { defer func() { <-apart }() _ = r.Respond(AnswerEngineCall(handle, verb, node, subject, data)) }() } } // EngineModule is the module whose code answers the seat, as discovery names it: the node-engine's manifest's. const EngineModule = "mesh-host" // EngineEndpointMetadata is what discovery reads of one verb's endpoint, in the shape every tool runner and the // controller announce a seat's verb in (mesh-tools node-tools' announce package): its kind, the seat, its scope, the // module whose code answers, the verb, the node, its description and its schema. Without the kind and the scope the // console listed none of it, and `/node-engine.` answered that nothing in the mesh is called node-engine // (the sweep of 2026-10-30). func EngineEndpointMetadata(v catalogue.Verb, node string) map[string]string { schema, err := json.Marshal(v.Input) if err != nil || v.Input == nil { schema = []byte("{}") } return map[string]string{"kind": "seat", "seat": EngineSeat, "scope": "node", "module": EngineModule, "tool": v.Name, "node": node, "interchangeable": "false", "description": v.Description, "schema": string(schema)} } // SayEngine says one of the seat's events about this node, on the events stream. func (l *natsLink) SayEngine(event string, body any) error { raw, err := json.Marshal(body) if err != nil { return err } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() _, err = l.js.Publish(EngineEventSubject(event, l.node), raw, nats.Context(ctx)) return err } // The engine asks the operator under its own asker name (asks.EngineAsker) on the router's seat (ADR 0259 §3, as ADR // 0563 §3 changes it): its ask and cancel on the seat's work stream, its warrants heard on its own decided subject, and // its asks' state read from the router's records under its own name. // RouterRecords is the router's bucket of asks, which each asker reads under its own name: the state `asks` of the // operator-channel's holder, the messenger (mesh-catalog modules/messenger). const RouterRecords = "messenger_asks" // OperatorAsks is a link that asks the operator as this node's engine. Optional. type OperatorAsks interface { PublishAsk(subject string, body any, id string) error RouterRecord(key string) (*stdio.StateEntry, error) HearWarrants(asker string, heard func(asks.Warrant)) error } // PublishAsk publishes an ask or a cancel on the router's seat, once per id within the stream's duplicate window. func (l *natsLink) PublishAsk(subject string, body any, id string) error { raw, err := json.Marshal(body) if err != nil { return err } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() _, err = l.js.Publish(subject, raw, nats.MsgId(id), nats.Context(ctx)) return err } // RouterRecord reads one key of the router's records, `.`; nil when it holds none. func (l *natsLink) RouterRecord(key string) (*stdio.StateEntry, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() msg, err := l.conn.RequestWithContext(ctx, "$JS.API.DIRECT.GET.KV_"+RouterRecords+".$KV."+RouterRecords+"."+key, nil) if err != nil { return nil, err } if status := msg.Header.Get("Status"); status == "404" { return nil, nil } else if status != "" { return nil, fmt.Errorf("the router's record %s could not be read: %s %s", key, status, msg.Header.Get("Description")) } return &stdio.StateEntry{Key: key, Value: msg.Data}, nil } // HearWarrants hears the warrants for asker on its decided subject, each handed to heard as it arrives. func (l *natsLink) HearWarrants(asker string, heard func(asks.Warrant)) error { sub, err := l.conn.Subscribe(asks.DecidedSubject(asker), func(msg *nats.Msg) { var w asks.Warrant if json.Unmarshal(msg.Data, &w) == nil { heard(w) } }) if err != nil { return err } l.warrants = sub return nil } // errNoAsking is a link that cannot ask the operator. var errNoAsking = errors.New("the link open now cannot ask the operator") // discoveryQuiet is how long the engine waits after the last answer to the bus's discovery, and discoveryMost the // longest it waits in all. const ( discoveryQuiet = 300 * time.Millisecond discoveryMost = 2 * time.Second ) // HeardServed asks the bus's discovery (novox/hq ADR 0197) whether a seat's verb is answered on this machine: an // endpoint naming the seat, the verb as its tool and this node. func (l *natsLink) HeardServed(seat, verb string) (bool, error) { inbox := l.conn.NewRespInbox() sub, err := l.conn.SubscribeSync(inbox) if err != nil { return false, err } defer func() { _ = sub.Unsubscribe() }() if err := l.conn.PublishRequest("$SRV.INFO", inbox, nil); err != nil { return false, fmt.Errorf("asking the bus who serves what: %w", err) } heard := false deadline := time.Now().Add(discoveryMost) for time.Now().Before(deadline) { msg, err := sub.NextMsg(discoveryQuiet) if err != nil { break } var info micro.Info if json.Unmarshal(msg.Data, &info) != nil { continue } for _, e := range info.Endpoints { node := e.Metadata["node"] if node == "" { node = info.ID } if e.Metadata["seat"] == seat && e.Metadata["tool"] == verb && node == l.node { heard = true } } } return heard, nil } // HeardServed is the link open now asking the bus's discovery whether a seat's verb is answered on this machine. func (q *Session) HeardServed(seat, verb string) (bool, error) { q.init() q.mu.Lock() bus := q.bus q.mu.Unlock() asker, ok := bus.(interface { HeardServed(string, string) (bool, error) }) if bus == nil || !ok { return false, fmt.Errorf("%w: this machine has no link to the bus open now", ErrNotAsked) } return asker.HeardServed(seat, verb) } // ---- the engine's acts in the bus's own storage (ADR 0563 §2, §3) ------------------------------------------------- // actsStore is this node's keys of ActsBucket over the link open now: bound, never created (the grants part creates // it, as the node-engine's declared state). func (l *natsLink) actsStore() (nats.KeyValue, error) { kv, err := l.js.KeyValue(ActsBucket) if err != nil { return nil, fmt.Errorf("the node-engine's state %s cannot be bound (it is created from the node-engine's "+ "manifest; asked again once it is): %w", ActsBucket, err) } return kv, nil } // BusState is the engine's ActStore: its own node's keys of ActsBucket, `.`, over whichever link the session // has open; with none open every read and write is refused, and nothing is kept anywhere else. type BusState struct { Session *Session Node string } func (b BusState) kv() (nats.KeyValue, error) { b.Session.init() b.Session.mu.Lock() bus := b.Session.bus b.Session.mu.Unlock() l, ok := bus.(interface{ actsStore() (nats.KeyValue, error) }) if bus == nil || !ok { return nil, fmt.Errorf("%w: this machine has no link to the bus open now, where its acts are kept", ErrNotAsked) } return l.actsStore() } func (b BusState) key(k string) string { return b.Node + "." + k } // changed is a write that lost its compare-and-set, as stdio.ErrChanged. func changed(err error) error { var api *nats.APIError if errors.Is(err, nats.ErrKeyExists) || (errors.As(err, &api) && api.ErrorCode == nats.JSErrCodeStreamWrongLastSequence) { return stdio.ErrChanged } return err } func (b BusState) Get(key string) (*stdio.StateEntry, error) { kv, err := b.kv() if err != nil { return nil, err } e, err := kv.Get(b.key(key)) if errors.Is(err, nats.ErrKeyNotFound) { return nil, nil } if err != nil { return nil, err } return &stdio.StateEntry{Key: key, Value: e.Value(), Revision: e.Revision()}, nil } func (b BusState) Create(key string, value any) (uint64, error) { kv, err := b.kv() if err != nil { return 0, err } raw, err := json.Marshal(value) if err != nil { return 0, err } rev, err := kv.Create(b.key(key), raw) return rev, changed(err) } func (b BusState) Update(key string, value any, revision uint64) (uint64, error) { kv, err := b.kv() if err != nil { return 0, err } raw, err := json.Marshal(value) if err != nil { return 0, err } rev, err := kv.Update(b.key(key), raw, revision) return rev, changed(err) } func (b BusState) Delete(key string) error { kv, err := b.kv() if err != nil { return err } return kv.Delete(b.key(key)) } // Keys are this node's keys under prefix, without the node: a watch of `.>` alone, the one filter the node's // grant names, its values left out. func (b BusState) Keys(prefix string) ([]string, error) { kv, err := b.kv() if err != nil { return nil, err } w, err := kv.Watch(b.Node+".>", nats.MetaOnly(), nats.IgnoreDeletes()) if err != nil { return nil, err } defer func() { _ = w.Stop() }() var out []string for { select { case e := <-w.Updates(): if e == nil { sort.Strings(out) return out, nil } if k := strings.TrimPrefix(e.Key(), b.Node+"."); strings.HasPrefix(k, prefix) { out = append(out, k) } case <-time.After(10 * time.Second): return nil, errors.New("the node-engine's acts could not be listed within 10s") } } }