Egress

Egress is spotlight egress, a container a host starts beside each sandbox for one run and destroys with it. It is the sandbox's only way out. The sandbox is the security boundary gives the reasons.

Topology

PartValue
Sandbox networkspotlight-<runner id>, an internal network. The sandbox joins only this network.
Egress containerspotlight-egress-<runner id>, on the sandbox network with the alias egress, and on SPOTLIGHT_SANDBOX_NETWORK or the engine's default network.
Egress imageSPOTLIGHT_EGRESS_IMAGE, default ghcr.io/usespotlightai/spotlight-runner:<release>, run with entrypoint spotlight egress.
Port3128
Proxy addresshttp://sandbox:<credential>@egress:3128, in the sandbox's HTTP_PROXY and HTTPS_PROXY. The credential is random, for this run alone.
Extra hosts entriesSPOTLIGHT_SANDBOX_HOSTS

A client in the sandbox that ignores the proxy has no route out. See Inside a sandbox for the proxy and certificate variables.

On start, egress reads /run/spotlight/egress.json from the host and deletes it. It redeems the run's secrets from Spotlight, for up to 1 minute. It then listens, and writes its certificate authority for the host to lay into the sandbox. The host waits up to 90 seconds for it. On SIGTERM it stops within 5 seconds and clears the secret values it held.

Admitted hosts

SPOTLIGHT_EGRESS_ALLOWAdmitted
unset or emptyAny host.
a listEach exact host listed, and the subdomains of each *.<domain> entry. *.example.com admits api.example.com, not example.com.

Host names are compared without regard to case. The Spotlight address, and the storage hosts Spotlight names for the run, are always admitted. * alone is refused at host start.

Refused addresses

Egress checks the address it connects to, after resolving the host name.

AddressRefused
Loopback, link-local (including 169.254.169.254), multicast, unspecified, 0.0.0.0/8Always.
Private (RFC 1918, IPv6 unique local), 100.64.0.0/10, 64:ff9b::/96, 2002::/16Unless an allow-list entry names the host. An empty allow-list names no host.
Any otherNever.

TLS

PropertyValue
Certificate authorityOne per run, ECDSA P-256, made by egress. The key stays in egress's memory.
Lifetime48 hours
Name constraintsThe hosts egress adds something to requests to. With none, spotlight.invalid.
InterceptedCONNECT to port 443 of a host egress adds something to requests to. HTTP/1.1, TLS 1.2 or later.
Not interceptedEvery other CONNECT, tunnelled to the host named in it.
Upstream trustThe egress image's system certificates, plus SPOTLIGHT_EGRESS_TRUST.

A client that pins certificates fails against an intercepted host. Egress removes Proxy-Authorization from every request it passes on.

What egress adds to requests

On requests to the host a secret is bound to, egress replaces a request header or query parameter with the secret's value.

Harness keys

A host holds the harness keys in its settings and hands them to egress. The sandbox gets spotlight-egress:<NAME> in their place. Egress replaces the header only when it holds exactly that stand-in, with its prefix, as in authorization: Bearer spotlight-egress:OPENAI_API_KEY.

Egress logs one of these reasons and sends the request without the harness key:

  • the host's allow-list does not admit <host>
  • it is empty or more than one line

Run secrets

A secret an MCP server's environment names, as secret.<NAME>, is added for the run. The MCP server gets spotlight-egress:<NAME>. Egress replaces the bound header only on requests to the bound host whose header holds that stand-in, with or without the binding's prefix, as in authorization: Bearer spotlight-egress:LINEAR_API_KEY. The stand-in must be the whole value, or come right after the exact prefix. Other requests to the host go out without the secret.

For a secret bound to a query parameter, egress replaces the parameter's value only where it is the stand-in, or the prefix followed by the stand-in, as in ?key=spotlight-egress:MAPS_KEY. The other parameters keep their order and encoding. Egress never logs the full URL. It answers a plain-HTTP or intercepted request it could not pass on with only Egress couldn't reach <host:port>., without the reason. See spotlight_secret and Give an MCP server a secret.

Secret sourceHow egress gets the value
Kept by SpotlightSealed to a key egress makes for the run (X25519, HKDF-SHA256, AES-256-GCM, bound to the run and the secret).
AWS Secrets ManagerRead by egress with the host's AWS identity, from the default credential chain and SPOTLIGHT_EGRESS_ENV. Only a secret SPOTLIGHT_EGRESS_READS admits.

Egress logs one of these reasons and sends the request without the run secret:

  • the host holds a key of its own for <host>
  • the host's allow-list does not admit <host>
  • the host does not let egress read <secret id>
  • another secret is already worn in <Header> toward <host>, or in ?<parameter> toward <host> for a query parameter
  • it is worn in neither a header nor a query parameter
  • it names no header to be worn in, or it names no query to be worn in
  • it holds more than one line, so it cannot be worn in a request

