HTTP API · 10. gRPC

The daemon registers two gRPC services on port 4007. A third service compiles into the proto crate and is never registered, so its five operations are reachable only over HTTP. If you are working from the repository's docs/api-reference.md, that third service is documented there as live. It is not.

Read chapter 0 for the scope model. gRPC uses the same scopes, read from request metadata rather than HTTP headers. There is no separate gRPC ops token.

10.1 The correction, stated first

docs/api-reference.md lists CoreCruxObserveV1 as a live gRPC service with five RPCs: QueryOpsFacts, QueryOpsErrors, GetOpsHealth, BootstrapPull and GetBootstrapStatus.

The proto is real and it compiles. crates/corecrux-proto/build.rs compiles proto/corecrux_observe_v1.proto alongside the dataplane proto (build.rs:20) and the crate exports it as observe_v1 (lib.rs:24).

No CoreCruxObserveV1Server is ever constructed or added to the server builder. grpc::serve registers exactly two services and stops (grpc.rs:978). A repository-wide search for the server type finds nothing outside the generated module.

Calling any of those five RPCs on port 4007 does not produce the documented behaviour. The operations exist, and they exist only over HTTP:

Documented gRPC RPCWhere it actually livesChapter
QueryOpsFactsGET /v1/ops/facts3
QueryOpsErrorsGET /v1/ops/errors3
GetOpsHealthGET /v1/ops/health3
BootstrapPullPOST /v1/bootstrap/pull3
GetBootstrapStatusGET /v1/bootstrap/status3

Status: STUBBED. The proto and the generated client and server types exist; no runtime is bound to them.

10.2 What is actually served

Port 4007 by default, configured by CORECRUXD_GRPC_HOST and CORECRUXD_GRPC_PORT (config.rs:812). Served by grpc::serve (grpc.rs:962). Protos live in proto/ and are compiled by crates/corecrux-proto/build.rs.

Two services are registered (grpc.rs:978):

ServiceProtoRPCs
CoreCruxDataPlaneV1corecrux_dataplane_v1.proto:99
CoreCruxExportV1corecrux_dataplane_v1.proto:231

CoreCruxDataPlaneV1

RPCRequestResponseScope checkBehaviour in this edition
AppendBatchAppendBatchRequestAppendBatchResponseevents:write, tenant-bound on req.tenant_id (grpc.rs:764)UNIMPLEMENTED, "requires the proprietary edition"
ReadStreamReadStreamRequeststream ReadStreamResponseevents:read, tenant-bound (grpc.rs:777)UNIMPLEMENTED
ReadStreamBatchedReadStreamBatchedRequeststream ReadStreamBatchResponsenone (grpc.rs:788)UNIMPLEMENTED
ReadStreamBatchedUnaryReadStreamBatchedRequestReadStreamBatchResponsenoneUNIMPLEMENTED
ReadManyBatchedUnaryReadManyBatchedRequestReadManyBatchedResponsenoneUNIMPLEMENTED
ReadManyFramesBatchedUnaryReadManyFramesBatchedRequestReadManyFramesBatchedResponsenoneUNIMPLEMENTED
ReadFramesBatchedUnaryReadStreamBatchedRequestReadFramesBatchRawResponsenoneUNIMPLEMENTED
ReplaySessionstream ReplaySessionRequeststream ReplaySessionResponsenoneUNIMPLEMENTED (grpc.rs:840)
ReadFramesReadFramesRequeststream ReadFramesResponsenoneUNIMPLEMENTED (grpc.rs:850)

CoreCruxExportV1

RPCRequestResponseScope checkBehaviour in this edition
ExportReceiptBundleExportReceiptBundleRequeststream ExportChunknone (grpc.rs:944)UNIMPLEMENTED

Read the scope column carefully

Only the two RPCs that carry a tenant_id check a scope before returning UNIMPLEMENTED. The other eight return UNIMPLEMENTED unconditionally.

That means their scope requirements are unobservable in this edition, not that they have none. A build that supplies the dataplane implementation would need to add those checks. Do not infer from a successful UNIMPLEMENTED that an RPC is open, and do not infer from the absence of a check here that a different build will not have one.

events:read and events:write are gRPC-only scopes. Nothing on the HTTP surface accepts either. If you have been provisioning events:read for HTTP receipt access, see chapter 4 §4.2; that is one of the seven factual errors in the older reference.

10.3 Transport hardening

Applied in grpc::serve from the same ingress configuration as the HTTP listeners (grpc.rs:969).

ControlEnv varDefaultNotes
HTTP/2 keep-alive ping intervalCORECRUXD_GRPC_KEEPALIVE_INTERVAL_SECS30 s (config.rs:161)0 disables pings. With pings disabled, a dead peer holds its connection open indefinitely.
Keep-alive acknowledgement timeoutCORECRUXD_GRPC_KEEPALIVE_TIMEOUT_SECS10 s (config.rs:163)Only meaningful when the interval is non-zero.
Max concurrent HTTP/2 streams per connectionCORECRUXD_GRPC_MAX_CONCURRENT_STREAMS1024 (config.rs:167)0 is unbounded. This is transport protection only; per-tenant fairness is a separate layer.
TCP_NODELAY-onMatches the HTTP listeners.
Panic recovery-onA handler panic becomes a clean INTERNAL status with grpc-status: 13, mirroring the HTTP CatchPanicLayer. Without it a panic aborted the task and dropped the connection (grpc.rs:969).

The gRPC listener does not inherit the HTTP ingress limits from chapter 0 §0.8. apply_ingress_limits is applied to the API router and the MCP router, not to the tonic server. The body cap, the per-IP rate limiter, the in-flight gate and the 30-second timeout are HTTP-plane controls. The three settings above are what protects port 4007.

10.4 Failure modes

SymptomMost likely cause
UNIMPLEMENTED from every dataplane RPCExpected in this edition. The dataplane implementation is not present.
Service-not-found for any CoreCruxObserveV1 RPCNo server is registered for it. Use the HTTP routes in §10.1.
A gRPC call succeeding with no credentialEight of the ten RPCs skip the scope check before returning UNIMPLEMENTED. It reached no data.
Connections accumulating from dead peersCORECRUXD_GRPC_KEEPALIVE_INTERVAL_SECS=0 disables pings, so nothing reaps them.
One client saturating the listenerCORECRUXD_GRPC_MAX_CONCURRENT_STREAMS=0 is unbounded. The default is 1024.
INTERNAL with grpc-status: 13A handler panicked and was recovered. The daemon and the connection survived; check the daemon log.
403 on HTTP receipt routes with an events:read tokenevents:read authorises ReadStream on 4007 and nothing on HTTP.

Sources