Changelog
User-facing changes in each release, newest first. Each release has a short summary; expand it for the itemised entries, where breaking changes come first. Each release's GitHub page lists every merged pull request.
Unreleased
The request trailers a service layer passes on are validated as a client's before they reach an origin, captures are written as JSON lines for line-oriented shippers, and upstream TLS no longer trusts the compromised eMudhra emSign hierarchy.
Details
Breaking
- The service layer protocol is
roxy.layer.v7, incompatible withroxy.layer.v6. A direction subscribedheadis a head alone each way, with no end message after it, and an observe stream is one way: the service sends no heads, bytes or ends on it, and sendsclosewhen it is done. A WASM layer subscribedheadpasses on that direction's head alone: bytes or trailers in the body it gives fail the exchange closed. A service subscribedheadno longer gets acontent-lengthon that direction's head, as a WASM layer never did (#592). - The flow log's
forward_aborted,answer_abortedandupstream_abortedreasons are one,layer_aborted. A cut that follows a fault or the client's loss is no longer a layer's abort, so a body cut on its way through a layer is the client's or the upstream's. A layer's answer is its own when it gives it before taking the response from upstream, whether or not that response had arrived (#593). - A raw backtick in an HTTP/1 request path is rejected with
invalid_pathinstead of being percent-encoded; HTTP/2 already refused it. In a query it is still percent-encoded to%60(#582). - Capture is written to
capture.jsonlwith one JSON object per line, in place ofcapture.rxcand its length-framed records: the head nested underhead, body bytes base64 underdata, and at most 48 KiB of body per line. A line-oriented shipper such as Vector or Fluent Bit can take the file as it is (#527).
Fixed
- Upstream TLS rejects any chain through the eight eMudhra emSign intermediates whose private keys are public (CVE-2026-111943), including their Sectigo cross-signs, and no longer trusts the emSign ECC Root CA - G3 they chain to. roxy 0.3.0 and earlier still trust them; an origin with a certificate under that root now fails verification (#577).
- The bundled roots follow Mozilla in retiring emSign Root CA - C1 and emSign ECC Root CA - C3 (#605).
- A service layer's request trailers are held to a client's rules on their way to an origin: refused without
http.allow_request_trailers, held tolimits.max_header_bytes, and never a framing, routing, authentication orcontent-*field. A section that breaks them is the layer'sinvalid_request: the exchange is refused503layer_error, or the error is logged if the origin has already answered, and the origin never sees the request complete. More thanlimits.max_headersfields is a service protocol violation (#581). - An HTTP/2 client's request trailer section is held to
limits.max_header_bytes, as an HTTP/1.1 client's chunked trailer section is; one over it is a stream reset,parse_errorreasonhead_too_large(#581). - HTTP/1.1 request trailers also meet the byte limit after normalisation, so a pass-through layer is not blamed for the client's oversized section (
431,parse_errorreasonhead_too_large). - A request held across the policy's
valid_until, by an addon or while its body is buffered for inspection, is denied_expiredwhen it reaches the rules, as a request sent after the instant is (#568). - Without
forwarded_for,client.ipis the canonical TCP peer address: an IPv4 client on a dual-stack[::]listener isa.b.c.d, not::ffff:a.b.c.d, in metric keys, the flow log and the addon principal. Rule comparisons already treated the two as one address.client.peer.ipin the flow log keeps the socket's form (#579). - JSON the node cannot parse, whether the
node.tokenrecord, an enrolment or renewal response, a lease or a flow upload's acknowledgement, is reported by the kind of fault and its line and column, never by quoting the input, which could put a token or a lease secret in the log (#532).
0.3.0 (2026-10-15)
Addons can now answer early and hold a request or response for approval, with the request and response lanes of an exchange living independent lives. This is a breaking change for addons: the service protocol is roxy.layer.v6 and the WASM interface is roxy:addon@0.3.0.
Details
Breaking
- The service layer protocol is
roxy.layer.v6. It is incompatible withroxy.layer.v5and the experimentalroxy.layer.v0, and roxy does not accept a v5 peer: both sides must handlestop,trailers, explicit ends in both directions and headless completion. The bundled Python helper implements the new contract (#558). - The WASM addon interface is
roxy:addon@0.3.0. Components built against0.2.xor the unversioned package must be rebuilt with the matching SDK and WIT (#558).
Added
- An addon can answer before forwarding, hold a request or an upstream response while it decides, and stream approved content into an answer it has already started. An approval addon can send
200 text/event-streamwith keepalive comments while it holds a request or a tool-call event, then approve, edit or deny it (#558). - The request and response lanes of an exchange have independent lifetimes: a layer can end or stop one without the other, and an upstream head that arrives after an early answer is still delivered to the addon (#558).
- Trailers pass through service layers and WASM host-side passthrough, so gRPC status trailers survive an addon stack. WASM byte-stream rewriting has no trailer API and the Python forwarding helpers do not send trailers (#558).
Changed
-
Upstream informational responses such as
103 Early Hintsare discarded, with or without addons, and an addon cannot publish an informational head (#558). -
A WebSocket upgrade reserves its relay resources before the
101reaches the client (#558). -
Publishing a second outgoing head on a lane is a layer failure and cuts an answer already in progress (#558).
-
A layer that fails while handling a core denial may produce a
503, but the flow record keeps the original deny rule and reason; the layer fault gets its own event (#558). -
The final flow record for an exchange waits for both lanes' bodies, their validation and every accepted host call to finish (#558).
-
The quickstart's sentinel sidecar streams a model response to the agent as it arrives, holding only each tool call's block while the sentinel judges it, instead of the whole response until the stream ends. A rejected call is replaced by its note in place, and
modifynow works on streamed responses (#563). -
A body that streams through a layer costs more CPU per byte than it did; the fixed cost of a layer is unchanged. Tracked in #561.
Security
- rustls 0.23.46 (RUSTSEC-2026-0346 to 0349) and wasmtime 49.0.3 (RUSTSEC-2026-0352 to 0364) (#557).
0.2.0 (2026-10-14)
Quarantine, a bearer-token node protocol, host services for service layers and X-Forwarded-For support, plus two security fixes (GHSA-4297-w88p-8xqv, GHSA-pwq9-p79h-pg35). Breaking for control planes, service layers (roxy.layer.v5), client.port users and the per-addon state store.
Details
Breaking
- Node protocol: a node's credential is a rotating bearer token instead of a client certificate. Enrolment returns
token,not_afterandrenew_after_seconds; later requests sendAuthorization: Bearer; the state dir holdsnode.tokenin place ofnode.crtandnode.key. A control plane built for mutual TLS no longer works (#528). - The service layer protocol is
roxy.layer.v5: theopenmessage carries the layer's grants andconfig, and a service may send host-service calls on its stream (#526). client.portis removed as a rule field and metric key. The flow log keeps the TCP peer asclient.peer: { ip, port }, soclient.portmoves toclient.peer.portin the log (#525).- The per-addon state store and the
addons[].stateblock are gone: a WASM layer'sstate-getandstate-putuse the store the rules read withstate[..], sized bylimits.max_state_entries. Astate-putwithout a TTL gets one hour, not six (#511). - Stricter YAML:
yes,no,onandoffare not booleans,<<merge keys and custom tags are errors, and a file holds one document (#482).
Added
- Quarantine: a rule's
quarantineaction or a listener'squarantine_on.parse_errorsthreshold puts the proxy into a durable state where every exchange is refused403 _quarantined. The trip is written to<state-dir>/quarantineand survives a restart; an operator removes the marker, or in node mode the control plane names the incident in the lease'squarantine.releaselist.roxy run --configaccepts--state-dir, required when the config uses quarantine (#505). - A layer of either kind can quarantine the proxy through the new
quarantinecapability:flow.quarantinefor WASM, aquarantinemessage for a service (#529). - Service layers get the host services WASM layers have:
log,record,add_tag,state_get,state_putandmetric_getmessages, each answered with aresult;capabilitiesandconfigare accepted on a service layer (#526). - An
httplistener takesforwarded_for: { trusted_hops: N }to recover the client IP fromX-Forwarded-For; the result isclient.ipeverywhere. A header with too few entries or a malformed trusted entry is refused400 forwarded_for_invalid. The setting is a config error on anhttp_proxylistener (#525). - Node protocol: the lease poll reports
seq_unreservedwhen the node could not persist its flow sequence counter (#502). - The docs site publishes each release under
/vX.Y/with a version menu (#522).
Changed
- The rule language calls a field admission when it is known at the forwarding decision and in-flight when it is re-checked as the exchange streams, in place of "head" and "watched";
roxy check,rule testand the compiler diagnostics use the new words (#524). - A tag an enforce layer adds is visible to
tag[..]in admission and in-flight rules and to thewhenof the layers below; the 64-tag / 4 KiB cap now counts the rules' tags too (#534). - The wait for a busy layer instance is bounded by the layer's
first_byte_timeout; past it the exchange fails closed withlayer_errorkindno_instance(#513). - The health and CA listener has its own connection cap, so a client filling
limits.max_connectionson the proxy port cannot starve/healthz(#503). roxy checkwarns about a metric keyed onhostorclient.portwithout awhere, with field-specific advice, and no longer warns forunique(..)fields (#497).- The WIT package is
roxy:addon@0.2.1, an additive bump that0.2.0components still load. The SDK'sflow::metric_getloses itskeyparameter (#542). - Node mode: a
401on/renewis retried with backoff rather than ending renewal;410or426at enrolment is reported as a refusal (#546). - A service observe stream ends on the service's last message rather than roxy's, with
first_byte_timeoutto send it, so a service observer can read a body and thenrecord. Host calls queued before a stream ends still run (#548). - A layer's own body that is still open splices the bypassed body as unknown length, so a held-back end or a failure is always a visible cut. A head-only WASM request layer that passes a stream to
nextnow sends a chunked request upstream (#543). - The node reserves flow sequence numbers off the emit path, so a slow disk no longer stalls every emitter (#485).
Fixed
- A WebSocket through a layer subscribed to
(request: full, response: head)no longer stalls (#516). - A service-layer head with too many header fields is refused at
limits.max_headersinstead of panicking (#499). - Client parse-error detail is redacted in the operational log as it is in the flow log (#498).
Security
- GHSA-4297-w88p-8xqv:
Host: [::ffff:1.2.3.4]bypassed a deny on1.2.3.4. An IPv4-mapped IPv6 literal now parses as the IPv4 address it embeds, and ahostortls.sniliteral written in the mapped form is a compile error (#506). - GHSA-pwq9-p79h-pg35: with
http.allow_obs_text: true, a header value holding a byte of0x80or above was invisible to an equality rule but forwarded. Rules now read header values as the bytes that are forwarded, one character per byte (#507). - YAML is read with
serde-saphyr; the unmaintainedunsafe-libyaml(RUSTSEC-2024-0449) is gone (#482).ruzstd0.9.1 for RUSTSEC-2026-0344 (#505).
0.1.1 (2026-10-12)
Heads cross the WASM boundary as records and bodies as bare streams (roxy:addon@0.2.0), layers subscribe to only the directions they need, and a batch of host hardening fixes. Breaking for WASM components and service layers (roxy.layer.v4).
Details
Breaking
- The WASM addon interface is
roxy:addon@0.2.0: a head crosses the guest boundary as one record per direction and bodies are barewasi:iostreams; thewasi:httpimport is gone. Components must be rebuilt against SDK 0.2.0, which is source-compatible (#424). - The service layer protocol is
roxy.layer.v4: thedenymessage is gone (a service refuses by answering with aresponse) andopencarries the layer's subscription (#420). - Removed
audit_endpoint, theauditflag offlow.record, endpointretriesand theattemptsfield of theendpoint_callevent (#420). - Removed the dead transparent-listener keys (
mode: transparent,allow_passthrough,upstream_target); they are now unknown-field errors (#457). - A
hostortls.sniliteral that can never match (trailing dot, non-ASCII, bracketed IPv6) is a compile error with a rewrite hint (#463).
Added
- A layer declares what it wants of each direction with
subscribe: { request: head | full, response: head | full }. A body a layer is not subscribed to bypasses it; bytes it passes on for that body fail the exchange closed (#420). SECURITY.md, pointing at private vulnerability reporting (#464).
Changed
first_byte_timeoutis one budget per head, request and response alike, for WASM layers as it already was for service layers (#431).- Service layer streams spread across up to
max_connectionsconnections instead of filling one, and writes are flushed in batches (#423). - The addon SDK moves bodies and heads a layer does not touch host-side, cutting a no-op layer's cost (#418).
- A fault after the response head carries its own reason into the
requestevent (buffer_budget_exhausted,upstream_body_failed,layer_erroror the client's parse reason) instead ofresponse_write_failed(#419). likepatterns are capped at 1 KiB and compiled with the same limits asmatches;?matches one character, as documented (#459).
Fixed
- A guest head with tens of thousands of header fields is refused at
limits.max_headersinstead of panicking the host (#458). - A guest that writes to stdout or stderr no longer fails every exchange through its layer (#469).
- A handler that returns without responding is treated like a trap: the instance is discarded and the exchange fails (#460).
- An endpoint call waits for its in-flight permit before its body leaves the guest (#465).
- A service connection whose write side has died fails its exchanges at once with
service:closedinstead of afterfirst_byte_timeout(#461). - Body frames on an ended service stream are dropped without crediting, so a misbehaving service cannot grow roxy's memory (#462).
response_body_idle_timeoutapplies while a response body is collected for inspection; a stalled upstream under a body rule gets a504(#467).- The reference says what the code does where it overclaimed: timeouts, drain and linger limits, the hop-by-hop list, node
410handling and lease fields (#457); HTTP/1.0 close-delimited responses are documented (#466).
0.1.0 (2026-10-08)
The first release: a strict, programmable HTTP firewall.
- Two ways to deploy it: as an egress proxy for a workload you don't trust (
mode: http_proxy, with TLS terminated by roxy's own CA), or as a gateway in front of the APIs your clients call (mode: http). - One canonical form: every request is parsed into one strict model and re-serialised, over HTTP/1.1 and HTTP/2, with WebSockets relayed. Anything roxy cannot parse, verify or classify is denied.
- A typed YAML rule language where deny always wins and nothing unlisted passes; rules can watch bodies and WebSocket messages as they stream. Metrics give rate limits and byte budgets; secret injection and AWS SigV4 signing mean clients hold only placeholders.
- Addons above the rules, as WASM components in process or external services streamed over WebSocket: they can rewrite, withhold, answer or block, and the rules judge whatever they pass on.
- A JSONL flow log naming the rule or addon behind every decision, with optional body capture and digests; backpressure holds traffic until the log catches up.
- Node mode: roxy enrols with a control plane, leases its policy and secrets into memory, and ships its flow log.