Skip to content

fix(endpoints): the workspace on disk counts only if it is the composition the runtime names - #68

Merged
antoinetoussaint-byte merged 3 commits into
mainfrom
issue-66-endpoints-a-composed-module-run-from-its-own
Oct 12, 2026
Merged

antoinetoussaint-byte merged 3 commits into
mainfrom
issue-66-endpoints-a-composed-module-run-from-its-own

Conversation

@antoinetoussaint-byte

@antoinetoussaint-byte antoinetoussaint-byte commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Closes #66. Supersedes #67 (see below).

Summary

ResolveNetworkInstance took eligibility from whatever workspace FindWorkspaceUp found above the process. A composed module run from its own checkout, a released tag resolved into CODEFLY_MODULE_CACHE, or a solution's tag finds its producer's workspace.codefly.yaml first. That file says nothing about the composition the process runs in:

  • module-saas-starter declares the module as saas-starter in saas-starter-dev, not saas, so every query failed with cannot find module <saas>. Services that resolve their own endpoint at boot exited, which is why no released host above 0.0.93 boots.
  • robin, annotations, documents, model and runtime declare the same module name as platform-core does. Their producers' workspaces were trusted: selection ran against the producer's declarations, and in local the native address was derived from the producer's workspace name (review probe: port 43292 instead of the composition's 42032).
  • core-solutions' solutions import their platform by release. Loading that workspace fails (requires host resolution) before any carrier is read.

The rule (internal/enclosing, used by every reader that falls back to the workspace on disk):

  • With CODEFLY__WORKSPACE injected: a found workspace counts only if its file declares that name. A workspace with another name is never loaded; only its name is decoded. Core's EnvironmentVariableManager.SetIdentity sets this variable for every agent-run service, and the booted processes carried CODEFLY__WORKSPACE=platform-core.
  • With nothing injected: no composition started the process (a local tool, a plain go test or go run; codefly run and codefly test both inject it), so the workspace found is the only one there is and is used as found. That is today's behaviour, held by TestAProcessNoCompositionStartedUsesTheWorkspaceAsFound. A plain codefly run at a composition root injects the root's own name, so it matches.
  • The name is compared exactly as written, so a name that differs only in case does not count.
  • A workspace file that does not decode cannot name itself, so under an injected composition it does not count and the injected carrier still answers. When no carrier was injected either, the refusal (endpoint, configuration fallback, fixture lookup) carries the file's decode error, because that file may have been the composition's own. With nothing injected, the decode error is returned as before.
  • A workspace that counts must also carry the queried module, by core's Workspace.Member. Modules composed through workspaces: or solutions: count, and the native address is computed under the composition's name.
  • Anything else is answered as a workspace-free process is: from the injected carrier. An absent carrier returns ErrEndpointCarrierAbsent; a malformed one is refused as malformed. When a counted workspace does not carry the module (a typo at a composition root), the refusal still names the modules it does carry: does not compose module <x>, present: [...].

The same rule now decides the other workspace readers:

  • the local configuration and secret files behind Configuration / Secret;
  • the packages fixture.Selected().Principal resolves;
  • the name codefly.Workspace reports when no identity is injected. That name is now read from the file without composing it, so a local tool under a solution's tag gets its name rather than a composition error.

The container-context note in the issue is a different defect, and main already fixes it. v0.3.1 refused a workspace-free process in local or unset environments with ErrNoDeclaredEndpoints behind its deployed() gate. #62 removed that gate. Putting the gate back fails TestDeployedProcessResolvesItsCarrierWithoutAWorkspace, TestDeployedProcessRefusesAnAbsentCarrierWithTheSentinel and TestLocalProcessWithoutAWorkspaceRefusesAMalformedCarrier.

Docs changed:

  • README.md: endpoint resolution, the composition rule, the other readers.
  • AGENTS.md: the "deployed process has no workspace" rule, and the fixture/ row, which pointed at a file that moved. AGENTS.md was 205 lines on main, failing TestAgentContextRootStaysWithinItsBudget since fix(workcontext)!: core's verifier is not re-exported; a verifier's keys come from an admitted endpoint, on the host's literal transport rule #53. It is now 200: the coverage-gate table duplicated the dual-module-change skill's "Coverage, which is two different gates", so it became a pointer to the skill.
  • scripts/allowed-imports.txt gains root github.com/codefly-dev/sdk-go/internal/enclosing. The deny-by-default gate refuses the new internal import without it. It is this module's own package and imports only core resources.

Why not #67. #67 makes the earlier one-line decision with a hand-rolled name loop. It also truncates workcontext/AGENTS.md from 422 lines to 7. #67 should be closed.

Fix or hack

Fix, at the owner of endpoint resolution. Which composition a process runs in comes from what the runtime injects, and membership comes from core's own predicate; neither is a second implementation. The AGENTS.md and allowlist changes are documentation and gate bookkeeping, not behaviour.

