Skip to content

Latest commit

 

History

History
628 lines (505 loc) · 29.3 KB

File metadata and controls

628 lines (505 loc) · 29.3 KB
title Stdio MCP Proxy Guide
created 2026-08-01
updated 2026-10-01

Stdio MCP Proxy

labby proxy runs a stdio MCP server in the foreground and exposes its tools as a Streamable HTTP endpoint. With --mcp-json, it aggregates local stdio and remote HTTP servers from a .mcp.json. Funnel makes the endpoint reachable from the public internet; Labby permits Funnel only with OAuth. In Funnel mode, this single proxy process also serves the OAuth routes, including the Google callback. It needs durable Labby OAuth state but no separate labby serve.

The 30-second version

You need: Node.js, signed-in Tailscale with Funnel enabled, and Apple Silicon or Linux with KVM. Verify the selected release includes Funnel proxy support with proxy --help.

  1. Get your Google callback first:

    npx -y @dinglebear/labby proxy --funnel --port 8443

    Copy the printed callback into a Google Cloud Web application OAuth client's authorized redirect URIs. This preview publishes nothing.

  2. Configure OAuth and save defaults. Replace the example origin with the printed origin; provide your Google client ID, secret, and admin email when setup requests them:

    npx -y @dinglebear/labby setup --role server --auth oauth --oauth google --public-url https://YOUR-NODE.ts.net:8443 --no-desktop --yes
    npx -y @dinglebear/labby config proxy set --exposure funnel --auth oauth --port 8443 --yes
  3. Start it and keep it running:

    npx -y @dinglebear/labby proxy -- npx -y microsandbox-mcp
  4. Connect ChatGPT: enable Developer mode, create an app using the printed /mcp URL and OAuth, then sign in with Google.

  5. Ask ChatGPT: “Check the Microsandbox runtime, install it if needed, then create, run, and remove a test sandbox.”

Keep ~/.labby across restarts. Subscriptions work; no OpenAI API integration is required. See the detailed steps below for prerequisites and troubleshooting, or use the private OpenAI tunnel.

ChatGPT web with Google OAuth and Microsandbox

Labby and the Microsandbox MCP adapter can both run through npx; there is no separate manual Labby binary install for this path. You still need Node.js 22+ and Tailscale. The Microsandbox runtime can be installed through its MCP tools after connecting ChatGPT. The Labby npm launcher downloads the matching native Labby release binary on first use and verifies its release checksum. It does not verify the release's build attestation; use the verified installer if that provenance check is required. The npm launcher supports macOS Apple Silicon and Linux x86-64. Keep npm's cache and $LABBY_HOME available across runs. The commands below require a Labby release containing Funnel proxy support. Verify proxy --help lists --funnel and --mcp-json in the selected release. If either is absent, replace npx -y @dinglebear/labby with a checkout binary containing those features. Keep the child command npx -y microsandbox-mcp as shown.

  1. Install Tailscale on the host that will run Labby and the microVMs. On macOS, Funnel requires a Tailscale open source variant. Sign in, enable Funnel for the node in the tailnet admin console, and ensure its HTTPS certificate is available. Run this before creating Google OAuth credentials:

    npx -y @dinglebear/labby proxy --funnel --port 8443

    Labby reads the node's public DNS name from tailscale status --json and prints the exact Google redirect URI and MCP URL when provider credentials are absent. The preview starts no child and makes no public mapping. Record the printed port; use that same port on every run so the callback and ChatGPT connector URL stay stable. Funnel supports HTTPS ports 443, 8443, and 10000.

  2. In Google Cloud, configure the OAuth consent screen, then create a Web application OAuth client. Add the printed redirect URI to its authorized redirect URIs. Save the client ID and secret. Set up Labby Google OAuth using the printed origin as --public-url, and select --auth oauth:

    npx -y @dinglebear/labby setup --role server --auth oauth --oauth google \
      --public-url https://node.example.ts.net:8443 --no-desktop --yes

    Provide LABBY_GOOGLE_CLIENT_ID, LABBY_GOOGLE_CLIENT_SECRET, and LABBY_AUTH_ADMIN_EMAIL through the setup's secret inputs. Server setup without --deployment prepares authentication without installing or starting a daemon. The proxy initializes its durable OAuth state on first startup. Keep $LABBY_HOME, its .env, auth database, and signing key across restarts. The configured public origin must exactly match the Funnel origin, including its port. Save the proxy defaults once, using the same port:

    npx -y @dinglebear/labby config proxy set \
      --exposure funnel --auth oauth --port 8443 --yes
  3. Ensure the host supports local microVMs: Linux needs KVM access, and macOS requires Apple Silicon. The official microsandbox-mcp adapter starts through npx even before the runtime is installed. It exposes runtime_check and runtime_install for the next step; starting the adapter alone does not automatically install the runtime.

  4. Start the proxy. It reads Funnel, OAuth, and the stable port from the saved configuration, so subsequent runs need no exposure or port flags:

    npx -y @dinglebear/labby proxy -- npx -y microsandbox-mcp

    Pin the MCP package for a durable deployment. The proxy runs in the foreground and prints its exact public /mcp URL. Keep it running. --funnel selects OAuth automatically. Explicit --auth none, bearer, or tailnet fails before publication. The longer --print-google-callback option remains available to reprint the callback later without starting the proxy.

  5. Enable ChatGPT web developer mode in Settings → Security and login. Open ChatGPT Plugins, select the plus button, and create a developer-mode app with the printed URL and OAuth authentication, then complete Labby's Google sign-in. On Business, a workspace admin/owner enables developer mode and creates the app; Enterprise/Edu admins grant developer access. Ask ChatGPT to call runtime_check, use runtime_install if needed, and check again. Then create, run, and remove one disposable sandbox. If MCP installation fails, use the official manual runtime installer on the host, then repeat runtime_check:

    curl -fsSL https://install.microsandbox.dev | sh

    Developer mode supports read and write tools on Plus, Pro, Business, Enterprise, and Education web accounts, subject to confirmation settings and workspace permissions. Select Developer mode and the app in the chat composer before asking it to control a sandbox.

