mobkit_gateway and the SDK-facing
rpc_gateway, bind 127.0.0.1 on an ephemeral port unless the launch selects
another address. This page explains the posture, the three shipped ways to
publish a gateway, and the exposure gate a non-loopback bind has to pass.
Why loopback is the default
Everything a gateway serves over HTTP hangs off one listener: the bundled console, console JSON-RPC, blobs, SSE event streams, and (when enabled) the live WebSocket transport.mobkit_gateway serves that console open: it has
no auth_config ingress and builds its decision state with
require_app_auth = false, so the loopback bind is its only access boundary.
rpc_gateway is fail-closed by default (it trusts no signing key and refuses
every console request until the host passes auth_config or opts out with
console_require_app_auth = false), and the common host choice is the opt-out,
which again makes loopback the boundary.
Loopback is therefore a security posture, not an oversight. The SDK process that
spawns a gateway shares its network namespace and reads the exact
http_base_url from the mobkit/init result, so nothing on the host needs a
fixed or public port.
Publishing a gateway
Same host or same network namespace: proxy to http_base_url
The pattern the shipped hosts use. The SDK runtime exposes the base URL after
connect() (runtime.rust_http_base_url in Python, runtime.rustHttpBaseUrl
in TypeScript); the host’s own web server proxies /console and the routes it
wants to publish to that base and authenticates in front. Works for a process
on the same machine, a Kubernetes sidecar in the same pod, or a Docker
container started with --network container:<gateway>. The port is ephemeral,
so read it at runtime rather than configuring it.
mobkit_gateway additionally records http_base_url in its runtime registry
under the gateway state directory, so a same-host proxy can discover a resumed
runtime without holding the init handshake.
Separate containers: bind a non-loopback address
A reverse proxy in another container on a bridge network cannot reach127.0.0.1 inside the gateway’s container. For that topology bind the listener
on the container’s interface and let the gate below decide whether the bind is
allowed:
- Python (rpc_gateway)
- TypeScript (rpc_gateway)
- mobkit_gateway
- Raw mobkit/init (rpc_gateway)
http_listen takes an IP literal and port (0.0.0.0:8080, [::]:8080,
192.168.0.10:8080); hostnames are refused so the bind cannot resolve
differently on the next host. HOST:0 still asks the kernel for a port.
The init result then carries two base URLs:
The public base is advertised, never bound: it is the operator’s statement of
how clients reach the proxy in front of the gateway. The Python runtime exposes
it as
rust_http_public_base_url (falling back to the same-host base when
none was declared) and TypeScript as rustHttpPublicBaseUrl.
Rust library host: serve the router on your own listener
UnifiedRuntime::run(listener, decisions, shutdown) and ::serve(listener, decisions) accept any TcpListener, and GatewayHttpBinding::bind(addr) is
the shared admission type the binaries use. examples/library_mode_reference.rs
(MOBKIT_REF_ADDR) is the template. A library host owns its own exposure
decision; run validate_http_bind_policy if you want the same gate.
The exposure gate: auth or acknowledgement
A non-loopbackhttp_listen is refused at init, before any runtime is
bootstrapped, unless one of two things is true:
- The console enforces app auth.
require_app_authis on AND the trusted JWKS carries at least one key, which is whatauth_config(Python.auth(...), TypeScript.auth(...)) produces. The listener then carries its own access boundary. Note that the fail-closed default,require_app_authwith an empty key set, does not count: refusing everyone protects the listener but authenticates nobody, and is not a deployment anyone intends. - The launch acknowledges the exposure with
allow_remote. This is the same word and the same rule as--allow-remoteonrkat-rpc --tcpandrkat mob host --listen-tcpin Meerkat: an explicit transport-exposure opt-in, not an auth mechanism. Pass it only when an authenticating proxy fronts the listener.
mobkit_gateway has no auth ingress, so on that binary only allow_remote
(init param, --allow-remote, or MOBKIT_HTTP_ALLOW_REMOTE=1) can open the
gate. On both binaries a refused bind answers mobkit/init with a -32602
error on the request id whose message names the address and every way out.
The listener is bound right behind the gate, before any bootstrap. A fixed
http_listen port makes “address already in use” a realistic init failure,
and it is answered the same way: -32603 on the request id naming the
address, with nothing built behind it (no runtime, no session store, and no
schedule executor lease that a process exit would leave held for its full
duration).
Every non-loopback bind that does pass logs one WARN line on stderr naming the
bound address and whether the console on it is authenticated or open. It cannot
be silenced below WARN by the default filter.
Reading schedule watchdog diagnostics
Both gateways probe the shared schedule service at boot and every 60 seconds. Only Pending occurrences more than 120 seconds overdue under an authoritative Active parent contribute to a firing-pipeline stall, its count, and its oldest due time. A boot backlog logs WARN; a new or changed periodic stall logs ERROR, with a WARN heartbeat every ten unchanged polls. Paused and Deleted parents can retain Pending occurrences without a firing fault. The DEBUGschedule overdue pending parent attribution record reports
separate active, paused, deleted, and unresolved counts; the probe does
not modify those rows. Enable meerkat_mobkit::schedule_wiring=debug in
RUST_LOG to see these buckets.
Missing parent evidence or failed/poisoned reads log observation incomplete
at ERROR, including at boot, rather than claiming health or attributing a
stall to inactive work. A mixed report retains unresolved evidence alongside
any known active backlog. Executor owner and fencing-token observations explain
an active backlog only; they never grant firing authority.
Docker sketch
http_base_url); only the proxy is
published. If the proxy instead shares the gateway’s network namespace, keep
the loopback default and proxy to http_base_url.
Resume behaviour (mobkit_gateway)
The HTTP listen address and the advertised http_public_base_url are both
part of the runtime resume fingerprint, like --control-listen. Relaunching
with a different http_listen or a different (or newly dropped)
http_public_base_url creates a runtime with the values you declared instead
of silently reporting a live runtime started with other ones; with a fixed
port that new runtime cannot bind while the old one still holds the port, and
init says so. A resumed launch (same fingerprint) reports the http_base_url
and http_public_base_url the live runtime was started with, which are by
construction the ones you declared.
Reading a gateway exit
Both gateways log on stderr, throughtracing, at the default filter (no
RUST_LOG needed). When the run loop ends, the binary writes one INFO line
naming which branch ended it, runs the graceful shutdown sequence, then writes
a closing bookend:
reason= is one of these tokens; signal= (SIGINT or SIGTERM) is present
only when the reason is signal.
mobkit_gateway does not exit on stdin EOF. It logs stdin closed by the launching process; mobkit_gateway keeps serving HTTP until SIGINT or SIGTERM
and stays up, so a later exit is attributed to what actually ended it. After a
signal it exits once shutdown completes whether or not stdin is still open (a
terminal, or a parent holding the pipe); both gateways exit explicitly on that
path because tokio’s stdin read cannot be cancelled.
Panics are reported through the same stream at ERROR, with thread, file,
line, column and payload fields, before the standard panic message
(RUST_BACKTRACE still works). A panic on a tokio worker unwinds one task and
the process keeps running, so this line may be the only record of one.
How to read a gateway that is gone:
- The “loop ended” line is present with
reason=signal: a supervisor or operator stopped it. Check who sent the signal (container stop, health check timing out during a long console operation,kill). reason=stdin_closedonrpc_gateway: the SDK host process exited or closed the pipe; look at the host first.- The “loop ended” line is present but the bookend is not: the shutdown sequence wedged or the process was killed during it.
- Neither line, but a
panicERROR: a crash; the line names the source location. - No line at all: the process was killed from outside (SIGKILL, an OOM kill,
a stack overflow) or aborted. The exit status names which;
137is SIGKILL,139is a segfault or stack overflow,134is an abort.
See also
- Configuration: the
http_listen,http_public_base_urlandallow_remoterows - Authentication: configuring
auth_config - Console: the console surface these binds publish
- Unified runtime:
GatewayHttpBindingin a library host