Test plan

  • go test ./... and (cd workcontext && go test ./...)
  • go test -race ./... and (cd workcontext && go test -race ./...)
  • golangci-lint run in each module (0 issues)
  • go mod tidy clean in each module; scripts/check-one-implementation.sh ok

endpoint_foreign_workspace_test.go covers four layouts:

  • a module checkout;
  • a module-cache tag;
  • a same-name producer tag (F1);
  • a solution tag importing its platform by release (F2).

Each layout first asserts what trusting its workspace would do: refuse the module, carry it under the producer's name, or fail to load. The cases then run across environments local, unset and production, carriers from runtime and embedded sources, and every reference form. Every case resolves the carrier, refuses an absent one with the sentinel, refuses a malformed one as malformed, and never reads the producer's configuration files.

endpoint_composition_root_test.go covers a root (platform-obin) that imports platform-core through workspaces: and adds wiki through solutions:, with the composition injected and with nothing injected (F3, F4):

  • selection judges both modules first (a private endpoint is refused even with a carrier present);
  • the native address is computed under the root's name;
  • a typo's refusal names the carried modules.

Also: TestFixturePrincipalReadsOnlyTheInjectedComposition and TestWorkspaceNamesAWorkspaceOnlyTheCLICanCompose (F5). The pre-existing TestEndpointResolutionFallsBackToDeterministicLocalWorkspace now requires the instance before dereferencing it (F6). The shared test helpers clear any ambient CODEFLY__WORKSPACE.

Mutations, each run against the whole root module and fixture, with no panics:

mutation failing tests
ignore the injected name the 4 foreign-workspace endpoint/config tests and the fixture test (same-name and solution-tag layouts)
trust the found workspace (main's behaviour) the 3 foreign-workspace endpoint tests and the typo-diagnostic test
carries rejects members composed from another workspace both composition-root selection tests
carries always false 15 tests, now all reported (F6)
drop the present-modules diagnostic TestACompositionRootNamesWhatItCarriesWhenAModuleIsUnknown
fixture reads the found workspace TestFixturePrincipalReadsOnlyTheInjectedComposition
Workspace() composes the file TestWorkspaceNamesAWorkspaceOnlyTheCLICanCompose
configuration reads the found workspace TestAComposedModuleUnderItsOwnWorkspaceReadsNoProducerConfiguration
compare the name case-insensitively TestACompositionRootCountsOnlyUnderTheExactInjectedName
load an undecodable file anyway TestAnUndecodableWorkspaceFileIsReportedNotTrusted
drop the decode error from the refusal TestAnUndecodableWorkspaceFileIsReportedNotTrusted

Local end-to-end proof (local override, no release)

The setup:

  • released CLI 0.1.185;
  • obin-ai/platform-core main b095b29, which pins saas 0.0.94;
  • codefly-dev/module-saas-starter v0.0.94 (9f6a4aa);
  • run as codefly run service frontend --fixture dev-admin --headless --runtime-context native, each run under its own --naming-scope.

The SDK comes from an uncommitted code/go.work (use . <sdk-go checkout>) in each Go service; the go-grpc agent honours a source-owned code/go.work, not one at the repository root. accounts/code/fixtures/seed.go moved to fixture.Selected() locally (see below).

run saas resolved from sdk-go result
checkout codefly.local.yaml resolve.saas.path: <v0.0.94 checkout>/module de65fa9 telemetry, accounts, auth-gateway, frontend Running; frontend ✓ Ready, GET / 200; accounts JWKS 200
module cache …/module-cache/codefly-dev/module-saas-starter/v0.0.94/module de65fa9 telemetry, accounts, auth-gateway, frontend Running
checkout same as the first row a1885cc (previous head) Running, the same as the first row
checkout same as the first row main 1ce8b27 telemetry: Codefly collector gRPC endpoint is unavailable: cannot find module <saas> in <saas-starter-dev>, present: [saas-starter], run exit 1 (the reviewer reproduced this too)

On de65fa9, telemetry and accounts ran with:

  • CODEFLY__WORKSPACE=platform-core and CODEFLY__MODULE=saas;
  • their working directory under the checkout whose workspace.codefly.yaml is name: saas-starter-dev;
  • dep github.com/codefly-dev/sdk-go (devel).

Telemetry logged OpenTelemetry collector listening on Codefly gRPC endpoint …. Accounts logged fixtures applied fixture=dev-admin, then its gRPC, REST, module-authority and Connect servers started.

The host needs one change to build on this SDK, regardless of this PR. #65 moved the fixture API, so module/services/accounts/code/fixtures/seed.go:185 must change codefly.Fixture() to fixture.Selected().

Not verified

  • The same-name producer and solution-tag layouts end to end. Neither is in the frontend's dependency closure, so this boot does not exercise them; they are covered by the unit tests and mutations above.
  • Container runtime context, end to end. It fails before any SDK code runs. The go-grpc agent 0.1.54's container build does not mount a module-cache service's source directory, so go mod download fails with chdir to cwd (…/services/telemetry/code) … no such file or directory. That belongs to the go-grpc agent / CLI container runner.
  • The receipts Postgres store tests skipped locally (no CODEFLY_TEST_POSTGRES_DSN); CI runs them.
  • No deployed (Kubernetes) environment was exercised. That path is unchanged: no workspace is on disk there.

🤖 Generated with Claude Code

antoinetoussaint-byte and others added 2 commits October 12, 2026 14:06
… not the process's composition (#66)

A composed module run from its own checkout, or a released tag resolved into
CODEFLY_MODULE_CACHE, finds the producer's own workspace.codefly.yaml above the
process. It declares the module under the producer's name (saas-starter in
saas-starter-dev), not the composition's (saas), so selection refused every
endpoint query with "cannot find module <saas>", and services resolving their
own endpoint at boot exited.

