Skip to content

Proposal: Assess replacing the laptop Go forward proxy with Praxis — blocked on three upstream capabilities #1437

Open
0 of 8 issues completed

Description

@huang195

Summary

This issue records an assessment of replacing the laptop Go forward proxy (cortex, run by agentop setup on 127.0.0.1:47600) with Praxis.

Conclusion: it is not possible today. Three capabilities the laptop depends on are missing from Praxis:

  1. HTTP CONNECT and forwarding to the destination the client named.
  2. HTTPS interception, meaning a TLS certificate minted per host from a local CA.
  3. Access, from a filter, to the client's TCP port.

Two of these have open Praxis issues. None has a merged design, and only one has an assignee. Once all three exist, the remaining work on the Cortex side is estimated at 35–52 engineer-weeks.

Scope is the laptop only. Kubernetes shapes (inbound reverse proxy, iptables capture, mTLS) are out of scope.

Assessed against Cortex main @ 2362f7eb, Praxis v0.7.4 (a50e58e5) and praxis-ai v0.7.0 on 2026-10-17. Prior issues: #504, #681 (closed 2026-10-05: on hold because Praxis has no forward or transparent proxy), #682.

What the laptop proxy does today

The laptop install runs only the forward proxy role. Per request it:

  1. Accepts plain HTTP_PROXY requests and HTTPS_PROXY CONNECT tunnels on one port.
  2. Decrypts a tunnel with a certificate it mints for that host from ~/.cortex/ca. Whether to decrypt is decided per program: port, TLS peek, passthrough hosts, and a skip memory for programs that refuse the CA.
  3. Attributes the request to a local program by looking up the owner of the TCP connection, then to a session from client headers, the program, or client affinity.
  4. Runs the outbound pipeline: inference-parser, mcp-parser, a2a-parser, tool-prune, and inference-router when agentop server has added a server. This includes:
    • body buffering;
    • three SSE relay paths;
    • redirects;
    • resending a refused request with a changed model.
  5. Records request, response, denied, failure and tunnel events. These feed the session store, the cost ledger, the usage aggregator, the session archive, and the APIs on :47601 and :47602.

Size, non-comment Go lines:

Component Lines
Forward proxy (core/listener/forwardproxy server and process) 1,414
TLS bridge (core/tlsbridge) 1,140
Process lookup (core/peerproc) 632

Generic forwarding is a small part of this. Most of it is interception, attribution and recording.

Findings

Verified by running Praxis v0.7.4 on macOS arm64:

Check Result
Native release build Succeeds: 2m43s, 21 MB binary
Health /healthy returns 200 on admin.metrics_address
curl --proxy http://127.0.0.1:<port> https://example.com/ 405 Method Not Allowed. The Pingora fork rejects CONNECT unless allow_connect_method_proxying is set, and Praxis does not set it
curl --proxy http://127.0.0.1:<port> http://example.com/ Forwarded to the configured cluster endpoint, not to example.com. There is no "connect where the client asked" routing

Read from source, not exercised:

