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.
When to Use It
Section titled “When to Use It”- 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.
Architecture
Section titled “Architecture”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.
Surfaces
Section titled “Surfaces”Each surface is independently opt-in via enabled: true in the YAML config. Any combination may run in one process.
| Surface | Direction | What it does |
|---|---|---|
| terminator | Gateway → local backend | Receives ProxyHttpRequest and Tunnel* envelopes and forwards them to one or more configured local backends (HTTP / WS / TCP / UDP). |
| initiator | Local HTTP → gateway | Exposes a local HTTP listener. Legacy clients (curl, scripts) send HTTP to it; each request becomes a ProxyHttpRequest envelope addressed at a configured target topic. |
| relay | Sandbox gRPC → gateway | Binds 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. |
Quick Start
Section titled “Quick Start”1. Build
Section titled “1. Build”cd servergo build -o proxy-sidecar ./cmd/proxy-sidecar2. Write a minimal config
Section titled “2. Write a minimal config”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: info3. Run
Section titled “3. Run”./proxy-sidecar -config proxy-sidecar.yamlAgents can now send requests to sv::my-service::default. The sidecar forwards them to http://localhost:8080.
Supervisor mode
Section titled “Supervisor mode”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.
./proxy-sidecar -config proxy-sidecar.yaml -- ./my-service --port 8080Send SIGHUP to the sidecar at any time to reload the config file without restarting:
kill -HUP $(pidof proxy-sidecar)Reloadable: terminator backends, gateway credentials, logging level.
Not reloadable: surface enable/disable (terminator.enabled, relay.enabled, initiator.enabled).
Configuration Reference
Section titled “Configuration Reference”All YAML fields are also overridable via environment variables. See Environment Variables for the full AETHER_* reference.
Top-level
Section titled “Top-level”| YAML key | Type | Default | Description |
|---|---|---|---|
gateway.address | string | — | Gateway gRPC address (host:port). Required. |
gateway.insecure | bool | false | Disable TLS (development only). |
gateway.api_key | string | — | Inline API key. Mutually exclusive with task_token. |
gateway.api_key_path | string | — | Path to a file containing the API key. |
gateway.task_token | string | — | Per-task token issued by CreateTask. Mutually exclusive with api_key. |
gateway.task_token_path | string | — | Path to a file containing the task token. |
gateway.tls.cert_file | string | — | Client TLS certificate. |
gateway.tls.key_file | string | — | Client TLS key. |
gateway.tls.ca_file | string | — | CA bundle for server verification. |
service.implementation | string | — | Implementation segment of sv::{impl}::{spec}. Required when terminator or relay is enabled. |
service.specifier | string | — | Specifier segment of sv::{impl}::{spec}. Required when terminator or relay is enabled. |
tenant_id | string | — | Tenant identifier injected as X-Auth-Tenant-ID when header_mode is strict or both. |
logging.level | string | info | Log level: debug, info, warn, error. |
logging.format | string | auto | console (human-readable) or json. Auto-detects TTY. |
Terminator surface
Section titled “Terminator surface”| YAML key | Type | Default | Description |
|---|---|---|---|
terminator.enabled | bool | false | Enable the terminator surface. |
terminator.backends | list | — | One or more backend definitions (see below). |
Backend fields (terminator.backends[]):
| Key | Type | Default | Description |
|---|---|---|---|
name | string | backend-N | Logical name. Callers may set backend_name on a request to select a specific backend directly; otherwise first-ACL-match wins. |
kind | string | http | Backend protocol: http, ws, tcp, udp. |
url | string | — | 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 | []string | all | HTTP methods allowed. |
max_body_bytes | int | 10485760 | Maximum request body size in bytes (HTTP only). |
idle_timeout_ms | int | 30000 | Idle connection timeout in milliseconds. |
header_mode | string | strict | Identity 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_bytes | int | 104857600 | Maximum bytes per TCP/WS tunnel session. |
max_datagram_bytes | int | 1400 | Maximum bytes per UDP datagram. |
Initiator surface
Section titled “Initiator surface”| YAML key | Type | Default | Description |
|---|---|---|---|
initiator.enabled | bool | false | Enable the initiator surface. |
initiator.listen.bind | string | localhost:8888 | Local HTTP listener address. |
initiator.target.topic | string | — | Target service topic (e.g. sv::memorylayer::default). Required. |
Relay surface
Section titled “Relay surface”| YAML key | Type | Default | Description |
|---|---|---|---|
relay.enabled | bool | false | Enable the relay surface. |
relay.listen | string | — | Bind address for the local AetherGateway server. UDS (unix:///run/aether.sock) preferred; TCP (host:port) supported. Required. |
relay.identity_override | string | enforce | How to handle the sandbox-claimed identity. Only enforce is supported (sandbox claim is discarded; sidecar identity is used). |
relay.allowed_ops | profile or list | sandbox-default | Operations 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.mode | string | reject | How 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. |
Environment variable overrides
Section titled “Environment variable overrides”These override the corresponding YAML values at startup:
| Variable | YAML equivalent | Description |
|---|---|---|
AETHER_ADDRESS | gateway.address | Gateway gRPC address. |
AETHER_API_KEY | gateway.api_key | Inline API key. |
AETHER_TASK_TOKEN | gateway.task_token | Per-task token. |
AETHER_TENANT_ID | tenant_id | Tenant identifier. |
AETHER_LOG_LEVEL | logging.level | Log level. |
PROXY_SIDECAR_LISTEN | initiator.listen.bind | Initiator HTTP listener address. |
PROXY_SIDECAR_TARGET | initiator.target.topic | Initiator target topic. |
PROXY_SIDECAR_RELAY_LISTEN | relay.listen | Relay listen address. |
See Environment Variables for the complete AETHER_* reference shared across all Aether binaries.
Calling a Service from an Agent
Section titled “Calling a Service from an Agent”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.
Performance
Section titled “Performance”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.
| Scenario | p50 | p99 | Notes |
|---|---|---|---|
| Tunnel open (wildcard resolution + pin write) | 20 ms | 38 ms | In-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.
Operational Notes
Section titled “Operational Notes”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.pemCredential options
Section titled “Credential options”- API key (
api_key/api_key_path): long-lived service key. Preferapi_key_pathso the key is not visible in process listings. - Task token (
task_token/task_token_path): short-lived token issued by the gateway viaCreateTaskwith atarget_identityofsv::{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.
Live reload
Section titled “Live reload”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.
Observability
Section titled “Observability”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.
Troubleshooting
Section titled “Troubleshooting”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.
See Also
Section titled “See Also”- Identity Model — Service principal and topic schema
- Architecture — gateway routing overview
- Auth Proxy — HTTP reverse proxy surface for the gateway itself
- Environment Variables — full
AETHER_*andPROXY_SIDECAR_*reference