The proxy exposes the Microsandbox MCP tools directly. It does not install a coding agent inside a VM, manage task worktrees, or turn a one-off command sandbox into a durable worker. Give each task an explicit source transfer, agent runtime, resource limit, and cleanup plan.

ChatGPT subscriptions can use custom MCP apps; a Responses API integration is not required.

Optional: private MCP connection with OpenAI Secure MCP Tunnel

Secure MCP Tunnel can connect ChatGPT to a private MCP endpoint. This alternative uses OpenAI organization/workspace permissions; the Google OAuth Funnel flow above remains available when Google sign-in is required.

  1. In Platform tunnel settings, create a tunnel and associate its owning Platform organization and target ChatGPT workspace. Personal accounts use their personal Platform organization. Creating a tunnel requires Tunnels Read + Manage; running/selecting it requires Tunnels Read + Use.

  2. Download tunnel-client from those settings or the official release. Start a loopback-only Labby proxy; record its URL:

    npx -y @dinglebear/labby proxy --local --auth none --port 18765 -- npx -y microsandbox-mcp
  3. Supply the runtime API key as CONTROL_PLANE_API_KEY, then configure and run:

    tunnel-client init --sample sample_mcp_stdio_local --profile labby \
      --tunnel-id tunnel_YOUR_ID --mcp-server-url http://127.0.0.1:18765/mcp
    tunnel-client doctor --profile labby --explain
    tunnel-client run --profile labby
  4. Create the ChatGPT developer-mode app with Connection → Tunnel, select the tunnel, and choose No Authentication for this private endpoint. Keep both processes running. Never publish this unauthenticated listener.

Tunnel transport does not automatically make an OAuth authorization server reachable for browser sign-in. If you select OAuth, its issuer and callback must still be reachable. Consult the linked tunnel documentation for permission and workspace-association troubleshooting.

Zero-flag quickstart

Run the one-time setup, check the host, then launch a JavaScript server:

labby config proxy set
labby doctor proxy
labby proxy /path/to/dist.js

The built-in defaults are Tailscale Serve exposure, tailnet authentication, /mcp, and a random port from 49152 through 65535. After setup, the normal run needs no proxy flags. Startup prints the resolved child command, public URL, exposure, and auth policy, then waits for Ctrl+C.

MCP proxy ready

  Server   node /path/to/dist.js
  URL      https://node.example.ts.net:53147/mcp
  Exposure Tailscale Serve
  Auth     Tailnet

Press Ctrl+C to stop.

Use --json for one machine-readable readiness object. It contains url, exposure, auth, external_port, local_addr, command, child_pid, and protocol_version. It never contains a bearer token, OAuth token, lease ID, authorization header, or child environment value.

Command and launcher resolution

Labby parses its options only before the first child token. Everything after that token belongs to the child, including values beginning with -:

labby proxy /path/to/dist.js --workspace /srv/data --read-only
labby proxy --port 52177 /path/to/dist.js --child-flag

An explicit separator is accepted but is not normally required:

labby proxy -- npx -y @modelcontextprotocol/server-filesystem /srv/data

The first child token resolves in this order:

  1. An existing executable file runs directly.
  2. An existing file with a valid shebang runs directly when executable; on Unix, a non-executable file may use a standards-compliant interpreter plus one optional shebang argument.
  3. .js, .mjs, and .cjs files use node from PATH.
  4. .py files use python3 from PATH.
  5. A bare command is resolved through PATH.
  6. An unknown, non-executable file fails with an explicit-command suggestion.

Labby never invokes a shell. TypeScript has no inferred launcher; use a shebang or an explicit command such as labby proxy -- npx tsx server.ts. Arguments are retained as OsString, including non-UTF-8 Unix arguments. --cwd changes the child working directory; otherwise the caller's current directory is used.

Exposure, authentication, ports, and output

Setting Behavior
tailscale exposure Binds HTTP to loopback, publishes one HTTPS port with Tailscale Serve, and prints the tailnet URL. This is the default.
funnel exposure Binds HTTP to loopback and publishes one public HTTPS port with Tailscale Funnel. Requires OAuth and port 443, 8443, or 10000. Random mode tries 8443, 10000, then 443.
local exposure Binds and prints a loopback HTTP URL only. Use --local; it never binds a LAN wildcard.
tailnet auth Adds no application token. Reachability and grants are owned by Tailscale. Valid only with Tailscale exposure. This is the default.
bearer auth Requires the separate proxy bearer token on every MCP request and SSE stream.
oauth auth Validates Labby-issued JWTs for the exact proxy resource URL and configured scopes. Funnel hosts its own OAuth router; Serve uses a live OAuth daemon.
none auth Adds no application authentication. Intended for explicit loopback use such as --local --auth none.
random port Chooses an unused external Serve port from the configured range, with collision retries.
fixed port Uses the numeric proxy.port or one-run --port; startup fails rather than replacing an existing mapping.

One-run examples:

labby proxy --port 52177 /path/to/dist.js
labby proxy --auth oauth /path/to/dist.js
labby proxy --funnel --port 8443 -- npx -y microsandbox-mcp
printf '%s\n' "$TOKEN" | labby proxy --auth bearer --bearer-token-stdin /path/to/dist.js
labby proxy --local --auth none /path/to/dist.js

--bearer-token also implies bearer mode, but shell history and process inspection can expose literal command-line secrets. Prefer setup-generated storage, the environment, or --bearer-token-stdin.

There is no silent fallback. A Tailscale, bearer, OAuth, port, child, or publication failure stops startup; a runtime failure begins owned cleanup.

Configuration and precedence

The effective order is:

  1. one-run CLI options;
  2. existing process environment, then values loaded from exactly $LABBY_HOME/.env (normally ~/.labby/.env) for unset names;
  3. exactly $LABBY_HOME/config.toml when the absolute override is set, otherwise ~/.labby/config.toml;
  4. built-in defaults.

Only secret and executable/logging controls use environment variables. Proxy preference keys do not have implicit one-to-one environment aliases. The working directory does not participate in configuration discovery. See Runtime Configuration for the canonical path contract.

Complete [proxy] table:

[proxy]
exposure = "tailscale"
auth = "tailnet"
path = "/mcp"
port = "random"
port_range_start = 49152
port_range_end = 65535
bearer_token_env = "LABBY_PROXY_BEARER_TOKEN"
oauth_scopes = ["mcp:read", "mcp:write"]
inherit_env = []
shutdown_grace_ms = 3000
Key Accepted values and validation
proxy.exposure tailscale, funnel, or local; default tailscale.
proxy.auth tailnet, bearer, oauth, or none; default tailnet. local plus tailnet is rejected.
proxy.path Absolute, non-root path without query, fragment, . segments, or .. segments; default /mcp.
proxy.port "random" or a nonzero integer. It is the external HTTPS port for Tailscale publication.
proxy.port_range_start First random candidate; default 49152, minimum 1024.
proxy.port_range_end Last random candidate; default 65535, and not below the start.
proxy.bearer_token_env Valid environment-variable name containing the bearer secret; default LABBY_PROXY_BEARER_TOKEN.
proxy.oauth_scopes Non-empty, whitespace-free scope tokens; default mcp:read and mcp:write.
proxy.inherit_env Extra valid ambient variable names copied into the otherwise scrubbed child environment.
proxy.shutdown_grace_ms Validated range 1..=60000; default 3000.