Capability Praxis v0.7.4 Where
Certificate generation in the shipping binary None. rcgen is only a dev-dependency (crates/protocol) or behind the opt-in bench-utils feature (crates/tls) crates/{protocol,tls}/Cargo.toml
Client address available to filters IP only: client_addr: Option<IpAddr>, no port crates/filter/src/context.rs
Async response-body hook No. on_response_body is a plain fn. Tracked in praxis-proxy/praxis#1105, which waits on a published Pingora release (praxis-proxy/praxis#1353) crates/filter/src/filter.rs
General ext_proc client Removed from Praxis core in v0.4.1. The only one left, in praxis-ai, is scoped to llm-d and rejects every response_body_mode except none ai/integrations/llmd/ext-proc/src/lib.rs
Filter-registered admin endpoints No. Admin routes are hard-coded (/api/kv/*, /api/pipelines, /api/log-level, /api/stats) crates/protocol/src/http/pingora/health/service.rs
Retry with a modified request No. Retries resend the same request router retry_policy
macOS in upstream CI No workflow runs on a macOS runner .github/workflows/

Blockers

All three are changes to Praxis itself; none can be done as a Cortex plugin or filter.

# Blocker Laptop impact Upstream status (2026-10-17)
B1 CONNECT and destination-from-request forwarding No agent traffic can pass praxis-proxy/praxis#781: assigned to esnible, milestone v0.8.0 (due 2026-11-16; 150 open issues on it). Maintainer agreed scope: HTTP/1.1 only, a destination allowlist required. No proposal in praxis-proxy/enhancements and no PR
B2 HTTPS interception (CA, per-host minting, cert cache, decrypted traffic re-entering the HTTP pipeline) No parsing, cost, tool-prune or routing for HTTPS, which is all LLM traffic praxis-proxy/praxis#782: unassigned, no comments, milestone v0.8.0
B3 Connection identity in filters (client port; per-connection state shared by a tunnel and the requests decrypted from it) No program attribution, per-program decrypt decision, or self-traffic filter Filed 2026-10-24 as praxis-proxy/praxis#1447 (client port and local address to HTTP filters). Praxis already has the port and drops it before filters see it. Per-connection state for decrypted requests is left to #782's design

Not blockers for this design:

  • macOS: builds and runs.
  • The synchronous response-body hook: avoided if the Go pipeline runs in-process; see Option A.
  • Private and loopback upstreams: allowed by insecure_options.allow_private_endpoints, which logs a warning. (Corrected 2026-10-24: allow_private_upstreams is a different key, covering DNS-resolved names only.)

Migration tracker

Living table, updated as asks get owners. Each row is one Cortex capability. It moves from Cortex only to both running (Praxis's version runs beside Cortex's on real traffic and outputs are compared) to Praxis only (Cortex's copy is deleted one release later). A row moves on when Praxis passes the same saved test cases as Cortex, not on belief.

Status as of 2026-10-25. No Cortex–Praxis integration exists yet. The interim path (LLM traffic to praxis-ai as an inference server, or all traffic through Praxis via #1491) is not decided.

Cortex capability Praxis equivalent today Gap Upstream ask State
CONNECT (forward proxy) none no CONNECT support praxis-proxy/praxis#781 (esnible) Cortex only
Decryption (TLS interception) none no per-host certificate minting praxis-proxy/praxis#782 (no owner) Cortex only
Client port to filters has the port, drops it not passed to filters praxis-proxy/praxis#1447 Cortex only
Which agent / which session none stays in Cortex for good — Cortex only
Inference parser praxis-ai token_usage model and tokens only; no messages, tools or tool calls praxis-ai Discussion (drafted, not posted) Cortex only
MCP parser praxis-ai agentic/mcp method and tool name only; no arguments or results same Discussion Cortex only
A2A parser praxis-ai agentic/a2a method and task id only; no message content same Discussion Cortex only
opa PPE opa probably none, not verified — Cortex only; could move first
jwt-validation PPE identity_jwt probably none, not verified — Cortex only; could move first
static-inject Praxis header / credential_injection probably none, not verified — Cortex only; could move first
Pricing Tokenomics (proposal only) no prices in code praxis-proxy/enhancements#32 Cortex only
tool-prune, ibac, sparc, inference-router none need parsed content; ibac, sparc and the router also need session history — Cortex only
Session store, archive, agentop none not a proxy concern; stays in Cortex — Cortex only

Cortex-side prep, none of which depends on the path decision: #1497, #1498, #1499, #1500, #1501, #1502, #1503, #1504.

Responsibilities

Praxis maintainers (praxis-proxy/praxis)

Cortex team (rossoctl/cortex)

  • Give Praxis written requirements for B1. Plain and CONNECT requests on one port, HTTP/1.1 is sufficient, and tunnel open/close hooks that carry byte counts.
  • File B3 and the modified-request retry gap upstream, with the use cases.
  • Decide whether Cortex offers engineers to implement B2 upstream. Today nobody owns it, and it is the critical path for the laptop.
  • If B1–B3 land, everything in Work breakdown marked "Cortex".
  • Until a decision to migrate, keep shipping the Go proxy. No Cortex release depends on Praxis.

praxis-ai maintainers (praxis-proxy/ai)

  • No work is required for this migration.
  • Optional, independent of it: Cortex could route to a praxis-ai instance as an inference backend for API translation (Anthropic Messages ⇄ Chat Completions). That uses praxis-ai as it ships today.

Joint

Work breakdown (only once B1–B3 are available)

Design: Praxis owns the sockets, CONNECT and interception. The Go pipeline, session store and APIs are linked into the same binary as a C library, the reverse of how cortex-cpex links libcpex_ffi.a. agentop stays unchanged.

Work Code lives in Estimate
CONNECT and forwarding to the client's destination (B1) Praxis 4–6 wk
HTTPS interception (B2): CA create/renew, bundle.crt, minting, HTTP/1.1 and HTTP/2 on decrypted connections Praxis 8–12 wk
Connection identity for filters (B3) Praxis 1–2 wk
Decrypt decision: ports, TLS peek, passthrough hosts, per-program skip memory, unread report Cortex (Rust filter) 3–4 wk
Program lookup port (macOS and Linux kernel tables), self-traffic filter, Go-on-macOS trust prediction Cortex (Rust) 2–4 wk
Go pipeline bridge: request, response, per-frame and end-of-request hooks; denials, body edits, redirect, resend Cortex 6–8 wk
Event recording: request, response, denied, failure and tunnel rows; byte counts; session-ID headers; affinity Cortex 3–4 wk
Packaging: one Go+Rust binary, darwin and linux release builds with cgo, supervisor and 502 fallback, existing CLI flags and ports Cortex 4–6 wk
agentop adjustments (small if flags, ports and CA paths are kept) Cortex 1–2 wk
Black-box parity tests against real agents. The existing Go proxy tests exercise Go internals and do not carry over Cortex 3–4 wk
Total 35–52 engineer-weeks

Of the total, 13–20 weeks is code in Praxis's own repository. Its schedule depends on Praxis review.

These are estimates from reading code, not from a prototype. The largest unknown is in B2: whether Praxis's Pingora fork can feed a decrypted connection back into its HTTP handling.

Benefits and costs

What adoption would bring:

  • Proxy features maintained by others: WebSocket upgrade passthrough (Cortex strips Upgrade today), retries, circuit breakers, rate limiting.
  • Ecosystem alignment. The Praxis Policy Engine is the successor of CPEX, which cortex-cpex links today.
  • Rust's safety profile and a FIPS build option.

What it would not change:

  • Latency. A 2026-10-02 measurement put Cortex at about 1% of an LLM call's wall time, mostly parsing and recording, which would stay in Go.
  • The plugins, session store or agentop. These stay in Go under this design.

Costs:

  • The effort above.
  • Two languages and cgo in every release build.
  • Cross-repository review for proxy changes. The Go proxy, TLS bridge and process lookup had about 90 commits in the last three months, each shipped in a single PR.
  • Upstream churn: Praxis went from v0.5.3 to v0.7.4 between 2026-08-19 and 2026-10-12.

Recommendation

  1. Do not start a migration now. Revisit when Spike: HTTP CONNECT forward-proxy tunneling praxis-proxy/praxis#781 and TLS interception for L7 inspection of HTTPS tunnels praxis-proxy/praxis#782 are merged.
  2. Cortex, now:
  3. Optional, independent of this issue: evaluate praxis-ai as an inference-router backend for cross-API server switching.

Open questions

  • Is HTTPS interception (TLS interception for L7 inspection of HTTPS tunnels praxis-proxy/praxis#782) in Praxis's intended scope, and would Praxis accept an external implementation?
  • Would Praxis expose connection-level identity (client port, per-connection state) to HTTP filters?
  • Is a Go+Rust single binary acceptable for the laptop release, or must the release stay a static Go build?

References

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

Activity

  1. added
    laptopThe local (laptop) tool: install, abctl, the numbers
    on Oct 18, 2026
  2. araujof commented on Oct 18, 2026

    @araujof
    Contributor

    Thanks for this analysis! Issues #781 and #782 look addressable. I wouldn't put much weight on the 35–52 week estimate yet. I'd start with a small end-to-end spike through CONNECT, interception, a filter, and SSE, then estimate the rest from what we learn.

    There is also one smaller gap around process attribution. Praxis uses Pingora to handle HTTP connections, and Pingora already knows both ends of each client connection. Praxis currently gives filters only the client IP. Cortex needs the ports to find which local process opened the connection. Exposing those addresses should be a small change. We should also make sure requests inside an intercepted tunnel keep the same connection identity as the CONNECT request (could be included in #782 design).

    Before opening a Praxis issue for model-changing retries, I'd test whether its experimental iterative request router supports Cortex's resend flow. It can transition on response status and send a replacement body in the next request.

    One alternative is to build the Cortex-specific pieces around Praxis and PPE, rather than wrap the entire Go pipeline as a Praxis filter. PPE handles identity, policy, delegation, and redaction, and has a plugin system, so wrapping all of Cortex would duplicate that work. Native PPE plugins could also give reusable Cortex capabilities a path upstream, while a filter embedding the whole Go pipeline would be harder to upstream. This doesn't dispense 781 and 782. It's an alternative (perhaps longer term) to keep in mind for a deeper consolidation.

  3. huang195 commented on Oct 25, 2026

    @huang195
    MemberAuthor

    Prep work in Cortex that makes a later move to Praxis easier. There is no Cortex–Praxis integration yet, and the interim path is not decided (LLM traffic to praxis-ai as an inference server, or all traffic through Praxis via #1491). None of these depends on that choice, and each is useful to Cortex on its own:

    Suggested order: #1499's parser test data first (cheapest), then #1497 and #1498, then #1500.

  4. self-assigned this
    on Oct 27, 2026
  5. huang195 commented on Oct 30, 2026

    @huang195
    MemberAuthor

    Interim path decision (2026-10-30)

    Decision: the interim Cortex–Praxis integration is the hairpin (#1491), gated on its W1 spike. "LLM traffic to praxis-ai as an inference server" is not chosen as the interim step: it is a product feature (a pinned server, the shape agentop server already provides), not a migration step. It is covered inside the same spike as a composition test (see the #1491 comment).

    Rationale. The drivers for an interim step are migration progress and cross-team momentum, not immediate user value. Against those:

    Spike. W1 extended to one loop covering both designs: the hairpin loop with a pinned praxis-ai leg inside it (the pin's target becomes an x-cortex-dest, per #1491's pins-compose-by-chaining rule). Exit criteria: #1491's checklist checks 1, 3, 5, 6, 8, 9 plus praxis-ai translation fidelity, SSE shape, and token_usage/token_count buffering (check 10). ~2–3 engineer-days. Everything built is throwaway; real fixes and graduated pieces (e.g. W2) are extracted forward into their own small PRs against main — nothing merges from the spike branch.

    Exit from the dual state

    The hairpin makes the network path worse before better. The ladder out:

    Rung Path What changes
    0. Today agent → cortex → dest one process
    1. Hairpin agent → cortex → praxis → cortex-egress → dest +2 legs, +1 process
    2. #781 lands agent → cortex → praxis → dest egress listener + destination header deleted; hairpin plumbing collapses
    3. B1–B3 land agent → praxis → dest cortex Go proxy process deleted; Go pipeline, sessions, cost and APIs embedded in the praxis binary as a C library

    Three facts to keep clear-eyed about:

    1. Even at rung 3, Go code stays on the path. Attribution, parsing, recording and sessions cannot move to Praxis (tracker: "stays in Cortex for good"). What exits is the second hop and second process, not the Go code. "No Cortex in the data plane" was never the destination; "one process, Praxis-owned sockets" is.
    2. Every rung past the hairpin is upstream-gated, and as of today nothing has moved: build(deps): Update python-dotenv requirement from >=1.2.2 to >=1.2.3 #781 assigned (esnible, milestone v0.8.0) but no proposal filed and last activity 2026-08-22; build(deps): Bump the codeql-action group with 4 updates #782 unassigned, zero comments; build(deps): Bump github.com/open-policy-agent/opa from 1.21.1 to 1.21.2 in /core #1447 filed 2026-10-24, no movement. The one lever Cortex holds is whether to offer engineers for build(deps): Bump the codeql-action group with 4 updates #782 — that decision, not the hairpin, determines whether rung 3 ever happens.
    3. Deletion trails by a release, per capability, on evidence — no big-bang cutover. Even after B1–B3 merge, the dual state shrinks over another release cycle or two.

    New decision items

    Upstream statuses above verified 2026-10-30.

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

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