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 RPC | Where it actually lives | Chapter |
|---|---|---|
QueryOpsFacts | GET /v1/ops/facts | 3 |
QueryOpsErrors | GET /v1/ops/errors | 3 |
GetOpsHealth | GET /v1/ops/health | 3 |
BootstrapPull | POST /v1/bootstrap/pull | 3 |
GetBootstrapStatus | GET /v1/bootstrap/status | 3 |
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):
| Service | Proto | RPCs |
|---|---|---|
CoreCruxDataPlaneV1 | corecrux_dataplane_v1.proto:9 | 9 |
CoreCruxExportV1 | corecrux_dataplane_v1.proto:23 | 1 |
CoreCruxDataPlaneV1
| RPC | Request | Response | Scope check | Behaviour in this edition |
|---|---|---|---|---|
AppendBatch | AppendBatchRequest | AppendBatchResponse | events:write, tenant-bound on req.tenant_id (grpc.rs:764) | UNIMPLEMENTED, "requires the proprietary edition" |
ReadStream | ReadStreamRequest | stream ReadStreamResponse | events:read, tenant-bound (grpc.rs:777) | UNIMPLEMENTED |
ReadStreamBatched | ReadStreamBatchedRequest | stream ReadStreamBatchResponse | none (grpc.rs:788) | UNIMPLEMENTED |
ReadStreamBatchedUnary | ReadStreamBatchedRequest | ReadStreamBatchResponse | none | UNIMPLEMENTED |
ReadManyBatchedUnary | ReadManyBatchedRequest | ReadManyBatchedResponse | none | UNIMPLEMENTED |
ReadManyFramesBatchedUnary | ReadManyFramesBatchedRequest | ReadManyFramesBatchedResponse | none | UNIMPLEMENTED |
ReadFramesBatchedUnary | ReadStreamBatchedRequest | ReadFramesBatchRawResponse | none | UNIMPLEMENTED |
ReplaySession | stream ReplaySessionRequest | stream ReplaySessionResponse | none | UNIMPLEMENTED (grpc.rs:840) |
ReadFrames | ReadFramesRequest | stream ReadFramesResponse | none | UNIMPLEMENTED (grpc.rs:850) |
CoreCruxExportV1
| RPC | Request | Response | Scope check | Behaviour in this edition |
|---|---|---|---|---|
ExportReceiptBundle | ExportReceiptBundleRequest | stream ExportChunk | none (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).
| Control | Env var | Default | Notes |
|---|---|---|---|
| HTTP/2 keep-alive ping interval | CORECRUXD_GRPC_KEEPALIVE_INTERVAL_SECS | 30 s (config.rs:161) | 0 disables pings. With pings disabled, a dead peer holds its connection open indefinitely. |
| Keep-alive acknowledgement timeout | CORECRUXD_GRPC_KEEPALIVE_TIMEOUT_SECS | 10 s (config.rs:163) | Only meaningful when the interval is non-zero. |
| Max concurrent HTTP/2 streams per connection | CORECRUXD_GRPC_MAX_CONCURRENT_STREAMS | 1024 (config.rs:167) | 0 is unbounded. This is transport protection only; per-tenant fairness is a separate layer. |
| TCP_NODELAY | - | on | Matches the HTTP listeners. |
| Panic recovery | - | on | A 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
| Symptom | Most likely cause |
|---|---|
UNIMPLEMENTED from every dataplane RPC | Expected in this edition. The dataplane implementation is not present. |
Service-not-found for any CoreCruxObserveV1 RPC | No server is registered for it. Use the HTTP routes in §10.1. |
| A gRPC call succeeding with no credential | Eight of the ten RPCs skip the scope check before returning UNIMPLEMENTED. It reached no data. |
| Connections accumulating from dead peers | CORECRUXD_GRPC_KEEPALIVE_INTERVAL_SECS=0 disables pings, so nothing reaps them. |
| One client saturating the listener | CORECRUXD_GRPC_MAX_CONCURRENT_STREAMS=0 is unbounded. The default is 1024. |
INTERNAL with grpc-status: 13 | A handler panicked and was recovered. The daemon and the connection survived; check the daemon log. |
403 on HTTP receipt routes with an events:read token | events:read authorises ReadStream on 4007 and nothing on HTTP. |
Sources
- crates/corecruxd/src/grpc.rs:962,
serve - crates/corecruxd/src/grpc.rs:978, the two
add_servicecalls, and nothing else - crates/corecruxd/src/grpc.rs:764,
events:write, tenant-bound - crates/corecruxd/src/grpc.rs:777,
events:read, tenant-bound - crates/corecruxd/src/grpc.rs:944, export RPC, no scope check
- proto/corecrux_dataplane_v1.proto:9,
CoreCruxDataPlaneV1 - proto/corecrux_dataplane_v1.proto:23,
CoreCruxExportV1 - proto/corecrux_observe_v1.proto:9,
CoreCruxObserveV1, compiled and never served - crates/corecrux-proto/build.rs:20, both protos compiled
- crates/corecrux-proto/src/lib.rs:24, the exported
observe_v1module - crates/corecruxd/src/config.rs:161, gRPC keep-alive and stream-cap defaults
- crates/corecruxd/src/config.rs:812, gRPC host and port

