Skip to content

Praxis mode 4/8: Add the hop and the egress listener to the forward proxy #1721

Closed

Description

@huang195

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, not pctx.Redirect. Exactly five branch points in serveOutbound: the praxis-mode entry strip, skip the request row (keep touch), count the request as ownEncoding (so the origin sees only gzip, 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), and X-Cortex-Egress-Error on a failed dial. It removes any X-Cortex-Egress-Error from 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.enabled with mtls, or without bind_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, br gives a parsed row and a decoded body. No internal hop header reaches the origin, and an application's own X-Cortex-Custom does.
  • A tokenless request to :47605 gets 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/sessions events, except timing within 50 ms and the gateway_* kinds.
  • The existing Go test suites pass unchanged.
  • A test runs the same requests through serveOutbound with 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, client X-Cortex-* headers reach the origin, ownEncoding is 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

#1719 and #1720.

Assisted-By: Claude (Anthropic AI) noreply@anthropic.com

Activity

  1. self-assigned this
    on Nov 7, 2026
  2. added
    laptopThe local (laptop) tool: install, abctl, the numbers
    on Nov 7, 2026
  3. added a commit that references this issue on Nov 9, 2026
  4. huang195 commented on Nov 9, 2026

    @huang195
    MemberAuthor

    Two items for this issue from #1719 (PR #1758):

    1. Keep X-Cortex-Dest and X-Cortex-Token again. Praxis mode 2/8: Build cortex-gateway and the Cortex ingestion endpoint #1719 asked cortex_attribution to 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_HEADERS in praxis/filters/src/record/attribution.rs). When the egress listener lands, keep these two for it, and remove them before any other upstream.
    2. Release the tunnel join of an erased line. fileIngest in core/sessionapi/ingest.go drops a line whose startedAt is before an erase of its session (the erased branch, 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

  5. added 2 commits that reference this issue on Nov 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

laptopThe local (laptop) tool: install, abctl, the numbers

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions