Skip to content

Proxy Sidecar

The proxy-sidecar is a standalone binary that lets you bring any existing HTTP, WebSocket, TCP, or UDP service into the Aether agent fleet without modifying the service itself. It connects to the Aether gateway as a Service principal (sv::{implementation}::{specifier}) and tunnels inbound requests from agents to your local backend, so callers that only know how to speak Aether can reach legacy services through the same authenticated, audited, access-controlled channel as any other participant.

A single sidecar process can run one or more independent surfaces at the same time over a shared gateway connection. All surfaces share one Aether identity and one distributed lock; mixing them in one process avoids the duplicate-identity error that two separate gateway connections would produce.

  • Wrapping a legacy REST API so agents can call it without a rewrite.
  • Exposing WebSocket, TCP, or UDP services as Aether-native targets.
  • Cross-network bridging: the sidecar sits inside the private network where your service lives and the gateway handles all the authenticated routing from outside.
  • Sandboxed agent execution: the relay surface lets a sandbox process use the Aether SDK without holding credentials; the sidecar injects its own identity and enforces an operation allow-list.
  • Gradual migration: deploy a sidecar in front of an existing service today, rewrite the service as a native Aether participant later, swap the config — no callers change.
Agent/Caller
│
│ ProxyHttpRequest / Tunnel* (over Aether gRPC stream)
▼
Aether Gateway ──► topic: sv::{implementation}::{specifier}
│
│ downstream envelope
▼
proxy-sidecar (terminator surface)
│
│ plain HTTP / WS / TCP / UDP
▼
Local Backend (your service at http://localhost:PORT)

The sidecar registers with the gateway using a Service principal whose topic is sv::{implementation}::{specifier} — for example, sv::memorylayer::default or sv::toolrunner::us-east. Agents address the service by its topic. The gateway routes the request to the sidecar, which forwards it to the configured local backend and returns the response.

Because ACL rules apply per message, the normal Aether permission model governs which agents may call which service topics. No changes to the service itself are required.

Each surface is independently opt-in via enabled: true in the YAML config. Any combination may run in one process.

SurfaceDirectionWhat it does
terminatorGateway → local backendReceives ProxyHttpRequest and Tunnel* envelopes and forwards them to one or more configured local backends (HTTP / WS / TCP / UDP).
initiatorLocal HTTP → gatewayExposes a local HTTP listener. Legacy clients (curl, scripts) send HTTP to it; each request becomes a ProxyHttpRequest envelope addressed at a configured target topic.
relaySandbox gRPC → gatewayBinds a local AetherGateway gRPC server (UDS or TCP). Sandbox processes dial it with no credentials; the sidecar injects its own identity and enforces an operation allow-list before forwarding upstream.
Terminal window
cd server
go build -o proxy-sidecar ./cmd/proxy-sidecar

Save as proxy-sidecar.yaml:

gateway:
address: localhost:50051
insecure: true
api_key: your-service-api-key
service:
implementation: my-service
specifier: default
tenant_id: my-workspace
terminator:
enabled: true
backends:
- name: default
kind: http
url: http://localhost:8080
allow_paths:
- "/*"
allow_methods:
- GET
- POST
- PUT
- DELETE
header_mode: strict # mint X-Auth-* headers; strip caller Authorization
logging:
level: info
Terminal window
./proxy-sidecar -config proxy-sidecar.yaml

Agents can now send requests to sv::my-service::default. The sidecar forwards them to http://localhost:8080.

Pass -- <cmd> [args...] to start a wrapped child process. The sidecar starts its surfaces first, then launches the child with inherited stdio and environment. Signals are forwarded; the sidecar exits with the child’s status code.

Terminal window
./proxy-sidecar -config proxy-sidecar.yaml -- ./my-service --port 8080

Send SIGHUP to the sidecar at any time to reload the config file without restarting:

Terminal window
kill -HUP $(pidof proxy-sidecar)

Reloadable: terminator backends, gateway credentials, logging level. Not reloadable: surface enable/disable (terminator.enabled, relay.enabled, initiator.enabled).

All YAML fields are also overridable via environment variables. See Environment Variables for the full AETHER_* reference.

YAML keyTypeDefaultDescription
gateway.addressstring—Gateway gRPC address (host:port). Required.
gateway.insecureboolfalseDisable TLS (development only).
gateway.api_keystring—Inline API key. Mutually exclusive with task_token.
gateway.api_key_pathstring—Path to a file containing the API key.
gateway.task_tokenstring—Per-task token issued by CreateTask. Mutually exclusive with api_key.
gateway.task_token_pathstring—Path to a file containing the task token.
gateway.tls.cert_filestring—Client TLS certificate.
gateway.tls.key_filestring—Client TLS key.
gateway.tls.ca_filestring—CA bundle for server verification.
service.implementationstring—Implementation segment of sv::{impl}::{spec}. Required when terminator or relay is enabled.
service.specifierstring—Specifier segment of sv::{impl}::{spec}. Required when terminator or relay is enabled.
tenant_idstring—Tenant identifier injected as X-Auth-Tenant-ID when header_mode is strict or both.
logging.levelstringinfoLog level: debug, info, warn, error.
logging.formatstringautoconsole (human-readable) or json. Auto-detects TTY.
YAML keyTypeDefaultDescription
terminator.enabledboolfalseEnable the terminator surface.
terminator.backendslist—One or more backend definitions (see below).

Backend fields (terminator.backends[]):

KeyTypeDefaultDescription
namestringbackend-NLogical name. Callers may set backend_name on a request to select a specific backend directly; otherwise first-ACL-match wins.
kindstringhttpBackend protocol: http, ws, tcp, udp.
urlstring—Target URL. HTTP: http://host:port. WS: ws://host:port. TCP/UDP: host:port or tcp://host:port.
allow_paths[]string["/*"]Glob patterns for allowed request paths (HTTP/WS only).
allow_methods[]stringallHTTP methods allowed.
max_body_bytesint10485760Maximum request body size in bytes (HTTP only).
idle_timeout_msint30000Idle connection timeout in milliseconds.
header_modestringstrictIdentity header handling: strict (mint trusted headers, strip caller auth), passthrough (forward as-is), both (forward caller headers, overlay minted headers).
allow_remote_hints[]string—Glob patterns for TunnelOpen.remote_hint (TCP/UDP). Empty = only the configured URL is reachable.
max_bytesint104857600Maximum bytes per TCP/WS tunnel session.
max_datagram_bytesint1400Maximum bytes per UDP datagram.
YAML keyTypeDefaultDescription
initiator.enabledboolfalseEnable the initiator surface.
initiator.listen.bindstringlocalhost:8888Local HTTP listener address.
initiator.target.topicstring—Target service topic (e.g. sv::memorylayer::default). Required.
YAML keyTypeDefaultDescription
relay.enabledboolfalseEnable the relay surface.
relay.listenstring—Bind address for the local AetherGateway server. UDS (unix:///run/aether.sock) preferred; TCP (host:port) supported. Required.
relay.identity_overridestringenforceHow to handle the sandbox-claimed identity. Only enforce is supported (sandbox claim is discarded; sidecar identity is used).
relay.allowed_opsprofile or listsandbox-defaultOperations the sandbox may send upstream. Named profiles: sandbox-default (SendMessage, ProgressReport, KVOperation), sandbox-tunnels (extends with proxy/tunnel ops), tool-stub-only (InitConnection only). Or pass a YAML list of op identifiers.
relay.target_topic_clamp.modestringrejectHow to enforce the allowed-target list: reject (deny non-matching topics) or rewrite_first_match (rewrite to first concrete entry).
relay.target_topic_clamp.allowed_targets[]string—Glob patterns the sandbox may address in outbound envelopes. Empty = deny all proxy/tunnel targets.

These override the corresponding YAML values at startup:

VariableYAML equivalentDescription
AETHER_ADDRESSgateway.addressGateway gRPC address.
AETHER_API_KEYgateway.api_keyInline API key.
AETHER_TASK_TOKENgateway.task_tokenPer-task token.
AETHER_TENANT_IDtenant_idTenant identifier.
AETHER_LOG_LEVELlogging.levelLog level.
PROXY_SIDECAR_LISTENinitiator.listen.bindInitiator HTTP listener address.
PROXY_SIDECAR_TARGETinitiator.target.topicInitiator target topic.
PROXY_SIDECAR_RELAY_LISTENrelay.listenRelay listen address.

See Environment Variables for the complete AETHER_* reference shared across all Aether binaries.

Once the sidecar is running, any agent or caller with the appropriate ACL can address the service topic. The Go SDK example below sends an HTTP request through the sidecar:

// client is an aether.ServiceClient or aether.AgentClient connected to the gateway.
// The sidecar registered as sv::my-service::default, so that is the target topic.
resp, err := client.ProxyHTTP(ctx, &pb.ProxyHttpRequest{
TargetTopic: "sv::my-service::default",
Method: "POST",
Path: "/v1/items",
Headers: map[string]string{
"Content-Type": "application/json",
},
Body: []byte(`{"name":"widget"}`),
})
if err != nil {
return err
}
fmt.Println(resp.StatusCode, string(resp.Body))

The gateway routes the ProxyHttpRequest to the sidecar’s terminator surface, which forwards it to the configured local backend and returns the ProxyHttpResponse to the caller. The caller never contacts the backend directly.

For WebSocket and raw TCP/UDP tunnels, use the corresponding TunnelOpen / TunnelData envelope types — the sidecar handles them through the same terminator surface.

Benchmark data for the proxy routing layer comes from an in-process harness with no real gRPC transport or message broker. It measures routing-layer overhead only.

Scenariop50p99Notes
Tunnel open (wildcard resolution + pin write)20 ms38 msIn-process only; no real network or broker
REST proxy throughput——Wire-level benchmark pending

Important: These numbers reflect a lower bound on routing overhead, not end-to-end latency in a deployed system. Real deployments add gRPC transport RTT (typically 1–10 ms same-AZ), Redis round-trips (1–5 ms), and broker dispatch overhead. A wire-level load test against a deployed stack has not yet produced steady-state numbers.

Source: server/docs/proxy-load-test-results.md.

Set gateway.insecure: false (the default) in production. Provide client certificate paths under gateway.tls if your gateway requires mTLS:

gateway:
address: gateway.example.com:50051
api_key_path: /etc/aether/sidecar.key
tls:
cert_file: /etc/aether/tls/cert.pem
key_file: /etc/aether/tls/key.pem
ca_file: /etc/aether/tls/ca.pem
  • API key (api_key / api_key_path): long-lived service key. Prefer api_key_path so the key is not visible in process listings.
  • Task token (task_token / task_token_path): short-lived token issued by the gateway via CreateTask with a target_identity of sv::{impl}::{spec}. Use this when the sidecar is spawned as part of a task and should inherit the task’s scope and lifetime.

Setting both credential types in the same config is a validation error.

Send SIGHUP to reload the config file in place. Terminator backends are reloaded without dropping in-flight requests; they continue using the old backend reference until they complete. Surface enable/disable flips require a restart.

The sidecar writes structured JSON logs (or human-readable console logs when stdout is a TTY) via zerolog. Set logging.level: debug to see per-request dispatching, tunnel open/close events, and relay filter decisions. In production, info is the recommended level.

The sidecar does not currently expose a Prometheus metrics endpoint. Audit events for proxied requests are written by the gateway and are visible in the standard Aether audit log.

Sidecar exits immediately with “at least one surface must be enabled” At least one surface (terminator, initiator, or relay) must have enabled: true.

“duplicate identity” error in logs Another process connected to the gateway with the same sv::{implementation}::{specifier} topic. Each (implementation, specifier) pair must be unique per tenant. Change service.specifier or shut down the other process.

Requests timeout at the caller; sidecar logs show successful reconnect The sidecar may be reconnecting to the message broker and missing in-flight stream frames. This is a known issue when the broker restarts; the sidecar’s stream consumer starts from the next offset after reconnect. Retry the request or wait for the reconnect cycle to stabilise. This does not occur under normal operation.

Backend returns unexpected identity headers Check header_mode on the backend. strict mints X-Auth-* headers from the caller’s OBO grant and strips any caller-supplied Authorization header. Use passthrough if your backend handles auth independently, or both to overlay minted headers on top of caller-supplied ones.