Generated machine-readable and Markdown inventories are in proxy-config-reference and the environment reference.

Proxy-relevant environment variables:

Variable Purpose
LABBY_PROXY_BEARER_TOKEN Default secret source. If bearer_token_env names another key, that configured key is used instead.
LABBY_TAILSCALE_BIN Overrides the tailscale executable used by publication and proxy preflight.
LABBY_GW_UPSTREAM_STDERR Controls the redacted child-stderr forwarding level; default debug, and off discards it.
LABBY_HOME Absolute durable-state root. Relocates config/secrets and fixes the access store at $LABBY_HOME/access.db; relative values fail closed.
LABBY_MCP_HTTP_HOST, LABBY_MCP_HTTP_PORT First live-daemon candidate for OAuth lease actions.
LABBY_MCP_HTTP_TOKEN Authenticates the proxy CLI to the daemon's admin gateway action route; it is not accepted by the proxied OAuth resource.
LABBY_PUBLIC_URL, LABBY_MCP_GATEWAY_URL Public live-daemon fallback candidates and stable OAuth issuer configuration, as described in the OAuth guide.
LABBY_PROXY_TEST_RENEW_MS Test-only renewal interval available with proxy-testkit; never use it as production configuration.

PATH is consulted for launcher inference. Runtime-essential variables are copied by the stdio spawn policy; other ambient variables require proxy.inherit_env, --inherit-env NAME, or an explicit --env NAME=VALUE. The latter two are child-process inputs, not persisted proxy preferences.

Setup, secrets, and doctor

labby config proxy set is interactive on a terminal. For automation use --yes and explicit flags; --dry-run previews without mutation. The setup action preserves unrelated TOML keys, comments, and .env entries and is byte-stable on a second identical run.

Bearer setup has two safe paths:

# Generate a new 64-character random hex token when none exists.
labby config proxy set --yes --auth bearer

# Store a supplied token from stdin; the literal is never written to TOML.
printf '%s\n' "$TOKEN" | labby config proxy set --yes --bearer-token-stdin

The non-secret preferences go to $LABBY_HOME/config.toml; the bearer secret goes to the environment key named by proxy.bearer_token_env in $LABBY_HOME/.env. Existing secrets are reused unless stdin supplies a replacement. On Unix, setup creates or repairs $LABBY_HOME to mode 0700 and .env to mode 0600. Output, debug formatting, and JSON report only that a secret changed, never its value.

labby doctor proxy with no route arguments performs the zero-route local preflight: proxy config validation, Node/Python launcher presence, selected auth prerequisites, Tailscale version/connectivity/DNS/Serve capability, and OAuth issuer/daemon/create-renew-release action checks where applicable.

The older routed reverse-proxy doctor remains available and unchanged:

labby doctor proxy \
  --app-url https://lab.example.com \
  --mcp-url https://mcp.example.com \
  --route /telemetry

Supplying route arguments selects that public Labby/protected-route check; it does not run the local stdio-proxy preflight.

Multiple MCP servers

Save a .mcp.json containing stdio entries in ~/.labby (or the configured LABBY_HOME), then run npx -y @dinglebear/labby proxy with your saved proxy defaults. If no file is present there, Labby looks beside its native executable. For the npm launcher, that means the downloaded binary's directory, not the Node wrapper's directory. The Labby home file wins when both exist.

An explicit child command bypasses discovery. --mcp-json PATH selects a specific file instead. An unreadable or invalid selected file fails startup; Labby does not silently try another configuration.

Example .mcp.json:

{
  "mcpServers": {
    "sandbox": { "command": "npx", "args": ["-y", "microsandbox-mcp"] },
    "files": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"] }
  }
}
npx -y @dinglebear/labby proxy --funnel --port 8443 --mcp-json /path/to/.mcp.json

