Repository navigation
Closed
Labels
laptopThe local (laptop) tool: install, abctl, the numbersThe local (laptop) tool: install, abctl, the numbers
Description
Part of #1714 · phase A, delivery step 4 · Sections 1 (Joins), 4 (The hop, The egress listener, Fail closed, Config) · design in #1717 (docs/superpowers/specs/2026-11-07-praxis-mode-design.md)
Goal
Send traffic through the gateway in praxis.enabled mode, with no change to direct mode.
Scope
core/listener/forwardproxy/hop.go: a dial-only rewrite at the dial, over HTTP/1, notpctx.Redirect. Exactly five branch points inserveOutbound: the praxis-mode entry strip, skip the request row (keeptouch), count the request asownEncoding(so the origin sees onlygzip, as in direct mode), swap the dial, skip the response row.- In praxis mode only, remove at entry the hop headers a client sends (
X-Cortex-Attribution,-Dest,-Token,-Request-Id,-Probe). Direct mode forwards them as today. - The egress listener on
:47606: token check,X-Cortex-Dest, a dial with the TLS bridge's upstream options (egress_guard, CA bundle,upstream_insecure), andX-Cortex-Egress-Erroron a failed dial. It removes anyX-Cortex-Egress-Errorfrom an origin's response. - Joins by request id: tunnel pairing (the first ingested request row of a bridged tunnel is written with its open row) and Cortex's own cuts (
proxy_error). - The startup probe, and
gateway_unavailable(503) when the dial to the gateway fails. - Config: with
praxis.enabled, Cortex does not build the outbound pipeline, at startup and at reload, and logs the plugins it skips. The config keeps them.praxis.enabledwithmtls, or withoutbind_loopback_only, is refused at startup. The Codex Responses WebSocket is declined. - The tunnel hold: each hopped request counts as "awaiting its row", and the tunnel settles only after the row joins or its 60 s join entry expires.
Known edge cases to handle or accept (from the PR #1717 review)
- A tunnel whose row never joins settles when its 60 s join entry expires, with a close row that says it carried nothing. This can come from a dropped line or from an ingestion stall over 60 s.
Done when
- The W1 checks pass as end-to-end tests:
- host and cost match direct mode.
- gzip SSE arrives as frames.
- a hang-up gives
client_canceled. - fail closed at startup.
- a 65 s silence mid-stream.
- more than 10 MiB each way.
- Codex's mislabeled SSE streams frame by frame through the gateway on a 2xx, and a 405 is parsed as JSON.
- A client's
Accept-Encoding: gzip, deflate, brgives a parsed row and a decoded body. No internal hop header reaches the origin, and an application's ownX-Cortex-Customdoes. - A tokenless request to
:47605gets 403 and leaves no row. - A tunnel that closes before its row arrives gets one pair and no "carried nothing" close row.
- Direct mode against praxis mode: the same
/v1/sessionsevents, except timing within 50 ms and thegateway_*kinds. - The existing Go test suites pass unchanged.
- A test runs the same requests through
serveOutboundwith praxis mode off and checks each of the five branch points: the request row and its capture are written, the response row and its capture are written, clientX-Cortex-*headers reach the origin,ownEncodingis as before, and the dial goes to the origin, not to the gateway.
Prototype
The #1491 W1 spike has a working hop and egress listener behind env flags: huang195:spike/hairpin-w1 (5ee3950b), with results in the W1 comment on #1491. Use it as a reference, not as code to merge. The design changed since then (for example, X-Cortex-Egress-Error, the five branch points, and the joins).
Depends on
Assisted-By: Claude (Anthropic AI) noreply@anthropic.com
Metadata
Metadata
Assignees
Labels
laptopThe local (laptop) tool: install, abctl, the numbersThe local (laptop) tool: install, abctl, the numbers
Type
Projects
- StatusShow more project fieldsDone
Activity
Docs: Name rossoctl#1721 for the hop, and record the accepted edge cases
huang195 commentedon Nov 9, 2026
Two items for this issue from #1719 (PR #1758):
X-Cortex-DestandX-Cortex-Tokenagain. Praxis mode 2/8: Build cortex-gateway and the Cortex ingestion endpoint #1719 askedcortex_attributionto keep them for the egress listener. With no egress listener yet, that sent the gateway's token to the origin, so PR Feat: Build cortex-gateway and the Cortex ingestion endpoint #1758 removes all hop headers before the upstream (HOP_HEADERSinpraxis/filters/src/record/attribution.rs). When the egress listener lands, keep these two for it, and remove them before any other upstream.fileIngestincore/sessionapi/ingest.godrops a line whosestartedAtis before an erase of its session (theerasedbranch, comment#1721 releases the line's tunnel join here). When the tunnel joins land, release the line's join at that point.Assisted-By: Claude (Anthropic AI) noreply@anthropic.com
Fix: Remove X-Cortex-Token and X-Cortex-Dest before the upstream
Docs: Name rossoctl#1721 for the hop, and record the accepted edge cases
fix: Log the skipped outbound plugins in praxis mode at Warn
fix: Keep the status the client got on a row that Cortex cut in praxi…
docs: Say that a cut row keeps the client's status in praxis mode
docs: Add the plan for praxis mode 4/8, the hop and the egress listener
feat: Keep X-Cortex-Dest and X-Cortex-Token for the egress listener
feat: Add the praxis-mode egress listener
feat: Hop praxis-mode requests to cortex-gateway at the dial
feat: Join ingested rows to Cortex's tunnels and cuts
feat: Hold a bridged tunnel until its gateway row joins