codefly.services.agent.v0.AgentInformation.contract is the CLI-agent wire
contract. Its protocol generation is independent of Core's Go module version,
the agent release, and the v0 protobuf package namespace. Implementations can
compile the schema under proto/codefly/services/agent/v0 in any language;
sharing Core's Go implementation is not a compatibility requirement.
Core and the CLI know their own supported protocols, not which named agent releases are compatible. Compatibility is a runtime verdict on the actual process's authenticated advertisement. There must be no agent-name exception, minimum agent-release table, linked-Core comparison, or compiled fleet roster in that decision. A binary built independently, in another language, or against an older Core is accepted when it implements the required protocol. A newer binary with an unsupported or missing declaration is rejected before work.
Artifact selection and compatibility are different. A user may select an immutable artifact for reproducibility; that selection is never proof of compatibility and is never rewritten merely because the host upgraded. Do not replace a rejected selection with another release behind the user's back. Automatic selection must use agent-owned declarations, not a host-maintained list of named agents and approved versions. Ambiguity requires an explicit selection, not a guessed default.
Changing Core or the CLI without changing the wire contract must not require rebuilding, republishing, or repinning agents. Do not bump the protocol just because a library release was cut. Real incompatible protocol changes require explicit adoption, checked at runtime; optional capabilities are required only by operations that use them. Generic runtime checks remain mandatory even for artifacts that passed release qualification.
Review every compatibility change with an unknown agent identity, independently versioned builds, missing and unsupported protocol declarations, absent required capabilities, and unknown extra capabilities. Tests must show that identity and release metadata cannot change the verdict. Framework build paths, native product cleanup and source-to-agent mappings belong to their owning plugins, not to compatibility enforcement in Core or the CLI.
Shared language/framework tooling can live in Core when consumers explicitly invoke it. That does not authorize host-side agent selection, compatibility rules, framework-specific runtime paths or product-specific recovery policy.
startup_protocol_version names the stdout handshake used to discover the gRPC
endpoint. It must match the host before lifecycle compatibility can be checked.
The manifest records both versions; the startup parser and contract checker
share one startup-version constant, tested against the release manifest.
Protocol 1 uses the existing authenticated agent discovery and
Builder/Runtime lifecycle RPCs. The host calls GetAgentInformation before
dispatching lifecycle operations. It accepts only its supported protocol
generation, then checks that the agent advertises every capability required by
the selected operation. Extra capabilities are ignored. A missing declaration
or version zero is undeclared, never evidence inferred from a Core version or
an acknowledgement header. Existing operation-specific advertisements such as
validation and build-cache support remain authoritative for those operations.
Core's services.LoadAgent rejects undeclared or incompatible protocol versions
before caching a connection, including for callers that bypass services.Load.
Connection and instance caches retain the admitted selection. A request for a
different agent or version under an occupied service key fails without stopping
the active process; its owner must call services.ClearAgent before replacing
it. Mutating the caller's resource does not relabel an already-running agent.
The direct SDK launcher performs the same live protocol check and requires its
Builder and Runtime capabilities before creating a service. Agent updates inspect
the candidate before changing a saved selection; a rejected candidate or failed
configuration write leaves the previous in-memory selection intact.
Callers use Instance.RequireAgentCapabilities
before dispatching operations that require optional features. Core does not
choose a CLI run's requirements. The CLI adoption, including replacing its
existing recovery check and publishing CLI compatibility notes, is tracked in
codefly-dev/cli#747. Until that
lands, this is a Core contract implementation, not a completed CLI rollout.
Core writes and reads typed service settings through YAML semantics, including inline fields and custom marshalers. Updates merge top-level keys into the existing spec: explicit values replace old values, while omitted and extension keys remain. A conversion failure leaves the spec unchanged. This does not repair already-malformed saved declarations; their owner must correct them.
container-recovery-scope/v1 promises validation of the inherited process
marker, discovery's codefly-container-recovery-scope response header, and
ownership labels on containers created through Core's Docker runner. See
runners/recoveryscope and runners/dockerrun for the marker and label formats.
An implementation must provide all three behaviors before advertising it.
agents.Serve supplies its implementation's contract only when a handler
leaves the contract absent. An explicit declaration is authoritative, including
its protocol version and omitted capabilities. Plugins extending the shared
contract can start from contract.Current() and add their own capabilities.
The shared implementation advertises support even when this particular process
received no valid scope. Handwritten servers must declare their own contract
and implement the behavior; importing the generated types alone promises
nothing.
Before an operation that creates recoverable containers, the host calls
Instance.RequireContainerRecoveryScope(expected), where expected is the
identity captured by that flow when it projected the scope. This first checks
capability support, then requires an exact acknowledgement of this run's
identity. Missing capability and wrong/missing acknowledgement are distinct
errors. Native operations that do not need recovery should not require this
capability. Advertising support never bypasses the ownership check.
InitRequest.runtime_image (field 10) carries one container image reference,
supplied by the CLI after it builds the agent's recipe for a local run needing
a container runtime. The reference is a name with an explicit non-latest tag
or a sha256 digest. It is invocation data, not image bytes, a recipe, registry
credentials or a deployed artifact selection. A scalar string is sufficient;
omitted and empty both mean no supplied image. The agent emits the recipe and
the CLI executes buildx; Init does not acquire a build responsibility.
An adopting agent may use the supplied image when the local manifest names
none. A nonempty manifest image retains precedence and must validate; a valid
request must not hide an invalid manifest. Image validation remains the agent's
responsibility: refuse malformed references, implicit latest (a name with
neither tag nor digest), explicit latest even beside a digest, and any
non-sha256 digest. If neither source supplies an image, the existing local
missing-image refusal remains. Core transports the value without parsing or selecting
images. Every deployed path leaves this field empty and retains the agent's
existing image resolution; absence is not permission to guess or build an image.
runtime-init-image/v1 names this optional behavior. The CLI requires it on
the live AgentContract through the existing capability check when relying on
the handoff. An agent advertises it only after implementing selection and
refusals; linking generated bindings or Core's shared server does not advertise
adoption. The release manifest records the operation contract while wire
protocol 1, startup protocol 2 and shared server capabilities remain unchanged.
The proposal was opened first in handbook#260. This is the Core transport for core#748, not a completed local run: the build-to-Init producer is cli#932, and consumer selection and refusal tests belong to service-libreoffice#63. Those implementations and a combined boot must qualify the downstream rollout.
DeploymentRequest.composition_provenance carries every member of the CLI's
one loaded, resolved composition, including transitive imports under their
composed names. Each member carries its declared role (module or
solution) and the name of its owning workspace, preserved through imports.
The CLI fills this from the same composition its own dependency judge uses.
The shared builder converts supplied membership to resources.Provenance and
passes it to ResolveDependencyNetworkMappings, which already takes that
interface. It uses the existing edge and endpoint verdicts. Request provenance
wins whenever present: the agent neither loads nor merges a repository's dev
workspace into it. An empty supplied composition still has presence; missing
edge members, duplicate names, empty names or owners, and unspecified or unknown
roles refuse as ErrUnjudgedProvenance, never trigger another source.
Only an absent field falls back to FindWorkspaceUpFrom at the service's
physical location. That older-CLI behavior is correct only for a checkout
inside the rendering composition. It does not fix pinned module caches,
composed renames or imports for an older CLI. No discovered workspace still
means unjudged dependency addresses.
This is stricter as well as correct: a solution's direct route to a module
endpoint returns ErrSolutionReachesThroughHost, even when its repo's dev
workspace treats it as a module and would admit the edge. The request preserves
the role needed for that refusal and the owning workspaces in its diagnostic.
deployment-composition-provenance/v1 names this optional operation contract.
The CLI requires the live AgentContract capability when relying on the
handoff. Agents advertise it only when their deployment path implements these
semantics; the shared server does not automatically advertise adoption.
Wire protocol 1 and startup protocol 2 are unchanged. No version roster or
fleet repin substitutes for live adoption.
Handbook proposal #266 preceded the proto edit for core#751. The CLI producer is sequenced after Core in cli#937. Core tests do not qualify that handoff, downstream agent adoption, or a staging render.
StartStatus.started_artifact (field 5) names what the agent actually ran:
executable, the absolute path of the program it started, and sources, the
absolute paths of the files that program was handed. A compiled runtime starts
the service's own build, so the executable alone identifies it and sources is
empty. An interpreted runtime starts an interpreter or a launcher, which
identifies nothing about the service: there the entrypoint, the modules it
loads and the lock file pinning its dependencies are the build, and the agent
names them. Every entry is a regular file — a consumer never decides for the
agent what a directory's closure contains.
A runtime reports it with RuntimeWrapper.ReportStartedArtifact before
StartResponse, which stamps it on the StartStatus that Start and every later
Information carry — and consumes it, the way it clears the health report
beside it. A report describes one process, so it applies to one start: an
agent that reports at its first start and not at its restart reports nothing
for the restart, rather than having the previous process's answer approved for
the one now running. Consumers read it through services.StartedArtifact,
which separates "the agent reported one" from "the agent reports none" — an
artifact naming no executable is the latter.
This exists because a consumer that pins the build a running process is at
otherwise has to guess one: the CLI's native run derived every workload's
build from the Go agents' .cache/native/main, so a service on any other
agent was never delivered (cli#989).
The answer is the agent's to give. A consumer that needs an artifact and is
reported none MUST refuse, naming the agent; there is no path it may derive
instead.
A Go runtime does not reconstruct that path. GoRunnerEnvironment.HostExecutable
returns the executable of the last successful BuildBinary as the host sees it,
which is the file Runner() runs, and reports false where there is no such
file to name: before a build succeeds — the state Runner() itself refuses from
— and for a Docker companion, whose executable exists only inside that
container. It is absolute, because BuildBinary records the target absolute
when it writes it, resolved against the directory the build process ran in; a
relative runner directory or cache location would otherwise leave a path that
this contract forbids and that exec.LookPath resolves somewhere else again.
The runner owns that answer because it owns the name and the backend's layout;
an agent deriving them again drifts into reporting a file that is not there,
which a consumer digests rather than refuses.
runtime-started-artifact/v1 names this optional operation contract. It is
host support here: a runtime advertises it in its own capabilities only once
it calls ReportStartedArtifact, and the shared contract never advertises it
for every agent built on this Core — that would be the false promise a
consumer then trusts. Wire protocol 1 and startup protocol 2 are unchanged.
Core tests do not qualify any agent's adoption.
A Go runtime built on runners/golang names its executable with
GoRunnerEnvironment.Executable(): the absolute path of the last successful
BuildBinary, the one Runner starts, empty with an error before a build and
again after a failed one. It reports StartedArtifact{Executable: path} with
no sources, since the executable is the build; it never derives the path from
the cache directory's layout.
AgentInformation.supported_backends is the plugin's own answer to "how can I
run on this host": LOCAL, NIX and DOCKER, in the plugin's preference
order, already filtered to what the machine has (runners/base.BackendSupport).
services.PlanRuntimeContexts reconciles a run's requested runtime context
against it, for every service, before anything boots.
A requested context is a preference, not an assertion every service of a graph
can honour. A plugin that is its runtime image — a LibreOffice engine, not a
compiled binary the host can run — declares DOCKER alone, and under
--runtime-context native it runs as a container. The alternative, which this replaces, was the plugin refusing at
its own Init with an error about an image field the user never set, and taking
the whole run down with it (core#820).
Not every substitution costs the same, so the decision says which one happened.
Moving into a container is safe — the image brings its own engine. Giving one
up is not: a stateful service then pairs on-disk state with whatever engine
version the host-native backend brings, and a data directory written by a
postgres 16 image, read by a nix postgres 17, fails later and opaquely.
RuntimeContextDecision.LeavesContainerImage marks exactly that hop, so a run
reports it apart from the substitutions that cost nothing but isolation. It
marks what is visible here and no more: a plugin whose supported_backends
omits DOCKER because the docker CLI is absent is indistinguishable from one
that ships no image, so a free run on such a host is not flagged.
RuntimeContextDecision.Cause separates whose fact caused a substitution. The
plugin's declaration is one input; the host's own state — a Docker engine that
is installed but not running — is another, and it arrives as
RuntimeContextRequest.Unavailable, never subtracted from Backends. Core
reconciles a declaration and probes nothing, so a host fact that is folded into
the declaration becomes indistinguishable from it, and every diagnostic
downstream then tells the user to fix the plugin when the remedy was to start a
daemon. SubstitutionUnavailable and SubstitutionUndeclared are that
distinction.
A non-empty declaration is read as complete whether or not the plugin advertises
anything: a list naming only DOCKER is already evidence that there is no
native path. An empty one is the ambiguous case — an older plugin that never
filled the field and a plugin that can run nowhere on this host are the same
empty repeated field on the wire. runtime-backend-declaration/v1 names which:
a plugin advertising it owns the completeness of supported_backends, so an
empty declaration is the plugin's own verdict that nothing here can run it.
Without it, an empty declaration is unjudged and the request passes through,
which is what it did before plugins declared backends at all. A plugin whose
BackendSupport is only partly declared must not advertise it;
services.Advertisement.BackendsComplete is the agent-side opt-in, and the
shared contract never advertises it for every plugin built on this Core.
Every service left with no usable backend is refused together, in one
services.NoRuntimeBackendError, before anything boots. It carries the refusals
as data rather than only as prose, because the remedy belongs to whoever owns
the fact that caused each one: a refusal whose Unavailable is empty is the
plugin's own verdict and nothing a user can act on, while one naming backends is
the host's report, and the host reaches these through errors.As to print what
it alone knows — which endpoint was unreachable, and how to start it — beside
them. errors.Is against services.ErrNoRuntimeBackend is the cheap check for
a caller that needs none of that.
This is the Core half: the host contract and its reconciliation. The CLI's run
resolves only the free hint today and honours an explicit context untouched,
so the substitution and the up-front refusal reach a user only once
pkg/orchestration plans through this. Core tests do not qualify that adoption
or any plugin's.
- Incompatible lifecycle semantics increment
protocol_version. Both peers must explicitly adopt that generation; there is no implicit downgrade. - Additive optional behavior gets a stable, versioned capability identifier. Changing its meaning requires a new identifier. Require it only for the operation that needs it.
- Core changes outside this contract leave its generation and capabilities unchanged. An unchanged contract requires no agent rebuild solely because Core changed.
agents/contract/contract.json is the JSON release manifest. Its
protocolVersion, startupProtocolVersion and capabilities fields supply
the shared server's AgentContract advertisement. operationContracts records
host-supported optional operation contracts, not capabilities automatically
advertised by executors. artifact-execution/v1 must be truthfully advertised
on the live Builder BuildCapabilities or Solution GetSolutionInformation
probe before selection-bound operations. A successful response must acknowledge
the exact request identity and every named output digest and relative file path.
Embedding a newer Core server does not implement or advertise this behavior.
codefly.dev/docker-build-recipe/v4 records support for explicit recipe context
roots. Only plans selecting an explicit root use v4; default service-root plans
retain v3 and its digest. The plan version, not the agent identity or Core pin,
prevents an older host from silently building the wrong tree. See
build recipe context selection.
codefly.dev/docker-build-recipe/v5 records support for declared Go module
downloads (DockerBuildRecipe.go_module_downloads). Only plans declaring one
use v5. A v5 host fetches each declared module graph before any image build,
supplies it as the named proxy build context, and refuses a declaration it did
not fetch.
codefly.dev/docker-build-recipe/v6 records support for declared git source
downloads (DockerBuildRecipe.git_source_downloads). Only plans declaring one
use v6, and v6 also covers v5's declarations. A v6 host fetches each declared
commit into a bare mirror at services.GitSourceMirrorPath(repository), under
services.GitSourceMirrorRef(commit), supplies the mirrors as the named build
context, and refuses a declaration it did not fetch.
runtime-backend-declaration/v1 is explicitly advertised by plugins that own
the completeness of their supported_backends, as above.
runtime-init-dependency-mappings/v1 is explicitly advertised by runtimes that
consume InitRequest.dependencies_network_mappings before executing a test
whose target is never started. These are the dependencies' accepted addresses,
not a fresh deterministic allocation. Hosts send them after dependency Init;
runtimes must reject a dependency-backed test with missing addresses rather than
reconstructing another invocation's endpoints. Ordinary Start still carries
dependency mappings. Merely linking this schema does not advertise adoption.
See composition selections.
The version-tag workflow attaches it to the GitHub release and compares it
with the latest reachable stable release tag. Release notes explicitly report
introduction, unchanged compatibility, protocol changes, or capability changes.
Startup changes also require compatible agents and are reported separately.
Publication resumes an existing draft after interruption and verifies the
uploaded manifest before publishing. Retrying a published release verifies its
manifest without replacing it; a missing or different artifact is an error.
Keep the manifest, schema, implementation, and this document together when
changing the contract. CLI releases must additionally state their required
capabilities; a Core server advertisement cannot determine CLI run policy.
The first rollout requires agents to declare protocol 1. Previously published agents without a declaration are rejected during discovery, including native agents. They need a one-time adoption, not ongoing rebuilds for every Core release. Source adoption and official artifact publication are separate from qualification of the published agent in a consuming workflow.
For release qualification, use codefly agent install to populate an isolated
CODEFLY_HOME with the official selections in the rollout inventory, then run:
GOWORK=off CODEFLY_HOME=/path/to/qualification-cache go test ./services \
-tags=published_agents_required -run TestPublishedAgentsDeclareRuntimeContract -count=1 -vThis checks every selected executable in that cache through authenticated live admission. Record download failures separately: an absent artifact is not a passing qualification. The inventory is release evidence, never a runtime roster.
Publisher, name and version are individual path components. Each starts with an
ASCII letter or digit and contains only letters, digits, ., _, + or -.
This is a syntax constraint, not an identity or compatibility roster. Explicit
release labels and semantic versions with prerelease/build metadata are valid;
empty versions and path/URL delimiters are rejected. Parsing, protobuf conversion,
cache path construction and GitHub release lookup enforce the same rule,
including for directly constructed resource values.
Names and versions cannot contain __, the cache filename separator. This
rejects both interpretations of previously colliding cache entries rather than
trusting their contents. Single underscores remain valid in names and release
labels. Existing unambiguous cache paths are unchanged.
GitHub agent downloads use GITHUB_TOKEN, then GH_TOKEN, when supplied.
manager.GitHubReleaseToken exposes this selection to consumers verifying
installability; a gh CLI login is not a loader credential. Both anonymous and
authenticated acquisition resolve the exact published tag and asset through the
GitHub API. The bounded download client follows asset redirects without API
credentials. Draft releases, duplicate assets and an asset URL that disagrees
with the loader selection are refused. An unauthenticated 404 can mean a missing
release/platform asset or a private release; supply a token with repository
access for the latter. Services do not need that download credential.
Before extraction, downloads compare the archive's SHA-256 with its producer's
published *checksums.txt entry, or GitHub's asset digest when no checksum file
exists. Missing entries, malformed or duplicate checksum entries, lookup/read
failures and digest mismatches are errors. A release confirmed to offer neither
digest remains installable with an explicit unverified warning for historical
releases. Checksums provide an integrity comparison, not signed provenance.
manager.FetchReleaseAsset exposes the same archive and expected digest to
consumers; OpenReleaseAsset retains its reader/size API. Existing local and OCI
cache selection and live admission rules are unchanged.
Agent downloads finish extraction before publishing a binary. Publication copies into a temporary file beside the destination, sets permissions, flushes and closes the file, then renames it atomically. Concurrent readers retain the old complete binary or open the new complete binary. An interrupted transfer or failed staging copy does not truncate an active installation. Installation does not establish protocol compatibility: authenticated live admission remains required before lifecycle operations.
Running service connections bind both the explicit selection and the SHA-256 digest of the executable admitted at startup. Reusing Agent, Builder, Runtime, Code, or Instance clients rechecks current executable content, including local symlink targets. Installing different bytes at the same version returns an error without killing the running agent. The flow owner must explicitly clear that service before admitting the replacement. Identical bytes remain reusable after atomic reinstallation. Startup also rejects a concurrently replaced executable before publishing the connection. These checks do not establish publisher authority or qualify a rebuilt runtime output.
base-image-bindings/v1 is an optional builder operation: the running agent
advertises its loaded service's image roles and recipe variants, validates
selected immutable bindings and renders them into its recipe. See
environment image bindings. Callers selecting this
policy require the live advertisement; unconfigured builds remain unchanged.