From df5f01bfa1ee08f38a91c3c9c1f22deb85e704b4 Mon Sep 17 00:00:00 2001 From: fix-ctl-pas Date: Fri, 16 Oct 2026 19:35:56 +0200 Subject: [PATCH] Name the credential tool of a software-issued offer in the plan (issue 928) --- internal/catalogue/bound.go | 4 +-- internal/catalogue/handed.go | 5 +++- internal/catalogue/handed_providers_test.go | 30 +++++++++++++++++++++ internal/catalogue/manifest.go | 7 +++++ internal/catalogue/resolve.go | 12 +++++---- internal/inventory/provisioning_test.go | 17 ++++++++++++ 6 files changed, 67 insertions(+), 8 deletions(-) diff --git a/internal/catalogue/bound.go b/internal/catalogue/bound.go index 17cde080..12941a4a 100644 --- a/internal/catalogue/bound.go +++ b/internal/catalogue/bound.go @@ -111,7 +111,7 @@ func boundNeed(n Needed, bound Chosen, catalogue map[string]Manifest, node Node, } held.From, held.At, held.Module = node.Name, at, m.Module held.Serves, held.SharedOwn, held.Identity = servedByOne(m, n.Name), sharedByOne(m, n.Name), m.IdentityBoundOf(n.Name) - held.CredentialForm = m.CredentialFormOf(n.Name) + held.CredentialForm, held.CredentialTool = m.CredentialFormOf(n.Name), m.CredentialToolOf(n.Name) return held, true, "" } matching := bound.among(world.Offered[n.Name]) @@ -125,7 +125,7 @@ func boundNeed(n Needed, bound Chosen, catalogue map[string]Manifest, node Node, identity := DefaultIdentityBound if pm, known := catalogue[p.Module]; known { held.SharedOwn, _ = pm.SharedCredentialOf(n.Name) - held.CredentialForm = pm.CredentialFormOf(n.Name) + held.CredentialForm, held.CredentialTool = pm.CredentialFormOf(n.Name), pm.CredentialToolOf(n.Name) identity = pm.IdentityBoundOf(n.Name) } held.From, held.At, held.Module, held.Serves, held.Identity = p.Node, p.At, p.Module, p.Serves, identity diff --git a/internal/catalogue/handed.go b/internal/catalogue/handed.go index d107bc9f..f019c539 100644 --- a/internal/catalogue/handed.go +++ b/internal/catalogue/handed.go @@ -104,6 +104,9 @@ type PlanProvision struct { Provider string `json:"provider"` ProviderModule string `json:"provider-module"` CredentialForm string `json:"credential-form,omitempty"` + // CredentialTool is, for an offer whose credential the provider's software issues (issue 928), the provider's tool + // the vault asks for each holder's value; empty for any other form. + CredentialTool string `json:"credential-tool,omitempty"` Path string `json:"path,omitempty"` // Contributes is what the consumer's manifest tells its provider for this provision (Manifest.Contributes: a // database's name, its extensions), so the provider's helper learns it from the bus (issue 776). Nil where the @@ -134,7 +137,7 @@ func provisionsOf(needs []Needed, module, as string, contributes map[string]map[ continue } p := PlanProvision{Provision: n.Name, Local: n.Local, Provider: n.From, ProviderModule: n.Module, - CredentialForm: n.CredentialForm, Contributes: maps.Clone(contributes[n.Name]), + CredentialForm: n.CredentialForm, CredentialTool: n.CredentialTool, Contributes: maps.Clone(contributes[n.Name]), At: n.At, From: n.From, As: as} // **A credential only a template names** (issue 788): named like a file's, so the client fills it from the // vault's row for this holder. diff --git a/internal/catalogue/handed_providers_test.go b/internal/catalogue/handed_providers_test.go index cb1af588..22744f8a 100644 --- a/internal/catalogue/handed_providers_test.go +++ b/internal/catalogue/handed_providers_test.go @@ -242,3 +242,33 @@ func TestAnOfferAnsweredByTheHelperIsReadAndHandsNoFile(t *testing.T) { t.Fatalf("the flag changed what is composed or handed:\nbefore: %s\nnow: %s\n%v %v", before, got, c2.Filled, c2.Plans) } } + +// A plan entry names the tool of an offer whose credential the provider's software issues (issue 928), so the vault +// asks that tool for each holder; an offer of any other form names none. +func TestAHandedModulesPlanNamesTheCredentialToolOfASoftwareIssuedOffer(t *testing.T) { + for credential, want := range map[string]string{ + ``: "", + `,"credential":{"own":"admin"}`: "", + `,"credential":{"issued-by":"outside"}`: "", + `,"credential":{"issued-by":"software","tool":"gitea_agent_token"}`: "gitea_agent_token", + } { + raw := `{"module":"store","version":"1","provides":[{"name":"postgres-database","scope":"mesh"` + + credential + `}],"serves":{"postgres-database":{"port":5432}}` + if strings.Contains(credential, `"own"`) { + raw += `,"own-secrets":{"admin":{"path":"/var/lib/store/admin","taken":"at-start"}}` + } + node := workstation() + node.At = "workstation.internal" + consumer := notifierConsumer(t, true, false) + r, err := Resolve(shelf(consumer, mustParse(t, raw+`}`)), []string{consumer.Module}, node, twoStores("anchor")) + if err != nil { + t.Fatal(err) + } + r.Account = "op" + c, _ := composeWith(t, r, handingTo("store:5000")) + got := c.Plans["notify"].Provisions + if len(got) != 1 || got[0].CredentialTool != want { + t.Errorf("an offer with %q: the plan names %+v, want the tool %q", credential, got, want) + } + } +} diff --git a/internal/catalogue/manifest.go b/internal/catalogue/manifest.go index b69c0f2b..3acc5cbd 100644 --- a/internal/catalogue/manifest.go +++ b/internal/catalogue/manifest.go @@ -303,6 +303,13 @@ func (m Manifest) IssuedCredentialOf(provision string) (string, bool) { return "", false } +// CredentialToolOf is the tool a plan names for a provision, set only where the offer says the provider's software +// issues its credential (issue 928); empty otherwise. +func (m Manifest) CredentialToolOf(provision string) string { + tool, _ := m.IssuedCredentialOf(provision) + return tool +} + // IssuedOutsideOf is whether this module's offer of a provision says only a person outside the mesh can give its // credential (novox/hq ADR 0337): the mesh never makes a value for it, and a consumer holds one only once a person // gave it with `secret accept`. diff --git a/internal/catalogue/resolve.go b/internal/catalogue/resolve.go index 1a91891a..6e2e08f2 100644 --- a/internal/catalogue/resolve.go +++ b/internal/catalogue/resolve.go @@ -230,6 +230,8 @@ type Needed struct { // CredentialForm is the credential form the provider's offer declares (Manifest.CredentialFormOf, issue 768), // named in a handed consumer's plan for the vault; empty where the provider's manifest is not in hand. CredentialForm string + // CredentialTool is the tool of the offer that the provider's software issues the credential through (issue 928). + CredentialTool string // Manager is set when this holder is a refreshable-grant licence's MANAGER, delivered the refresh // token rather than an access token (novox/hq ADR 0050). It changes one thing downstream: an empty // Sealed is tolerated — the manager has not adopted a refresh token yet, which is a real waiting @@ -543,7 +545,7 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world needs = append(needs, Needed{ Name: want, From: node.Name, At: at, Module: by.Module, Serves: servedByOne(by, want), For: because[want], - SharedOwn: sharedByOne(by, want), CredentialForm: by.CredentialFormOf(want), + SharedOwn: sharedByOne(by, want), CredentialForm: by.CredentialFormOf(want), CredentialTool: by.CredentialToolOf(want), Identity: by.IdentityBoundOf(want)}) } else if served := servedByOne(by, want); len(served) > 0 { // Answered here with no credential to mint, but the provider serves facts the @@ -565,7 +567,7 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world } needs = append(needs, Needed{ Name: want, From: node.Name, At: at, Serves: served, For: because[want], Module: by.Module, - CredentialForm: by.CredentialFormOf(want)}) + CredentialForm: by.CredentialFormOf(want), CredentialTool: by.CredentialToolOf(want)}) } continue } @@ -594,17 +596,17 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world node.Name, want, p.Node, meshNetwork)) return } - shared, form := "", "" + shared, form, tool := "", "", "" // A provider whose definition is not in hand is held to the tightest bound the // mesh knows, not to none: what it keeps is not known here (ADR 0225). bound := DefaultIdentityBound if pm, known := catalogue[p.Module]; known { shared, _ = pm.SharedCredentialOf(want) - form = pm.CredentialFormOf(want) + form, tool = pm.CredentialFormOf(want), pm.CredentialToolOf(want) bound = pm.IdentityBoundOf(want) } needs = append(needs, Needed{Name: want, From: p.Node, At: p.At, Module: p.Module, - Serves: p.Serves, For: because[want], SharedOwn: shared, CredentialForm: form, Identity: bound, + Serves: p.Serves, For: because[want], SharedOwn: shared, CredentialForm: form, CredentialTool: tool, Identity: bound, LeftOut: p.LeftOut}) } switch { diff --git a/internal/inventory/provisioning_test.go b/internal/inventory/provisioning_test.go index 21e88169..a8704131 100644 --- a/internal/inventory/provisioning_test.go +++ b/internal/inventory/provisioning_test.go @@ -374,3 +374,20 @@ func TestAKeptPlansProvisionsKeepContributesAtFromAsAndNames(t *testing.T) { t.Fatalf("the kept plan's provisions are %+v, want %+v", got, sent) } } + +// A kept plan's provision keeps the tool the vault asks for a software-issued credential (issue 928). +func TestAKeptPlansProvisionKeepsItsCredentialTool(t *testing.T) { + sent := catalogue.PlanProvision{Provision: "gitea-read", Provider: "novox", ProviderModule: "gitea", + CredentialForm: catalogue.CredentialFormIssued, CredentialTool: "gitea_agent_token"} + raw, err := json.Marshal(map[string]any{"claude-code": map[string]any{"provisions": []catalogue.PlanProvision{sent}}}) + if err != nil { + t.Fatal(err) + } + var kept map[string]keptPlan + if err := json.Unmarshal(raw, &kept); err != nil { + t.Fatal(err) + } + if got := kept["claude-code"].Provisions; len(got) != 1 || got[0].CredentialTool != "gitea_agent_token" { + t.Fatalf("the kept plan's provisions are %+v, want the tool kept", got) + } +}