ResolveNetworkInstance now trusts a found workspace only if it carries the
queried module, by core's Workspace.Member (the lookup LoadModuleFromName
resolves through). Otherwise the process is answered exactly as a
workspace-free one: from the injected carrier, refusing absence with
ErrEndpointCarrierAbsent; never the producer's declarations or native map.

AGENTS.md was 205 lines on main and failed its 200-line budget test; the
coverage-gate table it duplicated from the dual-module-change skill becomes a
pointer to that skill.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ition the runtime names (#66)

Review of #68 (F1-F6). Carrying the queried module is not the same as being
this process's composition. robin, annotations, documents, model and runtime
carry the same name in their producers' workspaces as in platform-core, so
those were still trusted: selection against the producer's declarations, and in
local a native address derived from the producer's workspace name. And a
solution's tag, whose workspace imports its platform by release, failed to load
("requires host resolution") before any carrier was read.

internal/enclosing now decides it once for every reader. With
CODEFLY__WORKSPACE injected (core's SetIdentity sets it for every agent-run
service), a found workspace counts only if the file declares that name, and one
that does not is never loaded. With nothing injected, no composition started
the process and the workspace found is used as found. A counted workspace must
still carry the queried module; one that does not is answered from the
carriers, and an absent carrier's refusal keeps the modules it does carry.

The same rule now decides the Configuration/Secret local fallback, the fixture
packages Principal resolves, and the name Workspace() reports, which is read
from the file without composing it.

Tests: same-name producer and solution-tag layouts under an injected
composition; a composition root carrying modules through workspaces: and
solutions:, its native address under its own name, and the modules a typo's
refusal names; the configuration, fixture and Workspace() readers. The
pre-existing test that dereferenced a nil instance now requires it, so a
mutation reports every failure instead of one panic.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@antoinetoussaint-byte antoinetoussaint-byte changed the title fix(endpoints): a workspace that does not carry the queried module is not the process's composition fix(endpoints): the workspace on disk counts only if it is the composition the runtime names Oct 12, 2026
…jected name is matched exactly (#66)

Verification of #68 (N1, N2). Under an injected composition, a workspace file
that does not decode cannot name itself, so it still does not count and the
injected carrier answers. But when no carrier was injected either, that file
may have been the composition's own, so enclosing.Composition now returns it
as Unreadable and the refusal (endpoint, configuration fallback, fixture
lookup) carries its decode error instead of dropping it. With no composition
injected, the decode error is returned as before.

Tests pin the exact-match rule (a case-folded name does not count) and the
undecodable file in all three cases: injected with a carrier, injected without
one, and nothing injected.

Docs: `codefly test` injects the identity too; only a plain `go test` or
`go run` runs without it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte merged commit 9627078 into main Oct 12, 2026
16 checks passed
@antoinetoussaint-byte
antoinetoussaint-byte deleted the issue-66-endpoints-a-composed-module-run-from-its-own branch October 12, 2026 20:13
antoinetoussaint-byte added a commit to saurabhmaheshwari-obin/module-saas-starter that referenced this pull request Oct 12, 2026
…inside a composition (codefly-dev#1095)

sdk-go v0.3.1 resolved a service's own endpoint from the first workspace
file above the process. A host run from its checkout or from a released
tag in the module cache found its own development workspace, which names
the module differently from the composition, so telemetry and accounts
exited at start. v0.3.2 (codefly-dev/sdk-go#68) trusts a found workspace
only when it is the process's injected composition.

accounts' fixture seeding moves from codefly.Fixture() to
fixture.Selected(), the API sdk-go#65 moved it to.

Co-authored-by: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

endpoints: a composed module run from its own checkout resolves from its injected carriers

1 participant