The seams were there and nothing chose a side: serve, push, ask and build all opened the old bus's connection and declared over its channel, whatever MESH_BUS_NATS said. So the switch moved every host and left the control plane unable to follow — "this control plane has no MESH_BROKER_AMQP" with the new bus named and standing (2026-09-28). That was task 4.3 of design 28, still open. One place now decides: connectLink reads the switch, refuses both buses named at once, raises the new bus's streams and this controller's consumers when it is handed the inventory, and opens the link over whichever bus it is on. Every caller that sent a declaration or asked a tool through the old channel goes through the server's bus instead, which the new transport has and the channel is not. OverNats is that outbound: a declaration is a JetStream publish into the node's own subject, an event is announced on the subject its name derives to, a tool is request and reply on the module's tool subject.
63 lines
1.9 KiB
Go
63 lines
1.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// ask calls one of a module's tools, through the control plane's own broker connection.
|
|
//
|
|
// A module serves tools under an account scoped to exactly that (novox/hq ADR 0047), and nothing
|
|
// else in the mesh held an account that could ask one — not an operator at a terminal, not an agent
|
|
// acting for one (novox/hq 04-ISSUES/049). The control plane does, so it is the way in: one
|
|
// process, one connection, one place a question can be seen to have been asked (ADR 0095).
|
|
func askCommand(ctx context.Context, args []string) error {
|
|
positionals, flags := split(args)
|
|
set := flag.NewFlagSet("ask", flag.ContinueOnError)
|
|
wait := set.Duration("wait", 60*time.Second, "how long to wait for the module's answer")
|
|
if err := set.Parse(flags); err != nil {
|
|
return err
|
|
}
|
|
if len(positionals) < 2 || len(positionals) > 3 {
|
|
return errors.New("ask <module> <tool> [json arguments] [--wait 60s]")
|
|
}
|
|
module, tool := positionals[0], positionals[1]
|
|
var arguments json.RawMessage
|
|
if len(positionals) == 3 {
|
|
if !json.Valid([]byte(positionals[2])) {
|
|
return fmt.Errorf("the arguments are not JSON: %s", positionals[2])
|
|
}
|
|
arguments = json.RawMessage(positionals[2])
|
|
}
|
|
|
|
server, err := connectLink(ctx, nil, nil, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer server.Close()
|
|
|
|
answer, err := link.Ask(ctx, server.Channel(), module, tool, arguments, *wait)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// The answer as the module gave it, to standard output, for a person or a program. A tool
|
|
// that answered with an error has still answered: printed the same way, and the exit status
|
|
// says which.
|
|
body, err := json.Marshal(answer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintln(os.Stdout, string(body))
|
|
if answer.Error != "" {
|
|
return fmt.Errorf("%s.%s answered with an error: %s", module, tool, answer.Error)
|
|
}
|
|
return nil
|
|
}
|