This site is being rebuilt and some pages are out of date. For current details, write to reach@inorbit.hr. This notice goes away when the rebuild is done.

No analytics unless you allow it, no tracking. This site keeps in your browser the language you pick, the theme, its colour, which site you chose, the currency on the pricing page and that you closed this notice; signing in adds session cookies. The legal page has the details.

Sign in

← Back to Platform

RFC 0040.2open2026-10-03

Surface checks on every protocol

Generic checks for HTTP, gRPC, SSE, WebSocket, MQTT, GraphQL and MCP that ask each surface three questions (does it answer, does it answer in the declared shape, does it refuse a caller it cannot identify), run from our cloud for verified public hosts and from the agent for everything else.

Part of RFC 0040 Chaos as a service, our test bench in the customer's network

Problem

The first question an autonomous change has to answer after it rolls is whether every surface of the platform still answers, still answers in the shape its callers expect, and still refuses a caller it cannot identify. A change to a route, a proto or a gateway rule can break any of the three, and the third breaks silently: nothing fails when a guard opens.

chaos validate asks these questions of our surfaces, by hand. Every check is written for one service: rest_evaluate posts to our evaluate route and looks for our stub field, graphql_evaluate asks for our version and engineReady, mqtt_rpc publishes to our RPC topic, http_mcp_tools lists our MCP tools. A new route gets no check until someone writes one, and the checks know nothing of our OpenAPI document or our protos.

The refusals are the weaker half. Six checks assert that a call without a verified caller is refused (grpc_agents_unauthenticated, grpc_runner_unauthenticated and four more), and all six are gRPC. Nothing checks that a REST route, an SSE stream, a WebSocket upgrade, an MQTT CONNECT, a GraphQL query or an MCP call without a token is refused. The gateway refuses them today; we trust that rather than check it.

The agent (RFC 0029) checks http, tcp, tls and grpc_health, and never reads more than a status line and headers, so it cannot judge the shape of an answer.

Proposal

Three questions per surface

Every surface gets the same three checks. The first two run with the connection's credential when it has one; the third runs without any.

Surface Answers Answers in the declared shape Refuses an unknown caller
HTTP / REST a named operation returns an expected status the body matches the operation's response schema for that status the same request without a credential gets 401 or 403, never 2xx
gRPC a named unary method returns OK the reply decodes against the method's output message without metadata: UNAUTHENTICATED or PERMISSION_DENIED
SSE the stream opens as text/event-stream and delivers N events within the limit each event's data matches the declared event schema refused before the first event
WebSocket the upgrade completes and a declared message gets a reply the reply matches its declared schema 401 before the upgrade, or a close with code 1008
MQTT 5 CONNECT gets a CONNACK with reason code 0x00; a declared subscription is granted the CONNACK announces limits; messages on a declared topic match their schema CONNECT without credentials gets 0x86 or 0x87
GraphQL a declared query returns data without errors the result matches the schema's types for that query 401, or an errors entry declared as the refusal
MCP initialize and tools/list answer every tool has an object input schema and the list matches the pinned one tools/list without a token gets 401

A refusal check that stops refusing goes down with the class guard_open, as the [[refuse]] checks of RFC 0040.1 do, and is never folded into an uptime figure.

A diagram is drawn here in the RFCs product; this page does not show diagrams yet.

Where the shape comes from

The shape is the target's own contract, attached to its connection and versioned with it. For us that is the OpenAPI document and the protos the protocol already serves:

  • HTTP and SSE: an OpenAPI 3.1 document, uploaded or fetched from a URL inside a verified domain. A check names an operation by its operationId.
  • gRPC: protobuf descriptors, through server reflection where it is on, or uploaded as a FileDescriptorSet (what buf build writes).
  • GraphQL: the schema as SDL, uploaded, or read by introspection where it is on.
  • WebSocket and MQTT: a JSON Schema per message or topic, in the connection.
  • MCP: tools/list is the shape. It is pinned the first time it is read, as RFC 0018 pins outside MCP servers, and a changed tool is a failed shape check until a person accepts the difference.

Every result names the hash of the contract it was judged against.

Reading a body, and what leaves the machine

Rule 5 of the parent says never a body. A shape check has to read one, so this child states the exception: the executor reads at most 1 MiB of a response (at most 20 events of a stream), validates it in memory, and drops it. What it reports is the verdict and the first mismatch as a location and two types, never a value:

FAIL shape listOrders 200 /items/3/total: expected string, found number.

The agent's admin page says so, and a policy that does not want it leaves those surfaces out.

Write tools on an MCP server

