From f41d280e66fcac49ee299b615393d5a134150be8 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 4 Sep 2026 01:33:03 +0200 Subject: [PATCH] =?UTF-8?q?cli:=20module=20issue=20=E2=80=94=20deliver=20a?= =?UTF-8?q?=20module=20its=20scoped=20broker=20account=20(ADR=200048)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 'module issue --node ' 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 --- cmd/mesh-control/main.go | 1 + cmd/mesh-control/modules.go | 77 ++++++++++++++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/cmd/mesh-control/main.go b/cmd/mesh-control/main.go index 43a58d6..613f709 100644 --- a/cmd/mesh-control/main.go +++ b/cmd/mesh-control/main.go @@ -135,6 +135,7 @@ func usage() { module list what modules this mesh knows about module moved the source has a newer commit than the mesh built module forget remove one, unless a node is running it + module issue --node 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 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 diff --git a/cmd/mesh-control/modules.go b/cmd/mesh-control/modules.go index cfa81cd..837b26e 100644 --- a/cmd/mesh-control/modules.go +++ b/cmd/mesh-control/modules.go @@ -2,6 +2,8 @@ package main import ( "context" + "crypto/rand" + "encoding/base64" "encoding/json" "errors" "flag" @@ -10,6 +12,7 @@ import ( "sort" "strings" + "github.com/novox/mesh-control/internal/broker" "github.com/novox/mesh-control/internal/catalogue" "github.com/novox/mesh-control/internal/inventory" "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]) 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 --node ") + } + 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: - 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]) } }