Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
880 lines (792 loc) · 48.1 KB
/
Copy pathMakefile
File metadata and controls
880 lines (792 loc) · 48.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
# -------------------------------------------------------------------
# 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)
OPENAI_CONFORMANCE_ARGS ?=
RESPONSES_CONFORMANCE_ARGS ?=
V ?=
# 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
INTEGRATION_EXPERIMENTAL_FEATURES := azure-ad-filter,basic-auth-filter,gcp-adc-filter,http-callout-filter,token-rate-limit-filter,token-ceiling-filter
# Features for `make release`. The published container image builds
# `full,store-sqlite` so it can also serve the SQLite-backed examples; `make
# release` stays on `full` (PostgreSQL only), which is the production backend.
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_SQLITE_FEATURES := standard,openai-all,store-sqlite
STORE_COMBINED_FEATURES := standard,openai-all,store-all
STORE_ALL_WORKSPACE_FEATURES := 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-inference-fixtures \
test-store-features \
test-postgres-unit test-postgres-integration test-environment \
test-token-rate-limit-valkey-unit test-token-rate-limit-valkey-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 \
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 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 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 $(_NOCAPTURE)
cargo test -p praxis-ai-build-support $(_NOCAPTURE)
test-unit-filters:
cargo test -p praxis-ai-filters $(_NOCAPTURE)
cargo test -p praxis-ai-filters --features full $(_NOCAPTURE)
cargo test -p praxis-ai-filters --features full,$(FILTER_EXPERIMENTAL_FEATURES) $(_NOCAPTURE)
test-unit-proxy:
cargo test -p praxis-ai-proxy $(_NOCAPTURE)
cargo test -p praxis-ai-proxy --features full $(_NOCAPTURE)
cargo test -p praxis-ai-proxy --features full,basic-auth-filter $(_NOCAPTURE)
test-store-features:
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_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}')"; \
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-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-postgres native-tls; do assert_has "$$default" "$$crate" default; done; \
for crate in sqlx-sqlite libsqlite3-sys; do assert_lacks "$$default" "$$crate" default; done
@# Lint each opt-in group on its own so a gate leak in a partial feature set
@# cannot hide behind the lean and full builds that other targets cover.
@for group in openai-responses openai-file-resolve-filter store store-postgres store-sqlite \
openai-conversations openai-compact openai-mcp-tools; do \
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 || exit 1; \
done
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 full --edges features -i sqlx-core | grep -q '_tls-native-tls' || \
(echo "ERROR: full proxy SQLx graph does not enable native TLS"; exit 1)
@if cargo tree -p praxis-ai-proxy --features full --edges features -i sqlx-core | grep -q '_tls-rustls'; then \
echo "ERROR: full proxy SQLx graph contains a rustls TLS backend"; \
exit 1; \
fi
test-feature-isolation:
@for group in 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; do \
echo "check: $$group"; \
cargo check -p praxis-ai-proxy --features $$group || exit 1; \
done
test-schema:
cargo test -p praxis-tests-schema --features 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:
cargo build -p praxis-ai-proxy --bin praxis-ai
PRAXIS_AI_BIN=$(abspath target/debug/praxis-ai) \
cargo test -p praxis-tests-integration --features store-all $(_NOCAPTURE)
cargo build -p praxis-ai-proxy --bin praxis-ai --features $(INTEGRATION_EXPERIMENTAL_FEATURES)
PRAXIS_AI_BIN=$(abspath target/debug/praxis-ai) \
cargo test -p praxis-tests-integration --features store-all,$(INTEGRATION_EXPERIMENTAL_FEATURES) --test suite \
-- examples::azure_ad examples::gcp_adc examples::lakera_guard examples::token_rate_limit examples::token_ceiling \
$(if $(V),--nocapture)
test-inference-fixtures:
cargo test -p praxis-test-utils --features store-all $(_NOCAPTURE)
cargo test -p xtask --features store-all inference_fixtures $(_NOCAPTURE)
cargo test -p praxis-tests-integration --features store-all --test suite inference_fixtures $(_NOCAPTURE)
# Both filters are needed. libtest matches each as a substring, and
# `store::tests::pg_` is not a substring of
# `store::tests::postgres_passes_shared_ownership_contract` (the character
# after `pg` is `o`), so that test was never selected by this target and
# never ran in CI. The file uses both naming conventions, so list both
# prefixes; libtest ORs every filter that follows `--`, same as the
# integration target below. When adding a PostgreSQL test, check it against
# this list, because a missed prefix drops it silently.
test-postgres-unit:
cargo test -p praxis-ai-apis --no-default-features --features store-all -- --ignored \
store::tests::pg_ store::tests::postgres_ $(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_response_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_response_store_postgres \
openai_response_store_postgres_mtls \
openai_conversations_postgres_mtls $(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-integration --features llmd-ext-proc llmd_ext_proc $(_NOCAPTURE)
cargo test -p praxis-tests-environment --features 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:
cargo clippy --workspace --all-targets -- -D warnings
cargo clippy --workspace --all-targets \
--features 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-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 \
-- -D warnings
$(MAKE) lint-lean
$(MAKE) check-dep-budget
cargo +nightly fmt --all -- --check
cargo machete --with-metadata .
# 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:
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 openresponses-coverage
# 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:
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" 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="$$(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="$$(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:"; \
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:
cargo +nightly 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
# -------------------------------------------------------------------
#
# The published image (`full,store-sqlite`) enables every non-experimental
# filter. The FIPS build turns off what is known not to be FIPS 140-3 compliant, so
# nobody has to know which features to pick:
#
# policy-engine praxis-policy carries its own cryptography (sha2,
# hmac, jsonwebtoken on aws-lc-rs)
# store, store-sqlite, store-postgres, openai-conversations, openai-compact
# sqlx enables sqlx-core's `migrate` feature with its
# tokio runtime, and that pulls sha2; store-postgres
# adds sqlx-postgres' md-5/hmac/sha2/rsa (SCRAM)
# openai-mcp-tools depends on store, which pulls sha2 through sqlx
# azure-ad-filter, gcp-adc-filter
# experimental; off for the same reasons they are off
# in the standard build
#
# The remaining non-experimental groups compile cleanly: openai-responses
# (the Responses kernel, no crates), openai-file-resolve-filter (migrated
# to SubRequestClient, no reqwest/aws-lc-rs), and aws-sigv4-filter (signs
# through system OpenSSL). FIPS_FEATURES is the single place this is
# defined;
# Containerfile.fips (CARGO_FEATURES) mirrors it and must be kept in sync.
#
# 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 := openai-responses,openai-file-resolve-filter,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 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 check the lean and store builds and lint each feature group alone"
@echo " test-schema schema validation tests"
@echo " test-integration integration tests"
@echo " test-inference-fixtures inference fixture and replay tests"
@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-environment llm-d ext_proc environment tests"
@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 " lint-xtask xtask meta lints (docs, registries, coverage) + FIPS dep guard"
@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"