diff --git a/go/catalogue/handed_providers_test.go b/go/catalogue/handed_providers_test.go index 22744f8..6541c74 100644 --- a/go/catalogue/handed_providers_test.go +++ b/go/catalogue/handed_providers_test.go @@ -42,9 +42,10 @@ func consumerOn(t *testing.T, m Manifest, w World) Resolution { node := workstation() node.At = "workstation.internal" stores := shelf(m, - Manifest{Module: "store", Version: "1", Provides: []Offer{{Name: "postgres-database", Scope: ScopeMesh}}, + Manifest{Module: "store", Version: "1", Provides: []Offer{{Name: "postgres-database", Scope: ScopeMesh, + Credential: &OfferCredential{Form: CredentialFormPair}}}, Serves: map[string]map[string]any{"postgres-database": {"port": 5432}}}, - Manifest{Module: "store-two", Version: "1", Provides: []Offer{{Name: "postgres-database", Scope: ScopeMesh}}, + Manifest{Module: "store-two", Version: "1", Provides: []Offer{{Name: "postgres-database", Scope: ScopeMesh, Credential: &OfferCredential{Form: CredentialFormPair}}}, Serves: map[string]map[string]any{"postgres-database": {"port": 5434}}}) r, err := Resolve(stores, []string{m.Module}, node, w) if err != nil { @@ -130,7 +131,10 @@ func TestAHandedModulesPlanNamesTheCredentialFormOfItsProvidersOffer(t *testing. return mustParse(t, raw+`}`) } for credential, want := range map[string]string{ - ``: CredentialFormPair, + // No default (the lead, 2026-10-29): an offer that says nothing has no form, and the catalogue refuses it. + ``: "", + `,"credential":{"form":"pair"}`: CredentialFormPair, + `,"credential":{"form":"none"}`: CredentialFormNone, `,"credential":{"own":"admin"}`: CredentialFormShared, `,"credential":{"own":"token"}`: CredentialFormGiven, `,"credential":{"issued-by":"outside"}`: CredentialFormGiven, @@ -153,6 +157,18 @@ func TestAHandedModulesPlanNamesTheCredentialFormOfItsProvidersOffer(t *testing. if got := (Manifest{Module: "x"}).CredentialFormOf("postgres-database"); got != "" { t.Errorf("a module offering nothing has the credential form %q", got) } + // The catalogue refuses an offer saying no form, and a form beside an own secret or not pair or none. + for credential, refused := range map[string]string{``: "says no credential form", `,"credential":{}`: "says no credential form", + `,"credential":{"form":"pair","own":"admin"}`: "beside an own secret", `,"credential":{"form":"maybe"}`: `not "pair" or "none"`, + `,"credential":{"form":"none"}`: ""} { + raw := `{"module":"store","version":"1","provides":[{"name":"postgres-database","scope":"mesh"` + credential + `}]}` + var m Manifest + _ = json.Unmarshal([]byte(raw), &m) + problems := strings.Join(CredentialFormProblems(m), "; ") + if (refused == "") != (problems == "") || !strings.Contains(problems, refused) { + t.Errorf("%q: %q, want %q", credential, problems, refused) + } + } } // A consumer's plan entry carries what its manifest contributes to that provision (issue 776), so the provider's diff --git a/go/catalogue/manifest.go b/go/catalogue/manifest.go index 0be481b..1d5242f 100644 --- a/go/catalogue/manifest.go +++ b/go/catalogue/manifest.go @@ -287,6 +287,10 @@ type OfferCredential struct { Own string `json:"own,omitempty"` IssuedBy string `json:"issued-by,omitempty"` Tool string `json:"tool,omitempty"` + // Form says, for an offer that names no own secret and no issuer, which it is: CredentialFormPair, a login of + // each consumer's own, or CredentialFormNone, no credential at all (the lead, 2026-10-29, for novox/hq ADR 0570): + // never left to a default. + Form string `json:"form,omitempty"` } // IssuedBySoftware says a provision's credential is issued by its provider's software (novox/hq ADR 0305). @@ -362,6 +366,9 @@ const ( // CredentialFormGiven is given by a person (novox/hq ADR 0337): the offer's credential, or the own secret it // shares, is issued outside the mesh; the mesh never makes it. CredentialFormGiven = "given" + // CredentialFormNone is an offer whose consumers present no credential: its outcome is the provider's have, and + // the vault mints nothing for it. + CredentialFormNone = "none" ) // CredentialFormOf is the credential form this module's offer of a provision declares, read from the offer's @@ -375,7 +382,7 @@ func (m Manifest) CredentialFormOf(provision string) string { } switch c := o.Credential; { case c == nil: - return CredentialFormPair + return "" // undeclared: refused at the catalogue (CredentialFormProblems), never read as a pair case c.IssuedBy == IssuedOutside: return CredentialFormGiven case c.IssuedBy == IssuedBySoftware: @@ -385,11 +392,33 @@ func (m Manifest) CredentialFormOf(provision string) string { case c.Own != "": return CredentialFormShared } - return CredentialFormPair + return o.Credential.Form } return "" } +// CredentialFormProblems refuses an offer that does not say its credential's form (the lead, 2026-10-29): an own +// secret, an issuer, or `form` pair or none, and `form` only where neither of the others is said. +func CredentialFormProblems(m Manifest) []string { + var problems []string + for _, o := range m.Provides { + c := o.Credential + switch { + case c == nil || (c.Own == "" && c.IssuedBy == "" && c.Form == ""): + problems = append(problems, fmt.Sprintf("%s provides %q and says no credential form: say `credential` with "+ + "`form` pair (a login of each consumer's own) or none (no credential), or the own secret or issuer it is", + m.Module, o.Name)) + case c.Form != "" && (c.Own != "" || c.IssuedBy != ""): + problems = append(problems, fmt.Sprintf("%s provides %q with `form` beside an own secret or an issuer; `form` "+ + "says only pair or none", m.Module, o.Name)) + case c.Form != "" && c.Form != CredentialFormPair && c.Form != CredentialFormNone: + problems = append(problems, fmt.Sprintf("%s provides %q with the form %q, not %q or %q", m.Module, o.Name, + c.Form, CredentialFormPair, CredentialFormNone)) + } + } + return problems +} + // ProvisionsSharing is every provision of this module whose credential is the named own secret. func (m Manifest) ProvisionsSharing(own string) []string { var out []string @@ -1995,6 +2024,8 @@ func parseManifest(raw []byte, predefined PredefinedList) (Manifest, error) { } else if offer.Credential != nil { own, declared := m.OwnSecrets[offer.Credential.Own] switch { + case offer.Credential.Own == "" && offer.Credential.Form != "": + // The form alone, pair or none (CredentialFormProblems judges it). case offer.Credential.Own == "": problems = append(problems, fmt.Sprintf( "%s provides %q with a credential that names no own secret", m.Module, p)) diff --git a/go/catalogue/seats_declared.go b/go/catalogue/seats_declared.go index 9165103..90087e2 100644 --- a/go/catalogue/seats_declared.go +++ b/go/catalogue/seats_declared.go @@ -282,6 +282,10 @@ func CatalogueProblems(shelf Shelf) []string { problems = append(problems, RestrictedHearingProblems(shelf)...) // A work-queue stream is never asked for by another module's consumes (novox/hq ADR 0436 §6). problems = append(problems, WorkQueueConsumedElsewhereProblems(shelf)...) + // Every offer says its credential's form, never left to a default (the lead, 2026-10-29, for novox/hq ADR 0570). + for _, module := range shelfOrder(shelf) { + problems = append(problems, CredentialFormProblems(shelf[module])...) + } // Who declares what, and who declared it first. declaredBy := map[string]string{} diff --git a/go/catalogue/seats_declared_test.go b/go/catalogue/seats_declared_test.go index 33b3ba6..25a8d0b 100644 --- a/go/catalogue/seats_declared_test.go +++ b/go/catalogue/seats_declared_test.go @@ -109,7 +109,7 @@ func TestAClaimMustMatchTheDeclaredScope(t *testing.T) { func TestTheMeshsOwnSeatsAreStillClaimable(t *testing.T) { // It delivers the bus, so its holder provides the bus — the rule this check now enforces. m := Manifest{Module: "nats", - Provides: []Offer{{Name: "mesh-bus", Scope: ScopeMesh}}, + Provides: []Offer{{Name: "mesh-bus", Scope: ScopeMesh, Credential: &OfferCredential{Form: CredentialFormNone}}}, Claims: []Claim{{Name: "mesh-broker", Scope: ScopeMesh}}} if got := problemsFor(t, Shelf{"nats": m}); got != "" { t.Fatalf("a mesh seat was refused by the derived check: %s", got) @@ -186,7 +186,7 @@ func TestTheParserDoesNotJudgeWhatOnlyTheStoreKnows(t *testing.T) { // A store whose bus seat delivers something this module does provide. UseSeats([]Seat{{Name: "mesh-broker", Scope: ScopeMesh, Delivers: "mesh-bus", Decision: "test"}}) raw := []byte(`{"module":"a-bus","version":"1",` + - `"provides":[{"name":"mesh-bus","scope":"mesh"}],` + + `"provides":[{"name":"mesh-bus","scope":"mesh","credential":{"form":"none"}}],` + `"claims":[{"name":"mesh-broker","scope":"mesh"}]}`) m, err := ParseManifest(raw) diff --git a/go/catalogue/seats_test.go b/go/catalogue/seats_test.go index ad001bb..828464e 100644 --- a/go/catalogue/seats_test.go +++ b/go/catalogue/seats_test.go @@ -148,7 +148,7 @@ func TestTheUplinkIsANodeSeatThatDeliversNothing(t *testing.T) { } func claimed(claims string) []byte { - return []byte(`{"module":"thing","version":"1","provides":[{"name":"npm-package-registry","scope":"mesh"}],"claims":` + claims + `}`) + return []byte(`{"module":"thing","version":"1","provides":[{"name":"npm-package-registry","scope":"mesh","credential":{"form":"none"}}],"claims":` + claims + `}`) } // **The refusal moved, it did not go** (novox/hq ADR 0118, superseding 0110). A module may now diff --git a/go/provisions/provisions.go b/go/provisions/provisions.go index a46753d..ca8d914 100644 --- a/go/provisions/provisions.go +++ b/go/provisions/provisions.go @@ -99,6 +99,9 @@ const ( FormShared = "shared" FormIssued = "issued" FormGiven = "given" + // FormNone is an offer whose consumers present no credential (catalogue.CredentialFormNone): the vault mints nothing, + // and the provision depends on no credential. + FormNone = "none" ) // VaultName is the vault's name of a provision's credential in form, as its verb `own` names it and the provider asks diff --git a/go/provisions/provisions_test.go b/go/provisions/provisions_test.go index de95a80..e0ba87a 100644 --- a/go/provisions/provisions_test.go +++ b/go/provisions/provisions_test.go @@ -74,7 +74,7 @@ func TestAProvisionIsAReferenceUnderItsConsumersKey(t *testing.T) { } // A reference's id is the vault's name as its verb own names it, in each form (the host agent, 2026-10-29). for form, want := range map[string]string{FormPair: "pair/novox/postgres-database/novox/keycloak", FormShared: "provision/novox/postgres-database", - FormIssued: "provision/novox/postgres-database/novox/keycloak", "none": ""} { + FormIssued: "provision/novox/postgres-database/novox/keycloak", FormNone: ""} { if got := VaultName(form, db); got != want { t.Errorf("the %s name is %q", form, got) } @@ -95,6 +95,8 @@ func TestAProvisionIsAReferenceUnderItsConsumersKey(t *testing.T) { {FormShared, nil, "ace.postgres.secret-postgres-database"}, {FormPair, providerPlan, "ace.postgres.backup-target,novox.keycloak.secret-postgres-database"}, {"", nil, ""}, + // An offer with no credential (the lead, 2026-10-29): nothing minted, nothing to wait for. + {FormNone, nil, ""}, } { entry := catalogue.PlanProvision{Provision: "postgres-database", Provider: postgres.Node, ProviderModule: postgres.Module, CredentialForm: c.form}