Skip to content

fix: a service whose faces are gRPC and Connect alone loads, syncs and starts - #113

Merged
antoinetoussaint-byte merged 1 commit into
mainfrom
fix/connect-only-service
Oct 26, 2026
Merged

antoinetoussaint-byte merged 1 commit into
mainfrom
fix/connect-only-service

Conversation

@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor

Goal: under the accepted one-Connect-face decision (handbook decisions/a-module-serves-one-connect-face.md), a module declares its gRPC endpoint and the Connect face over it, and no REST surface. This agent refused every such service. Its builder and runtime both required a REST endpoint to map uvicorn's port from, so codefly generate client / contracts failed with builder load failed: no rest endpoint found.

Change

  • HTTP endpoint is optional. The app's HTTP listener serves the service's REST endpoint, or a plain api: http endpoint (probes, no API document), or none. Two are refused by name, because the process has one listener (findHTTPEndpoint, used by builder and runtime Load). Service.RestEndpoint becomes HTTPEndpoint.
  • No HTTP endpoint. The app still runs under uvicorn, because its lifespan starts the gRPC server and the Connect face. uvicorn binds a loopback port the system picks, and no endpoint publishes it (uvicornArgs with port 0).
  • Readiness. Start waits on the HTTP endpoint as before. Without one, it waits on the Connect face (an HTTP answer). Without that, it waits on a gRPC HTTP/2 handshake (grpcAnswers), which an engine's port proxy cannot fake. Before this change, an empty serve address skipped the wait entirely.
  • Deploy probes the HTTP listener (container port 8080). For a service with none, it now names the missing endpoint. Before, it reported "requires explicit health.startup/liveness".
  • Deleted (dead):
    • Create's swallow of a missing OpenAPI document. The factory templates write openapi/api.swagger.json before CreateEndpoints, so the branch never ran; isFileNotExistErr went with it.
    • The watcher's api.json check. The watcher selects only code/src/*.py.

Classification: fix, in the agent that owns endpoint resolution and the process lifecycle.

Reused, not rebuilt:

  • core's FindEndpointsByAPI, LoadEndpoints and network mappings.
  • The existing awaitServing loop, now choosing its target.
  • grpc-go, already a direct dependency.
  • The existing scaffold and test helpers.

Relation to #112: this PR is independent of #112, which makes Init skip the OpenAPI step when a service ships no src/openapi.py. This PR does not touch that step. The two merge cleanly: I trial-merged #112's head 1d04001 into this branch, and vet plus the Init and connect-only tests pass on the result. A Connect-only service that also deleted src/openapi.py (the model gateway) needs both.

Tests (connect_only_test.go)

  • TestFindHTTPEndpoint: covers none, REST, plain HTTP, and two endpoints (refused).
  • TestConnectOnlyServiceLoads: a scaffolded service rewritten to grpc + connect with no REST endpoint. Builder Load is READY with no HTTP endpoint, runtime Load is READY, and Deploy names the missing listener. It fails on main with no rest endpoint found; I checked that by putting the old behaviour back.
  • TestConnectOnlyServiceSyncs: builder Sync writes the gRPC bindings (Buf, in Docker) and the Connect face.
  • TestConnectOnlyStartPublishesNoHTTPListener: Start binds uvicorn to 127.0.0.1:0 and waits on the Connect face.
  • TestConnectOnlyServiceStarts: real uv. Load, Init and Start on the scaffold reach STARTED, and a Connect call answers.
  • TestServeTargetWithoutHTTPEndpoint, TestGRPCAnswersNeedsAHandshake: a real gRPC server answers; an accept-and-hang-up listener does not.

go build, go vet, go mod tidy -diff, go test ./... (Docker and uv tests included) and go test -race -short ./... pass locally.

Checked against the consumers, with this agent built locally (CLI at cli 672416bb, the evaluation generator pin)

  • evaluation scorer (module-evaluation fix(deploy): a restricted render probes natively; refuse what a native probe cannot express #91, ba490fb):
    • With its rest endpoint deleted (gRPC only): main fails generate client with no rest endpoint found. With this build:
      • generate runnables evaluation --check passes.
      • generate-client-library.sh regenerates with no diff.
      • codefly run service evaluation/scorer --stand-alone (native, judge/authority configuration excluded) runs no OpenAPI step. It starts uvicorn on a loopback port and reports serving its gRPC server after 2.6 s.
    • As committed (with rest): the same checks pass.
  • model gateway (module-model-gateway #210, 89178c5), with its rest endpoint deleted (Connect + gRPC only):
    • main fails generate contracts model --check and generate-clients.sh with no rest endpoint found. With this build, generate contracts --check, generate runnables --check and generate-clients.sh all pass with no diff.
    • Start, with this branch merged with fix(runtime): a service without src/openapi.py starts without one #112: Load and Init pass, and uvicorn launches the app. The app then refuses for its own missing composition values (hop/PROOF, model-installation/*).

Not verified

  • A full start of the model gateway. Its playground on #210 references authority/gateway, but pins an authority commit (8a2fe5f0) that has no such service. A commit that has it serves codefly.dev/authority@0.1.0, and the gateway requires @0.2.0. That composition needs fixing on #210.
  • Deploying a service with no HTTP endpoint. Deploy refuses by name: the container recipe and manifests still bind and publish 8080, and probes from gRPC health are not rendered. The modules can keep their probes by declaring the probe listener api: http instead of api: rest; Deploy supports that unchanged. I did not run a cell deploy.

🤖 Generated with Claude Code

…d starts

The builder and the runtime refused every service without a REST endpoint
("builder load failed: no rest endpoint found"): both required one to map
uvicorn's port from. Under the one-Connect-face decision a module declares
its gRPC endpoint and the Connect face over it, and no REST surface.

- The app's HTTP listener serves the service's REST endpoint, or a plain
  `api: http` one (probes, no API document), or none. Two are refused by
  name: the process has one HTTP listener.
- With none, the app still runs under uvicorn, whose lifespan starts the gRPC
  server and Connect face, on a loopback port the system picks and no
  endpoint publishes. Start waits on the Connect face, else on a gRPC
  HTTP/2 handshake, which an engine's proxy cannot fake.
- Deploy, which probes the HTTP listener, names the missing endpoint
  instead of reporting missing health.
- Deleted: Create's swallow of a missing OpenAPI document (the factory
  templates write it first) and the watcher's dead api.json check.

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.

1 participant