The file can contain up to 16 named servers, mixing local stdio commands and remote Streamable HTTP endpoints. Stdio entries support command, args, and an env object. HTTP entries use url, optionally type: "http" or type: "streamable-http", and headers. Native gateway transport, socket_path, lifecycle and exposure settings also pass through shared validation. Paths resolve relative to the file's directory unless --cwd overrides it. Selected --inherit-env values reach all servers; each JSON entry overrides those values, and explicit --env values have the highest priority. Unselected ambient secrets remain scrubbed. Labby starts an isolated local MCP aggregator and connects to every configured local or remote server before public publication. Tools are qualified by upstream name, such as sandbox::runtime_check, so matching tool names can coexist. Native tool names containing :: are rejected before startup publication. Labby-owned control tools are excluded from this aggregate surface. The file and any environment secrets must be readable only by the operator. Streamable HTTP is supported; legacy standalone SSE transport is not.

{
  "mcpServers": {
    "sandbox": { "command": "npx", "args": ["-y", "microsandbox-mcp"] },
    "remote": { "type": "http", "url": "https://mcp.example.com/mcp", "bearer_token_env": "REMOTE_MCP_TOKEN" }
  }
}

bearer_token_env selects a credential from the supplied entry environment, CLI environment overrides, or the running Labby installation. Common headers: {"Authorization": "Bearer ..."} entries are also accepted and translated into private gateway credential references. HTTP credentials are kept in the isolated aggregate home and are not implicitly copied to unrelated stdio children; explicit --env and --inherit-env selections still apply to child processes. Missing configured credentials fail startup; there is no anonymous fallback. Other custom headers use the gateway validation rules.

OAuth resource lifecycle

The OAuth authorization server has a stable issuer such as https://lab.example.com. The ephemeral proxy is a separate protected resource. Its identity is the exact public URL, including port and path:

issuer:   https://lab.example.com
resource: https://node.example.ts.net:53147/mcp

Changing either 53147 or /mcp changes the JWT audience. Each random-port run therefore creates a distinct resource URL. Use a fixed proxy.port for a long-lived connector configuration; otherwise update the connector to the URL printed by every run.

With Funnel, OAuth startup requires LABBY_AUTH_MODE=oauth, Google provider credentials, and persistent signing and token state in $LABBY_HOME. The proxy serves its own authorization server at the Funnel origin and registers its exact /mcp URL as a protected resource. No daemon or lease is needed.

With Tailscale Serve, OAuth requires a stable LABBY_PUBLIC_URL (or equivalent [auth]/[public_urls] configuration), the same-host signing keys, and a reachable labby serve daemon. The CLI verifies the daemon's metadata and JWKS, checks the three lease actions, then dispatches through authenticated POST /v1/gateway:

  • gateway.oauth.resource_lease.create
  • gateway.oauth.resource_lease.renew
  • gateway.oauth.resource_lease.release

