Skip to content

fix(orchestration): name the agent behind a "requires host resolution" refusal - #1291

Merged
antoinetoussaint-byte merged 2 commits into
mainfrom
fix/imported-workspace-interface-bindings
Nov 4, 2026
Merged

antoinetoussaint-byte merged 2 commits into
mainfrom
fix/imported-workspace-interface-bindings

Conversation

@antoinetoussaint-byte

@antoinetoussaint-byte antoinetoussaint-byte commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Problem

codefly deploy gitops render <module> --env blue on obin-ai/platform-obin, which imports platform-core as workspaces: - source: obin-ai/platform-core, version: <sha>, refuses with:

cannot bind interface dependencies: resources.ApplyInterfaceBindings: cannot load the workspace that binds the service's interfaces: LoadFromDir: workspace "platform-core" at obin-ai/platform-core@5a8d5ffa… requires host resolution

so every Blue render has needed an uncommitted path: override pointing at ~/.codefly/modules/obin-ai/platform-core/<sha> — a directory the CLI itself had already fetched.

Diagnosis (verified in source)

The host is not what is missing:

  • The CLI registers composition.ResolveWorkspaceReference on every command context (cmd/root.go, cmd/common/context.go) and it resolves a source:+version: import through the same module cache a module pin uses (ensurePinnedArtifact). The composition loads; workspace.LoadModuleFromName → module.LoadServiceFromName binds the service's interface dependencies.
  • Core's services.Load already hands the agent that binding: ServiceIdentity.interface_bindings (field 7, core 4ab16f55c, 2026-10-25, in every core tag ≥ v0.19.7 (v0.19.6 predates it)), filled from Service.InterfaceBindingsProto() in both the builder and the runtime LoadRequest. The CLI has pinned a core carrying it since v0.1.213 (core pseudo-version 20261025185759-e501458995d0).
  • The sentence is produced in the agent process (core agents/services/base.go, cannot bind interface dependencies). An agent built on a core older than v0.19.7 ignores field 7, calls ApplyInterfaceBindings against workspace_path itself, and cannot compose the import because it registers no resolver. The services that declare interface: dependencies in the obin composition (wiki/backend, wiki/publisher, proposals/backend, robin-builder/backend) run on the go agent at 0.0.67; its latest release v0.0.74 is on core v0.19.5. Only go-grpc (v0.1.72) is on a core that reads the binding.

So the real fix is an agent release built on core ≥ v0.19.7 and a pin bump in the composition; a CLI change cannot make an old agent read a field it does not know, and the only lever on it (materialising a rewritten workspace file for the agent) is the hand path: override automated — refused.

Change (classification: fix, at the place in this repo that owns the behaviour)

The CLI owns what it tells the operator about a peer it loaded. When an agent's Load refusal carries core's requires host resolution and this host did supply the binding (instance.Service.InterfaceBindingsProto() != nil), the error now names the agent (the running selection, else the declared one), what it did instead of reading the binding, the release floor (core v0.19.7 or newer), and that a path: override hides this without fixing it. The agent's own error stays wrapped underneath. A service nothing bound — loaded outside its composition — keeps the agent's refusal as it stands, since binding against the workspace on disk is then the contract. Applied at the three places the orchestration loads an agent's builder or runtime (builder.go, runner.go, local_runtime_image.go), through one helper: pkg/orchestration/interface_binding_refusal.go.

Docs: a paragraph under What an agent must report in docs/agent-compatibility.md records the host/agent binding boundary and the floor.

Reuse

Reused core's field 7 and Service.InterfaceBindingsProto / services.Load (nothing added on the host side — it already supplies the binding), the existing resolver registration, and modulecache.Env for the test's cache root. Deliberately not built: a second binding path, a capability probe (core's agent contract has no capability for the supplied binding, so the refusal can only be recognised after Load), or any rewrite of the workspace the agent reads.

Tests

  • pkg/orchestration/interface_binding_refusal_test.go — a bound service + the Blue error yields the explained error (names codefly.dev/go:0.0.67, then the running 0.0.74 when a selection resolved; errors.Is the original); an unbound service, an unrelated error, nil error, nil instance are returned untouched. Fails before (helper absent), passes after.
  • pkg/composition/workspace_reference_test.go — a product workspace importing obin-ai/platform-core by source + commit pin, with the module cache (CODEFLY_MODULE_CACHE) populated at that pin and no path: override, loads through WithWorkspaceResolver, and the service it composes carries the binding to platform/api grpc; the same load without the resolver is core's requires host resolution. This one passes on main too: it locks the host-side contract the diagnosis rests on, so a regression there would read as this bug again.
  • go build ./..., go vet, golangci-lint run (0 issues) and the full pkg/orchestration + pkg/composition suites pass.

Not verified

  • No real Blue render was run from this branch (the Blue render runs on another binary; this PR changes only what the refusal says, not whether the old agent loads).
  • No agent was rebuilt. The outcome on Blue is unchanged until the go agent (and any other agent serving a service with interface: dependencies) is released on core ≥ v0.19.7 and the composition's pin moves.

Would file (not opened, per the brief)

  • codefly-dev/service-go: repin core ≥ v0.19.7 and release, so Load applies the host-supplied binding; same sweep for python-fastapi, nextjs, generic, postgres, redis, clickhouse, vault, object-storage, warehouse, solution-go, runnable-go/python (all on core < v0.19.7 at their latest tags, though only agents serving a service with interface: dependencies hit this).
  • codefly-dev/core: advertise the supplied-binding contract as an agent capability (e.g. service-interface-bindings/v1) so the host can refuse before Load, per "compatibility is runtime-only"; and let ServiceIdentity.Proto() take the bindings so no host path builds the proto without them.
  • obin-ai/platform-obin: drop the uncommitted path: override once the agent pin moves.

🤖 Generated with Claude Code

antoinetoussaint-byte and others added 2 commits November 3, 2026 23:35
…" refusal

`codefly deploy gitops render … --env blue` on a workspace that imports
another by `source:` + `version:` fails with

  cannot bind interface dependencies: resources.ApplyInterfaceBindings:
  cannot load the workspace that binds the service's interfaces:
  LoadFromDir: workspace "platform-core" at obin-ai/platform-core@5a8d5ffa…
  requires host resolution

and every operator has answered it with an uncommitted `path:` override on
the import. The host is not what is missing. The CLI registers a workspace
resolver on every command context and resolves the import through the
module cache; the composition loads and binds the service; core's
services.Load hands the agent that binding (ServiceIdentity.interface_bindings,
core v0.19.8). The sentence comes from the AGENT process: an agent built on a
core older than v0.19.8 ignores the field, loads the workspace at
workspace_path itself, and cannot compose the import because it registers no
resolver. The `go` agent every solution runs on is at core v0.19.5.

The CLI now says so. When an agent's Load refusal carries core's sentence and
this host did supply the binding, the error names the agent (the running
selection, else the declared one), what it did instead of reading the
binding, the release floor that reads it, and that a `path:` override hides
this without fixing it; the agent's own error stays wrapped underneath. A
service nothing bound — loaded outside its composition — keeps the agent's
refusal as it stands, because binding against the workspace on disk is then
the contract. Applied at the three places the orchestration loads an agent's
builder or runtime.

Tests lock both halves: the explained error for a bound service and the
untouched error for an unbound one or any other failure; and, in
pkg/composition, a workspace importing another by source and commit pin with
the module cache populated loads through the resolver with no `path:`
override, and the service it composes carries the binding the host hands its
agent, while the same load without the resolver is core's refusal.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…carrying it

4ab16f55c (2026-10-25) is in v0.19.7 (2026-10-27); v0.19.6 predates it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit 2e986b8 Nov 4, 2026
19 of 20 checks passed
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