Skip to content

fix!: register addresses a deployed solution can actually be reached at - #37

Merged
antoinetoussaint-byte merged 1 commit into
mainfrom
fix/deployed-solution-registration
Sep 24, 2026
Merged

antoinetoussaint-byte merged 1 commit into
mainfrom
fix/deployed-solution-registration

Conversation

@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor

Why

A deployed solution registered manifestUrl: http://localhost:8080/assets/mf-manifest.json and upstream http://localhost:8080. That is its own listen address, which it got through the PUBLIC_URL default. The product showed "solution failed to load", the gateway proxied the solution to itself, and the process still booted and looked healthy.

What changes

  1. Root-relative manifest (frontendManifestURL). When PUBLIC_URL is unset, manifestUrl is /assets/mf-manifest.json, a path on the solution's own backend. The host resolves it against the route it uses to reach the solution. The runtime does not encode the host's route layout. An explicit PUBLIC_URL still makes the URL absolute on that origin.
  2. Reachable upstream (selfUpstream / selfEndpoint). The gateway upstream comes from the self-endpoint carrier that core adds: CODEFLY__SELF_ENDPOINT__<MODULE>__<SERVICE>__HTTP__HTTP, normalised like CODEFLY__ENDPOINT__. The existing self CODEFLY__ENDPOINT__ stays the listen address. If the carrier is absent, the upstream falls back to the listen address. SELF_UPSTREAM still overrides it, and PUBLIC_URL no longer feeds it.
  3. Boot refusal (validate()). A loopback upstream or PUBLIC_URL is refused in a deployed runtime context: localhost, *.localhost, loopback IPs, or the unspecified address.
    • Signal: CODEFLY__RUNTIME_CONTEXT. The local kinds native, nix, container and free (or unset) count as local. Any other value counts as deployed, for example the kubernetes value that core's GitOps render will inject.
    • Never the environment name. Tests pin local-dogfood + kubernetes as refused and staging + native as accepted.
  4. Exchange errors (solutionCredential.exchange).
    • Only 401/403 produce the "no SOLUTION_REGISTRATION_SECRETS entry / digest mismatch" message.
    • A 5xx reports host unavailable (status N), retrying: <gateway body>.
    • Any other status reports a failure with its body.
    • The heartbeat keeps retrying all of them, on the short cap.
  5. Manifest.Assets fs.FS (additive). When it is set, /assets/ is served with http.FileServerFS from it, so nothing is read from disk and a read-only root filesystem works. When it is nil, ASSETS_DIR works as before. The CORS headers and content types are the same on both paths.
    • Content-hashed files are served immutable.
    • Everything else (the manifest, the remote entry) is served no-cache, and so is any non-file response. This means a 404 for a hashed name is never cached for a year.

README (configuration table, two new sections, exchange statuses) and AGENTS.md (the one localhost literal) are updated to match.

Classification

  • 1, 3, 4, 5: fix, at the owner of the behaviour.
  • 2: fix, with a labelled stopgap. The carrier is read by name in one helper (selfEndpoint, commented "PENDING codefly-dev/core v0.5.6") until core/sdk-go release an accessor. The runtime-context signal is also pending core v0.5.6. Until a cell is rendered by that core, it has neither the carrier nor the signal: it falls back to the listen address and is not refused.

Breaking (!)

This breaks on the wire, not in the Go API: Manifest.Assets is additive. A host has to resolve a root-relative manifestUrl, which is the parallel module-saas-starter change. Against an older host, a solution on this version registers a manifest that host cannot load.

Tests

  • TestManifestURLIsNeverTheListenAddress; TestServeRegistersOnListenPort now asserts the relative path is served.
  • TestSelfUpstreamIsTheReachableSelfEndpoint and TestGatewayRegistrationCarriesTheReachableUpstream (the carrier reaches the wire).
  • TestValidateRefusesLoopbackInADeployedRuntime and TestLoopbackRefusalIgnoresTheEnvironmentName.
  • TestSolutionExchangeBlamesTheCredentialOnlyWhenItWasJudged (401/403/500/502/503/429) and TestSolutionRegistrationKeepsRetryingAnUnavailableHost.
  • TestAssetsServeFromTheManifestFS (ASSETS_DIR points at a missing dir), TestAssetsServeFromAssetsDirWithoutAnFS and TestAssetCacheControl.

All four gate commands from AGENTS.md pass locally (go mod verify, go test -race ./..., go vet, gofmt).

Not verified

  • The suite is hermetic (httptest on 127.0.0.1). It has not been run against a real composition or cell.
  • Not verified: that a host resolves the root-relative manifest, that the gateway proxies to the in-cluster upstream, or a pod with readOnlyRootFilesystem.
  • The carrier name and the kubernetes runtime-context value come from the coordinated core change, which was not released when this was written. If core ships different names, selfEndpointCarrierPrefix and deployedRuntimeContext must follow before this is tagged.

🤖 Generated with Claude Code

A deployed solution registered `manifestUrl: http://localhost:8080/assets/mf-manifest.json`
and upstream `http://localhost:8080` — its own listen address, via the
PUBLIC_URL default — so the product showed "solution failed to load" and the
gateway proxied the solution to itself. Both booted and looked healthy.

- manifestUrl: with no PUBLIC_URL, register the root-relative path
  /assets/mf-manifest.json on the solution's own backend; the host resolves it
  against the route by which it reaches the solution. The runtime does not
  encode the host's route layout. BREAKING on the wire: the host must resolve a
  root-relative manifestUrl (the parallel module-saas-starter change).
- upstream: take it from the reachable self endpoint Codefly injects as
  CODEFLY__SELF_ENDPOINT__<MODULE>__<SERVICE>__HTTP__HTTP (core v0.5.6), falling
  back to the listen address; SELF_UPSTREAM still overrides, PUBLIC_URL no
  longer feeds it. Labelled stopgap: the carrier is read by name in one helper
  until core/sdk-go release an accessor.
- validate(): refuse a loopback upstream or PUBLIC_URL when
  CODEFLY__RUNTIME_CONTEXT names a deployed runtime (anything outside
  native/nix/container/free, e.g. a GitOps render's "kubernetes", core v0.5.6).
  Never keyed on the environment name.
- exchange: only 401/403 name the SOLUTION_REGISTRATION_SECRETS provisioning; a
  5xx reports "host unavailable (status N), retrying" with the gateway's body,
  other statuses a failure with their body; the heartbeat keeps retrying.
- Manifest.Assets fs.FS: when set, /assets/ is served from it (nothing read
  from disk, so a read-only root filesystem works); else ASSETS_DIR as before.
  Content-hashed files are cached immutable, everything else and every
  non-file response no-cache.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte merged commit 89e1aea into main Sep 24, 2026
1 check passed
@antoinetoussaint-byte
antoinetoussaint-byte deleted the fix/deployed-solution-registration branch September 24, 2026 00:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant