Skip to content

Tracker: Move the laptop data plane to Praxis when the upstream gaps close #1714

Open
8 of 11 issues completed

Description

@huang195

Follows #1437 (assessment and prep, closed) · laptop only · stays open until the switch is complete

Summary

This issue tracks the move of the laptop data plane from Cortex's Go proxy to Praxis. It holds the end state, the phases and their triggers, the work (sub-issues), the upstream asks, and the decisions made on 2026-11-07. The design is #1717.

#1437 assessed the move, and its prep sub-issues (#1497–#1504) are closed. Its migration tracker moves here, updated on 2026-11-07.

End state

Cortex is one Rust binary built on the Praxis crates, in the shape of praxis-ai today. It has three layers:

  • Praxis core: CONNECT, decryption and the dial.
  • Cortex's own filters: attribution, parsing, recording and pricing.
  • Cortex's own store, and the session API served by filters.

There is no Go service. agentop stays a Go client and does not change: the session API contract (docs/session-api.md, the golden fixtures, schemaVersion) is the boundary at every phase.

Each capability row below has three states:

  • Go only: Cortex's Go code does it, as today.
  • Rust port passes parity: the Rust version in the binary gives the same results as the Go version on the same saved test cases.
  • Rust only: we delete the Go copy one release later.

A row moves only on test results, not on belief.

Rule until then: build only what survives the switch. Keep throwaway parts as thin as possible.

Migration tracker

Status as of 2026-11-07. Praxis v0.8.0 is the latest release (2026-11-07). praxis-ai v0.10.0 is built on Praxis 0.7.5. On 2026-11-09, praxis-ai main (de1d372) pins Praxis 0.8.0.

