diff --git a/go/compose/input.go b/go/compose/input.go index d8f773d..a5d5c7d 100644 --- a/go/compose/input.go +++ b/go/compose/input.go @@ -247,7 +247,8 @@ type Licence struct { Serves map[string]any Holders []Holder // Keys is, by holder, the licence's key sealed to that holder's machine (held.KeyFor), empty where the mesh holds none: - // the plaintext was discarded when it was supplied. Optional; the composition needs no value of it. + // the plaintext was discarded when it was supplied. Optional: nil means not given, and the composition uses a stand-in named + // foreseen; given (even empty), a holder with no entry is one the mesh holds no key for. Keys map[Holder]string // Manager is the holder of a refreshable-grant licence that manages it (held.ManagerOf); the zero Holder when none. Manager Holder diff --git a/go/compose/resolve.go b/go/compose/resolve.go index 9154f45..8665086 100644 --- a/go/compose/resolve.go +++ b/go/compose/resolve.go @@ -56,7 +56,14 @@ func (c *composer) planFor(nodeName string) (Resolved, error) { if n.ByRecord { // Answered by something the mesh holds, so there is no pair-wise secret between two machines. licence, _ := c.licence(n.From) - resolved.Needs[i].Sealed = licence.Keys[Holder{Node: nodeName, Module: n.For}] + if licence.Keys == nil { + // The keys are not given: composed with a stand-in and named, not failed for a value only a send reads. + resolved.Needs[i].Sealed = catalogue.ForeseenSealed + out.Foreseen = append(out.Foreseen, fmt.Sprintf("%s on %s needs %s from %s", n.For, nodeName, n.Name, n.From)) + } else { + // Given, and none for this holder: the mesh holds none. + resolved.Needs[i].Sealed = licence.Keys[Holder{Node: nodeName, Module: n.For}] + } // If this holder is the licence's manager, hand it the manager node's PUBLIC sealing key in its bound facts // (novox/hq ADR 0050). if pub := c.managerPublicKeyFor(licence, nodeName, n.For); pub != "" { diff --git a/go/compose/resolve_test.go b/go/compose/resolve_test.go index f33eab4..ef250af 100644 --- a/go/compose/resolve_test.go +++ b/go/compose/resolve_test.go @@ -325,3 +325,36 @@ func TestSettingsAreReturnedForEveryResolvedModuleIncludingOneThatWasPulledIn(t t.Error("the settings returned share the Input's layers") } } + +// Round 1, finding 3: a licence key the Input does not carry (Keys not given) is composed with a stand-in and named foreseen, so the +// node still composes; Keys given that lack the holder is the mesh holding none, and the need stays empty. +func TestALicenceKeyTheInputDoesNotCarryIsAStandInAndNamedForeseen(t *testing.T) { + in := aMesh(t) + in.Catalogue["assistant"] = catalogue.Manifest{Module: "assistant", Version: "1", Requires: []string{LicenceProvision}} + laptop := nodeOf(t, &in, "laptop") + laptop.Assigned = append(laptop.Assigned, "assistant") + in.Licences = []Licence{{Name: "personal", Serves: map[string]any{"model": "x"}, Holders: []Holder{{Node: "laptop", Module: "assistant"}}}} + got, err := Resolve(in, "laptop") + if err != nil { + t.Fatal(err) + } + if n := need(t, got.Plan, "assistant", LicenceProvision); n.Sealed != catalogue.ForeseenSealed { + t.Errorf("a key not given is composed as %q", n.Sealed) + } + if len(got.Foreseen) != 1 || !strings.Contains(got.Foreseen[0], "assistant on laptop needs model-access") { + t.Errorf("foreseen %v", got.Foreseen) + } + // And a whole node composes with it. + if _, problems, err := Compose(in, "laptop"); err != nil || len(problems) != 0 { + t.Errorf("a node using a licence whose key is not carried: %v %v", err, problems) + } + // Keys given, and none for this holder: the mesh holds none. + in.Licences[0].Keys = map[Holder]string{} + got, err = Resolve(in, "laptop") + if err != nil { + t.Fatal(err) + } + if n := need(t, got.Plan, "assistant", LicenceProvision); n.Sealed != "" || len(got.Foreseen) != 0 { + t.Errorf("a key the mesh holds none of: %q %v", n.Sealed, got.Foreseen) + } +}