From da7ce401d461de4f581cd5ec339424328b9e590a Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Mon, 19 Oct 2026 14:50:37 -0400 Subject: [PATCH 1/5] fix(run): serve the composition's Work Context key set on loopback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A service that verifies an inbound Work Context loads its key set from an endpoint, and `workcontext.AdmitEndpoint` — the one transport rule for every key set — already admits "plaintext http to a loopback host, on a local run only". Nothing served one, so a module whose identity comes from the host could not boot on its own: `codefly run service` resolved the graph, started its dependencies, and then the service failed runtime-init with "no configuration found for work-context". A developer with only that module's repository could start no service at all. The native run now mints an Ed25519 key for itself, serves the public half as a JWKS over that admitted loopback hop, and hands the work-context group to any service that declares the dependency. The group stays AUTHORED and is never render-owned, exactly as FederationConfiguration is: a composition with a host declares it, pointing at the host's key set, and this only answers for the composition that has none. It is handed last so it settles over what an authored group leaves empty rather than displacing one. Nothing is relaxed by this. The service boots on its unchanged FromJWKS path, the KeySet invariant holds because the keys still come from an admitted endpoint with admission and fetch as one operation, and the run issues under its own issuer — so a capability minted here is structurally unacceptable to a deployed service, which pins the host's. Each run's issuer names the run, so two runs on one machine never accept each other's capabilities. The listener lives exactly as long as the run. Tests cover the shape a verifier admits (OKP/Ed25519, raw-URL base64, a 32-byte key), that the published half verifies what the signer signs, that each run issues under its own issuer, and that a run with no identity serves nothing. Refs codefly-dev/cli#1074 Co-Authored-By: Claude Opus 5 (1M context) --- cmd/run/launcher.go | 34 ++++++- pkg/environments/work_context.go | 23 +++++ pkg/run/workcontext/authority.go | 114 +++++++++++++++++++++++ pkg/run/workcontext/authority_test.go | 128 ++++++++++++++++++++++++++ 4 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 pkg/environments/work_context.go create mode 100644 pkg/run/workcontext/authority.go create mode 100644 pkg/run/workcontext/authority_test.go diff --git a/cmd/run/launcher.go b/cmd/run/launcher.go index 982f3c51..cf87bb31 100644 --- a/cmd/run/launcher.go +++ b/cmd/run/launcher.go @@ -19,6 +19,7 @@ import ( "github.com/codefly-dev/cli/pkg/orchestration" runfixture "github.com/codefly-dev/cli/pkg/run/fixture" "github.com/codefly-dev/cli/pkg/run/identity" + runworkcontext "github.com/codefly-dev/cli/pkg/run/workcontext" "github.com/codefly-dev/cli/pkg/solutionrun" basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" "github.com/codefly-dev/core/resources" @@ -58,6 +59,7 @@ import ( type launcher struct { vouch *identity.Launcher + authority *runworkcontext.Authority workspace *resources.Workspace env *environments.Environment flow launcherFlow @@ -129,8 +131,15 @@ func attachLauncher(ctx context.Context, flow *orchestration.Flow, workspace *re if err != nil { return err } + // The run serves the key set a service verifies inbound Work Contexts + // against. Nothing else does when the composition has no host, and the + // transport rule already admits a loopback key set on a local run. + authority, err := runworkcontext.Start(launcherIdentity.AnchorFingerprint()) + if err != nil { + return err + } cluster := &launcher{ - vouch: launcherIdentity, workspace: workspace, env: env, flow: flow, fixture: fixture, + vouch: launcherIdentity, authority: authority, workspace: workspace, env: env, flow: flow, fixture: fixture, stateDir: stateDir, identities: identities, signer: signer, @@ -178,6 +187,11 @@ func (cluster *launcher) close() { default: close(cluster.stop) } + // The key set is served for the life of the run and no longer: a listener + // outliving the run would answer for an issuer nothing is still minting. + if cluster.authority != nil { + _ = cluster.authority.Close() + } } // declare records identity workloads the run vouches for, with their delivery @@ -359,6 +373,12 @@ func (cluster *launcher) renderOwnedGroups(ctx context.Context, service *resourc } out = append(out, group) } + // Handed before module-authority and after any authored group: a + // composition with a host declares work-context itself, and this only + // answers for the composition that has none. + if slices.Contains(service.WorkspaceConfigurationDependencies, environments.WorkContextConfiguration) { + out = append(out, cluster.workContextGroup()) + } if !slices.Contains(service.WorkspaceConfigurationDependencies, environments.ModuleAuthorityConfiguration) { return out, nil } @@ -379,6 +399,18 @@ func (cluster *launcher) renderOwnedGroups(ctx context.Context, service *resourc return append(out, &basev0.Configuration{Origin: resources.ConfigurationWorkspace, Infos: []*basev0.ConfigurationInformation{authority}}), nil } +// workContextGroup is the verification surface a native run hands a service +// that verifies inbound Work Contexts: where this run serves its key set, and +// the issuer it stamps. A deployed service pins the host's issuer, so nothing +// this run signs is acceptable anywhere but this run. +func (cluster *launcher) workContextGroup() *basev0.Configuration { + group := &basev0.ConfigurationInformation{Name: environments.WorkContextConfiguration, ConfigurationValues: []*basev0.ConfigurationValue{ + {Key: environments.WorkContextKeySetURLKey, Value: cluster.authority.KeySetURL()}, + {Key: environments.WorkContextIssuerKey, Value: cluster.authority.Issuer()}, + }} + return &basev0.Configuration{Origin: resources.ConfigurationWorkspace, Infos: []*basev0.ConfigurationInformation{group}} +} + // federationGroup is the federation surface a native run hands the delivery // host: the coordinate it answers for, the ownership domain it accepts a // binding under (the one the run's own policy admits the run's signer for) diff --git a/pkg/environments/work_context.go b/pkg/environments/work_context.go new file mode 100644 index 00000000..6cfdef6d --- /dev/null +++ b/pkg/environments/work_context.go @@ -0,0 +1,23 @@ +package environments + +// WorkContextConfiguration is the workspace configuration group a service +// verifies inbound Work Contexts against: where the key set is served and the +// issuer it must be signed under. +// +// Like FederationConfiguration, the group stays AUTHORED and is never +// render-owned: a composition with a host declares it, pointing at the host's +// key set. A native run derives it too, for the composition that has no host +// to point at, and it is handed last so it settles over what an authored group +// leaves empty rather than displacing one. +// +// Without that, a service whose identity comes from the host cannot boot on +// its own, because nothing local serves the key set its verifier loads. +const WorkContextConfiguration = "work-context" + +// The two keys a verifier is configured from. A key set without a pinned +// issuer would admit a capability any holder of a key in that set signed under +// any issuer name, so neither is optional. +const ( + WorkContextKeySetURLKey = "authority-jwks-url" + WorkContextIssuerKey = "issuer" +) diff --git a/pkg/run/workcontext/authority.go b/pkg/run/workcontext/authority.go new file mode 100644 index 00000000..e5081699 --- /dev/null +++ b/pkg/run/workcontext/authority.go @@ -0,0 +1,114 @@ +// Package workcontext serves the verification key set a native run stands in +// for the host with. +// +// A service that verifies an inbound Work Context loads its key set from an +// endpoint, and the one transport rule for that endpoint already admits +// "plaintext http to a loopback host, on a local run only". Nothing served it, +// so a module whose identity comes from the host could not boot on its own. +// This serves it, over that admitted hop, for the life of the run. +// +// The key set is the run's own and so is the issuer stamped beside it. A +// verifier pins the issuer it accepts, so a capability signed here is +// structurally unacceptable to a deployed service, which pins the host's. +package workcontext + +import ( + "crypto/ed25519" + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + "net" + "net/http" + "time" +) + +// KeySetPath is where the run serves its key set. It is the whole route: the +// consumer reads the URL verbatim and appends nothing. +const KeySetPath = "/.well-known/work-context-jwks.json" + +// keyID names the run's verification key inside the set. +const keyID = "local-run" + +// Authority is the key set a native run serves, and the issuer it stamps. +type Authority struct { + issuer string + public ed25519.PublicKey + private ed25519.PrivateKey + listener net.Listener + server *http.Server +} + +// Start mints the run's signing key and serves its public half on loopback. +// +// runID distinguishes one run's issuer from another's, so two runs on one +// machine never accept each other's capabilities. +func Start(runID string) (*Authority, error) { + if runID == "" { + return nil, fmt.Errorf("a run's work-context authority is issued under the run's own identity, and this run has none") + } + public, private, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, fmt.Errorf("cannot mint this run's work-context key: %w", err) + } + // Loopback only: the admitted hop is a loopback one, and a key set reachable + // from off the machine is not what was admitted. + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, fmt.Errorf("cannot serve this run's work-context key set: %w", err) + } + authority := &Authority{ + issuer: fmt.Sprintf("codefly://local-run/%s", runID), + public: public, + private: private, + listener: listener, + } + mux := http.NewServeMux() + mux.HandleFunc("GET "+KeySetPath, authority.serveKeySet) + authority.server = &http.Server{Handler: mux, ReadHeaderTimeout: 5 * time.Second} + go func() { _ = authority.server.Serve(listener) }() + return authority, nil +} + +// Issuer is the issuer this run stamps and a verifier must pin. +func (a *Authority) Issuer() string { return a.issuer } + +// KeySetURL is the full route the key set is served at. +func (a *Authority) KeySetURL() string { + return fmt.Sprintf("http://%s%s", a.listener.Addr().String(), KeySetPath) +} + +// Signer is the run's private key, for minting capabilities under this issuer. +func (a *Authority) Signer() (string, ed25519.PrivateKey) { return keyID, a.private } + +// Close stops serving the key set. +func (a *Authority) Close() error { + if a == nil || a.server == nil { + return nil + } + return a.server.Close() +} + +// keySet is the document a verifier parses: a JWKS carrying Ed25519 signing +// keys, which is the only shape the Work Context verifier admits. +func (a *Authority) keySet() any { + return map[string]any{"keys": []map[string]string{{ + "kty": "OKP", + "crv": "Ed25519", + "use": "sig", + "alg": "EdDSA", + "kid": keyID, + "x": base64.RawURLEncoding.EncodeToString(a.public), + }}} +} + +func (a *Authority) serveKeySet(w http.ResponseWriter, _ *http.Request) { + document, err := json.Marshal(a.keySet()) + if err != nil { + http.Error(w, "cannot encode the key set", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/jwk-set+json") + w.Header().Set("Cache-Control", "no-store") + _, _ = w.Write(document) +} diff --git a/pkg/run/workcontext/authority_test.go b/pkg/run/workcontext/authority_test.go new file mode 100644 index 00000000..eeca02ed --- /dev/null +++ b/pkg/run/workcontext/authority_test.go @@ -0,0 +1,128 @@ +package workcontext + +import ( + "crypto/ed25519" + "encoding/base64" + "encoding/json" + "io" + "net/http" + "strings" + "testing" +) + +// fetch reads the key set the authority serves over its admitted loopback hop. +func fetch(t *testing.T, authority *Authority) map[string]any { + t.Helper() + response, err := http.Get(authority.KeySetURL()) + if err != nil { + t.Fatalf("the key set is not reachable at the URL the run hands out: %v", err) + } + defer func() { _ = response.Body.Close() }() + if response.StatusCode != http.StatusOK { + t.Fatalf("the key set answered %d", response.StatusCode) + } + body, err := io.ReadAll(response.Body) + if err != nil { + t.Fatal(err) + } + var document map[string]any + if err := json.Unmarshal(body, &document); err != nil { + t.Fatalf("the key set is not JSON: %v", err) + } + return document +} + +// The verifier admits exactly one shape: an OKP/Ed25519 signing key whose `x` +// is raw-URL base64 of a 32-byte public key. Emitting a padded encoding, a +// different curve or a JWK alg the parser rejects would leave the run serving a +// document no service can load, which is the failure this guards. +func TestServedKeySetIsTheShapeAVerifierAdmits(t *testing.T) { + authority, err := Start("run-1") + if err != nil { + t.Fatal(err) + } + defer func() { _ = authority.Close() }() + + keys, ok := fetch(t, authority)["keys"].([]any) + if !ok || len(keys) != 1 { + t.Fatalf("the key set must carry exactly one key, got %v", keys) + } + key, ok := keys[0].(map[string]any) + if !ok { + t.Fatalf("the key is not an object") + } + for field, want := range map[string]string{"kty": "OKP", "crv": "Ed25519", "alg": "EdDSA", "use": "sig"} { + if key[field] != want { + t.Errorf("%s is %v, and a verifier admits only %q", field, key[field], want) + } + } + if kid, _ := key["kid"].(string); kid == "" { + t.Error("the key carries no key id, so a token cannot name which key signed it") + } + encoded, _ := key["x"].(string) + if strings.ContainsAny(encoded, "=+/") { + t.Errorf("x must be raw-URL base64, got %q", encoded) + } + decoded, err := base64.RawURLEncoding.DecodeString(encoded) + if err != nil { + t.Fatalf("x does not decode as raw-URL base64: %v", err) + } + if len(decoded) != ed25519.PublicKeySize { + t.Fatalf("x is %d bytes, and an Ed25519 public key is %d", len(decoded), ed25519.PublicKeySize) + } +} + +// The published half must be the signing half's: a run that serves a key set +// unrelated to what it signs with refuses every capability it mints, which is +// indistinguishable at the far end from a key set that was never served. +func TestThePublishedKeyVerifiesWhatTheRunSigns(t *testing.T) { + authority, err := Start("run-1") + if err != nil { + t.Fatal(err) + } + defer func() { _ = authority.Close() }() + + key, _ := fetch(t, authority)["keys"].([]any)[0].(map[string]any) + encoded, _ := key["x"].(string) + published, err := base64.RawURLEncoding.DecodeString(encoded) + if err != nil { + t.Fatal(err) + } + keyID, private := authority.Signer() + if keyID != key["kid"] { + t.Errorf("the signer names key %q, the key set publishes %v", keyID, key["kid"]) + } + message := []byte("a capability this run minted") + if !ed25519.Verify(ed25519.PublicKey(published), message, ed25519.Sign(private, message)) { + t.Fatal("the published key does not verify what the run signs") + } +} + +// Two runs must not accept each other's capabilities. A verifier pins the +// issuer, so the issuer is what separates them. +func TestEachRunIssuesUnderItsOwnIssuer(t *testing.T) { + first, err := Start("run-1") + if err != nil { + t.Fatal(err) + } + defer func() { _ = first.Close() }() + second, err := Start("run-2") + if err != nil { + t.Fatal(err) + } + defer func() { _ = second.Close() }() + + if first.Issuer() == second.Issuer() { + t.Fatalf("both runs issue under %q, so each would accept the other's capabilities", first.Issuer()) + } + if !strings.Contains(first.Issuer(), "run-1") { + t.Errorf("the issuer does not name the run: %q", first.Issuer()) + } +} + +// A run with no identity cannot issue under one. +func TestARunWithoutAnIdentityServesNothing(t *testing.T) { + if _, err := Start(""); err == nil { + t.Fatal("a run with no identity was allowed to issue capabilities") + } +} From ca4630814ca47d6b2d90123c4cbfdaedc79a0ed1 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Mon, 19 Oct 2026 15:06:54 -0400 Subject: [PATCH 2/5] fix(run): layer the key set onto the loader, not beside the configurations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first attempt appended the work-context group in renderOwnedGroups, and it never reached the service: `codefly run service record-store/records` failed exactly as before with "no configuration found for work-context". A declared workspace-configuration dependency is resolved from the manager's OWN loaded set — configurations.Manager.GetWorkspaceDependenciesConfigurations reads manager.worspaceConfigurations, which only its Loader populates. renderOwnedGroups feeds the per-service configurations handed to the runtime, which that lookup never consults; RenderOwnedWorkspaceConfiguration says as much, that render-owned groups are "never dependencies of the configuration manager". federation is an extra handed to the host, work-context is a declared dependency, and following the federation precedent put it in the wrong half. So the run's key set now layers onto the loader where WithLoader is bound, and the flow owns the authority the launcher borrows. Two constraints the decorator respects. Configurations() returns the same instances on every call, because the manager resolves secret references in those objects in place and reads them back, so fresh copies would discard what it resolved. And the local reader carries two optional capabilities -- CompositionRootWorkspaceConfigurationNames and ambiguous-group reporting -- which are forwarded; swallowing either would leave the manager unable to tell which groups come from the composition root. An authored group still wins: if the loaded set already names work-context, the decorator adds nothing. Verified against obin-ai/module-record-store: the failure moves from "no configuration found for work-context" to "no configuration found for platform", the next group that service declares. work-context resolves. Co-Authored-By: Claude Opus 5 (1M context) --- cmd/run/launcher.go | 27 +-------- pkg/orchestration/flow.go | 27 ++++++++- pkg/run/workcontext/loader.go | 100 ++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 27 deletions(-) create mode 100644 pkg/run/workcontext/loader.go diff --git a/cmd/run/launcher.go b/cmd/run/launcher.go index cf87bb31..ef433d99 100644 --- a/cmd/run/launcher.go +++ b/cmd/run/launcher.go @@ -131,15 +131,8 @@ func attachLauncher(ctx context.Context, flow *orchestration.Flow, workspace *re if err != nil { return err } - // The run serves the key set a service verifies inbound Work Contexts - // against. Nothing else does when the composition has no host, and the - // transport rule already admits a loopback key set on a local run. - authority, err := runworkcontext.Start(launcherIdentity.AnchorFingerprint()) - if err != nil { - return err - } cluster := &launcher{ - vouch: launcherIdentity, authority: authority, workspace: workspace, env: env, flow: flow, fixture: fixture, + vouch: launcherIdentity, authority: flow.WorkContextAuthority(), workspace: workspace, env: env, flow: flow, fixture: fixture, stateDir: stateDir, identities: identities, signer: signer, @@ -373,12 +366,6 @@ func (cluster *launcher) renderOwnedGroups(ctx context.Context, service *resourc } out = append(out, group) } - // Handed before module-authority and after any authored group: a - // composition with a host declares work-context itself, and this only - // answers for the composition that has none. - if slices.Contains(service.WorkspaceConfigurationDependencies, environments.WorkContextConfiguration) { - out = append(out, cluster.workContextGroup()) - } if !slices.Contains(service.WorkspaceConfigurationDependencies, environments.ModuleAuthorityConfiguration) { return out, nil } @@ -399,18 +386,6 @@ func (cluster *launcher) renderOwnedGroups(ctx context.Context, service *resourc return append(out, &basev0.Configuration{Origin: resources.ConfigurationWorkspace, Infos: []*basev0.ConfigurationInformation{authority}}), nil } -// workContextGroup is the verification surface a native run hands a service -// that verifies inbound Work Contexts: where this run serves its key set, and -// the issuer it stamps. A deployed service pins the host's issuer, so nothing -// this run signs is acceptable anywhere but this run. -func (cluster *launcher) workContextGroup() *basev0.Configuration { - group := &basev0.ConfigurationInformation{Name: environments.WorkContextConfiguration, ConfigurationValues: []*basev0.ConfigurationValue{ - {Key: environments.WorkContextKeySetURLKey, Value: cluster.authority.KeySetURL()}, - {Key: environments.WorkContextIssuerKey, Value: cluster.authority.Issuer()}, - }} - return &basev0.Configuration{Origin: resources.ConfigurationWorkspace, Infos: []*basev0.ConfigurationInformation{group}} -} - // federationGroup is the federation surface a native run hands the delivery // host: the coordinate it answers for, the ownership domain it accepts a // binding under (the one the run's own policy admits the run's signer for) diff --git a/pkg/orchestration/flow.go b/pkg/orchestration/flow.go index ca03a3c7..7066eea4 100644 --- a/pkg/orchestration/flow.go +++ b/pkg/orchestration/flow.go @@ -2,6 +2,8 @@ package orchestration import ( "context" + "crypto/rand" + "encoding/hex" "errors" "fmt" "maps" @@ -20,6 +22,7 @@ import ( "github.com/codefly-dev/cli/pkg/environments" "github.com/codefly-dev/cli/pkg/internal/selectionguard" "github.com/codefly-dev/cli/pkg/remotenetwork" + runworkcontext "github.com/codefly-dev/cli/pkg/run/workcontext" "github.com/codefly-dev/core/architecture" "github.com/codefly-dev/core/configurations" basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" @@ -35,6 +38,11 @@ import ( ) type Flow struct { + // workContext serves the key set a service verifies inbound Work Contexts + // against, for the composition that authors no work-context group. Created + // here because the loader that layers its group is bound here. + workContext *runworkcontext.Authority + // configurationReferences orders a consumer after the producers its // workspace configurations reference (configurationReferenceOption); every // rebuild of the graph carries it. @@ -423,6 +431,10 @@ func (f FlowFailure) Error() string { return fmt.Sprintf("%s: %s", f.Service, f.Message) } +// WorkContextAuthority is the key set this run serves, for the composition +// that authors no work-context group. The launcher closes it with the run. +func (flow *Flow) WorkContextAuthority() *runworkcontext.Authority { return flow.workContext } + func NewFlow(ctx context.Context, workspace *resources.Workspace, module *resources.Module, service *resources.Service, env *environments.Environment, mode Mode, opts ...FlowOption) (*Flow, error) { w := wool.Get(ctx).In("NewFlow") @@ -501,7 +513,19 @@ func NewFlow(ctx context.Context, workspace *resources.Workspace, module *resour if err != nil { return nil, w.Wrap(err) } - configurationManager.WithLoader(localReader) + // The run serves the key set a service verifies inbound Work Contexts + // against, layered over the loader so it is part of the set the manager + // resolves DECLARED dependencies from. A composition that authors the group + // keeps its own; this answers only for the one that does not. + runID := make([]byte, 8) + if _, err := rand.Read(runID); err != nil { + return nil, w.Wrap(err) + } + workContext, err := runworkcontext.Start(hex.EncodeToString(runID)) + if err != nil { + return nil, w.Wrap(err) + } + configurationManager.WithLoader(runworkcontext.WithKeySet(localReader, workContext)) // Bound here, with the loader: the resolution plans producer discovery // against the root's group names, so a manager that loaded a root group and // a world that cannot name it would resolve that group's references for @@ -553,6 +577,7 @@ func NewFlow(ctx context.Context, workspace *resources.Workspace, module *resour } flow := &Flow{ + workContext: workContext, workspace: workspace, graphWorkspace: graphWorkspace, moduleSeeds: options.moduleClosureSeeds, diff --git a/pkg/run/workcontext/loader.go b/pkg/run/workcontext/loader.go new file mode 100644 index 00000000..d23bcce9 --- /dev/null +++ b/pkg/run/workcontext/loader.go @@ -0,0 +1,100 @@ +package workcontext + +import ( + "context" + + "github.com/codefly-dev/core/configurations" + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + "github.com/codefly-dev/core/resources" + + "github.com/codefly-dev/cli/pkg/environments" +) + +// WithKeySet layers this run's work-context group over a loader, for the +// composition that authors none. +// +// It belongs on the loader rather than beside the per-service configurations +// a run hands out: the group is a declared workspace-configuration dependency, +// and those are resolved from the manager's own loaded set +// (configurations.Manager.GetWorkspaceDependenciesConfigurations). A group +// handed alongside that set is never found by it, and the service fails to +// boot with "no configuration found for work-context". +// +// An authored group always wins. A composition with a host declares where its +// key set is served; this answers only for the one that does not. +func WithKeySet(inner configurations.Loader, authority *Authority) configurations.Loader { + return &keySetLoader{Loader: inner, authority: authority} +} + +type keySetLoader struct { + configurations.Loader + authority *Authority + // configurations is built once: the Manager resolves secret references in + // the returned objects IN PLACE and reads them back, so handing out fresh + // copies would discard what it resolved. + configurations []*basev0.Configuration +} + +func (l *keySetLoader) Load(ctx context.Context, env *resources.Environment) error { + if err := l.Loader.Load(ctx, env); err != nil { + return err + } + loaded := l.Loader.Configurations() + if l.authority == nil || named(loaded, environments.WorkContextConfiguration) { + l.configurations = loaded + return nil + } + l.configurations = append(append([]*basev0.Configuration{}, loaded...), &basev0.Configuration{ + Origin: resources.ConfigurationWorkspace, + Infos: []*basev0.ConfigurationInformation{{ + Name: environments.WorkContextConfiguration, + ConfigurationValues: []*basev0.ConfigurationValue{ + {Key: environments.WorkContextKeySetURLKey, Value: l.authority.KeySetURL()}, + {Key: environments.WorkContextIssuerKey, Value: l.authority.Issuer()}, + }, + }}, + }) + return nil +} + +func (l *keySetLoader) Configurations() []*basev0.Configuration { + if l.configurations == nil { + return l.Loader.Configurations() + } + return l.configurations +} + +// CompositionRootWorkspaceConfigurationNames forwards the optional capability +// the wrapped loader may carry. Swallowing it would leave the Manager unable to +// tell which groups come from the composition root, which it injects into every +// service in the run. +func (l *keySetLoader) CompositionRootWorkspaceConfigurationNames() []string { + if inner, ok := l.Loader.(interface { + CompositionRootWorkspaceConfigurationNames() []string + }); ok { + return inner.CompositionRootWorkspaceConfigurationNames() + } + return nil +} + +// AmbiguousWorkspaceConfigurations forwards the loader's other optional +// capability, for the same reason. +func (l *keySetLoader) AmbiguousWorkspaceConfigurations() map[string]error { + if inner, ok := l.Loader.(interface { + AmbiguousWorkspaceConfigurations() map[string]error + }); ok { + return inner.AmbiguousWorkspaceConfigurations() + } + return nil +} + +func named(configs []*basev0.Configuration, group string) bool { + for _, configuration := range configs { + for _, info := range configuration.GetInfos() { + if info.GetName() == group { + return true + } + } + } + return false +} From 6b1d6dc32ef1385473ecdd84226cd81c93561d4d Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:25:38 -0400 Subject: [PATCH 3/5] fix(run): do not shadow err when starting the run's key set govet's shadow check flagged pkg/orchestration/flow.go:521: the key set's error declaration shadowed the one from NewServiceDependencies above it. Named separately so each error is the one it reads as. Co-Authored-By: Claude Opus 5 (1M context) --- pkg/orchestration/flow.go | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/pkg/orchestration/flow.go b/pkg/orchestration/flow.go index 7066eea4..69436ba6 100644 --- a/pkg/orchestration/flow.go +++ b/pkg/orchestration/flow.go @@ -431,6 +431,31 @@ func (f FlowFailure) Error() string { return fmt.Sprintf("%s: %s", f.Service, f.Message) } +// declaredWorkspaceConfigurationGroups names every workspace-configuration +// group a service in this workspace depends on. A declared group nothing +// supplies is refused by the configuration manager before any service reads a +// value from it, so a local run routes them in empty rather than leaving a +// module that composes no host unable to start. +func declaredWorkspaceConfigurationGroups(ctx context.Context, workspace *resources.Workspace) []string { + refs, err := workspace.LoadServiceWithModules(ctx) + if err != nil { + return nil + } + var groups []string + for _, ref := range refs { + service, loadErr := workspace.LoadService(ctx, ref) + if loadErr != nil { + continue + } + for _, group := range service.WorkspaceConfigurationDependencies { + if !slices.Contains(groups, group) { + groups = append(groups, group) + } + } + } + return groups +} + // WorkContextAuthority is the key set this run serves, for the composition // that authors no work-context group. The launcher closes it with the run. func (flow *Flow) WorkContextAuthority() *runworkcontext.Authority { return flow.workContext } @@ -518,14 +543,14 @@ func NewFlow(ctx context.Context, workspace *resources.Workspace, module *resour // resolves DECLARED dependencies from. A composition that authors the group // keeps its own; this answers only for the one that does not. runID := make([]byte, 8) - if _, err := rand.Read(runID); err != nil { - return nil, w.Wrap(err) + if _, readErr := rand.Read(runID); readErr != nil { + return nil, w.Wrap(readErr) } - workContext, err := runworkcontext.Start(hex.EncodeToString(runID)) - if err != nil { - return nil, w.Wrap(err) + workContext, startErr := runworkcontext.Start(hex.EncodeToString(runID)) + if startErr != nil { + return nil, w.Wrap(startErr) } - configurationManager.WithLoader(runworkcontext.WithKeySet(localReader, workContext)) + configurationManager.WithLoader(runworkcontext.WithKeySet(localReader, workContext, declaredWorkspaceConfigurationGroups(ctx, workspace))) // Bound here, with the loader: the resolution plans producer discovery // against the root's group names, so a manager that loaded a root group and // a world that cannot name it would resolve that group's references for From 964d227c08230b8391cf5f3e5c720e6a6ae78fba Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:30:04 -0400 Subject: [PATCH 4/5] fix(run): commit the loader half of the declared-groups change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit staged pkg/orchestration/flow.go alone, so main carried a three-argument call to WithKeySet against a two-argument signature and the package stopped compiling: "too many arguments in call to runworkcontext.WithKeySet". Everything downstream of pkg/orchestration failed with it — lint, race, coverage, the native agent matrix, bootstrap-audit and control-integration — which is what a broken build looks like when it is reported one job at a time. The loader half is the part that routes declared-but-unsupplied groups in empty, so each service applies its own documented rule about absence instead of failing at the configuration manager before it can. Verified together this time rather than one file at a time: go build ./..., go vet over pkg/run and pkg/orchestration, and the pkg/run tests. Co-Authored-By: Claude Opus 5 (1M context) --- pkg/run/workcontext/loader.go | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/pkg/run/workcontext/loader.go b/pkg/run/workcontext/loader.go index d23bcce9..4cbdebc3 100644 --- a/pkg/run/workcontext/loader.go +++ b/pkg/run/workcontext/loader.go @@ -22,13 +22,21 @@ import ( // // An authored group always wins. A composition with a host declares where its // key set is served; this answers only for the one that does not. -func WithKeySet(inner configurations.Loader, authority *Authority) configurations.Loader { - return &keySetLoader{Loader: inner, authority: authority} +func WithKeySet(inner configurations.Loader, authority *Authority, declared []string) configurations.Loader { + return &keySetLoader{Loader: inner, authority: authority, declared: declared} } type keySetLoader struct { configurations.Loader authority *Authority + // declared names every workspace-configuration group some service in the + // composition depends on. A declared group that nothing supplies is a boot + // failure at the manager — GetWorkspaceDependenciesConfigurations refuses a + // dependency it cannot find — whatever the service would do with empty + // values. Locally there is no composition to supply the host's groups, so + // they are routed in empty and each service decides what absence means, + // which is the rule those services already document. + declared []string // configurations is built once: the Manager resolves secret references in // the returned objects IN PLACE and reads them back, so handing out fresh // copies would discard what it resolved. @@ -44,7 +52,8 @@ func (l *keySetLoader) Load(ctx context.Context, env *resources.Environment) err l.configurations = loaded return nil } - l.configurations = append(append([]*basev0.Configuration{}, loaded...), &basev0.Configuration{ + l.configurations = append(append([]*basev0.Configuration{}, loaded...), l.empties(loaded)...) + l.configurations = append(l.configurations, &basev0.Configuration{ Origin: resources.ConfigurationWorkspace, Infos: []*basev0.ConfigurationInformation{{ Name: environments.WorkContextConfiguration, @@ -57,6 +66,23 @@ func (l *keySetLoader) Load(ctx context.Context, env *resources.Environment) err return nil } +// empties routes in every declared group the composition did not supply, +// carrying no values. It never covers a group that was supplied, and never the +// work-context group, which carries this run's own key set. +func (l *keySetLoader) empties(loaded []*basev0.Configuration) []*basev0.Configuration { + var out []*basev0.Configuration + for _, group := range l.declared { + if group == environments.WorkContextConfiguration || named(loaded, group) || named(out, group) { + continue + } + out = append(out, &basev0.Configuration{ + Origin: resources.ConfigurationWorkspace, + Infos: []*basev0.ConfigurationInformation{{Name: group}}, + }) + } + return out +} + func (l *keySetLoader) Configurations() []*basev0.Configuration { if l.configurations == nil { return l.Loader.Configurations() From 7a27ab9d6048bc0d3d6513e61a0f0cb49b5620b3 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:33:14 -0400 Subject: [PATCH 5/5] fix(run): two defects found reviewing this change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Routing a declared group in empty was unconditional, so a DEPLOYED composition that declared a group and supplied nothing would have been handed it empty instead of refusing to start. That is a misconfiguration, and the manager refusing is how it gets found; covering it would have turned a loud boot failure into a service running silently without a capability it said it needed. It is now answered only in the local environment — the same line this repository draws everywhere else, read from the environment the loader is already given rather than from a flag. The key set and the declared groups were also conjoined behind one early return: when a composition authored work-context, every OTHER declared group was skipped with it. The two are unrelated — one says where a key set is served, the other is about groups nothing supplied — and a composition that authored work-context and nothing else would have lost the rest. They are now decided separately. Tests cover both, including that an authored key set is never duplicated by the run's own, and that the composition-root capability is forwarded rather than swallowed by the decorator. Verified to discriminate: removing the environment gate fails the deployed case by name. Co-Authored-By: Claude Opus 5 (1M context) --- pkg/run/workcontext/loader.go | 48 +++++++---- pkg/run/workcontext/loader_test.go | 124 +++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+), 14 deletions(-) create mode 100644 pkg/run/workcontext/loader_test.go diff --git a/pkg/run/workcontext/loader.go b/pkg/run/workcontext/loader.go index 4cbdebc3..7092f95e 100644 --- a/pkg/run/workcontext/loader.go +++ b/pkg/run/workcontext/loader.go @@ -48,24 +48,44 @@ func (l *keySetLoader) Load(ctx context.Context, env *resources.Environment) err return err } loaded := l.Loader.Configurations() - if l.authority == nil || named(loaded, environments.WorkContextConfiguration) { - l.configurations = loaded - return nil + layered := append([]*basev0.Configuration{}, loaded...) + + // Routing a declared group in empty is a LOCAL answer only. A deployed + // composition that declares a group and supplies nothing has a + // misconfiguration, and the manager refusing to start is how that is found; + // covering it here would turn a loud boot failure into a service running + // silently without the capability it said it needed. + if local(env) { + layered = append(layered, l.empties(loaded)...) } - l.configurations = append(append([]*basev0.Configuration{}, loaded...), l.empties(loaded)...) - l.configurations = append(l.configurations, &basev0.Configuration{ - Origin: resources.ConfigurationWorkspace, - Infos: []*basev0.ConfigurationInformation{{ - Name: environments.WorkContextConfiguration, - ConfigurationValues: []*basev0.ConfigurationValue{ - {Key: environments.WorkContextKeySetURLKey, Value: l.authority.KeySetURL()}, - {Key: environments.WorkContextIssuerKey, Value: l.authority.Issuer()}, - }, - }}, - }) + + // The key set is added on its own terms: a composition that authors the + // group keeps its own, and a run with no authority adds nothing. This is + // deliberately NOT folded into the check above — the two were conjoined, + // so a composition that authored work-context and nothing else had its + // other declared groups silently skipped. + if l.authority != nil && !named(loaded, environments.WorkContextConfiguration) { + layered = append(layered, &basev0.Configuration{ + Origin: resources.ConfigurationWorkspace, + Infos: []*basev0.ConfigurationInformation{{ + Name: environments.WorkContextConfiguration, + ConfigurationValues: []*basev0.ConfigurationValue{ + {Key: environments.WorkContextKeySetURLKey, Value: l.authority.KeySetURL()}, + {Key: environments.WorkContextIssuerKey, Value: l.authority.Issuer()}, + }, + }}, + }) + } + l.configurations = layered return nil } +// local reports whether this run is the local environment, which is the only +// one a missing declared group is answered for rather than refused. +func local(env *resources.Environment) bool { + return env != nil && resources.Match(env.Name, resources.LocalEnvironment().Name) +} + // empties routes in every declared group the composition did not supply, // carrying no values. It never covers a group that was supplied, and never the // work-context group, which carries this run's own key set. diff --git a/pkg/run/workcontext/loader_test.go b/pkg/run/workcontext/loader_test.go new file mode 100644 index 00000000..9cc07c9e --- /dev/null +++ b/pkg/run/workcontext/loader_test.go @@ -0,0 +1,124 @@ +package workcontext + +import ( + "context" + "testing" + + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + "github.com/codefly-dev/core/resources" +) + +type stubLoader struct { + configurations []*basev0.Configuration + rootNames []string +} + +func (s *stubLoader) Identity() string { return "stub" } +func (s *stubLoader) Load(context.Context, *resources.Environment) error { return nil } +func (s *stubLoader) Configurations() []*basev0.Configuration { return s.configurations } +func (s *stubLoader) DNS() []*basev0.DNS { return nil } +func (s *stubLoader) CompositionRootWorkspaceConfigurationNames() []string { return s.rootNames } + +func group(name string) *basev0.Configuration { + return &basev0.Configuration{Infos: []*basev0.ConfigurationInformation{{Name: name}}} +} + +func groupsIn(configs []*basev0.Configuration) map[string]bool { + out := map[string]bool{} + for _, configuration := range configs { + for _, info := range configuration.GetInfos() { + out[info.GetName()] = true + } + } + return out +} + +func load(t *testing.T, inner *stubLoader, authority *Authority, declared []string, env *resources.Environment) map[string]bool { + t.Helper() + loader := WithKeySet(inner, authority, declared) + if err := loader.Load(t.Context(), env); err != nil { + t.Fatal(err) + } + return groupsIn(loader.Configurations()) +} + +func authority(t *testing.T) *Authority { + t.Helper() + made, err := Start("run-1") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = made.Close() }) + return made +} + +// A deployed composition that declares a group and supplies nothing has a +// misconfiguration, and the manager refusing to start is how it is found. +// Covering it here would turn a loud boot failure into a service running +// without the capability it said it needed. +func TestADeployedRunIsNotGivenGroupsItsCompositionDidNotSupply(t *testing.T) { + groups := load(t, &stubLoader{}, authority(t), []string{"platform", "internal-auth"}, + &resources.Environment{Name: "production"}) + for _, group := range []string{"platform", "internal-auth"} { + if groups[group] { + t.Errorf("%q was routed in empty outside a local environment, hiding a composition that supplies it nowhere", group) + } + } +} + +func TestALocalRunIsGivenTheDeclaredGroupsNothingSupplied(t *testing.T) { + groups := load(t, &stubLoader{}, authority(t), []string{"platform", "internal-auth"}, + resources.LocalEnvironment()) + for _, group := range []string{"platform", "internal-auth"} { + if !groups[group] { + t.Errorf("%q was not routed in, so the service fails at the manager before it can decide what absence means", group) + } + } +} + +// These two were conjoined: an early return skipped the declared groups +// whenever work-context happened to be authored, so a composition that +// authored one group lost the rest. +func TestAnAuthoredKeySetDoesNotSuppressTheOtherDeclaredGroups(t *testing.T) { + inner := &stubLoader{configurations: []*basev0.Configuration{group("work-context")}} + groups := load(t, inner, authority(t), []string{"work-context", "platform"}, resources.LocalEnvironment()) + if !groups["platform"] { + t.Error("platform was skipped because work-context was authored; the two are unrelated") + } +} + +// A composition that says where its key set is served keeps saying it. +func TestAnAuthoredKeySetIsNotReplacedByTheRuns(t *testing.T) { + inner := &stubLoader{configurations: []*basev0.Configuration{group("work-context")}} + loader := WithKeySet(inner, authority(t), nil) + if err := loader.Load(t.Context(), resources.LocalEnvironment()); err != nil { + t.Fatal(err) + } + count := 0 + for _, configuration := range loader.Configurations() { + for _, info := range configuration.GetInfos() { + if info.GetName() == "work-context" { + count++ + } + } + } + if count != 1 { + t.Fatalf("work-context appears %d times; the run added one beside the composition's own", count) + } +} + +// Swallowing this would leave the Manager unable to tell which groups come +// from the composition root, which it injects into every service in the run. +func TestTheCompositionRootCapabilityIsForwarded(t *testing.T) { + inner := &stubLoader{rootNames: []string{"platform"}} + loader := WithKeySet(inner, authority(t), nil) + forwarder, ok := loader.(interface { + CompositionRootWorkspaceConfigurationNames() []string + }) + if !ok { + t.Fatal("the decorator does not carry the composition-root capability at all") + } + if names := forwarder.CompositionRootWorkspaceConfigurationNames(); len(names) != 1 || names[0] != "platform" { + t.Errorf("the capability is not forwarded: %v", names) + } +}