Imported from praxis-proxy/praxis (
AGENTS.md). Install upstream withnpx skills add praxis-proxy/praxis. Copyright stays with the author.
Agent Guidance
This file provides guidance to coding agents when working with code in this repository.
Tools may assist with implementation, but do not add any tool as a commit collaborator, co-author, or signatory. Commit sign-off belongs to the human contributor responsible for the change.
Requirements
- Rust stable 1.92+
- Rust nightly (for
rustfmt) - CMake 3.31+
- Docker 29.3.0+ or Podman (for container builds)
Quick Reference
Dev Utilities
cargo xtask echo # quick HTTP test server (static responses)
cargo xtask debug # run with dev settings (debug logs, single-threaded)
cargo xtask debug config.yaml # run example with dev settings
Build & Test
make setup-hooks # install git pre-commit hook (fmt + lint)
make build # workspace build (includes benches)
make test # tests outside tests/ (single pass, all features)
make fmt # format with nightly rustfmt
make lint # clippy + nightly fmt check + xtask lint-deps
make doc # rustdoc with -D warnings, including private items
make audit # cargo audit + cargo deny check
make coverage-check # fail if line coverage < 96%
make container # container image build
cargo run -p praxis-proxy # run the proxy
Targeted Testing
Prefer targeted test runs over full test suites. Run only the tests relevant to your changes unless doing a final verification before commit.
Run a single test:
cargo test -p praxis-tests-integration --test suite -- test_name
make test-integration V=1 # with --nocapture
Skip tests entirely for documentation-only changes (README, docs/*.md).
Individual test suites:
make test-unit # alias for make test (everything outside tests/)
make test-schema # config parsing + example validation
make test-integration # all tests/ suites: schema, security, resilience, integration
make test-conformance # RFC conformance (h2spec, HTTP semantics)
make test-security # request smuggling, header injection
make test-resilience # load, failure recovery, throughput
See docs/developing/getting-started.md for the full
command reference and dev tool usage.
Architecture
See docs/architecture/overview.md for the full design.
Crate dependency flow:
server -> protocol -> filter -> core -> tls
- server (
praxis): binary entry point, config loading, pipeline resolution, hot-reload watcher - core (
praxis-core): YAML config (serde), validation, error types, health state, KV store registry,PingoraServerRuntime - filter (
praxis-filter):HttpFilterandTcpFiltertraits, pipeline engine, condition evaluation, body access/buffering, all built-in filter implementations,FilterRegistry - protocol (
praxis-protocol):Protocoltrait, Pingora HTTP/TCP adapters, health check probes, admin endpoints - tls (
praxis-tls): TLS config types, SNI resolution (including wildcards), cert loading
Test crates (under tests/):
tests/utils: shared test harness (free_port,start_backend,start_proxy_with_registry)tests/schema: config parsing and example validationtests/integration: end-to-end filter and proxy teststests/conformance: RFC conformance (h2spec)tests/security: request smuggling, header injectiontests/resilience: load, failure recovery
Conventions
See docs/developing/conventions.md for the full
coding style guide, and
docs/developing/review-criteria.md for the criteria
used in deep analysis and audit passes over the
codebase. Praxis-specific coding conventions:
- Prefer
Option/Resultcombinator chains (strip_prefix(),filter(),map()) overif/elseblocks when the logic is a linear transform - Pre-computed numeric literals with trailing
comments for human-readable meaning
(e.g.
10_485_760; // 10 MiB) - Use enums, not strings, for fixed value sets
in config;
#[serde(deny_unknown_fields)]on config structs;#[serde(try_from)]for constrained numerics;#[serde(default)]instead ofOption<T>withunwrap_or. Seedocs/developing/type-design.md.
Test Requirements
New capabilities require:
- Unit tests
- Integration tests
- Example config in
examples/configs/ - Functional integration test for the example config
in
tests/integration/tests/suite/examples/ - Run
cargo xtask sync-example-readme --fixto regenerateexamples/README.md
Example configs must use this header comment format so the README generator can extract descriptions:
# Title
#
# One-line or multi-line description used in the
# README table (first sentence is taken).
#
# Usage:
# cargo run -p praxis-proxy -- -c examples/configs/...
Example config tests must exercise the actual functionality end-to-end (e.g. a WebSocket config must perform a real WebSocket handshake and message exchange). Parse-only validation is not sufficient; every example must prove its feature works with all configured variants.
Adding a Filter
See docs/filters/extensions.md for the full guide.
Choosing a category: HTTP filters go under observability,
payload_processing, security, traffic_management, or transformation.
TCP filters use observability or traffic_management. Review the category
README files in examples/configs/<category>/ to understand each category's
scope and choose the best fit.
- Create module under
crates/filter/src/builtins/<protocol>/<category>/ - Implement
HttpFilterorTcpFilterwith afrom_configfactory (fn(&serde_yaml::Value) -> Result<Box<dyn HttpFilter>, FilterError>) - Register in
crates/filter/src/registry.rs - Add unit tests and doctests
- Add example config in
examples/configs/<category>/(follow the header comment format below) - Add functional integration test in
tests/integration/tests/suite/examples/(must exercise actual functionality end-to-end) - Run
cargo xtask sync-example-readme --fix
Adding a Protocol
- Implement
Protocoltrait undercrates/protocol/src/ - Add variant to
ProtocolKindincrates/core/src/config/listener.rs - Wire in
crates/server/src/server.rs
Branch Chains
Conditional branching in filter pipelines based on filter results. Key files:
crates/core/src/config/branch_chain.rs: config typescrates/core/src/config/chain_ref.rs:ChainRefenumcrates/core/src/config/validate/branch_chain.rs: validationcrates/filter/src/results.rs:FilterResultSettypecrates/filter/src/pipeline/filter.rs:PipelineFiltercrates/filter/src/pipeline/branch.rs: runtime typescrates/filter/src/pipeline/build_branch.rs: resolutioncrates/filter/src/pipeline/evaluate.rs: execution
Filters write results to FilterResultSet without
knowing about branches. The pipeline executor reads
results to evaluate branch conditions and dispatch.
Branches rejoin at configurable points (next,
terminal, named filter, re-entrance with iteration
limits).
Terminology: Routing vs Pipelining
These two concepts are distinct, take care to not conflate them.
- Routing (runtime): the
routerfilter selects an upstream cluster at request time based on path, host, and headers. This decides where a request goes. - Pipelining (config-time): the operator composes
named filter chains per listener; chains are
resolved and concatenated into a single
FilterPipelineat startup. This decides what processing a request receives. Branch chains add conditional paths within a pipeline.
Reserved Headers
Client-unspoofable headers: Headers with x-praxis-* or x-ext-* prefixes
are reserved and automatically stripped from incoming requests. Use these for
metadata promoted from request bodies or set by trusted filters (like
json_body_field). Clients cannot forge these headers, making them safe for
security-sensitive routing and filtering decisions.
Example: json_body_field promotes a body field to x-praxis-guard-model,
which guardrails then uses in a condition. The client cannot bypass guardrails
by sending an x-praxis-guard-model header directly.
Key Patterns
- Classify → route → branch: classifier filters
promote facts to internal headers (
x-praxis-*) and the router matches those headers to select clusters (routing). Branch chains split pipelines (pipelining). - The inference path is proxy-parsed, not
classified (AI-specific): the
policyfilter attributes an OpenAI-style call to a model by reading the top-levelmodelout of the buffered body itself (no classifier, no header), then dispatches PPE'scmf.llm_input. Classifier metadata still wins where it exists. This pattern is specific to AI workloads; most filters should usejson_body_fieldto promote body fields to headers. - Branch on filter results: branch chains split
or rejoin request-phase pipelines based on filter
results (
on_result). Seeexamples/configs/pipeline/branch-chains.yaml. Branch sub-chains runon_requestandon_response;on_request_bodyandon_response_bodyare not executed for filters inside branch chains. Body-transforming filters must be in the main pipeline path or gated with normal filter conditions. - Prefer existing routing mechanisms: use classifier-promoted headers, router matches, filter conditions, and branch chains before adding new routing or capability mechanisms.
- Do not buffer full streaming responses:
streaming and SSE filters should use
BodyMode::Streamand process chunks incrementally unless the feature explicitly requires buffering. - Validate only proxy-needed fields: let the backend handle parameter ranges, model availability, and role ordering.
- Use dedicated rewrite filters for URL/path
translation: use
path_rewriteorurl_rewrite; provider and protocol filters should not setctx.rewritten_pathdirectly.
Filter Organization
Filters live under
crates/filter/src/builtins/<protocol>/<category>/.
See docs/filters/README.md for the filter system
documentation and docs/filters/reference.md
for built-in filter configurations.
Categories: observability, payload_processing,
security, traffic_management,
transformation (HTTP); observability,
traffic_management (TCP).
Example configs: examples/configs/<category>/.
Dynamic Config Reload
Praxis swaps filter pipelines at runtime without
restarting. Each handler holds
Arc<ArcSwap<FilterPipeline>>; a file watcher
(500ms debounce, hardcoded) monitors the config file, validates,
rebuilds pipelines, and swaps atomically. Listener
topology and TLS toggle changes cannot be applied
dynamically (logged as warnings); a protocol change
on a bound listener rejects the whole reload.
What reloads: Filter pipelines, filter configurations, routing rules. What does not reload: Listener addresses/ports, protocol types (HTTP/TCP), TLS on/off state, runtime worker count.
CI Workflows
CI workflows that post PR comments must use the
praxis-bot-app GitHub App token (via
actions/create-github-app-token), not the default
github.token.
Pingora Boundary
See docs/operating/security-hardening.md for details.
Pingora handles (upstream library, do not modify):
- Request smuggling prevention
- H2 backpressure
- Connection pool safety
- HTTP/1.1 upgrade detection and bidirectional forwarding (WebSocket, etc.)
Praxis handles (our code):
- Hop-by-hop header stripping (with conditional preservation for upgrade requests)
- Host validation
- X-Forwarded-* injection
- Retry logic
- Filter pipeline execution
- Routing and load balancing decisions
When adding features: If it's HTTP protocol compliance, connection management, or core request/response handling, check if Pingora already provides it before implementing in Praxis. If it's business logic, routing, filtering, or observability, implement in Praxis.
