# ------------------------------------------------------------------- # Configuration # ------------------------------------------------------------------- # sed rather than perl: the UBI 9 toolchain image and a minimal RHEL runner # have no perl, and the FIPS host targets run make inside both. VERSION ?= $(shell sed -n 's/^version[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' Cargo.toml | head -n 1) IMAGE ?= praxis-ai CONTAINER_ENGINE ?= $(shell command -v podman 2>/dev/null || command -v docker 2>/dev/null) SENTINEL_PRODUCT ?= valkey SENTINEL_ENV_FILE ?= target/sentinel.env SENTINEL_DEAD_ENDPOINT ?= SENTINEL_DEAD_ENDPOINT_ENV := $(if $(strip $(SENTINEL_DEAD_ENDPOINT)),TOKEN_RATE_LIMIT_SENTINEL_DEAD_ENDPOINT='$(SENTINEL_DEAD_ENDPOINT)',-u TOKEN_RATE_LIMIT_SENTINEL_DEAD_ENDPOINT) SENTINEL_FAILOVER_BEFORE_COMMAND ?= SENTINEL_REPLICATION_HEALTH_TEST ?= SENTINEL_ON_FAILURE ?= SENTINEL_TEST_FILTER ?= sentinel_ OPENAI_CONFORMANCE_ARGS ?= RESPONSES_CONFORMANCE_ARGS ?= V ?= # CI uses this stable pin for Clippy and refreshes floating nightly for rustfmt. LINT_RUST_VERSION := $(shell sed -n 's/^channel[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' rust-toolchain.toml) # Cargo also finds rustc/clippy/rustfmt through PATH; select their bin directory # so a Homebrew installation cannot override the selected toolchain's children. LINT_CARGO = PATH="$$(dirname "$$(rustup which --toolchain $(LINT_RUST_VERSION) cargo)"):$${PATH}" rustup run $(LINT_RUST_VERSION) cargo LINT_FMT_CARGO = PATH="$$(dirname "$$(rustup which --toolchain nightly cargo)"):$${PATH}" rustup run nightly cargo # Experimental filter features are package-specific and off by default. # Basic Auth is exposed by praxis-ai-proxy and forwarded by the integration-test # crate; it is not a praxis-ai-filters feature. FILTER_EXPERIMENTAL_FEATURES := azure-ad-filter,gcp-adc-filter,http-callout-filter,token-rate-limit-filter,token-ceiling-filter,inflight-tracker-filter,vertex-anthropic-filter INTEGRATION_EXPERIMENTAL_FEATURES := azure-ad-filter,basic-auth-filter,gcp-adc-filter,http-callout-filter,token-rate-limit-filter,token-ceiling-filter,inflight-tracker-filter,vertex-anthropic-filter # Experimental praxis-ai-apis features, whose unit tests live in that crate. APIS_EXPERIMENTAL_FEATURES := vertex-anthropic # Release and container builds exclude experimental Responses and agents. # Opt in with PRAXIS_AI_FEATURES=full,openai-all,store-sqlite when needed. PRAXIS_AI_FEATURES ?= full # Crates that must never enter the explicit lean (`standard`) proxy graph. # openssl-sys is not on the list: praxis performs all cryptography through the # system OpenSSL, so its bindings are part of every build by design. LEAN_GRAPH_DENY := sqlx sqlx-core libsqlite3-sys native-tls rmcp sse-stream \ jsonschema utoipa tiktoken-rs reqwest serde_json_path tonic prost # Upper bound on crates (name@version, normal + build edges, host target) in the # explicit lean graph. Linux hosts measure about 428, macOS about 432. LEAN_GRAPH_BUDGET ?= 434 STORE_BACKEND_FREE_FEATURES := standard,openai-all STORE_POSTGRES_FEATURES := standard,openai-all,store-postgres STORE_POSTGRES_CERT_AUTH_FEATURES := openai-responses,store-postgres-cert-auth STORE_SQLITE_FEATURES := standard,openai-all,store-sqlite STORE_COMBINED_FEATURES := standard,openai-all,store-all STORE_ALL_WORKSPACE_FEATURES := praxis-ai-benchmarks/store-all,praxis-ai-proxy/openai-all,praxis-tests-integration/openai-all,praxis-tests-schema/openai-all,praxis-ai-proxy/store-all,praxis-tests-integration/store-all,praxis-tests-schema/store-all,praxis-tests-environment/store-all ifneq ($(V),) _NOCAPTURE := -- --nocapture endif .PHONY: all build release check clean \ test test-unit test-unit-apis test-unit-filters test-unit-proxy \ test-schema test-integration test-integration-core test-integration-experimental \ test-inference-fixtures test-inference-fixtures-core test-inference-xtask bench-model-to-provider \ test-store-features test-store-features-check test-store-features-clippy \ test-store-features-clippy-group test-store-features-test print-store-clippy-groups \ test-responses-features \ test-postgres-unit test-postgres-integration test-environment test-environment-suite \ test-token-rate-limit-valkey-unit test-token-rate-limit-valkey-integration \ test-token-rate-limit-sentinel-integration \ openai-conformance check-openai-conformance-reference test-openai-conformance \ test-responses-conformance \ lint lint-clippy lint-xtask lint-lean check-dep-budget fmt doc audit coverage-check \ setup-lint-toolchains \ require-container-engine \ container container-run \ setup-hooks help \ patch-praxis unpatch-praxis \ require-podman require-go require-oc \ build-fips release-fips check-fips lint-fips test-fips test-fips-provider \ test-integration-fips test-schema-fips test-fips-host test-postgres-fips-host fips-toolchain fips-host-facts \ fips-host-check fips-runtime-probe fips-image-save fips-image-load fips-image-tag fips-version \ container-fips container-fips-run \ fips-check fips-check-ubi fips-deps fips-report fips-signature-store fips-verify-image \ fips-image-ref fips-oc fips-scan fips-scanner fips-smoke # ------------------------------------------------------------------- # All # ------------------------------------------------------------------- all: build fmt lint test audit # ------------------------------------------------------------------- # Build # ------------------------------------------------------------------- build: cargo build --workspace release: cargo build --release -p praxis-ai-proxy --no-default-features --features $(PRAXIS_AI_FEATURES) check: cargo check --workspace clean: cargo clean # ------------------------------------------------------------------- # Container # ------------------------------------------------------------------- require-container-engine: ifndef CONTAINER_ENGINE $(error No container engine found — install podman or docker) endif container: | require-container-engine $(CONTAINER_ENGINE) build -t $(IMAGE):$(VERSION) -f Containerfile . container-run: | require-container-engine $(CONTAINER_ENGINE) run --rm --network=host $(IMAGE):$(VERSION) 2>&1 # ------------------------------------------------------------------- # Test # ------------------------------------------------------------------- test: cargo build -p praxis-ai-proxy --bin praxis-ai --features openai-all,store-all PRAXIS_AI_BIN=$(abspath $(or $(CARGO_TARGET_DIR),target)/debug/praxis-ai) \ cargo test --workspace --features $(STORE_ALL_WORKSPACE_FEATURES) $(_NOCAPTURE) # `make test-unit` runs every crate's permutations serially for local use; CI # splits these into the test-unit-{apis,filters,proxy} targets so the three # crates' feature permutations compile in parallel jobs instead of one serial # recompile chain (which was the ~38m long pole of the Tests workflow). test-unit: test-unit-apis test-unit-filters test-unit-proxy test-unit-apis: cargo test -p praxis-ai-apis $(_NOCAPTURE) cargo test -p praxis-ai-apis --features full,openai-all,store-postgres $(_NOCAPTURE) cargo test -p praxis-ai-apis --features full,openai-all,store-postgres,$(APIS_EXPERIMENTAL_FEATURES) $(_NOCAPTURE) cargo test -p praxis-ai-build-support $(_NOCAPTURE) cargo test -p praxis-ai-store -p praxis-ai-store-backends --no-default-features \ --features praxis-ai-store-backends/sqlite $(_NOCAPTURE) test-unit-filters: cargo test -p praxis-ai-filters $(_NOCAPTURE) cargo test -p praxis-ai-filters --features full,openai-all,store-postgres $(_NOCAPTURE) cargo test -p praxis-ai-filters --features full,openai-all,store-postgres,$(FILTER_EXPERIMENTAL_FEATURES) $(_NOCAPTURE) bench-model-to-provider: cargo test --locked --release -p praxis-ai-filters --lib \ inference::model_to_provider::tests::mapping_latency_microbenchmark \ -- --ignored --exact --nocapture test-unit-proxy: cargo test -p praxis-ai-proxy $(_NOCAPTURE) cargo test -p praxis-ai-proxy --features full,openai-all,store-postgres $(_NOCAPTURE) cargo test -p praxis-ai-proxy --features full,openai-all,store-postgres,basic-auth-filter $(_NOCAPTURE) # Store backend feature-graph guard, split so CI can run check / clippy / test # as parallel jobs instead of serializing ~18 compiles in one runner. Running # `make test-store-features` locally still does all three in order. test-store-features: test-store-features-check test-store-features-clippy test-store-features-test test-store-features-check: cargo check -p praxis-ai-proxy cargo check -p praxis-ai-proxy --no-default-features --features $(STORE_BACKEND_FREE_FEATURES) cargo check -p praxis-ai-proxy --no-default-features --features $(STORE_POSTGRES_FEATURES) cargo check -p praxis-ai-proxy --no-default-features --features $(STORE_POSTGRES_CERT_AUTH_FEATURES) cargo check -p praxis-ai-proxy --no-default-features --features $(STORE_SQLITE_FEATURES) cargo check -p praxis-ai-proxy --no-default-features --features $(STORE_COMBINED_FEATURES) @set -eu; \ assert_has() { printf '%s\n' "$$1" | grep -q "^$$2 v" || { echo "ERROR: $$3 graph is missing $$2"; exit 1; }; }; \ assert_lacks() { if printf '%s\n' "$$1" | grep -q "^$$2 v"; then echo "ERROR: $$3 graph contains $$2"; exit 1; fi; }; \ backend_free="$$(cargo tree -p praxis-ai-proxy --no-default-features --features $(STORE_BACKEND_FREE_FEATURES) --edges normal --prefix none --format '{p}')"; \ postgres="$$(cargo tree -p praxis-ai-proxy --no-default-features --features $(STORE_POSTGRES_FEATURES) --edges normal --prefix none --format '{p}')"; \ postgres_cert_auth="$$(cargo tree -p praxis-ai-proxy --no-default-features --features $(STORE_POSTGRES_CERT_AUTH_FEATURES) --edges normal --prefix none --format '{p}')"; \ sqlite="$$(cargo tree -p praxis-ai-proxy --no-default-features --features $(STORE_SQLITE_FEATURES) --edges normal --prefix none --format '{p}')"; \ combined="$$(cargo tree -p praxis-ai-proxy --no-default-features --features $(STORE_COMBINED_FEATURES) --edges normal --prefix none --format '{p}')"; \ default="$$(cargo tree -p praxis-ai-proxy --edges normal --prefix none --format '{p}')"; \ for crate in sqlx sqlx-core sqlx-postgres sqlx-sqlite libsqlite3-sys native-tls; do assert_lacks "$$backend_free" "$$crate" backend-free; done; \ for crate in sqlx sqlx-postgres native-tls; do assert_has "$$postgres" "$$crate" PostgreSQL-only; done; \ for crate in sqlx-sqlite libsqlite3-sys; do assert_lacks "$$postgres" "$$crate" PostgreSQL-only; done; \ for crate in sqlx sqlx-postgres native-tls; do assert_has "$$postgres_cert_auth" "$$crate" PostgreSQL-certificate-only; done; \ for crate in sqlx-sqlite libsqlite3-sys; do assert_lacks "$$postgres_cert_auth" "$$crate" PostgreSQL-certificate-only; done; \ for crate in sqlx sqlx-sqlite libsqlite3-sys; do assert_has "$$sqlite" "$$crate" SQLite-only; done; \ for crate in sqlx-postgres native-tls; do assert_lacks "$$sqlite" "$$crate" SQLite-only; done; \ for crate in sqlx sqlx-postgres sqlx-sqlite libsqlite3-sys native-tls; do assert_has "$$combined" "$$crate" combined; done; \ for crate in sqlx sqlx-core sqlx-postgres native-tls sqlx-sqlite libsqlite3-sys; do assert_lacks "$$default" "$$crate" default; done # Clippy each opt-in feature group on its own so a gate leak in a partial # feature set cannot hide behind the lean and full builds other targets cover. # This list is the single source of truth: the store-features-clippy CI matrix # in .github/workflows/tests.yaml is generated from `print-store-clippy-groups`. STORE_FEATURE_CLIPPY_GROUPS := openai-responses openai-agentic-loop openai-file-resolve-filter store store-postgres store-postgres-cert-auth store-sqlite openai-conversations openai-compact openai-mcp-tools # Emit the group list (space-separated) so CI can build its matrix from here. print-store-clippy-groups: @echo $(STORE_FEATURE_CLIPPY_GROUPS) test-store-features-clippy: @for group in $(STORE_FEATURE_CLIPPY_GROUPS); do \ $(MAKE) --no-print-directory test-store-features-clippy-group GROUP=$$group || exit 1; \ done test-store-features-clippy-group: @test -n "$(GROUP)" || { echo "GROUP must be set, e.g. GROUP=store-sqlite" >&2; exit 2; } @echo "clippy: standard + $(GROUP)" cargo clippy -p praxis-ai-apis -p praxis-ai-filters -p praxis-ai-proxy --all-targets \ --no-default-features --features praxis-ai-proxy/standard,praxis-ai-proxy/$(GROUP) \ -- -D warnings test-store-features-test: cargo test -p praxis-ai-apis --no-default-features --features openai-all,store-sqlite $(_NOCAPTURE) cargo test -p praxis-ai-apis --no-default-features --features openai-all,store-all $(_NOCAPTURE) @if cargo tree -p praxis-ai-proxy --features full --edges normal | grep -q libsqlite3-sys; then \ echo "ERROR: full proxy dependency graph contains libsqlite3-sys"; \ exit 1; \ fi @cargo tree -p praxis-ai-proxy --features $(STORE_POSTGRES_FEATURES) --edges features -i sqlx-core | grep -q '_tls-native-tls' || \ (echo "ERROR: PostgreSQL proxy SQLx graph does not enable native TLS"; exit 1) @if cargo tree -p praxis-ai-proxy --features $(STORE_POSTGRES_FEATURES) --edges features -i sqlx-core | grep -q '_tls-rustls'; then \ echo "ERROR: PostgreSQL proxy SQLx graph contains a rustls TLS backend"; \ exit 1; \ fi # Exercise the independent gates and the Responses-only SDK path without vLLM. test-responses-features: @set -eu; \ for group in full experimental; do \ graph="$$(cargo tree -p praxis-ai-proxy --no-default-features --features $$group --edges normal --prefix none --format '{p} {f}')"; \ if printf '%s\n' "$$graph" | grep -Eq '^praxis-ai-(apis|filters|proxy) .*openai-(responses|agentic-loop)'; then \ echo "ERROR: $$group enables experimental Responses or agentic code"; exit 1; \ fi; \ done @for group in full experimental openai-responses openai-agentic-loop; do \ cargo test -p praxis-ai-filters --no-default-features --features full,$$group register::tests || exit 1; \ done cargo build -p praxis-ai-proxy --features openai-responses PRAXIS_AI_BIN=$(abspath $(or $(CARGO_TARGET_DIR),target)/debug/praxis-ai) PRAXIS_TEST_RESPONSES_ONLY=1 \ uv run tests/integration/sdk/openai/test_openai_conversations.py \ -k 'passthrough_chain_forwards_create or agentic_loop_requires_separate_feature' test-feature-isolation: test-responses-features @for group in openai-responses openai-agentic-loop openai-file-resolve-filter openai-mcp-tools; do \ echo "check: $$group"; \ cargo check -p praxis-ai-proxy --features $$group || exit 1; \ done @for group in azure-ad-filter gcp-adc-filter vertex-anthropic-filter; do \ echo "check: $$group"; \ cargo check -p praxis-ai-proxy --features $$group || exit 1; \ done test-schema: cargo test -p praxis-tests-schema --features openai-all,store-all $(_NOCAPTURE) # The suite's subprocess tests need the praxis-ai binary prebuilt and named: # the harness refuses to build it from inside a test (see praxis_ai_bin in # tests/utils), because a nested cargo build inherits the outer run's # instrumentation and target-dir locks and can run for minutes. test-integration: test-integration-core test-integration-experimental test-integration-core: cargo build -p praxis-ai-proxy --bin praxis-ai --features openai-all,store-all PRAXIS_AI_BIN=$(abspath $(or $(CARGO_TARGET_DIR),target)/debug/praxis-ai) \ cargo test -p praxis-tests-integration --features openai-all,store-all $(_NOCAPTURE) # The experimental-filter permutation builds a second feature graph (store-all # plus the example-config filters) and runs only the example suites that need # them. Split into its own parallel CI job so this rebuild no longer extends the # test-integration critical path. test-integration-experimental: cargo build -p praxis-ai-proxy --bin praxis-ai --features openai-all,store-all,$(INTEGRATION_EXPERIMENTAL_FEATURES) PRAXIS_AI_BIN=$(abspath $(or $(CARGO_TARGET_DIR),target)/debug/praxis-ai) \ cargo test -p praxis-tests-integration --features openai-all,store-all,$(INTEGRATION_EXPERIMENTAL_FEATURES) --test suite \ -- examples::azure_ad examples::gcp_adc examples::lakera_guard examples::token_rate_limit examples::token_ceiling examples::inflight_tracking \ examples::vertex_anthropic \ $(if $(V),--nocapture) test-inference-fixtures: test-inference-fixtures-core test-inference-xtask test-inference-fixtures-core: cargo test -p praxis-test-utils --features openai-all,store-all,vertex-anthropic-filter $(_NOCAPTURE) cargo test -p praxis-tests-integration --features openai-all,store-all,vertex-anthropic-filter --test suite inference_fixtures $(_NOCAPTURE) # The xtask inference-fixture compile is a distinct crate graph from the # test-utils/suite builds above; split into its own parallel CI job so it no # longer serializes behind them. test-inference-xtask: cargo test -p xtask --features openai-all,store-all inference_fixtures $(_NOCAPTURE) # Every ignored test in praxis-ai-apis is a PostgreSQL store contract, so # --ignored alone selects them all: no name filter can drop one silently. # Backend history regressions run the same way with only PostgreSQL enabled. test-postgres-unit: cargo test -p praxis-ai-apis --no-default-features --features openai-all,store-all -- --ignored $(if $(V),--nocapture) cargo test -p praxis-ai-store-backends --no-default-features --features postgres \ -- --ignored $(if $(V),--nocapture) # Every PostgreSQL integration test is #[ignore]d (each spawns its own # container), so it runs only when named here. Enumerate every module explicitly: # a bare substring filter such as `openai_responses_store_postgres` incidentally # matches the response-store mTLS variant (a prefix) but cannot select the # Conversations certificate-auth module, silently dropping it from CI. Filters # must follow `--` so libtest treats each as an OR filter. Add every new # PostgreSQL integration module to this list. test-postgres-integration: cargo test -p praxis-tests-integration --test suite -- --ignored \ openai_responses_store_postgres \ openai_responses_store_postgres_mtls \ openai_conversations_postgres_mtls $(if $(V),--nocapture) test-token-rate-limit-sentinel-integration: @test -n "$(SENTINEL_IMAGE)" || { echo "SENTINEL_IMAGE must be an immutable @sha256 image reference" >&2; exit 2; } @env \ $(SENTINEL_DEAD_ENDPOINT_ENV) \ TOKEN_RATE_LIMIT_SENTINEL_FAILOVER_BEFORE_COMMAND="$(SENTINEL_FAILOVER_BEFORE_COMMAND)" \ TOKEN_RATE_LIMIT_SENTINEL_REPLICATION_HEALTH_TEST="$(SENTINEL_REPLICATION_HEALTH_TEST)" \ TOKEN_RATE_LIMIT_SENTINEL_ON_FAILURE="$(SENTINEL_ON_FAILURE)" \ tests/integration/fixtures/token_rate_limit/start-sentinel-topology.sh \ --product "$(SENTINEL_PRODUCT)" --image "$(SENTINEL_IMAGE)" \ --env-file "$(SENTINEL_ENV_FILE)" -- \ sh -c 'cargo test -p praxis-ai-filters --features token-rate-limit-filter \ live_sentinel_ --lib -- --test-threads=1 $(if $(V),--nocapture) && \ cargo test -p praxis-tests-integration --features token-rate-limit-filter --test suite \ $(SENTINEL_TEST_FILTER) -- --ignored --test-threads=1 $(if $(V),--nocapture)' test-token-rate-limit-valkey-unit: cargo test -p praxis-ai-filters --features token-rate-limit-filter valkey $(_NOCAPTURE) test-token-rate-limit-valkey-integration: cargo test -p praxis-tests-integration --features basic-auth-filter,token-rate-limit-filter --test suite \ mixed_algorithm_rules_valkey_backend_isolates_budgets_across_gateway_replicas $(_NOCAPTURE) cargo test -p praxis-tests-integration --features basic-auth-filter,token-rate-limit-filter --test suite \ authenticated_subject_valkey_backend_isolates_budgets_across_gateway_replicas $(_NOCAPTURE) openai-conformance: cargo xtask openai-conformance $(OPENAI_CONFORMANCE_ARGS) check-openai-conformance-reference: cargo xtask openai-conformance-reference --check test-openai-conformance: openai-conformance test-responses-conformance: uv run tests/integration/sdk/openai/test_responses_conformance.py -v $(RESPONSES_CONFORMANCE_ARGS) test-environment: cargo test -p praxis-ai-llmd-ext-proc $(_NOCAPTURE) cargo test -p praxis-tests-environment --features llmd-ext-proc $(_NOCAPTURE) # The llmd-ext-proc integration suite is the heaviest compile in this group; # split into its own parallel CI job so it no longer serializes behind the # ext-proc and environment crates. test-environment-suite: cargo test -p praxis-tests-integration --features llmd-ext-proc llmd_ext_proc $(_NOCAPTURE) # ------------------------------------------------------------------- # Quality # ------------------------------------------------------------------- # `make lint` runs both halves serially for local use. CI splits them into the # lint-clippy and lint-xtask jobs: the workspace clippy passes and the xtask # meta-lint build share no build artifacts (clippy artifacts are unusable by # `cargo run`, and xtask's `dev` feature resolves a distinct dependency graph), # so running them serially wastes ~11m rebuilding the tree a second time. As two # jobs the wall-clock collapses to the heavier half instead of their sum. lint: lint-clippy lint-xtask # Clippy across every feature permutation, plus the dependency budget, the # rustfmt check, and the unused-dependency scan. These are the rustc/clippy # steps; `cargo machete` is here because CI installs it only in this job. lint-clippy: setup-lint-toolchains $(LINT_CARGO) clippy --workspace --all-targets -- -D warnings $(LINT_CARGO) clippy --workspace --all-targets \ --features $(STORE_ALL_WORKSPACE_FEATURES),xtask/openai-all,praxis-ai-proxy/azure-ad-filter,praxis-ai-proxy/basic-auth-filter,praxis-ai-proxy/gcp-adc-filter,praxis-ai-proxy/http-callout-filter,praxis-ai-proxy/token-rate-limit-filter,praxis-ai-proxy/token-ceiling-filter,praxis-ai-proxy/inflight-tracker-filter,praxis-ai-proxy/vertex-anthropic-filter,praxis-tests-integration/azure-ad-filter,praxis-tests-integration/basic-auth-filter,praxis-tests-integration/gcp-adc-filter,praxis-tests-integration/http-callout-filter,praxis-tests-integration/token-rate-limit-filter,praxis-tests-integration/token-ceiling-filter,praxis-tests-integration/inflight-tracker-filter,praxis-tests-integration/vertex-anthropic-filter \ -- -D warnings $(MAKE) lint-lean $(MAKE) check-dep-budget $(LINT_FMT_CARGO) fmt --all -- --check $(LINT_CARGO) machete --with-metadata . # Match setup-rust-lint in CI: an old local nightly must not silently pass. # Refresh failures are fatal, rather than falling back to cached toolchains. setup-lint-toolchains: @test -n "$(LINT_RUST_VERSION)" || { echo "ERROR: missing channel in rust-toolchain.toml" >&2; exit 1; } rustup toolchain install $(LINT_RUST_VERSION) --profile minimal --component clippy --no-self-update rustup toolchain install nightly --profile minimal --component rustfmt --allow-downgrade --no-self-update # xtask-driven meta lints (dep semver, separators, filter/example docs, markdown # links, README/registry/coverage sync checks) plus the FIPS dependency guard. # The first `cargo xtask` builds the full product tree once; grouping these # isolates that build from the clippy passes so the two halves run in parallel. lint-xtask: lint-license-headers cargo xtask lint-deps $(MAKE) fips-deps cargo xtask lint-separators cargo xtask lint-filter-docs cargo xtask lint-example-tests cargo xtask lint-markdown-links cargo xtask sync-example-readme cargo xtask sync-inference-readme cargo xtask sync-responses-readme cargo xtask sync-flow-visualizers cargo xtask check-inference cargo xtask check-responses-registry cargo xtask check-chat-completions-registry cargo xtask check-files-registry cargo xtask check-vector-stores-registry cargo xtask check-anthropic-messages-registry cargo xtask openresponses-coverage cargo xtask translation-matrix # Rust sources in this workspace use Apache-2.0. Catch accidental MIT headers # before the xtask lints; git grep limits the check to tracked Rust source files. lint-license-headers: @if git grep -n '^// SPDX-License-Identifier: MIT$$' -- '*.rs'; then \ echo "error: Rust source files must use the workspace Apache-2.0 SPDX header" >&2; \ exit 1; \ else \ status=$$?; \ if [ "$$status" -ne 1 ]; then exit "$$status"; fi; \ fi # Lint the product crates with only the default-on gates. `-p` without # `--workspace` keeps test-crate features from unifying in and hiding leaks. lint-lean: $(LINT_CARGO) clippy -p praxis-ai-apis -p praxis-ai-filters -p praxis-ai-proxy --all-targets \ --no-default-features --features praxis-ai-proxy/standard -- -D warnings RUSTDOCFLAGS="-D warnings" $(LINT_CARGO) doc -p praxis-ai-apis -p praxis-ai-filters -p praxis-ai-proxy \ --no-deps --document-private-items --no-default-features --features praxis-ai-proxy/standard # Fail if a heavy crate enters the explicit lean proxy graph, or if that graph # grows past LEAN_GRAPH_BUDGET crates. check-dep-budget: @tree="$$($(LINT_CARGO) tree --locked -p praxis-ai-proxy -e normal,build --target all \ --no-default-features --features standard --prefix none --format '{p}')" || { echo "ERROR: cargo tree failed"; exit 1; }; \ host="$$($(LINT_CARGO) tree --locked -p praxis-ai-proxy -e normal,build \ --no-default-features --features standard --prefix none --format '{p}')" || { echo "ERROR: cargo tree failed"; exit 1; }; \ graph="$$(printf '%s\n' "$$tree" | awk '{print $$1"@"$$2}' | sort -u)"; \ status=0; \ for crate in $(LEAN_GRAPH_DENY); do \ if printf '%s\n' "$$graph" | grep -q "^$$crate@"; then \ echo "ERROR: $$crate is in the lean praxis-ai-proxy graph:"; \ $(LINT_CARGO) tree --locked -p praxis-ai-proxy -e normal,build --target all \ --no-default-features --features standard -i "$$crate" | head -n 15; \ status=1; \ fi; \ done; \ count=$$(printf '%s\n' "$$host" | awk '{print $$1"@"$$2}' | sort -u | wc -l); \ [ "$$count" -gt 1 ] || { echo "ERROR: empty lean dependency graph"; exit 1; }; \ echo "lean praxis-ai-proxy graph: $$count crates for the host target (budget $(LEAN_GRAPH_BUDGET))"; \ [ "$$count" -le $(LEAN_GRAPH_BUDGET) ] || { echo "ERROR: over budget"; status=1; }; \ exit $$status fmt: setup-lint-toolchains $(LINT_FMT_CARGO) fmt --all doc: RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --document-private-items audit: cargo audit cargo deny check # The suite's subprocess tests use the praxis-ai binary that cargo llvm-cov # builds anyway (the server crate has integration tests, so cargo builds its # bin target). The harness cannot find it on its own because llvm-cov sets # --target-dir on the command line rather than CARGO_TARGET_DIR, so name it # here; a separate uninstrumented build would recompile the whole workspace # a second time (see praxis_ai_bin). coverage-check: PRAXIS_AI_BIN=$(abspath target/llvm-cov-target/debug/praxis-ai) \ cargo llvm-cov --workspace --features $(STORE_ALL_WORKSPACE_FEATURES) --json \ --exclude xtask \ --ignore-filename-regex '(target/|tests/|store-backends/src/postgres\.rs)' \ --output-path coverage.json @LINE_PCT=$$(jq '.data[0].totals.lines.percent' coverage.json); \ echo "Line coverage: $${LINE_PCT}%"; \ if [ $$(echo "$${LINE_PCT} < 95" | bc -l) -eq 1 ]; then \ echo "FAIL: coverage $${LINE_PCT}% is below 95% threshold"; \ exit 1; \ fi # ------------------------------------------------------------------- # FIPS # ------------------------------------------------------------------- # # Both published images leave experimental Responses and agentic loops out. # The default FIPS profile also excludes the policy engine's non-OpenSSL crypto. # Certificate-authenticated PostgreSQL and Responses remain explicit opt-ins: # make release-fips FIPS_FEATURES=aws-sigv4-filter,openai-responses,openai-file-resolve-filter,store-postgres-cert-auth # General SQLx password authentication, SQLite, compaction, Conversations and # MCP dependencies have not been cleared for the FIPS profile. # FIPS_FEATURES mirrors Containerfile.fips and xtask/src/fips.rs. # # The FIPS build goes to its own target directory so it never overwrites, # or is mistaken for, the standard build. # # make build-fips FIPS build, debug profile # make release-fips FIPS build, release profile # make lint-fips clippy + rustfmt for the FIPS feature set # make test-fips unit tests for the FIPS feature set # make test-fips-provider # the same tests with the RHEL FIPS provider # active in the test processes (needs fips.so) # make container-fips FIPS runtime image on UBI 9 (Red Hat toolchain, # signature-verified base images) # make fips-check build on UBI 9, run the crypto unit tests on the # FIPS provider, print the compliance report # make fips-report the same report against the local FIPS build # make fips-deps dependency graph only (seconds, no build; also # runs under `make lint`, so a PR cannot reintroduce # a denied crate into the FIPS build) # make fips-smoke run the FIPS image once (validates its config) # make fips-scan run Red Hat's scanner (check-payload) on the # FIPS image, warnings fatal: the actual gate # make fips-scanner build check-payload at the pinned revision # make fips-oc download the OpenShift CLI the scanner insists on # make fips-signature-store # point podman at Red Hat's signature store; needed # once on Debian/Ubuntu hosts, a no-op elsewhere # # The report, the image verification and the signature-store setup are # `cargo xtask fips` commands (xtask/src/fips/). XTASK_FIPS builds xtask # without its default features, so these targets never compile the standard # proxy build to run. # # See docs/developing/fips.md and docs/developing/getting-started.md. FIPS_FEATURES ?= aws-sigv4-filter # The same list qualified for a multi-package cargo invocation. _COMMA := , FIPS_FEATURES_QUALIFIED := $(subst $(_COMMA),$(_COMMA)praxis-ai-proxy/,praxis-ai-proxy/$(FIPS_FEATURES)) # Overridable so the FIPS host run can point the whole recursion at a # container volume (see test-fips-host). FIPS_TARGET_DIR ?= target/fips FIPS_BIN ?= $(FIPS_TARGET_DIR)/release/praxis-ai # Extra cargo arguments for every FIPS test target; the toolchain image sets # --ignore-rust-version because Red Hat's rust-toolset may trail the # workspace's rust-version. FIPS_CARGO_EXTRA ?= FIPS_CARGO_ARGS := -p praxis-ai-proxy --no-default-features --features $(FIPS_FEATURES) --target-dir $(FIPS_TARGET_DIR) # Red Hat's scanner reads the crate list that `cargo auditable` embeds in the # binary (the .dep-v0 section); without it a binary is graded inconclusive. # `make release-fips` embeds it when cargo-auditable is installed (`cargo # install cargo-auditable --version 0.7.6 --locked`); the report says so when # it was not. # # The list must be exactly the crates compiled in. On a stable toolchain # cargo-auditable derives it from `cargo metadata`, which unifies features # across the whole workspace and activates weak features (`dep?/feature`) # the real build never turns on; with rustls that puts `ring` in the manifest # of a binary that never compiled it, and the scanner fails on the name alone. # Cargo's SBOM precursor (`-Zsbom`, unstable) is the exact list, so the # release build enables it: RUSTC_BOOTSTRAP=1 lets stable cargo accept the # flag, and the env overrides hand rustc and every build script # RUSTC_BOOTSTRAP=-1, which forbids unstable features, so the code compiled is # the stable code. Drop this once cargo's `build.sbom` is stable # (rust-lang/cargo#13709). Same recipe in Containerfile.fips. CARGO_AUDITABLE := $(shell command -v cargo-auditable >/dev/null 2>&1 && echo "cargo auditable" || echo "cargo") FIPS_SBOM_ENV := RUSTC_BOOTSTRAP=1 CARGO_BUILD_SBOM=true FIPS_SBOM_ARGS := -Zsbom --config 'env.RUSTC_BOOTSTRAP.value="-1"' --config 'env.RUSTC_BOOTSTRAP.force=true' FIPS_UBI9_DIGEST := sha256:a4b9ec09b1e790a53ef25b7777c539976abe519248264298e5194dcbceac8c31 FIPS_UBI9_MINIMAL_DIGEST := sha256:8ebe2ad8fdf3cab3e5a53c1edc69194c98209cfadab24b884f4ad9ebcf7bbbfc FIPS_UBI9_IMAGE := registry.access.redhat.com/ubi9/ubi@$(FIPS_UBI9_DIGEST) FIPS_UBI9_MINIMAL_IMAGE := registry.access.redhat.com/ubi9/ubi-minimal@$(FIPS_UBI9_MINIMAL_DIGEST) FIPS_CHECK_IMAGE ?= praxis-ai-fips-check # Red Hat's toolchain and OpenSSL, no sources: the image `test-fips-host` # runs the suites in (the `toolchain` stage of Containerfile.fips). FIPS_TOOLCHAIN_IMAGE ?= praxis-ai-fips-toolchain FIPS_BUILD_ARGS := --build-arg UBI9_DIGEST=$(FIPS_UBI9_DIGEST) \ --build-arg UBI9_MINIMAL_DIGEST=$(FIPS_UBI9_MINIMAL_DIGEST) \ --build-arg CARGO_FEATURES=$(FIPS_FEATURES) XTASK_FIPS := cargo run -q -p xtask --no-default-features -- # Red Hat's scanner, openshift/check-payload, at the revision that added Rust # support (the head of its PR #360, fetched by commit so a rewrite of the PR # cannot break the build). `make fips-scanner` builds it into target/fips; # point CHECK_PAYLOAD at another build to use it instead. CHECK_PAYLOAD_REPO := https://github.com/openshift/check-payload CHECK_PAYLOAD_REV := 1ce4e04ed214b98997797ce19a2442f794632e65 CHECK_PAYLOAD_DIR := $(FIPS_TARGET_DIR)/check-payload CHECK_PAYLOAD ?= $(CHECK_PAYLOAD_DIR)/check-payload # The FIPS image as podman's storage names it: a bare name gets podman's # implicit localhost/ prefix, a registry-qualified IMAGE does not. _IMAGE_HEAD := $(firstword $(subst /, ,$(IMAGE))) FIPS_IMAGE_REF := $(if $(or $(findstring .,$(_IMAGE_HEAD)),$(findstring :,$(_IMAGE_HEAD)),$(filter localhost,$(_IMAGE_HEAD))),$(IMAGE),localhost/$(IMAGE)):$(VERSION)-fips # The scanner mounts the image from podman's store, which needs the user # namespace only for rootless podman. PODMAN_UNSHARE := $(if $(filter 0,$(shell id -u)),,podman unshare) # check-payload refuses to run without the OpenShift CLI (oc) on PATH, even # for a local image scan. `make fips-oc` downloads Red Hat's pinned client # release into target/fips/bin and checks its published sha256 (the checksum # Red Hat lists next to the tarball on mirror.openshift.com); fips-scan puts # that directory on PATH. Pinned for Linux x86_64, which is what CI runs; on # any other platform install oc yourself and it is picked up from PATH. UNAME_S := $(shell uname -s | tr '[:upper:]' '[:lower:]') UNAME_M := $(shell uname -m) SHA256SUM := $(if $(filter darwin,$(UNAME_S)),gsha256sum,sha256sum) OC_VERSION := 4.22.14 OC_DIR := $(FIPS_TARGET_DIR)/bin OC := $(OC_DIR)/oc OC_SHA256_linux_x86_64 := 7dbe8c2813bc09e18a666155eb4fa88dc3260c3832c553aa16d86c7c4277ba03 OC_SHA256 := $(OC_SHA256_$(UNAME_S)_$(UNAME_M)) require-podman: @command -v podman >/dev/null || { echo "podman is required: Red Hat image signatures can only be verified with podman"; exit 1; } require-go: @command -v go >/dev/null || { echo "go is required to build check-payload"; exit 1; } require-oc: @command -v oc >/dev/null || [ -x $(OC) ] || { echo "oc (the OpenShift CLI) is required: check-payload refuses to scan without it on PATH; run 'make fips-oc'"; exit 1; } $(OC): @mkdir -p $(OC_DIR) curl -sSfL -o $(OC_DIR)/oc.tar.gz \ https://mirror.openshift.com/pub/openshift-v4/$(UNAME_M)/clients/ocp/$(OC_VERSION)/openshift-client-linux-$(OC_VERSION).tar.gz $(if $(OC_SHA256),echo "$(OC_SHA256) $(OC_DIR)/oc.tar.gz" | $(SHA256SUM) -c,$(error no pinned SHA256 for oc on $(UNAME_S)/$(UNAME_M); refusing to use an unverified download)) tar xz -C $(OC_DIR) -f $(OC_DIR)/oc.tar.gz oc rm -f $(OC_DIR)/oc.tar.gz fips-oc: $(OC) # The debug build is the edit-compile loop; only the release build carries # the manifest. build-fips: cargo build $(FIPS_CARGO_ARGS) $(FIPS_CARGO_EXTRA) # cargo before 1.99 does not relink a binary when only the SBOM setting # changed (rust-lang/cargo#15695, fixed by #17216), so the old binary goes # first; everything else stays cached. Drop the clean once the toolchains in # use (here and the UBI rust-toolset) are 1.99 or newer. release-fips: ifeq ($(CARGO_AUDITABLE),cargo auditable) cargo clean --release -p praxis-ai-proxy --target-dir $(FIPS_TARGET_DIR) $(FIPS_SBOM_ENV) cargo auditable $(FIPS_SBOM_ARGS) build --release $(FIPS_CARGO_ARGS) else @echo "warning: cargo-auditable is not installed; no crate manifest will be embedded (cargo install cargo-auditable --version 0.7.6 --locked)" cargo build --release $(FIPS_CARGO_ARGS) endif check-fips: cargo check $(FIPS_CARGO_ARGS) # Clippy over every target of the FIPS build, plus the rustfmt check (which # is feature-independent but belongs in "is the FIPS version clean"). lint-fips: cargo clippy $(FIPS_CARGO_ARGS) --all-targets -- -D warnings cargo +nightly fmt --all -- --check # Unit tests of the crates that make up the FIPS binary, resolved exactly as # the FIPS build resolves them: no default features anywhere, only # FIPS_FEATURES on the binary. The integration suites run the standard build # through the test harness and are covered by `make test-integration`. test-fips: cargo test --target-dir $(FIPS_TARGET_DIR) --no-default-features \ -p praxis-ai-proxy -p praxis-ai-filters -p praxis-ai-apis \ --features $(FIPS_FEATURES_QUALIFIED) $(FIPS_CARGO_EXTRA) $(_NOCAPTURE) # The integration and schema suites resolved exactly as the FIPS build: no # default features anywhere, only FIPS_FEATURES on the test crates (which # forward them to the proxy). The proxy runs in-process in these suites, so # the test binary's dependency graph is the FIPS build's graph, and tests of # filters the FIPS build leaves out are compiled out with it. The tests that # spawn the binary get the FIPS binary (`build-fips`, named through # PRAXIS_AI_BIN) rather than the standard one the harness would build. # # On a host that is not in FIPS mode this proves the suites pass on the FIPS # feature set; every FIPS behavior test takes its non-FIPS branch. On a FIPS # host, run it through `test-fips-host`, which declares the host as such so # the same tests insist on their approved-mode branch instead. test-integration-fips: build-fips PRAXIS_AI_BIN=$(abspath $(FIPS_TARGET_DIR))/debug/praxis-ai \ cargo test --target-dir $(FIPS_TARGET_DIR) -p praxis-tests-integration \ --no-default-features --features $(FIPS_FEATURES) $(FIPS_CARGO_EXTRA) $(_NOCAPTURE) test-schema-fips: cargo test --target-dir $(FIPS_TARGET_DIR) -p praxis-tests-schema \ --no-default-features --features $(FIPS_FEATURES) $(FIPS_CARGO_EXTRA) $(_NOCAPTURE) # The certificate-authenticated PostgreSQL boundary on the actual FIPS host. # This runs outside the toolchain container because the test starts a sibling # PostgreSQL container through the host's rootless podman. It still resolves # the explicit experimental certificate-auth profile, fails closed unless the host and # provider report FIPS mode, and exercises a write/read round trip with a # password-less URL against a server whose only TCP rule is `hostssl ... cert`. # A second peer selects SCRAM and must receive the fork's explicit # password-authentication-disabled refusal before any exchange begins. test-postgres-fips-host: | require-podman PRAXIS_FIPS_HOST=1 PRAXIS_REQUIRE_FIPS=1 PRAXIS_TEST_FIPS_PROVIDER=1 \ cargo test --target-dir $(FIPS_TARGET_DIR) -p praxis-tests-integration \ --no-default-features --features $(FIPS_FEATURES),store-postgres-cert-auth \ --test suite -- --ignored openai_response_store_postgres_mtls $(if $(V),--nocapture) # The same unit tests with the RHEL FIPS provider active in every test # process: OPENSSL_CONF names xtask/assets/fips/fips-provider.cnf (the file # the report probes with), so OpenSSL's default properties are `fips=yes` # and every digest and MAC the tests compute (aws_sigv4_sign's HMAC-SHA256 # among them) has to come from the FIPS provider. The variable reaches the # test binaries through cargo's runner, not cargo itself, whose libgit2 # cannot run under that property; for the same reason only the lib and bin # unit tests run (the e2e test target spawns cargo). A missing or wrong # OPENSSL_CONF is silently ignored by OpenSSL, so the run also sets # PRAXIS_TEST_FIPS_PROVIDER, on cargo itself so it reaches the test binaries # whatever the runner does: the apis and filters test processes then assert # that the provider reports FIPS and refuses MD5, and fail otherwise. Needs # the host's fips module (Fedora and RHEL ship /usr/lib64/ossl-modules/fips.so); # the same tests run inside the UBI 9 report stage (make fips-check), and # only a FIPS-mode host proves a deployment (docs/fips.md). FIPS_PROVIDER_CNF := $(CURDIR)/xtask/assets/fips/fips-provider.cnf test-fips-provider: @OPENSSL_CONF=$(FIPS_PROVIDER_CNF) openssl list -providers 2>/dev/null | grep -q '^ fips$$' \ || { echo "no OpenSSL FIPS provider on this host: 'OPENSSL_CONF=$(FIPS_PROVIDER_CNF) openssl list -providers' does not list fips"; exit 1; } PRAXIS_TEST_FIPS_PROVIDER=1 cargo test --target-dir $(FIPS_TARGET_DIR) --no-default-features --lib --bins \ -p praxis-ai-proxy -p praxis-ai-filters -p praxis-ai-apis \ --features $(FIPS_FEATURES_QUALIFIED) \ --config 'target."cfg(all())".runner=["env","OPENSSL_CONF=$(FIPS_PROVIDER_CNF)"]' \ $(_NOCAPTURE) # podman finds Red Hat's detached image signatures through its registries.d # (containers-registries.d(5)). Fedora and RHEL ship the entry; Debian and # Ubuntu, GitHub's runners included, ship no registries.d at all, and then # every Red Hat image looks unsigned. This installs the bundled entry for the # current user when the registries.d podman reads names none, and does # nothing otherwise. CI runs it before fips-verify-image. fips-signature-store: $(XTASK_FIPS) fips signature-store --install fips-verify-image: | require-podman $(XTASK_FIPS) fips verify-image --pinned-in Containerfile.fips $(FIPS_UBI9_IMAGE) $(XTASK_FIPS) fips verify-image --pinned-in Containerfile.fips $(FIPS_UBI9_MINIMAL_IMAGE) fips-image-ref: @printf '%s\n' '$(FIPS_IMAGE_REF)' container-fips: fips-verify-image podman build -f Containerfile.fips --target runtime $(FIPS_BUILD_ARGS) \ -t $(IMAGE):$(VERSION)-fips . fips-toolchain: fips-verify-image podman build -f Containerfile.fips --target toolchain $(FIPS_BUILD_ARGS) \ -t $(FIPS_TOOLCHAIN_IMAGE) . # The test suites as the FIPS build, inside the toolchain image, on a # FIPS-enabled host: the runtime proof the hosted checks cannot give. The # checkout is bind-mounted, so the tests are the working tree's; the # toolchain and OpenSSL are the image's, the same packages the FIPS image is # built with; the kernel flag and the FIPS crypto policy are the host's, # which podman passes into the container. PRAXIS_FIPS_HOST makes the harness # fail closed unless the process really is in FIPS mode, PRAXIS_REQUIRE_FIPS # makes every proxy the suites start enforce it, and # PRAXIS_TEST_FIPS_PROVIDER arms the hash and SigV4 unit tests' assertion # that the provider is in approved mode. The cargo home and the target # directory live in named volumes so a second run is incremental. # # The container runs as the invoking user (rootless podman, keep-id): # praxis-ai refuses to start as root, and the tests that boot the real # server would fail for that reason alone as container root. # # Needs rootless podman on a RHEL 9 host in FIPS mode (docs/fips.md). On any # other host it fails at the first test, by design. test-fips-host: fips-toolchain podman run --rm --userns=keep-id --security-opt label=disable \ -v $(CURDIR):/src -w /src \ -v praxis-ai-fips-host-cargo:/cargo:U \ -v praxis-ai-fips-host-target:/target \ -e PRAXIS_FIPS_HOST=1 -e PRAXIS_REQUIRE_FIPS=1 -e PRAXIS_TEST_FIPS_PROVIDER=1 \ -e CARGO_TERM_COLOR=always \ $(FIPS_TOOLCHAIN_IMAGE) \ make fips-host-facts test-fips test-integration-fips test-schema-fips \ FIPS_TARGET_DIR=/target FIPS_CARGO_EXTRA=--ignore-rust-version $(if $(V),V=$(V)) # What the process the suites run as actually sees, printed into the log next # to the results: the user, the kernel flag and boot parameter, the crypto # policy, the OpenSSL packages, the providers OpenSSL loads, whether MD5 is # refused, and the variables that drive the FIPS tests. These are properties # of the container, so one process proving them proves them for every test # binary in the run. With PRAXIS_FIPS_HOST declared it fails here, before # anything compiles, unless the kernel flag, the active fips provider and # the MD5 refusal all agree. fips-host-facts: @echo "== FIPS host facts, as seen by the process the suites run as" @echo "user: $$(id -u):$$(id -g)" @echo "kernel fips_enabled: $$(cat /proc/sys/crypto/fips_enabled 2>/dev/null || echo unreadable)" @echo "kernel cmdline fips=1: $$(tr ' ' '\n' < /proc/cmdline | grep -qx 'fips=1' && echo yes || echo no)" @echo "crypto policy: $$(grep -v '^#' /etc/crypto-policies/config 2>/dev/null | grep -m1 . || echo none)" @echo "packages: $$(rpm -q openssl-libs openssl-fips-provider-so 2>/dev/null | tr '\n' ' ')" @echo "openssl: $$(openssl version 2>/dev/null || echo 'no openssl command')" @openssl list -providers 2>/dev/null | sed 's/^/ /' @echo "md5: $$(echo x | openssl dgst -md5 >/dev/null 2>&1 && echo works || echo refused)" @echo "PRAXIS_FIPS_HOST=$${PRAXIS_FIPS_HOST:-} PRAXIS_REQUIRE_FIPS=$${PRAXIS_REQUIRE_FIPS:-} PRAXIS_TEST_FIPS_PROVIDER=$${PRAXIS_TEST_FIPS_PROVIDER:-}" @case "$$(echo "$${PRAXIS_FIPS_HOST:-}" | tr A-Z a-z)" in \ ''|0|false|no|off) echo "verdict: PRAXIS_FIPS_HOST not declared; the FIPS tests take whichever branch the provider dictates" ;; \ *) [ "$$(cat /proc/sys/crypto/fips_enabled 2>/dev/null)" = 1 ] || { echo "verdict: PRAXIS_FIPS_HOST is set but the kernel is not in FIPS mode"; exit 1; }; \ openssl list -providers 2>/dev/null | grep -qx ' fips' || { echo "verdict: PRAXIS_FIPS_HOST is set but the fips provider is not active"; exit 1; }; \ echo x | openssl dgst -md5 >/dev/null 2>&1 && { echo "verdict: PRAXIS_FIPS_HOST is set but MD5 works"; exit 1; }; \ echo "verdict: FIPS mode confirmed for this container; every test below runs in it" ;; \ esac # The FIPS-host attestation: kernel flag, boot parameter, crypto policy and # the module the host's OpenSSL loads, then the same questions of the FIPS # image (the crypto policy podman propagates into it, and the build of # fips.so it carries, looked up in xtask/assets/fips/certified-modules.json). # Exit 1 on any unmet requirement; a module build still in validation is a # warning unless FIPS_HOST_CHECK_ARGS adds --require-certified. Writes the # attestation to target/fips/ for CI to keep. FIPS_HOST_CHECK_ARGS ?= fips-host-check: | require-podman @mkdir -p $(FIPS_TARGET_DIR) $(XTASK_FIPS) fips host-check --image $(FIPS_IMAGE_REF) \ --out $(FIPS_TARGET_DIR)/host-attestation.txt \ --json $(FIPS_TARGET_DIR)/host-attestation.json $(FIPS_HOST_CHECK_ARGS) # Run the FIPS image on this FIPS host under PRAXIS_REQUIRE_FIPS=1 and drive # the listener probes of the integration suite against it from the toolchain # image; keeps the container's log in target/fips/. fips-runtime-probe: | require-podman @mkdir -p $(FIPS_TARGET_DIR) $(XTASK_FIPS) fips runtime-probe $(FIPS_IMAGE_REF) \ --toolchain-image $(FIPS_TOOLCHAIN_IMAGE) --log $(FIPS_TARGET_DIR)/runtime-probe.log # Hand the built image to another machine as an archive (the FIPS runner # tests the exact image the hosted job built and scanned, not a rebuild). FIPS_IMAGE_ARCHIVE ?= $(FIPS_TARGET_DIR)/praxis-ai-fips-image.tar fips-image-save: | require-podman @mkdir -p $(dir $(FIPS_IMAGE_ARCHIVE)) podman save --output $(FIPS_IMAGE_ARCHIVE) $(FIPS_IMAGE_REF) podman image inspect --format '{{.Id}}' $(FIPS_IMAGE_REF) > $(FIPS_IMAGE_ARCHIVE).id # Load an archive `fips-image-save` wrote and check its id is the one that # was saved. fips-image-load: | require-podman podman load --input $(FIPS_IMAGE_ARCHIVE) @loaded=$$(podman image inspect --format '{{.Id}}' $(FIPS_IMAGE_REF)); \ saved=$$(cat $(FIPS_IMAGE_ARCHIVE).id); \ [ "$$loaded" = "$$saved" ] || { echo "loaded image $$loaded is not the saved image $$saved"; exit 1; }; \ echo "loaded $(FIPS_IMAGE_REF) $$loaded" # Name an image podman already has (a published digest that was pulled, say) # the way the FIPS targets expect it. fips-image-tag: | require-podman @[ -n "$(FIPS_IMAGE_SOURCE)" ] || { echo "set FIPS_IMAGE_SOURCE to the reference to tag as $(FIPS_IMAGE_REF)"; exit 1; } podman tag $(FIPS_IMAGE_SOURCE) $(FIPS_IMAGE_REF) # The version the FIPS image is tagged with, for scripts that need it. fips-version: @echo $(VERSION) container-fips-run: | require-podman podman run --rm --network=host $(IMAGE):$(VERSION)-fips 2>&1 # The binary starts on ubi-minimal, loads the system OpenSSL and accepts its # built-in default config; a cheap proof that the image runs before the scan. fips-smoke: | require-podman podman run --rm --entrypoint praxis-ai $(IMAGE):$(VERSION)-fips --validate fips-check: fips-check-ubi fips-check-ubi: fips-verify-image podman build -f Containerfile.fips --target report $(FIPS_BUILD_ARGS) \ -t $(FIPS_CHECK_IMAGE) . podman run --rm $(FIPS_CHECK_IMAGE) fips-report: $(XTASK_FIPS) fips report --features $(FIPS_FEATURES) $(FIPS_BIN) # The graph check is `cargo xtask fips report` (cargo tree scoped to the # binary and its feature set) rather than cargo-deny: cargo-deny resolves # features workspace-wide, and the test crates enable the policy engine and # the stores on the binary, so it cannot see the FIPS build's real graph. fips-deps: $(XTASK_FIPS) fips report --deps-only --features $(FIPS_FEATURES) # --fail-on-warnings makes an inconclusive verdict (for example a binary # without a crate manifest) fail, as Red Hat's gated scans do. Needs a Linux # podman (rootless or root), not a podman machine. fips-scan: | require-podman require-oc @[ -x "$(CHECK_PAYLOAD)" ] || { echo "check-payload not found at $(CHECK_PAYLOAD): run 'make fips-scanner' (needs go) or set CHECK_PAYLOAD"; exit 1; } PATH="$(abspath $(OC_DIR)):$$PATH" $(PODMAN_UNSHARE) $(CHECK_PAYLOAD) scan image \ --spec containers-storage:$(FIPS_IMAGE_REF) --fail-on-warnings # Built as upstream builds it (CGO_ENABLED=0, vendored modules). fips-scanner: | require-go @mkdir -p $(CHECK_PAYLOAD_DIR) @[ -d $(CHECK_PAYLOAD_DIR)/.git ] || git -C $(CHECK_PAYLOAD_DIR) init --quiet git -C $(CHECK_PAYLOAD_DIR) fetch --quiet --depth 1 $(CHECK_PAYLOAD_REPO) $(CHECK_PAYLOAD_REV) git -C $(CHECK_PAYLOAD_DIR) checkout --quiet FETCH_HEAD cd $(CHECK_PAYLOAD_DIR) && CGO_ENABLED=0 go build -o check-payload . # ------------------------------------------------------------------- # Praxis path override (test against local ../praxis) # ------------------------------------------------------------------- patch-praxis: @if [ ! -d "../praxis" ]; then \ echo "ERROR: ../praxis not found — clone praxis core as a sibling directory first"; \ exit 1; \ fi @if grep -q '\[patch\.crates-io\]' Cargo.toml; then \ echo "Already patched — run 'make unpatch-praxis' first"; \ exit 1; \ fi @printf '\n[patch.crates-io]\n\ praxis-proxy-core = { path = "../praxis/core" }\n\ praxis-proxy-filter = { path = "../praxis/filter" }\n\ praxis-proxy-protocol = { path = "../praxis/protocol" }\n\ praxis-proxy-tls = { path = "../praxis/tls" }\n\ praxis-proxy = { path = "../praxis/server" }\n' >> Cargo.toml @echo "Patched Cargo.toml to use ../praxis path dependencies" unpatch-praxis: @if ! grep -q '\[patch\.crates-io\]' Cargo.toml; then \ echo "Nothing to unpatch"; \ exit 0; \ fi @sed -i.bak '/^\[patch\.crates-io\]/,$$d' Cargo.toml && rm -f Cargo.toml.bak @echo "Removed [patch.crates-io] from Cargo.toml" # ------------------------------------------------------------------- # Dev Setup # ------------------------------------------------------------------- setup-hooks: ln -sf ../../.hooks/pre-commit .git/hooks/pre-commit @echo "Git hooks installed." # ------------------------------------------------------------------- # Help # ------------------------------------------------------------------- help: @echo "Variables:" @echo " V=1 show test output (--nocapture)" @echo "" @echo "Top-level:" @echo " all build + lint + test + audit" @echo "" @echo "Build:" @echo " build cargo build --workspace" @echo " release cargo build --release -p praxis-ai-proxy --no-default-features --features $(PRAXIS_AI_FEATURES)" @echo " check cargo check --workspace" @echo " clean cargo clean" @echo "" @echo "Test:" @echo " test run all tests" @echo " test-unit unit tests (providers, filters, server)" @echo " test-store-features run store checks, Clippy, and API tests" @echo " test-store-features-check check store feature graphs and backend boundaries" @echo " test-store-features-clippy lint each store feature group" @echo " test-store-features-clippy-group lint one group (GROUP=name)" @echo " test-store-features-test run store API tests and TLS graph checks" @echo " test-responses-features independent experimental gates and Responses-only SDK smoke tests" @echo " test-schema schema validation tests" @echo " test-integration full integration suite (core + experimental)" @echo " test-integration-core store-all integration suite" @echo " test-integration-experimental experimental-filter integration suite" @echo " test-inference-fixtures inference fixtures, replay, and xtask tests" @echo " test-inference-xtask inference fixture xtask tests" @echo " bench-model-to-provider optimized mapped and fallback request-body timings" @echo " test-postgres-unit postgres store unit tests (needs DATABASE_URL)" @echo " test-postgres-integration postgres store integration tests (needs container engine)" @echo " test-token-rate-limit-valkey-unit token_rate_limit Valkey unit tests (needs TOKEN_RATE_LIMIT_VALKEY_URL)" @echo " test-token-rate-limit-valkey-integration token_rate_limit Valkey integration test (needs TOKEN_RATE_LIMIT_VALKEY_URL)" @echo " test-token-rate-limit-sentinel-integration Sentinel fixture smoke (needs SENTINEL_IMAGE; failover with SENTINEL_FAILOVER_BEFORE_COMMAND=1)" @echo " test-environment llm-d ext_proc environment tests" @echo " test-environment-suite llmd-ext-proc integration suite" @echo " openai-conformance compare registered API areas with OpenAI's OpenAPI spec" @echo " check-openai-conformance-reference verify the pinned complete OpenAI reference" @echo " test-responses-conformance Run OpenResponses suite against the translation filter (needs bun, uv, vLLM)" @echo "" @echo "Quality:" @echo " lint clippy + rustfmt + dependency, docs, and example checks" @echo " lint-clippy clippy (all feature sets) + rustfmt + dep budget + machete" @echo " setup-lint-toolchains refresh stable Clippy and nightly rustfmt to match CI" @echo " lint-xtask xtask meta lints (docs, registries, coverage) + FIPS dep guard" @echo " lint-license-headers reject incorrect MIT SPDX headers in Rust sources" @echo " fmt format with nightly rustfmt" @echo " doc rustdoc with warnings" @echo " audit cargo audit + cargo deny" @echo "" @echo "Container:" @echo " container build praxis-ai container image" @echo " container-run run container in foreground (host network)" @echo "" @echo "FIPS (feature set: $(FIPS_FEATURES), see docs/developing/fips.md):" @echo " build-fips FIPS build, debug profile, into target/fips" @echo " release-fips FIPS build, release profile, with the cargo-auditable crate manifest" @echo " check-fips cargo check of the FIPS build" @echo " lint-fips clippy (all targets) + rustfmt check for the FIPS feature set" @echo " test-fips unit tests resolved as the FIPS build (no defaults, FIPS_FEATURES on the binary)" @echo " test-fips-provider the same unit tests with the RHEL FIPS provider active and asserted in each test process (needs fips.so)" @echo " container-fips FIPS runtime image on UBI 9 (Red Hat toolchain, signature-verified bases)" @echo " container-fips-run run the FIPS image in foreground (host network)" @echo " fips-check build on UBI 9 and print the compliance report (fails while findings remain)" @echo " fips-report compliance report against the local FIPS build (FIPS_BIN=target/fips/release/praxis-ai)" @echo " fips-smoke run the FIPS image once to validate its config" @echo " fips-scan run Red Hat's scanner (check-payload) on the FIPS image, warnings fatal" @echo " fips-scanner build check-payload at the pinned revision into target/fips (needs go)" @echo " fips-oc download the pinned OpenShift CLI (oc) the scanner insists on, checksum-verified" @echo " fips-deps dependency graph vs Red Hat's crypto denylist (seconds, no build)" @echo " fips-verify-image verify the pinned UBI 9 base images are Red Hat's (digest + signature)" @echo " fips-signature-store point podman at Red Hat's signature store (once, on Debian/Ubuntu hosts)" @echo "" @echo "FIPS host (RHEL 9 in FIPS mode; see docs/fips.md):" @echo " test-integration-fips the integration suite as the FIPS build (in-process proxy, FIPS binary for subprocess tests)" @echo " test-schema-fips the schema suite as the FIPS build" @echo " fips-toolchain build the UBI 9 toolchain image the host run uses" @echo " test-fips-host the suites as the FIPS build inside the toolchain image, fail-closed on FIPS mode" @echo " fips-host-facts print the container's FIPS facts; fails unless FIPS mode holds when declared" @echo " fips-host-check attest the host and the image's module build (target/fips/host-attestation.*)" @echo " fips-runtime-probe run the FIPS image under PRAXIS_REQUIRE_FIPS=1 and probe its listener" @echo " fips-image-save save the FIPS image and its id for handoff to the runner" @echo " fips-image-load load a saved image and check it is the one that was saved" @echo " fips-image-tag name a pulled digest the way the FIPS targets expect (FIPS_IMAGE_SOURCE=...)" @echo " fips-version the version the FIPS image is tagged with" @echo "" @echo "Praxis override:" @echo " patch-praxis use ../praxis path deps instead of crates.io" @echo " unpatch-praxis restore the pinned Praxis git dependencies"