Repository navigation
A host cannot express "reachable by what composes me": internal + allow_modules makes it name its own consumers #711
Description
module was "every module may reach this", and v0.11.0 replaced it with internal plus an explicit allow_modules list. For an ordinary module that is a strict improvement: the grant becomes visible, enforceable and reviewable instead of implicit.
For a host it appears to require something the boundary rules forbid.
The problem
A host is reached by the modules that compose it. Writing its endpoints as internal with allow_modules means the host enumerates the modules permitted to call it — and concepts/boundaries.md is explicit that a module never names what composes it. The host would carry, in its own interface declaration, a list of its consumers.
Three consequences, none of them hypothetical:
- The list is unmaintainable at the right layer. Every new module that composes the host becomes an edit to the host's own declaration, in the host's repository, by someone who may not own either side.
- It inverts the dependency. Today a module names what it consumes; this makes the consumed name its consumers.
- The alternative available today is worse.
allow_modules: ["*"]is accepted and means "every module", which is exactly the semanticsmodulehad — so the only way for a host to avoid naming its consumers is to write the wildcard, and the model's improvement is reduced to a spelling change.
Where it surfaced
codefly-dev/module-saas-starter has nine endpoint declarations still on visibility: module, and every CI run now emits a deprecation warning per declaration. The work to move them was examined and declined deliberately rather than deferred: it is not mechanical (cataloggen's topology and network-policy generation are keyed on "module", and catalog.proto carries ENDPOINT_VISIBILITY_MODULE), and more importantly the migration cannot be done correctly until this question has an answer. The reasoning is recorded in codefly-dev/module-saas-starter#1021.
What this issue asks for
Not a rename. An answer to: how does a host express "reachable by whatever composes me" in the new model, without naming its composers?
Candidate shapes, offered to be argued rather than chosen:
- A visibility value that means reachable from within the composition — the permission a host actually wants — distinct from both
private(same module) andinternal(an enumerated list). allow_modulespopulated by the composition at render rather than authored by the host, so the list exists and is enforceable but the host does not write it. This may already be what the CLI could do, given it performs the join.- A ruling that
allow_modules: ["*"]is the correct and intended answer for a host, in which case say so on the page, because today it reads as the thing the model was built to remove.
Why it matters beyond the warning
v0.11.0's own rationale is that a value the model does not know is refused everywhere rather than read as private by one path and denied by another. That strictness is right. But it means the deprecation is on a path to becoming a refusal, and when it does, every host endpoint either names its consumers or declares a wildcard. Deciding which before that happens is cheaper than deciding it after.
Refs codefly-dev/module-saas-starter#1021, obin-ai/handbook#196.
Activity
antoinetoussaint-byte commentedon Oct 6, 2026
The answer, from the owner: the ask lives with the asker, never with the target
That resolves this, and it is sharper than any of the three candidates above. The defect is not the vocabulary — it is the authorship of
allow_modules. A grant written on the target is the target naming its consumers, which is the boundary violation, and no new visibility value fixes that.The mechanism already exists, on the correct side. A service declares what it requires:
Service.ServiceDependencies(resources/service.go:43), carried on the wire asservice_dependencies(proto/codefly/base/v0/service.proto:31). The asker already states its need, in its own repository, as part of its own declaration.allow_moduleshas no counterpart on the asking side because it is the answer, not the question.So:
visibility: internalmeans "reachable from within the composition, not from outside it". It names nobody. That is the whole of what a host can honestly say about itself.What that buys, beyond fixing the boundary:
Consequence for the authored field. If
allow_modulesis derived, then a hand-authoredallow_moduleson an endpoint is a declaration of something the author does not own, and under the model's own strictness — a value the model does not know is refused everywhere rather than interpreted — the honest end state is that authoring it is refused, not merely discouraged. That is the same shape aslocation: externalreplacingvisibility: external: the thing was written in the wrong place, so the field moved rather than being renamed.That also disposes of the wildcard:
allow_modules: ["*"]stops being the only escape a host has, because the host no longer writes the list at all.Not proposed here: who validates that a declared dependency is permitted — a module asking for a host endpoint is not the same as being allowed to have it. That is an authorization question and it belongs with the installation and approval layer (obin-ai/handbook#196,
design/deployment-context/registry-specification.md), not with endpoint visibility. This issue only asks where the list is authored.Refs codefly-dev/module-saas-starter#1021.
antoinetoussaint-byte commentedon Oct 6, 2026
Clarification: the vocabulary is complete; only the authorship is wrong
A detour worth closing so this issue does not carry two readings.
I briefly pursued the idea that the model is missing a rung — a visibility between "within the module" and "outside the workspace" — and that
allow_modulesexisted to fake it. That is wrong.privateis the within-module rung. The ladder is complete:privateinternalpublicSo no new visibility value is needed, and
location: externalcorrectly sits beside visibility rather than inside it.The defect is only the authorship of
allow_modules, exactly as the comment above states: it is derived from the consumers' declaredservice_dependencies(resources/service.go:43), never authored on the target, because authoring it makes a host name its own consumers — which the boundary rules forbid and which puts the maintenance of the list in the wrong repository.One further distinction, raised by the owner and worth recording here because it bounds what this field can ever be:
A static allow-list is reachability, not authorization.
allow_modulescan only answer "may anything in module X reach this endpoint at all", which is a deployment-time fact enforced by mesh policy and NetworkPolicy. Whether a particular call is permitted — on whose behalf, for which installation, within which scopes — is per-call and runtime, and that machinery already exists:WorkContextV1carriesaudience("the exact service or trust boundary allowed to consume it"),authority_scopeswhere each hop's grant must be a subset of the preceding effective scopes, and asealbinding the capability to one installation and one execution.So the two must not be conflated, and neither substitutes for the other: derive the static list from declared dependencies for defence in depth, and never let it stand in for the runtime check.
antoinetoussaint-byte commentedon Oct 6, 2026
visibilityis doing two jobs: who may call, and how it is addressedThe owner's observation —
public"at least means LB access" — is correct, and it changes what this issue asks for.publicis not only an authorization statement; it has an addressing consequence.The evidence. At
network/runtime_manager.go:247, a public endpoint receives an additional network instance beyond its container and native addresses:And
resources/endpoint.go:909exists so a caller can ask the question directly:So one word asserts two independent facts:
AllowsModule— public consults nobody, it is openPublicDefault, and whatever the deployment renders from itWhy that is a defect and not a convenience. Those two facts do not have to agree, and today there is no way to state them separately. An endpoint that every module in the system should reach but that must not be externally addressable cannot be declared: it must choose
publicand get the external address, orinternalwith a wildcard allow-list — which is the pattern this issue was opened to remove. The conflation therefore creates the wildcard it complains about.This split is already precedent in the same model. v0.11.0 removed
externalfrom visibility for exactly this reason, in its own words: "externalwas a location written as a permission, now written aslocation: externalbeside the visibility that applies" (resources/endpoint.go:43). Addressing was separated from permission once already;publicis the half that did not get separated.Also worth stating plainly on the page, because the name invites a specific wrong reading:
publicdoes not mean "on the internet". Internet exposure isingress— a route with hostnames, declared in the deployment, not on the endpoint (apublicendpoint with no ingress is reachable by any module and published nowhere). An engineer who readspublicas "internet" will avoid it for things that legitimately need system-wide reach and reach forinternalplus["*"]instead.What this issue now asks for, consolidated
visibilityasserts reach only —private(same module),internal(across modules),public(unrestricted). No addressing consequence attached to any of them.locationalready is.allow_modulesis derived, never authored — computed from consumers' declaredservice_dependencies(resources/service.go:43) by the composition that performs the join, because authoring it on the target makes a host name its own consumers. Authoring it by hand should end up refused, not discouraged.WorkContextV1carriesaudience,authority_scopeswhere each hop's grant is a subset of the preceding effective scopes, and asealbinding the capability to one installation and one execution. Neither substitutes for the other.Each is independently landable and (1)+(2) are the breaking pair.
antoinetoussaint-byte commentedon Oct 6, 2026
lazybox has finished working on this.
claude(Opus 5) held it from 2026-10-07 14:08 UTC until 2026-10-07 20:34 UTC. Theworkinglabel has been removed; this comment is the record.claim detail
feat(resources)!: visibility is reach, exposure is addressing, and th…
feat(resources)!: visibility is reach, exposure is addressing, and th…
feat(resources)!: visibility is reach, exposure is addressing, and th…
fix(services,agents,sdk): every holder of dependency addresses judges…
fix(solutionhost): an endpoint's reach is held to the model's vocabul…