cli: module issue — deliver a module its scoped broker account (ADR 0048)

'module issue <module> --node <m>' looks up the module's emits/consumes from
the catalogue, ensures the bus exchanges exist, creates its scoped account
(CreateModuleAccount), and seals an amqps {url,fingerprint} to the node as the
module's broker own-secret — the same delivery as 'builder issue', now generic.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
2026-09-04 01:33:03 +02:00
parent 47bbb0cca6
commit f41d280e66
2 changed files with 77 additions and 1 deletions
+1
View File
@@ -135,6 +135,7 @@ func usage() {
module list what modules this mesh knows about module list what modules this mesh knows about
module moved <name> <commit> the source has a newer commit than the mesh built module moved <name> <commit> the source has a newer commit than the mesh built
module forget <name> remove one, unless a node is running it module forget <name> remove one, unless a node is running it
module issue <name> --node <m> a broker account for a module, scoped to its emits and consumes
status [--json] what is wrong, what is quiet, and what is out of date status [--json] what is wrong, what is quiet, and what is out of date
board [--listen ADDR] the same three questions, as a page that holds nothing board [--listen ADDR] the same three questions, as a page that holds nothing
api --issuer URL [--listen A] assign and unassign over http, for a surface that is not here api --issuer URL [--listen A] assign and unassign over http, for a surface that is not here
+76 -1
View File
@@ -2,6 +2,8 @@ package main
import ( import (
"context" "context"
"crypto/rand"
"encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"flag" "flag"
@@ -10,6 +12,7 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/novox/mesh-control/internal/broker"
"github.com/novox/mesh-control/internal/catalogue" "github.com/novox/mesh-control/internal/catalogue"
"github.com/novox/mesh-control/internal/inventory" "github.com/novox/mesh-control/internal/inventory"
"github.com/novox/mesh-control/internal/overlay" "github.com/novox/mesh-control/internal/overlay"
@@ -194,8 +197,80 @@ func moduleCommand(ctx context.Context, args []string) error {
fmt.Printf("%s forgotten\n", args[1]) fmt.Printf("%s forgotten\n", args[1])
return nil return nil
case "issue":
// A module's broker account, scoped by its emits and consumes (novox/hq ADR 0048) and
// sealed to the machine that will run it — the generic case the builder was the first of.
set := flag.NewFlagSet("module issue", flag.ContinueOnError)
forNode := set.String("node", "",
"the machine that will run it, so the credential is delivered instead of printed")
positionals, err := parseAround(set, args[1:])
if err != nil {
return err
}
if len(positionals) != 1 {
return errors.New("module issue <module> --node <machine>")
}
module := positionals[0]
if *forNode == "" {
return errors.New("module issue needs --node: a module's account is sealed to the " +
"machine that runs it, and the mesh cannot read it back to print")
}
shelf, err := inv.Catalogue(ctx)
if err != nil {
return err
}
m, ok := shelf[module]
if !ok {
return fmt.Errorf("this mesh knows no module %q; `module add` it first", module)
}
management, err := broker.ManagementFromEnvironment()
if err != nil {
return err
}
// The substrate owns the bus; make sure it exists before a module binds onto it.
if err := management.EnsureEventExchanges(ctx); err != nil {
return err
}
secret := make([]byte, 32)
if _, err := rand.Read(secret); err != nil {
return err
}
password := base64.RawURLEncoding.EncodeToString(secret)
account, err := management.CreateModuleAccount(ctx, *forNode, module, password, m.Emits, m.Consumes)
if err != nil {
return err
}
known, err := broker.FromEnvironment()
if err != nil {
return fmt.Errorf("cannot deliver a credential without knowing where the broker is: %w", err)
}
// The URL and what verifies the broker, together — a mesh's broker presents its own
// certificate, in no public trust store, so a URL alone fails at TLS (as `builder issue`).
held, err := json.Marshal(struct {
URL string `json:"url"`
Fingerprint string `json:"fingerprint,omitempty"`
}{
URL: fmt.Sprintf("amqps://%s:%s@%s/", account, password, known.Address),
Fingerprint: known.Fingerprint,
})
if err != nil {
return err
}
if err := inv.AcceptSecretForModule(ctx, *forNode, module, "broker", string(held)); err != nil {
return err
}
fmt.Printf("broker account %s created for %s, scoped to what it emits and consumes\n",
account, module)
fmt.Printf(" sealed to %s. It arrives with the next push — `push %s` to send it\n",
*forNode, *forNode)
return nil
default: default:
return fmt.Errorf("module has no %q; it has add, list, moved and forget", args[0]) return fmt.Errorf("module has no %q; it has add, list, moved, forget and issue", args[0])
} }
} }