Codefly defines the configuration contract. Infrastructure producers, including infra-base, emit it. The CLI does not translate a producer's resource inventory, database engine conventions, proxy mode, or credential naming scheme.
environments.CoordinateContract schema codefly/coordinate/v1 wraps the existing
environments.Environment model under environment. JSON keys inside that object
are exactly the workspace YAML keys. The CLI owns this deployment document; it projects runtime context into Core. Core does not parse the coordinate contract.
coordinate is an optional opaque provenance label; the document is named for
its subject, and "cell" is not a word the model has.
There is one spelling. A document naming itself codefly/cell/v1 or
codefly/cell/v2 is refused: under a strict parser the alternative to an error
is a silent guess, and a permanent fallback is how a retired word outlives every
decision to retire it.
Examples accepted by the CLI are in ../pkg/environments/testdata/coordinates/managed-identity.json
and ../pkg/environments/testdata/coordinates/password-auth.json. They are contract fixtures,
not claims that a particular infrastructure producer already emits this format.
Producers must supply:
-
The selected environment name and namespace. Import refuses a different target rather than reusing credentials or delivery paths resolved for another target.
-
Each managed service under its explicit service key, with its endpoint and port. Multiple services are supported without assigning the first one to
store. The key is<module>/<service>, or a bare<service>where only one module of the workspace declares that name. Composed modules routinely ship a service of the same name — aredis, say — and a bare key covers every one of them, so all of them would render with this entry's address and secrets while only one was meant; a bare key matching several is refused at workspace load, naming the candidates. A qualified key matching no service is refused too: it is a typo in the module or the service name, and the service it should have replaced would otherwise deploy as its module declares it with nothing reporting the entry went unused. A bare key matching nothing stays inert, so a contract may declare a fleet's managed services for a workspace composing only some of them. -
Explicit secret references, including the target Secret name, remote key, optional property and secret store. No declaration means no reference is added; it does not assert that the service is passwordless.
-
Any workload identity principal and opaque annotation/label attachments. Identities and secret references can coexist. The CLI does not infer an auth mode. There are two homes, for two questions. A managed service's
identityis reachable only through a managed service the workload consumes; what a workload authenticates as regardless of that goes underservice-identity, keyed the wayservice-configandservice-secretsare — an environment-widedefaultplus the services that differ. A per-service entry replaces the default outright: an override is total, and must restate the annotations and labels it still needs. A principal whose platform attachment is missing authenticates as nothing, so an override that carries only a principal is a declaration that cannot work where the identity webhook keys off a label. Unknown keys in either block are refused rather than dropped: a mistypedserviceswould otherwise leave a validdefaultstanding and silently collapse every override onto it. -
Any labels the cell needs on a service's Service under
service-mesh, keyed<module>/<service>: today the waypoint attachment that puts the cell's protected waypoint in front of a presenter. The values are the cell's convention, so the render carries them verbatim and derives none of them:service-mesh: services: shop/gateway: labels: istio.io/use-waypoint: <namespace>-waypoint istio.io/use-waypoint-namespace: istio-waypoint
The render stamps them as
metadata.labelson that service's Service, as a patch in the unit's environment overlay, and on no other Service: a Service is the finest attachment there is, so every port of an attached Service goes behind the waypoint's L7 hop. An entry is refused, naming the service, unless the service declares a presenter endpoint undercell:; a service the workspace does not have, an unknown key, acodefly.dev/label and a label the rendered Service already carries are refused too. -
Where every workload exports operational telemetry, under
telemetry: the OTLP intake the cell's infrastructure provides, resolved by the producer like every other address, as anhttps://(or, declaredinsecure,http://) URL:telemetry: otlp-endpoint: https://otel.internal.ops.example:443
The CLI delivers it to every service it runs or renders as
OTEL_EXPORTER_OTLP_ENDPOINT, withOTEL_SERVICE_NAMEandOTEL_RESOURCE_ATTRIBUTES(codefly run,docs/commands.md: "Where a service's telemetry goes"). No module declares a configuration group for it. -
Any application secret mappings through the existing
service-secretsmodel. -
Any resolved non-secret values through
service-config, keyed by consuming service and then by the exact key the service reads. The producer resolves the value; The CLI carries it verbatim and derives none of it. A declared service with nothing to inject, or a value that resolved to empty, is refused at load.
Values and secret references are two blocks, not one dictionary of either-or
entries: a resolved value belongs in service-config, a reference in
service-secrets. One service declaring the same key in both is refused rather
than resolved — both render an entry of that name into one container, where one
silently overwrites the other. The refusal compares explicit remote-keys; a
defaults template covers whichever of a service's own keys are declared
secret, which the environment block alone cannot see.
- Resolved delivery repository, branch and path when declaring a GitOps target. The CLI neither appends the namespace nor chooses a branch.
Local configuration can declare configuration-profile (or the explicit chain
configuration-profiles, e.g. [staging, local]: each configuration location
reads the first profile it holds, never merging profiles — Core's
docs/workspace-composition.md) and secrets without a cluster or registry. Import is configuration admission, not deployment approval:
CLI still validates the selected operation, service graph and deployment target.
Local secret resolution and deployed secret projection remain separate consumers
of the existing configuration model; secret values do not belong in descriptors.
Unknown fields and required capabilities are rejected. A producer declaring an
identity on a managed service uses
requires_capabilities: ["managed-service-identity"]; one declaring a consuming
service's own identity uses ["service-identity"].
Proxy containers, image choices and loopback routing are not part of this contract.
A workload a host admits proves who it is with a ServiceAccount token the render projects for the host's audience. Two things reach the container, and both are the render's:
- the
codefly-workload-identityvolume, mounted read-only at/var/run/secrets/codefly, with the token attokeninside it; and CODEFLY__WORKLOAD_TOKEN_FILE, carrying that file's full path.
The mount alone is not the contract. The SDK's ProjectedTokenFile is a
path value its caller supplies and reads no environment itself, and the
consumer that presents the credential reads only this variable and refuses when
it is absent — it infers no default path. A workload that mounts the token and
is told nothing therefore acquires no publication lease, and its readiness
refuses, with a correctly rendered volume and ServiceAccount. A native run is
handed the same variable name, from the same constant, pointing at the path it
writes the projection to; deployed and native differ only in the path.
The value is derived from the mount and the filename rather than written twice, so the projection and the path it advertises cannot drift. Only the container that mounts the token is told about it — never a sidecar beside it.
The variable's name contains TOKEN, so it is classified as a credential key,
and a literal value under such a key is normally refused: that is how a secret
would otherwise become a plaintext manifest. A mount path is not secret
material, so the render admits exactly one pair — this variable name
carrying exactly the path it mounts — and refuses everything adjacent: a
different value under the same name, the same path under another
credential-named key, a real token literal, and the variable on any container
that does not mount the token at that path. There is no general exemption for
*_FILE names and the classifier itself is unchanged.
A workspace configuration value may be bound to a file in a composed module instead of being written out as a literal, so a public artifact a module already ships — an installation document, for example — reaches services as configuration without being copied into the composition and kept in step by hand.
The binding is declared in the selected profile, in
configurations/<profile>/module-artifacts.codefly.json, and names only a
destination and a source:
{
"schemaVersion": 1,
"bindings": [
{"configuration": "docs", "key": "README",
"source": {"module": "wiki", "path": "docs/install.md"}}
]
}Acquired once per ordinary load, and nothing is frozen across loads. Every
ordinary configuration load — a run's delivery, a codefly doctor check —
acquires the declared file afresh and carries its exact bytes, with the SHA256
and byte count core computes from those bytes, on the value that load
delivers. A standalone check therefore describes what it observed and proves
nothing about a later delivery; an already running service keeps the
configuration it was delivered until its ordinary lifecycle replaces it. A run
whose planning read saw one version and whose delivery load saw another
delivers the second, and never presents the first as its evidence.
What the CLI refuses. The declared path is opened under the selected
module's directory with confined, non-blocking, regular-file access, so an
absolute path, a .. element, a symlink leaving the module, a FIFO, a
directory and a missing file all refuse; so does a file that changed while it
was being read. A composed module this machine has not materialized refuses
rather than being fetched behind the read, and a path the CLI materialized
needs a resolution receipt that answers the request the workspace makes now. An
explicit path:, an overlay path or a worktree legitimately has no receipt:
its bytes are local development evidence, not release authenticity. Core then
bounds the bytes at the inline carrier threshold (32 KiB, inclusive), requires
valid UTF-8 with no NUL, and refuses text that would need interpolation.
No value can quietly take a bound destination. A literal, secret, template
or structured document offered for the same destination is a conflict rather
than a silent winner, so moving an existing literal onto a binding is an
explicit source edit: remove the literal and add the binding. At delivery the
same rule holds against additional offers — a configuration the agent returns
from Init, a dependency's configuration at Start, a --set or derived runtime
override — including one that offers the identical bytes. Equality buys no
replacement right, and an empty override map does not skip the check. Removing
a binding restores ordinary ownership on the next load; there is no generated
state to clean up.
An explicit binding is the author's assertion that these bytes are public. Confinement and a digest do not establish publicness, and no scanner or filename rule can: a binding declaring private or mixed-secret content as public exposes both the content and a testable digest. That judgement belongs to whoever reviews the composition.
Such a value is delivered inline to the process. It cannot be exported through
--output-env when its bytes contain a line break, which that raw format
cannot represent (see docs/commands.md).
A few keys on an environment belong to a module agent, not to the CLI. The
CLI admits them, preserves them on load and save, and hands them verbatim to the
module bundle generator in the workspace it stages for it; it never interprets
them, never validates their values (the agent does, and refuses an unsupported
one by name), and never sends them to a service agent: Runtime() projects
none of them.
| Block | Key | Meaning (the module agent's) |
|---|---|---|
| environment | key-service |
Which key service holds this environment's keys, in the key-service configuration group's KEY_SERVICE_BACKEND vocabulary. A module reads it to decide whether it composes a secrets store here at all. |
managed-services.<svc> |
auth-mode |
password (a connection secret through secret-references) or external-identity (the workload authenticates as its own identity). |
managed-services.<svc> |
egress-namespace |
For an in-cluster managed dependency, the namespace the caller's egress policy selects on. |
managed-services.<svc> |
instance-connection-name |
The project:region:instance coordinate a Cloud SQL instance is addressed by, which is not a DNS name. |
environments:
- name: staging
key-service: kms
managed-services:
shop/store:
kind: cloud-sql-postgres
external-name: 127.0.0.1
port: 5432
auth-mode: external-identity
instance-connection-name: project:us-east1:storeThe list is closed (environments.CarriedEnvironmentKeys,
environments.CarriedManagedServiceKeys): every other key in either block is
still refused by name at workspace load, so key-servce fails exactly as a
mistyped CLI field does, and a module-owned key written on the wrong block is
refused too. Admitting a new one is one entry in that list. A key written
with no value is carried as null, which a module agent may read as unstated;
refusing it is the agent's, as for any other value. Because the keys
load, a declaration on one deployed environment no longer fails the whole
workspace — including the local environment a codefly run selects.
A module-owned key only reaches a module that renders its own bundle (a module
whose module.codefly.yaml names a module agent). It does not change the
service set the CLI itself renders for a module composed by version; that is
exclude-services, below.
exclude-services withdraws named services of composed modules from one
environment. The composition decides it; the module's own services: list is
untouched, so every other environment — the local run included — still
composes the service.
environments:
- name: staging
key-service: kms # the module's own word for why (module-owned key)
exclude-services:
- service: saas/vault
withdrawn-from: [saas/accounts]Nothing may still reach what is withdrawn: a service that declares a
dependency on it is refused unless withdrawn-from names it, and so are a
solution consuming one of its endpoints, an environment declaration naming it
and a configuration reference to one of its endpoints. Those refusals are
judged for the environment the operation selected, and the message names that
environment: an operation is not refused because a different environment's
exclude-services, service-scoped names or managed-services are
inconsistent with the workspace graph. A bare service-scoped key the producer
has re-keyed is retired explicitly, by an operator directive on
codefly environment import — never inferred from a matching name or equal
values; see the command reference. A malformed
exclusion is a separate matter — the check validates every environment's
declarations for shape before comparing the selected one against the graph, so
one of those refuses whatever was selected. It is the validation that refuses,
not the load: such a workspace still loads, and a clean environment still
selects. That distinction is load-bearing, because the graph a
declaration is held to is the workspace's current composition — an environment
imported before a module overlay added a service carries no withdrawn-from
for it, and holding every environment to that graph refuses operations on
environments whose own declarations are complete. A service the module's
interface: exports, or names its service-entry, is withdrawn only with the
whole module: an environment that names every service of a module leaves the
module out, and its exports leave with it. Withdrawing some of its services is
refused, because the module the environment still composes would promise an
endpoint that is not there.
environments:
- name: staging # the host is another module's service here
exclude-services:
- service: authority/local
- service: authority/hop
withdrawn-from: [documents/documents]
- service: authority/edge
- service: authority/storeA product whose hosted environment is imported can keep this decision in an
authored delivery.environments.yaml and pass it alongside the producer's
contract:
environments:
blue:
exclude-services:
- service: saas/vault
withdrawn-from: [saas/accounts]
green:
exclude-services:
- service: saas/vault
withdrawn-from: [saas/accounts]codefly environment import blue --coordinate-contract blue.json --product-intent delivery.environments.yamlThis optional input accepts only environments.<name>.exclude-services and,
optionally, environments.<name>.service-secrets with generate and
may-be-empty:
environments:
blue:
exclude-services: []
service-secrets:
generate:
- {scope: service, configuration: clickhouse, keys: [CLICKHOUSE_PASSWORD], services: [tracing/evidence]}
may-be-empty: [CODEFLY__SERVICE_SECRET_CONFIGURATION__SAAS__ACCOUNTS__SENTRY__DSN]Which keys are minted at random and which hold no value are decisions about the
product's own configuration, which no cell producer can know; where the cell's
store files each value is a cell fact. So the import merges the two: secret-store,
defaults and services (remote keys, per-service stores, templates, refresh
intervals) come from the contract, generate and may-be-empty from the intent,
and a contract declaring either of those is refused. Once the intent is passed it
is their only source: re-importing an intent that drops an entry withdraws it
from the workspace. Each generator must name a secret configuration a composed
service (or, for scope: workspace, the workspace) declares in the environment's
profile, with keys that configuration declares, and each may-be-empty key a
stored secret key of a declared configuration; anything else is refused by name.
An intent declaring service-secrets for an environment with no secret store in
the contract or the workspace is refused.
It is not the historical full delivery.intent.yaml launch format: profiles,
host and ingress are not interpreted here. The coordinate producer continues to
own placement, runtime bindings, the secret store and key-service. Unknown fields,
placement overrides, a missing target environment, malformed exclusions, an
invalid withdrawal and exclusions declared by both inputs are refused before
workspace or profile files are written. Only the selected environment is imported.
Use exclude-services: [] to clear an earlier exclusion; a missing or null list
is refused. Omitting --product-intent preserves the existing importer behavior,
including already imported exclusions omitted from the producer's contract.
In that environment an excluded service is not in the dependency graph: it is
not built, published, rendered or started, has no unit in the render
inventory, no tree under deployments/modules/<module>/services/, no workload
in the cell file and no mesh principal; build module and deploy module
skip it there. A tree of the excluded service's own module rendered before the
exclusion is refused by name — by the cell file and by publish — until that
module is rendered again. (Another module's tree rendered before the
exclusion is not re-checked: render the consumers again after excluding a
service they reached.)
Nothing may still consume what is withdrawn, so the render can never produce a graph with a hole in it. Each is refused before anything renders, builds or runs — every environment by the render's workspace validation, the selected environment by a run, build or deploy, and a configuration reference by the plan — naming the service and what holds on to it:
- A declared dependency. A service whose
service-dependenciesname the excluded one is refused unlesswithdrawn-fromnames it. Naming it withdraws that edge with the service: the consumer is handed no address for it, and no mesh edge or cell consumer is derived from it. Every consumer must be named, and every name must be a consumer, so a stale entry is refused too. Withdraw an edge only from a consumer that does not use it in that environment (in the example,accountson the cloud key service opens no Vault connection). - A configuration reference. A workspace configuration a rendered service
resolves — one it declares or one the composition root provides — that
carries
${endpoint:<module>/<service>/…}for an excluded service is refused at plan time, and by the cell file; drop the reference from that configuration in this environment, or keep the service. - A declaration of the environment.
ingress,host.gateway,host.delivery,cell,egress,service-mesh,service-identity,service-config,service-secrets(including a generator'sservices) andmanaged-servicesnaming the service are refused — a service is either rendered, managed or excluded, never two of them. - A module interface export, a service entry, a solution's consumption. A
module whose
interfaceexports one of the excluded service's endpoints, a module naming it itsservice-entry, and a solution whose manifestapi.consumesone of its endpoints are refused: each promises the composition what this environment does not have.
Entries are <module>/<service>, never bare names; an unknown key in an entry
is refused. A codefly run, build or deploy of the excluded service itself in
that environment is refused: there is nothing to run there.
Injecting a workload's configuration and secrets needs four declarations and no
others: the target (name, namespace, cluster.context), resolved values
under service-config, secret references under service-secrets, and a workload
identity under service-identity.
../pkg/environments/testdata/coordinates/config-injection.json is that whole shape.
The fourth is why service-identity exists rather than being read off a managed
service. A descriptor for this flow declares no managed-services, so an
identity carried there would be unreachable: the consumer renders no
ServiceAccount, the pod keeps the namespace default, and the ExternalSecret that
did render cannot authenticate to the store. Nothing fails at import or at
render, and the first signal is in-cluster. Environment.WorkloadIdentity(service)
resolves the per-service entry or the default; composing that with a managed
service's own identity belongs to the consumer, which knows which services
consume what — an Environment does not.
A producer emitting for this path populates nothing else. managed-services,
registry, ingress, resource-quota and dns serve other flows and are
correctly absent here; a producer's own inventory — database engines, store
isolation tiers, cloud and location, operator cluster access — has no home in
this contract by design, and a non-secret value does not become one by being
routed through a secret store to find somewhere to live.
Retired codefly/cell/v1 is rejected, not converted. Its database-to-store translation,
password_auth inference, manufactured secret-store/<namespace>/store handoff
and implicit delivery paths are removed. The producer must emit the resolved
Codefly environment directly, and the CLI import fixtures must move with it.
No compatibility shim reads infra-base's private format. Producers and CLI must
qualify against the CLI-owned coordinate contract before release.
Workload identity projection validates a staged manifest tree and atomically exchanges it with the destination on Linux and macOS. Cancellation before the exchange leaves the destination unchanged; after publication the complete tree is committed. Filesystems without atomic directory exchange fail without a per-file publication fallback. Existing file and directory permissions survive.
The strict parser and deployment declarations live in pkg/environments. Core
owns generic runtime configuration and secrets, not this document. Kubernetes
identity attachments, ingress, quotas and cloud deployment policy belong here.
The spelling codefly/coordinate/v1 and existing producer fields are unchanged.
service-config.values entries are injected under their exact environment names
into containers declaring that service through Core's CODEFLY__SERVICE runtime
variable, directly or through a referenced, namespace-matched ConfigMap. Container
names are agent-owned and are not service selectors. Only the selected overlay's
resource graph supplies these declarations. Explicit service-secrets
keys render both a container secretKeyRef and an ExternalSecret reference.
Neither secrets nor configuration are implicitly copied to sidecars. Rendering
refuses a declaration that cannot bind to a service container, and a literal
configuration value cannot replace a rendered secret reference.
A per-service secret mapping may specify refresh-interval and a template
with engine-version: v2, merge-policy: Merge and data expressions for its
explicit remote keys. These are External Secrets declarations, evaluated only by
ESO. The CLI never resolves or evaluates secret expressions. Other template
engines, replacement policies and undeclared output keys fail admission.
A producer may also declare one of its secret values as a template over its own
secret configuration values (core's ConfigurationValue.template). The CLI
translates it into the consumer's ExternalSecret target.template over the
producer's primitives, read from the producer's remote keys through whichever
surface the producer itself resolves through, and never reads the assembled
value from the store. The translation reproduces core's
EvaluateConfigurationValueTemplate byte for byte. A template of literals only
is refused: under a credential-named key it would be a value in the tree. So is
a template referencing a key the producer's own deployment does not read as a
secret, and a producer whose keys resolve through a different store than the
consumer. The template is emitted with mergePolicy: Replace and an entry for
every key the consumer references, so the primitives it reads are fetched but
never emitted into the consumer's Secret.
Rendered manifests admit template delimiters, and credential-named keys, only at
an ExternalSecret's spec.target.template.data.<key>, and only for a value that
carries a template action.
What is credential-named is core's decision, narrowed in exactly one place: a
public OAuth client identifier. A client id identifies the client to an
authorization server and is published in redirects, in discovery documents and
in browser requests; the matching secret is the client secret. Core already
calls a bare CLIENT_ID public; what it still classified was the spelling an
operator writes — OAUTH_CLIENT_ID, AUTH_CLIENT_ID — and only through the
AUTH word in front of it, so the render refused a public value and the only
way to deliver it was a secretKeyRef into a store entry somebody had to fill.
A key is read as a public client identifier only when all three hold: it ends in
the client-identifier spelling (client_id, client-id, clientId), core
classifies it today (this narrows core's reading and never widens it), and
removing the AUTH marker makes core stop classifying it — so the AUTH word
is the only credential signal in the name. CLIENT_SECRET, OAUTH_ACCESS_TOKEN,
AUTH_PASSWORD, OAUTH_CLIENT_KEY, CLIENT_ID_SECRET and
OAUTH_TOKEN_CLIENT_ID all keep their refusal, and a secret configuration
carrier (CODEFLY__WORKSPACE_SECRET_CONFIGURATION__…) is sensitive whatever its
key: the value was authored as a secret, and that declaration outranks any
reading of its name. Both sides are pinned by
TestTheRenderGuardAcceptsAPublicClientIdAndRefusesACredential and
TestRenderAcceptsAPlainClientIdAndRefusesAPlainClientSecret (pkg/gitops).
This narrows the render's own tree guard. Promotion to a secretKeyRef
(pkg/orchestration) is unchanged and still routes such a key to the secret
store, because core's restricted-deployment guard
(validateRestrictedDeploymentRequest) refuses the inline value by the same
classification; narrowing that half needs the change in core.
Validation rebuilds the selected overlay and checks the complete CLI-owned ExternalSecret delivery specification, including its store, target, keys and properties. Overlay patches may not redirect or replace that specification. The target Secret and every consuming workload must share a namespace. Workload identity is checked at pod scope against the ServiceAccount's namespace and name, independently of container configuration bindings.
The actual infra-base fixture and its immutable source provenance are under
pkg/environments/testdata/coordinates. The Lodestar output selects Accounts'
writer identity, public identity keys its source reads, exact remote secret
references, the existing registration-digest expression, cluster and GitOps
target. The integration test imports this emitted JSON and verifies effective
Kustomize output without managed services. Database convention values remain
informational for consumers that have not adopted those keys. Azure runtime
database bindings are not inferred from the GCP declaration.
Core transports structured runtime data through its scoped, versioned JSON
carrier, and SDK-Go exposes raw and typed document accessors. This is separate
from service-config.values, whose explicitly declared environment values
remain strings. Render qualification does not establish live cloud identity,
secret-store access or database connectivity.