On a request to a host egress holds a secret or a harness key for, a header or query parameter holding a stand-in it doesn't put a value in, such as a misspelt name, is removed before the request goes out, and egress logs took a stand-in egress does not hold off a request with the header's or parameter's name only.

Egress takes the secret back out of what the bound host answers before the sandbox sees it. In the Location, Content-Location, Link and Refresh headers, and in a text-like body (JSON, text/*, HTML, XML and form-encoded), every spelling of the value is swapped back to its stand-in: as written, query-escaped, path-escaped and escaped with lowercase hex, each with and without the binding's prefix. A binary body, or one the host compressed, is passed on as it is. Egress asks the bound host for an uncompressed answer. A response it rewrites loses its Content-Length and is sent chunked.

Reading a secret from AWS Secrets Manager fails with:

MessageCause
find the host's AWS identity: <error>No AWS identity.
AWS Secrets Manager answered <code>An AWS error.
AWS Secrets Manager could not be asked: <error>AWS not reached.
AWS Secrets Manager holds no text for itNo text value.
its value in AWS Secrets Manager is not JSON, so it has no <field> fieldfield set, value not JSON.
its value in AWS Secrets Manager holds no text at <field>field missing.

Egress logs each failure, and the request goes out without that secret.

The run's Spotlight token

A run of a setup chat reaches Spotlight's own API. When egress redeems the run, Spotlight mints a workspace token for that run's attempt alone and seals it to egress, like a secret it keeps.

PropertyValue
Scopesplan, which includes read.
Issued byThe person who started the chat. Recorded in the audit as minted by Spotlight, with the run and its attempt.
ExpiresAn hour after egress redeems the run.
RevokedWhen the run completes, is abandoned or withdrawn, when its attempt lapses or is parked, or when the runner loses the run while egress redeems it.
Stand-inspotlight-egress:spotlight-api, in SPOTLIGHT_API_TOKEN. See The agent's environment.
WornIn Authorization, in place of the stand-in or Bearer and the stand-in, on requests to the Spotlight address's host.

Egress wears it whatever the host's allow-list says, since the Spotlight address is always admitted. When the Spotlight address is plain HTTP, as in http://control-plane:3000 inside the host's network, egress also wears it over plain HTTP, on that address's port only. No other secret is worn over plain HTTP. Egress does not hold the token when the host holds a key of its own for the Spotlight address, or another secret is worn in Authorization toward it, and logs could not hold the run's Spotlight token with the reason.

The token is not listed among the workspace's tokens. A run that reaches Spotlight's API is never reserved for a runner a push pool provisions.

Responses

StatusMessageCause
407Egress answers only the sandbox it was started for.No proxy credential, or the wrong one.
400Egress is a proxy. Please ask it for an absolute URL, or CONNECT to a host.A request that is neither CONNECT nor an absolute URL.
403Egress doesn't let this sandbox reach <host>.The allow-list does not admit the host.
502Egress couldn't reach <host:port>. Error: Egress never opens a connection to <ip>.A tunnelled CONNECT to an always-refused address.
502Egress couldn't reach <host:port>. Error: Egress opens a connection to private address <ip> only if the allow-list names the host.A tunnelled CONNECT to a private address the allow-list does not name.
502Egress couldn't reach <host:port>. Error: <reason>Any other failure of a tunnelled CONNECT.
502Egress couldn't reach <host:port>.A plain-HTTP or intercepted request that failed for any of these causes. The reason and the URL are left out.
502Egress couldn't stand in for <host>. Error: <reason>No certificate could be made for an intercepted host.

Connections time out after 30 seconds, and TLS handshakes upstream after 15 seconds.

Egress start failures

These fail the run with The sandbox's egress didn't start. Error: <error>:

MessageCause
the control plane handed no credential for the sandbox's egressSpotlight handed no egress credential with the work.
egress was not ready within 1m30sEgress did not publish its authority in time.
pull the egress image <image>: <error>The egress image could not be pulled.

If Spotlight refuses to redeem the run's secrets, egress starts with none to add and logs the control plane gave egress nothing to wear. If it cannot reach Spotlight within 1 minute, it logs gave up redeeming the run's secrets; the sandbox goes out wearing none.

Hook calls

A hook that runs on a host is sent from a one-off egress container, spotlight-egress-call-<id>, under the same allow-list and address rules. Only http and https are sent, and redirects are not followed.

Limitations

  • Egress cannot itself send through another proxy.
  • A runner a push pool provisions has no egress. A run that uses secrets, or reaches Spotlight's API, is never reserved for one; it waits for a host.
  • CURSOR_API_KEY and any other variable not listed under Harness keys enter the sandbox as they are.
  • Logins that refresh their own tokens are not added.

On this page