Orchestrata Ultimate › Plans › grpc-retirement-design

gRPC retirement — design

Source: plans/grpc-retirement-design.md at 8a2cb42ad · last changed 2026-08-07 · first committed 2026-08-07 · 115 lines.

Prose pending.

Plan text

gRPC retirement — design

Branch: feat/cxp-direct-transport · Authorized: 2026-08-07 ("agreed merge, retire grpc, proceed as you recommend") · RUL-022 design-of-record for this change.

Why

Two independent drivers converge on the same removal.

  1. SPEC-023 (GIL-off). grpc.aio's EventEngine threads re-enable the GIL. cxpt — the vendored free-threaded direct transport — exists to replace it, and as of 2026-08-07 prod runs on it (env/prod.env: CXP_TRANSPORT=cxpt, both units verified serving). grpc is now dead weight on the RPC path.
  2. The merge is blocked by it. MR !8's architecture-check job fails at uv sync --dev:
    failed to query metadata of file vendor/wheels/grpcio-1.82.1-cp314-cp314t-linux_x86_64.whl:
    No such file or directory
    
    .gitignore:36 wheels/ makes the vendored wheels untracked by design ("fresh clones rebuild it"). CI has no wheel-build step, so any pinned [tool.uv.sources] path into vendor/wheels/ is an unresolvable dependency for a fresh clone.

The measured dependency split

grpcio (the RPC runtime) and protobuf (the message codec) are separable, and only the former is entangled with the transport.

file grpc contact verdict
cxp/proto/cxp_pb2.py none — imports google.protobuf only keep
cxp/grpc_server.py lines 1-504 grpc.aio.ServicerContext in annotations only (grep 'context\.' → zero hits) extract, neutral
cxp/grpc_server.py lines 505-599 (CXPServer) grpc.aio.server(), servicer registration delete
cxp/grpc_client.py GRPCClient 3 sites: aio.Channel, aio.insecure_channel, grpc.RpcError delete
cxp/grpc_client.py converters + GRPCClientConfig none extract, neutral
cxp/proto/cxp_pb2_grpc.py generated stubs, import grpc delete

That context is annotation-only is exactly why direct_cxp_adapter can already pass a bare sentinel into CXPServicer — the cxpt path has been running the "grpc" servicer in production all along. Extraction is therefore a move, not a rewrite: no behaviour changes hands.

Target shape

cxp/servicer.py     (new)  CXPServerConfig, CXPStats, CXPServicer, _env_*, _*_str_to_proto
                           — transport-neutral; `context: Any`
cxp/proto_codec.py  (new)  CXPClientConfig, packet<->proto converters
cxp/transport.py           cxpt is the ONLY transport; CXP_TRANSPORT=grpc now RAISES
cxp/direct_cxp_adapter.py  imports from servicer/proto_codec, not grpc_*
cxp/grpc_server.py         DELETED
cxp/grpc_client.py         DELETED
cxp/proto/cxp_pb2_grpc.py  DELETED

Call sites to rewire (11): agents/runtime_main.py, cxp/transport.py ×2, cxp/direct_cxp_adapter.py ×2, kernel/cli_main.py, kernel/dispatcher.py, knowledge_graph/server.py, gateway/server.py, cxp/proto/__init__.py, scripts/start_test_services.py.

Test modules to retire or repoint (6): tests/cxp/test_grpc{,_client,_fields,_kernel_endpoint}.py, tests/cxp/test_proto_grpc.py, tests/gateway/test_kernel_reply_consumer.py.

grpcio-tools — the second wheel, and why it cannot just move to PyPI

scripts/gen_cxp.py regenerates the stubs with grpc_tools.protoc, and tests/scripts/test_gen_cxp_cli.py drives it through a real protoc subprocess (RUL-002: no mocks). So grpcio-tools cannot simply be dropped.

It also cannot come from PyPI: measured 2026-08-07, grpcio-tools 1.83.0 publishes cp314 wheels but no cp314t. Under python3.14t that means a source build — which is why it was vendored FT in the first place.

Resolution: take it out of the dependency graph entirely. protoc is a build tool that emits text in a child process; it has zero runtime coupling to this package and no reason to be free-threaded. gen_cxp.py invokes it through an ephemeral GIL-enabled interpreter:

uv run --isolated --no-project --with grpcio-tools --python 3.14 \
    python -m grpc_tools.protoc ...

Both grpcio and grpcio-tools then leave [project.dependencies] and [tool.uv.sources], vendor/wheels/ stops being load-bearing for uv sync, and CI's fresh clone resolves.

Order of work

  1. Extract the neutral core (servicer.py, proto_codec.py); delete the grpc shells; rewire the 11 call sites. transport.py raises on CXP_TRANSPORT=grpc rather than silently falling back — a removed transport must fail loudly.
  2. Repoint/retire the 6 grpc test modules.
  3. gen_cxp.py → ephemeral protoc; verify regeneration is byte-identical to the committed stubs (minus cxp_pb2_grpc.py, which stops being generated).
  4. Drop grpcio + grpcio-tools from pyproject.toml deps and [tool.uv.sources]; uv lock; confirm vendor/wheels/ is no longer referenced by any resolver input.
  5. RUL-008 review battery; full observed gate; commit; push; retry glab mr merge 8.

Verification

Revert

Prod transport reverts by deleting CXP_TRANSPORT=cxpt from env/prod.env and restarting both units — but only while grpc_server.py/grpc_client.py still exist. After step 1 lands there is no grpc transport to revert to; the revert is git revert of the retirement commit plus rebuilding the vendored wheels.