Our http_mcp_tools check fails when a tool's name contains a verb such as _delete, _send or _inject_fault. That works for our names and no one else's. The generic check uses three sources and trusts none alone:

  • the tool's readOnlyHint and destructiveHint annotations (MCP tools), which the server sets about itself and are therefore hints;
  • the verbs in its name, as today;
  • a list of tools allowed to write, kept in the connection.

A tool neither annotated read-only nor on the list fails; one annotated read-only whose name says it deletes fails as a contradiction.

On the agent

Each surface is a capability the agent announces in its hello, as check:http is today: check:grpc (a unary method from descriptors, beside the existing check:grpc_health), check:sse, check:ws, check:mqtt, check:graphql and check:mcp. The policy's surfaces decides which are on:

[work]
checks = true
surfaces = ["http", "tls", "grpc_health", "grpc", "sse", "ws", "mqtt", "graphql", "mcp"]

An absent surfaces means all today; the new ones are not in that default, so an upgraded agent keeps the four it had until a person adds the rest.

From our cloud or from the agent

Our cloud runs every surface against public addresses of hosts inside a verified domain (RFC 0030), with credentials from our vault (RFC 0018). Only the agent reaches what has no public name or address, reflection against internal services, and checks whose credential stays in the company's own store. Either way a check is one request or one bounded stream, at most 30 seconds, and can run as a monitor (RFC 0037).

How an agent asks

A set of surface checks against a target is a run (RFC 0040.8). An autonomous change starts one before it asks for a merge and again after it rolls, through the API, the MCP tool reliability_check_surfaces or iohr reliability check --surfaces api, and reads the result as a list of checks with their verdicts and the contract hash. Starting it needs a scope to run checks, nothing that changes the platform.

Later, the same checks point at a customer's connections and judge the customer's own contract. None of it is offered until it has run against this platform.

Alternatives considered

Trust the gateway to refuse. What we do on six of seven surfaces today; a refusal nobody checks is one nobody notices losing.

Learn the shape from observed answers. No contract to upload, and the first wrong answer becomes the contract. We judge against what the contract declares.

Generate many requests from the contract. Property-based tools such as Schemathesis do this well and send writes. A surface check is a few fixed calls on a clock; generated requests belong to findings (RFC 0040.6).

Decision

Open. Proposed: the three questions on seven surfaces, the shape from the target's contract with the hash recorded, bodies read and dropped with only the location of a mismatch reported, MCP write tools judged by annotations, names and an allowlist, and new agent surfaces off until the policy names them. First proof: every surface of our public API checked every five minutes from the agent on our own server, including that REST, SSE, WebSocket, MQTT, GraphQL, gRPC and MCP each refuse a call without a token, and our MCP server judged by the generic MCP check in place of http_mcp_tools.

Publication

The developer docs gain a page per surface with its three checks and where its shape comes from; the agent's policy reference gains the new surfaces and the body exception; the console shows each target's checks grouped by surface.

Status log

  • 2026-10-03: opened, from the checks chaos validate runs against this platform and the four surfaces the agent checks today.
  • 2026-10-06: the platform accepts the transport surfaces grpc, sse, ws, mqtt, mcp and graphql on declared checks and jobs, and the result class answer (a transport answered in the wrong shape). A job for a declared check names its key; what the surface sends beyond its target stays in the agent's own file. The agent's executors follow in the dataplane; until an agent announces them, such a job is refused and its monitor pauses.
  • 2026-10-07: the method suite. The platform answers, to a probe account's token (a new scope, probe:read), every operation it serves on each transport (REST, SSE, the multiplexed socket, MQTT, gRPC, MCP and GraphQL), generated from the registry and the OpenAPI document: how to call it, an example request from the contract's defaults, which earlier call provides each path variable, and an order per resource (create, read, update, the resources under it, delete) so a sweep leaves nothing behind. A method a sweep must never call is listed as skipped with its reason: live money, outbound mail and messages, erasure and export of people, NDAs, role and credential changes, the administrators' areas, InOrbit's own accounts and books, calls above a cost cap, writes nothing deletes, and the person's and administrator's calls a probe token cannot make. A test fails when an operation or RPC is accounted for nowhere, so a new one cannot escape the sweep. Of 445 operations, 117 are called on at least one transport today; the sweep that walks the suite is next.
  • 2026-10-07: Checked: transport surfaces on declared checks (#489) run in agents and connections at dab02d1a. The method suite (#537) is merged, not rolled: protocol runs dab02d1a. The agent's executors (inorbithr/dataplane#22) are merged; their release is not verified here. Open.
  • 2026-10-07: the probe account the sweep runs in (RFC 0051, #551): a command makes it and its token, holding exactly the scopes the suite's called entries need, written only to the agent's local secret file. Not yet run against production. The sweep, as it will run:

A diagram is drawn here in the RFCs product; this page does not show diagrams yet.

← Back to Platform