Cortex capability Praxis equivalent today Gap Upstream ask State
CONNECT (forward proxy) config only: forward_proxy block, experimental forward-proxy feature (praxis#1477, in v0.8.0) no runtime in a release praxis#1479 (CONNECT runtime, open), praxis#1480 (absolute-form forwarding with TLS and SNI to the origin, draft), praxis#1484 (tests, draft), praxis#781 Go only (the front door). Praxis core takes it in phase C (#1728)
Decryption (TLS interception) none no per-host certificate minting praxis#782: accepted in triage, owner esnible, milestone v0.8.0 Go only (the front door). Praxis core takes it in phase C (#1728)
Client port to filters has the port, drops it not passed to filters praxis#1447: no owner, not triaged Go front door has it. The binary needs it for phase C (#1728)
Which agent / which session none needs the client port to filters praxis#1447 (for phase C) Go only. Moves into the binary in phase D (#1727)
Inference parser praxis-ai ai_operation, anthropic_messages_request, token_count, time_to_first_token model, tokens and TTFT only. No messages, tools or tool calls. Does not count gzip-coded JSON responses (measured 2026-11-07). The code keeps no parsed body by design (see "Parser ownership" below) — (our own Rust port) Go only. Port in #1718. praxis-ai's token counts matched Cortex on real Claude Code SSE (#1491 W1 results)
MCP parser praxis-ai agentic/mcp method and tool name only. No arguments or results — (our own Rust port) Go only. Port in #1724
A2A parser praxis-ai agentic/a2a method and task id only. No message content — (our own Rust port) Go only. Port in #1724
opa PPE opa probably none, not verified — Go only. Off in praxis mode. Port or retire: open
jwt-validation PPE identity_jwt probably none, not verified — Go only. Off in praxis mode. Port or retire: open
static-inject Praxis header / credential_injection probably none, not verified — Go only. Off in praxis mode. Port or retire: open
Pricing Tokenomics proposal 00121 (merged as proposed, praxis-proxy/enhancements#35). external_metering sends usage to an external service no prices in code praxis-proxy/enhancements#38 (open review) Go only. Cortex prices at ingestion in praxis mode. Rust port in phase D (#1727), Tokenomics when on par (D2)
tool-prune, ibac, sparc, inference-router, script none need parsed content. ibac, sparc and the router also need session history — Go only. Off in praxis mode. Port or retire: open, one decision each
Session store, archive, session API none — — Go only. Moves into the binary in phase D (#1727). agentop stays a Go client
Event feed into Cortex our cortex_record filter in cortex-gateway — — Planned in #1719. In-process write in phase D

Upstream asks

# Ask State 2026-11-07 Next step
U1 CONNECT and absolute-form forwarding praxis#1479 open, praxis#1480 draft, no milestone Review them. Offer the #1491 test setup as real-agent traffic
U1a Laptop requirements on praxis#781 Not posted (a #1437 recommendation) Post them
U2 TLS interception praxis#782 accepted, esnible Decide whether to offer engineers (a #1437 recommendation, still open)
U3 Client port to filters praxis#1447 not triaged Ask for triage. Phase C depends on it
U4 Parsed content (messages, tools, results) shared with other filters Discussion drafted, not posted Not needed for this plan (our own Rust parsers). Optional
U5 Pricing Tokenomics proposed Take part in enhancements#38. We switch to Tokenomics when it is on par (D2)
U6 Decoded bodies for filters (gzip) Not filed Not needed for this plan (cortex_parse decodes for itself). Optional
U7 Per-request event export from Praxis Not needed: our own filter in cortex-gateway emits the rows —
U8 Bugs: the OTLP HTTP exporter panics in an otel build. praxis-ai refuses Claude Code's mid-conversation role: system message with output_config Not filed File them
U9 The modified-retry gap (a #1437 recommendation) Not filed. First test the iterative router for resend Test, then file
U10 emit_deferred_record runs only when the chain has access_log praxis#1228 open Comment with our case. Until then the chain includes access_log (#1719)
U11 A CA and verify option for forward-proxy origination #1480 uses the system roots only Ask on #1480. Phase B (#1726) needs it for upstream_ca_bundle users
U12 For #782 and #1447: a filter decides interception per CONNECT, a filter learns of a TLS handshake failure, inner requests carry the outer socket address Not asked Ask on #782 and #1447. Phase C (#1728) needs them
U13 Per-message hooks on upgraded connections (WebSockets) Not asked Ask. Until then Codex's Responses WebSocket is declined in praxis mode
U14 #1479: expose the tunnel's close reason and dial address to filters Not asked Ask on #1479. Phase C tunnel rows need them
U15 Another owner for the parsers (D6), for example praxis-ai Not asked Ask after the Rust parsers pass parity (#1718, #1724). Needs U4

Plan

The order is A → B → D → C. B, D and C each start only on an upstream trigger: no work on them starts before Praxis upstream is ready (decided 2026-11-10).

A. Now: praxis mode (no upstream dependency). Cortex runs with an empty pipeline. Our own Praxis binary (cortex-gateway: the published praxis-proxy crates plus Cortex filters in Rust) parses each request and records the rows. The rows go back to the Go Cortex through a loopback ingestion endpoint. agentop's observation views work as before: sessions, the timeline, tokens, cost, tool calls and titles. The features that come from plugins stop: agentop server, the script commands, tools scan --write, the plugin pane, savings, and policy denials. Off by default. Praxis mode stays behind --praxis until phase C leaves one network proxy (decided 2026-11-10, criterion C7 on #1725). Built behind the #1491 hairpin on a pinned Praxis v0.8.0. Design: #1717. Work: #1718 to #1725.

B. Trigger: praxis#1479 and praxis#1480 in a release. The hop sends absolute-form requests, and Praxis dials the origin. The egress listener and X-Cortex-Dest go away. Cortex still does CONNECT and decryption. Work: #1726.

D. Trigger: praxis#782 and praxis#1447 in a release (the trigger of C). D runs first, then C. Cortex moves into the binary. (Changed 2026-11-10: users are not asked to use --praxis, so D does not start while praxis mode is only an opt-in. D exists to prepare C, so it waits for C's upstream.) The Go store, archive, timeline, pricing, ledger, session API and attribution (about 13.7k lines) are ported into the binary. They become a Cortex store crate on SQLite, and Rust filters. Attribution moves together with the store, because the store's events feed its state. The Go front door passes the client's socket address in a token-protected header, and the binary does the process lookup itself. The Go tests become parity cases. agentop sees no change. Work: #1727.

C. Trigger: praxis#782 and praxis#1447 in a release, after D. Praxis core takes over CONNECT and decryption. The Go front door and the hop go away, and one binary is left. The client address comes from the connection (#1447). If upstream lands before D is done, C waits for D, so no attribution service is ever built. Work: #1728.

Work

Each row is a sub-issue of this issue. The design is #1717.

Issue Work Depends on
#1718 Praxis mode 1/8: Rust workspace, inference parser, parity gate —
#1719 Praxis mode 2/8: cortex-gateway (cortex_observe) and /v1/ingest #1718
#1720 Praxis mode 3/8: supervision as siblings #1719
#1721 Praxis mode 4/8: hop and egress listener #1719, #1720
#1722 Praxis mode 5/8: agentop and installer #1720, #1721
#1724 Praxis mode 6/8: MCP and A2A parsers #1718
#1723 Praxis mode 7/8: standalone release (static musl first). Ships only after #1724 #1719, #1724
#1725 Praxis mode 8/8: live check, decide the default #1718 to #1724
#1726 Phase B: absolute form, no egress listener praxis#1479 and #1480
#1727 Phase D: Cortex moves into the binary #1725, and praxis#782 and #1447 in a release
#1728 Phase C: Praxis core takes CONNECT and decryption praxis#782 and #1447, after #1727

Sizing: not estimated for this plan. The 35–52 engineer-weeks on #1437 were for a different design (the Go pipeline linked into Praxis as a C library).

Decisions

Decided 2026-11-07:

  • D0. Build the hairpin, for processing and recording in Praxis (praxis mode), not for new checks.
  • D1. The store, archive and ledger stay in the Go Cortex until phase D, then move into the binary. agentop does not change.
  • D2. Cortex prices at ingestion in praxis mode. A Rust port of the pricing logic follows in the binary. When Praxis Tokenomics is on par, we switch to it.
  • D3. Attribution is Rust in the binary from phase D, with its own process lookup. No attribution service.
  • D4. Rust filters in our own binary on the published praxis-proxy crates (Praxis's manual registration). Not the Go pipeline as a C library, and not the praxis-ai binary.
  • D5. No stop-loss date. The owner of Praxis version bumps is decided later.

Decided 2026-11-09:

  • D6. Cortex owns the Rust parsers (option 1 in "Parser ownership"). It is the only option that does not wait for upstream. The goal is that another owner takes the parsers later, for example praxis-ai. Until then, the parity gate keeps the Go and Rust copies in step.

Still open:

  • Which remaining plugins (inference-router, tool-prune, script and the rest) are ported or retired. Each one is its own decision. The list must also name lineage, session-budget, litellm-budget-track, token-broker, token-exchange and token-exchange-aiac. They are on main, and the design does not name them.
  • Measurable criteria for the soft gates (see "Gates" below).

Review findings (2026-11-09)

Parser ownership

Praxis does not have the parsers that Cortex needs. Read on praxis-ai main (de1d372):

  • Inference request (apis/src/anthropic/messages_request, openai/chat_completions/request, openai/responses/request): format, model, stream, max_tokens and has_tools only, plus the Responses ids, store and background. messages_request/mod.rs:168 says: "The parsed body is deliberately not retained." There is no typed message model. The translation filters work on serde_json::Value.
  • Inference response (filters/src/token_usage): input, output and total tokens, cache read and write, and reasoning tokens. No gzip or zstd handling.
  • MCP (filters/src/agentic/mcp): method, kind, tool name, session id and protocol version. A2A (filters/src/agentic/a2a): method, kind, family, streaming and routing.
  • That is about 10 of the about 35 fields of Cortex's InferenceExtension (core/model/extensions.go). Messages, tools, tool calls, tool results, thinking, agentRole, servedModel and the completion have no praxis-ai equivalent.
  • The crates praxis-ai-apis and praxis-ai-filters have publish = false.

So Praxis removes the proxy core from our maintenance (phase C), not the parsers. The options:

  1. Own the Rust parsers (the current plan). During phase A we maintain two copies, Go and Rust.
  2. Contribute them to praxis-ai. This needs U4, and it goes against praxis-ai's stated design of keeping no parsed body. The crates must also be published.
  3. A mix: use praxis-ai's token_usage after U6 (gzip), and own only the conversation parsing.

Decided 2026-11-09: option 1 (D6).

Gates

Performance (estimates, not measured)

What phase A throws away at phase D

Five of the eight phase A issues survive without change: #1718, #1720, #1722, #1723 and #1724. The throwaway work is in #1719 (/v1/ingest and the emitter transport) and #1721 (the Go-side joins). The Go front door stays until phase C on both paths.

Other

  • The design skips the outbound pipeline in praxis mode. It does not say what happens to the inbound pipeline (for example jwt-validation).

Moved here from #1497

The ingestion endpoint and the cost headers on the response row are now part of #1719.

References

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

Activity

  1. added
    laptopThe local (laptop) tool: install, abctl, the numbers
    on Nov 7, 2026
  2. self-assigned this
    on Nov 7, 2026
  3. huang195 commented on Nov 10, 2026

    @huang195
    MemberAuthor

    The criteria for the praxis default are now on #1725: #1725 (comment)

    That comment fills two gaps under "Gates":

    • Criteria for the default decision. C1-C7: feature parity, facts, latency, resources, streaming, fail closed and one network proxy. Praxis mode becomes the default only when all seven pass. Until C7 passes, praxis mode stays behind the --praxis flag.
    • Rollback criteria. Five conditions. When one occurs after a flip, set praxis.enabled: false and restart.

    We wrote the criteria before the live check. The result comes later on #1725.

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