Repository navigation
endpoints: validate ingested endpoint carriers with core's ValidateEndpointCarriers #73
Description
Goal
Every endpoint carrier this SDK ingests is judged by core's resources.ValidateEndpointCarriers before it enters the snapshot that For(ctx) reads. After that, an absent carrier is a validated absence, never a mis-keyed carrier that reads as absent.
Found while moving to core v0.19.1 (#70, #71). It was left out of #71 on purpose because it changes endpoint resolution. The README now records the gap ("Core has shipped the declaration-aware carrier validator … since v0.14.0; this SDK does not call it yet").
What core offers (since v0.14.0)
resources.ValidateEndpointCarriers(envs []string, declared []*Endpoint) error (core#716) arrived in core v0.14.0 and is unchanged through v0.19.1, the version this SDK now pins. Every CODEFLY__ENDPOINT__* entry must be exactly the canonical key of one declared endpoint (EndpointAsEnvironmentVariableKey of its module, service, name and API), delivered once, with a non-empty value. Anything else is refused by name with ErrInvalidEndpointCarrier:
- a carrier with the API of another declaration (the refusal names the declaration and its correct carrier);
- a near-miss spelling;
- a key that names no declared endpoint;
- a duplicate;
- an empty value.
The prefix is matched case-insensitively, so codefly__endpoint__… is refused as a non-canonical carrier rather than passed over. Every refusal is reported. Declarations that cannot spell a carrier, or two declarations that spell the same carrier, are refused with ErrInvalidEndpointDeclaration. The self carrier (SelfEndpointPrefix) and route carriers (RestRoutePrefix) are not judged.
Core's docs/architecture.md ("Exact comparison means a carrier whose identity is wrong…") states the contract on the SDK side: an SDK calls it at each path that ingests an environment, before publishing a snapshot, and a fallback consumes a validated absence.
Where sdk-go ingests carriers without calling it
These are as of main @ fb8c298 (v0.4.0). git grep ValidateEndpointCarriers finds nothing outside the README.
codefly.go:87LoadEnvironmentVariables: builds the snapshot fromos.Environ()plus file-carried values (fileCarriedValues). It filters with a case-sensitivestrings.HasPrefix(env, "CODEFLY__"), so a lower-casecodefly__endpoint__…is silently dropped rather than refused.codefly.go:175InjectEndpoints: checks only that module, service, name, API and address are non-empty, then publishes. Its keys are built by core, so they are canonical, but nothing checks them against what is declared to this consumer.runtime_environment_file.go:24LoadRuntimeEnvironmentFile: importsCODEFLY__*lines from an--output-envartifact into the process environment. Validation would happen on the nextLoadEnvironmentVariables, not here.codefly.go:202rebuildEnvironmentSnapshotLocked: merges the three sources into the published snapshot.
The read side is for.go:247 carrierInstance. It calls resources.FindValueInEnvironmentVariables with the key built from the selected declaration (or, with no workspace, from the query's canonical identity at for.go:229) and treats any lookup error as absent.
Why adopting it changes endpoint resolution
- Local fallback stops covering for a mis-keyed carrier. Take a process with a workspace, in
localor with no environment set. Today a carrier spelled under another API, or with a near miss, sits under no key the lookup builds.ResolveNetworkInstance(for.go:166-174) then falls back to the native address with no error, so the process talks to a native address while the composition meant another. With validation it is an error. - Failure moves from query time to ingest time, and widens. Today one bad entry affects only the query that would have read it. Core's validator refuses the whole environment. So an ingest path (
Init→LoadEnvironmentVariables,InjectEndpoints) can now fail on an entry no query ever reads. This includes a carrier for an endpoint the composition injected but that is not declared to this consumer. - Lower-case keys go from ignored to refused.
- A deployed process has no declarations on disk. With no workspace, or a workspace that is not the process's composition (endpoints: a composed module run from its own checkout resolves from its injected carriers #66/fix(endpoints): the workspace on disk counts only if it is the composition the runtime names #68), there is nothing to pass as
declared. Today the carriers there are authoritative and keyed by the query's canonical identity. Calling the validator with an emptydeclaredwould refuse every carrier and break every deployed process. Skipping it silently would leave the rule false exactly where carriers are authoritative. The fix has to choose, and the right answer may belong to the CLI, which renders the composition, rather than to this repo. If so, the deliverable here includes the issue against that owner.
What a fix must prove
Tests are in package codefly_test with t.Setenv and a LoadEnvironmentVariables reload after each mutation, so nothing asserts against a stale snapshot.
- Wrong API, workspace present,
local: a carrier for a declared endpoint under another API is refused withErrInvalidEndpointCarrier(viaerrors.Is) and no native fallback happens. The same test fails onmaintoday: set it back and confirm it breaks. - Near miss, lower case, duplicate, empty value: each is refused by name, at each ingest path (
LoadEnvironmentVariablesfrom the environment and from a file carrier,LoadRuntimeEnvironmentFilefollowed byInit,InjectEndpoints). - A refused ingest publishes nothing: the previous snapshot stays in force, and no partial snapshot is visible to a concurrent
For(ctx)(-race). - A validated absence still falls back: with every carrier canonical and the queried one simply absent, local fallback works exactly as today.
- The workspace-free path is decided, not defaulted: whatever source of
declaredis chosen for a deployed process, the composition-root, deployed and foreign-workspace tests (endpoint_deployed_test.go,endpoint_composition_root_test.go,endpoint_foreign_workspace_test.go) pass unchanged. A test also pins the chosen behaviour for a carrier with no declaration. - Docs: the README paragraph on carriers ("A carrier keyed differently from the selected declaration…") and the
AGENTS.mdrule on carriers describe the new behaviour in the same PR. The PR states which consumers could see a new boot-time refusal.
References
- Move to core v0.19.1 and release #70 and build(deps): move both modules to codefly-dev/core v0.19.1 #71 (the core v0.19.1 move, released as v0.4.0), where this gap was found and the README corrected.
- core#716 (
ValidateEndpointCarriers), coredocs/architecture.md.
Activity