A tool's grant covers the machine-addressed subject, and a credential names the seats its module claims (ADR 0159) #185

Merged
mesh-admin merged 2 commits from feat/a-tool-call-names-the-machine into main 2026-10-01 12:01:29 +00:00
3 changed files with 66 additions and 8 deletions
+49 -7
View File
@@ -584,16 +584,25 @@ func issueOnTheNewBus(ctx context.Context, inv *inventory.Inventory, m catalogue
// (`rollout mint`, design 28 task 5.2) rather than the one in this process's environment. // (`rollout mint`, design 28 task 5.2) rather than the one in this process's environment.
func issueWith(ctx context.Context, inv *inventory.Inventory, m catalogue.Manifest, func issueWith(ctx context.Context, inv *inventory.Inventory, m catalogue.Manifest,
node, busAddress string, known broker.Broker, reachable, user, password string) error { node, busAddress string, known broker.Broker, reachable, user, password string) error {
// The seats this module claims, with the verbs each promises (novox/hq ADR 0159): the runtime
// serves a claimed seat's verbs with its tools of the same name, and the bus admits only the
// holder's subscription — so the runtime tries each claim and the grant decides. Written here
// because this file is the one thing the mesh writes that the runtime reads before it speaks.
claims, err := claimsFor(ctx, inv, m)
if err != nil {
return err
}
held, err := json.Marshal(struct { held, err := json.Marshal(struct {
URL string `json:"url"` URL string `json:"url"`
Fingerprint string `json:"fingerprint,omitempty"` Fingerprint string `json:"fingerprint,omitempty"`
Node string `json:"node"` Node string `json:"node"`
Module string `json:"module"` Module string `json:"module"`
User string `json:"user"` User string `json:"user"`
Password string `json:"password"` Password string `json:"password"`
Claims []seatClaimed `json:"claims,omitempty"`
}{ }{
URL: "nats://" + reachable, Fingerprint: known.Fingerprint, URL: "nats://" + reachable, Fingerprint: known.Fingerprint,
Node: node, Module: m.Module, User: user, Password: password, Node: node, Module: m.Module, User: user, Password: password, Claims: claims,
}) })
if err != nil { if err != nil {
return err return err
@@ -680,3 +689,36 @@ func namesNoInstallation(m catalogue.Manifest) error {
"on purpose under %s with its reason, or take it out:\n - %s", "on purpose under %s with its reason, or take it out:\n - %s",
m.Module, catalogue.NamesOnPurpose, strings.Join(named, "\n - ")) m.Module, catalogue.NamesOnPurpose, strings.Join(named, "\n - "))
} }
// seatClaimed is one seat a module claims, as its runtime needs it: the name, the scope (a
// node-scoped seat's verb carries the machine, design 33 §4) and the verbs the seat promises.
type seatClaimed struct {
Seat string `json:"seat"`
Scope string `json:"scope"`
Serves []string `json:"serves,omitempty"`
}
// claimsFor joins a module's claims with the seats' protocols from the mesh's records.
func claimsFor(ctx context.Context, inv *inventory.Inventory, m catalogue.Manifest) ([]seatClaimed, error) {
if len(m.Claims) == 0 {
return nil, nil
}
seats, err := inv.Seats(ctx)
if err != nil {
return nil, err
}
byName := map[string]catalogue.Seat{}
for _, s := range seats {
byName[s.Name] = s
}
var out []seatClaimed
for _, c := range m.Claims {
claimed := seatClaimed{Seat: c.Name, Scope: c.At()}
if s, known := byName[c.Name]; known {
claimed.Scope = s.Scope
claimed.Serves = catalogue.VerbNames(s.Serves)
}
out = append(out, claimed)
}
return out, nil
}
+13
View File
@@ -91,3 +91,16 @@ func TestADeclaredInvokeReachesTheComposedUser(t *testing.T) {
} }
has(t, perms.Publish, "mesh.mod.*.tool.>") has(t, perms.Publish, "mesh.mod.*.tool.>")
} }
// A module's tool is addressed two ways (novox/hq ADR 0159): to whichever instance answers, and to
// the instance on one machine. A grant for the tool covers both and nothing wider.
func TestInvokingAToolMayAddressTheMachineToo(t *testing.T) {
got, err := invokedSubjects([]string{"postgres.postgres_query"})
if err != nil {
t.Fatal(err)
}
want := []string{"mesh.mod.postgres.tool.postgres_query", "mesh.mod.postgres.tool.postgres_query.*"}
if len(got) != 2 || got[0] != want[0] || got[1] != want[1] {
t.Fatalf("the grant is %v, want %v", got, want)
}
}
+4 -1
View File
@@ -660,7 +660,10 @@ func invokedSubjects(invokes []string) ([]string, error) {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"%q does not name a tool: one invokes <module>.<tool>, seat:<seat>.<verb>, or * for every one", t) "%q does not name a tool: one invokes <module>.<tool>, seat:<seat>.<verb>, or * for every one", t)
} }
out = append(out, "mesh.mod."+module+".tool."+tool) // Both ways a module's tool is addressed (novox/hq ADR 0159): to whichever instance
// answers, and to the instance on one machine, which is the same subject with the machine
// as its last token.
out = append(out, "mesh.mod."+module+".tool."+tool, "mesh.mod."+module+".tool."+tool+".*")
} }
return out, nil return out, nil
} }