These are gateway actions, not dedicated /v1/internal/* routes. They require lab:admin; lease IDs are opaque secrets and are redacted from diagnostics.

The default lease TTL is 120 seconds. The proxy renews every 40 seconds plus up to four seconds of jitter. Renewal failure terminates the proxy. Normal shutdown releases the lease. Forced termination cannot perform an async release, so the daemon ignores the expired lease and prunes expired entries on its 30-second sweep. Configured protected resources and ephemeral leases are separate registries, so route refresh does not erase active proxy resources.

The proxy serves RFC 9728 metadata at the public origin root:

GET https://node.example.ts.net:53147/.well-known/oauth-protected-resource

That document advertises the exact resource with port and /mcp. The WWW-Authenticate challenge points to the same root metadata URL. This direct proxy does not use the path-suffixed metadata URLs used by configured Gateway protected routes. Local HTTP OAuth is intentionally rejected because daemon leases accept HTTPS resources only; there is no bearer or no-auth downgrade.

Tailscale Serve ownership and cleanup

Before publication Labby checks tailscale version, connected status, the node DNS name, and tailscale serve status --json. It treats ports present in either Serve TCP or web maps as occupied. A fixed-port collision fails. Random mode retries collision-shaped claim failures, up to 32 candidates.

The owned Serve command is exact:

tailscale serve --yes --https=<external-port> http://127.0.0.1:<local-port>

Funnel uses the same command shape with funnel instead of serve. Readiness and periodic checks require both the exact loopback backend and AllowFunnel=true for the exact public authority. If a mapping switches between Serve and Funnel, Labby stops and refuses to remove that changed mapping during cleanup.

Readiness requires the exact DNS-name, port, root handler, and loopback backend to appear in Serve status. While running, Labby watches both the foreground Serve child and that exact mapping. A disappeared or changed mapping is a runtime failure.

On Ctrl+C or a supervised component failure, cleanup stops HTTP, terminates the Serve child, waits for the mapping to disappear, reaps the stdio child, and releases the OAuth lease. If the owned mapping remains, Labby re-reads status and runs only:

tailscale serve --yes --https=<external-port> off

It does that only while the mapping still points to its recorded loopback backend. If ownership changed, cleanup refuses to remove it. Labby never calls tailscale serve reset and never rewrites unrelated mappings.

After an uncatchable process crash or power loss, inspect status before manual recovery. Remove only the printed port and only after confirming its backend is the dead proxy's 127.0.0.1:<local-port> target. Never use serve reset as a proxy cleanup shortcut. The OAuth lease recovers independently through TTL expiry; restart the proxy only after resolving any surviving exact-port mapping.

Security rationale

  • The HTTP listener binds only to 127.0.0.1; remote exposure belongs to Tailscale Serve. There is no LAN wildcard or public Funnel fallback.
  • RMCP Host validation admits loopback authorities and, when applicable, only the exact public resource host plus port. Origin validation similarly admits loopback origins and the exact public origin. Unexpected Host or Origin values are rejected to resist DNS rebinding and browser cross-origin abuse.
  • The child is launched as argv without a shell. Its environment starts with env_clear, then a small cross-platform runtime allowlist is restored, followed by explicitly inherited and explicit values. Ambient Labby/OAuth and upstream secrets are not inherited by default.
  • Child stderr is continuously drained so a full pipe cannot deadlock the MCP server. Diagnostic tails pass through central stdio redaction before logs.
  • Bearer tokens use constant-time comparison and are separate from LABBY_MCP_HTTP_TOKEN. OAuth disables static-admin-token fallback.
  • Unix process groups and Windows Job Objects own descendants so normal and supervised shutdown reap package-runner trees, not only the immediate PID.

Troubleshooting

proxy command resolution failed : Install node/python3, add the executable to PATH, use a valid shebang, or provide the launcher explicitly after --. .ts is never guessed.

tailnet auth requires Tailscale exposure : --local does not silently weaken tailnet. Select --auth bearer or --auth none explicitly for loopback use.

Tailscale publication port ... is already configured : Choose another fixed port or return to port = "random". Inspect tailscale serve status --json; do not reset unrelated routes.

Tailscale ... offline, missing DNS name, or Serve capability failure : Run labby doctor proxy, then repair Tailscale connectivity and HTTPS Serve support. Labby does not fall back to local exposure.

bearer auth requires ... : Run labby config proxy set --yes --auth bearer, export the configured key, or pipe the secret to --bearer-token-stdin. Confirm the effective $LABBY_HOME/config.toml sets the intended bearer_token_env.

proxy OAuth requires a stable Labby public issuer : For Serve exposure, configure OAuth and LABBY_PUBLIC_URL, start labby serve, and verify its authorization-server metadata. For Funnel, use the callback preview, configure Google OAuth in the same Labby home, and reuse the previewed port. Funnel uses its own public origin as issuer.

live Labby daemon does not support proxy OAuth leases : The CLI reached an older daemon. Upgrade/restart the daemon and confirm all three lease actions appear in GET /v1/gateway/actions.

OAuth 401 for a token that works elsewhere : Obtain a token whose audience is the exact printed URL, including external port and /mcp, and whose scopes cover proxy.oauth_scopes. A token for the same host on another port is intentionally rejected.

Host or Origin rejected : Connect to the exact printed URL. Reverse proxies and browser clients must preserve its authority and origin; do not replace the port or MCP path.

Proxy exits after startup : Inspect redacted logs for child closure, HTTP exit, Serve/Funnel ownership drift, or (in Serve OAuth mode) lease renewal failure. Cleanup errors are attached to the primary failure rather than replacing it.

Related documents