Repository navigation
Description
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-proxycrates (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-exchangeandtoken-exchange-aiac. They are onmain, 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_tokensandhas_toolsonly, plus the Responses ids,storeandbackground.messages_request/mod.rs:168says: "The parsed body is deliberately not retained." There is no typed message model. The translation filters work onserde_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,servedModeland the completion have no praxis-ai equivalent. - The crates
praxis-ai-apisandpraxis-ai-filtershavepublish = false.
So Praxis removes the proxy core from our maintenance (phase C), not the parsers. The options:
- Own the Rust parsers (the current plan). During phase A we maintain two copies, Go and Rust.
- 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.
- A mix: use praxis-ai's
token_usageafter U6 (gzip), and own only the conversation parsing.
Decided 2026-11-09: option 1 (D6).
Gates
- Inside phase A: the parity gate and the schema drift test are automatic. The live check (Praxis mode 8/8: Run the live check and decide the default #1725) is manual. The criteria for the default decision, C1-C7, are on Praxis mode 8/8: Run the live check and decide the default #1725 (comment). C7 (one network proxy) makes the default wait for phase C.
- A → B: praxis#1479 and [MOCA SBX] Fail closed on hosts that have a credential: never fall back to an opaque tunnel #1480 in a release, plus U11. No one owns the Praxis version bumps (D5), and the triggers for B and C need those bumps.
- → D: praxis#782 and build(deps): Bump github.com/open-policy-agent/opa from 1.21.1 to 1.21.2 in /core #1447 in a release, the same trigger as C. D runs first, then C. The first live run (2026-11-10, Praxis mode 8/8: Run the live check and decide the default #1725) passed C2-C6, apart from one wrong error kind (
gateway_refusedfor a client stop, fixed by fix: Record a client that leaves before the head as client_canceled #1777) and the Codex part of C5 (not run). D does not need B. - D → C: praxis#782 and build(deps): Bump github.com/open-policy-agent/opa from 1.21.1 to 1.21.2 in /core #1447 in a release, plus U12 and U14, and C waits for D.
- Rollback criteria: the five conditions on Praxis mode 8/8: Run the live check and decide the default #1725 (comment). When one occurs, set
praxis.enabled: falseand runagentop service install --restart.
Performance (estimates, not measured)
- A Claude Code request line on
/v1/ingestis about 7–12 MiB: the parsed conversation plus the base64 capture. Each turn sends the whole conversation again, with no interning on the wire. - The Go JSON decode is possibly 40–120 ms of CPU per late-session turn, off the latency path. The capture is held in both processes.
- The W1 result (about 0.4 ms p50 above direct mode) was measured in batches, not interleaved.
- Measure the CPU and heap of both processes, and interleaved latency, in Praxis mode 2/8: Build cortex-gateway and the Cortex ingestion endpoint #1719 and Praxis mode 8/8: Run the live check and decide the default #1725.
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
- Proposal: Assess replacing the laptop Go forward proxy with Praxis — blocked on three upstream capabilities #1437 — the assessment, blockers B1–B3, the work breakdown and the sizing
- Spec: Hairpin gateway — run the Praxis filter pipeline on all laptop traffic, with no upstream changes #1491 — the hairpin spec and its W1 results
- Docs: Design praxis mode, where Praxis processes and Cortex records #1717 — the design (praxis mode, and phases B, C and D)
- praxis-proxy/ai — the shape of the end state (one binary on the Praxis crates, with its own filters and store)
- Spike: HTTP CONNECT forward-proxy tunneling praxis-proxy/praxis#781, build(deps): Bump the codeql-action group with 4 updates #782, Docs: Add an Uninstall section to the README #1228, build(deps): Bump github.com/open-policy-agent/opa from 1.21.1 to 1.21.2 in /core #1447, [MOCA SBX] Per-VM Unix-socket listeners handed over at run time (SCM_RIGHTS), with a control channel #1477, [MOCA SBX] Inject into the header the credential declares (Bearer, Basic for git, x-api-key), always overwriting #1479, [MOCA SBX] Fail closed on hosts that have a credential: never fall back to an opaque tunnel #1480, [MOCA SBX] SSRF guard: resolve once, refuse private and metadata ranges, dial the checked address #1484
- Tokenomics: API + Capabilities praxis-proxy/enhancements#35 (Tokenomics 00121), Bump rojopolis/spellcheck-github-actions from 0.36.0 to 0.55.0 #38
Assisted-By: Claude (Anthropic AI) noreply@anthropic.com
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNew/ToDo
Activity
Docs: Design praxis mode, with Cortex recording and Praxis processing
Refactor: Remove the paused cortex-praxis converter
huang195 commentedon Nov 10, 2026
The criteria for the praxis default are now on #1725: #1725 (comment)
That comment fills two gaps under "Gates":
--praxisflag.praxis.enabled: falseand restart.We wrote the criteria before the live check. The result comes later on #1725.
docs: Add the plan for the praxis-mode live check (#1725)